jerlov 0.2.2__tar.gz → 0.3.1__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 (45) hide show
  1. {jerlov-0.2.2/jerlov.egg-info → jerlov-0.3.1}/PKG-INFO +34 -2
  2. {jerlov-0.2.2 → jerlov-0.3.1}/README.md +33 -1
  3. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/__init__.py +15 -1
  4. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/_data.py +10 -0
  5. jerlov-0.3.1/jerlov/backscattering.py +215 -0
  6. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/colour.py +2 -3
  7. jerlov-0.3.1/jerlov/data/boss2001_chi.csv +13 -0
  8. jerlov-0.3.1/jerlov/py.typed +0 -0
  9. {jerlov-0.2.2 → jerlov-0.3.1/jerlov.egg-info}/PKG-INFO +34 -2
  10. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov.egg-info/SOURCES.txt +5 -0
  11. {jerlov-0.2.2 → jerlov-0.3.1}/pyproject.toml +2 -1
  12. jerlov-0.3.1/tests/test_backscattering.py +167 -0
  13. jerlov-0.3.1/tests/test_packaging.py +275 -0
  14. jerlov-0.3.1/tests/test_quoted_figures.py +192 -0
  15. jerlov-0.2.2/tests/test_packaging.py +0 -142
  16. {jerlov-0.2.2 → jerlov-0.3.1}/LICENSE +0 -0
  17. {jerlov-0.2.2 → jerlov-0.3.1}/NOTICE +0 -0
  18. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/__init__.py +0 -0
  19. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/austin1986_kd.csv +0 -0
  20. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/austin1986_model.csv +0 -0
  21. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  22. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/cie_d65.csv +0 -0
  23. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/jerlov1968_kd.csv +0 -0
  24. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  25. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/jerlov1976_kd.csv +0 -0
  26. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/paulson1977_shortwave.csv +0 -0
  27. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/smart2007_b_from_c.csv +0 -0
  28. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/solonenko2015_iop.csv +0 -0
  29. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/williamson2022_iop.csv +0 -0
  30. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/williamson2022_measured.csv +0 -0
  31. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/data/williamson2023_depth.csv +0 -0
  32. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/scene.py +0 -0
  33. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/shortwave.py +0 -0
  34. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/sources.py +0 -0
  35. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov/water.py +0 -0
  36. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov.egg-info/dependency_links.txt +0 -0
  37. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov.egg-info/requires.txt +0 -0
  38. {jerlov-0.2.2 → jerlov-0.3.1}/jerlov.egg-info/top_level.txt +0 -0
  39. {jerlov-0.2.2 → jerlov-0.3.1}/setup.cfg +0 -0
  40. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_api.py +0 -0
  41. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_colour.py +0 -0
  42. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_depth.py +0 -0
  43. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_reproduces_papers.py +0 -0
  44. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_scene.py +0 -0
  45. {jerlov-0.2.2 → jerlov-0.3.1}/tests/test_shortwave.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Inherent optical properties of Jerlov water types, with provenance
5
5
  Author: T. Ishibashi
6
6
  License-Expression: Apache-2.0
@@ -203,6 +203,25 @@ Two silent errors are checked rather than assumed:
203
203
  - **Gamut.** Underwater colours often fall outside sRGB. Clipping changes
204
204
  them, so `GamutWarning` says so.
205
205
 
206
+ ## If you measured backscattering
207
+
208
+ `Water.bb` refuses to guess a backscattering ratio. If you have an instrument
209
+ you do not have to:
210
+
211
+ ```python
212
+ r = jerlov.bb_from_vsf(beta=0.0021, angle_deg=140, wavelength_nm=532)
213
+ r.bb, r.particulate, r.water # 1/m
214
+ r.chi_p, r.quoted_error_percent # 1.18, 3.5
215
+ ```
216
+
217
+ Boss & Pegau (2001). The pure sea water terms are analytic and are checked
218
+ against the definition of bb rather than transcribed; only the particle
219
+ conversion is tabulated. Angles outside 90-170 degrees are refused, and 170
220
+ warns: its quoted spread is **34.8 percent** against 3-6 in the middle.
221
+
222
+ This gives the bb of the water your instrument was in. It still gives no bb
223
+ for a Jerlov water type, because nothing does.
224
+
206
225
  ## The type changes with depth
