constraint
Specialized constraint types for trajectory optimization.
This module provides advanced constraint specification mechanisms that extend the basic Equality and Inequality constraints. These specialized constraint types enable precise control over when and how constraints are enforced in discretized trajectory optimization problems.
Key constraint types
- NodalConstraint: Enforces constraints only at specific discrete time points (nodes) along the trajectory. Useful for waypoint constraints, boundary conditions, and reducing computational cost by selective enforcement.
- CTCS (Continuous-Time Constraint Satisfaction): Guarantees strict constraint satisfaction throughout the entire continuous trajectory, not just at discrete nodes. Works by augmenting the state vector with additional states whose dynamics integrate constraint violation penalties. Essential for safety-critical applications where inter-node violations could be catastrophic.
Example
Nodal constraints for waypoints::
import openscvx as ox
x = ox.State("x", shape=(3,))
target = [10, 5, 0]
# Enforce position constraint only at specific nodes
waypoint_constraint = (x == target).at([0, 10, 20])
Continuous-time constraint for obstacle avoidance::
obstacle_center = ox.Parameter("obs", shape=(2,), value=[5, 5])
obstacle_radius = 2.0
# Distance from obstacle must be > radius for ALL time
distance = ox.Norm(x[:2] - obstacle_center)
safety_constraint = (distance >= obstacle_radius).over((0, 100))
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:
- Each constraint (in canonical form: lhs <= 0) is wrapped in a penalty function
- 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
- Each augmented state is constrained: s_aug_i(t) = 0 for all t (strictly enforced)
- 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
idxparameter 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
]
Source code in openscvx/symbolic/expr/constraint.py
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 | |
_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 |
Source code in openscvx/symbolic/expr/constraint.py
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 |
Source code in openscvx/symbolic/expr/constraint.py
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 |
Source code in openscvx/symbolic/expr/constraint.py
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))
Source code in openscvx/symbolic/expr/constraint.py
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.
Source code in openscvx/symbolic/expr/constraint.py
Constraint
¶
Bases: Expr
Abstract base class for optimization constraints.
Constraints represent relationships between expressions that must be satisfied in the optimization problem. This base class provides common functionality for both equality and inequality constraints.
Attributes:
| Name | Type | Description |
|---|---|---|
lhs |
Left-hand side expression |
|
rhs |
Right-hand side expression |
|
is_convex |
Flag indicating if the constraint is known to be convex |
Note
Constraints are canonicalized to standard form: (lhs - rhs) {op} 0
Source code in openscvx/symbolic/expr/constraint.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
at(nodes: Union[list, tuple])
¶
Apply this constraint only at specific discrete nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
Union[list, tuple]
|
List of node indices where the constraint should be enforced |
required |
Returns:
| Type | Description |
|---|---|
|
NodalConstraint wrapping this constraint with node specification |
Source code in openscvx/symbolic/expr/constraint.py
canonicalize() -> Expr
¶
Canonicalize constraint to standard form: (lhs - rhs) {op} 0.
This works for both Equality and Inequality by using type(self) to construct the appropriate subclass type.
Source code in openscvx/symbolic/expr/constraint.py
check_shape() -> Tuple[int, ...]
¶
Check that constraint operands are broadcastable. Returns scalar shape.
Source code in openscvx/symbolic/expr/constraint.py
convex() -> Constraint
¶
Mark this constraint as convex for CVXPy lowering.
Returns:
| Type | Description |
|---|---|
Constraint
|
Self with convex flag set to True (enables method chaining) |
over(interval: tuple[int, int], penalty: str = 'squared_relu', idx: Optional[int] = None, check_nodally: bool = False)
¶
Apply this constraint over a continuous interval using CTCS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
tuple[int, int]
|
Tuple of (start, end) node indices for the continuous interval |
required |
penalty
|
str
|
Penalty function type ("squared_relu", "huber", "smooth_relu") |
'squared_relu'
|
idx
|
Optional[int]
|
Optional grouping index for multiple augmented states |
None
|
check_nodally
|
bool
|
Whether to also enforce this constraint nodally |
False
|
Returns:
| Type | Description |
|---|---|
|
CTCS constraint wrapping this constraint with interval specification |
Source code in openscvx/symbolic/expr/constraint.py
CrossNodeConstraint
¶
Bases: Expr
A constraint that couples specific trajectory nodes via .at(k) references.
Unlike NodalConstraint which applies a constraint pattern at multiple nodes (via vmapping), CrossNodeConstraint is a single constraint with fixed node indices embedded in the expression via NodeReference nodes.
CrossNodeConstraint is created automatically when a bare Constraint contains NodeReference nodes (from .at(k) calls). Users should NOT manually wrap cross-node constraints - they are auto-detected during constraint separation.
Key differences from NodalConstraint:
- NodalConstraint: Same constraint evaluated at multiple nodes via vmapping. Signature: (x, u, node, params) → scalar, vmapped to (N, n_x) inputs.
- CrossNodeConstraint: Single constraint coupling specific fixed nodes. Signature: (X, U, params) → scalar, operates on full trajectory arrays.
Lowering:
- Non-convex: Lowered to JAX with automatic differentiation for SCP linearization
- Convex: Lowered to CVXPy and solved directly by the convex solver
Attributes:
| Name | Type | Description |
|---|---|---|
constraint |
The wrapped Constraint containing NodeReference nodes |
Example
Rate limit constraint (auto-detected as CrossNodeConstraint):
position = State("pos", shape=(3,))
# This creates a CrossNodeConstraint automatically:
rate_limit = position.at(5) - position.at(4) <= 0.1
# Mark as convex if the constraint is convex:
rate_limit_convex = (position.at(5) - position.at(4) <= 0.1).convex()
Creating multiple cross-node constraints with a loop:
constraints = []
for k in range(1, N):
# Each iteration creates one CrossNodeConstraint
rate_limit = position.at(k) - position.at(k-1) <= max_step
constraints.append(rate_limit)
Note
Do NOT use .at([...]) on cross-node constraints. The nodes are already specified via .at(k) inside the expression. Using .at([...]) will raise an error during constraint separation.
Source code in openscvx/symbolic/expr/constraint.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | |
is_convex: bool
property
¶
Whether the underlying constraint is marked as convex.
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if the constraint is convex, False otherwise |
canonicalize() -> Expr
¶
Canonicalize the wrapped constraint.
Returns:
| Name | Type | Description |
|---|---|---|
CrossNodeConstraint |
Expr
|
A new CrossNodeConstraint with canonicalized inner constraint |
Source code in openscvx/symbolic/expr/constraint.py
check_shape() -> Tuple[int, ...]
¶
Validate the wrapped constraint's shape.
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() -> CrossNodeConstraint
¶
Mark the underlying constraint as convex for CVXPy lowering.
Returns:
| Type | Description |
|---|---|
CrossNodeConstraint
|
Self with underlying constraint's convex flag set to True |
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))
Source code in openscvx/symbolic/expr/constraint.py
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))
Source code in openscvx/symbolic/expr/constraint.py
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)))
Source code in openscvx/symbolic/expr/constraint.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | |
_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 |
Source code in openscvx/symbolic/expr/constraint.py
canonicalize() -> Expr
¶
Canonicalize the wrapped constraint while preserving node specification.
Returns:
| Name | Type | Description |
|---|---|---|
NodalConstraint |
Expr
|
A new NodalConstraint with canonicalized inner constraint |
Source code in openscvx/symbolic/expr/constraint.py
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 |
Source code in openscvx/symbolic/expr/constraint.py
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()
Source code in openscvx/symbolic/expr/constraint.py
ctcs(constraint: Constraint, penalty: str = 'squared_relu', nodes: Optional[Tuple[int, int]] = None, idx: Optional[int] = None, check_nodally: bool = False) -> CTCS
¶
Helper function to create CTCS (Continuous-Time Constraint Satisfaction) constraints.
This is a convenience function that creates a CTCS constraint with the same parameters as the CTCS constructor. Useful for functional-style constraint building.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraint
|
Constraint
|
The Constraint to enforce continuously |
required |
penalty
|
str
|
Penalty function type ('squared_relu', 'huber', or 'smooth_relu'). Defaults to 'squared_relu'. |
'squared_relu'
|
nodes
|
Optional[Tuple[int, int]]
|
Optional (start, end) tuple of node indices for enforcement interval. None enforces over entire trajectory. |
None
|
idx
|
Optional[int]
|
Optional grouping index for multiple augmented states |
None
|
check_nodally
|
bool
|
Whether to also enforce constraint at discrete nodes. Defaults to False. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
CTCS |
CTCS
|
A CTCS constraint wrapping the input constraint |
Example
Using the helper function:
from openscvx.symbolic.expr.constraint import ctcs
altitude_constraint = ctcs(
altitude >= 10,
penalty="huber",
nodes=(0, 100),
check_nodally=True
)
Equivalent to using CTCS constructor:
altitude_constraint = CTCS(altitude >= 10, penalty="huber", nodes=(0, 100))
Also equivalent to using .over() method on constraint:
altitude_constraint = (altitude >= 10).over((0, 100), penalty="huber")