Skip to content

Add ASTER GED v3 surface-emissivity dataset#415

Open
xkykai wants to merge 21 commits into
mainfrom
xk/aster-ged
Open

Add ASTER GED v3 surface-emissivity dataset#415
xkykai wants to merge 21 commits into
mainfrom
xk/aster-ged

Conversation

@xkykai

@xkykai xkykai commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Adds ASTERGEDv3 — the ASTER Global Emissivity Dataset v3 (NASA LP DAAC): a static 2000–2008 clear-sky-mean land-surface emissivity climatology on a WGS84 lat/lon grid at 1 km (:low_1km, default) or 100 m (:high_100m), as a DataWrangling submodule.

Usage

using NumericalEarth, ArchGDAL   # ArchGDAL + Earthdata credentials activate the HDF5 read/download

region = BoundingBox(longitude = (-112.8, -111.2), latitude = (35.2, 36.8))
metadatum = Metadatum(:emissivity; dataset = ASTERGEDv3(), region)
ε = Field(metadatum, grid)
land_surface = SurfaceRadiationProperties(albedo = α, emissivity = ε)

What it does

  • Resolves the intersecting 1°×1° HDF5 tiles via NASA CMR, downloads them from Earthdata Cloud (EARTHDATA_USERNAME/EARTHDATA_PASSWORD; tiles cached for reuse), reads them through GDAL's HDF5 driver, and mosaics + clips to the BoundingBox.
  • Collapses the five ASTER TIR bands to one broadband ε with Ogawa & Schmugge (2004) coefficients (they sum to 1, so it is a convex combination), decoding in Float32 (mean ×0.001, SDev ×0.0001). The decoded broadband field is cached as a regional NetCDF.
  • Returns a reduced (Center, Center, Nothing) Field — safely stateindex-able at any k, so it plugs directly into SurfaceRadiationProperties. :emissivity_uncertainty propagates the per-band SDev to a broadband σ(ε).
  • ASTERGEDv3 is exported from NumericalEarth; :high_100m/:low_1km map internally to NASA's AG100/AG1KM short names.

Gap filling — surface-aware inpainting

ASTER GED retrieves an emissivity almost everywhere, including over water (open-ocean broadband ε ≈ 0.98 — a real value, not a fill). Genuine gaps appear only where a clear-sky retrieval was never obtained (persistent cloud in the humid tropics, snow screened as cloud) and decode to NaN. Building a Field fills them with NearestNeighborInpainting(Inf), so the field is finite everywhere.

The fill is surface-aware: the tiles' land/water map is read and written alongside the emissivity, so a land gap is filled only from land neighbors and a water gap only from water — a gap never borrows across the coastline (an isolated gap with no reachable same-class donor falls back to nearest-valid so the field stays finite).

This is generic, opt-in DataWrangling machinery: inpaint_mask! / Field take an optional per-cell regions label field, supplied per dataset via the inpainting_regions hook (ASTER GED returns its land/water partition). With no regions (the default) the inpainting is byte-for-byte unchanged, so ocean datasets (ECCO/EN4/WOA) that deliberately extend values across the coastline are unaffected.

Example — 100 m broadband emissivity

Clear-sky desert (Grand Canyon, gap-free) vs. humid tropics (Congo, cloud gaps inpainted). Plots the Fields directly — no NetCDF round-trip:

using NumericalEarth
using NumericalEarth.DataWrangling: BoundingBox
using Oceananigans
using ArchGDAL      # activates the ASTER GED HDF5 read/download extension
using CairoMakie

dataset = ASTERGEDv3(resolution = :high_100m)

function emissivity_field(dataset, name, longitude, latitude; size = (512, 512))
    grid = LatitudeLongitudeGrid(CPU(); size, longitude, latitude,
                                 topology = (Bounded, Bounded, Flat))
    region = BoundingBox(; longitude, latitude)
    return Field(Metadatum(name; dataset, region), grid)
end

gc_emissivity  = emissivity_field(dataset, :emissivity,             (-112.8, -111.2), (35.2, 36.8))
gc_uncertainty = emissivity_field(dataset, :emissivity_uncertainty, (-112.8, -111.2), (35.2, 36.8))
congo_emissivity = emissivity_field(dataset, :emissivity, (18, 20), (-1, 1))

fig = Figure(size = (1650, 520), backgroundcolor = :white)
Label(fig[0, 1:6], "ASTER GED v3 broadband emissivity (100 m)"; fontsize = 19, font = :bold)

