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 vehiclemission_script.models- Data models for representing vehicle statemission_script.nav- Navigation goals and parametersmission_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:
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).
Navigation Parameters¶
The NavigationParameters class allows you to specify various options for the navigation goal:
relative- IfTrue, 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 therelativeparameter.controlled_axes- Specifies which axes to control. This must be set to aControlledAxesobject, 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
ControlledAxesby specifying each controlled axis (e.g.ControlledAxes(x=True, y=True, yaw=True))
twist_max- Maximum speed for each axis. This is aTwistobject 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:
- The reference frame specifies both a position offset and orientation
- All poses in the navigation goal are interpreted as being relative to this frame
- The transformation applies the reference frame's orientation to the goal poses
- 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:
- Survey Patterns: Define survey patterns in a convenient coordinate system
- Target-Relative Navigation: Define movements relative to a target or landmark
- Structure Inspection: Define inspection points relative to a structure's orientation
- 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 framemove rel x, y- Move relative to current position, transformed by vehicle headingmove bearing deg, distance- Move at specified bearingdeg(degrees) for given distancedistancealtitude abs z- Go to absolute depth/altitudezaltitude rel z- Change depth/altitude byzheading abs deg, direction- Set absolute headingdegin degrees (direction:+or-)heading rel deg- Change heading bydegdegrees
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.