diff --git a/src/virtualship/instruments/adcp.py b/src/virtualship/instruments/adcp.py index 49a08120..26c8122d 100644 --- a/src/virtualship/instruments/adcp.py +++ b/src/virtualship/instruments/adcp.py @@ -5,7 +5,7 @@ import numpy as np from parcels import ParticleFile, ParticleSet -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.utils import build_particle_class_from_sensors, register_instrument @@ -61,9 +61,6 @@ class ADCPInstrument(Instrument): def __init__(self, expedition, from_data): """Initialize ADCPInstrument.""" variables = expedition.instruments_config.adcp_config.active_variables() - limit_spec = { - "spatial": True - } # spatial limits; lat/lon constrained to waypoint locations + buffer super().__init__( expedition, @@ -71,8 +68,7 @@ def __init__(self, expedition, from_data): add_bathymetry=False, allow_time_extrapolation=True, verbose_progress=False, - spacetime_buffer_size=None, - limit_spec=limit_spec, + fetch_spec=FetchSpec(), from_data=from_data, ) diff --git a/src/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py index b18c91ba..96b8e4f0 100644 --- a/src/virtualship/instruments/argo_float.py +++ b/src/virtualship/instruments/argo_float.py @@ -7,7 +7,7 @@ from parcels import ParticleFile, ParticleSet, StatusCode, Variable from parcels.kernels import AdvectionRK2 -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.models.spacetime import Spacetime @@ -253,14 +253,11 @@ def __init__(self, expedition, from_data): "V": "vo", **sensor_variables, } # advection variables (U and V) are always required for argo float simulation; sensor variables come from config - spacetime_buffer_size = { - "latlon": 3.0, # [degrees] - "time": expedition.instruments_config.argo_float_config.lifetime.total_seconds() + fetch_spec = FetchSpec( + latlon_buffer=3.0, # [degrees] + time_buffer=expedition.instruments_config.argo_float_config.lifetime.total_seconds() / (24 * 3600), # [days] - } - limit_spec = { - "spatial": True, # spatial limits; lat/lon constrained to waypoint locations + buffer - } + ) super().__init__( expedition, @@ -268,8 +265,7 @@ def __init__(self, expedition, from_data): add_bathymetry=True, allow_time_extrapolation=False, verbose_progress=True, - spacetime_buffer_size=spacetime_buffer_size, - limit_spec=limit_spec, + fetch_spec=fetch_spec, from_data=from_data, ) diff --git a/src/virtualship/instruments/base.py b/src/virtualship/instruments/base.py index 1595f5e4..f07bc0cf 100644 --- a/src/virtualship/instruments/base.py +++ b/src/virtualship/instruments/base.py @@ -2,6 +2,7 @@ import abc import collections +from dataclasses import dataclass from datetime import timedelta from itertools import pairwise from pathlib import Path @@ -28,6 +29,17 @@ from virtualship.models import Expedition +@dataclass +class FetchSpec: + """Fetch constraints and parameters for dataset retrieval.""" + + spatial: bool = True + latlon_buffer: float = 0.25 # degrees + time_buffer: float = 0.0 # days + depth_min: float | None = None + depth_max: float | None = None + + class Instrument(abc.ABC): """Base class for instruments and their simulation.""" @@ -50,8 +62,7 @@ def __init__( allow_time_extrapolation: bool, verbose_progress: bool, from_data: Path | None, - spacetime_buffer_size: dict | None = None, - limit_spec: dict | None = None, + fetch_spec: FetchSpec | None = None, ): """Initialise instrument.""" self.expedition = expedition @@ -67,8 +78,7 @@ def __init__( self.add_bathymetry = add_bathymetry self.allow_time_extrapolation = allow_time_extrapolation self.verbose_progress = verbose_progress - self.spacetime_buffer_size = spacetime_buffer_size - self.limit_spec = limit_spec + self.fetch_spec = fetch_spec or FetchSpec() wp_lats, wp_lons = _get_waypoint_latlons(expedition.schedule.waypoints) wp_times = [ @@ -152,12 +162,10 @@ def _get_copernicus_ds( variable=var if not physical else None, ) - latlon_buffer = self._get_spec_value( - "buffer", "latlon", 0.25 - ) # [degrees]; default 0.25 deg buffer to ensure coverage in field cell edge cases - depth_min = self._get_spec_value("limit", "depth_min", None) - depth_max = self._get_spec_value("limit", "depth_max", None) - spatial_constraint = self._get_spec_value("limit", "spatial", True) + latlon_buffer = self.fetch_spec.latlon_buffer + depth_min = self.fetch_spec.depth_min + depth_max = self.fetch_spec.depth_max + spatial_constraint = self.fetch_spec.spatial min_lon_bound = self.min_lon - latlon_buffer if spatial_constraint else None max_lon_bound = self.max_lon + latlon_buffer if spatial_constraint else None @@ -176,8 +184,6 @@ def _get_copernicus_ds( minimum_depth=depth_min, maximum_depth=depth_max, coordinates_selection_method="outside", - service="arco-geo-series", - chunk_size_limit=1, vertical_axis="elevation", ) @@ -190,7 +196,7 @@ def _generate_fieldset(self) -> parcels.FieldSet: fieldsets_list = [] keys = list(self.variables.keys()) - time_buffer = self._get_spec_value("buffer", "time", 0.0) + time_buffer = self.fetch_spec.time_buffer for key in keys: var = self.variables[key] @@ -251,8 +257,3 @@ def _generate_fieldset(self) -> parcels.FieldSet: base_fieldset.add_field(uv) return base_fieldset - - def _get_spec_value(self, spec_type: str, key: str, default=None): - """Helper to extract a value from spacetime_buffer_size or limit_spec.""" - spec = self.spacetime_buffer_size if spec_type == "buffer" else self.limit_spec - return spec.get(key) if spec and spec.get(key) is not None else default diff --git a/src/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py index 48164ca7..d6764130 100644 --- a/src/virtualship/instruments/ctd.py +++ b/src/virtualship/instruments/ctd.py @@ -7,7 +7,7 @@ from parcels import ParticleFile, ParticleSet, Variable from parcels._core.statuscodes import StatusCode -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.utils import ( @@ -142,9 +142,6 @@ class CTDInstrument(Instrument): def __init__(self, expedition, from_data): """Initialize CTDInstrument.""" variables = expedition.instruments_config.ctd_config.active_variables() - limit_spec = { - "spatial": True - } # spatial limits; lat/lon constrained to waypoint locations + buffer super().__init__( expedition, @@ -152,8 +149,7 @@ def __init__(self, expedition, from_data): add_bathymetry=True, allow_time_extrapolation=True, verbose_progress=False, - spacetime_buffer_size=None, - limit_spec=limit_spec, + fetch_spec=FetchSpec(), from_data=from_data, ) diff --git a/src/virtualship/instruments/drifter.py b/src/virtualship/instruments/drifter.py index a771b246..6319831e 100644 --- a/src/virtualship/instruments/drifter.py +++ b/src/virtualship/instruments/drifter.py @@ -8,7 +8,7 @@ from parcels._core.statuscodes import StatusCode from parcels.kernels import AdvectionRK2 -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.models.spacetime import Spacetime @@ -88,20 +88,17 @@ def __init__(self, expedition, from_data): "V": "vo", **sensor_variables, } # advection variables (U and V) are always required for drifter simulation; sensor variables come from config - spacetime_buffer_size = { - "latlon": None, - "time": expedition.instruments_config.drifter_config.lifetime.total_seconds() + fetch_spec = FetchSpec( + latlon_buffer=30.0, # TODO: generous buffer to limit tmp file size download, can potentially be removed in the future as and when Parcels streaming performance improves (see #358) + time_buffer=expedition.instruments_config.drifter_config.lifetime.total_seconds() / (24 * 3600), # [days] - } - limit_spec = { - "spatial": False, # no spatial limits; generate global fieldset - "depth_min": abs( + depth_min=abs( expedition.instruments_config.drifter_config.depth_meter ), # [meters] - "depth_max": abs( + depth_max=abs( expedition.instruments_config.drifter_config.depth_meter ), # [meters] - } + ) super().__init__( expedition, @@ -109,8 +106,7 @@ def __init__(self, expedition, from_data): add_bathymetry=False, allow_time_extrapolation=False, verbose_progress=True, - spacetime_buffer_size=spacetime_buffer_size, - limit_spec=limit_spec, + fetch_spec=fetch_spec, from_data=from_data, ) diff --git a/src/virtualship/instruments/ship_underwater_st.py b/src/virtualship/instruments/ship_underwater_st.py index 302d991f..1dc7522a 100644 --- a/src/virtualship/instruments/ship_underwater_st.py +++ b/src/virtualship/instruments/ship_underwater_st.py @@ -5,7 +5,7 @@ import numpy as np from parcels import ParticleFile, ParticleSet -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.utils import ( @@ -67,13 +67,6 @@ def __init__(self, expedition, from_data): variables = ( expedition.instruments_config.ship_underwater_st_config.active_variables() ) - spacetime_buffer_size = { - "latlon": 0.25, # [degrees] - "time": 0.0, # [days] - } - limit_spec = { - "spatial": True - } # spatial limits; lat/lon constrained to waypoint locations + buffer super().__init__( expedition, @@ -81,8 +74,7 @@ def __init__(self, expedition, from_data): add_bathymetry=False, allow_time_extrapolation=True, verbose_progress=False, - spacetime_buffer_size=spacetime_buffer_size, - limit_spec=limit_spec, + fetch_spec=FetchSpec(), from_data=from_data, ) diff --git a/src/virtualship/instruments/xbt.py b/src/virtualship/instruments/xbt.py index 92f6b3f2..7046fde1 100644 --- a/src/virtualship/instruments/xbt.py +++ b/src/virtualship/instruments/xbt.py @@ -7,7 +7,7 @@ from parcels import ParticleFile, ParticleSet, Variable from parcels._core.statuscodes import StatusCode -from virtualship.instruments.base import Instrument +from virtualship.instruments.base import FetchSpec, Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.models.spacetime import Spacetime @@ -95,9 +95,6 @@ class XBTInstrument(Instrument): def __init__(self, expedition, from_data): """Initialize XBTInstrument.""" variables = expedition.instruments_config.xbt_config.active_variables() - limit_spec = { - "spatial": True - } # spatial limits; lat/lon constrained to waypoint locations + buffer super().__init__( expedition, @@ -105,8 +102,7 @@ def __init__(self, expedition, from_data): add_bathymetry=True, allow_time_extrapolation=True, verbose_progress=False, - spacetime_buffer_size=None, - limit_spec=limit_spec, + fetch_spec=FetchSpec(), from_data=from_data, )