Trajectories
Trajectories in LAV2 define time-parameterized reference signals. They sit between high-level task intent and the control or policy path that ultimately drives the vehicle. In practice, they answer a simple but important question: given time t, what target should the system be trying to track?
In this repository, trajectories are not treated as controller logic. They do not decide how tracking error should be regulated, and they do not encode reward logic or backend-specific environment structure. Their job is narrower and cleaner: generate reference states with stable semantics so the same target family can be reused across local simulation, batched training backends, and evaluation workflows.
Role And Boundary
The main role of a trajectory is to produce a reference in the same state vocabulary used by the rest of the stack. For the flight path, that means a dict with keys pos(N,3), vel(N,3), acc(N,3), att_euler(N,3), ang_vel(N,3) where N is the number of parallel environments. The trajectory layer therefore sits above the controller cascade and below whichever task or script chooses the reference family.
This boundary is important because it keeps responsibilities separated. A trajectory describes desired motion over time. A controller translates that reference into force, moment, or actuator-level commands. A task backend decides when the reference is sampled, how it is exposed to the policy, and how tracking quality affects rewards or termination logic. Once these layers are kept distinct, trajectory implementations stay reusable instead of becoming hidden copies of controller or task logic.
flowchart LR
T[Time parameter] --> Traj[Trajectory]
Traj --> Ref[Dict reference target]
Ref --> Ctrl[Controller or policy path]
Ctrl --> Act[Actuators and dynamics]
Act --> State[Observed state]
Module Structure
LAV2 trajectory implementations live under lav2.trajectories as backend-agnostic Array API classes. Legacy NumPy and Torch implementations are preserved under lav2.trajectories.numpy and lav2.trajectories.torch for benchmarking reference.
BaseTrajectory defines the common interface with batched primitives and backend resolution (backend/xp/device/num_envs). SpatialTrajectory adds a world-frame center anchor point broadcast to shape (num_envs, 3).
Reference Semantics
The central assembly logic lives in BaseTrajectory.build_target. A trajectory implementation typically computes position, velocity, and acceleration first, then turns those primitives into a full target dict. Yaw may be inferred from planar velocity or kept fixed, depending on configuration, while roll, pitch, and body-rate terms come from the trajectory settings instead of being derived from closed-loop tracking logic.
This means the trajectory layer already carries some attitude-related reference semantics, but it still does not become a controller. It provides desired orientation-related quantities in the same contract expected by downstream control modules. The actual regulation of those quantities remains a controller or policy responsibility.
Another useful detail is that heading generation is shared across trajectory shapes. When yaw_from_velocity is enabled, the heading follows planar motion and the yaw-rate reference is computed from velocity and acceleration. When it is disabled, the trajectory can hold a fixed yaw instead. This keeps heading semantics consistent across helix, rectangle, lemniscate, and other spatial curves.
Representative Trajectory Families
The simplest built-in reference is HoverStepTrajectory. It produces piecewise-constant position and yaw targets, making it useful when the main question is transient behavior: overshoot, settling time, saturation, and mode switching are easy to read from step-like commands because the desired state is intentionally discontinuous.
RectangleTrajectory is useful for long constant-velocity segments interrupted by sharp corner transitions. In practice, the trajectory exposes how the system handles direction changes, finite control authority, and heading updates along a piecewise path. It is especially helpful when comparing smooth continuous curves against paths with hard geometric structure.
HelixTrajectory introduces coupled horizontal and vertical motion. It is a good reference when the goal is to observe whether the controller or policy remains stable while translation, yaw alignment, and altitude evolution are all active at once. The optional vertical oscillation also makes it more informative than a purely planar circle.
LemniscateTrajectory and LissajousTrajectory are both useful for broader workspace coverage. The lemniscate path repeatedly crosses through a central region, which helps reveal how the system behaves near curvature changes and self-crossing motion. The Lissajous path is more general and can cover a larger three-dimensional envelope with smooth periodic motion, making it useful for dense reference excitation instead of a single isolated maneuver.
Read together, these trajectory families serve different engineering purposes. Step-like references are good for transient analysis, geometric loops are good for continuous tracking behavior, and richer periodic curves are good for exposing coupling effects across axes. That distinction is often more important than the exact mathematical formula of the path itself.
How Trajectories Enter The Stack
In the local path, a trajectory is sampled against simulation time and the resulting target is passed into the controller interface expected by the MuJoCo runner. In training backends, a related reference may instead be owned by a command manager or backend-specific task term, but the conceptual role is the same: produce a target signal whose semantics stay aligned with the control stack.
flowchart TD
A[Trajectory class] --> B[Sample at time t]
B --> C[Reference target]
C --> D[Local runner or task backend]
D --> E[Controller or policy]
E --> F[Actuation and dynamics]
This is also where the boundary with tasks should remain clear. A task defines how references are scheduled, randomized, rewarded, or exposed to learning code. A trajectory defines the signal family itself. Keeping those concerns separate makes it easier to reuse the same motion primitive across local validation, controller evaluation, and reinforcement learning environments without having to duplicate the motion definition.
Extension Conventions
When adding a new trajectory, the most important rule is to preserve output semantics. A new class should still produce the same dict-based target meaning as the existing trajectory family. It should not embed controller heuristics, backend-specific reward assumptions, or local plotting concerns into the sampling function.
The second rule is to target the Array API top-level. A single implementation serves all backends — there is no need for separate NumPy or Torch copies.
The third rule is to keep time as the explicit organizing variable. Trajectories in this package are time-parameterized reference generators, not stateful planners that mutate task logic over time. That keeps their role stable and makes them easier to reason about when they are embedded inside larger control or training workflows.
API Cross-References
- Trajectory package:
lav2.trajectories - Base trajectory:
BaseTrajectory - Spatial trajectory:
SpatialTrajectory - Target assembly helper:
BaseTrajectory.build_target - Hover-step trajectory:
HoverStepTrajectory - Rectangle trajectory:
RectangleTrajectory - Helix trajectory:
HelixTrajectory - Lemniscate trajectory:
LemniscateTrajectory - Lissajous trajectory:
LissajousTrajectory