Skip to content
Closed
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
24 changes: 19 additions & 5 deletions src/virtualship/expedition/do_expedition.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -57,6 +61,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, input_data)

Expand All @@ -83,6 +89,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 @@ -91,20 +99,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,
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
4 changes: 2 additions & 2 deletions src/virtualship/expedition/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ 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..")
print("\nVerifying all waypoints are on water...")

# get all available fieldsets
available_fieldsets = []
Expand Down Expand Up @@ -178,7 +178,7 @@ 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.")
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
7 changes: 0 additions & 7 deletions src/virtualship/expedition/simulate_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ 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:
Expand All @@ -49,7 +48,6 @@ def simulate_measurements(
)

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:
Expand All @@ -64,7 +62,6 @@ def simulate_measurements(
)

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:
Expand All @@ -77,7 +74,6 @@ def simulate_measurements(
)

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:
Expand All @@ -90,7 +86,6 @@ def simulate_measurements(
)

if len(measurements.drifters) > 0:
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 @@ -105,7 +100,6 @@ def simulate_measurements(
)

if len(measurements.argo_floats) > 0:
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 @@ -119,7 +113,6 @@ def simulate_measurements(
)

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:
Expand Down
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",
]
50 changes: 34 additions & 16 deletions src/virtualship/instruments/adcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import numpy as np
from parcels import FieldSet, ParticleSet, ScipyParticle, Variable

from ..log_filter import Filter, external_logger
from ..spacetime import Spacetime
from ..utils import RotatePrint

