#!/usr/bin/env python3
"""Build a self-contained, print-ready visual Gate 3 evidence report."""

from __future__ import annotations

import base64
import hashlib
import html
import io
import json
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yaml

GREEN = "#167d5a"
LIME = "#8dc63f"
NAVY = "#102a43"
ORANGE = "#f59e0b"
RED = "#d64545"
SLATE = "#526575"


def image_uri(fig) -> str:
    buffer = io.BytesIO()
    fig.savefig(buffer, format="png", dpi=180, bbox_inches="tight", facecolor="white")
    plt.close(fig)
    return "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode()


def file_image_uri(path: Path) -> str:
    mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg"
    return f"data:{mime};base64," + base64.b64encode(path.read_bytes()).decode()


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            h.update(block)
    return h.hexdigest()


def table(df: pd.DataFrame, classes: str = "") -> str:
    return df.to_html(index=False, border=0, classes=f"data-table {classes}", escape=True)


def code_block(value: object, label: str = "View code / output") -> str:
    if not isinstance(value, str):
        value = json.dumps(value, indent=2)
    return (
        '<details class="code-disclosure">'
        f"<summary>{html.escape(label)}</summary>"
        f'<div class="code-shell"><button class="copy-code" type="button">Copy</button>'
        f"<pre><code>{html.escape(value)}</code></pre></div></details>"
    )


def hash_box(value: str) -> str:
    escaped = html.escape(value)
    return f'<code class="hash-box" title="{escaped}">{escaped}</code>'


def linked_file_table(rows: list[dict[str, str]]) -> str:
    body = []
    for row in rows:
        label = html.escape(row["label"])
        href = html.escape(row["href"], quote=True)
        size = html.escape(row.get("size", ""))
        digest = html.escape(row.get("sha256", ""))
        body.append(
            "<tr>"
            f'<td><a class="file-link" href="{href}" target="_blank" rel="noopener">{label}</a></td>'
            f"<td>{size}</td>"
            f'<td><code class="hash" title="{digest}">{digest}</code></td>'
            f'<td class="open-cell"><a class="open-link" href="{href}" target="_blank" rel="noopener">Open ↗</a></td>'
            "</tr>"
        )
    return (
        '<div class="table-scroll"><table class="data-table file-table">'
        "<thead><tr><th>Artifact</th><th>Size</th><th>SHA-256</th><th>Action</th></tr></thead>"
        f"<tbody>{''.join(body)}</tbody></table></div>"
    )


