this repo has no description
0
fork

Configure Feed

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

Add exponential rise time, remove single-bounce mode

- Signal shape control: step (instantaneous) or exponential RC rise
- Rise time τ_r/τ_d input, enabled only in exponential mode
- physics.js: riseShape, waveVoltageAt, totalVoltageAt, sumEventsWithRise
- render.js: drawSampledWave for continuous V(z) curves when τ_r > 0
- Node voltages (VS, VL readouts) also use smooth rise in exponential mode
- Remove "show only one reflection" checkbox; Γ_S always active
- Animation now always plays through to end of bounce series and stops

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+126 -42
+20 -20
app.js
··· 15 15 pauseBtn: document.getElementById("pauseBtn"), 16 16 resetBtn: document.getElementById("resetBtn"), 17 17 scrubToggle: document.getElementById("scrubToggle"), 18 - singleBounce: document.getElementById("singleBounce"), 19 18 Vg: document.getElementById("Vg"), 20 19 Rg: document.getElementById("Rg"), 21 20 Z0: document.getElementById("Z0"), ··· 28 27 vlRead: document.getElementById("vlRead"), 29 28 derivedValues: document.getElementById("derivedValues"), 30 29 waveValues: document.getElementById("waveValues"), 30 + riseMode: document.getElementById("riseMode"), 31 + riseTau: document.getElementById("riseTau"), 31 32 }; 32 33 33 34 // ---- model (physics parameters) ---- 34 35 // All entries here correspond to user-adjustable controls. 35 - // riseTimeTau: placeholder for future finite-rise-time feature (0 = instantaneous step). 36 36 const model = { 37 37 Vg: 5, 38 38 Rg: 50, ··· 40 40 RL: 30, 41 41 secPerTau: 2.5, // animation speed: real seconds per τ_d 42 42 reflectTol: 1, // % of |V1| below which reflections are dropped 43 - singleBounce: true, // when true, treat Γ_S = 0 (suppress re-reflection at source) 44 - // riseTimeTau: 0, // future: RC rise time in units of τ_d 43 + riseTimeTau: 0, // 0 = hard step; > 0 = RC rise time in units of τ_d 45 44 }; 46 45 47 46 // ---- animation state ---- ··· 62 61 model.RL = parseFloat(el.RL.value); 63 62 model.secPerTau = parseFloat(el.secPerTau.value); 64 63 model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 65 - model.singleBounce = !!el.singleBounce.checked; 64 + model.riseTimeTau = el.riseMode.value === "exp" 65 + ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) 66 + : 0; 66 67 } 67 68 68 69 // ---- derived-value readout panel ---- ··· 71 72 const V2calc = waves.gL * waves.V1; 72 73 const V3calc = gSEffective * V2calc; 73 74 const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 74 - const v3Reason = model.singleBounce 75 - ? "suppressed because single-bounce mode forces Γ_S = 0" 76 - : (v3Suppressed 77 - ? `suppressed since |V_3| ≤ ε (ε=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V_1|)` 78 - : "nonzero, so re-reflection should appear"); 75 + const v3Reason = v3Suppressed 76 + ? `suppressed since |V_3| ≤ ε (ε=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V_1|)` 77 + : "nonzero, so re-reflection should appear"; 79 78 80 79 el.derivedValues.innerHTML = [ 81 80 `Γ_L = ${fmt(waves.gL, 6)}`, 82 - `Γ_S = ${fmt(gSEffective, 6)}${model.singleBounce ? " (forced by single-bounce mode)" : ""}`, 81 + `Γ_S = ${fmt(gSEffective, 6)}`, 83 82 `V_1 = ${fmt(waves.V1, 6)} V`, 84 83 `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V_1| = ${fmt(bounce.ampTol, 6)} V`, 85 84 `V_2 = Γ_L·V_1 = (${fmt(waves.gL, 6)})·(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, ··· 103 102 syncModelFromInputs(); 104 103 const waves = computeWaveParams(model); 105 104 const bounce = buildBounceSeries(model, waves); 106 - const dyn = computeDynamicState(tNorm, bounce); 105 + const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTau); 107 106 timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 108 107 109 108 el.tRead.textContent = fmt(tNorm, 3); ··· 122 121 123 122 ensurePlotCanvasHeight(el.plot, 2); 124 123 const p = resizeCanvasToCSS(el.plot); 125 - drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme); 124 + drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTau); 126 125 127 126 if (!hasStarted) { 128 127 el.pauseBtn.textContent = "Pause"; ··· 142 141 tNorm += dt / Math.max(0.2, model.secPerTau); 143 142 144 143 if (tNorm > timeHorizon) { 145 - if (model.singleBounce) { 146 - running = false; 147 - lastTS = null; 148 - } else { 149 - tNorm = 0; 150 - } 144 + running = false; 145 + lastTS = null; 151 146 } 152 147 153 148 render(); ··· 201 196 el.pauseBtn.addEventListener("click", pause); 202 197 el.resetBtn.addEventListener("click", reset); 203 198 204 - for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.reflectTol, el.singleBounce]) { 199 + for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.reflectTol, el.riseTau]) { 205 200 inp.addEventListener("input", () => { if (!running) render(); }); 206 201 inp.addEventListener("change", () => { if (!running) render(); }); 207 202 } 203 + 204 + el.riseMode.addEventListener("change", () => { 205 + el.riseTau.disabled = el.riseMode.value !== "exp"; 206 + if (!running) render(); 207 + }); 208 208 209 209 el.plot.addEventListener("mousemove", scrubFromEvent); 210 210 el.circuit.addEventListener("mousemove", scrubFromEvent);
+62 -11
physics.js
··· 6 6 // Time convention: tNorm is dimensionless time in units of τ_d (one-way delay). 7 7 // A wave launched at tNorm=launch reaches the far end at tNorm=launch+1. 8 8 // 9 - // Future extension points: 10 - // • Rise time (RC/finite bandwidth): model.riseTimeTau > 0 means each wave front has a 11 - // smooth leading edge V = V_final * (1 − exp(−Δt / riseTimeTau)) rather than a hard step. 12 - // Δt for a rightward wave at position z is (tNorm − launch − z); for leftward, (tNorm − launch − (1−z)). 13 - // When implemented, totalSegmentsForWaves() in render.js would be replaced by a sampled 14 - // continuous V(z) function evaluated per-pixel. 9 + // Extension points: 10 + // • Rise time (RC/finite bandwidth): implemented — see riseShape, waveVoltageAt, totalVoltageAt. 11 + // model.riseTimeTau = 0 gives a hard step; > 0 gives V = V_final*(1−exp(−Δt/τ_r)). 15 12 // 16 13 // • Multi-segment lines: replace single Z0 with an array of segments [{Z0, length}]. 17 14 // Each impedance boundary generates both a reflected wave (Γ) and a transmitted wave (T = 1+Γ). ··· 40 37 // gSEffective, tolPct, ampTol, tEnd 41 38 // } 42 39 function buildBounceSeries(model, waves) { 43 - const gS = model.singleBounce ? 0 : waves.gS; 40 + const gS = waves.gS; 44 41 const tolPct = Number.isFinite(model.reflectTol) ? model.reflectTol : 1; 45 42 const ampTol = Math.max(BOUNCE_EPS, Math.abs(waves.V1) * (tolPct / 100)); 46 43 ··· 87 84 }; 88 85 } 89 86 90 - // ---- time query: node voltage ---- 87 + // ---- time query: node voltage (step model) ---- 91 88 // Sum all voltage step events that have occurred by time tn. 92 89 function sumEventsAtTime(events, tn) { 93 90 const eps = 1e-9; ··· 98 95 return v; 99 96 } 100 97 98 + // ---- rise-time wave shape ---- 99 + // Fraction of final amplitude reached after time dt has elapsed since the wave front passed. 100 + // tau = 0 → hard step (returns 1 immediately). 101 + // tau > 0 → exponential approach: 1 − exp(−dt / tau). 102 + function riseShape(dt, tau) { 103 + if (dt <= 0) return 0; 104 + if (tau <= 0) return 1; 105 + return 1 - Math.exp(-dt / tau); 106 + } 107 + 108 + // Voltage contribution of a single wave wf at spatial position z. 109 + // wf must carry {dir, u, front, A} (fields added by computeDynamicState). 110 + function waveVoltageAt(wf, z, tau) { 111 + if (wf.dir > 0) { 112 + // Rightward wave: non-zero behind the front (z ≤ front). 113 + // Time since the front passed z: Δt = u − z (wave speed = 1 in normalised units). 114 + if (z > wf.front + 1e-9) return 0; 115 + return wf.A * riseShape(wf.u - z, tau); 116 + } else { 117 + // Leftward wave: non-zero behind the front (z ≥ front). 118 + // Front started at z=1; time since it passed z: Δt = u − (1 − z). 119 + if (z < wf.front - 1e-9) return 0; 120 + return wf.A * riseShape(wf.u - (1 - z), tau); 121 + } 122 + } 123 + 124 + // Sum of all wave contributions at position z. 125 + function totalVoltageAt(z, launchedWaves, tau) { 126 + let V = 0; 127 + for (const wf of launchedWaves) V += waveVoltageAt(wf, z, tau); 128 + return V; 129 + } 130 + 131 + // Node voltage as a smooth sum of exponential-rise events. 132 + // Each event contributes dV * riseShape(tn − t, tau) once tn ≥ t. 133 + // At tau = 0 this is identical to sumEventsAtTime (minus the ε guard, but 134 + // the wave fronts are always at t + 0+, so the eps edge-case never matters here). 135 + function sumEventsWithRise(events, tn, tau) { 136 + let v = 0; 137 + for (const e of events) { 138 + if (tn >= e.t) v += e.dV * riseShape(tn - e.t, tau); 139 + } 140 + return v; 141 + } 142 + 101 143 // ---- dynamic state at a given tNorm ---- 102 144 // Returns the set of waves currently propagating and the node voltages. 103 145 // Each wave in launchedWaves gets two extra fields: 104 146 // u: time elapsed since launch (tNorm − launch) 105 147 // front: current position of the leading edge, ∈ [0,1] 106 - function computeDynamicState(tn, bounce) { 148 + // riseTimeTau = 0 gives hard-step node voltages; > 0 gives smooth exponential rise. 149 + function computeDynamicState(tn, bounce, riseTimeTau = 0) { 107 150 const { clamp } = TLUtils; 108 151 const launchedWaves = []; 109 152 const activeWaves = []; // waves whose front is still in transit ··· 120 163 return { 121 164 launchedWaves, 122 165 activeWaves, 123 - VS: sumEventsAtTime(bounce.srcEvents, tn), 124 - VL: sumEventsAtTime(bounce.loadEvents, tn), 166 + VS: riseTimeTau > 0 167 + ? sumEventsWithRise(bounce.srcEvents, tn, riseTimeTau) 168 + : sumEventsAtTime(bounce.srcEvents, tn), 169 + VL: riseTimeTau > 0 170 + ? sumEventsWithRise(bounce.loadEvents, tn, riseTimeTau) 171 + : sumEventsAtTime(bounce.loadEvents, tn), 125 172 }; 126 173 } 127 174 ··· 131 178 computeWaveParams, 132 179 buildBounceSeries, 133 180 sumEventsAtTime, 181 + sumEventsWithRise, 182 + riseShape, 183 + waveVoltageAt, 184 + totalVoltageAt, 134 185 computeDynamicState, 135 186 }; 136 187 })();
+35 -8
render.js
··· 4 4 // All draw functions are pure with respect to app state; they receive 5 5 // everything they need as parameters (ctx, dimensions, dynamic state, theme). 6 6 // 7 - // Future extension points: 8 - // • drawPlot: when riseTimeTau > 0, replace piecewise-constant segment drawing 9 - // with a sampled continuous curve per wave (see physics.js comments). 7 + // Extension points: 8 + // • drawPlot: riseTimeTau > 0 now handled — uses sampled continuous curves via TLPhysics. 10 9 // • drawCircuit: for multi-segment lines, the single T-line box becomes N 11 10 // abutting boxes, each labelled with its own Z0. 12 11 ··· 248 247 return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 249 248 } 250 249 251 - // Draw a piecewise-constant voltage profile. 252 - // (When rise time is added, this becomes drawContinuousWave with sampled V(z).) 250 + // Draw a piecewise-constant voltage profile (step / riseTimeTau = 0 case). 253 251 function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 254 252 ctx.strokeStyle = color; 255 253 ctx.lineWidth = width; ··· 262 260 ctx.stroke(); 263 261 } 264 262 263 + // Draw a continuous voltage profile by sampling vFn(z) at N evenly-spaced z values. 264 + // Used for the riseTimeTau > 0 case; N=400 gives one sample per ~2 CSS pixels. 265 + function drawSampledWave(ctx, xOfZ, yOfV, vFn, color, width, N = 400) { 266 + ctx.strokeStyle = color; 267 + ctx.lineWidth = width; 268 + ctx.beginPath(); 269 + for (let i = 0; i <= N; i++) { 270 + const z = i / N; 271 + if (i === 0) ctx.moveTo(xOfZ(z), yOfV(vFn(z))); 272 + else ctx.lineTo(xOfZ(z), yOfV(vFn(z))); 273 + } 274 + ctx.stroke(); 275 + } 276 + 265 277 // ---- plot canvas ---- 266 278 // xPlot0/xPlot1 must match the T-line box extents in drawCircuit so that 267 279 // z=0 and z=ℓ align vertically between the two canvases. 268 - function drawPlot(ctx, w, h, tn, dyn, theme) { 280 + // riseTimeTau = 0 → piecewise-constant (hard step); > 0 → sampled exponential curves. 281 + function drawPlot(ctx, w, h, tn, dyn, theme, riseTimeTau = 0) { 269 282 ctx.clearRect(0, 0, w, h); 270 283 ctx.fillStyle = theme.panel; 271 284 ctx.fillRect(0, 0, w, h); ··· 278 291 279 292 const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 280 293 const shownWaves = launched.slice(0, 6); // limit component panel to 6 curves 294 + // Piecewise segments used for scale computation regardless of riseTimeTau 295 + // (they represent the asymptotic / fully-settled values, a safe upper bound). 281 296 const sumSegs = totalSegmentsForWaves(launched); 282 297 283 298 // Shared vertical scale: symmetric, padded, fixed for the current parameter set. ··· 321 336 322 337 const panelH = PLOT_PANEL_H; 323 338 339 + const { waveVoltageAt, totalVoltageAt } = TLPhysics; 340 + const smooth = riseTimeTau > 0; 341 + 324 342 // Panel 1 — sum of all waves 325 343 const top0 = PLOT_PAD_T; 326 344 const bot0 = top0 + panelH; 327 345 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 328 346 drawPanelFrame(top0, bot0, y0, "Sum (all waves)"); 329 - drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 347 + if (smooth) { 348 + drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTau), theme.ok, 2.4); 349 + } else { 350 + drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 351 + } 330 352 drawFrontMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front)); 331 353 332 354 // Panel 2 — individual component waves ··· 346 368 ]; 347 369 348 370 for (let i = 0; i < shownWaves.length; i++) { 371 + const wf = shownWaves[i]; 349 372 const style = waveStyles[i]; 350 373 ctx.setLineDash(style.dash); 351 - drawPiecewise(ctx, xOfZ, y1, segmentsForWave(shownWaves[i]), style.color, 2.0); 374 + if (smooth) { 375 + drawSampledWave(ctx, xOfZ, y1, (z) => waveVoltageAt(wf, z, riseTimeTau), style.color, 2.0); 376 + } else { 377 + drawPiecewise(ctx, xOfZ, y1, segmentsForWave(wf), style.color, 2.0); 378 + } 352 379 ctx.setLineDash([]); 353 380 } 354 381 drawFrontMarkers(top1, bot1, shownWaves.filter((wf) => wf.u < 1).map((wf) => wf.front));
+1 -1
style.css
··· 30 30 .controls .readouts { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } 31 31 .transport { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; } 32 32 label { font-size: 12px; color: var(--muted); display: grid; gap: 6px; } 33 - input[type="number"], input[type="range"] { 33 + input[type="number"], input[type="range"], select { 34 34 width: 100%; 35 35 background: #0c1420; color: var(--ink); 36 36 border: 1px solid #24364e; border-radius: 10px;
+8 -2
tline_viz.html
··· 34 34 <label>Reflection termination tolerance (% of |V<sub>1</sub>|) 35 35 <input id="reflectTol" type="number" min="0" step="0.1" value="1" /> 36 36 </label> 37 - <label>Show only one reflection (set \(\Gamma_S=0\)) 38 - <input id="singleBounce" type="checkbox" checked /> 37 + <label>Signal shape 38 + <select id="riseMode"> 39 + <option value="step">Step (instantaneous)</option> 40 + <option value="exp">Exponential rise (RC)</option> 41 + </select> 42 + </label> 43 + <label>Rise time constant τ<sub>r</sub> / τ<sub>d</sub> 44 + <input id="riseTau" type="number" min="0.01" max="5" step="0.01" value="0.1" disabled /> 39 45 </label> 40 46 </div> 41 47 </div>