5G Proactive Handover // Real ML Simulation

model: loading... idle Pt: — Rt: —
RSRP (dBm) vs Time
RSRP
Serving RSRP Trad threshold Target RSRP Trad RSRP
ML Risk: Pt / St / Rt (real model output)
Risk
Pt St Rt Rt threshold
Vt — Volatility (dampens Rt → fewer false triggers)
Vt
Vt (high = noisy signal → Rt suppressed)
Handover triggers: ML vs Traditional
HO
ML trigger (Rt-based) Traditional (RSRP-based)
Time Below -100 dBm (seconds)
Below -100
ML RSRP Traditional RSRP
const GNB_POS=[0,1000,2000,3000]; const GNB_COL=['#3fb950','#79c0ff','#d2a8ff','#ffa657']; let _seed=42; function rnd(){_seed=(_seed*1664525+1013904223)&0xffffffff;return(_seed>>>0)/0xffffffff;} function rndN(){let u,v,r2;do{u=2*rnd()-1;v=2*rnd()-1;r2=u*u+v*v;}while(r2>=1||r2===0);return u*Math.sqrt(-2*Math.log(r2)/r2);} function rsrpAt(pos,gnbPos,noise,prev){const d=Math.max(Math.abs(pos-gnbPos),1);const raw=-60-15*Math.log10(d)+rndN()*noise;return prev==null?raw:0.3*raw+0.7*prev;} function bestGnb(pos){let b=0,br=-Infinity;GNB_POS.forEach((g,i)=>{const r=-60-15*Math.log10(Math.max(Math.abs(pos-g),1));if(r>br){br=r;b=i;}});return b;} function initState(){ _seed=42;const p=getP(); rsrpHistoryTrad=[];rsrpHistoryML=[];outageSegments=[]; return{p,t:0,dt:1,uePos:-500,rsrpPrev:null,servML:0,servTrad:0,hoML:0,hoTrad:0,kpiWindow:[],lastRsrp:-80,lastPt:0,lastSt:0,lastVt:0,lastRt:0,targetRsrp:-90,outageStart:null}; } async function tick(){ const s=simState; if(!s||s.t>=s.p.duration){stopSim();return;} s.uePos+=s.p.speed*s.dt;s.t+=s.dt; const rsrp=rsrpAt(s.uePos,GNB_POS[s.servML],s.p.noise,s.rsrpPrev); s.rsrpPrev=rsrp;s.lastRsrp=rsrp; const tradRsrp=rsrpAt(s.uePos,GNB_POS[s.servTrad],s.p.noise,s.rsrpPrevTrad); s.rsrpPrevTrad=tradRsrp; const bIdx=bestGnb(s.uePos); const targetRsrp=rsrpAt(s.uePos,GNB_POS[bIdx],s.p.noise,null); s.targetRsrp=targetRsrp; rsrpHistoryTrad.push(rsrp); const tradBelow = rsrp < s.p.thresh; if(tradBelow && !s.outageStart) s.outageStart = s.t; if(!tradBelow && s.outageStart) { outageSegments.push(s.t - s.outageStart); s.outageStart = null; } let Pt=0,St=0,Vt=0,Rt=0,mlHO=false; try{ const resp=await fetch(API+'/predict',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({window:s.kpiWindow,target_rsrp:targetRsrp,serving_rsrp:rsrp,rt_threshold:s.p.rtThr,alpha_ema:s.p.alpha})}); const d=await resp.json(); if(!d.error){ Pt=d.Pt;St=d.St;Vt=d.Vt;Rt=d.Rt; if(d.handover&&bIdx!==s.servML){s.hoML++;s.servML=bIdx;mlHO=true;} } }catch(e){console.warn('Backend:',e.message);} rsrpHistoryML.push(rsrp); const tradBest=bestGnb(s.uePos); const tradTgtR=rsrpAt(s.uePos,GNB_POS[tradBest],s.p.noise*1.2,null); const tradThreshold = (s.servTrad === 1) ? -102 : -105; const tradHO=tradRsrptradRsrp&&tradBest!==s.servTrad; if(tradHO){s.hoTrad++;s.servTrad=tradBest;} const label=s.t.toFixed(0)+'s'; push(charts.rsrp,label,rsrp,s.p.thresh,targetRsrp,tradRsrp); push(charts.pt,label,Pt,St,Rt,s.p.rtThr); push(charts.vt,label,Vt); push(charts.ho,label,mlHO?1:0,tradHO?1.5:0.5); if(rsrp<-100)below100ML++; if(tradRsrp<-100)below100Trad++; charts.below100.data.datasets[0].data=[below100ML,below100Trad]; charts.below100.update(); const mR=document.getElementById('m-rsrp'); mR.textContent=rsrp.toFixed(1)+' dBm';mR.className='mv '+(rsrp>-85?'good':rsrp>-97?'warn':'bad'); document.getElementById('m-time').textContent=s.t.toFixed(0)+' s'; document.getElementById('m-cell').textContent='gNB '+(s.servML+1); document.getElementById('m-homl').textContent=s.hoML; document.getElementById('m-hotrad').textContent=s.hoTrad; document.getElementById('m-pt').textContent=Pt.toFixed(3);document.getElementById('m-pt').className='mv '+(Pt>0.7?'bad':Pt>0.4?'warn':'good'); document.getElementById('m-st').textContent=St.toFixed(3); document.getElementById('m-vt').textContent=Vt.toFixed(3); document.getElementById('m-rt').textContent=Rt.toFixed(3);document.getElementById('m-rt').className='mv '+(Rt>s.p.rtThr?'bad':Rt>s.p.rtThr*0.7?'warn':'good'); document.getElementById('b-pt').textContent='Pt: '+Pt.toFixed(3); document.getElementById('b-rt').textContent='Rt: '+Rt.toFixed(3); document.getElementById('b-status').className=running?'badge live':'badge'; document.getElementById('b-status').textContent=running?'live':'done'; document.getElementById('status-box').textContent='t='+s.t.toFixed(0)+'s pos='+s.uePos.toFixed(0)+'m\nRSRP='+rsrp.toFixed(2)+' Target='+targetRsrp.toFixed(2)+'\nPt='+Pt.toFixed(4)+' St='+St.toFixed(4)+'\nVt='+Vt.toFixed(4)+' Rt='+Rt.toFixed(4)+'\nML HO: '+s.hoML+' Trad HO: '+s.hoTrad; if(s.t % 5 === 0) updateStatCharts(s.p.thresh); drawNet(); } function drawNet(stateArg){ const cv=document.getElementById('net-canvas');const ctx=cv.getContext('2d'); const W=cv.width,H=cv.height; ctx.clearRect(0,0,W,H);ctx.fillStyle='#161b22';ctx.fillRect(0,0,W,H); const roadY=H*0.52,xPad=70,xRange=W-xPad*2,minP=-600,maxP=3600; const pToX=p=>xPad+(p-minP)/(maxP-minP)*xRange; const s = stateArg !== undefined ? stateArg : simState; ctx.fillStyle='#21262d';ctx.fillRect(0,roadY-12,W,24); ctx.strokeStyle='#444c56';ctx.lineWidth=1;ctx.setLineDash([20,14]); ctx.beginPath();ctx.moveTo(0,roadY);ctx.lineTo(W,roadY);ctx.stroke();ctx.setLineDash([]); GNB_POS.forEach((gp,i)=>{ const gx=pToX(gp),col=GNB_COL[i],active=s?i===s.servML:i===0; const rad=active?160:90; const g=ctx.createRadialGradient(gx,roadY-12,0,gx,roadY-12,rad); g.addColorStop(0,col+(active?'28':'0f'));g.addColorStop(1,'transparent'); ctx.fillStyle=g;ctx.beginPath();ctx.ellipse(gx,roadY-12,rad*0.7,rad,0,0,Math.PI*2);ctx.fill(); ctx.globalAlpha=active?1:0.35;ctx.strokeStyle=col;ctx.lineWidth=active?2:1; ctx.beginPath();ctx.moveTo(gx,roadY-12);ctx.lineTo(gx,18);ctx.stroke(); ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(gx,14);ctx.lineTo(gx-10,38);ctx.lineTo(gx+10,38);ctx.closePath();ctx.fill(); ctx.globalAlpha=1;ctx.fillStyle=active?col:'#6e7681';ctx.font=(active?'600 ':'')+'9px JetBrains Mono';ctx.textAlign='center';ctx.fillText('gNB '+(i+1),gx,52); if(active){ [1,2,3].forEach(r=>{ctx.strokeStyle=col;ctx.lineWidth=0.5;ctx.globalAlpha=0.12+r*0.04;ctx.beginPath();ctx.arc(gx,roadY-12,r*28,Math.PI*1.3,Math.PI*1.7);ctx.stroke();}); ctx.globalAlpha=1; } if(active&&s){ const ux=pToX(s.uePos); const grad=ctx.createLinearGradient(gx,38,ux,roadY-10); grad.addColorStop(0,col+'cc');grad.addColorStop(1,col+'33'); ctx.strokeStyle=grad;ctx.lineWidth=1.5;ctx.setLineDash([5,6]); ctx.beginPath();ctx.moveTo(gx,38);ctx.lineTo(ux,roadY-10);ctx.stroke();ctx.setLineDash([]); } }); const ux=s?pToX(s.uePos):pToX(-500); ctx.fillStyle='#f0e68c';ctx.beginPath();ctx.roundRect(ux-18,roadY-20,36,16,3);ctx.fill(); ctx.fillStyle='rgba(13,17,23,0.6)';ctx.beginPath();ctx.roundRect(ux-14,roadY-19,10,8,2);ctx.fill();ctx.beginPath();ctx.roundRect(ux+4,roadY-19,10,8,2);ctx.fill(); ctx.fillStyle='#0d1117';ctx.font='bold 7px JetBrains Mono';ctx.textAlign='center';ctx.fillText('UE',ux,roadY-9); const rsrp=s?s.lastRsrp:-70;const sig=Math.max(0,Math.min(1,(rsrp+130)/70)); const bw=40,bh=5,bx=ux-bw/2,by=roadY+16; ctx.fillStyle='#21262d';ctx.fillRect(bx,by,bw,bh); ctx.fillStyle=sig>0.5?'#3fb950':sig>0.25?'#ffa657':'#f78166';ctx.fillRect(bx,by,bw*sig,bh); ctx.fillStyle='#6e7681';ctx.font='8px JetBrains Mono';ctx.textAlign='center';ctx.fillText(rsrp.toFixed(0)+'dBm',ux,by+bh+11); if(s&&s.lastPt>0){ const Pt=s.lastPt,Rt=s.lastRt; ctx.fillStyle='rgba(33,38,45,0.88)';ctx.beginPath();ctx.roundRect(ux-30,roadY-55,60,28,4);ctx.fill(); ctx.font='7px JetBrains Mono';ctx.textAlign='center'; ctx.fillStyle='#79c0ff';ctx.fillText('Pt='+Pt.toFixed(3),ux-10,roadY-43); ctx.fillStyle='#d2a8ff';ctx.fillText('Rt='+Rt.toFixed(3),ux+16,roadY-43); const rtBar=Math.min(Rt,1); ctx.fillStyle='#21262d';ctx.fillRect(ux-24,roadY-36,48,4); ctx.fillStyle=Rt>s.p.rtThr?'#f78166':'#d2a8ff';ctx.fillRect(ux-24,roadY-36,48*rtBar,4); ctx.strokeStyle='#f0e68c';ctx.lineWidth=1; ctx.beginPath();ctx.moveTo(ux-24+48*s.p.rtThr,roadY-38);ctx.lineTo(ux-24+48*s.p.rtThr,roadY-31);ctx.stroke(); } const pct=s?Math.max(0,Math.min(1,(s.uePos-(-600))/(3600-(-600)))):0; ctx.fillStyle='#21262d';ctx.fillRect(xPad,H-14,xRange,3); ctx.fillStyle='#79c0ff';ctx.fillRect(xPad,H-14,xRange*pct,3); ctx.fillStyle='#6e7681';ctx.font='8px JetBrains Mono'; ctx.textAlign='left';ctx.fillText('−500m',xPad,H-3); ctx.textAlign='right';ctx.fillText('3500m',W-xPad,H-3); if(s){ctx.textAlign='center';ctx.fillStyle='#79c0ff';ctx.fillText(s.uePos.toFixed(0)+'m',xPad+xRange*pct,H-3);} } async function startSim(){ if(running)return; try{await fetch(API+'/reset',{method:'POST'});}catch(e){} running=true;simState=initState();initCharts();below100ML=0;below100Trad=0; document.getElementById('btn-start').textContent='⏸ STOP'; document.getElementById('btn-start').classList.add('running'); document.getElementById('b-status').textContent='live';document.getElementById('b-status').className='badge live'; simTimer=setInterval(tick,200); } function stopSim(){ running=false;clearInterval(simTimer); document.getElementById('btn-start').textContent='▶ START SIMULATION'; document.getElementById('btn-start').classList.remove('running'); document.getElementById('b-status').textContent='done';document.getElementById('b-status').className='badge'; if(simState){ const s=simState;const red=s.hoTrad>0?((1-s.hoML/s.hoTrad)*100).toFixed(0)+'%':'N/A'; document.getElementById('status-box').textContent='Simulation complete.\nML handovers: '+s.hoML+'\nTrad handovers: '+s.hoTrad+'\nHO reduction: '+red+'\nFinal pos: '+s.uePos.toFixed(0)+'m'; } } function resetSim(){ stopSim();simState=null;initCharts();below100ML=0;below100Trad=0; ['m-time','m-rsrp','m-pt','m-st','m-vt','m-rt'].forEach(id=>{document.getElementById(id).textContent='—';document.getElementById(id).className='mv info';}); document.getElementById('m-homl').textContent='0';document.getElementById('m-hotrad').textContent='0'; document.getElementById('m-cell').textContent='gNB 1'; document.getElementById('b-pt').textContent='Pt: —';document.getElementById('b-rt').textContent='Rt: —'; document.getElementById('b-status').textContent='idle';document.getElementById('b-status').className='badge'; document.getElementById('status-box').textContent='Configure parameters → press Start'; rsrpHistoryTrad=[];rsrpHistoryML=[];outageSegments=[]; drawNet(); } function resizeCanvas(){const cv=document.getElementById('net-canvas');cv.width=cv.parentElement.offsetWidth;cv.height=210;drawNet();} window.addEventListener('load',()=>{ initCharts(); resizeCanvas(); simState=initState(); drawNet(); simState=null; document.getElementById('btn-start').addEventListener('click',()=>{running?stopSim():startSim();}); document.getElementById('btn-reset').addEventListener('click',resetSim); document.getElementById('btn-download-all').addEventListener('click',downloadAllGraphs); }); window.addEventListener('resize',resizeCanvas); const RAND_DURATION = 500; let randRunning = false; let randState = null; let rlog = {rsrp:[], pt:[], rt:[], st:[], vt:[], t:[]}; function randInitState(){ _seed = (Date.now() ^ 0xdeadbeef) & 0xffffffff; const p = getP(); rsrpHistoryTrad=[];rsrpHistoryML=[];outageSegments=[]; return { p, t:0, dt:1, uePos:1500, rsrpPrev:null, servML:1, servTrad:1, hoML:0, hoTrad:0, kpiWindow:[], lastRsrp:-80, lastPt:0, lastSt:0, lastVt:0, lastRt:0, targetRsrp:-90, speed:15, direction:1, distanceSinceDirectionChange:0, outageStart:null, }; } function randomWalkStep(s){ s.distanceSinceDirectionChange = (s.distanceSinceDirectionChange || 0) + Math.abs(s.direction * s.speed * s.dt); const minDistanceBeforeChange = 700 + rnd() * 800; if(s.distanceSinceDirectionChange >= minDistanceBeforeChange){ const roll = rnd(); if(roll < 0.15){ s.direction *= -1; s.distanceSinceDirectionChange = 0; } else if(roll < 0.40){ s.speed = 5 + rnd() * 30; } else if(roll < 0.60){ s.speed = 2 + rnd() * 8; } else{ s.speed = 15 + rnd() * 20; } } const rawPos = s.uePos + s.direction * s.speed * s.dt; if(rawPos <= -500){ s.direction = 1; s.distanceSinceDirectionChange = 0; } if(rawPos >= 3500){ s.direction = -1; s.distanceSinceDirectionChange = 0; } s.uePos = Math.max(-500, Math.min(3500, rawPos)); } async function randTick(){ if(!randRunning || !randState) return; const s = randState; if(s.t >= RAND_DURATION){ stopRandSim(); return; } try { randomWalkStep(s); s.t += s.dt; const rsrp = rsrpAt(s.uePos, GNB_POS[s.servML], s.p.noise, s.rsrpPrev); s.rsrpPrev = rsrp; s.lastRsrp = rsrp; const bIdx = bestGnb(s.uePos); const targetRsrp = rsrpAt(s.uePos, GNB_POS[bIdx], s.p.noise, null); s.targetRsrp = targetRsrp; rsrpHistoryTrad.push(rsrp); const tradBelow = rsrp < s.p.thresh; if(tradBelow && !s.outageStart) s.outageStart = s.t; if(!tradBelow && s.outageStart) { outageSegments.push(s.t - s.outageStart); s.outageStart = null; } const margin = Math.max(rsrp+140, 0); const kpiRow = { t:s.t, RSRP:rsrp, RSRQ:Math.max(-20, rsrp*0.25+rndN()*1.0), SNR: Math.max(0, rsrp+100 +rndN()*2), CQI: Math.max(1, Math.min(15, Math.round(margin/5+rndN()*0.5))), RSSI:rsrp+rndN()*1.5+10, Speed:Math.abs(s.speed)+rndN()*0.5, DL_bitrate:Math.max(1, margin*0.8+rndN()*5), UL_bitrate:Math.max(0.5, margin*0.4+rndN()*2), }; s.kpiWindow.push(kpiRow); if(s.kpiWindow.length > 18) s.kpiWindow.shift(); let Pt=0, St=0, Vt=0, Rt=0, mlHO=false; try{ const resp = await fetch(API+'/predict', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ window: s.kpiWindow, target_rsrp: targetRsrp, serving_rsrp: rsrp, rt_threshold: s.p.rtThr, alpha_ema: s.p.alpha, }) }); const d = await resp.json(); if(d && !d.error){ Pt=d.Pt; St=d.St; Vt=d.Vt; Rt=d.Rt; if(d.handover && bIdx !== s.servML){ s.hoML++; s.servML=bIdx; mlHO=true; } } }catch(e){ } rsrpHistoryML.push(rsrp); s.lastPt=Pt; s.lastSt=St; s.lastVt=Vt; s.lastRt=Rt; const tradBest = bestGnb(s.uePos); const tradTgtR = rsrpAt(s.uePos, GNB_POS[tradBest], s.p.noise, null); const tradHO = rsrp < s.p.thresh && tradTgtR > rsrp+3 && tradBest !== s.servTrad; if(tradHO){ s.hoTrad++; s.servTrad=tradBest; } if(s.t % 5 === 0){ rlog.t.push(s.t); rlog.rsrp.push(+rsrp.toFixed(2)); rlog.pt.push(+Pt.toFixed(4)); rlog.rt.push(+Rt.toFixed(4)); rlog.st.push(+St.toFixed(4)); rlog.vt.push(+Vt.toFixed(4)); } const mR = document.getElementById('m-rsrp'); mR.textContent = rsrp.toFixed(1)+' dBm'; mR.className = 'mv '+(rsrp>-85?'good':rsrp>-97?'warn':'bad'); document.getElementById('m-time').textContent = s.t.toFixed(0)+' s'; document.getElementById('m-cell').textContent = 'gNB '+(s.servML+1); document.getElementById('m-homl').textContent = s.hoML; document.getElementById('m-hotrad').textContent = s.hoTrad; document.getElementById('m-pt').textContent = Pt.toFixed(3); document.getElementById('m-rt').textContent = Rt.toFixed(3); document.getElementById('m-st').textContent = St.toFixed(3); document.getElementById('m-vt').textContent = Vt.toFixed(3); const pct = Math.round(s.t/RAND_DURATION*100); document.getElementById('btn-rand').textContent = '\u27f3 Random Walk '+pct+'%'; document.getElementById('b-status').textContent = 'rnd '+pct+'%'; document.getElementById('b-status').className = 'badge live'; document.getElementById('status-box').textContent = 'RANDOM WALK\nt='+s.t+'s pos='+s.uePos.toFixed(0)+'m\n'+ 'dir='+(s.direction>0?'right':'left')+' spd='+s.speed.toFixed(0)+'m/s\n'+ 'RSRP='+rsrp.toFixed(1)+' Pt='+Pt.toFixed(3)+'\n'+ 'ML HO: '+s.hoML+' Trad HO: '+s.hoTrad; const label = s.t.toFixed(0)+'s'; push(charts.rsrp, label, rsrp, s.p.thresh, targetRsrp); push(charts.pt, label, Pt, St, Rt, s.p.rtThr); push(charts.vt, label, Vt); push(charts.ho, label, mlHO?1:0, tradHO?1.5:0.5); if(s.t % 5 === 0) updateStatCharts(s.p.thresh); drawNet(randState); } catch(e) { console.error('randTick error:', e); } finally { if(randRunning) setTimeout(randTick, 10); } } async function startRandSim(){ if(running || randRunning) return; try{ await fetch(API+'/reset', {method:'POST'}); }catch(e){} randRunning = true; randState = randInitState(); rlog = {rsrp:[], pt:[], rt:[], st:[], vt:[], t:[]}; initCharts();below100ML=0;below100Trad=0; document.getElementById('btn-rand').textContent = '\u27f3 Random Walk 0%'; document.getElementById('btn-rand').classList.add('running'); document.getElementById('b-status').textContent = 'rnd 0%'; document.getElementById('b-status').className = 'badge live'; setTimeout(randTick, 50); } function stopRandSim(){ randRunning = false; document.getElementById('btn-rand').textContent = '\u27f3 Random Walk (500s)'; document.getElementById('btn-rand').classList.remove('running'); document.getElementById('b-status').textContent = 'done'; document.getElementById('b-status').className = 'badge'; if(randState) showReport(randState); } let rptCharts = {}; function showReport(s){ const mlHO = s.hoML, tradHO = s.hoTrad; const reduction = tradHO>0 ? ((1-mlHO/tradHO)*100).toFixed(1) : 'N/A'; const unnecessary = Math.max(0, tradHO-mlHO); const trigRateML = (mlHO /s.t*100).toFixed(2); const trigRateTrad = (tradHO/s.t*100).toFixed(2); const meanRsrp = rlog.rsrp.length ? (rlog.rsrp.reduce((a,b)=>a+b,0)/rlog.rsrp.length).toFixed(1) : '0'; const belowPct = rlog.rsrp.length ? (rlog.rsrp.filter(v=>va+b,0)/rlog.pt.length).toFixed(3) : '0'; const meanRt = rlog.rt.length ? (rlog.rt.reduce((a,b)=>a+b,0)/rlog.rt.length).toFixed(3) : '0'; document.getElementById('rpt-subtitle').textContent = '500s random mobility · '+s.t+' timesteps'; document.getElementById('rpt-homl').textContent = mlHO; document.getElementById('rpt-hotrad').textContent = tradHO; document.getElementById('rpt-reduction').textContent = reduction!=='N/A' ? reduction+'%' : 'N/A'; document.getElementById('rpt-unnecessary').textContent= unnecessary; document.getElementById('rpt-dist').textContent = s.t+'s · final pos: '+s.uePos.toFixed(0)+'m'; document.getElementById('rpt-rsrp-mean').textContent = meanRsrp+' dBm'; const bpEl = document.getElementById('rpt-below'); bpEl.textContent = belowPct+'%'; bpEl.className = 'rv '+(parseFloat(belowPct)>30?'bad':parseFloat(belowPct)>10?'amber':'good'); document.getElementById('rpt-pt-mean').textContent = meanPt; document.getElementById('rpt-rt-mean').textContent = meanRt; const maxRate = Math.max(parseFloat(trigRateML), parseFloat(trigRateTrad), 0.001); document.getElementById('rpt-ml-rate').textContent = trigRateML+' HO/s'; document.getElementById('rpt-trad-rate').textContent = trigRateTrad+' HO/s'; document.getElementById('rpt-ml-bar').style.width = Math.min(100,parseFloat(trigRateML) /maxRate*100)+'%'; document.getElementById('rpt-trad-bar').style.width = Math.min(100,parseFloat(trigRateTrad)/maxRate*100)+'%'; const tradVEl = document.getElementById('rpt-verdict-trad'); const ratio = mlHO>0 ? (tradHO/mlHO) : tradHO; const isPing = tradHO > mlHO*2.5 && tradHO>5; if(isPing){ tradVEl.className = 'verdict bad'; document.getElementById('rpt-vt-title').textContent = 'Ping-pong detected in traditional system'; document.getElementById('rpt-vt-body').textContent = 'Traditional fired '+tradHO+' HOs vs ML '+mlHO+' (ratio '+ratio.toFixed(1)+'x). '+ 'Repeated threshold crossings due to noise and direction changes cause redundant '+ 're-triggers. Each unnecessary HO adds ~50ms interruption and wastes RAN signalling. '+ '3dBm hysteresis applied but insufficient under high-noise random mobility.'; } else if(tradHO > mlHO*1.5 && tradHO>3){ tradVEl.className = 'verdict warn'; document.getElementById('rpt-vt-title').textContent = 'Traditional shows moderate over-triggering'; document.getElementById('rpt-vt-body').textContent = 'Traditional: '+tradHO+' · ML: '+mlHO+' ('+ratio.toFixed(1)+'x). Some redundant triggers '+ 'near cell boundaries where RSRP oscillates. RSRP threshold alone cannot distinguish '+ 'genuine degradation from transient channel noise.'; } else { tradVEl.className = 'verdict'; document.getElementById('rpt-vt-title').textContent = 'Traditional behaved reasonably'; document.getElementById('rpt-vt-body').textContent = 'Traditional: '+tradHO+' · ML: '+mlHO+'. No severe ping-pong under this mobility pattern. '+ 'However it still reacts only after RSRP drops — ML prepares handover proactively before degradation.'; } const mlVEl = document.getElementById('rpt-verdict-ml'); if(mlHO===0 && tradHO>3){ mlVEl.className = 'verdict warn'; document.getElementById('rpt-vm-title').textContent = 'ML was very conservative — possible under-triggering'; document.getElementById('rpt-vm-body').textContent = 'ML triggered 0 HOs while traditional triggered '+tradHO+'. Mean Rt='+meanRt+ ' stayed below threshold='+s.p.rtThr+'. High Vt may be suppressing Rt consistently. '+ 'Consider lowering Rt threshold or reducing noise.'; } else if(mlHO<=tradHO){ mlVEl.className = 'verdict'; document.getElementById('rpt-vm-title').textContent = 'ML performed well — stable, fewer triggers'; document.getElementById('rpt-vm-body').textContent = 'ML: '+mlHO+' HOs vs traditional: '+tradHO+'. Temporal pipeline filtered '+unnecessary+ ' unnecessary triggers. Mean Rt='+meanRt+' · Pt='+meanPt+'. '+ 'Vt dampened noise spikes, St smoothed transient Pt, Rt crossed threshold only on genuine degradation. '+ 'Core research claim validated: proactive + stable > reactive.'; } else { mlVEl.className = 'verdict warn'; document.getElementById('rpt-vm-title').textContent = 'ML over-triggered vs Traditional'; document.getElementById('rpt-vm-body').textContent = 'ML: '+mlHO+' vs traditional: '+tradHO+'. UE spent long time in weak signal zone. '+ 'Try increasing Rt threshold above '+s.p.rtThr+' or increasing EMA alpha.'; } Object.values(rptCharts).forEach(c=>c.destroy()); const gc='rgba(255,255,255,0.05)', tc='#6e7681'; const mkM = (id,datasets,yMin,yMax) => new Chart(document.getElementById(id),{ type:'line', data:{labels:rlog.t.map(v=>v+'s'), datasets:datasets.map(d=>({ label:d.label, data:d.data, borderColor:d.color, borderWidth:1.2, pointRadius:0, fill:false, tension:0.2, }))}, options:{responsive:true,maintainAspectRatio:false,animation:false, plugins:{legend:{display:true,labels:{color:tc,font:{size:8,family:'JetBrains Mono'},boxWidth:8}}}, scales:{ x:{ticks:{color:tc,font:{size:7},maxTicksLimit:8},grid:{color:gc},border:{color:'transparent'}}, y:{min:yMin,max:yMax,ticks:{color:tc,font:{size:7},maxTicksLimit:5},grid:{color:gc},border:{color:'transparent'}} }} }); rptCharts.rsrp = mkM('rpt-chart-rsrp',[ {label:'RSRP', data:rlog.rsrp, color:'#79c0ff'}, {label:'Thresh', data:rlog.t.map(()=>s.p.thresh), color:'rgba(247,129,102,0.5)'}, ],-140,-50); rptCharts.risk = mkM('rpt-chart-risk',[ {label:'Pt', data:rlog.pt, color:'#79c0ff'}, {label:'St', data:rlog.st, color:'#ffa657'}, {label:'Rt', data:rlog.rt, color:'#d2a8ff'}, {label:'Rt thr', data:rlog.t.map(()=>s.p.rtThr), color:'rgba(240,230,140,0.4)'}, ],0,1.05); document.getElementById('report-modal').classList.add('show'); } document.addEventListener('DOMContentLoaded', ()=>{ document.getElementById('modal-close').addEventListener('click',()=>{ document.getElementById('report-modal').classList.remove('show'); }); document.getElementById('report-modal').addEventListener('click',e=>{ if(e.target.id==='report-modal') document.getElementById('report-modal').classList.remove('show'); }); document.getElementById('btn-rand').addEventListener('click', ()=>{ if(running){ return; } if(randRunning){ stopRandSim(); } else{ startRandSim(); } }); }); function push(ch,label,...vals){ ch.data.labels.push(label); vals.forEach((v,i)=>ch.data.datasets[i].data.push(v)); if(ch.data.labels.length>MAX_PTS){ch.data.labels.shift();ch.data.datasets.forEach(d=>d.data.shift());} ch.update('none'); } function downloadAllGraphs(){ if(!charts.rsrp||!charts.rsrp.data.labels||charts.rsrp.data.labels.length===0){alert('No data to download. Run a simulation first.');return;} const csv=[];csv.push('Time(s),RSRP(dBm),Threshold(dBm),TargetRSRP(dBm),TradRSRP(dBm),Pt,St,Rt,Vt,MLHandover,TradHandover'); const n=charts.rsrp.data.labels.length; for(let i=0;itypeof v==='number'?v.toFixed(3):v).join(',')); } const blob=new Blob([csv.join('\n')],{type:'text/csv'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='simulation_all_graphs.csv';a.click();URL.revokeObjectURL(url); } const GNB_POS=[0,1000,2000,3000]; const GNB_COL=['#3fb950','#79c0ff','#d2a8ff','#ffa657']; let _seed=42; function rnd(){_seed=(_seed*1664525+1013904223)&0xffffffff;return(_seed>>>0)/0xffffffff;} function rndN(){let u,v,r2;do{u=2*rnd()-1;v=2*rnd()-1;r2=u*u+v*v;}while(r2>=1||r2===0);return u*Math.sqrt(-2*Math.log(r2)/r2);} function rsrpAt(pos,gnbPos,noise,prev){const d=Math.max(Math.abs(pos-gnbPos),1);const raw=-60-15*Math.log10(d)+rndN()*noise;return prev==null?raw:0.3*raw+0.7*prev;} function bestGnb(pos){let b=0,br=-Infinity;GNB_POS.forEach((g,i)=>{const r=-60-15*Math.log10(Math.max(Math.abs(pos-g),1));if(r>br){br=r;b=i;}});return b;} function initState(){ _seed=42;const p=getP(); return{p,t:0,dt:1,uePos:-500,rsrpPrev:null,rsrpPrevTrad:null,servML:0,servTrad:0,hoML:0,hoTrad:0,kpiWindow:[],lastRsrp:-80,lastPt:0,lastSt:0,lastVt:0,lastRt:0,targetRsrp:-90}; } async function tick(){ const s=simState; if(!s||s.t>=s.p.duration){stopSim();return;} s.uePos+=s.p.speed*s.dt;s.t+=s.dt; const rsrp=rsrpAt(s.uePos,GNB_POS[s.servML],s.p.noise,s.rsrpPrev); s.rsrpPrev=rsrp;s.lastRsrp=rsrp; const tradRsrp=rsrpAt(s.uePos,GNB_POS[s.servTrad],s.p.noise,s.rsrpPrevTrad); s.rsrpPrevTrad=tradRsrp; const bIdx=bestGnb(s.uePos); const targetRsrp=rsrpAt(s.uePos,GNB_POS[bIdx],s.p.noise,null); s.targetRsrp=targetRsrp; const margin=Math.max(rsrp+140,0); const kpiRow={ t:s.t,RSRP:rsrp, RSRQ:Math.max(-20,rsrp*0.25+rndN()*1.0), SNR:Math.max(0,rsrp+100+rndN()*2), CQI:Math.max(1,Math.min(15,Math.round(margin/5+rndN()*0.5))), RSSI:rsrp+rndN()*1.5+10, Speed:s.p.speed+rndN()*0.5, DL_bitrate:Math.max(1,margin*0.8+rndN()*5), UL_bitrate:Math.max(0.5,margin*0.4+rndN()*2), }; s.kpiWindow.push(kpiRow); if(s.kpiWindow.length>18)s.kpiWindow.shift(); let Pt=0,St=0,Vt=0,Rt=0,mlHO=false; try{ const resp=await fetch(API+'/predict',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({window:s.kpiWindow,target_rsrp:targetRsrp,serving_rsrp:rsrp,rt_threshold:s.p.rtThr,alpha_ema:s.p.alpha})}); const d=await resp.json(); if(!d.error){ Pt=d.Pt;St=d.St;Vt=d.Vt;Rt=d.Rt; if(d.handover&&bIdx!==s.servML){s.hoML++;s.servML=bIdx;mlHO=true;} } }catch(e){console.warn('Backend:',e.message);} s.lastPt=Pt;s.lastSt=St;s.lastVt=Vt;s.lastRt=Rt; const tradBest=bestGnb(s.uePos); const tradTgtR=rsrpAt(s.uePos,GNB_POS[tradBest],s.p.noise*1.2,null); // Traditional: directional hysteresis (3dB only on reverse Cell2→Cell1) const tradThreshold = (s.servTrad === 1) ? -102 : -105; const tradHO=tradRsrptradRsrp&&tradBest!==s.servTrad; if(tradHO){s.hoTrad++;s.servTrad=tradBest;} const label=s.t.toFixed(0)+'s'; push(charts.rsrp,label,rsrp,s.p.thresh,targetRsrp,tradRsrp); push(charts.pt,label,Pt,St,Rt,s.p.rtThr); push(charts.vt,label,Vt); push(charts.ho,label,mlHO?1:0,tradHO?1.5:0.5); if(rsrp<-100)below100ML++; if(tradRsrp<-100)below100Trad++; charts.below100.data.datasets[0].data=[below100ML,below100Trad]; charts.below100.update(); const mR=document.getElementById('m-rsrp'); mR.textContent=rsrp.toFixed(1)+' dBm';mR.className='mv '+(rsrp>-85?'good':rsrp>-97?'warn':'bad'); document.getElementById('m-time').textContent=s.t.toFixed(0)+' s'; document.getElementById('m-cell').textContent='gNB '+(s.servML+1); document.getElementById('m-homl').textContent=s.hoML; document.getElementById('m-hotrad').textContent=s.hoTrad; document.getElementById('m-pt').textContent=Pt.toFixed(3);document.getElementById('m-pt').className='mv '+(Pt>0.7?'bad':Pt>0.4?'warn':'good'); document.getElementById('m-st').textContent=St.toFixed(3); document.getElementById('m-vt').textContent=Vt.toFixed(3); document.getElementById('m-rt').textContent=Rt.toFixed(3);document.getElementById('m-rt').className='mv '+(Rt>s.p.rtThr?'bad':Rt>s.p.rtThr*0.7?'warn':'good'); document.getElementById('b-pt').textContent='Pt: '+Pt.toFixed(3); document.getElementById('b-rt').textContent='Rt: '+Rt.toFixed(3); document.getElementById('b-status').className=running?'badge live':'badge'; document.getElementById('b-status').textContent=running?'live':'done'; document.getElementById('status-box').textContent='t='+s.t.toFixed(0)+'s pos='+s.uePos.toFixed(0)+'m\nRSRP='+rsrp.toFixed(2)+' Target='+targetRsrp.toFixed(2)+'\nPt='+Pt.toFixed(4)+' St='+St.toFixed(4)+'\nVt='+Vt.toFixed(4)+' Rt='+Rt.toFixed(4)+'\nML HO: '+s.hoML+' Trad HO: '+s.hoTrad; drawNet(); } function drawNet(stateArg){ const cv=document.getElementById('net-canvas');const ctx=cv.getContext('2d'); const W=cv.width,H=cv.height; ctx.clearRect(0,0,W,H);ctx.fillStyle='#161b22';ctx.fillRect(0,0,W,H); const roadY=H*0.52,xPad=70,xRange=W-xPad*2,minP=-600,maxP=3600; const pToX=p=>xPad+(p-minP)/(maxP-minP)*xRange; const s = stateArg !== undefined ? stateArg : simState; ctx.fillStyle='#21262d';ctx.fillRect(0,roadY-12,W,24); ctx.strokeStyle='#444c56';ctx.lineWidth=1;ctx.setLineDash([20,14]); ctx.beginPath();ctx.moveTo(0,roadY);ctx.lineTo(W,roadY);ctx.stroke();ctx.setLineDash([]); GNB_POS.forEach((gp,i)=>{ const gx=pToX(gp),col=GNB_COL[i],active=s?i===s.servML:i===0; const rad=active?160:90; const g=ctx.createRadialGradient(gx,roadY-12,0,gx,roadY-12,rad); g.addColorStop(0,col+(active?'28':'0f'));g.addColorStop(1,'transparent'); ctx.fillStyle=g;ctx.beginPath();ctx.ellipse(gx,roadY-12,rad*0.7,rad,0,0,Math.PI*2);ctx.fill(); ctx.globalAlpha=active?1:0.35;ctx.strokeStyle=col;ctx.lineWidth=active?2:1; ctx.beginPath();ctx.moveTo(gx,roadY-12);ctx.lineTo(gx,18);ctx.stroke(); ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(gx,14);ctx.lineTo(gx-10,38);ctx.lineTo(gx+10,38);ctx.closePath();ctx.fill(); ctx.globalAlpha=1;ctx.fillStyle=active?col:'#6e7681';ctx.font=(active?'600 ':'')+'9px JetBrains Mono';ctx.textAlign='center';ctx.fillText('gNB '+(i+1),gx,52); if(active){ [1,2,3].forEach(r=>{ctx.strokeStyle=col;ctx.lineWidth=0.5;ctx.globalAlpha=0.12+r*0.04;ctx.beginPath();ctx.arc(gx,roadY-12,r*28,Math.PI*1.3,Math.PI*1.7);ctx.stroke();}); ctx.globalAlpha=1; } if(active&&s){ const ux=pToX(s.uePos); const grad=ctx.createLinearGradient(gx,38,ux,roadY-10); grad.addColorStop(0,col+'cc');grad.addColorStop(1,col+'33'); ctx.strokeStyle=grad;ctx.lineWidth=1.5;ctx.setLineDash([5,6]); ctx.beginPath();ctx.moveTo(gx,38);ctx.lineTo(ux,roadY-10);ctx.stroke();ctx.setLineDash([]); } }); const ux=s?pToX(s.uePos):pToX(-500); ctx.fillStyle='#f0e68c';ctx.beginPath();ctx.roundRect(ux-18,roadY-20,36,16,3);ctx.fill(); ctx.fillStyle='rgba(13,17,23,0.6)';ctx.beginPath();ctx.roundRect(ux-14,roadY-19,10,8,2);ctx.fill();ctx.beginPath();ctx.roundRect(ux+4,roadY-19,10,8,2);ctx.fill(); ctx.fillStyle='#0d1117';ctx.font='bold 7px JetBrains Mono';ctx.textAlign='center';ctx.fillText('UE',ux,roadY-9); const rsrp=s?s.lastRsrp:-70;const sig=Math.max(0,Math.min(1,(rsrp+130)/70)); const bw=40,bh=5,bx=ux-bw/2,by=roadY+16; ctx.fillStyle='#21262d';ctx.fillRect(bx,by,bw,bh); ctx.fillStyle=sig>0.5?'#3fb950':sig>0.25?'#ffa657':'#f78166';ctx.fillRect(bx,by,bw*sig,bh); ctx.fillStyle='#6e7681';ctx.font='8px JetBrains Mono';ctx.textAlign='center';ctx.fillText(rsrp.toFixed(0)+'dBm',ux,by+bh+11); if(s&&s.lastPt>0){ const Pt=s.lastPt,Rt=s.lastRt; ctx.fillStyle='rgba(33,38,45,0.88)';ctx.beginPath();ctx.roundRect(ux-30,roadY-55,60,28,4);ctx.fill(); ctx.font='7px JetBrains Mono';ctx.textAlign='center'; ctx.fillStyle='#79c0ff';ctx.fillText('Pt='+Pt.toFixed(3),ux-10,roadY-43); ctx.fillStyle='#d2a8ff';ctx.fillText('Rt='+Rt.toFixed(3),ux+16,roadY-43); const rtBar=Math.min(Rt,1); ctx.fillStyle='#21262d';ctx.fillRect(ux-24,roadY-36,48,4); ctx.fillStyle=Rt>s.p.rtThr?'#f78166':'#d2a8ff';ctx.fillRect(ux-24,roadY-36,48*rtBar,4); ctx.strokeStyle='#f0e68c';ctx.lineWidth=1; ctx.beginPath();ctx.moveTo(ux-24+48*s.p.rtThr,roadY-38);ctx.lineTo(ux-24+48*s.p.rtThr,roadY-31);ctx.stroke(); } const pct=s?Math.max(0,Math.min(1,(s.uePos-(-600))/(3600-(-600)))):0; ctx.fillStyle='#21262d';ctx.fillRect(xPad,H-14,xRange,3); ctx.fillStyle='#79c0ff';ctx.fillRect(xPad,H-14,xRange*pct,3); ctx.fillStyle='#6e7681';ctx.font='8px JetBrains Mono'; ctx.textAlign='left';ctx.fillText('−500m',xPad,H-3); ctx.textAlign='right';ctx.fillText('3500m',W-xPad,H-3); if(s){ctx.textAlign='center';ctx.fillStyle='#79c0ff';ctx.fillText(s.uePos.toFixed(0)+'m',xPad+xRange*pct,H-3);} } async function startSim(){ if(running)return; try{await fetch(API+'/reset',{method:'POST'});}catch(e){} running=true;simState=initState();initCharts();below100ML=0;below100Trad=0; document.getElementById('btn-start').textContent='⏸ STOP'; document.getElementById('btn-start').classList.add('running'); document.getElementById('b-status').textContent='live';document.getElementById('b-status').className='badge live'; simTimer=setInterval(tick,200); } function stopSim(){ running=false;clearInterval(simTimer); document.getElementById('btn-start').textContent='▶ START SIMULATION'; document.getElementById('btn-start').classList.remove('running'); document.getElementById('b-status').textContent='done';document.getElementById('b-status').className='badge'; if(simState){ const s=simState;const red=s.hoTrad>0?((1-s.hoML/s.hoTrad)*100).toFixed(0)+'%':'N/A'; document.getElementById('status-box').textContent='Simulation complete.\nML handovers: '+s.hoML+'\nTrad handovers: '+s.hoTrad+'\nHO reduction: '+red+'\nFinal pos: '+s.uePos.toFixed(0)+'m'; } } function resetSim(){ stopSim();simState=null;initCharts();below100ML=0;below100Trad=0; ['m-time','m-rsrp','m-pt','m-st','m-vt','m-rt'].forEach(id=>{document.getElementById(id).textContent='—';document.getElementById(id).className='mv info';}); document.getElementById('m-homl').textContent='0';document.getElementById('m-hotrad').textContent='0'; document.getElementById('m-cell').textContent='gNB 1'; document.getElementById('b-pt').textContent='Pt: —';document.getElementById('b-rt').textContent='Rt: —'; document.getElementById('b-status').textContent='idle';document.getElementById('b-status').className='badge'; document.getElementById('status-box').textContent='Configure parameters → press Start'; drawNet(); } function resizeCanvas(){const cv=document.getElementById('net-canvas');cv.width=cv.parentElement.offsetWidth;cv.height=210;drawNet();} window.addEventListener('load',()=>{ // Initialize charts and UI initCharts(); resizeCanvas(); simState=initState(); drawNet(); simState=null; // Attach event listeners (after elements exist) document.getElementById('btn-start').addEventListener('click',()=>{running?stopSim():startSim();}); document.getElementById('btn-reset').addEventListener('click',resetSim); document.getElementById('btn-download-all').addEventListener('click',downloadAllGraphs); }); window.addEventListener('resize',resizeCanvas); // ============================================================ // RANDOM WALK SIMULATION — 500s, pure random UE mobility // ============================================================ const RAND_DURATION = 500; let randRunning = false; let randState = null; let rlog = {rsrp:[], pt:[], rt:[], st:[], vt:[], t:[]}; function randInitState(){ _seed = (Date.now() ^ 0xdeadbeef) & 0xffffffff; const p = getP(); return { p, t:0, dt:1, uePos:1500, rsrpPrev:null, rsrpPrevTrad:null, servML:1, servTrad:1, hoML:0, hoTrad:0, kpiWindow:[], lastRsrp:-80, lastPt:0, lastSt:0, lastVt:0, lastRt:0, targetRsrp:-90, speed:15, direction:1, distanceSinceDirectionChange:0, // Track distance for min walk before turning }; } function randomWalkStep(s){ // Track distance traveled since last direction change s.distanceSinceDirectionChange = (s.distanceSinceDirectionChange || 0) + Math.abs(s.direction * s.speed * s.dt); // Minimum distance before allowing direction change: 700-1500m const minDistanceBeforeChange = 700 + rnd() * 800; // Random chance to change direction (only after min distance) if(s.distanceSinceDirectionChange >= minDistanceBeforeChange){ const roll = rnd(); if(roll < 0.15){ // 15% chance to change direction s.direction *= -1; s.distanceSinceDirectionChange = 0; // Reset distance counter } else if(roll < 0.40){ // 25% chance to change speed s.speed = 5 + rnd() * 30; } else if(roll < 0.60){ // 20% chance for slow speed s.speed = 2 + rnd() * 8; } else{ // 40% chance for normal speed s.speed = 15 + rnd() * 20; } } const rawPos = s.uePos + s.direction * s.speed * s.dt; // Force direction change at boundaries if(rawPos <= -500){ s.direction = 1; // Force east s.distanceSinceDirectionChange = 0; } if(rawPos >= 3500){ s.direction = -1; // Force west s.distanceSinceDirectionChange = 0; } s.uePos = Math.max(-500, Math.min(3500, rawPos)); } // Recursive tick — next call only scheduled AFTER current tick fully resolves. // Uses finally{} to guarantee rescheduling even if an exception occurs mid-tick. async function randTick(){ // Guard: if stopped externally, do nothing if(!randRunning || !randState) return; const s = randState; // ── Check completion ────────────────────────────────────────────────────── if(s.t >= RAND_DURATION){ stopRandSim(); return; } try { // ── Advance UE position ─────────────────────────────────────────────────── randomWalkStep(s); s.t += s.dt; // ── Generate signal ─────────────────────────────────────────────────────── const rsrp = rsrpAt(s.uePos, GNB_POS[s.servML], s.p.noise, s.rsrpPrev); s.rsrpPrev = rsrp; s.lastRsrp = rsrp; const tradRsrp = rsrpAt(s.uePos, GNB_POS[s.servTrad], s.p.noise, s.rsrpPrevTrad); s.rsrpPrevTrad = tradRsrp; const bIdx = bestGnb(s.uePos); const targetRsrp = rsrpAt(s.uePos, GNB_POS[bIdx], s.p.noise, null); s.targetRsrp = targetRsrp; // ── Build KPI row ───────────────────────────────────────────────────────── const margin = Math.max(rsrp+140, 0); const kpiRow = { t:s.t, RSRP:rsrp, RSRQ:Math.max(-20, rsrp*0.25+rndN()*1.0), SNR: Math.max(0, rsrp+100 +rndN()*2), CQI: Math.max(1, Math.min(15, Math.round(margin/5+rndN()*0.5))), RSSI:rsrp+rndN()*1.5+10, Speed:Math.abs(s.speed)+rndN()*0.5, DL_bitrate:Math.max(1, margin*0.8+rndN()*5), UL_bitrate:Math.max(0.5, margin*0.4+rndN()*2), }; s.kpiWindow.push(kpiRow); if(s.kpiWindow.length > 18) s.kpiWindow.shift(); // ── ML inference (Flask backend) ────────────────────────────────────────── let Pt=0, St=0, Vt=0, Rt=0, mlHO=false; try{ const resp = await fetch(API+'/predict', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ window: s.kpiWindow, target_rsrp: targetRsrp, serving_rsrp: rsrp, rt_threshold: s.p.rtThr, alpha_ema: s.p.alpha, }) }); const d = await resp.json(); if(d && !d.error){ Pt=d.Pt; St=d.St; Vt=d.Vt; Rt=d.Rt; if(d.handover && bIdx !== s.servML){ s.hoML++; s.servML=bIdx; mlHO=true; } } }catch(e){ /* Flask offline — Pt/Rt stay 0 */ } s.lastPt=Pt; s.lastSt=St; s.lastVt=Vt; s.lastRt=Rt; // ── Traditional handover (RSRP threshold + 3 dBm hysteresis) ───────────── const tradBest = bestGnb(s.uePos); const tradTgtR = rsrpAt(s.uePos, GNB_POS[tradBest], s.p.noise, null); const tradHO = rsrp < s.p.thresh && tradTgtR > rsrp+3 && tradBest !== s.servTrad; if(tradHO){ s.hoTrad++; s.servTrad=tradBest; } // ── Data log (every 5 s) ────────────────────────────────────────────────── if(s.t % 5 === 0){ rlog.t.push(s.t); rlog.rsrp.push(+rsrp.toFixed(2)); rlog.pt.push(+Pt.toFixed(4)); rlog.rt.push(+Rt.toFixed(4)); rlog.st.push(+St.toFixed(4)); rlog.vt.push(+Vt.toFixed(4)); } // ── Live UI update ──────────────────────────────────────────────────────── const mR = document.getElementById('m-rsrp'); mR.textContent = rsrp.toFixed(1)+' dBm'; mR.className = 'mv '+(rsrp>-85?'good':rsrp>-97?'warn':'bad'); document.getElementById('m-time').textContent = s.t.toFixed(0)+' s'; document.getElementById('m-cell').textContent = 'gNB '+(s.servML+1); document.getElementById('m-homl').textContent = s.hoML; document.getElementById('m-hotrad').textContent = s.hoTrad; document.getElementById('m-pt').textContent = Pt.toFixed(3); document.getElementById('m-rt').textContent = Rt.toFixed(3); document.getElementById('m-st').textContent = St.toFixed(3); document.getElementById('m-vt').textContent = Vt.toFixed(3); const pct = Math.round(s.t/RAND_DURATION*100); document.getElementById('btn-rand').textContent = '\u27f3 Random Walk '+pct+'%'; document.getElementById('b-status').textContent = 'rnd '+pct+'%'; document.getElementById('b-status').className = 'badge live'; document.getElementById('status-box').textContent = 'RANDOM WALK\nt='+s.t+'s pos='+s.uePos.toFixed(0)+'m\n'+ 'dir='+(s.direction>0?'right':'left')+' spd='+s.speed.toFixed(0)+'m/s\n'+ 'RSRP='+rsrp.toFixed(1)+' Pt='+Pt.toFixed(3)+'\n'+ 'ML HO: '+s.hoML+' Trad HO: '+s.hoTrad; const label = s.t.toFixed(0)+'s'; push(charts.rsrp, label, rsrp, s.p.thresh, targetRsrp, tradRsrp); push(charts.pt, label, Pt, St, Rt, s.p.rtThr); push(charts.vt, label, Vt); push(charts.ho, label, mlHO?1:0, tradHO?1.5:0.5); drawNet(randState); } catch(e) { console.error('randTick error:', e); } finally { // ── Schedule NEXT tick in finally — guaranteed to fire even if an exception // occurred above. setTimeout(fn, 10) avoids browser background-tab throttling // that can stretch setTimeout(fn,0) to 1000ms+, making 500 ticks take >8 min. if(randRunning) setTimeout(randTick, 10); } } async function startRandSim(){ if(running || randRunning) return; // Await reset so Flask session (St_prev, Pt_buf) is clean before first tick try{ await fetch(API+'/reset', {method:'POST'}); }catch(e){} randRunning = true; randState = randInitState(); rlog = {rsrp:[], pt:[], rt:[], st:[], vt:[], t:[]}; initCharts();below100ML=0;below100Trad=0; document.getElementById('btn-rand').textContent = '\u27f3 Random Walk 0%'; document.getElementById('btn-rand').classList.add('running'); document.getElementById('b-status').textContent = 'rnd 0%'; document.getElementById('b-status').className = 'badge live'; // Small delay before first tick so UI has time to repaint setTimeout(randTick, 50); } function stopRandSim(){ randRunning = false; document.getElementById('btn-rand').textContent = '\u27f3 Random Walk (500s)'; document.getElementById('btn-rand').classList.remove('running'); document.getElementById('b-status').textContent = 'done'; document.getElementById('b-status').className = 'badge'; if(randState) showReport(randState); } // ── REPORT MODAL ────────────────────────────────────────── let rptCharts = {}; function showReport(s){ const mlHO = s.hoML, tradHO = s.hoTrad; const reduction = tradHO>0 ? ((1-mlHO/tradHO)*100).toFixed(1) : 'N/A'; const unnecessary = Math.max(0, tradHO-mlHO); const trigRateML = (mlHO /s.t*100).toFixed(2); const trigRateTrad = (tradHO/s.t*100).toFixed(2); const meanRsrp = rlog.rsrp.length ? (rlog.rsrp.reduce((a,b)=>a+b,0)/rlog.rsrp.length).toFixed(1) : '0'; const belowPct = rlog.rsrp.length ? (rlog.rsrp.filter(v=>va+b,0)/rlog.pt.length).toFixed(3) : '0'; const meanRt = rlog.rt.length ? (rlog.rt.reduce((a,b)=>a+b,0)/rlog.rt.length).toFixed(3) : '0'; document.getElementById('rpt-subtitle').textContent = '500s random mobility · '+s.t+' timesteps'; document.getElementById('rpt-homl').textContent = mlHO; document.getElementById('rpt-hotrad').textContent = tradHO; document.getElementById('rpt-reduction').textContent = reduction!=='N/A' ? reduction+'%' : 'N/A'; document.getElementById('rpt-unnecessary').textContent= unnecessary; document.getElementById('rpt-dist').textContent = s.t+'s · final pos: '+s.uePos.toFixed(0)+'m'; document.getElementById('rpt-rsrp-mean').textContent = meanRsrp+' dBm'; const bpEl = document.getElementById('rpt-below'); bpEl.textContent = belowPct+'%'; bpEl.className = 'rv '+(parseFloat(belowPct)>30?'bad':parseFloat(belowPct)>10?'amber':'good'); document.getElementById('rpt-pt-mean').textContent = meanPt; document.getElementById('rpt-rt-mean').textContent = meanRt; const maxRate = Math.max(parseFloat(trigRateML), parseFloat(trigRateTrad), 0.001); document.getElementById('rpt-ml-rate').textContent = trigRateML+' HO/s'; document.getElementById('rpt-trad-rate').textContent = trigRateTrad+' HO/s'; document.getElementById('rpt-ml-bar').style.width = Math.min(100,parseFloat(trigRateML) /maxRate*100)+'%'; document.getElementById('rpt-trad-bar').style.width = Math.min(100,parseFloat(trigRateTrad)/maxRate*100)+'%'; // Traditional correctness audit const tradVEl = document.getElementById('rpt-verdict-trad'); const ratio = mlHO>0 ? (tradHO/mlHO) : tradHO; const isPing = tradHO > mlHO*2.5 && tradHO>5; if(isPing){ tradVEl.className = 'verdict bad'; document.getElementById('rpt-vt-title').textContent = 'Ping-pong detected in traditional system'; document.getElementById('rpt-vt-body').textContent = 'Traditional fired '+tradHO+' HOs vs ML '+mlHO+' (ratio '+ratio.toFixed(1)+'x). '+ 'Repeated threshold crossings due to noise and direction changes cause redundant '+ 're-triggers. Each unnecessary HO adds ~50ms interruption and wastes RAN signalling. '+ '3dBm hysteresis applied but insufficient under high-noise random mobility.'; } else if(tradHO > mlHO*1.5 && tradHO>3){ tradVEl.className = 'verdict warn'; document.getElementById('rpt-vt-title').textContent = 'Traditional shows moderate over-triggering'; document.getElementById('rpt-vt-body').textContent = 'Traditional: '+tradHO+' · ML: '+mlHO+' ('+ratio.toFixed(1)+'x). Some redundant triggers '+ 'near cell boundaries where RSRP oscillates. RSRP threshold alone cannot distinguish '+ 'genuine degradation from transient channel noise.'; } else { tradVEl.className = 'verdict'; document.getElementById('rpt-vt-title').textContent = 'Traditional behaved reasonably'; document.getElementById('rpt-vt-body').textContent = 'Traditional: '+tradHO+' · ML: '+mlHO+'. No severe ping-pong under this mobility pattern. '+ 'However it still reacts only after RSRP drops — ML prepares handover proactively before degradation.'; } // ML assessment const mlVEl = document.getElementById('rpt-verdict-ml'); if(mlHO===0 && tradHO>3){ mlVEl.className = 'verdict warn'; document.getElementById('rpt-vm-title').textContent = 'ML was very conservative — possible under-triggering'; document.getElementById('rpt-vm-body').textContent = 'ML triggered 0 HOs while traditional triggered '+tradHO+'. Mean Rt='+meanRt+ ' stayed below threshold='+s.p.rtThr+'. High Vt may be suppressing Rt consistently. '+ 'Consider lowering Rt threshold or reducing noise.'; } else if(mlHO<=tradHO){ mlVEl.className = 'verdict'; document.getElementById('rpt-vm-title').textContent = 'ML performed well — stable, fewer triggers'; document.getElementById('rpt-vm-body').textContent = 'ML: '+mlHO+' HOs vs traditional: '+tradHO+'. Temporal pipeline filtered '+unnecessary+ ' unnecessary triggers. Mean Rt='+meanRt+' · Pt='+meanPt+'. '+ 'Vt dampened noise spikes, St smoothed transient Pt, Rt crossed threshold only on genuine degradation. '+ 'Core research claim validated: proactive + stable > reactive.'; } else { mlVEl.className = 'verdict warn'; document.getElementById('rpt-vm-title').textContent = 'ML over-triggered vs Traditional'; document.getElementById('rpt-vm-body').textContent = 'ML: '+mlHO+' vs traditional: '+tradHO+'. UE spent long time in weak signal zone. '+ 'Try increasing Rt threshold above '+s.p.rtThr+' or increasing EMA alpha.'; } // Mini charts Object.values(rptCharts).forEach(c=>c.destroy()); const gc='rgba(255,255,255,0.05)', tc='#6e7681'; const mkM = (id,datasets,yMin,yMax) => new Chart(document.getElementById(id),{ type:'line', data:{labels:rlog.t.map(v=>v+'s'), datasets:datasets.map(d=>({ label:d.label, data:d.data, borderColor:d.color, borderWidth:1.2, pointRadius:0, fill:false, tension:0.2, }))}, options:{responsive:true,maintainAspectRatio:false,animation:false, plugins:{legend:{display:true,labels:{color:tc,font:{size:8,family:'JetBrains Mono'},boxWidth:8}}}, scales:{ x:{ticks:{color:tc,font:{size:7},maxTicksLimit:8},grid:{color:gc},border:{color:'transparent'}}, y:{min:yMin,max:yMax,ticks:{color:tc,font:{size:7},maxTicksLimit:5},grid:{color:gc},border:{color:'transparent'}} }} }); rptCharts.rsrp = mkM('rpt-chart-rsrp',[ {label:'RSRP', data:rlog.rsrp, color:'#79c0ff'}, {label:'Thresh', data:rlog.t.map(()=>s.p.thresh), color:'rgba(247,129,102,0.5)'}, ],-140,-50); rptCharts.risk = mkM('rpt-chart-risk',[ {label:'Pt', data:rlog.pt, color:'#79c0ff'}, {label:'St', data:rlog.st, color:'#ffa657'}, {label:'Rt', data:rlog.rt, color:'#d2a8ff'}, {label:'Rt thr', data:rlog.t.map(()=>s.p.rtThr), color:'rgba(240,230,140,0.4)'}, ],0,1.05); document.getElementById('report-modal').classList.add('show'); } document.addEventListener('DOMContentLoaded', ()=>{ document.getElementById('modal-close').addEventListener('click',()=>{ document.getElementById('report-modal').classList.remove('show'); }); document.getElementById('report-modal').addEventListener('click',e=>{ if(e.target.id==='report-modal') document.getElementById('report-modal').classList.remove('show'); }); document.getElementById('btn-rand').addEventListener('click', ()=>{ if(running){ return; } if(randRunning){ stopRandSim(); } else{ startRandSim(); } }); });