Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Reflectance

Radiometry, SRFs, and irradiance

UNEP
IMEO
MARS

Module: georeader/reflectance.py (971 LOC, 97 box-drawing characters — third densest in the package) Role: convert satellite imagery between physically meaningful radiometric quantities — radiance, top-of-atmosphere (ToA) reflectance, and band-integrated irradiance. Where the package crosses from “geospatial bookkeeping” into actual physics.


1. The job

Satellite imagery is delivered in any of three radiometric units depending on the sensor / processing level:

This module provides:


2. Unit conversion overview

┌─────────────────────────────────────────────────────────────────────────┐
│                    RADIOMETRIC UNIT CONVERSION FLOW                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Raw DN ──────►  Radiance ──────────────────────────►  Reflectance     │
│   (counts)        (W/m²/sr/nm)                          (unitless 0-1)  │
│                                                                         │
│   Supported radiance units:                                             │
│   ┌────────────────────────────────────────────────────────────────┐    │
│   │  Unit              │  Factor to W/m²/sr/nm                     │    │
│   ├────────────────────┼───────────────────────────────────────────┤    │
│   │  W/m²/sr/nm        │  1.0         (no conversion)              │    │
│   │  mW/m²/sr/nm       │  ÷ 1000      (milli → base)               │    │
│   │  µW/cm²/sr/nm      │  ÷ 100       (micro/cm² → base/m²)        │    │
│   └────────────────────┴───────────────────────────────────────────┘    │
│                                                                         │
│   Solar Irradiance: W/m²/nm or mW/m²/nm (at TOA, perpendicular)         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

The DN→radiance step is sensor-specific (linear scale + offset from per-band calibration coefficients) and lives inside the per-sensor reader, not this module. Once you have radiance, this module handles the rest.

The three supported radiance unit systems all ship in real-world products:

The function takes a units= string and applies the conversion internally, so user code stays in whatever units the sensor delivered.


3. The reflectance equation

The ToA reflectance formula:

ρ = (π × d² × L) / (E_sun × cos(θ_z))

where:
- L      = at-sensor radiance (W/m²/sr/nm)
- E_sun  = solar irradiance at TOA (W/m²/nm)
- d      = Earth-Sun distance in AU (varies ~3% annually)
- θ_z    = solar zenith angle (0° = Sun overhead)

Three corrections combined:

The module factors them into obfactor = π × d² / cos(θ_z)observation_date_correction_factor — so the reflectance line collapses to ρ = L × obfactor / E_sun.

┌─────────────────────────────────────────────────────────────────┐
│  UNIT CONVERSION FLOW                                            │
│                                                                   │
│  Input radiance        Normalized radiance      Output           │
│  (various units)   →   (W/m²/sr/nm)         →   reflectance     │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ Input Unit       │ factor_div │ Conversion                  │ │
│  ├──────────────────┼────────────┼─────────────────────────────┤ │
│  │ W/m²/sr/nm       │ 1          │ No conversion               │ │
│  │ mW/m²/sr/nm      │ 1000       │ ×10⁻³ (milli → base)       │ │
│  │ µW/cm²/sr/nm     │ 100        │ ×10⁻⁶×10⁴ = ×10⁻²         │ │
│  └──────────────────┴────────────┴─────────────────────────────┘ │
│                                                                   │
│  Final calculation:                                              │
│                                                                   │
│  L [W/m²/sr/nm] × obfactor [sr⁻¹] / E_sun [W/m²/nm]            │
│  = dimensionless reflectance                                     │
│                                                                   │
│  Note: The steradian cancels with implicit assumptions about     │
│  the solar disk's solid angle as seen from Earth.               │
└─────────────────────────────────────────────────────────────────┘

4. Earth-sun distance

