Skip to content

symbolic

Symbolic expression system for trajectory optimization.

See openscvx.symbolic.expr for detailed documentation and examples.

ConstraintSet dataclass

Container for categorized symbolic constraints.

This dataclass holds all symbolic constraint types in a structured way, providing type safety and a clear API for accessing constraint categories. This is a pre-lowering container - after lowering, constraints live in LoweredJaxConstraints and LoweredCvxpyConstraints.

The constraint set supports two lifecycle stages:

  1. Before preprocessing: Raw constraints live in unsorted
  2. After preprocessing: unsorted is empty, constraints are categorized

Use is_categorized to check which stage the constraint set is in.

Attributes:

Name Type Description
unsorted List[Union[Constraint, CTCS]]

Raw constraints before categorization. Empty after preprocessing.

ctcs List[CTCS]

CTCS (continuous-time) constraints.

nodal List[NodalConstraint]

Non-convex nodal constraints (will be lowered to JAX).

nodal_convex List[NodalConstraint]

Convex nodal constraints (will be lowered to CVXPy).

cross_node List[CrossNodeConstraint]

Non-convex cross-node constraints (will be lowered to JAX).

cross_node_convex List[CrossNodeConstraint]

Convex cross-node constraints (will be lowered to CVXPy).

Example

Before preprocessing (raw constraints)::

constraints = ConstraintSet(unsorted=[c1, c2, c3])
assert not constraints.is_categorized

After preprocessing (categorized)::

# preprocess_symbolic_problem drains unsorted -> fills categories
assert constraints.is_categorized
for c in constraints.nodal:
    # Process non-convex nodal constraints
    pass
Source code in openscvx/symbolic/constraint_set.py
@dataclass
class ConstraintSet:
    """Container for categorized symbolic constraints.

    This dataclass holds all symbolic constraint types in a structured way,
    providing type safety and a clear API for accessing constraint categories.
    This is a pre-lowering container - after lowering, constraints live in
    LoweredJaxConstraints and LoweredCvxpyConstraints.

    The constraint set supports two lifecycle stages:

    1. **Before preprocessing**: Raw constraints live in `unsorted`
    2. **After preprocessing**: `unsorted` is empty, constraints are categorized

    Use `is_categorized` to check which stage the constraint set is in.

    Attributes:
        unsorted: Raw constraints before categorization. Empty after preprocessing.
        ctcs: CTCS (continuous-time) constraints.
        nodal: Non-convex nodal constraints (will be lowered to JAX).
        nodal_convex: Convex nodal constraints (will be lowered to CVXPy).
        cross_node: Non-convex cross-node constraints (will be lowered to JAX).
        cross_node_convex: Convex cross-node constraints (will be lowered to CVXPy).

    Example:
        Before preprocessing (raw constraints)::

            constraints = ConstraintSet(unsorted=[c1, c2, c3])
            assert not constraints.is_categorized

        After preprocessing (categorized)::

            # preprocess_symbolic_problem drains unsorted -> fills categories
            assert constraints.is_categorized
            for c in constraints.nodal:
                # Process non-convex nodal constraints
                pass
    """

    # Raw constraints before categorization (empty after preprocessing)
    unsorted: List[Union["Constraint", "CTCS"]] = field(default_factory=list)

    # Categorized symbolic constraints (populated by preprocessing)
    ctcs: List["CTCS"] = field(default_factory=list)
    nodal: List["NodalConstraint"] = field(default_factory=list)
    nodal_convex: List["NodalConstraint"] = field(default_factory=list)
    cross_node: List["CrossNodeConstraint"] = field(default_factory=list)
    cross_node_convex: List["CrossNodeConstraint"] = field(default_factory=list)

    @property
    def is_categorized(self) -> bool:
        """True if all constraints have been sorted into categories.

        After preprocessing, `unsorted` should be empty and all constraints
        should be in their appropriate category lists.
        """
        return len(self.unsorted) == 0

    def __bool__(self) -> bool:
        """Return True if any constraint list is non-empty."""
        return bool(
            self.unsorted
            or self.ctcs
            or self.nodal
            or self.nodal_convex
            or self.cross_node
            or self.cross_node_convex
        )

    def __len__(self) -> int:
        """Return total number of constraints across all lists."""
        return (
            len(self.unsorted)
            + len(self.ctcs)
            + len(self.nodal)
            + len(self.nodal_convex)
            + len(self.cross_node)
            + len(self.cross_node_convex)
        )
is_categorized: bool property

True if all constraints have been sorted into categories.

After preprocessing, unsorted should be empty and all constraints should be in their appropriate category lists.

SymbolicProblem dataclass

Container for symbolic problem specification.

This dataclass holds a trajectory optimization problem in symbolic form, either as raw user input or after preprocessing/augmentation. It provides a typed interface for the preprocessing and lowering pipeline.