207
226
 
208
227
  The classification is defined on the top 10 m, but clarity does not stay put.
@@ -272,13 +291,26 @@ made.
272
291
  ## Provenance and design
273
292
 
274
293
  `DATA.md` records, for every shipped table, where it came from, what was
275
- verified, and what is known to be wrong with it. Seventeen entries are
294
+ verified, and what is known to be wrong with it. Eighteen entries are
276
295
  documented there: eight confirmed defects in the source literature, three
277
296
  questions the first edition of Jerlov settled, and the rest notes.
278
297
 
279
298
  `DECISIONS.md` records why the package is shaped the way it is, including the
280
299
  alternatives that were rejected and why.
281
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
+
282
314
  ## Licence
283
315
 
284
316
  Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
@@ -178,6 +178,25 @@ Two silent errors are checked rather than assumed:
178
178
  - **Gamut.** Underwater colours often fall outside sRGB. Clipping changes
179
179
  them, so `GamutWarning` says so.
180
180
 
181
+ ## If you measured backscattering
182
+
183
+ `Water.bb` refuses to guess a backscattering ratio. If you have an instrument
184
+ you do not have to:
185
+
186
+ ```python
187
+ r = jerlov.bb_from_vsf(beta=0.0021, angle_deg=140, wavelength_nm=532)
188
+ r.bb, r.particulate, r.water # 1/m
189
+ r.chi_p, r.quoted_error_percent # 1.18, 3.5
190
+ ```
191
+
192
+ Boss & Pegau (2001). The pure sea water terms are analytic and are checked
193
+ against the definition of bb rather than transcribed; only the particle
194
+ conversion is tabulated. Angles outside 90-170 degrees are refused, and 170
195
+ warns: its quoted spread is **34.8 percent** against 3-6 in the middle.
196
+
197
+ This gives the bb of the water your instrument was in. It still gives no bb
198
+ for a Jerlov water type, because nothing does.
199
+
181
200
  ## The type changes with depth
182
201
 
183
202
  The classification is defined on the top 10 m, but clarity does not stay put.
@@ -247,13 +266,26 @@ made.
247
266
  ## Provenance and design
248
267
 
249
268
  `DATA.md` records, for every shipped table, where it came from, what was
250
- verified, and what is known to be wrong with it. Seventeen entries are
269
+ verified, and what is known to be wrong with it. Eighteen entries are
251
270
  documented there: eight confirmed defects in the source literature, three
252
271
  questions the first edition of Jerlov settled, and the rest notes.
253
272
 
254
273
  `DECISIONS.md` records why the package is shaped the way it is, including the
255
274
  alternatives that were rejected and why.
256
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
+
257
289
  ## Licence
258
290
 
259
291
  Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
@@ -4,6 +4,14 @@ Every coefficient carries its source, and values that a published table got
4
4
  wrong are flagged rather than quietly repaired.
