c8e3016fd6
- Implement `sigen_daemon.py` to poll Sigenergy plant metrics and store snapshots. - Create `web_daemon.py` for serving a web interface with various endpoints. - Add debug scripts: - `debug_duplicates.py` to find duplicate target times in forecast data. - `debug_energy_forecast.py` to print baseline energy forecast curves. - `debug_oracle_evaluations.py` to run the oracle evaluator. - `debug_sigen.py` to inspect stored Sigenergy plant snapshots. - `debug_weather.py` to trace resolved truth data. - `modbus_test.py` for exploring Sigenergy plants or inverters over Modbus TCP. - Introduce `oracle_evaluator.py` for evaluating stored oracle predictions against actuals. - Add TCN training scripts in `tcn` directory for training usage sequence models.
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import timedelta
|
|
|
|
from gibil.classes.oracle.store import OracleStore
|
|
|
|
|
|
class OracleQualityDisplay:
|
|
"""Renders oracle prediction quality tables."""
|
|
|
|
def render(self) -> str:
|
|
return """
|
|
<section class="panel oracle-quality-panel" data-module="oracle-quality-display">
|
|
<div class="panel-heading">
|
|
<div>
|
|
<h2>Oracle Quality</h2>
|
|
<p>Prediction error by model and horizon</p>
|
|
</div>
|
|
<div class="control-row">
|
|
<label>
|
|
Window
|
|
<select id="quality-lookback">
|
|
<option value="24">24 hours</option>
|
|
<option value="168" selected>7 days</option>
|
|
<option value="720">30 days</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="table-shell">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Kind</th>
|
|
<th>Model</th>
|
|
<th>Horizon</th>
|
|
<th>Samples</th>
|
|
<th>Bias</th>
|
|
<th>MAE</th>
|
|
<th>Median AE</th>
|
|
<th>MAPE</th>
|
|
<th>Coverage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="quality-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
<script>
|
|
window.astrapeModules = window.astrapeModules || {};
|
|
window.astrapeModules.oracleQualityDisplay = (() => {
|
|
function init() {
|
|
document.getElementById("quality-lookback").addEventListener("change", refresh);
|
|
refresh();
|
|
setInterval(refresh, 10000);
|
|
}
|
|
|
|
async function refresh() {
|
|
const lookback = document.getElementById("quality-lookback").value;
|
|
const response = await fetch(`/api/oracle-quality?lookback_hours=${lookback}`, { cache: "no-store" });
|
|
const payload = await response.json();
|
|
render(payload.rows || []);
|
|
}
|
|
|
|
function render(rows) {
|
|
const tbody = document.getElementById("quality-rows");
|
|
if (!rows.length) {
|
|
tbody.innerHTML = `<tr><td colspan="9">No evaluated oracle predictions yet.</td></tr>`;
|
|
return;
|
|
}
|
|
tbody.innerHTML = rows.map((row) => `
|
|
<tr>
|
|
<td>${escapeHtml(row.kind)}</td>
|
|
<td>${escapeHtml(row.model_version)}</td>
|
|
<td>${formatHorizon(row)}</td>
|
|
<td>${row.evaluated_count}</td>
|
|
<td class="${biasClass(row.mean_error_w)}">${formatW(row.mean_error_w)}</td>
|
|
<td>${formatW(row.mean_absolute_error_w)}</td>
|
|
<td>${formatW(row.median_absolute_error_w)}</td>
|
|
<td>${formatPct(row.mean_absolute_pct_error)}</td>
|
|
<td>${formatPct(row.interval_coverage)}</td>
|
|
</tr>
|
|
`).join("");
|
|
}
|
|
|
|
function formatHorizon(row) {
|
|
if (row.horizon_label) return row.horizon_label;
|
|
return `${row.min_horizon_minutes}-${row.max_horizon_minutes}m`;
|
|
}
|
|
|
|
function formatW(value) {
|
|
if (value === null || value === undefined) return "n/a";
|
|
return `${Math.round(Number(value))} W`;
|
|
}
|
|
|
|
function formatPct(value) {
|
|
if (value === null || value === undefined) return "n/a";
|
|
return `${(Number(value) * 100).toFixed(1)}%`;
|
|
}
|
|
|
|
function biasClass(value) {
|
|
if (value === null || value === undefined) return "";
|
|
const absolute = Math.abs(Number(value));
|
|
if (absolute < 250) return "metric-good";
|
|
if (absolute < 1000) return "metric-warn";
|
|
return "metric-bad";
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
return { init };
|
|
})();
|
|
window.astrapeModules.oracleQualityDisplay.init();
|
|
</script>
|
|
"""
|
|
|
|
def data_payload(self, lookback_hours: float = 168) -> str:
|
|
try:
|
|
rows = OracleStore.from_env().load_evaluation_summary(
|
|
lookback=timedelta(hours=lookback_hours)
|
|
)
|
|
except Exception:
|
|
rows = []
|
|
|
|
return json.dumps(
|
|
{
|
|
"lookback_hours": lookback_hours,
|
|
"rows": [self._row(row) for row in rows],
|
|
}
|
|
)
|
|
|
|
def _row(self, row: dict[str, object]) -> dict[str, object]:
|
|
return {
|
|
key: self._json_value(value)
|
|
for key, value in row.items()
|
|
}
|
|
|
|
def _json_value(self, value: object) -> object:
|
|
if value is None or isinstance(value, (str, int, float, bool)):
|
|
return value
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return str(value)
|