Design Decisions

Understanding the “why” behind LumiX’s design.

Variable Families vs. Individual Variables

Decision: Variables are families that expand automatically.

Traditional Approach

# Manual loops, error-prone
x = {}
for i, product in enumerate(products):
    x[i] = model.addVar(name=f"x_{i}")

LumiX Approach

# Declarative, type-safe
production = LXVariable[Product, float]("production").from_data(products)

Rationale:

  1. Elimination of Manual Loops: Reduces boilerplate and bugs

  2. Type Safety: Lambda parameters are typed (IDE autocomplete)

  3. Data-Driven: Variables naturally map to business entities

  4. Solver Agnostic: Expansion logic separated from declaration

  5. Maintainability: Changes to data structure don’t require code changes

Trade-offs:

  • ✗ Slightly more memory for storing families

  • ✗ Learning curve for family concept

  • ✓ Far fewer bugs from manual indexing

  • ✓ Much more readable code

  • ✓ Better IDE support

Late Binding (Lazy Expansion)

Decision: Variables/constraints expand during solving, not construction.

Why Late Binding?

# Construction phase - only stores template
production = LXVariable[Product, float]("production").from_data(products)

# Solving phase - expands to solver variables
solution = optimizer.solve(model)

Rationale:

  1. Solver Independence: Same model works with any solver

  2. Flexibility: Can modify data before solving

  3. Memory Efficiency: Don’t create solver vars until needed

  4. Testing: Can validate model structure without solver

Trade-offs:

  • ✗ Can’t inspect solver variables before solving

  • ✓ Models are portable across solvers

  • ✓ Can validate logic without solver

  • ✓ Reduced memory footprint

Lambda Functions for Coefficients

Decision: Coefficients are lambdas, not values.

Example

# Lambda evaluated for each product
expr.add_term(production, lambda p: p.unit_cost * p.tax_rate)

Rationale:

  1. Type Safety: IDE knows p is a Product

  2. Expressiveness: Complex calculations inline

  3. Data-Driven: Coefficients come from data

  4. Lazy Evaluation: Computed only when needed

Alternatives Considered:

Dictionary Approach

# Rejected: Verbose, error-prone
coeffs = {p.id: p.unit_cost * p.tax_rate for p in products}
expr.add_term(production, coeffs)

Why lambdas won:

  • ✓ Type-safe (mypy checks lambda body)

  • ✓ Concise (one line vs. two)

  • ✓ Fewer opportunities for index mismatches

Method Chaining (Fluent API)

Decision: All core classes use fluent API.

Example

production = (
    LXVariable[Product, float]("production")
    .continuous()
    .bounds(lower=0)
    .from_data(products)
)

Rationale:

  1. Readability: Reads like a sentence

  2. Discoverability: IDE suggests next methods

  3. Immutability-Like: Each method returns modified self

  4. Consistency: Same pattern across all classes

Alternatives Considered:

Named Arguments

# Rejected: Less discoverable
production = LXVariable(
    name="production",
    var_type=VarType.CONTINUOUS,
    lower_bound=0,
    data=products
)

Why fluent API won:

  • ✓ Better IDE support (suggests next method)

  • ✓ More flexible (methods can have complex logic)

  • ✓ Easier to extend (add new methods)

Generic Type Parameters

Decision: Extensive use of Generic[TModel, TValue].

Example

production = LXVariable[Product, float]("production")
# TModel = Product, TValue = float

Rationale:

  1. IDE Autocomplete: In lambdas, IDE knows types

  2. Static Typing: mypy catches errors

  3. Self-Documenting: Types explicit in code

  4. Refactoring: Safer to rename attributes

Trade-offs:

  • ✗ More verbose type annotations

  • ✗ Generic syntax can be intimidating

  • ✓ Catches errors at development time

  • ✓ Makes code self-documenting

  • ✓ Excellent IDE experience

Data-Driven vs. Imperative

Decision: Prefer data-driven declarative style.

Data-Driven (LumiX)

# Declare what you want
production = LXVariable[Product, float]("production").from_data(products)

