Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 55 additions & 11 deletions src/virtualship/cli/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def _fetch(path: str | Path, username: str | None, password: str | None) -> None
be provided on prompt, via command line arguments, or via a YAML config file. Run
`virtualship fetch` on an expedition for more info.
"""
from virtualship.expedition.ship_config import InstrumentType

if sum([username is None, password is None]) == 1:
raise ValueError("Both username and password must be provided when using CLI.")

Expand Down Expand Up @@ -85,13 +87,13 @@ 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 hasattr(ship_config, "ship_underwater_st_config")
or hasattr(ship_config, "adcp_config")
or ship_config.ship_underwater_st_config is not None
or ship_config.adcp_config is not None
):
print("Ship data will be downloaded")
print("Ship data will be downloaded. Please wait...")

# Define all ship datasets to download, including bathymetry
download_dict = {
Expand Down Expand Up @@ -142,11 +144,10 @@ 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 "DRIFTER" in instruments_in_schedule:
print("Drifter data will be downloaded")
if InstrumentType.DRIFTER in instruments_in_schedule:
print("Drifter data will be downloaded. Please wait...")
drifter_download_dict = {
"UVdata": {
"dataset_id": "cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i",
Expand Down Expand Up @@ -185,11 +186,10 @@ 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 "ARGO_FLOAT" in instruments_in_schedule:
print("Argo float data will be downloaded")
if InstrumentType.ARGO_FLOAT in instruments_in_schedule:
print("Argo float data will be downloaded. Please wait...")
argo_download_dict = {
"UVdata": {
"dataset_id": "cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i",
Expand Down Expand Up @@ -233,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.")

Comment thread
VeckoTheGecko marked this conversation as resolved.
complete_download(download_folder)


def _hash(s: str, *, length: int) -> str:
"""Create a hash of a string."""
Expand Down
4 changes: 1 addition & 3 deletions src/virtualship/expedition/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

from .do_expedition import do_expedition
from .input_data import InputData
from .instrument_type import InstrumentType
from .schedule import Schedule
from .schedule import Schedule, Waypoint
from .ship_config import (
ADCPConfig,
ArgoFloatConfig,
Expand All @@ -13,7 +12,6 @@
ShipUnderwaterSTConfig,
)
from .space_time_region import SpaceTimeRegion
from .waypoint import Waypoint

__all__ = [
"ADCPConfig",
Expand Down
2 changes: 1 addition & 1 deletion src/virtualship/expedition/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import pydantic
import yaml

from .instrument_type import InstrumentType
from .schedule import Schedule
from .ship_config import InstrumentType


class _YamlDumper(yaml.SafeDumper):
Expand Down
12 changes: 0 additions & 12 deletions src/virtualship/expedition/instrument_type.py

This file was deleted.

21 changes: 18 additions & 3 deletions src/virtualship/expedition/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,37 @@
from __future__ import annotations

import itertools
from datetime import timedelta
from datetime import datetime, timedelta
from pathlib import Path

import pydantic
import pyproj
import yaml
from parcels import FieldSet

from ..location import Location
from .input_data import InputData
from .instrument_type import InstrumentType
from .ship_config import InstrumentType
from .space_time_region import SpaceTimeRegion
from .waypoint import Waypoint

projection: pyproj.Geod = pyproj.Geod(ellps="WGS84")


class Waypoint(pydantic.BaseModel):
"""A Waypoint to sail to with an optional time and an optional instrument."""

location: Location
time: datetime | None = None
instrument: InstrumentType | list[InstrumentType] | None = None

@pydantic.field_serializer("instrument")
def serialize_instrument(self, instrument):
"""Ensure InstrumentType is serialized as a string (or list of strings)."""
if isinstance(instrument, list):
return [inst.value for inst in instrument]
return instrument.value if instrument else None


class Schedule(pydantic.BaseModel):
"""Schedule of the virtual ship."""

Expand Down
53 changes: 51 additions & 2 deletions src/virtualship/expedition/ship_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@
from __future__ import annotations

from datetime import timedelta
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING

import pydantic
import yaml

from virtualship.utils import _validate_numeric_mins_to_timedelta

from .instrument_type import InstrumentType
from .schedule import Schedule
if TYPE_CHECKING:
from .schedule import Schedule


class InstrumentType(Enum):
"""Types of instruments."""

CTD = "CTD"
CTD_BGC = "CTD_BGC"
DRIFTER = "DRIFTER"
ARGO_FLOAT = "ARGO_FLOAT"
XBT = "XBT"


class ArgoFloatConfig(pydantic.BaseModel):
Expand Down Expand Up @@ -69,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."""

Expand Down Expand Up @@ -148,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.
Expand Down Expand Up @@ -228,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
Expand All @@ -237,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)
Expand All @@ -255,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
):
Expand Down
6 changes: 2 additions & 4 deletions src/virtualship/expedition/simulate_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@
from ..instruments.xbt import XBT
from ..location import Location
from ..spacetime import Spacetime
from .instrument_type import InstrumentType
from .schedule import Schedule
from .ship_config import ShipConfig
from .waypoint import Waypoint
from .schedule import Schedule, Waypoint
from .ship_config import InstrumentType, ShipConfig


@dataclass
Expand Down
23 changes: 0 additions & 23 deletions src/virtualship/expedition/waypoint.py

This file was deleted.

4 changes: 4 additions & 0 deletions src/virtualship/static/ship_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/virtualship/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,13 @@ def mfp_to_yaml(coordinates_file_path: str, yaml_output_path: str): # noqa: D41

"""
# Importing Schedule and related models from expedition module
from virtualship.expedition.instrument_type import InstrumentType
from virtualship.expedition.schedule import Schedule
from virtualship.expedition.schedule import Location, Schedule, Waypoint
from virtualship.expedition.ship_config import InstrumentType
from virtualship.expedition.space_time_region import (
SpaceTimeRegion,
SpatialRange,
TimeRange,
)
from virtualship.expedition.waypoint import Location, Waypoint

# Read data from file
coordinates_data = load_coordinates(coordinates_file_path)
Expand All @@ -157,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,
}
Expand Down
3 changes: 1 addition & 2 deletions tests/expedition/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import pytest

from virtualship import Location
from virtualship.expedition import Waypoint
from virtualship.expedition.do_expedition import _load_input_data
from virtualship.expedition.schedule import Schedule, ScheduleError
from virtualship.expedition.schedule import Schedule, ScheduleError, Waypoint
from virtualship.utils import _get_ship_config

projection = pyproj.Geod(ellps="WGS84")
Expand Down
2 changes: 1 addition & 1 deletion tests/test_mfp_to_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import pandas as pd
import pytest

from virtualship.expedition.instrument_type import InstrumentType
from virtualship.expedition.schedule import Schedule
from virtualship.expedition.ship_config import InstrumentType
from virtualship.utils import mfp_to_yaml


Expand Down