jerlov 0.3.1__tar.gz → 0.3.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 (44) hide show
  1. {jerlov-0.3.1/jerlov.egg-info → jerlov-0.3.2}/PKG-INFO +1 -1
  2. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/__init__.py +1 -1
  3. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/_data.py +37 -7
  4. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/backscattering.py +8 -4
  5. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/colour.py +2 -2
  6. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/water.py +13 -5
  7. {jerlov-0.3.1 → jerlov-0.3.2/jerlov.egg-info}/PKG-INFO +1 -1
  8. {jerlov-0.3.1 → jerlov-0.3.2}/pyproject.toml +1 -1
  9. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_api.py +55 -0
  10. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_backscattering.py +20 -0
  11. {jerlov-0.3.1 → jerlov-0.3.2}/LICENSE +0 -0
  12. {jerlov-0.3.1 → jerlov-0.3.2}/NOTICE +0 -0
  13. {jerlov-0.3.1 → jerlov-0.3.2}/README.md +0 -0
  14. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/__init__.py +0 -0
  15. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/austin1986_kd.csv +0 -0
  16. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/austin1986_model.csv +0 -0
  17. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/boss2001_chi.csv +0 -0
  18. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  19. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/cie_d65.csv +0 -0
  20. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/jerlov1968_kd.csv +0 -0
  21. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  22. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/jerlov1976_kd.csv +0 -0
  23. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/paulson1977_shortwave.csv +0 -0
  24. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/smart2007_b_from_c.csv +0 -0
  25. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/solonenko2015_iop.csv +0 -0
  26. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/williamson2022_iop.csv +0 -0
  27. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/williamson2022_measured.csv +0 -0
  28. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/data/williamson2023_depth.csv +0 -0
  29. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/py.typed +0 -0
  30. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/scene.py +0 -0
  31. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/shortwave.py +0 -0
  32. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov/sources.py +0 -0
  33. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov.egg-info/SOURCES.txt +0 -0
  34. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov.egg-info/dependency_links.txt +0 -0
  35. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov.egg-info/requires.txt +0 -0
  36. {jerlov-0.3.1 → jerlov-0.3.2}/jerlov.egg-info/top_level.txt +0 -0
  37. {jerlov-0.3.1 → jerlov-0.3.2}/setup.cfg +0 -0
  38. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_colour.py +0 -0
  39. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_depth.py +0 -0
  40. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_packaging.py +0 -0
  41. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_quoted_figures.py +0 -0
  42. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_reproduces_papers.py +0 -0
  43. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_scene.py +0 -0
  44. {jerlov-0.3.1 → jerlov-0.3.2}/tests/test_shortwave.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.1
3
+ Version: 0.3.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
@@ -78,4 +78,4 @@ __all__ = [
78
78
  "MissingQuantityError",
79
79
  ]
80
80
 
81
- __version__ = "0.3.1"
81
+ __version__ = "0.3.2"
@@ -8,13 +8,13 @@ silently cleaned up on the way in.
8
8
  from __future__ import annotations
9
9
 
10
10
  import csv
11
+ import os
12
+ import sys
11
13
  from functools import lru_cache
12
14
  from importlib import resources
13
15
 
14
16
  import numpy as np
15
17
 
16
- import numpy as np
17
-
18
18
  #: numpy.trapezoid is the name from NumPy 2.0; before that it was numpy.trapz,
19
19
  #: which 2.0 removed. The package claims to work from NumPy 1.22, so neither
20
20
  #: can be assumed. This lives here rather than in a module about colour so
@@ -23,6 +23,36 @@ import numpy as np
23
23
  #: supported NumPy.
24
24
  trapezoid = getattr(np, "trapezoid", None) or np.trapz
25
25
 
