Bilinear Products¶
The LXBilinearTerm represents products of two decision variables
(x * y), automatically linearized based on variable types.
Overview¶
Bilinear terms are products of two variables:
Common Use Cases:
Facility activation × flow amount
Price × quantity (revenue)
Area calculations (length × width)
Resource selection × usage
On/off controls for continuous flows
Linearization Methods¶
LumiX automatically selects the appropriate linearization based on variable types:
Binary × Binary (AND Logic)¶
For binary variables x, y ∈ {0, 1}, introduce z with constraints:
Properties: - Exact linearization - 3 constraints - No auxiliary variables beyond z - Very efficient
Binary × Continuous (Big-M)¶
For binary x ∈ {0, 1} and continuous y ∈ [L, U] (with finite bounds L and U), introduce z with:
Properties: - 4 constraints - Requires finite bounds on y - M = max(|L|, |U|) - Tighter bounds → better performance
Continuous × Continuous (McCormick Envelopes)¶
For continuous x ∈ [xL, xU] and y ∈ [yL, yU], introduce z with:
Properties: - 4 constraints (convex and concave envelopes) - Relaxation (not exact unless at vertices) - REQUIRES finite bounds on both variables - Tighter bounds → tighter relaxation
Basic Usage¶
Facility Activation × Flow¶
Flow is only active when facility is open:
from lumix import LXVariable
from lumix.nonlinear import LXBilinearTerm
# Binary: is facility open?
is_open = (
LXVariable[Facility, int]("is_open")
.binary()
.from_data(facilities)
)
# Continuous: potential flow
flow_amount = (
LXVariable[Facility, float]("flow")
.continuous()
.bounds(lower=0, upper=1000)
.from_data(facilities)
)
# Bilinear: actual flow = is_open * flow_amount
actual_flow = LXBilinearTerm(
var1=is_open,
var2=flow_amount,
coefficient=1.0
)
Rectangle Area¶
Calculate area from dimensions:
# Dimensions (both continuous)
length = (
LXVariable[Shape, float]("length")
.continuous()
.bounds(lower=1, upper=10)
)
width = (
LXVariable[Shape, float]("width")
.continuous()
.bounds(lower=1, upper=10)
)
# Area = length * width
area = LXBilinearTerm(
var1=length,
var2=width,
coefficient=1.0
)
Complete Examples¶
Example 1: Production with Setup Costs¶
Production is only possible if setup is done:
from dataclasses import dataclass
from typing import List
from lumix import LXModel, LXVariable, LXConstraint
from lumix.nonlinear import LXBilinearTerm
@dataclass
class Product:
id: str
unit_cost: float
setup_cost: float
max_production: float
products: List[Product] = [...]
# Binary: setup done?
setup = (
LXVariable[Product, int]("setup")
.binary()
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Continuous: production quantity
production = (
LXVariable[Product, float]("production")
.continuous()
.bounds(lower=0)
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Actual production = setup * production
actual_production = LXBilinearTerm(
var1=setup,
var2=production,
coefficient=1.0
)
# Capacity constraint on actual production
capacity = (
LXConstraint[Product]("capacity")
.expression(...) # Use actual_production
.le()
.rhs(lambda p: p.max_production)
.from_data(products)
.indexed_by(lambda p: p.id)
)
model = LXModel("production_setup")
Example 2: Dynamic Pricing (Price × Quantity)¶
Optimize both price and quantity:
@dataclass
class Product:
id: str
min_price: float
max_price: float
max_demand: float
products: List[Product] = [...]
# Decision: price
price = (
LXVariable[Product, float]("price")
.continuous()
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Decision: quantity sold
quantity = (
LXVariable[Product, float]("quantity")
.continuous()
.bounds(lower=0)
.indexed_by(lambda p: p.id)
.from_data(products)
)
# Revenue = price * quantity
revenue = LXBilinearTerm(
var1=price,
var2=quantity,
coefficient=1.0
)
# Price bounds (set via constraints or variable bounds)
# Quantity demand curve (add constraint: quantity <= f(price))
model = LXModel("dynamic_pricing")
# Maximize revenue
Example 3: Binary Selection¶
Select resources and allocate amounts:
# Binary: resource selected?
selected = (
LXVariable[Resource, int]("selected")
.binary()
.from_data(resources)
)
# Continuous: allocation amount
allocation = (
LXVariable[Resource, float]("allocation")
.continuous()
.bounds(lower=0, upper=100)
.from_data(resources)
)
# Actual allocation = selected * allocation
actual_alloc = LXBilinearTerm(
var1=selected,
var2=allocation,
coefficient=1.0
)
Advanced Patterns¶
Multiple Products in Single Expression¶
Sum multiple bilinear terms:
# Total revenue = sum of (price_i * quantity_i)
total_revenue_terms = [
LXBilinearTerm(var1=price, var2=quantity, coefficient=1.0)
for price, quantity in zip(prices, quantities)
]
Weighted Products¶
Apply coefficients to products:
# Discounted revenue = 0.9 * price * quantity
discounted_revenue = LXBilinearTerm(
var1=price,
var2=quantity,
coefficient=0.9
)
Bounds Management¶
Importance of Bounds¶
Critical: Bilinear linearization REQUIRES finite bounds!
# ✗ WRONG: No bounds
price = LXVariable[Product, float]("price").continuous()
quantity = LXVariable[Product, float]("quantity").continuous()
revenue = LXBilinearTerm(var1=price, var2=quantity)
# → Linearization will FAIL or use default (bad) bounds
# ✓ CORRECT: Explicit bounds
price = LXVariable[Product, float]("price").continuous().bounds(10, 100)
quantity = LXVariable[Product, float]("quantity").continuous().bounds(0, 1000)
revenue = LXBilinearTerm(var1=price, var2=quantity)
# → Linearization uses proper McCormick envelopes
Tightening Bounds¶
Tighter bounds → better linearization relaxation:
# Loose bounds (poor relaxation)
price = LXVariable("price").continuous().bounds(0, 1000)
quantity = LXVariable("quantity").continuous().bounds(0, 10000)
# → Large McCormick envelope, weak relaxation
# Tight bounds (good relaxation)
price = LXVariable("price").continuous().bounds(50, 150)
quantity = LXVariable("quantity").continuous().bounds(100, 500)
# → Tight McCormick envelope, strong relaxation
Dynamic Bounds¶
Compute bounds from data:
@dataclass
class Product:
min_price: float
max_price: float
max_demand: float
# Use data-driven bounds
price = (
LXVariable[Product, float]("price")
.continuous()
# Bounds computed per instance via constraints or manually
.from_data(products)
)
Performance Considerations¶
Linearization Overhead¶
Variable Types |
Aux Vars |
Constraints |
Quality |
|---|---|---|---|
Binary × Binary |
1 |
3 |
Exact |
Binary × Continuous |
1 |
4 |
Exact |
Continuous × Continuous |
1 |
4 |
Relaxation |
Model Size Impact¶
# 1000 bilinear terms → 1000 aux vars + 3000-4000 constraints
products = [...] # 1000 products
revenues = [
LXBilinearTerm(var1=price[i], var2=qty[i], coefficient=1.0)
for i in range(1000)
]
# Modern solvers handle this efficiently
Solver Performance¶
Binary × Continuous and Binary × Binary: Exact, solves efficiently
Continuous × Continuous: Relaxation, may need branching
# For cont × cont, consider tightening bounds or using SOS2 variables
# if the problem structure allows
Common Pitfalls¶
Missing Bounds¶
# ✗ ERROR
x = LXVariable("x").continuous() # No bounds!
y = LXVariable("y").continuous() # No bounds!
product = LXBilinearTerm(var1=x, var2=y)
# → Linearization fails or uses bad default bounds
Unbounded Variables¶
# ✗ VERY BAD
x = LXVariable("x").continuous().bounds(-1e9, 1e9)
y = LXVariable("y").continuous().bounds(-1e9, 1e9)
product = LXBilinearTerm(var1=x, var2=y)
# → Huge M values, poor numerics
Wrong Variable Order¶
For Binary × Continuous, put binary first (though LumiX should handle automatically):
# Preferred order
product = LXBilinearTerm(var1=binary_var, var2=continuous_var)
Integration with Expressions¶
Using in Linear Expressions¶
from lumix import LXLinearExpression
# Build expression with bilinear term
# (requires linearization engine to expand)
expr = LXLinearExpression()
# Add bilinear terms via linearization auxiliary variables
See Also¶
LXBilinearTerm- API referenceBilinear Product Linearization - Bilinear linearization details
Variables Guide - Variable bounds
Next Steps¶
Indicator Constraints - Conditional constraints
Piecewise-Linear Functions - Piecewise-linear functions
Linearization Concepts - Linearization configuration