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.
- {jerlov-0.9.2/jerlov.egg-info → jerlov-0.9.3}/PKG-INFO +7 -2
- {jerlov-0.9.2 → jerlov-0.9.3}/README.md +6 -1
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/__init__.py +3 -1
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/par.py +10 -8
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/water.py +111 -5
- {jerlov-0.9.2 → jerlov-0.9.3/jerlov.egg-info}/PKG-INFO +7 -2
- {jerlov-0.9.2 → jerlov-0.9.3}/pyproject.toml +1 -1
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_classify.py +30 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_depth.py +47 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_packaging.py +26 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_petzold.py +4 -1
- {jerlov-0.9.2 → jerlov-0.9.3}/LICENSE +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/NOTICE +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/_data.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/backscattering.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/colour.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/__init__.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/austin1986_kd.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/austin1986_model.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/boss2001_chi.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/cie_d65.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1968_kd.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/jerlov1976_kd.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/paulson1977_shortwave.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/petzold1972_stations.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/petzold1972_vsf.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/smart2007_b_from_c.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/solonenko2015_iop.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_aw.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_iop.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2022_measured.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/data/williamson2023_depth.csv +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/petzold.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/py.typed +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/scene.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/shortwave.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov/sources.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/SOURCES.txt +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/dependency_links.txt +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/requires.txt +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/jerlov.egg-info/top_level.txt +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/setup.cfg +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_api.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_backscattering.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_colour.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_par.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_quoted_figures.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_reproduces_papers.py +0 -0
- {jerlov-0.9.2 → jerlov-0.9.3}/tests/test_scene.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|