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.
Files changed (52) hide show
  1. {jerlov-0.9.0/jerlov.egg-info → jerlov-0.9.2}/PKG-INFO +6 -2
  2. {jerlov-0.9.0 → jerlov-0.9.2}/README.md +5 -1
  3. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/__init__.py +1 -1
  4. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/par.py +34 -17
  5. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/water.py +4 -2
  6. {jerlov-0.9.0 → jerlov-0.9.2/jerlov.egg-info}/PKG-INFO +6 -2
  7. {jerlov-0.9.0 → jerlov-0.9.2}/pyproject.toml +1 -1
  8. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_classify.py +2 -0
  9. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_packaging.py +19 -0
  10. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_par.py +37 -0
  11. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_quoted_figures.py +136 -0
  12. {jerlov-0.9.0 → jerlov-0.9.2}/LICENSE +0 -0
  13. {jerlov-0.9.0 → jerlov-0.9.2}/NOTICE +0 -0
  14. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/_data.py +0 -0
  15. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/backscattering.py +0 -0
  16. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/colour.py +0 -0
  17. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/__init__.py +0 -0
  18. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/austin1986_kd.csv +0 -0
  19. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/austin1986_model.csv +0 -0
  20. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/boss2001_chi.csv +0 -0
  21. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  22. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/cie_d65.csv +0 -0
  23. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1968_kd.csv +0 -0
  24. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  25. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/jerlov1976_kd.csv +0 -0
  26. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/paulson1977_shortwave.csv +0 -0
  27. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/petzold1972_stations.csv +0 -0
  28. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/petzold1972_vsf.csv +0 -0
  29. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/smart2007_b_from_c.csv +0 -0
  30. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/solonenko2015_iop.csv +0 -0
  31. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_aw.csv +0 -0
  32. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_iop.csv +0 -0
  33. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2022_measured.csv +0 -0
  34. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/data/williamson2023_depth.csv +0 -0
  35. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/petzold.py +0 -0
  36. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/py.typed +0 -0
  37. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/scene.py +0 -0
  38. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/shortwave.py +0 -0
  39. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov/sources.py +0 -0
  40. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/SOURCES.txt +0 -0
  41. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/dependency_links.txt +0 -0
  42. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/requires.txt +0 -0
  43. {jerlov-0.9.0 → jerlov-0.9.2}/jerlov.egg-info/top_level.txt +0 -0
  44. {jerlov-0.9.0 → jerlov-0.9.2}/setup.cfg +0 -0
  45. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_api.py +0 -0
  46. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_backscattering.py +0 -0
  47. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_colour.py +0 -0
  48. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_depth.py +0 -0
  49. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_petzold.py +0 -0
  50. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_reproduces_papers.py +0 -0
  51. {jerlov-0.9.0 → jerlov-0.9.2}/tests/test_scene.py +0 -0
  52. {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.0
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
- Eight scripts, each aimed at a different reader; `examples/README.md` says
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
- Eight scripts, each aimed at a different reader; `examples/README.md` says
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.
@@ -104,4 +104,4 @@ __all__ = [
104
104
  "MissingQuantityError",
105
105
  ]
106
106
 
107
- __version__ = "0.9.0"
107
+ __version__ = "0.9.2"
@@ -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
- return self.par / self.surface_par
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
- raise ValueError("downwelling irradiance cannot be negative")
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(wl))
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(wl)
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 else wl * 0
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(wl), dtype=float)
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
- if np.any(kd_values < 0):
254
- raise ValueError("kd cannot be negative")
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
- near = np.zeros(wl.shape, dtype=bool)
263
- near[np.searchsorted(wl, lo, side="right") - 1:
264
- np.searchsorted(wl, hi, side="left") + 1] = True
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, wl, _photons(surface, wl, unit))
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, wl, optical_depth(z))
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
- raise ValueError("Kd must be positive (NaN marks a gap)")
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.0
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
- Eight scripts, each aimed at a different reader; `examples/README.md` says
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jerlov"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Inherent optical properties of Jerlov water types, with provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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