Working with Constraints

Advanced constraint patterns and techniques
Published

May 28, 2026

1 Introduction

This tutorial covers advanced constraint techniques:

  • Different constraint types
  • Modeling tricks
  • Handling infeasibility
  • Constraint debugging

2 Constraint Types

2.1 Inequality Constraints

The most common type—limit something to be above or below a threshold:

from optyx import Variable

x = Variable("x")
y = Variable("y")

# Greater-than-or-equal
c1 = x + y >= 10        # x + y ≥ 10

# Less-than-or-equal
c2 = x**2 + y**2 <= 25  # x² + y² ≤ 25

print(f"c1 sense: {c1.sense}")
print(f"c2 sense: {c2.sense}")
c1 sense: >=
c2 sense: <=

2.2 Equality Constraints

When something must be exactly equal:

from optyx import Variable

x = Variable("x")
y = Variable("y")

# Use .eq() for equality
c = (x + y).eq(1)  # x + y = 1

print(f"Equality sense: {c.sense}")
Equality sense: ==
Important

Don’t use == for constraints! Python’s == returns a boolean, not a constraint. Always use .eq().


3 Bound Constraints

For simple variable bounds, prefer using variable bounds over explicit constraints:

from optyx import Variable, Problem

# Preferred: bounds on variable definition
x = Variable("x", lb=0, ub=10)

# Less efficient: as explicit constraints
y = Variable("y")

prob = (
    Problem()
    .minimize(x**2 + y**2)
    .subject_to(y >= 0)
    .subject_to(y <= 10)
    .solve()
)

print(f"x* = {prob['x']:.2f}, y* = {prob['y']:.2f}")
x* = 0.00, y* = 0.00

Why prefer variable bounds? - Solver handles them more efficiently - Cleaner problem formulation - Fewer constraint evaluations


4 Modeling Techniques

4.1 Sum Constraints

Ensure quantities add up:

from optyx import Variable, Problem

# Portfolio weights
w1 = Variable("stocks", lb=0, ub=1)
w2 = Variable("bonds", lb=0, ub=1)
w3 = Variable("cash", lb=0, ub=1)

# Weights must sum to 1 (with small tolerance for numerical stability)
sol = (
    Problem()
    .minimize(w1**2 + w2**2 + w3**2)  # Minimize concentration
    .subject_to(w1 + w2 + w3 >= 0.99)
    .subject_to(w1 + w2 + w3 <= 1.01)
    .solve()
)

total = sol['stocks'] + sol['bonds'] + sol['cash']
print(f"Sum of weights: {total:.4f}")
Sum of weights: 0.9900

4.2 Ratio Constraints

Control proportions:

from optyx import Variable, Problem

x = Variable("x", lb=0.1)  # Avoid division by zero
y = Variable("y", lb=0.1)

# y should be at least twice x
# y/x >= 2  →  y >= 2x
sol = (
    Problem()
    .minimize(x + y)
    .subject_to(y >= 2*x)
    .subject_to(x + y >= 10)
    .solve()
)

print(f"x = {sol['x']:.2f}, y = {sol['y']:.2f}")
print(f"Ratio y/x = {sol['y']/sol['x']:.2f}")
x = 0.10, y = 9.90
Ratio y/x = 99.00

4.3 Nonlinear Constraints

Optyx handles nonlinear constraints:

from optyx import Variable, Problem, sqrt

x = Variable("x")
y = Variable("y")

# Point must be inside a circle
sol = (
    Problem()
    .minimize((x - 3)**2 + (y - 4)**2)  # Closest to (3, 4)
    .subject_to(x**2 + y**2 <= 4)        # Inside circle of radius 2
    .solve()
)

dist_from_origin = sqrt(sol['x']**2 + sol['y']**2).evaluate({'x': sol['x'], 'y': sol['y']})
print(f"Point: ({sol['x']:.3f}, {sol['y']:.3f})")
print(f"Distance from origin: {dist_from_origin:.3f}")
Point: (1.200, 1.600)
Distance from origin: 2.000

4.4 Range Constraints (Between)

If a variable or expression must be strictly bounded between two values, you can use .between(lb, ub). This saves you from writing (and evaluating) two separate constraints.

# Instead of: prob.subject_to(x + y >= 5).subject_to(x + y <= 10)
prob.subject_to((x + y).between(5, 10))

5 Handling Infeasibility

5.1 Detecting Infeasible Problems

from optyx import Variable, Problem, SolverStatus

x = Variable("x", lb=0, ub=5)

# Impossible: x >= 10 but x <= 5
sol = (
    Problem()
    .minimize(x)
    .subject_to(x >= 10)
    .solve()
)

print(f"Status: {sol.status}")
if sol.status != SolverStatus.OPTIMAL:
    print(f"Problem: {sol.message}")
Status: SolverStatus.INFEASIBLE
Problem: The problem is infeasible. (HiGHS Status 8: model_status is Infeasible; primal_status is None) No feasible solution exists.

5.2 Soft Constraints with Penalties

When hard constraints might be infeasible, use penalties:

from optyx import Variable, Problem

