Skip to content

Commit f279780

Browse files
Biogeochemistry features for expedition execution (#184)
* update init command to use CTD_BGC as new instrument when making schedule * update example ship_config file to also include CTD_BGC instrument config * Prevent fetch from downloading unnecessary data and skipping data download for certain instruments (#175) * fixes for over-extensive data downloads and skipping certain instruments * fixes based on PR feedback #175 * Managing conflicts on branch. Move InstrumentType to ship_config.py * Move Waypoint to schedule.py * update init command to use CTD_BGC as new instrument when making schedule * add configuration for CTD_BGC * add bgc data download option for CTD_BGC instrument * add CTD_BGC to instruments which prompts ship data download * Review feedback on complete_download * bgc input data .nc files for testing * add CTD_BGC to static/example schedule (also necessary for ship_config test) * update test suite for new bgc sampling using ctd_bgc * new ctd_bgc instrument * add ctd_bgc sampling capability to virtualship run execution --------- Co-authored-by: Vecko <[email protected]>
1 parent d28a39a commit f279780

File tree

13 files changed

+377
-0
lines changed

13 files changed

+377
-0
lines changed

src/virtualship/expedition/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .ship_config import (
77
ADCPConfig,
88
ArgoFloatConfig,
9+
CTD_BGCConfig,
910
CTDConfig,
1011
DrifterConfig,
1112
ShipConfig,
@@ -17,6 +18,7 @@
1718
"ADCPConfig",
1819
"ArgoFloatConfig",
1920
"CTDConfig",
21+
"CTD_BGCConfig",
2022
"DrifterConfig",
2123
"InputData",
2224
"InstrumentType",

src/virtualship/expedition/do_expedition.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def _load_input_data(
136136
load_adcp=ship_config.adcp_config is not None,
137137
load_argo_float=ship_config.argo_float_config is not None,
138138
load_ctd=ship_config.ctd_config is not None,
139+
load_ctd_bgc=ship_config.ctd_bgc_config is not None,
139140
load_drifter=ship_config.drifter_config is not None,
140141
load_xbt=ship_config.xbt_config is not None,
141142
load_ship_underwater_st=ship_config.ship_underwater_st_config is not None,

src/virtualship/expedition/input_data.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class InputData:
1515
adcp_fieldset: FieldSet | None
1616
argo_float_fieldset: FieldSet | None
1717
ctd_fieldset: FieldSet | None
18+
ctd_bgc_fieldset: FieldSet | None
1819
drifter_fieldset: FieldSet | None
1920
xbt_fieldset: FieldSet | None
2021
ship_underwater_st_fieldset: FieldSet | None
@@ -26,6 +27,7 @@ def load(
2627
load_adcp: bool,
2728
load_argo_float: bool,
2829
load_ctd: bool,
30+
load_ctd_bgc: bool,
2931
load_drifter: bool,
3032
load_xbt: bool,
3133
load_ship_underwater_st: bool,
@@ -39,6 +41,7 @@ def load(
3941
:param load_adcp: Whether to load the ADCP fieldset.
4042
:param load_argo_float: Whether to load the argo float fieldset.
4143
:param load_ctd: Whether to load the CTD fieldset.
44+
:param load_ctd_bgc: Whether to load the CTD BGC fieldset.
4245
:param load_drifter: Whether to load the drifter fieldset.
4346
:param load_ship_underwater_st: Whether to load the ship underwater ST fieldset.
4447
:returns: An instance of this class with loaded fieldsets.
@@ -51,6 +54,10 @@ def load(
5154
argo_float_fieldset = cls._load_argo_float_fieldset(directory)
5255
else:
5356
argo_float_fieldset = None
57+
if load_ctd_bgc:
58+
ctd_bgc_fieldset = cls._load_ctd_bgc_fieldset(directory)
59+
else:
60+
ctd_bgc_fieldset = None
5461
if load_adcp or load_ctd or load_ship_underwater_st or load_xbt:
5562
ship_fieldset = cls._load_ship_fieldset(directory)
5663
if load_adcp:
@@ -74,6 +81,7 @@ def load(
7481
adcp_fieldset=adcp_fieldset,
7582
argo_float_fieldset=argo_float_fieldset,
7683
ctd_fieldset=ctd_fieldset,
84+
ctd_bgc_fieldset=ctd_bgc_fieldset,
7785
drifter_fieldset=drifter_fieldset,
7886
xbt_fieldset=xbt_fieldset,
7987
ship_underwater_st_fieldset=ship_underwater_st_fieldset,
@@ -122,6 +130,48 @@ def _load_ship_fieldset(cls, directory: Path) -> FieldSet:
122130

123131
return fieldset
124132

133+
@classmethod
134+
def _load_ctd_bgc_fieldset(cls, directory: Path) -> FieldSet:
135+
filenames = {
136+
"U": directory.joinpath("ship_uv.nc"),
137+
"V": directory.joinpath("ship_uv.nc"),
138+
"o2": directory.joinpath("ctd_bgc_o2.nc"),
139+
"chl": directory.joinpath("ctd_bgc_chloro.nc"),
140+
}
141+
variables = {"U": "uo", "V": "vo", "o2": "o2", "chl": "chl"}
142+
dimensions = {
143+
"lon": "longitude",
144+
"lat": "latitude",
145+
"time": "time",
146+
"depth": "depth",
147+
}
148+
149+
fieldset = FieldSet.from_netcdf(
150+
filenames, variables, dimensions, allow_time_extrapolation=True
151+
)
152+
fieldset.o2.interp_method = "linear_invdist_land_tracer"
153+
fieldset.chl.interp_method = "linear_invdist_land_tracer"
154+
155+
# make depth negative
156+
for g in fieldset.gridset.grids:
157+
g.negate_depth()
158+
159+
# add bathymetry data
160+
bathymetry_file = directory.joinpath("bathymetry.nc")
161+
bathymetry_variables = ("bathymetry", "deptho")
162+
bathymetry_dimensions = {"lon": "longitude", "lat": "latitude"}
163+
bathymetry_field = Field.from_netcdf(
164+
bathymetry_file, bathymetry_variables, bathymetry_dimensions
165+
)
166+
# make depth negative
167+
bathymetry_field.data = -bathymetry_field.data
168+
fieldset.add_field(bathymetry_field)
169+
170+
# read in data already
171+
fieldset.computeTimeChunk(0, 1)
172+
173+
return fieldset
174+
125175
@classmethod
126176
def _load_drifter_fieldset(cls, directory: Path) -> FieldSet:
127177
filenames = {

src/virtualship/expedition/simulate_measurements.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..instruments.adcp import simulate_adcp
77
from ..instruments.argo_float import simulate_argo_floats
88
from ..instruments.ctd import simulate_ctd
9+
from ..instruments.ctd_bgc import simulate_ctd_bgc
910
from ..instruments.drifter import simulate_drifters
1011
from ..instruments.ship_underwater_st import simulate_ship_underwater_st
1112
from ..instruments.xbt import simulate_xbt
@@ -75,6 +76,19 @@ def simulate_measurements(
7576
outputdt=timedelta(seconds=10),
7677
)
7778

79+
if len(measurements.ctd_bgcs) > 0:
80+
print("Simulating BGC CTD casts.")
81+
if ship_config.ctd_bgc_config is None:
82+
raise RuntimeError("No configuration for CTD_BGC provided.")
83+
if input_data.ctd_bgc_fieldset is None:
84+
raise RuntimeError("No fieldset for CTD_BGC provided.")
85+
simulate_ctd_bgc(
86+
out_path=expedition_dir.joinpath("results", "ctd_bgc.zarr"),
87+
fieldset=input_data.ctd_bgc_fieldset,
88+
ctd_bgcs=measurements.ctd_bgcs,
89+
outputdt=timedelta(seconds=10),
90+
)
91+
7892
if len(measurements.drifters) > 0:
7993
print("Simulating drifters")
8094
if ship_config.drifter_config is None:

src/virtualship/expedition/simulate_schedule.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from ..instruments.argo_float import ArgoFloat
1111
from ..instruments.ctd import CTD
12+
from ..instruments.ctd_bgc import CTD_BGC
1213
from ..instruments.drifter import Drifter
1314
from ..instruments.xbt import XBT
1415
from ..location import Location
@@ -42,6 +43,7 @@ class MeasurementsToSimulate:
4243
argo_floats: list[ArgoFloat] = field(default_factory=list, init=False)
4344
drifters: list[Drifter] = field(default_factory=list, init=False)
4445
ctds: list[CTD] = field(default_factory=list, init=False)
46+
ctd_bgcs: list[CTD_BGC] = field(default_factory=list, init=False)
4547
xbts: list[XBT] = field(default_factory=list, init=False)
4648

4749

@@ -102,6 +104,7 @@ def simulate(self) -> ScheduleOk | ScheduleProblem:
102104
# check if waypoint was reached in time
103105
if waypoint.time is not None and self._time > waypoint.time:
104106
print(
107+
# TODO: I think this should be wp_i + 1, not wp_i; otherwise it will be off by one
105108
f"Waypoint {wp_i} could not be reached in time. Current time: {self._time}. Waypoint time: {waypoint.time}."
106109
)
107110
return ScheduleProblem(self._time, wp_i)
@@ -251,6 +254,15 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta:
251254
)
252255
)
253256
time_costs.append(self._ship_config.ctd_config.stationkeeping_time)
257+
elif instrument is InstrumentType.CTD_BGC:
258+
self._measurements_to_simulate.ctd_bgcs.append(
259+
CTD_BGC(
260+
spacetime=Spacetime(self._location, self._time),
261+
min_depth=self._ship_config.ctd_bgc_config.min_depth_meter,
262+
max_depth=self._ship_config.ctd_bgc_config.max_depth_meter,
263+
)
264+
)
265+
time_costs.append(self._ship_config.ctd_bgc_config.stationkeeping_time)
254266
elif instrument is InstrumentType.DRIFTER:
255267
self._measurements_to_simulate.drifters.append(
256268
Drifter(
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""CTD_BGC instrument."""
2+
3+
from dataclasses import dataclass
4+
from datetime import timedelta
5+
from pathlib import Path
6+
7+
import numpy as np
8+
from parcels import FieldSet, JITParticle, ParticleSet, Variable
9+
10+
from ..spacetime import Spacetime
11+
12+
13+
@dataclass
14+
class CTD_BGC:
15+
"""Configuration for a single BGC CTD."""
16+
17+
spacetime: Spacetime
18+
min_depth: float
19+
max_depth: float
20+
21+
22+
_CTD_BGCParticle = JITParticle.add_variables(
23+
[
24+
Variable("o2", dtype=np.float32, initial=np.nan),
25+
Variable("chl", dtype=np.float32, initial=np.nan),
26+
Variable("raising", dtype=np.int8, initial=0.0), # bool. 0 is False, 1 is True.
27+
Variable("max_depth", dtype=np.float32),
28+
Variable("min_depth", dtype=np.float32),
29+
Variable("winch_speed", dtype=np.float32),
30+
]
31+
)
32+
33+
34+
def _sample_o2(particle, fieldset, time):
35+
particle.o2 = fieldset.o2[time, particle.depth, particle.lat, particle.lon]
36+
37+
38+
def _sample_chlorophyll(particle, fieldset, time):
39+
particle.chl = fieldset.chl[time, particle.depth, particle.lat, particle.lon]
40+
41+
42+
def _ctd_bgc_cast(particle, fieldset, time):
43+
# lowering
44+
if particle.raising == 0:
45+
particle_ddepth = -particle.winch_speed * particle.dt
46+
if particle.depth + particle_ddepth < particle.max_depth:
47+
particle.raising = 1
48+
particle_ddepth = -particle_ddepth
49+
# raising
50+
else:
51+
particle_ddepth = particle.winch_speed * particle.dt
52+
if particle.depth + particle_ddepth > particle.min_depth:
53+
particle.delete()
54+
55+
56+
def simulate_ctd_bgc(
57+
fieldset: FieldSet,
58+
out_path: str | Path,
59+
ctd_bgcs: list[CTD_BGC],
60+
outputdt: timedelta,
61+
) -> None:
62+
"""
63+
Use Parcels to simulate a set of BGC CTDs in a fieldset.
64+
65+
:param fieldset: The fieldset to simulate the BGC CTDs in.
66+
:param out_path: The path to write the results to.
67+
:param ctds: A list of BGC CTDs to simulate.
68+
:param outputdt: Interval which dictates the update frequency of file output during simulation
69+
:raises ValueError: Whenever provided BGC CTDs, fieldset, are not compatible with this function.
70+
"""
71+
WINCH_SPEED = 1.0 # sink and rise speed in m/s
72+
DT = 10.0 # dt of CTD simulation integrator
73+
74+
if len(ctd_bgcs) == 0:
75+
print(
76+
"No BGC CTDs provided. Parcels currently crashes when providing an empty particle set, so no BGC CTD simulation will be done and no files will be created."
77+
)
78+
# TODO when Parcels supports it this check can be removed.
79+
return
80+
81+
fieldset_starttime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[0])
82+
fieldset_endtime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[-1])
83+
84+
# deploy time for all ctds should be later than fieldset start time
85+
if not all(
86+
[
87+
np.datetime64(ctd_bgc.spacetime.time) >= fieldset_starttime
88+
for ctd_bgc in ctd_bgcs
89+
]
90+
):
91+
raise ValueError("BGC CTD deployed before fieldset starts.")
92+
93+
# depth the bgc ctd will go to. shallowest between bgc ctd max depth and bathymetry.
94+
max_depths = [
95+
max(
96+
ctd_bgc.max_depth,
97+
fieldset.bathymetry.eval(
98+
z=0,
99+
y=ctd_bgc.spacetime.location.lat,
100+
x=ctd_bgc.spacetime.location.lon,
101+
time=0,
102+
),
103+
)
104+
for ctd_bgc in ctd_bgcs
105+
]
106+
107+
# CTD depth can not be too shallow, because kernel would break.
108+
# This shallow is not useful anyway, no need to support.
109+
if not all([max_depth <= -DT * WINCH_SPEED for max_depth in max_depths]):
110+
raise ValueError(
111+
f"BGC CTD max_depth or bathymetry shallower than maximum {-DT * WINCH_SPEED}"
112+
)
113+
114+
# define parcel particles
115+
ctd_bgc_particleset = ParticleSet(
116+
fieldset=fieldset,
117+
pclass=_CTD_BGCParticle,
118+
lon=[ctd_bgc.spacetime.location.lon for ctd_bgc in ctd_bgcs],
119+
lat=[ctd_bgc.spacetime.location.lat for ctd_bgc in ctd_bgcs],
120+
depth=[ctd_bgc.min_depth for ctd_bgc in ctd_bgcs],
121+
time=[ctd_bgc.spacetime.time for ctd_bgc in ctd_bgcs],
122+
max_depth=max_depths,
123+
min_depth=[ctd_bgc.min_depth for ctd_bgc in ctd_bgcs],
124+
winch_speed=[WINCH_SPEED for _ in ctd_bgcs],
125+
)
126+
127+
# define output file for the simulation
128+
out_file = ctd_bgc_particleset.ParticleFile(name=out_path, outputdt=outputdt)
129+
130+
# execute simulation
131+
ctd_bgc_particleset.execute(
132+
[_sample_o2, _sample_chlorophyll, _ctd_bgc_cast],
133+
endtime=fieldset_endtime,
134+
dt=DT,
135+
verbose_progress=False,
136+
output_file=out_file,
137+
)
138+
139+
# there should be no particles left, as they delete themselves when they resurface
140+
if len(ctd_bgc_particleset.particledata) != 0:
141+
raise ValueError(
142+
"Simulation ended before BGC CTD resurfaced. This most likely means the field time dimension did not match the simulation time span."
143+
)

src/virtualship/static/schedule.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ space_time_region:
1212
waypoints:
1313
- instrument:
1414
- CTD
15+
- CTD_BGC
1516
location:
1617
latitude: 0
1718
longitude: 0
22.8 KB
Binary file not shown.
22.8 KB
Binary file not shown.

tests/expedition/expedition_dir/ship_config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ ctd_config:
1414
max_depth_meter: -2000.0
1515
min_depth_meter: -11.0
1616
stationkeeping_time_minutes: 20.0
17+
ctd_bgc_config:
18+
max_depth_meter: -2000.0
19+
min_depth_meter: -11.0
20+
stationkeeping_time_minutes: 20.0
1721
drifter_config:
1822
depth_meter: 0.0
1923
lifetime_minutes: 40320.0

0 commit comments

Comments
 (0)