bluegreencity-ki

BlueGreenCity-KI Architecture

⚠️ Legacy snapshot (2025-10-13). This document describes the initial (v1) system state. The criteria count, output file names, model directory layout, tree-ID field, and several sample values below have since changed. For the current operational state see configuration.md, workflow.md, and the ML documentation. The core design patterns (vector-first processing, pluggable criteria, backend abstraction, configuration-driven behavior) remain valid.

This document provides a comprehensive overview of the BlueGreenCity-KI system architecture, design patterns, and extension points.

Table of Contents

System Overview

BlueGreenCity-KI is a comprehensive, modular urban tree management system designed to support climate-resilient green infrastructure. The system evaluates drought risk for urban trees by computing and aggregating multiple criteria scores (each in 0–1 range) into a weighted risk score. Beyond risk assessment, the tool is designed to calculate precise water demands for individual trees and identify alternative water sources for irrigation during drought periods, helping maintain and enhance the ecosystem services that urban trees provide.

The system employs a vector-first architecture for efficient spatial processing and is built to integrate machine learning and deep learning methods in certain criteria and additional modules, enabling data-driven predictions and optimizations for urban forest management.

Key Characteristics

High-Level Component Diagram

┌─────────────────────────────────────────────────────────────┐
│                         CLI Runner                          │
│                   (bluegreencity_ki/runners)                │
└──────────────────────────┬──────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│                       Pipeline Layer                        │
│                  (bluegreencity_ki/pipelines)               │
│  ┌────────────────────────────────────────────────────┐    │
│  │  risk_scoring.run(ctx)                             │    │
│  │  - Load criteria from catalog                      │    │
│  │  - Execute each criterion                          │    │
│  │  - Aggregate weighted risk                         │    │
│  └────────────────────────────────────────────────────┘    │
└──────────────────────────┬──────────────────────────────────┘
         │
  ┌──────────────────┼──────────────────┐
  ▼                  ▼                  ▼
┌───────────────┐  ┌──────────────┐  ┌──────────────┐
│   Criteria    │  │     Core     │  │      IO      │
│   Registry    │  │   Context    │  │   Writers    │
└───────────────┘  └──────────────┘  └──────────────┘
  │                  │                  │
  ▼                  ▼                  ▼
┌───────────────┐  ┌──────────────┐  ┌──────────────┐
│  Criterion    │  │   Schema     │  │   Reports    │
│  Instances    │  │  Profiler    │  │              │
└───────────────┘  └──────────────┘  └──────────────┘
  │                  │
  └──────────┬───────┘
             ▼
    ┌────────────────┐
    │  Backend       │
    │  Adapters      │
    ├────────────────┤
    │ OSGeo Backend  │
    │ ArcPy Backend  │
    └────────────────┘
       │
       ▼
    ┌────────────────┐
    │  Data Sources  │
    │  - Vector      │
    │  - Raster      │
    │  - PostGIS     │
    └────────────────┘

Mermaid Version

flowchart TD
    A[CLI Runner<br/>bluegreencity_ki/runners] --> B[Pipeline Layer<br/>bluegreencity_ki/pipelines]
    B --> C[Criteria Registry]
    B --> D[Core Context]
    B --> E[IO Writers]
    C --> F[Criterion Instances]
    D --> G[Schema and Profiler]
    E --> H[Reports]
    F --> I[Backend Adapters<br/>OSGeo or ArcPy]
    G --> I
    I --> J[Data Sources<br/>Vector, Raster, PostGIS]

Architecture Principles

1. Separation of Concerns

Each module has a clear, single responsibility:

2. Plugin Architecture

The criteria system uses a registry pattern that allows new criteria to be added without modifying existing code. See Criteria System for details.

3. Backend Abstraction

Data loading is abstracted through backend adapters, allowing the system to work with different GIS libraries (GeoPandas/GDAL or ArcPy) transparently.

4. Immutable Context

A Context object carries all necessary data and configuration through the pipeline. Criteria receive the context as input and return results without side effects.

5. Configuration Over Code

System behavior (weights, thresholds, data sources) is controlled through YAML configuration files rather than hardcoded values.

Core Components

Context (bluegreencity_ki/core/context.py)

The Context class is the central data container that flows through the entire pipeline:

@dataclass
class Context:
    config: dict           # Full configuration from YAML
    run_date: date         # Execution date for age calculations
    schema: Schema         # Field name mappings
    backend: str           # 'osgeo' or 'arcpy'
    scenario: ScenarioProvider  # Configured scenario inputs
    vector: dict           # Loaded vector layers (trees, roads)
    raster: dict           # Loaded raster data (landuse, temperature)

