augmentation
State and dynamics augmentation for continuous-time constraint satisfaction.
This module provides utilities for augmenting trajectory optimization problems with additional states and dynamics to handle continuous-time constraint satisfaction (CTCS). The CTCS method enforces path constraints continuously along the trajectory rather than just at discretization nodes.
Key functionality
- CTCS constraint grouping: Sort and group CTCS constraints by time intervals
- Constraint separation: Separate CTCS, nodal, and convex constraints
- Vector decomposition: Decompose vector constraints into scalar components
- Time augmentation: Add time state with appropriate dynamics and constraints
- CTCS dynamics augmentation: Add augmented states and time dilation control
The augmentation process transforms the original dynamics x_dot = f(x, u) into an augmented system with additional states for constraint satisfaction and time dilation.
Architecture
The CTCS method works by:
- Grouping constraints by time interval and assigning index (idx)
- Creating augmented states (one per constraint group)
- Adding penalty dynamics: aug_dot = penalty(constraint_violation)
- Adding time dilation control to slow down near constraint boundaries
Example
Augmenting dynamics with CTCS constraints::
import openscvx as ox
# Define problem
x = ox.State("x", shape=(3,))
u = ox.Control("u", shape=(2,))
# Create dynamics
xdot = u @ A # Some dynamics expression
# Define path constraint
path_constraint = (ox.Norm(x) <= 1.0).over((0, 50)) # CTCS constraint
# Augment dynamics with CTCS
from openscvx.symbolic.augmentation import augment_dynamics_with_ctcs
xdot_aug, states_aug, controls_aug = augment_dynamics_with_ctcs(
xdot=xdot,
states=[x],
controls=[u],
constraints_ctcs=[path_constraint],
N=50
)
# xdot_aug now includes augmented state dynamics
# states_aug includes original states + augmented states
# controls_aug includes original controls + time dilation
augment_dynamics_with_ctcs(xdot: Expr, states: List[State], controls: List[Control], constraints_ctcs: List[CTCS], N: int, licq_min: float = 0.0, licq_max: float = 0.0001, time_dilation_factor_min: float = 0.3, time_dilation_factor_max: float = 3.0) -> Tuple[Expr, List[State], List[Control]]
¶
Augment dynamics with continuous-time constraint satisfaction states.
Implements the CTCS method by adding augmented states and time dilation control to the original dynamics. For each group of CTCS constraints, an augmented state is created whose dynamics are the penalty function of constraint violations.
The CTCS method enforces path constraints continuously by: 1. Creating augmented states with dynamics = penalty(constraint_violation) 2. Constraining augmented states to stay near zero (LICQ condition) 3. Adding time dilation control to slow down near constraint boundaries
The augmented dynamics become
x_dot = f(x, u) aug_dot = penalty(g(x, u)) # For each constraint group time_dot = time_dilation
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xdot
|
Expr
|
Original dynamics expression for states |
required |
states
|
List[State]
|
List of state variables (must include a state named "time") |
required |
controls
|
List[Control]
|
List of control variables |
required |
constraints_ctcs
|
List[CTCS]
|
List of CTCS constraints (should be sorted and grouped) |
required |
N
|
int
|
Number of discretization nodes |
required |
licq_min
|
float
|
Minimum bound for augmented states (default: 0.0) |
0.0
|
licq_max
|
float
|
Maximum bound for augmented states (default: 1e-4) |
0.0001
|
time_dilation_factor_min
|
float
|
Minimum time dilation factor (default: 0.3) |
0.3
|
time_dilation_factor_max
|
float
|
Maximum time dilation factor (default: 3.0) |
3.0
|
Returns:
| Type | Description |
|---|---|
Tuple[Expr, List[State], List[Control]]
|
Tuple of: - Augmented dynamics expression (original + augmented state dynamics) - Updated states list (original + augmented states) - Updated controls list (original + time dilation control) |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no state named "time" is found in the states list |
Example
Augment dynamics with CTCS penalty states:
x = ox.State("x", shape=(3,))
u = ox.Control("u", shape=(2,))
time = ox.State("time", shape=(1,))
xdot = u @ A # Some dynamics
constraint = (ox.Norm(x) <= 1.0).over((0, 50))
xdot_aug, states_aug, controls_aug = augment_dynamics_with_ctcs(
xdot=xdot,
states=[x, time],
controls=[u],
constraints_ctcs=[constraint],
N=50
)
states_aug includes x, time, and _ctcs_aug_0, controls_aug includes u and _time_dilation
Source code in openscvx/symbolic/augmentation.py
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 | |
augment_with_time_state(states: List[State], constraints: ConstraintSet, time_initial: float | tuple, time_final: float | tuple, time_min: float, time_max: float, N: int, time_scaling_min: Optional[float] = None, time_scaling_max: Optional[float] = None) -> Tuple[List[State], ConstraintSet]
¶
Augment problem with a time state variable.
Creates a time state variable if one doesn't already exist and adds it to the states list. Also adds CTCS constraints to enforce time bounds continuously throughout the trajectory.
The time state tracks physical time along the trajectory and is used for time-optimal control problems. Boundary conditions can be fixed values or free variables with initial guesses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
states
|
List[State]
|
List of State objects (will not be modified, copy is returned) |
required |
constraints
|
ConstraintSet
|
ConstraintSet with unsorted constraints (will be modified in place) |
required |
time_initial
|
float | tuple
|
Initial time boundary condition: - float: Fixed initial time - tuple: ("free", guess) for free initial time with initial guess |
required |
time_final
|
float | tuple
|
Final time boundary condition (same format as time_initial) |
required |
time_min
|
float
|
Minimum bound for time variable throughout trajectory |
required |
time_max
|
float
|
Maximum bound for time variable throughout trajectory |
required |
N
|
int
|
Number of discretization nodes (for initial guess generation) |
required |
Returns:
| Type | Description |
|---|---|
Tuple[List[State], ConstraintSet]
|
Tuple of: - Updated states list (original + time state if created) - The same ConstraintSet with time CTCS constraints added to unsorted |
Note
If a state named "time" already exists, it is not modified and no constraints are added.
Example
Get augmented states::
x = ox.State("x", shape=(3,))
constraints = ConstraintSet()
states_aug, constraints = augment_with_time_state(
states=[x],
constraints=constraints,
time_initial=0.0,
time_final=("free", 10.0),
time_min=0.0,
time_max=100.0,
N=50
)
states_aug now includes time state with initial=0, final=free
Source code in openscvx/symbolic/augmentation.py
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 451 452 453 454 455 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 | |
decompose_vector_nodal_constraints(constraints_nodal: List[NodalConstraint]) -> List[NodalConstraint]
¶
Decompose vector-valued nodal constraints into scalar constraints.
Decomposes vector constraints into individual scalar constraints, which is necessary for nonconvex nodal constraints that are lowered to JAX functions. The JAX-to-CVXPY interface expects scalar constraint values at each node.
For example, a constraint with shape (3,) is decomposed into 3 separate scalar constraints using indexing. CTCS constraints don't need decomposition since they handle vector values internally.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraints_nodal
|
List[NodalConstraint]
|
List of NodalConstraint objects (must be canonicalized) |
required |
Returns:
| Type | Description |
|---|---|
List[NodalConstraint]
|
List of NodalConstraint objects with vector constraints decomposed into scalars. |
List[NodalConstraint]
|
Scalar constraints are passed through unchanged. |
Note
Constraints are assumed to be in canonical form: residual <= 0 or residual == 0, where residual is the lhs of the constraint.
Example
Decompose vector constraint into 3 constraints:
x = ox.State("x", shape=(3,))
constraint = (x <= 5).at([0, 10, 20]) # Vector constraint, shape (3,)
decomposed = decompose_vector_nodal_constraints([constraint])
# Returns 3 constraints: x[0] <= 5, x[1] <= 5, x[2] <= 5
Source code in openscvx/symbolic/augmentation.py
get_nodal_constraints_from_ctcs(constraints_ctcs: List[CTCS]) -> List[tuple[Constraint, tuple[int, int]]]
¶
Extract constraints from CTCS wrappers that should be checked nodally.
Some CTCS constraints have the check_nodally flag set, indicating that the underlying constraint should be enforced both continuously (via CTCS) and discretely at the nodes. This function extracts those underlying constraints along with their node intervals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraints_ctcs
|
List[CTCS]
|
List of CTCS constraint wrappers |
required |
Returns:
| Type | Description |
|---|---|
List[tuple[Constraint, tuple[int, int]]]
|
List of tuples (constraint, nodes) where: - constraint: The underlying Constraint object from CTCS with check_nodally=True - nodes: The (start, end) interval from the CTCS wrapper |
Example
Extract CTCS constraint that should also be checked at nodes:
x = ox.State("x", shape=(3,))
constraint = (x <= 5).over((10, 50), check_nodally=True)
nodal = get_nodal_constraints_from_ctcs([constraint])
Returns [(x <= 5, (10, 50))] to be enforced at nodes 10 through 49
Source code in openscvx/symbolic/augmentation.py
separate_constraints(constraint_set: ConstraintSet, n_nodes: int) -> ConstraintSet
¶
Separate and categorize constraints by type and convexity.
Moves constraints from constraint_set.unsorted into their appropriate
category fields (ctcs, nodal, nodal_convex, cross_node, cross_node_convex).
Bare Constraint objects are automatically categorized: - If they contain NodeReferences (from .at(k) calls), they become CrossNodeConstraint - Otherwise, they become NodalConstraint applied at all nodes
Constraints within CTCS wrappers that have check_nodally=True are also extracted and added to the nodal constraint lists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraint_set
|
ConstraintSet
|
ConstraintSet with raw constraints in |
required |
n_nodes
|
int
|
Total number of nodes in the trajectory |
required |
Returns:
| Type | Description |
|---|---|
ConstraintSet
|
The same ConstraintSet with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a constraint is not one of the expected types |
ValueError
|
If a NodalConstraint contains NodeReferences (use bare Constraint instead) |
ValueError
|
If a CTCS constraint contains NodeReferences |
Example
Separate and categorize constraints::
x = ox.State("x", shape=(3,))
constraint_set = ConstraintSet(unsorted=[
(x <= 5).over((0, 50)), # CTCS
(x >= 0).at([0, 10, 20]), # NodalConstraint
ox.Norm(x) <= 1, # Bare -> all nodes
x.at(5) - x.at(4) <= 0.1, # Bare with NodeRef -> cross-node
])
separate_constraints(constraint_set, n_nodes=50)
assert constraint_set.is_categorized
# Access via: constraint_set.ctcs, constraint_set.nodal, etc.
Source code in openscvx/symbolic/augmentation.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 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 | |
sort_ctcs_constraints(constraints_ctcs: List[CTCS]) -> Tuple[List[CTCS], List[Tuple[int, int]], int]
¶
Sort and group CTCS constraints by time interval and assign indices.
Groups CTCS constraints by their time intervals (nodes) and assigns a unique index (idx) to each group. Constraints with the same time interval can share an augmented state (same idx), while constraints with different intervals must have different augmented states.
Grouping rules
- Constraints with the same node interval can share an idx
- Constraints with different node intervals must have different idx values
- idx values must form a contiguous block starting from 0
- Unspecified idx values are automatically assigned
- User-specified idx values are validated for consistency
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraints_ctcs
|
List[CTCS]
|
List of CTCS constraints to sort and group |
required |
Returns:
| Type | Description |
|---|---|
Tuple[List[CTCS], List[Tuple[int, int]], int]
|
Tuple of: - List of CTCS constraints with idx assigned to each - List of node intervals (start, end) in ascending idx order - Number of augmented states needed (number of unique idx values) |
Raises:
| Type | Description |
|---|---|
ValueError
|
If user-specified idx values are inconsistent or non-contiguous |
Example
Sort CTCS constraints by interval and index:
constraint1 = (x <= 5).over((0, 50)) # Auto-assigned idx
constraint2 = (y <= 10).over((0, 50)) # Same interval, same idx
constraint3 = (z <= 15).over((20, 80)) # Different interval, different idx
sorted_ctcs, intervals, n_aug = sort_ctcs_constraints([c1, c2, c3])
# constraint1.idx = 0, constraint2.idx = 0, constraint3.idx = 1
# intervals = [(0, 50), (20, 80)]
# n_aug = 2
Source code in openscvx/symbolic/augmentation.py
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |