Model Logging Guide

This guide covers the LXModelLogger class for tracking optimization model building, solving progress, and solution analysis.

Overview

The LXModelLogger provides domain-specific logging tailored for optimization models. Unlike generic Python logging, it includes specialized methods for common optimization events like model creation, variable/constraint addition, solving, and solution analysis.

Key Benefits:

  • Automatic solve time tracking

  • Consistent, formatted output for optimization events

  • Configurable logging levels

  • Built on Python’s standard logging module

Basic Usage

Creating a Logger

from lumix.utils import LXModelLogger
import logging

# Create logger with default settings
logger = LXModelLogger()

# Create with custom name and level
logger = LXModelLogger(
    name="production_model",
    level=logging.INFO  # or DEBUG, WARNING, ERROR
)

Logging Model Construction

from lumix import LXModel, LXVariable
from lumix.utils import LXModelLogger

logger = LXModelLogger(name="build", level=logging.DEBUG)

# Log model creation
model = LXModel("production_plan")
logger.log_model_creation("production_plan", num_vars=100, num_constraints=50)

# Log variable creation (DEBUG level)
logger.log_variable_creation("production", "continuous", count=50)

# Log constraint creation (DEBUG level)
logger.log_constraint_creation("capacity", "<=", count=10)

Logging Solve Process

from lumix import LXOptimizer

logger = LXModelLogger(name="solve")

# Start timing
logger.log_solve_start("Gurobi")

# Solve model
optimizer = LXOptimizer().use_solver("gurobi")
solution = optimizer.solve(model)

# Log results (time automatically calculated)
logger.log_solve_end(
    status=solution.status,
    objective_value=solution.objective_value
)

Advanced Features

Logging Linearization

When using automatic linearization, track the transformations:

logger = LXModelLogger(name="linearize", level=logging.DEBUG)

# Log when linearization is applied
logger.log_linearization(
    term_type="bilinear",
    method="McCormick",
    aux_vars=4
)

# Output: Linearized bilinear using McCormick (added 4 auxiliary variables)

Logging Scenario Analysis

Track scenario analysis runs:

logger = LXModelLogger(name="scenarios")

scenarios = [
    ("base_case", 0),
    ("high_demand", 5),
    ("low_cost", 3)
]

for name, modifications in scenarios:
    logger.log_scenario(name, modifications)
    # ... run scenario ...

# Output:
# Running scenario 'base_case' with 0 modifications
# Running scenario 'high_demand' with 5 modifications
# Running scenario 'low_cost' with 3 modifications

Logging Sensitivity Analysis

Track sensitivity analysis results:

logger = LXModelLogger(name="sensitivity", level=logging.DEBUG)

for var_name, reduced_cost in sensitivity_results.items():
    logger.log_sensitivity(var_name, reduced_cost)

# Output: Sensitivity: production[Widget_A] reduced cost = -2.500000

Generic Logging Methods

Use standard logging methods for custom messages:

logger = LXModelLogger(name="custom")

# Info level
logger.info("Starting preprocessing phase")

# Debug level
logger.debug(f"Constraint matrix sparsity: {sparsity:.2%}")

# Warning level
logger.warning("Model contains unbounded variables")

# Error level
logger.error("Failed to retrieve dual values")

Logging Levels

Choose the appropriate level based on your needs:

DEBUG Level

Most verbose - logs everything including variable/constraint creation:

logger = LXModelLogger(level=logging.DEBUG)

Use When:

  • Development and debugging

  • Detailed model analysis

  • Troubleshooting model building issues

Output Example:

2025-10-22 14:30:01 - production - DEBUG - Created 50 continuous variable(s): production
2025-10-22 14:30:01 - production - DEBUG - Created 10 constraint(s): capacity (<=)
2025-10-22 14:30:01 - production - INFO - Created model 'ProductionPlan' with 50 variables and 10 constraints

INFO Level (Default)

Standard level - logs major events:

logger = LXModelLogger(level=logging.INFO)  # or omit, it's default

Use When:

  • Production environments

  • Normal operations

  • Tracking solve progress

Output Example:

2025-10-22 14:30:01 - production - INFO - Created model 'ProductionPlan' with 50 variables and 10 constraints
2025-10-22 14:30:02 - production - INFO - Starting solve with Gurobi...
2025-10-22 14:30:05 - production - INFO - Solve completed: Optimal | Objective: 42500.7500 | Time: 2.85s

WARNING Level

Only warnings and errors:

