Skip to main content

jerlov

Inherent optical properties of the Jerlov optical water types, with provenance.

DOI

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.bb requires an explicit backscatter_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.

Coefficients for the Akkaynak-Treibitz form

Underwater vision work usually writes the same physics as

I = J * exp(-beta_D * r) + B_inf * (1 - exp(-beta_B * r))
p = scene.attenuation_coefficients((0.5, 5.0), veiling_radiance=b_inf)
p.beta_D, p.beta_B, p.B_inf
p.are_distinct      # False
p.note              # why

are_distinct is False, and that is the honest answer. Under single scattering both coefficients are the beam attenuation coefficient c. The central claim of Akkaynak & Treibitz (2018) is that in reality they differ and vary with range, depth and target reflectance; separating them needs measurements this package does not have.

It is still useful. Implementations that fit these coefficients often bound them by guesswork for want of a physical starting point. This gives them one, and says how far it can be trusted.

distance_range_m is required and recorded. A coefficient quoted without the range it describes is not a well-defined quantity, so the API does not let you omit it.

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 GamutWarning says 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=...)

Solar heating of the upper ocean

Ocean circulation models absorb shortwave radiation as a sum of two exponentials, with parameters per Jerlov type from Paulson & Simpson (1977):

jerlov.solar_fraction("IB", 10.0)        # 0.183 of the surface irradiance
p = jerlov.shortwave_parameters("IB")    # R, zeta1_m, zeta2_m

Broadband, 300-2500 nm; nothing to do with the spectral quantities above. The parameters are usually copied from secondary sources, so tools/ refits them from the table the paper fitted, Jerlov (1968) Table XXI, and reproduces R for every row.

They miss their own source by 46 percent at 1 m for types II and III. Two exponentials cannot follow the near-surface decay. DATA.md section 14 has the numbers; anyone heating a thin surface layer should read it.

Only the five oceanic types were fitted. Asking for a coastal type raises rather than substituting a neighbour.

Examples

python examples/sources_disagree.py
python examples/synthetic_underwater_images.py

The first shows why the source is an argument: at 510 nm the scattering coefficient of Jerlov III differs by a factor of 3.8 between the two published sets. The second synthesises how reflectance patches appear at range and depth, white-balanced two ways, and ends with a list of everything it assumed.

Both are run by CI, so an example that has stopped working is a failed build.

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. Seventeen entries are documented there: eight confirmed defects in the source literature, three questions the first edition of Jerlov settled, and the rest notes.

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.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jerlov 0.2.1
File Size Uploaded
jerlov-0.2.1.tar.gz 112.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jerlov 0.2.1
File Interpreter ABI Platform
jerlov-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 212.8 kB

Release files / jerlov-0.2.1.tar.gz

Download URL jerlov-0.2.1.tar.gz
Size 112.4 kB
Tags Source
SHA-256 checksum
How to use checksums
cbbdbfd2c40e628e85850e8e39055583f2833b547881fc407eb2aad26ef7981b
BLAKE2b-256 checksum
How to use checksums
90a9d11896c445eb8f18b0228e4baff353136545957a170eeccf90b5cd9c86b7
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 10, 2026.

Transparency log

Release files / jerlov-0.2.1-py3-none-any.whl

Download URL jerlov-0.2.1-py3-none-any.whl
Size 100.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ab8912aa98b7d2d69f25f19558c0027c8c56ff4c956b8612ef154ea2b8bd1b5b
BLAKE2b-256 checksum
How to use checksums
cee447ed4edcad859d1c2e98f3d21964bb66b42b3904556570b62f6dd0522fe5
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 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.16.0

2 release files

0.15.2

2 release files

0.15.1

2 release files

0.15.0

2 release files

0.14.4

2 release files

0.14.3

2 release files

0.14.2

2 release files

0.14.1

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page