Creation: Context.from_yaml(config_path) loads configuration, data sources, and initializes all components.

Schema (bluegreencity_ki/core/schema.py)

Defines field name mappings to decouple code from specific column names in input data:

class Schema:
    tree_id = "objectid"
    pflanzjahr = "pflanzjahr"  # planting year
    vitality = "vitalitaet"
    # ... more field mappings

This allows the system to work with datasets that use different column names.

Profiler (bluegreencity_ki/core/profiler.py)

Tracks performance metrics for each pipeline step:

with timer("criterion:age", profiler):
    result = criterion.compute(ctx)

Profiling data is included in output reports for performance monitoring.

Pipeline Pattern

Risk Scoring Pipeline (bluegreencity_ki/pipelines/risk_scoring.py)

The main pipeline orchestrates the risk assessment workflow:

def run(ctx) -> tuple[pd.DataFrame, dict, Profiler, dict]:
    # 1. Normalize criterion weights
    # 2. For each configured criterion:
    #    - Create criterion instance from catalog
    #    - Execute criterion.compute(ctx)
    #    - Collect score and metadata
    # 3. Aggregate weighted risk score
    # 4. Return results + statistics

Raster Processing Strategy

Raster-backed criteria use the shared OSGeo raster sampling utilities instead of opening and reading rasters criterion-by-criterion. The utilities normalize raw ctx.raster entries to their concrete dataset, cache point or point+zonal tree samples on the pipeline context, and cache raster band reads on the open dataset object. In addition, a persistent on-disk cache can be enabled via project.raster_sampling_cache so repeated development runs can reuse prior sampling results when raster signature, tree geometry/index, CRS, and sampling mode remain unchanged. This keeps output fields and scoring logic criterion-specific while avoiding repeated full-band reads when a criterion needs both zonal statistics and point fallback, or when multiple criteria reuse the same raster samples (for example day/night temperature and UHI).

Threshold strategies remain explicit in each criterion. Static and sample-based thresholds avoid full-raster statistics; spatial percentile thresholds still use compute_raster_statistics() only when configured because they intentionally need the raster-wide distribution. The existing criterion:<name> profiler timers continue to measure runtime per raster criterion, including cached reads and sampling.

Pipeline Execution Flow

┌─────────────────────────────────────────────────────────┐
│  1. Load Context                                        │
│     - Parse YAML config                                 │
│     - Load vector/raster data via adapters              │
│     - Initialize schema and scenario provider           │
└────────────────────┬────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────┐
│  2. Normalize Weights                                   │
│     - Sum all criterion weights                         │
│     - Normalize to sum=1.0                              │
└────────────────────┬────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────┐
│  3. Execute Criteria (loop)                             │
│     For each criterion in config:                       │
│     ┌─────────────────────────────────────────────┐    │
│     │ a. Create criterion from catalog            │    │
│     │ b. Call criterion.compute(ctx) → Result     │    │
│     │ c. Extract score series (index=tree_id)     │    │
│     │ d. Collect extras (e.g., group_size)        │    │
│     │ e. Compute statistics (min/max/mean)        │    │
│     │ f. Generate histogram                       │    │
│     └─────────────────────────────────────────────┘    │
└────────────────────┬────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────┐
│  4. Aggregate Risk                                      │
│     - Combine all criterion scores into DataFrame       │
│     - Compute: overall_drought_risk = Σ(score[i] * weight[i])     │
│     - Attach auxiliary columns (extras)                 │
└────────────────────┬────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────┐
│  5. Write Outputs                                       │
│     - Save results to GPKG/CSV                          │
│     - Generate run report (JSON/Markdown)               │
│     - Log sustainability metrics                        │
└─────────────────────────────────────────────────────────┘

Mermaid Version

flowchart TD
  A["1. Load Context<br/>Parse YAML config<br/>Load vector and raster data<br/>Initialize schema and scenario provider"]
  B["2. Normalize Weights<br/>Sum criterion weights<br/>Normalize to sum = 1.0"]
  C["3. Execute Criteria Loop<br/>Create criterion from catalog<br/>Run criterion.compute(ctx)<br/>Collect scores, extras, and statistics"]
  D["4. Aggregate Risk<br/>Combine criterion scores<br/>Compute overall_drought_risk<br/>Attach auxiliary columns"]
  E["5. Write Outputs<br/>Save GPKG and CSV results<br/>Generate reports<br/>Log sustainability metrics"]

  A --> B --> C --> D --> E

Key Pipeline Characteristics

📖 For a detailed explanation of the risk scoring workflow, including weight normalization, aggregation formulas, and worked examples, see risk_scoring_guide.md.

Criteria System

The criteria system implements a plugin architecture that allows new risk criteria to be added dynamically.

Criterion Interface

All criteria implement the Criterion protocol defined in bluegreencity_ki/criteria/base.py:

class Criterion(Protocol):
    name: str
    
    def compute(self, ctx: Context) -> CriterionResult:
        """Compute risk score for all trees in context.
        
        Returns:
            CriterionResult with:
            - name: criterion identifier
            - score: Series[tree_id -> float] in [0,1]
            - meta: optional metadata for reporting
            - extras: optional auxiliary data columns
        """
        ...

CriterionResult Structure

@dataclass
class CriterionResult:
    name: str              # Criterion identifier (e.g., "age", "vitality")
    score: pd.Series       # index=tree_id, values in [0,1]
                           # Stored as {name}_risk in output
    meta: Dict[str, Any] | None = None    # Optional metadata
    extras: Dict[str, pd.Series] | None = None  # Raw values and auxiliary data
                                                 # Key format: {name}_value

Field Naming Convention:

Score Convention: All risk scores are normalized to [0, 1] where:

Criteria Catalog

The catalog (bluegreencity_ki/criteria/criteria_catalog.py) is a registry mapping criterion names to builder functions:

CATALOG: Dict[str, Callable[[], Criterion]] = {
    "age": age.build,
    "street_type": street_type.build,
    "vitality": vitality.build,
    "planting_structure": planting_structure.build,
}

def create(name: str) -> Criterion:
    return CATALOG[name]()

Current Criteria

The registry now holds 25 criteria (see criteria/README.md); the four below are the initial v1 set kept as examples.

1. Age (age.py)

2. Vitality (vitality.py)

3. Street Type (street_type.py)

4. Planting Structure (planting_structure.py)

Adding a New Criterion

To add a new criterion (e.g., land_use):

  1. Create criterion module (bluegreencity_ki/criteria/land_use.py):
from .base import Criterion, CriterionResult
import pandas as pd

class LandUse(Criterion):
    name = "land_use"
    
    def compute(self, ctx) -> CriterionResult:
        trees = ctx.vector["trees"]
        
        # Your scoring logic here
        raw_landuse = sample_landuse_raster(trees)  # e.g., sealing percentage
        score = normalize_to_risk(raw_landuse)      # Convert to 0-1 risk score
        
        # Store raw values in extras using {name}_value convention
        extras = {"land_use_value": raw_landuse}
        
        return CriterionResult(self.name, score, extras=extras)

def build():
    return LandUse()
  1. Register in catalog (criteria_catalog.py):
from . import land_use

CATALOG = {
    # ... existing entries
    "land_use": land_use.build,
}
  1. Configure in YAML (config/config.yaml):
criteria:
  - name: land_use
    weight: 0.15
    params:
      # criterion-specific parameters

No changes to pipeline or other code are required!

Backend Adapters

The system supports two backend implementations for data loading, abstracted through adapter modules.

Adapter Architecture

bluegreencity_ki/adapters/
├── osgeo_backend/
│   ├── vector.py    # GeoPandas/Fiona-based loading
│   └── raster.py    # Rasterio-based loading
└── arcpy_backend/
    ├── vector.py    # ArcPy-based loading
    └── raster.py    # ArcPy raster handling

Backend Selection

Backend is configured in config.yaml:

project:
  backend: "osgeo"  # or "arcpy"

The readers.load_inputs() function dynamically imports the appropriate adapter:

def load_inputs(cfg) -> Tuple[dict, dict]:
    backend = cfg["project"]["backend"]
    if backend == "arcpy":
        from ..adapters.arcpy_backend import vector as v, raster as r
    elif backend in ("osgeo", "qgis", "geopandas"):
        from ..adapters.osgeo_backend import vector as v, raster as r
    # ... use v.load_layer() and r.load_raster()

Advantages:

Data Sources:

Key Functions (osgeo_backend/vector.py):

PostGIS Support:

inputs:
  trees:
    type: postgis
    schema: public
    table: trees
    filter: "district = 'Mitte'"
    id_field: objectid

ArcPy Backend (Legacy)

Use Cases:

Limitations:

Key Functions (arcpy_backend/vector.py):

