diff --git a/src/virtualship/cli/_fetch.py b/src/virtualship/cli/_fetch.py index 2e09dec2..f07c5b92 100644 --- a/src/virtualship/cli/_fetch.py +++ b/src/virtualship/cli/_fetch.py @@ -87,7 +87,7 @@ def _fetch(path: str | Path, username: str | None, password: str | None) -> None if ( ( - {"XBT", "CTD", "SHIP_UNDERWATER_ST"} + {"XBT", "CTD", "CDT_BGC", "SHIP_UNDERWATER_ST"} & set(instrument.name for instrument in instruments_in_schedule) ) or ship_config.ship_underwater_st_config is not None @@ -144,7 +144,6 @@ def _fetch(path: str | Path, username: str | None, password: str | None) -> None shutil.rmtree(download_folder) raise e - complete_download(download_folder) click.echo("Ship data download based on space-time region completed.") if InstrumentType.DRIFTER in instruments_in_schedule: @@ -187,7 +186,6 @@ def _fetch(path: str | Path, username: str | None, password: str | None) -> None shutil.rmtree(download_folder) raise e - complete_download(download_folder) click.echo("Drifter data download based on space-time region completed.") if InstrumentType.ARGO_FLOAT in instruments_in_schedule: @@ -235,9 +233,53 @@ def _fetch(path: str | Path, username: str | None, password: str | None) -> None shutil.rmtree(download_folder) raise e - complete_download(download_folder) click.echo("Argo_float data download based on space-time region completed.") + if InstrumentType.CTD_BGC in instruments_in_schedule: + print("CTD_BGC data will be downloaded. Please wait...") + + ctd_bgc_download_dict = { + "o2data": { + "dataset_id": "cmems_mod_glo_bgc-bio_anfc_0.25deg_P1D-m", + "variables": ["o2"], + "output_filename": "ctd_bgc_o2.nc", + }, + "chlorodata": { + "dataset_id": "cmems_mod_glo_bgc-pft_anfc_0.25deg_P1D-m", + "variables": ["chl"], + "output_filename": "ctd_bgc_chloro.nc", + }, + } + + # Iterate over all datasets and download each based on space_time_region + try: + for dataset in ctd_bgc_download_dict.values(): + copernicusmarine.subset( + dataset_id=dataset["dataset_id"], + variables=dataset["variables"], + minimum_longitude=spatial_range.minimum_longitude - 3.0, + maximum_longitude=spatial_range.maximum_longitude + 3.0, + minimum_latitude=spatial_range.minimum_latitude - 3.0, + maximum_latitude=spatial_range.maximum_latitude + 3.0, + start_datetime=start_datetime, + end_datetime=end_datetime + timedelta(days=21), + minimum_depth=abs(1), + maximum_depth=abs(spatial_range.maximum_depth), + output_filename=dataset["output_filename"], + output_directory=download_folder, + username=username, + password=password, + overwrite=True, + coordinates_selection_method="outside", + ) + except InvalidUsernameOrPassword as e: + shutil.rmtree(download_folder) + raise e + + click.echo("CTD_BGC data download based on space-time region completed.") + + complete_download(download_folder) + def _hash(s: str, *, length: int) -> str: """Create a hash of a string.""" diff --git a/src/virtualship/expedition/ship_config.py b/src/virtualship/expedition/ship_config.py index 4f238b36..c0814f5b 100644 --- a/src/virtualship/expedition/ship_config.py +++ b/src/virtualship/expedition/ship_config.py @@ -17,9 +17,10 @@ class InstrumentType(Enum): - """Types of instruments.""" + """Types of the instruments.""" CTD = "CTD" + CTD_BGC = "CTD_BGC" DRIFTER = "DRIFTER" ARGO_FLOAT = "ARGO_FLOAT" XBT = "XBT" @@ -80,6 +81,28 @@ def _validate_stationkeeping_time(cls, value: int | float | timedelta) -> timede return _validate_numeric_mins_to_timedelta(value) +class CTD_BGCConfig(pydantic.BaseModel): + """Configuration for CTD_BGC instrument.""" + + stationkeeping_time: timedelta = pydantic.Field( + serialization_alias="stationkeeping_time_minutes", + validation_alias="stationkeeping_time_minutes", + gt=timedelta(), + ) + min_depth_meter: float = pydantic.Field(le=0.0) + max_depth_meter: float = pydantic.Field(le=0.0) + + model_config = pydantic.ConfigDict(populate_by_name=True) + + @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 ShipUnderwaterSTConfig(pydantic.BaseModel): """Configuration for underwater ST.""" @@ -159,6 +182,13 @@ class ShipConfig(pydantic.BaseModel): If None, no CTDs can be cast. """ + ctd_bgc_config: CTD_BGCConfig | None = None + """ + CTD_BGC configuration. + + If None, no BGC CTDs can be cast. + """ + ship_underwater_st_config: ShipUnderwaterSTConfig | None = None """ Ship underwater salinity temperature measurementconfiguration. @@ -239,6 +269,7 @@ def verify(self, schedule: Schedule) -> None: "DRIFTER", "XBT", "CTD", + "CTD_BGC", ]: # TODO make instrument names consistent capitals or lowercase throughout codebase if hasattr(self, instrument.lower() + "_config") and not any( instrument == schedule_instrument.name @@ -248,6 +279,7 @@ def verify(self, schedule: Schedule) -> None: setattr(self, instrument.lower() + "_config", None) # verify instruments in schedule have configuration + # TODO: the ConfigError message could be improved to explain that the **schedule** file has X instrument but the **ship_config** file does not for instrument in instruments_in_schedule: try: InstrumentType(instrument) @@ -266,6 +298,12 @@ def verify(self, schedule: Schedule) -> None: raise ConfigError( "Planning has a waypoint with CTD instrument, but configuration does not configure CTDs." ) + if instrument == InstrumentType.CTD_BGC and ( + not hasattr(self, "ctd_bgc_config") or self.ctd_bgc_config is None + ): + raise ConfigError( + "Planning has a waypoint with CTD_BGC instrument, but configuration does not configure CTD_BGCs." + ) if instrument == InstrumentType.DRIFTER and ( not hasattr(self, "drifter_config") or self.drifter_config is None ): diff --git a/src/virtualship/static/ship_config.yaml b/src/virtualship/static/ship_config.yaml index 5066e38d..34d6c6ea 100644 --- a/src/virtualship/static/ship_config.yaml +++ b/src/virtualship/static/ship_config.yaml @@ -14,6 +14,10 @@ ctd_config: max_depth_meter: -2000.0 min_depth_meter: -11.0 stationkeeping_time_minutes: 20.0 +ctd_bgc_config: + max_depth_meter: -2000.0 + min_depth_meter: -11.0 + stationkeeping_time_minutes: 20.0 drifter_config: depth_meter: 0.0 lifetime_minutes: 60480.0 diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 1d3203de..6dbbb49c 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -156,6 +156,7 @@ def mfp_to_yaml(coordinates_file_path: str, yaml_output_path: str): # noqa: D41 instrument_max_depths = { "XBT": 2000, "CTD": 5000, + "CTD_BGC": 5000, "DRIFTER": 1, "ARGO_FLOAT": 2000, }