<?php
// Chiplet / advanced-packaging simulator — customer-facing page.
// Heavy lifting runs on the ChipFoundryServices compute pool: this page POSTs
// the design area (monolith-equivalent mm^2), the number of chiplets, the wafer
// defect density and clustering, the packaging class (organic MCM / EMIB bridge /
// silicon interposer / 3D hybrid bond), known-good-die test coverage, per-join
// assembly yield and the required die-to-die bandwidth to /edge/chiplet (same-
// origin), NGINX routes it to the compute node, the node computes the monolithic
// vs disaggregated defect-limited yield (negative-binomial), the system yield
// after KGD screening and assembly, the cost per good part, the beachfront D2D
// bandwidth and interconnect energy, the optimal chiplet count, and the regime
// (monolithic-better / beachfront-limited / assembly-yield-limited), and returns
// JSON, and we render it here. 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>Chiplet &amp; Advanced Packaging Yield Simulator | CFS</title>
<link rel="stylesheet" href="/cfs_theme.css?v=2">
<meta name="description" content="Compare monolithic and chiplet yield, known-good-die screening, assembly yield, cost, die-to-die bandwidth, and advanced packaging options online.">
<meta name="keywords" content="chiplet simulator, advanced packaging simulator, chiplet yield calculator, known good die, die-to-die bandwidth, CoWoS, EMIB, hybrid bonding">
<link rel="canonical" href="https://www.chipfoundryservices.com/chiplet">
<meta property="og:type" content="website"><meta property="og:site_name" content="ChipFoundryServices"><meta property="og:title" content="Chiplet &amp; Advanced Packaging Yield Simulator"><meta property="og:description" content="Compare monolithic and chiplet yield, cost, assembly loss, and die-to-die performance."><meta property="og:url" content="https://www.chipfoundryservices.com/chiplet"><meta property="og:image" content="https://www.chipfoundryservices.com/og-image.png">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Chiplet &amp; Advanced Packaging Yield Simulator"><meta name="twitter:description" content="Free online chiplet yield, cost, and packaging trade-off simulator."><meta name="twitter:image" content="https://www.chipfoundryservices.com/og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Chiplet and Advanced Packaging Yield Simulator","url":"https://www.chipfoundryservices.com/chiplet","applicationCategory":"EngineeringApplication","operatingSystem":"Web browser","description":"Online chiplet yield, cost, assembly, and die-to-die packaging simulator.","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"publisher":{"@type":"Organization","name":"ChipFoundryServices LLC","url":"https://www.chipfoundryservices.com/"}}</script>
<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;
            /* Anthropic/Claude.ai UI font — Styrene, NOT the serif answer body */
            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; }
        /* tones */
        .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; }
        /* connecting arrow */
        .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; }
        /* parallel arrows — one under each node column, matching the node row layout */
        .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; }
        /* tiered landscape row: labeled dashed band of peer cards, not a flow step */
        .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;
        }
        /* chip floorplan row: fixed-function blocks with memory feeder */
        .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;
        }
        /* grouped stage (fab loop) */
        .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;
        }
        /* loop caption sits as a centered footer BELOW the grid, not wedged between rows */
        .kb-body .cfs-group-loop {
            text-align: center;
            font-size: 13.5px;
            color: #c3bdf0;
            margin-top: 14px;
        }
        /* fab-loop CYCLE: 4 stages in a clockwise ring with internal arrows */
        .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>Chiplet &amp; Advanced Packaging Simulator</h1>
  <p class="lede">
    Model <b>when splitting a giant monolithic accelerator die into chiplets pays off</b>
    &mdash; then run it: the simulation executes on the ChipFoundryServices compute pool. Defect-limited
    yield is <b>super-linear in die area</b> &mdash; random particle defects at density <code>D0</code> kill any
    die they land on, so a reticle-sized SoC (the clustered negative-binomial law
    <code>Y = (1 + A&middot;D0/alpha)<sup>-alpha</sup></code>) can yield below 50%. <b>Disaggregation</b> cuts
    the design into <code>N</code> smaller chiplets that each yield far better &mdash; but you now pay for a
    <b>package</b> that wires them back together, <b>lose parts at assembly</b>, and <b>burn energy</b> shuttling
    bits across die boundaries. Chiplets are tested first (<b>known-good-die</b>): coverage <code>t</code> catches
    most bad dies, but escapes get built into a package and scrap the whole unit. System yield =
    <code>KGD &times; assembly</code>. Each packaging class &mdash; organic MCM, EMIB silicon bridge, CoWoS
    interposer, 3D hybrid bond &mdash; sets the <b>die-to-die bandwidth density</b> (GB/s per mm of shoreline) and
    the <b>energy per bit</b>. The node returns the monolithic-vs-chiplet <b>cost per good part</b>, the
    <b>system yield</b>, the <b>beachfront bandwidth</b> and interconnect power, the <b>optimal chiplet count</b>,
    and the operating <b>regime</b>. Reduced-order educational model. See also the
    <a href="/specdec">speculative-decoding</a>, <a href="/moe">Mixture-of-Experts</a>,
    <a href="/flashattention">FlashAttention</a>, <a href="/quantization">quantization</a>,
    <a href="/kvcache">KV-cache</a>, <a href="/hbm">HBM bandwidth</a>,
    <a href="/systolic">systolic array</a>, <a href="/parallelism">3D-parallelism</a>,
    <a href="/power">power &amp; thermal</a>, <a href="/transistor">transistor I-V</a>,
    <a href="/thermal">thermal</a>, <a href="/yield">die-yield</a> and
    <a href="/lithography">lithography</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="mi300">AMD MI300-class</span>
        <span class="preset" data-p="epyc">EPYC 8-chiplet</span>
        <span class="preset" data-p="mono">Small monolith</span>
        <span class="preset" data-p="stack3d">3D hybrid bond</span>
      </div>

      <label>Design area (monolith-equiv) <span class="val" id="v_area">800 mm²</span></label>
      <input type="range" id="die_area_mm2" min="50" max="2600" step="10" value="800">

      <label>Number of chiplets <span class="val" id="v_n">4</span></label>
      <input type="range" id="num_chiplets" min="1" max="16" step="1" value="4">

      <label>Defect density D<sub>0</sub> <span class="val" id="v_d0">0.10 /cm²</span></label>
      <input type="range" id="defect_density_d0" min="0.01" max="2" step="0.01" value="0.1">

      <label>Defect clustering &alpha; <span class="val" id="v_alpha">3.0</span></label>
      <input type="range" id="clustering_alpha" min="0.5" max="10" step="0.1" value="3">

      <label>Packaging <span class="val" id="v_pkg">Si interposer (CoWoS)</span></label>
      <input type="range" id="packaging_type" min="1" max="4" step="1" value="3">

      <label>KGD test coverage <span class="val" id="v_kgd">95%</span></label>
      <input type="range" id="kgd_coverage" min="0" max="0.999" step="0.01" value="0.95">

      <label>Assembly yield / join <span class="val" id="v_join">99.8%</span></label>
      <input type="range" id="assembly_yield_join" min="0.9" max="1" step="0.001" value="0.998">

      <label>Die-to-die traffic <span class="val" id="v_traffic">2.0 TB/s</span></label>
      <input type="range" id="d2d_traffic_tbps" min="0.1" max="20" step="0.1" value="2">

      <label>Wafer cost <span class="val" id="v_wcost">$16,000</span></label>
      <input type="range" id="wafer_cost_usd" min="1000" max="30000" step="500" value="16000">

      <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"><b>Cost-per-good-part savings vs chiplet count</b> N. Each bar is how much cheaper the disaggregated design is than one monolithic die: it rises as smaller chiplets yield better, then flattens or falls once packaging, assembly loss and known-good-die escapes pile up. The dashed line marks <b>1&times;</b> (break-even) and &#9647; marks your N; the tallest bar is the <b>optimal N</b></div>
      <div class="viz" id="viz2"><svg viewBox="0 0 600 240" id="svg2"></svg></div>
      <div class="vizcap">Left: <b>savings vs defect density</b> D<sub>0</sub> &mdash; chiplets win harder as wafers get dirtier and big dies yield worse (&#9647; marks your D<sub>0</sub>). Right: the <b>yield &amp; cost budget</b> &mdash; monolithic vs chiplet per-die yield, the system yield after KGD and assembly, and the die-to-die <b>shoreline bandwidth utilization</b></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/chiplet \
  -H "Content-Type: application/json" \
  -d '{"die_area_mm2":800,"num_chiplets":4,"defect_density_d0":0.10,
       "clustering_alpha":3,"packaging_type":3,"kgd_coverage":0.95,
       "assembly_yield_join":0.998,"d2d_traffic_tbps":2,"wafer_cost_usd":16000}'</pre>
    Returns JSON with <code>outputs</code> (format, packaging, chiplet_area_mm2, gross_die_mono,
    gross_die_chiplet, mono_yield_percent, chiplet_yield_percent, kgd_yield_percent, assembly_yield_percent,
    system_yield_percent, mono_cost_per_good_usd, silicon_cost_usd, package_cost_usd,
    chiplet_cost_per_good_usd, savings_x, shoreline_mm, avail_d2d_bw_tbps, bw_utilization_percent,
    beachfront_limited, d2d_energy_pj_bit, d2d_power_w, link_power_penalty_w, stack_thermal_rise_c,
    optimal_n, optimal_savings_x, verdict), the full <code>profile</code>
    (16-point <code>savings_vs_n</code> and <code>yield_vs_n</code> sweeps, 48-point
    <code>savings_vs_d0</code> sweep, plus <code>num_chiplets</code>, <code>optimal_n</code>,
    <code>savings_x</code>, <code>system_yield_percent</code>, <code>mono_yield_percent</code>,
    <code>chiplet_yield_percent</code>, <code>bw_utilization_percent</code>, <code>d2d_power_w</code>),
    the serving <code>node</code>, and <code>compute_ms</code>. Endpoint aliases
    <code>/edge/chiplets</code>, <code>/edge/packaging</code>, <code>/edge/disaggregation</code>,
    <code>/edge/cowos</code>, <code>/edge/interposer</code>, <code>/edge/d2d</code>.
  </div>
