stl
Signal Temporal Logic (STL) operations using GMSR smooth robustness.
This module provides symbolic expression nodes for STL operations backed by Generalized Mean-based Smooth Robustness (GMSR). These operators offer smooth, differentiable approximations of Boolean logic for use in trajectory optimization.
Unlike the stljax module (which uses the stljax library), this module uses GMSR parameterizations that work directly with constraint residuals.
Interval and time semantics are a work in progress
The logical operators in this module (And, Or, Not,
IfThen) are stable and compose freely. The temporal side of
STL — anything involving an enforcement interval or time-bounded
quantification — is still being built out:
- Intervals are typed: :class:
NodeInterval(node indices) is implemented end to end. :class:TimeInterval(seconds, resolved against the model's explicittimestate) is accepted at construction so downstream code can be sketched against the eventual API, but raisesNotImplementedErrorat lowering. The kind of interval is selected explicitly by theinterval_typekwarg on temporal constructors and on.over()— either"nodes"(the default) or"seconds". There is no implicit int-vs-float dispatch. - Mixed-interval nesting (e.g.
□[0,5] p ∧ ◇[3,8] q) has no defined lowering yet and is rejected at construction time. Temporal operators used as children of other STL operators must be interval-free and inherit the ambient window from the enclosing.over(). Compose distinct windows as separate top-level constraints instead. EventuallyandUntilare placeholder nodes only — construction is allowed so downstream code can be sketched, but any attempt to lower or enforce them raisesNotImplementedError. A novel formulation is in progress.
Treat this surface as load-bearing for logical task composition and as preview for temporal composition until those gaps close.
GMSR Robustness Convention
GMSR functions operate on constraint residuals (negative when satisfied). The expression nodes in this module follow the standard STL convention where robustness is positive when satisfied. The conversion happens during lowering.
Author: Samet Uzun and Chris Hayner
Reference
https://doi.org/10.48550/arxiv.2405.10996 https://doi.org/10.2514/6.2025-1895
See also
openscvx.symbolic.expr.logic provides All/Any/Cond for
hard-boolean branching inside expressions (e.g. switching dynamics
based on a predicate). Those are JAX-only and not differentiable
across the branch. The operators in this module are smooth and
differentiable everywhere, and are the right tool for composing
constraints into a task specification.
Always
¶
Bases: _TemporalSTLExpr
Enforce predicate at every point in interval (STL Always).
Always(p, (a, b)) is satisfied iff p holds at every time in
the interval [a, b]. This is the universal temporal quantifier of
STL and is the natural way to express invariants such as obstacle
avoidance, box constraints, or speed limits over a window of the
trajectory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
predicate
|
Union[Constraint, STLExpr]
|
A Constraint or STLExpr that should hold throughout the interval. |
required |
interval
|
Optional[IntervalLike]
|
Optional |
None
|
Example::
import openscvx as ox
avoid = ox.linalg.Norm(position - obs) >= safety_radius
constraints.append(ox.stl.Always(avoid, (0, N - 1)).over())
# Composes naturally with other STL operators:
spec = ox.stl.Always(avoid, (0, N - 1)) & ox.stl.Always(in_box, (0, N - 1))
constraints.append(spec.over())
Note
Standalone, Always is syntactic sugar around CTCS: enforcing
the integral of the constraint violation to be zero across the
interval is equivalent to requiring the predicate to hold
pointwise. .over() performs that wrapping.
When Always is nested inside another STL operator it
must be interval-free and inherits the ambient window from the
enclosing .over(). Passing an explicit interval to a nested
Always is rejected at construction time, since mixed-
interval nesting has no defined lowering yet. To compose
distinct windows, use separate top-level constraints.
Source code in openscvx/symbolic/expr/stl.py
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 | |
over(interval: Optional[IntervalLike] = None, penalty: str = 'smooth_relu', idx: Optional[int] = None, check_nodally: bool = False, interval_type: IntervalKind = 'nodes') -> CTCS
¶
Convert this Always to a CTCS constraint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
Optional[IntervalLike]
|
Enforcement interval, supplied here or at
construction — but not both. The interval is part of
what the formula says ( |
None
|
penalty
|
str
|
CTCS penalty function name. |
'smooth_relu'
|
idx
|
Optional[int]
|
Optional grouping index for multiple augmented states. |
None
|
check_nodally
|
bool
|
Whether to additionally enforce at discrete nodes. |
False
|
Returns:
| Type | Description |
|---|---|
CTCS
|
A |
CTCS
|
interval. |
Source code in openscvx/symbolic/expr/stl.py
And
¶
Bases: STLExpr
GMSR smooth conjunction.
Satisfied when all predicates are satisfied. Uses the GMSR smooth AND parameterization for differentiable optimization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*predicates
|
Union[Constraint, STLExpr]
|
Two or more Constraint or STLExpr objects |
()
|
c
|
float
|
Smoothing parameter (default 1e-4). Smaller values give tighter approximation to the true Boolean AND. |
0.0001
|
lite
|
bool
|
If True, use the lite variant that only considers the positive part of the AND function. |
False
|
Example::
import openscvx as ox
avoid_a = ox.Norm(position - obs_a) >= 1.0
avoid_b = ox.Norm(position - obs_b) >= 1.0
avoid_both = ox.stl.And(avoid_a, avoid_b)
constraints = [avoid_both.over((0, 10))]
Source code in openscvx/symbolic/expr/stl.py
Eventually
¶
Bases: _UnimplementedTemporal
STL Eventually operator: p holds at some time in the interval.
Semantically: Eventually(p, (a, b)) is satisfied iff p holds at
some time in [a, b]. The interval is optional — when omitted,
the operator inherits the ambient window from the enclosing seal
(e.g. .over()), the same way Always does.
Not yet implemented!
Constructing the node is allowed so that downstream
code can be sketched against the eventual API, but any attempt
to lower or enforce it raises NotImplementedError.
Source code in openscvx/symbolic/expr/stl.py
IfThen
¶
Bases: STLExpr
GMSR smooth implication.
Satisfied when: if the condition is satisfied, then the consequent is also satisfied. Uses the GMSR smooth implication parameterization.
Formally: IfThen(cond, conseq) holds iff (cond => conseq), i.e., either the condition is NOT satisfied, or the consequent IS satisfied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
Union[Constraint, STLExpr]
|
Constraint or STLExpr representing the antecedent |
required |
consequent
|
Union[Constraint, STLExpr]
|
Constraint or STLExpr representing the consequent |
required |
c
|
float
|
Smoothing parameter (default 1e-4) |
0.0001
|
lite
|
bool
|
If True, use the lite variant. The lite implication can also enforce continuous-time satisfaction via periodic auxiliary state. |
False
|
Example::
import openscvx as ox
in_zone = ox.Norm(position - zone_center) <= zone_radius
speed_limit = speed <= max_speed
# If in the zone, then obey speed limit
rule = ox.stl.IfThen(in_zone, speed_limit)
constraints = [rule.over((0, 10))]
Source code in openscvx/symbolic/expr/stl.py
IntegerVariable
¶
Bases: STLExpr
GMSR smooth discrete/integer variable constraint.
Constrains an expression to take one of a set of allowed discrete values. Uses smooth equality penalties combined with GMSR OR to enforce that the expression matches at least one of the given values.
The expression evaluates to a penalty that is zero when the variable equals
one of the allowed values, and positive otherwise. Use .over() or .at()
to enforce this as a constraint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expr
|
Expr
|
Symbolic expression to constrain (e.g. a State or Variable) |
required |
values
|
Union[list, ndarray]
|
Array-like of allowed discrete values |
required |
c
|
float
|
Smoothing parameter (default 1e-4) |
0.0001
|
Example::
import openscvx as ox
gear = ox.State("gear", shape=())
# Constrain gear to discrete values {1, 2, 3, 4}
discrete_gear = ox.stl.IntegerVariable(gear, [1, 2, 3, 4])
constraints = [discrete_gear.over((0, 10))]
Source code in openscvx/symbolic/expr/stl.py
Interval
¶
Base class for STL enforcement intervals.
Use :meth:coerce to build an Interval from a user-supplied
value (a tuple, an existing Interval, or a parser Constant).
Direct instantiation of the subclasses is also supported.
Source code in openscvx/symbolic/expr/stl.py
coerce(value: object, kind: IntervalKind = 'nodes') -> Interval
staticmethod
¶
Coerce a user-supplied interval-like value into an Interval.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
One of
- an existing |
required |
kind
|
IntervalKind
|
|
'nodes'
|
For kind="nodes" the bounds must be integral (Python int
or a float/array element exactly equal to an integer); a
non-integral bound is rejected loudly. For kind="seconds"
any numeric bound is accepted and cast to float.
Source code in openscvx/symbolic/expr/stl.py
NodeInterval
dataclass
¶
Bases: Interval
An interval expressed as [start_node, end_node] discretization indices.
Resolved statically at graph build time: the participating nodes are known before any optimization runs.
Source code in openscvx/symbolic/expr/stl.py
Not
¶
Bases: STLExpr
GMSR smooth negation.
Satisfied when the inner predicate is not satisfied. Under the GMSR
convention this is just a sign flip on the residual: if the inner
predicate has robustness r, then Not has robustness -r.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
predicate
|
Union[Constraint, STLExpr]
|
A Constraint or STLExpr to negate. |
required |
Example::
import openscvx as ox
in_zone = ox.linalg.Norm(position - center) <= radius
outside = ox.stl.Not(in_zone)
# Equivalently, with operator syntax once lifted into STL:
outside = ~ox.stl.Always(in_zone) # not always in zone
Note
Not is a thin sign flip — there is no smoothing parameter.
Composing Not with Or/And recovers De Morgan duals
through the GMSR machinery rather than as an algebraic rewrite.
Source code in openscvx/symbolic/expr/stl.py
Or
¶
Bases: STLExpr
GMSR smooth disjunction.
Satisfied when at least one predicate is satisfied. Uses the GMSR smooth OR parameterization for differentiable optimization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*predicates
|
Union[Constraint, STLExpr]
|
Two or more Constraint or STLExpr objects |
()
|
c
|
float
|
Smoothing parameter (default 1e-4). Smaller values give tighter approximation to the true Boolean OR. |
0.0001
|
lite
|
bool
|
If True, use the lite variant that only considers the positive part of the OR function. |
False
|
Example::
import openscvx as ox
reach_a = ox.Norm(position - goal_a) <= 0.5
reach_b = ox.Norm(position - goal_b) <= 0.5
reach_either = ox.stl.Or(reach_a, reach_b)
constraints = [reach_either.over((3, 5))]
Source code in openscvx/symbolic/expr/stl.py
STLExpr
¶
Bases: Expr
Base class for GMSR-based Signal Temporal Logic operators.
Provides common functionality for all GMSR STL operators including
helper methods .over() and .at() to convert STL expressions
to constraints for trajectory optimization.
Sealing a spec — .over() vs .at():
STL specs live in a symbolic world; to enforce one you must
seal it into a concrete constraint via one of two entry points.
Both bind the spec to a where on the trajectory, but they
differ in the shape of that where, and that shape is the
only thing that determines the lowering:
- ``.over(interval)`` binds the spec to a contiguous interval
(an :class:`Interval`, or a length-2 tuple coerced to one).
Lowers to a ``CTCS`` that integrates the violation across
the window — this is the natural realization of an STL
temporal quantifier ``□[a,b] p``.
- ``.at(nodes)`` binds the spec to a discrete list of node
indices. Lowers to a ``NodalConstraint`` that enforces the
predicate independently at each point — semantically a
finite conjunction ``p(n_1) ∧ p(n_2) ∧ ...``.
For a temporal operator like :class:`Always`, the interval is
part of the formula itself (``Always(p, I)``) and ``.over()``
seals it without taking a separate window. Specifying the
interval *both* on the operator and at the seal call is a
contradiction (not an override) and is rejected; the same
rule applies to ``Always(p, I).at(...)``. To pick a discrete
point set instead, leave the constructor's interval unset:
``Always(p).at([2, 3, 5])`` is the universal quantifier over
a finite set, equivalent to a finite conjunction.
STL Robustness Convention
STL uses "robustness" values that are positive when constraints are satisfied.
For an Inequality constraint lhs <= rhs:
- Constraint residual: lhs - rhs (should be <= 0 when satisfied)
- STL robustness: rhs - lhs (should be >= 0 when satisfied)
Example
STL operators can be converted to constraints using helper methods:
wp1 = Norm(pos - c_1) <= r_1
wp2 = Norm(pos - c_2) <= r_2
visit_either = ox.stl.Or(wp1, wp2) # STL Operator
# Convert to constraint with .over()
constraints = [visit_either.over((3, 5))]
Note
This is a base class. Use concrete subclasses like Or, And, Eventually, Always, or Until for actual STL specifications.
Source code in openscvx/symbolic/expr/stl.py
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 332 333 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 | |
at(nodes: Union[list, tuple]) -> NodalConstraint
¶
Apply this STL expression only at specific 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
|
Nodal constraint wrapper |
Example
Enforce STL expression over an interval:
visit_either = ox.stl.Or(wp1, wp2)
constraint = visit_either.at([0, 5, 10])
See also
:meth:over for binding the spec to a contiguous
interval instead of a discrete point set. See the class
docstring for the full story on how the region shape
determines the lowering.
Note
This is a base class. Use concrete subclasses like Or, And, Eventually, Always, or Until for actual STL specifications.
Source code in openscvx/symbolic/expr/stl.py
over(interval: IntervalLike, penalty: str = 'smooth_relu', idx: Optional[int] = None, check_nodally: bool = False, interval_type: IntervalKind = 'nodes') -> CTCS
¶
Apply this STL expression over an enforcement interval using CTCS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
IntervalLike
|
An |
required |
interval_type
|
IntervalKind
|
|
'nodes'
|
penalty
|
str
|
Penalty function type for CTCS |
'smooth_relu'
|
idx
|
Optional[int]
|
Optional grouping index for multiple augmented states |
None
|
check_nodally
|
bool
|
Whether to also enforce at discrete nodes |
False
|
Returns:
| Type | Description |
|---|---|
CTCS
|
Continuous-time constraint satisfaction wrapper |
Example::
visit_either = ox.stl.Or(wp1, wp2)
constraint = visit_either.over((3, 5))
See also
:meth:at for binding the spec to a discrete point set
instead of a contiguous interval. The two methods seal to
different constraint types (CTCS vs. NodalConstraint)
because they pick different region shapes — see the
class docstring for the full story.
Source code in openscvx/symbolic/expr/stl.py
TimeInterval
dataclass
¶
Bases: Interval
An interval expressed as [t_start, t_end] in seconds.
Resolved at evaluation time against the model's explicit time
state — the set of nodes that fall inside the window is iterate-
dependent and is realised by gating the predicate residual on
(time >= t_start) & (time <= t_end).
Not yet implemented
Construction is allowed so downstream code can be sketched
against the eventual API, but lowering raises
NotImplementedError. Use :class:NodeInterval for now.
Source code in openscvx/symbolic/expr/stl.py
Until
¶
Bases: _UnimplementedTemporal
STL Until operator: left holds until right holds.
Until(left, right, (a, b)) is satisfied iff there exists a time
t* ∈ [a, b] at which right holds, and left holds at every
time in [a, t*]. Until is the most expressive of the standard
STL temporal operators; Always and Eventually can both be
derived from it. The interval is optional — when omitted, the
operator inherits the ambient window from the enclosing seal, the
same way Always does.
Not yet implemented!
Constructing the node is allowed so
downstream code can be sketched against the eventual API, but
any attempt to lower or enforce it raises NotImplementedError.