ax1 = Axis(fig[1, 1]; title = "Emissivity — Grand Canyon (4-tile mosaic)",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm1 = heatmap!(ax1, gc_emissivity; colormap = :viridis, colorrange = (0.90, 0.98))
Colorbar(fig[1, 2], hm1; label = "broadband ε")

ax2 = Axis(fig[1, 3]; title = "Uncertainty σ(ε) (SDev path)",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm2 = heatmap!(ax2, gc_uncertainty; colormap = :viridis, colorrange = (0, 0.02))
Colorbar(fig[1, 4], hm2; label = "broadband σ(ε)")

ax3 = Axis(fig[1, 5]; title = "Emissivity — Congo basin (cloud gaps inpainted)",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm3 = heatmap!(ax3, congo_emissivity; colormap = :viridis, colorrange = (0.90, 0.98))
Colorbar(fig[1, 6], hm3; label = "broadband ε")

save("asterged_emissivity_map_100m.png", fig)

Notes / follow-ups

  • The broadband coefficients are a Sahara-desert regression applied globally; the static clear-sky mean also misses snow (ε 0.03–0.08 higher) and soil-moisture effects.
  • Docs @example page: follow-up.

🤖 Generated with Claude Code

Standalone static ASTERGED submodule (HDF5 lat/lon, not MODIS sinusoidal)
implementing broadband longwave surface emissivity from ASTER GED v3.

- ASTERGEDv3{R} <: AbstractStaticDataset with resolution (:AG100/:AG1km)
  and tunable 5-band broadband_coefficients (Ogawa 2003, normalized to a
  convex combination).
- Pure, unit-tested core: decode_mean (0.001 scale), decode_sdev (0.0001
  scale), broadband_emissivity/uncertainty, band-first broadband maps,
  water masking (documented LWmap coding parameter).
- CopernicusDEM-style regional windowing: BoundingBox required via
  validate_dataset_coverage, region+resolution-encoded metadata_filename,
  is_three_dimensional=false, default_inpainting=nothing,
  location=(Center,Center,Center), missing_value=-9999.
- HDF5 read + Earthdata download gated behind fallback errors (HDF5.jl is
  not a dependency and must not be added to root Project.toml; credentials
  required). Pure logic works and is tested without them.
- Registered in DataWrangling.jl and NumericalEarth.jl.
- Synthetic-tile tests in test/test_asterged.jl (56 assertions pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.17814% with 128 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ext/NumericalEarthArchGDALExt.jl 0.00% 101 Missing ⚠️
src/DataWrangling/ASTERGED/ASTERGED.jl 76.53% 23 Missing ⚠️
src/DataWrangling/inpainting.jl 89.18% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

xkykai and others added 2 commits July 6, 2026 10:56
Implement the ASTER GED v3 (AG100) HDF5 read + Earthdata/CMR download in the
ArchGDAL extension, replacing the erroring stubs, so
`Field(Metadatum(:emissivity; dataset=ASTERGEDv3(), region), grid)` returns real
broadband emissivity.

ext/NumericalEarthArchGDALExt.jl:
- `earthdata_cmr_granules` — CMR granules.json query for AG100.v003 tiles
  intersecting a BoundingBox, de-duplicated by granule.
- `earthdata_download` — authenticated fetch via `netrc_downloader` against
  urs.earthdata.nasa.gov (EARTHDATA_USERNAME / EARTHDATA_PASSWORD).
- `asterged_tiles_to_netcdf` — read `/Emissivity/Mean`, `/Emissivity/SDev` and
  `/Land_Water_Map/LWmap` via GDAL's HDF5 driver (`HDF5:"file"://path`), build
  lon/lat from `/Geolocation/*` (no reprojection — plain WGS84 lat/lon), clip +
  mosaic to the bbox, write a regional NetCDF of raw digital numbers.

src ASTERGED module:
- `retrieve_data` now reads that NetCDF and feeds the existing pure
  decode_mean / broadband_emissivity_map / mask_water core.
- `Downloads.download` dispatches to the extension; generic module fallbacks
  error clearly when ArchGDAL is not loaded, and the extension methods are
  strictly more specific so they add rather than overwrite (Julia 1.12
  precompile-time method overwriting is now a hard error).
- Add lon/lat name traits, CMR helpers, flip `reversed_latitude_axis` to false
  (NetCDF written south-to-north).

Fix `ASTERGED_WATER_CODE` from 2 to 1: verified on real tile AG100.v003.37.-112
that `/Land_Water_Map/LWmap` uses the 0=land / 1=water coding (851 water cells
trace the Colorado River). Update the water-masking test accordingly.

Verified live end-to-end: emissivity in [0.909, 0.979], all in [0.7, 1.0];
uncertainty path and 56/56 unit tests also pass. See STATUS report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iation

- Replace the misattributed, intercept-dropped Cheng et al. (2013) slopes
  with Ogawa & Schmugge (2004) coefficients: natively convex (sum exactly 1)
  over the 8.0-13.5 um window. The dropped intercept biased broadband
  emissivity ~0.02 low over low-emissivity deserts.
- Fill water cells with dataset.water_emissivity (default 0.985) instead of
  NaN, and inpaint retrieval gaps (NearestNeighborInpainting(Inf) default,
  cached) so no NaN reaches downstream flux kernels.
- Reduced (Center, Center, Nothing) location: the emissivity Field is
  stateindex-able at any k and slots directly into
  SurfaceRadiationProperties(albedo, emissivity = field).
  Export ASTERGEDv3 from NumericalEarth.
- Decode digital numbers in Float32 (matches eltype(Metadata), halves the
  host-to-device copy), retry tile downloads 3x with backoff, and guard
  unbounded regions on the download path.
- Remove two stale explicit imports (would fail the ExplicitImports QA test).
- Add an architecture-parameterized synthetic-NetCDF test exercising the
  full retrieve/decode/fill/inpaint/Field pipeline and the radiation wiring
  on CPU and GPU without credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xkykai

xkykai commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Running the script above gives
asterged_emissivity_map_100m

xkykai and others added 3 commits July 8, 2026 13:39
- Concretize the struct (broadband_coefficients::Vector{Float64},
  water_emissivity::Float64) so it is no longer a UnionAll. It now appears in
  supported_datasets(), matching sibling datasets ETOPO2022/SoilGrids2/WOA.
  The fields are CPU-only load-time config, never in a kernel, so concrete
  field types cost nothing.
- Set the default water_emissivity to 0.97, matching NumericalEarth's ocean
  surface emissivity, so a coupled domain has one consistent water value.
- Test that ASTERGEDv3 is in supported_datasets().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xkykai
xkykai marked this pull request as ready for review July 8, 2026 20:45
Add the ogawa2004mapping bib entry (DOI verified via Crossref) and cite it
inline in the ASTERGEDv3 docstring via [Author (year)](@cite key), per the
project citation convention; add the DOI to the coefficient constant's comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xkykai
xkykai requested review from bgroenks96 and glwagner July 8, 2026 20:51
@glwagner

glwagner commented Jul 9, 2026

Copy link
Copy Markdown
Member

Why did you "save for plotting" to NetCDF? is there some issue plotting the Field?

Comment thread ext/NumericalEarthArchGDALExt.jl Outdated
Comment thread src/DataWrangling/ASTERGED/ASTERGED.jl Outdated
Comment thread src/DataWrangling/ASTERGED/ASTERGED.jl Outdated
Comment thread src/DataWrangling/ASTERGED/ASTERGED.jl Outdated
"""
function broadband_emissivity_map(decoded_bands, coefficients)
_, Nx, Ny = size(decoded_bands)
result = Array{Float32}(undef, Nx, Ny)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be done on GPU? Or at least permitted?

@glwagner

glwagner commented Jul 9, 2026

Copy link
Copy Markdown
Member

It might make sense to add a section in the docs tutorial for land data + computations that are done here. It can be a future PR though.

@glwagner

glwagner commented Jul 9, 2026

Copy link
Copy Markdown
Member

Another note to self is that I am wondering how this will all work in a distributed context, eg if we run 100m resolution globally, we want to do the calculations in here independently on every node. It's ok to put optimizations in a future PR but we should think about it a bit.

xkykai and others added 2 commits July 9, 2026 09:14
The 0.97 water-surface emissivity was hardcoded at every ocean_surface
default and in the ASTERGEDv3 water fill. Follow the
default_stefan_boltzmann_constant pattern: one exported constant in
Radiations, referenced everywhere, so the values cannot drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite broadband_emissivity_map / broadband_uncertainty_map as
broadcast + band-dimension reductions so they run on whatever
architecture holds the decoded arrays (results are bitwise identical
to the previous CPU loop). Use the shared default_water_emissivity,
note that :AG100/:AG1km mirror NASA's product short names, and note in
the extension that the tile .h5 is the download unit (Mean, SDev and
LWmap are subdatasets of one file).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xkykai

xkykai commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Why did you "save for plotting" to NetCDF? is there some issue plotting the Field?

no there's nothing wrong with it. I was just stitching together two vibe-coded scripts because it'll take a while to rerun. but a script like

using NumericalEarth
using NumericalEarth.DataWrangling: BoundingBox, Metadatum
using Oceananigans
using ArchGDAL  # activates the HDF5 tile read/download extension
using CairoMakie

dataset = ASTERGEDv3()

#####
##### 1. Multi-tile static emissivity map — US Southwest (Grand Canyon / Colorado Plateau)
#####

southwest = BoundingBox(longitude = (-112.8, -111.2), latitude = (35.2, 36.8))
southwest_grid = LatitudeLongitudeGrid(CPU(), Float32;
                                       size = (320, 320),
                                       longitude = (-112.8, -111.2),
                                       latitude = (35.2, 36.8),
                                       topology = (Bounded, Bounded, Flat))

emissivity  = Field(Metadatum(:emissivity; dataset, region = southwest), southwest_grid)
uncertainty = Field(Metadatum(:emissivity_uncertainty; dataset, region = southwest), southwest_grid)

#####
##### 2. Coverage-hole probe — Congo basin (persistent cloud)
#####

congo = BoundingBox(longitude = (18, 20), latitude = (-1, 1))
congo_grid = LatitudeLongitudeGrid(CPU(), Float32;
                                   size = (200, 200),
                                   longitude = (18, 20),
                                   latitude = (-1, 1),
                                   topology = (Bounded, Bounded, Flat))

congo_emissivity = Field(Metadatum(:emissivity; dataset, region = congo), congo_grid)

#####
##### Plot the Fields directly — Oceananigans' Makie extension supplies the coordinates
#####

fig = Figure(size = (1650, 520))
Label(fig[0, 1:6], "ASTER GED v3 AG100 static broadband emissivity"; fontsize = 19, font = :bold)

ax1 = Axis(fig[1, 1]; title = "Emissivity — US Southwest (4-tile mosaic)",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm1 = heatmap!(ax1, emissivity; colormap = :viridis, colorrange = (0.90, 0.98))
Colorbar(fig[1, 2], hm1; label = "broadband ε")

ax2 = Axis(fig[1, 3]; title = "Uncertainty (SDev path)",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm2 = heatmap!(ax2, uncertainty; colormap = :viridis, colorrange = (0, 0.02))
Colorbar(fig[1, 4], hm2; label = "broadband σ(ε)")

ax3 = Axis(fig[1, 5]; title = "Congo basin — coverage holes inpainted, water filled",
           xlabel = "longitude (°)", ylabel = "latitude (°)", aspect = 1)
hm3 = heatmap!(ax3, congo_emissivity; colormap = :viridis, colorrange = (0.90, 0.98))
Colorbar(fig[1, 6], hm3; label = "broadband ε")

save("asterged_emissivity_static_map.png", fig)

would work fine.

xkykai and others added 2 commits July 9, 2026 10:19
The struct's user-facing resolution no longer follows NASA's AG100/AG1KM
product short names; those stay internal to CMR queries and tile
downloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xkykai

xkykai commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Another note to self is that I am wondering how this will all work in a distributed context, eg if we run 100m resolution globally, we want to do the calculations in here independently on every node. It's ok to put optimizations in a future PR but we should think about it a bit.

Suggestions by Fable 5:

  1. Each rank should build Metadatum with its own BoundingBox
  2. Derive each rank's BoundingBox automatically from its local grid (plus an interpolation margin).
  3. Each rank must fetch its own dataset.
  4. A persistent granule-keyed tile cache instead so neighboring ranks sharing a boundary tile don't re-download it.
  5. Gap inpainting is window-local: a rank whose window lies entirely inside a retrieval gap (humid-tropics cloud holes) has no valid source — either exchange halos between inpainting iterations or pre-fill gaps from the "1km" product.

xkykai and others added 2 commits July 9, 2026 10:30
More informative than :highresolution/:lowresolution: the names carry
both the qualitative ranking and the physical resolution (100 m / 1 km).
Underscore per the codebase's snake_case symbol convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread ext/NumericalEarthArchGDALExt.jl Outdated
xkykai added 2 commits July 10, 2026 10:27
Refactor Earthdata credentials retrieval to use get() for safer handling of environment variables.
xkykai and others added 6 commits July 20, 2026 11:17
- Removed unused water emissivity handling and associated constants.
- Updated dataset resolution defaults and descriptions for clarity.
- Simplified broadband emissivity and uncertainty calculations.
- Refactored tile placement logic to handle NaN values correctly.
- Enhanced test coverage for broadband synthesis and tile placement.
- Adjusted metadata handling to ensure variable independence in filenames.
- Improved documentation and comments throughout the codebase.
… queries, improve longitude folding in tile placement, and add axis spacing function to prevent divide-by-zero errors.
…val, gap filling, and land/water map handling.
…g land/water map, update related documentation, and add tests for region-gated inpainting functionality.
@xkykai
xkykai requested review from bgroenks96 and glwagner July 21, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants