75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from math import pi, sin
|
|
|
|
from gibil.classes.models import WeatherForecastPoint, WeatherResolvedTruth
|
|
from gibil.classes.weather_display import WeatherDisplayDataset
|
|
|
|
|
|
class WeatherSampleData:
|
|
"""Builds temporary display-only weather data for UI tuning."""
|
|
|
|
def build(self, hours: int = 72) -> WeatherDisplayDataset:
|
|
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
|
|
start = now - timedelta(hours=hours)
|
|
horizons = [2, 4, 8, 12, 24]
|
|
|
|
forecast_points: list[WeatherForecastPoint] = []
|
|
resolved_truth: list[WeatherResolvedTruth] = []
|
|
|
|
for offset in range(hours + 1):
|
|
target_at = start + timedelta(hours=offset)
|
|
truth_temperature = self._temperature(target_at, offset)
|
|
truth_solar = self._solar(target_at, offset)
|
|
|
|
resolved_truth.append(
|
|
WeatherResolvedTruth(
|
|
resolved_at=target_at,
|
|
source="sample",
|
|
temperature_c=truth_temperature,
|
|
shortwave_radiation_w_m2=truth_solar,
|
|
)
|
|
)
|
|
|
|
for horizon in horizons:
|
|
forecast_points.append(
|
|
WeatherForecastPoint(
|
|
issued_at=target_at - timedelta(hours=horizon),
|
|
target_at=target_at,
|
|
horizon_hours=horizon,
|
|
temperature_c=truth_temperature
|
|
+ self._temperature_error(offset, horizon),
|
|
shortwave_radiation_w_m2=max(
|
|
0,
|
|
truth_solar + self._solar_error(offset, horizon),
|
|
),
|
|
cloud_cover_pct=max(
|
|
0,
|
|
min(100, 45 + 30 * sin((offset + horizon) / 9)),
|
|
),
|
|
source="sample",
|
|
)
|
|
)
|
|
|
|
return WeatherDisplayDataset(
|
|
forecast_points=forecast_points,
|
|
resolved_truth=resolved_truth,
|
|
)
|
|
|
|
def _temperature(self, target_at: datetime, offset: int) -> float:
|
|
daily = sin(((target_at.hour - 7) / 24) * 2 * pi)
|
|
slow = sin(offset / 18)
|
|
return round(6.5 + daily * 5.5 + slow * 1.3, 1)
|
|
|
|
def _solar(self, target_at: datetime, offset: int) -> float:
|
|
daylight = max(0, sin(((target_at.hour - 5) / 15) * pi))
|
|
cloud_effect = 0.75 + 0.25 * sin(offset / 7)
|
|
return round(780 * daylight * cloud_effect, 1)
|
|
|
|
def _temperature_error(self, offset: int, horizon: int) -> float:
|
|
return round((horizon / 8) * sin((offset + horizon) / 5), 1)
|
|
|
|
def _solar_error(self, offset: int, horizon: int) -> float:
|
|
return round((horizon * 9) * sin((offset + horizon) / 4), 1)
|