Skip to content

Commit b879b9f

Browse files
EliEli
authored andcommitted
Formatting, unit test paths and pyproj.toml around new units implementation.
1 parent c54f899 commit b879b9f

File tree

4 files changed

+54
-27
lines changed

4 files changed

+54
-27
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ dependencies = [
3030
"matplotlib",
3131
"scikit-learn",
3232
"scipy",
33-
"statsmodels>=0.13"
33+
"statsmodels>=0.13",
34+
"plib"
3435
]
3536

3637
[project.optional-dependencies]

tests/pytest.ini

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11

22
[pytest]
33
markers =
4-
webtest: mark a test as a webtest.
4+
webtest: mark a test as a webtest.
5+
6+
testpaths = tests
7+
python_files = test_*.py
8+
python_classes = Test*
9+
python_functions = test_*
10+

tests/test_unit_conversions.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ def test_dataframe_conversion(sample_series_df):
6262
assert out.index.equals(df.index)
6363
assert (out.columns == df.columns).all()
6464
np.testing.assert_allclose(out.values, df.values * uc.CFS2CMS, atol=1e-12)
65+
66+
6567
# ----------------------------------------------------------------import importlib.util
6668

69+
6770
@pytest.mark.skipif(
68-
importlib.util.find_spec("cf_units") is None,
69-
reason="cf_units not installed")
71+
importlib.util.find_spec("cf_units") is None, reason="cf_units not installed"
72+
)
7073
def test_optional_cf_units_backend(monkeypatch):
7174
x = np.array([0.0, 1.0, 3.0])
7275
monkeypatch.setenv("VTOOLS_UNITS_BACKEND", "cf_units")
@@ -75,8 +78,6 @@ def test_optional_cf_units_backend(monkeypatch):
7578
monkeypatch.delenv("VTOOLS_UNITS_BACKEND", raising=False)
7679

7780

78-
79-
8081
# Aliases, backend consistency
8182
# -----------------------------------------------------------------------------
8283
@pytest.mark.parametrize("alias", ["cfs", "ft3/s", "ft^3/s"])
@@ -126,11 +127,11 @@ def test_backend_consistency_with_constants():
126127
)
127128

128129

129-
#@pytest.mark.skipif(
130+
# @pytest.mark.skipif(
130131
# not pytest.importorskip("cf_units", reason="cf_units not installed"),
131132
# reason="cf_units not available",
132-
#)
133-
#def test_optional_cf_units_backend(monkeypatch):
133+
# )
134+
# def test_optional_cf_units_backend(monkeypatch):
134135
# x = np.array([0.0, 1.0, 3.0])
135136
# monkeypatch.setenv("VTOOLS_UNITS_BACKEND", "cf_units")
136137
# out = uc.convert_units(x, "m", "ft")

vtools/functions/unit_conversions.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- a general-purpose unit conversion function `convert_units()` that uses Pint
1111
by default (with an optional cf_units backend via an environment variable),
1212
and that has fast paths for the above common conversions.
13-
13+
1414
Notes
1515
-----
1616
- PSU is treated here as a practical “unit” for salinity in workflows,
@@ -35,7 +35,6 @@
3535
from scipy.optimize import brentq
3636

3737

38-
3938
# -----------------------------------------------------------------------------
4039
# Constants for conversions (kept from legacy implementation)
4140
# -----------------------------------------------------------------------------
@@ -52,15 +51,15 @@
5251
K6 = 2.5842
5352
k = 0.0162
5453

55-
s_sea = 35.0 # representative ocean salinity, PSU
56-
ec_sea = 53087.0 # EC (μS/cm) associated with s_sea at 25 °C
54+
s_sea = 35.0 # representative ocean salinity, PSU
55+
ec_sea = 53087.0 # EC (μS/cm) associated with s_sea at 25 °C
5756

5857
# exact base
5958
FT2M = 0.3048 # exact by definition
6059

