jerlov 0.3.2__tar.gz → 0.3.3__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.2/jerlov.egg-info → jerlov-0.3.3}/PKG-INFO +1 -1
  2. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/__init__.py +1 -1
  3. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/_data.py +9 -1
  4. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/sources.py +1 -1
  5. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/water.py +101 -28
  6. {jerlov-0.3.2 → jerlov-0.3.3/jerlov.egg-info}/PKG-INFO +1 -1
  7. {jerlov-0.3.2 → jerlov-0.3.3}/pyproject.toml +1 -1
  8. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_api.py +56 -2
  9. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_packaging.py +31 -0
  10. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_reproduces_papers.py +2 -2
  11. {jerlov-0.3.2 → jerlov-0.3.3}/LICENSE +0 -0
  12. {jerlov-0.3.2 → jerlov-0.3.3}/NOTICE +0 -0
  13. {jerlov-0.3.2 → jerlov-0.3.3}/README.md +0 -0
  14. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/backscattering.py +0 -0
  15. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/colour.py +0 -0
  16. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/__init__.py +0 -0
  17. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/austin1986_kd.csv +0 -0
  18. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/austin1986_model.csv +0 -0
  19. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/boss2001_chi.csv +0 -0
  20. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  21. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/cie_d65.csv +0 -0
  22. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/jerlov1968_kd.csv +0 -0
  23. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  24. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/jerlov1976_kd.csv +0 -0
  25. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/paulson1977_shortwave.csv +0 -0
  26. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/smart2007_b_from_c.csv +0 -0
  27. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/solonenko2015_iop.csv +0 -0
  28. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/williamson2022_iop.csv +0 -0
  29. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/williamson2022_measured.csv +0 -0
  30. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/data/williamson2023_depth.csv +0 -0
  31. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/py.typed +0 -0
  32. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/scene.py +0 -0
  33. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov/shortwave.py +0 -0
  34. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov.egg-info/SOURCES.txt +0 -0
  35. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov.egg-info/dependency_links.txt +0 -0
  36. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov.egg-info/requires.txt +0 -0
  37. {jerlov-0.3.2 → jerlov-0.3.3}/jerlov.egg-info/top_level.txt +0 -0
  38. {jerlov-0.3.2 → jerlov-0.3.3}/setup.cfg +0 -0
  39. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_backscattering.py +0 -0
  40. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_colour.py +0 -0
  41. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_depth.py +0 -0
  42. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_quoted_figures.py +0 -0
  43. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_scene.py +0 -0
  44. {jerlov-0.3.2 → jerlov-0.3.3}/tests/test_shortwave.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.2
3
+ Version: 0.3.3
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.2"
81
+ __version__ = "0.3.3"
@@ -54,7 +54,7 @@ def _frozen(array: np.ndarray) -> np.ndarray:
54
54
  return array
55
55
 
56
56
  #: Values whose ``status`` is one of these should not be used without the
57
- #: caller being told. See README sections 1-6.
57
+ #: caller being told. See DATA.md sections 1-6.
58
58
  QUESTIONABLE = frozenset({"suspect", "missing", "extrapolated",
59
59
  "model_extrapolation", "reconstructed"})
60
60
 
@@ -137,6 +137,14 @@ def austin_model() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
137
137
  return _frozen(wl[order]), _frozen(m[order]), _frozen(kw[order])
138
138
 
139
139
 
140
+ @lru_cache(maxsize=None)
141
+ def austin_model_status() -> tuple[str, ...]:
142
+ """The ``status`` of each row of :func:`austin_model`, in the same order."""
143
+ rows = sorted(_rows("austin1986_model.csv"),
144
+ key=lambda r: float(r["wavelength_nm"]))
145
+ return tuple(r["status"] for r in rows)
146
+
147
+
140
148
  @lru_cache(maxsize=None)
141
149
  def b_from_c_ratio() -> tuple[np.ndarray, dict[str, np.ndarray]]:
