project
Adom Custom Electronic Load
Public Made by Adomby adom
Two-layer digitally controlled electronic-load prototype. Compact R11 is under active electrical, cooling and probe-access review; not a manufacturing release.
← Commit history
Record unclipped current failures, Kelvin-return diagnostic and AIFlow 0.1.80
8 files changed
+476−1
design/aiflow-unclipped-current-release.jsonadded+22@@ -0,0 +1,22 @@+{+ "version": "adom-aiflow 0.1.80",+ "publishedRegistry": {+ "published": "adom/adom-aiflow",+ "version": "0.1.80",+ "sha256": "sha256-afecff2ceaffc2c4762a3b7f9254d05fea2ef746d95cf79f2abca1ae2febc5d2",+ "visibility": "public",+ "url": "/adom/adom-aiflow",+ "install": "adom-wiki pkg install adom/adom-aiflow"+ },+ "installedBinarySha256": "b394986dd78ea876d573a47847de17005b6dc285881eaf2ec0e3638b2f7721c5",+ "workspaceTestsPassed": 128,+ "commandsPreserved": 57,+ "continuationGuard": "passed actual CLI workflow",+ "dashboardLifecycle": "passed actual CLI/server workflow",+ "acceptanceChecks": [+ "Legacy clipped report rejected",+ "Fresh unclipped saved-board report rejected on actual GND density violation"+ ],+ "ownerDependency": "Fields PR7 stacked on6 and5; not merged or installed",+ "liveBoardCurrentGate": "failed"+}
design/spec-kelvin-return-diagnostic.jsonadded+61@@ -0,0 +1,61 @@+{+ "board": "Kelvin return diagnostic only",+ "copperUm": 35,+ "stackupMm": [+ 1.53+ ],+ "dcMaxIterations": 60000,+ "loads": {+ "SHUNT_N_SENSE": {+ "amps": 0.006400819039631379,+ "terminals": [+ {+ "reference": "U2",+ "pad": "4",+ "amps": 0.002+ },+ {+ "reference": "U3",+ "pad": "2",+ "amps": 0.0018+ },+ {+ "reference": "U4",+ "pad": "7",+ "amps": 0.00044891566265060245+ },+ {+ "reference": "U7",+ "pad": "12",+ "amps": 0.0016+ },+ {+ "reference": "R10",+ "pad": "2",+ "amps": 6.313131313131313e-05+ },+ {+ "reference": "R19",+ "pad": "2",+ "amps": 0.00018796992481203009+ },+ {+ "reference": "R23",+ "pad": "2",+ "amps": 7.352941176470588e-05+ },+ {+ "reference": "R28",+ "pad": "2",+ "amps": 0.00022727272727272727+ },+ {+ "reference": "RSH1",+ "pad": "3",+ "amps": -0.006400819039631379+ }+ ]+ }+ },+ "hot": {}+}
design/step12-kelvin-field-summary.jsonadded+63@@ -0,0 +1,63 @@+{+ "formalAcceptance": false,+ "kind": "copper-field-kelvin-return-voltage-diagnostic",+ "inputIdentity": {+ "algorithm": "sha256",+ "board": "54f1aab0a34622d1c3675c13f4aeb7a678b0cee8b5a4d3bfd7484b10edb8dcf6",+ "solverVersion": "0.6.2",+ "spec": "c5d54ee5bfd4a7b6612f4a132a48dacc0abb34656e0241bafee9205b80910202"+ },+ "solverSha256": "17013baa2ba700870588f6be40434d08c0bfe24e9c08a4bc6f666f487a5934c5",+ "source": "Isolated diagnostic fork of Fields PR6, logging 1 mm neighborhood means before display clipping",+ "result": {+ "amps": 0.006400819039631379,+ "copperMm2": 57.3,+ "iterations": 1274,+ "jouleW": 0.0,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "SHUNT_N_SENSE",+ "peakAt": [+ 138.7,+ 82.69999999999999+ ],+ "peakJ": 1.3,+ "peakLayer": "B.Cu",+ "platedPads": 0,+ "residual": 7.721561438987511e-08,+ "vias": 25+ },+ "terminalVoltsRelativeToRshuntSense": {+ "RSH1.3": 0.0,+ "U3.2": 0.0001849279868472694,+ "R23.2": 0.0004612029987333694,+ "C1.2": 0.0004251957830863694,+ "R10.2": 0.0003304463472912694,+ "TP13.1": 0.0005694939323354694,+ "R19.2": 0.0004612579842573694,+ "R28.2": 0.0004556460655607694,+ "U2.4": 0.0004658550608479694,+ "C3.2": 5.61669364582694e-05,+ "C10.2": 0.0004490703301381694,+ "C17.2": 0.0006740318129704693,+ "C14.2": 7.97623189628394e-05,+ "C12.2": 0.0004704310832547694,+ "U4.7": 9.20520947530794e-05,+ "U4.9": 0.0001173410680877694,+ "C13.2": 0.0001656844856938694,+ "U7.12": 0.0007183723656561693+ },+ "ina226EquivalentCurrentOffsetA": 0.00586705340438847,+ "servoSetpointEquivalentOffsetA": 0.016693287962488597,+ "limits": [+ "Nominal 35 um copper and 25 um barrels, no temperature feedback",+ "Maximum listed static IC supply currents under differing datasheet conditions; not a measured operating point",+ "Comparator sink, dynamic gate current, startup and switching omitted",+ "1 mm neighborhood mean is not an exact pad terminal voltage",+ "RSH1 internal sense-to-power terminal parasitics excluded",+ "Reported peakJ is a clipped display statistic, not acceptance maximum",+ "No precision guarantee or calibration qualification"+ ]+}
design/step12-unclipped-peaks.jsonadded+181@@ -0,0 +1,181 @@+{+ "formalAcceptance": false,+ "inputIdentity": {+ "algorithm": "sha256",+ "board": "54f1aab0a34622d1c3675c13f4aeb7a678b0cee8b5a4d3bfd7484b10edb8dcf6",+ "solverVersion": "0.6.2",+ "spec": "012ba0487f597f1fe46672c3811dbb4db8c4eb57d33f4da81090f1b7795af72d"+ },+ "peakJDefinition": {+ "displayPercentile": 0.995,+ "minimumCopperFraction": 0.25,+ "rounded": false,+ "statistic": "unclipped-cell-maximum"+ },+ "nets": [+ {+ "amps": 3.000220385674931,+ "copperMm2": 71.8,+ "iterations": 647,+ "jouleW": 0.014,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "DUT_POS",+ "peakAt": [+ 107.1,+ 93.9+ ],+ "peakJ": 48.634830474853516,+ "peakLayer": "F.Cu",+ "platedPads": 1,+ "residual": 8.537579182423423e-08,+ "vias": 2+ },+ {+ "amps": 3.0000289156626505,+ "copperMm2": 159.2,+ "iterations": 733,+ "jouleW": 0.027,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "LOAD_POS",+ "peakAt": [+ 126.5,+ 86.3+ ],+ "peakJ": 68.60289001464844,+ "peakLayer": "F.Cu",+ "platedPads": 1,+ "residual": 9.802374552737071e-08,+ "vias": 15+ },+ {+ "amps": 3.0,+ "copperMm2": 5211.5,+ "iterations": 7021,+ "jouleW": 0.053,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "GND",+ "peakAt": [+ 136.1,+ 100.89999999999999+ ],+ "peakJ": 76.35523223876953,+ "peakLayer": "F.Cu",+ "platedPads": 14,+ "residual": 9.83067050263189e-08,+ "vias": 48+ },+ {+ "amps": 3.0,+ "copperMm2": 46.6,+ "iterations": 254,+ "jouleW": 0.026,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "SHUNT_P",+ "peakAt": [+ 127.1,+ 83.3+ ],+ "peakJ": 60.97330093383789,+ "peakLayer": "B.Cu",+ "platedPads": 1,+ "residual": 9.920461353185108e-08,+ "vias": 7+ },+ {+ "amps": 3.0,+ "copperMm2": 19.7,+ "iterations": 147,+ "jouleW": 0.003,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "DUT_FUSED",+ "peakAt": [+ 109.1,+ 91.9+ ],+ "peakJ": 45.40793228149414,+ "peakLayer": "F.Cu",+ "platedPads": 0,+ "residual": 7.208780991872171e-08,+ "vias": 2+ },+ {+ "amps": 0.3,+ "copperMm2": 66.6,+ "iterations": 756,+ "jouleW": 0.001,+ "layers": [+ "F.Cu",+ "B.Cu"+ ],+ "net": "5V_AUX",+ "peakAt": [+ 152.1,+ 90.1+ ],+ "peakJ": 18.182907104492188,+ "peakLayer": "F.Cu",+ "platedPads": 2,+ "residual": 8.34572086330967e-08,+ "vias": 7+ }+ ],+ "currentIssues": [+ {+ "at": [+ 136.1,+ 100.89999999999999+ ],+ "fix": "Widen the neck or add a parallel path on the other layer with vias; the density map shows where the current squeezes.",+ "kind": "current-neck",+ "layer": "F.Cu",+ "net": "GND",+ "reference": null,+ "severity": "medium",+ "what": "GND: 76 A/mm\u00b2 at its worst point on F.Cu (3 A through a neck about 1.12 mm wide at 35 \u00b5m; the threshold is 60 A/mm\u00b2)"+ },+ {+ "at": [+ 126.5,+ 86.3+ ],+ "fix": "Widen the neck or add a parallel path on the other layer with vias; the density map shows where the current squeezes.",+ "kind": "current-neck",+ "layer": "F.Cu",+ "net": "LOAD_POS",+ "reference": null,+ "severity": "medium",+ "what": "LOAD_POS: 69 A/mm\u00b2 at its worst point on F.Cu (3.0000289156626505 A through a neck about 1.25 mm wide at 35 \u00b5m; the threshold is 60 A/mm\u00b2)"+ },+ {+ "at": [+ 127.1,+ 83.3+ ],+ "fix": "Widen the neck or add a parallel path on the other layer with vias; the density map shows where the current squeezes.",+ "kind": "current-neck",+ "layer": "B.Cu",+ "net": "SHUNT_P",+ "reference": null,+ "severity": "medium",+ "what": "SHUNT_P: 61 A/mm\u00b2 at its worst point on B.Cu (3 A through a neck about 1.41 mm wide at 35 \u00b5m; the threshold is 60 A/mm\u00b2)"+ }+ ],+ "sourcePr": "https://wiki.adom.inc/adom/adom-fields/prs/7",+ "solverSha256": "0ab71adbbe0088c5d56d81f053703d0efb41d2b82f1d781686711f1e11829158",+ "correction": "Earlier 57.9 A/mm2 peak was display-clipped; earlier isolated current pass is invalid for maximum-density acceptance. Live current remained failed."+}
docs/completion-plan.md+3−1@@ -1,4 +1,6 @@-Latest 10 October work: explicit-terminal Fields source PR 6 (stacked on PR 5) is open, with 25 tests and source/diagnostic evidence. Sense-branch and auxiliary sweeps completed; an independent resistor model and an isolated AIFlow Rust candidate agree on the two misleading width failures. The candidate passed current in an isolated run under stated assumptions; live acceptance remains unchanged. See explicit-terminal-analysis.md. One 45-second Fields walkthrough was decoded/reviewed, bringing the raw inventory to 68. The dashboard now shows the saved board-19 diagnostic, not the old board-17 screenshot. Continue current-gate integration and physical cooling; do not stop at the source update.+Critical current finding: Fields peakJ was display-clipped. Unclipped 0.2 mm screening reports LOAD_POS 68.6 and SHUNT_P 61.0 A/mm² above 60. The earlier isolated pass is invalid for maximum-density acceptance; live current stayed failed. Review the actual hotspot geometry and fix the source reporting before acceptance. See design/step12-unclipped-peaks.json.++Latest 10 October work: AIFlow 0.1.79 is source-published, registry-published and installed; 127 workspace tests passed. Explicit branch-current analysis passed an isolated copy of this run, not the live acceptance gate. Fields PR 6 remains open on PR 5. The current ground review is quantifying analog-supply return offset on SHUNT_N_SENSE. A centerline resistor graph refused disconnected overlaps, so a separately instrumented copper-field diagnostic is being used instead. The dashboard is on step 12 and the reviewed raw video inventory is 68. Continue current/thermal engineering, not another source-release handoff. # Active completion contract
docs/current-hotspot-review.mdadded+11@@ -0,0 +1,11 @@+# Current hotspot review, 10 October++Saved board 19 remains the live geometry, revision 3213474890590f6f65d1603120ae19fabb7590241498da6d85c4736062ff51cc. No native changes have landed in this review.++Fields previously clipped peakJ at the 99.5th percentile before reporting and acceptance. PR 7 separates the raw maximum from its display texture. The corrected 0.2 mm solve finds GND 76.36 A/mm² at (136.1,100.9) F.Cu, LOAD_POS 68.60 at (126.5,86.3) F.Cu and SHUNT_P 60.97 at (127.1,83.3) B.Cu. All exceed the unchanged 60 screen. The earlier isolated AIFlow pass is invalid for maximum-density acceptance; the live gate was never passed.++The saved-copper drawing identifies a drain layer-change via, a source bend and a narrow ground strip around a contact clearance. Pads are omitted from that drawing; native DRC and exact pad geometry remain mandatory before accepting any rework. Candidate parallel drain vias and ground stitching will be tested on copies first. Mesh and injection sensitivity remain unresolved.++The Kelvin-return screen uses maximum listed static IC supply currents plus divider currents. It estimates 5.87 mA equivalent INA226 offset and 16.69 mA equivalent setpoint offset, using 1 mm copper-neighborhood means. These are not measured accuracy, an exact terminal solution, or a guarantee; comparator sink, gate dynamics, temperature and internal shunt parasitics remain excluded. No net has been silently shorted or reassigned.++The recording browser closed and the desktop reported only 2% free memory. The 68 existing raw takes remain available; this hotspot review has not yet been filmed. The final movie remains unfinished.
tools/hotspot_via_candidate.pyadded+10@@ -0,0 +1,10 @@+"""Run using native desktop KiCad Python; modifies only an isolated board copy."""+import pcbnew,sys,json+from pathlib import Path+source=Path(sys.argv[1]);out=Path(sys.argv[2]);board=pcbnew.LoadBoard(str(source));nets=board.GetNetsByName()+changes=[]+for net,x,y in [('LOAD_POS',126.4,87.5),('GND',133.0,100.8),('GND',138.0,100.8)]:+ via=pcbnew.PCB_VIA(board);via.SetPosition(pcbnew.VECTOR2I(pcbnew.FromMM(x),pcbnew.FromMM(y)));via.SetWidth(pcbnew.FromMM(.8));via.SetDrill(pcbnew.FromMM(.4));via.SetLayerPair(pcbnew.F_Cu,pcbnew.B_Cu);via.SetNet(nets[net]);board.Add(via);changes.append({'kind':'via','net':net,'at':[x,y],'sizeMm':.8,'drillMm':.4,'uuid':str(via.m_Uuid.AsString())})+track=pcbnew.PCB_TRACK(board);track.SetStart(pcbnew.VECTOR2I(pcbnew.FromMM(126.4),pcbnew.FromMM(86.5)));track.SetEnd(pcbnew.VECTOR2I(pcbnew.FromMM(126.4),pcbnew.FromMM(87.5)));track.SetWidth(pcbnew.FromMM(1));track.SetLayer(pcbnew.F_Cu);track.SetNet(nets['LOAD_POS']);board.Add(track);changes.append({'kind':'track','net':'LOAD_POS','start':[126.4,86.5],'end':[126.4,87.5],'widthMm':1,'layer':'F.Cu','uuid':str(track.m_Uuid.AsString())})+pcbnew.ZONE_FILLER(board).Fill(board.Zones());pcbnew.SaveBoard(str(out),board)+out.with_suffix('.changes.json').write_text(json.dumps({'source':str(source),'candidate':str(out),'changes':changes},indent=2));print(json.dumps({'saved':str(out),'changes':changes}))
tools/screen_kelvin_return.pyadded+125@@ -0,0 +1,125 @@+#!/usr/bin/env python3+"""Project-only Kelvin-return voltage diagnostic; nominal copper and ideal pad contacts."""+import hashlib,json,math+from pathlib import Path+import numpy as np+from sexp import parse,find,first+ROOT=Path(__file__).resolve().parents[1]+BOARD=ROOT/'aiflow/run/board-19.kicad_pcb'+SIGMA=58000.0 # S/mm at nominal reference temperature++def solve(edges,injections):+ nodes=sorted({n for a,b,r,meta in edges for n in [a,b]}); ids={n:i for i,n in enumerate(nodes)}+ matrix=np.zeros((len(nodes),len(nodes))); rhs=np.zeros(len(nodes))+ for a,b,r,meta in edges:+ if a==b:continue+ assert r>0+ i,j=ids[a],ids[b];g=1/r+ matrix[i,i]+=g;matrix[j,j]+=g;matrix[i,j]-=g;matrix[j,i]-=g+ for n,a in injections.items():rhs[ids[n]]+=a+ assert abs(sum(injections.values()))<1e-10+ adjacency={n:set() for n in nodes}+ for a,b,r,m in edges:adjacency[a].add(b);adjacency[b].add(a)+ reached=set();pending=[nodes[0]]+ while pending:+ n=pending.pop()+ if n in reached:continue+ reached.add(n);pending.extend(adjacency[n]-reached)+ if len(reached)!=len(nodes):+ print('DISCONNECTED', [n for n in nodes if n not in reached]);print('INJECTIONS',injections)+ raise ValueError(f'Disconnected resistor graph: {len(reached)} of {len(nodes)} nodes')+ v=np.zeros(len(nodes));v[1:]=np.linalg.solve(matrix[1:,1:],rhs[1:])+ residual=matrix@v-rhs+ assert np.max(np.abs(residual))<1e-8+ return [(v[ids[a]]-v[ids[b]])/r if a!=b else 0 for a,b,r,meta in edges],float(np.max(np.abs(residual))),{n:float(v[ids[n]]) for n in nodes}++# Analytic series and parallel checks prevent current-sharing/unit regressions.+assert np.allclose(np.abs(solve([('a','b',1,{}),('b','c',2,{})],{'a':3,'c':-3})[0]),[3,3])+assert np.allclose(np.abs(solve([('a','b',1,{}),('a','b',2,{})],{'a':3,'b':-3})[0]),[2,1])+b=parse(BOARD.read_text());netnames={str(n[1]):str(n[2]) for n in find(b,'net')}+def net(n):+ v=first(n,'net')[1];return netnames.get(str(v),str(v))+def xy(n,k):return tuple(float(x) for x in first(n,k)[1:3])+def node(layer,p):return (layer,round(p[0],6),round(p[1],6))+allresults=[]+for name in ['SHUNT_N_SENSE']:+ edges=[]; unions={}+ def root(n):+ while n in unions:n=unions[n]+ return n+ def union(a,c):+ a,c=root(a),root(c)+ if a!=c:unions[max(a,c)]=min(a,c)+ for s in find(b,'segment'):+ if net(s)!=name:continue+ a,c=xy(s,'start'),xy(s,'end');width=float(first(s,'width')[1]);layer=str(first(s,'layer')[1]);length=math.dist(a,c)+ edges.append((node(layer,a),node(layer,c),length/(SIGMA*width*.035),{'kind':'track','id':str(first(s,'uuid')[1]),'widthMm':width,'lengthMm':length,'layer':layer}))+ for v in find(b,'via'):+ if net(v)!=name:continue+ at=xy(v,'at');d=float(first(v,'drill')[1]);plating=.025+ area=math.pi*((d/2+plating)**2-(d/2)**2)+ edges.append((node('F.Cu',at),node('B.Cu',at),1.6/(SIGMA*area),{'kind':'via','id':str(first(v,'uuid')[1]),'barrelAreaMm2':area}))+ # Split centerlines at every existing same-layer endpoint before pad union.+ endpoints={n for a,c,r,m in edges for n in (a,c)}+ split=[]+ for a,c,r,m in edges:+ if m['kind']!='track':+ split.append((a,c,r,m));continue+ dx,dy=c[1]-a[1],c[2]-a[2];length2=dx*dx+dy*dy+ cuts={0.0:a,1.0:c}+ for n in endpoints:+ if n[0]!=a[0]:continue+ t=((n[1]-a[1])*dx+(n[2]-a[2])*dy)/length2+ if 1e-8<t<1-1e-8 and math.hypot(n[1]-a[1]-t*dx,n[2]-a[2]-t*dy)<1e-6:cuts[t]=n+ ordered=sorted(cuts.items())+ for (ta,na),(tb,nb) in zip(ordered,ordered[1:]):+ metadata=dict(m);metadata['lengthMm']*=tb-ta+ split.append((na,nb,r*(tb-ta),metadata))+ edges=split+ pads={}+ for f in find(b,'footprint'):+ ref=next((str(p[2]) for p in find(f,'property') if p[1]=='Reference'),'')+ at=first(f,'at');fx,fy=map(float,at[1:3]);angle=float(at[3]) if len(at)>3 else 0+ for p in find(f,'pad'):+ if not first(p,'net') or net(p)!=name:continue+ + px,py=xy(p,'at');c=math.cos(math.radians(angle));sn=math.sin(math.radians(angle));cx,cy=fx+px*c+py*sn,fy-px*sn+py*c;size=xy(p,'size');padnum=str(p[1]);pads[(ref,padnum)]=(cx,cy)+ layers=['F.Cu','B.Cu'] if p[2]=='thru_hole' else [str(first(p,'layers')[1])]+ for layer in layers:+ center=node(layer,(cx,cy))+ # Ideal equipotential pad, using its actual rotated copper outline.+ padat=first(p,'at');pa=math.radians(float(padat[3]) if len(padat)>3 else angle)+ radius=float(first(p,'roundrect_rratio')[1])*min(size) if first(p,'roundrect_rratio') else 0+ def contains(n):+ dx,dy=n[1]-cx,n[2]-cy+ x=abs(dx*math.cos(pa)-dy*math.sin(pa));y=abs(dx*math.sin(pa)+dy*math.cos(pa))+ if p[3]=='circle':return math.hypot(x,y)<=size[0]/2+1e-7+ if p[3] not in ('rect','roundrect'):raise ValueError(f'Unsupported pad shape {p[3]}')+ if x>size[0]/2+1e-7 or y>size[1]/2+1e-7:return False+ return math.hypot(max(0,x-size[0]/2+radius),max(0,y-size[1]/2+radius))<=radius+1e-7+ for edge in edges:+ for n in edge[:2]:+ if n[0]==layer and contains(n):union(center,n)+ if p[2]=='thru_hole':+ drill=float(first(p,'drill')[1]);area=math.pi*((drill/2+.025)**2-(drill/2)**2)+ edges.append((node('F.Cu',(cx,cy)),node('B.Cu',(cx,cy)),1.6/(SIGMA*area),{'kind':'plated-pad','pin':ref+'.'+padnum,'barrelAreaMm2':area}))+ edges=[(root(a),root(c),r,m) for a,c,r,m in edges]+ for source_layer in (['F.Cu','B.Cu'] if name=='DUT_POS' else ['F.Cu']):+ branch_loads={('U2','4'):.002,('U3','2'):.0018,('U4','7'):.00042+24/830000,('U7','12'):.0016,('R10','2'):2.5/(33000+5600+1000),('R19','2'):2.5/(10000+3300),('R23','2'):2.5/(33000+1000),('R28','2'):2.5/(1000+10000)}+ points=[('F.Cu',pads[k],amps) for k,amps in branch_loads.items()]+ points.append(('F.Cu',pads[('RSH1','3')],-sum(branch_loads.values())))+ injection={}+ for layer,p,amps in points:injection[root(node(layer,p))]=injection.get(root(node(layer,p)),0)+amps+ currents,residual,volts=solve(edges,injection);rows=[]+ for e,current in zip(edges,currents):+ m=dict(e[3]);m['amps']=abs(float(current));m['resistanceOhm']=e[2]+ if m['kind']=='track':+ area=m['widthMm']*.035*1550.0031+ m['ipc2221RiseC']=(m['amps']/(.048*area**.725))**(1/.44)+ rows.append(m)+ reference_v=volts[root(node('F.Cu',pads[('RSH1','3')]))]+ potentials={f'{ref}.{pin}':(volts[root(node('F.Cu',pos))]-reference_v) for (ref,pin),pos in pads.items()}+ allresults.append({'terminalVoltsRelativeToRshuntSense':potentials,'analogBiasAmps':sum(branch_loads.values()),'net':name,'sourceLayer':source_layer,'kclMaxResidualA':residual,'conductors':rows,'peakTrackRiseC':max(r.get('ipc2221RiseC',0) for r in rows)})+report={'formalAcceptance':False,'kind':'kelvin-return-voltage-diagnostic','boardSha256':hashlib.sha256(BOARD.read_bytes()).hexdigest(),'cases':allresults,'limits':['Tracks modeled as uniform straight resistors; no filled polygon current distribution','Pads treated equipotential; pad necks/contact spreading and track-overlap junctions not qualified','35 um copper, 25 um barrel plating, 1.6 mm barrels assumed, not measured','Maximum quiescent currents under their datasheet conditions plus divider currents; transient and comparator output sink currents excluded','No thermal coupling, conductivity-temperature feedback or pulse heating','This diagnostic does not override AIFlow or Fields gates']}+(ROOT/'design/step12-kelvin-return-summary.json').write_text(json.dumps(report,indent=2)+'\n')+for r in allresults:print(r['net'],r['analogBiasAmps'],'A',r['terminalVoltsRelativeToRshuntSense'])