def build(workspace: Path, run_id: str, output: Path) -> None:
    run = workspace / "06_Experiments/GATE_3/runs" / run_id
    processed = workspace / "03_Datasets/05_Processed_Final/DS03/gfis_ds03_canonical_v1.csv"
    processing = json.loads((workspace / "03_Datasets/05_Processed_Final/DS03/processing_manifest_v1.json").read_text())
    run_manifest = json.loads((run / "run_manifest.json").read_text())
    split = json.loads((run / "split_manifest.json").read_text())
    decision = json.loads((run / "scientific_decision.json").read_text())
    drift = json.loads((run / "temperature_drift.json").read_text())
    config = yaml.safe_load((run / "config.yaml").read_text())
    environment = json.loads((run / "environment.json").read_text())
    canonical = pd.read_csv(processed, parse_dates=["timestamp"])
    rolling = pd.read_csv(run / "rolling_summary.csv")
    folds = pd.read_csv(run / "fold_metrics.csv")
    final = pd.read_csv(run / "final_test_metrics.csv")
    epochs = pd.read_csv(run / "logs/epoch_history.csv")
    final_predictions = pd.read_csv(run / "predictions/final_test_predictions.csv", parse_dates=["timestamp"])
    simulator_run_id = "V2-SIM-20260724T2220Z"
    simulator_run = workspace / "06_Experiments/V2_SIMULATOR_EVIDENCE/runs" / simulator_run_id
    simulator_metadata = json.loads((simulator_run / "run_metadata.json").read_text())
    simulator_manifest = json.loads((simulator_run / "artifact_manifest.json").read_text())
    simulator_summary = pd.read_csv(simulator_run / "scenario_summary.csv")
    simulator_replay_plot = file_image_uri(simulator_run / "plots/simulator_capability_replay.png")
    screenshot_root = (
        workspace
        / "01_Product_Source/GFIS_Unified_Portal/level-2/library/"
        "current-midsem-2026-07-09/screenshots"
    )
    control_room_screenshot = file_image_uri(screenshot_root / "08_control_room_memory_export_panel.png")
    scenario_handoff_screenshot = file_image_uri(screenshot_root / "07_local_direct_scenario_loaded.png")
    simulator_memory_screenshot = file_image_uri(screenshot_root / "09_industrial_simulator_memory_panel.png")

    # 1. Data coverage and temperature.
    fig, axes = plt.subplots(2, 1, figsize=(11, 5.7), sharex=True, gridspec_kw={"height_ratios": [1, 2]})
    for i, (reactor, part) in enumerate(canonical.groupby("reactor_id")):
        axes[0].scatter(part["timestamp"], np.full(len(part), i), s=4, label=reactor)
    axes[0].set_yticks(range(4), ["R1", "R2", "R3", "R4"])
    axes[0].set_title("Reactor observation coverage")
    for reactor, part in canonical.groupby("reactor_id"):
        axes[1].plot(part["timestamp"], part["air_temperature_c"], lw=0.8, alpha=0.75, label=reactor)
    axes[1].set_ylabel("Air temperature (°C)")
    axes[1].set_title("Naturally observed process-sheet temperature")
    axes[1].xaxis.set_major_locator(mdates.MonthLocator(interval=2))
    axes[1].xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))
    axes[1].legend(ncol=4, frameon=False)
    fig.tight_layout()
    coverage_plot = image_uri(fig)

    # 2. Missingness.
    missing = pd.Series(processing["quality"]["missing_fraction"]).mul(100).sort_values()
    fig, ax = plt.subplots(figsize=(8.5, 4))
    colors = [GREEN if x < 5 else ORANGE if x < 50 else RED for x in missing]
    ax.barh([x.replace("_", " ") for x in missing.index], missing.values, color=colors)
    ax.set_xlabel("Missing observations (%)")
    ax.set_title("Critical field availability")
    for i, value in enumerate(missing.values):
        ax.text(value + 1, i, f"{value:.1f}%", va="center", fontsize=9)
    ax.set_xlim(0, 108)
    fig.tight_layout()
    missing_plot = image_uri(fig)

    # 3. Fold robustness (log scale because fold 2 is extreme).
    fold_summary = folds.groupby(["model", "window", "fold"], as_index=False)["rmse"].mean()
    best_by_model = {"persistence": 3, "xgboost": 3, "lstm": 3}
    fig, ax = plt.subplots(figsize=(9.5, 4.5))
    palette = {"persistence": SLATE, "xgboost": ORANGE, "lstm": GREEN}
    for model, window in best_by_model.items():
        part = fold_summary[(fold_summary.model == model) & (fold_summary.window == window)]
        ax.plot(part.fold, part.rmse, marker="o", lw=2, label=model.title(), color=palette[model])
    ax.set_yscale("log")
    ax.set_xticks([1, 2, 3])
    ax.set_xlabel("Rolling-origin fold")
    ax.set_ylabel("RMSE (mL, log scale)")
    ax.set_title("Rolling folds reveal severe regime instability")
    ax.legend(frameon=False, ncol=3)
    ax.grid(True, which="both", alpha=0.18)
    fig.tight_layout()
    fold_plot = image_uri(fig)

    # 4. Sequence window study.
    fig, ax = plt.subplots(figsize=(9.5, 4.5))
    for model in ["persistence", "xgboost", "lstm"]:
        part = rolling[rolling.model == model]
        ax.errorbar(part.window, part.rmse_mean, yerr=part.rmse_std, marker="o",
                    capsize=4, label=model.title(), color=palette[model])
    ax.set_xlabel("Sequence window (observations)")
    ax.set_ylabel("Rolling RMSE (mL)")
    ax.set_title("Window effects are small relative to fold dispersion")
    ax.legend(frameon=False, ncol=3)
    ax.grid(True, alpha=0.18)
    fig.tight_layout()
    window_plot = image_uri(fig)

    # 5. Final period model comparison.
    final_summary = final.groupby("model", as_index=False).agg(
        mae=("mae", "mean"), rmse=("rmse", "mean"), r2=("r2", "mean"), smape=("smape", "mean")
    )
    fig, axes = plt.subplots(1, 2, figsize=(10.5, 4))
    labels = [x.title() for x in final_summary.model]
    colors = [palette[x] for x in final_summary.model]
    axes[0].bar(labels, final_summary.rmse, color=colors)
    axes[0].set_title("Locked final-period RMSE")
    axes[0].set_ylabel("mL")
    axes[1].bar(labels, final_summary.smape, color=colors)
    axes[1].set_title("Locked final-period sMAPE")
    axes[1].set_ylabel("%")
    for ax in axes:
        ax.grid(axis="y", alpha=0.18)
    fig.tight_layout()
    final_plot = image_uri(fig)

    # 6. Timestamped final predictions, seed-averaged.
    pred = final_predictions.groupby(["timestamp", "model"], as_index=False).agg(
        actual=("actual", "first"), prediction=("prediction", "mean")
    )
    fig, ax = plt.subplots(figsize=(11, 4.6))
    actual = pred.drop_duplicates("timestamp").sort_values("timestamp")
    ax.plot(actual.timestamp, actual.actual, color=NAVY, lw=1.8, label="Observed total biogas")
    for model in ["persistence", "xgboost", "lstm"]:
        part = pred[pred.model == model].sort_values("timestamp")
        ax.plot(part.timestamp, part.prediction, lw=1, alpha=0.85,
                color=palette[model], label=model.title())
    ax.set_yscale("symlog", linthresh=1000)
    ax.set_ylabel("Interval total biogas (mL, symlog)")
    ax.set_title("Final-period observations and predictions")
    ax.legend(frameon=False, ncol=4)
    ax.grid(True, alpha=0.16)
    fig.tight_layout()
    prediction_plot = image_uri(fig)

    # 7. Epoch traces.
    fig, ax = plt.subplots(figsize=(10, 4.3))
    subset = epochs[(epochs.model == "lstm") & (epochs.window == 3)]
    for (fold, seed), part in subset.groupby(["fold", "seed"]):
        ax.plot(part.epoch, part.validation_loss, alpha=0.5, lw=1,
                label=f"fold {fold}, seed {seed}")
    ax.set_yscale("log")
    ax.set_xlabel("Epoch")
    ax.set_ylabel("Validation loss (scaled MSE)")
    ax.set_title("Current LSTM training histories")
    ax.grid(True, alpha=0.16)
    ax.legend(ncol=3, fontsize=7, frameon=False)
    fig.tight_layout()
    epoch_plot = image_uri(fig)

    # 8. Natural drift.
    drift_df = pd.DataFrame(drift)
    fig, ax = plt.subplots(figsize=(8.5, 3.8))
    bars = ax.bar(drift_df.fold.astype(str), drift_df.rmse_degradation_percent,
                  color=[RED if x > 0 else GREEN for x in drift_df.rmse_degradation_percent])
    ax.axhline(0, color=NAVY, lw=0.8)
    ax.set_xlabel("Rolling fold")
    ax.set_ylabel("RMSE degradation (%)")
    ax.set_title("Natural temperature-regime degradation is inconsistent")
    for bar, value in zip(bars, drift_df.rmse_degradation_percent):
        ax.text(bar.get_x() + bar.get_width()/2, value + (1 if value >= 0 else -3),
                f"{value:+.1f}%", ha="center", va="bottom" if value >= 0 else "top")
    fig.tight_layout()
    drift_plot = image_uri(fig)

    reactor_quality = pd.DataFrame(processing["quality"]["by_reactor"]).T.reset_index(names="reactor")
    reactor_quality = reactor_quality[[
        "reactor", "rows", "start", "end", "duplicate_timestamps",
        "non_daily_intervals", "temperature_missing", "methane_fraction_observations", "date_repairs"
    ]]
    display_final = final_summary.copy()
    display_final.columns = ["Model", "MAE mean", "RMSE mean", "R² mean", "sMAPE mean"]
    for c in display_final.columns[1:]:
        display_final[c] = display_final[c].map(lambda x: f"{x:,.3f}")
    window_table = rolling[["model", "window", "rmse_mean", "rmse_std", "mae_mean", "r2_mean"]].copy()
    window_table.columns = ["Model", "Window", "RMSE mean", "RMSE SD", "MAE mean", "R² mean"]
    for c in window_table.columns[2:]:
        window_table[c] = window_table[c].map(lambda x: f"{x:,.3f}")

    artifact_rows = []
    for relative, digest in run_manifest["artifacts"].items():
        path = run / relative
        artifact_rows.append({
            "label": relative,
            "href": f"../../06_Experiments/GATE_3/runs/{run_id}/{relative}",
            "size": f"{path.stat().st_size:,} B" if path.exists() else "missing",
            "sha256": digest,
        })
    source_rows = [
        {
            "label": "Raw Mendeley archive",
            "href": "../../03_Datasets/01_Raw_Public_Datasets/DS03_Mendeley_gk3f363sfg_v1/original/Farm-scale_Biodigester_Performance_dataset.zip",
            "size": "12,005,187 B",
            "sha256": run_manifest["dataset_archive_sha256"],
        },
        {
            "label": "Canonical processed CSV",
            "href": "../../03_Datasets/05_Processed_Final/DS03/gfis_ds03_canonical_v1.csv",
            "size": f"{processed.stat().st_size:,} B",
            "sha256": run_manifest["processed_data_sha256"],
        },
        {
            "label": "Processing manifest",
            "href": "../../03_Datasets/05_Processed_Final/DS03/processing_manifest_v1.json",
            "size": f"{(workspace / '03_Datasets/05_Processed_Final/DS03/processing_manifest_v1.json').stat().st_size:,} B",
            "sha256": sha256(workspace / "03_Datasets/05_Processed_Final/DS03/processing_manifest_v1.json"),
        },
    ]
    code_rows = []
    for label, relative in [
        ("Canonical data pipeline", "01_Product_Source/GFIS_Project/gfis/gate3/data_pipeline.py"),
        ("Leakage-safe experiment engine", "01_Product_Source/GFIS_Project/gfis/gate3/experiment.py"),
        ("Dataset preparation command", "01_Product_Source/GFIS_Project/scripts/gate3_prepare_dataset.py"),
        ("Training command", "01_Product_Source/GFIS_Project/scripts/gate3_run_experiment.py"),
        ("Integrated report generator", "01_Product_Source/GFIS_Project/scripts/build_gate3_integrated_report.py"),
        ("Experiment configuration", "06_Experiments/GATE_3/configs/gate3_ds03_v1.yaml"),
    ]:
        path = workspace / relative
        code_rows.append({
            "label": label,
            "href": f"../../{relative}",
            "size": f"{path.stat().st_size:,} B",
            "sha256": sha256(path),
        })
    simulator_rows = []
    for label, relative in [
        (
            "V2 simulator run metadata",
            f"06_Experiments/V2_SIMULATOR_EVIDENCE/runs/{simulator_run_id}/run_metadata.json",
        ),
        (
            "Nine-scenario summary",
            f"06_Experiments/V2_SIMULATOR_EVIDENCE/runs/{simulator_run_id}/scenario_summary.csv",
        ),
        (
            "Complete 432-row 48-hour traces",
            f"06_Experiments/V2_SIMULATOR_EVIDENCE/runs/{simulator_run_id}/simulator_48h_traces.csv",
        ),
        (
            "Simulator artifact manifest",
            f"06_Experiments/V2_SIMULATOR_EVIDENCE/runs/{simulator_run_id}/artifact_manifest.json",
        ),
        (
            "Replay evidence generator",
            "01_Product_Source/GFIS_Project/scripts/generate_v2_simulator_evidence.py",
        ),
    ]:
        path = workspace / relative
        simulator_rows.append(
            {
                "label": label,
                "href": f"../../{relative}",
                "size": f"{path.stat().st_size:,} B",
                "sha256": sha256(path),
            }
        )
    simulator_display = simulator_summary[
        [
            "scenario",
            "hours",
            "mean_methane_yield",
            "max_vfa_alk_ratio",
            "warning_or_critical_hours",
            "physics_violation_hours",
        ]
    ].copy()
    simulator_display.columns = [
        "Scenario",
        "Hours",
        "Mean methane yield",
        "Max VFA/ALK",
        "Warning/Critical hours",
        "Physics violation hours",
    ]

    css = """
    :root{--green:#167d5a;--lime:#8dc63f;--navy:#102a43;--ink:#263746;--muted:#66788a;--paper:#fff;--soft:#eff7f3;--line:#d9e3e8;--amber:#f59e0b;--red:#d64545}
    *{box-sizing:border-box} html{scroll-behavior:smooth}
    body{margin:0;background:#e9f0f2;color:var(--ink);font:15px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
    .report{width:min(1180px,calc(100% - 32px));margin:28px auto;background:var(--paper);box-shadow:0 18px 60px #17324a24;overflow:clip}
    .hero{padding:58px 64px 46px;background:linear-gradient(130deg,#0c3e35 0%,#12654d 55%,#1e8a63 100%);color:white;position:relative;overflow:hidden}
    .hero:after{content:"";position:absolute;width:440px;height:440px;border:80px solid #ffffff12;border-radius:50%;right:-180px;top:-210px}
    .eyebrow{font-weight:750;letter-spacing:.14em;text-transform:uppercase;color:#c6f6df;font-size:12px}
    h1{font-size:46px;line-height:1.05;margin:14px 0 18px;max-width:820px} .subtitle{font-size:19px;max-width:800px;color:#e3f8ed}
    .hero-meta{display:flex;gap:24px;flex-wrap:wrap;margin-top:30px;font-size:13px}.hero-meta span{padding:7px 12px;border:1px solid #ffffff38;border-radius:999px}
    nav{position:sticky;top:0;z-index:3;background:#fffef8f2;backdrop-filter:blur(12px);padding:12px 28px;border-bottom:1px solid var(--line);display:flex;gap:8px;overflow:auto}
    nav a{white-space:nowrap;color:var(--navy);text-decoration:none;padding:7px 10px;border-radius:7px;font-size:12px;font-weight:700}nav a:hover{background:var(--soft)}
    main{padding:18px 64px 60px;counter-reset:chapter;min-width:0} section{padding:34px 0;border-bottom:1px solid var(--line);scroll-margin-top:60px;min-width:0;counter-increment:chapter}
    h2{font-size:29px;color:var(--navy);margin:0 0 8px;display:flex;gap:12px;align-items:baseline}h2:before{content:counter(chapter,decimal-leading-zero);font-size:12px;letter-spacing:.08em;color:var(--green);background:var(--soft);padding:4px 7px;border-radius:6px}h3{font-size:19px;color:var(--navy);margin:24px 0 10px}.lead{font-size:17px;color:var(--muted);max-width:920px}
    .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:24px 0}.kpi{padding:18px;border-radius:14px;background:linear-gradient(160deg,#f8fcfa,#eef8f3);border:1px solid #cfe5da}.kpi b{display:block;font-size:28px;color:var(--green)}.kpi span{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
    .grid-2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:22px}.grid-2>*{min-width:0}.card{border:1px solid var(--line);border-radius:14px;padding:20px;background:#fff;min-width:0;overflow:hidden}.card.accent{border-left:5px solid var(--green)}.card.warn{border-left:5px solid var(--amber);background:#fffbeb}.card.stop{border-left:5px solid var(--red);background:#fff6f6}
    .verdict{font-size:23px;font-weight:800;color:var(--navy)}.badge{display:inline-block;padding:5px 9px;border-radius:999px;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em}.pass{background:#daf5e7;color:#116246}.caution{background:#fff0c7;color:#8a5a00}.fail{background:#ffe0e0;color:#a52626}
    .pipeline{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:9px;margin:25px 0}.step{padding:15px 10px;text-align:center;border-radius:10px;background:var(--navy);color:#fff;font-weight:700;position:relative;min-width:0;overflow-wrap:anywhere}.step:not(:last-child):after{content:"›";position:absolute;right:-9px;color:var(--lime);font-size:24px;top:8px;z-index:2}
    .figure{position:relative;margin:20px 0;padding:14px;border:1px solid var(--line);border-radius:14px;background:white;min-width:0;overflow:hidden}.figure:after{content:"Click to zoom";position:absolute;right:22px;top:22px;padding:5px 8px;border-radius:999px;background:#102a43dd;color:#fff;font-size:10px;font-weight:800;pointer-events:none}.figure img{width:100%;max-width:100%;height:auto;display:block;cursor:zoom-in;border-radius:8px;transition:transform .18s ease,box-shadow .18s ease}.figure img:hover,.figure img:focus{transform:scale(1.008);box-shadow:0 8px 24px #17324a22;outline:3px solid #8dc63f66}.caption{font-size:12px;color:var(--muted);margin:8px 4px 0}
    .data-table{width:100%;border-collapse:collapse;font-size:12px;margin:14px 0}.data-table th{background:var(--navy);color:white;text-align:left;padding:9px}.data-table td{padding:8px;border-bottom:1px solid var(--line);vertical-align:top}.data-table tr:nth-child(even){background:#f7fafb}.data-table td:last-child{overflow-wrap:anywhere}
    .table-scroll{width:100%;overflow-x:auto}.file-table{table-layout:fixed}.file-table th:nth-child(1){width:36%}.file-table th:nth-child(2){width:12%}.file-table th:nth-child(3){width:40%}.file-table th:nth-child(4){width:12%}
    .file-link{font-weight:750;color:var(--green);text-decoration:none;overflow-wrap:anywhere}.file-link:hover{text-decoration:underline}.open-link{display:inline-block;white-space:nowrap;padding:5px 8px;border-radius:6px;background:var(--soft);color:var(--navy);font-weight:800;text-decoration:none}.hash{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px}.open-cell{text-align:center}
    pre{max-width:100%;margin:0;background:#0d2537;color:#d7f5e7;border-radius:0 0 10px 10px;padding:16px;overflow:auto;font-size:11px;line-height:1.5;white-space:pre;tab-size:2}code{font-family:"SFMono-Regular",Consolas,monospace}
    details{max-width:100%;border:1px solid var(--line);border-radius:10px;padding:10px 14px;margin:10px 0;overflow:hidden}summary{cursor:pointer;font-weight:750;color:var(--navy)}.code-disclosure{padding:0}.code-disclosure summary{padding:11px 14px;background:#f6faf8}.code-shell{position:relative;min-width:0}.copy-code{position:absolute;right:9px;top:8px;z-index:1;border:1px solid #ffffff44;background:#173f55;color:#fff;border-radius:6px;padding:5px 9px;font-size:11px;cursor:pointer}.copy-code:hover{background:var(--green)}
    .hash-box{display:block;max-width:100%;padding:10px 12px;background:#eff7f3;color:#154a3c;border:1px solid #cfe5da;border-radius:8px;overflow-wrap:anywhere;font-size:11px}
    .equation{max-width:100%;margin:12px 0;padding:14px 18px;border-left:4px solid var(--lime);background:#f6faf8;color:var(--navy);font:15px/1.65 "STIX Two Text","Times New Roman",serif;overflow-x:auto;overflow-y:hidden;overflow-wrap:normal}
    .where{font-size:12px;color:var(--muted);margin:-6px 0 16px 20px}.eq-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}.eq-grid>*{min-width:0}
    .term{border-bottom:1px dotted var(--green);cursor:help;font-weight:700;color:var(--navy)}
    [data-tip]{position:relative}[data-tip]:hover:after,[data-tip]:focus:after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 9px);transform:translateX(-50%);width:min(280px,75vw);padding:9px 11px;border-radius:8px;background:#0d2537;color:#fff;font-size:11px;font-weight:500;line-height:1.4;box-shadow:0 8px 24px #0004;z-index:20;pointer-events:none;text-transform:none;letter-spacing:normal}[data-tip]:hover:before,[data-tip]:focus:before{content:"";position:absolute;left:50%;bottom:calc(100% + 3px);border:6px solid transparent;border-top-color:#0d2537;transform:translateX(-50%);z-index:21}
    .glossary .term:nth-child(4n+1):after,.kpis .kpi:first-child:after{left:0;transform:none}.glossary .term:nth-child(4n):after,.kpis .kpi:last-child:after{left:auto;right:0;transform:none}
    .glossary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:18px 0}.glossary .term{padding:10px;border:1px solid var(--line);border-radius:9px;background:#fbfdfc;text-align:center}
    .lightbox{position:fixed;inset:0;background:#07131deF;z-index:100;display:none;align-items:center;justify-content:center;padding:28px}.lightbox.open{display:flex}.lightbox-inner{width:min(1500px,96vw);height:min(94vh,1000px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:10px}.lightbox-bar{display:flex;justify-content:space-between;align-items:center;color:#fff}.lightbox-title{font-weight:750}.lightbox-close{border:1px solid #ffffff55;background:#ffffff15;color:white;border-radius:999px;width:38px;height:38px;font-size:21px;cursor:pointer}.lightbox-stage{min-height:0;overflow:auto;display:flex;align-items:flex-start;justify-content:center;background:#fff;border-radius:12px;padding:12px}.lightbox-stage img{width:auto;max-width:none;height:auto;min-width:100%;cursor:zoom-out}.lightbox-caption{color:#d8e7ef;font-size:12px}
    .impact-map{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin:20px 0}.impact-map .card{height:100%}.impact-arrow{color:var(--green);font-weight:900}.before-after{display:grid;grid-template-columns:minmax(0,1fr) 44px minmax(0,1fr);gap:12px;align-items:stretch}.before-after .bridge{display:grid;place-items:center;color:var(--green);font-size:30px;font-weight:900}
    .footer{padding:34px 64px 26px;background:linear-gradient(135deg,#0b2235,#103d43);color:#dce8ef;font-size:12px}.footer-grid{display:grid;grid-template-columns:minmax(260px,1.5fr) minmax(170px,.7fr) minmax(210px,1fr);gap:30px;align-items:start}.brand-lockup{display:flex;gap:14px;align-items:center;margin-bottom:14px}.brand-mark{display:grid;place-items:center;flex:0 0 48px;height:48px;border:1px solid #8dc63f88;border-radius:12px;background:#ffffff0d;color:#b8ee76;font-size:18px;font-weight:900;letter-spacing:-.05em}.brand-name{font-size:20px;font-weight:850;color:#fff}.brand-sub{color:#a9c5cf}.footer h3{margin:2px 0 10px;color:#b8ee76;font-size:12px;text-transform:uppercase;letter-spacing:.08em}.footer-links{display:grid;gap:7px}.footer a{color:#e3f8ed;text-decoration:none;overflow-wrap:anywhere}.footer a:hover,.footer a:focus{color:#b8ee76;text-decoration:underline}.legal{margin-top:22px;padding-top:16px;border-top:1px solid #ffffff20;color:#b9cbd3;display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap}.legal strong{color:#fff}.legal-note{max-width:660px}.print-btn{position:fixed;right:22px;bottom:22px;border:0;border-radius:999px;padding:13px 18px;background:var(--green);color:white;font-weight:800;box-shadow:0 8px 24px #0003;cursor:pointer}
    @page{size:A4;margin:12mm} @media print{body{background:#fff;font-size:10.5px}.report{width:auto;margin:0;max-width:none;box-shadow:none;overflow:visible}.hero{padding:36px 42px}.hero h1{font-size:32px}nav,.print-btn,.lightbox,.copy-code{display:none!important}main{padding:10px 36px}.kpis{grid-template-columns:repeat(4,minmax(0,1fr))}section{padding:22px 0;break-inside:auto}.card,.figure,.data-table,tr,pre{break-inside:avoid}.grid-2{gap:12px}.figure:after{display:none}.figure img{max-height:145mm;object-fit:contain;transform:none!important;box-shadow:none!important}h2{font-size:22px}h3{font-size:15px}.footer{padding:18px 36px}.pipeline{font-size:9px}.file-table{table-layout:auto}.file-table th:nth-child(1){width:36%}.file-table th:nth-child(2){width:12%}.file-table th:nth-child(3){width:52%}.file-table th:nth-child(4),.file-table td:nth-child(4){display:none}.hash{white-space:normal;overflow-wrap:anywhere;font-size:7px}.table-scroll{overflow:visible}.code-disclosure{border:0}.code-disclosure summary{display:none}.code-disclosure .code-shell{display:block!important}.code-disclosure pre{display:block!important;white-space:pre-wrap;overflow-wrap:anywhere;font-size:8px}.glossary{grid-template-columns:repeat(4,1fr)}[data-tip]:after,[data-tip]:before{display:none!important}}
    @media(max-width:800px){.report{width:100%;margin:0}.hero,main,.footer{padding-left:24px;padding-right:24px}h1{font-size:34px}.kpis,.grid-2,.pipeline,.eq-grid,.impact-map,.footer-grid{grid-template-columns:1fr}.before-after{grid-template-columns:1fr}.before-after .bridge{transform:rotate(90deg);min-height:36px}.glossary{grid-template-columns:1fr 1fr}.pipeline .step:after{display:none}.file-table{min-width:760px}}
    """

    html_doc = f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>GFIS Gate 3 — Integrated Scientific Evidence Report · Version 2</title><style>{css}</style></head>
<body><div class="report">
<header class="hero"><div class="eyebrow">GFIS Final Version 2026 · M.Tech Dissertation Evidence · Report V2</div>
<h1>Gate 3 Integrated Scientific Report</h1>
<p class="subtitle">Public time-series integration, leakage-safe baselines, temporal-memory study, natural temperature-drift evaluation, and reproducible experiment memory.</p>
<div class="hero-meta"><span>Accepted run: {run_id}</span><span>Git: {run_manifest['git_commit'][:12]}</span><span>Generated: 24 July 2026</span><span>GFIS core target: methane yield</span><span>DS-03 test target: total biogas</span></div></header>
<nav><a href="#verdict">Verdict</a><a href="#evaluator-impact">Evaluator impact</a><a href="#provenance">Inputs</a><a href="#quality">Quality</a><a href="#process">Process</a><a href="#equations">Equations</a><a href="#evaluation">Evaluation</a><a href="#training">Training</a><a href="#drift">Drift</a><a href="#memory">Run memory</a><a href="#replay">Replay</a><a href="#simulator-proof">Simulator proof</a><a href="#limitations">Validation boundary</a></nav>
<main>
<section id="verdict"><span class="badge caution" tabindex="0" data-tip="Gate 3 is complete, but the evidence does not support promoting temporal learning as the production champion.">Gate 3 complete · temporal claim rejected</span><h2>Executive scientific verdict</h2>
<p class="lead"><b>GFIS remains a methane-yield prediction and decision-support system.</b> Gate 3 adds a reproducible public-data experiment chain, but DS-03 evaluates total-biogas dynamics rather than measured methane. On that dataset, the current <span class="term" tabindex="0" data-tip="Long Short-Term Memory: a recurrent neural network designed to retain information across ordered observations.">LSTM</span> is not promoted: <span class="term" tabindex="0" data-tip="Persistence predicts that the next value equals the most recently observed value.">persistence</span> won two of three <span class="term" tabindex="0" data-tip="Rolling-origin validation repeatedly trains on the past and validates on the next chronological block.">rolling-origin folds</span>. The LSTM remains a short-window challenger while methane-specific external validation continues separately.</p>
<div class="glossary"><span class="term" tabindex="0" data-tip="Mean absolute error: average absolute prediction error in the target unit.">MAE</span><span class="term" tabindex="0" data-tip="Root mean squared error: penalizes large errors more strongly than MAE.">RMSE</span><span class="term" tabindex="0" data-tip="Coefficient of determination. Negative values mean worse performance than predicting the evaluation-period mean.">R²</span><span class="term" tabindex="0" data-tip="Symmetric mean absolute percentage error. Lower is better; zero-denominator terms are omitted.">sMAPE</span><span class="term" tabindex="0" data-tip="A fold is one chronological train/validation evaluation block.">Fold</span><span class="term" tabindex="0" data-tip="A seed controls reproducible model initialization and stochastic components.">Seed</span><span class="term" tabindex="0" data-tip="The sequence window is the number of ordered observations presented to the LSTM.">Window</span><span class="term" tabindex="0" data-tip="Temperature drift means validation temperatures outside the training fold's 10th–90th percentile range.">Drift</span></div>
<div class="kpis"><div class="kpi" tabindex="0" data-tip="Canonical process rows retained across all four reactor sheets before model eligibility filtering."><b>1,167</b><span>Process records</span></div><div class="kpi" tabindex="0" data-tip="R1 and R2 are flexible digesters; R3 and R4 are fixed-dome digesters. Identity is preserved."><b>4</b><span>Distinct digesters</span></div><div class="kpi" tabindex="0" data-tip="Uninterrupted five-minute weather-station observations from April to October 2024."><b>51,766</b><span>Weather records</span></div><div class="kpi" tabindex="0" data-tip="Three chronological validation folds, three repeated seeds and four tested sequence windows."><b>3 × 3 × 4</b><span>Folds · seeds · windows</span></div></div>
<div class="grid-2"><div class="card accent"><div class="verdict">Champion: persistence</div><p>Fold wins: persistence 2, LSTM 1. Selection was locked before the final test was opened.</p></div>
<div class="card warn"><div class="verdict">No methane validation claim from DS-03</div><p>This boundary applies only to the evaluator-suggested public dataset: methane fraction is 99.66% missing, so its total-biogas target is not relabelled as methane. GFIS methane prediction remains implemented and is demonstrated separately in the simulator evidence section.</p></div></div></section>

