Skip to content

time

Tau

Bases: Leaf

Normalized node coordinate, running 0 at the first node to 1 at the last.

Tau is internal plumbing for initial guesses, not part of the user-facing vocabulary. A user who needs the node coordinate writes their guess as a callable of one argument::

pos.guess = lambda tau: p0 + (pf - p0) * tau

and the library applies that callable once, at assignment, with a Tau instance. The result is an ordinary expression that the guess-resolution pass evaluates node by node (see :func:openscvx.symbolic.preprocessing.resolve_guess_exprs).

Physical time is a Time state; Tau is the dimensionless grid coordinate the trajectory is discretized on, and is meaningful only while building a guess.

Source code in openscvx/symbolic/expr/time.py
class Tau(Leaf):
    """Normalized node coordinate, running 0 at the first node to 1 at the last.

    ``Tau`` is internal plumbing for initial guesses, not part of the user-facing
    vocabulary. A user who needs the node coordinate writes their guess as a
    callable of one argument::

        pos.guess = lambda tau: p0 + (pf - p0) * tau

    and the library applies that callable once, at assignment, with a ``Tau``
    instance. The result is an ordinary expression that the guess-resolution pass
    evaluates node by node (see
    :func:`openscvx.symbolic.preprocessing.resolve_guess_exprs`).

    Physical time is a ``Time`` state; ``Tau`` is the dimensionless grid
    coordinate the trajectory is discretized on, and is meaningful only while
    building a guess.
    """

    def __init__(self):
        super().__init__("tau", shape=())

    def check_shape(self) -> Tuple[int, ...]:
        """Reject tau outside of an initial-guess expression.

        Raises:
            ValueError: Always. Dynamics, constraints, and costs are shape-checked
                eagerly, so this fires exactly when a captured ``tau`` has leaked
                out of the guess it was created for.
        """
        raise ValueError(
            "tau is only meaningful inside initial-guess expressions. It was received "
            "by a `guess = lambda tau: ...` callable and then used in dynamics, a "
            "constraint, or a cost, where there is no node grid to evaluate it on. "
            "Use the `time` state for physical time, or index nodes explicitly with "
            "`.at(k)`."
        )

    def __repr__(self) -> str:
        return "Tau()"
check_shape() -> Tuple[int, ...]

Reject tau outside of an initial-guess expression.

Raises:

Type Description
ValueError

Always. Dynamics, constraints, and costs are shape-checked eagerly, so this fires exactly when a captured tau has leaked out of the guess it was created for.

Source code in openscvx/symbolic/expr/time.py
def check_shape(self) -> Tuple[int, ...]:
    """Reject tau outside of an initial-guess expression.

    Raises:
        ValueError: Always. Dynamics, constraints, and costs are shape-checked
            eagerly, so this fires exactly when a captured ``tau`` has leaked
            out of the guess it was created for.
    """
    raise ValueError(
        "tau is only meaningful inside initial-guess expressions. It was received "
        "by a `guess = lambda tau: ...` callable and then used in dynamics, a "
        "constraint, or a cost, where there is no node grid to evaluate it on. "
        "Use the `time` state for physical time, or index nodes explicitly with "
        "`.at(k)`."
    )

Time

Bases: State

Time state variable for trajectory optimization.

Time is a State representing physical time along the trajectory. Used for time-optimal control and problems with time-dependent dynamics/constraints.

Since Time is a State, it can be:

  • Used directly in constraint expressions (e.g., time[0] <= 5.0)
  • Added to the states list, or auto-added via the time= argument

The constructor accepts scalar values for convenience, which are converted to arrays internally to match State's API. All parameters can also be set via property setters after construction.

Time dilation bounds and guesses can also be configured here via time_dilation_min, time_dilation_max, and time_dilation_guess. These control the augmented time dilation variable that the solver adds internally, and can be updated between solves (e.g. in a receding horizon loop) without reconstructing the problem.

Note

time_dilation_min and time_dilation_max are absolute bounds on the time dilation control variable.

Attributes:

Name Type Description
derivative float

Always 1.0 - time derivative in normalized coordinates.

Example

Constructor style::

time = ox.Time(initial=0.0, final=10.0, min=0.0, max=20.0)
problem = Problem(..., time=time)

Setter style::

time = ox.Time()
time.min = 0.0
time.max = 20.0
time.initial = 0.0
time.final = ox.Minimize(10.0)

With guess and time-dilation overrides::

time = ox.Time(
    initial=0.0,
    final=("minimize", 10.0),
    min=0.0,
    max=20.0,
    guess=np.linspace(0, 10, 50).reshape(-1, 1),
    time_dilation_min=1.0,
    time_dilation_max=30.0,
)

Using time in constraints::

time = ox.Time(initial=0.0, final=10.0, min=0.0, max=20.0)
states = [position, velocity, time]
constraint = ox.ctcs(time[0] <= 5.0)
Source code in openscvx/symbolic/expr/time.py
class Time(State):
    """Time state variable for trajectory optimization.

    Time is a State representing physical time along the trajectory. Used for
    time-optimal control and problems with time-dependent dynamics/constraints.

    Since Time is a State, it can be:

    - Used directly in constraint expressions (e.g., `time[0] <= 5.0`)
    - Added to the states list, or auto-added via the `time=` argument

    The constructor accepts scalar values for convenience, which are converted
    to arrays internally to match State's API. All parameters can also be set
    via property setters after construction.

    Time dilation bounds and guesses can also be configured here via
    `time_dilation_min`, `time_dilation_max`, and `time_dilation_guess`.
    These control the augmented time dilation variable that the solver adds
    internally, and can be updated between solves (e.g. in a receding horizon
    loop) without reconstructing the problem.

    !!! note
       `time_dilation_min` and `time_dilation_max` are **absolute** bounds
       on the time dilation control variable.

    Attributes:
        derivative (float): Always 1.0 - time derivative in normalized coordinates.

    Example:
        Constructor style::

            time = ox.Time(initial=0.0, final=10.0, min=0.0, max=20.0)
            problem = Problem(..., time=time)

        Setter style::

            time = ox.Time()
            time.min = 0.0
            time.max = 20.0
            time.initial = 0.0
            time.final = ox.Minimize(10.0)

        With guess and time-dilation overrides::

            time = ox.Time(
                initial=0.0,
                final=("minimize", 10.0),
                min=0.0,
                max=20.0,
                guess=np.linspace(0, 10, 50).reshape(-1, 1),
                time_dilation_min=1.0,
                time_dilation_max=30.0,
            )

        Using time in constraints::

            time = ox.Time(initial=0.0, final=10.0, min=0.0, max=20.0)
            states = [position, velocity, time]
            constraint = ox.ctcs(time[0] <= 5.0)
    """

    def __init__(
        self,
        initial: Optional[Union[float, tuple]] = None,
        final: Optional[Union[float, tuple]] = None,
        min: Optional[float] = None,
        max: Optional[float] = None,
        *,
        guess: Optional[np.ndarray] = None,
        time_dilation_min: Optional[float] = None,
        time_dilation_max: Optional[float] = None,
        time_dilation_guess: Optional[np.ndarray] = None,
        uniform_time_grid: bool = False,
    ):
        """Initialize a Time state.

        All parameters are optional and can be set later via property setters.

        Args:
            initial: Initial time. Either a float (fixed) or tuple like
                ("free", value), ("minimize", value), ("maximize", value).
            final: Final time. Same format as initial.
            min: Minimum time bound.
            max: Maximum time bound.
            guess: Initial guess for the time trajectory. 1D array of shape
                (N,), 2D of shape (N, 1), or any symbolic form accepted by
                :attr:`Variable.guess`. If not provided, a linear
                interpolation from initial to final is generated.
            time_dilation_min: Absolute minimum bound for time dilation.
                Defaults to `0.3 * time_final` if not set.
            time_dilation_max: Absolute maximum bound for time dilation.
                Defaults to `3.0 * time_final` if not set.
            time_dilation_guess: Initial guess for time dilation.
                1D array of shape (N,) or 2D of shape (N, 1) — arrays only,
                symbolic guesses are not accepted here. Overrides the default
                finite-difference computation.
            uniform_time_grid: If True, constrain the time dilation to be
                the same across all nodes (uniform time steps). Defaults
                to False.
        """
        self.derivative = 1.0
        self.uniform_time_grid = uniform_time_grid
        self._time_dilation_min = None
        self._time_dilation_max = None
        self._time_dilation_guess = None
        self._time_dilation_control = None  # set by augmentation for live propagation

        # Wrap scalars into the array forms that State/Variable setters expect.
        # Time is always shape (1,), so a bare scalar is unambiguous.
        if isinstance(min, (int, float, np.number)):
            min = np.array([min], dtype=float)
        if isinstance(max, (int, float, np.number)):
            max = np.array([max], dtype=float)
        if initial is not None and not isinstance(initial, (list, np.ndarray)):
            initial = [initial]
        if final is not None and not isinstance(final, (list, np.ndarray)):
            final = [final]

        super().__init__("time", shape=(1,), min=min, max=max, initial=initial, final=final)

        if guess is not None:
            self.guess = guess
        if time_dilation_min is not None:
            self.time_dilation_min = time_dilation_min
        if time_dilation_max is not None:
            self.time_dilation_max = time_dilation_max
        if time_dilation_guess is not None:
            self.time_dilation_guess = time_dilation_guess

    @State.min.setter
    def min(self, val):
        """Set the minimum time bound. Accepts a scalar or array."""
        if isinstance(val, (int, float, np.number)):
            val = np.array([val], dtype=float)
        State.min.fset(self, val)

    @State.max.setter
    def max(self, val):
        """Set the maximum time bound. Accepts a scalar or array."""
        if isinstance(val, (int, float, np.number)):
            val = np.array([val], dtype=float)
        State.max.fset(self, val)

    @State.initial.setter
    def initial(self, val):
        """Set the initial time. Accepts a scalar, tuple, or array."""
        if not isinstance(val, (list, np.ndarray)):
            val = [val]
        State.initial.fset(self, val)

    @State.final.setter
    def final(self, val):
        """Set the final time. Accepts a scalar, tuple, or array."""
        if not isinstance(val, (list, np.ndarray)):
            val = [val]
        State.final.fset(self, val)

    @Variable.guess.setter
    def guess(self, value):
        """Set the time guess.

        Accepts everything :attr:`Variable.guess` accepts — an array, a symbolic
        expression, or a callable of tau — and additionally reshapes a 1D ``(N,)``
        array to ``(N, 1)`` since Time is always shape ``(1,)``. A symbolic guess
        resolving to one value per node is reshaped the same way at build time::

            time.guess = lambda tau: t0 + (tf - t0) * tau**2

        Note:
            ``time_dilation_guess`` is a separate, array-only setting; it is not
            resolved from expressions.
        """
        if not isinstance(value, Expr) and not callable(value):
            value = np.asarray(value, dtype=float)
            if value.ndim == 1:
                value = value.reshape(-1, 1)
        Variable.guess.fset(self, value)

    @property
    def time_dilation_min(self) -> Optional[float]:
        """Absolute minimum bound for time dilation."""
        return self._time_dilation_min

    @time_dilation_min.setter
    def time_dilation_min(self, val: float):
        val = float(val)
        if val < 0:
            raise ValueError(f"time_dilation_min must be non-negative, got {val}")
        self._time_dilation_min = val
        self._sync_time_dilation_control()

    @property
    def time_dilation_max(self) -> Optional[float]:
        """Absolute maximum bound for time dilation."""
        return self._time_dilation_max

    @time_dilation_max.setter
    def time_dilation_max(self, val: float):
        self._time_dilation_max = float(val)
        self._sync_time_dilation_control()

    @property
    def time_dilation_guess(self) -> Optional[np.ndarray]:
        """Initial guess for time dilation, shape (N, 1). Arrays only."""
        return self._time_dilation_guess

    @time_dilation_guess.setter
    def time_dilation_guess(self, arr):
        arr = np.asarray(arr, dtype=float)
        if arr.ndim == 1:
            arr = arr.reshape(-1, 1)
        if arr.ndim != 2 or arr.shape[1] != 1:
            raise ValueError(f"time_dilation_guess expected shape (N, 1), got {arr.shape}")
        self._time_dilation_guess = arr
        self._sync_time_dilation_control()

    def _sync_time_dilation_control(self):
        """Push current time_dilation_* values to the linked Control."""
        ctrl = self._time_dilation_control
        if ctrl is None:
            return
        if self._time_dilation_min is not None:
            ctrl.min = np.array([self._time_dilation_min])
        if self._time_dilation_max is not None:
            ctrl.max = np.array([self._time_dilation_max])
        if self._time_dilation_guess is not None:
            ctrl.guess = self._time_dilation_guess

    def _generate_default_guess(self, N: int) -> np.ndarray:
        """Generate linear interpolation guess from initial to final time.

        Args:
            N: Number of discretization nodes.

        Returns:
            Array of shape (N, 1) with linear interpolation.
        """
        # _initial and _final hold the numeric values (State parses tuples)
        return np.linspace(self._initial[0], self._final[0], N).reshape(-1, 1)

    def __repr__(self):
        parts = []
        if self._initial is not None:
            parts.append(f"initial={self._initial[0]}")
        if self._final is not None:
            parts.append(f"final={self._final[0]}")
        if self._min is not None:
            parts.append(f"min={self._min[0]}")
        if self._max is not None:
            parts.append(f"max={self._max[0]}")
        return f"Time({', '.join(parts)})"
