Satellite Configuration

Satellites are the basic unit of agent in the environment. Four things must be specified in subclasses of Satellite:

  • The observation_spec, which defines the satellite’s observation.

  • The action_spec, which defines the satellite’s actions.

  • The dyn_type, which selects the underlying dynamics model used in simulation.

  • The fsw_type, which selects the underlying flight software model.

A very simple satellite is defined below:

[1]:
from bsk_rl import sats, act, obs, scene, data, SatelliteTasking
from bsk_rl.sim import dyn, fsw
import numpy as np

from Basilisk.architecture import bskLogging
bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)


class SimpleSatellite(sats.Satellite):
    observation_spec = [obs.Time()]  # Passed as list of instantiated classes
    action_spec = [act.Drift()]
    dyn_type = dyn.BasicDynamicsModel  # Passed as a type
    fsw_type = fsw.BasicFSWModel

Setting Satellite Parameters

Without instantiating the satellite, parameters that can be set in the various models can be inspected.

[2]:
SimpleSatellite.default_sat_args()
[2]:
{'hs_min': 0.0,
 'maxCounterValue': 4,
 'thrMinFireTime': 0.02,
 'desatAttitude': 'sun',
 'controlAxes_B': [1, 0, 0, 0, 1, 0, 0, 0, 1],
 'thrForceSign': 1,
 'K': 7.0,
 'Ki': -1,
 'P': 35.0,
 'utc_init': 'this value will be set by the world model',
 'batteryStorageCapacity': 288000.0,
 'storedCharge_Init': <function bsk_rl.sim.dyn.base.BasicDynamicsModel.<lambda>()>,
 'disturbance_vector': None,
 'dragCoeff': 2.2,
 'panelArea': 1.0,
 'basePowerDraw': 0.0,
 'wheelSpeeds': <function bsk_rl.sim.dyn.base.BasicDynamicsModel.<lambda>()>,
 'maxWheelSpeed': inf,
 'u_max': 0.2,
 'rwBasePower': 0.4,
 'rwMechToElecEfficiency': 0.0,
 'rwElecToMechEfficiency': 0.5,
 'panelEfficiency': 0.2,
 'nHat_B': array([ 0,  0, -1]),
 'mass': 330,
 'width': 1.38,
 'depth': 1.04,
 'height': 1.58,
 'sigma_init': <function bsk_rl.sim.dyn.base.DynamicsModel.<lambda>()>,
 'omega_init': <function bsk_rl.sim.dyn.base.DynamicsModel.<lambda>()>,
 'rN': None,
 'vN': None,
 'oe': <function bsk_rl.utils.orbital.random_orbit(i: Optional[float] = None, a: Optional[float] = 6871, e: float = 0, Omega: Optional[float] = None, omega: Optional[float] = None, f: Optional[float] = None, alt: float = None, r_body: float = 6371) -> Basilisk.utilities.orbitalMotion.ClassicElements>,
 'mu': 398600436000000.0,
 'min_orbital_radius': 6578136.6,
 'thrusterPowerDraw': 0.0}

These parameters can be overriden when instantiating the satellite through the sat_args argument.

[3]:
sat = SimpleSatellite(
    name="SimpleSat-1",
    sat_args=dict(
        mass=300,  # Setting a constant value
        dragCoeff=lambda: np.random.uniform(2.0, 2.4),  # Setting a randomized value
    ),
)

Each time the simulation is reset, all of the function-based randomizers are called.

[4]:
sat.generate_sat_args()  # Called by the environment on reset()
sat.sat_args
[4]:
{'hs_min': 0.0,
 'maxCounterValue': 4,
 'thrMinFireTime': 0.02,
 'desatAttitude': 'sun',
 'controlAxes_B': [1, 0, 0, 0, 1, 0, 0, 0, 1],
 'thrForceSign': 1,
 'K': 7.0,
 'Ki': -1,
 'P': 35.0,
 'utc_init': 'this value will be set by the world model',
 'batteryStorageCapacity': 288000.0,
 'storedCharge_Init': 160462.23773406853,
 'disturbance_vector': None,
 'dragCoeff': 2.284697078756926,
 'panelArea': 1.0,
 'basePowerDraw': 0.0,
 'wheelSpeeds': array([ 729.78835616, 1392.42657561, -493.29510495]),
 'maxWheelSpeed': inf,
 'u_max': 0.2,
 'rwBasePower': 0.4,
 'rwMechToElecEfficiency': 0.0,
 'rwElecToMechEfficiency': 0.5,
 'panelEfficiency': 0.2,
 'nHat_B': array([ 0,  0, -1]),
 'mass': 300,
 'width': 1.38,
 'depth': 1.04,
 'height': 1.58,
 'sigma_init': array([0.12672331, 0.52957393, 0.3049035 ]),
 'omega_init': array([-5.76695196e-06,  6.90080816e-05,  2.40917751e-05]),
 'rN': None,
 'vN': None,
 'oe': <Basilisk.utilities.orbitalMotion.ClassicElements at 0x7f45177f63b0>,
 'mu': 398600436000000.0,
 'min_orbital_radius': 6578136.6,
 'thrusterPowerDraw': 0.0}

As a result, each episode will have different randomized parameters:

[5]:
for _ in range(3):
    sat.generate_sat_args()  # Called by the environment on reset()
    print("New value of dragCoeff:", sat.sat_args["dragCoeff"])
New value of dragCoeff: 2.3626789586017134
New value of dragCoeff: 2.0708046155178153
New value of dragCoeff: 2.0766411376514533

The Observation Specification

A variety of observation elements are available for satellites. Full documentation can be found here, but some commonly used elements are explored below.

Info: In these examples, obs_type=dict is passed to the Satellite constructor so that the observation is human readable. While some RL libraries support dictionary-based observations, the default return type - the numpy array format - is more typically used.

Satellite Properties

The most common type of observations is introspective; i.e. what is my current state? Any @property in the dyn_type or fsw_type of the satellite can be accessed using SatProperties.

[6]:
class SatPropsSatellite(sats.Satellite):
    observation_spec = [
        obs.SatProperties(
            # At a minimum, specify the property to observe
            dict(prop="wheel_speeds"),
            # You can specify the module to use for the observation, but it is not necessary
            # if only one module has for the property
            dict(prop="battery_charge_fraction", module="dynamics"),
            # Properties can be normalized by some constant. This is generally desirable
            # for RL algorithms to keep values around [-1, 1].
            dict(prop="r_BN_P", norm=7e6),
        )
    ]
    action_spec = [act.Drift()]
    dyn_type = dyn.BasicDynamicsModel
    fsw_type = fsw.BasicFSWModel

env = SatelliteTasking(
    satellite=SatPropsSatellite("PropSat-1", {}, obs_type=dict),
    log_level="CRITICAL",
)
observation, _ = env.reset()
observation
/home/runner/work/bsk_rl/bsk_rl/src/bsk_rl/sim/dyn/base.py:330: BSKDeprecationWarning: Basilisk.utilities.unitTestSupport.np2EigenMatrix3d will be removed after 2027-06-23: Use simHelpers.np2EigenMatrix3d instead.
  self.scObject.hub.IHubPntBc_B = unitTestSupport.np2EigenMatrix3d(self.I_mat)
/home/runner/work/bsk_rl/bsk_rl/src/bsk_rl/sim/dyn/base.py:335: BSKDeprecationWarning: Basilisk.utilities.unitTestSupport.np2EigenVectorXd will be removed after 2027-06-23: Use simHelpers.np2EigenVectorXd instead.
  self.scObject.hub.r_CN_NInit = unitTestSupport.np2EigenVectorXd(rN)
/home/runner/work/bsk_rl/bsk_rl/src/bsk_rl/sim/dyn/base.py:336: BSKDeprecationWarning: Basilisk.utilities.unitTestSupport.np2EigenVectorXd will be removed after 2027-06-23: Use simHelpers.np2EigenVectorXd instead.
  self.scObject.hub.v_CN_NInit = unitTestSupport.np2EigenVectorXd(vN)