</div>

<script>
const $ = id => document.getElementById(id);
const fields = ["die_area_mm2","num_chiplets","defect_density_d0","clustering_alpha","packaging_type","kgd_coverage","assembly_yield_join","d2d_traffic_tbps","wafer_cost_usd"];
const labels = {die_area_mm2:"v_area", num_chiplets:"v_n", defect_density_d0:"v_d0", clustering_alpha:"v_alpha", packaging_type:"v_pkg", kgd_coverage:"v_kgd", assembly_yield_join:"v_join", d2d_traffic_tbps:"v_traffic", wafer_cost_usd:"v_wcost"};
const PKGNAMES = {1:"Organic substrate (MCM)", 2:"Silicon bridge (EMIB)", 3:"Si interposer (CoWoS)", 4:"3D hybrid bond"};

function syncLabels(){
  for(const f of fields){
    let v = parseFloat($(f).value), out;
    if(f==="die_area_mm2")              out = fmt(v,0) + " mm²";
    else if(f==="num_chiplets")         out = String(v) + (v<=1 ? " (monolithic)" : " chiplets");
    else if(f==="defect_density_d0")    out = v.toFixed(2) + " /cm²";
    else if(f==="clustering_alpha")     out = v.toFixed(1);
    else if(f==="packaging_type")       out = PKGNAMES[v] || v;
    else if(f==="kgd_coverage")         out = (v*100).toFixed(0) + "%";
    else if(f==="assembly_yield_join")  out = (v*100).toFixed(1) + "%";
    else if(f==="d2d_traffic_tbps")     out = v.toFixed(1) + " TB/s";
    else if(f==="wafer_cost_usd")       out = "$" + v.toLocaleString();
    else out = v;
    $(labels[f]).innerHTML = out;
  }
}
fields.forEach(f => $(f).addEventListener("input", syncLabels));

