jerlov 0.3.3__tar.gz → 0.4.0__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.3/jerlov.egg-info → jerlov-0.4.0}/PKG-INFO +1 -1
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/__init__.py +1 -1
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/colour.py +16 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/scene.py +21 -2
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/shortwave.py +15 -1
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/water.py +13 -4
- {jerlov-0.3.3 → jerlov-0.4.0/jerlov.egg-info}/PKG-INFO +1 -1
- {jerlov-0.3.3 → jerlov-0.4.0}/pyproject.toml +1 -1
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_colour.py +10 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_depth.py +14 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_scene.py +28 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_shortwave.py +8 -2
- {jerlov-0.3.3 → jerlov-0.4.0}/LICENSE +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/NOTICE +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/README.md +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/_data.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/backscattering.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/__init__.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/austin1986_kd.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/austin1986_model.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/boss2001_chi.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/cie_d65.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1968_kd.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1976_kd.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/paulson1977_shortwave.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/smart2007_b_from_c.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/solonenko2015_iop.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2022_iop.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2022_measured.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2023_depth.csv +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/py.typed +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/sources.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/SOURCES.txt +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/dependency_links.txt +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/requires.txt +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/top_level.txt +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/setup.cfg +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_api.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_backscattering.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_packaging.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_quoted_figures.py +0 -0
- {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_reproduces_papers.py +0 -0
|
@@ -136,6 +136,16 @@ def integrate_response(spectrum, wavelengths, response, response_wavelengths,
|
|
|
136
136
|
-------
|
|
137
137
|
Array of shape ``(k,)``: the integral of spectrum times each channel over
|
|
138
138
|
wavelength, in the units of the spectrum times nm.
|
|
139
|
+
|
|
140
|
+
Notes
|
|
141
|
+
-----
|
|
142
|
+
The integral is taken on the spectrum's own wavelengths, with the
|
|
143
|
+
response interpolated onto them. Sample at 5 nm or finer where you can:
|
|
144
|
+
on coarser grids the narrow peaks of a response, or of the spectrum,
|
|
145
|
+
fall between samples, and the error grows quickly. Interpolating the
|
|
146
|
+
spectrum onto the response's grid instead is no cure, since it only
|
|
147
|
+
trades this error for one in the shape assumed between the spectrum's
|
|
148
|
+
samples.
|
|
139
149
|
"""
|
|
140
150
|
spectrum = np.asarray(spectrum, dtype=float)
|
|
141
151
|
wavelengths = np.asarray(wavelengths, dtype=float)
|
|
@@ -149,6 +159,12 @@ def integrate_response(spectrum, wavelengths, response, response_wavelengths,
|
|
|
149
159
|
raise ValueError("response must be aligned with response_wavelengths")
|
|
150
160
|
if wavelengths.size < 2:
|
|
151
161
|
raise ValueError("at least two wavelengths are needed to integrate")
|
|
162
|
+
# Descending wavelengths make the integral negative and the coverage
|
|
163
|
+
# zero, so a reversed spectrum came out as a negative colour.
|
|
164
|
+
for grid, label in ((wavelengths, "wavelengths"),
|
|
165
|
+
(response_wavelengths, "response_wavelengths")):
|
|
166
|
+
if np.any(np.diff(grid) <= 0):
|
|
167
|
+
raise ValueError(f"{label} must be strictly ascending")
|
|
152
168
|
|
|
153
169
|
covered = [_coverage(wavelengths, response_wavelengths, response[:, k])
|
|
154
170
|
for k in range(response.shape[1])]
|
|
@@ -120,8 +120,27 @@ class Observation:
|
|
|
120
120
|
def contrast(self, background_radiance) -> np.ndarray:
|
|
121
121
|
"""Apparent contrast against a background seen through the same path.
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
``background_radiance`` is the radiance leaving the background, on
|
|
124
|
+
the scene's wavelengths and in the unit of the target radiance
|
|
125
|
+
(``reflectance * downwelling / pi``), not a reflectance.
|
|
126
|
+
|
|
127
|
+
With ``L_t`` and ``L_b`` the radiances leaving target and background,
|
|
128
|
+
``t`` the transmittance and ``B`` the veiling radiance at infinity::
|
|
129
|
+
|
|
130
|
+
C(r) = (L_t - L_b) t / (L_b t + B (1 - t))
|
|
131
|
+
= C_0 * L_b t / (L_b t + B (1 - t)), C_0 = (L_t - L_b) / L_b
|
|
132
|
+
|
|
133
|
+
The path radiance is common to both and cancels from the numerator,
|
|
134
|
+
but not from the denominator, so the loss of contrast is the veiling
|
|
135
|
+
light's doing rather than the transmittance's:
|
|
136
|
+
|
|
137
|
+
- With no veiling radiance (``B = 0``) the contrast is not reduced at
|
|
138
|
+
all; target and background are dimmed by the same factor.
|
|
139
|
+
- Against a background that is the water itself (``L_b = B``, a
|
|
140
|
+
horizontal line of sight into open water) it is ``C_0 * t``, the
|
|
141
|
+
classic result.
|
|
142
|
+
- It falls faster than that against a background darker than ``B``,
|
|
143
|
+
and more slowly against a brighter one.
|
|
125
144
|
"""
|
|
126
145
|
background = _as_array(background_radiance)
|
|
127
146
|
observed_background = (
|
|
@@ -63,7 +63,8 @@ def _rows():
|
|
|
63
63
|
return _data._rows("paulson1977_shortwave.csv")
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def shortwave_parameters(water_type: str
|
|
66
|
+
def shortwave_parameters(water_type: str, *,
|
|
67
|
+
include_non_types: bool = False) -> ShortwaveParameters:
|
|
67
68
|
"""Paulson & Simpson parameters for a Jerlov water type.
|
|
68
69
|
|
|
69
70
|
Only the five oceanic types I, IA, IB, II and III were fitted; the paper
|
|
@@ -72,10 +73,23 @@ def shortwave_parameters(water_type: str) -> ShortwaveParameters:
|
|
|
72
73
|
``water_type="I_upper50"`` selects the paper's alternative fit for type I
|
|
73
74
|
over the upper 50 m, which it gives because ``ln I`` against depth changes
|
|
74
75
|
slope below that. The plain ``"I"`` row is the 100 m fit.
|
|
76
|
+
|
|
77
|
+
Table 2 also prints three rows that are not water types: the authors'
|
|
78
|
+
composite observations, their Run 1, and a Kraus (1972) value from Crater
|
|
79
|
+
Lake (keys ``"composite_observations"``, ``"run_1"`` and
|
|
80
|
+
``"kraus_1972_very_clear"``). They are refused unless
|
|
81
|
+
``include_non_types=True`` is passed, so that one cannot be mistaken for
|
|
82
|
+
a Jerlov type; their ``water_type`` is empty. See DATA.md section 14.
|
|
75
83
|
"""
|
|
76
84
|
rows = _rows()
|
|
77
85
|
for row in rows:
|
|
78
86
|
if row["key"] == water_type:
|
|
87
|
+
if row["status"] == "not_a_water_type" and not include_non_types:
|
|
88
|
+
raise KeyError(
|
|
89
|
+
f"{water_type!r} is a row of Paulson & Simpson (1977) "
|
|
90
|
+
f"Table 2 but not a Jerlov water type: {row['note']}. "
|
|
91
|
+
"Pass include_non_types=True to get it anyway."
|
|
92
|
+
)
|
|
79
93
|
return ShortwaveParameters(
|
|
80
94
|
key=row["key"],
|
|
81
95
|
water_type=row["water_type"],
|
|
@@ -325,6 +325,11 @@ def water_type_at_depth(surface_water_type: str, depth_m: float) -> str | None:
|
|
|
325
325
|
any particular place or season. The paper says so explicitly, and gives
|
|
326
326
|
per-cell cruise and month counts for anyone who needs to judge that.
|
|
327
327
|
"""
|
|
328
|
+
depth_m = float(depth_m)
|
|
329
|
+
if not np.isfinite(depth_m):
|
|
330
|
+
# NaN fails every comparison below, so it would otherwise fall
|
|
331
|
+
# through to "no statement" and look like a deliberate answer.
|
|
332
|
+
raise ValueError(f"depth_m must be a finite number, not {depth_m!r}")
|
|
328
333
|
if depth_m < 0:
|
|
329
334
|
raise ValueError("depth_m cannot be negative")
|
|
330
335
|
rows = _data._rows("williamson2023_depth.csv")
|
|
@@ -334,10 +339,14 @@ def water_type_at_depth(surface_water_type: str, depth_m: float) -> str | None:
|
|
|
334
339
|
f"unknown water type {surface_water_type!r} "
|
|
335
340
|
f"(known: {', '.join(sorted(known))})"
|
|
336
341
|
)
|
|
337
|
-
for
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
own = [r for r in rows if r["surface_water_type"] == surface_water_type]
|
|
343
|
+
deepest = max(float(r["depth_max_m"]) for r in own)
|
|
344
|
+
for row in own:
|
|
345
|
+
top, bottom = float(row["depth_min_m"]), float(row["depth_max_m"])
|
|
346
|
+
# A boundary belongs to the layer below it, except at the bottom of
|
|
347
|
+
# the deepest layer, which has no layer below: 200 m is still
|
|
348
|
+
# inside the paper's profile.
|
|
349
|
+
if top <= depth_m < bottom or depth_m == bottom == deepest:
|
|
341
350
|
return row["water_type"] or None
|
|
342
351
|
# Beyond 200 m the paper makes no statement at all.
|
|
343
352
|
return None
|
|
@@ -222,3 +222,13 @@ def test_water_reddens_nothing_and_blues_everything():
|
|
|
222
222
|
# At the surface the target is neutral; underwater it is not.
|
|
223
223
|
assert np.allclose(surface, surface[0], atol=1e-6)
|
|
224
224
|
assert underwater[2] > underwater[0], "blue should survive better than red"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_wavelengths_must_ascend():
|
|
228
|
+
"""A reversed spectrum used to integrate to a negative XYZ."""
|
|
229
|
+
wl = np.arange(360.0, 831.0, 5.0)
|
|
230
|
+
flat = np.ones_like(wl)
|
|
231
|
+
with pytest.raises(ValueError, match="strictly ascending"):
|
|
232
|
+
jerlov.spectrum_to_xyz(flat[::-1], wl[::-1])
|
|
233
|
+
with pytest.raises(ValueError, match="strictly ascending"):
|
|
234
|
+
jerlov.integrate_response(flat, wl, flat, wl[::-1])
|
|
@@ -48,6 +48,20 @@ def test_boundaries_belong_to_the_layer_below():
|
|
|
48
48
|
assert jerlov.water_type_at_depth("I", 19.999) == "I" # end of 10-20
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def test_the_bottom_of_the_profile_is_inside_it():
|
|
52
|
+
"""200 m closes the deepest layer; there is no layer below to own it."""
|
|
53
|
+
assert jerlov.water_type_at_depth("I", 200.0) == "IB"
|
|
54
|
+
assert jerlov.water_type_at_depth("I", 199.9) == "IB"
|
|
55
|
+
assert jerlov.water_type_at_depth("I", 200.1) is None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_a_depth_that_is_not_a_number_is_refused():
|
|
59
|
+
"""NaN used to fall through to None, which reads as the paper's answer."""
|
|
60
|
+
for bad in (float("nan"), float("inf")):
|
|
61
|
+
with pytest.raises(ValueError, match="finite"):
|
|
62
|
+
jerlov.water_type_at_depth("I", bad)
|
|
63
|
+
|
|
64
|
+
|
|
51
65
|
def test_unknown_type_names_the_alternatives():
|
|
52
66
|
with pytest.raises(KeyError, match="known:"):
|
|
53
67
|
jerlov.water_type_at_depth("IV", 10.0)
|
|
@@ -89,6 +89,34 @@ def test_contrast_decays_with_distance():
|
|
|
89
89
|
assert contrasts[-1] < 0.01
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def test_contrast_follows_the_formula_in_its_docstring():
|
|
93
|
+
"""The docstring used to say the inherent contrast is simply reduced by
|
|
94
|
+
the transmittance. That holds only against a background as bright as the
|
|
95
|
+
water's own veiling light."""
|
|
96
|
+
s = scene()
|
|
97
|
+
target = flat(0.8) * s.downwelling / np.pi
|
|
98
|
+
background = flat(0.1) * s.downwelling / np.pi
|
|
99
|
+
inherent = (target - background) / background
|
|
100
|
+
veil = flat(0.02)
|
|
101
|
+
for r in (0.5, 2.0, 8.0):
|
|
102
|
+
t = s.transmittance(r)
|
|
103
|
+
obs = s.observe(flat(0.8), r, veiling_radiance=veil)
|
|
104
|
+
expected = inherent * background * t / (background * t + veil * (1 - t))
|
|
105
|
+
assert np.allclose(obs.contrast(background), expected)
|
|
106
|
+
# No veiling light: dimming target and background alike leaves the
|
|
107
|
+
# contrast where it was.
|
|
108
|
+
bare = s.observe(flat(0.8), r, veiling_radiance=flat(0.0))
|
|
109
|
+
assert np.allclose(bare.contrast(background), inherent)
|
|
110
|
+
# A background that is the water itself gives the classic C_0 * t.
|
|
111
|
+
water = s.observe(flat(0.8), r, veiling_radiance=background)
|
|
112
|
+
assert np.allclose(water.contrast(background), inherent * t)
|
|
113
|
+
# Darker than the water, it falls faster than that; brighter, slower.
|
|
114
|
+
assert np.all(np.abs(s.observe(flat(0.8), r, veiling_radiance=2 * background)
|
|
115
|
+
.contrast(background)) < np.abs(inherent * t))
|
|
116
|
+
assert np.all(np.abs(s.observe(flat(0.8), r, veiling_radiance=background / 2)
|
|
117
|
+
.contrast(background)) > np.abs(inherent * t))
|
|
118
|
+
|
|
119
|
+
|
|
92
120
|
def test_turbid_water_veils_faster_than_clear():
|
|
93
121
|
clear = scene("IB").observe(flat(), 3.0, veiling_radiance=flat(0.01))
|
|
94
122
|
turbid = scene("5C").observe(flat(), 3.0, veiling_radiance=flat(0.01))
|
|
@@ -96,9 +96,15 @@ def test_the_alternative_type_I_fit_is_reachable():
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def test_the_rows_that_are_not_water_types_are_not_offered():
|
|
99
|
+
"""They used to come back like any type, so `solar_fraction("run_1", z)`
|
|
100
|
+
gave a Jerlov-looking answer for one cruise's run."""
|
|
99
101
|
for key in ("composite_observations", "run_1", "kraus_1972_very_clear"):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
with pytest.raises(KeyError, match="not a Jerlov water type"):
|
|
103
|
+
jerlov.shortwave_parameters(key)
|
|
104
|
+
with pytest.raises(KeyError, match="not a Jerlov water type"):
|
|
105
|
+
jerlov.solar_fraction(key, 10.0)
|
|
106
|
+
p = jerlov.shortwave_parameters(key, include_non_types=True)
|
|
107
|
+
assert p.water_type == "" # reachable when asked for, not a type
|
|
102
108
|
assert "Not a Jerlov type" in p.note
|
|
103
109
|
|
|
104
110
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|