┌────────────────────────────────────────────────────────────────────┐
│       Earth-Sun Distance Throughout the Year                       │
│                                                                    │
│  Distance   ▲                                                      │
│  (AU)       │     Aphelion (~July 4)                               │
│             │          ╭───────╮                                   │
│  1.017 ─────┼─────────╱         ╲─────────────────────────         │
│             │        ╱           ╲                                 │
│  1.000 ─────┼───────╱─────────────╲──────────────────────          │
│             │      ╱               ╲                               │
│  0.983 ─────┼─────╱─────────────────╲────────────────────          │
│             │    ╱    Perihelion     ╲                             │
│             │   ╱     (~Jan 3)        ╲                            │
│             └───┴──────────────────────┴────────────────► Day      │
│                 0    91   182   273   365                          │
│                Jan  Apr   Jul   Oct   Jan                          │
│                                                                    │
│  d = 1 - 0.01673 × cos(0.0172 × (day_of_year - 4))                 │
│                                                                    │
│  Impact: ~6.5% variation in irradiance (d² factor)                 │
└────────────────────────────────────────────────────────────────────┘

The annual ≈ ±1.7% variation in distance becomes ≈ ±3.4% in and therefore ≈ 6.5% peak-to-peak in irradiance over the year. Ignoring this correction biases reflectance time series: you’d see a fake annual cycle of ~3% amplitude with peaks in the Northern winter (smaller d, more apparent radiance, higher uncorrected reflectance).

The closed-form expression d = 1 - 0.01673 × cos(0.0172 × (DOY − 4)) is what earth_sun_distance_correction_factor(date_of_acquisition) returns. It’s accurate to ~0.001 AU — well below the precision needed for radiometric calibration.


5. Spectral response functions

┌─────────────────────────────────────────────────────────────────┐
│  Spectral Response Function Convolution                          │
│                                                                   │
│  Hyperspectral               SRF for Band X             Result   │
│  Radiance L(λ)               R(λ)                       L_X     │
│                                                                   │
│  L(λ)│     ╱╲                R(λ)│   ╱╲                         │
│      │    ╱  ╲╱╲╱╲              │  ╱  ╲                         │
│      │   ╱        ╲             │ ╱    ╲                        │
│      │  ╱          ╲            │╱      ╲                       │
│      └──────────────── λ        └──────────── λ                 │
│            400-2500 nm              λ_center ± FWHM/2            │
│                                                                   │
│  Integration:  L_X = ∫ L(λ) × R(λ) dλ  /  ∫ R(λ) dλ             │
│                                                                   │
│  The SRF is typically Gaussian:                                  │
│  R(λ) = exp(-(λ - λ_center)² / (2σ²))                           │
│  where σ = FWHM / (2 × √(2 × ln(2))) ≈ FWHM / 2.355             │
└─────────────────────────────────────────────────────────────────┘

When you want to take a hyperspectral measurement (~285 narrow bands) and turn it into a multispectral measurement (~12 wide bands matching another sensor’s bands), you integrate the hyperspectral spectrum against the target sensor’s per-band SRF.

Two functions own this:

Why Gaussian? Most published SRFs are well-approximated by Gaussians, and Gaussian SRFs are uniquely specified by (centre, FWHM). The exact published SRFs (S2, Landsat, etc.) tend to have ~1% non-Gaussian deviations; for most ML applications the difference doesn’t matter, but for radiative-transfer studies you’d want to use the exact published curves rather than a Gaussian approximation.

The 2 × √(2 × ln(2)) ≈ 2.355 constant in the formula is the FWHM-to-σ ratio for a Gaussian — comes up again in any “spread parameter” discussion.


6. Band-integrated irradiance

┌────────────────────────────────────────────────────────────────┐
│  Spectral Integration Process                                   │
│                                                                  │
│  E_sun(λ)          R(λ)               E_sun(λ) × R(λ)          │
│  (Solar)           (SRF)              (Product)                 │
│                                                                  │
│    │╲              │ ╱╲                 │  ╱╲                   │
│    │ ╲╲            │╱  ╲                │ ╱  ╲                  │
│    │  ╲╲╲          │    ╲               │╱    ╲                 │
│    │   ╲╲╲╲        │     ╲              │      ╲                │
│    └────────λ      └──────λ            └────────λ              │
│                                         ████████ ← Area = E_k  │
│                                                                  │
│  Solar spectrum   Band response    Weighted → integrate & norm  │
│  (~200-2500 nm)   (Gaussian)       gives band-effective E_sun   │
└────────────────────────────────────────────────────────────────┘

