Skip to content
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
8 changes: 5 additions & 3 deletions tests/test_sailship.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
from parcels import Field, FieldSet

from virtual_ship import InstrumentType, Location, Waypoint
from virtual_ship import InstrumentType, Location, Schedule, Waypoint
from virtual_ship.sailship import PlanningError, _verify_waypoints, sailship
from virtual_ship.virtual_ship_config import (
ADCPConfig,
Expand Down Expand Up @@ -146,9 +146,11 @@ def test_sailship() -> None:
),
]

schedule = Schedule(waypoints=waypoints)

config = VirtualShipConfig(
ship_speed=5.14,
waypoints=waypoints,
schedule=schedule,
argo_float_config=argo_float_config,
adcp_config=adcp_config,
ship_underwater_st_config=ship_underwater_st_config,
Expand Down Expand Up @@ -220,7 +222,7 @@ def test_verify_waypoints() -> None:
for waypoints, expect_match in zip(WAYPOINTS, EXPECT_MATCH, strict=True):
config = VirtualShipConfig(
ship_speed=5.14,
waypoints=waypoints,
schedule=Schedule(waypoints),
argo_float_config=argo_float_config,
adcp_config=None,
ship_underwater_st_config=None,
Expand Down
18 changes: 18 additions & 0 deletions tests/test_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import py

from virtual_ship import Location, Schedule, Waypoint


def test_schedule(tmpdir: py.path.LocalPath) -> None:
out_path = tmpdir.join("schedule.yaml")

schedule = Schedule(
[
Waypoint(Location(0, 0), time=0, instrument=None),
Waypoint(Location(1, 1), time=1, instrument=None),
]
)
schedule.to_yaml(out_path)

schedule2 = Schedule.from_yaml(out_path)
assert schedule == schedule2
2 changes: 2 additions & 0 deletions virtual_ship/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
from .instrument_type import InstrumentType
from .location import Location
from .planning_error import PlanningError
from .schedule import Schedule
from .spacetime import Spacetime
from .waypoint import Waypoint

__all__ = [
"InstrumentType",
"Location",
"PlanningError",
"Schedule",
"Spacetime",
"Waypoint",
"instruments",
Expand Down
4 changes: 2 additions & 2 deletions virtual_ship/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ def costs(config: VirtualShipConfig, total_time: timedelta):
num_argos = len(
[
waypoint
for waypoint in config.waypoints
for waypoint in config.schedule.waypoints
if waypoint.instrument is InstrumentType.ARGO_FLOAT
]
)
argo_cost = num_argos * argo_deploy_cost
num_drifters = len(
[
waypoint
for waypoint in config.waypoints
for waypoint in config.schedule.waypoints
if waypoint.instrument is InstrumentType.DRIFTER
]
)
Expand Down
29 changes: 16 additions & 13 deletions virtual_ship/sailship.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def sailship(config: VirtualShipConfig):

# simulate the sailing and aggregate what measurements should be simulated
schedule_results = _simulate_schedule(
waypoints=config.waypoints,
projection=projection,
config=config,
)
Expand Down Expand Up @@ -98,29 +97,31 @@ def sailship(config: VirtualShipConfig):

# calculate cruise cost
assert (
config.waypoints[0].time is not None
config.schedule.waypoints[0].time is not None
), "First waypoints cannot have None time as this has been verified before during config verification."
time_past = schedule_results.end_spacetime.time - config.waypoints[0].time
time_past = schedule_results.end_spacetime.time - config.schedule.waypoints[0].time
cost = costs(config, time_past)
print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.")


def _simulate_schedule(
waypoints: list[Waypoint],
projection: pyproj.Geod,
config: VirtualShipConfig,
) -> _ScheduleResults:
"""
Simulate the sailing and aggregate the virtual measurements that should be taken.

:param waypoints: The waypoints.
:param projection: Projection used to sail between waypoints.
:param config: The cruise configuration.
:returns: Results from the simulation.
:raises NotImplementedError: When unsupported instruments are encountered.
:raises RuntimeError: When schedule appears infeasible. This should not happen in this version of virtual ship as the schedule is verified beforehand.
"""
cruise = _Cruise(Spacetime(waypoints[0].location, waypoints[0].time))
cruise = _Cruise(
Spacetime(
config.schedule.waypoints[0].location, config.schedule.waypoints[0].time
)
)
measurements = _MeasurementsToSimulate()

# add recurring tasks to task list
Expand All @@ -143,7 +144,7 @@ def _simulate_schedule(
)

# sail to each waypoint while executing tasks
for waypoint in waypoints:
for waypoint in config.schedule.waypoints:
if waypoint.time is not None and cruise.spacetime.time > waypoint.time:
raise RuntimeError(
"Could not reach waypoint in time. This should not happen in this version of virtual ship as the schedule is verified beforehand."
Expand Down Expand Up @@ -398,15 +399,15 @@ def _verify_waypoints(
:raises PlanningError: If waypoints are not feasible or incorrect.
:raises ValueError: If there are no fieldsets in the config, which are needed to verify all waypoints are on water.
"""
if len(config.waypoints) == 0:
if len(config.schedule.waypoints) == 0:
raise PlanningError("At least one waypoint must be provided.")

# check first waypoint has a time
if config.waypoints[0].time is None:
if config.schedule.waypoints[0].time is None:
raise PlanningError("First waypoint must have a specified time.")

# check waypoint times are in ascending order
timed_waypoints = [wp for wp in config.waypoints if wp.time is not None]
timed_waypoints = [wp for wp in config.schedule.waypoints if wp.time is not None]
if not all(
[
next.time >= cur.time
Expand Down Expand Up @@ -446,7 +447,7 @@ def _verify_waypoints(
# get waypoints with 0 UV
land_waypoints = [
(wp_i, wp)
for wp_i, wp in enumerate(config.waypoints)
for wp_i, wp in enumerate(config.schedule.waypoints)
if _is_on_land_zero_uv(fieldset, wp)
]
# raise an error if there are any
Expand All @@ -456,8 +457,10 @@ def _verify_waypoints(
)

# check that ship will arrive on time at each waypoint (in case no unexpected event happen)
time = config.waypoints[0].time
for wp_i, (wp, wp_next) in enumerate(zip(config.waypoints, config.waypoints[1:])):
time = config.schedule.waypoints[0].time
for wp_i, (wp, wp_next) in enumerate(
zip(config.schedule.waypoints, config.schedule.waypoints[1:])
):
if wp.instrument is InstrumentType.CTD:
time += timedelta(minutes=20)

Expand Down
60 changes: 60 additions & 0 deletions virtual_ship/schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Schedule class."""

from __future__ import annotations

from dataclasses import dataclass

import yaml

from .location import Location
from .waypoint import Waypoint


@dataclass
class Schedule:
"""Schedule of the virtual ship."""

waypoints: list[Waypoint]

@classmethod
def from_yaml(cls, path: str) -> Schedule:
"""
Load schedule from YAML file.

:param path: The file to read from.
:returns: Schedule of waypoints from the YAML file.
"""
with open(path, "r") as in_file:
data = yaml.safe_load(in_file)
waypoints = [
Waypoint(
location=Location(waypoint["lat"], waypoint["lon"]),
time=waypoint["time"],
instrument=waypoint["instrument"],
)
for waypoint in data
]
return Schedule(waypoints)

def to_yaml(self, path: str) -> None:
"""
Save schedule to YAML file.

:param path: The file to write to.
"""
with open(path, "w") as out_file:
print(
yaml.dump(
[
{
"lat": waypoint.location.lat,
"lon": waypoint.location.lon,
"time": waypoint.time,
"instrument": waypoint.instrument,
}
for waypoint in self.waypoints
],
out_file,
)
)
pass
6 changes: 3 additions & 3 deletions virtual_ship/virtual_ship_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from parcels import FieldSet

from .waypoint import Waypoint
from .schedule import Schedule


@dataclass
Expand Down Expand Up @@ -64,7 +64,7 @@ class VirtualShipConfig:

ship_speed: float # m/s

waypoints: list[Waypoint]
schedule: Schedule

argo_float_config: ArgoFloatConfig
adcp_config: ADCPConfig | None # if None, ADCP is disabled
Expand All @@ -80,7 +80,7 @@ def verify(self) -> None:

:raises ValueError: If not valid.
"""
if len(self.waypoints) < 2:
if len(self.schedule.waypoints) < 2:
raise ValueError("Waypoints require at least a start and an end.")

if self.argo_float_config.max_depth > 0:
Expand Down