jerlov
Inherent optical properties of the Jerlov optical water types, with provenance.
https://github.com/tishibashi-cpu/jerlov
pip install jerlov
Every coefficient carries the source it came from. Values that a published table got wrong are flagged rather than quietly repaired, and quantities that the data do not determine are refused rather than guessed.
What is claimed. That the published coefficients are implemented correctly, and that every shipped table can be regenerated from its primary source. The tests demonstrate both, by reproducing each paper's tables from that paper's own equations.
What is not claimed. That this predicts what a camera will record
underwater. That would need measurements which, as far as we can tell, have
not been made. DATA.md records what is uncertain and DECISIONS.md records
where the model stops.
import jerlov
w = jerlov.water("III") # Williamson & Hollins (2022) by default
w.a(550), w.b(550), w.c(550)
Why the source is part of the API
The same Jerlov water type has different coefficients in different papers, and the differences are not small. Solonenko & Mobley (2015) obtained a and b by inverting Kd; Williamson & Hollins (2022) measured them. At 510 nm their scattering coefficients differ by up to a factor of 2.6.
The equations differ too. Both papers use the scattering model of Haltrin (1999), but with different constants: Haltrin's Eq. (6) gives a small-particle coefficient of 1.151302, and Solonenko & Mobley print and compute with 1.513. Reproducing a published table therefore needs that paper's own constant, so constants are attached to sources.
What this package will not do
- Extrapolate. Asking for a wavelength outside the data raises.
- Fill a gap. Where a published value is wrong and could not be recovered, the value is NaN and stays NaN through interpolation.
- Supply bb. The backscattering coefficient is not determined by the
Jerlov classification. Deriving it from the particle concentrations of the
two sources gives answers differing by up to a factor of 31, so
Water.bbrequires an explicitbackscatter_ratio.
Provenance warnings
Interpolating across a value that a paper got wrong gives a number that looks
no different from a sound one. ProvenanceWarning is the only thing that
tells them apart.
>>> w = jerlov.water("5C", source="solonenko2015")
>>> w.b(650)
ProvenanceWarning: b for Jerlov 5C rests on flagged values:
reconstructed at 650 nm. ...
Water.caveats() returns everything known to be doubtful about that water.
Sources
| key | a, b from | types | range |
|---|---|---|---|
williamson2022 |
measurement | IB-5C | 300-800 nm |
solonenko2015 |
inversion of Kd | I-9C | 300-700 nm |
jerlov1976 |
Kd only | I-9C | 300-715 nm |
austin1986 |
Kd only, replacement values | I-1C | 350-700 nm |
jerlov.SOURCES holds the full citation, DOI and caveats for each.
Looking at something through water
For a horizontal path, the observed radiance splits into the target's light that survived and the light the water added along the way:
import numpy as np, jerlov
wl = np.arange(450., 651., 50.)
iops = jerlov.water("III")
kd = jerlov.water("III", source="austin1986")
scene = jerlov.Scene.at_depth(iops, 10.0, np.ones_like(wl), wl, kd=kd)
b_inf = jerlov.veiling_radiance_estimate(
iops, scene.downwelling, wl, backscatter_ratio=0.015
)
obs = scene.observe(np.full_like(wl, 0.8), distance_m=5.0,
veiling_radiance=b_inf)
obs.direct, obs.veiling, obs.radiance
obs.veiling_fraction # how much of what you see is just water
obs.contrast(background) # against another target down the same path
veiling_radiance has no default. B_inf depends on the backscattering
coefficient and the phase function, and neither follows from the water type.
veiling_radiance_estimate gives the usual single-scattering approximation
for callers with nothing better, but it has to be asked for.
Deliberate limits, all recorded in DECISIONS.md:
- Horizontal paths only. Observer and target at the same depth.
- No forward-scatter blur. This is the radiance of one point, not the sharpness of an image.
- Lambertian targets.
What colour is that
white = scene.downwelling / np.pi # a perfect diffuser at that depth
rgb = jerlov.spectrum_to_srgb(obs.radiance, wl, white=white)
jerlov.spectrum_to_xyz(spectrum, wl) # unnormalised CIE XYZ
jerlov.integrate_response(spectrum, wl, sensitivity, sensitivity_wl)
white has no default. A radiance spectrum has no colour until something is
called white, and underwater the useful reference is usually the downwelling
irradiance at that depth rather than daylight. Both are legitimate and they
answer different questions:
| white | question answered |
|---|---|
| downwelling at depth | what a diver's adapted eye, or a camera white-balanced there, sees |
| daylight at the surface | what a camera set at the surface records |
integrate_response takes any spectral sensitivity: three camera channels,
or a set of photoreceptor absorbances.
Two silent errors are checked rather than assumed:
- Coverage. A spectrum spanning 450-650 nm integrated against colour matching functions spanning 360-830 nm quietly drops the ends. Every integration reports how much of the observer it actually covered and warns when it is not essentially all of it.
- Gamut. Underwater colours often fall outside sRGB. Clipping changes
them, so
GamutWarningsays so.
The type changes with depth
The classification is defined on the top 10 m, but clarity does not stay put. Water that is Jerlov I at the surface is typically IB below 40 m; turbid coastal water clears as you go down.
jerlov.water_type_at_depth("I", 60.0) # 'IB'
jerlov.water_type_at_depth("3C", 45.0) # 'II'
jerlov.water_type_at_depth("9C", 15.0) # None: the paper declined to say
None means fewer than ten measurement campaigns supported a declaration, so
nothing is asserted. This is a lookup, not a correction applied on your
behalf.
Other entry points
# Reconstruct a Kd spectrum from one measured value (Austin & Petzold 1986).
jerlov.kd_spectrum(kd=0.06, wavelength_nm=490, at=[440, 550, 650])
# Estimate b from a transmissometer's c (Smart 2007).
jerlov.b_from_c(c=0.5, wavelength_nm=555, bw=0.0019, cw=0.0659)
# Use your own measurements; they take exactly the same path.
jerlov.Water.from_measurements(wavelengths, a=..., b=...)
Provenance and design
DATA.md records, for every shipped table, where it came from, what was
verified, and what is known to be wrong with it. Twelve defects in the source
literature are documented there, seven of them confirmed.
DECISIONS.md records why the package is shaped the way it is, including the
alternatives that were rejected and why.
Licence
Apache-2.0. The Williamson & Hollins data are Crown copyright, Dstl, under
the Open Government Licence v3.0; see NOTICE.
Citation
Cite the concept DOI, which always resolves to the latest version:
Ishibashi, T. jerlov: inherent optical properties of the Jerlov optical water types, with provenance. Zenodo. https://doi.org/10.5281/zenodo.22321312
Please also cite the sources the coefficients came from; they are listed with
their DOIs at the top of DATA.md. The package is a carrier for other
people's measurements, and this work does not replace citing them.
Metadata
Release files for jerlov 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jerlov-0.1.2.tar.gz | 101.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jerlov-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 194.6 kB
Release files / jerlov-0.1.2.tar.gz
| Download URL | jerlov-0.1.2.tar.gz |
|---|---|
| Size | 101.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
68fbb36469ece46beadf839c192c1330ce273f75bd53373189eb9588d9feb074
|
|
BLAKE2b-256 checksum How to use checksums |
d0ce7f58e1fca4d6c18d7fd005c9fd70e7b842a130a3d7469ef0d5baed64b77c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency logRelease files / jerlov-0.1.2-py3-none-any.whl
| Download URL | jerlov-0.1.2-py3-none-any.whl |
|---|---|
| Size | 92.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
abc666d5a44d0805723a2162cbd11b8f3f24e48c36a490aa361c1d4b182fed76
|
|
BLAKE2b-256 checksum How to use checksums |
4dcad9a4d8afbb2f040bcf19990bd310b635dc0f4da5d78315910ed6f67d1bce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency log