Bilinear Product Linearization¶
Guide for linearizing bilinear products (x × y) in optimization models.
Overview¶
Bilinear products appear frequently in optimization:
Revenue: price × quantity
Energy: power × time
Logistics: distance × flow
Finance: return × allocation
The LXBilinearLinearizer class
automatically selects the appropriate linearization technique based on variable types.
Variable Type Combinations¶
The linearization method depends on the types of the two variables:
Type 1 |
Type 2 |
Method |
Complexity |
|---|---|---|---|
Binary |
Binary |
AND logic |
3 constraints |
Binary |
Continuous |
Big-M |
4 constraints |
Binary |
Integer |
Big-M |
4 constraints |
Continuous |
Continuous |
McCormick |
4 constraints |
Continuous |
Integer |
McCormick |
4 constraints |
Integer |
Integer |
McCormick |
4 constraints |
Binary × Binary (AND Logic)¶
Mathematical Formulation¶
For z = x × y where x, y ∈ {0, 1}:
Logical Interpretation:
z = 1 if and only if both x = 1 and y = 1
Implements logical AND operation
Example¶
from lumix import LXModel, LXVariable
from lumix.linearization.techniques import LXBilinearLinearizer
from lumix.linearization.config import LXLinearizerConfig
from lumix.nonlinear.terms import LXBilinearTerm
# Define binary variables
is_open = LXVariable[Facility, int]("is_open").binary()
is_served = LXVariable[Customer, int]("is_served").binary()
# Create bilinear term: facility_open AND customer_served
term = LXBilinearTerm(
var1=is_open,
var2=is_served,
coefficient=1.0
)
# Linearize
config = LXLinearizerConfig()
linearizer = LXBilinearLinearizer(config)
z = linearizer.linearize_bilinear(term)
# z represents is_open AND is_served
Use Cases¶
Facility Location: Open facility AND serve customer
Scheduling: Assign shift AND assign driver
Logic: Boolean AND operations in constraints
Binary × Continuous (Big-M Method)¶
Mathematical Formulation¶
For z = b × x where b ∈ {0, 1}, x ∈ [L, U]:
where M = upper bound of x, m = lower bound of x.
Behavior:
When b = 0: z = 0 (forced by first two constraints)
When b = 1: z = x (forced by last two constraints)
Example¶
# Binary variable: is facility open?
is_open = (
LXVariable[Facility, int]("is_open")
.binary()
.indexed_by(lambda f: f.id)
.from_data(facilities)
)
# Continuous variable: flow amount
flow = (
LXVariable[Route, float]("flow")
.continuous()
.bounds(lower=0, upper=1000)
.indexed_by(lambda r: r.id)
.from_data(routes)
)
# Product: flow only if facility is open
# z = is_open × flow
term = LXBilinearTerm(var1=is_open, var2=flow, coefficient=1.0)
config = LXLinearizerConfig(big_m_value=1000)
linearizer = LXBilinearLinearizer(config)
z = linearizer.linearize_bilinear(term)
Choosing Big-M Value¶
Critical: M must be large enough but not too large.
Guidelines:
Use Variable Bounds:
flow = LXVariable[Route, float]("flow").bounds(lower=0, upper=1000) # M = 1000 (upper bound)
Problem Knowledge:
# For normalized probabilities [0, 1] config = LXLinearizerConfig(big_m_value=1.0) # For prices up to $10,000 config = LXLinearizerConfig(big_m_value=10000)
Conservative Multiplier:
max_flow = 1000 config = LXLinearizerConfig(big_m_value=10 * max_flow)
Validation:
# After solving, check that auxiliary variable doesn't hit M
solution = optimizer.solve(linearized_model)
for var_name, value in solution.variables.items():
if "aux_prod" in var_name:
assert value < 0.99 * config.big_m_value, \
f"Variable {var_name} hit Big-M bound!"
Use Cases¶
Conditional Flow: Flow only if route is open
Pricing: Revenue only if product is sold
Scheduling: Work hours only if worker is assigned
Continuous × Continuous (McCormick Envelopes)¶
Mathematical Formulation¶
For z = x × y where x ∈ [xL, xU], y ∈ [yL, yU]:
Geometric Interpretation:
These four constraints form the convex hull of the bilinear function over the box [xL, xU] × [yL, yU].
Note
McCormick envelopes provide the tightest possible linear relaxation of the bilinear term over the given bounds.
Example¶
# Continuous variable: price ($/unit)
price = (
LXVariable[Product, float]("price")
.continuous()
.bounds(lower=10, upper=100)
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Continuous variable: quantity (units)
quantity = (
LXVariable[Product, float]("quantity")
.continuous()
.bounds(lower=0, upper=1000)
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Product: revenue = price × quantity
term = LXBilinearTerm(var1=price, var2=quantity, coefficient=1.0)
config = LXLinearizerConfig(
mccormick_tighten_bounds=True # Improve relaxation
)
linearizer = LXBilinearLinearizer(config)
revenue = linearizer.linearize_bilinear(term)
Importance of Tight Bounds¶
Tighter bounds = stronger relaxation = faster solving
Example of bound impact:
# Wide bounds (weak relaxation)
x = LXVariable[Model, float]("x").bounds(lower=0, upper=1000)
y = LXVariable[Model, float]("y").bounds(lower=0, upper=1000)
# McCormick creates large convex hull
# Tight bounds (strong relaxation)
x = LXVariable[Model, float]("x").bounds(lower=10, upper=50)
y = LXVariable[Model, float]("y").bounds(lower=5, upper=20)
# McCormick creates tight convex hull
Bound Tightening:
Enable automatic bound tightening:
config = LXLinearizerConfig(
mccormick_tighten_bounds=True, # Apply preprocessing
auto_detect_bounds=True # Use variable bounds
)
Use Cases¶
Revenue: price × quantity
Energy: power × time
Economics: elasticity × demand
Engineering: force × displacement
Advanced: Multi-dimensional Products¶
For products of more than two variables, apply linearization recursively:
# For x × y × z:
# Step 1: w = x × y (linearize)
# Step 2: result = w × z (linearize)
from lumix.linearization.techniques import LXBilinearLinearizer
config = LXLinearizerConfig()
linearizer = LXBilinearLinearizer(config)
# First product: w = x × y
term_xy = LXBilinearTerm(var1=x, var2=y, coefficient=1.0)
w = linearizer.linearize_bilinear(term_xy)
# Second product: result = w × z
term_wz = LXBilinearTerm(var1=w, var2=z, coefficient=1.0)
result = linearizer.linearize_bilinear(term_wz)
# Add all auxiliary elements to model
for var in linearizer.auxiliary_vars:
model.add_variable(var)
for constraint in linearizer.auxiliary_constraints:
model.add_constraint(constraint)
Complete Example¶
Production Planning with Revenue Maximization¶
from lumix import LXModel, LXVariable, LXConstraint
from lumix.linearization import LXLinearizer, LXLinearizerConfig
from lumix.solvers import LXOptimizer
# Define data
@dataclass
class Product:
id: str
min_quantity: float
max_quantity: float
min_price: float
max_price: float
products = [
Product("A", 0, 1000, 10, 100),
Product("B", 0, 500, 20, 150),
]
# Define variables
quantity = (
LXVariable[Product, float]("quantity")
.continuous()
.indexed_by(lambda p: p.id)
.bounds_func(lambda p: (p.min_quantity, p.max_quantity))
.from_data(products)
)
price = (
LXVariable[Product, float]("price")
.continuous()
.indexed_by(lambda p: p.id)
.bounds_func(lambda p: (p.min_price, p.max_price))
.from_data(products)
)
# Build model
model = LXModel("production")
# Objective: maximize revenue (bilinear: price × quantity)
# This will be automatically linearized
revenue_expr = price * quantity # Nonlinear!
model.maximize(revenue_expr)
# Constraints
# Total quantity constraint
model.add_constraint(
LXConstraint("total_quantity")
.expression(quantity)
.le()
.rhs(1200)
)
# Configure linearization
config = LXLinearizerConfig(
default_method=LXLinearizationMethod.MCCORMICK,
mccormick_tighten_bounds=True,
verbose_logging=True
)
# Solve with linearization
optimizer = LXOptimizer().use_solver("glpk")
solver_capability = optimizer.get_capability()
linearizer = LXLinearizer(model, solver_capability, config)
linearized_model = linearizer.linearize_model()
# Get statistics
stats = linearizer.get_statistics()
print(f"Linearization added:")
print(f" - {stats['auxiliary_variables']} variables")
print(f" - {stats['auxiliary_constraints']} constraints")
# Solve
solution = optimizer.solve(linearized_model)
print(f"Optimal revenue: ${solution.objective_value:,.2f}")
Performance Tips¶
Always Provide Bounds
# Good: McCormick will be tight x = LXVariable[Model, float]("x").bounds(lower=0, upper=100) # Bad: Wide default bounds, weak relaxation x = LXVariable[Model, float]("x") # No bounds!
Enable Bound Tightening
config = LXLinearizerConfig(mccormick_tighten_bounds=True)
Use Appropriate Big-M
# Problem-specific, not default config = LXLinearizerConfig(big_m_value=1000) # Not 1e6!
Prefer Native Quadratic When Available
# For Gurobi/CPLEX with quadratic support, # solver may handle bilinear natively without linearization
See Also¶
Configuration - Configuration guide
Linearization Engine - Linearization engine
Linearization Module API - API reference
Linearization Architecture - Architecture details