Lifecycle Stages
  1. Before preprocessing: User creates with raw dynamics, states, controls, and unsorted constraints. Propagation fields are None.
  2. After preprocessing: Dynamics and states are augmented (CTCS, time dilation), constraints are categorized, propagation fields are populated.

Use is_preprocessed to check whether preprocessing has completed.

Attributes:

Name Type Description
dynamics Expr

Symbolic dynamics expression (dx/dt = f(x, u)). After preprocessing, includes CTCS augmented state dynamics.

states List[State]

List of State objects. After preprocessing, includes time state and CTCS augmented states.

controls List[Control]

List of Control objects. After preprocessing, includes time dilation control.

constraints ConstraintSet

ConstraintSet holding all constraints. Before preprocessing, raw constraints live in constraints.unsorted. After preprocessing, constraints are categorized into ctcs, nodal, nodal_convex, etc.

parameters Dict[str, any]

Dictionary mapping parameter names to numpy arrays.

N int

Number of discretization nodes.

node_intervals List[Tuple[int, int]]

List of (start, end) tuples for CTCS constraint intervals. Populated during preprocessing when CTCS constraints are sorted.

dynamics_prop Optional[Expr]

Propagation dynamics (may include extra states). None before preprocessing, populated after.

states_prop Optional[List[State]]

Propagation states (may include extra states). None before preprocessing, populated after.

controls_prop Optional[List[Control]]

Propagation controls (typically same as controls). None before preprocessing, populated after.

algebraic_prop Optional[Dict[str, Expr]]

Algebraic outputs computed during propagation (no integration). None before preprocessing, populated after.

Example

Before preprocessing::

problem = SymbolicProblem(
    dynamics=dynamics_expr,
    states=[x, v],
    controls=[u],
    constraints=ConstraintSet(unsorted=[c1, c2, c3]),
    parameters={"mass": 1.0},
    N=50,
)
assert not problem.is_preprocessed

After preprocessing::

processed = preprocess_symbolic_problem(problem, time=time_config)
assert processed.is_preprocessed
assert processed.constraints.is_categorized
# Now ready for lowering
lowered = lower_symbolic_problem(processed)
Source code in openscvx/symbolic/problem.py
@dataclass
class SymbolicProblem:
    """Container for symbolic problem specification.

    This dataclass holds a trajectory optimization problem in symbolic form,
    either as raw user input or after preprocessing/augmentation. It provides
    a typed interface for the preprocessing and lowering pipeline.

    Lifecycle Stages:
        1. **Before preprocessing**: User creates with raw dynamics, states,
           controls, and unsorted constraints. Propagation fields are None.
        2. **After preprocessing**: Dynamics and states are augmented (CTCS,
           time dilation), constraints are categorized, propagation fields
           are populated.

    Use `is_preprocessed` to check whether preprocessing has completed.

    Attributes:
        dynamics: Symbolic dynamics expression (dx/dt = f(x, u)).
            After preprocessing, includes CTCS augmented state dynamics.
        states: List of State objects. After preprocessing, includes
            time state and CTCS augmented states.
        controls: List of Control objects. After preprocessing, includes
            time dilation control.
        constraints: ConstraintSet holding all constraints. Before preprocessing,
            raw constraints live in `constraints.unsorted`. After preprocessing,
            constraints are categorized into ctcs, nodal, nodal_convex, etc.
        parameters: Dictionary mapping parameter names to numpy arrays.
        N: Number of discretization nodes.
        node_intervals: List of (start, end) tuples for CTCS constraint intervals.
            Populated during preprocessing when CTCS constraints are sorted.

        dynamics_prop: Propagation dynamics (may include extra states).
            None before preprocessing, populated after.
        states_prop: Propagation states (may include extra states).
            None before preprocessing, populated after.
        controls_prop: Propagation controls (typically same as controls).
            None before preprocessing, populated after.
        algebraic_prop: Algebraic outputs computed during propagation (no integration).
            None before preprocessing, populated after.

    Example:
        Before preprocessing::

            problem = SymbolicProblem(
                dynamics=dynamics_expr,
                states=[x, v],
                controls=[u],
                constraints=ConstraintSet(unsorted=[c1, c2, c3]),
                parameters={"mass": 1.0},
                N=50,
            )
            assert not problem.is_preprocessed

        After preprocessing::

            processed = preprocess_symbolic_problem(problem, time=time_config)
            assert processed.is_preprocessed
            assert processed.constraints.is_categorized
            # Now ready for lowering
            lowered = lower_symbolic_problem(processed)
    """

    # Core problem specification
    dynamics: "Expr"
    states: List["State"]
    controls: List["Control"]
    constraints: ConstraintSet
    parameters: Dict[str, any]
    N: int

    # Discrete dynamics
    dynamics_discrete: Optional["Expr"] = None

    # CTCS node intervals (populated during preprocessing)
    node_intervals: List[Tuple[int, int]] = field(default_factory=list)

    # Propagation (None before preprocessing, populated after)
    dynamics_prop: Optional["Expr"] = None
    states_prop: Optional[List["State"]] = None
    controls_prop: Optional[List["Control"]] = None

    # Algebraic outputs computed during propagation (no integration)
    # Maps output names to symbolic expressions
    algebraic_prop: Optional[Dict[str, "Expr"]] = None

    @property
    def is_preprocessed(self) -> bool:
        """True if the problem has been preprocessed and is ready for lowering.

        A problem is considered preprocessed when:
        1. All constraints have been categorized (unsorted is empty)
        2. Propagation dynamics have been set up
        """
        return self.constraints.is_categorized and self.dynamics_prop is not None

    def citations(self) -> Dict[str, List[str]]:
        """Collect BibTeX citations from every expression node in the problem.

        Walks the dynamics, constraint, and propagation expression graphs and
        gathers :meth:`~openscvx.symbolic.expr.expr.Expr.citation` from each
        node, so :meth:`openscvx.Problem.citation` can credit the published
        methods a problem actually uses (e.g. GMSR STL operators, jaxlie-backed
        Lie-group maps). Works at either lifecycle stage: unsorted constraints
        are scanned before preprocessing, categorized ones after.

        Returns:
            Mapping from node class name to the deduplicated BibTeX entries
            cited by nodes of that name, containing only names that cite
            anything. Same-named classes (e.g. the GMSR and stljax ``Or``)
            share a key; their entries are merged.
        """
        from openscvx.symbolic.expr import walk

        roots: List["Expr"] = [self.dynamics]
        for optional in (self.dynamics_discrete, self.dynamics_prop):
            if optional is not None:
                roots.append(optional)
        if self.algebraic_prop:
            roots.extend(self.algebraic_prop.values())
        c = self.constraints
        roots.extend(
            [*c.unsorted, *c.ctcs, *c.nodal, *c.nodal_convex, *c.cross_node, *c.cross_node_convex]
        )

        cited: Dict[str, List[str]] = {}
        for root in roots:
            for node in walk(root):
                entries = node.citation()
                if not entries:
                    continue
                bucket = cited.setdefault(type(node).__name__, [])
                bucket.extend(e for e in entries if e not in bucket)
        return cited