/home/runner/work/bsk_rl/bsk_rl/src/bsk_rl/sim/dyn/base.py:701: BSKDeprecationWarning: Basilisk.utilities.unitTestSupport.np2EigenVectorXd will be removed after 2027-06-23: Use simHelpers.np2EigenVectorXd instead.
  unitTestSupport.np2EigenVectorXd(nHat_B),
[6]:
{'sat_props': {'wheel_speeds': array([ -4.84486932, 131.4473033 , -84.47955424]),
  'battery_charge_fraction': 0.5477936985279923,
  'r_BN_P_normd': array([-0.16249286, -0.70557795,  0.66275055])}}

In some cases, you may want to access a bespoke property that is not natively implemented in a model. To do that, simply extend the model with your desired property.

[7]:
class BespokeFSWModel(fsw.BasicFSWModel):
    @property
    def meaning_of_life(self):
        return 42

class BespokeSatPropsSatellite(sats.Satellite):
    observation_spec = [
        obs.SatProperties(dict(prop="meaning_of_life"))
    ]
    action_spec = [act.Drift()]
    dyn_type = dyn.BasicDynamicsModel
    fsw_type = BespokeFSWModel

env = SatelliteTasking(
    satellite=BespokeSatPropsSatellite("BespokeSat-1", {}, obs_type=dict),
    log_level="CRITICAL",
)
observation, _ = env.reset()
observation
[7]:
{'sat_props': {'meaning_of_life': 42.0}}

Alternatively, define the property with a function that takes the satellite object as an argument.

[8]:
class CustomSatPropsSatellite(sats.Satellite):
    observation_spec = [
        obs.SatProperties(dict(prop="meaning_of_life", fn=lambda sat: 42))
    ]
    action_spec = [act.Drift()]
    dyn_type = dyn.BasicDynamicsModel
    fsw_type = fsw.BasicFSWModel

env = SatelliteTasking(
    satellite=CustomSatPropsSatellite("BespokeSat-1", {}, obs_type=dict),
    log_level="CRITICAL",
)
observation, _ = env.reset()
observation
[8]:
{'sat_props': {'meaning_of_life': 42.0}}

Opportunity Properties

Another common input to the observation is information about upcoming locations that are being accessed by the satellite. Currently, these include ground stations for downlink and targets for imaging, but OpportunityProperties will work with any location added by add_location_for_access_checking. In these examples,

[9]:
class OppPropsSatellite(sats.ImagingSatellite):
    observation_spec = [
        obs.OpportunityProperties(
            # Properties can be added by some default names
            dict(prop="priority"),
            # They can also be normalized
            dict(prop="opportunity_open", norm=5700.0),
            # Or they can be specified by an arbitrary function
            dict(fn=lambda sat, opp: opp["r_LP_P"] + 42),
            n_ahead_observe=3,
        )
    ]
    action_spec = [act.Drift()]
    dyn_type = dyn.ImagingDynModel
    fsw_type = fsw.ImagingFSWModel

env = SatelliteTasking(
    satellite=OppPropsSatellite("OppSat-1", {}, obs_type=dict),
    scenario=scene.UniformTargets(1000),
    rewarder=data.UniqueImageReward(),
    log_level="CRITICAL",
)
observation, _ = env.reset()
observation
[9]:
{'target': {'target_0': {'priority': 0.06635658561781399,
   'opportunity_open_normd': np.float64(0.0),
   'prop_2': array([6274091.64330826, 1074956.3529331 , -401812.07229673])},
  'target_1': {'priority': 0.47795290868987206,
   'opportunity_open_normd': np.float64(0.0),
   'prop_2': array([6213268.90276265, 1373221.14310639, -436784.03400394])},
  'target_2': {'priority': 0.43263810349971754,
   'opportunity_open_normd': 0.004714448935686492,
   'prop_2': array([6153770.88035017, 1647453.59838489, -313458.20878896])}}}