26
+ _PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) + os.sep
27
+
28
+
29
+ def caller_stacklevel() -> int:
30
+ """The ``stacklevel`` that points a warning at the caller's own code.
31
+
32
+ Call it from the function that calls ``warnings.warn``. A fixed number
33
+ is right only for one call path: ``Water.c`` reaches the flag check one
34
+ frame deeper than ``Water.a`` does, and ``Scene`` deeper still, so a
35
+ constant pointed some warnings at this package instead of at the line
36
+ that asked for the value.
37
+ """
38
+ frame = sys._getframe(1)
39
+ level = 1
40
+ while frame is not None and frame.f_code.co_filename.startswith(_PACKAGE_DIR):
41
+ frame = frame.f_back
42
+ level += 1
43
+ return level
44
+
45
+
46
+ def _frozen(array: np.ndarray) -> np.ndarray:
47
+ """Make a cached array read-only.
48
+
49
+ The loaders below are cached, so every caller gets the same array
50
+ objects. Writing into one would silently change the table for every
51
+ later caller in the process.
52
+ """
53
+ array.setflags(write=False)
54
+ return array
55
+
26
56
  #: Values whose ``status`` is one of these should not be used without the
27
57
  #: caller being told. See README sections 1-6.
28
58
  QUESTIONABLE = frozenset({"suspect", "missing", "extrapolated",
@@ -75,8 +105,8 @@ def spectrum(
75
105
  )
76
106
  order = np.argsort(wl)
77
107
  return (
78
- np.asarray(wl, dtype=float)[order],
79
- np.asarray(values, dtype=float)[order],
108
+ _frozen(np.asarray(wl, dtype=float)[order]),
109
+ _frozen(np.asarray(values, dtype=float)[order]),
80
110
  tuple(statuses[i] for i in order),
81
111
  )
82
112
 
@@ -104,7 +134,7 @@ def austin_model() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
104
134
  m = np.array([float(r["M_slope"]) for r in rows])
105
135
  kw = np.array([float(r["Kw_pure_seawater_per_m"]) for r in rows])
106
136
  order = np.argsort(wl)
107
- return wl[order], m[order], kw[order]
137
+ return _frozen(wl[order]), _frozen(m[order]), _frozen(kw[order])
108
138
 
109
139
 
110
140
  @lru_cache(maxsize=None)
@@ -119,5 +149,5 @@ def b_from_c_ratio() -> tuple[np.ndarray, dict[str, np.ndarray]]:
119
149
  for r in rows
120
150
  if r["statistic"] == stat
121
151
  }
122
- out[stat] = np.array([by_wl[w] for w in wl])
123
- return np.array(wl), out
152
+ out[stat] = _frozen(np.array([by_wl[w] for w in wl]))
153
+ return _frozen(np.array(wl)), out
@@ -70,11 +70,15 @@ def pure_water_vsf(angle_deg, wavelength_nm, salinity_psu: float = 37.0):
70
70
  The amplitude carries about 15 percent uncertainty, which the authors give
71
71
  as the agreement between measurement and theory.
72
72
  """
73
- angle = np.atleast_1d(np.asarray(angle_deg, dtype=float))
73
+ angle = np.asarray(angle_deg, dtype=float)
74
74
  amplitude = (1.38 * (np.asarray(wavelength_nm, dtype=float) / 500.0) ** -4.32
75
75
  * (1 + 0.3 * salinity_psu / 37.0) * 1e-4)
76
76
  out = amplitude * (1 + _ratio() * np.cos(np.radians(angle)) ** 2)
77
- return _like_input(np.atleast_1d(out), angle_deg)
77
+ # Angle and wavelength broadcast against each other; the answer is a
78
+ # float only when neither of them was an array.
79
+ if np.ndim(out) == 0:
80
+ return float(out)
81
+ return out
78
82
 
79
83
 
80
84
  def pure_water_backscattering(wavelength_nm, salinity_psu: float = 37.0):
@@ -181,7 +185,7 @@ def bb_from_vsf(beta, angle_deg: float, wavelength_nm, *,
181
185
  "scattering function varies steeply here and a single-angle "
182
186
  "measurement pins bb poorly.",
183
187
  AngleWarning,
184
- stacklevel=2,
188
+ stacklevel=_data.caller_stacklevel(),
185
189
  )
186
190
 
187
191
  scalar = np.ndim(beta) == 0 and np.ndim(wavelength_nm) == 0
@@ -198,7 +202,7 @@ def bb_from_vsf(beta, angle_deg: float, wavelength_nm, *,
198
202
  "so the particle contribution came out negative. Check the "
199
203
  "calibration, the wavelength and the salinity.",
200
204
  AngleWarning,
201
- stacklevel=2,
205
+ stacklevel=_data.caller_stacklevel(),
202
206
  )
203
207
 
204
208
  def shape(values):
@@ -163,7 +163,7 @@ def integrate_response(spectrum, wavelengths, response, response_wavelengths,
163
163
  "The integral is over the overlap and is biased by what was left "
164
164
  "out",
165
165
  CoverageWarning,
166
- stacklevel=2,
166
+ stacklevel=_data.caller_stacklevel(),
167
167
  )
168
168
 
169
169
  resampled = np.stack(
@@ -202,7 +202,7 @@ def xyz_to_srgb(xyz, *, clip: bool = True) -> np.ndarray:
202
202
  "the colour lies outside the sRGB gamut"
203
203
  + (" and has been clipped" if clip else ""),
204
204
  GamutWarning,
205
- stacklevel=2,
205
+ stacklevel=_data.caller_stacklevel(),
206
206
  )
207
207
  if clip:
208
208
  linear = np.clip(linear, 0.0, 1.0)
@@ -75,7 +75,9 @@ class Water:
75
75
  source: Source | None = None,
76
76
  flags: dict[str, tuple[str, ...]] | None = None,
77
77
  ) -> None:
78
- self.wavelengths = np.asarray(wavelengths, dtype=float)
78
+ # Copied, so that neither the caller's arrays nor the packaged tables
79
+ # can change underneath this object, and it cannot change them.
80
+ self.wavelengths = np.array(wavelengths, dtype=float)
79
81
  if self.wavelengths.ndim != 1 or self.wavelengths.size == 0:
80
82
  raise ValueError("wavelengths must be a non-empty 1-D array")
81
83
  if np.any(np.diff(self.wavelengths) <= 0):
@@ -85,7 +87,7 @@ class Water:
85
87
  for key, value in (("a", a), ("b", b), ("Kd", kd)):
86
88
  if value is None:
87
89
  continue
88
- arr = np.asarray(value, dtype=float)
90
+ arr = np.array(value, dtype=float)
89
91
  if arr.shape != self.wavelengths.shape:
90
92
  raise ValueError(f"{key} must have the same shape as wavelengths")
91
93
  self._series[key] = arr
@@ -163,7 +165,7 @@ class Water:
163
165
  + "; ".join(sorted(hit))
164
166
  + ". See the package README for what is known about them.",
165
167
  ProvenanceWarning,
166
- stacklevel=3,
168
+ stacklevel=_data.caller_stacklevel(),
167
169
  )
168
170
 
169
171
  def a(self, wl):
@@ -368,7 +370,7 @@ def kd_spectrum(kd, wavelength_nm: float, at):
368
370
  "Austin & Petzold (1986) reported exactly this problem in "
369
371
  "Jerlov's own type I values.",
370
372
  ProvenanceWarning,
371
- stacklevel=2,
373
+ stacklevel=_data.caller_stacklevel(),
372
374
  )
373
375
  result = np.interp(query, wl, m) / m1 * (kd - kw1) + np.interp(query, wl, kw)
374
376
  return _like_input(result, at)
@@ -395,5 +397,11 @@ def b_from_c(c, wavelength_nm, *, bw, cw, bound: str = "average"):
395
397
  f"wavelength outside the measured range ({wl[0]:g}-{wl[-1]:g} nm)"
396
398
  )
397
399
  ratio = np.interp(query, wl, ratios[bound])
400
+ if np.ndim(wavelength_nm) == 0:
401
+ ratio = ratio[0]
398
402
  result = (np.asarray(c, dtype=float) - cw) * ratio + bw
399
- return _like_input(np.atleast_1d(result), wavelength_nm)
403
+ # c and the wavelength broadcast against each other; the answer is a
404
+ # float only when neither of them was an array.
405
+ if np.ndim(result) == 0:
406
+ return float(result)
407
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.1
3
+ Version: 0.3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jerlov"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Inherent optical properties of Jerlov water types, with provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -213,3 +213,58 @@ def test_interpolating_across_a_gap_still_gives_nan():
213
213
  assert np.isnan(w.a(650)) # a published value that is wrong
214
214
  assert np.isnan(w.a(660)) # and interpolation across it
215
215
  assert np.isnan(w.a(675))
216
+
217
+
218
+ def test_b_from_c_keeps_every_c_at_a_single_wavelength():
219
+ """An array of c at one wavelength used to come back as its first value."""
220
+ c = np.array([0.5, 1.0, 2.0])
221
+ out = jerlov.b_from_c(c, 488.0, bw=0.003, cw=0.02)
222
+ assert out.shape == (3,)
223
+ for value, single in zip(out, c):
224
+ assert value == pytest.approx(
225
+ jerlov.b_from_c(float(single), 488.0, bw=0.003, cw=0.02)
226
+ )
227
+ assert isinstance(jerlov.b_from_c(0.5, 488.0, bw=0.003, cw=0.02), float)
228
+
229
+
230
+ def test_a_returned_water_cannot_corrupt_the_packaged_table():
231
+ """The loaders are cached; a caller's in-place edit must stay local."""
232
+ w = jerlov.water("III")
233
+ before = jerlov.water("III").wavelengths.copy()
234
+ w.wavelengths *= 2.0
235
+ assert np.array_equal(jerlov.water("III").wavelengths, before)
236
+
237
+
238
+ def test_the_cached_tables_are_read_only():
239
+ from jerlov import _data
240
+
241
+ wl, m, kw = _data.austin_model()
242
+ with pytest.raises(ValueError):
243
+ wl[0] = 0.0
244
+ wl, values, _ = _data.spectrum(
245
+ "williamson2022_iop.csv", "III", "a", "value_per_m"
246
+ )
247
+ with pytest.raises(ValueError):
248
+ values[0] = 0.0
249
+
250
+
251
+ def test_measurements_are_copied_not_shared():
252
+ wl = np.array([400.0, 500.0, 600.0])
253
+ a = np.array([0.1, 0.2, 0.3])
254
+ mine = Water.from_measurements(wl, a=a)
255
+ a[1] = 99.0
256
+ assert mine.a(500.0) == pytest.approx(0.2)
257
+
258
+
259
+ @pytest.mark.parametrize("method", ["kd", "c"])
260
+ def test_a_provenance_warning_points_at_the_callers_line(method):
261
+ """Not at water.py: `c` reaches the check one frame deeper than `kd`."""
262
+ source = "jerlov1976" if method == "kd" else "williamson2022"
263
+ w = jerlov.water("9C" if method == "kd" else "III", source=source)
264
+ query = 349.5 if method == "kd" else 305.0
265
+ with warnings.catch_warnings(record=True) as caught:
266
+ warnings.simplefilter("always")
267
+ getattr(w, method)(query)
268
+ flagged = [c for c in caught if issubclass(c.category, ProvenanceWarning)]
269
+ assert flagged
270
+ assert all(c.filename == __file__ for c in flagged)
@@ -165,3 +165,23 @@ def test_it_says_nothing_about_a_jerlov_type():
165
165
  """The point of DATA.md section 10 has not quietly gone away."""
166
166
  with pytest.raises(jerlov.MissingQuantityError):
167
167
  jerlov.water("III").bb(532)
168
+
169
+
170
+ def test_one_angle_many_wavelengths_gives_one_value_per_wavelength():
171
+ """A scalar angle used to return the first wavelength's value alone."""
172
+ nms = [450.0, 532.0, 700.0]
173
+ vsf = jerlov.pure_water_vsf(124.0, nms)
174
+ assert np.shape(vsf) == (3,)
175
+ for value, nm in zip(vsf, nms):
176
+ assert value == pytest.approx(jerlov.pure_water_vsf(124.0, nm))
177
+
178
+
179
+ def test_an_array_of_wavelengths_matches_one_call_per_wavelength():
180
+ """The water term must be subtracted at each wavelength, not the first."""
181
+ nms = [450.0, 532.0, 700.0]
182
+ together = jerlov.bb_from_vsf([0.001] * 3, 124.0, nms)
183
+ for k, nm in enumerate(nms):
184
+ alone = jerlov.bb_from_vsf(0.001, 124.0, nm)
185
+ assert together.bb[k] == pytest.approx(alone.bb)
186
+ assert together.particulate[k] == pytest.approx(alone.particulate)
187
+ assert together.water[k] == pytest.approx(alone.water)
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