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.BasicDynamicsModel.<lambda>()>,
'disturbance_vector': None,
'dragCoeff': 2.2,
'basePowerDraw': 0.0,
'wheelSpeeds': <function bsk_rl.sim.dyn.BasicDynamicsModel.<lambda>()>,
'maxWheelSpeed': inf,
'u_max': 0.2,
'rwBasePower': 0.4,
'rwMechToElecEfficiency': 0.0,
'rwElecToMechEfficiency': 0.5,
'panelArea': 1.0,
'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.BasicDynamicsModel.<lambda>()>,
'omega_init': <function bsk_rl.sim.dyn.BasicDynamicsModel.<lambda>()>,
'rN': None,
'vN': None,
'oe': <function bsk_rl.utils.orbital.random_orbit(i: Optional[float] = 45.0, alt: float = 500, r_body: float = 6371, e: float = 0, Omega: Optional[float] = None, omega: Optional[float] = 0, f: Optional[float] = None) -> Basilisk.utilities.orbitalMotion.ClassicElements>,
'mu': 398600436000000.0,
'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': 210686.167470996,
'disturbance_vector': None,
'dragCoeff': 2.210046657456083,
'basePowerDraw': 0.0,
'wheelSpeeds': array([-816.1821145 , 1283.7058494 , 43.00611181]),
'maxWheelSpeed': inf,
'u_max': 0.2,
'rwBasePower': 0.4,
'rwMechToElecEfficiency': 0.0,
'rwElecToMechEfficiency': 0.5,
'panelArea': 1.0,
'panelEfficiency': 0.2,
'nHat_B': array([ 0, 0, -1]),
'mass': 300,
'width': 1.38,
'depth': 1.04,
'height': 1.58,
'sigma_init': array([0.61557672, 0.38195961, 0.15085368]),
'omega_init': array([ 1.24763170e-05, -2.55555918e-05, 4.82727815e-05]),
'rN': None,
'vN': None,
'oe': <Basilisk.utilities.orbitalMotion.ClassicElements at 0x1075c4e80>,
'mu': 398600436000000.0,
'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.0296430408084785
New value of dragCoeff: 2.3594474912160166
New value of dragCoeff: 2.202687725963055
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
[6]:
{'sat_props': {'wheel_speeds': array([ -57.85337592, -149.13203924, -148.66485193]),
'battery_charge_fraction': 0.8640781799149756,
'r_BN_P_normd': array([-0.95867796, 0.19195187, 0.087026 ])}}
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.2986660083704773,
'opportunity_open_normd': 0.0,
'prop_2': array([5153180.64403888, -47538.05942313, 3758169.82399934])},
'target_1': {'priority': 0.6410321760315169,
'opportunity_open_normd': 0.01429045697253311,
'prop_2': array([4847885.53144301, 810420.93775173, 4064808.35984826])},
'target_2': {'priority': 0.032083764127623926,
'opportunity_open_normd': 0.0133455571753603,
'prop_2': array([4919052.35597872, 885395.17406771, 3962378.85635155])}}}
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)
2024-09-12 15:06:55,537 WARNING Creating logger for new env on PID=96241. Old environments in process may now log times incorrectly.
2024-09-12 15:06:55,743 gym INFO Resetting environment with seed=2323542144
2024-09-12 15:06:55,815 gym INFO <0.00> Environment reset
2024-09-12 15:06:55,815 gym INFO <0.00> === STARTING STEP ===
2024-09-12 15:06:55,815 sats.satellite.ActSat-1 INFO <0.00> ActSat-1: action_charge tasked for 120.0 seconds
2024-09-12 15:06:55,816 sats.satellite.ActSat-1 INFO <0.00> ActSat-1: setting timed terminal event at 120.0
2024-09-12 15:06:55,823 sats.satellite.ActSat-1 INFO <120.00> ActSat-1: timed termination at 120.0 for action_charge
2024-09-12 15:06:55,823 data.base INFO <120.00> Data reward: {'ActSat-1': 0.0}
2024-09-12 15:06:55,823 sats.satellite.ActSat-1 INFO <120.00> ActSat-1: Satellite ActSat-1 requires retasking
2024-09-12 15:06:55,824 gym INFO <120.00> Step reward: 0.0
2024-09-12 15:06:55,824 gym INFO <120.00> === STARTING STEP ===
2024-09-12 15:06:55,824 sats.satellite.ActSat-1 INFO <120.00> ActSat-1: action_desat tasked for 60.0 seconds
2024-09-12 15:06:55,825 sats.satellite.ActSat-1 INFO <120.00> ActSat-1: setting timed terminal event at 180.0
2024-09-12 15:06:55,829 sats.satellite.ActSat-1 INFO <180.00> ActSat-1: timed termination at 180.0 for action_desat
2024-09-12 15:06:55,829 data.base INFO <180.00> Data reward: {'ActSat-1': 0.0}
2024-09-12 15:06:55,829 sats.satellite.ActSat-1 INFO <180.00> ActSat-1: Satellite ActSat-1 requires retasking
2024-09-12 15:06:55,830 gym INFO <180.00> Step reward: 0.0
2024-09-12 15:06:55,830 gym INFO <180.00> === STARTING STEP ===
2024-09-12 15:06:55,830 sats.satellite.ActSat-1 INFO <180.00> ActSat-1: action_charge tasked for 600.0 seconds
2024-09-12 15:06:55,830 sats.satellite.ActSat-1 INFO <180.00> ActSat-1: setting timed terminal event at 780.0
2024-09-12 15:06:55,862 sats.satellite.ActSat-1 INFO <780.00> ActSat-1: timed termination at 780.0 for action_charge
2024-09-12 15:06:55,862 data.base INFO <780.00> Data reward: {'ActSat-1': 0.0}
2024-09-12 15:06:55,862 sats.satellite.ActSat-1 INFO <780.00> ActSat-1: Satellite ActSat-1 requires retasking
2024-09-12 15:06:55,863 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
2024-09-12 15:06:55,874 WARNING Creating logger for new env on PID=96241. Old environments in process may now log times incorrectly.
2024-09-12 15:06:56,205 gym INFO Resetting environment with seed=2784853144
2024-09-12 15:06:56,206 scene.targets INFO Generating 1000 targets
2024-09-12 15:06:56,728 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)
2024-09-12 15:06:56,733 sats.satellite.ActSat-2 INFO <0.00> ActSat-2: Finding opportunity windows from 0.00 to 600.00 seconds
2024-09-12 15:06:56,752 sats.satellite.ActSat-2 INFO <0.00> ActSat-2: Finding opportunity windows from 600.00 to 1200.00 seconds
2024-09-12 15:06:56,775 gym INFO <0.00> === STARTING STEP ===
2024-09-12 15:06:56,776 act.discrete_actions WARNING <0.00> Action 'Target(tgt-972)' is not an integer. Will attempt to use compatible set_action_override method.
2024-09-12 15:06:56,776 sats.satellite.ActSat-2 INFO <0.00> ActSat-2: Target(tgt-972) tasked for imaging
2024-09-12 15:06:56,777 sats.satellite.ActSat-2 INFO <0.00> ActSat-2: Target(tgt-972) window enabled: 1006.4 to 1137.8
2024-09-12 15:06:56,777 sats.satellite.ActSat-2 INFO <0.00> ActSat-2: setting timed terminal event at 1137.8
2024-09-12 15:06:56,855 sats.satellite.ActSat-2 INFO <1009.00> ActSat-2: imaged Target(tgt-972)
2024-09-12 15:06:56,856 data.base INFO <1009.00> Data reward: {'ActSat-2': 0.5081424423258969}
2024-09-12 15:06:56,857 sats.satellite.ActSat-2 INFO <1009.00> ActSat-2: Satellite ActSat-2 requires retasking
2024-09-12 15:06:56,857 gym INFO <1009.00> Step reward: 0.5081424423258969