this repo has no description
at v2 466 lines 17 kB view raw
1"use strict"; 2 3// Application entry point — state, animation loop, event wiring, UI updates. 4// Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) 5(() => { 6 const { clamp, fmt, resizeCanvasToCSS } = TLUtils; 7 const { 8 computeWaveParams, buildBounceSeries, computeDynamicState, 9 simulateTimeDomain, voltageAt, 10 } = TLPhysics; 11 const { getTheme, drawCircuit, drawPlot, drawPlotSim, drawProbe, drawProbeSim, ensurePlotCanvasHeight } = TLRender; 12 13 // ---- DOM references ---- 14 const el = { 15 probe: document.getElementById("probe"), 16 circuit: document.getElementById("circuit"), 17 plot: document.getElementById("plot"), 18 startBtn: document.getElementById("startBtn"), 19 pauseBtn: document.getElementById("pauseBtn"), 20 resetBtn: document.getElementById("resetBtn"), 21 Vg: document.getElementById("Vg"), 22 Rg: document.getElementById("Rg"), 23 loadType: document.getElementById("loadType"), 24 loadValue: document.getElementById("loadValue"), 25 loadUnit: document.getElementById("loadUnit"), 26 segCount: document.getElementById("segCount"), 27 segZ0List: document.getElementById("segZ0List"), 28 junctionList: document.getElementById("junctionList"), 29 tauD: document.getElementById("tauD"), 30 secPerTau: document.getElementById("secPerTau"), 31 secPerTauRead: document.getElementById("secPerTauRead"), 32 reflectTol: document.getElementById("reflectTol"), 33 tRead: document.getElementById("tRead"), 34 gLRead: document.getElementById("gLRead"), 35 vsRead: document.getElementById("vsRead"), 36 vlRead: document.getElementById("vlRead"), 37 derivedValues: document.getElementById("derivedValues"), 38 waveValues: document.getElementById("waveValues"), 39 riseMode: document.getElementById("riseMode"), 40 riseTau: document.getElementById("riseTau"), 41 }; 42 43 // ---- model (physics parameters) ---- 44 const model = { 45 Vg: 5, 46 Rg: 20, 47 segments: [{ Z0: 50 }], // backward-compat for bounce series 48 RL: 30, 49 blocks: [], // set by syncModelFromInputs 50 terminal: { type: 'R', value: 30 }, 51 tau_d: 1e-9, 52 secPerTau: 2.5, 53 reflectTol: 1, 54 riseTimeTr: 0, 55 riseShape: "step", 56 }; 57 58 // ---- segment + junction input management ---- 59 let segZ0Inputs = []; 60 let junctionInputs = []; // [{typeEl, valueEl}] for N-1 internal junctions 61 62 const UNIT_FOR_TYPE = { R: 'Ω', C: 'pF', L: 'nH', short: '', none: '' }; 63 64 function buildSegmentInputs(n) { 65 // Preserve existing Z0 values. 66 const prevZ0 = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); 67 const prevJ = junctionInputs.map((j) => ({ type: j.typeEl.value, val: j.valueEl.value })); 68 el.segZ0List.innerHTML = ""; 69 el.junctionList.innerHTML = ""; 70 segZ0Inputs = []; 71 junctionInputs = []; 72 73 for (let i = 0; i < n; i++) { 74 // Z0 input for segment i 75 const z0val = (prevZ0[i] != null) ? prevZ0[i] : 50; 76 const inp = document.createElement("input"); 77 inp.type = "number"; 78 inp.min = "0.1"; 79 inp.step = "0.1"; 80 inp.value = z0val; 81 inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; 82 inp.addEventListener("input", rerender); 83 inp.addEventListener("change", rerender); 84 el.segZ0List.appendChild(inp); 85 segZ0Inputs.push(inp); 86 87 // Junction element between segment i and i+1 88 if (i < n - 1) { 89 const p = prevJ[i] || { type: 'none', val: '100' }; 90 const row = document.createElement("div"); 91 row.className = "junction-item"; 92 93 const lbl = document.createElement("span"); 94 lbl.textContent = `${i + 1}\u21C4${i + 2}`; 95 96 const typeEl = document.createElement("select"); 97 for (const t of ['none', 'R', 'C', 'L', 'short']) { 98 const opt = document.createElement("option"); 99 opt.value = t; 100 opt.textContent = t === 'none' ? '— none —' : t; 101 if (t === p.type) opt.selected = true; 102 typeEl.appendChild(opt); 103 } 104 105 const valueEl = document.createElement("input"); 106 valueEl.type = "number"; 107 valueEl.min = "0"; 108 valueEl.step = "0.1"; 109 valueEl.value = p.val; 110 valueEl.style.display = (p.type === 'none' || p.type === 'short') ? 'none' : ''; 111 112 const unitEl = document.createElement("span"); 113 unitEl.textContent = UNIT_FOR_TYPE[p.type] || ''; 114 unitEl.style.minWidth = "24px"; 115 116 typeEl.addEventListener("change", () => { 117 const t = typeEl.value; 118 valueEl.style.display = (t === 'none' || t === 'short') ? 'none' : ''; 119 unitEl.textContent = UNIT_FOR_TYPE[t] || ''; 120 rerender(); 121 }); 122 valueEl.addEventListener("input", rerender); 123 valueEl.addEventListener("change", rerender); 124 125 row.appendChild(lbl); 126 row.appendChild(typeEl); 127 row.appendChild(valueEl); 128 row.appendChild(unitEl); 129 el.junctionList.appendChild(row); 130 junctionInputs.push({ typeEl, valueEl }); 131 } 132 } 133 } 134 135 // Initialise with N=1. 136 buildSegmentInputs(1); 137 138 // ---- load type control ---- 139 el.loadType.addEventListener("change", () => { 140 const t = el.loadType.value; 141 el.loadValue.disabled = (t === 'open' || t === 'short'); 142 el.loadUnit.textContent = UNIT_FOR_TYPE[t] || ''; 143 rerender(); 144 }); 145 el.loadValue.addEventListener("input", rerender); 146 el.loadValue.addEventListener("change", rerender); 147 148 // ---- animation state ---- 149 let running = false; 150 let hasStarted = false; 151 let tNorm = 0; 152 let lastTS = null; 153 let timeHorizon = 2.2; 154 155 let mathjaxTypesetDone = false; 156 let theme = getTheme(); 157 158 // ---- simulation cache ---- 159 // cachedSim is recomputed whenever the model changes (detected by JSON key). 160 let cachedSim = null; 161 let cachedSimKey = ''; 162 163 // Returns true if any junction or terminal has reactive (C or L) elements, 164 // meaning we must use simulateTimeDomain for correct physics. 165 function needsMoCSim() { 166 if (!model.blocks) return false; 167 for (const blk of model.blocks) { 168 if (blk.type === 'C' || blk.type === 'L') return true; 169 } 170 if (model.terminal.type === 'C' || model.terminal.type === 'L') return true; 171 return false; 172 } 173 174 // Returns true if the model has ANY shunt element (R, C, L, or short) at a 175 // junction — i.e., anything beyond the default "just T-line segments" topology. 176 function hasJunctionElements() { 177 if (!model.blocks) return false; 178 for (const blk of model.blocks) { 179 if (blk.type !== 'tl') return true; 180 } 181 return false; 182 } 183 184 function getOrComputeSim() { 185 const key = JSON.stringify(model.blocks) + JSON.stringify(model.terminal) 186 + model.tau_d + model.riseTimeTr + model.riseShape + model.Vg + model.Rg; 187 if (key !== cachedSimKey) { 188 const tEnd = Math.max(16, timeHorizon + 2); 189 cachedSim = simulateTimeDomain(model, { tEnd, oversample: 128 }); 190 cachedSimKey = key; 191 } 192 return cachedSim; 193 } 194 195 // ---- model sync ---- 196 function syncModelFromInputs() { 197 model.Vg = parseFloat(el.Vg.value); 198 model.Rg = parseFloat(el.Rg.value); 199 model.secPerTau = parseFloat(el.secPerTau.value); 200 model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 201 model.riseShape = el.riseMode.value === "step" ? "step" 202 : el.riseMode.value === "linear" ? "linear" : "erf"; 203 model.riseTimeTr = model.riseShape !== "step" 204 ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) 205 : 0; 206 model.tau_d = Math.max(1e-12, parseFloat(el.tauD.value) || 1) * 1e-9; 207 208 const segsZ0 = segZ0Inputs.map((inp) => Math.max(0.1, parseFloat(inp.value) || 50)); 209 const N = segsZ0.length; 210 const tau = 1 / N; // normalized tau per segment (each segment = 1/N of total τ_d) 211 212 // Build model.blocks: interleave TL segments with junction shunt elements. 213 const blocks = []; 214 for (let i = 0; i < N; i++) { 215 blocks.push({ type: 'tl', Z0: segsZ0[i], tau }); 216 if (i < N - 1 && junctionInputs[i]) { 217 const jt = junctionInputs[i].typeEl.value; 218 const jvRaw = parseFloat(junctionInputs[i].valueEl.value) || 0; 219 if (jt !== 'none') { 220 // Convert from display units to SI. 221 // R: Ω (direct), C: pF → F (* 1e-12), L: nH → H (* 1e-9) 222 const jv = jt === 'C' ? jvRaw * 1e-12 : jt === 'L' ? jvRaw * 1e-9 : jvRaw; 223 blocks.push({ type: jt, value: jv }); 224 } 225 } 226 } 227 model.blocks = blocks; 228 229 // Build model.terminal from load type/value. 230 const lt = el.loadType.value; 231 const lvRaw = parseFloat(el.loadValue.value) || 0; 232 if (lt === 'open') { 233 model.terminal = { type: 'open' }; 234 model.RL = Infinity; 235 } else if (lt === 'short') { 236 model.terminal = { type: 'short' }; 237 model.RL = 0; 238 } else if (lt === 'R') { 239 const rv = Math.max(0.001, lvRaw); 240 model.terminal = { type: 'R', value: rv }; 241 model.RL = rv; 242 } else if (lt === 'C') { 243 model.terminal = { type: 'C', value: lvRaw * 1e-12 }; 244 model.RL = Infinity; // cap looks open at DC initially 245 } else if (lt === 'L') { 246 model.terminal = { type: 'L', value: lvRaw * 1e-9 }; 247 model.RL = 0; // inductor shorts at DC 248 } 249 250 // Backward-compat: model.segments used by buildBounceSeries. 251 model.segments = segsZ0.map((Z0) => ({ Z0 })); 252 } 253 254 // ---- derived-value readout panel (bounce-series mode only) ---- 255 function updateDerivedDisplays(model, waves, bounce) { 256 const N = model.segments.length; 257 const gS = bounce.gSEffective; 258 259 const lines = [ 260 `\u0393<sub>S</sub> = ${fmt(gS, 6)}`, 261 `\u0393<sub>L</sub> = ${fmt(waves.gL, 6)}`, 262 ]; 263 264 if (N > 1) { 265 for (let i = 0; i < N - 1; i++) { 266 const Zl = model.segments[i].Z0; 267 const Zr = model.segments[i + 1].Z0; 268 const g = (Zr - Zl) / (Zr + Zl); 269 lines.push(`\u0393<sub>${i + 1}\u2192${i + 2}</sub> = ${fmt(g, 6)} (${Zl}\u03A9\u2192${Zr}\u03A9)`); 270 } 271 } 272 273 lines.push(`V<sub>1</sub> = ${fmt(waves.V1, 6)} V`); 274 275 if (N === 1) { 276 const V2calc = waves.gL * waves.V1; 277 const V3calc = gS * V2calc; 278 const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 279 const v3Reason = v3Suppressed 280 ? `suppressed since |V<sub>3</sub>| \u2264 \u03B5 (\u03B5=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>|)` 281 : "nonzero, so re-reflection should appear"; 282 lines.push( 283 `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>| = ${fmt(bounce.ampTol, 6)} V`, 284 `V<sub>2</sub> = \u0393<sub>L</sub>\u00B7V<sub>1</sub> = (${fmt(waves.gL, 6)})\u00B7(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, 285 `V<sub>3</sub> = \u0393<sub>S</sub>\u00B7V<sub>2</sub> = (${fmt(gS, 6)})\u00B7(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, 286 `V<sub>3</sub> status: ${v3Reason}`, 287 ); 288 } else { 289 lines.push(`Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>| = ${fmt(bounce.ampTol, 6)} V`); 290 } 291 292 lines.push(`Total generated waves: ${bounce.series.length}`); 293 el.derivedValues.innerHTML = lines.map((x) => `<div>${x}</div>`).join(""); 294 295 el.waveValues.innerHTML = bounce.series.map((w) => { 296 const arrow = w.dir > 0 ? "\u2192" : "\u2190"; 297 const segStr = N > 1 ? ` [seg&nbsp;${w.segIdx + 1}]` : ""; 298 return `<div>V<sub>${w.n}</sub> ${arrow}${segStr} = ${fmt(w.A, 6)} V</div>`; 299 }).join(""); 300 } 301 302 // Build junctions array for drawCircuit from junctionInputs. 303 function getJunctionsForDraw() { 304 return junctionInputs.map((j) => ({ type: j.typeEl.value })); 305 } 306 307 // ---- render frame ---- 308 function render() { 309 syncModelFromInputs(); 310 311 el.secPerTauRead.textContent = fmt(model.secPerTau, 1); 312 el.tRead.textContent = fmt(tNorm, 3); 313 314 if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 315 mathjaxTypesetDone = true; 316 window.MathJax.typesetPromise(); 317 } 318 319 if (!hasStarted) { 320 el.pauseBtn.textContent = "Pause"; 321 } else { 322 el.pauseBtn.textContent = running ? "Pause" : "Resume"; 323 } 324 el.pauseBtn.disabled = !hasStarted; 325 326 const junctions = getJunctionsForDraw(); 327 const simMode = hasJunctionElements() || needsMoCSim(); 328 329 if (simMode) { 330 // ---- MoC simulation mode ---- 331 const sim = getOrComputeSim(); 332 const tIdx = Math.min(Math.round(tNorm / sim.dt), sim.nSteps - 1); 333 const VS = sim.nodeV[0][tIdx]; 334 const VL = sim.nodeV[sim.nodeV.length - 1][tIdx]; 335 336 el.gLRead.textContent = '—'; 337 el.vsRead.textContent = `${fmt(VS, 3)} V`; 338 el.vlRead.textContent = `${fmt(VL, 3)} V`; 339 el.derivedValues.innerHTML = '<div>MoC simulation mode — junction elements active</div>'; 340 el.waveValues.innerHTML = ''; 341 342 // Compute timeHorizon from sim 343 timeHorizon = Math.min(sim.nSteps * sim.dt, tNorm + 1); 344 timeHorizon = Math.max(timeHorizon, 2.2); 345 346 // Probe — draw VS(t) from sim.nodeV[0] 347 const d = resizeCanvasToCSS(el.probe); 348 drawProbeSim(d.ctx, d.w, d.h, tNorm, sim, timeHorizon, theme); 349 350 // Circuit — show shunt element symbols 351 const c = resizeCanvasToCSS(el.circuit); 352 drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, []); 353 354 // Plot — V(z,t) from sim 355 ensurePlotCanvasHeight(el.plot, 2); 356 const p = resizeCanvasToCSS(el.plot); 357 drawPlotSim(p.ctx, p.w, p.h, tIdx, sim, theme); 358 359 } else { 360 // ---- Bounce-series mode (pure resistive, no junction elements) ---- 361 const waves = computeWaveParams(model); 362 const bounce = buildBounceSeries(model, waves); 363 const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); 364 timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 365 366 el.gLRead.textContent = fmt(waves.gL, 3); 367 el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 368 el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 369 updateDerivedDisplays(model, waves, bounce); 370 371 const wavefronts = dyn.activeWaves.map((wf) => ({ z: wf.front, dir: wf.dir })); 372 373 const d = resizeCanvasToCSS(el.probe); 374 drawProbe(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); 375 376 const c = resizeCanvasToCSS(el.circuit); 377 drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, wavefronts); 378 379 ensurePlotCanvasHeight(el.plot, 2); 380 const p = resizeCanvasToCSS(el.plot); 381 drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); 382 } 383 } 384 385 // ---- animation loop ---- 386 function tick(ts) { 387 if (!running) return; 388 if (lastTS == null) lastTS = ts; 389 const dt = (ts - lastTS) / 1000; 390 lastTS = ts; 391 392 tNorm += dt / Math.max(0.2, model.secPerTau); 393 394 if (tNorm > timeHorizon) { 395 running = false; 396 lastTS = null; 397 } 398 399 render(); 400 requestAnimationFrame(tick); 401 } 402 403 // ---- transport controls ---- 404 function start() { 405 hasStarted = true; 406 tNorm = 0; 407 running = true; 408 lastTS = null; 409 render(); 410 requestAnimationFrame(tick); 411 } 412 413 function pause() { 414 if (!hasStarted) return; 415 if (running) { 416 running = false; 417 lastTS = null; 418 render(); 419 return; 420 } 421 if (tNorm >= timeHorizon - 1e-9) tNorm = 0; 422 running = true; 423 lastTS = null; 424 render(); 425 requestAnimationFrame(tick); 426 } 427 428 function reset() { 429 running = false; 430 hasStarted = false; 431 lastTS = null; 432 tNorm = 0; 433 render(); 434 } 435 436 function rerender() { 437 if (!running) render(); 438 } 439 440 // ---- event wiring ---- 441 el.startBtn.addEventListener("click", start); 442 el.pauseBtn.addEventListener("click", pause); 443 el.resetBtn.addEventListener("click", reset); 444 445 for (const inp of [el.Vg, el.Rg, el.secPerTau, el.reflectTol, el.riseTau, el.tauD]) { 446 inp.addEventListener("input", rerender); 447 inp.addEventListener("change", rerender); 448 } 449 450 el.segCount.addEventListener("input", () => { 451 const n = Math.max(1, Math.min(10, parseInt(el.segCount.value) || 1)); 452 el.segCount.value = n; 453 buildSegmentInputs(n); 454 rerender(); 455 }); 456 457 el.riseMode.addEventListener("change", () => { 458 el.riseTau.disabled = el.riseMode.value === "step"; 459 rerender(); 460 }); 461 462 window.addEventListener("resize", () => { theme = getTheme(); render(); }); 463 464 // Initial render 465 render(); 466})();