Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
ba585c2
remove CTD_BGC instrument type from InstrumentType enum, add SensorTy…
j-atkins Mar 25, 2026
1f7e9b8
update utils: add sensor def mapping and remove old references to ctd…
j-atkins Mar 25, 2026
dbaa319
refactor: update SensorType enum and add source-truth for supported s…
j-atkins Mar 26, 2026
a2a7c81
add sensors configuration for various instruments
j-atkins Mar 26, 2026
67a04d8
new registries and helper functions
j-atkins Mar 26, 2026
057d9f9
update expedition models, now including SensorConfig model and associ…
j-atkins Mar 26, 2026
b82118d
modify adcp instrument class, also abstract expansion to u and v to h…
j-atkins Mar 26, 2026
8d96d8f
dynamic particle class building takes JIT or Scipy particle
j-atkins Mar 26, 2026
818e8f8
raise error when instrument has zero sensors enabled
j-atkins Mar 26, 2026
07c8461
use centralised particle class builder for ADCP now as well
j-atkins Mar 26, 2026
1bf517e
batch update instrument subclasses adapted to refactored sensor logic
j-atkins Mar 26, 2026
961f1fe
rename list
j-atkins Mar 26, 2026
ee002d7
adapt argo subclass to sensor refactoring, also separate the sampling…
j-atkins Mar 26, 2026
4777217
consistent particle variable naming
j-atkins Mar 26, 2026
c419399
add back in ctd_bgc for now
j-atkins Mar 26, 2026
daabfc4
fix import
j-atkins Mar 26, 2026
cece7bb
move sensor information to new sensors.py file
j-atkins Mar 27, 2026
b429331
update imports across codebase
j-atkins Mar 27, 2026
882a419
add validator/serialiser for reading from YAML, remove unnecessary pr…
j-atkins Mar 27, 2026
126ecc2
re-add JITParticle to particle class when creating instruments
j-atkins Mar 27, 2026
9011b2d
remove CTD_BGC instrument type from InstrumentType enum, add SensorTy…
j-atkins Mar 25, 2026
464b3e9
update utils: add sensor def mapping and remove old references to ctd…
j-atkins Mar 25, 2026
2f7d82d
refactor: update SensorType enum and add source-truth for supported s…
j-atkins Mar 26, 2026
1d7c158
add sensors configuration for various instruments
j-atkins Mar 26, 2026
f01bf0e
new registries and helper functions
j-atkins Mar 26, 2026
d9f9d10
update expedition models, now including SensorConfig model and associ…
j-atkins Mar 26, 2026
c50c43f
modify adcp instrument class, also abstract expansion to u and v to h…
j-atkins Mar 26, 2026
bb91f0c
dynamic particle class building takes JIT or Scipy particle
j-atkins Mar 26, 2026
b584d70
raise error when instrument has zero sensors enabled
j-atkins Mar 26, 2026
6fb6284
use centralised particle class builder for ADCP now as well
j-atkins Mar 26, 2026
243fb0d
batch update instrument subclasses adapted to refactored sensor logic
j-atkins Mar 26, 2026
21f3b8b
rename list
j-atkins Mar 26, 2026
f6e17ac
adapt argo subclass to sensor refactoring, also separate the sampling…
j-atkins Mar 26, 2026
0962261
consistent particle variable naming
j-atkins Mar 26, 2026
c9d0623
add back in ctd_bgc for now
j-atkins Mar 26, 2026
8151a60
fix import
j-atkins Mar 26, 2026
892c75d
move sensor information to new sensors.py file
j-atkins Mar 27, 2026
13ded3f
update imports across codebase
j-atkins Mar 27, 2026
a30f72b
add validator/serialiser for reading from YAML, remove unnecessary pr…
j-atkins Mar 27, 2026
f0f8a19
re-add JITParticle to particle class when creating instruments
j-atkins Mar 27, 2026
f245288
Merge branch 'refactor-sensors' of github.com:OceanParcels/virtualshi…
j-atkins Apr 8, 2026
92fe4a1
update with new analysis environment
j-atkins Apr 8, 2026
f4c6205
Merge branch 'new-pixi-env' into refactor-sensors
j-atkins Apr 8, 2026
4351657
Merge branch 'main' into refactor-sensors
j-atkins Apr 9, 2026
089d2b6
fix erroneous sampling during descent and drift
j-atkins Apr 9, 2026
96ff22d
update docstring
j-atkins Apr 9, 2026
73474e9
Add sensor configuration tests for various instruments and update con…
j-atkins Apr 9, 2026
c5ec69b
new tests for instruments, focused on new sensor logic
j-atkins Apr 9, 2026
7e9c5e9
new test_sensors suite
j-atkins Apr 9, 2026
18c1a2d
new tests for new sensor logic utils
j-atkins Apr 9, 2026
89e184a
remove some overlap/duplication of tests
j-atkins Apr 9, 2026
bf61220
deal with circular import issues
j-atkins Apr 10, 2026
51f4457
add ctd_bgc stationkeeping logic back in (accidentally removed earlier)
j-atkins Apr 10, 2026
bcbc28f
Merge branch 'main' into refactor-sensors
j-atkins Apr 10, 2026
f9f17f2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 10, 2026
548297b
correct comment
j-atkins Apr 10, 2026
7a163f7
update docstrings, rethink some testing
j-atkins Apr 10, 2026
6978c95
Merge branch 'refactor-sensors' of github.com:OceanParcels/virtualshi…
j-atkins Apr 10, 2026
bbd1862
change naming to nonsensor rather than fixed
j-atkins Apr 16, 2026
97d9b14
Merge branch 'main' into refactor-sensors
j-atkins Apr 28, 2026
9a00820
refactor sensor support handling across instruments to use dynamic ma…
j-atkins Apr 28, 2026
c97c248
refactor to mixin class for sharing serialisation, validation methods…
j-atkins Apr 28, 2026
9cc09d2
fix tests for new notation
j-atkins Apr 28, 2026
c5d3b18
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 28, 2026
f24a6f1
change order of supported sensors in CTD_BGC sensors description
j-atkins Apr 28, 2026
9e40b6c
neaten up instrument classes mapping sensor types to their relevant s…
j-atkins Apr 28, 2026
27a9b4e
sensor_kernels as base class attribute
j-atkins Apr 28, 2026
08c2ef5
Merge branch 'refactor-sensors' of github.com:OceanParcels/virtualshi…
j-atkins Apr 29, 2026
b057c4c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 29, 2026
399d5f4
rename sensor class, give type_ attribute and move sensor classes and…
j-atkins Apr 29, 2026
5581c79
move towards explit lists of parcels.Variable's in sensor class
j-atkins Apr 29, 2026
a29d860
make particle_vars explicitly define parcels.Variables, change partic…
j-atkins Apr 29, 2026
c210815
fix wrong type checking
j-atkins Apr 29, 2026
f6b0551
tidy up type annotation
j-atkins Apr 29, 2026
3b846e2
Merge branch 'refactor-sensors' of github.com:OceanParcels/virtualshi…
j-atkins Apr 29, 2026
e9e15db
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ 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" }
typing = { features = ["typing"], solve-group = "typing" }
pre-commit = { features = ["pre-commit"], no-default-feature = true }
39 changes: 26 additions & 13 deletions src/virtualship/instruments/adcp.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import ClassVar