<section id="evaluator-impact"><span class="badge pass" tabindex="0" data-tip="DS-03 was ranked first in the evaluator-suggested source catalogue because it offered public reactor time series plus measured weather for drift analysis.">Direct response to evaluator guidance</span><h2>Evaluator-suggested dataset: experimental impact</h2>
<p class="lead">Gate 3 selected the catalogue's rank-1 public source—<a class="file-link" href="https://data.mendeley.com/datasets/gk3f363sfg/1" target="_blank" rel="noopener">Mendeley DS-03, farm-scale digester process and weather data</a>—in direct response to the evaluation requirement for real/open evidence and temperature sensitivity. The evaluator guidance motivated the source category and experiment; it does not imply that the evaluator authored the dataset.</p>
<div class="impact-map">
<div class="card accent" tabindex="0" data-tip="The mid-semester response required GFIS to move beyond synthetic-only demonstrations."><h3>Guidance</h3><p>Use real/open anaerobic-digestion data, disclose limitations, test temperature sensitivity, compare LSTM with strong baselines, and keep claims traceable.</p></div>
<div class="card" tabindex="0" data-tip="Every model used identical chronological folds, with all preprocessing learned from training observations only."><h3>Gate 3 experiment</h3><p>Immutable DS-03 ingestion <span class="impact-arrow">→</span> quality audit <span class="impact-arrow">→</span> rolling-origin persistence/XGBoost/LSTM comparison <span class="impact-arrow">→</span> natural drift study <span class="impact-arrow">→</span> locked test.</p></div>
<div class="card warn" tabindex="0" data-tip="The strongest scientific result was a narrower, more defensible claim—not a larger headline score."><h3>Changed understanding</h3><p>Real farm-scale evidence did not justify promoting the LSTM. It exposed target and metadata limits, regime instability, and the need for a second licensed methane-rich dataset.</p></div>
</div>
<div class="before-after"><div class="card stop"><h3>Before DS-03</h3><ul><li>Synthetic evidence could demonstrate workflow, not field validity.</li><li>“Biogas” risked being discussed too loosely as methane.</li><li>A favourable aggregate score could hide unstable regimes.</li><li>Temperature robustness and experiment replay were not evidenced end to end.</li></ul></div><div class="bridge" aria-hidden="true">→</div><div class="card accent"><h3>After DS-03</h3><ul><li>Four reactor identities and 51,766 measured weather records are traceable.</li><li>The evaluated target is explicitly <b>total biogas</b>; methane is 99.66% missing.</li><li>Three chronological folds reveal persistence wins two folds and LSTM one.</li><li>Natural drift, repeated seeds, checksums, predictions, epochs and checkpoints are retained.</li></ul></div></div>
<h3>Experiment-to-learning traceability</h3>
<div class="table-scroll"><table class="data-table"><thead><tr><th>Experiment or audit</th><th>Evidence produced</th><th>What it changed in GFIS understanding</th></tr></thead><tbody>
<tr><td>Source and schema audit</td><td>Four farm-scale reactors; mixed Excel dates; duplicates, gaps and structural missingness documented.</td><td>Catalogue descriptions must be verified against files. GFIS now repairs dates transparently, flags exclusions and preserves reactor identity.</td></tr>
<tr><td>Target audit</td><td>Daily/interval total-biogas field usable; methane fraction 99.66% missing; VFA/ALK and required VS/BMP fields unavailable.</td><td>DS-03 cannot validate methane yield, VFA/ALK soft sensing or methane physics loss. Physics-violation rate is unavailable—not zero.</td></tr>
<tr><td>Leakage-safe rolling comparison</td><td>Persistence wins folds 1 and 3; LSTM wins fold 2; all models use identical chronological splits.</td><td>Temporal learning is regime-dependent. GFIS keeps persistence as the DS-03 champion and the compact LSTM only as a challenger.</td></tr>
<tr><td>Sequence windows 3/7/14/28</td><td>Window differences are small relative to fold-to-fold dispersion.</td><td>No evidence supports a larger model zoo or Transformer. Short memory is the defensible current choice.</td></tr>
<tr><td>Natural temperature drift</td><td>RMSE degradation is −21.2%, +29.3% and −21.2% across folds.</td><td>Temperature association is inconsistent; Gate 3 makes no causal temperature claim and labels observed versus synthetic stress separately.</td></tr>
<tr><td>Locked test and experiment memory</td><td>Checksummed data, split version, seeds, epochs, predictions, environment, checkpoints and replay artifacts.</td><td>A single favourable final score cannot override rolling evidence; every dissertation claim can now be replayed and audited.</td></tr>
</tbody></table></div>
<p><a class="open-link" href="../../04_Dissertation_Inputs/Evaluator_Suggestions/Final_Phase_Action_Items/GFIS_Final_Phase_Action_Plan_From_MidSem_Evaluation.md">Open evaluator action plan</a> <a class="open-link" href="../../04_Dissertation_Inputs/Evaluator_Suggestions/Response_Report/GFIS_Post_MidSem_Evaluation_Response_Report_2026-07-11.md">Open response report</a></p></section>

