From d79999343a0665af3b6fb003895dcea0aa70dee0 Mon Sep 17 00:00:00 2001 From: "Gregory L. Wagner" Date: Fri, 25 Sep 2026 04:56:57 +0000 Subject: [PATCH] Add ERA5 single-level blh, cape, cp, lsm, siconc, and fal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds boundary_layer_height, convective_available_potential_energy, convective_precipitation, land_sea_mask, sea_ice_concentration, and forecast_albedo to the ERA5 single-level variable maps. Convective precipitation is accumulated like total precipitation: it gets the same m/hr -> kg/m²/s conversion and the backward hourly averaging window. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/DataWrangling/ERA5/ERA5_single_levels.jl | 23 ++++++++++++++++---- test/test_cds.jl | 14 +++++++++++- test/test_metadata.jl | 6 +++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/DataWrangling/ERA5/ERA5_single_levels.jl b/src/DataWrangling/ERA5/ERA5_single_levels.jl index dfbdb8e7d..7c0c36140 100644 --- a/src/DataWrangling/ERA5/ERA5_single_levels.jl +++ b/src/DataWrangling/ERA5/ERA5_single_levels.jl @@ -36,7 +36,9 @@ end ##### ERA5 single-level variable name mappings ##### -# Variable name mappings from NumericalEarth names to ERA5/CDS API variable names +# Variable name mappings from NumericalEarth names to ERA5/CDS API variable names. +# Invariant fields (surface geopotential, land-sea mask) are requested like instantaneous +# variables: the CDS returns the same field for every date. ERA5_dataset_variable_names = Dict( :temperature => "2m_temperature", :dewpoint_temperature => "2m_dewpoint_temperature", @@ -66,6 +68,12 @@ ERA5_dataset_variable_names = Dict( :total_column_water_vapor => "total_column_water_vapour", :vertically_integrated_eastward_water_vapor_flux => "vertical_integral_of_eastward_water_vapour_flux", :vertically_integrated_northward_water_vapor_flux => "vertical_integral_of_northward_water_vapour_flux", + :boundary_layer_height => "boundary_layer_height", + :convective_available_potential_energy => "convective_available_potential_energy", + :convective_precipitation => "convective_precipitation", + :land_sea_mask => "land_sea_mask", + :sea_ice_concentration => "sea_ice_cover", + :forecast_albedo => "forecast_albedo", ) # NetCDF short variable names (what's actually in the downloaded files) @@ -101,6 +109,12 @@ ERA5_netcdf_variable_names = Dict( :total_column_water_vapor => "tcwv", :vertically_integrated_eastward_water_vapor_flux => "viwve", :vertically_integrated_northward_water_vapor_flux => "viwvn", + :boundary_layer_height => "blh", + :convective_available_potential_energy => "cape", + :convective_precipitation => "cp", + :land_sea_mask => "lsm", + :sea_ice_concentration => "siconc", # shortName: ci + :forecast_albedo => "fal", ) # Variables available for download @@ -116,10 +130,10 @@ DataWrangling.dataset_variable_name(md::ERA5Metadata) = ERA5_netcdf_variable_nam # - `topography` divides the (surface) geopotential by g to give ERA5's model # surface elevation in meters (`geopotential` is left in m² s⁻²); # - downwelling SW/LW are hourly-accumulated energy (J/m²) → mean flux (W/m²); -# - total precipitation is an hourly-accumulated depth (m) → mass flux (kg/m²/s). +# - total and convective precipitation are hourly-accumulated depths (m) → mass flux (kg/m²/s). function DataWrangling.conversion_units(md::ERA5Metadata) - md.name == :topography && return InverseGravity() - md.name == :total_precipitation && return MetersPerHour() + md.name == :topography && return InverseGravity() + md.name in (:total_precipitation, :convective_precipitation) && return MetersPerHour() md.name in (:downwelling_shortwave_radiation, :downwelling_longwave_radiation) && return JoulesPerSquareMeterPerHour() return nothing @@ -130,6 +144,7 @@ DataWrangling.default_inpainting(md::ERA5Metadata) = nothing # ERA5 accumulations and mean rates cover the hour *ending* at the stamp, so their windows run # backwards from it. Every other single-level variable is instantaneous. const ERA5_window_averaged_variables = (:total_precipitation, + :convective_precipitation, :evaporation, :mean_evaporation_rate, :downwelling_shortwave_radiation, diff --git a/test/test_cds.jl b/test/test_cds.jl index 2b26e58f1..61d6bde9e 100644 --- a/test/test_cds.jl +++ b/test/test_cds.jl @@ -42,6 +42,17 @@ start_date = DateTime(2005, 2, 16, 12) @test ERA5_dataset_variable_names[:temperature] == "2m_temperature" @test ERA5_dataset_variable_names[:eastward_velocity] == "10m_u_component_of_wind" @test ERA5_dataset_variable_names[:northward_velocity] == "10m_v_component_of_wind" + + # CDS request names and netcdf short names from the reanalysis-era5-single-levels catalog + for (name, request_name, short_name) in ((:boundary_layer_height, "boundary_layer_height", "blh"), + (:convective_available_potential_energy, "convective_available_potential_energy", "cape"), + (:convective_precipitation, "convective_precipitation", "cp"), + (:land_sea_mask, "land_sea_mask", "lsm"), + (:sea_ice_concentration, "sea_ice_cover", "siconc"), + (:forecast_albedo, "forecast_albedo", "fal")) + @test ERA5_dataset_variable_names[name] == request_name + @test ERA5_netcdf_variable_names[name] == short_name + end end @testset "ERA5 metadata properties" begin @@ -132,11 +143,12 @@ start_date = DateTime(2005, 2, 16, 12) era5m(name) = Metadatum(name; dataset=ds, date=start_date) # Surface geopotential ÷ g → meters; accumulated SW/LW (J/m²) ÷ 3600 → W/m²; - # accumulated precip depth (m) × 1000/3600 → kg/m²/s. Others are unconverted. + # accumulated total and convective precip depth (m) × 1000/3600 → kg/m²/s. Others are unconverted. @test conversion_units(era5m(:topography)) isa InverseGravity @test conversion_units(era5m(:downwelling_shortwave_radiation)) isa Jm²ph @test conversion_units(era5m(:downwelling_longwave_radiation)) isa Jm²ph @test conversion_units(era5m(:total_precipitation)) isa MetersPerHour + @test conversion_units(era5m(:convective_precipitation)) isa MetersPerHour @test conversion_units(era5m(:temperature)) === nothing @test convert_units(3600, Jm²ph()) ≈ 1 # 3600 J/m²/hr → 1 W/m² diff --git a/test/test_metadata.jl b/test/test_metadata.jl index 2eaccb992..de9a2ab7b 100644 --- a/test/test_metadata.jl +++ b/test/test_metadata.jl @@ -296,7 +296,7 @@ end @testset "ERA5 accumulation windows" begin # ERA5 accumulations and mean rates cover the hour ending at the stamp, so their windows # run backwards from it and their nodes sit half an hour before it. - for name in (:total_precipitation, :evaporation, :mean_evaporation_rate, + for name in (:total_precipitation, :convective_precipitation, :evaporation, :mean_evaporation_rate, :downwelling_shortwave_radiation, :downwelling_longwave_radiation, :mean_surface_momentum_flux_x, :mean_surface_momentum_flux_y, :mean_surface_sensible_heat_flux, :mean_surface_latent_heat_flux) @@ -306,7 +306,9 @@ end @test window_center(metadatum) == DateTime(2020, 4, 1, 12, 30) end - for name in (:temperature, :surface_pressure, :eastward_velocity, :significant_wave_height) + for name in (:temperature, :surface_pressure, :eastward_velocity, :significant_wave_height, + :boundary_layer_height, :convective_available_potential_energy, :land_sea_mask, + :sea_ice_concentration, :forecast_albedo) metadatum = Metadatum(name; dataset = ERA5HourlySingleLevel(), date = DateTime(2020, 4, 1, 13)) @test window_center(metadatum) == DateTime(2020, 4, 1, 13) end