The building_shading module evaluates tree drought risk based on building-induced shading. It provides two independent sub-criteria, each registered separately in the catalog:
Both sub-criteria are computed from Digital Surface Models (DSM) that combine terrain elevation (DGM) with building heights (LOD2). The SVF raster is pre-calculated by scripts/calculate_shadow_criteria.py and stored in data/derived/shadow/; the four sunlit-hours rasters (5 m, astronomical key dates) are provided under raster_base_dir in sunlit hours/ (see inputs in config/config.yaml).
bluegreencity_ki/criteria/building_shading.pytests/test_building_shading.py (18 tests)bluegreencity_ki/criteria/criteria_catalog.py (factory entries build_svf, build_sunlit)scripts/calculate_shadow_criteria.pyThe BuildingShading class uses a factory pattern with mode="svf" or mode="sunlit":
from bluegreencity_ki.criteria.building_shading import BuildingShading
svf_criterion = BuildingShading(mode="svf") # → sky_view_factor
sunlit_criterion = BuildingShading(mode="sunlit") # → sunlit_hours
Registration in the catalog via build_svf() / build_sunlit() factory functions.
ctx.raster)If the raster is not available, the criterion returns the configured neutral_score (default 0.5) for all trees — the pipeline does not crash.
SVF is a dimensionless value between 0 and 1:
The scoring direction is configuration-driven via the interpretation parameter:
| Mode | Low SVF (shaded) | High SVF (open) | Use Case |
|---|---|---|---|
shading_protection (default) |
Score ≈ 0 (low risk) | Score ≈ 1 (high risk) | Shade reduces evapotranspiration |
exposure |
Score ≈ 1 (high risk) | Score ≈ 0 (low risk) | Shade restricts light/airflow |
Shading protection is the default for drought risk assessment: trees shaded by buildings lose less water through evapotranspiration.
linear = clip((SVF − threshold_low) / (threshold_high − threshold_low), 0, 1)
shading_protection: score = linear # low SVF → low risk
exposure: score = 1 − linear # low SVF → high risk
Daily hours of direct sunlight, sampled from four seasonal rasters at the astronomical key dates (spring/summer/autumn/winter equinox/solstice). The criterion computes the growing-season mean (configurable) for each tree.
| File | Date | Description | Range |
|---|---|---|---|
320.tif |
March 20 | Spring equinox | 0–12 h |
621.tif |
June 21 | Summer solstice | 0–17 h |
922.tif |
September 22 | Autumn equinox | 0–12 h |
1221.tif |
December 21 | Winter solstice | 0–7 h |
All rasters: 5 m resolution, EPSG:25832, float32.
By default, the criterion averages the growing season (spring + summer + autumn). Winter is excluded because drought stress occurs during the growing season. The seasons parameter controls which rasters are included.
More sunlit hours → higher drought risk (single direction, no interpretation parameter):
mean_hours = mean(spring_hours, summer_hours, autumn_hours) # per tree
score = clip((mean_hours − threshold_low) / (threshold_high − threshold_low), 0, 1)
All three methods from the air_temperature criterion are supported:
Fixed values from config:
svf_low (default 0.3), svf_high (default 0.7)hours_low (default 4.0 h/day), hours_high (default 10.0 h/day)Thresholds derived from the raster-wide value distribution. Compares trees against the entire city raster (including parks, roads, etc.).
Thresholds derived from the tree-location value distribution. Compares trees only against other trees — avoids bias from non-tree areas. Configured via low_percentile (default 10) and high_percentile (default 90).
| Raster | Config Key | Schema Key | Description |
|---|---|---|---|
| SVF | sky_view_factor_raster |
raster_svf |
Sky View Factor (0–1), 1 m |
| Sunlit Spring | sunlit_hours_spring_raster |
raster_sunlit_hours_spring |
Sunlit hours March 20 |
| Sunlit Summer | sunlit_hours_summer_raster |
raster_sunlit_hours_summer |
Sunlit hours June 21 |
| Sunlit Autumn | sunlit_hours_autumn_raster |
raster_sunlit_hours_autumn |
Sunlit hours September 22 |
| Sunlit Winter | sunlit_hours_winter_raster |
raster_sunlit_hours_winter |
Sunlit hours December 21 |
All rasters loaded via bluegreencity_ki/io/readers.py using _safe_load_raster().
inputs:
sky_view_factor_raster:
type: "tif"
path: "D:\\...\\data\\derived\\shadow\\SVF_hannover_2026.tif"
# Sunlit hours: 4 seasonal rasters (relative to raster_base_dir)
sunlit_hours_spring_raster:
type: "tif"
path: "sunlit hours/320.tif"
sunlit_hours_summer_raster:
type: "tif"
path: "sunlit hours/621.tif"
sunlit_hours_autumn_raster:
type: "tif"
path: "sunlit hours/922.tif"
sunlit_hours_winter_raster:
type: "tif"
path: "sunlit hours/1221.tif"
Pre-compute rasters using the integrated shadow calculation script:
# Full pipeline (STAC download + SVF + sunlit hours)
python scripts/calculate_shadow_criteria.py --auto-extent --year 2024
# Skip STAC download (use existing DGM/LOD2 data)
python scripts/calculate_shadow_criteria.py --auto-extent --skip-download
# Custom extent (EPSG:25832)
python scripts/calculate_shadow_criteria.py --extent 545000,555000,5800000,5810000
Output files:
data/derived/shadow/
├── SVF_hannover_<year>.tif # Sky View Factor raster (1m)
├── shadow_criteria_metadata_<year>.json # Processing metadata
└── intermediate/ # DGM, DSM, tiles
(raster_base_dir)/
└── sunlit hours/ # Sunlit hours rasters, 5 m (320/621/922/1221.tif)
Requirements: none beyond Python (SVF is computed with rasterio/numpy). Accurate sunlit hours can additionally be produced with the external SOLWEIG model (--sunlit solweig); the default is a placeholder approximation derived from the SVF.
The rasters consumed by this criterion are generated by scripts/calculate_shadow_criteria.py. The pipeline has three phases:
Downloads DEM/DGM tiles and Buildings LOD2 data from STAC catalogs hosted by LGLN Niedersachsen:
https://dgm.stac.lgln.niedersachsen.dehttps://lod.stac.lgln.niedersachsen.dedata/cache/stac/dgm/ and data/cache/stac/buildings_lod2/--skip-download if data is already availableintermediate/dgm_merged.tifintermediate/buildings.gpkgDGM_B = max(DGM, DGM + building_height)intermediate/dgm_buildings.tifThe study area is divided into 500 m × 500 m tiles with 50 m overlap buffers for edge consistency. Each tile is processed independently, enabling parallel computation.
SVF Calculation (per tile):
Optional AOI filtering restricts processing to tiles near tree locations (100 m buffer), reducing computation for areas without trees.
SVF_hannover_<year>.tif (1 m resolution)Sunlit_hours_hannover_<year>.tifshadow_criteria_metadata_<year>.json| Parameter | Value | Description |
|---|---|---|
| CRS | EPSG:25832 | UTM Zone 32N (Hanover) |
| Resolution | 1.0 m | Output raster pixel size |
| Tile size | 500 m | Grid cell size for parallel processing |
| Tile buffer | 50 m | Overlap for edge consistency |
| SVF search radius | 200 m | Horizon sampling distance |
| Azimuth directions | 36 | Compass directions sampled (10° intervals) |
| AOI buffer | 100 m | Buffer around tree points |
| CPU cores | cpu_count - 2 |
Default parallel threads |
criteria:
- name: sky_view_factor
weight: 1.0
params:
threshold_method: tree_percentile
interpretation: shading_protection
low_percentile: 10
high_percentile: 90
criteria:
- name: sky_view_factor
weight: 1.0
params:
threshold_method: tree_percentile
interpretation: shading_protection
low_percentile: 10
high_percentile: 90
- name: sunlit_hours
weight: 0.5 # example; current calibrated weight in config/config.yaml
params:
threshold_method: tree_percentile
seasons: ["spring", "summer", "autumn"] # growing season (default)
low_percentile: 10
high_percentile: 90
criteria:
- name: sunlit_hours
weight: 1.0
params:
threshold_method: static
seasons: ["summer"] # only summer solstice raster
hours_low: 6.0
hours_high: 12.0
criteria:
- name: sky_view_factor
weight: 1.0
params:
threshold_method: static
interpretation: exposure
svf_low: 0.3
svf_high: 0.7
- name: sunlit_hours
weight: 1.0
params:
threshold_method: static
seasons: ["spring", "summer", "autumn", "winter"] # annual average
hours_low: 4.0
hours_high: 10.0
| Column | Type | Description |
|---|---|---|
sky_view_factor_risk |
float | Normalized SVF risk score (0–1) |
m_svf_value |
float | Raw SVF value at tree location |
sunlit_hours_risk |
float | Normalized sunlit hours risk score (0–1) |
m_sunlit_hours_value |
float | Growing-season mean sunlit hours |
m_sunlit_hours_spring |
float | Sunlit hours at spring equinox |
m_sunlit_hours_summer |
float | Sunlit hours at summer solstice |
m_sunlit_hours_autumn |
float | Sunlit hours at autumn equinox |
| Abbreviation | Full Term |
|---|---|
| DEM / DGM | Digital Elevation Model (DE: Digitales Geländemodell) |
| DSM / DOM | Digital Surface Model (DE: Digitales Oberflächenmodell) |
| SVF | Sky View Factor |
| LOD2 | Level of Detail 2 — building model with roof geometry |
| STAC | SpatioTemporal Asset Catalog |
| CRS | Coordinate Reference System |
| AOI | Area of Interest |
shading_protection: Lower SVF → lower risk scoreexposure: Lower SVF → higher risk scoreimport geopandas as gpd
import matplotlib.pyplot as plt
results = gpd.read_file("data/output/<run_date>_<scenario>/trees_at_risk.gpkg")
# Check SVF distribution
print(results['m_svf_value'].describe())
# Visualize SVF vs risk score
plt.scatter(results['m_svf_value'], results['sky_view_factor_risk'])
plt.xlabel('SVF Value')
plt.ylabel('Risk Score')
plt.title('SVF vs Risk Score')
plt.show()
# Trees with low SVF (high obstruction)
low_svf = results[results['m_svf_value'] < 0.3]
print(f"Trees with low SVF (<0.3): {len(low_svf)}")
import geopandas as gpd
import matplotlib.pyplot as plt
results = gpd.read_file("data/output/<run_date>_<scenario>/trees_at_risk.gpkg")
# Check distribution
print(results['m_sunlit_hours_value'].describe())
# Histogram + scatter plot
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].hist(results['m_sunlit_hours_value'], bins=30)
axes[0].set_xlabel('Mean Sunlit Hours (growing season)')
axes[0].set_ylabel('Number of Trees')
axes[0].set_title('Distribution of Sunlit Hours')
axes[1].scatter(results['m_sunlit_hours_value'], results['sunlit_hours_risk'])
axes[1].set_xlabel('Mean Sunlit Hours')
axes[1].set_ylabel('Risk Score')
axes[1].set_title('Sunlit Hours vs Risk Score')
plt.tight_layout()
plt.show()
# Highly exposed trees
high_exposure = results[results['m_sunlit_hours_value'] > 10]
print(f"Trees with >10 h sun/day: {len(high_exposure)}")
print(f"Mean risk score: {high_exposure['sunlit_hours_risk'].mean():.2f}")
Compare sub-criteria with each other and related data:
For large areas like Hannover (~200 km²), the preprocessing script includes:
Process only areas near trees to skip irrelevant regions:
# Enabled by default; disable with --no-aoi
python scripts/calculate_shadow_criteria.py --auto-extent --no-aoi
Large areas are split into tiles for efficient processing:
Reduce computation time with configurable temporal resolution:
Run the calculation script annually to update shadow criteria with latest building data:
python scripts/calculate_shadow_criteria.py --auto-extent --year 2025
calculate_shadow_criteria.py| Date | Change |
|---|---|
| 2025-07 | Initial implementation: SVF + sunlit hours factory pattern |
| 2025-07 | Sunlit hours: 4 seasonal rasters (key dates) replace single raster; growing-season averaging |
| 2025-07 | Documentation consolidated: SVF & sunlit hours docs merged into building_shading.md |