// ─── FEED MATRIX ───────────────────────────────────────────────────────────── const FEED_MATRIX = { wheat_straw: {name:"Wheat Straw (Bhusa)", dm:0.90, cp:0.030, tdn:0.50,ndf:0.742,ca:0.0030, p:0.0010, cost:8, cat:"Roughage", maxWetKg:99}, paddy_straw: {name:"Paddy Straw", dm:0.90, cp:0.046, tdn:0.60,ndf:0.662,ca:0.0025, p:0.0010, cost:8, cat:"Roughage", maxWetKg:99}, cultivated_maize: {name:"Fresh Maize Fodder", dm:0.22, cp:0.082, tdn:0.78,ndf:0.525,ca:0.0042, p:0.0014, cost:7, cat:"Green Cereal",maxWetKg:99}, napier_grass: {name:"Napier Grass (Mature)", dm:0.22, cp:0.092, tdn:0.58,ndf:0.717,ca:0.0030, p:0.0018, cost:5, cat:"Green Cereal",maxWetKg:99}, maize_silage: {name:"Maize Silage", dm:0.30, cp:0.079, tdn:0.68,ndf:0.460,ca:0.0042, p:0.0014, cost:8, cat:"Silage", maxWetKg:99}, jowar_green: {name:"Jowar Green Fodder", dm:0.22, cp:0.070, tdn:0.65,ndf:0.550,ca:0.0050, p:0.0020, cost:7, cat:"Green Cereal",maxWetKg:99}, berseem: {name:"Berseem (Green Clover)", dm:0.15, cp:0.174, tdn:0.72,ndf:0.424,ca:0.0200, p:0.0038, cost:5, cat:"Green Legume",maxWetKg:6.0}, hmf: {name:"Hydroponic Maize Fodder", dm:0.2207,cp:0.1427,tdn:0.72,ndf:0.180,ca:0.00105,p:0.004129,cost:9, cat:"Hydroponic", maxWetKg:99}, commercial_feed: {name:"Cattle Feed (BIS Type II)", dm:0.90, cp:0.200, tdn:0.72,ndf:0.300,ca:0.0070, p:0.0050, cost:28, cat:"Concentrate", maxWetKg:99}, cottonseed_cake: {name:"Cottonseed Cake (Binola Khali)",dm:0.90, cp:0.410, tdn:0.70,ndf:0.280,ca:0.0017, p:0.0060, cost:22, cat:"Concentrate", maxWetKg:2.0}, mustard_cake: {name:"Mustard Cake (Sarson Khali)", dm:0.90, cp:0.380, tdn:0.72,ndf:0.256,ca:0.0073, p:0.0085, cost:32, cat:"Concentrate", maxWetKg:2.0}, wheat_bran: {name:"Wheat Bran (Chokar)", dm:0.88, cp:0.160, tdn:0.70,ndf:0.640,ca:0.0017, p:0.0093, cost:28, cat:"Concentrate", maxWetKg:3.0}, oat_fodder: {name:"Oat Fodder (Jai Hara)", dm:0.20, cp:0.110, tdn:0.60,ndf:0.580,ca:0.0038, p:0.0022, cost:5, cat:"Green Cereal",maxWetKg:99}, wheat_grain: {name:"Wheat Grain (Gehun Dana)", dm:0.88, cp:0.110, tdn:0.78,ndf:0.160,ca:0.0003, p:0.0028, cost:26, cat:"Grain", maxWetKg:4.0}, maize_grain: {name:"Maize Grain (Makka Dana)", dm:0.88, cp:0.090, tdn:0.87,ndf:0.156,ca:0.0002, p:0.0035, cost:18, cat:"Grain", maxWetKg:4.0}, chuni: {name:"Chuni (Pulse Husk)", dm:0.88, cp:0.150, tdn:0.60,ndf:0.450,ca:0.0080, p:0.0050, cost:30, cat:"Concentrate", maxWetKg:3.0}, }; const FEEDS = Object.entries(FEED_MATRIX).map(([key,f])=>({ key,name:f.name,dm:f.dm,cp:f.cp*100,tdn:f.tdn*100,ndf:f.ndf*100, ca:f.ca*100,p:f.p*100,cost:f.cost,cat:f.cat,maxWetKg:f.maxWetKg, desc:{ wheat_straw:"Dry cereal straw, primary rumen mat. Low energy, essential for gut motility.", paddy_straw:"Rice straw, moderately high NDF. Good roughage base in paddy-growing regions.", cultivated_maize:"High-energy green fodder. Widely grown across India, good palatability.", napier_grass:"Pennisetum purpureum. High-yield perennial. Mature CP 9.2%, NDF 71.7%.", maize_silage:"Fermented maize, excellent energy and fibre balance. Year-round availability.", jowar_green:"Sorghum green fodder. Good calcium source. Common in Deccan and central India.", berseem:"Highest protein green fodder. Rich in Ca. Cap at 6 kg wet/day for palatability.", hmf:"HMF by Shunya Fodder & Forage Agritech. Lab-tested: 14.27% CP, 0.105% Ca, 0.413% P (SGS GG25-007509).", commercial_feed:"Compound cattle feed meeting BIS 1974 Type II norms. Balanced concentrate.", cottonseed_cake:"High protein oil cake. Max 2 kg/day wet. Avoid in late pregnancy (gossypol).", mustard_cake:"Rich in protein and phosphorus. High cost but excellent amino acid profile.", wheat_bran:"Excellent phosphorus source. Laxative effect at high doses. Max 3 kg/day.", oat_fodder:"Winter green fodder. High palatability and protein (10-11% CP). Good Ca source. NDDB-recommended winter forage for north India.", wheat_grain:"High energy grain (75-80% TDN). Rarely fed in India due to cost. Ground before feeding. Max 4 kg/day to avoid rumen acidosis.", maize_grain:"Highest energy grain (85-90% TDN). Ground before feeding. Rich in starch. Max 4 kg/day. Source of β-carotene in yellow variety.", chuni:"Pulse processing by-product (arhar/mung/gram chuni). CP 15-20%, TDN 55-65%. Good protein source. Max 3 kg/day.", }[key]||"" })); const ASMM = {ca_pct:0.20, p_pct:0.12, cost_per_g:0.15}; const NDF_MIN = 0.28; let customFeedCosts={}; Object.entries(FEED_MATRIX).forEach(([k,f])=>{customFeedCosts[k]=f.cost;}); customFeedCosts['asmm']=ASMM.cost_per_g; const feedImages={ wheat_straw:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Wheat-Straw-Bhusa.png", paddy_straw:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Paddy-Straw.jpeg", cultivated_maize:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Maize-Green-Forage.jpg", napier_grass:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-hybrid-napier-bajra-grass.jpg", jowar_green:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Jowar-Green-Grass.jpeg", berseem:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Berseem-Barseem.jpg", maize_silage:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-corn-silage-cattle-feed.jpg", hmf:"https://www.shunya.live/wp-content/uploads/2026/03/53d2a11b-f422-4b6b-b8fb-aa07e4ee8124-removebg-preview.png", commercial_feed:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Cattle-Feed.jpg", cottonseed_cake:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Cottonseed-Cake.jpg", mustard_cake:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Mustard-Cake.jpg", wheat_bran:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-wheat-bran-for-animal-feed-663.jpg", oat_fodder:"", wheat_grain:"", maize_grain:"", chuni:"", }; const SOURCES=[ {title:"NDDB RBP — Feed Calculator Logic",desc:"Factorial method formulas, worked example",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Feed-Calculator-Logic.pdf"}, {title:"Animal Nutrition Booklet — NDDB 2012",desc:"Feed composition tables: CP, NDF, Ca, P",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Animal-Nutrition-booklet-1.pdf"}, {title:"FAO Paper 173 — Garg 2012",desc:"SARA limits — rumen acidity guidelines",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/i3014e00.pdf"}, {title:"NDDB Compound Cattle Feed Norm",desc:"Daily cattle feed norms by body weight",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Compound_Cattle_Feed1.pdf"}, {title:"ICAR 2013 Nutrient Requirements",desc:"Indian standard nutrient requirement tables",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Mineral_Mixture1.pdf"}, {title:"SGS India Lab Report GG25-007509",desc:"HMF composition: protein, Ca, P by Shunya Fodder",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Report_maize-8aug-1.pdf"}, ]; const CAT_GROUPS=[ {label:"Dry Roughage",cats:["Roughage"]}, {label:"Green Fodder",cats:["Green Cereal","Green Legume"]}, {label:"Silage",cats:["Silage"]}, {label:"Hydroponic",cats:["Hydroponic"]}, {label:"Grain",cats:["Grain"]}, {label:"Concentrates",cats:["Concentrate"]}, ]; let baselineChecked={}, customQtyMap={}, customLockMap={}, milkPricePerLitre=null; let goalsState=null, baselineResult=null, customResult=null; // ─── ICAR 2013 requirement basis (optional) ────────────────────────────────── // Returns requirement targets in the same shape computeGoals uses, but on ICAR // 2013 factorial coefficients instead of the NDDB-RBP-fitted ones. Calf returns // null (ICAR growth table not implemented -> NDDB calf coefficients are kept). // Activated only when window.SFR_REQ_BASIS==='icar'; default behaviour is NDDB. function icarTargets(animalType,lifeStage,bw,milk,fat,mbw){ const fcm4=milk*(0.4+0.15*fat), fcm6=milk*(0.4+0.15*fat)/1.3; const HEADROOM=1.10; // ICAR gives one expected intake; give the solver fill room above it if(lifeStage==='calf') return null; if(lifeStage==='lactating'){ if(animalType==='buffalo'){ const dmiTarget=bw*0.0216+0.67*fcm6; return {dmiTarget,dmiMax:dmiTarget*HEADROOM,tdnTargetG:36.6*mbw+440*fcm6,cpTargetG:4.875*mbw+124*fcm6,caTargetG:4.5*bw/100+4.8*milk,pTargetG:2.0*bw/100+1.8*milk}; } const dmiTarget=bw*0.0216+0.51*fcm4; // ICAR: single cow value for crossbred & indigenous return {dmiTarget,dmiMax:dmiTarget*HEADROOM,tdnTargetG:36.6*mbw+330*fcm4,cpTargetG:4.875*mbw+96*fcm4,caTargetG:4.5*bw/100+3.2*milk,pTargetG:2.0*bw/100+1.8*milk}; } let dmi=bw*0.0216,tdn=36.6*mbw,cp=4.875*mbw,ca=4.5*bw/100,p=2.0*bw/100; if(lifeStage==='pregnant_t2'){ca+=1;p+=1.5;} // foetal growth negligible <190 d if(lifeStage==='pregnant_t3'){tdn+=740;cp+=216;ca+=10;p+=6;} // ~7-8 month allowance return {dmiTarget:dmi,dmiMax:dmi,tdnTargetG:tdn,cpTargetG:cp,caTargetG:ca,pTargetG:p}; } // ─── LP COST OPTIMISER (Big-M simplex, no dependencies) ────────────────────── // The greedy cost-min pass can only TRIM (green->straw, drop dearest concentrate). // It cannot SUBSTITUTE (drop a cheap-energy grain and buy the lost protein back), // so it leaves money on the table when the diet is protein-limited. This stage // solves the same constraint set exactly. If the LP is infeasible or not cheaper // than the greedy diet, the greedy diet is kept unchanged. // Disable at runtime with: window.SFR_LP = false function sfrSimplex(c,A,b,maxIter){ const m=A.length,n=c.length,M=1e7; let rows=A.map(r=>r.slice()),rhs=b.slice(),ge=[]; for(let i=0;i-v);rhs[i]=-rhs[i];ge.push(true);} else ge.push(false); } const nS=m,nA=ge.filter(Boolean).length,total=n+nS+nA; let T=[],basis=[],artIdx=[],ai=0; for(let i=0;i1e-9){const r=T[i][total]/T[i][enter]; if(r1e-12){const f=T[i][enter];for(let j=0;j<=total;j++)T[i][j]-=f*T[leave][j];} } basis[leave]=enter; } const x=new Array(n).fill(0); for(let i=0;i=0&&T[i][total]>1e-6) return null;} return {x,obj:x.reduce((t,v,j)=>t+c[j]*v,0)}; } function lpRefine(res,goals,checkedKeys,lockedOverrides){ try{ if(typeof window!=='undefined' && window.SFR_LP===false) return null; const keys=(checkedKeys||[]).filter(k=>FEED_MATRIX[k]); if(keys.length<2) return null; const locked=lockedOverrides||{}; const costOf=k=>(customFeedCosts[k]||FEED_MATRIX[k].cost); const caps={}; for(const k of keys){ let cap=capOf(FEED_MATRIX[k]); if(goals._baselineCaps&&locked[k]===undefined){ const c=FEED_MATRIX[k].cat; if(c==='Green Cereal'||c==='Silage'||c==='Green Legume'||c==='Concentrate'||c==='Grain'){ const bc=goals._baselineCaps[k]; cap=Math.min(cap,bc===undefined?0:bc); } } caps[k]=cap; } const P=k=>{const f=FEED_MATRIX[k];return{dm:f.dm,cp:f.dm*f.cp*1000,tdn:f.dm*f.tdn*1000,ndf:f.dm*f.ndf,ca:f.dm*f.ca*1000,p:f.dm*f.p*1000,cost:costOf(k),cat:f.cat};}; const C=keys.map(P), n=keys.length+1, A=[],b=[]; const push=(r,v)=>{A.push(r);b.push(v);}; const isConc=x=>x.cat==='Concentrate'||x.cat==='Grain'; const isGreen=x=>x.cat==='Green Cereal'||x.cat==='Silage'||x.cat==='Green Legume'||x.cat==='Hydroponic'; const anyGreen=C.some(isGreen), anyRough=C.some(x=>x.cat==='Roughage'); const lockedHydroDm=Object.keys(locked).reduce((s,k)=>FEED_MATRIX[k]&&FEED_MATRIX[k].cat==='Hydroponic'?s+locked[k]*FEED_MATRIX[k].dm:s,0); const concCeil=Math.max(0.5,(2.75+(goals.milk||0)*0.4)*0.92 - lockedHydroDm*1.5); const roughFloor=(goals.lifeStage==='calf'?0.0045:0.009)*goals.bw; const greenFloor=0.02*goals.bw; push([...C.map(x=>-x.tdn),0], -goals.tdnTargetG); push([...C.map(x=>-x.cp),0], -goals.cpTargetG*0.99); push([...C.map(x=>x.dm),0], goals.dmiMax); push([...C.map(x=>-x.dm),0], -Math.min(goals.dmiTarget,goals.dmiMax*0.999)); push([...C.map(x=>0.28*x.dm-x.ndf),0], 0); push([...C.map(x=>isConc(x)?x.dm:0),0], concCeil); if(anyRough) push([...C.map(x=>x.cat==='Roughage'?-x.dm:0),0], -roughFloor); if(anyGreen && goals.lifeStage==='lactating') push([...C.map(x=>isGreen(x)?-1:0),0], -greenFloor); push([...C.map(x=>-x.ca),-0.20], -goals.caTargetG); push([...C.map(x=>-x.p), -0.12], -goals.pTargetG); keys.forEach((k,i)=>{const r=new Array(n).fill(0);r[i]=1;push(r, locked[k]!==undefined?locked[k]:caps[k]);}); keys.forEach((k,i)=>{if(locked[k]!==undefined){const r=new Array(n).fill(0);r[i]=-1;push(r,-locked[k]);}}); {const r=new Array(n).fill(0);r[n-1]=1;push(r, Math.min(300,goals.bw*0.4));} const c=[...C.map(x=>x.cost),(customFeedCosts['asmm']||ASMM.cost_per_g)]; const sol=sfrSimplex(c,A,b); if(!sol) return null; const sched={}; keys.forEach((k,i)=>{const v=Math.round(sol.x[i]*10)/10; if(v>0.005) sched[k]=v;}); for(const k in locked) sched[k]=locked[k]; let dmiD=0,tdnD=0,cpD=0,ndfD=0,caFeedD=0,pFeedD=0; for(const k in sched){const f=FEED_MATRIX[k],dm=sched[k]*f.dm; dmiD+=dm;tdnD+=dm*f.tdn*1000;cpD+=dm*f.cp*1000;ndfD+=dm*f.ndf;caFeedD+=dm*f.ca*1000;pFeedD+=dm*f.p*1000;} if(dmiD>goals.dmiMax*1.001) return null; if(tdnD=oldCost-0.05) return null; return {...res,schedule:sched,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD,_lp:true}; }catch(e){ return null; } } // ─── GOALS ─────────────────────────────────────────────────────────────────── function computeGoals(){ const animalType=document.getElementById('animalType').value; const lifeStage=document.getElementById('lifeStage').value; const bw=parseFloat(document.getElementById('bodyWeight').value)||450; const milk=lifeStage==='lactating'?parseFloat(document.getElementById('milkYield').value)||0:0; const fat=lifeStage==='lactating'?parseFloat(document.getElementById('fatPct').value)||4.0:0; const mbw=Math.pow(bw,0.75); let dmiTarget,dmiMax,tdnTargetG,cpTargetG,caTargetG,pTargetG; if(lifeStage==='calf'){ dmiTarget=dmiMax=bw*0.022; tdnTargetG=bw*12; cpTargetG=bw*4.5; caTargetG=bw*0.35; pTargetG=bw*0.20; } else if(lifeStage==='pregnant_t1'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw; cpTargetG=5.0*mbw; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100; pTargetG=2.0*bw/100; } else if(lifeStage==='pregnant_t2'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw+700; cpTargetG=5.0*mbw+75; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100+5; pTargetG=2.0*bw/100+3; } else if(lifeStage==='pregnant_t3'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw+1500; cpTargetG=5.0*mbw+150; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100+11; pTargetG=2.0*bw/100+6.5; } else if(lifeStage==='dry'){ dmiTarget=dmiMax=bw*0.018; tdnTargetG=35*mbw; cpTargetG=5.0*mbw; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100; pTargetG=2.0*bw/100; } else { if(animalType==='buffalo'){ const fcm6=milk*(0.4+0.15*fat)/1.3; tdnTargetG=37*mbw+368*fcm6; cpTargetG=5.2*mbw+76*fcm6; caTargetG=4.5*bw/100+3.2*milk; pTargetG=2.0*bw/100+2.6*milk; dmiTarget=bw*0.025+milk*0.075; dmiMax=bw*0.025+milk*0.15; } else if(animalType==='cow_indigenous'){ const fcm4=milk*(0.4+0.15*fat); tdnTargetG=35*mbw+236*fcm4; cpTargetG=5.0*mbw+57*fcm4; caTargetG=4.0*bw/100+2.8*milk; pTargetG=2.0*bw/100+2.2*milk; dmiTarget=bw*0.022+milk*0.140; dmiMax=bw*0.022+milk*0.200; } else { const fcm4=milk*(0.4+0.15*fat); tdnTargetG=35*mbw+298*fcm4; cpTargetG=5.0*mbw+63*fcm4; caTargetG=4.0*bw/100+2.4*milk; pTargetG=2.0*bw/100+1.9*milk; dmiTarget=bw*0.025+milk*0.025; dmiMax=bw*0.025+milk*0.10; } } // ── ICAR 2013 basis override (additive; the NDDB path above is untouched) ── // Flip at runtime with window.SFR_REQ_BASIS='icar' then re-run the diet. // Default (flag unset or 'nddb') = current live behaviour, byte-for-byte. let reqBasis='nddb'; if(typeof window!=='undefined' && window.SFR_REQ_BASIS==='icar'){ const ic=icarTargets(animalType,lifeStage,bw,milk,fat,mbw); if(ic){dmiTarget=ic.dmiTarget;dmiMax=ic.dmiMax;tdnTargetG=ic.tdnTargetG;cpTargetG=ic.cpTargetG;caTargetG=ic.caTargetG;pTargetG=ic.pTargetG;reqBasis='icar';} } return {bw,milk,fat,animalType,lifeStage,mbw,dmiTarget,dmiMax,tdnTargetG,cpTargetG,caTargetG,pTargetG,reqBasis}; } function computeAsmm(caFeedD,pFeedD,caTarget,pTarget,bw){ const ca_gap=Math.max(0,caTarget-caFeedD); const p_gap=Math.max(0,pTarget-pFeedD); if(ca_gap<=0&&p_gap<=0) return {dose_g:0,ca:0,p:0,ca_gap:0,p_gap:0,caDriver:'none'}; const doseRaw=Math.max(ca_gap/ASMM.ca_pct, p_gap/ASMM.p_pct); const dynamicCeiling=bw?Math.min(300,bw*0.4):200; const dose_g=Math.min(dynamicCeiling,Math.ceil(doseRaw/10)*10); const caDriver=(ca_gap/ASMM.ca_pct)>=(p_gap/ASMM.p_pct)?'Ca':'P'; return {dose_g,ca:dose_g*ASMM.ca_pct,p:dose_g*ASMM.p_pct,ca_gap,p_gap,caDriver}; } // FIX: a cap of 0 means "this feed may NOT enter". `x.maxWetKg||99` turned 0 into 99 // (0 is falsy in JS), letting capped-out feeds enter freely. capOf() is null-safe. function capOf(f){return (f&&f.maxWetKg!==undefined&&f.maxWetKg!==null)?f.maxWetKg:99;} function runSolverCore(goals,lockedOverrides,checkedKeys){ const{dmiTarget,dmiMax,tdnTargetG,cpTargetG,bw,milk,lifeStage}=goals; const AF={}; for(const key of(checkedKeys||Object.keys(FEED_MATRIX))){ const fm=FEED_MATRIX[key];if(!fm)continue; const isLocked=!!(lockedOverrides&&lockedOverrides[key]!==undefined); // When _baselineCaps is set (custom diet run), cap each non-locked feed at its baseline quantity. // Feeds not in baseline get capped at 0 (cannot enter unless locked). // This is enforced as a hard maxWetKg so no phase can exceed it. let maxWetKg=capOf(fm); if(goals._baselineCaps&&!isLocked){ const capCat=fm.cat==='Green Cereal'||fm.cat==='Silage'||fm.cat==='Green Legume'||fm.cat==='Concentrate'||fm.cat==='Grain'; if(capCat){ const baseCap=goals._baselineCaps[key]!==undefined?goals._baselineCaps[key]:0; maxWetKg=Math.min(maxWetKg,baseCap); } } AF[key]={...fm,isLocked,forcedWet:isLocked?lockedOverrides[key]:0,maxWetKg}; } let schedule={},dmiD=0,tdnD=0,cpD=0,ndfD=0,caFeedD=0,pFeedD=0; function add(key,wet){ if(wet<=0||!AF[key])return false; const f=AF[key];const dm=wet*f.dm; if(dmiD+dm>dmiMax*1.001)return false; if((schedule[key]||0)+wet>capOf(f)*1.001)return false; schedule[key]=(schedule[key]||0)+wet; dmiD+=dm;tdnD+=dm*f.tdn*1000;cpD+=dm*f.cp*1000; ndfD+=dm*f.ndf;caFeedD+=dm*f.ca*1000;pFeedD+=dm*f.p*1000; return true; } const freeOf=cat=>Object.keys(AF).filter(k=>!AF[k].isLocked&&AF[k].cat===cat); const freeConc=()=>[...freeOf('Concentrate'),...freeOf('Grain')]; for(const k in AF){ if(!AF[k].isLocked)continue; const wet=AF[k].forcedWet; const allowed=Math.min(wet,Math.max(0,(dmiMax-dmiD)/AF[k].dm)); if(allowed>0.001){ schedule[k]=(schedule[k]||0)+allowed; const d=allowed*AF[k].dm; dmiD+=d;tdnD+=d*AF[k].tdn*1000;cpD+=d*AF[k].cp*1000; ndfD+=d*AF[k].ndf;caFeedD+=d*AF[k].ca*1000;pFeedD+=d*AF[k].p*1000; } } const roughFloorDm=(lifeStage==='calf'?bw*0.0045:bw*0.009); const alreadyRoughDm=Object.keys(AF).filter(k=>AF[k].isLocked&&AF[k].cat==='Roughage').reduce((s,k)=>s+AF[k].forcedWet*AF[k].dm,0); const roughNeedDm=Math.max(0,roughFloorDm-alreadyRoughDm); const fr=freeOf('Roughage').sort((a,b)=>AF[a].cost-AF[b].cost); if(fr.length>0&&roughNeedDm>0.05){ if(fr.length===1)add(fr[0],roughNeedDm/AF[fr[0]].dm); else{add(fr[0],roughNeedDm*0.70/AF[fr[0]].dm);add(fr[1],roughNeedDm*0.30/AF[fr[1]].dm);} } // Use actual TDN already placed (Phase 0 locks + Phase 1 roughage floor) not a hardcoded estimate. // Greens can occupy up to 75% of TDN target net of what is already placed. // A minimum green floor ensures meaningful seeding even when TDN headroom is tight. const roughTdnActual=tdnD; const tdnHeadroomForGreens=Math.max(0,tdnTargetG*0.75-roughTdnActual); const greenTdnPerKgWet=0.22*0.78*1000; const fullGBbyTDN=tdnHeadroomForGreens>0?tdnHeadroomForGreens/greenTdnPerKgWet:0; const greenFloorWet=bw*0.010+milk*0.15; const fullGB=Math.min(bw*0.030+milk*0.50,dmiTarget*1.2,Math.max(fullGBbyTDN,greenFloorWet)); const gCereals=[...freeOf('Green Cereal'),...freeOf('Silage')].sort((a,b)=>(AF[b].tdn-AF[a].tdn)||(AF[a].cost-AF[b].cost)); const gLegumes=freeOf('Green Legume').sort((a,b)=>AF[a].cost-AF[b].cost); if(gCereals.length>0&&gLegumes.length>0){add(gCereals[0],fullGB*0.80);add(gLegumes[0],fullGB*0.20);} else if(gCereals.length>0){add(gCereals[0],fullGB);} else if(gLegumes.length>0){add(gLegumes[0],Math.min(fullGB*0.50,capOf(AF[gLegumes[0]])));} const freeHmf=freeOf('Hydroponic'); if(freeHmf.length>0&&gCereals.length===0){add(freeHmf[0],fullGB*0.80);} const concNormWet=lifeStage==='calf'?0:(2.75+milk*0.4); const concNormDmCeil=lifeStage==='calf'?dmiMax*0.30:concNormWet*0.92; const concPool=freeConc(); const cappedC=concPool.filter(k=>capOf(AF[k])<=10); const sumCap=cappedC.reduce((s,k)=>s+capOf(AF[k])*AF[k].dm,0); const lockedConcDm=Object.keys(AF).filter(k=>AF[k].isLocked&&(AF[k].cat==='Concentrate'||AF[k].cat==='Grain')).reduce((s,k)=>s+AF[k].forcedWet*AF[k].dm,0); const freeDmHR=Math.max(0,concNormDmCeil-lockedConcDm); const cScale=(sumCap>0&&freeDmHRcapOf(AF[k])<=10?capOf(AF[k])*cScale:capOf(AF[k]); for(let i=0;i<2000;i++){ const remTdn=tdnTargetG-tdnD,remCp=cpTargetG-cpD; if(remTdn<=tdnTargetG*0.01&&remCp<=cpTargetG*0.01)break; if(dmiD>=dmiMax*0.999)break; const cDm=Object.keys(schedule).reduce((s,k)=>(FEED_MATRIX[k]?.cat==='Concentrate'||FEED_MATRIX[k]?.cat==='Grain')?s+(schedule[k]||0)*FEED_MATRIX[k].dm:s,0); if(concNormDmCeil>0&&cDm>=concNormDmCeil*0.99)break; if(cpD>cpTargetG*1.30)break; if(tdnD>tdnTargetG*1.05&&cpD>=cpTargetG*0.99)break; const cpCeil=cpD>cpTargetG*1.15; const eligPool=cpCeil?concPool.filter(k=>AF[k]).sort((a,b)=>AF[a].cp-AF[b].cp):concPool; const elig=eligPool.filter(k=>AF[k]&&(schedule[k]||0){ const f=AF[k];const s=(Math.max(0,remTdn)/tot*f.tdn*f.dm*1000+Math.max(0,remCp)/tot*f.cp*f.dm*1000)/f.cost; return s>b.score?{key:k,score:s}:b; },{key:null,score:-1}).key; if(!best)break; const micro=Math.min(0.04,(dmiMax-dmiD)*0.4,(effMax(best)-(schedule[best]||0))*AF[best].dm)/AF[best].dm; if(micro<=0||!add(best,micro))break; } if(tdnDcpTargetG*1.15){ const highCpConc=freeConc().filter(k=>AF[k].cp>0.25&&(schedule[k]||0)>0.1).sort((a,b)=>AF[b].cp-AF[a].cp); const lowCpConc=freeConc().filter(k=>AF[k].cp<=0.25).sort((a,b)=>AF[a].cost-AF[b].cost); for(const hk of highCpConc){ for(const lk of lowCpConc){ let go=true; while(go){ go=false; if((schedule[hk]||0)<0.11||tdnD>=tdnTargetG*0.99||cpD<=cpTargetG*1.05)break; const swapDm=0.1*AF[hk].dm,addWet=swapDm/AF[lk].dm; if((schedule[lk]||0)+addWet>effMax(lk)*1.001)break; const nT=tdnD-swapDm*AF[hk].tdn*1000+swapDm*AF[lk].tdn*1000; const nC=cpD-swapDm*AF[hk].cp*1000+swapDm*AF[lk].cp*1000; if(nC<=cpTargetG*1.40){ schedule[hk]=(schedule[hk]||0)-0.1;if(schedule[hk]<0.01)delete schedule[hk]; schedule[lk]=(schedule[lk]||0)+addWet;tdnD=nT;cpD=nC;go=true; } } } } const lowPool=freeConc().sort((a,b)=>AF[a].cp-AF[b].cp); for(const k of lowPool){ for(let i=0;i<500;i++){ if(tdnD>=tdnTargetG*0.99||dmiD>=dmiMax*0.999)break; if(cpD>cpTargetG*1.30)break; const cDm=Object.keys(schedule).reduce((s,j)=>(FEED_MATRIX[j]?.cat==='Concentrate'||FEED_MATRIX[j]?.cat==='Grain')?s+(schedule[j]||0)*FEED_MATRIX[j].dm:s,0); if(concNormDmCeil>0&&cDm>=concNormDmCeil*0.99)break; const micro=Math.min(0.04,(dmiMax-dmiD)*0.4,(effMax(k)-(schedule[k]||0))*AF[k].dm)/AF[k].dm; if(micro<=0||!add(k,micro))break; } if(tdnD>=tdnTargetG*0.99)break; } } if(dmiD0){ const k=fr[0],gap=dmiTarget-dmiD; const maxByTdn=tdnTargetG*1.10>tdnD?Math.max(0,(tdnTargetG*1.10-tdnD)/(AF[k].tdn*1000*AF[k].dm)):0; const allowed=Math.min(gap/AF[k].dm,(dmiMax-dmiD)/AF[k].dm,maxByTdn/AF[k].dm); if(allowed>0.1)add(k,allowed); } if(tdnDAF[a].cost-AF[b].cost); for(const k of tdnPool){ for(let i=0;i<600;i++){ if(tdnD>=tdnTargetG*0.99||cpD>=cpTargetG*1.30||dmiD>=dmiMax*0.999)break; const micro=Math.min(0.1,(dmiMax-dmiD)/AF[k].dm,capOf(AF[k])-(schedule[k]||0)); if(micro<=0||!add(k,micro))break; } if(tdnD>=tdnTargetG*0.99)break; } } if(dmiD>0.1&&(ndfD/dmiD)AF[b].ndf-AF[a].ndf); for(const k of hNdf){ const need=dmiD*NDF_MIN-ndfD;if(need<=0)break; const wet=Math.min(need/AF[k].ndf/AF[k].dm,(dmiMax-dmiD)/AF[k].dm); if(wet>0.05)add(k,wet); } } if(goals._costMinimise){ const baselineCaps=goals._baselineCaps||null; if(baselineCaps&&fr.length>0){ const capCats=new Set(['Green Cereal','Silage','Green Legume','Concentrate','Grain']); const cheapRK=fr[0]; for(const k of Object.keys(AF)){ if(!AF[k]||!capCats.has(AF[k].cat)||AF[k].isLocked)continue; // feeds not in baseline get capped at zero (cannot enter custom diet unless locked) const baseCap=baselineCaps[k]!==undefined?baselineCaps[k]:0; const cur=schedule[k]||0;const excess=cur-baseCap;if(excess<0.05)continue; const trimDm=excess*AF[k].dm; schedule[k]=baseCap;if(schedule[k]<0.01)delete schedule[k]; dmiD-=trimDm;tdnD-=trimDm*AF[k].tdn*1000;cpD-=trimDm*AF[k].cp*1000; ndfD-=trimDm*AF[k].ndf;caFeedD-=trimDm*AF[k].ca*1000;pFeedD-=trimDm*AF[k].p*1000; if(cheapRK){ const repWet=Math.min(trimDm/AF[cheapRK].dm,(dmiMax-dmiD)/AF[cheapRK].dm); if(repWet>0.01){ schedule[cheapRK]=(schedule[cheapRK]||0)+repWet; const repDm=repWet*AF[cheapRK].dm; dmiD+=repDm;tdnD+=repDm*AF[cheapRK].tdn*1000;cpD+=repDm*AF[cheapRK].cp*1000; ndfD+=repDm*AF[cheapRK].ndf;caFeedD+=repDm*AF[cheapRK].ca*1000;pFeedD+=repDm*AF[cheapRK].p*1000; } } } } const hmfTdnNow=Object.keys(schedule).filter(k=>AF[k]?.cat==='Hydroponic'&&AF[k].isLocked).reduce((s,k)=>(schedule[k]||0)*AF[k].dm*AF[k].tdn*1000+s,0); const hasLockedHmf=hmfTdnNow>0; if(!hasLockedHmf){ const TDN_FL=tdnTargetG*0.99,CP_FL=cpTargetG*0.99,cheapRK=fr[0]; if(tdnD>tdnTargetG*1.02){ // Green floor: BW × 2% wet kg minimum green in diet. // NDDB combined roughage+green floor is BW×2-3%; dry roughage already takes BW×0.9% DM, // so BW×2% wet for greens is a practical minimum in mixed rations. const greenMinWet=bw*0.020; const totalGreenWetNow=()=>Object.keys(schedule).filter(k=>['Green Cereal','Silage','Green Legume','Hydroponic'].includes(AF[k]?.cat)).reduce((s,k)=>s+(schedule[k]||0),0); const greenCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&(AF[k]?.cat==='Green Cereal'||AF[k]?.cat==='Silage')&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.tdn||0)-(AF[a]?.tdn||0)); for(const k of greenCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; if(totalGreenWetNow()-0.1dmiMax*1.001)break; const nT=tdnD-tDm*f.tdn*1000+(rf?repDm*rf.tdn*1000:0); const nC=cpD-tDm*f.cp*1000+(rf?repDm*rf.cp*1000:0); const nN=ndfD-tDm*f.ndf+(rf?repDm*rf.ndf:0); const nD=dmiD-tDm+repDm; if(nT>=TDN_FL&&nC>=cpTargetG*0.85&&nD>0){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN; if(rf&&repWet>0){schedule[cheapRK]=(schedule[cheapRK]||0)+repWet;caFeedD+=repDm*rf.ca*1000-tDm*f.ca*1000;pFeedD+=repDm*rf.p*1000-tDm*f.p*1000;} else{caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;} go=true; } } if(tdnD<=tdnTargetG*1.02)break; } } if(tdnD>tdnTargetG*1.02||cpD>cpTargetG*1.30){ const concCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&AF[k]?.cat==='Concentrate'&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.cost||0)-(AF[a]?.cost||0)); for(const k of concCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; const f=AF[k],tDm=0.1*f.dm; const nT=tdnD-tDm*f.tdn*1000,nC=cpD-tDm*f.cp*1000,nD=dmiD-tDm,nN=ndfD-tDm*f.ndf; if(nT>=TDN_FL&&nC>=CP_FL&&nD>=dmiTarget*0.95){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN;caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;go=true; } } } } if(dmiD0){ const k=fr[0],gap=dmiTarget-dmiD; const fw=Math.min(gap/AF[k].dm,(dmiMax-dmiD)/AF[k].dm); if(fw>0.05){ schedule[k]=(schedule[k]||0)+fw;const ad=fw*AF[k].dm; dmiD+=ad;tdnD+=ad*AF[k].tdn*1000;cpD+=ad*AF[k].cp*1000; ndfD+=ad*AF[k].ndf;caFeedD+=ad*AF[k].ca*1000;pFeedD+=ad*AF[k].p*1000; } } }else{ const TDN_FL=tdnTargetG*0.99,CP_FL=cpTargetG*0.99,cheapRK=fr[0]; const TRIM_CATS=new Set(['Concentrate','Hydroponic']); const trimCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&TRIM_CATS.has(AF[k]?.cat)&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.cost||0)-(AF[a]?.cost||0)); for(const k of trimCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; const f=AF[k],tDm=0.1*f.dm,rf=cheapRK?AF[cheapRK]:null; const repDm=rf?tDm:0,repWet=rf?repDm/rf.dm:0; if(rf&&(dmiD-tDm+repDm)>dmiMax*1.001)break; const nT=tdnD-tDm*f.tdn*1000+(rf?repDm*rf.tdn*1000:0); const nC=cpD-tDm*f.cp*1000+(rf?repDm*rf.cp*1000:0); const nN=ndfD-tDm*f.ndf+(rf?repDm*rf.ndf:0); const nD=dmiD-tDm+repDm; const ndfOk=nD<0.1||(nN/nD)>=NDF_MIN; if(nT>=TDN_FL&&nC>=CP_FL&&ndfOk&&nD>0){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN; if(rf&&repWet>0){schedule[cheapRK]=(schedule[cheapRK]||0)+repWet;caFeedD+=repDm*rf.ca*1000-tDm*f.ca*1000;pFeedD+=repDm*rf.p*1000-tDm*f.p*1000;} else{caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;} go=true; } } } if((tdnDAF[a].cost-AF[b].cost); for(const k of rec){ for(let i=0;i<500;i++){ if(tdnD>=tdnTargetG*0.99&&cpD>=cpTargetG*0.99)break; if(dmiD>=dmiMax*0.999)break; const m=Math.min(0.1,(dmiMax-dmiD)/AF[k].dm,capOf(AF[k])-(schedule[k]||0)); if(m<=0||!add(k,m))break; } if(tdnD>=tdnTargetG*0.99&&cpD>=cpTargetG*0.99)break; } } } } return{schedule,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; } // ─── MODALS ────────────────────────────────────────────────────────────────── function openModal(id){document.getElementById(id).classList.add('open');} function closeModal(id){document.getElementById(id).classList.remove('open');} // modal overlay click-to-close document.querySelectorAll('.modal-overlay').forEach(el=>el.addEventListener('click',function(e){if(e.target===this)closeModal(this.id);})); function buildSources(){ document.getElementById('sourcesList').innerHTML=SOURCES.map(s=>`
${s.title}
${s.desc}
${s.pdfUrl?`PDF`:``}
`).join(''); } function openFeedInfo(feedKey){ const f=FEEDS.find(x=>x.key===feedKey); document.getElementById('infoModalTitle').textContent=f.name; document.getElementById('infoNutrientChips').innerHTML=[ `DM ${(f.dm*100).toFixed(0)}%`, `CP ${f.cp.toFixed(1)}%`, `TDN ${f.tdn.toFixed(1)}%`, `NDF ${f.ndf.toFixed(1)}%`, `Ca ${f.ca.toFixed(2)}%`, `P ${f.p.toFixed(2)}%`, `₹${f.cost}/kg`, ].join(''); document.getElementById('infoDesc').textContent=f.desc; const ph=document.getElementById('infoImgPlaceholder'); ph.innerHTML=feedImages[feedKey]?``:`No image available`; openModal('infoModal'); } function expandFeedImage(el){ const img=el.querySelector('img');if(!img)return; const ov=document.createElement('div'); ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:99999;display:flex;align-items:center;justify-content:center;cursor:zoom-out'; const bi=document.createElement('img');bi.src=img.src; bi.style.cssText='max-width:92vw;max-height:88vh;border-radius:8px'; ov.appendChild(bi);ov.addEventListener('click',()=>ov.remove());document.body.appendChild(ov); } // ─── STEP FLOW ─────────────────────────────────────────────────────────────── function toggleStep(id){document.getElementById(id).classList.toggle('collapsed');} function openStep(id){ const el=document.getElementById(id);el.classList.remove('hidden','collapsed'); setTimeout(()=>{try{el.scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},100); } function collapseStep(id){document.getElementById(id).classList.add('collapsed');} function onLifeStageChange(){ const ls=document.getElementById('lifeStage').value; document.getElementById('milkField').style.display=ls==='lactating'?'':'none'; document.getElementById('fatField').style.display=ls==='lactating'?'':'none'; } document.getElementById('animalType').addEventListener('change',function(){ const fi=document.getElementById('fatPct');if(!fi)return; if(this.value==='cow_indigenous')fi.value=4.5; else if(this.value==='cow_crossbred')fi.value=3.5; updateBreedNote(); }); function updateBreedNote(){ const el=document.getElementById('breedNote');if(!el)return; const v=document.getElementById('animalType').value; el.textContent=v==='cow_indigenous' ?'DMI adjusted downward (BW×2.2% + milk×2.5%) — reflects lower voluntary intake of Gir / Kankrej / Sahiwal. Same TDN, CP, Ca, P coefficients as crossbred per ICAR 2013.' :v==='buffalo' ?'DMI: BW×2.5% + milk×2.5% · Buffalo-specific TDN and Ca/P coefficients' :'DMI: BW×2.5% + milk×2.5% · Nutrient coefficients: ICAR 2013 standard'; } function confirmAnimalDetails(){ goalsState=computeGoals(); document.getElementById('step1Num').classList.add('done'); collapseStep('step1Section');renderStep2();openStep('step2Section'); } function renderStep2(){ const g=goalsState; document.getElementById('goalsRow').innerHTML=[ {label:'DMI',val:g.dmiTarget.toFixed(1),unit:'kg/day'}, {label:'TDN',val:(g.tdnTargetG/1000).toFixed(2),unit:'kg/day'}, {label:'CP',val:g.cpTargetG.toFixed(0),unit:'g/day'}, {label:'NDF',val:'≥28%',unit:'of DM'}, {label:'Ca',val:g.caTargetG.toFixed(1),unit:'g/day'}, {label:'P',val:g.pTargetG.toFixed(1),unit:'g/day'}, ].map(d=>`
${d.label}
${d.val}
${d.unit}
`).join(''); FEEDS.forEach(f=>{if(baselineChecked[f.key]===undefined)baselineChecked[f.key]=true;}); } function openStep22(){ document.getElementById('step2Num').classList.add('done'); collapseStep('step2Section');renderFeedSelectList();openStep('step22Section'); } function renderFeedSelectList(){ const bc={'Roughage':'cat-roughage','Green Cereal':'cat-green','Green Legume':'cat-green','Silage':'cat-silage','Hydroponic':'cat-hydro','Concentrate':'cat-conc','Grain':'cat-conc'}; let html=''; html+=`
💡Set the local price for each feed before calculating. The solver uses these costs to build the cheapest balanced diet.
`; html+=`
FeedType₹/kg
`; CAT_GROUPS.forEach(grp=>{ const feeds=FEEDS.filter(f=>grp.cats.includes(f.cat));if(!feeds.length)return; html+=`
${grp.label}
`; feeds.forEach(f=>{ const checked=baselineChecked[f.key]!==false; const cost=customFeedCosts[f.key]||f.cost; html+=`
${f.cat} /kg
`; }); html+=`
`; }); document.getElementById('feedSelectList').innerHTML=html; } function toggleFeedCheck(key,val){ baselineChecked[key]=val; const row=document.getElementById('feedrow_'+key); if(row){val?row.classList.add('checked'):row.classList.remove('checked');} } function calculateBaseline(){ const checked=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); let res=runSolverCore({...goalsState,_costMinimise:true},{},checked); const lp=lpRefine(res,goalsState,checked,{}); if(lp) res=lp; const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...res,asmm,goals:goalsState,checked}; document.getElementById('step22Num').classList.add('done'); collapseStep('step22Section');renderDietSection('baseline',res,asmm,goalsState);injectBasisToggle();openStep('step3Section'); } function calcRationCost(schedule,dose_g){ let cost=0; FEEDS.forEach(f=>{cost+=(schedule[f.key]||0)*(customFeedCosts[f.key]||f.cost);}); cost+=dose_g*(customFeedCosts['asmm']||ASMM.cost_per_g); return cost; } function buildAlerts(res,asmm,goals){ const alerts=[]; const ndfPct=res.dmiD>0?res.ndfD/res.dmiD*100:0; const totalCaG=res.caFeedD+asmm.ca,totalPG=res.pFeedD+asmm.p; const concDmPct=(()=>{ const cd=Object.keys(res.schedule).reduce((s,k)=>{ const cat=FEED_MATRIX[k]?.cat,dm=(res.schedule[k]||0)*FEED_MATRIX[k].dm; return (cat==='Concentrate'||cat==='Grain')?s+dm:s; },0); return res.dmiD>0?cd/res.dmiD*100:0; })(); const milk=goals.milk||0,ls=goals.lifeStage; const isNonLac=(ls==='pregnant_t1'||ls==='pregnant_t2'||ls==='pregnant_t3'||ls==='dry'||ls==='calf'); const saraLim=isNonLac?30:milk<=5?40:milk<=10?50:milk<=15?60:70; const totalGreenWetKg=Object.keys(res.schedule).filter(k=>['Green Cereal','Silage','Green Legume','Hydroponic'].includes(FEED_MATRIX[k]?.cat)).reduce((s,k)=>s+(res.schedule[k]||0),0); if(totalGreenWetKg<0.5&&goals.lifeStage==='lactating')alerts.push(`
No Green Fodder in DietAdd maize fodder, jowar, napier, silage, berseem, or HMF to meet energy targets within SARA-safe limits.
`); if(res.dmiD>goals.dmiMax*1.01)alerts.push(`
Stomach Overfill RiskDMI ${res.dmiD.toFixed(2)} kg exceeds max ${goals.dmiMax.toFixed(2)} kg.
`); if(concDmPct>saraLim)alerts.push(`
Rumen Acidity Risk (SARA)Concentrate DM ${concDmPct.toFixed(1)}% exceeds safe limit ${saraLim}% for current yield.
`); if(ndfPct<28)alerts.push(`
Low Fibre (NDF)NDF ${ndfPct.toFixed(1)}% is below recommended 28%. Add more roughage.
`); if(res.tdnD=goals.dmiMax*0.98)alerts.push(`
Energy Gap — Feed Quality ConstraintTDN target cannot be met within stomach capacity. Consider higher-energy feeds.
`); if(totalCaG
Calcium ShortfallTotal Ca ${totalCaG.toFixed(1)}g below 90% of target ${goals.caTargetG.toFixed(1)}g.
`); if(totalPG
Phosphorus ShortfallTotal P ${totalPG.toFixed(1)}g below 90% of target ${goals.pTargetG.toFixed(1)}g.
`); if(res.tdnD
Energy ShortfallTDN ${(res.tdnD/1000).toFixed(2)}kg vs target ${(goals.tdnTargetG/1000).toFixed(2)}kg.
`); if(res.cpD>goals.cpTargetG*1.35)alerts.push(`
Excess Protein WarningCP ${res.cpD.toFixed(0)}g exceeds 135% of target. Risk of liver urea strain and wasted feed cost.
`); if(res.cpD!(res.schedule[k]>0.1)&&FEED_MATRIX[k]); const cpMsg=cpSuggest.length>0?`Add ${cpSuggest.map(k=>FEED_MATRIX[k].name).join(' or ')}.`:`Increase quantity of protein concentrate already in diet.`; alerts.push(`
Protein ShortfallCP ${res.cpD.toFixed(0)}g vs target ${goals.cpTargetG.toFixed(0)}g. ${cpMsg}
`); } if(res.dmiD
Low Dry Matter IntakeDMI ${res.dmiD.toFixed(2)}kg below target ${goals.dmiTarget.toFixed(2)}kg.
`); const allMet=res.tdnD>=goals.tdnTargetG*0.97 &&res.cpD>=goals.cpTargetG*0.97 &&ndfPct>=28 &&totalCaG>=goals.caTargetG*0.90 &&totalPG>=goals.pTargetG*0.90 &&res.dmiD<=goals.dmiMax*1.01 &&!alerts.some(a=>a.includes('alert-error')||a.includes('alert-warn')); if(allMet)alerts.push(`
All Nutrient Targets MetThis ration meets ICAR 2013 / NDDB RBP recommendations.
`); return alerts; } // FIX: the post-rounding top-ups previously looked only at FEED_MATRIX, so they could // (i) introduce feeds the farmer had UNCHECKED, and (ii) introduce feeds absent from the // baseline into the custom diet, bypassing _baselineCaps. topUpCapFor() closes both. function topUpCapFor(k,which){ const fm=FEED_MATRIX[k]; if(!fm) return 0; const lockedHere=which==='custom'&&!!customLockMap[k]; if(baselineChecked[k]===false&&!lockedHere) return 0; // never add an unchecked feed let cap=(fm.maxWetKg!==undefined&&fm.maxWetKg!==null)?fm.maxWetKg:99; if(which==='custom'&&baselineResult&&!lockedHere){ const c=fm.cat; if(c==='Green Cereal'||c==='Silage'||c==='Green Legume'||c==='Concentrate'||c==='Grain'){ const base=baselineResult.schedule[k]; cap=Math.min(cap, base===undefined?0:base); // cannot exceed baseline qty } } return cap; } function renderDietSection(which,res,asmm,goals){ const rounded={}; FEEDS.forEach(f=>{ const wet=res.schedule[f.key]||0; if(wet<0.01){rounded[f.key]=0;return;} rounded[f.key]=Math.round(wet*10)/10; }); let dmiCheck=FEEDS.reduce((s,f)=>{const w=rounded[f.key]||0;return s+w*(FEED_MATRIX[f.key]?.dm||0.9);},0); if(dmiCheck>goals.dmiMax*1.001){ const toTrim=FEEDS.filter(f=>rounded[f.key]>0&&FEED_MATRIX[f.key]?.cat==='Roughage').sort((a,b)=>(rounded[b.key]||0)-(rounded[a.key]||0)); for(const f of toTrim){ while(dmiCheck>goals.dmiMax*1.001&&rounded[f.key]>=0.1){ rounded[f.key]=Math.round((rounded[f.key]-0.1)*10)/10; dmiCheck=FEEDS.reduce((s,ff)=>{const w=rounded[ff.key]||0;return s+w*(FEED_MATRIX[ff.key]?.dm||0.9);},0); } if(dmiCheck<=goals.dmiMax*1.001)break; } } let dmiD=0,tdnD=0,cpD=0,ndfD=0,caFeedD=0,pFeedD=0; FEEDS.forEach(f=>{ const wet=rounded[f.key]||0;if(!wet)return; const fm=FEED_MATRIX[f.key],dm=wet*fm.dm; dmiD+=dm;tdnD+=dm*fm.tdn*1000;cpD+=dm*fm.cp*1000; ndfD+=dm*fm.ndf;caFeedD+=dm*fm.ca*1000;pFeedD+=dm*fm.p*1000; }); if(tdnDv)):{}; const topUpCandidates=['commercial_feed','cottonseed_cake','mustard_cake'] .filter(k=>FEED_MATRIX[k]&&!lockedInDisplay[k]&&(rounded[k]||0)FEED_MATRIX[a].cost-FEED_MATRIX[b].cost); for(const k of topUpCandidates){ while(tdnDgoals.dmiMax)break; if((rounded[k]||0)+0.1>topUpCapFor(k,which)+1e-9)break; rounded[k]=(rounded[k]||0)+0.1; dmiD+=addDm;tdnD+=addDm*fm.tdn*1000;cpD+=addDm*fm.cp*1000; ndfD+=addDm*fm.ndf;caFeedD+=addDm*fm.ca*1000;pFeedD+=addDm*fm.p*1000; } if(tdnD>=goals.tdnTargetG*0.97)break; } } // CP top-up: after rounding, if CP still short and room exists, add cheapest protein concentrate if(cpDv)):{}; const concNormDmCeil=(2.75+(goals.milk||0)*0.4)*0.92; const cpTopUp=['mustard_cake','cottonseed_cake','commercial_feed','chuni'] .filter(k=>FEED_MATRIX[k]&&!lockedInDisplay[k]&&(rounded[k]||0)FEED_MATRIX[b].cp-FEED_MATRIX[a].cp); for(const k of cpTopUp){ while(cpD(FEED_MATRIX[j]?.cat==='Concentrate'||FEED_MATRIX[j]?.cat==='Grain')?s+(rounded[j]||0)*FEED_MATRIX[j].dm:s,0); if(concDmNow>=concNormDmCeil*0.99)break; const addDm=0.1*fm.dm; if(dmiD+addDm>goals.dmiMax)break; if((rounded[k]||0)+0.1>topUpCapFor(k,which)+1e-9)break; rounded[k]=(rounded[k]||0)+0.1; dmiD+=addDm;tdnD+=addDm*fm.tdn*1000;cpD+=addDm*fm.cp*1000; ndfD+=addDm*fm.ndf;caFeedD+=addDm*fm.ca*1000;pFeedD+=addDm*fm.p*1000; } if(cpD>=goals.cpTargetG*0.99)break; } } res={...res,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; asmm=computeAsmm(caFeedD,pFeedD,goals.caTargetG,goals.pTargetG,goals.bw); // Keep baselineResult identical to the rendered baseline so custom-diet caps are computed // against the diet the farmer actually sees. if(which==='baseline'&&baselineResult){ baselineResult.schedule={...rounded}; baselineResult.dmiD=dmiD;baselineResult.tdnD=tdnD;baselineResult.cpD=cpD; baselineResult.ndfD=ndfD;baselineResult.caFeedD=caFeedD;baselineResult.pFeedD=pFeedD;baselineResult.asmm=asmm; } if(which==='custom'&&baselineResult){ const locked2={};FEEDS.forEach(f=>{if(customLockMap[f.key])locked2[f.key]=customQtyMap[f.key];}); if((locked2['hmf']||0)>0||(res.schedule['hmf']||0)>0){ const bRW=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(baselineResult.schedule[f.key]||0),0); const cRW=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(rounded[f.key]||0),0); if(cRWf.cat==='Roughage'&&!locked2[f.key]).sort((a,b)=>a.cost-b.cost)[0]?.key; if(cheapKey){ const roomDm=Math.max(0,goals.dmiMax-dmiD); const addWet=Math.min(Math.ceil(deficit*10)/10,Math.floor(roomDm/FEED_MATRIX[cheapKey].dm*10)/10); if(addWet>=0.1){ rounded[cheapKey]=(rounded[cheapKey]||0)+addWet; const ad=addWet*FEED_MATRIX[cheapKey].dm; dmiD+=ad;tdnD+=ad*FEED_MATRIX[cheapKey].tdn*1000;cpD+=ad*FEED_MATRIX[cheapKey].cp*1000; ndfD+=ad*FEED_MATRIX[cheapKey].ndf;caFeedD+=ad*FEED_MATRIX[cheapKey].ca*1000;pFeedD+=ad*FEED_MATRIX[cheapKey].p*1000; res={...res,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; asmm=computeAsmm(caFeedD,pFeedD,goals.caTargetG,goals.pTargetG,goals.bw); } } } } } const pfx=which==='baseline'?'baseline':'custom'; const totalCaG=res.caFeedD+asmm.ca,totalPG=res.pFeedD+asmm.p; const ndfPct=res.dmiD>0?(res.ndfD/res.dmiD*100):0; const kpis=[ {label:'DMI',actual:res.dmiD.toFixed(2),target:goals.dmiMax.toFixed(2),unit:'kg',sublabel:`min ${goals.dmiTarget.toFixed(1)}`,pct:res.dmiD/goals.dmiMax}, {label:'TDN',actual:(res.tdnD/1000).toFixed(1),target:(goals.tdnTargetG/1000).toFixed(1),unit:'kg',pct:res.tdnD/1000/(goals.tdnTargetG/1000)}, {label:'CP',actual:res.cpD.toFixed(0),target:goals.cpTargetG.toFixed(0),unit:'g',pct:res.cpD/goals.cpTargetG}, {label:'NDF',actual:ndfPct.toFixed(1)+'%',target:'≥28%',unit:'',pct:ndfPct/28}, {label:asmm.dose_g>0?`Ca +ASMM ${asmm.dose_g}g`:'Ca (feed only)',actual:totalCaG.toFixed(1),target:goals.caTargetG.toFixed(1),unit:'g',pct:totalCaG/goals.caTargetG}, {label:asmm.dose_g>0?`P +ASMM ${asmm.dose_g}g`:'P (feed only)',actual:totalPG.toFixed(1),target:goals.pTargetG.toFixed(1),unit:'g',pct:totalPG/goals.pTargetG}, ]; document.getElementById(pfx+'Kpis').innerHTML=`
${kpis.map(k=>{ const pct=Math.min(k.pct,2),cls=k.pct>=1?'prog-ok':k.pct>=0.9?'prog-warn':'prog-under'; const tl=k.sublabel?`cap ${k.target}${k.unit} · ${k.sublabel}${k.unit}`:`target ${k.target}${k.unit}`; return `
${k.label}
${k.actual}${k.unit}${tl}
`; }).join('')}
`; const alerts=buildAlerts(res,asmm,goals); document.getElementById(pfx+'Alerts').innerHTML=alerts.length?`
${alerts.join('')}
`:''; const activeRows=FEEDS.filter(f=>(res.schedule[f.key]||0)>0.01); document.getElementById(pfx+'RationWrap').innerHTML=`
${activeRows.map(f=>{const wet=res.schedule[f.key];const dm=wet*(FEED_MATRIX[f.key]?.dm||f.dm);return``;}).join('')} ${asmm.dose_g>0?``:''}
FeedWet kgDM kgTDN gCP gCa gP gCost ₹
${f.name}${wet.toFixed(1)}${dm.toFixed(3)}${(dm*f.tdn*10).toFixed(0)}${(dm*f.cp*10).toFixed(0)}${(dm*f.ca*10).toFixed(2)}${(dm*f.p*10).toFixed(2)}${(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)}
ASMM (Mineral Mix)${(asmm.dose_g/1000).toFixed(3)}${asmm.ca.toFixed(2)}${asmm.p.toFixed(2)}${(asmm.dose_g*(customFeedCosts['asmm']||ASMM.cost_per_g)).toFixed(1)}
Total${activeRows.reduce((s,f)=>s+(res.schedule[f.key]||0),0).toFixed(1)}${res.dmiD.toFixed(3)}${res.tdnD.toFixed(0)}${res.cpD.toFixed(0)}${totalCaG.toFixed(2)}${totalPG.toFixed(2)}${calcRationCost(res.schedule,asmm.dose_g).toFixed(1)}
`; document.getElementById(pfx+'AsmmBox').innerHTML=asmm.dose_g>0 ?`
Mineral Supplement (ASMM)
Feed Ca: ${res.caFeedD.toFixed(1)}g | Feed P: ${res.pFeedD.toFixed(1)}g | Targets — Ca: ${goals.caTargetG.toFixed(1)}g, P: ${goals.pTargetG.toFixed(1)}g
Recommended ASMM: ${asmm.dose_g}g/day (driven by ${asmm.caDriver} deficit) → Total Ca: ${totalCaG.toFixed(1)}g, P: ${totalPG.toFixed(2)}g${totalCaG` :`
No Mineral Supplement Needed
Feed-sourced Ca: ${res.caFeedD.toFixed(1)}g (target ${goals.caTargetG.toFixed(1)}g) | P: ${res.pFeedD.toFixed(1)}g (target ${goals.pTargetG.toFixed(1)}g)
Both Ca and P fully met by feed alone — no ASMM required today.
`; if(which==='baseline')baselineResult={...baselineResult,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD,asmm}; if(which==='custom')customResult={...customResult,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD,asmm}; } // ─── CUSTOMISE ─────────────────────────────────────────────────────────────── function openCustomize(){collapseStep('step3Section');renderCustomizeList();openStep('step4Section');} function renderCustomizeList(){ const res=baselineResult; const bc={'Roughage':'cat-roughage','Green Cereal':'cat-green','Green Legume':'cat-green','Silage':'cat-silage','Hydroponic':'cat-hydro','Concentrate':'cat-conc','Grain':'cat-conc'}; let html=''; CAT_GROUPS.forEach(grp=>{ const feeds=FEEDS.filter(f=>grp.cats.includes(f.cat)&&baselineChecked[f.key]!==false);if(!feeds.length)return; html+=`
${grp.label}
`; feeds.forEach(f=>{ const suggested=res.schedule[f.key]||0; const qty=customQtyMap[f.key]!==undefined?customQtyMap[f.key]:suggested; const locked=customLockMap[f.key]||false; html+=`
0?'checked':''} onchange="toggleCustCheck('${f.key}',this.checked)" id="custchk_${f.key}">
`; }); html+=`
`; }); document.getElementById('customizeFeedList').innerHTML=html; const hmfInBaseline=(baselineResult.schedule['hmf']||0)>0.1; document.getElementById('hmfPromptWrap').classList[hmfInBaseline?'add':'remove']('hidden'); if(hmfInBaseline)document.getElementById('hmfCheck').checked=true; } function toggleCustCheck(key,val){ const row=document.getElementById('custrow_'+key); if(row){val?row.classList.add('checked'):row.classList.remove('checked');} if(!val){customQtyMap[key]=0;document.getElementById('custqty_'+key).value='0.0';} else if(!customQtyMap[key]||customQtyMap[key]===0){ customQtyMap[key]=Math.round((baselineResult.schedule[key]||0.5)*10)/10; document.getElementById('custqty_'+key).value=customQtyMap[key].toFixed(1); } } function onCustQtyChange(key){ let val=Math.round((parseFloat(document.getElementById('custqty_'+key).value)||0)*10)/10; customQtyMap[key]=val;document.getElementById('custqty_'+key).value=val.toFixed(1); const row=document.getElementById('custrow_'+key); if(row){val>0?row.classList.add('checked'):row.classList.remove('checked');} } function toggleLock(key){ customLockMap[key]=!customLockMap[key]; const btn=document.getElementById('lockbtn_'+key),inp=document.getElementById('custqty_'+key); if(customLockMap[key]){ btn.classList.add('active');btn.textContent=' Locked';inp.classList.add('locked'); const snapped=Math.round((parseFloat(inp.value)||0)*10)/10; customQtyMap[key]=snapped;inp.value=snapped.toFixed(1); }else{btn.classList.remove('active');btn.textContent='Lock';inp.classList.remove('locked');} } function enforceRoughageFloor(cR,baseRes,lockedFeeds,dmiMaxG){ if(!baseRes||(lockedFeeds['hmf']||0)<=0)return; const baseRoughWet=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(baseRes.schedule[f.key]||0),0); const custRoughWet=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(cR.schedule[f.key]||0),0); if(custRoughWetf.cat==='Roughage'&&!lockedFeeds[f.key]).sort((a,b)=>a.cost-b.cost)[0]?.key; if(cheapKey){ const roomDm=dmiMaxG?Math.max(0,dmiMaxG-cR.dmiD):999; const addWet=Math.min(deficit,roomDm/FEED_MATRIX[cheapKey].dm); if(addWet>0.01){ cR.schedule[cheapKey]=(cR.schedule[cheapKey]||0)+addWet; const addDm=addWet*FEED_MATRIX[cheapKey].dm; cR.dmiD+=addDm;cR.tdnD+=addDm*FEED_MATRIX[cheapKey].tdn*1000; cR.cpD+=addDm*FEED_MATRIX[cheapKey].cp*1000;cR.ndfD+=addDm*FEED_MATRIX[cheapKey].ndf; cR.caFeedD+=addDm*FEED_MATRIX[cheapKey].ca*1000;cR.pFeedD+=addDm*FEED_MATRIX[cheapKey].p*1000; } } } } function recalculateCustom(){ const locked={}; FEEDS.forEach(f=>{ const raw=parseFloat(document.getElementById('custqty_'+f.key)?.value)||0; const val=Math.round(raw*10)/10; customQtyMap[f.key]=val; if(customLockMap[f.key])locked[f.key]=val; }); const hmfBox=document.getElementById('hmfCheck'); if(hmfBox&&hmfBox.checked&&locked['hmf']===undefined){ const hq=Math.round((parseFloat(document.getElementById('hmfQtyInput')?.value)||5)*10)/10; locked['hmf']=hq;customQtyMap['hmf']=hq; } const checkedSet=new Set(FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key)); Object.keys(locked).forEach(k=>checkedSet.add(k)); // When HMF is locked, reduce green cereal caps proportionally so HMF displaces greens // rather than stacking on top of them. HMF DM displaces green DM 1:1, up to 60% of each green. let bCaps=baselineResult?{...baselineResult.schedule}:null; if(bCaps&&locked['hmf']){ const hmfDm=locked['hmf']*FEED_MATRIX['hmf'].dm; let hmfDisplaceDm=hmfDm; const greenKeys=Object.keys(bCaps) .filter(k=>FEED_MATRIX[k]&&(FEED_MATRIX[k].cat==='Green Cereal'||FEED_MATRIX[k].cat==='Silage')&&(bCaps[k]||0)>0) .sort((a,b)=>(bCaps[b]||0)-(bCaps[a]||0)); for(const k of greenKeys){ if(hmfDisplaceDm<=0)break; const capDm=(bCaps[k]||0)*FEED_MATRIX[k].dm; const reduceDm=Math.min(capDm*0.6,hmfDisplaceDm); bCaps[k]=Math.max(0,(bCaps[k]||0)-reduceDm/FEED_MATRIX[k].dm); hmfDisplaceDm-=reduceDm; } } let res=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:bCaps},locked,Array.from(checkedSet)); const lpC=lpRefine(res,{...goalsState,_baselineCaps:bCaps},Array.from(checkedSet),locked); if(lpC) res=lpC; enforceRoughageFloor(res,baselineResult,locked,goalsState.dmiMax); const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...res,asmm,goals:goalsState}; document.getElementById('step4Num').classList.add('done'); collapseStep('step4Section');renderDietSection('custom',res,asmm,goalsState);openStep('step5Section'); document.getElementById('exportSection').classList.remove('hidden'); } function getMilkPrice(){ if(milkPricePerLitre)return milkPricePerLitre; const fat=goalsState.fat||4.0; if(goalsState.animalType==='buffalo')return(fat/100)*875*1.03; const snf=8.5,eqFat=fat+(snf*(2/3)); return(eqFat/100)*388.65*1.03; } function openEconomics(which){ if(which==='baseline'){document.getElementById('econBaselineContent').innerHTML=renderEconContent('baseline',null);openStep('econBaselineSection');} else{document.getElementById('econCustomContent').innerHTML=renderEconContent('custom','baseline');openStep('econCustomSection');} document.getElementById('exportSection').classList.remove('hidden'); } function renderEconContent(which,compareWith){ const r=which==='baseline'?baselineResult:customResult;if(!r)return '

No data yet.

'; const ls=goalsState.lifeStage,isLac=ls==='lactating',milk=goalsState.milk||0,bw=goalsState.bw||450; const mp=getMilkPrice(),dailyInc=milk*mp; const cost=calcRationCost(r.schedule,r.asmm.dose_g); const cpl=milk>0?cost/milk:0,costPct=dailyInc>0?cost/dailyInc*100:0; let breakdown=''; FEEDS.forEach(f=>{const wet=r.schedule[f.key]||0;if(wet<0.01)return;breakdown+=`
${f.name} (${wet.toFixed(1)} kg)₹${(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)}
`;}); if(r.asmm.dose_g>0)breakdown+=`
ASMM (${r.asmm.dose_g}g)₹${(r.asmm.dose_g*ASMM.cost_per_g).toFixed(1)}
`; let costInputs=`
Feed Costs (₹/kg)
Prices set in Step 2.1. Update here and hit Re-optimise to rebuild the diet at new costs.
`; const availableKeys=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); FEEDS.filter(f=>availableKeys.includes(f.key)).forEach(f=>{const wet=r.schedule[f.key]||0;const inDiet=wet>=0.01;costInputs+=`
`;}); if(isLac)costInputs+=`
`; costInputs+=`
`; const dirty=window['costsDirty_'+which]||false; const reoptBtn=dirty?`
Costs changed — diet not yet re-optimised
Re-run to get cheapest balanced diet at new costs.
`:''; let summaryMetrics=''; if(isLac){ const bCls=costPct<80?'econ-banner good':'econ-banner warn'; const bMsg=costPct<80?` Feed cost is ${costPct.toFixed(1)}% of daily milk income — within healthy range.`:` Feed cost is ${costPct.toFixed(1)}% of daily milk income — above 80% threshold.`; summaryMetrics=`
Total feed cost/day₹${cost.toFixed(1)}
Milk income/day₹${dailyInc.toFixed(1)}
Cost per litre milk₹${cpl.toFixed(2)}
Cost as % of income${costPct.toFixed(1)}%
${bMsg}
`; }else{ const stageLabel=ls==='calf'?'Heifer / Calf':ls==='dry'?'Dry Cow':ls==='pregnant_t1'?'Pregnant T1':ls==='pregnant_t2'?'Pregnant T2':'Pregnant T3'; summaryMetrics=`
Total feed cost/day₹${cost.toFixed(1)}
Cost per kg body weight₹${(cost/bw).toFixed(2)}
Monthly feed cost (est.)₹${(cost*30).toFixed(0)}
Daily maintenance cost for ${stageLabel}: ₹${cost.toFixed(1)}/day
`; } let compareHtml=''; if(compareWith==='baseline'&&baselineResult){ const costB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const savDay=costB-cost,savMo=savDay*30; const savCol=savMo>=0?'var(--green-dark)':'var(--red)'; const savSign=savMo>=0?'+':'-'; let cardRows=''; if(isLac){ const cplB=milk>0?costB/milk:0,pctB=dailyInc>0?costB/dailyInc*100:0; cardRows=`
Baseline Diet
Daily feed cost₹${costB.toFixed(1)}
Cost per litre₹${cplB.toFixed(2)}
% of milk income${pctB.toFixed(1)}%
Customised Diet
Daily feed cost₹${cost.toFixed(1)}
Cost per litre₹${cpl.toFixed(2)}
% of milk income${costPct.toFixed(1)}%
`; }else{ cardRows=`
Baseline Diet
Daily feed cost₹${costB.toFixed(1)}
Monthly cost₹${(costB*30).toFixed(0)}
Customised Diet
Daily feed cost₹${cost.toFixed(1)}
Monthly cost₹${(cost*30).toFixed(0)}
`; } if(isLac&&savDay<0){ compareHtml=`
Note on customised diet cost
There is some additional monthly cost with customised diet having HMF. The slightly extra amount provides year-round fodder availability, better digestibility and improved Ca:P balance.
`; }else{ compareHtml=`
${cardRows}
${savMo>=0?'Monthly savings with customised diet':'Additional monthly cost with customised diet'}
${savSign}₹${Math.abs(savMo).toFixed(0)}/month
`; } } return `
${reoptBtn}${breakdown}
${summaryMetrics}${compareHtml}
${costInputs}
`; } function markCostsDirty(which){window['costsDirty_'+which]=true;updateEcon();} function updateEcon(){ if(!document.getElementById('econBaselineSection').classList.contains('hidden'))document.getElementById('econBaselineContent').innerHTML=renderEconContent('baseline',null); if(!document.getElementById('econCustomSection').classList.contains('hidden'))document.getElementById('econCustomContent').innerHTML=renderEconContent('custom','baseline'); } function reoptimiseDiet(which){ if(which==='baseline'&&goalsState&&baselineResult){ const checked=baselineResult.checked||FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); const res=runSolverCore({...goalsState,_costMinimise:true},{},checked); const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...res,asmm,goals:goalsState,checked}; renderDietSection('baseline',res,asmm,goalsState); if(customResult){ const cL={};Object.entries(customQtyMap).forEach(([k,v])=>{if(customLockMap[k])cL[k]=v;}); const custFeeds=[...new Set([...checked,...Object.keys(cL)])]; let bCapsR={...res.schedule}; if(cL['hmf']){ const hmfDm=cL['hmf']*FEED_MATRIX['hmf'].dm;let hmfDisplaceDm=hmfDm; const gk=Object.keys(bCapsR).filter(k=>FEED_MATRIX[k]&&(FEED_MATRIX[k].cat==='Green Cereal'||FEED_MATRIX[k].cat==='Silage')&&(bCapsR[k]||0)>0).sort((a,b)=>(bCapsR[b]||0)-(bCapsR[a]||0)); for(const k of gk){if(hmfDisplaceDm<=0)break;const rd=Math.min((bCapsR[k]||0)*FEED_MATRIX[k].dm*0.6,hmfDisplaceDm);bCapsR[k]=Math.max(0,(bCapsR[k]||0)-rd/FEED_MATRIX[k].dm);hmfDisplaceDm-=rd;} } const cR=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:bCapsR},cL,custFeeds); enforceRoughageFloor(cR,{...res},cL,goalsState.dmiMax); const cA=computeAsmm(cR.caFeedD,cR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...cR,asmm:cA,goals:goalsState};renderDietSection('custom',cR,cA,goalsState); } window['costsDirty_baseline']=false;window['costsDirty_custom']=false;updateEcon(); setTimeout(()=>{try{document.getElementById('step3Section').scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},150); }else if(which==='custom'&&goalsState&&customResult){ const cL={};Object.entries(customQtyMap).forEach(([k,v])=>{if(customLockMap[k])cL[k]=v;}); const chk=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); const bR=runSolverCore({...goalsState,_costMinimise:true},{},chk); const bA=computeAsmm(bR.caFeedD,bR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...bR,asmm:bA,goals:goalsState,checked:chk}; const custFeeds2=[...new Set([...chk,...Object.keys(cL)])]; let bCapsC={...bR.schedule}; if(cL['hmf']){ const hmfDm=cL['hmf']*FEED_MATRIX['hmf'].dm;let hmfDisplaceDm=hmfDm; const gk=Object.keys(bCapsC).filter(k=>FEED_MATRIX[k]&&(FEED_MATRIX[k].cat==='Green Cereal'||FEED_MATRIX[k].cat==='Silage')&&(bCapsC[k]||0)>0).sort((a,b)=>(bCapsC[b]||0)-(bCapsC[a]||0)); for(const k of gk){if(hmfDisplaceDm<=0)break;const rd=Math.min((bCapsC[k]||0)*FEED_MATRIX[k].dm*0.6,hmfDisplaceDm);bCapsC[k]=Math.max(0,(bCapsC[k]||0)-rd/FEED_MATRIX[k].dm);hmfDisplaceDm-=rd;} } const cR=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:bCapsC},cL,custFeeds2); enforceRoughageFloor(cR,bR,cL,goalsState.dmiMax); const cA=computeAsmm(cR.caFeedD,cR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...cR,asmm:cA,goals:goalsState};renderDietSection('custom',cR,cA,goalsState); renderDietSection('baseline',bR,bA,goalsState); window['costsDirty_baseline']=false;window['costsDirty_custom']=false;updateEcon(); setTimeout(()=>{try{document.getElementById('step5Section').scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},150); } } function exportPDF(){ if(!goalsState){showToast('Please calculate a diet first.');return;} const g=goalsState, milk=g.milk||0, bw=g.bw||450, isLac=g.lifeStage==='lactating'; const mp=getMilkPrice(); const aLabel=g.animalType==='cow_crossbred'?'Crossbred Cow':g.animalType==='cow_indigenous'?'Indigenous Cow':'Buffalo'; const date=new Date().toLocaleDateString('en-IN',{day:'2-digit',month:'long',year:'numeric'}); const logo='https://www.shunya.live/wp-content/uploads/2026/02/Sunya_Logo_highres-1-scaled.png'; function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');} function rationRows(res){ if(!res)return ''; const ar=FEEDS.filter(function(f){return(res.schedule[f.key]||0)>0.01;}); let rows=ar.map(function(f){ const wet=res.schedule[f.key]; const dm=wet*(FEED_MATRIX[f.key]?FEED_MATRIX[f.key].dm:f.dm); return ''+esc(f.name)+''+wet.toFixed(1)+''+dm.toFixed(3)+''+(dm*f.tdn*10).toFixed(0)+''+(dm*f.cp*10).toFixed(0)+'₹'+(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)+''; }).join(''); if(res.asmm&&res.asmm.dose_g>0){ rows+='ASMM '+res.asmm.dose_g+'g/day'+(res.asmm.dose_g/1000).toFixed(3)+'———₹'+(res.asmm.dose_g*ASMM.cost_per_g).toFixed(1)+''; } const tc=calcRationCost(res.schedule,res.asmm?res.asmm.dose_g:0); const totalWet=ar.reduce(function(s,f){return s+(res.schedule[f.key]||0);},0); rows+='TOTAL'+totalWet.toFixed(1)+''+res.dmiD.toFixed(3)+''+res.tdnD.toFixed(0)+''+res.cpD.toFixed(0)+'₹'+tc.toFixed(1)+(isLac&&milk>0?' (₹'+(tc/milk).toFixed(2)+'/L)':'')+''; return rows; } let savingsHtml=''; if(baselineResult&&customResult){ const cB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const cC=calcRationCost(customResult.schedule,customResult.asmm.dose_g); const sD=cB-cC, sM=sD*30, sY=sD*365, sg=sD>=0?'+':'-'; const col=sD>=0?'#166534':'#991b1b'; savingsHtml='
' +'
Savings Summary
' +'' +'' +(isLac&&milk>0?'':'') +'' +'' +'
MetricBaselineCustomisedChange
Daily Feed Cost₹'+cB.toFixed(1)+'₹'+cC.toFixed(1)+''+sg+'₹'+Math.abs(sD).toFixed(1)+'/day
Cost per Litre₹'+(cB/milk).toFixed(2)+'₹'+(cC/milk).toFixed(2)+''+sg+'₹'+Math.abs(cB/milk-cC/milk).toFixed(2)+'/L
Monthly Saving30 days'+sg+'₹'+Math.abs(sM).toFixed(0)+'
Annual Saving365 days'+sg+'₹'+Math.abs(sY).toFixed(0)+'
'; } let milkEconHtml=''; if(isLac&&milk>0&&baselineResult){ const cB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const dailyInc=milk*mp, pctB=dailyInc>0?cB/dailyInc*100:0; const col=pctB<80?'#166534':'#c0392b'; milkEconHtml='
' +'
Milk Economics
' +'' +'' +'' +'' +'
Milk income/day₹'+dailyInc.toFixed(1)+' (@₹'+mp.toFixed(2)+'/L × '+milk+'L)
Feed cost per litre₹'+(cB/milk).toFixed(2)+'
Feed cost % of income'+pctB.toFixed(1)+'% '+(pctB<80?'✓ Healthy':'⚠ Above 80%')+'
'; } const html='' +'Shunya FeedRight Diet Report' +'' +'
' +'' +'
Shunya Feed Right
'+esc(date)+'
' +'
' +'

Diet Advisory Report

' +'
ICAR 2013 · NDDB RBP Method
' +'
' +'
Animal:'+esc(aLabel)+'
' +'
Stage:'+esc(g.lifeStage)+'
' +'
Weight:'+bw+' kg
' +(isLac?'
Milk:'+milk+' L/day @ '+g.fat+'% fat
':'') +'
' +'
' +[{l:'DMI',v:g.dmiTarget.toFixed(2)+' kg'},{l:'TDN',v:(g.tdnTargetG/1000).toFixed(2)+' kg'},{l:'CP',v:g.cpTargetG.toFixed(0)+' g'},{l:'Ca',v:g.caTargetG.toFixed(1)+' g'},{l:'P',v:g.pTargetG.toFixed(1)+' g'}] .map(function(k){return'
'+k.l+'
'+k.v+'
';}).join('') +'
' +(baselineResult?'
Baseline Recommended Diet
'+rationRows(baselineResult)+'
FeedWet kgDM kgTDN gCP gCost
':'') +milkEconHtml +(customResult?'
Customised Diet
'+rationRows(customResult)+'
FeedWet kgDM kgTDN gCP gCost
':'') +savingsHtml +'
Generated by Shunya Feed Right BETA · ICAR 2013 / NDDB RBP · Advisory use only. Consult a veterinarian before changing your animal\'s diet.
' +''; // Open in new tab — user prints to PDF / shares from there const win=window.open('','_blank'); if(!win){showToast('Please allow popups for this site, then try again.');return;} win.document.write(html); win.document.close(); // On mobile, trigger print immediately so the share/save dialog appears win.onload=function(){ setTimeout(function(){win.print();},300); }; // If onload already fired (cached) if(win.document.readyState==='complete'){ setTimeout(function(){win.print();},400); } } function showToast(msg){ let t=document.getElementById('sfrToast'); if(!t){t=document.createElement('div');t.id='sfrToast';t.style.cssText='position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1a3a2a;color:#fff;padding:12px 20px;border-radius:8px;font-size:13px;z-index:99999;max-width:90vw;text-align:center;box-shadow:0 4px 16px rgba(0,0,0,0.25);transition:opacity 0.3s';document.body.appendChild(t);} t.textContent=msg;t.style.opacity='1';clearTimeout(t._t);t._t=setTimeout(()=>{t.style.opacity='0';},4000); } // Init onLifeStageChange(); FEEDS.forEach(f=>{baselineChecked[f.key]=true;}); buildSources(); // ── Static element event bindings (replaces all removed inline handlers) ── // Header: Sources button document.getElementById('sourcesModal') && void 0; // modal already exists document.querySelector('.sources-btn') && document.querySelector('.sources-btn').addEventListener('click', function(){ openModal('sourcesModal'); }); // Modal close buttons document.querySelector('#sourcesModal .modal-close') && document.querySelector('#sourcesModal .modal-close').addEventListener('click', function(){ closeModal('sourcesModal'); }); document.querySelector('#infoModal .modal-close') && document.querySelector('#infoModal .modal-close').addEventListener('click', function(){ closeModal('infoModal'); }); // Feed info image expand var infoImgPh = document.getElementById('infoImgPlaceholder'); if(infoImgPh) infoImgPh.addEventListener('click', function(){ expandFeedImage(this); }); // About accordion toggle (function(){ var aboutBtns = document.querySelectorAll('.container > div:first-child button'); aboutBtns.forEach(function(btn){ btn.addEventListener('click', function(){ var panel = this.nextElementSibling; panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; var chev = this.querySelector('.chev'); if(chev) chev.textContent = chev.textContent === '▾' ? '▸' : '▾'; }); }); })(); // Step section header toggles ['step1Section','step2Section','step22Section','step3Section','step4Section','step5Section','econBaselineSection','econCustomSection'].forEach(function(id){ var section = document.getElementById(id); if(!section) return; var header = section.querySelector('.step-header'); if(header) header.addEventListener('click', function(){ toggleStep(id); }); }); // Animal type change var animalTypeEl = document.getElementById('animalType'); if(animalTypeEl) animalTypeEl.addEventListener('change', function(){ var fi = document.getElementById('fatPct'); if(!fi) return; if(this.value==='cow_indigenous') fi.value=4.5; else if(this.value==='cow_crossbred') fi.value=3.5; updateBreedNote(); }); // Life stage change var lifeStageEl = document.getElementById('lifeStage'); if(lifeStageEl) lifeStageEl.addEventListener('change', onLifeStageChange); // Step 1: Confirm Animal Details var confirmBtn = document.querySelector('#step1Section .btn-primary'); if(confirmBtn) confirmBtn.addEventListener('click', confirmAnimalDetails); // Step 2: Confirm Targets -> Select Feeds var step2Btn = document.querySelector('#step2Section .btn-primary'); if(step2Btn) step2Btn.addEventListener('click', openStep22); // Step 2.1: Calculate Baseline var baselineBtn = document.querySelector('#step22Section .btn-primary'); if(baselineBtn) baselineBtn.addEventListener('click', calculateBaseline); // Step 3: Customise + Check Economics (baseline) var customiseBtn = document.querySelector('#step3Section .btn-secondary'); if(customiseBtn) customiseBtn.addEventListener('click', openCustomize); var econBaseBtn = document.querySelector('#step3Section .btn-outline-gray'); if(econBaseBtn) econBaseBtn.addEventListener('click', function(){ openEconomics('baseline'); }); // Step 4: Redesign Diet var redesignBtn = document.querySelector('#step4Section .btn-purple'); if(redesignBtn) redesignBtn.addEventListener('click', recalculateCustom); // Step 5: Check Economics (custom) var econCustomBtn = document.querySelector('#step5Section .btn-purple'); if(econCustomBtn) econCustomBtn.addEventListener('click', function(){ openEconomics('custom'); }); // Export var exportBtnEl = document.getElementById('exportBtn'); if(exportBtnEl) exportBtnEl.addEventListener('click', exportPDF); // Modal overlay click-to-close document.querySelectorAll('.modal-overlay').forEach(function(el){ el.addEventListener('click', function(e){ if(e.target===this) closeModal(this.id); }); }); // ─── FARMER-FACING NUTRIENT-STANDARD TOGGLE (NDDB / ICAR 2013) ──────────────── // Flips the recommended (baseline) diet between the two requirement standards in // place. Default = NDDB (live behaviour). ICAR raises protein/energy targets and // may be unachievable for high yielders — the existing Protein/Energy shortfall // alerts fire normally in ICAR mode so a farmer is never shown a bad ration as OK. function rerunBaselineInPlace(){ if(!goalsState) return; const checked=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); let res=runSolverCore({...goalsState,_costMinimise:true},{},checked); const lp=lpRefine(res,goalsState,checked,{}); if(lp) res=lp; const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...res,asmm,goals:goalsState,checked}; renderDietSection('baseline',res,asmm,goalsState); } function setReqBasis(basis){ window.SFR_REQ_BASIS=(basis==='icar'?'icar':'nddb'); const nb=document.getElementById('basisBtnNddb'),ib=document.getElementById('basisBtnIcar'),note=document.getElementById('basisNote'); if(nb&&ib){ const active=window.SFR_REQ_BASIS==='icar'?ib:nb, idle=window.SFR_REQ_BASIS==='icar'?nb:ib; active.style.background='var(--green,#2bb536)';active.style.color='#fff';active.style.fontWeight='700'; idle.style.background='var(--white,#fff)';idle.style.color='var(--gray-500,#6b7280)';idle.style.fontWeight='600'; } if(note) note.textContent=window.SFR_REQ_BASIS==='icar' ?'ICAR 2013 standard — stricter targets; some high-yield animals may show a shortfall.' :'NDDB Ration Balancing Programme (recommended).'; if(goalsState){ goalsState=computeGoals(); renderStep2(); if(baselineResult) rerunBaselineInPlace(); } } function toggleBasisAdv(){ const p=document.getElementById('basisAdvPanel'),c=document.getElementById('basisAdvChev'); if(!p) return; const open=p.style.display!=='none'; p.style.display=open?'none':'block'; if(c) c.textContent=open?'\u25B8':'\u25BE'; } function injectBasisToggle(){ const anchor=document.getElementById('baselineKpis'); if(!anchor||document.getElementById('basisToggleBar')) return; const bar=document.createElement('div'); bar.id='basisToggleBar'; bar.style.cssText='margin-bottom:14px;'; // Collapsed by default: farmers see only a subtle "Advanced" link. bar.innerHTML='' +''; anchor.parentNode.insertBefore(bar,anchor); document.getElementById('basisAdvBtn').addEventListener('click',toggleBasisAdv); document.getElementById('basisBtnNddb').addEventListener('click',function(){setReqBasis('nddb');}); document.getElementById('basisBtnIcar').addEventListener('click',function(){setReqBasis('icar');}); } try{ injectBasisToggle(); }catch(e){}