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.
Files changed (44) hide show
  1. {jerlov-0.3.3/jerlov.egg-info → jerlov-0.4.0}/PKG-INFO +1 -1
  2. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/__init__.py +1 -1
  3. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/colour.py +16 -0
  4. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/scene.py +21 -2
  5. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/shortwave.py +15 -1
  6. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/water.py +13 -4
  7. {jerlov-0.3.3 → jerlov-0.4.0/jerlov.egg-info}/PKG-INFO +1 -1
  8. {jerlov-0.3.3 → jerlov-0.4.0}/pyproject.toml +1 -1
  9. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_colour.py +10 -0
  10. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_depth.py +14 -0
  11. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_scene.py +28 -0
  12. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_shortwave.py +8 -2
  13. {jerlov-0.3.3 → jerlov-0.4.0}/LICENSE +0 -0
  14. {jerlov-0.3.3 → jerlov-0.4.0}/NOTICE +0 -0
  15. {jerlov-0.3.3 → jerlov-0.4.0}/README.md +0 -0
  16. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/_data.py +0 -0
  17. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/backscattering.py +0 -0
  18. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/__init__.py +0 -0
  19. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/austin1986_kd.csv +0 -0
  20. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/austin1986_model.csv +0 -0
  21. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/boss2001_chi.csv +0 -0
  22. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  23. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/cie_d65.csv +0 -0
  24. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1968_kd.csv +0 -0
  25. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  26. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/jerlov1976_kd.csv +0 -0
  27. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/paulson1977_shortwave.csv +0 -0
  28. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/smart2007_b_from_c.csv +0 -0
  29. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/solonenko2015_iop.csv +0 -0
  30. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2022_iop.csv +0 -0
  31. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2022_measured.csv +0 -0
  32. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/data/williamson2023_depth.csv +0 -0
  33. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/py.typed +0 -0
  34. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov/sources.py +0 -0
  35. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/SOURCES.txt +0 -0
  36. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/dependency_links.txt +0 -0
  37. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/requires.txt +0 -0
  38. {jerlov-0.3.3 → jerlov-0.4.0}/jerlov.egg-info/top_level.txt +0 -0
  39. {jerlov-0.3.3 → jerlov-0.4.0}/setup.cfg +0 -0
  40. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_api.py +0 -0
  41. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_backscattering.py +0 -0
  42. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_packaging.py +0 -0
  43. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_quoted_figures.py +0 -0
  44. {jerlov-0.3.3 → jerlov-0.4.0}/tests/test_reproduces_papers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Inherent optical properties of Jerlov water types, with provenance
5
5
  Author: T. Ishibashi
6
6
  License-Expression: Apache-2.0
@@ -78,4 +78,4 @@ __all__ = [
78
78
  "MissingQuantityError",
79
79
  ]
80
80
 
81
- __version__ = "0.3.3"
81
+ __version__ = "0.4.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
- The path radiance is common to target and background and cancels, so
124
- this is the inherent contrast reduced by the transmittance.
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) -> ShortwaveParameters:
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 row in rows:
338
- if row["surface_water_type"] != surface_water_type:
339
- continue
340
- if float(row["depth_min_m"]) <= depth_m < float(row["depth_max_m"]):
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Inherent optical properties of Jerlov water types, with provenance
5
5
  Author: T. Ishibashi
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jerlov"
7
- version = "0.3.3"
7
+ version = "0.4.0"
8
8
  description = "Inherent optical properties of Jerlov water types, with provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- p = jerlov.shortwave_parameters(key)
101
- assert p.water_type == "" # reachable by key, but not a type
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