Skip to content

Commit

Permalink
Merge branch 'main' into faster-than-real-time
Browse files Browse the repository at this point in the history
  • Loading branch information
gomezzz authored Dec 21, 2022
2 parents b2b9f4b + 6b01df2 commit 7f0f8e2
Show file tree
Hide file tree
Showing 22 changed files with 816 additions and 143 deletions.
Binary file removed PASEOS_Example.png
Binary file not shown.
Binary file removed PASEOS_constraints.png
Binary file not shown.
602 changes: 471 additions & 131 deletions README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions paseos/activities/activity_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ def __init__(
self._paseos_time_multiplier = paseos_time_multiplier
self._paseos_instance = paseos_instance

def remove_activity(self, name: str):
"""Removes a registered activity
Args:
name (str): Name of the activity.
"""
if name not in self._activities.keys():
raise ValueError(
"Trying to remove non-existing activity with name: " + name
)
else:
del self._activities[name]

def register_activity(
self,
name: str,
Expand Down
14 changes: 10 additions & 4 deletions paseos/activities/activity_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,28 @@ async def check_constraint(self):
bool: True if still valid.
"""
if self.has_constraint():
logger.trace(f"Checking activity {self.name} constraints")
logger.debug(f"Checking activity {self.name} constraints")
try:
is_satisfied = await self._constraint_func(self._constraint_args)

if is_satisfied is None:
logger.error(
f"An exception occurred running the checking the activity {self.name} constraint."
+ " The constraint function failed to return True or False."
)
except Exception as e:
logger.error(
f"An exception occurred running the checking the activity's {self.name} constraint."
f"An exception occurred running the checking the activity {self.name} constraint."
)
logger.error(str(e))
return False
if not is_satisfied or is_satisfied is None:
if not is_satisfied:
logger.debug(
f"Constraint of activity {self.name} is no longer satisfied, cancelling."
)
if not self._was_stopped:
await self.stop()
return False
return False
else:
logger.warning(
f"Checking activity {self.name} constraints even though activity has no constraints."
Expand Down
7 changes: 7 additions & 0 deletions paseos/actors/actor_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def set_ground_station_location(
latitude: float,
longitude: float,
elevation: float = 0,
minimum_altitude_angle: float = 30,
):
"""Define the position of a ground station actor.
Expand All @@ -60,14 +61,20 @@ def set_ground_station_location(
elevation (float): A distance specifying elevation above (positive)
or below (negative) the surface of the Earth
ellipsoid specified by the WSG84 model in meters. Defaults to 0.
minimum_altitude_angle (float): Minimum angle above the horizon that
this station can communicate with.
"""
assert latitude >= -90 and latitude <= 90, "Latitude is -90 <= lat <= 90"
assert longitude >= -180 and longitude <= 180, "Longitude is -180 <= lat <= 180"
assert (
minimum_altitude_angle >= 0 and minimum_altitude_angle <= 90
), "0 <= minimum_altitude_angle <= 90."
actor._skyfield_position = wgs84.latlon(
latitude_degrees=latitude,
longitude_degrees=longitude,
elevation_m=elevation,
)
actor._minimum_altitude_angle = minimum_altitude_angle

def set_orbit(
actor: SpacecraftActor,
Expand Down
23 changes: 21 additions & 2 deletions paseos/actors/base_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions paseos/actors/ground_station_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class GroundstationActor(BaseActor):
# Timescale object to convert from pykep epoch to skyfield time
_skyfield_timescale = load.timescale()

# Minimum angle to communicate with this ground station
_minimum_altitude_angle = None

def __init__(
self,
name: str,
Expand Down
2 changes: 1 addition & 1 deletion paseos/actors/spacecraft_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions paseos/communication/get_communication_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def get_communication_window(
pk.epoch: communication window end time.
int: data that can be transmitted in the communication window [b].
"""
logger.debug(f"Computing comms window between {local_actor} and {target_actor}")

assert local_actor_communication_link_name in local_actor.communication_devices, (
"Trying to use a not-existing communication link with the name: "
Expand Down
4 changes: 4 additions & 0 deletions paseos/communication/is_in_line_of_sight.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,17 @@ def is_in_line_of_sight(
type(actor).__name__ == "GroundstationActor"
and type(other_actor).__name__ == "SpacecraftActor"
):
if minimum_altitude_angle is None:
minimum_altitude_angle = actor._minimum_altitude_angle
return _is_in_line_of_sight_ground_station_to_spacecraft(
actor, other_actor, epoch, minimum_altitude_angle, plot
)
elif (
type(actor).__name__ == "SpacecraftActor"
and type(other_actor).__name__ == "GroundstationActor"
):
if minimum_altitude_angle is None:
minimum_altitude_angle = other_actor._minimum_altitude_angle
return _is_in_line_of_sight_ground_station_to_spacecraft(
other_actor, actor, epoch, minimum_altitude_angle, plot
)
Expand Down
35 changes: 35 additions & 0 deletions paseos/paseos.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import types
import asyncio
import sys

from dotmap import DotMap
from loguru import logger
import pykep as pk

from paseos.actors.base_actor import BaseActor
from paseos.activities.activity_manager import ActivityManager
from paseos.utils.operations_monitor import OperationsMonitor


class PASEOS:
Expand All @@ -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

Expand Down Expand Up @@ -62,6 +68,20 @@ def __init__(self, local_actor: BaseActor, cfg=None):
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:
"""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
Expand Down Expand Up @@ -92,6 +112,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):
Expand Down Expand Up @@ -161,6 +188,14 @@ def remove_known_actor(self, actor_name: str):
), f"Actor {actor_name} is not in known. Available are {self.known_actors.keys()}"
del self._known_actors[actor_name]

def remove_activity(self, name: str):
"""Removes a registered activity
Args:
name (str): Name of the activity.
"""
self._activity_manager.remove_activity(name=name)

def register_activity(
self,
name: str,
Expand Down
3 changes: 3 additions & 0 deletions paseos/resources/default_cfg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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

[power]

[comms]
Expand Down
35 changes: 35 additions & 0 deletions paseos/tests/activity_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ async def wait_for_activity(sim):
await asyncio.sleep(0.1)


# Below test can be used to check what happens when you formulate an invalid constraint function.
# It is temporarily commented out as it doesn't really check right now because I could not figure
# out a way to get an exception raised from the async code.
# @pytest.mark.asyncio
# async def test_faulty_constraint_function():
# """Check whether specifying a wrong constraint function leads to an error"""

# sim, _, _ = get_default_instance()

# # A pointless activity
# async def func(args):
# await asyncio.sleep(1.5)

# # A constraint function that fails to return True or False
# async def constraint(args):
# pass

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

# # Run the activity
# sim.perform_activity("Testing")
# await wait_for_activity(sim)


# tell pytest to create an event loop and execute the tests using the event loop
@pytest.mark.asyncio
async def test_activity():
Expand Down Expand Up @@ -48,6 +78,11 @@ async def func(args):
# So should be roughly 20W - 6W consumed from starting 500
assert sat1.battery_level_in_Ws > 480 and sat1.battery_level_in_Ws < 490

# test removing the activity
assert "Testing" in sim._activity_manager._activities.keys()
sim.remove_activity("Testing")
assert "Testing" not in sim._activity_manager._activities.keys()


@pytest.mark.asyncio
async def test_running_two_activities():
Expand Down
2 changes: 1 addition & 1 deletion paseos/tests/actor_builder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading

0 comments on commit 7f0f8e2

Please sign in to comment.