From edbda0b83a50e1baa49d07aa775925e7de634435 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 11:25:58 +0200 Subject: [PATCH 01/17] add info on sensor configuration --- .../tutorials/working_with_expedition_yaml.md | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/tutorials/working_with_expedition_yaml.md b/docs/user-guide/tutorials/working_with_expedition_yaml.md index af059b3f..d0825794 100644 --- a/docs/user-guide/tutorials/working_with_expedition_yaml.md +++ b/docs/user-guide/tutorials/working_with_expedition_yaml.md @@ -49,8 +49,13 @@ instruments_config: # <-- 2. instrument configuration section num_bins: 40 max_depth_meter: -1000.0 period_minutes: 5.0 + sensors: + - VELOCITY ship_underwater_st_config: period_minutes: 5.0 + sensors: + - TEMPERATURE + - SALINITY argo_float_config: ... ctd_config: ... drifter_config: ... @@ -88,6 +93,27 @@ You can do multiple `DRIFTER` deployments at the same waypoint by adding multipl This section defines the configuration settings for each instrument used in the expedition. Each instrument has its own subsection where specific parameters can be set. +##### Sensors + +For most users, the most important instrument configuration settings to consider are the **sensors** for each instrument, which control what type of measurements/variables the instrument records in the simulation. For example, for the `CTD` instrument, you can specify which sensors to include in the simulation (e.g., `TEMPERATURE`, `SALINITY`, `OXYGEN`, etc.) by adding or removing entries from the `sensors` list in the `ctd_config` section. These must be added on _new lines_ and be in _uppercase_, for example: + +```yaml +ctd_config: + max_depth_meter: -2000.0 + min_depth_meter: -11.0 + stationkeeping_time_minutes: 50.0 + sensors: + - TEMPERATURE + - SALINITY + - OXYGEN +``` + +```{important} +See [here](../documentation/full_sensor_list.md) for a full list of available sensors for each instrument. Trying to add a sensor to an instrument that does not support it will result in errors in VirtualShip. +``` + +##### Underway Instruments + Because **underway instruments** (e.g., ADCP, Ship Underwater ST) collect data continuously while the ship is moving, their deployment is not tied to specific waypoints. Instead, the presence of their configuration sections in `instruments_config` indicates that they will be active throughout the expedition. This means that if you wish to turn off an underway instrument, you can remove its configuration section or simply set it to `null`, for example: ```yaml @@ -96,7 +122,7 @@ instruments_config: ship_underwater_st_config: null ``` -For **all other instruments**, e.g. CTD, ARGO_FLOAT etc., the parameters can often be left as the default values unless advanced customisations are required. +For **all other instruments**, e.g. CTD, ARGO_FLOAT etc., the parameters can often be left as the default values unless further, advanced customisations are required. #### 3. `ship_config` From cd5b523f83046ca1e3ec5ab42a32af36ee5aa967 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 11:26:26 +0200 Subject: [PATCH 02/17] add list of available sensors to documentation --- .../documentation/full_sensor_list.md | 26 +++++++++++++++++++ docs/user-guide/index.md | 1 + 2 files changed, 27 insertions(+) create mode 100644 docs/user-guide/documentation/full_sensor_list.md diff --git a/docs/user-guide/documentation/full_sensor_list.md b/docs/user-guide/documentation/full_sensor_list.md new file mode 100644 index 00000000..1cc072a7 --- /dev/null +++ b/docs/user-guide/documentation/full_sensor_list.md @@ -0,0 +1,26 @@ +# Full list of available instrument sensors + +The following table provides a comprehensive list of available sensors for each instrument. These sensors can be specified via the `virtualship plan` tool (see the [Quickstart guide](../quickstart.md)) or in the `sensors` section of the respective instrument configuration in the `expedition.yaml` file (see the [working with expedition.yaml tutorial](../tutorials/working_with_expedition_yaml.md)). + +```{note} +Trying to add a sensor to an instrument that does not support it will result in errors in VirtualShip. Always refer to this table to check which sensors are available for each instrument. +``` + +| Instrument | Sensor Name | Description | Units | Category | +| :--------------------- | :----------------- | :-------------------------------------------------- | :----------------------------------- | :-------------- | +| **ADCP** | VELOCITY | Current velocities (eastward (u) and northward (v)) | m/s | Physical | +| **Ship Underwater ST** | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| **CTD** | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| | OXYGEN | Oxygen concentration | mmol m-3 | Biogeochemistry | +| | CHLOROPHYLL | Chlorophyll concentration | mmol m-3 | Biogeochemistry | +| | NITRATE | Nitrate concentration | mmol m-3 | Biogeochemistry | +| | PHOSPHATE | Phosphate concentration | mmol m-3 | Biogeochemistry | +| | PH | pH | - | Biogeochemistry | +| | PHYTOPLANKTON | Phytoplankton concentration in carbon | mmol m-3 | Biogeochemistry | +| | PRIMARY_PRODUCTION | Net primary production | mmol m-3 day-1 | Biogeochemistry | +| **ARGO_FLOAT** | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| **DRIFTER** | TEMPERATURE | Temperature | °C | Physics | +| **XBT** | TEMPERATURE | Temperature | °C | Physics | diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index fac1c26c..d5d4a124 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -17,4 +17,5 @@ assignments/index documentation/copernicus_products.md documentation/pre_download_data.md documentation/example_copernicus_download.ipynb +documentation/full_sensor_list.md ``` From 4668e548e63dcd27457c6493e7245a8fcea34bc2 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 11:27:16 +0200 Subject: [PATCH 03/17] remove redundant TODO --- src/virtualship/static/expedition.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/virtualship/static/expedition.yaml b/src/virtualship/static/expedition.yaml index 3be45c4d..acb16dcf 100644 --- a/src/virtualship/static/expedition.yaml +++ b/src/virtualship/static/expedition.yaml @@ -1,7 +1,5 @@ # see https://virtualship.readthedocs.io/en/latest/user-guide/tutorials/working_with_expedition_yaml.html for more details on how to edit this file # -# TODO: add a link to docs where lists what sensors are supported for each instrument -# schedule: waypoints: - instrument: From ae2d528edc38f735d296a0a1577e31195aa33a34 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 11:52:09 +0200 Subject: [PATCH 04/17] improve phrasing --- docs/user-guide/tutorials/working_with_expedition_yaml.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/tutorials/working_with_expedition_yaml.md b/docs/user-guide/tutorials/working_with_expedition_yaml.md index d0825794..6bbf181e 100644 --- a/docs/user-guide/tutorials/working_with_expedition_yaml.md +++ b/docs/user-guide/tutorials/working_with_expedition_yaml.md @@ -95,7 +95,7 @@ This section defines the configuration settings for each instrument used in the ##### Sensors -For most users, the most important instrument configuration settings to consider are the **sensors** for each instrument, which control what type of measurements/variables the instrument records in the simulation. For example, for the `CTD` instrument, you can specify which sensors to include in the simulation (e.g., `TEMPERATURE`, `SALINITY`, `OXYGEN`, etc.) by adding or removing entries from the `sensors` list in the `ctd_config` section. These must be added on _new lines_ and be in _uppercase_, for example: +For most users, the most important instrument configuration setting to consider is the list of **sensors** for each instrument, which controls what type of measurements/variables the instrument records in the simulation. For example, for the `CTD` instrument, you can specify which sensors to include in the simulation (e.g., `TEMPERATURE`, `SALINITY`, `OXYGEN`, etc.) by adding or removing entries from the `sensors` list in the `ctd_config` section. These must be added on _new lines_ and be in _uppercase_, for example: ```yaml ctd_config: From 17d5b40bc335976b229a9cd4610d68039b1a8046 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 11:52:25 +0200 Subject: [PATCH 05/17] add details on sensor configurations to quickstart guide --- docs/user-guide/quickstart.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/user-guide/quickstart.md b/docs/user-guide/quickstart.md index cc43b8a0..a4f75997 100644 --- a/docs/user-guide/quickstart.md +++ b/docs/user-guide/quickstart.md @@ -56,7 +56,7 @@ This will create a folder/directory called `EXPEDITION_NAME` with a single file: For advanced users: it is also possible to run the expedition initialisation step without an MFP .xlsx export file. In this case you should simply run `virtualship init EXPEDITION_NAME` in the CLI. This will write an example `expedition.yaml` file in the `EXPEDITION_NAME` folder/directory. This file contains example waypoints, timings, instrument selections, and ship configuration, but can be edited or propagated through the rest of the workflow unedited to run a sample expedition. ``` -## 3) Expedition scheduling & ship configuration +## 3) Expedition scheduling & configuration ```{important} This section describes the process of finalising the expedition schedule and instrument selection using the `virtualship plan` application. This is the recommended way for most users but when expeditions become larger with many waypoints, it can become cumbersome to use the planning tool (note, using VirtualShip in a remote terminal / cloud-based environment can also introduce lag in the user-interface). **In this case, you may prefer to edit the `expedition.yaml` file directly (see [here](./tutorials/working_with_expedition_yaml.md) for more details on how to do so)**. @@ -82,6 +82,23 @@ VirtualShip is capable of taking underway temperature and salinity measurements, For the underway ADCP, there is a choice of using the 38 kHz OceanObserver or the 300 kHz SeaSeven version (see [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#ADCP) for more detail on the two ADCP types). +### Instrument configuration + +The most important instrument configuration setting to consider is the list of **sensors** for each instrument, which controls what type of measurements/variables the instrument records in the simulation and therefore what output data you will receive for each instrument. + +Sensor lists can be configured for each instrument under _Ship Config Editor_ > _Instrument Configurations_. For example, for the CTD instrument, you can specify which sensors to include in the simulation (e.g., `TEMPERATURE`, `SALINITY`, `OXYGEN`, etc.) by toggling the respective switches on or off. + +```{note} +Sensor choices are only relevant for the instruments you plan to deploy as [underway measurements](#underway-measurements) or at waypoints across your expedition schedule [(see below)](#instrument-selection). For example, if you do not select to deploy a CTD at any of your waypoints, the CTD sensor choices will not affect any output data. +``` + +```{tip} +See [here](../documentation/full_sensor_list.md) for more information on the sensors available for each instrument. + +``` + +There are other instrument configurations settings that can be adjusted in the editor as well (e.g. `max_depth` for the CTD), but these are more advanced and in most cases do not need to be changed from the default values. + ### Waypoint datetimes @@ -107,15 +124,11 @@ The MFP route planning tool will give estimated durations of sailing between sit You should now consider which measurements are to be taken at each sampling site, and therefore which instruments need to be selected in the planning tool. ```{tip} -Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on what measurement options are available, and a brief introduction to each instrument. +Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on what instrument options are available, and a brief introduction to each instrument. ``` You can make instrument selections for each waypoint in the same sub-panels as the [waypoint time](#waypoint-datetimes) selection by simply switching each on or off. Multiple instruments are allowed at each waypoint. -```{note} -For advanced users: you can also make further customisations to behaviours of all instruments under _Ship Config Editor_ > _Instrument Configurations_. -``` - ### Save changes When you are happy with your ship configuration and schedule plan, press _Save Changes_. From baa982aa43d99a8080b0b146164a03c81c10cd77 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 12:04:37 +0200 Subject: [PATCH 06/17] update sail_the_ship with sensor configuration instructions --- .../assignments/Sail_the_ship.ipynb | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/assignments/Sail_the_ship.ipynb b/docs/user-guide/assignments/Sail_the_ship.ipynb index 1e1f439d..b4b94f3d 100644 --- a/docs/user-guide/assignments/Sail_the_ship.ipynb +++ b/docs/user-guide/assignments/Sail_the_ship.ipynb @@ -157,7 +157,7 @@ "The next step is to finalise the expedition schedule plan, including setting times and instrument selection choices for each waypoint, as well as configuring the ship (including any underway measurement instruments). \n", "\n", "
\n", - "**NOTE**: This section describes the process of finalising the expedition schedule and instrument selection using the `virtualship plan` application. For expeditions with many waypoints, it can become cumbersome to use the planning tool (note, using VirtualShip in a remote terminal / cloud-based environment can also introduce lag in the user-interface). **In this case, you may prefer to edit the** `expedition.yaml` **file directly (see [here](../tutorials/working_with_expedition_yaml.md) for more details on how to do so)**.\n", + "**Note**: This section describes the process of finalising the expedition schedule and instrument selection using the `virtualship plan` application. For expeditions with many waypoints, it can become cumbersome to use the planning tool (note, using VirtualShip in a remote terminal / cloud-based environment can also introduce lag in the user-interface). **In this case, you may prefer to edit the** `expedition.yaml` **file directly (see [here](../tutorials/working_with_expedition_yaml.md) for more details on how to do so)**.\n", "
\n", "\n", "\n", @@ -177,6 +177,22 @@ "\n", "For the underway ADCP, there is a choice of using the 38 kHz OceanObserver or the 300 kHz SeaSeven version (see [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#ADCP) for more detail on the two ADCP types).\n", "\n", + "### Instrument/sensor configuration\n", + "\n", + "The most important instrument configuration setting to consider is the list of **sensors** for each instrument, which controls what type of measurements/variables the instrument records in the simulation and therefore what output data you will receive for each instrument.\n", + "\n", + "Sensor lists can be configured for each instrument under _Ship Config Editor_ > _Instrument Configurations_. For example, for the CTD instrument, you can specify which sensors to include in the simulation (e.g., `TEMPERATURE`, `SALINITY`, `OXYGEN`, etc.) by toggling the respective switches on or off.\n", + "\n", + "
\n", + "**Note**: Sensor choices are only relevant for the instruments you plan to deploy as [underway measurements](#underway-measurements) or at waypoints across your expedition schedule [(see below)](#instrument-selection). For example, if you do not select to deploy a CTD at any of your waypoints, the CTD sensor choices will not affect any output data.\n", + "
\n", + "\n", + "
\n", + "**TIP**: See [here](../documentation/full_sensor_list.md) for more information on the sensors available for each instrument.\n", + "
\n", + "\n", + "There are other instrument configurations settings that can be adjusted in the editor as well (e.g. `max_depth` for the CTD), but these are more advanced and in most cases do not need to be changed from the default values.\n", + "\n", "### Waypoint datetimes\n", "\n", "
\n", @@ -200,7 +216,7 @@ "You should now consider which measurements are to be taken at each sampling site (think about those required for your chosen research question), and therefore which instruments need to be selected in the planning tool at each waypoint.\n", "\n", "
\n", - "**Tip**: Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on what measurement options are available, and a brief introduction to each instrument.\n", + "**Tip**: Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on which instruments are available in VirtualShip, and a brief introduction to each.\n", "
\n", "\n", "You can make instrument selections for each waypoint in the same sub-panels as the [waypoint time](#waypoint-datetimes) selection by simply switching each on or off. Multiple instruments are allowed at each waypoint.\n", From a8a4978e8fb1dfd631388af9372fb7c71a33630b Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 12:04:44 +0200 Subject: [PATCH 07/17] enahnce phrasing --- docs/user-guide/quickstart.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/quickstart.md b/docs/user-guide/quickstart.md index a4f75997..7d984111 100644 --- a/docs/user-guide/quickstart.md +++ b/docs/user-guide/quickstart.md @@ -82,7 +82,7 @@ VirtualShip is capable of taking underway temperature and salinity measurements, For the underway ADCP, there is a choice of using the 38 kHz OceanObserver or the 300 kHz SeaSeven version (see [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#ADCP) for more detail on the two ADCP types). -### Instrument configuration +### Instrument/sensor configuration The most important instrument configuration setting to consider is the list of **sensors** for each instrument, which controls what type of measurements/variables the instrument records in the simulation and therefore what output data you will receive for each instrument. @@ -94,7 +94,6 @@ Sensor choices are only relevant for the instruments you plan to deploy as [unde ```{tip} See [here](../documentation/full_sensor_list.md) for more information on the sensors available for each instrument. - ``` There are other instrument configurations settings that can be adjusted in the editor as well (e.g. `max_depth` for the CTD), but these are more advanced and in most cases do not need to be changed from the default values. @@ -124,7 +123,7 @@ The MFP route planning tool will give estimated durations of sailing between sit You should now consider which measurements are to be taken at each sampling site, and therefore which instruments need to be selected in the planning tool. ```{tip} -Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on what instrument options are available, and a brief introduction to each instrument. +Click [here](https://virtualship.readthedocs.io/en/latest/user-guide/assignments/Research_proposal_intro.html#Measurement-Options) for more information on what instrument options are available in VirtualShip, and a brief introduction to each. ``` You can make instrument selections for each waypoint in the same sub-panels as the [waypoint time](#waypoint-datetimes) selection by simply switching each on or off. Multiple instruments are allowed at each waypoint. From 6b3628ed1d123d00fee7f3639753d28c6d0b199b Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 13:05:42 +0200 Subject: [PATCH 08/17] add testing that docs match the code for sensor options, ensure instrument column entries are consistent --- .../documentation/full_sensor_list.md | 38 ++++++----- tests/test_utils.py | 65 ++++++++++++++++++- 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/docs/user-guide/documentation/full_sensor_list.md b/docs/user-guide/documentation/full_sensor_list.md index 1cc072a7..7e108b67 100644 --- a/docs/user-guide/documentation/full_sensor_list.md +++ b/docs/user-guide/documentation/full_sensor_list.md @@ -6,21 +6,23 @@ The following table provides a comprehensive list of available sensors for each Trying to add a sensor to an instrument that does not support it will result in errors in VirtualShip. Always refer to this table to check which sensors are available for each instrument. ``` -| Instrument | Sensor Name | Description | Units | Category | -| :--------------------- | :----------------- | :-------------------------------------------------- | :----------------------------------- | :-------------- | -| **ADCP** | VELOCITY | Current velocities (eastward (u) and northward (v)) | m/s | Physical | -| **Ship Underwater ST** | TEMPERATURE | Temperature | °C | Physics | -| | SALINITY | Salinity | psu | Physics | -| **CTD** | TEMPERATURE | Temperature | °C | Physics | -| | SALINITY | Salinity | psu | Physics | -| | OXYGEN | Oxygen concentration | mmol m-3 | Biogeochemistry | -| | CHLOROPHYLL | Chlorophyll concentration | mmol m-3 | Biogeochemistry | -| | NITRATE | Nitrate concentration | mmol m-3 | Biogeochemistry | -| | PHOSPHATE | Phosphate concentration | mmol m-3 | Biogeochemistry | -| | PH | pH | - | Biogeochemistry | -| | PHYTOPLANKTON | Phytoplankton concentration in carbon | mmol m-3 | Biogeochemistry | -| | PRIMARY_PRODUCTION | Net primary production | mmol m-3 day-1 | Biogeochemistry | -| **ARGO_FLOAT** | TEMPERATURE | Temperature | °C | Physics | -| | SALINITY | Salinity | psu | Physics | -| **DRIFTER** | TEMPERATURE | Temperature | °C | Physics | -| **XBT** | TEMPERATURE | Temperature | °C | Physics | + + +| Instrument | Sensor Name | Description | Units | Category | +| :------------------------------------- | :----------------- | :-------------------------------------------------- | :----------------------------------- | :-------------- | +| **ADCP** | VELOCITY | Current velocities (eastward (u) and northward (v)) | m/s | Physical | +| **UNDERWATER_ST** (Ship Underwater ST) | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| **CTD** | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| | OXYGEN | Oxygen concentration | mmol m-3 | Biogeochemistry | +| | CHLOROPHYLL | Chlorophyll concentration | mmol m-3 | Biogeochemistry | +| | NITRATE | Nitrate concentration | mmol m-3 | Biogeochemistry | +| | PHOSPHATE | Phosphate concentration | mmol m-3 | Biogeochemistry | +| | PH | pH | - | Biogeochemistry | +| | PHYTOPLANKTON | Phytoplankton concentration in carbon | mmol m-3 | Biogeochemistry | +| | PRIMARY_PRODUCTION | Net primary production | mmol m-3 day-1 | Biogeochemistry | +| **ARGO_FLOAT** | TEMPERATURE | Temperature | °C | Physics | +| | SALINITY | Salinity | psu | Physics | +| **DRIFTER** | TEMPERATURE | Temperature | °C | Physics | +| **XBT** | TEMPERATURE | Temperature | °C | Physics | diff --git a/tests/test_utils.py b/tests/test_utils.py index fde6796f..3196fa03 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,12 +1,13 @@ import datetime +import re from pathlib import Path 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 @@ -399,3 +400,65 @@ def test_build_particle_class_scipy_base(): ParticleClass = build_particle_class_from_sensors(sensors, nonsensor, ScipyParticle) assert issubclass(ParticleClass, ScipyParticle) + + +def test_allowed_sensors_matches_docs(): + """Test that SUPPORTED_SENSORS_MAP (sensors allowed for each instrument) matches the sensor table in full_sensor_list.md.""" + # local imports to trigger instrument registration and avoid potential circular imports + import virtualship.instruments # noqa: F401 - ensures all @register_instrument decorators run + from virtualship.utils import INSTRUMENT_CLASS_MAP, SUPPORTED_SENSORS_MAP + + docs_path = ( + Path(__file__).parent.parent + / "docs/user-guide/documentation/full_sensor_list.md" + ) + content = docs_path.read_text(encoding="utf-8") + + display_name_to_instrument_type: dict[str, InstrumentType] = { + instrument_type.value: instrument_type + for instrument_type in INSTRUMENT_CLASS_MAP + if isinstance(instrument_type, InstrumentType) + } # all instruments should use their enum value as the bold display name in the markdown table + + # parse markdown table rows + row_pattern = re.compile(r"^\|([^|]*)\|([^|]*)\|.*$", re.MULTILINE) + + expected: dict[InstrumentType, set[SensorType]] = {} + current_instrument: InstrumentType | None = None + + for match in row_pattern.finditer(content): + instrument_cell = match.group(1).strip() + sensor_cell = match.group(2).strip() + + # extract only the **bold** text from the cell (e.g. "**UNDERWATER_ST** (Ship Underwater ST)" -> "UNDERWATER_ST") + bold_match = re.search(r"\*\*(.+?)\*\*", instrument_cell) + instrument_name = bold_match.group(1).strip() if bold_match else "" + + if instrument_name and instrument_name in display_name_to_instrument_type: + current_instrument = display_name_to_instrument_type[instrument_name] + if current_instrument not in expected: + expected[current_instrument] = set() + + # skip irrelevant cells + if ( + not sensor_cell + or sensor_cell.startswith(":") + or sensor_cell == "Sensor Name" + ): + continue + + sensor_name = sensor_cell.strip() + if current_instrument is not None and sensor_name: + expected[current_instrument].add(SensorType(sensor_name)) + + # verify each instrument in the docs is registered and has matching sensors + for instrument_type, doc_sensors in expected.items(): + assert instrument_type in SUPPORTED_SENSORS_MAP, ( + f"{instrument_type} is listed in full_sensor_list.md but not found in SUPPORTED_SENSORS_MAP." + ) + registered_sensors = set(SUPPORTED_SENSORS_MAP[instrument_type]) + assert registered_sensors == doc_sensors, ( + f"Sensor mismatch for {instrument_type}:\n" + f" In docs: {sorted(s.value for s in doc_sensors)}\n" + f" In code: {sorted(s.value for s in registered_sensors)}\n" + ) From e23087b61cae509ec7be54be780d57f4af21f48c Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 13:10:46 +0200 Subject: [PATCH 09/17] add check that all instruments in code are addressed in sensor table --- tests/test_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3196fa03..4ab96654 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -462,3 +462,9 @@ def test_allowed_sensors_matches_docs(): f" In docs: {sorted(s.value for s in doc_sensors)}\n" f" In code: {sorted(s.value for s in registered_sensors)}\n" ) + + # verify each instrument registered in code is also covered in the docs + for instrument_type in SUPPORTED_SENSORS_MAP: + assert instrument_type in expected, ( + f"{instrument_type} is registered in SUPPORTED_SENSORS_MAP but not listed in full_sensor_list.md." + ) From a5ff3a44362f0cd688f03836ae2ec41bb4f27c82 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 14:17:44 +0200 Subject: [PATCH 10/17] update environments to pull parcels v4 alpha --- pixi.toml | 24 +++++++++++------------- pyproject.toml | 6 +++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pixi.toml b/pixi.toml index ba024968..0d4b8d87 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,7 +1,7 @@ [workspace] name = "VirtualShip" preview = ["pixi-build"] -channels = ["conda-forge"] +channels = ["https://repo.prefix.dev/parcels", "conda-forge"] platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"] exclude-newer = "5d" # security pre-caution against compromised packages requires-pixi = ">=0.67.0" @@ -18,9 +18,9 @@ setuptools = "*" setuptools_scm = "*" [package.run-dependencies] # Keep in sync with `pyproject.toml` and feedstock recipe -python = ">=3.10" +python = "3.11.*" click = "*" -parcels = ">3.1.0" +parcels = ">=4.0.0alpha" pyproj = ">=3,<4" sortedcontainers = "==2.4.0" opensimplex = "==0.4.5" @@ -34,14 +34,15 @@ textual = "*" [dependencies] virtualship = { path = "." } -[feature.py310.dependencies] -python = "3.10.*" +# Commented out whilst parcels v4 alpha only supports Python 3.11 +# [feature.py310.dependencies] +# python = "3.10.*" -[feature.py311.dependencies] -python = "3.11.*" +# [feature.py311.dependencies] +# python = "3.11.*" -[feature.py312.dependencies] -python = "3.12.*" +# [feature.py312.dependencies] +# python = "3.12.*" [feature.test.dependencies] pytest = "*" @@ -98,11 +99,8 @@ lxml = "*" typing = "mypy src/virtualship --install-types" [environments] -default = { features = ["test", "notebooks", "typing", "pre-commit", "analysis"] } +default = { features = ["test", "notebooks", "typing", "pre-commit", "analysis"] } test-latest = { features = ["test"], solve-group = "test" } -test-py310 = { features = ["test", "py310"] } -test-py311 = { features = ["test", "py311"] } -test-py312 = { features = ["test", "py312"] } test-notebooks = { features = ["test", "notebooks"], solve-group = "test" } analysis = { features = ["analysis"], solve-group = "analysis" } docs = { features = ["docs"], solve-group = "docs" } diff --git a/pyproject.toml b/pyproject.toml index 7f9a2108..bc9346d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "virtualship" description = "Code for the Virtual Ship Classroom, where Marine Scientists can combine Copernicus Marine Data with an OceanParcels ship to go on a virtual expedition." readme = "README.md" dynamic = ["version"] -authors = [{ name = "oceanparcels.org team" }] +authors = [{ name = "parcels-code.org team" }] requires-python = ">=3.10" license = { file = "LICENSE" } classifiers = [ @@ -26,7 +26,7 @@ classifiers = [ ] dependencies = [ "click", - "parcels >3.1.0", + "parcels >=4.0.0alpha", "pyproj >= 3, < 4", "sortedcontainers == 2.4.0", "opensimplex == 0.4.5", @@ -40,7 +40,7 @@ dependencies = [ ] [project.urls] -Homepage = "https://oceanparcels.org/" # TODO: Update this to just be repo? +Homepage = "https://virtualship.parcels-code.org/" Repository = "https://github.com/OceanParcels/virtualship" Documentation = "https://virtualship.readthedocs.io/" "Bug Tracker" = "https://github.com/OceanParcels/virtualship/issues" From db443243b2d2d6c55031c7bfff7b0e893b2a1acc Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 14:18:34 +0200 Subject: [PATCH 11/17] changed parcels logging api --- src/virtualship/cli/_run.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/virtualship/cli/_run.py b/src/virtualship/cli/_run.py index f2622be3..703502f2 100644 --- a/src/virtualship/cli/_run.py +++ b/src/virtualship/cli/_run.py @@ -35,11 +35,9 @@ get_instrument_class, ) -# parcels logger (suppress INFO messages to prevent log being flooded) -external_logger = logging.getLogger("parcels.tools.loggers") -external_logger.setLevel(logging.WARNING) - -# copernicusmarine logger (suppress INFO messages to prevent log being flooded) +# suppress INFO messages from copernicusmarine and parcels loggers; prevent log flooding +parcels_logger = logging.getLogger("parcels._logger") +parcels_logger.setLevel(logging.WARNING) logging.getLogger("copernicusmarine").setLevel("ERROR") From 6f732d3511a53760ed99e86531d0a4eadc596b26 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 15:36:47 +0200 Subject: [PATCH 12/17] first wave of changes to the instrument logic with v4 logic, and particle building --- src/virtualship/cli/_run.py | 1 + src/virtualship/instruments/adcp.py | 14 +- src/virtualship/instruments/argo_float.py | 197 ++++++++++-------- src/virtualship/instruments/ctd.py | 92 +++++--- src/virtualship/instruments/drifter.py | 25 ++- .../instruments/ship_underwater_st.py | 16 +- src/virtualship/instruments/xbt.py | 35 ++-- src/virtualship/utils.py | 7 +- 8 files changed, 220 insertions(+), 167 deletions(-) diff --git a/src/virtualship/cli/_run.py b/src/virtualship/cli/_run.py index 703502f2..10afc5c5 100644 --- a/src/virtualship/cli/_run.py +++ b/src/virtualship/cli/_run.py @@ -200,6 +200,7 @@ def _run( ) # execute simulation + # TODO: outpath will be Parquet with v4... instrument.execute( measurements=measurements, out_path=expedition_dir.joinpath(RESULTS, f"{itype.name.lower()}.zarr"), diff --git a/src/virtualship/instruments/adcp.py b/src/virtualship/instruments/adcp.py index b2da6582..7ee718f6 100644 --- a/src/virtualship/instruments/adcp.py +++ b/src/virtualship/instruments/adcp.py @@ -3,8 +3,8 @@ from typing import ClassVar import numpy as np -from parcels import ParticleSet, ScipyParticle +from parcels import ParticleSet from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -35,9 +35,13 @@ class ADCP: # ===================================================== -def _sample_velocity(particle, fieldset, time): - particle.U, particle.V = fieldset.UV.eval( - time, particle.depth, particle.lat, particle.lon, applyConversion=False +def _sample_velocity(particles, fieldset): + particles.U, particles.V = fieldset.UV.eval( + particles.time, + particles.z, + particles.lat, + particles.lon, + applyConversion=False, ) @@ -96,7 +100,7 @@ def simulate(self, measurements, out_path) -> None: # build dynamic particle class from the active sensors adcp_config = self.expedition.instruments_config.adcp_config _ADCPParticle = build_particle_class_from_sensors( - adcp_config.sensors, _ADCP_NONSENSOR_VARIABLES, ScipyParticle + adcp_config.sensors, _ADCP_NONSENSOR_VARIABLES ) bins = np.linspace(MAX_DEPTH, MIN_DEPTH, NUM_BINS) diff --git a/src/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py index 8c90cfb2..70fcb146 100644 --- a/src/virtualship/instruments/argo_float.py +++ b/src/virtualship/instruments/argo_float.py @@ -1,12 +1,11 @@ -import math from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta from typing import ClassVar import numpy as np -from parcels import AdvectionRK4, JITParticle, ParticleSet, StatusCode, Variable +from parcels import AdvectionRK4, ParticleSet, StatusCode, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -53,103 +52,125 @@ class ArgoFloat: # SECTION: Kernels # ===================================================== - -def _argo_float_vertical_movement(particle, fieldset, time): - if particle.cycle_phase == 0: - # Phase 0: Sinking with vertical_speed until depth is drift_depth - particle_ddepth += ( # noqa - particle.vertical_speed * particle.dt +# TODO: need to add back in the shallow bathymetry checks (to phases 0 and 2?!) +# TODO: can this be refactored as well to a helper function? + + +def _argo_float_vertical_movement(particles, fieldset): + # Split particles based on their current cycle_phase + ptcls0 = particles[particles.cycle_phase == 0] + ptcls1 = particles[particles.cycle_phase == 1] + ptcls2 = particles[particles.cycle_phase == 2] + ptcls3 = particles[particles.cycle_phase == 3] + ptcls4 = particles[particles.cycle_phase == 4] + + # Phase 0: Sinking with vertical_speed until depth is driftdepth + ptcls0.dz += particles.vertical_speed * ptcls0.dt + loc_bathy = fieldset.bathymetry.eval(ptcls0.time, ptcls0.z, ptcls0.lat, ptcls0.lon) + driftdepth_mask = ptcls0.z + ptcls0.dz >= particles.drift_depth + bathy_mask = ptcls0.z + ptcls0.dz >= loc_bathy + next_phase = np.logical_and( + driftdepth_mask, bathy_mask + ) # combined mask; not at drift depth yet and not hitting bathymetry + ptcls0.cycle_phase[next_phase] = 1 + ptcls0.dz[next_phase] = ( + particles.drift_depth - ptcls0.z[next_phase] + ) # avoid overshoot + + # Phase 0.5: Check for grounding at bathymetry and raise if necessary + ptcls0.grounded[~bathy_mask] = 1 + if np.any(~bathy_mask): + print( + "Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to drift depth. Raising by 50m above bathymetry and continuing cycle." ) - - # bathymetry at particle location - loc_bathy = fieldset.bathymetry.eval( - time, particle.depth, particle.lat, particle.lon + ptcls0.dz[~bathy_mask] = ( + loc_bathy[~bathy_mask] - ptcls0.z[~bathy_mask] + 50.0 + ) # raise to 50m above bathymetry + ptcls0.cycle_phase[~bathy_mask] = 1 + + # Phase 1: Drifting at depth for drifttime seconds + ptcls1.drift_age += ptcls1.dt + next_phase = ptcls1.drift_age >= particles.drift_days * 86400 # [seconds] + ptcls1.cycle_phase[next_phase] = 2 + ptcls1.drift_age[next_phase] = 0 # reset drift_age for next cycle + + # Phase 2: Sinking further to maxdepth + ptcls2.dz += particles.vertical_speed * ptcls2.dt + loc_bathy = fieldset.bathymetry.eval(ptcls2.time, ptcls2.z, ptcls2.lat, ptcls2.lon) + maxdepth_mask = ptcls2.z + ptcls2.dz >= particles.max_depth + bathy_mask = ptcls2.z + ptcls2.dz >= loc_bathy + next_phase = np.logical_and( + maxdepth_mask, bathy_mask + ) # combined mask; not at max depth yet and not hitting bathymetry + ptcls2.cycle_phase[next_phase] = 3 + ptcls2.dz[next_phase] = ( + particles.max_depth - ptcls2.z[next_phase] + ) # avoid overshoot + + # Phase 2.5: Check for grounding at bathymetry and raise if necessary + ptcls2.grounded[~bathy_mask] = 1 + if np.any(~bathy_mask): + print( + "Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to max depth. Raising by 50m above bathymetry and continuing cycle." ) - if particle.depth + particle_ddepth <= loc_bathy: - particle_ddepth = loc_bathy - particle.depth + 50.0 # 50m above bathy - particle.cycle_phase = 1 - particle.grounded = 1 - print( - "Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to drift depth. Raising by 50m above bathymetry and continuing cycle." - ) + ptcls2.dz[~bathy_mask] = ( + loc_bathy[~bathy_mask] - ptcls2.z[~bathy_mask] + 50.0 + ) # raise to 50m above bathymetry + ptcls2.cycle_phase[~bathy_mask] = 3 - elif particle.depth + particle_ddepth <= particle.drift_depth: - particle_ddepth = particle.drift_depth - particle.depth - particle.cycle_phase = 1 - - elif particle.cycle_phase == 1: - # Phase 1: Drifting at depth for drifttime seconds - particle.drift_age += particle.dt - if particle.drift_age >= particle.drift_days * 86400: - particle.drift_age = 0 # reset drift_age for next cycle - particle.cycle_phase = 2 - - elif particle.cycle_phase == 2: - # Phase 2: Sinking further to max_depth - particle_ddepth += particle.vertical_speed * particle.dt - loc_bathy = fieldset.bathymetry.eval( - time, particle.depth, particle.lat, particle.lon - ) - if particle.depth + particle_ddepth <= loc_bathy: - particle_ddepth = loc_bathy - particle.depth + 50.0 # 50m above bathy - particle.cycle_phase = 3 - particle.grounded = 1 - print( - "Shallow bathymetry warning: Argo float grounded at bathymetry depth during sinking to max depth. Raising by 50m above bathymetry and continuing cycle." - ) - elif particle.depth + particle_ddepth <= particle.max_depth: - particle_ddepth = particle.max_depth - particle.depth - particle.cycle_phase = 3 - - elif particle.cycle_phase == 3: - # Phase 3: Rising with vertical_speed until at surface - particle_ddepth -= particle.vertical_speed * particle.dt - particle.cycle_age += ( - particle.dt - ) # solve issue of not updating cycle_age during ascent - particle.grounded = 0 - if particle.depth + particle_ddepth >= particle.min_depth: - particle_ddepth = particle.min_depth - particle.depth - particle.cycle_phase = 4 + # Phase 3: Rising with vertical_speed until at surface + ptcls3.dz -= particles.vertical_speed * ptcls3.dt + ptcls3.temp = fieldset.thetao[ptcls3.time, ptcls3.z, ptcls3.lat, ptcls3.lon] + next_phase = ptcls3.z + ptcls3.dz <= particles.min_depth + ptcls3.cycle_phase[next_phase] = 4 + ptcls3.dz[next_phase] = ( + particles.min_depth - ptcls3.z[next_phase] + ) # avoid overshoot - elif particle.cycle_phase == 4: - # Phase 4: Transmitting at surface until cycletime is reached - if particle.cycle_age > particle.cycle_days * 86400: - particle.cycle_phase = 0 - particle.cycle_age = 0 + # Phase 4: Transmitting at surface until cycletime is reached + next_phase = ptcls4.cycle_age >= particles.cycle_days * 86400 + ptcls4.cycle_phase[next_phase] = 0 + ptcls4.cycle_age[next_phase] = 0 # reset cycle_age for next cycle + ptcls4.temp = np.nan # no temperature measurement when at surface - if particle.state == StatusCode.Evaluate: - particle.cycle_age += particle.dt # update cycle_age + particles.cycle_age += particles.dt # update cycle_age -def _keep_at_surface(particle, fieldset, time): - # Prevent error when float reaches surface - if particle.state == StatusCode.ErrorThroughSurface: - particle.depth = particle.min_depth - particle.state = StatusCode.Success +def _keep_at_surface(particles, fieldset): + through_surface = particles.state == StatusCode.ErrorThroughSurface + particles.z[through_surface] = particles.min_depth[through_surface] + particles.state[through_surface] = StatusCode.Success -def _check_error(particle, fieldset, time): - if particle.state >= 50: # This captures all Errors - particle.delete() +def _check_error(particles, fieldset): + errors = particles.state >= 50 # captures all Errors + particles.state[errors] = StatusCode.Delete -def _argo_sample_temperature(particle, fieldset, time): +def _argo_sample_temperature(particles, fieldset): # Phase 3: ascending — sample temperature; NaN otherwise - if particle.cycle_phase == 3 and particle.depth < particle.min_depth: - particle.temperature = fieldset.T[ - time, particle.depth, particle.lat, particle.lon - ] - else: - particle.temperature = math.nan - - -def _argo_sample_salinity(particle, fieldset, time): + phase_mask = particles.cycle_phase == 3 + depth_mask = particles.depth < particles.min_depth + sampling_particles = particles[np.logical_and(phase_mask, depth_mask)] + sampling_particles.temperature = fieldset.T[ + sampling_particles.time, + sampling_particles.depth, + sampling_particles.lat, + sampling_particles.lon, + ] + + +def _argo_sample_salinity(particles, fieldset): # Phase 3: ascending — sample salinity; NaN otherwise - if particle.cycle_phase == 3 and particle.depth < particle.min_depth: - particle.salinity = fieldset.S[time, particle.depth, particle.lat, particle.lon] - else: - particle.salinity = math.nan + phase_mask = particles.cycle_phase == 3 + depth_mask = particles.depth < particles.min_depth + sampling_particles = particles[np.logical_and(phase_mask, depth_mask)] + sampling_particles.salinity = fieldset.S[ + sampling_particles.time, + sampling_particles.depth, + sampling_particles.lat, + sampling_particles.lon, + ] # ===================================================== @@ -229,9 +250,7 @@ def simulate(self, measurements, out_path) -> None: # build dynamic particle class from the active sensors argo_float_config = self.expedition.instruments_config.argo_float_config _ArgoParticle = build_particle_class_from_sensors( - argo_float_config.sensors, - _ARGO_NONSENSOR_VARIABLES, - JITParticle, + argo_float_config.sensors, _ARGO_NONSENSOR_VARIABLES ) # define parcel particles diff --git a/src/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py index 583a099c..a7a4218f 100644 --- a/src/virtualship/instruments/ctd.py +++ b/src/virtualship/instruments/ctd.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING, ClassVar import numpy as np -from parcels import JITParticle, ParticleSet, Variable +from parcels._core.statuscodes import StatusCode +from parcels import ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.utils import ( - add_dummy_UV, build_particle_class_from_sensors, register_instrument, ) @@ -52,60 +52,87 @@ class CTD: ## physical variables -def _sample_temperature(particle, fieldset, time): - particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] +def _sample_temperature(particles, fieldset): + particles.temperature = fieldset.T[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_salinity(particle, fieldset, time): - particle.salinity = fieldset.S[time, particle.depth, particle.lat, particle.lon] +def _sample_salinity(particles, fieldset): + particles.salinity = fieldset.S[ + particles.time, particles.z, particles.lat, particles.lon + ] ## bgc variables -def _sample_o2(particle, fieldset, time): - particle.o2 = fieldset.o2[time, particle.depth, particle.lat, particle.lon] +def _sample_o2(particles, fieldset): + particles.o2 = fieldset.o2[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_chlorophyll(particle, fieldset, time): - particle.chl = fieldset.chl[time, particle.depth, particle.lat, particle.lon] +def _sample_chlorophyll(particles, fieldset): + particles.chl = fieldset.chl[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_nitrate(particle, fieldset, time): - particle.no3 = fieldset.no3[time, particle.depth, particle.lat, particle.lon] +def _sample_nitrate(particles, fieldset): + particles.no3 = fieldset.no3[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_phosphate(particle, fieldset, time): - particle.po4 = fieldset.po4[time, particle.depth, particle.lat, particle.lon] +def _sample_phosphate(particles, fieldset): + particles.po4 = fieldset.po4[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_ph(particle, fieldset, time): - particle.ph = fieldset.ph[time, particle.depth, particle.lat, particle.lon] +def _sample_ph(particles, fieldset): + particles.ph = fieldset.ph[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_phytoplankton(particle, fieldset, time): - particle.phyc = fieldset.phyc[time, particle.depth, particle.lat, particle.lon] +def _sample_phytoplankton(particles, fieldset): + particles.phyc = fieldset.phyc[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _sample_primary_production(particle, fieldset, time): - particle.nppv = fieldset.nppv[time, particle.depth, particle.lat, particle.lon] +def _sample_primary_production(particles, fieldset): + particles.nppv = fieldset.nppv[ + particles.time, particles.z, particles.lat, particles.lon + ] ## cast -def _ctd_cast(particle, fieldset, time): +def _ctd_cast(particles, fieldset): + particles_lowering = particles[particles.raising == 0] + particles_raising = particles[particles.raising == 1] + + # TODO: change to boolean masking, like with Argo Floats? + # 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 + particles_lowering.dz = -particles_lowering.winch_speed * particles_lowering.dt + particles_lowering.raising = np.where( + particles_lowering.z + particles_lowering.dz < particles_lowering.max_depth, + 1, + particles_lowering.raising, + ) + # raising - else: - particle_ddepth = particle.winch_speed * particle.dt - if particle.depth + particle_ddepth > particle.min_depth: - particle.delete() + particles_raising.dz = particles_raising.winch_speed * particles_raising.dt + particles_raising.state = np.where( + particles_raising.z + particles_raising.dz > particles_raising.min_depth, + StatusCode.Delete, + particles_raising.state, + ) # ===================================================== @@ -162,9 +189,6 @@ def simulate(self, measurements, out_path) -> None: 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) @@ -208,7 +232,7 @@ def simulate(self, measurements, out_path) -> None: # build dynamic particle class from the active sensors ctd_config = self.expedition.instruments_config.ctd_config _CTDParticle = build_particle_class_from_sensors( - ctd_config.sensors, _CTD_NONSENSOR_VARIABLES, JITParticle + ctd_config.sensors, _CTD_NONSENSOR_VARIABLES ) # define parcel particles diff --git a/src/virtualship/instruments/drifter.py b/src/virtualship/instruments/drifter.py index 379334b3..46ae82e9 100644 --- a/src/virtualship/instruments/drifter.py +++ b/src/virtualship/instruments/drifter.py @@ -4,8 +4,9 @@ from typing import ClassVar import numpy as np -from parcels import AdvectionRK4, JITParticle, ParticleSet, Variable +from parcels._core.statuscodes import StatusCode +from parcels import AdvectionRK4, ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -46,15 +47,21 @@ class Drifter: # ===================================================== -def _sample_temperature(particle, fieldset, time): - particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] +def _sample_temperature(particles, fieldset): + particles.temperature = fieldset.T[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _check_lifetime(particle, fieldset, time): - if particle.has_lifetime == 1: - particle.age += particle.dt - if particle.age >= particle.lifetime: - particle.delete() +def _check_lifetime(particles, fieldset): + particles_wlifetime = particles[particles.has_lifetime == 1] + + particles_wlifetime.age += particles_wlifetime.dt + particles_wlifetime.state = np.where( + particles_wlifetime.age >= particles_wlifetime.lifetime, + StatusCode.Delete, + particles_wlifetime.state, + ) # ===================================================== @@ -123,7 +130,7 @@ def simulate(self, measurements, out_path) -> None: # build dynamic particle class from the active sensors drifter_config = self.expedition.instruments_config.drifter_config _DrifterParticle = build_particle_class_from_sensors( - drifter_config.sensors, _DRIFTER_NONSENSOR_VARIABLES, JITParticle + drifter_config.sensors, _DRIFTER_NONSENSOR_VARIABLES ) # define parcel particles diff --git a/src/virtualship/instruments/ship_underwater_st.py b/src/virtualship/instruments/ship_underwater_st.py index 6a564cc0..78c757d2 100644 --- a/src/virtualship/instruments/ship_underwater_st.py +++ b/src/virtualship/instruments/ship_underwater_st.py @@ -3,13 +3,12 @@ from typing import ClassVar import numpy as np -from parcels import ParticleSet, ScipyParticle +from parcels import ParticleSet from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType from virtualship.utils import ( - add_dummy_UV, build_particle_class_from_sensors, register_instrument, ) @@ -40,13 +39,13 @@ class Underwater_ST: # define function sampling Salinity -def _sample_salinity(particle, fieldset, time): - particle.salinity = fieldset.S[time, particle.depth, particle.lat, particle.lon] +def _sample_salinity(particles, fieldset): + particles.S = fieldset.S[particles.time, particles.z, particles.lat, particles.lon] # define function sampling Temperature -def _sample_temperature(particle, fieldset, time): - particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] +def _sample_temperature(particles, fieldset): + particles.T = fieldset.T[particles.time, particles.z, particles.lat, particles.lon] # ===================================================== @@ -95,13 +94,10 @@ def simulate(self, measurements, out_path) -> None: fieldset = self.load_input_data() - # add dummy U - add_dummy_UV(fieldset) # TODO: parcels v3 bodge; remove when parcels v4 is used - # build dynamic particle class from the active sensors st_config = self.expedition.instruments_config.ship_underwater_st_config _ShipSTParticle = build_particle_class_from_sensors( - st_config.sensors, _ST_NONSENSOR_VARIABLES, ScipyParticle + st_config.sensors, _ST_NONSENSOR_VARIABLES ) particleset = ParticleSet.from_list( diff --git a/src/virtualship/instruments/xbt.py b/src/virtualship/instruments/xbt.py index 051bf1fa..06e862a6 100644 --- a/src/virtualship/instruments/xbt.py +++ b/src/virtualship/instruments/xbt.py @@ -4,14 +4,14 @@ from typing import ClassVar import numpy as np -from parcels import JITParticle, ParticleSet, Variable +from parcels._core.statuscodes import StatusCode +from parcels import 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, ) @@ -50,26 +50,32 @@ class XBT: # ===================================================== -def _sample_temperature(particle, fieldset, time): - particle.temperature = fieldset.T[time, particle.depth, particle.lat, particle.lon] +def _sample_temperature(particles, fieldset): + particles.temperature = fieldset.T[ + particles.time, particles.z, particles.lat, particles.lon + ] -def _xbt_cast(particle, fieldset, time): - particle_ddepth = -particle.fall_speed * particle.dt +def _xbt_cast(particles, fieldset): + particles.dz = -particles.fall_speed * particles.dt # update the fall speed from the quadractic fall-rate equation # check https://doi.org/10.5194/os-7-231-2011 - particle.fall_speed = ( - particle.fall_speed - 2 * particle.deceleration_coefficient * particle.dt + particles.fall_speed = ( + particles.fall_speed - 2 * particles.deceleration_coefficient * particles.dt ) # delete particle if depth is exactly max_depth - if particle.depth == particle.max_depth: - particle.delete() + particles.state = np.where( + particles.z == particles.max_depth, StatusCode.Delete, particles.state + ) # set particle depth to max depth if it's too deep - if particle.depth + particle_ddepth < particle.max_depth: - particle_ddepth = particle.max_depth - particle.depth + particles.dz = np.where( + particles.z + particles.dz < particles.max_depth, + particles.max_depth - particles.z, + particles.z, + ) # ===================================================== @@ -117,9 +123,6 @@ def simulate(self, measurements, out_path) -> None: 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) @@ -166,7 +169,7 @@ def simulate(self, measurements, out_path) -> None: # build dynamic particle class from the active sensors xbt_config = self.expedition.instruments_config.xbt_config _XBTParticle = build_particle_class_from_sensors( - xbt_config.sensors, _XBT_NONSENSOR_VARIABLES, JITParticle + xbt_config.sensors, _XBT_NONSENSOR_VARIABLES ) # define xbt particles diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 7ad275cd..fb585934 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, Particle, Variable from virtualship.errors import CopernicusCatalogueError if TYPE_CHECKING: @@ -677,14 +677,13 @@ def _make_hash(s: str, length: int) -> str: def build_particle_class_from_sensors( sensors: list[SensorConfig], nonsensor_variables: list[Variable], - particle_class: type, # generic type annotation needed for v3 particle class behaviour # TODO: Update with Parcels v4 ) -> type: - """Build a Particle class (JITParticle or ScipyParticle) from nonsensor variables and active sensors.""" + """Build a Particle class from nonsensor variables and active sensors.""" sensor_variables = [ variable for sc in sensors if sc.enabled for variable in sc.meta.particle_vars ] - return particle_class.add_variables(nonsensor_variables + sensor_variables) + return Particle.add_variables(nonsensor_variables + sensor_variables) # ===================================================== From 71881ccc921374fd8d4ea5b8d662499229250361 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 15:37:14 +0200 Subject: [PATCH 13/17] remove add dummy UV func, shouldn't be needed in v4 (?) --- src/virtualship/utils.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index fb585934..3dab6b70 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -340,29 +340,6 @@ def _get_expedition(expedition_dir: Path) -> Expedition: ) from e -def add_dummy_UV(fieldset: FieldSet): - """Add a dummy U and V field to a FieldSet to satisfy parcels FieldSet completeness checks.""" - if "U" not in fieldset.__dict__.keys(): - for uv_var in ["U", "V"]: - dummy_field = getattr( - FieldSet.from_data( - {"U": 0, "V": 0}, {"lon": 0, "lat": 0}, mesh="spherical" - ), - uv_var, - ) - fieldset.add_field(dummy_field) - try: - fieldset.time_origin = ( - fieldset.T.grid.time_origin - if "T" in fieldset.__dict__.keys() - else fieldset.o2.grid.time_origin - ) - except Exception: - raise ValueError( - "Cannot determine time_origin for dummy UV fields. Assert T or o2 exists in fieldset." - ) from None - - def _select_product_id( physical: bool, schedule_start, From 62b2b1b62187bfb3bf0744ffec5525c6e336a633 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 16:54:31 +0200 Subject: [PATCH 14/17] pull v4 from parcels/main --- pixi.toml | 12 +++++++++--- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pixi.toml b/pixi.toml index 0d4b8d87..b86b8f60 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,7 +1,7 @@ [workspace] name = "VirtualShip" preview = ["pixi-build"] -channels = ["https://repo.prefix.dev/parcels", "conda-forge"] +channels = ["conda-forge"] platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"] exclude-newer = "5d" # security pre-caution against compromised packages requires-pixi = ">=0.67.0" @@ -20,11 +20,10 @@ setuptools_scm = "*" [package.run-dependencies] # Keep in sync with `pyproject.toml` and feedstock recipe python = "3.11.*" click = "*" -parcels = ">=4.0.0alpha" pyproj = ">=3,<4" sortedcontainers = "==2.4.0" opensimplex = "==0.4.5" -numpy = ">=1,<2" +numpy = ">=2.1.0" pydantic = ">=2,<3" pyyaml = "*" copernicusmarine = ">=2.2.2" @@ -33,6 +32,13 @@ textual = "*" [dependencies] virtualship = { path = "." } +# Pre-install as conda packages to avoid PyPI source builds +netcdf4 = "*" +numpy = ">=2.1.0" +dask = "*" + +[pypi-dependencies] +parcels = { git = "https://github.com/Parcels-code/Parcels", branch = "main" } # Commented out whilst parcels v4 alpha only supports Python 3.11 # [feature.py310.dependencies] diff --git a/pyproject.toml b/pyproject.toml index bc9346d0..e19024d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "pyproj >= 3, < 4", "sortedcontainers == 2.4.0", "opensimplex == 0.4.5", - "numpy >=1, < 2", + "numpy >=2.1.0", "pydantic >=2, <3", "PyYAML", "copernicusmarine >= 2.2.2", From 38afc560ab5c614244b2413e31ba14847b3d98fc Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 16:56:40 +0200 Subject: [PATCH 15/17] use AdvectionRK2 --- src/virtualship/instruments/argo_float.py | 5 +++-- src/virtualship/instruments/drifter.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py index 70fcb146..d1644849 100644 --- a/src/virtualship/instruments/argo_float.py +++ b/src/virtualship/instruments/argo_float.py @@ -4,8 +4,9 @@ from typing import ClassVar import numpy as np +from parcels.kernels import AdvectionRK2 -from parcels import AdvectionRK4, ParticleSet, StatusCode, Variable +from parcels import ParticleSet, StatusCode, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -291,7 +292,7 @@ def simulate(self, measurements, out_path) -> None: [ _argo_float_vertical_movement, *sampling_kernels, - AdvectionRK4, + AdvectionRK2, _keep_at_surface, _check_error, ], diff --git a/src/virtualship/instruments/drifter.py b/src/virtualship/instruments/drifter.py index 46ae82e9..46689e79 100644 --- a/src/virtualship/instruments/drifter.py +++ b/src/virtualship/instruments/drifter.py @@ -5,8 +5,9 @@ import numpy as np from parcels._core.statuscodes import StatusCode +from parcels.kernels import AdvectionRK2 -from parcels import AdvectionRK4, ParticleSet, Variable +from parcels import ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType @@ -176,7 +177,7 @@ def simulate(self, measurements, out_path) -> None: # execute simulation drifter_particleset.execute( - [AdvectionRK4, *sampling_kernels, _check_lifetime], + [AdvectionRK2, *sampling_kernels, _check_lifetime], endtime=endtime, dt=DT, output_file=out_file, From 573da9ed17b25b2b0327ff5f7f29f881ed88322a Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Wed, 20 May 2026 16:58:40 +0200 Subject: [PATCH 16/17] migrate fieldset ingestion protocol --- src/virtualship/instruments/base.py | 29 ++++++++++++++-------------- src/virtualship/models/expedition.py | 10 +--------- src/virtualship/utils.py | 21 +++++--------------- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/virtualship/instruments/base.py b/src/virtualship/instruments/base.py index d4e078e6..a3b7adb4 100644 --- a/src/virtualship/instruments/base.py +++ b/src/virtualship/instruments/base.py @@ -9,9 +9,9 @@ import copernicusmarine import xarray as xr -from parcels import FieldSet from yaspin import yaspin +import parcels from virtualship.errors import CopernicusCatalogueError from virtualship.utils import ( COPERNICUSMARINE_PHYS_VARIABLES, @@ -86,7 +86,7 @@ def __init__( self.min_lat, self.max_lat = min(wp_lats), max(wp_lats) self.min_lon, self.max_lon = min(wp_lons), max(wp_lons) - def load_input_data(self) -> FieldSet: + def load_input_data(self) -> parcels.FieldSet: """Load and return the input data as a FieldSet for the instrument.""" try: fieldset = self._generate_fieldset() @@ -97,7 +97,7 @@ def load_input_data(self) -> FieldSet: # interpolation methods for var in (v for v in self.variables if v not in ("U", "V")): - getattr(fieldset, var).interp_method = "linear_invdist_land_tracer" + getattr(fieldset, var).interp_method = parcels.interpolators.XLinear # depth negative for g in fieldset.gridset.grids: @@ -183,11 +183,11 @@ def _get_copernicus_ds( coordinates_selection_method="outside", ) - def _generate_fieldset(self) -> FieldSet: + def _generate_fieldset(self) -> parcels.FieldSet: """ Create and combine FieldSets for each variable, supporting both local and Copernicus Marine data sources. - Per variable avoids issues when using copernicusmarine and creating directly one FieldSet of ds's sourced from different Copernicus Marine product IDs, which is often the case for BGC variables. + N.B. Per variable avoids issues when using copernicusmarine and creating directly one FieldSet of ds's sourced from different Copernicus Marine product IDs (which can also have different temporal resolutions), which is often the case for BGC variables. """ fieldsets_list = [] keys = list(self.variables.keys()) @@ -217,12 +217,11 @@ def _generate_fieldset(self) -> FieldSet: [data_dir.joinpath(f) for f in files] ) # using: ds --> .from_xarray_dataset seems more robust than .from_netcdf for handling different temporal resolutions for different variables ... - fs = FieldSet.from_xarray_dataset( - ds, - variables={key: full_var_name}, - dimensions=self.dimensions, - mesh="spherical", - ) + # TODO: do docs on pre-downloading data need to be updated for these changes? Anything about conventions etc.? + fields = {key: ds[full_var_name]} + ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields) + fs = parcels.FieldSet.from_sgrid_conventions(ds_fset) + else: # stream via Copernicus Marine Service physical = var in COPERNICUSMARINE_PHYS_VARIABLES ds = self._get_copernicus_ds( @@ -230,9 +229,11 @@ def _generate_fieldset(self) -> FieldSet: physical=physical, var=var, ) - fs = FieldSet.from_xarray_dataset( - ds, {key: var}, self.dimensions, mesh="spherical" - ) + + fields = {key: ds[var]} + ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields) + fs = parcels.FieldSet.from_sgrid_conventions(ds_fset) + fieldsets_list.append(fs) base_fieldset = fieldsets_list[0] diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py index eef23c76..da084328 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -17,7 +17,6 @@ _calc_sail_time, _calc_wp_stationkeeping_time, _get_bathy_data, - _get_waypoint_latlons, _validate_numeric_to_timedelta, get_supported_sensors, register_instrument_config, @@ -131,14 +130,7 @@ def verify( land_waypoints = [] if not ignore_land_test: try: - wp_lats, wp_lons = _get_waypoint_latlons(self.waypoints) - bathymetry_field = _get_bathy_data( - min(wp_lats), - max(wp_lats), - min(wp_lons), - max(wp_lons), - from_data=from_data, - ).bathymetry + bathymetry_field = _get_bathy_data(from_data=from_data).bathymetry except Exception as e: raise ScheduleError( f"Problem loading bathymetry data (used to verify waypoints are in water) directly via copernicusmarine. \n\n original message: {e}" diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 3dab6b70..6ba2711e 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -16,6 +16,7 @@ import pyproj import xarray as xr +import parcels from parcels import FieldSet, Particle, Variable from virtualship.errors import CopernicusCatalogueError @@ -425,13 +426,7 @@ def _start_end_in_product_timerange( ) -def _get_bathy_data( - min_lat: float, - max_lat: float, - min_lon: float, - max_lon: float, - from_data: Path | None = None, -) -> FieldSet: +def _get_bathy_data(from_data: Path | None = None) -> FieldSet: """Bathymetry data from local or 'streamed' directly from Copernicus Marine.""" if from_data is not None: # load from local data var = "deptho" @@ -443,11 +438,6 @@ def _get_bathy_data( f"\n\n❗️ Could not find bathymetry variable '{var}' in data directory '{from_data}/bathymetry/'.\n\n❗️ Is the pre-downloaded data directory structure compliant with VirtualShip expectations?\n\n❗️ See the docs for more information on expectations: https://virtualship.readthedocs.io/en/latest/user-guide/index.html#documentation\n" ) from e ds_bathymetry = xr.open_dataset(bathy_dir.joinpath(filename)) - bathymetry_variables = {"bathymetry": "deptho"} - bathymetry_dimensions = {"lon": "longitude", "lat": "latitude"} - return FieldSet.from_xarray_dataset( - ds_bathymetry, bathymetry_variables, bathymetry_dimensions - ) else: # stream via Copernicus Marine Service ds_bathymetry = copernicusmarine.open_dataset( @@ -455,12 +445,11 @@ def _get_bathy_data( variables=["deptho"], coordinates_selection_method="outside", ) - bathymetry_variables = {"bathymetry": "deptho"} - bathymetry_dimensions = {"lon": "longitude", "lat": "latitude"} - return FieldSet.from_xarray_dataset( - ds_bathymetry, bathymetry_variables, bathymetry_dimensions + ds_fset = parcels.convert.copernicusmarine_to_sgrid( + fields={var: ds_bathymetry[var]} ) + return FieldSet.from_sgrid_conventions(ds_fset) def expedition_cost(schedule_results: ScheduleOk, time_past: timedelta) -> float: From d13a0369c4b7d7c7b26cf20063ec9552a4e6fb96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 15:02:29 +0000 Subject: [PATCH 17/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/virtualship/instruments/adcp.py | 2 +- src/virtualship/instruments/argo_float.py | 2 +- src/virtualship/instruments/base.py | 2 +- src/virtualship/instruments/ctd.py | 2 +- src/virtualship/instruments/drifter.py | 2 +- src/virtualship/instruments/ship_underwater_st.py | 2 +- src/virtualship/instruments/xbt.py | 2 +- src/virtualship/utils.py | 4 ++-- tests/test_utils.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/virtualship/instruments/adcp.py b/src/virtualship/instruments/adcp.py index 7ee718f6..626ca354 100644 --- a/src/virtualship/instruments/adcp.py +++ b/src/virtualship/instruments/adcp.py @@ -3,8 +3,8 @@ from typing import ClassVar import numpy as np - from parcels import ParticleSet + from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py index d1644849..4eae6350 100644 --- a/src/virtualship/instruments/argo_float.py +++ b/src/virtualship/instruments/argo_float.py @@ -4,9 +4,9 @@ from typing import ClassVar import numpy as np +from parcels import ParticleSet, StatusCode, Variable from parcels.kernels import AdvectionRK2 -from parcels import ParticleSet, StatusCode, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/instruments/base.py b/src/virtualship/instruments/base.py index a3b7adb4..f6afd3e0 100644 --- a/src/virtualship/instruments/base.py +++ b/src/virtualship/instruments/base.py @@ -8,10 +8,10 @@ from typing import TYPE_CHECKING, ClassVar import copernicusmarine +import parcels import xarray as xr from yaspin import yaspin -import parcels from virtualship.errors import CopernicusCatalogueError from virtualship.utils import ( COPERNICUSMARINE_PHYS_VARIABLES, diff --git a/src/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py index a7a4218f..d8f229c1 100644 --- a/src/virtualship/instruments/ctd.py +++ b/src/virtualship/instruments/ctd.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, ClassVar import numpy as np +from parcels import ParticleSet, Variable from parcels._core.statuscodes import StatusCode -from parcels import ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/instruments/drifter.py b/src/virtualship/instruments/drifter.py index 46689e79..79a1e34b 100644 --- a/src/virtualship/instruments/drifter.py +++ b/src/virtualship/instruments/drifter.py @@ -4,10 +4,10 @@ from typing import ClassVar import numpy as np +from parcels import ParticleSet, Variable from parcels._core.statuscodes import StatusCode from parcels.kernels import AdvectionRK2 -from parcels import ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/instruments/ship_underwater_st.py b/src/virtualship/instruments/ship_underwater_st.py index 78c757d2..d844b7a2 100644 --- a/src/virtualship/instruments/ship_underwater_st.py +++ b/src/virtualship/instruments/ship_underwater_st.py @@ -3,8 +3,8 @@ from typing import ClassVar import numpy as np - from parcels import ParticleSet + from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/instruments/xbt.py b/src/virtualship/instruments/xbt.py index 06e862a6..d02afb7a 100644 --- a/src/virtualship/instruments/xbt.py +++ b/src/virtualship/instruments/xbt.py @@ -4,9 +4,9 @@ from typing import ClassVar import numpy as np +from parcels import ParticleSet, Variable from parcels._core.statuscodes import StatusCode -from parcels import ParticleSet, Variable from virtualship.instruments.base import Instrument from virtualship.instruments.sensors import SensorType from virtualship.instruments.types import InstrumentType diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 6ba2711e..85b3ad75 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -13,11 +13,11 @@ import copernicusmarine import numpy as np +import parcels import pyproj import xarray as xr - -import parcels from parcels import FieldSet, Particle, Variable + from virtualship.errors import CopernicusCatalogueError if TYPE_CHECKING: diff --git a/tests/test_utils.py b/tests/test_utils.py index 4ab96654..2628793d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,9 +5,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