const presets = {
  // die_area_mm2, num_chiplets, defect_density_d0, clustering_alpha, packaging_type, kgd_coverage, assembly_yield_join, d2d_traffic_tbps, wafer_cost_usd
  mi300:   {die_area_mm2:1017, num_chiplets:8,  defect_density_d0:0.10, clustering_alpha:3, packaging_type:4, kgd_coverage:0.97, assembly_yield_join:0.998, d2d_traffic_tbps:4,  wafer_cost_usd:18000},
  epyc:    {die_area_mm2:600,  num_chiplets:8,  defect_density_d0:0.12, clustering_alpha:3, packaging_type:1, kgd_coverage:0.98, assembly_yield_join:0.999, d2d_traffic_tbps:0.5, wafer_cost_usd:12000},
  mono:    {die_area_mm2:120,  num_chiplets:4,  defect_density_d0:0.05, clustering_alpha:4, packaging_type:3, kgd_coverage:0.95, assembly_yield_join:0.998, d2d_traffic_tbps:1,  wafer_cost_usd:10000},
  stack3d: {die_area_mm2:1600, num_chiplets:4,  defect_density_d0:0.20, clustering_alpha:2, packaging_type:4, kgd_coverage:0.95, assembly_yield_join:0.997, d2d_traffic_tbps:8,  wafer_cost_usd:20000},
};
document.querySelectorAll(".preset").forEach(el=>{
  el.onclick = () => { const p = presets[el.dataset.p]; for(const k in p) $(k).value = p[k]; syncLabels(); run(); };
});
syncLabels();

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);
}

