Skip to content

Commit c54f899

Browse files
EliEli
authored andcommitted
Refactor unit_conversions around plib.
1 parent 0bec6ed commit c54f899

2 files changed

Lines changed: 610 additions & 110 deletions

File tree

tests/test_unit_conversions.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import os
2+
import math
3+
import numpy as np
4+
import pandas as pd
5+
import pytest
6+
import importlib.util
7+
8+
import vtools.functions.unit_conversions as uc
9+
10+
11+
# -----------------------------------------------------------------------------
12+
# Linear / affine converters
13+
# -----------------------------------------------------------------------------
14+
@pytest.mark.parametrize("x", [123.456, np.array([0.0, 1.0, 10.0, 123.456])])
15+
def test_m_ft_roundtrip(x):
16+
ft = uc.m_to_ft(x)
17+
m = uc.ft_to_m(ft)
18+
np.testing.assert_allclose(m, x, rtol=0, atol=1e-12)
19+
20+
21+
@pytest.mark.parametrize("x", [7.5, np.array([0.0, 1.0, 2.5, 1000.0])])
22+
def test_cms_cfs_roundtrip(x):
23+
cfs = uc.cms_to_cfs(x)
24+
cms = uc.cfs_to_cms(cfs)
25+
np.testing.assert_allclose(cms, x, rtol=0, atol=1e-12)
26+
27+
28+
def test_temperature_roundtrip():
29+
c = np.array([-40.0, 0.0, 20.0, 37.0, 100.0])
30+
f = uc.celsius_to_fahrenheit(c)
31+
c2 = uc.fahrenheit_to_celsius(f)
32+
np.testing.assert_allclose(c2, c, rtol=0, atol=1e-12)
33+
34+
35+
# -----------------------------------------------------------------------------
36+
# Series / DataFrame preservation
37+
# -----------------------------------------------------------------------------
38+
@pytest.fixture
39+
def sample_series_df():
40+
idx = pd.date_range("2020-01-01", periods=4, freq="h")
41+
ser = pd.Series([0.0, 1.0, 2.0, 3.0], index=idx, name="flow")
42+
df = pd.DataFrame(
43+
{"a": [0.0, 1.0, 2.0, 3.0], "b": [10.0, 11.0, 12.0, 13.0]},
44+
index=idx,
45+
)
46+
return ser, df
47+
48+
49+
def test_series_conversion(sample_series_df):
50+
ser, _ = sample_series_df
51+
out = uc.convert_units(ser, "m", "ft")
52+
assert isinstance(out, pd.Series)
53+
assert out.index.equals(ser.index)
54+
assert out.name == ser.name
55+
np.testing.assert_allclose(out.values, ser.values * uc.M2FT, atol=1e-12)
56+
57+
58+
def test_dataframe_conversion(sample_series_df):
59+
_, df = sample_series_df
60+
out = uc.convert_units(df, "ft^3 s-1", "m^3 s-1")
61+
assert isinstance(out, pd.DataFrame)
62+
assert out.index.equals(df.index)
63+
assert (out.columns == df.columns).all()
64+
np.testing.assert_allclose(out.values, df.values * uc.CFS2CMS, atol=1e-12)
65+
# ----------------------------------------------------------------import importlib.util
66+
67+
@pytest.mark.skipif(
68+
importlib.util.find_spec("cf_units") is None,
69+
reason="cf_units not installed")
70+
def test_optional_cf_units_backend(monkeypatch):
71+
x = np.array([0.0, 1.0, 3.0])
72+
monkeypatch.setenv("VTOOLS_UNITS_BACKEND", "cf_units")
73+
out = uc.convert_units(x, "m", "ft")
74+
np.testing.assert_allclose(out, x * uc.M2FT, atol=1e-12)
75+
monkeypatch.delenv("VTOOLS_UNITS_BACKEND", raising=False)
76+
77+
78+
79+
80+
# Aliases, backend consistency
81+
# -----------------------------------------------------------------------------
82+
@pytest.mark.parametrize("alias", ["cfs", "ft3/s", "ft^3/s"])
83+
def test_aliases_cfs_to_cms(alias):
84+
x = np.array([0.0, 35.31466621, 353.1466621])
85+
out = uc.convert_units(x, alias, "m^3 s-1")
86+
expected = x * uc.CFS2CMS
87+
np.testing.assert_allclose(out, expected, atol=1e-12)
88+
89+
90+
@pytest.mark.parametrize("alias", ["cms", "m3/s", "m^3/s"])
91+
def test_aliases_cms_to_cfs(alias):
92+
x = np.array([0.0, 1.0, 10.0])
93+
out = uc.convert_units(x, alias, "ft^3 s-1")
94+
expected = x * uc.CMS2CFS
95+
np.testing.assert_allclose(out, expected, atol=1e-12)
96+
97+
98+
def test_aliases_temperature():
99+
x = np.array([32.0, 212.0])
100+
out = uc.convert_units(x, "deg F", "degC")
101+
expected = uc.fahrenheit_to_celsius(x)
102+
np.testing.assert_allclose(out, expected, atol=1e-12)
103+
104+
105+
@pytest.mark.parametrize("alias", ["us/cm", "μs/cm", "micromhos/cm"])
106+
def test_aliases_conductivity(alias):
107+
x = np.array([0.0, 500.0, 5000.0, 50000.0])
108+
out = uc.convert_units(x, alias, "psu")
109+
direct = uc.ec_psu_25c(x, hill_correction=True)
110+
np.testing.assert_allclose(out, direct, atol=1e-12)
111+
112+
113+
def test_backend_consistency_with_constants():
114+
x = np.array([0.0, 1.23, 10.0, 123.45])
115+
116+
# m <-> ft
117+
np.testing.assert_allclose(uc.convert_units(x, "m", "ft"), x * uc.M2FT, atol=1e-12)
118+
np.testing.assert_allclose(uc.convert_units(x, "ft", "m"), x * uc.FT2M, atol=1e-12)
119+
120+
# cfs <-> cms
121+
np.testing.assert_allclose(
122+
uc.convert_units(x, "ft^3 s-1", "m^3 s-1"), x * uc.CFS2CMS, atol=1e-12
123+
)
124+
np.testing.assert_allclose(
125+
uc.convert_units(x, "m^3 s-1", "ft^3 s-1"), x * uc.CMS2CFS, atol=1e-12
126+
)
127+
128+
129+
#@pytest.mark.skipif(
130+
# not pytest.importorskip("cf_units", reason="cf_units not installed"),
131+
# reason="cf_units not available",
132+
#)
133+
#def test_optional_cf_units_backend(monkeypatch):
134+
# x = np.array([0.0, 1.0, 3.0])
135+
# monkeypatch.setenv("VTOOLS_UNITS_BACKEND", "cf_units")
136+
# out = uc.convert_units(x, "m", "ft")
137+
# np.testing.assert_allclose(out, x * uc.M2FT, atol=1e-12)
138+
# monkeypatch.delenv("VTOOLS_UNITS_BACKEND", raising=False)
139+
140+
141+
# -----------------------------------------------------------------------------
142+
# EC↔PSU consistency
143+
# -----------------------------------------------------------------------------
144+
def test_convert_units_matches_ec_psu():
145+
ec = np.array([0.0, 100.0, 1000.0, 50000.0])
146+
a = uc.convert_units(ec, "us/cm", "psu")
147+
b = uc.ec_psu_25c(ec, hill_correction=True)
148+
np.testing.assert_allclose(a, b, atol=1e-12)
149+
150+
151+
def test_convert_units_matches_psu_ec():
152+
psu = np.array([0.0, 1.0, 10.0, 33.0])
153+
a = uc.convert_units(psu, "psu", "us/cm")
154+
b = uc.psu_ec_25c(psu, refine=True, hill_correction=True)
155+
np.testing.assert_allclose(a, b, atol=1e-9) # root-finder tolerance
156+
157+
158+
def test_negative_ec_behavior():
159+
# Scalar negative → NaN
160+
assert math.isnan(uc.ec_psu_25c(-5.0))
161+
assert math.isnan(uc.convert_units(-5.0, "us/cm", "psu"))
162+
163+
# Array: negative entries become NaN
164+
ec = np.array([-5.0, 0.0, 10.0])
165+
out1 = uc.ec_psu_25c(ec)
166+
out2 = uc.convert_units(ec, "us/cm", "psu")
167+
assert np.isnan(out1[0]) and np.isnan(out2[0])
168+
np.testing.assert_allclose(out1[1:], out2[1:], atol=1e-12)

0 commit comments

Comments
 (0)