Using the Optimizer¶
The LXOptimizer class is the main interface for solving optimization models in LumiX.
Overview¶
The optimizer provides a fluent API for:
Selecting solvers
Configuring solver options
Enabling advanced features
Solving models
Accessing solutions
Basic Usage¶
Simple Solve¶
The simplest way to solve a model:
from lumix import LXOptimizer
optimizer = LXOptimizer().use_solver("ortools")
solution = optimizer.solve(model)
With Parameters¶
Pass solver-specific parameters:
solution = optimizer.solve(
model,
time_limit=300, # 5 minutes maximum
gap_tolerance=0.01, # 1% MIP gap
)
Fluent Configuration¶
Chain methods for complex configuration:
optimizer = (
LXOptimizer()
.use_solver("gurobi")
.enable_sensitivity()
.enable_rational_conversion()
)
solution = optimizer.solve(model, time_limit=600)
Optimizer Methods¶
use_solver()¶
Select which solver to use:
# Available solvers
optimizer = LXOptimizer().use_solver("ortools") # Default, free
optimizer = LXOptimizer().use_solver("gurobi") # Commercial
optimizer = LXOptimizer().use_solver("cplex") # Commercial
optimizer = LXOptimizer().use_solver("glpk") # Free, basic
optimizer = LXOptimizer().use_solver("cpsat") # CP solver
Signature:
def use_solver(
self,
name: Literal["ortools", "gurobi", "cplex", "cpsat", "glpk"],
**kwargs
) -> Self
Parameters:
name: Solver name (type-checked literal)**kwargs: Solver-specific initialization parameters
enable_rational_conversion()¶
Convert floating-point coefficients to rational numbers:
optimizer = (
LXOptimizer()
.use_solver("cpsat") # CP-SAT requires integers
.enable_rational_conversion(max_denom=10000)
)
Why? Some solvers (like CP-SAT) only work with integer coefficients. This feature automatically converts floats like 0.333 to fractions like 1/3.
Signature:
def enable_rational_conversion(
self,
max_denom: int = 10000
) -> Self
Parameters:
max_denom: Maximum denominator for rational approximation (default: 10000)
Example:
# Model with float coefficients: 0.333 * x
model = build_model_with_floats()
# Automatically converts to: 333/1000 * x
optimizer = LXOptimizer().enable_rational_conversion()
solution = optimizer.solve(model)
enable_linearization()¶
Automatically linearize nonlinear terms:
optimizer = (
LXOptimizer()
.use_solver("ortools") # Doesn't support quadratic
.enable_linearization(
big_m=1e6,
pwl_segments=20,
pwl_method="sos2",
adaptive_breakpoints=True
)
)
Why? Not all solvers support nonlinear terms (bilinear products, absolute values, min/max). This feature automatically linearizes them.
Signature:
def enable_linearization(
self,
big_m: float = 1e6,
pwl_segments: int = 20,
pwl_method: Literal["sos2", "incremental", "logarithmic"] = "sos2",
prefer_sos2: bool = True,
adaptive_breakpoints: bool = True,
mccormick_tighten_bounds: bool = True,
**kwargs
) -> Self
Parameters:
big_m: Big-M constant for conditional constraints (default: 1e6)pwl_segments: Number of segments for piecewise-linear approximations (default: 20)pwl_method: Method for PWL (“sos2”, “incremental”, “logarithmic”)prefer_sos2: Use SOS2 when solver supports it (default: True)adaptive_breakpoints: Use adaptive breakpoint generation (default: True)mccormick_tighten_bounds: Apply bound tightening for McCormick envelopes (default: True)
Example:
# Model with x * y bilinear products
model = build_bilinear_model()
optimizer = (
LXOptimizer()
.use_solver("ortools")
.enable_linearization(big_m=1e5, pwl_segments=30)
)
# Automatically linearizes x*y using McCormick envelopes
solution = optimizer.solve(model)
enable_sensitivity()¶
Enable sensitivity analysis (shadow prices, reduced costs):
optimizer = LXOptimizer().enable_sensitivity()
solution = optimizer.solve(model)
# Access sensitivity information
for constraint_name, shadow_price in solution.shadow_prices.items():
print(f"{constraint_name}: {shadow_price}")
Note: Only supported by solvers with sensitivity analysis capability (Gurobi, CPLEX, GLPK).
solve()¶
Solve the optimization model:
solution = optimizer.solve(
model,
time_limit=600,
gap_tolerance=0.01,
**solver_params
)
Signature:
def solve(
self,
model: LXModel,
**solver_params: Any
) -> LXSolution
Common Parameters:
time_limit: Maximum solve time in secondsgap_tolerance: MIP gap tolerance (for integer programs)
Solver-Specific Parameters:
Gurobi:
solution = optimizer.solve(
model,
Threads=4, # Number of threads
MIPFocus=1, # 1=feasibility, 2=optimality, 3=bound
Presolve=2, # -1=auto, 0=off, 1=conservative, 2=aggressive
Method=-1, # -1=auto, 0=primal, 1=dual, 2=barrier
LogToConsole=1, # 0=off, 1=on
)
CPLEX:
solution = optimizer.solve(
model,
threads=4,
mip_emphasis=1, # 0=balanced, 1=feasibility, 2=optimality, 3=bound, 4=hidden
preprocessing_presolve=1, # 0=off, 1=on
)
OR-Tools:
solution = optimizer.solve(
model,
num_search_workers=4, # Parallel workers
log_search_progress=True,
)
Returns:
LXSolution object containing:
status: Solution status (“optimal”, “feasible”, “infeasible”, etc.)objective_value: Optimal objective valuevariable_values: Dictionary of variable valuessolve_time: Time taken to solveshadow_prices: Shadow prices (if sensitivity enabled)reduced_costs: Reduced costs (if sensitivity enabled)
Complete Examples¶
Example 1: Production Planning with Gurobi¶
from lumix import LXModel, LXOptimizer
# Build model
model = build_production_model(products, resources)
# Configure optimizer
optimizer = (
LXOptimizer()
.use_solver("gurobi")
.enable_sensitivity()
)
# Solve with parameters
solution = optimizer.solve(
model,
time_limit=300,
gap_tolerance=0.001, # 0.1% gap
Threads=8,
MIPFocus=2, # Focus on optimality
)
# Check results
if solution.is_optimal():
print(f"Maximum profit: ${solution.objective_value:,.2f}")
# Show production quantities
for product in products:
value = solution.get_value(production, product)
print(f"Produce {value:.0f} units of {product.name}")
# Show shadow prices (resource values)
for resource in resources:
price = solution.get_shadow_price(capacity, resource)
print(f"{resource.name} shadow price: ${price:.2f}")
Example 2: Scheduling with CP-SAT¶
from lumix import LXModel, LXOptimizer
# Build integer scheduling model
model = build_scheduling_model(tasks, workers, days)
# Use CP-SAT (requires integer coefficients)
optimizer = (
LXOptimizer()
.use_solver("cpsat")
.enable_rational_conversion() # Convert floats to rationals
)
# Solve
solution = optimizer.solve(model, time_limit=60)
# Show assignments
for task in tasks:
for worker in workers:
for day in days:
if solution.get_value(assignment, (task, worker, day)) > 0.5:
print(f"{task.name} assigned to {worker.name} on {day}")
Example 3: Nonlinear Model with Linearization¶
from lumix import LXModel, LXOptimizer
# Model with bilinear terms (price * quantity)
model = build_pricing_model(products)
# Use OR-Tools with automatic linearization
optimizer = (
LXOptimizer()
.use_solver("ortools")
.enable_linearization(
big_m=1e6,
pwl_segments=30,
mccormick_tighten_bounds=True
)
)
# Solve (automatically linearizes bilinear terms)
solution = optimizer.solve(model)
# Results
for product in products:
price = solution.get_value(price_var, product)
quantity = solution.get_value(quantity_var, product)
revenue = price * quantity
print(f"{product.name}: price=${price:.2f}, qty={quantity:.0f}, revenue=${revenue:,.2f}")
Best Practices¶
Solver Selection
Start with OR-Tools (free, good for most problems)
Use Gurobi/CPLEX for large-scale or performance-critical problems
Use CP-SAT for pure integer/scheduling problems
Use GLPK only for very small problems or teaching
Time Limits
Always set a time limit for MIP problems (can run indefinitely)
Use
gap_tolerancefor MIP to stop when “good enough” solution found
Linearization
Enable only when needed (solver lacks native support)
Tune
big_mbased on your problem’s value rangesIncrease
pwl_segmentsfor better nonlinear function approximations
Sensitivity Analysis
Enable only for LP problems or after MIP solve
Not all solvers support it (check capabilities)
Reusing Optimizers
Create optimizer once, solve multiple models
Configuration persists across solves
optimizer = LXOptimizer().use_solver("gurobi").enable_sensitivity() solution1 = optimizer.solve(model1) solution2 = optimizer.solve(model2) # Same configuration
Error Handling¶
Import Errors¶
try:
optimizer = LXOptimizer().use_solver("gurobi")
solution = optimizer.solve(model)
except ImportError as e:
print(f"Solver not available: {e}")
# Fall back to OR-Tools
optimizer = LXOptimizer().use_solver("ortools")
solution = optimizer.solve(model)
Infeasible Models¶
solution = optimizer.solve(model)
if solution.status == "infeasible":
print("Model is infeasible!")
# Debug: Check constraint conflicts
# or relax some constraints
Time Limit Exceeded¶
solution = optimizer.solve(model, time_limit=60)
if solution.status == "time_limit":
print(f"Time limit reached. Best objective: {solution.objective_value}")
# Use best solution found so far (if available)
Next Steps¶
Choosing a Solver - How to choose the right solver
Solver Configuration - Advanced solver configuration
Solver Capabilities - Understanding solver capabilities
Advanced Solver Features - Callbacks, warm start, solution pools
Solvers Module API - Complete API reference