Fault Environment Example
This tutorial demonstrates how to configure and use a simple BSK-RL environment to model faults in a system with four reaction wheels (RWs).
Load Modules
[1]:
import numpy as np
from typing import Iterable
from Basilisk.architecture import bskLogging
from Basilisk.utilities import macros, orbitalMotion, simIncludeRW
from Basilisk.simulation import reactionWheelStateEffector
from Basilisk.fswAlgorithms import rwNullSpace
from Basilisk.architecture import messaging
from bsk_rl import SatelliteTasking, act, data, obs, scene, sats
from bsk_rl.sim import dyn, fsw, world
from bsk_rl.utils.orbital import random_orbit, random_unit_vector
from bsk_rl.utils.functional import default_args
bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)
Making Faults Cases
Creating a fault base class and defining individual fault types enables modeling multiple kinds of faults within a single satellite. In this example, a power draw limit is applied to RWs, causing it to operate at reduced speed compared to nominal conditions. By default, while a torque limit is enforced, there are no restrictions on power draw. time is used to define the time at which the fault occurs, reducedLimit specifies the power draw limit in watts, and wheel_Idx indicates which
RW is affected by the fault. It can be set to a value from 1 to 4, or to all to apply the fault to every RW.
[2]:
class FaultObject:
def __init__(self, name, time, verbose=True, **kwargs):
self.name = name
self.time = time
self.verbose = verbose
self.message = None
self.message_printed = False
def execute(self, satellite):
raise NotImplementedError(
f"{self.name} does not have a custom execute function!"
)
def print_message(self, message, satellite):
if not self.message_printed:
satellite.logger.info(message)
self.message_printed = True
def addFaultToSimulation(self, satellite, listIdx):
self.uniqueFaultIdx = listIdx # Index in the faultList array.
satellite.simulator.createNewEvent(
f"add{self.name}Fault",
satellite.dynamics.dyn_rate,
True,
[f"self.TotalSim.CurrentNanos>={self.time}"],
[
f"self.faultList[{self.uniqueFaultIdx}].execute({satellite._satellite_command})",
f"self.faultList[{self.uniqueFaultIdx}].print({satellite._satellite_command})",
],
)
class RwPowerFault(FaultObject):
def __init__(self, name, time, reducedLimit, wheelIdx):
super().__init__(name, time)
self.reducedLimit = reducedLimit
if isinstance(wheelIdx, float):
# int needed around wheelIdx because np.random.choice doesn't return
# a type int, and the index will not register in execute without it.
self.wheelIdx = int(wheelIdx)
elif isinstance(wheelIdx, int) or wheelIdx == "all":
# option to trigger the fault in all wheels reflecting a larger power issue
self.wheelIdx = wheelIdx
else:
raise ValueError(
"Fault parameter 'wheelIdx' must either be a number corresponding to a reaction wheel or the string 'all'"
)
def execute(self, satellite):
dynModels = satellite.dynamics
if self.wheelIdx == 1:
dynModels.rwFactory.rwList["RW1"].P_max = self.reducedLimit
elif self.wheelIdx == 2:
dynModels.rwFactory.rwList["RW2"].P_max = self.reducedLimit
elif self.wheelIdx == 3:
dynModels.rwFactory.rwList["RW3"].P_max = self.reducedLimit
elif self.wheelIdx == 4:
dynModels.rwFactory.rwList["RW4"].P_max = self.reducedLimit
elif self.wheelIdx == "all":
# option to trigger the fault in all wheels (not supported for all fault types)
dynModels.rwFactory.rwList["RW1"].P_max = self.reducedLimit
dynModels.rwFactory.rwList["RW2"].P_max = self.reducedLimit
dynModels.rwFactory.rwList["RW3"].P_max = self.reducedLimit
dynModels.rwFactory.rwList["RW4"].P_max = self.reducedLimit
def print(self, satellite):
if self.wheelIdx == "all":
self.message = f"RW Power Fault: all RW's power limit reduced to {self.reducedLimit} Watts at {self.time*macros.NANO2MIN} minutes!"
else:
self.message = f"RW Power Fault: RW{self.wheelIdx}'s power limit reduced to {self.reducedLimit} Watts at {self.time*macros.NANO2MIN} minutes!"
super().print_message(self.message, satellite)
Configure the Simulation Models
Dynamics model:
FullFeaturedDynModelis used as the base class, andsetup_reaction_wheel_dyn_effectoris overridden to support four RWs. Two additional properties are added: the angle between the Sun and the solar panel, and the speed fraction of each RW.
[3]:
class CustomDynModel(dyn.FullFeaturedDynModel):
@property
def solar_angle_norm(self) -> float:
sun_vec_N = (
self.world.gravFactory.spiceObject.planetStateOutMsgs[self.world.sun_index]
.read()
.PositionVector
)
sun_vec_N_hat = sun_vec_N / np.linalg.norm(sun_vec_N)
solar_panel_vec_B = np.array([0, 0, -1])
mat = np.transpose(self.BN)
solar_panel_vec_N = np.matmul(mat, solar_panel_vec_B)
error_angle = np.arccos(np.dot(solar_panel_vec_N, sun_vec_N_hat))
return error_angle / np.pi
@property
def wheel_speeds_frac(self):
rw_speed = self.wheel_speeds
return rw_speed[0:4] / (self.maxWheelSpeed * macros.rpm2radsec)
@default_args(
wheelSpeeds=lambda: np.random.uniform(-1500, 1500, 4),
maxWheelSpeed=np.inf,
u_max=0.200,
)
def setup_reaction_wheel_dyn_effector(
self,
wheelSpeeds: Iterable[float],
maxWheelSpeed: float,
u_max: float,
priority: int = 997,
**kwargs,
) -> None:
"""Set the RW state effector parameters.
Args:
wheelSpeeds: Initial speeds of each wheel [RPM]
maxWheelSpeed: Failure speed for wheels [RPM]
u_max: Torque producible by wheel [N*m]
priority: Model priority.
kwargs: Ignored
"""
def balancedHR16Triad(
useRandom=False, randomBounds=(-400, 400), wheelSpeeds=[500, 500, 500, 500]
):
"""Create a set of three HR16 reaction wheels.
Args:
useRandom: Use random values for wheel speeds.
randomBounds: Bounds for random wheel speeds.
wheelSpeeds: Fixed wheel speeds.
Returns:
tuple:
* **rwStateEffector**: Reaction wheel state effector instance.
* **rwFactory**: Factory containing defined reaction wheels.
* **wheelSpeeds**: Wheel speeds.
"""
rwFactory = simIncludeRW.rwFactory()
if useRandom:
wheelSpeeds = np.random.uniform(randomBounds[0], randomBounds[1], 4)
c = 3 ** (-0.5)
rwFactory.create(
"Honeywell_HR16",
[1, 0, 0],
maxMomentum=50.0,
Omega=wheelSpeeds[0],
)
rwFactory.create(
"Honeywell_HR16",
[0, 1, 0],
maxMomentum=50.0,
Omega=wheelSpeeds[1],
)
rwFactory.create(
"Honeywell_HR16",
[0, 0, 1],
maxMomentum=50.0,
Omega=wheelSpeeds[2],
)
rwFactory.create(
"Honeywell_HR16",
[c, c, c],
maxMomentum=50.0,
Omega=wheelSpeeds[3],
)
rwStateEffector = reactionWheelStateEffector.ReactionWheelStateEffector()
return rwStateEffector, rwFactory, wheelSpeeds
self.maxWheelSpeed = maxWheelSpeed
self.rwStateEffector, self.rwFactory, _ = balancedHR16Triad(
useRandom=False,
wheelSpeeds=wheelSpeeds,
)
for RW in self.rwFactory.rwList.values():
RW.u_max = u_max
self.rwStateEffector.ModelTag = "ReactionWheels"
self.rwFactory.addToSpacecraft(
self.scObject.ModelTag, self.rwStateEffector, self.scObject
)
self.simulator.AddModelToTask(
self.task_name, self.rwStateEffector, ModelPriority=priority
)
self.Gs = np.array(
[
[1, 0, 0, 1 / np.sqrt(3)], # RW1 and RW4 x-components
[0, 1, 0, 1 / np.sqrt(3)], # RW2 and RW4 y-components
[0, 0, 1, 1 / np.sqrt(3)], # RW3 and RW4 z-components
]
)
Flight software model: A custom flight software model is defined to support four RWs. It is based on the
SteeringImagerFSWModel, with the main modification being the addition of therwNullSpacemodule. Due to the redundancy of having four RWs, there are infinitely many solutions for mapping the required body control torque to individual RW torques. To address this, once the control torque is computed, the RW null space is used to decelerate the wheels without applying additional torque to the spacecraft.
[4]:
class CustomSteeringImagerFSWModel(fsw.SteeringImagerFSWModel):
def __init__(self, *args, **kwargs) -> None:
"""Convenience type that combines the imaging FSW model with MRP steering for four reaction wheels."""
super().__init__(*args, **kwargs)
def _set_config_msgs(self) -> None:
super()._set_config_msgs()
self._set_rw_constellation_msg()
def _set_rw_constellation_msg(self) -> None:
"""Set the reaction wheel constellation message."""
rwConstellationConfig = messaging.RWConstellationMsgPayload()
rwConstellationConfig.numRW = self.dynamics.rwFactory.getNumOfDevices()
rwConfigElementList = []
for i in range(4):
rwConfigElementMsg = messaging.RWConfigElementMsgPayload()
rwConfigElementMsg.gsHat_B = self.dynamics.Gs[:, i]
rwConfigElementMsg.Js = self.dynamics.rwFactory.rwList[f"RW{i+1}"].Js
rwConfigElementMsg.uMax = self.dynamics.rwFactory.rwList[f"RW{i+1}"].u_max
rwConfigElementList.append(rwConfigElementMsg)
rwConstellationConfig.reactionWheels = rwConfigElementList
self.rwConstellationConfigInMsg = messaging.RWConstellationMsg().write(
rwConstellationConfig
)
def _set_gateway_msgs(self) -> None:
"""Create C-wrapped gateway messages."""
self.attRefMsg = messaging.AttRefMsg_C()
self.attGuidMsg = messaging.AttGuidMsg_C()
self._zero_gateway_msgs()
# connect gateway FSW effector command msgs with the dynamics
self.dynamics.rwStateEffector.rwMotorCmdInMsg.subscribeTo(
self.rwNullSpace.rwMotorTorqueOutMsg
)
self.dynamics.thrusterSet.cmdsInMsg.subscribeTo(
self.thrDump.thrusterOnTimeOutMsg
)
class MRPControlTask(fsw.SteeringImagerFSWModel.MRPControlTask):
def _create_module_data(self) -> None:
super()._create_module_data()
self.rwNullSpace = self.fsw.rwNullSpace = rwNullSpace.rwNullSpace()
self.rwNullSpace.ModelTag = "rwNullSpace"
def _setup_fsw_objects(self, **kwargs) -> None:
super()._setup_fsw_objects(**kwargs)
self.set_rw_null_space(**kwargs)
@default_args(OmegaGain=0.3)
def set_rw_null_space(
self,
OmegaGain: float,
**kwargs,
) -> None:
"""Define the null space to slow down the wheels."""
self.rwNullSpace.rwMotorTorqueInMsg.subscribeTo(
self.rwMotorTorque.rwMotorTorqueOutMsg
)
self.rwNullSpace.rwSpeedsInMsg.subscribeTo(
self.fsw.dynamics.rwStateEffector.rwSpeedOutMsg
)
self.rwNullSpace.rwConfigInMsg.subscribeTo(
self.fsw.rwConstellationConfigInMsg
)
self.rwNullSpace.OmegaGain = OmegaGain
self._add_model_to_task(self.rwNullSpace, priority=1193)
Configure the Satellite
-
SatProperties: Body angular velocity, instrument pointing direction, body position, body velocity, battery charge (properties in flight software model or dynamics model). Also, customized dynamics property in CustomDynModel above: Angle between the sun and the solar panel and four RW speed fraction.
OpportunityProperties: Target’s priority, normalized location, and target angle (upcoming 32 targets).
Time: Simulation time.
Eclipse: Next eclipse start and end times.
-
Desat: Manage momentum for the RWs for 60 seconds.
Charge: Enter a sun-pointing charging mode for 60 seconds.
Image: Image target from upcoming 32 targets
The fault is introduced by overriding the reset_post_sim_init function. The probability of the fault occurring can be specified using the fault_chance argument, and the time of occurrence can be set using the fault_time argument.
[5]:
class CustomSatComposed(sats.ImagingSatellite):
observation_spec = [
obs.SatProperties(
dict(prop="omega_BP_P", norm=0.03),
dict(prop="c_hat_P"),
dict(prop="r_BN_P", norm=orbitalMotion.REQ_EARTH * 1e3),
dict(prop="v_BN_P", norm=7616.5),
dict(prop="battery_charge_fraction"),
dict(prop="solar_angle_norm"),
dict(prop="wheel_speeds_frac"),
),
obs.OpportunityProperties(
dict(prop="priority"),
dict(prop="r_LP_P", norm=orbitalMotion.REQ_EARTH * 1e3),
dict(prop="target_angle", norm=np.pi),
type="target",
n_ahead_observe=32,
),
obs.Time(),
obs.Eclipse(norm=5700),
]
action_spec = [
act.Desat(duration=60.0),
act.Charge(duration=60.0),
act.Image(n_ahead_image=32),
]
# Modified the constructor to include the fault chance and list
def __init__(self, *args, fault_chance=0, fault_time=0.0, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.fault_chance = fault_chance
self.fault_time = fault_time
self.faultList = [] # List to store faults
def reset_post_sim_init(self) -> None:
super().reset_post_sim_init()
if np.random.random() < self.fault_chance:
powerFault = RwPowerFault(
"rwPowerLimited", self.fault_time, reducedLimit=1.0, wheelIdx=1
)
self.faultList = [powerFault]
self.simulator.faultList = self.faultList
for i in range(len(self.faultList)):
self.faultList[i].addFaultToSimulation(self, i)
dyn_type = CustomDynModel
fsw_type = CustomSteeringImagerFSWModel
Configure Satellite Pareameters
When instantiating a satellite, these parameters can be overriden with a constant or rerandomized every time the environment is reset using the sat_args dictionary.
[6]:
dataStorageCapacity = 20 * 8e6 * 100
batteryStorageCapacity = 80.0 * 3600 * 2
sat_args = CustomSatComposed.default_sat_args(
oe=random_orbit,
imageAttErrorRequirement=0.01,
imageRateErrorRequirement=0.01,
batteryStorageCapacity=batteryStorageCapacity,
storedCharge_Init=lambda: np.random.uniform(0.4, 1.0) * batteryStorageCapacity,
u_max=0.2, # More realistic values than 1.0
K1=0.5, # Updated value to have smooth and more predictable control
nHat_B=np.array([0, 0, -1]),
imageTargetMinimumElevation=np.radians(45),
rwBasePower=20,
maxWheelSpeed=1500,
storageInit=lambda: np.random.randint(
0 * dataStorageCapacity,
0.01 * dataStorageCapacity,
),
wheelSpeeds=lambda: np.random.uniform(-900, 900, 4),
disturbance_vector=lambda: random_unit_vector(),
)
# Make the satellites
satellites = []
satellites.append(
CustomSatComposed(
"EO",
sat_args,
fault_chance=1.0,
fault_time=0.0, # Fault occurs at 0.0 (nano seconds)
)
)
Making and interacting the Environment
For this example, the single-agent SatelliteTasking environment is used. n addition to the configured satellite, the environment requires a scenario, which defines the context in which the satellite operates. In this case, the scenario uses UniformTargets, placing 1000 uniformly distributed targets across the Earth’s surface. The environment also takes a rewarder, which defines how
data collected from the scenario is rewarded. Here, UniqueImageReward is used, which assigns rewards based on the sum of the priorities of uniquely imaged targets in each episode.
[7]:
env = SatelliteTasking(
satellite=satellites,
terminate_on_time_limit=True,
world_type=world.GroundStationWorldModel,
world_args=world.GroundStationWorldModel.default_world_args(),
scenario=scene.UniformTargets(n_targets=1000),
rewarder=data.UniqueImageReward(),
sim_rate=0.5,
max_step_duration=300.0,
time_limit=95 * 60 * 3,
log_level="INFO",
failure_penalty=0,
# disable_env_checker=True, # For debugging
)
First, the environment is reset. A seed is provided to ensure reproducibility of the results; it can be removed to enable randomized testing.
[8]:
observation, info = env.reset(seed=1)
2025-10-16 18:33:26,826 gym INFO Resetting environment with seed=1
2025-10-16 18:33:26,828 scene.targets INFO Generating 1000 targets
2025-10-16 18:33:26,925 sats.satellite.EO INFO <0.00> EO: Finding opportunity windows from 0.00 to 17400.00 seconds
2025-10-16 18:33:27,232 gym INFO <0.00> Environment reset
The composed satellite action space returns a human-readable action map and each satellite has the same action space and similar observation space.
[9]:
print("Actions:", satellites[0].action_description)
print("States:", env.unwrapped.satellites[0].observation_description, "\n")
# Using the composed satellite features also provides a human-readable state:
for satellite in env.unwrapped.satellites:
for k, v in satellite.observation_builder.obs_dict().items():
print(f"{k}: {v}")
Actions: ['action_desat', 'action_charge', 'action_image_0', 'action_image_1', 'action_image_2', 'action_image_3', 'action_image_4', 'action_image_5', 'action_image_6', 'action_image_7', 'action_image_8', 'action_image_9', 'action_image_10', 'action_image_11', 'action_image_12', 'action_image_13', 'action_image_14', 'action_image_15', 'action_image_16', 'action_image_17', 'action_image_18', 'action_image_19', 'action_image_20', 'action_image_21', 'action_image_22', 'action_image_23', 'action_image_24', 'action_image_25', 'action_image_26', 'action_image_27', 'action_image_28', 'action_image_29', 'action_image_30', 'action_image_31']
States: [np.str_('sat_props.omega_BP_P_normd[0]'), np.str_('sat_props.omega_BP_P_normd[1]'), np.str_('sat_props.omega_BP_P_normd[2]'), np.str_('sat_props.c_hat_P[0]'), np.str_('sat_props.c_hat_P[1]'), np.str_('sat_props.c_hat_P[2]'), np.str_('sat_props.r_BN_P_normd[0]'), np.str_('sat_props.r_BN_P_normd[1]'), np.str_('sat_props.r_BN_P_normd[2]'), np.str_('sat_props.v_BN_P_normd[0]'), np.str_('sat_props.v_BN_P_normd[1]'), np.str_('sat_props.v_BN_P_normd[2]'), np.str_('sat_props.battery_charge_fraction'), np.str_('sat_props.solar_angle_norm'), np.str_('sat_props.wheel_speeds_frac[0]'), np.str_('sat_props.wheel_speeds_frac[1]'), np.str_('sat_props.wheel_speeds_frac[2]'), np.str_('target.target_0.priority'), np.str_('target.target_0.r_LP_P_normd[0]'), np.str_('target.target_0.r_LP_P_normd[1]'), np.str_('target.target_0.r_LP_P_normd[2]'), np.str_('target.target_0.target_angle_normd'), np.str_('target.target_1.priority'), np.str_('target.target_1.r_LP_P_normd[0]'), np.str_('target.target_1.r_LP_P_normd[1]'), np.str_('target.target_1.r_LP_P_normd[2]'), np.str_('target.target_1.target_angle_normd'), np.str_('target.target_2.priority'), np.str_('target.target_2.r_LP_P_normd[0]'), np.str_('target.target_2.r_LP_P_normd[1]'), np.str_('target.target_2.r_LP_P_normd[2]'), np.str_('target.target_2.target_angle_normd'), np.str_('target.target_3.priority'), np.str_('target.target_3.r_LP_P_normd[0]'), np.str_('target.target_3.r_LP_P_normd[1]'), np.str_('target.target_3.r_LP_P_normd[2]'), np.str_('target.target_3.target_angle_normd'), np.str_('target.target_4.priority'), np.str_('target.target_4.r_LP_P_normd[0]'), np.str_('target.target_4.r_LP_P_normd[1]'), np.str_('target.target_4.r_LP_P_normd[2]'), np.str_('target.target_4.target_angle_normd'), np.str_('target.target_5.priority'), np.str_('target.target_5.r_LP_P_normd[0]'), np.str_('target.target_5.r_LP_P_normd[1]'), np.str_('target.target_5.r_LP_P_normd[2]'), np.str_('target.target_5.target_angle_normd'), np.str_('target.target_6.priority'), np.str_('target.target_6.r_LP_P_normd[0]'), np.str_('target.target_6.r_LP_P_normd[1]'), np.str_('target.target_6.r_LP_P_normd[2]'), np.str_('target.target_6.target_angle_normd'), np.str_('target.target_7.priority'), np.str_('target.target_7.r_LP_P_normd[0]'), np.str_('target.target_7.r_LP_P_normd[1]'), np.str_('target.target_7.r_LP_P_normd[2]'), np.str_('target.target_7.target_angle_normd'), np.str_('target.target_8.priority'), np.str_('target.target_8.r_LP_P_normd[0]'), np.str_('target.target_8.r_LP_P_normd[1]'), np.str_('target.target_8.r_LP_P_normd[2]'), np.str_('target.target_8.target_angle_normd'), np.str_('target.target_9.priority'), np.str_('target.target_9.r_LP_P_normd[0]'), np.str_('target.target_9.r_LP_P_normd[1]'), np.str_('target.target_9.r_LP_P_normd[2]'), np.str_('target.target_9.target_angle_normd'), np.str_('target.target_10.priority'), np.str_('target.target_10.r_LP_P_normd[0]'), np.str_('target.target_10.r_LP_P_normd[1]'), np.str_('target.target_10.r_LP_P_normd[2]'), np.str_('target.target_10.target_angle_normd'), np.str_('target.target_11.priority'), np.str_('target.target_11.r_LP_P_normd[0]'), np.str_('target.target_11.r_LP_P_normd[1]'), np.str_('target.target_11.r_LP_P_normd[2]'), np.str_('target.target_11.target_angle_normd'), np.str_('target.target_12.priority'), np.str_('target.target_12.r_LP_P_normd[0]'), np.str_('target.target_12.r_LP_P_normd[1]'), np.str_('target.target_12.r_LP_P_normd[2]'), np.str_('target.target_12.target_angle_normd'), np.str_('target.target_13.priority'), np.str_('target.target_13.r_LP_P_normd[0]'), np.str_('target.target_13.r_LP_P_normd[1]'), np.str_('target.target_13.r_LP_P_normd[2]'), np.str_('target.target_13.target_angle_normd'), np.str_('target.target_14.priority'), np.str_('target.target_14.r_LP_P_normd[0]'), np.str_('target.target_14.r_LP_P_normd[1]'), np.str_('target.target_14.r_LP_P_normd[2]'), np.str_('target.target_14.target_angle_normd'), np.str_('target.target_15.priority'), np.str_('target.target_15.r_LP_P_normd[0]'), np.str_('target.target_15.r_LP_P_normd[1]'), np.str_('target.target_15.r_LP_P_normd[2]'), np.str_('target.target_15.target_angle_normd'), np.str_('target.target_16.priority'), np.str_('target.target_16.r_LP_P_normd[0]'), np.str_('target.target_16.r_LP_P_normd[1]'), np.str_('target.target_16.r_LP_P_normd[2]'), np.str_('target.target_16.target_angle_normd'), np.str_('target.target_17.priority'), np.str_('target.target_17.r_LP_P_normd[0]'), np.str_('target.target_17.r_LP_P_normd[1]'), np.str_('target.target_17.r_LP_P_normd[2]'), np.str_('target.target_17.target_angle_normd'), np.str_('target.target_18.priority'), np.str_('target.target_18.r_LP_P_normd[0]'), np.str_('target.target_18.r_LP_P_normd[1]'), np.str_('target.target_18.r_LP_P_normd[2]'), np.str_('target.target_18.target_angle_normd'), np.str_('target.target_19.priority'), np.str_('target.target_19.r_LP_P_normd[0]'), np.str_('target.target_19.r_LP_P_normd[1]'), np.str_('target.target_19.r_LP_P_normd[2]'), np.str_('target.target_19.target_angle_normd'), np.str_('target.target_20.priority'), np.str_('target.target_20.r_LP_P_normd[0]'), np.str_('target.target_20.r_LP_P_normd[1]'), np.str_('target.target_20.r_LP_P_normd[2]'), np.str_('target.target_20.target_angle_normd'), np.str_('target.target_21.priority'), np.str_('target.target_21.r_LP_P_normd[0]'), np.str_('target.target_21.r_LP_P_normd[1]'), np.str_('target.target_21.r_LP_P_normd[2]'), np.str_('target.target_21.target_angle_normd'), np.str_('target.target_22.priority'), np.str_('target.target_22.r_LP_P_normd[0]'), np.str_('target.target_22.r_LP_P_normd[1]'), np.str_('target.target_22.r_LP_P_normd[2]'), np.str_('target.target_22.target_angle_normd'), np.str_('target.target_23.priority'), np.str_('target.target_23.r_LP_P_normd[0]'), np.str_('target.target_23.r_LP_P_normd[1]'), np.str_('target.target_23.r_LP_P_normd[2]'), np.str_('target.target_23.target_angle_normd'), np.str_('target.target_24.priority'), np.str_('target.target_24.r_LP_P_normd[0]'), np.str_('target.target_24.r_LP_P_normd[1]'), np.str_('target.target_24.r_LP_P_normd[2]'), np.str_('target.target_24.target_angle_normd'), np.str_('target.target_25.priority'), np.str_('target.target_25.r_LP_P_normd[0]'), np.str_('target.target_25.r_LP_P_normd[1]'), np.str_('target.target_25.r_LP_P_normd[2]'), np.str_('target.target_25.target_angle_normd'), np.str_('target.target_26.priority'), np.str_('target.target_26.r_LP_P_normd[0]'), np.str_('target.target_26.r_LP_P_normd[1]'), np.str_('target.target_26.r_LP_P_normd[2]'), np.str_('target.target_26.target_angle_normd'), np.str_('target.target_27.priority'), np.str_('target.target_27.r_LP_P_normd[0]'), np.str_('target.target_27.r_LP_P_normd[1]'), np.str_('target.target_27.r_LP_P_normd[2]'), np.str_('target.target_27.target_angle_normd'), np.str_('target.target_28.priority'), np.str_('target.target_28.r_LP_P_normd[0]'), np.str_('target.target_28.r_LP_P_normd[1]'), np.str_('target.target_28.r_LP_P_normd[2]'), np.str_('target.target_28.target_angle_normd'), np.str_('target.target_29.priority'), np.str_('target.target_29.r_LP_P_normd[0]'), np.str_('target.target_29.r_LP_P_normd[1]'), np.str_('target.target_29.r_LP_P_normd[2]'), np.str_('target.target_29.target_angle_normd'), np.str_('target.target_30.priority'), np.str_('target.target_30.r_LP_P_normd[0]'), np.str_('target.target_30.r_LP_P_normd[1]'), np.str_('target.target_30.r_LP_P_normd[2]'), np.str_('target.target_30.target_angle_normd'), np.str_('target.target_31.priority'), np.str_('target.target_31.r_LP_P_normd[0]'), np.str_('target.target_31.r_LP_P_normd[1]'), np.str_('target.target_31.r_LP_P_normd[2]'), np.str_('target.target_31.target_angle_normd'), np.str_('time'), np.str_('eclipse[0]'), np.str_('eclipse[1]')]
sat_props: {'omega_BP_P_normd': array([0.00137284, 0.00080893, 0.00185074]), 'c_hat_P': array([-0.94095395, -0.07120216, -0.3309621 ]), 'r_BN_P_normd': array([-0.76023893, -0.76226973, 0.03873832]), 'v_BN_P_normd': array([-0.74001565, 0.72585949, -0.23976204]), 'battery_charge_fraction': 0.48805353449026784, 'solar_angle_norm': np.float64(0.3675725758375922), 'wheel_speeds_frac': array([ 0.2222634 , -0.3546573 , 0.45374092])}
target: {'target_0': {'priority': 0.6797657443023485, 'r_LP_P_normd': array([-0.72304393, -0.69048255, 0.02100749]), 'target_angle_normd': np.float64(0.6357597658378449)}, 'target_1': {'priority': 0.1011278274566988, 'r_LP_P_normd': array([-0.8839314 , -0.46366914, -0.06063175]), 'target_angle_normd': np.float64(0.3764086411837673)}, 'target_2': {'priority': 0.6931851990942818, 'r_LP_P_normd': array([-0.92043412, -0.36952122, -0.12749548]), 'target_angle_normd': np.float64(0.3723877146688926)}, 'target_3': {'priority': 0.17225514293500632, 'r_LP_P_normd': array([-0.92565997, -0.37833103, -0.00438879]), 'target_angle_normd': np.float64(0.38981813759316086)}, 'target_4': {'priority': 0.5711709172856598, 'r_LP_P_normd': array([-0.96343073, -0.26718398, -0.02034567]), 'target_angle_normd': np.float64(0.3943146022248573)}, 'target_5': {'priority': 0.39915339691165386, 'r_LP_P_normd': array([-0.9832796 , -0.17545716, -0.04874432]), 'target_angle_normd': np.float64(0.39937897834300234)}, 'target_6': {'priority': 0.3659991252731889, 'r_LP_P_normd': array([-0.99415125, -0.07715346, -0.07556872]), 'target_angle_normd': np.float64(0.4078843692651548)}, 'target_7': {'priority': 0.8196046614443331, 'r_LP_P_normd': array([-0.99765329, -0.01207943, -0.06739442]), 'target_angle_normd': np.float64(0.4168294389284707)}, 'target_8': {'priority': 0.31321450974410614, 'r_LP_P_normd': array([-0.97449116, 0.1725315 , -0.1435265 ]), 'target_angle_normd': np.float64(0.4358647819265422)}, 'target_9': {'priority': 0.9484757208286156, 'r_LP_P_normd': array([-0.96031054, 0.17441228, -0.21767872]), 'target_angle_normd': np.float64(0.433242760838707)}, 'target_10': {'priority': 0.48592850306846924, 'r_LP_P_normd': array([-0.96853779, 0.21402263, -0.12699944]), 'target_angle_normd': np.float64(0.4426489488945429)}, 'target_11': {'priority': 0.6184129633885858, 'r_LP_P_normd': array([-0.96210743, 0.21701823, -0.16508293]), 'target_angle_normd': np.float64(0.4411215055325246)}, 'target_12': {'priority': 0.17947175160982998, 'r_LP_P_normd': array([-0.94017117, 0.243405 , -0.23839502]), 'target_angle_normd': np.float64(0.4427323667761162)}, 'target_13': {'priority': 0.7384995398085523, 'r_LP_P_normd': array([-0.80487741, 0.52282763, -0.28075545]), 'target_angle_normd': np.float64(0.48648454206124414)}, 'target_14': {'priority': 0.27114812998614257, 'r_LP_P_normd': array([-0.76729423, 0.58576762, -0.26102845]), 'target_angle_normd': np.float64(0.4977256880591026)}, 'target_15': {'priority': 0.602211552115518, 'r_LP_P_normd': array([-0.69808315, 0.6848033 , -0.20910371]), 'target_angle_normd': np.float64(0.5172281968118192)}, 'target_16': {'priority': 0.379803286768697, 'r_LP_P_normd': array([-0.49501598, 0.82573585, -0.27040613]), 'target_angle_normd': np.float64(0.5507581988576493)}, 'target_17': {'priority': 0.4436831213331952, 'r_LP_P_normd': array([-0.45042693, 0.86931897, -0.20347018]), 'target_angle_normd': np.float64(0.5625207407345922)}, 'target_18': {'priority': 0.7048706468084478, 'r_LP_P_normd': array([-0.21420724, 0.9326702 , -0.29024395]), 'target_angle_normd': np.float64(0.59373704837557)}, 'target_19': {'priority': 0.9285111717464954, 'r_LP_P_normd': array([-0.19966509, 0.94633197, -0.25414496]), 'target_angle_normd': np.float64(0.5980174335717613)}, 'target_20': {'priority': 0.6283839193934228, 'r_LP_P_normd': array([ 0.04079828, 0.96062029, -0.27485298]), 'target_angle_normd': np.float64(0.6314125127394534)}, 'target_21': {'priority': 0.1929743249397491, 'r_LP_P_normd': array([ 0.0355612 , 0.97627369, -0.21360027]), 'target_angle_normd': np.float64(0.6341742328673365)}, 'target_22': {'priority': 0.44341724161916973, 'r_LP_P_normd': array([ 0.11710105, 0.96569059, -0.23179522]), 'target_angle_normd': np.float64(0.6446733019141536)}, 'target_23': {'priority': 0.11836853522372437, 'r_LP_P_normd': array([ 0.34219224, 0.92570533, -0.16116486]), 'target_angle_normd': np.float64(0.6810789969475336)}, 'target_24': {'priority': 0.8270836989643272, 'r_LP_P_normd': array([ 0.37614852, 0.90638066, -0.19231846]), 'target_angle_normd': np.float64(0.6842502087329536)}, 'target_25': {'priority': 0.5185496026201819, 'r_LP_P_normd': array([ 0.38424706, 0.90105932, -0.20111263]), 'target_angle_normd': np.float64(0.6849377870859835)}, 'target_26': {'priority': 0.9675170836931263, 'r_LP_P_normd': array([ 0.38356319, 0.9111912 , -0.15036584]), 'target_angle_normd': np.float64(0.6878383017756565)}, 'target_27': {'priority': 0.9065897890064923, 'r_LP_P_normd': array([ 0.55753272, 0.82426831, -0.09868644]), 'target_angle_normd': np.float64(0.7179839245481688)}, 'target_28': {'priority': 0.9282669521531632, 'r_LP_P_normd': array([ 0.62887363, 0.76646723, -0.13056009]), 'target_angle_normd': np.float64(0.7278214487998897)}, 'target_29': {'priority': 0.07379201140065461, 'r_LP_P_normd': array([ 0.83349796, 0.55251892, -0.00199665]), 'target_angle_normd': np.float64(0.7770010755992804)}, 'target_30': {'priority': 0.010627938976362383, 'r_LP_P_normd': array([ 0.87933586, 0.46953401, -0.07941194]), 'target_angle_normd': np.float64(0.7821048162611637)}, 'target_31': {'priority': 0.13642904696262903, 'r_LP_P_normd': array([ 0.90564527, 0.41905752, -0.06478763]), 'target_angle_normd': np.float64(0.7904117166194159)}}
time: 0.0
eclipse: [np.float64(0.7684210526315789), np.float64(0.14210526315789473)]
The simulation runs until either the battery is depleted, a RW exceeds its maximum speed (both considered failures), or a timeout occurs (which simply stops the simulation).
[10]:
total_reward = 0.0
while True:
observation, reward, terminated, truncated, info = env.step(
env.action_space.sample()
)
total_reward += reward
if terminated or truncated:
print("Episode complete.")
break
print("Total reward:", total_reward)
2025-10-16 18:33:27,247 gym INFO <0.00> === STARTING STEP ===
2025-10-16 18:33:27,248 sats.satellite.EO INFO <0.00> EO: target index 8 tasked
2025-10-16 18:33:27,248 sats.satellite.EO INFO <0.00> EO: Target(tgt-548) tasked for imaging
2025-10-16 18:33:27,250 sats.satellite.EO INFO <0.00> EO: Target(tgt-548) window enabled: 772.6 to 881.0
2025-10-16 18:33:27,250 sats.satellite.EO INFO <0.00> EO: setting timed terminal event at 881.0
2025-10-16 18:33:27,251 sats.satellite.EO INFO <0.00> EO: RW Power Fault: RW1's power limit reduced to 1.0 Watts at 0.0 minutes!
2025-10-16 18:33:27,252 sats.satellite.EO INFO <0.50> EO: imaged Target(tgt-548)
2025-10-16 18:33:27,253 data.base INFO <0.50> Total reward: {'EO': 0.31321450974410614}
2025-10-16 18:33:27,253 comm.communication INFO <0.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,254 sats.satellite.EO INFO <0.50> EO: Satellite EO requires retasking
2025-10-16 18:33:27,259 gym INFO <0.50> Step reward: 0.31321450974410614
2025-10-16 18:33:27,259 gym INFO <0.50> === STARTING STEP ===
2025-10-16 18:33:27,260 sats.satellite.EO INFO <0.50> EO: target index 8 tasked
2025-10-16 18:33:27,260 sats.satellite.EO INFO <0.50> EO: Target(tgt-894) tasked for imaging
2025-10-16 18:33:27,262 sats.satellite.EO INFO <0.50> EO: Target(tgt-894) window enabled: 799.2 to 882.8
2025-10-16 18:33:27,262 sats.satellite.EO INFO <0.50> EO: setting timed terminal event at 882.8
2025-10-16 18:33:27,263 sats.satellite.EO INFO <1.00> EO: imaged Target(tgt-894)
2025-10-16 18:33:27,264 data.base INFO <1.00> Total reward: {'EO': 0.9484757208286156}
2025-10-16 18:33:27,265 comm.communication INFO <1.00> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,265 sats.satellite.EO INFO <1.00> EO: Satellite EO requires retasking
2025-10-16 18:33:27,270 gym INFO <1.00> Step reward: 0.9484757208286156
2025-10-16 18:33:27,270 gym INFO <1.00> === STARTING STEP ===
2025-10-16 18:33:27,271 sats.satellite.EO INFO <1.00> EO: target index 25 tasked
2025-10-16 18:33:27,271 sats.satellite.EO INFO <1.00> EO: Target(tgt-500) tasked for imaging
2025-10-16 18:33:27,272 sats.satellite.EO INFO <1.00> EO: Target(tgt-500) window enabled: 2436.6 to 2543.4
2025-10-16 18:33:27,273 sats.satellite.EO INFO <1.00> EO: setting timed terminal event at 2543.4
2025-10-16 18:33:27,274 sats.satellite.EO INFO <1.50> EO: imaged Target(tgt-500)
2025-10-16 18:33:27,275 data.base INFO <1.50> Total reward: {'EO': 0.9065897890064923}
2025-10-16 18:33:27,275 comm.communication INFO <1.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,276 sats.satellite.EO INFO <1.50> EO: Satellite EO requires retasking
2025-10-16 18:33:27,281 gym INFO <1.50> Step reward: 0.9065897890064923
2025-10-16 18:33:27,281 gym INFO <1.50> === STARTING STEP ===
2025-10-16 18:33:27,282 sats.satellite.EO INFO <1.50> EO: target index 7 tasked
2025-10-16 18:33:27,282 sats.satellite.EO INFO <1.50> EO: Target(tgt-93) tasked for imaging
2025-10-16 18:33:27,283 sats.satellite.EO INFO <1.50> EO: Target(tgt-93) window enabled: 648.0 to 671.8
2025-10-16 18:33:27,283 sats.satellite.EO INFO <1.50> EO: setting timed terminal event at 671.8
2025-10-16 18:33:27,285 sats.satellite.EO INFO <2.00> EO: imaged Target(tgt-93)
2025-10-16 18:33:27,286 data.base INFO <2.00> Total reward: {'EO': 0.8196046614443331}
2025-10-16 18:33:27,286 comm.communication INFO <2.00> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,287 sats.satellite.EO INFO <2.00> EO: Satellite EO requires retasking
2025-10-16 18:33:27,291 gym INFO <2.00> Step reward: 0.8196046614443331
2025-10-16 18:33:27,292 gym INFO <2.00> === STARTING STEP ===
2025-10-16 18:33:27,293 sats.satellite.EO INFO <2.00> EO: target index 31 tasked
2025-10-16 18:33:27,293 sats.satellite.EO INFO <2.00> EO: Target(tgt-431) tasked for imaging
2025-10-16 18:33:27,294 sats.satellite.EO INFO <2.00> EO: Target(tgt-431) window enabled: 3091.7 to 3194.3
2025-10-16 18:33:27,294 sats.satellite.EO INFO <2.00> EO: setting timed terminal event at 3194.3
2025-10-16 18:33:27,296 sats.satellite.EO INFO <2.50> EO: imaged Target(tgt-431)
2025-10-16 18:33:27,297 data.base INFO <2.50> Total reward: {'EO': 0.992581228889437}
2025-10-16 18:33:27,297 comm.communication INFO <2.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,298 sats.satellite.EO INFO <2.50> EO: Satellite EO requires retasking
2025-10-16 18:33:27,302 gym INFO <2.50> Step reward: 0.992581228889437
2025-10-16 18:33:27,303 gym INFO <2.50> === STARTING STEP ===
2025-10-16 18:33:27,303 sats.satellite.EO INFO <2.50> EO: target index 6 tasked
2025-10-16 18:33:27,304 sats.satellite.EO INFO <2.50> EO: Target(tgt-378) tasked for imaging
2025-10-16 18:33:27,304 sats.satellite.EO INFO <2.50> EO: Target(tgt-378) window enabled: 564.0 to 650.0
2025-10-16 18:33:27,305 sats.satellite.EO INFO <2.50> EO: setting timed terminal event at 650.0
2025-10-16 18:33:27,306 sats.satellite.EO INFO <3.00> EO: imaged Target(tgt-378)
2025-10-16 18:33:27,307 data.base INFO <3.00> Total reward: {'EO': 0.3659991252731889}
2025-10-16 18:33:27,308 comm.communication INFO <3.00> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,308 sats.satellite.EO INFO <3.00> EO: Satellite EO requires retasking
2025-10-16 18:33:27,313 gym INFO <3.00> Step reward: 0.3659991252731889
2025-10-16 18:33:27,313 gym INFO <3.00> === STARTING STEP ===
2025-10-16 18:33:27,314 sats.satellite.EO INFO <3.00> EO: target index 15 tasked
2025-10-16 18:33:27,314 sats.satellite.EO INFO <3.00> EO: Target(tgt-239) tasked for imaging
2025-10-16 18:33:27,315 sats.satellite.EO INFO <3.00> EO: Target(tgt-239) window enabled: 1758.4 to 1865.8
2025-10-16 18:33:27,315 sats.satellite.EO INFO <3.00> EO: setting timed terminal event at 1865.8
2025-10-16 18:33:27,317 sats.satellite.EO INFO <3.50> EO: imaged Target(tgt-239)
2025-10-16 18:33:27,317 data.base INFO <3.50> Total reward: {'EO': 0.9285111717464954}
2025-10-16 18:33:27,318 comm.communication INFO <3.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,318 sats.satellite.EO INFO <3.50> EO: Satellite EO requires retasking
2025-10-16 18:33:27,323 gym INFO <3.50> Step reward: 0.9285111717464954
2025-10-16 18:33:27,323 gym INFO <3.50> === STARTING STEP ===
2025-10-16 18:33:27,324 sats.satellite.EO INFO <3.50> EO: target index 22 tasked
2025-10-16 18:33:27,324 sats.satellite.EO INFO <3.50> EO: Target(tgt-123) tasked for imaging
2025-10-16 18:33:27,325 sats.satellite.EO INFO <3.50> EO: Target(tgt-123) window enabled: 2508.8 to 2611.9
2025-10-16 18:33:27,325 sats.satellite.EO INFO <3.50> EO: setting timed terminal event at 2611.9
2025-10-16 18:33:27,327 sats.satellite.EO INFO <4.00> EO: imaged Target(tgt-123)
2025-10-16 18:33:27,328 data.base INFO <4.00> Total reward: {'EO': 0.9282669521531632}
2025-10-16 18:33:27,328 comm.communication INFO <4.00> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,328 sats.satellite.EO INFO <4.00> EO: Satellite EO requires retasking
2025-10-16 18:33:27,333 gym INFO <4.00> Step reward: 0.9282669521531632
2025-10-16 18:33:27,333 gym INFO <4.00> === STARTING STEP ===
2025-10-16 18:33:27,334 sats.satellite.EO INFO <4.00> EO: target index 19 tasked
2025-10-16 18:33:27,334 sats.satellite.EO INFO <4.00> EO: Target(tgt-208) tasked for imaging
2025-10-16 18:33:27,335 sats.satellite.EO INFO <4.00> EO: Target(tgt-208) window enabled: 2260.7 to 2359.2
2025-10-16 18:33:27,336 sats.satellite.EO INFO <4.00> EO: setting timed terminal event at 2359.2
2025-10-16 18:33:27,337 sats.satellite.EO INFO <4.50> EO: imaged Target(tgt-208)
2025-10-16 18:33:27,337 data.base INFO <4.50> Total reward: {'EO': 0.8270836989643272}
2025-10-16 18:33:27,338 comm.communication INFO <4.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,338 sats.satellite.EO INFO <4.50> EO: Satellite EO requires retasking
2025-10-16 18:33:27,343 gym INFO <4.50> Step reward: 0.8270836989643272
2025-10-16 18:33:27,344 gym INFO <4.50> === STARTING STEP ===
2025-10-16 18:33:27,344 sats.satellite.EO INFO <4.50> EO: target index 11 tasked
2025-10-16 18:33:27,345 sats.satellite.EO INFO <4.50> EO: Target(tgt-594) tasked for imaging
2025-10-16 18:33:27,345 sats.satellite.EO INFO <4.50> EO: Target(tgt-594) window enabled: 1276.7 to 1383.1
2025-10-16 18:33:27,346 sats.satellite.EO INFO <4.50> EO: setting timed terminal event at 1383.1
2025-10-16 18:33:27,416 sim.simulator INFO <304.50> Max step duration reached
2025-10-16 18:33:27,417 data.base INFO <304.50> Total reward: {}
2025-10-16 18:33:27,417 comm.communication INFO <304.50> Optimizing data communication between all pairs of satellites
2025-10-16 18:33:27,421 sats.satellite.EO WARNING <304.50> EO: failed rw_speeds_valid check
2025-10-16 18:33:27,421 gym INFO <304.50> Step reward: 0.0
2025-10-16 18:33:27,422 gym INFO <304.50> Episode terminated: True
2025-10-16 18:33:27,422 gym INFO <304.50> Episode truncated: False
Episode complete.
Total reward: 7.0303268580501594