Skip to content

TrajectorySegment

Full Docs

PyDynSys.core.euclidean.trajectory.TrajectorySegment

Represents a numerically computed segment of a trajectory on a monotone increasing evaluation space.

A segment corresponds to a single continuous solution from scipy.solve_ivp, representing the trajectory over a contiguous time interval with a single interpolant.

Fields

t (NDArray[np.float64]): Monotone increasing array of evaluation times, shape (len(t),) y (NDArray[np.float64]): Array of trajectory evaluations x(t), shape (n, len(t)) domain (Tuple[float, float]): Time domain [t[0], t[-1]] where segment is defined interpolant (Optional[Callable]): Continuous interpolant x(t) on domain, or None method (str): ODE solver method used ('RK45', 'LSODA', etc.) meta (Dict[str, Any]): Metadata about numerical solution (success, message, etc.)

Usage

Segments are created via from_scipy_solution() factory, not direct instantiation. Users primarily interact with EuclideanTrajectory, which aggregates segments.

Source code in src/PyDynSys/core/euclidean/trajectory.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 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
class TrajectorySegment: 
    """
    Represents a numerically computed segment of a trajectory on a monotone increasing evaluation space. 

    A segment corresponds to a single continuous solution from scipy.solve_ivp, representing
    the trajectory over a contiguous time interval with a single interpolant.

    Fields: 
        t (NDArray[np.float64]): Monotone increasing array of evaluation times, shape (len(t),)
        y (NDArray[np.float64]): Array of trajectory evaluations x(t), shape (n, len(t))
        domain (Tuple[float, float]): Time domain [t[0], t[-1]] where segment is defined
        interpolant (Optional[Callable]): Continuous interpolant x(t) on domain, or None
        method (str): ODE solver method used ('RK45', 'LSODA', etc.)
        meta (Dict[str, Any]): Metadata about numerical solution (success, message, etc.)

    Usage:
        Segments are created via from_scipy_solution() factory, not direct instantiation.
        Users primarily interact with EuclideanTrajectory, which aggregates segments.
    """


    ### --- Factory Methods --- ###


    @classmethod
    def from_scipy_solution(
        cls, 
        sol: SciPyIvpSolution, 
        method: str
    ) -> 'TrajectorySegment':
        """
        Factory: Create segment from scipy solve_ivp solution.

        Handles backward integration (monotone decreasing t) by reversing arrays to
        enforce monotone increasing time convention for all segments.

        Args:
            sol (SciPyIvpSolution): Wrapped scipy OdeResult from solve_ivp
            method (str): ODE solver method used (e.g. 'RK45', 'LSODA')

        Returns:
            EuclideanTrajectorySegment: Segment with monotone increasing time

        Example:
            >>> from scipy.integrate import solve_ivp
            >>> result = solve_ivp(fun, t_span, y0, t_eval, method, dense_output)
            >>> wrapped = SciPyIvpSolution(raw_solution=result) # not necessary, but provides type safety
            >>> segment = EuclideanTrajectorySegment.from_scipy_solution(wrapped, 'RK45') # default method is RK45
        """
        segment = cls()
        t = sol.t # shape = (n_points,)
        y = sol.y # shape = (n_dim, n_points)

        # Enforce monotone increasing time convention
        if len(t) > 1 and t[0] > t[-1]:
            # Backward integration detected (time decreases): reverse arrays
            t = t[::-1]
            y = y[:, ::-1]

        segment.t = t
        segment.y = y
        segment.domain = (float(t[0]), float(t[-1]))

        # Store interpolant (callable or None)
        ## NOTE: this is some iff dense_output=True flag passed to solve_ivp fn.
        segment.interpolant = sol.sol

        segment.method = method
        segment.meta = {
            'success': sol.success,
            'message': sol.message,
        }

        return segment


        ### --- Public Methods --- ###


    def in_domain(self, t: float) -> bool:
        """
        Check if time t is within segment domain.

        Args:
            t (float): Time point to check

        Returns:
            bool: True if t ∈ [domain[0], domain[1]], False otherwise
        """
        return self.domain[0] <= t <= self.domain[1]

    def interpolant_at_time(self, t: float) -> NDArray[np.float64]:
        """
        Evaluate interpolant at time t.

        Uses scipy's dense output interpolant to compute x(t) continuously within
        the segment domain. Raises errors if t is outside domain or if no interpolant
        is available (dense_output=False in original solve_ivp call).

        NOTE: For speed, we employ agressive programming here and assume 
            1. t is in domain 
            2. interpolant is available
        If either of these fails, an esoteric error may be incurred. This is a worthwhile 
        tradeoff as this function may be called thousands of times (e.g. when plotting a trajectory).

        Args:
            t (float): Time point for evaluation

        Returns:
            NDArray[np.float64]: State vector x(t) at time t, shape (n,)

        Example:
            >>> x_t = segment.interpolate(0.5)  # Evaluate at t=0.5
        """
        result = self.interpolant(t)
        if result.ndim == 2:
            return result[:, 0]  # Extract column vector as 1D
        return result

