Skip to content

Commit 0be180d

Browse files
committed
fixes to argo kernels + some refactoring
1 parent fd2232a commit 0be180d

1 file changed

Lines changed: 95 additions & 51 deletions

File tree

src/virtualship/instruments/argo_float.py

Lines changed: 95 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ class ArgoFloat:
5858
# SECTION: Kernels
5959
# =====================================================
6060

61-
# TODO: need to add back in the shallow bathymetry checks (to phases 0 and 2?!)
62-
# TODO: can this be refactored as well to a helper function?
63-
6461

6562
def _argo_float_vertical_movement(particles, fieldset):
6663
# Split particles based on their current cycle_phase
@@ -73,26 +70,21 @@ def _argo_float_vertical_movement(particles, fieldset):
7370
# Phase 0: Sinking with vertical_speed until depth is driftdepth
7471
ptcls0.dz += particles.vertical_speed * ptcls0.dt
7572
loc_bathy = fieldset.bathymetry.eval(ptcls0.time, ptcls0.z, ptcls0.lat, ptcls0.lon)
76-
driftdepth_mask = ptcls0.z + ptcls0.dz >= particles.drift_depth
77-
bathy_mask = ptcls0.z + ptcls0.dz >= loc_bathy
78-
next_phase = np.logical_and(
79-
driftdepth_mask, bathy_mask
80-
) # combined mask; not at drift depth yet and not hitting bathymetry
73+
driftdepth_mask = ptcls0.z + ptcls0.dz <= particles.drift_depth # noqa:has reached drift depth
74+
bathysafe_mask = ptcls0.z + ptcls0.dz >= loc_bathy # noqa:has not reached bathymetry
75+
next_phase = np.logical_and(driftdepth_mask, bathysafe_mask)
8176
ptcls0.cycle_phase[next_phase] = 1
82-
ptcls0.dz[next_phase] = (
83-
particles.drift_depth - ptcls0.z[next_phase]
84-
) # avoid overshoot
77+
ptcls0.dz[next_phase] = particles.drift_depth - ptcls0.z[next_phase] # noqa:avoid overshoot
8578

8679
# Phase 0.5: Check for grounding at bathymetry and raise if necessary
87-
ptcls0.grounded[~bathy_mask] = 1
88-
if np.any(~bathy_mask):
89-
print(
90-
"Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to drift depth. Raising by 50m above bathymetry and continuing cycle."
91-
)
92-
ptcls0.dz[~bathy_mask] = (
93-
loc_bathy[~bathy_mask] - ptcls0.z[~bathy_mask] + 50.0
94-
) # raise to 50m above bathymetry
95-
ptcls0.cycle_phase[~bathy_mask] = 1
80+
_handle_grounding(
81+
ptcls0,
82+
bathysafe_mask,
83+
loc_bathy,
84+
fieldset,
85+
"sinking to drift depth",
86+
target_phase=1,
87+
)
9688

9789
# Phase 1: Drifting at depth for drifttime seconds
9890
ptcls1.drift_age += ptcls1.dt
@@ -103,34 +95,27 @@ def _argo_float_vertical_movement(particles, fieldset):
10395
# Phase 2: Sinking further to maxdepth
10496
ptcls2.dz += particles.vertical_speed * ptcls2.dt
10597
loc_bathy = fieldset.bathymetry.eval(ptcls2.time, ptcls2.z, ptcls2.lat, ptcls2.lon)
106-
maxdepth_mask = ptcls2.z + ptcls2.dz >= particles.max_depth
107-
bathy_mask = ptcls2.z + ptcls2.dz >= loc_bathy
108-
next_phase = np.logical_and(
109-
maxdepth_mask, bathy_mask
110-
) # combined mask; not at max depth yet and not hitting bathymetry
98+
maxdepth_mask = ptcls2.z + ptcls2.dz <= particles.max_depth # noqa:has reached max depth
99+
bathysafe_mask = ptcls2.z + ptcls2.dz >= loc_bathy # noqa:has not reached bathymetry
100+
next_phase = np.logical_and(maxdepth_mask, bathysafe_mask)
111101
ptcls2.cycle_phase[next_phase] = 3
112-
ptcls2.dz[next_phase] = (
113-
particles.max_depth - ptcls2.z[next_phase]
114-
) # avoid overshoot
102+
ptcls2.dz[next_phase] = particles.max_depth - ptcls2.z[next_phase] # noqa:avoid overshoot
115103

116104
# Phase 2.5: Check for grounding at bathymetry and raise if necessary
117-
ptcls2.grounded[~bathy_mask] = 1
118-
if np.any(~bathy_mask):
119-
print(
120-
"Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to max depth. Raising by 50m above bathymetry and continuing cycle."
121-
)
122-
ptcls2.dz[~bathy_mask] = (
123-
loc_bathy[~bathy_mask] - ptcls2.z[~bathy_mask] + 50.0
124-
) # raise to 50m above bathymetry
125-
ptcls2.cycle_phase[~bathy_mask] = 3
105+
_handle_grounding(
106+
ptcls2,
107+
bathysafe_mask,
108+
loc_bathy,
109+
fieldset,
110+
"sinking to max depth",
111+
target_phase=3,
112+
)
126113

127114
# Phase 3: Rising with vertical_speed until at surface
128115
ptcls3.dz -= particles.vertical_speed * ptcls3.dt
129-
next_phase = ptcls3.z + ptcls3.dz <= particles.min_depth
116+
next_phase = ptcls3.z + ptcls3.dz >= particles.min_depth
130117
ptcls3.cycle_phase[next_phase] = 4
131-
ptcls3.dz[next_phase] = (
132-
particles.min_depth - ptcls3.z[next_phase]
133-
) # avoid overshoot
118+
ptcls3.dz[next_phase] = particles.min_depth - ptcls3.z[next_phase] # noqa:avoid overshoot
134119