<section id="provenance"><h2>Input provenance and integrity</h2><p class="lead">The selected source is Mendeley Data V1, DOI 10.17632/gk3f363sfg.1, contributed by Daniel Mulat and licensed CC BY 4.0.</p>
<div class="grid-2"><div class="card"><h3>Immutable source package</h3><p><b>Title:</b> Process monitoring and weather datasets on farm-scale biogas digesters</p><p><b>Archive:</b> 12,005,187 bytes</p><p><b>Archive SHA-256</b></p>{hash_box(run_manifest['dataset_archive_sha256'])}</div>
<div class="card"><h3>Canonical output</h3><p><b>Schema:</b> gfis-gate3-ds03-v1</p><p><b>Processed SHA-256</b></p>{hash_box(run_manifest['processed_data_sha256'])}<p><b>Raw files are preserved; processing is additive.</b></p></div></div>
<h3>Open input and processing artifacts</h3>{linked_file_table(source_rows)}
<div class="figure"><img src="{coverage_plot}" alt="Dataset coverage and temperature"><p class="caption">Figure 1. Four reactor histories and naturally observed air-temperature trajectories. Reactor identity is retained throughout.</p></div>
{table(reactor_quality)}</section>

<section id="quality"><h2>Data quality and target audit</h2><div class="grid-2"><div class="figure"><img src="{missing_plot}" alt="Missingness chart"><p class="caption">Figure 2. Predictor and target-field missingness. Methane coverage is scientifically unusable.</p></div>
<div class="card warn"><h3>Acceptance rules</h3><ul><li>One-day intervals only for comparable daily forecasting.</li><li>Duplicate reactor timestamps excluded.</li><li>Targets are never imputed.</li><li>Predictor imputation and scaling are fitted within each training fold.</li><li>R1–R4 identity is retained with group-aware sequence creation.</li><li>Mixed Excel dates are repaired monotonically and flagged.</li></ul></div></div>
{code_block(processing['quality'], "View complete processing-manifest quality payload")}</section>

<section id="process"><h2>Integrated processing and evaluation flow</h2><div class="pipeline"><div class="step" tabindex="0" data-tip="The downloaded ZIP is never modified; its publisher and local SHA-256 values match.">Immutable Mendeley archive</div><div class="step" tabindex="0" data-tip="Four reactor sheets and five-minute weather are normalized into one traceable daily schema.">Reactor + weather canonicalisation</div><div class="step" tabindex="0" data-tip="Lag, rolling and sequence features use only observations available at or before prediction time.">Past-only features and windows</div><div class="step" tabindex="0" data-tip="Persistence, XGBoost and LSTM are compared on the same chronological folds.">Rolling-origin model comparison</div><div class="step" tabindex="0" data-tip="The final 15% is opened after selection; every artifact is stored with checksums and replay instructions.">Locked test + evidence memory</div></div>
<div class="grid-2"><div class="card"><h3>Chronological split</h3><p><b>Split version:</b> {split['version']}</p><p><b>Final test:</b> {split['final_test_start']} to {split['final_test_end']}</p><p>The last 15% of dates stayed sealed during architecture selection.</p>{table(pd.DataFrame(split['rolling_folds']))}</div>
<div class="card"><h3>Past-only features</h3><ul><li>Prior total-biogas value</li><li>Past 3-observation mean and standard deviation</li><li>Feed and water values</li><li>Observed air temperature</li><li>Calendar sine/cosine</li><li>Explicit reactor identity</li></ul><p>No future target or future-fitted preprocessing enters a fold.</p></div></div></section>

<section id="equations"><h2>Equations, derivations, and statistical definitions</h2>
<p class="lead">These equations define exactly what the Gate 3 code computes. Symbols are indexed by reactor <i>r</i>, observation time <i>t</i>, rolling fold <i>k</i>, and random seed <i>s</i>.</p>