import numpy as np
from parcels import ParticleSet, ScipyParticle, Variable
from parcels import ParticleSet, ScipyParticle

from virtualship.instruments.base import Instrument
from virtualship.instruments.sensors import SensorType
from virtualship.instruments.types import InstrumentType
from virtualship.utils import (
register_instrument,
)
from virtualship.utils import build_particle_class_from_sensors, register_instrument

# =====================================================
# SECTION: Dataclass
Expand All @@ -23,16 +23,12 @@ class ADCP:


# =====================================================
# SECTION: Particle Class
# SECTION: non-sensor Particle Variables (non-sampling)
# =====================================================

# ADCP has no non-sensor variables, only sensor variables.
_ADCP_NONSENSOR_VARIABLES: list = []

_ADCPParticle = ScipyParticle.add_variables(
[
Variable("U", dtype=np.float32, initial=np.nan),
Variable("V", dtype=np.float32, initial=np.nan),
]
)

# =====================================================
# SECTION: Kernels
Expand All @@ -54,9 +50,13 @@ def _sample_velocity(particle, fieldset, time):
class ADCPInstrument(Instrument):
"""ADCP instrument class."""

sensor_kernels: ClassVar[dict[SensorType, Callable]] = {
SensorType.VELOCITY: _sample_velocity,
}

