diff --git a/src/virtualship/cli/_plan.py b/src/virtualship/cli/_plan.py index 8b2adb15..08845e0d 100644 --- a/src/virtualship/cli/_plan.py +++ b/src/virtualship/cli/_plan.py @@ -141,6 +141,7 @@ def log_exception_to_file( {"name": "vertical_speed_meter_per_second"}, {"name": "cycle_days"}, {"name": "drift_days"}, + {"name": "stationkeeping_time", "minutes": True}, ], }, "drifter_config": { @@ -149,6 +150,7 @@ def log_exception_to_file( "attributes": [ {"name": "depth_meter"}, {"name": "lifetime", "minutes": True}, + {"name": "stationkeeping_time", "minutes": True}, ], }, } diff --git a/src/virtualship/expedition/simulate_schedule.py b/src/virtualship/expedition/simulate_schedule.py index 0a567b1c..e450fcc7 100644 --- a/src/virtualship/expedition/simulate_schedule.py +++ b/src/virtualship/expedition/simulate_schedule.py @@ -255,6 +255,12 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta: # time costs of each measurement time_costs = [timedelta()] + # check if both CTD and CTD_BGC are present + # TODO: this can be avoided if CTD and CTD_BGC are merged into a single instrument + both_ctd_and_bgc = ( + InstrumentType.CTD in instruments and InstrumentType.CTD_BGC in instruments + ) + for instrument in instruments: if instrument is InstrumentType.ARGO_FLOAT: self._measurements_to_simulate.argo_floats.append( @@ -268,6 +274,7 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta: drift_days=self._expedition.instruments_config.argo_float_config.drift_days, ) ) + elif instrument is InstrumentType.CTD: self._measurements_to_simulate.ctds.append( CTD( @@ -287,9 +294,12 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta: max_depth=self._expedition.instruments_config.ctd_bgc_config.max_depth_meter, ) ) - time_costs.append( - self._expedition.instruments_config.ctd_bgc_config.stationkeeping_time - ) + if both_ctd_and_bgc: # only need to add time cost once if both CTD and CTD_BGC are being taken; in reality they would be done on the same instrument + pass + else: + time_costs.append( + self._expedition.instruments_config.ctd_bgc_config.stationkeeping_time + ) elif instrument is InstrumentType.DRIFTER: self._measurements_to_simulate.drifters.append( Drifter( diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py index 4847b10c..e6e80102 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -215,6 +215,22 @@ class ArgoFloatConfig(pydantic.BaseModel): cycle_days: float = pydantic.Field(gt=0.0) drift_days: float = pydantic.Field(gt=0.0) + stationkeeping_time: timedelta = pydantic.Field( + serialization_alias="stationkeeping_time_minutes", + validation_alias="stationkeeping_time_minutes", + gt=timedelta(), + ) + + @pydantic.field_serializer("stationkeeping_time") + def _serialize_stationkeeping_time(self, value: timedelta, _info): + return value.total_seconds() / 60.0 + + @pydantic.field_validator("stationkeeping_time", mode="before") + def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta: + return _validate_numeric_mins_to_timedelta(value) + + model_config = pydantic.ConfigDict(populate_by_name=True) + class ADCPConfig(pydantic.BaseModel): """Configuration for ADCP instrument.""" @@ -311,6 +327,11 @@ class DrifterConfig(pydantic.BaseModel): validation_alias="lifetime_minutes", gt=timedelta(), ) + stationkeeping_time: timedelta = pydantic.Field( + serialization_alias="stationkeeping_time_minutes", + validation_alias="stationkeeping_time_minutes", + gt=timedelta(), + ) model_config = pydantic.ConfigDict(populate_by_name=True) @@ -322,6 +343,14 @@ def _serialize_lifetime(self, value: timedelta, _info): def _validate_lifetime(cls, value: int | float | timedelta) -> timedelta: return _validate_numeric_mins_to_timedelta(value) + @pydantic.field_serializer("stationkeeping_time") + def _serialize_stationkeeping_time(self, value: timedelta, _info): + return value.total_seconds() / 60.0 + + @pydantic.field_validator("stationkeeping_time", mode="before") + def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timedelta: + return _validate_numeric_mins_to_timedelta(value) + class XBTConfig(pydantic.BaseModel): """Configuration for xbt instrument.""" diff --git a/src/virtualship/static/expedition.yaml b/src/virtualship/static/expedition.yaml index 2b770735..1c154b01 100644 --- a/src/virtualship/static/expedition.yaml +++ b/src/virtualship/static/expedition.yaml @@ -53,17 +53,19 @@ instruments_config: max_depth_meter: -2000.0 min_depth_meter: 0.0 vertical_speed_meter_per_second: -0.1 + stationkeeping_time_minutes: 20.0 ctd_config: max_depth_meter: -2000.0 min_depth_meter: -11.0 - stationkeeping_time_minutes: 20.0 + stationkeeping_time_minutes: 50.0 ctd_bgc_config: max_depth_meter: -2000.0 min_depth_meter: -11.0 - stationkeeping_time_minutes: 20.0 + stationkeeping_time_minutes: 50.0 drifter_config: depth_meter: -1.0 lifetime_minutes: 60480.0 + stationkeeping_time_minutes: 20.0 xbt_config: max_depth_meter: -285.0 min_depth_meter: -2.0 diff --git a/tests/expedition/expedition_dir/expedition.yaml b/tests/expedition/expedition_dir/expedition.yaml index 0ed9e5f4..cd3f532a 100644 --- a/tests/expedition/expedition_dir/expedition.yaml +++ b/tests/expedition/expedition_dir/expedition.yaml @@ -29,17 +29,19 @@ instruments_config: max_depth_meter: -2000.0 min_depth_meter: 0.0 vertical_speed_meter_per_second: -0.1 + stationkeeping_time_minutes: 20.0 ctd_config: max_depth_meter: -2000.0 min_depth_meter: -11.0 - stationkeeping_time_minutes: 20.0 + stationkeeping_time_minutes: 50.0 ctd_bgc_config: max_depth_meter: -2000.0 min_depth_meter: -11.0 - stationkeeping_time_minutes: 20.0 + stationkeeping_time_minutes: 50.0 drifter_config: depth_meter: -1.0 lifetime_minutes: 40320.0 + stationkeeping_time_minutes: 20.0 ship_underwater_st_config: period_minutes: 5.0 ship_config: diff --git a/tests/expedition/test_simulate_schedule.py b/tests/expedition/test_simulate_schedule.py index bad8c9ad..ff5a3597 100644 --- a/tests/expedition/test_simulate_schedule.py +++ b/tests/expedition/test_simulate_schedule.py @@ -54,8 +54,14 @@ def test_time_in_minutes_in_ship_schedule() -> None: "expedition_dir/expedition.yaml" ).instruments_config assert instruments_config.adcp_config.period == timedelta(minutes=5) - assert instruments_config.ctd_config.stationkeeping_time == timedelta(minutes=20) + assert instruments_config.ctd_config.stationkeeping_time == timedelta(minutes=50) assert instruments_config.ctd_bgc_config.stationkeeping_time == timedelta( + minutes=50 + ) + assert instruments_config.argo_float_config.stationkeeping_time == timedelta( + minutes=20 + ) + assert instruments_config.drifter_config.stationkeeping_time == timedelta( minutes=20 ) assert instruments_config.ship_underwater_st_config.period == timedelta(minutes=5)