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">← Surface array reference</a></p>
0271 <p class="eyebrow">ACTS · 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 — 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 → grid</strong>: project the module's reference position, register that one bin. <strong>Grid → 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 — 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 ±pi ruled at both ends. The edges look curved because they are <em>straight in space</em> — a straight chord at perpendicular distance d from the beam line is <span class="mono">r = d / cos(phi − phi₀)</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 · Surface → 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 · + grid → 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 · 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>· bin centre ✛ 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 ±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 ±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 — and it charges 3×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 × 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 — rectangle, trapezoid — 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 · full ring, r 200–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 200–300 mm. A radial sector of ±0.15 rad loses 0.84 mm at <span class="mono">q = 1</span> through <span class="mono">q = 8</span> alike — 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| = 3.5 go from <strong>4.9% to zero</strong>. With the ±1 expansion still in place that costs 9.0 → 25.0 candidates per query at eta = 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 = 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 sinh eta</span>, so the slide from the representative surface to a module offset by <span class="mono">Δr</span> is <span class="mono">Δr sinh eta</span>, and the chord the window is sized from is <span class="mono">tolerance × sinh 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">±1 bin</button>
0652 <button class="chip" data-d="2" aria-pressed="true">±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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 — 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 ±1 bin</h3>
0885 <p class="sub">what main serves — #5186's 1/|n·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 ±tolerance sinh 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>● crossing ◆ 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 — 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 — 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 — the reference frame with the curvilinear metric already folded in — 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 ±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 × 20 bins, 3.28M lookups, RelWithDebInfo with assertions forced on. The narrower window returns 4.5× fewer candidates, and each one it drops is an intersection the caller does not run — 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 — 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">← 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, "&").replace(/</g, "<");
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 × sinh η = ${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 η</text>`;
1707 s += `<text x="${M.l + pw}" y="${H - 2}" text-anchor="end">mean candidates per query · 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 ±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>