The Action Specification

The action specification works similarly to observation specification. A list of actions is set in the class definition of the satellite.

[14]:
class ActionSatellite(sats.Satellite):
    observation_spec = [obs.Time()]
    action_spec = [
        # If action duration is not set, the environment max_step_duration will be used;
        # however, being explicit is always preferable
        act.Charge(duration=120.0),
        act.Desat(duration=60.0),
        # One action can be included multiple time, if different settings are desired
        act.Charge(duration=600.0,),
    ]
    dyn_type = dyn.BasicDynamicsModel
    fsw_type = fsw.BasicFSWModel

env = SatelliteTasking(
    satellite=ActionSatellite("ActSat-1", {}, obs_type=dict),
    log_level="INFO",
)
env.reset()

# Try each action; index corresponds to the order of addition
_ =env.step(0)
_ =env.step(1)
_ =env.step(2)
2026-07-18 03:52:30,589                                WARNING    Creating logger for new env on PID=5314. Old environments in process may now log times incorrectly.
2026-07-18 03:52:30,591 gym                            INFO       Resetting environment with seed=1897722002
2026-07-18 03:52:30,610 gym                            INFO       <0.00> Environment reset
2026-07-18 03:52:30,611 gym                            INFO       <0.00> === STARTING STEP ===
2026-07-18 03:52:30,612 sats.satellite.ActSat-1        INFO       <0.00> ActSat-1: action_charge tasked for 120.0 seconds
2026-07-18 03:52:30,612 sats.satellite.ActSat-1        INFO       <0.00> ActSat-1: setting timed terminal event at 120.0
2026-07-18 03:52:30,620 sats.satellite.ActSat-1        INFO       <120.00> ActSat-1: timed termination at 120.0 for action_charge
2026-07-18 03:52:30,621 data.base                      INFO       <120.00> Total reward: {}
2026-07-18 03:52:30,622 comm.communication             INFO       <120.00> Optimizing data communication between all pairs of satellites
2026-07-18 03:52:30,622 sats.satellite.ActSat-1        INFO       <120.00> ActSat-1: Satellite ActSat-1 requires retasking
2026-07-18 03:52:30,624 gym                            INFO       <120.00> Step reward: 0.0
2026-07-18 03:52:30,624 gym                            INFO       <120.00> === STARTING STEP ===
2026-07-18 03:52:30,625 sats.satellite.ActSat-1        INFO       <120.00> ActSat-1: action_desat tasked for 60.0 seconds
2026-07-18 03:52:30,625 sats.satellite.ActSat-1        INFO       <120.00> ActSat-1: setting timed terminal event at 180.0
2026-07-18 03:52:30,630 sats.satellite.ActSat-1        INFO       <180.00> ActSat-1: timed termination at 180.0 for action_desat
2026-07-18 03:52:30,631 data.base                      INFO       <180.00> Total reward: {}
2026-07-18 03:52:30,631 comm.communication             INFO       <180.00> Optimizing data communication between all pairs of satellites
2026-07-18 03:52:30,632 sats.satellite.ActSat-1        INFO       <180.00> ActSat-1: Satellite ActSat-1 requires retasking
2026-07-18 03:52:30,633 gym                            INFO       <180.00> Step reward: 0.0
2026-07-18 03:52:30,634 gym                            INFO       <180.00> === STARTING STEP ===
2026-07-18 03:52:30,635 sats.satellite.ActSat-1        INFO       <180.00> ActSat-1: action_charge tasked for 600.0 seconds
2026-07-18 03:52:30,635 sats.satellite.ActSat-1        INFO       <180.00> ActSat-1: setting timed terminal event at 780.0
2026-07-18 03:52:30,668 sats.satellite.ActSat-1        INFO       <780.00> ActSat-1: timed termination at 780.0 for action_charge
2026-07-18 03:52:30,669 data.base                      INFO       <780.00> Total reward: {}
2026-07-18 03:52:30,670 comm.communication             INFO       <780.00> Optimizing data communication between all pairs of satellites
2026-07-18 03:52:30,670 sats.satellite.ActSat-1        INFO       <780.00> ActSat-1: Satellite ActSat-1 requires retasking
2026-07-18 03:52:30,672 gym                            INFO       <780.00> Step reward: 0.0

