diff --git a/docs/user-guide/assignments/Research_Proposal_only.ipynb b/docs/user-guide/assignments/Research_Proposal_only.ipynb
index 7b8e86919..ac8f057c2 100644
--- a/docs/user-guide/assignments/Research_Proposal_only.ipynb
+++ b/docs/user-guide/assignments/Research_Proposal_only.ipynb
@@ -176,7 +176,7 @@
"\n",
"Please provide a scheme with number of necessary work and in-transit days within the working areas and station times. This can be downloaded from [the NIOZ MFP website](https://nioz.marinefacilitiesplanning.com/cruiselocationplanning#) using the Export button on the right.\n",
"\n",
- "Please indicate at each station what instruments you want to deploy (CTD, Argo float, drifter, XBT) and take the deployment time into account. If you plan to use Argo floats, please give the required depth and cycle duration. In case of the CTD the deployment time depends on the depth of the ocean. \n",
+ "Please indicate at each station what instruments you want to deploy (CTD, Argo float, drifter, XBT) and take the deployment time into account. If you plan to use Argo floats, please give the required depth and cycle duration.\n",
"\n",
"Here is some sample code to sample the depth using the bathymetry data that the Virtual Ship will also use. "
]
diff --git a/docs/user-guide/assignments/Research_proposal_intro.ipynb b/docs/user-guide/assignments/Research_proposal_intro.ipynb
index 3f3a3f539..2f19e2b1d 100644
--- a/docs/user-guide/assignments/Research_proposal_intro.ipynb
+++ b/docs/user-guide/assignments/Research_proposal_intro.ipynb
@@ -97,11 +97,6 @@
"\n",
"In total, therefore, you can expect a CTD deployment to take approximately 50 minutes.\n",
"\n",
- "\n",
- "
\n",
- "**Note**: If you are deploying CTDs in both standard and biogeochemical configurations (`CTD` and `CTD_BGC`) in your [VirtualShip expeditions](./Sail_the_ship.ipynb), you only need to factor in the 50 minutes **once** per waypoint, as both can be deployed on the same cast.\n",
- "
\n",
"**Note**: On pressing _Save Changes_ the tool will check the selections are valid (for example that the ship will be able to reach each waypoint in time). If they are, the changes will be saved to the `expedition.yaml` file, ready for the next steps. If your selections are invalid you should be provided with information on how to fix them.\n",
- "
\n",
- "\n",
- "
\n",
- "**Caution**: The `virtualship plan` tool will check that the ship can reach each waypoint according to the prescribed ship speed (10 knots). However, before the ultimate simulation step (i.e. step 6 below) there will be a final, automated check that the schedule also accounts for the time taken to conduct the measurements at each site (e.g. a CTD cast in deeper waters will take longer). Therefore, we recommend to take this extra time into account at this stage of the planning by estimating how long each measurement will take and adding this time on.\n",
"
"
]
},
diff --git a/docs/user-guide/assignments/Virtualship_research_proposal.ipynb b/docs/user-guide/assignments/Virtualship_research_proposal.ipynb
index 31c8e127a..87097baa9 100644
--- a/docs/user-guide/assignments/Virtualship_research_proposal.ipynb
+++ b/docs/user-guide/assignments/Virtualship_research_proposal.ipynb
@@ -132,7 +132,7 @@
"source": [
"7. **Scientific work program**\n",
"\n",
- "Please upload a screenshot of the MFP website inlcuding port of departure and arrival, all stations (and transects) and total time. Please indicate at each station what instruments you want to deploy (CTD, Argo float, drifter, XBT) and take the deployment time into account. If you plan to use Argo floats, please give the required depth and cycle duration. In case of the CTD the deployment time depends on the depth of the ocean. Note that transit times from the port of departure to the first station and from the last station in the working area to the port of arrival are regarded as work days at sea, so they are deducted from your (three-week) availability. "
+ "Please upload a screenshot of the MFP website inlcuding port of departure and arrival, all stations (and transects) and total time. Please indicate at each station what instruments you want to deploy (CTD, Argo float, drifter, XBT) and take the deployment time into account. If you plan to use Argo floats, please give the required depth and cycle duration. Note that transit times from the port of departure to the first station and from the last station in the working area to the port of arrival are regarded as work days at sea, so they are deducted from your (three-week) availability. "
]
},
{
diff --git a/docs/user-guide/quickstart.md b/docs/user-guide/quickstart.md
index 580b8db35..cc43b8a0a 100644
--- a/docs/user-guide/quickstart.md
+++ b/docs/user-guide/quickstart.md
@@ -124,10 +124,6 @@ When you are happy with your ship configuration and schedule plan, press _Save C
On pressing _Save Changes_ the tool will check the selections are valid (for example that the ship will be able to reach each waypoint in time). If they are, the changes will be saved to the `expedition.yaml` file, ready for the next steps. If your selections are invalid you should be provided with information on how to fix them.
```
-```{caution}
-The `virtualship plan` tool will check that the ship can reach each waypoint according to the prescribed ship speed. However, before the ultimate [simulation step](#run-the-expedition) there will be a final, automated check that the schedule also accounts for the time taken to conduct the measurements at each site (e.g. a CTD cast in deeper waters will take longer). Therefore, we recommend to take this extra time into account at this stage of the planning by estimating how long each measurement will take and adding this time on.
-```
-
## 4) Run the expedition
You are now ready to run your virtual expedition! This stage will simulate the measurements taken by the instruments you selected at each waypoint in your expedition schedule, using input data sourced from the [Copernicus Marine Data Store](https://data.marine.copernicus.eu/products).
diff --git a/docs/user-guide/tutorials/CTD_transects.ipynb b/docs/user-guide/tutorials/CTD_transects.ipynb
index 90f1ef2ce..bcefcb822 100644
--- a/docs/user-guide/tutorials/CTD_transects.ipynb
+++ b/docs/user-guide/tutorials/CTD_transects.ipynb
@@ -9,7 +9,7 @@
"\n",
"This notebook demonstrates a simple plotting exercise for CTD data across a transect, using the output of a VirtualShip expedition. There are example plots embedded at the end, but these will ultimately be replaced by your own versions as you work through the notebook.\n",
"\n",
- "We can plot physical (temperature, salinity) or biogeochemical data (oxygen, chlorophyll, primary production, phytoplankton, nutrients, pH) as measured by the VirtualShip `CTD` and `CTD_BGC` instruments, respectively.\n",
+ "We can plot physical (temperature, salinity) or biogeochemical data (oxygen, chlorophyll, primary production, phytoplankton, nutrients, pH) as measured by the VirtualShip `CTD` instrument.\n",
"\n",
"The plot(s) we will produce are simple plots which follow the trajectory of the expedition as a function of distance from the first waypoint, and are intended to be a starting point for your analysis. \n",
"\n",
@@ -82,13 +82,11 @@
"source": [
"#### Variable choice\n",
"\n",
- "You should now consider which variable from your CTD casts you would like to plot. Which ones are available to you will depend on whether you have used the `CTD` (physical variables) or `CTD_BGC` (biogeochemical) instrument, or both. Below is a list of all valid variable choices for both instruments...\n",
+ "You should now consider which variable from your CTD casts you would like to plot. Which ones are available to you will depend on which sensors you deployed the `CTD` instrument with (via the `virtualship plan` tool and/or your `expedition.yaml` file). Below is the full list of valid variable choices...\n",
"\n",
- "`CTD` (physical):\n",
+ "`CTD`:\n",
"- \"temperature\"\n",
"- \"salinity\"\n",
- "\n",
- "`CTD_BGC` (biogeochemical):\n",
"- \"oxygen\"\n",
"- \"nitrate\"\n",
"- \"phosphate\"\n",
@@ -191,15 +189,13 @@
},
{
"cell_type": "code",
- "execution_count": 76,
+ "execution_count": null,
"id": "13f4664b",
"metadata": {},
"outputs": [],
"source": [
"# load CTD data\n",
- "filename = (\n",
- " \"ctd.zarr\" if plot_variable in [\"temperature\", \"salinity\"] else \"ctd_bgc.zarr\"\n",
- ")\n",
+ "filename = \"ctd.zarr\"\n",
"ctd_ds = xr.open_dataset(f\"{data_dir}/{filename}\")\n",
"if ctd_ds[\"trajectory\"].size <= 1:\n",
" raise ValueError(\"Number of waypoints must be > 1\")"
@@ -482,7 +478,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.9"
+ "version": "3.12.12"
}
},
"nbformat": 4,
diff --git a/docs/user-guide/tutorials/working_with_expedition_yaml.md b/docs/user-guide/tutorials/working_with_expedition_yaml.md
index 15443a34a..af059b3f8 100644
--- a/docs/user-guide/tutorials/working_with_expedition_yaml.md
+++ b/docs/user-guide/tutorials/working_with_expedition_yaml.md
@@ -29,7 +29,6 @@ schedule: # <-- 1. expedition schedule section
waypoints:
- instrument: # <-- Waypoint 1
- CTD
- - CTD_BGC
- ARGO_FLOAT
- DRIFTER
location:
@@ -53,7 +52,6 @@ instruments_config: # <-- 2. instrument configuration section
ship_underwater_st_config:
period_minutes: 5.0
argo_float_config: ...
- ctd_bgc_config: ...
ctd_config: ...
drifter_config: ...
xbt_config: ...
@@ -75,7 +73,7 @@ This section contains a list of `waypoints` that define the expedition's route.
- **Instruments (`instrument`)**: A list of instruments to be deployed at that waypoint. Add or remove instruments by adding or deleting entries on _new lines_. The instrument selection can also be left empty (i.e., no instruments deployed at that waypoint) by setting the parameter to: `instrument: null`.
```{tip}
-Full list of instruments supported for deployment at waypoints (case-sensitive): `CTD`, `CTD_BGC`, `DRIFTER`, `ARGO_FLOAT`, `XBT` (or `null`).
+Full list of instruments supported for deployment at waypoints (case-sensitive): `CTD`, `DRIFTER`, `ARGO_FLOAT`, `XBT` (or `null`).
```
```{tip}
diff --git a/src/virtualship/cli/_plan.py b/src/virtualship/cli/_plan.py
index 895abf293..e397aad72 100644
--- a/src/virtualship/cli/_plan.py
+++ b/src/virtualship/cli/_plan.py
@@ -32,7 +32,6 @@
from virtualship.models import (
ADCPConfig,
ArgoFloatConfig,
- CTD_BGCConfig,
CTDConfig,
DrifterConfig,
Expedition,
@@ -109,15 +108,6 @@ def log_exception_to_file(
{"name": "stationkeeping_time", "minutes": True},
],
},
- "ctd_bgc_config": {
- "class": CTD_BGCConfig,
- "title": "CTD-BGC",
- "attributes": [
- {"name": "max_depth_meter"},
- {"name": "min_depth_meter"},
- {"name": "stationkeeping_time", "minutes": True},
- ],
- },
"xbt_config": {
"class": XBTConfig,
"title": "XBT",
diff --git a/src/virtualship/expedition/simulate_schedule.py b/src/virtualship/expedition/simulate_schedule.py
index d96efc913..6af9d80ce 100644
--- a/src/virtualship/expedition/simulate_schedule.py
+++ b/src/virtualship/expedition/simulate_schedule.py
@@ -10,7 +10,6 @@
from virtualship.instruments.argo_float import ArgoFloat
from virtualship.instruments.ctd import CTD
-from virtualship.instruments.ctd_bgc import CTD_BGC
from virtualship.instruments.drifter import Drifter
from virtualship.instruments.types import InstrumentType
from virtualship.instruments.xbt import XBT
@@ -53,7 +52,6 @@ class MeasurementsToSimulate:
InstrumentType.ARGO_FLOAT: "argo_floats",
InstrumentType.DRIFTER: "drifters",
InstrumentType.CTD: "ctds",
- InstrumentType.CTD_BGC: "ctd_bgcs",
InstrumentType.XBT: "xbts",
}
@@ -67,7 +65,6 @@ def get_attr_for_instrumenttype(cls, instrument_type):
argo_floats: list[ArgoFloat] = field(default_factory=list, init=False)
drifters: list[Drifter] = field(default_factory=list, init=False)
ctds: list[CTD] = field(default_factory=list, init=False)
- ctd_bgcs: list[CTD_BGC] = field(default_factory=list, init=False)
xbts: list[XBT] = field(default_factory=list, init=False)
@@ -265,12 +262,6 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta:
# time costs of each measurement
time_costs = [timedelta()]
- # check if both CTD and CTD_BGC are present
- # TODO: this can be avoided if CTD and CTD_BGC are merged into a single instrument
- both_ctd_and_bgc = (
- InstrumentType.CTD in instruments and InstrumentType.CTD_BGC in instruments
- )
-
for instrument in instruments:
if instrument is InstrumentType.ARGO_FLOAT:
self._measurements_to_simulate.argo_floats.append(
@@ -302,20 +293,7 @@ def _make_measurements(self, waypoint: Waypoint) -> timedelta:
time_costs.append(
self._expedition.instruments_config.ctd_config.stationkeeping_time
)
- elif instrument is InstrumentType.CTD_BGC:
- self._measurements_to_simulate.ctd_bgcs.append(
- CTD_BGC(
- spacetime=Spacetime(self._location, self._time),
- min_depth=self._expedition.instruments_config.ctd_bgc_config.min_depth_meter,
- max_depth=self._expedition.instruments_config.ctd_bgc_config.max_depth_meter,
- )
- )
- if both_ctd_and_bgc: # only need to add time cost once if both CTD and CTD_BGC are being taken; in reality they would be done on the same instrument
- pass
- else:
- time_costs.append(
- self._expedition.instruments_config.ctd_bgc_config.stationkeeping_time
- )
+
elif instrument is InstrumentType.DRIFTER:
self._measurements_to_simulate.drifters.append(
Drifter(
diff --git a/src/virtualship/instruments/__init__.py b/src/virtualship/instruments/__init__.py
index b593ed38b..0811a7d25 100644
--- a/src/virtualship/instruments/__init__.py
+++ b/src/virtualship/instruments/__init__.py
@@ -4,7 +4,6 @@
adcp,
argo_float,
ctd,
- ctd_bgc,
drifter,
ship_underwater_st,
xbt,
@@ -14,7 +13,6 @@
"adcp",
"argo_float",
"ctd",
- "ctd_bgc",
"drifter",
"ship_underwater_st",
"xbt",
diff --git a/src/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py
index 122e1461c..583a099cc 100644
--- a/src/virtualship/instruments/ctd.py
+++ b/src/virtualship/instruments/ctd.py
@@ -49,6 +49,8 @@ class CTD:
# SECTION: Kernels
# =====================================================
+## physical variables
+
def _sample_temperature(particle, fieldset, time):
particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon]
@@ -58,6 +60,40 @@ def _sample_salinity(particle, fieldset, time):
particle.salinity = fieldset.S[time, particle.depth, particle.lat, particle.lon]
+## bgc variables
+
+
+def _sample_o2(particle, fieldset, time):
+ particle.o2 = fieldset.o2[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_chlorophyll(particle, fieldset, time):
+ particle.chl = fieldset.chl[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_nitrate(particle, fieldset, time):
+ particle.no3 = fieldset.no3[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_phosphate(particle, fieldset, time):
+ particle.po4 = fieldset.po4[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_ph(particle, fieldset, time):
+ particle.ph = fieldset.ph[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_phytoplankton(particle, fieldset, time):
+ particle.phyc = fieldset.phyc[time, particle.depth, particle.lat, particle.lon]
+
+
+def _sample_primary_production(particle, fieldset, time):
+ particle.nppv = fieldset.nppv[time, particle.depth, particle.lat, particle.lon]
+
+
+## cast
+
+
def _ctd_cast(particle, fieldset, time):
# lowering
if particle.raising == 0:
@@ -84,6 +120,13 @@ class CTDInstrument(Instrument):
sensor_kernels: ClassVar[dict[SensorType, Callable]] = {
SensorType.TEMPERATURE: _sample_temperature,
SensorType.SALINITY: _sample_salinity,
+ SensorType.OXYGEN: _sample_o2,
+ SensorType.CHLOROPHYLL: _sample_chlorophyll,
+ SensorType.NITRATE: _sample_nitrate,
+ SensorType.PHOSPHATE: _sample_phosphate,
+ SensorType.PH: _sample_ph,
+ SensorType.PHYTOPLANKTON: _sample_phytoplankton,
+ SensorType.PRIMARY_PRODUCTION: _sample_primary_production,
}
def __init__(self, expedition, from_data):
diff --git a/src/virtualship/instruments/ctd_bgc.py b/src/virtualship/instruments/ctd_bgc.py
deleted file mode 100644
index 3568d0a80..000000000
--- a/src/virtualship/instruments/ctd_bgc.py
+++ /dev/null
@@ -1,229 +0,0 @@
-from collections.abc import Callable
-from dataclasses import dataclass
-from datetime import timedelta
-from typing import ClassVar
-
-import numpy as np
-from parcels import JITParticle, ParticleSet, Variable
-
-from virtualship.instruments.base import Instrument
-from virtualship.instruments.sensors import SensorType
-from virtualship.instruments.types import InstrumentType
-from virtualship.models.spacetime import Spacetime
-from virtualship.utils import (
- add_dummy_UV,
- build_particle_class_from_sensors,
- register_instrument,
-)
-
-# =====================================================
-# SECTION: Dataclass
-# =====================================================
-
-
-@dataclass
-class CTD_BGC:
- """CTD_BGC configuration."""
-
- name: ClassVar[str] = "CTD_BGC"
- spacetime: Spacetime
- min_depth: float
- max_depth: float
-
-
-# =====================================================
-# SECTION: non-sensor Particle Variables (non-sampling)
-# =====================================================
-
-_CTD_BGC_NONSENSOR_VARIABLES = [
- Variable("raising", dtype=np.int8, initial=0.0), # bool. 0 is False, 1 is True.
- Variable("max_depth", dtype=np.float32),
- Variable("min_depth", dtype=np.float32),
- Variable("winch_speed", dtype=np.float32),
-]
-
-# =====================================================
-# SECTION: Kernels
-# =====================================================
-
-
-def _sample_o2(particle, fieldset, time):
- particle.o2 = fieldset.o2[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_chlorophyll(particle, fieldset, time):
- particle.chl = fieldset.chl[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_nitrate(particle, fieldset, time):
- particle.no3 = fieldset.no3[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_phosphate(particle, fieldset, time):
- particle.po4 = fieldset.po4[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_ph(particle, fieldset, time):
- particle.ph = fieldset.ph[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_phytoplankton(particle, fieldset, time):
- particle.phyc = fieldset.phyc[time, particle.depth, particle.lat, particle.lon]
-
-
-def _sample_primary_production(particle, fieldset, time):
- particle.nppv = fieldset.nppv[time, particle.depth, particle.lat, particle.lon]
-
-
-def _ctd_bgc_cast(particle, fieldset, time):
- # lowering
- if particle.raising == 0:
- particle_ddepth = -particle.winch_speed * particle.dt
- if particle.depth + particle_ddepth < particle.max_depth:
- particle.raising = 1
- particle_ddepth = -particle_ddepth
- # raising
- else:
- particle_ddepth = particle.winch_speed * particle.dt
- if particle.depth + particle_ddepth > particle.min_depth:
- particle.delete()
-
-
-# =====================================================
-# SECTION: Instrument Class
-# =====================================================
-
-
-@register_instrument(InstrumentType.CTD_BGC)
-class CTD_BGCInstrument(Instrument):
- """CTD_BGC instrument class."""
-
- sensor_kernels: ClassVar[dict[SensorType, Callable]] = {
- SensorType.OXYGEN: _sample_o2,
- SensorType.CHLOROPHYLL: _sample_chlorophyll,
- SensorType.NITRATE: _sample_nitrate,
- SensorType.PHOSPHATE: _sample_phosphate,
- SensorType.PH: _sample_ph,
- SensorType.PHYTOPLANKTON: _sample_phytoplankton,
- SensorType.PRIMARY_PRODUCTION: _sample_primary_production,
- }
-
- def __init__(self, expedition, from_data):
- """Initialize CTD_BGCInstrument."""
- variables = expedition.instruments_config.ctd_bgc_config.active_variables()
- limit_spec = {
- "spatial": True
- } # spatial limits; lat/lon constrained to waypoint locations + buffer
-
- super().__init__(
- expedition,
- variables,
- add_bathymetry=True,
- allow_time_extrapolation=True,
- verbose_progress=False,
- spacetime_buffer_size=None,
- limit_spec=limit_spec,
- from_data=from_data,
- )
-
- def simulate(self, measurements, out_path) -> None:
- """Simulate BGC CTD measurements using Parcels."""
- WINCH_SPEED = 1.0 # sink and rise speed in m/s
- DT = 10.0 # dt of CTD_BGC simulation integrator
- OUTPUT_DT = timedelta(seconds=10) # output dt for CTD_BGC simulation
-
- if len(measurements) == 0:
- print(
- "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."
- )
- # TODO when Parcels supports it this check can be removed.
- return
-
- fieldset = self.load_input_data()
-
- # add dummy U
- add_dummy_UV(fieldset) # TODO: parcels v3 bodge; remove when parcels v4 is used
-
- # use first active field for time reference
- _time_ref_key = next(iter(self.variables))
- _time_ref_field = getattr(fieldset, _time_ref_key)
- fieldset_starttime = _time_ref_field.grid.time_origin.fulltime(
- _time_ref_field.grid.time_full[0]
- )
- fieldset_endtime = _time_ref_field.grid.time_origin.fulltime(
- _time_ref_field.grid.time_full[-1]
- )
-
- # deploy time for all ctds should be later than fieldset start time
- if not all(
- [
- np.datetime64(ctd_bgc.spacetime.time) >= fieldset_starttime
- for ctd_bgc in measurements
- ]
- ):
- raise ValueError("BGC CTD deployed before fieldset starts.")
-
- # depth the bgc ctd will go to. shallowest between bgc ctd max depth and bathymetry.
- max_depths = [
- max(
- ctd_bgc.max_depth,
- fieldset.bathymetry.eval(
- z=0,
- y=ctd_bgc.spacetime.location.lat,
- x=ctd_bgc.spacetime.location.lon,
- time=0,
- ),
- )
- for ctd_bgc in measurements
- ]
-
- # CTD depth can not be too shallow, because kernel would break.
- # This shallow is not useful anyway, no need to support.
- if not all([max_depth <= -DT * WINCH_SPEED for max_depth in max_depths]):
- raise ValueError(
- f"BGC CTD max_depth or bathymetry shallower than maximum {-DT * WINCH_SPEED}"
- )
-
- # build dynamic particle class from the active sensors
- ctd_bgc_config = self.expedition.instruments_config.ctd_bgc_config
- _CTD_BGCParticle = build_particle_class_from_sensors(
- ctd_bgc_config.sensors, _CTD_BGC_NONSENSOR_VARIABLES, JITParticle
- )
-
- # define parcel particles
- ctd_bgc_particleset = ParticleSet(
- fieldset=fieldset,
- pclass=_CTD_BGCParticle,
- lon=[ctd_bgc.spacetime.location.lon for ctd_bgc in measurements],
- lat=[ctd_bgc.spacetime.location.lat for ctd_bgc in measurements],
- depth=[ctd_bgc.min_depth for ctd_bgc in measurements],
- time=[ctd_bgc.spacetime.time for ctd_bgc in measurements],
- max_depth=max_depths,
- min_depth=[ctd_bgc.min_depth for ctd_bgc in measurements],
- winch_speed=[WINCH_SPEED for _ in measurements],
- )
-
- # define output file for the simulation
- out_file = ctd_bgc_particleset.ParticleFile(name=out_path, outputdt=OUTPUT_DT)
-
- # build kernel list from active sensors only
- sampling_kernels = [
- self.sensor_kernels[sc.sensor_type]
- for sc in ctd_bgc_config.sensors
- if sc.enabled and sc.sensor_type in self.sensor_kernels
- ]
-
- # execute simulation
- ctd_bgc_particleset.execute(
- [*sampling_kernels, _ctd_bgc_cast],
- endtime=fieldset_endtime,
- dt=DT,
- verbose_progress=self.verbose_progress,
- output_file=out_file,
- )
-
- # there should be no particles left, as they delete themselves when they resurface
- if len(ctd_bgc_particleset.particledata) != 0:
- raise ValueError(
- "Simulation ended before BGC CTD resurfaced. This most likely means the field time dimension did not match the simulation time span."
- )
diff --git a/src/virtualship/instruments/types.py b/src/virtualship/instruments/types.py
index 489a331f6..aa47bc6bf 100644
--- a/src/virtualship/instruments/types.py
+++ b/src/virtualship/instruments/types.py
@@ -7,7 +7,6 @@ class InstrumentType(Enum):
"""Types of the instruments."""
CTD = "CTD"
- CTD_BGC = "CTD_BGC"
DRIFTER = "DRIFTER"
ARGO_FLOAT = "ARGO_FLOAT"
XBT = "XBT"
diff --git a/src/virtualship/models/__init__.py b/src/virtualship/models/__init__.py
index 7d3b08819..71f242117 100644
--- a/src/virtualship/models/__init__.py
+++ b/src/virtualship/models/__init__.py
@@ -4,7 +4,6 @@
from .expedition import (
ADCPConfig,
ArgoFloatConfig,
- CTD_BGCConfig,
CTDConfig,
DrifterConfig,
Expedition,
@@ -29,7 +28,6 @@
"ArgoFloatConfig",
"ADCPConfig",
"CTDConfig",
- "CTD_BGCConfig",
"ShipUnderwaterSTConfig",
"DrifterConfig",
"XBTConfig",
diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py
index 32855fc94..eef23c761 100644
--- a/src/virtualship/models/expedition.py
+++ b/src/virtualship/models/expedition.py
@@ -352,30 +352,6 @@ class CTDConfig(_InstrumentConfigMixin, pydantic.BaseModel):
default_factory=lambda: [
SensorConfig(sensor_type=SensorType.TEMPERATURE),
SensorConfig(sensor_type=SensorType.SALINITY),
- ],
- description=("Sensors fitted to the CTD. Supported: TEMPERATURE, SALINITY. "),
- )
-
- model_config = pydantic.ConfigDict(populate_by_name=True)
-
-
-@register_instrument_config(InstrumentType.CTD_BGC)
-class CTD_BGCConfig(_InstrumentConfigMixin, pydantic.BaseModel):
- """Configuration for CTD_BGC instrument."""
-
- _instrument_type: ClassVar[InstrumentType] = InstrumentType.CTD_BGC
- _instrument_name: ClassVar[str] = "CTD_BGC"
-
- stationkeeping_time: timedelta = pydantic.Field(
- serialization_alias="stationkeeping_time_minutes",
- validation_alias="stationkeeping_time_minutes",
- gt=timedelta(),
- )
- min_depth_meter: float = pydantic.Field(le=0.0)
- max_depth_meter: float = pydantic.Field(le=0.0)
-
- sensors: list[SensorConfig] = pydantic.Field(
- default_factory=lambda: [
SensorConfig(sensor_type=SensorType.OXYGEN),
SensorConfig(sensor_type=SensorType.CHLOROPHYLL),
SensorConfig(sensor_type=SensorType.NITRATE),
@@ -385,8 +361,7 @@ class CTD_BGCConfig(_InstrumentConfigMixin, pydantic.BaseModel):
SensorConfig(sensor_type=SensorType.PRIMARY_PRODUCTION),
],
description=(
- "Sensors fitted to the BGC CTD. "
- "Supported: OXYGEN, CHLOROPHYLL, NITRATE, PHOSPHATE, PH, PHYTOPLANKTON, PRIMARY_PRODUCTION. "
+ "Sensors fitted to the CTD. Supported: TEMPERATURE, SALINITY, OXYGEN, CHLOROPHYLL, NITRATE, PHOSPHATE, PH, PHYTOPLANKTON, PRIMARY_PRODUCTION. "
),
)
@@ -488,13 +463,6 @@ class InstrumentsConfig(pydantic.BaseModel):
If None, no CTDs can be cast.
"""
- ctd_bgc_config: CTD_BGCConfig | None = None
- """
- CTD_BGC configuration.
-
- If None, no BGC CTDs can be cast.
- """
-
ship_underwater_st_config: ShipUnderwaterSTConfig | None = None
"""
Ship underwater salinity temperature measurementconfiguration.
@@ -531,7 +499,6 @@ def verify(self, expedition: Expedition) -> None:
InstrumentType.DRIFTER: "drifter_config",
InstrumentType.XBT: "xbt_config",
InstrumentType.CTD: "ctd_config",
- InstrumentType.CTD_BGC: "ctd_bgc_config",
InstrumentType.ADCP: "adcp_config",
InstrumentType.UNDERWATER_ST: "ship_underwater_st_config",
}
diff --git a/src/virtualship/static/expedition.yaml b/src/virtualship/static/expedition.yaml
index 8ab72f8a2..3be45c4d7 100644
--- a/src/virtualship/static/expedition.yaml
+++ b/src/virtualship/static/expedition.yaml
@@ -6,7 +6,6 @@ schedule:
waypoints:
- instrument:
- CTD
- - CTD_BGC
location:
latitude: 0
longitude: 0
@@ -60,11 +59,6 @@ instruments_config:
sensors:
- TEMPERATURE
- SALINITY
- ctd_bgc_config:
- max_depth_meter: -2000.0
- min_depth_meter: -11.0
- stationkeeping_time_minutes: 50.0
- sensors:
- OXYGEN
- CHLOROPHYLL
- NITRATE
diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py
index 37bb44c43..7ad275cd9 100644
--- a/src/virtualship/utils.py
+++ b/src/virtualship/utils.py
@@ -641,14 +641,6 @@ def _calc_wp_stationkeeping_time(
if not wp_instrument_types:
wp_instrument_types = []
- # TODO: this can be removed if/when CTD and CTD_BGC are merged to a single instrument
- from virtualship.instruments.types import InstrumentType
-
- both_ctd_and_bgc = (
- InstrumentType.CTD in wp_instrument_types
- and InstrumentType.CTD_BGC in wp_instrument_types
- )
-
# extract configs for all instruments present in expedition
valid_instrument_configs = [
iconfig for _, iconfig in instruments_config.__dict__.items() if iconfig
@@ -669,10 +661,6 @@ def _calc_wp_stationkeeping_time(
# get wp total stationkeeping time
cumulative_stationkeeping_time = timedelta()
for iconfig in wp_instrument_configs:
- if both_ctd_and_bgc and iconfig.__class__.__name__ == instrument_config_map.get(
- InstrumentType.CTD_BGC
- ):
- continue # only count stationkeeping once when both CTD and CTD_BGC are present; in reality they would be done on the same instrument
if hasattr(iconfig, "stationkeeping_time"):
cumulative_stationkeeping_time += iconfig.stationkeeping_time
diff --git a/tests/expedition/expedition_dir/expedition.yaml b/tests/expedition/expedition_dir/expedition.yaml
index 0d2bb1551..6392076b3 100644
--- a/tests/expedition/expedition_dir/expedition.yaml
+++ b/tests/expedition/expedition_dir/expedition.yaml
@@ -37,10 +37,6 @@ instruments_config:
max_depth_meter: -2000.0
min_depth_meter: -11.0
stationkeeping_time_minutes: 50.0
- ctd_bgc_config:
- max_depth_meter: -2000.0
- min_depth_meter: -11.0
- stationkeeping_time_minutes: 50.0
drifter_config:
depth_meter: -1.0
lifetime_days: 28.0
diff --git a/tests/expedition/expedition_dir/input_data/.gitignore b/tests/expedition/expedition_dir/input_data/.gitignore
index b323da5ba..2235154f2 100644
--- a/tests/expedition/expedition_dir/input_data/.gitignore
+++ b/tests/expedition/expedition_dir/input_data/.gitignore
@@ -7,4 +7,3 @@
!ship_uv.nc
!drifter_t.nc
!drifter_uv.nc
-!ctd_bgc_*.nc
diff --git a/tests/expedition/test_expedition.py b/tests/expedition/test_expedition.py
index 6008309af..f6a84dfe0 100644
--- a/tests/expedition/test_expedition.py
+++ b/tests/expedition/test_expedition.py
@@ -268,12 +268,6 @@ def instruments_config_no_ctd(expedition):
return expedition.instruments_config
-@pytest.fixture
-def instruments_config_no_ctd_bgc(expedition):
- delattr(expedition.instruments_config, "ctd_bgc_config")
- return expedition.instruments_config
-
-
@pytest.fixture
def instruments_config_no_argo_float(expedition):
delattr(expedition.instruments_config, "argo_float_config")
@@ -321,12 +315,6 @@ def test_verify_instruments_config_no_instrument(expedition, expedition_no_xbt)
"Expedition includes instrument 'CTD', but instruments_config does not provide configuration for it.",
id="InstrumentsConfigNoCTD",
),
- pytest.param(
- "instruments_config_no_ctd_bgc",
- InstrumentsConfigError,
- "Expedition includes instrument 'CTD_BGC', but instruments_config does not provide configuration for it.",
- id="InstrumentsConfigNoCTD_BGC",
- ),
pytest.param(
"instruments_config_no_argo_float",
InstrumentsConfigError,
diff --git a/tests/expedition/test_simulate_schedule.py b/tests/expedition/test_simulate_schedule.py
index f90eecc2c..35dfbdea6 100644
--- a/tests/expedition/test_simulate_schedule.py
+++ b/tests/expedition/test_simulate_schedule.py
@@ -56,9 +56,6 @@ def test_time_in_minutes_in_ship_schedule() -> None:
).instruments_config
assert instruments_config.adcp_config.period == timedelta(minutes=5)
assert instruments_config.ctd_config.stationkeeping_time == timedelta(minutes=50)
- assert instruments_config.ctd_bgc_config.stationkeeping_time == timedelta(
- minutes=50
- )
assert instruments_config.argo_float_config.stationkeeping_time == timedelta(
minutes=20
)
diff --git a/tests/instruments/test_ctd.py b/tests/instruments/test_ctd.py
index 4fd6e28d9..c080f0f55 100644
--- a/tests/instruments/test_ctd.py
+++ b/tests/instruments/test_ctd.py
@@ -25,6 +25,7 @@
def test_simulate_ctds(tmpdir) -> None:
+ """Test that CTDInstrument simulates measurements correctly, incuding sampling physical and bgc variables."""
# arbitrary time offset for the dummy fieldset
base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d")
@@ -54,12 +55,18 @@ def test_simulate_ctds(tmpdir) -> None:
"surface": {
"salinity": 5,
"temperature": 6,
+ "o2": 10.0,
+ "chl": 20.0,
+ "no3": 30.0,
"lat": ctds[0].spacetime.location.lat,
"lon": ctds[0].spacetime.location.lon,
},
"maxdepth": {
"salinity": 7,
"temperature": 8,
+ "o2": 11.0,
+ "chl": 21.0,
+ "no3": 31.0,
"lat": ctds[0].spacetime.location.lat,
"lon": ctds[0].spacetime.location.lon,
},
@@ -68,12 +75,18 @@ def test_simulate_ctds(tmpdir) -> None:
"surface": {
"salinity": 5,
"temperature": 6,
+ "o2": 12.0,
+ "chl": 22.0,
+ "no3": 32.0,
"lat": ctds[1].spacetime.location.lat,
"lon": ctds[1].spacetime.location.lon,
},
"maxdepth": {
"salinity": 7,
"temperature": 8,
+ "o2": 13.0,
+ "chl": 23.0,
+ "no3": 33.0,
"lat": ctds[1].spacetime.location.lat,
"lon": ctds[1].spacetime.location.lon,
},
@@ -86,6 +99,9 @@ def test_simulate_ctds(tmpdir) -> None:
v = np.zeros((2, 2, 2, 2))
t = np.zeros((2, 2, 2, 2))
s = np.zeros((2, 2, 2, 2))
+ o2 = np.zeros((2, 2, 2, 2))
+ chl = np.zeros((2, 2, 2, 2))
+ no3 = np.zeros((2, 2, 2, 2))
t[:, 1, 0, 1] = ctd_exp[0]["surface"]["temperature"]
t[:, 0, 0, 1] = ctd_exp[0]["maxdepth"]["temperature"]
@@ -97,8 +113,23 @@ def test_simulate_ctds(tmpdir) -> None:
s[:, 1, 1, 0] = ctd_exp[1]["surface"]["salinity"]
s[:, 0, 1, 0] = ctd_exp[1]["maxdepth"]["salinity"]
+ o2[:, 1, 0, 1] = ctd_exp[0]["surface"]["o2"]
+ o2[:, 0, 0, 1] = ctd_exp[0]["maxdepth"]["o2"]
+ o2[:, 1, 1, 0] = ctd_exp[1]["surface"]["o2"]
+ o2[:, 0, 1, 0] = ctd_exp[1]["maxdepth"]["o2"]
+
+ chl[:, 1, 0, 1] = ctd_exp[0]["surface"]["chl"]
+ chl[:, 0, 0, 1] = ctd_exp[0]["maxdepth"]["chl"]
+ chl[:, 1, 1, 0] = ctd_exp[1]["surface"]["chl"]
+ chl[:, 0, 1, 0] = ctd_exp[1]["maxdepth"]["chl"]
+
+ no3[:, 1, 0, 1] = ctd_exp[0]["surface"]["no3"]
+ no3[:, 0, 0, 1] = ctd_exp[0]["maxdepth"]["no3"]
+ no3[:, 1, 1, 0] = ctd_exp[1]["surface"]["no3"]
+ no3[:, 0, 1, 0] = ctd_exp[1]["maxdepth"]["no3"]
+
fieldset = FieldSet.from_data(
- {"V": v, "U": u, "T": t, "S": s},
+ {"V": v, "U": u, "T": t, "S": s, "o2": o2, "chl": chl, "no3": no3},
{
"time": [
np.datetime64(base_time + datetime.timedelta(hours=0)),
@@ -130,6 +161,9 @@ class schedule:
sensors=[
SensorConfig(sensor_type=SensorType.TEMPERATURE),
SensorConfig(sensor_type=SensorType.SALINITY),
+ SensorConfig(sensor_type=SensorType.OXYGEN),
+ SensorConfig(sensor_type=SensorType.CHLOROPHYLL),
+ SensorConfig(sensor_type=SensorType.NITRATE),
],
)
)
@@ -160,7 +194,7 @@ class schedule:
(obs_maxdepth, "maxdepth"),
]:
exp = exp_bothloc[loc]
- for var in ["salinity", "temperature", "lat", "lon"]:
+ for var in ["salinity", "temperature", "o2", "chl", "no3", "lat", "lon"]:
obs_value = obs[var].values.item()
exp_value = exp[var]
@@ -177,10 +211,10 @@ def test_ctd_sensor_config_active_variables() -> None:
max_depth_meter=-2000.0,
sensors=[
SensorConfig(sensor_type=SensorType.TEMPERATURE),
- SensorConfig(sensor_type=SensorType.SALINITY),
+ SensorConfig(sensor_type=SensorType.OXYGEN),
],
)
- assert config_both.active_variables() == {"T": "thetao", "S": "so"}
+ assert config_both.active_variables() == {"T": "thetao", "o2": "o2"}
config_temp_only = CTDConfig(
stationkeeping_time_minutes=50,
@@ -200,15 +234,18 @@ def test_ctd_sensor_config_yaml() -> None:
min_depth_meter=-11.0,
max_depth_meter=-2000.0,
sensors=[
- SensorConfig(sensor_type=SensorType.TEMPERATURE)
+ SensorConfig(sensor_type=SensorType.TEMPERATURE),
+ SensorConfig(sensor_type=SensorType.OXYGEN),
], # SALINITY omitted = disabled
)
dumped = config.model_dump(by_alias=True)
loaded = CTDConfig.model_validate(dumped)
- assert len(loaded.sensors) == 1
+ assert len(loaded.sensors) == 2
assert loaded.sensors[0].sensor_type == SensorType.TEMPERATURE
assert loaded.sensors[0].enabled is True
+ assert loaded.sensors[1].sensor_type == SensorType.OXYGEN
+ assert loaded.sensors[1].enabled is True
def test_ctd_disabled_sensor_absent(tmpdir) -> None:
@@ -270,23 +307,43 @@ class schedule:
def test_ctd_supported_sensors():
- """CTD supports TEMPERATURE and SALINITY."""
+ """CTD supports TEMPERATURE, SALINITY and all BGC sensors."""
from virtualship.utils import get_supported_sensors
assert get_supported_sensors(InstrumentType.CTD) == frozenset(
- {SensorType.TEMPERATURE, SensorType.SALINITY}
+ {
+ SensorType.TEMPERATURE,
+ SensorType.SALINITY,
+ SensorType.OXYGEN,
+ SensorType.CHLOROPHYLL,
+ SensorType.NITRATE,
+ SensorType.PHOSPHATE,
+ SensorType.PH,
+ SensorType.PHYTOPLANKTON,
+ SensorType.PRIMARY_PRODUCTION,
+ }
)
def test_ctd_config_default_sensors():
- """CTDConfig defaults to TEMPERATURE + SALINITY."""
+ """CTDConfig defaults to all supported sensors (phys + bgc)."""
config = CTDConfig(
stationkeeping_time_minutes=50,
min_depth_meter=-11.0,
max_depth_meter=-2000.0,
)
types = {sc.sensor_type for sc in config.sensors}
- assert types == {SensorType.TEMPERATURE, SensorType.SALINITY}
+ assert types == {
+ SensorType.TEMPERATURE,
+ SensorType.SALINITY,
+ SensorType.OXYGEN,
+ SensorType.CHLOROPHYLL,
+ SensorType.NITRATE,
+ SensorType.PHOSPHATE,
+ SensorType.PH,
+ SensorType.PHYTOPLANKTON,
+ SensorType.PRIMARY_PRODUCTION,
+ }
# TODO: may need to be removed if add ADCP to CTDs in future PR...
@@ -299,3 +356,59 @@ def test_ctd_config_unsupported_sensor_rejected():
max_depth_meter=-2000.0,
sensors=[SensorConfig(sensor_type=SensorType.VELOCITY)],
)
+
+
+def test_sensor_absent(tmpdir) -> None:
+ """A (BGC) sensor that is disabled must not appear in the zarr output."""
+ base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d")
+
+ ctds = [
+ CTD(
+ spacetime=Spacetime(
+ location=Location(latitude=0, longitude=0),
+ time=base_time,
+ ),
+ min_depth=0,
+ max_depth=-20,
+ ),
+ ]
+
+ o2_data = np.full((2, 2, 2), 5.0)
+ fieldset = FieldSet.from_data(
+ {"o2": o2_data},
+ {
+ "lon": np.array([0.0, 1.0]),
+ "lat": np.array([0.0, 1.0]),
+ "time": [
+ np.datetime64(base_time + datetime.timedelta(seconds=0)),
+ np.datetime64(base_time + datetime.timedelta(hours=4)),
+ ],
+ },
+ )
+ fieldset.add_field(Field("bathymetry", [-1000], lon=0, lat=0))
+
+ class DummyExpedition:
+ class schedule:
+ waypoints = [Waypoint(location=Location(1, 2), time=base_time)]
+
+ instruments_config = InstrumentsConfig(
+ ctd_config=CTDConfig(
+ stationkeeping_time_minutes=50,
+ min_depth_meter=-11.0,
+ max_depth_meter=-2000.0,
+ sensors=[
+ SensorConfig(sensor_type=SensorType.OXYGEN),
+ # CHLOROPHYLL omitted = disabled
+ ],
+ )
+ )
+
+ expedition = DummyExpedition()
+ ctd_instrument = CTDInstrument(expedition, None)
+ out_path = tmpdir.join("out_bgc_disabled.zarr")
+ ctd_instrument.load_input_data = lambda: fieldset
+ ctd_instrument.simulate(ctds, out_path)
+
+ results = xr.open_zarr(out_path)
+ assert "o2" in results, "Enabled BGC sensor variable must be present"
+ assert "chl" not in results, "Disabled sensor variable must be absent from output"
diff --git a/tests/instruments/test_ctd_bgc.py b/tests/instruments/test_ctd_bgc.py
deleted file mode 100644
index ad485617e..000000000
--- a/tests/instruments/test_ctd_bgc.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""
-Test the simulation of CTD_BGC instruments.
-
-Fields are kept static over time and time component of CTD_BGC measurements is not tested because it's tricky to provide expected measurements.
-"""
-
-import datetime
-
-import numpy as np
-import pydantic
-import pytest
-import xarray as xr
-
-from parcels import Field, FieldSet
-from virtualship.instruments.ctd_bgc import CTD_BGC, CTD_BGCInstrument
-from virtualship.instruments.sensors import SensorType
-from virtualship.models import Location, Spacetime
-from virtualship.models.expedition import (
- CTD_BGCConfig,
- InstrumentsConfig,
- SensorConfig,
- Waypoint,
-)
-
-
-def test_simulate_ctd_bgcs(tmpdir) -> None:
- # arbitrary time offset for the dummy fieldset
- base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d")
-
- # where to cast CTD_BGCs
- ctd_bgcs = [
- CTD_BGC(
- spacetime=Spacetime(
- location=Location(latitude=0, longitude=1),
- time=base_time + datetime.timedelta(hours=0),
- ),
- min_depth=0,
- max_depth=float("-inf"),
- ),
- CTD_BGC(
- spacetime=Spacetime(
- location=Location(latitude=1, longitude=0),
- time=base_time,
- ),
- min_depth=0,
- max_depth=float("-inf"),
- ),
- ]
-
- # expected observations for ctd_bgcs at surface and at maximum depth
- ctd_bgc_exp = [
- {
- "surface": {
- "o2": 9,
- "chl": 10,
- "no3": 13,
- "po4": 14,
- "ph": 8.1,
- "phyc": 15,
- "nppv": 17,
- "lat": ctd_bgcs[0].spacetime.location.lat,
- "lon": ctd_bgcs[0].spacetime.location.lon,
- },
- "maxdepth": {
- "o2": 11,
- "chl": 12,
- "no3": 18,
- "po4": 19,
- "ph": 8.0,
- "phyc": 20,
- "nppv": 22,
- "lat": ctd_bgcs[0].spacetime.location.lat,
- "lon": ctd_bgcs[0].spacetime.location.lon,
- },
- },
- {
- "surface": {
- "o2": 9,
- "chl": 10,
- "no3": 13,
- "po4": 14,
- "ph": 8.1,
- "phyc": 15,
- "nppv": 17,
- "lat": ctd_bgcs[1].spacetime.location.lat,
- "lon": ctd_bgcs[1].spacetime.location.lon,
- },
- "maxdepth": {
- "o2": 11,
- "chl": 12,
- "no3": 18,
- "po4": 19,
- "ph": 8.0,
- "phyc": 20,
- "nppv": 22,
- "lat": ctd_bgcs[1].spacetime.location.lat,
- "lon": ctd_bgcs[1].spacetime.location.lon,
- },
- },
- ]
-
- # create fieldset based on the expected observations
- # indices are time, depth, latitude, longitude
- u = np.zeros((2, 2, 2, 2))
- v = np.zeros((2, 2, 2, 2))
- o2 = np.zeros((2, 2, 2, 2))
- chl = np.zeros((2, 2, 2, 2))
- no3 = np.zeros((2, 2, 2, 2))
- po4 = np.zeros((2, 2, 2, 2))
- ph = np.zeros((2, 2, 2, 2))
- phyc = np.zeros((2, 2, 2, 2))
- nppv = np.zeros((2, 2, 2, 2))
-
- # Fill fields for both CTDs at surface and maxdepth
- o2[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["o2"]
- o2[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["o2"]
- o2[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["o2"]
- o2[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["o2"]
-
- chl[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["chl"]
- chl[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["chl"]
- chl[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["chl"]
- chl[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["chl"]
-
- no3[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["no3"]
- no3[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["no3"]
- no3[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["no3"]
- no3[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["no3"]
-
- po4[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["po4"]
- po4[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["po4"]
- po4[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["po4"]
- po4[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["po4"]
-
- ph[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["ph"]
- ph[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["ph"]
- ph[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["ph"]
- ph[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["ph"]
-
- phyc[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["phyc"]
- phyc[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["phyc"]
- phyc[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["phyc"]
- phyc[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["phyc"]
-
- nppv[:, 1, 0, 1] = ctd_bgc_exp[0]["surface"]["nppv"]
- nppv[:, 0, 0, 1] = ctd_bgc_exp[0]["maxdepth"]["nppv"]
- nppv[:, 1, 1, 0] = ctd_bgc_exp[1]["surface"]["nppv"]
- nppv[:, 0, 1, 0] = ctd_bgc_exp[1]["maxdepth"]["nppv"]
-
- fieldset = FieldSet.from_data(
- {
- "V": v,
- "U": u,
- "o2": o2,
- "chl": chl,
- "no3": no3,
- "po4": po4,
- "ph": ph,
- "phyc": phyc,
- "nppv": nppv,
- },
- {
- "time": [
- np.datetime64(base_time + datetime.timedelta(hours=0)),
- np.datetime64(base_time + datetime.timedelta(hours=1)),
- ],
- "depth": [-1000, 0],
- "lat": [0, 1],
- "lon": [0, 1],
- },
- )
- fieldset.add_field(Field("bathymetry", [-1000], lon=0, lat=0))
-
- # dummy expedition for CTD_BGCInstrument
- class DummyExpedition:
- class schedule:
- # ruff: noqa
- waypoints = [
- Waypoint(
- location=Location(1, 2),
- time=base_time,
- ),
- ]
-
- instruments_config = InstrumentsConfig(
- ctd_bgc_config=CTD_BGCConfig(
- stationkeeping_time_minutes=50,
- min_depth_meter=-11.0,
- max_depth_meter=-2000.0,
- sensors=[
- SensorConfig(sensor_type=SensorType.OXYGEN),
- SensorConfig(sensor_type=SensorType.CHLOROPHYLL),
- SensorConfig(sensor_type=SensorType.NITRATE),
- SensorConfig(sensor_type=SensorType.PHOSPHATE),
- SensorConfig(sensor_type=SensorType.PH),
- SensorConfig(sensor_type=SensorType.PHYTOPLANKTON),
- SensorConfig(sensor_type=SensorType.PRIMARY_PRODUCTION),
- ],
- )
- )
-
- expedition = DummyExpedition()
- from_data = None
-
- ctd_bgc_instrument = CTD_BGCInstrument(expedition, from_data)
- out_path = tmpdir.join("out.zarr")
-
- ctd_bgc_instrument.load_input_data = lambda: fieldset
- ctd_bgc_instrument.simulate(ctd_bgcs, out_path)
-
- # test if output is as expected
- results = xr.open_zarr(out_path)
-
- assert len(results.trajectory) == len(ctd_bgcs)
-
- for ctd_i, (traj, exp_bothloc) in enumerate(
- zip(results.trajectory, ctd_bgc_exp, strict=True)
- ):
- obs_surface = results.sel(trajectory=traj, obs=0)
- min_index = np.argmin(results.sel(trajectory=traj)["z"].data)
- obs_maxdepth = results.sel(trajectory=traj, obs=min_index)
-
- for obs, loc in [
- (obs_surface, "surface"),
- (obs_maxdepth, "maxdepth"),
- ]:
- exp = exp_bothloc[loc]
- for var in [
- "o2",
- "chl",
- "no3",
- "po4",
- "ph",
- "phyc",
- "nppv",
- "lat",
- "lon",
- ]:
- obs_value = obs[var].values.item()
- exp_value = exp[var]
- assert np.isclose(obs_value, exp_value), (
- f"Observation incorrect {ctd_i=} {loc=} {var=} {obs_value=} {exp_value=}."
- )
-
-
-def test_ctd_bgc_sensor_config_active_variables() -> None:
- """active_variables() only returns variables for enabled sensors."""
- config_all = CTD_BGCConfig(
- stationkeeping_time_minutes=50,
- min_depth_meter=-11.0,
- max_depth_meter=-2000.0,
- sensors=[
- SensorConfig(sensor_type=SensorType.OXYGEN),
- SensorConfig(sensor_type=SensorType.CHLOROPHYLL),
- SensorConfig(sensor_type=SensorType.NITRATE),
- SensorConfig(sensor_type=SensorType.PHOSPHATE),
- SensorConfig(sensor_type=SensorType.PH),
- SensorConfig(sensor_type=SensorType.PHYTOPLANKTON),
- SensorConfig(sensor_type=SensorType.PRIMARY_PRODUCTION),
- ],
- )
- assert config_all.active_variables() == {
- "o2": "o2",
- "chl": "chl",
- "no3": "no3",
- "po4": "po4",
- "ph": "ph",
- "phyc": "phyc",
- "nppv": "nppv",
- }
-
- config_o2_only = CTD_BGCConfig(
- stationkeeping_time_minutes=50,
- min_depth_meter=-11.0,
- max_depth_meter=-2000.0,
- sensors=[
- SensorConfig(sensor_type=SensorType.OXYGEN)
- ], # all others omitted = disabled
- )
- assert config_o2_only.active_variables() == {"o2": "o2"}
-
-
-def test_ctd_bgc_sensor_config_yaml() -> None:
- """CTD_BGCConfig sensors survive YAML serialisation."""
- config = CTD_BGCConfig(
- stationkeeping_time_minutes=50,
- min_depth_meter=-11.0,
- max_depth_meter=-2000.0,
- sensors=[
- SensorConfig(sensor_type=SensorType.OXYGEN)
- ], # CHLOROPHYLL and others omitted = disabled
- )
- dumped = config.model_dump(by_alias=True)
- loaded = CTD_BGCConfig.model_validate(dumped)
- assert len(loaded.sensors) == 1
- assert loaded.sensors[0].sensor_type == SensorType.OXYGEN
- assert loaded.sensors[0].enabled is True
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4860f2f67..fde6796f9 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -272,9 +272,8 @@ def test_calc_wp_stationkeeping_time(expedition, monkeypatch):
"""Test _calc_wp_stationkeeping_time for correct stationkeeping time calculation."""
class DummyInstrumentsConfig:
- def __init__(self, ctd, ctd_bgc, argo, xbt, drifter):
+ def __init__(self, ctd, argo, xbt, drifter):
self.ctd = ctd
- self.ctd_bgc = ctd_bgc
self.argo = argo
self.xbt = xbt
self.drifter = drifter
@@ -282,9 +281,6 @@ def __init__(self, ctd, ctd_bgc, argo, xbt, drifter):
class CTDConfig:
stationkeeping_time = datetime.timedelta(minutes=50)
- class CTD_BGCConfig:
- stationkeeping_time = datetime.timedelta(minutes=50)
-
class ArgoFloatConfig:
stationkeeping_time = datetime.timedelta(minutes=20)
@@ -298,7 +294,6 @@ class DrifterConfig:
"virtualship.utils.INSTRUMENT_CONFIG_MAP",
{
InstrumentType.CTD: "CTDConfig",
- InstrumentType.CTD_BGC: "CTD_BGCConfig",
InstrumentType.ARGO_FLOAT: "ArgoFloatConfig",
InstrumentType.XBT: "XBTConfig",
InstrumentType.DRIFTER: "DrifterConfig",
@@ -308,7 +303,6 @@ class DrifterConfig:
# Create a dummy expedition with instruments_config containing the dummy configs
instruments_config = DummyInstrumentsConfig(
ctd=CTDConfig(),
- ctd_bgc=CTD_BGCConfig(),
argo=ArgoFloatConfig(),
xbt=XBTConfig(),
drifter=DrifterConfig(),
@@ -320,7 +314,6 @@ class DrifterConfig:
# instruments at a given waypoint
wp_instrument_types_all = [
InstrumentType.CTD,
- InstrumentType.CTD_BGC,
InstrumentType.ARGO_FLOAT,
InstrumentType.XBT,
InstrumentType.DRIFTER,
@@ -334,9 +327,6 @@ class DrifterConfig:
assert (
stationkeeping_time_all
== CTDConfig.stationkeeping_time
- + (
- CTD_BGCConfig.stationkeeping_time * 0.0
- ) # CTD(_BGC) counted once when both present
+ ArgoFloatConfig.stationkeeping_time
+ DrifterConfig.stationkeeping_time # drifter should only be counted once despite being present at wp twice
)