Skip to content

Constraints

Constraints in openscvx are created using symbolic expressions with comparison operators (==, <=, >=). By default, constraints are enforced at discrete nodes along the trajectory (nodal constraints). The symbolic expression system provides two specialized constraint wrappers for precise control over when and how constraints are enforced.

Basic Constraints

All basic constraints are automatically enforced at all discrete nodes unless wrapped with .at() or .over().

Equality

openscvx.symbolic.expr.constraint.Equality

Bases: Constraint

Equality constraint for optimization problems.

Represents an equality constraint: lhs == rhs. Can be created using the == operator on Expr objects.

Example

Define an Equality constraint:

x = ox.State("x", shape=(3,))
constraint = x == 0  # Creates Equality(x, Constant(0))

Inequality

openscvx.symbolic.expr.constraint.Inequality

Bases: Constraint

Inequality constraint for optimization problems.

Represents an inequality constraint: lhs <= rhs. Can be created using the <= operator on Expr objects.

Example

Define an Inequality constraint:

x = ox.State("x", shape=(3,))
constraint = x <= 10  # Creates Inequality(x, Constant(10))

Specialized Constraint Wrappers

NodalConstraint

NodalConstraint allows selective enforcement of constraints at specific time points (nodes) in a discretized trajectory. Created using the .at() method on constraints. Note: Bare constraints without .at() or .over() are automatically converted to NodalConstraints applied at all nodes.

openscvx.symbolic.expr.constraint.NodalConstraint

Bases: Expr

Wrapper for constraints enforced only at specific discrete trajectory nodes.

NodalConstraint allows selective enforcement of constraints at specific time points (nodes) in a discretized trajectory, rather than enforcing them at every node. This is useful for:

  • Specifying waypoint constraints (e.g., pass through point X at node 10)
  • Boundary conditions at non-standard locations
  • Reducing computational cost by checking constraints less frequently
  • Enforcing periodic constraints (e.g., every 5th node)

The wrapper maintains clean separation between the constraint's mathematical definition and the specification of where it should be applied during optimization.

Note

Bare Constraint objects (without .at() or .over()) are automatically converted to NodalConstraints applied at all nodes during preprocessing.

Attributes:

Name Type Description
constraint

The wrapped Constraint (Equality or Inequality) to enforce

nodes

List of integer node indices where the constraint is enforced

Example

Enforce position constraint only at nodes 0, 10, and 20:

x = State("x", shape=(3,))
target = [10, 5, 0]
constraint = (x == target).at([0, 10, 20])

Equivalent using NodalConstraint directly:

constraint = NodalConstraint(x == target, nodes=[0, 10, 20])

Periodic constraint enforcement (every 10th node):

velocity_limit = (vel <= 100).at(list(range(0, 100, 10)))

Bare constraints are automatically applied at all nodes. These are equivalent:

constraint1 = vel <= 100  # Auto-converted to all nodes
constraint2 = (vel <= 100).at(list(range(n_nodes)))
_hash_into(hasher: hashlib._Hash) -> None

Hash NodalConstraint including its node list.

Parameters:

Name Type Description Default
hasher _Hash

A hashlib hash object to update

required
canonicalize() -> Expr

Canonicalize the wrapped constraint while preserving node specification.

Returns:

Name Type Description
NodalConstraint Expr

A new NodalConstraint with canonicalized inner constraint

check_shape() -> Tuple[int, ...]

Validate the wrapped constraint's shape.

NodalConstraint wraps a constraint without changing its computational meaning, only specifying where it should be applied. Like all constraints, it produces a scalar result.

Returns:

Name Type Description
tuple Tuple[int, ...]

Empty tuple () representing scalar shape

children()

Return the wrapped constraint as the only child.

Returns:

Name Type Description
list

Single-element list containing the wrapped constraint

convex() -> NodalConstraint

Mark the underlying constraint as convex for CVXPy lowering.

Returns:

Type Description
NodalConstraint

Self with underlying constraint's convex flag set to True (enables method chaining)

Example

Mark a constraint as convex: constraint = (x <= 10).at([0, 5, 10]).convex()

CTCS (Continuous-Time Constraint Satisfaction)

CTCS guarantees strict constraint satisfaction throughout the entire continuous trajectory, not just at discrete nodes. It works by augmenting the state vector with additional states whose dynamics integrate constraint violation penalties. Created using the .over() method on constraints.

openscvx.symbolic.expr.constraint.CTCS

Bases: Expr

Continuous-Time Constraint Satisfaction using augmented state dynamics.

CTCS enables strict continuous-time constraint enforcement in discretized trajectory optimization by augmenting the state vector with additional states whose dynamics are the constraint violation penalties. By constraining these augmented states to remain at zero throughout the trajectory, the original constraints are guaranteed to be satisfied continuously, not just at discrete nodes.

