app
Adom Fields
Public Made by Adomby adom
Adom Fields: where the current and the heat go on your board.
main
John Lauer
adom-fields 0.7.16: the community's issues. #10 plated pads only on real copper, stackup from the board; #11 six-layer…
ee2efa2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
// Adom Fields: noise couplings. Two findings from the solve's own review (fields.json review.noise and
// noise.json for the mode shapes), drawn on the board:
// - the plane cavities: a power pour over the ground plane rings like a drum at its own frequencies; its
// real lowest modes flex in 3D, each tagged with its frequency against the nearest strong tone;
// - crosstalk: the switching copper pulses, arcs jump to each quiet net at its closest approach, the victim's
// copper glows by the millivolts it picks up per edge against the budget.
export async function attach(ctx) {
const { BAB, scene, V, camera, state, LAYERS, frameDt, root, flow } = ctx;
if (!flow || !flow.data) return null;
const { lay, real, W, H, d, meta, nets, zOf, bx, by } = flow.data;
const rv = (state.fields.review || {}).noise || { cavities: [], crosstalk: [] };
let shapes = { cavities: [] }; try { shapes = await fetch('noise.json').then(r => r.ok ? r.json() : { cavities: [] }); } catch (e) { void e; }
// the solve's own budget (spec crosstalkMv), and each victim's (victimBudgetsMv) (issue 9)
const budget = rv.budgetMv != null ? rv.budgetMv : 50;
const vb = (x) => x.budgetMv != null ? x.budgetMv : budget;
const amt = (mv) => mv >= 1 ? mv.toFixed(0) + ' mV' : mv >= 0.001 ? (mv * 1000).toFixed(0) + ' µV' : (mv * 1e6).toFixed(0) + ' nV';
const sw = (state.fields.nets || []).some(n => n.switching) || Object.keys(((state.fields.review || {}).converter) || {}).length > 0;
const plan = rv.frequencyPlan || [], paths = rv.circuitPaths || [];
const group = new BAB.TransformNode('fields-noise', scene); group.parent = root; group.setEnabled(false);
// ---- the drum: the largest cavity's modes as a flexing surface --------------------------------------
const cav = (rv.cavities || []).slice().sort((a, b) => b.areaMm2 - a.areaMm2)[0];
const cs = cav && shapes.cavities.find(c => c.net === cav.net);
let drum = null;
if (cav && cs && cs.shapes.length) {
const g = cs.grid, cw = g.cols, ch = g.rows, hc = g.cellMm;
const pos = new Float32Array(cw * ch * 3), col = new Float32Array(cw * ch * 4), ind = [];
for (let r = 0; r < ch; r++) for (let c = 0; c < cw; c++) { const v = r * cw + c; pos[v * 3] = bx(g.x0 + (c + 0.5) * hc); pos[v * 3 + 1] = by(g.y0 + (r + 0.5) * hc); pos[v * 3 + 2] = 0; }
for (let r = 0; r < ch - 1; r++) for (let c = 0; c < cw - 1; c++) { const a = r * cw + c; if (g.mask[a] && g.mask[a + 1] && g.mask[a + cw] && g.mask[a + cw + 1]) ind.push(a, a + cw, a + 1, a + 1, a + cw, a + cw + 1); }
const mesh = new BAB.Mesh('noise-drum', scene); const vd = new BAB.VertexData(); vd.positions = pos; vd.indices = ind; vd.colors = col; vd.applyToMesh(mesh, true);
const m = new BAB.StandardMaterial('noise-drum-m', scene); m.disableLighting = true; m.emissiveColor = new BAB.Color3(1, 1, 1); m.backFaceCulling = false; m.alpha = 0.85; mesh.useVertexColors = true; m.diffuseColor = new BAB.Color3(0, 0, 0);
mesh.material = m; mesh.hasVertexAlpha = true; mesh.parent = group; mesh.renderingGroupId = 2; mesh.isPickable = false;
drum = { mesh, mat: m, pos, col, g, amp: Math.min(3.5, 0.2 * Math.max(cw, ch) * hc) };
}
// ---- crosstalk: victim copper glow, arcs at the closest approach --------------------------------------
const xt = (rv.crosstalk || []).filter(x => x.coupledMv > vb(x) / 10);
const vtex = new Uint8Array(W * H * 4); const k0 = real[0];
const idOf = (name) => nets.findIndex(n => n.name === name);
const aggIds = new Set(xt.map(x => idOf(x.aggressor)));
for (const x of xt) { const id = idOf(x.victim); const t = Math.min(1, x.coupledMv / vb(x)); for (const k of real) for (let i = 0; i < W * H; i++) if (lay[k].net[i] === id) { const o = i * 4; vtex[o] = 255; vtex[o + 1] = Math.round(200 - 140 * t); vtex[o + 2] = 40; vtex[o + 3] = Math.round(90 + 165 * t); } }
const atex = new Uint8Array(W * H * 4);
for (const k of real) for (let i = 0; i < W * H; i++) if (aggIds.has(lay[k].net[i])) { const o = i * 4; atex[o] = 255; atex[o + 1] = 90; atex[o + 2] = 220; atex[o + 3] = 200; }
const plane = (name, data) => { const t = new BAB.RawTexture(data, W, H, BAB.Constants.TEXTUREFORMAT_RGBA, scene, false, false, BAB.Texture.NEAREST_SAMPLINGMODE, BAB.Constants.TEXTURETYPE_UNSIGNED_BYTE); const p = BAB.MeshBuilder.CreatePlane(name, { width: W * d, height: H * d, sideOrientation: BAB.Mesh.DOUBLESIDE }, scene); const uvs = p.getVerticesData(BAB.VertexBuffer.UVKind); for (let i = 1; i < uvs.length; i += 2) uvs[i] = 1 - uvs[i]; p.setVerticesData(BAB.VertexBuffer.UVKind, uvs); p.position.x = bx(meta.x0 + W * d / 2); p.position.y = by(meta.y0 + H * d / 2); const m = new BAB.StandardMaterial(name + '-m', scene); m.diffuseTexture = t; t.hasAlpha = true; m.useAlphaFromDiffuseTexture = true; m.emissiveColor = new BAB.Color3(1, 1, 1); m.disableLighting = true; m.backFaceCulling = false; p.material = m; p.parent = group; p.renderingGroupId = 2; p.isPickable = false; return { p, m }; };
const victims = plane('noise-victims', vtex), aggressors = plane('noise-aggressors', atex);
const arcs = [];
for (const x of xt) {
// from the nearest aggressor copper to the victim's closest point: a spark arc
const aid = idOf(x.aggressor); let best = null, bd = Infinity; const ci = Math.floor((x.at[1] - meta.y0) / d) * W + Math.floor((x.at[0] - meta.x0) / d);
const r0 = Math.floor(ci / W), c0 = ci % W, R = Math.ceil(2 / d);
for (let dr = -R; dr <= R; dr++) for (let dc = -R; dc <= R; dc++) { const r = r0 + dr, c = c0 + dc; if (r < 0 || c < 0 || r >= H || c >= W) continue; if (lay[k0].net[r * W + c] === aid) { const dd = dr * dr + dc * dc; if (dd < bd) { bd = dd; best = [meta.x0 + (c + 0.5) * d, meta.y0 + (r + 0.5) * d]; } } }
if (!best) continue;
const pts = []; for (let a = 0; a <= 12; a++) { const t = a / 12; pts.push(bx(best[0] + (x.at[0] - best[0]) * t), by(best[1] + (x.at[1] - best[1]) * t), 0.15 + 0.8 * Math.sin(Math.PI * t)); }
try { const m = BAB.CreateGreasedLine('noise-arc-' + x.victim, { points: pts }, { color: new BAB.Color3(1, 0.85, 0.3), width: 0.14, useDash: true, dashCount: 8, dashRatio: 0.5, sizeAttenuation: true }, scene); m.parent = group; m.renderingGroupId = 2; m.isPickable = false; arcs.push(m); } catch (e) { void e; }
}
// tags
const host = document.getElementById('labels'), tags = [];
const tag = (html, x, y, z) => { const el = document.createElement('div'); el.className = 'lbl noise'; el.innerHTML = html; el.hidden = true; host.appendChild(el); tags.push({ el, x, y, z }); };
for (const x of xt.slice(0, 5)) tag(`<b>${x.victim}</b> ← ${x.aggressor} <span class="${x.coupledMv > vb(x) ? 'v' : 'g'}">${amt(x.coupledMv)}</span> · ${x.closestMm} mm`, x.at[0], x.at[1], 1.3);
const modeTag = { el: null };
if (cav) { const el = document.createElement('div'); el.className = 'lbl noise'; el.hidden = true; host.appendChild(el); modeTag.el = el; }
// ---- the panel -------------------------------------------------------------------------------------------
const ghz = (h) => h >= 1e9 ? (h / 1e9).toFixed(2) + ' GHz' : (h / 1e6).toFixed(0) + ' MHz';
const tones = rv.tones || [], topTone = tones.length ? Math.max(...tones.map(t => t.fHz)) : 0;
const panel = document.createElement('div'); panel.className = 'cyclemeter noisepanel'; panel.hidden = true; document.querySelector('.stage').appendChild(panel);
panel.innerHTML = `<div class="ch" title="Click to fold">Noise couplings</div><div class="body">
<div class="row" style="color:var(--text-2)">Plane cavities (lowest mode)</div>
${(rv.cavities || []).map(c => `<div class="row"><span class="dot" style="background:${c.modes.some(m => m.near) ? '#ff5a4a' : '#5cf08f'}"></span>${c.net} over ${c.over}<i>${c.modes[0] ? ghz(c.modes[0].fHz) : '?'}${c.modes.some(m => m.near) ? ' · rings' : ''}</i></div>`).join('') || '<div class="row">none</div>'}
<div class="row" style="color:var(--text-2)">Strong tones reach ${topTone ? ghz(topTone) : '—'}</div>
<div class="row" style="color:var(--text-2);margin-top:6px">Crosstalk${sw ? ' per edge' : ''} (budget ${amt(budget)})</div>
${xt.slice(0, 4).map(x => `<div class="row"><span class="dot" style="background:${x.coupledMv > vb(x) ? '#ff5a4a' : '#5cf08f'}"></span>${x.victim} ← ${x.aggressor}<i>${amt(x.coupledMv)}</i></div>`).join('') || `<div class="row">no quiet net near ${sw ? 'a switching node' : 'a clock or oscillator'} over a tenth of its budget</div>`}
${plan.length ? `<div class="row" style="color:var(--text-2);margin-top:6px">Frequency plan: tones in a receive band</div>` + plan.slice(0, 5).map(p => `<div class="row"><span class="dot" style="background:${p.harmonic <= 5 ? '#ff5a4a' : '#ffc04d'}"></span>${p.tone.split('/').pop()} ×${p.harmonic}<i>${p.band} ${p.where === 'band' ? '' : 'image'}</i></div>`).join('') : ''}
${paths.length ? `<div class="row" style="color:var(--text-2);margin-top:6px">Circuit paths to sensitive nets</div>` + paths.map(p => `<div class="row"><span class="dot" style="background:#ffc04d"></span>${p.from.split('/').pop()} → ${p.to}<i>${p.hops.length} parts</i></div>`).join('') : ''}
</div>`;
panel.addEventListener('click', (e) => { if (e.target.closest('.ch')) panel.classList.toggle('folded'); });
// ---- frame ------------------------------------------------------------------------------------------------
const knob = { on: 0, to: 0, t: 0 };
scene.onBeforeRenderObservable.add(() => {
const dt = frameDt();
knob.on += (knob.to - knob.on) * (1 - Math.exp(-dt * 2.5)); if (Math.abs(knob.to - knob.on) < 1e-3) knob.on = knob.to;
const live = knob.on > 0.01; group.setEnabled(live); group.position.z = zOf(k0) + 0.1;
if (!live) { for (const t of tags) t.el.hidden = true; if (modeTag.el) modeTag.el.hidden = true; return; }
knob.t += dt * (+state.view.speed || 1);
// the aggressor pulses with the switching, the victims glow with it
const pulse = 0.5 + 0.5 * Math.tanh(4 * Math.sin(2 * Math.PI * 0.4 * knob.t));
aggressors.m.alpha = knob.on * (0.25 + 0.75 * pulse); victims.m.alpha = knob.on * (0.3 + 0.7 * pulse);
for (const a of arcs) { a.setEnabled(pulse > 0.5); if (a.greasedLineMaterial) a.greasedLineMaterial.dashOffset = -knob.t * 0.6; }
// the drum: each mode for six seconds, flexing
if (drum) {
const shp = cs.shapes, mi = Math.floor(knob.t / 6) % shp.length, s = shp[mi], ph = Math.sin(2 * Math.PI * 0.8 * knob.t), g = drum.g;
for (let i = 0; i < s.length; i++) { const v = s[i] * ph; drum.pos[i * 3 + 2] = drum.amp * 0.6 + v * drum.amp * 0.5; const t = Math.abs(v); // the two phases red and blue, the nodal lines (where the plate stays still) white
const sv = Math.abs(s[i]); if (sv < 0.06) { drum.col[i * 4] = 1; drum.col[i * 4 + 1] = 1; drum.col[i * 4 + 2] = 1; drum.col[i * 4 + 3] = g.mask[i] ? 0.95 : 0; continue; }
if (v >= 0) { drum.col[i * 4] = 1; drum.col[i * 4 + 1] = 0.25 * (1 - t); drum.col[i * 4 + 2] = 0.3 * (1 - t); } else { drum.col[i * 4] = 0.15 * (1 - t); drum.col[i * 4 + 1] = 0.45 * (1 - t) + 0.3; drum.col[i * 4 + 2] = 1; } drum.col[i * 4 + 3] = g.mask[i] ? 0.55 + 0.45 * sv : 0; }
drum.mesh.updateVerticesData(BAB.VertexBuffer.PositionKind, drum.pos); drum.mesh.updateVerticesData(BAB.VertexBuffer.ColorKind, drum.col); drum.mat.alpha = knob.on * 0.9;
if (modeTag.el) { const md = cav.modes[mi]; modeTag.el.innerHTML = md ? `<b>${cav.net} over ${cav.over}</b> mode ${mi + 1}: <span class="${md.near ? 'v' : 'g'}">${ghz(md.fHz)}</span>${md.near ? ' near the ' + md.near : ''}` : ''; modeTag._p = md ? md.antinode : null; }
}
const e = V.getEngine(), w = e.getRenderWidth(), h = e.getRenderHeight(), hostEl = document.getElementById('host'), wm = group.getWorldMatrix();
const place = (el, x, y, z) => { const p = BAB.Vector3.Project(BAB.Vector3.TransformCoordinates(new BAB.Vector3(bx(x), by(y), z), wm), BAB.Matrix.Identity(), scene.getTransformMatrix(), camera.viewport.toGlobal(w, h)); el.hidden = p.z > 1 || p.z < 0; el.style.left = (p.x / (w / hostEl.clientWidth)) + 'px'; el.style.top = (p.y / (h / hostEl.clientHeight)) + 'px'; };
for (const t of tags) place(t.el, t.x, t.y, t.z);
if (modeTag.el && modeTag._p) place(modeTag.el, modeTag._p[0], modeTag._p[1], (drum ? drum.amp : 1) + 0.8);
});
function apply(view) {
knob.to = view.quantity === 'noise' ? 1 : 0; panel.hidden = view.quantity !== 'noise';
if (view.quantity !== 'noise') return;
for (const Lname of LAYERS) { const m = ctx.layerMeshes[Lname]; if (m && m.material) m.material.alpha = Lname === 'F.Cu' ? 0.35 : 0; }
const leg = document.getElementById('legend'); if (leg) leg.hidden = true;
}
function describe() {
return [`<b>Noise couplings</b>: where ${sw ? 'switching' : 'clock and oscillator'} noise rings and spreads`,
'<b>The flexing sheet</b> = a power pour over the ground plane ringing like a drum: its real lowest modes, red and blue the two phases',
`<b>Pink copper</b> = ${sw ? 'the switching node, pulsing' : 'the clocks and oscillators'}; <b>yellow to red copper</b> = quiet nets picking it up, brighter nearer their budget`,
'<b>Arcs</b> = where the coupling is closest',
'A mode near a strong harmonic, a quiet net over its budget (' + amt(budget) + ' unless set per net), or a tone in a receive band is in the fix plan; parallel-plate and side-coupling estimates'];
}
function narration() {
const c0 = (rv.cavities || []).slice().sort((a, b) => b.areaMm2 - a.areaMm2)[0], x0 = xt[0];
const src = sw ? 'the switch node' : 'the clocks and oscillators';
const tone = sw ? "this converter's harmonics" : 'the strongest harmonics of its clocks and oscillators';
const say = (mv) => mv >= 1 ? `${Math.round(mv)} millivolts` : `${Math.round(mv * 1000)} microvolts`;
let t = `This is the Noise view: where ${sw ? 'switching' : 'clock and oscillator'} noise can ring and spread. `;
if (c0) t += `Any power pour sitting over a ground plane is a cavity, like a drum skin, and it rings at its own frequencies. The flexing sheet is the ${c0.net} pour on ${c0.layer || 'its layer'}, ringing in its real lowest modes. Its first mode is at ${c0.modes[0] ? (c0.modes[0].fHz / 1e6).toFixed(0) + ' megahertz' : 'an unknown frequency'}, ${c0.modes.some(m => m.near) ? 'and one of its modes lines up with a strong tone, so it is in the fix plan, with a small capacitor where it rings loudest.' : `away from ${tone}, so it will not ring.`} `;
if (plan.length) t += `The frequency plan finds ${plan.length} harmonic${plan.length > 1 ? 's' : ''} landing in a receive band or its image, for example harmonic ${plan[0].harmonic} of ${plan[0].tone.split('/').pop()} in the ${plan[0].band} ${plan[0].where}. `;
if (paths.length) t += `There ${paths.length > 1 ? 'are' : 'is'} ${paths.length} path${paths.length > 1 ? 's' : ''} through the circuit from an oscillator to a sensitive net, such as ${paths[0].from.split('/').pop()} to ${paths[0].to}; the fix plan asks for a simulation of each. `;
t += x0 ? `${sw ? 'The pink copper is the switch node, pulsing with every edge.' : 'The pink copper is the clocks and oscillators.'} The quiet nets near ${src} pick up a little through stray capacitance: ${x0.victim} gets about ${say(x0.coupledMv)}, ${x0.coupledMv > vb(x0) ? 'over its budget, so the fix plan reroutes it away or adds a ground guard.' : 'under its budget of ' + say(vb(x0)) + '.'}` : `No quiet net near ${src} picks up enough to matter.`;
return t + ' These are estimates, a screen for what to look at, and every finding goes into the fix plan for the AI.';
}
console.log(`noise: ${(rv.cavities || []).length} cavities${cav ? `, drum ${cav.net} ${cs ? cs.shapes.length : 0} modes` : ''}; crosstalk ${xt.map(x => x.victim + '←' + x.aggressor + ' ' + amt(x.coupledMv)).join(', ')}`);
return { apply, describe, narration };
}