x = Variable("x", lb=0)
slack = Variable("slack", lb=0)  # Violation amount

# Original constraint: x >= 10
# Soft version: x + slack >= 10, penalize slack

sol = (
    Problem()
    .minimize(x**2 + 1000*slack)  # Large penalty for violation
    .subject_to(x + slack >= 10)
    .subject_to(x <= 5)  # Conflicting constraint
    .solve()
)

print(f"x = {sol['x']:.2f}")
print(f"Constraint violation: {sol['slack']:.2f}")
x = 5.00
Constraint violation: 5.00

6 Multiple Constraints

6.1 Legacy Loops (Loops vs Generators)

For large problems, you can pass Python generator expressions directly into subject_to(). Optyx will unpack and add them efficiently.

from optyx import Problem, VectorVariable

x = VectorVariable("x", 100)
prob = Problem()

# Add 100 constraints in one clean line
prob.subject_to(x[i] >= i for i in range(100))

6.2 Direct Matrix Constraints

When dealing with massive constraint sets (10,000+), looping over components is slow. Use direct matrix constraints through subject_to(...):

from optyx import as_matrix

# Wrap scipy.sparse so A @ x stays symbolic
A_sparse = as_matrix(A_sparse, storage="sparse")
prob.subject_to(A_sparse @ x <= b_vector)
Note

Raw scipy.sparse matrices cannot be used directly on the left side of @ here. SciPy intercepts A @ x first and attempts a numeric sparse multiplication, which fails before Optyx can turn it into a symbolic matrix constraint. as_matrix(...) wraps the sparse matrix in an Optyx matrix object so the symbolic path is used instead.

If you already have a dense array but want explicit storage control, as_matrix() accepts storage="auto", storage="dense", and storage="sparse". storage="auto" keeps sparse inputs sparse and can convert large, low-density dense arrays into CSR storage automatically.

See the Performance Guide for more details.

6.3 Building Constraint Lists

from optyx import Variable, Problem
import numpy as np

n = 3
x = np.array([Variable(f"x_{i}", lb=0) for i in range(n)])

# Resource limits
limits = np.array([100, 80, 60])
usage = np.array([[2, 1, 3], [1, 2, 1], [1, 1, 2]])

prob = Problem().maximize(np.sum(10 * x))

# Add resource constraints using matrix notation
for j in range(len(limits)):
    prob.subject_to(usage[j] @ x <= limits[j])

sol = prob.solve()
for i in range(n):
    print(f"x_{i} = {sol[f'x_{i}']:.2f}")
x_0 = 40.00
x_1 = 20.00
x_2 = 0.00
TipNumPy Matrix Operations

Optyx variables work seamlessly with NumPy arrays. Use np.array([...]) to wrap your variables, then use @ for matrix multiplication and np.sum() for summations.

6.4 Legacy Loops (Loops vs Generators)

For large problems, generate constraints programmatically:

from optyx import Variable, Problem

# Grid of variables
rows, cols = 3, 3
grid = [[Variable(f"x_{i}_{j}", lb=0, ub=9) for j in range(cols)] for i in range(rows)]

prob = Problem().minimize(sum(grid[i][j] for i in range(rows) for j in range(cols)))

# Row sums >= 10
for i in range(rows):
    prob.subject_to(sum(grid[i][j] for j in range(cols)) >= 10)

# Column sums >= 10
for j in range(cols):
    prob.subject_to(sum(grid[i][j] for i in range(rows)) >= 10)

sol = prob.solve()

print("Grid solution:")
for i in range(rows):
    row = [f"{sol[f'x_{i}_{j}']:.1f}" for j in range(cols)]
    print(f"  {row}")
Grid solution:
  ['8.0', '1.0', '1.0']
  ['1.0', '9.0', '0.0']
  ['1.0', '0.0', '9.0']

7 Debugging Constraints

7.1 Check Constraint Values

from optyx import Variable, Problem

x = Variable("x", lb=0)
y = Variable("y", lb=0)

c1 = x + y >= 5
c2 = x - y <= 2
c3 = 2*x + 3*y <= 20

sol = (
    Problem()
    .minimize(x + y)
    .subject_to(c1)
    .subject_to(c2)
    .subject_to(c3)
    .solve()
)

# Evaluate constraint expressions at solution
vals = {'x': sol['x'], 'y': sol['y']}

print("Constraint values at solution:")
print(f"  x + y = {(x + y).evaluate(vals):.2f} (need >= 5)")
print(f"  x - y = {(x - y).evaluate(vals):.2f} (need <= 2)")
print(f"  2x + 3y = {(2*x + 3*y).evaluate(vals):.2f} (need <= 20)")
Constraint values at solution:
  x + y = 5.00 (need >= 5)
  x - y = -5.00 (need <= 2)
  2x + 3y = 15.00 (need <= 20)

8 Best Practices

  1. Use variable bounds for simple box constraints
  2. Avoid strict equality when possible (numerical issues)
  3. Scale constraints to similar magnitudes
  4. Check feasibility before solving complex problems
  5. Use meaningful names for debugging

9 Next Steps