capacity = LXConstraint[Resource]("capacity")\\
    .expression(...)\\
    .le()\\
    .rhs(lambda r: r.capacity)\\
    .from_data(resources)

Imperative (Traditional)

# Describe how to build it
x = {}
for i, product in enumerate(products):
    x[i] = model.addVar()

for j, resource in enumerate(resources):
    model.addConstr(
        sum(x[i] * usage[i][j] for i in range(len(products)))
        <= resource.capacity
    )

Rationale:

  1. Clarity: What vs. How

  2. Maintainability: Less code to break

  3. Testability: Easier to unit test

  4. Separation: Model logic separate from data

Single Module Import

Decision: Import everything from top-level lumix.

Example

from lumix import (
    LXModel,
    LXVariable,
    LXConstraint,
    LXLinearExpression,
)

Rationale:

  1. Simplicity: One import location

  2. Discoverability: IDE shows all available items

  3. Stability: Can reorganize internals without breaking imports

Implementation:

All public classes exported via __init__.py:

# src/lumix/__init__.py
from .core import LXModel, LXVariable, LXConstraint
from .solvers import LXOptimizer
# ...

Solver Abstraction

Decision: Unified interface across all solvers.

Interface

class LXSolverInterface:
    def create_model(self, lx_model: LXModel) -> Any:
        """Create solver-specific model."""

    def solve(self) -> LXSolution:
        """Solve and return solution."""

Rationale:

  1. Portability: Switch solvers with one line

  2. Testing: Test with free solver, deploy with commercial

  3. Comparison: Easy to benchmark different solvers

  4. Future-Proof: Add new solvers without changing user code

Trade-offs:

  • ✗ Can’t use solver-specific features directly

  • ✗ Interface must support lowest common denominator

  • ✓ Models are portable

  • ✓ Easy to add new solvers

  • ✓ Users not locked to one solver

Automatic Linearization

Decision: Provide automatic linearization for non-linear terms.

Example

# User writes non-linear
expr = LXNonLinearExpression()
expr.add_product(x, y)  # Bilinear

# LumiX linearizes automatically
linearized = linearizer.linearize(expr)

Rationale:

  1. Accessibility: Use free solvers for non-linear problems

  2. Transparency: Users don’t need to know linearization techniques

  3. Correctness: Implemented once, tested thoroughly

  4. Performance: Optimized implementations

Alternatives:

Manual linearization

Rejected because:

  • ✗ Error-prone (McCormick envelopes are tricky)

  • ✗ Verbose (many auxiliary variables and constraints)

  • ✗ Non-portable (different techniques for different terms)

Performance Considerations

Late Binding Overhead

Concern: Storing lambdas and evaluating them has overhead.

Analysis:

  • Lambda evaluation: ~10-100 ns

  • Model building time: Usually <1% of solve time

  • Large models (10,000+ variables): Overhead ~10-50ms

Decision: Trade-off acceptable because:

  • Solve time dominates (seconds to hours)

  • User productivity gain is huge

  • Can optimize hot paths if needed

Memory Usage

Concern: Storing families uses more memory than direct arrays.

Analysis:

  • Family metadata: ~500 bytes per family

  • Typical model: 5-20 families

  • Overhead: <10 KB

Decision: Negligible compared to:

  • Solver memory (MB to GB)

  • Data storage (typically larger)

Future Directions

Planned Improvements

  1. Compile-time validation: Detect errors before running

  2. JIT compilation: Compile lambdas for performance

  3. Incremental solving: Modify and re-solve efficiently

  4. Parallel expansion: Multi-threaded variable creation

  5. Symbolic differentiation: Auto-compute gradients

Research Questions

  1. Can we infer index structure from data relationships?

  2. How to best support streaming/online optimization?

  3. What’s the right abstraction for stochastic programming?

Summary

LumiX’s design prioritizes:

  1. User Experience: Type safety, IDE support, readability

  2. Maintainability: Less code, fewer bugs, easier refactoring

  3. Portability: Solver-agnostic models

  4. Correctness: Tested implementations of complex techniques

The trade-offs (slight overhead, learning curve) are worth it for the dramatic improvement in development experience and code quality.

Next Steps