142
150
  """Return ``(wavelengths, {statistic: ratio})`` from Smart (2007) Table 1."""
@@ -55,7 +55,7 @@ HALTRIN1999 = ScatteringConstants(
55
55
  #: Solonenko & Mobley (2015) Eqs. (8a)-(8d). The small-particle coefficient
56
56
  #: 1.513 does not match Haltrin's 1.151302; the digit appears to have been
57
57
  #: dropped in transcription. Their published tables were computed with 1.513,
58
- #: so this value is required to reproduce them. See README section 6.
58
+ #: so this value is required to reproduce them. See DATA.md section 6.
59
59
  SOLONENKO2015_SCATTERING = ScatteringConstants(
60
60
  bw_coeff=0.00583,
61
61
  bw_exponent=4.322,
@@ -44,6 +44,25 @@ def _like_input(result: np.ndarray, original) -> np.ndarray | float:
44
44
  return result
45
45
 
46
46
 
47
+ def _support(grid: np.ndarray, query: np.ndarray) -> list[tuple[int, ...]]:
48
+ """The indices of the samples of ``grid`` each query's answer rests on.
49
+
50
+ A query that lands exactly on a sample rests on that sample alone: it is
51
+ not interpolated, so its neighbours are no part of the answer. Otherwise
52
+ it rests on the samples either side. The NaN check and the provenance
53
+ warnings all use this, so that they cannot disagree about what a value
54
+ depends on.
55
+ """
56
+ n = grid.size
57
+ out: list[tuple[int, ...]] = []
58
+ for i, w_query in zip(np.searchsorted(grid, query), query):
59
+ if i < n and grid[i] == w_query:
60
+ out.append((int(i),))
61
+ else:
62
+ out.append((max(int(i) - 1, 0), min(int(i), n - 1)))
63
+ return out
64
+
65
+
47
66
  class Water:
48
67
  """Absorption and scattering coefficients as functions of wavelength.
49
68
 
@@ -129,33 +148,25 @@ class Water:
129
148
  f"wavelength outside the range of the data ({lo:g}-{hi:g} nm). "
130
149
  "This package does not extrapolate."
131
150
  )
132
- self._warn_if_flagged(quantity, query)
151
+ support = _support(self.wavelengths, query)
152
+ self._warn_if_flagged(quantity, support)
133
153
  values = self._series[quantity]
134
154
  out = np.interp(query, self.wavelengths, values)
135
155
  # np.interp happily bridges a NaN-free path around a NaN, so check the
136
- # samples the answer actually rests on. A query that lands exactly on
137
- # a sample rests on that sample alone: it is not interpolated, so a
138
- # missing neighbour must not poison it.
139
- idx = np.searchsorted(self.wavelengths, query)
140
- for k, (i, w_query) in enumerate(zip(idx, query)):
141
- exact = i < self.wavelengths.size and self.wavelengths[i] == w_query
142
- if exact:
143
- if np.isnan(values[i]):
144
- out[k] = np.nan
145
- continue
146
- left, right = max(i - 1, 0), min(i, values.size - 1)
147
- if np.any(np.isnan(values[left:right + 1])):
156
+ # samples the answer actually rests on.
157
+ for k, samples in enumerate(support):
158
+ if any(np.isnan(values[j]) for j in samples):
148
159
  out[k] = np.nan
149
160
  return _like_input(out, wl)
150
161
 
151
- def _warn_if_flagged(self, quantity: str, query: np.ndarray) -> None:
162
+ def _warn_if_flagged(self, quantity: str,
163
+ support: list[tuple[int, ...]]) -> None:
152
164
  statuses = self._flags.get(quantity)
153
165
  if not statuses:
154
166
  return
155
167
  hit: set[str] = set()
156
- idx = np.searchsorted(self.wavelengths, query)
157
- for i in idx:
158
- for j in (max(i - 1, 0), min(i, len(statuses) - 1)):
168
+ for samples in support:
169
+ for j in samples:
159
170
  status = statuses[j]
160
171
  if status in _data.QUESTIONABLE:
161
172
  hit.add(f"{status} at {self.wavelengths[j]:g} nm")
@@ -163,7 +174,7 @@ class Water:
163
174
  warnings.warn(
164
175
  f"{quantity} for Jerlov {self.name} rests on flagged values: "
165
176
  + "; ".join(sorted(hit))
166
- + ". See the package README for what is known about them.",
177
+ + ". See DATA.md for what is known about them.",
167
178
  ProvenanceWarning,
168
179
  stacklevel=_data.caller_stacklevel(),
169
180
  )
@@ -190,7 +201,7 @@ class Water:
190
201
  ``backscatter_ratio`` is bb/b and has no default. It is not determined
191
202
  by the Jerlov classification: deriving it from the particle
192
203
  concentrations of Solonenko & Mobley and of Williamson & Hollins gives
193
- answers that differ by up to a factor of 31. See README section 10.
204
+ answers that differ by up to a factor of 31. See DATA.md section 10.
194
205
 
195
206
  Reported ranges are roughly 0.005-0.01 for open ocean and 0.015-0.03
196
207
  for coastal water; the Petzold average-particle phase function gives
@@ -201,7 +212,7 @@ class Water:
201
212
  "bb is not determined by the water type. Pass "
202
213
  "backscatter_ratio=... explicitly (bb/b; roughly 0.005-0.01 "
203
214
  "for open ocean, 0.015-0.03 for coastal water). "
204
- "See README section 10."
215
+ "See DATA.md section 10."
205
216
  )
206
217
  if not 0.0 < backscatter_ratio < 0.5:
207
218
  raise ValueError("backscatter_ratio must lie in (0, 0.5)")
@@ -332,6 +343,10 @@ def water_type_at_depth(surface_water_type: str, depth_m: float) -> str | None:
332
343
  return None
333
344
 
334
345
 
346
+ #: Austin & Petzold (1986) state their model holds below this K(490), 1/m.
347
+ AUSTIN_KD490_LIMIT = 0.16
348
+
349
+
335
350
  def kd_spectrum(kd, wavelength_nm: float, at):
336
351
  """Reconstruct a Kd spectrum from a single measured value.
337
352
 
@@ -342,38 +357,96 @@ def kd_spectrum(kd, wavelength_nm: float, at):
342
357
  Parameters
343
358
  ----------
344
359
  kd:
345
- Measured Kd in 1/m.
360
+ Measured Kd in 1/m. A scalar, or an array of measurements all made at
361
+ ``wavelength_nm`` (several stations, say); each is reconstructed
362
+ separately.
346
363
  wavelength_nm:
347
- Wavelength at which ``kd`` was measured.
364
+ Wavelength at which ``kd`` was measured. A single value.
348
365
  at:
349
366
  Wavelength(s) at which to evaluate the spectrum.
350
367
 
368
+ Returns
369
+ -------
370
+ A float for scalar ``kd`` and ``at``. Otherwise an array of shape
371
+ ``kd.shape + at.shape``: one spectrum per measurement.
372
+
351
373
  Notes
352
374
  -----
353
- The authors state the model holds for K(490) < 0.16 1/m. Accuracy is about
375
+ The authors state the model holds for K(490) < 0.16 1/m, and a
376
+ :class:`ProvenanceWarning` is raised for any measurement whose K(490),
377
+ measured or implied by the model, is not below that. Accuracy is about
354
378
  8 percent at wavelengths up to 590 nm and degrades to about 31 percent at
355
- 670 nm; M below 365 nm is itself extrapolated.
379
+ 670 nm. M below 365 nm is itself extrapolated, and a result that rests on
380
+ it also warns.
356
381
  """
357
382
  wl, m, kw = _data.austin_model()
358
383
  lo, hi = float(wl[0]), float(wl[-1])
384
+ if np.ndim(wavelength_nm) != 0:
385
+ raise ValueError(
386
+ "wavelength_nm must be a single wavelength; for measurements at "
387
+ "different wavelengths, call kd_spectrum once for each"
388
+ )
389
+ wavelength_nm = float(wavelength_nm)
359
390
  query = _as_array(at)
360
391
  for value, label in ((wavelength_nm, "wavelength_nm"), (query, "at")):
361
392
  if np.any(np.asarray(value) < lo) or np.any(np.asarray(value) > hi):
362
393
  raise ValueError(f"{label} outside the model range ({lo:g}-{hi:g} nm)")
394
+ kd_values = np.asarray(kd, dtype=float)
363
395
 
364
396
  m1 = float(np.interp(wavelength_nm, wl, m))
365
397
  kw1 = float(np.interp(wavelength_nm, wl, kw))
366
- if kd < kw1:
398
+ below = kd_values < kw1
399
+ if np.any(below):
400
+ shown = ", ".join(f"{v:g}" for v in np.atleast_1d(kd_values)[
401
+ np.atleast_1d(below)][:5])
367
402
  warnings.warn(
368
- f"Kd={kd:g} is below the pure sea water value {kw1:g} at "
403
+ f"Kd={shown} is below the pure sea water value {kw1:g} at "
369
404
  f"{wavelength_nm:g} nm, which is not physically possible. "
370
405
  "Austin & Petzold (1986) reported exactly this problem in "
371
406
  "Jerlov's own type I values.",
372
407
  ProvenanceWarning,
373
408
  stacklevel=_data.caller_stacklevel(),
374
409
  )
375
- result = np.interp(query, wl, m) / m1 * (kd - kw1) + np.interp(query, wl, kw)
376
- return _like_input(result, at)
410
+
411
+ kd490 = (float(np.interp(490.0, wl, m)) / m1 * (kd_values - kw1)
412
+ + float(np.interp(490.0, wl, kw)))
413
+ outside = kd490 >= AUSTIN_KD490_LIMIT
414
+ if np.any(outside):
415
+ shown = ", ".join(f"{v:.3g}" for v in np.atleast_1d(kd490)[
416
+ np.atleast_1d(outside)][:5])
417
+ warnings.warn(
418
+ f"K(490) = {shown} 1/m"
419
+ + (" (implied by the model)" if wavelength_nm != 490.0 else "")
420
+ + f" is not below {AUSTIN_KD490_LIMIT:g} 1/m, the limit Austin & "
421
+ "Petzold (1986) give for their model. The reconstruction is "
422
+ "outside the range it was fitted to.",
423
+ ProvenanceWarning,
424
+ stacklevel=_data.caller_stacklevel(),
425
+ )
426
+
427
+ statuses = _data.austin_model_status()
428
+ flagged: set[str] = set()
429
+ for samples in _support(wl, np.concatenate(([wavelength_nm], query))):
430
+ for j in samples:
431
+ if statuses[j] in _data.QUESTIONABLE:
432
+ flagged.add(f"{statuses[j]} at {wl[j]:g} nm")
433
+ if flagged:
434
+ warnings.warn(
435
+ "the result rests on values of M that Austin & Petzold (1986) "
436
+ "flag as " + "; ".join(sorted(flagged)) + ", and say should be "
437
+ "used with caution",
438
+ ProvenanceWarning,
439
+ stacklevel=_data.caller_stacklevel(),
440
+ )
441
+
442
+ ratio = np.interp(query, wl, m) / m1
443
+ result = (ratio * (kd_values[..., None] - kw1)
444
+ + np.interp(query, wl, kw))
445
+ if np.ndim(at) == 0:
446
+ result = result[..., 0]
447
+ if np.ndim(result) == 0:
448
+ return float(result)
449
+ return result
377
450
 
378
451
 
379
452
  def b_from_c(c, wavelength_nm, *, bw, cw, bound: str = "average"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.3.2
3
+ Version: 0.3.3
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.2"
7
+ version = "0.3.3"
8
8
  description = "Inherent optical properties of Jerlov water types, with provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -63,7 +63,7 @@ def test_missing_values_stay_missing():
63
63
 
64
64
 
65
65
  def test_flagged_wavelengths_warn():
66
- """Jerlov IA's b is inconsistent with its own Table 3; see README 4."""
66
+ """Jerlov IA's b is inconsistent with its own Table 3; see DATA.md section 4."""
67
67
  w = jerlov.water("IA", source="solonenko2015")
68
68
  with pytest.warns(ProvenanceWarning, match="suspect"):
69
69
  w.b(550)
@@ -112,7 +112,10 @@ def test_kd_spectrum_reproduces_austin_table6():
112
112
  "austin1986_kd.csv", "II", None, "Kd_downwelling_per_m"
113
113
  )
114
114
  k475 = float(kd[np.where(wl == 475)[0][0]])
115
- predicted = jerlov.kd_spectrum(k475, 475, wl)
115
+ # Table VI starts at 350 nm, where the paper itself flags M as
116
+ # extrapolated, so reproducing it must say so.
117
+ with pytest.warns(ProvenanceWarning, match="extrapolated at 350 nm"):
118
+ predicted = jerlov.kd_spectrum(k475, 475, wl)
116
119
  assert np.max(np.abs(100 * (predicted - kd) / kd)) < 0.5
117
120
 
118
121
 
@@ -268,3 +271,54 @@ def test_a_provenance_warning_points_at_the_callers_line(method):
268
271
  flagged = [c for c in caught if issubclass(c.category, ProvenanceWarning)]
269
272
  assert flagged
270
273
  assert all(c.filename == __file__ for c in flagged)
274
+
275
+
276
+ def test_a_flag_on_a_neighbour_does_not_warn_at_an_exact_sample():
277
+ """350 nm is sound; only 349 nm is missing. The answer at 350 rests on
278
+ 350 alone, as the NaN check already knew."""
279
+ w = jerlov.water("9C", source="jerlov1976")
280
+ with warnings.catch_warnings():
281
+ warnings.simplefilter("error", ProvenanceWarning)
282
+ assert np.isfinite(w.kd(350.0))
283
+ with pytest.warns(ProvenanceWarning, match="missing at 349 nm"):
284
+ w.kd(349.5)
285
+
286
+
287
+ def test_kd_spectrum_takes_several_measurements_at_once():
288
+ kd = np.array([0.03, 0.06, 0.1])
289
+ at = np.array([440.0, 550.0, 650.0])
290
+ together = jerlov.kd_spectrum(kd, 490, at)
291
+ assert together.shape == (3, 3)
292
+ for row, single in zip(together, kd):
293
+ assert np.allclose(row, jerlov.kd_spectrum(float(single), 490, at))
294
+ at_one = jerlov.kd_spectrum(kd, 490, 550.0)
295
+ assert at_one.shape == (3,)
296
+ assert np.allclose(at_one, together[:, 1])
297
+
298
+
299
+ def test_kd_spectrum_wants_one_measurement_wavelength():
300
+ with pytest.raises(ValueError, match="single wavelength"):
301
+ jerlov.kd_spectrum(0.06, [490, 500], 550)
302
+
303
+
304
+ def test_kd_spectrum_warns_outside_the_fitted_range():
305
+ """Austin & Petzold: the model holds for K(490) < 0.16 1/m."""
306
+ with pytest.warns(ProvenanceWarning, match="0.16"):
307
+ jerlov.kd_spectrum(0.2, 490, 550)
308
+ # Measured elsewhere, the K(490) the model implies is what counts.
309
+ with pytest.warns(ProvenanceWarning, match="implied by the model"):
310
+ jerlov.kd_spectrum(0.3, 440, 550)
311
+ with warnings.catch_warnings():
312
+ warnings.simplefilter("error", ProvenanceWarning)
313
+ jerlov.kd_spectrum(0.15, 490, 550)
314
+
315
+
316
+ def test_kd_spectrum_warns_where_m_is_extrapolated():
317
+ with pytest.warns(ProvenanceWarning, match="extrapolated at 355 nm"):
318
+ jerlov.kd_spectrum(0.06, 490, 355.0)
319
+ with pytest.warns(ProvenanceWarning, match="extrapolated at 360 nm"):
320
+ jerlov.kd_spectrum(0.06, 490, 362.0)
321
+ # 365 nm is the first sound value of M, and rests on it alone.
322
+ with warnings.catch_warnings():
323
+ warnings.simplefilter("error", ProvenanceWarning)
324
+ jerlov.kd_spectrum(0.06, 490, [365.0, 550.0])
@@ -273,3 +273,34 @@ def test_type_hints_reach_the_caller():
273
273
  "py.typed exists but is not listed as package data, so it will not "
274
274
  "be installed"
275
275
  )
