diff --git a/.gitignore b/.gitignore index b4f80a95..3f4616a1 100644 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -# PDFs and Images -*.jpg - # pyenv .python-version diff --git a/docs/source/index.rst b/docs/source/index.rst index 991e72d2..15cd1d1a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,7 +26,31 @@ for working with these environments. A whitepaper on the design philosophy behind BSK-RL and an example use case can be :download:`downloaded here <_static/stephenson_bskrl_2024.pdf>`. -.. youtube:: 8qR-AGrCFQw +Capabilities +------------ + +Earth Observation +^^^^^^^^^^^^^^^^^ + +.. youtube:: lCN0TiNJ1i4 + +.. youtube:: 4orleGCi7n0 + +| + +Spacecraft Inspection +^^^^^^^^^^^^^^^^^^^^^ + +.. youtube:: eQEoTOYADKc + +| + +Space Domain Awareness +^^^^^^^^^^^^^^^^^^^^^^ + +.. youtube:: Aas0z43uS9M + +| Quickstart ---------- diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index e1033052..c5714c64 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -11,6 +11,12 @@ Development - |version| * Add ``bsk`` as a dependency in ``pyproject.toml``. * Update the CI/CD workflows to build BSK-RL using the new ``bsk`` dependency. * Optimize performance of AEOS environments, especially for high request counts. +* Allow for the Vizard output path to be specified as a .bin file instead of just a directory. +* Use Vizard 2.3.1 locations for visualization; results in significantly smaller output + files. +* 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. Version 1.2.0 diff --git a/examples/fault_environment.ipynb b/examples/fault_environment.ipynb index 2b088c93..15923e9f 100644 --- a/examples/fault_environment.ipynb +++ b/examples/fault_environment.ipynb @@ -69,9 +69,9 @@ " satellite.simulator.createNewEvent(\n", " f\"add{self.name}Fault\",\n", " satellite.dynamics.dyn_rate,\n", - " True,\n", - " [f\"self.TotalSim.CurrentNanos>={self.time}\"],\n", - " [\n", + " eventActive=True,\n", + " conditionTime=self.time,\n", + " actionList=[\n", " f\"self.faultList[{self.uniqueFaultIdx}].execute({satellite._satellite_command})\",\n", " f\"self.faultList[{self.uniqueFaultIdx}].print({satellite._satellite_command})\",\n", " ],\n", @@ -114,9 +114,9 @@ "\n", " def print(self, satellite):\n", " if self.wheelIdx == \"all\":\n", - " self.message = f\"RW Power Fault: all RW's power limit reduced to {self.reducedLimit} Watts at {self.time*macros.NANO2MIN} minutes!\"\n", + " self.message = f\"RW Power Fault: all RW's power limit reduced to {self.reducedLimit} Watts at {self.time * macros.NANO2MIN} minutes!\"\n", " else:\n", - " self.message = f\"RW Power Fault: RW{self.wheelIdx}'s power limit reduced to {self.reducedLimit} Watts at {self.time*macros.NANO2MIN} minutes!\"\n", + " self.message = f\"RW Power Fault: RW{self.wheelIdx}'s power limit reduced to {self.reducedLimit} Watts at {self.time * macros.NANO2MIN} minutes!\"\n", " super().print_message(self.message, satellite)" ] }, @@ -135,7 +135,6 @@ "outputs": [], "source": [ "class CustomDynModel(dyn.FullFeaturedDynModel):\n", - "\n", " @property\n", " def solar_angle_norm(self) -> float:\n", " sun_vec_N = (\n", @@ -283,8 +282,8 @@ " for i in range(4):\n", " rwConfigElementMsg = messaging.RWConfigElementMsgPayload()\n", " rwConfigElementMsg.gsHat_B = self.dynamics.Gs[:, i]\n", - " rwConfigElementMsg.Js = self.dynamics.rwFactory.rwList[f\"RW{i+1}\"].Js\n", - " rwConfigElementMsg.uMax = self.dynamics.rwFactory.rwList[f\"RW{i+1}\"].u_max\n", + " rwConfigElementMsg.Js = self.dynamics.rwFactory.rwList[f\"RW{i + 1}\"].Js\n", + " rwConfigElementMsg.uMax = self.dynamics.rwFactory.rwList[f\"RW{i + 1}\"].u_max\n", " rwConfigElementList.append(rwConfigElementMsg)\n", " rwConstellationConfig.reactionWheels = rwConfigElementList\n", " self.rwConstellationConfigInMsg = messaging.RWConstellationMsg().write(\n", @@ -544,7 +543,6 @@ "source": [ "total_reward = 0.0\n", "while True:\n", - "\n", " observation, reward, terminated, truncated, info = env.step(\n", " env.action_space.sample()\n", " )\n", @@ -558,6 +556,11 @@ } ], "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, "language_info": { "codemirror_mode": { "name": "ipython", @@ -568,7 +571,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.12" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/src/bsk_rl/_dat/world.200407.3x5400x2700.jpg b/src/bsk_rl/_dat/world.200407.3x5400x2700.jpg new file mode 100644 index 00000000..0a5c0bf8 Binary files /dev/null and b/src/bsk_rl/_dat/world.200407.3x5400x2700.jpg differ diff --git a/src/bsk_rl/data/rso_inspection.py b/src/bsk_rl/data/rso_inspection.py index 3e2f8473..ed4e6cc7 100644 --- a/src/bsk_rl/data/rso_inspection.py +++ b/src/bsk_rl/data/rso_inspection.py @@ -141,7 +141,7 @@ def get_log_state(self) -> Optional[tuple[list[list[bool]], list[list[bool]]]]: return inspected_logs, illuminated_logs def compare_log_states(self, _, logs) -> RSOInspectionData: - """Identify point status and update their colors in Vizard.""" + """Identify point status.""" if self.role == RSO: return RSOInspectionData() @@ -161,28 +161,6 @@ def compare_log_states(self, _, logs) -> RSOInspectionData: if any(log): point_illuminate_status[rso_point] = True - self.update_point_colors( - self.data.point_illuminate_status.keys(), - color="gray", - ) - self.update_point_colors( - [ - rso_point - for rso_point in point_illuminate_status - if point_illuminate_status[rso_point] - ], - color="yellow", - ) - self.update_point_colors( - [ - rso_point - for rso_point in point_inspect_status - if point_inspect_status[rso_point] - ], - color="chartreuse", - permanent=True, - ) - if len(point_inspect_status) > 0: self.satellite.logger.info( f"Inspected {len(point_inspect_status)} points this step" @@ -190,32 +168,6 @@ def compare_log_states(self, _, logs) -> RSOInspectionData: return RSOInspectionData(point_inspect_status, point_illuminate_status) - @vizard.visualize - def update_point_colors( - self, - rso_points, - color, - alpha=0.5, - permanent=False, - vizInstance=None, - vizSupport=None, - ): - """Update target colors in Vizard.""" - if not hasattr(self, "permanent_point_colors"): - self.permanent_point_colors = [] - - for location in vizInstance.locations: - if ( - location.stationName not in self.permanent_point_colors - and location.stationName in [str(point) for point in rso_points] - ): - if not all( - np.equal(location.color, vizSupport.toRGBA255(color, alpha=alpha)) - ): - location.color = vizSupport.toRGBA255(color, alpha=alpha) - if permanent: - self.permanent_point_colors.append(location.stationName) - class RSOInspectionReward(GlobalReward): data_store_type = RSOInspectionDataStore @@ -286,6 +238,43 @@ def initial_data(self, satellite: Satellite) -> RSOInspectionData: {point: False for point in self.scenario.rso_points}, ) + @vizard.visualize + def determine_point_colors(self, total_data, new_data_dict): + """Determine target colors in Vizard.""" + colors = ["grey"] * len(self.scenario.rso_points) + for i, point in enumerate(self.scenario.rso_points): + if any( + [ + data.point_illuminate_status.get(point, False) + for data in new_data_dict.values() + ] + ): + colors[i] = "yellow" + if total_data.point_inspect_status.get(point, False): + colors[i] = "chartreuse" + + return colors + + @vizard.visualize + def update_point_colors( + self, + rso_points, + colors, + alpha=0.5, + vizInstance=None, + vizSupport=None, + ): + """Update target colors in Vizard.""" + if not hasattr(self, "prev_colors"): + self.prev_colors = [None] * len(colors) + + for point, color, prev_color in zip(rso_points, colors, self.prev_colors): + color_vec = vizSupport.toRGBA255(color, alpha=alpha) + if prev_color != color: + vizSupport.changeLocation(vizInstance, str(point), color=color_vec) + + self.prev_colors = colors + def calculate_reward( self, new_data_dict: dict[str, RSOInspectionData] ) -> dict[str, float]: @@ -307,6 +296,10 @@ def calculate_reward( ) total_data += data + # Plot inspection status in Vizard + colors = self.determine_point_colors(total_data, new_data_dict) + self.update_point_colors(self.scenario.rso_points, colors) + # Check for completion bonus logger.info( f"Inspected/Illuminated/Total: {total_data.num_points_inspected}/{total_data.num_points_illuminated}/{total_points}" diff --git a/src/bsk_rl/data/unique_image_data.py b/src/bsk_rl/data/unique_image_data.py index 8f0fe3e2..9e8cc68b 100644 --- a/src/bsk_rl/data/unique_image_data.py +++ b/src/bsk_rl/data/unique_image_data.py @@ -109,9 +109,12 @@ def compare_log_states( @vizard.visualize def update_target_colors(self, targets, vizInstance=None, vizSupport=None): """Update target colors in Vizard.""" - for location in vizInstance.locations: - if location.stationName in [target.name for target in targets]: - location.color = vizSupport.toRGBA255(self.satellite.vizard_color) + for target in targets: + vizSupport.changeLocation( + vizInstance, + target.name, + color=vizSupport.toRGBA255(self.satellite.vizard_color), + ) class UniqueImageReward(GlobalReward): diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index 8ab9f425..9a673aa7 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -123,17 +123,19 @@ def __init__( modules will be imported. vizard_settings: Settings for Vizard visualization. Set in ``vizIstance.settings``. Additionally, the key ``vizard_rate`` can be set to the rate at which Vizard updates. - Valid setting can be found `here `_. + The key ``use_simple_earth`` can be set to use a lower detail Earth shader + that may help viewing ground locations. Other settings can be found + `in the Basilisk documentation `_. render_mode: Unused. """ self.seed = None self._configure_logging(log_level, log_dir) if vizard_dir is not None: vizard.VIZARD_PATH = vizard_dir - if vizard_settings is not None: - logger.warning( - "Vizard settings provided but Vizard is not enabled. Ignoring settings." - ) + if vizard_settings is not None and vizard_dir is None: + logger.warning( + "Vizard settings provided but Vizard is not enabled. Ignoring settings." + ) self.vizard_settings = vizard_settings if vizard_settings is not None else {} if isinstance(satellites, Satellite): diff --git a/src/bsk_rl/sats/satellite.py b/src/bsk_rl/sats/satellite.py index 0c5f827c..376ad112 100644 --- a/src/bsk_rl/sats/satellite.py +++ b/src/bsk_rl/sats/satellite.py @@ -362,7 +362,7 @@ def side_effect(sim): self._timed_terminal_event_name, macros.sec2nano(self.simulator.sim_rate), True, - conditionFunction=lambda sim: sim.sim_time >= t_close, + conditionTime=macros.sec2nano(t_close), actionFunction=side_effect, terminal=self.variable_interval, ) diff --git a/src/bsk_rl/scene/targets.py b/src/bsk_rl/scene/targets.py index d13772cc..074e1612 100644 --- a/src/bsk_rl/scene/targets.py +++ b/src/bsk_rl/scene/targets.py @@ -128,6 +128,7 @@ def visualize_target(self, target, vizSupport=None, vizInstance=None): fieldOfView=np.arctan(500 / 800), color=vizSupport.toRGBA255("white"), range=1000.0 * 1000, # meters + markerScale=np.sqrt(target.priority), ) if vizInstance.settings.showLocationCones == 0: vizInstance.settings.showLocationCones = -1 diff --git a/src/bsk_rl/sim/simulator.py b/src/bsk_rl/sim/simulator.py index 93f42b96..e8d9a27c 100644 --- a/src/bsk_rl/sim/simulator.py +++ b/src/bsk_rl/sim/simulator.py @@ -49,6 +49,7 @@ def __init__( self.max_step_duration = max_step_duration self.time_limit = time_limit self.logger = logger + self.use_simple_earth = False self.world: WorldModel @@ -65,6 +66,8 @@ def __init__( def finish_init(self) -> None: """Finish simulator initialization.""" self.set_vizard_epoch() + if self.use_simple_earth: + self.make_earth_simple() self.InitializeSimulation() self.TotalSim.StepUntilStop(0, -1) @@ -79,11 +82,15 @@ def sim_time(self) -> float: return self.sim_time_ns * mc.NANO2SEC @vizard.visualize - def setup_vizard(self, vizard_rate=None, vizSupport=None, **vizard_settings): + def setup_vizard( + self, + vizard_rate=None, + use_simple_earth=False, + vizSupport=None, + **vizard_settings, + ): """Setup Vizard for visualization.""" save_path = Path(vizard.VIZARD_PATH) - if not save_path.exists(): - os.makedirs(save_path, exist_ok=True) viz_proc_name = "VizProcess" viz_proc = self.CreateNewProcess(viz_proc_name, priority=400) @@ -100,15 +107,29 @@ def setup_vizard(self, vizard_rate=None, vizSupport=None, **vizard_settings): list_data[customizer] = [ sat.vizard_data.get(customizer, None) for sat in self.satellites ] + + # determine save file: if the configured path is a .bin file, use it directly, + # otherwise create a timestamped file inside the directory + if save_path.suffix == ".bin": + saveFile = save_path + else: + saveFile = save_path / f"viz_{time()}.bin" + saveFile.parent.mkdir(parents=True, exist_ok=True) + self.vizInstance = vizSupport.enableUnityVisualization( self, viz_task_name, scList=[sat.dynamics.scObject for sat in self.satellites], **list_data, - saveFile=save_path / f"viz_{time()}", + saveFile=str(saveFile), ) + + self.use_simple_earth = use_simple_earth + if self.use_simple_earth: + vizard_settings["atmospheresOff"] = 1 for key, value in vizard_settings.items(): setattr(self.vizInstance.settings, key, value) + vizard.VIZINSTANCE = self.vizInstance @vizard.visualize @@ -116,6 +137,21 @@ def set_vizard_epoch(self, vizInstance=None): """Set the Vizard epoch.""" vizInstance.epochInMsg.subscribeTo(self.world.gravFactory.epochMsg) + @vizard.visualize + def make_earth_simple(self, vizInstance=None, vizSupport=None): + """Make the Earth shader in Vizard lower detail to help viewing ground locations.""" + earth_texture_path = ( + Path(__file__).resolve().parent.parent + / "_dat" + / "world.200407.3x5400x2700.jpg" + ) + vizSupport.createCustomModel( + vizInstance, + simBodiesToModify=["earth"], + modelPath="HI_DEF_SPHERE", + customTexturePath=str(earth_texture_path), + ) + def _set_world( self, world_type: type["WorldModel"], world_args: dict[str, Any] ) -> None: @@ -143,7 +179,7 @@ def run(self) -> None: "max_step_duration", mc.sec2nano(self.sim_rate), True, - conditionFunction=lambda sim: sim.sim_time >= step_end_time, + conditionTime=mc.sec2nano(step_end_time), actionFunction=lambda sim: sim.logger.info("Max step duration reached"), terminal=True, ) @@ -157,8 +193,6 @@ def delete_event(self, event_name) -> None: necessary to remove created for tasks that are no longer needed (even if it is inactive), or else significant time is spent processing the event at each step. """ - event = self.eventMap[event_name] - self.eventList.remove(event) del self.eventMap[event_name] def get_satellite(self, name: str) -> "Satellite": diff --git a/tests/unittest/sim/test_simulator.py b/tests/unittest/sim/test_simulator.py index 1abbf23a..b26a721d 100644 --- a/tests/unittest/sim/test_simulator.py +++ b/tests/unittest/sim/test_simulator.py @@ -54,10 +54,8 @@ def test_delete_event(self, simbase_init): sim = self.mock_sim() event = MagicMock() sim.eventMap = {"event": event, "other": MagicMock()} - sim.eventList = [MagicMock(), event, MagicMock()] sim.delete_event("event") assert "event" not in sim.eventMap - assert event not in sim.eventList @pytest.mark.parametrize( "start_time,step_duration,time_limit,stop_time",