this repo has no description
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 ${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})();