Skip to main content

Constraints

Constraints define the feasible region for your optimization problem. They specify conditions that must be satisfied by the solution.

Constraint Declaration

Constraints are declared in the component constructor using add_constraint:

self.add_constraint(name, value=None, lower=0.0, upper=0.0, 
shape=None, units=None, label=None)

Equality Constraints

Equality constraints enforce that a function equals a specific value. In Amigo, this is specified by setting lower and upper to the same value:

# h(x) = 0
self.add_constraint("balance", value=0.0, lower=0.0, upper=0.0)

The value parameter provides the target value, while lower and upper being equal enforces the equality.

In the compute() method, set the constraint value:

def compute(self):
x = self.inputs["x"]

# Constraint: x^2 - 1 = 0
self.constraints["balance"] = x**2 - 1.0
note

By default, add_constraint creates an equality constraint with lower=0.0 and upper=0.0.

Inequality Constraints

One-Sided Inequalities

Upper bound constraint (g(x) ≤ 0):

# Declare: stress ≤ σ_yield  →  stress - σ_yield ≤ 0
self.add_constraint("stress", upper=0.0)

# Compute
def compute(self):
stress = self.compute_stress()
sigma_y = self.constants["sigma_y"]
self.constraints["stress"] = stress - sigma_y

Lower bound constraint (g(x) ≥ 0):

# Declare: clearance ≥ 0
self.add_constraint("clearance", lower=0.0, upper=float("inf"))

# Compute
def compute(self):
gap = self.compute_gap()
self.constraints["clearance"] = gap

Bounded Inequalities

Double-sided constraints (lower ≤ g(x) ≤ upper):

self.add_constraint("temperature", lower=20.0, upper=100.0)

def compute(self):
T = self.compute_temperature()
self.constraints["temperature"] = T

Vector Constraints

Define multiple constraints simultaneously using the shape parameter:

# System of 4 equality constraints
self.add_constraint("residual", shape=(4,), value=0.0,
lower=0.0, upper=0.0)

Set vector constraint values:

def compute(self):
# Compute 4 residual equations
res = [None] * 4
res[0] = self.inputs["q"][0] - self.inputs["qdot"][0]
res[1] = self.inputs["q"][1] - self.inputs["qdot"][1]
res[2] = ...
res[3] = ...

self.constraints["residual"] = res

Example: Structural Component

A complete example with multiple constraint types:

import amigo as am

class BeamComponent(am.Component):
def __init__(self):
super().__init__()

# Constants
self.add_constant("sigma_y", value=250e6) # Yield strength [Pa]
self.add_constant("E", value=200e9) # Young's modulus [Pa]

# Inputs
self.add_input("force", value=1000.0, units="N")
self.add_input("area", value=0.01, lower=0.001, upper=0.1, units="m^2")

# Constraints
self.add_constraint("stress_limit", upper=0.0) # σ ≤ σ_y
self.add_constraint("min_thickness", lower=0.0) # t ≥ t_min

# Objective
self.add_objective("mass", units="kg")

def compute(self):
F = self.inputs["force"]
A = self.inputs["area"]
sigma_y = self.constants["sigma_y"]

# Compute stress
stress = F / A

# Stress constraint: σ - σ_y ≤ 0
self.constraints["stress_limit"] = stress - sigma_y

# Minimum thickness constraint: A - A_min ≥ 0
self.constraints["min_thickness"] = A - 0.001

# Minimize mass (proportional to area)
self.objective["mass"] = A * 7850.0 * 1.0 # ρ * A * L

Constraint Linking

When constraints from different components are linked, their values are summed:

# In component 1
self.constraints["total_force"] = force1

# In component 2
self.constraints["total_force"] = force2

# After linking: total_force = force1 + force2
model.link("comp1.total_force", "comp2.total_force")

This is useful for enforcing global constraints across multiple components.