Initial commit: astrape-dashboard split out of the combined Astrape tree
The web frontend: the UI itself, the oracle, weather, station and quality renderers, and the serving daemon. The weather and station display modules were each split in two. The dataclasses went to astrape-oracle with the stores that return them; only the rendering stayed here, so the dependency runs dashboard -> oracle -> ems -> gibil with no cycle. Extracted from a single repository where the forecasting engine, the database plumbing, the hardware client, the ingest daemons and the web UI all imported each other freely. Verified that every module imports with gibil resolved from its own installed checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import pi, sin
|
||||
|
||||
from gibil.classes.models import WeatherForecastPoint, WeatherResolvedTruth
|
||||
from astrape_oracle.classes.datasets 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)
|
||||
Reference in New Issue
Block a user