135120
# Phase 4: Transmitting at surface until cycletime is reached
136121
next_phase = ptcls4.cycle_age >= particles.cycle_days * 86400
@@ -148,21 +133,38 @@ def _keep_at_surface(particles, fieldset):
148133

149134

150135
def _check_error(particles, fieldset):
151-
errors = particles.state >= 50 # captures all Errors
152-
# TODO: check print statements are as expected
136+
errors = particles.state >= 50
137+
if not np.any(errors):
138+
return
139+
140+
error_ints = particles.state[errors].astype(int)
141+
error_times, error_lats, error_lons = _format_log_metadata(
142+
particles, errors, fieldset
143+
)
144+
145+
error_details = ", ".join(
146+
f"{_STATUS_CODE_NAMES.get(err, str(err))} at time(s): {t}, lat(s): {lat}, lon(s): {lon}"
147+
for err, lat, lon, t in zip(
148+
error_ints, error_lats, error_lons, error_times, strict=True
149+
)
150+
)
153151
print(
154-
"WARNING: Error(s) found during Argo Float simulation but the expedition will continue..."
155-
f"\n\nError code(s): {', '.join(_STATUS_CODE_NAMES.get(error, str(error)) + 'at time: ' + str(particles.time[errors][i]) + ', lat: ' + str(particles.lat[errors][i]) + ', lon: ' + str(particles.lon[errors][i]) for i, error in enumerate(particles.state[errors]))}"
156-
"\n\nIf ErrorOutOfBounds, consider reducing the lifetime in Argo Float config (the fieldset spatial bounds are constrained under-the-hood). For further advice please contact the VirtualShip team via GitHub (https://github.com/Parcels-code/virtualship/issues) or email (virtualship@uu.nl)."
157-
"\nCarrying on with the expedition..."
152+
"WARNING: Error(s) found during Argo Float simulation but the expedition will continue...\n\n"
153+
f"Error code(s): {error_details}\n\n"
154+
"If ErrorOutOfBounds, consider reducing the lifetime in Argo Float config "
155+
"(the fieldset spatial bounds are constrained under-the-hood). For further advice "
156+
"please contact the VirtualShip team via GitHub (https://github.com/Parcels-code/virtualship/issues) "
157+
"or email (virtualship@uu.nl).\n"
158+
"Carrying on with the expedition..."
158159
)
160+
159161
particles.state[errors] = StatusCode.Delete
160162

161163

162164
def _argo_sample_temperature(particles, fieldset):
163-
# Phase 3: ascending — sample temperature; NaN otherwise
165+
# Phase 3: ascending — sample temperature
164166
phase_mask = particles.cycle_phase == 3
165-
depth_mask = particles.z < particles.min_depth
167+
depth_mask = particles.z < particles.min_depth # still ascending
166168
sampling_particles = particles[np.logical_and(phase_mask, depth_mask)]
167169
sampling_particles.temperature = fieldset.T[
168170
sampling_particles.time,
@@ -173,9 +175,9 @@ def _argo_sample_temperature(particles, fieldset):
173175

174176

175177
def _argo_sample_salinity(particles, fieldset):
176-
# Phase 3: ascending — sample salinity; NaN otherwise
178+
# Phase 3: ascending — sample salinity
177179
phase_mask = particles.cycle_phase == 3
178-
depth_mask = particles.z < particles.min_depth
180+
depth_mask = particles.z < particles.min_depth # still ascending
179181
sampling_particles = particles[np.logical_and(phase_mask, depth_mask)]
180182
sampling_particles.salinity = fieldset.S[
181183
sampling_particles.time,
@@ -185,6 +187,48 @@ def _argo_sample_salinity(particles, fieldset):
185187
]
186188

187189

190+
# =====================================================
191+
# SECTION: Helper Functions
192+
# =====================================================
193+
194+
195+
def _handle_grounding(
196+
ptcls_subset, bathysafe_mask, loc_bathy, fieldset, phase_name, target_phase
197+
):
198+
"""Handle grounding logic, logging warnings, and raising particles above bathymetry."""
199+
grounded_mask = ~bathysafe_mask
200+
if not np.any(grounded_mask):
201+
return
202+
203+
ptcls_subset.grounded[grounded_mask] = 1
204+
205+
# extract log data
206+
times, lats, lons = _format_log_metadata(ptcls_subset, grounded_mask, fieldset)
207+
208+
print(
209+
f"Shallow bathymetry warning: Argo float grounded at bathymetry during {phase_name} "
210+
f"(time(s): {times}, lat(s): {lats}, lon(s): {lons}). "
211+
f"Raising by 50m above bathymetry and continuing cycle."
212+
)
213+
214+
# adjust vertical displacement to be 50m above bathymetry and transition phase
215+
ptcls_subset.dz[grounded_mask] = (
216+
loc_bathy[grounded_mask] - ptcls_subset.z[grounded_mask] + 50.0
217+
)
218+
ptcls_subset.cycle_phase[grounded_mask] = target_phase
219+
220+
221+
def _format_log_metadata(ptcls_subset, mask, fieldset):
222+
"""Extracts and formats timestamps, latitudes, and longitudes for particles."""
223+
lats = ptcls_subset.lat[mask].astype(float)
224+
lons = ptcls_subset.lon[mask].astype(float)
225+
226+
time_origin = fieldset.U.data.time[0].values
227+
times = ptcls_subset.time[mask].astype("timedelta64[s]") + time_origin
228+
229+
return times, lats, lons
230+
231+
188232
# =====================================================
189233
# SECTION: Instrument Class
190234
# =====================================================

0 commit comments

Comments
 (0)