work on model training

This commit is contained in:
2026-03-05 11:03:20 +11:00
parent 96e72d7c43
commit c8e38cd597
10 changed files with 534 additions and 30 deletions

View File

@@ -24,14 +24,16 @@ type webServer struct {
}
type dashboardResponse struct {
GeneratedAt time.Time `json:"generated_at"`
Site string `json:"site"`
Model string `json:"model"`
RangeStart time.Time `json:"range_start"`
RangeEnd time.Time `json:"range_end"`
Observations []db.ObservationPoint `json:"observations"`
Forecast db.ForecastSeries `json:"forecast"`
Latest *db.ObservationPoint `json:"latest"`
GeneratedAt time.Time `json:"generated_at"`
Site string `json:"site"`
Model string `json:"model"`
RangeStart time.Time `json:"range_start"`
RangeEnd time.Time `json:"range_end"`
Observations []db.ObservationPoint `json:"observations"`
Forecast db.ForecastSeries `json:"forecast"`
Latest *db.ObservationPoint `json:"latest"`
LatestRainPredict *db.RainPredictionPoint `json:"latest_rain_prediction,omitempty"`
RainPredictionRange []db.RainPredictionPoint `json:"rain_predictions,omitempty"`
}
func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, addr string) error {
@@ -171,15 +173,33 @@ func (s *webServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
return
}
const rainModelName = "rain_next_1h"
latestRainPrediction, err := s.db.LatestRainPrediction(r.Context(), s.site.Name, rainModelName)
if err != nil {
http.Error(w, "failed to query latest rain prediction", http.StatusInternalServerError)
log.Printf("web dashboard latest rain prediction error: %v", err)
return
}
rainPredictionRange, err := s.db.RainPredictionSeriesRange(r.Context(), s.site.Name, rainModelName, start, end)
if err != nil {
http.Error(w, "failed to query rain predictions", http.StatusInternalServerError)
log.Printf("web dashboard rain prediction range error: %v", err)
return
}
resp := dashboardResponse{
GeneratedAt: time.Now().UTC(),
Site: s.site.Name,
Model: s.model,
RangeStart: start,
RangeEnd: end,
Observations: observations,
Forecast: forecast,
Latest: latest,
GeneratedAt: time.Now().UTC(),
Site: s.site.Name,
Model: s.model,
RangeStart: start,
RangeEnd: end,
Observations: observations,
Forecast: forecast,
Latest: latest,
LatestRainPredict: latestRainPrediction,
RainPredictionRange: rainPredictionRange,
}
w.Header().Set("Content-Type", "application/json")

View File

@@ -501,6 +501,40 @@ function buildRainProbabilitySeries(points) {
return out;
}
function buildRainProbabilitySeriesFromPredictions(points) {
return points.map((p) => {
const t = new Date(p.ts).getTime();
if (Number.isNaN(t)) {
return { x: null, y: null };
}
if (p.probability === null || p.probability === undefined) {
return { x: t, y: null };
}
return {
x: t,
y: Math.round(Number(p.probability) * 1000) / 10,
};
});
}
function thresholdSeries(range, threshold) {
if (!range || !range.axisStart || !range.axisEnd || threshold === null || threshold === undefined) {
return [];
}
const y = Math.round(Number(threshold) * 1000) / 10;
return [
{ x: range.axisStart.getTime(), y },
{ x: range.axisEnd.getTime(), y },
];
}
function predictionAgeMinutes(prediction) {
if (!prediction || !prediction.ts) return null;
const ts = new Date(prediction.ts).getTime();
if (Number.isNaN(ts)) return null;
return (Date.now() - ts) / (60 * 1000);
}
function updateWeatherIcons(latest, rainProb) {
const sunEl = document.getElementById("live-icon-sun");
const cloudEl = document.getElementById("live-icon-cloud");
@@ -693,10 +727,26 @@ function renderDashboard(data) {
const obsFiltered = filterRange(obs, rangeStart, rangeEnd);
const forecast = filterRange(forecastAll, rangeStart, rangeEnd);
const forecastLine = extendForecastTo(forecast, rangeEnd);
const rainPredictions = filterRange(data.rain_predictions || [], rangeStart, rangeEnd);
const latestRainPrediction = data.latest_rain_prediction || null;
const latestPredictionAgeMin = predictionAgeMinutes(latestRainPrediction);
const modelPredictionFresh = latestPredictionAgeMin !== null && latestPredictionAgeMin <= 90;
const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h");
const rainProb = computeRainProbability(latest);
const modelRainProb = modelPredictionFresh && latestRainPrediction && latestRainPrediction.probability !== null && latestRainPrediction.probability !== undefined
? {
prob: Number(latestRainPrediction.probability),
label: classifyRainProbability(Number(latestRainPrediction.probability)),
source: "model",
}
: null;
const heuristicRainProb = computeRainProbability(latest);
const rainProb = modelRainProb || heuristicRainProb;
if (rainProb) {
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`);
const sourceLabel = rainProb.source === "model" ? "model" : "heuristic";
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label}, ${sourceLabel})`);
} else if (latestRainPrediction && latestRainPrediction.probability !== null && latestRainPrediction.probability !== undefined) {
const stalePct = Math.round(Number(latestRainPrediction.probability) * 100);
updateText("live-rain-prob", `${stalePct}% (stale model)`);
} else {
updateText("live-rain-prob", "--");
}
@@ -901,13 +951,20 @@ function renderDashboard(data) {
data: {
datasets: [
{
label: "predicted rain probability (%)",
data: buildRainProbabilitySeries(obsFiltered),
label: rainPredictions.length ? "model rain probability (%)" : "heuristic rain probability (%)",
data: rainPredictions.length ? buildRainProbabilitySeriesFromPredictions(rainPredictions) : buildRainProbabilitySeries(obsFiltered),
borderColor: colors.rain,
backgroundColor: "rgba(78, 168, 222, 0.18)",
fill: true,
yAxisID: "y",
},
{
label: "decision threshold (%)",
data: thresholdSeries(range, latestRainPrediction ? latestRainPrediction.threshold : null),
borderColor: "#f4b942",
borderDash: [6, 4],
yAxisID: "y",
},
],
},
options: rainProbOptions,

View File

@@ -198,7 +198,7 @@
</div>
<div class="chart-card" data-chart="chart-rain-prob">
<div class="chart-header">
<div class="chart-title">Predicted Rain Probability (Observed Inputs)</div>
<div class="chart-title">Predicted Rain Probability (Model)</div>
<button class="chart-link" data-chart="chart-rain-prob" title="Copy chart link">Share</button>
</div>
<div class="chart-canvas">