Metadata-Version: 2.4
Name: jerlov
Version: 0.2.1
Summary: Inherent optical properties of Jerlov water types, with provenance
Author: T. Ishibashi
License-Expression: Apache-2.0
Keywords: ocean optics,Jerlov water types,inherent optical properties
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy>=1.22
Provides-Extra: plot
Requires-Dist: matplotlib; extra == "plot"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Provides-Extra: build
Requires-Dist: openpyxl; extra == "build"
Requires-Dist: colour-science; extra == "build"
Dynamic: license-file

# jerlov

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

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22321312.svg)](https://doi.org/10.5281/zenodo.22321312)

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.

```python
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.

```python
>>> 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:

```python
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))
```

```python
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

```python
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.

```python
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

```python
# 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):

```python
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.