time_dilation_guess: Optional[np.ndarray] property writable

Initial guess for time dilation, shape (N, 1). Arrays only.

time_dilation_max: Optional[float] property writable

Absolute maximum bound for time dilation.

time_dilation_min: Optional[float] property writable

Absolute minimum bound for time dilation.

__init__(initial: Optional[Union[float, tuple]] = None, final: Optional[Union[float, tuple]] = None, min: Optional[float] = None, max: Optional[float] = None, *, guess: Optional[np.ndarray] = None, time_dilation_min: Optional[float] = None, time_dilation_max: Optional[float] = None, time_dilation_guess: Optional[np.ndarray] = None, uniform_time_grid: bool = False)

Initialize a Time state.

All parameters are optional and can be set later via property setters.

Parameters:

Name Type Description Default
initial Optional[Union[float, tuple]]

Initial time. Either a float (fixed) or tuple like ("free", value), ("minimize", value), ("maximize", value).

None
final Optional[Union[float, tuple]]

Final time. Same format as initial.

None
min Optional[float]

Minimum time bound.

None
max Optional[float]

Maximum time bound.

None
guess Optional[ndarray]

Initial guess for the time trajectory. 1D array of shape (N,), 2D of shape (N, 1), or any symbolic form accepted by :attr:Variable.guess. If not provided, a linear interpolation from initial to final is generated.