from_scipy_solution(sol: SciPyIvpSolution, method: str) -> TrajectorySegment classmethod

Factory: Create segment from scipy solve_ivp solution.

Handles backward integration (monotone decreasing t) by reversing arrays to enforce monotone increasing time convention for all segments.

Parameters:

Name Type Description Default
sol SciPyIvpSolution

Wrapped scipy OdeResult from solve_ivp

required
method str

ODE solver method used (e.g. 'RK45', 'LSODA')

required

Returns:

Name Type Description
EuclideanTrajectorySegment TrajectorySegment

Segment with monotone increasing time

Example

from scipy.integrate import solve_ivp result = solve_ivp(fun, t_span, y0, t_eval, method, dense_output) wrapped = SciPyIvpSolution(raw_solution=result) # not necessary, but provides type safety segment = EuclideanTrajectorySegment.from_scipy_solution(wrapped, 'RK45') # default method is RK45

Source code in src/PyDynSys/core/euclidean/trajectory.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
@classmethod
def from_scipy_solution(
    cls, 
    sol: SciPyIvpSolution, 
    method: str
) -> 'TrajectorySegment':
    """
    Factory: Create segment from scipy solve_ivp solution.

    Handles backward integration (monotone decreasing t) by reversing arrays to
    enforce monotone increasing time convention for all segments.

    Args:
        sol (SciPyIvpSolution): Wrapped scipy OdeResult from solve_ivp
        method (str): ODE solver method used (e.g. 'RK45', 'LSODA')

    Returns:
        EuclideanTrajectorySegment: Segment with monotone increasing time

    Example:
        >>> from scipy.integrate import solve_ivp
        >>> result = solve_ivp(fun, t_span, y0, t_eval, method, dense_output)
        >>> wrapped = SciPyIvpSolution(raw_solution=result) # not necessary, but provides type safety
        >>> segment = EuclideanTrajectorySegment.from_scipy_solution(wrapped, 'RK45') # default method is RK45
    """
    segment = cls()
    t = sol.t # shape = (n_points,)
    y = sol.y # shape = (n_dim, n_points)

    # Enforce monotone increasing time convention
    if len(t) > 1 and t[0] > t[-1]:
        # Backward integration detected (time decreases): reverse arrays
        t = t[::-1]
        y = y[:, ::-1]

    segment.t = t
    segment.y = y
    segment.domain = (float(t[0]), float(t[-1]))

    # Store interpolant (callable or None)
    ## NOTE: this is some iff dense_output=True flag passed to solve_ivp fn.
    segment.interpolant = sol.sol

    segment.method = method
    segment.meta = {
        'success': sol.success,
        'message': sol.message,
    }

    return segment

in_domain(t: float) -> bool

Check if time t is within segment domain.

Parameters:

Name Type Description Default
t float

Time point to check

required

Returns:

Name Type Description
bool bool

True if t ∈ [domain[0], domain[1]], False otherwise

Source code in src/PyDynSys/core/euclidean/trajectory.py
89
90
91
92
93
94
95
96
97
98
99
def in_domain(self, t: float) -> bool:
    """
    Check if time t is within segment domain.

    Args:
        t (float): Time point to check

    Returns:
        bool: True if t ∈ [domain[0], domain[1]], False otherwise
    """
    return self.domain[0] <= t <= self.domain[1]

interpolant_at_time(t: float) -> NDArray[np.float64]

Evaluate interpolant at time t.

Uses scipy's dense output interpolant to compute x(t) continuously within the segment domain. Raises errors if t is outside domain or if no interpolant is available (dense_output=False in original solve_ivp call).

For speed, we employ agressive programming here and assume
  1. t is in domain
  2. interpolant is available

If either of these fails, an esoteric error may be incurred. This is a worthwhile tradeoff as this function may be called thousands of times (e.g. when plotting a trajectory).

Parameters:

Name Type Description Default
t float

Time point for evaluation

required

Returns:

Type Description
NDArray[float64]

NDArray[np.float64]: State vector x(t) at time t, shape (n,)

Example

x_t = segment.interpolate(0.5) # Evaluate at t=0.5

Source code in src/PyDynSys/core/euclidean/trajectory.py
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
def interpolant_at_time(self, t: float) -> NDArray[np.float64]:
    """
    Evaluate interpolant at time t.

    Uses scipy's dense output interpolant to compute x(t) continuously within
    the segment domain. Raises errors if t is outside domain or if no interpolant
    is available (dense_output=False in original solve_ivp call).

    NOTE: For speed, we employ agressive programming here and assume 
        1. t is in domain 
        2. interpolant is available
    If either of these fails, an esoteric error may be incurred. This is a worthwhile 
    tradeoff as this function may be called thousands of times (e.g. when plotting a trajectory).

    Args:
        t (float): Time point for evaluation

    Returns:
        NDArray[np.float64]: State vector x(t) at time t, shape (n,)

    Example:
        >>> x_t = segment.interpolate(0.5)  # Evaluate at t=0.5
    """
    result = self.interpolant(t)
    if result.ndim == 2:
        return result[:, 0]  # Extract column vector as 1D
    return result