# we specifically use ScipyParticle because we have many small calls to execute
# there is some overhead with JITParticle and this ends up being significantly faster
Expand Down Expand Up @@ -41,6 +43,8 @@ def simulate_adcp(
:param num_bins: How many samples to take in the complete range between max_depth and min_depth.
:param sample_points: The places and times to sample at.
"""
rotator = RotatePrint("Simulating onboard ADCP...")

sample_points.sort(key=lambda p: p.time)

bins = np.linspace(max_depth, min_depth, num_bins)
Expand All @@ -60,19 +64,33 @@ def simulate_adcp(
# outputdt set to infinite as we just want to write at the end of every call to 'execute'
out_file = particleset.ParticleFile(name=out_path, outputdt=np.inf)

for point in sample_points:
particleset.lon_nextloop[:] = point.location.lon
particleset.lat_nextloop[:] = point.location.lat
particleset.time_nextloop[:] = fieldset.time_origin.reltime(
np.datetime64(point.time)
)

# perform one step using the particleset
# dt and runtime are set so exactly one step is made.
particleset.execute(
[_sample_velocity],
dt=1,
runtime=1,
verbose_progress=False,
output_file=out_file,
)
# filter out Parcels logging messages
for handler in external_logger.handlers:
handler.addFilter(Filter())

# try/finally to ensure filter is always removed even if .execute fails (to avoid filter being appled universally)
# also suits starting and ending the rotator for custom log message
try:
rotator.start()

for point in sample_points:
particleset.lon_nextloop[:] = point.location.lon
particleset.lat_nextloop[:] = point.location.lat
particleset.time_nextloop[:] = fieldset.time_origin.reltime(
np.datetime64(point.time)
)

# perform one step using the particleset
# dt and runtime are set so exactly one step is made.
particleset.execute(
[_sample_velocity],
dt=1,
runtime=1,
verbose_progress=False,
output_file=out_file,
)

finally:
rotator.stop()
for handler in external_logger.handlers:
handler.removeFilter(handler.filters[0])
Comment on lines +71 to +96
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Python, this try finally pattern is normally done via context managers. This means that if something fails in the block of code, the code is torn down after.

This will simplify both the code for the spinner, as well as the code for the filtering, boiling down to something like

with spinner("Deploying drifters..."):
    with discard_parcels_warnings():
        # code to process measurement

38 changes: 25 additions & 13 deletions src/virtualship/instruments/argo_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Variable,
)

from ..log_filter import Filter, external_logger
from ..spacetime import Spacetime


Expand Down Expand Up @@ -131,6 +132,7 @@ def simulate_argo_floats(
:param outputdt: Interval which dictates the update frequency of file output during simulation
:param endtime: Stop at this time, or if None, continue until the end of the fieldset.
"""
print("Simulating argo floats...")
DT = 10.0 # dt of Argo float simulation integrator

if len(argo_floats) == 0:
Expand Down Expand Up @@ -171,16 +173,26 @@ def simulate_argo_floats(
else:
actual_endtime = np.timedelta64(endtime)

# execute simulation
argo_float_particleset.execute(
[
_argo_float_vertical_movement,
AdvectionRK4,
_keep_at_surface,
_check_error,
],
endtime=actual_endtime,
dt=DT,
output_file=out_file,
verbose_progress=True,
)
# filter out Parcels logging messages
for handler in external_logger.handlers:
handler.addFilter(Filter())

# try/finally to ensure filter is always removed even if .execute fails (to avoid filter being appled universally)
try:
argo_float_particleset.execute(
[
_argo_float_vertical_movement,
AdvectionRK4,
_keep_at_surface,
_check_error,
],
endtime=actual_endtime,
dt=DT,
output_file=out_file,
verbose_progress=True,
)

finally:
print("... [COMPLETED]")
for handler in external_logger.handlers:
handler.removeFilter(handler.filters[0])
33 changes: 25 additions & 8 deletions src/virtualship/instruments/ctd.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import numpy as np
from parcels import FieldSet, JITParticle, ParticleSet, Variable

from ..log_filter import Filter, external_logger
from ..spacetime import Spacetime
from ..utils import RotatePrint


@dataclass
Expand Down Expand Up @@ -68,6 +70,8 @@ def simulate_ctd(
:param outputdt: Interval which dictates the update frequency of file output during simulation
:raises ValueError: Whenever provided CTDs, fieldset, are not compatible with this function.
"""
rotator = RotatePrint("Simulating CTD casts...")

WINCH_SPEED = 1.0 # sink and rise speed in m/s
DT = 10.0 # dt of CTD simulation integrator

Expand Down Expand Up @@ -121,14 +125,27 @@ def simulate_ctd(
# define output file for the simulation
out_file = ctd_particleset.ParticleFile(name=out_path, outputdt=outputdt)

# execute simulation
ctd_particleset.execute(
[_sample_salinity, _sample_temperature, _ctd_cast],
endtime=fieldset_endtime,
dt=DT,
verbose_progress=False,
output_file=out_file,
)
# filter out Parcels logging messages
for handler in external_logger.handlers:
handler.addFilter(Filter())

# try/finally to ensure filter is always removed even if .execute fails (to avoid filter being appled universally)
# also suits starting and ending the rotator for custom log message
try:
rotator.start()

ctd_particleset.execute(
[_sample_salinity, _sample_temperature, _ctd_cast],
endtime=fieldset_endtime,
dt=DT,
verbose_progress=False,
output_file=out_file,
)

finally:
rotator.stop()
for handler in external_logger.handlers:
handler.removeFilter(handler.filters[0])

# there should be no particles left, as they delete themselves when they resurface
if len(ctd_particleset.particledata) != 0:
Expand Down
33 changes: 25 additions & 8 deletions src/virtualship/instruments/ctd_bgc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import numpy as np
from parcels import FieldSet, JITParticle, ParticleSet, Variable

from ..log_filter import Filter, external_logger
from ..spacetime import Spacetime
from ..utils import RotatePrint


@dataclass
Expand Down Expand Up @@ -68,6 +70,8 @@ def simulate_ctd_bgc(
:param outputdt: Interval which dictates the update frequency of file output during simulation
:raises ValueError: Whenever provided BGC CTDs, fieldset, are not compatible with this function.
"""
rotator = RotatePrint("Simulating BGC CTD casts...")

WINCH_SPEED = 1.0 # sink and rise speed in m/s
DT = 10.0 # dt of CTD simulation integrator

Expand Down Expand Up @@ -127,14 +131,27 @@ def simulate_ctd_bgc(
# define output file for the simulation
out_file = ctd_bgc_particleset.ParticleFile(name=out_path, outputdt=outputdt)

# execute simulation
ctd_bgc_particleset.execute(
[_sample_o2, _sample_chlorophyll, _ctd_bgc_cast],
endtime=fieldset_endtime,
dt=DT,
verbose_progress=False,
output_file=out_file,
)
# filter out Parcels logging messages
for handler in external_logger.handlers:
handler.addFilter(Filter())

# try/finally to ensure filter is always removed even if .execute fails (to avoid filter being appled universally)
# also suits starting and ending the rotator for custom log message
try:
rotator.start()

ctd_bgc_particleset.execute(
[_sample_o2, _sample_chlorophyll, _ctd_bgc_cast],
endtime=fieldset_endtime,
dt=DT,
verbose_progress=False,
output_file=out_file,
)

finally:
rotator.stop()
for handler in external_logger.handlers:
handler.removeFilter(handler.filters[0])

# there should be no particles left, as they delete themselves when they resurface
if len(ctd_bgc_particleset.particledata) != 0:
Expand Down
Loading