"use strict"; // Application entry point — state, animation loop, event wiring, UI updates. // Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) (() => { const { clamp, fmt, resizeCanvasToCSS } = TLUtils; const { computeWaveParams, buildBounceSeries, computeDynamicState, simulateTimeDomain, voltageAt, } = TLPhysics; const { getTheme, drawCircuit, drawPlot, drawPlotSim, drawProbe, drawProbeSim, ensurePlotCanvasHeight } = TLRender; // ---- DOM references ---- const el = { probe: document.getElementById("probe"), circuit: document.getElementById("circuit"), plot: document.getElementById("plot"), startBtn: document.getElementById("startBtn"), pauseBtn: document.getElementById("pauseBtn"), resetBtn: document.getElementById("resetBtn"), Vg: document.getElementById("Vg"), Rg: document.getElementById("Rg"), loadType: document.getElementById("loadType"), loadValue: document.getElementById("loadValue"), loadUnit: document.getElementById("loadUnit"), segCount: document.getElementById("segCount"), segZ0List: document.getElementById("segZ0List"), junctionList: document.getElementById("junctionList"), tauD: document.getElementById("tauD"), secPerTau: document.getElementById("secPerTau"), secPerTauRead: document.getElementById("secPerTauRead"), reflectTol: document.getElementById("reflectTol"), tRead: document.getElementById("tRead"), gLRead: document.getElementById("gLRead"), vsRead: document.getElementById("vsRead"), vlRead: document.getElementById("vlRead"), derivedValues: document.getElementById("derivedValues"), waveValues: document.getElementById("waveValues"), riseMode: document.getElementById("riseMode"), riseTau: document.getElementById("riseTau"), }; // ---- model (physics parameters) ---- const model = { Vg: 5, Rg: 20, segments: [{ Z0: 50 }], // backward-compat for bounce series RL: 30, blocks: [], // set by syncModelFromInputs terminal: { type: 'R', value: 30 }, tau_d: 1e-9, secPerTau: 2.5, reflectTol: 1, riseTimeTr: 0, riseShape: "step", }; // ---- segment + junction input management ---- let segZ0Inputs = []; let junctionInputs = []; // [{typeEl, valueEl}] for N-1 internal junctions const UNIT_FOR_TYPE = { R: 'Ω', C: 'pF', L: 'nH', short: '', none: '' }; function buildSegmentInputs(n) { // Preserve existing Z0 values. const prevZ0 = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); const prevJ = junctionInputs.map((j) => ({ type: j.typeEl.value, val: j.valueEl.value })); el.segZ0List.innerHTML = ""; el.junctionList.innerHTML = ""; segZ0Inputs = []; junctionInputs = []; for (let i = 0; i < n; i++) { // Z0 input for segment i const z0val = (prevZ0[i] != null) ? prevZ0[i] : 50; const inp = document.createElement("input"); inp.type = "number"; inp.min = "0.1"; inp.step = "0.1"; inp.value = z0val; inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; inp.addEventListener("input", rerender); inp.addEventListener("change", rerender); el.segZ0List.appendChild(inp); segZ0Inputs.push(inp); // Junction element between segment i and i+1 if (i < n - 1) { const p = prevJ[i] || { type: 'none', val: '100' }; const row = document.createElement("div"); row.className = "junction-item"; const lbl = document.createElement("span"); lbl.textContent = `${i + 1}\u21C4${i + 2}`; const typeEl = document.createElement("select"); for (const t of ['none', 'R', 'C', 'L', 'short']) { const opt = document.createElement("option"); opt.value = t; opt.textContent = t === 'none' ? '— none —' : t; if (t === p.type) opt.selected = true; typeEl.appendChild(opt); } const valueEl = document.createElement("input"); valueEl.type = "number"; valueEl.min = "0"; valueEl.step = "0.1"; valueEl.value = p.val; valueEl.style.display = (p.type === 'none' || p.type === 'short') ? 'none' : ''; const unitEl = document.createElement("span"); unitEl.textContent = UNIT_FOR_TYPE[p.type] || ''; unitEl.style.minWidth = "24px"; typeEl.addEventListener("change", () => { const t = typeEl.value; valueEl.style.display = (t === 'none' || t === 'short') ? 'none' : ''; unitEl.textContent = UNIT_FOR_TYPE[t] || ''; rerender(); }); valueEl.addEventListener("input", rerender); valueEl.addEventListener("change", rerender); row.appendChild(lbl); row.appendChild(typeEl); row.appendChild(valueEl); row.appendChild(unitEl); el.junctionList.appendChild(row); junctionInputs.push({ typeEl, valueEl }); } } } // Initialise with N=1. buildSegmentInputs(1); // ---- load type control ---- el.loadType.addEventListener("change", () => { const t = el.loadType.value; el.loadValue.disabled = (t === 'open' || t === 'short'); el.loadUnit.textContent = UNIT_FOR_TYPE[t] || ''; rerender(); }); el.loadValue.addEventListener("input", rerender); el.loadValue.addEventListener("change", rerender); // ---- animation state ---- let running = false; let hasStarted = false; let tNorm = 0; let lastTS = null; let timeHorizon = 2.2; let mathjaxTypesetDone = false; let theme = getTheme(); // ---- simulation cache ---- // cachedSim is recomputed whenever the model changes (detected by JSON key). let cachedSim = null; let cachedSimKey = ''; // Returns true if any junction or terminal has reactive (C or L) elements, // meaning we must use simulateTimeDomain for correct physics. function needsMoCSim() { if (!model.blocks) return false; for (const blk of model.blocks) { if (blk.type === 'C' || blk.type === 'L') return true; } if (model.terminal.type === 'C' || model.terminal.type === 'L') return true; return false; } // Returns true if the model has ANY shunt element (R, C, L, or short) at a // junction — i.e., anything beyond the default "just T-line segments" topology. function hasJunctionElements() { if (!model.blocks) return false; for (const blk of model.blocks) { if (blk.type !== 'tl') return true; } return false; } function getOrComputeSim() { const key = JSON.stringify(model.blocks) + JSON.stringify(model.terminal) + model.tau_d + model.riseTimeTr + model.riseShape + model.Vg + model.Rg; if (key !== cachedSimKey) { const tEnd = Math.max(16, timeHorizon + 2); cachedSim = simulateTimeDomain(model, { tEnd, oversample: 128 }); cachedSimKey = key; } return cachedSim; } // ---- model sync ---- function syncModelFromInputs() { model.Vg = parseFloat(el.Vg.value); model.Rg = parseFloat(el.Rg.value); model.secPerTau = parseFloat(el.secPerTau.value); model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); model.riseShape = el.riseMode.value === "step" ? "step" : el.riseMode.value === "linear" ? "linear" : "erf"; model.riseTimeTr = model.riseShape !== "step" ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) : 0; model.tau_d = Math.max(1e-12, parseFloat(el.tauD.value) || 1) * 1e-9; const segsZ0 = segZ0Inputs.map((inp) => Math.max(0.1, parseFloat(inp.value) || 50)); const N = segsZ0.length; const tau = 1 / N; // normalized tau per segment (each segment = 1/N of total τ_d) // Build model.blocks: interleave TL segments with junction shunt elements. const blocks = []; for (let i = 0; i < N; i++) { blocks.push({ type: 'tl', Z0: segsZ0[i], tau }); if (i < N - 1 && junctionInputs[i]) { const jt = junctionInputs[i].typeEl.value; const jvRaw = parseFloat(junctionInputs[i].valueEl.value) || 0; if (jt !== 'none') { // Convert from display units to SI. // R: Ω (direct), C: pF → F (* 1e-12), L: nH → H (* 1e-9) const jv = jt === 'C' ? jvRaw * 1e-12 : jt === 'L' ? jvRaw * 1e-9 : jvRaw; blocks.push({ type: jt, value: jv }); } } } model.blocks = blocks; // Build model.terminal from load type/value. const lt = el.loadType.value; const lvRaw = parseFloat(el.loadValue.value) || 0; if (lt === 'open') { model.terminal = { type: 'open' }; model.RL = Infinity; } else if (lt === 'short') { model.terminal = { type: 'short' }; model.RL = 0; } else if (lt === 'R') { const rv = Math.max(0.001, lvRaw); model.terminal = { type: 'R', value: rv }; model.RL = rv; } else if (lt === 'C') { model.terminal = { type: 'C', value: lvRaw * 1e-12 }; model.RL = Infinity; // cap looks open at DC initially } else if (lt === 'L') { model.terminal = { type: 'L', value: lvRaw * 1e-9 }; model.RL = 0; // inductor shorts at DC } // Backward-compat: model.segments used by buildBounceSeries. model.segments = segsZ0.map((Z0) => ({ Z0 })); } // ---- derived-value readout panel (bounce-series mode only) ---- function updateDerivedDisplays(model, waves, bounce) { const N = model.segments.length; const gS = bounce.gSEffective; const lines = [ `\u0393S = ${fmt(gS, 6)}`, `\u0393L = ${fmt(waves.gL, 6)}`, ]; if (N > 1) { for (let i = 0; i < N - 1; i++) { const Zl = model.segments[i].Z0; const Zr = model.segments[i + 1].Z0; const g = (Zr - Zl) / (Zr + Zl); lines.push(`\u0393${i + 1}\u2192${i + 2} = ${fmt(g, 6)} (${Zl}\u03A9\u2192${Zr}\u03A9)`); } } lines.push(`V1 = ${fmt(waves.V1, 6)} V`); if (N === 1) { const V2calc = waves.gL * waves.V1; const V3calc = gS * V2calc; const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; const v3Reason = v3Suppressed ? `suppressed since |V3| \u2264 \u03B5 (\u03B5=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V1|)` : "nonzero, so re-reflection should appear"; lines.push( `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`, `V2 = \u0393L\u00B7V1 = (${fmt(waves.gL, 6)})\u00B7(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, `V3 = \u0393S\u00B7V2 = (${fmt(gS, 6)})\u00B7(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, `V3 status: ${v3Reason}`, ); } else { lines.push(`Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`); } lines.push(`Total generated waves: ${bounce.series.length}`); el.derivedValues.innerHTML = lines.map((x) => `
${x}
`).join(""); el.waveValues.innerHTML = bounce.series.map((w) => { const arrow = w.dir > 0 ? "\u2192" : "\u2190"; const segStr = N > 1 ? ` [seg ${w.segIdx + 1}]` : ""; return `
V${w.n} ${arrow}${segStr} = ${fmt(w.A, 6)} V
`; }).join(""); } // Build junctions array for drawCircuit from junctionInputs. function getJunctionsForDraw() { return junctionInputs.map((j) => ({ type: j.typeEl.value })); } // ---- render frame ---- function render() { syncModelFromInputs(); el.secPerTauRead.textContent = fmt(model.secPerTau, 1); el.tRead.textContent = fmt(tNorm, 3); if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { mathjaxTypesetDone = true; window.MathJax.typesetPromise(); } if (!hasStarted) { el.pauseBtn.textContent = "Pause"; } else { el.pauseBtn.textContent = running ? "Pause" : "Resume"; } el.pauseBtn.disabled = !hasStarted; const junctions = getJunctionsForDraw(); const simMode = hasJunctionElements() || needsMoCSim(); if (simMode) { // ---- MoC simulation mode ---- const sim = getOrComputeSim(); const tIdx = Math.min(Math.round(tNorm / sim.dt), sim.nSteps - 1); const VS = sim.nodeV[0][tIdx]; const VL = sim.nodeV[sim.nodeV.length - 1][tIdx]; el.gLRead.textContent = '—'; el.vsRead.textContent = `${fmt(VS, 3)} V`; el.vlRead.textContent = `${fmt(VL, 3)} V`; el.derivedValues.innerHTML = '
MoC simulation mode — junction elements active
'; el.waveValues.innerHTML = ''; // Compute timeHorizon from sim timeHorizon = Math.min(sim.nSteps * sim.dt, tNorm + 1); timeHorizon = Math.max(timeHorizon, 2.2); // Probe — draw VS(t) from sim.nodeV[0] const d = resizeCanvasToCSS(el.probe); drawProbeSim(d.ctx, d.w, d.h, tNorm, sim, timeHorizon, theme); // Circuit — show shunt element symbols const c = resizeCanvasToCSS(el.circuit); drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, []); // Plot — V(z,t) from sim ensurePlotCanvasHeight(el.plot, 2); const p = resizeCanvasToCSS(el.plot); drawPlotSim(p.ctx, p.w, p.h, tIdx, sim, theme); } else { // ---- Bounce-series mode (pure resistive, no junction elements) ---- const waves = computeWaveParams(model); const bounce = buildBounceSeries(model, waves); const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); el.gLRead.textContent = fmt(waves.gL, 3); el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; updateDerivedDisplays(model, waves, bounce); const wavefronts = dyn.activeWaves.map((wf) => ({ z: wf.front, dir: wf.dir })); const d = resizeCanvasToCSS(el.probe); drawProbe(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); const c = resizeCanvasToCSS(el.circuit); drawCircuit(c.ctx, c.w, c.h, tNorm, theme, model.segments, model.terminal, junctions, wavefronts); ensurePlotCanvasHeight(el.plot, 2); const p = resizeCanvasToCSS(el.plot); drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); } } // ---- animation loop ---- function tick(ts) { if (!running) return; if (lastTS == null) lastTS = ts; const dt = (ts - lastTS) / 1000; lastTS = ts; tNorm += dt / Math.max(0.2, model.secPerTau); if (tNorm > timeHorizon) { running = false; lastTS = null; } render(); requestAnimationFrame(tick); } // ---- transport controls ---- function start() { hasStarted = true; tNorm = 0; running = true; lastTS = null; render(); requestAnimationFrame(tick); } function pause() { if (!hasStarted) return; if (running) { running = false; lastTS = null; render(); return; } if (tNorm >= timeHorizon - 1e-9) tNorm = 0; running = true; lastTS = null; render(); requestAnimationFrame(tick); } function reset() { running = false; hasStarted = false; lastTS = null; tNorm = 0; render(); } function rerender() { if (!running) render(); } // ---- event wiring ---- el.startBtn.addEventListener("click", start); el.pauseBtn.addEventListener("click", pause); el.resetBtn.addEventListener("click", reset); for (const inp of [el.Vg, el.Rg, el.secPerTau, el.reflectTol, el.riseTau, el.tauD]) { inp.addEventListener("input", rerender); inp.addEventListener("change", rerender); } el.segCount.addEventListener("input", () => { const n = Math.max(1, Math.min(10, parseInt(el.segCount.value) || 1)); el.segCount.value = n; buildSegmentInputs(n); rerender(); }); el.riseMode.addEventListener("change", () => { el.riseTau.disabled = el.riseMode.value === "step"; rerender(); }); window.addEventListener("resize", () => { theme = getTheme(); render(); }); // Initial render render(); })();