From b08948ede1627c23a1f5ff16944f21130c14bb07 Mon Sep 17 00:00:00 2001 From: Maria Oros Date: Tue, 19 May 2026 11:55:59 -0500 Subject: [PATCH 1/2] ADD blog content --- _toc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_toc.yml b/_toc.yml index 79dea18..7f0dc25 100644 --- a/_toc.yml +++ b/_toc.yml @@ -13,4 +13,4 @@ chapters: title: Blog sections: - file: blog/post/2026-05-18-hello-world - - file: blog/post/2026-05-18-agforecast-case-study \ No newline at end of file + - file: blog/post/2026-05-18-ag-forecasting-case-study \ No newline at end of file From 770e14893f8805157cdc67a7dba29e7182026ea6 Mon Sep 17 00:00:00 2001 From: Maria Oros Date: Tue, 19 May 2026 12:41:31 -0500 Subject: [PATCH 2/2] ADD blog content --- .gitignore | 1 + _build/.doctrees/README.doctree | Bin 7045 -> 0 bytes _build/.doctrees/ag-forecasting-api.doctree | Bin 152871 -> 0 bytes _build/.doctrees/blog/index.doctree | Bin 4056 -> 3372 bytes ...26-05-18-ag-forecasting-case-study.doctree | Bin 7729 -> 47712 bytes _build/.doctrees/environment.pickle | Bin 59412 -> 141418 bytes _build/.doctrees/worker.doctree | Bin 16815 -> 16763 bytes _build/html/.buildinfo | 2 +- _build/html/README.html | 510 ------ _build/html/_sources/README.md | 45 - _build/html/_sources/ag-forecasting-api.md | 932 ---------- _build/html/_sources/blog/index.md | 13 +- .../2026-05-18-ag-forecasting-case-study.md | 126 +- _build/html/_sources/worker.md | 2 +- _build/html/ag-forecasting-api.html | 1627 ----------------- .../html/applications/ag-forecasting-api.html | 63 +- _build/html/applications/index.html | 59 +- _build/html/blog.html | 49 +- _build/html/blog/2026.html | 49 +- _build/html/blog/2026/atom.xml | 2 +- _build/html/blog/archive.html | 49 +- _build/html/blog/atom.xml | 2 +- _build/html/blog/author.html | 57 +- _build/html/blog/author/maria-oros.html | 49 +- _build/html/blog/author/maria-oros/atom.xml | 2 +- _build/html/blog/author/openlambda-team.html | 49 +- .../html/blog/author/openlambda-team/atom.xml | 2 +- .../html/blog/author/tyler-caraza-harter.html | 49 +- .../blog/author/tyler-caraza-harter/atom.xml | 2 +- _build/html/blog/category.html | 49 +- _build/html/blog/category/case-studies.html | 49 +- .../html/blog/category/case-studies/atom.xml | 2 +- _build/html/blog/category/releases.html | 49 +- _build/html/blog/category/releases/atom.xml | 2 +- _build/html/blog/drafts.html | 49 +- _build/html/blog/index.html | 77 +- _build/html/blog/language.html | 49 +- _build/html/blog/language/en.html | 49 +- _build/html/blog/language/en/atom.xml | 2 +- .../2026-05-18-ag-forecasting-case-study.html | 193 +- .../blog/post/2026-05-18-hello-world.html | 63 +- _build/html/blog/tag.html | 49 +- _build/html/blog/tag/agforecast.html | 49 +- _build/html/blog/tag/agforecast/atom.xml | 2 +- _build/html/blog/tag/announcement.html | 49 +- _build/html/blog/tag/announcement/atom.xml | 2 +- _build/html/blog/tag/asgi.html | 49 +- _build/html/blog/tag/asgi/atom.xml | 2 +- _build/html/blog/tag/case-study.html | 49 +- _build/html/blog/tag/case-study/atom.xml | 2 +- _build/html/blog/tag/fastapi.html | 49 +- _build/html/blog/tag/fastapi/atom.xml | 2 +- _build/html/blog/tag/openlambda.html | 49 +- _build/html/blog/tag/openlambda/atom.xml | 2 +- _build/html/genindex.html | 54 +- _build/html/index.html | 54 +- _build/html/objects.inv | Bin 488 -> 445 bytes _build/html/search.html | 54 +- _build/html/searchindex.js | 2 +- _build/html/worker.html | 61 +- _config.yml | 12 +- _toc.yml | 3 - blog/index.md | 13 +- .../2026-05-18-ag-forecasting-case-study.md | 2 +- worker.md | 2 +- 65 files changed, 375 insertions(+), 4611 deletions(-) delete mode 100644 _build/.doctrees/README.doctree delete mode 100644 _build/.doctrees/ag-forecasting-api.doctree delete mode 100644 _build/html/README.html delete mode 100644 _build/html/_sources/README.md delete mode 100644 _build/html/_sources/ag-forecasting-api.md delete mode 100644 _build/html/ag-forecasting-api.html diff --git a/.gitignore b/.gitignore index 9f11b75..5ef4674 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +_build/.DS_Store \ No newline at end of file diff --git a/_build/.doctrees/README.doctree b/_build/.doctrees/README.doctree deleted file mode 100644 index 098f02a24a2f435bb1875ba26595cc7724f0c53d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7045 zcmeHMUvC}76_1nHv9JHd0SsyqI*=0Vmc5rEL}}hAP$(gWlC-LXqSfBrxp&6i-C1^K zW4jVk`Vfevd8ouzr4kY!0I8nnlL+7s9j3K>T^Ps^L7U7_tp#=2X7e@64rNR^I07qencoVi(Uhy^=qq=|WJ$#&n# zc=~pb^rL{iAFlxn}onm+ad9^&9M3kncolH)YqQ;U#ebqtiF|9!dDn-`^=K zo#wIt8kkb7*#&>3vN&buYE8H7=~$+`+$#Ek?F7c;vR@cjh%9nSMw+xg6UKqoAmVv8 z4U+0DOFsyAaz3Cvg*{_}AuW|+RXllhMqIQvdOy=V*S#djWuS7Ud#}|OfTzF& z*F(wEkpCprupI>Gz3}`CFZC3x=Ecrz-Jvw1=y#>+z4!VnuU&h+n?$8JFP6jwajNLU zSPRfK80JAH+Lr(04Iwqld8SyzvsmqG78FJ$fstVl$NS8v$r~xOOo#0Kw~B1v@SOcf zshyh}!kA27>GifNY9GX|%7>esT}Z{=pSZ#C?qpvZ4^lz%Zq9?q2#!0@q{Ai)G{ZYH zJQOL3!kX)WWGZE9z{qD<VP!?kgE3H-i^ z1u<)v+6Jinaeaz7X8FT5%Rqb^&Lf_J2VcV9Gx&QJf17ZgIUH5wvb2vMVbHP=-xEK8 z&p<%&Tq7*RT(>iwBXwA}v+f>_-0*T0;!taO;;I_Grl@1Ze)?dFi8t(}Mx$dc{TS_f z59<$O87J`j&M45-aQl^UQb+n8{s%@#W!TLyC2-B05CG9-5G0Anye zXPO%W@X>8XMukUm0c&D(Bk04f0E}8L%{GweSfX;yNM6kZ;=3(g&Oaz*&ME)tuGurw zG7{H!_l*D!0W>umsN8{7zd%ZCNk^b8hbayMv8+v5BXzGsT*u^HmG2B>_la8cFKX~TCJ$_Wzsp{YRf^&V1&}Mvf;0kpWvIpk?upS2Xkc<9{X={)R^Sg0 zr@*&r1-`5m__(<{`xl@b3!T7ClCO)=P$lgewf#J zpAs6g3D;+ysvPH-8l#z*i0AC1u3?H@qwZ(98lt$UCML5+!O_aACv4SJVGob+T2Ro= z*p;xzX&&Q`rY}nkrJBOn`KAXb?HUaP+{_6Xff|ko4fM~0xWM?OY1HkUDoj?Ga!agk z*@Yob(e40d*~CA@~&M$i6k@hK#A)x!0>!U;c?Y-_|&fZ z%~T2J)gRPjrY*Zf_?a6&=C?S^dD3=75RYDG?Hb>cu%Zb}p_@&!b~fVuVu<5bc7r@c zCxsoDyCk42;L1WyDv^9Lh0T17JXC~TC>0Ac@Lq-k;`zelqR;bO<-Pzc$2`XkkfFu5@Gu{}#VXc$VsM6oMwmz(Mr7LQQu zp2W=3K9ciL<&lq037&&vB;01p&Q*;mSwu#SLx?%MLPIfsOtL2>+-*53lFT2uBn;~a zC`fpo1~D;Qbrcs6bo*mcrg;%5A4NEJo#lI((%f%<>AWN~{w;~XcP_i?M^aNWTer{n z@Jtgb2)iXHgYjVM;tp%e^)}xnRvJ#O(-RH>VAC`McSGDFT=^n~yQ9~uGj@&Q4W5$@ z;fS$B-BeyX`>b8bc;=7hVWi9wQBVVLpF+bv(NusS>n=uw=#gm>h=_WOb~*vD(>FZ9 z^#IV&d+^MA=J^+4&y{=-PO&G-nEmC3J%TqNm)*hWP;xVHX8;hY)B}$-K_5|skz#pu z-d==HCjKZmM+*2=`*UE=>iyIJO@$na7=Q5J(Ja8j&H}|e#-~isELYWTW12rz3BcRYle`=ddkPL8E zPz56mq<2cVOw#8OB6uIYfZ*KVjK@$b1v@@Z0vY=#d|(xz1V5Fv=Mq_5&p7`m;G zP7z1D7bZ9^G^e_c&Q0(~mdi~E%zA8^SG|H_1HkS4lrq#}sNxt74jquX&%8?h3;HK8 z)1C4?fROt|!pE;Tnu;(A?sFsjo**xd$S!miWg5msM4>vQuR0N>m>P|#ezF}ZxqKZL KL$Vxxt@#J9HAEo* diff --git a/_build/.doctrees/ag-forecasting-api.doctree b/_build/.doctrees/ag-forecasting-api.doctree deleted file mode 100644 index 1cf61adbeeeffb2e294b775987a0967133affb07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152871 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≪HNCM{h-*!L z8w|U#R{CSuU7`FT?FGtKJXekRr@oWgSwG7thu-Q(mf`l?TTvnyUG?OXf{psyVDjEY zU~)Ak*xBwkA__&yvcH_$jC6lM+Tl7D1pdTC_4uf${ z&o=@Z+6Um2o}=rFNGVUZgo;>qk+v9aM>wT(2=w$?ovD0yy#kjVRp3)8aj1Vj#*?T% zTx)g9JdS)bG+&4{)PGP}TG8NP&uFM&|G;ADGuUu@&eu7P2`Y&axE61>Q6N+c+*gc< zy}ous0Q7k~W2~f&*fWrjm{PV{4 zEqCTBrAm9=%iSlW4z6e@-IbZrHdDG?kWL+dbfNB;069_mvTZxn$(fRB4RPRYGFv@5 zGSinpP963xe8-P?4~^f)ik=C^(0?>D{T-(NNCx@^$k5+^cqF|ka!ho^j1}v0?oPbW z)M;bZpeb9Ki*Pqg`T9a`Zv4V?@7RWGsKqK?j6hlyx{0eFGlxb$EE&=+XK)Vmj$N_&q+leLRufABpiQ%I>RGJIrhS%TWIdF<>V~&oiUBYsKg_ zF;q<_T+N-u(!+;^TdA0-saSxVSbWd$@b*zM&9J5h!e`<#rj+r~9lH{l{rMQbqRbvL zEi%XLPlh8GV$Gxy$8*Wk%yvdXvHd2^_6X*tC$8(xd+Fv6GVor2oOplR@bHe&!&v@= z@2`M<_hI=}H%G0yD9cxb4Kcg7LWbOaBo+v z87C1v%OYYVl!#VGaW;K$Eq4h?2ZmLK+1<76(1jIdO@be=W*w5;qzl7@H#sWpO}TPR z98;FlEUO>zTbK)08!4{9I}SB*9p`A^Plx`i1pixF6~j(Hi^wQH(y?!4Oy3|1Z)x74 z0dBZ`L9(X5WKdb~H*1d$!M?ja`CZa z`iYZaCcb{oH|ZpCcn`;&Z@7g?>3$PjZ;BbQb>_1%vZEt7;!M*rk9;$9{fAgXPNVrP zJenB|HJaCZDKwZK8HUIBU`|SRiJTndMEk24g;7?C(~Rd7cB&bK|321$6N_G87IF8) zBHM|^-?KQ;q*23&tlQV^+d_8>#SW+W&~RCGKt;o#b+<(A(NqR07ut6)cW%<4F1DTk z-5@|I8^$}>T?k4`!8uPQgp2wK)2bNi@|mgoPNr^H5KU(Zxy=N~P}hHCB)W}+KSnz& zdt%q{@ZMIdGZ!WxBwLm)iQ~8(fffk$d~6%-@=onQm|fkgG@F$f73Y~W_Z*5bBs!q3 zvwPurJo?e__IRwRR5tOhugmOZMAYnapBp`k<7wKgLu9VN8k>1v@(E}B9QGv! zo6P%?&nyCuzWb71JfbE}Z+TRvW*d^SFZp94c_67kYtxngjiyD7X5l40%={u1d zV+}bC;dk*6W;E0gz8P0>dLkIC;mnnJI1lEX^pnWDL0gdF7->-+Ic!0;#Tszp&AXX5 z+&%He-h%M=EL#w1)G#9B7Gy`5l!$g02=1J!BdT-TPMn-kE@s@M)p4Gb6cD+uaUBQO z#$@qZ6s#FD$Fs0eZB)LGdH9G1b=jy0&|v{e*{Ixe3JI1eyP>)z(+tj5>Ubt^+fKw} zf5XeRl)WiIN>1-ww1wGuk?m@N~_dO@XeO za6Tc{v1QX|-52}-^U7)?WnXan<7H&ESknVt6oS`%+>k180t}#94e=xpT z*x$h=t+PI|Ss)2WPWulLdbSzv#9Cf{Cl2s$im@ZA3>kJ1562pEQi&hq!Om!?!M@dV zrpR!aTj1GXFqftCL@p27R=hq&ZXoWDSI1=o+zR9OlPXmNIkV`}su=1% zn3=i{GIc+mfjR*))b$_ZiDtj(fYEcys#-lftTF=O#iu?u0)8zK8)_8V`D+G|m^uZcFx0=U8cjCX@~ zTRX?wV+}e925TNX`*fFkTfirIxS?rFnuCFYp z*#ueaz*%ub7JFk2I?3XvSr&|rmPHZ~MiyMzT^7>M0~ur?Kt>iBr+OJ#iBtU!+rzUL zr)VIjxNr{c(mpYUrV#6wkaXu4uc=AAW23GUf|;g9v6pC{|bh!FJ+Oc^7njhA#W&QHdGopkr-nNaRp z>F%|jq03rixDKmlW(UtNJEQ2};4#=_cJTc6BJk+z;OWI9>fq@ukBW7rRvKPfxPF9B6}nb2K#J@ZoNItu)KzW zS=Y=5gY$-16HW~JC1wyKKn${vSNVIE4Usfz@nThX2$_<~g()OoJY%OUcB&O7-uA1{=Fb<#w>~R;xipOl7YkBQ9Un*MuO)o8$HL@0`PjyMY+s{`r zL&|Yy`6`}?qVp%7ZnRoposWGrQ$#J+n9~&TYfJ^B5}zXYdzL9e8nu)?o6qF-dugWF zWTgEUr2SM8DsQF$y;*?LoFibXntJzm`E8@)JBYQ2;mFsg^Ya1n zBSGDepj-2J_HAfV=BMjpkj&wp?}i0Gr%_@Sq*61hx%?^S^3NE8)^%@5#?$o0wYjgs z?3Ru`Bpsz9sMsw)PV634?8ZU{cH^q=gAY6ieycc|sJlFc?KcqH?G~8ty#FI zq~fM@TY#K+ zd*8h+ zIqIzTx0y4u8I(wXW&|i@96>70sCq$Z+7> zGjrfG%zWH^&?f)VX9<0?AA$VD zAQ&FT6U$g2w~vpHd;08z4A^tXkRB%8)Rte=-FRP&6H&%4r!XAhH;9sgd8 zzfpk=8E}@B{mC%w^RZ@}#PlCnOpFAH$v(^D?^z52Y1D8eqd~A?A1(M^5$X7mYQKh| zpmvdToLA#8JK8-DsAib+cUhRDcF&(@&iu~|+9W`KB|s?)?m86(Tn_vyJ^s;xhO~%G z7IVL1L)G4NkNw{$MF{aRjj#*u; z!P^^n50awYNV){p5LOV!$!ZLhc5pPPaLCH5YUg z3f_gcyxn#WD=2#PR6UwX&O<`9<-KB$coY?!s)xtd7`qS)@;>Hp4M@FSTCU?UPjNPm zcsq8Hiy1WAugMUD?uCwRmY+EE2ys3M7s->P1)3h*)xg~BU1 zyoY&=QW(&a&z)+!1(#>>486NlM^aWT6IGEJt$u9sh-Kck-7#hJM)leXNXrc@Me!~2LLDiHRY}kaZ)vV%C=FWf&$W|ZFy=GkM~weO@iX!yH7DjUvnhuJhyyd}@aB!ZRAT6Zv$9fd@Nmc1>U%c5SJccky6-S!^>K20> zvLv_5dmb8rLd@^kX2FGn3xI}?iX5UYXTDkLf%!=t>xW%#-HJPyMF44 zEn&wj^U)qgs4=%|H*VTvjgpbhI;L0h0uN*h|1^oGP8dQ3(Jq?1M=62)gjz)Fg{n(3 z*rNmy(iN>l5?87aHYPu}JSA=QD3xg;q_NP(t3Z3U1m_!=6SpY1keBLDJxARSe%TY8 zD#PRhPo+v{^28QqTy?5IG2mN9Jo$t=^``w{F5K>KdCI(LY%~J3X-so_S`bgrXVt6v z7rz3`6Ar(S_1dMSlgF?c6^dC)OXt$k$pZ!RBOTOJi15rxR*to{@bx1O^wsP26M+u2kyy*?uy2IyIilu9+LdTRq)W=MVFS2{TaQs{jIXY zwcHt75PtNCy}?bgD`IKuOt^555+HK_E2opsLlSO$S=*yGJNFW zV|aA&Y^7PRkw+KTTaU;i#kk@+(V^5nH)waetyBS1q;Wv>-9&sK^7Gh}c@!=Nkf%IfXk!rb|)2s@Rl z>d;bvz9K*=XBf&~P@O0Lhwq)kEAHMy4?UEpR}A6%d1D7rfcRP~A%+7_R?_aW%A_v> z((vN8%)H1kFK*GGE+-!XbR}t5mm@qGgGZki6SSK8F~(8re~Px*_VMH0dk^e12TBP0Ywg8YlQk| z;XkIcb{5&7YKF?EGE@07rtZ*uOPgHNYYD^lh=+shtrcZZV*S8$Xko z8)M9k4+&wZil^2}0dnF-ZeP8Iu!c5ymWkr0>$M>7pYx#$kDlJAKEb;@*AO1K0~pFb zotg4+ru^43P%c1jl#}_}Y}AqDIvDe3@U}Vv1A_2`3x{=hUVUy3ITVcvpK}Hz#)kY;`|8!sT&`tDKcQ9)i zo%npr-?Ple(x_ojM#KEU$=K=B@vM9;T*TpWX_)#@KP+8nwnvh&*nEBqtF!O#^gO`LaJ@Xb9C96RK zT1E$w3k}Xcb%}5utN=pjJB84}3LrpE3UJRPxltw>K~O3ncL~h|)`xy8)M~4aVO;F1 z;n-&u!?6cg%D*AR_ehzGlBK-H!qS@Ws zc{Azd!tIjhbHq5FT# z)CaR$I{J!qG??82#VrS7%1>@F(Kr#_4$;=A%f$Y?Gq0h1{*we;sKz9U|%_t_(kJ!&YElTo0yYU8!6{Uw?AHK73;_Go_+3Q z2iMe#2U0olf)@rcMs!vTqJIJ-ki^03bBr(o4np*7H#kM78qKl zV@;+GJl^KLlF4C&Xlr8MAK~v=wt3R1;ZeqI-XTBQrEEQ+p@4p4f0npkDe2er}9>LmIUZo0ZQ31-1(+$xTlF@y9yo^<8Wl69IjjO zCZ$(06nZRVO^HiNR;Y^~$LUpZfsQ&59R6opA z|5OI51;~l&TlZ5ed*~+ZG;qd-7j)fi3}ZqU+@op>ASK zwsEl$XAQ{SUG0=;H9oQ*0;ZUK4;!AbYHel&L6x0admdrJ!3-3diNCl;42QmynM3=T zLw}TkLjq(tWENegQyZBR%)Dr=9qTA1vERab66&tpY4{xbxd{J0MsalYxUshxW_ePP zVdOF|i*5M@cs{Wp9%NoJQt=gpzh_xNq*24Ej4R09d*hG9!~Ze@qY>(e{g=dT46J9^ zb5|DjsFmXo^X3i>>S7)W(5(WLvU1!GzYsqWpG;v&4MW?`%(NY5+O`RrgVziJa-!{) zQ`%?Zm9^8v%iSlE>r-lIdL%PVN0_F=8E6t9Cz{qj4mSXvn}8nxNtOKbxM+ZNPvv7aNYVnRA`uOJX>|;J^O@;;gy}n*fj$9pqHm+R z^K1EKac3)_40p;r6Rl4W9 z;oqlX4W>@HyvaMkEM#=zOEiDavP4UxhCvyZ=n*y|k!7x}LMtZ6&e66W8$8^(p}i1Y zol<)seLRAF{n{QV>(J{@W9*5#+ivgMJaay!hLOR4i*=GN1MPx4$ueLR?Z;o^)+3)) z3D5=IrHlO_KwlD|6#D^sOcV5+u{>K!6jPn*84s}^cn%||l~n3ZW>OXq!?mG%G8mW2 zdU_4BY^4Tup;g@b>yUnrBzKz$943)l5Z$)VP5-^tcd*Exr&zy>#)Bm!Qm-RK;x{mI=fjraNvn<`}vlRZ8vEY zxIAt&FEahJW|rC7)+nejOl9`({lz>6Uno0FRs)h z;#4uLh1>I#bj?Gwu*VoDUXcWgjJMk6(Uk`qkF9UBhtd2I0F{y~y zKE8cdetcIRZviq4(SW$lqK{nQwHbBYl*mo4$A6hfB#1rpl|(0J}mF z5dg^r%2h^1DoSjs;@e=M1C2^?Wd^bO`w+_A^s9Ci>RR>r_!0Xv1s}BGpaU#1Z2OL# z7TMK$4KJ~TrHZS&?tj24W@?wCg+a;KYvYyT&a#xk8a{ z%oC%+ERr^zTt(lZr%KFANgEm_bMd@qRoC?Ir-q)DEAmSdRMJ$&gLY|xTAHAiCa5rp z$Jcs-3hgpn8-5XHnd?R5<+C{E?tcyW$t%&GF-xs|#~m=I2sW=Ru{>s7wwKNBgKK%Z z7G*p!wPJe^v4h<#_PbBxRz!XRcTaJL?hqc-Y|wK8IdwpfSPk)v4yQiruHBNaVHO<1ZiIvVD=qrYKR8H>#uh|>L* z@kYRjR{jH{mbDw!ilek6RwIUz7@4xQOSJu#R5-S;yiD82B^8FEUt$#&>)^L8opta% zWF44=&$15gvX-p8THmlX=h^9LIYRHYDuxT8TEw|#f;&`JTw^6z9beC$vOE@BaxEHL zM2ct~4vS(22)hi$hndw0`qO$5lm=3qpsUn*+(9_=*uQ9fF)Q)kXIhELB>4!h#D8N< zk`z4dquhqo0KsB>{gx7qiih^q@F>>;337I8Czy}#LTXv-b=iyevnZD(593vpB(n5A(KIjbck}Q7; zcSbGzuOlZv>ek!aSXRa74&~hLv{~pvp}d>H3x(99TlIk%4{%j&~RY-C8oth*7NR1xzpZ|y z>5u`|Px+IEsQGbe=HqbkP^}W>$B@&@I~-Ku^;a_`7ufwi?_GHAhj5e^W>V_I?l&BK)^Q5C$r9dQYIgZ$-{HaP631)&;k5u;2*+^x2-lGEyPo0$X^!Sa* zYu2%SR$l(NmOYPGXQyKcpObXq_I;rnhPq{a32VqZp|A#p)`-Pll(pEV!TQb+f zsXYDPE)#v&Lc&7Ky5(%wessxP*Ec%VASV9df9h}hU-}#UXUSY|x0pvu=6d2-P8`D4 zXid^EQ9n_;C3AhrTo+})GJp9uQcLE#=J(ft*GuO5*N)dq=DIbHL}!vDb6rk|ZiC<5 zF@9wB-yvoF?C;XQK8t_hD#@&s#QtM+K4iT3)-laJMSbXiY}YF1CuO}>5zhHpec)gY zZtmmC@VtcyLg|Ep0Xzc`JI;}V1alB#p|FaqnEW)IznHGnM^Ar@HOJ>Fa=4TAbm;Fh zoDQwsADj)U^@h5gJwY#x@hMS4i8*cAm-)2iD}37WDmiUQ3zJ`s+ms{)%JgdzaK^qG zd`@S^=N}oL*9bltZ+cyKQoXI9`q^(5WCl|pKu#1~b*Lh@I@I3=-xo9E zdj;eBjtuw;kQ2V+`|HIsbc|PT&<#MpI-;H_#O*-J$&ARWr{Sh4nIS2`W!V7@Cw?+B zC$3~p{6q#$2$11~nFiyR0TVMoiT0W?Xppzr5gq&xkplw;qⅈ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 diff --git a/_build/.doctrees/blog/index.doctree b/_build/.doctrees/blog/index.doctree index 8f5d0940f727ca3983c6315e2eb84636f64fdb78..e78e2f9edb09f8774edffc9ab7b105d13a528cae 100644 GIT binary patch delta 133 zcmca1zeb9+fpw}P??%= iIP(&7Q%foffWoy?GEzH>GZcYz!e(P$MaIcf`JDme5ijxp delta 701 zcmZWnJ#5oJ6poYHX`Mg^5CiFtD+IJqx232E20#@^2#PYWFrWYz`!&yzeP?`UnhaIT zK-8$W`4BL&v!EjEC$qvmXbxF zA_;)2nyN}?KNB-97MGWnFStu9?zxLDLKrf4gC(J_I*Q10K)o>aVLb@<>+lhgqY-KO z5DQede(5EP9*IT3s0OtYvhnwaTb3MYgsiT*(!#*^fvO#OXb3nAbmb^bqdFwutF@f8 zMIdw|M#Hw+N%1UtIEnK(wPs)iAEtGJ(*wRUn(K+0lH)_R?nXl57!pPBq)wgId#}6l=DYI!Axm;E(t?20G z(z5YA`)nUIo8}_w#SWKaltdufu@+=g;cl1XjJ`X)52YDnOB-~Ux4D4mK?IzW z1{}0$;Xm7sNjfCrc#H}XQ0^qKu0^39ID$EpQD_BR1hnD!%u6F}q|@JianQmQ?ICHb zvjjZCg{u#6xk&wXfBU6#4|JOaja*Yt)t||!G_Mc1MfrKF(p()pFO5uIF++F^p<2t! nkpWLVId@m@RBu*qC~0w)CLSo9*XlN}>WY5_)*fZw%GZ7a>^=aR diff --git a/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree b/_build/.doctrees/blog/post/2026-05-18-ag-forecasting-case-study.doctree index 8cde8b9826d2725ca69cac4fce27058982f5b9e4..90491a9db1364c8ef5785a60a8555aea363da2d9 100644 GIT binary patch literal 47712 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 bR2BfO0XphMXK{uikba!OQ@r{AjzmTPXDU)W diff --git a/_build/.doctrees/environment.pickle b/_build/.doctrees/environment.pickle index b44285e6d8b94c7d5df67c9f553642342f3564b6..22c195400a86972e17083458a0d9c486255fa8d2 100644 GIT binary patch literal 141418 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 literal 59412 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 JMq@nv{{Xc&_p|^2 diff --git a/_build/.doctrees/worker.doctree b/_build/.doctrees/worker.doctree index 9b6fe6fc27e0e1233c4123b8017a4fcbb3a0775d..597ba683510adb13d5b96b457f31f867b29890c5 100644 GIT binary patch delta 411 zcmZ4A%=o*Bk)?rkYQaX9+lJA7(SfCz$=SsviA5y}>7|(|sj(UvB_##LR{HwsnI##e zNqWiox%&A9sd>6NiMdHBi74!(octvH+{Da0{gnLVV*Nt6S$erClYbdnPZl?on4E27 z%D8ayLL=G91*|;5J#0m(X{AM(Q+nK~WYLt(DaM-^S+f?fFfdG3GhM`Z0%jR&)_RcG zZ&OEs`9Ma7oVQ(uY=%yTbk>o{K4yW8M<&lRvt&Ft`Jb`&W@hsWW=5^eHCDfv88aq- zu#=eFZzso?HhCS8OrLz#PKmJsC~5%|(X=;ZOr0EWFTq$oxx!wH{U6Zite-5CSK4dK zd;zgP0*TTd&b-9j)RM}A)Z!_%Q#7*vu}uD8FT)2?&{>+H2oeJ7k#f*t`pB}`%^{N! E066lKO8@`> delta 482 zcmey}#JIkhk)?rk>XMBtw+&_dq614ale3FU5{pU{(n~W_Qe!nz@{{#4N^&(PCm5Sd z&NpJyWa!~3NX<*h%uA22C`wJ6GI@$--k7&$R5 zW0`EoD`T$F!va>^!;)KBTrwp!Llvk{9f&n!r<7)BXDwl2V91cpS_qxYta00DK$*b&lM*CtDt1u|ZnJm1)m3nq4%Wpcl=+T;nGESt}pRWLK!Z}zbI#mrL6%D^z0 zkxgWBl$|_d_2eo$ImVjFGwqaEdSMcm?G%9$U+qk#s(?a0f - - - - - - - - - OpenLambda GitHub Pages — MyST Markdown Site — OpenLambda - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
-
-
-
-
- -
- -
- - - - - -
-
- - - - - -
- - - - - - - - - - - - - -
- -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - -
-

OpenLambda GitHub Pages — MyST Markdown Site

- -
- -
-
- - - - -
-
-

OpenLambda GitHub Pages — MyST Markdown Site#

-

This repo deploys automatically to GitHub Pages using Jupyter Book -with MyST Markdown as the technical documentation of Open Lambda.

-
-
-

File Structure#

-
.
-├── index.md                        ← Homepage content (MyST Markdown)
-├── _config.yml                     ← Jupyter Book site settings
-├── _toc.yml                        ← Table of contents (add more pages here)
-├── requirements.txt                ← Python deps for the build
-├── .gitignore
-└── .github/
-    └── workflows/
-        └── deploy.yml              ← GitHub Actions auto-deploy workflow
-
-
-
-
-
-

One-Time Setup (do this once per repo)#

-
-

1. Clone the repo and copy these files in#

-
git clone https://github.com/open-lambda/open-lambda.github.io.git
-cd open-lambda.github.io
-
-# Copy all files from this bundle into the repo root, then:
-git add .
-git commit -m "Add MyST Markdown Jupyter Book site"
-git push
-
-
-
-
-
-

Local Preview#

-
pip install -r requirements.txt
-jupyter-book build .
-# Open in browser:
-open _build/html/index.html
-
-
-
-
- - - -
- -
- -
- - - - - - -
- -
-
-
- -
- - - - - - -
-
- - -
- - -
-
-
- - - - - -
-
- - \ No newline at end of file diff --git a/_build/html/_sources/README.md b/_build/html/_sources/README.md deleted file mode 100644 index 42a0660..0000000 --- a/_build/html/_sources/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# OpenLambda GitHub Pages — MyST Markdown Site - -This repo deploys automatically to GitHub Pages using [Jupyter Book](https://jupyterbook.org) -with [MyST Markdown](https://myst-parser.readthedocs.io/) as the technical documentation of Open Lambda. - ---- - -## File Structure - -``` -. -├── index.md ← Homepage content (MyST Markdown) -├── _config.yml ← Jupyter Book site settings -├── _toc.yml ← Table of contents (add more pages here) -├── requirements.txt ← Python deps for the build -├── .gitignore -└── .github/ - └── workflows/ - └── deploy.yml ← GitHub Actions auto-deploy workflow -``` - ---- - -## One-Time Setup (do this once per repo) - -### 1. Clone the repo and copy these files in - -```bash -git clone https://github.com/open-lambda/open-lambda.github.io.git -cd open-lambda.github.io - -# Copy all files from this bundle into the repo root, then: -git add . -git commit -m "Add MyST Markdown Jupyter Book site" -git push -``` - -## Local Preview - -```bash -pip install -r requirements.txt -jupyter-book build . -# Open in browser: -open _build/html/index.html -``` diff --git a/_build/html/_sources/ag-forecasting-api.md b/_build/html/_sources/ag-forecasting-api.md deleted file mode 100644 index 2a2e377..0000000 --- a/_build/html/_sources/ag-forecasting-api.md +++ /dev/null @@ -1,932 +0,0 @@ -# Agricultural Forecasting API - Technical Documentation - -A FastAPI-based service for crop disease forecasting and biomass prediction using weather data integration from multiple sources. Developed by the University of Wisconsin-Madison Data Science Institute. - ---- - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Technology Stack](#technology-stack) -4. [API Endpoints](#api-endpoints) -5. [Core Features](#core-features) -6. [Data Flow](#data-flow) -7. [Disease Models](#disease-models) -8. [Biomass Models](#biomass-models) -9. [Weather Data Sources](#weather-data-sources) -10. [Response Format](#response-format) -11. [Development Setup](#development-setup) -12. [Deployment](#deployment) - ---- - -## Overview - -The Agricultural Forecasting API provides real-time crop disease risk predictions and winter rye biomass estimates for Wisconsin agricultural locations. The system integrates historical weather data with scientifically-validated disease forecasting models to help farmers and agricultural professionals make informed management decisions. - -**Key Capabilities:** -- Real-time disease risk predictions for corn and soybean -- Winter rye biomass accumulation forecasts -- Multi-source weather data integration (IBM EIS, WiscoNet, NOAA) -- Geospatial queries by coordinates or weather station ID -- Batch processing for multi-station analysis -- GeoJSON-formatted responses for mapping integration - ---- - -## Architecture - -### High-Level Design - -``` -┌─────────────────────────────────────────────────────────────┐ -│ FastAPI Application │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐ │ -│ │ /v2/ibm │ │ /v2/wisconet_g │ │ /v2/models │ │ -│ │ (Coordinates) │ │ (Station-based) │ │ (Metadata) │ │ -│ └────────┬────────┘ └────────┬─────────┘ └─────────────┘ │ -│ │ │ │ -├───────────┼────────────────────┼─────────────────────────────┤ -│ │ │ │ -│ ┌────────▼────────────┐ ┌───▼──────────────────┐ │ -│ │ IBM Service │ │ Pipeline / WiscoNet │ │ -│ │ (ibm_service.py) │ │ (pipeline.py) │ │ -│ │ │ │ │ │ -│ │ • Authenticate │ │ • Load stations │ │ -│ │ • Fetch raw data │ │ • Fetch measurements │ │ -│ │ • Build hourly │ │ • Compute biomass │ │ -│ │ • Build daily │ │ • Annotate risks │ │ -│ │ • Compute rolling │ │ │ │ -│ │ averages │ │ │ │ -│ └────────┬────────────┘ └───┬──────────────────┘ │ -│ │ │ │ -├───────────┼───────────────────┼──────────────────────────────┤ -│ │ │ │ -│ ┌────────▼─────────────────────┴──────┐ │ -│ │ Risk Models & Biomass Calc │ │ -│ │ (api/models/risk_models.py) │ │ -│ │ │ │ -│ │ • Tarspot risk (corn) │ │ -│ │ • Gray Leaf Spot (corn) │ │ -│ │ • Frogeye Leaf Spot (soybean) │ │ -│ │ • White Mold (soybean) │ │ -│ │ • Winter Rye Biomass (logistic) │ │ -│ └────────┬─────────────────────────────┘ │ -│ │ │ -├───────────┼──────────────────────────────────────────────────┤ -│ │ │ -│ ┌────────▼────────────────────────────┐ │ -│ │ Output Formatting & Response │ │ -│ │ (adapters/geojson_adapter.py) │ │ -│ │ │ │ -│ │ • GeoJSON Feature Collections │ │ -│ │ • Time-series grouping │ │ -│ │ • Station metadata embedding │ │ -│ └──────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Directory Structure - -``` -ag_forecasting_api/ -├── app.py # Main FastAPI entry point (v2) -├── app_v1.py # Legacy API (deprecated, v1) -├── requirements.txt # Python dependencies -├── Dockerfile # Container image definition -├── docker-compose.yml # Multi-container orchestration -│ -├── api/ # Core business logic -│ ├── __init__.py -│ ├── config/ -│ │ └── constants.py # Configuration, URLs, thresholds -│ │ -│ ├── models/ -│ │ ├── risk_models.py # Disease & biomass calculations -│ │ └── disease_metadata.py # Model metadata registry -│ │ -│ ├── services/ -│ │ ├── ibm_service.py # IBM EIS weather integration -│ │ ├── wisconet_service.py # WiscoNet station pipeline -│ │ └── risk_processor.py # Risk annotation orchestration -│ │ -│ ├── routes/ -│ │ ├── wisconet.py # /v2/ag_models_wrappers/wisconet_g -│ │ └── models.py # /v2/ag_models_wrappers/models -│ │ -│ ├── adapters/ -│ │ └── geojson_adapter.py # DataFrame → GeoJSON converter -│ │ -│ ├── schemas/ -│ │ ├── geojson_schema.py # Response schema definitions -│ │ └── model_schema.py # Model metadata schemas -│ │ -│ ├── utils/ -│ │ ├── math_helpers.py # GDD, logistic, rolling averages -│ │ ├── conversions.py # Unit conversions (F↔C, mph↔m/s) -│ │ └── caching.py # Station & measurement caching -│ │ -│ └── pipeline.py # Orchestrates WiscoNet workflow -│ -├── api_cache/ # Cached API responses (JSON) -│ ├── metadata_stations_*.json -│ └── station_data_*.json -│ -└── materials/ - └── example_callapi.ipynb # Usage examples -``` - -### Module Responsibilities - -| Module | Purpose | Key Functions | -|--------|---------|---------------| -| `ibm_service.py` | IBM weather API integration | `get_weather_with_risk()`, `_fetch_raw_hourly()`, `_build_daily()`, `_annotate_winter_rye_biomass()` | -| `wisconet_service.py` | WiscoNet station data pipeline | `_compute_winter_rye_biomass()`, station loading & filtering | -| `risk_models.py` | Disease & biomass calculations | `calculate_tarspot_risk()`, `calculate_winter_rye_biomass()`, etc. | -| `pipeline.py` | Async WiscoNet orchestration | `retrieve()`, multi-station batch processing | -| `math_helpers.py` | Shared mathematical utilities | `gdd_sine()`, `logistic()`, `rolling_mean()` | -| `geojson_adapter.py` | Response formatting | `dataframe_to_featurecollection()` | - ---- - -## Technology Stack - -### Core Dependencies - -| Component | Library | Version | Purpose | -|-----------|---------|---------|---------| -| Web Framework | FastAPI | ^0.100 | REST API framework | -| ASGI Server | Uvicorn | ^0.24 | ASGI server | -| Data Processing | Pandas | ^2.0 | Tabular data manipulation | -| Numeric Computing | NumPy | ^1.24 | Numerical calculations | -| Data Validation | Pydantic | ^2.0 | Request/response schema validation | -| HTTP Client | requests | ^2.31 | Synchronous HTTP calls | -| Async HTTP | aiohttp | ^3.9 | Async HTTP requests | -| Logging | Python logging | stdlib | Structured logging | - -### External Services - -- **IBM Environmental Intelligence Suite (EIS)**: Premium weather API with historical-on-demand (HOD) data -- **WiscoNet**: Public Wisconsin mesonet weather station network -- **NOAA**: National weather service data for historical context - -### Deployment - -- **Container**: Docker with Alpine Linux base -- **Orchestration**: Docker Compose for multi-service deployments -- **Reverse Proxy**: Traefik for URL routing and TLS termination -- **Health Monitoring**: Periodic curl-based health checks - ---- - -## API Endpoints - -### V2 Endpoints (Current) - -#### Disease Risk & Biomass by Coordinates (IBM) - -```http -GET /v2/ag_models_wrappers/ibm -``` - -Query parameters: -- `forecasting_date` (required): Reference date (YYYY-MM-DD) -- `latitude` (required): Decimal degrees -- `longitude` (required): Decimal degrees -- `planting_date` (optional): For biomass calculation (YYYY-MM-DD) -- `termination_date` (optional): For WiscoNet biomass integration (YYYY-MM-DD) -- `API_KEY` (required): IBM EIS credential -- `TENANT_ID` (required): IBM EIS credential -- `ORG_ID` (required): IBM EIS credential - -Example: -```bash -curl "http://localhost:8000/v2/ag_models_wrappers/ibm?forecasting_date=2025-05-15&latitude=43.0&longitude=-89.0&planting_date=2024-09-15&API_KEY=xxx&TENANT_ID=yyy&ORG_ID=zzz" -``` - -Response: GeoJSON FeatureCollection with hourly and daily weather data plus risk scores. - ---- - -#### Disease Risk & Biomass by WiscoNet Stations - -```http -GET /v2/ag_models_wrappers/wisconet_g -``` - -Query parameters: -- `forecasting_date` (required): Reference date (YYYY-MM-DD) -- `risk_days` (optional): Days to compute rolling risk (default: 1) -- `station_id` (optional): Filter to single station (e.g., "ALTN") -- `planting_date` (optional): For winter rye biomass (YYYY-MM-DD) -- `termination_date` (optional): For crop termination biomass (YYYY-MM-DD) -- `disease` (optional): Filter to specific disease model - -Example: -```bash -curl "http://localhost:8000/v2/ag_models_wrappers/wisconet_g?forecasting_date=2025-05-15&station_id=ALTN&planting_date=2024-09-15" -``` - -Response: GeoJSON FeatureCollection grouped by station with time-series risk data. - ---- - -#### Model Metadata - -```http -GET /v2/ag_models_wrappers/models -``` - -Returns list of all available disease models with descriptions, crops, risk categories, and input requirements. - ---- - -### Legacy V1 Endpoints - -The API maintains backward compatibility with v1 endpoints (mounted at `/v1`), including: -- `/v1/sites` - Station listing -- `/v1/predictions` - Disease predictions -- `/v1/models` - Model metadata - ---- - -## Core Features - -### 1. Multi-Source Weather Integration - -The API combines weather data from multiple sources with intelligent fallback: - -``` -┌──────────────────────────────────────────────────┐ -│ Weather Data Request (lat/lng, date range) │ -└────────────────┬─────────────────────────────────┘ - │ - ┌───────────┴──────────────┐ - │ │ - ▼ ▼ -┌─────────────────┐ ┌──────────────────┐ -│ IBM EIS API │ │ WiscoNet Network │ -│ (Premium, │ │ (Public, │ -│ high res) │ │ station-based) │ -└────────┬────────┘ └────────┬─────────┘ - │ │ - └────────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Data Harmonization │ - │ • Timezone aware │ - │ • Unit conversion │ - │ • Interpolation │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Growing Degree Days │ - │ • Sine method │ - │ • Base: 0°C, Cap: 35°C│ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Rolling Averages │ - │ • 7, 14, 21, 30-day │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Risk Model Input │ - │ (Ready for scoring) │ - └──────────────────────┘ -``` - -### 2. Efficient Data Caching - -Reduces redundant API calls through tiered caching: - -```python -# Cache layers (in api/utils/caching.py) -- Station metadata: 7-day TTL -- Measurements: 6-hour TTL -- IBM responses: Variable TTL based on forecast date -- File system: JSON cache in api_cache/ directory -``` - -### 3. Async Pipeline Processing - -Multi-station workflows use async processing for performance: - -```python -# pipeline.py -async def _run_pipeline(...): - # 1. Load all active stations (filtered) - stations = load_stations_sync(...) - - # 2. Fetch all measurements concurrently - measurements = await fetch_all_measurements_async(stations) - - # 3. Merge metadata (station location, timezone, etc.) - merged = merge_station_metadata(measurements, stations) - - # 4. Compute winter rye biomass (if planting_date provided) - if planting_date: - biomass = _compute_winter_rye_biomass(merged, planting_date) - merged = merge_biomass(merged, biomass) - - # 5. Optionally fetch WiscoNet-specific biomass data - if both planting_date and termination_date: - wisconet_biomass = await fetch_wisconet_biomass(...) - merged = merge_wisconet_biomass(merged, wisconet_biomass) - - # 6. Compute disease risks in parallel - risk_df = compute_risks_in_parallel( - merged, num_workers=4 - ) - - return risk_df -``` - -### 4. Intelligent Date & Timezone Handling - -- Automatic timezone conversion to station local time -- Date arithmetic aware of DST transitions -- Support for both absolute dates and relative forecasts - ---- - -## Disease Models - -### Tarspot Risk (Corn) - -**Crop:** Corn -**Disease:** *Phyllachora maydis* (Tar Spot fungus) - -**Model Type:** Ensemble logistic regression (two sub-models averaged) - -**Inputs:** -- 30-day mean air temperature (°C) -- 30-day max relative humidity (%) -- 14-day mean nighttime hours with RH ≥ 90% - -**Output:** Risk probability (0–1), classified as: -- **Inactive** (mean temp < 10°C) -- **1.Low** (prob < 0.20) -- **2.Moderate** (prob 0.20–0.35) -- **3.High** (prob > 0.35) - -**Reference:** Damon et al. - ---- - -### Gray Leaf Spot Risk (Corn) - -**Crop:** Corn -**Disease:** *Cercospora zeae-maydis* (Gray Leaf Spot) - -**Model Type:** Single logistic regression - -**Inputs:** -- 21-day min air temperature (°C) -- 30-day min dew point (°C) - -**Assumptions:** Growth stage V10–R3, no irrigation - -**Output:** Risk probability (0–1), same classification as Tarspot - ---- - -### Frogeye Leaf Spot Risk (Soybean) - -**Crop:** Soybean -**Disease:** *Cercospora sojina* (Frogeye Leaf Spot) - -**Model Type:** Logistic regression - -**Inputs:** -- Degree-days above 14°C (cumulative from emergence) -- Rainfall in previous 14 days (mm) -- Hours with RH ≥ 85% (14-day rolling) - -**Output:** Risk probability (0–1) - ---- - -### White Mold Risk (Soybean) - -**Crop:** Soybean -**Disease:** *Sclerotinia sclerotiorum* (White Mold / Sclerotinia) - -**Model Type:** Dual variant (irrigated & non-irrigated) - -**Inputs:** -- Days since planting -- Cumulative precipitation (mm) -- Soil moisture availability - -**Variants:** -- **Dry:** Non-irrigated fields -- **Irrigated 15":** 15-inch row spacing -- **Irrigated 30":** 30-inch row spacing - -**Output:** Apothecial presence probability (0–1) - ---- - -### Winter Rye Biomass (NEW) - -**Crop:** Winter Rye -**Metric:** Aboveground dry biomass (lb/acre) - -**Model Type:** Logistic growth model - -**Formula:** - -$$\text{logit} = b_0 + b_{pd} \cdot \text{plant\_doy} + b_{pf} \cdot \text{precip\_fall}$$ - -$$\text{pred} = \frac{\text{logit}}{1 + e^{-k(\text{gdd\_total}-x_0)}}$$ - -$$\text{biomass} = \max(0, \text{pred}^2)$$ - -**Coefficients:** -- $b_0 = 423.1$ (intercept) -- $b_{pd} = -1.031$ (day-of-year effect) -- $b_{pf} = -0.2878$ (fall precipitation effect) -- $k = 0.003663$ (logistic rate) -- $x_0 = 1049.0$ (inflection point GDD) - -**Inputs:** -- Planting day-of-year (1–366) -- Precipitation during fall establishment (mm) -- Cumulative GDD from planting to current date (0°C base) - -**Output:** -- **Biomass** (lb/acre): Numeric prediction -- **Biomass Color:** - - **Gray**: 0–2000 lb/acre (low coverage) - - **Yellow**: 2000–4500 lb/acre (moderate) - - **Green**: >4500 lb/acre (high/dense) -- **Biomass Message**: Human-readable interpretation - -**Use Cases:** -- Cover crop management planning -- Soil health assessment -- Erosion mitigation evaluation -- Residue management decisions - ---- - -## Biomass Models - -### Winter Rye Biomass Calculation Pipeline - -```python -def _annotate_winter_rye_biomass( - rye_daily: pd.DataFrame, - planting_date: str -) -> pd.DataFrame: - """ - Compute winter rye biomass from daily weather data. - - Steps: - 1. Parse planting_date → day-of-year - 2. Filter to dates >= planting_date - 3. Compute daily GDD (sine method, 0°C base, 35°C cap) - 4. Accumulate GDD and precipitation - 5. Call calculate_winter_rye_biomass() for each row - 6. Merge results back into DataFrame - """ - - # Step 1: Parse planting date - plant_date_obj = datetime.fromisoformat(planting_date).date() - plant_doy = plant_date_obj.timetuple().tm_yday - - # Step 2: Filter to >= planting date - rye_daily = rye_daily[rye_daily['date'] >= planting_date].copy() - - # Step 3-4: GDD accumulation - rye_daily['plant_doy'] = plant_doy - rye_daily['precip_daily'] = rye_daily['precip1Hour_sum'].fillna(0) - rye_daily['gdd_0c'] = rye_daily.apply( - lambda row: gdd_sine(row['temperature_min'], row['temperature_max']), - axis=1 - ) - rye_daily['gdd_total'] = rye_daily['gdd_0c'].cumsum() - - # Compute fall precipitation (Sep 1 to planting) - fall_start = plant_date_obj.replace(month=9, day=1) - fall_precip = rye_daily[ - (rye_daily['date'] >= str(fall_start)) & - (rye_daily['date'] < planting_date) - ]['precip_daily'].sum() - - # Step 5: Model calculation - biomass_results = rye_daily.apply( - lambda row: calculate_winter_rye_biomass( - plant_doy=row['plant_doy'], - precip_fall=fall_precip, - gdd_total=row['gdd_total'] - ), - axis=1, - result_type='expand' - ) - - # Step 6: Merge results - for col in ['biomass_lb_acre', 'biomass_color', 'biomass_message']: - rye_daily[col] = biomass_results[col] - - return rye_daily -``` - ---- - -## Weather Data Sources - -### IBM Environmental Intelligence Suite (EIS) - -**Characteristics:** -- Premium, high-resolution service -- Hourly data with 999-hour request window per call -- Requires JWT authentication -- Covers entire globe -- 15-minute resolution available - -**Data Acquisition:** -1. Obtain JWT token via SaaSCore authentication -2. Request hourly-on-demand (HOD) data in time chunks -3. Convert to timezone-aware UTC timestamps -4. Interpolate/aggregate to daily summaries - -**Cached Columns:** -- `temperature_min`, `temperature_max`, `temperature_mean` -- `temperatureDewPoint_min`, `temperatureDewPoint_max` -- `relativeHumidity_min`, `relativeHumidity_max` -- `windSpeed_max`, `windSpeed_mean` -- `precip1Hour_sum` (hourly accumulated) -- Rolling averages: 7, 14, 21, 30-day - ---- - -### WiscoNet (Public Mesonet) - -**Characteristics:** -- Wisconsin statewide mesonet network -- ~100 active weather stations -- Public API, no authentication required -- Daily data update cycle -- Station metadata includes: ID, name, lat/lng, county, region - -**Measurement Types:** -- Air temperature (min, avg, max) -- Dew point (min, avg, max) -- Relative humidity (%) -- Wind speed (max, direction) -- Precipitation (daily total) -- Soil temperature (select stations) -- Leaf wetness (select stations) - -**Data Retrieval:** -- Station list via `/wisconet_active_stations` -- Daily measurements via bulk query endpoint -- Cached in `station_measurements_cache/` (6-hour TTL) - ---- - -## Response Format - -### GeoJSON Feature Collection - -All responses follow the GeoJSON standard with extended properties: - -```json -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [-89.4, 43.1] - }, - "properties": { - "date": "2025-05-15", - "forecasting_date": "2025-05-15", - "station_id": "ALTN", - "station_name": "Alten", - "city": "Alten", - "county": "Columbia", - "region": "South Central", - "state": "WI", - "latitude": 43.1234, - "longitude": -89.4567, - "station_timezone": "America/Chicago", - - "temperature_min_c": 8.3, - "temperature_max_c": 21.5, - "temperature_mean_c": 15.2, - "relativeHumidity_max": 92, - "precip1Hour_sum": 2.5, - - "tarspot_risk": 0.45, - "tarspot_risk_class": "2.Moderate", - "gls_risk": 0.28, - "gls_risk_class": "1.Low", - "frogeye_risk": 0.12, - "frogeye_risk_class": "1.Low", - "whitemold_dry_risk": 0.35, - "whitemold_dry_risk_class": "2.Moderate", - - "biomass_lb_acre": 3250, - "biomass_color": "Yellow", - "biomass_message": "Moderate coverage" - } - } - ] -} -``` - -### Time-Series Grouping (WiscoNet) - -For multi-day forecasts, results are grouped by station with embedded time-series: - -```json -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { "type": "Point", "coordinates": [-89.4, 43.1] }, - "properties": { - "station_id": "ALTN", - "station_name": "Alten", - "city": "Alten", - "county": "Columbia", - "region": "South Central", - "state": "WI", - "time_series": [ - { - "date": "2025-05-15", - "temperature_min_c": 8.3, - "tarspot_risk": 0.45, - ... - }, - { - "date": "2025-05-16", - "temperature_min_c": 7.8, - "tarspot_risk": 0.38, - ... - } - ] - } - } - ] -} -``` - ---- - -## Data Flow - -### Complete Request → Response Journey - -``` -User Request - │ - ▼ -┌──────────────────────────────┐ -│ Route Handler (app.py) │ -│ Validate query parameters │ -│ Extract coordinates/dates │ -└────────────────┬─────────────┘ - │ - ┌────────────┴────────────┐ - │ │ - ▼ ▼ -┌─────────────────┐ ┌───────────────┐ -│ IBM Service │ │ WiscoNet │ -│ │ │ Pipeline │ -│ 1. Authenticate │ │ │ -│ with IBM │ │ 1. Load │ -│ 2. Chunk time │ │ stations │ -│ range │ │ 2. Fetch │ -│ 3. Fetch hourly │ │ measurements -│ data │ │ 3. Merge │ -│ 4. Build daily │ │ metadata │ -│ summaries │ │ 4. Cache │ -│ │ │ results │ -└────────┬────────┘ └────────┬──────┘ - │ │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Math Helpers │ - │ • Convert units │ - │ • GDD calculation │ - │ • Rolling averages │ - │ • Biomass prep │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Risk Model Scoring │ - │ Apply all diseases │ - │ Compute probabilities│ - │ Classify risk class │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Biomass Calculation │ - │ (if planting_date) │ - │ Logistic model │ - │ Color classification │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ GeoJSON Adapter │ - │ • Format geometry │ - │ • Embed properties │ - │ • Group by station │ - └──────────┬───────────┘ - │ - ▼ - Response (GeoJSON) -``` - ---- - -## Development Setup - -### Prerequisites - -- Python 3.10+ -- pip or conda -- Git -- Docker (optional, for containerized development) - -### Local Installation - -```bash -# Clone repository -git clone https://github.com/UW-Madison-DSI/ag_forecasting_api.git -cd ag_forecasting_api - -# Create virtual environment -python -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Install dependencies -pip install -r requirements.txt - -# Set environment variables -export IBM_API_KEY="your_key" -export TENANT_ID="your_tenant" -export ORG_ID="your_org" - -# Run development server -uvicorn app:app --reload --host 0.0.0.0 --port 8000 -``` - -### API Documentation - -Once running, view interactive documentation: -- **Swagger UI:** http://localhost:8000/docs -- **ReDoc:** http://localhost:8000/redoc -- **OpenAPI JSON:** http://localhost:8000/openapi.json - -### Testing - -```bash -# Syntax validation -python -m py_compile api/models/risk_models.py api/services/*.py - -# Run linting (if installed) -pylint api/ - -# Test endpoint with curl -curl "http://localhost:8000/v2/test/rye_biomass" -``` - ---- - -## Deployment - -### Docker Deployment - -```dockerfile -FROM python:3.11-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 8000 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] -``` - -### Docker Compose (Production) - -```yaml -version: "3.9" -services: - ag_forecasting_api: - build: . - expose: - - 8000 - environment: - - PYTHONPATH=/app - - ENVIRONMENT=production - - IBM_API_KEY=${IBM_API_KEY} - - TENANT_ID=${TENANT_ID} - - ORG_ID=${ORG_ID} - networks: - - traefik - labels: - - "traefik.enable=true" - - "traefik.http.routers.ag_api.rule=Host(`your-domain.com`)" - - "traefik.http.services.ag_api.loadbalancer.server.port=8000" - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/"] - interval: 30s - timeout: 10s - retries: 3 -``` - -### Environment Variables - -Required for production: -- `IBM_API_KEY`: IBM EIS authentication credential -- `TENANT_ID`: IBM EIS tenant identifier -- `ORG_ID`: IBM EIS organization identifier -- `ENVIRONMENT`: Set to "production" for optimized logging - -Optional: -- `PYTHONPATH`: Should include `/app` for imports -- `LOG_LEVEL`: Logging verbosity (default: INFO) - ---- - -## Performance Considerations - -### Caching Strategy - -- **Station Metadata**: 7-day TTL (rarely changes) -- **Measurements**: 6-hour TTL (updated daily by WiscoNet) -- **IBM Responses**: Variable (tied to forecast date relevance) -- **File System**: JSON cache in `api_cache/` for persistence - -### Concurrency - -- **Async Pipeline**: Multi-station queries use async/await for I/O efficiency -- **Parallel Risk Scoring**: Disease calculations use multiprocessing (4 workers default) -- **Connection Pooling**: HTTP clients maintain connection pools (max 50 concurrent) - -### Optimization Tips - -1. **Batch Requests**: Query multiple stations in one call rather than sequential individual queries -2. **Use WiscoNet**: Public API has lower latency than premium IBM service -3. **Limit Date Range**: Smaller date ranges reduce computation time -4. **Filter Diseases**: Specify disease models needed to reduce calculation overhead -5. **Cache Warming**: Pre-fetch station lists during off-peak hours - ---- - -## Additional Resources - -- **GitHub Repository**: https://github.com/UW-Madison-DSI/ag_forecasting_api -- **Interactive Dashboard**: https://connect.doit.wisc.edu/ag_forecasting/ -- **Example Notebook**: `/materials/example_callapi.ipynb` -- **University Contact**: UW-Madison Data Science Institute - ---- - -## License - -See LICENSE file for details. - ---- - -## Changelog - -### Version 2.0 (Current) - -- ✅ Winter rye biomass model integration -- ✅ Unified API versioning (v1 legacy, v2 current) -- ✅ Enhanced GeoJSON responses -- ✅ Improved async pipeline performance -- ✅ Extended documentation - -### Version 1.0 (Legacy) - -- Disease risk models for corn and soybean -- WiscoNet and IBM EIS integration -- Basic GeoJSON support - ---- - -**Last Updated:** May 2025 -**Maintained By:** UW-Madison Data Science Institute diff --git a/_build/html/_sources/blog/index.md b/_build/html/_sources/blog/index.md index ebc9baf..d30a5f0 100644 --- a/_build/html/_sources/blog/index.md +++ b/_build/html/_sources/blog/index.md @@ -1,11 +1,10 @@ # OpenLambda Blog - News, releases, and technical deep-dives from the OpenLambda project. +News, releases, and technical deep-dives from the OpenLambda project. ```{postlist} 10 - :format: "{title} — {author}, {date}" - :date: "%B %d, %Y" - :list-style: circle - :excerpts: - :sort: -``` \ No newline at end of file +:format: "{title} — {author}, {date}" +:date: "%B %d, %Y" +:list-style: circle +:excerpts: +``` diff --git a/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md b/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md index 3bdc1fd..fb36833 100644 --- a/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md +++ b/_build/html/_sources/blog/post/2026-05-18-ag-forecasting-case-study.md @@ -1,7 +1,7 @@ --- blogpost: true date: 2026-05-18 -author: Maria Oros, Tyler Caraza-Harter +author: maria-oros, tyler-caraza-harter category: Case Studies tags: case-study, agforecast, asgi, fastapi, openlambda language: en @@ -16,4 +16,126 @@ Our goal is to make an ever-growing set of applications deployable on OpenLambda Recently, we selected an agricultural forecasting application (AgForecast), developed by the [Data Science Institute at UW–Madison](https://dsi.wisc.edu/), to port to OpenLambda: . AgForecast is an interesting case study, because it implements its REST API using FastAPI, which in turn uses [ASGI](https://asgi.readthedocs.io/en/latest/), the so-called "spiritual successor" to WSGI, which we recently started supporting in OpenLambda. WSGI is the basis for popular Python web-programming packages such as Django and Flask; new ASGI support opens the door to an even broader range of applications. -In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation. \ No newline at end of file +In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation. + +## Background: Agricultural Forecasting API + +**Motivation.** The agricultural forecasting app is an open-source tool designed to address farmers' needs in a customizable way. We first built a backend infrastructure using FastAPI to serve crop disease forecasting models for corn and soybean, focusing on integration with Wisconet weather stations and on-demand model serving. We then developed a custom R Shiny front-end interface for farmer use. + +**Fit with OpenLambda.** We began exploring OpenLambda to leverage the benefits of serverless technology. Hosting our tool on this platform offers significant value not only to the developer community, but also to plant pathology practitioners and scientists who want to build on robust, nationwide infrastructure. + +**Technical overview.** The Ag Forecasting API is a FastAPI-based backend that serves crop disease forecasting models for corn and soybean across Wisconsin. It exposes two parallel data pipelines through a unified interface: one sourced from Wisconet, the public mesonet of weather stations across the state, and another from IBM Environmental Intelligence for point-location queries by latitude and longitude. The core logic lives in the `ag_models_wrappers` module, which dynamically pulls the daily and hourly weather variables each disease model requires for a given forecasting date, runs the risk calculations, and returns localized predictions. Currently supported models include Sporecaster (white mold in soybean, with dry and irrigated row-spacing variants), Tarspotter (tar spot of corn), gray leaf spot, and frogeye leaf spot — all based on peer-reviewed plant pathology research from UW–Madison. + +**Infrastructure and integration.** The project ships with a `Dockerfile` and `docker-compose.yml` for containerized deployment, and includes a Starlette `WSGIMiddleware` wrapper so the FastAPI app can also be served behind WSGI servers for legacy or mixed environments. A companion sub-package, `pywisconet`, provides a thin REST wrapper over the Wisconet v1 API with endpoints for active station discovery (`/wisconet/active_stations/`), station field metadata (`/station_fields/{station_id}`), and bulk measurement retrieval (`/bulk_measures/{station_id}`) at configurable frequencies (5-minute, hourly, or daily) for variables like air temperature, dew point, wind speed, and relative humidity. IBM credentials are handled via environment variables (`IBM_API_KEY`, `TENANT_ID`, `ORG_ID`) and validated against query parameters at request time. The API is MIT-licensed, fully open source, and currently deployed at [connect.doit.wisc.edu/ag_forecasting_api](https://connect.doit.wisc.edu/ag_forecasting_api). + +## Porting to OpenLambda + +When porting AgForecast to OL, we encountered 5 challenges related to: expectations about writable directories, package version selection, deployment from GitHub, asynchronous execution, and parallel pool execution. To overcome these challenges, we introduced four new features to OL and made minor changes to AgForecast itself. + +### Challenge 1: File Management + +AgForecast is semi-stateless: data files describing stations and measurements are used across requests, but if these files are deleted, it can generate them on-the-fly from upstream data sources, such as the IBM Weather API or Wisconet. This is a good match for FaaS platforms such as OL, where lambda instances frequently persist (along with their state) across multiple invocations, even if an instance can silently be terminated at any time to reclaim memory. + +However, most directory locations are read-only for an OL function; the one exception is a single "scratch directory" and various other locations that are symbolic links to the scratch directory (such as `/tmp`). Full-featured sandboxes such as Docker containers use union file systems to make many directories editable, on a copy-on-write basis. OL's limitation is due to its use of bind mounts, a leaner, but less flexible mechanism. AgForecast wasn't originally built for OL, so we modified the code to make the directory location for station and measurement data configurable via environment variables (otherwise AgForecast attempted to write to read-only locations). We added support to OL function configuration files to support the specification of environment variables, like this: + +```yaml +triggers: + http: + - method: "*" +environment: + MEASUREMENTS_CACHE_DIR: /host/tmp/cache + STATIONS_CACHE_FILE: /host/tmp/cache/wisconsin_stations_cache.csv + ... +``` + +### Challenge 2: Package Management + +Like many Python projects, AgForecast specifies PyPI package requirements in a `requirements.txt` file. Also like most projects, not all version requirements are exact. Here are 3 of the 21 lines in AgForecast's `requirements.txt`: + +``` +... +matplotlib==3.9.1 +fastapi>=0.95.0 +pydantic +... +``` + +Note the different levels of specificity: `matplotlib` must be a specific version, whereas pip can select any version for `pydantic` (probably the latest, barring version conflicts based on other dependencies); `fastapi` specifies a range. Note that some of these might also have indirect dependencies on other packages not explicitly listed. + +OL requires exact versions for all packages, direct or indirect. For this purpose, we recommend the use of [`pip-compile`](https://pypi.org/project/pip-tools/) to translate a partially specified `requirements.txt` file to a fully specified one, based on the latest packages at the time of compilation. + +One challenge is that pip and pip-compile sometimes select packages based on the host environment. For example, in certain cases, the suitable Python package version for Ubuntu 24.04 might be different than for Ubuntu 26.04. Initially, we encountered this when deploying AgForecast on OL. The `requirements.txt` created by doing pip-compile on the host machine was not quite compatible with the environment inside the lambda function. To address this, we created a new OL function that does pip-compile inside the OL environment. It works like this: + +```bash +curl -X POST -d '' http://localhost:5000/run/pip-compile/url > requirements.txt +``` + +### Challenge 3: Deployment + +AgForecast lives on a public GitHub repo: . We wanted to make it as easy as possible to deploy directly from GitHub to an OL function. Thus, we added a new `ol admin install` option to point directly to a repo: + +```bash +./ol admin install -c ol.yaml -r requirements.txt https://github.com//.git +``` + +### Challenge 4: Asynchronous Execution + +AgForecast is built on FastAPI, which in turn is based on ASGI. ASGI is an asynchronous alternative to WSGI (Web Server Gateway Interface). The idea of WSGI is to let you mix and match servers (for example, Gunicorn, uWSGI) with application frameworks (for example, Flask, Django). The server/framework interface is minimalist, a single function signature that the server calls for each incoming HTTP request (GET/POST/etc). The framework implements the function; a common framework pattern is to route the call to a user-written handler function. For example, consider these two functions: + +```python +app = Flask(__name__) + +@app.route("/") +def home(): + return "Home page" + +@app.route("/about") +def about(): + return "About page" +``` + +The `app` object is a Python callable (meaning it is an object that acts like a function); `app` implements the WSGI interface. So when a server sends a request to `app`, `app` in turn calls the correct user function (`home` or `about`) to execute and obtain a result. + +ASGI was introduced as an alternative to WSGI to provide more options for handling concurrent calls. Consider how (in the above Flask/WSGI example) two different users may want to visit the home and about pages at the same time. Can we handle the requests concurrently? + +There are a few ways to do this: multiple processes, multiple threads in a process, or [Python's async functionality introduced in Python 3.5 (2015)](https://peps.python.org/pep-0492/). If we want all execution in a single process (useful when there is shared state), we can either use threading (with WSGI) or async (with ASGI). + +Threads are a non-cooperative form of scheduling, meaning that a scheduler can switch from running one thread on a CPU to another thread at any time (perhaps a very inconvenient time!). Writing multi-threaded programs is notoriously difficult, as one must identify shared state, introduce locks to protect that state, and acquire/release locks at the right points. In contrast, async offers a form of cooperative scheduling, where switches can only occur at well-defined points (e.g., an `await`). Thus, programming is simpler (no need for locks). + +Normally, concurrent programs allow (a) parallel execution on multiple CPU cores at the same time and (b) execution of code at the same time that input/output occurs, say to the disk or network. In many languages, threads offer both benefits and cooperative scheduling only provides the I/O benefit. However, Python threads only offer the I/O benefit due to the GIL (Global Interpreter Lock), [though this may be changing](https://docs.python.org/3/howto/free-threading-python.html). Thus, async is especially appealing in Python since it (in theory) matches threading in terms of performance benefits, and surpasses threads in terms of ease-of-programming. + +AgForecast implements REST calls in FastAPI, which is based on ASGI, which is the async-based alternative to WSGI. To support AgForecast, we implemented ASGI server functionality in OpenLambda. This makes OpenLambda an ASGI server implementation (in the same role as Uvicorn, Gunicorn, Daphne, etc). When a request arrives, OpenLambda uses `asyncio.run(...)` to invoke the user-provided entry point, with async send/receive callbacks. The user-provided entry-point can then be a full application, written in any ASGI-compatible framework (FastAPI, Starlette, Django/Channels, etc). + +A user can indicate their lambda function is an ASGI application entry point by configuring their `ol.yaml` as follows: + +```yaml +environment: + OL_ENTRY_FILE: app.py + OL_ASGI_ENTRY: app +``` + +### Challenge 5: Worker Pools + +AgForecast indirectly uses `/dev/shm`, an in-memory file system called tmpfs. Docker containers have a `/dev/shm` mount by default; as it is frequently used for inter-process communication, its behavior is configurable via the `--ipc` flag. In contrast, OL functions do not have any `/dev/shm` mount. + +Why does AgForecast need `/dev/shm`? AgForecast uses a `concurrent.futures.ProcessPoolExecutor` to run a Python compute-heavy function called `compute_risks` in parallel over different chunks of data. `ProcessPoolExecutor` creates different Python processes for different chunks of work, which is desirable as a way to get around Python's per-process GIL (Global Interpreter Lock), which is held whenever regular Python code is being executed. With multiple processes (created by the pool), each process will have its own GIL that it holds during execution; thus, multiple processes can hold their own locks at the same time and execute on multiple CPU cores in parallel. + +As a fix to get AgForecast working as an OL function, we replaced `ProcessPoolExecutor` with a `ThreadPoolExecutor`; this avoids inter-process communication via `/dev/shm` because coordination occurs within a process, between threads. Unfortunately, all the threads share a GIL, so the performance benefits of using a pool for parallelism is lost in this case (the only value would be if `compute_risks` were I/O heavy, which it is not). + +Using a `ThreadPoolExecutor` works as a short term fix, but this experience suggests that eventually adding `/dev/shm` availability (and thus `ProcessPoolExecutor` functionality) would be a useful future feature for OL. + +## Recap of New Features + +Porting real, complex applications to serverless platforms highlights the most important features to develop. In this post, we described 5 challenges we encountered when porting AgForecast to OL, and the following features we added to better support similar applications: + +- environment variable configuration for lambda functions (challenge 1) +- pip-compile as a lambda function (challenge 2) +- direct GitHub deployment (challenge 3) +- ASGI support (challenge 4) + +We also made some minor changes to AgForecast: + +- customizable directory use for stations/measurements (challenge 1) +- use of a thread pool instead of a process pool (challenge 5) + +The second change suggests a future possible OL feature: adding `/dev/shm` availability to support process pools. \ No newline at end of file diff --git a/_build/html/_sources/worker.md b/_build/html/_sources/worker.md index ff4f29e..ff2d221 100644 --- a/_build/html/_sources/worker.md +++ b/_build/html/_sources/worker.md @@ -58,5 +58,5 @@ multi-node setups. ## Further Reading -- [Quickstart guide](doc.htm) — get a single worker running locally in minutes +- [Quickstart guide](https://github.com/open-lambda/open-lambda/blob/main/docs/quickstart.md) — get a single worker running locally in minutes - [SOCK: Rapid Task Provisioning with Serverless-Optimized Containers](https://www.usenix.org/conference/atc18/presentation/oakes) — the research paper describing the container backend. \ No newline at end of file diff --git a/_build/html/ag-forecasting-api.html b/_build/html/ag-forecasting-api.html deleted file mode 100644 index 1f0b844..0000000 --- a/_build/html/ag-forecasting-api.html +++ /dev/null @@ -1,1627 +0,0 @@ - - - - - - - - - - - Agricultural Forecasting API - Technical Documentation — OpenLambda - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
-
-
-
-
- -
- -
- - - - - -
-
- - - -
- - - - - - - - - - - - - -
- -
- - - -
- -
-
- -
-
- -
- -
- -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - -
- -
- -
-
- - - - - - - - -
- -
-

Agricultural Forecasting API - Technical Documentation#

-

A FastAPI-based service for crop disease forecasting and biomass prediction using weather data integration from multiple sources. Developed by the University of Wisconsin-Madison Data Science Institute.

-
-
-

Table of Contents#

-
    -
  1. Overview

  2. -
  3. Architecture

  4. -
  5. Technology Stack

  6. -
  7. API Endpoints

  8. -
  9. Core Features

  10. -
  11. Data Flow

  12. -
  13. Disease Models

  14. -
  15. Biomass Models

  16. -
  17. Weather Data Sources

  18. -
  19. Response Format

  20. -
  21. Development Setup

  22. -
  23. Deployment

  24. -
-
-
-
-

Overview#

-

The Agricultural Forecasting API provides real-time crop disease risk predictions and winter rye biomass estimates for Wisconsin agricultural locations. The system integrates historical weather data with scientifically-validated disease forecasting models to help farmers and agricultural professionals make informed management decisions.

-

Key Capabilities:

-
    -
  • Real-time disease risk predictions for corn and soybean

  • -
  • Winter rye biomass accumulation forecasts

  • -
  • Multi-source weather data integration (IBM EIS, WiscoNet, NOAA)

  • -
  • Geospatial queries by coordinates or weather station ID

  • -
  • Batch processing for multi-station analysis

  • -
  • GeoJSON-formatted responses for mapping integration

  • -
-
-
-
-

Architecture#

-
-

High-Level Design#

-
┌─────────────────────────────────────────────────────────────┐
-│                     FastAPI Application                      │
-├─────────────────────────────────────────────────────────────┤
-│                                                               │
-│  ┌─────────────────┐  ┌──────────────────┐  ┌─────────────┐ │
-│  │   /v2/ibm       │  │  /v2/wisconet_g  │  │  /v2/models │ │
-│  │  (Coordinates)  │  │  (Station-based) │  │  (Metadata) │ │
-│  └────────┬────────┘  └────────┬─────────┘  └─────────────┘ │
-│           │                    │                             │
-├───────────┼────────────────────┼─────────────────────────────┤
-│           │                    │                             │
-│  ┌────────▼────────────┐  ┌───▼──────────────────┐           │
-│  │  IBM Service        │  │  Pipeline / WiscoNet │           │
-│  │  (ibm_service.py)   │  │  (pipeline.py)       │           │
-│  │                     │  │                      │           │
-│  │ • Authenticate      │  │ • Load stations      │           │
-│  │ • Fetch raw data    │  │ • Fetch measurements │           │
-│  │ • Build hourly      │  │ • Compute biomass    │           │
-│  │ • Build daily       │  │ • Annotate risks     │           │
-│  │ • Compute rolling   │  │                      │           │
-│  │   averages          │  │                      │           │
-│  └────────┬────────────┘  └───┬──────────────────┘           │
-│           │                   │                              │
-├───────────┼───────────────────┼──────────────────────────────┤
-│           │                   │                              │
-│  ┌────────▼─────────────────────┴──────┐                     │
-│  │     Risk Models & Biomass Calc       │                    │
-│  │     (api/models/risk_models.py)      │                    │
-│  │                                      │                    │
-│  │ • Tarspot risk (corn)                │                    │
-│  │ • Gray Leaf Spot (corn)              │                    │
-│  │ • Frogeye Leaf Spot (soybean)        │                    │
-│  │ • White Mold (soybean)               │                    │
-│  │ • Winter Rye Biomass (logistic)      │                    │
-│  └────────┬─────────────────────────────┘                    │
-│           │                                                  │
-├───────────┼──────────────────────────────────────────────────┤
-│           │                                                  │
-│  ┌────────▼────────────────────────────┐                     │
-│  │  Output Formatting & Response        │                    │
-│  │  (adapters/geojson_adapter.py)       │                    │
-│  │                                      │                    │
-│  │ • GeoJSON Feature Collections        │                    │
-│  │ • Time-series grouping               │                    │
-│  │ • Station metadata embedding         │                    │
-│  └──────────────────────────────────────┘                    │
-│                                                               │
-└─────────────────────────────────────────────────────────────┘
-
-
-
-
-

Directory Structure#

-
ag_forecasting_api/
-├── app.py                          # Main FastAPI entry point (v2)
-├── app_v1.py                       # Legacy API (deprecated, v1)
-├── requirements.txt                # Python dependencies
-├── Dockerfile                      # Container image definition
-├── docker-compose.yml              # Multi-container orchestration
-│
-├── api/                            # Core business logic
-│   ├── __init__.py
-│   ├── config/
-│   │   └── constants.py            # Configuration, URLs, thresholds
-│   │
-│   ├── models/
-│   │   ├── risk_models.py          # Disease & biomass calculations
-│   │   └── disease_metadata.py     # Model metadata registry
-│   │
-│   ├── services/
-│   │   ├── ibm_service.py          # IBM EIS weather integration
-│   │   ├── wisconet_service.py     # WiscoNet station pipeline
-│   │   └── risk_processor.py       # Risk annotation orchestration
-│   │
-│   ├── routes/
-│   │   ├── wisconet.py             # /v2/ag_models_wrappers/wisconet_g
-│   │   └── models.py               # /v2/ag_models_wrappers/models
-│   │
-│   ├── adapters/
-│   │   └── geojson_adapter.py      # DataFrame → GeoJSON converter
-│   │
-│   ├── schemas/
-│   │   ├── geojson_schema.py       # Response schema definitions
-│   │   └── model_schema.py         # Model metadata schemas
-│   │
-│   ├── utils/
-│   │   ├── math_helpers.py         # GDD, logistic, rolling averages
-│   │   ├── conversions.py          # Unit conversions (F↔C, mph↔m/s)
-│   │   └── caching.py              # Station & measurement caching
-│   │
-│   └── pipeline.py                 # Orchestrates WiscoNet workflow
-│
-├── api_cache/                      # Cached API responses (JSON)
-│   ├── metadata_stations_*.json
-│   └── station_data_*.json
-│
-└── materials/
-    └── example_callapi.ipynb       # Usage examples
-
-
-
-
-

Module Responsibilities#

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Module

Purpose

Key Functions

ibm_service.py

IBM weather API integration

get_weather_with_risk(), _fetch_raw_hourly(), _build_daily(), _annotate_winter_rye_biomass()

wisconet_service.py

WiscoNet station data pipeline

_compute_winter_rye_biomass(), station loading & filtering

risk_models.py

Disease & biomass calculations

calculate_tarspot_risk(), calculate_winter_rye_biomass(), etc.

pipeline.py

Async WiscoNet orchestration

retrieve(), multi-station batch processing

math_helpers.py

Shared mathematical utilities

gdd_sine(), logistic(), rolling_mean()

geojson_adapter.py

Response formatting

dataframe_to_featurecollection()

-
-
-
-
-
-

Technology Stack#

-
-

Core Dependencies#

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Component

Library

Version

Purpose

Web Framework

FastAPI

^0.100

REST API framework

ASGI Server

Uvicorn

^0.24

ASGI server

Data Processing

Pandas

^2.0

Tabular data manipulation

Numeric Computing

NumPy

^1.24

Numerical calculations

Data Validation

Pydantic

^2.0

Request/response schema validation

HTTP Client

requests

^2.31

Synchronous HTTP calls

Async HTTP

aiohttp

^3.9

Async HTTP requests

Logging

Python logging

stdlib

Structured logging

-
-
-
-

External Services#

-
    -
  • IBM Environmental Intelligence Suite (EIS): Premium weather API with historical-on-demand (HOD) data

  • -
  • WiscoNet: Public Wisconsin mesonet weather station network

  • -
  • NOAA: National weather service data for historical context

  • -
-
-
-

Deployment#

-
    -
  • Container: Docker with Alpine Linux base

  • -
  • Orchestration: Docker Compose for multi-service deployments

  • -
  • Reverse Proxy: Traefik for URL routing and TLS termination

  • -
  • Health Monitoring: Periodic curl-based health checks

  • -
-
-
-
-
-

API Endpoints#

-
-

V2 Endpoints (Current)#

-
-

Disease Risk & Biomass by Coordinates (IBM)#

-
GET /v2/ag_models_wrappers/ibm
-
-
-

Query parameters:

-
    -
  • forecasting_date (required): Reference date (YYYY-MM-DD)

  • -
  • latitude (required): Decimal degrees

  • -
  • longitude (required): Decimal degrees

  • -
  • planting_date (optional): For biomass calculation (YYYY-MM-DD)

  • -
  • termination_date (optional): For WiscoNet biomass integration (YYYY-MM-DD)

  • -
  • API_KEY (required): IBM EIS credential

  • -
  • TENANT_ID (required): IBM EIS credential

  • -
  • ORG_ID (required): IBM EIS credential

  • -
-

Example:

-
curl "http://localhost:8000/v2/ag_models_wrappers/ibm?forecasting_date=2025-05-15&latitude=43.0&longitude=-89.0&planting_date=2024-09-15&API_KEY=xxx&TENANT_ID=yyy&ORG_ID=zzz"
-
-
-

Response: GeoJSON FeatureCollection with hourly and daily weather data plus risk scores.

-
-
-
-

Disease Risk & Biomass by WiscoNet Stations#

-
GET /v2/ag_models_wrappers/wisconet_g
-
-
-

Query parameters:

-
    -
  • forecasting_date (required): Reference date (YYYY-MM-DD)

  • -
  • risk_days (optional): Days to compute rolling risk (default: 1)

  • -
  • station_id (optional): Filter to single station (e.g., “ALTN”)

  • -
  • planting_date (optional): For winter rye biomass (YYYY-MM-DD)

  • -
  • termination_date (optional): For crop termination biomass (YYYY-MM-DD)

  • -
  • disease (optional): Filter to specific disease model

  • -
-

Example:

-
curl "http://localhost:8000/v2/ag_models_wrappers/wisconet_g?forecasting_date=2025-05-15&station_id=ALTN&planting_date=2024-09-15"
-
-
-

Response: GeoJSON FeatureCollection grouped by station with time-series risk data.

-
-
-
-

Model Metadata#

-
GET /v2/ag_models_wrappers/models
-
-
-

Returns list of all available disease models with descriptions, crops, risk categories, and input requirements.

-
-
-
-
-

Legacy V1 Endpoints#

-

The API maintains backward compatibility with v1 endpoints (mounted at /v1), including:

-
    -
  • /v1/sites - Station listing

  • -
  • /v1/predictions - Disease predictions

  • -
  • /v1/models - Model metadata

  • -
-
-
-
-
-

Core Features#

-
-

1. Multi-Source Weather Integration#

-

The API combines weather data from multiple sources with intelligent fallback:

-
┌──────────────────────────────────────────────────┐
-│ Weather Data Request (lat/lng, date range)       │
-└────────────────┬─────────────────────────────────┘
-                 │
-     ┌───────────┴──────────────┐
-     │                          │
-     ▼                          ▼
-┌─────────────────┐      ┌──────────────────┐
-│ IBM EIS API     │      │ WiscoNet Network │
-│ (Premium,       │      │ (Public,         │
-│ high res)       │      │ station-based)   │
-└────────┬────────┘      └────────┬─────────┘
-         │                        │
-         └────────────┬───────────┘
-                      │
-                      ▼
-            ┌──────────────────────┐
-            │ Data Harmonization   │
-            │ • Timezone aware     │
-            │ • Unit conversion    │
-            │ • Interpolation      │
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Growing Degree Days  │
-            │ • Sine method        │
-            │ • Base: 0°C, Cap: 35°C│
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Rolling Averages     │
-            │ • 7, 14, 21, 30-day  │
-            └──────────┬───────────┘
-                       │
-                       ▼
-            ┌──────────────────────┐
-            │ Risk Model Input     │
-            │ (Ready for scoring)  │
-            └──────────────────────┘
-
-
-
-
-

2. Efficient Data Caching#

-

Reduces redundant API calls through tiered caching:

-
# Cache layers (in api/utils/caching.py)
-- Station metadata: 7-day TTL
-- Measurements: 6-hour TTL
-- IBM responses: Variable TTL based on forecast date
-- File system: JSON cache in api_cache/ directory
-
-
-
-
-

3. Async Pipeline Processing#

-

Multi-station workflows use async processing for performance:

-
# pipeline.py
-async def _run_pipeline(...):
-    # 1. Load all active stations (filtered)
-    stations = load_stations_sync(...)
-    
-    # 2. Fetch all measurements concurrently
-    measurements = await fetch_all_measurements_async(stations)
-    
-    # 3. Merge metadata (station location, timezone, etc.)
-    merged = merge_station_metadata(measurements, stations)
-    
-    # 4. Compute winter rye biomass (if planting_date provided)
-    if planting_date:
-        biomass = _compute_winter_rye_biomass(merged, planting_date)
-        merged = merge_biomass(merged, biomass)
-    
-    # 5. Optionally fetch WiscoNet-specific biomass data
-    if both planting_date and termination_date:
-        wisconet_biomass = await fetch_wisconet_biomass(...)
-        merged = merge_wisconet_biomass(merged, wisconet_biomass)
-    
-    # 6. Compute disease risks in parallel
-    risk_df = compute_risks_in_parallel(
-        merged, num_workers=4
-    )
-    
-    return risk_df
-
-
-
-
-

4. Intelligent Date & Timezone Handling#

-
    -
  • Automatic timezone conversion to station local time

  • -
  • Date arithmetic aware of DST transitions

  • -
  • Support for both absolute dates and relative forecasts

  • -
-
-
-
-
-

Disease Models#

-
-

Tarspot Risk (Corn)#

-

Crop: Corn
-Disease: Phyllachora maydis (Tar Spot fungus)

-

Model Type: Ensemble logistic regression (two sub-models averaged)

-

Inputs:

-
    -
  • 30-day mean air temperature (°C)

  • -
  • 30-day max relative humidity (%)

  • -
  • 14-day mean nighttime hours with RH ≥ 90%

  • -
-

Output: Risk probability (0–1), classified as:

-
    -
  • Inactive (mean temp < 10°C)

  • -
  • 1.Low (prob < 0.20)

  • -
  • 2.Moderate (prob 0.20–0.35)

  • -
  • 3.High (prob > 0.35)

  • -
-

Reference: Damon et al.

-
-
-
-

Gray Leaf Spot Risk (Corn)#

-

Crop: Corn
-Disease: Cercospora zeae-maydis (Gray Leaf Spot)

-

Model Type: Single logistic regression

-

Inputs:

-
    -
  • 21-day min air temperature (°C)

  • -
  • 30-day min dew point (°C)

  • -
-

Assumptions: Growth stage V10–R3, no irrigation

-

Output: Risk probability (0–1), same classification as Tarspot

-
-
-
-

Frogeye Leaf Spot Risk (Soybean)#

-

Crop: Soybean
-Disease: Cercospora sojina (Frogeye Leaf Spot)

-

Model Type: Logistic regression

-

Inputs:

-
    -
  • Degree-days above 14°C (cumulative from emergence)

  • -
  • Rainfall in previous 14 days (mm)

  • -
  • Hours with RH ≥ 85% (14-day rolling)

  • -
-

Output: Risk probability (0–1)

-
-
-
-

White Mold Risk (Soybean)#

-

Crop: Soybean
-Disease: Sclerotinia sclerotiorum (White Mold / Sclerotinia)

-

Model Type: Dual variant (irrigated & non-irrigated)

-

Inputs:

-
    -
  • Days since planting

  • -
  • Cumulative precipitation (mm)

  • -
  • Soil moisture availability

  • -
-

Variants:

-
    -
  • Dry: Non-irrigated fields

  • -
  • Irrigated 15”: 15-inch row spacing

  • -
  • Irrigated 30”: 30-inch row spacing

  • -
-

Output: Apothecial presence probability (0–1)

-
-
-
-

Winter Rye Biomass (NEW)#

-

Crop: Winter Rye
-Metric: Aboveground dry biomass (lb/acre)

-

Model Type: Logistic growth model

-

Formula:

-
-\[\text{logit} = b_0 + b_{pd} \cdot \text{plant\_doy} + b_{pf} \cdot \text{precip\_fall}\]
-
-\[\text{pred} = \frac{\text{logit}}{1 + e^{-k(\text{gdd\_total}-x_0)}}\]
-
-\[\text{biomass} = \max(0, \text{pred}^2)\]
-

Coefficients:

-
    -
  • \(b_0 = 423.1\) (intercept)

  • -
  • \(b_{pd} = -1.031\) (day-of-year effect)

  • -
  • \(b_{pf} = -0.2878\) (fall precipitation effect)

  • -
  • \(k = 0.003663\) (logistic rate)

  • -
  • \(x_0 = 1049.0\) (inflection point GDD)

  • -
-

Inputs:

-
    -
  • Planting day-of-year (1–366)

  • -
  • Precipitation during fall establishment (mm)

  • -
  • Cumulative GDD from planting to current date (0°C base)

  • -
-

Output:

-
    -
  • Biomass (lb/acre): Numeric prediction

  • -
  • Biomass Color:

    -
      -
    • Gray: 0–2000 lb/acre (low coverage)

    • -
    • Yellow: 2000–4500 lb/acre (moderate)

    • -
    • Green: >4500 lb/acre (high/dense)

    • -
    -
  • -
  • Biomass Message: Human-readable interpretation

  • -
-

Use Cases:

-
    -
  • Cover crop management planning

  • -
  • Soil health assessment

  • -
  • Erosion mitigation evaluation

  • -
  • Residue management decisions

  • -
-
-
-
-
-

Biomass Models#

-
-

Winter Rye Biomass Calculation Pipeline#

-
def _annotate_winter_rye_biomass(
-    rye_daily: pd.DataFrame,
-    planting_date: str
-) -> pd.DataFrame:
-    """
-    Compute winter rye biomass from daily weather data.
-    
-    Steps:
-    1. Parse planting_date → day-of-year
-    2. Filter to dates >= planting_date
-    3. Compute daily GDD (sine method, 0°C base, 35°C cap)
-    4. Accumulate GDD and precipitation
-    5. Call calculate_winter_rye_biomass() for each row
-    6. Merge results back into DataFrame
-    """
-    
-    # Step 1: Parse planting date
-    plant_date_obj = datetime.fromisoformat(planting_date).date()
-    plant_doy = plant_date_obj.timetuple().tm_yday
-    
-    # Step 2: Filter to >= planting date
-    rye_daily = rye_daily[rye_daily['date'] >= planting_date].copy()
-    
-    # Step 3-4: GDD accumulation
-    rye_daily['plant_doy'] = plant_doy
-    rye_daily['precip_daily'] = rye_daily['precip1Hour_sum'].fillna(0)
-    rye_daily['gdd_0c'] = rye_daily.apply(
-        lambda row: gdd_sine(row['temperature_min'], row['temperature_max']),
-        axis=1
-    )
-    rye_daily['gdd_total'] = rye_daily['gdd_0c'].cumsum()
-    
-    # Compute fall precipitation (Sep 1 to planting)
-    fall_start = plant_date_obj.replace(month=9, day=1)
-    fall_precip = rye_daily[
-        (rye_daily['date'] >= str(fall_start)) & 
-        (rye_daily['date'] < planting_date)
-    ]['precip_daily'].sum()
-    
-    # Step 5: Model calculation
-    biomass_results = rye_daily.apply(
-        lambda row: calculate_winter_rye_biomass(
-            plant_doy=row['plant_doy'],
-            precip_fall=fall_precip,
-            gdd_total=row['gdd_total']
-        ),
-        axis=1,
-        result_type='expand'
-    )
-    
-    # Step 6: Merge results
-    for col in ['biomass_lb_acre', 'biomass_color', 'biomass_message']:
-        rye_daily[col] = biomass_results[col]
-    
-    return rye_daily
-
-
-
-
-
-
-

Weather Data Sources#

-
-

IBM Environmental Intelligence Suite (EIS)#

-

Characteristics:

-
    -
  • Premium, high-resolution service

  • -
  • Hourly data with 999-hour request window per call

  • -
  • Requires JWT authentication

  • -
  • Covers entire globe

  • -
  • 15-minute resolution available

  • -
-

Data Acquisition:

-
    -
  1. Obtain JWT token via SaaSCore authentication

  2. -
  3. Request hourly-on-demand (HOD) data in time chunks

  4. -
  5. Convert to timezone-aware UTC timestamps

  6. -
  7. Interpolate/aggregate to daily summaries

  8. -
-

Cached Columns:

-
    -
  • temperature_min, temperature_max, temperature_mean

  • -
  • temperatureDewPoint_min, temperatureDewPoint_max

  • -
  • relativeHumidity_min, relativeHumidity_max

  • -
  • windSpeed_max, windSpeed_mean

  • -
  • precip1Hour_sum (hourly accumulated)

  • -
  • Rolling averages: 7, 14, 21, 30-day

  • -
-
-
-
-

WiscoNet (Public Mesonet)#

-

Characteristics:

-
    -
  • Wisconsin statewide mesonet network

  • -
  • ~100 active weather stations

  • -
  • Public API, no authentication required

  • -
  • Daily data update cycle

  • -
  • Station metadata includes: ID, name, lat/lng, county, region

  • -
-

Measurement Types:

-
    -
  • Air temperature (min, avg, max)

  • -
  • Dew point (min, avg, max)

  • -
  • Relative humidity (%)

  • -
  • Wind speed (max, direction)

  • -
  • Precipitation (daily total)

  • -
  • Soil temperature (select stations)

  • -
  • Leaf wetness (select stations)

  • -
-

Data Retrieval:

-
    -
  • Station list via /wisconet_active_stations

  • -
  • Daily measurements via bulk query endpoint

  • -
  • Cached in station_measurements_cache/ (6-hour TTL)

  • -
-
-
-
-
-

Response Format#

-
-

GeoJSON Feature Collection#

-

All responses follow the GeoJSON standard with extended properties:

-
{
-  "type": "FeatureCollection",
-  "features": [
-    {
-      "type": "Feature",
-      "geometry": {
-        "type": "Point",
-        "coordinates": [-89.4, 43.1]
-      },
-      "properties": {
-        "date": "2025-05-15",
-        "forecasting_date": "2025-05-15",
-        "station_id": "ALTN",
-        "station_name": "Alten",
-        "city": "Alten",
-        "county": "Columbia",
-        "region": "South Central",
-        "state": "WI",
-        "latitude": 43.1234,
-        "longitude": -89.4567,
-        "station_timezone": "America/Chicago",
-        
-        "temperature_min_c": 8.3,
-        "temperature_max_c": 21.5,
-        "temperature_mean_c": 15.2,
-        "relativeHumidity_max": 92,
-        "precip1Hour_sum": 2.5,
-        
-        "tarspot_risk": 0.45,
-        "tarspot_risk_class": "2.Moderate",
-        "gls_risk": 0.28,
-        "gls_risk_class": "1.Low",
-        "frogeye_risk": 0.12,
-        "frogeye_risk_class": "1.Low",
-        "whitemold_dry_risk": 0.35,
-        "whitemold_dry_risk_class": "2.Moderate",
-        
-        "biomass_lb_acre": 3250,
-        "biomass_color": "Yellow",
-        "biomass_message": "Moderate coverage"
-      }
-    }
-  ]
-}
-
-
-
-
-

Time-Series Grouping (WiscoNet)#

-

For multi-day forecasts, results are grouped by station with embedded time-series:

-
{
-  "type": "FeatureCollection",
-  "features": [
-    {
-      "type": "Feature",
-      "geometry": { "type": "Point", "coordinates": [-89.4, 43.1] },
-      "properties": {
-        "station_id": "ALTN",
-        "station_name": "Alten",
-        "city": "Alten",
-        "county": "Columbia",
-        "region": "South Central",
-        "state": "WI",
-        "time_series": [
-          {
-            "date": "2025-05-15",
-            "temperature_min_c": 8.3,
-            "tarspot_risk": 0.45,
-            ...
-          },
-          {
-            "date": "2025-05-16",
-            "temperature_min_c": 7.8,
-            "tarspot_risk": 0.38,
-            ...
-          }
-        ]
-      }
-    }
-  ]
-}
-
-
-
-
-
-
-

Data Flow#

-
-

Complete Request → Response Journey#

-
User Request
-    │
-    ▼
-┌──────────────────────────────┐
-│ Route Handler (app.py)       │
-│ Validate query parameters    │
-│ Extract coordinates/dates    │
-└────────────────┬─────────────┘
-                 │
-    ┌────────────┴────────────┐
-    │                         │
-    ▼                         ▼
-┌─────────────────┐    ┌───────────────┐
-│ IBM Service     │    │ WiscoNet      │
-│                 │    │ Pipeline      │
-│ 1. Authenticate │    │               │
-│    with IBM     │    │ 1. Load       │
-│ 2. Chunk time   │    │    stations   │
-│    range        │    │ 2. Fetch      │
-│ 3. Fetch hourly │    │    measurements
-│    data         │    │ 3. Merge      │
-│ 4. Build daily  │    │    metadata   │
-│    summaries    │    │ 4. Cache      │
-│                 │    │    results    │
-└────────┬────────┘    └────────┬──────┘
-         │                      │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Math Helpers         │
-         │ • Convert units      │
-         │ • GDD calculation    │
-         │ • Rolling averages   │
-         │ • Biomass prep       │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Risk Model Scoring   │
-         │ Apply all diseases   │
-         │ Compute probabilities│
-         │ Classify risk class  │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ Biomass Calculation  │
-         │ (if planting_date)   │
-         │ Logistic model       │
-         │ Color classification │
-         └──────────┬───────────┘
-                    │
-                    ▼
-         ┌──────────────────────┐
-         │ GeoJSON Adapter      │
-         │ • Format geometry    │
-         │ • Embed properties   │
-         │ • Group by station   │
-         └──────────┬───────────┘
-                    │
-                    ▼
-        Response (GeoJSON)
-
-
-
-
-
-
-

Development Setup#

-
-

Prerequisites#

-
    -
  • Python 3.10+

  • -
  • pip or conda

  • -
  • Git

  • -
  • Docker (optional, for containerized development)

  • -
-
-
-

Local Installation#

-
# Clone repository
-git clone https://github.com/UW-Madison-DSI/ag_forecasting_api.git
-cd ag_forecasting_api
-
-# Create virtual environment
-python -m venv .venv
-source .venv/bin/activate  # On Windows: .venv\Scripts\activate
-
-# Install dependencies
-pip install -r requirements.txt
-
-# Set environment variables
-export IBM_API_KEY="your_key"
-export TENANT_ID="your_tenant"
-export ORG_ID="your_org"
-
-# Run development server
-uvicorn app:app --reload --host 0.0.0.0 --port 8000
-
-
-
-
-

API Documentation#

-

Once running, view interactive documentation:

- -
-
-

Testing#

-
# Syntax validation
-python -m py_compile api/models/risk_models.py api/services/*.py
-
-# Run linting (if installed)
-pylint api/
-
-# Test endpoint with curl
-curl "http://localhost:8000/v2/test/rye_biomass"
-
-
-
-
-
-
-

Deployment#

-
-

Docker Deployment#

-
FROM python:3.11-slim
-WORKDIR /app
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-COPY . .
-EXPOSE 8000
-CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
-
-
-
-
-

Docker Compose (Production)#

-
version: "3.9"
-services:
-  ag_forecasting_api:
-    build: .
-    expose:
-      - 8000
-    environment:
-      - PYTHONPATH=/app
-      - ENVIRONMENT=production
-      - IBM_API_KEY=${IBM_API_KEY}
-      - TENANT_ID=${TENANT_ID}
-      - ORG_ID=${ORG_ID}
-    networks:
-      - traefik
-    labels:
-      - "traefik.enable=true"
-      - "traefik.http.routers.ag_api.rule=Host(`your-domain.com`)"
-      - "traefik.http.services.ag_api.loadbalancer.server.port=8000"
-    healthcheck:
-      test: ["CMD", "curl", "-f", "http://localhost:8000/"]
-      interval: 30s
-      timeout: 10s
-      retries: 3
-
-
-
-
-

Environment Variables#

-

Required for production:

-
    -
  • IBM_API_KEY: IBM EIS authentication credential

  • -
  • TENANT_ID: IBM EIS tenant identifier

  • -
  • ORG_ID: IBM EIS organization identifier

  • -
  • ENVIRONMENT: Set to “production” for optimized logging

  • -
-

Optional:

-
    -
  • PYTHONPATH: Should include /app for imports

  • -
  • LOG_LEVEL: Logging verbosity (default: INFO)

  • -
-
-
-
-
-

Performance Considerations#

-
-

Caching Strategy#

-
    -
  • Station Metadata: 7-day TTL (rarely changes)

  • -
  • Measurements: 6-hour TTL (updated daily by WiscoNet)

  • -
  • IBM Responses: Variable (tied to forecast date relevance)

  • -
  • File System: JSON cache in api_cache/ for persistence

  • -
-
-
-

Concurrency#

-
    -
  • Async Pipeline: Multi-station queries use async/await for I/O efficiency

  • -
  • Parallel Risk Scoring: Disease calculations use multiprocessing (4 workers default)

  • -
  • Connection Pooling: HTTP clients maintain connection pools (max 50 concurrent)

  • -
-
-
-

Optimization Tips#

-
    -
  1. Batch Requests: Query multiple stations in one call rather than sequential individual queries

  2. -
  3. Use WiscoNet: Public API has lower latency than premium IBM service

  4. -
  5. Limit Date Range: Smaller date ranges reduce computation time

  6. -
  7. Filter Diseases: Specify disease models needed to reduce calculation overhead

  8. -
  9. Cache Warming: Pre-fetch station lists during off-peak hours

  10. -
-
-
-
-
-

Additional Resources#

- -
-
-
-

License#

-

See LICENSE file for details.

-
-
-
-

Changelog#

-
-

Version 2.0 (Current)#

-
    -
  • ✅ Winter rye biomass model integration

  • -
  • ✅ Unified API versioning (v1 legacy, v2 current)

  • -
  • ✅ Enhanced GeoJSON responses

  • -
  • ✅ Improved async pipeline performance

  • -
  • ✅ Extended documentation

  • -
-
-
-

Version 1.0 (Legacy)#

-
    -
  • Disease risk models for corn and soybean

  • -
  • WiscoNet and IBM EIS integration

  • -
  • Basic GeoJSON support

  • -
-
-

Last Updated: May 2025
-Maintained By: UW-Madison Data Science Institute

-
-
-
- - - - -
- - - - - - - - -
- - - - - - -
-
- - -
- - -
-
-
- - - - - -
-
- - \ No newline at end of file diff --git a/_build/html/applications/ag-forecasting-api.html b/_build/html/applications/ag-forecasting-api.html index 8ecb7b3..bd326b1 100644 --- a/_build/html/applications/ag-forecasting-api.html +++ b/_build/html/applications/ag-forecasting-api.html @@ -32,7 +32,7 @@ - + @@ -60,6 +60,7 @@ + @@ -181,13 +182,11 @@ @@ -232,53 +231,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -435,6 +390,23 @@

An Application Case Study: Forecasting Crop Disease with OpenLambda

@@ -450,6 +422,105 @@

An Application Case Study: Forecasting Crop Disease with OpenLambdaOur goal is to make an ever-growing set of applications deployable on OpenLambda (OL), with minimal modifications. We believe the best way to work towards this goal is to pick interesting applications that weren’t originally designed for serverless deployment, try to port them to OL, and identify pain points. This helps us identify the most useful features to add to OL, to support similar deployments.

Recently, we selected an agricultural forecasting application (AgForecast), developed by the Data Science Institute at UW–Madison, to port to OpenLambda: UW-Madison-DSI/ag_forecasting_api. AgForecast is an interesting case study, because it implements its REST API using FastAPI, which in turn uses ASGI, the so-called “spiritual successor” to WSGI, which we recently started supporting in OpenLambda. WSGI is the basis for popular Python web-programming packages such as Django and Flask; new ASGI support opens the door to an even broader range of applications.

In this post, we describe the challenges of porting AgForecast to OL, and four new features we added to OL to make deployment of similar applications in the future simpler. The features are built-in ASGI support, direct GitHub-to-OL deployments, OL function environment variables, and OL-based pip compilation.

+
+

Background: Agricultural Forecasting API#

+

Motivation. The agricultural forecasting app is an open-source tool designed to address farmers’ needs in a customizable way. We first built a backend infrastructure using FastAPI to serve crop disease forecasting models for corn and soybean, focusing on integration with Wisconet weather stations and on-demand model serving. We then developed a custom R Shiny front-end interface for farmer use.

+

Fit with OpenLambda. We began exploring OpenLambda to leverage the benefits of serverless technology. Hosting our tool on this platform offers significant value not only to the developer community, but also to plant pathology practitioners and scientists who want to build on robust, nationwide infrastructure.

+

Technical overview. The Ag Forecasting API is a FastAPI-based backend that serves crop disease forecasting models for corn and soybean across Wisconsin. It exposes two parallel data pipelines through a unified interface: one sourced from Wisconet, the public mesonet of weather stations across the state, and another from IBM Environmental Intelligence for point-location queries by latitude and longitude. The core logic lives in the ag_models_wrappers module, which dynamically pulls the daily and hourly weather variables each disease model requires for a given forecasting date, runs the risk calculations, and returns localized predictions. Currently supported models include Sporecaster (white mold in soybean, with dry and irrigated row-spacing variants), Tarspotter (tar spot of corn), gray leaf spot, and frogeye leaf spot — all based on peer-reviewed plant pathology research from UW–Madison.

+

Infrastructure and integration. The project ships with a Dockerfile and docker-compose.yml for containerized deployment, and includes a Starlette WSGIMiddleware wrapper so the FastAPI app can also be served behind WSGI servers for legacy or mixed environments. A companion sub-package, pywisconet, provides a thin REST wrapper over the Wisconet v1 API with endpoints for active station discovery (/wisconet/active_stations/), station field metadata (/station_fields/{station_id}), and bulk measurement retrieval (/bulk_measures/{station_id}) at configurable frequencies (5-minute, hourly, or daily) for variables like air temperature, dew point, wind speed, and relative humidity. IBM credentials are handled via environment variables (IBM_API_KEY, TENANT_ID, ORG_ID) and validated against query parameters at request time. The API is MIT-licensed, fully open source, and currently deployed at connect.doit.wisc.edu/ag_forecasting_api.

+
+
+

Porting to OpenLambda#

+

When porting AgForecast to OL, we encountered 5 challenges related to: expectations about writable directories, package version selection, deployment from GitHub, asynchronous execution, and parallel pool execution. To overcome these challenges, we introduced four new features to OL and made minor changes to AgForecast itself.

+
+

Challenge 1: File Management#

+

AgForecast is semi-stateless: data files describing stations and measurements are used across requests, but if these files are deleted, it can generate them on-the-fly from upstream data sources, such as the IBM Weather API or Wisconet. This is a good match for FaaS platforms such as OL, where lambda instances frequently persist (along with their state) across multiple invocations, even if an instance can silently be terminated at any time to reclaim memory.

+

However, most directory locations are read-only for an OL function; the one exception is a single “scratch directory” and various other locations that are symbolic links to the scratch directory (such as /tmp). Full-featured sandboxes such as Docker containers use union file systems to make many directories editable, on a copy-on-write basis. OL’s limitation is due to its use of bind mounts, a leaner, but less flexible mechanism. AgForecast wasn’t originally built for OL, so we modified the code to make the directory location for station and measurement data configurable via environment variables (otherwise AgForecast attempted to write to read-only locations). We added support to OL function configuration files to support the specification of environment variables, like this:

+
triggers:
+  http:
+    - method: "*"
+environment:
+  MEASUREMENTS_CACHE_DIR: /host/tmp/cache
+  STATIONS_CACHE_FILE: /host/tmp/cache/wisconsin_stations_cache.csv
+  ...
+
+
+
+
+

Challenge 2: Package Management#

+

Like many Python projects, AgForecast specifies PyPI package requirements in a requirements.txt file. Also like most projects, not all version requirements are exact. Here are 3 of the 21 lines in AgForecast’s requirements.txt:

+
...
+matplotlib==3.9.1
+fastapi>=0.95.0
+pydantic
+...
+
+
+

Note the different levels of specificity: matplotlib must be a specific version, whereas pip can select any version for pydantic (probably the latest, barring version conflicts based on other dependencies); fastapi specifies a range. Note that some of these might also have indirect dependencies on other packages not explicitly listed.

+

OL requires exact versions for all packages, direct or indirect. For this purpose, we recommend the use of pip-compile to translate a partially specified requirements.txt file to a fully specified one, based on the latest packages at the time of compilation.

+

One challenge is that pip and pip-compile sometimes select packages based on the host environment. For example, in certain cases, the suitable Python package version for Ubuntu 24.04 might be different than for Ubuntu 26.04. Initially, we encountered this when deploying AgForecast on OL. The requirements.txt created by doing pip-compile on the host machine was not quite compatible with the environment inside the lambda function. To address this, we created a new OL function that does pip-compile inside the OL environment. It works like this:

+
curl -X POST -d '<some URL>' http://localhost:5000/run/pip-compile/url > requirements.txt
+
+
+
+
+

Challenge 3: Deployment#

+

AgForecast lives on a public GitHub repo: UW-Madison-DSI/ag_forecasting_api. We wanted to make it as easy as possible to deploy directly from GitHub to an OL function. Thus, we added a new ol admin install option to point directly to a repo:

+
./ol admin install -c ol.yaml -r requirements.txt https://github.com/<org>/<repo>.git
+
+
+
+
+

Challenge 4: Asynchronous Execution#

+

AgForecast is built on FastAPI, which in turn is based on ASGI. ASGI is an asynchronous alternative to WSGI (Web Server Gateway Interface). The idea of WSGI is to let you mix and match servers (for example, Gunicorn, uWSGI) with application frameworks (for example, Flask, Django). The server/framework interface is minimalist, a single function signature that the server calls for each incoming HTTP request (GET/POST/etc). The framework implements the function; a common framework pattern is to route the call to a user-written handler function. For example, consider these two functions:

+
app = Flask(__name__)
+
+@app.route("/")
+def home():
+    return "Home page"
+
+@app.route("/about")
+def about():
+    return "About page"
+
+
+

The app object is a Python callable (meaning it is an object that acts like a function); app implements the WSGI interface. So when a server sends a request to app, app in turn calls the correct user function (home or about) to execute and obtain a result.

+

ASGI was introduced as an alternative to WSGI to provide more options for handling concurrent calls. Consider how (in the above Flask/WSGI example) two different users may want to visit the home and about pages at the same time. Can we handle the requests concurrently?

+

There are a few ways to do this: multiple processes, multiple threads in a process, or Python’s async functionality introduced in Python 3.5 (2015). If we want all execution in a single process (useful when there is shared state), we can either use threading (with WSGI) or async (with ASGI).

+

Threads are a non-cooperative form of scheduling, meaning that a scheduler can switch from running one thread on a CPU to another thread at any time (perhaps a very inconvenient time!). Writing multi-threaded programs is notoriously difficult, as one must identify shared state, introduce locks to protect that state, and acquire/release locks at the right points. In contrast, async offers a form of cooperative scheduling, where switches can only occur at well-defined points (e.g., an await). Thus, programming is simpler (no need for locks).

+

Normally, concurrent programs allow (a) parallel execution on multiple CPU cores at the same time and (b) execution of code at the same time that input/output occurs, say to the disk or network. In many languages, threads offer both benefits and cooperative scheduling only provides the I/O benefit. However, Python threads only offer the I/O benefit due to the GIL (Global Interpreter Lock), though this may be changing. Thus, async is especially appealing in Python since it (in theory) matches threading in terms of performance benefits, and surpasses threads in terms of ease-of-programming.

+

AgForecast implements REST calls in FastAPI, which is based on ASGI, which is the async-based alternative to WSGI. To support AgForecast, we implemented ASGI server functionality in OpenLambda. This makes OpenLambda an ASGI server implementation (in the same role as Uvicorn, Gunicorn, Daphne, etc). When a request arrives, OpenLambda uses asyncio.run(...) to invoke the user-provided entry point, with async send/receive callbacks. The user-provided entry-point can then be a full application, written in any ASGI-compatible framework (FastAPI, Starlette, Django/Channels, etc).

+

A user can indicate their lambda function is an ASGI application entry point by configuring their ol.yaml as follows:

+
environment:
+  OL_ENTRY_FILE: app.py
+  OL_ASGI_ENTRY: app
+
+
+
+
+

Challenge 5: Worker Pools#

+

AgForecast indirectly uses /dev/shm, an in-memory file system called tmpfs. Docker containers have a /dev/shm mount by default; as it is frequently used for inter-process communication, its behavior is configurable via the --ipc flag. In contrast, OL functions do not have any /dev/shm mount.

+

Why does AgForecast need /dev/shm? AgForecast uses a concurrent.futures.ProcessPoolExecutor to run a Python compute-heavy function called compute_risks in parallel over different chunks of data. ProcessPoolExecutor creates different Python processes for different chunks of work, which is desirable as a way to get around Python’s per-process GIL (Global Interpreter Lock), which is held whenever regular Python code is being executed. With multiple processes (created by the pool), each process will have its own GIL that it holds during execution; thus, multiple processes can hold their own locks at the same time and execute on multiple CPU cores in parallel.

+

As a fix to get AgForecast working as an OL function, we replaced ProcessPoolExecutor with a ThreadPoolExecutor; this avoids inter-process communication via /dev/shm because coordination occurs within a process, between threads. Unfortunately, all the threads share a GIL, so the performance benefits of using a pool for parallelism is lost in this case (the only value would be if compute_risks were I/O heavy, which it is not).

+

Using a ThreadPoolExecutor works as a short term fix, but this experience suggests that eventually adding /dev/shm availability (and thus ProcessPoolExecutor functionality) would be a useful future feature for OL.

+
+
+
+

Recap of New Features#

+

Porting real, complex applications to serverless platforms highlights the most important features to develop. In this post, we described 5 challenges we encountered when porting AgForecast to OL, and the following features we added to better support similar applications:

+
    +
  • environment variable configuration for lambda functions (challenge 1)

  • +
  • pip-compile as a lambda function (challenge 2)

  • +
  • direct GitHub deployment (challenge 3)

  • +
  • ASGI support (challenge 4)

  • +
+

We also made some minor changes to AgForecast:

+
    +
  • customizable directory use for stations/measurements (challenge 1)

  • +
  • use of a thread pool instead of a process pool (challenge 5)

  • +
+

The second change suggests a future possible OL feature: adding /dev/shm availability to support process pools.

+
- - + @@ -330,53 +331,6 @@

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -