Solution Mapping¶
Learn how to map solution values back to your data models and ORM instances.
Overview¶
The LXSolutionMapper class provides utilities to map
solution values from solver indices back to your original data model instances.
Why Mapping Matters:
Solver uses internal indices (0, 1, 2, …)
Your data has meaningful keys (product IDs, dates, etc.)
ORM instances need to be correlated with solution values
Business logic requires working with actual model objects
The Mapping Problem¶
Understanding the Gap¶
When you define a variable family:
@dataclass
class Product:
id: str
name: str
cost: float
products = [
Product(id="A", name="Widget", cost=10.0),
Product(id="B", name="Gadget", cost=15.0),
]
production = (
LXVariable[Product, float]("production")
.indexed_by(lambda p: p.id)
.from_data(products)
)
The solver creates internal variables, but you need to map results back:
# Solver stores by keys
solution.mapped["production"] = {"A": 10.0, "B": 20.0}
# But you need Product instances
for product in products:
quantity = ??? # How to get the value for this product?
Automatic Mapping in LXSolution¶
Built-in Mapped Access¶
LXSolution provides automatic mapping via the
mapped attribute and get_mapped() method:
# Automatic mapping by LumiX
for product_id, quantity in solution.get_mapped(production).items():
print(f"Product {product_id}: {quantity} units")
This works because:
Variable definition includes
indexed_by(lambda p: p.id)LumiX stores the mapping from keys to values
get_mapped()returns values indexed by those keys
Using LXSolutionMapper¶
For Advanced Mapping¶
The LXSolutionMapper class provides lower-level
mapping utilities for advanced scenarios:
from lumix.solution import LXSolutionMapper
mapper = LXSolutionMapper[Product]()
# Map solution values to model instances
instance_values = mapper.map_variable_to_models(
var=production,
solution_values=solution.mapped["production"],
model_instances=products
)
# Result: {Product(id="A"): 10.0, Product(id="B"): 20.0}
for product, quantity in instance_values.items():
print(f"{product.name}: {quantity} units")
Single-Indexed Variables¶
Mapping by ID¶
@dataclass
class Product:
id: str
name: str
products = [
Product(id="A", name="Widget"),
Product(id="B", name="Gadget"),
]
production = (
LXVariable[Product, float]("production")
.indexed_by(lambda p: p.id)
.from_data(products)
)
solution = optimizer.solve(model)
# Method 1: Use get_mapped() (recommended)
for product_id, qty in solution.get_mapped(production).items():
# Look up product by ID
product = next(p for p in products if p.id == product_id)
print(f"{product.name}: {qty}")
# Method 2: Use LXSolutionMapper
mapper = LXSolutionMapper[Product]()
instance_map = mapper.map_variable_to_models(
var=production,
solution_values=solution.mapped["production"],
model_instances=products
)
for product, qty in instance_map.items():
print(f"{product.name}: {qty}")
Mapping with Filtering¶
# Create lookup dictionary for efficient access
products_by_id = {p.id: p for p in products}
# Map and process
for product_id, quantity in solution.get_mapped(production).items():
product = products_by_id[product_id]
if quantity > 0.01: # Filter near-zero
print(f"Produce {quantity:.2f} units of {product.name}")
Multi-Indexed Variables¶
Mapping Cartesian Products¶
For variables indexed by multiple dimensions:
@dataclass
class Driver:
id: int
name: str
@dataclass
class Date:
date: str
drivers = [Driver(id=1, name="Alice"), Driver(id=2, name="Bob")]
dates = [Date(date="2024-01-01"), Date(date="2024-01-02")]
assignment = (
LXVariable[Tuple[Driver, Date], int]("assignment")
.binary()
.indexed_by_product(
LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
LXIndexDimension(Date, lambda dt: dt.date).from_data(dates),
)
)
solution = optimizer.solve(model)
# Method 1: Use get_mapped() for key tuples
for (driver_id, date_str), assigned in solution.get_mapped(assignment).items():
if assigned > 0.5:
print(f"Driver {driver_id} assigned on {date_str}")
# Method 2: Use LXSolutionMapper for instance tuples
mapper = LXSolutionMapper()
instance_map = mapper.map_multi_indexed_variable(
var=assignment,
solution_values=solution.mapped["assignment"]
)
# Result: {(Driver(id=1), Date(date="2024-01-01")): 1, ...}
for (driver, date), assigned in instance_map.items():
if assigned > 0.5:
print(f"{driver.name} assigned on {date.date}")
Processing Multi-Indexed Results¶
def process_assignment_solution(solution, assignment_var, drivers, dates):
"""Process multi-indexed assignment solution."""
# Create lookup dictionaries
drivers_by_id = {d.id: d for d in drivers}
dates_by_str = {dt.date: dt for dt in dates}
# Get mapped values (key tuples)
assignments = solution.get_mapped(assignment_var)
# Process assignments
schedule = {} # driver.name -> [dates]
for (driver_id, date_str), assigned in assignments.items():
if assigned > 0.5: # Binary variable threshold
driver = drivers_by_id[driver_id]
date = dates_by_str[date_str]
if driver.name not in schedule:
schedule[driver.name] = []
schedule[driver.name].append(date.date)
# Print schedule
for driver_name, assigned_dates in schedule.items():
print(f"{driver_name}: {', '.join(sorted(assigned_dates))}")
return schedule
ORM Integration¶
Mapping to Database Records¶
When using ORM models:
from sqlalchemy.orm import Session
from lumix.utils import LXORMContext
# Define ORM model
class Product(Base):
__tablename__ = "products"
id = Column(String, primary_key=True)
name = Column(String)
cost = Column(Float)
# Create variable from ORM
production = (
LXVariable[Product, float]("production")
.indexed_by(lambda p: p.id)
.from_model(Product, session=session)
)
solution = optimizer.solve(model)
# Map back to ORM instances
products = session.query(Product).all()
products_by_id = {p.id: p for p in products}
for product_id, quantity in solution.get_mapped(production).items():
product = products_by_id[product_id]
print(f"{product.name}: {quantity}")
# Update database
product.planned_production = quantity
session.commit()
Bulk Updates¶
def update_production_plan(session, solution, production_var):
"""Update production plan in database."""
from sqlalchemy import update
# Get all planned quantities
planned = solution.get_mapped(production_var)
# Bulk update
for product_id, quantity in planned.items():
stmt = (
update(Product)
.where(Product.id == product_id)
.values(planned_production=quantity)
)
session.execute(stmt)
session.commit()
print(f"Updated {len(planned)} product production plans")
Custom Mapping Logic¶
Complex Key Functions¶
For complex indexing:
@dataclass
class Route:
origin: str
destination: str
distance: float
routes = [
Route("NYC", "LA", 2800),
Route("NYC", "CHI", 800),
]
# Index by tuple
shipment = (
LXVariable[Route, float]("shipment")
.indexed_by(lambda r: (r.origin, r.destination))
.from_data(routes)
)
solution = optimizer.solve(model)
# Create reverse lookup
routes_by_key = {
(r.origin, r.destination): r for r in routes
}
# Map solution to routes
for (origin, dest), quantity in solution.get_mapped(shipment).items():
route = routes_by_key[(origin, dest)]
print(f"{route.origin} → {route.destination}: {quantity} units")
Hierarchical Mapping¶
@dataclass
class Product:
id: str
category: str
name: str
def map_by_category(solution, production_var, products):
"""Group production by category."""
from collections import defaultdict
by_category = defaultdict(list)
products_by_id = {p.id: p for p in products}
for product_id, quantity in solution.get_mapped(production_var).items():
product = products_by_id[product_id]
by_category[product.category].append({
'product': product,
'quantity': quantity
})
# Report by category
for category, items in by_category.items():
total = sum(item['quantity'] for item in items)
print(f"\\n{category} (Total: {total:.2f})")
for item in sorted(items, key=lambda x: -x['quantity']):
print(f" {item['product'].name}: {item['quantity']:.2f}")
Handling Missing Mappings¶
Defensive Mapping¶
def safe_map_to_instances(solution_values, model_instances, key_func):
"""Safely map values to instances with error handling."""
# Build reverse mapping
instance_by_key = {key_func(inst): inst for inst in model_instances}
mapped_values = {}
missing_keys = []
for key, value in solution_values.items():
if key in instance_by_key:
instance = instance_by_key[key]
mapped_values[instance] = value
else:
missing_keys.append(key)
if missing_keys:
print(f"Warning: {len(missing_keys)} keys not found in model instances")
print(f"Missing keys: {missing_keys[:5]}...") # Show first 5
return mapped_values
Handling Data Sync Issues¶
def sync_check(solution, production_var, current_products):
"""Check if solution data matches current data."""
solution_keys = set(solution.get_mapped(production_var).keys())
current_keys = {p.id for p in current_products}
missing_in_solution = current_keys - solution_keys
missing_in_current = solution_keys - current_keys
if missing_in_solution:
print(f"Warning: {len(missing_in_solution)} products not in solution")
if missing_in_current:
print(f"Warning: {len(missing_in_current)} solution keys not in current data")
print("This may indicate data changed since model was built")
Best Practices¶
Use get_mapped() for Most Cases
# Recommended: Use built-in mapping for key, value in solution.get_mapped(production).items(): # Work with keys directly pass # Advanced: Only use LXSolutionMapper for special cases mapper = LXSolutionMapper() # ... custom mapping logic
Create Lookup Dictionaries
# Good: Efficient lookup products_by_id = {p.id: p for p in products} for product_id, qty in solution.get_mapped(production).items(): product = products_by_id[product_id] # O(1) lookup # Bad: Linear search for product_id, qty in solution.get_mapped(production).items(): product = next(p for p in products if p.id == product_id) # O(n) lookup
Handle Missing Mappings Gracefully
products_by_id = {p.id: p for p in products} for product_id, qty in solution.get_mapped(production).items(): product = products_by_id.get(product_id) if product: print(f"{product.name}: {qty}") else: print(f"Warning: Product {product_id} not found")
Validate Key Consistency
# Ensure keys match between model building and solution processing original_keys = {production.index_func(p) for p in products} solution_keys = set(solution.get_mapped(production).keys()) assert original_keys == solution_keys, "Key mismatch!"
Common Patterns¶
Report Generation¶
def generate_production_report(solution, production_var, products):
"""Generate detailed production report."""
products_by_id = {p.id: p for p in products}
production_values = solution.get_mapped(production_var)
# Calculate totals
total_quantity = sum(production_values.values())
total_cost = sum(
qty * products_by_id[pid].cost
for pid, qty in production_values.items()
)
# Generate report
print("PRODUCTION REPORT")
print("=" * 80)
print(f"Total Quantity: {total_quantity:,.2f}")
print(f"Total Cost: ${total_cost:,.2f}")
print()
print(f"{'Product':<30} {'Quantity':<15} {'Unit Cost':<12} {'Total Cost'}")
print("-" * 80)
for product_id, quantity in sorted(production_values.items()):
product = products_by_id[product_id]
total = quantity * product.cost
print(f"{product.name:<30} {quantity:<15,.2f} ${product.cost:<11.2f} ${total:,.2f}")
Database Synchronization¶
def sync_solution_to_database(session, solution, production_var):
"""Synchronize solution values to database."""
# Get solution values
planned_production = solution.get_mapped(production_var)
# Query current products
products = session.query(Product).all()
products_by_id = {p.id: p for p in products}
# Update with solution values
updated_count = 0
for product_id, quantity in planned_production.items():
product = products_by_id.get(product_id)
if product:
product.planned_quantity = quantity
updated_count += 1
else:
print(f"Warning: Product {product_id} not found in database")
session.commit()
print(f"Updated {updated_count} product records")
Next Steps¶
Accessing Solutions - Learn about accessing solution values
Sensitivity Analysis - Perform sensitivity analysis
Goal Programming Solutions - Work with goal programming solutions
Solution Module API - Full API reference