From 9d78ccfaf06be08bc853e770d66f2569200a3197 Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 08:58:34 +0200
Subject: [PATCH 1/8] add bgc kernels and use helper func for sampling on
ascent
---
src/virtualship/instruments/argo_float.py | 52 +++++++++++++++++------
1 file changed, 38 insertions(+), 14 deletions(-)
diff --git a/src/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py
index 7b8a5faf..66911874 100644
--- a/src/virtualship/instruments/argo_float.py
+++ b/src/virtualship/instruments/argo_float.py
@@ -160,24 +160,34 @@ def _check_error(particles, fieldset):
particles.state[errors] = StatusCode.Delete
+## physical variables
+
+
def _argo_sample_temperature(particles, fieldset):
- # Phase 3: ascending — sample temperature
- phase_mask = particles.cycle_phase == 3
- depth_mask = particles.z < particles.min_depth # still ascending
- mask = np.logical_and(phase_mask, depth_mask)
- particles.temperature[~mask] = np.nan # no measurement outside the ascent
- sampling_particles = particles[mask]
- sampling_particles.temperature = fieldset.T[sampling_particles]
+ _sample_on_ascent(particles, fieldset.T, "temperature")
def _argo_sample_salinity(particles, fieldset):
- # Phase 3: ascending — sample salinity
- phase_mask = particles.cycle_phase == 3
- depth_mask = particles.z < particles.min_depth # still ascending
- mask = np.logical_and(phase_mask, depth_mask)
- particles.salinity[~mask] = np.nan # no measurement outside the ascent
- sampling_particles = particles[mask]
- sampling_particles.salinity = fieldset.S[sampling_particles]
+ _sample_on_ascent(particles, fieldset.S, "salinity")
+
+
+## bgc variables
+
+
+def _argo_sample_o2(particles, fieldset):
+ _sample_on_ascent(particles, fieldset.o2, "o2")
+
+
+def _argo_sample_chlorophyll(particles, fieldset):
+ _sample_on_ascent(particles, fieldset.chl, "chl")
+
+
+def _argo_sample_nitrate(particles, fieldset):
+ _sample_on_ascent(particles, fieldset.no3, "no3")
+
+
+def _argo_sample_ph(particles, fieldset):
+ _sample_on_ascent(particles, fieldset.ph, "ph")
# =====================================================
@@ -185,6 +195,16 @@ def _argo_sample_salinity(particles, fieldset):
# =====================================================
+def _sample_on_ascent(particles, field, var):
+ """Sample `field` into particle variable `var` during the ascent (phase 3) only."""
+ phase_mask = particles.cycle_phase == 3
+ depth_mask = particles.z < particles.min_depth # still ascending
+ mask = np.logical_and(phase_mask, depth_mask)
+ getattr(particles, var)[~mask] = np.nan # no measurement outside the ascent
+ sampling_particles = particles[mask]
+ setattr(sampling_particles, var, field[sampling_particles])
+
+
def _handle_grounding(
ptcls_subset, bathysafe_mask, loc_bathy, fieldset, phase_name, target_phase
):
@@ -237,6 +257,10 @@ class ArgoFloatInstrument(Instrument):
sensor_kernels: ClassVar[dict[SensorType, Callable]] = {
SensorType.TEMPERATURE: _argo_sample_temperature,
SensorType.SALINITY: _argo_sample_salinity,
+ SensorType.OXYGEN: _argo_sample_o2,
+ SensorType.CHLOROPHYLL: _argo_sample_chlorophyll,
+ SensorType.NITRATE: _argo_sample_nitrate,
+ SensorType.PH: _argo_sample_ph,
}
def __init__(self, expedition, from_data):
From f3754d3acdfaa64292bf1054f93a71ccba8ff10f Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 08:59:18 +0200
Subject: [PATCH 2/8] comment/description for bgc vars no on by default
---
src/virtualship/models/expedition.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py
index 18ee986e..90ade21a 100644
--- a/src/virtualship/models/expedition.py
+++ b/src/virtualship/models/expedition.py
@@ -508,9 +508,11 @@ class ArgoFloatConfig(_InstrumentConfigMixin, pydantic.BaseModel):
default_factory=lambda: [
SensorConfig(sensor_type=SensorType.TEMPERATURE),
SensorConfig(sensor_type=SensorType.SALINITY),
+ # BGC sensors are supported but not on by default
],
description=(
- "Sensors fitted to the Argo float. Supported: TEMPERATURE, SALINITY. "
+ "Sensors fitted to the Argo float. Supported: TEMPERATURE, SALINITY, OXYGEN, CHLOROPHYLL, NITRATE, PH. "
+ "BGC sensors (OXYGEN, CHLOROPHYLL, NITRATE, PH) are off by default. "
),
)
From edd4b21ed2ce6a69d4f262ce0ac7d9e02132ecd5 Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 09:01:12 +0200
Subject: [PATCH 3/8] plan tool shows full bgc argo sensor options but only
defaults are on
---
src/virtualship/cli/_plan.py | 39 +++++++++++++++++++++++-------------
1 file changed, 25 insertions(+), 14 deletions(-)
diff --git a/src/virtualship/cli/_plan.py b/src/virtualship/cli/_plan.py
index 7cc8fa48..23258181 100644
--- a/src/virtualship/cli/_plan.py
+++ b/src/virtualship/cli/_plan.py
@@ -33,6 +33,7 @@
type_to_textual,
)
from virtualship.errors import UnexpectedError, UserError
+from virtualship.instruments.sensors import SensorType
from virtualship.instruments.types import InstrumentType
from virtualship.models import (
ADCPConfig,
@@ -48,7 +49,7 @@
XBTConfig,
)
from virtualship.models.expedition import Port
-from virtualship.utils import EXPEDITION, INCOMPLETE_PORT_MSG
+from virtualship.utils import EXPEDITION, INCOMPLETE_PORT_MSG, get_supported_sensors
UNEXPECTED_MSG_ONSAVE = (
"Please ensure that:\n"
@@ -91,6 +92,19 @@ def _default_sensors(config_class) -> list:
return sensors_field.default_factory()
+def _sensor_options(config_class) -> list:
+ """SensorConfig for every sensor the config class's instrument supports (in SensorType order), enabled if on by default."""
+ on_by_default = {sc.sensor_type for sc in _default_sensors(config_class)}
+ if not on_by_default:
+ return []
+ supported = get_supported_sensors(config_class._instrument_type)
+ return [
+ SensorConfig(sensor_type=s, enabled=s in on_by_default)
+ for s in SensorType
+ if s in supported
+ ]
+
+
WAYPOINT_TIME_FORMAT = "%Y-%m-%d %H:%M"
WAYPOINT_TIME_TEMPLATE = "9999-99-99 99:99"
WAYPOINT_TIME_INVALID_MSG = (
@@ -467,9 +481,9 @@ def _compose_instrument_config(self, instrument_name: str, info: dict):
value=_config_display_value(config_instance, attr_meta),
)
yield _failure_label(f"{instrument_name}_{attr}")
- # sensor toggles, derived from the config class's sensors default_factory
- default_sensor_configs = _default_sensors(config_class)
- if default_sensor_configs:
+ # sensor toggles, one per sensor supported by the instrument
+ sensor_options = _sensor_options(config_class)
+ if sensor_options:
yield Label("[b]Sensors:[/b]", markup=True)
# which sensors are currently active
if config_instance and hasattr(config_instance, "sensors"):
@@ -477,11 +491,11 @@ def _compose_instrument_config(self, instrument_name: str, info: dict):
sc.sensor_type for sc in config_instance.sensors if sc.enabled
}
else:
- # if no config loaded yet, default all sensors on
+ # if no config loaded yet, only the default sensors are on
active_sensor_types = {
- sc.sensor_type for sc in default_sensor_configs
+ sc.sensor_type for sc in sensor_options if sc.enabled
}
- for sc in default_sensor_configs:
+ for sc in sensor_options:
with Horizontal(classes="sensor-toggle-row"):
yield Label(
f" {sc.sensor_type.value.replace('_', ' ').title()}:"
@@ -613,11 +627,11 @@ def _update_instrument_configs(self):
kwargs["max_depth_meter"] = -1000.0 if is_deep else -150.0
# collect sensor toggles
- default_sensor_configs = _default_sensors(config_class)
- if default_sensor_configs:
+ sensor_options = _sensor_options(config_class)
+ if sensor_options:
sensors = [
SensorConfig(sensor_type=sc.sensor_type)
- for sc in default_sensor_configs
+ for sc in sensor_options
if self.query_one(
f"#{instrument_name}_sensor_{sc.sensor_type.value}", Switch
).value
@@ -629,10 +643,7 @@ def _update_instrument_configs(self):
f"At least one sensor must be enabled for each active instrument."
)
# fall back to the default sensors so pydantic validation passes
- sensors = [
- SensorConfig(sensor_type=sc.sensor_type)
- for sc in default_sensor_configs
- ]
+ sensors = _default_sensors(config_class)
kwargs["sensors"] = sensors
try:
From 432bc2f0f01dfecb9e5fd5692ba99fe00d228818 Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 09:05:24 +0200
Subject: [PATCH 4/8] update docs with bgc info for argo floats
---
.../assignments/Research_proposal_intro.ipynb | 2 ++
docs/user-guide/documentation/full_sensor_list.md | 10 ++++++++++
docs/user-guide/documentation/pre_download_data.md | 2 +-
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/user-guide/assignments/Research_proposal_intro.ipynb b/docs/user-guide/assignments/Research_proposal_intro.ipynb
index 52e74651..01c0e47b 100644
--- a/docs/user-guide/assignments/Research_proposal_intro.ipynb
+++ b/docs/user-guide/assignments/Research_proposal_intro.ipynb
@@ -122,6 +122,8 @@
"\n",
"The floats periodically descend to a predetermined depth, typically around 1000 meters, and then ascend to the surface, measuring temperature and salinity profiles along the way.\n",
"\n",
+ "In VirtualShip, Argo floats can optionally also be fitted with biogeochemical (BGC) sensors for oxygen, chlorophyll, nitrate and pH, as on real [Biogeochemical-Argo](https://biogeochemical-argo.org/) floats. These are off by default (see the [full sensor list](../documentation/full_sensor_list.md)).\n",
+ "\n",
"For deployment, the ship needs to slow down to a stop, which takes 15 minutes (default in VirtualShip). You can assume the actual release takes no time but then after the deployment the ship needs to speed back up again and set course to the next waypoint, which takes another 15 minutes (default in VirtualShip).\n",
" \n",
""
diff --git a/docs/user-guide/documentation/full_sensor_list.md b/docs/user-guide/documentation/full_sensor_list.md
index 9b22c05d..b64605c0 100644
--- a/docs/user-guide/documentation/full_sensor_list.md
+++ b/docs/user-guide/documentation/full_sensor_list.md
@@ -26,5 +26,15 @@ Trying to add a sensor to an instrument that does not support it will result in
| | PRIMARY_PRODUCTION | Net Primary Production of Biomass | mmol m-3 day-1 | Biogeochemical |
| **ARGO_FLOAT** | TEMPERATURE | Sea Water Potential Temperature | °C | Physical |
| | SALINITY | Sea Water Salinity | psu | Physical |
+| | OXYGEN | Mole Concentration of Dissolved Molecular Oxygen in Sea Water (off by default) | mmol m-3 | Biogeochemical |
+| | CHLOROPHYLL | Mass Concentration of Chlorophyll a in Sea Water (off by default) | mmol m-3 | Biogeochemical |
+| | NITRATE | Mole Concentration of Nitrate in Sea Water (off by default) | mmol m-3 | Biogeochemical |
+| | PH | Sea Water pH (off by default) | - | Biogeochemical |
| **DRIFTER** | TEMPERATURE | Sea Water Potential Temperature | °C | Physical |
| **XBT** | TEMPERATURE | Sea Water Potential Temperature | °C | Physical |
+
+```{note}
+The `ARGO_FLOAT` BGC sensors (oxygen, chlorophyll, nitrate, ph) are off by default. Each extra sensor adds another variable for the simulation to load and sample, which can make the already-expensive Argo float simulations more intensive. When needed though, they can be turned on in the `virtualship plan` tool or by adding them to the `sensors` list of `argo_float_config` in `expedition.yaml`.
+
+Only BGC variables that real-life [BGC-Argo](https://biogeochemical-argo.org/) floats measure are available for the Argo floats in VirtualShip.
+```
diff --git a/docs/user-guide/documentation/pre_download_data.md b/docs/user-guide/documentation/pre_download_data.md
index 8649b70f..54f924ef 100644
--- a/docs/user-guide/documentation/pre_download_data.md
+++ b/docs/user-guide/documentation/pre_download_data.md
@@ -74,7 +74,7 @@ The following assumptions are also made about the data:
1. All pre-downloaded data files must be in NetCDF format (`.nc`).
2. Physical data files must contain the following variables: `uo`, `vo`, `so`, `thetao`
- Or these strings must appear as substrings within the variable names (e.g. `uo_glor` is acceptable for `uo`).
-3. If using BGC-enabled instruments (e.g. BGC variables on the `CTD`), the relevant biogeochemical data files must contain the following variables: `o2`, `chl`, `no3`, `po4`, `nppv`, `ph`, `phyc`.
+3. If using BGC-enabled instruments (e.g. BGC variables on the `CTD` or `ARGO_FLOAT`), the relevant biogeochemical data files must contain the following variables: `o2`, `chl`, `no3`, `po4`, `nppv`, `ph`, `phyc`.
- Or these strings must appear as substrings within the variable names (e.g. `o2_glor` is acceptable for `o2`).
4. Bathymetry data files must contain a variable named `deptho`.
5. Pre-downloaded data files must have a `"positive"` attribute for the depth dimension (e.g. `"positive": "down"` or `"positive": "up"`) in order to ensure that the depth dimension is correctly interpreted under-the-hood.
From 5ca22dc2d567f22cfb8e70c663aa05d03c6c7ebd Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 09:06:47 +0200
Subject: [PATCH 5/8] add tests for supported sensors on/off and default off
for bgc argo
---
tests/cli/test_plan.py | 67 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/tests/cli/test_plan.py b/tests/cli/test_plan.py
index efc0c32a..6d67574d 100644
--- a/tests/cli/test_plan.py
+++ b/tests/cli/test_plan.py
@@ -12,11 +12,13 @@
QuitConfirmScreen,
WaypointWidget,
_default_sensors,
+ _sensor_options,
parse_waypoint_time,
)
from virtualship.instruments.sensors import SensorType
from virtualship.instruments.types import InstrumentType
from virtualship.models import (
+ ArgoFloatConfig,
CTDConfig,
Expedition,
InstrumentsConfig,
@@ -331,6 +333,28 @@ def test_default_sensors_returns_declared_defaults():
assert SensorType.SALINITY in types
+def test_sensor_options_include_supported_sensors_off_by_default():
+ """_sensor_options() lists every supported sensor, with only the default sensors enabled."""
+ argo_options = _sensor_options(ArgoFloatConfig)
+ assert [sc.sensor_type for sc in argo_options if sc.enabled] == [
+ SensorType.TEMPERATURE,
+ SensorType.SALINITY,
+ ]
+ assert [sc.sensor_type for sc in argo_options if not sc.enabled] == [
+ SensorType.OXYGEN,
+ SensorType.CHLOROPHYLL,
+ SensorType.NITRATE,
+ SensorType.PH,
+ ]
+
+ # instruments where all supported sensors are on by default are unchanged
+ ctd_options = _sensor_options(CTDConfig)
+ assert all(sc.enabled for sc in ctd_options)
+ assert [sc.sensor_type for sc in ctd_options] == [
+ sc.sensor_type for sc in _default_sensors(CTDConfig)
+ ]
+
+
@pytest.mark.asyncio
async def test_sensor_toggle_saved_to_yaml(tmp_path):
"""Toggling a sensor switch off and saving should persist only the enabled sensors."""
@@ -519,6 +543,49 @@ async def test_sensor_initial_state_reflects_config(tmp_path):
assert sal_switch.value is False, "SALINITY should be OFF (not in saved config)"
+@pytest.mark.asyncio
+async def test_sensor_initial_state_reflects_defaults_without_config(tmp_path):
+ """Without a stored config, sensor switches should follow the default on/off state (e.g. Argo BGC sensors listed but OFF)."""
+ instruments_config = InstrumentsConfig.model_validate(
+ yaml.safe_load(_get_example_expedition()).get("instruments_config")
+ )
+ instruments_config.argo_float_config = None
+ _make_expedition(
+ tmp_path,
+ [
+ Port(location=None, time=None),
+ Waypoint(
+ location=Location(0, 0),
+ time=datetime(2022, 1, 1, 0, 0, 0),
+ instrument=["CTD"],
+ ),
+ Waypoint(
+ location=Location(0.01, 0.01),
+ time=datetime(2022, 1, 1, 1, 0, 0),
+ instrument=None,
+ ),
+ Port(location=None, time=None),
+ ],
+ instruments_config,
+ )
+
+ app = PlanApp(path=tmp_path)
+ async with app.run_test(size=(160, 120)) as pilot:
+ await pilot.pause(0.5)
+ expedition_editor = pilot.app.screen.query_one(ExpeditionEditor)
+
+ for sensor in ["TEMPERATURE", "SALINITY"]:
+ switch = expedition_editor.query_one(
+ f"#argo_float_config_sensor_{sensor}", Switch
+ )
+ assert switch.value is True, f"{sensor} should be ON by default"
+ for sensor in ["OXYGEN", "CHLOROPHYLL", "NITRATE", "PH"]:
+ switch = expedition_editor.query_one(
+ f"#argo_float_config_sensor_{sensor}", Switch
+ )
+ assert switch.value is False, f"{sensor} should be OFF by default"
+
+
@pytest.mark.asyncio
async def test_adcp_type_always_exactly_one_selected(tmp_path):
"""Switching one ADCP type off selects the other, so both can never be off/on together."""
From dbacf2f50dfc067b73e19e80de3d07c83dfc8dbf Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 09:15:03 +0200
Subject: [PATCH 6/8] update tests to cover BGC argos
---
tests/instruments/test_argo_float.py | 76 ++++++++++++++++++++++++----
1 file changed, 66 insertions(+), 10 deletions(-)
diff --git a/tests/instruments/test_argo_float.py b/tests/instruments/test_argo_float.py
index 5ca001f8..5ef8d35f 100644
--- a/tests/instruments/test_argo_float.py
+++ b/tests/instruments/test_argo_float.py
@@ -27,6 +27,7 @@
SensorConfig,
Waypoint,
)
+from virtualship.utils import get_supported_sensors
# Constants
BASE_TIME = datetime.strptime("1950-01-01", "%Y-%m-%d")
@@ -37,6 +38,14 @@
CYCLE_DAYS = 10
DRIFT_DAYS = 9
+ARGO_BGC_SENSORS = [
+ SensorType.OXYGEN,
+ SensorType.CHLOROPHYLL,
+ SensorType.NITRATE,
+ SensorType.PH,
+]
+ARGO_BGC_FS_KEYS = ["o2", "chl", "no3", "ph"]
+
@pytest.fixture
def argo_config_kwargs():
@@ -55,10 +64,11 @@ def create_fieldset(
lon_range=(0.0, 10.0),
lat_range=(0.0, 10.0),
include_salinity=True,
+ include_bgc=False,
lifetime_days=0.1,
depths=None,
):
- """Create a test fieldset with optional salinity and optional depth levels."""
+ """Create a test fieldset with optional salinity, optional BGC variables and optional depth levels."""
dims = ("time", "lat", "lon") if depths is None else ("time", "depth", "lat", "lon")
shape = (2, 2, 2) if depths is None else (2, len(depths), 2, 2)
bathy = np.full((2, 2), -5000.0)
@@ -72,6 +82,10 @@ def create_fieldset(
if include_salinity:
data_vars["S"] = (dims, np.full(shape, 1.0))
+ if include_bgc:
+ for var in ARGO_BGC_FS_KEYS:
+ data_vars[var] = (dims, np.full(shape, 1.0))
+
depth_coord = (
{}
if depths is None
@@ -255,19 +269,31 @@ def test_argo_float_disabled_sensor(tmpdir) -> None:
def test_argo_config_default_sensors(argo_config_kwargs):
- """ArgoFloatConfig defaults to TEMPERATURE + SALINITY."""
+ """ArgoFloatConfig defaults to TEMPERATURE + SALINITY, i.e. the BGC sensors are off by default."""
config = ArgoFloatConfig(**argo_config_kwargs, lifetime=timedelta(days=30))
types = {sc.sensor_type for sc in config.sensors}
assert types == {SensorType.TEMPERATURE, SensorType.SALINITY}
+ assert set(ARGO_BGC_SENSORS) <= get_supported_sensors(InstrumentType.ARGO_FLOAT)
+
+ # BGC fields are not loaded by default
+ assert config.active_variables() == {"T": "thetao", "S": "so"}
-def test_argo_config_unsupported_sensor_rejected(argo_config_kwargs):
+@pytest.mark.parametrize(
+ "sensor_type",
+ [
+ SensorType.PHOSPHATE,
+ SensorType.PHYTOPLANKTON,
+ SensorType.PRIMARY_PRODUCTION,
+ ],
+)
+def test_argo_config_unsupported_sensor_rejected(argo_config_kwargs, sensor_type):
"""Unsupported sensor on ArgoFloat is rejected."""
with pytest.raises(pydantic.ValidationError, match="does not support"):
ArgoFloatConfig(
**argo_config_kwargs,
lifetime=timedelta(days=30),
- sensors=[SensorConfig(sensor_type=SensorType.OXYGEN)],
+ sensors=[SensorConfig(sensor_type=sensor_type)],
)
@@ -348,10 +374,12 @@ def test_argo_fieldoutofbounds_error(tmpdir) -> None:
)
-def _simulate_cycling_float(tmpdir, sensor_types, lifetime_days):
+def _simulate_cycling_float(tmpdir, sensor_types, lifetime_days, include_bgc=False):
"""Simulate a float with a one day cycle (and no drift phase), which is enough time for one descent to max depth + ascent, with fieldset with depth levels."""
fieldset = create_fieldset(
- lifetime_days=lifetime_days, depths=[-2225.1, -1941.9, -1000.0, -0.5]
+ include_bgc=include_bgc,
+ lifetime_days=lifetime_days,
+ depths=[-2225.1, -1941.9, -1000.0, -0.5],
)
sensors = [SensorConfig(sensor_type=s) for s in sensor_types]
expedition = create_dummy_expedition(
@@ -395,18 +423,21 @@ def test_argo_float_reaches_max_depth_and_ascends(tmpdir) -> None:
assert np.isfinite(results["temperature"].to_numpy()[phase == 3]).any()
-def test_argo_no_sampling_outside_ascent(tmpdir) -> None:
- """Temperature and salinity are only recorded during the ascent (phase 3), not carried over into other phases."""
+def test_argo_sampling_only_on_ascent(tmpdir) -> None:
+ """All enabled sensor variables (T, S and BGC) are only recorded during the ascent (phase 3), not carried over into other phases."""
# two day lifetime so that the float completes a cycle and starts the next one
results = _simulate_cycling_float(
- tmpdir, [SensorType.TEMPERATURE, SensorType.SALINITY], lifetime_days=2.0
+ tmpdir,
+ [SensorType.TEMPERATURE, SensorType.SALINITY, *ARGO_BGC_SENSORS],
+ lifetime_days=2.0,
+ include_bgc=True,
)
phase = results["cycle_phase"].to_numpy()
# the float should have gone through every phase and back into a second cycle
assert set(np.unique(phase)) == {0, 1, 2, 3, 4}
- for var in ["temperature", "salinity"]:
+ for var in ["temperature", "salinity", *ARGO_BGC_FS_KEYS]:
values = results[var].to_numpy()
assert np.isfinite(values[phase == 3]).any(), (
f"{var} should be sampled on ascent"
@@ -416,6 +447,31 @@ def test_argo_no_sampling_outside_ascent(tmpdir) -> None:
)
+def test_argo_bgc_disabled_sensors_absent(tmpdir) -> None:
+ """Disabled BGC sensors (as in the default config) need no BGC fields and produce no output variables."""
+ fieldset = create_fieldset(include_bgc=False)
+
+ sensors = [
+ SensorConfig(sensor_type=SensorType.TEMPERATURE),
+ SensorConfig(sensor_type=SensorType.SALINITY),
+ *[SensorConfig(sensor_type=s, enabled=False) for s in ARGO_BGC_SENSORS],
+ ]
+ expedition = create_dummy_expedition(sensors)
+
+ argo_instrument = ArgoFloatInstrument(expedition, None)
+ assert set(argo_instrument.variables) == {"U", "V", "T", "S"}
+
+ argo_floats = [create_argo_float(wp) for wp in expedition.schedule.waypoints]
+ out_path = tmpdir.join("out_bgc_disabled.parquet")
+ argo_instrument.load_input_data = lambda: fieldset
+ argo_instrument.simulate(argo_floats, out_path)
+
+ results = parcels.read_particlefile(out_path)
+ assert "temperature" in results and "salinity" in results
+ for var in ARGO_BGC_FS_KEYS:
+ assert var not in results, f"Disabled sensor variable {var} must be absent"
+
+
def test_argo_salinity_only_sensor(tmpdir) -> None:
"""A float with a sensor removed (default is temperature and salinity) still simulates properly without crashing."""
results = _simulate_cycling_float(tmpdir, [SensorType.SALINITY], lifetime_days=2.0)
From 78135c831c244b6da5fae1e2cfd095e5716d483c Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 11:12:32 +0200
Subject: [PATCH 7/8] consitent use of "intensive"
---
docs/user-guide/documentation/full_sensor_list.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/user-guide/documentation/full_sensor_list.md b/docs/user-guide/documentation/full_sensor_list.md
index b64605c0..90fa36bf 100644
--- a/docs/user-guide/documentation/full_sensor_list.md
+++ b/docs/user-guide/documentation/full_sensor_list.md
@@ -34,7 +34,7 @@ Trying to add a sensor to an instrument that does not support it will result in
| **XBT** | TEMPERATURE | Sea Water Potential Temperature | °C | Physical |
```{note}
-The `ARGO_FLOAT` BGC sensors (oxygen, chlorophyll, nitrate, ph) are off by default. Each extra sensor adds another variable for the simulation to load and sample, which can make the already-expensive Argo float simulations more intensive. When needed though, they can be turned on in the `virtualship plan` tool or by adding them to the `sensors` list of `argo_float_config` in `expedition.yaml`.
+The `ARGO_FLOAT` BGC sensors (oxygen, chlorophyll, nitrate, ph) are off by default. Each extra sensor adds another variable for the simulation to load and sample, which can make the already-computationally intensive Argo float simulations even more intensive. When needed though, they can be turned on in the `virtualship plan` tool or by adding them to the `sensors` list of `argo_float_config` in `expedition.yaml`.
Only BGC variables that real-life [BGC-Argo](https://biogeochemical-argo.org/) floats measure are available for the Argo floats in VirtualShip.
```
From d0f582671511b3af58bf4b5762a11ef2a116934a Mon Sep 17 00:00:00 2001
From: j-atkins <106238905+j-atkins@users.noreply.github.com>
Date: Wed, 21 Oct 2026 11:14:38 +0200
Subject: [PATCH 8/8] fix typo
---
docs/user-guide/documentation/full_sensor_list.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/user-guide/documentation/full_sensor_list.md b/docs/user-guide/documentation/full_sensor_list.md
index 90fa36bf..0492b7ee 100644
--- a/docs/user-guide/documentation/full_sensor_list.md
+++ b/docs/user-guide/documentation/full_sensor_list.md
@@ -34,7 +34,7 @@ Trying to add a sensor to an instrument that does not support it will result in
| **XBT** | TEMPERATURE | Sea Water Potential Temperature | °C | Physical |
```{note}
-The `ARGO_FLOAT` BGC sensors (oxygen, chlorophyll, nitrate, ph) are off by default. Each extra sensor adds another variable for the simulation to load and sample, which can make the already-computationally intensive Argo float simulations even more intensive. When needed though, they can be turned on in the `virtualship plan` tool or by adding them to the `sensors` list of `argo_float_config` in `expedition.yaml`.
+The `ARGO_FLOAT` BGC sensors (oxygen, chlorophyll, nitrate, pH) are off by default. Each extra sensor adds another variable for the simulation to load and sample, which can make the already-computationally intensive Argo float simulations even more intensive. When needed though, they can be turned on in the `virtualship plan` tool or by adding them to the `sensors` list of `argo_float_config` in `expedition.yaml`.
Only BGC variables that real-life [BGC-Argo](https://biogeochemical-argo.org/) floats measure are available for the Argo floats in VirtualShip.
```