<?php
// Network-on-chip (NoC) latency & throughput simulator — customer-facing page.
// Heavy lifting runs on the ChipFoundryServices compute pool: this page POSTs
// the mesh radix, topology (mesh vs torus), injection rate, packet size, link
// width, router frequency, router pipeline depth, link cycles, virtual channels,
// buffer depth and latency budget to /edge/noc (same-origin). NGINX routes it to
// the compute node, which computes the average hop count, zero-load and queued
// latency, the per-node saturation injection rate set by the network bisection,
// bisection and aggregate bandwidth, and the routing efficiency — sweeping
// offered load to expose the latency "hockey stick" and mesh radix to expose the
// 1/k throughput fall-off. Results are annotated with explanatory text pulled
// live from the CFS knowledge base.
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Network-on-Chip (NoC) Simulator — Mesh/Torus Hop Latency, Bisection Bandwidth, Saturation Throughput &amp; Queueing | ChipFoundryServices</title>
<link rel="stylesheet" href="/cfs_theme.css?v=2">
<meta name="description" content="Run an on-chip network (NoC) latency &amp; throughput simulation in your browser — computed on the ChipFoundryServices compute pool. Compute the average hop count for uniform traffic on a k×k mesh or torus, the zero-load latency H·(router + link) + serialization, and the queueing latency that diverges as offered load approaches saturation. See how the network bisection caps the per-node saturation injection rate as ~1/k, how a torus doubles the bisection (and throughput) at the cost of longer wraparound links, and how virtual channels and buffering recover a fraction of the ideal bisection limit. Reduced-order educational model of the interconnect fabric behind many-core CPUs, GPUs and AI accelerators.">
<style>
  :root{ --green:#07C160; --ink:#16181d; --muted:#6b7280; --line:#e6e8eb; --bg:#f5f6f8; --si:#3a4a63; --film:#4f9cf9; --cav:#eef2f7; }
  *{ box-sizing:border-box; }
  body{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--ink); }
  .wrap{ max-width:1040px; margin:0 auto; padding:24px 16px 56px; }
  h1{ font-size:24px; margin:0 0 4px; }
  .lede{ color:var(--muted); font-size:14px; margin:0 0 20px; line-height:1.5; }
  .lede a{ color:var(--green); text-decoration:none; }
  .grid{ display:grid; grid-template-columns:320px 1fr; gap:20px; align-items:start; }
  @media(max-width:760px){ .grid{ grid-template-columns:1fr; } }
  .card{ background:#fff; border:1px solid var(--line); border-radius:12px; padding:18px; box-shadow:0 1px 3px rgba(0,0,0,.05); }
  label{ display:block; font-size:13px; font-weight:600; margin:14px 0 5px; }
  label:first-of-type{ margin-top:0; }
  .val{ float:right; color:var(--green); font-weight:700; font-variant-numeric:tabular-nums; }
  input[type=range]{ width:100%; accent-color:var(--green); }
  .presets{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
  .preset{ font-size:12px; padding:6px 10px; border:1px solid var(--line); border-radius:999px; background:#fafbfc; cursor:pointer; }
  .preset:hover{ border-color:var(--green); color:var(--green); }
  button.run{ width:100%; margin-top:18px; background:var(--green); color:#fff; border:0; border-radius:8px; padding:12px; font-size:15px; font-weight:700; cursor:pointer; }
  button.run:disabled{ opacity:.6; cursor:default; }
  .viz{ display:flex; justify-content:center; background:linear-gradient(#fbfcfd,#f3f5f7); border-radius:10px; padding:10px; margin-bottom:10px; }
  .vizcap{ font-size:12px; color:var(--muted); text-align:center; margin:-4px 0 12px; }
  svg{ max-width:100%; height:auto; }
  table{ width:100%; border-collapse:collapse; margin-top:14px; font-size:14px; }
  td,th{ text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); }
  th{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.03em; }
  td.num{ text-align:right; font-variant-numeric:tabular-nums; font-family:ui-monospace,Menlo,monospace; }
  .verdict{ margin-top:12px; padding:10px 12px; border-radius:8px; font-size:14px; font-weight:600; }
  .node{ margin-top:12px; font-size:13px; color:var(--muted); }
  .node b{ color:var(--green); }
  .kb{ margin-top:20px; }
  .kb h2{ font-size:15px; margin:0 0 4px; }
  .kb .src{ font-size:12px; color:var(--muted); margin:0 0 12px; }
  .kb .src a{ color:var(--green); text-decoration:none; }
  .kb-body{ font-size:14px; line-height:1.6; color:#e2e8f0; }
  .kb-body p{ margin:0 0 10px; }
  .kb-body ul{ margin:0 0 10px; padding-left:20px; }
  .kb-body li{ margin:2px 0; }
  .kb-block{ margin-bottom:16px; }
  .kb-body figure.kb-figure{ margin:18px 0; text-align:center; overflow-x:auto; -webkit-overflow-scrolling:touch; }
  .kb-body figure.kb-figure svg{ max-width:100%; height:auto; }
        /* Professional flowchart (Claude.ai-style grouped stage diagram) */
        .kb-body .cfs-flow {
            display: flex;
            flex-direction: column;
            align-items: stretch;
            gap: 0;
            margin: 22px 0 26px;
            font-family: 'Styrene B', 'Styrene A', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            -webkit-font-smoothing: antialiased;
        }
        .kb-body .cfs-flow-row {
            display: flex;
            justify-content: center;
            align-items: stretch;
            gap: 18px;
            flex-wrap: wrap;
        }
        .kb-body .cfs-node {
            flex: 1 1 220px;
            max-width: 340px;
            min-height: 62px;
            border-radius: 14px;
            padding: 14px 18px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            gap: 3px;
            border: 1px solid;
            box-sizing: border-box;
        }
        .kb-body .cfs-node-title { font-weight: 600; font-size: 16px; line-height: 1.25; }
        .kb-body .cfs-node-sub { font-size: 13.5px; opacity: .82; line-height: 1.3; }
        .kb-body .cfs-node.neutral { background: #2b2a27; border-color: #45433d; color: #efeee8; }
        .kb-body .cfs-node.green   { background: #14312a; border-color: #2f6d55; color: #d6f0e2; }
        .kb-body .cfs-node.green .cfs-node-title { color: #8fe3bd; }
        .kb-body .cfs-node.blue { background: #173f68; border-color: #2f74b8; color: #d7e6f7; }
        .kb-body .cfs-node.blue .cfs-node-title { color: #c4ddf7; }
        .kb-body .cfs-node.orange  { background: #4a2317; border-color: #8a3c22; color: #f3d8ca; }
        .kb-body .cfs-node.orange .cfs-node-title { color: #f0b79a; }
        .kb-body .cfs-arrow { display: flex; justify-content: center; align-items: center; height: 34px; color: #8a8880; }
        .kb-body .cfs-arrow svg { width: 18px; height: 30px; }
        .kb-body .cfs-arrow-row { display: flex; justify-content: center; gap: 18px; height: 34px; }
        .kb-body .cfs-arrow-col { flex: 1 1 220px; max-width: 340px; display: flex; justify-content: center; align-items: center; color: #8a8880; }
        .kb-body .cfs-arrow-col svg { width: 18px; height: 30px; }
        .kb-body .cfs-tier {
            border: 1px dashed #3d3d3a;
            border-radius: 16px;
            padding: 18px 20px 20px;
            margin: 0 0 16px;
        }
        .kb-body .cfs-tier:last-child { margin-bottom: 0; }
        .kb-body .cfs-tier-h {
            font-size: 17px;
            font-weight: 600;
            color: #f2f1ec;
            margin-bottom: 14px;
            font-family: 'Styrene B', 'Styrene A', ui-sans-serif, -apple-system, sans-serif;
        }
        .kb-body .cfs-tier-row {
            display: flex;
            gap: 14px;
            flex-wrap: wrap;
        }
        .kb-body .cfs-tier-row .cfs-node {
            flex: 1 1 150px;
            text-align: center;
        }
        .kb-body .cfs-die {
            border: 1px solid #3d3d3a; border-radius: 18px;
            background: rgba(255,255,255,0.03);
            padding: 20px 22px 22px; margin: 0;
        }
        .kb-body .cfs-die-h {
            text-align: center; font-size: 18px; font-weight: 600; color: #f2f1ec;
            font-family: 'Styrene B','Styrene A', ui-sans-serif, -apple-system, sans-serif;
        }
        .kb-body .cfs-die-note {
            text-align: center; font-size: 14px; color: #b8b7b1; margin: 2px 0 18px;
            font-family: 'Styrene B','Styrene A', ui-sans-serif, -apple-system, sans-serif;
        }
        .kb-body .cfs-die-top { display: flex; align-items: stretch; justify-content: center; gap: 0; }
        .kb-body .cfs-die-top .cfs-node { flex: 1 1 0; text-align: center; margin: 0; }
        .kb-body .cfs-die-in {
            display: flex; align-items: center; gap: 6px; white-space: nowrap;
            color: #b8b7b1; font-size: 14px; padding-right: 10px;
            font-family: 'Styrene B','Styrene A', ui-sans-serif, sans-serif;
        }
        .kb-body .cfs-die-ar, .kb-body .cfs-die-inar { display: flex; align-items: center; padding: 0 10px; color: #8a8880; }
        .kb-body .cfs-die-ar svg, .kb-body .cfs-die-inar svg { width: 34px; height: 16px; flex: 0 0 34px; }
        .kb-body .cfs-die-downs { display: flex; justify-content: space-around; padding: 6px 0 2px; }
        .kb-body .cfs-die-dar { display: flex; color: #8a8880; }
        .kb-body .cfs-die-dar svg { width: 16px; height: 28px; }
        .kb-body .cfs-die-base { margin-top: 2px; }
        .kb-body .cfs-die-base .cfs-node { width: 100%; max-width: 100%; text-align: center; margin: 0; }
        .kb-body .cfs-die-uar { display: flex; justify-content: center; padding: 4px 0; color: #8a8880; }
        .kb-body .cfs-die-uar svg { width: 16px; height: 28px; }
        .kb-body .cfs-die-feed {
            margin: 10px auto 0; max-width: 62%; text-align: center;
            padding: 12px 18px; background: #0f0f0e; border: 1px solid #3d3d3a;
            border-radius: 12px; color: #f2f1ec; font-weight: 600;
            font-family: 'Styrene B','Styrene A', ui-sans-serif, sans-serif;
        }
        .kb-body .cfs-group {
            background: #3b357a;
            border: 1px solid #4f4894;
            border-radius: 18px;
            padding: 16px 18px 18px;
            margin: 0 auto;
            width: 100%;
            max-width: 720px;
            box-sizing: border-box;
        }
        .kb-body .cfs-group-head { text-align: center; margin-bottom: 14px; }
        .kb-body .cfs-group-title { font-weight: 600; font-size: 17px; color: #eceaff; }
        .kb-body .cfs-group-note { font-size: 13.5px; color: #c3bdf0; margin-top: 2px; }
        .kb-body .cfs-group-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 14px 16px;
        }
        .kb-body .cfs-group-loop {
            text-align: center;
            font-size: 13.5px;
            color: #c3bdf0;
            margin-top: 14px;
        }
        .kb-body .cfs-cycle {
            display: grid;
            grid-template-columns: 1fr auto 1fr;
            grid-template-rows: auto auto auto;
            grid-template-areas:
                "tl ar tr"
                "lp x  dn"
                "bl al br";
            column-gap: 14px;
            row-gap: 10px;
            align-items: center;
        }
        .kb-body .cfs-cyc { display: flex; }
        .kb-body .cfs-cyc .cfs-node { width: 100%; max-width: 100%; }
        .kb-body .cfs-cyc.cyc-tl { grid-area: tl; }
        .kb-body .cfs-cyc.cyc-tr { grid-area: tr; }
        .kb-body .cfs-cyc.cyc-bl { grid-area: bl; }
        .kb-body .cfs-cyc.cyc-br { grid-area: br; }
        .kb-body .cfs-cyc-ar { display: flex; justify-content: center; align-items: center; color: #9c97c8; }
        .kb-body .cfs-cyc-ar.cyc-r { grid-area: ar; }
        .kb-body .cfs-cyc-ar.cyc-l { grid-area: al; }
        .kb-body .cfs-cyc-ar.cyc-d { grid-area: dn; }
        .kb-body .cfs-cyc-ar.cyc-r svg,
        .kb-body .cfs-cyc-ar.cyc-l svg { width: 30px; height: 16px; }
        .kb-body .cfs-cyc-ar.cyc-d svg { width: 16px; height: 28px; }
        .kb-body .cfs-cyc-loop {
            grid-area: lp;
            text-align: center;
            font-size: 13.5px;
            color: #c3bdf0;
        }
        @media (max-width: 620px) {
            .kb-body .cfs-flow-row { flex-direction: column; align-items: center; gap: 14px; }
            .kb-body .cfs-node { width: 100%; max-width: 100%; }
            .kb-body .cfs-tier-row { flex-direction: column; }
            .kb-body .cfs-tier-row .cfs-node { width: 100%; max-width: 100%; }
            .kb-body .cfs-die-top { flex-direction: column; }
            .kb-body .cfs-die-top .cfs-node { width: 100%; }
            .kb-body .cfs-die-ar { transform: rotate(90deg); align-self: center; }
            .kb-body .cfs-die-in { justify-content: center; padding: 0 0 8px; }
            .kb-body .cfs-die-feed { max-width: 100%; }
            .kb-body .cfs-group-grid { grid-template-columns: 1fr; }
            .kb-body .cfs-arrow-row { flex-direction: column; align-items: center; gap: 0; height: auto; }
            .kb-body .cfs-arrow-col { max-width: 100%; }
            .kb-body .cfs-cycle {
                grid-template-columns: 1fr;
                grid-template-areas: "tl" "tr" "br" "bl" "lp";
                row-gap: 12px;
            }
            .kb-body .cfs-cyc-ar { display: none; }
            .kb-body .cfs-cyc-loop { margin-top: 2px; }
        }

  .kb-body mjx-container[display="true"]{ overflow-x:auto; overflow-y:hidden; max-width:100%; margin:8px 0; }
  .kb-body mjx-container{ max-width:100%; }
  .kb-body .kb-tablewrap{ overflow-x:auto; margin:12px 0; -webkit-overflow-scrolling:touch; border:1px solid #e6e8eb; border-radius:8px; }
  .kb-body table.kb-table{ border-collapse:collapse; width:100%; font-size:13px; background:#fff; }
  .kb-body table.kb-table th, .kb-body table.kb-table td{ border-bottom:1px solid #eef1f4; border-right:1px solid #eef1f4; padding:8px 12px; text-align:left; vertical-align:top; white-space:nowrap; }
  .kb-body table.kb-table th:last-child, .kb-body table.kb-table td:last-child{ border-right:0; }
  .kb-body table.kb-table tbody tr:last-child td{ border-bottom:0; }
  .kb-body table.kb-table th{ background:#f5f6f8; font-weight:700; color:#16181d; text-transform:none; letter-spacing:0; font-size:12px; }
  .kb-body table.kb-table tbody tr:nth-child(even) td{ background:#fafbfc; }
  .api{ margin-top:26px; font-size:13px; color:var(--muted); line-height:1.6; }
  .api code,.api pre{ background:#0f1220; color:#e6e6ef; border-radius:6px; }
  .api code{ padding:1px 6px; font-size:12px; }
  .api pre{ padding:12px; overflow:auto; font-size:12px; }
  .err{ color:#c0392b; font-size:14px; padding:10px 0; }
</style>
<script>
  window.MathJax = {
    tex: { inlineMath: [['$','$'],['\\(','\\)']], displayMath: [['$$','$$'],['\\[','\\]']], processEscapes: true },
    options: { skipHtmlTags: ['script','noscript','style','textarea','pre','code'] },
    startup: { typeset: false }
  };
</script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js"></script>
</head>
<body>
<?php require_once __DIR__ . "/cfs_nav.php"; ?>

<div class="wrap">
  <h1>Network-on-Chip (NoC) Latency &amp; Throughput Simulator</h1>
  <p class="lede">
    Model <b>whether the on-chip network keeps up</b> with the cores it connects &mdash; then run it: the simulation
    executes on the ChipFoundryServices distributed compute pool. Cores, caches and memory controllers talk over a
    packet-switched fabric, usually a <code>k&times;k</code> <b>mesh</b> or <b>torus</b> of routers. A packet crossing
    <code>H</code> hops pays, per hop, a router pipeline plus a link traversal; its tail then serializes one flit per
    cycle &mdash; <b>zero-load latency</b> <code>T&#8320; = H&middot;(t_r + t_w) + L</code>. As offered load rises,
    contention adds <b>queueing delay</b> <code>&asymp; T&#8320;/(1 &minus; &rho;)</code>, the classic latency hockey
    stick that shoots up near saturation. <b>Throughput</b> under uniform traffic is bottlenecked at the network
    <b>bisection</b>: a mesh cuts into <code>2k</code> channels, so the per-node saturation injection rate falls like
    <code>~1/k</code> &mdash; bigger fabrics saturate at lower per-node bandwidth. A <b>torus</b> wraps around,
    doubling the bisection (and roughly the throughput) at the cost of longer links, while <b>virtual channels</b> and
    buffering recover a fraction of the ideal limit that head-of-line blocking would waste. Reduced-order educational
    model. See also the <a href="/clocktree">clock-tree</a>, <a href="/placeroute">place-and-route</a>,
    <a href="/pdn">power-delivery network</a>, <a href="/electromigration">electromigration</a>,
    <a href="/interconnect">interconnect RC</a>, <a href="/serdes">SerDes link</a>,
    <a href="/chiplet">chiplet / CoWoS</a>, <a href="/hbm">HBM bandwidth</a> and
    <a href="/systolic">systolic array</a> simulators and the <a href="/edge/">compute-pool status</a>.
  </p>

  <div class="grid">
    <!-- ============ controls ============ -->
    <form class="card" id="form" onsubmit="return false;">
      <div class="presets">
        <span class="preset" data-p="edge">Edge SoC</span>
        <span class="preset" data-p="cpu">Many-core CPU</span>
        <span class="preset" data-p="torus">Torus &middot; AI accelerator</span>
      </div>

      <label>Mesh radix (k&times;k) <span class="val" id="v_k">8&times;8</span></label>
      <input type="range" id="mesh_radix_k" min="2" max="32" step="1" value="8">

      <label>Topology <span class="val" id="v_topo">mesh</span></label>
      <input type="range" id="torus_enable" min="0" max="1" step="1" value="0">

      <label>Injection rate <span class="val" id="v_inj">0.15 fl/cyc</span></label>
      <input type="range" id="injection_rate" min="0.01" max="1" step="0.01" value="0.15">

      <label>Packet size <span class="val" id="v_pkt">5 flits</span></label>
      <input type="range" id="packet_size_flits" min="1" max="64" step="1" value="5">

      <label>Link width (flit) <span class="val" id="v_link">128 b</span></label>
      <input type="range" id="link_width_bits" min="32" max="512" step="32" value="128">

      <label>Router frequency <span class="val" id="v_freq">1500 MHz</span></label>
      <input type="range" id="router_freq_mhz" min="200" max="3000" step="50" value="1500">

      <label>Router pipeline <span class="val" id="v_stages">3 cyc/hop</span></label>
      <input type="range" id="router_stages" min="1" max="6" step="1" value="3">

      <label>Link traversal <span class="val" id="v_lc">1 cyc/hop</span></label>
      <input type="range" id="link_cycles" min="1" max="4" step="1" value="1">

      <label>Virtual channels <span class="val" id="v_vc">4</span></label>
      <input type="range" id="virtual_channels" min="1" max="16" step="1" value="4">

      <label>Buffer depth <span class="val" id="v_buf">8 flits</span></label>
      <input type="range" id="buffer_depth_flits" min="1" max="32" step="1" value="8">

      <label>Latency budget <span class="val" id="v_bud">100 ns</span></label>
      <input type="range" id="latency_budget_ns" min="10" max="500" step="10" value="100">

      <button class="run" id="run">Run simulation</button>
    </form>

    <!-- ============ results ============ -->
    <div class="card">
      <div class="viz" id="viz1"><svg viewBox="0 0 600 360" id="svg1"></svg></div>
      <div class="vizcap">Average packet <b>latency</b> vs <b>offered load</b> (flits/node/cycle) &mdash; the classic hockey stick. Zero-load latency is flat; as load nears the <b>saturation</b> injection rate (dashed amber) queueing delay diverges. It crosses the dashed red <b>budget</b> line where the network is too slow; the marker (&#9679;) is your operating point</div>
      <div class="viz" id="viz2"><svg viewBox="0 0 600 240" id="svg2"></svg></div>
      <div class="vizcap">Left: <b>saturation injection rate</b> vs <b>mesh radix</b> &mdash; the bisection caps per-node throughput at <code>~1/k</code>, so larger fabrics saturate lower (marker &#9679; = your k). Right: the <b>latency budget</b> &mdash; zero-load and average latency as a share of the budget, and offered load as a share of saturation (100&nbsp;% = at the edge)</div>
      <div id="err" class="err" style="display:none"></div>
      <div id="verdict" class="verdict" style="display:none"></div>
      <table id="metrics"><tbody></tbody></table>
      <div class="node" id="nodeline"></div>
    </div>
  </div>

  <!-- ============ knowledge-base explanation ============ -->
  <div class="card kb" id="kb" style="display:none">
    <h2>What these results mean</h2>
    <p class="src">Explanatory text pulled live from the <a href="/chat/">CFS knowledge base</a>.</p>
    <div class="kb-body" id="kbBody"></div>
  </div>

  <div class="api">
    <b>Developer API</b> — same simulation over HTTP (load-balanced across the pool):
    <pre>curl -X POST https://www.chipfoundryservices.com/edge/noc \
  -H "Content-Type: application/json" \
  -d '{"mesh_radix_k":8,"torus_enable":0,"injection_rate":0.15,"packet_size_flits":5,
       "link_width_bits":128,"router_freq_mhz":1500,"router_stages":3,"link_cycles":1,
       "virtual_channels":4,"buffer_depth_flits":8,"latency_budget_ns":100}'</pre>
    Returns JSON with <code>outputs</code> (num_nodes, avg_hops, diameter_hops, zero_load_latency_ns, avg_latency_ns,
    serialization_latency_ns, hop_latency_ns, queueing_latency_ns, load_percent, saturation_injection_rate,
    accepted_injection_rate, routing_efficiency_percent, flit_bytes, link_bandwidth_gbps, bisection_channels,
    bisection_bandwidth_gbps, injected_bw_per_node_gbps, aggregate_throughput_gbps, meets_budget,
    latency_headroom_percent, dominant_component, topology, verdict), the full <code>profile</code> (48-point
    <code>latency_vs_load</code> sweep <code>[injection_rate, avg_latency_ns]</code> and 48-point
    <code>sat_vs_radix</code> sweep <code>[mesh_radix_k, saturation_injection_rate]</code>), the serving
    <code>node</code>, and <code>compute_ms</code>. Endpoint aliases <code>/edge/networkonchip</code>,
    <code>/edge/interconnectfabric</code>, <code>/edge/meshnetwork</code>, <code>/edge/torus</code>,
    <code>/edge/routerlatency</code>, <code>/edge/bisection</code>.
  </div>
</div>

<script>
const $ = id => document.getElementById(id);
const fields = ["mesh_radix_k","torus_enable","injection_rate","packet_size_flits","link_width_bits","router_freq_mhz","router_stages","link_cycles","virtual_channels","buffer_depth_flits","latency_budget_ns"];
const labels = {mesh_radix_k:"v_k", torus_enable:"v_topo", injection_rate:"v_inj", packet_size_flits:"v_pkt", link_width_bits:"v_link", router_freq_mhz:"v_freq", router_stages:"v_stages", link_cycles:"v_lc", virtual_channels:"v_vc", buffer_depth_flits:"v_buf", latency_budget_ns:"v_bud"};

function syncLabels(){
  for(const f of fields){
    let v = parseFloat($(f).value), out;
    if(f==="mesh_radix_k")          out = v.toFixed(0) + "&times;" + v.toFixed(0) + " (" + (v*v).toFixed(0) + ")";
    else if(f==="torus_enable")     out = v>=0.5 ? "torus" : "mesh";
    else if(f==="injection_rate")   out = v.toFixed(2) + " fl/cyc";
    else if(f==="packet_size_flits")out = v.toFixed(0) + " flits";
    else if(f==="link_width_bits")  out = v.toFixed(0) + " b";
    else if(f==="router_freq_mhz")  out = v.toFixed(0) + " MHz";
    else if(f==="router_stages")    out = v.toFixed(0) + " cyc/hop";
    else if(f==="link_cycles")      out = v.toFixed(0) + " cyc/hop";
    else if(f==="virtual_channels") out = v.toFixed(0);
    else if(f==="buffer_depth_flits")out = v.toFixed(0) + " flits";
    else if(f==="latency_budget_ns")out = v.toFixed(0) + " ns";
    else out = v;
    $(labels[f]).innerHTML = out;
  }
}
fields.forEach(f => $(f).addEventListener("input", syncLabels));
syncLabels();

const presets = {
  // k, torus, inj, pkt, link, freq, stages, link_cyc, vc, buf, budget
  edge:  {mesh_radix_k:4,  torus_enable:0, injection_rate:0.20, packet_size_flits:4,  link_width_bits:128, router_freq_mhz:1000, router_stages:2, link_cycles:1, virtual_channels:2,  buffer_depth_flits:4,  latency_budget_ns:80},
  cpu:   {mesh_radix_k:8,  torus_enable:0, injection_rate:0.15, packet_size_flits:5,  link_width_bits:128, router_freq_mhz:1500, router_stages:3, link_cycles:1, virtual_channels:4,  buffer_depth_flits:8,  latency_budget_ns:100},
  torus: {mesh_radix_k:16, torus_enable:1, injection_rate:0.10, packet_size_flits:8,  link_width_bits:256, router_freq_mhz:2000, router_stages:3, link_cycles:1, virtual_channels:8,  buffer_depth_flits:16, latency_budget_ns:150},
};
document.querySelectorAll(".preset").forEach(el=>{
  el.onclick = () => { const p = presets[el.dataset.p]; for(const k in p) $(k).value = p[k]; syncLabels(); run(); };
});

function fmt(n, d=1){ return Number(n).toLocaleString(undefined,{maximumFractionDigits:d}); }
function fmtSig(n){
  const a = Math.abs(n);
  if(a===0) return "0";
  if(a>=1000) return fmt(n,0);
  if(a>=1) return fmt(n,1);
  if(a>=0.01) return fmt(n,3);
  return n.toExponential(1);
}

// ================= NoC drawing block =================

// ---- viz 1: average latency (ns) vs offered load (flits/node/cycle) ----
function drawLatency(r){
  const o = r.outputs, p = r.profile, inp = r.inputs;
  const lv = p.latency_vs_load;                       // [[injection_rate, avg_latency_ns], ...]
  const budget = o.__budget_ns = inp.latency_budget_ns;
  const VW=600, VH=360, padL=54, padR=150, padT=20, padB=46;
  const xmin = lv[0][0], xmax = lv[lv.length-1][0];
  let ymax = Math.max(budget, o.avg_latency_ns, o.zero_load_latency_ns);
  lv.forEach(d => { ymax = Math.max(ymax, Math.min(d[1], budget*3)); });  // clip the asymptote for a readable scale
  ymax *= 1.1;
  const X = v => padL + (v-xmin)/(xmax-xmin) * (VW-padL-padR);
  const Y = v => padT + (1 - Math.min(Math.max(v,0),ymax)/ymax) * (VH-padT-padB);
  const parts = [];
  // vertical gridlines every 0.1 fl/cyc
  for(let g=Math.ceil(xmin/0.1)*0.1; g<=xmax+1e-9; g+=0.1){
    const xx = X(g);
    parts.push(`<line x1="${xx.toFixed(1)}" y1="${padT}" x2="${xx.toFixed(1)}" y2="${(VH-padB).toFixed(1)}" stroke="#1e2e4a" stroke-width="1"/>`);
    parts.push(`<text x="${xx.toFixed(1)}" y="${(VH-padB+13).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="middle">${g.toFixed(1)}</text>`);
  }
  // horizontal gridlines
  for(let i=0;i<=4;i++){
    const gv = ymax*i/4, yy = Y(gv);
    parts.push(`<line x1="${padL}" y1="${yy.toFixed(1)}" x2="${(VW-padR).toFixed(1)}" y2="${yy.toFixed(1)}" stroke="#1e2e4a" stroke-width="1"/>`);
    parts.push(`<text x="${(padL-6).toFixed(1)}" y="${(yy+3).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">${gv.toFixed(0)}</text>`);
  }
  // axes
  parts.push(`<line x1="${padL}" y1="${padT}" x2="${padL}" y2="${(VH-padB).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<line x1="${padL}" y1="${(VH-padB).toFixed(1)}" x2="${(VW-padR).toFixed(1)}" y2="${(VH-padB).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  // budget line (dashed red)
  parts.push(`<line x1="${padL}" y1="${Y(budget).toFixed(1)}" x2="${(VW-padR).toFixed(1)}" y2="${Y(budget).toFixed(1)}" stroke="#c0392b" stroke-width="1.4" stroke-dasharray="5 3"/>`);
  parts.push(`<text x="${(padL+4).toFixed(1)}" y="${(Y(budget)-4).toFixed(1)}" fill="#c0392b" font-size="9">budget ${fmt(budget,0)} ns</text>`);
  // saturation vertical (dashed amber)
  const sat = o.saturation_injection_rate;
  if(sat>=xmin && sat<=xmax){
    parts.push(`<line x1="${X(sat).toFixed(1)}" y1="${padT}" x2="${X(sat).toFixed(1)}" y2="${(VH-padB).toFixed(1)}" stroke="#9a6b00" stroke-width="1.2" stroke-dasharray="3 3"/>`);
    parts.push(`<text x="${(X(sat)-3).toFixed(1)}" y="${(padT+10).toFixed(1)}" fill="#9a6b00" font-size="8.5" text-anchor="end">sat ${fmt(sat,2)}</text>`);
  }
  // latency curve (green), clipped to ymax
  { let d=""; lv.forEach((pt,i)=>{ d += (i?"L":"M") + X(pt[0]).toFixed(1) + " " + Y(pt[1]).toFixed(1) + " "; });
    parts.push(`<path d="${d}" fill="none" stroke="#07C160" stroke-width="2.8"/>`); }
  // operating point at current injection rate
  const iop = Math.min(Math.max(inp.injection_rate, xmin), xmax);
  const fail = !o.meets_budget;
  const opColor = fail ? "#c0392b" : "#16181d";
  const yop = Y(o.avg_latency_ns);
  parts.push(`<line x1="${X(iop).toFixed(1)}" y1="${padT}" x2="${X(iop).toFixed(1)}" y2="${(VH-padB).toFixed(1)}" stroke="${opColor}" stroke-width="1" stroke-dasharray="3 3"/>`);
  parts.push(`<circle cx="${X(iop).toFixed(1)}" cy="${yop.toFixed(1)}" r="5" fill="${opColor}"/>`);
  parts.push(`<text x="${(X(iop)+8).toFixed(1)}" y="${(yop-6).toFixed(1)}" fill="${opColor}" font-size="10" font-weight="700">${fmt(o.avg_latency_ns,1)} ns @ ${fmt(inp.injection_rate,2)}</text>`);
  // legend
  const lx = VW-padR+12; let ly = padT+8;
  const line = (color,dash,txt,wd) => { parts.push(`<line x1="${lx}" y1="${ly}" x2="${lx+20}" y2="${ly}" stroke="${color}" stroke-width="${wd||2.4}" ${dash?'stroke-dasharray="4 3"':''}/>`); parts.push(`<text x="${lx+26}" y="${ly+3}" fill="#94a3b8" font-size="10">${txt}</text>`); ly+=17; };
  line("#07C160", false, "latency", 2.8);
  line("#c0392b", true, "budget");
  line("#9a6b00", true, "saturation");
  ly += 6;
  const kv = (k,v,col) => { parts.push(`<text x="${lx}" y="${ly+3}" fill="#94a3b8" font-size="10">${k}</text>`); ly+=13; parts.push(`<text x="${lx}" y="${ly+3}" fill="${col||'#f1f5f9'}" font-size="12" font-weight="700">${v}</text>`); ly+=20; };
  kv("avg latency", fmt(o.avg_latency_ns,1)+" ns", fail?"#c0392b":"#34d399");
  kv("load", fmt(o.load_percent,0)+"%", fail?"#c0392b":"#34d399");
  kv("meets budget", o.meets_budget?"yes":"no", fail?"#c0392b":"#34d399");
  // axis labels
  parts.push(`<text x="${((padL+VW-padR)/2).toFixed(1)}" y="${(VH-4).toFixed(1)}" fill="#94a3b8" font-size="11" text-anchor="middle">offered load (flits/node/cycle)</text>`);
  parts.push(`<text x="14" y="${((padT+VH-padB)/2).toFixed(1)}" fill="#94a3b8" font-size="11" text-anchor="middle" transform="rotate(-90 14 ${((padT+VH-padB)/2).toFixed(1)})">average latency (ns)</text>`);
  $("viz1").innerHTML = '<svg viewBox="0 0 '+VW+' '+VH+'" width="100%" xmlns="http://www.w3.org/2000/svg" id="svg1">'+parts.join("")+'</svg>';
}

// ---- horizontal budget bars: value as a share of its limit (100% = edge) ----
function budgetBars(parts, box){
  return function(items){
    const n = items.length, gap = 16;
    const bh = (box.h - gap*(n-1)) / n;
    items.forEach((it,i)=>{
      const y = box.y0 + i*(bh+gap);
      const frac = Math.max(0, Math.min(1, it.v/100));
      parts.push(`<text x="${box.x0}" y="${(y+bh*0.5-2).toFixed(1)}" fill="#e2e8f0" font-size="10.5" font-weight="600">${it.k}</text>`);
      parts.push(`<rect x="${box.x0}" y="${(y+bh*0.5+2).toFixed(1)}" width="${box.w.toFixed(1)}" height="12" rx="6" fill="#142138"/>`);
      parts.push(`<rect x="${box.x0}" y="${(y+bh*0.5+2).toFixed(1)}" width="${(box.w*frac).toFixed(1)}" height="12" rx="6" fill="${it.color}"/>`);
      parts.push(`<text x="${(box.x0+box.w).toFixed(1)}" y="${(y+bh*0.5-2).toFixed(1)}" fill="${it.color}" font-size="11" font-weight="700" text-anchor="end">${fmt(it.v,0)} %</text>`);
    });
  };
}

// ---- viz 2: saturation vs radix + latency budget bars ----
function drawSatBudget(r){
  const o = r.outputs, p = r.profile, inp = r.inputs;
  const sv = p.sat_vs_radix;                          // [[mesh_radix_k, saturation_injection_rate], ...]
  const VW=600, VH=240, parts = [];
  const gap=48, w=(VW-gap*3)/2, h=150, y0=34;
  // ---- left panel: saturation vs radix ----
  const bx = {x0:gap, y0:y0, w:w, h:h};
  const kmin = sv[0][0], kmax = sv[sv.length-1][0];
  let shi = o.saturation_injection_rate;
  sv.forEach(d => { shi = Math.max(shi, d[1]); });
  shi *= 1.15;
  const X = v => bx.x0 + ((v-kmin)/(kmax-kmin)) * bx.w;
  const Y = v => bx.y0 + (1 - Math.min(Math.max(v,0),shi)/shi) * bx.h;
  parts.push(`<text x="${(bx.x0+bx.w/2).toFixed(1)}" y="${(y0-4).toFixed(1)}" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Saturation vs mesh radix</text>`);
  // vertical gridlines every 8
  for(let t=Math.ceil(kmin/8)*8; t<=kmax; t+=8){
    const xx = X(t);
    parts.push(`<line x1="${xx.toFixed(1)}" y1="${bx.y0}" x2="${xx.toFixed(1)}" y2="${(bx.y0+bx.h).toFixed(1)}" stroke="#1e2e4a" stroke-width="1"/>`);
    parts.push(`<text x="${xx.toFixed(1)}" y="${(bx.y0+bx.h+11).toFixed(1)}" fill="#94a3b8" font-size="8" text-anchor="middle">${t}</text>`);
  }
  // horizontal gridlines
  for(let i=0;i<=3;i++){
    const gv = shi*i/3, yy = Y(gv);
    parts.push(`<line x1="${bx.x0}" y1="${yy.toFixed(1)}" x2="${(bx.x0+bx.w).toFixed(1)}" y2="${yy.toFixed(1)}" stroke="#1e2e4a" stroke-width="1"/>`);
    parts.push(`<text x="${(bx.x0-4).toFixed(1)}" y="${(yy+3).toFixed(1)}" fill="#94a3b8" font-size="8" text-anchor="end">${fmtSig(gv)}</text>`);
  }
  parts.push(`<line x1="${bx.x0}" y1="${bx.y0}" x2="${bx.x0}" y2="${(bx.y0+bx.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<line x1="${bx.x0}" y1="${(bx.y0+bx.h).toFixed(1)}" x2="${(bx.x0+bx.w).toFixed(1)}" y2="${(bx.y0+bx.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  // sat(radix) curve (green)
  { let d=""; sv.forEach((pt,i)=>{ d += (i?"L":"M") + X(pt[0]).toFixed(1) + " " + Y(pt[1]).toFixed(1) + " "; });
    parts.push(`<path d="${d}" fill="none" stroke="#07C160" stroke-width="2.4"/>`); }
  // operating point (radix, saturation)
  const kop = Math.min(Math.max(inp.mesh_radix_k, kmin), kmax);
  const c = o.meets_budget ? "#16181d" : "#c0392b";
  parts.push(`<line x1="${X(kop).toFixed(1)}" y1="${bx.y0}" x2="${X(kop).toFixed(1)}" y2="${(bx.y0+bx.h).toFixed(1)}" stroke="${c}" stroke-width="1" stroke-dasharray="3 3"/>`);
  parts.push(`<circle cx="${X(kop).toFixed(1)}" cy="${Y(o.saturation_injection_rate).toFixed(1)}" r="4.5" fill="${c}"/>`);
  parts.push(`<text x="${(bx.x0+bx.w/2).toFixed(1)}" y="${(bx.y0+bx.h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">mesh radix k &rarr; bigger fabrics saturate lower (~1/k)</text>`);
  // ---- right panel: latency budget bars ----
  const rx = gap*2+w;
  parts.push(`<text x="${(rx+w/2).toFixed(1)}" y="${(y0-4).toFixed(1)}" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Latency &amp; load budget</text>`);
  const bud = inp.latency_budget_ns;
  const zlPct  = bud>0 ? o.zero_load_latency_ns/bud*100 : 0;
  const avgPct = bud>0 ? o.avg_latency_ns/bud*100 : 0;
  const loadPct = o.load_percent;
  const zlColor  = zlPct<=100 ? "#3f78c4" : "#c0392b";
  const avgColor = avgPct<=100 ? "#34d399" : "#c0392b";
  const loadColor = loadPct<=100 ? "#34d399" : "#c0392b";
  budgetBars(parts, {x0:rx, y0:y0+6, w:w, h:h-6})([
    {k:"zero-load / budget", v:zlPct,   color:zlColor},
    {k:"avg latency / budget", v:avgPct, color:avgColor},
    {k:"offered / saturation", v:loadPct, color:loadColor},
  ]);
  parts.push(`<text x="${(rx+w/2).toFixed(1)}" y="${(y0+h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">share of the budget; load &gt; 100 % &rArr; saturated</text>`);
  $("viz2").innerHTML = '<svg viewBox="0 0 '+VW+' '+VH+'" width="100%" xmlns="http://www.w3.org/2000/svg" id="svg2">'+parts.join("")+'</svg>';
}
// ================= end NoC block =================

function showMetrics(r){
  const o = r.outputs, inp = r.inputs;
  const rows = [
    ["Network size", fmt(o.num_nodes,0)+" nodes ("+fmt(inp.mesh_radix_k,0)+"&times;"+fmt(inp.mesh_radix_k,0)+" "+o.topology+")"],
    ["Average hops", fmt(o.avg_hops,2)],
    ["Network diameter", fmt(o.diameter_hops,0)+" hops"],
    ["Zero-load latency", fmt(o.zero_load_latency_ns,2)+" ns"],
    ["Average latency", fmt(o.avg_latency_ns,2)+" ns"],
    ["&nbsp;&nbsp;hop latency", fmt(o.hop_latency_ns,2)+" ns"],
    ["&nbsp;&nbsp;serialization", fmt(o.serialization_latency_ns,2)+" ns"],
    ["&nbsp;&nbsp;queueing", fmt(o.queueing_latency_ns,2)+" ns"],
    ["Offered load", fmt(o.load_percent,1)+" % of saturation"],
    ["Saturation injection", fmt(o.saturation_injection_rate,3)+" fl/node/cyc"],
    ["Accepted injection", fmt(o.accepted_injection_rate,3)+" fl/node/cyc"],
    ["Routing efficiency", fmt(o.routing_efficiency_percent,1)+" %"],
    ["Link bandwidth", fmt(o.link_bandwidth_gbps,0)+" Gbps ("+fmt(o.flit_bytes,0)+" B/flit)"],
    ["Bisection bandwidth", fmt(o.bisection_bandwidth_gbps,0)+" Gbps ("+fmt(o.bisection_channels,0)+" ch)"],
    ["Aggregate throughput", fmt(o.aggregate_throughput_gbps,0)+" Gbps"],
    ["Meets budget", o.meets_budget ? "yes" : "no &mdash; violation"],
    ["Dominant contributor", o.dominant_component],
  ];
  $("metrics").innerHTML = "<thead><tr><th>Metric</th><th style='text-align:right'>Value</th></tr></thead><tbody>"
    + rows.map(x=>`<tr><td>${x[0]}</td><td class="num">${x[1]}</td></tr>`).join("") + "</tbody>";

  const v = $("verdict");
  v.style.display = "block";
  v.textContent = "Result: " + o.verdict;
  if(/^Clean/.test(o.verdict)){ v.style.background="#e7f7ee"; v.style.color="#34d399"; }
  else if(/^Saturated/.test(o.verdict) || /^Latency violation/.test(o.verdict)){ v.style.background="#fdecea"; v.style.color="#c0392b"; }
  else { v.style.background="#fff6e6"; v.style.color="#9a6b00"; }

  $("nodeline").innerHTML = `Computed on <b>professional Chip Foundry Services</b> compute in ${fmt(r.compute_ms,0)} ms &middot; ${r.steps} sweep steps &middot; <span style="opacity:.8">${r.model}</span>`;
}

// ---- professional flowchart renderer (ported from the site's chat) ----
const escapeHtml = (t) => String(t).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]));
const applyBold = (raw) => escapeHtml(String(raw)).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
            const renderFlowchart = (jsonStr) => {
                let spec;
                try { spec = JSON.parse(jsonStr); } catch (e) { return `<pre><code>${escapeHtml(jsonStr)}</code></pre>`; }
                const rows = (spec && spec.rows) || [];
                const arrowSvg = '<svg viewBox="0 0 18 30" fill="none"><path d="M9 0 V24" stroke="currentColor" stroke-width="1.6"/><path d="M3 18 L9 26 L15 18" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round" stroke-linecap="round"/></svg>';
                const arrowRight = '<svg viewBox="0 0 34 16" fill="none"><path d="M0 8 H27" stroke="currentColor" stroke-width="1.6"/><path d="M21 2 L29 8 L21 14" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round" stroke-linecap="round"/></svg>';
                const arrowLeft = '<svg viewBox="0 0 34 16" fill="none"><path d="M34 8 H7" stroke="currentColor" stroke-width="1.6"/><path d="M13 2 L5 8 L13 14" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round" stroke-linecap="round"/></svg>';
                const arrowDownSm = '<svg viewBox="0 0 16 28" fill="none"><path d="M8 0 V22" stroke="currentColor" stroke-width="1.6"/><path d="M2 16 L8 24 L14 16" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round" stroke-linecap="round"/></svg>';
                const arrowUpSm = '<svg viewBox="0 0 16 28" fill="none"><path d="M8 28 V6" stroke="currentColor" stroke-width="1.6"/><path d="M2 12 L8 4 L14 12" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round" stroke-linecap="round"/></svg>';
                const connector = (count) => {
                    if (!count || count <= 1) return `<div class="cfs-arrow">${arrowSvg}</div>`;
                    let a = '<div class="cfs-arrow-row">';
                    for (let k = 0; k < count; k++) a += `<div class="cfs-arrow-col">${arrowSvg}</div>`;
                    return a + '</div>';
                };
                const node = (n) => {
                    const tone = ['neutral', 'green', 'orange', 'blue'].indexOf(n.tone) >= 0 ? n.tone : 'neutral';
                    const sub = n.sub ? `<span class="cfs-node-sub">${applyBold(String(n.sub))}</span>` : '';
                    return `<div class="cfs-node ${tone}"><span class="cfs-node-title">${applyBold(String(n.title || ''))}</span>${sub}</div>`;
                };
                let out = '<div class="cfs-flow">';
                let prevType = null, prevCount = 0;
                rows.forEach((r, idx) => {
                    if (r.type === 'arrow') { out += connector(prevCount); prevType = 'arrow'; return; }
                    if (idx > 0 && r.type !== 'tier' && r.type !== 'die' && (prevType === 'nodes' || prevType === 'group')) out += connector(prevCount);
                    if (r.type === 'die') {
                        out += '<div class="cfs-die">';
                        out += `<div class="cfs-die-h">${applyBold(String(r.title || ''))}</div>`;
                        if (r.note) out += `<div class="cfs-die-note">${applyBold(String(r.note))}</div>`;
                        out += '<div class="cfs-die-top">';
                        if (r.input) out += `<div class="cfs-die-in">${applyBold(String(r.input))}<span class="cfs-die-inar">${arrowRight}</span></div>`;
                        (r.top || []).forEach((n, i) => {
                            if (i > 0) out += `<span class="cfs-die-ar">${arrowRight}</span>`;
                            out += node(n);
                        });
                        out += '</div>';
                        if (r.base) {
                            out += '<div class="cfs-die-downs">';
                            (r.top || []).forEach(() => { out += `<span class="cfs-die-dar">${arrowDownSm}</span>`; });
                            out += '</div>';
                            out += `<div class="cfs-die-base">${node(r.base)}</div>`;
                        }
                        out += '</div>';
                        if (r.feed) {
                            out += `<div class="cfs-die-uar">${arrowUpSm}</div>`;
                            out += `<div class="cfs-die-feed">${applyBold(String(r.feed.title || ''))}</div>`;
                        }
                        prevType = 'die'; prevCount = 0;
                        return;
                    }
                    if (r.type === 'tier') {
                        out += '<div class="cfs-tier">';
                        out += `<div class="cfs-tier-h">${applyBold(String(r.title || ''))}</div>`;
                        out += '<div class="cfs-tier-row">';
                        (r.items || []).forEach(n => { out += node(n); });
                        out += '</div></div>';
                        prevType = 'tier'; prevCount = 0;
                        return;
                    }
                    if (r.type === 'group') {
                        out += '<div class="cfs-group"><div class="cfs-group-head">';
                        out += `<div class="cfs-group-title">${applyBold(String(r.title || ''))}</div>`;
                        if (r.note) out += `<div class="cfs-group-note">${applyBold(String(r.note))}</div>`;
                        out += '</div>';
                        const gi = r.items || [];
                        if (r.cycle && gi.length >= 4) {
                            out += '<div class="cfs-cycle">';
                            out += `<div class="cfs-cyc cyc-tl">${node(gi[0])}</div>`;
                            out += `<div class="cfs-cyc-ar cyc-r">${arrowRight}</div>`;
                            out += `<div class="cfs-cyc cyc-tr">${node(gi[1])}</div>`;
                            out += `<div class="cfs-cyc-loop">${applyBold(String(r.loop || ''))}</div>`;
                            out += `<div class="cfs-cyc-ar cyc-d">${arrowDownSm}</div>`;
                            out += `<div class="cfs-cyc cyc-bl">${node(gi[3])}</div>`;
                            out += `<div class="cfs-cyc-ar cyc-l">${arrowLeft}</div>`;
                            out += `<div class="cfs-cyc cyc-br">${node(gi[2])}</div>`;
                            out += '</div>';
                        } else {
                            out += '<div class="cfs-group-grid">';
                            gi.forEach(n => { out += node(n); });
                            out += '</div>';
                            if (r.loop) out += `<div class="cfs-group-loop">${applyBold(String(r.loop))}</div>`;
                        }
                        out += '</div>';
                        prevType = 'group'; prevCount = 1;
                        return;
                    }
                    out += '<div class="cfs-flow-row">';
                    (r.items || []).forEach(n => { out += node(n); });
                    out += '</div>';
                    prevType = 'nodes'; prevCount = (r.items || []).length;
                });
                out += '</div>';
                return out;
            };

// ---- minimal, safe markdown -> HTML for KB text ----
function mdToHtml(s){
  s = String(s);
  s = s.replace(/\\r\\n|\\r|\\n/g, "\n").replace(/\\t/g, "  ");
  const raw = [];
  const RSENT = "\u0000R";
  s = s.replace(/```flowchart[ \t]*\n([\s\S]*?)```/gi, (_, code) => {
    const i = raw.length; raw.push(renderFlowchart(code.trim())); return "\n\n" + RSENT + i + "\u0000\n\n";
  });
  s = s.replace(/```(?:svg|html)[ \t]*\n([\s\S]*?)```/gi, (_, code) => {
    const i = raw.length; raw.push(code.trim()); return "\n\n" + RSENT + i + "\u0000\n\n";
  });
  s = s.replace(/<svg[\s\S]*?<\/svg>/gi, (m) => {
    const i = raw.length; raw.push(m); return "\n\n" + RSENT + i + "\u0000\n\n";
  });
  const math = [];
  const SENT = "\u0000M";
  const patterns = [/\$\$[\s\S]+?\$\$/g, /\\\[[\s\S]+?\\\]/g, /\\\([\s\S]+?\\\)/g, /\$[^\$\n]+?\$/g];
  for(const re of patterns){
    s = s.replace(re, m => { const i = math.length; math.push(m); return SENT + i + "\u0000"; });
  }
  s = s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
  const blocks = s.split(/\n\n+/).map(b=>{
    b = b.trim(); if(!b) return "";
    const _tl = b.split(/\n/).map(l=>l.trim()).filter(l=>l);
    if(_tl.length>=2 && _tl.every(l=>l.startsWith("|")) && /-/.test(_tl[1]) && /^\|?[\s:|-]+\|?$/.test(_tl[1])){
      const _bold = c => c.replace(/\*\*(.+?)\*\*/g,"<b>$1</b>");
      const _cells = l => l.replace(/^\|/,"").replace(/\|$/,"").split("|").map(c=>_bold(c.trim()));
      const _head = _cells(_tl[0]).map(c=>`<th>${c}</th>`).join("");
      const _rows = _tl.slice(2).map(l=>"<tr>"+_cells(l).map(c=>`<td>${c}</td>`).join("")+"</tr>").join("");
      return `<div class="kb-tablewrap"><table class="kb-table"><thead><tr>${_head}</tr></thead><tbody>${_rows}</tbody></table></div>`;
    }
    b = b.replace(/\*\*(.+?)\*\*/g,"<b>$1</b>");
    if(/^-\s/m.test(b) && b.split(/\n/).every(l=>/^-\s/.test(l.trim())||l.trim()==="")){
      const items = b.split(/\n/).filter(l=>l.trim()).map(l=>`<li>${l.replace(/^-\s*/,"")}</li>`).join("");
      return `<ul>${items}</ul>`;
    }
    return `<p>${b.replace(/\n/g,"<br>")}</p>`;
  });
  let html = blocks.join("");
  html = html.replace(/\u0000M(\d+)\u0000/g, (_, i) => {
    return math[+i].replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
  });
  html = html.replace(/<p>\s*\u0000R(\d+)\u0000\s*<\/p>/g, (_, i) => `<figure class="kb-figure">${raw[+i]}</figure>`);
  html = html.replace(/\u0000R(\d+)\u0000/g, (_, i) => `<figure class="kb-figure">${raw[+i]}</figure>`);
  return html;
}

function typesetKB(){
  const el = document.getElementById("kbBody");
  if(!el) return;
  if(window.MathJax && MathJax.typesetPromise){ MathJax.typesetPromise([el]).catch(()=>{}); }
  else { setTimeout(typesetKB, 200); }
}

async function loadKB(r){
  const o = r.outputs;
  const second = o.topology === "torus" ? "torus network" : "interconnect fabric";
  const queries = [["Network on chip","network on chip"], ["", second]];
  const seen = {};
  const out = [];
  for(const [title,q] of queries){
    try{
      const res = await fetch("/api.php?q=" + encodeURIComponent(q));
      const j = await res.json();
      let txt = (j && j.response) ? j.response.trim() : "";
      if(!txt) continue;
      const key = txt.slice(0,60);
      if(seen[key]) continue; seen[key] = 1;
      out.push(`<div class="kb-block">${mdToHtml(txt)}</div>`);
    }catch(e){ /* KB optional — skip on failure */ }
  }
  if(out.length){
    $("kbBody").innerHTML = out.join("");
    $("kb").style.display = "block"; typesetKB();
  }
}

async function run(){
  const btn = $("run"); btn.disabled = true; btn.textContent = "Running on pool…";
  $("err").style.display = "none";
  const body = {};
  for(const f of fields) body[f] = parseFloat($(f).value);
  try{
    const res = await fetch("/edge/noc", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(body)});
    if(!res.ok) throw new Error("HTTP "+res.status);
    const r = await res.json();
    if(r.error) throw new Error(r.error);
    drawLatency(r); drawSatBudget(r); showMetrics(r);
    loadKB(r);
  }catch(e){
    $("err").style.display = "block";
    $("err").textContent = "Simulation failed: " + e.message + " (the compute pool may be temporarily unavailable — try again).";
  }finally{
    btn.disabled = false; btn.textContent = "Run simulation";
  }
}
$("run").onclick = run;
run();   // run once on load with defaults
</script>
</body>
</html>