None
time_dilation_min Optional[float]

Absolute minimum bound for time dilation. Defaults to 0.3 * time_final if not set.

None
time_dilation_max Optional[float]

Absolute maximum bound for time dilation. Defaults to 3.0 * time_final if not set.

None
time_dilation_guess Optional[ndarray]

Initial guess for time dilation. 1D array of shape (N,) or 2D of shape (N, 1) — arrays only, symbolic guesses are not accepted here. Overrides the default finite-difference computation.

None
uniform_time_grid bool

If True, constrain the time dilation to be the same across all nodes (uniform time steps). Defaults to False.

False
Source code in openscvx/symbolic/expr/time.py
def __init__(
    self,
    initial: Optional[Union[float, tuple]] = None,
    final: Optional[Union[float, tuple]] = None,
    min: Optional[float] = None,
    max: Optional[float] = None,
    *,
    guess: Optional[np.ndarray] = None,
    time_dilation_min: Optional[float] = None,
    time_dilation_max: Optional[float] = None,
    time_dilation_guess: Optional[np.ndarray] = None,
    uniform_time_grid: bool = False,
):
    """Initialize a Time state.

    All parameters are optional and can be set later via property setters.

    Args:
        initial: Initial time. Either a float (fixed) or tuple like
            ("free", value), ("minimize", value), ("maximize", value).
        final: Final time. Same format as initial.
        min: Minimum time bound.
        max: Maximum time bound.
        guess: Initial guess for the time trajectory. 1D array of shape
            (N,), 2D of shape (N, 1), or any symbolic form accepted by
            :attr:`Variable.guess`. If not provided, a linear
            interpolation from initial to final is generated.
        time_dilation_min: Absolute minimum bound for time dilation.
            Defaults to `0.3 * time_final` if not set.
        time_dilation_max: Absolute maximum bound for time dilation.
            Defaults to `3.0 * time_final` if not set.
        time_dilation_guess: Initial guess for time dilation.
            1D array of shape (N,) or 2D of shape (N, 1) — arrays only,
            symbolic guesses are not accepted here. Overrides the default
            finite-difference computation.
        uniform_time_grid: If True, constrain the time dilation to be
            the same across all nodes (uniform time steps). Defaults
            to False.
    """
    self.derivative = 1.0
    self.uniform_time_grid = uniform_time_grid
    self._time_dilation_min = None
    self._time_dilation_max = None
    self._time_dilation_guess = None
    self._time_dilation_control = None  # set by augmentation for live propagation

    # Wrap scalars into the array forms that State/Variable setters expect.
    # Time is always shape (1,), so a bare scalar is unambiguous.
    if isinstance(min, (int, float, np.number)):
        min = np.array([min], dtype=float)
    if isinstance(max, (int, float, np.number)):
        max = np.array([max], dtype=float)
    if initial is not None and not isinstance(initial, (list, np.ndarray)):
        initial = [initial]
    if final is not None and not isinstance(final, (list, np.ndarray)):
        final = [final]

    super().__init__("time", shape=(1,), min=min, max=max, initial=initial, final=final)

    if guess is not None:
        self.guess = guess
    if time_dilation_min is not None:
        self.time_dilation_min = time_dilation_min
    if time_dilation_max is not None:
        self.time_dilation_max = time_dilation_max
    if time_dilation_guess is not None:
        self.time_dilation_guess = time_dilation_guess