As with the observations, properties exist to help understand the actions available.

[15]:
env.action_space
[15]:
Discrete(3)
[16]:
env.unwrapped.satellite.action_description
[16]:
['action_charge', 'action_desat', 'action_charge']

Some actions take additional configurations, add multiple actions to the satellite, and/or have “special” features that are useful for manually interacting with the environment. For example, the imaging action can add an arbitrary number of actions corresponding to upcoming targets and process the name of a target directly instead of operating by action index.

[17]:
class ImageActSatellite(sats.ImagingSatellite):
    observation_spec = [obs.Time()]
    action_spec = [
        # Set the number of upcoming targets to consider
        act.Image(n_ahead_image=3)
    ]
    dyn_type = dyn.ImagingDynModel
    fsw_type = fsw.ImagingFSWModel

env = SatelliteTasking(
    satellite=ImageActSatellite("ActSat-2", {}),
    scenario=scene.UniformTargets(1000),
    rewarder=data.UniqueImageReward(),
    log_level="INFO",
)
env.reset()

env.unwrapped.satellite.action_description
2026-07-18 03:52:30,691                                WARNING    Creating logger for new env on PID=5314. Old environments in process may now log times incorrectly.
2026-07-18 03:52:30,693 gym                            INFO       Resetting environment with seed=1999704114
2026-07-18 03:52:30,694 scene.targets                  INFO       Generating 1000 targets
2026-07-18 03:52:30,735 gym                            INFO       <0.00> Environment reset
[17]:
['action_image_0', 'action_image_1', 'action_image_2']

Demonstrating the action overload feature, we task the satellite based on target name. While this is not part of the official Gym API, we find it useful in certain cases.

[18]:
target = env.unwrapped.satellite.find_next_opportunities(n=10)[9]["object"]
_ = env.step(target)
2026-07-18 03:52:30,742 sats.satellite.ActSat-2        INFO       <0.00> ActSat-2: Finding opportunity windows from 0.00 to 600.00 seconds
2026-07-18 03:52:30,777 sats.satellite.ActSat-2        INFO       <0.00> ActSat-2: Finding opportunity windows from 600.00 to 1200.00 seconds
2026-07-18 03:52:30,820 gym                            INFO       <0.00> === STARTING STEP ===
2026-07-18 03:52:30,820 act.discrete_actions           WARNING    <0.00> Action 'Target(tgt-17)' is not an integer. Will attempt to use compatible set_action_override method.
2026-07-18 03:52:30,821 sats.satellite.ActSat-2        INFO       <0.00> ActSat-2: Target(tgt-17) tasked for imaging
2026-07-18 03:52:30,821 sats.satellite.ActSat-2        INFO       <0.00> ActSat-2: Target(tgt-17) window enabled: 633.6 to 713.1
2026-07-18 03:52:30,822 sats.satellite.ActSat-2        INFO       <0.00> ActSat-2: setting timed terminal event at 713.1
2026-07-18 03:52:30,925 sats.satellite.ActSat-2        INFO       <714.00> ActSat-2: timed termination at 713.1 for Target(tgt-17) window
2026-07-18 03:52:30,925 data.base                      INFO       <714.00> Total reward: {}
2026-07-18 03:52:30,926 comm.communication             INFO       <714.00> Optimizing data communication between all pairs of satellites
2026-07-18 03:52:30,927 sats.satellite.ActSat-2        INFO       <714.00> ActSat-2: Satellite ActSat-2 requires retasking
2026-07-18 03:52:30,928 gym                            INFO       <714.00> Step reward: 0.0