Repository navigation
Programming STP Sims with Python
| Simulation Development Workflow → Programming STP Sims with Python |
|---|
Written by Neil McHenry and Connor Jakubik.
Prerequisites:
Note
This page explains how to author and run custom Python systems that interact with live simulations. For initial state configuration and JSON syntax, review The Sim Config. For parameter typing details, review The Parameter.
In Space Teams PRO, simulation tasks are organized cleanly into State and Behavior:
-
Entities (State): Everything that exists spatially in a simulation (spacecraft, celestial bodies, robotic arms, sensor masts) is an Entity. Entities store their state as parameters in a
ParamMap. Any parameter change on an Entity is replicated across the network to all connected clients. - Systems (Behavior): Logic that evaluates algorithms, calculates mathematical equations, reads inputs, and drives Entity parameters over time.
┌────────────────────────────────────────────────────────┐
│ Space Teams PRO │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ Rover Entity │ │ Planet Entity │ │
│ │ (Location, State) │ │ (BodyFixedFrame) │ │
│ └─────────▲──────────┘ └─────────▲──────────┘ │
│ │ │ │
│ Param Access Frame Query │
│ │ │ │
│ ┌─────────┴───────────────────────────┴──────────┐ │
│ │ Example Python System Process │ │
│ │ - Reads Config Inputs │ │
│ │ - Steps Kinematic Loop │ │
│ │ - Updates Entity Location & Rotation │ │
│ │ - Emits HUD Alerts and Logs │ │
│ └────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
Systems can be written as compiled C++ shared libraries (.dll or .so) or as standalone Python scripts (.py). When a simulation starts, Compute_Server launches configured Python systems as separate processes and connects them directly to the simulation runtime.
Important
Replication Rule: Only Entities have automatic parameter replication across the network. System instance parameters do not replicate. Systems are designed to execute locally, updating entity parameters that the platform core then synchronizes across all users. So, don't use System instance parameters to store information that needs to be saved mid-sim or that may be required by another portion of the sim.
Every Space Teams Python system must open with the standard connection and sim start boilerplate. Follow the instructions here on making a new Python System then come back.
Here is the top section of Python Template boilerplate code from v0.54.2:
import os, sys, time, datetime, traceback
import spaceteams as st
import numpy as np
# Can import other modules here, but any code that actually executes before st.connect_to_sim()
# will have less helpful logging and error handling. SimGlobals and logger functions are not yet available.
def before_init():
pass # Replace this to run code during the Sync state that needs to finish before Init starts anywhere in the sim.
# Sync state is where Sim Editing is done, so code in here shouldn't change parameters that would
# make saving and reloading the sim problematic.
def before_runtime():
pass # Replace this to run code during the Init state that needs to finish before Runtime starts anywhere in the sim.
#################################################################
# DON'T CHANGE ANY OF THE BELOW; NECESSARY FOR JOINING SIMULATION
def custom_exception_handler(exctype, value, tb):
error_message = "".join(traceback.format_exception(exctype, value, tb))
st.logger_fatal(error_message)
exit(1)
sys.excepthook = custom_exception_handler
st.BeforeInit(before_init)
st.BeforeRuntime(before_runtime)
st.connect_to_sim(sys.argv)
# DON'T CHANGE ANY OF THE ABOVE; NECESSARY FOR JOINING SIMULATION
#################################################################This boilerplate code is explained in the Make New Python System wiki page. Once the code has passed the st.connect_to_sim line, it is part of the running simulation.
A Python system accesses its own configuration settings and references to specific entities using st.GetThisSystem().
this = st.GetThisSystem()
# Read per-system-instance parameters configured in the SimConfig
loop_rate = this.GetParam(st.VarType.double, "LoopFreqHz")
drive_speed = this.GetParam(st.VarType.double, "DriveSpeed")
enable_feature = this.GetParam(st.VarType.bool, "EnableFeature")
# Retrieve Entity references from instance parameters in the SimConfig
rover_entity: st.Entity = this.GetParam(st.VarType.entityRef, "TargetEntity")
planet_entity: st.Entity = this.GetParam(st.VarType.entityRef, "PlanetRef")If entities have the Python System applied to them, instead of being individually referenced by an EntityRef parameter, they can be retrieved with st.get_entities().
entities = st.get_entities()
for en in entities:
st.logger_info(en.getName())| Enum | Python Type | Description |
|---|---|---|
st.VarType.bool |
bool |
Boolean value (True or False) |
st.VarType.int32 |
int |
32-bit signed integer |
st.VarType.double |
float |
Standard 64-bit floating point number |
st.VarType.string |
str |
Text string |
st.VarType.doubleV3 |
np.ndarray (3,) |
3D coordinate vector [x, y, z]
|
st.VarType.doubleV4 |
np.ndarray (4,) |
4D vector or quaternion [x, y, z, w]
|
st.VarType.entityRef |
st.Entity |
Reference pointer to a simulation entity |
See the full set of Python parameter types on the Python API wiki page.
Tip
In C++, EntityRef parameters return a SC_EntityRef object that must be .lock()ed to access the Entity inside it. In the Python API, this.GetParam(st.VarType.entityRef, ...) handles reference locking internally, returning an owning st.Entity reference. This has special significance when entities are deleted, but their owning references keep their data alive.
Space Teams uses a hierarchy of reference frames to manage parent-child spatial relationships. Input or output of entity 3D state is always relative to a frame. Commonly, one uses a Body-Fixed Frame of another entity as the frame.
# Obtain reference frames
planet_frame = planet_entity.GetBodyFixedFrame()
# Retrieve location expressed in the planet's coordinate frame
current_loc = rover_entity.getLocation().WRT_ExprIn(planet_frame)
# WRT_ExprIn means "With Respect To, and Expressed in"
print(f"Current coordinates (m): X={current_loc[0]}, Y={current_loc[1]}, Z={current_loc[2]}")This code means: "current_loc is the location of the rover entity with respect to the planet body fixed frame, expressed as a vector with x,y,z components in the planet body fixed frame."
To translate an entity through space, compute the target coordinates and apply them using one of the following methods:
# Option A: Direct Location Update (Kinematic Teleport / Prescribed Motion)
new_loc = current_loc + np.array([5.0, 0.0, 0.0])
rover_entity.setLocation(st.frames.FramedLoc(new_loc, planet_frame))
# Option B: Set Location and Zero-out Velocity and Acceleration in the given frame
rover_entity.setLocation_ZeroVel(st.frames.FramedLoc(new_loc, planet_frame))
# Option C: Set Location, Velocity, and Acceleration Simultaneously
vel_vector = np.array([2.5, 0.0, 0.0])
acc_vector = np.array([0.0, 0.0, 0.0])
rva_data = st.frames.rva_struct(new_loc, vel_vector, acc_vector)
rover_entity.setLocVelAcc(st.frames.FramedLocVelAcc(rva_data, planet_frame))Orientations are applied using FramedRot objects constructed from Direction Cosine Matrices (DCM):
# Construct a rotation matrix (3x3 DCM) or quaternion
dcm_matrix = np.identity(3)
rover_entity.setRotation(st.frames.FramedRot(dcm_matrix, planet_frame))We have a set of rotation formalism conversion functions in the st.math namespace, such as st.math.Quat_to_DCM(), st.math.DCM_to_Euler321(), and the other combinations.
Space Teams provides built-in mechanisms to present information directly to users inside the simulation viewport:
# On-Screen Alert: Temporary banner across user viewports (auto-dismisses)
st.OnScreenAlert(
"Target waypoint reached!",
"NavigationTopic",
st.Severity.Info
)
# On-Screen Log Message: Appends entry into persistent HUD message feed
st.OnScreenLogMessage(
"Thruster subsystem initialized.",
"Subsystems",
st.Severity.Info
)
# Logger output to Compute_Server console and session logs
st.logger_info("Step completed successfully.")Severity levels are controlled by the st.Severity enum: Info, Warning, Error, Fatal, and Chat. Fatal will, for some of the functions, immediately stop the sim with the output as the error message.
Below is a complete, standalone Python system that moves an entity in a horizontal circle around an origin point while logging progress:
import os, sys, time, datetime, traceback
import spaceteams as st
import numpy as np
# Can import other modules here, but any code that actually executes before st.connect_to_sim()
# will have less helpful logging and error handling. SimGlobals and logger functions are not yet available.
def before_init():
pass # Replace this to run code during the Sync state that needs to finish before Init starts anywhere in the sim.
# Sync state is where Sim Editing is done, so code in here shouldn't change parameters that would
# make saving and reloading the sim problematic.
def before_runtime():
pass # Replace this to run code during the Init state that needs to finish before Runtime starts anywhere in the sim.
#################################################################
# DON'T CHANGE ANY OF THE BELOW; NECESSARY FOR JOINING SIMULATION
def custom_exception_handler(exctype, value, tb):
error_message = "".join(traceback.format_exception(exctype, value, tb))
st.logger_fatal(error_message)
exit(1)
sys.excepthook = custom_exception_handler
st.BeforeInit(before_init)
st.BeforeRuntime(before_runtime)
st.connect_to_sim(sys.argv)
# DON'T CHANGE ANY OF THE ABOVE; NECESSARY FOR JOINING SIMULATION
#################################################################
st.OnScreenLogMessage("CircularMotionSystem starting...", "Navigation", st.Severity.Info)
# 1. Grab configuration parameters and entities
this = st.GetThisSystem()
target_entity: st.Entity = this.GetParam(st.VarType.entityRef, "ControlledEntity")
center_ref: st.Entity = this.GetParam(st.VarType.entityRef, "CenterFrameEntity")
reference_frame = center_ref.GetBodyFixedFrame()
# Motion parameters
radius = 25.0 # Orbit radius (meters)
angular_speed = 0.2 # Yaw rotation speed (rad/s)
loop_freq = this.GetParam(st.VarType.double, "LoopFreqHz") # Target frequency (Hz)
dt = 1.0 / loop_freq
st.OnScreenAlert("Entity autonomous movement engaged.", "Motion", st.Severity.Info)
time_prev = st.SimGlobals.SimClock.GetTimeNow()
time_now = time_prev
theta = 0.0
while True:
# Update sim timing
time_now = st.SimGlobals.SimClock.GetTimeNow()
dt = time_now - time_prev
time_prev = time_now
# 2. Compute circular trajectory coordinates
x = radius * np.cos(theta)
y = radius * np.sin(theta)
z = 0.0 # Planar elevation
# 3. Compute tangent velocity vector
vx = -radius * angular_speed * np.sin(theta)
vy = radius * angular_speed * np.cos(theta)
vz = 0.0
new_pos = np.array([x, y, z])
vel_vec = np.array([vx, vy, vz])
acc_vec = np.array([0.0, 0.0, 0.0]) # of course, real circular motion has acceleration, and that could be calculated
# 4. Commit kinematic update to entity
kinematic_state = st.frames.rva_struct(new_pos, vel_vec, acc_vec)
target_entity.setLocVelAcc(st.frames.FramedLocVelAcc(kinematic_state, reference_frame))
# 5. Advance phase angle and maintain execution pacing
theta += angular_speed * dt
if theta >= 2.0 * np.pi:
theta -= 2.0 * np.pi
# Note that this loop frequency does not track with possible timescale in the sim
time.sleep(1.0 / loop_freq)To run your Python script within a simulation, register it inside your .simconf file under SystemManager -> Systems:
{
"Nametag": "CircularMotionSystem",
"Source": "Local/Repos/MyRepo/CircularMotionSystem.py",
"Type": "Python",
"Active": true,
"Frequency": 50.0,
"Inst_Parameters": {
"ControlledEntity": [ "EntityRef", "Excavator_1" ],
"CenterFrameEntity": [ "EntityRef", "Moon" ],
"DriveSpeed": 2.5
}
}
All EntityRef parameters must reference an actual entity in the sim, or its included sims.
-
Nametag: Unique system identifier. -
Source: Path to your Python file (relative to your installation or local assets directory). -
Type: Must be set to"Python". -
Inst_Parameters: Defines initial parameters queried bythis.GetParam()inside your script.
-
Always Include Loop Sleeping: Ensure your update loop includes
time.sleep(1.0 / frequency), or something else like a UI library is handling the update rate. Without pacing, Python will consume 100% of a CPU core. -
Throttle Screen Messages: Do not call
st.OnScreenAlert()orst.OnScreenLogMessage()every frame in high-frequency loops; rate-limit notifications to avoid filling HUD message queues. -
Installing Third-Party Libraries: If your system imports external packages that we don't include by default (like
torchorrasterio), install them into Space Teams PRO's automatically generated virtual environment. We have a standard way of doing this:- On Windows:
- Open
Terminalin the<YOUR STP INSTALL DIR>\Compute_Serverfolder, and run this command:.\st_pip.bat install package_name
- Open
- On Linux:
-
sourcethe python virtual environment at~/.local/share/SimDynamX/STPro/Versions/v<STP VERSION HERE>/py
pip install package_name
-
- On Windows:
- Consult the Python API Docs for functionality details.