Germany: capacity map and register validation¶
Germany is the only place this project can check itself against an answer rather than against another estimate. Registration in the Marktstammdatenregister (MaStR) is legally mandatory for grid-connected PV, so German rooftop capacity per municipality is not a sample. This page is the first national end-to-end run there, produced 2026-08-31 from compose through the atlas below and re-derived twice since, because the register found two errors in it.
It is a different kind of page from the Pakistan capacity map or the Gujarat map. Those report what the pipeline estimates. This one reports how wrong that estimate is, because here that is measurable.
This atlas's tier totals fail a register check. Do not quote them.
The evidence atlas reports Verified 38,508 MWp and Best 49,324 MWp for Germany. Both are too high, and unlike anywhere else this project works, that can be proven rather than suspected.
Verified is the hand-mapped OpenStreetMap population, converted at the module constant for rooftop and the land constant for ground. Against the register:
| OSM-derived | MaStR registered | ||
|---|---|---|---|
| Rooftop | 22,032 MWp (122.4 km² × 0.18) | 74,823 MWp | 29.4% of all capacity, from a reference covering 3.6% of units |
| Ground | 43,965 MWp (879.3 km² × 0.05) | 37,138 MWp | 118% |
The ground component alone exceeds all registered German ground-mount capacity,
which is impossible. The cause is mapper convention, and it is already measured further
down this page: nothing in OSM records whether a generator:source=solar polygon
outlines the panel array or the whole roof or site it sits on, and Germany's implied
kWp/m² spans 0.02 to 0.99 against the project's 0.18. Area × a constant does
not give German capacity.
Germany's defensible number is the segmentation estimator est_mwp_rc_roof, at an OLS
slope of 0.405 against the register. The atlas is published for its structure and
per-cell geography, not for its headline figures.
Segmentation-only: no roofclf half, and a register cannot supply one
Germany has zero calibration quadrats, so neither roofclf nor the sub-400 m2
instruments exist for it, the same state as Gujarat. Verified therefore has no
roofclf-AND-SPPI population to add and Best no roofclf-alone density; both tiers stop at
the 400 m² floor, above which sits only 34.5% of German rooftop capacity. The
register does not substitute: MaStR publishes no coordinates below 30 kWp, which is
roofclf's entire domain. Closing this needs mapped German quadrats, and the 3.6% OSM
completeness measured below means they have to be mapped, not derived from an OSM pull.
The six-exposure page previously published here was deprecated and removed
(2026-09-02). density used to build it automatically alongside the real product, and
its hero figure read 114 GWp.
What ran¶
| 4,656 | 0.1° cells composited and inferred (of 4,664 selected; 8 returned empty composites from both STAC sources) |
| 2025-04-01 to 2025-09-30 | composite window, matched to the register cutoff and to the training imagery |
v4_combined_all epoch=41 |
checkpoint. Not the documented v3_combined_india, which is no longer on disk (owner-approved substitution, 2026-08-23) |
| 23,920 | capacity-relevant candidates (of 24,590; oversize blobs excluded as density does) |
| 6,613 / 17,307 | OSM-mapped / unmapped candidates |
| 10,533 of 10,949 | municipalities covered: 96.2% by count, 99.75% by capacity |
That coverage is what lets validate_density_against_mastr report a national result instead
of refusing. The guard is not decorative: a run covering 4 of Germany's 76 MGRS tiles would
have produced a national sum near 5% of truth and a slope that reads as catastrophic model
failure rather than as missing imagery.
How accurate is it¶
Truth here is kw_rooftop - kw_le_72 = 25,723.5 MWp, the registered capacity in units
above the 400 m² / 72 kWp segmentation floor. That is the denominator a ≥ 400 m²
model can actually see; measured against all rooftop capacity (74,637.6 MWp) every slope
below falls by roughly the 65.5% sub-floor share, which is a statement about the floor and
not about the model.
| Estimator | Slope (1.0 = unbiased) | Predicted MWp | Spearman ρ | Reading |
|---|---|---|---|---|
est_mwp_rc_roof |
0.405 | 12,509.7 | 0.628 | Recall-corrected. Germany's readable number |
est_mwp_exp |
0.388 | 13,110.4 | 0.656 | Probability-weighted ceiling |
est_mwp_det |
0.340 | 10,699.6 | 0.661 | Precision-honest floor |
est_mwp_cal |
0.167 | 5,190.2 | 0.651 | Precision-weighted |
Two things are worth taking away, and they point in opposite directions.
Ranking transfers; level does not. Every estimator lands at ρ ≈ 0.63 to 0.66 while slopes span 0.17 to 0.41. The model puts capacity in the right municipalities and gets the amount wrong. That is the same pattern as Pakistan's external comparison, now confirmed against a mandatory register rather than owner-attested quadrats.
No estimator here is close to unbiased. The best-calibrated recovers about 40% of the capacity it could see. Read that as the honest accuracy of segmentation-only detection at 10 m GSD, not as a defect introduced by the calibration work below.
What the register found, twice¶
The full derivation is in Validating against a complete register. Neither correction would have been visible without a complete reference.
1. p_unmapped was a zero. No instrument existed for P(candidate is real | no OSM
match), so Germany's table shipped 0.0 and priced every unmapped candidate at nothing.
MaStR closes that above 30 kWp, where it publishes per-unit coordinates. Measured per
placement and chance-corrected, rooftop p_unmapped runs 0.061 to 0.759 across the size
bins. est_mwp_cal moved 0.038 → 0.167.
2. That fix exposed a second, opposite error. est_mwp_rc_roof jumped from 0.262 to
3.11 — from understating truth to overstating it threefold. Two errors had been
cancelling, and removing one revealed the other.
The cause was not either hypothesis first written down.
capacity_calibration.derive_placement_tables restricted both sides of the recall
measurement by placement, so a rooftop reference installation only counted as found if the
candidate that found it was itself classified rooftop:
| Rooftop bin | vs same-placement candidates | vs any candidate | factor |
|---|---|---|---|
| 500-1k m² | 0.128 | 0.167 | 1.3x |
| 1k-5k | 0.214 | 0.268 | 1.25x |
| 5k-50k | 0.096 | 0.693 | 7.3x |
| >50k | 0.036 | 0.852 | 23.9x |
Precision and recall are asymmetric. mapped_frac asks "is this candidate real", so its
corroboration must come from references of its own placement. Recall asks "was this real
installation detected at all", and how postprocess labelled the finding candidate is
irrelevant to that. The mechanism explains why the error grew with size: a large array
overruns its imagery-derived VIDA footprint, building_overlap_frac collapses, and a
candidate that correctly found a rooftop installation is classified ground_adjacent or
no_building — the same undersizing the parcel label exists to handle. 1/recall
then inflated those candidates by up to the 20x clamp.
Fixing it moved est_mwp_rc from 114,145 to 24,687 MWp nationally and its slope from
3.11 to 0.405, making it the best-calibrated of the four estimators. est_mwp_det and
est_mwp_exp did not move at all, which is the sanity check: neither uses recall.
Pakistan shares this code and its published figures are overstated
The same restriction understated Pakistani recall: rooftop 0.423 → 0.808 in the
5k-50k bin, 0.065 → 0.952 above 50k, ground 0.107 → 0.417 in 500-1k. Because
1/recall is the multiplier, Pakistan's est_mwp_rc — and therefore its Best
estimate — is too high. Its numbers come from the checked-in calibration table
and do not move until that is deliberately re-derived, which needs the glint sample and
the calibration boxes. That re-derivation has not been run.
Two hypotheses were written down first and both were measured and refuted: that oversize
rooftop reference features deflated the top bin (they recall at 0.841, no different from
the rest) and that count-recall applied to area inflated the estimator (the area/count ratio
is 1.01 to 1.08). Recorded because the wrong diagnosis was the plausible one.
The plausibility gate¶
check-density is the pre-publication gate for failure modes p_real weighting cannot
catch. Across the three runs:
| Original | After p_unmapped |
After the recall fix | |
|---|---|---|---|
| ok | 19 | 17 | 18 |
| suspect | 1 (Saarland, ground-mount 3.6x rooftop) | 0 | 0 |
| fail | 0 | 3 (Hamburg, Bremen) | 2 (Hamburg) |
Saarland's flag cleared legitimately: it was flagged because ground-mount read 3.6x its
rooftop total, and rooftop capacity rising is exactly the correction that ratio wanted.
Bremen cleared when recall was fixed. Hamburg's remaining failure is structural rather than
a detection — a city-state spanning a handful of 0.1° cells has a top cell holding
31% of its total no matter what. That is the same reading this project already applies to
Islamabad Capital Territory, and the same standing precedent for publishing a
checked-genuine plausibility failure. The ground:rooftop ratio check correctly skipped it (it
requires mwp_ground >= 50; Hamburg has 7.0), while the concentration check has no
equivalent minimum-region-size guard.
One data quirk visible in the output: plausibility.csv carries 20 rows for Germany's 16
states, with Hamburg, Mecklenburg-Vorpommern, Niedersachsen and Schleswig-Holstein each
appearing twice. That is a duplication in the region polygons, not a duplicated estimate, and
it is why Hamburg counts as two failures.
What this run opens up¶
Both remaining items are tracked on Open questions.
MaStR also records installed pose, uncensored (item 11). glint_opportunity.py documents
its pose prior as necessarily assumed, because this project's own pose survey was fitted
from observed glints and is therefore censored by construction. The register carries azimuth
and tilt for 97.4% of 4.44M rooftop units, of which 225,138 also have coordinates. Checked
against the assumed prior it is far too narrow: 23.8% of German units are pitched steeper
than 40° where the prior implies 3.04%.
France, not Germany, is the place to check the small half (item 4). The 30 kWp coordinate
cliff means this register can say nothing about the sub-400 m² population, which is
where roofclf operates and where the evidence is still 30 purposive Pakistani quadrats.
DeepPVMapper and BDAPPV cover exactly that band in France.
Reproducing this¶
# compose is the long pole (~4,700 cells at ~35 cells/h). LimitNOFILE must be the
# soft:hard PAIR -- a bare 65536 sets only the hard limit, leaves the soft limit at
# 1024, and compose dies repeatedly on "Too many open files".
systemd-run --user --unit earthpv-compose-germany --working-directory=$PWD \
--property=Restart=on-failure --property=LimitNOFILE=65536:65536 \
bash -c '.pixi/envs/default/bin/python -m earthpv.cli compose --aoi germany \
--use-vida --workers 5 --window 2025-04-01:2025-09-30'
pixi run -e ml earthpv infer --aoi germany --checkpoint <ckpt>
pixi run earthpv postprocess --aoi germany --threshold 0.3
# Measure p_unmapped from geolocated MaStR units, then feed it to the calibration.
# Without this Germany's table carries p_unmapped = 0.0 and est_mwp_cal is a floor.
pixi run python scripts/mastr_p_unmapped.py --base-rate-cells 150
pixi run earthpv calibrate-candidates --aoi germany \
--mastr-p-unmapped results/germany_mastr_p_unmapped.csv
pixi run earthpv density --aoi germany --districts --force
pixi run earthpv check-density --aoi germany
pixi run earthpv validate-mastr --aoi germany --solar-path <national OSM solar pull>
# The atlas is no longer written by `density`. A raw rooftopsenti OSM pull has no
# `placement` column, so it is prepared first; omitting the --sub400-* pair selects
# the segmentation-only evidence atlas.
pixi run python scripts/prepare_national_osm_solar.py --aoi germany
pixi run earthpv atlas --aoi germany \
--osm-solar data/labels/germany_national_osm_solar.parquet
The composite window must match the register cutoff (2025-04-01:2025-09-30 against a
2025-09-30 cutoff); the compose default is a Punjab dry season, which is German winter.
See Setup New Country for the full runbook.