Backend Abstraction Benefits

  1. Portability: Code runs on both ArcGIS and open-source stacks
  2. Testing: Can test with lightweight OSGeo stack without ArcGIS license
  3. Migration: Gradual transition from ArcPy to open-source
  4. Flexibility: Choose best tool for deployment environment

Data Flow

Input Data Flow

Configuration File (config.yaml)
        ↓
Context.from_yaml()
        ↓
    ┌───────┴────────┐
    ▼                ▼
load_inputs()   ScenarioProvider
    ↓                ↓
Backend Adapter      ↓
    ↓                ↓
┌───┴────┐           ↓
▼        ▼           ▼
Vector  Raster       ↓
Data    Data         ↓
    ↓       ↓        ↓
    └───┬───┴────────┘
        ▼
    Context Object
        ↓
  Pipeline Execution

Vector Data Flow

  1. Source Definition (YAML config):
    inputs:
      trees:
        type: postgis
        table: trees
    
  2. Loading (via adapter):
    trees = v.load_layer(trees_spec, id_field="objectid")
    # Result: GeoDataFrame with geometry + attributes
    
  3. Enrichment (optional, via enrichment.py):
    trees = enrich_trees(trees, cfg)
    # Spatial join with OBSkH dataset → adds obskh_* columns
    # Merges crown diameter with QA tracking
    

    See Enrichment documentation for details.

  4. Context Storage:
    ctx.vector = {"trees": trees, "roads": roads}
    
  5. Criterion Access:
    trees = ctx.vector["trees"]
    score = compute_score(trees[ctx.schema.tree_id])
    

Raster Data Flow

  1. Source Definition:
    inputs:
      landuse_raster:
        type: raster
        path: data/landuse.tif
    
  2. Loading (via adapter):
    raster = r.load_raster(spec)
    # Result: Rasterio dataset or numpy array
    
  3. Sampling (in criterion):
    # Sample raster values at tree locations
    values = sample_raster_at_points(raster, tree_geometries)
    
  4. Caching: Sampled values are added as DataFrame columns, avoiding repeated sampling

Output Data Flow

Pipeline Results (DataFrame)
        ↓
    ┌───┴────┐
    ▼        ▼
Writers   Reports
    ↓        ↓
┌───┴─┐  ┌──┴───┐
▼     ▼  ▼      ▼
GPKG  CSV JSON  Markdown

Output Files (current naming; results.* was the v1 name):

Extension Points

1. Adding New Criteria

Location: bluegreencity_ki/criteria/

Steps:

  1. Create my_criterion.py with Criterion implementation
  2. Add to criteria_catalog.py
  3. Configure in config.yaml

Example Use Cases:

2. Adding New Data Sources

