From 2006f7b4a415a5e2bed8d4c964e0668307de0d14 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Tue, 22 Sep 2026 10:48:18 +0800 Subject: [PATCH] first commit --- .gitignore | 7 + ... ‡分析成绩基础数据_5次考试.xlsx | Bin 0 -> 92209 bytes README.md | 38 +++ app/__init__.py | 0 app/__main__.py | 11 + app/config.py | 14 + app/db.py | 83 +++++ app/excel_parse.py | 253 ++++++++++++++++ app/export_xlsx.py | 83 +++++ app/indicators.py | 282 +++++++++++++++++ app/kalman.py | 205 +++++++++++++ app/main.py | 283 ++++++++++++++++++ app/static/css/app.css | 149 +++++++++ app/static/js/app.js | 112 +++++++ app/templates/analysis.html | 263 ++++++++++++++++ app/templates/home.html | 55 ++++ app/templates/login.html | 25 ++ requirements.txt | 6 + run.sh | 8 + 高三指标划定操作指引.docx | Bin 0 -> 20728 bytes 20 files changed, 1877 insertions(+) create mode 100644 .gitignore create mode 100644 2024级指标分析成绩基础数据_5次考试.xlsx create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/__main__.py create mode 100644 app/config.py create mode 100644 app/db.py create mode 100644 app/excel_parse.py create mode 100644 app/export_xlsx.py create mode 100644 app/indicators.py create mode 100644 app/kalman.py create mode 100644 app/main.py create mode 100644 app/static/css/app.css create mode 100644 app/static/js/app.js create mode 100644 app/templates/analysis.html create mode 100644 app/templates/home.html create mode 100644 app/templates/login.html create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 高三指标划定操作指引.docx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8e2e0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +data/*.db +data/*.json +data/uploads/ +__pycache__/ +*.pyc +.DS_Store diff --git a/2024级指标分析成绩基础数据_5次考试.xlsx b/2024级指标分析成绩基础数据_5次考试.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4977a6e412d7eca77f879827b31bf30d55314899 GIT binary patch literal 92209 zcmZs>bzIfW);_%H?vU;d=}x7k*@Sd=cb7_cNVDk{6)8nZK)OS^OS-$>FX(;FeV+Hn zKl;JVI5TTqv93Wy9tIW}^!OE~v?Bia=RaRaz+cQ9O;ubRom|;efREt<-#~o&SUYw_ zQwS6YGz$#^VgCDL#!gPpz3lC>6Z>AlaNvesyg9)9;t1AtOisr-dP7_lwJmBfXk|o) zBuAo?`X>Lm`fw4&^W+AnRq4ZH-qT$5%d-EbAfOGIWHg7Byg3!4XGqE{&==`kK!0!yu# zT1*?!I`!tCd(%^-rSVza2|BD^(DgEAa!|baMN+%%VKQE-q+TMdMs}(hw1F{}ub0^> zZ_fp1Q6wFD&`?rlS|f+HrghC%>6H?}HIg;T(d;#2R*TNVzTO`oblqcGoC` z{L}8G{HFV2eW&m2A6B;S4nI8jU6^)ujf~CiTrEQES~i8(j}rV_>}8t+%ny*Z{4??+r%=15OUf!Rn! zqC~4nghMGm{+#xjd5bUu#7(-*hm&=&Ftfhw4{tr$>ul6xSHg2axQ>!VyaGNfTFbP^ zA533;ZcvRcKxfgx-?Sv0Oy+~%S1kDP71?e{u2=1&YmK`SIGT0wjT!GT`ub0IQySNM zE8f>!_r1J9I4?I|zcGCy5bA4G8YfvJgB&2a%?HK+{oojad>5V>*smH9!;cKzKDmQZuSQ35cI7R_wKe~ zc7hPXeFOKazd@*F#~p5^oVi(4%+LI?e#Khi5(|my(D^sPeXelPB;PtR9+KP!rS`j{ zd>TeY%jEdf`{kq?|HdlHi=l_NX&toRa;v8%-+6&>CHBYIRQ+K7_u>utnXx5-i+=}P zJkI}Iyt}KLqy6L6*Cy&KzkR%V#aoQRi($3SAh_3^UD&H3(wh!wjONr^2-baW9QU3= z&H-~ajkc~z5K+e=xzwR70fcflHYs>N45%<J< z!^!R_LJap9g>7$D8Te837Z<0p_GA?}&OgknowZ{WV15@p-&GMInXP#+;S!onMx&;- zjn*&xo-iU;l(YW(PTTkL9_o-g*V_M>@A1m%KnnI}8X54bWLY(_G0YiYjooa}AF3X?1c+|sDAX_;gvkp*A=fC+*@yj^a&kIMW>EV9Xu*L7{j#=_@)-mw*`cuop{YBu_`RroAgjTOO|0Z^YQduCGcQFEB8=90Tv~yITD1vF_I= z1L->10eD@T?C1RCi! ziRDG*C}^+l6_2*$1l}%B8V1~~v$X`Cw6lo?+Ui&6Ufu4w&J5-RG6Z)R-QC^+`z8;3 zU_-q1w3}!Bx@t87j zl@C*jQwjGEMd{}cBOUdMUlzMuMHPk{e@d&uzq_%87F^yVrx=e~vf;r7Px;qvTc z^1lAzBsH+n|LSP*{#r=T_0nSN0;A>j9pdFhMNVJ`dWw&)M}{6gF0`3pkw$eD`ohPC9|nTzk)|yI^&@h0$_<9QWySi3$6x zg~Cv!XUpB)O^WLGz-dzUTruvumLJ3Sr)OGDccCr!n>l3nhkJo0YXQGw!S1_XR`p14 zb}!XQ!*;#OVi|=DDoh7s56V~5NF67R(nxa#sxVfMl1UFHjQby;dD8OG2e=6+n^f&8iM_~zf*V|+Ccx(4A(s6QxeUDtC<9KZxhnYW^*Da_auSELd^ua82X26(L&qW zPzS#Bs7U5j`M5@zSY>ed%^akU`i{q8aN8Or3XWX*nbmF>kCHnX8J6sI9CikOxf9@; z!+MvbJ=`ytuxE`j$bo!!n`FDXDKsNz_|Dw!r;B(X56Zp1P!%!Gpvjezq;aT&Dyrg+ zH@`@TvtC<^j6bC8NM4P6)3KAo{_0F-G$;Isr;NXX(8nY^iROLS-cW}V&GG1M4@(54 zvtSoPp}z}jv?v!YzJ(RO-Hv@I6*8CG(?*3wN1l!x?<2Q;#K_M6M!N*{s|AmRHB~s5 zfz;$fTLjPTo0`?b^ZBtw?jE&z|Ow$0@Cfo8&3X8kaa#_LB{y;T5#|q}T zod1Hq#9n}z2&WFBVN$G!Q4NHk&eqSF+b(br6=OU&NgKO;q;oXPHM$K|f7XbBrGJ%R7kI&Ql`+Hc(FiK>_j1B!avu@o8W8}X1$fC{KUC5g5qNY-jDN5 zg+p2E08V)-S>I&=Y2!B^?J+;3h{P;bX!9cdV({C)*ua+>u2DKPCW(nQRNs^kP&3F6 zn45GREggzm&pOXIbt+Uu^(<{-4DVZvprRCXS8kL2zAW(5IJV*|l$v*%*@a!cx-&nq zR@|Fuse-HVS7OO=Jx+ePu3_>i-j283$FXk=tFz8i`m6EaJ8BxL-@IsC*p#0JU(i6f zB{y+P4o{`)ypS>McWoWbGE2fGvi;?=i=;(8N4&_$`ZLKX1 zC5ue5DBm33DAZ^m;;L4VNaicUuGMfUQ@xsX-Y1KafD)}-!B;(yJsdV_Ip?31l*)QJ zF2X6HczRXWUwY8=jC5L3^0}O7zJ+|jQ3C48DvUCe7H4!1JC6MswAA=EV#II$)8_~= zIFZ$Rzca!|gV*Oo_`-dtgYnTi`iLmJkX56q71e7r!W(3;c}5~41^9@HH{aZj@)c2w ziE0&dvkGsB4gl7 zJ*y6Ea6%-pJfd^Wx_*`0=b-8LFoY3}jfPyl_BmnfcQx4tx^_P=R4kWb6AH89x|YrA z0!yO(u|eUfUuS}M;<_&T5MqU<1gzX-zh9sq;4~eQ+wUHQxc=%{ZR0o8=^0b$gQv3> zWQ%L7&3gTMU!dGiIXq|1Uhz74A7reD&1%mup)$!=9%F7SkQ(A(V3nL&|(LPmsNu}6Vt{1<*dE(WV`pCQq>pZFLnl5Hq!7blU zNVhR5xmIUJZ+e#911349#bWCTvXtt*s|L16TZjBS%f5df}IOUc;xH z{|e#$pLTY$Is5{T2zfcNVh6^v#{*VWSLC*mnHwuKXuN=Ul>26+J=YHKLqB^XdeaHN zVfu&>k_*a7E-+?-!tCN0hx?Exfs*fuWHF9bLc=CJ*pcRtNg*Um`lOcc%Yb?ZqryDU z%D>ZJ-T58(|CTJ_^fdUgoIZ2keVjx=G@Ij`!tE0uVM0JNa(qF85W~-@iR!m;6FV+Ep!R>(l9~JVc$12I-q*5Rzi4~Bz8F_R zotavrin*RWWCVy5oH=u5c-OUstq}GxcDm*>4?S*@-u(pFq?Zr1t}t+w$B$PD?qC$2 zr}oa60<-H8J{Ml_B^Oi_z1RB^t?OhysJN_ZlpoW$fyn=zs4OX>uHhY(%^5Pav1A{P zjPD6e{&?JfkVj^-1}vutR7P^1IwE6=3zzy;lNUf<-8(gYJ)!eeb9{=S={dq*>8>s@ z19Q^I9LD2QT1u)5V8jmnw;>(LoL5;Z!;+`gczkKd7-HW~0IVoTWgW9jZ>uVN|E;ZK zId9eq!|1KkWJb7I!A(SNR*I3FKzUBVWp9p>P=dhr4Zz9qsme#=26~Pn{Rdz%O#pYS z#R~~bd8podkCosDkWwp_SO)joJCY?2xIV*7s80wSW0$^127NO3{@I8hr-LryI=w84 zE1&15b(%zSusVv8^e3gozCCw|7x0XO_-b(+=C>b39S>VIdkpb{7juU-|M1qJTd^oW z5Bwt%{Qla~D~V{zL+hmBX05jDjbnOA#sOJvSOQ%3QYb&UQ`7{(Gtpx|fTkh69GZ81 zTgHdo`HdZqY`gnW^UA`(X$>-*JXKtkacqqj25#kHI`-Z0b8dO+_ucTSc>SRQgNx}* zX|m;e2Up=fxlm%H{WxWVdM~yi#Oh56tmSRs(hSSY@^ElzgNz!_NG_?f@-@O2$V`zQ`@Zva~xGO*jDLgxe#9=xPq;RAD_S_kw1qtY*Q|5M|*4N$-P7q+cPT zmuzxG@!~3JXD~I6y#^A`1#qDQP3zmIMi9wlX4!dHYWb!F@i%oR=4hD0HC3PH{}ED) zvWRRe|H8GVi+oGQmTTW0m-4aa-C~i!yeAoGrSpp>Gsw>(tyzi*#=PIX7IYR&11jFL z_7{Fy#!QckH6{F4QU;UBvjLhoq$xa!lu!9aaFMX?V;?mkouXYGhT*C`IILNx!fnd< z(@GSKp^R`slL7mPWh($C+#w~y+(W#ISS7=5afR0TV})hEn;c7fxLTBaSWL4Q=3CrWOTH4;YKBnEg8WeiN=@ow9=>i=TS zyZj{ID(y-jUjD#rh)tMQ7H3+9Gnqc^qjqN`35++D;Cv}5YoM~F6!;t69&l+&(u0iX zIQl=uZBJeE?(>BeYJ7Pk%J~)C&KUCb-SW@MLe0{ye!KM?bCEWRCi>yxEts|HJ}fH; z0g{QDP>}RYXQop-HpRDSJ}G>ZA{z`?A>wH&CgG4$@4AR5c>EO-)vCdloJ)9Neh{A? zmxkld)%kW_#TBi#DVwj22PJiRB(Y*>la-Q3H@Va4v5NE$=0vU;8X|Qio2Y+CyuF)Q zwZp-4wi;gib>B@=3(EejP=b4KS5G%2oTL9G8Ue9vFV2pCG}&)})4-k>CfmF26>x2R z^qon4Yb8AZOWALH^K}4xAT;xgwZWr@@zTk%hvc&ojn&t(_KZPaX6ZU8v|Jc^=4TnF zvaB*FR0f*0S1oAug&y`Q&>@p0THW}HMjn@)TXQPY^m;A~0vIdh!?wc9Zv%$|&?pU; zFuhI@l#lV-H=);o^eJ)neiUBSIXT~1{g{j>HhQp=h$;sud0p8n%z>C_dE>hwFoF-E zJIO7vjo6S4>j6k_X)7igw0vEjgNKCS4lp0|=1jtIN2C`0D!Q+1pQlp$g;_tf8nNBaLfg_ z(G2F-NFLN(H~dF(1HiP|w{lFSTUWecD6VodQMe-T2DX=mod_J<2=G}Pc#yVn;RHrD zoa6bKghv9jmn)|8D30SMZMwLm8#;rW(Gnjnbfod!G!hfHLTfF%xN_*Z!hXQ@jIw5S z0~YlgO;f@U%3xAT^2Q!hGY)sQVir9E@yC#KiO(<^B$u$)N@Igp&axvVAoUZ}={nSq zl4Mh$g|fGBQ9vjO^1MdtVv&alXv*tWegmQH9Fnr$f{Ud@09H0-FpAKpL?Rg7PqS9n zl3JB3zWz}6PP4v{EMC8r{b^iWA;Ynk|AM&!(oFCD(fwSWcgaaU{XPK=$9+Fz-3=^) zWB(I3p{V+q_Dm-$*MS5-1>4PhnJ>0#@0x3DyClG`|81hT=l?WOZ=4ITT)9+{JZHaG zTtetJHSsx*d^ME3YcwMVm;D6h&+$H%U`G3OYhGouYuU33*Ndr<z(r-M95+EVgJmh^57uC0kcS66@@xJjE** zC!K)I$*$#+-bW>Q?DC^3H-Jw=N`Fq%G0o(o`RL9jO0tU6U-NfXpF|(`S6Jxz{Eqb{ z*TOXncz6LXxcAk9@Ka?h$q-@WZKCk(;)%1Ha;8(pKoTdgzdxN0s~J*qhzr@6-e6q8 zh3qE?E4o2Ug@uL~l!v%AQ$^9UEgM>YulS3v6D$d=z>|z1TrEr@vP20DnKdb~Grgq46 zEh*ME7St}g(qA(nF`h*n#o%G#mOFpUoe3~Mt`OkpfKkHe1=J&1ND$x5h!7;973WvB zjf*4#m&vFJTL6f?!6~<2X>EaB>q$E9GEg7xMSVFk#*~OUpKvOoCYVeqG zM_#|=$iHRT2TD!#s)_%P-NgSt@SBQofo{4$+xyza~H5s z%2xA8iMAr0Nt62mz@~()!yiekgzw8wuotrOiQNgVb7{3>S@n4L=5r6oP!Yo^1eJ@8 z$xH4Si&=R@?=%7RtKND+seRC`)|Tm;1S9=^qR{yNUc?&0-=jwIfP zaghl6Y=LZ7k_|>1{+h++4^Yr2Z6`UU&&v2~UN~0B9M?z-4mEt}@qKVtrc|9A2<^HY z%pGVjE3Lh@Ywg`M{nV>HvrQ3c!vbMB+21#Q((w#>HUE`J)r;TWo_H`-FDC$Cgp_;N zRRf%*K2KeiVKp3UvaqK+R`v_dhiy8jc8C#AtQ)gLQ;4q@KKXs8m3evPR9>l1LXo{u z$*Z?cf*JIxMc}VM4?j)V1v!18jURxo<#rfgkco3^@%<#4~%ZHt=#F)2Uu&I8yW z3VZxshIc%K{NuC}0pN^~sS>Skd@}+)Z%3lXMjk!e|c6fsbh%j zrwo6Fm0;KUJ-4Qk+zd0_hIqu2SooM7-7}pj^rf18;E;NIkxEYBz}u8x@_qqct9}a3 zm!;b)h$`DMz)X?{6}U$xsVk(mEyJXgw|xf;WBD<^S0zi{9*EovE_s95v^(E?1ZI-Y zln?hAYm9z;3}bd@m^=raRFMPU8-x0`tRRv}9^Zk3cdI(msN{m!9w!}c)woHYuW;s7mN236cl$EyW_;0FGSt)i%fFX2t8GPgU%Ar zT>amQ5r4LrsHC=L)&|rm+C;mT&sSLn&?YwnKVa6@2Eq@WkkUik{0)GwR&bw`j@vBm zY-{$Sxs3E%Lt+HhXFJP<7O~?4=iH)!Mhh5+k8wW+wP(t)Bg;~CXExl6x#vm>fDIBo zhADsaiEL(Sa-o|Sj1?w$^OvJ7Gc-MP=_-MFd#9p3)vi^iGNk;h>HzOnc$nvb?4lVV zzT@;r8qOMk3Ezs=F90TKqm1nWT|K@VB^q-^<-*)^fJ5z3f0B@O>t`a$eK0f!J($Xd z_-m;&LtZmU5f*Q~Ai=0day9*@(s)}VD8kgrSN;t^vQ@lV<1?KI=m(I?0LjANruoO_ z|8zYbj{*)W(zgwkNHw#nh`=kaiV$dut&85^kxJvZiLKsHsGX4R*u5b@VdA|>+4jGX zEWgzp66i86U+BiySk620+lv7PZD(YS3)ylykMb}V?2`rGk+PIc^)LNwh5lygmOpJ% zMy^>Ql}!0wKHtpE7(W?%`fw|+2xM$&XRNR*KdT3LW!V~H51~?8-XiN6lWafh__}ao z(>LH1&TSzG%pqX5`3_sCl~uZGAnP1G6G1SOc_6s`T#3Yy`taT+#lJLsrV}O zC~1E^Wk{(njVg|a#Lp^jBD%&I@%hHnfxTg8?xOjJ0s@9Mz_}Q_PdA~U89VnhdDW;C zxACL+11)hg)gMxS9Mhj5q3Hhy5;g!Ni~&d}C~{NDG|K@F%hSl~GEUID2VT!7h1%NA zXd`oX^47Qi`)Um>Fm+%7s8H*5A(>BgbP>q_LK*sB{tJrEUJLFV@c=HGuoQgW;-met zz4*7yx~d(ySU{kEh2+(9e5As~9U7N&yuT?Wr*tE+G0j93X~|MRpd9e-^3)2_kra9j z$QmbLu|5Fz_eU;-rLx+_L=sCPvNx;wXJbMmh6JBCC6V$&_6k@5i)Dawnz628GbF6b zBXJZhg}ZvCZlQNV19AH-CRWU!mGj);2p7^eCOrF}bvjoQPznzGB}BxKe$n=QWtKs@ z1$>_no)%}vdupx`&2kmVrv6dhk2poa!jGpcdLigJ6oYyuT^El0WKSOl^Sr_4qCqw4 zobOW+$7)0ea*(U^@A4^EFwEq$$2@&r9+#a*CtFWCAxJHF$Z-uYJe^jw3rX*g#$0Zh zF!wOf{&L`-#u*hd>z2=i6g$r#Yge*)Y}y}l;x4;GuX(qDV$L@vXDe5VQ+{#1^4;*x zH5PUUTFir3h`iT1twhW6G1+~z%a6H3-iyFL$!^t&!T~c&<~0PhvO@;z`E-ygm0*y+ z*;}t>QDTRdqVsW^N1e!GaorE5j~qLmy;T~EP5mn7nFMjuaz?4Rpz*S5H1N%`|3Ibm zW1B1;h+2M``U~GTU9_QcwQ#!ixZK5V0gNu6W6Qf#R_r{O2(HC@i97yUgrih=HVe{^ z19pG*+{gE0v3OR+b9f^+wWCqp;FI%O`3pMKC%5bVRjXWzEQAzRRzzX`ahDkn2&k%W(F)etX$qA@Ox5B>@PoM$a#zlMj}1ApLJy zNeNHIi{f4R=Eb)w-yy=49pV;l{AeklH#O0ri$3@j3#9)<3~ofiu)WqQ{? z$)_B7sJ|d0wu*BYvoK(6i{~2Ej8Or9(qI&fIDgl>EUV8CY?GRnYSW=1cw85$UE#?+ zaq(LG0Ak8nSt(sOAof(l5s}~7OI?VHP1e%>M)S>_anFsshdpd!H6^CeLg0f{QgEG4 z7&H+xg9i(1{TQS_DR}gC&i5^N6-U}uCh?FkB5&Z_Fu=K0E@X4*JaS(PKzxn>W(q#m zSfuUa-znPKr6%6i|7FW-^dinVAtA3WGW-ou9|Qb*l<^Y!6SLBqoorw^bw_iQ^21@L z$HX%)k{_>Jm<>?L7|}Jn6Tjc+tgDa@N2hdKtg^-Ek2@#_aSQSBi#k?Vo6cy%Gg? zKxQSYK_cDYqaN!~MP0dQ9jF|7h53mC-vSVvX^iYlKQowx)I78%i}pP!K8Ry-LP`t$ z2e5Yh2VYLj4CO%qOjEpC(hLm1$3ltN?*Xi23N+w{{e;(wyRWanG6zio*t&C>$|6PvjhX3FotMH4*q)oB+D%P-RasNkB5Ht9SX+v| zP%P&2dnSoOKCa-a9mpN3C_e(OkV@&rHd(}YN35G+ytKAb)5vghD^F|~sSS7k*|&Y$ z#s22WR(6kvtded{q}n?XrKT|$ty`s`jUMk;T)jXg#LqOLKaY%Up6OzQErj?&sF7U6@OdSdx48G)ve!ELR#R&}B%vci;4o&(8CvM80U~(V> z08rdBovQsQUG|HKWt-k(=27605mihsAZFPrGpS*fP`JlQ+L#QDm1X?^#(^}bgLADy zj+a*yKV_~(1~X5bw=>MME@E$f6IwGzm6q}SQkxkU5;nA@Y z%Ze^-2UBKM@psu#>g=VKivW*YJ=~V~TlH5L(<)-Mo{I1+RxQ{=;=0cLn)>mv>#;xpLqy1at6wau zLsoN?rvk#n3W1C@pNfr9p{15j7)iA49c<;m^Gv}jKW2H|iI#>7;jx73&<@Xxn|} z=0`l5(30|ZY`Xfi91gS{*{D>N+1Ao_qHx#5%W_QgdV=u|yKv>gbqOVCoGdnz}9~ zC&Lq;xbl{C9g7~MWWV$+jN>Eh*9irhrPtO~W#1avs(snP(EX_|K_n6}u`KOJ)}_Jv zaJ!Um%Ak42NL#7Y;CAWIrp6)de%YW_)vm~5_w#SlIhUN{4KbmeP&J^w6HRHfwy8s9ZlN1tl|g$X!Q0s&L^G!N zKzEFAh>T{z0}IPo3qXVDWpv(~5^7K=q0ea%a3N8`lf`^R@=n}{b2Uli1xxq;r1zW` zME8}*3=fHb$4{zB->1M&U^3ye?kK)?PAh&5iuJjIX9dxd4l%3UfU-!F@mn)bXLBxQ zM2y(E7w9kzW^jTh6|C&Cst=PxP-F8-E7J1y(Ku}2QNTZK>28I5xIA{uyB?HyW!j_0 zSR9pBd1}eu{?x*_z1Xl?>?_a*g_}lS5!k+xmjO=Su^P74+TQ&ZW8JBuuG?DhmsQFt zJ6)*XAV#-EZ+rwMMVAULOuRmt6+=f9w?;QU(%Wh{ZEh{f?{X5n6Rux{H9}cp(X$jc zonclAPrj>xvQEFeWZC*!Ep%?)HfzI%?f7BJt#Mp&i3i*%-;UnMq5gBnCP?iF8K?Fw z)-=kb@QaX|#C3WA>fb=?z$%tzniiJv})nPP9aWCjwF-EIu&NudVyNW({ zvBV(yu&?Iw=yMCitFMA|^`)WDBb8@6(bJT2KvN2Y>}bz8`9q0Fom)RTK;su9PbqkU zqGZQSNkZlep)zNnhH4~WoW?h=yD2g0W%Si>!;=ai*3@B1FlQfhyO!oFeST3D;#BH; z`&mpX5^^Z6TYAp(#~BdMy!@a5=genMt(kzlGC_moSxen1hkdp-Uq3~{73jY5Qm}bW zrsw2E?6t$tdy*-|&W|b8UX$BYOi|A$sh^WGEV_fGh0lvc6-!iTqqN*{s@^*~LGcgf zS{WHO@p)oK$>LRIa+`qBma3kkqIQe~cd=h`Gw*zb&>>)!Ao>-+233oORJ%jZZBt=? z##4Ct%~Y!yTr-=_2(___?!q6MuXx2h-WofoZ%|kUUFWe6Iv;$u!DPf|-CBH3^3n4i z#4k7wu2@^*fdcVMEyY>GIF0f-2=3}=NJ7`S$ieD~8a3+%D8?z1{%*t!GA%`(nqXhS z`^>J88CGm|;fdSu63;vhQF4Crod0ga%=OEpJ)4gyw!$O)yrpMxJp#z#` zAerLi4u8!VRfF<7(WORrn>r;x_GVaRFFle%?aC#Jvx&kBo>%Bh1HUZ2yQCol;;&7B z{Gw^C6b0(JOar~~vkK~wO%w$H8PfP4F2Vbp$!!D{T9W98(82`s3tBXfH^#@F@og=I zS_?`oF*|~k1j#xc$q7mZ#^I$P_Q91LEQl8jui#DwR8(5#R(5xr&dB(G@c<&v>BJg7 zlJ+k*oPb*To#sqZ%!m?{RM6Z2-_a9{ffSei4%1Yedxg%d!_OPkVe!(mV7N6W9e#zN zoXUfzv%B8ZKpMbUI4kx&X;a?OgOYT4c$4x|3wwu1m^qD?Y$mRXlaTJw8ofdR^sMR@ zS7_yx>$|TSvTNM-MM><69W^r0z`X&OXjMU^zz1reK#;}SL8T_OOBXXW z9{W|44gmnf$v1XPnI``wbFJvt}H!%V`UrFk>t-}jST0NgU_6XDEsg#l} z(GELYxcriny*qh|2ofXo^GMxehC7@Y#y2w0j|n*QsDrAI+w<8^Yf^M`4~6T zWEs{-v_Jn_J9c8D6oVZ1?M{T&=#j>1$U$PE?s)N{$99u7lP4^dzY4$cT+sf4%(2EZG{&2mC_+UALLfR zsoAUqr_uKFKPAo#3i<2Ouqj`n zKk+<>{K{MeAo~6g1CY`+*zaHxie9D(0>89|Xe)|okUQd)8DH{SM&}d4m2Gi!>hrl^ zzR+?n{rL`N1-wt``%8EUOa^?`jm77;8kVE}Q8zUDY(+u*ooPp}kk6n1OL55M0#fiP zjleQN4VRvSeW~yb=?NT-t>oLVf56efFxh7NUu?9`dKPf~kZg}aJr@Ohnn#BpZ4~O$ z2A2jJN-fO0sAC8L%VKQq@Me~Y%EhNnO)d*^#cUR2Ta2384RS!0BE91tXP?1ck;-7w zvqd0ohMN?^QNa6{$*l(_eg5cu$R^+G(iyfY3L0qUwUfE<32bXP{c7hjf~}vBr_p0B zr1=U9?=+CSJemswEXM(woM4~fHV9${;Gy&pfXByh_2mY+cAkkPvK_AeR$|g)h?B>X zdz!P$TGyfwM|{h)GpZ2M3kN)a@?OzG9jh-Ail#A26cb{_1L8eBq4#(a)G#U%c}2=XNWON2)qQ%cIgO@+$u6^5w~b; zS3W{K<^iC=XV07a%D-sjtqFOZj=bV9BmIF2ucJbaYTrht-w~(j<~&36$b6i3XOB`u z(EE-8fi2CMRz&1h!V7p|Ge0y7Nlg3U_Z7i_%e>K4StpjUl~_Wf@uR~EUUB5)zpST_ z!CXxV1mxrv?%OP$MeKfG1^TT!p+Ih69`;b8U0nyD zPSu(mJ9C6U0i19!aj8d4fe57o4_3rfBrO(4D&>>#Dhh}L>e2BL-lw3x@4Pu0^M@o1 zN(VkL8ja`yY?}SESkPpf1rYKL9Cb6N(IZy8$RL2S47{ItC6OKC=mm{9Wn#v)zQ)TC zg!*dH>m*NG@!d|W%&94_HHHXU&$!%a0C)Mg|2#Wdec8vA=wlgrDkp|h!Se$;^R6)^ zlDA+~;VcejfVgCroEh{DkFAfpPI^3J^+n7ArpSXKA;i_0DIQHDflM29N zV2n86zB?R7!xect;xNWBC}IUgv-`c`fAstySAB6$=U-e1wNU7&I8=a1lnPz3f`v*7~~kmYB|oMv?dlUtdSNvUu{exxEZ_WdU<^?m%i*)1kBGCrnD&xKO*KNj{(R6Fy6~8x9Hg+3rpoEDocR598=xL- zpbQutu;t6G#sC`s@2HtmzeD6lRZgv^*IfxS3)0S8+8m3Feo0B#eSoQfjHvT z<5NOB_sB1Il1udYXj}c5uXdl$YL!;xgf_r=?J2V3wa(X|y_RAM(n^Azn}GbVp$uAM zFf{*q$*cQLTjY5*wXo`_p6k2YagoQ}z5h|Ip=<(wf(Fn#=(5|&HfbGhyG>=qJck?gh3?)uTQK= z2#}nZ6J%QrU~Oipe1%WUmNk z>zgwN-W7KSA0@z(Iz3WHQ9;m-P528sh2b{Iy~YNlUn9g!tO1*Jg;T6NZI!CcbB zO9-me!-gWa8iX<8GH)H%20Sv8krp2m3K-6=SSU!U5E&T7K9`9|Tpc+r#rSwfVtFNf z+<##vBZ3KXj0`rtldV9#^m@`8b=xis+H62@KqpAk)B1Iva9eF9bc5cPf%c2-Tbb0L z8Z&5?KAxj~3|jDy8sNxFll5;h>(Ub=xPO7}cMxl$S8e)gk(jQ#k~9Cyr4?;b{7>Ke z0BVX%Kw6_b#esd9Y4K44GU9Jsg)2N{uIo=%_Ua)XqjSDz?Hj8d?&$t zQmx-k2S^;;aUv#>nUEaQ3*!KO>^d?M$Ok#1Y8*8*L};Zk8F0=2V5Y*#t)DZs*{rUB zw67x~wf>SFT5j-Q$D~JqloeG;q059_5+PE^$moi@cI1*c)Z!pbM>1g@2f%6{QicsA zQ;v}#b#thP0Aq!jasCe(^#6=HgT`ReFAO^85V!6jHwU0rZFhDzRpoK5}?M^Bfaz7rG~UT&L|zN%6ETGOJowiW&REr=_aX`7e|PdMWOC*)|ab6-ay_hn!vo59-7>9bN3zQQ0$tWV+s6%y!0OEollQ;MBZVUx6J! z(V%)%gt0Ur$m0?GvP;u#s0D-13H6zMMB4NXFAlFi%6~Ej(9rf>%>s@q38WV$se75g z)F)iof@KxDyGKe}`NCGlR4InZeQ6F~7@E^VU)akY+)>t+DpNhQ5YTY{@Vhz!DM;*#=>j-rIpd6xWmw!xS6_1dpa4(7;qga>~23(3KirZm_h1FY$l! ziobM^Z9l2I&ry3aJrPSVB%J^g5Bzp`yZeZacr3cY*tayO4%Hm5BO74wUT`Gta_(LJ zhgD9Jv>vk*FYXED>W03T8m$Y%z~r58?0>c}_cvh_0HWW1gy%~!z$sZ_7Jj!T)|@eJ zt);0{=2yb45SX@^`tilJwH9FUQIdyTT|Uts5*3iSQJsyc@L>VLR(=fpzlrb6x?zqF zdJSNEJT3})1kndYe{p4ZQ1=PG3%kC;wyJ zG2be%QA@I`3I*9E93K20nEI9~w=Tp-i4+W<@tD<8c71yFJO*X6%TE5-07LNo)K#t5*QT`C>u+PgLc&X0%z z;=Q(F43BX_AXu{c4tkT5^5^351r% z>(9SqQQ&q80;D7!kdkRdqAz8To>NmZ@20R?KUkcj z;}2A$O|IjHzc^S_j;&=-#-y?EH4uOQzXoWJMm7Wie1}J<7lB+#;<@%I@kOco(jGLkRk5>J`G*6?e{#@Qgf#oM3 zR5qdU4C`st7F_6#s%Hp|o*W*~g77eL(sS?JP$3?$7>~4yBMJB!SzvH}nz8=ZDG=(V z&c8sH>FW24=a8Z&6@t=_qzWs5VfITVi4`Q9Y`Oxq;)F^7SsHv}2{jxj6Dg*WHdLf# z7Pj>tn;<;a=T`y7&cws z(sTM=jp7rU{R_=v=x>?WW-Ax-zZI-v6&%}XC@(yG@Fy{nnr{vUoW)fWuYG!Atsh7R zLsL&u<97L-njSLFkW$rGt$<)+PjdaJ8c#!-4CZJ`*{7!<6HvdMzqx0$K>B0snQ!dt z4usnI-~Ocb@Q1I-J9lPneFBbL3Hd;=Sf9xZAicqS+h4sC+?NyuAuxnyh>jI8vW=SD z5nemC)3q z8G-lED{G}FWQk;xRP-H{ynxdlp`j$ZiLm_qD9^?Jltw)6(L`{6?grkp{^gCUWRcHc z!wV#mg}*>PTKGo}VwJ+JIgQW02{Ro>VqXGYKM-Y%KS=g3%5|zf=FlThfF)}{ehQ-L zB4JxCB7OR&9&p;)tB7}YzujJPp_BTyhn9R zK(E-06fCSsz3~>YI}-`*qr~UUAMjTMkzT=#71YwK9*j6r(B4t+0w))oES>IYGpMaV z2Xxsp#X2@aHnbi0HvwLqcQ3b@gSrTC#Q5OGRmH|@6_T3ecjN&J&z|5~uc6jC7xU9* zs}cF}NpH*v$_Z5}6WA|23%8KbKK|35+lK9+9-S2E=>=ERbJDXpVPYhhX9*5?M4DxB z(*vBT6*KARIaN|MHG~dxO_Lj7^$cvvNtem$bWXGF=M-Hk;~L@~Jh^4@ArVwiU()x* zfj1E|KI`t{YbYPRCKd!Oir9u*b#a$|j2oX>0G}jn*0#a?wb-H^FN1VPfafloSFrt# zuc@MdU8d^DdTKLlGk|~x+@Qzx{{6<{gFm)_2)sLvNBx`*o_u57AKxwjPm6X)M@oO) z{vt?MP71nCM;=;OY4sL&Rso5fjs7k0fQyI%`u_NY$$$;|*W+?IRA-6cF)a&VxZ_@{ zDLdmTp$ij=Qv<vH?a;3W4r<(Cz2Hko`6hywFo<=Ov^D?R-|;HUyFTzDAWd{4WIu>c zYDy$Tuuz}Q62Ty<_XPH^6A?$#nMgu9NcRPB>{s&83iP;-#wv}D#0)6(Yf&_w&%{mo zbd&1kg~If(YzVq3N@4&bXbl`>d^+auGp& z$O`ASK7wpnBdl(~Qy&!zXxGL_3wT14g8uUyJaX9NO(U@Rl%X@nK!4^Lb^SW0L6ben z0>5zTYtT*XC5Iv7+-tZ78C(RWnZu8`LQdX|X)gDppJ4!(!gTmFa4}#XKD`{2t+*!* z8iIuu4qJbI5rT0TFOee+td>w5?)kxo1mfQc%amVReo>(tlo=1D?A9EPGy^=-ix^J3 zTK?S}AUoySqml`rDPW$dK(%s$C-CYur(v6gK zgOqd&8|em>1_@Clq`L%ZLFp1yQcA)*8$91R=ezg* zAvi+&*{C^^1LG{+k6oI>XLBZDqtds}%n%An2lxE%nYiJCq|dZ5%pfE&Dc2;k3?L0r zfi(1jfsQj0dKI4Kft(oLN*!u-7ajpN@4syo?!^Ofcv~i3=HV#WxXOhT$i2p?oJ;|*~32)`lQ4|2`h^NvG3*0jOnh}U@ zpi^U0B|vopS+_byKK$u-@02J@gAU0_|P}+oy=d{aFWaQLY?wSW)c&E7KG&xGc`On{fnQMnlA7y z0U9owu5sKePdu|br0&8?j>sDaFruWFQ${PueyN~kjee}h6e}{q=6h3UcR0a3GNnu7 zBk+-2Y@N5iT?rgY%clT0v8%pOxHu@9xH5wJI8qM{e28)kR2hiuUxB~?WwDMl{9X+B z)O7D;mE}#$fw-Pb!SK+{8Ue-2iFqZ(9$sy4w{-;p2^83xaR`KUr*> zLJ;sxq-Z_kTJ6w`W1r&tR#6nAxOT>H!ru;D@ZpdtFdI?Ng-5WjLFNPF0S41HNt_Pk z&pQ7KVg&Ic^e{mFqf+a>%%=-aK^Xfv9*PP0Nlq_xz)zA0C3chG0BsB@Pp~?1447$V z4!iR8ec-^5Do7k-O3CBEaX;%W9*!}?<@=M8nK~UnuwMX9ciz)LoODm(z&@LoAjT@f zr$PY>0Hbx!#UYB6YywrpR;is;babV5Y-v3#+4}MbFam(z090yRf@N<3=@(VK_UYiA zmj&$jMc{j#+aINiSX%#9(s@S)Jr8CH<>5yXh+oJUk0mS+Jyj}uIZ+DJdZ+1rWgOWSmGSm=t zEY%fIv)w9Vl{)m;l|$@lZe%FBF3&o72p+=SAX$IsFdy3R8yyN?qgg57=O}t!8qI0B zDA9ylMH~(i1BQ%K^09J=@gH5rh~pXfGI-1T06O^dTmVdl<(wN;%PkWffPCbKx_{5K zbvBujuPs_vqs}t^B#h3P14mKO1eXZ`u=chbvZB4BQz$;oEC`&_uL$Bqf7!QJ0AMVZ zt`#u$|3SvcmQR4sbbTSf68glS|H<4o*PQ!u#v3Um{S>dAUMk0=B~i3Jz!Yul(^>Z% zt2BaEY@T}Tf*>vmA*PBoobA24hdjzVV*8g$(oRVLHTiH$BK#0BfdemFMmSk#)z`qr z!UGZM%lsCrH}RPb+5qjo0MmL5zyM3_gMwDxng^+2|4_AZ-I??P#Tle7FtP)n608V| zl0Ui(O7|->EF49Y^ei(jEAi+Qv&2aNthPj5q#b{LGyz-i7F67h-vbMN2w{N(qKyCb zHE=_O5*Z#3f{5!lX4{20C1hGvXxK2eKC#MvhZLfX_~nQZ1s24qWXz3T41ENZ00YpN zyhN1*Bx-(1IUgW*lF)jpUFcvZ6OiP-QYHP2a&W>st46}x_&!OZ3jKHR3tHg0 zwi`z7VY}x<0)RI`MDqPlsO%g49V)XIMF-jQ2Ngzou0@ab zRXxen-&jVxyQ7GdOZgTZ&TZl~!)&H0OE5S#>f?HxvD#jDV?|)+ncoDc7jTInJ^~gm za64}z1@Z?X1Cc`T116-y@-11QIwwC)egeYV&zyLT7DY-0u7yWd91-V>9xlk%Iv`u) zUxMk+rtxhl93U)}0AU$Y)I|v1x)BJ3^yMqu7K&D~=T{^rYv6=-Capw#;9b2&`hO8( zd+a9|0dR0W0P`kBVnB?rDu?tFEYATpHS@$4fW80SGR5N12Hfb4Cix^v5J3ml=nrWz zOr*9m=p!uE9}Hy{ge5SM{rp1@ z^vDtzi{tf(Rdrt~|1;Czs-K@a=1HqAAF$e&!N|xfVfUzAFzio+1=3i ziS2=ccWxf&6Cm;F2ya<@@NWN_qa-F`nfLEeF5O>7r8p<2tWr{rmQ)=d*IML62HXp| zkrn?P?%QEM@c9U$1>cL;^<}jM{oB9lgl-|^%=g4GXg`;jSkX+(3Z@7^i z)*H&@gfZ6|6;|Z|d5I!1zZ3$~g|uS=0GO7--H)9zrqF7&cmx7I4pDM3AhekK^)M4>%w7`iDck!0`yepCXj^PSASc*sAH>OgXAfRG;eqX2h#;#^Ku(FPGzbge{m_306#_}hFsbAi4{^0fU+dZ&_qiAEhb*+(tr<0luI5n-P zOmRPVlHTD~Dj^*>B*(npiLuWJIt}vbIktyh23+&bp=6>M^)Y6|m-=W7F6}}q_G~W;2`4486z81wlki*Mi(JsLX1xO=` zC&TN?7w-w+QvvNOs9j`Ilde;g-LsghZmBJ{{>YtmfqXlY+TI9Zo#bKmYTWLyOM*O5 z0^=7|A#JXA2z-Xg2~_CuUQesit@_J|X93m3D46>JE#dShgvuyKCjW)XtmR7_BW6)h zdI%7CP;_m)l2hk7`z$P->-8m_Y?A#}U+iApN2d0cWxx_>u@G?qU-uyNR;kkp_fMP| zQ1jcCUY!U%`6XRr#Gju?D~yIgi%U&0Fmm3WhV#H#NK6Kyg6xnAii;Rioy#A^oKrorgR6mH~#L^UDAa z3uj9n<$e;ry#qkE`W!jLi^93|f_Aoo32wjI3{L~Jvn*pto&V8K{+j}%+}ELUL0cdT({fvw@HX8LFq;A4;EA^FRSU2{Oz#l#Gl41VEY%F`Z-v zBcTR^S!IQM%jGiRel}n`J^%z2Kr;V_3I%h|WRLQMgZE0)+)@@96n@Di>Cf;T_9G zI#xsqbO$*7AI7mv=wJ0AYR?y_ZsA~?XRi1J2~dOh{-9Tjx%kb}6)~I8--J!;;tf;>(W`MV&bj;qsKv^u(uw0q@p2^ak)X4(h zrWlCt3cBPLJ@#1N^L0=b0D(|TAO4r5PyPqwpMo#4MaBBMhkMEeFTeS-8}{?~E+4Rn zzN2+fhXZZ1o2NrOiVgR^U60y8|8le43@m@s0NJ9#PU#C5x)YpEenERuBAS0{1@b!s zokIa}*Tcm_8=eByxT^(SBgv(R_Bh22NkJfXq#s88Q+P%kb?BpEGt6pNzGEq9p@0As zN)l-En^*QPl$l&o>KsTcLx9Axwj8SwLkdSRI`9Lnh^;t-2*i)5pX3OtZVE_YgGojc zCji|NCQZIgyT8i+Vd)hhi)?zP3_+n~wbVx-5q*a6np?LAFfp#Qe|ZOddJ;EeQ9z9W zXk${ZmaBPlAr;NjLWI5SO?nXsvqWHb_&zXb^*9zMhY+*PZ{7oKBq+NZ4pWmA zv;i{R1kSKNfPt(9&=ceuF!AA975?3QBcSs=;~)SysLT1ZvZ%A}^u0i9MR*@1M{{4`mN2O>T(AKz?P>=!W#hGgxrQkZZb{2}`s&B= z&$`EH_gHKc;nx*cNDvesE~-I9ME760WTNt}uiYr~wK;OzH=b;RT7!|LjyAg64Ce>J zrH+?y0El4>6A{THNMl7#Zl1Jj+UEe8PQ zsnN=ue?^@o1(1=g)~`P6)G9cbKM0vj>>lKC*TwL)LTpK=*n|vO-1cZ)mv>NM>5DN0 z4ADt(7+~11;nr;dMGCa>z6)T^*zvW03K&4)zD-Z!Wwl>NuO*vPC=4>06(NfRaN3uj zWbSYw%dI)f_K|azXS8DY{u%ZYg}{R2eeg8^POf^y0{kmCY>d&^1EApCOX+j0Sm}*J z7jJ?GZEY_eK-hfuquwD+dV1_r`a#tFjk01PfC$dbhYf;{f@I-tU}RF5WamaZoR<$A zHb|3iVDkLGH6g*@q7D3|R10>tTHu85WllX=`Q9;qAGCS7R`FV(-?>%i!%@kIhr{q* z{~4Ye4}k$I=)%;H|79~F&0QQY>q8=?pghdkn+qjBokCf6i_;=r92Di8L8g?2Sub7_ zs?u3rAJPFi5h_p3iztel82)ELSp+(c`6Zwj^vrAzg4}>OMicT_%!*H<$U+Jc1ipC? z_(;SX`Fds1G&ILVI3cAVW(g8M0~(IR@asB#1dkiRdh>nE6>+0XR%^b6P@tz7aGCIP za)>qo@}$kL9enBHAK*PbmU)aSXds?GsZUu{rw4JVa0gJmjn*?73|n4D+0)OL46K7l!( zgKPOSuV3&VVQtT{2=Fya8p7dq>fv9|?qHM$RY7v0&vDgr^}YCTC~1$n8$ z#+@7U$}Anwc4b%IWYzhDLpW5oLMS@T=u%MwnLq*w(3l_V0T)ky1o`z^OEjdxAr{fe zCRJGGs23Y1q+}8uSR$!__YtTq#DPgd#ye<(|H;@Oldw5khk&kC?#Y?Nw=s=l3{f%Y z#x_vFI8)r)0Wpl3QzD}2b6z}G3#oEp1 z<1>i+%vv`h3>*MPkyM5rE2(YoSInScTbISU@le5GIAg2VMf{~p!sPO}F^W<0=$)11 zgC5OlHRBM#0!P;@Eors!fbmnzTMrc$Ta7aURvUS+Xc|V3!D)20TT-vBO3b;9_H7>s zY6w0idx<@tLQf$1v6#>byG@b5!uJmXJ#X7r?mAUyO#XC)K$}~}Yei+58!J0<*FJtl z@R)kM{U#c*Nmqfvnk_?qU?ug-5g>~Xh22lffo=m>J;_zq;>3Xqa_&Y*6M^anMCbh_ zC*pnR)>HTvK%99PPdTu@-jR~@b`T&2fxrN(<$9kS>bMhscCIG@sFm#*gy8&NH^co& z1E5x1U#wj6PjU1(x`OL8xXmRV$eU<1$TPI5_#P?ESizsoP3QL?2MB`-YLj+IcvSp# z#kyQ$x;1B}uV?m)eq3$)6ZRmNG+bbDkq$wngczGL-l&mRfExK7iT!2~<*#0HphCIT zA{R=+)#g}ZJ)TDX{dy(`@+oNP;Q6Mi7%D)v1Mo9#+pv8;DzgKFG4 z>EzGrP$^Ylks6AL#fEt!SLCtYi%z6$G0;>q0a^ZIoLMEhXqchQ0g||q)n}7|yj=FQ zSNYAgL5bh88g2ywF!=0K_~733bZ+<_SV!9i45o>N%!3U8p;NdZVa?LOp6Y9 zut;=GVA#JA8fJg5xRAl+6?JfX^>qWj-b_K*hv+UDI5BD_8MgSfK*tV{PU8JJ4QFJd(& zV0z&?z>Sr~0c~Xo-G4p?{EhYXibNHK5gc(}AviFOoqBJ#AcdraXG8);c64IS17uM2 zdK`O`BQiuo!2@;;3o+lm^+Jy^?45U)tCA3JokGglvv>$V)r2y?04KD4*Yjg0#KnW+ zzAm|MiWL+E9vIYM$`=)2P&*s~{e#7j2l%XZR&Gh_J50+CcrY~0Er9_7vo8UE8ndBR z!CB5{kY`DS810(jm%rY9ngj1TagcsMauh*4Cgae;RFv|{)bD+#8Yvf{9O$?tA`h^< z5o&k|TU_RQzA>3t#dq@`Z#!oanCWBg(VJbq^!p(p=7!q~z3)RoO*L#pImdJK=)AgU zDG2j&|KkoVUAWpnj9;Bd6BtW8Woh1J4|DWXNAZ-J{<4W60Y~^Bp2&H=vlpt*0jjx! zMC2hpU93E(rQQt@sSi`g>#NnG0-F}$)SE1D{z$)d#->o7g&9;<>!hIc%wl54`$IqjP>^P%KLwV-w?AFMl zXc~sV%8a0UMMi*iuai?q13U!15NrMJvdN$@)oLacJrm<(O|gEqzBwAspaVru*HM8lDz+1GBzGvoB;BR{p&4-2_l! zKIF!Gc(>LBIH`0i4p2Aw(@GhD|8kxtI2W^I(=N50z;Lzr?{+T~4rGZZintJ~fQu2g7w;NI$lq zbPRT;-C0f$ohdLyF9hx_1ppZ%r3foXL22?kfgz`Vydx11OPCBLcKY3RiRZScp)AeW zzMAF>5tvJ1#`Limz-|giY&J8-i5BiZsPBYAXqfCYZkgI0Jh$aoW|Ek0L3OE&S8CMbRd%zdMlb<1kd@RSOc)5G z>_PBVXYF=?;4=}jZ!9@MZLW?)Xc-T^{4mJ|v@*l+v-q#paTxE%g;souNy6Q8nTCxh z7C(kfi{*rDdc1~$fnkyZ=>QleVLImb_T(@~poS^7u`=omcQ|YO=lQPjcT%(bFOARE zSR}<882V5|2-c?(Ns$tHQh-E=RLigBO|tTURUn+?UScU&bigWj$1hh_cK|Sl>kfF& zKsB^g!ebu++|Cu< zJ*|GnN{^Rkl;CfSegVj9P^S;eK(Dm(41pAwI@sg^Si-C*kvQsa@HrXM5C^)s;?z5h zGewU9s%dX^+o4l_-x1g+v&=-a0P=xaJLs>U=>e*rkLGkyR7LA%v%lhdRT&}xjr1J5 zXgl2E*SiJz_pX^JmpG}!?);(aNyN2W=x(N26+LkI1ag2}cpe0#eHiOsog|O^UindN zIVDXyE4aj>perY2cm>V3u+56ntxz&e!^Y++MC1Dx@e|9t{9l-?8(VDgm$Omx42RKB zg3xg0fB>$jh4wYL!!h?c5VbJdwUHsfU<)hP!^ujLxG4M>Q%noHm%duC^aX|! zoTnh)01IovCmp#!oebrx)6LACm-nS9tW<^JCzZ)ymou~gUCY`D&!1jQzUReaQ^(Ds zUg9!D{hIu_(%&5*+PMEY$vg@-V){o1uhi>aTCL(U06ln{qEbdWpoTB;Fu-Y??WBKw zHyRMw@WASbXhLMu`Jj8)Bp-70f#?6J>=M!E@W}nkWf}MkFu;ueqape}6H37aV7?Ak z&XRN@{!C8{E&K^EtJ(qdjYz@EKal1$159hQNT$5(eOrYqB|#M)4pRnroA{jS8d}RZ z&&nX1XdHr@$8ys%o(-Fh3uA8>|7a-ucnwcc2r#&Fx!5c^a4mqWk1U6WxmdsjC^s$^ zpN8U;R;8?!VqHU3kc7b*&ChcZ|PP&|?SPo1B%?s!yYma#7yvoKm8tZ;|@EOZl6 z7!c!Z0}z7(`H_8D&NGP~ZqxygSex$QJzFTE&2b2^J)kRh)m9YN?csrkPQZ=v)`6z* z7`fx=|6PBPv0(IQrakGns-XTp$tB+ zYoSQOCt0taow-+Sy_By-Ge)r>FMqxT6F;)yad?qqf|qAYi6U5km^C zy5uECL+;$jCkOg17YS~3ttohaVVIT5H<}iKZc3bbJaM>1Hid@SM{J`t9 zSSdnCkCxQ(%v#mawgj>~y^HLSRs#E#mV7&=6DwJOJJ?mq#keVq?du}qQ*O* z2D&k!E3lT>>vv~PswUFA3jhZ*3=Ia+Omu4xA$lt6`}jj>qWd}DNOlT%hhyPwv_mTVI#%3>!6th=`ooNYkkM`%lu*OMa#Kq_&Nm?Gxt}j-L2_+s{Dc3%fcot zvsZit0wcjixo(Z)-uO44qB$IN9yi{2AgZRX10}LN=n3#ES#5eZOVvpppmGABs$q7) zWz`uFC9`NuxLh@~2r%NMAFiD`^8GhGkfU>aE0w{;IX%Nxzw;iGPKUYIslEU5ncKc) zMtyATmk0#}3Pr7n9bW;GDys^vs5kUfk1mpg`#TgLyU4oK1D`}36Dn^W9;7adcYQon z4)m+iQhpe|eP!A)#5Q=(s`A91wer0HiTb9@0g<{N_$emmUlC&u^nmFrVs^l8Q4Tam zPx3JRaw&Y}Ef|Q^CU2&Cs@PJL>Q^`KJ^%d8dzX#ZLIiy+x6L^!&$t}btdeE+%`HwE zlZ^M1`o~{LQhmU3A566zF`y3VBYbAX_CtA_#|e@2#?vCVIKGHH0GwZrSX5_iEHofZ z?+jS!E5;km;(?a%Mk?6?<)VS*hrim*V?z}%Mr0A2^y^R_Z#S9X(IytIJ$IqA?WfI8 z!C%#*P68JNs`u$gNbD2e0YKWH3r&jPUsoc;OM}w)I*VJ_7_>4;{V>b&kC=0IBp`jS zw-bR~&tYWx)C1h|BiDLSKtW#cq8C580mpcofv);AHp6^Utp}(|rO0kZ`@a_3rKwIr z{Nsi^HpBqW*7T}JP82QCD<7eUMfNHT9o&r4k@K|hrIO(n2L|4!RlayQdpm3#D1002 zCA)M_ZzdeIo@oY{eqxnOndWBys1y2m+~$gb5@Cb9aA2hffjWT%!i+)<>^9qt6;w~a zg%^_G!iyD#m0}kQaJy6&`C`EVkQB3;=?fj`rQEP~pk4c;5ZMAEimQ;>clKz=%U?UH zB7bpf`0VTQK}zo!tZvNA{q(A=1_pe2$Fcq#Scoh?m{oa!wye7V58}g3$Qc2|$MQ4- z+~aA8#sBLd(#uOoUPL5Rx%MH$I5dbQ(tHXaivQ z2Q4UA(l4)pnWJL{hZBC5uMA-+E#O!)$*onP#l-+afT^)q-<7ID!=j1l_ndAnTGhhb zf%3(L)CRm$jiz%CyxZe?(`waUkbJK|1J8KR{JK&-*QAar9KI5#S2$87kv0a9FRG-% zYMKx#a0!^On|?R)PIpm=ahX_$LID4VkcJ#Dm7Mu4C-0f*;H4BZChvHjBe@Bh zT(6)diU{C*@Sk~-eD+Q@cHNOMwI}0K2yoRdjZewq1X|sV3ytvceqPBu-BomER3;Xv z5Re9Lt^#Uc^T5ZcKKAP#UL(hZgzN4eF|M3R;Wc*B!uJMdXM0Ej`PY~tlmq6a#PO)P z1+{He5s>>}Q;(It5UGBOrBk(7yyIPW1D_OIrwX3@gAzL%3Q82SQnZLdGc!GTWahl5 zc*{A*pSkGONiaS%pk{FeOX3H@_>=I-6(z(@m}n*t2*BjA^ix(HIl<2bDN>|5-tC-K z%Las_T7M{-`?+a*e z-Y3>yCI{Ph#slWMmPXSbj5%fwYU z9F)=kQX24Ys9GuH?t6Lxhj*Wzm~yC7uXCE4r#(@T!>5O!NKI|v(a}xD|L=pHX6|`DyjCOQtt;( zi09LR0XD3~$2JpjeRn>J9n2Gz%Cfr-8dgDR*9$^}USAH)H6nh!5WWg}fFAp(Ux#Y% z{R8rT7V*W5Nnen%)B2u1gFAA9>(Db)4mmeP#Sxt4=&Hu@J@)d)Qhr5Ta(EEMA>Q==J=14hPydj!Qt8qBPv9NTx{y2fb>=2f8Y z-gS@|Spv!=KIa*!=P_pdhlt%*-cg5D>fEqflhPLmkKR_&M`f16P95>J>3i*AqY4SK zhZNDR60pP9DES6qXgqY!IL=R0bY5Fx3PlqNy%e11^%_|JI>5$(x1mRy#8;oDzN&hq zlp*)rsYEqPI1F^dtasm&ef$V_rI1BngC6O;^pbtP5Et1-$bP8AXXv=s6)b~V5K_`j z>>(nuL&|p-csb#cj-LdUCy+Fd#acV{ru1ZPK7{21@#@iZO00ORvLP74#aqqP3(g|R ztfu9=o0Ra#qp1?R4=_ghiJd^e=ynH_ljM;iOA1{|3O&V}Sog|g5NndC@dq!S1%{&Z z*{Z}B3?(1HICCpkyA*c;mcGsAZV)H&3q_=`+pz9Y#}}^ANb2WM{_s`F!s_EeREv{#2m}}!wL4LO$0pezY*DhWjq8}hCsMAMd76`6tU#Yvm8ZdcLH_X z8w`0I&>J)+NQWbXXFST@rO7=`5*K-z*brw7LZqn{hxd`YkWJgVDD{J~@uZ;9`U>N~ zNag(>)y{2nIvek^8J4QamPjl6@Y)|QS$nEthq*yRt?X%m5Y5e->XNOVXX(1J|23hC z4i*}FL35KxliIAkk^9!QtO*8BFXuNwMM4G4cjeXv^JYTj40$ZcO1fMxw;pA zl4La!_yoDD98)|_0y({|F-@7_VP!#&VRrkpk0=F5DU-P55#T}w+%ZdiAyoYoI*^Ye z9Pi3}Qp>I_SI;~|2UUSgV2Qou8G;b&SIC_%mOlEt<8ZeL8Q&3j`9?w&u-tQoE`G{T zz^Og-2n9DqqP}ldZpzooDy0Oksq$$WkR^>X zpeEiiz6)wa=}e?x+%5xg_TdJnHIy@9j_dIrlhI$@@HYvi`9jLVmb=AP7EQ;dl zz5xjBCDU;E{VWR}4uQ)u20D0IAlkv`bWqgo_B@esOX()~jpQH+lF&%ig z`%JmQhq}8k29c$a0M(l{P`wQ_26=xQR~31>UHh+ePDnbd+c2{je}WZAd6s^m%KRQ& z{AX{X;S)O7Yzb`&;)O2${V{xtf=RJT!N*{dONtG@o^%f7ZOORKq;C#!Go2W zQn<^a1Voc8vn2j6F=Vqn@DFLT3J4Kx_-uywt(7)!!UIB(TB)oBI*K8I)XdtO?8GOK z_mNXf@UCM2O6&weeBomHr$QNowRI)*X-?-5cQ2Zw?tJ-p9-3@-e7d#mhZ@Wy7JwBI z#QUCgH|$mo1|L&OzF!M(_I1^(A1S%#r=QiHE4M%fvU37)xI%Y> zVs@G2RraobZB4HACQc52>pnlfvhk^JY7v%LDv**A4}KDSp7>kj(&n+>!7rcF%k$lX z%Rv9&^P0(T8|G4%Tfr?CenIC~$Dg-<`rwL-x3{%lo}8RqWqkWKd*NDI`hK?Z=ycMv zHc+Hpe4#b+cUxd<(ABj^^M!c+&!4_|u7bzMlMCHPjDBYu8%qc7*M}P`*Iu4&zt^^Z zUfxqTQ%`*C`r@i)iK?xg$u&6Wy5aIkP}2qJuk3mHY+t$bL36$A6A6hg?YZV-=8M(4 zV;Zwl{GW%fj;Vf~d%04GgdcmI9$ru{{l=pR#?HI=v2w8T!`$kzcDTChn}PzVV6&^s zYk%+GBULyEsm?VTu0N=2T(?Sd-Z#7-#@@RJ9~|6ty}x@TN75T}y}I#p<7#86&OEBQ z86!&Ta4dRqO#fi->A+>f(fO$z_-W&dVKpxwfANL>@%Bda;49(!(*FL_jNf^^-!^~W zt2tQe+&Es>z7{)|ik}@^GUt>mX!~+?a0X0kzfV_=J~y0ZTnu}CZy%MC%A;xv3P2}z za2I%YbbU!89wEh*Pt|p7Y-%RHa3ykml`=ZIQz=f-N_{a#_2yIVb}mN&{`IEkA-nKn z^`;U#E2&k@Am3AuYd>7hH{TEG1S`JuADOi;^?D5i4aRYcpN?NQ(8NS@a)s>(P+T26 zJlg%GND>{O$FG<4;$?jt1)-3h*7MaFRuvtJS1)CI6)VfM4hC7@m7S##G?64yvWixI zDty@xr;52Vo>ZnqNiit?YBqEaQRMB6965G-dwf}N;DC@{R>sD6i=w4xMFg!JClx|f zUWI+DOmmx+rC-Xb7yZemXA$hX7)4DZvt&=ln+FNrYbfK1y{b+yAJj_>*CHPBW3IAE zH!pt^U|%D6!8UzO{|ViOvZ($`hW=4KHVHbll{f~8P?PM`GkHsPAvABcg|Z{pWM`_N zr{j|&l%!mx2=;nBMncF|$-X3m_ed(|Lw1T?O-1d? z3R0r)3ii@rPpmGnC(K+n&OOOS4GV5s+@+32PJti&mw&G?|?kU?$Gau|D zEuY<2H>&igl=*3~w2P@!8y%~^arEqaUGNbJWTck#q-56Dpi_sNESW|X_O3SXmEScb z5tWo`2B?&PL{|_?&rT?|J-8K$K#s3W`_z@} z!^DNrBZ7?W&L~EXmW>Pnvd0r$!B;Y64v(k8=n(`I3?p}9F?8^%( z7yCW*2Tg2QQT+M!i$}ZY(F7}=B4mI2Fik<7)b}7PI#mS&&%WxUT}o>6L%8y)1`Nb$yJkX#vMmqS}(Xl?Du%XrEbeRMyPPw=6nfKL)fGiDuZ~_xUlgi}^3G+) z|F5NZ^|W7XRP*XM?~YUKN@G6QZhNc$A%x23eij*Wrvh0b{e%jfJ?}(Bl>DXa4*Wau%BpDDsSp}RXx4iw&pq4q9rmD z-c-TH;|F~r{L-TI3$`8Sn_d9{BI=gGj5s{YLk=GYek)XVbm3sZcH`vE$S}l>UhJDG=KF~n z($q@BLYr~;83G%^O6KgE2l>Iu46kP#;4g&*Y@oKUODw=A;;q*-*T)rwb~KT>h^0PP zlo`;QPNcsxWRfQpBBJ+os|ojYA8TZES_Ltlqw}W4gvC2ntsG@VhpzWo@ki2Po^D`w zcksk&OaK1rc=Z*T(W%T9_MGT)ty}kak4-rp0t%z)(PTdiTBOm&U9CP*Ytz&tOB`N! zK;nW_{1DSpH^Uqqni`!mcsI7({&I%U{uMj?E0v>hck5bpGHf<-d_B+46QLb!gfBbJ z#W9h7bvMI(Sn&+?dl=#L!K$ifaOObRayn-yN+3qJikU;0*Fc-}5qReZ`eUKpudG0j z%oMr5PEM+wsc*{o-bgsos=ytcz_@-G!k1k+uQf6&!`>yAsJDb6?u~`1mT8V77 zriB+QBuGtpVKSP#KC5e#J!FoQRC*PmF=^g!v;tl*F@rZXwKtT9{_J-XO3|g`^K^4~ zN)4M^Qdz^TSGCB9x`uIASe}uPUnQ0uM@Rli4i>8q z68bkVD5z}ojA3g&&@Ei3vx{{=qYovm$XDtWam)3tm`N%ICr6hT?_INw^Vdd93{$Dr z*+M_ZiWWtJDOMR|{^-&nTR2+>62uI9aj5DsM*u!G16KCOv8K4^`eEq`_f@q?R=U2_ zsdE-7C2WN%bS_Pi0)8pRUh^$ z23RXgS;W$Wy4fXl}horo`d#NAQ&PXnZNPLV_viEWE0NlcL(>A9&TsGFV9Q zwd*Oozi8T4n4a2UVPjU9Zr~yndq{;eeHk32JmR0>6)&mll2yH5PwMC~0hyFl%jI~Q zo>{_;m*L?q*7S_7J5{`juYUYzM{ssnljdS0E=JCOK`r<9991N1NRq1fK%P zNTnoIM6DDnUhA!Dr6Cc$tYFT8ZDHP8Jl})imy8uG@rbr%{&WH_QJ`;N@u!p0SFCcc zq~fU^WkYEB4*S<}3dyHhPlUrd<{n|vlyzN-F_S$=h(t?dCFH(?tW~8;pPkFEq|1Q_ z9{@#gBBrsUAM6q%mb@FwTOVf1 zsN~h;AR1CxbN|A!eg6C*__mRuL=;&8*@MmZrU~sU*o}&Jgn4zC6BN@D_$xjq1e(M& zhCQF352Ho%Iou{#jSFk#*ej$VklCy!9qhA=O{s}|RU3bvy&X^*C7|R3<$L!QNip3C z?TyL|K4~3;5wjzuVV&|?G9&#~CDN?BtF~FUqh`3s^QL+Bg({-gMqslT z!Db1`eekVrelw1Z*uZX-8Oi*Ob$W`3<*!`zXRjPe=b#teI`v0XABq=H3OoGB%u#(- zwW&vD3`X`Fhw#wdtEPO)Sd_x&Dt5b!h&bHU9y0KHCoF-ZHsHpn@y*1UU*cy0ED}6A zKC!koA2wNI?J<-m8g!aCQ&cQV@>L!^7au{0R7JoAXBW%S(U!^$tfo3`03OORyGzN$ zQ@daJ(#L~Vfps)g;hU;6|1iJ3Nr|;}$qFf<_r-)eTjtKFX5>+~+%Hx&s9TV)9!X6c z+=rJV@v7Z4jjC(z))j1&YBJd$N6Ry{cM>T;UglAI7=f;Bm~Enz^iU|V?TuidV_@#B*Nv8gUe-`N{{Bl~cf5yMZyZN2K{wj;= zle7af)b(%Yfsg6sgk9;ezsgK4Bsspy@-?w}5!qB>Qv|lqWZGyPEaiyyW@s88q$Q&* z&=gcFuX!-idsgWz6tA`M>Y5J!NSmAEP(Drb4GEB!Tt0uuComR2z)!?4UZV4rhJb!@ zUxuQ3K~09ty5iT|O~k($n!+b7VK90GRx43n;{`3o!Y4)A#s<}Caol*@3$jC8#Dg3` z9V*PbWvBN|h9l7?eJOlY{i?StW@z*o9XgX1WxuG-2vk=qdD@J#>h~wDh}w%+J6Stz zjv|lP6i0zw4{G0t*=%J(+jMwi%0TAGs9us@*CCsgT`A*a5H3IcYblKWK7XVk<4&xO z6&A&#yfSa=Sf+&gsqwkxO;2btjgxq%XE3yI@-K5wYu{G9q|wJ;0_uZMp^U4Io~6Pe2iy~?6$rp+PF+UiA|%NefRho?YWJor)O{Woj$4j z_yTrtquBhw<`@38`Q9_SF080SdD(IS-2FADnqtHBXg@E7pNq6eau+1(AmC>>h(jwY z&P(c&LGnrP`9~%r-0E=u$MbC)PoGpk;kt$ygN8<6#^MV?75lBsa0w=;a3 zTE#Ll(xsQdMEtbU~41K&RPmptGi`<)% zlbS^LG~HcAjMs?Yu09D$gx`|TBlv36_k85AOQ!mD(TaPT4bf_|?%D!)zUm`ZNwZ*( zz8gXMHjzkL0Ud4iOwtA@e6Z4-1EoAoKk}~GrSj+Q*Qc7sI=U0)QCSW0$3<^x*suZL zLD`Tjy4G|7r@&g%4wj<+1W0jOY7h0w{psY=0sipsX&b90*s$xb?HL-}&=Lj~Pre?QWN>;_hdsxrkN2xg(6O5C8Jt#0H;UO= z)MSJeJ8s?Wfd^_cx)u2YnvCP3y5GQGWErfaSq@b`(UjO^S(Q}|mNg@XpDFDcBEmFx z1axdJD~tjc-^VFnCRVW5bL&pW+KTIsbyU!T*57t{uvtZ*ULkJk8Re(RzgI)eIekmp z5Y{x$rM3e?OGO*?iM-6EL%2M9IZieG$j9!44#jjlr;B>rhcs?`S~k?G^&xsow%_5vzkNhkmp}GcOxpL zdpF1uyLXH-T`wuerUCH(-UgOu@6qvr&lu;qEd0%+ihdYuezo;_{6aa)t2M!AP46lz z#ov3h6+FnR>3azA)E(wB>^8v+U*=8dPb%k}<0rJ2ypVPaael^0yAi-{lqe-;j>!9=TRAC4k;dN1FsywW%==RI3*%`7`D7cDLC{>C?M| zt#CKnoyPK!=^&hsg*1U{ud+WwY088=Rw%gg;1G51@a$QG&oT`@%Y6=2y<$u4B;-Tg zU}lp)pT%|n9OHEPZ`R-#3)-91Su?oR2X($$YfBbs*@*fSz^$J%)RGqpf7P=J*~H@{ zwhc#Z=V`C;TIBK_G<;#+`k4QtDH5+}7M=`kdfU4SCTcTOR-FV* zcw;MBnX~B#QZ#Ev%1R>jchLS&$MSZY=7*Rbx*2xpAW7(q8*YG!`d>NYjc)MU33qdC z-mER80D`anhqxbJe0-k13x~AQoJp)uL{`Z}rEBOwy0yH|<|16mFJS+y!a-^HWu%C2 zUG)Ps=cIp!JA;$)ll$}Tu9+22+N)4xTIXVZ;XJAYokAdWha6#?!O`~^6RQtBYpm!T zJ-X((YHDQbqGKmk_d)Y<(;&5)!j0E$m#cIPRpywU|bM4aT!U82G974>;Rk)Yj8q7JUf;L4bqxY zoqy>scZXpL5M-U;i(1z;f1P<9%$sp?bwGu9z|4oCPx^dfFACc?=uCa2I1saY84Mty z4!+G=W8Folm|19I#n-q|f|pT0z7JGa53-tMvcduc6k2w)>ykc$zjM3pmhpZgVQ$Sy z-V_zm{tx!t8eVx{e%yZXYX&z8bu1j)LqGQ-r{aQ>;)0&5pwnaiFRg)3e1lqj#h;)e zylI+8M&bI|-TJej;pp^W%Pm-}J+gqYg!&_<6y_7O_B8VRspgh556Sl8-w{hYSAz3c zYn^BJyCz*Vg3p5gjc50c^{eHgKp=Fq|1X}+cY|jeefC%t#(A<-afLFphtM`4s2-_` zDHs8%&YqtieuJVai)k@)5|PzzKA8M{H$C~oeMnxR-eYR-&*`4O&$rI`{W-pE8kpKb zg?#d~{N{8o);9b$FORnlC#OgSoZaV$OLrcO=9=5A1qPj6HC!0$2D_RK)ls^mXV`AG zy!~Rb%+m^Ct)v)D~nrH!2+qZiup6)He6J4fPxa89- zZeP+2>o43nxvKadWR$+#xEA8-cPy;B`Vu^_*v9E1uJ;4mjVDFPb<8R%cx$b?I(z3M z^zpPGohe_TDidnlQ41f2VX`;p&Yo0tLE{P=_U`8cFQ;Jd6^G~{pIXoN3w;fae2b>3 z#56Ky;w+zkDo*Ncy|P2!T4~c>c9igIT%z_Piu_sFEIyLUwP?DlfQ2GDwA6n%6S7DD z;=G~a7iUQ6)rfoK>qDJg`4dH?-@Y7^hhEtt=u2A{n@gh=XQOKB{yF>a-?e`~8JzWh z%Z1&WsGXx%8VtQSvDsvRQe6}_R}HNl-5(Zm;Kqs^H}zg6I$Ih)A2@aW8b8`QwtK|% z=ps;VuX#C5L_;kF-%=nnxB_+LjaueFWHXLD5%uo39l=$x_&1SK>?{96*H;Ea-FMs4 z2uPQ7w}2p>(j|?ggoM(qbW1mgNJy8Yv~;H;B_Uk`f-pnQ00Z0~KF>YpJvY7qA2z$z z+H3Eb`Oi&UrUL_KPumGfPR7UXF4xcp?uywn(8)PWSm5!f?q}V&mJ#S9CP50Rm)5LI z-ErX~79VN_C>UN!WX9cltk23c7{@lU)>b$^MDlo+m8mB#egvwl(@cgvV(*-P-|nMp zfM4=}S(7qX#H)m)%d;M@fbBM{k)h{052D^HFlUlvGDy9oq^kQx_>+tbd(w%6!$0g? z6c$yOSVBscnq4iR)UVd=onrXo7b9NunV`H}B3erb`$-C4PxC!|jQCcRuY%ZNR6;aU ze4qE(Bk~izohf7Nq=b6LMQ3pc$bEil)vc1!6CN(xZm5d=gO8H&pzg@l_yym{ysC$z znCjEt4~X|2q9R^Mu;Mw>)1vFfxDXzuT&FpZFk|&czS~lr=hV3`#}XjTIwO=$Qpx-~ zje|eTPqTz?jrrM>EJL)hDz@HKnesGt?1(DY*l#g{H>WC=sw%4!mbgcc-h9%hiVu4d zLb>d`7#N(|H7--$>4rtxr~HhU{vulq+q@j&`j9mnWFY1hCDk@nz^p6-*F%=@+k=ir zdV*D-KX`sk+n(RkwbYqm=hHKhleqs4TfzHXoQjVl;UVRTM|>YaBu~hXxIcoN5z(2} zCgUvV6`~KPoH~HTlgIJMY*;og^0fq6!Z8s|nRnS-%mDW6^(=M9CJI>ljk@C~XGA=c zneDHTHEyIZKaE1(Iej#^&-Cp2Ewd#LhvBojYaQDN<@a{?(bU*+aKg(uIUwLn#|H@) zP%2Z+97*lhi}zK;tz!7jSQsHpfWP7Wv|!1enmP2%HX;B|M^9qio&K^qZSoGSK@4aXnR} zJwI8p#CKB6uPS2CV(usJefK5sRz+`g_1_rLamy>Bx44+MtnKqitgxL)bu8f2nz2>h zt|t7}?)vo2IR1V@3?x;ITA3O-hYwr zBP6)JbB$m?fvCS2c=1-<&_@|LF$Tr`I>Rp|ESyC!gEK`yUd&)NEUYw6%9^*5x$c`z za$cPxc@sruN=MV4imXvxP!0)uG0uY)4@0K;#Hio65q4Co=g_44>?U(>j&B*(3rL5E zg`9rXqE86rz0>q-BwEED;A{&ii)w6y#f!UVJLU-gw&J&0U5mh&eXUP`5|5yHz$^r|;$gl%`?~RE*DV4-3iElE#GGx*BNAd7Hd?~H5IDIU$7(SAZXBwzzTc6i$M(EN18fml6>JxNcO1} z!-4k?Ge@((&%@Prv)CPe-o2g8Vv3KWsJ!T2)F*l~$1|T1llpVcy#5u&HydqNm%X-q$~LUo#+C8Cwi8N z$Hp+;(5mLl1s(|m$9Zw7B*n2x{X=woMU%$IuaD)j-%@r{TcrGS=~~ubEqbp`8=+Du zonkCqvFtqL)paWdK2~^kQRk2VljDnt2)q2^l;brtceNgL({bq}Mg6=wYxCsgD)s)= zy7`$6{Hjm-)(tk99rS!r>Y2ZdniB|A6o*v03TQS-2w@I3vy??#UdD7K>ph3QQ zFn3alkjxdPbFXBj;~iiztdvg1xS)Bn=;2{!R$b`i@j%Xgot=CWopR>|m561`>-p>; zgv2!~P43oOC*-_s-~7N;UG{9|;sOqT5RlDNc%c1iomBbT;F;j9Pm56GukBU}sij7= z8;L}^!yK==80_n{=Uq$qw*=vlDpI6ab0)O(zQKy6V$e|zj&=B zHZ^oJ2^U^rFrJ|?GpYZ3!S`n2N5rHxqSEsN&Sn@Ex}xxNrR)2&HZEkC;R@VyY0;SRle-?4S|vy=WTMWHN8s5WC@N=@{*SvXQzOW z*ZIBBgXClaUaq8J;vYF-p}|+!L_dl2Q+S3gIy02*+nHn8KC3SpKTW-eD$;_V3#fkT zkU!UpqmFKhpB>hyuZf$n;Vr5D7G=|0uNTSp_1!ymRIHaO-@gI-qs;c&fRS@YBSCp{cV}IoX?; z8W|B5ylTOj7jYhn5y>I_L!!}9tgvOR?7?l^w;T6a7&InMH_%k1HHgbU#zH+W(1ez= zCf}iO`M04d9u(L5F`c#IT%HhTmYaute)Ih4%!{|rmO()M=74@vl}T2M}corPW@^ z{rTWo>YrZT_h2XcsRD)aBaMQQ0AZ5SS|62^$J>1G-ipXDDJ{hlGhdt9%et9j3w39F z3*)k0UsQOi@q5bf8ja@u?xdz|c_8kdw$HY^ZjD%6o?<9-RtTCLi=O)`ZcD_&_%U3e z;<;ue4LR&O6m{NbNyC~rQqRdVw4SGi;+B;V>qPZ7vwB!^CqENnR|z^;oK~4wORH^L zx>n=1{N%ipkwyOcLH$$kwXO6$4@Wk5LE7FAZl~Nga;MaZBenH43Y8Bue!pHf{B}c( z#If6*pV(8D+uu*NCaJi#byo^^cjB*m1?A2a6yZDSgnP0My zE6aGqmh}SsG9$j4TIYUO+QM#7Joju(27dHe0ryd5Jb%jv~Z?kc<#rPqD*gRr}scEy_syes-7NW;j#NpOE52 z)o+gtC&4*MaMG~O;)kR-OR7PB>crZw651BpG*>k1~*ey|rJ)XbDkncJVyA5tS+f6OXQ z0;0eD7NAKEt&9%NrbrWP)A3Awwn##4G~Wpw!MF-_4>L3kBQ$D_%y7`gG$J0b8BO{n zk8DJ6mWn=xTgOVNMi$q~G3^?um8hTk5p7rP;fELt$vAWzcO`A3`U*o~6O{;?iiAS0 zst-xNFHr*e&Nox9fJJ(~EHa*|?Czm`#mw2lE@yR7~d`BL`% zE6ST}2``?7teU667}G4xZ7pQ4Cj_;hm)tkikasuw{pi}WXA-v+gi9XR>av-48m`fI z%H3l8eqQ3$taf1VJLevQ^D9u>0Is2tlxIV3t5a-_e=EU*4@@QfHD#nhl7y};Z8R~g zxwyD4$vv8We_Ms>wTb&<{L*CcWaL;e{ zHjr~TL;EMB&G&Umy!e}fg-u(bgx@EdPaPf^=^TUZ-5H#sG%aB;(wFb8Y$!{woIOo# zoD`{BxM#hU^Vn$=1#2qmTM3I$tZP6VI^RX$zerfYp02O$Vwk>ZRC@|j;IHbj9zjcC zQbPWJcUsA-F!g@fM1A4nd$g1wLY&-V3mFpcvdZq6%B<=I^Yz`!EdD3Lh6)3DJ)T(I z&tu^fKY>&CO;cldv3Enua(Qf>Cjg>aEq-|RDeZ>SCY4a7s@X-45fe&S4u#smUAwWL z^R@mgry)zI>f)ZRQ{ScWBn!i+jUU-I27TtJPLj^i_nsKtQRc@unZUnIOWD-p@ez1c zZ*%hy183{x$rO&m!@M>u0H&DN^=a0IO?x7(D;RAvM)xVdy?4z2kN(&023dCmS+9y7 ztV1t+spjI0Evc{mMzQ)mix6+Y+HWMHqEk&iPKP%$d)u5`J?_tj2#1< zZ23VqxC&}#V@7bXVSfo#4qSL1y3GD@6t|j{@+)avW(7T0tSsil^NbxS!){ZDYow!4 z0Tr7rNa9o)%7OfV{eD&f9%|%y(j{3Ti?jIV9>b$QKOK25}8R2$dHP6tZTxzsl zPjVpqTs$8Bes)jCW-ar%zcv!N{I#nLQ!|aDLu8PUm9CEKNc^u zG=?cwSVGLJ$36v=Zu)toxr}z3&>uS$8+Sq(#w*v?itVbm$uBlWOdL!aF=xJVpT91P z?D{`+u4Rn8R8SHU(l=Yw|KB??1@Cn3$Byxot-+MT=Jbq}kl8CW{g)^i&xY`PocP{lP}$Eb z!MJu0?dCG~_Il2|^RgK~$p7+U(`B{ur+)x!y8^%K8hSL88+5hU>~gbz3qQ?0T=hTu zeFE#chF|m21zkZ8vTotf-(x{Hhh4XKZ}A+GzB!v-mG*}tNF@;GzZZprZV?x-F8`CU zQo5U)^*M-i!1WOfzpLkXdPo|)P4n^1Nn%&P$#0mnl>hA}yb!#f^`)Oz^X>K=B25C* zS8s`_Zxdp#(sP3@E>42t(j7=Ip)KIYap|s`6PTv?Ei`MkS8!q*IurGwxnsaLR?2q`Epo%GGVh4<}dv&-Qc0tNv&o$0#8?<(wE?G$al*pogr7k23c z{|}aqFC8Ei0RNTlvMS+aV%T-NelmW0zTO2M=ihmeo|vmW6m)ap5_BtZ-ZBmklD7^?Q^e3*c_y_EAbYg8`#lvd--Ck^iWZM8o%ih6`>mxyz%l>d}cGZ z95a$4pwt&J%_(pjW)>72w1?lcSR-tL*m=s~cTIS6QEr@%ZS{CPSAY0LEkT^kW+GIx z?cL#icuYQDxROH+uCHR}I~TOAd4-54SBEu0{)iK<`?ADRzv(w`ABc71adSGC@PyDt z(#H3Ao}`cA^_=!WuYy9u)`J*=I|ELzZegpPZCCrkw-G$>FwO*{+UOWveaFHLC;dMK z8+=@?W!|GPQk*L`15%th^&14F5QUH%sm7rIz3lo822LKM+I+xitRi z?U1aja;|(R)H#j51cJ?rcJ`CGZ$<|TGSEb4vf&y8&;0Ys1p>3~@ZJxEK?Prjr(3MD zLJYv4u4)zs(0d=y%@Pf2et0DKRhv2F(d+Y6Snu1h?fFORva;N8<`kpeaPro7uTu5i z4wSrYKK)wXd=B&54#GN*JalO7CwEzO8NQk^ne^*)KA?hY6rSxfJ&=CU`Qsvi5zAl2 z{n+e7&Ci!`GNNs+6r;{?a{D1*$$x;=A5u%5N15Fb5TwL(#9}CJpPOzwc0~2Z%kZ_j zuFRTvm8RJ^ximv36^bVbR{0~`1e4a*RG6%EkVDh?3GVB|A=#;F*XK_pqS1m%RUH@T zg;ZBa!M{@B?6jPQ9~_!eB_58itibf(EQa{kb53|PLDxioVEwn0l^^L=QOzqK&Lxb) zu}+-A`V1Qjl~K$sy^qa?&u1MI?w@coe=}gVja>A{;r%wpOln?>wR$aief+2Dn#PE? zr?pG-#HB&k^LgQnl2@Mia!R|D?-6mq*L)uml{~e8ldXy`pzpgDYtqy>DQ;c8ap|L!CLY5K zb^4WfChh}#aD42KgLOhW@^G~m$8@y*E=JwXtVjVdKfl}VLXQS#8U~7wcbPnLJDUw$ ztqeJQSL+>cy1^#zr71TAipq1g0*dN6CL|}U*7VeXx!$v$+^kS4tEhlWsMt@be)Ffv zm1$-M?@H(Dc4K`rEVALe6QeEbC+g~ZJ|D@ob>XKUpft~j33OqS=+fW3`M*D@7IU~)`QlBZ=Z6or)vbyp7I=x{(LG1l&hOw+2`rYQ}M!hG2 ztFX3)y_hZtM_u*glG8GT$;nztfBJn~%(&w+apk6>K!}j(ri$Q3Bz618IFE)ZMh!Xo zoFC&72fdmFPMQ351GZM)l*Qx5I0HWktJEGrgsed3eo4v0El3t}XQ{AL z3uKva{u=DICdT+vvOg$Ko%pAUps=k8Ljtql)aF*V9g)+~W6d^pu1O(v6_Yr{wg*EU zd49^v>jmir%P($|eEjeO(jyWbPCtt-jruxuTMN1&A{0;j>~}ZU(xV`@*Kc&ja5wa$ z3Nxa}Da(gb$@n$fp{Y?>CCogE>#w_hy23v;l3|~Q^ulDf14dmjdID349OvkFy)bte z6h$a41zivk-qHjyAYj`_4yD?{3W-nK7%p5zqRO8>kAzG!uWVojmBtiV8~$QDYPx*a zK2X-~cX?oR9qZGjE^M97-u*aMi1w?b0g0>XDdm_~b0O8ww3!ALrp>_<+|^SW;R37% zyKmCKrW04cb^*_sh^T9(yb0plG|wob<+!d9i<29@2#B$85hrF6UU1kbXK_Xu5qML2EeaHieT&C~b zn+!-N(|y&U*QZkTI(mC~u&PS1H30c)BiR}n%w{#P;M^qcM|X;T%}b<-QwJW_MQgQPG`aNun#^5!iPv_}`AT)7dVFIneYk(Urxd z+Xk{9wslc95yw3u?3gjsEA#t~=Xi6k~-*RQGpfYqZy#(->W> zJ!chRvr}4q#fg4VLYN88GiWP6-j=R`Qp#?7lT+G9uyI@Gc&$frUQu(Zg|{0N9uJ<+o6T|FS8NTCxOEj58+xWqyQT}P@ zWJE-1iE~5X$E@bws~qojHbP-_igio94VdU+L8F!#jCSZjcRGFdP5x=z<9nCS`vtLS z{Rxvrri3b@)yjv))=UD?f|g=f2}%`)0Y0JtK3KVRY1uF}QlnyXeKr(~qQBUwZdj7! z`AMXf7-u(@sQRm+F0f?cK)2kdKR(W9uh-j<+!hl!oQvHAedK5IDBF!8MF6%X2sYKc;biB*}e1X%nL3$^A5g3 zYAqcEqZ%gfSJ-U|%lpZxO>KS-qoMp^scQH7N2z9ci_9Xn9iEP}llz)LbQ8R1^%EmU zv;IXsSx}AuixY=@VcT%iZVh+qwME5d3v9ybrZ^wU;=5bcQ8VekliMV}qI^}k?KD!h zJ~edV_Z1cf97S;{2aF@^4T9I*ueV~7)WFMTJtC7nX^*Y@r@7fhAdsnpGb4v`n$YGW zz=Y{ud%uy1({mJC6DWcc#xu zcy9lq0E-JB_IK1A30>~*zMm*pF)4rZ^u*C{>pDN(afWp6uyM~p_i2lp!`%SOj@!>l zjWJh>x7JHJD-c^|@vMU;uLMm#8y;bNp@Ng~3P7e(UAK;`FrvEcgvRJ&Bc5vJPJ^1j zCZ8SUP?#U{$}VHgB&xnIcO|3KmNnp6i3Ba81-r-veZaFbcbI7a<`(|Nj2!^8)AYNB zfwJWI*}H0!Ezlfm1x1IBNlC)+LUX**o`?Aa?F7+zLptf3g%GUOKi(i1`?Li^gyWF_ zTrisN;Nq3P-eIO$p79GfcSXdeK4210w(P@&+muk_`rR*f14G zR^w4ez1|Pk-&X#RseljmC@$C;x==X-_#6K3M@=yzxC*nWml->z@cdCJ%`?zCsPQiH zw$CDZ%fA%{>{{Muv~SVi<;)@2)X#!_NH^stEfYM~`$f() zEKl0g8J)m~=|w#-%g5o;+P?|R3UkY9{L|m}i9mnC$o&S^e#FZ!m5oYR4{}EMYj~CjeNPB}O%u@2zEFi-#fDGFwm^TVC*v#N6{pz|z zEARQPNPf6G$b2abV( zQasbFqM+mniHviZL6&{xp5*S37D2h8BFJ8TVduoBzhw%>%6VF`>`Db{ysKH#$FLUs zL_SY>@|^{9*FJ^y6Cw$VLjGyIt|7l(T&1G;xNr6gQX|ZpKCM?P(X12O-!ko;<(o-2 z*tMy^S3YF_HGR?m-~!rbl3TI(V>1Pqf0WAzWAJs3aw~p+@OV|8r2E99NU*gGP^o0i zv!{FoEuwDGG>DQI^&^NEDw$n?lVR1>)KoU(&^CUj4Co5 zG<8Cws4FGsmdf?FdO%>aB^*Yv+C~Nr0=&CEx8n+2pI#&`mkA(sqRA{Ay@`LB{e8Ms zE^j$NM#BlN{yhKn?;I1s5Fze3jUN2zoVWz*wuo9K34O|vs!XGWG`bQ=JI)1rfdwC7 z!2A-MrUe%vto`X0E94X>^xZ17hY9pkiaf8J9KGHLQ}R5w6qnrBSI<+&^~MHZ<+4tA zldsaC0Hh35E-e7>Z$-83$al)okj?g^_kXJiM7FURtq#hxH=1f6JRZR$@XQJ* zsGfcLlg>Ur{U|8@Wjo^nrC8ly)gO?$D(1?IbqjTMSD=k%^`kbGY4*TZ91FZ% zR}nm^jIAr{$StiKb$d=dFOnlflYKVF*xezd)O36Oc_&IWD7tHP=T)xuni)fELywi9 z6C&cJQ>UrOn}VZ;i{HEdiZ|;5Q(GdZ$vX$N{G=mCbr>KsWewBf=sj@)LiO)Ae$8E} zt5$TrUEN_a3~hn~nlWnbeRNAoA+S*+DZu=&IJOtC4& z^H+jSxf&^M62B(IKq@^^eL?68`cX_z7(!4p8W%;?3jp&kgy+fw=6B-bJn+lZ8#mU( zU3U7OsD#`^H2n@ngnp3^-ruowQ3I5#bcNlOb;^`;Y-sIc+P}rQ2Y`_WfRPgYUxiM! z%_=G4R*ZF$PDvPZn^hBBv77CXNB+K1fDsUl^1a0j$Ty?Xs#+!&>tOwpa}JjEF^^+V znKkt<|L|4DCdi`8r1y&dF{JGiV|8W!;wYz2thNuG?E~?OAU>FnbT8nnYAveR*AHRn zYp#-B;*!|alc*Za?Et+w0KLX`YsN5DucJ}2tyO8w3f82?ZsyqM4?ac`tX`8_ zPe-iwH!hftek^B&mc)WbUAom?f++y47Wif&nB!$!@htEItmu<>g?4aBYW{f7(#|VN zAcapgnEKU7JoQ=}1R`;XlZA*>917c-x29d!H*EuV$S|#fB+FSRN(k3Tt{?qR$lzZ7 ztG}kU5Bhg34&Cbxh|nZgNJ~n5qeGn%4%xf;B%Rpf+ml(1#}-T9zY}ikt|gD(3CR1_ zt0KtnryCR~PO;-{qL3ZId0Z6uzeOitz+cyE_32hHX$NjciPG2(`Nz^CKGEB*Cfpug zBo>}u_`F~rs12syBNBBeyYPClJQBmq<6t2|>f`^r+!9`zK!r>K74k2wf~nkqUsaXX zC=Bxt{jR_(*Va25^y>xU8V9S4re-p3Zg6p)fQd>#zkblgc4cMi7meQ}+U$>9Aenj= zu-&MM_hnNJV_)XHUT3QnQXesn8*FWCtUKR63|ZJKILNE)JF&Al2wFXPT;_lXnBSo` zDkQu!6?K*{Yd&NqP`1@jw0#)(7um`lNym1nsHG~E1Nb)@q$3Uu1(p2^0xS-E*i~{; z3x}10q%uP;4rT?S?Xgu20mwAYwY&U4hoEu8nOwF)K~IjbWHfsH_?XxM#25Bhs=YI` zNtT+;9SXJ-O8Y3bnY@md-sCncf0d%P&t{Qq`GAo_{<>efhrM>NWbf8G3wD!$hS|tc zFE1cRTL8l6L<|1!_iM=kHIm=)4*+4|J6rNQ(#HAk)nfC$6Yp-!iC07B0*!0CMyy7> z3nYKFi3MicIk?(pQlv~3o%xp>ZBNXOta!naV(g)3U5YQ(wxmW!A>SXCd#>WNy2Nv~gKZX7p$v7A5dZ38 z$v`TBK$Ex9aqfB`PMUL)>wX+>af(;A2AOX@TIorAdinm%uSh%1OwPI)+hldQdC?Ez zfk=T^+ksgy`jlX%tZ%}r*b7YI4VjI_A0xiLAKs>^AL`*toOnEvElBlRV=_wPgmC)_|8vfpO{V9+~94#xUtQ;VoG@&s;2 zliN(9OKRGr^$&y)jT8FLnsXhscl{wkhr1J`-2!UOOLrjoV+<6Mj5S!fSzj6nA=ZmF z&475T;`>*;-7P1n-k`K#A6vK*)q9WaQ=|Cnqj|d|rpy_`C-&~PdCw|dW_HHsJhE*h zO|1m8=P?1a4jxrBOd!EoS8Foi^gQO$CBx`D~KD;`!K4j3&UflQYE84<{(^01z%0}Ujv{0_phiOC z_ahVuT?9W_SVyqJN6lAB6j!rX$hmxCm<)^na<>Ic>Dso=k8ue$8%{vFVf*NSO$SFh zy4g;3#q!6*ud1e3Rbf*ano6N9L#YolPt6MQ#!KD7eEHfY=)q)KpV_3ztKxYQhPX?k zP}M3R7{X;R@-l46!G%HKqygA!vQu5KB*~6!7e`d4moMkHQk~Gc`n&zP0y_P}{BQmm z?cjLgs#+^gredJfi}NGY3|8K5d?5!8s^0-_1ss!rq{cn^XJyo~$(6JDlmxTaTUi$v zb^9!iDU)ly-vxZC*XS=!Sg`U=oEN;Iy`rQ{>b`=aXEd%0H)w2Hv4dT{-R{CYCeShw z5cMl>14>!2^BAq9(QQlGffob{2fP;aJn>Mv-ch#ov3&6RBl2=VTkDS%L&)mP10N>d z2BA)TsY&LeF-RrtGh**%ldvOB%}M-|zgv7}KM3}}5~sJ~eH&&Ert!?~x3+IQGLN#| zKSW(IUP6s`<}>Hk@`s{U^kK;&$FddCU`-^@)KtYQIO@N^{gP_GM;O5Wov;~fI*n&w z)A?`dPqbQ$J_C5Ke|n|ZcAH$>ydXnq+CiZ3)TA<*5YWq%mecw6W6?xRK=f7El*>K% zO3f5BLqbgPc(jcK?29z7Ex7~1G0ukNAH}M*@_4j_#_#9-{_ljhIsFGaEH|u=f4-9EQTc{$T8#{eRN^zjXRp%$5J5lHA@+ zot}3xj;)jg&a0i&rR2#HBF!Q^rI`6r&`C`im%O^+Q}Ufx3qOMYkp$l+fm6wMccVA| zaW|UUwtD@&XX)f-OikcKsmW}63`~rbbE=awAg88)oSONWFo&ZwgEp0|5*xU?;=uns zsagOHDDwIuF6pPfQ+jSyiY#O7KubYJz;b@_D%7liz6&=n9tGAfD1RoI^4Ou!7mi!I zqD@TqjM%XGkQMfG62BfG!0_M0CrFaqlbnC?IlE5t@knP}=24NtNFQi2sX~<_7VH?9 zV!_(^Vh(KhwvzRbSanmRbF*hh1g=m6?H5L2|h4}M-XK2m@82AM+paj<6VRhYz z<_G8(QeEe6v&K<*I|jQvli6LH*MQc~4=B;+(dTBB`TCR5jB%GDcGo%rxQcV1*AmKCMcZLg?+s=JB=^D#27z>oKu#={cOUQHC|OrkFj z!guq()pB_kO;ct=k8}2mRdrIg%(p;@yTxj|?ey7>8;5ReH>|N_9TOYO+T3c$Tm z;;^9dEm%98`HVexhAhCve5{nuG}I@`c`peZ%-e{0hhp0m>%}iPdlhqD2|DH)5zYTj z-s!Mqblh;&nZ%vfk3r3__*X$if@98Sb=_XF!rsAjsWP%xRpGtc;&W;ORHZJbpV)0JbX;ETRjun`#mJlJ-B*(2G~vpLpPg7uu<2b`^Zuc#9CMD|J`?OY{6s8$~c> zx=vs!Oyg=K7A%8mjqKf5R(jx$>P**t68vpy;T~NeakAI%Joy>r~G5*Eh2F68!u%xFU0)w!bzQmiCQIp z=1ahPJL(DXzW38H3+YSktF>LPevM-mz18157iAq#kN2lWSfWLJ5v?yNp!!lp5}Bfp zb=N2PnLseodTHzTI%N_yKOlt(t)FkZi@-wHD-xYe@;8;2fud#lZ~%gY4r1C)r}JXsX{;MXk3wW0X9au96jhuj_ILH-1|#`(YMmsn$y zXF=1rns9uV>ov74qseU`U@y(IUJB3sr>E`xz3c>E171Mdd2PM4B6kU$P=ia(!rVT2 z#J{W@U(}*>V`6EbY#!&s?ep;(O7u+t{(g!6*C;OW4jtXymD@Fzu3`KBe<8)*6Y^Ve ztMW{59+9*zyM1-~ff@Vw)r%ULVEm+~9SpsSsKyHHNvB81O=VQOnxhAqJ}%4*KVv zLRvu1ojug}!*-nCm0daB8de7nXwofcX;UVwp+so#1-5a-9Q(Z;j|ljne(G8O5MB7d z!Z~;ri>H9LwFbF6|1;bUBO2`zvzP~M;7Jx)_nIZACB}DNOABi&tW?HGIqAinuNO;} z{|+Y!2=s{}@vfAWsxIm#S6i4+g(vqKQ9-lBb3Ia`c3_hs=JJHn%Y`UiUIToql?h6N zE#PB<)b_K%fC?!g=-{PzRvI;JpH=H8=Fb-h0Fib1K> zaiq@X&3=yrlm1Qv5tb=(&L zCmg#%xY^%FFvh?|IbM~Uz^n3%5aCgjG^kxsWKi-P{=Wg2!!L}m*Zll0rNq}T#m@(H zE`sSu17|f<)MWHAQQ}V)4%_Ii z!rQJYA#UoyU>^fACG|wn)tHLO*k==vSl;=0hytJgV%p2sYcQ$Hj`7BLQuLcV_NJB; zl&oJ=bTI+enX_IcsP3>`6LRW zS2x0?fU?t&R7zSJ+1~{s$`Mh9e~vKK?#7SVNEYXS-fL>)>QU0Lm-PSo7I&@&YR7>I zXN-Y<;vl?Oc~()>BfS3*A^UDU))qN%J=#9bXjNX&zm^k`BPI^2liS>c5~Q_Nm=+?< zeoTXGDb1d=(x=4T{eVhqK~cD$YXWIY#$lq5Z$Qg zZ5UK+428zqyEk!>DU6#Fv?kG*XB2n)GAIPj9jIb^=Fj32ERY?40NEpHaGV>N~eLIc&SRyD<7ml=;u-lWQ0YoPj|hH;+;7rA2^MmFeAXS zRFygMYTGDuY|nP05hakJV1fQvp#iB2`ffd|EP_{`3T`)n;p+m4ebrL5#^e_)<-I%2 z!_$qA-M+cBkj6*{zGiVK(RLoP(9Vzdx3amEsL+lIeHGJFbx_ys-e`V*Lhf!}2^%WLoe1eIAR;P{&QUOO3DjF3koo4+4gV^6FsHfvnT30 zZ`fst;XDk*wDN);F-%c4#2d{j4LzyCg~qaPxXgTJ$@NM*%9=T%Cw~CP(H`Vw6!=R0 zUlkENms^TAh{IbWrRjp8Ay#q{nvf6{U25Z)$kOb+LkCz=?y~#qYK$X&UcB=ON zNubnQY?>$7gbKO2*M+EP1J^6Reh<2lh6QO+ORQ*z!11w{jvJfeX)LZ-S~}O28lY{O zIMSp8cu&fwS-;q)QS)v()qj5-JQ7Y!_<`+2U=^8D^AqaR0{)wsavJ)Yy>WfweWZ(T z7CLJOVy)VN3<@hhiR~vh!)TqBSP;WFRSCCM%&E^2F4H>#!kjU1-D=)1qwp$vC84qF zR6vOEfoYVRf-i9(g@Lukh|)R7;8m{6jSeoIKZxi<%G*gLGnwZ~URN358= zyecT5yeSOTq(DJvDbABVg(g!Ow9HAO~t~%}D^l61vf&=}p8i zN~Du!MEJ*|A{b>Aj8lr4y1OB!$)q^a^}cjRwewaSAaBGsO$!8BUd5$xt9yEzchfU> zG-eofe#j7y3%u=9gOl)g1LPgOsf4mw5CvLC&Tgi^OQE{-`pxwQTFz@#!uK8d0Hs3JVlb^Z4gbBg4?d zKNG(26rE$}$#yj`=O2plH--9<5BqrK_3)f8<^O|W!p;3_r|lVIavm24LnKeB^`6lE z8BiIhv^99Ln`~;FXr`k>E7_GF_5!e#WPHOD;MnQQj|%2d{JwIucl*)$A@RJj_xF$4)-*^QL-FlOyw8>kaXaFel~;J0o?0 zVC|o-!K(QK>%VXC%D+WV2FOK(vz7Bfv&s_uYmK*by@v822Q+l`0K!uKSsA04zWR4m zskL`K)nOz zDW1ZUNsSZDbyNa(cgB-Zx}s15Z{8t8f!qRpc=WfxIB?L1UD5Uk%f{VOb5K`UyB-F@ zLg{_=K-Kq4ZNl?|c>T^gdIl)Nnfbfx4DihEeS-Qy?IWhOa#0&-#6UtiN6Z@k^*oi`iKcyp*$=iisO5ehy(DdMb0ax^`V^OQxBV;w-F&petakaTRb zMuG*ATGn5qKQQhx_wm4YVCAT4jb$McH@6`vkO7@8)Pu!uoaGIGWHYVE_ZR;cA=sRLYY>&D+mw4hz%FTy$>d4#n!y&iqQVUK#z6#AG?~{cE~~N(U~{@H8$Z#&l5_{gs%Cp4gdg z-F}qz=5)=of{j4kZx=n7LcUWofVkFvTbkGr1_}({?KkT?E>g$R)^|$|nj0@8;-kGTSnX^OtmQ|s&!BXNWwhOpg8(V;Q6S)5H`9jHydO4u9{za? z106--*)Gf2j|jvePgmzmGh_M4+ahPQch4gy%(TG^Sm!7d){Zx;jLAvw zc>+0D&Y$n6An99=^W+YO1QexqB1z95y>xMS9pkqZHrtoZJcSn2+|gF3D7zHP-dl=; z><=k_Huc``rPDC>o!&zBC-i~)cjHD}_Y$+M)~-CBd6G-^$Pf}udLVsPhnx6l7qx`a zU(bBXE`~yS7jyfSV))9VngZGolF3V22A|?jN}jufB%`~8j4_f0>dH(Wr}rS7uzU5b zIIM&bVY2W1-bRu6%CATJiiy3A-dlfC*-GG@H(J2uElzvn@!5(M8gqEFV&mj^0U`Ze zi!2u0jDIz_Xe!*J6bjV#2&WI+IYU&R@J>!N+duG!#AITBMXLYa3ka?=ifj!K+&{YA zLnbf=2Vpk(tJ38&%v4)$SCXl&-X zoL^r4c5!|$10bqq%WCz2b;2kJ8uyc7qcofW3^92!_?TEM*zpmJ08ZYcaP>RYv$6A3 z1i)$NO*iZfGrDLU5bx#b@KBV^uVdofwEyfavpb337|uaih`~})4SMP2$8aDl{&Qn_ zw(hZl0qyA6B!?h_rrZmpYH(V^$8LV*(O?DS&ed#*z-}SO9X9VF0v4u!#@?H;B|Zwd zWAzzMQ8Tn|Fns6!K;sK?hl^$*HQ45cMWL0 zH5L4sJJ@HNq981W42%|1lAufqwX|JZX^6aW-chvf8PPM1OQ77K~x$Tkxm45?)wr4vHIkY1Z&)lRSng|+J;G4?C`Eq4*FPRu{ACU^4 zv=j=laQT1hps0t?tQi?5LMS<7Ic|Zu{GBY&vs$~eSsByi`@N!T?kJ?|_ZY#>F+5`mrf|4J5*2cS5f`Q3k45cbW#3&_7)FKLisSkEe# znA9RNydwaP=0WwvX$6K&4IB9?d;dnp%@$^aMn{o<24#ugT8G$Dqu=dDo(#c2{QbXb z#FS-jj^yj2DQ+1eHD9qKsh!M5o`)~>xyhD+d>zuo{Pk|XzY=PXJ^&ke@OW%r>Zc+) z7PzjA2b4=Yw59-vvr@DmqBQ6X7?A594`LK18IaXpcgQQO7VCY-;ef_JJ9dkYIqcb? zE<>f~Y7o1GrKq}3cKvd}EkuoCe}km@JP*~|0323cH=&X++9$Dbz536oOJkwbg%Rii z@sSe%_aRGh?yG`O%`v3QHDrgItlGrNDc{qSnEyrBcgIut{{J6N#}RSJD(i3>R#8g! zc4!G@6h%XWl0^1aW|7KUMaH4XNTTeOM8gWDgk!`>#xc*q!S8h+z31om{j>hKANRSh z>w3MOujjaMP}-`ifN1F*N_JsGdr!!w!3?;dcdr%L48OhzvSz(7GPgPAC!e#gj@R2g zH$6WB9j)_=u~SqhEwcZ^AqvWNrzrb(U(?Y6HV=RW_ID>xU6$41g82#n3^JwC{_&2i z$V8}zmDc#deUhX$SLY~JjVnD<20sJyVrKI4T6#rAbJ!AVl@(d8sTB=xT}f@}V_ssK(iQT;<>A8OGiVK|Nh<**s9;m!7C(?37mPF=U}7FZEIa znKmb%_;|PNE?}{qs|Wt!20K9&k7DTNmn{({#Jy7CJs`HnJAVw40751IKqOXnWd zL4EV*McCTamfk^1ekKHq-T8%Ybe7cYL(%6B7}W`cF>$OTnzw zStji2t+^(#96bt@95EVp-{$$^W~rA?rAZkdQ2*WJf1!deda!3sEGMmR!D7UFK6SFp z=*HS{ure|!myNU(^?pqr5SAfloKw5Z4aDrzc%Y*JUNa|o&KxVOy`?1`AG}?c;Bq`Fw!vD zW3#*3$DICb)cFdQH=6HCw)nvBtX<=sSKa#EODMq9t)aiQgSCp*dg=hB@fhCo^8gTI zpNHo~5%|Y<^3Csa5(VdkSBaephdQ87W`J2(_snDrd%)ps{c#hereo^1==(uPU|yA= zrb7+ND|xl{dOzOyMJ1YSl#d>r8j210y4)S`|j zm^zgW6}xzYGa{~HpR09<>EwK7VF=x>*K1)Zf35J|P@U@I6Jw>D`EUcHf;505p#X29 zQumCk_Z_?iXv_Yt2*bn1aWm{Yf^R7gbjENw5;PmO2oy9MI-Hx`&&)SgD`>P{J2mv1 znQ~1*VBC|s^i-UCk%&hj+V^>URAq`zRz{pIvhp#u>-Vwvm449oo^Qok1VW6l0#qeK@@lAMQ7i_RBdS`d461 z%L2E=)6KmsQ*WQu>^c_)#Vr9Tz;6V_s&fj&Qh0uFOOS4Gn|i(%t2VfX<;mt^IDFw! z3$$7ncK*nTRu&S;GiaAAw*{76Yp$y+L|ak+mwI6OT|Mjt{f{aKPf>-;Mp5Z{{}`j% zGw;t63tLz?O#UR#c?01uu=i-?{CfHXj-Ka!Bd5@{64F<83(e~=e28Jgm`0;S)OgkA zUS&qOpuZS#&LxcygU_Sat9L^4k=nw5=0lg`Rcg2!w=1S<$^ezzc(?ujdoO3pghjp{ zS97<;-NfUh)AXBrwjZ{I_kEuyM0i=vF|Y^d8cU; zQYzPWH7YL2VW|E?rOXtw3>Q#1%?tse{YCT3dN*B1RTK$f-PsnesA!*9+v0=kB=g$v zQz|NaD9%Qjmz}<(#oOP}?{yL2nI%M;^?H*#b$3vJ8>l#Tz7^{34ugM=^sY&}KwR6Of0%Ju|0J{CvatEXgyIZGy{hC2 z!b|;4TXZxu*Skv2Rgi|oP#fk##Kf*!9tro$tB%l;nX3@9dFy<6yx7P!CoBff%h=v{ zFi?SDZofb8()a!M><>Fo-O2>N{F{%i;V&lc7Y6Is@E(ck_$yyEUVVF4+ItW1EG|33h& z^_t`yQ^Kp(9^~@?2Dg;Kf`cl&A!+glHxGZpX_Elz&T`prujLI_pzy5G|-d3^fqp$gml`uOa6ceL*W%dp$qpDdILF4PVzGqMqZJ%Eb-bA&W((jEdPV4e4}MERSipB1BG ziCXD#qs!{J>v)g-(AyMc$j(b25P3$s@f5rv?=iA+_ND^_vH~y?;V_;2)x9ky2dXz` ztp)Zqc06M4O3~$7{~4Vv5_1)PD(Np@s?}fP?I~eUrSIq1lWSDGFvtc zS+vWw9Qx&h@rTNM=+f{$&I&z)%#ZCFHf%w*eJ6Mi%y7U-QeAsAqJhu*hUe?vr+bcX z%)vzUvrDVph!~O7%~9Mu)A2KR!RG`w`GI_gz@wT)il>5DX;9I`R7qn^<=XRad3eM?eWp1_YO8hMCrtB$&Vv-rXh208wMZs!ZI3o!4Ny z$z~hm?95LMVRgRJdK|{-orz8RzeMp$$TD7BS3|8ggi>$+z!%%#P>tmUF(W>hn+)HB zDU^PI))6b^hT`4s7Wn$eIBIK_df-tf-Inp{x9>y&Br$vsn9g`~F*>!S)wXS(|O6Qe$_3Xz=2oxK>r&O$ze1AK0L%PzWes&ETYZ!Rh#i21ZKumW9dGs-hDw;JH2d z+q!(bpQ#Ru-}8acbQyN7Py0X-;+Cy;{$N}tzysq-{UHzyhzguE6BC?wAmV^Baq-^(;&n3iMguM~$&np^-MH(S&-G3V@zwF` z?uI2FauLxyb2*$EftU=gucN}iQUyz0%~YNFl*S6YWb3nN=Pc;+=4YA1Roh*=7tGsLUSnnP5@Zi_`_zLMkU2yEs17`l!>FwgnDnaBpj=U`QokI)|~sACbsB zg)DWW(fg@5QTn#z2p$#VdwtM6=%#>wGLINsSFt#X>K zcwmp5np>V>Xuo(#%B+jR+RfU78O{ZL`*e6|?YDK92OnS;=R>6(y+ZKx83fE_6&iS* zhc0htgA&Ezx-=AFNVw$D4qhOQ_%;jIKu}%E?QJiAUqU~87 z+wBxWFbWc~=j3WMPl)n1DpvR&o=BE5;x7(X}{im$8d`J*Ub zyhs){=A^4)+{~uZn=trd1F~-M#b$s2imRHsM>Zj?ue83_P;~%zyvg!zf(u^5O@%Xf z>kXl)Kpd^L!**ZLKxvmbkk4za#TG^O;paPt+t5sGbAkh*OEy+J14n$#-k53*@5$%~ z`q}LGs#QQB!2c2&_WE!+OdBNeEdFKCsRzS`fAl3XEs{a}d;vc`__UE{ zu@!wnj%-vGFk(byIj`x<2p{t6Imy8^Ymi{Rm3zVZcLVx|)2Di3o@kRoo8!iJr#&Q0 zg|IH_JG@xKNNK?OqXSH<>l?lBTrU6<8qnumr>dD4F89*>Jt<_ql?gEXdmB4Bt)zs-2w zoUxO&BAnpuxHpCNjD15(LI+FQAD-KYaKP*Mgn_;`6YbdG3Zm`I>5hiHq@iCE#QcGL z00v=~hfR**Gxn$8IGUcLprA_xzWPL8Kl}obff;)kor%!a6|0rA_5bKY zHsrtTPIa0Vg0+=JLbQd}XcN!EOr@C`eJwgCw6r$8i`Rlk3VECz_=TurLYj}=V?LzTwX;4?~DJwP4lVAv|Q1m zkn=OK;Z?JGVha{qp0Nv4ZFCWV%Zv4&*Sf`2iT4rjf;b0_inTC;4LgHyt`N>cvWt0Kr62^lZSDwVTe*QXp%V)2zR;Gfqo zZ|xa2=OJTpp65)?q5@3GjWw@2pFF_lmmvFFUprtuq8yz`mgQjd2pj{p_rLFV1}NEp zuF&s0UjRIh@>x1L(c(Atd_vQ${5G5Si(c`$)(wcZdb4sicajifYmnJ+&-&}s9QyaB zemUQuTDc#-p{+*Wgn2^ZK(@x&8PG#3jtcn24YcX)p7Igq%pZhHX!Si58JQ{L80e~- zR=#B(bcwl%>_Ee*+R6i)gc0Q5eh)z9>V7uNojn?f{BkKU)i6jZSvy1hn64zM569X! z6-61D7_c}L&9}%OrtaC8LK9~c>XDHaU<*>5M%9DhOd+#1CYt=@3c1%9>bYyhigZ|L zl&t|L85HXB2uJE^3o35rVjvhAZVx$vGU}gP)nLHkf1i5>Na@N$$O*ZGyq_Kiwd?t5 zGlZrjp^)PB-({gnjxc8vuKB`iTnNWM%HCpBwvy7+`RwQj2ID@$)cMJYfp0^}NaL&A ze4CZ=xoll!IIC|@5%@v`PO$xdkBP8vvb9(j1`eI)IiqW!$42&Xj=Q-sJw*&S?p__u@mD6ul2kE-ueC~2sd@!M&}G6U1d5q~&< zp{(9=4Q5VOilLv*0N&G4NCH*8?oXX`;HY2LTCxw(yH*gs!7$&19ZA!#T!gYTabByu zqFd4gs^aE&R_|aA0Q~KN;J;Ijviqah*SV!3F5uQa|GvCM%ewD1; z`EYuf!aFLLHb+$D#3oYLB5qk6GC)JDJ@sEl zqsbr8Q^yz3Z1>VPwRafDII*v_S1U19U+coPL7a9Z#bq<FD5>@KH!we(mdW;pEQD6unfJp zr|5Mm*=YX-KiqAN!y=0=3nKYMpjzfoS=+n=J%~}*8B{^(0ZAbW%MEE7ig#Z8_5@wl zM3tUftOg)OjGTIO>Z0T~4EJtP6511YVz?@Or@PlWly1=QE4P;vR^6b*rq3^cCn$m) zCI4;!oU4z(!;~cr*fICczztTWp$AMP=)iyZlk@Q*j={Xua_{dx*qiTK^y@QAFm9Pm z7`zbTxL17-!0e2Jy8-71@=Wk6^jV+pTxUk?*&%XNCIwIW26E!x-e_4muJ4@BB@H+seVyC0*La)xbEkDPwV~&>;h34hC}= z;t%-?5^rB{xPbL_u8&&4Fj90B6>4!DT7x1m4BMT%@G_;-=rp3gZ-tI;BFMB?I*hAf{u)jqume;7GPR!{+O((A&+jhRWd9Ioo0V z2X&JYj8lVmHI}18rQSeWSw?rWg18IH@gB^OA5;Hnr)>DGY21qG(qejU==1a2fk5!+ z4Ydz}vT#+YeJcnybl;2Rz2McYH|hbAWWCX^)&sBJr48TmW)?|#0ZN5j>AKY_`CC)` zEw=<$HaC0BM!u>1pfD>r1!8e>SL#I;-#lrpE^ctnlT>~# z3F8kvxs!B{x@wbdw>PnD(pY|f3%%H6*K%&u<&tMa zWYU-1FBkCcE=8{-W{Qh!#O5nD-_;u|Vtp%%Ant{^!c*of6&`V|u#^12jEwphTYLd7 zBXb338>!aTI7^P|gK_r}q*stSyLdI+JiDuMU$Se1HFpxMxdXjzL`2kH-h zR)Qv}{cj@Mz2wYYFyZ`tXtH9PX*%Ixnxj9~Lfk;`_XhWzY{EsJ@YY*L*9{VB+M~wf zySv_-PUo!|o?hWy1UKlDH}C&&ZH4*|61kI>O7xDimsxnD&(`LCxW}NK91>b zw0nOn&^gYfZq>hU(K0rD&JQ9uZe9oEAt`gnB0ZaBIfZ;epc>E=s=l$)$QBv0wqh6t z;28q4@dj$ufjOBH3|q%c)nx0xJUVT%FuRmRztw}8Up#L~`HI>EmRSE4{waQCTcC)# z^iai{IlmrDV5@*}lSddg;vlm?{8kPT!p%^@QlDwFk5<)u76-5fKj)=xWlBcn zQCKJ;*n=Xu(>&h0-+Ir`c~d=pUeEV{dSeK;iw9+%X1W#~=GE&H50)Qd=+?0d=oQL$aCJurZ zjYu!Nih*KLi;VYS3_Sy`P)V>*|DpOlu@0Old^nArI`Ti)_pCUB7t z@Cq-2DH80uG&;J{io8+h;cVl$b)!>q26td0qQOR_x3FZ&w6GZPtLxYV-fRs7E*%Ud zmL4+ZJf9bDK%lf9nxE+y7xY2!8x=>ZgF5 zO67^NqfwcU3Nla=2Pk`fCZQ-HtWtGKd)6S9PAXVB`-V*};SC@eaG#5bCH^O|P8EgD9D(l+0_s@}+MMJkX@NXY5;ym>P=c#$q zU9VM&(r=H+)k*Uvp8{Dr<2X0vF^}1dZxg1)XYo0^MTt!P)7Wvr!daPN3kl3`cy`m8 zmFW*8?0ydl&u`|JOKV9O$fwR4{OHoN5Bri*sJ&*Q^~c{-D4Z%jQoNDYAed{UAhR{5 z(izhU?Ib*DEA}5f=1-dem4~)!5RR{{v`SYR~v^lZ4ku7usb@q4|zL$l&OqSNWT`7ibOW+~`FJ87v1aNFf-<*|oV?e$|ze zo%Zk?1ChgLfP(fjJ8lTxc3_4yO|GIQ#)=~big9z2!zSC=fSU9lMT1-8)Gi~N^Sr+H zk)R3B+`-SxvvDI!3SJwgysq|aus^@#3c=C*O~pTzp`e(%*9ga zAEI7z%IvUf>yVfW!S%4S(d|9buzlF1S^8v}%BY8k<5s3IYISZo2h1siKiW@1Q$=)( z(r17{{E`tfMomCNav9cN#-Khl2L+98mj)o3B6Dmt&fOzLA=T(iju99Ny!~Y{Af+fh zx&D4UT6q1ci4CaeF=I%_K12g+ynN{8yON6cAXQRw4_zCKvZ$GQW(^e9RX!n^ELCkf zd72k`mdim8k+`*5@SFSKH{%vHJYd)+FkX!m{(##Y`#}5rQ0p)Go=Uua?m@_3=~%AC zj{R`NjQ6)lj6yxdqAPR;1XU?{=l>6~%)gBN-0jVmc^J#A-y10uYDgY$fmdjd~=rknKkbQBT zqw5=Rc|pXLQ!IsQz|Om}H47Sx@QUZxJC4EfWC8lUt#Uh9*I?YqpSB=y@hUqQOjV*^ z{#Zv?hSq{$vIA9R(LH!oz+{t0v^5{>_g=3w3kRrHyW>fO=N5*2lE!lRjGV?~_s0US zJHmY5Y3DlV&AkTdMwsYxQ5%B}yePYoz`bH^ER!~n_&QGGl5_JCu7QIY*IAeRoE2HI z&se3oknDB69FE6J(zLC0{Tt-Tt1fDDscL1jjf;)~JHisf(7y8(Bz^8=H69ogs9Uve z-B-v-!6;F&IGdHv_&TjAWSUWY%C!V`@7wE`jt$*)uy|%iHqC+MSv!gYu{oRB#27+fZywAto99CtMa3Ktuy= zA6YUp^%>MV?Z!^;sT^o~&qM2pPV4%9)2Y_ql&U2QIQ*uTlM<1#3d=&5{NzsN5Sa6k z001+l&F8!s98^w9K?W<66znJ-4!IcE(-k|uZx?_k-Mt)d{7k7rA^_v_@tLHbo{Kz@iQMGNZJbVj_G^>V6lq7C=B`JQDR5l zL4G?KwFa=yO`W`gcH=%|LepRL1yv zWYt+Yl9KE}0otW*{I)pjDHz^I=lkS?DZnXwff=Eq%x`Zk`USEivM^v4E}w;%5y9Vj z<8Oqk`+;7N$*drMKJQzi37;4&#$*#0L{pPKw5hEH^&qAia#bGqpHCW$v@U3R?;~Jl z%ykU_lX?5|K*r3%5O}tkW9BI!O)rc-)nV4%Yvkbq4lvV}iQQnW#;PCIXy1y$^DoZa z19U>7-Q}!-OFV#Tlk}p`-^j%R0Nt$yqYzOjQbZF(p=J_nN z*b3O&M*~GV#Z9dshz+irsfjLP*d$w97xvM!$6d^&-c&`5a7$Q@p}al_f)CJbew*Lersf_H6)Tr?2K*xfbt{Qtm>odQaIVbAS?BC zYvGv?Fb?99Jsc1sjSnEv`hDWdnijlFc}6xea}^}mgCKU^j0M3&P(FUgVwdzGWe5%K zM5zMGKW~F=@e@n*a%M^_LlYyUZuvdbr; zN_Wb`&i6RquB8$p$u=tmy0AuM@QY|g zaUiwyne;-@ILwFB9Gk$Vji-QVkxuww^qoq9w@mBuh8~pttwU~-&ujA-RUFxR>z5zUV~P61=A7UYbzC0X)L1fCnX4ziTpK3Xb79> z7K@lG%S$d0o;4y9$X~zKP%9~OxB<4};P%77kg=K!U$}|_UH;e71&{Ny>wWE`0Qd2O z)5et{0`xOy7^Dl&-b4}RF_NZEAQ;#>+lgbqQK-mH`VIR z0Wp|?+6i7-&GMDS`5v~_j84;p^b_8Bpj(fGnphMK`O0OwZ2xdwCbX8MM;t zry5}L2b&d%wLBcu9pk1^i#oGMJhO1SfNc(kxRk?CF)dQ;P}e35h-8yjg%D29#aN4H z-nK89(U$V3?;=F2*9nrfgyqkiPEUuSMWd^ZiR~6XDJ}F(%np^Mi~V6tFlv1sRyz z`fV~1gK#~~Ng!n*@p#7>T1RlRoSJP83y_72#cn<@wAU#3Bc3v?DvTH)iPcG~)&#q| zPi4-{E&wRl+4dRFsq2SfKrgDgZPD^0Wz-zk@eP6!c!_V#Y$mLh<-$Cf2K1bAm%>u zVjsUxH-(#D0eW|q^${%y}*rtT?;VQz;zT%V~v1+ zTrX!Nx59az@~D>)bjt0DGijWB|O88CV|H~s$cnF;q?)T3F8LG|iT zyc^;UcT0u6{R(|QYO5?1bwM?qIcO1lh&`S)Pr>%{tBZ%rPrRMbJZV90xwV~>p&RQ# zx7!!w`ucL?qRn6wz=_Rw>ys;UDnvBS+@{jo_`vakO2fJIl@Y1zW%YONO%-fJ6Mu8nkYo6mvqkX0SJNW7 z@9NvQ@UEtFNF~V%e9UQD2m2>Mam<|QbQ^sd2ymkEsM1`vAH%zj)M>Hk8A9@`;1sKl z|1>#l16{$xQ{(&P!auXr)am@{RhP~4Q8i)7RR9IeC7-zinYMwk1X;M$L~7j<*v9~f zFP`xB4WZB?S>-|KQL~mu`xjQ>_gy2n!S$B7e^PIK|Jm3U^zsdcgOCaI(1neya;K#6>#L!d$UakX}4K+sR5D+Eh zMQ1#)L$3Uxb}@F@{*`S1pNBDioPxQvr{a+2D=eVAFsw@Z=?!zB`Ip@Nd>AZMD!FN3 z=Y(uQ-vPCEHT52(0V*&69Mt)0!s<-?Kgi6n*55min_j`nxhL!`BtayO>0jL97MMa7 zIv=FnSu0||yE1-Y3h64Oya8EJXaIDF4eBt#9Dc1WusaNB35M*$dL#lVWflR~0%+qa z-I2>`k|#Li(>uqf%JKX6ZE8^Rrchh!_t?PA577`MIP}R_Yr%^LjiiqX16lr*OSZ`} zGb(fDzr%mYMGj~X51WvH7IX7_eCNqQunfrKzg2T+=f#Xg za`X6-YcDFRqn^1VNw&ijRH240mIs0vxx$}Uao!O6q2cx88kPXX_Nu}bmK}&pQ%ha# zd>x%8Qtq93bv=E2?>AP_TKeJ+2rKX|L5gT;4O~hz7bEDodxojWnBAW*Q*TlM`Gw$L z3fYXybyWg`p7xAmaGuSoP3(DBz8=}RdIUulj<{`Ft~gC^SpRZuzqV{-S7i#t`3F?56Ao*G} zw<*8TH9bZGawAg*_ifrJ0wJ661t^FMihptv>+h9w?+EG1?40)jBJMk)%$PdX26)ul z-2^oYfb{)prGSffIH@pXH^6@2ZS zk?BWYN3k-esx?*V_cWFni4drjB=nnX)VAmrUd~Nn%dP&a$#AS@zbA(^RgjQU247i2 zwEgzn>_BbL2D_2hWQf0@i(je}wWsudrP>g`KDkcH!ep=n*60Ym{{6j3o@ z!i0M@pqeEtbtS6Hy8{esD|!J@3hqLL>EhRT1QyOs7h6W)?vK@A3JYLFn6p^DX<83* z7k|f6{-@1EUreFRiA9^IoC4I$8sjp>mX}G+L4mnSW&G#otg6L3zKAW2(ly0!|Nd0^ zwaI)XS+*t_Txk$OM)IFh@`1G@f(fCF&_9MFZ`aihE5^>CM@ zcEoBk8fxqP*FR%L^3oZL!FQk9^Em8kQ;`SP&M~@W!q#t-ciW^~fBYOaVzk}xvT|w> zy*PX6nHxE(S*0gI;KK_Cmy*7OsOfBT3j0-ug36YS2bJ9FcgLpeTBEMT+Wk%S5L#@? z;Fo&H=J;>B0~K5gQ_o#ebYtVKYtji3R|`RIk9ky+^Xyba;~Cdh1B>%KR-BJgrDH;+ zW-xpm`awspiDy~{F8GLTdtXz%Nb%x>yt{fbhGn5Ay2AEIz?Qka>vPX5z_nFL#X%r) zEn41+mG4Bnw&_)~~HvIZJVDu(Q8_hL`=$Y@X6iJny zM0WQmIB<}jBj95OJ?GJB-rFYxMvcu2;BFiLL+=ACfT_yi9}N*pKA#fDNAV%P7Yl@2 z-E4e|-ui8m|6CBCeiu*kGU4oul75jPV(^^y%zBGclX5otk735Ptj;KoT*Z2=GTF|A z%ySBsXJect0P%6GJe&1wJqcGG^z1;*>Bc6D1VNqZ?C*N`$6LSju+P)+iHFH1@oSd} z9E^E;G!=Q~EkJyQAOHT{3}f?Yq3o&QGb3Qo?D>HfNfQgJ7;1DCzdL*4}v9XrXZ7~kcCc4dYBWA@ zh?t6P;dfDto!L^_1g7UNEKlC=hH1lGsh5QVa?b}zFVLA5waGChDqUM5#JW_U5FZ>` z>Z6^QjSbDDe6nnni_4$*ZrQ3rr@{3*+$()XYm$v5s)Yb{PoRQ;h5DKBGN|#x@;f4* z#Y|lX>k>&r*gN@5Tttsg0Y^6)|Cwds@byo_^%C(HCh~LA480`l#J<*O{Xw={w{;G< zxAO{5A%Ugc!obWCB1at>+2#;$9-%jf>tc;ol|HaETbYODS0@^|M|onEHx&wtIXGD@CGce{hfiTYetx_!00q2%L_0nvOmJAFbXpJ zVO5sDTGYU*1mtsc`)M^26^@=sHaUs*Sy?RB_PKNvZbMk+knaTdv+cor+#Y0CyL|7R zI;I!y$4i{strrLmhp!3O{_qpD7$v@}{(I{+O9r_B1oPs916D`fcPOsei6aKn8 z6W4xv_y;{r_w#gpOt|*>Z8b3RkK8-6uQ!n`0Pe!LIy$sr5jiAh0t77>#2Zydu3at7 zlZ~;)PgllSA)ByyvA6|Y&f0rg{w>VV?%rl%@J|w_^W|vcFz^NS|9sw#zAa|mi{AWr z&}aqS{V5e24z8usH^s?t05z*~xJ9;0z%0{&ek1Vh>@zI?53^rsShz@#<#dPmZQNNM z95wQC!k(+A*Re;Izc9kFM>3vEeJfhVnD)moTD+I%guKmsREJ?;;TbdpQ$OS_hXk0OJi?MZfHHQSiC>^JaB+=A4Xz?BJ>h$K}&6=_dBo&_nB_ zzNVeNU**ucZn+10I4lRXl385#h^`xoIJF87Q&`&fwW1C9eRTFSvPLX%&x6nT=m=V9 z>X>tCN|ecZ2S$vD1AWATxK+ zAHFb1tj>{#(<%(`n!RU&{iV?!*xXYx3$X-i6T7MX{)594A>tlByu^jwJi*M1$XbDH zTsUUlDga&nTD`VXaYhAeGiKm5=S;{Fc^F3mN2dum zIu~&KUsISFftRE7Z5%ByuK6JdjHX<2ffQL;4CypARhm_?EXC%rz^HIl{>n!;E|5|9H<;00L z&d;60x_mQB6Y4n>*HJ97X^x0f+5H@I&OU&?3c!YbO~nga@P9p`tOdv7jsJ_Ld<6rH zk40mF!tt|ncQfttEKijUH}6Uu3y#wq4v}w-Mt3VNn(7sq(D=Vl&@}$>q*%(Pop5Un z?vC+PY%|_ot^d~J{3wG3r=O}l-vgPQ>?SHKI9zWex7O>H16NGi4bsRvbIx|xAj~zsj`q5&oy}cyy4_u6 z)VCEqKd^N?vu@iJTCv`T$}M-`&XPAC=G=2S+VH@~75JOU+WvH|7TuCJ3L7`$r|0D! z0d3DHEY$tT)X+paedD!Zm&y*+#bqR$T+KP|vfQuw`N8EkaCzM1u*gm~)YC}m1;#O{ z>wcyZ`}D^`Ax+y6tRPFhJ$z0GMVdW<(kAUo#>v*ROOQG--@es`!cOpDDcDm=6oNy= z35SZ)-l`6DlH`rz$({>LOxK4B-j}j{gh*l04LVcqkb7O6hrdMV6c0Pcwt1VjHVK4s zMpHM9v*ZTGZudrJ8wdNhMrQ{u+|aEqdk-z5(Wn*uB6Ea=gdD6o2>@xD5WmOqWE*-G ze-##C)2x>)sj!2KdH-7p#f96)b#cVIj{)f@LCmfbaw+1sK~9S+e8K4xydmRPzEPORy48ZRp& zBshL!3V!syr1h2ceuFpGFM0RGuRE+CZM;;Ng1*Y@HAvYgGgu)v#WHx#DKYD;-DAgs z?;9{j=OW(kYW}%IswNEOmOm^xsx&lxYr|C4;?9i<`^% zT*#kI@T({wk+yLI{M6h#tT;ZK%yz8X2+I$H$;%g|k1-u{mkO3me0;vSfgf>eop zKG_9t)ubewTBD~QsP0!tul^#i{#tI(Ey16aY2?c@H^=k9r<@$kl7LTHeAH>?u@-kfz;rH+lj8g;?_#XE zKBf^KGjuhjzdw@PiA}On47>Bz-Jb_DM2P(8;19QgSv@E@HvGUU>*H~A&W`{%z_dSf zx+DKl(fg2+BIm5>_>j{okwNgBkQmL}Z93n()a`w`?i+n<70t8f-m-HzZ#bwV+;rg{ z*H8K-ogL7fTnp6j<$Js@X_d?F60!s7MymIzNrwwzY6hCgg9EC9!){fqLp?oC!na%o z_3vTT6@R6pCR50KF}fO^G!JFhU9`RZ<#&qHeHabnho(d>a`zJ=-#pb0F7}9vNO;MT zX?Z0sQstDrkbw1>Xis+w19;>9(*pe}wEGc3UZz(<(I3;g14K?;ve|*-NUYsk!P*|e zsxbGeY-^;ckdJ9Gx;{efjtZ>wPjMr$^PX=-(V0=whcoiI z+hOeK&Y5nPvv?w(g&Bddr`tg3TE6VHrYQKDLK!1OA37U}=uN%7C=hN`*QjE|F&y`1 z%h!PjngQnEWRhe%pEAmHM}4MMPPIPtXd7*d|X7J9C}6 zM4R)sOH30IpqaCJVsZ4D(@_N*25@;zbzJX@gKL*0>Wd=@lb^fZE_;8&SVhq7A8oPC z%5>H|wr~24oRu%@w}O&8;wB+oFq&K)z4Bha><)F?+5Z}+%bjds;tLPEkrbQLj~ zCig~Cb;9({UAE#Wi0f8@@vOM6+8^?b3&_fs0- z?G6&G*Cu3-z(Z&EcPz}QWJ@$|zWUgUA*S29rSP`I3ZC}5 zFRZi2Pp+;EN1N6}Vu$yIyoTX~z!F;E(_I<;Btn2Yx;+G!`1bP?OW{5iN?d#Dx;8aa zo}F(>!wJ_$3R1>T#Ksggj(C0AHhDB#@tTZ`sZ?v(dsyr*5Bm8FN{`+u4S2INT5UsL z1?gaoA#wJCk}SP%duF|&_+^Em7Kyxc?k#4+9|bd`*dLp`yB*z&S$)au>DV?GsAZ(( z8%LQZC#>oaW~8J#xHMwqs8F_P?*RYl5uucE0ZshYh@&xc{bR+Mov&2H7X|J^08imn z?mFpDv9(@XT;I(VPKp_ow8p{FB)A?9D)ESWknqwa>n&|RQFq-CN$=vvov^Ehwq`wu zve=~Q9Eh1tAzQ7j(dcyH-nz3cQJmkiH};!Adj)H6uWJk?Tul+#>?KKtem?cigh2QT zv+f@*6gl+>XuH&*oQFcQUc>QmH}e|R(2{H~$Yw2za2!2tFANN?c^5UJi3(S?ECg z^F)qX<#gBwmhP^$vTZuzqha@s8l_`zsDEfNq@ApP^-l2Oby2TGD?!Ks=p0lyJWyIs9Tfq zf$%=0qROjWHT0tu0UvjYH>}acJTEPRaP{E}lP7n2?%r0O7M-%LYcN)d`fgC%{ou*c z_3{YW!E;K3_a3Mi`84+^*w-6=eZ|T*m`-+W&1=<~4pq11@>ROJZ;mcKA?+V@+<-+j z;N#kcifq=B2O=!TO>aik5537&ygK8eX=!~7b&1Z}RrEHn^mc*DJO zjY5ah6O>-0brnVng}RpqQynRHeoeM`twFo-5-GWtj=g>`axT$kpZNJuSOzzsx3NAhclMd{N9h=NQ-f+M>Ne}vhtFlrmzUi&j|pp1E(??= zu|F!^M(8e+C3JtBZjrMHg&IcrYQX|E%q?HmO^d|ZQX%%UwD7VNapH)>o^mYyrwgZ7 z_0A1W<69`?>HQSbPdA%3twR$aF}9nFZC9oI*NTZL6F$}EY3;4 z+`}SJxxn@C$6s->AwmN1cL4W${ItBd<_KS`)W=_zwxD$ik8PCe@0RnvthR0nF2*7| z+fy(+b6Ar0t zqlC!Lrw@YQkUzefl<{nydfgCR`rESo`L*)QeYz7N_*UZSJee|bs|d#PR1Z|GScB>c)43hz~heR zM2dDjlICFPmMe8r(JjOc3z})Ll+pr?C-b``nnJqI(*As!cueA5Jso*ccw(u>|8+D% zeDYVm%3G^r?3DVUbVqr#9g7toV1fNIrtB1Wt5Z3z=^fX8>xRcCu0Dz+xC`STJtO{x zi~8!p`FxYv$*k)qET%g-tu`)hmd($$ND-%ZR2H1Lt2c}mfupgEjT{g<|Bq1})Ya4u%omJ3^@Y@xg4VJIcltfdi-l+ztY zn_~_HcFQgJ9>!D~-OyIw@7zTqBws{PYr=d64|T?V@Y>@3Zeh*n?rSW7I^7>81u|FR zE4r~$&f+a>=FzVfL9m(K^JU$&qPbciC_1Wo+sG&Xb`%FqxVCc(O2e~SFOMt#x+qNs zRzOuUOf;r#z?!vz$RFFrCG~%Owub49U{tr<6=}CiYC5<%-*YT%o)A1T#Ej|&xi(xr zjZL*W9j4ecVxz?))=A{gZ`+L;?SVJ>IQ`%E8p(O&{B`C|U9>{@R?0H%=Py$zcfJhC zK%Ygb-`9ldR{p#-+*{BYbC@fZ#(D zJL)4E+Q9wi5>1+^IHuGTWfE#gOxBi(-W&hx^Pv!~k4|GwySz;=IiN6eCp{9xE?hBU z8;)z)@?`)^6N}@Z5F%Ti-a!368`A4{-Q~+8m5TJ?A)y*9F9tfseZ~q^11)N5fPQiV#xqB3dF;{mVMhl^+m@Gz zlaB)OB?8&GdmV#BKFp2MqF}W9pA*o_d147;3wi>K6%G{9_Sx(eHk%1ET#2=v8e&Na z-E?sG=i_%5hUW^xK~sn^G14_c&P z#Fx}l1r(C9grf3!%E@+3!c+wVqcZr$VEx3f(InE*_4kONB7cqSFlBRSX%iG`?an}D zWq;dGW!B34bsC^>R5W%|?EwYKQtk|wyoipTmvA)up+wGNQO?I79JgUPZQp~`O`Vvp znQyEH43zlTCAs~}B)n;uzkp0)V5|ol&m7r>p~A~D3$IZP%B6u(<$xCc(3>&Nb_Iqr zK=M1zZN~2TV)0D127Ikj-CO^5vh)jy)2KU}HOIXpWjF5%oQ=;?04I1)>Zp05?Uo?sl z#~`*WV~5T<94cXEoO~t#{}m@*Qbige&==s#Lr~Osk-oh(Bk!;!cIn(j7p~V0uS*J= zjL@c}(&34M1ooOQO3cJ7CCsCjJ8j-{e?%SqumflgB6Zro2b^eQt$`G{Rl(>q3xpU0 zz59ia(_6l1N(*wZ>eDQETMWIs5>~2whm~Kjwp_!1%wv~nh-SNKtMgT>l9$MN1*#1L zlz2{}{2eByVq{gOz|Rjsu{#T|K@H3KZM!SiNSt_oc8eep<{bqMKMH&af@Saf+h%mm z7}j=T6G*|7m+Xm+_kYQrin`wIl%ejd=g*_*1*%1}N?6l9ZaF z_BzITXyS@jtWvuP=H(lQxX$3eE=Q{JBWy@hHHa^X=o`_-`wq%hQj>6h>oa~4yld{j z97qp&UQ(-qUQqGmsv-g}6RcSk(YX$hmpUTC;=AdZFKImZsQ}uFKgDVP4rrb~df+(` z_zFnAETRM{%L-U&qG<=|wA)LtILSI+bN2q`^p~l1T;$VugDRhHwUD4BG9dh&(V;-c z2SR{L#ZU5$uad=cfs`Jt4+nvsz{m5wiwqO&n`K>3 z;IkER*(&g?F5UCta+;CYqto2;=IRvdp3mcXaWx-!`*?O^wMw#l#%P$@=lS#y2_q0Bjw*jZUn z)#Ah02hV*^E|tdbwhSctoEWF3n8iOK?vB6k(+jMtQ9mB6>4?dkb_~49E}c>{uoU7R z>sm~r5@%3y0vdm2I#8I@(+#-#lg$2V!8a&Vh?IQGX0cei%q6Ismw1xH#MLq2NfMtZ z3ck;9$VqBpB}MFHVdmjPyC<2398I8tH!ivU$X|jYIX3uGA7^lo`}Nx)>H44wA5}D^ zUT|Wt^JB zjN;+!g47owcnv*R$x&hgapK@97j&i{n+Im5;a8-v-_{sd=xeV&PT24HoY@VZVs=m) zYaGmKsyf)-L$^;cm(3p7yVj(nf$6QnNSD%yBwam=mPU`$c^&>_KC-NlFKxO7f4ZVb z)J&DhAD1xtE{xRAI}?A3KmEzwb+`jRqUT-Q>D7os-J;zo^vAPd3OArcvAW|KG$u2i zZ2Z_l=97m6PoNaXBE7#Az%4te6Pr3HfV+`31TK8KNG;^lo#7rZ#vg$7Ei#GfqmO&g z21A&rkHIj&3SBGVyHKkZ-b8c6C6u(%;saAh1R8=%d73A1|4#98|1SDx;Aj}}v)$wF z_*oY*J-zWdLzwSIXZ)(i_0BADHeZ^@#rYZbmXmrn@V<+x?eWq1Ju0)w>+ulA^vCPN zA0g$uPftyK6sH#6A4F~=M1`|7H(?1+VeMD~Y$&M6Y+A9pP!)}#ki8w?_R`=9*Gk`i zGO9zdvUme(xj6E5A!5LMSCE z&74C<74a&>;WMO1V*DOrnS*(iBep#U?W9W}L8F)$?2v>XG5U7G8E4uXg4jrcjUX%} z4~*gpss0YFTM;c;no~P|oDVH;BP-2^Ex_(yrFcLHwb0l<5G8+LP@v|d&vwT6q9c5K z`uSbAyg~rs7zdpJUxXs8eCTxpjqi!gJE2RiTd+RbW%qdWzU6#uA1+pFVGYQjv_!Zg zN$M$?r*9^Yjy(zT1-yR`R)g=zos5^?SfgPPqzQ$*~-fFrlPAk*Pj`p*z z2M5PRTD)d8+L&quEsf3k^A*k z*K^hL>s3L+mGNfL5cqL;6E$srCiRvQhe4;%>YN`zjGveRZTJ?;gUf!I*f9g~b()BH zaowILg%HV>`O_fW&>f3mbs1br?EHOf40XX1=a8vJ0n3+S+;OSGFZEkAz9{j^z8yF*6bajp*71#&?yZ)PV1!FqQw1U-DaTNBKwfbmpx4b)3c-4WxxYx`R8AiuINfk0Kzi7rbgGY33GjUC6 zUE<$I4OT9>e5?PeeL14s59vWMdj8{GXL703+jnrL){6Q^tvMBnFzBzRk>fIvkXT{^ z03mEtv_K7rAs2c0IWbS9H#6@@aGu%Tw1<_M{rD*8rflb+FT6O?nwJ|hiizfH?dB(H zPKnG^uNz3S%aVs~NhKvR#%LXzS89>Bw2?xoeFi?b2HKZgJZRFkSdn$`Bf>l9ltH}} z_ziE%+)Dhdk@8^iI|PMXoL(S{@}&p(EYyMF5g4(hEtDvy?jjL3&8D1xRK&eI*f-xo zLR5NdLWD_FCF^waX9U{<;9^P@tCU7pp)BDT01o~k5+3UHgIt`{WvXCF+_82Xxg=a- z5KRy*2JLka0Xq8>TSeiG1ByTDeqK_JQTv)qZ~8;pr!nd=X_>dHDR@yQE$lGQdn0|6 zEYw>w`jcZHuSZ4b=9Q0Js<;pseYJAxs?!5Z>gG+}i| zj=1_{UTJi#My%kG?tmy!xZNjN#63;^4@So)A_CQ~kaf2lL#*jw&ie%mB)H zfnJVYw`m#=r;Y4~wOzvcjr6w^^cD~c_Xz(Tp5~@<{W(zqfFEoCz)R2qMX@g?`nE*^Y z1xdirNxT7>7_d288Dz#ziJmg59{z6h)kRl!wBoUhGoCDg$9vEB2WjIWe9s3*j-$^F zw_WGSj*^Vn9#4D52&1p!TRm=ulXKhJv~CY{pC5}?>A5v^oLer34^CF->07mKZ#K?5 ziFKdOu^HpyRnznjC(Z}`tyEQ7fn66ngSwtgx7Sza`6sQqT0WarCmv1@E>OUSuGMr# zPhi)^fmK|aww9Lj!}7%_-^1bHj+Jhkx`xyBRTFW%iVnl$b@8fO&FNuZ?mOVE&6(_l ztghR0PiHaqpp{OGnscs(ee7|IQ|tMD&zJl2(<{1SX3b~SXv@U)M4<8sveKtT;JzUK zcBp%g>2}m`aW7h|$t}y=Skcku)dN}T$vv~9JnVgDM*7%R5rgidM4%`Qk?X$d58z$# z`ZkaS`)S{Ebb7U2t8I3x9xpJX9BC)x2L_kYWGHXhLD%;-`>Ju)#02w&hC__#;tk#O zf?@5Fjd0bi06i0+YNLBGJU(YZ*hugrc%du?%w;z=8q}v z->{YNRcIc-wOgwv%NQyzJml14n<@q{XbvX$Ps$Ej;`oZqa((r+v2WbG;Zu_bq^z2a zea`g9R;T?jWya(8BnD#cueYbL+hJ=bC;Xck<<10|KJTA~F3OJR`3`)MAVF-(Z{ETFQIy!2 zf`_!Js3pwkv9N!;#X@zTsbkBTsiBud7eJOIrap`84(2c9s%_1o+T1lTp;WBaklD3S zRz`KI;EA+gnHg-N?<2^L^Ucm5PH9`zuqXr%UtYXWrFRGiw!>Yk<#i8#t7R75+;@s( z6NI#ktZ#Y|<09MjwXRa8#72+wg}d5J29_C=^yW&OO@ad>I7tO!m@6l>>T31H6)T4v z$gX*>W-{SpPH%iMD8O{Pj+E93uA}r*S}07EH4oR8_G^RVFC+F49Tkq6^t7(9RpaHg z3mObRp@e!xCsR*q5g+bl2l0MnC>CwPS+67@Q>~NUUN~YZACH9&6Jv9IF3TXfJ6D;( zu9t_W9Iso7r-HimlGeQ=tI)<9Pq+u4bt_-^+!1e47kGgxm1R%|%Q*chLkQL9kmPWm zCM+jU2oe5hKXMrdUR^2oM60aKs`J@loq}?*hGiSkPHwvTE7lX5m=i!2NV^SY-WUy` z3z*!v^#fEK@hT}s-#o)=_ZOM0I$X2@`U{OeaB^QlL1OruG8sq!QlYqW2^=tx>qi9I zGG@5)dS3yg>Mtsou%?jjv6A)M(WTBi>es9=kzuppPm3iLyIb@uOfqV(uiy}@0I5l( z3sdd7zM_C12xz*1qct{%=x^=vi9Ih!qjSM zrN?xL;0R9t)5;&!Kc@Vt!L68Z9Y#2a%|%aasR3L~M{ssog*-Gs)D<0~;u8BY9R&jX#tavQWDB zLuLfEmn7#fjy-*1O4w&sL_+MvD)Y7J%)tRC-R&ph4h9Iy=Bzm|;`rwZS)|r`NOOiB zK|^iQ^*JD-!YkV~93y}>QH2()O(+HrrrCt5N1@n5`lLsIX~I#7s7V=3^&F{j0Tn~j z^m!To4!lWQ(69Klq{OL7fq~$|Gi(+!6^vy&#;S~RyGQ5N%vYMK7C|^=#v*9hhAl}9 zaVR03^Ip!mr~wJ%uuA@lz>uXVGaKkY_zxDjY5g}O;16`z#jcYJ&|R-s5Q^v1FrnZBDeTne2l-^visU-TZbK7+|GYON z(anmR5qZ-t*WFS742vP@6|u|A0@LG03(>j}E|)C7zKeQMr21i;^p+3#M`}Kl2YzTI z4D)r|^bTnEVy-nFa~nt;t)0dlgt2M4=*D(;n-T#}Ay^7PLnHlMQvn|q4)&{?wno#* zEgZEj!{KXhpys650@o{)UBRq4wyHAVm&+fpbI$m1OBl*u{ZI-TW+p5Hq2LvQ8#qwU zNmEe<3}gi5h_fiMIls+ewJ*rvk~#Dw48-JweK-zkL~C9nb7FL`yXBd5s}2DFP!4C- zt<6BDWG%oKx`9~)+CEA^PNt)4k7Lmhy*WG#)}rXWCr#GUmbSarq%`-oSy+CM($~#wzH;_h=VDI?p(&Sf#%I&AN(kmsQn~H99bT(E8 zTf{ca0EHo1cV!eykhDYQP5_++TZbpMQxt~{FGfRIMvh31f}8V1FAv>71Q$ShRF>ty zX+&l~ID6Q5k;4c`Wc}dGW3W>?BWtrF_yyW*?Ofp0GYheK4=U@!V=e0^TH@oqgEDJX z0mPr@G8O3KKA^p%d*o$J-6IG*i|e;;(aEOA_?SV=E%;z}Riv*cOBZFTXyTBAao zrqyQp*EDICZTPj`@kYWp#zO8Sk)xTfu_o_Es^uEGT$JsLTOg* zk=2T}iGLWhnHGVDrBVz1MuMJ#eJ{|RllAsCh0kY#tHQNu2+~-|8GZ|w0{QHCpO@FV zT-i{uCcpCtJ{Y{nJO|>#K{*}HK+5zqE?s{Ziu8v$A-TnCPQ5sqfnDS&c4KkY(|H{M z40t$wnmGc~?7p?|jGXimI2RH&`PNNB%q-t*AEfjLMYIo@2P-_fUNopT0_0gBRPGXY^{uYh5?pSAa_?NSL z%}$g4$jt5(=GfE_#%K)wnHjAmbKER&DNvczteGZ$W1P+T%RI(rZIB|v{rx_Nt zW@bmEZ#v*}%nJJ{`ZF^K6IGy-9uTnCwRRl)!QGQ*^*URq0ZY=%No@*(%1CkBBZjR+ zezs52>oMC}B4SCK76qz`bhf_YGooeIHkXqe$AYg_+xh0gclE{;w^bYcF#CHRCm^L= z&f%Zye`Fzqo}a}y!YICtZH-E|D|DUj>dK=cO#v8@*Z>1NP)<5NI`Q9tIkpH29q#z< zpp-mLwwp20s6ll;R~+3{(K#Dc$#-_ZS`F=XJH3{*hn5g{=7m`?DwFXqqn-EHO5VA= zEF4aOYqee7n4fZ1>6DMQoEm!ywgN+?qsthPl;Wqb7qj+tJvz>r6OqkWYPjiB7#rO% z=1^m0>-#Tib2NVi6k8f9b;u#9~R;{|=5Usz>|p^-gGR?X<;MoCzQD|&403OTqj zfef=t8^(@`Qv^x9)`Ve`C_o&F3cOR)@4L^S1u|B9 zn^FH|Jh8&a{t)bzY87tPJwy&pXcQsdj}`Z?=tu zIbJ))Xp8ffvDB?=O*UY6J$2hZYd!~x?0vI|tfM8S=WKCs{dPwz=~fcEcIdZS#Y1KT zeGh@?x89e0je?zus@khrAH(RRq@K&Z9(*B9^2y8YS+-Dk_b?OZ6e@h?2FiqKeZKTm zAs_L>juXMue?m#-gr{qW{{R8GF}=3XeY z&<@>{Nhnf@@nj(uquxACY;yRtojvQGt7X$3ay8kQ70YgZ_gZ(M%-Ej;M&2&`0;VR2 zW%IBRSl>NE>3`JO?^8OW*p@7clF3;fF#(;NTAN&U8D&r^^06}EWc?wLgWDQ%7q{dT zMbx)5u7y6ISAFXI^7v+^7zp_9TEiP7eVQ*20Dv7V0D$tl*5GJmYi3|&W%g2LC{@u6 zpA^OPD4znZX*?uxI9TG-!VMUemUU-MSJH_DS0~LyUzr@?I)0j<6Kp17SS|w3QSpDb zz^jx|RN#Z%y|o(g@eZ5Eu&*~+gj!CAJMHZKs^j*{ zGE9sOD4+2oJSi#|1~VzPK*_MMmz;VFv{mK0&jq{l(_(-QlRFFYl|s zC=p*rPd>MY4X5kHwyN}#A&<5%=a=DklQ6z1t-B@8=^0G6u{6-skxI$UjKvd{MINhU zPNXZ61$hg8a^cbp7_#^D%SG>KGF&RJ=_z4a{d|qor%k1zx`MbcD>me_zh_?jq`G*h z8@z%i;Ida>@2JIdGQn_KPfjc3KZl!as>{Dgn?66g(sDbzDZHJ1m&eDG_Ixq>IQyZB z_+(U<=f_+1hM{xj$scEfbygmCa!IC@)$Iee3zsv7VuO(h4ZFh8^}MK^U1Wma^EDnW z&0mi^%ky__veLATSV-LM%Rso(zFo+HcbKwgb)=D`@lIa_nKId>ppPC&T?bW{(KJC zDu{>La|_EFZ?7MFkDrZQ>cxTC1ui8!DU5v?cW2G@BUefteaZKFk zew+IfrsWfCeuu|rYn=MwYcJHRMhc-xnp~a9gW}LG(el_p@p9Zp6Q}x_bJxJ(vBir< z9VaR0LW%O4vwM$Klq6Sexhv`l!x8HQgXIeiZ@yEmb`WZGAI#ZNLqZ-Sk=lU37 zm&$cgEpym@@a587=?w8)nSC_gK4Hub)hP{HFgZ{aGm$_;tYzo&NYCzI0z=*$nAu*V z-%_bUI1;D2se3FVrhd(e;04h#VRfzL(6@W}ZZv%*-;t2j7p$66)I?(w&q8uMl?Nq4 zx`ze#5J=QO8Nv{h8ILXo8DA0V$5%FYjaX~3|1ZI2Aj&1NWDdLDY90GaCnk0+F`}3`MfmVx|!(cE|laK(@ z1rLmF)xj+d^*SU;T|4-oO~xNXm5mEbWOuZ1EKzMyOp_x^;0Q*3lnFH&pUp(iRnb3@ zds67@*m+XJf3fe;|LaO#v}TCaa0Q@hVkUI6oP0>^S)#K!hXGKcT*~_EP3UHuLBie| zZoq-7$5?>BG=)aVBpVJ*m6>L;b{$4%2HWX1O26YSan$P);(!Xw`kMLcqz8|-dH*g2 zDKt+E2{knTp3z1;qV^3V?hZm}isQLWAyFO*TQhPpDyZqeqUdMj}y=)kVte zG($Vi!y!iz9WmvhoBnUI`mqItBS0H684$6bt3NfflLou7llr89UgK-NAED?Q3ib-=5K5jYl^AG%xoF0Rj+qX!jl*GhM%@= zT0Q`69$Fq9^Cskw<^6ufH{uG4z>?nR7K%;OPl?$=Dp7NAx6&k)yQ1IJl_V}sD_jo& zjswZ5-T;qU=@4qZwwbHz2OeyUX58D%Mfg2vQU$~0Gy0FZ^1C(tqCv0Z>dkLD<&71T z8+U#x6u_s(%x6*uDDE-jNUke|SeiFt7G>p$@zYub(><@^USBV@XEv=mUD(7e*GKd+ zJ=p(X72QK#8lEHs1mqPV5+m%HgTW!(#h9ZuX;FKrM@#0tl>%rne-lrP$OJb%XI+AC z(EncV-(kJ3OpYbBEg}X2pc%vkuPrapFk}Y$qsPevTWl&*!*b_${=kgI4i%kH{}$sD zIuPp2I8xHl<{K_JdkH>E$-VY8rsvDsw*vl&k+an`8o8(hP zAN8P&S>N;LA|Zzq<_KVhO~AdegPKOiLUEGGDA34q5itEA;QqD7&p<(r-lp3hOgb{D z-n#~~5!QbK{eu?usQH8f?%>WTZg&YYTHQ1$U_-hp)yBR$($U0cnC z?-b&MUJX_#4|CW{g<_A!oGG z3lVwaX>V-$l%l4v&Ii02IbF^-562g&YmSjx7|bhVKs&Rnt)%YAo_zDT8oapiz>Wq} zLB(|i3N{O%XI8|$w*9DqlNF9GQuX?mdsN2xx@mkI z?q@5D#?hA_PdoPZR(8b`e|iF+9ye3fNN{1S!4o=RN@NsLc%Bz`mmPaOIdI#6;R#`v zLuiDq2%Ua;KT%{x_#O?V1ma;(zfL_Fhx3AO_0|!8o!HIc$)}e*>*#(Xu8{R*H$^O7 zl%5ov^?RB(gMn~-06nRXCK)LjQ6sA*8rl*b+SgmXQNBDa;NkPGr4KR_m@6S=%g1r> zsOVAjH>9L`pJYXC?`N^|QP9!RFqjC*U*me(xjx1RX1lf#FSoEfuZ8%jbVD`giwh2t zq-W)RapKkqsFn@~+B3I4In#wnDUsq-Qr%OzbGY2HHs}uF1z@6Q%gRPSh_K=)u%@g& z_gqkgW1yoU1Yja$GxBmh>=3r!?wiOwKSEJ!C%*L=8(cq)eGKs|s zcP0&sc89;|ku_q+M^j`PiZ&s8OZ7QE0G3V>zC@~=+!33QtQD@y;c&P({ zhJ5>@i+`0j8KkZvav!ToR5yerDvsD$;vUwBXm`n*9`ig^4t%tRJj{k)B%2xvn-UdK zA5|2)>6$6z&{L(#t=UOG>7SJNN&jwHp{dnLIY|qLZw-N5LopnNUd}J2*7BNBQdqAl zCt>&!-k8i_Cdz<)k~ZaAkd|cd(hG+*V)Gcx2J~`3H)%P5c8EWC{x{!2g`rQN;h{xp z#*x1JMX|&z!jK;7UFI&fvM8P&D0YACRbIRJ8M$fw+h$+mKzN%p@i;is2gjDZ<#VBd ztg`c;@A?frN{XSP39S&<3b}7p0=!jKJsX>}YaAU24NLPZW@qRo>C5N0j#6tXr{`AY zZOn(q@c4ZDBjQsm3HD}sbjXY5DUw?bG1EN8O1}wU>%7Ag?iC%--I?8|;nd%cex(6k zGw*3C{5hi8&;9`TlKuWOo(qK60_fTrnSc2M%;n1A&wtF$8)+Pobj>7n#TcdrB~;20%Fl=@L?i-0`cR3l7y;@x)gs@umV{ACiI zw|QaBKKAlH{8s%At@5XCPHM|OVkIrp5Y9^0osdoY=LQdczT4a}3k>9u+HMMC%z<2Q zqCb!CDarAtbFqo!zvnX~^JQpkP zr%@K#O{K_Gd-DjaP+zy~xi-`*5oqZ5qqg?EgC~v>fhDD%=#fOU{1X&|m$tOpZ0HjE zv}A~bDKdud_$McKIy)lAHLC~3IQ=NH4uIs$echXJ&b(~8Vz%e>#g6~GEdPa#`zLXZ zSP~4ufB^u(P0F!m7t13o`bd>F@-0G&>fMkHO=JpZGU||v zeu6#hry8aq8pvn{#OMIMD1Ut!6g8bkh50|U(JI=O)eR&*dYIqPLQNqnOFJV=J6$Db zYojmPFEy3n*g?zA7rDjWqeRvz8)Tp;V|j}Z556kAgwS#&LHlaqjOgEX(#=nS3N<13 zRXV3T&2=OcBX==9UW64Quwnd-85R92?>Nydmz2uzbxF5T(n~>Fc^srSnt)1=lk+St zEw%QbBDOrbixpn$FlJu`Jp(!u4P0DRc-cD3q0$J9x6mt}YmVz#7ksb3mouxCKR}Yb zrFXmJ&hL}NfPt#qwQ2ZnkSnF8xvAj9yv0&lOMMm`A!jt_t8RG~%!;!rUZCOuQ?kEe z!pfkEisW}uo<)vS7D~Wj7#S^B$!8Rphx^SIF6J%$XGl0DdRn>fxnkD5fy6s4=AQ|s z0?Un+lxHbr#*-^~zZC1^8ie_cB7`McXRNejzeC5L9Op!ZX94KG`XPFKuRPCoJ+loIF9D*R@h4?^uM}QaZN9XktVHAi+3k8zgiN|=l4Hvog)3|y zGZ#AB@0-9x=F002PX9YCrM*kAtYzdQZYW4%!P?zH1CnqL<7e|7d^!oQD~&R!1CE@g7I zR@N{6@xS})LN9Ld2kGP@$V2|S@$deq{*z4K+WJL3|D<*p%U_iMQR{-J(SCLG@6j%R zuKv}jp_Q%CKauNl(4q^28v23AU$C|>$M4P+L1o9k$?d;@V77lV_-6@&DE@W4+($pi zMS-wU|Nm0j(*=7W__y2pZz=uh_+C2tJy2A={~-GxZtzce{fXZCn@=>~FFyYR%k?LR zKWjdJb4XYG&Ec;K(EtCv4|Dt?{Wl!?zwiA|K=j`|Uj8T9-|*-^hx}(5`ELe5*hF*36IpSnHb{fb@482~720|(&zi}LrZVL|=Uj{l(j z#o`yb`_JLiQ*(GFy#fH*T>yV@3G?_zF3f*x>ff;Pp3y$(6p-I1ef=jFo!Ecm!tzgC zLR_)4D?#e72txn;_Z=~%{v#LGf8t^gEX?Z}0RRk{K>Wc4r{EvCu>Ix&^^#@*ut0yr NAo)ESfGijA{{S1$EGPf~ literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e44d59d --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# 航标 · 班级达线指标预估 + +B/S 系统:管理员上传历次大考总分,系统用卡尔曼滤波估计每个学生的稳定水平,模拟后续考试的年级排名,把达线概率加总后下达各班指标。下达人数是“按现有轨迹本来会到的人数”,班级工作是超越它。 + +## 启动 + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +python3 -m app +``` + +浏览器打开 http://127.0.0.1:8765 + +默认账号:`admin` / `hangbiao2026`(请在 `app/config.py` 修改) + +## 成绩表要求 + +- Excel `.xlsx` +- 至少包含:学号或姓名、班级、多次考试总分 +- 建议包含:方向(物理/历史)、选科、学籍、异动 +- 物理/历史分表、艺体表、异动表都可以;系统会识别并在第二步让你勾选口径 + +同目录的 `2024级指标分析成绩基础数据_5次考试.xlsx` 可在首页一键演示。 + +## 四步分析 + +1. 数据体检:读到哪些人、哪些考试、分数尺度 +2. 口径与目标:算谁、年级总盘子、预估步长 +3. 滤波与回测:Q/R 含义,以及用历史考试验证准不准 +4. 下达指标:班级期望 / 下达 / 保底 / 冲刺,点班级看学生,点学生看轨迹 + +## 指标怎么理解 + +- **下达指标**:滤波期望取整,合计等于年级目标人数 +- **保底 / 冲刺**:模拟分布大约 20% / 80% 分位 +- **相对最近一次**:预估相对最近一场已在线人数的增减,用来找上升生和不稳的在线生 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__main__.py b/app/__main__.py new file mode 100644 index 0000000..41be52c --- /dev/null +++ b/app/__main__.py @@ -0,0 +1,11 @@ +import os + +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host="127.0.0.1", + port=int(os.environ.get("PORT", "8765")), + reload=os.environ.get("RELOAD", "1") != "0", + ) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..6a12aef --- /dev/null +++ b/app/config.py @@ -0,0 +1,14 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = BASE_DIR / "data" +UPLOAD_DIR = DATA_DIR / "uploads" +DB_PATH = DATA_DIR / "hangbiao.db" +SAMPLE_XLSX = BASE_DIR / "2024级指标分析成绩基础数据_5次考试.xlsx" + +SECRET_KEY = "hangbiao-dev-secret-change-me" +ADMIN_USER = "admin" +ADMIN_PASSWORD = "hangbiao2026" + +SESSION_MAX_AGE = 60 * 60 * 12 +MAX_UPLOAD_MB = 15 diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..1fcd67a --- /dev/null +++ b/app/db.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import json +import sqlite3 +from datetime import datetime +from pathlib import Path + +from .config import DB_PATH + + +def connect() -> sqlite3.Connection: + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def init_db() -> None: + conn = connect() + conn.execute( + """ + CREATE TABLE IF NOT EXISTS analyses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + filename TEXT, + created_at TEXT, + step INTEGER DEFAULT 1, + parsed_path TEXT, + settings_json TEXT, + result_path TEXT + ) + """ + ) + conn.commit() + conn.close() + + +def create_analysis(title: str, filename: str, parsed_path: str) -> int: + conn = connect() + cur = conn.execute( + """ + INSERT INTO analyses (title, filename, created_at, step, parsed_path) + VALUES (?, ?, ?, 1, ?) + """, + (title, filename, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), parsed_path), + ) + conn.commit() + aid = cur.lastrowid + conn.close() + return int(aid) + + +def list_analyses() -> list[dict]: + conn = connect() + rows = conn.execute("SELECT * FROM analyses ORDER BY id DESC").fetchall() + conn.close() + return [dict(r) for r in rows] + + +def get_analysis(aid: int) -> dict | None: + conn = connect() + row = conn.execute("SELECT * FROM analyses WHERE id=?", (aid,)).fetchone() + conn.close() + return dict(row) if row else None + + +def update_analysis(aid: int, **fields) -> None: + if not fields: + return + keys = ", ".join(f"{k}=?" for k in fields) + conn = connect() + conn.execute(f"UPDATE analyses SET {keys} WHERE id=?", (*fields.values(), aid)) + conn.commit() + conn.close() + + +def load_json(path: str): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def save_json(path: str, obj) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8") diff --git a/app/excel_parse.py b/app/excel_parse.py new file mode 100644 index 0000000..9171b70 --- /dev/null +++ b/app/excel_parse.py @@ -0,0 +1,253 @@ +from __future__ import annotations + +import re +from io import BytesIO +from pathlib import Path + +from openpyxl import load_workbook + + +META_ALIASES = { + "sid": ("学号", "考号", "id", "学生id"), + "name": ("姓名", "学生姓名", "名字"), + "class_name": ("班级", "班", "行政班"), + "track": ("方向", "科类", "首选", "首选科目"), + "subjects": ("选科", "再选", "科目组合"), + "status": ("学籍", "学籍类型", "身份"), + "move": ("异动", "备注", "说明"), +} + +EXAM_HINT = re.compile( + r"(总分|期末|联考|月考|模考|开学|学考|诊断|调研|高考)", + re.I, +) +SKIP_SHEETS = ("说明", "readme", "目录") +ART_RE = re.compile(r"艺术|体育|艺体") +LEAVE_RE = re.compile(r"请假|出国|休学|国际|不在校|港澳|港籍.*假") + + +def _norm(v) -> str: + if v is None: + return "" + if isinstance(v, float) and v.is_integer(): + return str(int(v)) + return str(v).strip() + + +def _is_exam_header(text: str) -> bool: + t = text.strip() + if not t or t in {"考试次数"}: + return False + if t.endswith("-总分") or t.endswith("总分"): + return True + return bool(EXAM_HINT.search(t)) and "次数" not in t + + +def _map_meta(header: str) -> str | None: + h = header.strip().lower() + for key, aliases in META_ALIASES.items(): + for a in aliases: + if h == a.lower() or h.replace(" ", "") == a.lower(): + return key + return None + + +def _sheet_kind(name: str) -> str: + n = name.lower() + if any(x in n for x in ("艺体", "艺术", "体育")): + return "art" + if "异动" in n: + return "leave" + if "历史" in n: + return "history" + if "物理" in n: + return "physics" + if n in ("全体", "全部", "汇总"): + return "all" + return "other" + + +def parse_workbook(path: Path | BytesIO) -> dict: + wb = load_workbook(path, data_only=True, read_only=True) + sheets = [] + students: list[dict] = [] + exam_order: list[str] = [] + warnings: list[str] = [] + + for ws in wb.worksheets: + kind = _sheet_kind(ws.title) + rows = ws.iter_rows(values_only=True) + try: + header_row = next(rows) + except StopIteration: + continue + headers = [_norm(h) for h in header_row] + if not any(headers): + continue + meta_idx = {} + exam_idx = {} + for i, h in enumerate(headers): + if not h: + continue + mk = _map_meta(h) + if mk: + meta_idx[mk] = i + continue + if _is_exam_header(h): + exam_idx[h] = i + if h not in exam_order: + exam_order.append(h) + if "sid" not in meta_idx and "name" not in meta_idx: + warnings.append(f"工作表「{ws.title}」未识别到学号/姓名,已跳过") + continue + + count = 0 + for raw in rows: + if raw is None or all(v is None or str(v).strip() == "" for v in raw): + continue + rec = { + "sid": _norm(raw[meta_idx["sid"]]) if "sid" in meta_idx else "", + "name": _norm(raw[meta_idx["name"]]) if "name" in meta_idx else "", + "class_name": _norm(raw[meta_idx["class_name"]]) if "class_name" in meta_idx else "未分班", + "track": _norm(raw[meta_idx["track"]]) if "track" in meta_idx else "", + "subjects": _norm(raw[meta_idx["subjects"]]) if "subjects" in meta_idx else "", + "status": _norm(raw[meta_idx["status"]]) if "status" in meta_idx else "", + "move": _norm(raw[meta_idx["move"]]) if "move" in meta_idx else "", + "sheet": ws.title, + "sheet_kind": kind, + "scores": {}, + } + if not rec["sid"] and not rec["name"]: + continue + if not rec["sid"]: + rec["sid"] = f"{ws.title}-{rec['name']}-{count}" + if kind == "physics" and not rec["track"]: + rec["track"] = "物理方向" + if kind == "history" and not rec["track"]: + rec["track"] = "历史方向" + if not rec["track"]: + if "物理" in rec["subjects"]: + rec["track"] = "物理方向" + elif "历史" in rec["subjects"]: + rec["track"] = "历史方向" + for exam, idx in exam_idx.items(): + if idx >= len(raw): + continue + val = raw[idx] + if val is None or str(val).strip() in ("", "缺考", "—", "-", "/"): + rec["scores"][exam] = None + continue + try: + rec["scores"][exam] = float(val) + except (TypeError, ValueError): + rec["scores"][exam] = None + rec["art"] = bool(ART_RE.search(rec["move"]) or ART_RE.search(ws.title)) + rec["leave"] = bool(LEAVE_RE.search(rec["move"])) + students.append(rec) + count += 1 + sheets.append( + { + "name": ws.title, + "kind": kind, + "rows": count, + "exams": list(exam_idx.keys()), + } + ) + + wb.close() + + # Prefer specialized sheets over 全体 to avoid duplicates + has_split = any(s["kind"] in ("physics", "history") for s in sheets) + if has_split: + kept = [] + skipped_all = 0 + for s in students: + if s["sheet_kind"] == "all": + skipped_all += 1 + continue + kept.append(s) + students = kept + if skipped_all: + warnings.append(f"已忽略「全体」表中的 {skipped_all} 行,避免与物理/历史表重复") + + # de-duplicate by sid, keep first non-all + seen = {} + uniq = [] + dups = 0 + for s in students: + if s["sid"] in seen: + dups += 1 + continue + seen[s["sid"]] = True + uniq.append(s) + students = uniq + if dups: + warnings.append(f"按学号去重 {dups} 条") + + if not exam_order: + warnings.append("未识别到考试成绩列,请确认表头含「总分」或「期末/联考/月考」") + + return { + "sheets": sheets, + "students": students, + "exams": exam_order, + "warnings": warnings, + } + + +def summarize_parse(parsed: dict) -> dict: + students = parsed["students"] + exams = parsed["exams"] + tracks = {} + classes = {} + status = {} + art = leave = 0 + exam_present = {e: 0 for e in exams} + for s in students: + tracks[s["track"] or "未分方向"] = tracks.get(s["track"] or "未分方向", 0) + 1 + classes[s["class_name"]] = classes.get(s["class_name"], 0) + 1 + status[s["status"] or "未填"] = status.get(s["status"] or "未填", 0) + 1 + if s["art"]: + art += 1 + if s["leave"]: + leave += 1 + for e in exams: + if s["scores"].get(e) is not None: + exam_present[e] += 1 + score_scale = [] + for e in exams: + vals = [s["scores"][e] for s in students if s["scores"].get(e) is not None] + if not vals: + continue + score_scale.append( + { + "exam": e, + "n": len(vals), + "min": round(min(vals), 1), + "max": round(max(vals), 1), + "mean": round(sum(vals) / len(vals), 1), + } + ) + return { + "n": len(students), + "n_class": len(classes), + "tracks": tracks, + "classes": dict(sorted(classes.items(), key=lambda kv: _class_key(kv[0]))), + "status": status, + "art": art, + "leave": leave, + "exam_present": exam_present, + "score_scale": score_scale, + "sheets": parsed["sheets"], + "exams": exams, + "warnings": parsed["warnings"], + } + + +def _class_key(name: str): + m = re.search(r"\d+", str(name)) + return (int(m.group()) if m else 999, str(name)) + + +def class_sort_key(name: str): + return _class_key(name) diff --git a/app/export_xlsx.py b/app/export_xlsx.py new file mode 100644 index 0000000..2a3c921 --- /dev/null +++ b/app/export_xlsx.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from io import BytesIO + +from openpyxl import Workbook +from openpyxl.styles import Alignment, Font, PatternFill, Border, Side + + +def build_export(result: dict) -> bytes: + wb = Workbook() + thin = Border( + left=Side(style="thin", color="D9D1C3"), + right=Side(style="thin", color="D9D1C3"), + top=Side(style="thin", color="D9D1C3"), + bottom=Side(style="thin", color="D9D1C3"), + ) + head = PatternFill("solid", fgColor="12233D") + head_font = Font(color="F4EFE6", bold=True) + wrap = Alignment(wrap_text=True, vertical="center") + + first = True + for g in result["groups"]: + for t in g["targets"]: + title = f"{g['group']}-{t['name']}"[:31] + ws = wb.active if first else wb.create_sheet(title) + if first: + ws.title = title + first = False + ws.append( + ["班级", "测算人数", "最近一次在线", "滤波期望", "下达指标", "保底下界", "冲刺上界", "相对最近一次"] + ) + for cell in ws[1]: + cell.fill = head + cell.font = head_font + for row in t["classes"]: + ws.append( + [ + row["class_name"], + row["n"], + row["current"], + row["expected"], + row["assigned"], + row["floor"], + row["stretch"], + row["delta"], + ] + ) + ws.append([]) + ws.append(["说明:下达指标为预估中位水平,班级应努力超越该人数。保底/冲刺来自模拟分布的约20%/80%分位。"]) + + ws = wb.create_sheet("学生明细") + exams = result["exams"] + headers = ["学号", "姓名", "班级", "方向", "学籍", "选科"] + headers += [f"{e}-排名" for e in exams] + # collect target names + tcols = [] + for g in result["groups"]: + for t in g["targets"]: + tcols.append((g["group"], t["id"], f"{g['group']}-{t['name']}概率")) + headers += [c[2] for c in tcols] + headers += ["滤波水平", "预测方差"] + ws.append(headers) + for cell in ws[1]: + cell.fill = head + cell.font = head_font + cell.alignment = wrap + for s in result["students"]: + row = [s["sid"], s["name"], s["class_name"], s["group"], s.get("status") or "", s.get("subjects") or ""] + ranks = s.get("ranks") or [] + for i, _e in enumerate(exams): + row.append(ranks[i] if i < len(ranks) else None) + for gname, tid, _ in tcols: + if s["group"] == gname: + row.append((s.get("probs") or {}).get(tid)) + else: + row.append(None) + row.append(s.get("pred_mean")) + row.append(s.get("pred_var")) + ws.append(row) + + bio = BytesIO() + wb.save(bio) + return bio.getvalue() diff --git a/app/indicators.py b/app/indicators.py new file mode 100644 index 0000000..909b356 --- /dev/null +++ b/app/indicators.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +from collections import defaultdict + +from .excel_parse import class_sort_key +from .kalman import ( + fit_qr, + hamilton, + holdout_hits, + kalman_filter, + monte_carlo, + ranks_desc, + zscores, +) + + +TRACK_DEFAULTS = { + "物理": [{"id": "t88", "name": "特控线上", "k": 88}], + "历史": [ + {"id": "top5", "name": "尖子生", "k": 5}, + {"id": "top12", "name": "600分以上", "k": 12}, + {"id": "top52", "name": "特控线上", "k": 52}, + ], +} + + +def infer_track_group(track: str) -> str: + t = track or "" + if "历史" in t: + return "历史" + if "物理" in t: + return "物理" + return t or "未分方向" + + +def default_targets(groups: list[str]) -> dict[str, list[dict]]: + out = {} + for g in groups: + if g in TRACK_DEFAULTS: + out[g] = [dict(x) for x in TRACK_DEFAULTS[g]] + else: + out[g] = [{"id": "top20", "name": "年级前20%", "k": 20}] + return out + + +def select_students(parsed: dict, settings: dict) -> list[dict]: + include_art = settings.get("include_art", False) + include_leave = settings.get("include_leave", False) + include_mingyuan = settings.get("include_mingyuan", True) + include_other = settings.get("include_other", True) + exams = settings.get("exams") or parsed["exams"] + selected = [] + for s in parsed["students"]: + if s["art"] and not include_art: + continue + if s["leave"] and not include_leave: + continue + st = s["status"] or "在籍" + if st == "明远" and not include_mingyuan: + continue + if st not in ("在籍", "明远", "") and not include_other: + continue + scores = {e: s["scores"].get(e) for e in exams} + if all(v is None for v in scores.values()): + continue + row = dict(s) + row["scores"] = scores + row["group"] = infer_track_group(s["track"]) + selected.append(row) + return selected + + +def _forecast_horizon(q: float, p: float, horizon: int) -> float: + return p + max(1, horizon) * q + + +def analyze(parsed: dict, settings: dict) -> dict: + exams = settings.get("exams") or parsed["exams"] + if not exams: + raise ValueError("没有可用的考试列") + horizon = int(settings.get("horizon", 1)) + n_sim = int(settings.get("n_sim", 600)) + selected = select_students(parsed, settings) + groups = sorted({s["group"] for s in selected}) + targets = settings.get("targets") or default_targets(groups) + + # fit Q,R per group on z-scores + group_models = {} + for g in groups: + members = [s for s in selected if s["group"] == g] + z_by_exam = {} + for e in exams: + z_by_exam[e] = zscores([s["scores"].get(e) for s in members]) + for i, s in enumerate(members): + s["z"] = [z_by_exam[e][i] for e in exams] + s["ranks"] = [] + for e in exams: + rk = ranks_desc([s["scores"].get(e) for s in members]) + for i, s in enumerate(members): + s["ranks"].append(rk[i]) + series = [s["z"] for s in members] + q, r, rmse = fit_qr(series) + group_models[g] = {"q": q, "r": r, "rmse": rmse, "n": len(members)} + for s in members: + fr = kalman_filter(s["z"], q, r) + s["kf_x"] = fr.x + s["kf_p"] = fr.p + s["kf_xs"] = fr.xs + s["kf_ps"] = fr.ps + if fr.x is None: + s["pred_mean"] = None + s["pred_var"] = None + else: + s["pred_mean"] = fr.x + s["pred_var"] = _forecast_horizon(q, fr.p if fr.p is not None else r, horizon) + + results = [] + for g in groups: + members = [s for s in selected if s["group"] == g] + g_targets = targets.get(g) or default_targets([g])[g] + model = group_models[g] + eligible = [s for s in members if s["pred_mean"] is not None] + last_ranks = [s["ranks"][-1] if s["ranks"] else None for s in members] + holdouts = [] + if len(exams) >= 3: + series = [s["z"] for s in members] + true_ranks = [s["ranks"][-1] if s["ranks"] else None for s in members] + last_train_ranks = [s["ranks"][-2] if len(s["ranks"]) >= 2 else None for s in members] + for t in g_targets: + k = min(int(t["k"]), max(1, sum(1 for r in true_ranks if r is not None))) + h = holdout_hits( + series, + last_train_ranks, + true_ranks, + model["q"], + model["r"], + k, + ) + h["target_id"] = t["id"] + h["target_name"] = t["name"] + h["k"] = k + holdouts.append(h) + + target_blocks = [] + for t in g_targets: + k = int(t["k"]) + k = max(1, min(k, len(eligible))) + means = [s["pred_mean"] for s in eligible] + variances = [s["pred_var"] for s in eligible] + class_ids = [s["class_name"] for s in eligible] + probs, class_q = monte_carlo(means, variances, class_ids, k, n_sim=n_sim) + for s, p in zip(eligible, probs): + s.setdefault("probs", {})[t["id"]] = p + # current on-line: last exam rank <= k + class_now = defaultdict(int) + class_n = defaultdict(int) + class_exp = defaultdict(float) + class_mingyuan_exp = defaultdict(float) + for s in members: + class_n[s["class_name"]] += 1 + for s in eligible: + class_exp[s["class_name"]] += s["probs"][t["id"]] + if s.get("status") == "明远": + class_mingyuan_exp[s["class_name"]] += s["probs"][t["id"]] + if s["ranks"] and s["ranks"][-1] is not None and s["ranks"][-1] <= k: + class_now[s["class_name"]] += 1 + class_names = sorted(class_n.keys(), key=class_sort_key) + expected = [class_exp[c] for c in class_names] + assigned = hamilton(expected, k) + rows = [] + for c, a in zip(class_names, assigned): + qtl = class_q.get(c, {"p20": 0, "p50": 0, "p80": 0, "mean": 0}) + rows.append( + { + "class_name": c, + "n": class_n[c], + "current": class_now[c], + "expected": round(class_exp[c], 2), + "assigned": a, + "floor": int(qtl["p20"]), + "median": int(qtl["p50"]), + "stretch": int(qtl["p80"]), + "delta": a - class_now[c], + "mingyuan_expected": round(class_mingyuan_exp[c], 2), + } + ) + critical = [] + for s in eligible: + p = s["probs"][t["id"]] + if 0.2 <= p <= 0.8: + critical.append( + { + "sid": s["sid"], + "name": s["name"], + "class_name": s["class_name"], + "p": round(p, 3), + "last_rank": s["ranks"][-1] if s["ranks"] else None, + "status": s.get("status") or "", + } + ) + critical.sort(key=lambda x: -x["p"]) + target_blocks.append( + { + "id": t["id"], + "name": t["name"], + "k": k, + "classes": rows, + "critical_n": len(critical), + "critical": critical[:80], + "sum_expected": round(sum(expected), 2), + } + ) + + results.append( + { + "group": g, + "n": len(members), + "model": { + "q": model["q"], + "r": model["r"], + "rmse": round(model["rmse"], 3) if model["rmse"] is not None else None, + "horizon": horizon, + "n_sim": n_sim, + }, + "holdout": holdouts, + "targets": target_blocks, + } + ) + + student_views = [] + for s in selected: + student_views.append( + { + "sid": s["sid"], + "name": s["name"], + "class_name": s["class_name"], + "group": s["group"], + "track": s["track"], + "subjects": s["subjects"], + "status": s.get("status") or "", + "move": s.get("move") or "", + "art": s["art"], + "leave": s["leave"], + "scores": s["scores"], + "z": s.get("z"), + "ranks": s.get("ranks"), + "kf_xs": s.get("kf_xs"), + "kf_ps": s.get("kf_ps"), + "pred_mean": s.get("pred_mean"), + "pred_var": s.get("pred_var"), + "probs": {k: round(v, 4) for k, v in (s.get("probs") or {}).items()}, + } + ) + + return { + "exams": exams, + "horizon": horizon, + "n_selected": len(selected), + "n_total": len(parsed["students"]), + "groups": results, + "students": student_views, + } + + +def class_comment(row: dict, target_name: str) -> str: + assigned = row["assigned"] + current = row["current"] + expected = row["expected"] + floor = row["floor"] + stretch = row["stretch"] + name = row["class_name"] + bits = [ + f"{name}班{target_name}下达 {assigned} 人(滤波期望 {expected:.1f})。" + ] + if assigned > current: + bits.append(f"比最近一次在线人数多 {assigned - current} 人,班级里有上升轨迹的临界生,需要把潜力兑现。") + elif assigned < current: + bits.append(f"比最近一次在线人数少 {current - assigned} 人,说明部分当前在线学生轨迹不稳,重点是保住临界生。") + else: + bits.append("与最近一次在线人数持平,按现有轨迹属于维持性目标。") + bits.append(f"模拟分布大约在 {floor}–{stretch} 人之间,超越下达人数才算跑赢预估。") + return "".join(bits) diff --git a/app/kalman.py b/app/kalman.py new file mode 100644 index 0000000..e299d0c --- /dev/null +++ b/app/kalman.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import math +import random +from dataclasses import dataclass + + +def mean_std(values: list[float]) -> tuple[float, float]: + n = len(values) + if n == 0: + return 0.0, 1.0 + mean = sum(values) / n + if n == 1: + return mean, 1.0 + var = sum((x - mean) ** 2 for x in values) / (n - 1) + return mean, math.sqrt(var) if var > 0 else 1.0 + + +def zscores(values: list[float | None]) -> list[float | None]: + present = [v for v in values if v is not None] + mean, std = mean_std(present) + out: list[float | None] = [] + for v in values: + if v is None: + out.append(None) + else: + out.append((v - mean) / std) + return out + + +def ranks_desc(values: list[float | None]) -> list[int | None]: + indexed = [(i, v) for i, v in enumerate(values) if v is not None] + indexed.sort(key=lambda iv: -iv[1]) + out: list[int | None] = [None] * len(values) + for rank, (i, _) in enumerate(indexed, 1): + out[i] = rank + return out + + +@dataclass +class FilterResult: + x: float | None + p: float | None + xs: list[float | None] + ps: list[float | None] + + +def kalman_filter(zs: list[float | None], q: float, r: float) -> FilterResult: + x = None + p = None + xs: list[float | None] = [] + ps: list[float | None] = [] + for z in zs: + if x is None: + if z is None: + xs.append(None) + ps.append(None) + continue + x, p = z, r + xs.append(x) + ps.append(p) + continue + x_pred = x + p_pred = p + q + if z is None: + x, p = x_pred, p_pred + xs.append(x) + ps.append(p) + continue + k = p_pred / (p_pred + r) + x = x_pred + k * (z - x_pred) + p = (1 - k) * p_pred + xs.append(x) + ps.append(p) + return FilterResult(x=x, p=p, xs=xs, ps=ps) + + +def one_step_rmse(series_list: list[list[float | None]], q: float, r: float) -> float | None: + se = 0.0 + n = 0 + for zs in series_list: + x = None + p = None + for z in zs: + if x is None: + if z is None: + continue + x, p = z, r + continue + x_pred = x + p_pred = p + q + if z is None: + x, p = x_pred, p_pred + continue + se += (x_pred - z) ** 2 + n += 1 + k = p_pred / (p_pred + r) + x = x_pred + k * (z - x_pred) + p = (1 - k) * p_pred + if n == 0: + return None + return math.sqrt(se / n) + + +def fit_qr(series_list: list[list[float | None]]) -> tuple[float, float, float]: + usable = [s for s in series_list if sum(1 for z in s if z is not None) >= 2] + best = (1e9, 0.08, 0.25) + for q in (0.03, 0.05, 0.08, 0.10, 0.15, 0.22, 0.30): + for r in (0.12, 0.18, 0.25, 0.32, 0.40, 0.55): + rmse = one_step_rmse(usable, q, r) + if rmse is not None and rmse < best[0]: + best = (rmse, q, r) + return best[1], best[2], best[0] + + +def holdout_hits( + series_list: list[list[float | None]], + last_ranks: list[int | None], + true_ranks: list[int | None], + q: float, + r: float, + target_k: int, +) -> dict: + preds: list[tuple[int, float]] = [] + last_pred: list[tuple[int, int]] = [] + for i, zs in enumerate(series_list): + if true_ranks[i] is None: + continue + train = zs[:-1] + result = kalman_filter(train, q, r) + if result.x is None: + continue + preds.append((i, result.x)) + if last_ranks[i] is not None: + last_pred.append((i, last_ranks[i])) + preds.sort(key=lambda t: -t[1]) + kf_set = {i for i, _ in preds[:target_k]} + last_pred.sort(key=lambda t: t[1]) + last_set = {i for i, _ in last_pred[:target_k]} + actual = {i for i, rk in enumerate(true_ranks) if rk is not None and rk <= target_k} + kf_hit = len(kf_set & actual) + last_hit = len(last_set & actual) + return { + "n_actual": len(actual), + "kf_hit": kf_hit, + "last_hit": last_hit, + "kf_precision": kf_hit / target_k if target_k else 0, + "last_precision": last_hit / target_k if target_k else 0, + } + + +def monte_carlo( + means: list[float], + variances: list[float], + class_ids: list[str], + target_k: int, + n_sim: int = 600, + seed: int = 42, +) -> tuple[list[float], dict[str, dict]]: + rng = random.Random(seed) + n = len(means) + hits = [0] * n + sigmas = [math.sqrt(max(v, 1e-8)) for v in variances] + unique = list(dict.fromkeys(class_ids)) + buckets = {c: [] for c in unique} + for _ in range(n_sim): + zs = [means[i] + sigmas[i] * rng.gauss(0, 1) for i in range(n)] + order = sorted(range(n), key=lambda i: zs[i], reverse=True) + counts = {c: 0 for c in unique} + for i in order[:target_k]: + hits[i] += 1 + counts[class_ids[i]] += 1 + for c, v in counts.items(): + buckets[c].append(v) + out = {} + for c, arr in buckets.items(): + arr.sort() + m = n_sim + out[c] = { + "p20": arr[max(0, int(0.20 * m) - 1)], + "p50": arr[max(0, int(0.50 * m) - 1)], + "p80": arr[max(0, int(0.80 * m) - 1)], + "mean": sum(arr) / m, + } + return [h / n_sim for h in hits], out + + +def hamilton(values: list[float], total: int) -> list[int]: + if total <= 0 or not values: + return [0] * len(values) + s = sum(values) + if s <= 0: + out = [0] * len(values) + return out + scaled = [v * total / s for v in values] + floors = [int(math.floor(v + 1e-9)) for v in scaled] + remain = total - sum(floors) + order = sorted( + range(len(scaled)), + key=lambda i: (scaled[i] - floors[i], scaled[i]), + reverse=True, + ) + for i in order[: max(0, remain)]: + floors[i] += 1 + return floors diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3f7d76f --- /dev/null +++ b/app/main.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import json +import shutil +import uuid +from pathlib import Path +from urllib.parse import quote + +from fastapi import FastAPI, File, Form, Request, UploadFile +from fastapi.responses import HTMLResponse, RedirectResponse, Response +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from starlette.middleware.sessions import SessionMiddleware + +from .config import ( + ADMIN_PASSWORD, + ADMIN_USER, + DATA_DIR, + MAX_UPLOAD_MB, + SAMPLE_XLSX, + SECRET_KEY, + UPLOAD_DIR, +) +from .db import ( + create_analysis, + get_analysis, + init_db, + list_analyses, + load_json, + save_json, + update_analysis, +) +from .excel_parse import parse_workbook, summarize_parse +from .export_xlsx import build_export +from .indicators import analyze, class_comment, default_targets, infer_track_group, select_students + +app = FastAPI(title="航标 · 班级达线指标预估") +app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, max_age=60 * 60 * 12) +app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") +templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) +templates.env.globals["class_comment"] = class_comment + + +def render(request: Request, name: str, **ctx): + ctx["request"] = request + ctx.setdefault("user", current_user(request)) + return templates.TemplateResponse(request, name, ctx) + + +@app.on_event("startup") +def startup(): + DATA_DIR.mkdir(parents=True, exist_ok=True) + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + init_db() + + +def current_user(request: Request) -> str | None: + return request.session.get("user") + + +def require_login(request: Request): + if not current_user(request): + return RedirectResponse("/login", status_code=303) + return None + + +@app.get("/login", response_class=HTMLResponse) +def login_page(request: Request): + if current_user(request): + return RedirectResponse("/", status_code=303) + return render(request, "login.html", error=None) + + +@app.post("/login", response_class=HTMLResponse) +def login_submit(request: Request, username: str = Form(...), password: str = Form(...)): + if username.strip() == ADMIN_USER and password == ADMIN_PASSWORD: + request.session["user"] = username.strip() + return RedirectResponse("/", status_code=303) + return render(request, "login.html", error="账号或密码不正确") + + +@app.post("/logout") +def logout(request: Request): + request.session.clear() + return RedirectResponse("/login", status_code=303) + + +@app.get("/", response_class=HTMLResponse) +def home(request: Request): + if (redir := require_login(request)): + return redir + items = list_analyses() + return render(request, "home.html", items=items, has_sample=SAMPLE_XLSX.exists()) + + +def _store_parsed(title: str, filename: str, source: Path) -> int: + parsed = parse_workbook(source) + summary = summarize_parse(parsed) + token = uuid.uuid4().hex[:10] + parsed_path = str(DATA_DIR / f"parsed_{token}.json") + # keep a lighter parsed blob + save_json(parsed_path, parsed) + aid = create_analysis(title, filename, parsed_path) + update_analysis(aid, settings_json=json.dumps({"summary": summary}, ensure_ascii=False)) + return aid + + +@app.post("/upload") +async def upload(request: Request, file: UploadFile = File(...)): + if (redir := require_login(request)): + return redir + raw = await file.read() + if len(raw) > MAX_UPLOAD_MB * 1024 * 1024: + return HTMLResponse("文件过大,请压缩到 15MB 以内。", status_code=400) + suffix = Path(file.filename or "scores.xlsx").suffix.lower() + if suffix not in (".xlsx", ".xlsm"): + return HTMLResponse("请上传 .xlsx 成绩表。", status_code=400) + dest = UPLOAD_DIR / f"{uuid.uuid4().hex}{suffix}" + dest.write_bytes(raw) + aid = _store_parsed(Path(file.filename).stem, file.filename or dest.name, dest) + return RedirectResponse(f"/analysis/{aid}?step=1", status_code=303) + + +@app.post("/demo") +def demo(request: Request): + if (redir := require_login(request)): + return redir + if not SAMPLE_XLSX.exists(): + return HTMLResponse("未找到示例成绩表。", status_code=404) + dest = UPLOAD_DIR / f"{uuid.uuid4().hex}.xlsx" + shutil.copy2(SAMPLE_XLSX, dest) + aid = _store_parsed("2024级示例数据", SAMPLE_XLSX.name, dest) + return RedirectResponse(f"/analysis/{aid}?step=1", status_code=303) + + +def _settings_from(parsed: dict, stored: dict | None) -> dict: + summary = summarize_parse(parsed) + groups = sorted({infer_track_group(s["track"]) for s in parsed["students"]}) + base = { + "exams": parsed["exams"], + "include_art": False, + "include_leave": False, + "include_mingyuan": True, + "include_other": True, + "horizon": 1, + "targets": default_targets(groups), + } + if stored: + for k in ("exams", "include_art", "include_leave", "include_mingyuan", "include_other", "horizon", "targets"): + if k in stored: + base[k] = stored[k] + base["summary"] = summary + base["groups"] = groups + return base + + +@app.get("/analysis/{aid}", response_class=HTMLResponse) +def analysis_page(request: Request, aid: int, step: int = 1): + if (redir := require_login(request)): + return redir + item = get_analysis(aid) + if not item: + return HTMLResponse("分析不存在", status_code=404) + parsed = load_json(item["parsed_path"]) + stored_settings = json.loads(item["settings_json"] or "{}") + settings = _settings_from(parsed, stored_settings) + result = load_json(item["result_path"]) if item.get("result_path") else None + preview_n = None + if step == 2: + preview_n = len(select_students(parsed, settings)) + return render( + request, + "analysis.html", + item=item, + step=step, + settings=settings, + summary=settings["summary"], + result=result, + preview_n=preview_n, + ) + + +@app.post("/analysis/{aid}/settings") +async def save_settings(request: Request, aid: int): + if (redir := require_login(request)): + return redir + item = get_analysis(aid) + if not item: + return HTMLResponse("分析不存在", status_code=404) + parsed = load_json(item["parsed_path"]) + form = await request.form() + exams = form.getlist("exams") + if not exams: + exams = parsed["exams"] + groups = sorted({infer_track_group(s["track"]) for s in parsed["students"]}) + targets = {} + for g in groups: + names = form.getlist(f"t_name_{g}") + ks = form.getlist(f"t_k_{g}") + ids = form.getlist(f"t_id_{g}") + rows = [] + for i, name in enumerate(names): + k_raw = ks[i] if i < len(ks) else "0" + try: + k = int(float(k_raw)) + except ValueError: + k = 0 + tid = ids[i] if i < len(ids) and ids[i] else f"t{i+1}" + if name.strip() and k > 0: + rows.append({"id": tid, "name": name.strip(), "k": k}) + targets[g] = rows or default_targets([g])[g] + settings = { + "exams": list(exams), + "include_art": form.get("include_art") == "on", + "include_leave": form.get("include_leave") == "on", + "include_mingyuan": form.get("include_mingyuan") == "on", + "include_other": form.get("include_other") == "on", + "horizon": int(form.get("horizon") or 1), + "targets": targets, + } + update_analysis(aid, settings_json=json.dumps(settings, ensure_ascii=False), step=2) + action = form.get("action") or "next" + if action == "compute": + result = analyze(parsed, settings) + token = uuid.uuid4().hex[:10] + result_path = str(DATA_DIR / f"result_{token}.json") + save_json(result_path, result) + update_analysis(aid, result_path=result_path, step=3) + return RedirectResponse(f"/analysis/{aid}?step=3", status_code=303) + return RedirectResponse(f"/analysis/{aid}?step=2", status_code=303) + + +@app.get("/analysis/{aid}/api/student/{sid}") +def student_api(request: Request, aid: int, sid: str): + if not current_user(request): + return Response("unauthorized", status_code=401) + item = get_analysis(aid) + if not item or not item.get("result_path"): + return Response("not found", status_code=404) + result = load_json(item["result_path"]) + for s in result["students"]: + if str(s["sid"]) == str(sid): + return s + return Response("not found", status_code=404) + + +@app.get("/analysis/{aid}/api/class") +def class_api(request: Request, aid: int, group: str, target: str, class_name: str): + if not current_user(request): + return Response("unauthorized", status_code=401) + item = get_analysis(aid) + if not item or not item.get("result_path"): + return Response("not found", status_code=404) + result = load_json(item["result_path"]) + rows = [ + s + for s in result["students"] + if s["group"] == group and s["class_name"] == class_name + ] + rows.sort(key=lambda s: -((s.get("probs") or {}).get(target) or 0)) + return {"exams": result["exams"], "students": rows[:80], "n": len(rows)} + + +@app.get("/analysis/{aid}/export") +def export_analysis(request: Request, aid: int): + if (redir := require_login(request)): + return redir + item = get_analysis(aid) + if not item or not item.get("result_path"): + return HTMLResponse("请先完成测算", status_code=400) + result = load_json(item["result_path"]) + data = build_export(result) + ascii_name = f"hangbiao-{aid}.xlsx" + star = quote(f"{item['title']}-下达指标.xlsx") + return Response( + content=data, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename={ascii_name}; filename*=UTF-8''{star}"}, + ) + + +# Jinja 3 / FastAPI 0.115+ TemplateResponse signature is (request, name, context) +# Keep compatibility if older: handled by FastAPI version we pin. diff --git a/app/static/css/app.css b/app/static/css/app.css new file mode 100644 index 0000000..fc852cf --- /dev/null +++ b/app/static/css/app.css @@ -0,0 +1,149 @@ +:root { + --ink: #12233d; + --ink-soft: #3a4a63; + --paper: #f3eee4; + --card: #fffdf8; + --line: #d8cfbf; + --teal: #1c6f63; + --teal-2: #2a9a86; + --amber: #c47b17; + --rose: #b44a4a; + --good: #2f7d4a; + --shadow: 0 18px 40px rgba(18, 35, 61, 0.08); + --font: "Avenir Next", "Segoe UI", "PingFang SC", "Hiragino Sans GB", sans-serif; +} +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font); + color: var(--ink); + background: + radial-gradient(1200px 500px at -10% -20%, #e7f3ee 0%, transparent 50%), + radial-gradient(900px 400px at 110% 0%, #f8e9d2 0%, transparent 46%), + var(--paper); + min-height: 100vh; +} +a { color: var(--teal); } +.topbar { + display: flex; align-items: center; justify-content: space-between; + padding: 16px 28px; + border-bottom: 1px solid var(--line); + background: rgba(255,253,248,0.86); + backdrop-filter: blur(10px); + position: sticky; top: 0; z-index: 10; +} +.brand { display: flex; gap: 12px; align-items: center; text-decoration: none; color: var(--ink); } +.mark { + width: 36px; height: 36px; border-radius: 50%; + border: 2px solid var(--ink); + display: grid; place-items: center; + font-size: 11px; letter-spacing: 0.08em; +} +.brand b { display: block; font-size: 16px; } +.brand span { display: block; font-size: 12px; color: var(--ink-soft); font-weight: 400; } +.topbar form { margin: 0; } +.ghost, .btn, .primary { + border: 1px solid var(--ink); background: transparent; color: var(--ink); + padding: 8px 14px; border-radius: 999px; cursor: pointer; font: inherit; + text-decoration: none; display: inline-flex; align-items: center; gap: 8px; +} +.primary { background: var(--ink); color: var(--paper); } +.btn.teal { background: var(--teal); border-color: var(--teal); color: white; } +.wrap { width: min(1180px, calc(100% - 40px)); margin: 28px auto 64px; } +.hero, .card, .login-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 22px; + box-shadow: var(--shadow); +} +.hero { padding: 28px; display: grid; grid-template-columns: 1.4fr 1fr; gap: 24px; } +.hero h1 { margin: 0 0 8px; font-size: 30px; letter-spacing: 0.02em; } +.hero p { margin: 0; color: var(--ink-soft); line-height: 1.7; } +.drop { + border: 1.5px dashed var(--teal); + border-radius: 18px; padding: 22px; background: #f4fbf8; +} +.drop h3 { margin: 0 0 8px; } +.muted { color: var(--ink-soft); font-size: 13px; line-height: 1.6; } +.list { margin-top: 22px; display: grid; gap: 10px; } +.item { + background: var(--card); border: 1px solid var(--line); border-radius: 14px; + padding: 14px 16px; display: flex; justify-content: space-between; align-items: center; +} +.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; } +.login-card { width: min(440px, 100%); padding: 32px; } +.login-card h1 { margin: 8px 0 4px; } +label { display: block; font-size: 13px; margin: 14px 0 6px; color: var(--ink-soft); } +input[type=text], input[type=password], input[type=number], select { + width: 100%; padding: 10px 12px; border-radius: 10px; + border: 1px solid var(--line); font: inherit; background: white; +} +.err { color: var(--rose); font-size: 13px; margin-top: 8px; } +.layout { display: grid; grid-template-columns: 250px 1fr; gap: 22px; } +.rail { + background: var(--ink); color: #e8e2d6; border-radius: 22px; padding: 20px 16px; + height: fit-content; position: sticky; top: 88px; +} +.rail h2 { font-size: 14px; margin: 0 0 16px; font-weight: 600; } +.step { + display: grid; grid-template-columns: 28px 1fr; gap: 10px; padding: 10px 8px; + border-radius: 12px; color: #c9c1b3; text-decoration: none; +} +.step b { display: block; color: inherit; font-size: 14px; } +.step span { display: block; font-size: 12px; opacity: 0.8; } +.dot { + width: 28px; height: 28px; border-radius: 50%; border: 1px solid #7d8796; + display: grid; place-items: center; font-size: 12px; +} +.step.on { background: rgba(255,255,255,0.06); color: white; } +.step.on .dot { background: var(--teal-2); border-color: var(--teal-2); color: white; } +.panel { padding: 24px 26px 30px; } +.panel h1 { margin: 0 0 6px; font-size: 26px; } +.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 18px 0; } +.kpi { border: 1px solid var(--line); border-radius: 14px; padding: 12px 14px; background: #fbfaf6; } +.kpi em { display: block; font-style: normal; font-size: 12px; color: var(--ink-soft); } +.kpi strong { font-size: 22px; } +.table { width: 100%; border-collapse: collapse; font-size: 14px; } +.table th { text-align: left; font-size: 12px; color: var(--ink-soft); font-weight: 600; padding: 8px 6px; border-bottom: 1px solid var(--line); } +.table td { padding: 9px 6px; border-bottom: 1px solid #eee6d8; } +.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; } +.chip { background: #efe8da; border-radius: 999px; padding: 4px 10px; font-size: 12px; } +.warn { background: #fff4e6; border: 1px solid #f1d2a3; color: #8a5a12; padding: 10px 12px; border-radius: 12px; margin: 8px 0; font-size: 13px; } +.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.check { display: flex; gap: 8px; align-items: flex-start; margin: 8px 0; font-size: 14px; } +.check input { margin-top: 3px; } +.actions { display: flex; gap: 10px; margin-top: 22px; } +.explain { + background: linear-gradient(180deg, #f7fbf9, #fff); + border: 1px solid #cfe3dc; border-radius: 16px; padding: 16px 18px; line-height: 1.7; +} +.route { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 4px; } +.route i { + font-style: normal; background: var(--ink); color: var(--paper); + border-radius: 999px; padding: 6px 10px; font-size: 12px; +} +.route i.alt { background: var(--teal); } +.bar-wrap { height: 10px; background: #efe8da; border-radius: 99px; overflow: hidden; } +.bar { height: 100%; background: var(--teal); } +.delta-up { color: var(--good); } +.delta-down { color: var(--rose); } +.tag { font-size: 11px; padding: 2px 7px; border-radius: 99px; background: #efe8da; } +.tag.mid { background: #f8e3bf; color: #8a5a12; } +.tag.high { background: #d8f0e5; color: #1c6f63; } +.drawer-bg { + position: fixed; inset: 0; background: rgba(18,35,61,0.35); display: none; + align-items: stretch; justify-content: flex-end; z-index: 20; +} +.drawer-bg.show { display: flex; } +.drawer { + width: min(560px, 100%); background: var(--card); padding: 22px; + overflow: auto; box-shadow: -20px 0 40px rgba(18,35,61,0.15); +} +.spark { width: 100%; height: 220px; } +.tiny { font-size: 12px; color: var(--ink-soft); } +.clickable { cursor: pointer; } +.clickable:hover { background: #f7f3ea; } +@media (max-width: 900px) { + .hero, .layout, .grid-2, .kpis { grid-template-columns: 1fr; } + .rail { position: static; } +} diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 100644 index 0000000..a8cd9cf --- /dev/null +++ b/app/static/js/app.js @@ -0,0 +1,112 @@ +function closeDrawer() { + const el = document.getElementById("drawer"); + if (el) el.classList.remove("show"); +} + +function sparkline(svg, exams, zs, xs, lastRank) { + const w = 520, h = 220, pad = 28; + const vals = []; + zs.forEach((z, i) => { if (z !== null && z !== undefined) vals.push(z); }); + xs.forEach((z) => { if (z !== null && z !== undefined) vals.push(z); }); + if (!vals.length) return; + const min = Math.min(...vals, -0.5) - 0.3; + const max = Math.max(...vals, 0.5) + 0.3; + const xAt = (i) => pad + (i * (w - pad * 2)) / Math.max(exams.length - 1, 1); + const yAt = (v) => pad + (max - v) * (h - pad * 2) / (max - min); + const pts = (arr) => arr.map((v, i) => v == null ? null : `${xAt(i)},${yAt(v)}`).filter(Boolean).join(" "); + let raw = ""; + zs.forEach((v, i) => { + if (v == null) return; + raw += ``; + }); + const pathXs = xs.map((v, i) => (v == null ? null : [xAt(i), yAt(v)])).filter(Boolean); + let d = ""; + pathXs.forEach((p, i) => { d += (i === 0 ? `M${p[0]} ${p[1]}` : ` L${p[0]} ${p[1]}`); }); + const labels = exams.map((e, i) => { + const short = String(e).replace("-总分", "").replace("高", ""); + return `${short}`; + }).join(""); + svg.setAttribute("viewBox", `0 0 ${w} ${h}`); + svg.innerHTML = ` + + + ${raw} + ${labels} + 标准分轨迹 · 橙点考试,绿线滤波估计 + `; +} + +async function openStudent(aid, sid) { + const res = await fetch(`/analysis/${aid}/api/student/${encodeURIComponent(sid)}`); + if (!res.ok) return; + const s = await res.json(); + const box = document.getElementById("drawer"); + const body = document.getElementById("drawer-body"); + const probs = Object.entries(s.probs || {}).map(([k, v]) => `${k}: ${(v * 100).toFixed(1)}%`).join(" "); + body.innerHTML = ` +
+
+

${s.name} ${s.sid}

+
${s.class_name}班 · ${s.group} · ${s.status || "在籍"} · ${s.subjects || ""}
+
+ +
+

+ 绿线是卡尔曼估计的稳定水平。如果橙点突然跳开绿线,系统会当成发挥波动,不会立刻改判断。 + 达线概率 ${probs || "—"}。 +

+ + + + +
考试分数方向内名次标准分滤波
+ `; + const rows = document.getElementById("spark-rows"); + (s.z || []).forEach((z, i) => { + const exam = (window.HB_EXAMS || [])[i] || `第${i + 1}次`; + const score = s.scores ? s.scores[exam] : ""; + const rank = (s.ranks || [])[i]; + const kf = (s.kf_xs || [])[i]; + rows.insertAdjacentHTML("beforeend", ` + ${exam}${score ?? "缺考"}${rank ?? "—"} + ${z == null ? "—" : z.toFixed(2)} + ${kf == null ? "—" : kf.toFixed(2)} + `); + }); + sparkline(document.getElementById("spark"), window.HB_EXAMS || [], s.z || [], s.kf_xs || []); + box.classList.add("show"); +} + +async function openClass(aid, group, target, className) { + const url = `/analysis/${aid}/api/class?group=${encodeURIComponent(group)}&target=${encodeURIComponent(target)}&class_name=${encodeURIComponent(className)}`; + const res = await fetch(url); + if (!res.ok) return; + const data = await res.json(); + const box = document.getElementById("drawer"); + const body = document.getElementById("drawer-body"); + const rows = data.students.map((s) => { + const p = ((s.probs || {})[target] || 0) * 100; + const tag = p >= 80 ? "high" : p >= 20 ? "mid" : ""; + return ` + ${s.name}${s.status || ""} + ${(s.ranks || []).slice(-1)[0] ?? "—"} + ${p.toFixed(1)}% + `; + }).join(""); + body.innerHTML = ` +
+

${className}班学生概率

+ +
+

点击学生可看成绩轨迹。共 ${data.n} 人,下列按达线概率排序。

+ + + ${rows} +
姓名学籍最近名次达线概率
+ `; + box.classList.add("show"); +} + +document.addEventListener("click", (e) => { + if (e.target.id === "drawer") closeDrawer(); +}); diff --git a/app/templates/analysis.html b/app/templates/analysis.html new file mode 100644 index 0000000..47efd2b --- /dev/null +++ b/app/templates/analysis.html @@ -0,0 +1,263 @@ + + + + + + {{ item.title }} · 航标 + + + +
+ +
航
+
航标{{ item.title }}
+
+
+ {% if result %} + 导出 Excel + {% endif %} +
+
+
+
+ + +
+ {% if step == 1 %} +

第一步:成绩表体检

+

先确认系统读到的是你想算的那批人。分数量纲可以不同,系统稍后会在每次考试内部标准化。

+
+
识别学生{{ summary.n }}
+
班级{{ summary.n_class }}
+
考试次数{{ summary.exams|length }}
+
艺体 / 请假类{{ summary.art }} / {{ summary.leave }}
+
+ {% for w in summary.warnings %} +
{{ w }}
+ {% endfor %} +
+
+

工作表

+ + + + {% for sh in summary.sheets %} + + {% endfor %} + +
表名类型行数
{{ sh.name }}{{ sh.kind }}{{ sh.rows }}
+

方向 / 学籍

+
+ {% for k,v in summary.tracks.items() %}{{ k or "未分方向" }} {{ v }}{% endfor %} +
+
+ {% for k,v in summary.status.items() %}{{ k }} {{ v }}{% endfor %} +
+
+
+

考试识别与分数尺度

+ + + + {% for e in summary.score_scale %} + + + + + {% endfor %} + +
考试有效人数最低最高平均
{{ e.exam }}{{ e.n }}{{ e.min }}{{ e.max }}{{ e.mean }}
+

平均分从高一到高三明显下降是正常的(试卷结构不同),所以不能直接比原始分,只能比同次考试里的相对位置。

+
+
+

各班人数

+
+ {% for k,v in summary.classes.items() %}{{ k }}班 {{ v }}人{% endfor %} +
+ + {% endif %} + + {% if step == 2 %} +

第二步:口径与目标线

+

下达指标前先把“算谁、瞄哪条线”钉死。默认排除艺体和长期请假,明远生可参与年级排名。

+
+

用于滤波的考试

+

建议全选。系统会自动让近考权重大、早考权重小;不是把五次考试简单平均。

+ {% for e in summary.exams %} + + {% endfor %} +
+
+

纳入测算

+ + + + + {% if preview_n is not none %}

按当前口径约 {{ preview_n }} 人进入测算。

{% endif %} +
+
+

预估多远

+ + +

步长越远,不确定性越大,临界生会变多,下达人数仍按期望取整到目标总量。

+
+
+

各方向目标人数

+

这是年级总盘子。班级指标由个人达线概率加总后再取整,合计等于这里的人数。

+ {% for g in settings.groups %} +
+ {{ g }} + {% for t in settings.targets[g] %} +
+ +
+
+
+ {% endfor %} +
+ {% endfor %} +
+ + +
+
+ {% endif %} + + {% if step == 3 %} +

第三步:滤波在做什么

+ {% if not result %} +

还没有测算结果。请回到第二步点击「开始滤波测算」。

+ 去设置口径 + {% else %} +
+ 原始分同次标准化卡尔曼估计水平模拟下一次排名个人概率班级加总取整 +
+

+ 把每次考试看成一次带噪声的定位:发挥好坏是测量噪声 R,真实进步是过程噪声 Q。 + R 相对大时,系统不会只信最近一次;Q 相对大时,会更快跟上近期趋势。这就是导航里“既看 GPS,也看车自己开的方向”。 +

+ {% for g in result.groups %} +

{{ g.group }} · 模型参数

+
+
过程噪声 Q(真实水平会变){{ g.model.q }}
+
测量噪声 R(发挥波动){{ g.model.r }}
+
一步预测 RMSE{{ g.model.rmse }}
+
测算人数 / 模拟次数{{ g.n }} / {{ g.model.n_sim }}
+
+ {% if g.holdout %} +

留出最近一次考试做对照:用更早的成绩预估最近一次,看滤波是否比“只看上一场名次”更准。

+ + + + {% for h in g.holdout %} + + + + + + + {% endfor %} + +
目标K滤波命中只看最近一次
{{ h.target_name }}{{ h.k }}{{ h.kf_hit }} / {{ h.k }}({{ '%.0f'|format(h.kf_precision * 100) }}%){{ h.last_hit }} / {{ h.k }}({{ '%.0f'|format(h.last_precision * 100) }}%)
+ {% endif %} + {% endfor %} + + {% endif %} + {% endif %} + + {% if step == 4 %} +

第四步:下达指标

+ {% if not result %} +

还没有测算结果。请先完成第二步测算。

+ {% else %} +

+ 「下达指标」是滤波期望取整后的人数,含义是:若学生按现有轨迹走,这个班本来应当到线的人数。 + 班会目标是超越它,而不是刚好打平。保底 / 冲刺是模拟分布的大约 20% / 80% 分位,用来看把握而不是另下两套考核。 +

+ {% for g in result.groups %} + {% for t in g.targets %} +

{{ g.group }} · {{ t.name }}(年级 {{ t.k }} 人)

+

期望合计 {{ t.sum_expected }},临界生(概率 20%–80%){{ t.critical_n }} 人。点击班级看学生概率,点击学生看轨迹。

+ + + + + + + + + {% for row in t.classes %} + + + + + + + + + + + + + + {% endfor %} + +
班级测算人数最近在线滤波期望下达指标保底冲刺相对最近
{{ row.class_name }}班{{ row.n }}{{ row.current }}{{ '%.2f'|format(row.expected) }}{{ row.assigned }}{{ row.floor }}{{ row.stretch }} + {% if row.delta > 0 %}+{{ row.delta }}{% else %}{{ row.delta }}{% endif %} +
{{ class_comment(row, t.name) }}
+ {% if t.critical %} +

临界生(优先关注)

+ + + + {% for s in t.critical[:15] %} + + + + + + {% endfor %} + +
姓名班最近名次达线概率
{{ s.name }}{{ s.class_name }}{{ s.last_rank or "—" }}{{ '%.1f'|format(s.p * 100) }}%
+ {% endif %} + {% endfor %} + {% endfor %} + + {% endif %} + {% endif %} +
+
+ +
+
+
+ + + + diff --git a/app/templates/home.html b/app/templates/home.html new file mode 100644 index 0000000..53b16af --- /dev/null +++ b/app/templates/home.html @@ -0,0 +1,55 @@ + + + + + + 航标 · 班级达线指标预估 + + + +
+ +
航
+
航标班级达线指标预估系统
+
+
+
+
+
+
+

把历史成绩当成导航轨迹,
而不是一张静态排名表。

+

+ 上传各次大考总分后,系统会把分数换成同一次考试内的标准分,用卡尔曼滤波估计每个学生的稳定水平, + 再模拟下一次全年级排名,把达线概率加总到班级。下达的是“按现有轨迹本来会到的人数”,班级工作是超越它。 +

+
+
+

上传成绩表

+

xlsx,需含学号/姓名/班级,以及多次考试总分列。若有「方向」或物理/历史分表,会分开测算。

+ +
+ +
+
+
+ {% if has_sample %} +
+ +
+ {% endif %} +
+ {% for it in items %} + +
+ {{ it.title }} +
{{ it.filename }} · {{ it.created_at }}
+
+ 继续第 {{ it.step or 1 }} 步 +
+ {% else %} +
还没有分析记录。上传成绩表后会在这里列出。
+ {% endfor %} +
+
+ + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..126b0f5 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,25 @@ + + + + + + 登录 · 航标 + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ff6ff7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.115 +uvicorn>=0.32 +python-multipart>=0.0.9 +jinja2>=3.1 +itsdangerous>=2.2.0 +openpyxl>=3.1 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..63c9567 --- /dev/null +++ b/run.sh @@ -0,0 +1,8 @@ +#!/bin/zsh +cd "$(dirname "$0")" +if [ ! -x .venv/bin/python ]; then + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt +fi +export PORT="${PORT:-8765}" +exec .venv/bin/python -m app diff --git a/高三指标划定操作指引.docx b/高三指标划定操作指引.docx new file mode 100644 index 0000000000000000000000000000000000000000..c9e4126a8a54986c93763614d73e09578f09590a GIT binary patch literal 20728 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 literal 0 HcmV?d00001