5
5
  """
6
6
 
7
+ from .backscattering import (
8
+ AngleWarning,
9
+ Backscattering,
10
+ bb_from_vsf,
11
+ particulate_chi,
12
+ pure_water_backscattering,
13
+ pure_water_vsf,
14
+ )
7
15
  from .colour import (
8
16
  CoverageWarning,
9
17
  GamutWarning,
@@ -57,6 +65,12 @@ __all__ = [
57
65
  "water_type_at_depth",
58
66
  "shortwave_parameters",
59
67
  "solar_fraction",
68
+ "bb_from_vsf",
69
+ "particulate_chi",
70
+ "pure_water_vsf",
71
+ "pure_water_backscattering",
72
+ "Backscattering",
73
+ "AngleWarning",
60
74
  "ShortwaveParameters",
61
75
  "kd_spectrum",
62
76
  "b_from_c",
@@ -64,4 +78,4 @@ __all__ = [
64
78
  "MissingQuantityError",
65
79
  ]
66
80
 
67
- __version__ = "0.2.2"
81
+ __version__ = "0.3.1"
@@ -13,6 +13,16 @@ from importlib import resources
13
13
 
14
14
  import numpy as np
15
15
 
16
+ import numpy as np
17
+
18
+ #: numpy.trapezoid is the name from NumPy 2.0; before that it was numpy.trapz,
19
+ #: which 2.0 removed. The package claims to work from NumPy 1.22, so neither
20
+ #: can be assumed. This lives here rather than in a module about colour so
21
+ #: that anything integrating over a grid finds it: putting it in colour.py
22
+ #: meant two later modules reached for np.trapezoid and broke the oldest
23
+ #: supported NumPy.
24
+ trapezoid = getattr(np, "trapezoid", None) or np.trapz
25
+
16
26
  #: Values whose ``status`` is one of these should not be used without the
17
27
  #: caller being told. See README sections 1-6.
18
28
  QUESTIONABLE = frozenset({"suspect", "missing", "extrapolated",
@@ -0,0 +1,215 @@
1
+ """From a backscattering sensor's reading to the backscattering coefficient.
2
+
3
+ `Water.bb` refuses to guess a backscattering ratio, because the Jerlov
4
+ classification does not determine one. If you have an instrument, you do not
5
+ have to guess: a HydroScat, an ECO-BB or a VSF meter measures the volume
6
+ scattering function at one angle in the backward hemisphere, and there is a
7
+ published relation from that to bb.
8
+
9
+ bb = 2 pi chi_p(theta) [beta(theta) - beta_w(theta)] + bb_w
10
+
11
+ Boss & Pegau (2001) Eq. (10). The water terms are analytic, from Morel's
12
+ formula for the volume scattering function of pure sea water; only chi_p is
13
+ tabulated, from 41 measured scattering functions.
14
+
15
+ This does not tell you the bb of a Jerlov water type. Nothing does. It tells
16
+ you the bb of the water your instrument was in.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ import warnings
23
+ from dataclasses import dataclass
24
+
25
+ import numpy as np
26
+
27
+ from . import _data
28
+ from .water import _like_input
29
+
30
+ CITATION = (
31
+ "Boss, E. and Pegau, W. S. (2001), 'Relationship of light scattering at "
32
+ "an angle in the backward direction to the backscattering coefficient', "
33
+ "Appl. Opt. 40, 5503-5507"
34
+ )
35
+ DOI = "10.1364/AO.40.005503"
36
+
37
+ #: Depolarisation ratio of pure sea water. Morel gives a range of 0.07 to
38
+ #: 0.11 and suggests 0.09, which is what Boss & Pegau use.
39
+ DEPOLARISATION_RATIO = 0.09
40
+
41
+ #: The angles the tabulation covers. Outside this the relation is not defined.
42
+ MIN_ANGLE_DEG, MAX_ANGLE_DEG = 90.0, 170.0
43
+
44
+ #: Above this the tabulated error exceeds 10 percent and a warning is raised.
45
+ _ERROR_WARN_PERCENT = 10.0
46
+
47
+
48
+ class AngleWarning(UserWarning):
49
+ """The conversion is poorly constrained at the requested angle.
50
+
51
+ The shape of the volume scattering function varies most steeply near 90
52
+ and 180 degrees, so a single-angle measurement there pins bb least well.
53
+ Both Boss & Pegau (2001) and Maffione & Dana (1997) recommend 110 to 160
54
+ degrees.
55
+ """
56
+
57
+
58
+ def _ratio() -> float:
59
+ return (1 - DEPOLARISATION_RATIO) / (1 + DEPOLARISATION_RATIO)
60
+
61
+
62
+ def pure_water_vsf(angle_deg, wavelength_nm, salinity_psu: float = 37.0):
63
+ """Volume scattering function of pure sea water, 1/(m sr).
64
+
65
+ Morel's formula as given by Boss & Pegau Eqs. (4) and (5)::
66
+
67
+ A(lambda, S) = 1.38 (lambda / 500 nm)^-4.32 (1 + 0.3 S / 37) 1e-4
68
+ beta_w(theta) = A [1 + cos^2(theta) (1 - delta) / (1 + delta)]
69
+
70
+ The amplitude carries about 15 percent uncertainty, which the authors give
71
+ as the agreement between measurement and theory.
72
+ """
73
+ angle = np.atleast_1d(np.asarray(angle_deg, dtype=float))
74
+ amplitude = (1.38 * (np.asarray(wavelength_nm, dtype=float) / 500.0) ** -4.32
75
+ * (1 + 0.3 * salinity_psu / 37.0) * 1e-4)
76
+ out = amplitude * (1 + _ratio() * np.cos(np.radians(angle)) ** 2)
77
+ return _like_input(np.atleast_1d(out), angle_deg)
78
+
79
+
80
+ def pure_water_backscattering(wavelength_nm, salinity_psu: float = 37.0):
81
+ """``bb_w``, the backscattering coefficient of pure sea water, 1/m.
82
+
83
+ Obtained by integrating :func:`pure_water_vsf` over the backward
84
+ hemisphere, which for this angular shape is analytic.
85
+ """
86
+ r = _ratio()
87
+ amplitude = (1.38 * (np.asarray(wavelength_nm, dtype=float) / 500.0) ** -4.32
88
+ * (1 + 0.3 * salinity_psu / 37.0) * 1e-4)
89
+ # integral of (1 + r cos^2) sin over 90..180 degrees is 1 + r/3
90
+ out = 2.0 * math.pi * amplitude * (1 + r / 3.0)
91
+ return _like_input(np.atleast_1d(out), wavelength_nm)
92
+
93
+
94
+ def _chi_table():
95
+ rows = [r for r in _data._rows("boss2001_chi.csv")
96
+ if r["source"] == "boss2001"]
97
+ angles = np.array([float(r["angle_deg"]) for r in rows])
98
+ order = np.argsort(angles)
99
+ return (angles[order],
100
+ np.array([float(rows[i]["chi"]) for i in order]),
101
+ np.array([float(rows[i]["percent_error"]) for i in order]))
102
+
103
+
104
+ def particulate_chi(angle_deg):
105
+ """``chi_p`` at one angle, interpolated within Boss & Pegau Table 1.
106
+
107
+ Refuses outside 90 to 170 degrees, the range their measurements cover.
108
+ """
109
+ angle = float(angle_deg)
110
+ if not MIN_ANGLE_DEG <= angle <= MAX_ANGLE_DEG:
111
+ raise ValueError(
112
+ f"chi_p is tabulated from {MIN_ANGLE_DEG:g} to {MAX_ANGLE_DEG:g} "
113
+ f"degrees and {angle:g} is outside that. Boss & Pegau made no "
114
+ "measurement beyond 170 degrees, and this package does not "
115
+ "extrapolate."
116
+ )
117
+ angles, values, _ = _chi_table()
118
+ return float(np.interp(angle, angles, values))
119
+
120
+
121
+ def _quoted_error(angle_deg: float) -> float:
122
+ angles, _, errors = _chi_table()
123
+ return float(np.interp(angle_deg, angles, errors))
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class Backscattering:
128
+ """``bb`` recovered from a single-angle measurement."""
129
+
130
+ bb: float | np.ndarray
131
+ """Total backscattering coefficient, 1/m."""
132
+ particulate: float | np.ndarray
133
+ """The particle contribution, ``bb - bb_w``."""
134
+ water: float | np.ndarray
135
+ """``bb_w``, from Morel's formula rather than from your instrument."""
136
+ angle_deg: float
137
+ chi_p: float
138
+ quoted_error_percent: float
139
+ """Boss & Pegau's own spread for chi_p at this angle. Not a total error."""
140
+
141
+ def __repr__(self) -> str: # pragma: no cover - convenience only
142
+ return (f"<Backscattering bb={np.mean(self.bb):.5f} 1/m at "
143
+ f"{self.angle_deg:g} deg, chi_p={self.chi_p:.3f} "
144
+ f"+-{self.quoted_error_percent:.1f}%>")
145
+
146
+
147
+ def bb_from_vsf(beta, angle_deg: float, wavelength_nm, *,
148
+ salinity_psu: float = 37.0) -> Backscattering:
149
+ """Convert a measured volume scattering function to ``bb``.
150
+
151
+ Parameters
152
+ ----------
153
+ beta:
154
+ The measured volume scattering function at ``angle_deg``, in
155
+ 1/(m sr). This is what the instrument reports once calibrated; it
156
+ includes scattering by the water itself.
157
+ angle_deg:
158
+ The instrument's nominal scattering angle. 90 to 170 degrees.
159
+ wavelength_nm, salinity_psu:
160
+ Needed for the pure sea water terms, which are subtracted and then
161
+ added back as ``bb_w``.
162
+
163
+ Notes
164
+ -----
165
+ Boss & Pegau's water-removal route, their Eq. (10). Removing the water
166
+ first matters because chi differs between water and particles everywhere
167
+ except near 118 degrees, where the two curves cross; that is why
168
+ instruments cluster near 120.
169
+
170
+ The quoted error on the result is the spread of chi_p alone. The amplitude
171
+ of the water term carries a further 15 percent, and your instrument's own
172
+ calibration is on top of both.
173
+ """
174
+ angle = float(angle_deg)
175
+ chi_p = particulate_chi(angle)
176
+ error = _quoted_error(angle)
177
+ if error > _ERROR_WARN_PERCENT:
178
+ warnings.warn(
179
+ f"chi_p at {angle:g} degrees carries a quoted spread of "
180
+ f"{error:.1f}%, against 3-6% between 100 and 160 degrees. The "
181
+ "scattering function varies steeply here and a single-angle "
182
+ "measurement pins bb poorly.",
183
+ AngleWarning,
184
+ stacklevel=2,
185
+ )
186
+
187
+ scalar = np.ndim(beta) == 0 and np.ndim(wavelength_nm) == 0
188
+ beta = np.atleast_1d(np.asarray(beta, dtype=float))
189
+ if np.any(beta < 0):
190
+ raise ValueError("a volume scattering function cannot be negative")
191
+ beta_w = np.atleast_1d(pure_water_vsf(angle, wavelength_nm, salinity_psu))
192
+ bb_w = np.atleast_1d(pure_water_backscattering(wavelength_nm, salinity_psu))
193
+
194
+ particulate = 2.0 * math.pi * chi_p * (beta - beta_w)
195
+ if np.any(particulate < 0):
196
+ warnings.warn(
197
+ "the measured scattering is below that of pure sea water alone, "
198
+ "so the particle contribution came out negative. Check the "
199
+ "calibration, the wavelength and the salinity.",
200
+ AngleWarning,
201
+ stacklevel=2,
202
+ )
203
+
204
+ def shape(values):
205
+ values = np.atleast_1d(values)
206
+ return float(values[0]) if scalar else values
207
+
208
+ return Backscattering(
209
+ bb=shape(particulate + bb_w),
210
+ particulate=shape(particulate),
211
+ water=shape(np.broadcast_to(bb_w, particulate.shape)),
212
+ angle_deg=angle,
213
+ chi_p=chi_p,
214
+ quoted_error_percent=error,
215
+ )
@@ -22,9 +22,8 @@ import numpy as np
22
22
 
