Phase 1, the technical integration for local daily observation, is complete. The SWBM remains separate from the rule-based/ML risk pipeline and does not yet issue irrigation work orders.
Implemented:
pyet 1.4,Pending:
The daily Windows task is registered and running (see below), and capillary
rise is enabled in operational forcing as of swbm-v0.3 (see
docs/water_balance/capillary_rise.md).
The prototype files were not parameter-identical. In particular, run.py uses
field capacity 0.29 and a capillary-rise lookup extending to 200 cm, while
script/step_historical.py / step3_realtime.py use field capacity 0.26
and a different lookup ending at 120 cm. The standalone maintainer confirmed
that only the 0.26 / 120 cm combination has ever been used in the
historical and real-time scripts; the run.py variant is a different, older
version. The integration therefore follows field capacity 0.26 and the 120 cm
lookup, which is the scientifically authoritative variant.
Capillary rise is enabled in operational forcing as of model version
swbm-v0.3 (climate.water_balance.capillary_rise.enabled: true in
config/config.yaml). The per-tree depth to groundwater (Flurabstand) is the
multi-year median per LHH monitoring well, kriging-interpolated (with
an IDW fallback) to each tree using the same well data set and coverage gate
as the groundwater risk criterion. Trees with fewer than
min_neighbors wells within max_distance_m simulate without capillary
rise (no extrapolation beyond the monitoring network). The resolved design
decisions (area, time basis, coverage gate, production activation) and the
divergence from the h_da standalone A_soil nearest-well logic are documented
in docs/water_balance/capillary_rise.md. The 120 cm lookup is also consumed
by the standalone regression validator for comparison.
The historical files ergebnisse_historisch_2020.csv through
ergebnisse_historisch_2025.csv (standalone v3, German column names) contain
one daily row per simulated tree with per-tree metadata (Blattnummer, Baumart,
Pflanzjahr, Kronendurchmesser_m), total and per-layer SWC, total and per-layer
nFK, theoretical water need (Wasserbedarf_theorie_L), practical irrigation
(Bewaesserung_praktisch_L), ET0, and raw precipitation. Each tree has a
complete 365- or 366-day sequence.
scripts/validate_water_balance_against_standalone.py streams selected tree
blocks without loading a multi-gigabyte file into memory. It normalizes the
v3 German column names, reconstructs the exact layer geometry from SWC/nFK,
reuses the recorded ET0 and precipitation, and replays the integrated
soil-water core. For later years it can replay the previous year to recover
the hidden surface-wetness state. An optional groundwater CSV assigns the same
nearest observation point and enables the historical capillary-rise lookup.
The standalone v2 discarded infiltration that exceeded the field capacity of
both active layers for young (two-layer) trees, because residual infiltration
was assigned exclusively to a third layer. The standalone maintainer confirmed
this was an unintended bug and fixed it in v3: excess water now enters the
deepest active layer and drains gradually via k_drain. The operational port
already behaved this way, so the two implementations are now identical and the
regression validator no longer emulates the historical loss. This model change
is a scientific correction and is reflected in the model version
(swbm-v0.2).
Standalone v3 changed the irrigation trigger to require both layer 1 and layer
2 below 40 % nFK (previously layer 1 only) and split the output into a
theoretical need (full root-zone area, legacy formula) and a practical
recommendation (tree-pit radius + 30 cm buffer, capped at the crown radius;
layer 1 only). The port implements the same rule via
calculate_irrigation_demand_liters_v3 and practical_irrigation_area_m2, with
the buffer configurable as soil.practical_pit_buffer_m. The validator
compares the theoretical need, which depends only on the reconstructed
profile. The practical footprint was validated separately against the
standalone Bewaesserung_praktisch_L column: the baumumfeld polygons
(Baumumfeld_zusammengefasst.gpkg, area <= 10 m2, spatial join within)
were joined to the 2025 inventory and the port’s
practical_irrigation_area_m2 + calculate_irrigation_demand_liters_v3
reproduced the standalone practical irrigation exactly (0.0000 L difference
across 15 trees, including trees with up to 87 irrigation days).
Validation results (standalone v2, pre-fix):
1.4e-14 m3;2.7e-12 l.Tree 427 separately verified an active capillary-rise path. Its groundwater
was 1.19 m below the root zone, within the historical lookup range. With
capillary rise enabled, all 366 days matched with a maximum total-SWC error of
7.1e-15 m3. The control replay without capillary rise diverged by up to
0.681 m3 total SWC and 1,125 l modeled irrigation demand, confirming that
the exact result exercises this process rather than a zero-rate lookup.
A previously observed deviation of roughly 15.7 m3 for a single tree was
traced to an initial-state artifact, not a model error: the comparison had
replayed the previous year through the port starting from field capacity
instead of seeding the exact standalone end-of-year state. Re-seeding with the
exact standalone 2024 end state reduced the full-year 2025 deviation to
0.35 m3 (capillary-inactive tree) and 0.13 m3 (capillary-active tree,
groundwater 0.06 m below the root zone, lookup rate 5 mm/day). Day-by-day
capillary fluxes matched the standalone formula exactly in both cases.
The comparison validates the ported soil-water dynamics, including irrigation and optional capillary rise. Because ET0 and precipitation are reused from the standalone output, it does not independently validate DWD preprocessing or the ET0 calculation; those paths have separate tests and real-data smoke runs.
Weather-pipeline comparison (2025, 10 trees, full year): precipitation
matched the standalone CSV exactly (0.0000 mm difference) because both read
the same DWD daily cache. ET0 matched to a mean absolute error of
approximately 0.04-0.2 mm per tree; a single-day intermediate-value
comparison (extraterrestrial radiation, clear-sky radiation, daylight hours,
shadow factor, clearness index, diffuse fraction, effective radiation) showed
all values identical to floating-point precision when the same DWD cell, SVF,
and sunlit-hour anchors are used. The small residual ET0 difference is
attributed to slightly different SVF/sunlit TIF versions (the standalone ran
against a colleague’s local TIF directory), not to a code difference.
Ahead of the SWBM history dashboard, the port was systematically re-checked
against the standalone v0831 outputs (2026-08-31 release). Sample:
10 trees — five young two-layer and five mature three-layer trees,
green strips (Baumstreifen), tree pits (Baumscheibe), and two trees without
a root-zone classification; crown areas 3–154 m²; groundwater depths
1.1–5.1 m. Forcing: the standalone’s own ET0 and precipitation (identical
DWD inputs), full year 2025 plus July 2025. Initial state: the exact
standalone 2024 end state per tree (SWC layers; the hidden surface-wetness
state fw is not exported and defaults to 0.3).
Per-tree groundwater depth came from the nearest LHH monitoring well
(multi-year median 2017–2025). Because the standalone’s own well file
(h_da’s Grundwasserstände_Flurabstände) is not available locally, the
effective groundwater depth was additionally inverted from the standalone
output by grid search (0.2 m steps) on the full-year SWC deviation.
Results (max |ΔSWC total|, full year 2025, port swbm-v0.3 incl. the
fidelity fixes below vs. v0831):
| Tree | Genus | Layers | Site | LHH well (m) | ΔSWC m³ | Inverted well (m) | ΔSWC m³ |
|---|---|---|---|---|---|---|---|
| 139858 | Quercus | 2 | green | 1.38 | 0.032 | 0.8 | 0.032 |
| 33512 | Quercus | 3 | green | 2.01 | 4.07 | 1.8 | 2.05 |
| 119657 | Tilia | 2 | green | 2.18 | 0.259 | 0.0 (off) | 0.259 |
| 181734 | Carpinus | 3 | — | 1.36 | 0.002 | 0.0 (off) | 0.002 |
| 33971 | Acer | 3 | pit | 2.51 | 0.002 | 0.0 (off) | 0.002 |
| 124671 | Corylus | 2 | green | 3.41 | 0.061 | 0.0 (off) | 0.061 |
| 15386 | Platanus | 3 | green | 5.14 | 0.202 | 0.0 (off) | 0.202 |
| 67472 | Fraxinus | 2 | green | 1.10 | 0.142 | 0.8 | 0.142 |
| 177103 | Acer | 3 | — | 1.80 | 3.67 | 2.2 | 0.42 |
| 35291 | Quercus | 3 | pit | 1.31 | 0.349 | 1.6 | 0.296 |
Reading:
fw at the 2024 start, which
scales with the 113 m² green evaporation area.v0831 fidelity fixes applied in place to swbm-v0.3 (2026-09-22, issue
#238): code review against v0831 identified and fixed two porting
deviations. No version bump: the fixes change simulated states by far less
than the documented deviation budget (≤0.001 m³ in the sample), and
swbm-v0.3 is intended to represent the v0831-faithful port. Consequence:
swbm_daily_state rows written before 2026-09-22 (including the 2025
backfill) were produced by the pre-fix engine; they remain valid within the
deviation budget above and will be superseded by the re-backfill under
swbm-v0.4 — the version was bumped in place on 2026-09-22 (issue #239)
so pre-fix and post-fix rows no longer share a label (re-backfill plan:
issues #239/#243).
History extended to 2017 (completed 2026-09-24, issues #239/#243/#245):
the re-backfill under swbm-v0.4 used per-year historical tree sets with
fixed ages (2022–2026 re-backfill chain, then the 2017–2021 chain).
swbm_daily_state therefore covers 2017-01-01 onwards (observation days
plus the rolling 7-day forecast rows), and the SWBM history dashboards
reflect the same range.
evaporable_depth_m, new field of SoilEvaporationDemand).min(2.0, A_soil).Both changes restore exact v0831 behaviour in edge cases (very dry layer 1, small pits) with a negligible impact on the 2025 sample (≤0.001 m³).
Irrigation trigger (issue #238 point 1): the port carries the v0831
dual-layer trigger (both layer 1 and layer 2 below 0.4 nFK) since
swbm-v0.2 (2026-09-13, #199) via calculate_irrigation_demand_liters_v3;
the config key trigger_soil_water_fraction_layer_1 is a legacy name and is
passed as the v3 trigger threshold. The two-layer infiltration fix
(v0831 notes item 2) is present: the engine models an adaptive layer count
and routes excess infiltration into the deepest active layer.
2026 soil-moisture bias (issue #238 point 3): the bias mentioned in the
v0831 notes.txt is not quantified anywhere in the standalone
documentation. It concerns the standalone real-time chain (2026 bootstrap
from the historical checkpoint plus Open-Meteo-filled days and parameter
drift). It does not affect the port: the 2025 backfill and the operational
run consume DWD cache data (quality 1.0); Open-Meteo serves gap-fill and the
7-day forecast only. States are versioned per model_version, so a
re-initialisation from a refreshed backfill is a clean, self-contained
operation.
DailyWaterBalance stores one model version and date per tree. Fractions use
the prototype SWBM convention where 0 is the wilting-point boundary, 1 is
field capacity, and values up to 1.2 represent temporary excess.
For exact continuation it also stores total and per-layer water volumes in m3
plus surface_wet_fraction. Fractions alone are not treated as sufficient
state because profile geometry and uncertainty scenarios can differ.
Persisted model versions include a fingerprint of layer geometry and hydraulic parameters. A changed crown, planting-site area, soil parameter, or root-zone scenario therefore starts a new state series instead of reusing an incompatible previous state.
IrrigationEvent stores occurrence and amount provenance. A missing source
amount remains null. It is never silently converted into an observation.
For an event without litres, sensitivity runs use:
| Scenario | Amount |
|---|---|
| Lower bound | 0 l |
| Central | SWBM deficit to the configured target |
| Alternative | Configured typical amount |
The stable event_key makes repeated CSV or WFS imports idempotent.
The three amount assumptions are propagated through separate multi-day runs.
Their model versions receive the suffixes :irrigation-lower,
:irrigation-central, and :irrigation-typical, so PostGIS can store and
compare them without overwriting another scenario.
If multiple source rows report irrigation for the same tree and day but no
amount, they provide evidence for one occurrence. The central deficit or
typical amount is therefore estimated once, not multiplied by the number of
rows. Exact duplicate source events are additionally removed by event_key.
Known observed litres remain fixed in every scenario. If observed and unknown
records coexist, the central estimate fills only the remaining modeled deficit.
Apply sql/migrations/2026-08-03_create_water_balance_tables.sql before
enabling PostGIS writes. It creates:
bgcki.swbm_daily_statebgcki.irrigation_eventThe migration was applied to the configured project database on 2026-08-03.
Verification found 20 columns and primary key
(tree_id, state_date, model_version) on swbm_daily_state, 10 columns and
primary key event_key on irrigation_event, plus all four expected date,
trigger, tree/date, and source/event indexes.
The additive 2026-08-03_add_swbm_meteorology_source.sql migration was also
applied and verified. meteorology_source is text NOT NULL DEFAULT 'unknown'
and records whether each daily state used dwd, openmeteo, or another
explicit source.
PostGIS is the system of record. Parquet snapshots under
data/derived/water_balance/ provide portable analysis artefacts.
Configure climate.water_balance.irrigation_event_import with the actual
source path and columns, then enable the import block. Column names are not
hardcoded because the current CSV and future WFS may use different schemas.
conda run -n bgcki_wesemann python scripts/import_irrigation_events.py `
--config config/config.yaml
The default configuration is disabled and performs no database or file write.
The incremental local runner is available as:
conda run -n bgcki_wesemann python scripts/run_daily_water_balance.py `
--config config/config.yaml `
--tree-features <prepared-pipeline-output.gpkg> `
--year 2026 `
--start-date 2026-08-01 `
--end-date 2026-08-10
Use --no-postgis for a local Parquet-only run and --limit N for a small
development sample. The CLI requires the feature file to contain canonical
tree/profile fields plus m_svf_value and seasonal sunlit-hour extras.
For the current operational state, warm up the model over the complete previous calendar year and then continue from 1 January of the current year through the configured seven-day forecast horizon:
.\scripts\run_swbm_warmup.ps1 -StartDetached
The warm-up keeps the current inventory year as --profile-reference-year, so
tree geometry and hydraulic profiles remain stable across both simulation
years. Seasonal sunlit hours are generated separately for each simulation
year. --latest-only executes every daily transition but persists only the
final state per profile/scenario, avoiding a full-city intermediate history.
The detached script writes progress and failures to
logs/water-balance/swbm_warmup_*.log.
For year YYYY, climate.water_balance.execution.dwd_cache_dir must contain:
daily_cells_YYYY.parquethourly_cells_YYYY.parquetdaily_cell_meta_YYYY.parquethourly_cell_meta_YYYY.parquetCreate these files with:
conda run -n bgcki_wesemann python scripts/preprocess_water_balance_dwd.py `
--config config/config.yaml `
--year YYYY
The preprocessor downloads HYRAS mean/min/max temperature and precipitation, plus monthly HOSTRADA wind, radiation, and relative humidity. It clips NetCDF grids to the configured Hannover WGS84 bbox before creating the four Parquets. Hourly wind is averaged and converted from 10 m to FAO-56 2 m; radiation is summed and converted from W/m2 hours to MJ/m2/day; humidity retains daily minimum and maximum. Current unavailable HOSTRADA months are skipped, but a year without any complete month fails explicitly.
Raw NetCDF downloads can require several gigabytes and substantial runtime.
They are cached under dwd_cache_dir/raw/ and are not started automatically by
the daily task.
No such caches were present in either workspace during the 2026-08-03 integration check. The complete CLI path is covered by a synthetic end-to-end test and a real one-tree Open-Meteo run, but has not yet been claimed as a real DWD Hannover run. Missing files or weather days cause explicit fallback/error/ skip behavior; they are never silently interpolated.
The DWD preprocessor was subsequently run for 2025:
A full-city SWBM run for 2025-07-15 completed successfully:
| Metric | Result |
|---|---|
| Unique trees | 61,193 |
| Profile/forcing scenarios | 91,585 |
| Missing-weather scenarios | 0 |
| Written daily states | 91,585 |
| Duplicate canonical keys | 0 |
| Parquet size | 9.64 MB |
| Runtime including process startup | 401.36 s |
| Daily stress range | 0.0000 to 0.3405 |
| Positive irrigation demand | 0 (wet reference date) |
A separate three-day smoke run persisted meteorology_source = dwd for every
state. DWD provenance quality is 1.0; lower final data_quality values in the
full-city output reflect tree/profile forcing uncertainty, not weather quality.
The same three states for stable tree ID 2 were written to the configured
PostGIS database and read back successfully. The live rows cover 2025-07-15
through 2025-07-17, use the expected profile/model version, retain
meteorology_source = dwd, and are mirrored to the standard Parquet history.
Tree identity must be the stable Baum ID. The consolidated CSV also contains
a year-coded observation column ID; it is mapped to inventory_record_id,
not objectid. Pipeline outputs created before this correction may contain
values such as 2024000000002 as objectid and cannot be repaired after raw
source columns were dropped. The SWBM CLI rejects these artefacts and requires
a fresh pipeline output. A one-tree Open-Meteo smoke run exposed this issue;
its physical execution succeeded, but its old tree identity is not accepted as
a valid persisted result.
If the configured irrigation-event Parquet exists, the CLI applies its dated
events. Observed litres stay fixed; unknown amounts follow the selected
--irrigation-scenario. Without PostGIS, the Parquet state history is also the
continuation source for the next run. With PostGIS enabled, PostGIS is primary
and Parquet receives identical mirrored records.
After producing a fresh pipeline output with stable Baum ID, register the
local daily task under the current Windows user:
pwsh -NoProfile -File scripts/register_water_balance_task.ps1 `
-TreeFeatures "data/output/<run>/hannover_gesamt/trees_at_risk_hannover_gesamt.gpkg" `
-At "04:00"
The task runs scripts/run_scheduled_water_balance.ps1, which selects the
current calendar day, invokes the bgcki_wesemann Conda environment, and writes
a timestamped log under logs/water-balance/. The Python CLI loads the project
.env before connecting to PostGIS.
The task uses the current Windows user with S4U rather than SYSTEM, preserving
the project/Tailscale context without storing an interactive password in the
task definition. Registration requires an existing feature file and config and
rejects missing paths before changing Task Scheduler.
Remove the task with:
pwsh -NoProfile -File scripts/unregister_water_balance_task.ps1
The task has been registered (“BlueGreenCity-KI daily soil water
balance”, 04:00 S4U) and has been running daily since 2026-09-12 after
a fresh pipeline output provided stable Baum ID mapping.
A full ML/SHAP run is not required to refresh the SWBM input. Run:
conda run -n bgcki_wesemann python scripts/prepare_water_balance_features.py `
--config config/config.yaml
The focused workflow loads the current stable-ID inventory, applies OBSkH and Citree enrichment, joins planting-site polygons, and samples SVF plus all four seasonal sunlit-hour rasters. It writes only the canonical SWBM columns and geometry, avoiding source-field collisions in GeoPackage.
Crown diameter provenance (matters for root-zone area): the SWBM does not
use the raw Baumkataster value. kr_durchm carries the enriched value: the
inventory measurement wins on any deviation >= 10 % versus OBSkH (field
values are considered more accurate), OBSkH fills missing inventory values,
close matches (< 10 %) are averaged, and values above the 35 m plausibility
limit are set to NaN and later imputed by the species/global crown
regression in the SWBM adapter. See
enrichment.md. The h_da standalone
uses the raw inventory value without the cap or OBSkH fill — a known source
of input divergence in cross-validation (issue #243).
On 2026-08-03 it produced data/derived/water_balance/tree_features.gpkg with
61,193 rows, 61,193 unique stable IDs (2 to 185490), and zero year-coded
IDs. Missing values were 6,779 for SVF and 12 for each sunlit anchor; these are
handled by the documented explicit scenarios/fallbacks.
A subsequent one-tree, one-day CLI run used stable tree_id=2, live
Open-Meteo weather, and wrote a valid local Parquet state with data quality
0.7. The earlier invalid-ID smoke record was deleted.
The GPKG is a derived artifact of the operative (current) inventory, not
of the historical panel spec. Because the active study-area trees spec in
config/config.yaml points at the historical panel file, the preparation
workflow resolves the trees_current sub-spec (the operative WFS-style
extract) as its source whenever present (resolve_swbm_tree_spec).
Every build stamps the source identity (file name, mtime, size, unique IDs)
as a fingerprint in the sidecar tree_features.gpkg.meta.json. The daily
SWBM run calls ensure_current_tree_features before reading the GPKG:
when the fingerprint of the current operative source no longer matches the
persisted one (a Baumkataster version switch, a WFS re-delivery), the daily
run rebuilds the GPKG in place and proceeds — so a stale tree set can never
be simulated silently again. Up-to-date artifacts pass through unchanged.
To verify consistency without running the simulation (e.g. before backfills, before planning runs, or in CI):
conda run -n bgcki_wesemann python scripts/check_swbm_tree_set.py --config config/config.yaml
Exit codes: 0 = aligned, 1 = stale/missing metadata (rebuild required),
2 = artifact not present. The raw source-vs-artifact ID overlap is printed
as a diagnostic only — the artifact intentionally holds active trees only.
Rule of thumb (see AGENTS.md): after any Baumkataster switch or retraining,
run scripts/prepare_water_balance_features.py (or the full
run_ml_refresh_overnight.ps1, whose step 5 does it) and re-check with
check_swbm_tree_set.py.
The first pure calculation preserves the current prototype semantics:
0.4.0.8 of available field capacity.Both fractions are configuration values. They are starting assumptions, not validated production thresholds.
Daily precipitation interception and site-dependent infiltration are also ported as a pure calculation. Their runoff and surrounding-surface fractions are configuration values and remain subject to local calibration.
Both prototype sources are normalized to one daily record containing:
DWD/Parquet observations currently receive quality 1.0; Open-Meteo
gap-fill/forecast rows receive 0.7; unknown sources receive 0.5. These are
configurable provenance weights, not statistical forecast confidence.
Open-Meteo requests are cached in memory by rounded location and date range. The default spatial precision is two decimals, allowing nearby trees and all profile scenarios of one tree to share weather responses. This reduces API load but does not replace the preferred DWD cache for full-city runs.
Reference evapotranspiration uses the MIT-licensed pyet implementation of
FAO-56 Penman-Monteith. The prototype correction separates direct and diffuse
radiation and applies sunlit hours plus Sky View Factor before ET0 is
calculated. Input validation rejects duplicate dates and inconsistent humidity
or temperature bounds.
LAI allometry, winter branch-area index, transpiration fractions, and leaf
on/off windows are stored under climate.water_balance.vegetation. The Python
code contains no hidden genus table. Unknown genera use the explicit configured
defaults and are marked with parameter_source = default.
The prototype transpiration demand is retained: reference ET0 is converted to volume over crown area, multiplied by the seasonal transpiring fraction, and reduced according to available root-zone water after accounting for the daily ET demand and infiltration. Consequently, a sufficiently large ET demand can already reduce transpiration when the day starts at field capacity. This is a tested model property and should be assessed during sensor calibration.
Soil evaporation is evaluated after transpiration from the updated layer-1
water content. The prototype surface-wetting fraction fw is retained across
days and persisted as surface_wet_fraction. Throughfall of at least 10 mm
sets it to 1.0; at least 4 mm raises it to 0.6; otherwise it carries over.
The weather-to-flux adapter now produces complete reviewed daily fluxes from a
weather record and current tree state. Capillary rise is enabled in
operational forcing as of swbm-v0.3, using the h_da 120 cm lookup and a
kriged groundwater surface (see docs/water_balance/capillary_rise.md).
The full daily engine is ported and running in production (swbm-v0.3,
incl. the 2026-09-22 v0831 fidelity fixes).
Before operational recommendations, validate water states and demand
against sensor measurements, precipitation, and documented irrigation dates.
Unknown amounts must be evaluated as scenarios rather than imputed as facts.
The enriched Hannover 2024 snapshot was checked end to end on 2026-08-03:
| Metric | Count |
|---|---|
| Analysable unique trees | 61,193 |
| Trees with a resolved age profile | 54,558 |
| Trees with unknown age | 6,635 |
| Unique trees covered by at least one SWBM profile | 61,193 |
| Total profiles including age scenarios | 74,463 |
Age provenance note (issue #245, 2026-09-23): the LHH v6 “wie WFS” inventory
extract encodes unknown hauptdaten_standalter / hauptdaten_pflanzjahr
as 0 (the historical extract leaves them blank). prepare_water_balance_features
therefore normalizes 0 to missing before the feature GPKG is written, so
age resolution falls back to planting year and then to stem-circumference
estimation instead of building age-0 sapling pit profiles. A new LHH extract
must be checked for this missing-value encoding before it goes into service.
Unknown age is not replaced by one arbitrary value. Each affected tree gets three explicit profiles:
unknown-age-young: 5 years / 0.6 m prototype root depth,unknown-age-mature: 20 years / 1.2 m root depth,unknown-age-old: 40 years / 1.5 m root depth.The resolved or scenario name must become part of the simulation model version when daily states are persisted. Downstream consumers must not average these profiles without displaying their uncertainty range.
Crown diameter provenance in this smoke test included 3,893 species-regression
profiles, 148 global-regression profiles, and 6,415 age-class fallback scenario
profiles. PreparedTreeProfile.data_quality is reduced for each derived age,
crown, or default planting-site area. This gives full computational coverage,
not full observational certainty.
The complete 2024 pipeline GeoPackage was checked on 2026-08-03 after binding SVF, stem size, coordinates, and seasonal sunlit hours:
| Metric | Count |
|---|---|
| Unique pipeline trees | 61,193 |
| Unique trees with at least one complete forcing context | 61,193 |
| Combined profile/forcing scenarios | 91,585 |
| Excluded forcing contexts | 0 |
| Scenarios caused by unknown SVF | 25,683 |
This is full computational coverage, not full measurement coverage. If both
stem diameter and circumference are missing, the explicit prototype default of
30 cm is used with quality 0.3. If SVF is missing, three scenarios are
computed (0.3, 0.6, 0.9) rather than storing one invented value. Missing
seasonal sunlit anchors use visible config defaults; the current output lacks a
winter extra, so all current forcing contexts are capped at quality 0.5 even
when the other inputs are measured.
The profile scenario becomes part of model_version, keeping unknown-age and
unknown-SVF states separate in PostGIS. Consumers must summarize uncertainty
across scenarios explicitly and must not count scenario rows as independent
trees.