Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies:
- pyyaml
- copernicusmarine >= 2
- openpyxl
- yaspin

# linting
- pre-commit
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"pydantic >=2, <3",
"PyYAML",
"copernicusmarine >= 2",
"yaspin",
]

[project.urls]
Expand Down
24 changes: 19 additions & 5 deletions src/virtualship/expedition/do_expedition.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
:param expedition_dir: The base directory for the expedition.
:param input_data: Input data folder (override used for testing).
"""
print("\n╔═════════════════════════════════════════════════╗")
print("║ VIRTUALSHIP EXPEDITION STATUS ║")
print("╚═════════════════════════════════════════════════╝")

if isinstance(expedition_dir, str):
expedition_dir = Path(expedition_dir)

Expand All @@ -56,6 +60,8 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
input_data=input_data,
)

print("\n---- WAYPOINT VERIFICATION ----")

# verify schedule is valid
schedule.verify(ship_config.ship_speed_knots, loaded_input_data)

Expand All @@ -82,6 +88,8 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
shutil.rmtree(expedition_dir.joinpath("results"))
os.makedirs(expedition_dir.joinpath("results"))

print("\n----- EXPEDITION SUMMARY ------")

# calculate expedition cost in US$
assert schedule.waypoints[0].time is not None, (
"First waypoint has no time. This should not be possible as it should have been verified before."
Expand All @@ -90,20 +98,26 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
cost = expedition_cost(schedule_results, time_past)
with open(expedition_dir.joinpath("results", "cost.txt"), "w") as file:
file.writelines(f"cost: {cost} US$")
print(f"This expedition took {time_past} and would have cost {cost:,.0f} US$.")
print(f"\nExpedition duration: {time_past}\nExpedition cost: US$ {cost:,.0f}.")

print("\n--- MEASUREMENT SIMULATIONS ---")

# simulate measurements
print("Simulating measurements. This may take a while..")
print("\nSimulating measurements. This may take a while...\n")
simulate_measurements(
expedition_dir,
ship_config,
loaded_input_data,
schedule_results.measurements_to_simulate,
)
print("Done simulating measurements.")
print("\nAll measurement simulations are complete.")

print("Your expedition has concluded successfully!")
print("Your measurements can be found in the results directory.")
print("\n----- EXPEDITION RESULTS ------")
print("\nYour expedition has concluded successfully!")
print(
f"Your measurements can be found in the '{expedition_dir}/results' directory."
)
print("\n------------- END -------------\n")


def _load_input_data(
Expand Down
115 changes: 70 additions & 45 deletions src/virtualship/expedition/simulate_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from __future__ import annotations

import logging
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING

from yaspin import yaspin

from virtualship.instruments.adcp import simulate_adcp
from virtualship.instruments.argo_float import simulate_argo_floats
from virtualship.instruments.ctd import simulate_ctd
Expand All @@ -14,12 +17,17 @@
from virtualship.instruments.ship_underwater_st import simulate_ship_underwater_st
from virtualship.instruments.xbt import simulate_xbt
from virtualship.models import ShipConfig
from virtualship.utils import ship_spinner

from .simulate_schedule import MeasurementsToSimulate

if TYPE_CHECKING:
from .input_data import InputData

# parcels logger (suppress INFO messages to prevent log being flooded)
external_logger = logging.getLogger("parcels.tools.loggers")
external_logger.setLevel(logging.WARNING)


def simulate_measurements(
expedition_dir: str | Path,
Expand All @@ -42,61 +50,91 @@ def simulate_measurements(
expedition_dir = Path(expedition_dir)

if len(measurements.ship_underwater_sts) > 0:
print("Simulating onboard salinity and temperature measurements.")
if ship_config.ship_underwater_st_config is None:
raise RuntimeError("No configuration for ship underwater ST provided.")
if input_data.ship_underwater_st_fieldset is None:
raise RuntimeError("No fieldset for ship underwater ST provided.")
simulate_ship_underwater_st(
fieldset=input_data.ship_underwater_st_fieldset,
out_path=expedition_dir.joinpath("results", "ship_underwater_st.zarr"),
depth=-2,
sample_points=measurements.ship_underwater_sts,
)
with yaspin(
text="Simulating onboard temperature and salinity measurements... ",
side="right",
spinner=ship_spinner,
) as spinner:
simulate_ship_underwater_st(
fieldset=input_data.ship_underwater_st_fieldset,
out_path=expedition_dir.joinpath("results", "ship_underwater_st.zarr"),
depth=-2,
sample_points=measurements.ship_underwater_sts,
)
spinner.ok("✅")

if len(measurements.adcps) > 0:
print("Simulating onboard ADCP.")
if ship_config.adcp_config is None:
raise RuntimeError("No configuration for ADCP provided.")
if input_data.adcp_fieldset is None:
raise RuntimeError("No fieldset for ADCP provided.")
simulate_adcp(
fieldset=input_data.adcp_fieldset,
out_path=expedition_dir.joinpath("results", "adcp.zarr"),
max_depth=ship_config.adcp_config.max_depth_meter,
min_depth=-5,
num_bins=ship_config.adcp_config.num_bins,
sample_points=measurements.adcps,
)
with yaspin(
text="Simulating onboard ADCP... ", side="right", spinner=ship_spinner
) as spinner:
simulate_adcp(
fieldset=input_data.adcp_fieldset,
out_path=expedition_dir.joinpath("results", "adcp.zarr"),
max_depth=ship_config.adcp_config.max_depth_meter,
min_depth=-5,
num_bins=ship_config.adcp_config.num_bins,
sample_points=measurements.adcps,
)
spinner.ok("✅")

if len(measurements.ctds) > 0:
print("Simulating CTD casts.")
if ship_config.ctd_config is None:
raise RuntimeError("No configuration for CTD provided.")
if input_data.ctd_fieldset is None:
raise RuntimeError("No fieldset for CTD provided.")
simulate_ctd(
out_path=expedition_dir.joinpath("results", "ctd.zarr"),
fieldset=input_data.ctd_fieldset,
ctds=measurements.ctds,
outputdt=timedelta(seconds=10),
)
with yaspin(
text="Simulating CTD casts... ", side="right", spinner=ship_spinner
) as spinner:
simulate_ctd(
out_path=expedition_dir.joinpath("results", "ctd.zarr"),
fieldset=input_data.ctd_fieldset,
ctds=measurements.ctds,
outputdt=timedelta(seconds=10),
)
spinner.ok("✅")

if len(measurements.ctd_bgcs) > 0:
print("Simulating BGC CTD casts.")
if ship_config.ctd_bgc_config is None:
raise RuntimeError("No configuration for CTD_BGC provided.")
if input_data.ctd_bgc_fieldset is None:
raise RuntimeError("No fieldset for CTD_BGC provided.")
simulate_ctd_bgc(
out_path=expedition_dir.joinpath("results", "ctd_bgc.zarr"),
fieldset=input_data.ctd_bgc_fieldset,
ctd_bgcs=measurements.ctd_bgcs,
outputdt=timedelta(seconds=10),
)
with yaspin(
text="Simulating BGC CTD casts... ", side="right", spinner=ship_spinner
) as spinner:
simulate_ctd_bgc(
out_path=expedition_dir.joinpath("results", "ctd_bgc.zarr"),
fieldset=input_data.ctd_bgc_fieldset,
ctd_bgcs=measurements.ctd_bgcs,
outputdt=timedelta(seconds=10),
)
spinner.ok("✅")

if len(measurements.xbts) > 0:
if ship_config.xbt_config is None:
raise RuntimeError("No configuration for XBTs provided.")
if input_data.xbt_fieldset is None:
raise RuntimeError("No fieldset for XBTs provided.")
with yaspin(
text="Simulating XBTs... ", side="right", spinner=ship_spinner
) as spinner:
simulate_xbt(
out_path=expedition_dir.joinpath("results", "xbts.zarr"),
fieldset=input_data.xbt_fieldset,
xbts=measurements.xbts,
outputdt=timedelta(seconds=1),
)
spinner.ok("✅")

if len(measurements.drifters) > 0:
print("Simulating drifters")
print("Simulating drifters... ")
if ship_config.drifter_config is None:
raise RuntimeError("No configuration for drifters provided.")
if input_data.drifter_fieldset is None:
Expand All @@ -111,7 +149,7 @@ def simulate_measurements(
)

if len(measurements.argo_floats) > 0:
print("Simulating argo floats")
print("Simulating argo floats... ")
if ship_config.argo_float_config is None:
raise RuntimeError("No configuration for argo floats provided.")
if input_data.argo_float_fieldset is None:
Expand All @@ -123,16 +161,3 @@ def simulate_measurements(
outputdt=timedelta(minutes=5),
endtime=None,
)

if len(measurements.xbts) > 0:
print("Simulating XBTs")
if ship_config.xbt_config is None:
raise RuntimeError("No configuration for XBTs provided.")
if input_data.xbt_fieldset is None:
raise RuntimeError("No fieldset for XBTs provided.")
simulate_xbt(
out_path=expedition_dir.joinpath("results", "xbts.zarr"),
fieldset=input_data.xbt_fieldset,
xbts=measurements.xbts,
outputdt=timedelta(seconds=1),
)
12 changes: 10 additions & 2 deletions src/virtualship/instruments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"""Measurement instrument that can be used with Parcels."""

from . import adcp, argo_float, ctd, drifter, ship_underwater_st, xbt
from . import adcp, argo_float, ctd, ctd_bgc, drifter, ship_underwater_st, xbt

__all__ = ["adcp", "argo_float", "ctd", "drifter", "ship_underwater_st", "xbt"]
__all__ = [
"adcp",
"argo_float",
"ctd",
"ctd_bgc",
"drifter",
"ship_underwater_st",
"xbt",
]
7 changes: 4 additions & 3 deletions src/virtualship/models/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def verify(
:raises NotImplementedError: If an instrument in the schedule is not implemented.
:return: None. The method doesn't return a value but raises exceptions if verification fails.
"""
print("\nVerifying route... ")

