jerlov 0.9.2__tar.gz → 0.9.4__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 (54) hide show
  1. {jerlov-0.9.2/jerlov.egg-info → jerlov-0.9.4}/PKG-INFO +21 -4
  2. {jerlov-0.9.2 → jerlov-0.9.4}/README.md +20 -3
  3. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/__init__.py +14 -2
  4. jerlov-0.9.4/jerlov/data/jerlov1977_quanta.csv +203 -0
  5. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/par.py +127 -8
  6. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/water.py +111 -5
  7. {jerlov-0.9.2 → jerlov-0.9.4/jerlov.egg-info}/PKG-INFO +21 -4
  8. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov.egg-info/SOURCES.txt +2 -0
  9. {jerlov-0.9.2 → jerlov-0.9.4}/pyproject.toml +1 -1
  10. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_classify.py +30 -0
  11. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_depth.py +47 -0
  12. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_packaging.py +26 -0
  13. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_petzold.py +4 -1
  14. jerlov-0.9.4/tests/test_quanta.py +168 -0
  15. {jerlov-0.9.2 → jerlov-0.9.4}/LICENSE +0 -0
  16. {jerlov-0.9.2 → jerlov-0.9.4}/NOTICE +0 -0
  17. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/_data.py +0 -0
  18. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/backscattering.py +0 -0
  19. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/colour.py +0 -0
  20. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/__init__.py +0 -0
  21. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/austin1986_kd.csv +0 -0
  22. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/austin1986_model.csv +0 -0
  23. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/boss2001_chi.csv +0 -0
  24. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/cie1931_2deg_cmf.csv +0 -0
  25. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/cie_d65.csv +0 -0
  26. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/jerlov1968_kd.csv +0 -0
  27. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/jerlov1968_total_irradiance.csv +0 -0
  28. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/jerlov1976_kd.csv +0 -0
  29. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/paulson1977_shortwave.csv +0 -0
  30. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/petzold1972_stations.csv +0 -0
  31. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/petzold1972_vsf.csv +0 -0
  32. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/smart2007_b_from_c.csv +0 -0
  33. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/solonenko2015_iop.csv +0 -0
  34. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/williamson2022_aw.csv +0 -0
  35. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/williamson2022_iop.csv +0 -0
  36. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/williamson2022_measured.csv +0 -0
  37. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/data/williamson2023_depth.csv +0 -0
  38. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/petzold.py +0 -0
  39. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/py.typed +0 -0
  40. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/scene.py +0 -0
  41. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/shortwave.py +0 -0
  42. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov/sources.py +0 -0
  43. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov.egg-info/dependency_links.txt +0 -0
  44. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov.egg-info/requires.txt +0 -0
  45. {jerlov-0.9.2 → jerlov-0.9.4}/jerlov.egg-info/top_level.txt +0 -0
  46. {jerlov-0.9.2 → jerlov-0.9.4}/setup.cfg +0 -0
  47. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_api.py +0 -0
  48. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_backscattering.py +0 -0
  49. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_colour.py +0 -0
  50. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_par.py +0 -0
  51. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_quoted_figures.py +0 -0
  52. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_reproduces_papers.py +0 -0
  53. {jerlov-0.9.2 → jerlov-0.9.4}/tests/test_scene.py +0 -0
  54. {jerlov-0.9.2 → jerlov-0.9.4}/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.4
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
 
@@ -304,6 +307,18 @@ default, because PAR counts photons and an energy spectrum read as photons is
304
307
  weighted wrongly with no sign of it. This is planar PAR, from Ed; PAR on
305
308
  scalar irradiance is larger by a factor this package cannot supply.
306
309
 
310
+ Jerlov measured quanta irradiance, 350-700 nm, by water type, and his
311
+ tables are shipped for checking against:
312
+
313
+ ```python
314
+ jerlov.jerlov1977_quanta("II") # <QuantaLevels II: 30% 10.5 m, 10% 24 m, 3% 39 m, 1% 52 m; ...>
315
+ ```
316
+
317
+ From II to 3C the package agrees with them within 2 m at every level. For
318
+ Jerlov I it agrees only through `descend`; for IB it puts the 1 percent
319
+ level a third too deep, because Jerlov's IB water grew more turbid below
320
+ 15 m. DATA.md section 22.
321
+
307
322
  ## Which type is my water nearest?
308
323
 
309
324
  ```python
@@ -315,7 +330,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
315
330
 
316
331
  It ranks the types of one edition; it draws no boundaries, since those differ
317
332
  between editions. A single wavelength is accepted, and ranks oceanic and
318
- coastal types less well apart than a spectrum does.
333
+ coastal types less well apart than a spectrum does. `types=` compares with
334
+ some of the types only, which keeps the wavelengths where only the others
335
+ have gaps.
319
336
 
320
337
  ## Other entry points
321
338
 
@@ -389,8 +406,8 @@ made.
389
406
  ## Provenance and design
390
407
 
391
408
  `DATA.md` records, for every shipped table, where it came from, what was
392
- verified, and what is known to be wrong with it. Twenty-one entries are
393
- documented there: eight confirmed defects in the source literature, three
409
+ verified, and what is known to be wrong with it. Twenty-two entries are
410
+ documented there: nine confirmed defects in the source literature, three
394
411
  questions the first edition of Jerlov settled, and the rest notes.
395
412
 
396
413
  `DECISIONS.md` records why the package is shaped the way it is, including the
@@ -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
 
@@ -274,6 +277,18 @@ default, because PAR counts photons and an energy spectrum read as photons is
274
277
  weighted wrongly with no sign of it. This is planar PAR, from Ed; PAR on
275
278
  scalar irradiance is larger by a factor this package cannot supply.
276
279
 
280
+ Jerlov measured quanta irradiance, 350-700 nm, by water type, and his
281
+ tables are shipped for checking against:
282
+
283
+ ```python
284
+ jerlov.jerlov1977_quanta("II") # <QuantaLevels II: 30% 10.5 m, 10% 24 m, 3% 39 m, 1% 52 m; ...>
285
+ ```
286
+
287
+ From II to 3C the package agrees with them within 2 m at every level. For
288
+ Jerlov I it agrees only through `descend`; for IB it puts the 1 percent
289
+ level a third too deep, because Jerlov's IB water grew more turbid below
290
+ 15 m. DATA.md section 22.
291
+
277
292
  ## Which type is my water nearest?
278
293
 
279
294
  ```python
