// Apache-2.0. Browser implementation of src/rod.py, same 48 angle elements. export const N=48; export const base={chi:.001,fx:.0003,fy:-.0001,moment:0,h:0,delta:.1,k1:0,k2:0,k3:0}; const norm=v=>Math.sqrt(v.reduce((s,x)=>s+x*x,0)); export function terms(z,c){ const g=Array(N).fill(0),d=Array(N).fill(0),e=Array(N-1).fill(0);let energy=0; for(let i=0;i=0;i--){if(Math.abs(d[i])<1e-16)throw Error('singular');x[i]=(b[i]-(i+1{let neg=0,p=0;for(let i=0;i0;i++)lo*=2;for(let i=0;i<40&&count(hi)===0;i++)hi*=2; for(let i=0;i<65;i++){const m=(lo+hi)/2;if(count(m))hi=m;else lo=m;}return (lo+hi)/2; } export function solve(c,seed=null,stability=true){ let z=seed?seed.slice():Array(N).fill(0),it=0; for(;it<60;it++){ const t=terms(z,c),old=norm(t.g);if(Math.max(...t.g.map(Math.abs))<1e-10)break; let dz;try{dz=tri(t.d,t.e,t.g.map(x=>-x));}catch{break;} let alpha=1,trial,accepted=false; for(let j=0;j<16;j++){trial=z.map((v,i)=>v+alpha*dz[i]);if(norm(terms(trial,c).g)(c.h?2*c.delta:0)&&z.every(Number.isFinite); const eig=good&&stability?eigen(t.d,t.e)/c.chi:null; return {z,xy,tip:xy[N],residual,accepted:good,lambda:eig,stability:eig===null?'unknown':eig>1e-4?'stable':eig< -1e-4?'unstable':'critical',energy:t.energy,iterations:it}; } export function forward(k,stability=true,baseAngle=0){ if(![3,6,12,24].includes(k.length)||!k.every(Number.isFinite)||!Number.isFinite(baseAngle))throw Error('Invalid controls'); // Prescribed motor angle: transform the fixed WORLD load into the clamp frame. // Stability is conditional on that prescribed angle, never a free hinge. const co=Math.cos(baseAngle),si=Math.sin(baseAngle); const c={...base,curvatures:k,fx:base.fx*co+base.fy*si,fy:-base.fx*si+base.fy*co};let angle=0; const seed=Array.from({length:N},(_,i)=>angle+=k[Math.floor(i*k.length/N)]/N); const s=solve(c,seed,stability);s.xy=s.xy.map(([x,y])=>[co*x-si*y,si*x+co*y]);s.tip=s.xy[N];s.baseAngle=baseAngle;return s; } export function resample(points,count=49){ const s=[0];for(let i=1;ipoints[0].slice()); let j=1;return Array.from({length:count},(_,i)=>{const t=s.at(-1)*i/(count-1);while(jv+a*(points[j][k]-v));}); } function dense3(A,b){ const n=b.length; A=A.map((r,i)=>[...r,b[i]]); for(let i=0;iMath.abs(A[p][i]))p=j;[A[i],A[p]]=[A[p],A[i]];if(Math.abs(A[i][i])<1e-15)return Array(n).fill(0);for(let j=i+1;j=0;i--){x[i]=A[i][n];for(let j=i+1;j({k:v.slice(0,zones),angle:rotating?v[zones]:0}); const residual=v=>{const u=unpack(v),s=forward(u.k,false,u.angle);if(!s.accepted)throw Error('No equilibrium');const a=shape?s.xy.flatMap((p,i)=>p.map((v,j)=>v-target[i][j])):s.tip.map((v,i)=>v-target[i]);return [...a.map(x=>20*x),...u.k.map(x=>.002*x),...(rotating?[.0002*u.angle]:[])];}; let best=null; const aim=shape?target[3]:target,heading=Math.atan2(aim[1],aim[0]); const pack=(k,a)=>rotating?[...k,a]:k; for(const initial of [pack(warm||Array(zones).fill(0),baseAngle),pack(Array(zones).fill(-1.2),heading),pack(Array(zones).fill(1.2),heading)]){ let k=initial.slice(),r=residual(k),cost=norm(r)**2,mu=.001; for(let iter=0;iter<45;iter++){ const jac=Array.from({length:m},(_,j)=>{const kk=k.slice();kk[j]+=1e-4;const rr=residual(kk);return rr.map((v,i)=>(v-r[i])/1e-4);}); const A=jac.map((col,i)=>jac.map((other,j)=>col.reduce((s,x,a)=>s+x*other[a],0)+(i===j?mu:0))); const b=jac.map(col=>-col.reduce((s,x,i)=>s+x*r[i],0)),delta=dense3(A,b); const trial=k.map((v,i)=>{const bound=i===zones?Math.PI:2.4;return Math.max(-bound,Math.min(bound,v+delta[i]));}),rr=residual(trial),next=norm(rr)**2; if(next1e8)break; } if(!best||costs+(p[0]-target[i][0])**2+(p[1]-target[i][1])**2,0)/49):Math.hypot(state.tip[0]-target[0],state.tip[1]-target[1]); return {k:u.k,baseAngle:u.angle,state,error,matched:error<.005&&state.accepted}; } export function branches(p){ const c={...base,chi:.0001,h:.01,delta:.1,fx:p,fy:0};const output={}; for(const [name,sign] of [['down',-1],['straight',0],['up',1]]){ let s=solve(c);if(sign)for(const amp of [1.2,.6,2,2.8,3.2]){s=solve(c,Array.from({length:N},(_,i)=>sign*amp*Math.sin(Math.PI*(i+1)/(2*N))));if(s.accepted&&s.tip[1]*sign>.001)break;} if(!s.accepted||(sign&&s.tip[1]*sign<=.001))throw Error('Requested branch not found');output[name]=s; }return output; }