Working with Constraints
Constraints define the feasibility conditions that solutions must satisfy in an optimization problem. LunaModel provides a flexible and intuitive way to create and manage constraints.
Overview
A constraint consists of three components:
- Left-hand side (LHS): An
Expression - Comparator: One of
==,<=, or>= - Right-hand side (RHS): A numerical constant
For example: 3x + 2y <= 10 has LHS=3x + 2y, comparator=<=, and RHS=10.
Creating Constraints
Basic Constraints
Create constraints using comparison operators on expressions:
from luna_model import Model, Vtype
model = Model()
x = model.add_variable("x", vtype=Vtype.BINARY)
y = model.add_variable("y", vtype=Vtype.BINARY)
# Equality constraint
model.constraints += x + y == 1
# Less than or equal
model.constraints += 3 * x + 2 * y <= 5
# Greater than or equal
model.constraints += x - y >= 0
Named Constraints
Give constraints descriptive names for debugging and analysis:
# Add constraint with name
model.add_constraint(x + y <= 1, name="selection_limit")
# Or use the constraints collection
constraint = x + y == 1
model.constraints.add_constraint(constraint, name="balance")
Multiple Constraints
Add multiple constraints at once:
# Add constraints one by one
for i in range(n):
model.constraints += items[i] >= 0
model.constraints += items[i] <= capacity[i]
# Or using a list
constraints = [items[i] <= capacity[i] for i in range(n)]
for constraint in constraints:
model.constraints += constraint
Constraint Types
Linear Constraints
Most common constraint type with linear expressions:
# Capacity constraint
model.constraints += quicksum(weights[i] * items[i] for i in range(n)) <= max_weight
# Balance constraint
model.constraints += quicksum(incoming[i] for i in inputs) == quicksum(outgoing[j] for j in outputs)
# Budget constraint
model.constraints += quicksum(costs[i] * quantities[i] for i in range(n)) <= budget
Quadratic Constraints
Constraints with quadratic expressions:
# Euclidean distance constraint
model.constraints += x * x + y * y <= radius_squared
# Covariance constraint
model.constraints += quicksum(
cov[i, j] * alloc[i] * alloc[j]
for i in assets
for j in assets
) <= max_variance
# Product constraint
model.constraints += x * y >= min_product
Higher-Order Constraints
LunaModel supports constraints with polynomial expressions:
# Cubic constraint
model.constraints += x * y * z <= 1
# Mixed degree
model.constraints += x * y * z + x * y + x == 5
Note: Higher-order constraints may need transformation for certain solvers.
Constraint Comparators
Equality (==)
Use for exact requirements:
from luna_model import Comparator
# Flow conservation
model.constraints += inflow == outflow
# Assignment constraint
model.constraints += quicksum(assignments[i] for i in workers) == num_tasks
# Allocation sum to 1
model.constraints += quicksum(portfolio[i] for i in assets) == 1.0
Less Than or Equal (<=)
Use for upper bounds and capacity constraints:
# Resource capacity
model.constraints += quicksum(resource_usage[i] * activities[i] for i in range(n)) <= capacity
# Time limit
model.constraints += duration <= max_time
# Selection limit
model.constraints += quicksum(selected[i] for i in range(n)) <= max_selections
Greater Than or Equal (>=)
Use for lower bounds and minimum requirements:
# Minimum production
model.constraints += quicksum(production[i] for i in facilities) >= demand
# Coverage requirement
model.constraints += quicksum(coverage[i, j] * sensors[i] for i in locations) >= min_coverage
# Quality constraint
model.constraints += quality_score >= min_quality
Working with Constraint Collections
Adding Constraints
# Using += operator
model.constraints += x + y <= 1
# Using add_constraint method
model.add_constraint(x + y <= 1, name="limit")
# Adding to collection directly
constraint = x + y <= 1
model.constraints.add(constraint)
Accessing Constraints
# Iterate over all constraints
for constraint in model.constraints:
print(f"{constraint.lhs} {constraint.sense} {constraint.rhs}")
# Get number of constraints
num_constraints = len(model.constraints)
print(f"Model has {num_constraints} constraints")
# Access by name (if named)
constraint = model.constraints["balance"]
Removing Constraints
# Remove by name
del model.constraints["old_constraint"]
# Clear all constraints
model.constraints.clear()
Common Constraint Patterns
Capacity Constraints
Limit resource usage:
# Single resource
model.constraints += quicksum(
resource_usage[i] * variables[i]
for i in range(n)
) <= capacity
# Multiple resources
for resource in resources:
model.constraints += quicksum(
usage[resource, task] * task_vars[task]
for task in tasks
) <= capacities[resource]
Assignment Constraints
Ensure each item is assigned exactly once:
# Each job assigned to exactly one machine
for job in jobs:
model.constraints += quicksum(
assignment[job, machine]
for machine in machines
) == 1
# Each machine gets at most one job
for machine in machines:
model.constraints += quicksum(
assignment[job, machine]
for job in jobs
) <= 1
Flow Conservation
Balance incoming and outgoing flows:
# For each node (except source and sink)
for node in intermediate_nodes:
incoming = quicksum(flow[i, node] for i in predecessors[node])
outgoing = quicksum(flow[node, j] for j in successors[node])
model.constraints += incoming == outgoing
Logical Constraints
Express logical relationships:
# If x is selected, then y must be selected
# x <= y (if x=1, then y must be 1)
model.constraints += x <= y
# At most one of x, y, z can be selected
model.constraints += x + y + z <= 1
# If x is selected, y cannot be selected (mutual exclusion)
model.constraints += x + y <= 1
# Implication: x=1 implies y=1
# Equivalent to: if x then y
model.constraints += y >= x
Bounds via Constraints
Express variable bounds as constraints:
# Lower bound
model.constraints += x >= 0
# Upper bound
model.constraints += x <= 10
# Range
model.constraints += x >= 0
model.constraints += x <= 10
# Note: Prefer using variable bounds when possible
x = model.add_variable("x", vtype=Vtype.INTEGER, lower=0, upper=10)
Constraint Properties
Accessing Components
constraint = 3 * x + 2 * y <= 10
# Left-hand side expression
lhs = constraint.lhs
print(lhs) # 3*x + 2*y
# Comparator/Sense
sense = constraint.sense
# Right-hand side value
rhs = constraint.rhs
print(rhs) # 10
# Constraint name (if set)
name = constraint.name
print(name)
Checking Feasibility
Check if a solution satisfies a constraint:
from luna_model import Model, Variable, Environment, Solution
with Environment():
x = Variable("x")
y = Variable("y")
constraint = x + y <= 5
# Create model and add constraint
model = Model()
model.add_constraint(constraint, name="sum_limit")
# Evaluate constraint with a solution
solution = Solution(samples=[{"x": 2, "y": 3}])
result = model.evaluate(solution)
is_satisfied = result.feasible[0]
print(f"Constraint satisfied: {is_satisfied}")
Advanced Constraints
Indicator Constraints
Use binary variables to activate constraints:
# z=1 implies x + y <= 5
# Reformulation: x + y <= 5 + M(1-z)
# where M is a large constant
M = 1000 # Big-M value
model.constraints += x + y <= 5 + M * (1 - z)
# When z=1: x + y <= 5 (constraint active)
# When z=0: x + y <= 1005 (constraint inactive)
Piecewise Linear Constraints
Model piecewise linear functions using auxiliary variables:
# Model y = max(0, x)
# Using auxiliary variables and constraints
y = model.add_variable("y", vtype=Vtype.REAL, lower=0)
x = model.add_variable("x", vtype=Vtype.REAL)
model.constraints += y >= 0
model.constraints += y >= x
SOS Constraints
Special Ordered Sets (when supported by solver):
# SOS Type 1: At most one variable can be non-zero
# Model using binary indicators
indicators = [model.add_variable(f"ind_{i}", vtype=Vtype.BINARY) for i in range(n)]
model.constraints += quicksum(indicators) <= 1
for i in range(n):
model.constraints += variables[i] <= M * indicators[i]
Best Practices
Naming
Give constraints meaningful names for debugging:
# Good: descriptive names
model.add_constraint(
quicksum(items[i] for i in range(n)) <= capacity,
name="warehouse_capacity"
)
# Avoid: generic names
model.add_constraint(x + y <= 5, name="c1")
Organization
Group related constraints:
# Capacity constraints
for resource in resources:
model.add_constraint(
quicksum(usage[resource, task] * task_vars[task] for task in tasks) <= cap[resource],
name=f"capacity_{resource}"
)
# Assignment constraints
for job in jobs:
model.add_constraint(
quicksum(assignment[job, m] for m in machines) == 1,
name=f"assign_job_{job}"
)
Performance
-
Avoid redundant constraints:
-
Use variable bounds instead of simple constraints:
-
Tighten constraint formulations:
Numerical Stability
-
Avoid very large or very small coefficients:
-
Keep Big-M values reasonable:
Constraint Violations
When a solution violates constraints, identify which ones:
# Check all constraints
for i, constraint in enumerate(model.constraints):
if not constraint.check(solution.samples[0]):
print(f"Constraint {i} violated: {constraint}")
Next Steps
- Solutions - Learn how to work with solutions and check feasibility
- API Reference: Constraint - Detailed API documentation