Reflectance computation needs E_sun per band — a single number that summarises the solar spectrum as seen by that band’s SRF. The closed-form:

E_k = ∫ E_sun(λ) × R_k(λ) dλ  /  ∫ R_k(λ) dλ

integrated_irradiance(srf, solar_irradiance=None, epsilon_srf=1e-4) computes the integral per band. If solar_irradiance=None, it loads the Thuillier (2003) reference spectrum bundled with the package as SolarIrradiance_Thuillier.csv (Solar Physics 214). The Thuillier spectrum covers 200–2400 nm at ~1 nm resolution — fine enough for any current orbital sensor.

The epsilon_srf threshold zeroes out SRF values below a tiny floor — important because Gaussian SRFs technically extend forever, and the integration would otherwise pick up out-of-band noise in E_sun(λ).

load_thuillier_irradiance() returns the cached DataFrame with columns ["Nanometer", "Radiance(mW/m2/nm)"]. The THUILLIER_RADIANCE = None module-level cache means it’s loaded lazily on first use.


7. The function reference

Conversion

Geometric correction factors

Spectral integration

Solar reference spectrum


8. Two idiomatic uses

A. Sentinel-2 DN → reflectance (already calibrated; needs only TOA correction):

# Suppose s2_radiance is a (B, H, W) GeoTensor in W/m²/sr/nm
import datetime as dt
toa_refl = reflectance.radiance_to_reflectance(
    data=s2_radiance,
    solar_irradiance=esun_per_band,            # (B,) array of W/m²/nm
    date_of_acquisition=dt.datetime(2024, 6, 15, 10, 30, tzinfo=dt.timezone.utc),
    units="W/m2/sr/nm",
)
# toa_refl is a (B, H, W) GeoTensor in [0, 1]

The solar_irradiance argument is what lets you specialise to any sensor — pass S2’s per-band E_sun for S2, Landsat’s for Landsat. The reader for each sensor typically ships these constants as a module-level array.

B. Hyperspectral EMIT → S2-equivalent multispectral:

# emit_radiance: (285, H, W) hyperspectral cube; wavelengths: (285,) array
# Build S2 SRF (12 bands) at EMIT wavelengths
s2_centers = np.array([443, 490, 560, 665, 705, 740, 783, 842, 865, 945, 1610, 2190])
s2_fwhm    = np.array([20,  65,  35,  30,  15,  15,  20,  115, 20,  20,  90,   180])
s2_srf = reflectance.srf(s2_centers, s2_fwhm, wavelengths=emit_wavelengths)

# Apply
s2_equivalent = reflectance.transform_to_srf(emit_radiance, s2_srf, wavelengths=emit_wavelengths)
# s2_equivalent: (12, H, W) — EMIT data convolved into S2-shaped bands

# Per-band irradiance for the new bands
s2_E = reflectance.integrated_irradiance(pd.DataFrame(s2_srf, index=emit_wavelengths))

This is the “spectral response binning” preset that the geotoolz.md plan mentions for presets.enmap.ENMAP_TO_S2_BANDS — same pattern, applied to EnMAP data.


9. Sharp edges


10. Why this module is denser than its size

971 LOC, 97 box-drawing characters — the third densest in the package because physics requires illustration. The Earth-sun distance plot, the SRF convolution diagram, and the integration visual are doing real explanatory work that prose alone wouldn’t. If you only keep one chapter’s diagrams from this whole tutorial, this is one of the candidates — they’re the ones that explain the why not just the what.


11. Connection to geotoolz

Three concrete operator-shapes from geotoolz.md wrap functions in this module:

The module is also implicitly used inside readers.emit, readers.prisma, and readers.enmap — those readers do DN → radiance internally (per their per-sensor calibration coefficients) and expose the radiance-units result. The reflectance step is then a one-line call to this module.

Next chapter: Save — writing GeoTensors to disk; full Cloud-Optimized GeoTIFF (COG) anatomy.