bluegreencity-ki

Building Shading Criterion

Overview

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).

Implementation

Location

Architecture

The 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.

How It Works

  1. Load raster: SVF raster or sunlit hours raster (from ctx.raster)
  2. Point sampling: Sample raster value at each tree location (no zonal aggregation — SVF is 1 m, sunlit-hours rasters are 5 m)
  3. Compute thresholds: Depending on method (tree_percentile, spatial_percentile, static)
  4. Risk scoring: Linear interpolation between thresholds → score [0, 1]

If the raster is not available, the criterion returns the configured neutral_score (default 0.5) for all trees — the pipeline does not crash.

Sub-Criteria

Sky View Factor (SVF)

SVF is a dimensionless value between 0 and 1:

Risk Interpretation

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.

Scoring Formula

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

Sunlit Hours

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.

Seasonal Rasters

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.

Aggregation

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.

Risk Interpretation

More sunlit hours → higher drought risk (single direction, no interpretation parameter):

Scoring Formula

mean_hours = mean(spring_hours, summer_hours, autumn_hours)  # per tree
score = clip((mean_hours − threshold_low) / (threshold_high − threshold_low), 0, 1)

Threshold Methods

All three methods from the air_temperature criterion are supported:

1. Static Thresholds

Fixed values from config:

2. Spatial Percentile

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).

Data Sources

Input Rasters

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().

Raster Configuration (config.yaml)

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"

Data Preparation

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.

Preprocessing Pipeline

The rasters consumed by this criterion are generated by scripts/calculate_shadow_criteria.py. The pipeline has three phases:

Phase 1: Data Acquisition (STAC Download)

Downloads DEM/DGM tiles and Buildings LOD2 data from STAC catalogs hosted by LGLN Niedersachsen:

Phase 2: Base Data Preparation

  1. Merge DGM tiles into a single raster → intermediate/dgm_merged.tif
  2. Extract & prepare buildings from LOD2 3D shapefiles → intermediate/buildings.gpkg
  3. Create DGM+Buildings raster (terrain elevation + building heights, no vegetation)
    • Formula: DGM_B = max(DGM, DGM + building_height)
    • Result: intermediate/dgm_buildings.tif

Phase 3: SVF Tile Processing

The 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.

Phase 4: Merge & Output

Key Parameters

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

Configuration Examples

Example 1: SVF Only (Shading Protection)

criteria:
  - name: sky_view_factor
    weight: 1.0
    params:
      threshold_method: tree_percentile
      interpretation: shading_protection
      low_percentile: 10
      high_percentile: 90

Example 2: Both Sub-Criteria (Growing Season)

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

Example 3: Summer Only

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

Example 4: Static Thresholds (All Seasons)

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

Output Columns

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

Abbreviations

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

Validation

SVF Validation

Basic Checks

  1. Value range: SVF values must be between 0.0 and 1.0
  2. Scoring direction:
    • shading_protection: Lower SVF → lower risk score
    • exposure: Lower SVF → higher risk score
  3. Spatial patterns: Trees in dense urban areas should have lower SVF than trees in parks

Advanced Validation

import 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)}")

Sunlit Hours Validation

Basic Checks

  1. Value range: Sunlit hours should be between 0 and ~17 h/day (summer solstice at 52°N)
  2. Scoring direction: Higher sunlit hours → higher risk score
  3. Spatial patterns: Parks/open areas → high sunlit hours; dense urban canyons → low sunlit hours

Advanced Validation

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}")

Cross-Validation

Compare sub-criteria with each other and related data:

Optimization Strategies

For large areas like Hannover (~200 km²), the preprocessing script includes:

1. AOI Filtering

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

2. Tiled Processing

Large areas are split into tiles for efficient processing:

3. Temporal Resolution

Reduce computation time with configurable temporal resolution:

Yearly Update Process

Run the calculation script annually to update shadow criteria with latest building data:

python scripts/calculate_shadow_criteria.py --auto-extent --year 2025

Known Limitations

  1. Pre-computation required: Rasters must be calculated in advance via calculate_shadow_criteria.py
  2. No vegetation canopy: SVF/sunlit hours are computed from DGM + buildings only (no tree canopy in DSM)
  3. Seasonal variation: SVF is static; sunlit hours vary by season (4 key dates available)
  4. Resolution: Accuracy depends on DGM resolution (1 m) and building detail (LOD2)
  5. GRASS GIS dependency: SVF computation requires GRASS GIS 8.4+ (external installation)
  6. Clear-sky assumption: Sunlit hours do not account for cloud cover or atmospheric conditions
  7. Deciduous canopy: Leaf-on vs. leaf-off not modelled (buildings only)
  8. Computational cost: Hourly shadow modelling for an entire city is resource-intensive (hours to days)

References

Changelog

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