From c63b13658f8030aa60ca281408fe53216c0806c0 Mon Sep 17 00:00:00 2001 From: johnowagon <97626704+johnowagon@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:45:59 -0700 Subject: [PATCH] Issue #17: Adding docs folder --- .gitignore | 4 +- docs/Makefile | 28 ++ docs/make.bat | 35 ++ docs/source/_images/static/Basilisk-Logo.png | Bin 0 -> 65064 bytes docs/source/_static/custom.css | 209 ++++++++++ docs/source/conf.py | 233 +++++++++++ docs/source/docsRequired.txt | 1 + docs/source/index.rst | 23 ++ docs/source/install.rst | 31 ++ .../multisat_aeos.py | 197 +++++----- .../satellite_customization.py | 370 +++++++++--------- .../general_satellite_tasking/single_sat.py | 143 +++---- examples/genetic_algorithm/ga_hp_solver.py | 13 +- .../plotting_tools/plot_a2c_hyperparams.py | 3 +- .../plotting_tools/plot_ppo_hyperparams.py | 3 +- examples/sb3/a2c_hyperparam_search.py | 14 +- examples/sb3/dqn_hyperparam_search.py | 14 +- examples/sb3/ppo_hyperparam_search.py | 14 +- examples/sb3/sppo_hyperparam_search.py | 13 +- pyproject.toml | 3 +- src/bsk_rl/__init__.py | 4 +- ...k_bsk_version.py => _check_bsk_version.py} | 2 +- .../{finish_install.py => _finish_install.py} | 4 +- src/bsk_rl/agents/mcts.py | 27 +- src/bsk_rl/envs/agile_eos/bsk_sim.py | 42 +- .../scenario/sat_observations.py | 24 +- .../utils/orbital.py | 2 +- src/bsk_rl/envs/multisensor_eos/bsk_sim.py | 63 +-- src/bsk_rl/envs/multisensor_eos/gym_env.py | 79 ++-- src/bsk_rl/envs/simple_eos/bsk_sim.py | 43 +- src/bsk_rl/envs/simple_eos/gym_env.py | 29 +- src/bsk_rl/envs/small_body_science/bsk_sim.py | 11 +- src/bsk_rl/envs/small_body_science/gym_env.py | 56 +-- .../envs/small_body_science_pomdp/gym_env.py | 55 +-- .../actuator_primitives.py | 4 +- .../leo_initial_conditions.py | 5 +- .../utilities/initial_conditions/leo_orbit.py | 48 +-- 37 files changed, 1224 insertions(+), 625 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_images/static/Basilisk-Logo.png create mode 100644 docs/source/_static/custom.css create mode 100644 docs/source/conf.py create mode 100644 docs/source/docsRequired.txt create mode 100644 docs/source/index.rst create mode 100644 docs/source/install.rst rename src/bsk_rl/{check_bsk_version.py => _check_bsk_version.py} (96%) rename src/bsk_rl/{finish_install.py => _finish_install.py} (89%) diff --git a/.gitignore b/.gitignore index e51bc55f..569f64af 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ wheels/ .installed.cfg *.egg + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -68,6 +69,8 @@ instance/ # Sphinx documentation docs/_build/ +docs/source/API Reference +docs/source/Examples # Pickles *.pkl @@ -81,7 +84,6 @@ target/ # PDFs and Images *.pdf *.jpg -*.png # pyenv .python-version diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..8ac15275 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,28 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +clean: + rm -rf "source/Examples" "source/API Reference" + + +view: + # works on macOS + open build/html/index.html \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..747ffb7b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_images/static/Basilisk-Logo.png b/docs/source/_images/static/Basilisk-Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b4cbd51a6787bf4855646b70561df3aa0b440e GIT binary patch literal 65064 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 00000000..27004ba8 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,209 @@ +div.textblock a, div.textblock a:visited, p a, p a:visited, +a, a:hover, a:visited, +a.reference, a.internal, a.current { + color: #cfb87c; + font-weight: bold; +} + +a.icon-home, a.icon-home:visited { + color: #666; +} + +div.wy-menu > p.caption > span.caption-text { + color: white; + font-weight: bold; +} +a.el, a.el:visited { + color: #565A5C; +} + +.wy-side-nav-search>div.version { + color: #565A5C; # CU dark gray +} + +.wy-nav-content { + max-width: none; +} + +code span.pre, code { + color: #cb7ccf; +} + +th.head, .wy-nav-top { + color: white; + background-color: #565A5C; +} + +th.head p { + margin: 0px; +} + +tr td p, th.head p { + font-size: small; +} + +img.logo { + filter: drop-shadow(0px 0px 8px #fff); +} +/* override table no-wrap */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + +.math { + text-align: left; +} +.eqno { + float: right; +} + +body, h1, h2, h3, h4, h5, .rst-content, .sidebar, .sidebar-title, p.caption { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", + "Lucida Grande", "Segoe UI" !important; +} + +:root { + color-scheme: light dark; +} + +.sidebar { + max-width: 500px; +} + +code, .rst-content tt, .rst-content code { + color: #E74C3C; +} + +ul.simple li, aside.sidebar ul li { + all: revert; +} + +ul.simple li p, aside.sidebar ul li p { + all: revert; + line-height: 24px; + font-size: 16px; + margin: 0px; +} + +ul.simple ul li { + list-style-type: circle; + margin-left: 1.5em; +} + +ul.simple, aside.sidebar ul { + all: revert; + padding-left: 1.5em; +} + +figure { + text-align: center; +} + +@media (prefers-color-scheme: dark) { + .wy-nav-content, .wy-body-for-nav, .wy-nav-content-wrap, math, span[id*='MathJax-Span'] { + background-color: black; + color: #ccc; + } + + .highlight .go { + color: #ccc; + } + + img.logo { + filter: drop-shadow(0px 0px 8px #000); + } + + .rst-content code { + background: #0004; + } + html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt, .rst-content .note { + background: #2D4151; + } + + .rst-content .warning, .rst-content .caution, .rst-content .attention { + background: #51402F; + } + + .rst-content .important, .rst-content .hint, .rst-content .tip { + background: #275145; + } + + .rst-content .danger, .rst-content .error { + background: #523A37; + } + + /* sidebar formatting */ + .sidebar { + border-color: #666; + } + .rst-content .sidebar { + background: #222; + } + .rst-content .sidebar .sidebar-title { + background: #666; + } + + + .btn-neutral, .btn-neutral:hover, .btn:visited { + background: #333 !important; + color: #ccc !important; + } + + .highlight { + background: #4448; + } + + .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #222; + } + + .rst-content pre.literal-block, .rst-content div[class^='highlight'], .rst-content code, .rst-content table.docutils, .wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th, .wy-table-bordered-all td, .rst-content table.docutils td { + border-color: gray; + } + + img[src$="svg"] { + background-color: white; + filter: invert(100%) hue-rotate(180deg) saturate(200%);; + } + + img[src$="jpg"], img[src$="png"] { + border-radius: 5px; + } + + .rst-content dl:not(.docutils) dt { + background-color: #2D4151; + } + + .rst-content dl:not(.docutils) { + padding-top: 1em; + } + + .rst-content dl:not(.docutils) code.descclassname, .rst-content dl:not(.docutils) code.descname { + color: #ccc; + } + + html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt, .rst-content dl:not(.docutils) dl dt { + background-color: #333; + color: #ccc; + } + + .wy-table caption, .rst-content table.docutils caption, .rst-content table.field-list caption { + color: inherit; + } + + span.vm, span.nf, span.nn { + color:#66f !important; + } + + span.normal { + color: #333 !important; + } + + td.linenos pre { + background: #ccc !important; + } + + .rst-content .highlighted { + background-color: #333; + } +} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..deb3a51a --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,233 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + + +import datetime + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import os +import re +import sys +from pathlib import Path + +# sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "src"))) +now = datetime.datetime.now() + +project = "bsk_rl" +copyright = str(now.year) + ", Autonomous Vehicle Systems (AVS) Laboratory" +author = "Mark Stephenson" +release = "0.0.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx_rtd_theme", +] + +templates_path = ["_templates"] +exclude_patterns = [] +source_suffix = ".rst" +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_theme_options = { + "style_nav_header_background": "#CFB87C", + "navigation_depth": -1, +} +html_static_path = ["_static"] +html_css_files = ["custom.css"] +html_logo = "./_images/static/Basilisk-Logo.png" + +add_module_names = False + + +def skip(app, what, name, obj, would_skip, options): + if name == "__init__": + return False + return would_skip + + +def setup(app): + app.connect("autodoc-skip-member", skip) + + +class FileCrawler: + def __init__(self, base_source_dir, base_doc_dir): + self.base_source_dir = base_source_dir + self.base_doc_dir = base_doc_dir + + def grab_files(self, dir_path): + dirs_in_dir = [x for x in dir_path.iterdir() if x.is_dir()] + files_in_dir = dir_path.glob("*.py") + + # Remove any directories that shouldn't be added directly to the website + dir_filters = [ + r".*__pycache__.*", + r".*\.ruff_cache.*", + r".*\.egg-info", + r".*\/simplemaps_worldcities", + ] + dirs_in_dir = list( + filter( + lambda dir: not any( + re.match(filter, str(dir)) for filter in dir_filters + ), + dirs_in_dir, + ) + ) + + file_filters = [ + r".*__init__\.py", + r"(.*\/|)_[a-zA-Z0-9_]*\.py", + ] + files_in_dir = list( + filter( + lambda file: not any( + re.match(filter, str(file)) for filter in file_filters + ), + files_in_dir, + ) + ) + + return sorted(list(files_in_dir)), sorted(list(dirs_in_dir)) + + def populate_doc_index(self, index_path, file_paths, dir_paths, source_dir): + name = index_path.stem + lines = "" + + # if a _default.rst file exists in a folder, then use it to generate the index.rst file + try: + docFileName = source_dir / "_default.rst" + with open(docFileName, "r") as docFile: + docContents = docFile.read() + lines += docContents + "\n\n" + except FileNotFoundError: # Auto-generate the index.rst file + # add page tag + qual_name = str( + source_dir.relative_to(self.base_source_dir.parent) + ).replace("/", ".") + lines += ".. _" + qual_name.replace(" ", "_") + ":\n\n" + + # Title the page + lines += name + "\n" + "=" * len(name) + "\n\n" + lines += f"``{qual_name}``\n\n" + + # pull in folder _doc.rst file if it exists + try: + docFileName = source_dir / "_doc.rst" + if os.path.isfile(docFileName): + with open(docFileName, "r") as docFile: + docContents = docFile.read() + lines += docContents + "\n\n" + except FileNotFoundError: + pass + + # Also check for docs in the __init__.py file + lines += ( + """.. automodule:: """ + + qual_name + + """\n :members:\n :show-inheritance:\n\n""" + ) + + # Add a linking point to all local files + lines += ( + """\n\n.. toctree::\n :maxdepth: 1\n :caption: """ + "Files:\n\n" + ) + added_names = [] + for file_path in sorted(file_paths): + file_name = os.path.basename(os.path.normpath(file_path)) + file_name = file_name[: file_name.rfind(".")] + + if file_name not in added_names: + lines += " " + file_name + "\n" + added_names.append(file_name) + lines += "\n" + + # Add a linking point to all local directories + lines += ( + """.. toctree::\n :maxdepth: 1\n :caption: """ + "Directories:\n\n" + ) + + for dir_path in sorted(dir_paths): + dirName = os.path.basename(os.path.normpath(dir_path)) + lines += " " + dirName + "/index\n" + + with open(os.path.join(index_path, "index.rst"), "w") as f: + f.write(lines) + + def generate_autodoc(self, doc_path, source_file): + short_name = source_file.name.replace(".py", "") + qual_name = ( + str(source_file.relative_to(self.base_source_dir.parent)) + .replace("/", ".") + .replace(".py", "") + ) + + # Generate the autodoc file + lines = ".. _" + qual_name + ":\n\n" + lines += short_name + "\n" + "=" * len(short_name) + "\n\n" + lines += f"``{qual_name}``\n\n" + lines += """.. toctree::\n :maxdepth: 1\n :caption: """ + "Files" + ":\n\n" + lines += ( + """.. automodule:: """ + + qual_name + + """\n :members:\n :show-inheritance:\n\n""" + ) + + # Write to file + with open(doc_path / f"{short_name}.rst", "w") as f: + f.write(lines) + + def run(self, source_dir=None): + if source_dir is None: + source_dir = self.base_source_dir + + file_paths, dir_paths = self.grab_files(source_dir) + index_path = source_dir.relative_to(self.base_source_dir) + + # Populate the index.rst file of the local directory + os.makedirs(self.base_doc_dir / index_path, exist_ok=True) + self.populate_doc_index( + self.base_doc_dir / index_path, file_paths, dir_paths, source_dir + ) + + # Generate the correct auto-doc function for python modules + for file in file_paths: + self.generate_autodoc( + self.base_doc_dir / index_path, + file, + ) + + # Recursively go through all directories in source, documenting what is available. + for dir_path in sorted(dir_paths): + self.run( + source_dir=dir_path, + ) + + return + + +sys.path.append(os.path.abspath("../..")) +FileCrawler(Path("../../src/bsk_rl/"), Path("./API Reference/")).run() +FileCrawler(Path("../../examples"), Path("./Examples/")).run() diff --git a/docs/source/docsRequired.txt b/docs/source/docsRequired.txt new file mode 100644 index 00000000..438b963c --- /dev/null +++ b/docs/source/docsRequired.txt @@ -0,0 +1 @@ +sphinx_rtd_theme==2.0.0 \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..9a10e6f9 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,23 @@ +Welcome to bsk_rl's documentation! +================================== + +`bsk-rl` is a Python package consisting of various `Gymnasium `_ environments, agents, training scripts, and examples for spacecraft planning and scheduling problems, with an emphasis on reinforcement learning. + +Installation instruction can be found at :doc:`install`. + +New environments should be based on :ref:`bsk_rl.envs.general_satellite_tasking`. + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + install + API Reference/index + Examples/index + + +.. Indices +.. ======= + +.. * :ref:`genindex` +.. * :ref:`modindex` diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 00000000..18e06d83 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,31 @@ +Installation +============ + +.. note:: + `bsk-rl` requires `Basilisk `_, a spacecraft simulation framework package, to be installed. Instructions for installing and compiling Basilisk may be found `here `_. + + +To install bsk_rl, clone the repo + +.. code-block:: console + + $ git clone git@github.com:AVSLab/bsk_rl.git + +and run the following command in your virtual environment: + +.. code-block:: console + + (.venv) $ python -m pip install -e . && finish_install + +while in the base directory. This will install `pip` dependencies and download data dependencies. + +.. note:: + See `#51 `_ for issues with `chebpy` installation on Silicon Macs. + +Test the installation by running + +.. code-block:: console + + pytest tests/examples + +in the base directory. \ No newline at end of file diff --git a/examples/general_satellite_tasking/multisat_aeos.py b/examples/general_satellite_tasking/multisat_aeos.py index 15b81f2f..04082d71 100644 --- a/examples/general_satellite_tasking/multisat_aeos.py +++ b/examples/general_satellite_tasking/multisat_aeos.py @@ -1,3 +1,10 @@ +""" +Multisat AEOS +============= + +some text here +""" + import gymnasium as gym import numpy as np @@ -9,104 +16,108 @@ from bsk_rl.envs.general_satellite_tasking.simulation import environment from bsk_rl.utilities.initial_conditions import leo_orbit -# This script demonstrates the configuration of an environment with multiple imaging -# satellites. - -# Data environment contains 5000 targets located near random cities, which are -# randomized on reset() -env_features = CityTargets(n_targets=500, location_offset=10e3) -# Data manager records and rewards uniquely imaged targets -data_manager = data.UniqueImagingManager(env_features) - -# Generate orbital parameters for each satellite in the constellation -oes = leo_orbit.walker_delta( - n_spacecraft=3, # Number of satellites - n_planes=1, - rel_phasing=0, - altitude=500 * 1e3, - inc=45, - clustersize=3, # Cluster all 3 satellites together - clusterspacing=30, # Space satellites by a true anomaly of 30 degrees -) -# Construct satellites of the FullFeaturedSatellite type -satellites = [] -sat_type = sats.FullFeaturedSatellite -for i, oe in enumerate(oes): - # Satellite configuration arguments are inferred from the satellite type. The - # function default_sat_args collects all of the parameters that must be set for FSW - # and dynamics in the Basilisk simulation. Any parameters that are to be overridden - # can be set as arguments to default_sat_args, and an error will be raised if the - # parameter is not valid for the satellite type. - - sat_args = sat_type.default_sat_args( - oe=oe, - imageAttErrorRequirement=0.01, # Change a default parameter - imageRateErrorRequirement=0.01, - # Parameters can also be set as a function that is called each time the - # environment is reset - panelEfficiency=lambda: 0.2 + np.random.uniform(-0.01, 0.01), +def run(): + """Demonstrate the configuration of an environment with multiple imaging satellites.""" + # Data environment contains 5000 targets located near random cities, which are + # randomized on reset() + env_features = CityTargets(n_targets=500, location_offset=10e3) + # Data manager records and rewards uniquely imaged targets + data_manager = data.UniqueImagingManager(env_features) + + # Generate orbital parameters for each satellite in the constellation + oes = leo_orbit.walker_delta( + n_spacecraft=3, # Number of satellites + n_planes=1, + rel_phasing=0, + altitude=500 * 1e3, + inc=45, + clustersize=3, # Cluster all 3 satellites together + clusterspacing=30, # Space satellites by a true anomaly of 30 degrees ) - # As an example, look at the arguments for one of the satellites - if i == 0: - print(sat_args) + # Construct satellites of the FullFeaturedSatellite type + satellites = [] + sat_type = sats.FullFeaturedSatellite + for i, oe in enumerate(oes): + # Satellite configuration arguments are inferred from the satellite type. The + # function default_sat_args collects all of the parameters that must be set for FSW + # and dynamics in the Basilisk simulation. Any parameters that are to be overridden + # can be set as arguments to default_sat_args, and an error will be raised if the + # parameter is not valid for the satellite type. - # Instantiate the satellite object. Arguments to the satellite class are set here. - satellite = sat_type( - "EO" + str(i + 1), sat_args, n_ahead_observe=15, n_ahead_act=15 - ) - satellites.append(satellite) - -# Instantiate the communication method -communicator = communication.LOSMultiCommunication(satellites) - -# Make the environment with Gymnasium -env = gym.make( - "GeneralSatelliteTasking-v1", - satellites=satellites, - # Pick the type for the Basilisk environment model. Note that it is not instantiated - # here. - env_type=environment.GroundStationEnvModel, - # Like default_sat_args, default_env_args infers model parameters from the type and - # specific parameters can be overridden or randomized. - env_args=environment.GroundStationEnvModel.default_env_args(), - # Pass configuration objects - env_features=env_features, - data_manager=data_manager, - communicator=communicator, - # Integration frequency in seconds - sim_rate=0.5, - # Environment will be propagated by at most max_step_duration before needing new - # actions selected; however, some satellites will instead end the step when the - # current task is finished - max_step_duration=600.0, - # Set 3-orbit long episodes - time_limit=95 * 60, - log_level="INFO", -) + sat_args = sat_type.default_sat_args( + oe=oe, + imageAttErrorRequirement=0.01, # Change a default parameter + imageRateErrorRequirement=0.01, + # Parameters can also be set as a function that is called each time the + # environment is reset + panelEfficiency=lambda: 0.2 + np.random.uniform(-0.01, 0.01), + ) + + # As an example, look at the arguments for one of the satellites + if i == 0: + print(sat_args) -# Run the simulation until timeout or agent failure -total_reward = 0.0 -observation, info = env.reset() - -while True: - """ - Task random actions. Look at the set_action function for the chosen satellite type - to see what actions do. In this case, the action mapping is as follows: - - 0: charge - - 1: desaturate - - 2: downlink - - 3+: image the (n-3)th upcoming target - - """ - observation, reward, terminated, truncated, info = env.step( - env.action_space.sample() + # Instantiate the satellite object. Arguments to the satellite class are set here. + satellite = sat_type( + "EO" + str(i + 1), sat_args, n_ahead_observe=15, n_ahead_act=15 + ) + satellites.append(satellite) + + # Instantiate the communication method + communicator = communication.LOSMultiCommunication(satellites) + + # Make the environment with Gymnasium + env = gym.make( + "GeneralSatelliteTasking-v1", + satellites=satellites, + # Pick the type for the Basilisk environment model. Note that it is not instantiated + # here. + env_type=environment.GroundStationEnvModel, + # Like default_sat_args, default_env_args infers model parameters from the type and + # specific parameters can be overridden or randomized. + env_args=environment.GroundStationEnvModel.default_env_args(), + # Pass configuration objects + env_features=env_features, + data_manager=data_manager, + communicator=communicator, + # Integration frequency in seconds + sim_rate=0.5, + # Environment will be propagated by at most max_step_duration before needing new + # actions selected; however, some satellites will instead end the step when the + # current task is finished + max_step_duration=600.0, + # Set 3-orbit long episodes + time_limit=95 * 60, + log_level="INFO", ) - total_reward += reward - print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") + # Run the simulation until timeout or agent failure + total_reward = 0.0 + observation, info = env.reset() + + while True: + """ + Task random actions. Look at the set_action function for the chosen satellite type + to see what actions do. In this case, the action mapping is as follows: + - 0: charge + - 1: desaturate + - 2: downlink + - 3+: image the (n-3)th upcoming target + + """ + observation, reward, terminated, truncated, info = env.step( + env.action_space.sample() + ) + + total_reward += reward + print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") + + if terminated or truncated: + print("Episode complete.") + break + - if terminated or truncated: - print("Episode complete.") - break +if __name__ == "__main__": + run() diff --git a/examples/general_satellite_tasking/satellite_customization.py b/examples/general_satellite_tasking/satellite_customization.py index b0f42e4b..1f02f340 100644 --- a/examples/general_satellite_tasking/satellite_customization.py +++ b/examples/general_satellite_tasking/satellite_customization.py @@ -24,195 +24,193 @@ # option (2) is to manually override methods for observations and actions in a satellite # subclass. +if __name__ == "__main__": + # OPTION 1: Define a new satellite class by composing existing types. + class CustomSatComposed( + # Action classes. Discrete actions are added in reverse order + # Thus produces an action space of the form: + # {'0': 'action_charge', '1': 'action_desat', '2-4': 'image'} + sa.ImagingActions.configure(n_ahead_act=3), + sa.DesatAction.configure(action_duration=120.0), + sa.ChargingAction.configure(action_duration=60.0), + # Observation classes. In the vectorized observation, these will be composed in + # reverse order. Default arguments for __init__ can be overriden with configure() to + # bake them into the class definition prior to instantiation. + # This produces an observaiton in the form: + # omega_BP_P_normd: [ 0.01489828 0.0004725 -0.08323254] + # c_hat_P: [ 0.66675533 -0.69281445 0.27467338] + # r_BN_P_normd: [ 0.09177786 -0.80203809 -0.7120501 ] + # v_BN_P_normd: [ 0.91321553 -0.11810811 0.25020653] + # battery_charge_fraction: 0.740410440543005 + # target_obs: {'tgt_value_0': 0.1878322060213219, + # 'tgt_loc_0_normd': array([ 0.21883092, -0.72328348, -0.6549610]), + # 'tgt_value_1': 0.8484751150377395, + # 'tgt_loc_1_normd': array([ 0.23371944, -0.73369242, -0.6380208]), + # 'tgt_value_2': 0.14482123441765427, + # 'tgt_loc_2_normd': array([ 0.23645694, -0.73721533, -0.63293101]) + # } + # normalized_time: 0.22505847953216376 + so.TimeState, + so.TargetState.configure(n_ahead_observe=3), + so.NormdPropertyState.configure( + obs_properties=[ + dict(prop="omega_BP_P", norm=0.03), + dict(prop="c_hat_P"), + dict(prop="r_BN_P", norm=orbitalMotion.REQ_EARTH * 1e3), + dict(prop="v_BN_P", norm=7616.5), + dict(prop="battery_charge_fraction"), + ] + ), + # Base class for this satellite + sats.ImagingSatellite, + ): + # Change the attitude controller by redefining fsw_type. In this case, we are using + # a MRP Feedback based controller instead of a the default PD feedback-based + # controller. + fsw_type = fsw.SteeringImagerFSWModel + + # In some cases, the specific model you want may not exactly exists. Models are + # designed to be easily composed, so a new model based on existing models can be + # quickly defined. + + class CustomDynModel(dynamics.ImagingDynModel, dynamics.LOSCommDynModel): + pass + + dyn_type = CustomDynModel + # Model compatibility between FSW, dynamics, and the environment should be + # automatically checked in most cases. + + # OPTION 2: Define a new satellite class manually, selecting a similar class as a + # starting point + # class CustomSatManual(sats.ImagingSatellite): + # # Select FSW and dynamics as in option 1 + # fsw_type = fsw.SteeringImagerFSWModel + + # class CustomDynModel(dynamics.ImagingDynModel, dynamics.LOSCommDynModel): + # pass + + # dyn_type = CustomDynModel + + # # A more common customization requirement is designing the observation and action + # # spaces. Three functions are most commonly overridden to achieve this: get_obs, + # # set_action, and n_actions + + # # Define a custom observation. Various properties from the Basilisk simulation are + # # exposed through the satellite class to make this process easier, including + # # r_BN_B, omega_BN_B, and many more. Typically, this function should return a + # # 1-dimensional numpy array. In this example, the satellite's dynamic state and + # # information about upcoming targets are normalized. + # def get_obs(self): + # dynamic_state = np.concatenate( + # [ + # self.dynamics.omega_BP_P / 0.03, + # self.fsw.c_hat_P, + # self.dynamics.r_BN_P / (orbitalMotion.REQ_EARTH * 1e3), + # self.dynamics.v_BN_P / 7616.5, + # ] + # ) + # images_state = np.array( + # [ + # np.concatenate( + # [ + # [target.priority], + # target.location / (orbitalMotion.REQ_EARTH * 1e3), + # ] + # ) + # for target in self.upcoming_targets(self.n_ahead_observe) + # ] + # ) + # images_state = images_state.flatten() + + # return np.concatenate((dynamic_state, images_state)) + + # # Define a custom action function. In most discrete RL contexts, this function + # # should accept a single integer; however, any parameterization is possible with + # # this package. An important note: it is generally undesirable to retask the same + # # action twice in a row as controller states will get reset. Good set_action + # # defintions should include protections against this. In this example: + # # - 0: charge + # # - 1: desaturate + # # - 2+: image the (n-3)th upcoming target + # def set_action(self, action): + # if action == 0 and self.current_action != 0: + # # Use functions defined in FSW with the @action decorator to interact with + # # the Basilisk sim. + # self.fsw.action_charge() + # # Save data to the info dictonary for debugging help + # self.log_info("charging tasked") + # if action == 1 and self.current_action != 1: + # self.fsw.action_desat() + # self.log_info("desat tasked") + # else: + # target_action = action + # if isinstance(target_action, int): + # target_action -= 2 + # # Use the standard ImagingSatellite tasking function + # super().set_action(target_action) + + # if action < 2: + # self.current_action = action + + # # The action space cannot be inferred; explicitly tell gymnasium how many actions + # # the satellite can take + # @property + # def action_space(self): + # return gym.spaces.Discrete(self.n_ahead_act + 2) + + # Configure the environent + env_features = CityTargets(n_targets=1000) + data_manager = data.UniqueImagingManager(env_features) + # Use the CustomSat type + sat_type = CustomSatComposed + sat_args = sat_type.default_sat_args( + imageAttErrorRequirement=0.01, + imageRateErrorRequirement=0.01, + oe=random_orbit, + ) + satellite = sat_type( + "EO1", + sat_args, + variable_interval=True, + ) + # The composed satellite action space returns a human-readable action map + print("Actions:", satellite.action_map) + + # Make the environment with Gymnasium + env = gym.make( + "SingleSatelliteTasking-v1", + satellites=satellite, + # Select an EnvironmentModel compatible with the models in the satellite + env_type=environment.BasicEnvironmentModel, + env_args=environment.BasicEnvironmentModel.default_env_args(), + env_features=env_features, + data_manager=data_manager, + sim_rate=0.5, + max_step_duration=600.0, + time_limit=95 * 60, + log_level="INFO", + ) -# OPTION 1: Define a new satellite class by composing existing types. -class CustomSatComposed( - # Action classes. Discrete actions are added in reverse order - # Thus produces an action space of the form: - # {'0': 'action_charge', '1': 'action_desat', '2-4': 'image'} - sa.ImagingActions.configure(n_ahead_act=3), - sa.DesatAction.configure(action_duration=120.0), - sa.ChargingAction.configure(action_duration=60.0), - # Observation classes. In the vectorized observation, these will be composed in - # reverse order. Default arguments for __init__ can be overriden with configure() to - # bake them into the class definition prior to instantiation. - # This produces an observaiton in the form: - # omega_BP_P_normd: [ 0.01489828 0.0004725 -0.08323254] - # c_hat_P: [ 0.66675533 -0.69281445 0.27467338] - # r_BN_P_normd: [ 0.09177786 -0.80203809 -0.7120501 ] - # v_BN_P_normd: [ 0.91321553 -0.11810811 0.25020653] - # battery_charge_fraction: 0.740410440543005 - # target_obs: {'tgt_value_0': 0.1878322060213219, - # 'tgt_loc_0_normd': array([ 0.21883092, -0.72328348, -0.6549610]), - # 'tgt_value_1': 0.8484751150377395, - # 'tgt_loc_1_normd': array([ 0.23371944, -0.73369242, -0.6380208]), - # 'tgt_value_2': 0.14482123441765427, - # 'tgt_loc_2_normd': array([ 0.23645694, -0.73721533, -0.63293101]) - # } - # normalized_time: 0.22505847953216376 - so.TimeState, - so.TargetState.configure(n_ahead_observe=3), - so.NormdPropertyState.configure( - obs_properties=[ - dict(prop="omega_BP_P", norm=0.03), - dict(prop="c_hat_P"), - dict(prop="r_BN_P", norm=orbitalMotion.REQ_EARTH * 1e3), - dict(prop="v_BN_P", norm=7616.5), - dict(prop="battery_charge_fraction"), - ] - ), - # Base class for this satellite - sats.ImagingSatellite, -): - # Change the attitude controller by redefining fsw_type. In this case, we are using - # a MRP Feedback based controller instead of a the default PD feedback-based - # controller. - fsw_type = fsw.SteeringImagerFSWModel - - # In some cases, the specific model you want may not exactly exists. Models are - # designed to be easily composed, so a new model based on existing models can be - # quickly defined. - - class CustomDynModel(dynamics.ImagingDynModel, dynamics.LOSCommDynModel): - pass - - dyn_type = CustomDynModel - # Model compatibility between FSW, dynamics, and the environment should be - # automatically checked in most cases. - - -# OPTION 2: Define a new satellite class manually, selecting a similar class as a -# starting point -# class CustomSatManual(sats.ImagingSatellite): -# # Select FSW and dynamics as in option 1 -# fsw_type = fsw.SteeringImagerFSWModel - -# class CustomDynModel(dynamics.ImagingDynModel, dynamics.LOSCommDynModel): -# pass - -# dyn_type = CustomDynModel - -# # A more common customization requirement is designing the observation and action -# # spaces. Three functions are most commonly overridden to achieve this: get_obs, -# # set_action, and n_actions - -# # Define a custom observation. Various properties from the Basilisk simulation are -# # exposed through the satellite class to make this process easier, including -# # r_BN_B, omega_BN_B, and many more. Typically, this function should return a -# # 1-dimensional numpy array. In this example, the satellite's dynamic state and -# # information about upcoming targets are normalized. -# def get_obs(self): -# dynamic_state = np.concatenate( -# [ -# self.dynamics.omega_BP_P / 0.03, -# self.fsw.c_hat_P, -# self.dynamics.r_BN_P / (orbitalMotion.REQ_EARTH * 1e3), -# self.dynamics.v_BN_P / 7616.5, -# ] -# ) -# images_state = np.array( -# [ -# np.concatenate( -# [ -# [target.priority], -# target.location / (orbitalMotion.REQ_EARTH * 1e3), -# ] -# ) -# for target in self.upcoming_targets(self.n_ahead_observe) -# ] -# ) -# images_state = images_state.flatten() - -# return np.concatenate((dynamic_state, images_state)) - -# # Define a custom action function. In most discrete RL contexts, this function -# # should accept a single integer; however, any parameterization is possible with -# # this package. An important note: it is generally undesirable to retask the same -# # action twice in a row as controller states will get reset. Good set_action -# # defintions should include protections against this. In this example: -# # - 0: charge -# # - 1: desaturate -# # - 2+: image the (n-3)th upcoming target -# def set_action(self, action): -# if action == 0 and self.current_action != 0: -# # Use functions defined in FSW with the @action decorator to interact with -# # the Basilisk sim. -# self.fsw.action_charge() -# # Save data to the info dictonary for debugging help -# self.log_info("charging tasked") -# if action == 1 and self.current_action != 1: -# self.fsw.action_desat() -# self.log_info("desat tasked") -# else: -# target_action = action -# if isinstance(target_action, int): -# target_action -= 2 -# # Use the standard ImagingSatellite tasking function -# super().set_action(target_action) - -# if action < 2: -# self.current_action = action - -# # The action space cannot be inferred; explicitly tell gymnasium how many actions -# # the satellite can take -# @property -# def action_space(self): -# return gym.spaces.Discrete(self.n_ahead_act + 2) - - -# Configure the environent -env_features = CityTargets(n_targets=1000) -data_manager = data.UniqueImagingManager(env_features) -# Use the CustomSat type -sat_type = CustomSatComposed -sat_args = sat_type.default_sat_args( - imageAttErrorRequirement=0.01, - imageRateErrorRequirement=0.01, - oe=random_orbit, -) -satellite = sat_type( - "EO1", - sat_args, - variable_interval=True, -) -# The composed satellite action space returns a human-readable action map -print("Actions:", satellite.action_map) - -# Make the environment with Gymnasium -env = gym.make( - "SingleSatelliteTasking-v1", - satellites=satellite, - # Select an EnvironmentModel compatible with the models in the satellite - env_type=environment.BasicEnvironmentModel, - env_args=environment.BasicEnvironmentModel.default_env_args(), - env_features=env_features, - data_manager=data_manager, - sim_rate=0.5, - max_step_duration=600.0, - time_limit=95 * 60, - log_level="INFO", -) - -# Run the simulation until timeout or agent failure -total_reward = 0.0 -observation, info = env.reset() + # Run the simulation until timeout or agent failure + total_reward = 0.0 + observation, info = env.reset() -while True: - print(f"") + while True: + print(f"") - observation, reward, terminated, truncated, info = env.step( - env.action_space.sample() # Task random actions - ) + observation, reward, terminated, truncated, info = env.step( + env.action_space.sample() # Task random actions + ) - # Show the custom normalized observation vector - print("\tObservation:", observation) + # Show the custom normalized observation vector + print("\tObservation:", observation) - # Using the composed satellite features also provides a human-readable state: - for k, v in env.satellite.obs_dict.items(): - print(f"\t\t{k}: {v}") + # Using the composed satellite features also provides a human-readable state: + for k, v in env.satellite.obs_dict.items(): + print(f"\t\t{k}: {v}") - total_reward += reward - print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") - if terminated or truncated: - print("Episode complete.") - break + total_reward += reward + print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") + if terminated or truncated: + print("Episode complete.") + break diff --git a/examples/general_satellite_tasking/single_sat.py b/examples/general_satellite_tasking/single_sat.py index 926e414f..18bbb49d 100644 --- a/examples/general_satellite_tasking/single_sat.py +++ b/examples/general_satellite_tasking/single_sat.py @@ -11,84 +11,85 @@ # This script demonstrates the configuration of an environment with a single imaging # satellite. -# Data environment contains 5000 targets randomly distributed -env_features = StaticTargets(n_targets=1000) -# Data manager records and rewards uniquely imaged targets -data_manager = data.UniqueImagingManager(env_features) +if __name__ == "__main__": + # Data environment contains 5000 targets randomly distributed + env_features = StaticTargets(n_targets=1000) + # Data manager records and rewards uniquely imaged targets + data_manager = data.UniqueImagingManager(env_features) -# Construct satellites of the FullFeaturedSatellite type -sat_type = sats.FullFeaturedSatellite -# Satellite configuration arguments are inferred from the satellite type. The function -# default_sat_args collects all of the parameters that must be set for FSW and dynamics -# in the Basilisk simulation. Any parameters that are to be overridden can be set as -# arguments to default_sat_args, and an error will be raised if the parameter is not -# valid for the satellite type. + # Construct satellites of the FullFeaturedSatellite type + sat_type = sats.FullFeaturedSatellite + # Satellite configuration arguments are inferred from the satellite type. The function + # default_sat_args collects all of the parameters that must be set for FSW and dynamics + # in the Basilisk simulation. Any parameters that are to be overridden can be set as + # arguments to default_sat_args, and an error will be raised if the parameter is not + # valid for the satellite type. -sat_args = sat_type.default_sat_args( - imageAttErrorRequirement=0.01, # Change a default parameter - imageRateErrorRequirement=0.01, - # Parameters can also be set as a function that is called each time the environment - # is reset - oe=random_orbit, -) -print(sat_args) + sat_args = sat_type.default_sat_args( + imageAttErrorRequirement=0.01, # Change a default parameter + imageRateErrorRequirement=0.01, + # Parameters can also be set as a function that is called each time the environment + # is reset + oe=random_orbit, + ) + print(sat_args) -# Instantiate the satellite object. Arguments to the satellite class are set here. -satellite = sat_type("EO1", sat_args, n_ahead_observe=30, n_ahead_act=15) + # Instantiate the satellite object. Arguments to the satellite class are set here. + satellite = sat_type("EO1", sat_args, n_ahead_observe=30, n_ahead_act=15) -# Make the environment with Gymnasium -env = gym.make( - # The SingleSatelliteTasking environment takes actions and observations directly - # from the satellite, instead of wrapping them in a tuple - "SingleSatelliteTasking-v1", - satellites=satellite, - # Pick the type for the Basilisk environment model. Note that it is not instantiated - # here. - env_type=environment.GroundStationEnvModel, - # Like default_sat_args, default_env_args infers model parameters from the type and - # specific parameters can be - # overridden or randomized. - env_args=environment.GroundStationEnvModel.default_env_args(), - # Pass configuration objects - env_features=env_features, - data_manager=data_manager, - # Integration frequency in seconds - sim_rate=0.5, - # Environment will be propagated by at most max_step_duration before needing new - # actions selected; however, some satellites will instead end the step when the - # current task is finished - max_step_duration=600.0, - # Set 3-orbit long episodes - time_limit=95 * 60, - # Send the terminated signal in addition to the truncated signal at the end of the - # episode. Needed for some RL algorithms to work correctly. - terminate_on_time_limit=True, - log_level="INFO", -) + # Make the environment with Gymnasium + env = gym.make( + # The SingleSatelliteTasking environment takes actions and observations directly + # from the satellite, instead of wrapping them in a tuple + "SingleSatelliteTasking-v1", + satellites=satellite, + # Pick the type for the Basilisk environment model. Note that it is not instantiated + # here. + env_type=environment.GroundStationEnvModel, + # Like default_sat_args, default_env_args infers model parameters from the type and + # specific parameters can be + # overridden or randomized. + env_args=environment.GroundStationEnvModel.default_env_args(), + # Pass configuration objects + env_features=env_features, + data_manager=data_manager, + # Integration frequency in seconds + sim_rate=0.5, + # Environment will be propagated by at most max_step_duration before needing new + # actions selected; however, some satellites will instead end the step when the + # current task is finished + max_step_duration=600.0, + # Set 3-orbit long episodes + time_limit=95 * 60, + # Send the terminated signal in addition to the truncated signal at the end of the + # episode. Needed for some RL algorithms to work correctly. + terminate_on_time_limit=True, + log_level="INFO", + ) -# Run the simulation until timeout or agent failure -total_reward = 0.0 -observation, info = env.reset() + # Run the simulation until timeout or agent failure + total_reward = 0.0 + observation, info = env.reset() -while True: - print(f"") + while True: + print(f"") - """ - Task random actions. Look at the set_action function for the chosen satellite type - to see what actions do. In this case, the action mapping is as follows: - - 0: charge - - 1: desaturate - - 2: downlink - - 3+: image the (n-3)th upcoming target + """ + Task random actions. Look at the set_action function for the chosen satellite type + to see what actions do. In this case, the action mapping is as follows: + - 0: charge + - 1: desaturate + - 2: downlink + - 3+: image the (n-3)th upcoming target - """ - observation, reward, terminated, truncated, info = env.step( - env.action_space.sample() - ) + """ + observation, reward, terminated, truncated, info = env.step( + env.action_space.sample() + ) - total_reward += reward - print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") + total_reward += reward + print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") - if terminated or truncated: - print("Episode complete.") - break + if terminated or truncated: + print("Episode complete.") + break diff --git a/examples/genetic_algorithm/ga_hp_solver.py b/examples/genetic_algorithm/ga_hp_solver.py index fefc2345..48c25f34 100644 --- a/examples/genetic_algorithm/ga_hp_solver.py +++ b/examples/genetic_algorithm/ga_hp_solver.py @@ -4,13 +4,6 @@ from bsk_rl.utilities.genetic_algorithm import experiments -# Check if any args were passed to the script -if len(sys.argv) > 1: - # Get the index of the job array - experiment_index = int(sys.argv[1]) -else: - experiment_index = None - if __name__ == "__main__": """ This script runs a hyperparameter search for the genetic algorithm on the @@ -31,6 +24,12 @@ - AgileEOS-v0 - MultiSatAgileEOS-v0 """ + # Check if any args were passed to the script + if len(sys.argv) > 1: + # Get the index of the job array + experiment_index = int(sys.argv[1]) + else: + experiment_index = None # Set the start method for multiprocessing (required for some Linux systems) multiprocessing.set_start_method("spawn") diff --git a/examples/plotting_tools/plot_a2c_hyperparams.py b/examples/plotting_tools/plot_a2c_hyperparams.py index 24f30873..47c793f1 100644 --- a/examples/plotting_tools/plot_a2c_hyperparams.py +++ b/examples/plotting_tools/plot_a2c_hyperparams.py @@ -4,7 +4,8 @@ import matplotlib as mpl import numpy as np from matplotlib import pyplot as plt -from plot_dqn_hyperparams import concatenate_results + +from .plot_dqn_hyperparams import concatenate_results SEP = os.path.sep diff --git a/examples/plotting_tools/plot_ppo_hyperparams.py b/examples/plotting_tools/plot_ppo_hyperparams.py index 0905bfe1..a15731cb 100644 --- a/examples/plotting_tools/plot_ppo_hyperparams.py +++ b/examples/plotting_tools/plot_ppo_hyperparams.py @@ -4,7 +4,8 @@ import matplotlib as mpl import numpy as np from matplotlib import pyplot as plt -from plot_dqn_hyperparams import concatenate_results + +from .plot_dqn_hyperparams import concatenate_results SEP = os.path.sep diff --git a/examples/sb3/a2c_hyperparam_search.py b/examples/sb3/a2c_hyperparam_search.py index 59a86650..f2928776 100644 --- a/examples/sb3/a2c_hyperparam_search.py +++ b/examples/sb3/a2c_hyperparam_search.py @@ -9,13 +9,6 @@ SEP = os.path.sep -# Check if any args were passed to the script -if len(sys.argv) > 1: - # Get the index of the job array - experiment_index = int(sys.argv[1]) -else: - experiment_index = None - if __name__ == "__main__": """ This script runs a hyperparameter search for the A2C algorithm on the @@ -47,6 +40,13 @@ - SimpleEOS-v0 (not yet tested) - AgileEOS-v0 """ + # Check if any args were passed to the script + if len(sys.argv) > 1: + # Get the index of the job array + experiment_index = int(sys.argv[1]) + else: + experiment_index = None + max_steps = 90 n_its = 1 base_steps = 90 diff --git a/examples/sb3/dqn_hyperparam_search.py b/examples/sb3/dqn_hyperparam_search.py index 8e39838d..c63da826 100644 --- a/examples/sb3/dqn_hyperparam_search.py +++ b/examples/sb3/dqn_hyperparam_search.py @@ -7,13 +7,6 @@ SEP = os.path.sep -# Check if any args were passed to the script -if len(sys.argv) > 1: - # Get the index of the job array - experiment_index = int(sys.argv[1]) -else: - experiment_index = None - if __name__ == "__main__": """ This script runs a hyperparameter search for the DQN algorithm on the @@ -45,6 +38,13 @@ - AgileEOS-v0 """ + # Check if any args were passed to the script + if len(sys.argv) > 1: + # Get the index of the job array + experiment_index = int(sys.argv[1]) + else: + experiment_index = None + n_steps = 90 num_cores = cpu_count() - 2 n_its = 1 diff --git a/examples/sb3/ppo_hyperparam_search.py b/examples/sb3/ppo_hyperparam_search.py index 0a0e8de0..0bffef0a 100644 --- a/examples/sb3/ppo_hyperparam_search.py +++ b/examples/sb3/ppo_hyperparam_search.py @@ -9,13 +9,6 @@ SEP = os.path.sep -# Check if any args were passed to the script -if len(sys.argv) > 1: - # Get the index of the job array - experiment_index = int(sys.argv[1]) -else: - experiment_index = None - if __name__ == "__main__": """ This script runs a hyperparameter search for the PPO algorithm on the @@ -46,6 +39,13 @@ - SimpleEOS-v0 (not yet tested) - AgileEOS-v0 """ + # Check if any args were passed to the script + if len(sys.argv) > 1: + # Get the index of the job array + experiment_index = int(sys.argv[1]) + else: + experiment_index = None + # Define the environment parameters max_steps = 90 env_name = "MultiSensorEOS-v0" diff --git a/examples/sb3/sppo_hyperparam_search.py b/examples/sb3/sppo_hyperparam_search.py index 97425a0d..7253e4a5 100644 --- a/examples/sb3/sppo_hyperparam_search.py +++ b/examples/sb3/sppo_hyperparam_search.py @@ -9,12 +9,6 @@ SEP = os.path.sep -# Check if any args were passed to the script -if len(sys.argv) > 1: - # Get the index of the job array - experiment_index = int(sys.argv[1]) -else: - experiment_index = None if __name__ == "__main__": """ @@ -51,6 +45,13 @@ - SimpleEOS-v0 (not yet tested) - AgileEOS-v0 """ + # Check if any args were passed to the script + if len(sys.argv) > 1: + # Get the index of the job array + experiment_index = int(sys.argv[1]) + else: + experiment_index = None + # Define the environment parameters max_steps = 90 env_name = "MultiSensorEOS-v0" diff --git a/pyproject.toml b/pyproject.toml index 0829a54b..fff3c082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,10 +28,11 @@ dependencies = [ "ruff>=0.1.9", "scikit-learn", "scipy", + "sphinx-rtd-theme", "stable-baselines3", "tensorflow", "torch", ] [project.scripts] -finish_install = "bsk_rl.finish_install:pck_install" +finish_install = "bsk_rl._finish_install:pck_install" diff --git a/src/bsk_rl/__init__.py b/src/bsk_rl/__init__.py index cd711e48..54d41e76 100644 --- a/src/bsk_rl/__init__.py +++ b/src/bsk_rl/__init__.py @@ -1,6 +1,6 @@ from gymnasium.envs.registration import register -from bsk_rl.check_bsk_version import check_bsk_version +from bsk_rl._check_bsk_version import _check_bsk_version register(id="SimpleEOS-v0", entry_point="bsk_rl.envs.simple_eos.gym_env:SimpleEOS") @@ -37,4 +37,4 @@ ) -check_bsk_version() +_check_bsk_version() diff --git a/src/bsk_rl/check_bsk_version.py b/src/bsk_rl/_check_bsk_version.py similarity index 96% rename from src/bsk_rl/check_bsk_version.py rename to src/bsk_rl/_check_bsk_version.py index d28a051c..70283c9b 100644 --- a/src/bsk_rl/check_bsk_version.py +++ b/src/bsk_rl/_check_bsk_version.py @@ -5,7 +5,7 @@ from packaging.version import parse as parse_version -def check_bsk_version(): +def _check_bsk_version(): f = open( os.path.join( os.path.dirname(os.path.realpath(__file__)), diff --git a/src/bsk_rl/finish_install.py b/src/bsk_rl/_finish_install.py similarity index 89% rename from src/bsk_rl/finish_install.py rename to src/bsk_rl/_finish_install.py index be45eaec..96fae5c9 100644 --- a/src/bsk_rl/finish_install.py +++ b/src/bsk_rl/_finish_install.py @@ -6,7 +6,7 @@ import requests -from bsk_rl.check_bsk_version import check_bsk_version +from bsk_rl._check_bsk_version import _check_bsk_version def pck_install(): @@ -32,4 +32,4 @@ def pck_install(): / "simplemaps_worldcities" ) - check_bsk_version() + _check_bsk_version() diff --git a/src/bsk_rl/agents/mcts.py b/src/bsk_rl/agents/mcts.py index 2c4a5ee5..6c48c6a2 100644 --- a/src/bsk_rl/agents/mcts.py +++ b/src/bsk_rl/agents/mcts.py @@ -14,26 +14,31 @@ class MCTS: If a heuristic rollout type is used, create the policy using the following two lines of code: - stateMachineMCTS = state_machine.StateMachine() - stateMachineMCTS.loadTransferConditions("agile_eos_ops.adv") - rollout_policy = AgileEOSRolloutPolicy(env=env, state_machine=stateMachineMCTS) + .. code-block:: python - Then, load the policy as the rollout_policy during initialization: + stateMachineMCTS = state_machine.StateMachine() + stateMachineMCTS.loadTransferConditions("agile_eos_ops.adv") + rollout_policy = AgileEOSRolloutPolicy(env=env, state_machine=stateMachineMCTS) - MCTS_Agent = MCTS(c=c, num_sims=num_sims, rollout_policy=rollout_policy) + Then, load the policy as the rollout_policy during initialization: - The parameter c controls the scaling of the exploration bonus + .. code-block:: python - The parameter num_sims determines the number of simulations per call of - selectAction() + MCTS_Agent = MCTS(c=c, num_sims=num_sims, rollout_policy=rollout_policy) The env and initial conditions must be loaded in after initialization. The algorithm will automatically restart the sim and step it forward to the last state. This is due to limitations in copying Basilisk: - MCTS_Agent.setEnv( - env_name, env.initial_conditions, max_steps=num_steps, max_length=t_final - ) + .. code-block:: python + + MCTS_Agent.setEnv( + env_name, env.initial_conditions, max_steps=num_steps, max_length=t_final + ) + + Args: + c: scaling of the exploration bonus + num_sims: number of simulations per call of selectAction() """ def __init__( diff --git a/src/bsk_rl/envs/agile_eos/bsk_sim.py b/src/bsk_rl/envs/agile_eos/bsk_sim.py index 47e34542..4ee1c7a5 100644 --- a/src/bsk_rl/envs/agile_eos/bsk_sim.py +++ b/src/bsk_rl/envs/agile_eos/bsk_sim.py @@ -38,7 +38,6 @@ unitTestSupport, vizSupport, ) -from numpy.random import uniform from bsk_rl.utilities.effector_primitives import actuator_primitives as ap from bsk_rl.utilities.initial_conditions import leo_orbit, sc_attitudes @@ -51,18 +50,21 @@ class AgileEOSSimulator(SimulationBaseClass.SimBaseClass): Simulates a spacecraft in LEO with atmospheric drag and J2. Dynamics Components - - Forces: J2, Atmospheric Drag - - Environment: Exponential density model; eclipse - - Actuators: ExternalForceTorque, reaction wheels - - Sensors: SimpleNav - - Power System: SimpleBattery, SimplePOwerSink, SimpleSolarPanel - - Data Management System: spaceToGroundTransmitter, simpleStorageUnit, - simpleInstrument + + * Forces: J2, Atmospheric Drag + * Environment: Exponential density model; eclipse + * Actuators: ExternalForceTorque, reaction wheels + * Sensors: SimpleNav + * Power System: SimpleBattery, SimplePOwerSink, SimpleSolarPanel + * Data Management System: spaceToGroundTransmitter, simpleStorageUnit, + simpleInstrument FSW Components: - - MRP Feedback controller - - locationPoint - targets, sun-pointing - - Desat + + * MRP Feedback controller + * locationPoint - targets, sun-pointing + * Desat + """ def __init__( @@ -196,7 +198,7 @@ def set_ICs(self): # Sample attitude and rates sigma_init, omega_init = sc_attitudes.random_tumble(maxSpinRate=0.00001) - wheel_speeds = uniform(-1500, 1500, 3) # RPMs + wheel_speeds = np.random.uniform(-1500, 1500, 3) # RPMs # Dict of initial conditions initial_conditions = { @@ -224,7 +226,7 @@ def set_ICs(self): "disturbance_magnitude": 4e-3, "disturbance_vector": np.random.standard_normal(3), # Reaction Wheel speeds - # "wheelSpeeds": uniform(-400,400,3), # RPM + # "wheelSpeeds": np.random.uniform(-400,400,3), # RPM "wheelSpeeds": wheel_speeds, # RPM "maxSpeed": 3000, # RPM # Solar Panel Parameters @@ -1858,12 +1860,14 @@ def close_gracefully(self): def compute_image_tuples(self, r_BN_N, v_BN_N): """ Computes the self.n_images image state tuples - 0-2: S/c Hill-Frame Position - 3: Priority - 4: Imaged? - 5: Downlinked? - :return: image state tuples (in a single np array) - normalized and - non-normalized + + * 0-2: S/c Hill-Frame Position + * 3: Priority + * 4: Imaged? + * 5: Downlinked? + + Returns: + image state tuples (in a single np array) - normalized and non-normalized """ # Initialize the image tuple array image_tuples = np.zeros(self.target_tuple_size * self.n_target_buffer) diff --git a/src/bsk_rl/envs/general_satellite_tasking/scenario/sat_observations.py b/src/bsk_rl/envs/general_satellite_tasking/scenario/sat_observations.py index 5280c6f4..31b6031e 100644 --- a/src/bsk_rl/envs/general_satellite_tasking/scenario/sat_observations.py +++ b/src/bsk_rl/envs/general_satellite_tasking/scenario/sat_observations.py @@ -92,8 +92,12 @@ def __init__( Args: obs_properties: List of properties that can be found in fsw or dynamics that are to be appended to the the observation. Properties are optionally - normalized by some factor. Specified in + normalized by some factor. Specified in the form + + :code-block: python + [dict(prop="prop_name", module="fsw"/"dynamics"/None, norm=1.0)] + If module is not specified or None, the source of the property is inferred. If norm is not specified, it is set to 1.0 (no normalization). args: Passed through to satellite @@ -184,11 +188,11 @@ def __init__( target_properties: List of properties to include in the observation in the format [dict(prop="prop_name", norm=norm)]. If norm is not specified, it is set to 1.0 (no normalization). Properties to choose from: - - priority - - location - - window_open - - window_mid - - window_close + * priority + * location + * window_open + * window_mid + * window_close args: Passed through to satellite kwargs: Passed through to satellite """ @@ -295,10 +299,10 @@ def __init__( in the format [dict(prop="prop_name", norm=norm)]. If norm is not specified, it is set to 1.0 (no normalization). Properties to choose from: - - location - - window_open - - window_mid - - window_close + * location + * window_open + * window_mid + * window_close args: Passed through to satellite kwargs: Passed through to satellite """ diff --git a/src/bsk_rl/envs/general_satellite_tasking/utils/orbital.py b/src/bsk_rl/envs/general_satellite_tasking/utils/orbital.py index 4ccc92b4..c23f7a99 100644 --- a/src/bsk_rl/envs/general_satellite_tasking/utils/orbital.py +++ b/src/bsk_rl/envs/general_satellite_tasking/utils/orbital.py @@ -61,7 +61,7 @@ def random_epoch(start: int = 2000, end: int = 2022): end: Final year. Returns: - Epoch in `YYYY MMM DD HH:MM:SS.SSS (UTC)` format + Epoch in ``YYYY MMM DD HH:MM:SS.SSS (UTC)`` format """ year = np.random.randint(start, end) month = np.random.choice( diff --git a/src/bsk_rl/envs/multisensor_eos/bsk_sim.py b/src/bsk_rl/envs/multisensor_eos/bsk_sim.py index f75da751..112d742e 100644 --- a/src/bsk_rl/envs/multisensor_eos/bsk_sim.py +++ b/src/bsk_rl/envs/multisensor_eos/bsk_sim.py @@ -38,15 +38,17 @@ class MultiSensorEOSSimulator(SimulationBaseClass.SimBaseClass): Simulates ground observations by a single spacecraft in LEO. Dynamics Components - - Forces: J2, Atmospheric Drag w/ COM offset - - Environment: Exponential density model; eclipse - - Actuators: ExternalForceTorque - - Sensors: SimpleNav - - Systems: SimpleBattery, SimpleSink, SimpleSolarPanel + + * Forces: J2, Atmospheric Drag w/ COM offset + * Environment: Exponential density model; eclipse + * Actuators: ExternalForceTorque + * Sensors: SimpleNav + * Systems: SimpleBattery, SimpleSink, SimpleSolarPanel FSW Components: - - mrpFeedback controller - - inertial3d (sun pointing), hillPoint (nadir pointing) + + * mrpFeedback controller + * inertial3d (sun pointing), hillPoint (nadir pointing) :return: """ @@ -134,11 +136,12 @@ def __init__( def set_env_dynamics(self): """ Sets up environmental dynamics for the sim, including: - - SPICE - - Eclipse - - Planetary atmosphere - - Gravity - - Spherical harmonics + + * SPICE + * Eclipse + * Planetary atmosphere + * Gravity + * Spherical harmonics """ # clear prior gravitational body and SPICE setup definitions self.gravFactory = simIncludeGravBody.gravBodyFactory() @@ -401,16 +404,19 @@ def setup_viz(self): def set_fsw(self): """ - Sets up the attitude guidance stack for the simulation. This simulator runs: - inertial3Dpoint - Sets the attitude guidance objective to point the main panel - at the sun. - hillPointTask: Sets the attitude guidance objective to point a "camera" - boresight towards nadir. - attitudeTrackingError: Computes the difference between estimated and guidance - attitudes - mrpFeedbackControl: Computes an appropriate control torque given an attitude - error - :return: + Sets up the attitude guidance stack for the simulation. + + This simulator runs: + + * inertial3Dpoint - Sets the attitude guidance objective to point the main panel + at the sun. + * hillPointTask: Sets the attitude guidance objective to point a "camera" + boresight towards nadir. + * attitudeTrackingError: Computes the difference between estimated and guidance + attitudes + * mrpFeedbackControl: Computes an appropriate control torque given an attitude + error + """ self.dyn_proc.addTask( self.CreateNewTask("sunPointTask", mc.sec2nano(self.fsw_step)), @@ -660,11 +666,14 @@ def zeroGateWayMsgs(self): def run_sim(self, action): """ Executes the sim for a specified duration given a mode command. - :param action: - 0 - Point solar panels at the sun - 1 - Desaturate reaction wheels - >1 - Image types - :return: + + Args: + action: + * 0 - Point solar panels at the sun + * 1 - Desaturate reaction wheels + * >1 - Image types + + Returns: sim_state - simulation states generated sim_over - episode over flag """ diff --git a/src/bsk_rl/envs/multisensor_eos/gym_env.py b/src/bsk_rl/envs/multisensor_eos/gym_env.py index fae74ae8..1ab32ef4 100644 --- a/src/bsk_rl/envs/multisensor_eos/gym_env.py +++ b/src/bsk_rl/envs/multisensor_eos/gym_env.py @@ -18,26 +18,29 @@ class MultiSensorEOS(gym.Env): results in full reward, other image types results in no reward. Action Space (discrete): - 0 - Points solar panels at the sun. - 1 - Desaturates the reaction wheels. - >1 - Orients the s/c towards the earth; takes image of type _. + + * 0 - Points solar panels at the sun. + * 1 - Desaturates the reaction wheels. + * >1 - Orients the s/c towards the earth; takes image of type _. Observation Space: - r_sc_I - float[3,] - spacecraft position. - v_sc - float[3,] - spacecraft velocity in PCPF. - |sigma_RB| - float [0,1] - norm of the spacecraft error MRP with respect to the - last reference frame specified. - |omega_BN| - float - norm of the total spacecraft bus rotational velocity with - respect to the inertial frame. - |omega_RW| - float - norm of the reaction wheel rotational velocities. - storedCharge - float [0,batCapacity] - indicates the s/c battery charge level in - W-s. - sun_indicator - float [0, 1] - indicates the flux mitigator due to eclipse. - access indicator - access to the next target - img_mode norm - float [0,1] - indicates the required imaging mode. + + * r_sc_I - float[3,] - spacecraft position. + * v_sc - float[3,] - spacecraft velocity in PCPF. + * sigma_RB - float [0,1] - norm of the spacecraft error MRP with respect to the + last reference frame specified. + * omega_BN - float - norm of the total spacecraft bus rotational velocity with + respect to the inertial frame. + * omega_RW - float - norm of the reaction wheel rotational velocities. + * storedCharge - float [0,batCapacity] - indicates the s/c battery charge level in + W-s. + * sun_indicator - float [0, 1] - indicates the flux mitigator due to eclipse. + * access indicator - access to the next target + * img_mode norm - float [0,1] - indicates the required imaging mode. Reward Function: r = 1/(1+ | sigma_RB|) if correct sensor + Intended to provide a rich reward in action 1 when the spacecraft is pointed towards the earth, decaying as sigma^2 as the pointing error increases. """ @@ -114,31 +117,27 @@ def step(self, action): The agent takes a step in the environment. Note that the simulator must be initialized - Parameters - ---------- - action : int - Returns - ------- - ob, reward, episode_over, truncated, info : tuple - ob (object) : - an environment-specific object representing your observation of - the environment. - reward (float) : - amount of reward achieved by the previous action. The scale - varies between environments, but the goal is always to increase - your total reward. - episode_over (bool) : - whether it's time to reset the environment again. Most (but not - all) tasks are divided up into well-defined episodes, and done - being True indicates the episode has terminated. (For example, - perhaps the pole tipped too far, or you lost your last life.) - truncated (truncated) : set to false. Gymnasium requirement. - info (dict) : - diagnostic information useful for debugging. It can sometimes - be useful for learning (for example, it might contain the raw - probabilities behind the environment's last state change). - However, official evaluations of your agent are not allowed to - use this for learning. + Args: + action: int + + Returns: + + * ob (object): an environment-specific object representing your observation of + the environment. + * reward (float): amount of reward achieved by the previous action. The scale + varies between environments, but the goal is always to increase + your total reward. + * episode_over (bool): whether it's time to reset the environment again. Most (but not + all) tasks are divided up into well-defined episodes, and done + being True indicates the episode has terminated. (For example, + perhaps the pole tipped too far, or you lost your last life.) + * truncated (truncated): set to false. Gymnasium requirement. + * info (dict): diagnostic information useful for debugging. It can sometimes + be useful for learning (for example, it might contain the raw + probabilities behind the environment's last state change). + However, official evaluations of your agent are not allowed to + use this for learning. + """ self.curr_step += 1 diff --git a/src/bsk_rl/envs/simple_eos/bsk_sim.py b/src/bsk_rl/envs/simple_eos/bsk_sim.py index 22418a74..1fae7ccc 100644 --- a/src/bsk_rl/envs/simple_eos/bsk_sim.py +++ b/src/bsk_rl/envs/simple_eos/bsk_sim.py @@ -40,7 +40,6 @@ unitTestSupport, vizSupport, ) -from numpy.random import uniform from bsk_rl.utilities.effector_primitives import actuator_primitives as ap from bsk_rl.utilities.initial_conditions import leo_orbit, sc_attitudes @@ -54,18 +53,21 @@ class SimpleEOSSimulator(SimulationBaseClass.SimBaseClass): the Earth. Dynamics Components - - Forces: J2, Atmospheric Drag w/ COM offset - - Environment: Exponential density model; eclipse - - Actuators: ExternalForceTorque - - Sensors: SimpleNav - - Power System: SimpleBattery, SimpleSink, SimpleSolarPanel - - Data Management System: spaceToGroundTransmitter, simpleStorageUnit, - simpleInstrument + + * Forces: J2, Atmospheric Drag w/ COM offset + * Environment: Exponential density model; eclipse + * Actuators: ExternalForceTorque + * Sensors: SimpleNav + * Power System: SimpleBattery, SimpleSink, SimpleSolarPanel + * Data Management System: spaceToGroundTransmitter, simpleStorageUnit, + simpleInstrument FSW Components: - - MRP Feedback controller - - inertial3d (sun pointing), hillPoint (nadir pointing) - - Desat + + * MRP Feedback controller + * inertial3d (sun pointing), hillPoint (nadir pointing) + * Desat + """ def __init__( @@ -188,7 +190,7 @@ def set_ICs(self): "disturbance_magnitude": 2e-3, "disturbance_vector": np.random.standard_normal(3), # Reaction Wheel speeds - "wheelSpeeds": uniform(-4000 * mc.RPM, 4000 * mc.RPM, 3), # rad/s + "wheelSpeeds": np.random.uniform(-4000 * mc.RPM, 4000 * mc.RPM, 3), # rad/s # Solar Panel Parameters "nHat_B": np.array([0, 1, 0]), "panelArea": 2 * 1.0 * 0.5, @@ -847,14 +849,15 @@ def set_dynamics(self): def set_fsw(self): """ Sets up the attitude guidance stack for the simulation. This simulator runs: - inertial3Dpoint - Sets the attitude guidance objective to point the main panel - at the sun. - hillPointTask: Sets the attitude guidance objective to point a "camera" angle - towards nadir. - attitudeTrackingError: Computes the difference between estimated and guidance - attitudes - mrpFeedbackControl: Computes an appropriate control torque given an attitude - error + + * inertial3Dpoint - Sets the attitude guidance objective to point the main panel + at the sun. + * hillPointTask: Sets the attitude guidance objective to point a "camera" angle + towards nadir. + * attitudeTrackingError: Computes the difference between estimated and guidance + attitudes + * mrpFeedbackControl: Computes an appropriate control torque given an attitude + error """ self.processName = self.DynamicsProcessName diff --git a/src/bsk_rl/envs/simple_eos/gym_env.py b/src/bsk_rl/envs/simple_eos/gym_env.py index d3b91f90..278bed02 100644 --- a/src/bsk_rl/envs/simple_eos/gym_env.py +++ b/src/bsk_rl/envs/simple_eos/gym_env.py @@ -14,27 +14,26 @@ class SimpleEOS(gym.Env): by nadir pointing. Specific imaging targets are not considered. Action Space (discrete, 0 or 1): - 0 - Imaging mode - 1 - Charging mode - 2 - Desat mode - 3 - Downlink mode + * 0 - Imaging mode + * 1 - Charging mode + * 2 - Desat mode + * 3 - Downlink mode Observation Space: - Inertial position and velocity - indices 0-5 - Attitude error and attitude rate - indices 6-7 - Reaction wheel speeds - indices 8-11 - Battery charge - indices 12 - Eclipse indicator - indices 13 - Stored data onboard spacecraft - indices 14 - Data transmitted over interval - indices 15 - Amount of time ground stations were accessible (s) - 16-22 - Percent through planning interval - 23 + * Inertial position and velocity - indices 0-5 + * Attitude error and attitude rate - indices 6-7 + * Reaction wheel speeds - indices 8-11 + * Battery charge - indices 12 + * Eclipse indicator - indices 13 + * Stored data onboard spacecraft - indices 14 + * Data transmitted over interval - indices 15 + * Amount of time ground stations were accessible (s) - 16-22 + * Percent through planning interval - 23 Reward Function: r = +1 for each MB downlinked and no failure r = +1 for each MB downlinked and no failure and +1 if t > t_max - r = - 1000 if failure - (battery drained, buffer overflow, reaction wheel speeds over max) + r = - 1000 if failure (battery drained, buffer overflow, reaction wheel speeds over max) """ def __init__(self): diff --git a/src/bsk_rl/envs/small_body_science/bsk_sim.py b/src/bsk_rl/envs/small_body_science/bsk_sim.py index 08ffb7f3..0c3d4af9 100644 --- a/src/bsk_rl/envs/small_body_science/bsk_sim.py +++ b/src/bsk_rl/envs/small_body_science/bsk_sim.py @@ -42,7 +42,6 @@ unitTestSupport, vizSupport, ) -from numpy.random import uniform from bsk_rl.utilities.effector_primitives import actuator_primitives as ap from bsk_rl.utilities.initial_conditions import sc_attitudes, small_body @@ -298,10 +297,10 @@ def set_ic(self): sigma_init, omega_init = sc_attitudes.random_tumble(maxSpinRate=0.00001) x_0_delta = np.zeros(12) - x_0_delta[0:3] = uniform(-50, 50.0, 3) # Relative s/c position - x_0_delta[3:6] = uniform(-0.1, 0.1, 3) # Relative s/c velocity - x_0_delta[6:9] = uniform(-0.1, 0.1, 3) # Small body attitude - x_0_delta[9:12] = uniform(-0.1, 0.1, 3) # Small body attitude rate + x_0_delta[0:3] = np.random.uniform(-50, 50.0, 3) # Relative s/c position + x_0_delta[3:6] = np.random.uniform(-0.1, 0.1, 3) # Relative s/c velocity + x_0_delta[6:9] = np.random.uniform(-0.1, 0.1, 3) # Small body attitude + x_0_delta[9:12] = np.random.uniform(-0.1, 0.1, 3) # Small body attitude rate mapping_points = small_body.generate_mapping_points( self.n_map_points, self.body_radius @@ -375,7 +374,7 @@ def set_ic(self): "sigma_init": sigma_init, "omega_init": omega_init, # Reaction Wheel speeds - "wheelSpeeds": uniform(-2000 * mc.RPM, 2000 * mc.RPM, 3), # rad/s + "wheelSpeeds": np.random.uniform(-2000 * mc.RPM, 2000 * mc.RPM, 3), # rad/s "max_dV": 40, # m/s # RW motor torque and thruster force mapping FSW config "controlAxes_B": [1, 0, 0, 0, 1, 0, 0, 0, 1], diff --git a/src/bsk_rl/envs/small_body_science/gym_env.py b/src/bsk_rl/envs/small_body_science/gym_env.py index 84e6414b..a12a0029 100644 --- a/src/bsk_rl/envs/small_body_science/gym_env.py +++ b/src/bsk_rl/envs/small_body_science/gym_env.py @@ -11,40 +11,40 @@ class SmallBodyScience(gym.Env): waypoints defined in the sun anti-momentum frame to image candidate landing sites or collect spectroscopy map data while avoiding resource constraint violations. Resource constraint violations include: - - Fuel - - Power - - Data storage - - Collision with the body (not necessarilly a resource, but considered a - failure condition) + * Fuel + * Power + * Data storage + * Collision with the body (not necessarilly a resource, but considered a + failure condition) Action Space (Discrete): - 0 - Charging Mode - 1 - 8 - Transition to waypoint 1-8 - 9 - Map - 10 - Downlink - 11 - Image + * 0 - Charging Mode + * 1 - 8 - Transition to waypoint 1-8 + * 9 - Map + * 10 - Downlink + * 11 - Image Observation Space (Box): - 0-2: Hill-frame position - 3-5: Hill-frame velocity - 6: Eclipse - 7: Data buffer storage - 8: Battery level - 9: dV consumed - 10: Downlink availability - 11-13: Current waypoint - 14-16: Last waypoint - 17: Imaged targets - 18: Downlinked targets - 19-21: Next closest unimaged target position in Hill frame - 22-30: Map regions collected + * 0-2: Hill-frame position + * 3-5: Hill-frame velocity + * 6: Eclipse + * 7: Data buffer storage + * 8: Battery level + * 9: dV consumed + * 10: Downlink availability + * 11-13: Current waypoint + * 14-16: Last waypoint + * 17: Imaged targets + * 18: Downlinked targets + * 19-21: Next closest unimaged target position in Hill frame + * 22-30: Map regions collected Reward Function: - r = +A each tgt downlinked for first time - r = +B for each tgt imaged for first time - r = +C for each map region downlinked for first time - r = +D for each map region collected for first time - r = -E for failure + * r = +A each tgt downlinked for first time + * r = +B for each tgt imaged for first time + * r = +C for each map region downlinked for first time + * r = +D for each map region collected for first time + * r = -E for failure """ def __init__( diff --git a/src/bsk_rl/envs/small_body_science_pomdp/gym_env.py b/src/bsk_rl/envs/small_body_science_pomdp/gym_env.py index 6bdcc47e..84cd8d3a 100644 --- a/src/bsk_rl/envs/small_body_science_pomdp/gym_env.py +++ b/src/bsk_rl/envs/small_body_science_pomdp/gym_env.py @@ -14,40 +14,41 @@ class SmallBodySciencePOMDP(SmallBodyScience): opposed to the SmallBodyScience environment, this environment is utilizes an EKF filter for the observation space to simulate a POMDP, which provides a belief state for the POMDP. + Resource constraint violations include: - - Fuel - - Power - - Data storage - - Collision with the body (not necessarilly a resource, but considered a - failure condition) + * Fuel + * Power + * Data storage + * Collision with the body (not necessarily a resource, but considered a + failure condition) Action Space (Discrete): - 0 - Charging Mode - 1 - 8 - Transition to waypoint 1-8 - 9 - Map - 10 - Downlink - 11 - Image - 12 - Navigation Mode + * 0 - Charging Mode + * 1 - 8 - Transition to waypoint 1-8 + * 9 - Map + * 10 - Downlink + * 11 - Image + * 12 - Navigation Mode Observation Space (Box): - 0-2: Hill-frame position - 3-5: Hill-frame velocity - 6: Eclipse - 7: Data buffer storage - 8: Battery level - 9: dV consumed - 10: Downlink availability - 11-13: Current waypoint - 14-16: Last waypoint - 17-20: Location of the next target for imaging - 20-26: Filter covariance diagonals + * 0-2: Hill-frame position + * 3-5: Hill-frame velocity + * 6: Eclipse + * 7: Data buffer storage + * 8: Battery level + * 9: dV consumed + * 10: Downlink availability + * 11-13: Current waypoint + * 14-16: Last waypoint + * 17-20: Location of the next target for imaging + * 20-26: Filter covariance diagonals Reward Function: - r = +A each tgt downlinked for first time - r = +B for each tgt imaged for first time - r = +C for each map region downlinked for first time - r = +D for each map region collected for first time - r = -E for failure + * r = +A each tgt downlinked for first time + * r = +B for each tgt imaged for first time + * r = +C for each map region downlinked for first time + * r = +D for each map region collected for first time + * r = -E for failure """ def __init__(self): diff --git a/src/bsk_rl/utilities/effector_primitives/actuator_primitives.py b/src/bsk_rl/utilities/effector_primitives/actuator_primitives.py index 8f704a72..0254c841 100644 --- a/src/bsk_rl/utilities/effector_primitives/actuator_primitives.py +++ b/src/bsk_rl/utilities/effector_primitives/actuator_primitives.py @@ -1,6 +1,6 @@ +import numpy as np from Basilisk.simulation import reactionWheelStateEffector, thrusterDynamicEffector from Basilisk.utilities import simIncludeRW, simIncludeThruster -from numpy.random import uniform def balancedHR16Triad( @@ -15,7 +15,7 @@ def balancedHR16Triad( """ rwFactory = simIncludeRW.rwFactory() if useRandom: - wheelSpeeds = uniform(randomBounds[0], randomBounds[1], 3) + wheelSpeeds = np.random.uniform(randomBounds[0], randomBounds[1], 3) rwFactory.create( "Honeywell_HR16", [1, 0, 0], maxMomentum=50.0, Omega=wheelSpeeds[0] # RPM diff --git a/src/bsk_rl/utilities/initial_conditions/leo_initial_conditions.py b/src/bsk_rl/utilities/initial_conditions/leo_initial_conditions.py index 5adb03c0..c452e96c 100644 --- a/src/bsk_rl/utilities/initial_conditions/leo_initial_conditions.py +++ b/src/bsk_rl/utilities/initial_conditions/leo_initial_conditions.py @@ -2,7 +2,6 @@ from Basilisk.utilities import astroFunctions from Basilisk.utilities import macros as mc from Basilisk.utilities import orbitalMotion -from numpy.random import uniform from bsk_rl.utilities.initial_conditions import leo_orbit, sc_attitudes @@ -37,7 +36,7 @@ def sampled_400km_leo_smallsat_tumble(): "disturbance_magnitude": 2e-4, "disturbance_vector": np.random.standard_normal(3), # Reaction Wheel speeds - "wheelSpeeds": uniform(-800, 800, 3), # RPM + "wheelSpeeds": np.random.uniform(-800, 800, 3), # RPM # Solar Panel Parameters "nHat_B": np.array([0, -1, 0]), "panelArea": 0.2 * 0.3, @@ -148,7 +147,7 @@ def walker_delta_single_sc_500_km(oe, sim_length, global_tgts, priorities): # Sample attitude and rates sigma_init, omega_init = sc_attitudes.random_tumble(maxSpinRate=0.00001) - wheel_speeds = uniform(-1500, 1500, 3) # RPMs + wheel_speeds = np.random.uniform(-1500, 1500, 3) # RPMs # Dict of initial conditions initial_conditions = { diff --git a/src/bsk_rl/utilities/initial_conditions/leo_orbit.py b/src/bsk_rl/utilities/initial_conditions/leo_orbit.py index d7129e41..76ec5124 100644 --- a/src/bsk_rl/utilities/initial_conditions/leo_orbit.py +++ b/src/bsk_rl/utilities/initial_conditions/leo_orbit.py @@ -7,7 +7,6 @@ from Basilisk.utilities import SimulationBaseClass from Basilisk.utilities import macros as mc from Basilisk.utilities import orbitalMotion, simIncludeGravBody -from numpy.random import uniform bskPath = __path__[0] @@ -40,7 +39,7 @@ def random_inclined_circular_300km(): """ oe = orbitalMotion.ClassicElements() - oe.a = 6371 * 1000.0 + uniform(290e3, 310e3) + oe.a = 6371 * 1000.0 + np.random.uniform(290e3, 310e3) oe.e = 0.0 oe.i = 45.0 * mc.D2R @@ -59,11 +58,11 @@ def sampled_400km(): """ oe = orbitalMotion.ClassicElements() oe.a = 6371 * 1000.0 + 400.0 * 1000 - oe.e = uniform(0, 0.001, 1) - oe.i = uniform(-90 * mc.D2R, 90 * mc.D2R, 1) - oe.Omega = uniform(0 * mc.D2R, 360 * mc.D2R, 1) - oe.omega = uniform(0 * mc.D2R, 360 * mc.D2R, 1) - oe.f = uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.e = np.random.uniform(0, 0.001, 1) + oe.i = np.random.uniform(-90 * mc.D2R, 90 * mc.D2R, 1) + oe.Omega = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.omega = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.f = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) rN, vN = orbitalMotion.elem2rv(mu, oe) return oe, rN, vN @@ -78,12 +77,12 @@ def sampled_500km_boulder_gs(): mu = 0.3986004415e15 oe = orbitalMotion.ClassicElements() oe.a = 6371 * 1000.0 + 500.0 * 1000 - oe.e = uniform(0, 0.01, 1) - # oe.i = uniform(40*mc.D2R, 60*mc.D2R,1) - oe.i = uniform(40 * mc.D2R, 60 * mc.D2R, 1) - oe.Omega = uniform(0 * mc.D2R, 20 * mc.D2R, 1) - oe.omega = uniform(0 * mc.D2R, 20 * mc.D2R, 1) - oe.f = uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.e = np.random.uniform(0, 0.01, 1) + # oe.i = np.random.uniform(40*mc.D2R, 60*mc.D2R,1) + oe.i = np.random.uniform(40 * mc.D2R, 60 * mc.D2R, 1) + oe.Omega = np.random.uniform(0 * mc.D2R, 20 * mc.D2R, 1) + oe.omega = np.random.uniform(0 * mc.D2R, 20 * mc.D2R, 1) + oe.f = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) rN, vN = orbitalMotion.elem2rv(mu, oe) return oe, rN, vN @@ -98,12 +97,12 @@ def sampled_boulder_gs(nominal_radius): mu = 0.3986004415e15 oe = orbitalMotion.ClassicElements() oe.a = nominal_radius - oe.e = uniform(0, 0.01, 1) - # oe.i = uniform(40*mc.D2R, 60*mc.D2R,1) - oe.i = uniform(40 * mc.D2R, 60 * mc.D2R, 1) - oe.Omega = uniform(0 * mc.D2R, 360 * mc.D2R, 1) - oe.omega = uniform(0 * mc.D2R, 360 * mc.D2R, 1) - oe.f = uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.e = np.random.uniform(0, 0.01, 1) + # oe.i = np.random.uniform(40*mc.D2R, 60*mc.D2R,1) + oe.i = np.random.uniform(40 * mc.D2R, 60 * mc.D2R, 1) + oe.Omega = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.omega = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) + oe.f = np.random.uniform(0 * mc.D2R, 360 * mc.D2R, 1) rN, vN = orbitalMotion.elem2rv(mu, oe) return oe, rN, vN @@ -456,10 +455,13 @@ def distribute_tgts(rN, vN, sim_length, utc_init, global_tgts, dt=60.0): def elrange_req(sc_pos, tgt_pos): """ Determines if the spacecraft is within the elevation and range requirements of - a target - :param sc_pos: spacecraft position expressed in the ECEF frame - :param tgt_pos: tgt_pos expressed in the ECEF frame - :return within: T/F - within el, range requirements or not + a target + + Args: + sc_pos: spacecraft position expressed in the ECEF frame + tgt_pos: tgt_pos expressed in the ECEF frame + Returns: + T/F - within el, range requirements or not """ # Import relevant library