def __init__(self, expedition, from_data):
"""Initialize ADCPInstrument."""
variables = {"U": "uo", "V": "vo"}
variables = expedition.instruments_config.adcp_config.active_variables()
limit_spec = {
"spatial": True
} # spatial limits; lat/lon constrained to waypoint locations + buffer
Expand Down Expand Up @@ -93,6 +93,12 @@ def simulate(self, measurements, out_path) -> None:

fieldset = self.load_input_data()

# 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
)

bins = np.linspace(MAX_DEPTH, MIN_DEPTH, NUM_BINS)
num_particles = len(bins)
particleset = ParticleSet.from_list(
Expand All @@ -108,6 +114,13 @@ def simulate(self, measurements, out_path) -> None:

out_file = particleset.ParticleFile(name=out_path, outputdt=np.inf)

# build kernel list from active sensors only
sampling_kernels = [
self.sensor_kernels[sc.sensor_type]
for sc in adcp_config.sensors
if sc.enabled and sc.sensor_type in self.sensor_kernels
]

for point in measurements:
particleset.lon_nextloop[:] = point.location.lon
particleset.lat_nextloop[:] = point.location.lat
Expand All @@ -116,7 +129,7 @@ def simulate(self, measurements, out_path) -> None:
)

particleset.execute(
[_sample_velocity],
sampling_kernels,
dt=1,
runtime=1,
verbose_progress=self.verbose_progress,
Expand Down
101 changes: 64 additions & 37 deletions src/virtualship/instruments/argo_float.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
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, JITParticle, ParticleSet, StatusCode, 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 register_instrument
from virtualship.utils import build_particle_class_from_sensors, register_instrument

# =====================================================
# SECTION: Dataclass
Expand All @@ -37,25 +33,21 @@ class ArgoFloat:


# =====================================================
# SECTION: Particle Class
# SECTION: non-sensor Particle Variables (non-sampling)
# =====================================================

_ArgoParticle = JITParticle.add_variables(
[
Variable("cycle_phase", dtype=np.int32, initial=0.0),
Variable("cycle_age", dtype=np.float32, initial=0.0),
Variable("drift_age", dtype=np.float32, initial=0.0),
Variable("salinity", dtype=np.float32, initial=np.nan),
Variable("temperature", dtype=np.float32, initial=np.nan),
Variable("min_depth", dtype=np.float32),
Variable("max_depth", dtype=np.float32),
Variable("drift_depth", dtype=np.float32),
Variable("vertical_speed", dtype=np.float32),
Variable("cycle_days", dtype=np.int32),
Variable("drift_days", dtype=np.int32),
Variable("grounded", dtype=np.int32, initial=0),
]
)
_ARGO_NONSENSOR_VARIABLES = [
Variable("cycle_phase", dtype=np.int32, initial=0.0),
Variable("cycle_age", dtype=np.float32, initial=0.0),
Variable("drift_age", dtype=np.float32, initial=0.0),
Variable("min_depth", dtype=np.float32),
Variable("max_depth", dtype=np.float32),
Variable("drift_depth", dtype=np.float32),
Variable("vertical_speed", dtype=np.float32),
Variable("cycle_days", dtype=np.int32),
Variable("drift_days", dtype=np.int32),
Variable("grounded", dtype=np.int32, initial=0),
]

# =====================================================
# SECTION: Kernels
Expand Down Expand Up @@ -118,18 +110,7 @@ def _argo_float_vertical_movement(particle, fieldset, time):
particle.grounded = 0
if particle.depth + particle_ddepth >= particle.min_depth:
particle_ddepth = particle.min_depth - particle.depth
particle.temperature = (
math.nan
) # reset temperature to NaN at end of sampling cycle
particle.salinity = math.nan # idem
particle.cycle_phase = 4
else:
particle.temperature = fieldset.T[
time, particle.depth, particle.lat, particle.lon
]
particle.salinity = fieldset.S[
time, particle.depth, particle.lat, particle.lon
]

elif particle.cycle_phase == 4:
# Phase 4: Transmitting at surface until cycletime is reached
Expand All @@ -153,6 +134,24 @@ def _check_error(particle, fieldset, time):
particle.delete()


def _argo_sample_temperature(particle, fieldset, time):
# 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 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


# =====================================================
# SECTION: Instrument Class
# =====================================================
Expand All @@ -162,9 +161,21 @@ def _check_error(particle, fieldset, time):
class ArgoFloatInstrument(Instrument):
"""ArgoFloat instrument class."""

sensor_kernels: ClassVar[dict[SensorType, Callable]] = {
SensorType.TEMPERATURE: _argo_sample_temperature,
SensorType.SALINITY: _argo_sample_salinity,
}

def __init__(self, expedition, from_data):
"""Initialize ArgoFloatInstrument."""
variables = {"U": "uo", "V": "vo", "S": "so", "T": "thetao"}
sensor_variables = (
expedition.instruments_config.argo_float_config.active_variables()
)
variables = {
"U": "uo",
"V": "vo",
**sensor_variables,
} # advection variables (U and V) are always required for argo float simulation; sensor variables come from config
spacetime_buffer_size = {
"latlon": 3.0, # [degrees]
"time": expedition.instruments_config.argo_float_config.lifetime.total_seconds()
Expand Down Expand Up @@ -215,6 +226,14 @@ def simulate(self, measurements, out_path) -> None:
f"{self.__class__.__name__} cannot be deployed in waters shallower than 50m. The following waypoints are too shallow: {shallow_waypoints}."
)

# 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,
)

