From 567845f88e649251dec509ebaea7844fca6d266f Mon Sep 17 00:00:00 2001 From: Adriano Caloiaro Date: Sun, 12 Apr 2026 07:38:30 -0600 Subject: [PATCH] fix(weather): apply Beer-Lambert extinction to clear-sky model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simple atmosphericCeiling*sin(elevation) model overestimates clear-sky radiation at low sun angles because it ignores the longer atmospheric path light travels near the horizon. At 5° elevation the path is ~11x vertical, making the real clear-sky ceiling ~26 W/m² rather than the 83 W/m² the naive model predicts — causing sunny low-angle readings to be misclassified as mostly cloudy. Replace with the Beer-Lambert correction: expectedClearSky = 950 * exp(-opticalDepth / sin(elevation)) * sin(elevation) opticalDepth=0.1 (clear atmosphere) produces accurate values across all angles: ~26 W/m² at 5°, ~93 W/m² at 10°, ~715 W/m² at solar noon. --- weather/condition.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/weather/condition.go b/weather/condition.go index 343bdca..93cfbdf 100644 --- a/weather/condition.go +++ b/weather/condition.go @@ -11,11 +11,18 @@ type Condition struct { Emoji string `json:"emoji"` } -// atmosphericCeiling is the clear-sky solar radiation (W/m²) with the sun -// directly overhead at sea level. Actual clear-sky radiation at any other -// elevation angle scales by sin(elevation). +// atmosphericCeiling is the extraterrestrial solar irradiance at sea level (W/m²) +// with the sun directly overhead. const atmosphericCeiling = 950.0 +// opticalDepth is the vertical atmospheric optical depth used in the Beer-Lambert +// extinction model. At low sun angles the light path through the atmosphere is +// 1/sin(elevation) times longer than vertical, so the effective transmittance +// drops sharply. A value of 0.1 represents a clear sky and produces realistic +// clear-sky readings across all elevation angles (e.g. ~26 W/m² at 5°, ~715 W/m² +// at solar noon for mid-latitudes in spring). +const opticalDepth = 0.1 + // solarPosition returns the solar elevation angle (radians) and hour angle // (degrees) for the given latitude, longitude, and UTC time. // @@ -80,8 +87,10 @@ func Infer(d Data, lat, lon float64) *Condition { } // Sun is above the horizon. Normalise against the clear-sky radiation - // expected at this elevation angle. - expectedClearSky := atmosphericCeiling * math.Sin(elevationRad) + // expected at this elevation angle, corrected for atmospheric extinction + // (Beer-Lambert): at low angles the light path is ~1/sin(elev) times + // longer than vertical, so the clear-sky ceiling drops sharply. + expectedClearSky := atmosphericCeiling * math.Exp(-opticalDepth/math.Sin(elevationRad)) * math.Sin(elevationRad) if solar < 10 { return &Condition{"Overcast", "☁️"} }