jerlov 0.9.2__tar.gz → 0.9.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 (52) hide show
  1. {jerlov-0.9.2/jerlov.egg-info → jerlov-0.9.3}/PKG-INFO +7 -2
  2. {jerlov-0.9.2 → jerlov-0.9.3}/README.md +6 -1
  3. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/__init__.py +3 -1
  4. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/par.py +10 -8
  5. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/water.py +111 -5
  6. {jerlov-0.9.2 → jerlov-0.9.3/jerlov.egg-info}/PKG-INFO +7 -2
  7. {jerlov-0.9.2 → jerlov-0.9.3}/pyproject.toml +1 -1
  8. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_classify.py +30 -0
  9. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_depth.py +47 -0
  10. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_packaging.py +26 -0
  11. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_petzold.py +4 -1
  12. {jerlov-0.9.2 → jerlov-0.9.3}/LICENSE +0 -0
  13. {jerlov-0.9.2 → jerlov-0.9.3}/NOTICE +0 -0
  14. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/_data.py +0 -0
  15. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/backscattering.py +0 -0
  16. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/colour.py +0 -0
  17. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/__init__.py +0 -0
  18. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/austin1986_kd.csv +0 -0
  19. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/austin1986_model.csv +0 -0
  20. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/boss2001_chi.csv +0 -0
  21. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  22. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/cie_d65.csv +0 -0
  23. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1968_kd.csv +0 -0
  24. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  25. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1976_kd.csv +0 -0
  26. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/paulson1977_shortwave.csv +0 -0
  27. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/petzold1972_stations.csv +0 -0
  28. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/petzold1972_vsf.csv +0 -0
  29. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/smart2007_b_from_c.csv +0 -0
  30. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/solonenko2015_iop.csv +0 -0
  31. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_aw.csv +0 -0
  32. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_iop.csv +0 -0
  33. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_measured.csv +0 -0
  34. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2023_depth.csv +0 -0
  35. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/petzold.py +0 -0
  36. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/py.typed +0 -0
  37. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/scene.py +0 -0
  38. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/shortwave.py +0 -0
  39. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/sources.py +0 -0
  40. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/SOURCES.txt +0 -0
  41. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/dependency_links.txt +0 -0
  42. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/requires.txt +0 -0
  43. {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/top_level.txt +0 -0
  44. {jerlov-0.9.2 → jerlov-0.9.3}/setup.cfg +0 -0
  45. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_api.py +0 -0
  46. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_backscattering.py +0 -0
  47. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_colour.py +0 -0
  48. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_par.py +0 -0
  49. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_quoted_figures.py +0 -0
  50. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_reproduces_papers.py +0 -0
  51. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_scene.py +0 -0
  52. {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_shortwave.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.9.2
3
+ Version: 0.9.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
@@ -285,6 +285,9 @@ scene = jerlov.Scene.at_depth(jerlov.water("II"), 45.0, surface, wl, kd=d)
285
285
 
286
286
  It refuses to go through a layer the paper declared nothing for, rather than
287
287
  carrying the last type on: 3C stops at 70 m, 9C at 10 m.
288
+ `jerlov.profile_depth("3C")` says how far a descent can go, and
289
+ `d.transmittance_at(z)` gives any depth above the descent's bottom without
290
+ descending again.
288
291
 
289
292
  ## Light for photosynthesis
290
293
 
@@ -315,7 +318,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
315
318
 
316
319
  It ranks the types of one edition; it draws no boundaries, since those differ
317
320
  between editions. A single wavelength is accepted, and ranks oceanic and
318
- coastal types less well apart than a spectrum does.
321
+ coastal types less well apart than a spectrum does. `types=` compares with
322
+ some of the types only, which keeps the wavelengths where only the others
323
+ have gaps.
319
324
 
320
325
  ## Other entry points
321
326
 
@@ -255,6 +255,9 @@ scene = jerlov.Scene.at_depth(jerlov.water("II"), 45.0, surface, wl, kd=d)
255
255
 
256
256
  It refuses to go through a layer the paper declared nothing for, rather than
257
257
  carrying the last type on: 3C stops at 70 m, 9C at 10 m.
258
+ `jerlov.profile_depth("3C")` says how far a descent can go, and
259
+ `d.transmittance_at(z)` gives any depth above the descent's bottom without
260
+ descending again.
258
261
 
259
262
  ## Light for photosynthesis
260
263
 
@@ -285,7 +288,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
285
288
 
286
289
  It ranks the types of one edition; it draws no boundaries, since those differ
287
290
  between editions. A single wavelength is accepted, and ranks oceanic and
288
- coastal types less well apart than a spectrum does.
291
+ coastal types less well apart than a spectrum does. `types=` compares with
292
+ some of the types only, which keeps the wavelengths where only the others
293
+ have gaps.
289
294
 
290
295
  ## Other entry points
291
296
 
@@ -50,6 +50,7 @@ from .water import (
50
50
  descend,
51
51
  kd_spectrum,
52
52
  measured_points,
53
+ profile_depth,
53
54
  pure_water_absorption,
54
55
  water,
55
56
  water_type_at_depth,
@@ -75,6 +76,7 @@ __all__ = [
75
76
  "water",
76
77
  "water_type_at_depth",
77
78
  "descend",
79
+ "profile_depth",
78
80
  "Descent",
79
81
  "classify_kd",
80
82
  "KdClassification",
@@ -104,4 +106,4 @@ __all__ = [
104
106
  "MissingQuantityError",
105
107
  ]
106
108
 
107
- __version__ = "0.9.2"
109
+ __version__ = "0.9.3"
@@ -30,7 +30,7 @@ from __future__ import annotations
30
30
  import numpy as np
31
31
 
32
32
  from . import _data
33
- from .water import Descent, MissingQuantityError, Water, _require_number, water
33
+ from .water import Descent, MissingQuantityError, Water, _require_number
34
34
 
35
35
  #: Planck constant (J s), speed of light (m/s) and Avogadro constant (1/mol),
36
36
  #: exact by the 2019 definition of the SI.
@@ -241,18 +241,20 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
241
241
  f"the descent stops at {deepest:g} m, above the deepest depth "
242
242
  f"asked for ({depths.max():g} m)"
243
243
  )
244
- layers = [(top, bottom, water(t, kd.source).kd(sub_wl))
245
- for top, bottom, t in kd.layers]
244
+ if len(kd._kd) != len(kd.layers):
245
+ raise MissingQuantityError(
246
+ "this Descent was not made by descend(), so it does not "
247
+ "carry the Kd of its layers"
248
+ )
246
249
  kd_from = (f"a descent from Jerlov {kd.surface_water_type} "
247
250
  f"({kd.source})")
251
+ descent = kd
248
252
 
249
253
  def optical_depth(z):
250
- total = np.zeros_like(sub_wl)
251
- for top, bottom, values in layers:
252
- total += values * min(max(z - top, 0.0), bottom - top)
253
- return total
254
+ # The descent's own layers, on the samples PAR rests on only.
255
+ return descent._optical_depth(z, near)
254
256
 
255
- kd_values = (np.vstack([v for _, _, v in layers]) if layers
257
+ kd_values = (np.vstack([v[near] for v in kd._kd]) if kd._kd
256
258
  else np.zeros_like(sub_wl))
257
259
  else:
258
260
  if isinstance(kd, Water):
@@ -9,7 +9,7 @@ same path.
9
9
  from __future__ import annotations
10
10
 
11
11
  import warnings
12
- from dataclasses import dataclass
12
+ from dataclasses import dataclass, field
13
13
 
14
14
  import numpy as np
15
15
 
@@ -579,8 +579,8 @@ class KdClassification:
579
579
  )
580
580
 
581
581
 
582
- def classify_kd(wavelengths, kd, *, source: str = "jerlov1976"
583
- ) -> KdClassification:
582
+ def classify_kd(wavelengths, kd, *, source: str = "jerlov1976",
583
+ types=None) -> KdClassification:
584
584
  """Find the Jerlov type whose Kd spectrum a measured one is closest to.
585
585
 
586
586
  Every type of ``source`` is compared with the measurement at the given
@@ -600,6 +600,12 @@ def classify_kd(wavelengths, kd, *, source: str = "jerlov1976"
600
600
  source:
601
601
  A source carrying Kd: ``"jerlov1976"`` (the default, all ten types),
602
602
  ``"jerlov1968"``, ``"austin1986"`` or ``"solonenko2015"``.
603
+ types:
604
+ The types to compare with, if not all of ``source``'s; the oceanic
605
+ ones, say, for a measurement made offshore. A wavelength is left
606
+ out wherever any compared type has no Kd, so leaving out types with
607
+ gaps keeps more wavelengths: Solonenko & Mobley's 3C and 5C have
608
+ none from 600 nm on.
603
609
 
604
610
  Notes
605
611
  -----
@@ -625,7 +631,24 @@ def classify_kd(wavelengths, kd, *, source: str = "jerlov1976"
625
631
  # were a gap, leaving the other wavelengths to decide alone.
626
632
  raise ValueError("Kd must be positive and finite (NaN marks a gap)")
627
633
 
628
- waters = {t: water(t, source) for t in src.water_types}
634
+ if types is None:
635
+ compared = src.water_types
636
+ else:
637
+ # One type given as a string is one type: tuple("III") would be
638
+ # three of type "I".
639
+ compared = (types,) if isinstance(types, str) else tuple(types)
640
+ unknown = [t for t in compared if t not in src.water_types]
641
+ if unknown:
642
+ raise KeyError(
643
+ f"source {source!r} does not cover Jerlov "
644
+ f"{', '.join(map(repr, unknown))} "
645
+ f"(available: {', '.join(src.water_types)})"
646
+ )
647
+ if not compared:
648
+ raise ValueError("types must name at least one water type")
649
+ if len(set(compared)) != len(compared):
650
+ raise ValueError("types names a water type more than once")
651
+ waters = {t: water(t, source) for t in compared}
629
652
  # Raises for a wavelength outside the source's range. The flags are
630
653
  # checked below, once, on the wavelengths actually compared: ten
631
654
  # warnings for one comparison would bury the one that matters.
@@ -637,7 +660,7 @@ def classify_kd(wavelengths, kd, *, source: str = "jerlov1976"
637
660
  if not usable.any():
638
661
  raise MissingQuantityError(
639
662
  "no wavelength has both a measured Kd and a Kd for every type "
640
- f"of {source!r}"
663
+ f"compared from {source!r}"
641
664
  )
642
665
  by_flags: dict[str, list[str]] = {}
643
666
  for water_type, w in waters.items():
@@ -699,6 +722,58 @@ class Descent:
699
722
  last stops at ``depth_m``."""
700
723
  transmittance: np.ndarray
701
724
  """``Ed(depth) / Ed(0)``: ``exp(-sum(Kd_i * thickness_i))``."""
725
+ _kd: tuple[np.ndarray, ...] = field(default=(), repr=False)
726
+ """The Kd of each layer in :attr:`layers`, on :attr:`wavelengths`."""
727
+
728
+ def _optical_depth(self, depth: float, columns=slice(None)) -> np.ndarray:
729
+ """``sum(Kd_i * thickness_i)`` from the surface to ``depth``.
730
+
731
+ ``columns`` picks wavelengths, so that a caller using only some of
732
+ them, as PAR does, never touches the rest.
733
+ """
734
+ total = np.zeros(np.size(self.wavelengths))[columns]
735
+ for (top, bottom, _), values in zip(self.layers, self._kd):
736
+ total = total + values[columns] * min(max(depth - top, 0.0),
737
+ bottom - top)
738
+ return total
739
+
740
+ def transmittance_at(self, depth_m):
741
+ """``Ed(z) / Ed(0)`` at any depth from the surface to :attr:`depth_m`.
742
+
743
+ Uses the layers already crossed, so it is what :func:`descend` to
744
+ that depth would give, without calling it again. A single depth
745
+ gives an array on :attr:`wavelengths` (a float if the descent was
746
+ asked for at one wavelength); an array of depths gives one row per
747
+ depth.
748
+
749
+ Raises
750
+ ------
751
+ MissingQuantityError
752
+ Below :attr:`depth_m`. Descend further, if the profile allows:
753
+ see :func:`profile_depth`.
754
+ """
755
+ if len(self._kd) != len(self.layers):
756
+ # Only descend() records the Kd of each layer; without it every
757
+ # depth would come back as 1, looking like perfectly clear water.
758
+ raise MissingQuantityError(
759
+ "this Descent was not made by descend(), so it does not "
760
+ "carry the Kd of its layers"
761
+ )
762
+ depths = np.asarray(depth_m, dtype=float)
763
+ if not np.all(np.isfinite(depths)):
764
+ raise ValueError(f"depth_m must be finite numbers, not {depth_m!r}")
765
+ if np.any(depths < 0):
766
+ raise ValueError("depth_m cannot be negative")
767
+ if np.any(depths > self.depth_m):
768
+ raise MissingQuantityError(
769
+ f"this descent stops at {self.depth_m:g} m, above "
770
+ f"{depths.max():g} m; descend further to go deeper"
771
+ )
772
+ out = np.array([np.exp(-self._optical_depth(float(z)))
773
+ for z in depths.ravel()])
774
+ if depths.ndim == 0:
775
+ return _like_input(out[0], self.transmittance)
776
+ return out.reshape(depths.shape + (np.size(self.wavelengths),))
702
777
 
703
778
  def __repr__(self) -> str:
704
779
  # Adjacent 10 m layers of one type are shown as one span.
@@ -808,7 +883,38 @@ def descend(surface_water_type: str, depth_m: float, wavelengths, *,
808
883
  wavelengths=query,
809
884
  layers=tuple(layers),
810
885
  transmittance=_like_input(np.exp(-optical_depth), wavelengths),
886
+ _kd=tuple(_frozen_copy(kd[t]) for _, _, t in layers),
887
+ )
888
+
889
+
890
+ def _frozen_copy(values: np.ndarray) -> np.ndarray:
891
+ out = np.array(values, dtype=float)
892
+ out.setflags(write=False)
893
+ return out
894
+
895
+
896
+ def profile_depth(surface_water_type: str) -> float:
897
+ """How deep :func:`descend` can go below ``surface_water_type``, in m.
898
+
899
+ The top of the first layer for which Williamson & Hollins (2023)
900
+ declared no type, or 200 m, where their profile ends. Coastal types run
901
+ out early: 9C at 10 m, 5C and 7C at 20 m, 3C at 70 m.
902
+
903
+ ::
904
+
905
+ d = jerlov.descend("3C", jerlov.profile_depth("3C"), wl) # to 70 m
906
+ """
907
+ rows = sorted(
908
+ (r for r in _data._rows("williamson2023_depth.csv")
909
+ if r["surface_water_type"] == surface_water_type),
910
+ key=lambda r: float(r["depth_min_m"]),
811
911
  )
912
+ if not rows:
913
+ water_type_at_depth(surface_water_type, 0.0) # raises, naming types
914
+ for row in rows:
915
+ if not row["water_type"]:
916
+ return float(row["depth_min_m"])
917
+ return float(rows[-1]["depth_max_m"])
812
918
 
813
919
 
814
920
  #: Why each source without a pure water absorption has none.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jerlov
3
- Version: 0.9.2
3
+ Version: 0.9.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
@@ -285,6 +285,9 @@ scene = jerlov.Scene.at_depth(jerlov.water("II"), 45.0, surface, wl, kd=d)
285
285
 
286
286
  It refuses to go through a layer the paper declared nothing for, rather than
287
287
  carrying the last type on: 3C stops at 70 m, 9C at 10 m.
288
+ `jerlov.profile_depth("3C")` says how far a descent can go, and
289
+ `d.transmittance_at(z)` gives any depth above the descent's bottom without
290
+ descending again.
288
291
 
289
292
  ## Light for photosynthesis
290
293
 
@@ -315,7 +318,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
315
318
 
316
319
  It ranks the types of one edition; it draws no boundaries, since those differ
317
320
  between editions. A single wavelength is accepted, and ranks oceanic and
318
- coastal types less well apart than a spectrum does.
321
+ coastal types less well apart than a spectrum does. `types=` compares with
322
+ some of the types only, which keeps the wavelengths where only the others
323
+ have gaps.
319
324
 
320
325
  ## Other entry points
321
326
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jerlov"
7
- version = "0.9.2"
7
+ version = "0.9.3"
8
8
  description = "Inherent optical properties of Jerlov water types, with provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -89,3 +89,33 @@ def test_what_cannot_be_compared_is_refused():
89
89
  jerlov.classify_kd(WL, [0.05])
90
90
  with pytest.raises(jerlov.MissingQuantityError, match="no wavelength"):
91
91
  jerlov.classify_kd([500.0], [np.nan])
92
+
93
+
94
+ def test_types_narrows_the_comparison_and_keeps_more_wavelengths():
95
+ """Solonenko & Mobley's 3C and 5C have no Kd from 600 nm on, which
96
+ took those wavelengths out of every comparison."""
97
+ wl = np.arange(400.0, 701.0, 25.0)
98
+ oceanic = ("I", "IA", "IB", "II", "III")
99
+ with warnings.catch_warnings():
100
+ warnings.simplefilter("ignore", jerlov.ProvenanceWarning)
101
+ kd = jerlov.water("II", source="solonenko2015").kd(wl)
102
+ every = jerlov.classify_kd(wl, kd, source="solonenko2015")
103
+ some = jerlov.classify_kd(wl, kd, source="solonenko2015",
104
+ types=oceanic)
105
+ assert every.excluded_nm == (600.0, 625.0, 650.0, 675.0, 700.0)
106
+ assert some.excluded_nm == ()
107
+ assert some.water_type == "II"
108
+ assert {t for t, _ in some.distances} == set(oceanic)
109
+
110
+
111
+ def test_types_must_be_types_of_the_source():
112
+ with pytest.raises(KeyError, match="does not cover Jerlov '3C'"):
113
+ jerlov.classify_kd(WL, _kd("II"), source="austin1986",
114
+ types=("II", "IA", "3C"))
115
+ with pytest.raises(ValueError, match="at least one"):
116
+ jerlov.classify_kd(WL, _kd("II"), types=())
117
+ with pytest.raises(ValueError, match="more than once"):
118
+ jerlov.classify_kd(WL, _kd("II"), types=("II", "II"))
119
+ # A string is one type, not a sequence of letters.
120
+ alone = jerlov.classify_kd(WL, _kd("III"), types="III")
121
+ assert alone.distances == (("III", 0.0),)
@@ -190,3 +190,50 @@ def test_a_descent_prints_its_path_not_its_arrays():
190
190
  "Kd from jerlov1976; 301 wavelengths>")
191
191
  assert repr(jerlov.descend("II", 0.0, 500.0)).startswith(
192
192
  "<Descent II to 0 m: no layer crossed;")
193
+
194
+
195
+ def test_profile_depth_is_where_descend_stops():
196
+ """Three examples searched for this by trying descend() every 10 m."""
197
+ expected = {"I": 200.0, "IB": 200.0, "III": 170.0, "1C": 110.0,
198
+ "3C": 70.0, "5C": 20.0, "7C": 20.0, "9C": 10.0}
199
+ for water_type, depth in expected.items():
200
+ assert jerlov.profile_depth(water_type) == depth
201
+ jerlov.descend(water_type, depth, 500.0) # reaches it
202
+ with pytest.raises(jerlov.MissingQuantityError):
203
+ jerlov.descend(water_type, depth + 0.1, 500.0)
204
+ with pytest.raises(KeyError, match="known"):
205
+ jerlov.profile_depth("XI")
206
+
207
+
208
+ def test_transmittance_at_is_a_shorter_descent():
209
+ wl = np.arange(400.0, 701.0, 10.0)
210
+ d = jerlov.descend("1C", 45.0, wl)
211
+ for z in (0.0, 7.5, 10.0, 20.0, 33.3, 45.0):
212
+ assert d.transmittance_at(z) == pytest.approx(
213
+ jerlov.descend("1C", z, wl).transmittance, rel=1e-12)
214
+ rows = d.transmittance_at([[0.0, 10.0], [20.0, 45.0]])
215
+ assert rows.shape == (2, 2, wl.size)
216
+ assert rows[1, 1] == pytest.approx(d.transmittance)
217
+ one = jerlov.descend("II", 30.0, 500.0)
218
+ assert isinstance(one.transmittance_at(12.0), float)
219
+ assert one.transmittance_at(12.0) == jerlov.descend("II", 12.0, 500.0).transmittance
220
+
221
+
222
+ def test_transmittance_at_refuses_what_the_descent_does_not_cover():
223
+ d = jerlov.descend("I", 30.0, [450.0, 550.0])
224
+ with pytest.raises(jerlov.MissingQuantityError, match="stops at 30 m"):
225
+ d.transmittance_at(31.0)
226
+ for bad in (-1.0, float("nan"), float("inf")):
227
+ with pytest.raises(ValueError):
228
+ d.transmittance_at(bad)
229
+ # A Descent built by hand has no layer Kd; it must not look clear.
230
+ hand = jerlov.Descent("I", 30.0, "jerlov1976", d.wavelengths, d.layers,
231
+ d.transmittance)
232
+ with pytest.raises(jerlov.MissingQuantityError, match="descend"):
233
+ hand.transmittance_at(10.0)
234
+ with pytest.raises(jerlov.MissingQuantityError, match="descend"):
235
+ jerlov.par_profile(np.ones(301), np.arange(400.0, 701.0), 0.0,
236
+ kd=jerlov.Descent("I", 30.0, "jerlov1976",
237
+ np.arange(400.0, 701.0),
238
+ d.layers, np.ones(301)),
239
+ unit="photons")
@@ -389,3 +389,29 @@ def test_the_READMEs_count_the_examples():
389
389
  for script in (ROOT / "examples").glob("*.py"):
390
390
  assert script.name in readme.read_text(), (
391
391
  f"{readme.relative_to(ROOT)} never mentions {script.name}")
392
+
393
+
394
+ @source_tree
395
+ def test_the_tests_read_the_installed_tables_not_the_source_tree():
396
+ """The suite is also run against the wheel, where there is no
397
+ jerlov/data/ beside the tests. test_petzold.py read it from there, and
398
+ the first run against a wheel failed."""
399
+ offenders = []
400
+ for path in (ROOT / "tests").glob("test_*.py"):
401
+ if path.name == "test_packaging.py":
402
+ continue
403
+ if re.search(r'"jerlov"\s*/\s*"data"', path.read_text()):
404
+ offenders.append(path.name)
405
+ assert not offenders, (
406
+ f"{offenders} read tables from the source tree; use "
407
+ "importlib.resources.files('jerlov.data')"
408
+ )
409
+
410
+
411
+ @source_tree
412
+ def test_publishing_checks_the_tag_and_runs_the_suite_on_the_wheel():
413
+ workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text()
414
+ assert "GITHUB_REF_NAME" in workflow and "pyproject.toml" in workflow, (
415
+ "publish.yml no longer compares the tag with the version")
416
+ assert "pytest wheel-tests" in workflow, (
417
+ "publish.yml no longer runs the tests against the installed wheel")
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import csv
6
6
  import re
7
+ from importlib import resources
7
8
  import warnings
8
9
  from pathlib import Path
9
10
 
@@ -18,7 +19,9 @@ ROOT = Path(__file__).resolve().parent.parent
18
19
 
19
20
 
20
21
  def _table(station):
21
- path = ROOT / "jerlov" / "data" / "petzold1972_vsf.csv"
22
+ # The installed package's own copy, not the source tree's: the suite is
23
+ # also run against the wheel, where there is no source tree to read.
24
+ path = resources.files("jerlov.data").joinpath("petzold1972_vsf.csv")
22
25
  with path.open(encoding="utf-8") as handle:
23
26
  return [r for r in csv.DictReader(handle) if r["station"] == station]
24
27
 
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