@@ -285,7 +300,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
285
300
 
286
301
  It ranks the types of one edition; it draws no boundaries, since those differ
287
302
  between editions. A single wavelength is accepted, and ranks oceanic and
288
- coastal types less well apart than a spectrum does.
303
+ coastal types less well apart than a spectrum does. `types=` compares with
304
+ some of the types only, which keeps the wavelengths where only the others
305
+ have gaps.
289
306
 
290
307
  ## Other entry points
291
308
 
@@ -359,8 +376,8 @@ made.
359
376
  ## Provenance and design
360
377
 
361
378
  `DATA.md` records, for every shipped table, where it came from, what was
362
- verified, and what is known to be wrong with it. Twenty-one entries are
363
- documented there: eight confirmed defects in the source literature, three
379
+ verified, and what is known to be wrong with it. Twenty-two entries are
380
+ documented there: nine confirmed defects in the source literature, three
364
381
  questions the first edition of Jerlov settled, and the rest notes.
365
382
 
366
383
  `DECISIONS.md` records why the package is shaped the way it is, including the
@@ -23,7 +23,14 @@ from .colour import (
23
23
  spectrum_to_xyz,
24
24
  xyz_to_srgb,
25
25
  )
26
- from .par import PAR_RANGE_NM, ParProfile, par_profile
26
+ from .par import (
27
+ PAR_RANGE_NM,
28
+ QUANTA_BAND_NM,
29
+ ParProfile,
30
+ QuantaLevels,
31
+ jerlov1977_quanta,
32
+ par_profile,
33
+ )
27
34
  from .petzold import PETZOLD_STATIONS, ScatteringFunction, petzold_scattering