276
+
277
+
278
+ @source_tree
279
+ def test_every_cited_section_exists():
280
+ """A reference that leads nowhere is worse than none.
281
+
282
+ Error messages and docstrings pointed readers at "README section 10",
283
+ but the README has no numbered sections; the numbered ones are in
284
+ DATA.md. Every "DATA.md section N" cited in the code must exist there,
285
+ and nothing may cite a numbered README section.
286
+ """
287
+ numbered = {
288
+ int(n) for n in re.findall(r"^## (\d+)\. ",
289
+ (ROOT / "DATA.md").read_text(), re.M)
290
+ }
291
+ offenders = []
292
+ for path in sorted(list(ROOT.glob("jerlov/*.py"))
293
+ + list(ROOT.glob("jerlov/data/*.csv"))
294
+ + list(ROOT.glob("tests/*.py"))
295
+ + list(ROOT.glob("examples/*.py"))):
296
+ text = path.read_text()
297
+ where = path.relative_to(ROOT)
298
+ for match in re.finditer(r"README (?:sections? )?\d", text):
299
+ if path.name != "test_packaging.py":
300
+ offenders.append(f"{where}: '{match.group(0)}'")
301
+ for first, last in re.findall(
302
+ r"DATA\.md sections? (\d+)(?:\s*(?:-|and)\s*(\d+))?", text):
303
+ for n in {int(first), int(last or first)}:
304
+ if n not in numbered:
305
+ offenders.append(f"{where}: DATA.md section {n}")
306
+ assert not offenders, f"references to nothing: {offenders}"
@@ -82,7 +82,7 @@ def test_solonenko_b_follows_from_its_own_table3(water_type):
82
82
  """Eq. (8) with the paper's own constants must give the shipped b.
83
83
 
84
84
  Jerlov I and IA are excluded: their Table 3 entries are not consistent
85
- with their b column. See README section 4.
85
+ with their b column. See DATA.md section 4.
86
86
  """
87
87
  wl, b, statuses = series("solonenko2015_iop.csv", water_type, "b")
88
88
  _, cl, cs, _ = SM_TABLE3[water_type]
@@ -97,7 +97,7 @@ def test_solonenko_b_follows_from_its_own_table3(water_type):
97
97
 
98
98
  @pytest.mark.parametrize("water_type", ["I", "IA"])
99
99
  def test_solonenko_table3_is_inconsistent_for_the_clearest_types(water_type):
100
- """Guard the known defect of README section 4, so a fix is noticed."""
100
+ """Guard the known defect of DATA.md section 4, so a fix is noticed."""
101
101
  wl, b, _ = series("solonenko2015_iop.csv", water_type, "b")
102
102
  _, cl, cs, _ = SM_TABLE3[water_type]
103
103
  predicted = scattering(SOLONENKO2015_SCATTERING, wl, cs, cl)
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