is_preprocessed: bool property

True if the problem has been preprocessed and is ready for lowering.

A problem is considered preprocessed when: 1. All constraints have been categorized (unsorted is empty) 2. Propagation dynamics have been set up

citations() -> Dict[str, List[str]]

Collect BibTeX citations from every expression node in the problem.

Walks the dynamics, constraint, and propagation expression graphs and gathers :meth:~openscvx.symbolic.expr.expr.Expr.citation from each node, so :meth:openscvx.Problem.citation can credit the published methods a problem actually uses (e.g. GMSR STL operators, jaxlie-backed Lie-group maps). Works at either lifecycle stage: unsorted constraints are scanned before preprocessing, categorized ones after.

Returns:

Type Description
Dict[str, List[str]]

Mapping from node class name to the deduplicated BibTeX entries

Dict[str, List[str]]

cited by nodes of that name, containing only names that cite

Dict[str, List[str]]

anything. Same-named classes (e.g. the GMSR and stljax Or)

Dict[str, List[str]]

share a key; their entries are merged.

Source code in openscvx/symbolic/problem.py
def citations(self) -> Dict[str, List[str]]:
    """Collect BibTeX citations from every expression node in the problem.

    Walks the dynamics, constraint, and propagation expression graphs and
    gathers :meth:`~openscvx.symbolic.expr.expr.Expr.citation` from each
    node, so :meth:`openscvx.Problem.citation` can credit the published
    methods a problem actually uses (e.g. GMSR STL operators, jaxlie-backed
    Lie-group maps). Works at either lifecycle stage: unsorted constraints
    are scanned before preprocessing, categorized ones after.

    Returns:
        Mapping from node class name to the deduplicated BibTeX entries
        cited by nodes of that name, containing only names that cite
        anything. Same-named classes (e.g. the GMSR and stljax ``Or``)
        share a key; their entries are merged.
    """
    from openscvx.symbolic.expr import walk

    roots: List["Expr"] = [self.dynamics]
    for optional in (self.dynamics_discrete, self.dynamics_prop):
        if optional is not None:
            roots.append(optional)
    if self.algebraic_prop:
        roots.extend(self.algebraic_prop.values())
    c = self.constraints
    roots.extend(
        [*c.unsorted, *c.ctcs, *c.nodal, *c.nodal_convex, *c.cross_node, *c.cross_node_convex]
    )

    cited: Dict[str, List[str]] = {}
    for root in roots:
        for node in walk(root):
            entries = node.citation()
            if not entries:
                continue
            bucket = cited.setdefault(type(node).__name__, [])
            bucket.extend(e for e in entries if e not in bucket)
    return cited