Skip to content

Commit 56424ed

Browse files
authored
Updated logging (informative log messages, spinners for progress, suppressing Parcels INFO messages) (#191)
* add ctd_bgc to list * new log output messages * add yaspin as dependency * add ship spinner to log messages * update test to reflect new log output
1 parent d963b2d commit 56424ed

File tree

8 files changed

+128
-56
lines changed

8 files changed

+128
-56
lines changed

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies:
1313
- pyyaml
1414
- copernicusmarine >= 2
1515
- openpyxl
16+
- yaspin
1617

1718
# linting
1819
- pre-commit

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"pydantic >=2, <3",
3535
"PyYAML",
3636
"copernicusmarine >= 2",
37+
"yaspin",
3738
]
3839

3940
[project.urls]

src/virtualship/expedition/do_expedition.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
3131
:param expedition_dir: The base directory for the expedition.
3232
:param input_data: Input data folder (override used for testing).
3333
"""
34+
print("\n╔═════════════════════════════════════════════════╗")
35+
print("║ VIRTUALSHIP EXPEDITION STATUS ║")
36+
print("╚═════════════════════════════════════════════════╝")
37+
3438
if isinstance(expedition_dir, str):
3539
expedition_dir = Path(expedition_dir)
3640

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

63+
print("\n---- WAYPOINT VERIFICATION ----")
64+
5965
# verify schedule is valid
6066
schedule.verify(ship_config.ship_speed_knots, loaded_input_data)
6167

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

91+
print("\n----- EXPEDITION SUMMARY ------")
92+
8593
# calculate expedition cost in US$
8694
assert schedule.waypoints[0].time is not None, (
8795
"First waypoint has no time. This should not be possible as it should have been verified before."
@@ -90,20 +98,26 @@ def do_expedition(expedition_dir: str | Path, input_data: Path | None = None) ->
9098
cost = expedition_cost(schedule_results, time_past)
9199
with open(expedition_dir.joinpath("results", "cost.txt"), "w") as file:
92100
file.writelines(f"cost: {cost} US$")
93-
print(f"This expedition took {time_past} and would have cost {cost:,.0f} US$.")
101+
print(f"\nExpedition duration: {time_past}\nExpedition cost: US$ {cost:,.0f}.")
102+
103+
print("\n--- MEASUREMENT SIMULATIONS ---")
94104

95105
# simulate measurements
96-
print("Simulating measurements. This may take a while..")
106+
print("\nSimulating measurements. This may take a while...\n")
97107
simulate_measurements(
98108
expedition_dir,
99109
ship_config,
100110
loaded_input_data,
101111
schedule_results.measurements_to_simulate,
102112
)
103-
print("Done simulating measurements.")
113+
print("\nAll measurement simulations are complete.")
104114

105-
print("Your expedition has concluded successfully!")
106-
print("Your measurements can be found in the results directory.")
115+
print("\n----- EXPEDITION RESULTS ------")
116+
print("\nYour expedition has concluded successfully!")
117+
print(
118+
f"Your measurements can be found in the '{expedition_dir}/results' directory."
119+
)
120+
print("\n------------- END -------------\n")
107121

108122

109123
def _load_input_data(

src/virtualship/expedition/simulate_measurements.py

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
from __future__ import annotations
44

5+
import logging
56
from datetime import timedelta
67
from pathlib import Path
78
from typing import TYPE_CHECKING
89

10+
from yaspin import yaspin
11+
912
from virtualship.instruments.adcp import simulate_adcp
1013
from virtualship.instruments.argo_float import simulate_argo_floats
1114
from virtualship.instruments.ctd import simulate_ctd
@@ -14,12 +17,17 @@
1417
from virtualship.instruments.ship_underwater_st import simulate_ship_underwater_st
1518
from virtualship.instruments.xbt import simulate_xbt
1619
from virtualship.models import ShipConfig
20+
from virtualship.utils import ship_spinner
1721

1822
from .simulate_schedule import MeasurementsToSimulate
1923

2024
if TYPE_CHECKING:
2125
from .input_data import InputData
2226

27+
# parcels logger (suppress INFO messages to prevent log being flooded)
28+
external_logger = logging.getLogger("parcels.tools.loggers")
29+
external_logger.setLevel(logging.WARNING)
30+
2331

2432
def simulate_measurements(
2533
expedition_dir: str | Path,
@@ -42,61 +50,91 @@ def simulate_measurements(
4250
expedition_dir = Path(expedition_dir)
4351

4452
if len(measurements.ship_underwater_sts) > 0:
45-
print("Simulating onboard salinity and temperature measurements.")
4653
if ship_config.ship_underwater_st_config is None:
4754
raise RuntimeError("No configuration for ship underwater ST provided.")
4855
if input_data.ship_underwater_st_fieldset is None:
4956
raise RuntimeError("No fieldset for ship underwater ST provided.")
50-
simulate_ship_underwater_st(
51-
fieldset=input_data.ship_underwater_st_fieldset,
52-
out_path=expedition_dir.joinpath("results", "ship_underwater_st.zarr"),
53-
depth=-2,
54-
sample_points=measurements.ship_underwater_sts,
55-
)
57+
with yaspin(
58+
text="Simulating onboard temperature and salinity measurements... ",
59+
side="right",
60+
spinner=ship_spinner,
61+
) as spinner:
62+
simulate_ship_underwater_st(
63+
fieldset=input_data.ship_underwater_st_fieldset,
64+
out_path=expedition_dir.joinpath("results", "ship_underwater_st.zarr"),
65+
depth=-2,
66+
sample_points=measurements.ship_underwater_sts,
67+
)
68+
spinner.ok("✅")
5669

5770
if len(measurements.adcps) > 0:
58-
print("Simulating onboard ADCP.")
5971
if ship_config.adcp_config is None:
6072
raise RuntimeError("No configuration for ADCP provided.")
6173
if input_data.adcp_fieldset is None:
6274
raise RuntimeError("No fieldset for ADCP provided.")
63-
simulate_adcp(
64-
fieldset=input_data.adcp_fieldset,
65-
out_path=expedition_dir.joinpath("results", "adcp.zarr"),
66-
max_depth=ship_config.adcp_config.max_depth_meter,
67-
min_depth=-5,
68-
num_bins=ship_config.adcp_config.num_bins,
69-
sample_points=measurements.adcps,
70-
)
75+
with yaspin(
76+
text="Simulating onboard ADCP... ", side="right", spinner=ship_spinner
77+
) as spinner:
78+
simulate_adcp(
79+
fieldset=input_data.adcp_fieldset,
80+
out_path=expedition_dir.joinpath("results", "adcp.zarr"),
81+
max_depth=ship_config.adcp_config.max_depth_meter,
82+
min_depth=-5,
83+
num_bins=ship_config.adcp_config.num_bins,
84+
sample_points=measurements.adcps,
85+
)
86+
spinner.ok("✅")
7187

7288
if len(measurements.ctds) > 0:
73-
print("Simulating CTD casts.")
7489
if ship_config.ctd_config is None:
7590
raise RuntimeError("No configuration for CTD provided.")
7691
if input_data.ctd_fieldset is None:
7792
raise RuntimeError("No fieldset for CTD provided.")
78-
simulate_ctd(
79-
out_path=expedition_dir.joinpath("results", "ctd.zarr"),
80-
fieldset=input_data.ctd_fieldset,
81-
ctds=measurements.ctds,
82-
outputdt=timedelta(seconds=10),
83-
)
93+
with yaspin(
94+
text="Simulating CTD casts... ", side="right", spinner=ship_spinner
95+
) as spinner:
96+
simulate_ctd(
97+
out_path=expedition_dir.joinpath("results", "ctd.zarr"),
98+
fieldset=input_data.ctd_fieldset,
99+
ctds=measurements.ctds,
100+
outputdt=timedelta(seconds=10),
101+
)
102+
spinner.ok("✅")
84103

85104
if len(measurements.ctd_bgcs) > 0:
86-
print("Simulating BGC CTD casts.")
87105
if ship_config.ctd_bgc_config is None:
88106
raise RuntimeError("No configuration for CTD_BGC provided.")
89107
if input_data.ctd_bgc_fieldset is None:
90108
raise RuntimeError("No fieldset for CTD_BGC provided.")
91-
simulate_ctd_bgc(
92-
out_path=expedition_dir.joinpath("results", "ctd_bgc.zarr"),
93-
fieldset=input_data.ctd_bgc_fieldset,
94-
ctd_bgcs=measurements.ctd_bgcs,
95-
outputdt=timedelta(seconds=10),
96-
)
109+
with yaspin(
110+
text="Simulating BGC CTD casts... ", side="right", spinner=ship_spinner
111+
) as spinner:
112+
simulate_ctd_bgc(
113+
out_path=expedition_dir.joinpath("results", "ctd_bgc.zarr"),
114+
fieldset=input_data.ctd_bgc_fieldset,
115+
ctd_bgcs=measurements.ctd_bgcs,
116+
outputdt=timedelta(seconds=10),
117+
)
118+
spinner.ok("✅")
119+
120+
if len(measurements.xbts) > 0:
121+
if ship_config.xbt_config is None:
122+
raise RuntimeError("No configuration for XBTs provided.")
123+
if input_data.xbt_fieldset is None:
124+
raise RuntimeError("No fieldset for XBTs provided.")
125+
with yaspin(
126+
text="Simulating XBTs... ", side="right", spinner=ship_spinner
127+
) as spinner:
128+
simulate_xbt(
129+
out_path=expedition_dir.joinpath("results", "xbts.zarr"),
130+
fieldset=input_data.xbt_fieldset,
131+
xbts=measurements.xbts,
132+
outputdt=timedelta(seconds=1),
133+
)
134+
spinner.ok("✅")
97135

98136
if len(measurements.drifters) > 0:
99-
print("Simulating drifters")
137+
print("Simulating drifters... ")
100138
if ship_config.drifter_config is None:
101139
raise RuntimeError("No configuration for drifters provided.")
102140
if input_data.drifter_fieldset is None:
@@ -111,7 +149,7 @@ def simulate_measurements(
111149
)
112150

113151
if len(measurements.argo_floats) > 0:
114-
print("Simulating argo floats")
152+
print("Simulating argo floats... ")
115153
if ship_config.argo_float_config is None:
116154
raise RuntimeError("No configuration for argo floats provided.")
117155
if input_data.argo_float_fieldset is None:
@@ -123,16 +161,3 @@ def simulate_measurements(
123161
outputdt=timedelta(minutes=5),
124162
endtime=None,
125163
)
126-
127-
if len(measurements.xbts) > 0:
128-
print("Simulating XBTs")
129-
if ship_config.xbt_config is None:
130-
raise RuntimeError("No configuration for XBTs provided.")
131-
if input_data.xbt_fieldset is None:
132-
raise RuntimeError("No fieldset for XBTs provided.")
133-
simulate_xbt(
134-
out_path=expedition_dir.joinpath("results", "xbts.zarr"),
135-
fieldset=input_data.xbt_fieldset,
136-
xbts=measurements.xbts,
137-
outputdt=timedelta(seconds=1),
138-
)
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
"""Measurement instrument that can be used with Parcels."""
22

3-
from . import adcp, argo_float, ctd, drifter, ship_underwater_st, xbt
3+
from . import adcp, argo_float, ctd, ctd_bgc, drifter, ship_underwater_st, xbt
44

5-
__all__ = ["adcp", "argo_float", "ctd", "drifter", "ship_underwater_st", "xbt"]
5+
__all__ = [
6+
"adcp",
7+
"argo_float",
8+
"ctd",
9+
"ctd_bgc",
10+
"drifter",
11+
"ship_underwater_st",
12+
"xbt",
13+
]

src/virtualship/models/schedule.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def verify(
121121
:raises NotImplementedError: If an instrument in the schedule is not implemented.
122122
:return: None. The method doesn't return a value but raises exceptions if verification fails.
123123
"""
124+
print("\nVerifying route... ")
125+
124126
if check_space_time_region and self.space_time_region is None:
125127
raise ScheduleError(
126128
"space_time_region not found in schedule, please define it to fetch the data."
@@ -145,8 +147,6 @@ def verify(
145147
# check if all waypoints are in water
146148
# this is done by picking an arbitrary provided fieldset and checking if UV is not zero
147149

148-
print("Verifying all waypoints are on water..")
149-
150150
# get all available fieldsets
151151
available_fieldsets = []
152152
if input_data is not None:
@@ -184,7 +184,6 @@ def verify(
184184
raise ScheduleError(
185185
f"The following waypoints are on land: {['#' + str(wp_i) + ' ' + str(wp) for (wp_i, wp) in land_waypoints]}"
186186
)
187-
print("Good, all waypoints are on water.")
188187

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

217+
print("... All good to go!")
218+
218219

219220
def _is_on_land_zero_uv(fieldset: FieldSet, waypoint: Waypoint) -> bool:
220221
"""

src/virtualship/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pathlib import Path
99
from typing import TYPE_CHECKING, TextIO
1010

11+
from yaspin import Spinner
12+
1113
if TYPE_CHECKING:
1214
from virtualship.models import Schedule, ShipConfig
1315

@@ -247,3 +249,21 @@ def _get_ship_config(expedition_dir: Path) -> ShipConfig:
247249
raise FileNotFoundError(
248250
f'Ship config not found. Save it to "{file_path}".'
249251
) from e
252+
253+
254+
# custom ship spinner
255+
ship_spinner = Spinner(
256+
interval=240,
257+
frames=[
258+
" 🚢 ",
259+
" 🚢 ",
260+
" 🚢 ",
261+
" 🚢 ",
262+
" 🚢",
263+
" 🚢 ",
264+
" 🚢 ",
265+
" 🚢 ",
266+
" 🚢 ",
267+
"🚢 ",
268+
],
269+
)

tests/expedition/test_do_expedition.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
def test_do_expedition(capfd: CaptureFixture) -> None:
99
do_expedition("expedition_dir", input_data=Path("expedition_dir/input_data"))
1010
out, _ = capfd.readouterr()
11-
assert "This expedition took" in out, "Expedition did not complete successfully."
11+
assert "Your expedition has concluded successfully!" in out, (
12+
"Expedition did not complete successfully."
13+
)

0 commit comments

Comments
 (0)