From 798144b9d0c85582cb6c9cee0083a4d7fa893c46 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 14:12:14 +0200 Subject: [PATCH 01/29] wip --- tests/parcelscrash.py | 59 ++++ tests/test_sailship.py | 34 +- virtual_ship/__init__.py | 11 +- virtual_ship/instrument_deployment.py | 2 + virtual_ship/sailship.py | 361 ++++++++++++--------- virtual_ship/virtual_ship_configuration.py | 84 +---- virtual_ship/waypoint.py | 11 + 7 files changed, 321 insertions(+), 241 deletions(-) create mode 100644 tests/parcelscrash.py create mode 100644 virtual_ship/instrument_deployment.py create mode 100644 virtual_ship/waypoint.py diff --git a/tests/parcelscrash.py b/tests/parcelscrash.py new file mode 100644 index 00000000..f39fef13 --- /dev/null +++ b/tests/parcelscrash.py @@ -0,0 +1,59 @@ +import numpy as np +from parcels import FieldSet, JITParticle, ParticleSet, Variable + +from datetime import datetime + + +Particle = JITParticle.add_variables( + [ + Variable("temperature", dtype=np.float32, initial=np.nan), + ] +) + + +def sample_temperature(particle, fieldset, time): + particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] + + +def simulate_ctd() -> None: + base_time = datetime.strptime("1950-01-01", "%Y-%m-%d") + + fieldset = FieldSet.from_data( + { + "V": np.zeros((2, 2, 2, 2)), + "U": np.zeros((2, 2, 2, 2)), + "T": np.zeros((2, 2, 2, 2)), + }, + { + "time": [ + np.datetime64(base_time + datetime.timedelta(hours=0)), + np.datetime64(base_time + datetime.timedelta(hours=1)), + ], + "depth": [-1000, 0], + "lat": [0, 1], + "lon": [0, 1], + }, + ) + + fieldset_endtime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[-1]) + + particleset = ParticleSet( + fieldset=fieldset, + pclass=Particle, + lon=[], + lat=[], + depth=[], + time=[], + ) + + # define output file for the simulation + out_file = particleset.ParticleFile(name="test") + + # execute simulation + particleset.execute( + [sample_temperature], + endtime=fieldset_endtime, + dt=5, + verbose_progress=False, + output_file=out_file, + ) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 2f3dd974..5b220975 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -5,7 +5,7 @@ import numpy as np from parcels import Field, FieldSet -from virtual_ship import Location +from virtual_ship import Location, Waypoint, InstrumentDeployment from virtual_ship.sailship import sailship from virtual_ship.virtual_ship_configuration import ( ADCPConfig, @@ -93,26 +93,32 @@ def test_sailship() -> None: adcp_config = ADCPConfig(max_depth=-1000, bin_size_m=24) - config = VirtualShipConfig( - start_time=datetime.datetime.strptime( - "2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S" + waypoints = [ + Waypoint( + location=Location(latitude=-23.071289, longitude=63.743631), + time=datetime.datetime.strptime("2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"), ), - route_coordinates=[ - Location(latitude=-23.071289, longitude=63.743631), - Location(latitude=-23.081289, longitude=63.743631), - Location(latitude=-23.191289, longitude=63.743631), - ], + Waypoint(location=Location(latitude=-23.081289, longitude=63.743631)), + ] + + config = VirtualShipConfig( + ship_speed=5.14, + waypoints=waypoints, adcp_fieldset=adcp_fieldset, ship_underwater_st_fieldset=ship_underwater_st_fieldset, ctd_fieldset=ctd_fieldset, drifter_fieldset=drifter_fieldset, - argo_float_deploy_locations=[ - Location(latitude=-23.081289, longitude=63.743631) - ], - drifter_deploy_locations=[Location(latitude=-23.081289, longitude=63.743631)], - ctd_deploy_locations=[Location(latitude=-23.081289, longitude=63.743631)], argo_float_config=argo_float_config, adcp_config=adcp_config, ) sailship(config) + + # start_time=datetime.datetime.strptime( + # "2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S" + # ), + # route_coordinates=[ + # Location(latitude=-23.071289, longitude=63.743631), + # Location(latitude=-23.081289, longitude=63.743631), + # Location(latitude=-23.191289, longitude=63.743631), + # ], diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index dbf175e3..1db24aec 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -3,5 +3,14 @@ from . import instruments, sailship from .location import Location from .spacetime import Spacetime +from .waypoint import Waypoint +from .instrument_deployment import InstrumentDeployment -__all__ = ["Location", "Spacetime", "instruments", "sailship"] +__all__ = [ + "Location", + "Spacetime", + "instruments", + "sailship", + "InstrumentDeployment", + "Waypoint", +] diff --git a/virtual_ship/instrument_deployment.py b/virtual_ship/instrument_deployment.py new file mode 100644 index 00000000..a055362e --- /dev/null +++ b/virtual_ship/instrument_deployment.py @@ -0,0 +1,2 @@ +class InstrumentDeployment: + pass diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 9a02c828..7c723352 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -16,6 +16,7 @@ from .postprocess import postprocess from .spacetime import Spacetime from .virtual_ship_configuration import VirtualShipConfig +from .waypoint import Waypoint def sailship(config: VirtualShipConfig): @@ -26,162 +27,44 @@ def sailship(config: VirtualShipConfig): """ config.verify() - # combine identical instrument deploy location - argo_locations = set(config.argo_float_deploy_locations) - if len(argo_locations) != len(config.argo_float_deploy_locations): - print( - "WARN: Some argo float deployment locations are identical and have been combined." - ) - - drifter_locations = set(config.drifter_deploy_locations) - if len(drifter_locations) != len(config.drifter_deploy_locations): - print( - "WARN: Some drifter deployment locations are identical and have been combined." - ) - - ctd_locations = set(config.ctd_deploy_locations) - if len(drifter_locations) != len(config.ctd_deploy_locations): - print("WARN: Some CTD locations are identical and have been combined.") - - # get discrete points along the ships route were sampling and deployments will be performed - route_dt = timedelta(minutes=5) - route_points = shiproute(config=config, dt=route_dt) + verify_waypoints(config.waypoints) - # adcp objects to be used in simulation + # lists of instrument deployments to be simulated adcps: list[Spacetime] = [] - - # ship st objects to be used in simulation ship_underwater_sts: list[Spacetime] = [] - - # argo float deployment locations that have been visited - argo_locations_visited: set[Location] = set() - # argo float objects to be used in simulation argo_floats: list[ArgoFloat] = [] - - # drifter deployment locations that have been visited - drifter_locations_visited: set[Location] = set() - # drifter objects to be used in simulation drifters: list[Drifter] = [] - - # ctd cast locations that have been visited - ctd_locations_visited: set[Location] = set() - # ctd cast objects to be used in simulation ctds: list[CTD] = [] - # iterate over each discrete route point, find deployment and measurement locations and times, and measure how much time this took - # TODO between drifters, argo floats, ctd there is quite some repetition - print("Traversing ship route.") - time_past = timedelta() - for i, route_point in enumerate(route_points): - if i % 96 == 0: - print(f"Gathered data {time_past} hours since start.") + # projection used to sail between waypoints + geod = pyproj.Geod(ellps="WGS84") - # find drifter deployments to be done at this location - drifters_here = set( - [ - drifter - for drifter in drifter_locations - drifter_locations_visited - if all( - np.isclose( - [drifter.lat, drifter.lon], [route_point.lat, route_point.lon] - ) - ) - ] - ) - if len(drifters_here) > 1: - print( - "WARN: Multiple drifter deployments match the current location. Only a single deployment will be performed." - ) - drifters.append( - Drifter( - spacetime=Spacetime( - location=route_point, time=time_past.total_seconds() - ), - depth=-config.drifter_fieldset.U.depth[0], - lifetime=timedelta(weeks=4), - ) - ) - drifter_locations_visited = drifter_locations_visited.union(drifters_here) + time = config.waypoints[0].time + assert time is not None # cannot happen as verify_waypoints checks this - # find argo float deployments to be done at this location - argos_here = set( - [ - argo - for argo in argo_locations - argo_locations_visited - if all( - np.isclose([argo.lat, argo.lon], [route_point.lat, route_point.lon]) - ) - ] - ) - if len(argos_here) > 1: - print( - "WARN: Multiple argo float deployments match the current location. Only a single deployment will be performed." - ) - argo_floats.append( - ArgoFloat( - spacetime=Spacetime( - location=route_point, time=time_past.total_seconds() - ), - min_depth=-config.argo_float_config.fieldset.U.depth[0], - max_depth=config.argo_float_config.max_depth, - drift_depth=config.argo_float_config.drift_depth, - vertical_speed=config.argo_float_config.vertical_speed, - cycle_days=config.argo_float_config.cycle_days, - drift_days=config.argo_float_config.drift_days, - ) - ) - argo_locations_visited = argo_locations_visited.union(argos_here) + for wp, wp_next in zip(config.waypoints, config.waypoints[1:] + [None]): + if wp.instrument is not None: + pass # TODO - # find CTD casts to be done at this location - ctds_here = set( - [ - ctd - for ctd in ctd_locations - ctd_locations_visited - if all( - np.isclose([ctd.lat, ctd.lon], [route_point.lat, route_point.lon]) - ) - ] - ) - if len(ctds_here) > 1: - print( - "WARN: Multiple CTD casts match the current location. Only a single cast will be performed." - ) + if wp_next is None: + break - ctds.append( - CTD( - spacetime=Spacetime( - location=route_point, - time=config.start_time + time_past, - ), - min_depth=config.ctd_fieldset.U.depth[0], - max_depth=config.ctd_fieldset.U.depth[-1], - ) - ) - ctd_locations_visited = ctd_locations_visited.union(ctds_here) - # add 20 minutes to sailing time for deployment - if len(ctds_here) != 0: - time_past += timedelta(minutes=20) - - # add time it takes to move to the next route point - time_past += route_dt - # remove the last one, because no sailing to the next point was needed - time_past -= route_dt - - # check if all drifter, argo float, ctd locations were visited - if len(drifter_locations_visited) != len(drifter_locations): - print( - "WARN: some drifter deployments were not planned along the route and have not been performed." + geodinv: tuple[float, float, float] = geod.inv( + wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat ) + distance = geodinv[2] - if len(argo_locations_visited) != len(argo_locations): - print( - "WARN: some argo float deployments were not planned along the route and have not been performed." - ) + time_to_reach = timedelta(seconds=distance / config.ship_speed) + arrival_time = time + time_to_reach - if len(ctd_locations_visited) != len(ctd_locations): - print( - "WARN: some CTD casts were not planned along the route and have not been performed." - ) + if wp_next.time is None: + time = arrival_time + elif arrival_time > wp_next.time: + raise RuntimeError( + "Arrived too late at the next waypoint. This error should never happen because the schedule should have been verified beforehand in this function." + ) + else: + time = wp_next.time print("Simulating onboard salinity and temperature measurements.") simulate_ship_underwater_st( @@ -228,14 +111,192 @@ def sailship(config: VirtualShipConfig): endtime=None, ) - # convert CTD data to CSV - print("Postprocessing..") - postprocess() - - print("All data has been gathered and postprocessed, returning home.") - - cost = costs(config, time_past) - print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") + # # iterate over each discrete route point, find deployment and measurement locations and times, and measure how much time this took + # # TODO between drifters, argo floats, ctd there is quite some repetition + # print("Traversing ship route.") + # time_past = timedelta() + # for i, route_point in enumerate(route_points): + # if i % 96 == 0: + # print(f"Gathered data {time_past} hours since start.") + + # # find drifter deployments to be done at this location + # drifters_here = set( + # [ + # drifter + # for drifter in drifter_locations - drifter_locations_visited + # if all( + # np.isclose( + # [drifter.lat, drifter.lon], [route_point.lat, route_point.lon] + # ) + # ) + # ] + # ) + # if len(drifters_here) > 1: + # print( + # "WARN: Multiple drifter deployments match the current location. Only a single deployment will be performed." + # ) + # drifters.append( + # Drifter( + # spacetime=Spacetime( + # location=route_point, time=time_past.total_seconds() + # ), + # depth=-config.drifter_fieldset.U.depth[0], + # lifetime=timedelta(weeks=4), + # ) + # ) + # drifter_locations_visited = drifter_locations_visited.union(drifters_here) + + # # find argo float deployments to be done at this location + # argos_here = set( + # [ + # argo + # for argo in argo_locations - argo_locations_visited + # if all( + # np.isclose([argo.lat, argo.lon], [route_point.lat, route_point.lon]) + # ) + # ] + # ) + # if len(argos_here) > 1: + # print( + # "WARN: Multiple argo float deployments match the current location. Only a single deployment will be performed." + # ) + # argo_floats.append( + # ArgoFloat( + # spacetime=Spacetime( + # location=route_point, time=time_past.total_seconds() + # ), + # min_depth=-config.argo_float_config.fieldset.U.depth[0], + # max_depth=config.argo_float_config.max_depth, + # drift_depth=config.argo_float_config.drift_depth, + # vertical_speed=config.argo_float_config.vertical_speed, + # cycle_days=config.argo_float_config.cycle_days, + # drift_days=config.argo_float_config.drift_days, + # ) + # ) + # argo_locations_visited = argo_locations_visited.union(argos_here) + + # # find CTD casts to be done at this location + # ctds_here = set( + # [ + # ctd + # for ctd in ctd_locations - ctd_locations_visited + # if all( + # np.isclose([ctd.lat, ctd.lon], [route_point.lat, route_point.lon]) + # ) + # ] + # ) + # if len(ctds_here) > 1: + # print( + # "WARN: Multiple CTD casts match the current location. Only a single cast will be performed." + # ) + + # ctds.append( + # CTD( + # spacetime=Spacetime( + # location=route_point, + # time=config.start_time + time_past, + # ), + # min_depth=config.ctd_fieldset.U.depth[0], + # max_depth=config.ctd_fieldset.U.depth[-1], + # ) + # ) + # ctd_locations_visited = ctd_locations_visited.union(ctds_here) + # # add 20 minutes to sailing time for deployment + # if len(ctds_here) != 0: + # time_past += timedelta(minutes=20) + + # # add time it takes to move to the next route point + # time_past += route_dt + # # remove the last one, because no sailing to the next point was needed + # time_past -= route_dt + + # # check if all drifter, argo float, ctd locations were visited + # if len(drifter_locations_visited) != len(drifter_locations): + # print( + # "WARN: some drifter deployments were not planned along the route and have not been performed." + # ) + + # if len(argo_locations_visited) != len(argo_locations): + # print( + # "WARN: some argo float deployments were not planned along the route and have not been performed." + # ) + + # if len(ctd_locations_visited) != len(ctd_locations): + # print( + # "WARN: some CTD casts were not planned along the route and have not been performed." + # ) + + # print("Simulating onboard salinity and temperature measurements.") + # simulate_ship_underwater_st( + # fieldset=config.ship_underwater_st_fieldset, + # out_path=os.path.join("results", "ship_underwater_st.zarr"), + # depth=-2, + # sample_points=ship_underwater_sts, + # ) + + # print("Simulating onboard ADCP.") + # simulate_adcp( + # fieldset=config.adcp_fieldset, + # out_path=os.path.join("results", "adcp.zarr"), + # max_depth=config.adcp_config.max_depth, + # min_depth=-5, + # num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, + # sample_points=adcps, + # ) + + # print("Simulating CTD casts.") + # simulate_ctd( + # out_path=os.path.join("results", "ctd.zarr"), + # fieldset=config.ctd_fieldset, + # ctds=ctds, + # outputdt=timedelta(seconds=10), + # ) + + # print("Simulating drifters") + # simulate_drifters( + # out_path=os.path.join("results", "drifters.zarr"), + # fieldset=config.drifter_fieldset, + # drifters=drifters, + # outputdt=timedelta(hours=5), + # dt=timedelta(minutes=5), + # endtime=None, + # ) + + # print("Simulating argo floats") + # simulate_argo_floats( + # out_path=os.path.join("results", "argo_floats.zarr"), + # argo_floats=argo_floats, + # fieldset=config.argo_float_config.fieldset, + # outputdt=timedelta(minutes=5), + # endtime=None, + # ) + + # # convert CTD data to CSV + # print("Postprocessing..") + # postprocess() + + # print("All data has been gathered and postprocessed, returning home.") + + # cost = costs(config, time_past) + # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") + + +def verify_waypoints(waypoints: list[Waypoint]) -> None: + # check first waypoint has a time + if waypoints[0].time is None: + raise ValueError("First waypoint must have a specified time.") + + # check waypoint times are in ascending order + timed_waypoints = [wp for wp in waypoints if wp.time is not None] + if not all( + [ + next.time >= cur.time + for cur, next in zip(timed_waypoints, timed_waypoints[1:]) + ] + ): + raise ValueError("Each waypoint should be timed after all previous waypoints") + + # TODO more def shiproute(config: VirtualShipConfig, dt: timedelta) -> list[Location]: diff --git a/virtual_ship/virtual_ship_configuration.py b/virtual_ship/virtual_ship_configuration.py index 8d48d4c0..e082641d 100644 --- a/virtual_ship/virtual_ship_configuration.py +++ b/virtual_ship/virtual_ship_configuration.py @@ -7,6 +7,7 @@ from parcels import FieldSet from .location import Location +from .waypoint import Waypoint @dataclass @@ -33,18 +34,15 @@ class ADCPConfig: class VirtualShipConfig: """Configuration of the virtual ship.""" - start_time: datetime - route_coordinates: list[Location] + ship_speed: float # m/s + + waypoints: list[Waypoint] adcp_fieldset: FieldSet ship_underwater_st_fieldset: FieldSet ctd_fieldset: FieldSet drifter_fieldset: FieldSet - argo_float_deploy_locations: list[Location] - drifter_deploy_locations: list[Location] - ctd_deploy_locations: list[Location] - argo_float_config: ArgoFloatConfig adcp_config: ADCPConfig @@ -54,79 +52,13 @@ def verify(self) -> None: :raises ValueError: If not valid. """ - if len(self.route_coordinates) < 2: - raise ValueError("Route needs to consist of at least locations.") - - if not all( - [self._is_valid_location(coord) for coord in self.route_coordinates] - ): - raise ValueError("Invalid coordinates in route.") - - if not all( - [ - self._is_valid_location(coord) - for coord in self.argo_float_deploy_locations - ] - ): - raise ValueError("Argo float deploy locations are not valid coordinates.") - - if not all( - [ - any( - [ - np.isclose(deploy.lat, coord.lat) - and np.isclose(deploy.lon, coord.lon) - for coord in self.route_coordinates - ] - ) - for deploy in self.argo_float_deploy_locations - ] - ): - raise ValueError( - "Argo float deploy locations are not exactly on route coordinates." - ) - - if not all( - [self._is_valid_location(coord) for coord in self.drifter_deploy_locations] - ): - raise ValueError("Drifter deploy locations are not valid coordinates.") - - if not all( - [ - any( - [ - np.isclose(deploy.lat, coord.lat) - and np.isclose(deploy.lon, coord.lon) - for coord in self.route_coordinates - ] - ) - for deploy in self.drifter_deploy_locations - ] - ): - raise ValueError( - "Drifter deploy locations are not exactly on route coordinates." - ) - - if not all( - [self._is_valid_location(coord) for coord in self.ctd_deploy_locations] - ): - raise ValueError("CTD deploy locations are not valid coordinates.") + if len(self.waypoints) < 2: + raise ValueError("Waypoints require at least a start and an end.") if not all( - [ - any( - [ - np.isclose(deploy.lat, coord.lat) - and np.isclose(deploy.lon, coord.lon) - for coord in self.route_coordinates - ] - ) - for deploy in self.ctd_deploy_locations - ] + [self._is_valid_location(waypoint.location) for waypoint in self.waypoints] ): - raise ValueError( - "CTD deploy locations are not exactly on route coordinates." - ) + raise ValueError("Invalid location for waypoint.") if self.argo_float_config.max_depth > 0: raise ValueError("Argo float max depth must be negative or zero.") diff --git a/virtual_ship/waypoint.py b/virtual_ship/waypoint.py new file mode 100644 index 00000000..225a93cb --- /dev/null +++ b/virtual_ship/waypoint.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from .location import Location +from datetime import datetime +from .instrument_deployment import InstrumentDeployment + + +@dataclass +class Waypoint: + location: Location + time: datetime | None = None + instrument: InstrumentDeployment | None = None From 7ed6f0ccd677271cfcea3cda16a55707ddabd489 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 14:52:03 +0200 Subject: [PATCH 02/29] fix crash --- virtual_ship/instruments/argo_float.py | 7 +++++++ virtual_ship/instruments/ctd.py | 7 +++++++ virtual_ship/instruments/drifter.py | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/virtual_ship/instruments/argo_float.py b/virtual_ship/instruments/argo_float.py index 031c8309..3de9858f 100644 --- a/virtual_ship/instruments/argo_float.py +++ b/virtual_ship/instruments/argo_float.py @@ -133,6 +133,13 @@ def simulate_argo_floats( """ DT = 10.0 # dt of Argo float simulation integrator + if len(argo_floats) == 0: + print( + "No Argo floats provided. Parcels currently crashes when providing an empty particle set, so no argo floats simulation will be done and no files will be created." + ) + # TODO when parcels supports it this check can be removed. + return + # define parcel particles argo_float_particleset = ParticleSet( fieldset=fieldset, diff --git a/virtual_ship/instruments/ctd.py b/virtual_ship/instruments/ctd.py index cc8734c9..94487db4 100644 --- a/virtual_ship/instruments/ctd.py +++ b/virtual_ship/instruments/ctd.py @@ -71,6 +71,13 @@ def simulate_ctd( WINCH_SPEED = 1.0 # sink and rise speed in m/s DT = 10.0 # dt of CTD simulation integrator + if len(ctds) == 0: + print( + "No CTDs provided. Parcels currently crashes when providing an empty particle set, so no CTD simulation will be done and no files will be created." + ) + # TODO when parcels supports it this check can be removed. + return + fieldset_starttime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[0]) fieldset_endtime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[-1]) diff --git a/virtual_ship/instruments/drifter.py b/virtual_ship/instruments/drifter.py index 3a5be7ab..aa163380 100644 --- a/virtual_ship/instruments/drifter.py +++ b/virtual_ship/instruments/drifter.py @@ -58,6 +58,13 @@ def simulate_drifters( :param dt: Dt for integration. :param endtime: Stop at this time, or if None, continue until the end of the fieldset or until all drifters ended. If this is earlier than the last drifter ended or later than the end of the fieldset, a warning will be printed. """ + if len(drifters) == 0: + print( + "No drifters provided. Parcels currently crashes when providing an empty particle set, so no drifter simulation will be done and no files will be created." + ) + # TODO when parcels supports it this check can be removed. + return + # define parcel particles drifter_particleset = ParticleSet( fieldset=fieldset, From c86d61e868135498c9096bf5c88733387daa3a66 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 15:55:33 +0200 Subject: [PATCH 03/29] works except for ongoing measurements --- tests/test_sailship.py | 14 +- virtual_ship/__init__.py | 4 +- virtual_ship/instrument_deployment.py | 2 - virtual_ship/instrument_type.py | 7 + virtual_ship/sailship.py | 241 +++++++------------------- virtual_ship/waypoint.py | 4 +- 6 files changed, 82 insertions(+), 190 deletions(-) delete mode 100644 virtual_ship/instrument_deployment.py create mode 100644 virtual_ship/instrument_type.py diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 5b220975..62b14e7f 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -5,7 +5,7 @@ import numpy as np from parcels import Field, FieldSet -from virtual_ship import Location, Waypoint, InstrumentDeployment +from virtual_ship import Location, Waypoint, InstrumentType from virtual_ship.sailship import sailship from virtual_ship.virtual_ship_configuration import ( ADCPConfig, @@ -96,9 +96,17 @@ def test_sailship() -> None: waypoints = [ Waypoint( location=Location(latitude=-23.071289, longitude=63.743631), - time=datetime.datetime.strptime("2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"), + time=base_time, + ), + Waypoint( + location=Location(latitude=-23.081289, longitude=63.743631), + instrument=InstrumentType.CTD, + ), + Waypoint( + location=Location(latitude=-23.181289, longitude=63.743631), + time=base_time + datetime.timedelta(hours=1), + instrument=InstrumentType.CTD, ), - Waypoint(location=Location(latitude=-23.081289, longitude=63.743631)), ] config = VirtualShipConfig( diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index 1db24aec..affddf7c 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -4,13 +4,13 @@ from .location import Location from .spacetime import Spacetime from .waypoint import Waypoint -from .instrument_deployment import InstrumentDeployment +from .instrument_type import InstrumentType __all__ = [ "Location", "Spacetime", "instruments", "sailship", - "InstrumentDeployment", + "InstrumentType", "Waypoint", ] diff --git a/virtual_ship/instrument_deployment.py b/virtual_ship/instrument_deployment.py deleted file mode 100644 index a055362e..00000000 --- a/virtual_ship/instrument_deployment.py +++ /dev/null @@ -1,2 +0,0 @@ -class InstrumentDeployment: - pass diff --git a/virtual_ship/instrument_type.py b/virtual_ship/instrument_type.py new file mode 100644 index 00000000..ee16f32d --- /dev/null +++ b/virtual_ship/instrument_type.py @@ -0,0 +1,7 @@ +from enum import Enum, auto + + +class InstrumentType(Enum): + CTD = auto() + DRIFTER = auto() + ARGO_FLOAT = auto() diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 7c723352..18ffac5f 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -17,6 +17,7 @@ from .spacetime import Spacetime from .virtual_ship_configuration import VirtualShipConfig from .waypoint import Waypoint +from .instrument_type import InstrumentType def sailship(config: VirtualShipConfig): @@ -27,7 +28,7 @@ def sailship(config: VirtualShipConfig): """ config.verify() - verify_waypoints(config.waypoints) + verify_waypoints(config.waypoints, config.ship_speed) # lists of instrument deployments to be simulated adcps: list[Spacetime] = [] @@ -43,8 +44,43 @@ def sailship(config: VirtualShipConfig): assert time is not None # cannot happen as verify_waypoints checks this for wp, wp_next in zip(config.waypoints, config.waypoints[1:] + [None]): - if wp.instrument is not None: - pass # TODO + spacetime = Spacetime(location=wp.location, time=time) + + match wp.instrument: + case None: + pass + case InstrumentType.CTD: + ctds.append( + CTD( + spacetime=spacetime, + min_depth=config.ctd_fieldset.U.depth[0], + max_depth=config.ctd_fieldset.U.depth[-1], + ) + ) + # use 20 minutes to case the CTD + time += timedelta(minutes=20) + case InstrumentType.DRIFTER: + drifters.append( + Drifter( + spacetime=spacetime, + depth=-config.drifter_fieldset.U.depth[0], + lifetime=timedelta(weeks=4), + ) + ) + case InstrumentType.ARGO_FLOAT: + argo_floats.append( + ArgoFloat( + spacetime=spacetime, + min_depth=-config.argo_float_config.fieldset.U.depth[0], + max_depth=config.argo_float_config.max_depth, + drift_depth=config.argo_float_config.drift_depth, + vertical_speed=config.argo_float_config.vertical_speed, + cycle_days=config.argo_float_config.cycle_days, + drift_days=config.argo_float_config.drift_days, + ) + ) + case _: + raise NotImplementedError() if wp_next is None: break @@ -119,113 +155,9 @@ def sailship(config: VirtualShipConfig): # if i % 96 == 0: # print(f"Gathered data {time_past} hours since start.") - # # find drifter deployments to be done at this location - # drifters_here = set( - # [ - # drifter - # for drifter in drifter_locations - drifter_locations_visited - # if all( - # np.isclose( - # [drifter.lat, drifter.lon], [route_point.lat, route_point.lon] - # ) - # ) - # ] - # ) - # if len(drifters_here) > 1: - # print( - # "WARN: Multiple drifter deployments match the current location. Only a single deployment will be performed." - # ) - # drifters.append( - # Drifter( - # spacetime=Spacetime( - # location=route_point, time=time_past.total_seconds() - # ), - # depth=-config.drifter_fieldset.U.depth[0], - # lifetime=timedelta(weeks=4), - # ) - # ) - # drifter_locations_visited = drifter_locations_visited.union(drifters_here) - - # # find argo float deployments to be done at this location - # argos_here = set( - # [ - # argo - # for argo in argo_locations - argo_locations_visited - # if all( - # np.isclose([argo.lat, argo.lon], [route_point.lat, route_point.lon]) - # ) - # ] - # ) - # if len(argos_here) > 1: - # print( - # "WARN: Multiple argo float deployments match the current location. Only a single deployment will be performed." - # ) - # argo_floats.append( - # ArgoFloat( - # spacetime=Spacetime( - # location=route_point, time=time_past.total_seconds() - # ), - # min_depth=-config.argo_float_config.fieldset.U.depth[0], - # max_depth=config.argo_float_config.max_depth, - # drift_depth=config.argo_float_config.drift_depth, - # vertical_speed=config.argo_float_config.vertical_speed, - # cycle_days=config.argo_float_config.cycle_days, - # drift_days=config.argo_float_config.drift_days, - # ) - # ) - # argo_locations_visited = argo_locations_visited.union(argos_here) - - # # find CTD casts to be done at this location - # ctds_here = set( - # [ - # ctd - # for ctd in ctd_locations - ctd_locations_visited - # if all( - # np.isclose([ctd.lat, ctd.lon], [route_point.lat, route_point.lon]) - # ) - # ] - # ) - # if len(ctds_here) > 1: - # print( - # "WARN: Multiple CTD casts match the current location. Only a single cast will be performed." - # ) - - # ctds.append( - # CTD( - # spacetime=Spacetime( - # location=route_point, - # time=config.start_time + time_past, - # ), - # min_depth=config.ctd_fieldset.U.depth[0], - # max_depth=config.ctd_fieldset.U.depth[-1], - # ) - # ) - # ctd_locations_visited = ctd_locations_visited.union(ctds_here) - # # add 20 minutes to sailing time for deployment - # if len(ctds_here) != 0: - # time_past += timedelta(minutes=20) - - # # add time it takes to move to the next route point - # time_past += route_dt # # remove the last one, because no sailing to the next point was needed # time_past -= route_dt - # # check if all drifter, argo float, ctd locations were visited - # if len(drifter_locations_visited) != len(drifter_locations): - # print( - # "WARN: some drifter deployments were not planned along the route and have not been performed." - # ) - - # if len(argo_locations_visited) != len(argo_locations): - # print( - # "WARN: some argo float deployments were not planned along the route and have not been performed." - # ) - - # if len(ctd_locations_visited) != len(ctd_locations): - # print( - # "WARN: some CTD casts were not planned along the route and have not been performed." - # ) - # print("Simulating onboard salinity and temperature measurements.") # simulate_ship_underwater_st( # fieldset=config.ship_underwater_st_fieldset, @@ -244,33 +176,6 @@ def sailship(config: VirtualShipConfig): # sample_points=adcps, # ) - # print("Simulating CTD casts.") - # simulate_ctd( - # out_path=os.path.join("results", "ctd.zarr"), - # fieldset=config.ctd_fieldset, - # ctds=ctds, - # outputdt=timedelta(seconds=10), - # ) - - # print("Simulating drifters") - # simulate_drifters( - # out_path=os.path.join("results", "drifters.zarr"), - # fieldset=config.drifter_fieldset, - # drifters=drifters, - # outputdt=timedelta(hours=5), - # dt=timedelta(minutes=5), - # endtime=None, - # ) - - # print("Simulating argo floats") - # simulate_argo_floats( - # out_path=os.path.join("results", "argo_floats.zarr"), - # argo_floats=argo_floats, - # fieldset=config.argo_float_config.fieldset, - # outputdt=timedelta(minutes=5), - # endtime=None, - # ) - # # convert CTD data to CSV # print("Postprocessing..") # postprocess() @@ -281,7 +186,7 @@ def sailship(config: VirtualShipConfig): # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") -def verify_waypoints(waypoints: list[Waypoint]) -> None: +def verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: # check first waypoint has a time if waypoints[0].time is None: raise ValueError("First waypoint must have a specified time.") @@ -296,55 +201,29 @@ def verify_waypoints(waypoints: list[Waypoint]) -> None: ): raise ValueError("Each waypoint should be timed after all previous waypoints") - # TODO more - - -def shiproute(config: VirtualShipConfig, dt: timedelta) -> list[Location]: - """ - Take in route coordinates and return lat and lon points within region of interest to sample. - - :param config: The cruise configuration. - :param dt: Sailing time between each discrete route point. - :returns: lat and lon points within region of interest to sample. - """ - CRUISE_SPEED = 5.14 - - # discrete points the ship will pass - sample_points: list[Location] = [] - - # projection used to get discrete locations + # projection used to sail between waypoints geod = pyproj.Geod(ellps="WGS84") - # loop over station coordinates and calculate intermediate points along great circle path - for startloc, endloc in zip(config.route_coordinates, config.route_coordinates[1:]): - # iterate over each coordinate and the next coordinate - # last coordinate has no next coordinate and is skipped - - # get locations between start and end, seperate by 5 minutes of cruising - # excludes final point, but this is added explicitly after this loop - int_points = geod.inv_intermediate( - startloc.lon, - startloc.lat, - endloc.lon, - endloc.lat, - del_s=CRUISE_SPEED * dt.total_seconds(), - initial_idx=0, - return_back_azimuth=False, - ) + # check that ship will arrive on time at each waypoint (in case nothing goes wrong) + time = waypoints[0].time + for wp, wp_next in zip(waypoints, waypoints[1:]): + match wp.instrument: + case InstrumentType.CTD: + time += timedelta(minutes=20) - sample_points.extend( - [ - Location(latitude=lat, longitude=lon) - for lat, lon in zip(int_points.lats, int_points.lons, strict=True) - ] + geodinv: tuple[float, float, float] = geod.inv( + wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat ) + distance = geodinv[2] - # explitly include final point which is not added by the previous loop - sample_points.append( - Location( - latitude=config.route_coordinates[-1].lat, - longitude=config.route_coordinates[-1].lon, - ) - ) + time_to_reach = timedelta(seconds=distance / ship_speed) + arrival_time = time + time_to_reach - return sample_points + if wp_next.time is None: + time = arrival_time + elif arrival_time > wp_next.time: + raise RuntimeError( + "Waypoint planning is not valid: would arrive too late a waypoint." + ) + else: + time = wp_next.time diff --git a/virtual_ship/waypoint.py b/virtual_ship/waypoint.py index 225a93cb..1377fec9 100644 --- a/virtual_ship/waypoint.py +++ b/virtual_ship/waypoint.py @@ -1,11 +1,11 @@ from dataclasses import dataclass from .location import Location from datetime import datetime -from .instrument_deployment import InstrumentDeployment +from .instrument_type import InstrumentType @dataclass class Waypoint: location: Location time: datetime | None = None - instrument: InstrumentDeployment | None = None + instrument: InstrumentType | None = None From 1db01b53fb1ba18a7f0853c66a598cf1fb959460 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 16:13:55 +0200 Subject: [PATCH 04/29] codetools fixes --- tests/parcelscrash.py | 59 ------------------ tests/test_sailship.py | 4 +- virtual_ship/__init__.py | 8 ++- virtual_ship/costs.py | 2 +- virtual_ship/instrument_type.py | 4 ++ virtual_ship/planning_error.py | 7 +++ virtual_ship/sailship.py | 61 ++++++------------- ...onfiguration.py => virtual_ship_config.py} | 2 - virtual_ship/waypoint.py | 7 ++- 9 files changed, 43 insertions(+), 111 deletions(-) delete mode 100644 tests/parcelscrash.py create mode 100644 virtual_ship/planning_error.py rename virtual_ship/{virtual_ship_configuration.py => virtual_ship_config.py} (97%) diff --git a/tests/parcelscrash.py b/tests/parcelscrash.py deleted file mode 100644 index f39fef13..00000000 --- a/tests/parcelscrash.py +++ /dev/null @@ -1,59 +0,0 @@ -import numpy as np -from parcels import FieldSet, JITParticle, ParticleSet, Variable - -from datetime import datetime - - -Particle = JITParticle.add_variables( - [ - Variable("temperature", dtype=np.float32, initial=np.nan), - ] -) - - -def sample_temperature(particle, fieldset, time): - particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] - - -def simulate_ctd() -> None: - base_time = datetime.strptime("1950-01-01", "%Y-%m-%d") - - fieldset = FieldSet.from_data( - { - "V": np.zeros((2, 2, 2, 2)), - "U": np.zeros((2, 2, 2, 2)), - "T": np.zeros((2, 2, 2, 2)), - }, - { - "time": [ - np.datetime64(base_time + datetime.timedelta(hours=0)), - np.datetime64(base_time + datetime.timedelta(hours=1)), - ], - "depth": [-1000, 0], - "lat": [0, 1], - "lon": [0, 1], - }, - ) - - fieldset_endtime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[-1]) - - particleset = ParticleSet( - fieldset=fieldset, - pclass=Particle, - lon=[], - lat=[], - depth=[], - time=[], - ) - - # define output file for the simulation - out_file = particleset.ParticleFile(name="test") - - # execute simulation - particleset.execute( - [sample_temperature], - endtime=fieldset_endtime, - dt=5, - verbose_progress=False, - output_file=out_file, - ) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 62b14e7f..2eb98b1b 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -5,9 +5,9 @@ import numpy as np from parcels import Field, FieldSet -from virtual_ship import Location, Waypoint, InstrumentType +from virtual_ship import InstrumentType, Location, Waypoint from virtual_ship.sailship import sailship -from virtual_ship.virtual_ship_configuration import ( +from virtual_ship.virtual_ship_config import ( ADCPConfig, ArgoFloatConfig, VirtualShipConfig, diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index affddf7c..31a64875 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -1,16 +1,18 @@ """Code for the Virtual Ship Classroom, where Marine Scientists can combine Copernicus Marine Data with an OceanParcels ship to go on a virtual expedition.""" from . import instruments, sailship +from .instrument_type import InstrumentType from .location import Location +from .planning_error import PlanningError from .spacetime import Spacetime from .waypoint import Waypoint -from .instrument_type import InstrumentType __all__ = [ + "InstrumentType", "Location", + "PlanningError", "Spacetime", + "Waypoint", "instruments", "sailship", - "InstrumentType", - "Waypoint", ] diff --git a/virtual_ship/costs.py b/virtual_ship/costs.py index 459dba67..c2473cb0 100644 --- a/virtual_ship/costs.py +++ b/virtual_ship/costs.py @@ -2,7 +2,7 @@ from datetime import timedelta -from .virtual_ship_configuration import VirtualShipConfig +from .virtual_ship_config import VirtualShipConfig def costs(config: VirtualShipConfig, total_time: timedelta): diff --git a/virtual_ship/instrument_type.py b/virtual_ship/instrument_type.py index ee16f32d..9a19b814 100644 --- a/virtual_ship/instrument_type.py +++ b/virtual_ship/instrument_type.py @@ -1,7 +1,11 @@ +"""InstrumentType Enum.""" + from enum import Enum, auto class InstrumentType(Enum): + """Types of instruments.""" + CTD = auto() DRIFTER = auto() ARGO_FLOAT = auto() diff --git a/virtual_ship/planning_error.py b/virtual_ship/planning_error.py new file mode 100644 index 00000000..5d599dd2 --- /dev/null +++ b/virtual_ship/planning_error.py @@ -0,0 +1,7 @@ +"""PlanningError Exception.""" + + +class PlanningError(RuntimeError): + """An error when checking the schedule or during sailing.""" + + pass diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 18ffac5f..04cc8662 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -3,21 +3,18 @@ import os from datetime import timedelta -import numpy as np import pyproj -from .costs import costs +from .instrument_type import InstrumentType from .instruments.adcp import simulate_adcp from .instruments.argo_float import ArgoFloat, simulate_argo_floats from .instruments.ctd import CTD, simulate_ctd from .instruments.drifter import Drifter, simulate_drifters from .instruments.ship_underwater_st import simulate_ship_underwater_st -from .location import Location -from .postprocess import postprocess +from .planning_error import PlanningError from .spacetime import Spacetime -from .virtual_ship_configuration import VirtualShipConfig +from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint -from .instrument_type import InstrumentType def sailship(config: VirtualShipConfig): @@ -25,10 +22,12 @@ def sailship(config: VirtualShipConfig): Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. :param config: The cruise configuration. + :raises NotImplementedError: In case an instrument is not supported. + :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. """ config.verify() - verify_waypoints(config.waypoints, config.ship_speed) + _verify_waypoints(config.waypoints, config.ship_speed) # lists of instrument deployments to be simulated adcps: list[Spacetime] = [] @@ -40,8 +39,10 @@ def sailship(config: VirtualShipConfig): # projection used to sail between waypoints geod = pyproj.Geod(ellps="WGS84") + assert ( + config.waypoints[0].time is not None + ) # cannot happen as verify_waypoints checks this time = config.waypoints[0].time - assert time is not None # cannot happen as verify_waypoints checks this for wp, wp_next in zip(config.waypoints, config.waypoints[1:] + [None]): spacetime = Spacetime(location=wp.location, time=time) @@ -96,7 +97,7 @@ def sailship(config: VirtualShipConfig): if wp_next.time is None: time = arrival_time elif arrival_time > wp_next.time: - raise RuntimeError( + raise PlanningError( "Arrived too late at the next waypoint. This error should never happen because the schedule should have been verified beforehand in this function." ) else: @@ -147,49 +148,21 @@ def sailship(config: VirtualShipConfig): endtime=None, ) - # # iterate over each discrete route point, find deployment and measurement locations and times, and measure how much time this took - # # TODO between drifters, argo floats, ctd there is quite some repetition - # print("Traversing ship route.") - # time_past = timedelta() - # for i, route_point in enumerate(route_points): - # if i % 96 == 0: - # print(f"Gathered data {time_past} hours since start.") - - # # remove the last one, because no sailing to the next point was needed - # time_past -= route_dt - - # print("Simulating onboard salinity and temperature measurements.") - # simulate_ship_underwater_st( - # fieldset=config.ship_underwater_st_fieldset, - # out_path=os.path.join("results", "ship_underwater_st.zarr"), - # depth=-2, - # sample_points=ship_underwater_sts, - # ) - - # print("Simulating onboard ADCP.") - # simulate_adcp( - # fieldset=config.adcp_fieldset, - # out_path=os.path.join("results", "adcp.zarr"), - # max_depth=config.adcp_config.max_depth, - # min_depth=-5, - # num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, - # sample_points=adcps, - # ) - - # # convert CTD data to CSV + # convert CTD data to CSV # print("Postprocessing..") # postprocess() # print("All data has been gathered and postprocessed, returning home.") + # time_past = time - config.waypoints[0].time # cost = costs(config, time_past) # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") -def verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: +def _verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: # check first waypoint has a time if waypoints[0].time is None: - raise ValueError("First waypoint must have a specified time.") + raise PlanningError("First waypoint must have a specified time.") # check waypoint times are in ascending order timed_waypoints = [wp for wp in waypoints if wp.time is not None] @@ -199,7 +172,9 @@ def verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: for cur, next in zip(timed_waypoints, timed_waypoints[1:]) ] ): - raise ValueError("Each waypoint should be timed after all previous waypoints") + raise PlanningError( + "Each waypoint should be timed after all previous waypoints" + ) # projection used to sail between waypoints geod = pyproj.Geod(ellps="WGS84") @@ -222,7 +197,7 @@ def verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: if wp_next.time is None: time = arrival_time elif arrival_time > wp_next.time: - raise RuntimeError( + raise PlanningError( "Waypoint planning is not valid: would arrive too late a waypoint." ) else: diff --git a/virtual_ship/virtual_ship_configuration.py b/virtual_ship/virtual_ship_config.py similarity index 97% rename from virtual_ship/virtual_ship_configuration.py rename to virtual_ship/virtual_ship_config.py index e082641d..83288086 100644 --- a/virtual_ship/virtual_ship_configuration.py +++ b/virtual_ship/virtual_ship_config.py @@ -1,9 +1,7 @@ """VirtualShipConfig class.""" from dataclasses import dataclass -from datetime import datetime -import numpy as np from parcels import FieldSet from .location import Location diff --git a/virtual_ship/waypoint.py b/virtual_ship/waypoint.py index 1377fec9..6094abd9 100644 --- a/virtual_ship/waypoint.py +++ b/virtual_ship/waypoint.py @@ -1,11 +1,16 @@ +"""Waypoint class.""" + from dataclasses import dataclass -from .location import Location from datetime import datetime + from .instrument_type import InstrumentType +from .location import Location @dataclass class Waypoint: + """A Waypoint to sail to.""" + location: Location time: datetime | None = None instrument: InstrumentType | None = None From 8c42c799af03dd071ff9a3cbedbbb6c655c4b171 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 16:17:45 +0200 Subject: [PATCH 05/29] remove duplication --- virtual_ship/sailship.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 04cc8662..b91dcd4b 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -27,7 +27,10 @@ def sailship(config: VirtualShipConfig): """ config.verify() - _verify_waypoints(config.waypoints, config.ship_speed) + # projection used to sail between waypoints + projection = pyproj.Geod(ellps="WGS84") + + _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) # lists of instrument deployments to be simulated adcps: list[Spacetime] = [] @@ -36,9 +39,6 @@ def sailship(config: VirtualShipConfig): drifters: list[Drifter] = [] ctds: list[CTD] = [] - # projection used to sail between waypoints - geod = pyproj.Geod(ellps="WGS84") - assert ( config.waypoints[0].time is not None ) # cannot happen as verify_waypoints checks this @@ -86,10 +86,10 @@ def sailship(config: VirtualShipConfig): if wp_next is None: break - geodinv: tuple[float, float, float] = geod.inv( + projection_inv: tuple[float, float, float] = projection.inv( wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat ) - distance = geodinv[2] + distance = projection_inv[2] time_to_reach = timedelta(seconds=distance / config.ship_speed) arrival_time = time + time_to_reach @@ -159,7 +159,9 @@ def sailship(config: VirtualShipConfig): # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") -def _verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: +def _verify_waypoints( + waypoints: list[Waypoint], ship_speed: float, projection: pyproj.Geod +) -> None: # check first waypoint has a time if waypoints[0].time is None: raise PlanningError("First waypoint must have a specified time.") @@ -176,9 +178,6 @@ def _verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: "Each waypoint should be timed after all previous waypoints" ) - # projection used to sail between waypoints - geod = pyproj.Geod(ellps="WGS84") - # check that ship will arrive on time at each waypoint (in case nothing goes wrong) time = waypoints[0].time for wp, wp_next in zip(waypoints, waypoints[1:]): @@ -186,7 +185,7 @@ def _verify_waypoints(waypoints: list[Waypoint], ship_speed: float) -> None: case InstrumentType.CTD: time += timedelta(minutes=20) - geodinv: tuple[float, float, float] = geod.inv( + geodinv: tuple[float, float, float] = projection.inv( wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat ) distance = geodinv[2] From c6851bca5617a561927019272d5783f6a7caac87 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 30 Jul 2024 16:29:22 +0200 Subject: [PATCH 06/29] add small sanity check --- virtual_ship/sailship.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index b91dcd4b..e4dbecea 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -44,7 +44,9 @@ def sailship(config: VirtualShipConfig): ) # cannot happen as verify_waypoints checks this time = config.waypoints[0].time - for wp, wp_next in zip(config.waypoints, config.waypoints[1:] + [None]): + for wp, wp_next in zip( + config.waypoints, config.waypoints[1:] + [None], strict=True + ): spacetime = Spacetime(location=wp.location, time=time) match wp.instrument: From 4f1c114b4b06304fdf690636b8f85ba712f4d0f8 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Sun, 4 Aug 2024 17:44:35 +0200 Subject: [PATCH 07/29] magic it actually works --- tests/test_sailship.py | 8 +- virtual_ship/sailship.py | 340 ++++++++++++++++++++++------ virtual_ship/sorted_queue.py | 26 +++ virtual_ship/virtual_ship_config.py | 6 + 4 files changed, 307 insertions(+), 73 deletions(-) create mode 100644 virtual_ship/sorted_queue.py diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 2eb98b1b..55afbfe5 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -12,6 +12,7 @@ ArgoFloatConfig, VirtualShipConfig, ) +from datetime import timedelta def _make_ctd_fieldset(base_time: datetime) -> FieldSet: @@ -65,7 +66,7 @@ def test_sailship() -> None: ) ship_underwater_st_fieldset = FieldSet.from_data( - {"U": 0, "V": 0, "S": 0, "T": 0}, + {"U": 0, "V": 0, "salinity": 0, "temperature": 0}, {"lon": 0, "lat": 0}, ) @@ -74,7 +75,7 @@ def test_sailship() -> None: drifter_fieldset = _make_drifter_fieldset(base_time) argo_float_fieldset = FieldSet.from_data( - {"U": 0, "V": 0, "T": 0, "S": 0}, + {"U": 0, "V": 0, "T": 0, "z": 0}, { "lon": 0, "lat": 0, @@ -118,6 +119,9 @@ def test_sailship() -> None: drifter_fieldset=drifter_fieldset, argo_float_config=argo_float_config, adcp_config=adcp_config, + ship_underwater_st_period=timedelta(minutes=5), + adcp_period=timedelta(minutes=5), + ctd_stationkeeping_time=timedelta(minutes=20), ) sailship(config) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index e4dbecea..952e1504 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -1,5 +1,6 @@ """sailship function.""" +from __future__ import annotations import os from datetime import timedelta @@ -15,102 +16,299 @@ from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint +from datetime import datetime, timedelta +from dataclasses import dataclass, field +from typing import Generator, Callable +from collections import deque +from .location import Location +from contextlib import contextmanager +from .sorted_queue import SortedQueue -def sailship(config: VirtualShipConfig): - """ - Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. +class Cruise: + _finished: bool + _sail_lock_count: int + spacetime: Spacetime - :param config: The cruise configuration. - :raises NotImplementedError: In case an instrument is not supported. - :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. - """ - config.verify() + def __init__(self, spacetime: Spacetime) -> None: + self._finished = False + self._sail_lock_count = 0 + self.spacetime = spacetime - # projection used to sail between waypoints - projection = pyproj.Geod(ellps="WGS84") + @property + def finished(self) -> bool: + return self._finished - _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) + @contextmanager + def do_not_sail(self) -> Generator[None, None, None]: + try: + self._sail_lock_count += 1 + yield + finally: + self._sail_lock_count -= 1 - # lists of instrument deployments to be simulated - adcps: list[Spacetime] = [] - ship_underwater_sts: list[Spacetime] = [] - argo_floats: list[ArgoFloat] = [] - drifters: list[Drifter] = [] - ctds: list[CTD] = [] + def finish(self) -> None: + self._finished = True - assert ( - config.waypoints[0].time is not None - ) # cannot happen as verify_waypoints checks this - time = config.waypoints[0].time - for wp, wp_next in zip( - config.waypoints, config.waypoints[1:] + [None], strict=True - ): - spacetime = Spacetime(location=wp.location, time=time) +@dataclass +class ScheduleResults: + adcps: list[Spacetime] = field(default_factory=list, init=False) + ship_underwater_sts: list[Spacetime] = field(default_factory=list, init=False) + argo_floats: list[ArgoFloat] = field(default_factory=list, init=False) + drifters: list[Drifter] = field(default_factory=list, init=False) + ctds: list[CTD] = field(default_factory=list, init=False) - match wp.instrument: - case None: - pass - case InstrumentType.CTD: - ctds.append( - CTD( - spacetime=spacetime, - min_depth=config.ctd_fieldset.U.depth[0], - max_depth=config.ctd_fieldset.U.depth[-1], + +@dataclass +class WaitFor: + time: timedelta + + +TaskGenerator = Generator[WaitFor, None, None] +Task = Callable[[Cruise, ScheduleResults], TaskGenerator] + + +class WaitingTask: + _task: TaskGenerator + _wait_until: datetime + + def __init__(self, task: TaskGenerator, wait_until: datetime) -> None: + self._task = task + self._wait_until = wait_until + + def __lt__(self, other: WaitingTask): + return self._wait_until < other._wait_until + + @property + def task(self) -> TaskGenerator: + return self._task + + @property + def wait_until(self) -> datetime: + return self._wait_until + + +def simulate_schedule( + tasks: list[Task], + waypoints: list[Waypoint], + projection: pyproj.Geod, + ship_speed: float, + ctd_stationkeeping_time: timedelta, + ctd_min_depth: float, + ctd_max_depth: float, +) -> ScheduleResults: + # TODO verify waypoint reached in time + + cruise = Cruise(Spacetime(waypoints[0].location, waypoints[0].time)) + results = ScheduleResults() + + waiting_tasks = SortedQueue[WaitingTask]() + for task in tasks: + waiting_tasks.push( + WaitingTask(task=task(cruise, results), wait_until=cruise.spacetime.time) + ) + + for waypoint in waypoints: + match waypoint.instrument: + case InstrumentType.ARGO_FLOAT: + waiting_tasks.push( + WaitingTask( + ArgoFloatTask()(cruise, results), + wait_until=cruise.spacetime.time, ) ) - # use 20 minutes to case the CTD - time += timedelta(minutes=20) case InstrumentType.DRIFTER: - drifters.append( - Drifter( - spacetime=spacetime, - depth=-config.drifter_fieldset.U.depth[0], - lifetime=timedelta(weeks=4), + waiting_tasks.push( + WaitingTask( + DrifterTask()(cruise, results), wait_until=cruise.spacetime.time ) ) - case InstrumentType.ARGO_FLOAT: - argo_floats.append( - ArgoFloat( - spacetime=spacetime, - min_depth=-config.argo_float_config.fieldset.U.depth[0], - max_depth=config.argo_float_config.max_depth, - drift_depth=config.argo_float_config.drift_depth, - vertical_speed=config.argo_float_config.vertical_speed, - cycle_days=config.argo_float_config.cycle_days, - drift_days=config.argo_float_config.drift_days, + case InstrumentType.CTD: + waiting_tasks.push( + WaitingTask( + CTDTask(ctd_stationkeeping_time, ctd_min_depth, ctd_max_depth)( + cruise, results + ), + cruise.spacetime.time, ) ) + case None: + pass case _: raise NotImplementedError() - if wp_next is None: - break + waypoint_reached = False + while not waypoint_reached: + # execute all tasks planned for current time + while ( + not waiting_tasks.is_empty() + and waiting_tasks.peek().wait_until <= cruise.spacetime.time + ): + task = waiting_tasks.pop() + try: + wait_for = next(task.task) + waiting_tasks.push( + WaitingTask(task.task, cruise.spacetime.time + wait_for.time) + ) + except StopIteration: + pass - projection_inv: tuple[float, float, float] = projection.inv( - wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat - ) - distance = projection_inv[2] + # get time of next waiting task + next_task_at = waiting_tasks.peek().wait_until - time_to_reach = timedelta(seconds=distance / config.ship_speed) - arrival_time = time + time_to_reach + # calculate time at which waypoint would be reached if simply sailing + geodinv: tuple[float, float, float] = projection.inv( + lons1=cruise.spacetime.location.lon, + lats1=cruise.spacetime.location.lat, + lons2=waypoint.location.lon, + lats2=waypoint.location.lat, + ) + azimuth1 = geodinv[0] + distance_to_next_waypoint = geodinv[2] + time_to_reach = timedelta(seconds=distance_to_next_waypoint / ship_speed) + arrival_time = cruise.spacetime.time + time_to_reach - if wp_next.time is None: - time = arrival_time - elif arrival_time > wp_next.time: - raise PlanningError( - "Arrived too late at the next waypoint. This error should never happen because the schedule should have been verified beforehand in this function." + # if waypoint is reached before task starts, sail to the waypoint + if arrival_time <= next_task_at: + cruise.spacetime = Spacetime(waypoint.location, arrival_time) + waypoint_reached = True + # else, sail until task starts + else: + time_to_sail = next_task_at - cruise.spacetime.time + distance_to_move = ship_speed * time_to_sail.total_seconds() + geodfwd: tuple[float, float, float] = projection.fwd( + lons=cruise.spacetime.location.lon, + lats=cruise.spacetime.location.lat, + az=azimuth1, + dist=distance_to_move, + ) + lon = geodfwd[0] + lat = geodfwd[1] + cruise.spacetime = Spacetime( + Location(latitude=lat, longitude=lon), + cruise.spacetime.time + time_to_sail, + ) + + cruise.finish() + + while len(waiting_tasks) > 0: + task = waiting_tasks.pop() + try: + wait_for = next(task.task) + waiting_tasks.push( + WaitingTask(task.task, cruise.spacetime.time + wait_for.time) ) - else: - time = wp_next.time + except StopIteration: + pass + + return results + + +class ShipUnderwaterStLoop: + _sample_period: timedelta + + def __init__(self, sample_period: timedelta) -> None: + self._sample_period = sample_period + + def __call__( + self, cruise: Cruise, schedule_results: ScheduleResults + ) -> Generator[WaitFor, None, None]: + while not cruise.finished: + schedule_results.ship_underwater_sts.append(cruise.spacetime) + yield WaitFor(self._sample_period) + + +class ADCPLoop: + _sample_period: timedelta + + def __init__(self, sample_period: timedelta) -> None: + self._sample_period = sample_period + + def __call__( + self, cruise: Cruise, schedule_results: ScheduleResults + ) -> Generator[WaitFor, None, None]: + while not cruise.finished: + schedule_results.adcps.append(cruise.spacetime) + yield WaitFor(self._sample_period) + + +class CTDTask: + _stationkeeping_time: timedelta + _min_depth: float + _max_depth: float + + def __init__( + self, stationkeeping_time: timedelta, min_depth: float, max_depth: float + ) -> None: + self._stationkeeping_time = stationkeeping_time + self._min_depth = min_depth + self._max_depth = max_depth + + def __call__( + self, cruise: Cruise, schedule_results: ScheduleResults + ) -> Generator[WaitFor, None, None]: + with cruise.do_not_sail(): + schedule_results.ctds.append( + CTD( + spacetime=cruise.spacetime, + min_depth=self._min_depth, + max_depth=self._max_depth, + ) + ) + yield WaitFor(self._stationkeeping_time) + + +class DrifterTask: + def __call__( + self, cruise: Cruise, schedule_results: ScheduleResults + ) -> Generator[WaitFor, None, None]: + # TODO add drifter to drifter list + pass + + +class ArgoFloatTask: + def __call__( + self, cruise: Cruise, schedule_results: ScheduleResults + ) -> Generator[WaitFor, None, None]: + # TODO add argo float to argo float list + pass + + +def sailship(config: VirtualShipConfig): + """ + Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. + + :param config: The cruise configuration. + :raises NotImplementedError: In case an instrument is not supported. + :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. + """ + config.verify() + + # projection used to sail between waypoints + projection = pyproj.Geod(ellps="WGS84") + + _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) + + schedule_results = simulate_schedule( + [ + ShipUnderwaterStLoop(config.ship_underwater_st_period), + ADCPLoop(config.adcp_period), + ], + waypoints=config.waypoints, + projection=projection, + ship_speed=config.ship_speed, + ctd_stationkeeping_time=config.ctd_stationkeeping_time, + ctd_min_depth=config.ctd_fieldset.U.depth[0], + ctd_max_depth=config.ctd_fieldset.U.depth[-1], + ) print("Simulating onboard salinity and temperature measurements.") simulate_ship_underwater_st( fieldset=config.ship_underwater_st_fieldset, out_path=os.path.join("results", "ship_underwater_st.zarr"), depth=-2, - sample_points=ship_underwater_sts, + sample_points=schedule_results.ship_underwater_sts, ) print("Simulating onboard ADCP.") @@ -120,14 +318,14 @@ def sailship(config: VirtualShipConfig): max_depth=config.adcp_config.max_depth, min_depth=-5, num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, - sample_points=adcps, + sample_points=schedule_results.adcps, ) print("Simulating CTD casts.") simulate_ctd( out_path=os.path.join("results", "ctd.zarr"), fieldset=config.ctd_fieldset, - ctds=ctds, + ctds=schedule_results.ctds, outputdt=timedelta(seconds=10), ) @@ -135,7 +333,7 @@ def sailship(config: VirtualShipConfig): simulate_drifters( out_path=os.path.join("results", "drifters.zarr"), fieldset=config.drifter_fieldset, - drifters=drifters, + drifters=schedule_results.drifters, outputdt=timedelta(hours=5), dt=timedelta(minutes=5), endtime=None, @@ -144,7 +342,7 @@ def sailship(config: VirtualShipConfig): print("Simulating argo floats") simulate_argo_floats( out_path=os.path.join("results", "argo_floats.zarr"), - argo_floats=argo_floats, + argo_floats=schedule_results.argo_floats, fieldset=config.argo_float_config.fieldset, outputdt=timedelta(minutes=5), endtime=None, diff --git a/virtual_ship/sorted_queue.py b/virtual_ship/sorted_queue.py new file mode 100644 index 00000000..5bd8e0e6 --- /dev/null +++ b/virtual_ship/sorted_queue.py @@ -0,0 +1,26 @@ +import heapq +from typing import TypeVar, Generic + +T = TypeVar("T") + + +class SortedQueue(Generic[T]): + _queue: list[T] + + def __init__(self) -> None: + self._queue: list[T] = [] + + def push(self, item: T) -> None: + heapq.heappush(self._queue, item) + + def pop(self) -> T: + return heapq.heappop(self._queue) + + def peek(self) -> T: + return self._queue[0] + + def is_empty(self) -> bool: + return len(self._queue) == 0 + + def __len__(self) -> int: + return len(self._queue) diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 83288086..40d5cb22 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -6,6 +6,7 @@ from .location import Location from .waypoint import Waypoint +from datetime import timedelta @dataclass @@ -44,6 +45,11 @@ class VirtualShipConfig: argo_float_config: ArgoFloatConfig adcp_config: ADCPConfig + ship_underwater_st_period: timedelta + adcp_period: timedelta + + ctd_stationkeeping_time: timedelta + def verify(self) -> None: """ Verify this configuration is valid. From 396b747d115e48434a02fd7bb700579ebaf182bc Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:23:21 +0200 Subject: [PATCH 08/29] simplify --- virtual_ship/sailship.py | 270 ++++++++++++++++++++------------------- 1 file changed, 139 insertions(+), 131 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 952e1504..49bc0d50 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -25,7 +25,7 @@ from .sorted_queue import SortedQueue -class Cruise: +class _Cruise: _finished: bool _sail_lock_count: int spacetime: Spacetime @@ -50,9 +50,13 @@ def do_not_sail(self) -> Generator[None, None, None]: def finish(self) -> None: self._finished = True + @property + def sail_is_locked(self) -> bool: + return self._sail_lock_count > 0 + @dataclass -class ScheduleResults: +class _ScheduleResults: adcps: list[Spacetime] = field(default_factory=list, init=False) ship_underwater_sts: list[Spacetime] = field(default_factory=list, init=False) argo_floats: list[ArgoFloat] = field(default_factory=list, init=False) @@ -61,27 +65,25 @@ class ScheduleResults: @dataclass -class WaitFor: +class _WaitFor: time: timedelta -TaskGenerator = Generator[WaitFor, None, None] -Task = Callable[[Cruise, ScheduleResults], TaskGenerator] - - -class WaitingTask: - _task: TaskGenerator +class _WaitingTask: + _task: Generator[_WaitFor, None, None] _wait_until: datetime - def __init__(self, task: TaskGenerator, wait_until: datetime) -> None: + def __init__( + self, task: Generator[_WaitFor, None, None], wait_until: datetime + ) -> None: self._task = task self._wait_until = wait_until - def __lt__(self, other: WaitingTask): + def __lt__(self, other: _WaitingTask): return self._wait_until < other._wait_until @property - def task(self) -> TaskGenerator: + def task(self) -> Generator[_WaitFor, None, None]: return self._task @property @@ -89,46 +91,62 @@ def wait_until(self) -> datetime: return self._wait_until -def simulate_schedule( - tasks: list[Task], +def _simulate_schedule( waypoints: list[Waypoint], projection: pyproj.Geod, ship_speed: float, ctd_stationkeeping_time: timedelta, ctd_min_depth: float, ctd_max_depth: float, -) -> ScheduleResults: + config: VirtualShipConfig, +) -> _ScheduleResults: # TODO verify waypoint reached in time - cruise = Cruise(Spacetime(waypoints[0].location, waypoints[0].time)) - results = ScheduleResults() + cruise = _Cruise(Spacetime(waypoints[0].location, waypoints[0].time)) + results = _ScheduleResults() - waiting_tasks = SortedQueue[WaitingTask]() - for task in tasks: - waiting_tasks.push( - WaitingTask(task=task(cruise, results), wait_until=cruise.spacetime.time) + waiting_tasks = SortedQueue[_WaitingTask]() + waiting_tasks.push( + _WaitingTask( + task=_ship_underwater_st_loop( + config.ship_underwater_st_period, cruise, results + ), + wait_until=cruise.spacetime.time, + ) + ) + waiting_tasks.push( + _WaitingTask( + task=_adcp_loop(config.adcp_period, cruise, results), + wait_until=cruise.spacetime.time, ) + ) + # sail to each waypoint while executing tasks for waypoint in waypoints: + # add task to the task queue for the instrument at the current waypoint match waypoint.instrument: case InstrumentType.ARGO_FLOAT: waiting_tasks.push( - WaitingTask( - ArgoFloatTask()(cruise, results), + _WaitingTask( + _argo_float_task(cruise, results), wait_until=cruise.spacetime.time, ) ) case InstrumentType.DRIFTER: waiting_tasks.push( - WaitingTask( - DrifterTask()(cruise, results), wait_until=cruise.spacetime.time + _WaitingTask( + _drifter_Task(cruise, results), wait_until=cruise.spacetime.time ) ) case InstrumentType.CTD: waiting_tasks.push( - WaitingTask( - CTDTask(ctd_stationkeeping_time, ctd_min_depth, ctd_max_depth)( - cruise, results + _WaitingTask( + _ctd_task( + ctd_stationkeeping_time, + ctd_min_depth, + ctd_max_depth, + cruise, + results, ), cruise.spacetime.time, ) @@ -138,6 +156,7 @@ def simulate_schedule( case _: raise NotImplementedError() + # sail to the next waypoint waypoint_reached = False while not waypoint_reached: # execute all tasks planned for current time @@ -149,55 +168,67 @@ def simulate_schedule( try: wait_for = next(task.task) waiting_tasks.push( - WaitingTask(task.task, cruise.spacetime.time + wait_for.time) + _WaitingTask(task.task, cruise.spacetime.time + wait_for.time) ) except StopIteration: pass - # get time of next waiting task - next_task_at = waiting_tasks.peek().wait_until - - # calculate time at which waypoint would be reached if simply sailing - geodinv: tuple[float, float, float] = projection.inv( - lons1=cruise.spacetime.location.lon, - lats1=cruise.spacetime.location.lat, - lons2=waypoint.location.lon, - lats2=waypoint.location.lat, - ) - azimuth1 = geodinv[0] - distance_to_next_waypoint = geodinv[2] - time_to_reach = timedelta(seconds=distance_to_next_waypoint / ship_speed) - arrival_time = cruise.spacetime.time + time_to_reach - - # if waypoint is reached before task starts, sail to the waypoint - if arrival_time <= next_task_at: - cruise.spacetime = Spacetime(waypoint.location, arrival_time) - waypoint_reached = True - # else, sail until task starts + # if sailing is prevented by a current task, just let time pass until the next task + if cruise.sail_is_locked: + cruise.spacetime = Spacetime( + cruise.spacetime.location, waiting_tasks.peek().wait_until + ) + # else, let time pass while sailing else: - time_to_sail = next_task_at - cruise.spacetime.time - distance_to_move = ship_speed * time_to_sail.total_seconds() - geodfwd: tuple[float, float, float] = projection.fwd( - lons=cruise.spacetime.location.lon, - lats=cruise.spacetime.location.lat, - az=azimuth1, - dist=distance_to_move, + # calculate time at which waypoint would be reached if simply sailing + geodinv: tuple[float, float, float] = projection.inv( + lons1=cruise.spacetime.location.lon, + lats1=cruise.spacetime.location.lat, + lons2=waypoint.location.lon, + lats2=waypoint.location.lat, ) - lon = geodfwd[0] - lat = geodfwd[1] - cruise.spacetime = Spacetime( - Location(latitude=lat, longitude=lon), - cruise.spacetime.time + time_to_sail, + azimuth1 = geodinv[0] + distance_to_next_waypoint = geodinv[2] + time_to_reach = timedelta( + seconds=distance_to_next_waypoint / ship_speed ) + arrival_time = cruise.spacetime.time + time_to_reach + + # if waypoint is reached before next task, sail to the waypoint + if ( + waiting_tasks.is_empty() + or arrival_time <= waiting_tasks.peek().wait_until + ): + cruise.spacetime = Spacetime(waypoint.location, arrival_time) + waypoint_reached = True + # else, sail until task starts + else: + time_to_sail = ( + waiting_tasks.peek().wait_until - cruise.spacetime.time + ) + distance_to_move = ship_speed * time_to_sail.total_seconds() + geodfwd: tuple[float, float, float] = projection.fwd( + lons=cruise.spacetime.location.lon, + lats=cruise.spacetime.location.lat, + az=azimuth1, + dist=distance_to_move, + ) + lon = geodfwd[0] + lat = geodfwd[1] + cruise.spacetime = Spacetime( + Location(latitude=lat, longitude=lon), + cruise.spacetime.time + time_to_sail, + ) cruise.finish() - while len(waiting_tasks) > 0: + # don't sail anymore, but let tasks finish + while not waiting_tasks.is_empty(): task = waiting_tasks.pop() try: wait_for = next(task.task) waiting_tasks.push( - WaitingTask(task.task, cruise.spacetime.time + wait_for.time) + _WaitingTask(task.task, cruise.spacetime.time + wait_for.time) ) except StopIteration: pass @@ -205,74 +236,54 @@ def simulate_schedule( return results -class ShipUnderwaterStLoop: - _sample_period: timedelta - - def __init__(self, sample_period: timedelta) -> None: - self._sample_period = sample_period - - def __call__( - self, cruise: Cruise, schedule_results: ScheduleResults - ) -> Generator[WaitFor, None, None]: - while not cruise.finished: - schedule_results.ship_underwater_sts.append(cruise.spacetime) - yield WaitFor(self._sample_period) - - -class ADCPLoop: - _sample_period: timedelta - - def __init__(self, sample_period: timedelta) -> None: - self._sample_period = sample_period - - def __call__( - self, cruise: Cruise, schedule_results: ScheduleResults - ) -> Generator[WaitFor, None, None]: - while not cruise.finished: - schedule_results.adcps.append(cruise.spacetime) - yield WaitFor(self._sample_period) - - -class CTDTask: - _stationkeeping_time: timedelta - _min_depth: float - _max_depth: float - - def __init__( - self, stationkeeping_time: timedelta, min_depth: float, max_depth: float - ) -> None: - self._stationkeeping_time = stationkeeping_time - self._min_depth = min_depth - self._max_depth = max_depth - - def __call__( - self, cruise: Cruise, schedule_results: ScheduleResults - ) -> Generator[WaitFor, None, None]: - with cruise.do_not_sail(): - schedule_results.ctds.append( - CTD( - spacetime=cruise.spacetime, - min_depth=self._min_depth, - max_depth=self._max_depth, - ) +def _ship_underwater_st_loop( + sample_period: timedelta, cruise: _Cruise, schedule_results: _ScheduleResults +) -> Generator[_WaitFor, None, None]: + while not cruise.finished: + schedule_results.ship_underwater_sts.append(cruise.spacetime) + yield _WaitFor(sample_period) + + +def _adcp_loop( + sample_period: timedelta, cruise: _Cruise, schedule_results: _ScheduleResults +) -> Generator[_WaitFor, None, None]: + while not cruise.finished: + schedule_results.adcps.append(cruise.spacetime) + yield _WaitFor(sample_period) + + +def _ctd_task( + stationkeeping_time: timedelta, + min_depth: float, + max_depth: float, + cruise: _Cruise, + schedule_results: _ScheduleResults, +) -> Generator[_WaitFor, None, None]: + with cruise.do_not_sail(): + schedule_results.ctds.append( + CTD( + spacetime=cruise.spacetime, + min_depth=min_depth, + max_depth=max_depth, ) - yield WaitFor(self._stationkeeping_time) + ) + yield _WaitFor(stationkeeping_time) -class DrifterTask: - def __call__( - self, cruise: Cruise, schedule_results: ScheduleResults - ) -> Generator[WaitFor, None, None]: - # TODO add drifter to drifter list - pass +def _drifter_Task( + cruise: _Cruise, schedule_results: _ScheduleResults +) -> Generator[_WaitFor, None, None]: + # TODO add drifter to drifter list + # yield 0 second wait time so python understands that the function must be a generator + yield _WaitFor(timedelta()) -class ArgoFloatTask: - def __call__( - self, cruise: Cruise, schedule_results: ScheduleResults - ) -> Generator[WaitFor, None, None]: - # TODO add argo float to argo float list - pass +def _argo_float_task( + cruise: _Cruise, schedule_results: _ScheduleResults +) -> Generator[_WaitFor, None, None]: + # TODO add argo float to argo float list + # yield 0 second wait time so python understands that the function must be a generator + yield _WaitFor(timedelta()) def sailship(config: VirtualShipConfig): @@ -290,17 +301,14 @@ def sailship(config: VirtualShipConfig): _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) - schedule_results = simulate_schedule( - [ - ShipUnderwaterStLoop(config.ship_underwater_st_period), - ADCPLoop(config.adcp_period), - ], + schedule_results = _simulate_schedule( waypoints=config.waypoints, projection=projection, ship_speed=config.ship_speed, ctd_stationkeeping_time=config.ctd_stationkeeping_time, ctd_min_depth=config.ctd_fieldset.U.depth[0], ctd_max_depth=config.ctd_fieldset.U.depth[-1], + config=config, ) print("Simulating onboard salinity and temperature measurements.") From 00247c83648be7cf23442fcd4740c9323b36f6d4 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:38:21 +0200 Subject: [PATCH 09/29] cleanup --- tests/test_sailship.py | 33 +++- virtual_ship/sailship.py | 280 ++++++++++++++-------------- virtual_ship/virtual_ship_config.py | 40 +++- 3 files changed, 191 insertions(+), 162 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 55afbfe5..d9b5ab83 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -11,6 +11,9 @@ ADCPConfig, ArgoFloatConfig, VirtualShipConfig, + DrifterConfig, + ShipUnderwaterSTConfig, + CTDConfig, ) from datetime import timedelta @@ -92,7 +95,25 @@ def test_sailship() -> None: drift_days=9, ) - adcp_config = ADCPConfig(max_depth=-1000, bin_size_m=24) + adcp_config = ADCPConfig( + max_depth=-1000, + bin_size_m=24, + period=timedelta(minutes=5), + fieldset=adcp_fieldset, + ) + + ship_underwater_st_config = ShipUnderwaterSTConfig( + period=timedelta(minutes=5), fieldset=ship_underwater_st_fieldset + ) + + ctd_config = CTDConfig( + stationkeeping_time=timedelta(minutes=20), + fieldset=ctd_fieldset, + min_depth=ctd_fieldset.U.depth[0], + max_depth=ctd_fieldset.U.depth[-1], + ) + + drifter_config = DrifterConfig(fieldset=drifter_fieldset) waypoints = [ Waypoint( @@ -113,15 +134,11 @@ def test_sailship() -> None: config = VirtualShipConfig( ship_speed=5.14, waypoints=waypoints, - adcp_fieldset=adcp_fieldset, - ship_underwater_st_fieldset=ship_underwater_st_fieldset, - ctd_fieldset=ctd_fieldset, - drifter_fieldset=drifter_fieldset, argo_float_config=argo_float_config, adcp_config=adcp_config, - ship_underwater_st_period=timedelta(minutes=5), - adcp_period=timedelta(minutes=5), - ctd_stationkeeping_time=timedelta(minutes=20), + ship_underwater_st_config=ship_underwater_st_config, + ctd_config=ctd_config, + drifter_config=drifter_config, ) sailship(config) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 49bc0d50..ab96da6d 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -25,79 +25,86 @@ from .sorted_queue import SortedQueue -class _Cruise: - _finished: bool - _sail_lock_count: int - spacetime: Spacetime - - def __init__(self, spacetime: Spacetime) -> None: - self._finished = False - self._sail_lock_count = 0 - self.spacetime = spacetime - - @property - def finished(self) -> bool: - return self._finished - - @contextmanager - def do_not_sail(self) -> Generator[None, None, None]: - try: - self._sail_lock_count += 1 - yield - finally: - self._sail_lock_count -= 1 +def sailship(config: VirtualShipConfig): + """ + Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. - def finish(self) -> None: - self._finished = True + :param config: The cruise configuration. + :raises NotImplementedError: In case an instrument is not supported. + :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. + """ + config.verify() - @property - def sail_is_locked(self) -> bool: - return self._sail_lock_count > 0 + # projection used to sail between waypoints + projection = pyproj.Geod(ellps="WGS84") + _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) -@dataclass -class _ScheduleResults: - adcps: list[Spacetime] = field(default_factory=list, init=False) - ship_underwater_sts: list[Spacetime] = field(default_factory=list, init=False) - argo_floats: list[ArgoFloat] = field(default_factory=list, init=False) - drifters: list[Drifter] = field(default_factory=list, init=False) - ctds: list[CTD] = field(default_factory=list, init=False) + schedule_results = _simulate_schedule( + waypoints=config.waypoints, + projection=projection, + config=config, + ) + print("Simulating onboard salinity and temperature measurements.") + simulate_ship_underwater_st( + fieldset=config.ship_underwater_st_config.fieldset, + out_path=os.path.join("results", "ship_underwater_st.zarr"), + depth=-2, + sample_points=schedule_results.ship_underwater_sts, + ) -@dataclass -class _WaitFor: - time: timedelta + print("Simulating onboard ADCP.") + simulate_adcp( + fieldset=config.adcp_config.fieldset, + out_path=os.path.join("results", "adcp.zarr"), + max_depth=config.adcp_config.max_depth, + min_depth=-5, + num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, + sample_points=schedule_results.adcps, + ) + print("Simulating CTD casts.") + simulate_ctd( + out_path=os.path.join("results", "ctd.zarr"), + fieldset=config.ctd_config.fieldset, + ctds=schedule_results.ctds, + outputdt=timedelta(seconds=10), + ) -class _WaitingTask: - _task: Generator[_WaitFor, None, None] - _wait_until: datetime + print("Simulating drifters") + simulate_drifters( + out_path=os.path.join("results", "drifters.zarr"), + fieldset=config.drifter_config.fieldset, + drifters=schedule_results.drifters, + outputdt=timedelta(hours=5), + dt=timedelta(minutes=5), + endtime=None, + ) - def __init__( - self, task: Generator[_WaitFor, None, None], wait_until: datetime - ) -> None: - self._task = task - self._wait_until = wait_until + print("Simulating argo floats") + simulate_argo_floats( + out_path=os.path.join("results", "argo_floats.zarr"), + argo_floats=schedule_results.argo_floats, + fieldset=config.argo_float_config.fieldset, + outputdt=timedelta(minutes=5), + endtime=None, + ) - def __lt__(self, other: _WaitingTask): - return self._wait_until < other._wait_until + # convert CTD data to CSV + # print("Postprocessing..") + # postprocess() - @property - def task(self) -> Generator[_WaitFor, None, None]: - return self._task + # print("All data has been gathered and postprocessed, returning home.") - @property - def wait_until(self) -> datetime: - return self._wait_until + # time_past = time - config.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, - ship_speed: float, - ctd_stationkeeping_time: timedelta, - ctd_min_depth: float, - ctd_max_depth: float, config: VirtualShipConfig, ) -> _ScheduleResults: # TODO verify waypoint reached in time @@ -109,14 +116,14 @@ def _simulate_schedule( waiting_tasks.push( _WaitingTask( task=_ship_underwater_st_loop( - config.ship_underwater_st_period, cruise, results + config.ship_underwater_st_config.period, cruise, results ), wait_until=cruise.spacetime.time, ) ) waiting_tasks.push( _WaitingTask( - task=_adcp_loop(config.adcp_period, cruise, results), + task=_adcp_loop(config.adcp_config.period, cruise, results), wait_until=cruise.spacetime.time, ) ) @@ -142,9 +149,9 @@ def _simulate_schedule( waiting_tasks.push( _WaitingTask( _ctd_task( - ctd_stationkeeping_time, - ctd_min_depth, - ctd_max_depth, + config.ctd_config.stationkeeping_time, + config.ctd_config.min_depth, + config.ctd_config.max_depth, cruise, results, ), @@ -190,7 +197,7 @@ def _simulate_schedule( azimuth1 = geodinv[0] distance_to_next_waypoint = geodinv[2] time_to_reach = timedelta( - seconds=distance_to_next_waypoint / ship_speed + seconds=distance_to_next_waypoint / config.ship_speed ) arrival_time = cruise.spacetime.time + time_to_reach @@ -206,7 +213,7 @@ def _simulate_schedule( time_to_sail = ( waiting_tasks.peek().wait_until - cruise.spacetime.time ) - distance_to_move = ship_speed * time_to_sail.total_seconds() + distance_to_move = config.ship_speed * time_to_sail.total_seconds() geodfwd: tuple[float, float, float] = projection.fwd( lons=cruise.spacetime.location.lon, lats=cruise.spacetime.location.lat, @@ -236,6 +243,72 @@ def _simulate_schedule( return results +class _Cruise: + _finished: bool + _sail_lock_count: int + spacetime: Spacetime + + def __init__(self, spacetime: Spacetime) -> None: + self._finished = False + self._sail_lock_count = 0 + self.spacetime = spacetime + + @property + def finished(self) -> bool: + return self._finished + + @contextmanager + def do_not_sail(self) -> Generator[None, None, None]: + try: + self._sail_lock_count += 1 + yield + finally: + self._sail_lock_count -= 1 + + def finish(self) -> None: + self._finished = True + + @property + def sail_is_locked(self) -> bool: + return self._sail_lock_count > 0 + + +@dataclass +class _ScheduleResults: + adcps: list[Spacetime] = field(default_factory=list, init=False) + ship_underwater_sts: list[Spacetime] = field(default_factory=list, init=False) + argo_floats: list[ArgoFloat] = field(default_factory=list, init=False) + drifters: list[Drifter] = field(default_factory=list, init=False) + ctds: list[CTD] = field(default_factory=list, init=False) + + +@dataclass +class _WaitFor: + time: timedelta + + +class _WaitingTask: + _task: Generator[_WaitFor, None, None] + _wait_until: datetime + + def __init__( + self, task: Generator[_WaitFor, None, None], wait_until: datetime + ) -> None: + self._task = task + self._wait_until = wait_until + + def __lt__(self, other: _WaitingTask): + return self._wait_until < other._wait_until + + @property + def task(self) -> Generator[_WaitFor, None, None]: + return self._task + + @property + def wait_until(self) -> datetime: + return self._wait_until + + def _ship_underwater_st_loop( sample_period: timedelta, cruise: _Cruise, schedule_results: _ScheduleResults ) -> Generator[_WaitFor, None, None]: @@ -286,87 +359,6 @@ def _argo_float_task( yield _WaitFor(timedelta()) -def sailship(config: VirtualShipConfig): - """ - Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. - - :param config: The cruise configuration. - :raises NotImplementedError: In case an instrument is not supported. - :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. - """ - config.verify() - - # projection used to sail between waypoints - projection = pyproj.Geod(ellps="WGS84") - - _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) - - schedule_results = _simulate_schedule( - waypoints=config.waypoints, - projection=projection, - ship_speed=config.ship_speed, - ctd_stationkeeping_time=config.ctd_stationkeeping_time, - ctd_min_depth=config.ctd_fieldset.U.depth[0], - ctd_max_depth=config.ctd_fieldset.U.depth[-1], - config=config, - ) - - print("Simulating onboard salinity and temperature measurements.") - simulate_ship_underwater_st( - fieldset=config.ship_underwater_st_fieldset, - out_path=os.path.join("results", "ship_underwater_st.zarr"), - depth=-2, - sample_points=schedule_results.ship_underwater_sts, - ) - - print("Simulating onboard ADCP.") - simulate_adcp( - fieldset=config.adcp_fieldset, - out_path=os.path.join("results", "adcp.zarr"), - max_depth=config.adcp_config.max_depth, - min_depth=-5, - num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, - sample_points=schedule_results.adcps, - ) - - print("Simulating CTD casts.") - simulate_ctd( - out_path=os.path.join("results", "ctd.zarr"), - fieldset=config.ctd_fieldset, - ctds=schedule_results.ctds, - outputdt=timedelta(seconds=10), - ) - - print("Simulating drifters") - simulate_drifters( - out_path=os.path.join("results", "drifters.zarr"), - fieldset=config.drifter_fieldset, - drifters=schedule_results.drifters, - outputdt=timedelta(hours=5), - dt=timedelta(minutes=5), - endtime=None, - ) - - print("Simulating argo floats") - simulate_argo_floats( - out_path=os.path.join("results", "argo_floats.zarr"), - argo_floats=schedule_results.argo_floats, - fieldset=config.argo_float_config.fieldset, - outputdt=timedelta(minutes=5), - endtime=None, - ) - - # convert CTD data to CSV - # print("Postprocessing..") - # postprocess() - - # print("All data has been gathered and postprocessed, returning home.") - - # time_past = time - config.waypoints[0].time - # cost = costs(config, time_past) - # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") - - def _verify_waypoints( waypoints: list[Waypoint], ship_speed: float, projection: pyproj.Geod ) -> None: diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 40d5cb22..1bcedd81 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -27,6 +27,33 @@ class ADCPConfig: max_depth: float bin_size_m: int + period: timedelta + fieldset: FieldSet + + +@dataclass +class CTDConfig: + """Configuration for CTD instrument""" + + stationkeeping_time: timedelta + fieldset: FieldSet + min_depth: float + max_depth: float + + +@dataclass +class ShipUnderwaterSTConfig: + """Configuration for underwater ST""" + + period: timedelta + fieldset: FieldSet + + +@dataclass +class DrifterConfig: + """Configuration for drifters""" + + fieldset: FieldSet @dataclass @@ -37,18 +64,11 @@ class VirtualShipConfig: waypoints: list[Waypoint] - adcp_fieldset: FieldSet - ship_underwater_st_fieldset: FieldSet - ctd_fieldset: FieldSet - drifter_fieldset: FieldSet - argo_float_config: ArgoFloatConfig adcp_config: ADCPConfig - - ship_underwater_st_period: timedelta - adcp_period: timedelta - - ctd_stationkeeping_time: timedelta + ctd_config: CTDConfig + ship_underwater_st_config: ShipUnderwaterSTConfig + drifter_config: DrifterConfig def verify(self) -> None: """ From ae8ff0a2368263cf67b4c38842bfa49d5e2e61f7 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:43:34 +0200 Subject: [PATCH 10/29] codetools --- tests/test_sailship.py | 6 +++--- virtual_ship/sailship.py | 19 +++++++---------- virtual_ship/sorted_queue.py | 33 +++++++++++++++++++++++++++-- virtual_ship/virtual_ship_config.py | 8 +++---- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index d9b5ab83..723244a3 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -1,6 +1,7 @@ """Performs a complete cruise with virtual ship.""" import datetime +from datetime import timedelta import numpy as np from parcels import Field, FieldSet @@ -10,12 +11,11 @@ from virtual_ship.virtual_ship_config import ( ADCPConfig, ArgoFloatConfig, - VirtualShipConfig, + CTDConfig, DrifterConfig, ShipUnderwaterSTConfig, - CTDConfig, + VirtualShipConfig, ) -from datetime import timedelta def _make_ctd_fieldset(base_time: datetime) -> FieldSet: diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index ab96da6d..4f7575f2 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -1,8 +1,12 @@ """sailship function.""" from __future__ import annotations + import os -from datetime import timedelta +from contextlib import contextmanager +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from typing import Generator import pyproj @@ -12,17 +16,12 @@ from .instruments.ctd import CTD, simulate_ctd from .instruments.drifter import Drifter, simulate_drifters from .instruments.ship_underwater_st import simulate_ship_underwater_st +from .location import Location from .planning_error import PlanningError +from .sorted_queue import PriorityQueue from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint -from datetime import datetime, timedelta -from dataclasses import dataclass, field -from typing import Generator, Callable -from collections import deque -from .location import Location -from contextlib import contextmanager -from .sorted_queue import SortedQueue def sailship(config: VirtualShipConfig): @@ -30,8 +29,6 @@ def sailship(config: VirtualShipConfig): Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. :param config: The cruise configuration. - :raises NotImplementedError: In case an instrument is not supported. - :raises PlanningError: In case the schedule is not feasible when checking before sailing, or if it turns out not to be feasible during sailing. """ config.verify() @@ -112,7 +109,7 @@ def _simulate_schedule( cruise = _Cruise(Spacetime(waypoints[0].location, waypoints[0].time)) results = _ScheduleResults() - waiting_tasks = SortedQueue[_WaitingTask]() + waiting_tasks = PriorityQueue[_WaitingTask]() waiting_tasks.push( _WaitingTask( task=_ship_underwater_st_loop( diff --git a/virtual_ship/sorted_queue.py b/virtual_ship/sorted_queue.py index 5bd8e0e6..92cc8230 100644 --- a/virtual_ship/sorted_queue.py +++ b/virtual_ship/sorted_queue.py @@ -1,26 +1,55 @@ +"""PriorityQueue class.""" + import heapq -from typing import TypeVar, Generic +from typing import Generic, TypeVar T = TypeVar("T") -class SortedQueue(Generic[T]): +class PriorityQueue(Generic[T]): + """A priority queue: a queue that pops the smallest value first.""" + _queue: list[T] def __init__(self) -> None: + """Initialize this object.""" self._queue: list[T] = [] def push(self, item: T) -> None: + """ + Add an item to the queue. + + :param item: The item to add. + """ heapq.heappush(self._queue, item) def pop(self) -> T: + """ + Get and remove the smallest item from the queue. + + :returns: The removed item. + """ return heapq.heappop(self._queue) def peek(self) -> T: + """ + Look at the smallest item in the queue. + + :returns: The smallest item. + """ return self._queue[0] def is_empty(self) -> bool: + """ + Check if the queue is empty. + + :returns: Whether the queue is empty. + """ return len(self._queue) == 0 def __len__(self) -> int: + """Get the number of items in the queue. + + :returns: The number of items in the queue. + """ return len(self._queue) diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 1bcedd81..25d64a2a 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -1,12 +1,12 @@ """VirtualShipConfig class.""" from dataclasses import dataclass +from datetime import timedelta from parcels import FieldSet from .location import Location from .waypoint import Waypoint -from datetime import timedelta @dataclass @@ -33,7 +33,7 @@ class ADCPConfig: @dataclass class CTDConfig: - """Configuration for CTD instrument""" + """Configuration for CTD instrument.""" stationkeeping_time: timedelta fieldset: FieldSet @@ -43,7 +43,7 @@ class CTDConfig: @dataclass class ShipUnderwaterSTConfig: - """Configuration for underwater ST""" + """Configuration for underwater ST.""" period: timedelta fieldset: FieldSet @@ -51,7 +51,7 @@ class ShipUnderwaterSTConfig: @dataclass class DrifterConfig: - """Configuration for drifters""" + """Configuration for drifters.""" fieldset: FieldSet From 1978c43fb5578eca70fa9c787ed57ed3bd18f662 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:44:44 +0200 Subject: [PATCH 11/29] rename to priority queue --- virtual_ship/{sorted_queue.py => priority_queue.py} | 0 virtual_ship/sailship.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename virtual_ship/{sorted_queue.py => priority_queue.py} (100%) diff --git a/virtual_ship/sorted_queue.py b/virtual_ship/priority_queue.py similarity index 100% rename from virtual_ship/sorted_queue.py rename to virtual_ship/priority_queue.py diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 4f7575f2..4bdd5d81 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -18,7 +18,7 @@ from .instruments.ship_underwater_st import simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError -from .sorted_queue import PriorityQueue +from .priority_queue import PriorityQueue from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint From 512a2c24a240a36cec86f22915f0b26f92587d23 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:45:59 +0200 Subject: [PATCH 12/29] cleanup --- virtual_ship/sailship.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 4bdd5d81..7dfd9d3c 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -11,11 +11,11 @@ import pyproj from .instrument_type import InstrumentType -from .instruments.adcp import simulate_adcp -from .instruments.argo_float import ArgoFloat, simulate_argo_floats -from .instruments.ctd import CTD, simulate_ctd -from .instruments.drifter import Drifter, simulate_drifters -from .instruments.ship_underwater_st import simulate_ship_underwater_st +from .instruments.adcp import _simulate_adcp +from .instruments.argo_float import ArgoFloat, _simulate_argo_floats +from .instruments.ctd import CTD, _simulate_ctd +from .instruments.drifter import Drifter, _simulate_drifters +from .instruments.ship_underwater_st import _simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError from .priority_queue import PriorityQueue @@ -44,7 +44,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating onboard salinity and temperature measurements.") - simulate_ship_underwater_st( + _simulate_ship_underwater_st( fieldset=config.ship_underwater_st_config.fieldset, out_path=os.path.join("results", "ship_underwater_st.zarr"), depth=-2, @@ -52,7 +52,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating onboard ADCP.") - simulate_adcp( + _simulate_adcp( fieldset=config.adcp_config.fieldset, out_path=os.path.join("results", "adcp.zarr"), max_depth=config.adcp_config.max_depth, @@ -62,7 +62,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating CTD casts.") - simulate_ctd( + _simulate_ctd( out_path=os.path.join("results", "ctd.zarr"), fieldset=config.ctd_config.fieldset, ctds=schedule_results.ctds, @@ -70,7 +70,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating drifters") - simulate_drifters( + _simulate_drifters( out_path=os.path.join("results", "drifters.zarr"), fieldset=config.drifter_config.fieldset, drifters=schedule_results.drifters, @@ -80,7 +80,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating argo floats") - simulate_argo_floats( + _simulate_argo_floats( out_path=os.path.join("results", "argo_floats.zarr"), argo_floats=schedule_results.argo_floats, fieldset=config.argo_float_config.fieldset, From 0d00ceefe8547164ecd4e22c0bde0656b2bdd560 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:46:54 +0200 Subject: [PATCH 13/29] cleanup --- virtual_ship/sailship.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 7dfd9d3c..4bdd5d81 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -11,11 +11,11 @@ import pyproj from .instrument_type import InstrumentType -from .instruments.adcp import _simulate_adcp -from .instruments.argo_float import ArgoFloat, _simulate_argo_floats -from .instruments.ctd import CTD, _simulate_ctd -from .instruments.drifter import Drifter, _simulate_drifters -from .instruments.ship_underwater_st import _simulate_ship_underwater_st +from .instruments.adcp import simulate_adcp +from .instruments.argo_float import ArgoFloat, simulate_argo_floats +from .instruments.ctd import CTD, simulate_ctd +from .instruments.drifter import Drifter, simulate_drifters +from .instruments.ship_underwater_st import simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError from .priority_queue import PriorityQueue @@ -44,7 +44,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating onboard salinity and temperature measurements.") - _simulate_ship_underwater_st( + simulate_ship_underwater_st( fieldset=config.ship_underwater_st_config.fieldset, out_path=os.path.join("results", "ship_underwater_st.zarr"), depth=-2, @@ -52,7 +52,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating onboard ADCP.") - _simulate_adcp( + simulate_adcp( fieldset=config.adcp_config.fieldset, out_path=os.path.join("results", "adcp.zarr"), max_depth=config.adcp_config.max_depth, @@ -62,7 +62,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating CTD casts.") - _simulate_ctd( + simulate_ctd( out_path=os.path.join("results", "ctd.zarr"), fieldset=config.ctd_config.fieldset, ctds=schedule_results.ctds, @@ -70,7 +70,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating drifters") - _simulate_drifters( + simulate_drifters( out_path=os.path.join("results", "drifters.zarr"), fieldset=config.drifter_config.fieldset, drifters=schedule_results.drifters, @@ -80,7 +80,7 @@ def sailship(config: VirtualShipConfig): ) print("Simulating argo floats") - _simulate_argo_floats( + simulate_argo_floats( out_path=os.path.join("results", "argo_floats.zarr"), argo_floats=schedule_results.argo_floats, fieldset=config.argo_float_config.fieldset, From 592d8735a2e981c67128b5fd01b415b241b6706c Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 12:47:56 +0200 Subject: [PATCH 14/29] typo --- virtual_ship/sailship.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 4bdd5d81..5687552a 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -394,7 +394,7 @@ def _verify_waypoints( time = arrival_time elif arrival_time > wp_next.time: raise PlanningError( - "Waypoint planning is not valid: would arrive too late a waypoint." + "Waypoint planning is not valid: would arrive too late at a waypoint." ) else: time = wp_next.time From 7ac763f2037c7c1f9f178032e074dc062ac525c4 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 15:25:24 +0200 Subject: [PATCH 15/29] doc --- virtual_ship/sailship.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 5687552a..f287825f 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -359,6 +359,14 @@ def _argo_float_task( def _verify_waypoints( waypoints: list[Waypoint], ship_speed: float, projection: pyproj.Geod ) -> None: + """ + Verify waypoints are ordered by time, first waypoint has a start time, and that schedule is feasible in terms of time if no unexpected things happen. + + :param waypoints: The waypoints to check. + :param ship_speed: Speed of the ship. + :param projection: projection used to sail between waypoints. + :raises PlanningError: If waypoints are not feasible or incorrect. + """ # check first waypoint has a time if waypoints[0].time is None: raise PlanningError("First waypoint must have a specified time.") From 6252377c5fa22d1c89c6391632967cff0de2bc83 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 15:35:30 +0200 Subject: [PATCH 16/29] added drifters & argos --- tests/test_sailship.py | 7 ++++- virtual_ship/sailship.py | 47 +++++++++++++++-------------- virtual_ship/virtual_ship_config.py | 3 ++ 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 723244a3..6fff1a39 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -88,6 +88,7 @@ def test_sailship() -> None: argo_float_config = ArgoFloatConfig( fieldset=argo_float_fieldset, + min_depth=-argo_float_fieldset.U.depth[0], max_depth=-2000, drift_depth=-1000, vertical_speed=-0.10, @@ -113,7 +114,11 @@ def test_sailship() -> None: max_depth=ctd_fieldset.U.depth[-1], ) - drifter_config = DrifterConfig(fieldset=drifter_fieldset) + drifter_config = DrifterConfig( + fieldset=drifter_fieldset, + depth=-drifter_fieldset.U.depth[0], + lifetime=timedelta(weeks=4), + ) waypoints = [ Waypoint( diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index f287825f..0cef30e2 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -130,18 +130,9 @@ def _simulate_schedule( # add task to the task queue for the instrument at the current waypoint match waypoint.instrument: case InstrumentType.ARGO_FLOAT: - waiting_tasks.push( - _WaitingTask( - _argo_float_task(cruise, results), - wait_until=cruise.spacetime.time, - ) - ) + _argo_float_task(cruise, results) case InstrumentType.DRIFTER: - waiting_tasks.push( - _WaitingTask( - _drifter_Task(cruise, results), wait_until=cruise.spacetime.time - ) - ) + _drifter_task(cruise, results) case InstrumentType.CTD: waiting_tasks.push( _WaitingTask( @@ -340,20 +331,32 @@ def _ctd_task( yield _WaitFor(stationkeeping_time) -def _drifter_Task( - cruise: _Cruise, schedule_results: _ScheduleResults -) -> Generator[_WaitFor, None, None]: - # TODO add drifter to drifter list - # yield 0 second wait time so python understands that the function must be a generator - yield _WaitFor(timedelta()) +def _drifter_task( + cruise: _Cruise, schedule_results: _ScheduleResults, config: VirtualShipConfig +) -> None: + schedule_results.drifters.append( + Drifter( + cruise.spacetime, + depth=config.drifter_config.depth, + lifetime=config.drifter_config.lifetime, + ) + ) def _argo_float_task( - cruise: _Cruise, schedule_results: _ScheduleResults -) -> Generator[_WaitFor, None, None]: - # TODO add argo float to argo float list - # yield 0 second wait time so python understands that the function must be a generator - yield _WaitFor(timedelta()) + cruise: _Cruise, schedule_results: _ScheduleResults, config: VirtualShipConfig +) -> None: + schedule_results.argo_floats.append( + ArgoFloat( + spacetime=cruise.spacetime, + min_depth=config.argo_float_config.min_depth, + max_depth=config.argo_float_config.max_depth, + drift_depth=config.argo_float_config.drift_depth, + vertical_speed=config.argo_float_config.vertical_speed, + cycle_days=config.argo_float_config.cycle_days, + drift_days=config.argo_float_config.drift_days, + ) + ) def _verify_waypoints( diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index 25d64a2a..e8113005 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -14,6 +14,7 @@ class ArgoFloatConfig: """Configuration for argos floats.""" fieldset: FieldSet + min_depth: float max_depth: float drift_depth: float vertical_speed: float @@ -54,6 +55,8 @@ class DrifterConfig: """Configuration for drifters.""" fieldset: FieldSet + depth: float + lifetime: timedelta @dataclass From 2e95eb5a459cf2229928f92c421ed1ccae3aae80 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 15:59:08 +0200 Subject: [PATCH 17/29] Add cruise cost --- virtual_ship/costs.py | 19 ++++++++- virtual_ship/sailship.py | 88 +++++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/virtual_ship/costs.py b/virtual_ship/costs.py index c2473cb0..4339eca4 100644 --- a/virtual_ship/costs.py +++ b/virtual_ship/costs.py @@ -2,6 +2,7 @@ from datetime import timedelta +from .instrument_type import InstrumentType from .virtual_ship_config import VirtualShipConfig @@ -18,8 +19,22 @@ def costs(config: VirtualShipConfig, total_time: timedelta): argo_deploy_cost = 15000 ship_cost = ship_cost_per_day / 24 * total_time.total_seconds() // 3600 - argo_cost = len(config.argo_float_deploy_locations) * argo_deploy_cost - drifter_cost = len(config.drifter_deploy_locations) * drifter_deploy_cost + num_argos = len( + [ + waypoint + for waypoint in config.waypoints + if waypoint.instrument is InstrumentType.ARGO_FLOAT + ] + ) + argo_cost = num_argos * argo_deploy_cost + num_drifters = len( + [ + waypoint + for waypoint in config.waypoints + if waypoint.instrument is InstrumentType.DRIFTER + ] + ) + drifter_cost = num_drifters * drifter_deploy_cost cost = ship_cost + argo_cost + drifter_cost return cost diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 0cef30e2..99d20095 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -10,6 +10,7 @@ import pyproj +from .costs import costs from .instrument_type import InstrumentType from .instruments.adcp import simulate_adcp from .instruments.argo_float import ArgoFloat, simulate_argo_floats @@ -37,18 +38,21 @@ def sailship(config: VirtualShipConfig): _verify_waypoints(config.waypoints, config.ship_speed, projection=projection) + # simulate the sailing and aggregate what measurements should be simulated schedule_results = _simulate_schedule( waypoints=config.waypoints, projection=projection, config=config, ) + # simulate the measurements + print("Simulating onboard salinity and temperature measurements.") simulate_ship_underwater_st( fieldset=config.ship_underwater_st_config.fieldset, out_path=os.path.join("results", "ship_underwater_st.zarr"), depth=-2, - sample_points=schedule_results.ship_underwater_sts, + sample_points=schedule_results.measurements_to_simulate.ship_underwater_sts, ) print("Simulating onboard ADCP.") @@ -58,14 +62,14 @@ def sailship(config: VirtualShipConfig): max_depth=config.adcp_config.max_depth, min_depth=-5, num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, - sample_points=schedule_results.adcps, + sample_points=schedule_results.measurements_to_simulate.adcps, ) print("Simulating CTD casts.") simulate_ctd( out_path=os.path.join("results", "ctd.zarr"), fieldset=config.ctd_config.fieldset, - ctds=schedule_results.ctds, + ctds=schedule_results.measurements_to_simulate.ctds, outputdt=timedelta(seconds=10), ) @@ -73,7 +77,7 @@ def sailship(config: VirtualShipConfig): simulate_drifters( out_path=os.path.join("results", "drifters.zarr"), fieldset=config.drifter_config.fieldset, - drifters=schedule_results.drifters, + drifters=schedule_results.measurements_to_simulate.drifters, outputdt=timedelta(hours=5), dt=timedelta(minutes=5), endtime=None, @@ -82,21 +86,19 @@ def sailship(config: VirtualShipConfig): print("Simulating argo floats") simulate_argo_floats( out_path=os.path.join("results", "argo_floats.zarr"), - argo_floats=schedule_results.argo_floats, + argo_floats=schedule_results.measurements_to_simulate.argo_floats, fieldset=config.argo_float_config.fieldset, outputdt=timedelta(minutes=5), endtime=None, ) - # convert CTD data to CSV - # print("Postprocessing..") - # postprocess() - - # print("All data has been gathered and postprocessed, returning home.") - - # time_past = time - config.waypoints[0].time - # cost = costs(config, time_past) - # print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") + # calculate cruise cost + assert ( + config.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 + cost = costs(config, time_past) + print(f"This cruise took {time_past} and would have cost {cost:,.0f} euros.") def _simulate_schedule( @@ -104,35 +106,39 @@ def _simulate_schedule( projection: pyproj.Geod, config: VirtualShipConfig, ) -> _ScheduleResults: - # TODO verify waypoint reached in time - cruise = _Cruise(Spacetime(waypoints[0].location, waypoints[0].time)) - results = _ScheduleResults() + measurements = _MeasurementsToSimulate() + # add recurring tasks to task list waiting_tasks = PriorityQueue[_WaitingTask]() waiting_tasks.push( _WaitingTask( task=_ship_underwater_st_loop( - config.ship_underwater_st_config.period, cruise, results + config.ship_underwater_st_config.period, cruise, measurements ), wait_until=cruise.spacetime.time, ) ) waiting_tasks.push( _WaitingTask( - task=_adcp_loop(config.adcp_config.period, cruise, results), + task=_adcp_loop(config.adcp_config.period, cruise, measurements), wait_until=cruise.spacetime.time, ) ) # sail to each waypoint while executing tasks for waypoint in 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." + ) + # add task to the task queue for the instrument at the current waypoint match waypoint.instrument: case InstrumentType.ARGO_FLOAT: - _argo_float_task(cruise, results) + _argo_float_task(cruise, measurements) case InstrumentType.DRIFTER: - _drifter_task(cruise, results) + _drifter_task(cruise, measurements) case InstrumentType.CTD: waiting_tasks.push( _WaitingTask( @@ -141,7 +147,7 @@ def _simulate_schedule( config.ctd_config.min_depth, config.ctd_config.max_depth, cruise, - results, + measurements, ), cruise.spacetime.time, ) @@ -228,13 +234,15 @@ def _simulate_schedule( except StopIteration: pass - return results + return _ScheduleResults( + measurements_to_simulate=measurements, end_spacetime=cruise.spacetime + ) class _Cruise: - _finished: bool - _sail_lock_count: int - spacetime: Spacetime + _finished: bool # if last waypoint has been reached + _sail_lock_count: int # if sailing should be paused because of tasks; number of tasks that requested a pause; 0 means good to go sail + spacetime: Spacetime # current location and time def __init__(self, spacetime: Spacetime) -> None: self._finished = False @@ -262,7 +270,7 @@ def sail_is_locked(self) -> bool: @dataclass -class _ScheduleResults: +class _MeasurementsToSimulate: adcps: list[Spacetime] = field(default_factory=list, init=False) ship_underwater_sts: list[Spacetime] = field(default_factory=list, init=False) argo_floats: list[ArgoFloat] = field(default_factory=list, init=False) @@ -270,6 +278,12 @@ class _ScheduleResults: ctds: list[CTD] = field(default_factory=list, init=False) +@dataclass +class _ScheduleResults: + measurements_to_simulate: _MeasurementsToSimulate + end_spacetime: Spacetime + + @dataclass class _WaitFor: time: timedelta @@ -298,18 +312,18 @@ def wait_until(self) -> datetime: def _ship_underwater_st_loop( - sample_period: timedelta, cruise: _Cruise, schedule_results: _ScheduleResults + sample_period: timedelta, cruise: _Cruise, measurements: _MeasurementsToSimulate ) -> Generator[_WaitFor, None, None]: while not cruise.finished: - schedule_results.ship_underwater_sts.append(cruise.spacetime) + measurements.ship_underwater_sts.append(cruise.spacetime) yield _WaitFor(sample_period) def _adcp_loop( - sample_period: timedelta, cruise: _Cruise, schedule_results: _ScheduleResults + sample_period: timedelta, cruise: _Cruise, measurements: _MeasurementsToSimulate ) -> Generator[_WaitFor, None, None]: while not cruise.finished: - schedule_results.adcps.append(cruise.spacetime) + measurements.adcps.append(cruise.spacetime) yield _WaitFor(sample_period) @@ -318,10 +332,10 @@ def _ctd_task( min_depth: float, max_depth: float, cruise: _Cruise, - schedule_results: _ScheduleResults, + measurements: _MeasurementsToSimulate, ) -> Generator[_WaitFor, None, None]: with cruise.do_not_sail(): - schedule_results.ctds.append( + measurements.ctds.append( CTD( spacetime=cruise.spacetime, min_depth=min_depth, @@ -332,9 +346,9 @@ def _ctd_task( def _drifter_task( - cruise: _Cruise, schedule_results: _ScheduleResults, config: VirtualShipConfig + cruise: _Cruise, measurements: _MeasurementsToSimulate, config: VirtualShipConfig ) -> None: - schedule_results.drifters.append( + measurements.drifters.append( Drifter( cruise.spacetime, depth=config.drifter_config.depth, @@ -344,9 +358,9 @@ def _drifter_task( def _argo_float_task( - cruise: _Cruise, schedule_results: _ScheduleResults, config: VirtualShipConfig + cruise: _Cruise, measurements: _MeasurementsToSimulate, config: VirtualShipConfig ) -> None: - schedule_results.argo_floats.append( + measurements.argo_floats.append( ArgoFloat( spacetime=cruise.spacetime, min_depth=config.argo_float_config.min_depth, From 15a7165a1176af7128e4babd98e76b376ceada7c Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:04:49 +0200 Subject: [PATCH 18/29] comments --- virtual_ship/sailship.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 99d20095..77c3bc01 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -106,6 +106,16 @@ def _simulate_schedule( projection: pyproj.Geod, config: VirtualShipConfig, ) -> _ScheduleResults: + """ + Simulate the sailing and aggregate what measurements should be simulated. + + :param waypoints: The schedule. + :param projection: Projection used to sail between waypoints. + :param config: The cruise configuration. + :returns: Results from the simulation. + :raises NotImplementedError: When not supported 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)) measurements = _MeasurementsToSimulate() From 186c46df32c1d7c2dee8ccdf2a9962afdb5eecb3 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:09:58 +0200 Subject: [PATCH 19/29] add drifter & argos to test waypoints --- tests/test_sailship.py | 19 +++++++++---------- virtual_ship/sailship.py | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/test_sailship.py b/tests/test_sailship.py index 6fff1a39..f7e42027 100644 --- a/tests/test_sailship.py +++ b/tests/test_sailship.py @@ -78,7 +78,7 @@ def test_sailship() -> None: drifter_fieldset = _make_drifter_fieldset(base_time) argo_float_fieldset = FieldSet.from_data( - {"U": 0, "V": 0, "T": 0, "z": 0}, + {"U": 0, "V": 0, "T": 0, "S": 0}, { "lon": 0, "lat": 0, @@ -134,6 +134,14 @@ def test_sailship() -> None: time=base_time + datetime.timedelta(hours=1), instrument=InstrumentType.CTD, ), + Waypoint( + location=Location(latitude=-23.281289, longitude=63.743631), + instrument=InstrumentType.DRIFTER, + ), + Waypoint( + location=Location(latitude=-23.381289, longitude=63.743631), + instrument=InstrumentType.ARGO_FLOAT, + ), ] config = VirtualShipConfig( @@ -147,12 +155,3 @@ def test_sailship() -> None: ) sailship(config) - - # start_time=datetime.datetime.strptime( - # "2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S" - # ), - # route_coordinates=[ - # Location(latitude=-23.071289, longitude=63.743631), - # Location(latitude=-23.081289, longitude=63.743631), - # Location(latitude=-23.191289, longitude=63.743631), - # ], diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 77c3bc01..fac085ea 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -146,9 +146,9 @@ def _simulate_schedule( # add task to the task queue for the instrument at the current waypoint match waypoint.instrument: case InstrumentType.ARGO_FLOAT: - _argo_float_task(cruise, measurements) + _argo_float_task(cruise, measurements, config=config) case InstrumentType.DRIFTER: - _drifter_task(cruise, measurements) + _drifter_task(cruise, measurements, config=config) case InstrumentType.CTD: waiting_tasks.push( _WaitingTask( From 9f50f6ee0f54d0d7f7a3886aaf843ae01bb683fc Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:15:00 +0200 Subject: [PATCH 20/29] improve schedule error --- virtual_ship/sailship.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index fac085ea..13b570e4 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -429,7 +429,7 @@ def _verify_waypoints( time = arrival_time elif arrival_time > wp_next.time: raise PlanningError( - "Waypoint planning is not valid: would arrive too late at a waypoint." + f"Waypoint planning is not valid: would arrive too late at a waypoint requiring time {wp_next.time}" ) else: time = wp_next.time From 159235c71f20ba97e34746d4884c10927690c136 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:16:29 +0200 Subject: [PATCH 21/29] docstring improvements --- virtual_ship/waypoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtual_ship/waypoint.py b/virtual_ship/waypoint.py index 6094abd9..bf3d33b4 100644 --- a/virtual_ship/waypoint.py +++ b/virtual_ship/waypoint.py @@ -9,7 +9,7 @@ @dataclass class Waypoint: - """A Waypoint to sail to.""" + """A Waypoint to sail to with an optional time and an optional instrument.""" location: Location time: datetime | None = None From 363cd3428c202518be84cacdbb6328ef51909355 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:21:08 +0200 Subject: [PATCH 22/29] remove match statements as they are not available before python 3.10 --- virtual_ship/sailship.py | 46 +++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 13b570e4..b14df55e 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -144,28 +144,27 @@ def _simulate_schedule( ) # add task to the task queue for the instrument at the current waypoint - match waypoint.instrument: - case InstrumentType.ARGO_FLOAT: - _argo_float_task(cruise, measurements, config=config) - case InstrumentType.DRIFTER: - _drifter_task(cruise, measurements, config=config) - case InstrumentType.CTD: - waiting_tasks.push( - _WaitingTask( - _ctd_task( - config.ctd_config.stationkeeping_time, - config.ctd_config.min_depth, - config.ctd_config.max_depth, - cruise, - measurements, - ), - cruise.spacetime.time, - ) + if waypoint.instrument is InstrumentType.ARGO_FLOAT: + _argo_float_task(cruise, measurements, config=config) + elif waypoint.instrument is InstrumentType.DRIFTER: + _drifter_task(cruise, measurements, config=config) + elif waypoint.instrument is InstrumentType.CTD: + waiting_tasks.push( + _WaitingTask( + _ctd_task( + config.ctd_config.stationkeeping_time, + config.ctd_config.min_depth, + config.ctd_config.max_depth, + cruise, + measurements, + ), + cruise.spacetime.time, ) - case None: - pass - case _: - raise NotImplementedError() + ) + elif waypoint.instrument is None: + pass + else: + raise NotImplementedError() # sail to the next waypoint waypoint_reached = False @@ -413,9 +412,8 @@ def _verify_waypoints( # check that ship will arrive on time at each waypoint (in case nothing goes wrong) time = waypoints[0].time for wp, wp_next in zip(waypoints, waypoints[1:]): - match wp.instrument: - case InstrumentType.CTD: - time += timedelta(minutes=20) + if wp.instrument is InstrumentType.CTD: + time += timedelta(minutes=20) geodinv: tuple[float, float, float] = projection.inv( wp.location.lon, wp.location.lat, wp_next.location.lon, wp_next.location.lat From 3b8afb5228496c94d4869e691ccc2339c05541ec Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Mon, 5 Aug 2024 16:24:52 +0200 Subject: [PATCH 23/29] comment --- virtual_ship/sailship.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index b14df55e..83b36dcd 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -27,7 +27,7 @@ def sailship(config: VirtualShipConfig): """ - Use parcels to simulate the ship, take ctd_instruments and measure ADCP and underwaydata. + Use parcels to simulate a cruise. :param config: The cruise configuration. """ From 791e138b35f2f381cff6d7356ae80ee8f75b18fd Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 11:40:53 +0200 Subject: [PATCH 24/29] add disabling adcp, shipst --- virtual_ship/sailship.py | 61 ++++++++++++++++------------- virtual_ship/virtual_ship_config.py | 8 ++-- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 83b36dcd..11de3f65 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -47,23 +47,26 @@ def sailship(config: VirtualShipConfig): # simulate the measurements - print("Simulating onboard salinity and temperature measurements.") - simulate_ship_underwater_st( - fieldset=config.ship_underwater_st_config.fieldset, - out_path=os.path.join("results", "ship_underwater_st.zarr"), - depth=-2, - sample_points=schedule_results.measurements_to_simulate.ship_underwater_sts, - ) + if config.ship_underwater_st_config is not None: + print("Simulating onboard salinity and temperature measurements.") + simulate_ship_underwater_st( + fieldset=config.ship_underwater_st_config.fieldset, + out_path=os.path.join("results", "ship_underwater_st.zarr"), + depth=-2, + sample_points=schedule_results.measurements_to_simulate.ship_underwater_sts, + ) - print("Simulating onboard ADCP.") - simulate_adcp( - fieldset=config.adcp_config.fieldset, - out_path=os.path.join("results", "adcp.zarr"), - max_depth=config.adcp_config.max_depth, - min_depth=-5, - num_bins=(-5 - config.adcp_config.max_depth) // config.adcp_config.bin_size_m, - sample_points=schedule_results.measurements_to_simulate.adcps, - ) + if config.adcp_config is not None: + print("Simulating onboard ADCP.") + simulate_adcp( + fieldset=config.adcp_config.fieldset, + out_path=os.path.join("results", "adcp.zarr"), + max_depth=config.adcp_config.max_depth, + min_depth=-5, + num_bins=(-5 - config.adcp_config.max_depth) + // config.adcp_config.bin_size_m, + sample_points=schedule_results.measurements_to_simulate.adcps, + ) print("Simulating CTD casts.") simulate_ctd( @@ -121,20 +124,22 @@ def _simulate_schedule( # add recurring tasks to task list waiting_tasks = PriorityQueue[_WaitingTask]() - waiting_tasks.push( - _WaitingTask( - task=_ship_underwater_st_loop( - config.ship_underwater_st_config.period, cruise, measurements - ), - wait_until=cruise.spacetime.time, + if config.ship_underwater_st_config is not None: + waiting_tasks.push( + _WaitingTask( + task=_ship_underwater_st_loop( + config.ship_underwater_st_config.period, cruise, measurements + ), + wait_until=cruise.spacetime.time, + ) ) - ) - waiting_tasks.push( - _WaitingTask( - task=_adcp_loop(config.adcp_config.period, cruise, measurements), - wait_until=cruise.spacetime.time, + if config.adcp_config is not None: + waiting_tasks.push( + _WaitingTask( + task=_adcp_loop(config.adcp_config.period, cruise, measurements), + wait_until=cruise.spacetime.time, + ) ) - ) # sail to each waypoint while executing tasks for waypoint in waypoints: diff --git a/virtual_ship/virtual_ship_config.py b/virtual_ship/virtual_ship_config.py index e8113005..7dba815e 100644 --- a/virtual_ship/virtual_ship_config.py +++ b/virtual_ship/virtual_ship_config.py @@ -68,9 +68,11 @@ class VirtualShipConfig: waypoints: list[Waypoint] argo_float_config: ArgoFloatConfig - adcp_config: ADCPConfig + adcp_config: ADCPConfig | None # if None, ADCP is disabled ctd_config: CTDConfig - ship_underwater_st_config: ShipUnderwaterSTConfig + ship_underwater_st_config: ( + ShipUnderwaterSTConfig | None + ) # if None, ship underwater st is disabled drifter_config: DrifterConfig def verify(self) -> None: @@ -102,7 +104,7 @@ def verify(self) -> None: if self.argo_float_config.drift_days <= 0: raise ValueError("Argo drift cycle days must be larger than zero.") - if self.adcp_config.max_depth > 0: + if self.adcp_config is not None and self.adcp_config.max_depth > 0: raise ValueError("ADCP max depth must be negative.") @staticmethod From 497d0d877ca27d37cbb1e8d8ff415722c1093be1 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 12:13:05 +0200 Subject: [PATCH 25/29] Replace PriorityQueue with sortedcontainers.SortedList --- pyproject.toml | 2 ++ virtual_ship/priority_queue.py | 55 ---------------------------------- virtual_ship/sailship.py | 34 ++++++++++----------- 3 files changed, 18 insertions(+), 73 deletions(-) delete mode 100644 virtual_ship/priority_queue.py diff --git a/pyproject.toml b/pyproject.toml index 033d157a..c9867834 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,8 @@ dev = [ "pytest == 8.2.0", "pytest-cov == 5.0.0", "codecov == 2.1.13", + "sortedcontainers == 2.4.0", + "sortedcontainers-stubs == 2.4.2", ] [tool.isort] diff --git a/virtual_ship/priority_queue.py b/virtual_ship/priority_queue.py deleted file mode 100644 index 92cc8230..00000000 --- a/virtual_ship/priority_queue.py +++ /dev/null @@ -1,55 +0,0 @@ -"""PriorityQueue class.""" - -import heapq -from typing import Generic, TypeVar - -T = TypeVar("T") - - -class PriorityQueue(Generic[T]): - """A priority queue: a queue that pops the smallest value first.""" - - _queue: list[T] - - def __init__(self) -> None: - """Initialize this object.""" - self._queue: list[T] = [] - - def push(self, item: T) -> None: - """ - Add an item to the queue. - - :param item: The item to add. - """ - heapq.heappush(self._queue, item) - - def pop(self) -> T: - """ - Get and remove the smallest item from the queue. - - :returns: The removed item. - """ - return heapq.heappop(self._queue) - - def peek(self) -> T: - """ - Look at the smallest item in the queue. - - :returns: The smallest item. - """ - return self._queue[0] - - def is_empty(self) -> bool: - """ - Check if the queue is empty. - - :returns: Whether the queue is empty. - """ - return len(self._queue) == 0 - - def __len__(self) -> int: - """Get the number of items in the queue. - - :returns: The number of items in the queue. - """ - return len(self._queue) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 11de3f65..38520067 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -9,6 +9,7 @@ from typing import Generator import pyproj +from sortedcontainers import SortedList from .costs import costs from .instrument_type import InstrumentType @@ -19,7 +20,6 @@ from .instruments.ship_underwater_st import simulate_ship_underwater_st from .location import Location from .planning_error import PlanningError -from .priority_queue import PriorityQueue from .spacetime import Spacetime from .virtual_ship_config import VirtualShipConfig from .waypoint import Waypoint @@ -123,9 +123,9 @@ def _simulate_schedule( measurements = _MeasurementsToSimulate() # add recurring tasks to task list - waiting_tasks = PriorityQueue[_WaitingTask]() + waiting_tasks = SortedList[_WaitingTask]() if config.ship_underwater_st_config is not None: - waiting_tasks.push( + waiting_tasks.add( _WaitingTask( task=_ship_underwater_st_loop( config.ship_underwater_st_config.period, cruise, measurements @@ -134,7 +134,7 @@ def _simulate_schedule( ) ) if config.adcp_config is not None: - waiting_tasks.push( + waiting_tasks.add( _WaitingTask( task=_adcp_loop(config.adcp_config.period, cruise, measurements), wait_until=cruise.spacetime.time, @@ -154,7 +154,7 @@ def _simulate_schedule( elif waypoint.instrument is InstrumentType.DRIFTER: _drifter_task(cruise, measurements, config=config) elif waypoint.instrument is InstrumentType.CTD: - waiting_tasks.push( + waiting_tasks.add( _WaitingTask( _ctd_task( config.ctd_config.stationkeeping_time, @@ -176,13 +176,13 @@ def _simulate_schedule( while not waypoint_reached: # execute all tasks planned for current time while ( - not waiting_tasks.is_empty() - and waiting_tasks.peek().wait_until <= cruise.spacetime.time + len(waiting_tasks) > 0 + and waiting_tasks[0].wait_until <= cruise.spacetime.time ): - task = waiting_tasks.pop() + task = waiting_tasks.pop(0) try: wait_for = next(task.task) - waiting_tasks.push( + waiting_tasks.add( _WaitingTask(task.task, cruise.spacetime.time + wait_for.time) ) except StopIteration: @@ -191,7 +191,7 @@ def _simulate_schedule( # if sailing is prevented by a current task, just let time pass until the next task if cruise.sail_is_locked: cruise.spacetime = Spacetime( - cruise.spacetime.location, waiting_tasks.peek().wait_until + cruise.spacetime.location, waiting_tasks[0].wait_until ) # else, let time pass while sailing else: @@ -211,16 +211,14 @@ def _simulate_schedule( # if waypoint is reached before next task, sail to the waypoint if ( - waiting_tasks.is_empty() - or arrival_time <= waiting_tasks.peek().wait_until + len(waiting_tasks) == 0 + or arrival_time <= waiting_tasks[0].wait_until ): cruise.spacetime = Spacetime(waypoint.location, arrival_time) waypoint_reached = True # else, sail until task starts else: - time_to_sail = ( - waiting_tasks.peek().wait_until - cruise.spacetime.time - ) + time_to_sail = waiting_tasks[0].wait_until - cruise.spacetime.time distance_to_move = config.ship_speed * time_to_sail.total_seconds() geodfwd: tuple[float, float, float] = projection.fwd( lons=cruise.spacetime.location.lon, @@ -238,11 +236,11 @@ def _simulate_schedule( cruise.finish() # don't sail anymore, but let tasks finish - while not waiting_tasks.is_empty(): - task = waiting_tasks.pop() + while len(waiting_tasks) > 0: + task = waiting_tasks.pop(0) try: wait_for = next(task.task) - waiting_tasks.push( + waiting_tasks.add( _WaitingTask(task.task, cruise.spacetime.time + wait_for.time) ) except StopIteration: From 4e3b6398bb8386d2d4880e0da13d410934a5e172 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 12:16:42 +0200 Subject: [PATCH 26/29] rename cruise to expedition --- virtual_ship/sailship.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 38520067..0ecda3c4 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -27,9 +27,9 @@ def sailship(config: VirtualShipConfig): """ - Use parcels to simulate a cruise. + Use parcels to simulate a virtual ship expedition. - :param config: The cruise configuration. + :param config: The expedition configuration. """ config.verify() From 3ee7c456acdb5650d5350ecff06ac6275e093825 Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 12:18:38 +0200 Subject: [PATCH 27/29] Capitalize parcels to Parcels --- virtual_ship/instruments/adcp.py | 2 +- virtual_ship/instruments/argo_float.py | 4 ++-- virtual_ship/instruments/ctd.py | 4 ++-- virtual_ship/instruments/drifter.py | 4 ++-- virtual_ship/instruments/ship_underwater_st.py | 2 +- virtual_ship/sailship.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/virtual_ship/instruments/adcp.py b/virtual_ship/instruments/adcp.py index 096facf1..bca4ab94 100644 --- a/virtual_ship/instruments/adcp.py +++ b/virtual_ship/instruments/adcp.py @@ -31,7 +31,7 @@ def simulate_adcp( sample_points: list[Spacetime], ) -> None: """ - Use parcels to simulate an ADCP in a fieldset. + Use Parcels to simulate an ADCP in a fieldset. :param fieldset: The fieldset to simulate the ADCP in. :param out_path: The path to write the results to. diff --git a/virtual_ship/instruments/argo_float.py b/virtual_ship/instruments/argo_float.py index 3de9858f..ad4a4871 100644 --- a/virtual_ship/instruments/argo_float.py +++ b/virtual_ship/instruments/argo_float.py @@ -123,7 +123,7 @@ def simulate_argo_floats( endtime: datetime | None, ) -> None: """ - Use parcels to simulate a set of Argo floats in a fieldset. + Use Parcels to simulate a set of Argo floats in a fieldset. :param fieldset: The fieldset to simulate the Argo floats in. :param out_path: The path to write the results to. @@ -137,7 +137,7 @@ def simulate_argo_floats( print( "No Argo floats provided. Parcels currently crashes when providing an empty particle set, so no argo floats simulation will be done and no files will be created." ) - # TODO when parcels supports it this check can be removed. + # TODO when Parcels supports it this check can be removed. return # define parcel particles diff --git a/virtual_ship/instruments/ctd.py b/virtual_ship/instruments/ctd.py index 94487db4..97057f46 100644 --- a/virtual_ship/instruments/ctd.py +++ b/virtual_ship/instruments/ctd.py @@ -60,7 +60,7 @@ def simulate_ctd( outputdt: timedelta, ) -> None: """ - Use parcels to simulate a set of CTDs in a fieldset. + Use Parcels to simulate a set of CTDs in a fieldset. :param fieldset: The fieldset to simulate the CTDs in. :param out_path: The path to write the results to. @@ -75,7 +75,7 @@ def simulate_ctd( print( "No CTDs provided. Parcels currently crashes when providing an empty particle set, so no CTD simulation will be done and no files will be created." ) - # TODO when parcels supports it this check can be removed. + # TODO when Parcels supports it this check can be removed. return fieldset_starttime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[0]) diff --git a/virtual_ship/instruments/drifter.py b/virtual_ship/instruments/drifter.py index aa163380..7854d4cb 100644 --- a/virtual_ship/instruments/drifter.py +++ b/virtual_ship/instruments/drifter.py @@ -49,7 +49,7 @@ def simulate_drifters( endtime: datetime | None, ) -> None: """ - Use parcels to simulate a set of drifters in a fieldset. + Use Parcels to simulate a set of drifters in a fieldset. :param fieldset: The fieldset to simulate the Drifters in. :param out_path: The path to write the results to. @@ -62,7 +62,7 @@ def simulate_drifters( print( "No drifters provided. Parcels currently crashes when providing an empty particle set, so no drifter simulation will be done and no files will be created." ) - # TODO when parcels supports it this check can be removed. + # TODO when Parcels supports it this check can be removed. return # define parcel particles diff --git a/virtual_ship/instruments/ship_underwater_st.py b/virtual_ship/instruments/ship_underwater_st.py index b2226381..3b4ac59e 100644 --- a/virtual_ship/instruments/ship_underwater_st.py +++ b/virtual_ship/instruments/ship_underwater_st.py @@ -37,7 +37,7 @@ def simulate_ship_underwater_st( sample_points: list[Spacetime], ) -> None: """ - Use parcels to simulate underway data, measuring salinity and temperature at the given depth along the ship track in a fieldset. + Use Parcels to simulate underway data, measuring salinity and temperature at the given depth along the ship track in a fieldset. :param fieldset: The fieldset to simulate the sampling in. :param out_path: The path to write the results to. diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 0ecda3c4..28b7a57d 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -27,7 +27,7 @@ def sailship(config: VirtualShipConfig): """ - Use parcels to simulate a virtual ship expedition. + Use Parcels to simulate a virtual ship expedition. :param config: The expedition configuration. """ From a3ee894791e4571c929c5a3b29e891d3779e334d Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 12:19:55 +0200 Subject: [PATCH 28/29] comment --- virtual_ship/sailship.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 28b7a57d..4b07dbe8 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -110,7 +110,7 @@ def _simulate_schedule( config: VirtualShipConfig, ) -> _ScheduleResults: """ - Simulate the sailing and aggregate what measurements should be simulated. + Simulate the sailing and aggregate the virtual measurements that should be taken. :param waypoints: The schedule. :param projection: Projection used to sail between waypoints. From c4d4a16da4a712c1ffd3459ad2dc7c6bf07811df Mon Sep 17 00:00:00 2001 From: Aart Stuurman Date: Tue, 6 Aug 2024 12:26:32 +0200 Subject: [PATCH 29/29] commets and improved error messages --- virtual_ship/sailship.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/virtual_ship/sailship.py b/virtual_ship/sailship.py index 4b07dbe8..ea92fa24 100644 --- a/virtual_ship/sailship.py +++ b/virtual_ship/sailship.py @@ -112,11 +112,11 @@ def _simulate_schedule( """ Simulate the sailing and aggregate the virtual measurements that should be taken. - :param waypoints: The schedule. + :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 not supported instruments are encountered. + :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)) @@ -389,7 +389,7 @@ def _verify_waypoints( waypoints: list[Waypoint], ship_speed: float, projection: pyproj.Geod ) -> None: """ - Verify waypoints are ordered by time, first waypoint has a start time, and that schedule is feasible in terms of time if no unexpected things happen. + Verify waypoints are ordered by time, first waypoint has a start time, and that schedule is feasible in terms of time if no unexpected events happen. :param waypoints: The waypoints to check. :param ship_speed: Speed of the ship. @@ -412,9 +412,9 @@ def _verify_waypoints( "Each waypoint should be timed after all previous waypoints" ) - # check that ship will arrive on time at each waypoint (in case nothing goes wrong) + # check that ship will arrive on time at each waypoint (in case no unexpected event happen) time = waypoints[0].time - for wp, wp_next in zip(waypoints, waypoints[1:]): + for wp_i, (wp, wp_next) in enumerate(zip(waypoints, waypoints[1:])): if wp.instrument is InstrumentType.CTD: time += timedelta(minutes=20) @@ -430,7 +430,7 @@ def _verify_waypoints( time = arrival_time elif arrival_time > wp_next.time: raise PlanningError( - f"Waypoint planning is not valid: would arrive too late at a waypoint requiring time {wp_next.time}" + f"Waypoint planning is not valid: would arrive too late at a waypoint number {wp_i}. location: {wp.location} time: {wp.time} instrument: {wp.instrument}" ) else: time = wp_next.time