|
4 | 4 | import numpy as np |
5 | 5 | import pytest |
6 | 6 | import xarray as xr |
7 | | -from parcels import FieldSet |
8 | 7 |
|
9 | 8 | import virtualship.utils |
| 9 | +from parcels import FieldSet, JITParticle, ScipyParticle, Variable |
| 10 | +from virtualship.instruments.sensors import SensorType |
10 | 11 | from virtualship.instruments.types import InstrumentType |
11 | | -from virtualship.models.expedition import Expedition |
| 12 | +from virtualship.models.expedition import Expedition, SensorConfig |
12 | 13 | from virtualship.models.location import Location |
13 | 14 | from virtualship.utils import ( |
14 | 15 | PROJECTION, |
| 16 | + SENSOR_REGISTRY, |
15 | 17 | _calc_sail_time, |
16 | 18 | _calc_wp_stationkeeping_time, |
17 | 19 | _find_nc_file_with_variable, |
18 | 20 | _get_bathy_data, |
19 | 21 | _select_product_id, |
20 | 22 | _start_end_in_product_timerange, |
21 | 23 | add_dummy_UV, |
| 24 | + build_particle_class_from_sensors, |
22 | 25 | get_example_expedition, |
23 | 26 | ) |
24 | 27 |
|
@@ -360,3 +363,96 @@ def test_calc_wp_stationkeeping_time_no_instruments(expedition): |
360 | 363 |
|
361 | 364 | assert stationkeeping_null == stationkeeping_emptylist # are equivalent |
362 | 365 | assert stationkeeping_null == datetime.timedelta(0) # at least one is 0 time |
| 366 | + |
| 367 | + |
| 368 | +def test_sensor_registry_every_sensor_type_has_entry(): |
| 369 | + """Every SensorType must be present as a key in SENSOR_REGISTRY.""" |
| 370 | + for sensor in SensorType: |
| 371 | + assert sensor in SENSOR_REGISTRY, f"{sensor} missing from SENSOR_REGISTRY" |
| 372 | + |
| 373 | + |
| 374 | +def test_sensor_registry_no_extra_keys(): |
| 375 | + """SENSOR_REGISTRY should not contain keys outside SensorType.""" |
| 376 | + for key in SENSOR_REGISTRY: |
| 377 | + assert isinstance(key, SensorType) |
| 378 | + |
| 379 | + |
| 380 | +@pytest.mark.parametrize( |
| 381 | + "sensor_type", |
| 382 | + [ |
| 383 | + SensorType.OXYGEN, |
| 384 | + SensorType.CHLOROPHYLL, |
| 385 | + SensorType.NITRATE, |
| 386 | + SensorType.PHOSPHATE, |
| 387 | + SensorType.PH, |
| 388 | + SensorType.PHYTOPLANKTON, |
| 389 | + SensorType.PRIMARY_PRODUCTION, |
| 390 | + ], |
| 391 | +) |
| 392 | +def test_sensor_registry_bgc_entries_category(sensor_type): |
| 393 | + """All BGC sensors must have category 'bgc'.""" |
| 394 | + assert SENSOR_REGISTRY[sensor_type].category == "bgc" |
| 395 | + |
| 396 | + |
| 397 | +def test_sensor_registry_unique_fs_keys(): |
| 398 | + """No two sensors should share an fs_key.""" |
| 399 | + fs_keys = [meta.fs_key for meta in SENSOR_REGISTRY.values()] |
| 400 | + assert len(fs_keys) == len(set(fs_keys)), ( |
| 401 | + "Duplicate fs_key found in SENSOR_REGISTRY" |
| 402 | + ) |
| 403 | + |
| 404 | + |
| 405 | +# helper |
| 406 | +def _make_sensors(*sensor_types, enabled=True): |
| 407 | + """Helper to build a list of SensorConfig from SensorType values.""" |
| 408 | + return [SensorConfig(sensor_type=st, enabled=enabled) for st in sensor_types] |
| 409 | + |
| 410 | + |
| 411 | +def test_build_basic_particle_class(): |
| 412 | + """Build basic particle class with T+S sensors and fixed variables.""" |
| 413 | + fixed = [Variable("cycle_phase", dtype=np.int32, initial=0)] |
| 414 | + sensors = _make_sensors(SensorType.TEMPERATURE, SensorType.SALINITY) |
| 415 | + |
| 416 | + ParticleClass = build_particle_class_from_sensors(sensors, fixed, JITParticle) |
| 417 | + assert issubclass(ParticleClass, JITParticle) |
| 418 | + |
| 419 | + |
| 420 | +def test_build_particle_class_disabled_sensors_excluded(): |
| 421 | + """Disabled sensors should not contribute variables.""" |
| 422 | + fixed = [] |
| 423 | + sensors = [ |
| 424 | + SensorConfig(sensor_type=SensorType.TEMPERATURE, enabled=True), |
| 425 | + SensorConfig(sensor_type=SensorType.SALINITY, enabled=False), |
| 426 | + ] |
| 427 | + |
| 428 | + ParticleClass = build_particle_class_from_sensors(sensors, fixed, JITParticle) |
| 429 | + assert hasattr(ParticleClass, "temperature") |
| 430 | + assert not hasattr(ParticleClass, "salinity") |
| 431 | + |
| 432 | + |
| 433 | +def test_build_particle_class_empty_sensors(): |
| 434 | + """With no sensors, build_particle_class_from_sensors returns a class with only fixed variables.""" |
| 435 | + fixed = [Variable("raising", dtype=np.int8, initial=0)] |
| 436 | + sensors = [] |
| 437 | + |
| 438 | + ParticleClass = build_particle_class_from_sensors(sensors, fixed, JITParticle) |
| 439 | + assert hasattr(ParticleClass, "raising") |
| 440 | + |
| 441 | + |
| 442 | +def test_build_particle_class_velocity_adds_U_V(): |
| 443 | + """VELOCITY sensor should add both U and V particle variables.""" |
| 444 | + fixed = [] |
| 445 | + sensors = _make_sensors(SensorType.VELOCITY) |
| 446 | + |
| 447 | + ParticleClass = build_particle_class_from_sensors(sensors, fixed, JITParticle) |
| 448 | + assert hasattr(ParticleClass, "U") |
| 449 | + assert hasattr(ParticleClass, "V") |
| 450 | + |
| 451 | + |
| 452 | +def test_build_particle_class_scipy_base(): |
| 453 | + """Should also work with ScipyParticle as the base class.""" |
| 454 | + fixed = [] |
| 455 | + sensors = _make_sensors(SensorType.TEMPERATURE) |
| 456 | + |
| 457 | + ParticleClass = build_particle_class_from_sensors(sensors, fixed, ScipyParticle) |
| 458 | + assert issubclass(ParticleClass, ScipyParticle) |
0 commit comments