Multi-Model Indexing¶
Multi-model indexing creates variables indexed by tuples of multiple data models using cartesian products. This is LumiX’s most powerful feature for complex scheduling, routing, and allocation problems.
Concept¶
A multi-model indexed variable represents a family of solver variables, one for each valid combination of instances from multiple models:
from typing import Tuple
duty = LXVariable[Tuple[Driver, Date], int]("duty").indexed_by_product(...)
# Expands to:
# duty[(driver1, date1)], duty[(driver1, date2)], ...
# duty[(driver2, date1)], duty[(driver2, date2)], ...
Basic Usage¶
Two-Dimensional Indexing¶
from typing import Tuple
from lumix import LXVariable, LXIndexDimension
@dataclass
class Driver:
id: str
name: str
daily_rate: float
@dataclass
class Date:
date: datetime.date
min_drivers: int
# Define variable indexed by (Driver, Date)
duty = (
LXVariable[Tuple[Driver, Date], int]("duty")
.binary()
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates)
)
.cost_multi(lambda driver, date: driver.daily_rate)
)
Three-Dimensional Indexing¶
from typing import Tuple
@dataclass
class Shift:
id: str
start_time: str
multiplier: float
# Define variable indexed by (Driver, Date, Shift)
schedule = (
LXVariable[Tuple[Driver, Date, Shift], int]("schedule")
.binary()
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates),
LXIndexDimension(Shift, lambda s: s.id).from_data(shifts)
)
.cost_multi(lambda driver, date, shift:
driver.daily_rate * shift.multiplier
)
)
Cartesian Products¶
Creating Products¶
from lumix import LXCartesianProduct, LXIndexDimension
# Method 1: Via variable definition
duty = (
LXVariable[Tuple[Driver, Date], int]("duty")
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates)
)
)
# Method 2: Explicit cartesian product
product = LXCartesianProduct(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates)
)
duty = (
LXVariable[Tuple[Driver, Date], int]("duty")
.binary()
.from_data(product)
)
Cross-Dimension Filtering¶
Filter combinations based on relationships between models:
duty = (
LXVariable[Tuple[Driver, Date], int]("duty")
.binary()
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates)
)
.where_multi(lambda driver, date:
# Only create variables for valid combinations
date.weekday() not in driver.days_off and
driver.is_available_on(date)
)
)
Using Multi-Indexed Variables¶
In Objective Functions¶
from lumix import LXLinearExpression
# Cost function with both models
cost_expr = (
LXLinearExpression()
.add_multi_term(
duty,
coeff=lambda driver, date: driver.daily_rate * date.overtime_mult
)
)
model.minimize(cost_expr)
In Constraints¶
Summing Over One Dimension:
# Each driver works at most 5 days
for driver in drivers:
model.add_constraint(
LXConstraint(f"max_days_{driver.id}")
.expression(
LXLinearExpression()
.add_multi_term(
duty,
coeff=lambda d, dt: 1.0,
where=lambda d, dt: d.id == driver.id # Fix driver dimension
)
)
.le()
.rhs(5.0)
)
# Each date needs at least 3 drivers
for date in dates:
model.add_constraint(
LXConstraint(f"coverage_{date.date}")
.expression(
LXLinearExpression()
.add_multi_term(
duty,
coeff=lambda d, dt: 1.0,
where=lambda d, dt: dt.date == date.date # Fix date dimension
)
)
.ge()
.rhs(float(date.min_drivers))
)
Complete Example: Driver Scheduling¶
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Tuple
from lumix import (
LXModel,
LXVariable,
LXConstraint,
LXLinearExpression,
LXIndexDimension,
LXOptimizer,
)
@dataclass
class Driver:
id: str
name: str
daily_rate: float
max_days: int
days_off: list[int] # Weekdays (0=Monday)
is_active: bool
@dataclass
class Date:
date: date
min_drivers: int
overtime_multiplier: float
# Sample data
drivers = [
Driver("D1", "Alice", 200, 5, [5, 6], True),
Driver("D2", "Bob", 180, 6, [6], True),
Driver("D3", "Carol", 220, 4, [0, 6], True),
]
start_date = date(2024, 1, 1)
dates = [
Date(start_date + timedelta(days=i), 2, 1.5 if (start_date + timedelta(days=i)).weekday() >= 5 else 1.0)
for i in range(7)
]
# Helper function
def is_available(driver: Driver, dt: Date) -> bool:
return dt.date.weekday() not in driver.days_off
# Define multi-indexed variable
duty = (
LXVariable[Tuple[Driver, Date], int]("duty")
.binary()
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id)
.where(lambda d: d.is_active)
.from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date)
.from_data(dates)
)
.cost_multi(lambda driver, date: driver.daily_rate * date.overtime_multiplier)
.where_multi(lambda driver, date: is_available(driver, date))
)
# Build model
model = (
LXModel("driver_scheduling")
.add_variable(duty)
.minimize(
LXLinearExpression()
.add_multi_term(duty, lambda d, dt: d.daily_rate * dt.overtime_multiplier)
)
)
# Constraint: Each driver works at most max_days
for driver in drivers:
if not driver.is_active:
continue
model.add_constraint(
LXConstraint(f"max_days_{driver.id}")
.expression(
LXLinearExpression()
.add_multi_term(
duty,
coeff=lambda d, dt: 1.0,
where=lambda d, dt: d.id == driver.id
)
)
.le()
.rhs(float(driver.max_days))
)
# Constraint: Each date needs minimum drivers
for dt in dates:
model.add_constraint(
LXConstraint(f"coverage_{dt.date}")
.expression(
LXLinearExpression()
.add_multi_term(
duty,
coeff=lambda d, date: 1.0,
where=lambda d, date: date.date == dt.date
)
)
.ge()
.rhs(float(dt.min_drivers))
)
# Solve
optimizer = LXOptimizer().use_solver("ortools")
solution = optimizer.solve(model)
# Display results
if solution.is_optimal():
print(f"Optimal cost: ${solution.objective_value:,.2f}")
for dt in dates:
print(f"\n{dt.date.strftime('%A %Y-%m-%d')}:")
for driver in drivers:
if not driver.is_active or not is_available(driver, dt):
continue
value = solution.variables["duty"].get((driver.id, dt.date), 0)
if value > 0.5:
cost = driver.daily_rate * dt.overtime_multiplier
print(f" {driver.name}: ${cost:.2f}")
Common Use Cases¶
Assignment Problems¶
# Worker × Task assignment
assignment = (
LXVariable[Tuple[Worker, Task], int]("assignment")
.binary()
.indexed_by_product(
LXIndexDimension(Worker, lambda w: w.id).from_data(workers),
LXIndexDimension(Task, lambda t: t.id).from_data(tasks)
)
.where_multi(lambda w, t: t.required_skill in w.skills)
)
Transportation Problems¶
# Origin × Destination shipments
shipment = (
LXVariable[Tuple[Warehouse, Customer], float]("shipment")
.continuous()
.bounds(lower=0)
.indexed_by_product(
LXIndexDimension(Warehouse, lambda w: w.id).from_data(warehouses),
LXIndexDimension(Customer, lambda c: c.id).from_data(customers)
)
.cost_multi(lambda w, c: calculate_shipping_cost(w, c))
.where_multi(lambda w, c: w.can_serve_region(c.region))
)
Resource Allocation¶
# Project × Resource × TimePeriod allocation
allocation = (
LXVariable[Tuple[Project, Resource, Period], float]("allocation")
.continuous()
.bounds(lower=0)
.indexed_by_product(
LXIndexDimension(Project, lambda p: p.id).from_data(projects),
LXIndexDimension(Resource, lambda r: r.id).from_data(resources),
LXIndexDimension(Period, lambda t: t.id).from_data(periods)
)
.where_multi(lambda p, r, t:
p.start_period <= t.id <= p.end_period and
r.type in p.required_resource_types
)
)
Best Practices¶
Use type annotations for tuples:
# Good: Explicit tuple type duty = LXVariable[Tuple[Driver, Date], int]("duty") # Bad: No type information duty = LXVariable("duty")
Filter at dimension level first:
# Good: Reduce data before cartesian product LXIndexDimension(Driver, lambda d: d.id).where(lambda d: d.is_active) # Then apply cross-dimension filters .where_multi(lambda d, dt: ...)
Be mindful of combinatorial explosion:
# 10 × 10 = 100 variables (fine) # 100 × 100 = 10,000 variables (fine) # 1000 × 1000 = 1,000,000 variables (problematic without filtering)
Use sparse indexing:
# Only create variables where needed .where_multi(lambda d, dt: valid_combination(d, dt))
Next Steps¶
Index Dimensions - Deep dive into index dimensions
Filtering Strategies - Advanced filtering strategies
Examples - See driver scheduling example
Expressions Guide - Using multi-indexed variables in expressions