// ================= REPLACED-FOR-CHIPLET drawing block =================

// ---- viz 1: cost-savings vs chiplet count N (bars), break-even line, optimal N ----
function drawRouting(r){
  const o = r.outputs, p = r.profile;
  const VW=600, VH=360, padL=48, padR=150, padT=22, padB=44;
  const svg = (p.savings_vs_n && p.savings_vs_n.length) ? p.savings_vs_n : [[1,1]];
  const n = svg.length;
  const curN = p.num_chiplets, optN = p.optimal_n;
  const ymax = Math.max(1.05, Math.max(...svg.map(d=>d[1]))) * 1.15;
  const plotW = VW-padL-padR, plotH = VH-padT-padB;
  const Y = v => padT + (1 - Math.min(Math.max(v,0),ymax)/ymax) * plotH;
  const bw = plotW / n * 0.78, gap = plotW / n * 0.22;
  const parts = [];
  const grid = (val,col,lab,dash) => {
    const yy=Y(val);
    parts.push(`<line x1="${padL}" y1="${yy.toFixed(1)}" x2="${(VW-padR).toFixed(1)}" y2="${yy.toFixed(1)}" stroke="${col}" stroke-width="1"${dash?` stroke-dasharray="${dash}"`:""}/>`);
    if(lab) parts.push(`<text x="${(padL-5).toFixed(1)}" y="${(yy+3).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">${lab}</text>`);
  };
  grid(0, "#c3c9d2", "0");
  grid(Math.round(ymax), "#eef1f4", fmt(Math.round(ymax),0)+"×");
  parts.push(`<line x1="${padL}" y1="${padT}" x2="${padL}" y2="${(VH-padB).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  // bars: green normally, brighter for the optimal N, dark outline for the current N
  svg.forEach((d,i)=>{
    const nn=d[0], sv=d[1];
    const x = padL + i*(bw+gap) + gap*0.5;
    const below = sv < 1.0;
    const col = (nn===optN) ? "#07C160" : (below ? "#e2b4ab" : "#8fce9f");
    parts.push(`<rect x="${x.toFixed(1)}" y="${Y(sv).toFixed(1)}" width="${bw.toFixed(1)}" height="${(Y(0)-Y(sv)).toFixed(1)}" fill="${col}"/>`);
    if(nn===curN){
      parts.push(`<rect x="${x.toFixed(1)}" y="${Y(sv).toFixed(1)}" width="${bw.toFixed(1)}" height="${(Y(0)-Y(sv)).toFixed(1)}" fill="none" stroke="#16181d" stroke-width="2"/>`);
      parts.push(`<text x="${(x+bw/2).toFixed(1)}" y="${(Y(sv)-4).toFixed(1)}" fill="#16181d" font-size="10" font-weight="700" text-anchor="middle">${fmt(sv,2)}×</text>`);
    }
    if(i%2===0 || n<=16) parts.push(`<text x="${(x+bw/2).toFixed(1)}" y="${(VH-padB+12).toFixed(1)}" fill="#94a3b8" font-size="8.5" text-anchor="middle">${nn}</text>`);
  });
  // break-even line at 1x
  grid(1.0, "#c0392b", "1× (break-even)", "5 3");
  // optimal-N marker
  const oi = svg.findIndex(d=>d[0]===optN);
  if(oi>=0){ const ox = padL + oi*(bw+gap) + gap*0.5 + bw/2; parts.push(`<text x="${ox.toFixed(1)}" y="${(Y(svg[oi][1])-4).toFixed(1)}" fill="#34d399" font-size="9" font-weight="700" text-anchor="middle">opt</text>`); }
  // legend / key metrics
  const lx = VW-padR+12; let ly = padT+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="13" font-weight="700">${v}</text>`); ly+=21; };
  kv("savings (your N)", fmt(o.savings_x,2)+"×", o.savings_x>=1 ? "#34d399" : "#c0392b");
  kv("system yield", fmt(o.system_yield_percent,0)+"%", o.system_yield_percent>=80 ? "#34d399" : "#c0392b");
  kv("mono → chiplet die Y", fmt(o.mono_yield_percent,0)+"% → "+fmt(o.chiplet_yield_percent,0)+"%", "#16181d");
  kv("optimal N", optN+" ("+fmt(o.optimal_savings_x,2)+"×)", "#34d399");
  kv("D2D shoreline use", fmt(o.bw_utilization_percent,0)+"%", o.beachfront_limited ? "#9a6b00" : "#34d399");
  parts.push(`<text x="${((padL+VW-padR)/2).toFixed(1)}" y="${(VH-4).toFixed(1)}" fill="#94a3b8" font-size="11" text-anchor="middle">number of chiplets N (design split into N dies)</text>`);
  parts.push(`<text x="13" y="${((padT+VH-padB)/2).toFixed(1)}" fill="#94a3b8" font-size="11" text-anchor="middle" transform="rotate(-90 13 ${((padT+VH-padB)/2).toFixed(1)})">cost/good-part savings vs monolithic</text>`);
  $("viz1").innerHTML = '<svg viewBox="0 0 '+VW+' '+VH+'" width="100%" xmlns="http://www.w3.org/2000/svg" id="svg1">'+parts.join("")+'</svg>';
}

