Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster than real time execution #73

Merged
merged 7 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ PASEOS is a `Python` module that simulates the environment to operate multiple s
<br> 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):
Expand All @@ -81,7 +83,8 @@ To install PASEOS you can use [conda](https://docs.conda.io/en/latest/) as follo
cd PASEOS
conda env create -f environment.yml
```
This will create a new conda environment called ``PASEOS`` and install the required software packages.

This will create a new conda environment called `PASEOS` and install the required software packages.
To activate the new environment, you can use:

```
Expand All @@ -96,6 +99,7 @@ pip install -e .
```

## Examples

The next examples will introduce you to the use of PASEOS.

### Actors
Expand All @@ -104,8 +108,10 @@ The code snippet below shows how to create a PASEOS [actor](#actor) named **mySa
[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,
Expand Down Expand Up @@ -229,7 +235,6 @@ ActorBuilder.set_power_devices(actor=sat_actor,
#### 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. <br>


```py
import pykep as pk
import paseos
Expand Down Expand Up @@ -290,7 +295,17 @@ cfg.sim.start_time=today.mjd2000 * pk.DAY2SEC
sim = paseos.init_sim(local_actor)
```

### Faster than real-time execution

In some cases, you may be interested to simulate your spacecraft operating for an extended period. By default, PASEOS operates in real-time, thus this would take a lot of time. However, you can increase the rate of time passing (i.e. the spacecraft moving, power being charged / consumed etc.) using the `time_multiplier` parameter. Set it as follows when initializing PASEOS.

```py

cfg = load_default_cfg() # loading cfg to modify defaults
cfg.sim.time_multiplier = 10 # setting the parameter so that in 1s real time, paseos models 10s having passed
paseos_instance = paseos.init_sim(my_local_actor, cfg) # initialize paseos instance

```

### Activities
#### Simple activity
Expand Down Expand Up @@ -423,7 +438,6 @@ sim.perform_activity("my_activity",
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. <br>
Expand Down Expand Up @@ -620,7 +634,6 @@ paseos_instance.save_status_log_csv("output.csv")
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).<br>
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.

Expand All @@ -646,7 +659,8 @@ paseos_instance.save_status_log_csv("output.csv")
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)).

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
Expand All @@ -656,12 +670,14 @@ To contribute, please proceed as follow:
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
* Vinutha Magal Shreenath - vinutha at ai.se
20 changes: 18 additions & 2 deletions paseos/activities/activity_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ class ActivityManager:
"""This class is used to handle registering, performing and collection of activities.
There can only be one instance of it and each paseos instance has one."""

def __new__(self, paseos_instance, paseos_update_interval: float):
def __new__(
self,
paseos_instance,
paseos_update_interval: float,
paseos_time_multiplier: float,
):
if not hasattr(self, "instance"):
self.instance = super(ActivityManager, self).__new__(self)
else:
Expand All @@ -22,19 +27,29 @@ def __new__(self, paseos_instance, paseos_update_interval: float):
)
return self.instance

def __init__(self, paseos_instance, paseos_update_interval: float):
def __init__(
self,
paseos_instance,
paseos_update_interval: float,
paseos_time_multiplier: float,
):
"""Creates a new activity manager. Singleton, so only one instance allowed.

Args:
paseos_instance (PASEOS): The main paseos instance.
paseos_update_interval (float): Update interval for paseos.
paseos_time_multiplier (float): Multiplier for the time. At 1, it is real time.
"""
logger.trace("Initializing ActivityManager")
assert (
paseos_update_interval > 1e-4
), f"Too small paseos update interval. Should not be less than 1e-4, was {paseos_update_interval}"
assert (
paseos_time_multiplier > 1e-4
), f"Too small paseos paseos_time_multiplier. Should not be less than 1e-4, was {paseos_time_multiplier}"
self._activities = DotMap(_dynamic=False)
self._paseos_update_interval = paseos_update_interval
self._paseos_time_multiplier = paseos_time_multiplier
self._paseos_instance = paseos_instance

def remove_activity(self, name: str):
Expand Down Expand Up @@ -134,6 +149,7 @@ async def job():
power_consumption_in_watt=activity.power_consumption_in_watt,
paseos_instance=self._paseos_instance,
activity_runner=activity_runner,
time_multiplier=self._paseos_time_multiplier,
advance_paseos_clock=self._paseos_instance.use_automatic_clock,
)

Expand Down
7 changes: 7 additions & 0 deletions paseos/activities/activity_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(
power_consumption_in_watt: float,
paseos_instance,
activity_runner: ActivityRunner,
time_multiplier: float = 1,
advance_paseos_clock=True,
):
"""Initializes the ActivityProcessor.
Expand All @@ -44,6 +45,7 @@ def __init__(
paseos_instance (PASEOS): Local paseos instance.
activity_runner (ActivityRunner): Runner of the activity that is performed.
Needed check if constraints are still valid.
time_multiplier (float): Specifies the rate at which times passes to allow faster-than-real time modeling.
advance_paseos_clock (bool, optional): Whether to advanced the local time of
the actor and thus local simulation. Defaults to True.
"""
Expand All @@ -56,7 +58,10 @@ def __init__(
)

self._power_consumption_in_watt = power_consumption_in_watt
assert update_interval > 0, "update_interval has to be > 0"
self.update_interval = update_interval
assert time_multiplier > 1e-4, "time_multiplier has to be > 1e-4"
self._time_multiplier = time_multiplier
self._is_started = False
self._task = None
self._paseos_instance = paseos_instance
Expand Down Expand Up @@ -97,6 +102,8 @@ async def _update(self, elapsed_time: float):
assert elapsed_time > 0, "Elapsed time cannot be negative."
logger.debug("Running ActivityProcessor update.")
logger.debug(f"Time since last update: {elapsed_time}s")
logger.trace(f"Applying time multiplier of {self._time_multiplier}")
elapsed_time *= self._time_multiplier
if self._advance_paseos_clock:
self._paseos_instance.advance_time(elapsed_time)

Expand Down
4 changes: 3 additions & 1 deletion paseos/paseos.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ def __init__(self, local_actor: BaseActor, cfg=None):
self._local_actor = local_actor
# 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._activity_manager = ActivityManager(
self, self._cfg.sim.activity_timestep, self._cfg.sim.time_multiplier
)
self._operations_monitor = OperationsMonitor(self._local_actor.name)

def save_status_log_csv(self, filename) -> None:
Expand Down
1 change: 1 addition & 0 deletions paseos/resources/default_cfg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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.
time_multiplier = 1.0 # [unitless] Defines how much real time passes

[io]
logging_interval = 10 # [s] after how many seconds should paseos log the actor status
Expand Down
63 changes: 63 additions & 0 deletions paseos/tests/time_multiplier_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Simple test of modifying rate of time passing"""

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_activity():
"""Test to see if twice as much power is consumed given the higher than real-time multiplier"""
# 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.time_multiplier = 10
sim = paseos.init_sim(sat1, cfg)

# Initial power is 500
assert sat1.battery_level_in_Ws == 500

# Out test case is a function that increments a value, genius.
# (needs a list to increase the actual value by reference and not create a copy)
test_val = [0]

async def func(args):
for _ in range(10):
args[0][0] += 1
await asyncio.sleep(0.2)

# Register an activity that draws 10 watt per second
sim.register_activity(
"Testing", activity_function=func, power_consumption_in_watt=10
)

# Run the activity
sim.perform_activity("Testing", activity_func_args=[test_val])
await wait_for_activity(sim)

# Check activity result
assert test_val[0] == 10

# Check power was depleted as expected
# Activity should run roughly 2s
# We charge 1W per second
# 1s real time equals 10s simulation
# And discharge 10W per second
# So should be roughly 200W - 20W consumed from starting 500
assert sat1.battery_level_in_Ws > 315 and sat1.battery_level_in_Ws < 325