jerlov 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jerlov-0.3.0/jerlov.egg-info → jerlov-0.3.2}/PKG-INFO +14 -1
- {jerlov-0.3.0 → jerlov-0.3.2}/README.md +13 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/__init__.py +1 -1
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/_data.py +37 -7
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/backscattering.py +8 -4
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/colour.py +2 -2
- jerlov-0.3.2/jerlov/py.typed +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/water.py +13 -5
- {jerlov-0.3.0 → jerlov-0.3.2/jerlov.egg-info}/PKG-INFO +14 -1
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov.egg-info/SOURCES.txt +2 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/pyproject.toml +2 -1
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_api.py +55 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_backscattering.py +20 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_packaging.py +38 -0
- jerlov-0.3.2/tests/test_quoted_figures.py +192 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/LICENSE +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/NOTICE +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/__init__.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/austin1986_kd.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/austin1986_model.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/boss2001_chi.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/cie_d65.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/jerlov1968_kd.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/jerlov1976_kd.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/paulson1977_shortwave.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/smart2007_b_from_c.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/solonenko2015_iop.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/williamson2022_iop.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/williamson2022_measured.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/data/williamson2023_depth.csv +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/scene.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/shortwave.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov/sources.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov.egg-info/dependency_links.txt +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov.egg-info/requires.txt +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/jerlov.egg-info/top_level.txt +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/setup.cfg +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_colour.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_depth.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_reproduces_papers.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_scene.py +0 -0
- {jerlov-0.3.0 → jerlov-0.3.2}/tests/test_shortwave.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jerlov
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Inherent optical properties of Jerlov water types, with provenance
|
|
5
5
|
Author: T. Ishibashi
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -298,6 +298,19 @@ questions the first edition of Jerlov settled, and the rest notes.
|
|
|
298
298
|
`DECISIONS.md` records why the package is shaped the way it is, including the
|
|
299
299
|
alternatives that were rejected and why.
|
|
300
300
|
|
|
301
|
+
## Contributing, and reporting a wrong number
|
|
302
|
+
|
|
303
|
+
`CONTRIBUTING.md` sets out what goes in and what it takes: a primary source, a
|
|
304
|
+
script in `tools/` that produces the table, checks inside that script against
|
|
305
|
+
the paper's own equations, an entry in `DATA.md`, and a regression test.
|
|
306
|
+
|
|
307
|
+
**A coefficient that disagrees with a paper you have is the most useful report
|
|
308
|
+
this package can receive.** `DATA.md` exists because several such
|
|
309
|
+
disagreements turned out to be defects in the literature rather than here.
|
|
310
|
+
Issues: <https://github.com/tishibashi-cpu/jerlov/issues>
|
|
311
|
+
|
|
312
|
+
`CHANGELOG.md` covers every release.
|
|
313
|
+
|
|
301
314
|
## Licence
|
|
302
315
|
|
|
303
316
|
Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
|
|
@@ -273,6 +273,19 @@ questions the first edition of Jerlov settled, and the rest notes.
|
|
|
273
273
|
`DECISIONS.md` records why the package is shaped the way it is, including the
|
|
274
274
|
alternatives that were rejected and why.
|
|
275
275
|
|
|
276
|
+
## Contributing, and reporting a wrong number
|
|
277
|
+
|
|
278
|
+
`CONTRIBUTING.md` sets out what goes in and what it takes: a primary source, a
|
|
279
|
+
script in `tools/` that produces the table, checks inside that script against
|
|
280
|
+
the paper's own equations, an entry in `DATA.md`, and a regression test.
|
|
281
|
+
|
|
282
|
+
**A coefficient that disagrees with a paper you have is the most useful report
|
|
283
|
+
this package can receive.** `DATA.md` exists because several such
|
|
284
|
+
disagreements turned out to be defects in the literature rather than here.
|
|
285
|
+
Issues: <https://github.com/tishibashi-cpu/jerlov/issues>
|
|
286
|
+
|
|
287
|
+
`CHANGELOG.md` covers every release.
|
|
288
|
+
|
|
276
289
|
## Licence
|
|
277
290
|
|
|
278
291
|
Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
|
|
@@ -8,13 +8,13 @@ silently cleaned up on the way in.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import csv
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
11
13
|
from functools import lru_cache
|
|
12
14
|
from importlib import resources
|
|
13
15
|
|
|
14
16
|
import numpy as np
|
|
15
17
|
|
|
16
|
-
import numpy as np
|
|
17
|
-
|
|
18
18
|
#: numpy.trapezoid is the name from NumPy 2.0; before that it was numpy.trapz,
|
|
19
19
|
#: which 2.0 removed. The package claims to work from NumPy 1.22, so neither
|
|
20
20
|
#: can be assumed. This lives here rather than in a module about colour so
|
|
@@ -23,6 +23,36 @@ import numpy as np
|
|
|
23
23
|
#: supported NumPy.
|
|
24
24
|
trapezoid = getattr(np, "trapezoid", None) or np.trapz
|
|
25
25
|
|
|
26
|
+
_PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) + os.sep
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def caller_stacklevel() -> int:
|
|
30
|
+
"""The ``stacklevel`` that points a warning at the caller's own code.
|
|
31
|
+
|
|
32
|
+
Call it from the function that calls ``warnings.warn``. A fixed number
|
|
33
|
+
is right only for one call path: ``Water.c`` reaches the flag check one
|
|
34
|
+
frame deeper than ``Water.a`` does, and ``Scene`` deeper still, so a
|
|
35
|
+
constant pointed some warnings at this package instead of at the line
|
|
36
|
+
that asked for the value.
|
|
37
|
+
"""
|
|
38
|
+
frame = sys._getframe(1)
|
|
39
|
+
level = 1
|
|
40
|
+
while frame is not None and frame.f_code.co_filename.startswith(_PACKAGE_DIR):
|
|
41
|
+
frame = frame.f_back
|
|
42
|
+
level += 1
|
|
43
|
+
return level
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _frozen(array: np.ndarray) -> np.ndarray:
|
|
47
|
+
"""Make a cached array read-only.
|
|
48
|
+
|
|
49
|
+
The loaders below are cached, so every caller gets the same array
|
|
50
|
+
objects. Writing into one would silently change the table for every
|
|
51
|
+
later caller in the process.
|
|
52
|
+
"""
|
|
53
|
+
array.setflags(write=False)
|
|
54
|
+
return array
|
|
55
|
+
|
|
26
56
|
#: Values whose ``status`` is one of these should not be used without the
|
|
27
57
|
#: caller being told. See README sections 1-6.
|
|
28
58
|
QUESTIONABLE = frozenset({"suspect", "missing", "extrapolated",
|
|
@@ -75,8 +105,8 @@ def spectrum(
|
|
|
75
105
|
)
|
|
76
106
|
order = np.argsort(wl)
|
|
77
107
|
return (
|
|
78
|
-
np.asarray(wl, dtype=float)[order],
|
|
79
|
-
np.asarray(values, dtype=float)[order],
|
|
108
|
+
_frozen(np.asarray(wl, dtype=float)[order]),
|
|
109
|
+
_frozen(np.asarray(values, dtype=float)[order]),
|
|
80
110
|
tuple(statuses[i] for i in order),
|
|
81
111
|
)
|
|
82
112
|
|
|
@@ -104,7 +134,7 @@ def austin_model() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
|
104
134
|
m = np.array([float(r["M_slope"]) for r in rows])
|
|
105
135
|
kw = np.array([float(r["Kw_pure_seawater_per_m"]) for r in rows])
|
|
106
136
|
order = np.argsort(wl)
|
|
107
|
-
return wl[order], m[order], kw[order]
|
|
137
|
+
return _frozen(wl[order]), _frozen(m[order]), _frozen(kw[order])
|
|
108
138
|
|
|
109
139
|
|
|
110
140
|
@lru_cache(maxsize=None)
|
|
@@ -119,5 +149,5 @@ def b_from_c_ratio() -> tuple[np.ndarray, dict[str, np.ndarray]]:
|
|
|
119
149
|
for r in rows
|
|
120
150
|
if r["statistic"] == stat
|
|
121
151
|
}
|
|
122
|
-
out[stat] = np.array([by_wl[w] for w in wl])
|
|
123
|
-
return np.array(wl), out
|
|
152
|
+
out[stat] = _frozen(np.array([by_wl[w] for w in wl]))
|
|
153
|
+
return _frozen(np.array(wl)), out
|
|
@@ -70,11 +70,15 @@ def pure_water_vsf(angle_deg, wavelength_nm, salinity_psu: float = 37.0):
|
|
|
70
70
|
The amplitude carries about 15 percent uncertainty, which the authors give
|
|
71
71
|
as the agreement between measurement and theory.
|
|
72
72
|
"""
|
|
73
|
-
angle = np.
|
|
73
|
+
angle = np.asarray(angle_deg, dtype=float)
|
|
74
74
|
amplitude = (1.38 * (np.asarray(wavelength_nm, dtype=float) / 500.0) ** -4.32
|
|
75
75
|
* (1 + 0.3 * salinity_psu / 37.0) * 1e-4)
|
|
76
76
|
out = amplitude * (1 + _ratio() * np.cos(np.radians(angle)) ** 2)
|
|
77
|
-
|
|
77
|
+
# Angle and wavelength broadcast against each other; the answer is a
|
|
78
|
+
# float only when neither of them was an array.
|
|
79
|
+
if np.ndim(out) == 0:
|
|
80
|
+
return float(out)
|
|
81
|
+
return out
|
|
78
82
|
|
|
79
83
|
|
|
80
84
|
def pure_water_backscattering(wavelength_nm, salinity_psu: float = 37.0):
|
|
@@ -181,7 +185,7 @@ def bb_from_vsf(beta, angle_deg: float, wavelength_nm, *,
|
|
|
181
185
|
"scattering function varies steeply here and a single-angle "
|
|
182
186
|
"measurement pins bb poorly.",
|
|
183
187
|
AngleWarning,
|
|
184
|
-
stacklevel=
|
|
188
|
+
stacklevel=_data.caller_stacklevel(),
|
|
185
189
|
)
|
|
186
190
|
|
|
187
191
|
scalar = np.ndim(beta) == 0 and np.ndim(wavelength_nm) == 0
|
|
@@ -198,7 +202,7 @@ def bb_from_vsf(beta, angle_deg: float, wavelength_nm, *,
|
|
|
198
202
|
"so the particle contribution came out negative. Check the "
|
|
199
203
|
"calibration, the wavelength and the salinity.",
|
|
200
204
|
AngleWarning,
|
|
201
|
-
stacklevel=
|
|
205
|
+
stacklevel=_data.caller_stacklevel(),
|
|
202
206
|
)
|
|
203
207
|
|
|
204
208
|
def shape(values):
|
|
@@ -163,7 +163,7 @@ def integrate_response(spectrum, wavelengths, response, response_wavelengths,
|
|
|
163
163
|
"The integral is over the overlap and is biased by what was left "
|
|
164
164
|
"out",
|
|
165
165
|
CoverageWarning,
|
|
166
|
-
stacklevel=
|
|
166
|
+
stacklevel=_data.caller_stacklevel(),
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
resampled = np.stack(
|
|
@@ -202,7 +202,7 @@ def xyz_to_srgb(xyz, *, clip: bool = True) -> np.ndarray:
|
|
|
202
202
|
"the colour lies outside the sRGB gamut"
|
|
203
203
|
+ (" and has been clipped" if clip else ""),
|
|
204
204
|
GamutWarning,
|
|
205
|
-
stacklevel=
|
|
205
|
+
stacklevel=_data.caller_stacklevel(),
|
|
206
206
|
)
|
|
207
207
|
if clip:
|
|
208
208
|
linear = np.clip(linear, 0.0, 1.0)
|
|
File without changes
|
|
@@ -75,7 +75,9 @@ class Water:
|
|
|
75
75
|
source: Source | None = None,
|
|
76
76
|
flags: dict[str, tuple[str, ...]] | None = None,
|
|
77
77
|
) -> None:
|
|
78
|
-
|
|
78
|
+
# Copied, so that neither the caller's arrays nor the packaged tables
|
|
79
|
+
# can change underneath this object, and it cannot change them.
|
|
80
|
+
self.wavelengths = np.array(wavelengths, dtype=float)
|
|
79
81
|
if self.wavelengths.ndim != 1 or self.wavelengths.size == 0:
|
|
80
82
|
raise ValueError("wavelengths must be a non-empty 1-D array")
|
|
81
83
|
if np.any(np.diff(self.wavelengths) <= 0):
|
|
@@ -85,7 +87,7 @@ class Water:
|
|
|
85
87
|
for key, value in (("a", a), ("b", b), ("Kd", kd)):
|
|
86
88
|
if value is None:
|
|
87
89
|
continue
|
|
88
|
-
arr = np.
|
|
90
|
+
arr = np.array(value, dtype=float)
|
|
89
91
|
if arr.shape != self.wavelengths.shape:
|
|
90
92
|
raise ValueError(f"{key} must have the same shape as wavelengths")
|
|
91
93
|
self._series[key] = arr
|
|
@@ -163,7 +165,7 @@ class Water:
|
|
|
163
165
|
+ "; ".join(sorted(hit))
|
|
164
166
|
+ ". See the package README for what is known about them.",
|
|
165
167
|
ProvenanceWarning,
|
|
166
|
-
stacklevel=
|
|
168
|
+
stacklevel=_data.caller_stacklevel(),
|
|
167
169
|
)
|
|
168
170
|
|
|
169
171
|
def a(self, wl):
|
|
@@ -368,7 +370,7 @@ def kd_spectrum(kd, wavelength_nm: float, at):
|
|
|
368
370
|
"Austin & Petzold (1986) reported exactly this problem in "
|
|
369
371
|
"Jerlov's own type I values.",
|
|
370
372
|
ProvenanceWarning,
|
|
371
|
-
stacklevel=
|
|
373
|
+
stacklevel=_data.caller_stacklevel(),
|
|
372
374
|
)
|
|
373
375
|
result = np.interp(query, wl, m) / m1 * (kd - kw1) + np.interp(query, wl, kw)
|
|
374
376
|
return _like_input(result, at)
|
|
@@ -395,5 +397,11 @@ def b_from_c(c, wavelength_nm, *, bw, cw, bound: str = "average"):
|
|
|
395
397
|
f"wavelength outside the measured range ({wl[0]:g}-{wl[-1]:g} nm)"
|
|
396
398
|
)
|
|
397
399
|
ratio = np.interp(query, wl, ratios[bound])
|
|
400
|
+
if np.ndim(wavelength_nm) == 0:
|
|
401
|
+
ratio = ratio[0]
|
|
398
402
|
result = (np.asarray(c, dtype=float) - cw) * ratio + bw
|
|
399
|
-
|
|
403
|
+
# c and the wavelength broadcast against each other; the answer is a
|
|
404
|
+
# float only when neither of them was an array.
|
|
405
|
+
if np.ndim(result) == 0:
|
|
406
|
+
return float(result)
|
|
407
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jerlov
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Inherent optical properties of Jerlov water types, with provenance
|
|
5
5
|
Author: T. Ishibashi
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -298,6 +298,19 @@ questions the first edition of Jerlov settled, and the rest notes.
|
|
|
298
298
|
`DECISIONS.md` records why the package is shaped the way it is, including the
|
|
299
299
|
alternatives that were rejected and why.
|
|
300
300
|
|
|
301
|
+
## Contributing, and reporting a wrong number
|
|
302
|
+
|
|
303
|
+
`CONTRIBUTING.md` sets out what goes in and what it takes: a primary source, a
|
|
304
|
+
script in `tools/` that produces the table, checks inside that script against
|
|
305
|
+
the paper's own equations, an entry in `DATA.md`, and a regression test.
|
|
306
|
+
|
|
307
|
+
**A coefficient that disagrees with a paper you have is the most useful report
|
|
308
|
+
this package can receive.** `DATA.md` exists because several such
|
|
309
|
+
disagreements turned out to be defects in the literature rather than here.
|
|
310
|
+
Issues: <https://github.com/tishibashi-cpu/jerlov/issues>
|
|
311
|
+
|
|
312
|
+
`CHANGELOG.md` covers every release.
|
|
313
|
+
|
|
301
314
|
## Licence
|
|
302
315
|
|
|
303
316
|
Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
|
|
@@ -6,6 +6,7 @@ jerlov/__init__.py
|
|
|
6
6
|
jerlov/_data.py
|
|
7
7
|
jerlov/backscattering.py
|
|
8
8
|
jerlov/colour.py
|
|
9
|
+
jerlov/py.typed
|
|
9
10
|
jerlov/scene.py
|
|
10
11
|
jerlov/shortwave.py
|
|
11
12
|
jerlov/sources.py
|
|
@@ -35,6 +36,7 @@ tests/test_backscattering.py
|
|
|
35
36
|
tests/test_colour.py
|
|
36
37
|
tests/test_depth.py
|
|
37
38
|
tests/test_packaging.py
|
|
39
|
+
tests/test_quoted_figures.py
|
|
38
40
|
tests/test_reproduces_papers.py
|
|
39
41
|
tests/test_scene.py
|
|
40
42
|
tests/test_shortwave.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jerlov"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Inherent optical properties of Jerlov water types, with provenance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -30,3 +30,4 @@ include = ["jerlov*"]
|
|
|
30
30
|
|
|
31
31
|
[tool.setuptools.package-data]
|
|
32
32
|
"jerlov.data" = ["*.csv"]
|
|
33
|
+
"jerlov" = ["py.typed"]
|
|
@@ -213,3 +213,58 @@ def test_interpolating_across_a_gap_still_gives_nan():
|
|
|
213
213
|
assert np.isnan(w.a(650)) # a published value that is wrong
|
|
214
214
|
assert np.isnan(w.a(660)) # and interpolation across it
|
|
215
215
|
assert np.isnan(w.a(675))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_b_from_c_keeps_every_c_at_a_single_wavelength():
|
|
219
|
+
"""An array of c at one wavelength used to come back as its first value."""
|
|
220
|
+
c = np.array([0.5, 1.0, 2.0])
|
|
221
|
+
out = jerlov.b_from_c(c, 488.0, bw=0.003, cw=0.02)
|
|
222
|
+
assert out.shape == (3,)
|
|
223
|
+
for value, single in zip(out, c):
|
|
224
|
+
assert value == pytest.approx(
|
|
225
|
+
jerlov.b_from_c(float(single), 488.0, bw=0.003, cw=0.02)
|
|
226
|
+
)
|
|
227
|
+
assert isinstance(jerlov.b_from_c(0.5, 488.0, bw=0.003, cw=0.02), float)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_a_returned_water_cannot_corrupt_the_packaged_table():
|
|
231
|
+
"""The loaders are cached; a caller's in-place edit must stay local."""
|
|
232
|
+
w = jerlov.water("III")
|
|
233
|
+
before = jerlov.water("III").wavelengths.copy()
|
|
234
|
+
w.wavelengths *= 2.0
|
|
235
|
+
assert np.array_equal(jerlov.water("III").wavelengths, before)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_the_cached_tables_are_read_only():
|
|
239
|
+
from jerlov import _data
|
|
240
|
+
|
|
241
|
+
wl, m, kw = _data.austin_model()
|
|
242
|
+
with pytest.raises(ValueError):
|
|
243
|
+
wl[0] = 0.0
|
|
244
|
+
wl, values, _ = _data.spectrum(
|
|
245
|
+
"williamson2022_iop.csv", "III", "a", "value_per_m"
|
|
246
|
+
)
|
|
247
|
+
with pytest.raises(ValueError):
|
|
248
|
+
values[0] = 0.0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_measurements_are_copied_not_shared():
|
|
252
|
+
wl = np.array([400.0, 500.0, 600.0])
|
|
253
|
+
a = np.array([0.1, 0.2, 0.3])
|
|
254
|
+
mine = Water.from_measurements(wl, a=a)
|
|
255
|
+
a[1] = 99.0
|
|
256
|
+
assert mine.a(500.0) == pytest.approx(0.2)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.mark.parametrize("method", ["kd", "c"])
|
|
260
|
+
def test_a_provenance_warning_points_at_the_callers_line(method):
|
|
261
|
+
"""Not at water.py: `c` reaches the check one frame deeper than `kd`."""
|
|
262
|
+
source = "jerlov1976" if method == "kd" else "williamson2022"
|
|
263
|
+
w = jerlov.water("9C" if method == "kd" else "III", source=source)
|
|
264
|
+
query = 349.5 if method == "kd" else 305.0
|
|
265
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
266
|
+
warnings.simplefilter("always")
|
|
267
|
+
getattr(w, method)(query)
|
|
268
|
+
flagged = [c for c in caught if issubclass(c.category, ProvenanceWarning)]
|
|
269
|
+
assert flagged
|
|
270
|
+
assert all(c.filename == __file__ for c in flagged)
|
|
@@ -165,3 +165,23 @@ def test_it_says_nothing_about_a_jerlov_type():
|
|
|
165
165
|
"""The point of DATA.md section 10 has not quietly gone away."""
|
|
166
166
|
with pytest.raises(jerlov.MissingQuantityError):
|
|
167
167
|
jerlov.water("III").bb(532)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_one_angle_many_wavelengths_gives_one_value_per_wavelength():
|
|
171
|
+
"""A scalar angle used to return the first wavelength's value alone."""
|
|
172
|
+
nms = [450.0, 532.0, 700.0]
|
|
173
|
+
vsf = jerlov.pure_water_vsf(124.0, nms)
|
|
174
|
+
assert np.shape(vsf) == (3,)
|
|
175
|
+
for value, nm in zip(vsf, nms):
|
|
176
|
+
assert value == pytest.approx(jerlov.pure_water_vsf(124.0, nm))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_an_array_of_wavelengths_matches_one_call_per_wavelength():
|
|
180
|
+
"""The water term must be subtracted at each wavelength, not the first."""
|
|
181
|
+
nms = [450.0, 532.0, 700.0]
|
|
182
|
+
together = jerlov.bb_from_vsf([0.001] * 3, 124.0, nms)
|
|
183
|
+
for k, nm in enumerate(nms):
|
|
184
|
+
alone = jerlov.bb_from_vsf(0.001, 124.0, nm)
|
|
185
|
+
assert together.bb[k] == pytest.approx(alone.bb)
|
|
186
|
+
assert together.particulate[k] == pytest.approx(alone.particulate)
|
|
187
|
+
assert together.water[k] == pytest.approx(alone.water)
|
|
@@ -235,3 +235,41 @@ def test_nothing_reaches_for_a_numpy_2_only_name():
|
|
|
235
235
|
"use jerlov._data.trapezoid instead, which resolves whichever name "
|
|
236
236
|
f"the installed NumPy has: {offenders}"
|
|
237
237
|
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@source_tree
|
|
241
|
+
def test_the_files_a_reviewer_looks_for_are_present():
|
|
242
|
+
"""JOSS checks for community guidelines; the rest are cheap to keep."""
|
|
243
|
+
for name in ("README.md", "LICENSE", "NOTICE", "CITATION.cff",
|
|
244
|
+
"CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "CHANGELOG.md",
|
|
245
|
+
"DATA.md", "DECISIONS.md", ".zenodo.json"):
|
|
246
|
+
assert (ROOT / name).exists(), f"{name} is missing"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@source_tree
|
|
250
|
+
def test_contributing_says_how_to_report_and_how_to_contribute():
|
|
251
|
+
"""The three things JOSS asks a contributing guide to cover."""
|
|
252
|
+
text = (ROOT / "CONTRIBUTING.md").read_text().lower()
|
|
253
|
+
assert "issues" in text, "no way to report a problem is given"
|
|
254
|
+
assert "pip install -e" in text, "no way to set the package up is given"
|
|
255
|
+
assert "pull request" in text, "no way to contribute a change is given"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@source_tree
|
|
259
|
+
def test_the_changelog_covers_the_current_version():
|
|
260
|
+
"""A release whose changes are only in a GitHub Release is invisible to
|
|
261
|
+
anyone who installed from PyPI."""
|
|
262
|
+
changelog = (ROOT / "CHANGELOG.md").read_text()
|
|
263
|
+
assert f"## {jerlov.__version__}" in changelog, (
|
|
264
|
+
f"CHANGELOG.md has no entry for {jerlov.__version__}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@source_tree
|
|
269
|
+
def test_type_hints_reach_the_caller():
|
|
270
|
+
"""Annotations are useless to a user without the marker file."""
|
|
271
|
+
assert (ROOT / "jerlov" / "py.typed").exists()
|
|
272
|
+
assert '"jerlov" = ["py.typed"]' in (ROOT / "pyproject.toml").read_text(), (
|
|
273
|
+
"py.typed exists but is not listed as package data, so it will not "
|
|
274
|
+
"be installed"
|
|
275
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""The numbers the prose quotes must still be the numbers the code gives.
|
|
2
|
+
|
|
3
|
+
README.md, DATA.md and the examples all state figures: a factor of 3.8 here, 46
|
|
4
|
+
percent there. Those were computed once. Nothing has been stopping them from
|
|
5
|
+
drifting away from what the package now returns, and a reader has no way to
|
|
6
|
+
tell when they have.
|
|
7
|
+
|
|
8
|
+
Each test below recomputes one quoted figure. When one fails, the prose is
|
|
9
|
+
what needs fixing, not the test — unless the change to the code was a mistake,
|
|
10
|
+
in which case it is the other way round and the test has done its job.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import pathlib
|
|
17
|
+
import re
|
|
18
|
+
import warnings
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
import jerlov
|
|
24
|
+
from jerlov import _data
|
|
25
|
+
|
|
26
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
27
|
+
source_tree = pytest.mark.skipif(
|
|
28
|
+
not (ROOT / "README.md").exists(),
|
|
29
|
+
reason="not running from a source tree",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quiet():
|
|
34
|
+
return warnings.catch_warnings()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# -- the sources disagree, and by how much --------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_scattering_at_510nm_differs_by_the_stated_factor():
|
|
41
|
+
"""README and sources_disagree.py both say 3.8 for Jerlov III."""
|
|
42
|
+
with quiet():
|
|
43
|
+
warnings.simplefilter("ignore")
|
|
44
|
+
measured = float(jerlov.water("III").b(510))
|
|
45
|
+
inverted = float(jerlov.water("III", source="solonenko2015").b(510))
|
|
46
|
+
assert inverted / measured == pytest.approx(3.8, abs=0.05)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_the_disagreement_does_not_run_one_way():
|
|
50
|
+
"""sources_disagree.py claims Jerlov IB goes the other direction."""
|
|
51
|
+
with quiet():
|
|
52
|
+
warnings.simplefilter("ignore")
|
|
53
|
+
measured = float(jerlov.water("IB").b(510))
|
|
54
|
+
inverted = float(jerlov.water("IB", source="solonenko2015").b(510))
|
|
55
|
+
assert inverted / measured < 0.5
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_the_three_routes_span_the_stated_factor_at_532nm():
|
|
59
|
+
"""DATA.md section 17 says 5.1 for Jerlov III."""
|
|
60
|
+
chlorophyll_model = 1.8998 # Abd El-Mottaleb et al. (2024)
|
|
61
|
+
with quiet():
|
|
62
|
+
warnings.simplefilter("ignore")
|
|
63
|
+
measured = float(jerlov.water("III").c(532))
|
|
64
|
+
inverted = float(jerlov.water("III", source="solonenko2015").c(532))
|
|
65
|
+
assert measured == pytest.approx(0.371, abs=0.001)
|
|
66
|
+
assert inverted == pytest.approx(1.092, abs=0.001)
|
|
67
|
+
assert chlorophyll_model / measured == pytest.approx(5.1, abs=0.05)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# -- the scattering model constants ---------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_the_small_particle_coefficients_differ_by_the_stated_amount():
|
|
74
|
+
"""DATA.md section 6: 1.513 is 31 percent larger than 1.151302."""
|
|
75
|
+
haltrin = jerlov.get_source("williamson2022").scattering.small_coeff
|
|
76
|
+
solonenko = jerlov.get_source("solonenko2015").scattering.small_coeff
|
|
77
|
+
assert haltrin == pytest.approx(1.151302)
|
|
78
|
+
assert solonenko == pytest.approx(1.513)
|
|
79
|
+
assert solonenko / haltrin - 1 == pytest.approx(0.31, abs=0.005)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# -- shortwave penetration ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_the_shortwave_fit_misses_its_source_by_the_stated_amount():
|
|
86
|
+
"""README, DATA.md section 14 and solar_heating.py all say 46 percent."""
|
|
87
|
+
rows = _data._rows("jerlov1968_total_irradiance.csv")
|
|
88
|
+
worst = 0.0
|
|
89
|
+
for row in rows:
|
|
90
|
+
if row["water_type"] not in ("I", "IA", "IB", "II", "III"):
|
|
91
|
+
continue
|
|
92
|
+
if not row["percent_of_surface"]:
|
|
93
|
+
continue
|
|
94
|
+
depth = float(row["depth_m"])
|
|
95
|
+
if depth == 0 or depth > 100:
|
|
96
|
+
continue
|
|
97
|
+
want = float(row["percent_of_surface"]) / 100
|
|
98
|
+
got = jerlov.solar_fraction(row["water_type"], depth)
|
|
99
|
+
worst = max(worst, abs(got - want) / want)
|
|
100
|
+
assert worst == pytest.approx(0.46, abs=0.01)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_the_published_R_values_are_what_the_docs_print():
|
|
104
|
+
published = {"I": 0.58, "IA": 0.62, "IB": 0.67, "II": 0.77, "III": 0.78}
|
|
105
|
+
for water_type, value in published.items():
|
|
106
|
+
assert jerlov.shortwave_parameters(water_type).R == pytest.approx(value)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# -- backscattering -------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_the_worst_chi_p_spread_is_the_stated_figure():
|
|
113
|
+
"""README and DATA.md section 18 both say 34.8 percent at 170 degrees."""
|
|
114
|
+
with pytest.warns(jerlov.AngleWarning, match="34.8"):
|
|
115
|
+
result = jerlov.bb_from_vsf(0.002, 170.0, 532.0)
|
|
116
|
+
assert result.quoted_error_percent == pytest.approx(34.8)
|
|
117
|
+
middle = [jerlov.bb_from_vsf(0.002, a, 532.0).quoted_error_percent
|
|
118
|
+
for a in (100.0, 120.0, 140.0, 160.0)]
|
|
119
|
+
assert max(middle) <= 6.5, "the 3-6 percent claim no longer holds"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_chi_w_and_chi_p_cross_where_the_docs_say():
|
|
123
|
+
"""DATA.md section 18 quotes 116.9 degrees against the paper's ~118."""
|
|
124
|
+
r = (1 - jerlov.backscattering.DEPOLARISATION_RATIO) / (
|
|
125
|
+
1 + jerlov.backscattering.DEPOLARISATION_RATIO)
|
|
126
|
+
angles = np.linspace(90.0, 170.0, 80001)
|
|
127
|
+
gap = [(1 + r / 3) / (1 + r * math.cos(math.radians(a)) ** 2)
|
|
128
|
+
- jerlov.particulate_chi(a) for a in angles]
|
|
129
|
+
crossing = float(angles[int(np.argmin(np.abs(gap)))])
|
|
130
|
+
assert crossing == pytest.approx(116.9, abs=0.1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# -- the example that argues against defaults -----------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_the_guess_overstates_contrast_by_the_stated_amount():
|
|
137
|
+
"""from_one_measurement.py computes 65 percent and the prose repeats it.
|
|
138
|
+
|
|
139
|
+
The example computes this rather than hard-coding it, so this test is
|
|
140
|
+
what stops the *prose* around it from going stale.
|
|
141
|
+
"""
|
|
142
|
+
wl = np.arange(420.0, 681.0, 20.0)
|
|
143
|
+
a = 0.02 + 0.35 * np.exp(-(wl - 420.0) / 90.0) + 0.30 * (wl > 600)
|
|
144
|
+
b = 0.45 * (550.0 / wl) ** 0.8
|
|
145
|
+
water = jerlov.Water.from_measurements(wl, a=a, b=b)
|
|
146
|
+
|
|
147
|
+
measured = jerlov.bb_from_vsf(0.0021, 140.0, 532.0, salinity_psu=35.0)
|
|
148
|
+
ratio = measured.bb / water.b(532.0)
|
|
149
|
+
assert ratio == pytest.approx(0.033, abs=0.001)
|
|
150
|
+
|
|
151
|
+
surface = np.interp(wl, *jerlov.d65(), left=0.0, right=0.0)
|
|
152
|
+
with quiet():
|
|
153
|
+
warnings.simplefilter("ignore")
|
|
154
|
+
scene = jerlov.Scene.at_depth(
|
|
155
|
+
water, 8.0, surface, wl,
|
|
156
|
+
kd=jerlov.water("II", source="austin1986"))
|
|
157
|
+
white = scene.downwelling / np.pi
|
|
158
|
+
contrasts = {}
|
|
159
|
+
for label, value in (("guess", 0.015), ("measured", ratio)):
|
|
160
|
+
b_inf = jerlov.veiling_radiance_estimate(
|
|
161
|
+
water, scene.downwelling, wl, backscatter_ratio=value)
|
|
162
|
+
observed = scene.observe(np.full_like(wl, 0.5), 5.0,
|
|
163
|
+
veiling_radiance=b_inf)
|
|
164
|
+
contrasts[label] = float(
|
|
165
|
+
np.mean(np.abs(observed.contrast(0.1 * white))))
|
|
166
|
+
overstatement = contrasts["guess"] / contrasts["measured"] - 1
|
|
167
|
+
assert overstatement == pytest.approx(0.65, abs=0.03)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# -- the prose and the code are checked against each other ----------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@source_tree
|
|
174
|
+
def test_the_readme_quotes_a_transmittance_the_package_produces():
|
|
175
|
+
"""`solar_fraction("IB", 10.0)` appears in the README with its answer."""
|
|
176
|
+
readme = (ROOT / "README.md").read_text()
|
|
177
|
+
match = re.search(
|
|
178
|
+
r'solar_fraction\("IB", 10\.0\).*?#\s*([\d.]+)', readme)
|
|
179
|
+
assert match, "the README no longer shows that call with its result"
|
|
180
|
+
assert jerlov.solar_fraction("IB", 10.0) == pytest.approx(
|
|
181
|
+
float(match.group(1)), abs=0.0005)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@source_tree
|
|
185
|
+
def test_the_readme_quotes_a_chi_p_the_package_produces():
|
|
186
|
+
readme = (ROOT / "README.md").read_text()
|
|
187
|
+
match = re.search(r"r\.chi_p, r\.quoted_error_percent\s*#\s*([\d.]+),\s*([\d.]+)",
|
|
188
|
+
readme)
|
|
189
|
+
assert match, "the README no longer shows chi_p with its value"
|
|
190
|
+
result = jerlov.bb_from_vsf(0.0021, 140.0, 532.0)
|
|
191
|
+
assert result.chi_p == pytest.approx(float(match.group(1)))
|
|
192
|
+
assert result.quoted_error_percent == pytest.approx(float(match.group(2)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|