// ---- small line-chart helper (utilization-vs-dimension panel) ----
function sweepPanel(parts, box, data, curX, curY, opt){
  const xs = data.map(d=>d[0]), ys = data.map(d=>d[1]*opt.scale);
  const xmin = xs[0], xmax = xs[xs.length-1];
  const ymax = opt.ymax != null ? opt.ymax : Math.max(1e-6, Math.max(...ys), opt.cap||0) * 1.12;
  const ymin = 0;
  const xfmt = opt.xfmt || (v=>fmt(v,0));
  const X = v => box.x0 + ((v-xmin)/(xmax-xmin)) * box.w;
  const Y = v => box.y0 + (1 - (Math.min(Math.max(v,ymin),ymax)-ymin)/(ymax-ymin)) * box.h;
  parts.push(`<line x1="${box.x0}" y1="${box.y0}" x2="${box.x0}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<line x1="${box.x0}" y1="${(box.y0+box.h).toFixed(1)}" x2="${(box.x0+box.w).toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(box.y0+8).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">${fmt(ymax,0)}</text>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(Y(0)+3).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">0</text>`);
  parts.push(`<text x="${box.x0.toFixed(1)}" y="${(box.y0+box.h+12).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="middle">${xfmt(xmin)}</text>`);
  parts.push(`<text x="${(box.x0+box.w).toFixed(1)}" y="${(box.y0+box.h+12).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="middle">${xfmt(xmax)}</text>`);
  // optional ceiling (dashed red)
  if(opt.cap){
    const cy = Y(opt.cap);
    parts.push(`<line x1="${box.x0}" y1="${cy.toFixed(1)}" x2="${(box.x0+box.w).toFixed(1)}" y2="${cy.toFixed(1)}" stroke="#c0392b" stroke-width="1.2" stroke-dasharray="5 3"/>`);
    parts.push(`<text x="${(box.x0+box.w).toFixed(1)}" y="${(cy-3).toFixed(1)}" fill="#c0392b" font-size="8.5" text-anchor="end">${opt.capLabel||"cap"}</text>`);
  }
  let d=""; data.forEach((pt,i)=>{ d += (i?"L":"M") + X(pt[0]).toFixed(1) + " " + Y(pt[1]*opt.scale).toFixed(1) + " "; });
  parts.push(`<path d="${d}" fill="none" stroke="${opt.color}" stroke-width="2.2"/>`);
  const mx = X(Math.min(Math.max(curX,xmin),xmax)), my = Y(Math.min(curY,ymax));
  parts.push(`<line x1="${mx.toFixed(1)}" y1="${box.y0}" x2="${mx.toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#38bdf8" stroke-width="1" stroke-dasharray="4 3"/>`);
  parts.push(`<circle cx="${mx.toFixed(1)}" cy="${my.toFixed(1)}" r="4" fill="#38bdf8"/>`);
  parts.push(`<text x="${(mx+5).toFixed(1)}" y="${(box.y0+10).toFixed(1)}" fill="#38bdf8" font-size="9.5" font-weight="700">${opt.markLabel}</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0-4).toFixed(1)}" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">${opt.title}</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0+box.h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">${opt.xlab}</text>`);
}

