Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
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
Expand Up @@ -2,13 +2,12 @@
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 +22,12 @@ class ADCP:


# =====================================================
# SECTION: Particle Class
# SECTION: fixed/mechanical Particle Variables (non-sampling)
# =====================================================

# ADCP has no fixed/mechanical variables, only sensor variables.
_ADCP_FIXED_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 @@ -45,6 +40,11 @@ def _sample_velocity(particle, fieldset, time):
)


_ADCP_SENSOR_KERNELS: dict[SensorType, callable] = {
SensorType.VELOCITY: _sample_velocity,
}


# =====================================================
# SECTION: Instrument Class
# =====================================================
Expand All @@ -56,7 +56,7 @@ class ADCPInstrument(Instrument):

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_FIXED_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 = [
_ADCP_SENSOR_KERNELS[sc.sensor_type]
for sc in adcp_config.sensors
if sc.enabled and sc.sensor_type in _ADCP_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
Expand Up @@ -4,18 +4,13 @@
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 +32,21 @@ class ArgoFloat:


# =====================================================
# SECTION: Particle Class
# SECTION: fixed/mechanical 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_FIXED_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 +109,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 +133,30 @@ 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


_ARGO_SENSOR_KERNELS: dict[SensorType, callable] = {
SensorType.TEMPERATURE: _argo_sample_temperature,
SensorType.SALINITY: _argo_sample_salinity,
}


# =====================================================
# SECTION: Instrument Class
# =====================================================
Expand All @@ -164,7 +168,14 @@ class ArgoFloatInstrument(Instrument):

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_FIXED_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 = [
_ARGO_SENSOR_KERNELS[sc.sensor_type]
for sc in argo_float_config.sensors
if sc.enabled and sc.sensor_type in _ARGO_SENSOR_KERNELS
]

# execute simulation
argo_float_particleset.execute(
[
_argo_float_vertical_movement,
*sampling_kernels,
AdvectionRK4,
_keep_at_surface,
_check_error,
Expand Down
59 changes: 41 additions & 18 deletions src/virtualship/instruments/ctd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
from parcels import JITParticle, ParticleSet, Variable

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

if TYPE_CHECKING:
from virtualship.models.spacetime import Spacetime
from virtualship.utils import add_dummy_UV, register_instrument

# =====================================================
# SECTION: Dataclass
Expand All @@ -28,19 +33,15 @@ class CTD:


# =====================================================
# SECTION: Particle Class
# SECTION: fixed/mechanical Particle Variables (non-sampling)
# =====================================================

_CTDParticle = JITParticle.add_variables(
[
Variable("salinity", dtype=np.float32, initial=np.nan),
Variable("temperature", dtype=np.float32, initial=np.nan),
Variable("raising", dtype=np.int8, initial=0.0), # bool. 0 is False, 1 is True.
Variable("max_depth", dtype=np.float32),
Variable("min_depth", dtype=np.float32),
Variable("winch_speed", dtype=np.float32),
]
)
_CTD_FIXED_VARIABLES = [
Variable("raising", dtype=np.int8, initial=0.0), # bool. 0 is False, 1 is True.
Variable("max_depth", dtype=np.float32),
Variable("min_depth", dtype=np.float32),
Variable("winch_speed", dtype=np.float32),
]


# =====================================================
Expand Down Expand Up @@ -70,6 +71,12 @@ def _ctd_cast(particle, fieldset, time):
particle.delete()


_CTD_SENSOR_KERNELS: dict[SensorType, callable] = {
SensorType.TEMPERATURE: _sample_temperature,
SensorType.SALINITY: _sample_salinity,
}


# =====================================================
# SECTION: Instrument Class
# =====================================================
Expand All @@ -81,7 +88,7 @@ class CTDInstrument(Instrument):

def __init__(self, expedition, from_data):
"""Initialize CTDInstrument."""
variables = {"S": "so", "T": "thetao"}
variables = expedition.instruments_config.ctd_config.active_variables()
limit_spec = {
"spatial": True
} # spatial limits; lat/lon constrained to waypoint locations + buffer
Expand Down Expand Up @@ -115,11 +122,14 @@ def simulate(self, measurements, out_path) -> None:
# add dummy U
add_dummy_UV(fieldset) # TODO: parcels v3 bodge; remove when parcels v4 is used

fieldset_starttime = fieldset.T.grid.time_origin.fulltime(
fieldset.T.grid.time_full[0]
# use first active field for time reference
_time_ref_key = next(iter(self.variables))
_time_ref_field = getattr(fieldset, _time_ref_key)
fieldset_starttime = _time_ref_field.grid.time_origin.fulltime(
_time_ref_field.grid.time_full[0]
)
fieldset_endtime = fieldset.T.grid.time_origin.fulltime(
fieldset.T.grid.time_full[-1]
fieldset_endtime = _time_ref_field.grid.time_origin.fulltime(
_time_ref_field.grid.time_full[-1]
)

# deploy time for all ctds should be later than fieldset start time
Expand Down Expand Up @@ -152,6 +162,12 @@ def simulate(self, measurements, out_path) -> None:
f"CTD max_depth or bathymetry shallower than maximum {-DT * WINCH_SPEED}"
)

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

# define parcel particles
ctd_particleset = ParticleSet(
fieldset=fieldset,
Expand All @@ -168,9 +184,16 @@ def simulate(self, measurements, out_path) -> None:
# define output file for the simulation
out_file = ctd_particleset.ParticleFile(name=out_path, outputdt=OUTPUT_DT)

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

# execute simulation
ctd_particleset.execute(
[_sample_salinity, _sample_temperature, _ctd_cast],
[*sampling_kernels, _ctd_cast],
endtime=fieldset_endtime,
dt=DT,
verbose_progress=self.verbose_progress,
Expand Down
Loading
Loading