23
23
  from . import _data
24
24
 
25
- # numpy.trapezoid is the name from NumPy 2.0; before that it was numpy.trapz.
26
- # The package claims to work from NumPy 1.22, so it must not assume either.
27
- _trapezoid = getattr(np, "trapezoid", None) or np.trapz
25
+ #: Kept as a module-level name because tests and examples import it.
26
+ _trapezoid = _data.trapezoid
28
27
 
29
28
  #: sRGB primaries and white point, IEC 61966-2-1.
30
29
  SRGB_PRIMARIES = np.array([
@@ -0,0 +1,13 @@
1
+ source,angle_deg,chi,quantity,percent_error,status,note
2
+ boss2001,90,0.71,chi_p,4.3,ok,"the water contribution to the measured signal is largest here, so removing it matters most"
3
+ boss2001,100,0.9,chi_p,2.6,ok,
4
+ boss2001,110,1.03,chi_p,3.1,ok,
5
+ boss2001,120,1.12,chi_p,4.2,ok,
6
+ boss2001,130,1.17,chi_p,3.3,ok,
7
+ boss2001,140,1.18,chi_p,3.5,ok,
8
+ boss2001,150,1.13,chi_p,4.2,ok,
9
+ boss2001,160,1.0,chi_p,6.4,ok,
10
+ boss2001,170,0.62,chi_p,34.8,suspect,"the volume scattering function varies most steeply here, and no measurement in the set went beyond 170 deg"
11
+ oishi1990,120,1.14,chi,,other_definition,"chi including water, quoted by Maffione & Dana (1997) from Oishi (1990), Appl. Opt. 29, 4658"
12
+ oishi1990,140,1.08,chi,,other_definition,as above
13
+ maffione1997,140,1.08,chi,9.0,other_definition,"chi including water, Maffione & Dana (1997), Appl. Opt. 36, 6057, from Mie computations; the 9 percent is their standard deviation"
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Inherent optical properties of Jerlov water types, with provenance
5
5
  Author: T. Ishibashi
6
6
  License-Expression: Apache-2.0
@@ -203,6 +203,25 @@ Two silent errors are checked rather than assumed:
203
203
  - **Gamut.** Underwater colours often fall outside sRGB. Clipping changes
204
204
  them, so `GamutWarning` says so.
205
205
 
206
+ ## If you measured backscattering
207
+
208
+ `Water.bb` refuses to guess a backscattering ratio. If you have an instrument
209
+ you do not have to:
210
+
211
+ ```python
212
+ r = jerlov.bb_from_vsf(beta=0.0021, angle_deg=140, wavelength_nm=532)
213
+ r.bb, r.particulate, r.water # 1/m
214
+ r.chi_p, r.quoted_error_percent # 1.18, 3.5
215
+ ```
216
+
217
+ Boss & Pegau (2001). The pure sea water terms are analytic and are checked
218
+ against the definition of bb rather than transcribed; only the particle
219
+ conversion is tabulated. Angles outside 90-170 degrees are refused, and 170
220
+ warns: its quoted spread is **34.8 percent** against 3-6 in the middle.
221
+
222
+ This gives the bb of the water your instrument was in. It still gives no bb
223
+ for a Jerlov water type, because nothing does.
224
+
206
225
  ## The type changes with depth
207
226
 
208
227
  The classification is defined on the top 10 m, but clarity does not stay put.
@@ -272,13 +291,26 @@ made.
272
291
  ## Provenance and design
273
292
 
274
293
  `DATA.md` records, for every shipped table, where it came from, what was
275
- verified, and what is known to be wrong with it. Seventeen entries are
294
+ verified, and what is known to be wrong with it. Eighteen entries are
276
295
  documented there: eight confirmed defects in the source literature, three
277
296
  questions the first edition of Jerlov settled, and the rest notes.
278
297
 
279
298
  `DECISIONS.md` records why the package is shaped the way it is, including the
280
299
  alternatives that were rejected and why.
281
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
+
282
314
  ## Licence
283
315
 
284
316
  Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
@@ -4,7 +4,9 @@ README.md
4
4
  pyproject.toml
5
5
  jerlov/__init__.py
6
6
  jerlov/_data.py
7
+ jerlov/backscattering.py
7
8
  jerlov/colour.py
9
+ jerlov/py.typed
8
10
  jerlov/scene.py
9
11
  jerlov/shortwave.py
10
12
  jerlov/sources.py
@@ -17,6 +19,7 @@ jerlov.egg-info/top_level.txt
17
19
  jerlov/data/__init__.py
18
20
  jerlov/data/austin1986_kd.csv
19
21
  jerlov/data/austin1986_model.csv
22
+ jerlov/data/boss2001_chi.csv
20
23
  jerlov/data/cie1931_2deg_cmf.csv
21
24
  jerlov/data/cie_d65.csv
22
25
  jerlov/data/jerlov1968_kd.csv
@@ -29,9 +32,11 @@ jerlov/data/williamson2022_iop.csv
29
32
  jerlov/data/williamson2022_measured.csv
30
33
  jerlov/data/williamson2023_depth.csv
31
34
  tests/test_api.py
35
+ tests/test_backscattering.py
32
36
  tests/test_colour.py
33
37
  tests/test_depth.py
34
38
  tests/test_packaging.py
39
+ tests/test_quoted_figures.py
35
40
  tests/test_reproduces_papers.py
36
41
  tests/test_scene.py
37
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.2.2"
7
+ version = "0.3.1"
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"]
@@ -0,0 +1,167 @@
1
+ """Turning a single-angle measurement into bb, and the limits of doing so."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import warnings
7
+
8
+ import numpy as np
9
+ import pytest
10
+
11
+ import jerlov
12
+ from jerlov._data import trapezoid
13
+ from jerlov.backscattering import AngleWarning, DEPOLARISATION_RATIO
14
+
15
+
16
+ # -- the analytic half ----------------------------------------------------
17
+
18
+
19
+ def test_pure_water_backscattering_is_the_integral_of_its_own_vsf():
20
+ """bb_w must be what Eq. (3) gives when applied to Eq. (4)."""
21
+ for nm in (440.0, 532.0, 650.0):
22
+ theta = np.radians(np.linspace(90.0, 180.0, 90001))
23
+ vsf = jerlov.pure_water_vsf(np.degrees(theta), nm)
24
+ integrated = 2 * math.pi * float(trapezoid(vsf * np.sin(theta), theta))
25
+ assert jerlov.pure_water_backscattering(nm) == pytest.approx(
26
+ integrated, rel=1e-6
27
+ )
28
+
29
+
30
+ def test_chi_w_gives_the_same_answer_at_every_angle():
31
+ """The water conversion is exact, so it cannot depend on where you look."""
32
+ r = (1 - DEPOLARISATION_RATIO) / (1 + DEPOLARISATION_RATIO)
33
+ nm = 532.0
34
+ expected = jerlov.pure_water_backscattering(nm)
35
+ for angle in (90.0, 110.0, 130.0, 150.0, 170.0):
36
+ chi_w = (1 + r / 3) / (1 + r * math.cos(math.radians(angle)) ** 2)
37
+ recovered = 2 * math.pi * jerlov.pure_water_vsf(angle, nm) * chi_w
38
+ assert recovered == pytest.approx(expected, rel=1e-9)
39
+
40
+
41
+ def test_pure_water_scattering_rises_steeply_towards_the_blue():
42
+ """Morel's exponent is -4.32, close to Rayleigh's -4."""
43
+ blue = jerlov.pure_water_backscattering(400.0)
44
+ red = jerlov.pure_water_backscattering(700.0)
45
+ assert blue / red == pytest.approx((700.0 / 400.0) ** 4.32, rel=1e-9)
46
+
47
+
48
+ def test_fresher_water_scatters_less():
49
+ assert (jerlov.pure_water_backscattering(532.0, salinity_psu=0.0)
50
+ < jerlov.pure_water_backscattering(532.0, salinity_psu=37.0))
51
+
52
+
53
+ # -- the tabulated half ---------------------------------------------------
54
+
55
+
56
+ def test_chi_p_matches_the_published_table():
57
+ published = {90: 0.71, 100: 0.90, 110: 1.03, 120: 1.12, 130: 1.17,
58
+ 140: 1.18, 150: 1.13, 160: 1.00, 170: 0.62}
59
+ for angle, value in published.items():
60
+ assert jerlov.particulate_chi(angle) == pytest.approx(value)
61
+
62
+
63
+ def test_chi_p_peaks_in_the_middle_of_the_backward_hemisphere():
64
+ """Which is why both papers recommend 110-160 degrees."""
65
+ angles = np.arange(90.0, 171.0, 1.0)
66
+ values = [jerlov.particulate_chi(a) for a in angles]
67
+ assert 130.0 <= angles[int(np.argmax(values))] <= 145.0
68
+
69
+
70
+ def test_chi_w_and_chi_p_cross_near_118_degrees():
71
+ """The paper's reason that instruments settled on 120."""
72
+ r = (1 - DEPOLARISATION_RATIO) / (1 + DEPOLARISATION_RATIO)
73
+ angles = np.linspace(90.0, 170.0, 8001)
74
+ gap = [
75
+ (1 + r / 3) / (1 + r * math.cos(math.radians(a)) ** 2)
76
+ - jerlov.particulate_chi(a)
77
+ for a in angles
78
+ ]
79
+ crossing = angles[int(np.argmin(np.abs(gap)))]
80
+ assert 114.0 < crossing < 122.0, crossing
81
+
82
+
83
+ def test_angles_outside_the_measurements_are_refused():
84
+ for angle in (89.0, 171.0, 180.0, 0.0):
85
+ with pytest.raises(ValueError, match="90 to 170"):
86
+ jerlov.particulate_chi(angle)
87
+
88
+
89
+ # -- the conversion -------------------------------------------------------
90
+
91
+
92
+ def test_it_reproduces_the_published_equation():
93
+ beta, angle, nm = 0.0021, 140.0, 532.0
94
+ result = jerlov.bb_from_vsf(beta, angle, nm)
95
+ chi_p = jerlov.particulate_chi(angle)
96
+ expected = (2 * math.pi * chi_p
97
+ * (beta - jerlov.pure_water_vsf(angle, nm))
98
+ + jerlov.pure_water_backscattering(nm))
99
+ assert result.bb == pytest.approx(expected)
100
+ assert result.particulate + result.water == pytest.approx(result.bb)
101
+
102
+
103
+ def test_water_alone_gives_the_water_answer():
104
+ """Measuring pure sea water must return bb_w and no particles."""
105
+ nm = 532.0
106
+ beta = jerlov.pure_water_vsf(130.0, nm)
107
+ result = jerlov.bb_from_vsf(beta, 130.0, nm)
108
+ assert result.particulate == pytest.approx(0.0, abs=1e-12)
109
+ assert result.bb == pytest.approx(jerlov.pure_water_backscattering(nm))
110
+
111
+
112
+ def test_more_scattering_gives_more_backscattering():
113
+ previous = -1.0
114
+ for beta in (0.0005, 0.001, 0.002, 0.005):
115
+ value = jerlov.bb_from_vsf(beta, 140.0, 532.0).bb
116
+ assert value > previous
117
+ previous = value
118
+
119
+
120
+ def test_a_typical_coastal_reading_lands_in_the_reported_range():
121
+ """A sanity anchor, not a validation. Coastal bb/b is put at 0.015-0.03."""
122
+ result = jerlov.bb_from_vsf(0.0021, 140.0, 532.0, salinity_psu=35.0)
123
+ assert 0.005 < result.bb < 0.05
124
+
125
+
126
+ def test_170_degrees_warns_about_its_own_error():
127
+ with pytest.warns(AngleWarning, match="34.8%"):
128
+ jerlov.bb_from_vsf(0.002, 170.0, 532.0)
129
+
130
+
131
+ def test_the_recommended_angles_do_not_warn():
132
+ with warnings.catch_warnings():
133
+ warnings.simplefilter("error")
134
+ for angle in (110.0, 120.0, 130.0, 140.0, 150.0, 160.0):
135
+ jerlov.bb_from_vsf(0.002, angle, 532.0)
136
+
137
+
138
+ def test_a_reading_below_pure_water_warns_rather_than_passing():
139
+ with pytest.warns(AngleWarning, match="below that of pure sea water"):
140
+ jerlov.bb_from_vsf(1e-6, 140.0, 400.0)
141
+
142
+
143
+ def test_negative_scattering_is_refused():
144
+ with pytest.raises(ValueError, match="cannot be negative"):
145
+ jerlov.bb_from_vsf(-0.001, 140.0, 532.0)
146
+
147
+
148
+ def test_the_result_carries_the_quoted_error():
149
+ result = jerlov.bb_from_vsf(0.002, 140.0, 532.0)
150
+ assert result.chi_p == pytest.approx(1.18)
151
+ assert result.quoted_error_percent == pytest.approx(3.5)
152
+ assert result.angle_deg == 140.0
153
+
154
+
155
+ def test_scalar_in_scalar_out():
156
+ assert isinstance(jerlov.bb_from_vsf(0.002, 140.0, 532.0).bb, float)
157
+ array = jerlov.bb_from_vsf([0.001, 0.002], 140.0, 532.0).bb
158
+ assert isinstance(array, np.ndarray) and array.shape == (2,)
159
+
160
+
161
+ # -- what it does not do --------------------------------------------------
162
+
163
+
164
+ def test_it_says_nothing_about_a_jerlov_type():
165
+ """The point of DATA.md section 10 has not quietly gone away."""
166
+ with pytest.raises(jerlov.MissingQuantityError):
167
+ jerlov.water("III").bb(532)