// ---- horizontal efficiency-budget bars: spatial x temporal = utilization ----
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,1)} %</text>`);
    });
  };
}

// ---- log-x sweep panel (for HBM reduction vs sequence length, which spans decades) ----
function logSweepPanel(parts, box, data, curX, curY, opt){
  const xs = data.map(d=>d[0]), ys = data.map(d=>d[1]*opt.scale);
  const xmin = xs[0], xmax = xs[xs.length-1];
  const ymax = opt.ymax != null ? opt.ymax : Math.max(1e-6, Math.max(...ys)) * 1.12;
  const lxmin = Math.log10(xmin), lxmax = Math.log10(xmax);
  const xfmt = opt.xfmt || (v=>fmt(v,0));
  const X = v => box.x0 + (Math.log10(Math.min(Math.max(v,xmin),xmax))-lxmin)/(lxmax-lxmin) * box.w;
  const Y = v => box.y0 + (1 - Math.min(Math.max(v,0),ymax)/ymax) * box.h;
  parts.push(`<line x1="${box.x0}" y1="${box.y0}" x2="${box.x0}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<line x1="${box.x0}" y1="${(box.y0+box.h).toFixed(1)}" x2="${(box.x0+box.w).toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(box.y0+8).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">${fmt(ymax,0)}</text>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(Y(0)+3).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">0</text>`);
  // decade ticks on x
  for(let e=Math.ceil(lxmin); e<=Math.floor(lxmax); e++){
    const xx=X(Math.pow(10,e));
    parts.push(`<text x="${xx.toFixed(1)}" y="${(box.y0+box.h+12).toFixed(1)}" fill="#94a3b8" font-size="8.5" text-anchor="middle">${xfmt(Math.pow(10,e))}</text>`);
  }
  let d=""; data.forEach((pt,i)=>{ d += (i?"L":"M") + X(pt[0]).toFixed(1) + " " + Y(pt[1]*opt.scale).toFixed(1) + " "; });
  parts.push(`<path d="${d}" fill="none" stroke="${opt.color}" stroke-width="2.2"/>`);
  const mx = X(Math.min(Math.max(curX,xmin),xmax)), my = Y(Math.min(curY,ymax));
  parts.push(`<line x1="${mx.toFixed(1)}" y1="${box.y0}" x2="${mx.toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#38bdf8" stroke-width="1" stroke-dasharray="4 3"/>`);
  parts.push(`<circle cx="${mx.toFixed(1)}" cy="${my.toFixed(1)}" r="4" fill="#38bdf8"/>`);
  parts.push(`<text x="${(mx+5).toFixed(1)}" y="${(box.y0+10).toFixed(1)}" fill="#38bdf8" font-size="9.5" font-weight="700">${opt.markLabel}</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0-4).toFixed(1)}" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">${opt.title}</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0+box.h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">${opt.xlab}</text>`);
}

