#!/usr/bin/env python3
"""Assemble the AC28/ME1a comparison and all ME1a result links."""
from __future__ import annotations

import html
import json
from pathlib import Path

PROJECT=Path(__file__).resolve().parents[1]
ME=PROJECT/"experiments/ME1a_DT"
OUT=PROJECT/"results/ME1a_DT_comparison"


def load(path: Path):return json.loads(path.read_text())
def link(path: str, label: str) -> str:return f'<a href="{html.escape(path)}">{html.escape(label)}</a>'
def number(value: float, digits: int=3) -> str:return f"{value:,.{digits}f}"


def main() -> None:
    rep=load(OUT/"representation_sensitivity.json")["records"]
    graph_fit=load(OUT/"graph_fit/report.json")["records"]
    trained=load(ME/"results/models/cad_predictions/report.json")
    ac28_trained=load(PROJECT/"results/models/cad_predictions/report.json")
    smoke=load(ME/"results/models/report.json")
    weak=load(ME/"results/models/weak_gnn/report.json")
    diffusion=load(ME/"gen_models/outputs/report.json")
    gnn_soft=load(OUT/"gnn_joint/report.json")["rows"]
    gnn_hard=load(OUT/"gnn_joint_hard/report.json")["rows"]
    target=load(ME/"results/inspection/target_review.json")
    scan=load(ME/"results/inspection/component_audit.json")
    manifest=load(ME/"analysis/results/source_manifest.json")
    brep=load(ME/"data_processed/brep/metadata.json")
    graphs=load(ME/"data_processed/graphs/index.json")
    budgets=(12000,6000,5000,3000,1500,750,300,100)

    graph_rows=[]
    for budget in budgets:
        a=next(r for r in rep if r["part"]=="AC28" and r["requested_nodes"]==budget)
        m=next(r for r in rep if r["part"]=="ME1a_DT" and r["requested_nodes"]==budget)
        fa=next(r for r in graph_fit if r["part"]=="AC28" and r["requested_nodes"]==budget)
        fm=next(r for r in graph_fit if r["part"]=="ME1a_DT" and r["requested_nodes"]==budget)
        graph_rows.append(f"<tr><th>{budget:,}</th><td>{a['actual_nodes']:,}</td><td>{m['actual_nodes']:,}</td>"
            f"<td>{number(a['source_to_graph_p95_mm'])}</td><td>{number(m['source_to_graph_p95_mm'])}</td>"
            f"<td>{number(a['image_silhouette_iou_128'])}</td><td>{number(m['image_silhouette_iou_128'])}</td>"
            f"<td>{number(a['surface_voxel_jaccard_32'])}</td><td>{number(m['surface_voxel_jaccard_32'])}</td>"
            f"<td>{number(fa['weak_label_transfer_iou_on_finest_graph'])}</td>"
            f"<td>{number(fm['weak_label_transfer_iou_on_finest_graph'])}</td></tr>")

    model_rows=[]
    for name,row in trained["models"].items():
        baseline=ac28_trained["models"].get(name)
        model_rows.append(f"<tr><th>{html.escape(name)}</th><td>{row['epochs']}</td>"
            f"<td>{number(row['loss_final'])}</td><td>{number(row['average_precision'])}</td>"
            f"<td>{number(row['threshold_iou'])}</td><td>{number(row['threshold_dice'])}</td>"
            f"<td>{number(row['display_coverage'])}</td>"
            f"<td>{number(baseline['average_precision']) if baseline else '—'}</td>"
            f"<td>{number(baseline['threshold_iou']) if baseline else '—'}</td></tr>")
    diffusion_rows=[]
    for row in diffusion["variants"]:
        metric=row["metrics"][-1]
        diffusion_rows.append(f"<tr><th>{html.escape(row['variant'])}</th><td>{row['nodes_per_run']:,}</td>"
            f"<td>{number(row['best_loss'])}</td><td>{number(metric['MAE_mm'])}</td>"
            f"<td>{number(metric['risk']['AP'])}</td><td>{number(metric['risk']['IoU_at_0.5'])}</td></tr>")
    def gnn_score(rows, run, setup):
        return next(r for r in rows if r["budget"]==12000 and r["kind"]=="residual_geo"
                    and r["eval_run"]==run and r["setup"]==setup)
    gnn_runs=("AC28_16h","AC28_18h01","ME1a_14h06")
    gnn_rows=[]
    for run in gnn_runs:
        single=gnn_score(gnn_hard,run,f"single_{run}")
        joint=gnn_score(gnn_hard,run,"joint")
        held=gnn_score(gnn_hard,run,f"holdout_{run}")
        gnn_rows.append(f"<tr><th>{run}</th><td>{number(single['mapped_ap'])}</td><td>{number(single['mapped_iou'])}</td>"
            f"<td>{number(joint['mapped_ap'])}</td><td>{number(joint['mapped_iou'])}</td>"
            f"<td>{number(held['mapped_ap'],4)}</td><td>{number(held['mapped_iou'])}</td></tr>")

    p95=[r["sample_to_fused_p95_mm"] for r in scan["rows"]]
    smoke_pass=sum(r["status"]=="forward_backward_smoke" for r in smoke["models"])
    summary=(
        f"ME1a used {manifest['process_rows']} XLS process rows, {manifest['defect_rows']} CSV defect marks, "
        f"{manifest['fused_check']['vertex_count']:,} fused PLY vertices, {brep['face_count']:,} B-Rep faces "
        f"and {graphs['source_vertices']:,} welded STEP-mesh vertices. "
        f"The same eight graph budgets as AC28 were prepared. "
        f"All {smoke_pass} recorded architecture forward/backward checks passed; "
        f"{len(trained['models'])} prediction-model families and {len(diffusion['variants'])} diffusion variants completed weak-label diagnostic fitting."
    )
    md=["# ME1a_DT and AC28 experiment results","",summary,"",
        "## Graph node changes and representation fidelity","",
        "Values compare each part with its own finest graph. Surface P95 is sampled source-to-simplified distance; image IoU uses six 128² views; voxel Jaccard uses 32-cell surface occupancy; weak-label transfer IoU maps each graph's 10 mm CSV-seeded mask back to the finest graph.","",
        "| Requested | AC28 actual | ME1a actual | AC28 P95 mm | ME1a P95 mm | AC28 image IoU | ME1a image IoU | AC28 voxel Jaccard | ME1a voxel Jaccard | AC28 weak-label transfer IoU | ME1a weak-label transfer IoU |",
        "|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|"]
    for budget in budgets:
        a=next(r for r in rep if r["part"]=="AC28" and r["requested_nodes"]==budget)
        m=next(r for r in rep if r["part"]=="ME1a_DT" and r["requested_nodes"]==budget)
        fa=next(r for r in graph_fit if r["part"]=="AC28" and r["requested_nodes"]==budget)
        fm=next(r for r in graph_fit if r["part"]=="ME1a_DT" and r["requested_nodes"]==budget)
        md.append("| "+" | ".join((f"{budget:,}",f"{a['actual_nodes']:,}",f"{m['actual_nodes']:,}",
            number(a["source_to_graph_p95_mm"]),number(m["source_to_graph_p95_mm"]),
            number(a["image_silhouette_iou_128"]),number(m["image_silhouette_iou_128"]),
            number(a["surface_voxel_jaccard_32"]),number(m["surface_voxel_jaccard_32"]),
            number(fa["weak_label_transfer_iou_on_finest_graph"]),
            number(fm["weak_label_transfer_iou_on_finest_graph"])))+" |")
    md += ["","The ME1a 100-node request produced 144 nodes, a 387.356 mm surface P95 error and 0.151 image silhouette IoU. The 3,000-node request produced 3,083 nodes, a 0.563 mm surface P95 error and 0.988 silhouette IoU. Small 10 mm weak defect masks lose substantial support even at 3,000 nodes; geometry fidelity alone does not preserve localization labels.","",
        "## Scan and annotation inspection","",
        f"- All six PLY files passed full vertex/bounds checks with no nonfinite vertices. The five component files are views of the same production run, not independent training samples. Sample-to-fused P95 distances span {min(p95):.3f}–{max(p95):.3f} mm.",
        f"- The inferred merged-height raster-to-PLY mapping has {target['raster_mapping_check']['height_abs_p90_mm']:.3f} mm height P90 error on sampled scan points. All six CSV anchors are within 30 mm of a CAD node after a diagnostic proper-rigid fit.",
        f"- The fitted pose has capped 80 mm RMSE {target['registration_candidates'][0]['capped_rmse80_mm']:.2f} mm and is not independently validated. The six CSV seeds produce {target['weak_10mm_positive_nodes']} positive nodes at an assumed 10 mm geodesic radius. All `training_valid` masks remain false.","",
        "## Model execution and weak-label fits","",
        f"- {smoke_pass} forward/backward architecture checks passed on ME1a inputs; {len(smoke['synthetic'])} synthetic checks/demos are separate from real labels.",
        f"- The constrained risk/deformation GNN ran 400 weak-fit epochs: loss {weak['loss_initial']:.3f} → {weak['loss_final']:.3f}; same-run threshold IoU {weak['training_threshold_iou']:.3f}, Dice {weak['training_threshold_dice']:.3f}. Its defect-type head cannot learn from one run-level class.",
        "- All ten feasible model families were fitted to the same ME1a weak target. Values below are same-run training diagnostics and cannot rank production quality or be compared as held-out AC28 vs ME1a accuracy.","",
        "| Model | Epochs | ME1a loss | ME1a AP | ME1a IoU | ME1a Dice | CAD mapping coverage | AC28 AP | AC28 IoU |",
        "|---|---:|---:|---:|---:|---:|---:|---:|---:|"]
    for name,row in trained["models"].items():
        base=ac28_trained["models"].get(name)
        md.append("| "+" | ".join((name,str(row["epochs"]),number(row["loss_final"]),
            number(row["average_precision"]),number(row["threshold_iou"]),
            number(row["threshold_dice"]),number(row["display_coverage"]),
            number(base["average_precision"]) if base else "—",
            number(base["threshold_iou"]) if base else "—"))+" |")
    md += ["","## Conditional diffusion variants","",
        "Four representation-specific diffusion denoisers and a paper-inspired TAG Graph U-Net were also fitted on the one ME1a run. Deformation MAE uses a nearest-scan proxy under unvalidated registration; risk AP/IoU are same-run weak-label diagnostics.","",
        "| Variant | Nodes/run | Best loss | ME1a deformation MAE mm | Weak risk AP | Weak risk IoU |",
        "|---|---:|---:|---:|---:|---:|"]
    for row in diffusion["variants"]:
        metric=row["metrics"][-1]
        md.append("| "+" | ".join((row["variant"],f"{row['nodes_per_run']:,}",number(row["best_loss"]),
            number(metric["MAE_mm"]),number(metric["risk"]["AP"]),number(metric["risk"]["IoU_at_0.5"])))+" |")
    md += ["","## Joint AC28 + ME1a GNN experiment","",
        "The residual geometric GNN was compared with the prior Mesh GNN and a position-aware MLP control at 3k, 5k, 6k and 12k requested nodes. A second 12k fit used hard, class-balanced weak masks for 180 epochs. The numbers below are weak-label diagnostics, not verified prediction accuracy.","",
        "| Run | Individual AP | Individual IoU | Joint AP | Joint IoU | Held-out AP | Held-out IoU |",
        "|---|---:|---:|---:|---:|---:|---:|"]
    for run in gnn_runs:
        values=[gnn_score(gnn_hard,run,f"single_{run}"),gnn_score(gnn_hard,run,"joint"),
                gnn_score(gnn_hard,run,f"holdout_{run}")]
        md.append("| "+" | ".join((run,*[number(r[key]) for r in values for key in ("mapped_ap","mapped_iou")]))+" |")
    md += ["","ME1a individual-fit AP reached 0.629 and IoU 0.476, exceeding the earlier FNO same-run AP 0.444 and IoU 0.143. Its held-out AP stayed near its 0.001 prevalence and IoU was zero. Adding ME1a to AC28 holdout training did not consistently improve either AC28 run. The GNN fits ME1a's weak training mask more closely. Cross-part prediction remains unproven.","",
        "For localized risk output, fix the current graph at **12,000 requested nodes** (AC28 11,956; ME1a 12,324 actual). The 5k ME1a graph has 5,132 actual nodes and transfers only 0.175 of the finest weak mask by IoU; its 6k graph transfers 0.333 and 3k transfers 0.129. Lower-node graphs can be context inputs, but should not define the spatial output grid. This is a representability choice, not a statistically validated predictive optimum.","",
        "- [Four-size architecture/pooled-fit comparison](gnn_joint/index.html) and [full CSV](gnn_joint/report.csv)",
        "- [Optimized 12k residual GNN results](gnn_joint_hard/index.html), [prediction maps](gnn_joint_hard/prediction_maps.png), and [full CSV](gnn_joint_hard/report.csv)",""]
    md += ["","## Open results","",
        "- [Interactive report](index.html)",
        "- [All risk models compared on AC28 and ME1a](model_ranking/index.html) and [full metrics CSV](model_ranking/report.csv)",
        "- [ME1a preprocessing gallery](../../experiments/ME1a_DT/data_processed/index.html)",
        "- [ME1a graph viewer](../../experiments/ME1a_DT/results/graphs/viewer.html)",
        "- [ME1a all-model training gallery](../../experiments/ME1a_DT/results/models/cad_predictions/index.html)",
        "- [ME1a weak GNN gallery](../../experiments/ME1a_DT/results/models/weak_gnn/index.html)",
        "- [ME1a conditional diffusion gallery](../../experiments/ME1a_DT/gen_models/outputs/index.html) and [report JSON](../../experiments/ME1a_DT/gen_models/outputs/report.json)",
        "- [Graph representation CSV](representation_sensitivity.csv) and [per-resolution training CSV](graph_fit/report.csv)",
        "- [AC28 preprocessing gallery](../../data_processed/index.html) and [AC28 all-model training gallery](../models/cad_predictions/index.html)","",
        "The fused PLY is an outcome for target construction, not an input to pre-process prediction. Missing independently verified registration, reviewed spatial labels, OK examples and held-out production runs prevent a valid prediction-accuracy claim.",""]
    (OUT/"README.md").write_text("\n".join(md),encoding="utf-8")

    css="""body{font:15px/1.5 system-ui,sans-serif;background:#f5f7fb;color:#1a2532;max-width:1450px;margin:auto;padding:24px}h1,h2{line-height:1.2}a{color:#125a93}section{background:white;border:1px solid #dce3ec;border-radius:12px;padding:20px;margin:20px 0}.lead{font-size:18px}.warning{background:#fff2cf;border-left:5px solid #d29a13;padding:14px}.scroll{overflow:auto}table{border-collapse:collapse;min-width:850px;width:100%}th,td{border-bottom:1px solid #e1e6ec;padding:8px;text-align:right;white-space:nowrap}th:first-child{text-align:left}thead th{background:#edf3f8}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(310px,1fr));gap:14px}.grid img,.wide{width:100%;border:1px solid #e0e6ed;border-radius:6px}.kpis{display:flex;flex-wrap:wrap;gap:12px}.kpis span{background:#eaf1f7;border-radius:8px;padding:10px 14px}small{color:#546474}@media(max-width:700px){body{padding:12px}.grid{display:block}}"""
    page=f"""<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ME1a_DT + AC28 results</title><style>{css}</style>
<h1>ME1a_DT with AC28: preprocessing and model results</h1><p class="lead">{html.escape(summary)}</p>
<p class="warning"><strong>Interpretation:</strong> PLY-to-CAD registration and CSV defect extents are not independently validated. Training metrics below measure fit to weak masks on the same physical run. They are not held-out accuracy or evidence of design transfer.</p>
<div class="kpis"><span><b>{graphs['source_vertices']:,}</b> STEP mesh vertices</span><span><b>{graphs['resolutions'][0]['actual_nodes']:,}</b> finest graph nodes</span><span><b>{brep['face_count']:,}</b> B-Rep faces</span><span><b>{manifest['fused_check']['vertex_count']:,}</b> fused PLY vertices</span><span><b>{smoke_pass}</b> architecture checks</span><span><b>{len(trained['models'])}</b> prediction-model families</span><span><b>{len(diffusion['variants'])}</b> diffusion variants</span></div>
<section><h2>Graph nodes change what the representations retain</h2><p>Each part is compared with its own finest graph. P95 is sampled source-surface error in mm; six-view image silhouette IoU and surface voxel Jaccard are measured after regenerating each representation from every graph.</p><div class="scroll"><table><thead><tr><th>Requested nodes</th><th>AC28 actual</th><th>ME1a actual</th><th>AC28 P95 mm</th><th>ME1a P95 mm</th><th>AC28 image IoU</th><th>ME1a image IoU</th><th>AC28 voxel Jaccard</th><th>ME1a voxel Jaccard</th><th>AC28 weak-label transfer IoU</th><th>ME1a weak-label transfer IoU</th></tr></thead><tbody>{''.join(graph_rows)}</tbody></table></div><p><strong>ME1a at 3,083 nodes:</strong> 0.563 mm P95 and 0.988 image IoU. <strong>ME1a at 144 nodes:</strong> 387.356 mm P95 and 0.151 image IoU. Fine geometry views can remain similar while 10 mm defect masks lose support.</p><div class="grid"><img src="resolution_views.png" alt="AC28 and ME1a depth views by graph budget"><img src="sensitivity_curves.png" alt="Representation quality versus node count"><img src="graph_fit/resolution_training.png" alt="Weak label and training fit by node count"></div><p>{link('representation_sensitivity.csv','Representation metrics CSV')} · {link('graph_fit/report.csv','All graph-resolution fits CSV')} · {link('../../experiments/ME1a_DT/results/graphs/viewer.html','ME1a interactive graph viewer')}</p></section>
<section><h2>Source and visual inspection</h2><p>All six PLY files have finite vertices. The component scans are five views of one run. The merged-height raster matches sampled PLY heights at P90 {target['raster_mapping_check']['height_abs_p90_mm']:.3f} mm. The automatic rigid pose has capped 80 mm RMSE {target['registration_candidates'][0]['capped_rmse80_mm']:.2f} mm, so residual/deformation values remain diagnostic.</p><div class="grid"><img src="../../experiments/ME1a_DT/results/inspection/csv_defects_on_merged_image.png" alt="Six CSV defect marks on merged image"><img src="../../experiments/ME1a_DT/results/inspection/ply_components.png" alt="Fused and component PLY scans"><img src="../../experiments/ME1a_DT/results/inspection/alignment_targets.png" alt="CAD and fused PLY fit with diagnostic targets"></div><p>{link('../../experiments/ME1a_DT/results/inspection/target_review.json','Annotation and pose review JSON')} · {link('../../experiments/ME1a_DT/results/inspection/component_audit.json','All PLY audit JSON')} · {link('../../experiments/ME1a_DT/data_processed/index.html','ME1a preprocessing gallery')}</p></section>
<section><h2>All model weak-label fits</h2><p>Ten model families fit the same ME1a diagnostic mask. AP, IoU and Dice are measured on that training run. AC28 columns show its own separate weak-label training run and are included for traceability, not a performance ranking. The constrained GNN's 400-epoch training IoU was {weak['training_threshold_iou']:.3f}.</p><div class="scroll"><table><thead><tr><th>Model</th><th>Epochs</th><th>ME1a loss</th><th>ME1a AP</th><th>ME1a IoU</th><th>ME1a Dice</th><th>CAD map coverage</th><th>AC28 AP</th><th>AC28 IoU</th></tr></thead><tbody>{''.join(model_rows)}</tbody></table></div><img class="wide" src="../../experiments/ME1a_DT/results/models/cad_predictions/comparison.png" alt="All ME1a weak-fit model predictions on one CAD"><p>{link('../../experiments/ME1a_DT/results/models/cad_predictions/index.html','ME1a model image gallery and checkpoints')} · {link('../../experiments/ME1a_DT/results/models/cad_predictions/report.json','All model metrics JSON')} · {link('../../experiments/ME1a_DT/results/models/weak_gnn/index.html','Constrained GNN training')} · {link('../../experiments/ME1a_DT/results/models/report.json','Architecture execution report')} · {link('../models/cad_predictions/index.html','AC28 model results')}</p></section>
<section><h2>Conditional diffusion diagnostics</h2><p>Four representation-specific denoisers and the TAG Graph U-Net were fitted on the one ME1a run. Deformation MAE uses an unvalidated nearest-scan target. Weak risk AP and IoU are measured on that same run, so these values do not establish generation or prediction quality.</p><div class="scroll"><table><thead><tr><th>Variant</th><th>Nodes/run</th><th>Best loss</th><th>Deformation MAE mm</th><th>Weak risk AP</th><th>Weak risk IoU</th></tr></thead><tbody>{''.join(diffusion_rows)}</tbody></table></div><div class="grid"><img src="../../experiments/ME1a_DT/gen_models/outputs/14h06_four_variant_comparison.png" alt="Four ME1a diffusion variants"><img src="../../experiments/ME1a_DT/gen_models/outputs/graph_unet_6000_14h06_comparison.png" alt="Graph baseline and TAG U-Net on ME1a"></div><p>{link('../../experiments/ME1a_DT/gen_models/outputs/index.html','Diffusion gallery, generated clouds and checkpoints')} · {link('../../experiments/ME1a_DT/gen_models/outputs/report.json','Five-variant report JSON')} · {link('../../gen_models/outputs/index.html','AC28 diffusion gallery')}</p></section>
<section><h2>Can one GNN learn from both parts?</h2><p>The residual geometric GNN was compared with the prior Mesh GNN and a coordinate MLP control at 3k, 5k, 6k and 12k graph budgets. The optimized 12k fit uses hard, class-balanced provisional masks. Individual and joint scores are measured on their training runs; held-out scores predict a run excluded from fitting.</p><div class="scroll"><table><thead><tr><th>Run</th><th>Individual AP</th><th>Individual IoU</th><th>Joint AP</th><th>Joint IoU</th><th>Held-out AP</th><th>Held-out IoU</th></tr></thead><tbody>{''.join(gnn_rows)}</tbody></table></div><p><strong>Result:</strong> ME1a individual fit AP 0.629 / IoU 0.476 exceeds its previous FNO same-run 0.444 / 0.143, but held-out ME1a AP is near chance and IoU is zero. Adding ME1a did not consistently improve held-out AC28 prediction. <strong>Node choice:</strong> use 12k requested nodes (11,956 AC28; 12,324 ME1a actual) for spatial outputs. ME1a's 5k graph has 5,132 actual nodes and retains only 0.175 of the finest weak mask by IoU; its 6k graph retains 0.333 and 3k retains 0.129. This choice protects localized labels; it is not a validated predictive optimum.</p><div class="grid"><img src="gnn_joint/comparison.png" alt="GNN variants and node sizes, joint and held-out results"><img src="gnn_joint_hard/prediction_maps.png" alt="Individual, pooled and held-out GNN prediction maps"></div><p>{link('model_ranking/index.html','Risk GNN versus every other model')} · {link('gnn_joint/index.html','Four-size architecture experiment')} · {link('gnn_joint_hard/index.html','Optimized 12k GNN and pooling experiment')} · {link('gnn_joint_hard/report.csv','All optimized per-run metrics CSV')}</p></section>
<section><h2>Prepared inputs</h2><p>ME1a has six CAD image views at 128²/256², sparse surface voxels at 32/64/128, one 8,192-point CAD set, a separate 8,192-point post-scan diagnostic set, tabular XLS conditions and the trimmed B-Rep face graph. The scan is an outcome used for target construction, not a pre-process predictor.</p><p>{link('../../experiments/ME1a_DT/data_processed/images/index.html','Images')} · {link('../../experiments/ME1a_DT/data_processed/voxels/index.html','Voxels')} · {link('../../experiments/ME1a_DT/data_processed/points/index.html','Points')} · {link('../../experiments/ME1a_DT/data_processed/tabular/index.html','Process table')} · {link('../../experiments/ME1a_DT/data_processed/brep/index.html','B-Rep')} · {link('../../data_processed/index.html','AC28 inputs')} · {link('README.md','Full written summary')}</p></section></html>"""
    (OUT/"index.html").write_text(page,encoding="utf-8")
    print(OUT/"index.html")


if __name__=="__main__":main()
