☀️ Solar panel detector and OpenSTEF prediction in Delft, Rijswijk and Zoetermeer.

Rooftop solar is growing fast across Delft, Rijswijk and Zoetermeer, yet nearly invisible to the grid that has to carry it. This prototype changes that: a vision model detects the panels on PDOK aerial photos, groups them per hexagon, and estimates the power they make every 15 minutes from cloud cover and the sun's position. Drag the slider to step through the day. The page ends with an OpenSTEF demo: a one-week-ahead forecast of all the panels combined, with an uncertainty band. More settings at the bottom.
capacity
–
now
–
day total
–
running total
–
power
0100
time –
weather & scope
⚙ settings: model assumptions (recompute live)
what am I looking at?

The map shows every roof in the region where the model found panels, colored by how much power they are making right now. Drag the slider, or hit play, and time moves: the sun comes up, clouds roll in, and output follows. The date picker jumps to another day.

The dashed vertical line in each chart is where the slider sits. Hover a chart to read exact values. Click a hexagon to zoom into that cell and see the detected panel masks in red over the aerial photo. The ⚙ settings below change the assumed panel setup and recalculate everything.

the math behind the curves

Every 15 minutes the same two steps run twice: once on the measured weather (orange) and once on clear-sky irradiance (blue dashed). GHI is the total sunlight on a flat surface, DNI the direct beam, DHI the scattered sky light, all in W/m².

  1. Sunlight on the tilted panel (plane-of-array irradiance): POA = DNI·cos(AOI) + DHI·[(1−AI)·(1+cos β)/2 + AI·Rb] + GHI·ρ·(1−cos β)/2
    In words: the direct beam, dimmed by how far the sun is from straight ahead of the panel (cos(AOI), the angle between the sun and the panel's surface); plus the scattered sky light, part coming evenly from the whole sky and part boosted around the sun's direction (Rb grows the more the panel faces the sun); plus a little ground reflection (ρ = 20% albedo). β is the panel tilt.
  2. Cell temperature (the panel heats up in the sun): Tcell = Tair + (NOCT − 20)/800 · POA
    In words: the cell runs above air temperature by an amount proportional to the sunlight hitting it. The stronger the sun, the hotter the panel.

That sun-on-panel value becomes power through two plain effects: the panel converts it at a rate that shrinks as the cell heats up (the ⚙ temperature coefficient), and the result is scaled by the roof's installed capacity (panel area × 200 Wp/m²) and the system losses (14% default). Solar position comes from pvlib's NREL SPA; clear-sky irradiance is the pvlib Ineichen model (Linke turbidity 3.0).

how it's built

The red panel masks come from a SegFormer segmentation model (PyTorch) trained on aerial orthophotos. The polygons on the map are its direct output, and each hexagon's capacity is detected panel area × 200 Wp/m². Everything else runs in this one self-contained HTML file: Leaflet for the map, uPlot for the charts, pvlib for the solar physics (NREL SPA, Ineichen clear-sky, PVWatts), Open-Meteo for the weather, H3 cells to aggregate the rooftops.

output today: clear vs actual
cumulative energy
sun & sky: GHI + cloud cover

What the panels will make: an OpenSTEF forecast

whole fleet
–
last 12 months
–
forecast week P50
–
what actually happened
–
daylight band coverage nominal 80% · – above 5% of peak
–
One week ahead, one point every 15 minutes: green = P50 (median forecast), grey band = P10–P90 range, dashed = what actually happened · issued –
How the two models are combined: a stacking model gives each base learner a weight at every 15-minute step in the forecast week (they sum to 1). gblinear models the smooth clear-sky shape and carries most of the daylight forecast; lgbm gets more weight at night and around sunrise and sunset, where its trees handle the nighttime flatlines a linear model cannot correct.
what moves the forecast: one row per input feature, one column per 15-minute step (SHAP on the P50, lgbm base model, forecast week). Red means the feature pushed the forecast up at that moment, blue means it pulled it down; darker is stronger. The irradiance rows dominate, tilted GHI most of all, which is what you would expect: the inputs are sun angle, day length and measured sunlight. The small rows underneath are the model's smaller corrections.
is the band honest? Every point is one of the band's three quantiles (P10, P50, P90). The x-axis is the coverage it claims, the y-axis is the coverage measured on the forecast week; on the dashed diagonal the two match. The raw OpenSTEF quantiles in orange overshoot at the top: the top edge covers 98% of the outcomes where it claims 90%. In green, after the conformal calibration, the top edge lands back on the diagonal, and the whole P10–P90 band covers 83% where it claims 80% · forecast week, daylight rows