// ---- dual-curve panel: two % series sharing linear axes (drop down + waste up) ----
function crossPanel(parts, box, dataA, dataB, curX, opt){
  const xs = dataA.map(d=>d[0]);
  const xmin = xs[0], xmax = xs[xs.length-1];
  const ymax = 100;
  const X = v => box.x0 + ((Math.min(Math.max(v,xmin),xmax)-xmin)/(xmax-xmin)) * box.w;
  const Y = v => box.y0 + (1 - Math.min(Math.max(v,0),ymax)/ymax) * box.h;
  parts.push(`<line x1="${box.x0}" y1="${box.y0}" x2="${box.x0}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<line x1="${box.x0}" y1="${(box.y0+box.h).toFixed(1)}" x2="${(box.x0+box.w).toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#334d70" stroke-width="1"/>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(box.y0+8).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">100%</text>`);
  parts.push(`<text x="${(box.x0-5).toFixed(1)}" y="${(Y(0)+3).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="end">0</text>`);
  parts.push(`<text x="${box.x0.toFixed(1)}" y="${(box.y0+box.h+12).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="middle">${fmt(xmin,2)}×</text>`);
  parts.push(`<text x="${(box.x0+box.w).toFixed(1)}" y="${(box.y0+box.h+12).toFixed(1)}" fill="#94a3b8" font-size="9" text-anchor="middle">${fmt(xmax,1)}×</text>`);
  const path = (data,col) => { let d=""; data.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="${col}" stroke-width="2.2"/>`); };
  path(dataA, "#c0392b");   // drop % (falling)
  path(dataB, "#e0821a");   // waste % (rising)
  // current-setting marker
  const mx = X(curX);
  parts.push(`<line x1="${mx.toFixed(1)}" y1="${box.y0}" x2="${mx.toFixed(1)}" y2="${(box.y0+box.h).toFixed(1)}" stroke="#38bdf8" stroke-width="1" stroke-dasharray="4 3"/>`);
  parts.push(`<text x="${(mx+4).toFixed(1)}" y="${(box.y0+10).toFixed(1)}" fill="#16181d" font-size="9.5">cf ${fmt(curX,2)}×</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0-4).toFixed(1)}" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">${opt.title}</text>`);
  parts.push(`<text x="${(box.x0+box.w/2).toFixed(1)}" y="${(box.y0+box.h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">${opt.xlab}</text>`);
  // inline legend
  parts.push(`<text x="${(box.x0+4).toFixed(1)}" y="${(box.y0+box.h-6).toFixed(1)}" fill="#c0392b" font-size="9" font-weight="700">drop %</text>`);
  parts.push(`<text x="${(box.x0+box.w-4).toFixed(1)}" y="${(box.y0+box.h-6).toFixed(1)}" fill="#e0821a" font-size="9" font-weight="700" text-anchor="end">waste %</text>`);
}

// ---- viz 2: savings-vs-defect-density curve + yield/cost budget bars ----
function drawBudget(r){
  const o = r.outputs, p = r.profile;
  const VW=600, VH=240;
  const parts = [];
  const gap=48, w=(VW-gap*3)/2, h=150, y0=34;
  // left: savings vs defect density D0 (rising), break-even line + current-D0 marker
  const d0cur = (r.inputs && r.inputs.defect_density_d0) || o.__d0 || p.__d0 || 0.1;
  sweepPanel(parts, {x0:gap, y0:y0, w:w, h:h},
    p.savings_vs_d0, d0cur, o.savings_x,
    {scale:1, color:"#34d399", cap:1.0, capLabel:"1× break-even",
     xfmt:v=>fmt(v,2), title:"Savings vs defect density D₀", xlab:"defect density D₀ (defects/cm²)",
     markLabel:"D₀ "+fmt(d0cur,2)});
  parts.push(`<text x="${gap}" y="18" fill="#94a3b8" font-size="10">×</text>`);
  // right: yield & cost 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">Yield &amp; link budget</text>`);
  budgetBars(parts, {x0:rx, y0:y0+6, w:w, h:h-6})([
    {k:"Monolithic die yield", v:o.mono_yield_percent,    color:o.mono_yield_percent<50 ? "#c0392b" : "#e0821a"},
    {k:"Chiplet die yield",    v:o.chiplet_yield_percent, color:"#07C160"},
    {k:"System yield (KGD×asm)",v:o.system_yield_percent, color:o.system_yield_percent<80 ? "#c0392b" : "#3f78c4"},
    {k:"D2D shoreline use",    v:o.bw_utilization_percent,color:o.beachfront_limited ? "#c0392b" : "#8a63d2"},
  ]);
  parts.push(`<text x="${(rx+w/2).toFixed(1)}" y="${(y0+h+24).toFixed(1)}" fill="#94a3b8" font-size="10" text-anchor="middle">mono vs chiplet yield, system yield &amp; link utilization</text>`);
  $("viz2").innerHTML = '<svg viewBox="0 0 '+VW+' '+VH+'" width="100%" xmlns="http://www.w3.org/2000/svg" id="svg2">'+parts.join("")+'</svg>';
}
// ================= end replaced block =================

