Foo
This commit is contained in:
parent
77e1488830
commit
374be6865d
19 changed files with 1276 additions and 64 deletions
283
webapp/gap_validation.html
Normal file
283
webapp/gap_validation.html
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gap validation</title>
|
||||
<style>
|
||||
body { margin: 0; font-family: sans-serif; }
|
||||
.nav { margin-bottom: 15px; font-size: 14px; }
|
||||
.nav a { margin-right: 12px; color: #0066cc; text-decoration: none; }
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
.nav a.active { font-weight: bold; }
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
|
||||
.selectors { margin-bottom: 18px; }
|
||||
.selectors select { padding: 5px 10px; font-size: 14px; margin-right: 15px; }
|
||||
h1 { font-size: 22px; margin-top: 0; }
|
||||
h2 { font-size: 16px; margin-top: 22px; color: #333; }
|
||||
h2:first-of-type { margin-top: 8px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 12px; margin-bottom: 14px; }
|
||||
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; vertical-align: top; }
|
||||
th { background: #f5f5f5; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
td.paths { font-size: 11px; word-break: break-all; color: #444; max-width: 420px; }
|
||||
.intro { font-size: 13px; color: #333; background: #fafafa; border: 1px solid #e5e5e5;
|
||||
padding: 10px 12px; border-radius: 4px; margin-bottom: 16px; line-height: 1.5; }
|
||||
.intro code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
|
||||
.section-note { font-size: 12px; color: #555; margin: -6px 0 8px 0; line-height: 1.45; }
|
||||
.empty { color: #666; font-style: italic; }
|
||||
.err { color: #a00; }
|
||||
details.meta { font-size: 12px; margin-top: 12px; border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px 12px; background: #fafafa; }
|
||||
details.meta summary { cursor: pointer; font-weight: 600; }
|
||||
details.meta pre { margin: 8px 0 0; overflow: auto; font-size: 11px; max-height: 200px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
<a href="index.html">Full</a>
|
||||
<a href="preselection.html">Pre-selection</a>
|
||||
<a href="prepared.html">Prepared</a>
|
||||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html" class="active">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="pageTitle">Gap validation</h1>
|
||||
<div class="selectors">
|
||||
<label>Site:</label>
|
||||
<select id="siteSelect"></select>
|
||||
<label>Season:</label>
|
||||
<select id="seasonSelect"></select>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
</div>
|
||||
<script>
|
||||
let siteName = "innsbruck",
|
||||
season = "2024";
|
||||
let availableSiteSeasons = {};
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
async function probeSummary(sn, s) {
|
||||
try {
|
||||
const res = await fetch(`data/${sn}/${s}/validation/gap_validation_summary.json`, {
|
||||
method: "HEAD",
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(v, d = 4) {
|
||||
if (v == null || typeof v !== "number" || !Number.isFinite(v)) return "—";
|
||||
return v.toFixed(d);
|
||||
}
|
||||
|
||||
function fmtInt(v) {
|
||||
if (v == null || typeof v !== "number" || !Number.isFinite(v)) return "—";
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
function crossoverBlock(summary) {
|
||||
const scen = summary.scenario;
|
||||
const wcRoot = summary.whittaker_crossover || {};
|
||||
const wc = (scen && wcRoot[scen]) || Object.values(wcRoot)[0];
|
||||
if (!wc) return "";
|
||||
const first = wc.first_gap_days_fusion_nse_below_whittaker;
|
||||
const def = wc.whittaker_definition || "";
|
||||
let h = `<h2>Whittaker crossover (NSE<sub>S2</sub>)</h2>`;
|
||||
h += `<p class="section-note">${def}</p>`;
|
||||
h += `<p class="section-note"><b>First gap length (days)</b> where fusion NSE<sub>S2</sub> < Whittaker NSE<sub>S2</sub> (strict): <b>${first != null ? first : "—"}</b> (none if fusion never falls below).</p>`;
|
||||
const rows = wc.by_gap || [];
|
||||
if (rows.length) {
|
||||
h += `<table><tr><th>Gap days</th><th class="num">NSE<sub>S2</sub> fusion</th><th class="num">NSE<sub>S2</sub> Whittaker</th></tr>`;
|
||||
for (const r of rows) {
|
||||
h += `<tr><td>${r.gap_days}</td><td class="num">${fmt(r.nse_s2_fusion, 3)}</td><td class="num">${fmt(r.nse_s2_whittaker, 3)}</td></tr>`;
|
||||
}
|
||||
h += `</table>`;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function manifestTable(manifest) {
|
||||
if (!manifest?.entries?.length) return "";
|
||||
let h = `<h2>Gap manifest</h2>`;
|
||||
h += `<p class="section-note">From <code>data/${siteName}/${season}/validation/gap_manifest.json</code>. Midpoint rule: ${manifest.entries[0]?.midpoint_rule || "—"}.</p>`;
|
||||
h += `<table><tr><th>Gap days</th><th>Prediction</th><th>Window</th><th>Withheld S2</th></tr>`;
|
||||
for (const e of manifest.entries) {
|
||||
const w = `${e.window_start} → ${e.window_end}`;
|
||||
h += `<tr><td>${e.gap_days}</td><td>${e.prediction_date}</td><td>${w}</td><td>${e.withheld_s2_filename || "—"}</td></tr>`;
|
||||
}
|
||||
h += `</table>`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function resultsTable(results) {
|
||||
if (!results?.length) return `<p class="empty">No result rows in summary.</p>`;
|
||||
const head = `<tr>
|
||||
<th>Gap</th><th>Prediction</th><th>Withheld REFL</th>
|
||||
<th class="num">RMSE<br><span style="font-weight:normal">gap</span></th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">gap</span></th>
|
||||
<th class="num">RMSE<br><span style="font-weight:normal">no gap</span></th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">no gap</span></th>
|
||||
<th class="num">ΔRMSE</th><th class="num">ΔNSE</th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">Whitt.</span></th>
|
||||
<th class="num">n</th>
|
||||
<th>Paths / error</th>
|
||||
</tr>`;
|
||||
const parts = [head];
|
||||
for (const r of results) {
|
||||
if (r.error) {
|
||||
parts.push(
|
||||
`<tr><td>${r.gap_days ?? "—"}</td><td colspan="10" class="err">${r.error}</td><td class="paths">${r.fused_gap_path || ""}</td></tr>`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const g = r.spatial?.gap || {};
|
||||
const ng = r.spatial?.no_gap || {};
|
||||
const wh = r.spatial?.whittaker || {};
|
||||
const dRm = r.spatial?.delta_rmse;
|
||||
const dNs = r.spatial?.delta_nse;
|
||||
const p = r.paths || {};
|
||||
const pathNote = [p.fused_gap, p.fused_no_gap, p.withheld_s2_refl].filter(Boolean).join("<br>");
|
||||
parts.push(`<tr>
|
||||
<td>${r.gap_days}</td>
|
||||
<td>${r.prediction_date || "—"}</td>
|
||||
<td style="font-size:11px">${r.withheld_s2_filename || "—"}</td>
|
||||
<td class="num">${fmt(g.rmse)}</td>
|
||||
<td class="num">${fmt(g.nse_s2, 3)}</td>
|
||||
<td class="num">${fmt(ng.rmse)}</td>
|
||||
<td class="num">${fmt(ng.nse_s2, 3)}</td>
|
||||
<td class="num">${fmt(dRm)}</td>
|
||||
<td class="num">${fmt(dNs, 3)}</td>
|
||||
<td class="num">${fmt(wh.nse_s2, 3)}</td>
|
||||
<td class="num">${fmtInt(g.n_pixels)}</td>
|
||||
<td class="paths">${pathNote}</td>
|
||||
</tr>`);
|
||||
}
|
||||
return `<table>${parts.join("")}</table>`;
|
||||
}
|
||||
|
||||
function metaDetails(summary) {
|
||||
const cmd = summary.command_line;
|
||||
const git = summary.git_commit;
|
||||
if (!cmd && !git) return "";
|
||||
let h = `<details class="meta"><summary>Run metadata</summary>`;
|
||||
if (git) h += `<p>Git: <code>${git}</code></p>`;
|
||||
if (cmd?.length) h += `<pre>${cmd.map((x) => String(x)).join(" ")}</pre>`;
|
||||
h += `</details>`;
|
||||
return h;
|
||||
}
|
||||
|
||||
async function render(summary, manifest) {
|
||||
const el = document.getElementById("content");
|
||||
if (!summary) {
|
||||
el.innerHTML = `<p class="err">Could not load <code>data/${siteName}/${season}/validation/gap_validation_summary.json</code>.</p>
|
||||
<p class="section-note">From <code>processing/</code>: <code>python -m gap_validation.run --site ${siteName} --season ${season} --lat LAT --lon LON</code> (see <code>--help</code>). Serve from <code>processing/</code>: <code>python3 -m http.server 8000</code> → <code>/webapp/gap_validation.html</code> (<code>webapp/data</code> → <code>../data</code>).</p>`;
|
||||
if (manifest?.entries) el.innerHTML += manifestTable(manifest);
|
||||
return;
|
||||
}
|
||||
const scen = summary.scenario || "—";
|
||||
const sn = summary.site_name ?? siteName;
|
||||
const se = summary.season ?? season;
|
||||
let html = `<div class="intro">
|
||||
Tier-2 withheld S2, spatial GCC vs withheld scene, NSE<sub>S2</sub>, and Whittaker comparison.
|
||||
Summary: <code>data/${sn}/${se}/validation/gap_validation_summary.json</code>.
|
||||
Scenario in this file: <b>${scen}</b> (one run overwrites; re-run CLI for other strategy/σ/mode).
|
||||
</div>`;
|
||||
html += `<h2>Spatial metrics (per gap length)</h2>`;
|
||||
html += `<p class="section-note">Reference = GCC from withheld S2 REFL (bilinear to fusion grid). Prediction = fused GCC. ΔRMSE = RMSE<sub>gap</sub> − RMSE<sub>no gap</sub>; ΔNSE = NSE<sub>no gap</sub> − NSE<sub>gap</sub>.</p>`;
|
||||
html += resultsTable(summary.results);
|
||||
html += crossoverBlock(summary);
|
||||
html += metaDetails(summary);
|
||||
if (manifest?.entries) html += manifestTable(manifest);
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
let summary = null,
|
||||
manifest = null;
|
||||
try {
|
||||
const r1 = await fetch(`data/${siteName}/${season}/validation/gap_validation_summary.json`);
|
||||
summary = r1.ok ? await r1.json() : null;
|
||||
} catch {
|
||||
summary = null;
|
||||
}
|
||||
try {
|
||||
const r2 = await fetch(`data/${siteName}/${season}/validation/gap_manifest.json`);
|
||||
manifest = r2.ok ? await r2.json() : null;
|
||||
} catch {
|
||||
manifest = null;
|
||||
}
|
||||
await render(summary, manifest);
|
||||
const site = window.sitesData?.features?.find((f) => f.properties?.sitename === siteName);
|
||||
document.getElementById("pageTitle").textContent =
|
||||
(site?.properties?.description || siteName) + " — gap validation — " + season;
|
||||
urlParams.set("site", siteName);
|
||||
urlParams.set("season", season);
|
||||
history.replaceState({}, "", `?${urlParams}`);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch("data/sites.geojson");
|
||||
window.sitesData = res.ok ? await res.json() : { features: [] };
|
||||
} catch {
|
||||
window.sitesData = { features: [] };
|
||||
}
|
||||
const features = window.sitesData.features || [];
|
||||
for (const f of features) {
|
||||
const sn = f.properties?.sitename;
|
||||
if (!sn) continue;
|
||||
const seasonsFromGeo = f.properties?.seasons ? Object.keys(f.properties.seasons).sort() : [];
|
||||
const withData = [];
|
||||
for (const s of seasonsFromGeo) {
|
||||
if (await probeSummary(sn, s)) withData.push(s);
|
||||
}
|
||||
if (withData.length) availableSiteSeasons[sn] = withData;
|
||||
}
|
||||
const availableSites = Object.keys(availableSiteSeasons);
|
||||
const siteSelect = document.getElementById("siteSelect");
|
||||
siteSelect.innerHTML = "";
|
||||
(availableSites.length ? availableSites.sort() : ["innsbruck"]).forEach((sn) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = sn;
|
||||
opt.textContent = sn;
|
||||
siteSelect.appendChild(opt);
|
||||
if (!availableSiteSeasons[sn]) availableSiteSeasons[sn] = ["2024"];
|
||||
});
|
||||
const urlSite = urlParams.get("site");
|
||||
const urlSeason = urlParams.get("season");
|
||||
const initialSite = urlSite && availableSiteSeasons[urlSite] ? urlSite : availableSites[0] || "innsbruck";
|
||||
const initialSeason =
|
||||
urlSeason && (availableSiteSeasons[initialSite] || []).includes(urlSeason)
|
||||
? urlSeason
|
||||
: (availableSiteSeasons[initialSite] || [])[0] || "2024";
|
||||
siteSelect.value = initialSite;
|
||||
document.getElementById("seasonSelect").innerHTML = (availableSiteSeasons[initialSite] || [])
|
||||
.map((s) => `<option value="${s}">${s}</option>`)
|
||||
.join("");
|
||||
document.getElementById("seasonSelect").value = initialSeason;
|
||||
siteName = initialSite;
|
||||
season = initialSeason;
|
||||
|
||||
siteSelect.addEventListener("change", function () {
|
||||
const sn = this.value;
|
||||
const seas = availableSiteSeasons[sn] || [];
|
||||
document.getElementById("seasonSelect").innerHTML = seas.map((s) => `<option value="${s}">${s}</option>`).join("");
|
||||
document.getElementById("seasonSelect").value = seas[0] || "2024";
|
||||
siteName = sn;
|
||||
season = document.getElementById("seasonSelect").value;
|
||||
load();
|
||||
});
|
||||
document.getElementById("seasonSelect").addEventListener("change", function () {
|
||||
season = this.value;
|
||||
load();
|
||||
});
|
||||
await load();
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue