Introduction
The Python Bindings panel allows you to run custom Python code inside SatModeler’s simulation loop, at spacecraft level. This is the main extension mechanism in SatModeler: it lets you add behavior that is not provided by built-in models, without modifying the simulator itself.
In the current version, each spacecraft exposes four binding slots:
- External Force 1 → returns a force vector [Fx, Fy, Fz] in N
- External Force 2 → returns a force vector [Fx, Fy, Fz] in N
- External Torque 1 → returns a torque vector [Tx, Ty, Tz] in N·m
- External Torque 2 → returns a torque vector [Tx, Ty, Tz] in N·m
These four slots are intentionally generic. You can use them to apply real physical effects (thrusters, magnetic torquers, reaction wheels proxy models, deployables, custom drag/lift, custom SRP, etc.), but also to simulate onboard subsystems such as:
- Sensors and estimation pipelines
- Actuator logic and control laws
- OBC (on-board computer) decision making
- Power generation / battery models
- Custom telemetry logging and fault injection
Even if your subsystem does not naturally produce a force or torque, you can still run it in the binding and return a zero vector.
How bindings are executed (important)
SatModeler uses a Runge–Kutta 4 (RK4) numerical integrator.
As a result:
- Each enabled Python binding is called four times per solver time step (Δt).
This behavior is expected and correct.
Practical implications:
- Avoid heavy computation and excessive printing inside bindings.
- If logic should update only once per solver step, implement gating or caching based on time.
Main workflow
Create Python File
Clicking Create Python File generates a ready-to-edit .py file containing four default functions:
fcn_ext_force_1fcn_ext_force_2fcn_ext_torque_1fcn_ext_torque_2
The file and function names are automatically linked to the corresponding binding slots.
By default, all functions return [0.0, 0.0, 0.0], so the simulation behavior is unchanged until custom logic is added.
Linking your own file and functions
Each binding slot contains:
- Path — the
.pyfile containing your function - Function Name — the Python function to be called
This allows flexible project organization, for example:
- One shared file used by multiple spacecraft
- One file per spacecraft
- One file per subsystem (ADCS, power, payload, etc.)
Required function signature
Each slot calls a Python function with the signature:
def my_function(SimData):
return [x, y, z]SimDatais the simulation context passed by SatModeler (explained in detail below).- The function must return a 3-element vector.
Each function return has units. These are:
- External Force 1 and 2 must return [Fx, Fy, Fz] in Newtons (N)
- External Torque 1 and 2 must return [Tx, Ty, Tz] in Newton-meters (N·m)
If you do not want to apply any force/torque but still want to run code, return [0.0, 0.0, 0.0].
Using your own Python classes
Bindings are standard Python. You can define classes, instantiate them once, and reuse them on every call.
A common pattern is:
- Define a class representing a subsystem (e.g., a sensor, controller, power system)
- Create a single global instance of that class
- Use that instance inside the binding function
Example:
import numpy as np
class BDotController:
def __init__(self, gain):
self.gain = gain
self.last_t = None
self.cached_torque = np.zeros(3)
def update_once_per_step(self, SimData):
# Gate updates so we only compute once per solver step (even though RK4 calls 4 times)
t = SimData.elapsed_time
if self.last_t == t:
return self.cached_torque
self.last_t = t
sc = SimData.Spacecraft
w = np.array(sc.angular_velocity_BF) # rad/s
B = np.array(sc.magnetic_field_BF) # T
# Illustrative example: compute a torque direction based on w and B
# (Your own model / units strategy applies)
self.cached_torque = -self.gain * np.cross(w, B)
return self.cached_torque
controller = BDotController(gain=1e-3)
def fcn_ext_torque_1(SimData):
T = controller.update_once_per_step(SimData)
return T.tolist()This approach is ideal for modeling sensors/actuators/OBC/power because you can store internal state (filters, integrators, counters, memory, etc.) inside your class.
SimData
What is SimData?
SimData is a data container SatModeler passes to every binding call. It gives your Python function access to:
- The current simulation time and step size
- The current UTC simulation date
- The environment (Earth, Sun, Moon, planets)
- The spacecraft that owns this binding
- All spacecraft in the mission
| Category | Python field | What it is | Units / type |
|---|---|---|---|
| Solver | SimData.elapsed_time |
Elapsed simulation time since start | s |
SimData.delta_time |
Solver step size (Δt) | s | |
SimData.maximum_simulation_time |
Maximum simulation duration | s | |
SimData.current_simulation_date_UTC |
Current simulation date/time in UTC | GregorianDate |
|
| Spacecraft | SimData.Spacecraft |
The spacecraft associated with this binding call | Spacecraft |
SimData.all_spacecraft |
All spacecraft in the mission (includes SimData.Spacecraft) |
list of Spacecraft |
|
| Environment | SimData.Earth |
Earth model used in the simulation | Earth |
SimData.Sun |
Sun model used in the simulation | Sun |
|
SimData.Moon |
Moon model used in the simulation | Moon |
|
SimData.Mercury |
Mercury model | CelestialBody |
|
SimData.Venus |
Venus model | CelestialBody |
|
SimData.Mars |
Mars model | CelestialBody |
|
SimData.Jupiter |
Jupiter model | CelestialBody |
|
SimData.Saturn |
Saturn model | CelestialBody |
|
SimData.Uranus |
Uranus model | CelestialBody |
|
SimData.Neptune |
Neptune model | CelestialBody |
|
SimData.Pluto |
Pluto model | CelestialBody |
Accessing the Simulation date (GregorianDate)
SimData.current_simulation_date_UTC exposes:
SimData.current_simulation_date_UTC.yearSimData.current_simulation_date_UTC.monthSimData.current_simulation_date_UTC.daySimData.current_simulation_date_UTC.hourSimData.current_simulation_date_UTC.minuteSimData.current_simulation_date_UTC.second
Example:
utc = SimData.current_simulation_date_UTC
print(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second)Environment objects
CelestialBody (generic planets)
Planets are exposed as CelestialBody. You can read:
body.grav_param— gravitational parameter μ [m³/s²]body.name— name of the bodybody.pos_ICRF_from_solar_sys_bar— ICRF position w.r.t Solar System barycenter [m]
Example:
mars = SimData.Mars
mu = mars.grav_param
r_icrf = mars.pos_ICRF_from_solar_sys_barEarth-specific fields
In addition to CelestialBody fields, Earth exposes:
SimData.Earth.mass[kg]SimData.Earth.angular_velocity[rad/s]SimData.Earth.C_ECI_ECEF— ECEF→ECI rotation matrixSimData.Earth.q_ECI_ECEF— quaternion form of that transformSimData.Earth.determine_mag_field_ECI(lat_deg, lon_deg, h_ellip, dec_year)→ returns B in ECI [T]
Sun-specific fields
SimData.Sun.pos_ECI[m]SimData.Sun.unit_pos_ECISimData.Sun.right_ascension[rad]SimData.Sun.declination[rad]
Moon-specific fields
SimData.Moon.pos_ICRF_from_Earth[m]
Spacecraft object
| Category | Python field | What it is | Units / type |
|---|---|---|---|
| Metadata | sc.name |
Spacecraft name | string |
| Physical | sc.mass |
Spacecraft mass | kg |
sc.inertia_matrix_BF |
Inertia matrix in body frame | kg·m² | |
| Environment at S/C | sc.atmospheric_density |
Atmospheric density at current position | kg/m³ |
sc.magnetic_field_BF |
Earth magnetic field at S/C, in body frame | T | |
sc.eclipse_status |
Eclipse state (sunlight/penumbra/umbra/annular) | EclipseType |
|
| Translational | sc.pos_ECI |
Position vector in ECI | m |
sc.vel_ECI |
Velocity vector in ECI | m/s | |
sc.pos_ECEF |
Position vector in ECEF | m | |
sc.vel_ECEF |
Velocity vector in ECEF | m/s | |
sc.lat_gd |
Geodetic latitude | rad | |
sc.lon |
Longitude | rad | |
sc.h_ellip |
Altitude above reference ellipsoid | m | |
sc.orbital_elements |
Keplerian elements derived from current state | KeplerianElements |
|
| Attitude | sc.angular_velocity_BF |
Angular velocity in body frame | rad/s |
sc.quaternion_BF_ECI |
Body w.r.t ECI quaternion | Quaternion |
|
sc.rotation_matrix_BF_ECI |
Body w.r.t ECI rotation matrix | 3×3 matrix | |
| Perturbations | sc.gravity_acc_ECI |
Gravity acceleration in ECI | m/s² |
sc.third_body_acc_ECI |
Total third-body acceleration in ECI | m/s² | |
sc.solar_pressure_acc_ECI |
Solar pressure acceleration in ECI | m/s² | |
sc.solar_pressure_torque_BF |
Solar pressure torque in body frame | N·m | |
sc.aerodynamic_acc_ECI |
Aerodynamic acceleration in ECI | m/s² | |
sc.aerodynamic_torque_BF |
Aerodynamic torque in body frame | N·m | |
sc.magnetic_torque_BF |
Magnetic torque in body frame | N·m | |
sc.gravity_gradient_torque_BF |
Gravity-gradient torque in body frame | N·m |
KeplerianElements fields
kep = sc.orbital_elements
kep.p— semi-parameter [m]kep.a— semi-major axis [m]kep.e— eccentricitykep.i— inclination [rad]kep.right_asc— RAAN [rad]kep.arg_of_perigee— argument of perigee [rad]kep.true_anom— true anomaly [rad]kep.true_arg_of_perigee— extended element [rad]kep.arg_of_lat— extended element [rad]kep.true_lon— extended element [rad]
Quaternion fields
If you use sc.quaternion_BF_ECI, you can access:
q.x(),q.y(),q.z(),q.w()q.coeffs()→ returns[x, y, z, w]
Minimal “start here” example
import numpy as np
def fcn_ext_force_1(SimData):
sc = SimData.Spacecraft
t = SimData.elapsed_time
r = np.array(sc.pos_ECI)
v = np.array(sc.vel_ECI)
# Do whatever logic you want here...
# Return a real force, or return zero
return [0.0, 0.0, 0.0]
