jerlov 0.9.0__tar.gz → 0.9.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.9.0/jerlov.egg-info → jerlov-0.9.2}/PKG-INFO +6 -2
- {jerlov-0.9.0 → jerlov-0.9.2}/README.md +5 -1
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/__init__.py +1 -1
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/par.py +34 -17
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/water.py +4 -2
- {jerlov-0.9.0 → jerlov-0.9.2/jerlov.egg-info}/PKG-INFO +6 -2
- {jerlov-0.9.0 → jerlov-0.9.2}/pyproject.toml +1 -1
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_classify.py +2 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_packaging.py +19 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_par.py +37 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_quoted_figures.py +136 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/LICENSE +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/NOTICE +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/_data.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/backscattering.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/colour.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/__init__.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/austin1986_kd.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/austin1986_model.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/boss2001_chi.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/cie_d65.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1968_kd.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1976_kd.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/paulson1977_shortwave.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/petzold1972_stations.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/petzold1972_vsf.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/smart2007_b_from_c.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/solonenko2015_iop.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_aw.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_iop.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_measured.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2023_depth.csv +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/petzold.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/py.typed +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/scene.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/shortwave.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/sources.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/SOURCES.txt +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/dependency_links.txt +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/requires.txt +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/top_level.txt +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/setup.cfg +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_api.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_backscattering.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_colour.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_depth.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_petzold.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_reproduces_papers.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_scene.py +0 -0
- {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_shortwave.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jerlov
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -375,9 +375,13 @@ python examples/what_an_eye_sees.py any spectral sensitivity
|
|
|
375
375
|
python examples/light_at_depth.py light through the depth profile
|
|
376
376
|
python examples/measured_scattering.py Petzold's measured bb/b
|
|
377
377
|
python examples/what_is_in_the_water.py absorption without the water
|
|
378
|
+
python examples/euphotic_zone.py PAR and the 1 percent depth
|
|
379
|
+
python examples/optical_link.py laser range for links and lidar
|
|
380
|
+
python examples/seagrass_depth_limit.py light at the bottom, by requirement
|
|
381
|
+
python examples/isolumes.py light levels and vertical migration
|
|
378
382
|
```
|
|
379
383
|
|
|
380
|
-
|
|
384
|
+
Twelve scripts, each aimed at a different reader; `examples/README.md` says
|
|
381
385
|
which to start with. CI runs all of them on every push, so an example that has
|
|
382
386
|
stopped working is a failed build. Every one ends with the assumptions it
|
|
383
387
|
made.
|
|
@@ -345,9 +345,13 @@ python examples/what_an_eye_sees.py any spectral sensitivity
|
|
|
345
345
|
python examples/light_at_depth.py light through the depth profile
|
|
346
346
|
python examples/measured_scattering.py Petzold's measured bb/b
|
|
347
347
|
python examples/what_is_in_the_water.py absorption without the water
|
|
348
|
+
python examples/euphotic_zone.py PAR and the 1 percent depth
|
|
349
|
+
python examples/optical_link.py laser range for links and lidar
|
|
350
|
+
python examples/seagrass_depth_limit.py light at the bottom, by requirement
|
|
351
|
+
python examples/isolumes.py light levels and vertical migration
|
|
348
352
|
```
|
|
349
353
|
|
|
350
|
-
|
|
354
|
+
Twelve scripts, each aimed at a different reader; `examples/README.md` says
|
|
351
355
|
which to start with. CI runs all of them on every push, so an example that has
|
|
352
356
|
stopped working is a failed build. Every one ends with the assumptions it
|
|
353
357
|
made.
|
|
@@ -88,8 +88,11 @@ class ParProfile:
|
|
|
88
88
|
|
|
89
89
|
@property
|
|
90
90
|
def fraction(self) -> np.ndarray:
|
|
91
|
-
"""``PAR(z) / PAR(0)``, 0 to 1.
|
|
92
|
-
|
|
91
|
+
"""``PAR(z) / PAR(0)``, 0 to 1. NaN if there is no light at the
|
|
92
|
+
surface."""
|
|
93
|
+
return np.divide(self.par, self.surface_par,
|
|
94
|
+
out=np.full_like(self.par, np.nan),
|
|
95
|
+
where=self.surface_par != 0)
|
|
93
96
|
|
|
94
97
|
def depth_of_fraction(self, fraction: float = 0.01) -> float:
|
|
95
98
|
"""The depth, in m, at which PAR falls to ``fraction`` of its surface value.
|
|
@@ -205,8 +208,19 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
|
|
|
205
208
|
if surface.shape != wl.shape:
|
|
206
209
|
raise ValueError(
|
|
207
210
|
"surface_downwelling must have the same shape as wavelengths")
|
|
208
|
-
if np.any(surface < 0):
|
|
209
|
-
|
|
211
|
+
if np.any(surface < 0) or np.any(np.isinf(surface)):
|
|
212
|
+
# NaN, a gap, passes here and is refused below only if PAR rests on it.
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"downwelling irradiance must be finite and not negative")
|
|
215
|
+
|
|
216
|
+
# Only the samples that PAR rests on: those inside 400-700 nm and the
|
|
217
|
+
# one either side of each end. Kd is evaluated on these alone, so that
|
|
218
|
+
# a spectrum running past the Kd table, or a flagged Kd beyond 700 nm,
|
|
219
|
+
# neither refuses nor warns about light that PAR does not count.
|
|
220
|
+
near = np.zeros(wl.shape, dtype=bool)
|
|
221
|
+
near[np.searchsorted(wl, lo, side="right") - 1:
|
|
222
|
+
np.searchsorted(wl, hi, side="left") + 1] = True
|
|
223
|
+
sub_wl = wl[near]
|
|
210
224
|
|
|
211
225
|
depths = np.array(depths_m, dtype=float)
|
|
212
226
|
if not np.all(np.isfinite(depths)):
|
|
@@ -227,21 +241,22 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
|
|
|
227
241
|
f"the descent stops at {deepest:g} m, above the deepest depth "
|
|
228
242
|
f"asked for ({depths.max():g} m)"
|
|
229
243
|
)
|
|
230
|
-
layers = [(top, bottom, water(t, kd.source).kd(
|
|
244
|
+
layers = [(top, bottom, water(t, kd.source).kd(sub_wl))
|
|
231
245
|
for top, bottom, t in kd.layers]
|
|
232
246
|
kd_from = (f"a descent from Jerlov {kd.surface_water_type} "
|
|
233
247
|
f"({kd.source})")
|
|
234
248
|
|
|
235
249
|
def optical_depth(z):
|
|
236
|
-
total = np.zeros_like(
|
|
250
|
+
total = np.zeros_like(sub_wl)
|
|
237
251
|
for top, bottom, values in layers:
|
|
238
252
|
total += values * min(max(z - top, 0.0), bottom - top)
|
|
239
253
|
return total
|
|
240
254
|
|
|
241
|
-
kd_values = np.vstack([v for _, _, v in layers]) if layers
|
|
255
|
+
kd_values = (np.vstack([v for _, _, v in layers]) if layers
|
|
256
|
+
else np.zeros_like(sub_wl))
|
|
242
257
|
else:
|
|
243
258
|
if isinstance(kd, Water):
|
|
244
|
-
kd_values = np.asarray(kd.kd(
|
|
259
|
+
kd_values = np.asarray(kd.kd(sub_wl), dtype=float)
|
|
245
260
|
kd_from = f"Jerlov {kd.name}" if kd.name else "the given water"
|
|
246
261
|
if kd.source is not None:
|
|
247
262
|
kd_from += f" ({kd.source.key})"
|
|
@@ -250,8 +265,11 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
|
|
|
250
265
|
kd_from = "the given Kd"
|
|
251
266
|
if kd_values.shape != wl.shape:
|
|
252
267
|
raise ValueError("kd must have the same shape as wavelengths")
|
|
253
|
-
|
|
254
|
-
|
|
268
|
+
kd_values = kd_values[near]
|
|
269
|
+
if np.any(kd_values < 0) or np.any(np.isinf(kd_values)):
|
|
270
|
+
# inf times a depth of 0 is NaN, so PAR at the surface came out
|
|
271
|
+
# NaN with only a NumPy RuntimeWarning.
|
|
272
|
+
raise ValueError("kd must be finite and not negative")
|
|
255
273
|
|
|
256
274
|
def optical_depth(z):
|
|
257
275
|
return kd_values * z
|
|
@@ -259,18 +277,17 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
|
|
|
259
277
|
# The band: every sample inside 400-700 nm, plus the two ends.
|
|
260
278
|
inside = (wl > lo) & (wl < hi)
|
|
261
279
|
band = np.concatenate(([lo], wl[inside], [hi]))
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for name, values in (("surface_downwelling", surface), ("Kd", kd_values)):
|
|
266
|
-
if np.any(np.isnan(np.atleast_2d(values)[:, near])):
|
|
280
|
+
for name, values in (("surface_downwelling", surface[near]),
|
|
281
|
+
("Kd", kd_values)):
|
|
282
|
+
if np.any(np.isnan(values)):
|
|
267
283
|
raise MissingQuantityError(
|
|
268
284
|
f"{name} has a gap between {lo:g} and {hi:g} nm, so PAR, "
|
|
269
285
|
"which integrates over all of it, is not determined"
|
|
270
286
|
)
|
|
271
|
-
surface_q = np.interp(band,
|
|
287
|
+
surface_q = np.interp(band, sub_wl,
|
|
288
|
+
_photons(surface[near], sub_wl, unit))
|
|
272
289
|
|
|
273
290
|
def tau(z):
|
|
274
|
-
return np.interp(band,
|
|
291
|
+
return np.interp(band, sub_wl, optical_depth(z))
|
|
275
292
|
|
|
276
293
|
return ParProfile(depths, band, surface_q, tau, deepest, kd_from)
|
|
@@ -620,8 +620,10 @@ def classify_kd(wavelengths, kd, *, source: str = "jerlov1976"
|
|
|
620
620
|
measured = np.array(kd, dtype=float, ndmin=1)
|
|
621
621
|
if wl.ndim != 1 or measured.shape != wl.shape:
|
|
622
622
|
raise ValueError("kd must be one value per wavelength")
|
|
623
|
-
if np.any(measured <= 0):
|
|
624
|
-
|
|
623
|
+
if np.any(measured <= 0) or np.any(np.isinf(measured)):
|
|
624
|
+
# inf would otherwise fail isfinite below and be dropped as if it
|
|
625
|
+
# were a gap, leaving the other wavelengths to decide alone.
|
|
626
|
+
raise ValueError("Kd must be positive and finite (NaN marks a gap)")
|
|
625
627
|
|
|
626
628
|
waters = {t: water(t, source) for t in src.water_types}
|
|
627
629
|
# Raises for a wavelength outside the source's range. The flags are
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jerlov
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -375,9 +375,13 @@ python examples/what_an_eye_sees.py any spectral sensitivity
|
|
|
375
375
|
python examples/light_at_depth.py light through the depth profile
|
|
376
376
|
python examples/measured_scattering.py Petzold's measured bb/b
|
|
377
377
|
python examples/what_is_in_the_water.py absorption without the water
|
|
378
|
+
python examples/euphotic_zone.py PAR and the 1 percent depth
|
|
379
|
+
python examples/optical_link.py laser range for links and lidar
|
|
380
|
+
python examples/seagrass_depth_limit.py light at the bottom, by requirement
|
|
381
|
+
python examples/isolumes.py light levels and vertical migration
|
|
378
382
|
```
|
|
379
383
|
|
|
380
|
-
|
|
384
|
+
Twelve scripts, each aimed at a different reader; `examples/README.md` says
|
|
381
385
|
which to start with. CI runs all of them on every push, so an example that has
|
|
382
386
|
stopped working is a failed build. Every one ends with the assumptions it
|
|
383
387
|
made.
|
|
@@ -83,6 +83,8 @@ def test_what_cannot_be_compared_is_refused():
|
|
|
83
83
|
jerlov.classify_kd([250.0, 500.0], [1.0, 0.05])
|
|
84
84
|
with pytest.raises(ValueError, match="positive"):
|
|
85
85
|
jerlov.classify_kd([450.0, 500.0], [0.0, 0.05])
|
|
86
|
+
with pytest.raises(ValueError, match="finite"):
|
|
87
|
+
jerlov.classify_kd([450.0, 500.0], [np.inf, 0.05])
|
|
86
88
|
with pytest.raises(ValueError, match="one value per wavelength"):
|
|
87
89
|
jerlov.classify_kd(WL, [0.05])
|
|
88
90
|
with pytest.raises(jerlov.MissingQuantityError, match="no wavelength"):
|
|
@@ -370,3 +370,22 @@ def test_every_optional_extra_is_used_by_something():
|
|
|
370
370
|
assert re.search(rf"^\s*(import|from) {name}\b", code, re.M), (
|
|
371
371
|
f"extra {extra!r} installs {dist}, which nothing imports"
|
|
372
372
|
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
WORD_COUNTS = {"Eight": 8, "Nine": 9, "Ten": 10, "Eleven": 11, "Twelve": 12}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@source_tree
|
|
379
|
+
def test_the_READMEs_count_the_examples():
|
|
380
|
+
"""Both said "Eight scripts", and a ninth was about to make them wrong."""
|
|
381
|
+
scripts = len(list((ROOT / "examples").glob("*.py")))
|
|
382
|
+
for readme in (ROOT / "README.md", ROOT / "examples" / "README.md"):
|
|
383
|
+
match = re.search(r"\b(\w+) scripts[,.]", readme.read_text())
|
|
384
|
+
assert match, f"{readme.name} no longer counts the examples"
|
|
385
|
+
assert WORD_COUNTS.get(match.group(1)) == scripts, (
|
|
386
|
+
f"{readme.relative_to(ROOT)} says {match.group(1)} scripts; "
|
|
387
|
+
f"examples/ has {scripts}"
|
|
388
|
+
)
|
|
389
|
+
for script in (ROOT / "examples").glob("*.py"):
|
|
390
|
+
assert script.name in readme.read_text(), (
|
|
391
|
+
f"{readme.relative_to(ROOT)} never mentions {script.name}")
|
|
@@ -107,6 +107,17 @@ def test_what_par_cannot_be_computed_from_is_refused():
|
|
|
107
107
|
for bad in (-1.0, np.nan, np.inf):
|
|
108
108
|
with pytest.raises(ValueError):
|
|
109
109
|
jerlov.par_profile(FLAT, WL, bad, kd=FLAT, unit="photons")
|
|
110
|
+
infinite = FLAT.copy()
|
|
111
|
+
infinite[0] = np.inf
|
|
112
|
+
with pytest.raises(ValueError, match="finite"):
|
|
113
|
+
jerlov.par_profile(infinite, WL, 0.0, kd=FLAT, unit="photons")
|
|
114
|
+
with pytest.raises(ValueError, match="finite"):
|
|
115
|
+
jerlov.par_profile(FLAT, WL, 0.0, kd=infinite, unit="photons")
|
|
116
|
+
with warnings.catch_warnings():
|
|
117
|
+
warnings.simplefilter("error")
|
|
118
|
+
dark = jerlov.par_profile(FLAT * 0, WL, [0.0, 1.0], kd=FLAT,
|
|
119
|
+
unit="photons")
|
|
120
|
+
assert np.all(np.isnan(dark.fraction))
|
|
110
121
|
with pytest.raises(ValueError, match="other wavelengths"):
|
|
111
122
|
jerlov.par_profile(FLAT, WL, 0.0, unit="photons",
|
|
112
123
|
kd=jerlov.descend("I", 20.0, WL[::2]))
|
|
@@ -128,3 +139,29 @@ def test_the_profile_keeps_the_shape_of_the_depths_and_cannot_be_changed():
|
|
|
128
139
|
depths[0, 0] = 99.0
|
|
129
140
|
assert p.depths_m[0, 0] == 0.0
|
|
130
141
|
assert repr(p).startswith("<ParProfile surface 300 umol m-2 s-1, 4 depths")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_light_outside_the_band_neither_refuses_nor_warns():
|
|
145
|
+
"""A radiometer's 300-800 nm spectrum was refused because Jerlov (1976)
|
|
146
|
+
stops at 715 nm, and Kd flagged at 705 nm warned, though PAR counts
|
|
147
|
+
neither."""
|
|
148
|
+
w = jerlov.water("II", source="jerlov1976")
|
|
149
|
+
wide = np.arange(300.0, 801.0, 5.0)
|
|
150
|
+
band = np.arange(400.0, 701.0, 5.0)
|
|
151
|
+
with warnings.catch_warnings():
|
|
152
|
+
warnings.simplefilter("error")
|
|
153
|
+
p_wide = jerlov.par_profile(np.ones_like(wide), wide, [0.0, 10.0],
|
|
154
|
+
kd=w, unit="energy")
|
|
155
|
+
p_band = jerlov.par_profile(np.ones_like(band), band, [0.0, 10.0],
|
|
156
|
+
kd=w, unit="energy")
|
|
157
|
+
just_past = np.arange(380.0, 711.0, 5.0)
|
|
158
|
+
jerlov.par_profile(np.ones_like(just_past), just_past, 10.0, kd=w,
|
|
159
|
+
unit="energy")
|
|
160
|
+
assert p_wide.par == pytest.approx(p_band.par, rel=1e-12)
|
|
161
|
+
# A gap outside the band does not matter either; one inside does.
|
|
162
|
+
kd = np.full_like(wide, 0.1)
|
|
163
|
+
kd[0] = np.nan
|
|
164
|
+
assert jerlov.par_profile(FLAT, WL, 1.0, kd=np.full_like(WL, 0.1),
|
|
165
|
+
unit="photons").par == pytest.approx(
|
|
166
|
+
jerlov.par_profile(np.ones_like(wide), wide, 1.0, kd=kd,
|
|
167
|
+
unit="photons").par)
|
|
@@ -411,3 +411,139 @@ def test_README_figures_for_classify_kd():
|
|
|
411
411
|
warnings.simplefilter("ignore", jerlov.ProvenanceWarning)
|
|
412
412
|
result = jerlov.classify_kd(band, jerlov.kd_spectrum(0.060, 490, band))
|
|
413
413
|
assert repr(result) == quoted
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_figures_for_the_euphotic_zone_example():
|
|
417
|
+
"""examples/README.md: IA with IB close behind; 154 m by the shortcut,
|
|
418
|
+
101 m layer by layer; 87 percent violet-blue there; the editions agree
|
|
419
|
+
at Kd(490) = 0.030 and not at 0.045."""
|
|
420
|
+
band = np.arange(400.0, 701.0, 5.0)
|
|
421
|
+
surface = np.interp(band, *jerlov.d65())
|
|
422
|
+
measured = jerlov.kd_spectrum(0.030, 490.0, band)
|
|
423
|
+
r = jerlov.classify_kd(band, measured)
|
|
424
|
+
assert [t for t, _ in r.distances[:2]] == ["IA", "IB"]
|
|
425
|
+
assert round(math.log(100) / 0.030) == 154
|
|
426
|
+
d = jerlov.descend("IA", 200.0, band)
|
|
427
|
+
assert d.layers[3][2] == "IB" and d.layers[2][2] == "IA"
|
|
428
|
+
layered = jerlov.par_profile(surface, band, 0.0, kd=d, unit="energy")
|
|
429
|
+
z1 = layered.depth_of_fraction()
|
|
430
|
+
assert round(z1) == 101
|
|
431
|
+
one_kd = jerlov.par_profile(surface, band, 0.0, unit="energy",
|
|
432
|
+
kd=jerlov.water("IA", source="jerlov1976"))
|
|
433
|
+
spectral = jerlov.par_profile(surface, band, 0.0, kd=measured,
|
|
434
|
+
unit="energy")
|
|
435
|
+
# "The shortcut is the deepest."
|
|
436
|
+
assert math.log(100) / 0.030 > max(one_kd.depth_of_fraction(),
|
|
437
|
+
spectral.depth_of_fraction(), z1)
|
|
438
|
+
blue = jerlov.par_profile(surface * (band < 500), band, [0.0, z1],
|
|
439
|
+
kd=d, unit="energy").par
|
|
440
|
+
total = jerlov.par_profile(surface, band, [0.0, z1], kd=d,
|
|
441
|
+
unit="energy").par
|
|
442
|
+
assert round(100 * blue[1] / total[1]) == 87
|
|
443
|
+
editions = {s: jerlov.classify_kd(band, measured, source=s).water_type
|
|
444
|
+
for s in ("jerlov1976", "jerlov1968", "austin1986")}
|
|
445
|
+
assert set(editions.values()) == {"IA"}
|
|
446
|
+
other = jerlov.kd_spectrum(0.045, 490.0, band)
|
|
447
|
+
assert (jerlov.classify_kd(band, other).water_type,
|
|
448
|
+
jerlov.classify_kd(band, other, source="jerlov1968").water_type
|
|
449
|
+
) == ("IB", "II")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
BAND = np.arange(400.0, 701.0, 5.0)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _d65():
|
|
456
|
+
return np.interp(BAND, *jerlov.d65())
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_figures_for_the_optical_link_example():
|
|
460
|
+
"""4.34 dB per attenuation length, 9.2 in 40 dB; clearest at 494 nm in
|
|
461
|
+
IB and 564 nm in 5C; the sources' ranges at 532 nm differ by up to 2.9,
|
|
462
|
+
in III."""
|
|
463
|
+
per_length = 10 * math.log10(math.e)
|
|
464
|
+
assert round(per_length, 2) == 4.34
|
|
465
|
+
assert round(40 / per_length, 1) == 9.2
|
|
466
|
+
grid = np.arange(420.0, 651.0, 1.0)
|
|
467
|
+
for t, nm in (("IB", 494), ("5C", 564)):
|
|
468
|
+
assert grid[np.argmin(jerlov.water(t).c(grid))] == nm
|
|
469
|
+
ratios = {}
|
|
470
|
+
for t in ("IB", "II", "III", "1C", "3C", "5C"):
|
|
471
|
+
a = jerlov.water(t).c(532.0)
|
|
472
|
+
b = jerlov.water(t, source="solonenko2015").c(532.0)
|
|
473
|
+
ratios[t] = max(a, b) / min(a, b)
|
|
474
|
+
assert max(ratios, key=ratios.get) == "III"
|
|
475
|
+
assert round(ratios["III"], 1) == 2.9
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _layered_limit(water_type, fraction):
|
|
479
|
+
for depth in np.arange(200.0, 0.0, -10.0):
|
|
480
|
+
try:
|
|
481
|
+
d = jerlov.descend(water_type, depth, BAND)
|
|
482
|
+
except jerlov.MissingQuantityError:
|
|
483
|
+
continue
|
|
484
|
+
p = jerlov.par_profile(_d65(), BAND, 0.0, kd=d, unit="energy")
|
|
485
|
+
return p.depth_of_fraction(fraction)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def test_figures_for_the_seagrass_example():
|
|
489
|
+
"""At 11 percent: 13.4 m in III, 2.7 m in 9C, 3.3 m lost from 1C to 3C;
|
|
490
|
+
the top-metre Kd(PAR) puts III at 9.4 m. 5 percent in 3C goes as deep
|
|
491
|
+
as 11 percent in 1C."""
|
|
492
|
+
limit = {t: _layered_limit(t, 0.11) for t in ("III", "1C", "3C", "9C")}
|
|
493
|
+
assert round(limit["III"], 1) == 13.4
|
|
494
|
+
assert round(limit["9C"], 1) == 2.7
|
|
495
|
+
assert round(limit["1C"] - limit["3C"], 1) == 3.3
|
|
496
|
+
top = jerlov.par_profile(_d65(), BAND, 1.0, unit="energy",
|
|
497
|
+
kd=jerlov.water("III", source="jerlov1976"))
|
|
498
|
+
assert round(math.log(1 / 0.11) / -math.log(top.fraction), 1) == 9.4
|
|
499
|
+
assert round(_layered_limit("3C", 0.05), 1) == round(limit["1C"], 1)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_figures_for_the_isolumes_example():
|
|
503
|
+
"""Steps of 58 and 61 m in I, 106 and 114 m with one Kd; steps grow with
|
|
504
|
+
depth in every oceanic type; 1e-3 lies 9 times shallower in 9C than in
|
|
505
|
+
II; the light peaks at 475 nm below, 550 nm at the surface."""
|
|
506
|
+
def steps(kd, n):
|
|
507
|
+
p = jerlov.par_profile(_d65(), BAND, 0.0, kd=kd, unit="energy")
|
|
508
|
+
z = []
|
|
509
|
+
for level in (1e-1, 1e-2, 1e-3, 1e-4, 1e-5)[:n]:
|
|
510
|
+
try:
|
|
511
|
+
z.append(p.depth_of_fraction(level))
|
|
512
|
+
except jerlov.MissingQuantityError:
|
|
513
|
+
break
|
|
514
|
+
return np.diff(z)
|
|
515
|
+
|
|
516
|
+
deepest = {"I": 200.0, "IA": 200.0, "IB": 200.0, "II": 200.0,
|
|
517
|
+
"III": 170.0}
|
|
518
|
+
layered_i = steps(jerlov.descend("I", 200.0, BAND), 3)
|
|
519
|
+
assert [round(s) for s in layered_i] == [58, 61]
|
|
520
|
+
flat_i = steps(jerlov.water("I", source="jerlov1976"), 3)
|
|
521
|
+
assert [round(s) for s in flat_i] == [106, 114]
|
|
522
|
+
for t, depth in deepest.items():
|
|
523
|
+
s = steps(jerlov.descend(t, depth, BAND), 5)
|
|
524
|
+
assert np.all(np.diff(s) > 0), t
|
|
525
|
+
milli = {t: jerlov.par_profile(_d65(), BAND, 0.0, unit="energy",
|
|
526
|
+
kd=jerlov.water(t, source="jerlov1976")
|
|
527
|
+
).depth_of_fraction(1e-3)
|
|
528
|
+
for t in ("II", "9C")}
|
|
529
|
+
assert round(milli["II"] / milli["9C"]) == 9
|
|
530
|
+
photons = _d65() * BAND
|
|
531
|
+
assert BAND[np.argmax(photons)] == 550.0
|
|
532
|
+
t23 = jerlov.descend("II", 23.0, BAND).transmittance
|
|
533
|
+
assert BAND[np.argmax(photons * t23)] == 475.0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_figures_for_the_turbid_seagrass_section():
|
|
537
|
+
"""Kd(PAR) to the 11 percent depth is above 0.27 1/m from 3C on and
|
|
538
|
+
below it to 1C; there 33.3 percent puts the limit 2.1 to 2.2 times
|
|
539
|
+
shallower than 11 percent."""
|
|
540
|
+
kd_column, ratio = {}, {}
|
|
541
|
+
for t in ("II", "III", "1C", "3C", "5C", "7C", "9C"):
|
|
542
|
+
z11 = _layered_limit(t, 0.11)
|
|
543
|
+
kd_column[t] = math.log(1 / 0.11) / z11
|
|
544
|
+
if kd_column[t] > 0.27:
|
|
545
|
+
ratio[t] = z11 / _layered_limit(t, 0.333)
|
|
546
|
+
assert set(ratio) == {"3C", "5C", "7C", "9C"}
|
|
547
|
+
assert kd_column["1C"] < 0.27
|
|
548
|
+
assert (round(min(ratio.values()), 1), round(max(ratio.values()), 1)) \
|
|
549
|
+
== (2.1, 2.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
|
|
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
|