final(val)

Set the final time. Accepts a scalar, tuple, or array.

Source code in openscvx/symbolic/expr/time.py
@State.final.setter
def final(self, val):
    """Set the final time. Accepts a scalar, tuple, or array."""
    if not isinstance(val, (list, np.ndarray)):
        val = [val]
    State.final.fset(self, val)
guess(value)

Set the time guess.

Accepts everything :attr:Variable.guess accepts — an array, a symbolic expression, or a callable of tau — and additionally reshapes a 1D (N,) array to (N, 1) since Time is always shape (1,). A symbolic guess resolving to one value per node is reshaped the same way at build time::

time.guess = lambda tau: t0 + (tf - t0) * tau**2
Note

time_dilation_guess is a separate, array-only setting; it is not resolved from expressions.

Source code in openscvx/symbolic/expr/time.py
@Variable.guess.setter
def guess(self, value):
    """Set the time guess.

    Accepts everything :attr:`Variable.guess` accepts — an array, a symbolic
    expression, or a callable of tau — and additionally reshapes a 1D ``(N,)``
    array to ``(N, 1)`` since Time is always shape ``(1,)``. A symbolic guess
    resolving to one value per node is reshaped the same way at build time::

        time.guess = lambda tau: t0 + (tf - t0) * tau**2

    Note:
        ``time_dilation_guess`` is a separate, array-only setting; it is not
        resolved from expressions.
    """
    if not isinstance(value, Expr) and not callable(value):
        value = np.asarray(value, dtype=float)
        if value.ndim == 1:
            value = value.reshape(-1, 1)
    Variable.guess.fset(self, value)
