Skip to content

Commit

Permalink
Merge pull request #73 from aidotse/faster-than-real-time
Browse files Browse the repository at this point in the history
Faster than real time execution
  • Loading branch information
gomezzz authored Dec 21, 2022
2 parents 6b01df2 + d78f563 commit 6561211
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 9 deletions.
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

0 comments on commit 6561211

Please sign in to comment.