<h3>1. Target construction and eligibility</h3>
<div class="equation">y<sub>r,t</sub> = B<sub>r,t</sub> − B<sub>r,t−1</sub></div>
<p class="where">B is the cumulative biogas-meter reading. The source workbook supplies this difference as “Daily Biogas (mL).” Gate 3 interprets it as interval total biogas and accepts it as daily only when Δt = 1 day.</p>
<div class="equation">𝓔<sub>r,t</sub> = 𝟙[Δt = 1] · 𝟙[not duplicate] · 𝟙[y<sub>r,t</sub> observed]</div>
<p class="where">Only observations with 𝓔 = 1 enter model evaluation. Missing targets are never imputed.</p>

<h3>2. Past-only temporal features</h3>
<div class="eq-grid"><div><div class="equation">Lag1<sub>r,t</sub> = y<sub>r,t−1</sub></div>
<p class="where">The most recently observed total-biogas interval.</p></div>
<div><div class="equation">μ̄<sup>(3)</sup><sub>r,t</sub> = (1/3) Σ<sub>j=1</sub><sup>3</sup> y<sub>r,t−j</sub></div>
<p class="where">Three-observation past-only rolling mean.</p></div></div>
<div class="equation">s<sup>(3)</sup><sub>r,t</sub> = √[(1/(3−1)) Σ<sub>j=1</sub><sup>3</sup>(y<sub>r,t−j</sub> − μ̄<sup>(3)</sup><sub>r,t</sub>)²]</div>
<p class="where">Sample rolling standard deviation. The current or future target never enters these features.</p>
<div class="equation">d<sub>sin,t</sub> = sin(2π·DOY<sub>t</sub>/366), &nbsp; d<sub>cos,t</sub> = cos(2π·DOY<sub>t</sub>/366)</div>
<p class="where">Cyclic calendar encoding prevents an artificial discontinuity between 31 December and 1 January.</p>

<h3>3. Fold-local preprocessing derivation</h3>
<div class="equation">m<sub>k,j</sub> = median&#123;x<sub>i,j</sub> : i ∈ Train<sub>k</sub>&#125;</div>
<div class="equation">x̃<sub>i,j</sub> = x<sub>i,j</sub> if observed; otherwise m<sub>k,j</sub></div>
<div class="equation">z<sub>i,j</sub> = (x̃<sub>i,j</sub> − μ<sub>k,j</sub>) / σ<sub>k,j</sub></div>
<p class="where">The imputation median m, mean μ, and standard deviation σ are estimated only from the current training fold. Validation and test observations are transformed but never used to fit preprocessing.</p>

<h3>4. Persistence baseline</h3>
<div class="equation">ŷ<sup>persist</sup><sub>r,t</sub> = y<sub>r,t−1</sub></div>
<p class="where">A temporal model must outperform this operationally meaningful baseline on leakage-safe validation before an improvement claim is accepted.</p>

<h3>5. XGBoost objective</h3>
<div class="equation">ŷ<sub>i</sub> = Σ<sub>m=1</sub><sup>M</sup> f<sub>m</sub>(x<sub>i</sub>), &nbsp; f<sub>m</sub> ∈ 𝓕</div>
<div class="equation">𝓛<sup>(m)</sup> = Σ<sub>i</sub>(y<sub>i</sub> − ŷ<sup>(m−1)</sup><sub>i</sub> − f<sub>m</sub>(x<sub>i</sub>))² + Ω(f<sub>m</sub>)</div>
<p class="where">Each tree fits residual structure from the current ensemble; Ω regularizes tree complexity. All lag and rolling predictors remain past-only.</p>

<h3>6. Current LSTM state derivation</h3>
<div class="equation">
i<sub>t</sub> = σ(W<sub>xi</sub>x<sub>t</sub> + W<sub>hi</sub>h<sub>t−1</sub> + b<sub>i</sub>)<br>
f<sub>t</sub> = σ(W<sub>xf</sub>x<sub>t</sub> + W<sub>hf</sub>h<sub>t−1</sub> + b<sub>f</sub>)<br>
g<sub>t</sub> = tanh(W<sub>xg</sub>x<sub>t</sub> + W<sub>hg</sub>h<sub>t−1</sub> + b<sub>g</sub>)<br>
o<sub>t</sub> = σ(W<sub>xo</sub>x<sub>t</sub> + W<sub>ho</sub>h<sub>t−1</sub> + b<sub>o</sub>)<br>
c<sub>t</sub> = f<sub>t</sub> ⊙ c<sub>t−1</sub> + i<sub>t</sub> ⊙ g<sub>t</sub><br>
h<sub>t</sub> = o<sub>t</sub> ⊙ tanh(c<sub>t</sub>)<br>
ŷ<sub>t</sub> = w<sub>y</sub><sup>T</sup>h<sub>t</sub> + b<sub>y</sub>
</div>
<p class="where">σ is the logistic sigmoid and ⊙ is element-wise multiplication. Gate 3 uses one layer, hidden size 32, no random shuffling, and windows of 3, 7, 14, and 28 observations.</p>
<div class="equation">MSE<sub>train</sub> = (1/N) Σ<sub>i=1</sub><sup>N</sup>(z(y<sub>i</sub>) − z(ŷ<sub>i</sub>))²</div>
<p class="where">Targets are standardized from training data only. Adam minimizes scaled MSE; early stopping retains the state with lowest validation MSE.</p>

<h3>7. Reported error metrics</h3>
<div class="eq-grid"><div><div class="equation">MAE = (1/N) Σ |y<sub>i</sub> − ŷ<sub>i</sub>|</div></div>
<div><div class="equation">RMSE = √[(1/N) Σ (y<sub>i</sub> − ŷ<sub>i</sub>)²]</div></div>
<div><div class="equation">R² = 1 − [Σ(y<sub>i</sub> − ŷ<sub>i</sub>)² / Σ(y<sub>i</sub> − ȳ)²]</div></div>
<div><div class="equation">sMAPE = (100/N) Σ [2|ŷ<sub>i</sub> − y<sub>i</sub>| / (|y<sub>i</sub>| + |ŷ<sub>i</sub>|)]</div></div></div>
<p class="where">sMAPE terms with a zero denominator are excluded. Negative R² means the model is worse than predicting the evaluation-period mean.</p>

<h3>8. Repeated-seed aggregation and dispersion</h3>
<div class="equation">M̄<sub>k,w,m</sub> = (1/S) Σ<sub>s=1</sub><sup>S</sup> M<sub>k,w,m,s</sub></div>
<div class="equation">SD(M) = √[(1/(n−1)) Σ<sub>q=1</sub><sup>n</sup>(M<sub>q</sub> − M̄)²]</div>
<p class="where">S = 3 seeds (17, 29, 43). The report presents mean and sample standard deviation across fold/seed observations.</p>

<h3>9. Natural temperature-drift definition</h3>
<div class="equation">Drift<sub>k,t</sub> = 𝟙[T<sub>t</sub> &lt; Q<sub>0.10</sub>(T<sub>Train,k</sub>) ∨ T<sub>t</sub> &gt; Q<sub>0.90</sub>(T<sub>Train,k</sub>)]</div>
<div class="equation">Degradation<sub>k</sub>(%) = 100 · [RMSE<sub>drift,k</sub> − RMSE<sub>stable,k</sub>] / RMSE<sub>stable,k</sub></div>
<p class="where">Positive degradation means worse error in naturally outlying temperature conditions. This is an association test, not a causal temperature derivation.</p>

<h3>10. Robust architecture-selection rule</h3>
<div class="equation">Winner<sub>k</sub> = arg min<sub>m</sub> mean<sub>s</sub>[RMSE<sub>k,m,s</sub>]</div>
<div class="equation">Champion = arg max<sub>m</sub> Σ<sub>k</sub> 𝟙[Winner<sub>k</sub> = m]</div>
<p class="where">Ties are resolved by median fold RMSE and then dispersion. The rule was applied before opening the final test. Persistence won two folds; LSTM won one.</p>

<h3>11. Future methane-physics formulation — not evaluated here</h3>
<div class="card warn"><span class="badge caution">Method proposal only</span>
<div class="equation">ŷ<sub>CH₄,t</sub> = ŷ<sub>mech,t</sub> + f<sub>θ</sub>(x<sub>t−w:t</sub>)</div>
<div class="equation">𝓛<sub>total</sub> = 𝓛<sub>data</sub> + λ<sub>yield</sub>·mean[max(0, ŷ<sub>CH₄</sub> − BMP·VS<sub>fed</sub>)²] + λ<sub>state</sub>𝓛<sub>state</sub> + λ<sub>stability</sub>𝓛<sub>VFA/ALK</sub></div>
<p>This residual/physics loss requires measured methane, substrate VS/BMP, and stability variables. DS-03 lacks them, so Gate 3 does not compute, tune, or claim this loss.</p></div>
</section>