function showMetrics(r){
  const o = r.outputs, inp = r.inputs;
  const rows = [
    ["Configuration", o.format],
    ["Cost/good-part savings", fmt(o.savings_x,2)+"× vs monolithic"],
    ["Chiplet area (each)", fmt(o.chiplet_area_mm2,0)+" mm²"],
    ["Gross dies/wafer (mono → chiplet)", fmt(o.gross_die_mono,0)+" → "+fmt(o.gross_die_chiplet,0)],
    ["Monolithic die yield", fmt(o.mono_yield_percent,1)+" %"],
    ["Chiplet die yield (each)", fmt(o.chiplet_yield_percent,1)+" %"],
    ["Known-good-die yield", fmt(o.kgd_yield_percent,1)+" %"],
    ["Assembly yield", fmt(o.assembly_yield_percent,1)+" %"],
    ["System yield (KGD × assembly)", fmt(o.system_yield_percent,1)+" %"],
    ["Monolithic cost / good part", "$"+fmt(o.mono_cost_per_good_usd,0)],
    ["Chiplet silicon cost", "$"+fmt(o.silicon_cost_usd,0)],
    ["Package cost", "$"+fmt(o.package_cost_usd,0)],
    ["Chiplet cost / good part", "$"+fmt(o.chiplet_cost_per_good_usd,0)],
    ["Packaging class", o.packaging],
    ["Die-to-die shoreline", fmt(o.shoreline_mm,0)+" mm"],
    ["Available D2D bandwidth", fmt(o.avail_d2d_bw_tbps,1)+" TB/s"],
    ["D2D bandwidth utilization", fmt(o.bw_utilization_percent,0)+" %"+(o.beachfront_limited?" (beachfront-limited)":"")],
    ["D2D interconnect energy", fmt(o.d2d_energy_pj_bit,2)+" pJ/bit"],
    ["D2D link power", fmt(o.d2d_power_w,1)+" W (+"+fmt(o.link_power_penalty_w,1)+" W vs on-die)"],
    ["Stack thermal rise", fmt(o.stack_thermal_rise_c,0)+" °C"],
    ["Optimal chiplet count N", o.optimal_n+" ("+fmt(o.optimal_savings_x,2)+"×)"],
  ];
  $("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(/^chiplet-win/.test(o.verdict)){ v.style.background="#e7f7ee"; v.style.color="#34d399"; }
  else if(/^monolithic-better/.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>';
                // One arrow per column of the preceding node row (parallel arrows, Claude.ai-style).
                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) => {
                    // explicit arrow row: draw the connector, don't double up
                    if (r.type === 'arrow') { out += connector(prevCount); prevType = 'arrow'; return; }
                    // auto-connect sequential flow rows only; tier/die rows are diagrams, not steps
                    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) {
                            // clockwise ring: items = [top-left, top-right, bottom-right, bottom-left]
                            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;
                    }
                    // default: a row of nodes
                    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);
  // KB stores newlines/tabs as literal backslash sequences — restore real
  // breaks so paragraphs and diagrams separate instead of running together.
  s = s.replace(/\\r\\n|\\r|\\n/g, "\n").replace(/\\t/g, "  ");
  // Hold aside authored diagram blocks (fenced ```svg / ```html, or a bare
  // <svg>…</svg>) so they render as real figures rather than escaped tag text.
  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;");
  });
  // Restore authored diagrams as real figures (unwrap the stray <p> the block
  // splitter puts around a lone sentinel so we don't nest a figure inside <p>).
  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 queries = [["Chiplet","chiplet"], ["", "advanced packaging"], ["", "known good die"]];
  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/chiplet", {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);
    drawRouting(r); drawBudget(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>