6160
# derive reciprocals/products to avoid mismatch and rounding drift
62-
M2FT = 1.0 / FT2M
63-
CFS2CMS = FT2M ** 3 # (ft^3/s) → (m^3/s)
61+
M2FT = 1.0 / FT2M
62+
CFS2CMS = FT2M**3 # (ft^3/s) → (m^3/s)
6463
CMS2CFS = 1.0 / CFS2CMS
6564

6665

@@ -76,13 +75,11 @@
7675
"cms": "m^3 s-1",
7776
"m3/s": "m^3 s-1",
7877
"m^3/s": "m^3 s-1",
79-
8078
# temperature (return case-sensitive names Pint expects)
8179
"deg f": "degF",
8280
"degree_fahrenheit": "degF",
8381
"deg c": "degC",
8482
"degree_celsius": "degC",
85-
8683
# conductivity spellings
8784
"us/cm": "uS cm-1",
8885
"μs/cm": "uS cm-1",
@@ -92,6 +89,7 @@
9289
"micromhos/cm@25c": "uS cm-1",
9390
}
9491

92+
9593
def _norm(u: str) -> str:
9694
"""Normalize common shorthands to canonical spellings without
9795
destroying case needed by Pint (e.g., degC/degF)."""
@@ -102,52 +100,66 @@ def _norm(u: str) -> str:
102100
k = s.lower()
103101
return _ALIASES.get(k, s)
104102

103+
105104
def _rewrap_like(values, arr):
106105
if isinstance(values, pd.DataFrame):
107106
return pd.DataFrame(arr, index=values.index, columns=values.columns)
108107
if isinstance(values, pd.Series):
109108
return pd.Series(arr, index=values.index, name=values.name)
110109
return arr
111110

111+
112112
# ---- Optional backend flip via env var (no public API) -----------------------
113113
def _want_cf_units() -> bool:
114114
return os.environ.get("VTOOLS_UNITS_BACKEND", "").lower() == "cf_units"
115115

116+
116117
@functools.lru_cache(maxsize=128)
117118
def _get_converter(iu: str, ou: str):
118119
"""Return a callable(arr)->arr using Pint by default; cf_units if env-forced."""
119120
if _want_cf_units():
120121
try:
121122
from cf_units import Unit
123+
122124
u_in, u_out = Unit(iu), Unit(ou)
123-
def conv(arr): return u_in.convert(np.asarray(arr), u_out)
125+
126+
def conv(arr):
127+
return u_in.convert(np.asarray(arr), u_out)
128+
124129
return conv
125130
except Exception:
126131
# fall through to Pint if cf_units not available
127132
pass
128133

129134
import pint
135+
130136
ureg = pint.UnitRegistry(autoconvert_offset_to_baseunit=True)
131137

132138
q_in, q_out = 1.0 * ureg(iu), 1.0 * ureg(ou)
133139
# --- Skip fast scaling for temperature units (affine with offsets) ---
134140
OFFSET_UNITS = {"degC", "degF", "degree_Celsius", "degree_Fahrenheit", "degR"}
135141
if iu in OFFSET_UNITS or ou in OFFSET_UNITS:
142+
136143
def conv(arr):
137144
a = np.asarray(arr)
138145
return (a * ureg(iu)).to(ureg(ou)).m
146+
139147
return conv
140148

141149
# --- Otherwise use fast pure-scale path ---
142150
try:
143151
factor = q_in.to(q_out).magnitude
152+
144153
def conv(arr):
145154
return np.asarray(arr) * factor
155+
146156
return conv
147157
except Exception:
158+
148159
def conv(arr):
149160
a = np.asarray(arr)
150161
return (a * ureg(iu)).to(ureg(ou)).m
162+
151163
return conv
152164

153165