<section id="evaluation"><h2>Rolling-origin model evidence</h2><p class="lead">A single mean score would conceal the extreme second regime. Model selection therefore uses fold wins, then median fold RMSE and dispersion.</p>
<div class="figure"><img src="{fold_plot}" alt="Rolling fold robustness"><p class="caption">Figure 3. Fold-level RMSE on a logarithmic axis. Persistence is strongest in folds 1 and 3; LSTM is strongest only in fold 2.</p></div>
<div class="figure"><img src="{window_plot}" alt="Sequence-window study"><p class="caption">Figure 4. Sequence-window changes are negligible relative to fold-to-fold dispersion.</p></div>
<details><summary>Complete model-window rolling summary</summary>{table(window_table)}</details>
<h3>Locked final-period comparison</h3><div class="figure"><img src="{final_plot}" alt="Final period metrics"><p class="caption">Figure 5. The LSTM has slightly lower final-test RMSE, but much worse sMAPE and was not the rolling-validation champion.</p></div>
{table(display_final)}
<div class="figure"><img src="{prediction_plot}" alt="Timestamped final predictions"><p class="caption">Figure 6. Seed-averaged final-period predictions. Symlog scaling exposes both normal and extreme intervals.</p></div></section>

<section id="training"><h2>Training sessions and epoch evidence</h2><p class="lead">Every LSTM epoch is retained with run, fold, seed, window, training loss, and validation loss. Final-fit LSTM and XGBoost checkpoints are checksum-registered.</p>
<div class="figure"><img src="{epoch_plot}" alt="Epoch histories"><p class="caption">Figure 7. Window-3 rolling LSTM validation traces across all folds and seeds.</p></div>
<div class="grid-2"><div class="card"><h3>Experiment scale</h3><p><b>{len(folds)}</b> fold/seed/model/window metric records</p><p><b>{len(epochs)}</b> epoch records</p><p><b>{len(pd.read_csv(run / 'predictions/rolling_predictions.csv')):,}</b> rolling prediction rows</p><p><b>{len(final_predictions):,}</b> final prediction rows</p></div>
<div class="card"><h3>Model configuration</h3>{code_block(config, "Click to inspect model and training configuration")}</div></div>
<h3>Open source code and configuration</h3>{linked_file_table(code_rows)}</section>

<section id="drift"><h2>Natural temperature-drift study</h2><p class="lead">Drift observations fall outside each training fold's 10th–90th percentile temperature range. No artificial perturbation is presented as plant data.</p>
<div class="figure"><img src="{drift_plot}" alt="Temperature drift degradation"><p class="caption">Figure 8. Degradation is inconsistent across folds: −21.2%, +29.3%, and −21.2%. No causal temperature claim is supported.</p></div>
{table(drift_df[['fold','natural_drift_n','stable_n','training_temperature_low_c','training_temperature_high_c','rmse_degradation_percent']].round(3))}</section>

<section id="memory"><h2>Persistent experiment memory</h2><p class="lead">The accepted run binds source identity, checksums, Git commit, split version, seeds, windows, environment, configurations, predictions, epoch histories, plots, checkpoints, and replay instructions.</p>
<div class="grid-2"><div class="card"><h3>Run identity</h3><p><b>{run_id}</b></p><p>Git commit: <code>{run_manifest['git_commit']}</code></p><p>Run manifest SHA-256: <code>{sha256(run / 'run_manifest.json')}</code></p></div>
<div class="card"><h3>Environment</h3>{code_block(environment, "Click to inspect package and runtime versions")}</div></div>
<details open><summary>Complete clickable artifact inventory and checksums</summary>{linked_file_table(artifact_rows)}</details></section>

<section id="replay"><h2>Exact replay</h2><p class="lead">The accepted run was replayed. Fold metrics, summaries, final-test metrics, scientific decision, drift results, prediction files, and epoch history matched byte-for-byte.</p>
{code_block("""PYTHONPATH=01_Product_Source/GFIS_Project python3 \\
  01_Product_Source/GFIS_Project/scripts/gate3_prepare_dataset.py \\
  --workbook "03_Datasets/01_Raw_Public_Datasets/DS03_Mendeley_gk3f363sfg_v1/extracted/Farm-scale_Biodigester_Perfromance_dataset/Farm-scale Biodigester 2024_Performance.xlsx" \\
  --weather-workbook "03_Datasets/01_Raw_Public_Datasets/DS03_Mendeley_gk3f363sfg_v1/extracted/Farm-scale_Biodigester_Perfromance_dataset/Mazingira main(z6-02152)-1729245757_weather_data.xlsx" \\
  --output 03_Datasets/05_Processed_Final/DS03/gfis_ds03_canonical_v1.csv \\
  --manifest 03_Datasets/05_Processed_Final/DS03/processing_manifest_v1.json

PYTHONPATH=01_Product_Source/GFIS_Project python3 \\
  01_Product_Source/GFIS_Project/scripts/gate3_run_experiment.py \\
  --run-id G3-E01-20260724T1530Z-ds03-v3-replay""", "Click to view exact replay commands")}</section>

<section id="simulator-proof"><span class="badge pass" tabindex="0" data-tip="This section combines previously captured localhost UI evidence with a fresh backend replay generated for Version 2.">Executable simulator proof</span><h2>GFIS capabilities working end to end</h2>
<p class="lead">The simulator evidence below is not a conceptual mock-up. The trained GFIS service bundle was loaded and executed for <b>{simulator_metadata['scenario_count']} controlled scenarios × {simulator_metadata['hours_per_scenario']} hours</b>, producing {simulator_metadata['trace_row_count']} hour-indexed output rows. UI screenshots show the corresponding Control Room, scenario handoff, alarm state and experiment-memory surfaces.</p>
<div class="grid-2"><div class="card accent"><h3>Fresh Version 2 replay</h3><p><b>Run:</b> {simulator_run_id}</p><p><b>Trained model bundle loaded:</b> {str(simulator_metadata['trained_bundle_loaded']).lower()}</p><p><b>Model SHA-256:</b></p>{hash_box(simulator_metadata['model_bundle_sha256'])}<p><b>Evidence label:</b> {simulator_metadata['evidence_label']}</p></div>
<div class="card warn"><h3>What “working” means here</h3><ul><li>Python service executed predictions and stateful 48-hour traces.</li><li>VFA/ALK warning states changed under overload stress.</li><li>A low-VS boundary challenge triggered and recorded the physics constraint.</li><li>Every output row, configuration identity and checksum is retained.</li><li>This is controlled synthetic replay evidence—not measured plant validation.</li></ul></div></div>
<div class="figure"><img src="{simulator_replay_plot}" alt="Executed GFIS simulator capability replay"><p class="caption">Figure 9. Fresh Version 2 backend replay. Methane trajectories, VFA/ALK responses and the VS-dependent physics ceiling are plotted directly from the preserved 432-row simulator output.</p></div>
<h3>Executed scenario results</h3>{table(simulator_display.round(3))}
<div class="grid-2"><div class="figure"><img src="{control_room_screenshot}" alt="GFIS Model Control Room with experiment memory"><p class="caption">Figure 10. Localhost Control Room evidence captured 9 July 2026: process controls, methane/physics/VFA outputs, 48-hour handoff, and CSV/JSON/report experiment-memory export. The historical panel's model-status message is superseded by the fresh Version 2 replay above, which confirms the trained bundle loaded.</p></div>
<div class="figure"><img src="{scenario_handoff_screenshot}" alt="Industrial simulator receiving a Control Room stress scenario"><p class="caption">Figure 11. Localhost scenario-handoff evidence captured 9 July 2026: the industrial simulator imports OLR, pH, temperature, HRT, TS and VS, detects VFA/ALK stress, raises warning/critical UI states, and records the operator-applied process state.</p></div></div>
<div class="figure"><img src="{simulator_memory_screenshot}" alt="Industrial simulator memory and export panel"><p class="caption">Figure 12. Localhost industrial-simulator memory evidence captured 9 July 2026: scenario variables, action/effect records, VFA/ALK warning state, physics message and CSV/report export controls remain visible in one audit surface.</p></div>
<h3>Open replayable simulator evidence</h3>{linked_file_table(simulator_rows)}
{code_block(simulator_metadata['replay_command_template'], "Click to view the simulator replay command template")}
<div class="card accent"><h3>Capability conclusion</h3><p>This evidence supports the statement that GFIS <b>implements and executes</b> continuous multi-hour methane simulation, VFA/ALK soft sensing, VS-based feasibility checking, scenario coordination and persistent experiment export. It does not convert controlled replay into an industrial-plant accuracy claim; that remains the next external-validation layer.</p></div></section>

