Skip to content

Commit ebf596e

Browse files
committed
fix: conductivity unit handling: verify in readers, normalise in pipeline
1 parent 9219904 commit ebf596e

24 files changed

Lines changed: 1453 additions & 32 deletions

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,9 @@ where = ["."]
103103
[tool.setuptools.package-data]
104104
"seasenselib.knowledge" = ["**/*.json"]
105105
"seasenselib.config.pipeline" = ["*.json"]
106+
107+
[tool.coverage.run]
108+
source = ["seasenselib"]
109+
110+
[tool.coverage.report]
111+
omit = ["tests/*"]

seasenselib/config/pipeline/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"name": "unit_handling",
1717
"config": {
1818
"handlers": [
19-
"normalize"
19+
"normalize",
20+
"conductivity_normalize"
2021
]
2122
}
2223
},

seasenselib/config/pipeline/full.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"config": {
1818
"handlers": [
1919
"normalize",
20+
"conductivity_normalize",
2021
"convert"
2122
]
2223
}

seasenselib/knowledge/pipeline/metadata_enrichment/parameters_metadata.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"measurement_type": "Measured",
1111
"short_name": "COND",
1212
"standard_name": "sea_water_electrical_conductivity",
13-
"units": "S m-1"
13+
"units": "mS cm-1"
1414
},
1515
"correlation": {
1616
"long_name": "Correlation",
@@ -89,13 +89,13 @@
8989
"long_name": "Potential Temperature \u03b8",
9090
"measurement_type": "Derived",
9191
"standard_name": "sea_water_potential_temperature",
92-
"units": "degC"
92+
"units": "degree_C"
9393
},
9494
"conservative_temperature": {
9595
"long_name": "Conservative Temperature",
9696
"measurement_type": "Derived",
9797
"standard_name": "sea_water_conservative_temperature",
98-
"units": "degC"
98+
"units": "degree_C"
9999
},
100100
"absolute_salinity": {
101101
"long_name": "Absolute Salinity",
@@ -133,12 +133,13 @@
133133
"units": "m/s"
134134
},
135135
"temperature": {
136+
"_comment": "Units are degree_C, assumed ITS-90 for all modern instruments. A variable named temperature_68 would indicate IPTS-68 scale and must be handled separately. See https://exchange-format.readthedocs.io/en/latest/parameters.html#ctdtmp",
136137
"coverage_content_type": "physicalMeasurement",
137138
"long_name": "Temperature",
138139
"measurement_type": "Measured",
139140
"short_name": "WT",
140141
"standard_name": "sea_water_temperature",
141-
"units": "ITS-90, deg C"
142+
"units": "degree_C"
142143
},
143144
"time": {
144145
"coverage_content_type": "coordinate",

seasenselib/knowledge/pipeline/unit_handling/expected_units.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"battery_voltage": "volts",
3-
"conductivity": "S m-1",
3+
"conductivity": "mS cm-1",
44
"correlation": "unitless",
55
"density": "kg m-3",
66
"depth": "meters",
@@ -13,11 +13,11 @@
1313
"magnitude": "m/s",
1414
"north_velocity": "m/s",
1515
"pitch": "degrees",
16-
"potential_temperature": "K",
16+
"potential_temperature": "degree_C",
1717
"power_supply_input": "V",
1818
"pressure": "dbar",
1919
"roll": "degrees",
2020
"speed_of_sound": "m/s",
21-
"temperature": "K",
21+
"temperature": "degree_C",
2222
"up_velocity": "m/s"
2323
}

seasenselib/knowledge/pipeline/unit_handling/unit_normalizations.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"degree": "degrees",
2323
"V": "volts",
2424
"volt": "volts",
25-
"Sverdrup/m": "Sv/m",
25+
"S/m": "S m-1",
26+
"mS/cm": "mS cm-1",
2627
"PetaWatts": "PW",
2728
"petawatts": "PW",
2829
"kg/m³": "kg m-3",

seasenselib/parameters.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@
4242