if check_space_time_region and self.space_time_region is None:
raise ScheduleError(
"space_time_region not found in schedule, please define it to fetch the data."
Expand All @@ -145,8 +147,6 @@ def verify(
# check if all waypoints are in water
# this is done by picking an arbitrary provided fieldset and checking if UV is not zero

print("Verifying all waypoints are on water..")

# get all available fieldsets
available_fieldsets = []
if input_data is not None:
Expand Down Expand Up @@ -184,7 +184,6 @@ def verify(
raise ScheduleError(
f"The following waypoints are on land: {['#' + str(wp_i) + ' ' + str(wp) for (wp_i, wp) in land_waypoints]}"
)
print("Good, all waypoints are on water.")

# check that ship will arrive on time at each waypoint (in case no unexpected event happen)
time = self.waypoints[0].time
Expand Down Expand Up @@ -215,6 +214,8 @@ def verify(
else:
time = wp_next.time

print("... All good to go!")


def _is_on_land_zero_uv(fieldset: FieldSet, waypoint: Waypoint) -> bool:
"""
Expand Down
20 changes: 20 additions & 0 deletions src/virtualship/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, TextIO

from yaspin import Spinner

if TYPE_CHECKING:
from virtualship.models import Schedule, ShipConfig

Expand Down Expand Up @@ -247,3 +249,21 @@ def _get_ship_config(expedition_dir: Path) -> ShipConfig:
raise FileNotFoundError(
f'Ship config not found. Save it to "{file_path}".'
) from e


# custom ship spinner
ship_spinner = Spinner(
interval=240,
frames=[
" 🚢 ",
" 🚢 ",
" 🚢 ",
" 🚢 ",
" 🚢",
" 🚢 ",
" 🚢 ",
" 🚢 ",
" 🚢 ",
"🚢 ",
],
)
4 changes: 3 additions & 1 deletion tests/expedition/test_do_expedition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
def test_do_expedition(capfd: CaptureFixture) -> None:
do_expedition("expedition_dir", input_data=Path("expedition_dir/input_data"))
out, _ = capfd.readouterr()
assert "This expedition took" in out, "Expedition did not complete successfully."
assert "Your expedition has concluded successfully!" in out, (
"Expedition did not complete successfully."
)
Loading