Basic Linear Programming Example¶
Overview¶
This is the SIMPLEST LumiX example - perfect for learning the absolute basics of data-driven optimization modeling. If you’re new to LumiX, start here!
This example demonstrates the classic Diet Problem: finding the minimum-cost combination of foods that meets nutritional requirements. It’s one of the first practical applications of linear programming, solved by George Stigler in 1945.
Problem Description¶
A person wants to plan their daily diet to minimize food costs while meeting minimum nutritional requirements.
Objective: Minimize total food cost.
Nutritional Requirements:
Calories (energy): At least 2000 per day
Protein (muscle building): At least 50g per day
Calcium (bone health): At least 800mg per day
Available Foods: Each food has cost per serving and nutritional content.
Decision: How many servings of each food to consume?
Mathematical Formulation¶
Decision Variables:
where \(x_f\) represents the number of servings of food \(f\).
Objective Function:
Constraints:
Minimum Calories:
\[\sum_{f \in \text{Foods}} \text{calories}_f \cdot x_f \geq 2000\]Minimum Protein:
\[\sum_{f \in \text{Foods}} \text{protein}_f \cdot x_f \geq 50\]Minimum Calcium:
\[\sum_{f \in \text{Foods}} \text{calcium}_f \cdot x_f \geq 800\]
Key Features¶
Variable Families from Data¶
ONE variable declaration expands to multiple solver variables:
servings = (
LXVariable[Food, float]("servings")
.continuous()
.bounds(lower=0)
.indexed_by(lambda f: f.name)
.from_data(FOODS) # Data-driven: auto-expands to one var per food
)
Key Points:
LXVariable[Food, float]creates a variable family typed by Food.continuous()specifies continuous (non-integer) variables.bounds(lower=0)sets non-negativity constraint.indexed_by(lambda f: f.name)specifies how to index each variable.from_data(FOODS)auto-creates one variable per food item
Data-Driven Coefficients¶
Coefficients extracted from data using lambda functions:
cost_expr = LXLinearExpression().add_term(
servings,
coeff=lambda f: f.cost_per_serving
)
The lambda function lambda f: f.cost_per_serving extracts the cost attribute from each Food instance.
Automatic Expression Expansion¶
Expressions automatically sum over all indexed data:
model.add_constraint(
LXConstraint("min_calories")
.expression(
LXLinearExpression().add_term(servings, lambda f: f.calories)
)
.ge()
.rhs(MIN_CALORIES)
)
This constraint expands to: \(\sum_f \text{calories}_f \cdot x_f \geq 2000\) automatically.
Type-Safe Solution Access¶
Access solutions using the index keys:
# Get mapped solution (dictionary indexed by food name)
solution_dict = solution.get_mapped(servings)
for food_name, qty in solution_dict.items():
if qty > 0.01: # Only non-zero servings
food = food_by_name[food_name]
cost = food.cost_per_serving * qty
print(f"{food.name}: {qty:.2f} servings (${cost:.2f})")
Running the Example¶
Prerequisites:
pip install lumix[ortools] # or cplex, gurobi, glpk
Run:
cd examples/04_basic_lp
python basic_lp.py
Expected Output:
============================================================
LumiX Example: Basic Diet Problem
============================================================
Building optimization model...
Model Summary:
Variables: 1 family (6 decision variables)
Constraints: 3 (calories, protein, calcium)
Objective: Minimize total cost
Solving...
============================================================
SOLUTION
============================================================
Status: optimal
Optimal Cost: $3.15
Solve Time: 0.042s
Optimal Diet Plan:
------------------------------------------------------------
Oatmeal : 4.00 servings (cost: $1.20)
Eggs : 2.50 servings (cost: $1.25)
Milk : 1.25 servings (cost: $0.75)
Nutritional Totals:
------------------------------------------------------------
Total Cost: $3.15
Calories: 2000.0 (min: 2000)
Protein: 51.5g (min: 50g)
Calcium: 811.3mg (min: 800mg)
Complete Code Walkthrough¶
Step 1: Define Data Model¶
@dataclass
class Food:
"""Represents a food item with nutritional information."""
name: str
cost_per_serving: float # $ per serving
calories: float # calories per serving
protein: float # grams per serving
calcium: float # mg per serving
FOODS = [
Food("Oatmeal", 0.30, 110, 4, 2),
Food("Chicken", 2.40, 205, 32, 12),
Food("Eggs", 0.50, 160, 13, 60),
Food("Milk", 0.60, 160, 8, 285),
Food("Apple Pie", 1.60, 420, 4, 22),
Food("Pork", 2.90, 260, 14, 10),
]
Step 2: Create Variable Family¶
servings = (
LXVariable[Food, float]("servings")
.continuous()
.bounds(lower=0)
.indexed_by(lambda f: f.name)
.from_data(FOODS) # Data-driven: auto-expands to one var per food
)
Step 3: Set Objective Function¶
# Objective: Minimize total cost
# Expression automatically sums over all foods
cost_expr = LXLinearExpression().add_term(
servings,
coeff=lambda f: f.cost_per_serving
)
model.minimize(cost_expr) # TODO feels like optimization runs in this line, naming confusion
Step 4: Add Nutritional Constraints¶
# Expression automatically sums calories from all foods
model.add_constraint(
LXConstraint("min_calories")
.expression(
LXLinearExpression().add_term(servings, lambda f: f.calories)
)
.ge()
.rhs(MIN_CALORIES)
)
# Constraint 2: Minimum protein
model.add_constraint(
LXConstraint("min_protein")
.expression(
LXLinearExpression().add_term(servings, lambda f: f.protein)
)
.ge()
.rhs(MIN_PROTEIN)
)
# Constraint 3: Minimum calcium
model.add_constraint(
LXConstraint("min_calcium")
.expression(
LXLinearExpression().add_term(servings, lambda f: f.calcium)
)
.ge()
.rhs(MIN_CALCIUM)
)
Step 5: Solve and Interpret Solution¶
optimizer = LXOptimizer().use_solver("ortools")
solution = optimizer.solve(model)
if solution.is_optimal():
for food_name, qty in solution.get_mapped(servings).items():
print(f"{food_name}: {qty:.2f} servings")
Learning Objectives¶
After completing this example, you should understand:
Variable Families: How one
LXVariableexpands to multiple solver variablesData-Driven Modeling: Using
.from_data()to auto-create variablesLambda Coefficients: Extracting data attributes for model coefficients
Automatic Summation: How expressions expand over all indexed instances
Fluent API: Chaining method calls for readable model building
Solution Access: Using
.get_mapped()for type-safe solution retrieval
Common Patterns¶
Pattern 1: Single-Model Variable Family¶
variable = (
LXVariable[DataModel, VarType]("var_name")
.continuous() # or .integer(), .binary()
.bounds(lower=0, upper=None)
.indexed_by(lambda m: m.key)
.from_data(DATA)
)
Pattern 2: Data-Driven Objective¶
cost_expr = LXLinearExpression().add_term(
variable,
coeff=lambda m: m.cost_attribute
)
model.minimize(cost_expr) # or .maximize()
Pattern 3: Resource Constraints¶
model.add_constraint(
LXConstraint("resource_name")
.expression(
LXLinearExpression().add_term(
variable,
coeff=lambda m: m.resource_usage
)
)
.ge() # or .le(), .eq()
.rhs(MINIMUM_REQUIREMENT)
)
Understanding the Solution¶
Why This Diet?¶
The optimal solution chose:
Oatmeal (4.00 servings): Cheapest source of calories
Eggs (2.50 servings): Good protein-to-cost ratio
Milk (1.25 servings): Excellent calcium source
It avoided:
Chicken, Pork: Too expensive for the nutrition provided
Apple Pie: High cost, low nutritional value relative to needs
Solution Characteristics¶
The solution is at a vertex of the feasible region where exactly 3 constraints are binding (satisfied with equality):
Calories constraint: Exactly 2000 (at minimum)
Protein constraint: ~51.5g (slightly above minimum)
Calcium constraint: ~811mg (slightly above minimum)
This is typical of linear programming: optimal solutions occur at vertices where constraint boundaries intersect.
Linear Programming Basics¶
Key Properties¶
LP problems have several important characteristics:
Linear objective: No products of variables, only weighted sums
Linear constraints: All constraint expressions are linear
Continuous variables: Can take any non-negative value (not just integers)
Convex feasible region: The set of feasible solutions is convex
Vertex optimality: Optimal solution occurs at a vertex
Polynomial-time solvable: Efficiently solved with simplex or interior-point methods
Why Linear Programming?¶
LP is useful when:
Divisibility: Resources can be fractionally allocated (e.g., servings)
Additivity: Total effect equals sum of individual effects
Proportionality: Effect is proportional to amount (doubling input doubles output)
Deterministic: All parameters are known with certainty
Extending the Example¶
Try These Modifications¶
Add Upper Bounds: Limit maximum servings per food
.bounds(lower=0, upper=5) # At most 5 servings
Add Food Groups: Ensure variety from different food groups
# At least 2 servings from dairy dairy_expr = LXLinearExpression().add_term( servings, coeff=lambda f: 1.0 if f.group == "dairy" else 0.0 ) model.add_constraint( LXConstraint("min_dairy").expression(dairy_expr).ge().rhs(2.0) )
Add Maximum Constraints: Limit sugar, fat, sodium
model.add_constraint( LXConstraint("max_sugar") .expression(sugar_expr) .le() .rhs(MAX_SUGAR) )
Multiple Nutrients: Add vitamins, minerals, fiber
Binary Choices: Include or exclude foods entirely (convert to MIP)
Multi-Day Planning: Add time dimension for weekly meal planning
Comparison with Traditional Libraries¶
PuLP / Pyomo Approach¶
# Traditional approach - manual dictionary creation
servings = {f.name: LpVariable(f"servings_{f.name}", lowBound=0)
for f in FOODS}
cost = lpSum([f.cost_per_serving * servings[f.name] for f in FOODS])
LumiX Approach¶
# LumiX approach - automatic from data
servings = (
LXVariable[Food, float]("servings")
.continuous()
.from_data(FOODS)
)
cost = LXLinearExpression().add_term(
servings,
coeff=lambda f: f.cost_per_serving
)
Advantages:
Type-safe:
servingsis typed asLXVariable[Food, float]Data-driven: Automatically expands from
FOODSNo manual dictionaries
IDE autocomplete and type checking
Cleaner, more maintainable code
Next Steps¶
After mastering this example:
Example 01 (Production Planning): More complex single-model indexing
Example 02 (Driver Scheduling): Multi-model indexing (THE KEY FEATURE)
Example 03 (Facility Location): Mixed-integer programming
Example 09 (Sensitivity Analysis): Understanding shadow prices
See Also¶
Related Examples:
Production Planning Example - More complex single-model indexing
Driver Scheduling Example - Multi-model indexing introduction
Facility Location Example - Mixed-integer programming with binary variables
API Reference:
User Guide:
Quick Start Guide - Getting started guide
Variables Guide - Variable types and families
Constraints Guide - Constraint modeling
Files in This Example¶
basic_lp.py- Main optimization model and solution displayREADME.md- Detailed documentation and usage guide