Location: Backend adapters (adapters/*/vector.py, raster.py)

Extension Points:

Example:

def load_layer(spec, id_field=None):
    t = spec["type"]
    if t == "wfs":
        return load_from_wfs(spec["url"], spec["layer"])
    elif t == "postgis":
        # existing PostGIS implementation

3. Adding New Backends

Location: bluegreencity_ki/adapters/

Steps:

  1. Create new_backend/ directory
  2. Implement vector.py and raster.py with standard interface
  3. Update readers.load_inputs() to recognize new backend
  4. Add configuration option

Example: QGIS backend using PyQGIS

4. Adding New Pipeline Stages

Location: bluegreencity_ki/pipelines/

Use Cases:

Example:

# pipelines/irrigation_demand.py
def run(ctx, risk_results):
    # Compute watering needs based on risk scores
    return demand_schedule

5. Custom Normalizers

Location: bluegreencity_ki/core/normalizers.py

Current: quantile_minmax() (quantile-anchored min–max, optional clipping and inversion)

Extension: Add domain-specific normalization functions

6. Scenario Providers

Location: bluegreencity_ki/scenarios/

Current: Baseline scenario provider

Possible extensions:

7. Custom Writers

Location: bluegreencity_ki/io/writers.py

Extension Points:

Configuration System

Configuration Structure

project:
  backend: "osgeo"           # Backend selection
  run_date: "auto"           # or ISO date string
  neutral_score: 0.5         # Default for missing data
  scenario: "baseline"       # Scenario identifier

  study_areas:
    active: "hannover_gesamt"
    definitions:
      hannover_gesamt:
        name: "City of Hannover (complete)"
        bbox_source: "auto"
        trees: &hannover_tree_inventory
          type: "csv_inventory"
          path: "./data/input/trees/Baum-2010-2025-v6_historisch_prod.csv"
          delimiter: ";"
      hannover_ml_training_2010:
        name: "Hannover ML Training (2010 Snapshot)"
        bbox_source: "auto"
        trees:
          <<: *hannover_tree_inventory
          timestamp_filter: "2010-12-31"

inputs:
  trees:                     # Legacy: single data source
    type: postgis
    table: trees
  
  roads:
    type: postgis
    table: osm_highways

criteria:
  - name: age
    weight: 0.2              # Normalized automatically
    normalizer_params:
      q_low: 0.1
      q_high: 0.9
  
  - name: vitality
    weight: 0.3
    params:
      exclude_stages: [4, 5]
      stage0_score: 0.5
  
  - name: planting_structure
    weight: 0.25
    params:
      mode: "breaks"
      radius_m: 20
      neighbor_breaks: [0, 4, 9]
      neighbor_scores: [0.8, 0.5, 0.3, 0.1]

output:
  path: "./data/output/{run_date_underscore}_{scenario}/trees_at_risk.gpkg"

Configuration Loading

from bluegreencity_ki.core.config import load_config

cfg = load_config("config/config.yaml")
# Result: Parsed dict with validation

Study Areas (Multi-Area Support)

Allows running analysis on different geographic areas with area-specific data sources:

project:
  study_areas:
    active: "hannover_gesamt"
    definitions:
      hannover_gesamt:
        name: "City of Hannover (complete)"
        bbox_source: "auto"
        trees: &hannover_tree_inventory
          type: "csv_inventory"
          path: "./data/input/trees/Baum-2010-2025-v6_historisch_prod.csv"
          delimiter: ";"

ML Pipeline (AutoGluon)

The system includes an optional ML-based drought risk prediction pipeline using AutoGluon, running in parallel with the weighted-sum scoring.

ML Architecture

┌──────────────────────────────────────────────────────────────────┐
│                    ML Pipeline (parallel path)                   │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Label Extraction (offline / one-time)                        │
│     bluegreencity_ki/ml/labels/label_extractor.py                │
│     → Crown diagnosis (Wipfeldürre, Totholz, Belaubung)         │
│     → drought_damage_binary (combined OR label)                  │
│                                                                  │
│  2. Training (offline / periodic)                                │
│     bluegreencity_ki/ml/training/trainer.py                      │
│     → AutoGluonTrainer wraps TabularPredictor.fit()              │
│     → bagging/stacking, roc_auc metric                           │
│     → Models: data/models/autogluon/{operational,                │
│       annual_validation}/<YYYY_MM>/train_…/                      │
│                                                                  │
│  3. Validation (offline / after training)                        │
│     bluegreencity_ki/validation/validator.py                     │
│     → ModelValidator: ROC-AUC, Brier, F1, calibration            │
│     → Extreme-group analysis (Top/Bottom 5% lift)                │
│     → Per-species metrics, baseline comparison                   │
│                                                                  │
│  4. Inference (within pipeline, if ml.enabled=true)              │
│     bluegreencity_ki/ml/inference/predictor.py                   │
│     → DroughtRiskPredictor loads model, scores trees             │
│     → Output: drought_risk_ml (P), drought_risk_ml_class         │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Integration with Risk Scoring Pipeline

When ml.enabled: true in config.yaml, risk_scoring.run() executes the ML block after the weighted-sum aggregation:

  1. Loads the trained AutoGluon model from <model_path>/<label>/
  2. Builds feature DataFrame from tree attributes + computed criterion scores
  3. Appends drought_risk_ml (probability) and drought_risk_ml_class (categorical) to the output

Both scoring paths are independent — the weighted sum (drought_risk_rulebased) and the ML prediction (drought_risk_ml) are computed separately and both included in the output.

Key Design Decisions

Core Documentation

Criteria Documentation

ML Documentation

Database & Operations

Architecture Decision Records (ADRs)

For significant architectural decisions, we recommend creating ADRs in docs/adr/:

ADR Template

# ADR-001: Decision Title

## Status
Accepted | Proposed | Deprecated

## Context
What is the issue we're trying to solve?

## Decision
What did we decide?

## Consequences
What are the positive and negative outcomes?

## Alternatives Considered
What other options did we evaluate?

Example ADR Topics

Note: number ADR-002 is already taken by the spatial-database evaluation (see ../research/sedonadb-duckdb/); new ADRs should continue from ADR-003. See the ADR index.


Document Version: 1.1 (legacy snapshot with corrected references)
Last Updated: 2026-10-12
Maintainer: BlueGreenCity-KI Team