Skip to content

Mission Scripting

MOLARS provides a powerful Python API for creating and executing missions. This page covers how to use this API to control the vehicle through scripting.

Mission Script API

The Mission Script API is a collection of Python modules that provide a high-level interface to the MOLARS system. These modules include:

  • mission_script.context - Core functions for interacting with the vehicle
  • mission_script.models - Data models for representing vehicle state
  • mission_script.nav - Navigation goals and parameters
  • mission_script.planning - Utilities for planning paths and trajectories

Using the REPL

The simplest way to interact with MOLARS is through the Python REPL, which can be started with:

python repl.py

This starts an interactive Python shell with common functions from mission_script.context already imported.

🧭 Basic Motion Control

Getting Current State

You can query the current state of the vehicle using the get_state() function:

>>> state = get_state()
>>> print(state.pose)
Pose(position=Point(x=0.0, y=0.0, z=0.0), orientation=Quaternion(x=0.0, y=0.0, z=0.0, w=1.0))

Similarly, you can get the current reference (target state) using get_reference():

>>> reference = get_reference()
>>> print(reference.pose)
Pose(position=Point(x=0.0, y=0.0, z=0.0), orientation=Quaternion(x=0.0, y=0.0, z=0.0, w=1.0))

Moving to a Position

To move the vehicle to a specific position (with an identity orientation), you can use the go_to_position() function. Note that this command uses absolute coordinates in the vehicle's local reference frame, not relative movements:

>>> # Move to position (2.0, 1.0, 0.5) in the local reference frame
>>> target = Point(x=2.0, y=1.0, z=0.5)
>>> run(go_to_position(target))

Absolute vs. Relative Movement

All basic movement commands in MOLARS (go_to_position, go_to_pose, and move) operate in absolute coordinates within the local reference frame, not as relative offsets from the current position.

Moving to a Pose

To control both position and orientation, use the go_to_pose() function. This also uses absolute coordinates:

>>> from mission_script.models import Pose, Point, Quaternion
>>> 
>>> # Move to position (3.0, 0.0, 0.0) with 45-degree heading in the local reference frame
>>> position = Point(x=3.0, y=0.0, z=0.0)
>>> orientation = Quaternion.from_rph(h=0.785)  # 45 degrees in radians
>>> target_pose = Pose(position=position, orientation=orientation)
>>> 
>>> # Move to the target pose
>>> run(go_to_pose(target_pose))

Using the Simplified Movement API

For convenience, MOLARS also provides a simplified move() function. Despite its name, this also specifies an absolute destination, not a relative movement:

>>> # Move to position (2.0, 1.0, 0.0) with a 30-degree heading
>>> run(move(x=2.0, y=1.0, h=0.523))

Making Relative Movements

To make relative movements, you need to get the current state and calculate the target position based on that:

>>> # Get current state
>>> current = get_state()
>>> 
>>> # Move 2 meters forward from current position
>>> current_position = current.pose.position
>>> new_position = Point(
...     x=current_position.x + 2.0,
...     y=current_position.y,
...     z=current_position.z
... )
>>> run(go_to_position(new_position))

For more complex relative movements that account for the vehicle's current orientation:

>>> # Get current state
>>> current = get_state()
>>> current_pose = current.pose
>>> 
>>> # Define a position offset in the vehicle's frame (2m forward, 1m right)
>>> offset = Point(x=2.0, y=1.0, z=0.0)
>>> 
>>> # Apply the vehicle's orientation to transform the offset to world coordinates
>>> world_offset = current_pose.orientation @ offset
>>> 
>>> # Add the transformed offset to the current position
>>> target_position = Point(
...     x=current_pose.position.x + world_offset.x,
...     y=current_pose.position.y + world_offset.y,
...     z=current_pose.position.z + world_offset.z
... )
>>> 
>>> # Go to the new position
>>> run(go_to_position(target_position))

🗺 Advanced Navigation

Creating and Executing Navigation Goals

For more complex navigation tasks, use the Navigation API with the navigate() function.

There are several types of navigation goals you can create:

  • PoseGoal - Move to a specific pose (position and orientation)
  • PathGoal - Follow a predefined path with multiple waypoints (list of poses)
  • ScriptGoal - Execute a script-based navigation sequence