4343
# Meta data should use standardized values from https://cfconventions.org/
4444
metadata = {
45+
# Units are degree_C, assumed ITS-90 for all modern instruments.
46+
# A variable named temperature_68 indicates IPTS-68 scale and must be handled separately.
47+
# See https://exchange-format.readthedocs.io/en/latest/parameters.html#ctdtmp
4548
TEMPERATURE: {
4649
'long_name': "Temperature",
47-
'units': "ITS-90, deg C",
50+
'units': "degree_C",
4851
'coverage_content_type': 'physicalMeasurement',
4952
'standard_name': 'sea_water_temperature',
5053
'short_name': "WT",
@@ -61,7 +64,7 @@
6164
CONDUCTIVITY: {
6265
'long_name': "Conductivity",
6366
'coverage_content_type': 'physicalMeasurement',
64-
'units': "S m-1",
67+
'units': "mS cm-1",
6568
'standard_name': 'sea_water_electrical_conductivity',
6669
'short_name': "COND",
6770
'measurement_type': "Measured",
@@ -101,13 +104,13 @@
101104
},
102105
POTENTIAL_TEMPERATURE: {
103106
'long_name': 'Potential Temperature θ',
104-
'units': 'degC',
107+
'units': 'degree_C',
105108
'standard_name': 'sea_water_potential_temperature',
106109
'measurement_type': 'Derived',
107110
},
108111
CONSERVATIVE_TEMPERATURE: {
109112
'long_name': 'Conservative Temperature',
110-
'units': 'degC',
113+
'units': 'degree_C',
111114
'standard_name': 'sea_water_conservative_temperature',
112115
'measurement_type': 'Derived',
113116
},

seasenselib/pipeline/derivation/stage.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,33 @@ def configure(self, config: Dict[str, Any]) -> None:
136136
)
137137

138138
def process(self, context: StageContext) -> StageContext:
139+
self._warn_if_conductivity_in_sm(context.dataset)
139140
return self._derivation.process(context)
141+
142+
@staticmethod
143+
def _warn_if_conductivity_in_sm(ds) -> None:
144+
"""Warn if a conductivity variable arrives with S/m units.
145+
146+
The conductivity_normalize handler in unit_handling should have
147+
converted it to mS cm-1 before this stage runs. If it hasn't,
148+
any future salinity-from-conductivity derivation would produce
149+
values that are wrong by a factor of 10.
150+
"""
151+
import warnings
152+
import seasenselib.parameters as params
153+
_SM_UNITS = frozenset({"S/m", "S m-1"})
154+
cond_key = params.CONDUCTIVITY
155+
for var_name in ds.data_vars:
156+
if var_name == cond_key or (
157+
var_name.startswith(cond_key + "_") and var_name[len(cond_key) + 1:].isdigit()
158+
):
159+
units = ds[var_name].attrs.get("units", "")
160+
if units in _SM_UNITS:
161+
warnings.warn(
162+
f"Variable '{var_name}' has units '{units}' at the derivation stage. "
163+
"Conductivity should be in mS cm-1 before derivation. "
164+
"Check that the 'conductivity_normalize' handler is enabled "
165+
"in the unit_handling stage.",
166+
UserWarning,
167+
stacklevel=4,
168+
)

seasenselib/pipeline/unit_handling/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
from .unit_normalizer import UnitNormalizer
88
from .unit_converter import UnitConverter
9+
from .conductivity_normalizer import ConductivityNormalizer
910

1011
__all__ = [
1112
"UnitNormalizer",
1213
"UnitConverter",
14+
"ConductivityNormalizer",
1315
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Conductivity normalizer.
3+
4+
Converts conductivity variables to the canonical unit mS cm-1 before the
5+
derivation stage runs. Uses magnitude-based inference to detect and warn
6+
about declared-vs-actual mismatches.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import logging
12+
from typing import List, Tuple
13+
14+
import xarray as xr
15+
16+
from .utils import _base_name
17+
from seasenselib.readers.utils.conductivity_units import (
18+
infer_conductivity_unit,
19+
to_mS_cm,
20+
)
21+
22+
logger = logging.getLogger(__name__)
23+
24+
_TARGET_UNIT = "mS cm-1"
25+
26+
27+
class ConductivityNormalizer:
28+
"""Convert conductivity variables to mS cm-1."""
29+
30+
def normalize(self, ds: xr.Dataset) -> Tuple[xr.Dataset, List[str]]:
31+
"""Normalise all conductivity variables to mS cm-1.
32+
33+
Parameters
34+
----------
35+
ds : xr.Dataset
36+
37+
Returns
38+
-------
39+
tuple[xr.Dataset, list[str]]
40+
Updated dataset and list of normalisation records
41+
(``"var: old_unit -> mS cm-1"``).
42+
"""
43+
normalizations: List[str] = []
44+
45+
for var_name in list(ds.data_vars):
46+
if _base_name(var_name) != "conductivity":
47+
continue
48+
49+
current_units = ds[var_name].attrs.get("units", "")
50+
if not current_units:
51+
continue
52+
if current_units == _TARGET_UNIT:
53+
continue
54+
55+
values = ds[var_name].values
56+
try:
57+
inferred = infer_conductivity_unit(values, declared=current_units)
58+
converted, canonical = to_mS_cm(values, inferred)
59+
except ValueError as exc:
60+
logger.warning(
61+
"ConductivityNormalizer: cannot convert '%s' (units='%s'): %s",
62+
var_name,
63+
current_units,
64+
exc,
65+
)
66+
continue
67+
68+
saved_attrs = dict(ds[var_name].attrs)
69+
dims = ds[var_name].dims
70+
ds[var_name] = (dims, converted)
71+
ds[var_name].attrs.update(saved_attrs)
72+
ds[var_name].attrs["units"] = canonical
73+
ds[var_name].attrs["conductivity_normalised_from"] = current_units
74+
normalizations.append(f"{var_name}: {current_units} -> {canonical}")
75+
logger.info(
76+
"Normalised conductivity '%s': %s -> %s",
77+
var_name,
78+
current_units,
79+
canonical,
80+
)
81+
82+
return ds, normalizations

0 commit comments

Comments
 (0)