logger = LXModelLogger(level=logging.WARNING)

Use When:

  • Production with minimal logging

  • Only want to see problems

  • Performance-critical applications

ERROR Level

Only errors:

logger = LXModelLogger(level=logging.ERROR)

Use When:

  • Error tracking only

  • Silent operation except for failures

Integration Examples

Complete Model Workflow

import logging
from lumix import LXModel, LXVariable, LXConstraint, LXLinearExpression, LXOptimizer
from lumix.utils import LXModelLogger

# Create logger
logger = LXModelLogger(name="production", level=logging.INFO)

logger.info("=== Building Production Planning Model ===")

# Build model
model = LXModel("production_plan")

production = (
    LXVariable[Product, float]("production")
    .continuous()
    .bounds(lower=0)
    .from_data(products)
)
model.add_variable(production)
logger.log_variable_creation("production", "continuous", count=len(products))

capacity_constraint = (
    LXConstraint[Resource]("capacity")
    .expression(
        LXLinearExpression().add_term(production, lambda p, r: p.usage[r.id])
    )
    .le()
    .rhs(lambda r: r.capacity)
    .from_data(resources)
)
model.add_constraint(capacity_constraint)
logger.log_constraint_creation("capacity", "<=", count=len(resources))

model.maximize(
    LXLinearExpression().add_term(production, lambda p: p.profit)
)

logger.log_model_creation(
    "production_plan",
    num_vars=len(products),
    num_constraints=len(resources)
)

# Solve
logger.log_solve_start("Gurobi")
optimizer = LXOptimizer().use_solver("gurobi")
solution = optimizer.solve(model)
logger.log_solve_end(solution.status, solution.objective_value)

# Solution summary
nonzero_vars = sum(1 for v in solution.values.values() if abs(v) > 1e-6)
logger.log_solution_summary(nonzero_vars, len(products))

logger.info("=== Optimization Complete ===")

Multiple Models

Use different loggers for different models:

# Separate loggers
logger_prod = LXModelLogger(name="production")
logger_routing = LXModelLogger(name="routing")

# Production model
logger_prod.info("Building production model")
# ... build and solve ...

# Routing model
logger_routing.info("Building routing model")
# ... build and solve ...

Custom Formatting

Customize the log format by modifying the handler:

import logging

logger = LXModelLogger(name="custom_format")

# Get the underlying Python logger
py_logger = logger.logger

# Remove existing handlers
py_logger.handlers.clear()

# Add custom handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    '[%(levelname)s] %(message)s'  # Simple format
))
py_logger.addHandler(handler)

logger.info("This uses custom format")

Best Practices

  1. Consistent Naming

    Use descriptive, hierarchical names:

    # Good
    logger = LXModelLogger(name="supply_chain.production")
    logger = LXModelLogger(name="supply_chain.routing")
    
    # Avoid
    logger = LXModelLogger(name="logger1")
    
  2. Appropriate Levels

    # Development: DEBUG
    logger = LXModelLogger(level=logging.DEBUG)
    
    # Production: INFO
    logger = LXModelLogger(level=logging.INFO)
    
    # Performance-critical: WARNING
    logger = LXModelLogger(level=logging.WARNING)
    
  3. Structured Logging

    Log at key milestones, not in tight loops:

    # Good
    logger.log_variable_creation("production", "continuous", count=len(products))
    
    # Avoid
    for product in products:
        logger.log_variable_creation(f"x[{product.id}]", "continuous")
    
  4. Conditional Logging

    For expensive log messages, use conditionals:

    if logger.logger.isEnabledFor(logging.DEBUG):
        expensive_message = compute_detailed_stats()
        logger.debug(expensive_message)
    
  5. Error Handling

    Always log errors with context:

    try:
        solution = optimizer.solve(model)
    except Exception as e:
        logger.error(f"Solve failed: {e}")
        raise
    

Common Patterns

Progress Tracking

logger = LXModelLogger(name="progress")

total = len(scenarios)
for i, scenario in enumerate(scenarios, 1):
    logger.info(f"Processing scenario {i}/{total}: {scenario.name}")
    # ... process scenario ...

Performance Monitoring

import time

logger = LXModelLogger(name="performance")

start = time.time()
# ... build model ...
build_time = time.time() - start

logger.info(f"Model built in {build_time:.2f}s")

logger.log_solve_start("Gurobi")
# ... solve ...
logger.log_solve_end(status, objective_value)

See Also