@@ -177,13 +189,12 @@ def convert_units(values, in_unit: str, out_unit: str):
177189
# --- Custom domain-first paths -------------------------------------------
178190
# Temperature
179191
if iu == "degf" and ou == "degc":
180-
arr = (np.asarray(values) - 32.0) * (5.0/9.0)
192+
arr = (np.asarray(values) - 32.0) * (5.0 / 9.0)
181193
return _rewrap_like(values, arr)
182194
if iu == "degc" and ou == "degf":
183195
arr = np.asarray(values) * 1.8 + 32.0
184196
return _rewrap_like(values, arr)
185197

186-
187198
# Length / Flow shorthands (scale only)
188199
if iu == "ft" and ou == "m":
189200
return _rewrap_like(values, np.asarray(values) * FT2M)
@@ -196,7 +207,7 @@ def convert_units(values, in_unit: str, out_unit: str):
196207

197208
# EC ↔ PSU at 25C (never hand 'psu' to a generic backend)
198209
if iu in ("ec", "us/cm", "uS cm-1", "micromhos/cm") and ou == "psu":
199-
return ec_psu_25c(values, hill_correction=True) # uses your existing impl
210+
return ec_psu_25c(values, hill_correction=True) # uses your existing impl
200211
if iu == "psu" and ou in ("ec", "us/cm", "uS cm-1", "micromhos/cm"):
201212
out = psu_ec_25c(values, refine=True, hill_correction=True)
202213
return out
@@ -208,8 +219,6 @@ def convert_units(values, in_unit: str, out_unit: str):
208219
return _rewrap_like(values, out)
209220

210221

211-
212-
213222
# -----------------------------------------------------------------------------
214223
# Linear / affine engineering conversions (functional)
215224
# -----------------------------------------------------------------------------
@@ -425,7 +434,11 @@ def ec_psu_25c(ec, hill_correction=True):
425434
a_0 = 0.008
426435
f_ = (25.0 - 15.0) / (1.0 + k * (25.0 - 15.0)) # f(T=25)
427436
b_0_f = 0.0005 * f_
428-
s = s - a_0 / (1.0 + 1.5 * x + x * x) - b_0_f / (1.0 + np.sqrt(y) + y + y * np.sqrt(y))
437+
s = (
438+
s
439+
- a_0 / (1.0 + 1.5 * x + x * x)
440+
- b_0_f / (1.0 + np.sqrt(y) + y + y * np.sqrt(y))
441+
)
429442

430443
if np.isscalar(ec):
431444
return float(s) if not np.isnan(s) else s
@@ -473,19 +486,25 @@ def psu_ec_25c_scalar(psu, refine=True, hill_correction=True):
473486
return np.nan
474487

475488
if hill_correction and not refine:
476-
raise ValueError("Unrefined (refine=False) psu-to-ec correction cannot have hill_correction")
489+
raise ValueError(
490+
"Unrefined (refine=False) psu-to-ec correction cannot have hill_correction"
491+
)
477492

478493
if refine:
479494
if psu > 34.99969:
480495
raise ValueError(f"psu is over sea salinity: {psu}")
481496
ec = brentq(psu_ec_resid, 1.0, ec_sea, args=(psu, hill_correction))
482497
else:
483498
sqrtpsu = np.sqrt(psu)
484-
ec = (psu / s_sea) * ec_sea + psu * (psu - s_sea) * (J1 + J2 * sqrtpsu + J3 * psu + J4 * sqrtpsu * psu)
499+
ec = (psu / s_sea) * ec_sea + psu * (psu - s_sea) * (
500+
J1 + J2 * sqrtpsu + J3 * psu + J4 * sqrtpsu * psu
501+
)
485502
return ec
486503

487504

488-
psu_ec_25c_vec = np.vectorize(psu_ec_25c_scalar, otypes="d", excluded=["refine", "hill_correction"])
505+
psu_ec_25c_vec = np.vectorize(
506+
psu_ec_25c_scalar, otypes="d", excluded=["refine", "hill_correction"]
507+
)
489508

490509

491510
def psu_ec_25c(psu, refine=True, hill_correction=True):

0 commit comments

Comments
 (0)