For example, to create a PoseGoal:

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.nav import PoseGoal, NavigationParameters, ControlledAxes
>>>
>>> # Create a target pose 5 meters forward of the origin
>>> position = Point(x=5.0)
>>> orientation = Quaternion()
>>> target_pose = Pose(position=position, orientation=orientation)
>>>
>>> # Configure navigation parameters with relative=True to make it relative to current reference
>>> params = NavigationParameters(
...     relative=True,  # Make this a relative movement!
...     controlled_axes=ControlledAxes.rov(),  # Control x, y, z, and yaw
...     linear_epsilon=0.2,  # Tighter position tolerance
...     angular_epsilon_rad=0.1  # Tighter orientation tolerance
... )
>>>
>>> # Create and execute the navigation goal
>>> goal = PoseGoal(target_pose, params)
>>> run(navigate(goal))

This will move the vehicle 5 meters forward from its current position, not to the absolute position (5,0,0).

The NavigationParameters class allows you to specify various options for the navigation goal:

  • relative - If True, the movement is relative to the controller's current reference frame.
  • reference_frame - Allows specifying an explicit reference frame for the goal (relative to the estimator's local frame). When provided, this takes precedence over the relative parameter.
  • controlled_axes - Specifies which axes to control. This must be set to a ControlledAxes object, which specifies each axis as a boolean value.
    • ControlledAxes.all() controls all axes (x, y, z, roll, pitch, yaw).
    • ControlledAxes.rov() controls x, y, z, and yaw.
    • ControlledAxes.linear_only() controls only x, y, and z.
    • ControlledAxes.angular_only() controls only roll, pitch, and yaw.
    • ControlledAxes.none() does not control any axes.
    • Otherwise, you can create a custom ControlledAxes by specifying each controlled axis (e.g. ControlledAxes(x=True, y=True, yaw=True))
  • twist_max - Maximum speed for each axis. This is a Twist object that specifies the maximum linear and angular speeds. If any value is 0 or negative, the corresponding axis is not limited in speed.
  • linear_epsilon - Linear position tolerance for reaching the target.
  • angular_epsilon_rad - Angular position tolerance (in radians) for reaching the target.
  • linear_horizon - Linear distance horizon for the reference sent to the controller. If 0 or negative, the controller will not use a horizon.
  • angular_horizon - Angular distance horizon (in radians) for the reference sent to the controller. IF 0 or negative, the controller will not use a horizon.

The default NavigationParameters are:

NavigationParameters(
    reference_frame=None,
    relative=False,
    controlled_axes=ControlledAxes.all(),
    twist_max=None,
    linear_epsilon=0.5,
    angular_epsilon_rad=pi / 32,
    linear_horizon=0.0,
    angular_horizon=0.0
)

Reference Frame Transformation

The reference_frame parameter provides a way to express navigation goals in a different coordinate system. When specified, all goal poses will be transformed according to this reference frame.

Understanding Reference Frames

A reference frame defines both a position offset and orientation relative to the estimator's local frame. When you specify a reference frame, all poses in your navigation goal are interpreted as being relative to this new frame.

Using an Explicit Reference Frame

Here's an example of using an explicit reference frame:

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.nav import PoseGoal, NavigationParameters
>>>
>>> # Create a reference frame 10 meters forward and rotated 90 degrees (Ï€/2)
>>> ref_frame = Pose(
...     position=Point(x=10.0, y=0.0, z=0.0),
...     orientation=Quaternion.from_rph(h=1.57)  # ~90 degrees
... )
>>>
>>> # Create navigation parameters with the reference frame
>>> params = NavigationParameters(reference_frame=ref_frame)
>>>
>>> # Create a target pose at (1, 0, 0) in the reference frame
>>> target_pose = Pose(position=Point(x=1.0, y=0.0, z=0.0))
>>> goal = PoseGoal(target_pose, params)
>>>
>>> # This will move the vehicle to a point that is:
>>> # - 1 meter forward in the reference frame (x-axis of ref frame)
>>> # - Which is 10m + 1m rotated by 90° in the global frame
>>> # - So approximately (10, 1, 0) in the global frame
>>> run(navigate(goal))

The explicit reference frame transformation works as follows:

  1. The reference frame specifies both a position offset and orientation
  2. All poses in the navigation goal are interpreted as being relative to this frame
  3. The transformation applies the reference frame's orientation to the goal poses
  4. The transformed poses are then offset by the reference frame's position

Reference Frame vs. Relative Parameter

Parameter Precedence

When both reference_frame and relative=True are specified, the reference_frame parameter takes precedence and the relative parameter is ignored.

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.nav import PathGoal, NavigationParameters
>>>
>>> # Define a reference frame
>>> ref_frame = Pose(
...     position=Point(x=5.0, y=0.0, z=0.0),
...     orientation=Quaternion.from_rph(h=0.78)  # ~45 degrees
... )
>>>
>>> # Create waypoints in the reference frame
>>> waypoints = [
...     Pose(position=Point(x=0.0, y=0.0, z=0.0)),
...     Pose(position=Point(x=2.0, y=0.0, z=0.0)),
...     Pose(position=Point(x=2.0, y=2.0, z=0.0)),
...     Pose(position=Point(x=0.0, y=2.0, z=0.0)),
...     Pose(position=Point(x=0.0, y=0.0, z=0.0)),
... ]
>>>
>>> # Even though relative=True, the reference_frame takes precedence
>>> params = NavigationParameters(
...     reference_frame=ref_frame,
...     relative=True  # This will be ignored since reference_frame is provided
... )
>>>
>>> # Create and execute the path goal
>>> goal = PathGoal(waypoints, params)
>>> run(navigate(goal))

Practical Applications

Common Use Cases for Reference Frames

The reference frame parameter is particularly useful for:

  1. Survey Patterns: Define survey patterns in a convenient coordinate system
  2. Target-Relative Navigation: Define movements relative to a target or landmark
  3. Structure Inspection: Define inspection points relative to a structure's orientation
  4. Sensor Alignment: Align movements with sensor coordinate systems

Example of structure inspection:

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.nav import PathGoal, NavigationParameters
>>>
>>> # Define the structure's position and orientation
>>> structure_pose = Pose(
...     position=Point(x=20.0, y=15.0, z=-5.0),
...     orientation=Quaternion.from_rph(h=0.35)  # ~20 degrees
... )
>>>
>>> # Define inspection waypoints relative to the structure
>>> # These are defined as if the structure is at the origin and aligned with axes
>>> inspection_points = [
...     Pose(position=Point(x=0.0, y=0.0, z=2.0)),    # 2m above structure
...     Pose(position=Point(x=2.0, y=0.0, z=0.0)),    # Front side
...     Pose(position=Point(x=0.0, y=2.0, z=0.0)),    # Right side
...     Pose(position=Point(x=-2.0, y=0.0, z=0.0)),   # Back side
...     Pose(position=Point(x=0.0, y=-2.0, z=0.0)),   # Left side
... ]
>>>
>>> # Use the structure as the reference frame
>>> params = NavigationParameters(reference_frame=structure_pose)
>>>
>>> # Create and execute the inspection path
>>> goal = PathGoal(inspection_points, params)
>>> run(navigate(goal))

Path Following

Path Following

Path following navigates through a sequence of waypoints in order. The vehicle will move to each waypoint, come to a stop, and then proceed to the next waypoint.

To follow a predefined path with multiple waypoints:

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.nav import PathGoal, NavigationParameters
>>>
>>> # Create a series of waypoints forming a square
>>> waypoints = [
...     Pose(position=Point(x=2.0, y=0.0, z=0.0), orientation=Quaternion()),
...     Pose(position=Point(x=2.0, y=2.0, z=0.0), orientation=Quaternion()),
...     Pose(position=Point(x=0.0, y=2.0, z=0.0), orientation=Quaternion()),
...     Pose(position=Point(x=0.0, y=0.0, z=0.0), orientation=Quaternion())
... ]
>>>
>>> # Create and execute the path goal
>>> goal = PathGoal(waypoints)
>>> run(navigate(goal))

Survey Patterns

Survey Patterns

MOLARS provides built-in utilities for common survey patterns to simplify mission planning. These patterns can be customized to fit your specific needs.

MOLARS provides utilities for generating common survey patterns, such as transects:

>>> from mission_script.models import Pose, Point, Quaternion
>>> from mission_script.planning import transect
>>> from mission_script.nav import NavigationParameters
>>>
>>> # Create a starting pose
>>> start_pose = Pose(
...     position=Point(x=0.0, y=0.0, z=5.0),  # 5m depth
...     orientation=Quaternion()
... )
>>>
>>> # Create a transect pattern: 50m long lines, 10m apart, 5 lines total
>>> params = NavigationParameters(linear_epsilon=0.5)
>>> transect_goal = transect(
...     start=start_pose,
...     dx=50.0,    # Each line is 50m long
...     dy=40.0,    # Cover 40m in the y direction
...     lines=5,    # 5 parallel lines
...     params=params
... )
>>>
>>> # Execute the transect survey
>>> run(navigate(transect_goal))

Script-Based Navigation

MOLARS offers a powerful script-based navigation system that allows you to define sequences of movements using a simple command language. These scripts can be executed as navigation goals:

>>> from mission_script.nav import ScriptGoal, NavigationParameters, ControlledAxes
>>>
>>> # Create a navigation script that forms a square path
>>> script = """
... move abs 0, 0
... altitude abs 5.0
... heading abs 0, +
... move rel 10, 0
... move rel 0, 10
... move rel -10, 0
... move rel 0, -10
... """
>>>
>>> # Configure relative movement and controlled axes
>>> params = NavigationParameters(
...     relative=True,        # Make movements relative to current reference
...     controlled_axes=ControlledAxes.rov()  # Control x, y, z, and yaw
... )
>>>
>>> # Create and execute the script goal
>>> goal = ScriptGoal(script, params=params)
>>> run(navigate(goal))

The script language supports the following commands:

  • move abs x, y - Move to absolute position (x, y) in the local reference frame
  • move rel x, y - Move relative to current position, transformed by vehicle heading
  • move bearing deg, distance - Move at specified bearing deg (degrees) for given distance distance
  • altitude abs z - Go to absolute depth/altitude z
  • altitude rel z - Change depth/altitude by z
  • heading abs deg, direction - Set absolute heading deg in degrees (direction: + or -)
  • heading rel deg - Change heading by deg degrees

Script-based navigation is useful for creating predefined paths that can be stored and reused:

>>> # Load a script from a file
>>> with open("survey_pattern.txt") as f:
...     survey_script = f.read()
>>> 
>>> # Execute the loaded script
>>> goal = ScriptGoal(survey_script)
>>> run(navigate(goal))

Creating Asynchronous Tasks

Asynchronous Programming

MOLARS uses Python's asyncio library for asynchronous programming. This allows you to run multiple operations concurrently and build more complex behaviors.

For more complex operations, you can create asynchronous tasks that run in the background:

>>> import asyncio
>>> from copy import deepcopy
>>>
>>> # Get the current state and reference
>>> start_state = get_state()
>>> start_reference = get_reference()
>>>
>>> async def execute_sequence():
...     # Move forward 2 meters
...     await move(x=2.0)
...     print("Reached first waypoint")
...     
...     # Move right 1 meter
...     await move(y=1.0)
...     print("Reached second waypoint")
...     
...     # Return to start
...     original_pose = start_reference.pose
...     await go_to_pose(original_pose)
...     print("Returned to start")
>>>
>>> # Define an async main function that creates and manages the task
>>> async def main():
...     # Execute as a background task
...     task = asyncio.create_task(execute_sequence())
...     
...     # Continue with other operations while the sequence executes
...     print("Task started, we can do other things...")
...     
...     # Wait for the sequence to complete
...     await task
...     print("Task completed")
>>>
>>> # Run the main function
>>> run(main())
Task started, we can do other things...
Reached first waypoint
Reached second waypoint
Returned to start
Task completed

Error Handling

Error Handling

Always include error handling in your mission scripts to ensure the vehicle can recover safely from unexpected situations.

It's important to handle potential errors in your scripts:

>>> async def safe_navigation():
...     try:
...         goal_pose = Pose(position=Point(x=10.0))
...         await go_to_pose(goal_pose)
...         print("Navigation successful")
...     except Exception as e:
...         print(f"Navigation failed: {e}")
...         # Implement recovery behavior here
...         await go_to_pose(get_state().pose)  # Stay in place
>>>
>>> run(safe_navigation())

Combining with Control Flow

Power of Python

One of the advantages of using Python for mission scripting is the ability to use all of Python's control flow constructs (loops, conditionals, etc.) to create dynamic mission behaviors.

You can use Python's control flow constructs to create more complex behaviors:

>>> import random
>>>
>>> async def random_survey():
...     # Define search area bounds
...     x_min, x_max = 0.0, 10.0
...     y_min, y_max = 0.0, 10.0
...     z = 5.0  # constant depth
...     
...     # Visit 5 random points in the search area
...     for i in range(5):
...         x = random.uniform(x_min, x_max)
...         y = random.uniform(y_min, y_max)
...         
...         print(f"Moving to point {i+1}: ({x:.1f}, {y:.1f}, {z:.1f})")
...         await go_to_position(Point(x=x, y=y, z=z))
...         
...         # Simulate taking a measurement
...         print(f"Taking measurement at point {i+1}")
...         await asyncio.sleep(2.0)
...     
...     print("Survey complete")
>>>
>>> run(random_survey())

Advanced Example: Adaptive Survey

Advanced Example

This example demonstrates how to implement an adaptive survey that adjusts based on sensor readings. It showcases more complex logic and decision-making in a mission script.

This example shows how to implement an adaptive survey that adjusts based on sensor readings:

>>> from mission_script.models import Point, Pose, Quaternion
>>> from mission_script.nav import PoseGoal, NavigationParameters
>>> import random
>>> import asyncio
>>> import math
>>>
>>> def simulate_sensor_reading(point):
...     # In a real system, this would interface with actual sensors
...     # Here we just generate a random value
...     return random.random()
>>>
>>> async def detailed_scan(center_point):
...     # Perform a detailed scan around the point of interest
...     radius = 5.0  # meters
...     
...     # Scan in a circular pattern
...     for angle in range(0, 360, 45):  # 8 points around the circle
...         rad = math.radians(angle)
...         offset_x = radius * math.cos(rad)
...         offset_y = radius * math.sin(rad)
...         
...         scan_point = Point(
...             x=center_point.x + offset_x,
...             y=center_point.y + offset_y,
...             z=center_point.z
...         )
...         
...         print(f"Detailed scan at angle {angle}°")
...         await go_to_position(scan_point)
...         # In a real system, you would collect and record sensor data here
...         await asyncio.sleep(1.0)  # Simulate data collection
...     
...     # Return to center
...     await go_to_position(center_point)
>>>
>>> async def adaptive_survey():
...     # Initial survey area
...     area_x_min, area_x_max = 0.0, 100.0
...     area_y_min, area_y_max = 0.0, 100.0
...     depth = 10.0
...     
...     # Generate grid of waypoints (simplified to 3x3 for example)
...     grid_size = 3
...     x_step = (area_x_max - area_x_min) / (grid_size - 1)
...     y_step = (area_y_max - area_y_min) / (grid_size - 1)
...     
...     waypoints = []
...     for i in range(grid_size):
...         for j in range(grid_size):
...             # Alternate direction for each row (lawnmower pattern)
...             if i % 2 == 0:
...                 y = area_y_min + j * y_step
...             else:
...                 y = area_y_max - j * y_step
...             
...             x = area_x_min + i * x_step
...             waypoints.append(Point(x=x, y=y, z=depth))
...     
...     # Execute the survey, adapt if needed
...     regions_of_interest = []
...     
...     for idx, waypoint in enumerate(waypoints):
...         print(f"Moving to waypoint {idx+1}/{len(waypoints)}")
...         await go_to_position(waypoint)
...         
...         # Simulate sensor reading
...         sensor_value = simulate_sensor_reading(waypoint)
...         
...         # If sensor value exceeds threshold, mark as region of interest
...         if sensor_value > 0.7:
...             print(f"Found region of interest at {waypoint}")
...             regions_of_interest.append(waypoint)
...             
...             # Do a detailed scan around this point
...             await detailed_scan(waypoint)
...     
...     print(f"Survey complete. Found {len(regions_of_interest)} regions of interest.")
...     return regions_of_interest
>>>
>>> # Run the adaptive survey mission
>>> regions = run(adaptive_survey())
Moving to waypoint 1/9
Found region of interest at Point(x=0.0, y=0.0, z=10.0)
Detailed scan at angle 0°
Detailed scan at angle 45°
...
Moving to waypoint 2/9
...
Survey complete. Found 3 regions of interest.