The air_temperature module evaluates drought and heat stress risk based on air temperature data from FITNAH 3D climate model rasters. It provides three independent criteria, each registered separately in the catalog:
For day/night: higher temperatures = higher risk. For UHI: higher heat island effect = higher risk (deviation of the modelled near-surface night air temperature from the green/open space mean of 14.8 °C).
bluegreencity_ki/criteria/air_temperature.pytests/test_air_temperature.py (31 tests)bluegreencity_ki/criteria/criteria_catalog.py (factory entries build_day, build_night, build_uhi)The AirTemperature class uses a factory pattern with mode="day"|"night"|"uhi":
from bluegreencity_ki.criteria.air_temperature import AirTemperature
day_criterion = AirTemperature(mode="day") # → air_temperature_day
night_criterion = AirTemperature(mode="night") # → air_temperature_night
uhi_criterion = AirTemperature(mode="uhi") # → uhi_intensity
Registration in the catalog via build_day() / build_night() / build_uhi() factory functions.
uhi_siedlungsraum (settlement areas, excluding buildings) and uhi_verkehr (traffic areas) — two complementary 10 m rasters from the GEONET Klimaanalyse 2022. Values = deviation (K) of the modelled near-surface night air temperature from the green/open space mean (14.8 °C in the study area).outside_coverage_value (default 0.0 K).score = clip((uhi − low) / (high − low), 0, 1)
Method change (issue #171): The previous implementation derived a UHI proxy from the day-night difference ΔT = T_14h − T_04h of the FITNAH rasters with inverted scoring. It was replaced by the proper GEONET Waermeinsel datasets, which model the heat island effect directly. The scoring direction is now direct (higher effect = higher risk) and the extras column
m_uhi_intensity_valuenow stores the UHI effect in K (deviation from the green space mean) instead of ΔT.
Day / Night (direct):
Temperature < temp_low → Risk Score = 0.0 (no risk)
Temperature = temp_low → Risk Score = 0.0
Temperature in range → Risk Score = (temp - temp_low) / (temp_high - temp_low)
Temperature = temp_high → Risk Score = 1.0
Temperature > temp_high → Risk Score = 1.0 (high risk, clipped)
Missing data (NoData) → Risk Score = 0.5 (neutral)
UHI (direct, GEONET Waermeinsel):
UHI effect < uhi_low → Risk Score = 0.0 (green space level, low risk)
UHI effect = uhi_low → Risk Score = 0.0
UHI effect in range → Risk Score = (uhi − uhi_low) / (uhi_high − uhi_low)
UHI effect = uhi_high → Risk Score = 1.0
UHI effect > uhi_high → Risk Score = 1.0 (strong heat island, clipped)
Outside both rasters → UHI effect = outside_coverage_value (default 0.0 K)
No rasters available → Risk Score = 0.5 (neutral)
When crown diameter (kr_durchm) is available and use_zonal_statistics: true is set:
criteria:
- name: "air_temperature_day"
params:
use_zonal_statistics: true # default: true
Advantages:
Metadata fields:
sampling_method: “zonal” or “point”n_zonal: Number of trees sampled via zonal statisticsn_point_fallback: Number of trees sampled via point sampling fallbackWhen use_zonal_statistics: false or no crown diameter column is available:
The criterion supports three methods for threshold determination:
Computes thresholds from the temperatures actually sampled at tree locations:
criteria:
- name: "air_temperature_day"
weight: 1.0
params:
threshold_method: "tree_percentile"
low_percentile: 10 # Coolest 10% of trees → Score 0.0
high_percentile: 90 # Hottest 10% of trees → Score 1.0
use_zonal_statistics: true
How it works:
Advantages over spatial_percentile:
Computes thresholds from the entire raster distribution (all pixels):
criteria:
- name: "air_temperature_day"
params:
threshold_method: "spatial_percentile"
low_percentile: 25 # Coolest 25% of the city
high_percentile: 75 # Hottest 25% of the city
Drawbacks:
Fixed temperature values:
criteria:
- name: "air_temperature_day"
params:
threshold_method: "static"
temp_low: 20.0 # or temp_day_low
temp_high: 30.0 # or temp_day_high
Default thresholds:
criteria:
- name: "air_temperature_day"
weight: 1.0
params:
threshold_method: "tree_percentile"
low_percentile: 5
high_percentile: 95
use_zonal_statistics: true
- name: "air_temperature_night"
weight: 1.0
params:
threshold_method: "tree_percentile"
low_percentile: 5
high_percentile: 95
use_zonal_statistics: true
- name: "uhi_intensity"
weight: 0.5 # example; current calibrated weight in config/config.yaml
params:
threshold_method: "tree_percentile"
low_percentile: 5
high_percentile: 95
use_zonal_statistics: true
uhi_low: 0.0 # static fallback (K) — green space reference
uhi_high: 4.0 # static fallback (K)
outside_coverage_value: 0.0 # UHI effect for trees in green/open spaces
Ensure the temperature rasters are configured in the inputs section:
inputs:
airtemp_raster_14h:
type: "file"
path: "Klimaanalyse Hannover/Grid-Dateien_Klimaanalyse2022/2_21_059_ist_t14_rev00.tif" # relative to raster_base_dir
airtemp_raster_04h:
type: "file"
path: "Klimaanalyse Hannover/Grid-Dateien_Klimaanalyse2022/2_21_059_ist_t04_rev00.tif" # relative to raster_base_dir
# GEONET Waermeinsel rasters (uhi_intensity, issue #171)
uhi_siedlungsraum_raster:
type: "file"
path: "Klimaanalyse Hannover/Waermeinsel/waermeinsel_siedlungsraum.tif"
uhi_verkehr_raster:
type: "file"
path: "Klimaanalyse Hannover/Waermeinsel/waermeinsel_verkehr.tif"
| Parameter | Type | Default | Description |
|---|---|---|---|
threshold_method |
str | "static" |
"tree_percentile", "spatial_percentile", or "static" |
low_percentile |
int | 10 (tree) / 25 (spatial) | Lower percentile for dynamic thresholds |
high_percentile |
int | 90 (tree) / 75 (spatial) | Upper percentile for dynamic thresholds |
use_zonal_statistics |
bool | true |
Enable zonal stats over tree crowns |
temp_low / temp_day_low |
float | 20.0 | Static lower threshold for day (°C) |
temp_high / temp_day_high |
float | 30.0 | Static upper threshold for day (°C) |
temp_night_low |
float | 15.0 | Static lower threshold for night (°C) |
temp_night_high |
float | 22.0 | Static upper threshold for night (°C) |
uhi_low |
float | 0.0 | Static lower UHI effect threshold (K) |
uhi_high |
float | 4.0 | Static upper UHI effect threshold (K) |
outside_coverage_value |
float | 0.0 | UHI effect (K) for trees outside both Waermeinsel rasters (green/open spaces) |
nodata=None → values < -100°C are automatically detected as NoData (fallback)kr_durchm (crown diameter in meters)Each criterion returns its own CriterionResult:
air_temperature_day → score series (0–1) per treeair_temperature_night → score series (0–1) per treeuhi_intensity → score series (0–1) per tree (direct: higher UHI effect = higher risk)m_air_temp_14h_value: Raw 14h temperature per tree (°C)m_air_temp_04h_value: Raw 04h temperature per tree (°C)m_uhi_intensity_value: UHI effect per tree (K, deviation from the green space mean)m_uhi_source: Value origin per tree (siedlungsraum / verkehr / gruenflaeche){
"mode": "day", # or "night"
"threshold_method": "tree_percentile",
"sampling_method": "zonal", # or "point"
"temp_low_threshold": 27.3, # Computed lower threshold
"temp_high_threshold": 31.8, # Computed upper threshold
"temp_min": 22.5, # Min sampled temperature
"temp_max": 34.1, # Max sampled temperature
"temp_mean": 29.2, # Mean sampled temperature
"n_trees_sampled": 48219, # Total trees
"n_valid_samples": 48150, # Trees with valid temperature value
"n_missing_samples": 69, # Trees with NoData
"n_zonal": 42000, # Sampled via zonal statistics
"n_point_fallback": 6150, # Sampled via point sampling (no crown diameter)
}
The air temperature criterion complements:
conda activate <project-conda-env>
python -m pytest tests/test_air_temperature.py -v
31 tests covering:
nodata=None fallback)build_day, build_night, build_uhi)waermeinsel_siedlungsraum.tif + waermeinsel_verkehr.tif, complementary 10 m rasters)outside_coverage_value (0.0 K)m_uhi_intensity_value now stores the UHI effect in K; new extra m_uhi_sourceuhi_low: 0.0, uhi_high: 4.0uhi_intensity — Urban Heat Island intensity based on ΔT = T_14h − T_04h
_sample_trees() helper for both rasters (no code duplication)tree_percentile and static threshold methodsm_uhi_intensity_value (raw ΔT in Kelvin)_sample_trees() helper from compute()uhi_intensity entry in config.yamlbuild_uhi() factory registered as uhi_intensitykr_durchm (crown diameter) from OBSkH enrichmentuse_zonal_statistics: true/falsetree_percentile
air_temperature_day and air_temperature_night are separate catalog entriesnodata=None → -9999 values are now correctly detectedair_temperature_day_* and air_temperature_night_*Last Updated: 2026-03