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: FullFeaturedDynModel is used as the base class, and setup_reaction_wheel_dyn_effector is 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 the rwNullSpace module. 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

  • Observations:

    • 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.

  • Actions:

    • 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-09-30 17:48:16,309 gym                            INFO       Resetting environment with seed=1
2025-09-30 17:48:16,311 scene.targets                  INFO       Generating 1000 targets
2025-09-30 17:48:16,409 sats.satellite.EO              INFO       <0.00> EO: Finding opportunity windows from 0.00 to 17100.00 seconds
2025-09-30 17:48:16,723 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-09-30 17:48:16,737 gym                            INFO       <0.00> === STARTING STEP ===
2025-09-30 17:48:16,738 sats.satellite.EO              INFO       <0.00> EO: target index 26 tasked
2025-09-30 17:48:16,739 sats.satellite.EO              INFO       <0.00> EO: Target(tgt-196) tasked for imaging
2025-09-30 17:48:16,741 sats.satellite.EO              INFO       <0.00> EO: Target(tgt-196) window enabled: 2264.0 to 2376.7
2025-09-30 17:48:16,741 sats.satellite.EO              INFO       <0.00> EO: setting timed terminal event at 2376.7
2025-09-30 17:48:16,742 sats.satellite.EO              INFO       <0.00> EO: RW Power Fault: RW1's power limit reduced to 1.0 Watts at 0.0 minutes!
2025-09-30 17:48:16,744 sats.satellite.EO              INFO       <0.50> EO: imaged Target(tgt-196)
2025-09-30 17:48:16,746 data.base                      INFO       <0.50> Total reward: {'EO': 0.9675170836931263}
2025-09-30 17:48:16,746 comm.communication             INFO       <0.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,747 sats.satellite.EO              INFO       <0.50> EO: Satellite EO requires retasking
2025-09-30 17:48:16,752 gym                            INFO       <0.50> Step reward: 0.9675170836931263
2025-09-30 17:48:16,754 gym                            INFO       <0.50> === STARTING STEP ===
2025-09-30 17:48:16,755 sats.satellite.EO              INFO       <0.50> EO: target index 22 tasked
2025-09-30 17:48:16,755 sats.satellite.EO              INFO       <0.50> EO: Target(tgt-967) tasked for imaging
2025-09-30 17:48:16,757 sats.satellite.EO              INFO       <0.50> EO: Target(tgt-967) window enabled: 2032.2 to 2132.1
2025-09-30 17:48:16,757 sats.satellite.EO              INFO       <0.50> EO: setting timed terminal event at 2132.1
2025-09-30 17:48:16,759 sats.satellite.EO              INFO       <1.00> EO: imaged Target(tgt-967)
2025-09-30 17:48:16,761 data.base                      INFO       <1.00> Total reward: {'EO': 0.44341724161916973}
2025-09-30 17:48:16,761 comm.communication             INFO       <1.00> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,762 sats.satellite.EO              INFO       <1.00> EO: Satellite EO requires retasking
2025-09-30 17:48:16,768 gym                            INFO       <1.00> Step reward: 0.44341724161916973
2025-09-30 17:48:16,769 gym                            INFO       <1.00> === STARTING STEP ===
2025-09-30 17:48:16,770 sats.satellite.EO              INFO       <1.00> EO: target index 1 tasked
2025-09-30 17:48:16,770 sats.satellite.EO              INFO       <1.00> EO: Target(tgt-634) tasked for imaging
2025-09-30 17:48:16,772 sats.satellite.EO              INFO       <1.00> EO: Target(tgt-634) window enabled: 214.4 to 324.0
2025-09-30 17:48:16,772 sats.satellite.EO              INFO       <1.00> EO: setting timed terminal event at 324.0
2025-09-30 17:48:16,773 sats.satellite.EO              INFO       <1.50> EO: imaged Target(tgt-634)
2025-09-30 17:48:16,776 data.base                      INFO       <1.50> Total reward: {'EO': 0.1011278274566988}
2025-09-30 17:48:16,776 comm.communication             INFO       <1.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,777 sats.satellite.EO              INFO       <1.50> EO: Satellite EO requires retasking
2025-09-30 17:48:16,782 gym                            INFO       <1.50> Step reward: 0.1011278274566988
2025-09-30 17:48:16,783 gym                            INFO       <1.50> === STARTING STEP ===
2025-09-30 17:48:16,784 sats.satellite.EO              INFO       <1.50> EO: target index 20 tasked
2025-09-30 17:48:16,785 sats.satellite.EO              INFO       <1.50> EO: Target(tgt-842) tasked for imaging
2025-09-30 17:48:16,786 sats.satellite.EO              INFO       <1.50> EO: Target(tgt-842) window enabled: 1958.3 to 2072.1
2025-09-30 17:48:16,787 sats.satellite.EO              INFO       <1.50> EO: setting timed terminal event at 2072.1
2025-09-30 17:48:16,788 sats.satellite.EO              INFO       <2.00> EO: imaged Target(tgt-842)
2025-09-30 17:48:16,790 data.base                      INFO       <2.00> Total reward: {'EO': 0.1929743249397491}
2025-09-30 17:48:16,790 comm.communication             INFO       <2.00> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,792 sats.satellite.EO              INFO       <2.00> EO: Satellite EO requires retasking
2025-09-30 17:48:16,796 gym                            INFO       <2.00> Step reward: 0.1929743249397491
2025-09-30 17:48:16,797 gym                            INFO       <2.00> === STARTING STEP ===
2025-09-30 17:48:16,798 sats.satellite.EO              INFO       <2.00> EO: target index 8 tasked
2025-09-30 17:48:16,799 sats.satellite.EO              INFO       <2.00> EO: Target(tgt-894) tasked for imaging
2025-09-30 17:48:16,801 sats.satellite.EO              INFO       <2.00> EO: Target(tgt-894) window enabled: 799.2 to 882.8
2025-09-30 17:48:16,801 sats.satellite.EO              INFO       <2.00> EO: setting timed terminal event at 882.8
2025-09-30 17:48:16,803 sats.satellite.EO              INFO       <2.50> EO: imaged Target(tgt-894)
2025-09-30 17:48:16,805 data.base                      INFO       <2.50> Total reward: {'EO': 0.9484757208286156}
2025-09-30 17:48:16,805 comm.communication             INFO       <2.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,806 sats.satellite.EO              INFO       <2.50> EO: Satellite EO requires retasking
2025-09-30 17:48:16,811 gym                            INFO       <2.50> Step reward: 0.9484757208286156
2025-09-30 17:48:16,813 gym                            INFO       <2.50> === STARTING STEP ===
2025-09-30 17:48:16,813 sats.satellite.EO              INFO       <2.50> EO: target index 28 tasked
2025-09-30 17:48:16,814 sats.satellite.EO              INFO       <2.50> EO: Target(tgt-846) tasked for imaging
2025-09-30 17:48:16,815 sats.satellite.EO              INFO       <2.50> EO: Target(tgt-846) window enabled: 2997.6 to 3109.8
2025-09-30 17:48:16,816 sats.satellite.EO              INFO       <2.50> EO: setting timed terminal event at 3109.8
2025-09-30 17:48:16,817 sats.satellite.EO              INFO       <3.00> EO: imaged Target(tgt-846)
2025-09-30 17:48:16,819 data.base                      INFO       <3.00> Total reward: {'EO': 0.6230685362738178}
2025-09-30 17:48:16,820 comm.communication             INFO       <3.00> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,821 sats.satellite.EO              INFO       <3.00> EO: Satellite EO requires retasking
2025-09-30 17:48:16,826 gym                            INFO       <3.00> Step reward: 0.6230685362738178
2025-09-30 17:48:16,827 gym                            INFO       <3.00> === STARTING STEP ===
2025-09-30 17:48:16,828 sats.satellite.EO              INFO       <3.00> EO: target index 16 tasked
2025-09-30 17:48:16,828 sats.satellite.EO              INFO       <3.00> EO: Target(tgt-364) tasked for imaging
2025-09-30 17:48:16,830 sats.satellite.EO              INFO       <3.00> EO: Target(tgt-364) window enabled: 1768.2 to 1822.8
2025-09-30 17:48:16,831 sats.satellite.EO              INFO       <3.00> EO: setting timed terminal event at 1822.8
2025-09-30 17:48:16,832 sats.satellite.EO              INFO       <3.50> EO: imaged Target(tgt-364)
2025-09-30 17:48:16,834 data.base                      INFO       <3.50> Total reward: {'EO': 0.7048706468084478}
2025-09-30 17:48:16,835 comm.communication             INFO       <3.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,836 sats.satellite.EO              INFO       <3.50> EO: Satellite EO requires retasking
2025-09-30 17:48:16,840 gym                            INFO       <3.50> Step reward: 0.7048706468084478
2025-09-30 17:48:16,842 gym                            INFO       <3.50> === STARTING STEP ===
2025-09-30 17:48:16,842 sats.satellite.EO              INFO       <3.50> EO: target index 15 tasked
2025-09-30 17:48:16,843 sats.satellite.EO              INFO       <3.50> EO: Target(tgt-530) tasked for imaging
2025-09-30 17:48:16,844 sats.satellite.EO              INFO       <3.50> EO: Target(tgt-530) window enabled: 1545.1 to 1638.7
2025-09-30 17:48:16,845 sats.satellite.EO              INFO       <3.50> EO: setting timed terminal event at 1638.7
2025-09-30 17:48:16,847 sats.satellite.EO              INFO       <4.00> EO: imaged Target(tgt-530)
2025-09-30 17:48:16,849 data.base                      INFO       <4.00> Total reward: {'EO': 0.4436831213331952}
2025-09-30 17:48:16,849 comm.communication             INFO       <4.00> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,850 sats.satellite.EO              INFO       <4.00> EO: Satellite EO requires retasking
2025-09-30 17:48:16,855 gym                            INFO       <4.00> Step reward: 0.4436831213331952
2025-09-30 17:48:16,856 gym                            INFO       <4.00> === STARTING STEP ===
2025-09-30 17:48:16,857 sats.satellite.EO              INFO       <4.00> EO: target index 5 tasked
2025-09-30 17:48:16,857 sats.satellite.EO              INFO       <4.00> EO: Target(tgt-378) tasked for imaging
2025-09-30 17:48:16,859 sats.satellite.EO              INFO       <4.00> EO: Target(tgt-378) window enabled: 564.0 to 650.0
2025-09-30 17:48:16,859 sats.satellite.EO              INFO       <4.00> EO: setting timed terminal event at 650.0
2025-09-30 17:48:16,861 sats.satellite.EO              INFO       <4.50> EO: imaged Target(tgt-378)
2025-09-30 17:48:16,863 data.base                      INFO       <4.50> Total reward: {'EO': 0.3659991252731889}
2025-09-30 17:48:16,863 comm.communication             INFO       <4.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:16,864 sats.satellite.EO              INFO       <4.50> EO: Satellite EO requires retasking
2025-09-30 17:48:16,869 gym                            INFO       <4.50> Step reward: 0.3659991252731889
2025-09-30 17:48:16,870 gym                            INFO       <4.50> === STARTING STEP ===
2025-09-30 17:48:16,871 sats.satellite.EO              INFO       <4.50> EO: target index 9 tasked
2025-09-30 17:48:16,871 sats.satellite.EO              INFO       <4.50> EO: Target(tgt-417) tasked for imaging
2025-09-30 17:48:16,873 sats.satellite.EO              INFO       <4.50> EO: Target(tgt-417) window enabled: 872.2 to 935.8
2025-09-30 17:48:16,873 sats.satellite.EO              INFO       <4.50> EO: setting timed terminal event at 935.8
2025-09-30 17:48:17,016 sim.simulator                  INFO       <304.50> Max step duration reached
2025-09-30 17:48:17,018 data.base                      INFO       <304.50> Total reward: {}
2025-09-30 17:48:17,019 comm.communication             INFO       <304.50> Optimizing data communication between all pairs of satellites
2025-09-30 17:48:17,025 sats.satellite.EO              WARNING    <304.50> EO: failed rw_speeds_valid check
2025-09-30 17:48:17,025 gym                            INFO       <304.50> Step reward: 0.0
2025-09-30 17:48:17,026 gym                            INFO       <304.50> Episode terminated: True
2025-09-30 17:48:17,026 gym                            INFO       <304.50> Episode truncated: False
Episode complete.
Total reward: 4.791133628226008