28
35
  from .scene import (
29
36
  AttenuationCoefficients,
@@ -50,6 +57,7 @@ from .water import (
50
57
  descend,
51
58
  kd_spectrum,
52
59
  measured_points,
60
+ profile_depth,
53
61
  pure_water_absorption,
54
62
  water,
55
63
  water_type_at_depth,
@@ -75,12 +83,16 @@ __all__ = [
75
83
  "water",
76
84
  "water_type_at_depth",
77
85
  "descend",
86
+ "profile_depth",
78
87
  "Descent",
79
88
  "classify_kd",
80
89
  "KdClassification",
81
90
  "par_profile",
82
91
  "ParProfile",
83
92
  "PAR_RANGE_NM",
93
+ "jerlov1977_quanta",
94
+ "QuantaLevels",
95
+ "QUANTA_BAND_NM",
84
96
  "pure_water_absorption",
85
97
  "petzold_scattering",
86
98
  "PETZOLD_STATIONS",
@@ -104,4 +116,4 @@ __all__ = [
104
116
  "MissingQuantityError",
105
117
  ]
106
118
 
107
- __version__ = "0.9.2"
119
+ __version__ = "0.9.4"
@@ -0,0 +1,203 @@
1
+ table,water_type,depth_top_m,depth_bottom_m,percent_top,percent_bottom,value,unit,status,note
2
+ 2,I,19,19,30,30,19,m,ok,
3
+ 2,I,49,49,10,10,49,m,ok,
4
+ 2,I,79,79,3,3,79,m,ok,
5
+ 2,I,103,103,1,1,103,m,ok,
6
+ 2n,I,,,30,30,0.39,z/z(10),ok,
7
+ 2n,I,,,3,3,1.65,z/z(10),suspect,"does not follow from the depths in Table 2, even allowing for rounding (DATA.md section 22)"
8
+ 2n,I,,,1,1,2.15,z/z(10),suspect,"does not follow from the depths in Table 2, even allowing for rounding (DATA.md section 22)"
9
+ 3,I,0,0,,,100,percent,ok,
10
+ 3,I,2,2,,,79,percent,ok,
11
+ 3,I,5,5,,,64,percent,ok,
12
+ 3,I,10,10,,,46,percent,ok,
13
+ 3,I,15,15,,,35.5,percent,ok,
14
+ 3,I,20,20,,,29,percent,ok,
15
+ 3,I,30,30,,,20,percent,ok,
16
+ 3,I,40,40,,,13.7,percent,ok,
17
+ 3,I,50,50,,,9.5,percent,ok,
18
+ 3,I,60,60,,,6.5,percent,ok,
19
+ 3,I,70,70,,,4.3,percent,ok,
20
+ 3,I,80,80,,,2.9,percent,ok,
21
+ 3,I,90,90,,,1.85,percent,ok,
22
+ 3,I,100,100,,,1.14,percent,ok,
23
+ 4,I,0,19,100,30,0.063,1/m,ok,
24
+ 4,I,19,49,30,10,0.037,1/m,ok,
25
+ 4,I,49,79,10,3,0.04,1/m,ok,
26
+ 4,I,79,103,3,1,0.046,1/m,ok,
27
+ 5,I,0,2,,,0.118,1/m,ok,
28
+ 5,I,2,5,,,0.073,1/m,ok,
29
+ 5,I,5,10,,,0.065,1/m,ok,
30
+ 5,I,10,15,,,0.052,1/m,ok,
31
+ 5,I,15,20,,,0.04,1/m,ok,
32
+ 5,I,20,30,,,0.039,1/m,ok,
33
+ 5,I,30,40,,,0.039,1/m,ok,
34
+ 5,I,40,50,,,0.037,1/m,ok,
35
+ 5,I,50,60,,,0.038,1/m,ok,
36
+ 5,I,60,70,,,0.04,1/m,ok,
37
+ 5,I,70,80,,,0.04,1/m,ok,
38
+ 5,I,80,90,,,0.045,1/m,ok,
39
+ 5,I,90,100,,,0.048,1/m,ok,
40
+ 2,IA,16.5,16.5,30,30,16.5,m,ok,
41
+ 2,IA,41,41,10,10,41,m,ok,
42
+ 2,IA,66,66,3,3,66,m,ok,
43
+ 2,IA,87,87,1,1,87,m,ok,
44
+ 2n,IA,,,30,30,0.4,z/z(10),ok,
45
+ 2n,IA,,,3,3,1.66,z/z(10),suspect,"does not follow from the depths in Table 2, even allowing for rounding (DATA.md section 22)"
46
+ 2n,IA,,,1,1,2.12,z/z(10),ok,
47
+ 3,IA,0,0,,,100,percent,ok,
48
+ 3,IA,2,2,,,77,percent,ok,
49
+ 3,IA,5,5,,,59,percent,ok,
50
+ 3,IA,10,10,,,42,percent,ok,
51
+ 3,IA,15,15,,,32,percent,ok,
52
+ 3,IA,20,20,,,25.5,percent,ok,
53
+ 3,IA,30,30,,,16.4,percent,ok,
54
+ 3,IA,40,40,,,10.3,percent,ok,
55
+ 3,IA,50,50,,,6.6,percent,ok,
56
+ 3,IA,60,60,,,4.1,percent,ok,
57
+ 3,IA,70,70,,,2.5,percent,ok,
58
+ 3,IA,80,80,,,1.47,percent,ok,
59
+ 4,IA,0,16.5,100,30,0.073,1/m,ok,
60
+ 4,IA,16.5,41,30,10,0.043,1/m,suspect,"does not follow from the depths in Table 2, even allowing for rounding (DATA.md section 22)"
61
+ 4,IA,41,66,10,3,0.048,1/m,ok,
62
+ 4,IA,66,87,3,1,0.052,1/m,ok,
63
+ 5,IA,0,2,,,0.13,1/m,ok,
64
+ 5,IA,2,5,,,0.089,1/m,ok,
65
+ 5,IA,5,10,,,0.068,1/m,ok,
66
+ 5,IA,10,15,,,0.054,1/m,ok,
67
+ 5,IA,15,20,,,0.042,1/m,ok,
68
+ 5,IA,20,30,,,0.046,1/m,suspect,"does not follow from the percentages in Table 3, even allowing for rounding (DATA.md section 22)"
69
+ 5,IA,30,40,,,0.046,1/m,ok,
70
+ 5,IA,40,50,,,0.045,1/m,ok,
71
+ 5,IA,50,60,,,0.048,1/m,ok,
72
+ 5,IA,60,70,,,0.049,1/m,ok,
73
+ 5,IA,70,80,,,0.053,1/m,ok,
74
+ 2,IB,14,14,30,30,14,m,ok,
75
+ 2,IB,33,33,10,10,33,m,ok,
76
+ 2,IB,54,54,3,3,54,m,ok,
77
+ 2,IB,70,70,1,1,70,m,ok,
78
+ 2n,IB,,,30,30,0.42,z/z(10),ok,
79
+ 2n,IB,,,3,3,1.63,z/z(10),ok,
80
+ 2n,IB,,,1,1,2.12,z/z(10),ok,
81
+ 3,IB,0,0,,,100,percent,ok,
82
+ 3,IB,2,2,,,75,percent,ok,
83
+ 3,IB,5,5,,,54,percent,ok,
84
+ 3,IB,10,10,,,38,percent,ok,
85
+ 3,IB,15,15,,,28,percent,ok,
86
+ 3,IB,20,20,,,21,percent,ok,
87
+ 3,IB,30,30,,,12.0,percent,ok,
88
+ 3,IB,40,40,,,6.7,percent,ok,
89
+ 3,IB,50,50,,,3.8,percent,ok,
90
+ 3,IB,60,60,,,2.0,percent,ok,
91
+ 3,IB,70,70,,,1.01,percent,ok,
92
+ 4,IB,0,14,100,30,0.086,1/m,ok,
93
+ 4,IB,14,33,30,10,0.053,1/m,suspect,"does not follow from the depths in Table 2, even allowing for rounding (DATA.md section 22)"
94
+ 4,IB,33,54,10,3,0.057,1/m,ok,
95
+ 4,IB,54,70,3,1,0.068,1/m,ok,
96
+ 5,IB,0,2,,,0.144,1/m,ok,
97
+ 5,IB,2,5,,,0.107,1/m,ok,
98
+ 5,IB,5,10,,,0.071,1/m,ok,
99
+ 5,IB,10,15,,,0.061,1/m,ok,
100
+ 5,IB,15,20,,,0.056,1/m,ok,
101
+ 5,IB,20,30,,,0.057,1/m,ok,
102
+ 5,IB,30,40,,,0.058,1/m,ok,
103
+ 5,IB,40,50,,,0.059,1/m,ok,
104
+ 5,IB,50,60,,,0.064,1/m,ok,
105
+ 5,IB,60,70,,,0.069,1/m,ok,
106
+ 2,II,10.5,10.5,30,30,10.5,m,ok,
107
+ 2,II,24,24,10,10,24,m,ok,
108
+ 2,II,39,39,3,3,39,m,ok,
109
+ 2,II,52,52,1,1,52,m,ok,
110
+ 2n,II,,,30,30,0.44,z/z(10),ok,
111
+ 2n,II,,,3,3,1.63,z/z(10),ok,
112
+ 2n,II,,,1,1,2.17,z/z(10),ok,
113
+ 3,II,0,0,,,100,percent,ok,
114
+ 3,II,2,2,,,70,percent,ok,
115
+ 3,II,5,5,,,48,percent,ok,
116
+ 3,II,10,10,,,32,percent,ok,
117
+ 3,II,15,15,,,21,percent,ok,
118
+ 3,II,20,20,,,14.2,percent,ok,
119
+ 3,II,30,30,,,6.4,percent,ok,
120
+ 3,II,40,40,,,2.8,percent,ok,
121
+ 3,II,50,50,,,1.23,percent,ok,
122
+ 4,II,0,10.5,100,30,0.114,1/m,ok,
123
+ 4,II,10.5,24,30,10,0.082,1/m,ok,
124
+ 4,II,24,39,10,3,0.08,1/m,ok,
125
+ 4,II,39,52,3,1,0.084,1/m,ok,
126
+ 5,II,0,2,,,0.178,1/m,ok,
127
+ 5,II,2,5,,,0.126,1/m,ok,
128
+ 5,II,5,10,,,0.099,1/m,suspect,"does not follow from the percentages in Table 3, even allowing for rounding (DATA.md section 22)"
129
+ 5,II,10,15,,,0.084,1/m,ok,
130
+ 5,II,15,20,,,0.078,1/m,ok,
131
+ 5,II,20,30,,,0.08,1/m,ok,
132
+ 5,II,30,40,,,0.082,1/m,ok,
133
+ 5,II,40,50,,,0.082,1/m,ok,
134
+ 2,III,6.5,6.5,30,30,6.5,m,ok,
135
+ 2,III,14,14,10,10,14,m,ok,
136
+ 2,III,23,23,3,3,23,m,ok,
137
+ 2,III,31,31,1,1,31,m,ok,
138
+ 2n,III,,,30,30,0.46,z/z(10),ok,
139
+ 2n,III,,,3,3,1.64,z/z(10),ok,
140
+ 2n,III,,,1,1,2.21,z/z(10),ok,
141
+ 3,III,0,0,,,100,percent,ok,
142
+ 3,III,2,2,,,63,percent,ok,
143
+ 3,III,5,5,,,38,percent,ok,
144
+ 3,III,10,10,,,17.5,percent,ok,
145
+ 3,III,15,15,,,8.5,percent,ok,
146
+ 3,III,20,20,,,4.6,percent,ok,
147
+ 3,III,30,30,,,1.22,percent,ok,
148
+ 4,III,0,6.5,100,30,0.185,1/m,ok,
149
+ 4,III,6.5,14,30,10,0.146,1/m,ok,
150
+ 4,III,14,23,10,3,0.134,1/m,ok,
151
+ 4,III,23,31,3,1,0.137,1/m,ok,
152
+ 5,III,0,2,,,0.23,1/m,ok,
153
+ 5,III,2,5,,,0.169,1/m,ok,
154
+ 5,III,5,10,,,0.155,1/m,ok,
155
+ 5,III,10,15,,,0.144,1/m,ok,
156
+ 5,III,15,20,,,0.123,1/m,ok,
157
+ 5,III,20,30,,,0.134,1/m,ok,
158
+ 2,1C,5.3,5.3,30,30,5.3,m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
159
+ 2,1C,12,12,10,10,12,m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
160
+ 2,1C,20,20,3,3,20,m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
161
+ 2,1C,27,27,1,1,27,m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
162
+ 2n,1C,,,30,30,0.44,z/z(10),ok,printed as coastal type '1'; written 1C here as elsewhere in this package
163
+ 2n,1C,,,3,3,1.67,z/z(10),ok,printed as coastal type '1'; written 1C here as elsewhere in this package
164
+ 2n,1C,,,1,1,2.25,z/z(10),ok,printed as coastal type '1'; written 1C here as elsewhere in this package
165
+ 3,1C,0,0,,,100,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
166
+ 3,1C,2,2,,,54,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
167
+ 3,1C,5,5,,,30,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
168
+ 3,1C,10,10,,,13.3,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
169
+ 3,1C,15,15,,,6.0,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
170
+ 3,1C,20,20,,,3.0,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
171
+ 3,1C,30,30,,,0.65,percent,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
172
+ 4,1C,0,5.3,100,30,0.227,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
173
+ 4,1C,5.3,12,30,10,0.164,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
174
+ 4,1C,12,20,10,3,0.15,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
175
+ 4,1C,20,27,3,1,0.157,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
176
+ 5,1C,0,2,,,0.31,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
177
+ 5,1C,2,5,,,0.196,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
178
+ 5,1C,5,10,,,0.163,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
179
+ 5,1C,10,15,,,0.154,1/m,suspect,"does not follow from the percentages in Table 3, even allowing for rounding (DATA.md section 22)"
180
+ 5,1C,15,20,,,0.14,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
181
+ 5,1C,20,30,,,0.153,1/m,ok,printed as coastal type '1'; written 1C here as elsewhere in this package
182
+ 2,3C,3.7,3.7,30,30,3.7,m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
183
+ 2,3C,8.5,8.5,10,10,8.5,m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
184
+ 2,3C,14,14,3,3,14,m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
185
+ 2,3C,19,19,1,1,19,m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
186
+ 2n,3C,,,30,30,0.44,z/z(10),ok,printed as coastal type '3'; written 3C here as elsewhere in this package
187
+ 2n,3C,,,3,3,1.65,z/z(10),ok,printed as coastal type '3'; written 3C here as elsewhere in this package
188
+ 2n,3C,,,1,1,2.23,z/z(10),ok,printed as coastal type '3'; written 3C here as elsewhere in this package
189
+ 3,3C,0,0,,,100,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
190
+ 3,3C,2,2,,,44,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
191
+ 3,3C,5,5,,,21,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
192
+ 3,3C,10,10,,,7.0,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
193
+ 3,3C,15,15,,,2.3,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
194
+ 3,3C,20,20,,,0.73,percent,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
195
+ 4,3C,0,3.7,100,30,0.325,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
196
+ 4,3C,3.7,8.5,30,10,0.229,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
197
+ 4,3C,8.5,14,10,3,0.218,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
198
+ 4,3C,14,19,3,1,0.22,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
199
+ 5,3C,0,2,,,0.41,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
200
+ 5,3C,2,5,,,0.25,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
201
+ 5,3C,5,10,,,0.22,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
202
+ 5,3C,10,15,,,0.22,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
203
+ 5,3C,15,20,,,0.23,1/m,ok,printed as coastal type '3'; written 3C here as elsewhere in this package
@@ -27,10 +27,19 @@ Three things are stated rather than hidden:
27
27
 
28
28
  from __future__ import annotations
29
29
 
30
+ import warnings
31
+ from dataclasses import dataclass
32
+
30
33
  import numpy as np
31
34
 
32
35
  from . import _data
33
- from .water import Descent, MissingQuantityError, Water, _require_number, water
36
+ from .water import (
37
+ Descent,
38
+ MissingQuantityError,
39
+ ProvenanceWarning,
40
+ Water,
41
+ _require_number,
42
+ )
34
43
 
35
44
  #: Planck constant (J s), speed of light (m/s) and Avogadro constant (1/mol),
36
45
  #: exact by the 2019 definition of the SI.
@@ -241,18 +250,20 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
241
250
  f"the descent stops at {deepest:g} m, above the deepest depth "
242
251
  f"asked for ({depths.max():g} m)"
243
252
  )
244
- layers = [(top, bottom, water(t, kd.source).kd(sub_wl))
245
- for top, bottom, t in kd.layers]
253
+ if len(kd._kd) != len(kd.layers):
254
+ raise MissingQuantityError(
255
+ "this Descent was not made by descend(), so it does not "
256
+ "carry the Kd of its layers"
257
+ )
246
258
  kd_from = (f"a descent from Jerlov {kd.surface_water_type} "
247
259
  f"({kd.source})")
260
+ descent = kd
248
261
 
249
262
  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
263
+ # The descent's own layers, on the samples PAR rests on only.
264
+ return descent._optical_depth(z, near)
254
265
 
255
- kd_values = (np.vstack([v for _, _, v in layers]) if layers
266
+ kd_values = (np.vstack([v[near] for v in kd._kd]) if kd._kd
256
267
  else np.zeros_like(sub_wl))
257
268
  else:
258
269
  if isinstance(kd, Water):
@@ -291,3 +302,111 @@ def par_profile(surface_downwelling, wavelengths, depths_m, *, kd,
291
302
  return np.interp(band, sub_wl, optical_depth(z))
292
303
 
293
304
  return ParProfile(depths, band, surface_q, tau, deepest, kd_from)
305
+
306
+
307
+ QUANTA_BAND_NM = (350.0, 700.0)
308
+ """The waveband of Jerlov's quanta meters, nm. Not that of PAR."""
309
+
310
+
311
+ @dataclass(frozen=True, eq=False, repr=False)
312
+ class QuantaLevels:
313
+ """Jerlov's measured profiles of quanta irradiance for one water type.
314
+
315
+ Built by :func:`jerlov1977_quanta`. Every depth is in m, every
316
+ coefficient in 1/m, and the band is 350-700 nm, :data:`QUANTA_BAND_NM`,
317
+ not the 400-700 nm of :func:`par_profile`.
318
+ """
319
+
320
+ water_type: str
321
+ level_percent: tuple[float, ...]
322
+ """30, 10, 3 and 1: the levels of Table 2."""
323
+ level_depth_m: np.ndarray
324
+ """The depth of each level, Table 2."""
325
+ profile_depth_m: np.ndarray
326
+ """Table 3's depths, as far as the table goes for this type."""
327
+ profile_percent: np.ndarray
328
+ """Percent of surface quanta at :attr:`profile_depth_m`, Table 3."""
329
+ layer_m: tuple[tuple[float, float], ...]
330
+ """``(top, bottom)`` of each layer of Table 5."""
331
+ layer_kd: np.ndarray
332
+ """Kd for quanta in each layer, Table 5."""
333
+ suspect: tuple[str, ...]
334
+ """The layers of :attr:`layer_kd` whose printed Kd does not follow from
335
+ Table 3, even allowing for rounding; DATA.md section 22."""
336
+
337
+ def __repr__(self) -> str:
338
+ levels = ", ".join(f"{p:g}% {z:g} m" for p, z in
339
+ zip(self.level_percent, self.level_depth_m))
340
+ flag = f"; {len(self.suspect)} suspect" if self.suspect else ""
341
+ return f"<QuantaLevels {self.water_type}: {levels}{flag}>"
342
+
343
+
344
+ def jerlov1977_quanta(water_type: str) -> QuantaLevels:
345
+ """Jerlov's measured quanta irradiance, 350-700 nm, by water type.
346
+
347
+ From profiles measured with quanta meters in fourteen regions, Jerlov
348
+ tabulated the depth at which 30, 10, 3 and 1 percent of the surface
349
+ quanta remain (Table 2), the percentage at depth (Table 3) and Kd for
350
+ quanta layer by layer (Table 5), for types I, IA, IB, II, III, 1C and 3C.
351
+ They are measurements, so they are what to compare :func:`par_profile`
352
+ against; DATA.md section 22 does, and finds agreement within 2 m at
353
+ every level from II to 3C, and a 1 percent depth 33 percent too deep
354
+ for IB.
355
+
356
+ Jerlov, N. G., "Classification of sea water in terms of quanta
357
+ irradiance", J. Cons. int. Explor. Mer 37(3), 281-287.
358
+
359
+ Notes
360
+ -----
361
+ The band is 350-700 nm, wider than PAR's. The profiles are of real
362
+ water at real stations, so they include whatever changed with depth
363
+ there; Jerlov says so himself (p. 286). Three of the Kd values in Table
364
+ 5 do not follow from Table 3, even allowing for rounding: for IA, II
365
+ and 1C, one layer each. They are listed in :attr:`QuantaLevels.suspect`,
366
+ and asking for those types warns. Table 2's normalised column and Table
367
+ 4, which have five more such values, are in the shipped table
368
+ ``jerlov1977_quanta.csv`` but not returned here, since both follow from
369
+ the depths that are.
370
+ """
371
+ rows = [r for r in _data._rows("jerlov1977_quanta.csv")
372
+ if r["water_type"] == water_type]
373
+ if not rows:
374
+ known = []
375
+ for r in _data._rows("jerlov1977_quanta.csv"):
376
+ if r["water_type"] not in known:
377
+ known.append(r["water_type"])
378
+ raise KeyError(f"Jerlov's quanta tables have no water type "
379
+ f"{water_type!r} (known: {', '.join(known)})")
380
+
381
+ def table(name):
382
+ return [r for r in rows if r["table"] == name]
383
+
384
+ layers = table("5")
385
+ suspect = tuple(
386
+ f"Kd {r['value']} 1/m at {r['depth_top_m']}-{r['depth_bottom_m']} m"
387
+ for r in layers if r["status"] in _data.QUESTIONABLE
388
+ )
389
+ if suspect:
390
+ warnings.warn(
391
+ f"Jerlov's Table 5 for {water_type} prints a Kd that does not "
392
+ f"follow from his Table 3: " + "; ".join(suspect)
393
+ + ". See DATA.md section 22.",
394
+ ProvenanceWarning,
395
+ stacklevel=_data.caller_stacklevel(),
396
+ )
397
+ levels = table("2")
398
+ profile = table("3")
399
+ return QuantaLevels(
400
+ water_type=water_type,
401
+ level_percent=tuple(float(r["percent_top"]) for r in levels),
402
+ level_depth_m=_data._frozen(np.array([float(r["value"])
403
+ for r in levels])),
404
+ profile_depth_m=_data._frozen(np.array([float(r["depth_top_m"])
405
+ for r in profile])),
406
+ profile_percent=_data._frozen(np.array([float(r["value"])
407
+ for r in profile])),
408
+ layer_m=tuple((float(r["depth_top_m"]), float(r["depth_bottom_m"]))
409
+ for r in layers),
410
+ layer_kd=_data._frozen(np.array([float(r["value"]) for r in layers])),
411
+ suspect=suspect,
412
+ )
@@ -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.4
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
 
@@ -304,6 +307,18 @@ default, because PAR counts photons and an energy spectrum read as photons is
304
307
  weighted wrongly with no sign of it. This is planar PAR, from Ed; PAR on
305
308
  scalar irradiance is larger by a factor this package cannot supply.
306
309
 
310
+ Jerlov measured quanta irradiance, 350-700 nm, by water type, and his
311
+ tables are shipped for checking against:
312
+
313
+ ```python
314
+ jerlov.jerlov1977_quanta("II") # <QuantaLevels II: 30% 10.5 m, 10% 24 m, 3% 39 m, 1% 52 m; ...>
315
+ ```
316
+
317
+ From II to 3C the package agrees with them within 2 m at every level. For
318
+ Jerlov I it agrees only through `descend`; for IB it puts the 1 percent
319
+ level a third too deep, because Jerlov's IB water grew more turbid below
320
+ 15 m. DATA.md section 22.
321
+
307
322
  ## Which type is my water nearest?
308
323
 
309
324
  ```python
@@ -315,7 +330,9 @@ r.beyond # "clearer" or "more turbid" than every type, or None
315
330
 
316
331
  It ranks the types of one edition; it draws no boundaries, since those differ
317
332
  between editions. A single wavelength is accepted, and ranks oceanic and
318
- coastal types less well apart than a spectrum does.
333
+ coastal types less well apart than a spectrum does. `types=` compares with
334
+ some of the types only, which keeps the wavelengths where only the others
335
+ have gaps.
319
336
 
320
337
  ## Other entry points
321
338
 
@@ -389,8 +406,8 @@ made.
389
406
  ## Provenance and design
390
407
 
391
408
  `DATA.md` records, for every shipped table, where it came from, what was
392
- verified, and what is known to be wrong with it. Twenty-one entries are
393
- documented there: eight confirmed defects in the source literature, three
409
+ verified, and what is known to be wrong with it. Twenty-two entries are
410
+ documented there: nine confirmed defects in the source literature, three
394
411
  questions the first edition of Jerlov settled, and the rest notes.
395
412
 
396
413
  `DECISIONS.md` records why the package is shaped the way it is, including the
@@ -27,6 +27,7 @@ jerlov/data/cie_d65.csv
27
27
  jerlov/data/jerlov1968_kd.csv
28
28
  jerlov/data/jerlov1968_total_irradiance.csv
29
29
  jerlov/data/jerlov1976_kd.csv
30
+ jerlov/data/jerlov1977_quanta.csv
30
31
  jerlov/data/paulson1977_shortwave.csv
31
32
  jerlov/data/petzold1972_stations.csv
32
33
  jerlov/data/petzold1972_vsf.csv
@@ -44,6 +45,7 @@ tests/test_depth.py
44
45
  tests/test_packaging.py
45
46
  tests/test_par.py
46
47
  tests/test_petzold.py
48
+ tests/test_quanta.py
47
49
  tests/test_quoted_figures.py
48
50
  tests/test_reproduces_papers.py
49
51
  tests/test_scene.py
@@ -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.4"
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
 
@@ -0,0 +1,168 @@
1
+ """Jerlov's measured quanta irradiance, and the package checked against it.
2
+
3
+ The tables are transcribed in tools/build_jerlov1977.py, which also checks
4
+ them against each other. Here: that they are read back as printed, and the
5
+ comparison DATA.md section 22 makes between them and what `descend` and
6
+ Jerlov's (1976) Kd predict.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import warnings
13
+
14
+ import numpy as np
15
+ import pytest
16
+
17
+ import jerlov
18
+ from jerlov._data import trapezoid
19
+
20
+ TYPES = ("I", "IA", "IB", "II", "III", "1C", "3C")
21
+ BAND = np.arange(350.0, 701.0, 5.0) # Jerlov's quanta band
22
+ PHOTONS = np.interp(BAND, *jerlov.d65()) * BAND
23
+
24
+
25
+ def _quanta(water_type):
26
+ with warnings.catch_warnings():
27
+ warnings.simplefilter("ignore", jerlov.ProvenanceWarning)
28
+ return jerlov.jerlov1977_quanta(water_type)
29
+
30
+
31
+ def _level_depths(transmittance_at, deepest):
32
+ """Depths of 30, 10, 3 and 1 percent of surface quanta, 350-700 nm."""
33
+ surface = trapezoid(PHOTONS, BAND)
34
+
35
+ def fraction(z):
36
+ return trapezoid(PHOTONS * transmittance_at(z), BAND) / surface
37
+
38
+ out = []
39
+ for level in (0.30, 0.10, 0.03, 0.01):
40
+ low, high = 0.0, deepest
41
+ if fraction(high) > level:
42
+ out.append(None)
43
+ continue
44
+ for _ in range(60):
45
+ middle = 0.5 * (low + high)
46
+ if fraction(middle) > level:
47
+ low = middle
48
+ else:
49
+ high = middle
50
+ out.append(0.5 * (low + high))
51
+ return out
52
+
53
+
54
+ def _one_kd(water_type):
55
+ kd = jerlov.water(water_type, source="jerlov1976").kd(BAND)
56
+ return _level_depths(lambda z: np.exp(-kd * z), 1000.0)
57
+
58
+
59
+ def _layered(water_type):
60
+ d = jerlov.descend(water_type, jerlov.profile_depth(water_type), BAND)
61
+ return _level_depths(d.transmittance_at, d.depth_m)
62
+
63
+
64
+ # -- the tables as printed ---------------------------------------------------
65
+
66
+ def test_table_2_is_read_back_as_printed():
67
+ q = _quanta("I")
68
+ assert q.level_percent == (30.0, 10.0, 3.0, 1.0)
69
+ assert q.level_depth_m.tolist() == [19, 49, 79, 103]
70
+ assert _quanta("3C").level_depth_m.tolist() == [3.7, 8.5, 14, 19]
71
+ assert repr(_quanta("II")) == (
72
+ "<QuantaLevels II: 30% 10.5 m, 10% 24 m, 3% 39 m, 1% 52 m; "
73
+ "1 suspect>")
74
+
75
+
76
+ def test_tables_3_and_5_go_as_deep_as_the_paper():
77
+ q = _quanta("I")
78
+ assert q.profile_depth_m[-1] == 100 and q.profile_percent[-1] == 1.14
79
+ assert q.layer_m[-1] == (90.0, 100.0) and q.layer_kd[-1] == 0.048
80
+ q = _quanta("3C")
81
+ assert q.profile_depth_m.tolist() == [0, 2, 5, 10, 15, 20]
82
+ assert len(q.layer_kd) == 5
83
+ for array in (q.level_depth_m, q.profile_percent, q.layer_kd):
84
+ with pytest.raises(ValueError):
85
+ array[0] = 0.0
86
+
87
+
88
+ def test_the_suspect_layers_warn_and_the_others_do_not():
89
+ for water_type, layer in (("IA", "20.0-30.0"), ("II", "5.0-10.0"),
90
+ ("1C", "10.0-15.0")):
91
+ with pytest.warns(jerlov.ProvenanceWarning, match="Table 5"):
92
+ q = jerlov.jerlov1977_quanta(water_type)
93
+ assert len(q.suspect) == 1 and layer.replace(".0", "") in \
94
+ q.suspect[0].replace(".0", "")
95
+ with warnings.catch_warnings():
96
+ warnings.simplefilter("error")
97
+ for water_type in ("I", "IB", "III", "3C"):
98
+ assert jerlov.jerlov1977_quanta(water_type).suspect == ()
99
+
100
+
101
+ def test_an_unknown_type_names_the_known_ones():
102
+ with pytest.raises(KeyError, match="known: I, IA, IB, II, III, 1C, 3C"):
103
+ jerlov.jerlov1977_quanta("5C")
104
+
105
+
106
+ # -- the package against the measurements: DATA.md section 22 ---------------
107
+
108
+ def test_from_II_to_3C_one_kd_agrees_within_2_m_at_every_level():
109
+ for water_type in ("II", "III", "1C", "3C"):
110
+ model = _one_kd(water_type)
111
+ printed = _quanta(water_type).level_depth_m
112
+ assert max(abs(m - p) for m, p in zip(model, printed)) < 2.0, \
113
+ water_type
114
+
115
+
116
+ def test_jerlov_I_needs_the_depth_profile():
117
+ """One Kd puts the 1 percent level of I at 165 m; Jerlov measured 103;
118
+ layer by layer it is 106."""
119
+ assert round(_one_kd("I")[3]) == 165
120
+ assert round(_layered("I")[3]) == 106
121
+ assert round(_layered("I")[1]) == 49 # 10 percent: 49 printed
122
+
123
+
124
+ def test_IA_and_IB_stay_too_deep():
125
+ """Layer by layer IA reaches 1 percent at 100 m against 87, and IB at
126
+ 93 m against 70: 15 and 33 percent too deep."""
127
+ ia, ib = _layered("IA")[3], _layered("IB")[3]
128
+ assert (round(ia), round(ib)) == (100, 93)
129
+ assert round(100 * (ia / 87 - 1)) == 15
130
+ assert round(100 * (ib / 70 - 1)) == 33
131
+
132
+
133
+ def test_IB_agrees_in_its_own_10_m_and_departs_below():
134
+ """Kd for quanta in IB, Jerlov (1976) against Table 5: within 0.004 1/m
135
+ above 10 m; below 15 m one falls from 0.051 to 0.041, the other rises
136
+ from 0.056 to 0.069."""
137
+ kd = jerlov.water("IB", source="jerlov1976").kd(BAND)
138
+
139
+ def quanta(z):
140
+ return trapezoid(PHOTONS * np.exp(-kd * z), BAND)
141
+
142
+ q = _quanta("IB")
143
+ model = np.array([math.log(quanta(a) / quanta(b)) / (b - a)
144
+ for a, b in q.layer_m])
145
+ top = [i for i, (a, b) in enumerate(q.layer_m) if b <= 10]
146
+ deep = [i for i, (a, b) in enumerate(q.layer_m) if a >= 15]
147
+ assert np.max(np.abs(model[top] - q.layer_kd[top])) < 0.004
148
+ assert (round(model[deep[0]], 3),
149
+ round(model[deep[-1]], 3)) == (0.051, 0.041)
150
+ assert (q.layer_kd[deep[0]], q.layer_kd[deep[-1]]) == (0.056, 0.069)
151
+ assert np.all(np.diff(model[deep]) < 0)
152
+ assert np.all(np.diff(q.layer_kd[deep]) >= 0)
153
+
154
+
155
+ def test_DATA_md_figures_for_the_tables_against_each_other():
156
+ """The recomputed ranges in DATA.md section 22, and integrating Table 5
157
+ down to 10 m for II gives 29 percent against Table 3's 32."""
158
+ def ratio_range(z, z10):
159
+ r = [(z + a) / (z10 + b) for a in (-0.5, 0.5) for b in (-0.5, 0.5)]
160
+ return f"{min(r):.2f}-{max(r):.2f}"
161
+ assert ratio_range(79, 49) == "1.59-1.64"
162
+ assert ratio_range(103, 49) == "2.07-2.13"
163
+ assert ratio_range(66, 41) == "1.58-1.64"
164
+ q = _quanta("II")
165
+ tau = sum(k * (b - a) for (a, b), k in zip(q.layer_m, q.layer_kd)
166
+ if b <= 10)
167
+ assert round(100 * math.exp(-tau)) == 29
168
+ assert q.profile_percent[q.profile_depth_m.tolist().index(10)] == 32
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