How it works:

  1. Each constraint (in canonical form: lhs <= 0) is wrapped in a penalty function
  2. Augmented states s_aug_i are added with dynamics: ds_aug_i/dt = sum(penalty_j(lhs_j)) for all CTCS constraints j in group i
  3. Each augmented state is constrained: s_aug_i(t) = 0 for all t (strictly enforced)
  4. Since s_aug_i integrates the penalties, s_aug_i = 0 implies all penalties in the group are zero, which means all constraints in the group are satisfied continuously

Grouping and augmented states:

  • CTCS constraints with the same node interval are grouped into a single augmented state by default (their penalties are summed)
  • CTCS constraints with different node intervals create separate augmented states
  • Using the idx parameter explicitly assigns constraints to specific augmented states, allowing manual control over grouping
  • Each unique group creates one augmented state named _ctcs_aug_0, _ctcs_aug_1, etc.

This is particularly useful for:

  • Path constraints that must hold throughout the entire trajectory (not just at nodes)
  • Obstacle avoidance where constraint violation between nodes could be catastrophic
  • State limits that should be respected continuously (e.g., altitude > 0 for aircraft)
  • Ensuring smooth, feasible trajectories between discretization points

Penalty functions (applied to constraint violations):

  • squared_relu: Square(PositivePart(lhs)) - smooth, differentiable (default)
  • huber: Huber(PositivePart(lhs)) - less sensitive to outliers than squared
  • smooth_relu: SmoothReLU(lhs) - smooth approximation of ReLU

Attributes:

Name Type Description
constraint

The wrapped Constraint (typically Inequality) to enforce continuously

penalty

Penalty function type ('squared_relu', 'huber', or 'smooth_relu')

nodes

Optional (start, end) tuple specifying the interval for enforcement, or None to enforce over the entire trajectory

idx

Optional grouping index for managing multiple augmented states. CTCS constraints with the same idx and nodes are grouped together, sharing an augmented state. If None, auto-assigned based on node intervals.

check_nodally

Whether to also enforce the constraint at discrete nodes for additional numerical robustness (creates both continuous and nodal constraints)

Example

Single augmented state (default behavior - same node interval):

altitude = State("alt", shape=(1,))
constraints = [
    (altitude >= 10).over((0, 10)),  # Both constraints share
    (altitude <= 1000).over((0, 10))  # one augmented state
]

Multiple augmented states (different node intervals):

constraints = [
    (altitude >= 10).over((0, 5)),  # Creates _ctcs_aug_0
    (altitude >= 20).over((5, 10))  # Creates _ctcs_aug_1
]

Manual grouping with idx parameter:

constraints = [
    (altitude >= 10).over((0, 10), idx=0),    # Group 0
    (velocity <= 100).over((0, 10), idx=1),   # Group 1 (separate state)
    (altitude <= 1000).over((0, 10), idx=0)   # Also group 0
]
_hash_into(hasher: hashlib._Hash) -> None

Hash CTCS including all its parameters.

Parameters:

Name Type Description Default
hasher _Hash

A hashlib hash object to update

required
canonicalize() -> Expr

Canonicalize the inner constraint while preserving CTCS parameters.

Returns:

Name Type Description
CTCS Expr

A new CTCS with canonicalized inner constraint and same parameters

check_shape() -> Tuple[int, ...]

Validate the constraint and penalty expression shapes.

CTCS transforms the wrapped constraint into a penalty expression that is summed (integrated) over the trajectory, always producing a scalar result.

Returns:

Name Type Description
tuple Tuple[int, ...]

Empty tuple () representing scalar shape

Raises:

Type Description
ValueError

If the wrapped constraint has invalid shape

ValueError

If the generated penalty expression is not scalar

children()

Return the wrapped constraint as the only child.

Returns:

Name Type Description
list

Single-element list containing the wrapped constraint

over(interval: tuple[int, int]) -> CTCS

Set or update the continuous interval for this CTCS constraint.

Parameters:

Name Type Description Default
interval tuple[int, int]

Tuple of (start, end) node indices defining the enforcement interval

required

Returns:

Name Type Description
CTCS CTCS

New CTCS constraint with the specified interval

Example

Define constraint over range:

constraint = (altitude >= 10).over((0, 50))

Update interval to cover different range:

constraint_updated = constraint.over((50, 100))
penalty_expr() -> Expr

Build the penalty expression for this CTCS constraint.

Transforms the constraint's left-hand side (in canonical form: lhs <= 0) into a penalty expression using the specified penalty function. The penalty is zero when the constraint is satisfied and positive when violated.

This penalty expression becomes part of the dynamics of an augmented state. Multiple CTCS constraints in the same group (same idx) have their penalties summed: ds_aug_i/dt = sum(penalty_j) for all j in group i. By constraining s_aug_i(t) = 0 for all t, we ensure all penalties in the group are zero, which strictly enforces all constraints in the group continuously.

Returns:

Name Type Description
Expr Expr

Sum of the penalty function applied to the constraint violation

Raises:

Type Description
ValueError

If an unknown penalty type is specified

Note

This method is used internally during problem compilation to create augmented state dynamics. Multiple penalty expressions with the same idx are summed together before being added to the dynamics vector via Concat.