Skip to content

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 12-dimensional target ordered as [x, y, z, vx, vy, vz, roll, pitch, yaw, wx, wy, wz]. 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[12D reference target]
  Ref --> Ctrl[Controller or policy path]
  Ctrl --> Act[Actuators and dynamics]
  Act --> State[Observed state]

Module Structure

LAV2 provides trajectory implementations under lav2.trajectories and batched counterparts under lav2.trajectories.torch. The NumPy path is designed for local or single-instance use, while the Torch path is designed for parallel backends where many environments must sample references at once.

At the base level, BaseTrajectory defines the common interface for NumPy trajectories, and SpatialTrajectory adds the notion of a world-frame anchor point. The Torch side mirrors this structure with lav2.trajectories.torch.base, preserving the same high-level meaning while adding a leading batch dimension.

This NumPy and Torch pairing is an important design choice in the repository. It allows one trajectory family to stay conceptually identical across local debugging and large-scale training. A batched implementation may add an environment axis, but it should not silently change the meaning of the target being produced.

Reference Semantics

The central assembly logic lives in BaseTrajectory.build_target and its Torch counterpart. A trajectory implementation typically computes position, velocity, and acceleration first, then turns those primitives into a full target vector. Yaw may be inferred from planar velocity or kept fixed, depending on configuration, while roll, pitch, and body-rate terms are inserted from the trajectory settings rather than 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 rather than reimplemented by each trajectory shape. 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 when you want long constant-velocity segments interrupted by sharp corner transitions. In practice, this 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 rather than one 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 12-dimensional 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 keep the NumPy and Torch paths aligned. If a trajectory exists for local use and later becomes useful in parallel environments, the Torch version should mirror the same motion definition and target layout rather than inventing a similar-but-different variant. Consistent naming and parameter meaning matter more here than strict code symmetry.

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