this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

WIP

+371 -202
+371 -202
tline_viz.html
··· 48 48 .cfg { display: grid; grid-template-columns: repeat(12, 1fr); gap: 10px; margin-top: 10px; } 49 49 .cfg > label { grid-column: span 3; } 50 50 .cfg > label.wide { grid-column: span 6; } 51 + .top-layout { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; gap: 12px; margin-top: 10px; } 52 + .cfg-vert { display: grid; gap: 8px; align-content: start; } 53 + .cfg-row { 54 + display: grid; 55 + grid-template-columns: auto 1fr auto; 56 + gap: 8px; 57 + align-items: center; 58 + font-size: 13px; 59 + color: var(--muted); 60 + background: #101a28; 61 + border: 1px solid #24364e; 62 + border-radius: 10px; 63 + padding: 8px 10px; 64 + } 65 + .cfg-extra { 66 + display: grid; 67 + gap: 8px; 68 + background: #101a28; 69 + border: 1px solid #24364e; 70 + border-radius: 10px; 71 + padding: 8px 10px; 72 + color: var(--muted); 73 + font-size: 12px; 74 + } 75 + .eq-panel { 76 + background: #101a28; 77 + border: 1px solid #24364e; 78 + border-radius: 10px; 79 + padding: 10px; 80 + } 81 + .eq-title { font-size: 12px; color: var(--ink); font-weight: 650; margin: 2px 0 6px; } 82 + .eq-lines { color: var(--muted); font-size: 13px; line-height: 1.5; } 83 + .calc-values { color: var(--muted); font-size: 13px; line-height: 1.45; margin-top: 6px; } 84 + .wave-values { 85 + margin-top: 6px; 86 + max-height: 170px; 87 + overflow: auto; 88 + border-top: 1px solid #24364e; 89 + padding-top: 6px; 90 + color: var(--muted); 91 + font-size: 12px; 92 + line-height: 1.35; 93 + } 51 94 .note { color: var(--muted); font-size: 12px; line-height: 1.35; margin-top: 10px; } 52 95 canvas { width: 100%; height: auto; display: block; border-radius: 12px; } 53 96 .legend { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; } ··· 59 102 .small { font-size: 12px; color: var(--muted); } 60 103 .toggle { display:flex; align-items:center; gap:8px; margin-left: 4px; } 61 104 input[type="checkbox"]{ width: 16px; height: 16px; accent-color: var(--accent); } 62 - .footer { margin-top: 10px; color: var(--muted); font-size: 12px; } 63 105 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-variant-numeric: tabular-nums; } 64 106 </style> 65 107 </head> ··· 82 124 <div class="readouts"> 83 125 <div class="pill"><span>t / τ<sub>d</sub>:</span> <b class="mono" id="tRead">0.000</b></div> 84 126 <div class="pill"><span>Γ<sub>L</sub>:</span> <b class="mono" id="gLRead">0.000</b></div> 85 - <div class="pill"><span>V₁ (incident):</span> <b class="mono" id="v1Read">0.000 V</b></div> 86 - <div class="pill"><span>V₂ (reflected):</span> <b class="mono" id="v2Read">0.000 V</b></div> 87 127 <div class="pill"><span>V<sub>S</sub>:</span> <b class="mono" id="vsRead">0.000 V</b></div> 88 128 <div class="pill"><span>V<sub>L</sub>:</span> <b class="mono" id="vlRead">0.000 V</b></div> 89 129 </div> 90 130 </div> 91 131 92 - <div class="cfg"> 93 - <label> 94 - V<sub>g</sub> (V) 95 - <input id="Vg" type="number" step="0.1" value="5.0" /> 96 - </label> 97 - <label> 98 - R<sub>g</sub> (Ω) 99 - <input id="Rg" type="number" step="0.1" value="50.0" /> 100 - </label> 101 - <label> 102 - Z<sub>0</sub> (Ω) 103 - <input id="Z0" type="number" step="0.1" value="50.0" /> 104 - </label> 105 - <label> 106 - R<sub>L</sub> (Ω) 107 - <input id="RL" type="number" step="0.1" value="30.0" /> 108 - </label> 109 - 110 - <label class="wide"> 111 - Time scale (speed) — seconds per τ<sub>d</sub> 112 - <input id="secPerTau" type="range" min="0.5" max="6" step="0.1" value="2.5" /> 113 - </label> 114 - <label class="wide"> 115 - Show only one reflection (matched source, Γ<sub>S</sub>=0) 116 - <input id="singleBounce" type="checkbox" checked /> 117 - </label> 118 - </div> 119 - 120 - <div class="note"> 121 - Model: at <span class="mono">t=0</span> the switch closes, the source initially “sees” <span class="mono">Z0</span>, launching 122 - <span class="mono">V1 = Vg·Z0/(Rg+Z0)</span>. When the step reaches the load at <span class="mono">t=τd</span>, it reflects with 123 - <span class="mono">ΓL = (RL−Z0)/(RL+Z0)</span> giving <span class="mono">V2 = ΓL·V1</span>. The plot below shows the spatial waveform vs <span class="mono">z</span>, 124 - and the circuit highlight shows the moving wavefront(s). :contentReference[oaicite:1]{index=1} 132 + <div class="top-layout"> 133 + <div class="cfg-vert"> 134 + <div class="cfg-row"><span>V<sub>g</sub> =</span><input id="Vg" type="number" step="0.1" value="5.0" /><span>V</span></div> 135 + <div class="cfg-row"><span>R<sub>g</sub> =</span><input id="Rg" type="number" step="0.1" value="50.0" /><span>(Ω)</span></div> 136 + <div class="cfg-row"><span>Z<sub>0</sub> =</span><input id="Z0" type="number" step="0.1" value="50.0" /><span>(Ω)</span></div> 137 + <div class="cfg-row"><span>R<sub>L</sub> =</span><input id="RL" type="number" step="0.1" value="30.0" /><span>(Ω)</span></div> 138 + <div class="cfg-extra"> 139 + <label>Time scale — seconds per τ<sub>d</sub> 140 + <input id="secPerTau" type="range" min="0.5" max="6" step="0.1" value="2.5" /> 141 + </label> 142 + <label>Reflection termination tolerance (% of |V<sub>1</sub>|) 143 + <input id="reflectTol" type="number" min="0" step="0.1" value="1" /> 144 + </label> 145 + <label>Show only one reflection (set \(\Gamma_S=0\)) 146 + <input id="singleBounce" type="checkbox" checked /> 147 + </label> 148 + </div> 149 + </div> 150 + <div class="eq-panel"> 151 + <div class="eq-lines"> 152 + <div>\[\Gamma_L = \dfrac{R_L - Z_0}{R_L + Z_0}, \quad \Gamma_S = \dfrac{R_g - Z_0}{R_g + Z_0}\]</div> 153 + <div>\[V_1 = V_g \dfrac{Z_0}{R_g + Z_0}\]</div> 154 + <div>\[V_{2k} = \Gamma_L V_{2k-1},\quad k \ge 1\]</div> 155 + <div>\[V_{2k+1} = \Gamma_S V_{2k},\quad k \ge 1\]</div> 156 + </div> 157 + <div class="eq-title">Computed Values</div> 158 + <div id="derivedValues" class="calc-values mono"></div> 159 + <div id="waveValues" class="wave-values mono"></div> 160 + </div> 125 161 </div> 126 162 127 163 <div class="divider"></div> ··· 140 176 <div class="key"><span class="swatch sum"></span> total (incident + reflected)</div> 141 177 <div class="small">Axes: horizontal is position <span class="mono">z</span> (0 → ℓ). Vertical is voltage.</div> 142 178 </div> 143 - 144 - <div class="footer"> 145 - Implementation note: code is structured so you can later replace the time-evolution with direct cursor control of the wavefront, 146 - or add multiple bounces (Γ<sub>S</sub> ≠ 0) via a bounce-diagram style event list. 147 - </div> 148 179 </div> 149 180 </div> 150 181 </div> 151 182 152 183 <script> 184 + window.MathJax = { 185 + tex: { inlineMath: [['\\(', '\\)'], ['$', '$']] }, 186 + svg: { fontCache: 'global' } 187 + }; 188 + </script> 189 + <script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script> 190 + <script> 153 191 (() => { 154 192 // ----------------------------- 155 193 // Utilities ··· 190 228 Z0: document.getElementById("Z0"), 191 229 RL: document.getElementById("RL"), 192 230 secPerTau: document.getElementById("secPerTau"), 231 + reflectTol: document.getElementById("reflectTol"), 193 232 tRead: document.getElementById("tRead"), 194 233 gLRead: document.getElementById("gLRead"), 195 - v1Read: document.getElementById("v1Read"), 196 - v2Read: document.getElementById("v2Read"), 197 234 vsRead: document.getElementById("vsRead"), 198 235 vlRead: document.getElementById("vlRead"), 236 + derivedValues: document.getElementById("derivedValues"), 237 + waveValues: document.getElementById("waveValues"), 199 238 }; 200 239 201 240 const theme = { ··· 230 269 Z0: 50, 231 270 RL: 30, 232 271 secPerTau: 2.5, 272 + reflectTol: 1, 233 273 singleBounce: true, // if true, assume matched source => no re-reflection 234 274 }; 235 275 276 + const BOUNCE_EPS = 1e-6; 277 + const MAX_BOUNCES = 5000; // safety guard if a user picks non-convergent parameters/tolerance. 278 + let timeHorizon = 2.2; 279 + let mathjaxTypesetDone = false; 280 + const PLOT_PANEL_H = 150; 281 + const PLOT_PANEL_GAP = 10; 282 + const PLOT_PAD_T = 14; 283 + const PLOT_PAD_B = 26; 284 + 236 285 function recomputeModelFromInputs() { 237 286 model.Vg = parseFloat(el.Vg.value); 238 287 model.Rg = parseFloat(el.Rg.value); 239 288 model.Z0 = parseFloat(el.Z0.value); 240 289 model.RL = parseFloat(el.RL.value); 241 290 model.secPerTau = parseFloat(el.secPerTau.value); 291 + model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 242 292 model.singleBounce = !!el.singleBounce.checked; 243 293 } 244 294 ··· 252 302 return { V1, V2, gL, gS }; 253 303 } 254 304 255 - // For this first draft, show VS/VL consistent with a *single* load reflection and matched source (ΓS=0), 256 - // which matches the “clean” narrative used in the examples immediately after Fig. 6.3. 257 - function computeEndVoltages(tn, V1, V2) { 305 + function buildBounceSeries(waves) { 306 + const gS = model.singleBounce ? 0 : waves.gS; 307 + const tolPct = Number.isFinite(model.reflectTol) ? model.reflectTol : 1; 308 + const ampTol = Math.max(BOUNCE_EPS, Math.abs(waves.V1) * (tolPct / 100)); 309 + const series = [{ n: 1, A: waves.V1, dir: +1, launch: 0 }]; 310 + 311 + let A = waves.V1; 312 + let launch = 1; 313 + let reflectAtLoad = true; // V2 is generated at the load from V1. 314 + while (series.length < MAX_BOUNCES) { 315 + const g = reflectAtLoad ? waves.gL : gS; 316 + const nextA = A * g; 317 + if (Math.abs(nextA) <= ampTol) break; 318 + series.push({ 319 + n: series.length + 1, 320 + A: nextA, 321 + dir: reflectAtLoad ? -1 : +1, 322 + launch 323 + }); 324 + A = nextA; 325 + launch += 1; 326 + reflectAtLoad = !reflectAtLoad; 327 + } 328 + 329 + const srcEvents = [{ t: 0, dV: waves.V1 }]; 330 + const loadEvents = []; 331 + for (const w of series) { 332 + const arrive = w.launch + 1; 333 + if (w.dir > 0) { 334 + // Right-going wave arrives at load and creates a reflected left-going wave. 335 + loadEvents.push({ t: arrive, dV: (1 + waves.gL) * w.A }); 336 + } else { 337 + // Left-going wave arrives at source and may re-reflect right-going wave. 338 + srcEvents.push({ t: arrive, dV: (1 + gS) * w.A }); 339 + } 340 + } 341 + 342 + return { 343 + series, 344 + srcEvents, 345 + loadEvents, 346 + gSEffective: gS, 347 + tolPct, 348 + ampTol, 349 + tEnd: (series.length > 0 ? series[series.length - 1].launch + 1 : 2) 350 + }; 351 + } 352 + 353 + function sumEventsAtTime(events, tn) { 258 354 const eps = 1e-9; 259 - // Load: 260 - let VL = (tn < 1 - eps) ? 0 : (V1 + V2); 261 - // Source: 262 - let VS; 263 - if (model.singleBounce) { 264 - VS = (tn < 2 - eps) ? V1 : (V1 + V2); // after reflected wave returns (t=2τd), source sees updated level 265 - } else { 266 - // If not single-bounce, we still *display* something reasonable: 267 - // keep VS at V1 for now to avoid implying correctness for multi-bounce without implementing it. 268 - VS = V1; 355 + let v = 0; 356 + for (const e of events) { 357 + if (tn >= e.t - eps) v += e.dV; 358 + } 359 + return v; 360 + } 361 + 362 + function computeDynamicState(tn, bounce) { 363 + const launchedWaves = []; 364 + const activeWaves = []; 365 + for (const w of bounce.series) { 366 + const u = tn - w.launch; 367 + if (u < 0) continue; // not launched yet 368 + const front = (w.dir > 0) ? clamp(u, 0, 1) : clamp(1 - u, 0, 1); 369 + const ww = { ...w, u, front }; 370 + launchedWaves.push(ww); 371 + if (u < 1) activeWaves.push(ww); // front still propagating along the line 372 + } 373 + 374 + return { 375 + launchedWaves, 376 + activeWaves, 377 + VS: sumEventsAtTime(bounce.srcEvents, tn), 378 + VL: sumEventsAtTime(bounce.loadEvents, tn) 379 + }; 380 + } 381 + 382 + function updateDerivedDisplays(waves, bounce) { 383 + const gSEffective = bounce.gSEffective; 384 + const V2calc = waves.gL * waves.V1; 385 + const V3calc = gSEffective * V2calc; 386 + const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 387 + const v3Reason = model.singleBounce 388 + ? "suppressed because single-bounce mode forces Γ_S = 0" 389 + : (v3Suppressed ? `suppressed since |V_3| ≤ ε (ε=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V_1|)` : "nonzero, so re-reflection should appear"); 390 + 391 + el.derivedValues.innerHTML = [ 392 + `Γ_L = ${fmt(waves.gL, 6)}`, 393 + `Γ_S = ${fmt(gSEffective, 6)}${model.singleBounce ? " (forced by single-bounce mode)" : ""}`, 394 + `V_1 = ${fmt(waves.V1, 6)} V`, 395 + `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V_1| = ${fmt(bounce.ampTol, 6)} V`, 396 + `V_2 = Γ_L·V_1 = (${fmt(waves.gL, 6)})·(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, 397 + `V_3 = Γ_S·V_2 = (${fmt(gSEffective, 6)})·(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, 398 + `V_3 status: ${v3Reason}`, 399 + `Total generated waves: ${bounce.series.length}` 400 + ].map((x) => `<div>${x}</div>`).join(""); 401 + 402 + const waveLines = bounce.series.map((w) => { 403 + const kind = (w.dir > 0) ? "incident (→ load)" : "reflected (→ source)"; 404 + return `<div>V<sub>${w.n}</sub> [${kind}] = ${fmt(w.A, 6)} V</div>`; 405 + }); 406 + if (bounce.series.length < 3) { 407 + waveLines.push(`<div>V<sub>3</sub> not launched: ${v3Reason}</div>`); 269 408 } 270 - return { VS, VL }; 409 + el.waveValues.innerHTML = waveLines.join(""); 271 410 } 272 411 273 412 // ----------------------------- 274 413 // Rendering: Circuit 275 414 // ----------------------------- 276 - function drawCircuit(ctx, w, h, tn, waves) { 415 + function drawCircuit(ctx, w, h, tn, dyn) { 277 416 ctx.clearRect(0, 0, w, h); 278 417 ctx.fillStyle = theme.panel; 279 418 ctx.fillRect(0, 0, w, h); ··· 299 438 line(ctx, xTL1, yTop, xLoad, yTop); 300 439 line(ctx, xLoad, yTop, xRight, yTop); 301 440 302 - line(ctx, xSourceL, yBot, xRight, yBot); 303 - 304 - // Left vertical (source) 305 - line(ctx, xSourceL, yTop, xSourceL, yBot); 441 + // Bottom return path with transmission-line symbol 442 + line(ctx, xSourceL, yBot, xTL0, yBot); 443 + line(ctx, xTL1, yBot, xRight, yBot); 306 444 307 445 // Voltage source symbol (circle) 308 446 const vsx = xSourceL, vsy = (yTop + yBot) / 2; 447 + // Source lead wires stop at the source body so no line passes through the symbol. 448 + line(ctx, xSourceL, yTop, xSourceL, vsy - 20); 449 + line(ctx, xSourceL, vsy + 20, xSourceL, yBot); 309 450 circle(ctx, vsx, vsy, 20, theme.ink); 310 451 // +/- inside 311 452 ctx.fillStyle = theme.ink; 312 453 ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 313 454 ctx.fillText("+", vsx - 4, vsy - 6); 314 455 ctx.fillText("−", vsx - 4, vsy + 14); 315 - label(ctx, "Vg", vsx - 10, vsy - 30, theme.muted); 456 + label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted); 316 457 317 458 // Source resistor Rg on top wire (zigzag) 318 459 const r0 = xSourceL + 20, r1 = xSwitch - 20; ··· 327 468 ctx.lineWidth = 2; 328 469 ctx.strokeRect(xTL0, yTop - 16, xTL1 - xTL0, 32); 329 470 label(ctx, "Z0", (xTL0 + xTL1) / 2 - 10, yTop - 26, theme.muted); 471 + 472 + // Bottom transmission line rectangle 473 + ctx.strokeRect(xTL0, yBot - 16, xTL1 - xTL0, 32); 330 474 331 475 // Load resistor RL (vertical on right) 332 - drawResistor(ctx, xLoad, yTop, xLoad, yBot, 8, 8); 476 + const rlTop = yTop + 22; 477 + const rlBot = yBot - 22; 478 + ctx.strokeStyle = theme.ink; 479 + ctx.lineWidth = 2; 480 + line(ctx, xLoad, yTop, xLoad, rlTop); 481 + drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 6, 8); 482 + line(ctx, xLoad, rlBot, xLoad, yBot); 333 483 label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 334 484 335 485 // VS and VL polarity markers ··· 342 492 ctx.arc(xRight, yBot, 3.5, 0, Math.PI * 2); 343 493 ctx.fill(); 344 494 345 - // Wavefront highlight in the transmission line rectangle: 346 - // incident: small bar moving right; reflected: small bar moving left (after tn>=1) 495 + // Wavefront highlights: dashed vertical lines crossing both T-line symbols. 347 496 const tlW = (xTL1 - xTL0); 348 - const barH = 24; 349 - const barW = 8; // intentionally small: "not wider than the transmission line symbol" 350 - const zy = yTop - barH / 2; 497 + const wfY0 = yTop - 16; 498 + const wfY1 = yBot + 16; 351 499 352 - // Incident front position 353 - const zi = clamp(tn, 0, 1); 354 - const xi = xTL0 + zi * tlW; 355 - 356 - if (tn >= 0) { 357 - ctx.fillStyle = theme.accent; 358 - ctx.globalAlpha = 0.95; 359 - ctx.fillRect(xi - barW / 2, zy, barW, barH); 360 - ctx.globalAlpha = 1; 361 - } 362 - 363 - // Reflected front position 364 - if (tn >= 1) { 365 - const zr = clamp(1 - (tn - 1), 0, 1); // starts at 1 and moves left 366 - const xr = xTL0 + zr * tlW; 367 - ctx.fillStyle = theme.accent2; 368 - ctx.globalAlpha = 0.95; 369 - ctx.fillRect(xr - barW / 2, zy, barW, barH); 370 - ctx.globalAlpha = 1; 371 - 372 - // Little curved arrow near the load to emphasize reversal (like Fig. 6.3) 373 - ctx.strokeStyle = theme.accent2; 374 - ctx.lineWidth = 2; 375 - curvedArrow(ctx, xTL1 + 10, yTop - 22, xTL1 - 40, yTop - 22); 500 + for (const wf of dyn.activeWaves) { 501 + const xw = xTL0 + wf.front * tlW; 502 + ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 503 + ctx.lineWidth = 3; 504 + ctx.setLineDash([9, 7]); 505 + line(ctx, xw, wfY0, xw, wfY1); 506 + ctx.setLineDash([]); 376 507 } 377 508 378 509 // z=0 and z=l labels 379 - label(ctx, "z = 0", xTL0 - 20, yBot - 8, theme.muted); 380 - label(ctx, "z = ℓ", xTL1 - 18, yBot - 8, theme.muted); 510 + label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted); 511 + label(ctx, "z = ℓ", xTL1 - 18, yBot + 34, theme.muted); 381 512 } 382 513 383 514 function drawSwitch(ctx, x, y, tn) { ··· 394 525 // right contact 395 526 circleFill(ctx, b.x, b.y, 3.2, theme.ink); 396 527 528 + // Always draw as an angled blade to keep the switch symbol visually clear. 397 529 if (closed) { 398 - line(ctx, a.x, a.y, b.x, b.y); 530 + line(ctx, a.x + 1, a.y - 8, b.x, b.y); 399 531 } else { 400 - // angled blade 401 - line(ctx, a.x, a.y, b.x - 2, b.y - 14); 532 + line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14); 402 533 } 403 534 label(ctx, "t = 0", x - 18, y + 26, theme.muted); 404 535 } ··· 406 537 function drawVoltageProbe(ctx, x, yTop, yBot, name) { 407 538 // + at top, - at bottom with a brace line 408 539 ctx.strokeStyle = theme.muted; 409 - ctx.lineWidth = 1.5; 410 - line(ctx, x, yTop + 6, x, yBot - 6); 540 + //ctx.lineWidth = 1.5; 541 + //line(ctx, x, yTop + 6, x, yBot - 6); 411 542 ctx.fillStyle = theme.muted; 412 - ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 543 + ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 413 544 ctx.fillText("+", x + 6, yTop + 12); 414 545 ctx.fillText("−", x + 6, yBot - 4); 415 546 label(ctx, name, x + 10, (yTop + yBot) / 2 - 6, theme.muted); ··· 499 630 // ----------------------------- 500 631 // Rendering: Plot (V vs z) 501 632 // ----------------------------- 502 - function drawPlot(ctx, w, h, tn, waves) { 633 + function drawPlot(ctx, w, h, tn, dyn) { 503 634 ctx.clearRect(0, 0, w, h); 504 635 ctx.fillStyle = theme.panel; 505 636 ctx.fillRect(0, 0, w, h); 506 637 507 - const padL = 58, padR = 18, padT = 22, padB = 44; 508 - const plotW = w - padL - padR; 509 - const plotH = h - padT - padB; 638 + const padT = PLOT_PAD_T, padB = PLOT_PAD_B, panelGap = PLOT_PANEL_GAP; 639 + // Match circuit T-line x extents so z=0 and z=ℓ align vertically with the circuit view. 640 + const circuitPad = 18; 641 + const xPlot0 = circuitPad + 240; 642 + const xPlot1 = w - circuitPad - 210; 643 + const plotW = xPlot1 - xPlot0; 644 + const xOfZ = (z) => xPlot0 + z * plotW; 510 645 511 - // Determine vertical range a bit intelligently 512 - const Vmin = Math.min(0, waves.V1 + Math.min(0, waves.V2)) - 0.25 * Math.max(1e-6, Math.abs(waves.V1)); 513 - const Vmax = Math.max(0, waves.V1 + Math.max(0, waves.V2)) + 0.25 * Math.max(1e-6, Math.abs(waves.V1)); 514 - const vLo = (Number.isFinite(Vmin) ? Vmin : -1); 515 - const vHi = (Number.isFinite(Vmax) ? Vmax : 1); 646 + const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 647 + const shownWaves = launched; 516 648 517 - const xOfZ = (z) => padL + z * plotW; 518 - const yOfV = (V) => padT + (vHi - V) / (vHi - vLo) * plotH; 649 + function totalSegmentsForWaves(waves) { 650 + const breakpoints = [0, 1]; 651 + for (const wf of waves) breakpoints.push(wf.front); 652 + breakpoints.sort((a, b) => a - b); 653 + const pts = []; 654 + for (const x of breakpoints) { 655 + if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x); 656 + } 657 + const segs = []; 658 + for (let i = 0; i < pts.length - 1; i++) { 659 + const a = pts[i], b = pts[i + 1]; 660 + const m = (a + b) / 2; 661 + let V = 0; 662 + for (const wf of waves) { 663 + if (wf.dir > 0 && m <= wf.front) V += wf.A; 664 + if (wf.dir < 0 && m >= wf.front) V += wf.A; 665 + } 666 + segs.push({ a, b, V }); 667 + } 668 + return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 669 + } 519 670 520 - // Grid 521 - ctx.strokeStyle = theme.grid; 522 - ctx.lineWidth = 1; 523 - const nGridX = 10, nGridY = 6; 524 - for (let i = 0; i <= nGridX; i++) { 525 - const x = padL + (i / nGridX) * plotW; 526 - line(ctx, x, padT, x, padT + plotH); 671 + function segmentsForWave(wf) { 672 + if (wf.dir > 0) return [ 673 + { a: 0, b: wf.front, V: wf.A }, 674 + { a: wf.front, b: 1, V: 0 } 675 + ]; 676 + return [ 677 + { a: 0, b: wf.front, V: 0 }, 678 + { a: wf.front, b: 1, V: wf.A } 679 + ]; 527 680 } 528 - for (let j = 0; j <= nGridY; j++) { 529 - const y = padT + (j / nGridY) * plotH; 530 - line(ctx, padL, y, padL + plotW, y); 681 + 682 + const panels = [{ 683 + label: "Sum (all waves)", 684 + color: theme.ok, 685 + segments: totalSegmentsForWaves(launched), 686 + markers: dyn.activeWaves.map(wf => wf.front) 687 + }]; 688 + for (const wf of shownWaves) { 689 + panels.push({ 690 + label: `V${wf.n} ${wf.dir > 0 ? "incident" : "reflected"}`, 691 + color: wf.dir > 0 ? theme.accent : theme.accent2, 692 + segments: segmentsForWave(wf), 693 + markers: (wf.u < 1) ? [wf.front] : [] 694 + }); 531 695 } 532 696 533 - // Axes 534 - ctx.strokeStyle = theme.ink; 535 - ctx.lineWidth = 1.8; 536 - line(ctx, padL, padT, padL, padT + plotH); 537 - line(ctx, padL, padT + plotH, padL + plotW, padT + plotH); 697 + const nPanels = panels.length; 698 + const panelH = PLOT_PANEL_H; 538 699 539 - // Axis labels 540 - ctx.fillStyle = theme.muted; 541 - ctx.font = "13px ui-sans-serif, system-ui"; 542 - ctx.fillText("Voltage", 12, padT + 14); 543 - ctx.fillText("z (position)", padL + plotW - 85, padT + plotH + 34); 700 + // Fixed vertical scale for all panels/time for the current parameter set. 701 + const maxAbsWave = Math.max(1e-6, ...shownWaves.map((wf) => Math.abs(wf.A))); 702 + const maxAbsSum = Math.max(1e-6, panels[0].segments.reduce((m, seg) => Math.max(m, Math.abs(seg.V)), 0)); 703 + const vScale = Math.max(maxAbsWave, maxAbsSum); 704 + const vLo = -1.15 * vScale; 705 + const vHi = 1.15 * vScale; 544 706 545 - // Tick labels (just endpoints + 0V) 546 - ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 547 707 ctx.fillStyle = theme.muted; 548 - ctx.fillText("0", padL - 10, padT + plotH + 18); 549 - ctx.fillText("ℓ", padL + plotW - 10, padT + plotH + 18); 550 - const y0 = yOfV(0); 551 - ctx.fillText("0V", 18, y0 + 4); 708 + ctx.font = "12px ui-sans-serif, system-ui"; 709 + ctx.fillText("Voltage along the T-line", 12, 14); 552 710 553 - // Compute spatial waveforms at time tn 554 - // Incident: step from z=0 to z=zi with amplitude V1 555 - // Reflected (after tn>=1): step from z=1 down to z=zr with amplitude V2 556 - const zi = clamp(tn, 0, 1); 557 - const zr = (tn >= 1) ? clamp(1 - (tn - 1), 0, 1) : 1; // when tn=1 -> zr=1 711 + for (let p = 0; p < nPanels; p++) { 712 + const top = padT + p * (panelH + panelGap); 713 + const bottom = top + panelH; 714 + const midY = (top + bottom) / 2; 558 715 559 - // Draw incident waveform 560 - drawStep(ctx, xOfZ, yOfV, 0, zi, waves.V1, theme.accent); 716 + const yOfV = (V) => top + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 561 717 562 - // Draw reflected waveform 563 - if (tn >= 1) { 564 - drawStep(ctx, xOfZ, yOfV, zr, 1, waves.V2, theme.accent2); 565 - } 718 + // Panel frame and axes 719 + ctx.strokeStyle = theme.grid; 720 + ctx.lineWidth = 1; 721 + line(ctx, xPlot0, top, xPlot1, top); 722 + line(ctx, xPlot0, bottom, xPlot1, bottom); 723 + line(ctx, xPlot0, top, xPlot0, bottom); 724 + line(ctx, xPlot1, top, xPlot1, bottom); 566 725 567 - // Draw total waveform (superposition) 568 - // Total is V1 on [0, zi]; plus V2 on [zr, 1] (when reflected exists). 569 - // Overlap region exists when zr < zi (late times): total becomes V1+V2 there. 570 - const segments = []; 571 - // Build segments on z axis with constant total voltage 572 - // Casework: 573 - // - Before reflection: [0,zi] = V1; [zi,1]=0 574 - // - After reflection: add V2 on [zr,1] 575 - if (tn < 1) { 576 - segments.push({ a: 0, b: zi, V: waves.V1 }); 577 - segments.push({ a: zi, b: 1, V: 0 }); 578 - } else { 579 - // Partition at sorted breakpoints 580 - const pts = [0, zi, zr, 1].sort((a,b)=>a-b); 581 - for (let k=0;k<pts.length-1;k++){ 582 - const a = pts[k], b = pts[k+1]; 583 - const mid = (a+b)/2; 584 - let V = 0; 585 - if (mid <= zi) V += waves.V1; 586 - if (mid >= zr) V += waves.V2; 587 - segments.push({ a, b, V }); 726 + ctx.strokeStyle = theme.muted; 727 + ctx.lineWidth = 1.2; 728 + line(ctx, xPlot0, yOfV(0), xPlot1, yOfV(0)); 729 + 730 + drawPiecewise(ctx, xOfZ, yOfV, panels[p].segments, panels[p].color, p === 0 ? 2.4 : 2.0); 731 + 732 + // Wavefront markers 733 + if (panels[p].markers.length) { 734 + ctx.strokeStyle = theme.warn; 735 + ctx.lineWidth = 1.2; 736 + ctx.setLineDash([4, 5]); 737 + for (const zf of panels[p].markers) line(ctx, xOfZ(zf), top, xOfZ(zf), bottom); 738 + ctx.setLineDash([]); 588 739 } 740 + 741 + // Labels 742 + ctx.fillStyle = theme.muted; 743 + ctx.font = "12px ui-sans-serif, system-ui"; 744 + ctx.fillText(panels[p].label, xPlot0 + 8, top + 14); 745 + ctx.fillText("0", xPlot0 - 14, yOfV(0) + 4); 746 + ctx.fillText("z", xPlot1 + 6, midY + 4); 589 747 } 590 - drawPiecewise(ctx, xOfZ, yOfV, segments, theme.ok, 2.8); 591 748 592 - // Wavefront markers (vertical dotted lines at zi and zr) 593 - ctx.strokeStyle = theme.warn; 594 - ctx.lineWidth = 1.4; 595 - ctx.setLineDash([5, 5]); 596 - line(ctx, xOfZ(zi), padT, xOfZ(zi), padT + plotH); 597 - if (tn >= 1) line(ctx, xOfZ(zr), padT, xOfZ(zr), padT + plotH); 598 - ctx.setLineDash([]); 749 + // Shared z-axis endpoint labels 750 + ctx.fillStyle = theme.muted; 751 + ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 752 + ctx.fillText("0", xPlot0 - 4, h - 6); 753 + ctx.fillText("ℓ", xPlot1 - 4, h - 6); 754 + } 599 755 600 - // Caption-ish 601 - ctx.fillStyle = theme.muted; 602 - ctx.font = "12px ui-sans-serif, system-ui"; 603 - ctx.fillText("Wavefront(s) shown as dashed lines; total is the physically relevant waveform along the line.", padL, padT - 6); 756 + function ensurePlotCanvasHeight(nPanels) { 757 + const targetH = Math.round(PLOT_PAD_T + PLOT_PAD_B + nPanels * PLOT_PANEL_H + (nPanels - 1) * PLOT_PANEL_GAP); 758 + const curr = parseInt(el.plot.getAttribute("height") || "0", 10); 759 + if (curr !== targetH) el.plot.setAttribute("height", String(targetH)); 604 760 } 605 761 606 762 function drawStep(ctx, xOfZ, yOfV, zA, zB, V, color) { ··· 637 793 function render() { 638 794 recomputeModelFromInputs(); 639 795 const waves = computeWaveParams(); 640 - const ends = computeEndVoltages(tNorm, waves.V1, waves.V2); 796 + const bounce = buildBounceSeries(waves); 797 + const dyn = computeDynamicState(tNorm, bounce); 798 + timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 641 799 642 800 el.tRead.textContent = fmt(tNorm, 3); 643 801 el.gLRead.textContent = fmt(waves.gL, 3); 644 - el.v1Read.textContent = `${fmt(waves.V1, 3)} V`; 645 - el.v2Read.textContent = `${fmt(waves.V2, 3)} V`; 646 - el.vsRead.textContent = `${fmt(ends.VS, 3)} V`; 647 - el.vlRead.textContent = `${fmt(ends.VL, 3)} V`; 802 + el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 803 + el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 804 + updateDerivedDisplays(waves, bounce); 805 + 806 + if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 807 + mathjaxTypesetDone = true; 808 + window.MathJax.typesetPromise(); 809 + } 648 810 649 811 // Circuit 650 812 const c = resizeCanvasToCSS(el.circuit); 651 - drawCircuit(c.ctx, c.w, c.h, tNorm, waves); 813 + drawCircuit(c.ctx, c.w, c.h, tNorm, dyn); 652 814 653 815 // Plot 816 + ensurePlotCanvasHeight(1 + dyn.launchedWaves.length); 654 817 const p = resizeCanvasToCSS(el.plot); 655 - drawPlot(p.ctx, p.w, p.h, tNorm, waves); 818 + drawPlot(p.ctx, p.w, p.h, tNorm, dyn); 656 819 } 657 820 658 821 function tick(ts) { ··· 665 828 const dtn = dt / secPerTau; 666 829 tNorm += dtn; 667 830 668 - // Stop condition: 669 - // - If singleBounce: run a bit past 2τd so you see the return to the source. 670 - // - Else: just loop. 671 - if (model.singleBounce) { 672 - if (tNorm > 2.4) { // let it linger a moment 831 + // Stop/loop at the current model-dependent horizon. 832 + if (tNorm > timeHorizon) { 833 + if (model.singleBounce) { 673 834 running = false; 674 835 lastTS = null; 836 + } else { 837 + tNorm = 0; 675 838 } 676 - } else { 677 - if (tNorm > 3.0) tNorm = 0; // placeholder loop for now 678 839 } 679 840 680 841 render(); ··· 690 851 } 691 852 692 853 function pause() { 693 - running = false; 854 + if (running) { 855 + running = false; 856 + lastTS = null; 857 + render(); 858 + return; 859 + } 860 + // Resume from current time; if already at the end, restart from t=0. 861 + if (tNorm >= timeHorizon - 1e-9) tNorm = 0; 862 + running = true; 694 863 lastTS = null; 695 864 render(); 865 + requestAnimationFrame(tick); 696 866 } 697 867 698 868 function reset() { ··· 703 873 } 704 874 705 875 // ----------------------------- 706 - // Scrubbing (optional) 876 + // Scrubbing 707 877 // ----------------------------- 708 878 function scrubFromEvent(ev) { 709 879 if (!el.scrubToggle.checked) return; 710 880 if (running) return; 711 881 712 - // Map x-position over either canvas to tNorm in [0,2.2] for single-bounce view 882 + // Map x-position over either canvas to tNorm in [0,timeHorizon]. 713 883 const rect = ev.target.getBoundingClientRect(); 714 884 const x = clamp(ev.clientX - rect.left, 0, rect.width); 715 885 const u = x / rect.width; 716 886 717 - // Focus on 0..2τd timeline (so you can sweep incident to reflection-to-return) 718 - tNorm = u * 2.2; 887 + tNorm = u * timeHorizon; 719 888 render(); 720 889 } 721 890 ··· 726 895 el.pauseBtn.addEventListener("click", pause); 727 896 el.resetBtn.addEventListener("click", reset); 728 897 729 - for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.singleBounce]) { 898 + for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.reflectTol, el.singleBounce]) { 730 899 inp.addEventListener("input", () => { if (!running) render(); }); 731 900 inp.addEventListener("change", () => { if (!running) render(); }); 732 901 }