From b97909b7237cf7fab74009d5f71e99952ff270da Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:14:41 +0200 Subject: [PATCH 01/13] mod ctd instrument to take bgc sampling --- src/virtualship/instruments/ctd.py | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py index 122e1461..cc1239d4 100644 --- a/src/virtualship/instruments/ctd.py +++ b/src/virtualship/instruments/ctd.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING, ClassVar import numpy as np -from parcels import JITParticle, ParticleSet, Variable +from parcels import JITParticle, ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -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): From f2fed18f81ee5cb7ab49703c052c0fba4caef9d0 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:15:02 +0200 Subject: [PATCH 02/13] delete ctd_bgc instrument --- src/virtualship/instruments/ctd_bgc.py | 229 ------------------------- 1 file changed, 229 deletions(-) delete mode 100644 src/virtualship/instruments/ctd_bgc.py diff --git a/src/virtualship/instruments/ctd_bgc.py b/src/virtualship/instruments/ctd_bgc.py deleted file mode 100644 index 3568d0a8..00000000 --- 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." - ) From 0d4b7e919c935d346e846b51920bf325510f72e1 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:23:40 +0200 Subject: [PATCH 03/13] remove rest of CTD_BGC logic from codebase --- src/virtualship/cli/_plan.py | 10 - .../expedition/simulate_schedule.py | 24 +- src/virtualship/instruments/__init__.py | 2 - src/virtualship/instruments/types.py | 1 - src/virtualship/models/expedition.py | 7 - src/virtualship/utils.py | 14 +- .../expedition/expedition_dir/expedition.yaml | 4 - .../expedition_dir/input_data/.gitignore | 1 - tests/expedition/test_expedition.py | 14 +- tests/expedition/test_simulate_schedule.py | 3 - tests/instruments/test_ctd_bgc.py | 297 ------------------ tests/test_utils.py | 6 +- 12 files changed, 5 insertions(+), 378 deletions(-) delete mode 100644 tests/instruments/test_ctd_bgc.py diff --git a/src/virtualship/cli/_plan.py b/src/virtualship/cli/_plan.py index 895abf29..e397aad7 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 d96efc91..6af9d80c 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 b593ed38..0811a7d2 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/types.py b/src/virtualship/instruments/types.py index 489a331f..aa47bc6b 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/expedition.py b/src/virtualship/models/expedition.py index 32855fc9..0eb74530 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -488,13 +488,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. diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 37bb44c4..2f008dbc 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -15,8 +15,8 @@ import numpy as np import pyproj import xarray as xr -from parcels import FieldSet, Variable +from parcels import FieldSet, Variable from virtualship.errors import CopernicusCatalogueError if TYPE_CHECKING: @@ -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 0d2bb155..6392076b 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 b323da5b..2235154f 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 6008309a..ddef53e7 100644 --- a/tests/expedition/test_expedition.py +++ b/tests/expedition/test_expedition.py @@ -6,8 +6,8 @@ import pyproj import pytest import xarray as xr -from parcels import FieldSet +from parcels import FieldSet from virtualship.errors import InstrumentsConfigError, ScheduleError from virtualship.models import ( Expedition, @@ -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 f90eecc2..35dfbdea 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_bgc.py b/tests/instruments/test_ctd_bgc.py deleted file mode 100644 index ad485617..00000000 --- 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 4860f2f6..295ac6be 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,9 +4,9 @@ import numpy as np import pytest import xarray as xr -from parcels import FieldSet, JITParticle, ScipyParticle, Variable import virtualship.utils +from parcels import FieldSet, JITParticle, ScipyParticle, Variable from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.models.expedition import Expedition, SensorConfig @@ -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 @@ -308,7 +307,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(), From 7e7efad6dff3d0b4ae433c819428934dfd4798b7 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:24:11 +0200 Subject: [PATCH 04/13] further deletions and move bgc sensors to default CTD config --- src/virtualship/models/expedition.py | 1 - src/virtualship/static/expedition.yaml | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py index 0eb74530..c9fe4692 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -524,7 +524,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 8ab72f8a..b947c7d0 100644 --- a/src/virtualship/static/expedition.yaml +++ b/src/virtualship/static/expedition.yaml @@ -60,11 +60,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 From 8433a22015469133dbea9ef71b21083c52cc9131 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:27:07 +0200 Subject: [PATCH 05/13] further deletions of CTD_BGC logic --- src/virtualship/models/__init__.py | 2 -- src/virtualship/models/expedition.py | 34 -------------------------- src/virtualship/static/expedition.yaml | 1 - tests/test_utils.py | 8 ------ 4 files changed, 45 deletions(-) diff --git a/src/virtualship/models/__init__.py b/src/virtualship/models/__init__.py index 7d3b0881..71f24211 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 c9fe4692..f1c83037 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -359,40 +359,6 @@ class CTDConfig(_InstrumentConfigMixin, pydantic.BaseModel): 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), - SensorConfig(sensor_type=SensorType.PHOSPHATE), - SensorConfig(sensor_type=SensorType.PH), - SensorConfig(sensor_type=SensorType.PHYTOPLANKTON), - SensorConfig(sensor_type=SensorType.PRIMARY_PRODUCTION), - ], - description=( - "Sensors fitted to the BGC CTD. " - "Supported: OXYGEN, CHLOROPHYLL, NITRATE, PHOSPHATE, PH, PHYTOPLANKTON, PRIMARY_PRODUCTION. " - ), - ) - - model_config = pydantic.ConfigDict(populate_by_name=True) - - @register_instrument_config(InstrumentType.UNDERWATER_ST) class ShipUnderwaterSTConfig(_InstrumentConfigMixin, pydantic.BaseModel): """Configuration for underwater ST.""" diff --git a/src/virtualship/static/expedition.yaml b/src/virtualship/static/expedition.yaml index b947c7d0..3be45c4d 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 diff --git a/tests/test_utils.py b/tests/test_utils.py index 295ac6be..c0187caf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -281,9 +281,6 @@ def __init__(self, ctd, 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) @@ -297,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", @@ -318,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, @@ -332,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 ) From ae919ebb5e942c47bacac9a6b98a3404deb18420 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:39:16 +0200 Subject: [PATCH 06/13] update ctd testing for both phys and bgc sampling capabilities --- tests/instruments/test_ctd.py | 119 +++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/tests/instruments/test_ctd.py b/tests/instruments/test_ctd.py index 4fd6e28d..2f93dfa2 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,11 +307,21 @@ 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, + } ) @@ -299,3 +346,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" From 245e7f0a7a0872903c99712a8091e357e3e333cc Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Mon, 4 May 2026 14:54:44 +0200 Subject: [PATCH 07/13] first round update docs, including removing some other redunant sections --- docs/user-guide/assignments/Research_Proposal_only.ipynb | 2 +- docs/user-guide/assignments/Research_proposal_intro.ipynb | 5 ----- docs/user-guide/assignments/Sail_the_ship.ipynb | 4 ---- .../assignments/Virtualship_research_proposal.ipynb | 2 +- docs/user-guide/tutorials/CTD_transects.ipynb | 4 ++-- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/user-guide/assignments/Research_Proposal_only.ipynb b/docs/user-guide/assignments/Research_Proposal_only.ipynb index 7b8e8691..ac8f057c 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 3f3a3f53..2f19e2b1 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", - "