From a02d23ca2c37ddfa930b90516a804a5bb01d5507 Mon Sep 17 00:00:00 2001 From: Mark Stephenson Date: Tue, 14 May 2024 16:27:39 -0600 Subject: [PATCH] Issue #140: Renaming, documentation, and data refactor --- .github/pull_request_template.md | 11 +- .github/workflows/documentation.yml | 2 +- .github/workflows/documentation_refactor.yml | 44 + .gitignore | 19 +- .isort.cfg | 2 +- .ruff.toml | 2 +- README.md | 15 +- deprecated/environments/agile_eos/gym_env.py | 2 +- .../multisat_agile_eos/bsk_sim.py | 6 +- .../multisat_agile_eos/gym_env.py | 4 +- .../environments/multisensor_eos/gym_env.py | 4 +- deprecated/environments/simple_eos/gym_env.py | 2 +- .../small_body_science/gym_env.py | 2 +- .../small_body_science_pomdp/bsk_sim.py | 2 +- .../small_body_science_pomdp/gym_env.py | 4 +- .../network_validation_multiprocessing.py | 2 +- docs/Makefile | 2 +- docs/build/doctrees/nbsphinx/README.txt | 3 + .../nbsphinx/examples/multiagent_envs.ipynb | 1342 +++ .../nbsphinx/examples/rllib_training.ipynb | 8248 +++++++++++++++++ .../examples/satellite_configuration.ipynb | 1152 +++ .../examples/simple_environment.ipynb | 1187 +++ docs/source/_images/static/Basilisk-Logo.png | Bin 65064 -> 0 bytes docs/source/_images/static/bsk_rl-logo.png | Bin 0 -> 206259 bytes docs/source/_static/custom.css | 216 +- docs/source/conf.py | 167 +- docs/source/index.rst | 76 +- docs/source/install.rst | 54 +- docs/source/release_notes.rst | 18 + examples/_default.rst | 10 + examples/configurations/aeos.py | 44 - examples/configurations/multisat_aeos.py | 0 examples/configurations/safety_eos.py | 0 examples/configurations/todo | 0 examples/multiagent_envs.ipynb | 358 + examples/rllib_training.ipynb | 323 + examples/satellite_configuration.ipynb | 497 + examples/simple_environment.ipynb | 234 + examples/tutorials/multisat_aeos.py | 114 - examples/tutorials/rllib_train.py | 0 examples/tutorials/satellite_customization.py | 212 - examples/tutorials/shield.py | 0 examples/tutorials/single_sat.py | 91 - pyproject.toml | 14 +- src/.ruff.toml | 3 +- src/bsk_rl/__init__.py | 26 +- src/bsk_rl/_default.rst | 51 + src/bsk_rl/act/__init__.py | 66 + src/bsk_rl/act/actions.py | 115 + .../actions.py => act/discrete_actions.py} | 198 +- ...ck_bsk_version.py => check_bsk_version.py} | 5 +- src/bsk_rl/comm/__init__.py | 66 + .../{env/scenario => comm}/communication.py | 106 +- src/bsk_rl/data/__init__.py | 90 + src/bsk_rl/data/base.py | 186 + src/bsk_rl/data/nadir_data.py | 113 + src/bsk_rl/data/no_data.py | 56 + src/bsk_rl/data/unique_image_data.py | 178 + src/bsk_rl/env/__init__.py | 0 src/bsk_rl/env/scenario/__init__.py | 0 src/bsk_rl/env/scenario/data.py | 527 -- src/bsk_rl/env/simulation/__init__.py | 0 src/bsk_rl/env/simulation/environment.py | 313 - src/bsk_rl/env/types.py | 12 - .../{_finish_install.py => finish_install.py} | 9 +- src/bsk_rl/{env/gym_env.py => gym.py} | 208 +- src/bsk_rl/obs/__init__.py | 45 + .../{env/scenario => obs}/observations.py | 117 +- src/bsk_rl/sats/__init__.py | 103 + .../access_satellite.py} | 545 +- src/bsk_rl/sats/satellite.py | 300 + src/bsk_rl/scene/__init__.py | 20 + src/bsk_rl/scene/scenario.py | 40 + .../targets.py} | 131 +- src/bsk_rl/sim/__init__.py | 35 + .../simulation/dynamics.py => sim/dyn.py} | 649 +- src/bsk_rl/{env/simulation => sim}/fsw.py | 376 +- .../{env/simulation => sim}/simulator.py | 66 +- src/bsk_rl/sim/world.py | 397 + src/bsk_rl/training/mcts_learn | 0 src/bsk_rl/training/rllib | 0 src/bsk_rl/training/shields | 0 src/bsk_rl/training/todo | 0 src/bsk_rl/utils/__init__.py | 10 + src/bsk_rl/utils/actuator_primitives.py | 34 +- src/bsk_rl/utils/attitude.py | 29 +- src/bsk_rl/utils/functional.py | 113 +- src/bsk_rl/utils/logging_config.py | 6 +- src/bsk_rl/utils/orbital.py | 69 +- src/bsk_rl/utils/rllib.py | 83 + tests/examples/test_tutorials.py | 13 - .../test_int_actions.py} | 55 +- .../test_int_communication.py | 20 +- .../{env/scenario => data}/test_int_data.py | 0 .../env/simulation/test_int_environment.py | 1 - .../test_int_observations.py} | 69 +- .../scenario => sats}/test_int_satellites.py | 19 +- .../test_int_scenarios.py} | 25 +- .../simulation => sim}/test_int_dynamics.py | 32 +- .../{env/simulation => sim}/test_int_fsw.py | 0 tests/integration/sim/test_int_world.py | 1 + .../{env => }/test_int_full_environments.py | 23 +- .../integration/{env => }/test_int_gym_env.py | 35 +- .../{env/scenario => act}/test_actions.py | 20 +- .../scenario => comm}/test_communication.py | 49 +- tests/unittest/data/test_data.py | 289 + tests/unittest/env/scenario/test_data.py | 280 - .../env/simulation/test_environment.py | 165 - .../scenario => obs}/test_observations.py | 27 +- .../test_access_satellite.py} | 271 +- tests/unittest/sats/test_satellite.py | 135 + .../test_scenario.py} | 63 +- .../{env/simulation => sim}/test_dynamics.py | 115 +- .../{env/simulation => sim}/test_fsw.py | 18 +- .../{env/simulation => sim}/test_simulator.py | 10 +- tests/unittest/sim/test_world.py | 161 + tests/unittest/{env => }/test_gym_env.py | 278 +- .../{env => }/utils/test_functional.py | 48 - .../unittest/{env => }/utils/test_orbital.py | 6 +- 119 files changed, 18139 insertions(+), 4070 deletions(-) create mode 100644 .github/workflows/documentation_refactor.yml create mode 100644 docs/build/doctrees/nbsphinx/README.txt create mode 100644 docs/build/doctrees/nbsphinx/examples/multiagent_envs.ipynb create mode 100644 docs/build/doctrees/nbsphinx/examples/rllib_training.ipynb create mode 100644 docs/build/doctrees/nbsphinx/examples/satellite_configuration.ipynb create mode 100644 docs/build/doctrees/nbsphinx/examples/simple_environment.ipynb delete mode 100644 docs/source/_images/static/Basilisk-Logo.png create mode 100644 docs/source/_images/static/bsk_rl-logo.png create mode 100644 docs/source/release_notes.rst create mode 100644 examples/_default.rst delete mode 100644 examples/configurations/aeos.py delete mode 100644 examples/configurations/multisat_aeos.py delete mode 100644 examples/configurations/safety_eos.py delete mode 100644 examples/configurations/todo create mode 100644 examples/multiagent_envs.ipynb create mode 100644 examples/rllib_training.ipynb create mode 100644 examples/satellite_configuration.ipynb create mode 100644 examples/simple_environment.ipynb delete mode 100644 examples/tutorials/multisat_aeos.py delete mode 100644 examples/tutorials/rllib_train.py delete mode 100644 examples/tutorials/satellite_customization.py delete mode 100644 examples/tutorials/shield.py delete mode 100644 examples/tutorials/single_sat.py create mode 100644 src/bsk_rl/_default.rst create mode 100644 src/bsk_rl/act/__init__.py create mode 100644 src/bsk_rl/act/actions.py rename src/bsk_rl/{env/scenario/actions.py => act/discrete_actions.py} (57%) rename src/bsk_rl/{_check_bsk_version.py => check_bsk_version.py} (89%) create mode 100644 src/bsk_rl/comm/__init__.py rename src/bsk_rl/{env/scenario => comm}/communication.py (53%) create mode 100644 src/bsk_rl/data/__init__.py create mode 100644 src/bsk_rl/data/base.py create mode 100644 src/bsk_rl/data/nadir_data.py create mode 100644 src/bsk_rl/data/no_data.py create mode 100644 src/bsk_rl/data/unique_image_data.py delete mode 100644 src/bsk_rl/env/__init__.py delete mode 100644 src/bsk_rl/env/scenario/__init__.py delete mode 100644 src/bsk_rl/env/scenario/data.py delete mode 100644 src/bsk_rl/env/simulation/__init__.py delete mode 100644 src/bsk_rl/env/simulation/environment.py delete mode 100644 src/bsk_rl/env/types.py rename src/bsk_rl/{_finish_install.py => finish_install.py} (54%) rename src/bsk_rl/{env/gym_env.py => gym.py} (74%) create mode 100644 src/bsk_rl/obs/__init__.py rename src/bsk_rl/{env/scenario => obs}/observations.py (81%) create mode 100644 src/bsk_rl/sats/__init__.py rename src/bsk_rl/{env/scenario/satellites.py => sats/access_satellite.py} (53%) create mode 100644 src/bsk_rl/sats/satellite.py create mode 100644 src/bsk_rl/scene/__init__.py create mode 100644 src/bsk_rl/scene/scenario.py rename src/bsk_rl/{env/scenario/environment_features.py => scene/targets.py} (54%) create mode 100644 src/bsk_rl/sim/__init__.py rename src/bsk_rl/{env/simulation/dynamics.py => sim/dyn.py} (57%) rename src/bsk_rl/{env/simulation => sim}/fsw.py (69%) rename src/bsk_rl/{env/simulation => sim}/simulator.py (55%) create mode 100644 src/bsk_rl/sim/world.py delete mode 100644 src/bsk_rl/training/mcts_learn delete mode 100644 src/bsk_rl/training/rllib delete mode 100644 src/bsk_rl/training/shields delete mode 100644 src/bsk_rl/training/todo create mode 100644 src/bsk_rl/utils/rllib.py delete mode 100644 tests/examples/test_tutorials.py rename tests/integration/{env/scenario/test_int_sat_actions.py => act/test_int_actions.py} (80%) rename tests/integration/{env/scenario => comm}/test_int_communication.py (87%) rename tests/integration/{env/scenario => data}/test_int_data.py (100%) delete mode 100644 tests/integration/env/simulation/test_int_environment.py rename tests/integration/{env/scenario/test_int_sat_observations.py => obs/test_int_observations.py} (77%) rename tests/integration/{env/scenario => sats}/test_int_satellites.py (76%) rename tests/integration/{env/scenario/test_int_environment_features.py => scene/test_int_scenarios.py} (65%) rename tests/integration/{env/simulation => sim}/test_int_dynamics.py (74%) rename tests/integration/{env/simulation => sim}/test_int_fsw.py (100%) create mode 100644 tests/integration/sim/test_int_world.py rename tests/integration/{env => }/test_int_full_environments.py (80%) rename tests/integration/{env => }/test_int_gym_env.py (81%) rename tests/unittest/{env/scenario => act}/test_actions.py (89%) rename tests/unittest/{env/scenario => comm}/test_communication.py (74%) create mode 100644 tests/unittest/data/test_data.py delete mode 100644 tests/unittest/env/scenario/test_data.py delete mode 100644 tests/unittest/env/simulation/test_environment.py rename tests/unittest/{env/scenario => obs}/test_observations.py (90%) rename tests/unittest/{env/scenario/test_satellites.py => sats/test_access_satellite.py} (58%) create mode 100644 tests/unittest/sats/test_satellite.py rename tests/unittest/{env/scenario/test_environment_features.py => scene/test_scenario.py} (73%) rename tests/unittest/{env/simulation => sim}/test_dynamics.py (73%) rename tests/unittest/{env/simulation => sim}/test_fsw.py (87%) rename tests/unittest/{env/simulation => sim}/test_simulator.py (88%) create mode 100644 tests/unittest/sim/test_world.py rename tests/unittest/{env => }/test_gym_env.py (69%) rename tests/unittest/{env => }/utils/test_functional.py (78%) rename tests/unittest/{env => }/utils/test_orbital.py (95%) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 91d433f9..3588b18d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,9 +17,9 @@ Please include a summary, motivation, and context of the changes and the related Please describe the tests that you ran to verify your changes. ### Passes Tests -- [ ] __Unit tests__ (General Environment only) `pytest --cov bsk_rl/env --cov-report term-missing tests/unittest` -- [ ] __Integrated tests__ (General Environment only) `pytest --cov bsk_rl/env --cov-report term-missing tests/integration` -- [ ] __Examples__ (General Environment only) `pytest tests/examples` +- [ ] __Unit tests__ `pytest --cov bsk_rl --cov-report term-missing tests/unittest` +- [ ] __Integrated tests__ `pytest --cov bsk_rl --cov-report term-missing tests/integration` +- [ ] __Documentation builds__ `cd docs; make html` ### Test Configuration - Python: @@ -30,8 +30,9 @@ Please describe the tests that you ran to verify your changes. - [ ] My code follows the style guidelines of this project (passes Black, ruff, and isort) - [ ] I have performed a self-review of my code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation +- [ ] I have commented my code in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation and release notes - [ ] Commit messages are atomic, are in the form `Issue #XXX: Message` and have a useful message - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] If I changed an example ipynb, I have locally rebuilt the documentation diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 48380cd6..da4939be 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -25,7 +25,7 @@ jobs: cp docs/sitecustomize.py $(python -c 'import site; print(site.getsitepackages()[0])')/sitecustomize.py - name: Install dependencies run: | - pip install -e . + pip install -e '.[docs]' # skip finish install steps - name: Sphinx build run: | diff --git a/.github/workflows/documentation_refactor.yml b/.github/workflows/documentation_refactor.yml new file mode 100644 index 00000000..626b98c8 --- /dev/null +++ b/.github/workflows/documentation_refactor.yml @@ -0,0 +1,44 @@ +name: Documentation + +on: + push: + branches: + - refactor/v1_0_0 + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: write + pages: write + id-token: write + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: pandoc/actions/setup@main + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Mock Basilisk + run: | + cp docs/sitecustomize.py $(python -c 'import site; print(site.getsitepackages()[0])')/sitecustomize.py + - name: Install dependencies + run: | + pip install -e '.[docs,rllib]' + # skip finish install steps + - name: Sphinx build + run: | + cd docs + make html + cd .. + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/build/html + force_orphan: true + publish_branch: gh-pages-refactor + + diff --git a/.gitignore b/.gitignore index be8f909a..acdb8a17 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ __pycache__/ # Distribution / packaging .Python bin/ -build/ develop-eggs/ dist/ downloads/ @@ -66,11 +65,6 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ -docs/source/API Reference -docs/source/Examples - # Pickles *.pkl @@ -125,4 +119,15 @@ bsk_rl/results/ .DS_Store # data files -src/bsk_rl/data/simplemaps_worldcities/ +src/bsk_rl/_dat/simplemaps_worldcities/ + +# Sphinx documentation +/docs/build/* +/docs/source/api_reference/* +/docs/source/examples/* + +# executed ipynb cache +!/docs/build/doctrees +docs/build/doctrees/* +!/docs/build/doctrees/nbsphinx +!/docs/build/doctrees/nbsphinx/* \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg index b9fb3f3e..bf7b9fbd 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,2 @@ [settings] -profile=black +profile=black \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml index 7ba5db15..99b1bc10 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1 +1 @@ -ignore-init-module-imports = true +extend-include = ["*.ipynb"] diff --git a/README.md b/README.md index b768cd0e..1ed913d5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ -# BSK-RL: Environments and Algorithms for Spacecraft Planning and Scheduling -[BSK-RL](https://avslab.github.io/bsk_rl/) ([Basilisk](https://hanspeterschaub.info/basilisk) + [Reinforcement Learning](https://en.wikipedia.org/wiki/Reinforcement_learning)) is a Python package for constructing [Gymnasium](https://gymnasium.farama.org/index.html) environments for spacecraft tasking problems. It is built on top of [Basilisk](https://hanspeterschaub.info/basilisk), a modular and fast spacecraft simulation framework, making the simulation environments high-fidelity and computationally efficient. BSK-RL also includes a collection of agents, training scripts, and examples for working with these environments. +# BSK-RL: Environments for Spacecraft Planning and Scheduling + + + + +[BSK-RL](https://avslab.github.io/bsk_rl/) ([Basilisk](https://hanspeterschaub.info/basilisk) + [Reinforcement Learning](https://en.wikipedia.org/wiki/Reinforcement_learning)) is a Python package for constructing [Gymnasium](https://gymnasium.farama.org/index.html) environments for spacecraft tasking problems. It is built on top of [Basilisk](https://hanspeterschaub.info/basilisk), a modular and fast spacecraft simulation framework, making the simulation environments high-fidelity and computationally efficient. + +### Usage +Installation instructions, examples, and documentation can be found on the [BSK-RL website](https://avslab.github.io/bsk_rl/). + +### Acknowledgment BSK-RL is developed by the [Autonomous Vehicle Systems (AVS) Lab](https://hanspeterschaub.info/AVSlab.html) at the University of Colorado Boulder. -## Usage -Installation instructions, examples, and documentation can be found on the [BSK-RL website](https://avslab.github.io/bsk_rl/) (under construction). diff --git a/deprecated/environments/agile_eos/gym_env.py b/deprecated/environments/agile_eos/gym_env.py index 1e10f3ae..914cd345 100644 --- a/deprecated/environments/agile_eos/gym_env.py +++ b/deprecated/environments/agile_eos/gym_env.py @@ -3,7 +3,7 @@ from Basilisk.utilities import macros as mc from gymnasium import spaces -from bsk_rl.env.agile_eos.bsk_sim import AgileEOSSimulator +from bsk_rl.scene.agile_eos.bsk_sim import AgileEOSSimulator class AgileEOS(gym.Env): diff --git a/deprecated/environments/multisat_agile_eos/bsk_sim.py b/deprecated/environments/multisat_agile_eos/bsk_sim.py index cbf99461..12cb1098 100644 --- a/deprecated/environments/multisat_agile_eos/bsk_sim.py +++ b/deprecated/environments/multisat_agile_eos/bsk_sim.py @@ -7,9 +7,9 @@ from Basilisk.utilities import orbitalMotion, vizSupport from scipy.sparse.csgraph import connected_components -from bsk_rl.env.multisat_agile_eos.bsk_models import dynamics, environment -from bsk_rl.env.multisat_agile_eos.bsk_models import fsw as fsw_feedback -from bsk_rl.env.multisat_agile_eos.bsk_models import fsw_steering +from bsk_rl.scene.multisat_agile_eos.bsk_models import dynamics, environment +from bsk_rl.scene.multisat_agile_eos.bsk_models import fsw as fsw_feedback +from bsk_rl.scene.multisat_agile_eos.bsk_models import fsw_steering from bsk_rl.utilities.initial_conditions import leo_initial_conditions diff --git a/deprecated/environments/multisat_agile_eos/gym_env.py b/deprecated/environments/multisat_agile_eos/gym_env.py index 44242ee7..0ab2c72b 100644 --- a/deprecated/environments/multisat_agile_eos/gym_env.py +++ b/deprecated/environments/multisat_agile_eos/gym_env.py @@ -5,8 +5,8 @@ from Basilisk.utilities import macros as mc from gymnasium import spaces -from bsk_rl.env.multisat_agile_eos import env_settings -from bsk_rl.env.multisat_agile_eos.bsk_sim import MultiSatAgileEOSSimulator +from bsk_rl.scene.multisat_agile_eos import env_settings +from bsk_rl.scene.multisat_agile_eos.bsk_sim import MultiSatAgileEOSSimulator gym.utils.passive_env_checker.logger.setLevel( 40 diff --git a/deprecated/environments/multisensor_eos/gym_env.py b/deprecated/environments/multisensor_eos/gym_env.py index fdd01eb6..273fb334 100644 --- a/deprecated/environments/multisensor_eos/gym_env.py +++ b/deprecated/environments/multisensor_eos/gym_env.py @@ -4,8 +4,8 @@ import numpy as np from gymnasium import spaces -from bsk_rl.env.multisensor_eos.bsk_sim import MultiSensorEOSSimulator -from bsk_rl.env.multisensor_eos.env_settings import Settings +from bsk_rl.scene.multisensor_eos.bsk_sim import MultiSensorEOSSimulator +from bsk_rl.scene.multisensor_eos.env_settings import Settings class MultiSensorEOS(gym.Env): diff --git a/deprecated/environments/simple_eos/gym_env.py b/deprecated/environments/simple_eos/gym_env.py index bbdca269..bf5e3840 100644 --- a/deprecated/environments/simple_eos/gym_env.py +++ b/deprecated/environments/simple_eos/gym_env.py @@ -3,7 +3,7 @@ from Basilisk.utilities import macros as mc from gymnasium import spaces -from bsk_rl.env.simple_eos import bsk_sim +from bsk_rl.scene.simple_eos import bsk_sim class SimpleEOS(gym.Env): diff --git a/deprecated/environments/small_body_science/gym_env.py b/deprecated/environments/small_body_science/gym_env.py index e0875271..f1617271 100644 --- a/deprecated/environments/small_body_science/gym_env.py +++ b/deprecated/environments/small_body_science/gym_env.py @@ -2,7 +2,7 @@ import numpy as np from gymnasium import spaces -from bsk_rl.env.small_body_science.bsk_sim import SmallBodyScienceSimulator +from bsk_rl.scene.small_body_science.bsk_sim import SmallBodyScienceSimulator class SmallBodyScience(gym.Env): diff --git a/deprecated/environments/small_body_science_pomdp/bsk_sim.py b/deprecated/environments/small_body_science_pomdp/bsk_sim.py index d5c346ac..82581f41 100644 --- a/deprecated/environments/small_body_science_pomdp/bsk_sim.py +++ b/deprecated/environments/small_body_science_pomdp/bsk_sim.py @@ -5,7 +5,7 @@ from Basilisk.utilities import macros as mc from Basilisk.utilities import orbitalMotion, unitTestSupport -from bsk_rl.env.small_body_science.bsk_sim import SmallBodyScienceSimulator +from bsk_rl.scene.small_body_science.bsk_sim import SmallBodyScienceSimulator class SmallBodySciencePOMDPSimulator(SmallBodyScienceSimulator): diff --git a/deprecated/environments/small_body_science_pomdp/gym_env.py b/deprecated/environments/small_body_science_pomdp/gym_env.py index 9b8a63e6..0ce91de3 100644 --- a/deprecated/environments/small_body_science_pomdp/gym_env.py +++ b/deprecated/environments/small_body_science_pomdp/gym_env.py @@ -2,8 +2,8 @@ import numpy as np from gymnasium import spaces -from bsk_rl.env.small_body_science.gym_env import SmallBodyScience -from bsk_rl.env.small_body_science_pomdp.bsk_sim import SmallBodySciencePOMDPSimulator +from bsk_rl.scene.small_body_science.gym_env import SmallBodyScience +from bsk_rl.scene.small_body_science_pomdp.bsk_sim import SmallBodySciencePOMDPSimulator class SmallBodySciencePOMDP(SmallBodyScience): diff --git a/deprecated/examples/mcts/network_validation_multiprocessing.py b/deprecated/examples/mcts/network_validation_multiprocessing.py index 65ba4eee..de4efd62 100644 --- a/deprecated/examples/mcts/network_validation_multiprocessing.py +++ b/deprecated/examples/mcts/network_validation_multiprocessing.py @@ -6,7 +6,7 @@ import gymnasium as gym import numpy as np -from bsk_rl.env.agile_eos.gym_env import AgileEOS # noqa: F401; needed for gym +from bsk_rl.scene.agile_eos.gym_env import AgileEOS # noqa: F401; needed for gym os.environ["CUDA_VISIBLE_DEVICES"] = "-1" diff --git a/docs/Makefile b/docs/Makefile index 311aaf07..bbb99d99 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,7 +20,7 @@ help: @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: - rm -rf "source/Examples" "source/API Reference" + rm -rf "source/examples" "source/api_reference" "build" view: diff --git a/docs/build/doctrees/nbsphinx/README.txt b/docs/build/doctrees/nbsphinx/README.txt new file mode 100644 index 00000000..fb9b64bf --- /dev/null +++ b/docs/build/doctrees/nbsphinx/README.txt @@ -0,0 +1,3 @@ +This directory includes executed copies of example notebooks so that they do not have to +run on GitHub actions. To generate these, build the docs locally by running `make html` +from the `docs` directory. \ No newline at end of file diff --git a/docs/build/doctrees/nbsphinx/examples/multiagent_envs.ipynb b/docs/build/doctrees/nbsphinx/examples/multiagent_envs.ipynb new file mode 100644 index 00000000..6330ce8a --- /dev/null +++ b/docs/build/doctrees/nbsphinx/examples/multiagent_envs.ipynb @@ -0,0 +1,1342 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Agent Environments\n", + "\n", + "Two multiagent environments are given in the package:\n", + "\n", + "* [GeneralSatelliteTasking](../api_reference/index.rst#bsk_rl.GeneralSatelliteTasking), \n", + " a [Gymnasium](https://gymnasium.farama.org)-based environment and the basis for all other environments.\n", + "* [ConstellationTasking](../api_reference/index.rst#bsk_rl.ConstellationTasking), which\n", + " implements the [PettingZoo parallel API](https://pettingzoo.farama.org/api/parallel/).\n", + "\n", + "The latter is preferable for multi-agent RL (MARL) settings, as most algorithms are designed\n", + "for this kind of API.\n", + "\n", + "## Configuring the Environment\n", + "\n", + "For this example, a multisatellite target imaging environment will be used. The goal is\n", + "to maximize the value of unique images taken.\n", + "\n", + "As usual, the satellite type is defined first." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:48.425654Z", + "iopub.status.busy": "2024-05-31T23:24:48.425317Z", + "iopub.status.idle": "2024-05-31T23:24:49.387782Z", + "shell.execute_reply": "2024-05-31T23:24:49.387487Z" + } + }, + "outputs": [], + "source": [ + "from bsk_rl import sats, act, obs, scene, data, comm\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "class ImagingSatellite(sats.ImagingSatellite):\n", + " observation_spec = [\n", + " obs.OpportunityProperties(\n", + " dict(prop=\"priority\"), \n", + " dict(prop=\"opportunity_open\", norm=5700.0),\n", + " n_ahead_observe=10,\n", + " )\n", + " ]\n", + " action_spec = [act.Image(n_ahead_image=10)]\n", + " dyn_type = dyn.FullFeaturedDynModel\n", + " fsw_type = fsw.SteeringImagerFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Satellite properties are set to give the satellite near-unlimited power and storage\n", + "resources, and put the satellite at a 800 km orbit." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.389660Z", + "iopub.status.busy": "2024-05-31T23:24:49.389501Z", + "iopub.status.idle": "2024-05-31T23:24:49.391573Z", + "shell.execute_reply": "2024-05-31T23:24:49.391353Z" + } + }, + "outputs": [], + "source": [ + "\n", + "from bsk_rl.utils.orbital import random_orbit\n", + "\n", + "sat_args = dict(\n", + " imageAttErrorRequirement=0.01,\n", + " imageRateErrorRequirement=0.01,\n", + " batteryStorageCapacity=1e9,\n", + " storedCharge_Init=1e9,\n", + " dataStorageCapacity=1e12,\n", + " u_max=0.4,\n", + " K1=0.25,\n", + " K3=3.0,\n", + " omega_max=0.087,\n", + " servo_Ki=5.0,\n", + " servo_P=150 / 5,\n", + " oe=lambda: random_orbit(alt=800),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Gym API\n", + "\n", + "GeneralSatelliteTasking uses tuples of actions and observations to interact with the\n", + "environment." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.392927Z", + "iopub.status.busy": "2024-05-31T23:24:49.392834Z", + "iopub.status.idle": "2024-05-31T23:24:49.632571Z", + "shell.execute_reply": "2024-05-31T23:24:49.632286Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,395 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=3731917456\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,395 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 1000 targets\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,559 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,581 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,604 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,628 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-1_4594229376', 'EO-2_4594228272', 'EO-3_11383596672']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,628 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "Tuple(Box(-1e+16, 1e+16, (20,), float64), Box(-1e+16, 1e+16, (20,), float64), Box(-1e+16, 1e+16, (20,), float64))" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bsk_rl import GeneralSatelliteTasking\n", + "\n", + "env = GeneralSatelliteTasking(\n", + " satellites=[\n", + " ImagingSatellite(\"EO-1\", sat_args),\n", + " ImagingSatellite(\"EO-2\", sat_args),\n", + " ImagingSatellite(\"EO-3\", sat_args),\n", + " ],\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " communicator=comm.LOSCommunication(), # Note that dyn must inherit from LOSCommunication\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.observation_space" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.634093Z", + "iopub.status.busy": "2024-05-31T23:24:49.633994Z", + "iopub.status.idle": "2024-05-31T23:24:49.636359Z", + "shell.execute_reply": "2024-05-31T23:24:49.636079Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Tuple(Discrete(10), Discrete(10), Discrete(10))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.action_space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Consequently, actions are passed as a tuple. The step will stop the first time any\n", + "satellite completes an action." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.637912Z", + "iopub.status.busy": "2024-05-31T23:24:49.637795Z", + "iopub.status.idle": "2024-05-31T23:24:49.679079Z", + "shell.execute_reply": "2024-05-31T23:24:49.678845Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,638 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,638 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,638 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-48) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,639 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-48) window enabled: 128.7 to 311.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,640 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[msetting timed terminal event at 311.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,640 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mtarget index 9 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,640 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-358) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,641 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-358) window enabled: 274.0 to 465.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,641 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[msetting timed terminal event at 465.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,641 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mtarget index 8 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,641 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-492) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,642 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-492) window enabled: 360.4 to 445.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,642 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[msetting timed terminal event at 445.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,643 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 1000000000.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,670 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mimaged Target(tgt-48)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,672 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[mData reward: {'EO-1_4594229376': 0.8490970043045871, 'EO-2_4594228272': 0.0, 'EO-3_11383596672': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,677 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-1_4594229376']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,677 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[mStep reward: 0.8490970043045871\u001b[0m\n" + ] + } + ], + "source": [ + "observation, reward, terminated, truncated, info = env.step([7, 9, 8])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.680564Z", + "iopub.status.busy": "2024-05-31T23:24:49.680455Z", + "iopub.status.idle": "2024-05-31T23:24:49.682667Z", + "shell.execute_reply": "2024-05-31T23:24:49.682453Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([ 0.92952453, -0.02171651, 0.09485104, -0.01888697, 0.69626008,\n", + " -0.01323879, 0.0592699 , -0.01956626, 0.31841486, 0.02726043,\n", + " 0.26016688, 0.03952533, 0.32174418, 0.04452711, 0.60532024,\n", + " 0.05192149, 0.64168396, 0.05674144, 0.59718288, 0.04868392]),\n", + " array([ 0.46677668, -0.02298246, 0.26183512, -0.00185962, 0.12937522,\n", + " -0.00525075, 0.31902371, 0.00385037, 0.22778555, 0.00161488,\n", + " 0.94978415, 0.01525485, 0.83953352, 0.01127847, 0.54275875,\n", + " 0.02438937, 0.46276273, 0.02508408, 0.38153857, 0.02664706]),\n", + " array([ 0.52066306, -0.02298246, 0.95595429, -0.01953964, 0.90617328,\n", + " -0.00112268, 0.78384523, 0.01969529, 0.13597152, 0.01157903,\n", + " 0.63195448, 0.0099964 , 0.0967253 , 0.04024514, 0.21411318,\n", + " 0.03755702, 0.6472315 , 0.04509715, 0.77396428, 0.0801271 ]))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point, either every satellite can be retasked, or satellites can continue their\n", + "previous action by passing `None` as the action. To see which satellites must be\n", + "retasked (i.e. their previous action is done and they have nothing more to do), look at\n", + "`info[\"requires_retasking\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.684082Z", + "iopub.status.busy": "2024-05-31T23:24:49.683977Z", + "iopub.status.idle": "2024-05-31T23:24:49.685934Z", + "shell.execute_reply": "2024-05-31T23:24:49.685717Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['EO-1_4594229376']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "info[\"requires_retasking\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on this list, we decide here to only retask the satellite that needs it." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.687262Z", + "iopub.status.busy": "2024-05-31T23:24:49.687168Z", + "iopub.status.idle": "2024-05-31T23:24:49.689261Z", + "shell.execute_reply": "2024-05-31T23:24:49.689024Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, None, None]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "actions = [None, None, None]\n", + "actions[int(info[\"requires_retasking\"][0][3]) - 1] = 7\n", + "actions" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.690566Z", + "iopub.status.busy": "2024-05-31T23:24:49.690473Z", + "iopub.status.idle": "2024-05-31T23:24:49.778479Z", + "shell.execute_reply": "2024-05-31T23:24:49.778234Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,690 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,691 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,691 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-565) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,692 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-565) window enabled: 427.0 to 596.3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,692 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[msetting timed terminal event at 596.3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,692 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<131.00> \u001b[0m\u001b[mRunning simulation at most to 1000000131.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,721 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mimaged Target(tgt-358)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,722 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[mData reward: {'EO-1_4594229376': 0.0, 'EO-2_4594228272': 0.46276272986490175, 'EO-3_11383596672': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,726 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,752 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,776 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-2_4594228272']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,776 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[mStep reward: 0.46276272986490175\u001b[0m\n" + ] + } + ], + "source": [ + "observation, reward, terminated, truncated, info = env.step(actions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this environment, the environment will stop if any agent dies. To demonstrate this,\n", + "one satellite is forcibly killed." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.780055Z", + "iopub.status.busy": "2024-05-31T23:24:49.779953Z", + "iopub.status.idle": "2024-05-31T23:24:49.849423Z", + "shell.execute_reply": "2024-05-31T23:24:49.849091Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,781 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,781 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mtarget index 6 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,781 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-266) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,782 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-266) window enabled: 503.7 to 672.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,782 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[msetting timed terminal event at 672.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,783 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,783 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-506) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,784 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-506) window enabled: 425.6 to 618.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,784 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[msetting timed terminal event at 618.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,784 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mtarget index 9 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,784 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-739) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,785 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-739) window enabled: 453.6 to 600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,785 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[msetting timed terminal event at 600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,785 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<276.00> \u001b[0m\u001b[mRunning simulation at most to 1000000276.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,816 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mimaged Target(tgt-506)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,818 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[mData reward: {'EO-1_4594229376': 0.0, 'EO-2_4594228272': 0.539840444868018, 'EO-3_11383596672': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,822 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,846 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mfailed battery_valid check\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,847 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-2_4594228272']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,847 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[mStep reward: -0.46015955513198203\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,847 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[mEpisode terminated: True\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,847 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<428.00> \u001b[0m\u001b[mEpisode truncated: False\u001b[0m\n" + ] + } + ], + "source": [ + "from Basilisk.architecture import messaging\n", + "\n", + "def isnt_alive(log_failure=False):\n", + " \"\"\"Mock satellite 0 dying.\"\"\"\n", + " self = env.unwrapped.satellites[0]\n", + " death_message = messaging.PowerStorageStatusMsgPayload()\n", + " death_message.storageLevel = 0.0\n", + " self.dynamics.powerMonitor.batPowerOutMsg.write(death_message)\n", + " return self.dynamics.is_alive(log_failure=log_failure) and self.fsw.is_alive(\n", + " log_failure=log_failure\n", + " )\n", + "\n", + "env.unwrapped.satellites[0].is_alive = isnt_alive\n", + "observation, reward, terminated, truncated, info = env.step([6, 7, 9])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PettingZoo API\n", + "\n", + "The [PettingZoo parallel API](https://pettingzoo.farama.org/api/parallel/) environment, \n", + "ConstellationTasking, is largely the same as GeneralSatelliteTasking. See their\n", + "documentation for a full description of the API. It tends to separate things into\n", + "dictionaries keyed by agent, rather than tuples." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:49.851091Z", + "iopub.status.busy": "2024-05-31T23:24:49.851004Z", + "iopub.status.idle": "2024-05-31T23:24:50.305325Z", + "shell.execute_reply": "2024-05-31T23:24:50.305060Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:49,853 \u001b[0m\u001b[m \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[93mCreating logger for new env on PID=90763. Old environments in process may now log times incorrectly.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,058 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=100802160\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,059 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 1000 targets\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,214 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,235 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,256 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,280 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,301 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-1_11385528784', 'EO-2_11385529888', 'EO-3_11385529456']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,302 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'EO-1_11385528784': Box(-1e+16, 1e+16, (20,), float64),\n", + " 'EO-2_11385529888': Box(-1e+16, 1e+16, (20,), float64),\n", + " 'EO-3_11385529456': Box(-1e+16, 1e+16, (20,), float64)}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bsk_rl import ConstellationTasking\n", + "\n", + "env = ConstellationTasking(\n", + " satellites=[\n", + " ImagingSatellite(\"EO-1\", sat_args),\n", + " ImagingSatellite(\"EO-2\", sat_args),\n", + " ImagingSatellite(\"EO-3\", sat_args),\n", + " ],\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " communicator=comm.LOSCommunication(), # Note that dyn must inherit from LOSCommunication\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.observation_spaces" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:50.306756Z", + "iopub.status.busy": "2024-05-31T23:24:50.306667Z", + "iopub.status.idle": "2024-05-31T23:24:50.308741Z", + "shell.execute_reply": "2024-05-31T23:24:50.308477Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'EO-1_11385528784': Discrete(10),\n", + " 'EO-2_11385529888': Discrete(10),\n", + " 'EO-3_11385529456': Discrete(10)}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.action_spaces" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Actions are passed as a dictionary; the agent names can be accessed through the `agents`\n", + "property." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:50.310153Z", + "iopub.status.busy": "2024-05-31T23:24:50.310075Z", + "iopub.status.idle": "2024-05-31T23:24:50.415334Z", + "shell.execute_reply": "2024-05-31T23:24:50.415115Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,311 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,312 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,312 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-225) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,313 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[mTarget(tgt-225) window enabled: 549.0 to 711.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,313 \u001b[0m\u001b[36msats.satellite.EO-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO-1: \u001b[0m\u001b[msetting timed terminal event at 711.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,313 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mtarget index 9 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,313 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-84) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,314 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-84) window enabled: 280.2 to 446.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,314 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[msetting timed terminal event at 446.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,314 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mtarget index 8 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,315 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-905) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,315 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-905) window enabled: 410.5 to 600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,315 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[msetting timed terminal event at 600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,316 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 1000000000.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,373 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mimaged Target(tgt-84)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,375 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mData reward: {'EO-1_11385528784': 0.0, 'EO-2_11385529888': 0.1736317989411481, 'EO-3_11385529456': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,382 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,412 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-2_11385529888']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,413 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mStep reward: {'EO-1_11385528784': 0.0, 'EO-2_11385529888': 0.1736317989411481, 'EO-3_11385529456': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,413 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mEpisode terminated: {'EO-1_11385528784': False, 'EO-2_11385529888': False, 'EO-3_11385529456': False}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,413 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mEpisode truncated: {'EO-1_11385528784': False, 'EO-2_11385529888': False, 'EO-3_11385529456': False}\u001b[0m\n" + ] + } + ], + "source": [ + "observation, reward, terminated, truncated, info = env.step(\n", + " {\n", + " env.agents[0]: 7,\n", + " env.agents[1]: 9,\n", + " env.agents[2]: 8,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:50.416817Z", + "iopub.status.busy": "2024-05-31T23:24:50.416737Z", + "iopub.status.idle": "2024-05-31T23:24:50.418835Z", + "shell.execute_reply": "2024-05-31T23:24:50.418560Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'EO-1_11385528784': array([ 0.60338287, -0.03011611, 0.87323203, 0.00419951, 0.80172144,\n", + " 0.00750755, 0.37153971, 0.04032373, 0.61646756, 0.04666259,\n", + " 0.48161493, 0.04785926, 0.47384484, 0.07925763, 0.0035083 ,\n", + " 0.07716695, 0.00291903, 0.08894771, 0.50867957, 0.12433105]),\n", + " 'EO-2_11385529888': array([ 0.63433108, -0.03101471, 0.23779057, -0.01630323, 0.58209618,\n", + " -0.02437096, 0.50745344, -0.02125649, 0.96829724, -0.01826611,\n", + " 0.9566993 , -0.00644389, 0.59672177, 0.01027947, 0.36671403,\n", + " 0.03685701, 0.84354774, 0.01354962, 0.41031534, 0.04520229]),\n", + " 'EO-3_11385529456': array([ 0.18204768, -0.01113924, 0.29690295, 0.00256507, 0.76019462,\n", + " 0.02354056, 0.53067777, 0.03641876, 0.32337915, 0.04132224,\n", + " 0.27812719, 0.023271 , 0.96258472, 0.02236515, 0.05312964,\n", + " 0.03625353, 0.99471926, 0.07202423, 0.80033185, 0.05908512])}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other than compatibility with MARL algorithms, the main benefit of the PettingZoo API\n", + "is that it allows for individual agents to fail without terminating the entire environment." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:50.420306Z", + "iopub.status.busy": "2024-05-31T23:24:50.420228Z", + "iopub.status.idle": "2024-05-31T23:24:50.422613Z", + "shell.execute_reply": "2024-05-31T23:24:50.422387Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['EO-2_11385529888', 'EO-3_11385529456']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Immediately kill satellite 0\n", + "env.unwrapped.satellites[0].is_alive = isnt_alive\n", + "env.agents" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T23:24:50.423873Z", + "iopub.status.busy": "2024-05-31T23:24:50.423781Z", + "iopub.status.idle": "2024-05-31T23:24:50.508872Z", + "shell.execute_reply": "2024-05-31T23:24:50.508425Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,424 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,425 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,425 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-420) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,426 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mTarget(tgt-420) window enabled: 493.1 to 549.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,426 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[msetting timed terminal event at 549.7\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,426 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mtarget index 9 tasked\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,427 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-405) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,427 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[mTarget(tgt-405) window enabled: 619.8 to 745.3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,427 \u001b[0m\u001b[34msats.satellite.EO-3 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[34mEO-3: \u001b[0m\u001b[msetting timed terminal event at 745.3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,428 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<283.00> \u001b[0m\u001b[mRunning simulation at most to 1000000283.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,470 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mimaged Target(tgt-420)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,472 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[mData reward: {'EO-1_11385528784': 0.0, 'EO-2_11385529888': 0.3667140288589458, 'EO-3_11385529456': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,477 \u001b[0m\u001b[92msats.satellite.EO-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[92mEO-2: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,505 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO-2_11385529888']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[mStep reward: {'EO-2_11385529888': 0.3667140288589458, 'EO-3_11385529456': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[mEpisode terminated: {'EO-2_11385529888': False, 'EO-3_11385529456': False}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 16:24:50,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<496.00> \u001b[0m\u001b[mEpisode truncated: {'EO-2_11385529888': False, 'EO-3_11385529456': False}\u001b[0m\n" + ] + } + ], + "source": [ + "observation, reward, terminated, truncated, info = env.step({\n", + " env.agents[0]: 7,\n", + " env.agents[1]: 9,\n", + " }\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/build/doctrees/nbsphinx/examples/rllib_training.ipynb b/docs/build/doctrees/nbsphinx/examples/rllib_training.ipynb new file mode 100644 index 00000000..9a415119 --- /dev/null +++ b/docs/build/doctrees/nbsphinx/examples/rllib_training.ipynb @@ -0,0 +1,8248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Training with RLlib PPO\n", + "[RLlib](https://docs.ray.io/en/latest/rllib/index.html) is a high-performance, distributed\n", + "reinforcement learning library. It is preferable to other RL libraries (e.g. Stable Baselines\n", + "3) for `bsk_rl` environments because it steps environments copies asynchronously; because \n", + "of the variable step lengths, variable episode step counts, and long episode reset times, \n", + "stepping each environment independently can increase step throughput by 2-5 times.\n", + "\n", + "
\n", + "\n", + "**Warning:** RLlib currently has a bug that results in an undesirable timeout which stops\n", + "training. Check here to see if it has been resolved: https://github.com/ray-project/ray/pull/45147\n", + "\n", + "
\n", + "\n", + "\n", + "## Define the Environment\n", + "A nadir-scanning environment is created, to the one used in [this paper](https://hanspeterschaub.info/Papers/Stephenson2024.pdf). \n", + "The satellite has to collect data while managing the data buffer level and battery level.\n", + "\n", + "First, the satellite class is defined. A custom dynamics model is created that defines\n", + "a few additional properties to use in the state." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:41.365086Z", + "iopub.status.busy": "2024-05-31T20:59:41.364918Z", + "iopub.status.idle": "2024-05-31T20:59:42.280245Z", + "shell.execute_reply": "2024-05-31T20:59:42.279937Z" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from bsk_rl import act, data, obs, sats, scene\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "class ScanningDownlinkDynModel(dyn.ContinuousImagingDynModel, dyn.GroundStationDynModel):\n", + " # Define some custom properties to be accessed in the state\n", + " @property\n", + " def instrument_pointing_error(self) -> float:\n", + " r_BN_P_unit = self.r_BN_P/np.linalg.norm(self.r_BN_P) \n", + " c_hat_P = self.satellite.fsw.c_hat_P\n", + " return np.arccos(np.dot(-r_BN_P_unit, c_hat_P))\n", + " \n", + " @property\n", + " def solar_pointing_error(self) -> float:\n", + " a = self.world.gravFactory.spiceObject.planetStateOutMsgs[\n", + " self.world.sun_index\n", + " ].read().PositionVector\n", + " a_hat_N = a / np.linalg.norm(a)\n", + " nHat_B = self.satellite.sat_args[\"nHat_B\"]\n", + " NB = np.transpose(self.BN)\n", + " nHat_N = NB @ nHat_B\n", + " return np.arccos(np.dot(nHat_N, a_hat_N))\n", + "\n", + "class ScanningSatellite(sats.AccessSatellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " dict(prop=\"storage_level_fraction\"),\n", + " dict(prop=\"battery_charge_fraction\"),\n", + " dict(prop=\"wheel_speeds_fraction\"),\n", + " dict(prop=\"instrument_pointing_error\", norm=np.pi),\n", + " dict(prop=\"solar_pointing_error\", norm=np.pi)\n", + " ),\n", + " obs.Eclipse(),\n", + " obs.OpportunityProperties(\n", + " dict(prop=\"opportunity_open\", norm=5700),\n", + " dict(prop=\"opportunity_close\", norm=5700),\n", + " type=\"ground_station\",\n", + " n_ahead_observe=1,\n", + " ),\n", + " ]\n", + " action_spec = [\n", + " act.Scan(duration=180.0),\n", + " act.Charge(duration=180.0),\n", + " act.Downlink(duration=60.0),\n", + " act.Desat(duration=60.0),\n", + " ]\n", + " dyn_type = ScanningDownlinkDynModel\n", + " fsw_type = fsw.ContinuousImagingFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, parameters are set. Since this scenario is focused on maintaining acceptable data\n", + "and power levels, these are tuned to create a sufficiently interesting mission." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:42.282097Z", + "iopub.status.busy": "2024-05-31T20:59:42.281947Z", + "iopub.status.idle": "2024-05-31T20:59:42.285198Z", + "shell.execute_reply": "2024-05-31T20:59:42.284950Z" + } + }, + "outputs": [], + "source": [ + "sat = ScanningSatellite(\n", + " \"Scanner-1\",\n", + " sat_args=dict(\n", + " # Data\n", + " dataStorageCapacity=5000 * 8e6, # MB to bits\n", + " storageInit=lambda: np.random.uniform(0, 5000 * 8e6),\n", + " instrumentBaudRate=0.5e6,\n", + " transmitterBaudRate=-112e6,\n", + " # Power\n", + " batteryStorageCapacity=400 * 3600, # Wh to W*s\n", + " storedCharge_Init=lambda: np.random.uniform(400 * 3600 * 0.2, 400 * 3600 * 0.8),\n", + " basePowerDraw=-10.0,\n", + " instrumentPowerDraw=-30.0,\n", + " transmitterPowerDraw=-25.0,\n", + " thrusterPowerDraw=-80.0,\n", + " # Attitude\n", + " imageAttErrorRequirement=0.1,\n", + " imageRateErrorRequirement=0.1,\n", + " disturbance_vector=lambda: np.random.normal(scale=0.0001, size=3),\n", + " maxWheelSpeed=6000.0, # RPM\n", + " wheelSpeeds=lambda: np.random.uniform(-3000, 3000, 3),\n", + " desatAttitude=\"nadir\",\n", + " nHat_B=np.array([0, 0, -1]), # Solar panel orientation\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the environment arguments are set. Stepping through this environment is \n", + "demonstrated at the bottom of the page." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:42.286571Z", + "iopub.status.busy": "2024-05-31T20:59:42.286488Z", + "iopub.status.idle": "2024-05-31T20:59:42.288158Z", + "shell.execute_reply": "2024-05-31T20:59:42.287921Z" + } + }, + "outputs": [], + "source": [ + "duration = 3 * 5700.0\n", + "env_args = dict(\n", + " satellite=sat,\n", + " scenario=scene.UniformNadirScanning(value_per_second=1/duration),\n", + " rewarder=data.ScanningTimeReward(),\n", + " time_limit=duration,\n", + " failure_penalty=-1.0,\n", + " terminate_on_time_limit=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure Ray and PPO\n", + "\n", + "The `bsk_rl` package supplies a utility to make logging information at the end of episodes\n", + "easier. This is useful to see how an agent's policy is changing over time, using a\n", + "monitoring program such as [TensorBoard](https://www.tensorflow.org/tensorboard)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:42.289431Z", + "iopub.status.busy": "2024-05-31T20:59:42.289344Z", + "iopub.status.idle": "2024-05-31T20:59:48.791034Z", + "shell.execute_reply": "2024-05-31T20:59:48.790692Z" + } + }, + "outputs": [], + "source": [ + "from bsk_rl.utils.rllib import EpisodeDataCallbacks\n", + "\n", + "class CustomDataCallbacks(EpisodeDataCallbacks):\n", + " def pull_env_metrics(self, env):\n", + " reward = env.rewarder.cum_reward\n", + " orbits = env.simulator.sim_time / (95 * 60)\n", + "\n", + " data = dict(\n", + " reward=reward,\n", + " reward_per_orbit=reward / orbits,\n", + " # Are satellites dying, and how and when?\n", + " alive=float(env.satellite.is_alive()),\n", + " rw_status_valid=float(env.satellite.dynamics.rw_speeds_valid()),\n", + " battery_status_valid=float(env.satellite.dynamics.battery_valid()),\n", + " orbits_complete=orbits,\n", + " )\n", + " if not env.satellite.is_alive():\n", + " data[\"orbits_complete_partial_only\"] = orbits\n", + " return data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, PPO (or some other algorithm) can be configured. Of particular importance\n", + "are setting `sample_timeout_s` and `metrics_episode_collection_timeout_s` to appropriately\n", + "high values for this environment." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:48.792806Z", + "iopub.status.busy": "2024-05-31T20:59:48.792588Z", + "iopub.status.idle": "2024-05-31T20:59:48.795422Z", + "shell.execute_reply": "2024-05-31T20:59:48.795192Z" + } + }, + "outputs": [], + "source": [ + "from bsk_rl import SatelliteTasking\n", + "from bsk_rl.utils.rllib import unpack_config\n", + "from ray.rllib.algorithms.ppo import PPOConfig\n", + "\n", + "training_args = dict(\n", + " lr=0.00003,\n", + " gamma=0.999,\n", + " train_batch_size=5000,\n", + " num_sgd_iter=10,\n", + " model=dict(fcnet_hiddens=[512, 512], vf_share_layers=False),\n", + " lambda_=0.95,\n", + " use_kl_loss=False,\n", + " clip_param=0.1,\n", + " grad_clip=0.5,\n", + ")\n", + "\n", + "config = (\n", + " PPOConfig()\n", + " .training(**training_args)\n", + " .env_runners(num_env_runners=7, sample_timeout_s=1000.0)\n", + " .environment(\n", + " env=unpack_config(SatelliteTasking),\n", + " env_config=env_args,\n", + " )\n", + " .callbacks(CustomDataCallbacks)\n", + " .reporting(\n", + " metrics_num_episodes_for_smoothing=25,\n", + " metrics_episode_collection_timeout_s=180,\n", + " )\n", + " .checkpointing(export_native_model_files=True)\n", + " .framework(framework=\"tf2\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once the PPO configuration has been set, `ray` can be started and the agent can be\n", + "trained.\n", + "\n", + "```python\n", + "import ray\n", + "\n", + "ray.init(\n", + " ignore_reinit_error=True,\n", + " num_cpus=8,\n", + " object_store_memory=2_000_000_000, # 2 GB\n", + ")\n", + "\n", + "ppo = PPO(config)\n", + "\n", + "# Train the policy as you see fit\n", + "for _ in range(10):\n", + " ppo.train()\n", + " ppo.checkpoint()\n", + "\n", + "ray.shutdown()\n", + "```\n", + "\n", + "Training on a reasonably modern machine, we can achieve 5M steps over 32 processors in 6\n", + "to 18 hours, depending on specific environment configurations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading the Policy Network\n", + "\n", + "The policy network can be found in the `model` subdirectory of the checkpoint output." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stepping Through the Environment\n", + "\n", + "The environment is stepped through with random actions to give a sense of how it acts." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T20:59:48.796859Z", + "iopub.status.busy": "2024-05-31T20:59:48.796754Z", + "iopub.status.idle": "2024-05-31T20:59:50.570406Z", + "shell.execute_reply": "2024-05-31T20:59:50.569692Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,797 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=443345856\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,888 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,930 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,931 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,932 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,932 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,932 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,933 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,943 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 180.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,943 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,944 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,944 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,945 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,945 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,945 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 360.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,945 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,956 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 360.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,956 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,957 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,957 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,957 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,958 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,958 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 540.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,958 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<360.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,968 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 540.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,968 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.007543859649122808}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,969 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,969 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[mStep reward: 0.007543859649122808\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,969 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,970 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,970 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,970 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<540.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,974 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 600.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,974 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,975 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,975 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,975 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,976 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,976 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,976 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<600.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,986 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 780.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,986 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,987 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,987 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,987 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,988 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,988 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 840.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,988 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,992 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 840.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,992 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,993 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,993 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,994 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,994 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,994 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 900.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,994 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<840.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,998 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 900.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,998 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,999 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:48,999 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,000 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,000 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,000 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1080.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,000 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<900.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,011 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1080.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,011 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,012 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,012 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,012 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,013 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,013 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1260.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,013 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1080.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,023 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1260.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,024 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,025 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,025 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,025 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,025 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,026 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,026 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1260.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,030 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1320.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,030 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,031 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,031 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,031 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,032 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,032 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1380.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,032 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,036 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1380.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,036 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,037 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,037 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,037 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,038 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,038 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1560.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,038 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,049 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1560.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,049 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,050 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,050 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,050 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,050 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,051 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1740.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,051 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1560.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,061 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1740.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,061 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,062 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,062 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,063 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,063 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,063 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1920.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,063 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1740.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,073 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1920.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,074 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,074 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,075 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,075 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,075 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,075 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 1980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,075 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1920.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,079 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 1980.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,079 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,080 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,080 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,081 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,081 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,081 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2160.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,081 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,092 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2160.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,092 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,093 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,093 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,094 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,094 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,094 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2340.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,094 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2160.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,105 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2340.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,105 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,106 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,106 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,106 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,107 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,107 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2400.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,107 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2340.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,111 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2400.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,111 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,112 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,112 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,113 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,113 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,113 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2460.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,113 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2400.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,117 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2460.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,117 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,118 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,118 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,119 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,119 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,119 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2520.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,119 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2460.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,123 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2520.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,123 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,124 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,124 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,124 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,125 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,125 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2700.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,125 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2520.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,136 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2700.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,136 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,137 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,137 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,137 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,138 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,138 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 2880.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,138 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2700.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,149 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 2880.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,149 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,150 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,150 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,151 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,151 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,151 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3060.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,152 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2880.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,162 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3060.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,163 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,164 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,164 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,164 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,165 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,165 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3120.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,165 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3060.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,169 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3120.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,169 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,170 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,170 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,171 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,171 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,171 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3300.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,171 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3120.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,182 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3300.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,182 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008187134502923977}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,183 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,183 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[mStep reward: 0.008187134502923977\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,184 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,184 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,184 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3360.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,185 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3300.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,188 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3360.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,189 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,190 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,190 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,190 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,191 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,191 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3420.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,191 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3360.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,195 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3420.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,195 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,196 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,196 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,197 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,197 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,197 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3480.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,197 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3420.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,201 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3480.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,202 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,203 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,203 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,203 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,203 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,204 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3660.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,204 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3480.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,215 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3660.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,215 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,216 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,216 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,216 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,217 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,217 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3840.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,217 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3660.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,227 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3840.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,228 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,228 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,229 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,229 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,229 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,230 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3900.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,230 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3840.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,234 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3900.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,234 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,235 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,235 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,236 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,236 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,236 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 3960.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,236 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3900.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,240 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 3960.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,241 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,241 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,242 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,242 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,242 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,242 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4140.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,243 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3960.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,253 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4140.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,254 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,254 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,255 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,255 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,255 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,255 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,256 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4140.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,266 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4320.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,266 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0076608187134502926}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,267 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,267 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[mStep reward: 0.0076608187134502926\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,268 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,268 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,268 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4500.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,268 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,279 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4500.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,279 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,280 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,280 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,281 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,281 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,281 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4680.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,281 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4500.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,292 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4680.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,292 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,293 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,293 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,293 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,294 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,294 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4740.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,294 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4680.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,298 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4740.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,298 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,299 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,299 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,300 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,300 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,300 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4920.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,300 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4740.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,311 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4920.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,311 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,312 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,312 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,313 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,313 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,313 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 4980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,313 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4920.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,317 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 4980.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,318 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,319 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,319 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,319 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,319 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,320 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5160.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,320 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,330 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5160.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,331 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,332 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,332 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,332 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,332 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,333 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5340.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,333 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5160.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,344 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5340.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,344 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0077192982456140355}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[mStep reward: 0.0077192982456140355\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,346 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,346 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5520.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,346 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5340.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,356 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5520.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,357 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,358 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,358 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,358 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,358 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,359 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5580.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,359 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5520.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,363 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5580.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,363 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,364 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,364 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,364 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,365 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,365 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5640.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,365 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,369 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5640.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,369 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,370 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,371 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,371 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,371 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,371 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 5820.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,372 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5640.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,382 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 5820.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,382 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,383 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,383 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,384 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,384 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,384 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6000.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,384 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5820.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,395 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6000.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,395 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,397 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,397 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,397 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,398 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,398 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,398 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6000.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,408 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6180.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,409 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008128654970760233}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,410 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,410 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[mStep reward: 0.008128654970760233\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,410 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,410 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,411 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6240.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,411 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6180.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,415 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6240.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,415 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,416 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,416 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,416 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,417 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,417 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6300.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,417 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6240.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,421 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6300.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,421 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,422 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,422 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,423 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,423 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,423 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6360.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,423 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6300.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,427 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6360.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,427 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,428 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,428 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,429 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,429 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,429 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6420.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,429 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6360.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,433 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6420.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,433 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,434 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,435 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,435 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,435 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,435 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,436 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6420.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,446 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6600.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,447 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,448 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,448 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,448 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,448 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,449 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6660.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,449 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6600.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,453 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6660.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,453 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,454 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,454 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,454 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,455 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,455 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6720.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,455 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6660.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,459 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6720.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,459 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,460 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,460 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,461 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,461 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,461 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 6900.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,461 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6720.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,472 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 6900.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,472 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,473 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,474 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,474 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,474 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,474 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7080.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,475 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<6900.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,485 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7080.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,485 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,486 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,486 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,487 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,487 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,487 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7260.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,487 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7080.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,498 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7260.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,498 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,499 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,499 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,500 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,500 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,500 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,500 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7260.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,504 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7320.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,505 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,507 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,507 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7380.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,507 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,511 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7380.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,511 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,512 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,513 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,513 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,513 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,514 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7560.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,514 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7380.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,525 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7560.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,525 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,526 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,526 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,526 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,527 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,527 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7740.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,527 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7560.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,538 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7740.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,538 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,539 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,539 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,540 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,540 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,540 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 7920.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,540 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7740.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,551 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 7920.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,551 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,552 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,552 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,553 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,553 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,553 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8100.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,553 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<7920.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,564 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8100.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,564 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,565 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,565 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,566 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,566 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,566 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8280.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,566 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8100.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,577 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8280.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,577 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,578 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,578 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,579 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,579 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,579 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8340.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,580 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8280.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,583 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8340.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,584 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,585 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,585 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,585 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,586 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,586 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8520.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,586 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8340.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,596 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8520.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,597 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008128654970760233}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,598 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,598 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[mStep reward: 0.008128654970760233\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,598 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,598 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,599 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8700.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,599 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8520.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,609 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8700.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,610 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,611 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,611 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,612 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,612 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,612 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8880.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,612 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8700.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,622 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8880.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,623 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,623 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,624 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,624 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,624 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,625 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 8940.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,625 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8880.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,629 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 8940.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,629 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,630 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,630 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,631 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,631 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,631 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9000.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,631 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<8940.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,635 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9000.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,636 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,636 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,637 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,637 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,637 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,637 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,638 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9000.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,648 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9180.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,649 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,650 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,650 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,650 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,650 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,651 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9240.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,651 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9180.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,655 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9240.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,655 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,656 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,656 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,657 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,657 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,657 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9300.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,657 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9240.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,661 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9300.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,661 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,662 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,662 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,663 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,663 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,663 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9360.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,663 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9300.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,667 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9360.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,668 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,668 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,669 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,669 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,669 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,669 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9540.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,670 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9360.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,680 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9540.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,681 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,682 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,682 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,682 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,683 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,683 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9720.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,683 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9540.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,693 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9720.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,694 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,695 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,695 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,696 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,696 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,696 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,696 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9720.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,700 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9780.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,701 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,701 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,702 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,702 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,702 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,702 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 9960.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,703 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9780.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,753 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 9960.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,753 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,754 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,754 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,755 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,755 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,755 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10020.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,755 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<9960.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,759 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10020.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,760 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,761 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,761 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,761 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,762 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,762 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10200.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,762 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10020.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,773 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10200.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,773 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008128654970760233}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,774 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,774 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[mStep reward: 0.008128654970760233\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,775 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,775 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,775 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10260.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,776 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10200.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,780 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10260.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,780 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,781 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,781 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,782 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,782 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,782 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,782 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10260.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,786 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10320.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,786 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,787 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,788 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,788 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,788 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,789 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10380.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,789 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,793 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10380.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,793 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,794 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,794 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,795 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,795 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,795 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10440.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,796 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10380.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,800 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10440.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,800 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,801 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,801 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,801 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,802 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,802 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10620.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,802 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10440.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,813 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10620.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,814 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,814 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,815 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,815 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,815 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,816 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10800.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,816 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10620.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,826 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10800.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,827 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.007485380116959065}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,828 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,828 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[mStep reward: 0.007485380116959065\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,828 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,829 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,829 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 10980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,829 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10800.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,840 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 10980.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,840 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,841 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,842 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,842 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,842 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,842 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11160.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,843 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<10980.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,853 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11160.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,854 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,855 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,855 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,855 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,856 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,856 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11220.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,856 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11160.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,860 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11220.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,860 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,861 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,861 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,862 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,862 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,862 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11280.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,863 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11220.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,866 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11280.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,867 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,868 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,868 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,869 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,869 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,869 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11340.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,869 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11280.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,873 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11340.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,874 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,874 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,875 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,875 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,876 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,876 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11520.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,876 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11340.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,887 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11520.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,887 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008187134502923977}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,888 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,889 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[mStep reward: 0.008187134502923977\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,889 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,889 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,890 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11580.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,890 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11520.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,894 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11580.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,894 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,895 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,895 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,896 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,896 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,896 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11760.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,896 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11580.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,907 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11760.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,908 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,909 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,909 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,909 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,909 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,910 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 11820.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,910 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11760.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,914 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 11820.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,914 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,985 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,986 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,987 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,987 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,988 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12000.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:49,988 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<11820.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,000 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12000.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,001 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,003 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,003 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,004 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,005 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,005 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12060.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,005 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12000.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,010 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12060.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,011 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,012 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,013 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,013 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,013 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,014 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12120.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,014 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12060.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,019 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12120.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,019 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,021 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,021 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,022 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,023 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,023 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,023 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12120.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,028 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12180.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,029 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,030 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,030 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,031 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,031 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,032 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12240.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,032 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12180.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,036 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12240.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,037 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,038 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,038 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,038 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,039 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,039 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12420.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,039 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12240.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,052 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12420.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,053 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,054 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,054 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,055 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,055 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,056 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12600.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,056 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12420.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,067 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12600.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,068 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,069 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,069 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,070 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,070 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,070 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12660.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,071 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12600.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,075 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12660.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,075 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,076 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,077 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,077 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,077 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,078 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12720.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,078 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12660.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,082 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12720.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,083 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,084 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,084 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,084 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,085 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,085 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,085 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12720.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,089 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12780.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,089 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,090 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,091 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,091 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,092 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,092 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 12960.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,092 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12780.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,103 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 12960.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,104 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,105 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,105 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,106 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,106 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,106 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13140.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,107 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<12960.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,117 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13140.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,118 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.009473684210526316}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,119 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,119 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[mStep reward: 0.009473684210526316\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,120 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,120 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,120 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13200.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,121 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13140.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,125 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13200.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,126 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,126 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,127 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,127 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,127 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,128 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13260.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,128 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13200.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,133 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13260.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,133 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,134 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,135 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,135 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,135 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,136 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,136 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13260.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,140 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13320.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,141 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,142 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,142 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,142 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,143 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,143 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13500.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,143 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,154 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13500.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,155 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,156 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,156 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,156 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,157 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,157 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13680.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,157 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13500.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,168 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13680.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,168 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,169 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,170 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,170 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,171 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,171 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13740.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,172 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13680.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,175 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13740.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,175 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,176 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,176 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,177 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,177 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,177 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13920.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,178 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13740.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,189 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13920.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,189 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008128654970760233}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,190 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,191 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[mStep reward: 0.008128654970760233\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,191 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,191 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,192 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 13980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,192 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13920.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,196 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 13980.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,197 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,198 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,198 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,198 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,198 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,199 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14040.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,199 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<13980.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,203 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14040.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,203 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,204 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,205 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,205 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,205 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,206 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14100.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,206 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14040.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,210 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14100.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,210 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,211 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,212 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,212 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,212 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,213 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14280.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,213 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14100.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,224 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14280.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,225 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,226 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,226 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,226 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,227 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,227 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14460.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,227 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14280.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,238 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14460.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,239 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,239 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,240 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,240 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,269 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,270 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14520.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,270 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14460.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,274 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14520.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,275 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,277 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,277 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14580.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,277 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14520.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,282 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14580.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,282 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,283 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,283 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,284 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,284 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,284 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14760.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,284 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14580.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,295 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14760.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,296 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,296 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,297 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,297 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,297 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,298 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14820.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,298 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14760.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,302 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14820.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,302 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,303 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,304 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,304 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,304 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,305 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 14880.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,305 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14820.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,309 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 14880.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,309 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,311 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,311 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,311 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,312 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,312 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15060.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,312 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<14880.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,323 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15060.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,324 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.00824561403508772}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,324 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,325 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[mStep reward: 0.00824561403508772\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,325 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,325 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,326 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15240.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,326 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15060.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,337 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15240.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,337 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,338 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,338 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,339 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,339 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,339 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15420.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,340 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15240.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,351 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15420.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,351 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008070175438596491}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,412 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,413 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[mStep reward: 0.008070175438596491\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,414 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,414 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,414 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15480.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,415 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15420.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,419 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15480.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,419 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,420 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,420 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,421 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,421 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,421 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15540.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,421 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15480.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,425 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15540.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,425 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,426 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,426 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,427 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,427 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,427 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15720.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,428 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15540.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,438 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15720.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,438 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008187134502923977}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,439 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mFinding opportunity windows from 17400.00 to 18000.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,445 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mFinding opportunity windows from 18000.00 to 18600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,453 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,453 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[mStep reward: 0.008187134502923977\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,453 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,454 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,454 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 15900.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,454 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15720.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,465 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 15900.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,466 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,466 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,467 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,467 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,467 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,468 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16080.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,468 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<15900.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,478 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16080.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,478 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,479 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,480 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,480 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,480 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,481 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16260.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,481 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16080.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,492 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16260.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,492 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,493 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,493 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,494 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,494 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,494 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16320.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,495 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16260.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,499 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16320.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,499 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,500 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,500 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,501 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,501 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,501 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16500.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,502 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16320.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,513 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16500.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,513 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,514 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,514 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,515 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,515 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,515 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16560.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,516 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16500.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,520 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16560.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,520 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,521 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,521 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,522 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,522 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_downlink tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,522 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16620.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,522 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16560.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,526 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16620.0 for action_downlink\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,527 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,527 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,528 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,528 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,528 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,528 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16800.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,529 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16620.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,539 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16800.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,540 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.008187134502923977}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,541 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,541 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[mStep reward: 0.008187134502923977\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,542 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,542 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_nadir_scan tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,542 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 16980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,542 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16800.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,554 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[mtimed termination at 16980.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,555 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.010526315789473684}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,556 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['Scanner-1_4827225424']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,556 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[mStep reward: 0.010526315789473684\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,557 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,557 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[maction_charge tasked for 180.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,557 \u001b[0m\u001b[36msats.satellite.Scanner-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[36mScanner-1: \u001b[0m\u001b[msetting timed terminal event at 17160.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,558 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<16980.00> \u001b[0m\u001b[mRunning simulation at most to 17100.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,566 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<17100.00> \u001b[0m\u001b[mData reward: {'Scanner-1_4827225424': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,567 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<17100.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,567 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<17100.00> \u001b[0m\u001b[mEpisode terminated: True\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 13:59:50,567 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<17100.00> \u001b[0m\u001b[mEpisode truncated: True\u001b[0m\n" + ] + } + ], + "source": [ + "env = SatelliteTasking(**env_args, log_level=\"INFO\")\n", + "env.reset()\n", + "terminated = False\n", + "while not terminated:\n", + " action = env.action_space.sample()\n", + " observation, reward, terminated, truncated, info = env.step(action)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/build/doctrees/nbsphinx/examples/satellite_configuration.ipynb b/docs/build/doctrees/nbsphinx/examples/satellite_configuration.ipynb new file mode 100644 index 00000000..3c68f06e --- /dev/null +++ b/docs/build/doctrees/nbsphinx/examples/satellite_configuration.ipynb @@ -0,0 +1,1152 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Satellite Configuration\n", + "\n", + "[Satellites](../api_reference/sats/index.rst) are the basic unit of agent in the \n", + "environment. Four things must be specified in subclasses of `Satellite`:\n", + "\n", + "* The `observation_spec`, which defines the satellite's [observation](../api_reference/obs/index.rst).\n", + "* The `action_spec`, which defines the satellite's [actions](../api_reference/act/index.rst).\n", + "* The `dyn_type`, which selects the underlying [dynamics model](../api_reference/sim/dyn.rst) used in simulation.\n", + "* The `fsw_type`, which selects the underlying [flight software model](../api_reference/sim/fsw.rst).\n", + "\n", + "A very simple satellite is defined below:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:53.786245Z", + "iopub.status.busy": "2024-05-31T19:57:53.783988Z", + "iopub.status.idle": "2024-05-31T19:57:54.837135Z", + "shell.execute_reply": "2024-05-31T19:57:54.836830Z" + } + }, + "outputs": [], + "source": [ + "from bsk_rl import sats, act, obs, scene, data, SatelliteTasking\n", + "from bsk_rl.sim import dyn, fsw\n", + "import numpy as np\n", + "\n", + "from Basilisk.architecture import bskLogging\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)\n", + "\n", + "\n", + "class SimpleSatellite(sats.Satellite):\n", + " observation_spec = [obs.Time()] # Passed as list of instantiated classes\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel # Passed as a type\n", + " fsw_type = fsw.BasicFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Satellite Parameters\n", + "\n", + "Without instantiating the satellite, parameters that can be set in the various models\n", + "can be inspected." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.838921Z", + "iopub.status.busy": "2024-05-31T19:57:54.838768Z", + "iopub.status.idle": "2024-05-31T19:57:54.842509Z", + "shell.execute_reply": "2024-05-31T19:57:54.842272Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'hs_min': 0.0,\n", + " 'maxCounterValue': 4,\n", + " 'thrMinFireTime': 0.02,\n", + " 'desatAttitude': 'sun',\n", + " 'controlAxes_B': [1, 0, 0, 0, 1, 0, 0, 0, 1],\n", + " 'thrForceSign': 1,\n", + " 'K': 7.0,\n", + " 'Ki': -1,\n", + " 'P': 35.0,\n", + " 'batteryStorageCapacity': 288000.0,\n", + " 'storedCharge_Init': ()>,\n", + " 'disturbance_vector': None,\n", + " 'dragCoeff': 2.2,\n", + " 'basePowerDraw': 0.0,\n", + " 'wheelSpeeds': ()>,\n", + " 'maxWheelSpeed': inf,\n", + " 'u_max': 0.2,\n", + " 'rwBasePower': 0.4,\n", + " 'rwMechToElecEfficiency': 0.0,\n", + " 'rwElecToMechEfficiency': 0.5,\n", + " 'panelArea': 1.0,\n", + " 'panelEfficiency': 0.2,\n", + " 'nHat_B': array([0, 1, 0]),\n", + " 'mass': 330,\n", + " 'width': 1.38,\n", + " 'depth': 1.04,\n", + " 'height': 1.58,\n", + " 'sigma_init': ()>,\n", + " 'omega_init': ()>,\n", + " 'rN': None,\n", + " 'vN': None,\n", + " 'oe': Basilisk.utilities.orbitalMotion.ClassicElements>,\n", + " 'mu': 398600436000000.0,\n", + " 'thrusterPowerDraw': 0.0}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "SimpleSatellite.default_sat_args()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These parameters can be overriden when instantiating the satellite through the `sat_args`\n", + "argument. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.860445Z", + "iopub.status.busy": "2024-05-31T19:57:54.860341Z", + "iopub.status.idle": "2024-05-31T19:57:54.862452Z", + "shell.execute_reply": "2024-05-31T19:57:54.862205Z" + } + }, + "outputs": [], + "source": [ + "sat = SimpleSatellite(\n", + " name=\"SimpleSat-1\",\n", + " sat_args=dict(\n", + " mass=300, # Setting a constant value\n", + " dragCoeff=lambda: np.random.uniform(2.0, 2.4), # Setting a randomized value\n", + " ),\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each time the simulation is reset, all of the function-based randomizers are called." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.863842Z", + "iopub.status.busy": "2024-05-31T19:57:54.863747Z", + "iopub.status.idle": "2024-05-31T19:57:54.866405Z", + "shell.execute_reply": "2024-05-31T19:57:54.866153Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'hs_min': 0.0,\n", + " 'maxCounterValue': 4,\n", + " 'thrMinFireTime': 0.02,\n", + " 'desatAttitude': 'sun',\n", + " 'controlAxes_B': [1, 0, 0, 0, 1, 0, 0, 0, 1],\n", + " 'thrForceSign': 1,\n", + " 'K': 7.0,\n", + " 'Ki': -1,\n", + " 'P': 35.0,\n", + " 'batteryStorageCapacity': 288000.0,\n", + " 'storedCharge_Init': 235425.4589902638,\n", + " 'disturbance_vector': None,\n", + " 'dragCoeff': 2.2853498599675968,\n", + " 'basePowerDraw': 0.0,\n", + " 'wheelSpeeds': array([ 304.42817499, -736.9173528 , -908.58106354]),\n", + " 'maxWheelSpeed': inf,\n", + " 'u_max': 0.2,\n", + " 'rwBasePower': 0.4,\n", + " 'rwMechToElecEfficiency': 0.0,\n", + " 'rwElecToMechEfficiency': 0.5,\n", + " 'panelArea': 1.0,\n", + " 'panelEfficiency': 0.2,\n", + " 'nHat_B': array([0, 1, 0]),\n", + " 'mass': 300,\n", + " 'width': 1.38,\n", + " 'depth': 1.04,\n", + " 'height': 1.58,\n", + " 'sigma_init': array([0.30942003, 0.33854245, 0.44306868]),\n", + " 'omega_init': array([-6.86365558e-05, -1.14659135e-05, -2.54266860e-05]),\n", + " 'rN': None,\n", + " 'vN': None,\n", + " 'oe': ,\n", + " 'mu': 398600436000000.0,\n", + " 'thrusterPowerDraw': 0.0}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sat._generate_sat_args() # Called by the environment on reset()\n", + "sat.sat_args" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a result, each episode will have different randomized parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.867787Z", + "iopub.status.busy": "2024-05-31T19:57:54.867695Z", + "iopub.status.idle": "2024-05-31T19:57:54.870139Z", + "shell.execute_reply": "2024-05-31T19:57:54.869855Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New value of dragCoeff: 2.3237228881150997\n", + "New value of dragCoeff: 2.1169960307463387\n", + "New value of dragCoeff: 2.1165628273706467\n" + ] + } + ], + "source": [ + "for _ in range(3):\n", + " sat._generate_sat_args() # Called by the environment on reset()\n", + " print(\"New value of dragCoeff:\", sat.sat_args[\"dragCoeff\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Observation Specification\n", + "\n", + "A variety of observation elements are available for satellites. Full documentation\n", + "can be [found here](../api_reference/obs/index.rst), but some commonly used elements\n", + "are explored below.\n", + "\n", + "
\n", + "\n", + "**Info:** In these examples, `obs_type=dict` is passed to the `Satellite` constructor\n", + "so that the observation is human readable. While some RL libraries support dictionary-based\n", + "observations, the default return type - the numpy array format - is more typically used.\n", + "\n", + "
\n", + "\n", + "\n", + "### Satellite Properties\n", + "\n", + "The most common type of observations is introspective; i.e. what is my current state?\n", + "Any `@property` in the `dyn_type` or `fsw_type` of the satellite can be accessed using\n", + "SatProperties." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.871528Z", + "iopub.status.busy": "2024-05-31T19:57:54.871429Z", + "iopub.status.idle": "2024-05-31T19:57:54.958716Z", + "shell.execute_reply": "2024-05-31T19:57:54.958271Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'sat_props': {'wheel_speeds': array([-111.73880531, -125.96152137, 28.3861012 ]),\n", + " 'battery_charge_fraction': 0.7697315050692438,\n", + " 'r_BN_P_normd': array([-0.91282806, -0.3211816 , -0.16452898])}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class SatPropsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " # At a minimum, specify the property to observe\n", + " dict(prop=\"wheel_speeds\"),\n", + " # You can specify the module to use for the observation, but it is not necessary\n", + " # if only one module has for the property\n", + " dict(prop=\"battery_charge_fraction\", module=\"dynamics\"), \n", + " # Properties can be normalized by some constant. This is generally desirable\n", + " # for RL algorithms to keep values around [-1, 1].\n", + " dict(prop=\"r_BN_P\", norm=7e6),\n", + " )\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=SatPropsSatellite(\"PropSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In some cases, you may want to access a bespoke property that is not natively implemented\n", + "in a model. To do that, simply extend the model with your desired property." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:54.960424Z", + "iopub.status.busy": "2024-05-31T19:57:54.960279Z", + "iopub.status.idle": "2024-05-31T19:57:55.143080Z", + "shell.execute_reply": "2024-05-31T19:57:55.142754Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'sat_props': {'meaning_of_life': 42.0}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class BespokeFSWModel(fsw.BasicFSWModel):\n", + " @property\n", + " def meaning_of_life(self):\n", + " return 42\n", + " \n", + "class BespokeSatPropsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.SatProperties(dict(prop=\"meaning_of_life\"))\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = BespokeFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=BespokeSatPropsSatellite(\"BespokeSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Opportunity Properties\n", + "Another common input to the observation is information about upcoming locations that \n", + "are being accessed by the satellite. Currently, these include ground stations for\n", + "downlink and targets for imaging, but `OpportunityProperties` will work with any\n", + "location added by `add_location_for_access_checking`. In these examples, " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:55.144834Z", + "iopub.status.busy": "2024-05-31T19:57:55.144700Z", + "iopub.status.idle": "2024-05-31T19:57:55.409391Z", + "shell.execute_reply": "2024-05-31T19:57:55.409079Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'target': {'target_0': {'priority': 0.9478156415307375,\n", + " 'opportunity_open_normd': 0.0,\n", + " 'prop_2': array([ 1724999.9002634 , -6018236.01560621, -1218759.23787657])},\n", + " 'target_1': {'priority': 0.1945452564622594,\n", + " 'opportunity_open_normd': 0.0,\n", + " 'prop_2': array([ 2342787.53660583, -5828566.86296108, -1104262.56015745])},\n", + " 'target_2': {'priority': 0.775106357508238,\n", + " 'opportunity_open_normd': 0.011243806833161772,\n", + " 'prop_2': array([ 2487909.97946065, -5862237.91118463, -353247.75070764])}}}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class OppPropsSatellite(sats.ImagingSatellite):\n", + " observation_spec = [\n", + " obs.OpportunityProperties(\n", + " # Properties can be added by some default names\n", + " dict(prop=\"priority\"), \n", + " # They can also be normalized\n", + " dict(prop=\"opportunity_open\", norm=5700.0),\n", + " # Or they can be specified by an arbitrary function\n", + " dict(fn=lambda sat, opp: opp[\"r_LP_P\"] + 42),\n", + " n_ahead_observe=3,\n", + " )\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.ImagingDynModel\n", + " fsw_type = fsw.ImagingFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=OppPropsSatellite(\"OppSat-1\", {}, obs_type=dict),\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Navigating the Observation\n", + "\n", + "Usually, multiple observation types need to be composed to sufficiently represent the\n", + "environment for the learning agent. Simply add multiple observations to the observation\n", + "specification list to combine them in the observation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:55.411131Z", + "iopub.status.busy": "2024-05-31T19:57:55.411012Z", + "iopub.status.idle": "2024-05-31T19:57:55.724948Z", + "shell.execute_reply": "2024-05-31T19:57:55.724656Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'eclipse': [1260.0, 3270.0],\n", + " 'sat_props': {'battery_charge_fraction': 0.3974105784122624}}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class ComposedObsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.Eclipse(),\n", + " obs.SatProperties(dict(prop=\"battery_charge_fraction\"))\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ComposedObsSatellite(\"PropSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "A few useful functions exist for inspecting the observation. The `observation_space`\n", + "property of the satellite and the environment return a Gym observation space to describe\n", + "the observation. In the single agent `SatelliteTasking` environment, these are the same.\n", + "\n", + "
\n", + "\n", + "**Info:** Here, we return to the `ndarray` default observation type.\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:55.726544Z", + "iopub.status.busy": "2024-05-31T19:57:55.726435Z", + "iopub.status.idle": "2024-05-31T19:57:56.104773Z", + "shell.execute_reply": "2024-05-31T19:57:56.104487Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(Box(-1e+16, 1e+16, (3,), float64), Box(-1e+16, 1e+16, (3,), float64))" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env = SatelliteTasking(\n", + " satellite=ComposedObsSatellite(\"PropSat-1\", {}),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "(env.observation_space, env.unwrapped.satellite.observation_space)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "With the flattened-vector type observation, it can be hard for the user to relate\n", + "elements to specific observations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:56.106273Z", + "iopub.status.busy": "2024-05-31T19:57:56.106184Z", + "iopub.status.idle": "2024-05-31T19:57:56.879567Z", + "shell.execute_reply": "2024-05-31T19:57:56.879257Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1.89000000e+03, 4.02000000e+03, 7.67604203e-01])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `observation_description` property can help the user understand what elements are \n", + "present in the observation." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:56.881113Z", + "iopub.status.busy": "2024-05-31T19:57:56.881011Z", + "iopub.status.idle": "2024-05-31T19:57:56.883188Z", + "shell.execute_reply": "2024-05-31T19:57:56.882927Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['eclipse[0]', 'eclipse[1]', 'sat_props.battery_charge_fraction']" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.unwrapped.satellite.observation_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## The Action Specification\n", + "\n", + "The [action specification](../api_reference/act/index.rst) works similarly to observation\n", + "specification. A list of actions is set in the class definition of the satellite." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:56.884620Z", + "iopub.status.busy": "2024-05-31T19:57:56.884523Z", + "iopub.status.idle": "2024-05-31T19:57:58.125148Z", + "shell.execute_reply": "2024-05-31T19:57:58.124884Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:57,901 \u001b[0m\u001b[m \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[93mCreating logger for new env on PID=82750. Old environments in process may now log times incorrectly.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,001 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=498972600\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,073 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-1_11727552080']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,074 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,074 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,074 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[maction_charge tasked for 120.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,075 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[msetting timed terminal event at 120.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,075 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 1000000000.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,082 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[mtimed termination at 120.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,082 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mData reward: {'ActSat-1_11727552080': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,083 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-1_11727552080']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,083 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,084 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,084 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[maction_desat tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,084 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[msetting timed terminal event at 180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,084 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mRunning simulation at most to 1000000120.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,088 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[mtimed termination at 180.0 for action_desat\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,088 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mData reward: {'ActSat-1_11727552080': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,089 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-1_11727552080']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,089 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,089 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,090 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,090 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[msetting timed terminal event at 780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,090 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mRunning simulation at most to 1000000180.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,122 \u001b[0m\u001b[36msats.satellite.ActSat-1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mActSat-1: \u001b[0m\u001b[mtimed termination at 780.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,123 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mData reward: {'ActSat-1_11727552080': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,123 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-1_11727552080']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,123 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + } + ], + "source": [ + "class ActionSatellite(sats.Satellite):\n", + " observation_spec = [obs.Time()]\n", + " action_spec = [\n", + " # If action duration is not set, the environment max_step_duration will be used;\n", + " # however, being explicit is always preferable\n", + " act.Charge(duration=120.0),\n", + " act.Desat(duration=60.0),\n", + " # One action can be included multiple time, if different settings are desired\n", + " act.Charge(duration=600.0,),\n", + " ]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ActionSatellite(\"ActSat-1\", {}, obs_type=dict),\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "# Try each action; index corresponds to the order of addition\n", + "_ =env.step(0)\n", + "_ =env.step(1)\n", + "_ =env.step(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with the observations, properties exist to help understand the actions available." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:58.126823Z", + "iopub.status.busy": "2024-05-31T19:57:58.126682Z", + "iopub.status.idle": "2024-05-31T19:57:58.128969Z", + "shell.execute_reply": "2024-05-31T19:57:58.128705Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Discrete(3)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.action_space" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:58.130575Z", + "iopub.status.busy": "2024-05-31T19:57:58.130432Z", + "iopub.status.idle": "2024-05-31T19:57:58.132413Z", + "shell.execute_reply": "2024-05-31T19:57:58.132183Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['action_charge', 'action_desat', 'action_charge']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.unwrapped.satellite.action_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some actions take additional configurations, add multiple actions to the satellite, and/or\n", + "have \"special\" features that are useful for manually interacting with the environment. \n", + "For example, the imaging action can add an arbitrary number of actions corresponding to\n", + "upcoming targets and process the name of a target directly instead of operating by\n", + "action index." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:58.134046Z", + "iopub.status.busy": "2024-05-31T19:57:58.133816Z", + "iopub.status.idle": "2024-05-31T19:57:58.483653Z", + "shell.execute_reply": "2024-05-31T19:57:58.483366Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,135 \u001b[0m\u001b[m \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[93mCreating logger for new env on PID=82750. Old environments in process may now log times incorrectly.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,298 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=795526600\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,298 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 1000 targets\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,481 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-2_11727536768']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,481 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "['action_image_0', 'action_image_1', 'action_image_2']" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class ImageActSatellite(sats.ImagingSatellite):\n", + " observation_spec = [obs.Time()]\n", + " action_spec = [\n", + " # Set the number of upcoming targets to consider\n", + " act.Image(n_ahead_image=3)\n", + " ]\n", + " dyn_type = dyn.ImagingDynModel\n", + " fsw_type = fsw.ImagingFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ImageActSatellite(\"ActSat-2\", {}),\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.unwrapped.satellite.action_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demonstrating the action overload feature, we task the satellite based on target name.\n", + "While this is not part of the official Gym API, we find it useful in certain cases." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-31T19:57:58.485095Z", + "iopub.status.busy": "2024-05-31T19:57:58.484992Z", + "iopub.status.idle": "2024-05-31T19:57:58.594750Z", + "shell.execute_reply": "2024-05-31T19:57:58.594276Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,485 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 600.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,508 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[mFinding opportunity windows from 600.00 to 1200.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,534 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,535 \u001b[0m\u001b[mact.discrete_actions \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93mAction 'Target(tgt-772)' is not an integer. Will attempt to use compatible set_action_override method.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,535 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[mTarget(tgt-772) tasked for imaging\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,536 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[mTarget(tgt-772) window enabled: 623.1 to 696.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,536 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[msetting timed terminal event at 696.8\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,536 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 1000000000.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,590 \u001b[0m\u001b[36msats.satellite.ActSat-2 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<672.00> \u001b[0m\u001b[36mActSat-2: \u001b[0m\u001b[mimaged Target(tgt-772)\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,592 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<672.00> \u001b[0m\u001b[mData reward: {'ActSat-2_11727536768': 0.6335611864701242}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,592 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<672.00> \u001b[0m\u001b[mSatellites requiring retasking: ['ActSat-2_11727536768']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-31 12:57:58,593 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<672.00> \u001b[0m\u001b[mStep reward: 0.6335611864701242\u001b[0m\n" + ] + } + ], + "source": [ + "target = env.unwrapped.satellite.find_next_opportunities(n=10)[9][\"target\"]\n", + "_ = env.step(target)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/build/doctrees/nbsphinx/examples/simple_environment.ipynb b/docs/build/doctrees/nbsphinx/examples/simple_environment.ipynb new file mode 100644 index 00000000..ad8abdc5 --- /dev/null +++ b/docs/build/doctrees/nbsphinx/examples/simple_environment.ipynb @@ -0,0 +1,1187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started\n", + "This tutorial demonstrates the configuration and use of a simple BSK-RL environment.\n", + "BSK-RL and dependencies should already be installed at this point (see [Installation](../install.rst)\n", + "if you haven't installed the package yet).\n", + "\n", + "## Load Modules\n", + "In this tutorial, the environment will be created with `gym.make`, so it is necessary to\n", + "import the top-level `bsk_rl` module as well as `gym` and `bsk_rl` components." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:14.731764Z", + "iopub.status.busy": "2024-05-30T19:06:14.731612Z", + "iopub.status.idle": "2024-05-30T19:06:15.572295Z", + "shell.execute_reply": "2024-05-30T19:06:15.571903Z" + } + }, + "outputs": [], + "source": [ + "import gymnasium as gym\n", + "import numpy as np\n", + "from bsk_rl import act, data, obs, scene, sats\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "from Basilisk.architecture import bskLogging\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If no errors were raised, you have a functional installation of `bsk_rl`.\n", + "\n", + "## Configure the Satellite\n", + "[Satellites](../api_reference/sats/index.rst) are configurable agents in the environment.\n", + "To make a new environment, start by specifying the [observations](../api_reference/obs/index.rst)\n", + "and [actions](../api_reference/act/index.rst) of a satellite type, as well as the underlying\n", + "Basilisk [simulation](../api_reference/sim/index.rst) models used by the satellite." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.574111Z", + "iopub.status.busy": "2024-05-30T19:06:15.573972Z", + "iopub.status.idle": "2024-05-30T19:06:15.576026Z", + "shell.execute_reply": "2024-05-30T19:06:15.575771Z" + } + }, + "outputs": [], + "source": [ + "class MyScanningSatellite(sats.AccessSatellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " dict(prop=\"storage_level_fraction\"),\n", + " dict(prop=\"battery_charge_fraction\")\n", + " ),\n", + " obs.Eclipse(),\n", + " ]\n", + " action_spec = [\n", + " act.Scan(duration=60.0), # Scan for 1 minute\n", + " act.Charge(duration=600.0), # Charge for 10 minutes\n", + " ]\n", + " dyn_type = dyn.ContinuousImagingDynModel\n", + " fsw_type = fsw.ContinuousImagingFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on this class specification, a list of configurable parameters for the satellite\n", + "can be generated." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.577443Z", + "iopub.status.busy": "2024-05-30T19:06:15.577348Z", + "iopub.status.idle": "2024-05-30T19:06:15.581120Z", + "shell.execute_reply": "2024-05-30T19:06:15.580843Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'hs_min': 0.0,\n", + " 'maxCounterValue': 4,\n", + " 'thrMinFireTime': 0.02,\n", + " 'desatAttitude': 'sun',\n", + " 'controlAxes_B': [1, 0, 0, 0, 1, 0, 0, 0, 1],\n", + " 'thrForceSign': 1,\n", + " 'K': 7.0,\n", + " 'Ki': -1,\n", + " 'P': 35.0,\n", + " 'imageAttErrorRequirement': 0.01,\n", + " 'imageRateErrorRequirement': None,\n", + " 'inst_pHat_B': [0, 0, 1],\n", + " 'batteryStorageCapacity': 288000.0,\n", + " 'storedCharge_Init': ()>,\n", + " 'disturbance_vector': None,\n", + " 'dragCoeff': 2.2,\n", + " 'imageTargetMaximumRange': -1,\n", + " 'instrumentBaudRate': 8000000.0,\n", + " 'instrumentPowerDraw': -30.0,\n", + " 'basePowerDraw': 0.0,\n", + " 'wheelSpeeds': ()>,\n", + " 'maxWheelSpeed': inf,\n", + " 'u_max': 0.2,\n", + " 'rwBasePower': 0.4,\n", + " 'rwMechToElecEfficiency': 0.0,\n", + " 'rwElecToMechEfficiency': 0.5,\n", + " 'panelArea': 1.0,\n", + " 'panelEfficiency': 0.2,\n", + " 'nHat_B': array([0, 1, 0]),\n", + " 'mass': 330,\n", + " 'width': 1.38,\n", + " 'depth': 1.04,\n", + " 'height': 1.58,\n", + " 'sigma_init': ()>,\n", + " 'omega_init': ()>,\n", + " 'rN': None,\n", + " 'vN': None,\n", + " 'oe': Basilisk.utilities.orbitalMotion.ClassicElements>,\n", + " 'mu': 398600436000000.0,\n", + " 'dataStorageCapacity': 160000000.0,\n", + " 'storageUnitValidCheck': False,\n", + " 'storageInit': 0,\n", + " 'thrusterPowerDraw': 0.0,\n", + " 'transmitterBaudRate': -8000000.0,\n", + " 'transmitterNumBuffers': 100,\n", + " 'transmitterPowerDraw': -15.0}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "MyScanningSatellite.default_sat_args()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When instantiating a satellite, these parameters can be overriden with a constant or \n", + "rerandomized every time the environment is reset using the ``sat_args`` dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.582425Z", + "iopub.status.busy": "2024-05-30T19:06:15.582343Z", + "iopub.status.idle": "2024-05-30T19:06:15.584737Z", + "shell.execute_reply": "2024-05-30T19:06:15.584473Z" + } + }, + "outputs": [], + "source": [ + "sat_args = {}\n", + "\n", + "# Set some parameters as constants\n", + "sat_args[\"imageAttErrorRequirement\"] = 0.05\n", + "sat_args[\"dataStorageCapacity\"] = 1e10\n", + "sat_args[\"instrumentBaudRate\"] = 1e7\n", + "sat_args[\"storedCharge_Init\"] = 50000.0\n", + "\n", + "# Randomize the initial storage level on every reset\n", + "sat_args[\"storageInit\"] = lambda: np.random.uniform(0.25, 0.75) * 1e10\n", + "\n", + "# Make the satellite\n", + "sat = MyScanningSatellite(name=\"EO1\", sat_args=sat_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making the Environment\n", + "For this example, we will be using the single-agent [SatelliteTasking](../api_reference/index.rst) \n", + "environment. Along with passing the satellite that we configured, the environment takes\n", + "a [scenario](../api_reference/scene/index.rst), which defines the environment the\n", + "satellite is acting in, and a [rewarder](../api_reference/data/index.rst), which defines\n", + "how data collected from the scenario is rewarded." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.586034Z", + "iopub.status.busy": "2024-05-30T19:06:15.585954Z", + "iopub.status.idle": "2024-05-30T19:06:15.685470Z", + "shell.execute_reply": "2024-05-30T19:06:15.685157Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,586 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mCalling env.reset() to get observation space\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,587 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=907950944\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,669 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,683 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,683 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + } + ], + "source": [ + "env = gym.make(\n", + " \"SatelliteTasking-v1\",\n", + " satellite=sat,\n", + " scenario=scene.UniformNadirScanning(),\n", + " rewarder=data.ScanningTimeReward(),\n", + " time_limit=5700.0, # approximately 1 orbit\n", + " log_level=\"INFO\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interacting with the Environment\n", + "\n", + "First, the environment is reset." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.686984Z", + "iopub.status.busy": "2024-05-30T19:06:15.686876Z", + "iopub.status.idle": "2024-05-30T19:06:15.969366Z", + "shell.execute_reply": "2024-05-30T19:06:15.969077Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,787 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,955 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,967 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,967 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + } + ], + "source": [ + "observation, info = env.reset(seed=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we take the scan action (`action=0`) a few times. This allows for the satellite to\n", + "settle its attitude in the nadir pointing mode to satisfy imaging conditions. Note that \n", + "the logs show little or no data accumulated in the first two steps as it settles, but\n", + "achieves 60 reward (corresponding to 60 seconds of imaging) by the third step." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.970790Z", + "iopub.status.busy": "2024-05-30T19:06:15.970681Z", + "iopub.status.idle": "2024-05-30T19:06:15.989442Z", + "shell.execute_reply": "2024-05-30T19:06:15.989200Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,971 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,971 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_nadir_scan tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,971 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 60.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,972 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,975 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 60.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,976 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,976 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,977 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,977 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,977 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_nadir_scan tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,977 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 120.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,978 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<60.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,981 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 120.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,981 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 30.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,982 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,982 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mStep reward: 30.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,982 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,983 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_nadir_scan tasked for 60.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,983 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,983 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<120.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,987 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 180.0 for action_nadir_scan\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,987 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 60.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,987 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,988 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mStep reward: 60.0\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial data level: 0.7341307878 (randomized by sat_args)\n", + " Final data level: 0.8241307878\n" + ] + } + ], + "source": [ + "print(\"Initial data level:\", observation[0], \"(randomized by sat_args)\")\n", + "for _ in range(3):\n", + " observation, reward, terminated, truncated, info = env.step(action=0)\n", + "print(\" Final data level:\", observation[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The observation reflects the increase in stored data. The first element, corresponding\n", + "to `storage_level_fraction`, starts at a random value set by the `storageInit` function\n", + "in `sat_args` and increases based on the time spent imaging.\n", + "\n", + "Finally, the charging mode is tasked repeatedly in 10-minute increments until the\n", + "environment time limit is reached." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2024-05-30T19:06:15.990947Z", + "iopub.status.busy": "2024-05-30T19:06:15.990840Z", + "iopub.status.idle": "2024-05-30T19:06:16.355598Z", + "shell.execute_reply": "2024-05-30T19:06:16.355331Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,991 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,991 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,991 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:15,992 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,024 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 780.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,024 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,047 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,048 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,048 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,048 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,048 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 1380.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,049 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<780.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,080 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 1380.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,080 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,081 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,081 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,081 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,081 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,082 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 1980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,082 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1380.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,113 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 1980.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,113 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,114 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,114 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,114 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,114 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,115 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 2580.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,115 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1980.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,146 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 2580.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,146 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,175 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Charge level: 0.160 (780.0 seconds)\n", + "\tEclipse: start: 5340.0 end: 1800.0\n", + "Charge level: 0.158 (1380.0 seconds)\n", + "\tEclipse: start: 4740.0 end: 1200.0\n", + "Charge level: 0.155 (1980.0 seconds)\n", + "\tEclipse: start: 4140.0 end: 600.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,175 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,175 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,176 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,176 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 3180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,176 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<2580.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,207 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 3180.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,207 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,208 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,208 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,208 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,209 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,209 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 3780.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,209 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3180.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,240 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 3780.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,241 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,241 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,241 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,242 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,242 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,242 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 4380.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,242 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<3780.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,274 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 4380.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Charge level: 0.175 (2580.0 seconds)\n", + "\tEclipse: start: 3540.0 end: 5670.0\n", + "Charge level: 0.763 (3180.0 seconds)\n", + "\tEclipse: start: 2940.0 end: 5070.0\n", + "Charge level: 1.000 (3780.0 seconds)\n", + "\tEclipse: start: 2340.0 end: 4470.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,275 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,276 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,277 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 4980.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,277 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4380.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,309 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 4980.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,309 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,310 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,310 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,311 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,311 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,311 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 5580.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,311 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<4980.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,344 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[mtimed termination at 5580.0 for action_charge\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,344 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mSatellites requiring retasking: ['EO1_11826041840']\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,345 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,345 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[maction_charge tasked for 600.0 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,346 \u001b[0m\u001b[36msats.satellite.EO1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[36mEO1: \u001b[0m\u001b[msetting timed terminal event at 6180.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,346 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5580.00> \u001b[0m\u001b[mRunning simulation at most to 5700.00 seconds\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,353 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5700.00> \u001b[0m\u001b[mData reward: {'EO1_11826041840': 0.0}\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,353 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5700.00> \u001b[0m\u001b[mStep reward: 0.0\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,353 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5700.00> \u001b[0m\u001b[mEpisode terminated: False\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2024-05-30 12:06:16,354 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<5700.00> \u001b[0m\u001b[mEpisode truncated: True\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Charge level: 1.000 (4380.0 seconds)\n", + "\tEclipse: start: 1740.0 end: 3870.0\n", + "Charge level: 1.000 (4980.0 seconds)\n", + "\tEclipse: start: 1140.0 end: 3270.0\n", + "Charge level: 1.000 (5580.0 seconds)\n", + "\tEclipse: start: 540.0 end: 2670.0\n", + "Charge level: 1.000 (5700.0 seconds)\n", + "\tEclipse: start: 420.0 end: 2550.0\n" + ] + } + ], + "source": [ + "while not truncated:\n", + " observation, reward, terminated, truncated, info = env.step(action=1)\n", + " print(f\"Charge level: {observation[1]:.3f} ({env.unwrapped.simulator.sim_time:.1f} seconds)\\n\\tEclipse: start: {observation[2]:.1f} end: {observation[3]:.1f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is observed that the battery decrease while the satellite is in eclipse, but once the\n", + "satellite is out of eclipse, the battery quickly increases to full charge." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/_images/static/Basilisk-Logo.png b/docs/source/_images/static/Basilisk-Logo.png deleted file mode 100644 index d0b4cbd51a6787bf4855646b70561df3aa0b440e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65064 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 diff --git a/docs/source/_images/static/bsk_rl-logo.png b/docs/source/_images/static/bsk_rl-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d5edcee9339e36bf1d5562f50dd5979f697239ee GIT binary patch literal 206259 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 p.caption > span.caption-text { +div.wy-menu>p.caption>span.caption-text { color: white; font-weight: bold; } -a.el, a.el:visited { + +a.el, +a.el:visited { color: #565A5C; } .wy-side-nav-search>div.version { - color: #565A5C; # CU dark gray + color: #565A5C; } -@media screen and (min-width: 1100px) { - .wy-nav-side, - .wy-grid-for-nav { - margin-left: calc(((100vw - 1100px) / 2)); - } - .wy-nav-content-wrap { - background: none; - } - .wy-body-for-nav { - background: #565A5C; - } +.wy-nav-content { + max-width: 900px; } -code span.pre, code { +@media screen and (min-width: 1200px) { + + .wy-nav-side, + .wy-grid-for-nav { + margin-left: calc(((100vw - 1200px) / 2)); + } + + .wy-nav-content-wrap { + background: none; + } + + .wy-body-for-nav { + background: #565A5C; + } +} + +div.nboutput.container div.output_area.stderr { + background: rgb(233, 242, 249); +} + +div.nbinput.nblast.docutils.container { + margin-bottom: 15px !important; +} + +div.nboutput.nblast.docutils.container { + margin-bottom: 15px !important; +} + + +code span.pre, +code { color: #cb7ccf; } -th.head, .wy-nav-top { +th.head, +.wy-nav-top { color: white; background-color: #565A5C; } @@ -48,28 +80,41 @@ th.head p { margin: 0px; } -tr td p, th.head p { +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; +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: normal; } .math { - text-align: left; + text-align: left; } + .eqno { - float: right; + 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; +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 { @@ -80,15 +125,19 @@ body, h1, h2, h3, h4, h5, .rst-content, .sidebar, .sidebar-title, p.caption { max-width: 500px; } -code, .rst-content tt, .rst-content code { +code, +.rst-content tt, +.rst-content code { color: #E74C3C; } -ul.simple li, aside.sidebar ul li { +ul.simple li, +aside.sidebar ul li { all: revert; } -ul.simple li p, aside.sidebar ul li p { +ul.simple li p, +aside.sidebar ul li p { all: revert; line-height: 24px; font-size: 16px; @@ -99,7 +148,8 @@ ul.simple ul li { list-style-type: circle; } -ul.simple, aside.sidebar ul { +ul.simple, +aside.sidebar ul { all: revert; padding-left: 0em; } @@ -109,13 +159,43 @@ figure { } @media (prefers-color-scheme: dark) { - .wy-nav-content, .wy-body-for-nav, .wy-nav-content-wrap, math, span[id*='MathJax-Span'] { + + .wy-nav-content, + .wy-body-for-nav, + .wy-nav-content-wrap, + math, + span[id*='MathJax-Span'] { background-color: black; - color: #ccc; + color: #ccc; + } + + 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(.citation):not(.glossary):not(.simple)>dt { + background-color: rgb(45, 65, 81); + } + + html.writer-html4 .rst-content dl:not(.docutils) .descclassname, + html.writer-html4 .rst-content dl:not(.docutils) .descname, + html.writer-html4 .rst-content dl:not(.docutils) .sig-name, + html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname, + html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname, + html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name { + color: rgb(237, 240, 242); + } + + html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt, + html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt { + background-color: #333; + ; + } + + html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt, + html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt { + color: rgb(41, 128, 185); } .highlight .go { - color: #ccc; + color: #ccc; } img.logo { @@ -125,19 +205,27 @@ figure { .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 { + + 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 { + .rst-content .warning, + .rst-content .caution, + .rst-content .attention { background: #51402F; } - .rst-content .important, .rst-content .hint, .rst-content .tip { + .rst-content .important, + .rst-content .hint, + .rst-content .tip { background: #275145; } - .rst-content .danger, .rst-content .error { + .rst-content .danger, + .rst-content .error { background: #523A37; } @@ -145,15 +233,19 @@ figure { .sidebar { border-color: #666; } + .rst-content .sidebar { background: #222; } + .rst-content .sidebar .sidebar-title { background: #666; } - .btn-neutral, .btn-neutral:hover, .btn:visited { + .btn-neutral, + .btn-neutral:hover, + .btn:visited { background: #333 !important; color: #ccc !important; } @@ -166,16 +258,26 @@ figure { 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 { + .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%);; + filter: invert(100%) hue-rotate(180deg) saturate(200%); + ; } - img[src$="jpg"], img[src$="png"] { + img[src$="jpg"], + img[src$="png"] { border-radius: 5px; } @@ -187,21 +289,27 @@ figure { padding-top: 1em; } - .rst-content dl:not(.docutils) code.descclassname, .rst-content dl:not(.docutils) code.descname { - color: #ccc; + .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; + 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: black; } - .wy-table caption, .rst-content table.docutils caption, .rst-content table.field-list caption { + .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.vm, + span.nf, + span.nn { + color: #66f !important; } span.normal { @@ -209,10 +317,14 @@ figure { } td.linenos pre { - background: #ccc !important; + background: #ccc !important; } .rst-content .highlighted { background-color: #333; } + + div.nboutput.container div.output_area.stderr { + background: rgb(9, 11, 12); + } } \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index e9c21996..6893c644 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,15 +5,20 @@ import datetime +import importlib # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import re +import shutil import sys from importlib import metadata from pathlib import Path +from nbdime.diffing.notebooks import diff_notebooks +from nbdime.utils import read_notebook + # sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "src"))) now = datetime.datetime.now() @@ -34,6 +39,7 @@ "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx_rtd_theme", + "nbsphinx", ] templates_path = ["_templates"] @@ -45,6 +51,9 @@ autodoc_default_options = { "undoc-members": None, } +autodoc_typehints = "both" +# nbsphinx_execute = "never" +nbsphinx_allow_errors = True # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -64,7 +73,7 @@ } html_static_path = ["_static"] html_css_files = ["custom.css"] -html_logo = "./_images/static/Basilisk-Logo.png" +html_logo = "./_images/static/bsk_rl-logo.png" add_module_names = False @@ -77,22 +86,34 @@ def setup(app): app.connect("autodoc-skip-member", skip) -class FileCrawler: - def __init__(self, base_source_dir, base_doc_dir): +def has_all(qual_name): + try: + all = importlib.import_module(qual_name).__all__ + if len(all) == 0: + return False + except AttributeError: + return False + + return True + + +class PackageCrawler: + def __init__( + self, base_source_dir, base_doc_dir, filter_all=True, nb_cache_dir=None + ): self.base_source_dir = base_source_dir self.base_doc_dir = base_doc_dir + self.filter_all = filter_all + self.nb_cache_dir = nb_cache_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", - r".*\/data", + r".*\/_.*", ] dirs_in_dir = list( filter( @@ -103,10 +124,10 @@ def grab_files(self, dir_path): ) ) + files_in_dir = dir_path.glob("*.py") file_filters = [ r".*__init__\.py", r"(.*\/|)_[a-zA-Z0-9_]*\.py", - r".*types.py", ] files_in_dir = list( filter( @@ -117,11 +138,34 @@ def grab_files(self, dir_path): ) ) - return sorted(list(files_in_dir)), sorted(list(dirs_in_dir)) + if self.filter_all: + files_in_dir = list( + filter( + lambda file: has_all( + str(file.relative_to(self.base_source_dir.parent)) + .replace("/", ".") + .replace(".py", "") + ), + files_in_dir, + ) + ) - def populate_doc_index(self, index_path, file_paths, dir_paths, source_dir): - name = index_path.stem + notebooks_in_dir = dir_path.glob("*.ipynb") + + return ( + sorted(list(files_in_dir)), + sorted(list(dirs_in_dir)), + sorted(list(notebooks_in_dir)), + ) + + def generate_index(self, index_path, file_paths, dir_paths, source_dir): + # Make header lines = "" + qual_name = str(source_dir.relative_to(self.base_source_dir.parent)).replace( + "/", "." + ) + lines += ".. _" + qual_name.replace(" ", "_") + ":\n" + lines += f".. currentmodule:: {qual_name}\n\n" # if a _default.rst file exists in a folder, then use it to generate the index.rst file try: @@ -131,36 +175,43 @@ def populate_doc_index(self, index_path, file_paths, dir_paths, source_dir): 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 + docContents = "" 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 + # Title the page + if "\n===" not in docContents: + # Make title + doc_title = qual_name + try: + doc_title = importlib.import_module(qual_name).__doc_title__ + except AttributeError: + pass + lines += doc_title + "\n" + "=" * len(doc_title) + "\n\n" + + lines += docContents + "\n\n" + # Also check for docs in the __init__.py file + # lines += "\n\nReference\n----------\n\n" lines += ( """.. automodule:: """ + qual_name + """\n :members:\n :show-inheritance:\n\n""" ) + if len(file_paths) > 0 or len(dir_paths) > 0: + # lines += "\n\nModules\n----------\n\n" + pass + # Add a linking point to all local files - lines += ( - """\n\n.. toctree::\n :maxdepth: 1\n :caption: """ + "Files:\n\n" - ) + lines += "\n\n.. toctree::\n :maxdepth: 1\n :hidden:\n\n" added_names = [] for file_path in sorted(file_paths): file_name = os.path.basename(os.path.normpath(file_path)) @@ -172,9 +223,7 @@ def populate_doc_index(self, index_path, file_paths, dir_paths, source_dir): lines += "\n" # Add a linking point to all local directories - lines += ( - """.. toctree::\n :maxdepth: 1\n :caption: """ + "Directories:\n\n" - ) + lines += ".. toctree::\n :maxdepth: 1\n :hidden:\n\n" for dir_path in sorted(dir_paths): dirName = os.path.basename(os.path.normpath(dir_path)) @@ -184,18 +233,29 @@ def populate_doc_index(self, index_path, file_paths, dir_paths, source_dir): f.write(lines) def generate_autodoc(self, doc_path, source_file): + + # Make header short_name = source_file.name.replace(".py", "") + lines = "" qual_name = ( str(source_file.relative_to(self.base_source_dir.parent)) .replace("/", ".") .replace(".py", "") ) + lines += ".. _" + qual_name.replace(" ", "_") + ":\n" + lines += f".. currentmodule:: {qual_name}\n\n" + + # Make title + doc_title = short_name + try: + doc_title = importlib.import_module(qual_name).__doc_title__ + except (AttributeError, ModuleNotFoundError): + pass + lines += doc_title + "\n" + "=" * len(doc_title) + "\n\n" # 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 += f"``{qual_name}``\n\n" + lines += ".. toctree::\n :maxdepth: 1\n\n" lines += ( """.. automodule:: """ + qual_name @@ -210,13 +270,13 @@ 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) + file_paths, dir_paths, nb_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 + self.generate_index( + self.base_doc_dir / index_path, file_paths + nb_paths, dir_paths, source_dir ) # Generate the correct auto-doc function for python modules @@ -226,6 +286,35 @@ def run(self, source_dir=None): file, ) + for notebook in nb_paths: + nb_cache = ( + self.nb_cache_dir / self.base_doc_dir / index_path / notebook.name + ) + if ( + self.nb_cache_dir is not None + and nb_cache.is_file() + and ( + "'source'" + not in diff_notebooks( + read_notebook(notebook.resolve(), on_null="empty"), + read_notebook(nb_cache.resolve(), on_null="empty"), + ).__repr__() + ) + and ( + "'source'" + not in diff_notebooks( + read_notebook(nb_cache.resolve(), on_null="empty"), + read_notebook(notebook.resolve(), on_null="empty"), + ).__repr__() + ) + ): + shutil.copy( + nb_cache, + self.base_doc_dir / index_path / notebook.name, + ) + else: + shutil.copy(notebook, self.base_doc_dir / index_path / notebook.name) + # Recursively go through all directories in source, documenting what is available. for dir_path in sorted(dir_paths): self.run( @@ -236,5 +325,13 @@ def run(self, source_dir=None): sys.path.append(os.path.abspath("../..")) -FileCrawler(Path("../../src/bsk_rl/"), Path("./API Reference/")).run() -FileCrawler(Path("../../examples"), Path("./Examples/")).run() +nb_cache_dir = Path("../build/doctrees/nbsphinx") +PackageCrawler( + Path("../../src/bsk_rl/"), Path("./api_reference/"), nb_cache_dir=nb_cache_dir +).run() +PackageCrawler( + Path("../../examples"), + Path("./examples/"), + filter_all=False, + nb_cache_dir=nb_cache_dir, +).run() diff --git a/docs/source/index.rst b/docs/source/index.rst index bc149405..ea26c295 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,35 +1,48 @@ -BSK-RL: Environments and Algorithms for Spacecraft Planning and Scheduling -========================================================================== +BSK-RL: Environments for Spacecraft Planning and Scheduling +=========================================================== .. toctree:: :hidden: install - Examples/index - API Reference/index + examples/index + api_reference/index + release_notes publications citation GitHub -.. note:: +.. warning:: + + Docs are currently being build from the ``refactor/v1_0_0``. This branch will be + merged into ``develop`` imminently, at which point this warning will go away. - BSK-RL and its documentation are under active development. Please continue to check back for updates. .. warning:: - With the 1.0.0 release, one-off environments and associated scripts have been deprecated. The :code:`envs.general_satellite_tasking` module has been renamed to :code:`env`. + The 1.0.0 release has significant changes from previous versions. See the + :doc:`Release Notes ` for more information. -**BSK-RL** (`Basilisk `_ + `Reinforcement Learning `_) is a Python package for constructing `Gymnasium `_ environments for spacecraft tasking problems. It is built on top of `Basilisk `_, a modular and fast spacecraft simulation framework, making the simulation environments high-fidelity and computationally efficient. BSK-RL also includes a collection of agents, training scripts, and examples for working with these environments. +**BSK-RL** (`Basilisk `_ + +`Reinforcement Learning `_) is a +Python package for constructing `Gymnasium `_ +environments for spacecraft tasking problems. It is built on top of +`Basilisk `_, a modular and fast spacecraft +simulation framework, making the simulation environments high-fidelity and computationally +efficient. BSK-RL also includes a collection of agents, training scripts, and examples +for working with these environments. Quickstart ---------- Installation ^^^^^^^^^^^^ -Complete installation instructions and common troubleshooting tips can be found :doc:`here `. To install BSK-RL: +Complete installation instructions and common troubleshooting tips can be found +:doc:`here `. To install BSK-RL: -#. Install the `Basilisk `_ spacecraft simulation framework. +#. Install the `Basilisk `_ spacecraft simulation + framework. #. Clone BSK-RL. .. code-block:: console @@ -46,45 +59,24 @@ Complete installation instructions and common troubleshooting tips can be found .. code-block:: console - (.venv) $ pytest ./tests/examples + (.venv) $ pytest . Construct an Environment ^^^^^^^^^^^^^^^^^^^^^^^^ -TODO: Add more detail to this example - -.. code-block:: python - - import gymnasium as gym - - from bsk_rl.env.scenario import data - from bsk_rl.env.scenario import satellites as sats - from bsk_rl.env.scenario.environment_features import StaticTargets - from bsk_rl.env.simulation import environment - from bsk_rl.utils.orbital import random_orbit - - env = gym.make( - "SingleSatelliteTasking-v1", - satellites=sats.FullFeaturedSatellite( - "EO1", - sats.FullFeaturedSatellite.default_sat_args(oe=random_orbit), n_ahead_observe=30, - n_ahead_act=15 - ), - env_features=StaticTargets(n_targets=1000), - data_manager=data.UniqueImagingManager, - max_step_duration=600.0, - time_limit=5700.0, - terminate_on_time_limit=True, - ) -Train an Agent -^^^^^^^^^^^^^^ -Show RLLib or SB3 configs here. +A quick but comprehensive tutorial can be found at :doc:`examples/simple_environment`. Acknowledgements ---------------- -BSK-RL is developed by the `Autonomous Vehicle Systems (AVS) Lab `_ at the University of Colorado Boulder. The AVS Lab is part of the `Colorado Center for Astrodynamics Research (CCAR) `_ and the `Department of Aerospace Engineering Sciences `_. +BSK-RL is developed by the `Autonomous Vehicle Systems (AVS) Lab `_ +at the University of Colorado Boulder. The AVS Lab is part of the `Colorado Center for Astrodynamics Research (CCAR) `_ +and the `Department of Aerospace Engineering Sciences `_. -Development has been supported by NASA Space Technology Graduate Research Opportunity (NSTGRO) grants, 80NSSC20K1162 and 80NSSC23K1182. This work has also been supported by Air Force Research Lab grant FA9453-22-2-0050. +Development has been supported by NASA Space Technology Graduate Research Opportunity +(NSTGRO) grants, 80NSSC20K1162 and 80NSSC23K1182. This work has also been supported by +Air Force Research Lab grant FA9453-22-2-0050. -Development of this software has utilized the Alpine high performance computing resource at the University of Colorado Boulder. Alpine is jointly funded by the University of Colorado Boulder, the University of Colorado Anschutz, and Colorado State University. \ No newline at end of file +Development of this software has utilized the Alpine high performance computing resource +at the University of Colorado Boulder. Alpine is jointly funded by the University of +Colorado Boulder, the University of Colorado Anschutz, and Colorado State University. \ No newline at end of file diff --git a/docs/source/install.rst b/docs/source/install.rst index fbbf37fa..66bc9bd4 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -7,7 +7,10 @@ Installation Instructions ------------ -#. Install the `Basilisk `_ spacecraft simulation framework, following instructions for the appropriate operating system. Installation on MacOS and Linux is preferable to Windows. Use a Python virtual environment as suggested in the Basilisk installation instructions. +#. Install the `Basilisk `_ spacecraft + simulation framework, following instructions for the appropriate operating system. + Installation on MacOS and Linux is preferable to Windows. Use a Python virtual + environment as suggested in the Basilisk installation instructions. #. Clone the BSK-RL repository. .. code-block:: console @@ -20,29 +23,60 @@ Instructions $ cd bsk_rl -#. Ensure that the virtual environment Basilisk is installed in is active. Install BSK-RL with the following command. +#. Ensure that the virtual environment Basilisk is installed in is active. Install + BSK-RL with the following command. .. code-block:: console - (.venv) $ python -m pip install -e . && finish_install + (.venv) $ python -m pip install -e "." && finish_install - The first half of this command will install ``pip`` dependencies and an editable copy of the BSK-RL package. ``finish_install`` downloads data dependencies and other packages not available through ``pip``. The installation of Basilisk is also verified at this step. + The first half of this command will install ``pip`` dependencies and an editable copy + of the BSK-RL package. ``finish_install`` downloads data dependencies and verifies the + installation of Basilisk. -#. Test the installation by running the example scripts from the base directory. + For a more granular installation, ``.[docs]`` (for documentation dependencies) or + ``.[rllib]`` (for RLlib tools) can be specified. ``.[all]`` installs all dependencies. + +#. Test the installation by running the unit tests and integration tests. .. code-block:: console - (.venv) $ pytest tests/examples + (.venv) $ pytest tests/unittest + (.venv) $ pytest tests/integration + + The installation can also be verified by running :doc:`examples` from the ``examples`` + directory. - For additional verification, the unit tests and integration tests can also be executed. +#. To build documentation locally, run: .. code-block:: console - (.venv) $ pytest tests/unittest - (.venv) $ pytest tests/integration + (.venv) $ cd docs + (.venv) $ make html + (.venv) $ make view Common Issues ------------- -Please report new installation issues on GitHub. \ No newline at end of file +Please report new installation issues on GitHub. + +SPICE Errors +^^^^^^^^^^^^ + +Errors such as + + .. code-block:: console + + Toolkit version: N0065 + + SPICE(NOSUCHFILE) -- + + The attempt to load + "/home/user/basilisk/dist3/Basilisk/supportData/EphemerisData/de430.bsp" by + the routine FURNSH failed. It could not be located. + + A traceback follows. The name of the highest level module is first. + furnsh_c --> FURNSH --> ZZLDKER + +can be resolved by ensuring that `Basilisk is installed using git-lfs `_. diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst new file mode 100644 index 00000000..d6db0189 --- /dev/null +++ b/docs/source/release_notes.rst @@ -0,0 +1,18 @@ +Release Notes +============= + +Version 1.0.0 +------------- +*Release Date: MMM. DD, YYYY* + +First major release of BSK-RL. + +* Refactored the repository to prioritize use of the :class:`~bsk_rl.GeneralSatelliteTasking` + environment. The general environment is now at the base level of ``bsk_rl``. +* Renamed various elements of the environment for simplicity and clarity. See the + :ref:`bsk_rl` for further details. +* Refactored the satellite :ref:`bsk_rl.obs` and :ref:`bsk_rl.act` specification + to be more clear and avoid conflicting variable names. +* Rewrote the documentation and added useful :ref:`examples`. +* Deprecated one-off environments and training scripts. These are still accessible + in the `git history of the repository `_. \ No newline at end of file diff --git a/examples/_default.rst b/examples/_default.rst new file mode 100644 index 00000000..b0eb3b4b --- /dev/null +++ b/examples/_default.rst @@ -0,0 +1,10 @@ +Examples +======== + +.. toctree:: + :maxdepth: 1 + + simple_environment + satellite_configuration + rllib_training + multiagent_envs diff --git a/examples/configurations/aeos.py b/examples/configurations/aeos.py deleted file mode 100644 index 301d8315..00000000 --- a/examples/configurations/aeos.py +++ /dev/null @@ -1,44 +0,0 @@ -class TargetInfoSat( - sa.ImagingActions, - so.TimeState, - DensityState.configure( - density_interval=60 * 5, density_windows=20, density_normalization=5 - ), - so.TargetState.configure( - target_properties=[ - dict(prop="priority"), - dict(prop="r_TB_H", norm=800 * 1e3), - dict(prop="theta_error", norm=np.pi / 2), - dict(prop="omega_error", norm=0.03), - ] - ), - so.NormdPropertyState.configure( - obs_properties=[ - dict(prop="omega_BH_H", module="dynamics", norm=0.03), - dict(prop="c_hat_H", module="fsw"), - dict(prop="r_BN_P", module="dynamics", norm=orbitalMotion.REQ_EARTH * 1e3), - dict(prop="v_BN_P", module="dynamics", norm=7616.5), - ] - ), - sats.SteeringImagerSatellite, -): - pass - - -class TargetInfoSat(sats.SteeringImagerSatellite, sa.ImagingActions): - observation_spec = [ - obs.SatProperty( - dict(prop="priority"), - dict(prop="r_TB_H", norm=800 * 1e3), - dict(prop="theta_error", norm=np.pi / 2), - dict(prop="omega_error", norm=0.03), - ), - obs.TargetProperties( - dict(prop="omega_BH_H", module="dynamics", norm=0.03), - dict(prop="c_hat_H", module="fsw"), - dict(prop="r_BN_P", module="dynamics", norm=orbitalMotion.REQ_EARTH * 1e3), - dict(prop="v_BN_P", module="dynamics", norm=7616.5), - n_ahead_observe=32, - ), - obs.Time(), - ] diff --git a/examples/configurations/multisat_aeos.py b/examples/configurations/multisat_aeos.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/configurations/safety_eos.py b/examples/configurations/safety_eos.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/configurations/todo b/examples/configurations/todo deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/multiagent_envs.ipynb b/examples/multiagent_envs.ipynb new file mode 100644 index 00000000..8804646d --- /dev/null +++ b/examples/multiagent_envs.ipynb @@ -0,0 +1,358 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Agent Environments\n", + "\n", + "Two multiagent environments are given in the package:\n", + "\n", + "* [GeneralSatelliteTasking](../api_reference/index.rst#bsk_rl.GeneralSatelliteTasking), \n", + " a [Gymnasium](https://gymnasium.farama.org)-based environment and the basis for all other environments.\n", + "* [ConstellationTasking](../api_reference/index.rst#bsk_rl.ConstellationTasking), which\n", + " implements the [PettingZoo parallel API](https://pettingzoo.farama.org/api/parallel/).\n", + "\n", + "The latter is preferable for multi-agent RL (MARL) settings, as most algorithms are designed\n", + "for this kind of API.\n", + "\n", + "## Configuring the Environment\n", + "\n", + "For this example, a multisatellite target imaging environment will be used. The goal is\n", + "to maximize the value of unique images taken.\n", + "\n", + "As usual, the satellite type is defined first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl import sats, act, obs, scene, data, comm\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "class ImagingSatellite(sats.ImagingSatellite):\n", + " observation_spec = [\n", + " obs.OpportunityProperties(\n", + " dict(prop=\"priority\"), \n", + " dict(prop=\"opportunity_open\", norm=5700.0),\n", + " n_ahead_observe=10,\n", + " )\n", + " ]\n", + " action_spec = [act.Image(n_ahead_image=10)]\n", + " dyn_type = dyn.FullFeaturedDynModel\n", + " fsw_type = fsw.SteeringImagerFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Satellite properties are set to give the satellite near-unlimited power and storage\n", + "resources, and put the satellite at a 800 km orbit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from bsk_rl.utils.orbital import random_orbit\n", + "\n", + "sat_args = dict(\n", + " imageAttErrorRequirement=0.01,\n", + " imageRateErrorRequirement=0.01,\n", + " batteryStorageCapacity=1e9,\n", + " storedCharge_Init=1e9,\n", + " dataStorageCapacity=1e12,\n", + " u_max=0.4,\n", + " K1=0.25,\n", + " K3=3.0,\n", + " omega_max=0.087,\n", + " servo_Ki=5.0,\n", + " servo_P=150 / 5,\n", + " oe=lambda: random_orbit(alt=800),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Gym API\n", + "\n", + "GeneralSatelliteTasking uses tuples of actions and observations to interact with the\n", + "environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl import GeneralSatelliteTasking\n", + "\n", + "env = GeneralSatelliteTasking(\n", + " satellites=[\n", + " ImagingSatellite(\"EO-1\", sat_args),\n", + " ImagingSatellite(\"EO-2\", sat_args),\n", + " ImagingSatellite(\"EO-3\", sat_args),\n", + " ],\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " communicator=comm.LOSCommunication(), # Note that dyn must inherit from LOSCommunication\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.observation_space" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.action_space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Consequently, actions are passed as a tuple. The step will stop the first time any\n", + "satellite completes an action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, reward, terminated, truncated, info = env.step([7, 9, 8])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point, either every satellite can be retasked, or satellites can continue their\n", + "previous action by passing `None` as the action. To see which satellites must be\n", + "retasked (i.e. their previous action is done and they have nothing more to do), look at\n", + "`info[\"requires_retasking\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "info[\"requires_retasking\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on this list, we decide here to only retask the satellite that needs it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "actions = [None, None, None]\n", + "actions[int(info[\"requires_retasking\"][0][3]) - 1] = 7\n", + "actions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, reward, terminated, truncated, info = env.step(actions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this environment, the environment will stop if any agent dies. To demonstrate this,\n", + "one satellite is forcibly killed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from Basilisk.architecture import messaging\n", + "\n", + "def isnt_alive(log_failure=False):\n", + " \"\"\"Mock satellite 0 dying.\"\"\"\n", + " self = env.unwrapped.satellites[0]\n", + " death_message = messaging.PowerStorageStatusMsgPayload()\n", + " death_message.storageLevel = 0.0\n", + " self.dynamics.powerMonitor.batPowerOutMsg.write(death_message)\n", + " return self.dynamics.is_alive(log_failure=log_failure) and self.fsw.is_alive(\n", + " log_failure=log_failure\n", + " )\n", + "\n", + "env.unwrapped.satellites[0].is_alive = isnt_alive\n", + "observation, reward, terminated, truncated, info = env.step([6, 7, 9])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PettingZoo API\n", + "\n", + "The [PettingZoo parallel API](https://pettingzoo.farama.org/api/parallel/) environment, \n", + "ConstellationTasking, is largely the same as GeneralSatelliteTasking. See their\n", + "documentation for a full description of the API. It tends to separate things into\n", + "dictionaries keyed by agent, rather than tuples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl import ConstellationTasking\n", + "\n", + "env = ConstellationTasking(\n", + " satellites=[\n", + " ImagingSatellite(\"EO-1\", sat_args),\n", + " ImagingSatellite(\"EO-2\", sat_args),\n", + " ImagingSatellite(\"EO-3\", sat_args),\n", + " ],\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " communicator=comm.LOSCommunication(), # Note that dyn must inherit from LOSCommunication\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.observation_spaces" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.action_spaces" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Actions are passed as a dictionary; the agent names can be accessed through the `agents`\n", + "property." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, reward, terminated, truncated, info = env.step(\n", + " {\n", + " env.agents[0]: 7,\n", + " env.agents[1]: 9,\n", + " env.agents[2]: 8,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other than compatibility with MARL algorithms, the main benefit of the PettingZoo API\n", + "is that it allows for individual agents to fail without terminating the entire environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Immediately kill satellite 0\n", + "env.unwrapped.satellites[0].is_alive = isnt_alive\n", + "env.agents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, reward, terminated, truncated, info = env.step({\n", + " env.agents[0]: 7,\n", + " env.agents[1]: 9,\n", + " }\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/rllib_training.ipynb b/examples/rllib_training.ipynb new file mode 100644 index 00000000..43c48fae --- /dev/null +++ b/examples/rllib_training.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Training with RLlib PPO\n", + "[RLlib](https://docs.ray.io/en/latest/rllib/index.html) is a high-performance, distributed\n", + "reinforcement learning library. It is preferable to other RL libraries (e.g. Stable Baselines\n", + "3) for `bsk_rl` environments because it steps environments copies asynchronously; because \n", + "of the variable step lengths, variable episode step counts, and long episode reset times, \n", + "stepping each environment independently can increase step throughput by 2-5 times.\n", + "\n", + "

\n", + "\n", + "\n", + "## Define the Environment\n", + "A nadir-scanning environment is created, to the one used in [this paper](https://hanspeterschaub.info/Papers/Stephenson2024.pdf). \n", + "The satellite has to collect data while managing the data buffer level and battery level.\n", + "\n", + "First, the satellite class is defined. A custom dynamics model is created that defines\n", + "a few additional properties to use in the state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from bsk_rl import act, data, obs, sats, scene\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "class ScanningDownlinkDynModel(dyn.ContinuousImagingDynModel, dyn.GroundStationDynModel):\n", + " # Define some custom properties to be accessed in the state\n", + " @property\n", + " def instrument_pointing_error(self) -> float:\n", + " r_BN_P_unit = self.r_BN_P/np.linalg.norm(self.r_BN_P) \n", + " c_hat_P = self.satellite.fsw.c_hat_P\n", + " return np.arccos(np.dot(-r_BN_P_unit, c_hat_P))\n", + " \n", + " @property\n", + " def solar_pointing_error(self) -> float:\n", + " a = self.world.gravFactory.spiceObject.planetStateOutMsgs[\n", + " self.world.sun_index\n", + " ].read().PositionVector\n", + " a_hat_N = a / np.linalg.norm(a)\n", + " nHat_B = self.satellite.sat_args[\"nHat_B\"]\n", + " NB = np.transpose(self.BN)\n", + " nHat_N = NB @ nHat_B\n", + " return np.arccos(np.dot(nHat_N, a_hat_N))\n", + "\n", + "class ScanningSatellite(sats.AccessSatellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " dict(prop=\"storage_level_fraction\"),\n", + " dict(prop=\"battery_charge_fraction\"),\n", + " dict(prop=\"wheel_speeds_fraction\"),\n", + " dict(prop=\"instrument_pointing_error\", norm=np.pi),\n", + " dict(prop=\"solar_pointing_error\", norm=np.pi)\n", + " ),\n", + " obs.Eclipse(),\n", + " obs.OpportunityProperties(\n", + " dict(prop=\"opportunity_open\", norm=5700),\n", + " dict(prop=\"opportunity_close\", norm=5700),\n", + " type=\"ground_station\",\n", + " n_ahead_observe=1,\n", + " ),\n", + " ]\n", + " action_spec = [\n", + " act.Scan(duration=180.0),\n", + " act.Charge(duration=180.0),\n", + " act.Downlink(duration=60.0),\n", + " act.Desat(duration=60.0),\n", + " ]\n", + " dyn_type = ScanningDownlinkDynModel\n", + " fsw_type = fsw.ContinuousImagingFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, parameters are set. Since this scenario is focused on maintaining acceptable data\n", + "and power levels, these are tuned to create a sufficiently interesting mission." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sat = ScanningSatellite(\n", + " \"Scanner-1\",\n", + " sat_args=dict(\n", + " # Data\n", + " dataStorageCapacity=5000 * 8e6, # MB to bits\n", + " storageInit=lambda: np.random.uniform(0, 5000 * 8e6),\n", + " instrumentBaudRate=0.5e6,\n", + " transmitterBaudRate=-112e6,\n", + " # Power\n", + " batteryStorageCapacity=400 * 3600, # Wh to W*s\n", + " storedCharge_Init=lambda: np.random.uniform(400 * 3600 * 0.2, 400 * 3600 * 0.8),\n", + " basePowerDraw=-10.0,\n", + " instrumentPowerDraw=-30.0,\n", + " transmitterPowerDraw=-25.0,\n", + " thrusterPowerDraw=-80.0,\n", + " # Attitude\n", + " imageAttErrorRequirement=0.1,\n", + " imageRateErrorRequirement=0.1,\n", + " disturbance_vector=lambda: np.random.normal(scale=0.0001, size=3),\n", + " maxWheelSpeed=6000.0, # RPM\n", + " wheelSpeeds=lambda: np.random.uniform(-3000, 3000, 3),\n", + " desatAttitude=\"nadir\",\n", + " nHat_B=np.array([0, 0, -1]), # Solar panel orientation\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the environment arguments are set. Stepping through this environment is \n", + "demonstrated at the bottom of the page." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "duration = 3 * 5700.0\n", + "env_args = dict(\n", + " satellite=sat,\n", + " scenario=scene.UniformNadirScanning(value_per_second=1/duration),\n", + " rewarder=data.ScanningTimeReward(),\n", + " time_limit=duration,\n", + " failure_penalty=-1.0,\n", + " terminate_on_time_limit=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure Ray and PPO\n", + "\n", + "The `bsk_rl` package supplies a utility to make logging information at the end of episodes\n", + "easier. This is useful to see how an agent's policy is changing over time, using a\n", + "monitoring program such as [TensorBoard](https://www.tensorflow.org/tensorboard)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl.utils.rllib import EpisodeDataCallbacks\n", + "\n", + "class CustomDataCallbacks(EpisodeDataCallbacks):\n", + " def pull_env_metrics(self, env):\n", + " reward = env.rewarder.cum_reward\n", + " orbits = env.simulator.sim_time / (95 * 60)\n", + "\n", + " data = dict(\n", + " reward=reward,\n", + " reward_per_orbit=reward / orbits,\n", + " # Are satellites dying, and how and when?\n", + " alive=float(env.satellite.is_alive()),\n", + " rw_status_valid=float(env.satellite.dynamics.rw_speeds_valid()),\n", + " battery_status_valid=float(env.satellite.dynamics.battery_valid()),\n", + " orbits_complete=orbits,\n", + " )\n", + " if not env.satellite.is_alive():\n", + " data[\"orbits_complete_partial_only\"] = orbits\n", + " return data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, PPO (or some other algorithm) can be configured. Of particular importance\n", + "are setting `sample_timeout_s` and `metrics_episode_collection_timeout_s` to appropriately\n", + "high values for this environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl import SatelliteTasking\n", + "from bsk_rl.utils.rllib import unpack_config\n", + "from ray.rllib.algorithms.ppo import PPOConfig\n", + "\n", + "training_args = dict(\n", + " lr=0.00003,\n", + " gamma=0.999,\n", + " train_batch_size=5000,\n", + " num_sgd_iter=10,\n", + " model=dict(fcnet_hiddens=[512, 512], vf_share_layers=False),\n", + " lambda_=0.95,\n", + " use_kl_loss=False,\n", + " clip_param=0.1,\n", + " grad_clip=0.5,\n", + ")\n", + "\n", + "config = (\n", + " PPOConfig()\n", + " .training(**training_args)\n", + " .env_runners(num_env_runners=7, sample_timeout_s=1000.0)\n", + " .environment(\n", + " env=unpack_config(SatelliteTasking),\n", + " env_config=env_args,\n", + " )\n", + " .callbacks(CustomDataCallbacks)\n", + " .reporting(\n", + " metrics_num_episodes_for_smoothing=25,\n", + " metrics_episode_collection_timeout_s=180,\n", + " )\n", + " .checkpointing(export_native_model_files=True)\n", + " .framework(framework=\"tf2\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once the PPO configuration has been set, `ray` can be started and the agent can be\n", + "trained.\n", + "\n", + "```python\n", + "import ray\n", + "\n", + "ray.init(\n", + " ignore_reinit_error=True,\n", + " num_cpus=8,\n", + " object_store_memory=2_000_000_000, # 2 GB\n", + ")\n", + "\n", + "ppo = PPO(config)\n", + "\n", + "# Train the policy as you see fit\n", + "for _ in range(10):\n", + " ppo.train()\n", + " ppo.checkpoint()\n", + "\n", + "ray.shutdown()\n", + "```\n", + "\n", + "Training on a reasonably modern machine, we can achieve 5M steps over 32 processors in 6\n", + "to 18 hours, depending on specific environment configurations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading the Policy Network\n", + "\n", + "The policy network can be found in the `model` subdirectory of the checkpoint output." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stepping Through the Environment\n", + "\n", + "The environment is stepped through with random actions to give a sense of how it acts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env = SatelliteTasking(**env_args, log_level=\"INFO\")\n", + "env.reset()\n", + "terminated = False\n", + "while not terminated:\n", + " action = env.action_space.sample()\n", + " observation, reward, terminated, truncated, info = env.step(action)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/satellite_configuration.ipynb b/examples/satellite_configuration.ipynb new file mode 100644 index 00000000..84e53e62 --- /dev/null +++ b/examples/satellite_configuration.ipynb @@ -0,0 +1,497 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Satellite Configuration\n", + "\n", + "[Satellites](../api_reference/sats/index.rst) are the basic unit of agent in the \n", + "environment. Four things must be specified in subclasses of `Satellite`:\n", + "\n", + "* The `observation_spec`, which defines the satellite's [observation](../api_reference/obs/index.rst).\n", + "* The `action_spec`, which defines the satellite's [actions](../api_reference/act/index.rst).\n", + "* The `dyn_type`, which selects the underlying [dynamics model](../api_reference/sim/dyn.rst) used in simulation.\n", + "* The `fsw_type`, which selects the underlying [flight software model](../api_reference/sim/fsw.rst).\n", + "\n", + "A very simple satellite is defined below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl import sats, act, obs, scene, data, SatelliteTasking\n", + "from bsk_rl.sim import dyn, fsw\n", + "import numpy as np\n", + "\n", + "from Basilisk.architecture import bskLogging\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)\n", + "\n", + "\n", + "class SimpleSatellite(sats.Satellite):\n", + " observation_spec = [obs.Time()] # Passed as list of instantiated classes\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel # Passed as a type\n", + " fsw_type = fsw.BasicFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Satellite Parameters\n", + "\n", + "Without instantiating the satellite, parameters that can be set in the various models\n", + "can be inspected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SimpleSatellite.default_sat_args()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These parameters can be overriden when instantiating the satellite through the `sat_args`\n", + "argument. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sat = SimpleSatellite(\n", + " name=\"SimpleSat-1\",\n", + " sat_args=dict(\n", + " mass=300, # Setting a constant value\n", + " dragCoeff=lambda: np.random.uniform(2.0, 2.4), # Setting a randomized value\n", + " ),\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each time the simulation is reset, all of the function-based randomizers are called." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sat._generate_sat_args() # Called by the environment on reset()\n", + "sat.sat_args" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a result, each episode will have different randomized parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for _ in range(3):\n", + " sat._generate_sat_args() # Called by the environment on reset()\n", + " print(\"New value of dragCoeff:\", sat.sat_args[\"dragCoeff\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Observation Specification\n", + "\n", + "A variety of observation elements are available for satellites. Full documentation\n", + "can be [found here](../api_reference/obs/index.rst), but some commonly used elements\n", + "are explored below.\n", + "\n", + "
\n", + "\n", + "**Info:** In these examples, `obs_type=dict` is passed to the `Satellite` constructor\n", + "so that the observation is human readable. While some RL libraries support dictionary-based\n", + "observations, the default return type - the numpy array format - is more typically used.\n", + "\n", + "
\n", + "\n", + "\n", + "### Satellite Properties\n", + "\n", + "The most common type of observations is introspective; i.e. what is my current state?\n", + "Any `@property` in the `dyn_type` or `fsw_type` of the satellite can be accessed using\n", + "SatProperties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class SatPropsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " # At a minimum, specify the property to observe\n", + " dict(prop=\"wheel_speeds\"),\n", + " # You can specify the module to use for the observation, but it is not necessary\n", + " # if only one module has for the property\n", + " dict(prop=\"battery_charge_fraction\", module=\"dynamics\"), \n", + " # Properties can be normalized by some constant. This is generally desirable\n", + " # for RL algorithms to keep values around [-1, 1].\n", + " dict(prop=\"r_BN_P\", norm=7e6),\n", + " )\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=SatPropsSatellite(\"PropSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In some cases, you may want to access a bespoke property that is not natively implemented\n", + "in a model. To do that, simply extend the model with your desired property." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class BespokeFSWModel(fsw.BasicFSWModel):\n", + " @property\n", + " def meaning_of_life(self):\n", + " return 42\n", + " \n", + "class BespokeSatPropsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.SatProperties(dict(prop=\"meaning_of_life\"))\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = BespokeFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=BespokeSatPropsSatellite(\"BespokeSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Opportunity Properties\n", + "Another common input to the observation is information about upcoming locations that \n", + "are being accessed by the satellite. Currently, these include ground stations for\n", + "downlink and targets for imaging, but `OpportunityProperties` will work with any\n", + "location added by `add_location_for_access_checking`. In these examples, " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OppPropsSatellite(sats.ImagingSatellite):\n", + " observation_spec = [\n", + " obs.OpportunityProperties(\n", + " # Properties can be added by some default names\n", + " dict(prop=\"priority\"), \n", + " # They can also be normalized\n", + " dict(prop=\"opportunity_open\", norm=5700.0),\n", + " # Or they can be specified by an arbitrary function\n", + " dict(fn=lambda sat, opp: opp[\"r_LP_P\"] + 42),\n", + " n_ahead_observe=3,\n", + " )\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.ImagingDynModel\n", + " fsw_type = fsw.ImagingFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=OppPropsSatellite(\"OppSat-1\", {}, obs_type=dict),\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Navigating the Observation\n", + "\n", + "Usually, multiple observation types need to be composed to sufficiently represent the\n", + "environment for the learning agent. Simply add multiple observations to the observation\n", + "specification list to combine them in the observation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ComposedObsSatellite(sats.Satellite):\n", + " observation_spec = [\n", + " obs.Eclipse(),\n", + " obs.SatProperties(dict(prop=\"battery_charge_fraction\"))\n", + " ]\n", + " action_spec = [act.Drift()]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ComposedObsSatellite(\"PropSat-1\", {}, obs_type=dict),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "A few useful functions exist for inspecting the observation. The `observation_space`\n", + "property of the satellite and the environment return a Gym observation space to describe\n", + "the observation. In the single agent `SatelliteTasking` environment, these are the same.\n", + "\n", + "
\n", + "\n", + "**Info:** Here, we return to the `ndarray` default observation type.\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env = SatelliteTasking(\n", + " satellite=ComposedObsSatellite(\"PropSat-1\", {}),\n", + " log_level=\"CRITICAL\",\n", + ")\n", + "(env.observation_space, env.unwrapped.satellite.observation_space)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "With the flattened-vector type observation, it can be hard for the user to relate\n", + "elements to specific observations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, _ = env.reset()\n", + "observation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `observation_description` property can help the user understand what elements are \n", + "present in the observation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.unwrapped.satellite.observation_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## The Action Specification\n", + "\n", + "The [action specification](../api_reference/act/index.rst) works similarly to observation\n", + "specification. A list of actions is set in the class definition of the satellite." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ActionSatellite(sats.Satellite):\n", + " observation_spec = [obs.Time()]\n", + " action_spec = [\n", + " # If action duration is not set, the environment max_step_duration will be used;\n", + " # however, being explicit is always preferable\n", + " act.Charge(duration=120.0),\n", + " act.Desat(duration=60.0),\n", + " # One action can be included multiple time, if different settings are desired\n", + " act.Charge(duration=600.0,),\n", + " ]\n", + " dyn_type = dyn.BasicDynamicsModel\n", + " fsw_type = fsw.BasicFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ActionSatellite(\"ActSat-1\", {}, obs_type=dict),\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "# Try each action; index corresponds to the order of addition\n", + "_ =env.step(0)\n", + "_ =env.step(1)\n", + "_ =env.step(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with the observations, properties exist to help understand the actions available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.action_space" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.unwrapped.satellite.action_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some actions take additional configurations, add multiple actions to the satellite, and/or\n", + "have \"special\" features that are useful for manually interacting with the environment. \n", + "For example, the imaging action can add an arbitrary number of actions corresponding to\n", + "upcoming targets and process the name of a target directly instead of operating by\n", + "action index." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ImageActSatellite(sats.ImagingSatellite):\n", + " observation_spec = [obs.Time()]\n", + " action_spec = [\n", + " # Set the number of upcoming targets to consider\n", + " act.Image(n_ahead_image=3)\n", + " ]\n", + " dyn_type = dyn.ImagingDynModel\n", + " fsw_type = fsw.ImagingFSWModel\n", + "\n", + "env = SatelliteTasking(\n", + " satellite=ImageActSatellite(\"ActSat-2\", {}),\n", + " scenario=scene.UniformTargets(1000),\n", + " rewarder=data.UniqueImageReward(),\n", + " log_level=\"INFO\",\n", + ")\n", + "env.reset()\n", + "\n", + "env.unwrapped.satellite.action_description" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demonstrating the action overload feature, we task the satellite based on target name.\n", + "While this is not part of the official Gym API, we find it useful in certain cases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "target = env.unwrapped.satellite.find_next_opportunities(n=10)[9][\"target\"]\n", + "_ = env.step(target)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/simple_environment.ipynb b/examples/simple_environment.ipynb new file mode 100644 index 00000000..c192ff9a --- /dev/null +++ b/examples/simple_environment.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started\n", + "This tutorial demonstrates the configuration and use of a simple BSK-RL environment.\n", + "BSK-RL and dependencies should already be installed at this point (see [Installation](../install.rst)\n", + "if you haven't installed the package yet).\n", + "\n", + "## Load Modules\n", + "In this tutorial, the environment will be created with `gym.make`, so it is necessary to\n", + "import the top-level `bsk_rl` module as well as `gym` and `bsk_rl` components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import gymnasium as gym\n", + "import numpy as np\n", + "from bsk_rl import act, data, obs, scene, sats\n", + "from bsk_rl.sim import dyn, fsw\n", + "\n", + "from Basilisk.architecture import bskLogging\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If no errors were raised, you have a functional installation of `bsk_rl`.\n", + "\n", + "## Configure the Satellite\n", + "[Satellites](../api_reference/sats/index.rst) are configurable agents in the environment.\n", + "To make a new environment, start by specifying the [observations](../api_reference/obs/index.rst)\n", + "and [actions](../api_reference/act/index.rst) of a satellite type, as well as the underlying\n", + "Basilisk [simulation](../api_reference/sim/index.rst) models used by the satellite." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class MyScanningSatellite(sats.AccessSatellite):\n", + " observation_spec = [\n", + " obs.SatProperties(\n", + " dict(prop=\"storage_level_fraction\"),\n", + " dict(prop=\"battery_charge_fraction\")\n", + " ),\n", + " obs.Eclipse(),\n", + " ]\n", + " action_spec = [\n", + " act.Scan(duration=60.0), # Scan for 1 minute\n", + " act.Charge(duration=600.0), # Charge for 10 minutes\n", + " ]\n", + " dyn_type = dyn.ContinuousImagingDynModel\n", + " fsw_type = fsw.ContinuousImagingFSWModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on this class specification, a list of configurable parameters for the satellite\n", + "can be generated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "MyScanningSatellite.default_sat_args()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When instantiating a satellite, these parameters can be overriden with a constant or \n", + "rerandomized every time the environment is reset using the ``sat_args`` dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sat_args = {}\n", + "\n", + "# Set some parameters as constants\n", + "sat_args[\"imageAttErrorRequirement\"] = 0.05\n", + "sat_args[\"dataStorageCapacity\"] = 1e10\n", + "sat_args[\"instrumentBaudRate\"] = 1e7\n", + "sat_args[\"storedCharge_Init\"] = 50000.0\n", + "\n", + "# Randomize the initial storage level on every reset\n", + "sat_args[\"storageInit\"] = lambda: np.random.uniform(0.25, 0.75) * 1e10\n", + "\n", + "# Make the satellite\n", + "sat = MyScanningSatellite(name=\"EO1\", sat_args=sat_args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making the Environment\n", + "For this example, we will be using the single-agent [SatelliteTasking](../api_reference/index.rst) \n", + "environment. Along with passing the satellite that we configured, the environment takes\n", + "a [scenario](../api_reference/scene/index.rst), which defines the environment the\n", + "satellite is acting in, and a [rewarder](../api_reference/data/index.rst), which defines\n", + "how data collected from the scenario is rewarded." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env = gym.make(\n", + " \"SatelliteTasking-v1\",\n", + " satellite=sat,\n", + " scenario=scene.UniformNadirScanning(),\n", + " rewarder=data.ScanningTimeReward(),\n", + " time_limit=5700.0, # approximately 1 orbit\n", + " log_level=\"INFO\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interacting with the Environment\n", + "\n", + "First, the environment is reset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, info = env.reset(seed=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we take the scan action (`action=0`) a few times. This allows for the satellite to\n", + "settle its attitude in the nadir pointing mode to satisfy imaging conditions. Note that \n", + "the logs show little or no data accumulated in the first two steps as it settles, but\n", + "achieves 60 reward (corresponding to 60 seconds of imaging) by the third step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Initial data level:\", observation[0], \"(randomized by sat_args)\")\n", + "for _ in range(3):\n", + " observation, reward, terminated, truncated, info = env.step(action=0)\n", + "print(\" Final data level:\", observation[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The observation reflects the increase in stored data. The first element, corresponding\n", + "to `storage_level_fraction`, starts at a random value set by the `storageInit` function\n", + "in `sat_args` and increases based on the time spent imaging.\n", + "\n", + "Finally, the charging mode is tasked repeatedly in 10-minute increments until the\n", + "environment time limit is reached." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "while not truncated:\n", + " observation, reward, terminated, truncated, info = env.step(action=1)\n", + " print(f\"Charge level: {observation[1]:.3f} ({env.unwrapped.simulator.sim_time:.1f} seconds)\\n\\tEclipse: start: {observation[2]:.1f} end: {observation[3]:.1f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is observed that the battery decrease while the satellite is in eclipse, but once the\n", + "satellite is out of eclipse, the battery quickly increases to full charge." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_refactor", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/tutorials/multisat_aeos.py b/examples/tutorials/multisat_aeos.py deleted file mode 100644 index 50467871..00000000 --- a/examples/tutorials/multisat_aeos.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Multisat AEOS -============= - -some text here -""" - -import gymnasium as gym -import numpy as np - -from bsk_rl.env.scenario import communication, data -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import CityTargets -from bsk_rl.utils.orbital import walker_delta - - -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 = 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), - ) - - # As an example, look at the arguments for one of the satellites - if i == 0: - print(sat_args) - - # 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, - # 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", - ) - - # 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 __name__ == "__main__": - run() diff --git a/examples/tutorials/rllib_train.py b/examples/tutorials/rllib_train.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/tutorials/satellite_customization.py b/examples/tutorials/satellite_customization.py deleted file mode 100644 index 26e326aa..00000000 --- a/examples/tutorials/satellite_customization.py +++ /dev/null @@ -1,212 +0,0 @@ -import gymnasium as gym -from Basilisk.architecture import bskLogging -from Basilisk.utilities import orbitalMotion - -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import CityTargets -from bsk_rl.env.simulation import dynamics, environment, fsw -from bsk_rl.utils.orbital import random_orbit - -bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) - -# This script demonstrates customization options for a satellite, including: -# - Model selection -# - Observation space configuration -# - Action space configuration - -# There are two primary methods of customization. The preferred option (1) is to use the -# built-in observation space and actions space satellite subclasses. Alternatively, -# 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_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() - - while True: - print(f"") - - observation, reward, terminated, truncated, info = env.step( - env.action_space.sample() # Task random actions - ) - - # 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}") - - total_reward += reward - print(f"\tReward: {reward:.3f} ({total_reward:.3f} cumulative)") - if terminated or truncated: - print("Episode complete.") - break diff --git a/examples/tutorials/shield.py b/examples/tutorials/shield.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/tutorials/single_sat.py b/examples/tutorials/single_sat.py deleted file mode 100644 index aa5f773b..00000000 --- a/examples/tutorials/single_sat.py +++ /dev/null @@ -1,91 +0,0 @@ -import gymnasium as gym - -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.utils.orbital import random_orbit - -# This script demonstrates the configuration of an environment with a single imaging -# satellite. - -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. - - 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) - - # 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. This can be automatically inferred from the satellite types - # 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={}, - # 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() - - 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 - - """ - 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 diff --git a/pyproject.toml b/pyproject.toml index 9e7a0917..3847de2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,7 @@ readme = "README.md" requires-python = ">=3.9.0" license = { text = "MIT" } dependencies = [ - "deap==1.3.3", - "Deprecated", "gymnasium", - "matplotlib", "numpy", "pandas", "pettingzoo", @@ -26,13 +23,12 @@ dependencies = [ "pytest-repeat", "requests", "ruff>=0.1.9", - "scikit-learn", "scipy", - "sphinx-rtd-theme", - "stable-baselines3", - "tensorflow", - "torch", ] +[project.optional-dependencies] +docs = ["ipykernel", "ipywidgets", "nbdime", "nbsphinx", "sphinx-rtd-theme"] +rllib = ["dm_tree", "pyarrow", "ray[rllib]", "scikit-image", "typer"] + [project.scripts] -finish_install = "bsk_rl._finish_install:pck_install" +finish_install = "bsk_rl.finish_install:pck_install" diff --git a/src/.ruff.toml b/src/.ruff.toml index 54df2d55..0d19985e 100644 --- a/src/.ruff.toml +++ b/src/.ruff.toml @@ -1,8 +1,7 @@ -extend-safe-fixes = ["D"] - [lint.pydocstyle] convention = "google" [lint] select = ["D", "W", "D404"] ignore = ["D101", "D104"] +extend-safe-fixes = ["D"] diff --git a/src/bsk_rl/__init__.py b/src/bsk_rl/__init__.py index 35ad2eec..b5ce49e8 100644 --- a/src/bsk_rl/__init__.py +++ b/src/bsk_rl/__init__.py @@ -1,26 +1,28 @@ import gymnasium as gym from gymnasium.envs.registration import register -from bsk_rl._check_bsk_version import _check_bsk_version -from bsk_rl.env.gym_env import ( - GeneralSatelliteTasking, - MultiagentSatelliteTasking, - SingleSatelliteTasking, -) +from bsk_rl.check_bsk_version import check_bsk_version +from bsk_rl.gym import ConstellationTasking, GeneralSatelliteTasking, SatelliteTasking + +__all__ = [ + "GeneralSatelliteTasking", + "SatelliteTasking", + "ConstellationTasking", +] register( id="GeneralSatelliteTasking-v1", - entry_point="bsk_rl.env.gym_env:GeneralSatelliteTasking", + entry_point="bsk_rl.gym:GeneralSatelliteTasking", ) register( - id="SingleSatelliteTasking-v1", - entry_point="bsk_rl.env.gym_env:SingleSatelliteTasking", + id="SatelliteTasking-v1", + entry_point="bsk_rl.gym:SatelliteTasking", ) register( - id="MultiagentSatelliteTasking-v1", - entry_point="bsk_rl.env.gym_env:MultiagentSatelliteTasking", + id="ConstellationTasking-v1", + entry_point="bsk_rl.gym:ConstellationTasking", ) -_check_bsk_version() +check_bsk_version() diff --git a/src/bsk_rl/_default.rst b/src/bsk_rl/_default.rst new file mode 100644 index 00000000..9d1bc0db --- /dev/null +++ b/src/bsk_rl/_default.rst @@ -0,0 +1,51 @@ +API Reference +============= + +``bsk_rl`` is a framework for creating satellite tasking reinforcement learning environments. +Three base environment classes are provided for configuring new environments: + ++-------------------------------------+------------+---------------+--------------------------------------------------------------------+ +| **Environment** | **API** |**Agent Count**| **Purpose** | ++-------------------------------------+------------+---------------+--------------------------------------------------------------------+ +| :class:`SatelliteTasking` | Gymnasium | 1 | Single-agent training; compatible with most RL libraries. | ++-------------------------------------+------------+---------------+--------------------------------------------------------------------+ +| :class:`GeneralSatelliteTasking` | Gymnasium | ≥1 | Multi-agent testing; actions and observations are given in tuples. | ++-------------------------------------+------------+---------------+--------------------------------------------------------------------+ +| :class:`ConstellationTasking` | PettingZoo | ≥1 | Multi-agent training; compatible with multiagent RL libraries. | ++-------------------------------------+------------+---------------+--------------------------------------------------------------------+ + +Environments are customized by passing keyword arguments to the environment constructor. +When using ``gym.make``, the syntax looks like this: + +.. code-block:: python + + env = gym.make( + "SatelliteTasking-v1", + satellite=Satellite(...), + scenario=UniformTargets(...), + ... + ) + +In some cases (e.g. the multiprocessed Gymnasium vector environment), it is necessary +for compatibility to instead register a new environment using the GeneralSatelliteTasking +class and a kwargs dict. + +See the :ref:`examples` for more information on environment configuration arguments. + +.. automodule:: bsk_rl + :members: + :show-inheritance: + +.. toctree:: + :maxdepth: 1 + :caption: Modules + :hidden: + + sats/index + obs/index + act/index + scene/index + data/index + comm/index + sim/index + utils/index diff --git a/src/bsk_rl/act/__init__.py b/src/bsk_rl/act/__init__.py new file mode 100644 index 00000000..6e49bec7 --- /dev/null +++ b/src/bsk_rl/act/__init__.py @@ -0,0 +1,66 @@ +"""Actions ``bsk_rl.act`` can be used to add actions to an agent. + +To configure the observation, set the ``action_spec`` attribute of a :class:`~bsk_rl.env.scenario.satellites.Satellite` +subclass. For example: + +.. code-block:: python + + class MyActionSatellite(Satellite): + action_spec = [ + Charge(duration=60.0), + Desat(duration=30.0), + Downlink(duration=60.0), + Image(n_ahead_image=10), + ] + +Actions in an ``action_spec`` should all be of the same subclass of :class:`Action`. The +following actions are currently available: + +Discrete Actions +---------------- + +Use :class:`DiscreteAction` for integer-indexable, discrete actions. + ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| **Action** |**Count**| **Description** | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`DiscreteFSWAction` | 1 | Call an arbitrary ``@action`` decorated function in the :class:`~bsk_rl.env.simulation.fsw.FSWModel`. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Charge` | 1 | Point the solar panels at the sun. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Drift` | 1 | Do nothing. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Desat` | 1 | Desaturate the reaction wheels with RCS thrusters. Needs to be called multiple times. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Downlink` | 1 | Downlink data to any ground station that is in range. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Image` | ≥1 | Image one of the next ``N`` upcoming, unimaged targets once in range. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +| :class:`Scan` | 1 | Scan nadir, collecting data when pointing within a threshold. | ++----------------------------+---------+-------------------------------------------------------------------------------------------------------+ +""" + +from bsk_rl.act.actions import Action +from bsk_rl.act.discrete_actions import ( + Charge, + Desat, + DiscreteAction, + DiscreteFSWAction, + Downlink, + Drift, + Image, + Scan, +) + +__doc_title__ = "Actions" +__all__ = [ + "Action", + "DiscreteAction", + "DiscreteFSWAction", + "Charge", + "Drift", + "Desat", + "Downlink", + "Image", + "Scan", +] diff --git a/src/bsk_rl/act/actions.py b/src/bsk_rl/act/actions.py new file mode 100644 index 00000000..da457b77 --- /dev/null +++ b/src/bsk_rl/act/actions.py @@ -0,0 +1,115 @@ +"""Satellite action types can be used to add actions to an agent.""" + +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import TYPE_CHECKING, Any + +from bsk_rl.utils.functional import AbstractClassProperty + +if TYPE_CHECKING: # pragma: no cover + from gymnasium import spaces + + from bsk_rl.sats import Satellite + from bsk_rl.sim import Simulator + + +def select_action_builder(satellite: "Satellite") -> "ActionBuilder": + """Identify the proper action builder based on a satellite's action spec. + + Args: + satellite: Satellite to build actions for. + + Returns: + action builder of the appropriate type + """ + builder_types = [spec.builder_type for spec in satellite.action_spec] + if all([builder_type == builder_types[0] for builder_type in builder_types]): + return builder_types[0](satellite) + else: + raise NotImplementedError("Heterogenous action builders not supported.") + + +class ActionBuilder(ABC): + + def __init__(self, satellite: "Satellite") -> None: + """Base class for all action builders. + + Args: + satellite: Satellite to build actions for. + """ + self.satellite = satellite + self.simulator: "Simulator" + self.action_spec = deepcopy(self.satellite.action_spec) + for act in self.action_spec: + act.link_satellite(self.satellite) + + def reset_post_sim(self) -> None: + """Perform any once-per-episode setup.""" + self.simulator = self.satellite.simulator # already a proxy + for act in self.action_spec: + act.link_simulator(self.simulator) # already a proxy + act.reset_post_sim() + + @property + @abstractmethod + def action_space(self) -> "spaces.Space": + """Return the action space.""" + pass + + @property + @abstractmethod + def action_description(self) -> Any: + """Return a description of the action space.""" + pass + + @abstractmethod + def set_action(self, action: Any) -> None: + """Set the action to be taken.""" + pass + + +class Action(ABC): + builder_type: type[ActionBuilder] = AbstractClassProperty() #: :meta private: + + def __init__(self, name: str = "act") -> None: + """Base class for all actions. + + Args: + name: Name of the action. + """ + self.name = name + self.satellite: "Satellite" + self.simulator: "Simulator" + + def link_satellite(self, satellite: "Satellite") -> None: + """Link the action to a satellite. + + Args: + satellite: Satellite to link to + + :meta private: + """ + self.satellite = satellite # already a proxy + + def link_simulator(self, simulator: "Simulator") -> None: + """Link the action to a simulator. + + Args: + simulator: Simulator to link to + + :meta private: + """ + self.simulator = simulator # already a proxy + + def reset_post_sim(self) -> None: # pragma: no cover + """Perform any once-per-episode setup.""" + pass + + @abstractmethod + def set_action(self, action: Any) -> None: # pragma: no cover + """Execute code to perform an action.""" + pass + + +__doc_title__ = "Backend" +__all__ = ["ActionBuilder"] diff --git a/src/bsk_rl/env/scenario/actions.py b/src/bsk_rl/act/discrete_actions.py similarity index 57% rename from src/bsk_rl/env/scenario/actions.py rename to src/bsk_rl/act/discrete_actions.py index 9cb84701..e88ed670 100644 --- a/src/bsk_rl/env/scenario/actions.py +++ b/src/bsk_rl/act/discrete_actions.py @@ -1,128 +1,45 @@ -"""Satellite action types can be used to add actions to an agent. - -To configure the observation, set the ``action_spec`` attribute of a -:class:`~bsk_rl.env.scenario.satellites.Satellite` subclass. For example: - -.. code-block:: python - - class MyActionSatellite(Satellite): - action_spec = [ - Charge(duration=60.0), - Desat(duration=30.0), - Downlink(duration=60.0), - Image(n_ahead_image=10), - ] - -Actions in an ``action_spec`` should all be of the same subclass of :class:`Action`. The -following actions are currently available: - -Discrete Actions: :class:`DiscreteAction` ------------------------------------------ -For integer-indexable, discrete actions. - -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| **Action** |**Count**| **Description** | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`DiscreteFSWAction` | 1 | Call an arbitrary ``@action`` decorated function in the :class:`~bsk_rl.env.simulation.fsw.FSWModel`. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`Charge` | 1 | Point the solar panels at the sun. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`Drift` | 1 | Do nothing. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`Desat` | 1 | Desaturate the reaction wheels with RCS thrusters. Needs to be called multiple times. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`Downlink` | 1 | Downlink data to any ground station that is in range. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ -| :class:`Image` | ≥1 | Image one of the next ``N`` upcoming, unimaged targets once in range. | -+----------------------------+---------+-------------------------------------------------------------------------------------------------------+ - -""" +"""Discrete actions are indexable by integer.""" import logging -from abc import ABC, abstractmethod -from copy import deepcopy +from abc import abstractmethod from typing import TYPE_CHECKING, Any, Optional, Union -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import Satellite, Simulator - import numpy as np from gymnasium import spaces -from bsk_rl.env.scenario.environment_features import Target -from bsk_rl.utils.functional import AbstractClassProperty, bind, configurable - - -def select_action_builder(satellite: "Satellite") -> "ActionBuilder": - """Identify the proper action builder based on a satellite's action spec. - - Args: - satellite: Satellite to build actions for. - - Returns: - action builder of the appropriate type - - :meta private: - """ - builder_types = [spec.builder_type for spec in satellite.action_spec] - if all([builder_type == builder_types[0] for builder_type in builder_types]): - return builder_types[0](satellite) - else: - raise NotImplementedError("Heterogenous action builders not supported.") - - -class ActionBuilder(ABC): - """:meta private:""" - - def __init__(self, satellite: "Satellite") -> None: - self.satellite = satellite - self.simulator: "Simulator" - self.action_spec = deepcopy(self.satellite.action_spec) - for act in self.action_spec: - act.link_satellite(self.satellite) - - def reset_post_sim(self) -> None: - """Perform any once-per-episode setup.""" - self.simulator = self.satellite.simulator # already a proxy - for act in self.action_spec: - act.link_simulator(self.simulator) # already a proxy - act.reset_post_sim() - - @property - @abstractmethod - def action_space(self) -> spaces.Space: - """Return the action space.""" - pass +from bsk_rl.act.actions import Action, ActionBuilder - @property - @abstractmethod - def action_description(self) -> Any: - """Return a description of the action space.""" - pass +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.scene.targets import Target - @abstractmethod - def set_action(self, action: Any) -> None: - """Set the action to be taken.""" - pass +logger = logging.getLogger(__name__) class DiscreteActionBuilder(ActionBuilder): - """:meta private:""" def __init__(self, satellite: "Satellite") -> None: + """Processes actions for a discrete action space. + + Args: + satellite: Satellite to create actions for. + """ super().__init__(satellite) self.prev_action_key = None def reset_post_sim(self) -> None: + """Log previous action key.""" super().reset_post_sim() self.prev_action_key = None @property def action_space(self) -> spaces.Discrete: + """Discrete action space.""" return spaces.Discrete(sum([act.n_actions for act in self.action_spec])) @property def action_description(self) -> list[str]: + """Return a list of strings corresponding to action names.""" actions = [] for act in self.action_spec: if act.n_actions == 1: @@ -132,9 +49,14 @@ def action_description(self) -> list[str]: return actions def set_action(self, action: int) -> None: - self.satellite._disable_timed_terminal_event() + """Sets the action based on the integer index. + + If the action is not an integer, the satellite will attempt to call ``set_action_override`` + for each action, in order, until one works. + """ + self.satellite.disable_timed_terminal_event() if not np.issubdtype(type(action), np.integer): - logging.warning( + logger.warning( f"Action '{action}' is not an integer. Will attempt to use compatible set_action_override method." ) for act in self.action_spec: @@ -163,49 +85,6 @@ def set_action(self, action: int) -> None: raise ValueError(f"Action index {action} out of range.") -class Action(ABC): - builder_type: type[ActionBuilder] = AbstractClassProperty() #: :meta private: - - def __init__(self, name: str = "act") -> None: - """Base class for all actions. - - Args: - name: Name of the action. - """ - self.name = name - self.satellite: "Satellite" - self.simulator: "Simulator" - - def link_satellite(self, satellite: "Satellite") -> None: - """Link the action to a satellite. - - Args: - satellite: Satellite to link to - - :meta private: - """ - self.satellite = satellite # already a proxy - - def link_simulator(self, simulator: "Simulator") -> None: - """Link the action to a simulator. - - Args: - simulator: Simulator to link to - - :meta private: - """ - self.simulator = simulator # already a proxy - - def reset_post_sim(self) -> None: # pragma: no cover - """Perform any once-per-episode setup.""" - pass - - @abstractmethod - def set_action(self, action: Any) -> None: # pragma: no cover - """Execute code to perform an action.""" - pass - - class DiscreteAction(Action): builder_type = DiscreteActionBuilder @@ -277,7 +156,7 @@ def set_action(self, action: int, prev_action_key=None) -> str: """ assert action == 0 self.satellite.log_info(f"{self.name} tasked for {self.duration} seconds") - self.satellite._update_timed_terminal_event( + self.satellite.update_timed_terminal_event( self.simulator.sim_time + self.duration, info=f"for {self.fsw_action}" ) @@ -331,7 +210,7 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) """Action to transmit data from the data buffer (:class:`~bsk_rl.env.simulation.fsw.ImagingFSWModel.action_downlink`). If not in range of a ground station (defined in - :class:`~bsk_rl.env.simulation.environment.GroundStationEnvModel`), no data will + :class:`~bsk_rl.env.world.GroundStationWorldModel`), no data will be downlinked. Args: @@ -343,7 +222,7 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) class Scan(DiscreteFSWAction): def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): - """Action to collect data from a :class:`~bsk_rl.env.scenario.environment_features.UniformNadirFeature` (:class:`~bsk_rl.env.simulation.fsw.ContinuousImagingFSWModel.action_nadir_scan`). + """Action to collect data from a :class:`~bsk_rl.scene.UniformNadirScanning` (:class:`~bsk_rl.sim.fsw.ContinuousImagingFSWModel.action_nadir_scan`). Args: name: Action name. @@ -360,28 +239,35 @@ def __init__( ): """Actions to image upcoming target (:class:`~bsk_rl.env.simulation.fsw.ImagingFSWModel.action_image`). - Adds `n_ahead_image` actions to the action space, corresponding to the next - `n_ahead_image` unimaged targets. The action may be unsuccessful if the target + Adds ``n_ahead_image`` actions to the action space, corresponding to the next + ``n_ahead_image`` unimaged targets. The action may be unsuccessful if the target exits the satellite's field of regard before the satellite settles on the target and takes an image. The action with stop as soon as the image is successfully taken, or when the the target exits the field of regard. - This action implements a `set_action_override` that allows a target to be tasked + This action implements a ``set_action_override`` that allows a target to be tasked based on the target's ID string or the Target object. Args: name: Action name. n_ahead_image: Number of unimaged, along-track targets to consider. """ - from bsk_rl.env.scenario.satellites import ImagingSatellite + from bsk_rl.sats import ImagingSatellite self.satellite: "ImagingSatellite" super().__init__(name=name, n_actions=n_ahead_image) def image( - self, target: Union[int, Target, str], prev_action_key: Optional[str] = None + self, target: Union[int, "Target", str], prev_action_key: Optional[str] = None ) -> str: - """:meta private:""" + """Task or retask a satellite for imaging a target. + + Args: + target: Target to image. + prev_action_key: Previous action key. + + :meta private: + """ target = self.satellite.parse_target_selection(target) if target.id != prev_action_key: self.satellite.task_target_for_imaging(target) @@ -403,14 +289,18 @@ def set_action(self, action: int, prev_action_key: Optional[str] = None) -> str: return self.image(action, prev_action_key) def set_action_override( - self, action: Union[Target, str], prev_action_key: Optional[str] = None + self, action: Union["Target", str], prev_action_key: Optional[str] = None ) -> str: """Image a target by target index, Target, or ID. Args: - target: Target to image. + action: Target to image in the form of a Target object, target ID, or target index. prev_action_key: Previous action key. :meta_private: """ return self.image(action, prev_action_key) + + +__doc_title__ = "Discrete Backend" +__all__ = ["DiscreteActionBuilder"] diff --git a/src/bsk_rl/_check_bsk_version.py b/src/bsk_rl/check_bsk_version.py similarity index 89% rename from src/bsk_rl/_check_bsk_version.py rename to src/bsk_rl/check_bsk_version.py index c6ddc419..35124897 100644 --- a/src/bsk_rl/_check_bsk_version.py +++ b/src/bsk_rl/check_bsk_version.py @@ -1,3 +1,5 @@ +"""Version checking for Basilisk.""" + import os from importlib.metadata import PackageNotFoundError, version from warnings import warn @@ -5,7 +7,8 @@ from packaging.version import parse as parse_version -def _check_bsk_version(): +def check_bsk_version(): + """Check Basilisk version against requirement.""" # Don't run check if Basilisk is mocked try: if os.environ["PYTHON_MOCK_BASILISK"] == "1": diff --git a/src/bsk_rl/comm/__init__.py b/src/bsk_rl/comm/__init__.py new file mode 100644 index 00000000..268f5e8d --- /dev/null +++ b/src/bsk_rl/comm/__init__.py @@ -0,0 +1,66 @@ +"""``bsk_rl.comm`` provides methods of communication for multisatellite environments. + +Communication methods are used to induce collaborative behavior between satellites. +While the :class:`~bsk_rl.data.GlobalReward` acts as a global critic for the environment, +individual satellites may not have complete environmental knowledge; for example, in a +target imaging scenario, individual satellites do not know what requests have already +been fulfilled by other satellites. With communication, satellites can share data to +improve decision-making. + +Communication works by sharing data between satellites, updating each other's local +knowledge of the scenario. After each environment step, +:class:`CommunicationMethod.communication_pairs` is evaluated to determine which pairs +of satellites should share data. Then, each local :class:`~bsk_rl.data.DataStore` is +updated with the other satellite's data. + +Configuration +------------- + +Communication methods can be configured by passing an instance of :class:`CommunicationMethod` +to the ``communicator`` field of the environment constructor. + +.. code-block:: python + + env = ConstellationTasking( + ..., + communicator=LOSMultiCommunication(), + ... + ) + + +Types of Communication +---------------------- + +* :class:`NoCommunication`: No communication between satellites. +* :class:`FreeCommunication`: Free communication between all satellites. This method is + cheap to evaluate, and in scenarios with many satellites or tightly clustered + satellites, it is often functionally equivalent to more complex models. +* :class:`LOSCommunication`: Line-of-sight communication between satellites. This method + evaluates whether a direct line of sight exists between two satellites. If so, they + can communicate. +* :class:`MultiDegreeCommunication`: This allows for "paths" of communication between + satellites linked by some other method. +* :class:`LOSMultiCommunication`: A combination of :class:`LOSCommunication` and + :class:`MultiDegreeCommunication` communication. This method allows for instantaneous + communication by satellites that are obscured from each other but have a path of + connected satellites acting as relays between them. +""" + +from bsk_rl.comm.communication import ( + CommunicationMethod, + FreeCommunication, + LOSCommunication, + LOSMultiCommunication, + MultiDegreeCommunication, + NoCommunication, +) + +__doc_title__ = "Communication" +__all__ = [ + "CommunicationMethod", + "NoCommunication", + "FreeCommunication", + "LOSCommunication", + "MultiDegreeCommunication", + "LOSMultiCommunication", +] diff --git a/src/bsk_rl/env/scenario/communication.py b/src/bsk_rl/comm/communication.py similarity index 53% rename from src/bsk_rl/env/scenario/communication.py rename to src/bsk_rl/comm/communication.py index 16c81609..07764a4a 100644 --- a/src/bsk_rl/env/scenario/communication.py +++ b/src/bsk_rl/comm/communication.py @@ -5,13 +5,13 @@ from itertools import combinations from typing import TYPE_CHECKING, Optional -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import Satellite - import numpy as np from scipy.sparse.csgraph import connected_components -from bsk_rl.env.simulation.dynamics import LOSCommDynModel +from bsk_rl.sim.dyn import LOSCommDynModel + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite logger = logging.getLogger(__name__) @@ -19,58 +19,90 @@ class CommunicationMethod(ABC): """Base class for defining data sharing between satellites.""" - def __init__(self, satellites: Optional[list["Satellite"]] = None) -> None: - """Construct base communication class. + def __init__(self) -> None: + """The base communication class. + + Subclasses implement a way of determining which pairs of satellites share data + at each environment step. + """ + self.satellites: list["Satellite"] + + def link_satellites(self, satellites: list["Satellite"]) -> None: + """Link the environment satellite list to the communication method. - Subclasses implement a way of determining which pairs of satellites share data. + Args: + satellites: List of satellites to communicate between. """ self.satellites = satellites - def reset(self) -> None: + def reset_post_sim(self) -> None: """Reset communication after simulator initialization.""" pass @abstractmethod # pragma: no cover - def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: - """List pair of satellite that should share data.""" + def communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + """List pairs of satellite that should share data. + + To define a new communication type, this method must be implemented. + """ pass def communicate(self) -> None: """Share data between paired satellites.""" - for sat_1, sat_2 in self._communication_pairs(): + for sat_1, sat_2 in self.communication_pairs(): sat_1.data_store.stage_communicated_data(sat_2.data_store.data) sat_2.data_store.stage_communicated_data(sat_1.data_store.data) for satellite in self.satellites: - satellite.data_store.communication_update() + satellite.data_store.update_with_communicated_data() class NoCommunication(CommunicationMethod): - """Implements no communication between satellite.""" + """Implements no communication between satellites.""" + + def __init__(self): + """Implements no communication between satellites. - def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + This is the default communication method if no other method is specified. Satellites + will maintain their own :class:`~bsk_rl.data.DataStore` and not share data with others. + """ + super().__init__() + + def communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + """Return no communication pairs.""" return [] class FreeCommunication(CommunicationMethod): - """Implements communication between satellites at every step.""" + """Implements free communication between every satellite at every step.""" - def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + def communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + """Return all possible communication pairs.""" return list(combinations(self.satellites, 2)) class LOSCommunication(CommunicationMethod): - """Implements communication between satellites with a direct line-of-sight. + """Implements communication between satellites with a direct line-of-sight.""" # TODO only communicate data from before latest LOS time - """ - def __init__(self, satellites: list["Satellite"]) -> None: - """Construct line-of-sigh communication management. + def __init__(self) -> None: + """Implements communication between satellites with a direct line-of-sight. + + At the end of each step, satellites will communicate with each other if they have a + line-of-sight between them that is not occluded by the Earth. + + Satellites must have a dynamics model that is a subclass of + :class:`~bsk_rl.sim.dyn.LOSCommDynModel`. to use this communication method. + """ + super().__init__() + + def link_satellites(self, satellites: list["Satellite"]) -> None: + """Link the environment satellite list to the communication method. Args: satellites: List of satellites to communicate between. """ - super().__init__(satellites) + super().link_satellites(satellites) for satellite in self.satellites: if not issubclass(satellite.dyn_type, LOSCommDynModel): raise TypeError( @@ -78,9 +110,9 @@ def __init__(self, satellites: list["Satellite"]) -> None: + "of LOSCommDynModel to use LOSCommunication" ) - def reset(self) -> None: + def reset_post_sim(self) -> None: """Add loggers to satellites to track line-of-sight communication.""" - super().reset() + super().reset_post_sim() self.los_logs = {} for sat_1 in self.satellites: @@ -99,7 +131,8 @@ def reset(self) -> None: sat_1.dynamics.task_name, logger, ModelPriority=586 ) - def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + def communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + """Return pairs of satellites that have line-of-sight visibility.""" pairs = [] for sat_1, logs in self.los_logs.items(): for sat_2, logger in logs.items(): @@ -116,15 +149,21 @@ def communicate(self) -> None: class MultiDegreeCommunication(CommunicationMethod): - """Compose with another type to use multi-degree communications. + """Compose with another type to use multi-degree communications.""" - For example, if a <-> b and b <-> c, multidegree communication will also communicate - between a <-> c. - """ + def __init__(self) -> None: + """Compose with another communication type to propagate multi-degree communication. + + If a communication method allows satellites A and B to communicate and satellites + B and C to communicate, MultiDegreeCommunication will allow satellites A and C to + communicate on the same step as well. + """ + super().__init__() - def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + def communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: + """Return pairs of satellites that are connected by a path of communication through other satellites.""" graph = np.zeros((len(self.satellites), len(self.satellites)), dtype=bool) - for sat_1, sat_2 in super()._communication_pairs(): + for sat_1, sat_2 in super().communication_pairs(): graph[self.satellites.index(sat_1), self.satellites.index(sat_2)] = True pairs = [] @@ -136,9 +175,12 @@ def _communication_pairs(self) -> list[tuple["Satellite", "Satellite"]]: class LOSMultiCommunication(MultiDegreeCommunication, LOSCommunication): - """Multidegree line of sight communication. + """Multidegree line-of-sight communication. - Composes MultiDegreeCommunication with LOSCommunication. + Composes :class:`MultiDegreeCommunication` with :class:`LOSCommunication`. """ pass + + +__all__ = [] diff --git a/src/bsk_rl/data/__init__.py b/src/bsk_rl/data/__init__.py new file mode 100644 index 00000000..c83bac01 --- /dev/null +++ b/src/bsk_rl/data/__init__.py @@ -0,0 +1,90 @@ +"""Data collection and reward calculation is given in ``bsk_rl.data``. + +Reward System Components +------------------------ + +The reward system has three main components: :class:`GlobalReward`, :class:`~bsk_rl.data.base.DataStore`, +and :class:`~bsk_rl.data.base.Data`. + +:class:`GlobalReward` acts as a global critic for the environment, rewarding each +agent's performance. Has full knowledge of the environment and can provide rewards +based on the global state of the environment, even if the agent does not have access +to that information; for example, you may not want to reward an agent for imaging a +target that has already been imaged by another agent, even if the agent does not know +that the target has been imaged. Reward is generally calculated by processing the +dictionary of new :class:`~bsk_rl.data.base.Data` per-satellite generated at each step +with the :class:`GlobalReward.calculate_reward` method. + +The :class:`~bsk_rl.data.base.DataStore` handles each satellite's local knowledge of the +scenario and the data it generates. The data store gains data in three ways: + +1. On environment reset, the :class:`~bsk_rl.scene.Scenario` calls + :class:`~bsk_rl.scene.Scenario.initial_data` to provide the initial knowledge of the + scenario for each satellite. This may be empty or may contain some a priori knowledge, + such as a list of targets that are desired to be imaged. +2. At the end of each step, the result of :class:`~bsk_rl.data.base.DataStore.get_log_state` + is compared to the previous step's result via :class:`~bsk_rl.data.base.DataStore.compare_log_states`. + A unit of :class:`~bsk_rl.data.base.Data` is returned. For example, the log state may + be the level of each target's buffer partition in the storage unit, so a change in + a certain buffer level leads to a unit of data that indicates the corresponding target + has been imaged. +3. At the end of each step, satellites communicate based on the :ref:`bsk_rl.comm` + system being used. Satellites merge the contents of their data stores with any other + satellite's data store that they have communicated with. + +Finally, :class:`~bsk_rl.data.base.Data` can represent data generated by the satellite +towards some goal (e.g. images of targets, time spend in a desireable mode, etc.) as well +as information about the environment that is useful toward completing its mission (e.g. +desired targets to image, what targets have already been imaged, etc.). + +Implementing Data & Reward Types +================================ + +See :ref:`bsk_rl.data.base` for full documentation of the reward system components to +when implementing custom data and reward types. + +Reward System Types +------------------- + +A variety of reward systems are available for use in the environment. The following table +provides a summary of the available reward systems: + ++-----------------------------+-------------------------------------------------------------------------+---------------------------------------------------------------------+ +| **Type** | **Purpose** | **Compatibility** | ++-----------------------------+-------------------------------------------------------------------------+---------------------------------------------------------------------+ +| :class:`NoReward` | Returns zero reward for every agent at every step. | | ++-----------------------------+-------------------------------------------------------------------------+---------------------------------------------------------------------+ +| :class:`UniqueImageReward` | Returns reward corresponding to target priority the | Should be used with :class:`~bsk_rl.sats.ImagingSatellite` and a | +| | first time a target is imaged by any agent. Causes | :class:`~bsk_rl.scene.Target`-based scenario. | +| | satellites to filter targets that are known to have | | +| | been imaged already. | | ++-----------------------------+-------------------------------------------------------------------------+---------------------------------------------------------------------+ +| :class:`ScanningTimeReward` | Returns reward based on time spend in the nadir-pointing scanning mode. | Should be used with the :class:`~bsk_rl.scene.UniformNadirScanning` | +| | | scenario. | ++-----------------------------+-------------------------------------------------------------------------+---------------------------------------------------------------------+ + +To select a reward system to use, pass an instance of :class:`GlobalReward` to the ``data`` +field of the environment constructor: + +.. code-block:: python + + env = ConstellationTasking( + ..., + data=ScanningTimeReward(), + ... + ) + +""" + +from bsk_rl.data.base import GlobalReward +from bsk_rl.data.nadir_data import ScanningTimeReward +from bsk_rl.data.no_data import NoReward +from bsk_rl.data.unique_image_data import UniqueImageReward + +__doc_title__ = "Data & Reward" +__all__ = [ + "GlobalReward", + "NoReward", + "UniqueImageReward", + "ScanningTimeReward", +] diff --git a/src/bsk_rl/data/base.py b/src/bsk_rl/data/base.py new file mode 100644 index 00000000..39089343 --- /dev/null +++ b/src/bsk_rl/data/base.py @@ -0,0 +1,186 @@ +"""Data logging, management, and reward calculation.""" + +import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, Optional + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.scene import Scenario + + +logger = logging.getLogger(__name__) + +LogStateType = Any + + +class Data(ABC): + """Base class for units of satellite data. + + Only needs to implement the ``__add__`` method, which is used to combine two units + of data. This is used when adding new data from actions or communication to the + data store. + """ + + @abstractmethod # pragma: no cover + def __add__(self, other: "Data") -> "Data": + """Define the combination of two units of data.""" + pass + + +class DataStore(ABC): + """Base class for satellite data logging.""" + + data_type: type[Data] # Define the unit of data used by the DataStore + + def __init__( + self, satellite: "Satellite", initial_data: Optional[Data] = None + ) -> None: + """Base class for satellite data logging. + + One DataStore is created for each satellite in the scenario each time the + scenario is reset. The DataStore is responsible for generating data from the + satellite's environment and actions by comparing the current and previous-step + state from :class:`~DataStore.get_log_state` and returning a unit of data with + :class:`~DataStore.compare_log_states`. These two methods must be implemented + by subclasses. + + Args: + satellite: Satellite which data is being stored for. + initial_data: Initial data to start the store with. Usually comes from + :class:`~bsk_rl.scene.Scenario.initial_data`. + """ + self.satellite = satellite + self.staged_data = [] + + if initial_data: + self.data = initial_data + else: + self.data = self.data_type() + self.new_data = self.data_type() + + def get_log_state(self) -> LogStateType: + """Pull information used in determining current data contribution.""" + pass + + @abstractmethod # pragma: no cover + def compare_log_states( + self, old_state: LogStateType, new_state: LogStateType + ) -> "Data": + """Generate a unit of data based on previous step and current step logs. + + Args: + old_state: A previous result of :class:`~DataStore.get_log_state`. + new_state: A newer result of :class:`~DataStore.get_log_state`. + + Returns: + Data: New data generated by the satellite. + """ + pass + + def update_from_logs(self) -> "Data": + """Update the data store based on collected information. + + Returns: + New data from the previous step. + """ + if not hasattr(self, "log_state"): + self.log_state = self.get_log_state() + return self.data_type() + old_log_state = self.log_state + self.log_state = self.get_log_state() + new_data = self.compare_log_states(old_log_state, self.log_state) + self.data += new_data + self.new_data = new_data + return new_data + + def stage_communicated_data(self, external_data: "Data") -> None: + """Prepare data to be added from another source, but don't add it yet. + + Works with :class:`~DataStore.update_with_communicated_data` to add data from + other satellites without erroneously propagating it through other satellites. + + Args: + external_data: Data from another satellite to be added + """ + self.staged_data.append(external_data) + + def update_with_communicated_data(self) -> None: + """Update the data store from staged data.""" + for staged in self.staged_data: + self.data += staged + self.staged_data = [] + + +class GlobalReward(ABC): + """Base class for simulation-wide data management.""" + + datastore_type: type[DataStore] # type of DataStore managed by the GlobalReward + + def __init__(self) -> None: + """Base class for simulation-wide data management and rewarding. + + The method :class:`calculate_reward` must be overridden by subclasses. Other + methods may be extended as necessary for housekeeping. + """ + self.scenario: "Scenario" + self.data_type = self.datastore_type.data_type + + def link_scenario(self, scenario: "Scenario") -> None: + """Link the data manager to the scenario. + + Args: + scenario: The scenario that the data manager is being used with. + """ + self.scenario = scenario + + def reset_pre_sim(self) -> None: + """Refresh data and cumulative reward for a new episode prior to simulator construction.""" + self.data = self.data_type() + self.cum_reward = {} + + def create_data_store(self, satellite: "Satellite") -> None: + """Create a data store for a satellite. + + Args: + satellite: Satellite to create a data store for. + """ + satellite.data_store = self.datastore_type( + satellite, + initial_data=self.scenario.initial_data(satellite, self.data_type), + ) + self.cum_reward[satellite.id] = 0.0 + + @abstractmethod # pragma: no cover + def calculate_reward(self, new_data_dict: dict[str, Data]) -> dict[str, float]: + """Calculate step reward based on all satellite data from a step. + + Returns a dictionary of rewards for each satellite based on the new data + generated by each satellite during the previous step, in the form: + + .. code-block:: python + + {"sat-1_id": 0.23, "sat-2_id": 0.0, ...} + + + Args: + new_data_dict: A dictionary of new data generated by each satellite, in the + form: + + .. code-block:: python + + {"sat-1_id": data1, "sat-2_id": data2, ...} + """ + pass + + def reward(self, new_data_dict: dict[str, Data]) -> dict[str, float]: + """Call :class:`calculate_reward` and log cumulative reward.""" + reward = self.calculate_reward(new_data_dict) + for satellite_id, sat_reward in reward.items(): + self.cum_reward[satellite_id] += sat_reward + logger.info(f"Data reward: {reward}") + return reward + + +__doc_title__ = "Base Data" +__all__ = ["GlobalReward", "DataStore", "Data"] diff --git a/src/bsk_rl/data/nadir_data.py b/src/bsk_rl/data/nadir_data.py new file mode 100644 index 00000000..563950ab --- /dev/null +++ b/src/bsk_rl/data/nadir_data.py @@ -0,0 +1,113 @@ +"""Data logging and management for nadir scanning.""" + +from typing import Callable, Optional + +from bsk_rl.data.base import Data, DataStore, GlobalReward +from bsk_rl.sats.satellite import Satellite + + +class ScanningTime(Data): + """Data for time spent scanning nadir.""" + + def __init__(self, scanning_time: float = 0.0) -> None: + """Data for time spent scanning nadir. + + Time spent scanning nadir ``scanning_time`` is stored in seconds. + + Args: + scanning_time: [s] Time spent scanning nadir. + """ + self.scanning_time = scanning_time + + def __add__(self, other: "ScanningTime") -> "ScanningTime": + """Define the combination of two units of data.""" + scanning_time = self.scanning_time + other.scanning_time + + return self.__class__(scanning_time) + + +class ScanningTimeStore(DataStore): + """DataStore for time spent scanning nadir.""" + + data_type = ScanningTime + + def __init__(self, *args, **kwargs) -> None: + """DataStore for time spent scanning nadir. + + Stores the amount of time spent scanning nadir. Calculates new time spent + scanning based on baud rate of the instrument and the increase in data stored + in the buffer. + """ + super().__init__(*args, **kwargs) + + def get_log_state(self) -> float: + """Return the amount of data currently stored in the storage unit.""" + storage_unit = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read() + stored_amount = storage_unit.storageLevel + + return stored_amount + + def compare_log_states(self, old_state: float, new_state: float) -> "ScanningTime": + """Generate a unit of data based on change in stored data amount. + + Args: + old_state: Previous amount of data in the storage unit. + new_state: Current amount of data in the storage unit. + + Returns: + Data: Data generated + """ + instrument_baudrate = self.satellite.dynamics.instrument.nodeBaudRate + + if new_state > old_state: + data_generated = (new_state - old_state) / instrument_baudrate + else: + data_generated = 0.0 + + return ScanningTime(scanning_time=data_generated) + + +class ScanningTimeReward(GlobalReward): + """GlobalReward for rewarding time spent scanning nadir.""" + + datastore_type = ScanningTimeStore # type of DataStore managed by the GlobalReward + + def __init__( + self, + reward_fn: Optional[Callable] = None, + ) -> None: + """GlobalReward for rewarding time spent scanning nadir. + + This class should be used with the :class:`~bsk_rl.scene.UniformNadirScanning` + scenario and a satellite with :class:`~bsk_rl.sim.fsw.ContinuousImagingFSWModel` + and the :class:`~bsk_rl.act.Scan` action. + + Time is computed based on the amount of data in the satellite's buffer. In the + basic configuration, this is the amount of time that the :class:`~bsk_rl.act.Scan` + action is enabled and pointing thresholds are met. However, if other models are + used to prevent the accumulation of data, the satellite will not be rewarded for + those times. + + Args: + reward_fn: Reward as function of time spend pointing nadir. By default, + is set to the time spent scanning times ``scenario.value_per_second``. + """ + super().__init__() + if reward_fn is None: + reward_fn = lambda t: t * self.scenario.value_per_second + + self.reward_fn = reward_fn + + def calculate_reward( + self, new_data_dict: dict[str, "ScanningTime"] + ) -> dict[str, float]: + """Calculate reward based on ``reward_fn``.""" + reward = {} + for sat, scanning_time in new_data_dict.items(): + reward[sat] = self.reward_fn(scanning_time.scanning_time) + + return reward + + +__doc_title__ = "Nadir Scanning" +__all__ = ["ScanningTimeReward", "ScanningTimeStore", "ScanningTime"] diff --git a/src/bsk_rl/data/no_data.py b/src/bsk_rl/data/no_data.py new file mode 100644 index 00000000..70428977 --- /dev/null +++ b/src/bsk_rl/data/no_data.py @@ -0,0 +1,56 @@ +"""A data and reward system that does nothing, returning zero reward on every step.""" + +import logging +from typing import TYPE_CHECKING, Any + +from bsk_rl.data.base import Data, DataStore, GlobalReward + +logger = logging.getLogger(__name__) + + +class NoData(Data): + """Holds no data.""" + + def __init__(self, *args, **kwargs): + """Holds no data.""" + return super().__init__(*args, **kwargs) + + def __add__(self, other): + """Add nothing to nothing.""" + return self.__class__() + + +class NoDataStore(DataStore): + """DataStore for no data.""" + + data_type = NoData + + def __init__(self, *args, **kwargs): + """Stores and generates no data.""" + return super().__init__(*args, **kwargs) + + def compare_log_states(self, old_state, new_state): + """Always returns no data.""" + return self.data_type() + + +class NoReward(GlobalReward): + """GlobalReward for no data.""" + + datastore_type = NoDataStore + + def __init__(self, *args, **kwargs): + """Returns zero reward at every step. + + This reward system is useful for debugging environments, but is not useful for + training, since reward is always zero for every satellite. + """ + return super().__init__(*args, **kwargs) + + def calculate_reward(self, new_data_dict): + """Reward nothing.""" + return {sat: 0.0 for sat in new_data_dict.keys()} + + +__doc_title__ = "No Data" +__all__ = ["NoReward", "NoDataStore", "NoData"] diff --git a/src/bsk_rl/data/unique_image_data.py b/src/bsk_rl/data/unique_image_data.py new file mode 100644 index 00000000..f0269814 --- /dev/null +++ b/src/bsk_rl/data/unique_image_data.py @@ -0,0 +1,178 @@ +"""Data system for recording unique images of targets.""" + +import logging +from typing import TYPE_CHECKING, Callable, Optional + +import numpy as np + +from bsk_rl.data.base import Data, DataStore, GlobalReward + +if TYPE_CHECKING: + from bsk_rl.sats import Satellite + from bsk_rl.scene.targets import Target + +logger = logging.getLogger(__name__) + + +class UniqueImageData(Data): + """Data for unique images of targets.""" + + def __init__( + self, + imaged: Optional[list["Target"]] = None, + duplicates: int = 0, + known: Optional[list["Target"]] = None, + ) -> None: + """Construct unit of data to record unique images. + + Keeps track of ``imaged`` targets, a count of ``duplicates`` (i.e. images that + were not rewarded due to the target already having been imaged), and all + ``known`` targets in the environment. + + Args: + imaged: List of targets that are known to be imaged. + duplicates: Count of target imaging duplication. + known: List of targets that are known to exist (imaged and unimaged). + """ + if imaged is None: + imaged = [] + self.imaged = list(set(imaged)) + self.duplicates = duplicates + len(imaged) - len(self.imaged) + if known is None: + known = [] + self.known = list(set(known)) + + def __add__(self, other: "UniqueImageData") -> "UniqueImageData": + """Combine two units of data. + + Args: + other: Another unit of data to combine with this one. + + Returns: + Combined unit of data. + """ + imaged = list(set(self.imaged + other.imaged)) + duplicates = ( + self.duplicates + + other.duplicates + + len(self.imaged) + + len(other.imaged) + - len(imaged) + ) + known = list(set(self.known + other.known)) + return self.__class__(imaged=imaged, duplicates=duplicates, known=known) + + +class UniqueImageStore(DataStore): + """DataStore for unique images of targets.""" + + data_type = UniqueImageData + + def __init__(self, *args, **kwargs) -> None: + """DataStore for unique images. + + Detects new images by watching for an increase in data in each target's corresponding + buffer. + """ + super().__init__(*args, **kwargs) + + def get_log_state(self) -> np.ndarray: + """Log the instantaneous storage unit state at the end of each step. + + Returns: + array: storedData from satellite storage unit + """ + return np.array( + self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData + ) + + def compare_log_states( + self, old_state: np.ndarray, new_state: np.ndarray + ) -> UniqueImageData: + """Check for an increase in logged data to identify new images. + + Args: + old_state: Older storedData from satellite storage unit. + new_state: Newer storedData from satellite storage unit. + + Returns: + list: Targets imaged at new_state that were unimaged at old_state. + """ + update_idx = np.where(new_state - old_state > 0)[0] + imaged = [] + for idx in update_idx: + message = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg + target_id = message.read().storedDataName[int(idx)] + imaged.append( + [target for target in self.data.known if target.id == target_id][0] + ) + return UniqueImageData(imaged=imaged) + + +class UniqueImageReward(GlobalReward): + """GlobalReward for rewarding unique images.""" + + datastore_type = UniqueImageStore + + def __init__( + self, + reward_fn: Callable = lambda p: p, + ) -> None: + """GlobalReward for rewarding unique images. + + This data system should be used with the :class:`~bsk_rl.sats.ImagingSatellite` and + a scenario that generates targets, such as :class:`~bsk_rl.scene.UniformTargets` or + :class:`~bsk_rl.scene.CityTargets`. + + The satellites all start with complete knowledge of the targets in the scenario. + Each target can only give one satellite a reward once; if any satellite has imaged + a target, reward will never again be given for that target. The satellites filter + known imaged targets from consideration for imaging to prevent duplicates. + Communication can transmit information about what targets have been imaged in order + to prevent reimaging. + + + Args: + scenario: GlobalReward.scenario + reward_fn: Reward as function of priority. + """ + super().__init__() + self.reward_fn = reward_fn + + def create_data_store(self, satellite: "Satellite") -> None: + """Override the access filter in addition to creating the data store.""" + super().create_data_store(satellite) + satellite.get_access_filter = lambda: satellite.data_store.data.imaged + + def calculate_reward( + self, new_data_dict: dict[str, UniqueImageData] + ) -> dict[str, float]: + """Reward each new unique image once. + + Reward is evaluated based on ``self.reward_fn(target.priority)``. + + Args: + new_data_dict: Record of new images for each satellite + + Returns: + reward: Cumulative reward across satellites for one step + """ + reward = {} + imaged_targets = sum( + [new_data.imaged for new_data in new_data_dict.values()], [] + ) + for sat_id, new_data in new_data_dict.items(): + reward[sat_id] = 0.0 + for target in new_data.imaged: + if target not in self.data.imaged: + reward[sat_id] += self.reward_fn( + target.priority + ) / imaged_targets.count(target) + + for new_data in new_data_dict.values(): + self.data += new_data + return reward + + +__doc_title__ = "Unique Images" +__all__ = ["UniqueImageReward", "UniqueImageStore", "UniqueImageData"] diff --git a/src/bsk_rl/env/__init__.py b/src/bsk_rl/env/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/env/scenario/__init__.py b/src/bsk_rl/env/scenario/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/env/scenario/data.py b/src/bsk_rl/env/scenario/data.py deleted file mode 100644 index e5dc459e..00000000 --- a/src/bsk_rl/env/scenario/data.py +++ /dev/null @@ -1,527 +0,0 @@ -"""Data logging, management, and reward calculation.""" - -import logging -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Optional - -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.scenario.environment_features import ( - Target, - ) - from bsk_rl.env.types import ( - EnvironmentFeatures, - Satellite, - ) - -import numpy as np - -logger = logging.getLogger(__name__) - -LogStateType = Any - - -class DataType(ABC): - """Base class for units of satellite data.""" - - @abstractmethod # pragma: no cover - def __add__(self, other: "DataType") -> "DataType": - """Define the combination of two units of data.""" - pass - - -class DataStore(ABC): - """Base class for satellite data logging. - - One DataStore is created per satellite. - """ - - DataType: type[DataType] # Define the unit of data used by the DataStore - - def __init__(self, data_manager: "DataManager", satellite: "Satellite") -> None: - """Construct a DataStore base class. - - Args: - data_manager: Simulation data manager to report back to - satellite: Satellite's data being stored - """ - self.satellite = satellite - self.is_fresh = True - self.staged_data = [] - - self._initialize_knowledge(data_manager.env_features) - self.data = self.DataType() - self.new_data = self.DataType() - - def _initialize_knowledge(self, env_features: "EnvironmentFeatures") -> None: - """Establish knowledge about the world known to the satellite. - - Defaults to knowing everything about the environment. - """ - self.env_knowledge = env_features - - def _clear_logs(self) -> None: - """If necessary, clear any loggers.""" - pass - - def _get_log_state(self) -> LogStateType: - """Pull information for current data contribution e.g. sensor readings.""" - pass - - @abstractmethod # pragma: no cover - def _compare_log_states( - self, old_state: LogStateType, new_state: LogStateType - ) -> "DataType": - """Generate a unit of data based on previous step and current step logs. - - Args: - old_state: A previous result of _get_log_state() - new_state: A newer result of _get_log_state() - - Returns: - DataType: Data generated - """ - pass - - def internal_update(self) -> "DataType": - """Update the data store based on collected information. - - Returns: - New data from the previous step - """ - if not hasattr(self, "log_state"): - self.log_state = self._get_log_state() - self._clear_logs() - return self.DataType() - old_log_state = self.log_state - self.log_state = self._get_log_state() - self._clear_logs() - new_data = self._compare_log_states(old_log_state, self.log_state) - self.data += new_data - self.new_data = new_data - return new_data - - def stage_communicated_data(self, external_data: "DataType") -> None: - """Prepare data to be added from another source, but don't add it yet. - - Args: - external_data: Data from another satellite to be added - """ - self.staged_data.append(external_data) - - def communication_update(self) -> None: - """Update the data store from staged data. - - Args: - external_data (DataType): Data collected by another satellite - """ - for staged in self.staged_data: - self.data += staged - self.staged_data = [] - - -class DataManager(ABC): - """Base class for simulation-wide data management.""" - - DataStore: type[DataStore] # type of DataStore managed by the DataManager - - def __init__(self, env_features: Optional["EnvironmentFeatures"] = None) -> None: - """Construct base class to handle data recording and rewarding. - - TODO: allow for creation/composition of multiple managers. - - Args: - env_features: Information about the environment that can be collected as - data - """ - self.env_features = env_features - self.DataType = self.DataStore.DataType - - def reset(self) -> None: - """Refresh data and cumulative reward for a new episode.""" - self.data = self.DataType() - self.cum_reward = {} - - def create_data_store(self, satellite: "Satellite") -> None: - """Create a data store for a satellite.""" - satellite.data_store = self.DataStore(self, satellite) - self.cum_reward[satellite.id] = 0.0 - - @abstractmethod # pragma: no cover - def _calc_reward(self, new_data_dict: dict[str, DataType]) -> dict[str, float]: - """Calculate step reward based on all satellite data from a step. - - Args: - new_data_dict: Satellite-DataType pairs of new data from a step - - Returns: - Step reward for each satellite - """ - pass - - def reward(self, new_data_dict: dict[str, DataType]) -> dict[str, float]: - """Call _calc_reward and log cumulative reward.""" - reward = self._calc_reward(new_data_dict) - for satellite_id, sat_reward in reward.items(): - self.cum_reward[satellite_id] += sat_reward - logger.info(f"Data reward: {reward}") - return reward - - -########### -# No Data # -########### -class NoData(DataType): - """DataType for no data.""" - - def __add__(self, other): - """Add nothing to nothing.""" - return self.__class__() - - -class NoDataStore(DataStore): - """DataStore for no data.""" - - DataType = NoData - - def _compare_log_states(self, old_state, new_state): - return self.DataType() - - -class NoDataManager(DataManager): - """DataManager for no data.""" - - DataStore = NoDataStore - - def _calc_reward(self, new_data_dict): - """Reward nothing.""" - return {sat: 0.0 for sat in new_data_dict.keys()} - - -####################################### -# Unique Targets with Constant Values # -####################################### - - -class UniqueImageData(DataType): - """DataType for unique images of targets.""" - - def __init__( - self, imaged: Optional[list["Target"]] = None, duplicates: int = 0 - ) -> None: - """Construct unit of data to record unique images. - - Args: - imaged: List of targets that are known to be imaged. - duplicates: Count of target imaging duplication. - """ - if imaged is None: - imaged = [] - - self.imaged = list(set(imaged)) - self.duplicates = duplicates + len(imaged) - len(self.imaged) - - def __add__(self, other: "UniqueImageData") -> "UniqueImageData": - """Combine two units of data. - - Args: - other: Another unit of data to combine with this one. - - Returns: - Combined unit of data. - """ - imaged = list(set(self.imaged + other.imaged)) - duplicates = ( - self.duplicates - + other.duplicates - + len(self.imaged) - + len(other.imaged) - - len(imaged) - ) - return self.__class__(imaged=imaged, duplicates=duplicates) - - -class UniqueImageStore(DataStore): - """DataStore for unique images of targets.""" - - DataType = UniqueImageData - - def _get_log_state(self) -> np.ndarray: - """Log the instantaneous storage unit state at the end of each step. - - Returns: - array: storedData from satellite storage unit - """ - return np.array( - self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData - ) - - def _compare_log_states( - self, old_state: np.ndarray, new_state: np.ndarray - ) -> UniqueImageData: - """Check for an increase in logged data to identify new images. - - Args: - old_state: older storedData from satellite storage unit - new_state: newer storedData from satellite storage unit - - Returns: - list: Targets imaged at new_state that were unimaged at old_state - """ - update_idx = np.where(new_state - old_state > 0)[0] - imaged = [] - for idx in update_idx: - message = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg - target_id = message.read().storedDataName[int(idx)] - imaged.append( - [ - target - for target in self.env_knowledge.targets - if target.id == target_id - ][0] - ) - return UniqueImageData(imaged=imaged) - - -class UniqueImagingManager(DataManager): - """DataManager for rewarding unique images.""" - - DataStore = UniqueImageStore - - def __init__( - self, - env_features: Optional["EnvironmentFeatures"] = None, - reward_fn: Callable = lambda p: p, - ) -> None: - """DataManager for rewarding unique images. - - Args: - env_features: DataManager.env_features - reward_fn: Reward as function of priority. - """ - super().__init__(env_features) - self.reward_fn = reward_fn - - def _calc_reward( - self, new_data_dict: dict[str, UniqueImageData] - ) -> dict[str, float]: - """Reward new each unique image once using self.reward_fn(). - - Args: - new_data_dict: Record of new images for each satellite - - Returns: - reward: Cumulative reward across satellites for one step - """ - reward = {} - imaged_targets = sum( - [new_data.imaged for new_data in new_data_dict.values()], [] - ) - for sat_id, new_data in new_data_dict.items(): - reward[sat_id] = 0.0 - for target in new_data.imaged: - if target not in self.data.imaged: - reward[sat_id] += self.reward_fn( - target.priority - ) / imaged_targets.count(target) - - for new_data in new_data_dict.values(): - self.data += new_data - return reward - - -"""Targets with Time-Dependent Rewards""" - - -# class TimeDepImageData(DataType): -# def __init__(self, rewards=None) -> None: -# """DataType to log scalar imaging - -# Args: -# imaged (dict, optional): Reward obtained from each target. -# """ -# if rewards is None: -# rewards = {} -# self.rewards = rewards - -# def __add__(self, other) -> "TimeDepImageData": -# rewards = copy(self.rewards) -# for target, reward in other.rewards.items(): -# if target in rewards: -# rewards[target] = max(rewards[target], reward) -# else: -# rewards[target] = reward -# return self.__class__(rewards=rewards) - - -# class TimeDepImageStore(DataStore): -# DataType = TimeDepImageData - -# def _get_log_state(self) -> Iterable[float]: -# """Log the instaneous storage unit state at the end of each step - -# Returns: -# array: storedData from satellite storage unit -# """ -# return np.array( -# self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData -# ) - -# def _compare_log_states(self, old_state, new_state) -> TimeDepImageData: -# """Checks two storage unit logs for an increase in logged data to identify new -# images - -# Args: -# old_state (array): older storedData from satellite storage unit -# new_state (array): newer storedData from satellite storage unit - -# Returns: -# list: Targets imaged at new_state that were unimaged at old_state -# """ -# update_idx = np.where(new_state - old_state > 0)[0] -# imaged = [] -# for idx in update_idx: -# message = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg -# target_id = message.read().storedDataName[int(idx)] -# imaged.append( -# [ -# target -# for target in self.env_knowledge.targets -# if target.id == target_id -# ][0] -# ) -# return self.DataType(imaged=imaged) - - -# class TimeDepImagingManager(DataManager): -# DataStore = TimeDepImageStore - -# # def __init__(self, env_features): -# # """DataManager for rewarding time-dependent images. Will only give marginal -# # reward for reimaging at higher value - -# # Args: -# # env_features (EnvironmentFeatures): DataManager.env_features -# # reward_fn (function, optional): Reward as function of priority. -# # """ -# # super().__init__(env_features) - -# def _calc_reward(self, new_data_dict): -# """Reward each image for additional reward from higher quality images - -# Args: -# new_data_dict (dict): Record of new images for each satellite - -# Returns: -# float: Cumulative new reward -# """ -# reward = 0.0 -# for new_data in new_data_dict.values(): -# for target, reward in new_data.rewards.items(): -# if target not in self.data.rewards: -# reward += reward -# elif reward > self.data.rewards[target]: -# reward += reward - self.data.rewards[target] -# self.data += new_data -# return reward - -################## -# Nadir Pointing # -################## - - -class NadirScanningTimeData(DataType): - """DataType for time spent scanning nadir.""" - - def __init__(self, scanning_time: float = 0.0) -> None: - """DataType to log data generated scanning nadir. - - Args: - scanning_time: Time spent scanning nadir - """ - self.scanning_time = scanning_time - - def __add__(self, other: "NadirScanningTimeData") -> "NadirScanningTimeData": - """Define the combination of two units of data.""" - scanning_time = self.scanning_time + other.scanning_time - - return self.__class__(scanning_time) - - -class ScanningNadirTimeStore(DataStore): - """DataStore for time spent scanning nadir.""" - - DataType = NadirScanningTimeData - - def _get_log_state(self) -> LogStateType: - """Return the amount of data stored in the storage unit.""" - storage_unit = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read() - stored_amount = storage_unit.storageLevel - - # return amount of data stored - return stored_amount - - def _compare_log_states( - self, old_state: float, new_state: float - ) -> "NadirScanningTimeData": - """Generate a unit of data based on change in stored data amount. - - Args: - old_state: Previous amount of data in the storage unit - new_state: Current amount of data in the storage unit - - Returns: - DataType: Data generated - """ - instrument_baudrate = self.satellite.dynamics.instrument.nodeBaudRate - - if new_state > old_state: - data_generated = (new_state - old_state) / instrument_baudrate - else: - data_generated = 0.0 - - return NadirScanningTimeData(data_generated) - - -class NadirScanningManager(DataManager): - """DataManager for rewarding time spent scanning nadir.""" - - DataStore = ScanningNadirTimeStore # type of DataStore managed by the DataManager - - def __init__( - self, - env_features: Optional["EnvironmentFeatures"] = None, - reward_fn: Optional[Callable] = None, - ) -> None: - """Construct a data manager for nadir scanning. - - Args: - env_features: Information about the environment that can be collected as - data - reward_fn: Reward as function of time spend pointing nadir. - """ - super().__init__(env_features) - if reward_fn is None: - - def reward_fn(p): - # Reward as a function of time send pointing nadir (p) and value - # per second - return p * self.env_features.value_per_second - - self.reward_fn = reward_fn - - def _calc_reward( - self, new_data_dict: dict[str, "NadirScanningTimeData"] - ) -> dict[str, float]: - """Calculate step reward based on all satellite data from a step. - - Args: - new_data_dict (dict): Satellite-DataType of new data from a step - - Returns: - Step reward - """ - reward = {} - for sat, scanning_time in new_data_dict.items(): - reward[sat] = self.reward_fn(scanning_time.scanning_time) - - return reward diff --git a/src/bsk_rl/env/simulation/__init__.py b/src/bsk_rl/env/simulation/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/env/simulation/environment.py b/src/bsk_rl/env/simulation/environment.py deleted file mode 100644 index 4dda64b0..00000000 --- a/src/bsk_rl/env/simulation/environment.py +++ /dev/null @@ -1,313 +0,0 @@ -"""Basilisk environment models.""" - -import logging -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Optional, Union -from weakref import proxy - -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import Simulator - -import numpy as np -from Basilisk import __path__ -from Basilisk.simulation import ( - eclipse, - ephemerisConverter, - exponentialAtmosphere, - groundLocation, -) -from Basilisk.utilities import macros as mc -from Basilisk.utilities import orbitalMotion, simIncludeGravBody - -from bsk_rl.utils.functional import collect_default_args, default_args -from bsk_rl.utils.orbital import random_epoch - -logger = logging.getLogger(__name__) - -bsk_path = __path__[0] - - -class EnvironmentModel(ABC): - """Abstract Basilisk environment model. - - One EnvironmentModel is instantiated for the environment each time a new simulator - is created. - """ - - @classmethod - def default_env_args(cls, **kwargs) -> dict[str, Any]: - """Compile default argments for the environment model.""" - defaults = collect_default_args(cls) - for k, v in kwargs.items(): - if k not in defaults: - raise KeyError(f"{k} not a valid key for env_args") - defaults[k] = v - return defaults - - def __init__( - self, - simulator: "Simulator", - env_rate: float, - priority: int = 300, - **kwargs, - ) -> None: - """Construct base environment model. - - Args: - simulator: Simulator using this model - env_rate: Rate of environment simulation [s] - priority: Model priority. - kwargs: Ignored - """ - self.simulator: Simulator = proxy(simulator) - - env_proc_name = "EnvironmentProcess" - env_proc = self.simulator.CreateNewProcess(env_proc_name, priority) - - # Define process name, task name and task time-step - self.env_task_name = "EnvironmentTask" - env_proc.addTask( - self.simulator.CreateNewTask(self.env_task_name, mc.sec2nano(env_rate)) - ) - - self._init_environment_objects(**kwargs) - - def __del__(self): - """Log when environment is deleted.""" - logger.debug("Basilisk environment deleted") - - @abstractmethod # pragma: no cover - def _init_environment_objects(self, **kwargs) -> None: - """Caller for all environment objects.""" - pass - - -class BasicEnvironmentModel(EnvironmentModel): - """Basic Environment with minimum necessary Basilisk environment components.""" - - @property - def PN(self): - """Planet relative to inertial frame rotation matrix.""" - return np.array( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - .read() - .J20002Pfix - ).reshape((3, 3)) - - @property - def omega_PN_N(self): - """Planet angular velocity in inertial frame [rad/s].""" - PNdot = np.array( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - .read() - .J20002Pfix_dot - ).reshape((3, 3)) - skew_PN_N = -np.matmul(np.transpose(self.PN), PNdot) - return np.array([skew_PN_N[2, 1], skew_PN_N[0, 2], skew_PN_N[1, 0]]) - - def _init_environment_objects(self, **kwargs) -> None: - self._set_gravity_bodies(**kwargs) - self._set_epoch_object(**kwargs) - self._set_atmosphere_density_model(**kwargs) - self._set_eclipse_object(**kwargs) - - @default_args(utc_init=random_epoch) - def _set_gravity_bodies( - self, utc_init: str, priority: int = 1100, **kwargs - ) -> None: - """Specify gravitational models to use in the simulation. - - Args: - utc_init: UTC datetime string - priority: Model priority. - kwargs: Ignored - """ - self.gravFactory = simIncludeGravBody.gravBodyFactory() - self.gravFactory.createSun() - self.planet = self.gravFactory.createEarth() - self.sun_index = 0 - self.body_index = 1 - - self.planet.isCentralBody = ( - True # ensure this is the central gravitational body - ) - self.planet.useSphericalHarmonicsGravityModel( - bsk_path + "/supportData/LocalGravData/GGM03S.txt", 10 - ) - - # setup Spice interface for some solar system bodies - timeInitString = utc_init - self.gravFactory.createSpiceInterface( - bsk_path + "/supportData/EphemerisData/", timeInitString - ) - self.gravFactory.spiceObject.zeroBase = "earth" - - self.simulator.AddModelToTask( - self.env_task_name, self.gravFactory.spiceObject, ModelPriority=priority - ) - - def _set_epoch_object(self, priority: int = 988, **kwargs) -> None: - """Add the ephemeris object to use with the SPICE library. - - Args: - priority: Model priority. - kwargs: Ignored - """ - self.ephemConverter = ephemerisConverter.EphemerisConverter() - self.ephemConverter.ModelTag = "ephemConverter" - self.ephemConverter.addSpiceInputMsg( - self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] - ) - self.ephemConverter.addSpiceInputMsg( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - ) - self.simulator.AddModelToTask( - self.env_task_name, self.ephemConverter, ModelPriority=priority - ) - - @default_args( - planetRadius=orbitalMotion.REQ_EARTH * 1e3, - baseDensity=1.22, - scaleHeight=8e3, - ) - def _set_atmosphere_density_model( - self, - planetRadius: float, - baseDensity: float, - scaleHeight: float, - priority: int = 1000, - **kwargs, - ) -> None: - """Add the exponential gravity model. - - Args: - planetRadius: Planet ground radius [m] - baseDensity: Exponential model parameter [kg/m^3] - scaleHeight: Exponential model parameter [m] - priority (int, optional): Model priority. - kwargs: Ignored - """ - self.densityModel = exponentialAtmosphere.ExponentialAtmosphere() - self.densityModel.ModelTag = "expDensity" - self.densityModel.planetRadius = planetRadius - self.densityModel.baseDensity = baseDensity - self.densityModel.scaleHeight = scaleHeight - self.densityModel.planetPosInMsg.subscribeTo( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - ) - self.simulator.AddModelToTask( - self.env_task_name, self.densityModel, ModelPriority=priority - ) - - def _set_eclipse_object(self, priority: int = 988, **kwargs) -> None: - """Specify what celestial object is causing an eclipse message. - - Args: - priority: Model priority. - kwargs: Ignored - """ - self.eclipseObject = eclipse.Eclipse() - self.eclipseObject.addPlanetToModel( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - ) - self.eclipseObject.sunInMsg.subscribeTo( - self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] - ) - self.simulator.AddModelToTask( - self.env_task_name, self.eclipseObject, ModelPriority=priority - ) - - def __del__(self) -> None: - """Log when environment is deleted and unload SPICE.""" - super().__del__() - try: - self.gravFactory.unloadSpiceKernels() - except AttributeError: - pass - - -class GroundStationEnvModel(BasicEnvironmentModel): - """Model that includes downlink ground stations.""" - - def _init_environment_objects(self, **kwargs) -> None: - super()._init_environment_objects(**kwargs) - self._set_ground_locations(**kwargs) - - @default_args( - groundStationsData=[ - dict(name="Boulder", lat=40.009971, long=-105.243895, elev=1624), - dict(name="Merritt", lat=28.3181, long=-80.6660, elev=0.9144), - dict(name="Singapore", lat=1.3521, long=103.8198, elev=15), - dict(name="Weilheim", lat=47.8407, long=11.1421, elev=563), - dict(name="Santiago", lat=-33.4489, long=-70.6693, elev=570), - dict(name="Dongara", lat=-29.2452, long=114.9326, elev=34), - dict(name="Hawaii", lat=19.8968, long=-155.5828, elev=9), - ], - groundLocationPlanetRadius=orbitalMotion.REQ_EARTH * 1e3, - gsMinimumElevation=10.0 * mc.D2R, - gsMaximumRange=-1, - ) - def _set_ground_locations( - self, - groundStationsData: list[dict[str, Union[str, float]]], - groundLocationPlanetRadius: float, - gsMinimumElevation: float, - gsMaximumRange: float, - priority: int = 1399, - **kwargs, - ) -> None: - """Specify the ground locations of interest. - - Args: - groundStationsData: Dicts with name (optional), lat (required), long - (required), and elevation (optional). - groundLocationPlanetRadius: Radius of ground locations from center of planet - [m] - gsMinimumElevation: Minimum elevation angle from station to satellite when - downlinking [rad] - gsMaximumRange: Maximum range from station to satellite when downlinking. -1 - to disable. [m] - priority: Model priority. - kwargs: Ignored - """ - self.groundStations = [] - self.groundLocationPlanetRadius = groundLocationPlanetRadius - self.gsMinimumElevation = gsMinimumElevation - self.gsMaximumRange = gsMaximumRange - for i, groundStationData in enumerate(groundStationsData): - self._create_ground_station(**groundStationData, priority=priority - i) - - def _create_ground_station( - self, - lat: float, - long: float, - elev: float = 0, - name: Optional[str] = None, - priority: int = 1399, - ) -> None: - """Add a ground station with given parameters. - - Args: - lat: Latitude [deg] - long: Longitude [deg] - elev: Elevation [m]. - name: Ground station identifier. - priority: Model priority. - """ - if name is None: - name = str(len(self.groundStations)) - - groundStation = groundLocation.GroundLocation() - groundStation.ModelTag = "GroundStation" + name - groundStation.planetRadius = self.groundLocationPlanetRadius - groundStation.specifyLocation(lat * mc.D2R, long * mc.D2R, elev) - groundStation.planetInMsg.subscribeTo( - self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] - ) - groundStation.minimumElevation = self.gsMinimumElevation - groundStation.maximumRange = self.gsMaximumRange - self.groundStations.append(groundStation) - - self.simulator.AddModelToTask( - self.env_task_name, groundStation, ModelPriority=priority - ) diff --git a/src/bsk_rl/env/types.py b/src/bsk_rl/env/types.py deleted file mode 100644 index 415f65a5..00000000 --- a/src/bsk_rl/env/types.py +++ /dev/null @@ -1,12 +0,0 @@ -# flake8: noqa -from __future__ import annotations - -from bsk_rl.env.scenario.communication import CommunicationMethod -from bsk_rl.env.scenario.data import DataManager, DataStore, DataType -from bsk_rl.env.scenario.environment_features import EnvironmentFeatures -from bsk_rl.env.scenario.observations import Observation -from bsk_rl.env.scenario.satellites import Satellite -from bsk_rl.env.simulation.dynamics import DynamicsModel -from bsk_rl.env.simulation.environment import EnvironmentModel -from bsk_rl.env.simulation.fsw import FSWModel -from bsk_rl.env.simulation.simulator import Simulator diff --git a/src/bsk_rl/_finish_install.py b/src/bsk_rl/finish_install.py similarity index 54% rename from src/bsk_rl/_finish_install.py rename to src/bsk_rl/finish_install.py index 1be068cc..21fa5b26 100644 --- a/src/bsk_rl/_finish_install.py +++ b/src/bsk_rl/finish_install.py @@ -1,17 +1,20 @@ +"""Package install scripts.""" + import io import zipfile from pathlib import Path 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(): + """Download data dependencies and check package readiness.""" r = requests.get( "https://simplemaps.com/static/data/world-cities/basic/simplemaps_worldcities_basicv1.76.zip" ) z = zipfile.ZipFile(io.BytesIO(r.content)) - z.extractall(Path(__file__).parent.resolve() / "data" / "simplemaps_worldcities") + z.extractall(Path(__file__).parent.resolve() / "_dat" / "simplemaps_worldcities") - _check_bsk_version() + check_bsk_version() diff --git a/src/bsk_rl/env/gym_env.py b/src/bsk_rl/gym.py similarity index 74% rename from src/bsk_rl/env/gym_env.py rename to src/bsk_rl/gym.py index 431d0fa7..024492cd 100644 --- a/src/bsk_rl/env/gym_env.py +++ b/src/bsk_rl/gym.py @@ -1,35 +1,4 @@ -"""General Satellite Tasking is a framework for creating satellite tasking RL environments. - -Three classes are provided for creating environments: - -+-------------------------------------+------------+---------------+--------------------------------------------------------------------+ -| **Environment** | **API** |**Agent Count**| **Purpose** | -+-------------------------------------+------------+---------------+--------------------------------------------------------------------+ -| :class:`SingleSatelliteTasking` | Gymnasium | 1 | Single-agent training; compatible with most RL libraries. | -+-------------------------------------+------------+---------------+--------------------------------------------------------------------+ -| :class:`GeneralSatelliteTasking` | Gymnasium | ≥1 | Multi-agent testing; actions and observations are given in tuples. | -+-------------------------------------+------------+---------------+--------------------------------------------------------------------+ -| :class:`MultiagentSatelliteTasking` | PettingZoo | ≥1 | Multi-agent training; compatible with multiagency RL libraries. | -+-------------------------------------+------------+---------------+--------------------------------------------------------------------+ - -Environments are customized by passing keyword arguments to the environment constructor. -When using ``gym.make``, the syntax looks like this: - -.. code-block:: python - - env = gym.make( - "SingleSatelliteTasking-v1", - satellites=Satellite(...), - env_features=StaticTargets(...), - ... - ) - -In some cases (e.g. the multiprocessed Gymnasium vector environment), it is necessary -for compatibility to instead register a new environment using the GeneralSatelliteTasking -class and a kwargs dict. - -See the :ref:`examples` for more information on environment configuration arguments. -""" +"""Gymnasium and PettingZoo environments for satellite tasking problems.""" import functools import logging @@ -42,15 +11,12 @@ class and a kwargs dict. from gymnasium import Env, spaces from pettingzoo.utils.env import AgentID, ParallelEnv -from bsk_rl.env.scenario.communication import NoCommunication -from bsk_rl.env.simulation.simulator import Simulator -from bsk_rl.env.types import ( - CommunicationMethod, - DataManager, - EnvironmentFeatures, - EnvironmentModel, - Satellite, -) +from bsk_rl.comm import CommunicationMethod, NoCommunication +from bsk_rl.data import GlobalReward, NoReward +from bsk_rl.sats import Satellite +from bsk_rl.scene import Scenario +from bsk_rl.sim import Simulator +from bsk_rl.sim.world import WorldModel from bsk_rl.utils import logging_config logger = logging.getLogger(__name__) @@ -67,26 +33,26 @@ class GeneralSatelliteTasking(Env, Generic[SatObs, SatAct]): def __init__( self, satellites: Union[Satellite, list[Satellite]], - env_features: EnvironmentFeatures, - data_manager: DataManager, - env_type: Optional[type[EnvironmentModel]] = None, - env_args: Optional[dict[str, Any]] = None, + scenario: Optional[Scenario] = None, + rewarder: Optional[GlobalReward] = None, + world_type: Optional[type[WorldModel]] = None, + world_args: Optional[dict[str, Any]] = None, communicator: Optional[CommunicationMethod] = None, sim_rate: float = 1.0, - max_step_duration: float = 600.0, - failure_penalty: float = -100, + max_step_duration: float = 1e9, + failure_penalty: float = -1.0, time_limit: float = float("inf"), terminate_on_time_limit: bool = False, log_level: Union[int, str] = logging.WARNING, log_dir: Optional[str] = None, render_mode=None, ) -> None: - """A Gymnasium environment adaptable to a wide range satellite tasking problems. + """A `Gymnasium `_ environment adaptable to a wide range satellite tasking problems. These problems involve satellite(s) being tasked to complete tasks and maintain aliveness. These tasks often include rewards for data collection. The environment can be configured for any collection of satellites, including heterogenous - constellations. Other configurable aspects are environment features (e.g. + constellations. Other configurable aspects are the scenario (e.g. imaging targets), data collection and recording, and intersatellite communication of data. @@ -94,22 +60,23 @@ def __init__( assigned as a tuple of actions, one per satellite. Args: - satellites: Satellite(s) to be simulated. - env_features: Environment the satellite is acting in; contains information - about targets, etc. - data_manager: Handles recording and rewarding for data collection towards - objectives. - communicator: Manages communication between satellites. - env_type: Type of Basilisk environment model to be constructed. - env_args: Arguments for environment model construction. {key: value or key: - function}, where function is called at reset to set the value (used for - randomization). - sim_rate: Rate for model simulation [s]. - max_step_duration: Maximum time to propagate sim at a step [s]. If - satellites are using variable interval actions, the step duration will - be less than or equal to this value. + satellites: Satellite(s) to be simulated. See :ref:`bsk_rl.sats`. + scenario: Environment the satellite is acting in; contains information + about targets, etc. See :ref:`bsk_rl.scene`. + rewarder: Handles recording and rewarding for data collection towards + objectives. See :ref:`bsk_rl.data`. + communicator: Manages communication between satellites. See :ref:`bsk_rl.comm`. + world_type: Type of Basilisk world model to be constructed. + world_args: Arguments for :class:`~bsk_rl.sim.world.WorldModel` construction. + Should be in the form of a dictionary with keys corresponding to the + arguments of the constructor and values that are either the desired value + or a function that takes no arguments and returns a randomized value. + sim_rate: [s] Rate for model simulation. + max_step_duration: [s] Maximum time to propagate sim at a step. If + satellites are using variable interval actions, the actual step duration + will be less than or equal to this value. failure_penalty: Reward for satellite failure. Should be nonpositive. - time_limit: Time at which to truncate the simulation [s]. + time_limit: [s] Time at which to truncate the simulation. terminate_on_time_limit: Send terminations signal time_limit instead of just truncation. log_level: Logging level for the environment. Default is ``WARNING``. @@ -118,40 +85,48 @@ def __init__( """ self.seed = None self._configure_logging(log_level, log_dir) + if isinstance(satellites, Satellite): satellites = [satellites] self.satellites = satellites self.simulator: Simulator - if env_type is None: - env_type = self._minimum_env_model() - self.env_type = env_type - if env_args is None: - env_args = self.env_type.default_env_args() - self.env_args_generator = self.env_type.default_env_args(**env_args) - self.env_features = env_features - self.data_manager = data_manager - if self.data_manager.env_features is None: - self.data_manager.env_features = self.env_features + + if scenario is None: + scenario = Scenario() + if rewarder is None: + rewarder = NoReward() + + if world_type is None: + world_type = self._minimum_world_model() + self.world_type = world_type + if world_args is None: + world_args = self.world_type.default_world_args() + self.world_args_generator = self.world_type.default_world_args(**world_args) + + self.scenario = scenario + self.rewarder = rewarder + self.rewarder.link_scenario(self.scenario) if communicator is None: communicator = NoCommunication() self.communicator = communicator - if self.communicator.satellites is None: - self.communicator.satellites = self.satellites + self.communicator.link_satellites(self.satellites) self.sim_rate = sim_rate self.max_step_duration = max_step_duration self.failure_penalty = failure_penalty + if self.failure_penalty > 0: + logger.warn("Failure penalty should be nonpositive") self.time_limit = time_limit self.terminate_on_time_limit = terminate_on_time_limit self.latest_step_duration = 0.0 self.render_mode = render_mode - def _minimum_env_model(self) -> type[EnvironmentModel]: - """Determine the minimum environment model required by the satellites.""" + def _minimum_world_model(self) -> type[WorldModel]: + """Determine the minimum world model required by the satellites.""" types = set( sum( - [satellite.dyn_type._requires_env() for satellite in self.satellites], + [satellite.dyn_type._requires_world() for satellite in self.satellites], [], ) ) @@ -170,7 +145,7 @@ class MinimumEnv(*types): def _configure_logging(self, log_level, log_dir=None): if isinstance(log_level, str): log_level = log_level.upper() - logger = logging.getLogger("bsk_rl.env") + logger = logging.getLogger("bsk_rl") logger.setLevel(log_level) # Ensure each process has its own logger to avoid conflicts when printing @@ -198,10 +173,11 @@ def _configure_logging(self, log_level, log_dir=None): fh.addFilter(logging_config.ContextFilter(env=self, proc_id=os.getpid())) logger.addHandler(fh) - def _generate_env_args(self) -> None: - """Instantiate env_args from any randomizers in provided env_args.""" - self.env_args = { - k: v if not callable(v) else v() for k, v in self.env_args_generator.items() + def _generate_world_args(self) -> None: + """Instantiate world_args from any randomizers in provided world_args.""" + self.world_args = { + k: v if not callable(v) else v() + for k, v in self.world_args_generator.items() } def reset( @@ -209,9 +185,16 @@ def reset( seed: Optional[int] = None, options=None, ) -> tuple[MultiSatObs, dict[str, Any]]: - """Reconstruct the simulator and wipe data records. + """Reconstruct the simulator and reset the scenario. - Satellite and environment arguments get randomized here, if applicable. + Satellite and world arguments get randomized on reset, if :class:`~bsk_rl.GeneralSatelliteTasking` ``.world_args`` + or :class:`~bsk_rl.sats.Satellite` ``.sat_args`` includes randomization functions. + + Certain classes in ``bsk_rl`` have a ``reset_pre_sim`` and/or ``reset_post_sim`` + method. These methods are respectively called before and after the new Basilisk + :class:`~bsk_rl.sim.Simulator` is created. These allow for reset actions that + feed into the underlying simulation and those that are dependent on the underlying + simulation to be performed. Args: seed: Gymnasium environment seed. @@ -229,32 +212,32 @@ def reset( self.seed = seed super().reset(seed=self.seed) np.random.seed(self.seed) - self._generate_env_args() + self._generate_world_args() self.latest_step_duration = 0.0 - self.env_features.reset() - self.data_manager.reset() + self.scenario.reset_pre_sim() + self.rewarder.reset_pre_sim() for satellite in self.satellites: - self.data_manager.create_data_store(satellite) - satellite.sat_args_generator["utc_init"] = self.env_args["utc_init"] + self.rewarder.create_data_store(satellite) + satellite.sat_args_generator["utc_init"] = self.world_args["utc_init"] satellite.reset_pre_sim() self.simulator = Simulator( self.satellites, - self.env_type, - self.env_args, + self.world_type, + self.world_args, sim_rate=self.sim_rate, max_step_duration=self.max_step_duration, time_limit=self.time_limit, ) - self.communicator.reset() + self.communicator.reset_post_sim() for satellite in self.satellites: satellite.reset_post_sim() - satellite.data_store.internal_update() + satellite.data_store.update_from_logs() observation = self._get_obs() info = self._get_info() @@ -266,7 +249,7 @@ def delete_simulator(self): Only the simulator contains strong references to BSK models, so deleting it will delete all Basilisk objects. Enable debug-level logging to verify that the - simulator, FSW, dynamics, and environment models are all deleted on reset. + simulator, FSW, dynamics, and world models are all deleted on reset. """ try: del self.simulator @@ -367,10 +350,10 @@ def _step(self, actions: MultiSatAct) -> None: self.latest_step_duration = self.simulator.sim_time - previous_time new_data = { - satellite.id: satellite.data_store.internal_update() + satellite.id: satellite.data_store.update_from_logs() for satellite in self.satellites } - self.reward_dict = self.data_manager.reward(new_data) + self.reward_dict = self.rewarder.reward(new_data) self.communicator.communicate() @@ -394,8 +377,12 @@ def step( truncated = self._get_truncated() info = self._get_info() logger.info(f"Step reward: {reward}") - logger.info(f"Episode terminated: {terminated}") - logger.info(f"Episode truncated: {truncated}") + if terminated or truncated: + logger.info(f"Episode terminated: {terminated}") + logger.info(f"Episode truncated: {truncated}") + else: + logger.debug(f"Episode terminated: {terminated}") + logger.debug(f"Episode truncated: {truncated}") logger.debug(f"Step info: {info}") logger.debug(f"Step observation: {observation}") return observation, reward, terminated, truncated, info @@ -410,8 +397,8 @@ def close(self) -> None: del self.simulator -class SingleSatelliteTasking(GeneralSatelliteTasking, Generic[SatObs, SatAct]): - def __init__(self, *args, **kwargs) -> None: +class SatelliteTasking(GeneralSatelliteTasking, Generic[SatObs, SatAct]): + def __init__(self, satellite: Satellite, *args, **kwargs) -> None: """A special case of :class:`GeneralSatelliteTasking` for one satellite. For compatibility with standard training APIs, actions and observations are @@ -419,13 +406,14 @@ def __init__(self, *args, **kwargs) -> None: tuple. Args: + satellite: Satellite to be simulated. *args: Passed to :class:`GeneralSatelliteTasking`. **kwargs: Passed to :class:`GeneralSatelliteTasking`. """ - super().__init__(*args, **kwargs) + super().__init__(satellites=satellite, *args, **kwargs) if not len(self.satellites) == 1: raise ValueError( - "SingleSatelliteTasking must be initialized with a single satellite." + "SatelliteTasking must be initialized with a single satellite." ) @property @@ -441,10 +429,7 @@ def observation_space(self) -> spaces.Box: @property def satellite(self) -> Satellite: - """Satellite being tasked. - - :meta private: - """ + """Satellite being tasked.""" return self.satellites[0] def step(self, action) -> tuple[Any, float, bool, bool, dict[str, Any]]: @@ -455,11 +440,11 @@ def _get_obs(self) -> Any: return self.satellite.get_obs() -class MultiagentSatelliteTasking( +class ConstellationTasking( GeneralSatelliteTasking, ParallelEnv, Generic[SatObs, SatAct, AgentID] ): def __init__(self, *args, **kwargs) -> None: - """Implements the PettingZoo parallel API for the :class:`GeneralSatelliteTasking` environment. + """Implements the `PettingZoo `_ parallel API for the :class:`GeneralSatelliteTasking` environment. Args: *args: Passed to :class:`GeneralSatelliteTasking`. @@ -620,3 +605,6 @@ def step( logger.debug(f"Step info: {info}") logger.debug(f"Step observation: {observation}") return observation, reward, terminated, truncated, info + + +__all__ = [] diff --git a/src/bsk_rl/obs/__init__.py b/src/bsk_rl/obs/__init__.py new file mode 100644 index 00000000..cf2dc75d --- /dev/null +++ b/src/bsk_rl/obs/__init__.py @@ -0,0 +1,45 @@ +"""Observations are found at ``bsk_rl.obs``. + +Satellite observation types can be used to add information to the observation. +:class:`Observation` provides an interface for creating new observation types. To +configure the observation, set the ``observation_spec`` attribute of a +:class:`~bsk_rl.env.scenario.satellites.Satellite` subclass. For example: + +.. code-block:: python + + class MyObservationSatellite(Satellite): + observation_spec = [ + SatProperties( + dict(prop="r_BN_P", module="dynamics", norm=REQ_EARTH * 1e3), + dict(prop="v_BN_P", module="dynamics", norm=7616.5), + ), + obs.OpportunityProperties( + dict(prop="priority"), + dict(prop="r_LP_P", norm=REQ_EARTH * 1e3), + n_ahead_observe=16, + ), + obs.Time(), + ] + +The format of the observation can setting the ``obs_type`` attribute of the +:class:`~bsk_rl.env.scenario.satellites.Satellite`. The default is ``np.ndarray``, but +it can also be set to a human-readable ``dict`` or a ``list``. + +Some commonly used observations are provided: + +* :class:`SatProperties` - Add arbitrary ``dynamics`` and ``fsw`` properties. +* :class:`Time` - Add simulation time to the observation. +* :class:`OpportunityProperties` - Add information about upcoming targets or other ground access points to the observation. +* :class:`Eclipse` - Add a tuple of the next orbit start and end. +""" + +from bsk_rl.obs.observations import ( + Eclipse, + Observation, + OpportunityProperties, + SatProperties, + Time, +) + +__doc_title__ = "Observations" +__all__ = ["Observation", "SatProperties", "Time", "OpportunityProperties", "Eclipse"] diff --git a/src/bsk_rl/env/scenario/observations.py b/src/bsk_rl/obs/observations.py similarity index 81% rename from src/bsk_rl/env/scenario/observations.py rename to src/bsk_rl/obs/observations.py index 08db20c4..3f9609ad 100644 --- a/src/bsk_rl/env/scenario/observations.py +++ b/src/bsk_rl/obs/observations.py @@ -1,57 +1,26 @@ -"""Satellite observation types can be used to add information to the observation. - -:class:`Observation` provides an interface for creating new observation types. To -configure the observation, set the ``observation_spec`` attribute of a -:class:`~bsk_rl.env.scenario.satellites.Satellite` subclass. For example: - -.. code-block:: python - - class MyObservationSatellite(Satellite): - observation_spec = [ - SatProperties( - dict(prop="r_BN_P", module="dynamics", norm=REQ_EARTH * 1e3), - dict(prop="v_BN_P", module="dynamics", norm=7616.5), - ), - obs.TargetProperties( - dict(prop="priority"), - dict(prop="location", norm=REQ_EARTH * 1e3), - n_ahead_observe=16, - ), - obs.Time(), - ] - -The format of the observation can setting the ``obs_type`` attribute of the -:class:`~bsk_rl.env.scenario.satellites.Satellite`. The default is ``np.ndarray``, but -it can also be set to a human-readable ``dict`` or a ``list``. - -Some commonly used observations are provided: - -* :class:`SatProperties` - Add arbitrary ``dynamics`` and ``fsw`` properties. -* :class:`Time` - Add simulation time to the observation. -* :class:`TargetProperties` - Add information about upcoming targets or other ground access points to the observation. -* :class:`Eclipse` - Add a tuple of the next orbit start and end. -""" +"""Classes for composing observations for a satellite.""" import logging from abc import ABC, abstractmethod from copy import deepcopy from typing import TYPE_CHECKING, Any, Callable, Optional, Union -from gymnasium import spaces - -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import Satellite, Simulator - import numpy as np from Basilisk.utilities import orbitalMotion +from gymnasium import spaces from bsk_rl.utils.functional import vectorize_nested_dict +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.sim import Simulator + + logger = logging.getLogger(__name__) -def obs_dict_to_space(obs_dict): - """Convert an observation dictionary to a gym space. +def nested_obs_to_space(obs_dict): + """Convert a nested observation dictionary to a gym space. Args: obs_dict: Observation dictionary @@ -63,7 +32,7 @@ def obs_dict_to_space(obs_dict): """ if isinstance(obs_dict, dict): return spaces.Dict( - {key: obs_dict_to_space(value) for key, value in obs_dict.items()} + {key: nested_obs_to_space(value) for key, value in obs_dict.items()} ) elif isinstance(obs_dict, list): return spaces.Box( @@ -71,12 +40,13 @@ def obs_dict_to_space(obs_dict): ) elif isinstance(obs_dict, (float, int)): return spaces.Box(low=-1e16, high=1e16, shape=(1,), dtype=np.float64) - else: + elif isinstance(obs_dict, np.ndarray): return spaces.Box(low=-1e16, high=1e16, shape=obs_dict.shape, dtype=np.float64) + else: + raise TypeError(f"Cannot convert {obs_dict} to gym space.") class ObservationBuilder: - """:meta private:""" def __init__(self, satellite: "Satellite", obs_type: type = np.ndarray) -> None: """Satellite subclass for composing observations. @@ -153,12 +123,7 @@ def get_obs(self) -> Union[dict, np.ndarray, list]: def observation_space(self) -> spaces.Space: """Space of the observation.""" obs = self.get_obs() - if isinstance(obs, (list, np.ndarray)): - return spaces.Box(low=-1e16, high=1e16, shape=obs.shape, dtype=np.float64) - elif isinstance(obs, dict): - return obs_dict_to_space(obs) - else: - raise ValueError(f"Invalid observation type: {self.obs_type}") + return nested_obs_to_space(obs) @property def observation_description(self) -> Any: @@ -243,6 +208,9 @@ def __init__( """ super().__init__(name=name) for obs_property in obs_properties: + for key in obs_property: + if key not in ["prop", "module", "norm", "name"]: + raise ValueError(f"Invalid property key: {key}") if "norm" not in obs_property: obs_property["norm"] = 1.0 if "name" not in obs_property: @@ -313,7 +281,7 @@ def get_obs(self) -> float: def _target_angle(sat, opp): - vector_target_spacecraft_P = opp["location"] - sat.dynamics.r_BN_P + vector_target_spacecraft_P = opp["r_LP_P"] - sat.dynamics.r_BN_P vector_target_spacecraft_P_hat = vector_target_spacecraft_P / np.linalg.norm( vector_target_spacecraft_P ) @@ -324,7 +292,7 @@ class OpportunityProperties(Observation): _fn_map = { "priority": lambda sat, opp: opp[opp["type"]].priority, - "location": lambda sat, opp: opp["location"], + "r_LP_P": lambda sat, opp: opp["r_LP_P"], "opportunity_open": lambda sat, opp: opp["window"][0] - sat.simulator.sim_time, "opportunity_mid": lambda sat, opp: sum(opp["window"]) / 2 - sat.simulator.sim_time, @@ -347,8 +315,8 @@ def __init__( .. code-block:: python - TargetProperties( - dict(prop="location", norm=REQ_EARTH * 1e3), + OpportunityProperties( + dict(prop="r_LP_P", norm=REQ_EARTH * 1e3), dict(prop="double_priority", fn=lambda sat, opp: opp["target"].priority * 2.0), n_ahead_observe=16, ) @@ -359,10 +327,11 @@ def __init__( Each observation is a dictionary with the keys: * ``name``: Name of the observation element. - * ``fn`` `optional`: Function to calculate property, in the form ``fn(satellite, opportunity)``. If not provided, the name will be used to look up a preset function: + * ``fn`` `optional`: Function to calculate property, in the form ``fn(satellite, opportunity)``. + If not provided, the key ``prop`` will be used to look up a preset function: * ``priority``: Priority of the target. - * ``location``: Location of the target in the planet-fixed frame. + * ``r_LP_P``: Location of the target in the planet-fixed frame. * ``opportunity_open``: Time until the opportunity opens. * ``opportunity_mid``: Time until the opportunity midpoint. * ``opportunity_close``: Time until the opportunity closes. @@ -373,7 +342,7 @@ def __init__( n_ahead_observe: Number of upcoming targets to consider. type: The type of opportunity to consider. Can be ``target``, ``ground_station``, or any other type of opportunity that has been added via - :obj:`~bsk_rl.env.scenario.satellites.AccessSatellite.add_location_for_access_checking`. + :obj:`~bsk_rl.sats.AccessSatellite.add_location_for_access_checking`. name: Name of the observation. """ if name is None: @@ -381,19 +350,31 @@ def __init__( super().__init__(name=name) self.type = type self.target_properties = target_properties - for prop_spec in self.target_properties: + for i, prop_spec in enumerate(self.target_properties): + for key in prop_spec: + if key not in ["fn", "norm", "name", "prop"]: + raise ValueError(f"Invalid property key: {key}") + if "norm" not in prop_spec: prop_spec["norm"] = 1.0 + if "fn" not in prop_spec: try: prop_spec["fn"] = self._fn_map[prop_spec["prop"]] except KeyError: raise ValueError( - f"Property {prop_spec['prop']} is not predefined and no `fn` was provided." + f"Property prop={prop_spec['prop']} is not predefined and no `fn` was provided." ) + else: + if "prop" in prop_spec: + logger.warning("Ignoring `prop` key when `fn` is provided.") if "name" not in prop_spec: - prop_spec["name"] = prop_spec["prop"] + if "prop" in prop_spec: + prop_spec["name"] = prop_spec["prop"] + else: + prop_spec["name"] = f"prop_{i}" + if prop_spec["norm"] != 1.0: prop_spec["name"] += "_normd" @@ -405,10 +386,10 @@ def reset_post_sim(self) -> None: :meta private: """ if self.type == "ground_station": - for ground_station in self.simulator.environment.groundStations: + for ground_station in self.simulator.world.groundStations: self.satellite.add_location_for_access_checking( object=ground_station.ModelTag, - location=np.array(ground_station.r_LP_P_Init).flatten(), + r_LP_P=np.array(ground_station.r_LP_P_Init).flatten(), min_elev=ground_station.minimumElevation, type="ground_station", ) @@ -418,7 +399,7 @@ def get_obs(self): :meta private: """ - from bsk_rl.env.scenario.satellites import AccessSatellite + from bsk_rl.sats import AccessSatellite if not isinstance(self.satellite, AccessSatellite): logger.warning( @@ -429,16 +410,14 @@ def get_obs(self): for i, opportunity in enumerate( self.satellite.find_next_opportunities( n=self.n_ahead_observe, - filter=self.satellite._get_access_filter(), + filter=self.satellite.get_access_filter(), types=self.type, ) ): props = {} for prop_spec in self.target_properties: - name = prop_spec["prop"] + name = prop_spec["name"] norm = prop_spec["norm"] - if norm != 1.0: - name += "_normd" value = prop_spec["fn"](self.satellite, opportunity) props[name] = value / norm obs[f"{self.name}_{i}"] = props @@ -446,7 +425,7 @@ def get_obs(self): class Eclipse(Observation): - def __init__(self, norm=5700.0, name="eclipse"): + def __init__(self, norm=1.0, name="eclipse"): """Include a tuple of the next eclipse start and end times in the observation. Args: @@ -468,3 +447,7 @@ def get_obs(self): (eclipse_start - self.simulator.sim_time) / self.norm, (eclipse_end - self.simulator.sim_time) / self.norm, ] + + +__doc_title__ = "Backend" +__all__ = ["ObservationBuilder"] diff --git a/src/bsk_rl/sats/__init__.py b/src/bsk_rl/sats/__init__.py new file mode 100644 index 00000000..edcdca26 --- /dev/null +++ b/src/bsk_rl/sats/__init__.py @@ -0,0 +1,103 @@ +"""``bsk_rl.sats`` defines satellite agents for the environments. + +A reference for working with satellites is given below. For a step-by-step guide, see +`this example <../../examples/satellite_configuration.ipynb>`_. + +Configuring a Satellite +----------------------- + +A subclass of a :class:`Satellite` type must be defined before can be used as an agent. +Two fields (``observation_spec`` and ``action_spec``) must be specified: these define +the observation and action spaces for the satellite. :ref:`bsk_rl.act` and :ref:`bsk_rl.obs` +provide more information on specifying the observation and action spaces. + +Two other fields (``dyn_model`` and ``fsw_model``) may be specified to select the +underlying dynamics and FSW models used by the Basilisk simulation. Some actions, +communication methods, or other environment configurations may necessitate the use of a +specific dynamics or FSW model. See :ref:`bsk_rl.sim.fsw` and :ref:`bsk_rl.sim.dyn` for +more information on selecting these models. + +In practice, configuring a satellite and passing it to an environment is straightforward: + +.. code-block:: python + + class MySatellite(Satellite): + observation_spec = [obs.Time(), ...] # list of observations + action_spec = [act.Drift(), ...] # list of actions + dyn_model = MyDynamicsModel # dynamics model type + fsw_model = MyFSWModel # FSW model type + + my_sat = MySatellite(name="my_satellite") + env = gym.make("SatelliteTasking-v1", satellite=my_sat, ...) + + +Setting Satellite Parameters +---------------------------- + +To specify satellite parameters such as physical properties and controller gains, a +``sat_args`` dictionary can be passed to the satellite constructor, which in turn is +used when initializing the FSW and dynamics simulations. Call the class method +:class:`~Satellite.default_sat_args` to list what parameters are available: + +.. code-block:: python + + >>> MySatellite.default_sat_args() + + {'mass': 100.0, 'Kp': 0.1, 'Ki': 0.01, 'Kd': 0.01, ...} + + +These parameters are documented in :ref:`bsk_rl.sim.fsw` and :ref:`bsk_rl.sim.dyn`. To +override the default parameters, pass a dictionary with the desired values. Parameters +can be set by value, or by passing a function that returns a value. In the latter case, +the randomizer function will be called each time the simulation is reset. For example: + +.. code-block:: python + + >>> my_sat = MySatellite( + name="my_satellite", + sat_args={"mass": lambda: np.random.uniform(95.0, 105.0), "Kp": 0.3} + ) + >>> env = gym.make("SatelliteTasking-v1", satellite=my_sat, ...) + >>> env.reset() + >>> my_sat.sat_args + + {'mass': 98.372, 'Kp': 0.3, 'Ki': 0.01, 'Kd': 0.01, ...} + +If one attempts to set a parameter that is not recognized, an error will be raised. + +Helpful Methods for Debugging +----------------------------- +A variety of methods are available for debugging and introspection: + +* :class:`~Satellite.observation_description` - Returns a human-interpretable description + of the observation. For array-type observations, this can be useful to map indices to + specific observation elements. +* :class:`~Satellite.action_description` - Returns a human-interpretable description of + the actions. For discrete actions, this will be a list of action names. +* :class:`~Satellite.observation_space` - Returns the observation space for the single agent. +* :class:`~Satellite.action_space` - Returns the action space for the single agent. +* :class:`~Satellite.is_alive` - Returns whether the satellite is still operational based on + ``@aliveness_checker`` s in the FSW and dynamics simulators. + +Helpful Methods for Extending +----------------------------- +When extending the satellite class, certain convenience methods are available: + +* :class:`~Satellite.reset_pre_sim` - Called on reset before the simulation is constructed. +* :class:`~Satellite.reset_post_sim` - Called on reset after the simulation is constructed. +* :class:`~Satellite.log_info` - Logs a message to ``INFO``, associating it with the satellite. + +Satellite Varieties +------------------- +* :class:`AccessSatellite` - Provides methods for determining when a satellite has + access to a ground location based on elevation angle. Can return ordered lists of + upcoming opportunities. +* :class:`ImagingSatellite` - Extends :class:`AccessSatellite` to provide methods for + interacting with :class:`bsk_rl.scene.Target` objects. +""" + +from bsk_rl.sats.access_satellite import AccessSatellite, ImagingSatellite +from bsk_rl.sats.satellite import Satellite + +__doc_title__ = "Satellites" +__all__ = ["Satellite", "AccessSatellite", "ImagingSatellite"] diff --git a/src/bsk_rl/env/scenario/satellites.py b/src/bsk_rl/sats/access_satellite.py similarity index 53% rename from src/bsk_rl/env/scenario/satellites.py rename to src/bsk_rl/sats/access_satellite.py index 9135b905..86efa8a1 100644 --- a/src/bsk_rl/env/scenario/satellites.py +++ b/src/bsk_rl/sats/access_satellite.py @@ -1,302 +1,30 @@ """Satellites are the agents in the environment.""" import bisect -import inspect import logging -from abc import ABC from typing import TYPE_CHECKING, Any, Iterable, Optional, Union -from weakref import proxy - -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import ( - DynamicsModel, - FSWModel, - Simulator, - Observation, - ) import numpy as np from Basilisk.utilities import macros -from gymnasium import spaces from scipy.optimize import minimize_scalar, root_scalar -from bsk_rl.env.scenario.actions import select_action_builder -from bsk_rl.env.scenario.data import DataStore, UniqueImageStore -from bsk_rl.env.scenario.environment_features import Target -from bsk_rl.env.scenario.observations import ObservationBuilder -from bsk_rl.env.simulation import dynamics, fsw -from bsk_rl.utils.functional import ( - AbstractClassProperty, - collect_default_args, - safe_dict_merge, - valid_func_name, -) -from bsk_rl.utils.orbital import TrajectorySimulator, elevation - -SatObs = Any -SatAct = Any - - -class Satellite(ABC): - """Abstract base class for satellites.""" - - dyn_type: type["DynamicsModel"] = AbstractClassProperty() - fsw_type: type["FSWModel"] = AbstractClassProperty() - observation_spec: list["Observation"] = AbstractClassProperty() - action_spec: list["Action"] = AbstractClassProperty() - - @classmethod - def default_sat_args(cls, **kwargs) -> dict[str, Any]: - """Compile default arguments for FSW and dynamics models. - - Returns: - default arguments for satellite models - """ - defaults = collect_default_args(cls.dyn_type) - defaults = safe_dict_merge(defaults, collect_default_args(cls.fsw_type)) - for name in dir(cls.fsw_type): - if inspect.isclass(getattr(cls.fsw_type, name)) and issubclass( - getattr(cls.fsw_type, name), fsw.Task - ): - defaults = safe_dict_merge( - defaults, collect_default_args(getattr(cls.fsw_type, name)) - ) - - for k, v in kwargs.items(): - if k not in defaults: - raise KeyError(f"{k} not a valid key for sat_args") - defaults[k] = v - return defaults - - def __init__( - self, - name: str, - sat_args: Optional[dict[str, Any]], - obs_type=np.ndarray, - variable_interval: bool = True, - ) -> None: - """Construct base satellite. - - Args: - name: identifier for satellite; does not need to be unique - sat_args: arguments for FSW and dynamic model construction. {key: value or - key: function}, where function is called at reset to set the value (used - for randomization). - variable_interval: Stop simulation at terminal events - """ - self.name = name - self.logger = logging.getLogger(__name__).getChild(self.name) - if sat_args is None: - sat_args = self.default_sat_args() - self.sat_args_generator = self.default_sat_args(**sat_args) - self.simulator: Simulator - self.fsw: "FSWModel" - self.dynamics: "DynamicsModel" - self.data_store: DataStore - self.requires_retasking: bool - self.variable_interval = variable_interval - self._timed_terminal_event_name = None - self.observation_builder = ObservationBuilder(self, obs_type=obs_type) - self.action_builder = select_action_builder(self) - - @property - def id(self) -> str: - """Unique human-readable identifier.""" - return f"{self.name}_{id(self)}" - - def _generate_sat_args(self) -> None: - """Instantiate sat_args from any randomizers in provided sat_args.""" - self.sat_args = { - k: v if not callable(v) else v() for k, v in self.sat_args_generator.items() - } - self.logger.debug(f"Satellite initialized with {self.sat_args}") - - def reset_pre_sim(self) -> None: - """Reset during environment reset, before simulator initialization.""" - self.info = [] - self.requires_retasking = True - self._generate_sat_args() - assert self.data_store.is_fresh - self.data_store.is_fresh = False - - self.trajectory = TrajectorySimulator( - utc_init=self.sat_args["utc_init"], - rN=self.sat_args["rN"], - vN=self.sat_args["vN"], - oe=self.sat_args["oe"], - mu=self.sat_args["mu"], - ) - self._timed_terminal_event_name = None - - def set_simulator(self, simulator: "Simulator"): - """Set the simulator for models. - - Called during simulator initialization. - - Args: - simulator: Basilisk simulator - """ - self.simulator = proxy(simulator) +from bsk_rl.sats.satellite import Satellite +from bsk_rl.scene.targets import Target +from bsk_rl.sim import dyn, fsw +from bsk_rl.utils.functional import valid_func_name +from bsk_rl.utils.orbital import elevation - def set_dynamics(self, dyn_rate: float) -> "DynamicsModel": - """Create dynamics model; called during simulator initialization. - - Args: - dyn_rate: rate for dynamics simulation [s] - - Returns: - Satellite's dynamics model - """ - dynamics = self.dyn_type(self, dyn_rate, **self.sat_args) - self.dynamics = proxy(dynamics) - return dynamics - - def set_fsw(self, fsw_rate: float) -> "FSWModel": - """Create flight software model; called during simulator initialization. - - Args: - fsw_rate: rate for FSW simulation [s] - - Returns: - Satellite's FSW model - """ - fsw = self.fsw_type(self, fsw_rate, **self.sat_args) - self.fsw = proxy(fsw) - return fsw - - def reset_post_sim(self) -> None: - """Reset in environment reset, after simulator initialization.""" - self.observation_builder.reset_post_sim() - self.action_builder.reset_post_sim() - - @property - def observation_space(self) -> spaces.Space: - """Observation space for single satellite, determined from observation. - - Returns: - gymanisium observation space - """ - return self.observation_builder.observation_space - - @property - def observation_description(self) -> Any: - """Human-interpretable description of observation space.""" - return self.observation_builder.observation_description - - @property - def action_space(self) -> spaces.Space: - """Action space for single satellite. - - Returns: - gymanisium action space - """ - return self.action_builder.action_space - - @property - def action_description(self) -> Any: - """Human-interpretable description of action space.""" - return self.action_builder.action_description - - def is_alive(self, log_failure=False) -> bool: - """Check if the satellite is violating any aliveness requirements. - - Checkes aliveness checkers in dynamics and FSW models. - - Returns: - is_alive - """ - return self.dynamics.is_alive(log_failure=log_failure) and self.fsw.is_alive( - log_failure=log_failure - ) - - @property - def _satellite_command(self) -> str: - """Generate string that refers to self in simBase.""" - return ( - "[satellite for satellite in self.satellites " - + f"if satellite.id=='{self.id}'][0]" - ) - - def _info_command(self, info: str) -> str: - """Generate command to log to info from an event. - - Args: - info: information to log; cannot include `'` or `"` - - Returns: - actionList action for simBase.createNewEvent - """ - return self._satellite_command + f".log_info('{info}')" - - def log_info(self, info: Any) -> None: - """Record information at the current time. - - Args: - info: Information to log - """ - self.info.append((self.simulator.sim_time, info)) - self.logger.info(f"{info}") - - def _update_timed_terminal_event( - self, t_close: float, info: str = "", extra_actions: list[str] = [] - ) -> None: - """Create a simulator event that stops the simulation a certain time. - - Args: - t_close: Termination time [s] - info: Additional identifying info to log at terminal time - extra_actions: Additional actions to perform at terminal time - """ - self._disable_timed_terminal_event() - self.log_info(f"setting timed terminal event at {t_close:.1f}") - - # Create new timed terminal event - self._timed_terminal_event_name = valid_func_name( - f"timed_terminal_{t_close}_{self.id}" - ) - self.simulator.createNewEvent( - self._timed_terminal_event_name, - macros.sec2nano(self.simulator.sim_rate), - True, - [f"self.TotalSim.CurrentNanos * {macros.NANO2SEC} >= {t_close}"], - [ - self._info_command(f"timed termination at {t_close:.1f} " + info), - self._satellite_command + ".requires_retasking = True", - ] - + extra_actions, - terminal=self.variable_interval, - ) - self.simulator.eventMap[self._timed_terminal_event_name].eventActive = True - - def _disable_timed_terminal_event(self) -> None: - """Turn off simulator termination due to window close checker.""" - if ( - self._timed_terminal_event_name is not None - and self._timed_terminal_event_name in self.simulator.eventMap - ): - self.simulator.delete_event(self._timed_terminal_event_name) - - def get_obs(self) -> SatObs: - """Construct the satellite's observation. - - Returns: - satellite observation - """ - return self.observation_builder.get_obs() - - def set_action(self, action: Any) -> None: - """Enable certain processes in the simulator to command the satellite task. +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.data.unique_image_data import UniqueImageStore - Should call an @action from FSW, among other things. +logger = logging.getLogger(__name__) - Args: - action: action index - """ - self.action_builder.set_action(action) +SatObs = Any +SatAct = Any class AccessSatellite(Satellite): - """Satellite that can detect access opportunities for ground locations.""" + """Satellite that detects access opportunities for ground locations.""" def __init__( self, @@ -305,23 +33,32 @@ def __init__( initial_generation_duration: Optional[float] = None, **kwargs, ) -> None: - """Construct an AccessSatellite. + """Satellite that detects access opportunities for ground locations. + + This satellite can be used to computes access opportunities for ground locations + such as imaging targets or ground stations. The satellite will calculate upcoming + opportunities for each location and order the opportunities by close time. + Opportunities are calculated based on a per-location minimum elevation angle. Args: - generation_duration: Duration to calculate additional imaging windows for - when windows are exhausted. If `None`, generate for the simulation - `time_limit` unless the simulation is infinite. [s] - initial_generation_duration: Duration to initially calculate imaging windows - [s] - args: Passed through to Satellite constructor - kwargs: Passed through to Satellite constructor + args: Passed through to :class:`Satellite` constructor. + generation_duration: [s] Duration to calculate additional opportunities for + when the simulation time reaches the current calculation time. If + `None`, generate opportunities for the simulation `time_limit` unless + the simulation is infinite. + initial_generation_duration: [s] Period to calculate opportunities for on + environment reset. + kwargs: Passed through to :class:`Satellite` constructor. """ super().__init__(*args, **kwargs) self.generation_duration = generation_duration self.initial_generation_duration = initial_generation_duration def reset_pre_sim(self) -> None: - """Reset satellite window calculations and lists.""" + """Reset satellite opportunity calculations and lists. + + :meta private: + """ super().reset_pre_sim() self.opportunities: list[dict] = [] self.window_calculation_time = 0 @@ -330,27 +67,32 @@ def reset_pre_sim(self) -> None: def add_location_for_access_checking( self, object: Any, - location: np.ndarray, + r_LP_P: np.ndarray, min_elev: float, type: str, ) -> None: - """Add a location to be included in window calculations. + """Add a location to be included in opportunity calculations. - Note that this location will only be included in future calls to - calculate_additional_windows. + .. warning:: + The added location will only be considered in future calls to + :class:`~AccessSatellite.calculate_additional_windows`; opportunities are not + computed retroactively. Args: - object: Object to add window for - location: Objects PCPF location [m] - min_elev: Minimum elevation angle for access [rad] - type: Category of windows to add location to + object: Object for with to compute opportunities. + r_LP_P: [m] Objects planet-fixed location. + min_elev: [rad] Minimum elevation angle for access. + type: Category of opportunity target provides. """ - location_dict = dict(location=location, min_elev=min_elev, type=type) + location_dict = dict(r_LP_P=r_LP_P, min_elev=min_elev, type=type) location_dict[type] = object self.locations_for_access_checking.append(location_dict) def reset_post_sim(self) -> None: - """Handle initial window calculations for new simulation.""" + """Handle initial window calculations for new simulation. + + :meta private: + """ super().reset_post_sim() if self.initial_generation_duration is None: if self.simulator.time_limit == float("inf"): @@ -394,18 +136,18 @@ def calculate_additional_windows(self, duration: float) -> None: r_max = np.max(np.linalg.norm(positions, axis=-1)) access_dist_thresh_multiplier = 1.1 for location in self.locations_for_access_checking: - alt_est = r_max - np.linalg.norm(location["location"]) + alt_est = r_max - np.linalg.norm(location["r_LP_P"]) access_dist_threshold = ( access_dist_thresh_multiplier * alt_est / np.sin(location["min_elev"]) ) candidate_windows = self._find_candidate_windows( - location["location"], times, positions, access_dist_threshold + location["r_LP_P"], times, positions, access_dist_threshold ) for candidate_window in candidate_windows: roots = self._find_elevation_roots( r_BP_P_interp, - location["location"], + location["r_LP_P"], location["min_elev"], candidate_window, ) @@ -417,7 +159,7 @@ def calculate_additional_windows(self, duration: float) -> None: location[location["type"]], new_window, type=location["type"], - location=location["location"], + r_LP_P=location["r_LP_P"], merge_time=times[0], ) @@ -443,7 +185,7 @@ def root_fn(t): elev_0, elev_1 = root_fn(window[0]), root_fn(window[1]) if elev_0 < 0 and elev_1 < 0: - logging.warning( + logger.warning( "initial_generation_duration is shorter than the maximum window length; some windows may be neglected." ) return [] @@ -515,7 +257,7 @@ def _add_window( object: Any, new_window: tuple[float, float], type: str, - location: np.ndarray, + r_LP_P: np.ndarray, merge_time: Optional[float] = None, ): """Add an opportunity window. @@ -524,6 +266,7 @@ def _add_window( object: Object to add window for new_window: New window for target type: Type of window being added + r_LP_P: Planet-fixed location of object merge_time: Time at which merges with existing windows will occur. If None, check all windows for merges. """ @@ -538,17 +281,13 @@ def _add_window( return bisect.insort( self.opportunities, - {type: object, "window": new_window, "type": type, "location": location}, + {type: object, "window": new_window, "type": type, "r_LP_P": r_LP_P}, key=lambda x: x["window"][1], ) @property def upcoming_opportunities(self) -> list[dict]: - """Ordered list of opportunities that have not yet closed. - - Returns: - list: list of upcoming opportunities - """ + """Ordered list of opportunities that have not yet closed.""" start = bisect.bisect_left( self.opportunities, self.simulator.sim_time + 1e-12, @@ -567,9 +306,6 @@ def opportunities_dict( Args: types: Types of opportunities to include. If None, include all types. filter: Objects to exclude from the dictionary. - - Returns: - windows: objects -> windows list """ if isinstance(types, str): types = [types] @@ -588,16 +324,13 @@ def upcoming_opportunities_dict( types: Optional[Union[str, list[str]]] = None, filter: list = [], ) -> dict[Any, list[tuple[float, float]]]: - """Get dictionary of opportunities. + """Get dictionary of upcoming opportunities. Maps objects to lists of windows that have not yet closed. Args: types: Types of opportunities to include. If None, include all types. filter: Objects to exclude from the dictionary. - - Returns: - windows: objects -> windows list (upcoming only) """ if isinstance(types, str): types = [types] @@ -621,9 +354,6 @@ def next_opportunities_dict( Args: types: Types of opportunities to include. If None, include all types. filter: Objects to exclude from the dictionary. - - Returns: - windows: objects -> next window """ if isinstance(types, str): types = [types] @@ -655,7 +385,7 @@ def find_next_opportunities( filter: Objects to exclude from the dictionary. Returns: - list: n nearest opportunities, ordered + ``n`` nearest opportunities, ordered """ if isinstance(types, str): types = [types] @@ -674,20 +404,29 @@ def find_next_opportunities( if len(next_opportunities) >= n: return next_opportunities self.calculate_additional_windows(self.generation_duration) - if pad: + if pad and len(next_opportunities) >= 1: next_opportunities += [next_opportunities[-1]] * ( n - len(next_opportunities) ) + else: + raise RuntimeError( + "No opportunities found! Use add_location_for_access_checking to add locations." + ) return next_opportunities - def _get_access_filter(self): + def get_access_filter(self): + """Return a list of objects that should not be considered for access checking. + + For example, ground stations that are offline or targets that are no longer + interesting. + """ return [] class ImagingSatellite(AccessSatellite): """Satellite with agile imaging capabilities.""" - dyn_type = dynamics.ImagingDynModel + dyn_type = dyn.ImagingDynModel fsw_type = fsw.ImagingFSWModel def __init__( @@ -695,96 +434,54 @@ def __init__( *args, **kwargs, ) -> None: - """Construct an ImagingSatellite. + """Satellite with agile imaging capabilities. - Can stop the simulation when a target is imaged or missed. + Stop the simulation when a target is imaged or missed so that time is not wasted + on an inaccessible or already imaged target. """ super().__init__(*args, **kwargs) self.fsw: ImagingSatellite.fsw_type self.dynamics: ImagingSatellite.dyn_type - self.data_store: UniqueImageStore + self.data_store: "UniqueImageStore" + + @property + def known_targets(self) -> list["Target"]: + """List of known targets.""" + try: + return self.data_store.data.known + except AttributeError: + return [] def reset_pre_sim(self) -> None: - """Set the buffer parameters based on computed windows.""" + """Set the buffer parameters based on computed windows. + + :meta private: + """ super().reset_pre_sim() - self.sat_args["transmitterNumBuffers"] = len( - self.data_store.env_knowledge.targets - ) - self.sat_args["bufferNames"] = [ - target.id for target in self.data_store.env_knowledge.targets - ] + self.sat_args["transmitterNumBuffers"] = len(self.known_targets) + self.sat_args["bufferNames"] = [target.id for target in self.known_targets] self._image_event_name = None self.imaged = 0 self.missed = 0 def reset_post_sim(self) -> None: - """Handle initial_generation_duration setting and calculate windows.""" - for target in self.data_store.env_knowledge.targets: + """Handle initial_generation_duration setting and calculate windows. + + :meta private: + """ + # TODO: This should add any targets the satellite could know about, then + # filter unknown ones instead. As is, if the satellite learns about a target + # later than reset, it will never generate opportunities for it. + for target in self.known_targets: self.add_location_for_access_checking( object=target, - location=target.location, + r_LP_P=target.r_LP_P, min_elev=self.sat_args["imageTargetMinimumElevation"], type="target", ) super().reset_post_sim() - def _get_access_filter(self): - try: - return self.data_store.data.imaged - except AttributeError: - return [] - - @property - def windows(self) -> dict[Target, list[tuple[float, float]]]: - """Access windows via dict of targets -> list of windows.""" - return self.opportunities_dict(types="target", filter=self._get_access_filter()) - - @property - def upcoming_windows(self) -> dict[Target, list[tuple[float, float]]]: - """Access upcoming windows in a dict of targets -> list of windows.""" - return self.upcoming_opportunities_dict( - types="target", filter=self._get_access_filter() - ) - - @property - def next_windows(self) -> dict[Target, tuple[float, float]]: - """Soonest window for each target. - - Returns: - dict: first non-closed window for each target - """ - return self.next_opportunities_dict( - types="target", filter=self._get_access_filter() - ) - - def upcoming_targets( - self, n: int, pad: bool = True, max_lookahead: int = 100 - ) -> list[Target]: - """Find the n nearest targets. - - Targets are sorted by window close time; currently open windows are included. - - Args: - n: number of windows to look ahead - pad: if true, duplicates the last target if the number of targets found is - less than n - max_lookahead: maximum times to call calculate_additional_windows - - Returns: - list: n nearest targets, ordered - """ - return [ - opportunity["target"] - for opportunity in self.find_next_opportunities( - n=n, - pad=pad, - max_lookahead=max_lookahead, - filter=self._get_access_filter(), - types="target", - ) - ] - - def _update_image_event(self, target: Target) -> None: + def _update_image_event(self, target: "Target") -> None: """Create a simulator event that terminates on imaging. Causes the simulation to stop when a target is imaged. @@ -842,34 +539,45 @@ def parse_target_selection(self, target_query: Union[int, Target, str]): target_query: Taret upcoming index, object, or id. """ if np.issubdtype(type(target_query), np.integer): - target = self.upcoming_targets(target_query + 1)[-1] + target = self.find_next_opportunities( + n=target_query + 1, + filter=self.get_access_filter(), + types="target", + )[-1]["target"] elif isinstance(target_query, Target): target = target_query elif isinstance(target_query, str): - target = [ - target - for target in self.data_store.env_knowledge.targets - if target.id == target_query - ][0] + try: + target = [ + target for target in self.known_targets if target.id == target_query + ][0] + except IndexError: + raise ValueError(f"Target {target_query} not a known target!") else: raise TypeError(f"Invalid target_query! Cannot be a {type(target_query)}!") return target - def enable_target_window(self, target: Target): - """Enable the next window close event for target.""" + def enable_target_window(self, target: "Target"): + """Enable a timed opportunity close event and a successfully imaged event. + + Args: + target: Target to terminate the step on imaging or when out of range. + """ self._update_image_event(target) - next_window = self.next_windows[target] + next_window = self.next_opportunities_dict( + types="target", filter=self.get_access_filter() + )[target] self.log_info( f"{target} window enabled: {next_window[0]:.1f} to {next_window[1]:.1f}" ) - self._update_timed_terminal_event( + self.update_timed_terminal_event( next_window[1], info=f"for {target} window", extra_actions=[self._satellite_command + ".missed += 1"], ) - def task_target_for_imaging(self, target: Target): + def task_target_for_imaging(self, target: "Target"): """Task the satellite to image a target. Args: @@ -877,22 +585,5 @@ def task_target_for_imaging(self, target: Target): """ msg = f"{target} tasked for imaging" self.log_info(msg) - self.fsw.action_image(target.location, target.id) + self.fsw.action_image(target.r_LP_P, target.id) self.enable_target_window(target) - - -######################### -### Convenience Types ### -######################### -class SteeringImagerSatellite(ImagingSatellite): - """Convenience type for an imaging satellite with MRP steering.""" - - dyn_type = dynamics.FullFeaturedDynModel - fsw_type = fsw.SteeringImagerFSWModel - - -class FBImagerSatellite(ImagingSatellite): - """Convenience type for an imaging satellite with feedback control.""" - - dyn_type = dynamics.FullFeaturedDynModel - fsw_type = fsw.ImagingFSWModel diff --git a/src/bsk_rl/sats/satellite.py b/src/bsk_rl/sats/satellite.py new file mode 100644 index 00000000..59539f96 --- /dev/null +++ b/src/bsk_rl/sats/satellite.py @@ -0,0 +1,300 @@ +"""Satellites are the agents in the environment.""" + +import inspect +import logging +from abc import ABC +from typing import TYPE_CHECKING, Any, Optional +from weakref import proxy + +import numpy as np +from Basilisk.utilities import macros +from gymnasium import spaces + +from bsk_rl.act.actions import select_action_builder +from bsk_rl.obs.observations import ObservationBuilder +from bsk_rl.sim import dyn, fsw +from bsk_rl.utils.functional import ( + AbstractClassProperty, + collect_default_args, + safe_dict_merge, + valid_func_name, +) +from bsk_rl.utils.orbital import TrajectorySimulator + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.act import Action + from bsk_rl.data.base import DataStore + from bsk_rl.obs import Observation + from bsk_rl.sim import Simulator + + +SatObs = Any +SatAct = Any + + +class Satellite(ABC): + """Abstract base class for satellites.""" + + dyn_type: type["dyn.DynamicsModel"] = AbstractClassProperty() + fsw_type: type["fsw.FSWModel"] = AbstractClassProperty() + observation_spec: list["Observation"] = AbstractClassProperty() + action_spec: list["Action"] = AbstractClassProperty() + + @classmethod + def default_sat_args(cls, **kwargs) -> dict[str, Any]: + """Compile default arguments for :class:`~bsk_rl.sim.dyn.DynamicsModel` and :class:`~bsk_rl.sim.fsw.FSWModel`, replacing those specified. + + Args: + **kwargs: Arguments to override in the default arguments. + + Returns: + Dictionary of arguments for simulation models. + """ + defaults = collect_default_args(cls.dyn_type) + defaults = safe_dict_merge(defaults, collect_default_args(cls.fsw_type)) + for name in dir(cls.fsw_type): + if inspect.isclass(getattr(cls.fsw_type, name)) and issubclass( + getattr(cls.fsw_type, name), fsw.Task + ): + defaults = safe_dict_merge( + defaults, collect_default_args(getattr(cls.fsw_type, name)) + ) + + for k, v in kwargs.items(): + if k not in defaults: + raise KeyError(f"{k} not a valid key for sat_args") + defaults[k] = v + return defaults + + def __init__( + self, + name: str, + sat_args: Optional[dict[str, Any]], + obs_type=np.ndarray, + variable_interval: bool = True, + ) -> None: + """The base satellite agent class. + + Args: + name: Identifier for satellite; does not need to be unique. + sat_args: Arguments for :class:`~bsk_rl.sim.dyn.DynamicsModel` and + :class:`~bsk_rl.sim.fsw.FSWModel` construction. Should be in the form of + a dictionary with keys corresponding to the arguments of the constructor + and values that are either the desired value or a function that takes no + arguments and returns a randomized value. + obs_type: Observation format for the satellite. The :class:`bsk_rl.obs.observations.ObservationBuilder` + will convert the observation to this format. + variable_interval: Whether to stop the simulation at terminal events. If + False, only the ``max_step_duration`` setting in :class:`~bsk_rl.GeneralSatelliteTasking` + will stop the simulation. + """ + self.name = name + self.logger = logging.getLogger(__name__).getChild(self.name) + if sat_args is None: + sat_args = self.default_sat_args() + self.sat_args_generator = self.default_sat_args(**sat_args) + self.simulator: "Simulator" + self.fsw: "fsw.FSWModel" + self.dynamics: "dyn.DynamicsModel" + self.data_store: "DataStore" + self.requires_retasking: bool + self.variable_interval = variable_interval + self._timed_terminal_event_name = None + self.observation_builder = ObservationBuilder(self, obs_type=obs_type) + self.action_builder = select_action_builder(self) + + @property + def id(self) -> str: + """Unique human-readable identifier.""" + return f"{self.name}_{id(self)}" + + def _generate_sat_args(self) -> None: + """Instantiate sat_args from any randomizers in provided sat_args.""" + self.sat_args = { + k: v if not callable(v) else v() for k, v in self.sat_args_generator.items() + } + self.logger.debug(f"Satellite initialized with {self.sat_args}") + + def reset_pre_sim(self) -> None: + """Called during environment reset, before Basilisk simulation initialization.""" + self.info = [] + self.requires_retasking = True + self._generate_sat_args() + self.trajectory = TrajectorySimulator( + utc_init=self.sat_args["utc_init"], + rN=self.sat_args["rN"], + vN=self.sat_args["vN"], + oe=self.sat_args["oe"], + mu=self.sat_args["mu"], + ) + self._timed_terminal_event_name = None + + def set_simulator(self, simulator: "Simulator"): + """Set the simulator for models. + + Called during simulator initialization. + + Args: + simulator: Basilisk simulator + + :meta private: + """ + self.simulator = proxy(simulator) + + def set_dynamics(self, dyn_rate: float) -> "dyn.DynamicsModel": + """Create dynamics model; called during simulator initialization. + + Args: + dyn_rate: rate for dynamics simulation [s] + + Returns: + Satellite's dynamics model + + :meta private: + """ + dynamics = self.dyn_type(self, dyn_rate, **self.sat_args) + self.dynamics = proxy(dynamics) + return dynamics + + def set_fsw(self, fsw_rate: float) -> "fsw.FSWModel": + """Create flight software model; called during simulator initialization. + + Args: + fsw_rate: rate for FSW simulation [s] + + Returns: + Satellite's FSW model + + :meta private: + """ + fsw = self.fsw_type(self, fsw_rate, **self.sat_args) + self.fsw = proxy(fsw) + return fsw + + def reset_post_sim(self) -> None: + """Called during environment reset, after Basilisk simulation initialization.""" + self.observation_builder.reset_post_sim() + self.action_builder.reset_post_sim() + + @property + def observation_space(self) -> spaces.Space: + """Observation space for single satellite, determined from observation. + + Returns: + gymanisium observation space + """ + return self.observation_builder.observation_space + + def get_obs(self) -> SatObs: + """Construct the satellite's observation. + + Returns: + satellite observation + """ + return self.observation_builder.get_obs() + + @property + def observation_description(self) -> Any: + """Human-interpretable description of observation space.""" + return self.observation_builder.observation_description + + @property + def action_space(self) -> spaces.Space: + """Action space for single satellite. + + Returns: + gymanisium action space + """ + return self.action_builder.action_space + + def set_action(self, action: Any) -> None: + """Enable certain processes in the simulator to command the satellite task. + + Args: + action: Action to take, according to the :class:`action_spec` + """ + self.action_builder.set_action(action) + + @property + def action_description(self) -> Any: + """Human-interpretable description of action space.""" + return self.action_builder.action_description + + def is_alive(self, log_failure=False) -> bool: + """Check if the satellite is violating any aliveness requirements. + + Checks aliveness checkers in dynamics and FSW models. + + Returns: + is_alive + """ + return self.dynamics.is_alive(log_failure=log_failure) and self.fsw.is_alive( + log_failure=log_failure + ) + + @property + def _satellite_command(self) -> str: + """Generate string that refers to self in simBase.""" + return ( + "[satellite for satellite in self.satellites " + + f"if satellite.id=='{self.id}'][0]" + ) + + def _info_command(self, info: str) -> str: + """Generate command to log to info from an event. + + Args: + info: information to log; cannot include `'` or `"` + + Returns: + actionList action for simBase.createNewEvent + """ + return self._satellite_command + f".log_info('{info}')" + + def log_info(self, info: Any) -> None: + """Record information at the current simulation time. + + Args: + info: Information to log + """ + self.info.append((self.simulator.sim_time, info)) + self.logger.info(f"{info}") + + def update_timed_terminal_event( + self, t_close: float, info: str = "", extra_actions: list[str] = [] + ) -> None: + """Create a simulator event that stops the simulation a certain time. + + Args: + t_close: Termination time [s] + info: Additional identifying info to log at terminal time + extra_actions: Additional actions to perform at terminal time + """ + self.disable_timed_terminal_event() + self.log_info(f"setting timed terminal event at {t_close:.1f}") + + # Create new timed terminal event + self._timed_terminal_event_name = valid_func_name( + f"timed_terminal_{t_close}_{self.id}" + ) + self.simulator.createNewEvent( + self._timed_terminal_event_name, + macros.sec2nano(self.simulator.sim_rate), + True, + [f"self.TotalSim.CurrentNanos * {macros.NANO2SEC} >= {t_close}"], + [ + self._info_command(f"timed termination at {t_close:.1f} " + info), + self._satellite_command + ".requires_retasking = True", + ] + + extra_actions, + terminal=self.variable_interval, + ) + self.simulator.eventMap[self._timed_terminal_event_name].eventActive = True + + def disable_timed_terminal_event(self) -> None: + """Turn off simulator termination due to :class:`update_timed_terminal_event`.""" + if ( + self._timed_terminal_event_name is not None + and self._timed_terminal_event_name in self.simulator.eventMap + ): + self.simulator.delete_event(self._timed_terminal_event_name) diff --git a/src/bsk_rl/scene/__init__.py b/src/bsk_rl/scene/__init__.py new file mode 100644 index 00000000..47bb17d2 --- /dev/null +++ b/src/bsk_rl/scene/__init__.py @@ -0,0 +1,20 @@ +"""``bsk_rl.scene`` provides scenarios, or the underlying environment in which the satellite can collect data. + +Scenarios typically correspond to certain type(s) of :ref:`bsk_rl.data` systems. The +following scenarios have been implemented: + +* :class:`UniformTargets`: Uniformly distributed targets to be imaged by an :class:`~bsk_rl.sats.ImagingSatellite`. +* :class:`CityTargets`: Targets distributed near population centers. +* :class:`UniformNadirScanning`: Uniformly desireable data over the surface of the Earth. +""" + +from bsk_rl.scene.scenario import Scenario, UniformNadirScanning +from bsk_rl.scene.targets import CityTargets, UniformTargets + +__doc_title__ = "Scenario" +__all__ = [ + "Scenario", + "UniformTargets", + "CityTargets", + "UniformNadirScanning", +] diff --git a/src/bsk_rl/scene/scenario.py b/src/bsk_rl/scene/scenario.py new file mode 100644 index 00000000..38afff48 --- /dev/null +++ b/src/bsk_rl/scene/scenario.py @@ -0,0 +1,40 @@ +"""Scenarios define data available for satellites to collect.""" + +import logging +from abc import ABC +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.data.base import Data + from bsk_rl.sats import Satellite + +logger = logging.getLogger(__name__) + + +class Scenario(ABC): + """Base scenario class.""" + + def reset_pre_sim(self) -> None: # pragma: no cover + """Reset the scenario before initializing the simulator.""" + pass + + def initial_data(self, satellite: "Satellite", data_type: type["Data"]) -> "Data": + """Furnish the :class:`~bsk_rl.data.base.DataStore` with initial data.""" + return data_type() + + +class UniformNadirScanning(Scenario): + """Defines a nadir target center at the center of the planet.""" + + def __init__(self, value_per_second: float = 1.0) -> None: + """Construct uniform data over the surface of the planet. + + Can be used with :class:`~bsk_rl.data.ScanningTimeReward`. + + Args: + value_per_second: Reward per second for imaging nadir. + """ + self.value_per_second = value_per_second + + +__all__ = [] diff --git a/src/bsk_rl/env/scenario/environment_features.py b/src/bsk_rl/scene/targets.py similarity index 54% rename from src/bsk_rl/env/scenario/environment_features.py rename to src/bsk_rl/scene/targets.py index 477d7011..1a61f79d 100644 --- a/src/bsk_rl/env/scenario/environment_features.py +++ b/src/bsk_rl/scene/targets.py @@ -1,49 +1,47 @@ -"""Environment features define data available for satellites to collect.""" +"""Target scenarios distribute ground targets with some distribution. + +Currently, targets are all known to the satellites a priori and are available based on +the imaging requirements given by the dynamics and flight software models. +""" import logging import os import sys -from abc import ABC from pathlib import Path -from typing import Callable, Iterable, Optional, Union +from typing import TYPE_CHECKING, Callable, Iterable, Optional, Union import numpy as np import pandas as pd from Basilisk.utilities import orbitalMotion -logger = logging.getLogger(__name__) - +from bsk_rl.scene import Scenario +from bsk_rl.utils.orbital import lla2ecef -class EnvironmentFeatures(ABC): - """Base environment feature class.""" +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.data.base import Data + from bsk_rl.sats import Satellite - def reset(self) -> None: # pragma: no cover - """Reset environment features.""" - pass +logger = logging.getLogger(__name__) class Target: """Ground target with associated value.""" - def __init__(self, name: str, location: Iterable[float], priority: float) -> None: - """Construct a Target. + def __init__(self, name: str, r_LP_P: Iterable[float], priority: float) -> None: + """Ground target with associated priority and location. Args: name: Identifier; does not need to be unique - location: PCPF location [m] + r_LP_P: Planet-fixed, planet relative location [m] priority: Value metric. """ self.name = name - self.location = np.array(location) + self.r_LP_P = np.array(r_LP_P) self.priority = priority @property def id(self) -> str: - """Get unique human-readable identifier. - - Returns: - Unique human-readable identifier. - """ + """Get unique, human-readable identifier.""" try: return self._id except AttributeError: @@ -57,7 +55,7 @@ def __hash__(self) -> int: def __repr__(self) -> str: """Get string representation of target. - Use target.id for a unique string identifier. + Use ``target.id`` for a unique string identifier. Returns: Target string @@ -65,7 +63,7 @@ def __repr__(self) -> str: return f"Target({self.name})" -class StaticTargets(EnvironmentFeatures): +class UniformTargets(Scenario): """Environment with targets distributed uniformly.""" def __init__( @@ -74,12 +72,18 @@ def __init__( priority_distribution: Optional[Callable] = None, radius: float = orbitalMotion.REQ_EARTH * 1e3, ) -> None: - """Construct an environment with evenly-distributed static targets. + """An environment with evenly-distributed static targets. + + Can be used with :class:`~bsk_rl.data.UniqueImageReward`. Args: - n_targets: Number (or range) of targets to generate - priority_distribution: Function for generating target priority. - radius: Radius to place targets from body center. + n_targets: Number of targets to generate. Can also be specified as a range + ``(low, high)`` where the number of targets generated is uniformly selected + ``low ≤ n_targets ≤ high``. + priority_distribution: Function for generating target priority. Defaults + to ``lambda: uniform(0, 1)`` if not specified. + radius: [m] Radius to place targets from body center. Defaults to Earth's + equatorial radius. """ self._n_targets = n_targets if priority_distribution is None: @@ -88,7 +92,7 @@ def __init__( self.radius = radius self.targets = [] - def reset(self) -> None: + def reset_pre_sim(self) -> None: """Regenerate target set for new episode.""" if isinstance(self._n_targets, int): self.n_targets = self._n_targets @@ -98,65 +102,71 @@ def reset(self) -> None: self.regenerate_targets() def regenerate_targets(self) -> None: - """Regenerate targets uniformly.""" + """Regenerate targets uniformly. + + Override this method (ash demonstrated in :class:`CityTargets`) to generate + other distributions. + """ self.targets = [] for i in range(self.n_targets): x = np.random.normal(size=3) x *= self.radius / np.linalg.norm(x) self.targets.append( - Target( - name=f"tgt-{i}", location=x, priority=self.priority_distribution() - ) + Target(name=f"tgt-{i}", r_LP_P=x, priority=self.priority_distribution()) ) + def initial_data(self, satellite: "Satellite", data_type: type["Data"]) -> "Data": + """Furnish data to the scenario. -def lla2ecef(lat: float, long: float, radius: float): - """Project LLA to Earth Centered, Earth Fixed location. - - Args: - lat: [deg] - long: [deg] - radius: [any] - """ - lat = np.radians(lat) - long = np.radians(long) - return radius * np.array( - [np.cos(lat) * np.cos(long), np.cos(lat) * np.sin(long), np.sin(lat)] - ) + Currently, it is assumed that all targets are known a priori, so the initial data + given to the data store is the list of all targets. + """ + try: + return data_type(known=self.targets) + except TypeError: + return data_type() -class CityTargets(StaticTargets): +class CityTargets(UniformTargets): """Environment with targets distributed around population centers.""" def __init__( self, n_targets: Union[int, tuple[int, int]], - n_select_from: int = sys.maxsize, + n_select_from: Optional[int] = None, location_offset: float = 0, priority_distribution: Optional[Callable] = None, radius: float = orbitalMotion.REQ_EARTH * 1e3, ) -> None: - """Construct environment with of static targets around population centers. + """Construct environment with static targets around population centers. + + Uses the `simplemaps Word Cities Database `_ + for population center locations. This data is installed by ``finish_install``. Args: - n_targets: Number of targets to generate - n_select_from: Generate targets from the top n most populous. - location_offset: Offset targets randomly from the city center [m]. + n_targets: Number of targets to generate, as a fixed number or a range. + n_select_from: Generate targets from the top `n_select_from` most populous + cities. Will use all cities in the database if not specified. + location_offset: [m] Offset targets randomly from the city center by up to + this amount. priority_distribution: Function for generating target priority. radius: Radius to place targets from body center. """ super().__init__(n_targets, priority_distribution, radius) - if n_select_from == "all": + if n_select_from == "all" or n_select_from is None: n_select_from = sys.maxsize self.n_select_from = n_select_from self.location_offset = location_offset def regenerate_targets(self) -> None: - """Regenerate targets based on cities.""" + """Regenerate targets based on cities. + + :meta private: + """ self.targets = [] cities = pd.read_csv( - Path(os.path.realpath(__file__)).parent.parent.parent - / "data" + Path(os.path.realpath(__file__)).parent.parent + / "_dat" / "simplemaps_worldcities" / "worldcities.csv", ) @@ -176,20 +186,11 @@ def regenerate_targets(self) -> None: self.targets.append( Target( name=city["city"].replace("'", ""), - location=location, + r_LP_P=location, priority=self.priority_distribution(), ) ) -class UniformNadirFeature(EnvironmentFeatures): - """Defines a nadir target center at the center of the planet.""" - - def __init__(self, value_per_second: float = 1.0) -> None: - """Construct uniform data over the surface of the planet. - - Args: - value_per_second: Amount of reward per second imaging nadir. - """ - self.name = "NadirFeature" - self.value_per_second = value_per_second +__doc_title__ = "Target Scenarios" +__all__ = ["Target", "UniformTargets", "CityTargets"] diff --git a/src/bsk_rl/sim/__init__.py b/src/bsk_rl/sim/__init__.py new file mode 100644 index 00000000..f35f531d --- /dev/null +++ b/src/bsk_rl/sim/__init__.py @@ -0,0 +1,35 @@ +"""``bsk_rl.sim`` is a package for the underlying Basilisk simulation. + +The simulation is divided into three categories of Basilisk models: + +* :ref:`bsk_rl.sim.world`, capturing elements of the simulation environment common to + all satellites. This includes things such as gravity and atmosphere models, the epoch, + and ground station locations. While the world model can be specified in the :class:`~bsk_rl.GeneralSatelliteTasking` + constructor, it is generally automatically inferred from the satellite requirements. +* :ref:`bsk_rl.sim.dyn`, representing the dynamics model for each satellite. This is + specified on a per-satellite basis by the :class:`~bsk_rl.Satellite` type definition. + The dynamics model captures the properties of the satellite, such as physical configurations, + actuators models, instrument models, the power system, and storage resources. +* :ref:`bsk_rl.sim.fsw`, representing the flight software models for each satellite. As + with flight software, this specified by the :class:`~bsk_rl.Satellite`. The flight + software model represents the low-level algorithms used for actuator and instrument + control. + +Generally, this can be thought of as a hierarchy of models, with dynamics models acting +in the world model, and flight software models controlling the dynamics models, and +other parts of ``bsk_rl`` controlling the flight software models. This hierarchy +contributes to the realism of the simulation, as the satellite is being controlled +through its flight software. + +The :class:`~bsk_rl.Simulator` is the main class for the simulation environment, +subclassing from the `Basilisk SimBaseClass `_. +At each environment reset, the simulator and the associated flight software, dynamics, +and world models are deleted and reconstructed, generating a fresh Basilisk simulation. +""" + +from bsk_rl.sim.simulator import Simulator + +__doc_title__ = "Simulation (BSK)" +__all__ = [ + "Simulator", +] diff --git a/src/bsk_rl/env/simulation/dynamics.py b/src/bsk_rl/sim/dyn.py similarity index 57% rename from src/bsk_rl/env/simulation/dynamics.py rename to src/bsk_rl/sim/dyn.py index d1df327b..f17e3e31 100644 --- a/src/bsk_rl/env/simulation/dynamics.py +++ b/src/bsk_rl/sim/dyn.py @@ -1,15 +1,44 @@ -"""Basilisk dynamics models.""" +"""Basilisk dynamics models are given in ``bsk_rl.sim.dyn``. + +The dynamics model is the core of the satellite simulation, representing the physical +properties of the satellite and its interactions with the environment. The dynamics model +can be customized to represent different satellite configurations, actuator models, and +instrument models. + +The dynamics model is selected using the ``dyn_type`` class property of the +:class:`~bsk_rl.sats.Satellite`. Certain environment elements may require specific +dynamics models, such as :class:`~bsk_rl.comm.LOSCommunication` requiring a dynamics +model that inherits from :class:`~bsk_rl.sim.dyn.LOSCommDynModel` or :class:`~bsk_rl.sats.ImagingSatellite` +requiring a dynamics model that inherits from :class:`~bsk_rl.sim.dyn.ImagingDynModel`. + +Setting Parameters +------------------ + +Customization of the dynamics model parameters is achieved through the ``sat_args`` +dictionary passed to the :class:`~bsk_rl.sats.Satellite` constructor. This dictionary is +passed on to the dynamics model setup functions, which are called each time the simulator +is reset. + +Properties +---------- + +The dynamics model provides a number of properties for easy access to the satellite state. +These can be accessed directly from the dynamics model instance, or in the observation +via the :class:`~bsk_rl.obs.SatProperties` observation. + + +Aliveness Checking +------------------ + +Certain functions in the dynamics model are decorated with the :func:`~bsk_rl.utils.functional.aliveness_checker` +decorator. These functions are called at each step to check if the satellite is still +operational, returning true if the satellite is still alive. + +""" from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Iterable, Optional -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import ( - EnvironmentModel, - Satellite, - Simulator, - ) - import numpy as np from Basilisk.simulation import ( ReactionWheelPower, @@ -34,7 +63,7 @@ unitTestSupport, ) -from bsk_rl.env.simulation import environment +from bsk_rl.sim import world from bsk_rl.utils import actuator_primitives as aP from bsk_rl.utils.attitude import random_tumble from bsk_rl.utils.functional import ( @@ -44,17 +73,18 @@ ) from bsk_rl.utils.orbital import random_orbit +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.sim import Simulator + from bsk_rl.sim.world import WorldModel + class DynamicsModel(ABC): - """Abstract Basilisk dynamics model. - - One DynamicsModel is instantiated for each satellite in the environment each time a - new simulator is created. - """ + """Abstract Basilisk dynamics model.""" @classmethod - def _requires_env(cls) -> list[type["EnvironmentModel"]]: - """Define minimum EnvironmentModels for compatibility.""" + def _requires_world(cls) -> list[type["WorldModel"]]: + """Define minimum :class:`~bsk_rl.sim.world.WorldModel` for compatibility.""" return [] def __init__( @@ -64,21 +94,24 @@ def __init__( priority: int = 200, **kwargs, ) -> None: - """Construct a base dynamics model. + """The abstract base dynamics model. + + One DynamicsModel is instantiated for each satellite in the environment each + time the environment is reset and new simulator is created. Args: - satellite: Satellite modelled by this model - dyn_rate: Rate of dynamics simulation [s] + satellite: Satellite represented by this model. + dyn_rate: [s] Rate of dynamics simulation. priority: Model priority. - kwargs: Ignored + kwargs: Passed through to setup functions. """ self.satellite = satellite self.logger = self.satellite.logger.getChild(self.__class__.__name__) - for required in self._requires_env(): - if not issubclass(type(self.simulator.environment), required): + for required in self._requires_world(): + if not issubclass(type(self.simulator.world), required): raise TypeError( - f"{self.simulator.environment} must be a subclass of {required} to " + f"{self.simulator.world} must be a subclass of {required} to " + f"use dynamics model of type {self.__class__}" ) @@ -92,7 +125,7 @@ def __init__( # Initialize all modules and write init one-time messages self.scObject: spacecraft.Spacecraft - self._init_dynamics_objects(**kwargs) + self._setup_dynamics_objects(**kwargs) @property def simulator(self) -> "Simulator": @@ -100,12 +133,12 @@ def simulator(self) -> "Simulator": return self.satellite.simulator @property - def environment(self) -> "EnvironmentModel": - """Reference to the episode environment model.""" - return self.simulator.environment + def world(self) -> "WorldModel": + """Reference to the episode world model.""" + return self.simulator.world @abstractmethod # pragma: no cover - def _init_dynamics_objects(self, **kwargs) -> None: + def _setup_dynamics_objects(self, **kwargs) -> None: """Caller for all dynamics object initialization.""" pass @@ -113,12 +146,12 @@ def is_alive(self, log_failure=False) -> bool: """Check if the dynamics model has failed any aliveness requirements. Returns: - If the satellite dynamics are still alive + ``True`` if the satellite dynamics are still alive. """ return check_aliveness_checkers(self, log_failure=log_failure) def reset_for_action(self) -> None: - """Reset whenever a FSW @action is called.""" + """Reset whenever a flight software :class:`~bsk_rl.sim.fsw.action` is called.""" pass def __del__(self): @@ -130,8 +163,28 @@ class BasicDynamicsModel(DynamicsModel): """Basic Dynamics model with minimum necessary Basilisk components.""" @classmethod - def _requires_env(cls) -> list[type["EnvironmentModel"]]: - return [environment.BasicEnvironmentModel] + def _requires_world(cls) -> list[type["WorldModel"]]: + return [world.BasicWorldModel] + + def __init__(self, *args, **kwargs) -> None: + """A dynamics model with a basic feature set. + + Includes the following: + + * Spacecraft hub physical properties + * Gravity + * Constant disturbance torque (defaults to none) + * Aerodynamic drag + * Eclipse checking for power generation + * Reaction wheels + * Momentum desaturation thrusters + * Solar panels, battery, and power system + + Args: + *args: Passed to superclass + **kwargs: Passed to superclass + """ + super().__init__(*args, **kwargs) @property def sigma_BN(self): @@ -151,7 +204,7 @@ def omega_BN_B(self): @property def BP(self): """Body relative to planet freame rotation matrix.""" - return np.matmul(self.BN, self.environment.PN.T) + return np.matmul(self.BN, self.world.PN.T) @property def r_BN_N(self): @@ -161,7 +214,7 @@ def r_BN_N(self): @property def r_BN_P(self): """Body position relative to inertial origin in planet frame [m].""" - return np.matmul(self.environment.PN, self.r_BN_N) + return np.matmul(self.world.PN, self.r_BN_N) @property def v_BN_N(self): @@ -170,18 +223,16 @@ def v_BN_N(self): @property def v_BN_P(self): - """P-frame derivative of r_BN.""" - omega_NP_P = np.matmul(self.environment.PN, -self.environment.omega_PN_N) - return np.matmul(self.environment.PN, self.v_BN_N) + np.cross( - omega_NP_P, self.r_BN_P - ) + """Planet-frame derivative of ``r_BN``.""" + omega_NP_P = np.matmul(self.world.PN, -self.world.omega_PN_N) + return np.matmul(self.world.PN, self.v_BN_N) + np.cross(omega_NP_P, self.r_BN_P) @property def omega_BP_P(self): """Body angular velocity relative to planet frame in plant frame [rad/s].""" omega_BN_N = np.matmul(self.BN.T, self.omega_BN_B) - omega_BP_N = omega_BN_N - self.environment.omega_PN_N - return np.matmul(self.environment.PN, omega_BP_N) + omega_BP_N = omega_BN_N - self.world.omega_PN_N + return np.matmul(self.world.PN, omega_BP_N) @property def battery_charge(self): @@ -196,24 +247,25 @@ def battery_charge_fraction(self): @property def wheel_speeds(self): """Wheel speeds [rad/s].""" - return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds) + return np.array(self.rwStateEffector.rwSpeedOutMsg.read().wheelSpeeds)[0:3] @property def wheel_speeds_fraction(self): - """Wheel speeds normalized by maximum.""" + """Wheel speeds normalized by maximum allowable speed.""" return self.wheel_speeds / (self.maxWheelSpeed * macros.rpm2radsec) - def _init_dynamics_objects(self, **kwargs) -> None: - self._set_spacecraft_hub(**kwargs) - self._set_drag_effector(**kwargs) - self._set_reaction_wheel_dyn_effector(**kwargs) - self._set_thruster_dyn_effector() - self._set_simple_nav_object() - self._set_eclipse_object() - self._set_solar_panel(**kwargs) - self._set_battery(**kwargs) - self._set_reaction_wheel_power(**kwargs) - self._set_thruster_power(**kwargs) + def _setup_dynamics_objects(self, **kwargs) -> None: + self.setup_spacecraft_hub(**kwargs) + self.setup_drag_effector(**kwargs) + self.setup_reaction_wheel_dyn_effector(**kwargs) + self.setup_thruster_dyn_effector() + self.setup_simple_nav_object() + self.setup_eclipse_object() + self.setup_solar_panel(**kwargs) + self.setup_battery(**kwargs) + self.setup_power_sink(**kwargs) + self.setup_reaction_wheel_power(**kwargs) + self.setup_thruster_power(**kwargs) @default_args( mass=330, @@ -227,7 +279,7 @@ def _init_dynamics_objects(self, **kwargs) -> None: oe=random_orbit, mu=orbitalMotion.MU_EARTH * 1e9, ) - def _set_spacecraft_hub( + def setup_spacecraft_hub( self, mass: float, width: float, @@ -242,21 +294,27 @@ def _set_spacecraft_hub( priority: int = 2000, **kwargs, ) -> None: - """Set the spacecraft object properties. + """Set up the spacecraft hub physical properties and state. + + The hub is assumed to be a uniform-density rectangular prism with the center of + mass at the center. Args: - mass: Hub mass [kg] - width: Hub width [m] - depth: Hub depth [m] - height: Hub height [m] - sigma_init: Initial MRP - omega_init: Initial body rate [rad/s] - oe: (a, e, i, AN, AP, f); alternative to rN, vN [km, rad] - rN: Initial inertial position [m] - vN: Initial inertial velocity [m/s] - mu: Gravitational parameter (used only with oe) + mass: [kg] Hub mass. + width: [m] Hub width. + depth: [m] Hub depth. + height: [m] Hub height. + sigma_init: Initial attitude MRP. + omega_init: [rad/s] Initial body rate. + oe: Orbital element tuple of (semimajor axis [km], eccentricity, inclination + [rad], ascending node [rad], argument of periapsis [rad], initial true + anomaly [rad]). Either ``oe`` and ``mu`` or ``rN`` and ``vN`` must be + provided, but not both. + mu: Gravitational parameter (used only with ``oe``). + rN: [m] Initial inertial position. + vN: [m/s] Initial inertial velocity. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ if rN is not None and vN is not None and oe is None: pass @@ -286,25 +344,25 @@ def _set_spacecraft_hub( self.task_name, self.scObject, ModelPriority=priority ) - self._set_gravity_bodies() - self._set_disturbance_torque(**kwargs) - self._set_density_model() + self.setup_gravity_bodies() + self.setup_disturbance_torque(**kwargs) + self.setup_density_model() - def _set_gravity_bodies(self) -> None: - """Specify what gravitational bodies to include in the simulation.""" + def setup_gravity_bodies(self) -> None: + """Set up gravitational bodies from the :class:`~bsk_rl.sim.world.WorldModel` to included in the simulation.""" self.scObject.gravField.gravBodies = spacecraft.GravBodyVector( - list(self.environment.gravFactory.gravBodies.values()) + list(self.world.gravFactory.gravBodies.values()) ) @default_args(disturbance_vector=None) - def _set_disturbance_torque( + def setup_disturbance_torque( self, disturbance_vector: Optional[Iterable[float]] = None, **kwargs ) -> None: - """Attach the disturbance torque to the satellite. + """Set up a constant disturbance torque acting on the satellite. Args: - disturbance_vector: Constant disturbance torque [N*m]. - kwargs: Ignored + disturbance_vector: [N*m] Constant disturbance torque in the body frame. + kwargs: Passed to other setup functions. """ if disturbance_vector is None: disturbance_vector = np.array([0, 0, 0]) @@ -313,53 +371,72 @@ def _set_disturbance_torque( self.extForceTorqueObject.extTorquePntB_B = disturbance_vector self.scObject.addDynamicEffector(self.extForceTorqueObject) - def _set_density_model(self) -> None: - """Attaches the density model effector to the satellite.""" - self.environment.densityModel.addSpacecraftToModel(self.scObject.scStateOutMsg) + def setup_density_model(self) -> None: + """Set up the atmospheric density model effector.""" + self.world.densityModel.addSpacecraftToModel(self.scObject.scStateOutMsg) - def _set_drag_effector( + @default_args(dragCoeff=2.2) + def setup_drag_effector( self, width: float, depth: float, height: float, panelArea: float, + dragCoeff: float, priority: int = 999, **kwargs, ) -> None: - """Attach the drag effector to the satellite. + """Set up the satellite drag effector. + + The drag effector causes aerodynamic forces and torques to act on the satellite. + For purposes of this model, the satellite is assumed to be a rectangular prism + with a solar panel on one end. Args: - width: Hub width [m] - depth: Hub depth [m] - height: Hub height [m] - panelArea: Solar panel surface area [m**2] + width: [m] Hub width. + depth: [m] Hub depth. + height: [m] Hub height. + panelArea: [m^2] Solar panel surface area. + dragCoeff: Drag coefficient. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.dragEffector = facetDragDynamicEffector.FacetDragDynamicEffector() self.dragEffector.ModelTag = "FacetDrag" # Set up the geometry of a small satellite, starting w/ bus - self.dragEffector.addFacet(width * depth, 2.2, [1, 0, 0], [height / 2, 0.0, 0]) - self.dragEffector.addFacet(width * depth, 2.2, [-1, 0, 0], [height / 2, 0.0, 0]) - self.dragEffector.addFacet(height * width, 2.2, [0, 1, 0], [0, depth / 2, 0]) - self.dragEffector.addFacet(height * width, 2.2, [0, -1, 0], [0, -depth / 2, 0]) - self.dragEffector.addFacet(height * depth, 2.2, [0, 0, 1], [0, 0, width / 2]) - self.dragEffector.addFacet(height * depth, 2.2, [0, 0, -1], [0, 0, -width / 2]) + self.dragEffector.addFacet( + width * depth, dragCoeff, [1, 0, 0], [height / 2, 0.0, 0] + ) + self.dragEffector.addFacet( + width * depth, dragCoeff, [-1, 0, 0], [height / 2, 0.0, 0] + ) + self.dragEffector.addFacet( + height * width, dragCoeff, [0, 1, 0], [0, depth / 2, 0] + ) + self.dragEffector.addFacet( + height * width, dragCoeff, [0, -1, 0], [0, -depth / 2, 0] + ) + self.dragEffector.addFacet( + height * depth, dragCoeff, [0, 0, 1], [0, 0, width / 2] + ) + self.dragEffector.addFacet( + height * depth, dragCoeff, [0, 0, -1], [0, 0, -width / 2] + ) # Add solar panels self.dragEffector.addFacet( panelArea / 2, - 2.2, + dragCoeff, [0, 1, 0], [0, height, 0], ) self.dragEffector.addFacet( panelArea / 2, - 2.2, + dragCoeff, [0, -1, 0], [0, height, 0], ) self.dragEffector.atmoDensInMsg.subscribeTo( - self.environment.densityModel.envOutMsgs[-1] + self.world.densityModel.envOutMsgs[-1] ) self.scObject.addDynamicEffector(self.dragEffector) @@ -367,12 +444,12 @@ def _set_drag_effector( self.task_name, self.dragEffector, ModelPriority=priority ) - def _set_simple_nav_object(self, priority: int = 1400, **kwargs) -> None: - """Make the navigation module. + def setup_simple_nav_object(self, priority: int = 1400, **kwargs) -> None: + """Set up the navigation module. Args: priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.simpleNavObject = simpleNav.SimpleNav() self.simpleNavObject.ModelTag = "SimpleNav" @@ -394,7 +471,7 @@ def altitude_valid(self) -> bool: maxWheelSpeed=np.inf, u_max=0.200, ) - def _set_reaction_wheel_dyn_effector( + def setup_reaction_wheel_dyn_effector( self, wheelSpeeds: Iterable[float], maxWheelSpeed: float, @@ -402,14 +479,16 @@ def _set_reaction_wheel_dyn_effector( priority: int = 997, **kwargs, ) -> None: - """Set the RW state effector parameters. + """Set the reaction wheel state effector parameters. + + Three reaction wheels modeled on the HR16 wheel are used. Args: - wheelSpeeds: Initial speeds of each wheel [RPM] - maxWheelSpeed: Failure speed for wheels [RPM] - u_max: Torque producible by wheel [N*m] + wheelSpeeds: [rpm] Initial speeds of each wheel. + maxWheelSpeed: [rpm] Failure speed for wheels. + u_max: [N*m] Maximum torque producible by each wheel. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.maxWheelSpeed = maxWheelSpeed self.rwStateEffector, self.rwFactory, _ = aP.balancedHR16Triad( @@ -427,15 +506,15 @@ def _set_reaction_wheel_dyn_effector( @aliveness_checker def rw_speeds_valid(self) -> bool: - """Check if any wheel speed exceeds the maximum.""" + """Check if any wheel speed exceeds the ``maxWheelSpeed``.""" valid = all( abs(speed) < self.maxWheelSpeed * macros.rpm2radsec for speed in self.wheel_speeds ) return valid - def _set_thruster_dyn_effector(self, priority: int = 996) -> None: - """Make the thruster state effector. + def setup_thruster_dyn_effector(self, priority: int = 996) -> None: + """Set up the thruster state effector. Args: priority: Model priority. @@ -448,15 +527,17 @@ def _set_thruster_dyn_effector(self, priority: int = 996) -> None: ) @default_args(thrusterPowerDraw=0.0) - def _set_thruster_power( + def setup_thruster_power( self, thrusterPowerDraw, priority: int = 899, **kwargs ) -> None: - """Set the thruster power draw. + """Set up the thruster power draw. + + When momentum desaturating using wheels, power is consumed at this rate. Args: - thrusterPowerDraw: Constant power draw desat mode is active. [W] + thrusterPowerDraw: [W] Constant power draw desat mode is active. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.thrusterPowerSink = simplePowerSink.SimplePowerSink() self.thrusterPowerSink.ModelTag = "thrustPowerSink" + self.satellite.id @@ -466,17 +547,17 @@ def _set_thruster_power( ) self.powerMonitor.addPowerNodeToModel(self.thrusterPowerSink.nodePowerOutMsg) - def _set_eclipse_object(self) -> None: + def setup_eclipse_object(self) -> None: """Add the spacecraft to the eclipse module.""" - self.environment.eclipseObject.addSpacecraftToModel(self.scObject.scStateOutMsg) - self.eclipse_index = len(self.environment.eclipseObject.eclipseOutMsgs) - 1 + self.world.eclipseObject.addSpacecraftToModel(self.scObject.scStateOutMsg) + self.eclipse_index = len(self.world.eclipseObject.eclipseOutMsgs) - 1 @default_args( panelArea=2 * 1.0 * 0.5, panelEfficiency=0.20, nHat_B=np.array([0, 1, 0]), ) - def _set_solar_panel( + def setup_solar_panel( self, panelArea: float, panelEfficiency: float, @@ -486,24 +567,25 @@ def _set_solar_panel( ) -> None: """Set the solar panel parameters for power generation. + Power generation takes into account panel size and efficiency, the eclipse + state, and the angle of solar incidence. + Args: - panelArea: Solar panel surface area [m**2] + panelArea: [m^2] Solar panel area. panelEfficiency: Efficiency coefficient of solar to electrical power - conversion - nHat_B: Body-fixed array normal vector + conversion. + nHat_B: Body-fixed array normal vector. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.solarPanel = simpleSolarPanel.SimpleSolarPanel() self.solarPanel.ModelTag = "solarPanel" + self.satellite.id self.solarPanel.stateInMsg.subscribeTo(self.scObject.scStateOutMsg) self.solarPanel.sunEclipseInMsg.subscribeTo( - self.environment.eclipseObject.eclipseOutMsgs[self.eclipse_index] + self.world.eclipseObject.eclipseOutMsgs[self.eclipse_index] ) self.solarPanel.sunInMsg.subscribeTo( - self.environment.gravFactory.spiceObject.planetStateOutMsgs[ - self.environment.sun_index - ] + self.world.gravFactory.spiceObject.planetStateOutMsgs[self.world.sun_index] ) self.solarPanel.setPanelParameters( unitTestSupport.np2EigenVectorXd(nHat_B), @@ -518,7 +600,7 @@ def _set_solar_panel( batteryStorageCapacity=80.0 * 3600.0, storedCharge_Init=lambda: np.random.uniform(30.0 * 3600.0, 70.0 * 3600.0), ) - def _set_battery( + def setup_battery( self, batteryStorageCapacity: float, storedCharge_Init: float, @@ -528,10 +610,10 @@ def _set_battery( """Set the battery model parameters. Args: - batteryStorageCapacity: Maximum battery charge [W*s] - storedCharge_Init: Initial battery charge [W*s] + batteryStorageCapacity: [W*s] Maximum battery charge. + storedCharge_Init: [W*s] Initial battery charge. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.powerMonitor = simpleBattery.SimpleBattery() self.powerMonitor.ModelTag = "powerMonitor" @@ -544,13 +626,40 @@ def _set_battery( @aliveness_checker def battery_valid(self) -> bool: - """Check if the battery has charge remaining.""" + """Check if the battery has charge remaining. + + Note that this check is instantaneous. If a satellite runs out of power during a + environment step but then recharges to have positive power at the end of the step, + the satellite will still be considered alive. + """ return self.battery_charge > 0 + @default_args(basePowerDraw=0.0) + def setup_power_sink( + self, basePowerDraw: float, priority: int = 897, **kwargs + ) -> None: + """Set the instrument power sink parameters. + + Args: + basePowerDraw: [W] Baseline satellite power draw. Should be negative. + priority: Model priority. + kwargs: Passed to other setup functions. + """ + if basePowerDraw > 0: + self.logger.warning("basePowerDraw should probably be zero or negative.") + self.basePowerSink = simplePowerSink.SimplePowerSink() + self.basePowerSink.ModelTag = "basePowerSink" + self.satellite.id + self.basePowerSink.nodePowerOut = basePowerDraw # Watts + self.simulator.AddModelToTask( + self.task_name, self.basePowerSink, ModelPriority=priority + ) + self.powerMonitor.addPowerNodeToModel(self.basePowerSink.nodePowerOutMsg) + self.basePowerSink.powerStatus = 1 + @default_args( rwBasePower=0.4, rwMechToElecEfficiency=0.0, rwElecToMechEfficiency=0.5 ) - def _set_reaction_wheel_power( + def setup_reaction_wheel_power( self, rwBasePower: float, rwMechToElecEfficiency: float, @@ -561,19 +670,19 @@ def _set_reaction_wheel_power( """Set the reaction wheel power draw. Args: - rwBasePower: Constant power draw when operational [W] + rwBasePower: [W] Constant power draw when operational. rwMechToElecEfficiency: Efficiency factor to convert mechanical power to - electrical power + electrical power. rwElecToMechEfficiency: Efficiency factor to convert electrical power to - mechanical power + mechanical power. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.rwPowerList = [] for i_device in range(self.rwFactory.getNumOfDevices()): powerRW = ReactionWheelPower.ReactionWheelPower() powerRW.ModelTag = "rwPower" + str(i_device) - powerRW.basePowerNeed = rwBasePower # baseline power draw, Watts + powerRW.basePowerNeed = rwBasePower powerRW.rwStateInMsg.subscribeTo(self.rwStateEffector.rwOutMsgs[i_device]) powerRW.mechToElecEfficiency = rwMechToElecEfficiency powerRW.elecToMechEfficiency = rwElecToMechEfficiency @@ -587,28 +696,37 @@ def _set_reaction_wheel_power( class LOSCommDynModel(BasicDynamicsModel): """For evaluating line-of-sight connections between satellites for communication.""" - def _init_dynamics_objects(self, **kwargs) -> None: - super()._init_dynamics_objects(**kwargs) - self._set_los_comms(**kwargs) + def __init__(self, *args, **kwargs) -> None: + """Allow for line-of-sight checking between satellites. + + Necessary for :class:`~bsk_rl.comm.LOSCommunication` to function. + """ + super().__init__(*args, **kwargs) + + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_los_comms(**kwargs) - def _set_los_comms(self, priority: int = 500, **kwargs) -> None: + @default_args(losMaximumRange=-1.0) + def setup_los_comms( + self, losMaximumRange: float, priority: int = 500, **kwargs + ) -> None: """Set up line-of-sight visibility checking between satellites. Args: + losMaximumRange: [m] Maximum range for line-of-sight visibility. -1 for unlimited. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.losComms = spacecraftLocation.SpacecraftLocation() self.losComms.ModelTag = "losComms" self.losComms.primaryScStateInMsg.subscribeTo(self.scObject.scStateOutMsg) self.losComms.planetInMsg.subscribeTo( - self.environment.gravFactory.spiceObject.planetStateOutMsgs[ - self.environment.body_index - ] + self.world.gravFactory.spiceObject.planetStateOutMsgs[self.world.body_index] ) - self.losComms.rEquator = self.simulator.environment.planet.radEquator - self.losComms.rPolar = self.simulator.environment.planet.radEquator * 0.98 - self.losComms.maximumRange = -1.0 # m, unlimited + self.losComms.rEquator = self.simulator.world.planet.radEquator + self.losComms.rPolar = self.simulator.world.planet.radEquator * 0.98 + self.losComms.maximumRange = losMaximumRange self.los_comms_ids = [] @@ -632,6 +750,15 @@ def _set_los_comms(self, priority: int = 500, **kwargs) -> None: class ImagingDynModel(BasicDynamicsModel): """Equips the satellite with an instrument, storage unit, and transmitter.""" + def __init__(self, *args, **kwargs) -> None: + """Equips the satellite with an instrument, storage unit, and transmitter. + + This dynamics model is used with :class:`~bsk_rl.sats.ImagingSatellite`. It + provides the satellite with the ability to take images of a point target. To + enable downlink, use :class:`GroundStationDynModel` and :class:`~bsk_rl.sim.world.GroundStationWorldModel`. + """ + super().__init__(*args, **kwargs) + @property def storage_level(self): """Storage level [bits].""" @@ -642,38 +769,38 @@ def storage_level_fraction(self): """Storage level as a fraction of capacity.""" return self.storage_level / self.storageUnit.storageCapacity - def _init_dynamics_objects(self, **kwargs) -> None: - super()._init_dynamics_objects(**kwargs) - self._set_instrument_power_sink(**kwargs) - self._set_transmitter_power_sink(**kwargs) - self._set_instrument(**kwargs) - self._set_transmitter(**kwargs) - self._set_storage_unit(**kwargs) - self._set_imaging_target(**kwargs) + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_instrument_power_sink(**kwargs) + self.setup_transmitter_power_sink(**kwargs) + self.setup_instrument(**kwargs) + self.setup_transmitter(**kwargs) + self.setup_storage_unit(**kwargs) + self.setup_imaging_target(**kwargs) @default_args(instrumentBaudRate=8e6) - def _set_instrument( + def setup_instrument( self, instrumentBaudRate: float, priority: int = 895, **kwargs ) -> None: - """Create the instrument model. + """Set up the instrument data collection model. Args: - instrumentBaudRate: Data generated in a single step by an image [bits] + instrumentBaudRate: [bits] Data generated by an image. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.instrument = simpleInstrument.SimpleInstrument() self.instrument.ModelTag = "instrument" + self.satellite.id self.instrument.nodeBaudRate = ( instrumentBaudRate / self.dyn_rate - ) # make imaging instantaneous + ) # makes imaging instantaneous self.instrument.nodeDataName = "Instrument" + self.satellite.id self.simulator.AddModelToTask( self.task_name, self.instrument, ModelPriority=priority ) @default_args(transmitterBaudRate=-8e6, transmitterNumBuffers=100) - def _set_transmitter( + def setup_transmitter( self, transmitterBaudRate: float, instrumentBaudRate: float, @@ -681,20 +808,17 @@ def _set_transmitter( priority: int = 798, **kwargs, ) -> None: - """Create the transmitter model. + """Set up the transmitter model for downlinking data. Args: - transmitterBaudRate: Rate of data downlink. Should be negative. [baud] - instrumentBaudRate: Image size, used to set packet size [bits] + transmitterBaudRate: [baud] Rate of data downlink. Should be negative. + instrumentBaudRate: [bits] Image size, used to set packet size. transmitterNumBuffers: Number of transmitter buffers priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ if transmitterBaudRate > 0: - self.logger.warning( - "Positive transmitterBaudRate will lead to increased data in buffer " - + "on downlink" - ) + self.logger.warning("transmitterBaudRate should probably be negative.") self.transmitter = spaceToGroundTransmitter.SpaceToGroundTransmitter() self.transmitter.ModelTag = "transmitter" + self.satellite.id self.transmitter.nodeBaudRate = transmitterBaudRate # baud @@ -706,38 +830,52 @@ def _set_transmitter( ) @default_args(instrumentPowerDraw=-30.0) - def _set_instrument_power_sink( + def setup_instrument_power_sink( self, instrumentPowerDraw: float, priority: int = 897, **kwargs ) -> None: """Set the instrument power sink parameters. + The instrument draws power when in an imaging task, representing the power cost + of operating the instrument. + Args: - instrumentPowerDraw: Power draw when instrument is enabled [W] + instrumentPowerDraw: [W] Power draw when instrument is enabled. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ + if instrumentPowerDraw > 0: + self.logger.warning( + "instrumentPowerDraw should probably be zero or negative." + ) self.instrumentPowerSink = simplePowerSink.SimplePowerSink() self.instrumentPowerSink.ModelTag = "insPowerSink" + self.satellite.id - self.instrumentPowerSink.nodePowerOut = instrumentPowerDraw # Watts + self.instrumentPowerSink.nodePowerOut = instrumentPowerDraw self.simulator.AddModelToTask( self.task_name, self.instrumentPowerSink, ModelPriority=priority ) self.powerMonitor.addPowerNodeToModel(self.instrumentPowerSink.nodePowerOutMsg) @default_args(transmitterPowerDraw=-15.0) - def _set_transmitter_power_sink( + def setup_transmitter_power_sink( self, transmitterPowerDraw: float, priority: int = 896, **kwargs ) -> None: """Set the transmitter power sink parameters. + The transmitter draws power when in a downlink task, representing the power cost + of downlinking data. + Args: - transmitterPowerDraw: Power draw when transmitter is enabled [W] + transmitterPowerDraw: [W] Power draw when transmitter is enabled. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ + if transmitterPowerDraw > 0: + self.logger.warning( + "transmitterPowerDraw should probably be zero or negative." + ) self.transmitterPowerSink = simplePowerSink.SimplePowerSink() self.transmitterPowerSink.ModelTag = "transPowerSink" + self.satellite.id - self.transmitterPowerSink.nodePowerOut = transmitterPowerDraw # Watts + self.transmitterPowerSink.nodePowerOut = transmitterPowerDraw self.simulator.AddModelToTask( self.task_name, self.transmitterPowerSink, ModelPriority=priority ) @@ -746,31 +884,34 @@ def _set_transmitter_power_sink( @default_args( dataStorageCapacity=20 * 8e6, bufferNames=None, - storageUnitValidCheck=True, + storageUnitValidCheck=False, storageInit=0, ) - def _set_storage_unit( + def setup_storage_unit( self, dataStorageCapacity: int, + storageUnitValidCheck: bool, + storageInit: int, transmitterNumBuffers: Optional[int] = None, bufferNames: Optional[Iterable[str]] = None, priority: int = 699, - storageUnitValidCheck: bool = True, - storageInit: int = 0, **kwargs, ) -> None: """Configure the storage unit and its buffers. + Separate buffers can be used to track imaging of different targets. Often, the + buffer names will be set up by satellite based on the scenario configuration. + Args: - dataStorageCapacity: Maximum data to be stored [bits] - transmitterNumBuffers: Number of unit buffers. Not necessary if bufferNames - given. - bufferNames: List of buffer names to use. Named by number if None. + dataStorageCapacity: [bits] Maximum data that can be stored. + transmitterNumBuffers: Number of unit buffers. Not necessary if ``bufferNames`` + are given. + bufferNames: List of buffer names to use. Named by number if ``None``. + storageUnitValidCheck: If ``True``, enforce that the storage level is below + the storage capacity when checking aliveness. + storageInit: [bits] Initial storage level. priority: Model priority. - storageUnitValidCheck: If True, check that the storage level is below the - storage capacity. - storageInit: Initial storage level [bits] - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.storageUnit = partitionedStorageUnit.PartitionedStorageUnit() self.storageUnit.ModelTag = "storageUnit" + self.satellite.id @@ -806,7 +947,12 @@ def _set_storage_unit( @aliveness_checker def data_storage_valid(self) -> bool: - """Check that the buffer has not run out of space.""" + """Check that the buffer has not run out of space. + + Only is checked if ``storageUnitValidCheck`` is ``True``; otherwise, a full storage + unit will prevent additional data from being stored but will not cause the satellite + to be considered dead. + """ storage_check = self.storageUnitValidCheck if storage_check: return self.storage_level < self.storageUnit.storageCapacity or np.isclose( @@ -820,7 +966,7 @@ def data_storage_valid(self) -> bool: imageTargetMinimumElevation=np.radians(45.0), imageTargetMaximumRange=-1, ) - def _set_imaging_target( + def setup_imaging_target( self, groundLocationPlanetRadius: float, imageTargetMinimumElevation: float, @@ -833,36 +979,33 @@ def _set_imaging_target( The target must be updated with a particular location when used. Args: - groundLocationPlanetRadius: Radius of ground locations from center of planet - [m] - imageTargetMinimumElevation: Minimum elevation angle from target to - satellite when imaging [rad] - imageTargetMaximumRange: Maximum range from target to satellite when - imaging. -1 to disable. [m] + groundLocationPlanetRadius: [m] Radius of ground locations from center of planet. + imageTargetMinimumElevation: [rad] Minimum elevation angle from target to + satellite when imaging. + imageTargetMaximumRange: [m] Maximum range from target to satellite when + imaging. -1 to disable. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.imagingTarget = groundLocation.GroundLocation() self.imagingTarget.ModelTag = "ImagingTarget" self.imagingTarget.planetRadius = groundLocationPlanetRadius self.imagingTarget.specifyLocation(0.0, 0.0, 1000.0) self.imagingTarget.planetInMsg.subscribeTo( - self.environment.gravFactory.spiceObject.planetStateOutMsgs[ - self.environment.body_index - ] + self.world.gravFactory.spiceObject.planetStateOutMsgs[self.world.body_index] ) self.imagingTarget.minimumElevation = imageTargetMinimumElevation self.imagingTarget.maximumRange = imageTargetMaximumRange self.simulator.AddModelToTask( - self.environment.env_task_name, + self.world.world_task_name, self.imagingTarget, ModelPriority=priority, ) self.imagingTarget.addSpacecraftToModel(self.scObject.scStateOutMsg) def reset_for_action(self) -> None: - """Shut off power sinks.""" + """Shut off power sinks unless the transmitter or instrument is being used.""" super().reset_for_action() self.transmitter.dataStatus = 0 self.transmitterPowerSink.powerStatus = 0 @@ -870,22 +1013,28 @@ def reset_for_action(self) -> None: class ContinuousImagingDynModel(ImagingDynModel): - """Equips the satellite for continuous nadir imaging. + """Equips the satellite for continuous nadir imaging.""" - Equips satellite with an instrument, storage unit, and transmitter - for continuous nadir imaging. - """ + def __init__(self, *args, **kwargs) -> None: + """Equips the satellite for continuous nadir imaging. + + Equips satellite with an instrument, storage unit, and transmitter + for continuous nadir imaging. A single data buffer is used for storage, and data + is accumulated continuously while imaging. The imaging target is fixed at the + center of the Earth for nadir imaging. + """ + super().__init__(*args, **kwargs) @default_args(instrumentBaudRate=8e6) - def _set_instrument( + def setup_instrument( self, instrumentBaudRate: float, priority: int = 895, **kwargs ) -> None: - """Create the continuous instrument model. + """Set up the continuous instrument model. Args: - instrumentBaudRate: Data generated in step by continuous imaging [bits] + instrumentBaudRate: [baud] Data generation rate step when continuously imaging. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.instrument = simpleInstrument.SimpleInstrument() self.instrument.ModelTag = "instrument" + self.satellite.id @@ -896,25 +1045,27 @@ def _set_instrument( ) @default_args( - dataStorageCapacity=20 * 8e6, storageUnitValidCheck=True, storageInit=0 + dataStorageCapacity=20 * 8e6, + storageUnitValidCheck=False, + storageInit=0, ) - def _set_storage_unit( + def setup_storage_unit( self, dataStorageCapacity: int, + storageUnitValidCheck: bool, + storageInit: int, priority: int = 699, - storageUnitValidCheck: bool = True, - storageInit: int = 0, **kwargs, ) -> None: """Configure the storage unit and its buffers. Args: - dataStorageCapacity: Maximum data to be stored [bits] - priority: Model priority. + dataStorageCapacity: [bits] Maximum data that can be stored. storageUnitValidCheck: If True, check that the storage level is below the storage capacity. - storageInit: Initial storage level [bits] - kwargs: Ignored + storageInit: [bits] Initial storage level. + priority: Model priority. + kwargs: Passed to other setup functions. """ self.storageUnit = simpleStorageUnit.SimpleStorageUnit() self.storageUnit.ModelTag = "storageUnit" + self.satellite.id @@ -933,39 +1084,33 @@ def _set_storage_unit( self.task_name, self.storageUnit, ModelPriority=priority ) - @default_args( - imageTargetMaximumRange=-1, - ) - def _set_imaging_target( + @default_args(imageTargetMaximumRange=-1) + def setup_imaging_target( self, imageTargetMaximumRange: float = -1, priority: int = 2000, **kwargs, ) -> None: - """Add a generic imaging target to dynamics. - - The target must be updated with a particular location when used. + """Add a imaging target at the center of the Earth. Args: - imageTargetMaximumRange: Maximum range from target to satellite when - imaging. -1 to disable. [m] + imageTargetMaximumRange: [m] Maximum range from target to satellite when + imaging. -1 to disable. priority: Model priority. - kwargs: Ignored + kwargs: Passed to other setup functions. """ self.imagingTarget = groundLocation.GroundLocation() self.imagingTarget.ModelTag = "scanningTarget" self.imagingTarget.planetRadius = 1e-6 self.imagingTarget.specifyLocation(0, 0, 0) self.imagingTarget.planetInMsg.subscribeTo( - self.environment.gravFactory.spiceObject.planetStateOutMsgs[ - self.environment.body_index - ] + self.world.gravFactory.spiceObject.planetStateOutMsgs[self.world.body_index] ) self.imagingTarget.minimumElevation = np.radians(-90) self.imagingTarget.maximumRange = imageTargetMaximumRange self.simulator.AddModelToTask( - self.environment.env_task_name, + self.world.world_task_name, self.imagingTarget, ModelPriority=priority, ) @@ -973,19 +1118,27 @@ def _set_imaging_target( class GroundStationDynModel(ImagingDynModel): - """Model that connects satellite to environment ground stations.""" + """Model that connects satellite to world ground stations.""" + + def __init__(self, *args, **kwargs) -> None: + """Model that connects satellite to world ground stations. + + This model enables the use of ground stations defined in :class:`~bsk_rl.sim.world.GroundStationWorldModel` + for data downlink. + """ + super().__init__(*args, **kwargs) @classmethod - def _requires_env(cls) -> list[type["EnvironmentModel"]]: - return super()._requires_env() + [environment.GroundStationEnvModel] + def _requires_world(cls) -> list[type["WorldModel"]]: + return super()._requires_world() + [world.GroundStationWorldModel] - def _init_dynamics_objects(self, **kwargs) -> None: - super()._init_dynamics_objects(**kwargs) - self._set_ground_station_locations() + def _setup_dynamics_objects(self, **kwargs) -> None: + super()._setup_dynamics_objects(**kwargs) + self.setup_ground_station_locations() - def _set_ground_station_locations(self) -> None: + def setup_ground_station_locations(self) -> None: """Connect the transmitter to ground stations.""" - for groundStation in self.environment.groundStations: + for groundStation in self.world.groundStations: groundStation.addSpacecraftToModel(self.scObject.scStateOutMsg) self.transmitter.addAccessMsgToTransmitter(groundStation.accessOutMsgs[-1]) @@ -993,4 +1146,18 @@ def _set_ground_station_locations(self) -> None: class FullFeaturedDynModel(GroundStationDynModel, LOSCommDynModel): """Convenience class for a satellite with ground station and line-of-sight comms.""" - pass + def __init__(self, *args, **kwargs) -> None: + """Convenience class for an imaging satellite with ground stations and line-of-sight communication.""" + super().__init__(*args, **kwargs) + + +__doc_title__ = "Dynamics Sims" +__all__ = [ + "DynamicsModel", + "BasicDynamicsModel", + "LOSCommDynModel", + "ImagingDynModel", + "ContinuousImagingDynModel", + "GroundStationDynModel", + "FullFeaturedDynModel", +] diff --git a/src/bsk_rl/env/simulation/fsw.py b/src/bsk_rl/sim/fsw.py similarity index 69% rename from src/bsk_rl/env/simulation/fsw.py rename to src/bsk_rl/sim/fsw.py index 5a489da4..914109be 100644 --- a/src/bsk_rl/env/simulation/fsw.py +++ b/src/bsk_rl/sim/fsw.py @@ -1,17 +1,41 @@ -"""Basilisk flight software models.""" +"""Basilisk flight software models (FSW) are given in ``bsk_rl.sim.fsw``. + +Flight software models serve as the interface between the operation of the satellite in +simulation and the Gymnasium environment. While some FSW models add additional +functionality to the satellite, such as imaging instrument control in :class:`ImagingFSWModel`, +others replace the default control laws with a more complex algorithms, such as :class:`SteeringFSWModel` +vis a vis :class:`BasicFSWModel`. + +Actions +------- + +Each FSW model has a number of actions that can be called to task the satellite. These +actions are decorated with the :func:`~bsk_rl.sim.fsw.action` decorator, which performs +housekeeping tasks before the action is executed. These actions are the primary way to +control the satellite simulation from other parts of the Gymnasium environment. + +Properties +---------- + +The FSW model provides a number of properties for easy access to the satellite state. +These can be accessed directly from the dynamics model instance, or in the observation +via the :class:`~bsk_rl.obs.SatProperties` observation. + +Aliveness Checking +------------------ + +Certain functions in the FSW models are decorated with the :func:`~bsk_rl.utils.functional.aliveness_checker` +decorator. These functions are called at each step to check if the satellite is still +operational, returning true if the satellite is still alive. + + +""" from abc import ABC, abstractmethod +from functools import wraps from typing import TYPE_CHECKING, Callable, Iterable, Optional from weakref import proxy -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import ( - DynamicsModel, - EnvironmentModel, - Satellite, - Simulator, - ) - import Basilisk.architecture.cMsgCInterfacePy as cMsgPy import numpy as np from Basilisk.architecture import messaging @@ -31,15 +55,33 @@ ) from Basilisk.utilities import macros as mc -from bsk_rl.env.simulation import dynamics -from bsk_rl.utils.functional import check_aliveness_checkers, default_args +from bsk_rl.sim import dyn +from bsk_rl.utils.functional import ( + AbstractClassProperty, + check_aliveness_checkers, + default_args, +) + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.sim import Simulator + from bsk_rl.sim.dyn import DynamicsModel + from bsk_rl.sim.world import WorldModel def action( func: Callable[..., None] ) -> Callable[Callable[..., None], Callable[..., None]]: - """Decorate to run housekeeping for action functions called by the satellite.""" + """Decorator to reset the satellite software before executing an action. + Each time an action is called, the FSW tasks and dynamics models call their + ``reset_for_action`` methods to ensure that the satellite is in a consistent state + before the action is executed. + + Action functions are typically called by :ref:`bsk_rl.act` to task the satellite. + """ + + @wraps(func) def inner(self, *args, **kwargs) -> Callable[..., None]: self.fsw_proc.disableAllTasks() self._zero_gateway_msgs() @@ -48,29 +90,32 @@ def inner(self, *args, **kwargs) -> Callable[..., None]: task.reset_for_action() return func(self, *args, **kwargs) + inner.__doc__ = "*Decorated with* :class:`~bsk_rl.sim.fsw.action`\n\n" + str( + func.__doc__ + ) + return inner class FSWModel(ABC): - """Abstract Basilisk flight software model. - - One FSWModel is instantiated for each satellite in the environment each time a - new simulator is created. - """ + """Abstract Basilisk flight software model.""" @classmethod def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - """Define minimum DynamicsModels for compatibility.""" + """Define minimum :class:`~bsk_rl.sim.dyn.DynamicsModel` for compatibility.""" return [] def __init__( self, satellite: "Satellite", fsw_rate: float, priority: int = 100, **kwargs ) -> None: - """Construct a base flight software model. + """The abstract base flight software model. + + One FSWModel is instantiated for each satellite in the environment each time the + environment is reset and new simulator is created. Args: satellite: Satellite modelled by this model - fsw_rate: Rate of FSW simulation [s] + fsw_rate: [s] Rate of FSW simulation. priority: Model priority. kwargs: Passed to task creation functions """ @@ -99,7 +144,7 @@ def __init__( self._set_messages() for task in self.tasks: - task._init_objects(**kwargs) + task._setup_fsw_objects(**kwargs) self.fsw_proc.disableAllTasks() @@ -109,9 +154,9 @@ def simulator(self) -> "Simulator": return self.satellite.simulator @property - def environment(self) -> "EnvironmentModel": - """Reference to the episode environment model.""" - return self.simulator.environment + def world(self) -> "WorldModel": + """Reference to the episode world model.""" + return self.simulator.world @property def dynamics(self) -> "DynamicsModel": @@ -127,10 +172,10 @@ def _set_messages(self) -> None: pass def is_alive(self, log_failure=False) -> bool: - """Check if the fsw model has failed any aliveness requirements. + """Check if the FSW model has failed any aliveness requirements. Returns: - If the satellite fsw is still alive + ``True`` if the satellite FSW is still alive. """ return check_aliveness_checkers(self, log_failure=log_failure) @@ -142,14 +187,14 @@ def __del__(self): class Task(ABC): """Abstract class for defining FSW tasks.""" - @property - @abstractmethod # pragma: no cover - def name(self) -> str: # noqa: D102 - pass + name: str = AbstractClassProperty() def __init__(self, fsw: FSWModel, priority: int) -> None: """Template class for defining FSW processes. + Each FSW process has a task associated with it, which handle certain housekeeping + functions. + Args: fsw: FSW model task contributes to priority: Task priority @@ -172,7 +217,7 @@ def _create_module_data(self) -> None: pass @abstractmethod # pragma: no cover - def _init_objects(self, **kwargs) -> None: + def _setup_fsw_objects(self, **kwargs) -> None: """Initialize model parameters with satellite arguments.""" pass @@ -202,7 +247,7 @@ class BasicFSWModel(FSWModel): @classmethod def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - return [dynamics.BasicDynamicsModel] + return [dyn.BasicDynamicsModel] def _make_task_list(self) -> list[Task]: return [ @@ -259,7 +304,7 @@ def _zero_gateway_msgs(self) -> None: @action def action_drift(self) -> None: - """Disable all tasks.""" + """Disable all tasks and do nothing.""" self.simulator.disableTask( BasicFSWModel.MRPControlTask.name + self.satellite.id ) @@ -270,18 +315,23 @@ class SunPointTask(Task): name = "sunPointTask" def __init__(self, fsw, priority=99) -> None: # noqa: D107 + """Task to generate a sun-pointing reference.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: self.sunPoint = self.fsw.sunPoint = locationPointing.locationPointing() self.sunPoint.ModelTag = "sunPoint" - def _init_objects(self, nHat_B: Iterable[float], **kwargs) -> None: - """Configure the sun-pointing task. + def _setup_fsw_objects(self, **kwargs) -> None: + """Configure the solar array sun-pointing task.""" + self.setup_sun_pointing(**kwargs) + + def setup_sun_pointing(self, nHat_B: Iterable[float], **kwargs) -> None: + """Configure the solar array sun-pointing task. Args: - nHat_B: Solar array normal vector - kwargs: Ignored + nHat_B: Solar array normal vector. + kwargs: Passed to other setup functions. """ self.sunPoint.pHat_B = nHat_B self.sunPoint.scAttInMsg.subscribeTo( @@ -291,9 +341,7 @@ def _init_objects(self, nHat_B: Iterable[float], **kwargs) -> None: self.fsw.dynamics.simpleNavObject.transOutMsg ) self.sunPoint.celBodyInMsg.subscribeTo( - self.fsw.environment.ephemConverter.ephemOutMsgs[ - self.fsw.environment.sun_index - ] + self.fsw.world.ephemConverter.ephemOutMsgs[self.fsw.world.sun_index] ) self.sunPoint.useBoresightRateDamping = 1 cMsgPy.AttGuidMsg_C_addAuthor( @@ -304,7 +352,7 @@ def _init_objects(self, nHat_B: Iterable[float], **kwargs) -> None: @action def action_charge(self) -> None: - """Charge battery using solar panels.""" + """Charge battery by pointing the solar panels at the sun.""" self.sunPoint.Reset(self.simulator.sim_time_ns) self.simulator.enableTask(self.SunPointTask.name + self.satellite.id) @@ -314,21 +362,24 @@ class NadirPointTask(Task): name = "nadirPointTask" def __init__(self, fsw, priority=98) -> None: # noqa: D107 + """Task to generate nadir-pointing reference.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: self.hillPoint = self.fsw.hillPoint = hillPoint.hillPoint() self.hillPoint.ModelTag = "hillPoint" - def _init_objects(self, **kwargs) -> None: - """Configure the nadir-pointing task.""" + def _setup_fsw_objects(self, **kwargs) -> None: + """Configure the nadir-pointing task. + + Args: + kwargs: Passed to other setup functions. + """ self.hillPoint.transNavInMsg.subscribeTo( self.fsw.dynamics.simpleNavObject.transOutMsg ) self.hillPoint.celBodyInMsg.subscribeTo( - self.fsw.environment.ephemConverter.ephemOutMsgs[ - self.fsw.environment.body_index - ] + self.fsw.world.ephemConverter.ephemOutMsgs[self.fsw.world.body_index] ) cMsgPy.AttRefMsg_C_addAuthor( self.hillPoint.attRefOutMsg, self.fsw.attRefMsg @@ -342,6 +393,7 @@ class RWDesatTask(Task): name = "rwDesatTask" def __init__(self, fsw, priority=97) -> None: # noqa: D107 + """Task to desaturate reaction wheels using thrusters.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: @@ -361,21 +413,26 @@ def _create_module_data(self) -> None: ) self.thrForceMapping.ModelTag = "thrForceMapping" - def _init_objects(self, **kwargs) -> None: - self._set_thruster_mapping(**kwargs) - self._set_momentum_dumping(**kwargs) + def _setup_fsw_objects(self, **kwargs) -> None: + """Set up thrusters and momentum dumping. + + Args: + kwargs: Passed to other setup functions. + """ + self.setup_thruster_mapping(**kwargs) + self.setup_momentum_dumping(**kwargs) @default_args(controlAxes_B=[1, 0, 0, 0, 1, 0, 0, 0, 1], thrForceSign=+1) - def _set_thruster_mapping( + def setup_thruster_mapping( self, controlAxes_B: Iterable[float], thrForceSign: int, **kwargs ) -> None: """Configure the thruster mapping. Args: - controlAxes_B: Control unit axes + controlAxes_B: Control unit axes. thrForceSign: Flag indicating if pos (+1) or negative (-1) thruster - solutions are found - kwargs: Ignored + solutions are found. + kwargs: Passed to other setup functions. """ self.thrForceMapping.cmdTorqueInMsg.subscribeTo( self.thrDesatControl.deltaHOutMsg @@ -394,7 +451,7 @@ def _set_thruster_mapping( thrMinFireTime=0.02, desatAttitude="sun", ) - def _set_momentum_dumping( + def setup_momentum_dumping( self, hs_min: float, maxCounterValue: int, @@ -405,13 +462,16 @@ def _set_momentum_dumping( """Configure the momentum dumping algorithm. Args: - hs_min: minimum RW cluster momentum for dumping [N*m*s] - maxCounterValue: Control periods between firing thrusters - thrMinFireTime: Minimum thruster firing time [s] - desatAttitude: Direction to point while desaturating: "sun" points - panels at sun, "nadir" points instrument nadir, None disables - attitude control - kwargs: Ignored + hs_min: [N*m*s] Minimum RW cluster momentum for dumping. + maxCounterValue: Control periods between firing thrusters. + thrMinFireTime: [s] Minimum thruster firing time. + desatAttitude: Direction to point while desaturating: + + * ``"sun"`` points panels at sun + * ``"nadir"`` points instrument nadir + * ``None`` disables attitude control. + + kwargs: Passed to other setup functions. """ self.fsw.desatAttitude = desatAttitude self.thrDesatControl.hs_min = hs_min # Nms @@ -432,13 +492,18 @@ def _set_momentum_dumping( self._add_model_to_task(self.thrDump, priority=1191) def reset_for_action(self) -> None: - """Disable power draw for thrusters.""" + """Disable power draw for thrusters when a new action is selected.""" super().reset_for_action() self.fsw.dynamics.thrusterPowerSink.powerStatus = 0 @action def action_desat(self) -> None: - """Charge while desaturating reaction wheels.""" + """Charge while desaturating reaction wheels. + + This action maneuvers the satellite into ``desatAttitude``, turns on the thruster + power sink, and enables the desaturation tasks. This action typically needs to be + called multiple times to fully desaturate the wheels. + """ self.trackingError.Reset(self.simulator.sim_time_ns) self.thrDesatControl.Reset(self.simulator.sim_time_ns) self.thrDump.Reset(self.simulator.sim_time_ns) @@ -464,6 +529,7 @@ class TrackingErrorTask(Task): name = "trackingErrTask" def __init__(self, fsw, priority=90) -> None: # noqa: D107 + """Task to convert an attitude reference to guidance.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: @@ -472,7 +538,7 @@ def _create_module_data(self) -> None: ) self.trackingError.ModelTag = "trackingError" - def _init_objects(self, **kwargs) -> None: + def _setup_fsw_objects(self, **kwargs) -> None: self.trackingError.attNavInMsg.subscribeTo( self.fsw.dynamics.simpleNavObject.attOutMsg ) @@ -484,11 +550,12 @@ def _init_objects(self, **kwargs) -> None: self._add_model_to_task(self.trackingError, priority=1197) class MRPControlTask(Task): - """Task to control the satellite with reaction wheels.""" + """Task to control the satellite attitude using reaction wheels.""" name = "mrpControlTask" def __init__(self, fsw, priority=80) -> None: # noqa: D107 + """Task to control the satellite with reaction wheels.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: @@ -502,21 +569,21 @@ def _create_module_data(self) -> None: self.rwMotorTorque = self.fsw.rwMotorTorque = rwMotorTorque.rwMotorTorque() self.rwMotorTorque.ModelTag = "rwMotorTorque" - def _init_objects(self, **kwargs) -> None: - self._set_mrp_feedback_rwa(**kwargs) - self._set_rw_motor_torque(**kwargs) + def _setup_fsw_objects(self, **kwargs) -> None: + self.setup_mrp_feedback_rwa(**kwargs) + self.setup_rw_motor_torque(**kwargs) @default_args(K=7.0, Ki=-1, P=35.0) - def _set_mrp_feedback_rwa( + def setup_mrp_feedback_rwa( self, K: float, Ki: float, P: float, **kwargs ) -> None: """Set the MRP feedback control properties. Args: - K: Proportional gain - Ki: Integral gain - P: Derivative gain - kwargs: Ignored + K: Proportional gain. + Ki: Integral gain. + P: Derivative gain. + kwargs: Passed to other setup functions. """ self.mrpFeedbackControl.guidInMsg.subscribeTo(self.fsw.attGuidMsg) self.mrpFeedbackControl.vehConfigInMsg.subscribeTo(self.fsw.vcConfigMsg) @@ -529,14 +596,14 @@ def _set_mrp_feedback_rwa( self._add_model_to_task(self.mrpFeedbackControl, priority=1196) - def _set_rw_motor_torque( + def setup_rw_motor_torque( self, controlAxes_B: Iterable[float], **kwargs ) -> None: """Set parameters for finding motor torque from the control law. Args: - controlAxes_B: Control unit axes - kwargs: Ignored + controlAxes_B: Control unit axes. + kwargs: Passed to other setup functions. """ self.rwMotorTorque.rwParamsInMsg.subscribeTo(self.fsw.fswRwConfigMsg) self.rwMotorTorque.vehControlInMsg.subscribeTo( @@ -547,7 +614,7 @@ def _set_rw_motor_torque( self._add_model_to_task(self.rwMotorTorque, priority=1195) def reset_for_action(self) -> None: - """MRP control enabled by default for all tasks.""" + """MRP control is enabled by default for all tasks.""" self.fsw.simulator.enableTask(self.name + self.fsw.satellite.id) @@ -556,7 +623,11 @@ class ImagingFSWModel(BasicFSWModel): @classmethod def _requires_dyn(cls) -> list[type["DynamicsModel"]]: - return super()._requires_dyn() + [dynamics.ImagingDynModel] + return super()._requires_dyn() + [dyn.ImagingDynModel] + + def __init__(self, *args, **kwargs) -> None: + """Adds instrument pointing and triggering control to FSW.""" + super().__init__(*args, **kwargs) @property def c_hat_P(self): @@ -579,6 +650,7 @@ class LocPointTask(Task): name = "locPointTask" def __init__(self, fsw, priority=96) -> None: # noqa: D107 + """Task to point the instrument at ground targets.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: @@ -592,19 +664,19 @@ def _create_module_data(self) -> None: ) self.insControl.ModelTag = "instrumentController" - def _init_objects(self, **kwargs) -> None: - self._set_location_pointing(**kwargs) - self._set_instrument_controller(**kwargs) + def _setup_fsw_objects(self, **kwargs) -> None: + self.setup_location_pointing(**kwargs) + self.setup_instrument_controller(**kwargs) @default_args(inst_pHat_B=[0, 0, 1]) - def _set_location_pointing( + def setup_location_pointing( self, inst_pHat_B: Iterable[float], **kwargs ) -> None: """Set the Earth location pointing guidance module. Args: - inst_pHat_B: Instrument pointing direction - kwargs: Ignored + inst_pHat_B: Instrument pointing direction. + kwargs: Passed to other setup functions. """ self.locPoint.pHat_B = inst_pHat_B self.locPoint.scAttInMsg.subscribeTo( @@ -624,7 +696,7 @@ def _set_location_pointing( self._add_model_to_task(self.locPoint, priority=1198) @default_args(imageAttErrorRequirement=0.01, imageRateErrorRequirement=None) - def _set_instrument_controller( + def setup_instrument_controller( self, imageAttErrorRequirement: float, imageRateErrorRequirement: float, @@ -632,12 +704,17 @@ def _set_instrument_controller( ) -> None: """Set the instrument controller parameters. + The instrument controller is used to take an image when certain relative + attitude requirements are met, along with the access requirements of the + target (i.e. ``imageTargetMinimumElevation`` and ``imageTargetMaximumRange`` + as set in :class:`~bsk_rl.sim.dyn.ImagingDynModel.setup_imaging_target`). + Args: - imageAttErrorRequirement: Pointing attitude error tolerance for imaging - [MRP norm] - imageRateErrorRequirement: Rate tolerance for imaging. Disable with - None. [rad/s] - kwargs: Ignored + imageAttErrorRequirement: [MRP norm] Pointing attitude error tolerance + for imaging. + imageRateErrorRequirement: [rad/s] Rate tolerance for imaging. Disable + with ``None``. + kwargs: Passed to other setup functions. """ self.insControl.attErrTolerance = imageAttErrorRequirement if imageRateErrorRequirement is not None: @@ -658,23 +735,33 @@ def reset_for_action(self) -> None: return super().reset_for_action() @action - def action_image(self, location: Iterable[float], data_name: str) -> None: + def action_image(self, r_LP_P: Iterable[float], data_name: str) -> None: """Attempt to image a target at a location. + This action sets the target attitude to one tracking a ground location. If the + target is within the imaging constraints, an image will be taken and stored in + the data buffer. The instrument power sink will be active as long as the task is + enabled. + Args: - location: PCPF target location [m] - data_name: Data buffer to store image data to + r_LP_P: [m] Planet-fixed planet relative target location. + data_name: Data buffer to store image data to. """ self.insControl.controllerStatus = 1 self.dynamics.instrumentPowerSink.powerStatus = 1 - self.dynamics.imagingTarget.r_LP_P_Init = location + self.dynamics.imagingTarget.r_LP_P_Init = r_LP_P self.dynamics.instrument.nodeDataName = data_name self.insControl.imaged = 0 self.simulator.enableTask(self.LocPointTask.name + self.satellite.id) @action def action_downlink(self) -> None: - """Attempt to downlink data.""" + """Attempt to downlink data. + + This action points the satellite nadir and attempts to downlink data. If the + satellite is in range of a ground station, data will be downlinked at the specified + baud rate. The transmitter power sink will be active as long as the task is enabled. + """ self.hillPoint.Reset(self.simulator.sim_time_ns) self.trackingError.Reset(self.simulator.sim_time_ns) self.dynamics.transmitter.dataStatus = 1 @@ -688,8 +775,20 @@ def action_downlink(self) -> None: class ContinuousImagingFSWModel(ImagingFSWModel): """FSW model for continuous nadir scanning.""" + def __init__(self, *args, **kwargs) -> None: + """FSW model for continuous nadir scanning. + + Instead of imaging point targets, this model is used to continuously scan the + ground while pointing nadir. + """ + super().__init__(*args, **kwargs) + class LocPointTask(ImagingFSWModel.LocPointTask): - """Task to point at targets and trigger the instrument.""" + """Task to point nadir and trigger the instrument.""" + + def __init__(self, *args, **kwargs) -> None: + """Task to point nadir and trigger the instrument.""" + super().__init__(*args, **kwargs) def _create_module_data(self) -> None: # Location pointing configuration @@ -703,20 +802,22 @@ def _create_module_data(self) -> None: self.insControl.ModelTag = "instrumentController" @default_args(imageAttErrorRequirement=0.01, imageRateErrorRequirement=None) - def _set_instrument_controller( + def setup_instrument_controller( self, imageAttErrorRequirement: float, imageRateErrorRequirement: float, **kwargs, ) -> None: - """Set the instrument controller parameters. + """Set the instrument controller parameters for scanning. + + As long as these two conditions are met, scanning will occur continuously. Args: - imageAttErrorRequirement: Pointing attitude error tolerance for imaging - [MRP norm] - imageRateErrorRequirement: Rate tolerance for imaging. Disable with - None. [rad/s] - kwargs: Ignored + imageAttErrorRequirement: [MRP norm] Pointing attitude error tolerance + for imaging. + imageRateErrorRequirement: [rad/s] Rate tolerance for imaging. Disable + with None. + kwargs: Passed to other setup functions. """ self.insControl.attErrTolerance = imageAttErrorRequirement if imageRateErrorRequirement is not None: @@ -740,34 +841,48 @@ def reset_for_action(self) -> None: def action_nadir_scan(self) -> None: """Scan nadir. - Args: - location: PCPF target location [m] - data_name: Data buffer to store image data to + This action points the instrument nadir and continuously adds data to the buffer + as long as attitude requirements are met. The instrument power sink is active + as long as the action is set. """ self.dynamics.instrument.nodeStatusInMsg.subscribeTo( self.insControl.deviceCmdOutMsg ) self.insControl.controllerStatus = 1 self.dynamics.instrumentPowerSink.powerStatus = 1 - self.dynamics.imagingTarget.r_LP_P_Init = np.array([0, 0, 0.1]) + self.dynamics.imagingTarget.r_LP_P_Init = np.array( + [0, 0, 0.1] + ) # All zero causes an error self.dynamics.instrument.nodeDataName = "nadir" self.simulator.enableTask(self.LocPointTask.name + self.satellite.id) @action def action_image(self, *args, **kwargs) -> None: - """Disable imaging from parent class.""" + """Disable ``action_image`` from parent class. + + :meta private: + """ raise NotImplementedError("Use action_nadir_scan instead") class SteeringFSWModel(BasicFSWModel): """FSW extending MRP control to use MRP steering instead of MRP feedback.""" + def __init__(self, *args, **kwargs) -> None: + """FSW extending attitude control to use MRP steering instead of MRP feedback. + + This class replaces the simple attitude feedback control law with a more + sophisticated `MRP steering control law `_. + """ + super().__init__(*args, **kwargs) + class MRPControlTask(Task): """Task that uses MRP steering to control reaction wheels.""" name = "mrpControlTask" def __init__(self, fsw, priority=80) -> None: # noqa: D107 + """Task to control the satellite with reaction wheels.""" super().__init__(fsw, priority) def _create_module_data(self) -> None: @@ -787,12 +902,14 @@ def _create_module_data(self) -> None: self.rwMotorTorque = self.fsw.rwMotorTorque = rwMotorTorque.rwMotorTorque() self.rwMotorTorque.ModelTag = "rwMotorTorque" - def _init_objects(self, **kwargs) -> None: - self._set_mrp_steering_rwa(**kwargs) - self._set_rw_motor_torque(**kwargs) + def _setup_fsw_objects(self, **kwargs) -> None: + self.setup_mrp_steering_rwa(**kwargs) + self.setup_rw_motor_torque(**kwargs) - @default_args(K1=0.25, K3=3.0, omega_max=3 * mc.D2R, servo_Ki=5.0, servo_P=150) - def _set_mrp_steering_rwa( + @default_args( + K1=0.25, K3=3.0, omega_max=np.radians(3), servo_Ki=5.0, servo_P=150 + ) + def setup_mrp_steering_rwa( self, K1: float, K3: float, @@ -804,12 +921,12 @@ def _set_mrp_steering_rwa( """Define the control properties. Args: - K1: MRP steering gain - K3: MRP steering gain - omega_max: Maximum targetable spacecraft body rate [rad/s] - servo_Ki: Servo gain - servo_P: Servo gain - kwargs: Ignored + K1: MRP steering gain. + K3: MRP steering gain. + omega_max: [rad/s] Maximum targetable spacecraft body rate. + servo_Ki: Servo gain. + servo_P: Servo gain. + kwargs: Passed to other setup functions. """ self.mrpSteeringControl.guidInMsg.subscribeTo(self.fsw.attGuidMsg) self.mrpSteeringControl.K1 = K1 @@ -834,14 +951,14 @@ def _set_mrp_steering_rwa( self._add_model_to_task(self.mrpSteeringControl, priority=1196) self._add_model_to_task(self.servo, priority=1195) - def _set_rw_motor_torque( + def setup_rw_motor_torque( self, controlAxes_B: Iterable[float], **kwargs ) -> None: """Define the motor torque from the control law. Args: - controlAxes_B: Control unit axes - kwargs: Ignored + controlAxes_B: Control unit axes. + kwargs: Passed to other setup functions. """ self.rwMotorTorque.rwParamsInMsg.subscribeTo(self.fsw.fswRwConfigMsg) self.rwMotorTorque.vehControlInMsg.subscribeTo(self.servo.cmdTorqueOutMsg) @@ -857,4 +974,17 @@ def reset_for_action(self) -> None: class SteeringImagerFSWModel(SteeringFSWModel, ImagingFSWModel): """Convenience type for ImagingFSWModel with MRP steering.""" - pass + def __init__(self, *args, **kwargs) -> None: + """Convenience type that combines the imaging FSW model with MRP steering.""" + super().__init__(*args, **kwargs) + + +__doc_title__ = "FSW Sims" +__all__ = [ + "action", + "BasicFSWModel", + "ImagingFSWModel", + "ContinuousImagingFSWModel", + "SteeringFSWModel", + "SteeringImagerFSWModel", +] diff --git a/src/bsk_rl/env/simulation/simulator.py b/src/bsk_rl/sim/simulator.py similarity index 55% rename from src/bsk_rl/env/simulation/simulator.py rename to src/bsk_rl/sim/simulator.py index a41ecae5..8f6601f0 100644 --- a/src/bsk_rl/env/simulation/simulator.py +++ b/src/bsk_rl/sim/simulator.py @@ -1,18 +1,15 @@ """Extended Basilisk SimBaseClass for GeneralSatelliteTasking environments.""" -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: # pragma: no cover - from bsk_rl.env.types import ( - EnvironmentModel, - Satellite, - ) - import logging +from typing import TYPE_CHECKING, Any from Basilisk.utilities import SimulationBaseClass from Basilisk.utilities import macros as mc +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sats import Satellite + from bsk_rl.sim.world import WorldModel + logger = logging.getLogger(__name__) @@ -22,21 +19,24 @@ class Simulator(SimulationBaseClass.SimBaseClass): def __init__( self, satellites: list["Satellite"], - env_type: type["EnvironmentModel"], - env_args: dict[str, Any], + world_type: type["WorldModel"], + world_args: dict[str, Any], sim_rate: float = 1.0, max_step_duration: float = 600.0, time_limit: float = float("inf"), ) -> None: - """Construct Basilisk simulator. + """Basilisk simulator for satellite tasking environments. + + The simulator is reconstructed each time the environment :class:`~bsk_rl.GeneralSatelliteTasking.reset` + is called, generating a fresh Basilisk simulation. Args: satellites: Satellites to be simulated - env_type: Type of environment model to be constructed - env_args: Arguments for environment model construction - sim_rate: Rate for model simulation [s]. - max_step_duration: Maximum time to propagate sim at a step [s]. - time_limit: Latest time simulation will propagate to. + world_type: Type of world model to be constructed + world_args: Arguments for world model construction + sim_rate: [s] Rate for model simulation. + max_step_duration: [s] Maximum time to propagate sim at a step. + time_limit: [s] Latest time simulation will propagate to. """ super().__init__() self.sim_rate = sim_rate @@ -44,9 +44,9 @@ def __init__( self.max_step_duration = max_step_duration self.time_limit = time_limit - self.environment: EnvironmentModel + self.world: WorldModel - self._set_environment(env_type, env_args) + self._set_world(world_type, world_args) self.fsw_list = {} self.dynamics_list = {} @@ -70,30 +70,39 @@ def sim_time(self) -> float: """Simulation time in seconds, tied to SimBase integrator.""" return self.sim_time_ns * mc.NANO2SEC - def _set_environment( - self, env_type: type["EnvironmentModel"], env_args: dict[str, Any] + def _set_world( + self, world_type: type["WorldModel"], world_args: dict[str, Any] ) -> None: - """Construct the simulator environment model. + """Construct the simulator world model. Args: - env_type: type of environment model to be constructed - env_args: arguments for environment model construction + world_type: Type of world model to be constructed. + world_args: Arguments for world model construction, passed to the world + from the environment. """ - self.environment = env_type(self, self.sim_rate, **env_args) + self.world = world_type(self, self.sim_rate, **world_args) def run(self) -> None: - """Propagate the simulator.""" + """Propagate the simulator. + + Propagates for a duration up to the ``max_step_duration``, stopping if the + environment time limit is reached or an event is triggered. + """ simulation_time = mc.sec2nano( min(self.sim_time + self.max_step_duration, self.time_limit) ) - logger.info(f"Running simulation to {simulation_time*mc.NANO2SEC:.2f} seconds") + logger.info( + f"Running simulation at most to {simulation_time*mc.NANO2SEC:.2f} seconds" + ) self.ConfigureStopTime(simulation_time) self.ExecuteSimulation() def delete_event(self, event_name) -> None: """Remove an event from the event map. - Makes event checking faster. + Makes event checking faster. Due to a performance issue in Basilisk, it is + 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) @@ -102,3 +111,6 @@ def delete_event(self, event_name) -> None: def __del__(self): """Log when simulator is deleted.""" logger.debug("Basilisk simulator deleted") + + +__all__ = [] diff --git a/src/bsk_rl/sim/world.py b/src/bsk_rl/sim/world.py new file mode 100644 index 00000000..e4f8ff98 --- /dev/null +++ b/src/bsk_rl/sim/world.py @@ -0,0 +1,397 @@ +"""Basilisk world models are given in ``bsk_rl.sim.world``. + +In most cases, the user does not need to specify the world model, as it is inferred from +the requirements of the :class:`~bsk_rl.sim.fsw.FSWModel`. However, the user can specify +the world model in the :class:`~bsk_rl.GeneralSatelliteTasking` constructor if desired. + +Customization of the world model parameters is via the ``world_args`` parameter in the +:class:`~bsk_rl.GeneralSatelliteTasking`. As with ``sat_args``, these parameters are +passed as a dictionary of key-value or key-function pairs, with the latter called to +generate the value each time the simulation is reset. + +.. code-block:: python + + world_args = dict( + utc_init="2018 SEP 29 21:00:00.000 (UTC)", # set the epoch + scaleHeight=np.random.uniform(7e3, 9e3), # randomize the atmosphere + ) + +In general, ``world_args`` parameter names match those used in Basilisk. See the setup +functions for short descriptions of what parameters do and the Basilisk documentation +for more detail on their exact model effects. + +""" + +import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Optional, Union +from weakref import proxy + +import numpy as np +from Basilisk import __path__ +from Basilisk.simulation import ( + eclipse, + ephemerisConverter, + exponentialAtmosphere, + groundLocation, +) +from Basilisk.utilities import macros as mc +from Basilisk.utilities import orbitalMotion, simIncludeGravBody + +from bsk_rl.utils.functional import collect_default_args, default_args +from bsk_rl.utils.orbital import random_epoch + +if TYPE_CHECKING: # pragma: no cover + from bsk_rl.sim import Simulator + +logger = logging.getLogger(__name__) + +bsk_path = __path__[0] + + +class WorldModel(ABC): + """Abstract Basilisk world model.""" + + @classmethod + def default_world_args(cls, **kwargs) -> dict[str, Any]: + """Compile default arguments for the world model. + + Args: + **kwargs: Arguments to override in the default arguments. + + Returns: + Dictionary of arguments for simulation models. + """ + defaults = collect_default_args(cls) + for k, v in kwargs.items(): + if k not in defaults: + raise KeyError(f"{k} not a valid key for world_args") + defaults[k] = v + return defaults + + def __init__( + self, + simulator: "Simulator", + world_rate: float, + priority: int = 300, + **kwargs, + ) -> None: + """Abstract Basilisk world model. + + One WorldModel is instantiated for the environment each time a new simulator + is created. + + Args: + simulator: Simulator using this model. + world_rate: Rate of world simulation [s] + priority: Model priority. + kwargs: Passed through to setup functions. + """ + self.simulator: Simulator = proxy(simulator) + + world_proc_name = "WorldProcess" + world_proc = self.simulator.CreateNewProcess(world_proc_name, priority) + + # Define process name, task name and task time-step + self.world_task_name = "WorldTask" + world_proc.addTask( + self.simulator.CreateNewTask(self.world_task_name, mc.sec2nano(world_rate)) + ) + + self._setup_world_objects(**kwargs) + + def __del__(self): + """Log when world is deleted.""" + logger.debug("Basilisk world deleted") + + @abstractmethod # pragma: no cover + def _setup_world_objects(self, **kwargs) -> None: + """Caller for all world objects.""" + pass + + +class BasicWorldModel(WorldModel): + """Basic world with minimum necessary Basilisk world components.""" + + def __init__(self, *args, **kwargs) -> None: + """Basic world with minimum necessary Basilisk world components. + + This model includes ephemeris and SPICE-based Earth gravity and dynamics models, + an exponential atmosphere model, and an eclipse model. + + Args: + *args: Passed to superclass. + **kwargs: Passed to superclass. + """ + super().__init__(*args, **kwargs) + + @property + def PN(self): + """Planet relative to inertial frame rotation matrix.""" + return np.array( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + .read() + .J20002Pfix + ).reshape((3, 3)) + + @property + def omega_PN_N(self): + """Planet angular velocity in inertial frame [rad/s].""" + PNdot = np.array( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + .read() + .J20002Pfix_dot + ).reshape((3, 3)) + skew_PN_N = -np.matmul(np.transpose(self.PN), PNdot) + return np.array([skew_PN_N[2, 1], skew_PN_N[0, 2], skew_PN_N[1, 0]]) + + def _setup_world_objects(self, **kwargs) -> None: + self.setup_gravity_bodies(**kwargs) + self.setup_ephem_object(**kwargs) + self.setup_atmosphere_density_model(**kwargs) + self.setup_eclipse_object(**kwargs) + + @default_args(utc_init=random_epoch) + def setup_gravity_bodies( + self, utc_init: str, priority: int = 1100, **kwargs + ) -> None: + """Specify gravitational models to use in the simulation. + + Args: + utc_init: UTC datetime string, in the format ``YYYY MMM DD hh:mm:ss.sss (UTC)`` + priority: Model priority. + **kwargs: Passed to other setup functions. + """ + self.gravFactory = simIncludeGravBody.gravBodyFactory() + self.gravFactory.createSun() + self.planet = self.gravFactory.createEarth() + self.sun_index = 0 + self.body_index = 1 + + self.planet.isCentralBody = ( + True # ensure this is the central gravitational body + ) + self.planet.useSphericalHarmonicsGravityModel( + bsk_path + "/supportData/LocalGravData/GGM03S.txt", 10 + ) + + # setup Spice interface for some solar system bodies + timeInitString = utc_init + self.gravFactory.createSpiceInterface( + bsk_path + "/supportData/EphemerisData/", timeInitString + ) + self.gravFactory.spiceObject.zeroBase = "earth" + + self.simulator.AddModelToTask( + self.world_task_name, self.gravFactory.spiceObject, ModelPriority=priority + ) + + def setup_ephem_object(self, priority: int = 988, **kwargs) -> None: + """Set up the ephemeris object to use with the SPICE library. + + Args: + priority: Model priority. + **kwargs: Passed to other setup functions. + """ + self.ephemConverter = ephemerisConverter.EphemerisConverter() + self.ephemConverter.ModelTag = "ephemConverter" + self.ephemConverter.addSpiceInputMsg( + self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] + ) + self.ephemConverter.addSpiceInputMsg( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + ) + self.simulator.AddModelToTask( + self.world_task_name, self.ephemConverter, ModelPriority=priority + ) + + @default_args( + planetRadius=orbitalMotion.REQ_EARTH * 1e3, + baseDensity=1.22, + scaleHeight=8e3, + ) + def setup_atmosphere_density_model( + self, + planetRadius: float, + baseDensity: float, + scaleHeight: float, + priority: int = 1000, + **kwargs, + ) -> None: + """Set up the exponential gravity model. + + Args: + planetRadius: [m] Planet ground radius. + baseDensity: [kg/m^3] Exponential model parameter. + scaleHeight: [m] Exponential model parameter. + priority: Model priority. + **kwargs: Passed to other setup functions. + """ + self.densityModel = exponentialAtmosphere.ExponentialAtmosphere() + self.densityModel.ModelTag = "expDensity" + self.densityModel.planetRadius = planetRadius + self.densityModel.baseDensity = baseDensity + self.densityModel.scaleHeight = scaleHeight + self.densityModel.planetPosInMsg.subscribeTo( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + ) + self.simulator.AddModelToTask( + self.world_task_name, self.densityModel, ModelPriority=priority + ) + + def setup_eclipse_object(self, priority: int = 988, **kwargs) -> None: + """Set up the celestial object that is causing an eclipse message. + + Args: + priority: Model priority. + kwargs: Ignored + """ + self.eclipseObject = eclipse.Eclipse() + self.eclipseObject.addPlanetToModel( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + ) + self.eclipseObject.sunInMsg.subscribeTo( + self.gravFactory.spiceObject.planetStateOutMsgs[self.sun_index] + ) + self.simulator.AddModelToTask( + self.world_task_name, self.eclipseObject, ModelPriority=priority + ) + + def __del__(self) -> None: + """Log when world is deleted and unload SPICE.""" + super().__del__() + try: + self.gravFactory.unloadSpiceKernels() + except AttributeError: + pass + + +class GroundStationWorldModel(BasicWorldModel): + """Model that includes downlink ground stations.""" + + def __init__(self, *args, **kwargs) -> None: + """Model that includes downlink ground stations. + + This model includes the basic world components, as well as ground stations for + downlinking data. + + Args: + *args: Passed to superclass. + **kwargs: Passed to superclass. + """ + super().__init__(*args, **kwargs) + + def _setup_world_objects(self, **kwargs) -> None: + super()._setup_world_objects(**kwargs) + self.setup_ground_locations(**kwargs) + + @default_args( + groundStationsData=[ + dict(name="Boulder", lat=40.009971, long=-105.243895, elev=1624), + dict(name="Merritt", lat=28.3181, long=-80.6660, elev=0.9144), + dict(name="Singapore", lat=1.3521, long=103.8198, elev=15), + dict(name="Weilheim", lat=47.8407, long=11.1421, elev=563), + dict(name="Santiago", lat=-33.4489, long=-70.6693, elev=570), + dict(name="Dongara", lat=-29.2452, long=114.9326, elev=34), + dict(name="Hawaii", lat=19.8968, long=-155.5828, elev=9), + ], + groundLocationPlanetRadius=orbitalMotion.REQ_EARTH * 1e3, + gsMinimumElevation=np.radians(10.0), + gsMaximumRange=-1, + ) + def setup_ground_locations( + self, + groundStationsData: list[dict[str, Union[str, float]]], + groundLocationPlanetRadius: float, + gsMinimumElevation: float, + gsMaximumRange: float, + priority: int = 1399, + **kwargs, + ) -> None: + """Specify the ground locations of interest. + + Args: + groundStationsData: List of dictionaries of ground station data. Each dictionary + must include keys for ``lat`` and ``long`` [deg], and may include + ``elev`` [m], ``name``. For example: + + .. code-block:: python + + groundStationsData=[ + dict(name="Boulder", lat=40.009971, long=-105.243895, elev=1624), + dict(lat=28.3181, long=-80.6660), + ] + + ``groundLocationPlanetRadius``, ``gsMinimumElevation``, and ``gsMaximumRange`` + may also be specified in the dictionary to override the global values + for those parameters for a specific ground station. + + groundLocationPlanetRadius: [m] Radius of ground locations from center of + planet. + gsMinimumElevation: [rad] Minimum elevation angle from station to satellite + to be able to downlink data. + gsMaximumRange: [m] Maximum range from station to satellite when + downlinking. Set to ``-1`` to disable. + priority: Model priority. + kwargs: Passed to other setup functions. + """ + self.groundStations = [] + self.groundLocationPlanetRadius = groundLocationPlanetRadius + self.gsMinimumElevation = gsMinimumElevation + self.gsMaximumRange = gsMaximumRange + for i, groundStationData in enumerate(groundStationsData): + self._create_ground_station(**groundStationData, priority=priority - i) + + def _create_ground_station( + self, + lat: float, + long: float, + elev: float = 0, + name: Optional[str] = None, + groundLocationPlanetRadius: Optional[float] = None, + gsMinimumElevation: Optional[float] = None, + gsMaximumRange: Optional[float] = None, + priority: int = 1399, + ) -> None: + """Add a ground station with given parameters. + + Args: + lat: [deg] Latitude. + long: [deg] Longitude. + elev: [m] Elevation. + name: Ground station identifier. + groundLocationPlanetRadius: [m] Radius of planet. + gsMinimumElevation: [rad] Minimum elevation angle to downlink to ground station. + gsMaximumRange: [m] Maximum range to downlink to ground station. Set to ``-1`` for infinity. + priority: Model priority. + """ + if name is None: + name = str(len(self.groundStations)) + + groundStation = groundLocation.GroundLocation() + groundStation.ModelTag = "GroundStation" + name + if groundLocationPlanetRadius: + groundStation.planetRadius = groundLocationPlanetRadius + else: + groundStation.planetRadius = self.groundLocationPlanetRadius + groundStation.specifyLocation(np.radians(lat), np.radians(long), elev) + groundStation.planetInMsg.subscribeTo( + self.gravFactory.spiceObject.planetStateOutMsgs[self.body_index] + ) + if gsMinimumElevation: + groundStation.minimumElevation = gsMinimumElevation + else: + groundStation.minimumElevation = self.gsMinimumElevation + if gsMaximumRange: + groundStation.maximumRange = gsMaximumRange + else: + groundStation.maximumRange = self.gsMaximumRange + self.groundStations.append(groundStation) + + self.simulator.AddModelToTask( + self.world_task_name, groundStation, ModelPriority=priority + ) + + +__doc_title__ = "World Sims" +__all__ = ["WorldModel", "BasicWorldModel", "GroundStationWorldModel"] diff --git a/src/bsk_rl/training/mcts_learn b/src/bsk_rl/training/mcts_learn deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/training/rllib b/src/bsk_rl/training/rllib deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/training/shields b/src/bsk_rl/training/shields deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/training/todo b/src/bsk_rl/training/todo deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bsk_rl/utils/__init__.py b/src/bsk_rl/utils/__init__.py index e69de29b..6b507ac0 100644 --- a/src/bsk_rl/utils/__init__.py +++ b/src/bsk_rl/utils/__init__.py @@ -0,0 +1,10 @@ +"""A collection of utilities at ``bsk_rl.utils``. + +* :ref:`bsk_rl.utils.actuator_primitives` +* :ref:`bsk_rl.utils.attitude` +* :ref:`bsk_rl.utils.functional` +* :ref:`bsk_rl.utils.orbital` +* :ref:`bsk_rl.utils.rllib` +""" + +__doc_title__ = "Utilities" diff --git a/src/bsk_rl/utils/actuator_primitives.py b/src/bsk_rl/utils/actuator_primitives.py index aa0511f4..61cc9159 100644 --- a/src/bsk_rl/utils/actuator_primitives.py +++ b/src/bsk_rl/utils/actuator_primitives.py @@ -1,4 +1,4 @@ -"""Preset spacecraft components.""" +"""``bsk_rl.utils.actuator_primitives``: Preset spacecraft components.""" import numpy as np from Basilisk.simulation import reactionWheelStateEffector, thrusterDynamicEffector @@ -9,18 +9,23 @@ def balancedHR16Triad( useRandom: bool = False, randomBounds: tuple[float, float] = (-400, 400), wheelSpeeds: list[float] = [500, 500, 500], -): +) -> tuple[ + reactionWheelStateEffector.ReactionWheelStateEffector, + simIncludeRW.rwFactory, + list[float], +]: """Create a set of three HR16 reaction wheels. Args: - useRandom: Use random values for wheel speeds - randomBounds: Bounds for random wheel speeds - wheelSpeeds: Fixed wheel speeds + useRandom: Use random values for wheel speeds. + randomBounds: Bounds for random wheel speeds. + wheelSpeeds: Fixed wheel speeds. Returns: - rwStateEffector: Reaction wheel state effector instance - rwFactory: Factory containing defined reaction wheels - wheelSpeeds: Wheel speeds + tuple: + * **rwStateEffector**: Reaction wheel state effector instance. + * **rwFactory**: Factory containing defined reaction wheels. + * **wheelSpeeds**: Wheel speeds. """ rwFactory = simIncludeRW.rwFactory() if useRandom: @@ -41,13 +46,16 @@ def balancedHR16Triad( return rwStateEffector, rwFactory, wheelSpeeds -def idealMonarc1Octet(): +def idealMonarc1Octet() -> tuple: """Create a set of eight ADCS thrusters using MOOG Monarc-1 attributes. Returns a set of thrusters and thrusterFac instance to add thrusters to a spacecraft. - :return thrusterSet: thruster dynamic effector instance - :return thrusterFac: factory containing defined thrusters. + + Returns: + tuple: + * **thrusterSet**: Thruster dynamic effector instance. + * **thrusterFac**: Factory containing defined thrusters. """ location = [ [3.874945160902288e-2, -1.206182747348013, 0.85245], @@ -75,3 +83,7 @@ def idealMonarc1Octet(): for pos_B, dir_B in zip(location, direction): thFactory.create("MOOG_Monarc_1", pos_B, dir_B) return thrusterSet, thFactory + + +__doc_title__ = "Actuator Primitives" +__all__ = ["balancedHR16Triad", "idealMonarc1Octet"] diff --git a/src/bsk_rl/utils/attitude.py b/src/bsk_rl/utils/attitude.py index bc55e920..502ff2f8 100644 --- a/src/bsk_rl/utils/attitude.py +++ b/src/bsk_rl/utils/attitude.py @@ -1,4 +1,4 @@ -"""Attitude dynamics related utilities.""" +"""``bsk_rl.utils.attitude``: Attitude dynamics related utilities.""" import numpy as np @@ -7,25 +7,18 @@ def random_tumble(maxSpinRate: float = 0.001): """Generate a spacecraft random tumble with uniformly sampled conditions. Args: - maxSpinRate: Maximum spin rate [rad/s]. + maxSpinRate: [rad/s] Maximum spin rate. Returns: - sigma_bn: Initial spacecraft attitude [rad]. - omega_bn: Initial spacecraft angular velocity [rad/s]. + tuple: + * **sigma_bn**: [rad] Initial spacecraft attitude. + * **omega_bn**: [rad/s] Initial spacecraft angular velocity. """ - sigma_bn = np.random.uniform( - 0, - 1.0, - [ - 3, - ], - ) - omega_bn = np.random.uniform( - -maxSpinRate, - maxSpinRate, - [ - 3, - ], - ) + sigma_bn = np.random.uniform(0, 1.0, [3]) + omega_bn = np.random.uniform(-maxSpinRate, maxSpinRate, [3]) return sigma_bn, omega_bn + + +__doc_title__ = "Attitude" +__all__ = ["random_tumble"] diff --git a/src/bsk_rl/utils/functional.py b/src/bsk_rl/utils/functional.py index 6b2f1d2a..b75a8d92 100644 --- a/src/bsk_rl/utils/functional.py +++ b/src/bsk_rl/utils/functional.py @@ -1,10 +1,10 @@ -"""General utility functions.""" +"""``bsk_rl.utils.functional``: General utility functions.""" -import inspect import re import warnings from copy import deepcopy -from typing import Any, Callable +from functools import wraps +from typing import Any, Callable, ParamSpec, TypeVar import numpy as np @@ -13,10 +13,10 @@ def valid_func_name(name: str) -> str: """Convert a string into a valid function name. Args: - name: desired function name + name: Desired function name. Returns: - sanitized function name + Sanitized function name. """ # Remove all characters except for letters, digits, and underscores name = re.sub(r"\W+", "_", name) @@ -29,12 +29,21 @@ def valid_func_name(name: str) -> str: def safe_dict_merge(updates: dict, base: dict) -> dict: """Merge a dict with another dict, warning for conflicts. + .. code-block:: python + + >>> safe_dict_merge(dict(a=1, b=2), dict(b=2, c=3)) + {'a': 1, 'b': 2, 'c': 3} + + >>> safe_dict_merge(dict(a=1, b=4), dict(b=2, c=3)) + Warning: Conflicting values for b: overwriting 2 with 4 + {'a': 1, 'b': 4, 'c': 3} + Args: - updates: dictionary to be added to base - base: base dictionary to be modified + updates: Dictionary to be added to base. + base: Base dictionary to be modified. Returns: - dict: updated base + dict: Updated copy of base. """ # Updates base with a copy of elements in updates for k, v in updates.items(): @@ -44,27 +53,33 @@ def safe_dict_merge(updates: dict, base: dict) -> dict: return base +P = ParamSpec("P") +T = TypeVar("T") + + def default_args(**defaults) -> Callable: """Decorate function to enumerate default arguments for collection.""" - def inner_dec(func) -> Callable: - def inner(*args, **kwargs) -> Callable: + def inner_dec(func: Callable[P, T]) -> Callable[P, T]: + @wraps(func) + def inner(*args: P.args, **kwargs: P.kwargs) -> T: return func(*args, **kwargs) inner.defaults = dict(**defaults) + return inner return inner_dec def collect_default_args(object: object) -> dict[str, Any]: - """Collect all function @default_args in an object. + """Collect all function :class:`default_args` in an object. Args: - object: object with @default_args decorated functions + object: Object with :class:`default_args`-decorated functions. Returns: - dict: dict of keyword-value pairs of default arguments + dict: Dict of keyword-value pairs of default arguments. """ defaults = {} for name in dir(object): @@ -101,6 +116,7 @@ def vectorize_nested_dict(dictionary: dict) -> tuple[list[str], np.ndarray]: def aliveness_checker(func: Callable[..., bool]) -> Callable[..., bool]: """Decorate function to evaluate when checking for satellite aliveness.""" + @wraps(func) def inner(*args, log_failure=False, **kwargs) -> bool: self = args[0] alive = func(*args, **kwargs) @@ -108,6 +124,10 @@ def inner(*args, log_failure=False, **kwargs) -> bool: self.satellite.log_info(f"failed {func.__name__} check") return alive + inner.__doc__ = ( + "*Decorated with* :class:`~bsk_rl.utils.functional.aliveness_checker`\n\n" + + str(func.__doc__) + ) inner.is_aliveness_checker = True return inner @@ -116,11 +136,11 @@ def check_aliveness_checkers(model: Any, log_failure=False) -> bool: """Evaluate all functions with @aliveness_checker in a model. Args: - model: Model to search for checkers in - log_failure: Whether to log on checker failure + model: Model to search for checkers in. + log_failure: Whether to log to the logger on checker failure. Returns: - bool: Model aliveness status + bool: Model aliveness status. """ is_alive = True for name in dir(model): @@ -135,62 +155,37 @@ def check_aliveness_checkers(model: Any, log_failure=False) -> bool: def is_property(obj: Any, attr_name: str) -> bool: - """Check if obj has an @property attr_name without calling it.""" + """Check if obj has an ``@property`` called ``attr_name`` without calling it.""" cls = type(obj) attribute = getattr(cls, attr_name, None) return attribute is not None and isinstance(attribute, property) -def configurable(cls): - """Class decorator to create class with different init defaults.""" - - @classmethod - def configure(cls, **config_kwargs): - class Configurable(cls): - def __init__(self, *args, **kwargs): - init_kwargs = deepcopy(config_kwargs) - for key in init_kwargs.keys(): - if not ( - key in inspect.getfullargspec(super().__init__).args - or key in inspect.getfullargspec(super().__init__).kwonlyargs - ): - raise KeyError( - f"{key} not a keyword argument for {cls.__name__}" - ) - for k, v in kwargs.items(): - init_kwargs[k] = v - super().__init__(*args, **init_kwargs) - - configcls = Configurable - return configcls - - cls.configure = configure - return cls - - -def bind(instance, func, as_name=None): - """Bind the function *func* to *instance*. - - Uses either provided name *as_name* or the existing name of *func*. The provided - *func* should accept the instance as the first argument, i.e. "self". - """ - if as_name is None: - as_name = func.__name__ - bound_method = func.__get__(instance, instance.__class__) - setattr(instance, as_name, bound_method) - return bound_method - - class AbstractClassProperty: def __init__(self): + """Assign a class property to act like an abstract field.""" self.__isabstractmethod__ = True - def __set_name__(self, owner, name): + def __set_name__(self, owner, name): # noqa self.name = name - def __get__(self, instance, owner): + def __get__(self, instance, owner): # noqa if instance is None: return self raise NotImplementedError( f"AbstractClassProperty '{self.name}' must be set in subclass" ) + + +__doc_title__ = "Functional" +__all__ = [ + "valid_func_name", + "safe_dict_merge", + "default_args", + "collect_default_args", + "vectorize_nested_dict", + "aliveness_checker", + "check_aliveness_checkers", + "is_property", + "AbstractClassProperty", +] diff --git a/src/bsk_rl/utils/logging_config.py b/src/bsk_rl/utils/logging_config.py index 87769c07..bf9a83ca 100644 --- a/src/bsk_rl/utils/logging_config.py +++ b/src/bsk_rl/utils/logging_config.py @@ -99,8 +99,8 @@ def format(self, record): sat_name = None sat_color = None try: - if record.name.split(".")[4] == "satellites": - sat_name = record.name.split(".")[5] + if record.name.split(".")[1] == "sats": + sat_name = record.name.split(".")[3] if sat_name not in self.satellite_colors: self.satellite_colors[sat_name] = len(self.satellite_colors) % len( sat_color_cycle @@ -109,7 +109,7 @@ def format(self, record): except IndexError: pass - record.shortname = ".".join(record.name.split(".")[3:]) + record.shortname = ".".join(record.name.split(".")[1:]) fstr += style_string("%(shortname)-30s ", color=sat_color, no_format=no_format) fstr += style_string( "%(levelname)-10s ", diff --git a/src/bsk_rl/utils/orbital.py b/src/bsk_rl/utils/orbital.py index 48a7eb01..9654d955 100644 --- a/src/bsk_rl/utils/orbital.py +++ b/src/bsk_rl/utils/orbital.py @@ -1,4 +1,4 @@ -"""Utilities for computing orbital events.""" +"""``bsk_rl.utils.orbital``:Utilities for computing orbital events.""" from typing import Iterable, Optional @@ -28,16 +28,17 @@ def random_orbit( ) -> ClassicElements: """Create a set of orbit elements. - Parameters are fixed if specified and randomized if None. + Parameters are fixed if specified and randomized if ``None``. Defaults to a random + circular orbit at 500 km altitude and 45 deg inclination. Args: - i: inclination [deg], randomized in [-pi, pi] - alt: altitude above r_body [km] - r_body: body radius [km] - e: eccentricity - Omega: LAN [deg], randomized in [0, 2pi] - omega: Argument of periapsis [deg], randomized in [0, 2pi] - f: true anomaly [deg], randomized in [0, 2pi] + i: [deg] Inclination, randomized in ``[-pi, pi]``. + alt: [km] Altitude above r_body. + r_body: [km] Body radius. + e: Eccentricity. + Omega: [deg] LAN, randomized in ``[0, 2pi]``. + omega: [deg] Argument of periapsis, randomized in ``[0, 2pi]``. + f: [deg] True anomaly, randomized in ``[0, 2pi]``. Returns: ClassicElements: orbital elements @@ -104,8 +105,8 @@ def elevation(r_sat: np.ndarray, r_target: np.ndarray) -> np.ndarray: """Find the elevation angle from a target to a satellite. Args: - r_sat: Satellite position(s) - r_target: Target position + r_sat: Satellite position(s). + r_target: Target position. Returns: Elevation angle(s) @@ -134,13 +135,13 @@ def walker_delta( """Compute the initial orbit conditions of a Walker-delta constellation. Args: - n_spacecraft: Number of spacecraft - n_planes: Number of orbital planes - rel_phasing: Relative phasing between planes [deg] - altitude: Altitude above Earth's surface [m] - inc: Inclination [deg] - clustersize: Number of spacecraft in each cluster - clusterspacing: Spacing between spacecraft in a cluster [deg] + n_spacecraft: Number of spacecraft. + n_planes: Number of orbital planes. + rel_phasing: [deg] Relative phasing between planes. + altitude: [m] Altitude above Earth's surface. + inc: [deg] Inclination. + clustersize: Number of spacecraft in each cluster. + clusterspacing: [deg] Spacing between spacecraft in a cluster. Returns: list: List of orbital elements @@ -193,11 +194,11 @@ def __init__( mu: Optional[float] = None, dt: float = 30.0, ) -> None: - """Initialize simulator conditions. + """Class for propagating trajectory using a point mass simulation. Simulated under the effect of Earth's gravity. Returns interpolators for - position as well as upcoming eclipse predictions. Specify either (rN, vN) or - (oe, mu). + position as well as upcoming eclipse predictions. Specify either ``(rN, vN)`` or + ``(oe, mu)``. Args: utc_init: Simulation start time. @@ -387,3 +388,29 @@ def __del__(self) -> None: self.gravFactory.unloadSpiceKernels() except AttributeError: pass + + +def lla2ecef(lat: float, long: float, radius: float): + """Project LLA to Earth Centered, Earth Fixed location. + + Args: + lat: [deg] + long: [deg] + radius: [any] + """ + lat = np.radians(lat) + long = np.radians(long) + return radius * np.array( + [np.cos(lat) * np.cos(long), np.cos(lat) * np.sin(long), np.sin(lat)] + ) + + +__doc_title__ = "Orbital" +__all__ = [ + "random_orbit", + "random_epoch", + "lla2ecef", + "elevation", + "walker_delta", + "TrajectorySimulator", +] diff --git a/src/bsk_rl/utils/rllib.py b/src/bsk_rl/utils/rllib.py new file mode 100644 index 00000000..35108f70 --- /dev/null +++ b/src/bsk_rl/utils/rllib.py @@ -0,0 +1,83 @@ +"""``bsk_rl.utils.rllib`` is a collection of utilities for working with RLlib.""" + +from typing import Any + +from ray.rllib.algorithms.callbacks import DefaultCallbacks + + +def unpack_config(env): + """Create a wrapped version of an env class that unpacks env_config from Ray into kwargs. + + Necessary when setting + + .. code-block:: python + + config.environment( + env=unpack_config(SatelliteTasking), + env_config=env_args + ) + + which generates environments that look like + + .. code-block:: python + + SatelliteTasking(**env_args) + + since RLlib expects the environment to take a dictionary called ``kwargs`` instead + of the actual arguments. + + """ + + class UnpackedEnv(env): + def __init__(self, env_config): + super().__init__(**env_config) + + UnpackedEnv.__name__ = f"{env.__name__}_Unpacked" + + return UnpackedEnv + + +class EpisodeDataCallbacks(DefaultCallbacks): + def __init__(self, *args, **kwargs): + """Log information at the end of each episode. + + Make a subclass of ``EpisodeDataCallbacks`` and override ``pull_env_metrics`` to + log environment-specific information at the end of each episode. + """ + super().__init__(*args, **kwargs) + + def pull_env_metrics(self, env) -> dict[str, float]: + """Log environment metrics at the end of each episode. + + This function is called whenever ``env`` is terminated or truncated. It should + return a dictionary of metrics to log. + + Args: + env: An environment that has completed. + """ + return {} + + def on_episode_end( + self, *, worker, base_env, policies, episode, env_index, **kwargs + ) -> None: + """Call pull_env_metrics and log the results. + + :meta private: + """ + env = base_env.vector_env.envs[0] # noqa: F841; how to access the environment + env_data = self.pull_env_metrics(env) + for k, v in env_data.items(): + episode.custom_metrics[k] = v + + def on_train_result(self, *, algorithm, result, **kwargs): + """Log frames per second. + + :meta private: + """ + result["fps"] = ( + result["num_env_steps_sampled_this_iter"] / result["time_this_iter_s"] + ) + + +__doc_title__ = "RLlib Utilities" +__all__ = ["unpack_config", "EpisodeDataCallbacks"] diff --git a/tests/examples/test_tutorials.py b/tests/examples/test_tutorials.py deleted file mode 100644 index 57e3cf50..00000000 --- a/tests/examples/test_tutorials.py +++ /dev/null @@ -1,13 +0,0 @@ -import pathlib -import runpy - -import pytest - -examples_path = pathlib.Path(__file__).parent / ".." / ".." / "examples" / "tutorials" -scripts = examples_path.resolve().glob("*.py") -script_names = {script.name: script for script in scripts} - - -@pytest.mark.parametrize("script", script_names) -def test_example_script(script): - runpy.run_path(script_names[script]) diff --git a/tests/integration/env/scenario/test_int_sat_actions.py b/tests/integration/act/test_int_actions.py similarity index 80% rename from tests/integration/env/scenario/test_int_sat_actions.py rename to tests/integration/act/test_int_actions.py index b907747c..2cf8acae 100644 --- a/tests/integration/env/scenario/test_int_sat_actions.py +++ b/tests/integration/act/test_int_actions.py @@ -2,12 +2,9 @@ import numpy as np from pytest import approx -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets, UniformNadirFeature -from bsk_rl.env.simulation import dynamics, environment, fsw +from bsk_rl import act, data, obs, sats +from bsk_rl.scene import UniformNadirScanning, UniformTargets +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit ######################### @@ -16,15 +13,15 @@ class TestImagingAndDownlink: - class ImageSat(sats.SteeringImagerSatellite): - dyn_type = dynamics.GroundStationDynModel + class ImageSat(sats.ImagingSatellite): + dyn_type = dyn.GroundStationDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Downlink(), act.Image(n_ahead_image=10)] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", sat_args=ImageSat.default_sat_args( oe=random_orbit, @@ -35,8 +32,8 @@ class ImageSat(sats.SteeringImagerSatellite): transmitterBaudRate=-1.0, ), ), - env_features=StaticTargets(n_targets=1000), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), sim_rate=1.0, time_limit=10000.0, max_step_duration=1e9, @@ -60,9 +57,9 @@ def test_downlink(self): def test_image_by_name(self): # Smoketest self.env.reset() - target = self.env.unwrapped.satellite.upcoming_targets(10)[9] + target = self.env.unwrapped.satellite.find_next_opportunities(n=10)[9]["target"] self.env.step(target) - target = self.env.unwrapped.satellite.upcoming_targets(10)[9] + target = self.env.unwrapped.satellite.find_next_opportunities(n=10)[9]["target"] self.env.step(target.id) assert True @@ -74,14 +71,14 @@ def test_image_by_name(self): class TestChargingAction: class ChargeSat(sats.Satellite): - dyn_type = dynamics.BasicDynamicsModel + dyn_type = dyn.BasicDynamicsModel fsw_type = fsw.BasicFSWModel observation_spec = [obs.Time()] action_spec = [act.Charge()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ChargeSat( + "SatelliteTasking-v1", + satellite=ChargeSat( "Charger", sat_args=ChargeSat.default_sat_args( # High, inclined orbit makes eclipse unlikely @@ -90,8 +87,8 @@ class ChargeSat(sats.Satellite): storedCharge_Init=250_000, ), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=300.0, time_limit=300.0, @@ -107,23 +104,23 @@ def test_charging_action(self): class TestDesatAction: class DesatSat(sats.Satellite): - dyn_type = dynamics.BasicDynamicsModel + dyn_type = dyn.BasicDynamicsModel fsw_type = fsw.BasicFSWModel observation_spec = [obs.Time()] action_spec = [act.Desat()] def make_env(self): return gym.make( - "SingleSatelliteTasking-v1", - satellites=self.DesatSat( + "SatelliteTasking-v1", + satellite=self.DesatSat( "Ellite", sat_args=self.DesatSat.default_sat_args( oe=random_orbit, wheelSpeeds=[1000.0, -1000.0, 1000.0], ), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=300.0, time_limit=1200.0, @@ -168,14 +165,14 @@ def test_desat_action_pointing(self): class TestNadirImagingActions: class ImageSat(sats.Satellite): - dyn_type = dynamics.ContinuousImagingDynModel + dyn_type = dyn.ContinuousImagingDynModel fsw_type = fsw.ContinuousImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Scan()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", sat_args=ImageSat.default_sat_args( oe=random_orbit, @@ -186,8 +183,8 @@ class ImageSat(sats.Satellite): transmitterBaudRate=-1.0, ), ), - env_features=UniformNadirFeature(), - data_manager=data.NoDataManager(), + scenario=UniformNadirScanning(), + rewarder=data.NoReward(), sim_rate=1.0, time_limit=10000.0, max_step_duration=1e9, diff --git a/tests/integration/env/scenario/test_int_communication.py b/tests/integration/comm/test_int_communication.py similarity index 87% rename from tests/integration/env/scenario/test_int_communication.py rename to tests/integration/comm/test_int_communication.py index ecdb07fa..a4d07385 100644 --- a/tests/integration/env/scenario/test_int_communication.py +++ b/tests/integration/comm/test_int_communication.py @@ -1,17 +1,14 @@ import gymnasium as gym -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.communication import ( +from bsk_rl import act, data, obs, sats +from bsk_rl.comm import ( FreeCommunication, LOSCommunication, LOSMultiCommunication, NoCommunication, ) -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.env.simulation import environment +from bsk_rl.scene import UniformTargets +from bsk_rl.sim.dyn import FullFeaturedDynModel from bsk_rl.utils.orbital import walker_delta oes_visible = walker_delta( @@ -35,12 +32,13 @@ ) -class FullFeaturedSatellite(sats.SteeringImagerSatellite): +class FullFeaturedSatellite(sats.ImagingSatellite): observation_spec = [ obs.SatProperties(dict(prop="r_BN_P", module="dynamics", norm=6e6)), obs.Time(), ] action_spec = [act.Image(n_ahead_image=10)] + dyn_type = FullFeaturedDynModel def make_communication_env(oes, comm_type): @@ -60,9 +58,9 @@ def make_communication_env(oes, comm_type): env = gym.make( "GeneralSatelliteTasking-v1", satellites=satellites, - env_features=StaticTargets(n_targets=1000), - data_manager=data.UniqueImagingManager(), - communicator=comm_type(satellites), + scenario=UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), + communicator=comm_type(), sim_rate=1.0, time_limit=5700.0, disable_env_checker=True, diff --git a/tests/integration/env/scenario/test_int_data.py b/tests/integration/data/test_int_data.py similarity index 100% rename from tests/integration/env/scenario/test_int_data.py rename to tests/integration/data/test_int_data.py diff --git a/tests/integration/env/simulation/test_int_environment.py b/tests/integration/env/simulation/test_int_environment.py deleted file mode 100644 index 06693f8b..00000000 --- a/tests/integration/env/simulation/test_int_environment.py +++ /dev/null @@ -1 +0,0 @@ -# For environment models not tested in other tests diff --git a/tests/integration/env/scenario/test_int_sat_observations.py b/tests/integration/obs/test_int_observations.py similarity index 77% rename from tests/integration/env/scenario/test_int_sat_observations.py rename to tests/integration/obs/test_int_observations.py index 2a3af7f7..a3f10d7a 100644 --- a/tests/integration/env/scenario/test_int_sat_observations.py +++ b/tests/integration/obs/test_int_observations.py @@ -2,12 +2,9 @@ import numpy as np from pytest import approx -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.env.simulation import dynamics, fsw +from bsk_rl import act, data, obs, sats +from bsk_rl.scene import UniformTargets +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit @@ -16,7 +13,7 @@ ############################## class TestComposedState: class ComposedPropSat(sats.ImagingSatellite): - dyn_type = dynamics.ImagingDynModel + dyn_type = dyn.ImagingDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [ obs.Time(), @@ -30,13 +27,13 @@ class ComposedPropSat(sats.ImagingSatellite): action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ComposedPropSat( + "SatelliteTasking-v1", + satellite=ComposedPropSat( "Explorer 1", sat_args=ComposedPropSat.default_sat_args(oe=random_orbit), ), - env_features=StaticTargets(n_targets=1000), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, @@ -59,7 +56,7 @@ def test_normd_property_state(self): class TestSatProperties: class SatPropertiesSat(sats.Satellite): - dyn_type = dynamics.BasicDynamicsModel + dyn_type = dyn.BasicDynamicsModel fsw_type = fsw.BasicFSWModel observation_spec = [ obs.SatProperties( @@ -70,15 +67,15 @@ class SatPropertiesSat(sats.Satellite): action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=SatPropertiesSat( + "SatelliteTasking-v1", + satellite=SatPropertiesSat( "Sputnik", sat_args=SatPropertiesSat.default_sat_args( oe=random_orbit(r_body=7000, alt=0) ), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, @@ -93,19 +90,19 @@ def test_normd_property_state(self): class TestTime: class TimedSat(sats.Satellite): - dyn_type = dynamics.BasicDynamicsModel + dyn_type = dyn.BasicDynamicsModel fsw_type = fsw.BasicFSWModel observation_spec = [obs.Time()] action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=TimedSat( + "SatelliteTasking-v1", + satellite=TimedSat( "Voyager", sat_args=TimedSat.default_sat_args(oe=random_orbit()), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, @@ -121,7 +118,7 @@ def test_normd_property_state(self): class TestOpportunityProperties: class TargetSat(sats.ImagingSatellite): - dyn_type = dynamics.ImagingDynModel + dyn_type = dyn.ImagingDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [ obs.OpportunityProperties(dict(prop="priority"), n_ahead_observe=2) @@ -129,14 +126,14 @@ class TargetSat(sats.ImagingSatellite): action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=TargetSat( + "SatelliteTasking-v1", + satellite=TargetSat( "Bullseye", obs_type=dict, sat_args=TargetSat.default_sat_args(oe=random_orbit()), ), - env_features=StaticTargets(n_targets=100), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=100), + rewarder=data.UniqueImageReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, @@ -151,20 +148,20 @@ def test_target_state(self): class TestEclipse: class EclipseSat(sats.Satellite): - dyn_type = dynamics.BasicDynamicsModel + dyn_type = dyn.BasicDynamicsModel fsw_type = fsw.BasicFSWModel observation_spec = [obs.Eclipse()] action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=EclipseSat( + "SatelliteTasking-v1", + satellite=EclipseSat( "PinkFloyd", obs_type=list, sat_args=EclipseSat.default_sat_args(oe=random_orbit()), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=5700.0, time_limit=5800.0, @@ -180,7 +177,7 @@ def test_eclipse_state(self): class TestGroundStationProperties: class GroundSat(sats.AccessSatellite): - dyn_type = dynamics.GroundStationDynModel + dyn_type = dyn.GroundStationDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [ obs.OpportunityProperties( @@ -194,14 +191,14 @@ class GroundSat(sats.AccessSatellite): action_spec = [act.Drift()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=GroundSat( + "SatelliteTasking-v1", + satellite=GroundSat( "Satellite", obs_type=list, sat_args=GroundSat.default_sat_args(oe=random_orbit()), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=5700.0, time_limit=5700.0, diff --git a/tests/integration/env/scenario/test_int_satellites.py b/tests/integration/sats/test_int_satellites.py similarity index 76% rename from tests/integration/env/scenario/test_int_satellites.py rename to tests/integration/sats/test_int_satellites.py index dc987f6f..010ee9eb 100644 --- a/tests/integration/env/scenario/test_int_satellites.py +++ b/tests/integration/sats/test_int_satellites.py @@ -2,25 +2,22 @@ import numpy as np from pytest import approx -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.env.simulation import dynamics, fsw +from bsk_rl import act, data, obs, sats +from bsk_rl.scene import UniformTargets +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit class TestImagingSatellite: class ImageSat(sats.ImagingSatellite): - dyn_type = dynamics.ImagingDynModel + dyn_type = dyn.ImagingDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Image(n_ahead_image=10)] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", initial_generation_duration=1000.0, generation_duration=100.0, @@ -30,8 +27,8 @@ class ImageSat(sats.ImagingSatellite): imageRateErrorRequirement=0.05, ), ), - env_features=StaticTargets(n_targets=5000), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=5000), + rewarder=data.UniqueImageReward(), sim_rate=1.0, time_limit=2000.0, max_step_duration=500.0, diff --git a/tests/integration/env/scenario/test_int_environment_features.py b/tests/integration/scene/test_int_scenarios.py similarity index 65% rename from tests/integration/env/scenario/test_int_environment_features.py rename to tests/integration/scene/test_int_scenarios.py index f779a7d7..c33caa5c 100644 --- a/tests/integration/env/scenario/test_int_environment_features.py +++ b/tests/integration/scene/test_int_scenarios.py @@ -1,24 +1,21 @@ import gymnasium as gym -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import CityTargets, StaticTargets -from bsk_rl.env.simulation import dynamics, environment, fsw +from bsk_rl import act, data, obs, sats +from bsk_rl.scene import CityTargets, UniformTargets +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit -def make_env(env_features): +def make_env(scenario): class ImageSat(sats.ImagingSatellite): - dyn_type = dynamics.GroundStationDynModel + dyn_type = dyn.GroundStationDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Image(n_ahead_image=10)] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", sat_args=ImageSat.default_sat_args( oe=random_orbit, @@ -26,8 +23,8 @@ class ImageSat(sats.ImagingSatellite): imageRateErrorRequirement=0.05, ), ), - env_features=env_features, - data_manager=data.UniqueImagingManager(), + scenario=scenario, + rewarder=data.UniqueImageReward(), sim_rate=1.0, time_limit=5700.0, max_step_duration=1e9, @@ -37,9 +34,9 @@ class ImageSat(sats.ImagingSatellite): return env -class TestStaticTargets: +class TestUniformTargets: def test_priority_dist(self): - env = make_env(StaticTargets(n_targets=1000, priority_distribution=lambda: 1)) + env = make_env(UniformTargets(n_targets=1000, priority_distribution=lambda: 1)) env.reset() for i in range(10): observation, reward, terminated, truncated, info = env.step(i) diff --git a/tests/integration/env/simulation/test_int_dynamics.py b/tests/integration/sim/test_int_dynamics.py similarity index 74% rename from tests/integration/env/simulation/test_int_dynamics.py rename to tests/integration/sim/test_int_dynamics.py index 81b60363..ae9b50f8 100644 --- a/tests/integration/env/simulation/test_int_dynamics.py +++ b/tests/integration/sim/test_int_dynamics.py @@ -1,18 +1,10 @@ import gymnasium as gym import pytest -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.env.simulation import dynamics, fsw +from bsk_rl import act, data, obs, sats, scene +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit -########################### -# Composed Dynamics Tests # -########################### - class TestImagingDynModelStorage: @@ -29,14 +21,14 @@ class TestImagingDynModelStorage: def test_storageInit(self, storage_capacity, initial_storage): class ImageSat(sats.ImagingSatellite): - dyn_type = dynamics.ImagingDynModel + dyn_type = dyn.ImagingDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Downlink(), act.Image(n_ahead_image=10)] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", sat_args=ImageSat.default_sat_args( oe=random_orbit, @@ -44,8 +36,8 @@ class ImageSat(sats.ImagingSatellite): storageInit=initial_storage, ), ), - env_features=StaticTargets(n_targets=1000), - data_manager=data.NoDataManager(), + scenario=scene.UniformTargets(n_targets=1000), + rewarder=data.NoReward(), sim_rate=1.0, time_limit=10000.0, max_step_duration=1e9, @@ -69,14 +61,14 @@ class ImageSat(sats.ImagingSatellite): def test_storageInit_downlink(self, storage_capacity, initial_storage): class ImageSat(sats.ImagingSatellite): - dyn_type = dynamics.FullFeaturedDynModel + dyn_type = dyn.FullFeaturedDynModel fsw_type = fsw.ImagingFSWModel observation_spec = [obs.Time()] action_spec = [act.Downlink()] env = gym.make( - "SingleSatelliteTasking-v1", - satellites=ImageSat( + "SatelliteTasking-v1", + satellite=ImageSat( "EO-1", sat_args=ImageSat.default_sat_args( oe=random_orbit, @@ -84,8 +76,8 @@ class ImageSat(sats.ImagingSatellite): storageInit=initial_storage, ), ), - env_features=StaticTargets(n_targets=1000), - data_manager=data.NoDataManager(), + scenario=scene.UniformTargets(n_targets=1000), + rewarder=data.NoReward(), sim_rate=1.0, time_limit=10000.0, max_step_duration=1e9, diff --git a/tests/integration/env/simulation/test_int_fsw.py b/tests/integration/sim/test_int_fsw.py similarity index 100% rename from tests/integration/env/simulation/test_int_fsw.py rename to tests/integration/sim/test_int_fsw.py diff --git a/tests/integration/sim/test_int_world.py b/tests/integration/sim/test_int_world.py new file mode 100644 index 00000000..6f73c2f7 --- /dev/null +++ b/tests/integration/sim/test_int_world.py @@ -0,0 +1 @@ +# For world models not tested in other tests diff --git a/tests/integration/env/test_int_full_environments.py b/tests/integration/test_int_full_environments.py similarity index 80% rename from tests/integration/env/test_int_full_environments.py rename to tests/integration/test_int_full_environments.py index 707d3f39..1cf452cc 100644 --- a/tests/integration/env/test_int_full_environments.py +++ b/tests/integration/test_int_full_environments.py @@ -1,20 +1,15 @@ from warnings import warn import gymnasium as gym +import numpy as np import pytest from pettingzoo.test.parallel_test import parallel_api_test -from bsk_rl.env.gym_env import MultiagentSatelliteTasking -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets -from bsk_rl.env.simulation import environment +from bsk_rl import ConstellationTasking, act, data, obs, sats, scene from bsk_rl.utils.orbital import random_orbit -class FullFeaturedSatellite(sats.SteeringImagerSatellite): +class FullFeaturedSatellite(sats.ImagingSatellite): observation_spec = [ obs.SatProperties(dict(prop="r_BN_P", module="dynamics", norm=6e6)), obs.Time(), @@ -42,15 +37,15 @@ class FullFeaturedSatellite(sats.SteeringImagerSatellite): ), ), ], - env_features=StaticTargets(n_targets=1000), - data_manager=data.UniqueImagingManager(), + scenario=scene.UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), sim_rate=0.5, max_step_duration=1e9, time_limit=5700.0, disable_env_checker=True, ) -parallel_env = MultiagentSatelliteTasking( +parallel_env = ConstellationTasking( satellites=[ FullFeaturedSatellite( "Sentinel-2A", @@ -69,8 +64,8 @@ class FullFeaturedSatellite(sats.SteeringImagerSatellite): ), ), ], - env_features=StaticTargets(n_targets=1000), - data_manager=data.UniqueImagingManager(), + scenario=scene.UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), sim_rate=0.5, max_step_duration=1e9, time_limit=5700.0, @@ -93,7 +88,7 @@ def test_reproducibility(env): reward_sum_2 = 0 observation_2, info = env.reset(seed=0) for o1, o2 in zip(observation_1, observation_2): - assert abs(o1 - o2 < 1e-6).all() + np.testing.assert_allclose(o1, o2) for action in actions: observation, reward, terminated, truncated, info = env.step(action) diff --git a/tests/integration/env/test_int_gym_env.py b/tests/integration/test_int_gym_env.py similarity index 81% rename from tests/integration/env/test_int_gym_env.py rename to tests/integration/test_int_gym_env.py index 04198c41..4d9d50b5 100644 --- a/tests/integration/env/test_int_gym_env.py +++ b/tests/integration/test_int_gym_env.py @@ -2,28 +2,25 @@ import numpy as np from gymnasium import spaces -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario import data -from bsk_rl.env.scenario import observations as obs -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import StaticTargets +from bsk_rl import act, data, obs, sats +from bsk_rl.scene import UniformTargets from bsk_rl.utils.orbital import random_orbit -class DoNothingSatellite(sats.SteeringImagerSatellite): +class DoNothingSatellite(sats.ImagingSatellite): observation_spec = [obs.Time()] action_spec = [act.Drift()] -class TestSingleSatelliteTasking: +class TestSatelliteTasking: env = gym.make( - "SingleSatelliteTasking-v1", - satellites=DoNothingSatellite( + "SatelliteTasking-v1", + satellite=DoNothingSatellite( "Sputnik", sat_args=DoNothingSatellite.default_sat_args(oe=random_orbit), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, @@ -53,10 +50,10 @@ def test_truncate(self): def test_repeatable(self): self.env.reset(seed=0) - env_args_old = self.env.unwrapped.env_args + world_args_old = self.env.unwrapped.world_args sat_args_old = self.env.unwrapped.satellite.sat_args self.env.reset(seed=0) - assert self.env.unwrapped.env_args == env_args_old + assert self.env.unwrapped.world_args == world_args_old for val, val_old in zip( self.env.unwrapped.satellite.sat_args.values(), sat_args_old.values() ): @@ -70,15 +67,15 @@ def test_repeatable(self): class TestSingleSatelliteDeath: env = gym.make( - "SingleSatelliteTasking-v1", - satellites=DoNothingSatellite( + "SatelliteTasking-v1", + satellite=DoNothingSatellite( "Skydiver", sat_args=DoNothingSatellite.default_sat_args( rN=[0, 0, 7e6], vN=[0, 0, -100.0], oe=None ), ), - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, time_limit=1000.0, failure_penalty=-1000, @@ -108,8 +105,8 @@ class TestGeneralSatelliteTasking: sat_args=DoNothingSatellite.default_sat_args(oe=random_orbit), ), ], - env_features=StaticTargets(n_targets=0), - data_manager=data.NoDataManager(), + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), sim_rate=1.0, max_step_duration=10.0, time_limit=100.0, diff --git a/tests/unittest/env/scenario/test_actions.py b/tests/unittest/act/test_actions.py similarity index 89% rename from tests/unittest/env/scenario/test_actions.py rename to tests/unittest/act/test_actions.py index f37c5d79..a5db9891 100644 --- a/tests/unittest/env/scenario/test_actions.py +++ b/tests/unittest/act/test_actions.py @@ -1,23 +1,23 @@ from unittest.mock import MagicMock, call, patch -import pytest from gymnasium import spaces -from bsk_rl.env.scenario import actions as act -from bsk_rl.env.scenario.environment_features import Target +from bsk_rl import act +from bsk_rl.act.actions import ActionBuilder +from bsk_rl.act.discrete_actions import DiscreteActionBuilder -@patch.multiple(act.ActionBuilder, __abstractmethods__=set()) +@patch.multiple(ActionBuilder, __abstractmethods__=set()) class TestActionBuilder: def test_init(self): action_spec = [MagicMock() for _ in range(3)] satellite = MagicMock(action_spec=action_spec) - ab = act.ActionBuilder(satellite) + ab = ActionBuilder(satellite) for a in ab.action_spec: a.link_satellite.assert_called_once() def test_reset_post_sim(self): - ab = act.ActionBuilder(MagicMock(action_spec=[MagicMock() for _ in range(3)])) + ab = ActionBuilder(MagicMock(action_spec=[MagicMock() for _ in range(3)])) ab.reset_post_sim() for a in ab.action_spec: a.link_simulator.assert_called_once() @@ -29,7 +29,7 @@ def test_action_space(self): satellite = MagicMock( action_spec=[MagicMock(n_actions=1), MagicMock(n_actions=2)] ) - ab = act.DiscreteActionBuilder(satellite) + ab = DiscreteActionBuilder(satellite) assert ab.action_space == spaces.Discrete(3) def test_action_description(self): @@ -41,7 +41,7 @@ def test_action_description(self): ) satellite.action_spec[0].name = "foo" satellite.action_spec[1].name = "bar" - ab = act.DiscreteActionBuilder(satellite) + ab = DiscreteActionBuilder(satellite) assert ab.action_description == ["foo", "bar_0", "bar_1"] def test_set_action(self): @@ -52,7 +52,7 @@ def test_set_action(self): MagicMock(n_actions=1, set_action=MagicMock(return_value="baz")), ] ) - ab = act.DiscreteActionBuilder(satellite) + ab = DiscreteActionBuilder(satellite) ab.set_action(0) assert ab.action_spec[0].set_action.call_args == call(0, prev_action_key=None) ab.set_action(1) @@ -69,7 +69,7 @@ def test_set_action_override(self): MagicMock(n_actions=2, set_action_override=MagicMock()), ] ) - ab = act.DiscreteActionBuilder(satellite) + ab = DiscreteActionBuilder(satellite) ab.set_action("foo") assert ab.action_spec[1].set_action_override.call_args == call( "foo", prev_action_key=None diff --git a/tests/unittest/env/scenario/test_communication.py b/tests/unittest/comm/test_communication.py similarity index 74% rename from tests/unittest/env/scenario/test_communication.py rename to tests/unittest/comm/test_communication.py index 7dc951ce..64580fcd 100644 --- a/tests/unittest/env/scenario/test_communication.py +++ b/tests/unittest/comm/test_communication.py @@ -2,22 +2,23 @@ import pytest -from bsk_rl.env.scenario.communication import ( +from bsk_rl.comm import ( CommunicationMethod, FreeCommunication, LOSCommunication, MultiDegreeCommunication, NoCommunication, ) -from bsk_rl.env.simulation.dynamics import LOSCommDynModel +from bsk_rl.sim.dyn import LOSCommDynModel @patch.multiple(CommunicationMethod, __abstractmethods__=set()) class TestCommunicationMethod: def test_communicate(self): mock_sats = [MagicMock(), MagicMock()] - comms = CommunicationMethod(mock_sats) - comms._communication_pairs = MagicMock( + comms = CommunicationMethod() + comms.link_satellites(mock_sats) + comms.communication_pairs = MagicMock( return_value=[(mock_sats[1], mock_sats[0])] ) comms.communicate() @@ -28,13 +29,14 @@ def test_communicate(self): mock_sats[0].data_store.data ) for sat in mock_sats: - sat.data_store.communication_update.assert_called_once() + sat.data_store.update_with_communicated_data.assert_called_once() class TestNoCommunication: def test_communicate(self): mock_sats = [MagicMock(), MagicMock()] - comms = NoCommunication(mock_sats) + comms = NoCommunication() + comms.link_satellites(mock_sats) comms.communicate() mock_sats[0].data_store.stage_communicated_data.assert_not_called() @@ -42,8 +44,9 @@ def test_communicate(self): class TestFreeCommunication: def test_communication_pairs(self): mock_sats = [MagicMock() for i in range(4)] - comms = FreeCommunication(mock_sats) - pairs = comms._communication_pairs() + comms = FreeCommunication() + comms.link_satellites(mock_sats) + pairs = comms.communication_pairs() for sat1, sat2 in zip(mock_sats, mock_sats): if sat1 != sat2: assert (sat1, sat2) in pairs or (sat2, sat1) in pairs @@ -52,12 +55,14 @@ def test_communication_pairs(self): class TestLOSCommunication: def test_dyn_model_check_valid(self): mock_sats = [MagicMock(dyn_type=LOSCommDynModel) for i in range(3)] - LOSCommunication(mock_sats) + comms = LOSCommunication() + comms.link_satellites(mock_sats) def test_dyn_model_check_invalid(self): mock_sats = [MagicMock(dyn_type="NotLOSComm") for i in range(3)] with pytest.raises(Exception): - LOSCommunication(mock_sats) + comms = LOSCommunication() + comms.link_satellites(mock_sats) class LOSDynMock(MagicMock, LOSCommDynModel): pass @@ -67,8 +72,9 @@ def test_reset(self): MagicMock(dyn_type=LOSCommDynModel, dynamics=self.LOSDynMock()) for i in range(3) ] - comms = LOSCommunication(mock_sats) - comms.reset() + comms = LOSCommunication() + comms.link_satellites(mock_sats) + comms.reset_post_sim() for sat1 in mock_sats: assert sat1 in comms.los_logs for sat2 in mock_sats: @@ -89,23 +95,25 @@ def test_reset(self): ) def test_communication_pairs(self, access1, access2, access): mock_sats = [MagicMock(dyn_type=LOSCommDynModel) for i in range(2)] - comms = LOSCommunication(mock_sats) + comms = LOSCommunication() + comms.link_satellites(mock_sats) comms.los_logs = { mock_sats[0]: {mock_sats[1]: MagicMock(hasAccess=access1)}, mock_sats[1]: {mock_sats[0]: MagicMock(hasAccess=access2)}, } if access: - assert len(comms._communication_pairs()) >= 1 + assert len(comms.communication_pairs()) >= 1 else: - assert len(comms._communication_pairs()) == 0 + assert len(comms.communication_pairs()) == 0 @patch( - "bsk_rl.env.scenario.communication.CommunicationMethod.communicate", + "bsk_rl.comm.CommunicationMethod.communicate", MagicMock(), ) def test_communicate(self): mock_sats = [MagicMock(dyn_type=LOSCommDynModel) for i in range(2)] - comms = LOSCommunication(mock_sats) + comms = LOSCommunication() + comms.link_satellites(mock_sats) loggerA = MagicMock() loggerB = MagicMock() comms.los_logs = { @@ -118,7 +126,7 @@ def test_communicate(self): class TestMultiDegreeCommunication: - @patch("bsk_rl.env.scenario.communication.CommunicationMethod._communication_pairs") + @patch("bsk_rl.comm.CommunicationMethod.communication_pairs") def test_communication_pairs(self, mock_pairs): mock_sats = [MagicMock() for i in range(6)] mock_pairs.return_value = [ @@ -126,8 +134,9 @@ def test_communication_pairs(self, mock_pairs): (mock_sats[1], mock_sats[3]), (mock_sats[3], mock_sats[4]), ] - comms = MultiDegreeCommunication(mock_sats) - pairs = comms._communication_pairs() + comms = MultiDegreeCommunication() + comms.link_satellites(mock_sats) + pairs = comms.communication_pairs() assert (mock_sats[0], mock_sats[4]) in pairs assert (mock_sats[0], mock_sats[5]) not in pairs diff --git a/tests/unittest/data/test_data.py b/tests/unittest/data/test_data.py new file mode 100644 index 00000000..e699b914 --- /dev/null +++ b/tests/unittest/data/test_data.py @@ -0,0 +1,289 @@ +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +from pytest import approx + +from bsk_rl.data.base import Data, DataStore, GlobalReward +from bsk_rl.data.nadir_data import ScanningTime, ScanningTimeReward, ScanningTimeStore +from bsk_rl.data.no_data import NoData, NoDataStore, NoReward +from bsk_rl.data.unique_image_data import ( + UniqueImageData, + UniqueImageReward, + UniqueImageStore, +) + + +@patch.multiple(DataStore, __abstractmethods__=set()) +class TestDataStore: + def test_init(self): + # Essentially a smoketest + DataStore.data_type = MagicMock + ds = DataStore(MagicMock()) + ds.get_log_state() + + def test_update_from_logs(self): + # Essentially a smoketest + DataStore.data_type = MagicMock + ds = DataStore(MagicMock()) + ds.update_from_logs() + ds.update_from_logs() + + def test_update_with_communicated_data(self): + DataStore.data_type = MagicMock + ds = DataStore(MagicMock()) + ds.data = [] + ds.stage_communicated_data([100]) + ds.update_with_communicated_data() + assert ds.data == [100] + + +@patch.multiple(GlobalReward, __abstractmethods__=set()) +class TestGlobalReward: + def test_reset(self): + GlobalReward.datastore_type = MagicMock() + dm = GlobalReward() + dm.reset_pre_sim() + assert dm.cum_reward == {} + + def test_create_data_store(self): + sat = MagicMock() + GlobalReward.datastore_type = MagicMock(return_value="ds") + dm = GlobalReward() + dm.scenario = MagicMock() + dm.reset_pre_sim() + dm.create_data_store(sat) + assert sat.data_store == "ds" + assert sat.id in dm.cum_reward + + def test_reward(self): + dm = GlobalReward() + dm.calculate_reward = MagicMock(return_value={"sat": 10.0}) + dm.cum_reward = {"sat": 5.0} + assert {"sat": 10.0} == dm.reward({"sat": "data"}) + assert dm.cum_reward == {"sat": 15.0} + + +class TestNoData: + def test_add(self): + dat1 = NoData() + dat2 = NoData() + dat = dat1 + dat2 + assert isinstance(dat, NoData) + + +class TestNoDataStore: + def test_compare_log_states(self): + ds = NoDataStore(MagicMock()) + assert isinstance(ds.compare_log_states(0, 1), Data) + + +class TestNoGlobalReward: + def test_calculate_reward(self): + dm = NoReward() + reward = dm.calculate_reward({"sat1": 0, "sat2": 1}) + assert reward == {"sat1": 0.0, "sat2": 0.0} + + +class TestUniqueImageData: + def test_identify_duplicates(self): + dat1 = UniqueImageData([1, 1, 2]) + assert dat1.duplicates == 1 + + def test_add_null(self): + dat1 = UniqueImageData() + dat2 = UniqueImageData() + dat = dat1 + dat2 + assert dat.imaged == [] + assert dat.duplicates == 0 + + def test_add_to_null(self): + dat1 = UniqueImageData(imaged=[1, 2]) + dat2 = UniqueImageData() + dat = dat1 + dat2 + assert dat.imaged == [1, 2] + assert dat.duplicates == 0 + + def test_add(self): + dat1 = UniqueImageData(imaged=[1, 2]) + dat2 = UniqueImageData(imaged=[3, 4]) + dat = dat1 + dat2 + assert dat.imaged == [1, 2, 3, 4] + assert dat.duplicates == 0 + + def test_add_duplicates(self): + dat1 = UniqueImageData(imaged=[1, 2]) + dat2 = UniqueImageData(imaged=[2, 3]) + dat = dat1 + dat2 + assert dat.imaged == [1, 2, 3] + assert dat.duplicates == 1 + + def test_add_duplicates_existing(self): + dat1 = UniqueImageData(imaged=[1, 2], duplicates=2) + dat2 = UniqueImageData(imaged=[2, 3], duplicates=3) + dat = dat1 + dat2 + assert dat.imaged == [1, 2, 3] + assert dat.duplicates == 6 + + +class TestUniqueImageStore: + def test_get_log_state(self): + sat = MagicMock() + sat.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData = [1, 2, 3] + ds = UniqueImageStore(sat) + assert (ds.get_log_state() == np.array([1, 2, 3])).all() + + @pytest.mark.parametrize( + "before,after,imaged", + [ + ([0, 0, 0], [0, 0, 0], []), + ([0, 0, 1], [0, 0, 1], []), + ([0, 0, 1], [0, 0, 0], []), + ([0, 0, 0], [1, 0, 0], [0]), + ([0, 0, 0], [0, 1, 1], [1, 2]), + ], + ) + def test_compare_log_states(self, before, after, imaged): + sat = MagicMock() + targets = [MagicMock() for i in range(3)] + ds = UniqueImageStore(sat) + ds.data.known = targets + message = sat.dynamics.storageUnit.storageUnitDataOutMsg + message.read.return_value.storedDataName.__getitem__.side_effect = ( + lambda x: targets[x].id + ) + dat = ds.compare_log_states(np.array(before), np.array(after)) + assert len(dat.imaged) == len(imaged) + for i in imaged: + assert targets[i] in dat.imaged + + +class TestUniqueImagingManager: + def test_calculate_reward(self): + dm = UniqueImageReward() + dm.data = UniqueImageData([]) + reward = dm.calculate_reward( + { + "sat1": UniqueImageData([MagicMock(priority=0.1)]), + "sat2": UniqueImageData([MagicMock(priority=0.2)]), + } + ) + assert reward == {"sat1": approx(0.1), "sat2": approx(0.2)} + + def test_calculate_reward_existing(self): + tgt = MagicMock(priority=0.2) + dm = UniqueImageReward() + dm.data = UniqueImageData([tgt]) + reward = dm.calculate_reward( + { + "sat1": UniqueImageData([MagicMock(priority=0.1)]), + "sat2": UniqueImageData([tgt]), + } + ) + assert reward == {"sat1": approx(0.1), "sat2": 0.0} + + def test_calculate_reward_repeated(self): + tgt = MagicMock(priority=0.2) + dm = UniqueImageReward() + dm.data = UniqueImageData([]) + reward = dm.calculate_reward( + { + "sat1": UniqueImageData([tgt]), + "sat2": UniqueImageData([tgt]), + } + ) + assert reward == {"sat1": approx(0.1), "sat2": approx(0.1)} + + def test_calculate_reward_custom_fn(self): + dm = UniqueImageReward(reward_fn=lambda x: 1 / x) + dm.data = UniqueImageData([]) + reward = dm.calculate_reward( + { + "sat1": UniqueImageData([MagicMock(priority=1)]), + "sat2": UniqueImageData([MagicMock(priority=2)]), + } + ) + assert reward == {"sat1": approx(1.0), "sat2": 0.5} + + +class TestNadirScanningTimeData: + def test_add_null(self): + dat1 = ScanningTime() + dat2 = ScanningTime() + dat = dat1 + dat2 + assert dat.scanning_time == 0.0 + + def test_add_to_null(self): + dat1 = ScanningTime(1.0) + dat2 = ScanningTime() + dat = dat1 + dat2 + assert dat.scanning_time == 1.0 + + def test_add(self): + dat1 = ScanningTime(1.0) + dat2 = ScanningTime(3.0) + dat = dat1 + dat2 + assert dat.scanning_time == 4.0 + + +class TestScanningNadirTimeStore: + def test_get_log_state(self): + sat = MagicMock() + sat.dynamics.storageUnit.storageUnitDataOutMsg.read().storageLevel = 6 + ds = ScanningTimeStore(sat) + assert ds.get_log_state() == 6.0 + + @pytest.mark.parametrize( + "before,after,new_time", + [ + (0, 3, 1), + (3, 6, 1), + (1, 1, 0), + (0, 6, 2), + ], + ) + def test_compare_log_states(self, before, after, new_time): + sat = MagicMock() + ds = ScanningTimeStore(sat) + sat.dynamics.instrument.nodeBaudRate = 3 + dat = ds.compare_log_states(before, after) + assert dat.scanning_time == new_time + + +class TestNadirScanningManager: + def test_calculate_reward(self): + dm = ScanningTimeReward() + dm.scenario = MagicMock() + dm.data = ScanningTime([]) + dm.scenario.value_per_second = 1.0 + reward = dm.calculate_reward( + { + "sat1": ScanningTime(1), + "sat2": ScanningTime(2), + } + ) + assert reward == {"sat1": 1.0, "sat2": 2.0} + + def test_calculate_reward_existing(self): + dm = ScanningTimeReward() + dm.scenario = MagicMock() + dm.data = ScanningTime(1) + dm.scenario.value_per_second = 1.0 + reward = dm.calculate_reward( + { + "sat1": ScanningTime(2), + "sat2": ScanningTime(3), + } + ) + assert reward == {"sat1": 2.0, "sat2": 3.0} + + def test_calculate_reward_custom_fn(self): + dm = ScanningTimeReward(reward_fn=lambda x: 1 / x) + dm.data = ScanningTime([]) + reward = dm.calculate_reward( + { + "sat1": ScanningTime(2), + "sat2": ScanningTime(2), + } + ) + assert reward == {"sat1": 0.5, "sat2": 0.5} diff --git a/tests/unittest/env/scenario/test_data.py b/tests/unittest/env/scenario/test_data.py deleted file mode 100644 index 1f5cc156..00000000 --- a/tests/unittest/env/scenario/test_data.py +++ /dev/null @@ -1,280 +0,0 @@ -from unittest.mock import MagicMock, patch - -import numpy as np -import pytest -from pytest import approx - -from bsk_rl.env.scenario import data - - -@patch.multiple(data.DataStore, __abstractmethods__=set()) -class TestDataStore: - def test_init(self): - # Essentially a smoketest - data.DataStore.DataType = MagicMock - ds = data.DataStore(MagicMock(), MagicMock()) - ds._clear_logs() - ds._get_log_state() - - def test_internal_update(self): - # Essentially a smoketest - data.DataStore.DataType = MagicMock - ds = data.DataStore(MagicMock(), MagicMock()) - ds.internal_update() - ds.internal_update() - - def test_communication_update(self): - data.DataStore.DataType = MagicMock - ds = data.DataStore(MagicMock(), MagicMock()) - ds.data = [] - ds.stage_communicated_data([100]) - ds.communication_update() - assert ds.data == [100] - - -@patch.multiple(data.DataManager, __abstractmethods__=set()) -class TestDataManager: - def test_reset(self): - data.DataManager.DataStore = MagicMock() - dm = data.DataManager(MagicMock()) - dm.reset() - assert dm.cum_reward == {} - - def test_create_data_store(self): - sat = MagicMock() - data.DataManager.DataStore = MagicMock(return_value="ds") - dm = data.DataManager(MagicMock()) - dm.reset() - dm.create_data_store(sat) - assert sat.data_store == "ds" - assert sat.id in dm.cum_reward - - def test_reward(self): - dm = data.DataManager(MagicMock()) - dm._calc_reward = MagicMock(return_value={"sat": 10.0}) - dm.cum_reward = {"sat": 5.0} - assert {"sat": 10.0} == dm.reward({"sat": "data"}) - assert dm.cum_reward == {"sat": 15.0} - - -class TestNoData: - def test_add(self): - dat1 = data.NoData() - dat2 = data.NoData() - dat = dat1 + dat2 - assert isinstance(dat, data.NoData) - - -class TestNoDataStore: - def test_compare_log_states(self): - ds = data.NoDataStore(MagicMock(), MagicMock()) - assert isinstance(ds._compare_log_states(0, 1), data.DataType) - - -class TestNoDataManager: - def test_calc_reward(self): - dm = data.NoDataManager(MagicMock()) - reward = dm._calc_reward({"sat1": 0, "sat2": 1}) - assert reward == {"sat1": 0.0, "sat2": 0.0} - - -class TestUniqueImageData: - def test_identify_duplicates(self): - dat1 = data.UniqueImageData([1, 1, 2]) - assert dat1.duplicates == 1 - - def test_add_null(self): - dat1 = data.UniqueImageData() - dat2 = data.UniqueImageData() - dat = dat1 + dat2 - assert dat.imaged == [] - assert dat.duplicates == 0 - - def test_add_to_null(self): - dat1 = data.UniqueImageData(imaged=[1, 2]) - dat2 = data.UniqueImageData() - dat = dat1 + dat2 - assert dat.imaged == [1, 2] - assert dat.duplicates == 0 - - def test_add(self): - dat1 = data.UniqueImageData(imaged=[1, 2]) - dat2 = data.UniqueImageData(imaged=[3, 4]) - dat = dat1 + dat2 - assert dat.imaged == [1, 2, 3, 4] - assert dat.duplicates == 0 - - def test_add_duplicates(self): - dat1 = data.UniqueImageData(imaged=[1, 2]) - dat2 = data.UniqueImageData(imaged=[2, 3]) - dat = dat1 + dat2 - assert dat.imaged == [1, 2, 3] - assert dat.duplicates == 1 - - def test_add_duplicates_existing(self): - dat1 = data.UniqueImageData(imaged=[1, 2], duplicates=2) - dat2 = data.UniqueImageData(imaged=[2, 3], duplicates=3) - dat = dat1 + dat2 - assert dat.imaged == [1, 2, 3] - assert dat.duplicates == 6 - - -class TestUniqueImageStore: - def test_get_log_state(self): - sat = MagicMock() - sat.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData = [1, 2, 3] - ds = data.UniqueImageStore(MagicMock(), sat) - assert (ds._get_log_state() == np.array([1, 2, 3])).all() - - @pytest.mark.parametrize( - "before,after,imaged", - [ - ([0, 0, 0], [0, 0, 0], []), - ([0, 0, 1], [0, 0, 1], []), - ([0, 0, 1], [0, 0, 0], []), - ([0, 0, 0], [1, 0, 0], [0]), - ([0, 0, 0], [0, 1, 1], [1, 2]), - ], - ) - def test_compare_log_states(self, before, after, imaged): - sat = MagicMock() - targets = [MagicMock() for i in range(3)] - ds = data.UniqueImageStore(MagicMock(), sat) - ds.env_knowledge = MagicMock(targets=targets) - message = sat.dynamics.storageUnit.storageUnitDataOutMsg - message.read.return_value.storedDataName.__getitem__.side_effect = ( - lambda x: targets[x].id - ) - dat = ds._compare_log_states(np.array(before), np.array(after)) - assert len(dat.imaged) == len(imaged) - for i in imaged: - assert targets[i] in dat.imaged - - -class TestUniqueImagingManager: - def test_calc_reward(self): - dm = data.UniqueImagingManager(MagicMock()) - dm.data = data.UniqueImageData([]) - reward = dm._calc_reward( - { - "sat1": data.UniqueImageData([MagicMock(priority=0.1)]), - "sat2": data.UniqueImageData([MagicMock(priority=0.2)]), - } - ) - assert reward == {"sat1": approx(0.1), "sat2": approx(0.2)} - - def test_calc_reward_existing(self): - tgt = MagicMock(priority=0.2) - dm = data.UniqueImagingManager(MagicMock()) - dm.data = data.UniqueImageData([tgt]) - reward = dm._calc_reward( - { - "sat1": data.UniqueImageData([MagicMock(priority=0.1)]), - "sat2": data.UniqueImageData([tgt]), - } - ) - assert reward == {"sat1": approx(0.1), "sat2": 0.0} - - def test_calc_reward_repeated(self): - tgt = MagicMock(priority=0.2) - dm = data.UniqueImagingManager(MagicMock()) - dm.data = data.UniqueImageData([]) - reward = dm._calc_reward( - { - "sat1": data.UniqueImageData([tgt]), - "sat2": data.UniqueImageData([tgt]), - } - ) - assert reward == {"sat1": approx(0.1), "sat2": approx(0.1)} - - def test_calc_reward_custom_fn(self): - dm = data.UniqueImagingManager(MagicMock(), reward_fn=lambda x: 1 / x) - dm.data = data.UniqueImageData([]) - reward = dm._calc_reward( - { - "sat1": data.UniqueImageData([MagicMock(priority=1)]), - "sat2": data.UniqueImageData([MagicMock(priority=2)]), - } - ) - assert reward == {"sat1": approx(1.0), "sat2": 0.5} - - -class TestNadirScanningTimeData: - def test_add_null(self): - dat1 = data.NadirScanningTimeData() - dat2 = data.NadirScanningTimeData() - dat = dat1 + dat2 - assert dat.scanning_time == 0.0 - - def test_add_to_null(self): - dat1 = data.NadirScanningTimeData(1.0) - dat2 = data.NadirScanningTimeData() - dat = dat1 + dat2 - assert dat.scanning_time == 1.0 - - def test_add(self): - dat1 = data.NadirScanningTimeData(1.0) - dat2 = data.NadirScanningTimeData(3.0) - dat = dat1 + dat2 - assert dat.scanning_time == 4.0 - - -class TestScanningNadirTimeStore: - def test_get_log_state(self): - sat = MagicMock() - sat.dynamics.storageUnit.storageUnitDataOutMsg.read().storageLevel = 6 - ds = data.ScanningNadirTimeStore(MagicMock(), sat) - assert ds._get_log_state() == 6.0 - - @pytest.mark.parametrize( - "before,after,new_time", - [ - (0, 3, 1), - (3, 6, 1), - (1, 1, 0), - (0, 6, 2), - ], - ) - def test_compare_log_states(self, before, after, new_time): - sat = MagicMock() - ds = data.ScanningNadirTimeStore(MagicMock(), sat) - sat.dynamics.instrument.nodeBaudRate = 3 - dat = ds._compare_log_states(before, after) - assert dat.scanning_time == new_time - - -class TestNadirScanningManager: - def test_calc_reward(self): - dm = data.NadirScanningManager(MagicMock()) - dm.data = data.NadirScanningTimeData([]) - dm.env_features.value_per_second = 1.0 - reward = dm._calc_reward( - { - "sat1": data.NadirScanningTimeData(1), - "sat2": data.NadirScanningTimeData(2), - } - ) - assert reward == {"sat1": 1.0, "sat2": 2.0} - - def test_calc_reward_existing(self): - dm = data.NadirScanningManager(MagicMock()) - dm.data = data.NadirScanningTimeData(1) - dm.env_features.value_per_second = 1.0 - reward = dm._calc_reward( - { - "sat1": data.NadirScanningTimeData(2), - "sat2": data.NadirScanningTimeData(3), - } - ) - assert reward == {"sat1": 2.0, "sat2": 3.0} - - def test_calc_reward_custom_fn(self): - dm = data.NadirScanningManager(MagicMock(), reward_fn=lambda x: 1 / x) - dm.data = data.NadirScanningTimeData([]) - reward = dm._calc_reward( - { - "sat1": data.NadirScanningTimeData(2), - "sat2": data.NadirScanningTimeData(2), - } - ) - assert reward == {"sat1": 0.5, "sat2": 0.5} diff --git a/tests/unittest/env/simulation/test_environment.py b/tests/unittest/env/simulation/test_environment.py deleted file mode 100644 index 9f5ef736..00000000 --- a/tests/unittest/env/simulation/test_environment.py +++ /dev/null @@ -1,165 +0,0 @@ -from unittest.mock import MagicMock, call, patch - -import numpy as np -import pytest - -from bsk_rl.env.simulation.environment import ( - BasicEnvironmentModel, - EnvironmentModel, - GroundStationEnvModel, -) - -module = "bsk_rl.env.simulation.environment." - - -class TestEnvironmentModel: - @patch(module + "collect_default_args") - def test_default_env_args(self, mock_collect): - mock_collect.return_value = {"a": 1, "b": 2, "c": 3} - assert EnvironmentModel.default_env_args() == {"a": 1, "b": 2, "c": 3} - - @pytest.mark.parametrize( - "overwrite,error", [({"c": 4}, False), ({"not_c": 4}, True)] - ) - @patch(module + "collect_default_args") - def test_default_sat_args_overwrote(self, mock_collect, overwrite, error): - mock_collect.return_value = {"a": 1, "b": 2, "c": 3} - if not error: - assert EnvironmentModel.default_env_args(**overwrite) == { - "a": 1, - "b": 2, - "c": 4, - } - else: - with pytest.raises(KeyError): - EnvironmentModel.default_env_args(**overwrite) - - @patch.multiple(EnvironmentModel, __abstractmethods__=set()) - @patch(module + "EnvironmentModel._init_environment_objects") - def test_init(self, mock_obj_init): - mock_sim = MagicMock() - kwargs = dict(a=1, b=2) - env = EnvironmentModel(mock_sim, 1.0, **kwargs) - mock_sim.CreateNewProcess.assert_called_once() - mock_sim.CreateNewTask.assert_called_once() - mock_obj_init.assert_called_once_with(**kwargs) - assert env.simulator is not mock_sim - assert env.simulator == mock_sim - - -class TestBasicEnvironmentModel: - basicenv = module + "BasicEnvironmentModel." - - @patch(basicenv + "__init__", MagicMock(return_value=None)) - def test_PN(self): - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.gravFactory = MagicMock() - env.body_index = 0 - msg = env.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ - msg.return_value.read.return_value.J20002Pfix = [1, 0, 0, 0, 1, 0, 0, 0, 1] - assert (env.PN == np.identity(3)).all() - - @patch(basicenv + "__init__", MagicMock(return_value=None)) - def test_omega_PN_N(self): - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.gravFactory = MagicMock() - env.body_index = 0 - msg = env.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ - msg.return_value.read.return_value.J20002Pfix_dot = [1, 0, 0, 0, 1, 0, 0, 0, 1] - msg.return_value.read.return_value.J20002Pfix = [0, -1, 1, 1, 0, -1, -1, 1, 0] - assert (env.omega_PN_N == np.array([1, 1, 1])).all() - - @patch(basicenv + "_set_gravity_bodies") - @patch(basicenv + "_set_epoch_object") - @patch(basicenv + "_set_atmosphere_density_model") - @patch(basicenv + "_set_eclipse_object") - def test_init_and_delete(self, grav_set, epoch_set, atmos_set, eclipse_set): - env = BasicEnvironmentModel(MagicMock(), 1.0) - for setter in (grav_set, epoch_set, atmos_set, eclipse_set): - setter.assert_called_once() - unload_function = MagicMock() - env.gravFactory = MagicMock(unloadSpiceKernels=unload_function) - del env - unload_function.assert_called_once() - - @patch(basicenv + "_init_environment_objects", MagicMock()) - @patch(module + "simIncludeGravBody", MagicMock()) - def test_set_gravity_bodies(self): - # Smoke test - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.simulator = MagicMock() - env._set_gravity_bodies(utc_init="time") - env.simulator.AddModelToTask.assert_called_once() - - @patch(basicenv + "_init_environment_objects", MagicMock()) - @patch(module + "ephemerisConverter", MagicMock()) - def test_set_epoch_object(self): - # Smoke test - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.simulator = MagicMock() - env.gravFactory = MagicMock() - env.sun_index = 0 - env.body_index = 1 - env._set_epoch_object() - env.simulator.AddModelToTask.assert_called_once() - - @patch(basicenv + "_init_environment_objects", MagicMock()) - @patch(module + "exponentialAtmosphere", MagicMock()) - def test_set_atmosphere_density_model(self): - # Smoke test - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.simulator = MagicMock() - env.gravFactory = MagicMock() - env.body_index = 1 - - env._set_atmosphere_density_model( - planetRadius=1.0, - baseDensity=1.0, - scaleHeight=1.0, - ) - env.simulator.AddModelToTask.assert_called_once() - - @patch(basicenv + "_init_environment_objects", MagicMock()) - @patch(module + "eclipse", MagicMock()) - def test_set_eclipse_object(self): - # Smoke test - env = BasicEnvironmentModel(MagicMock(), 1.0) - env.simulator = MagicMock() - env.gravFactory = MagicMock() - env.sun_index = 0 - env.body_index = 1 - env._set_eclipse_object() - env.simulator.AddModelToTask.assert_called_once() - - -class TestGroundStationEnvModel: - groundenv = module + "GroundStationEnvModel." - - @patch(groundenv + "_set_ground_locations") - @patch(module + "BasicEnvironmentModel._init_environment_objects", MagicMock()) - def test_init_environment_objects(self, ground_set): - GroundStationEnvModel(MagicMock(), 1.0) - ground_set.assert_called_once() - - @patch(groundenv + "_init_environment_objects", MagicMock()) - @patch(groundenv + "_create_ground_station") - def test_set_ground_locations(self, mock_gs_create): - env = GroundStationEnvModel(MagicMock(), 1.0) - env._set_ground_locations([dict(a=1), dict(b=2)], 1000.0, 1.0, 1000.0) - mock_gs_create.assert_has_calls( - [call(a=1, priority=1399), call(b=2, priority=1398)] - ) - - @patch(groundenv + "_init_environment_objects", MagicMock()) - @patch(module + "groundLocation", MagicMock()) - def test_create_ground_station(self): - env = GroundStationEnvModel(MagicMock(), 1.0) - env.simulator = MagicMock() - env.gravFactory = MagicMock() - env.groundStations = [] - env.groundLocationPlanetRadius = 10.0 - env.gsMinimumElevation = 1.0 - env.gsMaximumRange = 1000.0 - env.body_index = 1 - env._create_ground_station(0.0, 0.0) - env.simulator.AddModelToTask.assert_called_once() diff --git a/tests/unittest/env/scenario/test_observations.py b/tests/unittest/obs/test_observations.py similarity index 90% rename from tests/unittest/env/scenario/test_observations.py rename to tests/unittest/obs/test_observations.py index bf6d46be..1cb716b0 100644 --- a/tests/unittest/env/scenario/test_observations.py +++ b/tests/unittest/obs/test_observations.py @@ -1,10 +1,11 @@ -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock import numpy as np import pytest from gymnasium import spaces -from bsk_rl.env.scenario import observations as obs +from bsk_rl import obs +from bsk_rl.obs.observations import ObservationBuilder, _target_angle class TestObservationBuilder: @@ -14,14 +15,12 @@ def test_init(self): for os, name in zip(observation_spec, names): os.name = name - ob = obs.ObservationBuilder( - satellite=MagicMock(observation_spec=observation_spec) - ) + ob = ObservationBuilder(satellite=MagicMock(observation_spec=observation_spec)) assert len(ob.observation_spec) == 3 assert ob.observation_spec[2].name == "obs_A_2" def test_reset_post_sim(self): - ob = obs.ObservationBuilder( + ob = ObservationBuilder( satellite=MagicMock(observation_spec=[MagicMock() for _ in range(3)]) ) for os in ob.observation_spec: @@ -35,7 +34,7 @@ def make_mocked_observation(self, obs_type=np.ndarray): for i, os in enumerate(observation_spec): os.name = f"obs_{i}" sat.observation_spec = observation_spec - ob = obs.ObservationBuilder(sat, obs_type=obs_type) + ob = ObservationBuilder(sat, obs_type=obs_type) ob.simulator = sat.simulator return ob @@ -96,7 +95,7 @@ def test_obs_cache(self): ], ) def test_obs_space(self, observation, space): - ob = obs.ObservationBuilder(MagicMock()) + ob = ObservationBuilder(MagicMock()) ob.get_obs = MagicMock(return_value=observation) assert ob.observation_space == space @@ -147,10 +146,10 @@ def test_fns(self): type="target", target=MagicMock(priority=1.0), window=[20.0, 30.0], - location=1.0, + r_LP_P=1.0, ) assert fns["priority"](sat, opp) == 1.0 - assert fns["location"](sat, opp) == 1.0 + assert fns["r_LP_P"](sat, opp) == 1.0 assert fns["opportunity_open"](sat, opp) == 10.0 assert fns["opportunity_close"](sat, opp) == 20.0 assert fns["opportunity_mid"](sat, opp) == 15.0 @@ -160,19 +159,19 @@ def test_target_angle(self): dynamics=MagicMock(r_BN_P=np.array([1.0, 0.0, 0.0])), fsw=MagicMock(c_hat_P=np.array([0.0, 1.0, 0.0])), ) - opp = dict(location=np.array([0.0, 0.0, 0.0])) - assert np.isclose(obs._target_angle(sat, opp), np.pi / 2) + opp = dict(r_LP_P=np.array([0.0, 0.0, 0.0])) + assert np.isclose(_target_angle(sat, opp), np.pi / 2) def test_init(self): ob = obs.OpportunityProperties( - dict(prop="location", norm=2.0), + dict(prop="r_LP_P", norm=2.0), dict( prop="double_priority", fn=lambda sat, opp: opp["target"].priority * 2.0 ), n_ahead_observe=2, ) assert ob.target_properties[0]["fn"] - assert ob.target_properties[0]["name"] == "location_normd" + assert ob.target_properties[0]["name"] == "r_LP_P_normd" assert ob.target_properties[1]["norm"] == 1.0 def test_get_obs(self): diff --git a/tests/unittest/env/scenario/test_satellites.py b/tests/unittest/sats/test_access_satellite.py similarity index 58% rename from tests/unittest/env/scenario/test_satellites.py rename to tests/unittest/sats/test_access_satellite.py index 7287efc2..089e0cb3 100644 --- a/tests/unittest/env/scenario/test_satellites.py +++ b/tests/unittest/sats/test_access_satellite.py @@ -3,146 +3,15 @@ import numpy as np import pytest -from gymnasium.spaces import Box from pytest import approx -from bsk_rl.env.scenario import satellites as sats -from bsk_rl.env.scenario.environment_features import Target -from bsk_rl.env.simulation.fsw import Task +from bsk_rl import sats +from bsk_rl.scene.targets import Target from bsk_rl.utils.functional import valid_func_name -@patch.multiple(sats.Satellite, __abstractmethods__=set()) -@patch("bsk_rl.env.scenario.satellites.Satellite.observation_spec", MagicMock()) -@patch("bsk_rl.env.scenario.satellites.Satellite.action_spec", [MagicMock()]) -class TestSatellite: - sats.Satellite.dyn_type = MagicMock(with_defaults=MagicMock(defaults={"a": 1})) - Task.with_defaults = MagicMock(defaults={"c": 3}) - sats.Satellite.fsw_type = MagicMock( - with_defaults=MagicMock(defaults={"b": 2}), - some_task=Task, - ) - sats.Satellite.logger = MagicMock() - - def test_default_sat_args(self): - assert sats.Satellite.default_sat_args() == {"a": 1, "b": 2, "c": 3} - - @pytest.mark.parametrize( - "overwrite,error", [({"c": 4}, False), ({"not_c": 4}, True)] - ) - def test_default_sat_args_overwrote(self, overwrite, error): - if not error: - assert sats.Satellite.default_sat_args(**overwrite) == { - "a": 1, - "b": 2, - "c": 4, - } - else: - with pytest.raises(KeyError): - sats.Satellite.default_sat_args(**overwrite) - - def test_init_default(self): - sat = sats.Satellite(name="TestSat", sat_args=None) - assert sat.sat_args_generator == {"a": 1, "b": 2, "c": 3} - - def test_id(self): - sat1 = sats.Satellite(name="TestSat", sat_args={}) - sat2 = sats.Satellite(name="TestSat", sat_args={}) - assert sat1.id != sat2.id - assert sat1.id.startswith("TestSat") - - def test_generate_sat_args(self): - sat = sats.Satellite(name="TestSat", sat_args={"a": 4, "b": lambda: 5}) - sat._generate_sat_args() - assert sat.sat_args == {"a": 4, "b": 5, "c": 3} - - # @patch("bsk_rl.env.utils.orbital.TrajectorySimulator") - # def test_reset_pre_sim(self, trajsim_patch): - # sat = sats.Satellite(name="TestSat", sat_args=None) - # sat.data_store = MagicMock(is_fresh=True) - # sat._generate_sat_args = MagicMock() - # sat.sat_args = {"utc_init": 0, "rN": 0, "vN": 0, "oe": 0, "mu": 0} - # sat.reset_pre_sim() - # trajsim_patch.assert_called_once() - # sat._generate_sat_args.assert_called_once() - # assert sat.info == [] - # assert sat._timed_terminal_event_name is None - - @pytest.mark.parametrize( - "dyn_state,fsw_state", - [(False, False), (False, True), (True, False), (True, True)], - ) - def test_is_alive(self, dyn_state, fsw_state): - sat = sats.Satellite(name="TestSat", sat_args={}) - sat.dynamics = MagicMock(is_alive=MagicMock(return_value=dyn_state)) - sat.fsw = MagicMock(is_alive=MagicMock(return_value=fsw_state)) - assert sat.is_alive() == (dyn_state and fsw_state) - - def test_satellite_command(self): - sat1 = sats.Satellite(name="TestSat", sat_args={}) - sat2 = sats.Satellite(name="TestSat", sat_args={}) - self.satellites = [sat1, sat2] - assert sat1 == eval(sat1._satellite_command) - assert sat1 != eval(sat2._satellite_command) - assert sat2 == eval(sat2._satellite_command) - - def test_info_command(self): - sat = sats.Satellite(name="TestSat", sat_args={}) - sat.info = [] - sat.simulator = MagicMock(sim_time=0.0) - self.satellites = [sat] - self.sim_time = 0.0 - eval(sat._info_command("some info")) - assert sat.info[0] == (0.0, "some info") - - def test_log_info(self): - sat = sats.Satellite(name="TestSat", sat_args={}) - sat.info = [] - sat.simulator = MagicMock(sim_time=0.0) - sat.log_info("some info") - assert sat.info[0] == (0.0, "some info") - - def test_update_timed_terminal_event(self): - pass # Probably better with integration testing - - def test_disable_timed_event(self): - sat = sats.Satellite(name="TestSat", sat_args={}) - sat.simulator = MagicMock(eventMap={"some_event": 1}) - sat._timed_terminal_event_name = "some_event" - sat._disable_timed_terminal_event() - sat.simulator.delete_event.assert_called_with("some_event") - - def test_disable_timed_event_no_event(self): - sat = sats.Satellite(name="TestSat", sat_args={}) - sat.simulator = MagicMock(eventMap={"some_event": 1}) - sat._timed_terminal_event_name = None - sat._disable_timed_terminal_event() - assert not sat.simulator.delete_event.called - - def test_proxy_setters(self): - # Must be last test or others break - mock_sim = MagicMock() - mock_dyn = MagicMock() - mock_fsw = MagicMock() - - sat = sats.Satellite(name="TestSat", sat_args=None) - sat.dyn_type.return_value = mock_dyn - sat.fsw_type.return_value = mock_fsw - sat._generate_sat_args() - sat.set_simulator(mock_sim) - assert mock_dyn == sat.set_dynamics(1.0) - assert mock_fsw == sat.set_fsw(1.0) - # Should be proxies, not the actual object - assert sat.simulator is not mock_sim - assert sat.simulator == mock_sim - assert sat.fsw is not mock_fsw - assert sat.fsw == mock_fsw - assert sat.dynamics is not mock_dyn - assert sat.dynamics == mock_dyn - - @patch( - "bsk_rl.env.scenario.satellites.Satellite.__init__", + "bsk_rl.sats.Satellite.__init__", MagicMock(), ) @patch("bsk_rl.utils.orbital.elevation", lambda x, y: y - x) @@ -159,10 +28,10 @@ def test_add_location_for_access_checking(self): sat.locations_for_access_checking = [] target = MagicMock() sat.add_location_for_access_checking( - object=target, location=[0, 0, 0], min_elev=1.0, type="target" + object=target, r_LP_P=[0, 0, 0], min_elev=1.0, type="target" ) assert ( - dict(target=target, location=[0, 0, 0], min_elev=1.0, type="target") + dict(target=target, r_LP_P=[0, 0, 0], min_elev=1.0, type="target") in sat.locations_for_access_checking ) @@ -190,7 +59,7 @@ def test_calculate_windows_duration( assert sat.trajectory.extend_to.call_args[0][0] - start >= traj_dt * 2 def test_calculate_windows(self): - tgt = Target("tgt_0", location=[0.0, 0.0, 1.0], priority=1.0) + tgt = Target("tgt_0", r_LP_P=[0.0, 0.0, 1.0], priority=1.0) sat = self.make_sat() sat.window_calculation_time = 0.0 sat.opportunities = [] @@ -210,7 +79,7 @@ def test_calculate_windows(self): ), ) sat.locations_for_access_checking = [ - dict(target=tgt, type="target", min_elev=1.3, location=tgt.location) + dict(target=tgt, type="target", min_elev=1.3, r_LP_P=tgt.r_LP_P) ] sat.calculate_additional_windows(100.0) assert tgt in sat.opportunities_dict() @@ -326,9 +195,9 @@ def test_refine_windows_impossible(self): [1.0, 2.0, 3.0], (0.0, 4.0), (0.5, 3.5) ) - tgt0 = Target("tgt_0", location=[0.0, 0.0, 0.0], priority=1.0) - tgt1 = Target("tgt_1", location=[0.0, 0.0, 0.0], priority=1.0) - tgt2 = Target("tgt_2", location=[0.0, 0.0, 0.0], priority=1.0) + tgt0 = Target("tgt_0", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) + tgt1 = Target("tgt_1", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) + tgt2 = Target("tgt_2", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) @pytest.mark.parametrize( "merge_time", @@ -349,7 +218,7 @@ def test_add_window(self, merge_time, tgt, window, expected_window): dict(target=self.tgt0, window=(2.0, 10.0), type="target"), ] sat._add_window( - tgt, window, merge_time=merge_time, type="target", location=np.zeros(3) + tgt, window, merge_time=merge_time, type="target", r_LP_P=np.zeros(3) ) assert expected_window in sat.opportunities_dict()[tgt] @@ -407,7 +276,7 @@ def test_find_next_opportunities(self): pass # Tested in TestImagingSatellite -@patch("bsk_rl.env.scenario.satellites.Satellite.__init__") +@patch("bsk_rl.sats.Satellite.__init__") @patch.multiple(sats.ImagingSatellite, __abstractmethods__=set()) def test_init(mock_init): sats.ImagingSatellite( @@ -418,7 +287,7 @@ def test_init(mock_init): @patch( - "bsk_rl.env.scenario.satellites.Satellite.__init__", + "bsk_rl.sats.Satellite.__init__", MagicMock(), ) @patch.multiple(sats.ImagingSatellite, __abstractmethods__=set()) @@ -429,11 +298,11 @@ def make_sat(self): sat_args={"imageTargetMinimumElevation": 1}, ) - @patch("bsk_rl.env.scenario.satellites.Satellite.reset_pre_sim") + @patch("bsk_rl.sats.Satellite.reset_pre_sim") def test_reset_pre_sim(self, mock_reset): sat = self.make_sat() sat.data_store = MagicMock() - sat.data_store.env_knowledge.targets = [MagicMock()] * 5 + sat.data_store.data.known = [MagicMock()] * 5 sat.sat_args = {} sat.reset_pre_sim() mock_reset.assert_called_once() @@ -444,7 +313,7 @@ def test_reset_pre_sim(self, mock_reset): "gen_duration,time_limit,expected", [(None, float("inf"), 0), (None, 100.0, 100.0), (10.0, 100.0, 10.0)], ) - @patch("bsk_rl.env.scenario.satellites.Satellite.reset_post_sim") + @patch("bsk_rl.sats.Satellite.reset_post_sim") def test_reset_post_sim(self, mock_reset, gen_duration, time_limit, expected): sat = self.make_sat() sat.sat_args = {} @@ -457,9 +326,9 @@ def test_reset_post_sim(self, mock_reset, gen_duration, time_limit, expected): assert sat.initial_generation_duration == expected sat.calculate_additional_windows.assert_called_once() - tgt0 = Target("tgt_0", location=[0.0, 0.0, 0.0], priority=1.0) - tgt1 = Target("tgt_1", location=[0.0, 0.0, 0.0], priority=1.0) - tgt2 = Target("tgt_2", location=[0.0, 0.0, 0.0], priority=1.0) + tgt0 = Target("tgt_0", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) + tgt1 = Target("tgt_1", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) + tgt2 = Target("tgt_2", r_LP_P=[0.0, 0.0, 0.0], priority=1.0) windows = { tgt0: [(0.0, 10.0), (20.0, 30.0), (40.0, 50.0)], tgt1: [(10.0, 20.0)], @@ -473,88 +342,8 @@ def test_reset_post_sim(self, mock_reset, gen_duration, time_limit, expected): dict(target=tgt0, window=(40.0, 50.0), type="target"), ] - def test_upcoming_windows_unfiltered(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=25.0) - sat.opportunities = self.opportunities - assert sat.upcoming_windows == { - self.tgt0: [(20.0, 30.0), (40.0, 50.0)], - self.tgt2: [(30.0, 40.0)], - } - - def test_upcoming_windows_filtered(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=25.0) - sat.opportunities = self.opportunities - sat.data_store = MagicMock(data=MagicMock(imaged=[self.tgt0])) - assert sat.upcoming_windows == { - self.tgt2: [(30.0, 40.0)], - } - - def test_windows(self): - sat = self.make_sat() - sat.opportunities = self.opportunities - assert sat.windows == self.windows - - def test_next_windows(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=35.0) - sat.opportunities = self.opportunities - assert sat.next_windows == { - self.tgt0: (40.0, 50.0), - self.tgt2: (30.0, 40.0), - } - - def test_upcoming_targets(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=35.0) - sat.opportunities = self.opportunities - assert sat.upcoming_targets(2) == [self.tgt2, self.tgt0] - - def test_no_upcoming_targets(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=35.0) - sat.opportunities = self.opportunities - assert sat.upcoming_targets(0) == [] - - def test_upcoming_targets_pad(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=35.0) - sat.opportunities = self.opportunities - sat.calculate_additional_windows = MagicMock() - assert sat.upcoming_targets(4, pad=True) == [ - self.tgt2, - self.tgt0, - self.tgt0, - self.tgt0, - ] - - def test_upcoming_targets_generate_more(self): - sat = self.make_sat() - sat.simulator = MagicMock(sim_time=35.0) - sat.opportunities = self.opportunities - sat.calculate_additional_windows = MagicMock( - side_effect=lambda t: self.opportunities.append( - dict(target=self.tgt1, window=(60.0, 70.0), type="target") - ) - ) - print(sat.opportunities) - print( - sat.upcoming_targets(3, pad=True), - [ - self.tgt2, - self.tgt0, - self.tgt1, - ], - ) - assert sat.upcoming_targets(3, pad=True) == [ - self.tgt2, - self.tgt0, - self.tgt1, - ] - @patch( - "bsk_rl.env.scenario.satellites.ImagingSatellite._disable_image_event", + "bsk_rl.sats.ImagingSatellite._disable_image_event", MagicMock(), ) def test_update_image_event_existing(self): @@ -595,18 +384,18 @@ def test_disable_image_event_no_event(self): ) def test_parse_target_selection(self, query, expected): sat = self.make_sat() - sat.upcoming_targets = lambda x: self.upcoming_targets[0:x] - sat.data_store = MagicMock( - env_knowledge=MagicMock(targets=self.upcoming_targets) - ) + sat.find_next_opportunities = lambda *args, **kwargs: [ + dict(target=target) for target in self.upcoming_targets[0 : kwargs["n"]] + ] + sat.data_store = MagicMock() + sat.data_store.data.known = self.upcoming_targets assert expected == sat.parse_target_selection(query).name def test_parse_target_selection_invalid(self): sat = self.make_sat() sat.upcoming_targets = lambda x: self.upcoming_targets[0:x] - sat.data_store = MagicMock( - env_knowledge=MagicMock(targets=self.upcoming_targets) - ) + sat.data_store = MagicMock() + sat.data_store.data.known = self.upcoming_targets with pytest.raises(TypeError): sat.parse_target_selection(np.zeros(10)) @@ -617,7 +406,7 @@ def test_task_target_for_imaging(self): sat.fsw = MagicMock() sat.simulator = MagicMock(sim_time=35.0) sat._update_image_event = MagicMock() - sat._update_timed_terminal_event = MagicMock() + sat.update_timed_terminal_event = MagicMock() sat.log_info = MagicMock() sat.task_target_for_imaging(self.tgt0) sat.fsw.action_image.assert_called_once() @@ -625,5 +414,5 @@ def test_task_target_for_imaging(self): sat.log_info.assert_called() sat._update_image_event.assert_called_once() assert sat._update_image_event.call_args[0][0] == self.tgt0 - sat._update_timed_terminal_event.assert_called_once() - assert sat._update_timed_terminal_event.call_args[0][0] == 50.0 + sat.update_timed_terminal_event.assert_called_once() + assert sat.update_timed_terminal_event.call_args[0][0] == 50.0 diff --git a/tests/unittest/sats/test_satellite.py b/tests/unittest/sats/test_satellite.py new file mode 100644 index 00000000..1fb0365d --- /dev/null +++ b/tests/unittest/sats/test_satellite.py @@ -0,0 +1,135 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from bsk_rl import sats +from bsk_rl.sim.fsw import Task + + +@patch.multiple(sats.Satellite, __abstractmethods__=set()) +@patch("bsk_rl.sats.Satellite.observation_spec", MagicMock()) +@patch("bsk_rl.sats.Satellite.action_spec", [MagicMock()]) +class TestSatellite: + sats.Satellite.dyn_type = MagicMock(with_defaults=MagicMock(defaults={"a": 1})) + Task.with_defaults = MagicMock(defaults={"c": 3}) + sats.Satellite.fsw_type = MagicMock( + with_defaults=MagicMock(defaults={"b": 2}), + some_task=Task, + ) + sats.Satellite.logger = MagicMock() + + def test_default_sat_args(self): + assert sats.Satellite.default_sat_args() == {"a": 1, "b": 2, "c": 3} + + @pytest.mark.parametrize( + "overwrite,error", [({"c": 4}, False), ({"not_c": 4}, True)] + ) + def test_default_sat_args_overwrote(self, overwrite, error): + if not error: + assert sats.Satellite.default_sat_args(**overwrite) == { + "a": 1, + "b": 2, + "c": 4, + } + else: + with pytest.raises(KeyError): + sats.Satellite.default_sat_args(**overwrite) + + def test_init_default(self): + sat = sats.Satellite(name="TestSat", sat_args=None) + assert sat.sat_args_generator == {"a": 1, "b": 2, "c": 3} + + def test_id(self): + sat1 = sats.Satellite(name="TestSat", sat_args={}) + sat2 = sats.Satellite(name="TestSat", sat_args={}) + assert sat1.id != sat2.id + assert sat1.id.startswith("TestSat") + + def test_generate_sat_args(self): + sat = sats.Satellite(name="TestSat", sat_args={"a": 4, "b": lambda: 5}) + sat._generate_sat_args() + assert sat.sat_args == {"a": 4, "b": 5, "c": 3} + + # @patch("bsk_rl.env.utils.orbital.TrajectorySimulator") + # def test_reset_pre_sim(self, trajsim_patch): + # sat = sats.Satellite(name="TestSat", sat_args=None) + # sat.data_store = MagicMock(is_fresh=True) + # sat._generate_sat_args = MagicMock() + # sat.sat_args = {"utc_init": 0, "rN": 0, "vN": 0, "oe": 0, "mu": 0} + # sat.reset_pre_sim() + # trajsim_patch.assert_called_once() + # sat._generate_sat_args.assert_called_once() + # assert sat.info == [] + # assert sat._timed_terminal_event_name is None + + @pytest.mark.parametrize( + "dyn_state,fsw_state", + [(False, False), (False, True), (True, False), (True, True)], + ) + def test_is_alive(self, dyn_state, fsw_state): + sat = sats.Satellite(name="TestSat", sat_args={}) + sat.dynamics = MagicMock(is_alive=MagicMock(return_value=dyn_state)) + sat.fsw = MagicMock(is_alive=MagicMock(return_value=fsw_state)) + assert sat.is_alive() == (dyn_state and fsw_state) + + def test_satellite_command(self): + sat1 = sats.Satellite(name="TestSat", sat_args={}) + sat2 = sats.Satellite(name="TestSat", sat_args={}) + self.satellites = [sat1, sat2] + assert sat1 == eval(sat1._satellite_command) + assert sat1 != eval(sat2._satellite_command) + assert sat2 == eval(sat2._satellite_command) + + def test_info_command(self): + sat = sats.Satellite(name="TestSat", sat_args={}) + sat.info = [] + sat.simulator = MagicMock(sim_time=0.0) + self.satellites = [sat] + self.sim_time = 0.0 + eval(sat._info_command("some info")) + assert sat.info[0] == (0.0, "some info") + + def test_log_info(self): + sat = sats.Satellite(name="TestSat", sat_args={}) + sat.info = [] + sat.simulator = MagicMock(sim_time=0.0) + sat.log_info("some info") + assert sat.info[0] == (0.0, "some info") + + def test_update_timed_terminal_event(self): + pass # Probably better with integration testing + + def test_disable_timed_event(self): + sat = sats.Satellite(name="TestSat", sat_args={}) + sat.simulator = MagicMock(eventMap={"some_event": 1}) + sat._timed_terminal_event_name = "some_event" + sat.disable_timed_terminal_event() + sat.simulator.delete_event.assert_called_with("some_event") + + def test_disable_timed_event_no_event(self): + sat = sats.Satellite(name="TestSat", sat_args={}) + sat.simulator = MagicMock(eventMap={"some_event": 1}) + sat._timed_terminal_event_name = None + sat.disable_timed_terminal_event() + assert not sat.simulator.delete_event.called + + def test_proxy_setters(self): + # Must be last test or others break + mock_sim = MagicMock() + mock_dyn = MagicMock() + mock_fsw = MagicMock() + + sat = sats.Satellite(name="TestSat", sat_args=None) + sat.dyn_type.return_value = mock_dyn + sat.fsw_type.return_value = mock_fsw + sat._generate_sat_args() + sat.set_simulator(mock_sim) + assert mock_dyn == sat.set_dynamics(1.0) + assert mock_fsw == sat.set_fsw(1.0) + # Should be proxies, not the actual object + assert sat.simulator is not mock_sim + assert sat.simulator == mock_sim + assert sat.fsw is not mock_fsw + assert sat.fsw == mock_fsw + assert sat.dynamics is not mock_dyn + assert sat.dynamics == mock_dyn diff --git a/tests/unittest/env/scenario/test_environment_features.py b/tests/unittest/scene/test_scenario.py similarity index 73% rename from tests/unittest/env/scenario/test_environment_features.py rename to tests/unittest/scene/test_scenario.py index cbdfd1b2..f3e5f143 100644 --- a/tests/unittest/env/scenario/test_environment_features.py +++ b/tests/unittest/scene/test_scenario.py @@ -4,13 +4,9 @@ import pytest from pytest import approx -from bsk_rl.env.scenario.environment_features import ( - CityTargets, - StaticTargets, - Target, - UniformNadirFeature, - lla2ecef, -) +from bsk_rl.scene import CityTargets, UniformNadirScanning, UniformTargets +from bsk_rl.scene.targets import Target +from bsk_rl.utils.orbital import lla2ecef class TestTarget: @@ -28,48 +24,48 @@ def test_repr_(self): assert self.T2.__repr__() == "Target(Pleasanton)" def test_location_type(self): - assert np.all(self.T1a.location == np.array([1, 2, 3])) - assert not isinstance(self.T1a.location, list) - assert np.all(self.T2.location == np.array([0, 0, 0])) + assert np.all(self.T1a.r_LP_P == np.array([1, 2, 3])) + assert not isinstance(self.T1a.r_LP_P, list) + assert np.all(self.T2.r_LP_P == np.array([0, 0, 0])) -class TestStaticTargets: +class TestUniformTargets: def test_init(self): - st = StaticTargets(1) + st = UniformTargets(1) assert st.priority_distribution is not None def test_reset_constant(self): - st = StaticTargets(10) + st = UniformTargets(10) st.regenerate_targets = MagicMock() - st.reset() + st.reset_pre_sim() assert st.n_targets == 10 st.regenerate_targets.assert_called_once() @pytest.mark.repeat(10) def test_reset_variable(self): - st = StaticTargets((8, 10)) + st = UniformTargets((8, 10)) st.regenerate_targets = MagicMock() - st.reset() + st.reset_pre_sim() assert 8 <= st.n_targets <= 10 def test_regenerate_targets(self): - st = StaticTargets(3, radius=1.0, priority_distribution=lambda: 1) + st = UniformTargets(3, radius=1.0, priority_distribution=lambda: 1) st.n_targets = st._n_targets st.regenerate_targets() assert len(st.targets) == 3 for target in st.targets: - assert np.linalg.norm(target.location) == approx(1.0) + assert np.linalg.norm(target.r_LP_P) == approx(1.0) assert target.priority == 1 def test_regenerate_targets_repeatable(self): np.random.seed(0) - st1 = StaticTargets(3, radius=1.0) - st1.reset() + st1 = UniformTargets(3, radius=1.0) + st1.reset_pre_sim() np.random.seed(0) - st2 = StaticTargets(3, radius=1.0) - st2.reset() + st2 = UniformTargets(3, radius=1.0) + st2.reset_pre_sim() for t1, t2 in zip(st1.targets, st2.targets): - assert (t1.location == t2.location).all() + assert (t1.r_LP_P == t2.r_LP_P).all() class TestCityTargets: @@ -83,7 +79,7 @@ class TestCityTargets: ], ) def test_lla2ecef(self, lat, long, radius, expected): - assert abs(lla2ecef(lat, long, radius) - expected < 1e-9).all() + np.testing.assert_allclose(lla2ecef(lat, long, radius), expected, atol=1e-6) def mock_data(self, mock_read_csv, n_database=5): mock_read_csv.return_value = MagicMock( @@ -108,9 +104,9 @@ def test_regenerate_targets(self, mock_read_csv, n_targets): ct = CityTargets(n_targets) if n_targets > n_database: with pytest.raises(ValueError): - ct.reset() + ct.reset_pre_sim() else: - ct.reset() + ct.reset_pre_sim() assert len(ct.targets) == n_targets possible_names = [f"city{i}" for i in range(5)] for target in ct.targets: @@ -126,14 +122,14 @@ def test_regenerate_targets_n_select_from( ): self.mock_data(mock_read_csv) ct = CityTargets(n_targets, n_select_from=n_select_from) - ct.reset() + ct.reset_pre_sim() assert len(ct.targets) == n_targets if isinstance(n_select_from, int): possible_names = [f"city{i}" for i in range(n_select_from)] for target in ct.targets: assert target.name in possible_names - @patch("bsk_rl.env.scenario.environment_features.lla2ecef") + @patch("bsk_rl.scene.targets.lla2ecef") @patch("pandas.read_csv") def test_regenerate_targets_offset(self, mock_read_csv, mock_lla2ecef): nominal = np.array([1.0, 0.0, 0.0]) @@ -141,13 +137,12 @@ def test_regenerate_targets_offset(self, mock_read_csv, mock_lla2ecef): self.mock_data(mock_read_csv, n_database=10) n_targets = 10 ct = CityTargets(n_targets, location_offset=0.01, radius=1.0) - ct.reset() + ct.reset_pre_sim() for target in ct.targets: - assert np.linalg.norm(target.location - nominal) <= 0.03 - assert np.linalg.norm(target.location) == approx(1.0) + assert np.linalg.norm(target.r_LP_P - nominal) <= 0.03 + assert np.linalg.norm(target.r_LP_P) == approx(1.0) -class TestUniformNadirFeature: +class TestUniformNadirScanning: def test_init(self): - st = UniformNadirFeature() - assert st.name == "NadirFeature" + UniformNadirScanning() diff --git a/tests/unittest/env/simulation/test_dynamics.py b/tests/unittest/sim/test_dynamics.py similarity index 73% rename from tests/unittest/env/simulation/test_dynamics.py rename to tests/unittest/sim/test_dynamics.py index 86c6ab70..40bd5485 100644 --- a/tests/unittest/env/simulation/test_dynamics.py +++ b/tests/unittest/sim/test_dynamics.py @@ -4,8 +4,8 @@ import pytest from Basilisk.utilities import macros as mc -from bsk_rl.env.simulation import environment -from bsk_rl.env.simulation.dynamics import ( +from bsk_rl.sim import world +from bsk_rl.sim.dyn import ( BasicDynamicsModel, ContinuousImagingDynModel, DynamicsModel, @@ -14,7 +14,7 @@ LOSCommDynModel, ) -module = "bsk_rl.env.simulation.dynamics." +module = "bsk_rl.sim.dyn." @patch.multiple(DynamicsModel, __abstractmethods__=set()) @@ -23,7 +23,7 @@ def test_base_class(self): sat = MagicMock() dyn = DynamicsModel(sat, 1.0) dyn.simulator.CreateNewProcess.assert_called_once() - assert sat.simulator.environment == dyn.environment + assert sat.simulator.world == dyn.world dyn.reset_for_action() @patch(module + "check_aliveness_checkers", MagicMock(return_value=True)) @@ -35,33 +35,34 @@ def test_is_alive(self): basicdyn = module + "BasicDynamicsModel." -def test_basic_requires_env(): - assert environment.BasicEnvironmentModel in BasicDynamicsModel._requires_env() +def test_basic_requires_world(): + assert world.BasicWorldModel in BasicDynamicsModel._requires_world() -@patch(basicdyn + "_requires_env", MagicMock(return_value=[])) -@patch(basicdyn + "_set_spacecraft_hub") -@patch(basicdyn + "_set_drag_effector") -@patch(basicdyn + "_set_reaction_wheel_dyn_effector") -@patch(basicdyn + "_set_thruster_dyn_effector") -@patch(basicdyn + "_set_simple_nav_object") -@patch(basicdyn + "_set_eclipse_object") -@patch(basicdyn + "_set_solar_panel") -@patch(basicdyn + "_set_battery") -@patch(basicdyn + "_set_reaction_wheel_power") -@patch(basicdyn + "_set_thruster_power") -def test_basic_init_objects(self, *args): +@patch(basicdyn + "_requires_world", MagicMock(return_value=[])) +@patch(basicdyn + "setup_spacecraft_hub") +@patch(basicdyn + "setup_drag_effector") +@patch(basicdyn + "setup_reaction_wheel_dyn_effector") +@patch(basicdyn + "setup_thruster_dyn_effector") +@patch(basicdyn + "setup_simple_nav_object") +@patch(basicdyn + "setup_eclipse_object") +@patch(basicdyn + "setup_solar_panel") +@patch(basicdyn + "setup_battery") +@patch(basicdyn + "setup_power_sink") +@patch(basicdyn + "setup_reaction_wheel_power") +@patch(basicdyn + "setup_thruster_power") +def test_basic_setup_objects(self, *args): BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: setter.assert_called_once() -@patch(basicdyn + "_requires_env", MagicMock(return_value=[])) -@patch(basicdyn + "_init_dynamics_objects", MagicMock()) +@patch(basicdyn + "_requires_world", MagicMock(return_value=[])) +@patch(basicdyn + "_setup_dynamics_objects", MagicMock()) class TestBasicDynamicsModel: def test_dynamic_properties(self): dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) - dyn.simulator.environment = MagicMock(PN=np.identity(3), omega_PN_N=np.zeros(3)) + dyn.simulator.world = MagicMock(PN=np.identity(3), omega_PN_N=np.zeros(3)) dyn.scObject = MagicMock() message = dyn.scObject.scStateOutMsg.read.return_value message.sigma_BN = np.zeros(3) @@ -93,7 +94,7 @@ def test_wheel_properties(self): dyn.rwStateEffector.rwSpeedOutMsg.read.return_value.wheelSpeeds = speeds dyn.maxWheelSpeed = 100.0 assert (dyn.wheel_speeds == speeds).all() - assert (abs(dyn.wheel_speeds_fraction - np.array([0.1, 0.2, 0.3])) < 1e-9).all() + np.testing.assert_allclose(dyn.wheel_speeds_fraction, np.array([0.1, 0.2, 0.3])) @pytest.mark.parametrize( "vec,valid", @@ -101,7 +102,7 @@ def test_wheel_properties(self): ) def test_altitude_valid(self, vec, valid): dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) - dyn.simulator.environment = MagicMock() + dyn.simulator.world = MagicMock() dyn.scObject = MagicMock() message = dyn.scObject.scStateOutMsg.read.return_value message.r_BN_N = vec @@ -117,7 +118,7 @@ def test_altitude_valid(self, vec, valid): ) def test_rw_speeds_valid(self, speeds, valid): dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0) - dyn.simulator.environment = MagicMock() + dyn.simulator.world = MagicMock() dyn.rwStateEffector = MagicMock() speeds = np.array(speeds) * mc.rpm2radsec dyn.rwStateEffector.rwSpeedOutMsg.read.return_value.wheelSpeeds = speeds @@ -139,29 +140,29 @@ def test_battery_valid(self, level, valid): class TestLOSCommDynModel: losdyn = module + "LOSCommDynModel." - @patch(losdyn + "_requires_env", MagicMock(return_value=[])) - @patch(module + "BasicDynamicsModel._init_dynamics_objects", MagicMock()) - @patch(losdyn + "_set_los_comms") - def test_init_objects(self, *args): + @patch(losdyn + "_requires_world", MagicMock(return_value=[])) + @patch(module + "BasicDynamicsModel._setup_dynamics_objects", MagicMock()) + @patch(losdyn + "setup_los_comms") + def test_setup_objects(self, *args): LOSCommDynModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: setter.assert_called_once() - @patch(losdyn + "_requires_env", MagicMock(return_value=[])) - @patch(losdyn + "_init_dynamics_objects", MagicMock()) + @patch(losdyn + "_requires_world", MagicMock(return_value=[])) + @patch(losdyn + "_setup_dynamics_objects", MagicMock()) @patch(module + "spacecraftLocation", MagicMock()) - def test_set_los_comms(self): + def test_setup_los_comms(self): mock_sim = MagicMock() dyn1 = LOSCommDynModel(MagicMock(simulator=mock_sim), 1.0) dyn1.scObject = MagicMock() - dyn1._set_los_comms(priority=1) + dyn1.setup_los_comms(losMaximumRange=-1, priority=1) mock_sim.dynamics_list = {1: dyn1} mock_sim.AddModelToTask.assert_not_called() assert dyn1.los_comms_ids == [] dyn2 = LOSCommDynModel(MagicMock(simulator=mock_sim), 1.0) dyn2.scObject = MagicMock() - dyn2._set_los_comms(priority=1) + dyn2.setup_los_comms(losMaximumRange=-1, priority=1) mock_sim.dynamics_list[2] = dyn2 assert dyn1.los_comms_ids == [dyn2.satellite.id] assert dyn2.los_comms_ids == [dyn1.satellite.id] @@ -173,7 +174,7 @@ def test_set_los_comms(self): dyn3 = LOSCommDynModel(MagicMock(simulator=mock_sim), 1.0) dyn3.scObject = MagicMock() - dyn3._set_los_comms(priority=1) + dyn3.setup_los_comms(losMaximumRange=-1, priority=1) mock_sim.dynamics_list[3] = dyn3 assert dyn1.los_comms_ids == [dyn2.satellite.id, dyn3.satellite.id] assert dyn2.los_comms_ids == [dyn1.satellite.id, dyn3.satellite.id] @@ -185,22 +186,22 @@ def test_set_los_comms(self): imdyn = module + "ImagingDynModel." -@patch(imdyn + "_requires_env", MagicMock(return_value=[])) -@patch(module + "BasicDynamicsModel._init_dynamics_objects", MagicMock()) -@patch(imdyn + "_set_instrument_power_sink") -@patch(imdyn + "_set_transmitter_power_sink") -@patch(imdyn + "_set_instrument") -@patch(imdyn + "_set_transmitter") -@patch(imdyn + "_set_storage_unit") -@patch(imdyn + "_set_imaging_target") -def test_init_objects(*args): +@patch(imdyn + "_requires_world", MagicMock(return_value=[])) +@patch(module + "BasicDynamicsModel._setup_dynamics_objects", MagicMock()) +@patch(imdyn + "setup_instrument_power_sink") +@patch(imdyn + "setup_transmitter_power_sink") +@patch(imdyn + "setup_instrument") +@patch(imdyn + "setup_transmitter") +@patch(imdyn + "setup_storage_unit") +@patch(imdyn + "setup_imaging_target") +def test_setup_objects(*args): ImagingDynModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: setter.assert_called_once() -@patch(imdyn + "_requires_env", MagicMock(return_value=[])) -@patch(imdyn + "_init_dynamics_objects", MagicMock()) +@patch(imdyn + "_requires_world", MagicMock(return_value=[])) +@patch(imdyn + "_setup_dynamics_objects", MagicMock()) class TestImagingDynModel: def test_storage_properties(self): dyn = ImagingDynModel(MagicMock(simulator=MagicMock()), 1.0) @@ -239,39 +240,37 @@ def test_data_storage_valid(self, level, valid_check, valid): (2, ["a", "b", "c"], ValueError), ], ) - def test_set_storage_unit(self, buffers, names, expected): + def test_setup_storage_unit(self, buffers, names, expected): mock_sim = MagicMock() dyn = ImagingDynModel(MagicMock(simulator=mock_sim), 1.0) dyn.instrument = MagicMock() dyn.transmitter = MagicMock() if isinstance(expected, type): with pytest.raises(expected): - dyn._set_storage_unit(1000, buffers, names) + dyn.setup_storage_unit(1000, False, 0, buffers, names) return - dyn._set_storage_unit(1000, buffers, names) + dyn.setup_storage_unit(1000, False, 0, buffers, names) dyn.storageUnit.addPartition.assert_has_calls([call(name) for name in expected]) dyn.simulator.CreateNewProcess.assert_called_once() class TestGroundStationDynModel: - def test_requires_env(self): - assert ( - environment.GroundStationEnvModel in GroundStationDynModel._requires_env() - ) + def test_requires_world(self): + assert world.GroundStationWorldModel in GroundStationDynModel._requires_world() gsdyn = module + "GroundStationDynModel." - @patch(gsdyn + "_requires_env", MagicMock(return_value=[])) - @patch(module + "ImagingDynModel._init_dynamics_objects", MagicMock()) - @patch(gsdyn + "_set_ground_station_locations") - def test_init_objects(self, *args): + @patch(gsdyn + "_requires_world", MagicMock(return_value=[])) + @patch(module + "ImagingDynModel._setup_dynamics_objects", MagicMock()) + @patch(gsdyn + "setup_ground_station_locations") + def test_setup_objects(self, *args): GroundStationDynModel(MagicMock(simulator=MagicMock()), 1.0) for setter in args: setter.assert_called_once() -@patch(imdyn + "_requires_env", MagicMock(return_value=[])) -@patch(imdyn + "_init_dynamics_objects", MagicMock()) +@patch(imdyn + "_requires_world", MagicMock(return_value=[])) +@patch(imdyn + "_setup_dynamics_objects", MagicMock()) class TestContinuousImagingDynModel: def test_storage_properties(self): dyn = ContinuousImagingDynModel(MagicMock(simulator=MagicMock()), 1.0) diff --git a/tests/unittest/env/simulation/test_fsw.py b/tests/unittest/sim/test_fsw.py similarity index 87% rename from tests/unittest/env/simulation/test_fsw.py rename to tests/unittest/sim/test_fsw.py index 44f945ee..5c9ccb89 100644 --- a/tests/unittest/env/simulation/test_fsw.py +++ b/tests/unittest/sim/test_fsw.py @@ -2,15 +2,9 @@ import numpy as np -from bsk_rl.env.simulation.fsw import ( - BasicFSWModel, - FSWModel, - ImagingFSWModel, - Task, - action, -) +from bsk_rl.sim.fsw import BasicFSWModel, FSWModel, ImagingFSWModel, Task, action -module = "bsk_rl.env.simulation.fsw." +module = "bsk_rl.sim.fsw." def test_action_decorator(): @@ -37,7 +31,7 @@ def test_base_class(self): sat = MagicMock() fsw = FSWModel(sat, 1.0) # fsw.simulator.CreateNewProcess.assert_called_once() - assert sat.simulator.environment == fsw.environment + assert sat.simulator.world == fsw.world assert sat.dynamics == fsw.dynamics @patch(module + "check_aliveness_checkers", MagicMock(return_value=True)) @@ -46,14 +40,14 @@ def test_is_alive(self): assert fsw.is_alive() -@patch.multiple(Task, __abstractmethods__=set()) +@patch.multiple(Task, __abstractmethods__=set(), name="task") class TestTask: def test_base_class(self): fsw = MagicMock() task = Task(fsw, 1) task.create_task() task.fsw.simulator.CreateNewTask.assert_called_once() - task._init_objects() + task._setup_fsw_objects() task.reset_for_action() task.fsw.simulator.disableTask.assert_called_once() @@ -74,7 +68,7 @@ def test_make_tasks(self, *args): for task in fsw.tasks: task.create_task.assert_called_once() task._create_module_data.assert_called_once() - task._init_objects.assert_called_once() + task._setup_fsw_objects.assert_called_once() imagingfsw = module + "ImagingFSWModel." diff --git a/tests/unittest/env/simulation/test_simulator.py b/tests/unittest/sim/test_simulator.py similarity index 88% rename from tests/unittest/env/simulation/test_simulator.py rename to tests/unittest/sim/test_simulator.py index 687ed505..7cf87e64 100644 --- a/tests/unittest/env/simulation/test_simulator.py +++ b/tests/unittest/sim/test_simulator.py @@ -2,7 +2,7 @@ import pytest -from bsk_rl.env.simulation.simulator import Simulator +from bsk_rl.sim import Simulator @patch("Basilisk.utilities.SimulationBaseClass.SimBaseClass.__init__") @@ -30,7 +30,7 @@ def id(self): set_dynamics=MagicMock(return_value=self.dyn), set_fsw=MagicMock(return_value=self.fsw), ) - sim = Simulator([sat], env_type=self.MockEnv, env_args={}, **kwargs) + sim = Simulator([sat], world_type=self.MockEnv, world_args={}, **kwargs) sim.TotalSim = MagicMock(CurrentNanos=1000000000) return sim @@ -48,10 +48,10 @@ def test_sim_time(self, simbase_init): sim = self.mock_sim() assert sim.sim_time == 1.0 - def test_set_environment(self, simbase_init): + def test_set_world(self, simbase_init): sim = self.mock_sim() - assert sim.environment.sim == sim - assert sim.environment.rate == sim.sim_rate + assert sim.world.sim == sim + assert sim.world.rate == sim.sim_rate def test_delete_event(self, simbase_init): sim = self.mock_sim() diff --git a/tests/unittest/sim/test_world.py b/tests/unittest/sim/test_world.py new file mode 100644 index 00000000..aa37fd11 --- /dev/null +++ b/tests/unittest/sim/test_world.py @@ -0,0 +1,161 @@ +from unittest.mock import MagicMock, call, patch + +import numpy as np +import pytest + +from bsk_rl.sim.world import BasicWorldModel, GroundStationWorldModel, WorldModel + +module = "bsk_rl.sim.world." + + +class TestWorldModel: + @patch(module + "collect_default_args") + def test_default_world_args(self, mock_collect): + mock_collect.return_value = {"a": 1, "b": 2, "c": 3} + assert WorldModel.default_world_args() == {"a": 1, "b": 2, "c": 3} + + @pytest.mark.parametrize( + "overwrite,error", [({"c": 4}, False), ({"not_c": 4}, True)] + ) + @patch(module + "collect_default_args") + def test_default_sat_args_overwrote(self, mock_collect, overwrite, error): + mock_collect.return_value = {"a": 1, "b": 2, "c": 3} + if not error: + assert WorldModel.default_world_args(**overwrite) == { + "a": 1, + "b": 2, + "c": 4, + } + else: + with pytest.raises(KeyError): + WorldModel.default_world_args(**overwrite) + + @patch.multiple(WorldModel, __abstractmethods__=set()) + @patch(module + "WorldModel._setup_world_objects") + def test_init(self, mock_obj_init): + mock_sim = MagicMock() + kwargs = dict(a=1, b=2) + world = WorldModel(mock_sim, 1.0, **kwargs) + mock_sim.CreateNewProcess.assert_called_once() + mock_sim.CreateNewTask.assert_called_once() + mock_obj_init.assert_called_once_with(**kwargs) + assert world.simulator is not mock_sim + assert world.simulator == mock_sim + + +class TestBasicWorldModel: + basicworld = module + "BasicWorldModel." + + @patch(basicworld + "__init__", MagicMock(return_value=None)) + def test_PN(self): + world = BasicWorldModel(MagicMock(), 1.0) + world.gravFactory = MagicMock() + world.body_index = 0 + msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ + msg.return_value.read.return_value.J20002Pfix = [1, 0, 0, 0, 1, 0, 0, 0, 1] + assert (world.PN == np.identity(3)).all() + + @patch(basicworld + "__init__", MagicMock(return_value=None)) + def test_omega_PN_N(self): + world = BasicWorldModel(MagicMock(), 1.0) + world.gravFactory = MagicMock() + world.body_index = 0 + msg = world.gravFactory.spiceObject.planetStateOutMsgs.__getitem__ + msg.return_value.read.return_value.J20002Pfix_dot = [1, 0, 0, 0, 1, 0, 0, 0, 1] + msg.return_value.read.return_value.J20002Pfix = [0, -1, 1, 1, 0, -1, -1, 1, 0] + assert (world.omega_PN_N == np.array([1, 1, 1])).all() + + @patch(basicworld + "setup_gravity_bodies") + @patch(basicworld + "setup_ephem_object") + @patch(basicworld + "setup_atmosphere_density_model") + @patch(basicworld + "setup_eclipse_object") + def test_setup_and_delete(self, grav_set, epoch_set, atmos_set, eclipse_set): + world = BasicWorldModel(MagicMock(), 1.0) + for setter in (grav_set, epoch_set, atmos_set, eclipse_set): + setter.assert_called_once() + unload_function = MagicMock() + world.gravFactory = MagicMock(unloadSpiceKernels=unload_function) + del world + unload_function.assert_called_once() + + @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(module + "simIncludeGravBody", MagicMock()) + def testsetup_gravity_bodies(self): + # Smoke test + world = BasicWorldModel(MagicMock(), 1.0) + world.simulator = MagicMock() + world.setup_gravity_bodies(utc_init="time") + world.simulator.AddModelToTask.assert_called_once() + + @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(module + "ephemerisConverter", MagicMock()) + def testsetup_epoch_object(self): + # Smoke test + world = BasicWorldModel(MagicMock(), 1.0) + world.simulator = MagicMock() + world.gravFactory = MagicMock() + world.sun_index = 0 + world.body_index = 1 + world.setup_ephem_object() + world.simulator.AddModelToTask.assert_called_once() + + @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(module + "exponentialAtmosphere", MagicMock()) + def testsetup_atmosphere_density_model(self): + # Smoke test + world = BasicWorldModel(MagicMock(), 1.0) + world.simulator = MagicMock() + world.gravFactory = MagicMock() + world.body_index = 1 + + world.setup_atmosphere_density_model( + planetRadius=1.0, + baseDensity=1.0, + scaleHeight=1.0, + ) + world.simulator.AddModelToTask.assert_called_once() + + @patch(basicworld + "_setup_world_objects", MagicMock()) + @patch(module + "eclipse", MagicMock()) + def testsetup_eclipse_object(self): + # Smoke test + world = BasicWorldModel(MagicMock(), 1.0) + world.simulator = MagicMock() + world.gravFactory = MagicMock() + world.sun_index = 0 + world.body_index = 1 + world.setup_eclipse_object() + world.simulator.AddModelToTask.assert_called_once() + + +class TestGroundStationWorldModel: + groundworld = module + "GroundStationWorldModel." + + @patch(groundworld + "setup_ground_locations") + @patch(module + "BasicWorldModel._setup_world_objects", MagicMock()) + def test_setup_world_objects(self, ground_set): + GroundStationWorldModel(MagicMock(), 1.0) + ground_set.assert_called_once() + + @patch(groundworld + "_setup_world_objects", MagicMock()) + @patch(groundworld + "_create_ground_station") + def testsetup_ground_locations(self, mock_gs_create): + world = GroundStationWorldModel(MagicMock(), 1.0) + world.setup_ground_locations([dict(a=1), dict(b=2)], 1000.0, 1.0, 1000.0) + mock_gs_create.assert_has_calls( + [call(a=1, priority=1399), call(b=2, priority=1398)] + ) + + @patch(groundworld + "_setup_world_objects", MagicMock()) + @patch(module + "groundLocation", MagicMock()) + def test_create_ground_station(self): + world = GroundStationWorldModel(MagicMock(), 1.0) + world.simulator = MagicMock() + world.gravFactory = MagicMock() + world.groundStations = [] + world.groundLocationPlanetRadius = 10.0 + world.gsMinimumElevation = 1.0 + world.gsMaximumRange = 1000.0 + world.body_index = 1 + world._create_ground_station(0.0, 0.0) + world.simulator.AddModelToTask.assert_called_once() diff --git a/tests/unittest/env/test_gym_env.py b/tests/unittest/test_gym_env.py similarity index 69% rename from tests/unittest/env/test_gym_env.py rename to tests/unittest/test_gym_env.py index e333dd0e..770302c6 100644 --- a/tests/unittest/env/test_gym_env.py +++ b/tests/unittest/test_gym_env.py @@ -3,12 +3,8 @@ import pytest from gymnasium import spaces -from bsk_rl.env.gym_env import ( - GeneralSatelliteTasking, - MultiagentSatelliteTasking, - SingleSatelliteTasking, -) -from bsk_rl.env.scenario.satellites import Satellite +from bsk_rl import ConstellationTasking, GeneralSatelliteTasking, SatelliteTasking +from bsk_rl.sats import Satellite class TypeA: @@ -25,14 +21,14 @@ class TypeAprime(TypeA): class TestGeneralSatelliteTasking: @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking.__init__", + "bsk_rl.GeneralSatelliteTasking.__init__", MagicMock(return_value=None), ) - def test_generate_env_args(self): + def test_generate_world_args(self): env = GeneralSatelliteTasking() - env.unwrapped.env_args_generator = {"a": 1, "b": lambda: 2} - env._generate_env_args() - assert env.unwrapped.env_args == {"a": 1, "b": 2} + env.unwrapped.world_args_generator = {"a": 1, "b": lambda: 2} + env._generate_world_args() + assert env.unwrapped.world_args == {"a": 1, "b": 2} @pytest.mark.parametrize( "classes,result", @@ -43,67 +39,65 @@ def test_generate_env_args(self): ], ) @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking.__init__", + "bsk_rl.GeneralSatelliteTasking.__init__", MagicMock(return_value=None), ) - def test_minimum_env_model(self, classes, result): + def test_minimum_world_model(self, classes, result): env = GeneralSatelliteTasking() env.unwrapped.satellites = [ MagicMock( - dyn_type=MagicMock(_requires_env=MagicMock(return_value=class_list)) + dyn_type=MagicMock(_requires_world=MagicMock(return_value=class_list)) ) for class_list in classes ] - assert env._minimum_env_model() == result + assert env._minimum_world_model() == result @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking.__init__", + "bsk_rl.GeneralSatelliteTasking.__init__", MagicMock(return_value=None), ) - def test_minimum_env_model_mixed(self): + def test_minimum_world_model_mixed(self): env = GeneralSatelliteTasking() env.unwrapped.satellites = [ MagicMock( - dyn_type=MagicMock(_requires_env=MagicMock(return_value=class_list)) + dyn_type=MagicMock(_requires_world=MagicMock(return_value=class_list)) ) for class_list in [[TypeA], [TypeB]] ] - model = env._minimum_env_model() + model = env._minimum_world_model() assert issubclass(model, TypeA) assert issubclass(model, TypeB) - @patch( - "bsk_rl.env.gym_env.Simulator", - ) + @patch("bsk_rl.gym.Simulator") def test_reset(self, mock_sim): mock_sat = MagicMock() mock_sat.sat_args_generator = {} - mock_data = MagicMock(env_features=None) + mock_data = MagicMock(scenario=None) env = GeneralSatelliteTasking( satellites=[mock_sat], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=mock_data, + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=mock_data, ) - env.unwrapped.env_args_generator = {"utc_init": "a long time ago"} + env.unwrapped.world_args_generator = {"utc_init": "a long time ago"} env.communicator = MagicMock() env.reset() assert ( mock_sat.sat_args_generator["utc_init"] - == env.unwrapped.env_args["utc_init"] + == env.unwrapped.world_args["utc_init"] ) mock_sim.assert_called_once() mock_sat.reset_pre_sim.assert_called_once() mock_data.create_data_store.assert_called_once_with(mock_sat) - env.communicator.reset.assert_called_once() + env.communicator.reset_post_sim.assert_called_once() mock_sat.reset_post_sim.assert_called_once() def test_get_obs(self): env = GeneralSatelliteTasking( satellites=[MagicMock(get_obs=MagicMock(return_value=i)) for i in range(3)], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) assert env._get_obs() == (0, 1, 2) @@ -111,9 +105,9 @@ def test_get_info(self): mock_sats = [MagicMock(info={"sat_index": i}) for i in range(3)] env = GeneralSatelliteTasking( satellites=mock_sats, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.latest_step_duration = 10.0 expected = {sat.id: {"sat_index": i} for i, sat in enumerate(mock_sats)} @@ -126,9 +120,9 @@ def test_action_space(self): satellites=[ MagicMock(action_space=spaces.Discrete(i + 1)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) assert env.action_space == spaces.Tuple( (spaces.Discrete(1), spaces.Discrete(2), spaces.Discrete(3)) @@ -139,9 +133,9 @@ def test_obs_space_no_sim(self): satellites=[ MagicMock(observation_space=spaces.Discrete(i + 1)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.seed = 123 old_seed = env.seed @@ -156,9 +150,9 @@ def test_obs_space_existing_sim(self): satellites=[ MagicMock(observation_space=spaces.Discrete(i + 1)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.unwrapped.simulator = MagicMock() env.reset = MagicMock() @@ -171,9 +165,9 @@ def test_step(self): mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock( + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock( reward=MagicMock(return_value={sat.id: 12.5 for sat in mock_sats}) ), ) @@ -184,16 +178,16 @@ def test_step(self): env.unwrapped.simulator.run.assert_called_once() assert env.latest_step_duration == 0.0 for sat in mock_sats: - sat.data_store.internal_update.assert_called_once() + sat.data_store.update_from_logs.assert_called_once() assert reward == 25.0 def test_step_bad_action(self): mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(reward=MagicMock(return_value=25.0)), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(reward=MagicMock(return_value=25.0)), ) env.unwrapped.simulator = MagicMock(sim_time=101.0) with pytest.raises(ValueError): @@ -208,9 +202,9 @@ def test_step_stopped(self, sat_death, timeout, terminate_on_time_limit): mock_sats = [MagicMock() for _ in range(2)] env = GeneralSatelliteTasking( satellites=mock_sats, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock( + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock( reward=MagicMock(return_value={sat.id: 12.5 for sat in mock_sats}) ), terminate_on_time_limit=terminate_on_time_limit, @@ -232,11 +226,11 @@ def test_step_stopped(self, sat_death, timeout, terminate_on_time_limit): @patch.multiple(Satellite, __abstractmethods__=set()) def test_step_retask_needed(self, capfd): mock_sat = MagicMock() - env = SingleSatelliteTasking( - satellites=[mock_sat], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(reward=MagicMock(return_value={mock_sat.id: 25.0})), + env = SatelliteTasking( + satellite=[mock_sat], + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(reward=MagicMock(return_value={mock_sat.id: 25.0})), ) env.unwrapped.simulator = MagicMock(sim_time=101.0) env.step(None) @@ -251,16 +245,16 @@ def test_render(self): def test_close(self): env = GeneralSatelliteTasking( satellites=[MagicMock()], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.unwrapped.simulator = MagicMock() env.close() assert not hasattr(env, "simulator") -class TestSingleSatelliteTasking: +class TestSatelliteTasking: @patch.multiple( Satellite, __abstractmethods__=set(), @@ -268,31 +262,31 @@ class TestSingleSatelliteTasking: ) def test_init(self): mock_sat = Satellite("sat", {}) - env = SingleSatelliteTasking( - satellites=mock_sat, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + env = SatelliteTasking( + satellite=mock_sat, + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) assert env.unwrapped.satellite == mock_sat def test_init_multisat(self): with pytest.raises(ValueError): - SingleSatelliteTasking( - satellites=[MagicMock(), MagicMock()], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + SatelliteTasking( + satellite=[MagicMock(), MagicMock()], + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) @staticmethod def make_env(): mock_sat = MagicMock() - env = SingleSatelliteTasking( - satellites=[mock_sat], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + env = SatelliteTasking( + satellite=[mock_sat], + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) return env, mock_sat @@ -300,13 +294,13 @@ def test_action_space(self): env, mock_sat = self.make_env() assert env.action_space == mock_sat.action_space - @patch("bsk_rl.env.gym_env.GeneralSatelliteTasking.observation_space") + @patch("bsk_rl.GeneralSatelliteTasking.observation_space") def test_observation_space(self, obs_patch): env, mock_sat = self.make_env() env.unwrapped.simulator = MagicMock() assert env.observation_space == mock_sat.observation_space - @patch("bsk_rl.env.gym_env.GeneralSatelliteTasking.step") + @patch("bsk_rl.GeneralSatelliteTasking.step") def test_step(self, step_patch): env, mock_sat = self.make_env() env.step("action") @@ -317,44 +311,44 @@ def test_get_obs(self): assert env._get_obs() == mock_sat.get_obs() -class TestMultiagentSatelliteTasking: +class TestConstellationTasking: @patch( - "bsk_rl.env.gym_env.Simulator", + "bsk_rl.gym.Simulator", ) @patch( - "bsk_rl.env.gym_env.MultiagentSatelliteTasking._get_obs", + "bsk_rl.ConstellationTasking._get_obs", ) @patch( - "bsk_rl.env.gym_env.MultiagentSatelliteTasking._get_info", + "bsk_rl.ConstellationTasking._get_info", ) def test_reset(self, mock_sim, obs_fn, info_fn): mock_sat_1 = MagicMock() mock_sat_2 = MagicMock() mock_sat_1.sat_args_generator = {} mock_sat_2.sat_args_generator = {} - mock_data = MagicMock(env_features=None) - env = MultiagentSatelliteTasking( + mock_data = MagicMock(scenario=None) + env = ConstellationTasking( satellites=[mock_sat_1, mock_sat_2], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=mock_data, + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=mock_data, ) - env.unwrapped.env_args_generator = {"utc_init": "a long time ago"} + env.unwrapped.world_args_generator = {"utc_init": "a long time ago"} env.communicator = MagicMock() obs, info = env.reset() obs_fn.assert_called_once() info_fn.assert_called_once() @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) def test_agents(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[MagicMock() for i in range(3)], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) assert env.agents == [sat.id for sat in env.unwrapped.satellites] assert env.num_agents == 3 @@ -362,15 +356,15 @@ def test_agents(self): assert env.max_num_agents == 3 @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) def test_get_obs(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[MagicMock(get_obs=MagicMock(return_value=i)) for i in range(3)], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.newly_dead = [] assert env._get_obs() == { @@ -378,16 +372,16 @@ def test_get_obs(self): } @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) def test_get_info(self): mock_sats = [MagicMock(info={"sat_index": i}) for i in range(3)] - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=mock_sats, - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.newly_dead = [] env.latest_step_duration = 10.0 @@ -397,13 +391,13 @@ def test_get_info(self): assert env._get_info() == expected def test_action_spaces(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[ MagicMock(action_space=spaces.Discrete(i + 1)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) assert env.action_spaces == { env.unwrapped.satellites[0].id: spaces.Discrete(1), @@ -412,13 +406,13 @@ def test_action_spaces(self): } def test_obs_spaces(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[ MagicMock(observation_space=spaces.Discrete(i + 1)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.unwrapped.simulator = MagicMock() env.reset = MagicMock() @@ -429,17 +423,17 @@ def test_obs_spaces(self): } @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) def test_get_reward(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[ MagicMock(is_alive=MagicMock(return_value=False)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), failure_penalty=-20.0, ) env.newly_dead = [sat.id for sat in env.unwrapped.satellites] @@ -453,14 +447,14 @@ def test_get_reward(self): @pytest.mark.parametrize("timeout", [False, True]) @pytest.mark.parametrize("terminate_on_time_limit", [False, True]) def test_get_terminated(self, timeout, terminate_on_time_limit): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[ MagicMock(is_alive=MagicMock(return_value=True if i != 0 else False)) for i in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), terminate_on_time_limit=terminate_on_time_limit, time_limit=100, ) @@ -483,11 +477,11 @@ def test_get_terminated(self, timeout, terminate_on_time_limit): @pytest.mark.parametrize("time", [99, 101]) def test_get_truncated(self, time): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[MagicMock() for i in range(3)], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), time_limit=100, ) env.unwrapped.simulator = MagicMock(sim_time=time) @@ -501,26 +495,26 @@ def test_get_truncated(self, time): } def test_close(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[MagicMock()], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.unwrapped.simulator = MagicMock() env.close() assert not hasattr(env, "simulator") @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) def test_dead(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[MagicMock() for _ in range(3)], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) env.unwrapped.satellites[1].is_alive = MagicMock(return_value=False) env.unwrapped.satellites[2].is_alive = MagicMock(return_value=False) @@ -529,10 +523,10 @@ def test_dead(self): assert env.agents == [env.unwrapped.satellites[0].id] assert env.possible_agents == [sat.id for sat in env.unwrapped.satellites] - mst = "bsk_rl.env.gym_env.MultiagentSatelliteTasking." + mst = "bsk_rl.ConstellationTasking." @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._get_truncated", + "bsk_rl.GeneralSatelliteTasking._get_truncated", MagicMock(return_value=False), ) @patch(mst + "_get_obs", MagicMock()) @@ -541,17 +535,17 @@ def test_dead(self): @patch(mst + "_get_truncated", MagicMock()) @patch(mst + "_get_info", MagicMock()) @patch( - "bsk_rl.env.gym_env.GeneralSatelliteTasking._step", + "bsk_rl.GeneralSatelliteTasking._step", MagicMock(), ) def test_step(self): - env = MultiagentSatelliteTasking( + env = ConstellationTasking( satellites=[ MagicMock(is_alive=MagicMock(return_value=True)) for _ in range(3) ], - env_type=MagicMock(), - env_features=MagicMock(), - data_manager=MagicMock(), + world_type=MagicMock(), + scenario=MagicMock(), + rewarder=MagicMock(), ) def kill_sat_2(): diff --git a/tests/unittest/env/utils/test_functional.py b/tests/unittest/utils/test_functional.py similarity index 78% rename from tests/unittest/env/utils/test_functional.py rename to tests/unittest/utils/test_functional.py index d6db8f98..785b724e 100644 --- a/tests/unittest/env/utils/test_functional.py +++ b/tests/unittest/utils/test_functional.py @@ -140,51 +140,3 @@ def prop(self): c = Class() assert functional.is_property(c, prop_name) == expected - - -class TestConfigurable: - @functional.configurable - class Class: - def __init__(self, *, a=1, b=2): - self.a = a - self.b = b - - ClassConfigured = Class.configure(b=4) - - def test_default(self): - tc = self.Class() - assert tc.a == 1 - assert tc.b == 2 - - def test_configured(self): - tc = self.ClassConfigured() - assert tc.a == 1 - assert tc.b == 4 - - def test_configured_overwrite(self): - tc = self.ClassConfigured(a=3) - assert tc.a == 3 - assert tc.b == 4 - tc = self.ClassConfigured(b=2) - assert tc.a == 1 - assert tc.b == 2 - - ClassNotConfigured = Class.configure(c=0) - - def test_not_configurable(self): - with pytest.raises(KeyError): - self.ClassNotConfigured() - - -def test_bind(): - class Thing: - def __init__(self, val): - self.val = val - - something = Thing(21) - - def double(self): - return 2 * self.val - - functional.bind(something, double) - assert something.double() == 42 diff --git a/tests/unittest/env/utils/test_orbital.py b/tests/unittest/utils/test_orbital.py similarity index 95% rename from tests/unittest/env/utils/test_orbital.py rename to tests/unittest/utils/test_orbital.py index de44c1bd..6d649053 100644 --- a/tests/unittest/env/utils/test_orbital.py +++ b/tests/unittest/utils/test_orbital.py @@ -97,10 +97,12 @@ def test_extend_to_and_time_properties(self, dt, extend1, extend2): assert ts.sim_time == 0 ts.extend_to(extend1) assert ts.sim_time == dt * np.floor(extend1 / dt) - assert (abs(ts.times - np.arange(0, extend1, dt)) < 1e-9).all() + np.testing.assert_allclose(ts.times, np.arange(0, extend1, dt), atol=1e-9) ts.extend_to(extend2) assert ts.sim_time == dt * np.floor(max(extend1, extend2) / dt) - assert (abs(ts.times - np.arange(0, max(extend1, extend2), dt)) < 1e-9).all() + np.testing.assert_allclose( + ts.times, np.arange(0, max(extend1, extend2), dt), atol=1e-9 + ) def test_eclipse(self): ts = orbital.TrajectorySimulator(self.epoch, oe=self.oe, mu=self.mu, dt=500.0)

w1}Bo5z#0{BJwxK(|BXryCT^TL}Bc+zD~7 zMunKHqK1I#{S-qQY%nI2$7&W~Njzcr>wk8c*7F=5vf)4|{FThQ0gOux0!Cqp19Qp9 zg?xb;S&fRgBdi#42H)DWLWDHE`Iw%CEq}G;JJO3?$BoUhht*Q&bNVL}A}NmeLcrJv zm|zjehvo9k3v8{4t~5dNvoq_0WY4i>=Z{-fH9)N(_&q0)YpJn-47U>!*>e0f;Z^0o zJ5MeLZ%&$Cv4uN) z(E9k^d4>{9j>YKR@fRi4KA02SV5WTYLERx65H1GxTUJyKZaa6wI}cc3 zG}+glF(q!K%%AxJn*|h5nAoStu|3^O0V@`65LXFC;Gg16#jXvfTd@#jty*ex1mG{? z5aK}pnOMy-HQ=7826v-g^~`->tvW&C(918S2|~mEJB{_+W4Yu;!8HT|TCagh@cAcF z<*gRttC!OhW@PxDQo8k%zmhvwR3C<%8`9_?clSqjehIv3hs&*8k)%9mww)KVPi7~= z-383EPfxbIoo7#^!KZ&i+W`;0+qTzn6YPi$lQK7D*Q?XzjZ_9u4x1uJny4w*Kbi}^f%XY?nzbtp4QyC(9$V%K6P?a@R`HZ_f27h0nSbaK3`P|2p zbnfE$C?~dKA|(aBdCa3pY_w`hqRj7u;hEM`ySuQSWggB)*Een;MsW~1uDhoMse4Pi zqj6nc+236}O8o>ocftdC1XTGP1mwY@AF}4uc;?9nD_gMc5R7c~|E&rlK} z2&PoKnJX_&4zf)8M_=zX@BZ(F1lxGzY}WRMg4Fupidk-R_3t($m^rXK5J(sxTr3G? zX=xRd@(GgU;L|LW$&I((DlzB?%0I7gw`K^qz4g2X#_n;~B(Z$74BY0<@cvV(#sNIj zz$S;L0p2QPm?6_+3S|sJW4Y|CO+Kbg%l#CRpKsp;Y)L~W3P^U&O|0|M{;AJ?Rc!lX zVXSA1GR0HRp6-8pgmKYE+8Yb`ldiK@oZq|djz_-MHL|?XvQM-(@u*PrFPtj!U;A9Xt<~2JLcW(V?>H+Bl$VzJtB5_k z6sp3};)P2i>jP)BrIC|X?c{Y3aDwQqLNH#W`#ngRTe zl|J3@I0SYulz1B3e22g0t8tW_va-RANX=!z3a~^7wGYXW!EwM5P!^w6Z3QiNP|{DN zAiv3p>;c2x-*xlpL`&*R2n79hkf-wpH-h(I8%KdYO&Sa*c@m<;MnxTNeW1)R#Lyw#Fmy_HcLJdSpf?^D5 z)Rt?hgK=}@15SOz-EZM9e4PE+*}U!(H?i!zg3G_>Y;Y%MQj$B}YhIamb`9a*t~5i= zI4H*ZS|;_DHzstoQqv%6SXj`|(<5yG zZ-@d<6Pr3Iynda?!1ECZ11`7cwz(PWm%2b5NiCRH1TRaLXhrf#!-fv;>SbpeNs)2Q zj7ZOXalfKzd$0ES_AANW=ByIOvL4GjJ?s-2H^uWQLy|N23Q`f@+^%ekTyc)EqNRyV z?T6$8m5G|~4j!c5dc2AvT;!x|@-EpA)>Xk_?Pz!|8sK>G6jXCj%yu-+KT?0Vnf)$E zbT>BY|8sUEo1Qj1Pf^}Ufw7cwC39}%d_8)sWR1gAUQgh9d4O((T{f#^%Bh57A)VA3 zwC&yIG*Smt9z-Fj%_CCPJz;I9TOkgY!&1tRW2Qdx^5I(-K%pB_UCr};ZxWi6oNNZX zBr{rOAP_y|2pd=a>O2dTc&ftofASDmtbadA+iV|=iDwoVS-&P(U0oAqZ8TAOIvu4v zhA?Gylc{HacwR9^Et={%2ASY%`-h|1%6>ThzHC=m%=5zZ?nQiyW9Gs8ObT^lkXg5* zXOT_m9gp`b@D^`m`ItIqOIdz-qW{>$D@Ko+w}ley6Bf5Ac+dNz&2>7)^&Y>ABqC)WV-^vTcB9QyfRDT1; z<{z*4CbNR~Gd&Ap7v~BK*Gm1DGaTOUQ4oLgA>k2B_54x6JOLTACH&yw12Po4q%ah3 zXiGn0sKccb$t&sQ?|uyhs2XhKMm#$=k7{L4h6PAAMM(7?!F>mT#an@OW**Vb)FBD4=23Z)G;iyw;K4Y#@KesF7Y+EfA97)*%|>5>jr751N2G!kHY1FVKNJ=b+{uHn(nLzN>g6H(Y6I4Bf z#tiDLp@*Yim0S!lpcMwRqK;N-(}IL}tDg}5d_|5|C0cIU?DMxPQO1ncG+r)Q9di0j z1f>qzf||NC`EYm>LS2-oJ@%qX?)W(#BJg`3+wu`x#3?g}giQRrMB=ydhZH5pA)VKG zROC#?-}4I0R2J*|*ByewT9`^`S82NXfskze+NJ6HcVuN1<$@hjo-EpRd4wUG>CfG_ zcJ-+~h6XhySJ^Sm!K>b~hIonbuO-#wg0`$mt2vyO)kt{)- zKih$=1N1x7Rlkwl%rMWLqPg>A>&z@o(*2Ua7M3XP*gXdKBmt{0Pe0P!Awbl78dFZ_^~eK!HSZ!5Y6$G5>3`rUcq6j#DN^su|;hTM}_>vz!c~ULcNy) zBcCAMFMYFxtfLu=*>J7nCWfv=KgzIE5}}wXz<~>hk6Z5Ia$NB!gt(2bdt0>6yY*Ka zd@j5+c&B&6J!z%(Ps%&qd+4IrrQ+GKI@t!V{vpDKPiQh9y4teg%NI)Hcpl)%V6SzW=5i3lqYya_!= z7^TbI!AYPvh3RDE(ML}3+js~}KDWFjapwl8SSz{JW8g@v3lvs{4Y;|QV@xhHj5yUj zNRlw!vAwt~`{_^kjDv^$RuUho8M&IW;FpWh_j|GEzJmM6_t4`N#L3TjwvFpnW<(JUE_0AAUOWd9oYv9W|@h#+Poq+tV0 zsadMAH-Zzs7gw64U@Yi&R1Gzmw00@#QSe(cPEV!-sC2%4@;ZQD=Q zn)FUug16?Y_PjzU61=oerqCQ~J_vFU97x=-K_gYq%!o}zD6{1H70yIC=xIYXG)3Vh z+w9svQ|RB<{4w%grC?mb%Bx3+=o0NCTGWBkIXnbu``{aGH|DH?9PwRM#0c6gH9MFt zqN^j0wPa395j!Nou?-}iKRZ|2;EB?KadH*BN}aRkZPc0k-cdR{w|QwvxGkiEZY zHB9g!C*5;8W~WK+qvkeFml|nJv#h(Jz9e&7#xsk@@_mDT4uTS^1RgQ-Iq$hxWT{l; zD3=+_VstE_!u|*Yfw?jp*j()IBI&3>eRzr)=bl(M=#Rmcm<}CW>evErxFyPBb-hV| zCANS@CJPOVyO@=>&X!w&y)5HU%(lD-eiyiQ87{dhOpsljp@#%kasls&CYXm2GkoVnQYGS-Q2^h)fKPuEVl5BCJK$&r z@&RF|njvO`4n&5pp-b4Je&qUYZWg1uNSzW7#Ym)5`q`XcfqOhR;0T~j>&EAH7qq6z+zPP zhe~d21n?3tKrgv_5bu>v+Nbon#q()~@Qc}o2@Ow9u)y#17B{32k}JDqYK0?u6W69tZxazrCB~X^rO*>xY^N8Fc-3CZ*OA>%BXCj4AFUW=q3 zHXdPj_3C>YIOJ34I}?V#F^C&q$Pa%=n=HK0X&2$byT{;O00@V)`-Znel1VgQeU;MD zap%;Scr0rEIB;oriv(;{M_qU&{h<>C;Uyphb#3mNFcy7pCBx(DvBcN5o5J7SEwNWS zilhKWvKklo`!WA778Pj>5np8QBZ1+p7g~J=!SKY=zzOOxhGZzsH7{3(cxV|<;f~KpjE+;nU zs(bkh-06ng)@}B&)@5QMh|q#yZ~|Z(xYroku;Eq4NIKT+>Et!)o6RxR?mxhn9s-g3 zem$o>@t~$)?tda=bNIl;U=XVfYXanP7V%;crwxLaP9H3QE$~CYf4yj`^4~-cKv|&0 z#2gA>b#u;Plv9q+MhbAhDi&}`mt*Hze#JUR6w1wg{t4&DekWPu3v)=%Kc}iEGFu=V zU_nom;Zt&0?tNOV_k=ZXziW$iRXo@T0gL|~52*TnbXJPtt)|1%GgzH}Ozv4VHAo7W zeb^csYs!oV;#+G_viQF*xZ zBbQp8Vka%K9+%5?SABJj>}S9{Lr-|dMqGhFIg)YaSJ2d7ff}8m#%9a-IwC1udXv!B z#Ne(rG`va~usanw)}eHP>%=5^ldS!H4(@JAT-F599WYIBMqn=g{c*D4!{f@5lvnCv zw46B{u*Npsh?BuL|EBjbG`0-Bn~2Z~IYXC4jms}ZBce*^UuO%-0m3qzW-jW#zf&O8 zB}cK;!QX;>at5{9UMBAjp0e#Z$15`@P~Sn(a3e%=hiT*z+3;;;dXl2lr0^q+$fA^*X1Fgn@Er{eFQJmGDn zL|~)MYBZGsE5b#==9XPG0PgjPrgN(h2?in@#~E6>gponRhK`O7@aTixBc$wnu<@SN z!B8Ce=fD|K*}b%gEqJr-bu-d-*H2>W*qIa}grPjoPV@Aa&P)OpX!t||oI9i2l#1E4 zXq30x%-;9S(&;yQ`lzV;cKuw0bsnCc!3mO=;~#tk=2j$YiIK4aAu!d|3M^bhhXIeRK>xcLvhZ+Z zo{36_hE92PWBnv{o9HSQFnLn>??REso9(YWHD8_F{;bt#GqKjQr1wlzLk-YP6`$NdeO%mFD9s;`Jm@54nEZ}aos702m zzGnfWUY45};w+%UAit_3j4*ZmW>m>3DAe}`gPm9_2G~5%U%`-rNXhu}HLj1Q6UR`F zCC!eSj~5s5e|}TM0tpLF3$PY-s8pWv;Cnm`R#Fq%4|ZtAV(CJ*TmZFsW+y%$3vrOA zR)^>lP;q*lTmn>eyx=ca2nJtI^FxOeLq`B+Z2r3iUB}%x0|VzKA!pVoCmZl)n0^r? zZAtWjH|xFUp6}OH(S2pvk4dIci_z_|%t%CsO`5M$pajTOO0SnfKc3@y-_%7obx?-C zj4utlakgv^;iL$q!8bwv$;j#DM1z=OgB_{g{(N^(iF4@q8R^TV_CE=MM!0VVZzh`U zYW8-|&o3}NYnXO0fFmo93wM^3H*1$ygg>If)QsGNK34eT=#7Byn^r$%um_B7OmY4u zgH|%QR9Av~lj%x}K!cR;;%3*O7(9SMeR!1s$LX|Q5WyD=jH!B*%z3zEzF(U5mzw}_ zzqepxeTSI(N9a<-h+TQs)W}WUKE^T(USfjbQFl=x)c(ut(%Z7`Mb5%-IVH}L`->Rw z6OZ)M{+H>EXJ`dOHwX>Od`O}ngFO!43NtnPK>b(FDn4`Y2yuo3utM$IXK{Q57Y&$c z5rkoR0()HqJplXPCd& z`o;v{2fs8V?tIVIp%-cprc_Krn4+NKGKEthS51<&T5!AN{VK=fugE~KAS?|70JMi% zcJ^k1@SWJn31-m7g6`gMwIElD_79dJx+G4$H>zDj>F42cS8F^Tmzrvqk6xTXJCr|g zmVKCdjbZ{C6n@wQ-m|t!P5&{A5iy`ArUeqe^WxoXL$xY+`;Y$xjNO)5lUDqO{GQ zbA1bgbAlFAl2>Y~YyXDNZHU)17uDeMkX6txopQF^htFlRW0umkO&n4U23FsR|HSQLoA5)guash=kvZNtVLZOg@%GSu%rG!MuW~#W~FOMvO3~ zBofLAI?Ukw54&`>|LV(sLaKN!A2TRgwA|DaWFv9Ni7eFP_=&(|)SzeXNBD?pDuSm3 zY3s9Y$qQDBlQ3-QV*zz{z9oThoV8t5+t|(V7$rfc^q@IY< zBITQ0Q>y@C3*Gl}YOab8VkT%!oYDLjGN;>bwJ$Eg*XEak%N6(~{$W1xegnSti-?_$eAoELL_c44n;P_t2g@D>?i$C*H~mz)nTi#=z>39i=kJdEqlAw>awxOnc!|lHRqoDvy3C@ zKQ;On%JMHL zj3R}6(CWt}$>(=ZJ$wJpwz%}C1(z_^C&)oGLaL6Lk#xvM_|O!|ULSJqn*tR$m*7!! z=Ek^DsWltVB*n_@KQj$xvMgKX#~&&R(=rs$z?HksG|LG+&K%I81xnjy>mxX8nHUA& zWgGr&QI5tkTttgm#ftP9<@#;6!KlCd?8CwSw9DGtJ#*FgX9M8LU0komE)s==q`KEC zRvG$;w|~egR0iOEg7ito6#evGkE^8(nnne3plRFtZv{H`hlSp13|gGZ>L$*9Wt_~y z(fl+Z*eDBVF`-&V)EgHnh!s?Do;uS<6!Nq&WkKca^+pgyV0+W!VbsYL8i|XL>xPyt z#&Oh;$wM|pKEt^j^!ipu^s^ucfQ61Cm&*O?rS2kpWJ1%r1n38OZq!8 zMbdv|&sDItvs^V9{4}W|?g*U%6_)$MKB)u*gDlddZ6f>|sOwrX< zw3VilK9V=)We-}- z?Er4F?h$Vh>*aGH^@s}SqYq`wA3`C647u8Rt@L`nVsgQ6&8)G_JL2-?-$!r?Pz3GI z$?^XN7s-Ri8C^<>!K9=FtAI4Mg78zspT1h-R+4;HT4uN+IkFeH9NWPzyZz^Ep`$EysM; z`d+r?U29P0ZzYwLhM(hxRT;lDtEJY*$|~Bae8oHA_2+47rfWs^Gt#YmC4qafCXLi; ze`3b(qbq|fxhD{OOud_i{Yhv2SLY5Z51VZh{FcxC+UPd$JS@gS&f51ibdu-<8Ec{j zm9Wfmj>aAGlsq~XJQNXFyv!YJi%WtRV;(Zqc&mG@Gp&&H-X3P=04iKu-qSo^Di^Ii zotGzKVV<$C^4T$fRjS=wCB*vza7HEjaIOaq5?A*(Xh?ij`DDvek~Ct&j0BanG1?+_s7<2QDU+>I>E;$;JnHcL{s z1rUB+Fyd71v?R|ul}tr9Mu}X!|FPG4h~Ps@Fv3%5>) zH#nz&Vy!$9bxbXhb+a#`w%&V*13JMN?CyA!j4@+EldXk@g0Jl)oiabriF$8u&Hi`B zd;B2-{USxr>sP(PjrdN+cR4%#tT9JleNS_@kkj#@{S)g4>*ZrFx0ySPG4u&A;>aEn z+rzP5QHLLZT)Z@~zc9Z@(G@IPn*#wNEH!BVnUTN9SdUZ;^a9&i<-2{Wj{fJ|B+Px> zwpMvJgQ3Bs-P8~WRPI}lD!JYye7c2rAtsb`)0ubiB7td_`|DFBY8f4x6J*YD-bNmu z4PnF|C8GJm z1f7(Bu=Gt?g}?i{BE!$mCsFd}S9wzJbT&)?hPMmuR?-0UBEO-lHVn=el=8!woZ*ax3g_^&$NN1@ zbdbtgodlUGz#~xM- zt-E+=eH-?keBn54ACGQGk$R-HjM7pVA(nLBEyC3PG^e&xC!rjJd+=x+RITmg=tt(D zH{+jElWNj^d+Mw4Ac>j|FDYC9aGj-InsF9@y05bgRI7O7fu$QFS9^uR$VgJM=CQHd zOW;IW(in&WY{4DW0?cD%#Jl&SQ$8XZA-A&Qo2mOg)ntc2?HVWs+IX_Ja7G^v1kz|I zD(}x1Pw!J32Vo38%Drs)Y8l-8pq2%HuyX&;*rO0YlA*)x?dOreAA9qvW%+1TftPuJ zwUG6~w3HJ7?TQ)jqNfFfnPIBWRiD5M#caJJR3l-4M`b7LGIVv`=t=^cvgKRlRdko^ zRfI#rsPwlp*lpqY7JV^C>yLn_-CydaDI7eaNsz3i=9siZP5joVs;5!Mf();}0vgPu z>2oU8ohXZUrA{Azx*x4?A;n&dKT#=RC=rB7LvN7&3X6LKgMjBiY-+EYezX^P968Y6 z@SuMRV}CMBY`U?b29~AtnO7#zCntisyrSmw^Ei6E7-^B@{-gVECkpUCuzmT8{N()C z`ag97f;)hLk1b^tXoA}tY}8)v&*@f7}o!*g}IP}`+w$PXZuMLhNu#D zXOCLCg}T%}|GfVNHWBbIFE`N)!o%oEQ+srh&z4;uH49gbE4E^OZz30PZY{d7bJsA!h>wgl5{u9-q{NI-Ccjgkhr-$?{D{G=WF zp6x~?U^G$e#QaSARQe1-hCB|#MzJ0Ooj&|9apQd3G7^~<;A_N{J$$%^Un(J&)dLuF z-p{T5Ny7@(l%Bf>2zwGpPxAb^0vcv#f{s+PD|0FDJhc`0{4S-a=Wu06v$jTPL7RI{ zg*kb7*!GC@iwqbziT`IvkOY_EDXN;(1UU@j!(^5{W+Tj=@s#h?j?8;@YJ2`}dT1I4jUOSq?eXA@^hq z`q z;52*R{h*QLMTsG>!7Jx1D)>{N4@-Tjsa*sc=|R=w@FRP(){@iDm9i-;&OzZRO|8D1 zR-Z$r#G)TGWUl;5wy&XNEY)s@o4S2-Klzw=dr^SGrAEj4*xW&5XwVzjR$L5~-o~V_ zXH#B&30pf+zSscRTi1VN;gPNY_{Dyv$*@Zz#i>Ev5cyYr8|ROh1Jels(O;}Z36j@j zdecgfCKTWz_E%gaR48)bCOjNEK}x`H0AOYfgPHs9ADXIU8lUYrGPz85ybj*%sgT~f zU=u}@CD^j;ndP`iKseQk9}WQbrgDl<{t#V3_ihQ)S3Gg1{4BL3vzvTxYbIJ3w_KGT z<{7Ur%H`WbaQFaiV;+&{B&BUF+jeOh@cMX z{(fox7)ql;2{8T|=C}P+`olpM&%7Dc^RTG)BaNv+mk{lmiXSNFL(f-E>27?eSrHQ8(1)PHF=OI!&Pf90wZ5$YiQ&PMiV37pFifbx6q zUyo0ML(1zlJweadhK)xqQT$vnO+ZHS6+H!0RmP4h>?ixBjC6ow2~GNbaFPQQV-w2u zEgV+(OQ$yNvIZDDCpFuJXB^$h3WZivk^;ru&j2IOtBbx)w1?kO3ZeW;Z z9o`{uo+L?uVQ|EHNU|mbZbD(-TaCO;H>#{UN1qgzXRR^b(><+<-SyhMxjc>?GVIb) zgujFWHZR0NO(ZuVgRX*CW5ncMgX+W|Vj+W!8e0Czils5(cVFfuHzk}#{Hx&JhA6-Fg&{Vk zqIJ0#c^z|3-qTQzp2F%K8z|mtq@;`^(ipHKy(DU}gfA!-O8-96<`iixTpSZ3~1!Qnc%@J_Vl(iq^+;_A;FX_)NC)??^)~qW5PSXhBOmr z2kwx^HtoT5=e~qiozOMXtBfz~L`u52Ci2uBMxhEsW{3pg1CxihLqderf$Ukk9=VPs z2j0-*c##&sb_BdJ25gZgpvD$FW{tcA^h_KZ?S5mo&mD*`P}aZ-4YnFNhQz@wqMaR? zgCus5EO;ESyli>rwh~?6cuV@wBkLB{+px0$To9!0>ax*uAW7?=oI6n&ui>5C@o zr9S0A3P?>>mLuP-V=wqfeGwdB3*-t{I|n+>hY=+$xyC)9<19 zuB45IOnn=7lQ9GsH9Z(e!AgDP$&Rms_I4Gh9xBH+<7Pp?hKOJYtYX1xzTXiAv^{w5 z`ClR{mJ9o>i(0=51POI-bL*-@Oeyq)KnSYg5FR5Jb+~6K#AUt=%E7^L($88kTNbdB zehsmFC8S_f?Qer1j5JeR;Mr+?_QM9eq=p9B(-LI4`p?BYE zHLrb5qUJWt9lG_kHIWE(JREU41{C40z#}O|5}jS)r~8$pEh=A{+*tqT7@Um)#A`=6 z%56DtHh!^IT7`BY6z{*2$-tdHz*ae(h2EEn;{i7gJ`NPqqM zwR&`f)z(%_9gC&<_*^+=L($8c?-U|?b&X$@rZ1egB(2gylnU~Ku)Xxs zg?v!!FnU|wDAy?8;MHN}t^xaZHbrv(DQtd{H=7)oY-W7o)JL;FZ|M!#tjG+ihcb5B zMfu)Y+N+(A;<=!LWwUU(tD`gSq8c^^ad~n~H+#RYqyc>rmcrOrP9_jTv5()hT3r+F zN3*#8O;vM6H)-~|7WrR+nx}OEXchJkMIz+uBM*LUvw#|oWKqMq!O9ni$5dgk89-Df z=H79fpdZ2n<5L$PZjlEu{)y%;qWs|xm{TRi0_9IH;T9;2C$k-&B&|;B+3n^4+m_Du zpBgGmpsay^htyA^*D}BS&mbs*#I`>N@KtS(TXAQU)*ZnVPypqO#@0n|*6SKiK{p@p zJKRES0;yYg7E}jji3bI1ANxNEtRKu(^CbhY1*z6~g=4u>5MK`h*xykP({sl#issjN z=@ylo?q@K6l6~>r4YbA|Ds^wn<&}`xep-dLhr$gq7^&~>DQB;5ZHS-F2~RGaEA01) z#-;G7SE{x^OoPIOBJn6{5Dbr=X2kYSFf}4TG-89w0@)siaQt!zUnLgkv}UJ|d^ik~ z1cYMZwD|}VugN_;eVHjVe+tO~pc1I$0}z)%O--So1aO>_=uXZCa+Tj!7;myAA0}qI z%9H+nHXnL#ax>|`{0PyXO`YaDaT9EkBy(M9);}%Qq7kOZW~^h>v-mLq=jlKiB9={A z@*0?`Eiq zZaoR&Z-dxG@3U>z(W<2w_>mP7>M>SxdX5P&Vs#zZ$D*h}L9#!*0ZO30CMaeZLL_}L z^BLfTlAv*3M>xul#t9O{G3ZX+eZ*W?BbY%&TLpp9(b;6an3(h?8=qlA-X_(1D6y&yTGHjI*!QZIR zNZHkS%*zE74fcWbNb24}GtMd@0@*<2aWa`xXX5e$Oix;7t z=Sc5}yy`vo6e>Mk7Q2SfY1i>QK&}sgzh8dVOkG%MC~Uj#%A)=)BVi=MvLroncx}UPDAsn#5agu!3d@cAQu;n&SJ*y(J$db5 zIo`v=fU>iz)s20^00YA{9+F5u|?$drH2PN zr_`Vgz`L;Mt^4e83L->hA|o^Y+}NO+8Z=lI#*ic~=5cRSY1_kGT>LZnca9A?o~fK9 z+ma6H2@vy3bSwqRb)5}G5Qk_LYzf#gZJQe#?j)bhi1IfOx!t%SacZwtnf$xG;5D;f zAyP*N=I1U^-NL<%?~9qK{r!nvY-jT|RMeW?4a-2>SqNei)T;(BRE%_RKh~`~5#Mdj z;7qa0c_|&OBLhJd>ELFCRUQ5`q#)kO>@VyqWplKQwpI77LEIQ1 zW&zUePXOmY4^AryX&!-&qYH$G$oMnK+7rXR0(9X7y z&g0DX%ncbY_o#=M zs1}#7g&Q-YLk^K8PM+W$0+7pqi_+C5I^CbjZn^i>9S zE%t>OmYicfXhcJ5VRD=SboyjtTrhYNT*u8K(!-@mMWLaO)TzJEjiOA<^W)XB7Y1+> zrx$hNAe8i^Aj@#i*;=u>$Cjba3>vprHYc0xR6qh~ml`*?7STx#nHd3pD;bEt$OW$f z0(Z=zotp@efO8(*vdJi*E*QE<8PH%{`u}>9|IUBrVZ?sKZuzc291EZkEB_@U} zv;Uv{05Ikelc!C8$mLTEv#g)?1PoA&rTOzzDoT1>^Oq0_7R`yK(=@q%!Enj)hmUzh6}nwelElZ;whk5w8fc1$G5ZXz|~fUHRTx6 zDh|b;*^qk-^5~lXVXFsJ>gNO3OX;&E2`YI!uY=$Cmp~fqdkUbK$gng4;Ns|@c_zuL zznV6>4eU|hmam8G6@Z2*f%z03U-8g}pXS7T#}-vTib~2&R7YN)z^fNb_p|W7l{d(G zEhl2_s9`;<@2#m+o!?2dU2NPI$%zqK@C+wa*#QDP;S^^!22}AAJWuEcb=pdc858|N zOqE>W2NwE=W|sVUb2=bZB?RI^1RrBh-I2OC)ZuP7nhsMs(P+*hbbx%3hpBB4rT#Xn%?@cz4V)s-zAG8e71ezg9gdBZEHq#VQ}=I`R;&XF+*!S z9|BHe)#*C-{#nSi(*l%LSR0Q#*ayt-M66gBf`E(D}Y%G!v^~yqlF~h>H9oW)&cc|Zc z`)qu8UP;M>C|}+zojmmWu0G1G0F6_XM|P&(qN>mQPt&bG6aDzhI9;ZZ+{h#0I)JVM z6#nt4E>WALWi(UqzhMJ6gWGT*`M!V(iiot`|L7>R$sFTn*4Yay_%*+SHH!l*91Lhn z2lNXNH0${@X;5dx%qG?B3ELb<v*W$S4|&8ZRiOTE3m>ETJRy@F%IT;7lOw2k!W4Vxxn4z8oMrK{Yf0enb++mE(4{gPV$TKieS zQF101RPJl`MgVNwM+~9SS&X3`4d`baRCp9YZ?asYYI?$V5L~5Uqi8_@7F2BI{FX|X z)T%v^tJy^B!#XAj467a%n7LudK}H%2DNtWs4s)I<(dmCz;A}fU$dQ>geJ0lgc&ZWt z-+)4#auyb7#O~Za?ZtW>yNOiS3LOLCMTq6|SNDJC5yOa-fr&gTL30)F;2lrGNJVc&^M!iC#*ERj+fDtktK4yr0Fc%s~h%0{GFU zFK>)DcGnpEWDF`34s0*X?QZuL2{|@wfzc%d%02UOxZ(#fkOWD4NYeMx+qiqWe=_!t z1s}^4W?YCR8Y)uMWETy4%==3-^RT`hJ4dryZ{TY#&XkF29)pHG+#ef|X9a ziS&D*8#gEL7D;2^2#LhShsWtMg{IPCMX0!h$i!D`k2U&ojtay#w;buzKUb%)Ptx)E z!!i#?`#4Rdgau##i?uIOSaM~DPAvP$2-_8CntzB1?!A_Exj5nbfWQ*u%DS(e>~uYs zBO7w(Ma!3mb6!!x0`fYRToPJYC1wJ;uk^Op0(YlFw&@$HX2#rJC&8EB{0ECV zR)Ll>f2(Cz87I`P&qln4NPl3>DTb{y%Xk`?&uh_9I4bQX`e zpH|NBl<3O*3y*vU5&e67V4cL$Qf>7t7&;3Av6Qt73sREdKU=Rf3?c+_3T_i^Rh$j& z1@_|Xm3}|I3Ry`!RRX*i-=R@=iL3R@g+F0LRl^f%yk1h5H)0xkM&ou@{Yh*<>Bz{` zFm)bOWA7L4V8bNmr-7YF<2f5ii+*Ou1c2xrs-qq;~DCDI@| zNA2Pk!REGgnDeb2lb0vGIu8c@*BT?o^WV#V#g{sL8rIrVKkk#n($RphP-9f~F`I$s z9h5anu%C&vSj0&iT41c?&s48*Sx*9&h*y5CRK{9qamPuN*FV`-6c_=-HH z=Fo+h`MScSv{0a2;``%ioAXyX(3v!`uu?knGLI_A=!z6{wgha5*vQW-YA&Haaqoek`{Xl#lH`o;gZV4Q`RT)@^= z)_$?ZKs|rk&BG&Xb=8!Kg@wBzzM20V8Z&McuHdP{0GdL`H!75B%sY^H#0WnQZDMWJ ze-Q~>zlS$CWUwWe$^+P|7*1%e*2SfASSV^r2q&Ae=KndJUi$I9y-pF^Uq9Lc8wrP2eL?PQxEWZ)%|?I{OHuRAqEOZs zwc@hV&&!sTBj_x((%VsFh5_+7=h-!v!_L$Y0x3|Y{`n;JFHdA%SEVTqa?K#gr;l>E3!nT%Y1Glz%nkp__&sHa?Bi`nvg+OrSHs38BAIf5 z^6!TK1LnM}auyVuM#7UXa2VeX15{<~k-eSFa>xd*D_}S_%==WPTcX-!>Un2hgLShTi?ubEz!Ig2&p7%d8r(`8^F@#YwgU{dD*+`cb zIB<6n)AD%=S(_PQn>dUwc zs!Ty*|Je%^?U~u_?}lE2F^uF%a4Mo%lpf|c4b0n9Wa%$ph1mKIrMo8I50$(Y=cYm| z2e2a2DQ#ggq8C_8`?b?iPCMxy{@8t3fYfS9r`7?tGk7TTSOV_fP&&de2CV_=mqi(K zdhEtuBIIy9zEXh=hrSFzZ>n6I(oR1)*L}v~_VUh|s&6lO?oTc)dmHadjzo`90Lwph z)Xraf70V{B5EI|!`QzsQ*|1e+ob?yplVd{_li=$6{{U`8#bCguNE(lJ{{3>tz}THCIaU7`*pQvL#sSecWM-JE~v{y z;8EiGHr4FpGzshEpMpE}#tf$eA#u^ZBwsbzehHGwIvLQ?qEdrrhLccM!V%VfFu~7L zm&b%Z-TE0VOkbL;c(LCbkN0?X+c;~^`tXxG#{mI`QuW4CGteJbYV_^w?fVex=`icq z=u@xQ8Y)3?Jez1ZVEG@d(Q2&CRJs)IBV9>PZ#h%wBaI9P+=W1L0cM^fjUY!FJkFq@ zqgU6zu(aWN78a?Z7UrvP8C+3E9(K@#XB#5FpAeVUHB!(f%fUSi78^Tf0u^0K;S5hy?2X$uYv8BYwvwE#XUFACNf6-xi4t z9r{Xbx!M$$fc3nbW3jo<;@`?Hy3~L8h|lxY+(No-q9b+gdOqk0M~XvLgs6oMWPqD1 zp#n6()lQugj;P=jzfjg_wmFp)e6Bnc;j&=h+7enWhv$CxzmOxpokO%5)tcF}Lj4liz&G*u>ZxA3z-Wg8hm!{Hy#}1p=bh!mh6nHKv1(Vv{ z?YVi|SrgtAQL4$pD?I-H1J2orGMC;eUP|`!e7|gi;5?6Mneb%@xJAkBIAf(SiiFPN9#>;Uu=I z(aucB!*){t4~;)uY>MzNP}y4SKv&!Ej>YWslra0IH#&2bn*Spg1+hRWI8ZG1p#9^9+S@IiV*Ns!>ExLsJDU3V<-O}|9Q&Ct9uOs zqQM8?QlL8LmGUyTQ)4|NaKv;gLXp1@iZIUMPUczGvp;!kzkI#@aXO}ytq;#|OUiwb z%f6|$3N^}o8125&L#|A>?P%mH#BL$*mkQF>IaM_(&^lo@Gv5*0={ z;FSRzLLu-{@>VK@S(IbDrv)F{R+`XE{)|J3K#pNxK;8hTRGd9ac_*w(7)L`KpWomY6<#f{{p?$i62v*7M!N&brA>5l zyUg&?6RaLjpOXyV7e|Lg?RxNc*Mlc~YZSacIOIwKzYX*N#svSc0-Zz-U7le7tY{cV z*&?Rw{7;TUTz+zJ6i7#)-j7ayN)Que<-H>_mw%c)VkbuUdHsj+6Tujt@}`;z@H#AX zi}(^fs&|xmO}?1OkIByOy1AY;b+H~?6#?j6j-*N4e$^=YzhJM;1(4;ZcpznN1{%n( z^LZ;9mNPf$!-7(F1pH3+=s)jS0^4Ai?U#u!Jnh%I505);*{ZO0zj^OEV2E7*N!nfa zZT?`W$=ipyIh$HBEz^Dea63>jfClrM2ohMF#{%>PG~zQ8Dzx)|cauG-+8M8i z%vmT{Zma^cI{;P1>iQT2H(|WT;Ro4nY@?1tlrdr8lb4k9$i;?ck`LlYu9pmLm?=9TDxCxaOw6en?KCQ|{^DmJ z?X8jbaXGHdeM^kUT!)P*LH&B#hGL2gJov4sIj(nG?4xjpU)yky|3Uxa?k>1H0H#;) z?GoAbc3!B7gNM*|<@cc^Vj#?5*q~torOe5E77<27k@1kOvs{0GHn>EYKXL zcyPUJ?Sw>CcB7>A+WWbR+L!X5Rm%Z4AC(0KoQFb~#Qy3^t}mwdH}e1j`P1#M;MO>g zs*V1Iv5ad~;*ruQ?V zp68|b#qBM-#ScApHa1n(H$Y@G2hgZsVP$3Cp{AhlEh;K<3ov4;gTs+7n-Z!}l?nW7 zmX3WdH9v=SnwxTVtd9>@z{v!+qR5>Fp4=aLFzI%atVF;73gp0cRsuJ|X_r;h0~|~Q zV7R_90}^XO_N{-cs^VLxyC4$q5Z*COn~_|;kI+tUaU~F5BEe#3 zZwcG#7+ho}l>R1Iy3oSI&=F*8IypK+yk~kN;z4#5vfEIihimU|R&w0_o|s1_(RrE< z;brxWasR=#SnG8TSbr@$^-P)K2i}K^wCmDVMIJUU1>jn6c2yfdjO1WY_^u=zx{of(9qCohdpT;M#kW)D+eySEr`$B ze-Qiw0<;flHwv7)y)XLGpZ`ApEF*co`a(nVIS5%`^%J_e0Q|HrjQ<#%Ewh#*JFu)* zKv!l&1GEPck%9U7@=%evnxoRNzt0KxLcn^NzuVqKrQJPwmMAb6;eQ>ac6sF#^q_Hv zpP<}+>EH87vN!yV2f}YUbe%Lu9icJvw`?&vcwT<6;)4Yj5VxEqfwJ1UG5-JsND#gv z(C_&yC2j6s+`fYVv~vC5UgQ~x9eE>=A(r01-W{X%yL_A@++E7LxW5nC-8Io@cV~}J zOcc@7#64MUM9y^khYO2& zd7dCT0-w|_LR|{e4?RCunWG0_K6(#ziE;G&g?IFQ^ABwyrmW29 zqCO|nOl(nr2h-}Pko_*4U*5*m%jStAlH0CabQA$2jPZ>J*a02sGQU57mt~XMsboA7 z|CX_fR3`wG>O}4tPyZ(#=z_vU_519hI&g{ipYtv{+cPhqX1n2auPc|`l%Egfe7XXy zz<(P*&oLFxqfZBOz2-RC4Mart)I(K9v&XAFW}`mX5JQ7uJX|aA7E3^QYrPE%RDCpz zLZ>r`)A5AFQD+~;jlkwzeX{HC`1@SO4ABkIBTa;6HCFmArGOK}^XBcRf=`OJ zYor45NP8y0n3Qa@EkvV0T$k%l-Mkax|I@88Xar}UMw##^Qb=#O{GuvblF4QM-YOV~ z@O|SuuF2z&Kk#tBFbS~94aja}wHmUsNpSex**q`0VN%EGElD1d^u7J4GhIQH5^p+m z?!92y;cB0EZ{52WKx>tRmKLt~9}gBOWD7S1oCK`AG=s;QqHVf#wr- znLpu$E0p2Z4&4_`a2oB%f%!7HiB;hUVgvEe3aip7FpgjAm)uta4XWK%J{?$=#<0kKr5hK2^P2rRGji7!|?m5-mF(z0$Y zBG-WhP{Lz7S#MR(YnJCE)vYr_%+JqXikyp}94jf8{P`%?jiS67&&r|)hybW-?trc! z%T(`T?EL^qmRn|L+?*c%Olg|c5ZMe=X5TegA@PprphX*xlOf`=R^c}M z?%MBrfN$YJd)Jlzs!{()ziO{TF+~8G%Fejs77Z$JIBbXCthAlr0c7VuCIuqEUU zZRZVmkrqV^%g;T{+F0)8im*vEf-OY&Lh36^K47i@0rnRqpbOcu!_LkQ78(}K67cGp zpT}(Zd0aS+DrmnQ2>G)-6{y;N&*CHMwm3~g^@Si4BY}k}2L>cmf@@m;0IF!Trcp5B zcTmV#29S;(69kJq(q1n%{rrmBn#KGDZpg65pTIkFpx#!%$Yg?!C|!;T2G%l;Qr(;E zxU(Ml0ND$a@UGXqC-4%xk(~Lh+t=FJKfojxez^cM(y#x{g9e;O8-UEfUM*0PAydt!1_Gk;8snR#|wye0u3rg&dDUc)^C`wzu|DOD8unk6LZk`i zFoUW3!g#3VgP}A~xIDM@s(EXjmevzhce5CGpGMIhu+MgLq>OA3$m8G?@7 zXmYP{2e)|hhfFbozX7n`LIV&1<4jO$^R-P!Z7!q1FSQ0`zn)1J8d~y&i)A_gJZ%6$R!Purt zl4caYB}2tiK2ZyU#$XBf+!L0Uo<1O-DG;|W3I+^nf!H+UO+^LX9*iR+U=z@DYRLe_ zOK3XQ9!GB1T|U0&dl$p0A*YKwH?HKjS<_xhvac4oZuhX?^Sf-B%v=N+&e?v%5;he0(_ z#efCYC_G-Q^)h}%!ui1rBJR$60;H}5tWLWUET?S(YZ2K(uKS z2|s|qfg7s!@Rh^^C^m{Y_1v#nH1E@NY8nb_0HKhwzfmzQVuesO(xIZnKa(VGaD5y+e>m8JLxP*iN&y6tx$0m33`;jy!FfI1-{JC!{D<(8Abj3+ zrIIMMf`Nk?gJ=)Ss*2}oeONPtmoK~ph%WldP0V(7y1oh_fI`H6t4ZO`U>1BUug+$p z@!A$STo*Q5$B^(w8$gT+x;f=neTrM(J?Zzd>g+_0AageQ(+i zWuhbvH@48WzU|=8CE=QCZJ<&EzM!WG4-ozpELceXP~W8tuG-F{!Geio0CPwH&>}ZyAQbF^AN`J=Rye=f%rjU=n1+8jlRAW+M)muXjJGcaQX{py z(!rXWex5ARPRs%pDh4@6Z{J@(MYnsv&*<|(3>tMm7@)Esp>YoBP7N zLl6PS_h!d3)voT!z>@IP5PLYvboGDn#2757WL(MX9tn;oT&8=RyP+>}Z9HF!^(DXJ zh6xMmEh3+Oa8EG3&VP3@Ez7x@-rlECpH}t|*{J(YDT^Uw^5cUb1R_F6yp<^Xusm z0pyP%)YaAN+@>Ohj*kLyE6tG2@}dACXJ)f*tzQRpbG-|kgMd^qu*WssR=Po(~B)azZSRc zW%ao7DYokYP9)4+nSl=DG-g@LG;l-Hn$*AMRa zU41qw>Wm|E$pLEN9)5*h{^(a^vrccwe^nij_e@Vj9~E5Tc&%MK0!pXAeV^o^{wKc_^G}9z28)FhnN~Aq)2G} zC&Z(tASdP(RRBc)I%L88D{AB3N zBg;H%J*D|R)6u7H_$-tAnkdn9LN5u!EP?pX%Jpf=4xh*BEK~ZY+TRssb)}(?;(;R! ztdI3H2z0BeN5^ex>WGk6vDY2u=i_&nqg&YKu4<3A#PS8BgL~vZ<|;%#T;14JT+C`Q zxuQ<+heR;pX^dk1g9a$Et(6yK*|6Gyx!m!R#iM;jov%%lYHZugP#_W#3P`BIp#0Eg zAqXfj$|;ZST5DcTfrc1bv`*SmL%)bi;t0p3)3~ZOhAiS*5Ch^YFwl}&a6dGMQ&tsk zJ@0Ix8;z9lwZ4VHgHsV0LIp6^GKR=csSeV-W(T!8Kxmd!Q4f{5YBRQtZdhjx5#s!~ zzjhYCTGDNF{43knniq6tb3aF_$(|qZ%hI1%q;$0`;%jv2vn$2AFD>#4D6=*#>mwtN z;4XR!(y)*V|3+7^u2m>*tZME_h!VX8zr8siWvn-}csAxw8m&J%-Wh!|-6xwCnu$Z) zVJBmDb5bwW=FOIH{K}oY4pC3VV*__nlg|fNjR+!9BnQDpEy5Ci41j?BDz_D^R>v|Y zb&CTZqb(RT|0(=*v5dQ7-64iiPpYx!BpD5=T#^L%ZXBbId>$2|NV)m$W(tf1r=;E5 zQM`2_?_2bSYbdOqw6(vv8+FEG?*+|$U%+s|MQ#RPYa5@%MBRYg4?g@1K|B{cW1JpS z35OZRw>DvWNT)r7zx{g~VeL z5)uOO!5M59SAr&J&Qwpx%fwEzy)<>osy~8AAV586iZ%@}hTH}mC=ifAXd0c~on|Xd znQ%x^Y)YXrc6N5zLp+c-*PBHN2Lhgd9KrKTa|M8KE3H`I^K@`l^lz=a2JpjA6)TZj zEmmeM<6r@Era-aH;gQaOu;nLBG}bY}l08ZM5Eb?I$AA0YQ=GQ^7g0zA~f+ z()X{wKOJU!luRx){&~BHxbua8_4Rdt1=uL={ZE8`2-RcFwXu>ZOp-Ylk%iFI8o z3L}7?faw$i(<0NPbZ8Ze!366dJ?FXauaUIewhmp}bo0KX8c;E?bZ+4Hx3kkTBJ6s$ z;>)(?V<@%HmldXkfnRSw#J>DITKm}&8%rI=fCCAc1IJd5E%qT#`xAe~ zuj@mVF!)14RKy+(Sm5*;K@k2sYTaQ{!Vpk>R9o4z6QtCGqJ2G1Mypso8xfc72hr=7 zs)Z6}Az=foBswqw$BLmCV>U>t(L^i#LIDW^p zwspPuIP0kJ^&_@cU0VXQ29flDSHU)EaW`BHe*2Q?e(BDsf!|Pr|*AVI%Tl6mupxoeHmrGIEuyiE(MBx2Fc`#b<40RprKg+jnY?Pm`7gE;!HFpp!{ zQ6l|*k)3Edds0&228)4*%$sg?8rj0WYx7Bg_vYtxn(j-i$lhXaF$mq}M9ayhijKR8 z2n0HMdU9;+Sy25NsmC-vHl9F|*l?Toz{Ri4>mWn-%Hi=3yB}X2ux^kl7yVwW8x6YA zXX8=asN7w5&VnVII}5~{gUPb+)MDx!SS_N|Hw^a{!=3UXgy*Z;r}2TDlD0%U4hP}SVhQvEzm*cOzj9q~ATbov$F zhg%vq1==V%01*WxuuK0l^A~doeFp~z43mx{D%_1K#0s5Pm!GWV7H^KQy^2Wey^OJG^r%6xB# zrQ+aAQMiJg=n>Bmv}pI_O`ox9yvL59SKu1zLNPvfi4J|?+?Vl_c=&CrOm&>bX>4qy zN=}P)$KlL5nPeW!0>>&yKiqiI0{OnQv0c_L@RKO7%;EcTit5$-`KSUEmat!lKH;v}R zn6DNZ$ZcUORFmsHym`d%GwWj`G4KNEXo*K`HQ6#CZ~G;kJ~L|gC^>)c24tK`Zw6YU z^}e2dvszxIc+!g+Q>qSv+NE88kLbFPoI64p1Z z(n4JrrZ6LT2_d1cAV_aL4Bqm*7?m&{>``2%e52d4T)dz!z=2SzSpWc2fq*j$-IS}18L4_2Cxy1_geQU;?Uf*P7g)>RIcHSdr$y6p*^fj(>VJ7v;b~6 zdhc>dTl$w48~hv$xS2X#N;LK?PDU+nNbFxUjZYhQ#~{ zN*?vQfzLhDj-~6_wC42CW6?@xSG3#DquLsIXS=)Qbej|v660(& z_uszn1pPq4+2}-Ueb^=$G4kI0sfNMJ3<8OuZT0sXw{T)QNGt6dVSlZ{$dC7?DOJMx z013P|Dl7>XBgiBfczKj8-(a>$-s6mrI^wIsqM^`pvjnW7sAKs~8^Or(Vmi3LzH6{A z9XsVT-S$MTxUBj7UKjK7&7{?|KC<(K*dn$%WwZsG0-5rnL@!|k1h$yYr@VW*bR#lF zWkud~w11QtshC)YhhFAYI%g*ut`VUH+;S8c`+IIj-5?bc%0iSjOF0NK(rJI)?nfso z%_6MVA1Sw_cs^|!Wl7T38*=9TwNyunD=Z(fAm)gd^L@7^T4_w62_K@T$@wr?`Ro5o z+tK_E+kt0#f9CK;0V$)dR@JGO85ZoeYGAibfHZaoPHhQ$L)iE-HgT)aE{=3l&qg#i zMz=`rY2d)t{(6|jo(7vI49NZPrrcS}>!I4~+F3WoepsD3x_vGVw4BJ*4Dki@#zhVy zuQ4eI)@6A+GZfmX8Ok(JkXw-4E@W2fO{z)ho3|nK0NKy?KU+;MLb|;=+?FTjN&FM% z?+ewZTU%e>8nml(vp#FQ*-n8IbmHreR1Dv^k$L8*=ynzQiAe*lJW8P^r!^B63OhKpLVM(^2__!^GpK^kEfsl0ehI8RC4%uzpk221wVPJeH8BOMVo$I}i4W*?y3^^2H#i?=Z8dTuP3(SF^jwKk&^mJOP4ODa)fLScM4D_Y}y^MIeGUpCzMe~rniPJ zbWSHZ%rdE;48o7X?waed;vEp87}o1{zl=_v?1_Osz4`w zk79PncR$=ypoe;k-J*vv9a~xDPL!f;zd#Lzr;L3{-Gj23B&fQz{ntdxdQ@+PH6&o> zn*QeETNlm?J^#Zx-tY{6CfVyGwUGDp=&vXMnSpdAa8Z6$FNVcz-hIVJ#MS8#KYW~@ zKs-=~09S9LmZrr4huvQXpEP^m3EDu;?FvTih8;`A5cfN&RP5(y_jWsP+AG3179@ZH zJlC%kMab@j(bo=)G?t;1Z`C_tXBT~b50}-eZcBrcP-X_+bY^eUtxo9v?byd2`Z`@ zmS1xPL72~%J$#@>>!?=u2fQ%*M9lpZ&gsr}cGLdy>VMuq=mzPnu{P;Fe|o+8mFP7* zzh3lmp_}#CA2YNNfDnB(0liF%`Lq{|;E2V>#4yGv7-1d?_bY|HT>iP=s{9K8hxHfUIduKIeUzo$kL|Gjc#S7qe&Gdi~o`gnF% za3#fhzNLbHpm-L(-1}+BB;OV;E~>lgUe$O&WTBWSFcdyX|ygSYRE61!$P&6I;ZtYhfWHSpBXjXYZkG*DI{ zPcthn=jK;D3-6kmsyY&RW*s3$d%5YFIH+M=)(Y#QI&u0Xa#gdFVNtqdFBL}8(4bDB z9Qghlf@Sc>RlSiC5V#WH#3FT{1}6{|Doh*Q6w2H5X{(MG5%Vv}d1hC}VG_1kUh0iO zJ~bkiKb(?=^+ZG5~gX@E*m zG*ckw{V2(W%_IeemC(_p%6CM!%xnCdYAbpmV-WA@@F&kmhkbXpp|fmKw$85YDJt3MMZkU1tE z8n8e_$&Z_@om$rJtyzt9>-N?Mk()?q)(W#7QQ z#O3cYpk`&M(OZexp7HGrvnt42sYuf{k5&c!E>2#>tiI|>Vq?6lk2nd*wsh$fdu4ZX z>?#VQr*_nhiiZixO4`xMp)mUwN3f(!str!=w(5K$B;t);|X8}>vL@z33B^(Wi_AS_Jy{#6P$RmVkG47 zwcZuv3LJ8u2b>D_vR>dW)JTA#G(pXdMg_04wdPe9%^$oO&`dsV?|OJ|OYYm<6IWRL zhM~e_6n}@1_gXK$gZ%1RJF}^DcakxJ(Jw{P;Re0g4C768tvFdHQe_1iXHP<#R6qHo zZ@yO;mVdZj6>Q^%kKY_&MlPQHot`e&`ejUNPdW#Y2bXAWpV*}-LIHrTiJIPlmPy2h z{escTHtzn*9V@dC*ms>SCyMrGDBC)X*(gQ)?d=$yYD~!LEiSU0dzGS9>UE<*Of)W# z7X)CqI!4}Qb0_>Cf_nxQ1b0++2#5b>=`VfKxywNhz3HQ;5i0IdF?V5%CYTD&^tOnh zIaVucV0-%;P0P=!kKu7=mS0~VucKoFP1L;MnkNw{HaOKb*goeo5F+91hIN5FDycjY zv@Y>nqr?ySeu0Id+(}M3Xj}ABZawev=b|{RNN~NAY`FMg|B3TyCNl5?)`Z3_vm_P{ za{25?jG-P8+dia|H)Pk0npS*o?P&BXKC}7n&z~*yViLX|I&pB?y!?{CS-SD5V$^W^ zNz-R(+Lf$l2Ld$FzmmiJK?j#zbH}$;V$s@HwHkK@FyJHSQyDyRN|f-2c!4uOwQtwa zPmjq7oJ~hAd_0?OyWx;Iq}zi0;}oa86}e$AzYipegnslVNFe}53Ue53kBcL3ZsD2T zAWtfDgd6-!(e>f)>B{<3Y^TXE+Dg0ji@}YYnvPPDl8V@Ma#+HJ>{aloO@=+7uY# z%Bg*?&nF2tO>9L&UaH$(!xZ>--=n!_M)QEX(G-$%VEzmlt&}E!taw_ew2M8HJpAJN z#rv^u(bRT)T~548fIaM?4(GI}Q+|5l19m?%G(L%UX-J zXDqU#OMPce8yOR8dH~yXPcIz2MG5M*Q^PnLm4l&S=ymTFP{JY;C2u^A3NPRU)Z9XR z^h7ef{|1woKH94z>*L)%m}$Lt;uMN{w|jDCQc@|lmeg#xj`X+ZV)g9y3#u@kvhs~6 zH8m|7i3_X(rK!W~l|drEt-P?Z9u7zCxB&neCj85f;|IdL5|5Lvd$J0GmiIz_T)C}P zBkk_*kdCZqe59|~bQibwim{r1||W@PQ+ffeH)c zHQ3lc0>Q!8*Awn!<>H2VSBQ1**zh*f>u%lV$#Z?{nS|7zGY*wlp2pZJqG< zSWJ-Dwbxt}+EYCYG_;btyL*5A+1Z(@va)gw!xMbsBkxU)ZLwiUtVCg*^0sB~`z6I(kiR(0HG`KGqpL3jY3lIy}o+E^)SJrt@Wn zX|l;Z-86YsV%ngxpzW;}W@vS`LLx$zuPS>B zqRv`r@QtUvX|^XQ5FtI)w@(9j(SB4RJ01HX1SFt5hZWvBkBpwTUue}exL0q5|B|)j zh=G|j6DTQAc(O)TBiBm=n=HPS5&|;~Fq30=#+K%hYLan8uYt-l4h$Kfol+9TN?yi< zjfYbk`_KrF%r$A9t6YbjF%CWI3JoQ+_fm`GGmwhxqBDknovFg9m!R&zAXt->66G#q)}AR&BOHF&EWbN&CRGo^QRQ#-dfYw;eF?+$Jfo;i2FS!n{!UtTvNe4 zbE=wbXWVADM|`I-xs;6K^i~fTWkoUkuda1Ot zQVWBFs~K4QU;I_+?7c-Yn`1<>yvsB&`GzOZ5oAQSI%?L7ZKKU*aIm}R;+VD8d7X!O zL9#)#KoArXvWJS{u542I7y{ft^UO}azV;DE`;@`IS5~hP5Ot<%B ztT?#u!*N}|u?Ljzkr2h~^xGaxbn^XOy5BZJ3*V<{>2p4>E>G-^`lDUQm z?|R*n-W^N$KFqR@`|tC^>50)JqNOJ7{y~UoFJeEiX&dZiVuc__Ft5qBn5o}}D+Q4| z9h{Sja#67-Z@uZ|+s3DB%@S#1ysQxRTUpm~7Si69TIwGZ`0c?gA__qgihwNG5EKla zgCEJ$2?!d$o`;$erAH_fO6olf)OO=QgUC=1mRcCw+b!C@F8sP9ZkN5*U!_Uq_h429 zY`l5nfzaSNq%qrm(uQ0wxz1s=(USBMlzF#c=5m|Qh{{-Rr%j|gI=`}dMX6gAkcSRk zeNUcb@>lpe=7})*%$+{bu##oDN>q@c@O6`krMXjs@WX5SLF>2sxU#ZC>yl90;gV&x z*kO~JY(hNPq0Fvu$B^l+1D~__*!DUX2Sv$AiGD`!Yx$6BWNUrCIG=?#2V2 zv0R?SY;A@(C3ann(}eQFkJQDS94~EcOJ`YlP!9^116?SiUh}sprDV;r%mVyT<@4nd z!+AoxzV9GhTwDxdVx}LY6Lf78(%drIn}iBl4K)+4_g~;kG^2;VMbxgZmpp2vPWOj1 z3ZP5aBHkB3AbYN3AAfgj&Pp3DIygCFuYWD$zF9A8KM0Tv2tBT_eYYaYSihz;)e{uu zS*R*?etvE$^)@>9i8qZ5$HdvUes&%;gc|4<7_K%)^^=J0p6gt!*zU!WBl}C@{7TDB zO>bTh$k@dOuG+rJ3lVg44JX0DfNeM13N*XjLS*iqjf|oqUfGi58O>&iA|m+WoetcM~)GV^a!zM2GM1}Vtj`()`$i}Pu!;w&5w@}l2_e5+6-&zf<* zt$yb?is)tU+Rdo{O5C+cCLQS}@DyxEp#+~hQ>I$zmu<=P4Cb4xRk*YQu@dWRjr+V*gC!c=)vBedRmOm!P zJoXIM-H1^Djo9uZF~mY{m$TehED@h+8XO3a5$adjiF}};5Yw2HMnh%o)~sTg zk}5x-kKLcij}vQc(3Z}>Xw67F#(}~2d2_!jw+C@GIy;}$N73*s@@k#qwcJczQRdz&H@Zhwy5^|q~eQ8 z*_VzFB4{x5xIc}sHQ#Kzajvc}i|ZF#m^$CyATD{Mri6sfpgs=P=QsV$*gZ2#csrEp zWO85oT~vDF`{6%WP7P(3RgjTx@fkoK+3p}p@s`Eq*H*Xr``_o&HKNHKmcEUign{PH z3cj1m7mqDoxem!JfAH-ybl9Fry8DFy6Myv@VL)CuHbA8>NnLmZLkiDACPFb z{|d<8Omq40+=|geS};zC(`lQ2S2zwM+=#`SL&W$oEZ%Qgf0$(aAN5lm?;Ac6Y8G{F zdT56mF?-zL9>UFH2gL{&p|9^Q_(@(cy1;}dHz56CYRPj*m>Czql=#r~Q;0Zge)D$@ z&IeE~>cfvgTxscHwDHW=G~ml~P7c#2;awY%Ow-L?13_WHeyzn^+jw8rX^!q_iPmh3 z+pn-cvdFoobpgeWgxDTke(YLbNxCNE%p%S$|5}U&d)@CCj?mN8d}o&M^Zf-?*ngKl zWgtFU(za;6w%toc7C;NeXfX^xp=au0^&eTc1T|r{=UKjZ%oO%RoyMb`hqG{D+zOtZ z@E;^(H=Nz;`DDjB`9s^h92ZF`cY|eAXZR7$@^+1&x}Zsna!DIUCbxjY*?k}D@Q-%N zZ|DR}`X1-OHcneL22W~)Xx~YQ$_JSv5FdG5xWWh$k%VAst@QNm}>tva@gz)Aj&-T!d zxwFvLB+#05n2}Rbf4UJ3NM;oD_sJDWT1u8L%gNeI6FXFo+{5)JU!9E!>7asz z8DB_t`l#{6Ds^ciZbFby!WQW^N2@ZdT;|7&*7PtR=u~Si$8VZp3}3Z(L*Ir+NJkAC z5o%X{>+<D#QO}M!AlO3%Q1%58Y zx!a8~y6ng)JnRJS2u+D>(>S}Zvz5NuZU>x#qEXJ9P~wqp%m;wpfB-p=0&KL-1k?-I z01zSDCE9)a&t+v=<02Ou2~v0}VeBhR*-&EW{Rm+gXM#p8b6YqsGa2Xdg{zA>3}xLx z{ceZ#ZjUr{6O+sig#bM{=QT!_=k=%F(CXjAsWsA-L+<^+!))lFsHGx0#%Wtdbo=*B z!5Zv&V{FX+?`4bUd9^J4-nVGznBS#jg$`XTk8WG2x3?E|unth1meDv6} z3m>piT$BTTL-Do%y-wASl^VPLz%D$$t8Klw@fTRAKC+9@q2-+k`KbD>Y0}-kOu%P^ zY}`Sw4-*>ODZ2^UMLE|ca+}Klf*3m%V`Uw*)ps3L#C&!4L3w54k4gQ=z3Gj!$|0Nm zZPpIVKo`@?N{!|azjM)+$^ktJ8Q3+}VnTLX46|WHw}|OupNJ^k7EPjwF8Kd^2O083 zB!05lml}MhWQtnV$)?ffBg2%Ap19MU4D(30?B=jf{=UUfcSOyy=8XBjtm>3ZnB@o; zQPfn#A}i|;cX7#nbq-l&N$!PlH}Ut*_0e#Y{Cgw)-vSzQKd(NpMOW= z*Q=ck$Q%yp>ZDtC8;cH2E3G-c($deB&mYA{A-}D1LAr2a%7uTc6vK~;oGX>OPLIr@ z`utzQ|4arvxA+n>EKsa0z=_Fu3#NS))Gs+!{n9=)Sa|!b^!Z{{9=2SoUH2wI9(Oeo zZvh@{oe@D=Ve6=5^Pq}}h~1&Ddph>x_)~fhK0W7_VSTm_XXUWUdW0XL(I{OEw;Aj)RX4jD0KS~M!2Bl)F}93MSUqUZb3p~4*E+f zz-DEj&E4wCxmg?TwDR7nP5mGJ4e0@HG`DBEl>_zPRwa~14PJ$OS(e9Q6wBE~*RI91 zZFJ%nq$Vm+P+7;4ym?@4FhbGV68lV~l?W+^iTXh$nbTVuw*T9Rn@&pl$Tiwf$q9eDkjH=5Aiot` z;x!~*u=WJcGsDl$W#g8(#@#FUpKXRgbWBT&6FCEyiUFQ&nd!XAf7e2t*ytX9LiJ;_ zr||c65em>{A&p7V{R;Rb`g+=-J%6#@5Bt3jV)_&aYElNzi`O)Nm!c&G+h45lLA#7+ z0us*&Eru0auH4Zcysk5kMLiY*aXLzWvBkNu#03PM^x4W(wy8_c3rz8}E)TPER|3DF zR0j%0M(R>%fzfM5K%drD{L)q%LG~dMkF@dstJAdeO{jgqL>xh8#Uuho!yM>(-A^%Ya z5(t~re#JGTOmXt>Eqv{*t?GQt6SgqoA0V62rYxLpCW?GQ&-Cgfa-81#GILjhgxyf4 ztfVcEN9^Mh943ZxR^;4q!<>Ex1WbDu{P=}Rz+^u2Swq}s0;#Zy`bni2+gmT*G4{Ex zv51~u(45AjEc{uo)0;e`W5r46%yo+xR`KLQelEMxA>5Uf``y-fdt)V71r8k9Nfh}Z zJv}Nik)wc_CSPAaUp4*u^z}qn(GsqY_cl^GM&KkSZSY=0Uk?=jz?V-v71Xy=;wI=II3`fK6!}*1m4fKkX-7|>?=j}k)tWw)Su+7&ZYK~=SK4Q+hEyk_uix2{{0ok zz*CiRlH4?Npfk0)+|0^l!uW_O=c~`xVhd&sg%wP-DxSpm7$GwMgQl*4V`E3Ne+H-V zWTC94`%eTnY?381TQjJRW;ZJT3qozz%bTAKj55glzme`X$~wAXzLDdM3&3HRP|haq z8~G=!7ZRiJ``XrzBNb>nw0k6gZ2jzakRkdC-9US(-<;8EKw^io>0a*zaUZ z*P{MHb^s7>;*IbDBnFIrA%%T0(T%Omy(Y8zMMa!XeGJ%w$3h%XTd?4oAz2>i#LsHx zPy?5j0Spc4*7{jS6D!jN%l|l1de<9II1+gI^8rL_IkRR(OmHB_7l1bI4QB#WD}ohx zK=otq7D{BSpJTR8O^YV0Ruct;jo*(OHBs=VK5T)BU*sGV5}IlWe=}&22D^RP=4O2g zJ7Zw#dHe%~ruEA734_ah9f!r&!%9c*-k$e{V zXdIjeI=u@zQGYDIv*X{X^voknCk?UEgxss#ARA7nET?uK@4HN&XKg6Rii}!V0+2%C zr}iD^N>MywN$ue1BUcSCD^c`aA?vlMzmib5APtG6Rw)aEgsrO<_+Dnv7st*`- z+I~yu=fV~ptQk&ug1p?>3K&n1(gYQ$$d`00CG(7vys6P$wmyRYG#Y=1jtshOwr(ll zZ{|i-Je~_r78G~58Ya*mh(3^g>&X0vAgqsqlF;=yYFU355|F#HJFvE{bFfDpjJ|jv zS*H)8e)EiudTCjM^$kzM26v&YplRt194-slT6QEZ=zd)IXIy>`_f+PMmiD+*{tV*h z;mEe{SMa_>2YgZ1*XK=;nO19pF?Acib9DXzi$+Bv2k5#$z%G4( ze0i$diHj}({6|9~TYv59_+)=wCNVP6J3gcpemrGE-Cc4E5>{1otM2?x*~r0+oxj|% zShH7=N^owJC(c-{A7!caFEmf*yEeHGFbFbR?3Ok>PCsSQna;^L{F!S)6WWz$dNB@U zaXlyW>3To`wPr&Q(mS($tA^Alg|)WM{Z*hPo~A=5F&2FZ0umi|jk6GcUg?U^#QoKL z+BoUti($zD=zno16rv+m&EGEx9E5-dny%gx9hI!d{M?(v@nWMntJR3VFOXE#K~{;D z4+E{8G4j;jSF=EbFV^-jHH96$<)d4!5){ailQMr})AG0dDk}OB_^=}jOV(cE|D)+D zqq1zeE}hcSE#2J>(%s#SG)PK`ba#VvgLH?|-O}CC-SAyE&-bo{YkB>f`^=dW`|Lf# zV}QFin0)H`IHZ&wN`E=5RdKl#XWiAs@4)@=7yZr0n{gAysnQ9kSoy$T?^uk@EL+;N z8~nU{Y7{E5U&!WM<8DY{VPVs`>T)W4 zez=f0?upWZx~D0T^RAebJ*51}pE`NV!6FZ+lhz#9SKbNKWQWahs-!o3(@I4mryB=m z^@h>-*k-9b25AXhAs9YAwS)7k_`z7~Nb!QzSpadihcEHg-orOP+=gxm|2SkjuzDqufP>~jp_;8s zKFovG-@6*}UuMpI?Gix&2?K(-dR2a2rB>X~j={m*>*P?mz&Y6uOwE>+ug*k zm_Xa+3sukiqNi~gEFDW%H-@vGpr@aibjaV{hzKwf@w`munB@ATzIkF+3BC4Kyt@6y zji^W$RyM#xjE%|L`nFAC7?FLa+%(0cDWF6t$!IoMPHm~K^~Wdk;7(hl-Zd4a^c=B} zR?gxi2JS9F&XWJtgw}^a#MrzJESC-LS=MN6^tJa4Pt|7T3n5Wdgswrw|JW%~@?eKb zDmmZ_0Fgn7J%HorZY>pUz9+{g|Foz#=zLjHotBE%^^3-qZgKqpy_*rMiE`sP`}+7O)m z%^Y%fhg74uyD56Lg}*?N4v(uM-(-F)9`^P+TK_WA1RHGNy;O#QicKL6OaQ|M@U297 z;c7b1EQ*Fb+OL?V!LCL^*&Xl63h<3@4H5CGeJPro9y#BPz%F#Frj9d&my&v}k6`c6 z=08=hcq!^gAn`?3w99H#PZXA$@5a!KyGA<}=DUblt08i!w&qU|!tvh?8kG^Fd4!Pt zUB4Mx5^%loO1?q*4FNYB>yOyx?Y#HHfFo0Uc=H%LGoWjEkrk}*Pk({1 z0E`$8nv-xx11u@N=E3y0R19O(!OR-Le1HZ&!l0&6pZT+^E1i#KD3taV)qXomeR=zX zsP<-}zMX+Q`!VVDb($M_bK^h?($Yu4II4AI8hAHlfJcg0Zir)IjXX&tqFmo)JEs6Y#op~|p6xzn;v^kiW4r<^4}r3Ok5m*KaEHcJbMA1G zD(UooN>Wm%1{oaAhxedsMB=}}y#@ygxg=l#i*-UOY-3#m(5W)DG<##m0|he_Y$!b> zM(6?6okGHrid1Q{ng)m{)GjjzO6ORuHMR9uQepuyVZH4@tr z(~X2^QShveIV@cWr+cvgN{EuaVWMo==}T{*PxUyl^ti(GbFcSR$9m{|s4yfr1?q-4 zw|*LDmkNFx%#Gfgv_mn?|A^SmpGd~IsCdx)nci258nCvV{4D3SL_X=7r#RE>3m0Yn zxc%O7$>}E;_&iZ<;fAZ!r(t-h$lXz@qz22Up)Fk8@V9=mEsxV&d;R(W+!@=QIR8b( z-pciJiap*@f$U*SVWVzvnbkTO=`8K%mf!TU^Y+8s2L4xL`~6D(IVm)&&}VfIcNq1i zT_z4cF*0E(6S)EaqAX2oxwFv7qI<5ab!%_Tt~~DAd!-L31GuX)U7!^~-IK7e*wViC zu|kb;#{-ri;(k65X7532NWPsum9gB#w*oBYy5 zFYB&2RiGo8K9}qwleBzzW)djx&dZcgdRv9NbILnGfl&@wf{WGM>p6S$vR-4hnHEle zlMNe6yB@dnQ+I=}77_jn_uChXx|;1=M4t z_YuLNRhSC)*8a9g-C2|dsGN?5vt4%&@O|DcAg9}FGQRpSChTX;%1)up9=R6huc0q0 zR7D`EUAi9?J#UXE9(ei|xs#{<^)Yy9P>8|-3aAXu?qHR#j4Uab#FW;F&2pBf^zjjS zi%S!dN;$4Cb%j){(@d@)89Fc3r;laLSv#H(;H=rks=<5hY{&}na>!{`lz zAwgZ%RZS&h-YIE2Wurwlz7XXh2j(y@QT6qwu`dr1Fzsv3Po({gyklCgAK7nY_&2Fq z2P@NfD-eQVwLKw8Q3pDqi~+Eel5t(t#VtKMGlq%|;sYIDH3X4=V}R-wpy;N4u*cs= zD+fMeaRUaEy%pzBPFd0qF=WZWbi|0zHeX$7ShC`)WD}Bf#qL;EqT$Reu;bIA2l$$z z63yn~zCuU=EEF=T1o9e`d?0IOrYLYR66XQDlEJBqznaIPdp^`$+X&{AImx4onI?n+ z_^J_^1Zmt2NDciWw7l_p64ye1t6#a4_gjj!ejJ+l7u+18hPrd16S+$aqlg04Li>SnC_VuN* zd%E^DY3kIZPr`OO;X3gNx0>No_s;m`ot6@Tdfs|i))Ka(|*YZ!2NxO46uIy&FH*=s%ciZ|2C z!w?v>6mDV2fQS&z>8i7A7KqKAio9GDKQWQ7%eW`lE*pVry# zP~Rex%Y3fK7^Ep=QUXHc@Ka;&Xhiu8?IcSx`Ap#Fu2)N zAOvtogu?&-bW5kCOjT@j5&lzc95Q^=BP1^Cwq+C^tiAl1NFmK zWZwEJy2W_+<1S%CKR+ebC*}`8N_8}IRzXoXwL4|jzL`HF*z4=7OeyLPuaN4-efM2t z5TU;Zv+h4QLO3Uk4xRA&Q8OvLXL%-yDC@6g>1mGxADQE;Gakd!J<5>A*qcFe$46O! zj!Aw69=t1NH1W89`22bnNpN`%p9@f6DY=*~nDPh|Y1l6NG&pbd)sJs1>lA-u)IF-^ z5Xp}bQO)`nB+`5N>@tbUhjvgzu({cs~3HtfRkH4Cm{pBh^i%PAF%hczOyw&yI_6WDLi1zcTA6z;xV|lx7 zCSvz;^d9;~!$^}4{dNb5iMhNdjy2T%=a)LXM_n87nYp_NZR26-=x>p$?6 zPgDR3r`!4bTOP3IE;?v_lxt9~YU`vxMJ1WeC)O(JJS=c^u15|WaO|5rt6g#do}cQafYRHJr>3cIf8;xF38!A82#TIGu!d9jm}1%2la zF29xgDOXvzpZ4lOG((zF#_AW&ECX zA8wO)s^~D5UEJ2m%FbR&H?=d1pIC4EVhU1$#6MoFCih&=XJ5rXf^CCh>R>1s-gI#i z2c|5_5zCMIYN#FyETR^P#?J;KNK@_)OR zj-b}nt9UC%TWR;_%B%;@9FDaraYC<$u+3gX1erO7O!=a_&IQIK&jo1k2>8*gwV3WSRLBBUC6Z0n>f@D>e+!2Xi)wV>N?a5Hsi1@M! zKHqKD5u!^ejB!FGEj2~P+TI{(p)s%P5Y?YipN1zNlMnVfN*iP8cm2vpcMT(>+SiTe zYsk3H%WHR)QRKJzs_3&qU@gk$)G%IIQ$jXm?z+AtH!b6SGJ-e4nqDUzXykR&r$srj zV^VK88U;jrc85*ggUASY!bT|4$!bfs^a86&rTCKwUDW3OB7@R&B)H{WNQ$KBTkH;GNk;UnQ7OW5EL^I+w+sqNVUti2zB*1s~I- zFv-a!>ElfgO~H(Hlu@sojsnsW64#PtRFv3!qM;m1osw*g;aY);>5sqN6pNWiN6@$k zPhOs_GJ9faI;OG3!^6tQnpi^%zkk(3HD@kXNGnhCDvKS-aoQkf{0U}yn?pqne$PhT z0GEZ4SxF8Sw8%NMd*|)!X8-|@AS57wQJ+_=8c?I9LVzz5_d5%`Ezz4mdwygJSPfxR zu2w)AQv8F+ccOL6tLeexySL;=*h|#*DK^0^48cW3oB5koU+bbJ1c#N-_o|$AX=v>s z%%>pVIK#5yMg5M!W%aZN7IQoSq_=0vtY4d-GJ))Tkfqs=f69&a_?*GE{)Nu3=tZ(R zl?}>AL>LRI2++WEv~eT_F=6rK7AtxWFnUhp)!+bXZ@{`nXyE3)Y>if>|9xMQD8;H= z5=F#O4ENw)b%Y+SC8l#BaQKl(`1^l`3YVR|zJB9WZtTU4Wuv+-pfPQe{tRxh`+4KI zYR(nl8%Z$ol&@Ibo8k{Ljkbzp1tN?Q&DTI=-uY%o^HnF}In@Kx)z zLP{>X^)I9ARP-qT**^=OjE~N>EfX08W7}yw{9Af%gsO-HogLcuKOc z=C?nMe9Wxq4Z>k1SY%k&ed8|$`EeFHH8pLa>VDNs_ZkB^!{l5X<{>@Z%(V0#V!98I z_l>bVAAcx2nmr+)jx(OBb)6`+dU%kP%2ud`RA%R3+&CO-7^}iFc^I!QX4D=6sb0o$ z{uA*RfCS%Gtxy0a#1zqw$Z-J7iLDj~{NUba+RE_!SLa4HOG+*91jnB?zNb2k_+et= zMS^+&FgR4!feXs_S^ysIny6{VvZ&^lKE%xTRNF=JOo?j;viE6L?{E?9OW_J6i@LR$WT?PYH>IeB+SYD7D@9 zRIqlMPrURCAsU^m0T3^RNRc@msWR2o&*CLbc);9U8J^t^RkR(o*d!x6-W3}6jP|b| z_d6zx0MZnFS0vH{0U4a&0*CvxDtx?!RVIM2b!V?kB@Y{0dqHVC2zOw=WYl;7a zo`FpVTmZwrB0PSA2;>k-?BGR*X`BL8Axux7*7d^>FC5(|u#?ybdE-jnLm39Q7jM`n zGKf+^Z>h!B#%jd}Xo}uCj;Z4mKn9*WGT>_l%@dN)rYQ^^suOTC$W*|h&-*CWCrD~g zqTTIj#l}^(eMH~us*xc!VN!1nt2X;-AnSG~Nhux)#!gI2_?a-7g@|bD3rBR3oE{*E zh+)!^oOjqsohI}V7IuRQzO|D#4RMRjGNj}ym_Pd`V1MXBc_%3eDrCUGMc@IoxKd{7 z2T=MLCHu)R^AckoOLsee01*FZ63shu1N^7q+Hc1vTu-NwhE}ryazd@&q^!auCjm9h zXx`X}go<(WMNm8-R8X0{=YlreIsNR3-7s6XGBmBvSkSTgxhgvPT7QPw=9KXpR>y|X zSn4l@RHjNVA9MiSq`_x$ZMEvtXiZEdEyM?U0xaN>3#THWOV;K$xC}UER3Wt9mpO$c zcwa0ZrT?fCpWuTQFEGfshWcx}qRGzCD2Y5mM9kR1wdcKYyd(AX&+|cy{>2_jKOAao ztUpj413k%y$AWC4Ds(T=*$y#Cl`mRx;~pOm6R$r@*|t}3$5ds#WTd-Akx|W6-kDhi zG8WJ<k@5COXD zymrPMT9BY~NgeBdI=ZQ-?KJL!;JUiu42%~`^&{ZkyU0{w3pY}Mn@0&+>O{c^x7!^n zFAo_9K0$mfUMfenhmSAyHW1bv{o-uik={}1ZW$yv1zDOh(qv+*PV%*6Nym<)J{Nvp z1A)h9vDdlE<=2k$D&lpoByz7A5hI_TWc-}AmKj10fPYDm7@f;J;8k-%3X9IPo*?Sc z$uqmj11Kt!@PL~|%laa00AU1pi$tqVK$*)_Zvx8MwOO;2KAjydmti@YZq5fF{Jaa0 zXCIDcyEI+4+(o4pb?UkeW4pLc1ma16U1nej04@rlp<)QYb89coYbIb~#a^_aUTyvs zd&ycgyU@d0+3q@ULyxQH)1Nj=r5$Ja;s?3Ci->G|2~XIua?qCXP?6nmsndq_)oM|; zW@q=1vgNAlj9XS#aD}pk*b&r)Z3OZ3l!&bUOqU})(dy{I@S$Q@WUR#a^&Vs10j77d z6$vCnPW8HX80Nn17z7D}8Hi4H{-K%wBNlrGKFnaCu#NV!+)f(?1_R><#ZN7Tz9NK7iKfQ$+PaN%5ZcrFR}``g|aDTdR!X*cVp0|gRAmhC)Y2M zOf#}0zGhsNn|_hx4lp>FKft^IgBR@VnGx?@*I$Eh4!)%Vp{@-Dyfls|< zP?JG11_t;5Bl1UgB+F`a-SGvicG=HQZ(LkoaBK#NG|RPRKH=lqq5YrS@7kiAb1IoiEyU7)xFI7 zTv`0o=B3T5_KRgnO_FxC$4m`jwCzj2zz7kOnIAsvg19zCed?+FK|`@b9OB5gl-8_{ z)Zv{1PBg*tLP3mQh8PgX7I=^AY^K!Ric4?C9NLhF0G&`lzf?=R53iCi@Er`cfcl8* zABQW}{4Nv!j(Y#Cr*qWWsY7oO`_)Q>@}nm@R}t*d?xD<6v>w(X>u7*KGZdqqG#6AP zZ$l~7=nsz6AnaDVb=L3DgG?&8jNEhn1+^iRSq}-oMIEX}S;?4)$}OHXk(c1E7W+2S zNAz7dY3bF4A+6qV#SxwXhX|53=c3idthB|iNy<|?ZcP%z#3;PlYBJcevmEEWk7AO@ zR$_=mgrei)({IOH*x>u^*Myi@_+OTEb0Fyb{eMCAf3Q~Ud1^p&Q$e!?B&9;12N-?S zKPNB)Y%1VPDItMhbRlVO|Nkhy1A}^Im)_W&$C5e;@+}t~?X+6$4p5&?req*bwpL1- ziR1xbZM_O*(32T>uR_-Sb{*`2&a+$o+*1&zG2TvK)NNYZZ*}j+OyhLQPh2PRu(S^| zLG17xBYAFSn(;!GeGJQ9&Ef43Gh+hGG~s}V1(ioyZ^GX@GcHXLNE?>1!fZa!2k=03 zyRmsH3b4ZmCs;59Oy0aWbfyI?P264I%g4U?cW^Zym&nT8t zD)l!cNMvK}72w(F*47->d;E41kacF_JCrNrON*4rWfl_Q*Piqgt8+w9m(EGjPwT3y zBLnbhYI)rG%1iqpgR68B#rD>KX)RJ!c2p(afW?ITp@{+2ni0pzqm*0RJFFeGkKQnw z+(6`FK#!g0lGY`UKJ6DizYWFNy`Nk(VPwW!bl@mNi!xtj$Syx+jb0caNN0794^oYYTSOLB zOa&K*ATs#ce-Qs_rf15Zz-tmvWI&g@Od#-LZ7I)@nrPD{q$k5cGv5LJU;Hx4iy_C_7`N%KUs->uf_bdsx+Ez;JYl z6k)Z=r|my5eRS1Ak(jL&`+IV_wFq=)STA24{kb~otoR#j@fcfA_;BeNW+v+#z9|k& z=(WulA43Al46XA8rSOXU9lp}*7@%uvG<4e7WCs1b1#ST3uzQ9w^#sn*F!?X9)y{IZ z{yVE~*5f5wCuqS2{*uTSY9ZX53ZMjIgTsNds^@+Da_0GO5Wy9u=4Z<6*wj>j*H`<` zw3MVyJ7)2-r`?@Vs8ZuE#mvRABgQh!1w3bR<{@eY6WCNeb(k|mOeTJHPia3wKUON6 z#pX%~+u^R*{h2o``aAiZ%kQTc6?$Pecm@dxH~8xVmq1nV##J~6Y1^LfzhK6T&6}f% zPcEP75B|ICf)X8df4MAd{3mds2e41w3=rl8-eEp*$$Ofw3|#cS9EaIeJKHDr7akv= z0B$WcEn_|vR%NkEiv8ek!a$U}s6&Z}YyuHR)*0<&Bob~DtN>N6*?Iwy)R=VEsO+`H zEJagIi}*bPy5`b4gU0(TtOweeJ>rdRr(!XU)%gY=AFs0;>5m6z=IVS!3RN0k4ukqK z4!djUijYNwP-8j3sob1AZaX^6PqtSxP2Z6B`vLGP@Ns5Uug?jTW`6NO3=%Z8sYXKs z4JUO_H}=u(u0qU7uDhe-LGi~Nx~IcG_AYmKHwD1u7#eF)^CUmHA^8 zIDc2e(8Dd%$m=ip9H1v9+I$@Nh&&PjHbIm#J#`7;Zc~~4mw>ULxti7bB+P`@@Mn?6 zY1M4xp+m&t!afDk?UmUjEp%exRG5j^R>?SsAK5E(wKI87$i&33D(a0)1sDn7gMLR% zse@C??=T9%DK*2lX>&>D4s`2 ztOvMUNTtV1gd-EWM?}~QbSVY9!xV3;hq$2li4I4lJaLC7lM@dtJ8B7hfDlvC^u$G< zg<_m$UcNcsRq0elDZ7gpr~Xi9(#Ho|6_})hUU(%0?RIJ=lVR znM7%LJ>ihy8Kj1Dt@ZTGosNq!$9dO^{wE1+bUPJ8Ni9Bu$Zx zaj52K!3mOmbcE+BI39KfEOs1%nRpd8=flgn3};7pLF&Zr(_WSIm9;>A{Zk?}!BjPw z>gPDgnR)Z6zxU|F3=h>m>=_YfTYpXXQ)>i@9%BuXhorZ)O+Jo7l&2yim5I|~_2ZpV z)`Wv2Qtk#^Xs*8wDN+6YzKH_?QA=@tUjg3B1Vh(&cwK!Rt=pG64j%C~_Xv1GbkimneKll)#OU+SSyYlP;Z?p_Dxox} z!7S|}Te&96os&a}5JdxXu8e0cCF^=PTmNoCxO1?(MnsB_>g!40J0kr)eM~;In%`CN z@-qY%Xs3z5I-~wcHb1B4MyLFY%!W>A(#Jvt(_%qQx-i_y7^fq+u&MNd$%|ysHDrqR>X0Ci^LLi%DA58o7Ei-h)&2VC7dRGpK zMcXN2$x5T(vYgTwE2{F4gs@GCo%NOXCCjqYY&6Su0O`ysR@tex)@&rpR2T3GD19_nVOOQr2PXI^%o;(nq;>=z&gmsNg{+rog%yKi~^aL zK9h!6n&``B$Y}o?B?xVhetK$`o`HfD>4N@t=}&XB@!Wp+2=(xC*1wk0&&Az^9Wb?g z{G4WcszeEdHT5mbY03LqQ)5HiJXCz(_rg#r`m?4|pm3V7;6N5D^1#2b!fzuiM=gHyh-bz%~0$kF-Y)III~|L_Q9|UPDc?2 zWv2etOoE!is!zW@b|R0{e`p);i0Nv1HoY~kpPjdjV7OPv=j zAAjP-_22TzLk8;G?z-(af3*HB*WuCy*rk1xRd<;WPJ1Mt?jacKVc{ZD|aFzC8lxxNqpV3p(W z_I38q!zK$0>`Bk(X0F3;`3cT&j?%kXu#E92bb{1PeB^3!DxX@vn3ic$`6IK@1hc}k zc_g_=Y?e+!MPnz|ZZuN`v8G|rqz_L8yAhw$FSzu^<}r zyx^0O8&8BFwEYf%>O+Lbdtu1PYFP2mD){sLE%B(1=ZVhcNd z=WkV^*^DWZk0gzCi?a(7&`SJ-GVp#RLO`ydILORXD4JWvcRBOEcqd~6p$RBzF=E%7 zy6~9bG2Rz>0ss`o4v*K(27<@YXQwiHl>oP}<=5!%zyS=E@D~eC$K5{^jUUCQR<*{l_PK zyCsqz8uXF*(UnKiy9y0%iaClbR*{w(2z#1*V6Y;>xlql|^QpfHKmHS_i5Gq(oT`z{ z(L9FO0J@NO8YD2JyWcTA1or!qEqf;9Ad9i!ScuZADe7h-_v8vCXw_OPN z4#oiy_8XLv?&z_0@RYZQ$WyzAhT=Z`d9hxuszcWb zkw!%g=`QU3z>Ah6r>u{M_IAP4gmv(ySTlZ{`Ki?rx{r6)&DVA==UZj;s?ejS7Iy!s_2rXTbNAQa>un(@G8m127OK z3rav#q3#O7pPV_&(yn|P~6iw#yJM&(fU1^ZJ~ zfhi??BP4gX&Izlm>f;Kt5zuG&vxs@7#nsBvr8;hce@K_f9Gib8QbKtpFnPa8&#BRq zEvE@w;swd<{_#6@PRm#S(l|r4e)=%MatsEpOOA@J7GaRe7MW@85jXsC?O2ghQ@04e z$zr?nlj>vsJJHqO{~1o|&Q8sjegh4(rm~UYBD*VnKc3m6!Rw>ZtS1hF7t-?x8lGQ4JAz7wZYqyBI(x7SYpl3p~v6h;b z?>=suLKeJupG%G8nqXzpB`u)%UXIM->C`_{Ejc0v4tB%Q}FhxE+?MlrY*+{I+rq$wpl zHm*D4I~}z;62Fc{x1O9{B#_5EANX3~R{g@(z?tcw!C#ASyDTMC0ixB%J?NU!xuc@U zjLTC`5y~EN(4hc>h(}X0LltM$0XPG|7b^wOg|1cL)(Ge?#5`K1wQ5!B+SBXCd|^2g zh*z=`B7g$dB?azjR|kP^`h4spk{UQl(PL@WBJOsEIjwBO6M9mGwv2fG0mx-LtdhV&sH zSgW2b6jtl<6G4JD85jbK3Huf4&hcyr~DCK4l_ z0KgBYRjcg^+P|ZZP@x%o}i-0BjZgg+z&HHb2fwwu>n+$@AMd9VK9jM zATqGea^N77Ig^~<6a^LnKvQAuvf9AlZ(kDfaue<%S;|oz`yy60XIOh_$C**@}G=FS5Yv)n(Sk7ikP2sC5n8H zF{rVyK=|8rBHDlkon1IllG@#DwwpljXfyCjW)Scg4+I)l3YtLl1UB);4=yAmgaOZg zR36Qra1jtMbA#hVrg==ho9F199&_IR{WydWo(&AZg5;R+kf45$IM)CAVFKAcWJqx` zfw7Oy!BZ!i>vT3+Ds&5A?`1LB!5 zu@|Ac9~yWE3Qwfp>lYs{f;L#S+=gfXK)jr44?3a&G)AaGEbwsDaovb6!O8-04KG1E z1@lv4KHC~ZADtCO|3f-SK#JVLU6dnW*8!9SL{O%Xk2%uLF^_4&_~wj8d41tV)u1Cr zwUr?L3kCzEmAs(Ge>x~7|Mxdn(6hO1aHM6q-N~XB<0Oq>G&WTuF-cV_CBc5}FJuOZ z5m7({{g*bXrw^g7s%c!42pt`ZPdo`jf(OvUW`xg3-V{lb-f?TB z@VQz_XcV}I=t?!{eYIZ}G2Q3&Q~VEFPhZXF8{Sqi_2(NK)ju91DG3)~2Y`W3FORn0 z2{E<4VyYHR2srqjU4@aeovnJG-YJ+k?{&He3*mcT(usveuI5g(?)zcEJ}E!5&A)G^{kL(V);q0flgPBz2cIO3D0B}GE`G-KlQ+2vR!V3Bzf*;M~KffAGEs^y& z$$FGk)WxyB8@Ik*D{f04;ix}nC;IMx2p!q_B%yMmqzxT~yA<{3&+J>nnAXX1xh{;p z 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