initial(val)

Set the initial time. Accepts a scalar, tuple, or array.

Source code in openscvx/symbolic/expr/time.py
@State.initial.setter
def initial(self, val):
    """Set the initial time. Accepts a scalar, tuple, or array."""
    if not isinstance(val, (list, np.ndarray)):
        val = [val]
    State.initial.fset(self, val)
max(val)

Set the maximum time bound. Accepts a scalar or array.

Source code in openscvx/symbolic/expr/time.py
@State.max.setter
def max(self, val):
    """Set the maximum time bound. Accepts a scalar or array."""
    if isinstance(val, (int, float, np.number)):
        val = np.array([val], dtype=float)
    State.max.fset(self, val)
min(val)

Set the minimum time bound. Accepts a scalar or array.

Source code in openscvx/symbolic/expr/time.py
@State.min.setter
def min(self, val):
    """Set the minimum time bound. Accepts a scalar or array."""
    if isinstance(val, (int, float, np.number)):
        val = np.array([val], dtype=float)
    State.min.fset(self, val)

TimeSpec

Bases: StateSpec

Validates Time configuration from YAML/JSON/dict input.

Extends :class:StateSpec with time-specific fields. name and shape are fixed ("time" and [1]), and initial/final are required (a Time must have boundary conditions).

YAML scalars and [tag, value] pairs are normalised into the List forms that :class:StateSpec expects via field_validator so that no field type overrides are needed.

Source code in openscvx/symbolic/expr/time.py
class TimeSpec(StateSpec):
    """Validates Time configuration from YAML/JSON/dict input.

    Extends :class:`StateSpec` with time-specific fields.  ``name`` and
    ``shape`` are fixed (``"time"`` and ``[1]``), and ``initial``/``final``
    are required (a Time must have boundary conditions).

    YAML scalars and ``[tag, value]`` pairs are normalised into the
    ``List`` forms that :class:`StateSpec` expects via ``field_validator``
    so that no field type overrides are needed.
    """

    # Time is always named "time" with shape (1,)
    name: str = "time"
    shape: List[int] = [1]

    # Required for Time (StateSpec has these Optional; re-declared without
    # a default so pydantic treats them as required).
    initial: Optional[List[Any]]
    final: Optional[List[Any]]

    # Required for Time
    min: Optional[List[float]]
    max: Optional[List[float]]

    # Time-specific fields
    uniform_time_grid: bool = False
    time_dilation_min: Optional[float] = None
    time_dilation_max: Optional[float] = None
    time_dilation_guess: Optional[List[float]] = None

    model_config = ConfigDict(extra="forbid")

    @field_validator("initial", "final", mode="before")
    @classmethod
    def _wrap_scalar_boundary(cls, v: Any) -> Any:
        """Wrap a scalar or ``[tag, value]`` pair into a single-element list."""
        if v is None:
            return v
        if not isinstance(v, list):
            return [v]
        # bare [tag, value] pair like ["minimize", 10.0]
        if len(v) == 2 and isinstance(v[0], str) and not isinstance(v[1], list):
            return [v]
        return v

    @field_validator("min", "max", mode="before")
    @classmethod
    def _wrap_scalar_bound(cls, v: Any) -> Any:
        """Wrap a scalar float into a single-element list."""
        if v is None:
            return v
        if not isinstance(v, list):
            return [float(v)]
        return v

    def to_time(self) -> "Time":
        return Time(
            initial=_parse_time_boundary(self.initial[0]) if self.initial else None,
            final=_parse_time_boundary(self.final[0]) if self.final else None,
            min=self.min[0] if self.min else None,
            max=self.max[0] if self.max else None,
            uniform_time_grid=self.uniform_time_grid,
            time_dilation_min=self.time_dilation_min,
            time_dilation_max=self.time_dilation_max,
            time_dilation_guess=(
                np.asarray(self.time_dilation_guess, dtype=float)
                if self.time_dilation_guess is not None
                else None
            ),
        )