this repo has no description
0
fork

Configure Feed

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

Fix voltage spike at segment boundaries in smooth plot mode

In smooth (rise-time) mode, drawSampledWave samples totalVoltageAt at
evenly-spaced z values including exact boundary points (e.g. z=0.5 for
N=2 segments). At those points both the ending wave of the left segment
and the starting wave of the right segment passed the ±1e-9 range checks
in waveVoltageAt, doubling the displayed voltage — a visible spike.

Fix: add segmentForZ(z, N) which maps each sample to its owning segment,
and filter launchedWaves in totalVoltageAt to only sum waves whose segIdx
matches. The +1e-9 nudge in segmentForZ handles floating-point imprecision
when z lands exactly on a boundary (e.g. 200/400 = 0.5 exactly).

The bug was absent in step mode because totalSegmentsForWaves uses midpoints
between breakpoints, never probing the exact boundary z. It was also subtle
with 3 segments (boundary at 1/3) since 1/3·400 is never an integer.

Adds two regression tests that directly catch the spike.

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

+83 -3
+18 -2
physics.js
··· 216 216 } 217 217 } 218 218 219 + // Map z ∈ [0,1] to the index of the segment that owns it. 220 + // Boundaries are assigned to the right-hand (higher-index) segment; z=1 goes to the last. 221 + // The +1e-9 nudge handles floating-point imprecision when z is exactly i/N. 222 + function segmentForZ(z, N) { 223 + if (z >= 1 - 1e-9) return N - 1; 224 + return Math.min(N - 1, Math.floor(z * N + 1e-9)); 225 + } 226 + 219 227 // Sum of all wave contributions at position z. 220 - function totalVoltageAt(z, launchedWaves, tau, tr = 0) { 228 + // For multi-segment models (N > 1), only waves whose segIdx matches the segment 229 + // that owns z are summed. This prevents double-counting at segment boundaries 230 + // where adjacent waves share a z value (a plotting artifact, not a physics error). 231 + function totalVoltageAt(z, launchedWaves, tau, tr = 0, N = 1) { 232 + const segIdx = N > 1 ? segmentForZ(z, N) : 0; 221 233 let V = 0; 222 - for (const wf of launchedWaves) V += waveVoltageAt(wf, z, tau, tr); 234 + for (const wf of launchedWaves) { 235 + if (N > 1 && wf.segIdx !== segIdx) continue; 236 + V += waveVoltageAt(wf, z, tau, tr); 237 + } 223 238 return V; 224 239 } 225 240 ··· 295 310 riseShapeLinear, 296 311 waveVoltageAt, 297 312 totalVoltageAt, 313 + segmentForZ, 298 314 computeDynamicState, 299 315 }; 300 316 })();
+64
physics.test.js
··· 23 23 sumEventsWithLinearRamp, 24 24 riseShape, 25 25 riseShapeLinear, 26 + totalVoltageAt, 27 + computeDynamicState, 26 28 } = globalThis.TLPhysics; 27 29 28 30 // Helper: assert two numbers are within `tol` of each other. ··· 240 242 // PULSE( 0 1 0 100p 100p 1u 2u ) → linear ramp, TR = 100ps = 0.1·τ_d 241 243 // 242 244 // TSV columns: t[s] v(a) t[s] v(b) 245 + // ──────────────────────────────────────────────────────────────────────────── 246 + 247 + // ──────────────────────────────────────────────────────────────────────────── 248 + // totalVoltageAt: no spike at segment boundaries (multi-segment plotting bug) 249 + // 250 + // In smooth mode, drawSampledWave evaluates totalVoltageAt at exact boundary 251 + // z values (e.g. z=0.5 for 2 segments). Without segment filtering, the parent 252 + // wave (dir=+1, zEnd=boundary) and the transmitted wave (dir=+1, zStart=boundary) 253 + // both pass the range check, doubling the voltage at the boundary. 254 + // ──────────────────────────────────────────────────────────────────────────── 255 + 256 + test("totalVoltageAt: no spike at segment boundary (2 segments, step mode)", () => { 257 + // Z0=40 → Z0=60, Rg=50, RL=∞. 258 + // Γ_bound = (60-40)/(60+40) = 0.2. V1 = 1 * 40/(50+40) = 4/9. 259 + // At tNorm=0.6 the boundary crossing (tNorm=0.5) is complete: 260 + // seg-0 has incident(4/9) + reflected(0.2·4/9), seg-1 has transmitted(1.2·4/9). 261 + // Correct V at z=0.5 = (1+0.2)·(4/9) = 4.8/9 = 8/15. 262 + // Buggy code (no segIdx filter) would give 2·(8/15) = 16/15 — a visible spike. 263 + const model = { Vg: 1, Rg: 50, RL: Infinity, 264 + segments: [{ Z0: 40 }, { Z0: 60 }], reflectTol: 0.001 }; 265 + const waves = computeWaveParams(model); 266 + const bounce = buildBounceSeries(model, waves); 267 + const dyn = computeDynamicState(0.6, bounce, 0, 0); 268 + const N = model.segments.length; 269 + const eps = 1e-4; 270 + 271 + const vL = totalVoltageAt(0.5 - eps, dyn.launchedWaves, 0, 0, N); 272 + const vB = totalVoltageAt(0.5, dyn.launchedWaves, 0, 0, N); 273 + const vR = totalVoltageAt(0.5 + eps, dyn.launchedWaves, 0, 0, N); 274 + 275 + // Correct value: (1 + 0.2) * (4/9) = 8/15 276 + near(vB, 8 / 15, "V at boundary = (1+Γ)·V1", 1e-6); 277 + near(vB, vL, "no spike: V(boundary) ≈ V(boundary-ε)", 1e-6); 278 + near(vB, vR, "no spike: V(boundary) ≈ V(boundary+ε)", 1e-6); 279 + }); 280 + 281 + test("totalVoltageAt: no spike at boundaries with 4 segments", () => { 282 + // 4 equal segments: boundaries at 0.25, 0.5, 0.75. 283 + // tNorm=0.3 puts the front in seg 1; all three boundary z-values should be spike-free. 284 + const model = { Vg: 1, Rg: 50, RL: 100, 285 + segments: [{ Z0: 50 }, { Z0: 75 }, { Z0: 50 }, { Z0: 75 }], 286 + reflectTol: 0.001 }; 287 + const waves = computeWaveParams(model); 288 + const bounce = buildBounceSeries(model, waves); 289 + const N = model.segments.length; 290 + const eps = 1e-4; 291 + 292 + for (const tNorm of [0.3, 0.6, 1.2, 2.0]) { 293 + const dyn = computeDynamicState(tNorm, bounce, 0, 0); 294 + for (const zB of [0.25, 0.5, 0.75]) { 295 + const vL = totalVoltageAt(zB - eps, dyn.launchedWaves, 0, 0, N); 296 + const vB = totalVoltageAt(zB, dyn.launchedWaves, 0, 0, N); 297 + const vR = totalVoltageAt(zB + eps, dyn.launchedWaves, 0, 0, N); 298 + // No spike: boundary value must lie between its two neighbours (within tolerance). 299 + const lo = Math.min(vL, vR) - 1e-6; 300 + const hi = Math.max(vL, vR) + 1e-6; 301 + assert.ok(vB >= lo && vB <= hi, 302 + `spike at z=${zB}, tNorm=${tNorm}: V=${vB}, neighbours ${vL}..${vR}`); 303 + } 304 + } 305 + }); 306 + 243 307 // ──────────────────────────────────────────────────────────────────────────── 244 308 245 309 test("SPICE comparison: linear ramp, open-circuit load (tline-oc.sp)", () => {
+1 -1
render.js
··· 439 439 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 440 440 drawPanelFrame(top0, bot0, y0, "Sum (all waves)", vLo, vHi); 441 441 if (smooth) { 442 - drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTau, riseTimeTr), theme.ok, 2.4); 442 + drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTau, riseTimeTr, segments.length), theme.ok, 2.4); 443 443 } else { 444 444 drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 445 445 }