# define parcel particles
argo_float_particleset = ParticleSet(
fieldset=fieldset,
Expand All @@ -241,10 +260,18 @@ def simulate(self, measurements, out_path) -> None:
# endtime
endtime = fieldset.time_origin.fulltime(fieldset.U.grid.time_full[-1])

# build kernel list from active sensors only
sampling_kernels = [
self.sensor_kernels[sc.sensor_type]
for sc in argo_float_config.sensors
if sc.enabled and sc.sensor_type in self.sensor_kernels
]

# execute simulation
argo_float_particleset.execute(
[
_argo_float_vertical_movement,
*sampling_kernels,
AdvectionRK4,
_keep_at_surface,
_check_error,
Expand Down
18 changes: 15 additions & 3 deletions src/virtualship/instruments/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import annotations

import abc
from collections import OrderedDict
import collections
from datetime import timedelta
from itertools import pairwise
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar

import copernicusmarine
import xarray as xr
Expand All @@ -24,12 +24,24 @@
)

if TYPE_CHECKING:
from virtualship.instruments.sensors import SensorType
from virtualship.models import Expedition


class Instrument(abc.ABC):
"""Base class for instruments and their simulation."""

# all instruments have sensor_kernels dict, mapping SensorType to sampling kernel
sensor_kernels: ClassVar[dict[SensorType, collections.abc.Callable]]

def __init_subclass__(cls, **kwargs: object) -> None:
"""Ensure subclasses define sensor_kernels as class attribute."""
super().__init_subclass__(**kwargs)
if "sensor_kernels" not in cls.__dict__:
raise TypeError(
f"Instrument subclass '{cls.__name__}' must define 'sensor_kernels' as a class attribute."
)

def __init__(
self,
expedition: Expedition,
Expand All @@ -45,7 +57,7 @@ def __init__(
self.expedition = expedition
self.from_data = from_data

self.variables = OrderedDict(variables)
self.variables = collections.OrderedDict(variables)
self.dimensions = {
"lon": "longitude",
"lat": "latitude",
Expand Down
Loading
Loading