From fce8fb4260ad16661b02a3caefbe49633e48a363 Mon Sep 17 00:00:00 2001 From: Emma Daniels Date: Fri, 16 Aug 2024 15:51:38 +0000 Subject: [PATCH 1/6] work in progress --- tests/test_schedule.py | 5 +++++ virtual_ship/__init__.py | 2 ++ virtual_ship/schedule.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 tests/test_schedule.py create mode 100644 virtual_ship/schedule.py diff --git a/tests/test_schedule.py b/tests/test_schedule.py new file mode 100644 index 00000000..bb577719 --- /dev/null +++ b/tests/test_schedule.py @@ -0,0 +1,5 @@ +from virtual_ship import Schedule, Waypoint, Location + +def test_schedule() -> None: + schedule = Schedule([Waypoint(Location(0, 0), 0), Waypoint(Location(1, 1), 1)]) + schedule.to_yaml("schedule.yaml") \ No newline at end of file diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index 31a64875..19b6a830 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -6,6 +6,7 @@ from .planning_error import PlanningError from .spacetime import Spacetime from .waypoint import Waypoint +from .schedule import Schedule __all__ = [ "InstrumentType", @@ -15,4 +16,5 @@ "Waypoint", "instruments", "sailship", + "Schedule", ] diff --git a/virtual_ship/schedule.py b/virtual_ship/schedule.py new file mode 100644 index 00000000..44706650 --- /dev/null +++ b/virtual_ship/schedule.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from dataclasses import dataclass +from .waypoint import Waypoint +import yaml + +@dataclass +class Schedule: + """Schedule of the virtual ship.""" + + waypoints: list[Waypoint] + + def from_yaml(cls, path: str) -> Schedule: + """Load schedule from YAML file.""" + pass + + def to_yaml(self, path: str) -> None: + """Save schedule to YAML file.""" + print(yaml.dumpf([waypoint.to_dict() for waypoint in self.waypoints], path)) + pass \ No newline at end of file From 5e7925b3775729c175762bcf612388befd5a8c72 Mon Sep 17 00:00:00 2001 From: Emma Daniels Date: Sun, 18 Aug 2024 13:22:32 +0000 Subject: [PATCH 2/6] add waypoint schedule --- tests/test_schedule.py | 19 ++++++++--- virtual_ship/__init__.py | 4 +-- virtual_ship/sailship.py | 5 +-- virtual_ship/schedule.py | 52 +++++++++++++++++++++++++---- virtual_ship/virtual_ship_config.py | 4 +-- 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/tests/test_schedule.py b/tests/test_schedule.py index bb577719..ae92910d 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -1,5 +1,16 @@ -from virtual_ship import Schedule, Waypoint, Location +import py +from virtual_ship import Location, Schedule, Waypoint -def test_schedule() -> None: - schedule = Schedule([Waypoint(Location(0, 0), 0), Waypoint(Location(1, 1), 1)]) - schedule.to_yaml("schedule.yaml") \ No newline at end of file +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 diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index 19b6a830..717f5c38 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -4,17 +4,17 @@ 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 -from .schedule import Schedule __all__ = [ "InstrumentType", "Location", "PlanningError", + "Schedule", "Spacetime", "Waypoint", "instruments", "sailship", - "Schedule", ] diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 4569135b..11af5753 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -21,6 +21,7 @@ from .instruments.ship_underwater_st import simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError +from .schedule import Schedule from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint @@ -106,14 +107,14 @@ def sailship(config: VirtualShipConfig): def _simulate_schedule( - waypoints: list[Waypoint], + waypoints: Schedule, projection: pyproj.Geod, config: VirtualShipConfig, ) -> _ScheduleResults: """ Simulate the sailing and aggregate the virtual measurements that should be taken. - :param waypoints: The waypoints. + :param waypoints: The schedule of waypoints. :param projection: Projection used to sail between waypoints. :param config: The cruise configuration. :returns: Results from the simulation. diff --git a/virtual_ship/schedule.py b/virtual_ship/schedule.py index 44706650..5418d2e4 100644 --- a/virtual_ship/schedule.py +++ b/virtual_ship/schedule.py @@ -1,20 +1,60 @@ +"""Schedule class.""" + from __future__ import annotations from dataclasses import dataclass -from .waypoint import Waypoint + 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.""" - pass + """ + 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.""" - print(yaml.dumpf([waypoint.to_dict() for waypoint in self.waypoints], path)) - pass \ No newline at end of file + """ + 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 diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 42e8c5d8..953a7621 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -5,7 +5,7 @@ from parcels import FieldSet -from .waypoint import Waypoint +from .schedule import Schedule @dataclass @@ -64,7 +64,7 @@ class VirtualShipConfig: ship_speed: float # m/s - waypoints: list[Waypoint] + waypoints: Schedule argo_float_config: ArgoFloatConfig adcp_config: ADCPConfig | None # if None, ADCP is disabled From 19731ed0e77f0280ba3b4ce0ddb3ffab903abdf4 Mon Sep 17 00:00:00 2001 From: Emma Daniels Date: Sun, 18 Aug 2024 13:26:11 +0000 Subject: [PATCH 3/6] fix blank line --- tests/test_schedule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_schedule.py b/tests/test_schedule.py index ae92910d..bc00fd6d 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -1,6 +1,7 @@ import py from virtual_ship import Location, Schedule, Waypoint + def test_schedule(tmpdir: py.path.LocalPath) -> None: out_path = tmpdir.join("schedule.yaml") From 5ed2f88e56e49c09dd65277c9a41e4dfbf24ea5f Mon Sep 17 00:00:00 2001 From: Emma Daniels Date: Sun, 18 Aug 2024 13:31:17 +0000 Subject: [PATCH 4/6] fix isort --- tests/test_schedule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_schedule.py b/tests/test_schedule.py index bc00fd6d..371c786f 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -1,4 +1,5 @@ import py + from virtual_ship import Location, Schedule, Waypoint From 54970ecc24179943c99d33a17b8c6f952bcc8759 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Sun, 18 Aug 2024 15:52:02 +0200 Subject: [PATCH 5/6] use schedule in some more places --- tests/test_sailship.py | 7 +++++-- virtual_ship/costs.py | 4 ++-- virtual_ship/sailship.py | 28 ++++++++++++++++------------ virtual_ship/virtual_ship_config.py | 4 ++-- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index f3691d49..6c0bdd72 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -18,6 +18,7 @@ ShipUnderwaterSTConfig, VirtualShipConfig, ) +from virtual_ship import Schedule def _make_ctd_fieldset(base_time: datetime) -> FieldSet: @@ -146,9 +147,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, @@ -220,7 +223,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, diff --git a/virtual_ship/costs.py b/virtual_ship/costs.py index 4339eca4..9e4c20e6 100644 --- a/virtual_ship/costs.py +++ b/virtual_ship/costs.py @@ -22,7 +22,7 @@ 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 ] ) @@ -30,7 +30,7 @@ def costs(config: VirtualShipConfig, total_time: timedelta): num_drifters = len( [ waypoint - for waypoint in config.waypoints + for waypoint in config.schedule.waypoints if waypoint.instrument is InstrumentType.DRIFTER ] ) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 11af5753..da843780 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -42,7 +42,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, ) @@ -99,15 +98,14 @@ 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: Schedule, projection: pyproj.Geod, config: VirtualShipConfig, ) -> _ScheduleResults: @@ -121,7 +119,11 @@ def _simulate_schedule( :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 @@ -144,7 +146,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." @@ -399,15 +401,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 @@ -447,7 +449,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 @@ -457,8 +459,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) diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 953a7621..9dd20a20 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -64,7 +64,7 @@ class VirtualShipConfig: ship_speed: float # m/s - waypoints: Schedule + schedule: Schedule argo_float_config: ArgoFloatConfig adcp_config: ADCPConfig | None # if None, ADCP is disabled @@ -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: From 647fcfbc4ca4f745f8e1657b8d29223f2d6cc6dd Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Sun, 18 Aug 2024 15:53:21 +0200 Subject: [PATCH 6/6] codetools --- tests/test_sailship.py | 3 +-- virtual_ship/sailship.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 6c0bdd72..0cdcd521 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -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, @@ -18,7 +18,6 @@ ShipUnderwaterSTConfig, VirtualShipConfig, ) -from virtual_ship import Schedule def _make_ctd_fieldset(base_time: datetime) -> FieldSet: diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index da843780..5da43dae 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -21,7 +21,6 @@ from .instruments.ship_underwater_st import simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError -from .schedule import Schedule from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint @@ -112,7 +111,6 @@ def _simulate_schedule( """ Simulate the sailing and aggregate the virtual measurements that should be taken. - :param waypoints: The schedule of waypoints. :param projection: Projection used to sail between waypoints. :param config: The cruise configuration. :returns: Results from the simulation.