Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-26 08:03:07

0001 <!DOCTYPE html>
0002 <html lang="en">
0003 <head>
0004 <meta charset="utf-8">
0005 <meta name="viewport" content="width=device-width, initial-scale=1">
0006 <title>Surface array: filling and lookup</title>
0007 
0008 <style>
0009   :root {
0010     --bg: #eef1f2;
0011     --panel: #f9fbfb;
0012     --panel-2: #e6ecec;
0013     --ink: #101619;
0014     --muted: #5a666c;
0015     --rule: #ccd6d9;
0016     --rule-soft: #dde4e6;
0017     --served: #0f766e;
0018     --served-fill: rgba(15, 118, 110, .16);
0019     --served-line: rgba(15, 118, 110, .45);
0020     --miss: #b0341f;
0021     --miss-fill: rgba(176, 52, 31, .15);
0022     --waste: #9a6a12;
0023     --waste-fill: rgba(154, 106, 18, .16);
0024     --shadow: 0 1px 2px rgba(16, 22, 25, .06), 0 6px 18px -10px rgba(16, 22, 25, .20);
0025 
0026     --f-display: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
0027     --f-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
0028     --f-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
0029   }
0030 
0031   @media (prefers-color-scheme: dark) {
0032     :root:not([data-theme="light"]) {
0033       --bg: #0d1214;
0034       --panel: #151c1f;
0035       --panel-2: #1a2327;
0036       --ink: #e3eaec;
0037       --muted: #8d9aa0;
0038       --rule: #263034;
0039       --rule-soft: #1e272a;
0040       --served: #5eccc0;
0041       --served-fill: rgba(94, 204, 192, .20);
0042       --served-line: rgba(94, 204, 192, .50);
0043       --miss: #e8806e;
0044       --miss-fill: rgba(232, 128, 110, .19);
0045       --waste: #d3a34e;
0046       --waste-fill: rgba(211, 163, 78, .18);
0047       --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 8px 22px -12px rgba(0, 0, 0, .8);
0048     }
0049   }
0050 
0051   :root[data-theme="dark"] {
0052     --bg: #0d1214;
0053     --panel: #151c1f;
0054     --panel-2: #1a2327;
0055     --ink: #e3eaec;
0056     --muted: #8d9aa0;
0057     --rule: #263034;
0058     --rule-soft: #1e272a;
0059     --served: #5eccc0;
0060     --served-fill: rgba(94, 204, 192, .20);
0061     --served-line: rgba(94, 204, 192, .50);
0062     --miss: #e8806e;
0063     --miss-fill: rgba(232, 128, 110, .19);
0064     --waste: #d3a34e;
0065     --waste-fill: rgba(211, 163, 78, .18);
0066     --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 8px 22px -12px rgba(0, 0, 0, .8);
0067   }
0068 
0069   * { box-sizing: border-box; }
0070 
0071   body {
0072     margin: 0;
0073     background: var(--bg);
0074     color: var(--ink);
0075     font-family: var(--f-body);
0076     font-size: 16px;
0077     line-height: 1.6;
0078     -webkit-font-smoothing: antialiased;
0079   }
0080 
0081   img { max-width: 100%; }
0082 
0083   .wrap {
0084     max-width: 1120px;
0085     margin: 0 auto;
0086     padding: 40px 24px 88px;
0087     display: flex;
0088     flex-direction: column;
0089     gap: 42px;
0090   }
0091 
0092   .prose { max-width: 66ch; display: flex; flex-direction: column; gap: 14px; }
0093   .prose p { margin: 0; }
0094 
0095   a { color: var(--served); }
0096 
0097   .back {
0098     font-family: var(--f-mono);
0099     font-size: 12px;
0100     margin: 0 0 26px;
0101   }
0102 
0103   header .note { margin-top: 14px; }
0104 
0105   .eyebrow {
0106     font-family: var(--f-mono);
0107     font-size: 12px;
0108     letter-spacing: .13em;
0109     text-transform: uppercase;
0110     color: var(--muted);
0111     margin: 0 0 12px;
0112   }
0113 
0114   h1 {
0115     font-family: var(--f-display);
0116     font-weight: 700;
0117     font-size: clamp(30px, 4.3vw, 44px);
0118     line-height: 1.05;
0119     letter-spacing: -.02em;
0120     margin: 0 0 16px;
0121     text-wrap: balance;
0122   }
0123 
0124   h2 {
0125     font-family: var(--f-display);
0126     font-weight: 600;
0127     font-size: 21px;
0128     letter-spacing: -.01em;
0129     margin: 0 0 2px;
0130     text-wrap: balance;
0131   }
0132 
0133   h3 { font-family: var(--f-display); font-weight: 600; font-size: 14px; margin: 0; }
0134 
0135   .standfirst { font-size: 18px; color: var(--muted); max-width: 62ch; margin: 0; }
0136 
0137   code, .mono { font-family: var(--f-mono); font-size: .87em; }
0138   code { background: var(--panel-2); padding: .1em .34em; border-radius: 3px; }
0139   strong { font-weight: 600; }
0140 
0141   .instrument {
0142     background: var(--panel);
0143     border: 1px solid var(--rule);
0144     border-radius: 8px;
0145     box-shadow: var(--shadow);
0146     overflow: hidden;
0147   }
0148 
0149   .rail {
0150     display: flex;
0151     flex-wrap: wrap;
0152     align-items: flex-end;
0153     gap: 16px 26px;
0154     padding: 17px 22px;
0155     border-bottom: 1px solid var(--rule);
0156     background: var(--panel-2);
0157   }
0158 
0159   .ctl { display: flex; flex-direction: column; gap: 5px; min-width: 150px; flex: 1 1 150px; }
0160 
0161   .ctl > label {
0162     font-family: var(--f-mono);
0163     font-size: 10.5px;
0164     letter-spacing: .09em;
0165     text-transform: uppercase;
0166     color: var(--muted);
0167     display: flex;
0168     justify-content: space-between;
0169     gap: 10px;
0170   }
0171 
0172   .ctl output { color: var(--ink); font-variant-numeric: tabular-nums; }
0173   input[type="range"] { width: 100%; accent-color: var(--served); margin: 0; }
0174   input:focus-visible, button:focus-visible { outline: 2px solid var(--served); outline-offset: 2px; }
0175 
0176   .stage { padding: 16px 22px 6px; }
0177   .stage svg, .sweep svg { width: 100%; height: auto; display: block; }
0178 
0179   /* --- three-way comparison of the fill, and the policy strips --- */
0180 
0181   .panels, .policies {
0182     display: grid;
0183     grid-template-columns: repeat(3, minmax(0, 1fr));
0184     gap: 1px;
0185     background: var(--rule);
0186   }
0187   .policies { border-top: 1px solid var(--rule); }
0188   @media (max-width: 860px) { .panels, .policies { grid-template-columns: 1fr; } }
0189 
0190   .panel, .policy { background: var(--panel); padding: 15px 18px 17px; display: flex; flex-direction: column; gap: 9px; }
0191   .panel-head { display: flex; flex-direction: column; gap: 3px; }
0192   .panel .sub, .policy .sub { font-family: var(--f-mono); font-size: 10.5px; color: var(--muted); line-height: 1.5; margin: 0; }
0193 
0194   .panel svg { width: 100%; height: auto; display: block; touch-action: none; cursor: grab; }
0195   .panel svg:active { cursor: grabbing; }
0196 
0197   .stats { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; font-family: var(--f-mono); font-size: 12px; }
0198   .stats dt { color: var(--muted); }
0199   .stats dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
0200   .ok, .good { color: var(--served); font-weight: 500; }
0201   .bad { color: var(--miss); font-weight: 500; }
0202   .warn { color: var(--waste); font-weight: 500; }
0203 
0204   .legend {
0205     display: flex; flex-wrap: wrap; gap: 8px 22px;
0206     padding: 12px 22px; border-top: 1px solid var(--rule);
0207     background: var(--panel-2); font-family: var(--f-mono); font-size: 11.5px; color: var(--muted);
0208   }
0209   .legend span { display: inline-flex; align-items: center; gap: 7px; }
0210   .swatch { width: 13px; height: 13px; border-radius: 2px; border: 1px solid var(--rule); flex: none; }
0211 
0212   /* --- verdict block --- */
0213 
0214   .verdict {
0215     border: 1px solid var(--rule);
0216     border-radius: 8px;
0217     background: var(--panel);
0218     padding: 20px 22px;
0219     box-shadow: var(--shadow);
0220   }
0221   .verdict-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 18px; }
0222   .vcell { display: flex; flex-direction: column; gap: 4px; }
0223   .vcell .k { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
0224   .vcell .v { font-family: var(--f-display); font-size: 25px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
0225   .vcell .n { font-family: var(--f-mono); font-size: 11.5px; color: var(--muted); line-height: 1.5; }
0226   .vcell .v.bad { color: var(--miss); }
0227   .vcell .v.good { color: var(--served); }
0228 
0229   /* --- side-by-side figure and table --- */
0230 
0231   .sweep, .ring { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr); gap: 24px; align-items: start; }
0232   @media (max-width: 780px) { .sweep, .ring { grid-template-columns: 1fr; } }
0233   .ring svg { width: 100%; height: auto; display: block; }
0234 
0235   table.data { border-collapse: collapse; font-family: var(--f-mono); font-size: 12.5px; width: 100%; }
0236   table.data th, table.data td { padding: 6px 10px 6px 0; text-align: left; border-bottom: 1px solid var(--rule-soft); }
0237   table.data th { font-weight: 500; color: var(--muted); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; }
0238   table.data td:last-child, table.data th:last-child { text-align: right; padding-right: 0; font-variant-numeric: tabular-nums; }
0239   .scroll { overflow-x: auto; }
0240 
0241   .note { font-size: 14.5px; color: var(--muted); max-width: 66ch; margin: 0; }
0242   hr.rule { border: 0; border-top: 1px solid var(--rule); margin: 0; }
0243 
0244   .figrail { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px;
0245              padding: 12px 16px; background: var(--panel-2);
0246              border: 1px solid var(--rule); border-radius: 8px; margin-bottom: -22px; }
0247   .grouplabel { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .09em;
0248                 text-transform: uppercase; color: var(--muted); }
0249   .chips { display: flex; flex-wrap: wrap; gap: 6px; }
0250   .chip { font-family: var(--f-mono); font-size: 11.5px; padding: 5px 10px; border-radius: 4px;
0251           border: 1px solid var(--rule); background: var(--panel); color: var(--muted); cursor: pointer; }
0252   .chip:hover { color: var(--ink); border-color: var(--served-line); }
0253   .chip[aria-pressed="true"] { background: var(--served); border-color: var(--served); color: var(--panel); }
0254   .verdict-inline { font-family: var(--f-mono); font-size: 11.5px; margin-left: auto; }
0255 
0256   figure { margin: 0; display: flex; flex-direction: column; gap: 10px; }
0257   figure svg { width: 100%; height: auto; display: block; }
0258   figcaption { font-size: 13.5px; color: var(--muted); line-height: 1.5; max-width: 62ch; }
0259   .plate { background: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
0260            box-shadow: var(--shadow); padding: 14px 16px; }
0261   .pair { display: grid; grid-template-columns: 1fr; gap: 26px; }
0262   @media (min-width: 900px) { .pair { grid-template-columns: 1fr 1fr; } }
0263 </style>
0264 </head>
0265 <body>
0266 
0267 <div class="wrap">
0268 
0269   <header>
0270     <p class="back"><a href="group__surface__array.html">&larr; Surface array reference</a></p>
0271     <p class="eyebrow">ACTS &middot; Acts::SurfaceArray</p>
0272     <h1>Filling a layer, and finding the module again</h1>
0273     <p class="standfirst">A surface array bins the modules of one layer on a representative surface, and answers, for a track crossing it, which of them the track could hit. Two questions decide whether that works: which bins hold a surface, and how far from the crossing bin the module it hits can be.</p>
0274     <p class="note">The figures below are live. The prose that surrounds them is deliberately thin &mdash; the written description of the class, its axes, its bounds and its cache is in the <a href="group__surface__array.html">reference page</a>.</p>
0275   </header>
0276 
0277   <hr class="rule">
0278 
0279   <section class="prose">
0280     <h2 id="fill">Which bins hold a surface</h2>
0281     <p>Registration is an area question: does this module overlap this bin? The fill that preceded <code>fillSurfaceFootprint</code> answered it with two point tests. <strong>Surface&nbsp;&rarr;&nbsp;grid</strong>: project the module's reference position, register that one bin. <strong>Grid&nbsp;&rarr;&nbsp;surface</strong>: flood-fill outward from it, keeping every bin whose <em>centre</em> projects onto the module. One direction handles a module smaller than a bin; the other handles a module larger than a bin, and the intuition was that they covered each other.</p>
0282     <p>They do not. The first samples one point of the module against the grid, the second samples one point of the bin against the module, and two point tests do not compose into a set-overlap test. What the flood fill computes is the true footprint <strong>eroded by up to half a bin</strong> on each axis &mdash; and a module can be smaller than a bin on one axis while spanning many bins on the other, which is where the erosion stops being a boundary ring and starts swallowing the surface whole.</p>
0283   </section>
0284 
0285   <section class="figrail">
0286     <span class="grouplabel">how a bin qualifies</span>
0287     <div class="chips" id="fstep">
0288       <button class="chip" data-m="area" aria-pressed="true">area overlap</button>
0289       <button class="chip" data-m="centre" aria-pressed="false">bin centre inside</button>
0290     </div>
0291     <span class="verdict-inline" id="fverdict"></span>
0292   </section>
0293 
0294   <section>
0295     <figure>
0296       <div class="plate"><svg class="figfoot" viewBox="0 0 640 300" role="img"
0297      aria-label="A trapezoidal module projected onto the (phi, r) grid of a disc layer, with the bins the span fill registers">
0298   <style>
0299 .figfoot text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; fill: var(--muted); }
0300 .figfoot .ink { fill: var(--ink); }
0301 .figfoot .acc { fill: var(--served); }
0302 </style>
0303   <rect width="640" height="300" fill="var(--panel)"/>
0304   <!-- registered bins: every bin of every column span -->
0305   <g fill="var(--served)" fill-opacity=".17" stroke="var(--served)" stroke-opacity=".45" stroke-width=".8" data-w="fbins">
0306     <rect x="336" y="176" width="40" height="35"/>
0307     <rect x="336" y="141" width="40" height="35"/>
0308     <rect x="336" y="106" width="40" height="35"/>
0309     <rect x="336" y="71" width="40" height="35"/>
0310     <rect x="336" y="36" width="40" height="35"/>
0311     <rect x="376" y="176" width="40" height="35"/>
0312     <rect x="376" y="141" width="40" height="35"/>
0313     <rect x="376" y="106" width="40" height="35"/>
0314     <rect x="376" y="71" width="40" height="35"/>
0315     <rect x="376" y="36" width="40" height="35"/>
0316     <rect x="416" y="176" width="40" height="35"/>
0317     <rect x="416" y="141" width="40" height="35"/>
0318     <rect x="416" y="106" width="40" height="35"/>
0319     <rect x="416" y="71" width="40" height="35"/>
0320     <rect x="416" y="36" width="40" height="35"/>
0321     <rect x="56" y="211" width="40" height="35"/>
0322     <rect x="56" y="176" width="40" height="35"/>
0323     <rect x="56" y="141" width="40" height="35"/>
0324     <rect x="56" y="106" width="40" height="35"/>
0325     <rect x="576" y="211" width="40" height="35"/>
0326     <rect x="576" y="176" width="40" height="35"/>
0327     <rect x="576" y="141" width="40" height="35"/>
0328     <rect x="576" y="106" width="40" height="35"/>
0329   </g>
0330   <!-- grid rules -->
0331   <g stroke="var(--rule)" stroke-width=".6" fill="none">
0332     <line x1="56" y1="36" x2="56" y2="246"/>
0333     <line x1="96" y1="36" x2="96" y2="246"/>
0334     <line x1="136" y1="36" x2="136" y2="246"/>
0335     <line x1="176" y1="36" x2="176" y2="246"/>
0336     <line x1="216" y1="36" x2="216" y2="246"/>
0337     <line x1="256" y1="36" x2="256" y2="246"/>
0338     <line x1="296" y1="36" x2="296" y2="246"/>
0339     <line x1="336" y1="36" x2="336" y2="246"/>
0340     <line x1="376" y1="36" x2="376" y2="246"/>
0341     <line x1="416" y1="36" x2="416" y2="246"/>
0342     <line x1="456" y1="36" x2="456" y2="246"/>
0343     <line x1="496" y1="36" x2="496" y2="246"/>
0344     <line x1="536" y1="36" x2="536" y2="246"/>
0345     <line x1="576" y1="36" x2="576" y2="246"/>
0346     <line x1="616" y1="36" x2="616" y2="246"/>
0347     <line x1="56" y1="36" x2="616" y2="36"/>
0348     <line x1="56" y1="71" x2="616" y2="71"/>
0349     <line x1="56" y1="106" x2="616" y2="106"/>
0350     <line x1="56" y1="141" x2="616" y2="141"/>
0351     <line x1="56" y1="176" x2="616" y2="176"/>
0352     <line x1="56" y1="211" x2="616" y2="211"/>
0353     <line x1="56" y1="246" x2="616" y2="246"/>
0354   </g>
0355   <!-- the seam: the closed phi axis wraps here -->
0356   <g stroke="var(--ink)" stroke-width="1.2"><line x1="56" y1="32" x2="56" y2="250"/><line x1="616" y1="32" x2="616" y2="250"/></g>
0357   <!-- bin centers: registration is by area overlap, not by the center -->
0358   <g data-w="fcentres">
0359     <circle cx="76.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0360     <circle cx="76.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0361     <circle cx="76.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0362     <circle cx="76.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0363     <circle cx="76.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0364     <circle cx="76.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0365     <circle cx="116.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0366     <circle cx="116.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0367     <circle cx="116.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0368     <circle cx="116.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0369     <circle cx="116.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0370     <circle cx="116.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0371     <circle cx="156.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0372     <circle cx="156.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0373     <circle cx="156.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0374     <circle cx="156.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0375     <circle cx="156.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0376     <circle cx="156.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0377     <circle cx="196.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0378     <circle cx="196.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0379     <circle cx="196.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0380     <circle cx="196.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0381     <circle cx="196.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0382     <circle cx="196.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0383     <circle cx="236.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0384     <circle cx="236.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0385     <circle cx="236.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0386     <circle cx="236.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0387     <circle cx="236.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0388     <circle cx="236.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0389     <circle cx="276.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0390     <circle cx="276.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0391     <circle cx="276.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0392     <circle cx="276.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0393     <circle cx="276.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0394     <circle cx="276.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0395     <circle cx="316.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0396     <circle cx="316.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0397     <circle cx="316.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0398     <circle cx="316.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0399     <circle cx="316.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0400     <circle cx="316.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0401     <circle cx="356.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0402     <circle cx="356.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0403     <circle cx="356.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0404     <circle cx="356.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0405     <circle cx="356.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0406     <circle cx="356.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0407     <circle cx="396.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0408     <circle cx="396.0" cy="193.5" r="1.8" fill="var(--served)"/>
0409     <circle cx="396.0" cy="158.5" r="1.8" fill="var(--served)"/>
0410     <circle cx="396.0" cy="123.5" r="1.8" fill="var(--served)"/>
0411     <circle cx="396.0" cy="88.5" r="1.8" fill="var(--served)"/>
0412     <circle cx="396.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0413     <circle cx="436.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0414     <circle cx="436.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0415     <circle cx="436.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0416     <circle cx="436.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0417     <circle cx="436.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0418     <circle cx="436.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0419     <circle cx="476.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0420     <circle cx="476.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0421     <circle cx="476.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0422     <circle cx="476.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0423     <circle cx="476.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0424     <circle cx="476.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0425     <circle cx="516.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0426     <circle cx="516.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0427     <circle cx="516.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0428     <circle cx="516.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0429     <circle cx="516.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0430     <circle cx="516.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0431     <circle cx="556.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0432     <circle cx="556.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0433     <circle cx="556.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0434     <circle cx="556.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0435     <circle cx="556.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0436     <circle cx="556.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0437     <circle cx="596.0" cy="228.5" r="1.1" fill="var(--rule)"/>
0438     <circle cx="596.0" cy="193.5" r="1.1" fill="var(--rule)"/>
0439     <circle cx="596.0" cy="158.5" r="1.1" fill="var(--rule)"/>
0440     <circle cx="596.0" cy="123.5" r="1.1" fill="var(--rule)"/>
0441     <circle cx="596.0" cy="88.5" r="1.1" fill="var(--rule)"/>
0442     <circle cx="596.0" cy="53.5" r="1.1" fill="var(--rule)"/>
0443   </g>
0444   <!-- projected outlines, sampled along each straight edge -->
0445   <g fill="none" stroke="var(--served)" stroke-width="1.6" stroke-linejoin="round">
0446     <path d="M373.9,192.4 L377.4,195.5 L380.8,198.3 L384.4,200.6 L388.0,202.6 L391.7,204.1 L395.4,205.2 L399.1,205.9 L402.8,206.1 L406.6,205.9 L410.3,205.2 L414.0,204.1 L417.7,202.6 L421.3,200.6 L424.9,198.3 L428.3,195.5 L431.7,192.4 L431.7,183.8 L431.7,175.3 L431.6,166.7 L431.6,158.2 L431.6,149.6 L431.6,141.1 L431.5,132.5 L431.5,124.0 L431.5,115.4 L431.5,106.9 L431.5,98.3 L431.4,89.8 L431.4,81.2 L431.4,72.7 L431.4,64.1 L431.4,55.6 L428.0,60.2 L424.6,64.3 L421.1,67.8 L417.5,70.7 L413.9,73.0 L410.2,74.6 L406.5,75.6 L402.8,75.9 L399.2,75.6 L395.5,74.6 L391.8,73.0 L388.2,70.7 L384.6,67.8 L381.1,64.3 L377.7,60.2 L374.3,55.6 L374.3,64.1 L374.3,72.7 L374.3,81.2 L374.2,89.8 L374.2,98.3 L374.2,106.9 L374.2,115.4 L374.2,124.0 L374.1,132.5 L374.1,141.1 L374.1,149.6 L374.1,158.2 L374.0,166.7 L374.0,175.3 L374.0,183.8 Z"/>
0447     <path d="M616.0,133.7 L614.1,133.6 L612.2,133.4 L610.3,133.0 L608.5,132.5 L606.6,131.8 L604.7,131.1 L602.9,130.1 L601.0,129.1 L601.1,135.1 L601.1,141.1 L601.2,147.1 L601.3,153.1 L601.3,159.2 L601.4,165.2 L601.4,171.2 L601.5,177.2 L601.6,183.2 L601.6,189.3 L601.7,195.3 L601.8,201.3 L601.9,207.3 L602.0,213.3 L602.1,219.4 L602.1,225.4 L603.9,226.0 L605.6,226.6 L607.3,227.1 L609.0,227.5 L610.8,227.8 L612.5,228.0 L614.3,228.1 L616.0,228.2"/>
0448     <path d="M56.9,228.1 L58.6,228.1 L60.4,227.9 L62.1,227.6 L63.8,227.3 L65.6,226.8 L67.3,226.3 L69.0,225.7 L69.9,222.4 L70.0,216.3 L70.1,210.3 L70.2,204.3 L70.2,198.3 L70.3,192.3 L70.4,186.2 L70.5,180.2 L70.5,174.2 L70.6,168.2 L70.7,162.2 L70.7,156.2 L70.8,150.1 L70.8,144.1 L70.9,138.1 L70.9,132.1 L70.0,129.6 L68.2,130.6 L66.3,131.5 L64.5,132.2 L62.6,132.8 L60.7,133.2 L58.8,133.5 L56.9,133.6"/>
0449   </g>
0450   <g fill="var(--served)">
0451     <circle cx="373.9" cy="192.4" r="1.6"/>
0452     <circle cx="388.0" cy="202.6" r="1.6"/>
0453     <circle cx="402.8" cy="206.1" r="1.6"/>
0454     <circle cx="417.7" cy="202.6" r="1.6"/>
0455     <circle cx="431.7" cy="192.4" r="1.6"/>
0456     <circle cx="431.6" cy="158.2" r="1.6"/>
0457     <circle cx="431.5" cy="124.0" r="1.6"/>
0458     <circle cx="431.4" cy="89.8" r="1.6"/>
0459     <circle cx="431.4" cy="55.6" r="1.6"/>
0460     <circle cx="417.5" cy="70.7" r="1.6"/>
0461     <circle cx="402.8" cy="75.9" r="1.6"/>
0462     <circle cx="388.2" cy="70.7" r="1.6"/>
0463     <circle cx="374.3" cy="55.6" r="1.6"/>
0464     <circle cx="374.2" cy="89.8" r="1.6"/>
0465     <circle cx="374.2" cy="124.0" r="1.6"/>
0466     <circle cx="374.1" cy="158.2" r="1.6"/>
0467     <circle cx="602.1" cy="225.4" r="1.6"/>
0468     <circle cx="609.0" cy="227.5" r="1.6"/>
0469     <circle cx="616.0" cy="228.2" r="1.6"/>
0470     <circle cx="63.0" cy="227.5" r="1.6"/>
0471     <circle cx="69.9" cy="225.4" r="1.6"/>
0472     <circle cx="70.2" cy="201.3" r="1.6"/>
0473     <circle cx="70.5" cy="177.2" r="1.6"/>
0474     <circle cx="70.7" cy="153.1" r="1.6"/>
0475     <circle cx="71.0" cy="129.1" r="1.6"/>
0476     <circle cx="63.5" cy="132.5" r="1.6"/>
0477     <circle cx="616.0" cy="133.7" r="1.6"/>
0478     <circle cx="608.5" cy="132.5" r="1.6"/>
0479     <circle cx="601.0" cy="129.1" r="1.6"/>
0480     <circle cx="601.3" cy="153.1" r="1.6"/>
0481     <circle cx="601.5" cy="177.2" r="1.6"/>
0482     <circle cx="601.8" cy="201.3" r="1.6"/>
0483   </g>
0484   <!-- one span per phi column -->
0485   <g stroke="var(--ink)" stroke-width="1" fill="none"><line x1="462" y1="36" x2="462" y2="211"/><line x1="459" y1="36" x2="465" y2="36"/><line x1="459" y1="211" x2="465" y2="211"/></g>
0486   <text x="469" y="122" class="ink">span of</text>
0487   <text x="469" y="134" class="ink">column 9</text>
0488   <text x="616" y="26" text-anchor="end" class="ink">seam</text>
0489   <text x="403" y="26" text-anchor="middle" class="acc">projected module outline</text>
0490   <text x="56" y="26" class="ink">seam: columns 0 and 13 hold their own spans</text>
0491   <text x="56" y="262">-pi</text>
0492   <text x="616" y="262" text-anchor="end">pi</text>
0493   <text x="336" y="262" text-anchor="middle">phi (axis 1, Closed)</text>
0494   <text x="50" y="40" text-anchor="end">400</text>
0495   <text x="50" y="246" text-anchor="end">200</text>
0496   <text x="50" y="144" text-anchor="end">r</text>
0497   <text x="336" y="278" text-anchor="middle">tinted: bins the surface is registered in; dots: bin centers inside the outline</text>
0498 </svg></div>
0499       <figcaption>A trapezoidal module on a disc layer, binned in <span class="mono">(r, phi)</span>: phi across, r up, the seam at &plusmn;pi ruled at both ends. The edges look curved because they are <em>straight in space</em> &mdash; a straight chord at perpendicular distance d from the beam line is <span class="mono">r = d / cos(phi &minus; phi&#8320;)</span> in these coordinates, so the inner and outer edges both bow toward smaller r at their midpoint. That is why the outline has to be sampled rather than taken from the vertices. Switch the test above: dashed red is a bin the module really overlaps that a bin-centre predicate throws away.</figcaption>
0500     </figure>
0501   </section>
0502 
0503   <section class="prose">
0504     <p>The three fills side by side, on the same module. Drag it inside any panel, or use the sliders; the presets walk through the three regimes.</p>
0505   </section>
0506 
0507   <section class="instrument">
0508     <div class="rail">
0509       <div class="ctl" style="min-width:210px;flex:0 0 auto">
0510         <span class="grouplabel">Regime</span>
0511         <div class="chips" id="preset">
0512           <button class="chip" data-preset="normal" aria-pressed="true">several bins across</button>
0513           <button class="chip" data-preset="small" aria-pressed="false">under one bin</button>
0514           <button class="chip" data-preset="aniso" aria-pressed="false">thin in r, wide in phi</button>
0515         </div>
0516       </div>
0517 
0518       <div class="ctl" style="min-width:150px;flex:0 0 auto">
0519         <span class="grouplabel">Bounds</span>
0520         <div class="chips" id="shape">
0521           <button class="chip" data-shape="sector" aria-pressed="true">radial</button>
0522           <button class="chip" data-shape="trapezoid" aria-pressed="false">disc trapezoid</button>
0523         </div>
0524       </div>
0525 
0526       <div class="ctl">
0527         <label for="rc"><span>centre radius</span><output id="rc-out"></output></label>
0528         <input type="range" id="rc" min="45" max="155" step="1">
0529       </div>
0530 
0531       <div class="ctl">
0532         <label for="hh"><span>half-length in r</span><output id="hh-out"></output></label>
0533         <input type="range" id="hh" min="4" max="55" step="1">
0534       </div>
0535 
0536       <div class="ctl">
0537         <label for="hw"><span>half-width</span><output id="hw-out"></output></label>
0538         <input type="range" id="hw" min="3" max="60" step="1">
0539       </div>
0540 
0541       <div class="ctl" style="min-width:110px;flex:0 0 auto">
0542         <label for="nr"><span>r bins</span><output id="nr-out"></output></label>
0543         <input type="range" id="nr" min="4" max="12" step="1">
0544       </div>
0545 
0546       <div class="ctl" style="min-width:110px;flex:0 0 auto">
0547         <label for="nphi"><span>phi bins in view</span><output id="nphi-out"></output></label>
0548         <input type="range" id="nphi" min="8" max="22" step="1">
0549       </div>
0550     </div>
0551 
0552     <div class="panels">
0553       <div class="panel">
0554         <div class="panel-head">
0555           <h3>1 &middot; Surface &rarr; grid</h3>
0556           <p class="sub">fillSurfaceToBinMapping<br>the reference position's bin, and nothing else</p>
0557         </div>
0558         <div id="svg-a"></div>
0559         <dl class="stats" id="stats-a"></dl>
0560       </div>
0561 
0562       <div class="panel">
0563         <div class="panel-head">
0564           <h3>2 &middot; + grid &rarr; surface</h3>
0565           <p class="sub">fillBinToSurfaceMapping<br>flood fill, keep bins whose centre is on the module</p>
0566         </div>
0567         <div id="svg-b"></div>
0568         <dl class="stats" id="stats-b"></dl>
0569       </div>
0570 
0571       <div class="panel">
0572         <div class="panel-head">
0573           <h3>3 &middot; Projected footprint</h3>
0574           <p class="sub">fillSurfaceFootprint<br>sample the outline, span-fill each phi column</p>
0575         </div>
0576         <div id="svg-c"></div>
0577         <dl class="stats" id="stats-c"></dl>
0578       </div>
0579     </div>
0580 
0581     <div class="legend">
0582       <span><i class="swatch" style="background:var(--served-fill);border-color:var(--served-line)"></i>registered, really overlapped</span>
0583       <span><i class="swatch" style="background:var(--miss-fill);border-color:var(--miss)"></i>overlapped but not registered</span>
0584       <span><i class="swatch" style="background:var(--waste-fill);border-color:var(--waste)"></i>registered without overlap</span>
0585       <span><i class="swatch" style="background:transparent"></i>&middot; bin centre &nbsp; &#10011; reference position</span>
0586     </div>
0587   </section>
0588 
0589   <section class="prose">
0590     <p class="note">What this view simplifies: it shows a phi sector with hard edges, so a module pushed off the side is clipped. The real phi axis is <code>Closed</code> and a sample past &plusmn;pi wraps to the bin on the other side. The r axis is <code>Bound</code>, where the wrap clamps into the edge bin rather than dropping the sample, which is why dragging past the top or bottom leaves no holes.</p>
0591   </section>
0592 
0593   <section class="prose">
0594     <h2>What &plusmn;1 at lookup time can and cannot repair</h2>
0595     <p>A lookup that widens every query to the neighbouring bins repairs a half-bin erosion, because the lost bins are all one step from a kept one &mdash; and it charges 3&times;3 candidates on <em>every</em> query to do it. It cannot repair a gap deeper than the expansion, and in the anisotropic regime the gap is not one bin.</p>
0596   </section>
0597 
0598   <div class="verdict" id="verdict"></div>
0599 
0600   <hr class="rule">
0601 
0602   <section class="prose">
0603     <h2>How finely the outline is sampled</h2>
0604     <p>The footprint fill walks the surface's polyhedron. That polyhedron approximates a curved edge with straight segments on a <em>global</em> phi grid of <code>4 &times; quarterSegments</code> steps, so at <code>quarterSegments = 1</code> a full ring is a square inscribed in the circle and the fill loses everything outside it. Planar bounds &mdash; rectangle, trapezoid &mdash; ignore the argument entirely, which is why the generic detector never showed this.</p>
0605   </section>
0606 
0607   <section class="ring">
0608     <div class="instrument" style="padding:16px 18px;display:flex;flex-direction:column;gap:12px">
0609       <div id="svg-ring"></div>
0610       <div class="chips" id="qseg" style="justify-content:center">
0611         <button class="chip" data-q="1" aria-pressed="true">1</button>
0612         <button class="chip" data-q="2" aria-pressed="false">2</button>
0613         <button class="chip" data-q="4" aria-pressed="false">4</button>
0614         <button class="chip" data-q="8" aria-pressed="false">8</button>
0615         <button class="chip" data-q="32" aria-pressed="false">32</button>
0616       </div>
0617       <p class="sub" style="font-family:var(--f-mono);font-size:11px;color:var(--muted);text-align:center;margin:0">quarterSegments &middot; full ring, r 200&ndash;300 mm, unrolled in phi</p>
0618     </div>
0619 
0620     <div>
0621       <div class="scroll">
0622         <table class="data">
0623           <thead>
0624             <tr><th>quarterSegments</th><th>outline falls short by</th></tr>
0625           </thead>
0626           <tbody id="qseg-table"></tbody>
0627         </table>
0628       </div>
0629       <p class="note" style="font-size:13.5px;margin-top:14px">Measured against a converged outline, per phi, for a full ring of r&nbsp;200&ndash;300&nbsp;mm. A radial sector of &plusmn;0.15&nbsp;rad loses 0.84&nbsp;mm at <span class="mono">q&nbsp;=&nbsp;1</span> through <span class="mono">q&nbsp;=&nbsp;8</span> alike &mdash; its phi range contains no multiple of pi/2, so nothing is added until the global grid is fine enough to drop a point inside it.</p>
0630     </div>
0631   </section>
0632 
0633   <section class="prose">
0634     <p>Spanning a column assumes an unbroken interval in r; a concave projection can include extra cells. Sampling can also miss cells between outline points on sufficiently fine grids. The optional <code>overfill</code> construction parameter expands each matched cell by a radius in bins: zero preserves the footprint, one includes immediate neighbors and diagonals, and n reaches n cells along each axis. It wraps the phi seam and stops at nonperiodic grid edges.</p>
0635     <p>On the generic detector, across all 48 layers against brute-force truth: hits lost at |eta|&nbsp;=&nbsp;3.5 go from <strong>4.9% to zero</strong>. With the &plusmn;1 expansion still in place that costs 9.0&nbsp;&rarr;&nbsp;25.0 candidates per query at eta&nbsp;=&nbsp;0. With the expansion switched off, the same scan gives <strong>3.9 candidates on the disc and 9.0 on the barrel with nothing lost</strong>: today's cost, for strictly better coverage.</p>
0636   </section>
0637 
0638   <hr class="rule">
0639 
0640   <section class="prose">
0641     <h2 id="window">How far the track slides</h2>
0642     <p>A barrel layer is not a cylinder. Its modules sit staggered either side of one, and the array bins them by projecting each module radially onto the representative cylinder in between. A lookup intersects that same cylinder and reads the bin it lands in.</p>
0643     <p>Those two points are not the same point. A track crossing at an angle enters the layer, travels a chord through its thickness and leaves somewhere else in z. At normal incidence the displacement is nothing; at <span class="mono">eta&nbsp;=&nbsp;3</span> the chord is ten times the layer half-thickness and the crossing bin no longer holds the module the track hits. The window around the crossing bin is what closes the gap, and the question is how wide it has to be.</p>
0644     <p>Everything below is computed from the controls, not quoted: <span class="mono">z(r) = r&nbsp;sinh&nbsp;eta</span>, so the slide from the representative surface to a module offset by <span class="mono">&Delta;r</span> is <span class="mono">&Delta;r&nbsp;sinh&nbsp;eta</span>, and the chord the window is sized from is <span class="mono">tolerance&nbsp;&times;&nbsp;sinh&nbsp;eta</span>.</p>
0645   </section>
0646 
0647   <section class="figrail">
0648     <span class="grouplabel">neighbor window on both views</span>
0649     <div class="chips" id="wstep">
0650       <button class="chip" data-d="0" aria-pressed="false">crossing bin only</button>
0651       <button class="chip" data-d="1" aria-pressed="false">&plusmn;1 bin</button>
0652       <button class="chip" data-d="2" aria-pressed="true">&plusmn;2 bins</button>
0653     </div>
0654     <span class="verdict-inline" id="wverdict"></span>
0655   </section>
0656 
0657   <section class="pair">
0658     <figure>
0659       <div class="plate"><svg class="figrz" viewBox="0 0 640 300" role="img"
0660      aria-label="Lookup geometry in the r-z plane: staggered modules at two radii, the representative cylinder, an inclined track and the slide in z through the layer thickness">
0661   <style>
0662 .figrz text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; fill: var(--muted); }
0663 .figrz .ink { fill: var(--ink); }
0664 .figrz .acc { fill: var(--served); }
0665 .figrz .alert { fill: var(--miss); }
0666 </style>
0667   <defs>
0668     <marker id="arrow-figrz" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
0669       <path d="M0,0.5 L8,4 L0,7.5 Z" fill="var(--ink)"/>
0670     </marker>
0671   </defs>
0672   <rect width="640" height="300" fill="var(--panel)"/>
0673 
0674   <!-- z bins: the window the lookup serves, and the crossing bin inside it -->
0675   <rect x="160" y="30" width="250" height="220" fill="var(--served)" fill-opacity=".09" data-w="rfill"/>
0676   <rect x="260" y="30" width="50" height="220" fill="var(--served)" fill-opacity=".16"/>
0677   <g stroke="var(--rule-soft)" stroke-width=".6">
0678     <line x1="60" y1="30" x2="60" y2="250"/>
0679     <line x1="110" y1="30" x2="110" y2="250"/>
0680     <line x1="160" y1="30" x2="160" y2="250"/>
0681     <line x1="210" y1="30" x2="210" y2="250"/>
0682     <line x1="260" y1="30" x2="260" y2="250"/>
0683     <line x1="310" y1="30" x2="310" y2="250"/>
0684     <line x1="360" y1="30" x2="360" y2="250"/>
0685     <line x1="410" y1="30" x2="410" y2="250"/>
0686     <line x1="460" y1="30" x2="460" y2="250"/>
0687     <line x1="510" y1="30" x2="510" y2="250"/>
0688     <line x1="560" y1="30" x2="560" y2="250"/>
0689     <line x1="610" y1="30" x2="610" y2="250"/>
0690   </g>
0691   <text x="285" y="44" text-anchor="middle" class="acc" data-w="rlabel">window: 2 bins each side</text>
0692 
0693   <!-- layer thickness: the tolerance the array was built with -->
0694   <g stroke="var(--muted)" stroke-width=".8" stroke-dasharray="3 3">
0695     <line x1="60" y1="115" x2="610" y2="115"/>
0696     <line x1="60" y1="165" x2="610" y2="165"/>
0697   </g>
0698   <g stroke="var(--muted)" stroke-width="1">
0699     <line x1="74" y1="115" x2="74" y2="140"/>
0700     <line x1="71" y1="115" x2="77" y2="115"/>
0701     <line x1="71" y1="140" x2="77" y2="140"/>
0702   </g>
0703   <text x="80" y="131">tolerance</text>
0704 
0705   <!-- representative cylinder at radius R -->
0706   <line x1="60" y1="140" x2="610" y2="140" stroke="var(--served)" stroke-width="1.5"/>
0707   <text x="608" y="134" text-anchor="end" class="acc">representative cylinder</text>
0708 
0709   <!-- footprint of the hit module on the representative surface: bins 5, 6, 7 -->
0710   <rect x="310" y="137" width="150" height="6" fill="var(--served)" fill-opacity=".35" data-w="rband"/>
0711   <text x="385" y="156" text-anchor="middle" class="acc">registered footprint</text>
0712 
0713   <!-- modules, staggered on two radii -->
0714   <g fill="var(--muted)">
0715     <rect x="60" y="157" width="80" height="6"/>
0716     <rect x="250" y="157" width="110" height="6"/>
0717     <rect x="420" y="157" width="110" height="6"/>
0718     <rect x="130" y="117" width="130" height="6"/>
0719     <rect x="520" y="117" width="90" height="6"/>
0720   </g>
0721   <rect x="320" y="117" width="110" height="6" fill="var(--served)" data-w="rmod"/>
0722   <text x="608" y="181" text-anchor="end">modules at two radii</text>
0723 
0724   <!-- the track, and the chord it cuts through the layer -->
0725   <line x1="225" y1="165" x2="385" y2="115" stroke="var(--served)" stroke-opacity=".35" stroke-width="6"/>
0726   <line x1="100" y1="204" x2="560" y2="60" stroke="var(--ink)" stroke-width="1.4" marker-end="url(#arrow-figrz)"/>
0727   <text x="108" y="222" class="ink">track</text>
0728   <text x="236" y="178" class="ink">half chord = tolerance / |n.d|</text>
0729 
0730   <!-- crossing of the representative surface, and the hit -->
0731   <circle cx="305" cy="140" r="3.5" fill="var(--served)"/>
0732   <text x="298" y="134" text-anchor="end" class="acc">crossing</text>
0733   <circle cx="369" cy="120" r="3.5" fill="var(--miss)"/>
0734   <text x="376" y="111" class="alert">hit</text>
0735 
0736   <!-- the slide: the chord without its component along the normal -->
0737   <g stroke="var(--ink)" stroke-width="1">
0738     <line x1="225" y1="232" x2="385" y2="232"/>
0739     <line x1="225" y1="228" x2="225" y2="236"/>
0740     <line x1="305" y1="228" x2="305" y2="236"/>
0741     <line x1="385" y1="228" x2="385" y2="236"/>
0742   </g>
0743   <text x="265" y="246" text-anchor="middle" class="ink">slide</text>
0744   <text x="345" y="246" text-anchor="middle" class="ink">slide</text>
0745 
0746   <!-- axes -->
0747   <text x="22" y="143">r</text>
0748   <text x="610" y="270" text-anchor="end">z</text>
0749   <text x="335" y="270" text-anchor="middle" data-w="rcaption">bins 2 to 6 are served; the module the track hits is registered in bins 5 to 7</text>
0750 </svg></div>
0751       <figcaption>Staggered modules either side of the representative cylinder. The track crosses the cylinder in one bin and reaches the module it hits in another; the chord through the layer thickness is what separates them.</figcaption>
0752     </figure>
0753     <figure>
0754       <div class="plate"><svg class="figgrid" viewBox="0 0 640 300" role="img"
0755      aria-label="The same crossing on the (phi, z) grid: the crossing bin, the window opened along z only, and the registered footprint of the module the track hits">
0756   <style>
0757 .figgrid text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; fill: var(--muted); }
0758 .figgrid .ink { fill: var(--ink); }
0759 .figgrid .acc { fill: var(--served); }
0760 .figgrid .alert { fill: var(--miss); }
0761 .figgrid .idx { font-size: 9px; }
0762 </style>
0763   <defs>
0764     <pattern id="hatch-figgrid" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
0765       <line x1="0" y1="0" x2="0" y2="6" stroke="var(--served)" stroke-opacity=".55" stroke-width="1"/>
0766     </pattern>
0767   </defs>
0768   <rect width="640" height="300" fill="var(--panel)"/>
0769 
0770   <!-- the window: z distance 2, phi distance 0, around the crossing bin -->
0771   <rect x="160" y="130" width="250" height="30" fill="var(--served)" fill-opacity=".12" data-w="gfill"/>
0772   <rect x="260" y="130" width="50" height="30" fill="var(--served)" fill-opacity=".2"/>
0773 
0774   <!-- registered footprint of the hit module: z bins 5 to 7, phi bin 3 -->
0775   <rect x="310" y="130" width="150" height="30" fill="url(#hatch-figgrid)"/>
0776   <rect x="160" y="130" width="250" height="30" fill="none" stroke="var(--served)" stroke-width="1.2" data-w="gline"/>
0777 
0778   <!-- grid rules: 11 z bins by 7 phi bins -->
0779   <g stroke="var(--rule)" stroke-width=".6" fill="none">
0780     <line x1="60" y1="40" x2="60" y2="250"/>
0781     <line x1="110" y1="40" x2="110" y2="250"/>
0782     <line x1="160" y1="40" x2="160" y2="250"/>
0783     <line x1="210" y1="40" x2="210" y2="250"/>
0784     <line x1="260" y1="40" x2="260" y2="250"/>
0785     <line x1="310" y1="40" x2="310" y2="250"/>
0786     <line x1="360" y1="40" x2="360" y2="250"/>
0787     <line x1="410" y1="40" x2="410" y2="250"/>
0788     <line x1="460" y1="40" x2="460" y2="250"/>
0789     <line x1="510" y1="40" x2="510" y2="250"/>
0790     <line x1="560" y1="40" x2="560" y2="250"/>
0791     <line x1="610" y1="40" x2="610" y2="250"/>
0792     <line x1="60" y1="40" x2="610" y2="40"/>
0793     <line x1="60" y1="70" x2="610" y2="70"/>
0794     <line x1="60" y1="100" x2="610" y2="100"/>
0795     <line x1="60" y1="130" x2="610" y2="130"/>
0796     <line x1="60" y1="160" x2="610" y2="160"/>
0797     <line x1="60" y1="190" x2="610" y2="190"/>
0798     <line x1="60" y1="220" x2="610" y2="220"/>
0799     <line x1="60" y1="250" x2="610" y2="250"/>
0800   </g>
0801 
0802   <!-- module outline: a rectangle on a cylinder projects to a rectangle in (phi, z) -->
0803   <rect x="316" y="134" width="138" height="22" fill="none" stroke="var(--served)" stroke-width="1.6" data-w="gmod"/>
0804   <text x="466" y="142" class="acc">footprint of</text>
0805   <text x="466" y="154" class="acc">the hit module</text>
0806 
0807   <!-- crossing, slide along z, hit -->
0808   <line x1="305" y1="145" x2="369" y2="145" stroke="var(--ink)" stroke-width="1" stroke-dasharray="3 2"/>
0809   <circle cx="305" cy="145" r="3.5" fill="var(--served)"/>
0810   <circle cx="369" cy="145" r="3.5" fill="var(--miss)"/>
0811   <text x="305" y="122" text-anchor="middle" class="acc">crossing bin</text>
0812   <text x="369" y="122" text-anchor="middle" class="alert">hit</text>
0813   <text x="160" y="182" data-w="glabel">window: 2 in z, 0 in phi</text>
0814   <text x="160" y="206" class="ink" data-w="gverdict">the crossing bin alone would miss the module; the window in z reaches it</text>
0815 
0816   <!-- bin indices -->
0817   <g class="idx" text-anchor="middle">
0818     <text x="85" y="264">0</text>
0819     <text x="135" y="264">1</text>
0820     <text x="185" y="264">2</text>
0821     <text x="235" y="264">3</text>
0822     <text x="285" y="264">4</text>
0823     <text x="335" y="264">5</text>
0824     <text x="385" y="264">6</text>
0825     <text x="435" y="264">7</text>
0826     <text x="485" y="264">8</text>
0827     <text x="535" y="264">9</text>
0828     <text x="585" y="264">10</text>
0829   </g>
0830   <g class="idx" text-anchor="end">
0831     <text x="52" y="58">6</text>
0832     <text x="52" y="88">5</text>
0833     <text x="52" y="118">4</text>
0834     <text x="52" y="148">3</text>
0835     <text x="52" y="178">2</text>
0836     <text x="52" y="208">1</text>
0837     <text x="52" y="238">0</text>
0838   </g>
0839   <text x="610" y="284" text-anchor="end">z bin (axis 1, Bound)</text>
0840   <text x="60" y="284">phi bin (axis 0, Closed)</text>
0841 </svg></div>
0842       <figcaption>The same crossing on the bin grid. The window opens along z only &mdash; a straight track from the beam line barely moves in the angular coordinate, and widening that axis would only multiply candidates.</figcaption>
0843     </figure>
0844   </section>
0845 
0846   <section class="prose">
0847     <h2>Vary it</h2>
0848     <p>Those two are one crossing at one angle. Below, the same geometry with the angle, the stagger, the binning and the module length in your hands, and three window policies scored against the modules the track actually crosses.</p>
0849   </section>
0850 
0851   <section class="instrument">
0852     <div class="rail">
0853       <div class="ctl">
0854         <label for="eta"><span>pseudorapidity eta</span><output id="eta-out"></output></label>
0855         <input type="range" id="eta" min="0" max="3.5" step="0.01">
0856       </div>
0857       <div class="ctl">
0858         <label for="stag"><span>radial stagger</span><output id="stag-out"></output></label>
0859         <input type="range" id="stag" min="0.5" max="6" step="0.1">
0860       </div>
0861       <div class="ctl">
0862         <label for="bf"><span>bins per module</span><output id="bf-out"></output></label>
0863         <input type="range" id="bf" min="1" max="8" step="1">
0864       </div>
0865       <div class="ctl">
0866         <label for="hz"><span>module half-length in z</span><output id="hz-out"></output></label>
0867         <input type="range" id="hz" min="10" max="45" step="1">
0868       </div>
0869     </div>
0870 
0871     <div class="stage"><div id="svg-rz"></div></div>
0872 
0873     <div class="policies">
0874       <div class="policy">
0875         <div>
0876           <h3>Crossing bin only</h3>
0877           <p class="sub">no window</p>
0878         </div>
0879         <div id="strip-0"></div>
0880         <dl class="stats" id="stats-0"></dl>
0881       </div>
0882       <div class="policy">
0883         <div>
0884           <h3>Fixed &plusmn;1 bin</h3>
0885           <p class="sub">what main serves &mdash; #5186's 1/|n&middot;d| term is capped at 1 by every caller, so it never fires</p>
0886         </div>
0887         <div id="strip-1"></div>
0888         <dl class="stats" id="stats-1"></dl>
0889       </div>
0890       <div class="policy">
0891         <div>
0892           <h3>Sized by the slide</h3>
0893           <p class="sub">bins spanned by &plusmn;tolerance&nbsp;sinh&nbsp;eta, clamped to NeighborWindow max 2 in z</p>
0894         </div>
0895         <div id="strip-2"></div>
0896         <dl class="stats" id="stats-2"></dl>
0897       </div>
0898     </div>
0899 
0900     <div class="legend">
0901       <span><i class="swatch" style="background:var(--served-fill);border-color:var(--served-line)"></i>bin served by the lookup</span>
0902       <span><i class="swatch" style="background:var(--waste-fill);border-color:var(--waste)"></i>served, holds nothing the track hits</span>
0903       <span><i class="swatch" style="background:var(--miss-fill);border-color:var(--miss)"></i>holds the hit module, not served</span>
0904       <span>&#9679; crossing &nbsp; &#9670; where the track meets the module</span>
0905     </div>
0906   </section>
0907 
0908   <section class="prose">
0909     <p class="note">The stagger and the tolerance are tied, as they are in the creator: <code>SurfaceArrayCreator</code> takes the tolerance from <code>protoLayer.range(AxisR) * 0.5</code>, so it is the stagger plus the module half-thickness. That is why the window always covers the slide it needs to &mdash; the same radial extent sets both.</p>
0910   </section>
0911 
0912   <hr class="rule">
0913 
0914   <section class="prose">
0915     <h2>Across the barrel</h2>
0916     <p>One eta proves nothing. Sweeping the whole range at the current stagger and binning gives the miss rate each policy carries, and what it charges in candidates to get there.</p>
0917   </section>
0918 
0919   <section class="sweep">
0920     <div class="instrument" style="padding:16px 18px 10px">
0921       <div id="svg-sweep"></div>
0922     </div>
0923     <div>
0924       <div class="scroll">
0925         <table class="data">
0926           <thead>
0927             <tr><th>policy</th><th>misses</th><th>candidates</th></tr>
0928           </thead>
0929           <tbody id="sweep-table"></tbody>
0930         </table>
0931       </div>
0932       <p class="note" style="font-size:13.5px;margin-top:14px">Miss rate over 400 eta samples from 0 to 3.5, mean candidates per query over the same. A miss is a track whose module is registered only in bins the lookup never reads &mdash; the hit is unrecoverable, no later stage sees it.</p>
0933     </div>
0934   </section>
0935 
0936   <hr class="rule">
0937 
0938   <section class="prose">
0939     <h2 id="cost">What it costs</h2>
0940     <p>Measuring the slide is not free. As first written it went through two full <code>Surface::intersect</code> calls to project the ends of the chord, and that tripled the price of a lookup. Taking the same step from <code>localCartesianToBoundLocalDerivative</code> instead &mdash; the reference frame with the curvilinear metric already folded in &mdash; costs one Jacobian apply.</p>
0941   </section>
0942 
0943   <section class="instrument" style="padding:18px 22px 20px">
0944     <div class="scroll">
0945       <table class="data">
0946         <thead>
0947           <tr><th>variant</th><th>ns / lookup</th><th>surfaces returned</th></tr>
0948         </thead>
0949         <tbody>
0950           <tr><td>before, fixed &plusmn;1</td><td>76</td><td>14.2</td></tr>
0951           <tr><td>window from two projections</td><td class="bad">245</td><td>3.14</td></tr>
0952           <tr><td>window from the surface metric</td><td>128</td><td>3.14</td></tr>
0953           <tr><td>+ Jacobian without the angle</td><td>110</td><td>3.14</td></tr>
0954           <tr><td>+ grid scale resolved once</td><td>104</td><td>3.14</td></tr>
0955           <tr><td>+ rigid placement inverses</td><td class="ok">80</td><td>3.14</td></tr>
0956         </tbody>
0957       </table>
0958     </div>
0959     <p class="note" style="margin-top:14px">Toy barrel, 60 &times; 20 bins, 3.28M lookups, RelWithDebInfo with assertions forced on. The narrower window returns 4.5&times; fewer candidates, and each one it drops is an intersection the caller does not run &mdash; so even the 245 ns version paid for itself. The remaining 80 ns is roughly what the fixed window cost before any of this.</p>
0960   </section>
0961 
0962   <section class="prose">
0963     <p class="note">What this simplifies: one phi slice, straight rays from the origin, and a barrel of identical modules on two radii. A real track curves and starts off-axis, which moves the angular axis too &mdash; the reason the window's phi bound is 1 rather than 0 even though a straight ray from the origin never needs it. The disc case swaps the axes: the slide runs in r, and the same metric returns <span class="mono">(dr, dphi)</span> without a per-surface-type case.</p>
0964   </section>
0965 
0966   <hr class="rule">
0967 
0968   <p class="back"><a href="group__surface__array.html">&larr; Surface array reference</a></p>
0969 
0970 </div>
0971 
0972 <script>
0973 (() => {
0974   "use strict";
0975 
0976   // ---------------------------------------------------------------- geometry
0977 
0978   const LAYER = { rLo: 30, rHi: 170, phiLo: -0.42, phiHi: 0.42 };
0979 
0980   const S = {
0981     shape: "sector",
0982     rc: 100, hh: 32, hw: 14, pc: 0,
0983     nR: 7, nPhi: 14,
0984   };
0985 
0986   // edges chosen to sit inside bins, not on their boundaries, so each regime
0987   // shows its own erosion rather than a coincidence of the binning
0988   const PRESETS = {
0989     normal: { rc: 100, hh: 32, hw: 14, pc: 0,    nR: 7, nPhi: 14 },
0990     small:  { rc: 100, hh: 5,  hw: 3,  pc: 0.03, nR: 7, nPhi: 14 },
0991     aniso:  { rc: 90,  hh: 6,  hw: 20, pc: 0,    nR: 7, nPhi: 14 },
0992   };
0993 
0994   const edges = (lo, hi, n) => Array.from({ length: n + 1 }, (_, i) => lo + (hi - lo) * i / n);
0995 
0996   // multiples of (pi/2)/q strictly inside [a,b], plus the ends — Acts::detail::VerticesHelper::phiSegments
0997   function phiSegments(a, b, q) {
0998     const out = [a, b];
0999     const step = 2 * Math.PI / (4 * q);
1000     for (let i = 0; i < 4 * q + 1; i++) {
1001       const p = -Math.PI + i * step;
1002       if (p > a && p < b && !out.some(x => Math.abs(x - p) < 1e-12)) out.push(p);
1003     }
1004     return out.sort((x, y) => x - y);
1005   }
1006 
1007   // cartesian outline vertices, the way polyhedronRepresentation would produce them
1008   function polyhedron(q) {
1009     if (S.shape === "trapezoid") {
1010       // TrapezoidBounds::vertices ignores the segment count
1011       const c = Math.cos(S.pc), s = Math.sin(S.pc);
1012       return [[-S.hw, -S.hh], [S.hw, -S.hh], [S.hw, S.hh], [-S.hw, S.hh]]
1013         // local y runs along r, local x tangentially, as a disc trapezoid does
1014         .map(([lx, ly]) => { const r = ly + S.rc; return [c * r - s * lx, s * r + c * lx]; });
1015     }
1016     const half = Math.atan(S.hw / S.rc);
1017     const rIn = Math.max(1, S.rc - S.hh), rOut = S.rc + S.hh;
1018     const phis = phiSegments(S.pc - half, S.pc + half, q);
1019     const out = [];
1020     for (const p of phis) out.push([rOut * Math.cos(p), rOut * Math.sin(p)]);
1021     for (let i = phis.length - 1; i >= 0; i--) {
1022       const p = phis[i];
1023       out.push([rIn * Math.cos(p), rIn * Math.sin(p)]);
1024     }
1025     return out;
1026   }
1027 
1028   // walk the outline in straight chords, mapping every sample into (phi, r)
1029   function outlineInGrid(q, perChord) {
1030     const v = polyhedron(q), out = [];
1031     for (let i = 0; i < v.length; i++) {
1032       const a = v[i], b = v[(i + 1) % v.length];
1033       for (let k = 0; k < perChord; k++) {
1034         const t = k / perChord;
1035         const x = a[0] + t * (b[0] - a[0]), y = a[1] + t * (b[1] - a[1]);
1036         out.push([Math.atan2(y, x), Math.hypot(x, y)]);
1037       }
1038     }
1039     return out;
1040   }
1041 
1042   const truthPolygon = () => outlineInGrid(256, 8);
1043 
1044   function pointInPolygon(px, py, poly) {
1045     let inside = false;
1046     for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
1047       const [xi, yi] = poly[i], [xj, yj] = poly[j];
1048       if ((yi > py) !== (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
1049     }
1050     return inside;
1051   }
1052 
1053   // Sutherland–Hodgman against an axis-aligned box; non-zero area means real overlap
1054   function overlapsBox(poly, x0, x1, y0, y1) {
1055     let cur = poly;
1056     const clip = (pts, keep, cut) => {
1057       const out = [];
1058       for (let i = 0; i < pts.length; i++) {
1059         const a = pts[(i + pts.length - 1) % pts.length], b = pts[i];
1060         const ka = keep(a), kb = keep(b);
1061         if (kb) { if (!ka) out.push(cut(a, b)); out.push(b); }
1062         else if (ka) out.push(cut(a, b));
1063       }
1064       return out;
1065     };
1066     const lerpX = (a, b, x) => [x, a[1] + (b[1] - a[1]) * (x - a[0]) / (b[0] - a[0])];
1067     const lerpY = (a, b, y) => [a[0] + (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]), y];
1068     cur = clip(cur, p => p[0] >= x0, (a, b) => lerpX(a, b, x0));
1069     if (!cur.length) return false;
1070     cur = clip(cur, p => p[0] <= x1, (a, b) => lerpX(a, b, x1));
1071     if (!cur.length) return false;
1072     cur = clip(cur, p => p[1] >= y0, (a, b) => lerpY(a, b, y0));
1073     if (!cur.length) return false;
1074     cur = clip(cur, p => p[1] <= y1, (a, b) => lerpY(a, b, y1));
1075     if (cur.length < 3) return false;
1076     let area = 0;
1077     for (let i = 0; i < cur.length; i++) {
1078       const a = cur[i], b = cur[(i + 1) % cur.length];
1079       area += a[0] * b[1] - b[0] * a[1];
1080     }
1081     return Math.abs(area) > 1e-9;
1082   }
1083 
1084   // ------------------------------------------------------------- the filling
1085 
1086   function model() {
1087     const pe = edges(LAYER.phiLo, LAYER.phiHi, S.nPhi);
1088     const re = edges(LAYER.rLo, LAYER.rHi, S.nR);
1089     const poly = truthPolygon();
1090     const key = (i, j) => j * S.nPhi + i;
1091 
1092     const truth = new Set();
1093     for (let i = 0; i < S.nPhi; i++)
1094       for (let j = 0; j < S.nR; j++)
1095         if (overlapsBox(poly, pe[i], pe[i + 1], re[j], re[j + 1])) truth.add(key(i, j));
1096 
1097     // 1 — the reference position's bin
1098     const refPhi = S.pc, refR = S.rc;
1099     const bi = Math.floor((refPhi - LAYER.phiLo) / (LAYER.phiHi - LAYER.phiLo) * S.nPhi);
1100     const bj = Math.floor((refR - LAYER.rLo) / (LAYER.rHi - LAYER.rLo) * S.nR);
1101     const centreSet = new Set();
1102     const inView = bi >= 0 && bi < S.nPhi && bj >= 0 && bj < S.nR;
1103     if (inView) centreSet.add(key(bi, bj));
1104 
1105     // 2 — flood fill on the bin-centre predicate, seeded from that bin
1106     const floodSet = new Set(centreSet);
1107     if (inView) {
1108       const seen = new Set([key(bi, bj)]);
1109       const queue = [[bi, bj]];
1110       while (queue.length) {
1111         const [i, j] = queue.pop();
1112         for (let di = -1; di <= 1; di++) for (let dj = -1; dj <= 1; dj++) {
1113           const ni = i + di, nj = j + dj;
1114           if (ni < 0 || nj < 0 || ni >= S.nPhi || nj >= S.nR) continue;
1115           const k = key(ni, nj);
1116           if (seen.has(k)) continue;
1117           seen.add(k);
1118           const cp = (pe[ni] + pe[ni + 1]) / 2, cr = (re[nj] + re[nj + 1]) / 2;
1119           if (!pointInPolygon(cp, cr, poly)) continue;
1120           floodSet.add(k);
1121           queue.push([ni, nj]);
1122         }
1123       }
1124     }
1125 
1126     // 3 — outline samples, then a span per phi column
1127     const cols = new Map();
1128     for (const [p, r] of outlineInGrid(32, 32)) {
1129       const i = Math.floor((p - LAYER.phiLo) / (LAYER.phiHi - LAYER.phiLo) * S.nPhi);
1130       // the r axis is Bound, and Axis::wrapBin clamps a Bound index into
1131       // [1, nBins] — a sample past the layer edge lands in the edge bin
1132       const j = Math.min(S.nR - 1, Math.max(0,
1133         Math.floor((r - LAYER.rLo) / (LAYER.rHi - LAYER.rLo) * S.nR)));
1134       if (i < 0 || i >= S.nPhi) continue;
1135       const span = cols.get(i);
1136       if (!span) cols.set(i, [j, j]);
1137       else { span[0] = Math.min(span[0], j); span[1] = Math.max(span[1], j); }
1138     }
1139     const footSet = new Set();
1140     for (const [i, [lo, hi]] of cols) for (let j = lo; j <= hi; j++) footSet.add(key(i, j));
1141 
1142     return { pe, re, poly, truth, centreSet, floodSet, footSet, key };
1143   }
1144 
1145   // Chebyshev distance from every truth bin to the nearest registered bin
1146   function gapStats(reg, truth, key) {
1147     let worst = 0, unreachable = 0;
1148     for (const t of truth) {
1149       if (reg.has(t)) continue;
1150       const ti = t % S.nPhi, tj = Math.floor(t / S.nPhi);
1151       let best = Infinity;
1152       for (const r of reg) {
1153         const ri = r % S.nPhi, rj = Math.floor(r / S.nPhi);
1154         best = Math.min(best, Math.max(Math.abs(ti - ri), Math.abs(tj - rj)));
1155       }
1156       if (!isFinite(best)) unreachable++;
1157       else worst = Math.max(worst, best);
1158     }
1159     return { worst, unreachable };
1160   }
1161 
1162   function recoveredAt(reg, truth, d) {
1163     let n = 0;
1164     for (const t of truth) {
1165       const ti = t % S.nPhi, tj = Math.floor(t / S.nPhi);
1166       for (const r of reg) {
1167         const ri = r % S.nPhi, rj = Math.floor(r / S.nPhi);
1168         if (Math.max(Math.abs(ti - ri), Math.abs(tj - rj)) <= d) { n++; break; }
1169       }
1170     }
1171     return n;
1172   }
1173 
1174   // ------------------------------------------------------------------ canvas
1175 
1176   const W = 300, H = 236, M = { l: 34, r: 10, t: 10, b: 26 };
1177   const PW = W - M.l - M.r, PH = H - M.t - M.b;
1178 
1179   const sx = p => M.l + (p - LAYER.phiLo) / (LAYER.phiHi - LAYER.phiLo) * PW;
1180   const sy = r => M.t + PH - (r - LAYER.rLo) / (LAYER.rHi - LAYER.rLo) * PH;
1181 
1182   const esc = n => Number(n).toFixed(2);
1183 
1184   function drawPanel(m, reg) {
1185     const { pe, re, poly, truth } = m;
1186     let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="phi versus r bin grid with the module footprint">`;
1187 
1188     for (let i = 0; i < S.nPhi; i++) for (let j = 0; j < S.nR; j++) {
1189       const k = m.key(i, j), isTruth = truth.has(k), isReg = reg.has(k);
1190       if (!isTruth && !isReg) continue;
1191       const x = sx(pe[i]), y = sy(re[j + 1]);
1192       const w = sx(pe[i + 1]) - x, h = sy(re[j]) - y;
1193       let fill = "var(--served-fill)", stroke = "var(--served-line)";
1194       if (isTruth && !isReg) { fill = "var(--miss-fill)"; stroke = "var(--miss)"; }
1195       else if (!isTruth && isReg) { fill = "var(--waste-fill)"; stroke = "var(--waste)"; }
1196       s += `<rect x="${esc(x)}" y="${esc(y)}" width="${esc(w)}" height="${esc(h)}" fill="${fill}" stroke="${stroke}" stroke-width=".8"/>`;
1197     }
1198 
1199     s += `<g stroke="var(--rule)" stroke-width=".5" fill="none">`;
1200     for (let i = 0; i <= S.nPhi; i++) s += `<line x1="${esc(sx(pe[i]))}" y1="${M.t}" x2="${esc(sx(pe[i]))}" y2="${M.t + PH}"/>`;
1201     for (let j = 0; j <= S.nR; j++) s += `<line x1="${M.l}" y1="${esc(sy(re[j]))}" x2="${M.l + PW}" y2="${esc(sy(re[j]))}"/>`;
1202     s += `</g>`;
1203 
1204     s += `<g fill="var(--muted)">`;
1205     for (let i = 0; i < S.nPhi; i++) for (let j = 0; j < S.nR; j++) {
1206       const cp = (pe[i] + pe[i + 1]) / 2, cr = (re[j] + re[j + 1]) / 2;
1207       const on = pointInPolygon(cp, cr, poly);
1208       s += `<circle cx="${esc(sx(cp))}" cy="${esc(sy(cr))}" r="${on ? 1.9 : 1}" fill="${on ? "var(--served)" : "var(--rule)"}"/>`;
1209     }
1210     s += `</g>`;
1211 
1212     const d = poly.map((p, i) => `${i ? "L" : "M"}${esc(sx(p[0]))} ${esc(sy(p[1]))}`).join("") + "Z";
1213     s += `<path d="${d}" fill="none" stroke="var(--served)" stroke-width="1.6" stroke-linejoin="round"/>`;
1214 
1215     const cx = sx(S.pc), cy = sy(S.rc);
1216     s += `<g stroke="var(--served)" stroke-width="1.2"><line x1="${esc(cx - 4)}" y1="${esc(cy)}" x2="${esc(cx + 4)}" y2="${esc(cy)}"/><line x1="${esc(cx)}" y1="${esc(cy - 4)}" x2="${esc(cx)}" y2="${esc(cy + 4)}"/></g>`;
1217 
1218     s += `<g fill="var(--muted)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="8">`;
1219     s += `<text x="${M.l}" y="${H - 8}">φ ${LAYER.phiLo}</text>`;
1220     s += `<text x="${M.l + PW}" y="${H - 8}" text-anchor="end">${LAYER.phiHi} rad</text>`;
1221     s += `<text x="4" y="${M.t + 7}">${LAYER.rHi}</text>`;
1222     s += `<text x="4" y="${M.t + PH}">${LAYER.rLo} mm</text>`;
1223     s += `</g></svg>`;
1224     return s;
1225   }
1226 
1227   function statsHTML(m, reg) {
1228     const covered = [...m.truth].filter(k => reg.has(k)).length;
1229     const missed = m.truth.size - covered;
1230     const spurious = [...reg].filter(k => !m.truth.has(k)).length;
1231     const { worst } = gapStats(reg, m.truth, m.key);
1232     const pct = m.truth.size ? Math.round(100 * covered / m.truth.size) : 100;
1233     return `
1234       <dt>registered</dt><dd>${reg.size}</dd>
1235       <dt>of ${m.truth.size} overlapped</dt><dd class="${pct === 100 ? "good" : "bad"}">${pct}%</dd>
1236       <dt>missed</dt><dd class="${missed ? "bad" : ""}">${missed}</dd>
1237       <dt>without overlap</dt><dd>${spurious}</dd>
1238       <dt>deepest gap</dt><dd class="${worst > 1 ? "bad" : ""}">${missed ? worst + " bin" + (worst === 1 ? "" : "s") : "—"}</dd>`;
1239   }
1240 
1241   function verdictHTML(m) {
1242     const need = d => recoveredAt(m.floodSet, m.truth, d) === m.truth.size;
1243     let dNeed = 0;
1244     while (dNeed < 12 && !need(dNeed)) dNeed++;
1245     const at1 = recoveredAt(m.floodSet, m.truth, 1);
1246     const lostAt1 = m.truth.size - at1;
1247     const footOk = [...m.truth].every(k => m.footSet.has(k));
1248 
1249     return `
1250       <div class="verdict-grid">
1251         <div class="vcell">
1252           <span class="k">flood fill, ±1 expansion</span>
1253           <span class="v ${lostAt1 ? "bad" : "good"}">${at1} / ${m.truth.size}</span>
1254           <span class="n">${lostAt1 ? lostAt1 + " overlapped bin" + (lostAt1 === 1 ? "" : "s") + " still unreachable" : "the erosion is exactly one bin deep here"}</span>
1255         </div>
1256         <div class="vcell">
1257           <span class="k">expansion it would need</span>
1258           <span class="v ${dNeed > 1 ? "bad" : ""}">±${dNeed}</span>
1259           <span class="n">${(2 * dNeed + 1) ** 2}× the candidates on every query, to repair a fill-time error</span>
1260         </div>
1261         <div class="vcell">
1262           <span class="k">projected footprint</span>
1263           <span class="v ${footOk ? "good" : "bad"}">±0</span>
1264           <span class="n">${footOk ? "complete with no expansion at all" : "incomplete — the outline is not convex per column here"}</span>
1265         </div>
1266       </div>`;
1267   }
1268 
1269   // ------------------------------------------------------------- ring figure
1270 
1271   const RING = { rLo: 200, rHi: 300, rIn: 200, rOut: 300, nR: 5, nPhi: 32 };
1272   let ringQ = 1;
1273 
1274   const RING_MEASURED = [
1275     ["1", "88 mm"], ["2", "22.8 mm"], ["4", "5.76 mm"],
1276     ["8", "1.44 mm"], ["16", "0.36 mm"], ["32", "0.089 mm"],
1277   ];
1278 
1279   function drawRing() {
1280     const w = 460, h = 190, m = { l: 40, r: 12, t: 12, b: 26 };
1281     const pw = w - m.l - m.r, ph = h - m.t - m.b;
1282     const rx = p => m.l + (p + Math.PI) / (2 * Math.PI) * pw;
1283     const ry = r => m.t + ph - (r - RING.rLo) / (RING.rHi - RING.rLo) * ph;
1284     const pe = edges(-Math.PI, Math.PI, RING.nPhi);
1285     const re = edges(RING.rLo, RING.rHi, RING.nR);
1286 
1287     const phis = phiSegments(-Math.PI, Math.PI, ringQ);
1288     const verts = [];
1289     for (const p of phis) verts.push([RING.rOut * Math.cos(p), RING.rOut * Math.sin(p)]);
1290     for (let i = phis.length - 1; i >= 0; i--) verts.push([RING.rIn * Math.cos(phis[i]), RING.rIn * Math.sin(phis[i])]);
1291 
1292     const cols = new Map();
1293     const trace = [];
1294     for (let i = 0; i < verts.length; i++) {
1295       const a = verts[i], b = verts[(i + 1) % verts.length];
1296       for (let k = 0; k < 24; k++) {
1297         const t = k / 24;
1298         const x = a[0] + t * (b[0] - a[0]), y = a[1] + t * (b[1] - a[1]);
1299         const p = Math.atan2(y, x), r = Math.hypot(x, y);
1300         trace.push([p, r]);
1301         const ci = Math.min(RING.nPhi - 1, Math.max(0, Math.floor((p + Math.PI) / (2 * Math.PI) * RING.nPhi)));
1302         const cj = Math.floor((r - RING.rLo) / (RING.rHi - RING.rLo) * RING.nR);
1303         if (cj < 0 || cj >= RING.nR) continue;
1304         const span = cols.get(ci);
1305         if (!span) cols.set(ci, [cj, cj]);
1306         else { span[0] = Math.min(span[0], cj); span[1] = Math.max(span[1], cj); }
1307       }
1308     }
1309 
1310     let s = `<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="full ring unrolled in phi, showing how coarsely the outline is sampled">`;
1311     for (let i = 0; i < RING.nPhi; i++) for (let j = 0; j < RING.nR; j++) {
1312       const truth = re[j + 1] > RING.rIn && re[j] < RING.rOut;
1313       const span = cols.get(i);
1314       const reg = span && j >= span[0] && j <= span[1];
1315       if (!truth && !reg) continue;
1316       const x = rx(pe[i]), y = ry(re[j + 1]);
1317       const ww = rx(pe[i + 1]) - x, hh = ry(re[j]) - y;
1318       const fill = truth && reg ? "var(--served-fill)" : truth ? "var(--miss-fill)" : "var(--waste-fill)";
1319       const stroke = truth && reg ? "var(--served-line)" : truth ? "var(--miss)" : "var(--waste)";
1320       s += `<rect x="${esc(x)}" y="${esc(y)}" width="${esc(ww)}" height="${esc(hh)}" fill="${fill}" stroke="${stroke}" stroke-width=".7"/>`;
1321     }
1322     s += `<g stroke="var(--rule)" stroke-width=".5" fill="none">`;
1323     for (let i = 0; i <= RING.nPhi; i++) s += `<line x1="${esc(rx(pe[i]))}" y1="${m.t}" x2="${esc(rx(pe[i]))}" y2="${m.t + ph}"/>`;
1324     for (let j = 0; j <= RING.nR; j++) s += `<line x1="${m.l}" y1="${esc(ry(re[j]))}" x2="${m.l + pw}" y2="${esc(ry(re[j]))}"/>`;
1325     s += `</g>`;
1326 
1327     let d = "";
1328     let prev = null;
1329     for (const [p, r] of trace) {
1330       const yr = ry(Math.max(RING.rLo - 8, r));   // chords that leave the layer are cropped, not hidden
1331       if (prev !== null && Math.abs(p - prev) > Math.PI) d += `M${esc(rx(p))} ${esc(yr)}`;
1332       else d += `${d ? "L" : "M"}${esc(rx(p))} ${esc(yr)}`;
1333       prev = p;
1334     }
1335     s += `<path d="${d}" fill="none" stroke="var(--served)" stroke-width="1.5" stroke-linejoin="round"/>`;
1336 
1337     s += `<g fill="var(--muted)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="8">`;
1338     s += `<text x="${m.l}" y="${h - 8}">φ −π</text><text x="${m.l + pw}" y="${h - 8}" text-anchor="end">π</text>`;
1339     s += `<text x="4" y="${esc(ry(RING.rOut) + 3)}">300</text><text x="4" y="${esc(ry(RING.rIn) + 3)}">200 mm</text>`;
1340     s += `</g></svg>`;
1341     return s;
1342   }
1343 
1344   // ------------------------------------------------------------------ render
1345 
1346   const $ = id => document.getElementById(id);
1347 
1348   function render() {
1349     const m = model();
1350     $("svg-a").innerHTML = drawPanel(m, m.centreSet);
1351     $("svg-b").innerHTML = drawPanel(m, m.floodSet);
1352     $("svg-c").innerHTML = drawPanel(m, m.footSet);
1353     $("stats-a").innerHTML = statsHTML(m, m.centreSet);
1354     $("stats-b").innerHTML = statsHTML(m, m.floodSet);
1355     $("stats-c").innerHTML = statsHTML(m, m.footSet);
1356     $("verdict").innerHTML = verdictHTML(m);
1357 
1358     $("rc-out").textContent = S.rc + " mm";
1359     $("hh-out").textContent = S.hh + " mm";
1360     $("hw-out").textContent = S.hw + " mm";
1361     $("nr-out").textContent = S.nR;
1362     $("nphi-out").textContent = S.nPhi;
1363     for (const k of ["rc", "hh", "hw"]) $(k).value = S[k];
1364     $("nr").value = S.nR;
1365     $("nphi").value = S.nPhi;
1366   }
1367 
1368   function showRing() {
1369     $("svg-ring").innerHTML = drawRing();
1370     $("qseg-table").innerHTML = RING_MEASURED
1371       .map(([q, v]) => `<tr${String(ringQ) === q ? ' style="color:var(--served)"' : ""}><td>${q}</td><td>${v}</td></tr>`)
1372       .join("");
1373   }
1374 
1375   // ------------------------------------------------------------------ events
1376 
1377   for (const k of ["rc", "hh", "hw"]) $(k).addEventListener("input", e => { S[k] = +e.target.value; render(); });
1378   $("nr").addEventListener("input", e => { S.nR = +e.target.value; render(); });
1379   $("nphi").addEventListener("input", e => { S.nPhi = +e.target.value; render(); });
1380 
1381   $("preset").addEventListener("click", e => {
1382     const b = e.target.closest("[data-preset]");
1383     if (!b) return;
1384     Object.assign(S, PRESETS[b.dataset.preset]);
1385     for (const c of $("preset").children) c.setAttribute("aria-pressed", String(c === b));
1386     render();
1387   });
1388 
1389   $("shape").addEventListener("click", e => {
1390     const b = e.target.closest("[data-shape]");
1391     if (!b) return;
1392     S.shape = b.dataset.shape;
1393     for (const c of $("shape").children) c.setAttribute("aria-pressed", String(c === b));
1394     render();
1395   });
1396 
1397   $("qseg").addEventListener("click", e => {
1398     const b = e.target.closest("[data-q]");
1399     if (!b) return;
1400     ringQ = +b.dataset.q;
1401     for (const c of $("qseg").children) c.setAttribute("aria-pressed", String(c === b));
1402     showRing();
1403   });
1404 
1405   for (const id of ["svg-a", "svg-b", "svg-c"]) {
1406     const host = $(id);
1407     const move = ev => {
1408       const svg = host.querySelector("svg");
1409       if (!svg) return;
1410       const box = svg.getBoundingClientRect();
1411       const px = (ev.clientX - box.left) / box.width * W;
1412       const py = (ev.clientY - box.top) / box.height * H;
1413       const phi = LAYER.phiLo + (px - M.l) / PW * (LAYER.phiHi - LAYER.phiLo);
1414       const r = LAYER.rLo + (M.t + PH - py) / PH * (LAYER.rHi - LAYER.rLo);
1415       S.pc = Math.max(LAYER.phiLo, Math.min(LAYER.phiHi, phi));
1416       S.rc = Math.round(Math.max(45, Math.min(155, r)));
1417       render();
1418     };
1419     host.addEventListener("pointerdown", ev => {
1420       ev.preventDefault();
1421       const svg = host.querySelector("svg");
1422       if (svg) svg.setPointerCapture(ev.pointerId);
1423       move(ev);
1424       const up = () => { host.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
1425       host.addEventListener("pointermove", move);
1426       window.addEventListener("pointerup", up);
1427     });
1428   }
1429 
1430   render();
1431   showRing();
1432 })();
1433 (() => {
1434   "use strict";
1435 
1436   const S = { eta: 2.2, stag: 2.5, bf: 5, hz: 30 };
1437 
1438   const R = 100;          // representative cylinder radius, mm
1439   const MODULE_T = 0.15;  // module half-thickness in r, mm
1440   const ETA_MAX = 3.5;
1441   const CAP_Z = 2;        // NeighborWindow max on the z axis
1442   const VIEW_BINS = 13;   // bins either side of the crossing shown in r-z
1443   const STRIP_BINS = 9;   // bins either side of the crossing shown in a strip
1444 
1445   // ---------------------------------------------------------------- model
1446 
1447   // a barrel of identical modules on two radii, tiled along z with a slight
1448   // overlap, exactly the arrangement the stagger describes
1449   // long enough that a track still crosses it at the top of the eta range,
1450   // the way an ITk-scale barrel is
1451   function layer(s, hz) {
1452     const pitch = 2 * hz * 0.92;
1453     const nMod = 2 * Math.ceil(R * Math.sinh(ETA_MAX + 0.1) / pitch) + 1;
1454     const zHalf = nMod * pitch / 2;
1455     const mods = [];
1456     for (let i = 0; i < nMod; i++) {
1457       const zc = (i - (nMod - 1) / 2) * pitch;
1458       mods.push({ zc, z0: zc - hz, z1: zc + hz, r: R + (i % 2 === 0 ? -s : s) });
1459     }
1460     return { mods, zHalf, pitch, nMod };
1461   }
1462 
1463   function binning(L, bf) {
1464     const n = Math.max(4, Math.round(L.nMod * bf));
1465     return { n, lo: -L.zHalf, hi: L.zHalf, w: 2 * L.zHalf / n };
1466   }
1467 
1468   const binOf = (b, z) =>
1469     Math.min(b.n - 1, Math.max(0, Math.floor((z - b.lo) / b.w)));
1470 
1471   function state(eta, s, bf, hz) {
1472     const L = layer(s, hz);
1473     const b = binning(L, bf);
1474     const sh = Math.sinh(eta);
1475     const tolerance = s + MODULE_T;
1476 
1477     const zCross = R * sh;
1478     const inRange = Math.abs(zCross) < L.zHalf;
1479     const bCross = binOf(b, zCross);
1480 
1481     // bins each module is registered in: the projection onto the cylinder is
1482     // radial, so it keeps the module's z span
1483     for (const m of L.mods) {
1484       m.b0 = binOf(b, m.z0);
1485       m.b1 = binOf(b, m.z1);
1486       // the track's z where it reaches this module's radius
1487       m.zAt = m.r * sh;
1488       m.hit = m.zAt >= m.z0 && m.zAt <= m.z1 && Math.abs(m.zAt) < L.zHalf;
1489     }
1490     // the rows overlap in z, so a track can cross one module on each radius.
1491     // They slide in opposite directions and both have to stay reachable
1492     const hits = L.mods.filter(m => m.hit);
1493 
1494     // the chord through the layer, projected onto the layer: tolerance / |n.d|
1495     // times the tangential part, which in this slice is tolerance * sinh(eta)
1496     const slide = tolerance * sh;
1497     const dSlide = Math.max(
1498       Math.abs(binOf(b, zCross + slide) - bCross),
1499       Math.abs(binOf(b, zCross - slide) - bCross));
1500 
1501     const dists = [0, 1, Math.min(CAP_Z, dSlide)];
1502 
1503     const policies = dists.map(d => {
1504       const lo = Math.max(0, bCross - d), hi = Math.min(b.n - 1, bCross + d);
1505       const served = new Set();
1506       for (let i = lo; i <= hi; i++) served.add(i);
1507       const reaches = m => {
1508         for (let i = m.b0; i <= m.b1; i++) if (served.has(i)) return true;
1509         return false;
1510       };
1511       const cand = L.mods.filter(reaches);
1512       const reached = hits.filter(reaches);
1513       return {
1514         d, lo, hi, served,
1515         cand: cand.length,
1516         reached: reached.length,
1517         covered: hits.length > 0 && reached.length === hits.length,
1518         reaches,
1519       };
1520     });
1521 
1522     return { L, b, sh, tolerance, zCross, bCross, inRange, hits, slide, dSlide, policies };
1523   }
1524 
1525   // sweep eta at the current geometry
1526   function sweep(s, bf, hz) {
1527     const N = 400, out = [[], [], []];
1528     const miss = [0, 0, 0], cand = [0, 0, 0];
1529     let n = 0;
1530     for (let i = 0; i < N; i++) {
1531       const eta = 3.5 * i / (N - 1);
1532       const st = state(eta, s, bf, hz);
1533       if (!st.hits.length || !st.inRange) continue;
1534       n++;
1535       st.policies.forEach((p, k) => {
1536         if (!p.covered) miss[k]++;
1537         cand[k] += p.cand;
1538         out[k].push([eta, p.covered ? 0 : 1]);
1539         void 0;
1540       });
1541     }
1542     return {
1543       n,
1544       miss: miss.map(m => n ? m / n : 0),
1545       cand: cand.map(c => n ? c / n : 0),
1546       curves: out,
1547     };
1548   }
1549 
1550   // ---------------------------------------------------------------- drawing
1551 
1552   const f = (v) => Number(v).toFixed(2);
1553   const esc = (t) => String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;");
1554 
1555   const POLICY_COLOR = ["var(--muted)", "var(--waste)", "var(--served)"];
1556 
1557   function drawRZ(st) {
1558     const W = 1040, H = 300, M = { l: 46, r: 16, t: 22, b: 42 };
1559     const pw = W - M.l - M.r, ph = H - M.t - M.b;
1560     const { L, b } = st;
1561     const rLo = R - S.stag - 14, rHi = R + S.stag + 14;
1562     const sy = r => M.t + ph - (r - rLo) / (rHi - rLo) * ph;
1563     // the layer runs metres in z, so the view follows the crossing. It has to
1564     // hold the track's own traverse across the drawn r range as well as the
1565     // window, or a shallow track leaves the frame immediately
1566     const half = Math.max(VIEW_BINS * b.w, 0.6 * (rHi - rLo) * st.sh,
1567                           2.5 * st.slide, 3 * S.hz);
1568     const zLo = st.zCross - half, zHi = st.zCross + half;
1569     const sx = z => M.l + (z - zLo) / (zHi - zLo) * pw;
1570 
1571     const p = st.policies[2];
1572     let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Layer in the r-z plane with the track, the chord through the layer and the bins the window serves">`;
1573 
1574     // served window
1575     const wx0 = sx(b.lo + p.lo * b.w), wx1 = sx(b.lo + (p.hi + 1) * b.w);
1576     s += `<rect x="${f(wx0)}" y="${M.t}" width="${f(wx1 - wx0)}" height="${ph}" fill="var(--served-fill)"/>`;
1577 
1578     // bin rules, thinned when the view holds more than it can show
1579     const iLo = Math.max(0, Math.floor((zLo - b.lo) / b.w));
1580     const iHi = Math.min(b.n, Math.ceil((zHi - b.lo) / b.w));
1581     const step = Math.max(1, Math.ceil((iHi - iLo) / 70));
1582     s += `<g stroke="var(--rule)" stroke-width=".5" fill="none">`;
1583     for (let i = iLo; i <= iHi; i += step) {
1584       const x = sx(b.lo + i * b.w);
1585       s += `<line x1="${f(x)}" y1="${M.t}" x2="${f(x)}" y2="${M.t + ph}"/>`;
1586     }
1587     s += `</g>`;
1588 
1589     // tolerance band
1590     s += `<rect x="${M.l}" y="${f(sy(R + st.tolerance))}" width="${pw}" height="${f(sy(R - st.tolerance) - sy(R + st.tolerance))}" fill="var(--served)" fill-opacity=".07"/>`;
1591 
1592     // modules
1593     for (const m of L.mods) {
1594       if (m.z1 < zLo || m.z0 > zHi) continue;
1595       const x = sx(m.z0), w = sx(m.z1) - sx(m.z0);
1596       let fill = "var(--muted)", op = ".55";
1597       if (m.hit) { fill = p.reaches(m) ? "var(--served)" : "var(--miss)"; op = "1"; }
1598       s += `<rect x="${f(x)}" y="${f(sy(m.r) - 3)}" width="${f(w)}" height="6" rx="1" fill="${fill}" fill-opacity="${op}"/>`;
1599     }
1600 
1601     // representative cylinder
1602     s += `<line x1="${M.l}" y1="${f(sy(R))}" x2="${M.l + pw}" y2="${f(sy(R))}" stroke="var(--served)" stroke-width="1.4"/>`;
1603 
1604     // track
1605     s += `<line x1="${f(sx(rLo * st.sh))}" y1="${f(sy(rLo))}" x2="${f(sx(rHi * st.sh))}" y2="${f(sy(rHi))}" stroke="var(--ink)" stroke-width="1.5"/>`;
1606 
1607     // chord ends on the representative surface
1608     const xc = sx(st.zCross);
1609     s += `<line x1="${f(sx(st.zCross - st.slide))}" y1="${f(sy(R))}" x2="${f(sx(st.zCross + st.slide))}" y2="${f(sy(R))}" stroke="var(--served)" stroke-width="3.4" stroke-opacity=".55" stroke-linecap="round"/>`;
1610     s += `<circle cx="${f(xc)}" cy="${f(sy(R))}" r="4" fill="var(--served)"/>`;
1611 
1612     for (const h of st.hits) {
1613       const hx = sx(h.zAt), hy = sy(h.r);
1614       s += `<path d="M${f(hx)} ${f(hy - 5)}L${f(hx + 5)} ${f(hy)}L${f(hx)} ${f(hy + 5)}L${f(hx - 5)} ${f(hy)}Z" fill="${p.reaches(h) ? "var(--served)" : "var(--miss)"}"/>`;
1615     }
1616 
1617     // labels
1618     s += `<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="10.5" fill="var(--muted)">`;
1619     s += `<text x="${M.l}" y="${H - 24}">z = ${zLo.toFixed(0)} mm</text>`;
1620     s += `<text x="${M.l + pw}" y="${H - 24}" text-anchor="end">${zHi.toFixed(0)} mm</text>`;
1621     s += `<text x="8" y="${f(sy(R) + 3)}">r=${R}</text>`;
1622     s += `<text x="${M.l + pw}" y="${M.t - 8}" text-anchor="end" fill="var(--served)">window: ${p.d} bin${p.d === 1 ? "" : "s"} each side</text>`;
1623     s += `<text x="${f(xc)}" y="${M.t - 8}" text-anchor="middle" fill="var(--served)">crossing</text>`;
1624     s += `<text x="${M.l}" y="${H - 8}">slide = tolerance &#215; sinh &#951; = ${f(st.slide)} mm = ${f(st.slide / b.w)} bins</text>`;
1625     s += `</g></svg>`;
1626     return s;
1627   }
1628 
1629   function drawStrip(st, k) {
1630     const W = 320, H = 54, M = { l: 4, r: 4, t: 6, b: 18 };
1631     const pw = W - M.l - M.r;
1632     const { b } = st;
1633     const p = st.policies[k];
1634     const vLo = Math.max(0, st.bCross - STRIP_BINS);
1635     const vHi = Math.min(b.n - 1, st.bCross + STRIP_BINS);
1636     const nv = vHi - vLo + 1;
1637     const sx = i => M.l + (i - vLo) / nv * pw;
1638     let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="bins served along z around the crossing">`;
1639 
1640     for (let i = vLo; i <= vHi; i++) {
1641       const inServed = p.served.has(i);
1642       const inHit = st.hits.some(h => i >= h.b0 && i <= h.b1);
1643       let fill = "none", stroke = "var(--rule)";
1644       if (inServed && inHit) { fill = "var(--served-fill)"; stroke = "var(--served-line)"; }
1645       else if (inServed) { fill = "var(--waste-fill)"; stroke = "var(--waste)"; }
1646       else if (inHit) { fill = "var(--miss-fill)"; stroke = "var(--miss)"; }
1647       s += `<rect x="${f(sx(i))}" y="${M.t}" width="${f(pw / nv)}" height="${H - M.t - M.b}" fill="${fill}" stroke="${stroke}" stroke-width=".6"/>`;
1648     }
1649     const cx = sx(st.bCross) + pw / nv / 2;
1650     s += `<circle cx="${f(cx)}" cy="${f(H - M.b + 5)}" r="3" fill="var(--served)"/>`;
1651     s += `</svg>`;
1652     return s;
1653   }
1654 
1655   function statsHTML(st, k) {
1656     const p = st.policies[k];
1657     const n = st.hits.length;
1658     return `
1659       <dt>bins served</dt><dd>${p.served.size}</dd>
1660       <dt>candidates</dt><dd class="${p.cand > 4 ? "warn" : ""}">${p.cand}</dd>
1661       <dt>modules crossed</dt><dd>${n}</dd>
1662       <dt>of those reachable</dt><dd class="${p.covered ? "ok" : "bad"}">${p.reached} of ${n}</dd>
1663       <dt>candidates wasted</dt><dd>${Math.max(0, p.cand - p.reached)}</dd>`;
1664   }
1665 
1666   function drawSweep(sw) {
1667     const W = 560, H = 250, M = { l: 44, r: 14, t: 16, b: 34 };
1668     const pw = W - M.l - M.r, ph = H - M.t - M.b;
1669     const sx = e => M.l + e / 3.5 * pw;
1670     const maxC = Math.max(4, ...sw.cand) * 1.15;
1671     const sy = c => M.t + ph - c / maxC * ph;
1672 
1673     let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="mean candidates per query against pseudorapidity for each policy">`;
1674     s += `<g stroke="var(--rule-soft)" stroke-width=".7">`;
1675     for (let i = 0; i <= 4; i++) {
1676       const y = M.t + ph * i / 4;
1677       s += `<line x1="${M.l}" y1="${f(y)}" x2="${M.l + pw}" y2="${f(y)}"/>`;
1678     }
1679     s += `</g>`;
1680 
1681     // candidate curves, recomputed per eta
1682     const N = 90;
1683     for (let k = 0; k < 3; k++) {
1684       let d = "";
1685       for (let i = 0; i < N; i++) {
1686         const eta = 3.5 * i / (N - 1);
1687         const st = state(eta, S.stag, S.bf, S.hz);
1688         if (!st.inRange) continue;
1689         const p = st.policies[k];
1690         d += `${d ? "L" : "M"}${f(sx(eta))} ${f(sy(p.cand))}`;
1691         if (!p.covered && st.hits.length) {
1692           s += `<circle cx="${f(sx(eta))}" cy="${f(sy(p.cand))}" r="2.2" fill="var(--miss)"/>`;
1693         }
1694       }
1695       s += `<path d="${d}" fill="none" stroke="${POLICY_COLOR[k]}" stroke-width="${k === 2 ? 2 : 1.4}" stroke-linejoin="round"/>`;
1696     }
1697 
1698     s += `<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="10" fill="var(--muted)">`;
1699     for (let i = 0; i <= 4; i++) {
1700       const y = M.t + ph * i / 4;
1701       s += `<text x="${M.l - 7}" y="${f(y + 3)}" text-anchor="end">${f(maxC * (1 - i / 4))}</text>`;
1702     }
1703     for (let e = 0; e <= 3.5; e += 0.5) {
1704       s += `<text x="${f(sx(e))}" y="${H - 14}" text-anchor="middle">${e.toFixed(1)}</text>`;
1705     }
1706     s += `<text x="${M.l}" y="${H - 2}">pseudorapidity &#951;</text>`;
1707     s += `<text x="${M.l + pw}" y="${H - 2}" text-anchor="end">mean candidates per query &#183; red dot = miss</text>`;
1708     s += `</g></svg>`;
1709     return s;
1710   }
1711 
1712   // ---------------------------------------------------------------- render
1713 
1714   const $ = id => document.getElementById(id);
1715 
1716   function render() {
1717     const st = state(S.eta, S.stag, S.bf, S.hz);
1718     $("svg-rz").innerHTML = drawRZ(st);
1719     for (let k = 0; k < 3; k++) {
1720       $("strip-" + k).innerHTML = drawStrip(st, k);
1721       $("stats-" + k).innerHTML = statsHTML(st, k);
1722     }
1723 
1724     const sw = sweep(S.stag, S.bf, S.hz);
1725     $("svg-sweep").innerHTML = drawSweep(sw);
1726     const names = ["crossing bin only", "fixed &plusmn;1", "sized by the slide"];
1727     $("sweep-table").innerHTML = names.map((nm, k) => {
1728       const pct = 100 * sw.miss[k];
1729       const cls = pct > 0.001 ? "bad" : "ok";
1730       return `<tr><td style="color:${POLICY_COLOR[k]}">${nm}</td>` +
1731              `<td class="${cls}">${pct < 0.001 ? "0%" : pct.toFixed(1) + "%"}</td>` +
1732              `<td>${sw.cand[k].toFixed(1)}</td></tr>`;
1733     }).join("");
1734 
1735     $("eta-out").textContent = S.eta.toFixed(2);
1736     $("stag-out").textContent = S.stag.toFixed(1) + " mm";
1737     $("bf-out").textContent = S.bf;
1738     $("hz-out").textContent = S.hz + " mm";
1739   }
1740 
1741   // Open where the two windows part company. A module is registered over
1742   // 2*hz of z and a bin is 1.84*hz/bf wide, so the crossing falls outside the
1743   // module's bins once stagger*sinh(eta) passes one bin; two bins is where a
1744   // fixed +-1 can no longer reach it.
1745   (function pickEta() {
1746     const guess = Math.asinh(2 * 1.84 * S.hz / (S.stag * S.bf));
1747     const clamp = e => Math.min(3.5, Math.max(0, e));
1748     S.eta = Math.round(clamp(guess) * 100) / 100;
1749     for (let d = 0; d <= 60; d++) {
1750       for (const sign of (d ? [-1, 1] : [0])) {
1751         const eta = clamp(guess + sign * d / 100);
1752         const st = state(eta, S.stag, S.bf, S.hz);
1753         if (st.hits.length && st.inRange &&
1754             !st.policies[1].covered && st.policies[2].covered) {
1755           S.eta = Math.round(eta * 100) / 100;
1756           return;
1757         }
1758       }
1759     }
1760   })();
1761 
1762   for (const [id, key, num] of [["eta", "eta", parseFloat], ["stag", "stag", parseFloat],
1763                                 ["bf", "bf", parseInt], ["hz", "hz", parseInt]]) {
1764     const el = $(id);
1765     el.value = S[key];
1766     el.addEventListener("input", e => { S[key] = num(e.target.value); render(); });
1767   }
1768 
1769   render();
1770 
1771   // ---- the two static figures, driven --------------------------------------
1772   // both were drawn on the same grid: 50px bins from x=60, crossing in bin 4,
1773   // the hit module registered in bins 5 to 7
1774   const FIG = { bin: 50, x0: 60, cross: 4, mod: [5, 7] };
1775 
1776   function setWindow(d) {
1777     const lo = FIG.cross - d, hi = FIG.cross + d;
1778     const x = FIG.x0 + FIG.bin * lo, w = FIG.bin * (2 * d + 1);
1779     const reaches = hi >= FIG.mod[0] && lo <= FIG.mod[1];
1780     const tone = reaches ? "var(--served)" : "var(--miss)";
1781 
1782     const q = r => document.querySelector(`[data-w="${r}"]`);
1783     for (const r of ["gfill", "gline", "rfill"]) {
1784       const el = q(r);
1785       if (el) { el.setAttribute("x", x); el.setAttribute("width", w); }
1786     }
1787     for (const r of ["gline", "gfill", "rfill"]) {
1788       const el = q(r);
1789       if (!el) continue;
1790       if (el.getAttribute("stroke")) el.setAttribute("stroke", tone);
1791       else el.setAttribute("fill", tone);
1792     }
1793     for (const r of ["gmod", "rmod", "rband"]) {
1794       const el = q(r);
1795       if (!el) continue;
1796       el.setAttribute(el.getAttribute("stroke") ? "stroke" : "fill", tone);
1797     }
1798     const gl = q("glabel"); if (gl) gl.textContent = `window: ${d} in z, 0 in phi`;
1799     const rl = q("rlabel"); if (rl) rl.textContent = `window: ${d} bin${d === 1 ? "" : "s"} each side`;
1800     const rc = q("rcaption");
1801     if (rc) rc.textContent = `bins ${lo} to ${hi} are served; the module the track hits is registered in bins 5 to 7`;
1802     const gv = q("gverdict");
1803     if (gv) gv.textContent = reaches
1804       ? "the window in z reaches the module the crossing bin alone would miss"
1805       : "nothing served holds the module the track hits";
1806 
1807     const out = document.getElementById("wverdict");
1808     out.textContent = reaches ? "hit reachable" : "hit lost";
1809     out.className = "verdict-inline " + (reaches ? "ok" : "bad");
1810   }
1811 
1812   document.getElementById("wstep").addEventListener("click", e => {
1813     const b = e.target.closest("[data-d]");
1814     if (!b) return;
1815     for (const c of b.parentElement.children) {
1816       c.setAttribute("aria-pressed", String(c === b));
1817     }
1818     setWindow(+b.dataset.d);
1819   });
1820 
1821   setWindow(2);
1822 
1823   // ---- the footprint figure: which test decides a bin is registered --------
1824   // the drawn bins are the area-overlap answer; the accent centre dots mark
1825   // the bins a centre test would have kept
1826   (function footprint() {
1827     const bins = document.querySelector('[data-w="fbins"]');
1828     const centres = document.querySelector('[data-w="fcentres"]');
1829     if (!bins || !centres) return;
1830 
1831     const inside = [...centres.querySelectorAll("circle")]
1832       .filter(c => c.getAttribute("r") === "1.8")
1833       .map(c => [+c.getAttribute("cx"), +c.getAttribute("cy")]);
1834 
1835     const rects = [...bins.querySelectorAll("rect")].map(r => {
1836       const cx = +r.getAttribute("x") + +r.getAttribute("width") / 2;
1837       const cy = +r.getAttribute("y") + +r.getAttribute("height") / 2;
1838       return {
1839         el: r,
1840         byCentre: inside.some(([X, Y]) => Math.abs(X - cx) < 1.5 && Math.abs(Y - cy) < 1.5),
1841       };
1842     });
1843 
1844     const out = document.getElementById("fverdict");
1845 
1846     function setMode(mode) {
1847       let kept = 0;
1848       for (const r of rects) {
1849         const keep = mode === "area" || r.byCentre;
1850         if (keep) kept++;
1851         r.el.setAttribute("fill", keep ? "var(--served)" : "var(--miss)");
1852         r.el.setAttribute("stroke", keep ? "var(--served)" : "var(--miss)");
1853         r.el.setAttribute("fill-opacity", keep ? ".17" : ".13");
1854         r.el.setAttribute("stroke-opacity", keep ? ".45" : ".8");
1855         r.el.setAttribute("stroke-dasharray", keep ? "none" : "2 2");
1856       }
1857       const lost = rects.length - kept;
1858       out.textContent = mode === "area"
1859         ? `${kept} bins registered`
1860         : `${kept} of ${rects.length} bins, ${lost} lost`;
1861       out.className = "verdict-inline " + (lost ? "bad" : "ok");
1862     }
1863 
1864     document.getElementById("fstep").addEventListener("click", e => {
1865       const b = e.target.closest("[data-m]");
1866       if (!b) return;
1867       for (const c of b.parentElement.children) {
1868         c.setAttribute("aria-pressed", String(c === b));
1869       }
1870       setMode(b.dataset.m);
1871     });
1872 
1873     setMode("area");
1874   })();
1875 })();
1876 </script>
1877 
1878 </body>
1879 </html>