Skip to content

Commit aef558a

Browse files
committed
Merge branch 'delete-spacetime' into joss-paper
2 parents c13ae47 + 984d441 commit aef558a

File tree

7 files changed

+61
-44
lines changed

7 files changed

+61
-44
lines changed

docs/user-guide/documentation/pre_download_data.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ In addition, all pre-downloaded data must be split into separate files per times
2020
**Monthly data**: when using monthly data, ensure that your final .nc file download is for the month *after* your expedition schedule end date. This is to ensure that a Parcels FieldSet can be generated under-the-hood which fully covers the expedition period. For example, if your expedition runs from 1st May to 15th May, your final monthly data file should be in June. Daily data files only need to cover the expedition period exactly.
2121
```
2222

23+
```{note}
24+
**Argo and Drifter data**: if using Argo floats or Drifters in your expedition, ensure that: 1) the temporal extent of the downloaded data also accounts for the full *lifetime* of the instruments, not just the expedition period, and 2) the spatial bounds of the downloaded data also accounts for the likely drift distance of the instruments over their lifetimes. Otherwise, simulations will end prematurely (out-of-bounds errors) when the data runs out.
25+
```
26+
2327
Further, VirtualShip expects pre-downloaded data to be organised in a specific directory & filename structure within the specified local data directory. The expected structure is as outlined in the subsequent sections.
2428

2529
#### Directory structure

src/virtualship/cli/_plan.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,15 @@ def log_exception_to_file(
138138
{"name": "cycle_days"},
139139
{"name": "drift_days"},
140140
{"name": "stationkeeping_time", "minutes": True},
141-
{"name": "lifetime", "minutes": True},
141+
{"name": "lifetime", "days": True},
142142
],
143143
},
144144
"drifter_config": {
145145
"class": DrifterConfig,
146146
"title": "Drifter",
147147
"attributes": [
148148
{"name": "depth_meter"},
149-
{"name": "lifetime", "minutes": True},
149+
{"name": "lifetime", "days": True},
150150
{"name": "stationkeeping_time", "minutes": True},
151151
],
152152
},
@@ -264,7 +264,10 @@ def compose(self) -> ComposeResult:
264264
with Container(classes="instrument-config"):
265265
for attr_meta in attributes:
266266
attr = attr_meta["name"]
267-
is_minutes = attr_meta.get("minutes", False)
267+
is_minutes, is_days = (
268+
attr_meta.get("minutes", False),
269+
attr_meta.get("days", False),
270+
)
268271
validators = group_validators(config_class, attr)
269272
if config_instance:
270273
raw_value = getattr(config_instance, attr, "")
@@ -275,16 +278,23 @@ def compose(self) -> ComposeResult:
275278
)
276279
except AttributeError:
277280
value = str(raw_value)
281+
elif is_days and raw_value != "":
282+
try:
283+
value = str(
284+
raw_value.total_seconds() / 86400.0
285+
)
286+
except AttributeError:
287+
value = str(raw_value)
278288
else:
279289
value = str(raw_value)
280290
else:
281291
value = ""
282292
label = f"{attr.replace('_', ' ').title()}:"
283-
yield Label(
284-
label
285-
if not is_minutes
286-
else label.replace(":", " Minutes:")
287-
)
293+
if is_minutes:
294+
label = label.replace(":", " Minutes:")
295+
elif is_days:
296+
label = label.replace(":", " Days:")
297+
yield Label(label)
288298
yield Input(
289299
id=f"{instrument_name}_{attr}",
290300
type=type_to_textual(
@@ -392,11 +402,14 @@ def _update_instrument_configs(self):
392402
for attr_meta in attributes:
393403
attr = attr_meta["name"]
394404
is_minutes = attr_meta.get("minutes", False)
405+
is_days = attr_meta.get("days", False)
395406
input_id = f"{instrument_name}_{attr}"
396407
value = self.query_one(f"#{input_id}").value
397408
field_type = get_field_type(config_class, attr)
398409
if is_minutes and field_type is datetime.timedelta:
399410
value = datetime.timedelta(minutes=float(value))
411+
elif is_days and field_type is datetime.timedelta:
412+
value = datetime.timedelta(days=float(value))
400413
else:
401414
value = field_type(value)
402415
kwargs[attr] = value

src/virtualship/instruments/base.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,20 +149,17 @@ def _get_copernicus_ds(
149149
depth_max = self._get_spec_value("limit", "depth_max", None)
150150
spatial_constraint = self._get_spec_value("limit", "spatial", True)
151151

152+
min_lon_bound = self.min_lon - latlon_buffer if spatial_constraint else None
153+
max_lon_bound = self.max_lon + latlon_buffer if spatial_constraint else None
154+
min_lat_bound = self.min_lat - latlon_buffer if spatial_constraint else None
155+
max_lat_bound = self.max_lat + latlon_buffer if spatial_constraint else None
156+
152157
return copernicusmarine.open_dataset(
153158
dataset_id=product_id,
154-
minimum_longitude=self.min_lon - latlon_buffer
155-
if spatial_constraint
156-
else None,
157-
maximum_longitude=self.max_lon + latlon_buffer
158-
if spatial_constraint
159-
else None,
160-
minimum_latitude=self.min_lat - latlon_buffer
161-
if spatial_constraint
162-
else None,
163-
maximum_latitude=self.max_lat + latlon_buffer
164-
if spatial_constraint
165-
else None,
159+
minimum_longitude=min_lon_bound,
160+
maximum_longitude=max_lon_bound,
161+
minimum_latitude=min_lat_bound,
162+
maximum_latitude=max_lat_bound,
166163
variables=[var],
167164
start_datetime=self.min_time,
168165
end_datetime=self.max_time + timedelta(days=time_buffer),
@@ -182,8 +179,6 @@ def _generate_fieldset(self) -> FieldSet:
182179

183180
time_buffer = self._get_spec_value("buffer", "time", 0.0)
184181

185-
# TODO: also limit from-data to spatial domain?
186-
187182
for key in keys:
188183
var = self.variables[key]
189184
if self.from_data is not None: # load from local data

src/virtualship/models/expedition.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from virtualship.utils import (
1515
_get_bathy_data,
1616
_get_waypoint_latlons,
17-
_validate_numeric_mins_to_timedelta,
17+
_validate_numeric_to_timedelta,
1818
)
1919

2020
from .location import Location
@@ -214,8 +214,8 @@ class ArgoFloatConfig(pydantic.BaseModel):
214214
cycle_days: float = pydantic.Field(gt=0.0)
215215
drift_days: float = pydantic.Field(gt=0.0)
216216
lifetime: timedelta = pydantic.Field(
217-
serialization_alias="lifetime_minutes",
218-
validation_alias="lifetime_minutes",
217+
serialization_alias="lifetime_days",
218+
validation_alias="lifetime_days",
219219
gt=timedelta(),
220220
)
221221

@@ -227,19 +227,19 @@ class ArgoFloatConfig(pydantic.BaseModel):
227227

228228
@pydantic.field_serializer("lifetime")
229229
def _serialize_lifetime(self, value: timedelta, _info):
230-
return value.total_seconds() / 60.0
230+
return value.total_seconds() / 86400.0 # [days]
231231

232232
@pydantic.field_validator("lifetime", mode="before")
233233
def _validate_lifetime(cls, value: int | float | timedelta) -> timedelta:
234-
return _validate_numeric_mins_to_timedelta(value)
234+
return _validate_numeric_to_timedelta(value, "days")
235235

236236
@pydantic.field_serializer("stationkeeping_time")
237237
def _serialize_stationkeeping_time(self, value: timedelta, _info):
238238
return value.total_seconds() / 60.0
239239

240240
@pydantic.field_validator("stationkeeping_time", mode="before")
241241
def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta:
242-
return _validate_numeric_mins_to_timedelta(value)
242+
return _validate_numeric_to_timedelta(value, "minutes")
243243

244244
model_config = pydantic.ConfigDict(populate_by_name=True)
245245

@@ -263,7 +263,7 @@ def _serialize_period(self, value: timedelta, _info):
263263

264264
@pydantic.field_validator("period", mode="before")
265265
def _validate_period(cls, value: int | float | timedelta) -> timedelta:
266-
return _validate_numeric_mins_to_timedelta(value)
266+
return _validate_numeric_to_timedelta(value, "minutes")
267267

268268

269269
class CTDConfig(pydantic.BaseModel):
@@ -285,7 +285,7 @@ def _serialize_stationkeeping_time(self, value: timedelta, _info):
285285

286286
@pydantic.field_validator("stationkeeping_time", mode="before")
287287
def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta:
288-
return _validate_numeric_mins_to_timedelta(value)
288+
return _validate_numeric_to_timedelta(value, "minutes")
289289

290290

291291
class CTD_BGCConfig(pydantic.BaseModel):
@@ -307,7 +307,7 @@ def _serialize_stationkeeping_time(self, value: timedelta, _info):
307307

308308
@pydantic.field_validator("stationkeeping_time", mode="before")
309309
def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta:
310-
return _validate_numeric_mins_to_timedelta(value)
310+
return _validate_numeric_to_timedelta(value, "minutes")
311311

312312

313313
class ShipUnderwaterSTConfig(pydantic.BaseModel):
@@ -327,16 +327,16 @@ def _serialize_period(self, value: timedelta, _info):
327327

328328
@pydantic.field_validator("period", mode="before")
329329
def _validate_period(cls, value: int | float | timedelta) -> timedelta:
330-
return _validate_numeric_mins_to_timedelta(value)
330+
return _validate_numeric_to_timedelta(value, "minutes")
331331

332332

333333
class DrifterConfig(pydantic.BaseModel):
334334
"""Configuration for drifters."""
335335

336336
depth_meter: float = pydantic.Field(le=0.0)
337337
lifetime: timedelta = pydantic.Field(
338-
serialization_alias="lifetime_minutes",
339-
validation_alias="lifetime_minutes",
338+
serialization_alias="lifetime_days",
339+
validation_alias="lifetime_days",
340340
gt=timedelta(),
341341
)
342342
stationkeeping_time: timedelta = pydantic.Field(
@@ -349,19 +349,19 @@ class DrifterConfig(pydantic.BaseModel):
349349

350350
@pydantic.field_serializer("lifetime")
351351
def _serialize_lifetime(self, value: timedelta, _info):
352-
return value.total_seconds() / 60.0
352+
return value.total_seconds() / 86400.0 # [days]
353353

354354
@pydantic.field_validator("lifetime", mode="before")
355355
def _validate_lifetime(cls, value: int | float | timedelta) -> timedelta:
356-
return _validate_numeric_mins_to_timedelta(value)
356+
return _validate_numeric_to_timedelta(value, "days")
357357

358358
@pydantic.field_serializer("stationkeeping_time")
359359
def _serialize_stationkeeping_time(self, value: timedelta, _info):
360360
return value.total_seconds() / 60.0
361361

362362
@pydantic.field_validator("stationkeeping_time", mode="before")
363363
def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta:
364-
return _validate_numeric_mins_to_timedelta(value)
364+
return _validate_numeric_to_timedelta(value, "minutes")
365365

366366

367367
class XBTConfig(pydantic.BaseModel):

src/virtualship/static/expedition.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ instruments_config:
4343
min_depth_meter: 0.0
4444
vertical_speed_meter_per_second: -0.1
4545
stationkeeping_time_minutes: 20.0
46-
lifetime_minutes: 90720.0
46+
lifetime_days: 63.0
4747
ctd_config:
4848
max_depth_meter: -2000.0
4949
min_depth_meter: -11.0
@@ -54,7 +54,7 @@ instruments_config:
5454
stationkeeping_time_minutes: 50.0
5555
drifter_config:
5656
depth_meter: -1.0
57-
lifetime_minutes: 60480.0
57+
lifetime_days: 42.0
5858
stationkeeping_time_minutes: 20.0
5959
xbt_config:
6060
max_depth_meter: -285.0

src/virtualship/utils.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,16 @@ def mfp_to_yaml(coordinates_file_path: str, yaml_output_path: str): # noqa: D41
186186
expedition.to_yaml(yaml_output_path)
187187

188188

189-
def _validate_numeric_mins_to_timedelta(value: int | float | timedelta) -> timedelta:
190-
"""Convert minutes to timedelta when reading."""
189+
def _validate_numeric_to_timedelta(
190+
value: int | float | timedelta, unit: str
191+
) -> timedelta:
192+
"""Convert to timedelta when reading."""
191193
if isinstance(value, timedelta):
192194
return value
193-
return timedelta(minutes=value)
195+
if unit == "minutes":
196+
return timedelta(minutes=float(value))
197+
elif unit == "days":
198+
return timedelta(days=float(value))
194199

195200

196201
def _get_expedition(expedition_dir: Path) -> Expedition:

tests/expedition/expedition_dir/expedition.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ instruments_config:
3030
min_depth_meter: 0.0
3131
vertical_speed_meter_per_second: -0.1
3232
stationkeeping_time_minutes: 20.0
33-
lifetime_minutes: 90720.0
33+
lifetime_days: 63.0
3434
ctd_config:
3535
max_depth_meter: -2000.0
3636
min_depth_meter: -11.0
@@ -41,7 +41,7 @@ instruments_config:
4141
stationkeeping_time_minutes: 50.0
4242
drifter_config:
4343
depth_meter: -1.0
44-
lifetime_minutes: 40320.0
44+
lifetime_days: 28.0
4545
stationkeeping_time_minutes: 20.0
4646
ship_underwater_st_config:
4747
period_minutes: 5.0

0 commit comments

Comments
 (0)