<section id="limitations"><h2>Implemented GFIS capability versus external validation</h2>
<p class="lead">These are not missing product ideas. They already exist in the GFIS prototype and simulator. Gate 3's limitation is narrower: DS-03 cannot independently validate every capability because it provides total biogas with almost no measured methane and lacks the required VFA/ALK and VS/BMP targets.</p>
<div class="table-scroll"><table class="data-table"><thead><tr><th>GFIS capability</th><th>Implemented evidence now</th><th>Validation boundary</th><th>Next evidence upgrade</th></tr></thead><tbody>
<tr><td>Continuous methane forecasting</td><td>Trained methane predictor, temporal inputs and deterministic 48-hour plant trace with feedback history.</td><td>Demonstrated on synthetic/scenario data; DS-03 does not contain a usable continuous methane target.</td><td>Replay on a licensed timestamped methane dataset and compare chronologically against persistence and XGBoost.</td></tr>
<tr><td>VFA/ALK virtual soft sensor</td><td>Random-forest soft sensor, stability thresholds, overload/acidification scenarios, API output and simulator alarms.</td><td>Functional prototype evidence; no independent measured VFA/ALK labels in DS-03.</td><td>Validate classification and regression against laboratory VFA and alkalinity samples, including lead-time to warning.</td></tr>
<tr><td>VS-based physics guidance</td><td>VS-dependent methane ceiling, violation flag, post-prediction correction and scenario violation memory.</td><td>The current implementation is a feasibility constraint/post-processing layer—not yet a trained physics-loss experiment on measured methane/VS/BMP.</td><td>Add an ablation-controlled loss term and report accuracy, violation rate and yield-bound calibration.</td></tr>
<tr><td>Temporal memory</td><td>LSTM branch, lag/rolling history, sequence-window experiments and complete epoch logs/checkpoints.</td><td>On DS-03, longer windows do not improve rolling-origin robustness and persistence wins two of three folds.</td><td>Keep the compact LSTM challenger; retest only when a longer, methane-rich series provides adequate memory evidence.</td></tr>
<tr><td>Industrial digital-twin workflow</td><td>Control Room, 48-hour industrial simulator, scenario handoff, optimization, warnings and JSON/CSV/report memory export.</td><td>Product workflow is demonstrable; it is not yet a field-performance claim from a commissioned industrial plant.</td><td>Run shadow-mode pilot validation with sensor mapping, calibration records, uptime, error and operator-decision metrics.</td></tr>
</tbody></table></div>
<div class="grid-2"><div class="card accent"><h3>Defensible Version 2 statement</h3><p>GFIS implements and demonstrates methane prediction, VFA/ALK soft sensing, VS-based feasibility checking, temporal modelling, and a memory-enabled industrial simulator. The validation level is explicitly labelled as synthetic, public-dataset, or future plant validation for each result.</p></div>
<div class="card warn"><h3>Recommended temporal architecture</h3><p>Keep persistence as the safe DS-03 champion. Retain the compact 3–7 observation LSTM as challenger. Advance the physics-guided methane architecture when a licensed continuous dataset supplies reactor identity, measured methane, temperature, feed/VS or BMP, and preferably VFA/ALK.</p></div></div>
<p><a class="open-link" href="../../01_Product_Source/GFIS_Project/gfis/service.py">Open prediction and 48-hour simulation service</a> <a class="open-link" href="../../01_Product_Source/GFIS_Project/gfis/models.py">Open LSTM and physics-constraint models</a> <a class="open-link" href="../../01_Product_Source/GFIS_Project/reports/research_scenario_experiments.md">Open controlled scenario evidence</a></p></section>
</main><footer class="footer">
<div class="footer-grid"><div><div class="brand-lockup"><div class="brand-mark" aria-hidden="true">CI</div><div><div class="brand-name">Chatake Innoworks</div><div class="brand-sub">GFIS research and product development</div></div></div><p>GFIS — GreenFuel Intelligence System<br>Physics-guided AI and digital-twin-ready simulation for anaerobic digestion.</p></div>
<div><h3>Company</h3><div class="footer-links"><a href="https://www.chatakeinnoworks.com" target="_blank" rel="noopener">Official website</a><a href="https://about.chatakeinnoworks.com" target="_blank" rel="noopener">Corporate profile</a><a href="mailto:gfis@chatakeinnoworks.com">gfis@chatakeinnoworks.com</a></div></div>
<div><h3>GFIS product links</h3><div class="footer-links"><a href="https://gfis.chatakeinnoworks.com/" target="_blank" rel="noopener">GFIS portal</a><a href="https://gfis.chatakeinnoworks.com/level-2/" target="_blank" rel="noopener">Level 2 platform</a><a href="https://gfis.chatakeinnoworks.com/level-2/apps/model-control-room/" target="_blank" rel="noopener">Model Control Room</a><a href="https://gfis.chatakeinnoworks.com/level-2/apps/model-control-room/industrial_simulation.html" target="_blank" rel="noopener">Industrial Simulator</a></div></div></div>
<div class="legal"><div class="legal-note"><strong>© 2026 Chatake Innoworks · GFIS Final Version 2026.</strong> Copyright application diary no. <strong>LD-12628/2026-CO</strong> · Work title: <i>GFIS - GreenFuel Intelligence System</i> · Applicant/author: Akash Shivdas Chatake. Diary details identify an application record and are not represented as a registration or grant.</div><div><a href="../../01_Product_Source/GFIS_Unified_Portal/level-2/library/current-midsem-2026-07-09/level1-ip/GFIS_Copyright_Application_LD-12628_2026-CO.pdf">Open copyright application record</a><br>Scientific evidence report · self-contained · printable</div></div>
</footer></div>
<button class="print-btn" onclick="window.print()">Print / Save PDF</button>
<div class="lightbox" id="figure-lightbox" role="dialog" aria-modal="true" aria-label="Expanded scientific figure">
  <div class="lightbox-inner"><div class="lightbox-bar"><div class="lightbox-title">Expanded figure</div><button class="lightbox-close" type="button" aria-label="Close expanded figure">×</button></div>
  <div class="lightbox-stage"><img alt="Expanded report figure"></div><div class="lightbox-caption"></div></div>
</div>
<script>
(() => {{
  const lightbox = document.getElementById("figure-lightbox");
  const expanded = lightbox.querySelector("img");
  const expandedCaption = lightbox.querySelector(".lightbox-caption");
  const expandedTitle = lightbox.querySelector(".lightbox-title");
  const closeButton = lightbox.querySelector(".lightbox-close");
  const closeLightbox = () => {{
    lightbox.classList.remove("open");
    document.body.style.overflow = "";
  }};
  document.querySelectorAll(".figure").forEach((figure, index) => {{
    const image = figure.querySelector("img");
    const caption = figure.querySelector(".caption");
    const explanation = caption ? caption.textContent.trim() : `Scientific figure ${{index + 1}}`;
    image.tabIndex = 0;
    image.setAttribute("role", "button");
    image.setAttribute("aria-label", `Zoom ${{explanation}}`);
    image.title = `${{explanation}} Click to zoom and inspect.`;
    const open = () => {{
      expanded.src = image.src;
      expanded.alt = image.alt;
      expandedCaption.textContent = explanation;
      expandedTitle.textContent = image.alt || `Figure ${{index + 1}}`;
      lightbox.classList.add("open");
      document.body.style.overflow = "hidden";
      closeButton.focus();
    }};
    image.addEventListener("click", open);
    image.addEventListener("keydown", event => {{
      if (event.key === "Enter" || event.key === " ") {{ event.preventDefault(); open(); }}
    }});
  }});
  closeButton.addEventListener("click", closeLightbox);
  lightbox.addEventListener("click", event => {{ if (event.target === lightbox) closeLightbox(); }});
  document.addEventListener("keydown", event => {{ if (event.key === "Escape") closeLightbox(); }});
  document.querySelectorAll(".copy-code").forEach(button => {{
    button.addEventListener("click", async () => {{
      const code = button.parentElement.querySelector("code").textContent;
      try {{
        await navigator.clipboard.writeText(code);
        button.textContent = "Copied";
        setTimeout(() => button.textContent = "Copy", 1400);
      }} catch (_) {{
        button.textContent = "Select text";
      }}
    }});
  }});
  document.querySelectorAll("section > h2").forEach(heading => {{
    heading.title = `Section: ${{heading.textContent.trim()}}`;
  }});
}})();
</script></body></html>"""
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(html_doc, encoding="utf-8")
    print(output)
    print(sha256(output))


if __name__ == "__main__":
    workspace = Path(__file__).resolve().parents[3]
    build(
        workspace,
        "G3-E01-20260724T1530Z-ds03-v3",
        workspace / "07_Evidence_Outputs/GATE_3/GFIS_GATE_3_INTEGRATED_REPORT_V2.html",
    )
