Variables Guide

Variables represent the decisions to be made in your optimization model.

Variable Families

Key Concept: In LumiX, an LXVariable is not a single variable, but a family that expands to multiple solver variables based on your data.

# This ONE LXVariable object represents MANY solver variables
production = LXVariable[Product, float]("production").from_data(products)

# Automatically expands to:
#   production[product1], production[product2], production[product3], ...

Variable Types

Continuous Variables

Real-valued variables for quantities that can take any value:

production = (
    LXVariable[Product, float]("production")
    .continuous()  # Default type
    .bounds(lower=0, upper=1000)
    .from_data(products)
)

Use for: Production quantities, percentages, weights, allocations

Integer Variables

Whole number variables:

num_trucks = (
    LXVariable[Route, int]("trucks")
    .integer()
    .bounds(lower=0, upper=10)
    .from_data(routes)
)

Use for: Counts, discrete quantities, number of items

Binary Variables

Yes/no decision variables (0 or 1):

is_open = (
    LXVariable[Facility, int]("is_open")
    .binary()  # Automatically sets bounds to [0, 1]
    .from_data(facilities)
)

Use for: Selection, activation, yes/no decisions

Indexing

Single-Model Indexing

Index variables by a single data model:

production = (
    LXVariable[Product, float]("production")
    .continuous()
    .indexed_by(lambda p: p.id)  # Index function
    .from_data(products)  # Data source
)

Index Options:

  • Simple: .indexed_by(lambda p: p.id)

  • Tuple: .indexed_by(lambda p: (p.category, p.id))

  • String: .indexed_by(lambda p: f”{p.factory}_{p.id}”)

Multi-Model Indexing

Index by multiple models using Cartesian product:

from lumix import LXIndexDimension, LXCartesianProduct

assignment = (
    LXVariable[tuple[Driver, Date, Shift], int]("assignment")
    .binary()
    .indexed_by_product(
        LXIndexDimension(Driver, lambda d: d.id).from_data(drivers),
        LXIndexDimension(Date, lambda dt: dt.id).from_data(dates),
        LXIndexDimension(Shift, lambda s: s.id).from_data(shifts),
    )
)

This creates variables for every combination of driver, date, and shift.

Filtering

Filter which instances to include:

Single-Model Filter

production = (
    LXVariable[Product, float]("production")
    .continuous()
    .where(lambda p: p.is_active and p.stock > 0)
    .from_data(products)
)

Multi-Model Filter

assignment = (
    LXVariable[tuple[Driver, Date], int]("assignment")
    .binary()
    .indexed_by_product(
        LXIndexDimension(Driver, lambda d: d.id)
            .where(lambda d: d.is_qualified)  # Per-dimension filter
            .from_data(drivers),
        LXIndexDimension(Date, lambda dt: dt.id)
            .from_data(dates),
    )
    .where_multi(lambda driver, date:  # Cross-dimension filter
        date not in driver.days_off
    )
)

Data Sources

Direct Data

Provide data directly:

products = [Product(id="A", cost=10), Product(id="B", cost=20)]

production = (
    LXVariable[Product, float]("production")
    .from_data(products)
)

ORM Integration

Query from database:

from sqlalchemy.orm import Session

production = (
    LXVariable[Product, float]("production")
    .from_model(Product, session=db_session)
)

Cartesian Product

For multi-dimensional variables:

from lumix import LXCartesianProduct

assignment = (
    LXVariable[tuple[Driver, Date], int]("assignment")
    .from_data(LXCartesianProduct(drivers, dates))
)

Cost Coefficients

Define objective coefficients:

Single-Model

production = (
    LXVariable[Product, float]("production")
    .cost(lambda p: p.unit_profit)  # Coefficient from data
    .from_data(products)
)

# Use in objective
model.maximize(
    LXLinearExpression().add_term(production, lambda p: p.profit)
)

Multi-Model

shipment = (
    LXVariable[tuple[Origin, Destination], float]("shipment")
    .cost_multi(lambda o, d: calculate_shipping_cost(o, d))
    .indexed_by_product(...)
)

Bounds

Set variable bounds:

Simple Bounds

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

Data-Driven Bounds

Bounds can vary per instance:

# Use where clause for conditional bounds
production = (
    LXVariable[Product, float]("production")
    .continuous()
    .bounds(lower=0)
    .from_data(products)
)

# Add constraints for upper bounds from data
model.add_constraint(
    LXConstraint[Product]("max_production")
    .expression(LXLinearExpression().add_term(production, 1.0))
    .le()
    .rhs(lambda p: p.max_capacity)
    .from_data(products)
)

Best Practices

  1. Use Type Annotations

    # Good: Type-safe
    production = LXVariable[Product, float]("production")
    
    # Bad: No type safety
    production = LXVariable("production")
    
  2. Name Variables Clearly

    # Good: Descriptive
    daily_production = LXVariable[Product, float]("daily_production")
    
    # Bad: Cryptic
    x = LXVariable[Product, float]("x")
    
  3. Use Fluent API

    # Good: Readable chain
    production = (
         LXVariable[Product, float]("production")
         .continuous()
         .bounds(lower=0)
         .from_data(products)
     )
    
  4. Filter Early

    # Good: Filter at variable level
    production = (
        LXVariable[Product, float]("production")
        .where(lambda p: p.is_active)
        .from_data(products)
    )
    
    # Less efficient: Filter in constraints
    # Creates unnecessary variables
    

Common Patterns

Production Planning

production = (
    LXVariable[Product, float]("production")
    .continuous()
    .bounds(lower=0)
    .cost(lambda p: -p.unit_cost)  # Negative for minimization
    .from_data(products)
)

Facility Location

is_open = (
    LXVariable[Facility, int]("is_open")
    .binary()
    .cost(lambda f: f.fixed_cost)
    .from_data(facilities)
)

Assignment

assign = (
    LXVariable[tuple[Worker, Task], int]("assign")
    .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.skill_level <= w.skill_level)
)

Next Steps