diff --git a/README.md b/README.md index f7f13a9e..b713f343 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,667 @@ # PASEOS -PASEOS - PAseos Simulates the Environment for Operating multiple Spacecraft -This project is currently under development. Use at your own risk. :) +PASEOS - PAseos Simulates the Environment for Operating multiple Spacecraft -## How PASEOS works +![Alt Text](resources/images/sat_gif.gif) + +Disclaimer: This project is currently under development. Use at your own risk. + + +
+ Table of Contents +
    +
  1. About the Project
  2. +
  3. PASEOS space environment simulation
  4. +
  5. Installation
  6. +
  7. Examples
  8. + +
  9. System Design of PASEOS
  10. +
  11. Glossary
  12. +
  13. Contributing
  14. +
  15. License
  16. +
  17. Contact
  18. +
+
+ +## About the project + +PASEOS is a `Python` module that simulates the environment to operate multiple spacecraft. In particular, PASEOS offers the user some utilities to run their own [activities](#activity) by taking into account both operational and onboard (e.g. limited-power-budget, radiation, and thermal effects) constraints.
PASEOS is designed to be: + +* **open-source**: the source code of PASEOS is available under a GPL license. +* **fully decentralised**: one instance of PASEOS shall be executed in every node, i.e. individual spacecraft (actor), of the emulated spacecraft. Each instance of PASEOS is responsible for handling the user [activities](#activity) executed on that node (the local actor) while keeping track of the status of the other nodes. In this way, the design of PASEOS is completely decentralised and independent of the number of nodes of the constellation. Because of that, both single-node and multi-node scenarios are possible. +* **application-agnostic**: each user operation that has to be executed on a node is modelled as an [activity](#activity). The user is only required to provide the code to run and some parameters (e.g., power consumption) for each [activity](#activity). Thus, activities can be any code the user wants to simulate running on a spacecraft and thereby PASEOS is completely application-agnostic. Conceivable applications range from modelling constellations to training machine learning methods. + +
The project is being developed by $\Phi$[-lab@Sweden](https://www.ai.se/en/data-factory/f-lab-sweden) in the frame of a collaboration between [AI Sweden](https://www.ai.se/en/) and the [European Space Agency](https://www.esa.int/) to explore distributed edge learning for space applications. For more information on PASEOS and $\Phi$-lab@Sweden, please take a look at the recording of the $\Phi$-lab@Sweden [kick-off event](https://www.youtube.com/watch?v=KuFRCcNxLgo&t=2365s). + +## PASEOS space environment simulation +![Alt Text](resources/images/PASEOS_constraints.png) +PASEOS allows simulating the effect of onboard and operational constraints on user-registered [activities](#activity). The image above showcases the different phenomena considered (or to be implemented) in PASEOS. + +## Installation +`pip` and `conda` support will follow in the near future. + +For now, first of all clone the [GitHub](https://github.com/aidotse/PASEOS.git) repository as follows ([Git](https://git-scm.com/) required): + +``` +git clone https://github.com/aidotse/PASEOS.git +``` + +To install PASEOS you can use [conda](https://docs.conda.io/en/latest/) as follows: + +``` +cd PASEOS +conda env create -f environment.yml +``` +This will create a new conda environment called ``PASEOS`` and install the required software packages. +To activate the new environment, you can use: + +``` +conda activate paseos +``` + +Alternatively, you can install PASEOS by using [pip](https://www.pypy.org/) as follows: + +``` +cd PASEOS +pip install -e . +``` + +## Examples +The next examples will introduce you to the use of PASEOS. + +### Actors +#### Create a PASEOS actor +The code snippet below shows how to create a PASEOS [actor](#actor) named **mySat** of type [SpacecraftActor](#spacecraftactor). [pykep](https://esa.github.io/pykep/) is used to define the satellite [epoch](https://en.wikipedia.org/wiki/Epoch_(astronomy)) in format [mjd2000](https://en.wikipedia.org/wiki/Julian_day) format.
+[actors](#actor) are created by using an `ActorBuilder`. The latter is used to define the [actor](#actor) `scaffold` that includes the [actor](#actor) minimal properties. In this way, [actors](#actor) are built in a modular fashion that enables their use also for non-space applications. + +```py +import pykep as pk +from paseos import ActorBuilder, SpacecraftActor +# Define an actor of type SpacecraftActor of name mySat +sat_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + + +``` +#### Local and Known Actors +Once you have instantiated a [PASEOS simulation](#initializing-paseos) to know how to create an instance of PASEOS)), you can add other PASEOS [actors](#actor) ([Known actors](#known-actors)) to the simulation. You can use this, e.g., to facilitate communications between actors and to automatically monitor communication windows.
+The next code snippet will add both a [SpacecraftActor](#spacecraftactor) and a [GroundstationActor](#ground-stationactor) (`other_sat`). An orbit is set for `other_sat`, which is placed around Earth at position `(x,y,z)=(-10000,0,0)` and velocity `(vx,vy,vz)=(0,-8000,0)` at epoch `epoch=pk.epoch(0)`. + The latter (`grndStation`) will be placed at coordinates `(lat,lon)=(79.002723, 14.642972)` and elevation of 0 m.
You cannot add a power device and an orbit to a `GroundstationActor`. + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor, GroundstationActor +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Let's set the orbit of local_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + central_body=pk.epoch(0)) + +# Initialize PASEOS simulation +sim = paseos.init_sim(local_actor) + +# Create another SpacecraftActor +other_spacraft_actor = ActorBuilder.get_actor_scaffold(name="other_sat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Let's set the orbit of other_spacraft_actor. +ActorBuilder.set_orbit(actor=other_spacraft_actor, + position=[-10000000, 0, 0], + velocity=[0, -8000.0, 0], + epoch=pk.epoch(0), central_body=pk.epoch(0)) + +#Create GroundstationActor +grndStation = GroundstationActor(name="grndStation", epoch=pk.epoch(0)) + +#Set the ground station at lat lon 79.002723 / 14.642972 +# and its elevation 0m +ActorBuilder.set_ground_station_location(grndStation, + latitude=79.002723, + longitude=14.642972, + elevation=0) + +# Adding other_spacraft_actor to PASEOS. +sim.add_known_actor(other_spacraft_actor) + +# Adding grndStation to PASEOS. +sim.add_known_actor(grndStation) +``` + +### Physical Models +#### Set an orbit for a PASEOS SpacecraftActor +Once you have defined a [SpacecraftActor](#spacecraftactor), you can assign a [Keplerian orbit](https://en.wikipedia.org/wiki/Kepler_orbit) to it. To this aim, you need to define the central body the [SpacecraftActor](#spacecraftactor) is orbiting around and specify its position and velocity (in the central body's [inertial frame](https://en.wikipedia.org/wiki/Inertial_frame_of_reference)) and an epoch. In this case, we will use `Earth` as a central body. + +```py +import pykep as pk +from paseos import ActorBuilder, SpacecraftActor +# Define an actor of type SpacecraftActor of name mySat +sat_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of sat_actor. +ActorBuilder.set_orbit(actor=sat_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), central_body=earth) +``` +#### How to add a communication device +The following code snippet shows how to add a communication device to a [SpacecraftActors] (#spacecraftactor). A communication device is needed to model the communication between [SpacecraftActors] (#spacecraftactor) or a [SpacecraftActor](#spacecraftactor) and [GroundstationActor](#ground-stationactor). Currently, given the maximum transmission data rate of a communication device, PASEOS calculates the maximum data that can be transmitted by multiplying the transmission data rate by the length of the communication window. The latter is calculated by taking the period for which two actors are in line-of-sight into account. + +```py +import pykep as pk +from paseos import ActorBuilder, SpacecraftActor +# Define an actor of type SpacecraftActor of name mySat +sat_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) +# Add a communication device +ActorBuilder.add_comm_device(actor=sat_actor, + # Communication device name + device_name="my_communication_device", + # Bandwidth in kbps. + bandwidth_in_kbps=100000) +``` + +#### How to add a power device +The following code snippet shows how to add a power device to a [SpacecraftActor](#spacecraftactor). At the moment, only one battery device is supported. +Moreover, PASEOS assumes that the battery will be charged by solar panels, which will provide energy thanks to the incoming solar radiation when the spacecraft is not eclipsed. Charging and discharging happens automatically during [activities](#activity). + +```py +import pykep as pk +from paseos import ActorBuilder, SpacecraftActor +# Define an actor of type SpacecraftActor of name mySat +sat_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) +# Add a power device +ActorBuilder.set_power_devices(actor=sat_actor, + # Battery level at the start of the simulation in Ws + battery_level_in_Ws=100, + # Max battery level in Ws + max_battery_level_in_Ws=2000, + # Charging rate in W + charging_rate_in_W=10) +``` +### Simulation Settings +#### Initializing PASEOS +We will now show how to create an instance of PASEOS. An instance of PASEOS shall be bounded to one PASEOS [actor](#actor) that we call [local actor](#local-actor). Please, notice that an orbit shall be placed for a [SpacecraftActor](#spacecraftactor) before being added to a PASEOS instance.
+ + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of local_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + central_body=earth) + +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) +``` +#### How to use the cfg +When you instantiate PASEOS as shown in [Initializing PASEOS](#initializing-paseos), PASEOS instance is created by using the default configuration. However, sometimes it is useful to use a custom configuration.
The next code snippet will show how to start the PASEOS simulation with a time different from `pk.epoch(0)` by loading a custom configuration. + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor + +#Define today as pykep epoch (16-06-22) +#please, refer to https://esa.github.io/pykep/documentation/core.html#pykep.epoch +today = pk.epoch_from_string('2022-06-16 00:00:00.000') + +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +# pk.epoch is set to today +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=today) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of local_actor. +# pk.epoch is set to today +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=today, + central_body=earth) + +# Loading cfg to modify defaults +cfg=load_default_cfg() +# Set simulation starting time by converting epoch to seconds +cfg.sim.start_time=today.mjd2000 * pk.DAY2SEC +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) +``` + + + +### Activities +#### Simple activity +PASEOS enables the user to register their [activities](#activity) that will be executed on the `local actor`.
+To register an activity, it is first necessary to define an asynchronous [activity function](#activity-function). The following code snippet shows how to create a simple [activity function](#activity-function) `activity_function_A` that prints "Hello Universe!". Then, it waits for 0.1 s before concluding the activity.
When you register an [activity](#activity), you need to specify the power consumption associated to the activity. + +```py +#Activity function +async def activity_function_A(args): + print("Hello Universe!") + await asyncio.sleep(0.1) #Await is needed inside an async function. +``` + +Once an activity is registered, the user shall call `perform_activity(...)` to run the registered activity. +The next snippet will showcase how to register and perform the activity `activity_A`.
+ +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor +import asyncio +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of sat_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + central_body=earth) + +# Add a power device +ActorBuilder.set_power_devices(actor=local_actor, + # Battery level at the start of the simulation in Ws + battery_level_in_Ws=100, + # Max battery level in Ws + max_battery_level_in_Ws=2000, + # Charging rate in W + charging_rate_in_W=10) + +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) + +#Activity function +async def activity_function_A(args): + print("Hello Universe!") + await asyncio.sleep(0.1) #Await is needed inside an async function. + +# Register an activity that emulate event detection +sim.register_activity( + "activity_A", + activity_function=activity_function_A, + power_consumption_in_watt=10 +) + +#Run the activity +sim.perform_activity("activity_A") +``` + +#### Activities with Inputs and Outputs +The next code snippet will show how to register and perform activities with inputs and outputs. In particular, we will register an [activity function](#activity-function) `activity_function_with_in_and_outs` that takes an input argument and returns its value multiplied by two. Then, it waits for 0.1 s before concluding the activity.
+Please, notice that the output value is placed in `args[1][0]`, which is returned as reference. + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor +import asyncio +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of sat_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + entral_body=earth) + +# Add a power device +ActorBuilder.set_power_devices(actor=local_actor, + # Battery level at the start of the simulation in Ws + battery_level_in_Ws=100, + # Max battery level in Ws + max_battery_level_in_Ws=2000, + # Charging rate in W + charging_rate_in_W=10) + +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) + +#Activity function +async def activity_function_with_in_and_outs(args): + activity_in=args[0] + activity_out=activity_in * 2 + args[1][0]=activity_out + await asyncio.sleep(0.1) #Await is needed inside an async function. + +# Register an activity that emulate event detection +sim.register_activity( + "my_activity", + activity_function=activity_function_with_in_and_outs, + power_consumption_in_watt=10, +) + +#Creatie an input variable for activity +activity_in=1 + +#Create a placeholder variable to contain the output of the activity function. +#It is created as a list so its first value is edited +# as reference by the activity function. +activity_out=[None] + +#Run the activity +sim.perform_activity("my_activity", + activity_func_args=[activity_in, activity_out], + ) +#Print return value +print("The output of the activity function is: ", activity_out[0]) +``` + + +#### Constraint Function + +It is possible to associate a [constraint function](#constraint-function) with each [activity](#activity) to ensure that some particular constraints are met during the [activity](#activity) execution. When constraints are not met, the activity is interrupted. Constraints can be used, e.g., to impose power requirements, communication windows or maximum operational temperatures.
+The next code snippet shows how to: +* create a [constraint function](#constraint-function) (`constraint_function_A`) which returns `True` when the local actor's temperature is below ~86°C and `False` otherwise (this requires a thermal model on the actor) +* how use `constraint_function_A` to constraint our [Simple Activity](#simple-activity). + + + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor +import asyncio +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of sat_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + central_body=earth) + +# Add a power device +ActorBuilder.set_power_devices(actor=local_actor, + # Battery level at the start of the simulation in Ws + battery_level_in_Ws=100, + # Max battery level in Ws + max_battery_level_in_Ws=2000, + # Charging rate in W + charging_rate_in_W=10) + +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) + +#Activity function +async def activity_function_A(args): + print("Hello Universe!") + await asyncio.sleep(0.1) #Await is needed inside an async function. + +#Constraint function +async def constraint_function_A(args): + local_actor_temperature=args[0] + return (local_actor_temperature < 350) + +# Register an activity that emulate event detection +sim.register_activity( + "activity_A_with_constraint", + activity_function=activity_function_A, + power_consumption_in_watt=10, + constraint_function=constraint_function_A +) + +#The constraint function is related to the operational temperature of the local actor. +operational_temperature_in_K=local_actor.temperature_in_K + +#Run the activity +sim.perform_activity("activity_A_with_constraint", + constraint_func_args=[operational_temperature_in_K], + ) +``` + +#### On-termination Function +It is also possible to define an [on-termination function](#on-termination-function) to perform some specific operations when on termination of the [activity](#activity). The next code snippet shows: + * how to create an [on-termination function](#on-termination-function) that prints "activity (activity_A_with_termination_function) ended.". + * How to associate our [on-termination function](#on-termination-function) to our [Simple Activity](#simple-activity). + +The name of the [activity](#activity) is passed as input to the [on-termination function](#on-termination-function) to showcase to handle [on-termination function](#on-termination-function) inputs. + +```py +import pykep as pk +import paseos +from paseos import ActorBuilder, SpacecraftActor +import asyncio +# Define an actor of type SpacecraftActor of name mySat +# (that will be the local actor) +local_actor = ActorBuilder.get_actor_scaffold(name="mySat", + actor_type=SpacecraftActor, + epoch=pk.epoch(0)) + +# Define the central body as Earth by using pykep APIs. +earth = pk.planet.jpl_lp("earth") + +# Let's set the orbit of sat_actor. +ActorBuilder.set_orbit(actor=local_actor, + position=[10000000, 0, 0], + velocity=[0, 8000.0, 0], + epoch=pk.epoch(0), + central_body=earth) + +# Add a power device +ActorBuilder.set_power_devices(actor=local_actor, + # Battery level at the start of the simulation in Ws + battery_level_in_Ws=100, + # Max battery level in Ws + max_battery_level_in_Ws=2000, + # Charging rate in W + charging_rate_in_W=10) + +# initialize PASEOS simulation +sim = paseos.init_sim(local_actor) + +#Activity function +async def activity_function_A(args): + print("Hello Universe!") + await asyncio.sleep(0.1) #Await is needed inside an async function. + +#On-termination function +async def on_termination_function_A(args): + #Fetching input + activity_name=args[0] + print("Activity ("+str(activity_name)+") ended.") + +# Register an activity that emulate event detection +sim.register_activity( + "activity_A_with_termination_function", + activity_function=activity_function_A, + power_consumption_in_watt=10, + on_termination_function=on_termination_function_A +) + +#The termination function input is the activity name +activity_name="activity_A_with_termination_function" + +#Run the activity +sim.perform_activity("activity_A_with_termination_function", + termination_func_args=[activity_name], + ) +``` +### Utilities +#### Visualization +Navigate to paseos/visualization to find a jupyter notebook containing examples of how to visualize PASEOS. +Visualization can be done in interactive mode or as an animation that is saved to your disc. +In the figure below, Earth is visualized in the centre as a blue sphere with different spacecraft in orbit. +Each spacecraft has a name and if provided, a battery level and a communications device. +The local device is illustrated with white text. +In the upper-right corner, the status of the communication link between each spacecraft is shown. +Finally, the time in the lower left and lower right corners corresponds to the epoch and the PASEOS local simulation time.

- Scheme + Scheme

- Description of PASEOS data structure + Snapshot of PASEOS visualization

+#### Writing Simulation Results to a File + +To evaluate your results, you will likely want to track the operational parameters, such as actor battery status, currently running activitiy etc. of actors over the course of your simulation. By default, PASEOS will log the current actor status every 10 seconds, however you can change that rate by editing the default configuration, as explained in [How to use the cfg](#how-to-use-the-cfg). You can save the current log to a \*.csv file at any point. + +```py +cfg = load_default_cfg() # loading cfg to modify defaults +cfg.io.logging_interval = 0.25 # log every 0.25 seconds +paseos_instance = paseos.init_sim(my_local_actor, cfg) # initialize paseos instance + +# Performing activities, running the simulation (...) + +paseos_instance.save_status_log_csv("output.csv") +``` + +## System Design of PASEOS +

- Scheme + Scheme

- Description of PASEOS workflow on an individual device + Description of PASEOS data structure

-## Visualising PASEOS -Navigate to paseos/visualization to find a jupyter notebook containing examples of how to visualize PASEOS. -Visualization can be done in interactive mode or as an animation that is saved to disc. -In the figure below, Earth is visualized in the center as a blue sphere with different spacecraft in orbit. -Each spacecraft has a name and if provided, a battery level and a communications device. -The local device is illustrated with white text. -In the upper-right corner, the status of the communication link between each spacecraft is shown. -Finally, the time in the lower left and lower right corners correspond to the epoch and the PASEOS local simulation time. -

- Scheme + Scheme

- Snapshot of PASEOS visualization + Description of PASEOS workflow on an individual device

-

\ No newline at end of file +

+ + + +## Glossary +* ### Activity + Activity is the abstraction that PASEOS uses to keep track of specific actions performed by an [actor](#actor) upon a request from the user. >PASEOS is responsible for the execution of the activity and for updating the system status depending on the effects of the activity (e.g., by discharging the satellite battery).
+ When registering an activity, the user can specify a [constraint function](#constraint-function) to specify constraints to be met during the execution of the activity and an [on-termination](#on-termination) function to specify additional operations to be performed by PASEOS on termination of the activity function. + + +* ### Activity function + User-defined function emulating any operation to be executed in a PASEOS by an [actor](#actor). Activity functions are necessary to register [activities](#activity). Activity functions might include data transmission, housekeeping operations, onboard data acquisition and processing, and others. + +* ### Actor + Since PASEOS is fully-decentralised, each node of a PASEOS constellation shall run an instance of PASEOS modelling all the nodes of that constellation. The abstraction of a constellation node inside a PASEOS instance is a PASEOS `actor`. + +* ### Constraint function + A constraint function is an asynchronous function that can be used by the PASEOS user to specify some constraints that shall be met during the execution of an activity. + +* ### GroundstationActor + `PASEOS actor` emulating a ground station. + +* ### Local actor + The `local actor` is the `actor` whose behaviour is modelled by the locally running PASEOS instance. + +* ### Known actors + In a PASEOS instance, `known actors` are all the other actors that are known to the [local actor](#local-actor). + +* ### On-termination function + An on-termination function is an asynchronous function that can be used by the PASEOS user to specify some operations to be executed on termination of the predefined PASEOS user's activity. + +* ### SpacecraftActor + PASEOS [actor](actor) emulating a spacecraft or a satellite. + +## Contributing +The ```PASEOS``` project is open to contributions. To contribute, you can open an [issue](https://github.com/gomezzz/MSMatch/issues) to report a bug or to request a new feature. If you prefer discussing new ideas and applications, you can contact us via email (please, refer to [Contact](#contact)). +To contribute, please proceed as follow: + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## License +Distributed under the GPL-3.0 License. + +## Contact +Created by $\Phi$[-lab@Sweden](https://www.ai.se/en/data-factory/f-lab-sweden). + +* Pablo Gómez - pablo.gomez at esa.int, pablo.gomez at ai.se +* Gabriele Meoni - gabriele.meoni at esa.int, gabriele.meoni at ai.se +* Johan Östman - johan.ostman at ai.se +* Vinutha Magal Shreenath - vinutha at ai.se diff --git a/paseos/actors/base_actor.py b/paseos/actors/base_actor.py index 78090aed..6175922b 100644 --- a/paseos/actors/base_actor.py +++ b/paseos/actors/base_actor.py @@ -43,6 +43,11 @@ class BaseActor(ABC): # Tracks the current activity _current_activity = None + # The following variables are used to track last evaluated state vectors to avoid recomputation. + _last_position = None + _last_velocity = None + _last_eclipse_status = None + def __init__(self, name: str, epoch: pk.epoch) -> None: """Constructor for a base actor @@ -57,6 +62,15 @@ def __init__(self, name: str, epoch: pk.epoch) -> None: self._communication_devices = DotMap(_dynamic=False) + @property + def current_activity(self) -> str: + """Returns the name of the activity the actor is currently performing. + + Returns: + str: Activity name. None if no activity being performed. + """ + return self._current_activity + @property def local_time(self) -> pk.epoch: """Returns local time of the actor as pykep epoch. Use e.g. epoch.mjd2000 to get time in days. @@ -145,6 +159,7 @@ def get_position(self, epoch: pk.epoch): # If the actor has no orbit, return position if self._orbital_parameters is None: if self._position is not None: + self._last_position = self._position return self._position else: return self._orbital_parameters.eph(epoch)[0] @@ -174,7 +189,10 @@ def get_position_velocity(self, epoch: pk.epoch): + str(epoch.mjd2000) + " (mjd2000)." ) - return self._orbital_parameters.eph(epoch) + pos, vel = self._orbital_parameters.eph(epoch) + self._last_position = pos + self._last_velocity = vel + return pos, vel def is_in_line_of_sight( self, @@ -208,7 +226,8 @@ def is_in_eclipse(self, t: pk.epoch = None): """ if t is None: t = self._local_time - return is_in_eclipse(self, self._central_body, t) + self._last_eclipse_status = is_in_eclipse(self, self._central_body, t) + return self._last_eclipse_status def get_communication_window( self, diff --git a/paseos/actors/spacecraft_actor.py b/paseos/actors/spacecraft_actor.py index 558c249f..d892be95 100644 --- a/paseos/actors/spacecraft_actor.py +++ b/paseos/actors/spacecraft_actor.py @@ -49,7 +49,7 @@ def battery_level_in_Ws(self): return self._battery_level_in_Ws @property - def battery_level_ratio(self): + def state_of_charge(self): """Get the current battery level as ratio of maximum. Returns: diff --git a/paseos/paseos.py b/paseos/paseos.py index 20408354..cb9b33f5 100644 --- a/paseos/paseos.py +++ b/paseos/paseos.py @@ -1,5 +1,6 @@ import types import asyncio +import sys from dotmap import DotMap from loguru import logger @@ -7,6 +8,7 @@ from paseos.actors.base_actor import BaseActor from paseos.activities.activity_manager import ActivityManager +from paseos.utils.operations_monitor import OperationsMonitor class PASEOS: @@ -32,6 +34,10 @@ class PASEOS: # Semaphore to track if an activity is currently running _is_running_activity = False + # Used to monitor the local actor over execution and write performance stats + _operations_monitor = None + _time_since_last_log = sys.float_info.max + # Use automatic clock (default on for now) use_automatic_clock = True @@ -60,6 +66,20 @@ def __init__(self, local_actor: BaseActor, cfg=None): # Update local actor time to simulation start time. self._local_actor.set_time(pk.epoch(self._cfg.sim.start_time * pk.SEC2DAY)) self._activity_manager = ActivityManager(self, self._cfg.sim.activity_timestep) + self._operations_monitor = OperationsMonitor(self._local_actor.name) + + def save_status_log_csv(self, filename) -> None: + """Saves the status log incl. all kinds of information such as battery charge, + running activtiy, etc. + + Args: + filename (str): File to save the log in. + """ + self._operations_monitor.save_to_csv(filename) + + def log_status(self): + """Updates the status log.""" + self._operations_monitor.log(self._local_actor, self.known_actor_names) def advance_time(self, time_to_advance: float): """Advances the simulation by a specified amount of time @@ -90,6 +110,13 @@ def advance_time(self, time_to_advance: float): self._state.time += dt self._local_actor.set_time(pk.epoch(self._state.time * pk.SEC2DAY)) + # Check if we should update the status log + if self._time_since_last_log > self._cfg.io.logging_interval: + self.log_status() + self._time_since_last_log = 0 + else: + self._time_since_last_log += dt + logger.debug("New time is: " + str(self._state.time) + " s.") def add_known_actor(self, actor: BaseActor): diff --git a/paseos/resources/default_cfg.toml b/paseos/resources/default_cfg.toml index e34771b6..87128467 100644 --- a/paseos/resources/default_cfg.toml +++ b/paseos/resources/default_cfg.toml @@ -3,6 +3,9 @@ start_time = 0 # [s] Start time of the simulation in seconds after MJD2000 dt = 10 # [s] Maximal Internal timestep used for computing charging, etc. activity_timestep = 1 # [s] Internal timestep at which activities update, try to charge and discharge etc. +[io] +logging_interval = 10 # [s] after how many seconds should paseos log the actor status + [power] [comms] diff --git a/paseos/tests/actor_builder_test.py b/paseos/tests/actor_builder_test.py index 5128d9e1..b5160922 100644 --- a/paseos/tests/actor_builder_test.py +++ b/paseos/tests/actor_builder_test.py @@ -40,7 +40,7 @@ def test_add_power_devices(): _, sat1, _ = get_default_instance() ActorBuilder.set_power_devices(sat1, 42, 42, 42) assert sat1.battery_level_in_Ws == 42 - assert sat1.battery_level_ratio == 1 + assert sat1.state_of_charge == 1 assert sat1.charging_rate_in_W == 42 diff --git a/paseos/tests/operations_monitor_test.py b/paseos/tests/operations_monitor_test.py new file mode 100644 index 00000000..d1397f1e --- /dev/null +++ b/paseos/tests/operations_monitor_test.py @@ -0,0 +1,59 @@ +"""Simple test for the operations monitor""" + +import asyncio +import pytest +import pykep as pk + +import paseos +from paseos import ActorBuilder, SpacecraftActor, load_default_cfg + + +async def wait_for_activity(sim): + while sim._is_running_activity is True: + await asyncio.sleep(0.1) + + +# tell pytest to create an event loop and execute the tests using the event loop +@pytest.mark.asyncio +async def test_monitor(): + """Test to see if we can log while performing an activity and write to file then. + To fully judge the outcome, have a look at the generated test file.""" + # Define central body + earth = pk.planet.jpl_lp("earth") + + # Define local actor + sat1 = ActorBuilder.get_actor_scaffold("sat1", SpacecraftActor, pk.epoch(0)) + ActorBuilder.set_orbit(sat1, [10000000, 0, 0], [0, 8000.0, 0], pk.epoch(0), earth) + ActorBuilder.set_power_devices(sat1, 500, 10000, 1) + + # init simulation + cfg = load_default_cfg() # loading cfg to modify defaults + cfg.sim.dt = 0.1 # setting lower timestep to run things quickly + cfg.sim.activity_timestep = 0.1 + cfg.io.logging_interval = 0.25 # log every 0.25 seconds + cfg.sim.time_multiplier = 10 # speed up execution for convenience + sim = paseos.init_sim(sat1, cfg) + + async def func1(args): + await asyncio.sleep(0.5) + + async def func2(args): + await asyncio.sleep(1.0) + + # Register an activity that draws 10 watt per second + sim.register_activity( + "Activity_1", activity_function=func1, power_consumption_in_watt=2 + ) + + sim.register_activity( + "Activity_2", activity_function=func2, power_consumption_in_watt=10 + ) + + # Run the activity + sim.perform_activity("Activity_1") + await wait_for_activity(sim) + + sim.perform_activity("Activity_2") + await wait_for_activity(sim) + + sim.save_status_log_csv("test.csv") diff --git a/paseos/utils/operations_monitor.py b/paseos/utils/operations_monitor.py new file mode 100644 index 00000000..c294090f --- /dev/null +++ b/paseos/utils/operations_monitor.py @@ -0,0 +1,81 @@ +import csv + +from loguru import logger +from dotmap import DotMap +import pykep as pk + +from paseos.actors.base_actor import BaseActor +from paseos.actors.spacecraft_actor import SpacecraftActor + + +class OperationsMonitor: + """This class is used to track actor status and activities over time.""" + + def __init__(self, actor_name): + """Initializes the OperationsMonitor + + Args: + actor_name (str): Name of the local actor. + """ + logger.trace("Initializing OperationsMonitor for " + actor_name) + self._actor_name = actor_name + self._log = DotMap(_dynamic=False) + self._log.timesteps = [] + self._log.current_activity = [] + self._log.state_of_charge = [] + self._log.is_in_eclipse = [] + self._log.known_actors = [] + self._log.position = [] + self._log.velocity = [] + + def log( + self, + local_actor: BaseActor, + known_actors: list, + ): + """Log the current time step. + + Args: + local_actor (BaseActor): The local actors whose status we are monitoring. + known_actors (list): List of names of the known actors. + """ + logger.trace("Logging iteration") + assert local_actor.name == self._actor_name, ( + "Expected actor's name was" + self._actor_name + ) + self._log.timesteps.append(local_actor.local_time.mjd2000 * pk.DAY2SEC) + self._log.current_activity.append(local_actor.current_activity) + self._log.position.append(local_actor._last_position) + self._log.velocity.append(local_actor._last_velocity) + self._log.known_actors.append(known_actors) + if isinstance(local_actor, SpacecraftActor): + self._log.state_of_charge.append(local_actor.state_of_charge) + else: + self._log.state_of_charge.append(1.0) + + if local_actor._last_eclipse_status is None: + self._log.is_in_eclipse.append(False) + else: + self._log.is_in_eclipse.append(local_actor._last_eclipse_status) + + def save_to_csv(self, filename): + """Write the created log file to a csv file. + + Args: + filename (str): File to store the log in. + """ + logger.trace("Writing status log file to " + filename) + with open(filename, "w", newline="") as f: + w = csv.DictWriter(f, self._log.keys()) + w.writeheader() + for i in range(len(self._log.timesteps)): + row = { + "timesteps": self._log.timesteps[i], + "current_activity": self._log.current_activity[i], + "position": self._log.position[i], + "velocity": self._log.velocity[i], + "known_actors": self._log.known_actors[i], + "state_of_charge": self._log.state_of_charge[i], + "is_in_eclipse": self._log.is_in_eclipse[i], + } + w.writerow(row) diff --git a/paseos/visualization/space_animation.py b/paseos/visualization/space_animation.py index 51377c0a..85a59603 100644 --- a/paseos/visualization/space_animation.py +++ b/paseos/visualization/space_animation.py @@ -147,7 +147,7 @@ def _populate_textbox(self, actor: BaseActor) -> str: info_str = f"{actor.name}" if isinstance(actor, SpacecraftActor): if actor.battery_level_in_Ws is not None: - battery_level = actor.battery_level_ratio * 100 + battery_level = actor.state_of_charge * 100 info_str += f"\nBattery: {battery_level:.0f}%" for name in actor.communication_devices.keys(): diff --git a/resources/images/PASEOS_constraints.png b/resources/images/PASEOS_constraints.png new file mode 100644 index 00000000..19b03bc4 Binary files /dev/null and b/resources/images/PASEOS_constraints.png differ diff --git a/resources/images/sat_gif.gif b/resources/images/sat_gif.gif new file mode 100644 index 00000000..9c40d5e0 Binary files /dev/null and b/resources/images/sat_gif.gif differ