From 2adee8e3d25fbd42ae8241bf05a85ba476000e6c Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Thu, 2 Oct 2025 17:02:57 -0600 Subject: [PATCH 1/8] [#156] Make dynamics, fsw more modular --- src/bsk_rl/gym.py | 2 + src/bsk_rl/sim/dyn/__init__.py | 3 +- src/bsk_rl/sim/dyn/base.py | 168 +++++++++++++------------- src/bsk_rl/sim/dyn/relative_motion.py | 8 +- src/bsk_rl/sim/fsw/__init__.py | 10 +- src/bsk_rl/sim/fsw/base.py | 160 +++++++++++++----------- src/bsk_rl/sim/fsw/orbital.py | 4 +- 7 files changed, 195 insertions(+), 160 deletions(-) diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index 9a673aa7..358a735b 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -365,6 +365,8 @@ def reset( self.scenario.reset_during_sim_init() self.rewarder.reset_during_sim_init() self.communicator.reset_during_sim_init() + for satellite in self.satellites: + satellite.reset_during_sim_init() self.simulator.finish_init() diff --git a/src/bsk_rl/sim/dyn/__init__.py b/src/bsk_rl/sim/dyn/__init__.py index e8e3db33..0a8f7c83 100644 --- a/src/bsk_rl/sim/dyn/__init__.py +++ b/src/bsk_rl/sim/dyn/__init__.py @@ -38,7 +38,7 @@ from deprecated import deprecated -from bsk_rl.sim.dyn.base import BasicDynamicsModel, DynamicsModel +from bsk_rl.sim.dyn.base import BaseDynamicsModel, BasicDynamicsModel, DynamicsModel from bsk_rl.sim.dyn.ground_imaging import ( ContinuousImagingDynModel, GroundStationDynModel, @@ -66,6 +66,7 @@ def __init__(self, *args, **kwargs) -> None: __doc_title__ = "Dynamics Sims" __all__ = [ "DynamicsModel", + "BaseDynamicsModel", "BasicDynamicsModel", "LOSCommDynModel", "ImagingDynModel", diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index 5dd81b9f..4fda0756 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -122,33 +122,7 @@ def __del__(self): self.logger.debug("Basilisk dynamics deleted") -class BasicDynamicsModel(DynamicsModel): - """Basic Dynamics model with minimum necessary Basilisk components.""" - - @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: - return [world.BasicWorldModel] - - def __init__(self, *args, **kwargs) -> None: - """A dynamics model with a basic feature set. - - Includes the following: - - * Spacecraft hub physical properties - * Gravity - * Constant disturbance torque (defaults to none) - * Aerodynamic drag - * Eclipse checking for power generation - * Reaction wheels - * Momentum desaturation thrusters - * Solar panels, battery, and power system - - Args: - *args: Passed to superclass - **kwargs: Passed to superclass - """ - super().__init__(*args, **kwargs) - +class BaseDynamicsModel(DynamicsModel): @property def sigma_BN(self): """Body attitude MRP relative to inertial frame.""" @@ -273,38 +247,10 @@ def beta_angle(self): ) return beta - @property - def battery_charge(self): - """Battery charge [W*s].""" - return self.powerMonitor.batPowerOutMsg.read().storageLevel - - @property - def battery_charge_fraction(self): - """Battery charge as a fraction of capacity.""" - return self.battery_charge / self.powerMonitor.storageCapacity - - @property - def wheel_speeds(self): - """Wheel speeds [rad/s].""" - return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds)[0:3] - - @property - def wheel_speeds_fraction(self): - """Wheel speeds normalized by maximum allowable speed.""" - return self.wheel_speeds / (self.maxWheelSpeed * macros.rpm2radsec) - def _setup_dynamics_objects(self, **kwargs) -> None: + """Caller for all dynamics object initialization.""" self.setup_spacecraft_hub(**kwargs) - self.setup_drag_effector(**kwargs) - self.setup_reaction_wheel_dyn_effector(**kwargs) - self.setup_thruster_dyn_effector() self.setup_simple_nav_object() - self.setup_eclipse_object() - self.setup_solar_panel(**kwargs) - self.setup_battery(**kwargs) - self.setup_power_sink(**kwargs) - self.setup_reaction_wheel_power(**kwargs) - self.setup_thruster_power(**kwargs) @default_args( mass=330, @@ -390,8 +336,6 @@ def setup_spacecraft_hub( self.min_orbital_radius = min_orbital_radius self.setup_gravity_bodies() - self.setup_disturbance_torque(**kwargs) - self.setup_density_model() def setup_gravity_bodies(self) -> None: """Set up gravitational bodies from the :class:`~bsk_rl.sim.world.WorldModel` to included in the simulation.""" @@ -399,6 +343,90 @@ def setup_gravity_bodies(self) -> None: list(self.world.gravFactory.gravBodies.values()) ) + def setup_simple_nav_object(self, priority: int = 1400, **kwargs) -> None: + """Set up the navigation module. + + Args: + priority: Model priority. + kwargs: Passed to other setup functions. + """ + self.simpleNavObject = simpleNav.SimpleNav() + self.simpleNavObject.ModelTag = "SimpleNav" + self.simpleNavObject.scStateInMsg.subscribeTo(self.scObject.scStateOutMsg) + self.simulator.AddModelToTask( + self.task_name, self.simpleNavObject, ModelPriority=priority + ) + + @aliveness_checker + def altitude_valid(self) -> bool: + """Check that satellite has not deorbited. + + Checks if altitude is greater than 200km above Earth's surface. + """ + return np.linalg.norm(self.r_BN_N) > self.min_orbital_radius + + +class BasicDynamicsModel(BaseDynamicsModel): + """Basic Dynamics model with minimum necessary Basilisk components.""" + + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return [world.BasicWorldModel] + + def __init__(self, *args, **kwargs) -> None: + """A dynamics model with a basic feature set. + + Includes the following: + + * Spacecraft hub physical properties + * Gravity + * Constant disturbance torque (defaults to none) + * Aerodynamic drag + * Eclipse checking for power generation + * Reaction wheels + * Momentum desaturation thrusters + * Solar panels, battery, and power system + + Args: + *args: Passed to superclass + **kwargs: Passed to superclass + """ + super().__init__(*args, **kwargs) + + @property + def battery_charge(self): + """Battery charge [W*s].""" + return self.powerMonitor.batPowerOutMsg.read().storageLevel + + @property + def battery_charge_fraction(self): + """Battery charge as a fraction of capacity.""" + return self.battery_charge / self.powerMonitor.storageCapacity + + @property + def wheel_speeds(self): + """Wheel speeds [rad/s].""" + return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds)[0:3] + + @property + def wheel_speeds_fraction(self): + """Wheel speeds normalized by maximum allowable speed.""" + return self.wheel_speeds / (self.maxWheelSpeed * macros.rpm2radsec) + + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_disturbance_torque(**kwargs) + self.setup_density_model() + self.setup_drag_effector(**kwargs) + self.setup_reaction_wheel_dyn_effector(**kwargs) + self.setup_thruster_dyn_effector() + self.setup_eclipse_object() + self.setup_solar_panel(**kwargs) + self.setup_battery(**kwargs) + self.setup_power_sink(**kwargs) + self.setup_reaction_wheel_power(**kwargs) + self.setup_thruster_power(**kwargs) + @default_args(disturbance_vector=None) def setup_disturbance_torque( self, disturbance_vector: Optional[Iterable[float]] = None, **kwargs @@ -489,29 +517,6 @@ def setup_drag_effector( self.task_name, self.dragEffector, ModelPriority=priority ) - def setup_simple_nav_object(self, priority: int = 1400, **kwargs) -> None: - """Set up the navigation module. - - Args: - priority: Model priority. - kwargs: Passed to other setup functions. - """ - self.simpleNavObject = simpleNav.SimpleNav() - self.simpleNavObject.ModelTag = "SimpleNav" - self.simpleNavObject.scStateInMsg.subscribeTo(self.scObject.scStateOutMsg) - self.simulator.AddModelToTask( - self.task_name, self.simpleNavObject, ModelPriority=priority - ) - - @aliveness_checker - def altitude_valid(self) -> bool: - """Check that satellite has not deorbited. - - Checks if altitude is greater than the configurable minimum orbital radius - (`min_orbital_radius`). The default value corresponds to 200km above Earth's surface. - """ - return np.linalg.norm(self.r_BN_N) > self.min_orbital_radius - @default_args( wheelSpeeds=lambda: np.random.uniform(-1500, 1500, 3), maxWheelSpeed=np.inf, @@ -747,5 +752,6 @@ def setup_reaction_wheel_power( __all__ = [ "DynamicsModel", + "BaseDynamicsModel", "BasicDynamicsModel", ] diff --git a/src/bsk_rl/sim/dyn/relative_motion.py b/src/bsk_rl/sim/dyn/relative_motion.py index 482327ab..e35b8545 100644 --- a/src/bsk_rl/sim/dyn/relative_motion.py +++ b/src/bsk_rl/sim/dyn/relative_motion.py @@ -6,11 +6,11 @@ from Basilisk.simulation import spacecraftLocation from Basilisk.utilities import macros -from bsk_rl.sim.dyn import BasicDynamicsModel +from bsk_rl.sim.dyn import DynamicsModel from bsk_rl.utils.functional import aliveness_checker, default_args, valid_func_name -class LOSCommDynModel(BasicDynamicsModel): +class LOSCommDynModel(DynamicsModel): """For evaluating line-of-sight connections between satellites for communication.""" def __init__(self, *args, **kwargs) -> None: @@ -64,7 +64,7 @@ def setup_los_comms( ) -class ConjunctionDynModel(BasicDynamicsModel): +class ConjunctionDynModel(DynamicsModel): """For evaluating conjunctions between satellites.""" def __init__(self, *args, **kwargs) -> None: @@ -134,7 +134,7 @@ def side_effect(sim): ) -class MaxRangeDynModel(BasicDynamicsModel): +class MaxRangeDynModel(DynamicsModel): """For evaluating a maximum range limitation between satellites.""" def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/fsw/__init__.py b/src/bsk_rl/sim/fsw/__init__.py index e959bec2..bfaf8279 100644 --- a/src/bsk_rl/sim/fsw/__init__.py +++ b/src/bsk_rl/sim/fsw/__init__.py @@ -29,7 +29,14 @@ operational, returning true if the satellite is still alive. """ -from bsk_rl.sim.fsw.base import BasicFSWModel, FSWModel, SteeringFSWModel, Task, action +from bsk_rl.sim.fsw.base import ( + BaseFSWModel, + BasicFSWModel, + FSWModel, + SteeringFSWModel, + Task, + action, +) from bsk_rl.sim.fsw.ground_imaging import ( ContinuousImagingFSWModel, ImagingFSWModel, @@ -49,4 +56,5 @@ "SteeringImagerFSWModel", "MagicOrbitalManeuverFSWModel", "RSOInspectorFSWModel", + "BaseFSWModel", ] diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 42ef9fea..278bbc77 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -63,6 +63,64 @@ def inner(self, *args, **kwargs) -> Callable[..., None]: return inner +class Task(ABC): + """Abstract class for defining FSW tasks.""" + + name: str = AbstractClassProperty() + + def __init__(self, fsw: "FSWModel", priority: int) -> None: + """Template class for defining FSW processes. + + Each FSW process has a task associated with it, which handle certain housekeeping + functions. + + Args: + fsw: FSW model task contributes to + priority: Task priority + """ + self.fsw: "FSWModel" = proxy(fsw) + self.priority = priority + + def create_task(self) -> None: + """Add task to FSW with a unique name.""" + self.fsw.fsw_proc.addTask( + self.fsw.simulator.CreateNewTask( + self.name + self.fsw.satellite.name, mc.sec2nano(self.fsw.fsw_rate) + ), + taskPriority=self.priority, + ) + + @abstractmethod # pragma: no cover + def _create_module_data(self) -> None: + """Create module data wrappers.""" + pass + + @abstractmethod # pragma: no cover + def _setup_fsw_objects(self, **kwargs) -> None: + """Initialize model parameters with satellite arguments.""" + pass + + def _add_model_to_task(self, module, priority) -> None: + """Add a model to this task. + + Args: + module: Basilisk module + priority: Model priority + """ + self.fsw.simulator.AddModelToTask( + self.name + self.fsw.satellite.name, + module, + ModelPriority=priority, + ) + + def reset_for_action(self) -> None: + """Housekeeping for task when a new action is called. + + Disables task by default, can be overridden by subclasses. + """ + self.fsw.simulator.disableTask(self.name + self.fsw.satellite.name) + + class FSWModel(ABC): """Abstract Basilisk flight software model.""" @@ -132,7 +190,6 @@ def dynamics(self) -> "DynamicsModel": def _make_task_list(self) -> list["Task"]: return [] - @abstractmethod # pragma: no cover def _set_messages(self) -> None: """Message setup after task creation.""" pass @@ -150,65 +207,45 @@ def __del__(self): self.logger.debug("Basilisk FSW deleted") -class Task(ABC): - """Abstract class for defining FSW tasks.""" - - name: str = AbstractClassProperty() - - def __init__(self, fsw: FSWModel, priority: int) -> None: - """Template class for defining FSW processes. - - Each FSW process has a task associated with it, which handle certain housekeeping - functions. +class BaseFSWModel(FSWModel): + @classmethod + def _requires_dyn(cls) -> list[type["DynamicsModel"]]: + return [dyn.BaseDynamicsModel] - Args: - fsw: FSW model task contributes to - priority: Task priority - """ - self.fsw: FSWModel = proxy(fsw) - self.priority = priority + def _set_messages(self) -> None: + self._set_config_msgs() + self._set_gateway_msgs() - def create_task(self) -> None: - """Add task to FSW with a unique name.""" - self.fsw.fsw_proc.addTask( - self.fsw.simulator.CreateNewTask( - self.name + self.fsw.satellite.name, mc.sec2nano(self.fsw.fsw_rate) - ), - taskPriority=self.priority, - ) + def _set_config_msgs(self) -> None: + self._set_vehicle_config_msg() - @abstractmethod # pragma: no cover - def _create_module_data(self) -> None: - """Create module data wrappers.""" - pass + def _set_vehicle_config_msg(self) -> None: + """Set the vehicle configuration message.""" + # Use the same inertia in the FSW algorithm as in the simulation + vehicleConfigOut = messaging.VehicleConfigMsgPayload() + vehicleConfigOut.ISCPntB_B = self.dynamics.I_mat + self.vcConfigMsg = messaging.VehicleConfigMsg().write(vehicleConfigOut) - @abstractmethod # pragma: no cover - def _setup_fsw_objects(self, **kwargs) -> None: - """Initialize model parameters with satellite arguments.""" - pass + 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() - def _add_model_to_task(self, module, priority) -> None: - """Add a model to this task. + def _zero_gateway_msgs(self) -> None: + """Zero all the FSW gateway message payloads.""" + self.attRefMsg.write(messaging.AttRefMsgPayload()) + self.attGuidMsg.write(messaging.AttGuidMsgPayload()) - Args: - module: Basilisk module - priority: Model priority - """ - self.fsw.simulator.AddModelToTask( - self.name + self.fsw.satellite.name, - module, - ModelPriority=priority, - ) + def _make_task_list(self): + return [] - def reset_for_action(self) -> None: - """Housekeeping for task when a new action is called. - Disables task by default, can be overridden by subclasses. - """ - self.fsw.simulator.disableTask(self.name + self.fsw.satellite.name) +class MagicPointingFSWModel(BaseFSWModel): + pass # TODO -class BasicFSWModel(FSWModel): +class BasicFSWModel(BaseFSWModel): """Basic FSW model with minimum necessary Basilisk components.""" @classmethod @@ -224,22 +261,11 @@ def _make_task_list(self) -> list[Task]: self.MRPControlTask(self), ] - def _set_messages(self) -> None: - self._set_config_msgs() - self._set_gateway_msgs() - def _set_config_msgs(self) -> None: - self._set_vehicle_config_msg() + super()._set_config_msgs() self._set_thrusters_config_msg() self._set_rw_config_msg() - def _set_vehicle_config_msg(self) -> None: - """Set the vehicle configuration message.""" - # Use the same inertia in the FSW algorithm as in the simulation - vehicleConfigOut = messaging.VehicleConfigMsgPayload() - vehicleConfigOut.ISCPntB_B = self.dynamics.I_mat - self.vcConfigMsg = messaging.VehicleConfigMsg().write(vehicleConfigOut) - def _set_thrusters_config_msg(self) -> None: """Import the thrusters configuration information.""" self.thrusterConfigMsg = self.dynamics.thrFactory.getConfigMessage() @@ -250,10 +276,7 @@ def _set_rw_config_msg(self) -> None: 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() + super()._set_gateway_msgs() # connect gateway FSW effector command msgs with the dynamics self.dynamics.rwStateEffector.rwMotorCmdInMsg.subscribeTo( @@ -263,11 +286,6 @@ def _set_gateway_msgs(self) -> None: self.thrDump.thrusterOnTimeOutMsg ) - def _zero_gateway_msgs(self) -> None: - """Zero all the FSW gateway message payloads.""" - self.attRefMsg.write(messaging.AttRefMsgPayload()) - self.attGuidMsg.write(messaging.AttGuidMsgPayload()) - @action def action_drift(self) -> None: """Disable all tasks and do nothing.""" diff --git a/src/bsk_rl/sim/fsw/orbital.py b/src/bsk_rl/sim/fsw/orbital.py index ee389ed6..c53c8391 100644 --- a/src/bsk_rl/sim/fsw/orbital.py +++ b/src/bsk_rl/sim/fsw/orbital.py @@ -2,11 +2,11 @@ import numpy as np -from bsk_rl.sim.fsw import BasicFSWModel, action +from bsk_rl.sim.fsw import FSWModel, action from bsk_rl.utils.functional import aliveness_checker, default_args -class MagicOrbitalManeuverFSWModel(BasicFSWModel): +class MagicOrbitalManeuverFSWModel(FSWModel): """Model that allows for instantaneous Delta V maneuvers.""" def __init__(self, *args, **kwargs) -> None: From e882f1c894e724cd2576e2b0b07fa1a32106c0fc Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Fri, 3 Oct 2025 11:08:30 -0600 Subject: [PATCH 2/8] [#156] Add resets to sim models --- src/bsk_rl/gym.py | 7 ++++++- src/bsk_rl/sim/dyn/base.py | 3 ++- src/bsk_rl/sim/fsw/base.py | 3 ++- src/bsk_rl/sim/world.py | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index 358a735b..d8103982 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -362,20 +362,25 @@ def reset( ) self.simulator.setup_vizard(**self.vizard_settings) + self.simulator.world.reset_during_sim_init() self.scenario.reset_during_sim_init() self.rewarder.reset_during_sim_init() self.communicator.reset_during_sim_init() for satellite in self.satellites: satellite.reset_during_sim_init() + satellite.dynamics.reset_during_sim_init() + satellite.fsw.reset_during_sim_init() self.simulator.finish_init() + self.simulator.world.reset_post_sim_init() self.scenario.reset_post_sim_init() self.rewarder.reset_post_sim_init() self.communicator.reset_post_sim_init() - for satellite in self.satellites: satellite.reset_post_sim_init() + satellite.dynamics.reset_post_sim_init() + satellite.fsw.reset_post_sim_init() satellite.data_store.update_from_logs() observation = self._get_obs() diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index 4fda0756..49eb29f4 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -25,6 +25,7 @@ from bsk_rl.utils import actuator_primitives as aP from bsk_rl.utils.attitude import random_tumble from bsk_rl.utils.functional import ( + Resetable, aliveness_checker, check_aliveness_checkers, default_args, @@ -37,7 +38,7 @@ from bsk_rl.sim.world import WorldModel -class DynamicsModel(ABC): +class DynamicsModel(ABC, Resetable): """Abstract Basilisk dynamics model.""" @classmethod diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 278bbc77..36f96f24 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -24,6 +24,7 @@ from bsk_rl.sim import dyn from bsk_rl.utils.functional import ( AbstractClassProperty, + Resetable, check_aliveness_checkers, default_args, ) @@ -121,7 +122,7 @@ def reset_for_action(self) -> None: self.fsw.simulator.disableTask(self.name + self.fsw.satellite.name) -class FSWModel(ABC): +class FSWModel(ABC, Resetable): """Abstract Basilisk flight software model.""" @classmethod diff --git a/src/bsk_rl/sim/world.py b/src/bsk_rl/sim/world.py index 957d50e4..0daa059a 100644 --- a/src/bsk_rl/sim/world.py +++ b/src/bsk_rl/sim/world.py @@ -38,7 +38,7 @@ from Basilisk.utilities import macros as mc from Basilisk.utilities import orbitalMotion, simIncludeGravBody -from bsk_rl.utils.functional import collect_default_args, default_args +from bsk_rl.utils.functional import Resetable, collect_default_args, default_args from bsk_rl.utils.orbital import random_epoch if TYPE_CHECKING: # pragma: no cover @@ -49,7 +49,7 @@ bsk_path = __path__[0] -class WorldModel(ABC): +class WorldModel(ABC, Resetable): """Abstract Basilisk world model.""" @classmethod From 7d0742b7ad63a804ba23c9439af623c9b67afc17 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 4 Nov 2025 12:08:10 -0700 Subject: [PATCH 3/8] [#156] Allow for fsw and dyn types to be specified as tuples --- examples/cloud_environment.ipynb | 10 +- .../cloud_environment_with_reimaging.ipynb | 4 +- examples/curriculum_learning.ipynb | 27 +++-- examples/fault_environment.ipynb | 4 +- examples/rso_inspection.ipynb | 27 ++--- examples/training_with_shield.ipynb | 15 +-- src/bsk_rl/gym.py | 18 +-- src/bsk_rl/sats/__init__.py | 3 +- src/bsk_rl/sats/satellite.py | 67 +++++++++-- src/bsk_rl/sim/dyn/base.py | 16 ++- src/bsk_rl/sim/dyn/relative_motion.py | 4 +- src/bsk_rl/sim/fsw/__init__.py | 2 +- src/bsk_rl/sim/fsw/base.py | 20 ++-- src/bsk_rl/sim/fsw/orbital.py | 5 + src/bsk_rl/sim/world.py | 105 ++++++++++++------ src/bsk_rl/utils/functional.py | 52 ++++++++- .../comm/test_int_communication.py | 6 +- tests/integration/sim/test_int_dynamics.py | 8 +- tests/integration/test_int_rso_env.py | 14 +-- tests/unittest/sats/test_satellite.py | 41 +++++-- tests/unittest/sim/test_dynamics.py | 8 +- tests/unittest/sim/test_world.py | 58 ++++++---- tests/unittest/test_gym_env.py | 34 +----- 23 files changed, 332 insertions(+), 216 deletions(-) diff --git a/examples/cloud_environment.ipynb b/examples/cloud_environment.ipynb index a1595dc6..e8f49bc4 100644 --- a/examples/cloud_environment.ipynb +++ b/examples/cloud_environment.ipynb @@ -23,7 +23,7 @@ "from Basilisk.architecture import bskLogging\n", "from Basilisk.utilities import orbitalMotion\n", "from bsk_rl import act, obs, sats\n", - "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim import dyn, fsw\n", "from bsk_rl.utils.orbital import random_orbit\n", "from bsk_rl.scene.targets import UniformTargets\n", "from bsk_rl.data.unique_image_data import (\n", @@ -92,7 +92,6 @@ " ]\n", "\n", " class CustomDynModel(dyn.FullFeaturedDynModel):\n", - "\n", " @property\n", " def solar_angle_norm(self) -> float:\n", " sun_vec_N = (\n", @@ -419,8 +418,6 @@ " \"GeneralSatelliteTasking-v1\",\n", " satellites=satellites,\n", " terminate_on_time_limit=True,\n", - " world_type=world.GroundStationWorldModel,\n", - " world_args=world.GroundStationWorldModel.default_world_args(),\n", " scenario=scenario,\n", " rewarder=rewarder,\n", " sim_rate=0.5,\n", @@ -485,7 +482,6 @@ "source": [ "count = 0\n", "while True:\n", - "\n", " if count == 0:\n", " # Vector with an action for each satellite (we can pass different actions for each satellite)\n", " # Tasking all satellites to charge (tasking None as the first action will raise a warning)\n", @@ -532,7 +528,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv_update_cloud_env_JAIS", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -546,7 +542,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/examples/cloud_environment_with_reimaging.ipynb b/examples/cloud_environment_with_reimaging.ipynb index ba8b2c20..be523e0b 100644 --- a/examples/cloud_environment_with_reimaging.ipynb +++ b/examples/cloud_environment_with_reimaging.ipynb @@ -23,7 +23,7 @@ "from Basilisk.architecture import bskLogging\n", "from Basilisk.utilities import orbitalMotion\n", "from bsk_rl import act, obs, sats\n", - "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim import dyn, fsw\n", "from bsk_rl.scene.targets import UniformTargets\n", "from bsk_rl.data.base import Data, DataStore, GlobalReward\n", "from bsk_rl.data.unique_image_data import (\n", @@ -844,8 +844,6 @@ "\n", "env = ConstellationTasking(\n", " satellites=satellites,\n", - " world_type=world.GroundStationWorldModel,\n", - " world_args=world.GroundStationWorldModel.default_world_args(),\n", " scenario=scenario,\n", " rewarder=rewarder,\n", " sat_arg_randomizer=sat_arg_randomizer,\n", diff --git a/examples/curriculum_learning.ipynb b/examples/curriculum_learning.ipynb index 0a4262c6..a439ff10 100644 --- a/examples/curriculum_learning.ipynb +++ b/examples/curriculum_learning.ipynb @@ -24,7 +24,7 @@ "source": [ "import numpy as np\n", "from bsk_rl import act, data, obs, scene, sats\n", - "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim import dyn, fsw\n", "from bsk_rl.gym import SatelliteTasking\n", "from typing import Any, Callable, Optional, TypeVar\n", "from bsk_rl.utils.rllib.callbacks import WrappedEpisodeDataCallbacks, EpisodeDataWrapper\n", @@ -61,7 +61,6 @@ "outputs": [], "source": [ "class SatelliteTaskingCL(SatelliteTasking):\n", - "\n", " def __init__(\n", " self,\n", " satellite: Satellite,\n", @@ -70,7 +69,6 @@ " CL_params={},\n", " **kwargs,\n", " ):\n", - "\n", " super().__init__(\n", " satellite,\n", " *args,\n", @@ -85,7 +83,6 @@ " seed: Optional[int] = None,\n", " options=None,\n", " ) -> tuple[MultiSatObs, dict[str, Any]]:\n", - "\n", " self.update_sat_params() # Update satellite parameters based on difficulty before resetting\n", " obs, info = super().reset(seed=seed, options=options)\n", " return obs, info\n", @@ -361,7 +358,6 @@ "outputs": [], "source": [ "class CLCallbacks(WrappedEpisodeDataCallbacks):\n", - "\n", " def on_episode_start(\n", " self,\n", " *,\n", @@ -376,7 +372,6 @@ " env_index,\n", " **kwargs,\n", " ) -> None:\n", - "\n", " try:\n", " n_steps = metrics_logger.peek(\"num_env_steps_sampled_lifetime\")\n", " if n_steps is None:\n", @@ -627,7 +622,11 @@ " init_val = CL_options[CL_case][key][\"init_val\"]\n", " final_val = CL_options[CL_case][key][\"final_val\"]\n", " CL_params[CL_options[CL_case][key][\"var_name\"]] = (\n", - " lambda difficulty, capacity=capacity, init_val=init_val, final_val=final_val, time_seed=current_time: external_disturbance_fn(\n", + " lambda difficulty,\n", + " capacity=capacity,\n", + " init_val=init_val,\n", + " final_val=final_val,\n", + " time_seed=current_time: external_disturbance_fn(\n", " time_seed,\n", " capacity * init_val,\n", " capacity * final_val,\n", @@ -644,7 +643,11 @@ " else:\n", " temp_name = CL_options[CL_case][key][\"name\"]\n", " CL_params[temp_name] = (\n", - " lambda difficulty, capacity=capacity, init_val=init_val, final_val=final_val, time_seed=current_time: capacity_fn(\n", + " lambda difficulty,\n", + " capacity=capacity,\n", + " init_val=init_val,\n", + " final_val=final_val,\n", + " time_seed=current_time: capacity_fn(\n", " time_seed,\n", " capacity * init_val,\n", " capacity * final_val,\n", @@ -658,7 +661,12 @@ " init_val = CL_options[CL_case][key][\"init_val\"]\n", " final_val = CL_options[CL_case][key][\"final_val\"]\n", " CL_params[CL_options[CL_case][key][\"name_init\"]] = (\n", - " lambda difficulty, capacity=capacity, init_val=init_val, final_val=final_val, init_range=init_range, time_seed=current_time: capacity_init_fn(\n", + " lambda difficulty,\n", + " capacity=capacity,\n", + " init_val=init_val,\n", + " final_val=final_val,\n", + " init_range=init_range,\n", + " time_seed=current_time: capacity_init_fn(\n", " time_seed,\n", " capacity * init_val,\n", " capacity * final_val,\n", @@ -692,7 +700,6 @@ " satellite=sat,\n", " scenario=scene.UniformNadirScanning(value_per_second=1 / duration),\n", " rewarder=data.ScanningTimeReward(),\n", - " world_type=world.GroundStationWorldModel,\n", " time_limit=duration,\n", " failure_penalty=-1.0,\n", " difficulty=0.0,\n", diff --git a/examples/fault_environment.ipynb b/examples/fault_environment.ipynb index 15923e9f..3db80d6c 100644 --- a/examples/fault_environment.ipynb +++ b/examples/fault_environment.ipynb @@ -25,7 +25,7 @@ "from Basilisk.fswAlgorithms import rwNullSpace\n", "from Basilisk.architecture import messaging\n", "from bsk_rl import SatelliteTasking, act, data, obs, scene, sats\n", - "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim import dyn, fsw\n", "from bsk_rl.utils.orbital import random_orbit, random_unit_vector\n", "from bsk_rl.utils.functional import default_args\n", "\n", @@ -477,8 +477,6 @@ "env = SatelliteTasking(\n", " satellite=satellites,\n", " terminate_on_time_limit=True,\n", - " world_type=world.GroundStationWorldModel,\n", - " world_args=world.GroundStationWorldModel.default_world_args(),\n", " scenario=scene.UniformTargets(n_targets=1000),\n", " rewarder=data.UniqueImageReward(),\n", " sim_rate=0.5,\n", diff --git a/examples/rso_inspection.ipynb b/examples/rso_inspection.ipynb index 8b5a1bca..1effc47d 100644 --- a/examples/rso_inspection.ipynb +++ b/examples/rso_inspection.ipynb @@ -24,7 +24,6 @@ "from bsk_rl.obs.relative_observations import rso_imaged_regions\n", "from bsk_rl.utils.orbital import fibonacci_sphere\n", "from bsk_rl.sim import dyn, fsw\n", - "import types\n", "import numpy as np\n", "from Basilisk.architecture import bskLogging\n", "from functools import partial\n", @@ -77,9 +76,7 @@ " obs.SatProperties(dict(prop=\"one\", fn=lambda _: 1.0)),\n", " ]\n", " action_spec = [act.Downlink(duration=1e9)]\n", - " dyn_type = types.new_class(\n", - " \"Dyn\", (dyn.ImagingDynModel, dyn.ConjunctionDynModel, dyn.RSODynModel)\n", - " )\n", + " dyn_type = (dyn.ImagingDynModel, dyn.ConjunctionDynModel, dyn.RSODynModel)\n", " fsw_type = fsw.ContinuousImagingFSWModel\n" ] }, @@ -199,21 +196,11 @@ " fsw_action=\"action_inspect_rso\",\n", " )\n", " ]\n", - " dyn_type = types.new_class(\n", - " \"Dyn\",\n", - " (\n", - " dyn.MaxRangeDynModel,\n", - " dyn.ConjunctionDynModel,\n", - " dyn.RSOInspectorDynModel,\n", - " ),\n", - " )\n", - " fsw_type = types.new_class(\n", - " \"FSW\",\n", - " (\n", - " fsw.SteeringFSWModel,\n", - " fsw.MagicOrbitalManeuverFSWModel,\n", - " fsw.RSOInspectorFSWModel,\n", - " ),\n", + " dyn_type = (dyn.MaxRangeDynModel, dyn.ConjunctionDynModel, dyn.RSOInspectorDynModel)\n", + " fsw_type = (\n", + " fsw.SteeringFSWModel,\n", + " fsw.MagicOrbitalManeuverFSWModel,\n", + " fsw.RSOInspectorFSWModel,\n", " )\n" ] }, @@ -452,7 +439,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/examples/training_with_shield.ipynb b/examples/training_with_shield.ipynb index d17b4655..5b0c06bf 100644 --- a/examples/training_with_shield.ipynb +++ b/examples/training_with_shield.ipynb @@ -29,7 +29,7 @@ "\n", "from Basilisk.architecture import bskLogging\n", "from bsk_rl import act, data, obs, scene, sats\n", - "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim import dyn, fsw\n", "from bsk_rl.utils.orbital import random_orbit\n", "from Basilisk.utilities import orbitalMotion\n", "from typing import Callable, Union, Dict, Any, Iterable, TypeVar\n", @@ -325,7 +325,6 @@ " env = gym.make(\n", " \"SatelliteTasking-v1\",\n", " satellite=satellite,\n", - " world_type=world.GroundStationWorldModel,\n", " scenario=scene_features,\n", " rewarder=data.UniqueImageReward(),\n", " time_limit=np.floor(T_ORBIT * horizon),\n", @@ -340,7 +339,6 @@ " env = None\n", " env_args_dict = dict(\n", " satellite=satellite,\n", - " world_type=world.GroundStationWorldModel,\n", " scenario=scene_features,\n", " rewarder=data.UniqueImageReward(),\n", " time_limit=np.floor(T_ORBIT * horizon),\n", @@ -353,7 +351,6 @@ " env = gym.make(\n", " \"SatelliteTasking-v1\",\n", " satellite=satellite,\n", - " world_type=world.GroundStationWorldModel,\n", " scenario=scene_features,\n", " rewarder=data.UniqueImageReward(),\n", " time_limit=np.floor(0.1 * T_ORBIT),\n", @@ -422,12 +419,10 @@ "outputs": [], "source": [ "class WrapperActionLogging(Wrapper):\n", - "\n", " def __init__(\n", " self,\n", " env: Satellite,\n", " ):\n", - "\n", " super().__init__(env)\n", " self._initialize_action_logger()\n", "\n", @@ -453,7 +448,6 @@ " return self.env.reset(seed=seed, options=options)\n", "\n", " def step(self, action: int):\n", - "\n", " if action == 0:\n", " self.action_logger[\"action_charge_count\"] += 1\n", " elif action == 1:\n", @@ -505,7 +499,6 @@ " return modified_action\n", "\n", " def reward(self, reward: float) -> float:\n", - "\n", " if self.shield_info[\"shield_interference\"]:\n", " reward += self.shield_penalty\n", " self.shield_info[\"shield_penalty_total\"] += self.shield_penalty\n", @@ -682,7 +675,6 @@ " )\n", "\n", " if shield_type == \"unshielded\":\n", - "\n", " if shield_mode == \"postposed\":\n", "\n", " def shield_function(obs: list[float], act: int) -> int:\n", @@ -698,7 +690,6 @@ " return mask_function\n", "\n", " elif shield_type == \"handmade\":\n", - "\n", " if shield_mode == \"postposed\":\n", "\n", " def shield_function(obs: list[float], act: int) -> Union[int, None]:\n", @@ -757,7 +748,6 @@ "\n", "\n", "class ActionMaskingTorchRLModule(BaseActionMaskingTorchRLModule):\n", - "\n", " @override(ValueFunctionAPI)\n", " def compute_values(self, batch: Dict[str, TensorType]):\n", " # Preprocess the batch to extract the `observations` to `Columns.OBS`.\n", @@ -1052,7 +1042,6 @@ "\n", "\n", "class HiddenTargetsMask:\n", - "\n", " def __init__(self, list_targets: list, targets_max: int):\n", " \"\"\"Initialize the mask for hidden targets\n", "\n", @@ -1070,7 +1059,6 @@ " self.hidden_set = None\n", "\n", " def compute_mask(self, n_imaged: int):\n", - "\n", " if self.hidden_set is None or self.n_imaged != n_imaged:\n", " self.n_imaged = n_imaged\n", " self.mask = self.replace_targets(\n", @@ -1291,7 +1279,6 @@ ")\n", "\n", "while True:\n", - "\n", " sat = env.satellite\n", "\n", " hidden_targets = hidden_targets_mask.compute_mask(sat.imaged)\n", diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index d8103982..f0d0eb57 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -17,8 +17,8 @@ from bsk_rl.sats import Satellite from bsk_rl.scene import Scenario from bsk_rl.sim import Simulator -from bsk_rl.sim.world import WorldModel -from bsk_rl.utils import logging_config, vizard +from bsk_rl.sim.world import BaseWorldModel, WorldModel +from bsk_rl.utils import functional, logging_config, vizard logger = logging.getLogger(__name__) @@ -215,23 +215,13 @@ def __init__( def _minimum_world_model(self) -> type[WorldModel]: """Determine the minimum world model required by the satellites.""" - types = set( + world_types = set( sum( [satellite.dyn_type._requires_world() for satellite in self.satellites], [], ) ) - if len(types) == 1: - return list(types)[0] - for test_type in types: - if all([issubclass(test_type, other_type) for other_type in types]): - return test_type - - # Else compose all types into a new class - class MinimumEnv(*types): - pass - - return MinimumEnv + return functional.compose_types(*world_types, BaseWorldModel, name="World") def get_satellite(self, name: str) -> "Satellite": """Get a satellite by name. diff --git a/src/bsk_rl/sats/__init__.py b/src/bsk_rl/sats/__init__.py index b6082468..4f3bd902 100644 --- a/src/bsk_rl/sats/__init__.py +++ b/src/bsk_rl/sats/__init__.py @@ -15,7 +15,8 @@ underlying dynamics and FSW models used by the Basilisk simulation. Some actions, communication methods, or other environment configurations may necessitate the use of a specific dynamics or FSW model. See :ref:`bsk_rl.sim.fsw` and :ref:`bsk_rl.sim.dyn` for -more information on selecting these models. +more information on selecting these models. The models can be specified as a single type +or as a tuple of types to use multiple inheritance. In practice, configuring a satellite and passing it to an environment is straightforward: diff --git a/src/bsk_rl/sats/satellite.py b/src/bsk_rl/sats/satellite.py index 376ad112..23eb14c3 100644 --- a/src/bsk_rl/sats/satellite.py +++ b/src/bsk_rl/sats/satellite.py @@ -19,6 +19,7 @@ AbstractClassProperty, Resetable, collect_default_args, + compose_types, safe_dict_merge, valid_func_name, ) @@ -38,11 +39,55 @@ class Satellite(ABC, Resetable): """Abstract base class for satellites.""" - dyn_type: type["dyn.DynamicsModel"] = AbstractClassProperty() - fsw_type: type["fsw.FSWModel"] = AbstractClassProperty() + dyn_type: Union[ + type["dyn.DynamicsModel"], tuple[type["dyn.DynamicsModel"], ...] + ] = AbstractClassProperty() + fsw_type: Union[type["fsw.FSWModel"], tuple[type["fsw.FSWModel"], ...]] = ( + AbstractClassProperty() + ) observation_spec: list["Observation"] = AbstractClassProperty() action_spec: list["Action"] = AbstractClassProperty() + _dyn_type = None + + @classmethod + def get_dyn_type(cls) -> type["dyn.DynamicsModel"]: + """Get the dynamics model type for the satellite. + + This should be used in class methods instead of referencing ``dyn_type``. In + instantiated satellites, ``self.dyn_type`` refers to the output of this function. + + Returns: + Dynamics model type + """ + if cls._dyn_type is None: + if isinstance(cls.dyn_type, (list, tuple)): + dyn_types = cls.dyn_type + else: + dyn_types = (cls.dyn_type,) + cls._dyn_type = compose_types(*dyn_types, dyn.BaseDynamicsModel, name="Dyn") + return cls._dyn_type + + _fsw_type = None + + @classmethod + def get_fsw_type(cls) -> type["fsw.FSWModel"]: + """Get the flight software model type for the satellite. + + This should be used in class methods instead of referencing ``fsw_type``. In + instantiated satellites, ``self.fsw_type`` refers to the output of this function. + + Returns: + Flight software model type + """ + if cls._fsw_type is None: + if isinstance(cls.fsw_type, (list, tuple)): + fsw_types = cls.fsw_type + else: + fsw_types = (cls.fsw_type,) + cls._fsw_type = compose_types(*fsw_types, fsw.BaseFSWModel, name="FSW") + return cls._fsw_type + @classmethod def default_sat_args(cls, **kwargs) -> dict[str, Any]: """Compile default arguments for :class:`~bsk_rl.sim.dyn.DynamicsModel` and :class:`~bsk_rl.sim.fsw.FSWModel`, replacing those specified. @@ -53,14 +98,17 @@ def default_sat_args(cls, **kwargs) -> dict[str, Any]: Returns: Dictionary of arguments for simulation models. """ - defaults = collect_default_args(cls.dyn_type) - defaults = safe_dict_merge(defaults, collect_default_args(cls.fsw_type)) - for name in dir(cls.fsw_type): - if inspect.isclass(getattr(cls.fsw_type, name)) and issubclass( - getattr(cls.fsw_type, name), fsw.Task + defaults = collect_default_args(cls.get_dyn_type()) + fsw_type = cls.get_fsw_type() + defaults = safe_dict_merge(defaults, collect_default_args(fsw_type)) + for name in dir(fsw_type): + if ( + not name.startswith("__") + and inspect.isclass(getattr(fsw_type, name)) + and issubclass(getattr(fsw_type, name), fsw.Task) ): defaults = safe_dict_merge( - defaults, collect_default_args(getattr(cls.fsw_type, name)) + defaults, collect_default_args(getattr(fsw_type, name)) ) for k, v in kwargs.items(): @@ -110,6 +158,9 @@ def __init__( ) self.action_builder = select_action_builder(self) + self.fsw_type = self.get_fsw_type() + self.dyn_type = self.get_dyn_type() + @property @deprecated(reason="Use satellite.name instead") def id(self) -> str: diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index 49eb29f4..9c7aa2a8 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -124,6 +124,10 @@ def __del__(self): class BaseDynamicsModel(DynamicsModel): + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return [] + @property def sigma_BN(self): """Body attitude MRP relative to inertial frame.""" @@ -250,6 +254,7 @@ def beta_angle(self): def _setup_dynamics_objects(self, **kwargs) -> None: """Caller for all dynamics object initialization.""" + super()._setup_dynamics_objects(**kwargs) self.setup_spacecraft_hub(**kwargs) self.setup_simple_nav_object() @@ -370,10 +375,6 @@ def altitude_valid(self) -> bool: class BasicDynamicsModel(BaseDynamicsModel): """Basic Dynamics model with minimum necessary Basilisk components.""" - @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: - return [world.BasicWorldModel] - def __init__(self, *args, **kwargs) -> None: """A dynamics model with a basic feature set. @@ -394,6 +395,13 @@ def __init__(self, *args, **kwargs) -> None: """ super().__init__(*args, **kwargs) + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return [ + world.EclipseWorldModel, + world.AtmosphereWorldModel, + ] + super()._requires_world() + @property def battery_charge(self): """Battery charge [W*s].""" diff --git a/src/bsk_rl/sim/dyn/relative_motion.py b/src/bsk_rl/sim/dyn/relative_motion.py index e35b8545..1342b6f2 100644 --- a/src/bsk_rl/sim/dyn/relative_motion.py +++ b/src/bsk_rl/sim/dyn/relative_motion.py @@ -6,11 +6,11 @@ from Basilisk.simulation import spacecraftLocation from Basilisk.utilities import macros -from bsk_rl.sim.dyn import DynamicsModel +from bsk_rl.sim.dyn import BaseDynamicsModel, DynamicsModel from bsk_rl.utils.functional import aliveness_checker, default_args, valid_func_name -class LOSCommDynModel(DynamicsModel): +class LOSCommDynModel(BaseDynamicsModel): """For evaluating line-of-sight connections between satellites for communication.""" def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/fsw/__init__.py b/src/bsk_rl/sim/fsw/__init__.py index bfaf8279..764cb670 100644 --- a/src/bsk_rl/sim/fsw/__init__.py +++ b/src/bsk_rl/sim/fsw/__init__.py @@ -49,6 +49,7 @@ __all__ = [ "action", "FSWModel", + "BaseFSWModel", "BasicFSWModel", "ImagingFSWModel", "ContinuousImagingFSWModel", @@ -56,5 +57,4 @@ "SteeringImagerFSWModel", "MagicOrbitalManeuverFSWModel", "RSOInspectorFSWModel", - "BaseFSWModel", ] diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 36f96f24..802d2220 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -195,6 +195,10 @@ def _set_messages(self) -> None: """Message setup after task creation.""" pass + def _zero_gateway_msgs(self) -> None: + """Zero all the FSW gateway message payloads.""" + pass + def is_alive(self, log_failure=False) -> bool: """Check if the FSW model has failed any aliveness requirements. @@ -211,7 +215,7 @@ def __del__(self): class BaseFSWModel(FSWModel): @classmethod def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - return [dyn.BaseDynamicsModel] + return super()._requires_dyn() + [dyn.BaseDynamicsModel] def _set_messages(self) -> None: self._set_config_msgs() @@ -241,6 +245,13 @@ def _zero_gateway_msgs(self) -> None: def _make_task_list(self): return [] + @action + def action_drift(self) -> None: + """Disable all tasks and do nothing.""" + self.simulator.disableTask( + BasicFSWModel.MRPControlTask.name + self.satellite.name + ) + class MagicPointingFSWModel(BaseFSWModel): pass # TODO @@ -287,13 +298,6 @@ def _set_gateway_msgs(self) -> None: self.thrDump.thrusterOnTimeOutMsg ) - @action - def action_drift(self) -> None: - """Disable all tasks and do nothing.""" - self.simulator.disableTask( - BasicFSWModel.MRPControlTask.name + self.satellite.name - ) - class SunPointTask(Task): """Task to generate sun-pointing reference.""" diff --git a/src/bsk_rl/sim/fsw/orbital.py b/src/bsk_rl/sim/fsw/orbital.py index c53c8391..d46cb191 100644 --- a/src/bsk_rl/sim/fsw/orbital.py +++ b/src/bsk_rl/sim/fsw/orbital.py @@ -2,6 +2,7 @@ import numpy as np +from bsk_rl.sim import dyn from bsk_rl.sim.fsw import FSWModel, action from bsk_rl.utils.functional import aliveness_checker, default_args @@ -9,6 +10,10 @@ class MagicOrbitalManeuverFSWModel(FSWModel): """Model that allows for instantaneous Delta V maneuvers.""" + @classmethod + def _requires_dyn(cls) -> list[type["DynamicsModel"]]: + return super()._requires_dyn() + [dyn.BaseDynamicsModel] + def __init__(self, *args, **kwargs) -> None: """Model that allows for instantaneous Delta V maneuvers.""" super().__init__(*args, **kwargs) diff --git a/src/bsk_rl/sim/world.py b/src/bsk_rl/sim/world.py index 0daa059a..d6c60f38 100644 --- a/src/bsk_rl/sim/world.py +++ b/src/bsk_rl/sim/world.py @@ -110,7 +110,7 @@ def _setup_world_objects(self, **kwargs) -> None: pass -class BasicWorldModel(WorldModel): +class BaseWorldModel(WorldModel): """Basic world with minimum necessary Basilisk world components.""" def __init__(self, *args, **kwargs) -> None: @@ -148,8 +148,6 @@ def omega_PN_N(self): def _setup_world_objects(self, **kwargs) -> None: self.setup_gravity_bodies(**kwargs) self.setup_ephem_object(**kwargs) - self.setup_atmosphere_density_model(**kwargs) - self.setup_eclipse_object(**kwargs) @default_args(utc_init=random_epoch) def setup_gravity_bodies( @@ -205,6 +203,68 @@ def setup_ephem_object(self, priority: int = 988, **kwargs) -> None: self.world_task_name, self.ephemConverter, ModelPriority=priority ) + def __del__(self) -> None: + """Log when world is deleted and unload SPICE.""" + super().__del__() + try: + self.gravFactory.unloadSpiceKernels() + except AttributeError: + pass + + +class EclipseWorldModel(BaseWorldModel): + def __init__(self, *args, **kwargs) -> None: + """Model that includes eclipse messages. + + This model adds eclipses for power generation and other purposes. + + Args: + *args: Passed to superclass. + **kwargs: Passed to superclass. + + """ + super().__init__(*args, **kwargs) + + def _setup_world_objects(self, **kwargs) -> None: + super()._setup_world_objects(**kwargs) + self.setup_eclipse_object(**kwargs) + + def setup_eclipse_object(self, priority: int = 988, **kwargs) -> None: + """Set up the celestial object that is causing an eclipse message. + + Args: + priority: Model priority. + kwargs: Ignored + """ + self.eclipseObject = eclipse.Eclipse() + self.eclipseObject.addPlanetToModel( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + ) + self.eclipseObject.sunInMsg.subscribeTo( + self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] + ) + self.simulator.AddModelToTask( + self.world_task_name, self.eclipseObject, ModelPriority=priority + ) + + +class AtmosphereWorldModel(BaseWorldModel): + def __init__(self, *args, **kwargs) -> None: + """Model that includes an atmosphere. + + This model adds an atmosphere to the planet that can be used for drag. + + Args: + *args: Passed to superclass. + **kwargs: Passed to superclass. + + """ + super().__init__(*args, **kwargs) + + def _setup_world_objects(self, **kwargs) -> None: + super()._setup_world_objects(**kwargs) + self.setup_atmosphere_density_model(**kwargs) + @default_args( planetRadius=orbitalMotion.REQ_EARTH * 1e3, baseDensity=1.22, @@ -239,41 +299,14 @@ def setup_atmosphere_density_model( self.world_task_name, self.densityModel, ModelPriority=priority ) - def setup_eclipse_object(self, priority: int = 988, **kwargs) -> None: - """Set up the celestial object that is causing an eclipse message. - - Args: - priority: Model priority. - kwargs: Ignored - """ - self.eclipseObject = eclipse.Eclipse() - self.eclipseObject.addPlanetToModel( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - ) - self.eclipseObject.sunInMsg.subscribeTo( - self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] - ) - self.simulator.AddModelToTask( - self.world_task_name, self.eclipseObject, ModelPriority=priority - ) - - def __del__(self) -> None: - """Log when world is deleted and unload SPICE.""" - super().__del__() - try: - self.gravFactory.unloadSpiceKernels() - except AttributeError: - pass - -class GroundStationWorldModel(BasicWorldModel): +class GroundStationWorldModel(BaseWorldModel): """Model that includes downlink ground stations.""" def __init__(self, *args, **kwargs) -> None: """Model that includes downlink ground stations. - This model includes the basic world components, as well as ground stations for - downlinking data. + This model adds ground stations for downlinking data. Args: *args: Passed to superclass. @@ -394,4 +427,10 @@ def _create_ground_station( __doc_title__ = "World Sims" -__all__ = ["WorldModel", "BasicWorldModel", "GroundStationWorldModel"] +__all__ = [ + "WorldModel", + "BaseWorldModel", + "EclipseWorldModel", + "AtmosphereWorldModel", + "GroundStationWorldModel", +] diff --git a/src/bsk_rl/utils/functional.py b/src/bsk_rl/utils/functional.py index 3aceb955..b17c6674 100644 --- a/src/bsk_rl/utils/functional.py +++ b/src/bsk_rl/utils/functional.py @@ -4,7 +4,9 @@ import warnings from copy import deepcopy from functools import wraps +from types import new_class from typing import Any, Callable, ParamSpec, TypeVar +from unittest.mock import MagicMock import numpy as np @@ -84,8 +86,8 @@ def collect_default_args(object: object) -> dict[str, Any]: defaults = {} for name in dir(object): if ( - callable(getattr(object, name)) - and not name.startswith("__") + not name.startswith("__") + and callable(getattr(object, name)) and hasattr(getattr(object, name), "defaults") ): safe_dict_merge(getattr(object, name).defaults, defaults) @@ -161,6 +163,52 @@ def is_property(obj: Any, attr_name: str) -> bool: return attribute is not None and isinstance(attribute, property) +def mock_safe_bases(*bases): + """Return bases with MagicMock replaced by their specs.""" + clean_bases = [] + for b in bases: + if isinstance(b, MagicMock): + # Use its spec if available, else object + clean_bases.append(getattr(b, "_spec_class", object)) + else: + clean_bases.append(b) + return tuple(clean_bases) + + +def remove_duplicate_bases(*bases): + """Removes duplicate bases while preserving order.""" + seen = set() + clean_bases = [] + for b in bases: + if b not in seen: + seen.add(b) + clean_bases.append(b) + return tuple(clean_bases) + + +def remove_inherited_bases(*bases): + """Removes bases that are subclasses of other bases to help with MRO.""" + clean_bases = [] + for b in bases: + for b2 in bases: + if b != b2 and issubclass(b2, b): + break + else: + clean_bases.append(b) + return tuple(clean_bases) + + +def compose_types(*types, name=""): + """Compose multiple types into a new type.""" + try: + type_name = f"{name}({', '.join([t.__name__ for t in types])})" + except (AttributeError, TypeError): + type_name = f"{name}({len(types)})" + types = remove_duplicate_bases(*types) + types = remove_inherited_bases(*types) + return new_class(type_name, bases=mock_safe_bases(*types)) + + class AbstractClassProperty: def __init__(self): """Assign a class property to act like an abstract field.""" diff --git a/tests/integration/comm/test_int_communication.py b/tests/integration/comm/test_int_communication.py index 3fcc5f79..b7de09ba 100644 --- a/tests/integration/comm/test_int_communication.py +++ b/tests/integration/comm/test_int_communication.py @@ -33,17 +33,13 @@ ) -class FullFeaturedDynModel(dyn.GroundStationDynModel, dyn.LOSCommDynModel): - pass - - class FullFeaturedSatellite(sats.ImagingSatellite): observation_spec = [ obs.SatProperties(dict(prop="r_BN_P", module="dynamics", norm=6e6)), obs.Time(), ] action_spec = [act.Image(n_ahead_image=10)] - dyn_type = FullFeaturedDynModel + dyn_type = (dyn.LOSCommDynModel, dyn.GroundStationDynModel) def make_communication_env(oes, comm_type): diff --git a/tests/integration/sim/test_int_dynamics.py b/tests/integration/sim/test_int_dynamics.py index bb5af5d6..583b3a0c 100644 --- a/tests/integration/sim/test_int_dynamics.py +++ b/tests/integration/sim/test_int_dynamics.py @@ -108,7 +108,7 @@ class TestConjunctionDynModel: ) def test_conjunction(self, rN1, vN1, collision): class CollisionSat(sats.Satellite): - fsw_type = fsw.BasicFSWModel + fsw_type = fsw.BaseFSWModel dyn_type = dyn.ConjunctionDynModel observation_spec = [obs.Time()] action_spec = [act.Drift()] @@ -177,13 +177,13 @@ class TestMaxRangeDynModel: @pytest.mark.parametrize("fail_chief", [True, False]) def test_max_range(self, rN1, rN2, max_range_violation, fail_chief): class ChiefSat(sats.Satellite): - fsw_type = fsw.BasicFSWModel - dyn_type = dyn.BasicDynamicsModel + fsw_type = fsw.BaseFSWModel + dyn_type = dyn.BaseDynamicsModel observation_spec = [obs.Time()] action_spec = [act.Drift()] class DeputySat(sats.Satellite): - fsw_type = fsw.BasicFSWModel + fsw_type = fsw.BaseFSWModel dyn_type = dyn.MaxRangeDynModel observation_spec = [obs.Time()] action_spec = [act.Drift()] diff --git a/tests/integration/test_int_rso_env.py b/tests/integration/test_int_rso_env.py index 8a9b3f55..48057748 100644 --- a/tests/integration/test_int_rso_env.py +++ b/tests/integration/test_int_rso_env.py @@ -1,4 +1,3 @@ -import types from functools import partial import numpy as np @@ -42,14 +41,11 @@ class InspectorSat(sats.Satellite): fsw_action="action_inspect_rso", ) ] - dyn_type = types.new_class("Dyn", (dyn.MaxRangeDynModel, dyn.RSOInspectorDynModel)) - fsw_type = types.new_class( - "FSW", - ( - fsw.SteeringFSWModel, - fsw.MagicOrbitalManeuverFSWModel, - fsw.RSOInspectorFSWModel, - ), + dyn_type = (dyn.MaxRangeDynModel, dyn.RSOInspectorDynModel) + fsw_type = ( + fsw.SteeringFSWModel, + fsw.MagicOrbitalManeuverFSWModel, + fsw.RSOInspectorFSWModel, ) diff --git a/tests/unittest/sats/test_satellite.py b/tests/unittest/sats/test_satellite.py index 6c023664..6905efef 100644 --- a/tests/unittest/sats/test_satellite.py +++ b/tests/unittest/sats/test_satellite.py @@ -9,18 +9,41 @@ from bsk_rl.sim.fsw import Task +class MockDynType: + def with_defaults(self): + pass + + +MockDynType.with_defaults.defaults = {"a": 1, "d": np.array([4, 5])} + + +class MockFSWType: + def with_defaults(self): + pass + + some_task = Task + + +MockFSWType.with_defaults.defaults = {"b": 2} + + +def with_defaults(): + pass + + +MockFSWType.some_task.with_defaults = with_defaults +MockFSWType.some_task.with_defaults.defaults = {"c": 3} + + @patch.multiple(sats.Satellite, __abstractmethods__=set()) @patch("bsk_rl.sats.Satellite.observation_spec", MagicMock()) @patch("bsk_rl.sats.Satellite.action_spec", [MagicMock()]) +# Mock to avoid picking up the base models +@patch("bsk_rl.sats.Satellite.get_dyn_type", MagicMock(return_value=MockDynType)) +@patch("bsk_rl.sats.Satellite.get_fsw_type", MagicMock(return_value=MockFSWType)) class TestSatellite: - sats.Satellite.dyn_type = MagicMock( - with_defaults=MagicMock(defaults={"a": 1, "d": np.array([4, 5])}) - ) - Task.with_defaults = MagicMock(defaults={"c": 3}) - sats.Satellite.fsw_type = MagicMock( - with_defaults=MagicMock(defaults={"b": 2}), - some_task=Task, - ) + sats.Satellite.dyn_type = MockDynType + sats.Satellite.fsw_type = MockFSWType sats.Satellite.logger = MagicMock() def test_default_sat_args(self): @@ -141,7 +164,9 @@ def test_proxy_setters(self): mock_fsw = MagicMock() sat = sats.Satellite(name="TestSat", sat_args=None) + sat.dyn_type = MagicMock() sat.dyn_type.return_value = mock_dyn + sat.fsw_type = MagicMock() sat.fsw_type.return_value = mock_fsw sat.generate_sat_args() sat.set_simulator(mock_sim) diff --git a/tests/unittest/sim/test_dynamics.py b/tests/unittest/sim/test_dynamics.py index a4d2c806..9d71641c 100644 --- a/tests/unittest/sim/test_dynamics.py +++ b/tests/unittest/sim/test_dynamics.py @@ -36,10 +36,6 @@ def test_is_alive(self): basicdyn = module + "BasicDynamicsModel." -def test_basic_requires_world(): - assert world.BasicWorldModel in BasicDynamicsModel._requires_world() - - @patch(basicdyn + "_requires_world", MagicMock(return_value=[])) @patch(basicdyn + "setup_spacecraft_hub") @patch(basicdyn + "setup_drag_effector") @@ -52,6 +48,8 @@ def test_basic_requires_world(): @patch(basicdyn + "setup_power_sink") @patch(basicdyn + "setup_reaction_wheel_power") @patch(basicdyn + "setup_thruster_power") +@patch(basicdyn + "setup_disturbance_torque") +@patch(basicdyn + "setup_density_model") def test_basic_setup_objects(self, *args): BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: @@ -218,7 +216,7 @@ class TestLOSCommDynModel: losdyn = module + "LOSCommDynModel." @patch(losdyn + "_requires_world", MagicMock(return_value=[])) - @patch(module + "BasicDynamicsModel._setup_dynamics_objects", MagicMock()) + @patch(module + "BaseDynamicsModel._setup_dynamics_objects", MagicMock()) @patch(losdyn + "setup_los_comms") def test_setup_objects(self, *args): LOSCommDynModel(MagicMock(simulator=MagicMock()), 1.0) diff --git a/tests/unittest/sim/test_world.py b/tests/unittest/sim/test_world.py index aa37fd11..63182fea 100644 --- a/tests/unittest/sim/test_world.py +++ b/tests/unittest/sim/test_world.py @@ -3,7 +3,13 @@ import numpy as np import pytest -from bsk_rl.sim.world import BasicWorldModel, GroundStationWorldModel, WorldModel +from bsk_rl.sim.world import ( + AtmosphereWorldModel, + BaseWorldModel, + EclipseWorldModel, + GroundStationWorldModel, + WorldModel, +) module = "bsk_rl.sim.world." @@ -43,21 +49,21 @@ def test_init(self, mock_obj_init): assert world.simulator == mock_sim -class TestBasicWorldModel: - basicworld = module + "BasicWorldModel." +class TestBaseWorldModel: + baseworld = module + "BaseWorldModel." - @patch(basicworld + "__init__", MagicMock(return_value=None)) + @patch(baseworld + "__init__", MagicMock(return_value=None)) def test_PN(self): - world = BasicWorldModel(MagicMock(), 1.0) + world = BaseWorldModel(MagicMock(), 1.0) world.gravFactory = MagicMock() world.body_index = 0 msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ msg.return_value.read.return_value.J20002Pfix = [1, 0, 0, 0, 1, 0, 0, 0, 1] assert (world.PN == np.identity(3)).all() - @patch(basicworld + "__init__", MagicMock(return_value=None)) + @patch(baseworld + "__init__", MagicMock(return_value=None)) def test_omega_PN_N(self): - world = BasicWorldModel(MagicMock(), 1.0) + world = BaseWorldModel(MagicMock(), 1.0) world.gravFactory = MagicMock() world.body_index = 0 msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ @@ -65,33 +71,31 @@ def test_omega_PN_N(self): msg.return_value.read.return_value.J20002Pfix = [0, -1, 1, 1, 0, -1, -1, 1, 0] assert (world.omega_PN_N == np.array([1, 1, 1])).all() - @patch(basicworld + "setup_gravity_bodies") - @patch(basicworld + "setup_ephem_object") - @patch(basicworld + "setup_atmosphere_density_model") - @patch(basicworld + "setup_eclipse_object") - def test_setup_and_delete(self, grav_set, epoch_set, atmos_set, eclipse_set): - world = BasicWorldModel(MagicMock(), 1.0) - for setter in (grav_set, epoch_set, atmos_set, eclipse_set): + @patch(baseworld + "setup_gravity_bodies") + @patch(baseworld + "setup_ephem_object") + def test_setup_and_delete(self, grav_set, epoch_set): + world = BaseWorldModel(MagicMock(), 1.0) + for setter in (grav_set, epoch_set): setter.assert_called_once() unload_function = MagicMock() world.gravFactory = MagicMock(unloadSpiceKernels=unload_function) del world unload_function.assert_called_once() - @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(baseworld + "_setup_world_objects", MagicMock()) @patch(module + "simIncludeGravBody", MagicMock()) def testsetup_gravity_bodies(self): # Smoke test - world = BasicWorldModel(MagicMock(), 1.0) + world = BaseWorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.setup_gravity_bodies(utc_init="time") world.simulator.AddModelToTask.assert_called_once() - @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(baseworld + "_setup_world_objects", MagicMock()) @patch(module + "ephemerisConverter", MagicMock()) def testsetup_epoch_object(self): # Smoke test - world = BasicWorldModel(MagicMock(), 1.0) + world = BaseWorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.gravFactory = MagicMock() world.sun_index = 0 @@ -99,11 +103,15 @@ def testsetup_epoch_object(self): world.setup_ephem_object() world.simulator.AddModelToTask.assert_called_once() - @patch(basicworld + "_setup_world_objects", MagicMock()) + +class TestAtmosphereWorldModel: + baseworld = module + "AtmosphereWorldModel." + + @patch(baseworld + "_setup_world_objects", MagicMock()) @patch(module + "exponentialAtmosphere", MagicMock()) def testsetup_atmosphere_density_model(self): # Smoke test - world = BasicWorldModel(MagicMock(), 1.0) + world = AtmosphereWorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.gravFactory = MagicMock() world.body_index = 1 @@ -115,11 +123,15 @@ def testsetup_atmosphere_density_model(self): ) world.simulator.AddModelToTask.assert_called_once() - @patch(basicworld + "_setup_world_objects", MagicMock()) + +class TestEclipseWorldModel: + baseworld = module + "EclipseWorldModel." + + @patch(baseworld + "_setup_world_objects", MagicMock()) @patch(module + "eclipse", MagicMock()) def testsetup_eclipse_object(self): # Smoke test - world = BasicWorldModel(MagicMock(), 1.0) + world = EclipseWorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.gravFactory = MagicMock() world.sun_index = 0 @@ -132,7 +144,7 @@ class TestGroundStationWorldModel: groundworld = module + "GroundStationWorldModel." @patch(groundworld + "setup_ground_locations") - @patch(module + "BasicWorldModel._setup_world_objects", MagicMock()) + @patch(module + "BaseWorldModel._setup_world_objects", MagicMock()) def test_setup_world_objects(self, ground_set): GroundStationWorldModel(MagicMock(), 1.0) ground_set.assert_called_once() diff --git a/tests/unittest/test_gym_env.py b/tests/unittest/test_gym_env.py index 7fbc29ae..98a016fc 100644 --- a/tests/unittest/test_gym_env.py +++ b/tests/unittest/test_gym_env.py @@ -52,7 +52,7 @@ def test_minimum_world_model(self, classes, result): ) for class_list in classes ] - assert env._minimum_world_model() == result + assert issubclass(env._minimum_world_model(), result) @patch( "bsk_rl.GeneralSatelliteTasking.__init__", @@ -76,7 +76,6 @@ def test_multiple_rewarders(self): mock_rewarder = [MagicMock(scenario=None), MagicMock(scenario=None)] env = GeneralSatelliteTasking( satellites=[mock_sat], - world_type=MagicMock(), scenario=MagicMock(), rewarder=mock_rewarder, ) @@ -89,7 +88,6 @@ def test_reset(self, mock_sim): mock_rewarder = MagicMock(scenario=None) env = GeneralSatelliteTasking( satellites=[mock_sat], - world_type=MagicMock(), scenario=MagicMock(), rewarder=mock_rewarder, ) @@ -127,7 +125,6 @@ def test_name_conflict(self, sat_names, expected): sat.name = name env = GeneralSatelliteTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(scenario=None), ) @@ -138,7 +135,6 @@ def test_name_conflict(self, sat_names, expected): def test_get_obs(self): env = GeneralSatelliteTasking( satellites=[MagicMock(get_obs=MagicMock(return_value=i)) for i in range(3)], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -156,7 +152,6 @@ def test_get_obs_retasking_only(self): ) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), generate_obs_retasking_only=True, @@ -169,7 +164,6 @@ def test_get_info(self): sat.name = f"sat{i}" env = GeneralSatelliteTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -185,7 +179,6 @@ def test_action_space(self): satellites=[ MagicMock(action_space=spaces.Discrete(i + 1)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -198,7 +191,6 @@ def test_obs_space_no_sim(self): satellites=[ MagicMock(observation_space=spaces.Discrete(i + 1)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -215,7 +207,6 @@ def test_obs_space_existing_sim(self): satellites=[ MagicMock(observation_space=spaces.Discrete(i + 1)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -230,7 +221,6 @@ def test_step(self): mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), communicator=MagicMock(), rewarder=MagicMock( @@ -253,7 +243,6 @@ def test_step_bad_action(self): mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(reward=MagicMock(return_value=25.0)), ) @@ -279,7 +268,6 @@ def test_step_stopped( mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), communicator=MagicMock(), rewarder=MagicMock( @@ -313,7 +301,6 @@ def test_step_retask_needed(self, capfd): mock_sat = MagicMock() env = SatelliteTasking( satellite=[mock_sat], - world_type=MagicMock(), scenario=MagicMock(), communicator=MagicMock(), rewarder=MagicMock(reward=MagicMock(return_value={mock_sat.name: 25.0})), @@ -332,7 +319,6 @@ def test_render(self): def test_close(self): env = GeneralSatelliteTasking( satellites=[MagicMock()], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -346,13 +332,13 @@ class TestSatelliteTasking: Satellite, __abstractmethods__=set(), __init__=MagicMock(return_value=None), + dyn_type=MagicMock(), ) def test_init(self): mock_sat = Satellite("sat", {}) mock_sat.name = "sat" env = SatelliteTasking( satellite=mock_sat, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -362,7 +348,6 @@ def test_init_multisat(self): with pytest.raises(ValueError): SatelliteTasking( satellite=[MagicMock(), MagicMock()], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -372,7 +357,6 @@ def make_env(): mock_sat = MagicMock() env = SatelliteTasking( satellite=[mock_sat], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -412,7 +396,6 @@ def test_reset(self, mock_sim, obs_fn, info_fn): mock_data = MagicMock(scenario=None) env = ConstellationTasking( satellites=[mock_sat_1, mock_sat_2], - world_type=MagicMock(), scenario=MagicMock(), rewarder=mock_data, ) @@ -429,7 +412,6 @@ def test_reset(self, mock_sim, obs_fn, info_fn): def test_agents(self): env = ConstellationTasking( satellites=[MagicMock() for i in range(3)], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -447,7 +429,6 @@ def test_agents(self): def test_get_obs(self): env = ConstellationTasking( satellites=[MagicMock(get_obs=MagicMock(return_value=i)) for i in range(3)], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -468,7 +449,6 @@ def test_get_info(self): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -490,7 +470,6 @@ def test_action_spaces(self): satellites=[ MagicMock(action_space=spaces.Discrete(i + 1)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -506,7 +485,6 @@ def test_obs_spaces(self): MagicMock(observation_space=spaces.Box(low=0, high=i + 1, shape=(1,))) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -527,7 +505,6 @@ def test_get_reward(self): satellites=[ MagicMock(is_alive=MagicMock(return_value=False)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), failure_penalty=-20.0, @@ -551,7 +528,6 @@ def test_get_reward_missing_sat(self): satellites=[ MagicMock(is_alive=MagicMock(return_value=False)) for i in range(3) ], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), failure_penalty=-20.0, @@ -579,7 +555,6 @@ def test_get_terminated(self, timeout, terminate_on_time_limit): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock( is_terminated=MagicMock(return_value=False), @@ -615,7 +590,6 @@ def test_get_truncated(self, time): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock( is_terminated=MagicMock(return_value=False), @@ -642,7 +616,6 @@ def test_time_limit_generator(self): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), time_limit=lambda: np.random.uniform(10, 20), @@ -661,7 +634,6 @@ def test_time_limit_generator(self): def test_close(self): env = ConstellationTasking( satellites=[MagicMock()], - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -679,7 +651,6 @@ def test_dead(self): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) @@ -713,7 +684,6 @@ def test_step(self): sat.name = f"sat{i}" env = ConstellationTasking( satellites=mock_sats, - world_type=MagicMock(), scenario=MagicMock(), rewarder=MagicMock(), ) From 6f01d04a1cb8ca729159e2b1aaec97301b5bd3e9 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 4 Nov 2025 12:18:42 -0700 Subject: [PATCH 4/8] [#156] Update release notes --- docs/source/release_notes.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index c5714c64..24691656 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -17,6 +17,19 @@ Development - |version| * Allow for a simpler Earth model to be used in Vizard by setting ``use_simple_earth=True`` in the Vizard settings dictionary. This is helpful for when visualizing may Earth-fixed targets. +* Allow flight software and dynamics models to be specified as lists of classes. This allows + for multiple inheritance to be used for easily creating more complex satellite models. +* The inheritance structure of flight software and dynamics models has changed. Most models + now inherit from :class:`~bsk_rl.sim.fsw.BaseFSWModel` or :class:`~bsk_rl.sim.dyn.BaseDynModel` + instead of :class:`~bsk_rl.sim.fsw.FSWModel` or :class:`~bsk_rl.sim.dyn.DynamicsModel`. + These are lighter-weight base classes that lack some functionality that was not always + wanted. + + .. warning:: + + If your custom satellite configurations break as a result of this change, add + :class:`~bsk_rl.sim.fsw.BasicFSWModel` and :class:`~bsk_rl.sim.dyn.BasicDynModel` + to your ``fsw_type`` and ``dyn_type`` lists in your satellite classes. Version 1.2.0 From d27abf9b2d239ba6e0e14b6706a7fc29b46d6555 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 4 Nov 2025 17:22:26 -0700 Subject: [PATCH 5/8] [#156] Add warnings for changed behavior --- src/bsk_rl/gym.py | 14 ++++++++++++-- src/bsk_rl/sim/fsw/base.py | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index f0d0eb57..6549b2c2 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -56,7 +56,9 @@ def __init__( satellites: Union[Satellite, list[Satellite]], scenario: Optional[Scenario] = None, rewarder: Optional[Union[GlobalReward, list[GlobalReward]]] = None, - world_type: Optional[type[WorldModel]] = None, + world_type: Optional[ + Union[type[WorldModel], tuple[type[WorldModel], ...]] + ] = None, world_args: Optional[dict[str, Any]] = None, communicator: Optional[CommunicationMethod] = None, sat_arg_randomizer: Optional[SatArgRandomizer] = None, @@ -96,7 +98,7 @@ def __init__( sat_arg_randomizer: For correlated randomization of satellites arguments. Should be a function that takes a list of satellites and returns a dictionary that maps satellites to dictionaries of satellite model arguments to be overridden. - world_type: Type of Basilisk world model to be constructed. + world_type: Type or tuple of types of Basilisk world model to be constructed. world_args: Arguments for :class:`~bsk_rl.sim.world.WorldModel` construction. Should be in the form of a dictionary with keys corresponding to the arguments of the constructor and values that are either the desired value @@ -175,6 +177,14 @@ def __init__( if world_type is None: world_type = self._minimum_world_model() + else: + logger.warning( + "Using user-specified world type. Generally, the env-determined world " + "type is sufficient." + ) + if not isinstance(world_type, (list, tuple)): + world_type = (world_type,) + world_type = functional.compose_types(*world_type, WorldModel, name="World") self.world_type = world_type if world_args is None: world_args = self.world_type.default_world_args() diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 802d2220..755a4a79 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -149,6 +149,11 @@ def __init__( for required in self._requires_dyn(): if not issubclass(satellite.dyn_type, required): + self.satellite.logger.warning( + "Some sim model inheritance has changed. Explicitly include" + "BasicFSWModel and BasicDynamicsModel if your configurations no " + "longer work." + ) raise TypeError( f"{satellite.dyn_type} must be a subclass of {required} to " + f"use FSW model of type {self.__class__}" From cbe93503810ec819a51d2d12da880a6c5c7aeecc Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 4 Nov 2025 23:50:42 -0700 Subject: [PATCH 6/8] [#156] Add direct attitude control and separate eclipse dynamics --- docs/source/release_notes.rst | 6 +- examples/rso_inspection.ipynb | 21 ++---- src/bsk_rl/act/__init__.py | 3 + src/bsk_rl/act/discrete_actions.py | 13 +++- src/bsk_rl/sim/dyn/__init__.py | 8 ++- src/bsk_rl/sim/dyn/base.py | 32 ++++++--- src/bsk_rl/sim/dyn/rso_inspection.py | 4 +- src/bsk_rl/sim/fsw/base.py | 97 ++++++++++++++++----------- tests/integration/test_int_rso_env.py | 2 +- 9 files changed, 118 insertions(+), 68 deletions(-) diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index 24691656..8a6b57fe 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -21,7 +21,7 @@ Development - |version| for multiple inheritance to be used for easily creating more complex satellite models. * The inheritance structure of flight software and dynamics models has changed. Most models now inherit from :class:`~bsk_rl.sim.fsw.BaseFSWModel` or :class:`~bsk_rl.sim.dyn.BaseDynModel` - instead of :class:`~bsk_rl.sim.fsw.FSWModel` or :class:`~bsk_rl.sim.dyn.DynamicsModel`. + instead of :class:`~bsk_rl.sim.fsw.BasicFSWModel` or :class:`~bsk_rl.sim.dyn.BasicDynModel`. These are lighter-weight base classes that lack some functionality that was not always wanted. @@ -31,6 +31,10 @@ Development - |version| :class:`~bsk_rl.sim.fsw.BasicFSWModel` and :class:`~bsk_rl.sim.dyn.BasicDynModel` to your ``fsw_type`` and ``dyn_type`` lists in your satellite classes. +* :class:`~bsk_rl.sim.fsw.BaseFSWModel` implements direct actuator-less attitude control. + By inheriting from a FSW class that overrides the ``MRPControlTask``, such as + :class:`~bsk_rl.sim.fsw.BasicFSWModel` or :class:`~bsk_rl.sim.fsw.SteeringFSWModel`, + users can implement custom attitude control strategies. Version 1.2.0 ------------- diff --git a/examples/rso_inspection.ipynb b/examples/rso_inspection.ipynb index 1effc47d..187cdabc 100644 --- a/examples/rso_inspection.ipynb +++ b/examples/rso_inspection.ipynb @@ -60,8 +60,7 @@ "source": [ "## Defining the Satellites\n", "\n", - "First, the RSO satellite is configured. It is given support for nadir pointing through\n", - "the ``ImagingDynModel`` and ``Downlink`` action." + "First, the RSO satellite is configured. A simple model is used that has no actuators modelled and just deterministically points nadir." ] }, { @@ -75,9 +74,9 @@ " observation_spec = [\n", " obs.SatProperties(dict(prop=\"one\", fn=lambda _: 1.0)),\n", " ]\n", - " action_spec = [act.Downlink(duration=1e9)]\n", - " dyn_type = (dyn.ImagingDynModel, dyn.ConjunctionDynModel, dyn.RSODynModel)\n", - " fsw_type = fsw.ContinuousImagingFSWModel\n" + " action_spec = [act.NadirPoint(duration=1e9)]\n", + " dyn_type = (dyn.ConjunctionDynModel, dyn.RSODynModel)\n", + " fsw_type = fsw.BaseFSWModel\n" ] }, { @@ -95,17 +94,7 @@ "metadata": {}, "outputs": [], "source": [ - "rso_sat_args = dict(\n", - " conjunction_radius=2.0,\n", - " K=7.0 / 20,\n", - " P=35.0 / 20,\n", - " Ki=1e-6,\n", - " dragCoeff=0.0,\n", - " batteryStorageCapacity=1e9,\n", - " storedCharge_Init=1e9,\n", - " wheelSpeeds=[0.0, 0.0, 0.0],\n", - " u_max=1.0,\n", - ")" + "rso_sat_args = dict(conjunction_radius=2.0)" ] }, { diff --git a/src/bsk_rl/act/__init__.py b/src/bsk_rl/act/__init__.py index 23858b1b..ac931ebc 100644 --- a/src/bsk_rl/act/__init__.py +++ b/src/bsk_rl/act/__init__.py @@ -30,6 +30,8 @@ class MyActionSatellite(Satellite): +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ | :class:`Drift` | 1 | Do nothing. | +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`NadirPoint` | 1 | Point the satellite nadir. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ | :class:`Desat` | 1 | Desaturate the reaction wheels with RCS thrusters. Needs to be called multiple times. | +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ | :class:`Downlink` | 1 | Downlink data to any ground station that is in range. | @@ -72,6 +74,7 @@ class MyActionSatellite(Satellite): Downlink, Drift, Image, + NadirPoint, Scan, ) diff --git a/src/bsk_rl/act/discrete_actions.py b/src/bsk_rl/act/discrete_actions.py index b8b7efe7..8baf4532 100644 --- a/src/bsk_rl/act/discrete_actions.py +++ b/src/bsk_rl/act/discrete_actions.py @@ -180,7 +180,7 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) class Drift(DiscreteFSWAction): def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): - """Action to disable all FSW tasks (:class:`~bsk_rl.sim.fsw.BasicFSWModel.action_drift`). + """Action to disable all FSW tasks (:class:`~bsk_rl.sim.fsw.BaseFSWModel.action_drift`). Args: name: Action name. @@ -189,6 +189,17 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) super().__init__(fsw_action="action_drift", name=name, duration=duration) +class NadirPoint(DiscreteFSWAction): + def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + """Action to point nadir (:class:`~bsk_rl.sim.fsw.BaseFSWModel.action_nadir_point`). + + Args: + name: Action name. + duration: Time to task action, in seconds. + """ + super().__init__(fsw_action="action_nadir_point", name=name, duration=duration) + + class Desat(DiscreteFSWAction): def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): """Action to desaturate reaction wheels (:class:`~bsk_rl.sim.fsw.BasicFSWModel.action_desat`). diff --git a/src/bsk_rl/sim/dyn/__init__.py b/src/bsk_rl/sim/dyn/__init__.py index 0a8f7c83..067b5562 100644 --- a/src/bsk_rl/sim/dyn/__init__.py +++ b/src/bsk_rl/sim/dyn/__init__.py @@ -38,7 +38,12 @@ from deprecated import deprecated -from bsk_rl.sim.dyn.base import BaseDynamicsModel, BasicDynamicsModel, DynamicsModel +from bsk_rl.sim.dyn.base import ( + BaseDynamicsModel, + BasicDynamicsModel, + DynamicsModel, + EclipseDynModel, +) from bsk_rl.sim.dyn.ground_imaging import ( ContinuousImagingDynModel, GroundStationDynModel, @@ -68,6 +73,7 @@ def __init__(self, *args, **kwargs) -> None: "DynamicsModel", "BaseDynamicsModel", "BasicDynamicsModel", + "EclipseDynModel", "LOSCommDynModel", "ImagingDynModel", "ContinuousImagingDynModel", diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index 9c7aa2a8..88ee235f 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -372,7 +372,30 @@ def altitude_valid(self) -> bool: return np.linalg.norm(self.r_BN_N) > self.min_orbital_radius -class BasicDynamicsModel(BaseDynamicsModel): +class EclipseDynModel(BaseDynamicsModel): + """Dynamics model with eclipse checking.""" + + def __init__(self, *args, **kwargs) -> None: + """Dynamics model with eclipse checking.""" + super().__init__(*args, **kwargs) + + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return [ + world.EclipseWorldModel, + ] + super()._requires_world() + + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_eclipse_object() + + def setup_eclipse_object(self) -> None: + """Add the spacecraft to the eclipse module.""" + self.world.eclipseObject.addSpacecraftToModel(self.scObject.scStateOutMsg) + self.eclipse_index = len(self.world.eclipseObject.eclipseOutMsgs) - 1 + + +class BasicDynamicsModel(EclipseDynModel, BaseDynamicsModel): """Basic Dynamics model with minimum necessary Basilisk components.""" def __init__(self, *args, **kwargs) -> None: @@ -398,7 +421,6 @@ def __init__(self, *args, **kwargs) -> None: @classmethod def _requires_world(cls) -> list[type["WorldModel"]]: return [ - world.EclipseWorldModel, world.AtmosphereWorldModel, ] + super()._requires_world() @@ -429,7 +451,6 @@ def _setup_dynamics_objects(self, **kwargs) -> None: self.setup_drag_effector(**kwargs) self.setup_reaction_wheel_dyn_effector(**kwargs) self.setup_thruster_dyn_effector() - self.setup_eclipse_object() self.setup_solar_panel(**kwargs) self.setup_battery(**kwargs) self.setup_power_sink(**kwargs) @@ -607,11 +628,6 @@ def setup_thruster_power( ) self.powerMonitor.addPowerNodeToModel(self.thrusterPowerSink.nodePowerOutMsg) - def setup_eclipse_object(self) -> None: - """Add the spacecraft to the eclipse module.""" - self.world.eclipseObject.addSpacecraftToModel(self.scObject.scStateOutMsg) - self.eclipse_index = len(self.world.eclipseObject.eclipseOutMsgs) - 1 - @default_args( panelArea=2 * 1.0 * 0.5, panelEfficiency=0.20, diff --git a/src/bsk_rl/sim/dyn/rso_inspection.py b/src/bsk_rl/sim/dyn/rso_inspection.py index ec1c281c..54dfbd5c 100644 --- a/src/bsk_rl/sim/dyn/rso_inspection.py +++ b/src/bsk_rl/sim/dyn/rso_inspection.py @@ -4,10 +4,10 @@ from Basilisk.simulation import spacecraftLocation from Basilisk.utilities import macros -from bsk_rl.sim.dyn import BasicDynamicsModel, ContinuousImagingDynModel +from bsk_rl.sim.dyn import BaseDynamicsModel, ContinuousImagingDynModel, EclipseDynModel -class RSODynModel(BasicDynamicsModel): +class RSODynModel(EclipseDynModel, BaseDynamicsModel): """For an RSO with points targets for observation.""" def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 755a4a79..463b210b 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -91,7 +91,6 @@ def create_task(self) -> None: taskPriority=self.priority, ) - @abstractmethod # pragma: no cover def _create_module_data(self) -> None: """Create module data wrappers.""" pass @@ -248,7 +247,27 @@ def _zero_gateway_msgs(self) -> None: self.attGuidMsg.write(messaging.AttGuidMsgPayload()) def _make_task_list(self): - return [] + return [self.MRPControlTask(self), self.NadirPointTask(self)] + + class MRPControlTask(Task): + """Task to control the satellite attitude magically (i.e. without actuators).""" + + name = "mrpControlTask" + + def __init__(self, fsw, priority=80) -> None: # noqa: D107 + """Task to control the satellite magically.""" + super().__init__(fsw, priority) + + def _setup_fsw_objects(self, **kwargs) -> None: + self.setup_automatic_pointing() + + def setup_automatic_pointing(self): + """Connect the attitude to the reference.""" + self.fsw.dynamics.scObject.attRefInMsg.subscribeTo(self.fsw.attRefMsg) + + def reset_for_action(self) -> None: + """MRP control is enabled by default for all tasks.""" + self.fsw.simulator.enableTask(self.name + self.fsw.satellite.name) @action def action_drift(self) -> None: @@ -257,9 +276,44 @@ def action_drift(self) -> None: BasicFSWModel.MRPControlTask.name + self.satellite.name ) + class NadirPointTask(Task): + """Task to generate nadir-pointing reference.""" + + name = "nadirPointTask" + + def __init__(self, fsw, priority=98) -> None: # noqa: D107 + """Task to generate nadir-pointing reference.""" + super().__init__(fsw, priority) -class MagicPointingFSWModel(BaseFSWModel): - pass # TODO + def _create_module_data(self) -> None: + self.hillPoint = self.fsw.hillPoint = hillPoint.hillPoint() + self.hillPoint.ModelTag = "hillPoint" + + def _setup_fsw_objects(self, **kwargs) -> None: + """Configure the nadir-pointing task. + + Args: + kwargs: Passed to other setup functions. + """ + self.hillPoint.transNavInMsg.subscribeTo( + self.fsw.dynamics.simpleNavObject.transOutMsg + ) + self.hillPoint.celBodyInMsg.subscribeTo( + self.fsw.world.ephemConverter.ephemOutMsgs[self.fsw.world.body_index] + ) + messaging.AttRefMsg_C_addAuthor( + self.hillPoint.attRefOutMsg, self.fsw.attRefMsg + ) + + self._add_model_to_task(self.hillPoint, priority=1199) + + @action + def action_nadir_point(self) -> None: + """Point the satellite nadir.""" + self.hillPoint.Reset(self.simulator.sim_time_ns) + self.simulator.enableTask( + BasicFSWModel.NadirPointTask.name + self.satellite.name + ) class BasicFSWModel(BaseFSWModel): @@ -272,11 +326,9 @@ def _requires_dyn(cls) -> list[type["DynamicsModel"]]: def _make_task_list(self) -> list[Task]: return [ self.SunPointTask(self), - self.NadirPointTask(self), self.RWDesatTask(self), self.TrackingErrorTask(self), - self.MRPControlTask(self), - ] + ] + super()._make_task_list() def _set_config_msgs(self) -> None: super()._set_config_msgs() @@ -350,37 +402,6 @@ def action_charge(self) -> None: self.sunPoint.Reset(self.simulator.sim_time_ns) self.simulator.enableTask(self.SunPointTask.name + self.satellite.name) - class NadirPointTask(Task): - """Task to generate nadir-pointing reference.""" - - name = "nadirPointTask" - - def __init__(self, fsw, priority=98) -> None: # noqa: D107 - """Task to generate nadir-pointing reference.""" - super().__init__(fsw, priority) - - def _create_module_data(self) -> None: - self.hillPoint = self.fsw.hillPoint = hillPoint.hillPoint() - self.hillPoint.ModelTag = "hillPoint" - - def _setup_fsw_objects(self, **kwargs) -> None: - """Configure the nadir-pointing task. - - Args: - kwargs: Passed to other setup functions. - """ - self.hillPoint.transNavInMsg.subscribeTo( - self.fsw.dynamics.simpleNavObject.transOutMsg - ) - self.hillPoint.celBodyInMsg.subscribeTo( - self.fsw.world.ephemConverter.ephemOutMsgs[self.fsw.world.body_index] - ) - messaging.AttRefMsg_C_addAuthor( - self.hillPoint.attRefOutMsg, self.fsw.attRefMsg - ) - - self._add_model_to_task(self.hillPoint, priority=1199) - class RWDesatTask(Task): """Task to desaturate reaction wheels.""" diff --git a/tests/integration/test_int_rso_env.py b/tests/integration/test_int_rso_env.py index 48057748..14a5cfe9 100644 --- a/tests/integration/test_int_rso_env.py +++ b/tests/integration/test_int_rso_env.py @@ -15,7 +15,7 @@ class RSOSat(sats.Satellite): ] action_spec = [act.Drift(duration=1e9)] dyn_type = dyn.RSODynModel - fsw_type = fsw.BasicFSWModel + fsw_type = fsw.BaseFSWModel class InspectorSat(sats.Satellite): From c9bdaa23abee84a6add6618bbd0fa1a30de2a8d7 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 4 Nov 2025 23:56:44 -0700 Subject: [PATCH 7/8] [#156] Separate atmospheric dynamics models --- src/bsk_rl/sim/dyn/__init__.py | 4 + src/bsk_rl/sim/dyn/base.py | 147 +++++++++++++++++----------- tests/unittest/sim/test_dynamics.py | 6 +- 3 files changed, 99 insertions(+), 58 deletions(-) diff --git a/src/bsk_rl/sim/dyn/__init__.py b/src/bsk_rl/sim/dyn/__init__.py index 067b5562..d2dab357 100644 --- a/src/bsk_rl/sim/dyn/__init__.py +++ b/src/bsk_rl/sim/dyn/__init__.py @@ -39,8 +39,10 @@ from deprecated import deprecated from bsk_rl.sim.dyn.base import ( + AtmosphericDragDynModel, BaseDynamicsModel, BasicDynamicsModel, + DisturbanceTorqueDynModel, DynamicsModel, EclipseDynModel, ) @@ -74,6 +76,8 @@ def __init__(self, *args, **kwargs) -> None: "BaseDynamicsModel", "BasicDynamicsModel", "EclipseDynModel", + "DisturbanceTorqueDynModel", + "AtmosphericDragDynModel", "LOSCommDynModel", "ImagingDynModel", "ContinuousImagingDynModel", diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index 88ee235f..da3969ab 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -395,67 +395,16 @@ def setup_eclipse_object(self) -> None: self.eclipse_index = len(self.world.eclipseObject.eclipseOutMsgs) - 1 -class BasicDynamicsModel(EclipseDynModel, BaseDynamicsModel): - """Basic Dynamics model with minimum necessary Basilisk components.""" +class DisturbanceTorqueDynModel(BaseDynamicsModel): + """Dynamics model with constant disturbance torque.""" def __init__(self, *args, **kwargs) -> None: - """A dynamics model with a basic feature set. - - Includes the following: - - * Spacecraft hub physical properties - * Gravity - * Constant disturbance torque (defaults to none) - * Aerodynamic drag - * Eclipse checking for power generation - * Reaction wheels - * Momentum desaturation thrusters - * Solar panels, battery, and power system - - Args: - *args: Passed to superclass - **kwargs: Passed to superclass - """ + """Dynamics model with constant disturbance torque.""" super().__init__(*args, **kwargs) - @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: - return [ - world.AtmosphereWorldModel, - ] + super()._requires_world() - - @property - def battery_charge(self): - """Battery charge [W*s].""" - return self.powerMonitor.batPowerOutMsg.read().storageLevel - - @property - def battery_charge_fraction(self): - """Battery charge as a fraction of capacity.""" - return self.battery_charge / self.powerMonitor.storageCapacity - - @property - def wheel_speeds(self): - """Wheel speeds [rad/s].""" - return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds)[0:3] - - @property - def wheel_speeds_fraction(self): - """Wheel speeds normalized by maximum allowable speed.""" - return self.wheel_speeds / (self.maxWheelSpeed * macros.rpm2radsec) - def _setup_dynamics_objects(self, **kwargs) -> None: super()._setup_dynamics_objects(**kwargs) self.setup_disturbance_torque(**kwargs) - self.setup_density_model() - self.setup_drag_effector(**kwargs) - self.setup_reaction_wheel_dyn_effector(**kwargs) - self.setup_thruster_dyn_effector() - self.setup_solar_panel(**kwargs) - self.setup_battery(**kwargs) - self.setup_power_sink(**kwargs) - self.setup_reaction_wheel_power(**kwargs) - self.setup_thruster_power(**kwargs) @default_args(disturbance_vector=None) def setup_disturbance_torque( @@ -474,11 +423,33 @@ def setup_disturbance_torque( self.extForceTorqueObject.extTorquePntB_B = disturbance_vector self.scObject.addDynamicEffector(self.extForceTorqueObject) + +class AtmosphericDragDynModel(BaseDynamicsModel): + """Dynamics model with atmospheric drag.""" + + def __init__(self, *args, **kwargs) -> None: + """Dynamics model with atmospheric drag.""" + super().__init__(*args, **kwargs) + + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return [ + world.AtmosphereWorldModel, + ] + super()._requires_world() + + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_density_model() + self.setup_drag_effector(**kwargs) + def setup_density_model(self) -> None: """Set up the atmospheric density model effector.""" self.world.densityModel.addSpacecraftToModel(self.scObject.scStateOutMsg) - @default_args(dragCoeff=2.2) + @default_args( + dragCoeff=2.2, + panelArea=2 * 1.0 * 0.5, + ) def setup_drag_effector( self, width: float, @@ -547,6 +518,69 @@ def setup_drag_effector( self.task_name, self.dragEffector, ModelPriority=priority ) + +class BasicDynamicsModel( + EclipseDynModel, + DisturbanceTorqueDynModel, + AtmosphericDragDynModel, + BaseDynamicsModel, +): + """Basic Dynamics model with minimum necessary Basilisk components.""" + + def __init__(self, *args, **kwargs) -> None: + """A dynamics model with a basic feature set. + + Includes the following: + + * Spacecraft hub physical properties + * Gravity + * Constant disturbance torque (defaults to none) + * Aerodynamic drag + * Eclipse checking for power generation + * Reaction wheels + * Momentum desaturation thrusters + * Solar panels, battery, and power system + + Args: + *args: Passed to superclass + **kwargs: Passed to superclass + """ + super().__init__(*args, **kwargs) + + @classmethod + def _requires_world(cls) -> list[type["WorldModel"]]: + return super()._requires_world() + + @property + def battery_charge(self): + """Battery charge [W*s].""" + return self.powerMonitor.batPowerOutMsg.read().storageLevel + + @property + def battery_charge_fraction(self): + """Battery charge as a fraction of capacity.""" + return self.battery_charge / self.powerMonitor.storageCapacity + + @property + def wheel_speeds(self): + """Wheel speeds [rad/s].""" + return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds)[0:3] + + @property + def wheel_speeds_fraction(self): + """Wheel speeds normalized by maximum allowable speed.""" + return self.wheel_speeds / (self.maxWheelSpeed * macros.rpm2radsec) + + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_reaction_wheel_dyn_effector(**kwargs) + self.setup_thruster_dyn_effector() + self.setup_solar_panel(**kwargs) + self.setup_battery(**kwargs) + self.setup_power_sink(**kwargs) + self.setup_reaction_wheel_power(**kwargs) + self.setup_thruster_power(**kwargs) + @default_args( wheelSpeeds=lambda: np.random.uniform(-1500, 1500, 3), maxWheelSpeed=np.inf, @@ -779,4 +813,7 @@ def setup_reaction_wheel_power( "DynamicsModel", "BaseDynamicsModel", "BasicDynamicsModel", + "EclipseDynModel", + "DisturbanceTorqueDynModel", + "AtmosphericDragDynModel", ] diff --git a/tests/unittest/sim/test_dynamics.py b/tests/unittest/sim/test_dynamics.py index 9d71641c..55e1542f 100644 --- a/tests/unittest/sim/test_dynamics.py +++ b/tests/unittest/sim/test_dynamics.py @@ -38,7 +38,6 @@ def test_is_alive(self): @patch(basicdyn + "_requires_world", MagicMock(return_value=[])) @patch(basicdyn + "setup_spacecraft_hub") -@patch(basicdyn + "setup_drag_effector") @patch(basicdyn + "setup_reaction_wheel_dyn_effector") @patch(basicdyn + "setup_thruster_dyn_effector") @patch(basicdyn + "setup_simple_nav_object") @@ -48,8 +47,9 @@ def test_is_alive(self): @patch(basicdyn + "setup_power_sink") @patch(basicdyn + "setup_reaction_wheel_power") @patch(basicdyn + "setup_thruster_power") -@patch(basicdyn + "setup_disturbance_torque") -@patch(basicdyn + "setup_density_model") +@patch(module + "DisturbanceTorqueDynModel." + "setup_disturbance_torque") +@patch(module + "AtmosphericDragDynModel." + "setup_density_model") +@patch(module + "AtmosphericDragDynModel." + "setup_drag_effector") def test_basic_setup_objects(self, *args): BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: From 1b7883ec32852b25e6bbfd31071a7795201777a2 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 30 Dec 2025 11:27:43 -0700 Subject: [PATCH 8/8] [#323] Restructure dyn and fsw model inheritance --- docs/source/release_notes.rst | 11 +++---- examples/rso_inspection.ipynb | 2 +- src/bsk_rl/act/__init__.py | 2 +- src/bsk_rl/act/discrete_actions.py | 2 +- src/bsk_rl/gym.py | 14 +++++---- src/bsk_rl/sats/satellite.py | 26 ++++++++--------- src/bsk_rl/sim/dyn/__init__.py | 4 +-- src/bsk_rl/sim/dyn/base.py | 34 +++++++++++----------- src/bsk_rl/sim/dyn/ground_imaging.py | 4 +-- src/bsk_rl/sim/dyn/relative_motion.py | 8 ++--- src/bsk_rl/sim/dyn/rso_inspection.py | 4 +-- src/bsk_rl/sim/fsw/__init__.py | 4 +-- src/bsk_rl/sim/fsw/base.py | 32 ++++++++++---------- src/bsk_rl/sim/fsw/ground_imaging.py | 4 +-- src/bsk_rl/sim/fsw/orbital.py | 8 ++--- src/bsk_rl/sim/simulator.py | 8 ++--- src/bsk_rl/sim/world.py | 16 +++++----- tests/integration/sim/test_int_dynamics.py | 8 ++--- tests/integration/test_int_rso_env.py | 2 +- tests/unittest/sim/test_dynamics.py | 12 ++++---- tests/unittest/sim/test_fsw.py | 8 ++--- tests/unittest/sim/test_world.py | 32 ++++++++++---------- 22 files changed, 124 insertions(+), 121 deletions(-) diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index 8a6b57fe..c060a6ff 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -20,10 +20,11 @@ Development - |version| * Allow flight software and dynamics models to be specified as lists of classes. This allows for multiple inheritance to be used for easily creating more complex satellite models. * The inheritance structure of flight software and dynamics models has changed. Most models - now inherit from :class:`~bsk_rl.sim.fsw.BaseFSWModel` or :class:`~bsk_rl.sim.dyn.BaseDynModel` - instead of :class:`~bsk_rl.sim.fsw.BasicFSWModel` or :class:`~bsk_rl.sim.dyn.BasicDynModel`. - These are lighter-weight base classes that lack some functionality that was not always - wanted. + now inherit from :class:`~bsk_rl.sim.fsw.FSWModel` or :class:`~bsk_rl.sim.dyn.DynModel`, + which are instantiable versions of the abstract bases :class:`~bsk_rl.sim.fsw.FSWModelABC` and + :class:`~bsk_rl.sim.dyn.DynModelABC`, instead of from :class:`~bsk_rl.sim.fsw.BasicFSWModel` + or :class:`~bsk_rl.sim.dyn.BasicDynModel`. These are lighter-weight base classes that lack + some functionality that was not always wanted. .. warning:: @@ -31,7 +32,7 @@ Development - |version| :class:`~bsk_rl.sim.fsw.BasicFSWModel` and :class:`~bsk_rl.sim.dyn.BasicDynModel` to your ``fsw_type`` and ``dyn_type`` lists in your satellite classes. -* :class:`~bsk_rl.sim.fsw.BaseFSWModel` implements direct actuator-less attitude control. +* :class:`~bsk_rl.sim.fsw.FSWModel` implements direct actuator-less attitude control. By inheriting from a FSW class that overrides the ``MRPControlTask``, such as :class:`~bsk_rl.sim.fsw.BasicFSWModel` or :class:`~bsk_rl.sim.fsw.SteeringFSWModel`, users can implement custom attitude control strategies. diff --git a/examples/rso_inspection.ipynb b/examples/rso_inspection.ipynb index 187cdabc..bf6fda3b 100644 --- a/examples/rso_inspection.ipynb +++ b/examples/rso_inspection.ipynb @@ -76,7 +76,7 @@ " ]\n", " action_spec = [act.NadirPoint(duration=1e9)]\n", " dyn_type = (dyn.ConjunctionDynModel, dyn.RSODynModel)\n", - " fsw_type = fsw.BaseFSWModel\n" + " fsw_type = fsw.FSWModel\n" ] }, { diff --git a/src/bsk_rl/act/__init__.py b/src/bsk_rl/act/__init__.py index ac931ebc..b4949606 100644 --- a/src/bsk_rl/act/__init__.py +++ b/src/bsk_rl/act/__init__.py @@ -24,7 +24,7 @@ class MyActionSatellite(Satellite): +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ | **Action** |**Count**| **Description** | +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`DiscreteFSWAction` | 1 | Call an arbitrary ``@action`` decorated function in the :class:`~bsk_rl.sim.fsw.FSWModel`. | +| :class:`DiscreteFSWAction` | 1 | Call an arbitrary ``@action`` decorated function in the :class:`~bsk_rl.sim.fsw.FSWModelABC`. | +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ | :class:`Charge` | 1 | Point the solar panels at the sun. | +----------------------------+---------+-------------------------------------------------------------------------------------------------------+ diff --git a/src/bsk_rl/act/discrete_actions.py b/src/bsk_rl/act/discrete_actions.py index 8baf4532..6cd15a8e 100644 --- a/src/bsk_rl/act/discrete_actions.py +++ b/src/bsk_rl/act/discrete_actions.py @@ -120,7 +120,7 @@ def __init__( ): """Discrete action to task a flight software action function. - This action executes a function of a :class:`~bsk_rl.sim.fsw.FSWModel` + This action executes a function of a :class:`~bsk_rl.sim.fsw.FSWModelABC` instance that takes no arguments, typically decorated with ``@action``. Args: diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index 6549b2c2..c29ef792 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -17,7 +17,7 @@ from bsk_rl.sats import Satellite from bsk_rl.scene import Scenario from bsk_rl.sim import Simulator -from bsk_rl.sim.world import BaseWorldModel, WorldModel +from bsk_rl.sim.world import WorldModel, WorldModelABC from bsk_rl.utils import functional, logging_config, vizard logger = logging.getLogger(__name__) @@ -57,7 +57,7 @@ def __init__( scenario: Optional[Scenario] = None, rewarder: Optional[Union[GlobalReward, list[GlobalReward]]] = None, world_type: Optional[ - Union[type[WorldModel], tuple[type[WorldModel], ...]] + Union[type[WorldModelABC], tuple[type[WorldModelABC], ...]] ] = None, world_args: Optional[dict[str, Any]] = None, communicator: Optional[CommunicationMethod] = None, @@ -99,7 +99,7 @@ def __init__( be a function that takes a list of satellites and returns a dictionary that maps satellites to dictionaries of satellite model arguments to be overridden. world_type: Type or tuple of types of Basilisk world model to be constructed. - world_args: Arguments for :class:`~bsk_rl.sim.world.WorldModel` construction. + world_args: Arguments for :class:`~bsk_rl.sim.world.WorldModelABC` construction. Should be in the form of a dictionary with keys corresponding to the arguments of the constructor and values that are either the desired value or a function that takes no arguments and returns a randomized value. @@ -184,7 +184,9 @@ def __init__( ) if not isinstance(world_type, (list, tuple)): world_type = (world_type,) - world_type = functional.compose_types(*world_type, WorldModel, name="World") + world_type = functional.compose_types( + *world_type, WorldModelABC, name="World" + ) self.world_type = world_type if world_args is None: world_args = self.world_type.default_world_args() @@ -223,7 +225,7 @@ def __init__( self.render_mode = render_mode self.generate_obs_retasking_only = generate_obs_retasking_only - def _minimum_world_model(self) -> type[WorldModel]: + def _minimum_world_model(self) -> type[WorldModelABC]: """Determine the minimum world model required by the satellites.""" world_types = set( sum( @@ -231,7 +233,7 @@ def _minimum_world_model(self) -> type[WorldModel]: [], ) ) - return functional.compose_types(*world_types, BaseWorldModel, name="World") + return functional.compose_types(*world_types, WorldModel, name="World") def get_satellite(self, name: str) -> "Satellite": """Get a satellite by name. diff --git a/src/bsk_rl/sats/satellite.py b/src/bsk_rl/sats/satellite.py index 23eb14c3..7b032101 100644 --- a/src/bsk_rl/sats/satellite.py +++ b/src/bsk_rl/sats/satellite.py @@ -40,9 +40,9 @@ class Satellite(ABC, Resetable): """Abstract base class for satellites.""" dyn_type: Union[ - type["dyn.DynamicsModel"], tuple[type["dyn.DynamicsModel"], ...] + type["dyn.DynamicsModelABC"], tuple[type["dyn.DynamicsModelABC"], ...] ] = AbstractClassProperty() - fsw_type: Union[type["fsw.FSWModel"], tuple[type["fsw.FSWModel"], ...]] = ( + fsw_type: Union[type["fsw.FSWModelABC"], tuple[type["fsw.FSWModelABC"], ...]] = ( AbstractClassProperty() ) observation_spec: list["Observation"] = AbstractClassProperty() @@ -51,7 +51,7 @@ class Satellite(ABC, Resetable): _dyn_type = None @classmethod - def get_dyn_type(cls) -> type["dyn.DynamicsModel"]: + def get_dyn_type(cls) -> type["dyn.DynamicsModelABC"]: """Get the dynamics model type for the satellite. This should be used in class methods instead of referencing ``dyn_type``. In @@ -65,13 +65,13 @@ def get_dyn_type(cls) -> type["dyn.DynamicsModel"]: dyn_types = cls.dyn_type else: dyn_types = (cls.dyn_type,) - cls._dyn_type = compose_types(*dyn_types, dyn.BaseDynamicsModel, name="Dyn") + cls._dyn_type = compose_types(*dyn_types, dyn.DynamicsModel, name="Dyn") return cls._dyn_type _fsw_type = None @classmethod - def get_fsw_type(cls) -> type["fsw.FSWModel"]: + def get_fsw_type(cls) -> type["fsw.FSWModelABC"]: """Get the flight software model type for the satellite. This should be used in class methods instead of referencing ``fsw_type``. In @@ -85,12 +85,12 @@ def get_fsw_type(cls) -> type["fsw.FSWModel"]: fsw_types = cls.fsw_type else: fsw_types = (cls.fsw_type,) - cls._fsw_type = compose_types(*fsw_types, fsw.BaseFSWModel, name="FSW") + cls._fsw_type = compose_types(*fsw_types, fsw.FSWModel, name="FSW") return cls._fsw_type @classmethod def default_sat_args(cls, **kwargs) -> dict[str, Any]: - """Compile default arguments for :class:`~bsk_rl.sim.dyn.DynamicsModel` and :class:`~bsk_rl.sim.fsw.FSWModel`, replacing those specified. + """Compile default arguments for :class:`~bsk_rl.sim.dyn.DynamicsModelABC` and :class:`~bsk_rl.sim.fsw.FSWModelABC`, replacing those specified. Args: **kwargs: Arguments to override in the default arguments. @@ -129,8 +129,8 @@ def __init__( Args: name: Identifier for satellite; does not need to be unique. - sat_args: Arguments for :class:`~bsk_rl.sim.dyn.DynamicsModel` and - :class:`~bsk_rl.sim.fsw.FSWModel` construction. Should be in the form of + sat_args: Arguments for :class:`~bsk_rl.sim.dyn.DynamicsModelABC` and + :class:`~bsk_rl.sim.fsw.FSWModelABC` construction. Should be in the form of a dictionary with keys corresponding to the arguments of the constructor and values that are either the desired value or a function that takes no arguments and returns a randomized value. @@ -147,8 +147,8 @@ def __init__( sat_args = self.default_sat_args() self.sat_args_generator = self.default_sat_args(**sat_args) self.simulator: "Simulator" - self.fsw: "fsw.FSWModel" - self.dynamics: "dyn.DynamicsModel" + self.fsw: "fsw.FSWModelABC" + self.dynamics: "dyn.DynamicsModelABC" self.data_store: "DataStore" self.requires_retasking: bool self.variable_interval = variable_interval @@ -228,7 +228,7 @@ def set_simulator(self, simulator: "Simulator"): """ self.simulator = proxy(simulator) - def set_dynamics(self, dyn_rate: float) -> "dyn.DynamicsModel": + def set_dynamics(self, dyn_rate: float) -> "dyn.DynamicsModelABC": """Create dynamics model; called during simulator initialization. Args: @@ -243,7 +243,7 @@ def set_dynamics(self, dyn_rate: float) -> "dyn.DynamicsModel": self.dynamics = proxy(dynamics) return dynamics - def set_fsw(self, fsw_rate: float) -> "fsw.FSWModel": + def set_fsw(self, fsw_rate: float) -> "fsw.FSWModelABC": """Create flight software model; called during simulator initialization. Args: diff --git a/src/bsk_rl/sim/dyn/__init__.py b/src/bsk_rl/sim/dyn/__init__.py index d2dab357..3432ab96 100644 --- a/src/bsk_rl/sim/dyn/__init__.py +++ b/src/bsk_rl/sim/dyn/__init__.py @@ -40,10 +40,10 @@ from bsk_rl.sim.dyn.base import ( AtmosphericDragDynModel, - BaseDynamicsModel, BasicDynamicsModel, DisturbanceTorqueDynModel, DynamicsModel, + DynamicsModelABC, EclipseDynModel, ) from bsk_rl.sim.dyn.ground_imaging import ( @@ -72,8 +72,8 @@ def __init__(self, *args, **kwargs) -> None: __doc_title__ = "Dynamics Sims" __all__ = [ + "DynamicsModelABC", "DynamicsModel", - "BaseDynamicsModel", "BasicDynamicsModel", "EclipseDynModel", "DisturbanceTorqueDynModel", diff --git a/src/bsk_rl/sim/dyn/base.py b/src/bsk_rl/sim/dyn/base.py index da3969ab..96806644 100644 --- a/src/bsk_rl/sim/dyn/base.py +++ b/src/bsk_rl/sim/dyn/base.py @@ -35,15 +35,15 @@ if TYPE_CHECKING: # pragma: no cover from bsk_rl.sats import Satellite from bsk_rl.sim import Simulator - from bsk_rl.sim.world import WorldModel + from bsk_rl.sim.world import WorldModelABC -class DynamicsModel(ABC, Resetable): +class DynamicsModelABC(ABC, Resetable): """Abstract Basilisk dynamics model.""" @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: - """Define minimum :class:`~bsk_rl.sim.world.WorldModel` for compatibility.""" + def _requires_world(cls) -> list[type["WorldModelABC"]]: + """Define minimum :class:`~bsk_rl.sim.world.WorldModelABC` for compatibility.""" return [] def __init__( @@ -55,7 +55,7 @@ def __init__( ) -> None: """The abstract base dynamics model. - One DynamicsModel is instantiated for each satellite in the environment each + One DynamicsModelABC is instantiated for each satellite in the environment each time the environment is reset and new simulator is created. Args: @@ -92,7 +92,7 @@ def simulator(self) -> "Simulator": return self.satellite.simulator @property - def world(self) -> "WorldModel": + def world(self) -> "WorldModelABC": """Reference to the episode world model.""" return self.simulator.world @@ -123,9 +123,9 @@ def __del__(self): self.logger.debug("Basilisk dynamics deleted") -class BaseDynamicsModel(DynamicsModel): +class DynamicsModel(DynamicsModelABC): @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: + def _requires_world(cls) -> list[type["WorldModelABC"]]: return [] @property @@ -344,7 +344,7 @@ def setup_spacecraft_hub( self.setup_gravity_bodies() def setup_gravity_bodies(self) -> None: - """Set up gravitational bodies from the :class:`~bsk_rl.sim.world.WorldModel` to included in the simulation.""" + """Set up gravitational bodies from the :class:`~bsk_rl.sim.world.WorldModelABC` to included in the simulation.""" self.scObject.gravField.gravBodies = spacecraft.GravBodyVector( list(self.world.gravFactory.gravBodies.values()) ) @@ -372,7 +372,7 @@ def altitude_valid(self) -> bool: return np.linalg.norm(self.r_BN_N) > self.min_orbital_radius -class EclipseDynModel(BaseDynamicsModel): +class EclipseDynModel(DynamicsModel): """Dynamics model with eclipse checking.""" def __init__(self, *args, **kwargs) -> None: @@ -380,7 +380,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: + def _requires_world(cls) -> list[type["WorldModelABC"]]: return [ world.EclipseWorldModel, ] + super()._requires_world() @@ -395,7 +395,7 @@ def setup_eclipse_object(self) -> None: self.eclipse_index = len(self.world.eclipseObject.eclipseOutMsgs) - 1 -class DisturbanceTorqueDynModel(BaseDynamicsModel): +class DisturbanceTorqueDynModel(DynamicsModel): """Dynamics model with constant disturbance torque.""" def __init__(self, *args, **kwargs) -> None: @@ -424,7 +424,7 @@ def setup_disturbance_torque( self.scObject.addDynamicEffector(self.extForceTorqueObject) -class AtmosphericDragDynModel(BaseDynamicsModel): +class AtmosphericDragDynModel(DynamicsModel): """Dynamics model with atmospheric drag.""" def __init__(self, *args, **kwargs) -> None: @@ -432,7 +432,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: + def _requires_world(cls) -> list[type["WorldModelABC"]]: return [ world.AtmosphereWorldModel, ] + super()._requires_world() @@ -523,7 +523,7 @@ class BasicDynamicsModel( EclipseDynModel, DisturbanceTorqueDynModel, AtmosphericDragDynModel, - BaseDynamicsModel, + DynamicsModel, ): """Basic Dynamics model with minimum necessary Basilisk components.""" @@ -548,7 +548,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: + def _requires_world(cls) -> list[type["WorldModelABC"]]: return super()._requires_world() @property @@ -810,8 +810,8 @@ def setup_reaction_wheel_power( __doc_title__ = "Dynamics Base" __all__ = [ + "DynamicsModelABC", "DynamicsModel", - "BaseDynamicsModel", "BasicDynamicsModel", "EclipseDynModel", "DisturbanceTorqueDynModel", diff --git a/src/bsk_rl/sim/dyn/ground_imaging.py b/src/bsk_rl/sim/dyn/ground_imaging.py index 35995f7b..fe17a599 100644 --- a/src/bsk_rl/sim/dyn/ground_imaging.py +++ b/src/bsk_rl/sim/dyn/ground_imaging.py @@ -19,7 +19,7 @@ from bsk_rl.utils.functional import aliveness_checker, default_args if TYPE_CHECKING: # pragma: no cover - from bsk_rl.sim.world import WorldModel + from bsk_rl.sim.world import WorldModelABC class ImagingDynModel(BasicDynamicsModel): @@ -419,7 +419,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @classmethod - def _requires_world(cls) -> list[type["WorldModel"]]: + def _requires_world(cls) -> list[type["WorldModelABC"]]: return super()._requires_world() + [world.GroundStationWorldModel] def _setup_dynamics_objects(self, **kwargs) -> None: diff --git a/src/bsk_rl/sim/dyn/relative_motion.py b/src/bsk_rl/sim/dyn/relative_motion.py index 1342b6f2..d8829862 100644 --- a/src/bsk_rl/sim/dyn/relative_motion.py +++ b/src/bsk_rl/sim/dyn/relative_motion.py @@ -6,11 +6,11 @@ from Basilisk.simulation import spacecraftLocation from Basilisk.utilities import macros -from bsk_rl.sim.dyn import BaseDynamicsModel, DynamicsModel +from bsk_rl.sim.dyn import DynamicsModel, DynamicsModelABC from bsk_rl.utils.functional import aliveness_checker, default_args, valid_func_name -class LOSCommDynModel(BaseDynamicsModel): +class LOSCommDynModel(DynamicsModel): """For evaluating line-of-sight connections between satellites for communication.""" def __init__(self, *args, **kwargs) -> None: @@ -64,7 +64,7 @@ def setup_los_comms( ) -class ConjunctionDynModel(DynamicsModel): +class ConjunctionDynModel(DynamicsModelABC): """For evaluating conjunctions between satellites.""" def __init__(self, *args, **kwargs) -> None: @@ -134,7 +134,7 @@ def side_effect(sim): ) -class MaxRangeDynModel(DynamicsModel): +class MaxRangeDynModel(DynamicsModelABC): """For evaluating a maximum range limitation between satellites.""" def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/dyn/rso_inspection.py b/src/bsk_rl/sim/dyn/rso_inspection.py index 54dfbd5c..bc51a298 100644 --- a/src/bsk_rl/sim/dyn/rso_inspection.py +++ b/src/bsk_rl/sim/dyn/rso_inspection.py @@ -4,10 +4,10 @@ from Basilisk.simulation import spacecraftLocation from Basilisk.utilities import macros -from bsk_rl.sim.dyn import BaseDynamicsModel, ContinuousImagingDynModel, EclipseDynModel +from bsk_rl.sim.dyn import ContinuousImagingDynModel, DynamicsModel, EclipseDynModel -class RSODynModel(EclipseDynModel, BaseDynamicsModel): +class RSODynModel(EclipseDynModel, DynamicsModel): """For an RSO with points targets for observation.""" def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/fsw/__init__.py b/src/bsk_rl/sim/fsw/__init__.py index 764cb670..4426b1ed 100644 --- a/src/bsk_rl/sim/fsw/__init__.py +++ b/src/bsk_rl/sim/fsw/__init__.py @@ -30,9 +30,9 @@ """ from bsk_rl.sim.fsw.base import ( - BaseFSWModel, BasicFSWModel, FSWModel, + FSWModelABC, SteeringFSWModel, Task, action, @@ -48,8 +48,8 @@ __doc_title__ = "FSW Sims" __all__ = [ "action", + "FSWModelABC", "FSWModel", - "BaseFSWModel", "BasicFSWModel", "ImagingFSWModel", "ContinuousImagingFSWModel", diff --git a/src/bsk_rl/sim/fsw/base.py b/src/bsk_rl/sim/fsw/base.py index 463b210b..661016ea 100644 --- a/src/bsk_rl/sim/fsw/base.py +++ b/src/bsk_rl/sim/fsw/base.py @@ -32,8 +32,8 @@ if TYPE_CHECKING: # pragma: no cover from bsk_rl.sats import Satellite from bsk_rl.sim import Simulator - from bsk_rl.sim.dyn.base import DynamicsModel - from bsk_rl.sim.world import WorldModel + from bsk_rl.sim.dyn.base import DynamicsModelABC + from bsk_rl.sim.world import WorldModelABC def action( @@ -69,7 +69,7 @@ class Task(ABC): name: str = AbstractClassProperty() - def __init__(self, fsw: "FSWModel", priority: int) -> None: + def __init__(self, fsw: "FSWModelABC", priority: int) -> None: """Template class for defining FSW processes. Each FSW process has a task associated with it, which handle certain housekeeping @@ -79,7 +79,7 @@ def __init__(self, fsw: "FSWModel", priority: int) -> None: fsw: FSW model task contributes to priority: Task priority """ - self.fsw: "FSWModel" = proxy(fsw) + self.fsw: "FSWModelABC" = proxy(fsw) self.priority = priority def create_task(self) -> None: @@ -121,12 +121,12 @@ def reset_for_action(self) -> None: self.fsw.simulator.disableTask(self.name + self.fsw.satellite.name) -class FSWModel(ABC, Resetable): +class FSWModelABC(ABC, Resetable): """Abstract Basilisk flight software model.""" @classmethod - def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - """Define minimum :class:`DynamicsModel` for compatibility.""" + def _requires_dyn(cls) -> list[type["DynamicsModelABC"]]: + """Define minimum :class:`DynamicsModelABC` for compatibility.""" return [] def __init__( @@ -134,7 +134,7 @@ def __init__( ) -> None: """The abstract base flight software model. - One FSWModel is instantiated for each satellite in the environment each time the + One FSWModelABC is instantiated for each satellite in the environment each time the environment is reset and new simulator is created. Args: @@ -183,12 +183,12 @@ def simulator(self) -> "Simulator": return self.satellite.simulator @property - def world(self) -> "WorldModel": + def world(self) -> "WorldModelABC": """Reference to the episode world model.""" return self.simulator.world @property - def dynamics(self) -> "DynamicsModel": + def dynamics(self) -> "DynamicsModelABC": """Reference to the satellite dynamics model for the episode.""" return self.satellite.dynamics @@ -216,10 +216,10 @@ def __del__(self): self.logger.debug("Basilisk FSW deleted") -class BaseFSWModel(FSWModel): +class FSWModel(FSWModelABC): @classmethod - def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - return super()._requires_dyn() + [dyn.BaseDynamicsModel] + def _requires_dyn(cls) -> list[type["DynamicsModelABC"]]: + return super()._requires_dyn() + [dyn.DynamicsModel] def _set_messages(self) -> None: self._set_config_msgs() @@ -316,11 +316,11 @@ def action_nadir_point(self) -> None: ) -class BasicFSWModel(BaseFSWModel): +class BasicFSWModel(FSWModel): """Basic FSW model with minimum necessary Basilisk components.""" @classmethod - def _requires_dyn(cls) -> list[type["DynamicsModel"]]: + def _requires_dyn(cls) -> list[type["DynamicsModelABC"]]: return [dyn.BasicDynamicsModel] def _make_task_list(self) -> list[Task]: @@ -742,7 +742,7 @@ def reset_for_action(self) -> None: __doc_title__ = "FSW Base" __all__ = [ "action", - "FSWModel", + "FSWModelABC", "Task", "BasicFSWModel", "SteeringFSWModel", diff --git a/src/bsk_rl/sim/fsw/ground_imaging.py b/src/bsk_rl/sim/fsw/ground_imaging.py index 612aecd1..5af1f68c 100644 --- a/src/bsk_rl/sim/fsw/ground_imaging.py +++ b/src/bsk_rl/sim/fsw/ground_imaging.py @@ -17,14 +17,14 @@ from bsk_rl.utils.orbital import rv2HN if TYPE_CHECKING: # pragma: no cover - from bsk_rl.sim.dyn import DynamicsModel + from bsk_rl.sim.dyn import DynamicsModelABC class ImagingFSWModel(BasicFSWModel): """Extend FSW with instrument pointing and triggering control.""" @classmethod - def _requires_dyn(cls) -> list[type["DynamicsModel"]]: + def _requires_dyn(cls) -> list[type["DynamicsModelABC"]]: return super()._requires_dyn() + [dyn.ImagingDynModel] def __init__(self, *args, **kwargs) -> None: diff --git a/src/bsk_rl/sim/fsw/orbital.py b/src/bsk_rl/sim/fsw/orbital.py index d46cb191..91d7c766 100644 --- a/src/bsk_rl/sim/fsw/orbital.py +++ b/src/bsk_rl/sim/fsw/orbital.py @@ -3,16 +3,16 @@ import numpy as np from bsk_rl.sim import dyn -from bsk_rl.sim.fsw import FSWModel, action +from bsk_rl.sim.fsw import FSWModelABC, action from bsk_rl.utils.functional import aliveness_checker, default_args -class MagicOrbitalManeuverFSWModel(FSWModel): +class MagicOrbitalManeuverFSWModel(FSWModelABC): """Model that allows for instantaneous Delta V maneuvers.""" @classmethod - def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - return super()._requires_dyn() + [dyn.BaseDynamicsModel] + def _requires_dyn(cls) -> list[type["DynamicsModelABC"]]: + return super()._requires_dyn() + [dyn.DynamicsModel] def __init__(self, *args, **kwargs) -> None: """Model that allows for instantaneous Delta V maneuvers.""" diff --git a/src/bsk_rl/sim/simulator.py b/src/bsk_rl/sim/simulator.py index e8d9a27c..9a1a74a5 100644 --- a/src/bsk_rl/sim/simulator.py +++ b/src/bsk_rl/sim/simulator.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: # pragma: no cover from bsk_rl.sats import Satellite - from bsk_rl.sim.world import WorldModel + from bsk_rl.sim.world import WorldModelABC logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ class Simulator(SimulationBaseClass.SimBaseClass): def __init__( self, satellites: list["Satellite"], - world_type: type["WorldModel"], + world_type: type["WorldModelABC"], world_args: dict[str, Any], sim_rate: float = 1.0, max_step_duration: float = 600.0, @@ -51,7 +51,7 @@ def __init__( self.logger = logger self.use_simple_earth = False - self.world: WorldModel + self.world: WorldModelABC self._set_world(world_type, world_args) @@ -153,7 +153,7 @@ def make_earth_simple(self, vizInstance=None, vizSupport=None): ) def _set_world( - self, world_type: type["WorldModel"], world_args: dict[str, Any] + self, world_type: type["WorldModelABC"], world_args: dict[str, Any] ) -> None: """Construct the simulator world model. diff --git a/src/bsk_rl/sim/world.py b/src/bsk_rl/sim/world.py index d6c60f38..63437df6 100644 --- a/src/bsk_rl/sim/world.py +++ b/src/bsk_rl/sim/world.py @@ -1,7 +1,7 @@ """Basilisk world models are given in ``bsk_rl.sim.world``. In most cases, the user does not need to specify the world model, as it is inferred from -the requirements of the :class:`~bsk_rl.sim.fsw.FSWModel`. However, the user can specify +the requirements of the :class:`~bsk_rl.sim.fsw.FSWModelABC`. However, the user can specify the world model in the :class:`~bsk_rl.GeneralSatelliteTasking` constructor if desired. Customization of the world model parameters is via the ``world_args`` parameter in the @@ -49,7 +49,7 @@ bsk_path = __path__[0] -class WorldModel(ABC, Resetable): +class WorldModelABC(ABC, Resetable): """Abstract Basilisk world model.""" @classmethod @@ -78,7 +78,7 @@ def __init__( ) -> None: """Abstract Basilisk world model. - One WorldModel is instantiated for the environment each time a new simulator + One WorldModelABC is instantiated for the environment each time a new simulator is created. Args: @@ -110,7 +110,7 @@ def _setup_world_objects(self, **kwargs) -> None: pass -class BaseWorldModel(WorldModel): +class WorldModel(WorldModelABC): """Basic world with minimum necessary Basilisk world components.""" def __init__(self, *args, **kwargs) -> None: @@ -212,7 +212,7 @@ def __del__(self) -> None: pass -class EclipseWorldModel(BaseWorldModel): +class EclipseWorldModel(WorldModel): def __init__(self, *args, **kwargs) -> None: """Model that includes eclipse messages. @@ -248,7 +248,7 @@ def setup_eclipse_object(self, priority: int = 988, **kwargs) -> None: ) -class AtmosphereWorldModel(BaseWorldModel): +class AtmosphereWorldModel(WorldModel): def __init__(self, *args, **kwargs) -> None: """Model that includes an atmosphere. @@ -300,7 +300,7 @@ def setup_atmosphere_density_model( ) -class GroundStationWorldModel(BaseWorldModel): +class GroundStationWorldModel(WorldModel): """Model that includes downlink ground stations.""" def __init__(self, *args, **kwargs) -> None: @@ -428,8 +428,8 @@ def _create_ground_station( __doc_title__ = "World Sims" __all__ = [ + "WorldModelABC", "WorldModel", - "BaseWorldModel", "EclipseWorldModel", "AtmosphereWorldModel", "GroundStationWorldModel", diff --git a/tests/integration/sim/test_int_dynamics.py b/tests/integration/sim/test_int_dynamics.py index 583b3a0c..37f87ad1 100644 --- a/tests/integration/sim/test_int_dynamics.py +++ b/tests/integration/sim/test_int_dynamics.py @@ -108,7 +108,7 @@ class TestConjunctionDynModel: ) def test_conjunction(self, rN1, vN1, collision): class CollisionSat(sats.Satellite): - fsw_type = fsw.BaseFSWModel + fsw_type = fsw.FSWModel dyn_type = dyn.ConjunctionDynModel observation_spec = [obs.Time()] action_spec = [act.Drift()] @@ -177,13 +177,13 @@ class TestMaxRangeDynModel: @pytest.mark.parametrize("fail_chief", [True, False]) def test_max_range(self, rN1, rN2, max_range_violation, fail_chief): class ChiefSat(sats.Satellite): - fsw_type = fsw.BaseFSWModel - dyn_type = dyn.BaseDynamicsModel + fsw_type = fsw.FSWModel + dyn_type = dyn.DynamicsModel observation_spec = [obs.Time()] action_spec = [act.Drift()] class DeputySat(sats.Satellite): - fsw_type = fsw.BaseFSWModel + fsw_type = fsw.FSWModel dyn_type = dyn.MaxRangeDynModel observation_spec = [obs.Time()] action_spec = [act.Drift()] diff --git a/tests/integration/test_int_rso_env.py b/tests/integration/test_int_rso_env.py index 14a5cfe9..8780b7b5 100644 --- a/tests/integration/test_int_rso_env.py +++ b/tests/integration/test_int_rso_env.py @@ -15,7 +15,7 @@ class RSOSat(sats.Satellite): ] action_spec = [act.Drift(duration=1e9)] dyn_type = dyn.RSODynModel - fsw_type = fsw.BaseFSWModel + fsw_type = fsw.FSWModel class InspectorSat(sats.Satellite): diff --git a/tests/unittest/sim/test_dynamics.py b/tests/unittest/sim/test_dynamics.py index 55e1542f..10139a60 100644 --- a/tests/unittest/sim/test_dynamics.py +++ b/tests/unittest/sim/test_dynamics.py @@ -8,7 +8,7 @@ from bsk_rl.sim.dyn import ( BasicDynamicsModel, ContinuousImagingDynModel, - DynamicsModel, + DynamicsModelABC, GroundStationDynModel, ImagingDynModel, LOSCommDynModel, @@ -18,18 +18,18 @@ module = "bsk_rl.sim.dyn." -@patch.multiple(DynamicsModel, __abstractmethods__=set()) -class TestDynamicsModel: +@patch.multiple(DynamicsModelABC, __abstractmethods__=set()) +class TestDynamicsModelABC: def test_base_class(self): sat = MagicMock() - dyn = DynamicsModel(sat, 1.0) + dyn = DynamicsModelABC(sat, 1.0) dyn.simulator.CreateNewProcess.assert_called_once() assert sat.simulator.world == dyn.world dyn.reset_for_action() @patch(module + "base.check_aliveness_checkers", MagicMock(return_value=True)) def test_is_alive(self): - dyn = DynamicsModel(MagicMock(), 1.0) + dyn = DynamicsModelABC(MagicMock(), 1.0) assert dyn.is_alive() @@ -216,7 +216,7 @@ class TestLOSCommDynModel: losdyn = module + "LOSCommDynModel." @patch(losdyn + "_requires_world", MagicMock(return_value=[])) - @patch(module + "BaseDynamicsModel._setup_dynamics_objects", MagicMock()) + @patch(module + "DynamicsModel._setup_dynamics_objects", MagicMock()) @patch(losdyn + "setup_los_comms") def test_setup_objects(self, *args): LOSCommDynModel(MagicMock(simulator=MagicMock()), 1.0) diff --git a/tests/unittest/sim/test_fsw.py b/tests/unittest/sim/test_fsw.py index da4d9b49..37b18bef 100644 --- a/tests/unittest/sim/test_fsw.py +++ b/tests/unittest/sim/test_fsw.py @@ -2,7 +2,7 @@ import numpy as np -from bsk_rl.sim.fsw import BasicFSWModel, FSWModel, ImagingFSWModel, Task, action +from bsk_rl.sim.fsw import BasicFSWModel, FSWModelABC, ImagingFSWModel, Task, action module = "bsk_rl.sim.fsw." @@ -25,18 +25,18 @@ def action_function(self, foo, bar=2): mock_actions.assert_called_once_with(3, bar=4) -@patch.multiple(FSWModel, __abstractmethods__=set()) +@patch.multiple(FSWModelABC, __abstractmethods__=set()) class TestFSWModel: def test_base_class(self): sat = MagicMock() - fsw = FSWModel(sat, 1.0) + fsw = FSWModelABC(sat, 1.0) # fsw.simulator.CreateNewProcess.assert_called_once() assert sat.simulator.world == fsw.world assert sat.dynamics == fsw.dynamics @patch(module + "base.check_aliveness_checkers", MagicMock(return_value=True)) def test_is_alive(self): - fsw = FSWModel(MagicMock(), 1.0) + fsw = FSWModelABC(MagicMock(), 1.0) assert fsw.is_alive() diff --git a/tests/unittest/sim/test_world.py b/tests/unittest/sim/test_world.py index 63182fea..53d8f633 100644 --- a/tests/unittest/sim/test_world.py +++ b/tests/unittest/sim/test_world.py @@ -5,20 +5,20 @@ from bsk_rl.sim.world import ( AtmosphereWorldModel, - BaseWorldModel, EclipseWorldModel, GroundStationWorldModel, WorldModel, + WorldModelABC, ) module = "bsk_rl.sim.world." -class TestWorldModel: +class TestWorldModelABC: @patch(module + "collect_default_args") def test_default_world_args(self, mock_collect): mock_collect.return_value = {"a": 1, "b": 2, "c": 3} - assert WorldModel.default_world_args() == {"a": 1, "b": 2, "c": 3} + assert WorldModelABC.default_world_args() == {"a": 1, "b": 2, "c": 3} @pytest.mark.parametrize( "overwrite,error", [({"c": 4}, False), ({"not_c": 4}, True)] @@ -27,21 +27,21 @@ def test_default_world_args(self, mock_collect): def test_default_sat_args_overwrote(self, mock_collect, overwrite, error): mock_collect.return_value = {"a": 1, "b": 2, "c": 3} if not error: - assert WorldModel.default_world_args(**overwrite) == { + assert WorldModelABC.default_world_args(**overwrite) == { "a": 1, "b": 2, "c": 4, } else: with pytest.raises(KeyError): - WorldModel.default_world_args(**overwrite) + WorldModelABC.default_world_args(**overwrite) - @patch.multiple(WorldModel, __abstractmethods__=set()) - @patch(module + "WorldModel._setup_world_objects") + @patch.multiple(WorldModelABC, __abstractmethods__=set()) + @patch(module + "WorldModelABC._setup_world_objects") def test_init(self, mock_obj_init): mock_sim = MagicMock() kwargs = dict(a=1, b=2) - world = WorldModel(mock_sim, 1.0, **kwargs) + world = WorldModelABC(mock_sim, 1.0, **kwargs) mock_sim.CreateNewProcess.assert_called_once() mock_sim.CreateNewTask.assert_called_once() mock_obj_init.assert_called_once_with(**kwargs) @@ -49,12 +49,12 @@ def test_init(self, mock_obj_init): assert world.simulator == mock_sim -class TestBaseWorldModel: - baseworld = module + "BaseWorldModel." +class TestWorldModel: + baseworld = module + "WorldModel." @patch(baseworld + "__init__", MagicMock(return_value=None)) def test_PN(self): - world = BaseWorldModel(MagicMock(), 1.0) + world = WorldModel(MagicMock(), 1.0) world.gravFactory = MagicMock() world.body_index = 0 msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ @@ -63,7 +63,7 @@ def test_PN(self): @patch(baseworld + "__init__", MagicMock(return_value=None)) def test_omega_PN_N(self): - world = BaseWorldModel(MagicMock(), 1.0) + world = WorldModel(MagicMock(), 1.0) world.gravFactory = MagicMock() world.body_index = 0 msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ @@ -74,7 +74,7 @@ def test_omega_PN_N(self): @patch(baseworld + "setup_gravity_bodies") @patch(baseworld + "setup_ephem_object") def test_setup_and_delete(self, grav_set, epoch_set): - world = BaseWorldModel(MagicMock(), 1.0) + world = WorldModel(MagicMock(), 1.0) for setter in (grav_set, epoch_set): setter.assert_called_once() unload_function = MagicMock() @@ -86,7 +86,7 @@ def test_setup_and_delete(self, grav_set, epoch_set): @patch(module + "simIncludeGravBody", MagicMock()) def testsetup_gravity_bodies(self): # Smoke test - world = BaseWorldModel(MagicMock(), 1.0) + world = WorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.setup_gravity_bodies(utc_init="time") world.simulator.AddModelToTask.assert_called_once() @@ -95,7 +95,7 @@ def testsetup_gravity_bodies(self): @patch(module + "ephemerisConverter", MagicMock()) def testsetup_epoch_object(self): # Smoke test - world = BaseWorldModel(MagicMock(), 1.0) + world = WorldModel(MagicMock(), 1.0) world.simulator = MagicMock() world.gravFactory = MagicMock() world.sun_index = 0 @@ -144,7 +144,7 @@ class TestGroundStationWorldModel: groundworld = module + "GroundStationWorldModel." @patch(groundworld + "setup_ground_locations") - @patch(module + "BaseWorldModel._setup_world_objects", MagicMock()) + @patch(module + "WorldModel._setup_world_objects", MagicMock()) def test_setup_world_objects(self, ground_set): GroundStationWorldModel(MagicMock(), 1.0) ground_set.assert_called_once()