Skip to content

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:

  1. Left-hand side (LHS): An Expression
  2. Comparator: One of ==, <=, or >=
  3. 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:

Python
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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
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:

Python
# 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:

Python
# 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

Python
# 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

Python
# 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

Python
# Remove by name
del model.constraints["old_constraint"]

# Clear all constraints
model.constraints.clear()

Common Constraint Patterns

Capacity Constraints

Limit resource usage:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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

Python
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:

Python
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:

Python
# 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:

Python
# 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):

Python
# 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:

Python
# 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:

Python
# 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

  1. Avoid redundant constraints:

    Python
    # Redundant
    model.constraints += x + y <= 10
    model.constraints += x + y <= 5  # This makes the first unnecessary
    
    # Better
    model.constraints += x + y <= 5
    

  2. Use variable bounds instead of simple constraints:

    Python
    # Less efficient - creating variable then adding bound constraints
    model = Model()
    x = model.add_variable("x")
    model.constraints += x >= 0
    model.constraints += x <= 10
    
    # Better - specify bounds during variable creation
    x = model.add_variable("x", vtype=Vtype.INTEGER, lower=0, upper=10)
    

  3. Tighten constraint formulations:

    Python
    # Weak formulation
    model.constraints += x + y <= 100 * z  # z is binary
    
    # Tighter formulation (if you know x, y <= 10)
    model.constraints += x + y <= 10 * z
    

Numerical Stability

  1. Avoid very large or very small coefficients:

    Python
    # Problematic
    model.constraints += 0.000001 * x + 1000000 * y <= 1
    
    # Better: scale coefficients
    model.constraints += x + 10**12 * y <= 10**6
    

  2. Keep Big-M values reasonable:

    Python
    # Too large (numerical issues)
    M = 10**20
    
    # Better: use smallest M that works
    M = max(upper_bounds)
    

Constraint Violations

When a solution violates constraints, identify which ones:

Python
# 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