From 10cff870489ca5db3a33ea5d6c46eca3c1ce02c9 Mon Sep 17 00:00:00 2001 From: Yizhuang Zhou <62599194+zhouyizhuang-megvii@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:00:42 +0800 Subject: [PATCH] feat(gan): add GAN codebase (#31) --- official/assets/dcgan.png | Bin 0 -> 164680 bytes official/vision/gan/README.md | 57 +++ .../vision/gan/megengine_mimicry/__init__.py | 16 + .../megengine_mimicry/datasets/__init__.py | 1 + .../megengine_mimicry/datasets/data_utils.py | 77 ++++ .../datasets/image_loader.py | 100 ++++++ .../gan/megengine_mimicry/metrics/__init__.py | 20 ++ .../megengine_mimicry/metrics/compute_fid.py | 237 +++++++++++++ .../megengine_mimicry/metrics/compute_is.py | 89 +++++ .../megengine_mimicry/metrics/compute_kid.py | 241 +++++++++++++ .../metrics/compute_metrics.py | 191 ++++++++++ .../megengine_mimicry/metrics/fid/__init__.py | 1 + .../metrics/fid/fid_utils.py | 104 ++++++ .../metrics/inception_model/__init__.py | 1 + .../inception_model/inception_utils.py | 159 +++++++++ .../metrics/inception_score/__init__.py | 1 + .../inception_score/inception_score_utils.py | 118 +++++++ .../megengine_mimicry/metrics/kid/__init__.py | 1 + .../metrics/kid/kid_utils.py | 153 ++++++++ .../gan/megengine_mimicry/metrics/utils.py | 46 +++ .../gan/megengine_mimicry/nets/__init__.py | 0 .../gan/megengine_mimicry/nets/basemodel.py | 164 +++++++++ .../gan/megengine_mimicry/nets/blocks.py | 243 +++++++++++++ .../megengine_mimicry/nets/dcgan/__init__.py | 0 .../nets/dcgan/dcgan_base.py | 46 +++ .../nets/dcgan/dcgan_cifar.py | 122 +++++++ .../vision/gan/megengine_mimicry/nets/gan.py | 178 ++++++++++ .../gan/megengine_mimicry/nets/losses.py | 114 ++++++ .../megengine_mimicry/nets/wgan/__init__.py | 0 .../megengine_mimicry/nets/wgan/wgan_base.py | 94 +++++ .../megengine_mimicry/nets/wgan/wgan_cifar.py | 123 +++++++ .../megengine_mimicry/training/__init__.py | 16 + .../gan/megengine_mimicry/training/logger.py | 216 ++++++++++++ .../megengine_mimicry/training/metric_log.py | 85 +++++ .../megengine_mimicry/training/scheduler.py | 127 +++++++ .../gan/megengine_mimicry/training/trainer.py | 330 ++++++++++++++++++ .../gan/megengine_mimicry/utils/__init__.py | 16 + .../gan/megengine_mimicry/utils/common.py | 51 +++ .../vision/gan/megengine_mimicry/utils/vis.py | 59 ++++ official/vision/gan/requirements.txt | 2 + official/vision/gan/train_dcgan.py | 85 +++++ official/vision/gan/train_wgan.py | 85 +++++ 42 files changed, 3769 insertions(+) create mode 100644 official/assets/dcgan.png create mode 100644 official/vision/gan/README.md create mode 100644 official/vision/gan/megengine_mimicry/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/datasets/__init__.py create mode 100755 official/vision/gan/megengine_mimicry/datasets/data_utils.py create mode 100644 official/vision/gan/megengine_mimicry/datasets/image_loader.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/__init__.py create mode 100755 official/vision/gan/megengine_mimicry/metrics/compute_fid.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/compute_is.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/compute_kid.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/compute_metrics.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/fid/__init__.py create mode 100755 official/vision/gan/megengine_mimicry/metrics/fid/fid_utils.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/inception_model/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/inception_model/inception_utils.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/inception_score/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/inception_score/inception_score_utils.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/kid/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/kid/kid_utils.py create mode 100644 official/vision/gan/megengine_mimicry/metrics/utils.py create mode 100644 official/vision/gan/megengine_mimicry/nets/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/nets/basemodel.py create mode 100644 official/vision/gan/megengine_mimicry/nets/blocks.py create mode 100644 official/vision/gan/megengine_mimicry/nets/dcgan/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_base.py create mode 100644 official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_cifar.py create mode 100644 official/vision/gan/megengine_mimicry/nets/gan.py create mode 100644 official/vision/gan/megengine_mimicry/nets/losses.py create mode 100644 official/vision/gan/megengine_mimicry/nets/wgan/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/nets/wgan/wgan_base.py create mode 100644 official/vision/gan/megengine_mimicry/nets/wgan/wgan_cifar.py create mode 100644 official/vision/gan/megengine_mimicry/training/__init__.py create mode 100644 official/vision/gan/megengine_mimicry/training/logger.py create mode 100644 official/vision/gan/megengine_mimicry/training/metric_log.py create mode 100644 official/vision/gan/megengine_mimicry/training/scheduler.py create mode 100644 official/vision/gan/megengine_mimicry/training/trainer.py create mode 100644 official/vision/gan/megengine_mimicry/utils/__init__.py create mode 100755 official/vision/gan/megengine_mimicry/utils/common.py create mode 100644 official/vision/gan/megengine_mimicry/utils/vis.py create mode 100644 official/vision/gan/requirements.txt create mode 100644 official/vision/gan/train_dcgan.py create mode 100644 official/vision/gan/train_wgan.py diff --git a/official/assets/dcgan.png b/official/assets/dcgan.png new file mode 100644 index 0000000000000000000000000000000000000000..65406770bbf324c72dcd1e85ce4606a360d67872 GIT binary patch literal 164680 zcmV)>K!d-DP);+_+`WuB==)dEW1*O0*{?HbUF5Ra3gMzBoI7;=pj&-?Vwt#?8AA-}iIt?B=s} zY`^gG!RccwM^21<5l*-FKK%Ii?%IERdGYegHeYc5j;EeFw(sB^jBJIqfBfO&0J`fL z!xg2Gn%ebhuRr|VcfP7t2`_)?6=PA=c|SJ2Q^?B3iA`G9Q>(Ub*-)!!DF_VD>=~Zf z0g|8ufsq)yur}Dc@6qL@eNg7TZ@U)2?O*yzAidJcc~|5mdlEu0s3^)TOPp~+i9o9` z&`PO1H_WV*u+G850|W$-R#F+mS)R9=4H76ztF#u(&inUW^Ctkl_T6tmSSSS(M7k>B zPtTp0n_t;e1H$Zy)PdHdEGL1dk0h7f`X-ZSVKQjievo{jURv(9-bMUofu zOY1-V#s2sI&({EW-ABI1f`!zQz)Vgkl4y#oE%MUIHKRvP6<%5|9ZTZ~o&_A30Z8RT zN&|1bs-#YP4+!hQkc3zZ6)5L_eC=fbzH`GhtybJEvxSwUKy|9U%amqVWWF3JmlS4Y zn5~8(rD?KdWTg{@?HYpd!J1qt>?3o_Dnh_w{lwZ6PaH3G z^}a{{3BddA=`pj41cU&f1PLN0V*DQ@0SQPz;28vf0R}$<%nSxG6Eopy4A1QOX}R;x z`A@!d0s!GzSKjgqA%qkV&Ity?<)$bdSti5IF?-Ko=a{_*Kwtua*)x03493&=X#f!c z3}*ITfR#iPMEm~yY5@0q>x&!rY^}ALN=4TAm6NmSAg#7y=Tcw#xEcw`B$QA=5XM4i zZ^|rN3FAsw9qXPtF2hP~e5Q2f@BZf74<3AU<4pL**IeDK)yrZSQFFAET=9-;0i0eM zNZ6&-wT0#NmtKA$5pMd}2kyD+C+9wAH@hs+upfla*9bP zL4rU47~XSgyjDU9D z2G3#LCQXxx z<_4+jfBeyJ0yuhV$z<8={9?0HKmV-VQi$)}^_?%@aYwAxxfh%zbmOcGpMQG(_`N^< z@he~Tj;mkt@@gDg&ngJCl#0lC>m7q65lUtP0wH?+{@p+Q-pRE`8#VdIZ~G*G>uq>$1$r*+_&2_yu0=UI|> zCJMs$zxzD^-u<4NOPk49TP{f0EL|(Ilss(78)h~*YpPLwwU?ZA)@Ko08;o}C z+)}Al27?h1nX+8%u0Q9(b4ZBQ^|d2M4-QA`DilGi`or$r(SH={sge#15{^OV23D z2^ru2(N_Za!xx=ri(5o*rLZ9dFbxJ@YwHWBP{=-g~m!ADYY<6NhSidZtpV{p~;fJAjYwPnlUu z@&aIJ5CaZihG#rYFaaQdKpr4~7*Atn0L%chU=|F33Cv*j>>WIy1U`6a0|4P!FSzOF zib#XO46u~m2`)-$9kZ~GykKr~-qL2|X(u*&xhZZnf9+kL2Jn-IpGte9qqD~@ICsy^%`?vA zk3DqHUHAT=QHx)C)$3HyXm!RPdGe9|sCUJ4Ujk5;4B|jlDoqhYaTxZ~Gzcqk5CH4| zFgkJIoqOo@bCz3nCd*WY?Gc~V*vNGq+B^xj!(g_O=afY`Bf4(vU7cqU?J ztufX(TdP*J4!~eAhyWReK^(=#7|-6AQcL;4_kRGu2d=#{PptzJc}ON{N}oG#b)b|M z)@4#!&88C9nvL-+&53x!#u=reB+2u#<%!(lg!ghOh|+&CYg@tNB` z2jG$q{<9>hBuRoGKq^ZVrR(QU6%nGNWqa9Z@T{C+cptZ!iA;m!0b{*iC=%oE2tiS_ujj6)>|ev6iI)W^a~#ZarpOl z{S?3*hcm}S1TO$0&G3K$!0-%aJdKzHp5Xy7i17?&2ABb6$t)NICV-heGkJKXK=FId zuK^%D>$x}mLJ3j??+NUbG@g7>D)wZ_SdV6eb@gxxian->|r5P>=D`DJP zTR4%9`juK+L8ZNQr9%}~R1oyn*HLS;(90_e-R1dKv$p%}vyL2mxE$mgw(oxNTiy@g z?#G|ho=?xst<0Sm4Ohx^O%r>mmB`OL>k<*PRUl8EI#6$oD_QAwSKIYqsyS&KNCvG= zC(X;LshJ=MWk8K;H4Z}p*!%k*x&OeC2aRyoyzN>5H{AANIFecc6zU+7%>qV0mx)u&u+dxZwmY5iPBZm+V)ATIOg1K? zn5wPLdbfYWUw#L`S5M}iNic*YL68i`uuKFd1_R9SOkjY)U}k^;27`&g0DB0}V8;K& zp240yQ54`0&#nLqu7%tSAe{wdY*Q$Tn1{5J0&aO9l(~Y z_ZHp>ZxuPA1V}oF!hx-eJZrX55d8ek*8*6)|6lV#PgLsbOS5668dR#o^<@$uBIijc z1$IGDg+M+SI1e3$S_ZI8Y#l^tvz3LFu+e$^iO18SnP{|5FCJf5TH3RHYj2p2jkm73 z{o4S3wD$pJ%+b?F4($Dzbm``q9jE5!XQn38bZN(q3%b25_XA%P6C1YFE6pg5vvfEb z_Up~5w8$Z7ZKd06PBfe2q4MXRd(PCFmn_}je3Iw3CJWJI%Anx27*}5o;sCg$+m6VDwT?7&)!?>NGKgB zLBbg8*()J$yzYYlKL53U0lU%}n%zDEP&_T^j$$x5`%UNq_(dJWT^+myU0!u0*W~9c~;LxoSk7dt)64VD`+`J9ymxsjC6JQfQX__6KUo!`GI_STf9FwS@vg<+t>N*J(bV@l&qn&pGusMqi1MH0vH=1p7TC?o<% zfXO+h0>zF?V;E4RZu#)_0RI2K{?AS%t)%YC{L=a`b=o)=5L8sj$&nxwGR>%Gv}Fb? z6TB2G*!zswNfP`tIA$lj&=e3r-ovCO&x9zx`jw9YxZ<||21`Lq@B{%7#t;p?xTn3= z@v;+FpBz;8jlzOOKp?PSc$T43$?<(t&8}=r%W!jw>JZvFGS*seVXgPRFwPWi@9nPv z@P}8PHzUo4U1QGEVlT7N_K8+0g5mMy(ZaCXh(cw#b&#*Q>HKG1l*| z=cQa5+5Vt7WA|jU7TDaUxj8zwp5*q-?YqKyTnyGf_V?cd@LvlfA{9uIf-I=;RC+iD z0}Ob;GnfI-1%U)&W;hg%QqPi5C$LAtFmN?Mfi#f=l_gTo^@!iLBLYBp*7H8OPkM%D z=h#0@T7ZQo0V1&XWW5NjgD#CEX-RlyPtLQ4_b}|)GaSePp5Yjd!Is&=G9-!Fuf7<- z;NXwTWQA3&zjQj!tg2Kzyp9?|$TAy%Ii#}MnJALJXJPUIsd`xyQLQ6GHCmk0K_iMQ z`JgM6jmI{Th*Y(f^wvzW9)tmu+WP!A0J!dRH!3mKu4hM{c+{Kpf^(kpz(bFWw>Iq9 zF++AG^PS;JqV2NfIoK^PUg}kt7mep+uS{iwn!$ zm9=D$r)e@iHMaeXZGuP%DI_tI)*-Va62@BZ**hCZ{n1-L0^qxU|6k)(F)Hk_Q^%M4 zJwSSwYe9hyz3_n&kqDXM)Tksko;^c2sYpwb)>@xg%AL#0OfafC77(Q^oU?(1FAD*C z$L%)*xa#BIbB+L(PvPgrX)Nv!1I*v36l`uik+jds~5T5nik3A+lGr>E48iXVA!jm8fX77aaQh0{4 z#7;U8!!vn?13MUa&rA;F00%h0d1lAX!FU!VPk-r!0FskGx7nJf`qI(;><~5EX*Tje zt=^JKv-g^OwKGXl6{GcJZP^(wf{20&JG*k~mw}dy<}=bJmh%u+S`|D4i;; zvCarm*1}kCijlP@(7LqFmi{xJ`51t!KlwdpJeUPJ^5i)(j;A~3<>$ziC-zFxn?Iwc%dso^U9$eGhPUnmveS2{6D+X!m<-EL0Fxt3q`; zO%aAFsx>?9B+0S@_4tHjf1j7^@=8x5EG%Y5QcHXKId z(>n@}Lq`sl>F|O*(^|^8`N5N?7uNGKCbfI4HOO;jR|{07*(|&oCT?MUbh_IgZ&urJ z{jLN10bF$bTi55PbJln=lHy9IyVRpv6vnC{5R67jPXEgdZv;Si*2N!rND#3g00a=R z025d;vlj}g&TNVzRayed8gkCEa?H6etyIhq)-tmtSSAZtFf(};9!8KldFK@XdQbh3 zxm0nxEHYaZBsB=^LCirENgWa!Z*zuV?@X2;tmGvhe{5fEyge~JS*up03c1Wl1-67U zIdkD{0kBkxgcv{nwE*7nt~V(kY~P~?r{}X|Ff~0bRVNOoSB9tN=8n2lnuO+NSEFWU zY%FfH>ubX`8m*~8l3FuhG&Z!Sc5FXq*CkWck#aUIG+C=_@e{MF)u}VaXEy%+>wX=; zbsxUb8Y2{?w)BQNjha@Hh`cka-PMH?bIICTS>!qj9oQNxgh)ndHLmJPbkXIP=^)CB zoV^!B1OhV%N|07k>i~@FKXe0tkALvPp)5RjeOLxjUC5wtG~#G2L9%i(sQ7SdDo&G4 z^{}vs^&uimg<(5%B^9|BB%2^k@^^i!?)c6;OZN`5)QhdLKugY zYQ?j(MhH7RYtPtVX?8+GlR7pxEWL;#1(T6*?5qUV(kyH=HnyA1Fg`U14`%huDP}lQ zju|YR*1r^`H5P)ANL7=f(tT)49Ior-E#qo~!+gChsxjVk_m9JGV*}nCX zvxh7BV@Ho~+q`jkWj3zV*M@~-W-|a)=4RLRWK01?xtKAbLj8YT^Ev>8XI*s5140m! z06{Q_K9+w8IMv!5LyUHqQV74wrVOFRr9i7XV{q%C; z5R!`A<%uh~G~AdTQ*|wb6gmLB046Se1At$D=ga*FLBmxg*Mg+kX%S6zSLpc3Q=|2s z8TJ>Jdi~sP-#s%qS>3i}XEfgEt*>x@)LrdXYL(5KH|^PTzLzmkDp=*2N1k#%*mvTv zscgUWs+a%KYhDN7y*GaZ-j&Jf((%J3s#aCS3Xm_$SXSUum009U=S}CoS5CYzR=(-yLeEdTn(GqECbL*v)%wQG! z)~0=jPp7K~>$PCxwrRcE-!NWVTwm;Uv&oH{CZcHjlsZ0lawKAwV;;`%<%A9 zN+Adg3Hs2%y0pMK!} z`)Xk@KGjNl-DXv+r25gL-BE69V;id*cIvPL6&fbT0ISj}=fy8R|2hEQ_|1#AY}^sL z;-Buhe{#&9eW_|~M^6Qh&-q90>kk&~V3|jKKOF^UZrxMVld z>xk?y&2<=`bLLJRhbzk~{a#WHXr(*+#)F3eeCeB?St?>Zy>VuG)0xwo2ASzD_0|?v z9zJwp>Vln%6&0TzzLr`$FWz>`Enm6pC6}5V6Yajtmbn8|4{NO(-u*5BglAoF^8-=$h=edg?AHDFTs~$zee3(j zj-EWs@#b9BXpmiYPV1*)Ms`RocPz)&Q8Y zbhZ$Rqh>Q`j>#Ysty zpR>I#)vn3OM!Qz1$dNmB{Af&OYGT@kRcAcsa2XGKgY|T+g4=rZEeF?TZP(q);h}bVwC1_6fr|U6wY7xf=f4T z-JWE*cNWY9FazETB?F~MfPkAnd@F!kKKOBEeeN?6Xy<)abyl6-y}QxtE=^R- z#Hh!zJUDl}V%2nO+*=bjTkHKCQJnkG3phc{0xYd{f`k;pGYb-uaQmNs2Ed!%@%D;w zo$=24dan|z_4QsDg)FpTf)M0AJRGwWR1f0SZok!uR2Z!HlSm|9>POcaL)ED^YvU8s zLD2G~9SiT_y)aIB&kx-3S^z(|`Awa$@?YQmNxaEFvbc2aPPP5q&L_XRytL%vNN(F4 zz4)qDxoV&IS$}P@%tct~bjI4vMm=6$UsH_0*-jSoKg-((8T~S$Q?c_>Dvn?w@u^4n0c3<-Tw_XK+@T?1NzF!FzLP-Hh zEJ%PQ6G?JDO`4~Far?VpJ-KP>AO7RPFW>q3)S0m}Uf4T*`ZwPCKi>P!D{Qio_R0rO zs9XNw)JQlHC=a&mod+{O&O@I3!ixa(4*l2{1(UZP-g#ySDTzIpLEhU;1yw|iL4UY# z`qYVohmJmZY<_lCMA7u**sEW6+06DWJ|8$!+Oi}lE(>SN#^$rw*>c$R-fn&Ay8zsL z$Gd})lVre2u{NA{+SQ^>kKgm;p<~CJt!Q#{^V}WJnH<{)?G#%!u3r=-mF;?cxHia= z%sb@$Tv*{$$n}70u=z@RTqL<%SgJNQv{t03001BWNklQB)nJ>2Q=tsiGhVgJ^x2opZstXPk3R=3vU) zSwjGMDy@lh;MswomAdUC9|Lgx2mjoag||bg$P*m%Fey?)m6>hNc}`TMD>X{CR=lr9 zKYZY>C`~qv*OaP_iq`zmev+%)#WJqRS^zLHNl2x%b} zcGi^MjndSyc3yf2Fq}2c8t<(nC@rHX)Ujre@yZsXW{qZRFCI>u*_e!5qOhqe3-V0< z8EmX@jvl=8)d2qe+BbJ+kDXr1uKum(KeX^~|MahGo5rG_J!TKj6;e>B@UpWjzxswv zCy%Gg2l{g-()l4fsir#Z#zbeT(W!^Jbf`6&qckabSQ=St)@IrPmjg-B+T!X*|MMXL z?|$!RWV>lmHloU8wb2M;sYRaWl|ThT#Tk!{%)pzYo7zPBd;~CNp)jITNy?%Tg^{OD zEi6X)SSjlZoK18F<$CnMy=LQ%q!)z4WfPzqC@Rq{C!&b@2TkKUC=E!rbDEt~mdBmz+@v1SNe@pSbB8kN?*%*2Gjt z2<6B+=RJeTSuc*?`9c7r*+)#W2BAsCWu6piX3JFRkig#LI*$AO?x817KehkJ?8&*+ zZm%@Xf;){)VY16F-Sz5Udxh45q!cPtQ7ee5-et+^+>!h43If&Myk*bpJ_z83o8IeC zY1ZTA`QyW4Nj7aWoH%_n>CYbz#)8W(zGPE%Vqv+vv_3aV2EFxOuQ$j{*{;co6um)N z7^xa#adSd7>Kn#oBPiw<7mN@qtD@cB+}OP1D>vT;;N921S08_@-96mbrajqNIz8Bb ze86$!wHj|#t&@4FNJT*urD>YwMNyhc9LLq_C^Iu#Hl2O`g-}tJCC(X1!dV|^Wvxll zQ4r`r>Q8+9Qvg2jXSbJmVmVjDK@b#d)4a%wB5Xvh_*7|^Mc^Ac*s$r0y^H;1tvAzB z5ZtLYr=_Z=x%TSOMSp0h!%9sP34j4(iYyrksVYJB>tFvOfVaNlswh+gE(|$GD$5EX zLhpzmh@^lZcEp~*1QtrLU=hX!l8lRVZnnK+rLi?=hBY_j%sRu)vv<6uyXm$nS_Q`RM#COOFaLtm8?snpp(snj73W$5jdGH5(9s_vMkQ>XHO z_~Od8=ic+C*Cx7l%gZJK5T13>ZF@tBK#&#?>_dr-Z9UW0&)rtvT=!cit5F=5Y2T(r z+@27EK+-e3^B!!9{GK2D;|s31`sj+k^~?A4G49)~oJH=51qsP%Rvx|m3IOTSlinuI z_;hW-c|kJBhU-!)t%5Qii7=j>UHR*;edp9{mjw~QvvuA#T9ec7mHc=hh)IR%_06zG>KbAhWqR7);YWq=3Z`-s@`C7j}8-xTK zoZNS`KN=QpBz#Wn3+w8w&W`OnPMlttU&)%C*7hyqWuBg#TRZd2T~X{$%rCQX(-WH- z)6-!2;cGt*;7|YTnqdB5yMJV%7)@;)8>|hMmh)UFqn?pva1@cQgkfk+nH7aGE{bDE zB8n@G@$s!Y&(LwL$WkQUt$s3Ns}swc+lT$g#dZS%~QscBu74XKQ!eW&^dPLGx{X+Y)(Wy4ss)Z2R3JuByK8*7ZkNABOdwXLrD&F9N1k9sTn_pU7*87$0>x@$Hd(apG* zd3Ma8#BoiFY8;Gj8k?Tk*r@1_fBRklxBbocq9D)`jau!glXLA>z0zz5MP90UtDer! z%?PTEcb-bkpe&mthuxKCg@o3|g8&tTPKYQ9FKdMDCzgxH4yKR(qFi0+UUc!HxBpHL z)$6XD06=)w3vPR`qVtAQjaYT6!Ojhp3pdASY-}_t5fQ@|Wieb`?Rlsu2pf%92a4He zc~Tbn@#Fg+diZB&ZQpsxWp6H=I=Gx4TJ=X)@1FG`df6F&^k;u;`*ZB9 zitDy4i($7hvsEYwl0uR2YV3m71GwoU?`?#w_E^39w84^6rWx1r#4XL9oa-K^+zi&? zwXD`usDkl{DH&Chlwba;S1&C-HT%?oBKM0+-6Sj8Q)9c%-1g|>2PdW`_H5nVnra(w z0+N65&Km%{_uBUagXLiP@zM>ou8c-`nVWv;a%SfRlg?cL5s(y0ko9aGNXgQn)~Gip zCZ;#K1WcWN{?QBI)){B&qem zG&|dxJa7D>tCM&`MM1mVyV6?g44j7pvlsV&`Sk$);g5dx*xLHhQT61J)zwo6l;Y=J zQGdCh8P$;d2Zv~l-lB`bgR{RaWu{?9+*k_B<6RfmTXexeaLB8nMzPGb z$pzV7%A7DR%gU0OmG+*MkWxwZh@oL)s-H1c-7y(&pKi6Pk=9ZW0Sxw~wcdk-NVCE@TMhKW>7z%E9-Ln}*&i%* zrpBsKUB#UX&VSK(d%`dsS}*S2w|Mux`%j))vbC8bUw$2c4L|bR}kA zM(r7bH~q!&3*QLfJs)}(8LX`Iw1lpEQ)FC7(=)x{d}}-p;fp+0x+6r;?+v61MHm%| zu6W+Ah5ZK)9+=gk7L`gV1#5MBt~)v1x#FVp$EGK%%_f{lN9iB@!A$@@aN~6ttzmip zxK6rj!z_g-T1yHQ$nL1HUW7rl^fC$*36!3UCkL<)VO(jBO>Eq{yVmTKMV{q}w?+va z#Z`i1_5hna|J-Nq0C2-~x0xavrY0+MX4V4E=h-M73^SXa*dUg+#bsq9;&B-v^&yvy zh*U^ENYXedTS88yw&@MVV^;~>@+ewfRbExVM3QV){MBE43BU{g$Jh>%r zC$zH1p8COGbfSPMj3CEa)rFOAVT1^xpxRo`{V45@)>p#XMBHd7Q*Pb)9JTGmvc8?* z*jwkEw``p^-aE^C|ME=$zW#eJkx_O3tU3DRT(P{cFIh_kUwlUOz1O~M`^>h5C+7a* z8+XqxCztN2owu{L?K$o4!ctsQlT+hT(35-T<`+zFz^ynW@0|=MHZ+XMmF0!Sq>%By z?;kySVhO;lfA`&32TCYNS*uoOnqfnjVKOp8SHhshX_&9eIG&fS2CAy%A!2nD1B}=&n<7t$+C8c`6=1=jCZ-{Fax_03bZ;4PSrg${o>`T3FR;tg0Ka zuEr`36u}XZ5=uxVpP?3(R+2$ljVu4~o&Ovrt5?77CDR+Gl~&eqQW_mppu&}smoj&} zoBaHNqkH#0nY#MiU;PGv*}ebPUp%FBjRXmTDKZg+N@$a=lMJk=e(jF0&&{t_YZWeS zKj~HCfK=FMws-Cvzxq`#t~Dxf9`H%h2T`mmDi9*js?w4waOwKgbKeHwkN@oVAG+_M zxw%E}?DXc=_*5mN*o}C4gV?=+W)Ck5J=U1oS{d73dI4?w-D9T?AFe?bMl~kKIZzMp ze{wi9#@d<98((?(3mW6CR;#gf^X9?&`t1DaKlzi70{GCaH&d3B-2>I+bdi_Y&<+bH zgd7xQW}NXL5)L|!A`lYez0k(kxLT266vVZeEjw$iPLUVhTW?AdDvD!xO9UZ=H}*51 z{v3ec{_S^XX;K!YWk=+N00X&oMd{OGO_hr(?~m1ltTWs&)UGwtAQtsX6sX9%TAoJz zGR}jVvyRIm>6fLC$H%9mxKcXfvhr{L=4$|c<=vml!%i&flIx{Y-m+VHV)@~(S7j-D z5faaA-g)TQqV*iMCMy$Xk6ciWR#p$)UG20xla=YJU>4!{xo+Zerk3|?9b4<4X1({8 z_kQCo0Pc9ph3&D4fBC_IrIq0*D~HZ5I1?%{Gai5X=HK2t)jVs0*-)e8YJZI6K)?U$pAMpe!0KdD4`3Jup17y7`22t0 zM?{)LUYcDS$9J{DRv^m4jfx^r6uFV>EvC><`LPv5f%gn9z=rWODWs4RKuJ`yy@?0* zWrtRk5bKK%#aF)iiJi|^QS~ENZ3aMi)~D_}d1;69{-D<#H9}phs7A9|t3(W|mA2Lz zV=QB;)t+1GX31cDtn-7P-WSv=zxDdpNGYr{)|$dt<3K_t#?2*m|I+ZgKRfXFf#ZeU zxb)X=1Tg#P58?8tKH+WQZQ)D~_9XOh=~U@`P#u5xCqF$sH`i&5&7M4EYcmqkQ&Y{CUA%4m#N6rmWUSWOea=~DU-ZJDJ~2Btcl_9qqlfn&9h%xyQ)lME z`yN?d?$?{Ot-Cf~e$fTBR?FsvbNST7xM5#r#Rsmt9l#AAy$#->zmzWSEd~Pxk>!>k zS&#$f<#F;OPH(Y3`Y6{~$NfmhV zuJC*R=FI?ZdDZznistA0uYbWy|NY00?3@#7rm0a%2&G2`3nQ~w(B8vK51#Dj*!;iQgYQR6 z>+(?a&+{#BUwyCL_w)t~9RrA<97D-~F;)%I2C((Oz;)xAKlbGBjo;vpiSf=H8rnSm*LW_E9@ul1I1doIrT3(oI;K5Kt{4FH^Y=q?2C{LmIdB9ea4 z-CRH`{8Du|?Dku|Qe{kd{_!Usy8nSkFFAPN+!tN_?BOSicJIO$UaqrlDE)GyF8z?e z`e79ak)t#D#xsu%+G|t$&)f6*4*g{h=M7te$hho^4Z;j;L0y>q1+b zPfUzEZm6IS-+UVYeB=|KAVw}t&0wkD+Yl=?Q~km zWE@0y-~BBBxZ?eH7E}#FUuXlsWf+>?`tk35O7+%E){O$cJaJy05A(D=w&RNGt{3My zb&ef7PzhK*2-f?XsYwN?0%Dpp3R>&_`N^#JbET^5$!w(%@ zJhRm-%hk=)fiVs$RUw79SlIf-dk-yDzjw!f0KnHDJ*=HkN;3wvD5&`&mdMDtg34h~ z38lw~F|R$$hdfvu+SP$E)*0q35+D*X1T2t)?y)2DRWJ0>9y@(sE%nVx?P7-*Y{7sF$vux*vV+k`u*g7*mE{riYhey8hMgVyK)!S7m`HkPaQ7!h~_OV-AN*QMW2mqfa7%%($^_T5>+gmTL zpMJbD6F#@$Wll6=F}rtiu(kU16RYiB?o02`Vt1fS!k7%;>yYjkoyTh^1fA$*y zaL3OMS!axO#?sQWhl;~zaL05qH+kOl#8fG)`sfpdL7)r)^9+v{v^Y7SEVqP?5D*yw z7R-5B7f+vAU0-i+F3GSzec`LKXewgzBQM_p00?LO;KWjF`(ZH>MAGw7_{KrGT#f=C z03_!KMC5>oGcKfLj9X`&b41oUYaLnZtOMf!IS)M|?6EQrrMeW+3|6Ju7x;YSWhUz8dVq4b%kO*KQwP1aL+RvRC; zHYWo*%g1-_^Q$!kXS1AKG5g|o1HdgGzp1m9F0{6Ky-|@5v>J^DX}_O>bG|Pk3e6Zy&ra3Gri5SO zl6#(Sj73BM%+j>TwdYIE^T=6_aO>wj4**wO{c5FhMD#q*m%?*3`N{N8kVMT>!A}f|rET zJ1ZBw&QOHFgka2?(~sT1dFU&CRNa5|@5!*bxp1g;=Hc4J!Rq)NqF8$7-p=x)OmGBZ zz&V#Fyln?wJhtmf!xIY(069^iGo5>neEqio-~(6fV2$SMZ+y+C|KeZ1{mgTYz>$Nq z5P&mQ4Fd#w`RWWSHuvBo?e@U!8jq@V-rF2JcBsV}hRxWO#nV6E+*;Q@_fz4Y zR(WNndd|U1zIMl50Ps&gJw#4fAjvU5wRGZ}|ByO~aCa@sGT0I{+Y@^`%Finu~d? zhosVKr4BsIEmbS!CBAQQ!0`vZZZ$DaTBEHhkTK547AfZfWw0`TA`Dp&*N*<;u|tm@Kli+Irnb$E z2Aw3SR3|5_9t9qtv(DK(>zC_ezKBpr5tkUE>TfWK+h6ua0PwXt{_@1hQ-18JTtE55 z)6UCtrNE?vR*x~ZzN*HnwcUHBPc5x=+j%5QNh65olC+<%EwyWn>g@DHSmk?mPe}$) zFrZR@mdx6huwQ7{qiAm4*o@X5)fDpo28l{<01p#md4Dr^_-39h5!hb zOz3&v`;Tt}z~toa+SImq`z0{82mG2Blo+zrrytsQ`a3~+cIRbp629!M&Ucm%he@qE zHW_%~`iVzci;r=Ec{)tuS`?R=VCBYKeeOINl&p2&G_$#~nbGpW@B9q_{QgDTn!EO0 z_Ft~~-4FcD+Gv2th%7u0kpWDG{^fheA3nKOlKhtUyl^V$-TA}Ciw{g+{({-=!s+g2 zdi+dph%mW*yx1)6{n>hFtVky3WEm8gh10teaD`?$>g|Sud3?wQdl1d88hc;H$+haD>hg1p>9u1oKVlR znmkLsx*|m+WBP-o`<9+R!!QzT@1>5-RD*<5Pp}VYcj!Zh}rD+z#Wx*Yy)w!|O zR3_%6#KG3;$)}%LS>3$qs+Y=Av~>K)*u?bYp7V-93qS$6JnehFAC{^dxh_WSl?CJ? zieo0k_UnEh0RHoyuP!d1vH6h5KJwE?yLwPyrbk74tG{#K9NNlwFg7te{`CBF>x*0c z&D4u=?}an9T21A7xzw!IDxJ=zL#Wr|MqIKfSZ6en^d&e~DEQ<5{4~Wh=hRB2mOh_RK$QTDeoFM@jZMD__ zz!;EqpTF%30Pxc5-b~D@JX0zM#A>w}N5RJGX-*KuaTtLQ#GD%$)HY2lAfwb-&ZUr& zgpUj}O~XP&N#ujmMWF#0qB73+B{=(o?|ugWCZ_i?F3Mw*fD}#c1|Boy{f#s2<>!Q# z)aDLwE{Z`XZ7n)mM3oxnaoXFkc~=HJ9}VN6RIN04z&$UhRL6qQXK2AFum!nHtMKl7 z{uKb;bjcnzHhyNUHGguwHR=HXA|`(93E0&L-tmh4sWx}q{mkBqyy=F6|9=0ozi6xX zzTv{lFPk|1(33+%?I*{cZLck4T7+S_d3d8mj54DZHwTuBg9p#Q_lFMxz%6(Eq!OBP z-WE|UtWOL`@+{r+;|IdWpV&f^Mu9+GErwp|atg3iae)xE+OS%!p1)T_VSpYJ(V)|d ztI^1*V_p5Mj964bCkya>Pkij!SpYyd>!J^RZn`pd`HtkAvGRBlle3(&B#x6f_N9=V zbB51j3jfO?++dM?vqQ;EvbyxkOlp(zXbs1ANUFo zcR)p^wpJH~9|R#MP(@aZ`h$9H(zzVCWWq0(8u?(oNHs91*2km_d#!bXoXbG^0hv^I zQu?0BhMGL-$5E01nduk49RR*^=cg*wCV|^rS^M&xUuT{jsBUXxSgl0o96T`9oGT?W z<1-Vxw$F68)(<^(^vv=}?Xv0diE1q=C)F%hsnVG-<&r;0+fI`ZvN<+2)oeCvH7?n8 z*ZnR4eB|RFv*Zu}kN|+3v&I&Mf~<4$xkGtYl*{#*9lJ#oMShqn0}P#Yf^kdWh>$TL z)Y>>_9a#V}+7N*hBJe#f_}~7&e+vNDzwSmN%8SvU*BcJ{V-s`8u-#g*N*P_0lZY`8 zj0+hsiKfWuc>ul?LEw8q42a5tra%gwWnD*rfI|AU>R6OSzU2RO+us4e)Wxqc?GuSt zDTP>Xl(Z$rgf==)hq`d8$P1k#V}f&)kE}IBC}kK#p5QDAeBrp~%P97}Ah1Rw8bs%; zC2O@)=z@D5{0RVDPRezj001BWNklyY@xr ziX@mnwEC| zrbnJ#+!|!Nn)OzHG`}@$PK-bHC&AG|_Q`Kghy*kYuXpSBxuf}W+{46 zAU<~8HUL04YwC~h01}m0?3yTz3+qPR5KTRjleM@JdnI24LU@e%LV8kqQUbAIR&+<1 zA@F_KtR~aVdK894WGoF+eb=MMZ@>G|#Wk&xapu_Yu6F^z{Ez?H=9-5|x3#fw@>r!@ zjZ0xrN`w~}ty*hKD~pTudb81-BIgFZ9yvQPHG{@%Zf=P<5!_0KRuvm7OV!3$W2}ax zK$f@&!YE(@X0LuH0Q~ta@4e*wOA1{)@W2oL^Zv)`jWBnr(;bdC%Hvbxd-m;GZReF* zvUg_3D9zF=ONU#mP^xfIrCcc|qhY3%vA}YZGtRk?O6l=Nqm+bW)j%V@>+PQafDhe# zGa)cU2hJG-KmcfrQF)&CHwN8SmK76Ib8)T0g-^r~Ib%!;VI1W}hA21}#V_TEY^d@v50Eix%NIExM#GUJE|E(POENa^R9mXcMhLkzGplZRxj#1Z(23>ix4Mnn|FIbMw}iB)OE%-xDHqr5+NmO`o;mm8 z(~Ze&?No5=a8l$+d1BSm$Meh}igHuE_?4xZT>(ffV$Wx{T)zhZ5Y9^e;4TX&JRVoe z5o4%QPZ*!O*r#g1Bj!luRw>CKkh~NHG6)Bbb#fg@Z+|U%>5j(!$(r<}aHT=?B0pQHT-*Vak_qW!U&m23Jl;Uc=>W3jPEL1N2cr<9M;XwK& z-;cTEQc84WvJAb@OCltt(m|FL&OuVDC6VOZWrHp-FN`7p+Iii71%PX>I{)e`UNAC) z=T4jwP7RAvCk3v0CBa4}bS&0Qlg|ABD3B90DSOv4$KKN^x*#ip`C+Zogk` zj3t#?=q1P*ks~5w$T};fFQqJs+!!r5CnBx2HM%Ga6Wo*HA8x%30ABmX8vg*Q z-f%D^W23lSu2u*AZeENk)#|Xj1#w7r8FpM!o zF1a^M^CXnQ>280d3SA04k3rxCk?&=Bh6LaG>B9hU*KKabat$wz+hvm=W(?)%*HA9?Bs0NnYFyV(OD!~vHAoMoQn0SHM!*$)C=4^o@w9IcNK z`JN|z#$+6bv4}O)aF?)?rFiI|r|8IXdSoSC=@o-Q6cCaxBtZYR*8sr6kH4<96@FM` z>iE;YP+B#{$DPw$axaYYG#&K^&KMvU_=)HF*)SD?)f;0X2pA_stFjEZFxKfp)f-Ks zbZy~etuYms67GdNuYCsq+;Hv1v)iYFQlJLI<+Y6yOB;stShZXfdU~$WEH`|wzP!2K z>yLu4Wb(YQqwS661?Qb-0)K5|eRFN?qJ!t=Mpx=pU_P@&uiI_+di(e86C%tC^LxMl zrvUKbkKIf_GjtD|{Oi0cJ5gKiz=NIW9Rc3bkE?P- zhXCMrE}jdj$*$?S&CS8Fg~du3)|<`q_syPMZY`wl`b&0Rwy(Z?_^D&v%o~%#_HeCT zkmq|c+_rPuSS9Fh3{J229y@x*6}c)h!tnfq`_|Vssuj=2_)NF^t^1zM(WNI(Er0dC`wB;nU{q^}@rj)~_lz}XYR$%9{_bl40O73go&RR@4A3wF1_^t{@I7*n{&(m5NYY{yHa#lGQXhZ) z#y3w_R5~23uP+rvx3%8w4D7aTv(wuef)KSO(w$NM(9@@z@%Y4q^g}lsp>m8|;T#Gl zhpPDKSAGZpfBDt_1e4}!WK=#Jbo#CKAkDM955vA~ccQEahC`>+fSf^I2}{xLIe+JN z0bV=p=YuY$>CDtvyVG4bx`?@BUZ`qSsFWvXcT{WRp6A{A!FK@w!dc00ebpF89!E`R z5G6xqn9q?}777tZV6+GX8qEk9vdEJ&jbKlcO?urZJ0S-r`u)w+^EWfm&8la*rR8BW zbqz-mG0UtRefdoQu=L1Rv0&gV=Rr{z?t5CN&KSmk`;v_IglCnu#xMz-drqTQDidh# zg#cC{I8ByFjNUW6Zs+&gQ76ID;Uj<#s#kr~|tv z&-t|rBEQmF%g49v1d*LudGgeJd+*MJwyzvh*|0A?FXZFn_3GUAO0U0ha=9g>S8ddo z8csH*`rUkEYx8{{`7{80+ssoh+8F0}82CxzIY((>eJ_!|??_iGm0>T- zvf{xX{Tl%6{_Ss~Hqz7e*oyAF+I)&eYiJ5`4#43r0RaJlbJmeV21sZvkR@{DfCNJZ zfIto$11A}B&YUFy;4*mN*gODy;oUD*EE-vtwKG(C5+z0iCDu8*xV0*iN-3`TNiFfK zA;yf2MbX6USU%Xu)2zrXkqs-=er8z_CN~qd05NMUFhmAMWqF=ytF2>Tr6Y8lk?@&} zQ3_xB|M0Q*006>SwKsjP2%6wU=r{sHjtGuP4&az%ekBIu7y|~5Aq1h{7z>dK?$W%o zJY4z(GcD&#ni(E7WjO6bW2nnn8aO9_DdBAE&Nl);>!}|V{Z#}`2*4#d3*fl$Roan3 zqD{fjwtjx%29!aBzp zS4ue|&X|+}iPqMa)4_0NcBk+IYm|JR@RZSvNvoe%Fvc7KAQ)?`F06IPc%Hl4|KW20 z@awOC)8=r?SQmJaA4JIcXq5JQBO+HV$Hp1oFKwTlt=8n}^#vTEEpqUn*GrADoqi7; z_dKuOED6D!Wl{!(aY`4S@T_rzk$w2b-vxm4-h7)5BaRq_yq2&a+f0|A9<3j@#lYi8 z1lC$-43c9=1dwZEjN{0G2pk}yGTP^YG31;BILT0OkjSfox|zH0xuXDZ{|Elyxvi6i zQ{;J5V~s2~j(g=8wl+4lv?3XXEGb8R5;9*HJ?Pczb(N-CY0vl4EE9p0A^;OO7-&if zB9H<11e2aB(!%lA{>eWAz-R9IX<<<+4P?k!=PWsLK;#@F#~E=39Emd&LNLYv(GdX= zIcts9#%Qg_T@GHOaEB}WO&RTr#$%$=y zN5i!zp1Nah#|y@)2Re(Vm!5e>_*t!1AFQA0^iLVxpBVW=f}vnsAmhN0kRbz(Bp3;aoXj5HeC%% zAs~~I6KC2wP{t$cOF(ChF^m&O_q^Ovp`9rdpbbLKnC32xLp}tv)^6o#@sBsX1pxl^ z{qJH_pi|B%Ad5uSIEQG_`qFa+35HBCLMBjTX=bdo4C{@WhoaZ&0s|ffpxoB#dZ99u zo*zVv2`S}pG#rga|9sCQ0C4i*Z~8(ZlF@~;iV4pY+A6J8W{ee{)GD`7%vxu*z0X|(}nfc2ykZqfy=Lc#oXKr0N{(C{~Ti$fQXzi z+G^*B0GT5K0zg1Sh6o7%n{|%P(pGb1NPvjsoHbS%+uCR!dG=^?qEW5*##zqTXKwvx z0I1dC!Z;#i$RtOBpAOa#Z z#xY<5akbY00EDw1xch5+4qouw{C6nt>_6|-Q8;nx=ikd$jw_eeCwCOO&Eia=7X`a? znyJ-AvypdHmxVeJ;?%Lsw}}vvb@+0-H+SB7BCaj1Eo^l=USsCo`+vN5`_50_`)vUD z;O$Rpn+9eWM_w#k1kM+X$5IF$IAEMPArQDAMkq-J93=;i$blg+1dasA97#!jfSzE2 z0ppAzSRw(qe`gHFMEtLA5jXfIn(5Xwh|l{#K{0z4gh4WHHD)*bC6r^QB+}CjRd+lNqX66 z*j*`u|B>H*4FLSnyWfJY;G6)QV8{@iHr8pwSri6PfM9@<5i$>1kro6tceYZi`pDax zU1AiJ5&rg!Lu+;D6U%cxF0C4)zZwq083^@W~L{4R?)rHZSF%}sa zqXA)j$3c{y>-Pj09(shJQth<$k`kaZT3fC1-uhs3Yh!D;)E)+ns%%VO_0l(NpFI}< zZvUsxAz5b)kplqdoH3RF2pIq{WCZj)AV&a*XsuJKP^utD%p<`91{}aSBIl^x>YO+^ zKQ>V>#Sw6@Me#rX?hXJLpPM3(#EG0?&RMOkbpS{JfWSEe3%YRNhzkZxl0$MPj(jPY zRjTGmaw>OZog?JVIdTMm)&erltXuAF0sz8UfBwODUUdUWZqPZg z`OLk}xv;WZhGLK{mP*@s5%-qPAh~j7I%}B!OtEzym#B4nVId%!-a*@ zy%c*^ftT;tzwJBUdi;tjU-~D1{Ye1$`>#IID|%A(XD7<#STg2k%7A1(vd}Tk1xbz! zxkU%Uh=57ShygK!z&S}q#xY<#^1$~%aLyS5A_4+K&S39s6#y>3<^Z8(p&;vT8{bmPn=i~97|EmV;Mv|O%-MSa9B)lYi_mr9A&AVco2FJ3W(aBmJMt< z^a78cURp`Y!MDG34*(pzc=tQr`^ozB`7%g^?{`Md08XX1Z;`ECGM zeCl5XIdYl^0hx0qOMAA+tuB-?4#+tRz-p5_cpQkq$m1kJNho!u5v2%2B5j|iP1arR zZ*H!2(v@x+)M`8TU9jumi>4=c1HeCh@iWd^YqZt{=i+$;pp?-@10XvKkvV6ra}1xy z@+|B3+u0}=OfX4Fx$Xy&99io&HaAxm7sndaQnd^cB*G^?@p%9^{{@#6I&%V$hygfb zl+w-uazbJS_>u!TaFxL8^@jq`SaJl;SwOUo93e7J#%=Uk z0Dy4TCqML#OZV(=wU0KV*~fnV!0vtZ-RE6!@|lMtUyn^sB5QWr8^TXQst#LyLeOcN zx6|2ub9riy99{k4qlbTOde^nDy&?C~PyO8&x`yu_o7puTHJkh=k1byJ`nSFNLw^ha zpZUszqqILQw`Qj*MLvwi_VujL2;7Olq7(og2Sl=D3DDyaXWWr-sKE*_zDWlVht2V_ z&jnfw98q${7!pI|NING>0C3f7FHpn6aV9a|R+}3en_6z4w6^Cntu(9G%9|S-R-2?! zWxg2ZX`zf&6vt6gE2X0Xjr1hvEF$DZZXIdqQN%-me67{<6ZY?4`#u1?@XEOZ2d@bI zsh7O$#+iM4dRDG=ZC_hvY>dR2Gu#oThHwVR9zmTQMp8c2XIPZ$_ zu{i+v((Rvd#%gP+4a8x0Dt+VA19FvmhYP^ zi`R0V}XI4P{)ju!@1FP23pcs6V!q7k08x@c|6?JwN5|A1tKHn%7p z$jB39ozjM*o13Ttz^i`en&DbYljE2)OXJ(>(r!^QvDT z-+f^UapC!j2{Z=L02W{w8Wh|z0RoYsbdKl7kuwE=11H^Rc;@~y&piHy-};kMWjvC? zsiHgR7NlNs;l%*(f8TTi5D_>81R{pqQNh6w3Pm6VV}OG5FbE~*MUip#JXG4`dCoy7 zYgaeB)hI6eEKZ_!Z!k=YdbO;b?es^=z*oNeAONf%`91?h&1_ zxbsV&b;c2b(Hapr zd>8QU`lde|$T(r8YAN<_zWLJtaK-E1EQKcnDY(Z;dcH4xB8x#B0qfNr%7OjK!;h`B zT5~&Qr>5#@UTn3xQRI7)5d+5A{IO$cFZDTRWVA8P5djdOBSruloi+d#?JY)75Bt zVn)Q}(D%KB&+MJuJTw3F&rexVzv2ZucV1Bqg2olE{uls!;M4c~+W9-OBVQjZ9*=6t zo@;J09EXFYQMlU+E2Og^T^aQjf{Jk-GXMOhO8xRrlGpk3}m7?a}9VTLP)i7@r;q*)x*3*q1OMA0>_T78W!|l;% z&z|Pm;@VbcSTFg-K+kTUW1_y$T{^I*nrqrV-P~HH%*2;ou~&;z9VOoM+n)u1-@NIQ zZ-3)8!xKO3FC8;zUi799hV{zg%Dms)UP)@IyOAzE(%xFY0wLQ-SX!$EL0r|j)y{5i zY%X{3;Ol;W*Pi`Znl5jg+`a2Ua@g*cyIfzdh? zVV-w`Drv3q0<4x!=cPdA1q|}k^Ws3z_*5eO(#olBKPv$8iAv4$Ld8wQQ5QTJ^Vc^9 zosp}S{DAQ8fBg{v+<5J5PJGu`)nvL>4*W2wO;1?e8y~97^@^F&XG8Rvql?FcwBH}iYSP+vA*=uJSFFV00F^~F&M^ee zg|P+*2*?o;fHBrO$B=2IjL8SX-mzznt*xDI)M?N8v)xo2IX&{4(e4Z7m>gzHurVJr zq8bQ~g5TWVUjP6g07*naRJa<&AOGav0KjE$dXrO%OCdbZV~H;Vgp3I(Jef#U99ceb zY9ZJ*)z~{ztH&xeg1lmwqXh;))}fR-AFM9S_uH*}G)RZ3(=G`7IP?jC5j?xJ1ONzU zedc{v@7cRcd2C^R>*SG>`}fX>fI9tLVz^`5On@ad=zAisO-+6I>-Wv>XzZFT9XT{V zJ~x{U&C=9tY-2>x2wLr}HCEtqhWL zCeOS4CqMpCeWrZFn{PsgMx~3Vj~qVq^EeFN{+^Ekz@@Ld$c{!cl^Jeid)W4TVZ~4h zlMa>2wCBP&2_y%Vo6cb9dw!!7056hyy|toNmbwY<@*jWxz*8cGKH zFMB1&px5o}*}dW7GThT?QB**na^4yzizPC(5A( zm#ZRGx?E|DvJ|)trH5p6F>=;#BcI_;J*E{+mF91I6}kp`Z}kdY+>GKFTq9PrjK z-|CH=F48%L#2SyumM9lUF-BU+SE-ju^W*7u4PLiKJwrXAEBwTv!j?1o=2PcZbI{o7D zvWDqW=#jI3_1Cuoz(ucry;TM|a|V48_$(B{O34%9?VKFT);q^epN?lMUNt0V&`==b zJkv#{Jl`Wn!Y>&|dEU)Oy<*r|TUzRF^~izmNl);Qd&ic~006>SAAjS{OJ8!8KQT5~ zX|0@@Kli}c#+G{MiIsgjXJ#7BbgLW0K^Tyq#83X>#QyW15%Cr&LaZB|OrYp%V%G`{25(<@*6?)SIX!vE)!AGq>WZv=qPzyH_AD%FXJ$P*xv z>iEHHPds<@&fCBC>bL#Lu3g*g>F-H(C>wR8-xw)A$P48t)4aQt#lARra6Iqln`!X> z(*+-Jd!BWn_VO$Tk0CTWspC+}X*p7*@&Yn+n5|IaxekLUZ9_TWADesg4E;JTacI(v3@!(`sV z;qu&Z#@T27?qLAfyl0Dwqm-tbb}EdzTFQt~uhs5JK}^HR+I>R$z+=mCJSM|#lr>2t zLZnl#!C3@@Lv|sP_Ik4Cg&IjB&^6nk3b}Tw)9qlS_C2{D0B+oxc5PA2IR)2sEL}HA zz0n08_Z8d(C^1Nma+%;thalis_=!lknOkJ3!D4>n#Av=R?>FnmkDba}mPFj`AL!q{ zYjWeo+4K-5gkWoIcme=E{N6trD5L|gEu{3AAet4jNY{{#6%tVxbmKTlrJPbj~qHZZNynH0^9ArN_(*{oXI0I1B*Y`A3mrfb`I7 zv!E*h`(DM+2-b{*a}oJLz+$djPP)2yCh2vk!iMAMj?LnTU~Je9ei=cA=@5b$=K)jQ zkTJ~~9oaCk<%&#p5CHz|`}Zb{3y=sf!l0BCQUYM5lvG?QKnh7A5}Z^k)s?dgo%*UE zLN}0()#feh1J(cYpFP{@wJ*JNon?CCh1MOfDuqe<{-+B;=(-O6!sq`60JdCzvuWCz zW+uL$ag1Uv+ib3;Q|Zm4)3@AoJ?G-F2Od9DS@5|eh9S7nHBH01=Xp{IO*b$#6+j{q zA!Wj1zfq|zE(M;i6d(;@psTC10D$1UFa7qoKHT5Ge%QkhO_<^5iUwqNUljbsNXWMyCMuy^kF_ zxm5qtH^1@5_r22{7z2R&KJ(kQM$#D@VTEni?4MX&>pt?c2M(QT+<5J)M}}%yy4q+& z&6--O#jTJB2`FT0SR({y^yHd7lM%>+2hZ(4wLCgGwBnJO`NhqX#api2&u?7X(RJx)-#9I7xg39F>OL zTsGaPh0Rtko7E#uIY${gop!P^i6S06_w+shc+=IR%WKt3r&AOYE1i~x?RUJi$sE^c zx08tL`4q)64id~_DA_!llJgBXTT?N|V?+IgT)Hps#O=08MPZ;Xv6Bxy*Hx?$K>K%-Po1{xhYG69a$#*)IaXwi|AfQ9N84 zaBR&s(e+pEW=S-hFKpScjWHA^Nb%s&=bt`!;#|x`5QjQOI;Mf=DNv?m3xz$;Gb|e@ zVN51*61Hne*H;+0CY6+DU)&D>2+sTZJ1z(Y^Os-yiU+^_onk52xXsSm7exHRPN$kd zN!raOD8*Qe1S>C|B?b-spi!-*(&pAnU$Js-{-twkaU>j_pE*3U<@(F|2l6VEk3RT9 zG2Q>6&;3Ol)W)uS6#zW)7kBB@a4bzjGKyn{Ft##GxcC0kk4)3ry?g!fnK`fSEjOb` zLMolHoJ`7bHN!}|Mz_@ps^wuz_7_rpL+*Ts{l_y)J(ld=K6%mlloxsin4kFOlK^nR zHS6QJYnqgBlRzSQP@Hfe?Y5Ss4#aOmq6@oZPzyKLx|ku0nj-OI#aM`qWuSk3S0luh^8biMsrPv7BQjDC#vDJi*1ueAMmqYC$Ni z$*D=kTdduVyNT->sk}iAz0-;69fq;45yvu8HkFQsXf`vk>-vEmH=6kpmr8JzO}T|! z1^_9}3LqRrk&sex&b_FZ&6N6bqa*or%F=aHAxtSXZ5uF_3Wo^~xkxZA6ecIP zU!BVj1Hkvc^Hr%pAq1cRNht*U29`=GBmk*ICd)I+t8;Ul$GRm4`dv_x#a($?3e!;0M=i76Nw^+;7fLI-7qD;VP*FC*=sfyH(xk>Vz%+<(c1b=>x+GMySmm_aN;2T;*Smhz~wh>>h)T>rKe~* zXm^q*Oc(Wu?W6TF@IZAdEC7jY&~5O%KzE1;5bOFtxsV78OfW^1Y8cg*n{L4v93K>k zRDw{V0U|7lpMC5A0Ni+SmMFO5l64!0`VJkNfv{r`G%(q>w%Ty8ka~7xdc(;RGdOHw z!QFf+lS>OBdd;q(skEahrFSFS(#^h7-bowz>1#J!_q)1fajArq8o{n<0KlJq;QjSZ z4FsN?URUZbHEQ*Gb#-K*5Vaa{=yMetnr2#tLNeh}A5%9;`NtnW3IN{9GXyEDIZr}UtKuRTmfh!qt zh5#Mg`!E0?IPbgfy`oLE^%o9hRD0>n;S=XNev968*%hZxG@6WUSvRU>GM!2#lXp9w zhcygHX@n5X@;mLuTF1@znfd&g)8&VrIr6r*-ZnCrT|Rjx>WWv~a%ZQy((ul_?xQ~h zfcrlED&RWBI*${An4;WB=htfD*9Yclrc{{TEID8inL27TPA#u44Ga{h?Pk+MB4M>^ zvoB-U&YWs3uI%2B?-KL)ia#_kFf`u3va)8mR<+jt#=|cGz|C*GsNSyX*vOKc-|@pv z5R14lkkQNxNQo0;x!w~}SvC#gE-8|3SIT;*Bf*3;bwj6y6bMwpV`gQ{Ts~*$n!+ed z5-tP?@%$4<0N^w4xbpb%vt#|K!E81=G6Eus8x_V9rle`oSZIBHnKLI>!XzpncaAmZn+AOP)bUn5oBpP0DRzm@9Fs+1Y~q_ z%CYSz==r^#{tH+P*drEMoIA};; zskXwHVNFjvx}~9*v+w-JUI6e`_fv`xF*?gfTC1~$Z3~1~s_i33kDZ&H6$wiKQmMRQ z+XPg~wY_G?1fmg9D(3qKind{BrfWH_VVMYkOBHj$6i7m_mS4B?n!e%`0DSk`Ujd0Q z#=4=0VM{Uznnozq4T5CYtgoRYaH)Yciv#}gPk(xDp?dY@oBS}W*4hap)XEh~15`Jq zlz|tvS{)%c2S^vPRMR$&<^TFi-vxlFn{FG)WOi+xEZLcbN=IXAz@^t*f9oSZzn_7g zh-A-=7w_1*?dOj@dSL&PAY`S{>~uM25=lWYNXh+fPd6+=5CP;`R>~oM6a=6}iYQPg zpMD$w5S;hRuiR9&SUOvrw#HUo+FxrlEj?p2j3bBVc*K)?6jnMwQmL%;`~U$@7o8dxwt#-Q=NX`*8 za{2s@9oKBV`xBaFOQpC11b{&yP?S&rc+WfkfJ*^@@`XYa#fC<(5Oy|ixwaqrsZ8Fo z9YV0FTZMdXarQ*qsc6LL8yKtmVJ@AwO#IL<|6N2qQ`6%F{3s5BnCjHhsbS+rt@Yzy zJ_`W;+CfZ|WPW|_h!h-At=#l}{p_>Ls|!*x1&}aqIZpq;u#?Ia77WYAN^&kD&JzHMiBRVB_N$8n8v)>Z|NJ$bD6a6-6^>2I=0JgvWjobR{#P{kAcH=8wZ7XOjoWA*v+rIG+-`TVKdbgM>SLd#| z;i`kjW=_r=oEUKSA3e0P(rwmR)D7YwkSGZ|T?ELnt&NkT+cu5HUc2jgXB&Pq))Yaf zp56-p2+n)(Td(c)*)|N6AD>O7CNfilGmFQL?K?VUc|4cdwB?3^sZi1~(z$wd-iy1K zV$SHwa@`BPfkLiO$V+M#M#qUxu<6FpiWSFNBE~MdfuZ3;b9-O=+D`z${U5tUr&J{o zOHkRvsI;RTJ3YUZ+l}Ld@dQzGwboAJBUhOn?wwH<3~ZNeO}{O2qPoy+;Ay$P+)B zKk*1o)=VO59p(w%44CHREv2oVJKG8TRyVP2IyK}5&8|kZzPy{s*iOm<#SJ?ZBsgkC zS{Ppb=64P5cq2jx02Ba%|Bpfl1HhlW|Bpfzd%d8L&&Lv^0*i=LCTlrPyVtQY*=#N^ zgfcCA*XAv?@=~X~;->Rn;9DuTe|UUsWc1Mo@B8&rkNI7%(e?ph+p!FTm^#u7eD>tr z)B8^VKzHfaAVMB@yjm@hqUDFrJ+trl>}k$J$0TvYEK9dD8LFB2T%Vy^L91<04sO|dK~~<|F(B#ytNk(9PjHJy79)FGwH$$`=7byirr5>_e3T$ zvUy_X%>1$6dF$&J*V+%<|5GDz$C%#?8~yrF?qs z-07jBb#SFVUlXz9XPhGC~I8FN{w%u@?D8(pPHw-?(sEoJkj<5-!zO6M16PZDLfI}4>;>Vix6@D{?^ zYx$6S!3CEAz>og;1`V*s$ll}C%;>glmt8kosjRKcs9sgmWN9>S6G0HyH6=`qAzKq{ zW#*je+CU8#Yn%JBDdAg+r*r1oQgt=T9IbJWX!DgutdOqL$A10*0Q~MdUZ-fN8sVWs zGpTIWuu^u;HZyuCVxPqzC`uBq+wr;)YXnMD%+{%-4kVV1x?TvbVYrq~Fjk-usw74V zAt`ynHDm~(_CI|H03N^ZTivxIuGTR`r&V3Ea>y)LD=k#5q25{Ln z&*c0l0L@5ea;}>i9vB=d4RFrN_j-EYm^u=bkZcvG3qU#t@Go3H$2B82c zHx+c_Y)b32>%>cPrcoTr71v*oD@|uoIRnK>!a~j^0~Zc@m&skB*LU7np^t~EMm7b~l^j>1|tlgYR#g+;<+KZv|; z*K#afHxY&;VLhKUT7JTk{=()DfBb{jTzL@yyzldWMf~Q!{on^t*PC9yW9QB-mYJR& zpE!2`>-=Qn27< zbfmkMR&P|YDcKY-vlazRoP6#n06=iw``$IZ=hkblz3?sXyzRFy-*fpzH%=ct`r>$Y zLuc+(Sgz&^eWRN;P!!f1y;$@(hIZR)wS7wQ;6N5DYB)~FR7~BuW5QV?_Wo zswn}WKyXPE!b-y$`t{y}0Px(ikJjf7i)h|rtET2pZ=ZDfN~aG#d*FDZzm)1LZpb>t z`J;#2G_5r}(=6oDj#3C|bY$Z;-FEUL7h1V>m{cNMr-8gQC z#Gsd4u(KP4YxDEHc5BnR2`&@Ol+w^wn4m|NN03bN;tM^>5m4>!XOfMW<7#ZEt zW^*qdI6il#w_XQSlN)w!+j#Syc=ZI*%^+&G+Vw`;r$#E79T230;F1&pr0KyS@Mbw_dhuxe>Gio=If~GvvY{bMr{LR;ex4 zz5b#dE4>rbQpQQ^O1DrZ?GE|`UJ^b`C0KDTvcenj|w~~lhVXS+x?=uerF6MGh+K$6SD(XdEt6fVf0WwLX0}7Or zaH^w3AeqvLW9S57g$SX7ON5k?Krt3faU~8sa{>SsSC_lZs^490myadQBcmf)E-hw` z%_q)~n|6W@EA&mKOgrwZRI6>@7rBfbGboH~p1SmPR(1drf)It@K=A)lN&+Z^2mm02 z0N{h~{Ubk)JDqrBpm2c~3{#_1NH2{RB{D6l0VZQ(6G7lJ&N$})xD)t`3(Muzm1eE# z1)ipxlcOWA*t2_bdeXGaa%HVjuT-0D7A3ydIk5MIN1k{T0A`-L7YW%(;Kk>U96o)j z()KZc6uqF=N;_JvFN>*RXjBSrYE%($Ohd$s`Mvf^BbTy~))e z^1%;HuABPh&+b3;{L?2-pV~M*ZW<)PYIJP48HjG1?S1Sa06=iw55E7wUR3Q%JL?D6 zcY^xTT6u14gKXH8fIWNN+W2e$Q(%T4okg zMCq~_cb5Fl5C8Fb0J!DO>sin=iJ{rLLMjO3I1+;6lwsJep&6JOY2WWwYn8AZa4dWz zwM5!TAss78REmN_HG+x66zhU>i~%E%QYnGBNcQbL0RZ)OTQI3q90$GX>aoV$!}Zfo zcS6IOxHR^wrs$51j=I!MygIQnUce&XbJ7D_uDr82wFeU&{x3=?P!iC~FaRKg0N?}f zeRs$tmr{2#+k;**dsdB2EacLeTwbR%h~lBaelBDfMAd4gTB}DaNf_e^mqH*V!ziXY znHU?{wrPDPn{u)_-L^o}mzP)Dz0To%FaF?L-v)rQd+$|PKYjT4p<~AuD$TAR5QHwe zU;{|MvRY1(gi=B%Aru1w%dxUKH*B>1UT0~cS;#u2(VSuH(mt*3kScgVZ;H)F;WO5S6BqLfu&J8er|PbWi9Y|wcZGm2rvNzfItck z2qip`Qb`4^W{XQ@7<$Te3{#6@R&RDY^73(kF&|ZBh>;=nfYgQ(&kWpD&k^pci*ob-~Ptmqs3?}pWD6Rb^!R+7eCdlmuXnen<%9z zJ7t7XeE7)xLL+d}x`~v5N!m0l9py4sK4<6p9Mdopp=KA>e4mew6on}=CG<{WEl#lcEk}f4nNHe3EgEYMR{k@<6&2zOkyU*;*oS8G{ynz=sI|xL5^W=#33+q80 zXZ@4l0Fh@PZ`q$%b0r(%PDg0q*s&49 zQB(nd{v3`9D3wM5$dUm-vVha!lR&ekDJ|f~H;Ek{MlM|Xgb~?7xFLL-YAUb7gHOC= z$Of^92-8q8fW)lGCC`X=AX~S^jIMl2Z&%+|`&DUhgYvKc$PQkYTZpH{LbRUB;pYa^ zTB*}rIpC>wCtElfB`YrOOHof7i@(NArcIom%hl+Kg2T3V<-D6rYP{MBVil4*B7(t3y)#YQ zVd2G6j^>_C(peT zA|@}8w~_ZAjQyJjaP{h3GDM@dYE4%LWcxfk#9w(VI+ot zh&uI5*Sg&1%ilcw8K&{yXxk}VY;YJmn-M6B_Hs0n5|sFmZO9WmOyjeWJdD)EK;L=X z8-8Zto0FcJ6^kX>mk{bfhCaVgv= zr;w6%^1k6C-(bU>wWv)+W(CjFV>fEg5Qp$W4y(s|tkSJ+Gy zyTU%JbD~(;jyZHC4w?$m^f{Aa8VPs&DX6&peZ)9SghRnN;H0$u?D9l_bn*OPjAaNf zohEz2CJ`*~ndSL%zUpedxy3PO>8HbU>WjC9Rc3yBcgV^%HiBXC^EO03!Lb9y0{a;c zV+T19N7f=BO3$hnLcNktlbxZzTF_3}H7-epmrz!vvSdr*Po66B6TB#4rMj}pl>n3g za_uCI_sd>~zw8JnCb#%s+c~$p|IzqVzB4vwRJVi&xPRJ=9UD`&hYZFa<}8T(#YHmuBH7^<$y?r!6`rxSRaUFx@a52oz*oc5Zvrd2(83;^G!*JnR`8 zs$MRp4|!yob-Ny?rTjZowViI;TIz6Wa${t{r^XFE9_eQxh5JBL+i%NE;s={=F`UY# zDhhLtD4h}KlOj|rUOft>MbuuR4?j{a(@*Y*0S~=Lq;+o1(N8~$>~x2JeYM+$$Cm3I z9YaUMe;)ao60=B&mG!ZeJBHt$b4==o-53(f8gh`*1 z+CnW`%0tA>By0XYje`J-DGfciG-Rhh5)&|CN~#iql>)TFP#&@IzS==N-cNZnF02a(Z}&JXMy5)F{Nc0*j|w`{9#ZDON#-0*ZOAXzL|g%vubnm z+w|}(C3ynZY1_ziPsG0S-yO1U_QATx(~ONpTYk0+ck9w;f%8fQYRgJaJbaGIfDDj# zqf<$0VAZ5PyhQwe;cnhRzQw!%LetZ71KcTi1p;?uI3WvE*GuG36`f zeoh00U~NM%0)9KXQ%`h#xWxsZ~C90dc>*M@fqk))XaY3TNbJ&`PzP016eTT23$K0`}<4T2YVPfxWb;;pRTN zc=zbQ1D|a` z)lnJO%lJ;9)aLgxH-|RbwCYGKCrLYG074%Rqz8XR-0i+aG2i5XV!>yUW}`KTWt6ic zRO&IXR5q05LI}X(kiWl`@s8m-hYAVMwFH z@(Dm-0X|;0e+4qrq2OQ{1aH}Y`a8RnJ)C9+RVT#K&RcTb-%YNpbQx(qB55&uRQ#5G ziqQEg&4vX;K@LWdpxx=YAFG^x20jHrF|t< z#?VHgi$fubL!xw{?>m?+t%^rxfl-fO_3EEf*CDhTh%Y<$-S;Q}(TDZUUteXNCkM{^ zsx6jHTl&eIpr5zXw-;w{@*@(%obmEp8yrC28SQ_2r24l7_v%`F_-Ou3usxmP6OB84 zr7Bw}-RTBMux9D(UJ-8QGk1d8vr<&3`TJFR)@L%_UNX6mclKK?{97P(79cx^+k~Q> zqs!TPY~(||-EKtLo)*$!BA9mevwBP=a;q3}A3*j(W<9zlms!MdMNqQqECCU~2rzcN zsL_>6yU262U;5d}t4Tpixz^@!FZ~>({eAg}tGdQ!)t&4{DS;M6rO@wC|H94o#fv+Z zAr|euJldPMd*G%inF>u^dKK0V>mTKBds8a&LuT#Qg#bXWci`2(L`2}>E_C^YmPmEp z>$Wy?jWqs{)Ww;aZ6ALqoPF!wH1K{-oCneDe7O7A{l&g{MX34GwIdZNqS1Pe+WqeJ z@~#I*{^~(2Qt=i|rSs>~%dnK7ebw}?Zx>MC093$r;4z*{6~QpBMTj6VmiMl%btS<* zS^Sl)SSOtWC8;Clwn-|;G6%CXiTz&#uc=A(kM^!jLq}moP(n!056&}w5Cp36~OH?7;Z5uE81%?1A-Iu>M|O<2<~S^E#Vw)P+z*T48NGQnoEBzcP0}{0iAGhPe8Oq$EEN8$|c>rtN*x z&~dJrRP$r|i-3Rlq5g8WYTPl!ICX(ILlh6!v)+XCAs|#h7zp5ZD(U4NY-nH?iL0in z`W`dUsyA!h7`w=>o%SxM-eL(5?n<<3^rmEmsM9k9z#<@JHhs=bl1A~a4pMea5STO zsAXWSsAi+d`~)5;L}zcBSdsZ)?t}3wl?@COogloLrOzFM$sA5!NCBJ$ero*&vfs2c zK&ZR`-e14Dk97qHTX#JTIbv~4k!}jmFB@BI-Se2ZQRW$7ZR}EFD`YUaN=s; z6!dD=IP19UlRU(!IZG|FHV+g{mXF;ZmJGQxELUB_K*${ihPHtLLKIrr{c-IAAAydn z%+$|2ZzUS8Z`?X3BYMw*^c7!tM8?d8Hw%>eHUmWzT#p8QO?`*)s_!=wQ}#z0c1CZIiPrROpU^q5~doUe5?ionE~FtQSdjr&h?L zYC^y3|&k^Ji)o7D7}hp@Ix2TJx5PT6|Y>JJ+>#@~!4d}38B459KM1vM;- z(rN4uWJ=oDm1$dXbne5$0WXn?u$|O6bLtG5zh+rcQ4Ml*KcW8wW5(c32v%rRRz^`n zePS=FRFdL4kPB#_q=U7CqHHDoGJEHn>V^hPUzmux_=!UU1l zk3k^JpzG(Ss=t{O9*H7i0mKn`0&HO^0Bnmes^u7lf-&3S;RrqeJvx0H2o)m_rx&A4 zS47QZCYeDex8)seVB-h3?CE4XX09n5vB3LNQ-J8jA--VfkW5uuV?DGngZr-scBTE0 zr)t3txkj(+{ex-o@@EXw(0l6mkfSp9i2-1gP3vAr_NY~fOI^*^e)3pL5}X9qPd}D? zpkJ=IXaPYx1$T*Rqe7hd>shkO1Vgi6dDT~5Z1(y2l3p8wXjK;%S2mKs4lQJ!L}BDE z)rJ7s(08KMT9wHz9~{>eG{ig>3pj>xN54`XLARUz?((~t z%isCqR)&%3j7ZY+$lK@J@dwMQB^vTP(T9zT8XKi~_1;zo@tIVj^!=sNQ|02tPl5;j z{t6gC#vTaUI^CbXc%xXkY*@hF2?{b7^7uXD8=C%6!hehz)|Tc;=X3S{F?GIg-KMu` z)Z);6^?iA0SZBK*|7m0R`f=pC*Q$8Q;CnOkATw~(qr8`G-iWh(}9G-_W{T5HOEYH03cMX5%uL+JgzuPwJ2G0GTq|+9Ipo)z|V6bQiY?|O(m59 zp*Vt%sI3lks43xm(}EMc{A{WscnU&1r|n0)xUH&}d1Y1;NHZr9dQDU|aLJYdCVi}& z#w&)PH=PV12Q5XBLCGw4Fnh>19Y7h&HI|J;1tcBEv=e@n&^Oi zym91-8(~ZgHqpA~Mws+z+TRY^k3`!ow7(O5Cx}6@7;d=89&fL#Q7R0y`3c*o2<62r zx_S*)>V87H*El|R@3mvbc(7NoBvK2_ur0&6Ld6_STigQbtjKCVE#?hEm_xkjL=}Nn zoZ+CLN=G|A3KVUXiZmFxDj^%20=meuL(Zg`|JVzZbtS2rM!RWu=+;8*r_WjJkY2$F zw%ovqHDcMy@ z9n_2K0+(q~dytp7C>mNlR%$_RG)kOt3}X<4-&p*j``w6{q(S8+i9Uz^VcSj5lPx~= z7c5W$IpW8s86o|nHxC;YGrX)t+pe$kJyOFM)qpZ-pA#HE%qW_@xt@YOQUAw5A}#Ff zSoKKY>?GFpMHw&>Cb7)V==@dpV!6eB&F#^SAnWQcx#YiqJD8N8Jz}Xnz|Q?qn(Fp3 z%(|w!a5qMqemaB0k3f*a1stZp=0Vs$JJe!&*$aEd0jxaliGLX&q(={lVv2;NCGz;9 zDhI~kzEZ0j##FsUIv1K#&Yrl7*O)R}mt!xQ4y%Hvxk5pluP_z(6yomN9bMTNhr-Z9 zUZI}t<39lbrXD+kAizh%;>Cj>J`px(Lm3J}YJH~j{CF7VxdWcK+{r!sDr5}5cyTEY z7?^X4>G49qHZG{xNKDo0 zet(yYl4}ma=KSXWM0t(!Uk|q{mM(#$c7K`J*qTxt44+{sN5VATpi_eajdSBe5tUGl-5HMv0S6DA-AfaVL(ye z>jB(~VVf7`Q|!L#a6$_@@p~LgM(2tnR*cdP17*Yva_X=`Vsad$G6;jz-oBj7d(+u! zm@caz^BbYhtIkeH;%vR!uE=YJMMMRp0}GX~Dk{5t+fsUi)VD&8zFF$Xt$eU-!Qhb$ zYPl%vh;%)&NgLN(Ph$()7k@J~3XLW5(&10{G*Z6qSI`bq3zEpf!1XsvdjlwG9p9u< zsVS)ww0=XW5W;AeLX3NznPL+Nxtkb1||pPj-YCFoe)&#I%e$um^Mg7Ra+*3@S+4&$e(5; z`r!j=yZv)jxe9(Mif=Uhb{B~uq8n}Ss3sh47B8^PQD&h)QCD`%UicvxAy16Mz*ZI( z49asuzHGw!EiC3BkVb0?c7gvE5P)99QtCs_nuLuU%mybx=yi?6x=w}z|8y{RUl(-q zdzh%a@|9#vmj(7dm!*X~BZWVcPRDs@Sz|0SoQnd`N>Iaidn=M)_PJE-TLNWv2gLq|1t%yyhu*W2ZPBt=yChe<5EOZf)13f@7b zA1GC8P10s%0&r9V!stQ*FJsOAH+p$&d^#JUZ%O-HciX>No9z+2zF|-pf#l#eM9C|` z<}`Q;a55=Y1-2_K3xx{Kmuy}v!^7%fUuAH@%q5jJGxG5i4e6-gdu*hVD`QmQ02`3IK z>=oU5+;Ya?2U+)g6_{@lRa(FWK6s-Sl6`6@h@`+l3Q) z%^*N9a@tIffsN9K1iG4pTY-eyDp&ji7?#hmk~g?bm+Ub4$DejJJV#UblJZyInQ7gJgLGF?h2;G9A5nQUJ(`?;Y|C$!l+>M#`Gz~be49q0?abl;7W=pJI|(oB}0- z5}&UT%?X#fprOA5ix-g5-ry9*U%V^sOZcwW&kxbO)o5X&iL|4-{hE8t43@`Ly!=<>=`$x2ak|16`MM& zfsaV}{c!uYqQ#VRz>&u&6uHee_CSokzVe{}bB*1VPTV z6RFsgZrmW(d+>CF12}$KIOuqU&JDSkq)fJ5R9#E@OdE(BWBofeH8XSYx}T~&${iYt z^yp}5{JCN4=THsK3{!Jw@RQ;fHe>xrqFL)&B4lVpIiwf~>hjt2eIo3cgDs9%6V((w ziiQXse(;x5Ng~l^F4(y*7!_-r=iBR1{c$*~!o6uX&?)Ldr65gZmuetwP}4ls&}>00 ztNA8Zk%8teQ4Zkq$Stj{Yy%_&WYMF`BoVu#JatBDPYb;IcB&FR*7q%Bh+q%q=4+R<8S*D{jqwYhr4q|iS3cf zw7ykiZWaq361E>c3LZksVCs69c6;v7WqSpSJczFhJ7ZYXwf{UHjZB4?y0 z&7gc#7N{_0B~ewVS1;wi7@N4Keb}C!?o%~$pDE2W>pJ<3yb<%;z3cQ9b}+0<9WYov zbpIBR1}jT<-xvt0sY(Nl8me+qg||HYp+!LkP_KC1vS3cGkztPH(y@HIux_o_c0_Qx zxvskR+?=SWuRF#Hk;b$brmfqI+tqM^C^Pk1eZUv@pCs1|c3P~& z0y3K8J4+J@x>2d%d^RwaPiHOBO-WRid-aG{DVnrWL&CRT%!gkt@4Oy`f~p+XwkXLz zSrC!`+mNReO=9<)nTGj*--+#%hR~TF^~p1PhEwWd3T#p2e_c+XQelQN7kPG4ueKr*_ZD5NA_Xq`tXVMvetkEtaH6Y9K< zb`t246NQ;K74$VW>;s4>*nIIEBglFzqtJTM&>?OLQ~n?R^LZ>lL*kz`O#RcUK*Reg z9uFS}n3vLbG%@V*$pW<<)o{z^E?(MUvD~o_7fc4iaD!r*BnZYHr;!1X5$0Dic?dZi ztT25r=58^6g*1w0p$#bS7q);e8Cdb~F4Z@ufQ={>8ib*w%v#l@2WGB=)<#fAJ{44r z66aFq;$I=}=ykN%qf$i^4D%=Xb4w8T%M;v2_XScnXn?}Irk1h?Dmhi2)6gGs+(z}= zyfLBfOD;bb*lp0m=u%DW1oihh*bp$A#&MDiuBaN7J9;eo5_3Bl5^3T=`;n_T%9A^P zKv-vO(xrf1jx%Y4*LZees_x2@@7`0u_KB9OS}@%ag+TE!t8 zBaQr3G!OnR5iW}sPAaEcigwYK=yVb1jorzw979vVlh)HA70Yx4aM+nE=yABbeEzIj$_m|%^;`pE4=dy_^XzHdpCkKC-u!MX|cOJcRlpD!_fve16P41yCV13=(Y&b~Sj!4raS zQy&QU^BheMz@Y~`oZXupu(Hxi|BgP6Oa`T4OTb)8lh9G4xQwh+QZ_}kA$DY>pkz9F zFcHdtIdmLOQ5>Hzfss+89eBB825`R2d}e>O2)2o`X=;EQ;z@Xwh?#FXd%Fx_X=kg| zHaEhLJVWf;jcJtc{H@}36Won^(6g)QvM+sQy+IHbEH>+f6xU*n{<4@%#El_xy?B3; zQY$9r9(>k>AZR*}&%f zHAx=WqG=hon1*vW`Dq(n{{OfDQ_%s#z6d-dL-yv6UQ2vd>83zfnzjm-4I32#CYo?? z%LW}AumFC#CqIOif%60sZHT6UqvaWH5<1=hf==za|Jgl9 z0+G+|e}PfXQyhp}MFzQ)XaK4-I*=X(1RSRW^_T~tB$<ss3pV+($Z>fE zP?i>WV}1y`P*K13IV>`hfbvSse6G8?w;&!=`R4JBiv?fmhAxQO$|we{RHK{dfOor+ z*o{!}uxH|T`g}b8Jp1U#!^T#DX94% zP)1a$TO=mIFX?vNt3=ava#p@S@eit=UH%e|@&K!QgG#TA+$2~A^=6!M_LBfi&{3rU zWV{ij#M;b&h+`?Bh)91rEXOKv@-N&8dULDmbo|Ei!&B2_5(ceErkL7s1+NlJTQP(|) zdiOuK!e{%6s_NdVjMZMMyzX<$%wS;qK)B@Cfv}ILRVA))l)NW(OINQ6| zbb6kkd6{)xmmL{hyLV`b#59sG9tr9l*alqwwoSKk>O9kv5)#utQ9Al}y0HGMXJ|%1 z=@%p&JMEkZ`;>Cm{bwbbY5K zvVUD%F{OJnBrmqWYfwRg-14|0IHNbd{SpkPgwTTT&{g4myjkStmOz|F`>4a! z!W)#J%%6Aas2R%GGTPGEjpK9%yO#7)V%n6Hv>nGw97B;q7+5&UgvlB|privTX&+}! z{=KD^x?Sk<*U{Obu(O-OP_eJJz9(a2PI6 zj7!*(|Il*Su1$%89UgPBXt7PICuUb18p%g!*MXk< z7llvlz^jV!q2J(#_T$UfY~`_S_Kw4oCXCHX$*D-b^PBXn`n(bRZI0E7&XxC97*gKH zoI$GGE)ky#L^4bzXpWY|Ha<9e`h0&)yKL~6-_&f`v!cnn1@YrHWfMa}a$d`y;0kx9oD5#8&;qlK#);EDm*= zH*fT%QaKj11L~*pJ9aAToT!$KxnJVzN3t*0L?%eB#vGK&bhCb+&>L2+lt3`#neMiA zrB+_*vg?hiiNGNFYk~$H)`x)rq|O&@PF&ue`#W2SsPCYOi(J>q?@b*G|4B8d=KxFQoMrQ@+Z$o`b=1pXb5&^) z1Tyat1^~(eL*CW0r^oiAF?M(!tu;DWm1<6+2ZI>kVi5P@VyMGX*-eB;-(1rcKKEYYNrvV2?e^ z-m5gBq11v*$RP8SmKp_Gu@&yXBXLR1!}D*Q#ADx$%(o{O?0dE}`KYJS zClWSX(is30-mG5q`sSsNm0@%!LFmCfxHiZ>I%|^ZjqTb?FAVwvX1ai9?b_T31OM8jTmB5t0N`4#pH7l>A5QNlqyO3r9X4IZklGe zsw!8fWJU>Is>O{)jC;Lt!R#r2I^I+&w@EB~{m^yYb=00ek4@Mu;f}a$WAwEC;|=SK z_kXM@e6E|I6v_N4J6=;`HBT+(FLo$666@*I<>}*dw>7R?i{uGYeMsKB7F&IC6+Xh- zX|$_KJ$zm9rMvTr2Sws^PD(st-+lec!~(;TdF(?!*aotY74U62{4AdbCHVkfas5y?+;(1e#IRMuH6YFk`r3-ESmd88X0;<>~RB%$R z9W2DG@?(=7<|TpPNyp9=aae4!gDrgI${iI=n3;)&uIz`S!oee3dzCCSftOp+sS|VW zQV0ICWj2Dt>xJJ+-r)dzp@+J%PPOrpJ9nfXaIzi5p};}0IAhoq|9jx3clJV-8uLLb z`)9Ax;*y;kna>^p-1@9ymG;vIEEch;tf@TAT8-_Emp4TC&l_iv*HTSVLOK4sQW~f? z3;F@_{8dyc<~}4uC~;tw61sU0q_!Xz0F%?mhBp-$HI7O{P%Rsj0{b=#-yX^ zocU{7SNZeP&V}P=xjX+7^5lDO{EGhAY|TnCI;UmtaroCS@gAQ_0?YiMRO2hF%SZnP z5cK1TH5$wOBrN{~{i~>A!!+Vq}zEU?SEzZFjY_gVsN9;(=#% zHE`K`yS?3NJ4UqK(AyAt&vI|!Ix_)-p_i5?Qor$gSeRVR{hXe>?P=@d>A5+OkmmdX zt$exgT3d$4!L0EJ4RHK;r=)RD`8Fu-w>*`RaKkG;N`nkeON-)?4nhB$wR?RSH*sT| zQQ(gC*>B~yqv*KPz4|^>_A&f!oA$(YxxN6DZio|#>9LPelTdk6L;UKA&}3FHO!ON z*tJ8bYR6#ZQKlI@M@JuC?onN==jP(#{AC<`HafjA^P=!>QSLfl)Z5QlXZ!es8Y%IG z|E%s|nV34t@u>$uW-cR(?Mhu4pU_Mqk1LGqrU@FtEy%{ys3QMJPnsFLmvnnTSN@AV zc>cS7vo*-vD!hlbdv6ec3PVE|z+rVD9Mi*Mq0{ns%8KA$3lAoxC!mP2e%mIvP9Su`k%!<`}H^L+at76XTPVt;BT9~ zoF?Ep>_t-&nHTPpSsus-X4f^HB#PY)ho{K4ePfQ zcFJ#O}z;@LRK96z7s8JH}&jG)C(X zKsegIFN93ly)Of$F@a%^wNj!8H5|73^uMHby(S)w!tj8NLI;S$$pX3)L;*lgBAD-k z0tXF|U|Yd{kzar;qbH>MzS0Tx%hOmOPILo2WbvAc%{mXP%~nYq8V+VlF9xd&;=82oIy%#VVV0oJS>gzAeyXa zq?;EG=Dkb;gp_Y(#E!iT*pjY3^hiU~;*0Ck*kxSGJ2TjDOB%_yF~z2^9j+sYTFCV$%(~2&9o|}(%_;MFq2!-p#%>BuA?5GhHwQuGfVx@oDN+3n~ zb%R3i$tpX%A!1Q%8V1oCvlhn3G;L2Z5fN?CPV`L{hAzjqubPrEyUbq_PH@80NAQ4M|Y~R7IozU3JCJk z6!7c2%~t>?AJ~g|8x0eW$5Ng0mm_pf@wsN2X1Q^Oc5=m`vU{5|sT_ay`=Ei+Dv%pFUnj$9KJe&2Idr!{hxRU=&R*X@Qh#W1{sO}F zh!!Y{xeS!))040@Q*OqbzA*j=2?7zhy#S!OMC>UCIw80O6oQV`4nW5Y#WHWCv*BHi zjcblr3>s;yH|I@-Vh$jlmg~@3udM4l{FQ()eFZ{TSeC*N$ne#jmXZonsof77dSuz7 zt|MiNw&|a~me3y&Jsfy6B^wr2+X$edOeJ$5??bI#T+e1qL?ff62pG~^2GE;#WN zt70Ugynif6aqM2!PMAdv*X{jiLh=2_y+cdn`DxGLc**(n?X)?2E)l0*=HDHg$ySKt za@Py__@O1DM9G`@!29cxrdF4oT|~gY0DzP4SngXc&f~VbjCk>X1|+q`S-&WRtA!|) zfJ#6Iu22-Ru?Pz;X@J|pL4ILj?i-{7NiSOGTn zAY*oQaMog@vb$x?S^ihzR?3K2M(^iex??b(Btweak?pZtzYh~D`%4?Q+u$Nr4anj{ ztm)%QbacZojaa*=B;u%L=)09wKM|ts+BZAStH0pSvj@xy!0gHDY40Hmom~7FKVNe} z6dl(3rfzTAuc5n%fouK*0SZe(ue9{QR-aYI9bb%?aL4=3Dsu-;2F!#^QzarlX6AQfrye z_&NCR^}Be+=80Tkwrx(%e0%ppS<%dX1{SB6E0s$Br~@YFU@G#n{Hx-1 zcZ^6`=|m?rmog6ZrGdVsg(X&OlQF(rc>@uxl4Tq30R4c_>g#+#_3(JpfG_V`3#>7y zv8Ssg5%bIq8U3PK+8`n}g+2?bZueno0Fbovsnf}3s0nEPz|w~SmX;5RLPtvd^A;$e z^Zr6z$Kez(g4*K=1Y7h1QX4YB{+l{x7^jU0pPC)-=&2eFIhJTz(eVuByMiipRFJf` z%AZ!l>!SE(e!i`{U{FTN8-d79N6X|e6+)viVip2}WP*@pq{_iBD*&==Zu_5f+gW~a zt7;%bfBL=qYvuOSSl{)lCUtbu)jXn`TS%Le)#4#(WR+@@09e7QhR=YKBLfrY{%;_8 zdoX*9dzmAxW69ut5KZcC4Hf`lUTLrTch>z#5aiW$Ma^OZ6YK3+J22CvAlDluf+$84 zpbw`5;`sbf7Es|KQ6+Rr-!;mY?FBc%*>C)hD_;b{4Ra_cmK$wh$P=337DV2;aSvg% zROf{P;C!zdG=Q5#G*~3WOkEOz>FVogv)U!;`lFC-8|uhJvmL_)pM*0#tm&GMQqEoke8KIhT00lXG~q)E-$#9eH;kYq6x)z$)bGT{IbQxu z-%Bs$P54@@_W8G_F;^Wv-Nj&^w2QmNpG!#byQh$ZfEF`{jh0U-|rd z3R>Dxd=k^<%~ik~Zv08QUeY0ImQMD( zG$wlY%mx0A0<>V(1m8!~^%oyd=+RZ!=4{v8U;LNud{6T00Vs4qXWaw}&O( zHN3}$qV1E@jHp@T9anfoJZihNX{l*1bdXebOVjPD=HRcI#0fJ6`J=?6ON}ar(+_#( z0usW>HEA9BljD2EL44KY$*$BBij;D)24f^J21_#P@U!fh^0K#*I^-2l$|JXOjfukiS(UWWcif#D zDMz{NZGVoIs$^cyGhUy^qojHiL$4Hpv6K|IEs8&-vJ z8U&E;#LI@-cRP`jiu7%=4h!$s+-5u%-@*zEOieyr6)#u?UYt(Kv0SUSOqw=z{A(_h zz%=(u{f)Wy6_=3TWXp9AET_7&41MuOdx?lmg?FVTDTJMEwB6NXVtn4piZ>6xAaJI1 ze$B7*AY1D3mz2|DzDnC-+r{eb+~i(mMwPCIpzqCZ!XTsp*0iMlN!RTVhkc`u{rRtm ztAhmfljo7g7i*_OBKnPM*8%69lyz;!S0=4*bk#XU-_0DcGl~bQ-#N4;iVcvg! z@0L}<-HQc;maDxSJP&q<^jQ3SoPc0#8JI!7eC#L;y?zZaM%uFF_9gh}*+2z{%Ovutb4_8KoTM^!PZB zvf_LZo2H2iRqUueU=33N(^2iYr)o;;H)SV(qo(1NV+_|=IBKSp#b%w?ht5=A+c!~u zvf|j8B+bxE0n?8W57RFg+Ns%V6nw$#G3(qsx~B5G`)3ru*dW*;=(V`U5xd zpF%Hb_n91k_NnKEjh=p<`x#Oc=Nut3tDwYTTd;6Dty02)VUh(X8DyX0>}_#2 zkSup~o86n7>>%!mmC~uv3daj~@Aqt4HpbQUy7AUseZ=rA?8#V5-v;5-DE28O6b-#=DHlxM%d;0XubzXM0(v>MXeN2Dv z+dwhA^>$LEK)?0TAdmOd_>}+Z+NfJ?*WdB!6@hX_9x`Aqhee(5_S%?mLVQ}t)ln(4 z@<{bZmOmrKZ}C5f!1j*aTH-`H+$2RRJ`7a;eeml~c=x?{uP^lNYU-KW_m7CjpWSGu zq=;L{Dz+8p^1|){DufLpEuX_!6i#QALGq=RZ#PX~6OGzkPwtg$hGg=HElEv07upiu zPtWeUW_o5N2-7Px_`2K47s~%NelY5BHCYe=nz``+E+%J|j6D`|p4FpPPX_{LkpcL~ z?0d~|7+8N}-iD zyErCS_iW`S_aA>8jSOlu1<#c0tR4r&J4gv@L#zaitZJD4S}Eb9e3`G|@o~4==jwL5 zUos3lpA`sLKknR!sV{y@<13*&)*GyjH8!~DX3CriBWaIsv#Sj>?Z~`b=Ft4?Y;Pr& zc`!>#_IJ9f?m~iG;o&Cq`uQ*tVT3In(3a$hUHF{o1o(1w@{qw8+S_o|(46?jU(}?g z>lA~hI$ncpEFmRYXo$Zg3Dt$Twx+g5$S~oXy>PlpX){xjUAACqIGy6kCoiMeN7nDS zPR*QnTS_ObDAH`HL=gT^`Z!_516k%KYu|Q>wF@6+>O?r)AuNL}JvD(MilGG|O6C#h z>1@-s=vB&^?!Q-V&)_#d@qu4pM(5SLwT6Ie9ov^&P#b7s;_Blr3`OGk_2(|TWkRO6 z@M-qB(OGDsmwh7cYk%0(@fQ)a_LO(tBzsg>9l|9RJMKs!hG#E zmV2XU*T+&q2(cdvXZ&mXe%F=$Re-femX^WvyTG=q*Ewxhb1|LwTcKxqIk#a*+w%ten_=8G}m`H zts5&TtdT?MSVU%h!G{9xBd7^Tq@x~g7-}C)g`jSntoi+~79cz zYr0ckWT{ibtwxMjn>8~;7s5V;C0^M>`~4+0%asb$0>{5^I=(qlVF94=) z!}ikVzWwpIW97O)kznaW53E1ytQ9@0N97{+?G2#kS^G4FVx%DeD zF2enO9|*6D23NuBXs>5v#1B7xOTCu#INw@IOvpDg#X49mS%U+f4li10ov6CEy$|x? zOI@3(M)R+#!tt7y>?XO!p+-8TY4dnTwI1_$TP^NOtEFvCRSjclX8BSE*b-Ll!v)z0 zpMIu2evdny)cD&YqDIWgZ4svGSGn#p$7CvHP(VM);h)PQ;!3+F%~RfP}yLAhp^{9`M2SH|-ky3>Ps9Y21MTs3csLU@hFaEH}DEs{1^91J_MXGYhp z*0&2vcC3sj>SiQ#)UWIbGFqjeH@s^|>-#vs_iJNdsu(Hh3`M+$uhquqy2r^M1k6rs zyF$JWIA|EBkNnf&O;tvS8e=shg$fR_6Od48S=+lhS*!w(~bf9w}aSLw*^ zv$}p94vsuI^8R+Pqtsc{{hY~zG%izm12;CYuYsN~y0_;TeOekB8S$)2##^~Eg2p+m zWn+hY^jK8%*t?Tq@78rPOlQZN`>CtPo*8aMU)?;AA(MlFrCdKXG-RLk#%I_KCCyD? zPdP*|$6^8Ok7jSy?Rc_1H>H+#Dauz{Jbk;b+@gh#j*oHoJLl0T)CeO0C;s4UtWIjD*c2O)p%gS5A{B%O|EimcL}I` z+t$4CUr8TA+XZ6L9Aeuk8vaw)|gyPDeYNn* zlOGM)+1b%w1P|-r`uOmO7w-sZ#GEG)qWr?z2784fXjoGJ#|6-^cFR_q^img+thXdp zLtLKc>pMMg0J^WkVf9RCW4O$iSPn9De+;_>P@VL}-;7ujl_&h8$b`w>-j zUB`D-cP@UOKDDbe`Hmtwm47c@iKI7yv`V;uU@oG5LI6%ana%$PE@^CE3T$gMQU=Hz z;?%_}8;Z^(#qHX315FlBT6~$c9N7^Dl`VIVXCccj=TegVozNw{1ju$8qxH}j-aE_` z*1Up(B3=kixhD7C&pnSjDeo;nkbWn#e};PPR%u5F<>b3fU!D%X8F;?F)0CF6kbP zFpwBMx)G2@x&|oS-F5ao=XHMAAF%C-`?>GydS8=m!#}=(&35~qhY;W)5;@kFN<$Ba zD$2yyt33(kpuxj}EGOFyQgMCY+1(bLX<6KeP`?7rhePZ1$4L24f+(eD5RFhEJ$HVN z!W{SEqlT1VdE9u0WhnSW?6X(zJT#U|Hc0FdxXDs@+ORj@&C{ zFaqejj)EErqm`1%fLxLFw=LuphzDNSvur!OtNDfrc5nJz>G1f1$g1;td76j`Q6=;# zx3%bKTD8@d7o0teU_3C_SG&#O<(>DyXd@FgE+) zN%Z}DQ3Tab?R7}RHe21CW&t(JPusZ=RI;&{h|reue|Xz2r@1=c<7h|c1CM8nkesjM zn+F&*`OkSgy{(wPVlLo7iuqzmsEk6ZRHfZlo-akmRSVk7yzT>5!IsVP?uJ@nUU$f({`%&E7ojvXCwoV>^`;y?_)SsOEHjh#{^FdhmC(^KRkcP1!!5O#o4u>9{5L2`6fm+X zl7SXtm|L*Gcp=K6O4!2q-+>4oHK5ilO8zt>(`j6YdY$+4Z~Lx`?3`u?^74WLT~Tpz z&rMkojK@lp{|w04YoPO_JHGuydW?x`3F&ahl!sOLWG>jw>gx%sQ~S}I=1$I|k55}U zx3iI3p$E{1{QWbp7aL$te_4=k$MuIFBdw3Or(G8ozwd_+G-`Zg=~F9r`dgAG?lg7f z+Fg`99_w2Ng$*w^OA>3Swzt4GoRMw;BwN`FeR&uJbeoVs|%` z;y4@dg6FiPksiUm>-5>S^&^;FwcE3^iG}4(&!5!8W=AgrFWZN>Brv68n@3<#^P;0+ z6RZNpRh1!(2(Gb`hGu3Q9%}}eni`muCj_p*n_4r;W1!DF|AWss|FJvur6vQ(qtg=t zkdTeZM(2C_!iyaFA~LU)ERK_oqdNCn(_4LlN_3Y$9VDq^H`&Lri!72jZw-e%YY@tr zAZjb*iKrWti?Hl)#&vZz`R%0L& ze`dbA^g&Erc5lWTl$5^iPU7v%@Z0e9SJTodv{etIfoq=kg5sxBDwC)QkHS{teQ#Hj+ireLwH>672p?=IycZV@AK zS4$S`{rQzZKYw`BoTc~8Rk2yvw!e#ck)gQ%!A76r<`KAIvsk)6eR;Fnf0llpX!~E& zDD2z%J-i)&&+d!BNuY5eathF;k{YoQLWwG9s7E^Qadb~MEK;RW)XmW%(339bQo#P* z#~x)=)eUF*a8hs0if!8Fk**k1&gir4#2}0ww*o&(dEbNz4a~`aa}~J0pMy{L5qq0X z4#&M59vdu?z3bor`n#Pi;UC1e4gTBezs^yI&8R`kZ}ro7T0|$eDUM|6kx%#4L3hzK z9-@}gh5cv+$Y)_S&ztv4flIga8H3BU>t5-|F0+ZgU$U%^g-yL%>K)BnKD*i>oFYk# zam>V@lSBhHqW58g{L1EOp6V6w1@YDH(;l8lY|oW@C=pY}6Fj0wMEuMX5cHhhUN%7` z9^@V*?5x4EvQRqnT?E>+8*K_{@eXu3dD{PgO$`CE4Cq32KaxVy@(Y@))uv-SQpPRo zTN`(+q*W-8(wO9t=@U{l+n>RB9Y_*$qrQQ@EAK^YVICN9$6kl5SeIC=F7_yR3r9aK z(o}^e{25qK_q#~9(#a=C9#vn!!)}%E&q5V&2dw?~b-3nfH^rcYAE?tK;g9L|e-H6a zP@JC)VfVINK2skjX>(G15=Z8wVEliMJPln&iCKB<3LX#o^<Dk z?m4g%6F-rmmw$PI_q=aJJe0Rjbo=P0Fg&htYxpm%lJ%0c+9kX5RSN7VU;B7L4`vFavSx;U(j2+M7r#d3>I1zNzSUIn zmi(C(c;4|t^1q)bXwrp&%^ne;5>?^-KjkVJ$Ebn$?!vm2n$S<2yn>v9wH=L(njdw5 zgw;aa{70=ZojL>a}`PJ^L#|RI1Si6eD^r; zo#WVOwLwgjchQXOhNosiz81p_{*Wxc-@r5KcN=Hm)9*wj=6t%_eI-xF_PUX{UK@Cs z;c1}O?Y#$%QWX{IZkoqNdKynPs@vTE^K0Y9h-&TjAt#ZTM#N*!x@%q_jaoZsiNB_% z430h{Td!x3%RV7n)V=H>QuJYq1o}<{tXSaxaF>r&O}WagA8ScDZoNC@%{aHP=bQ+) z`tPc*FVCs-X%fl*sxk8geB>&%H%A>^+%XZQ>uD- z;r;oX)I+RH)0HRI=_w{ve5oK{1CY%Bk+d7jc)g1fV3J*muj?UPeQck#TJJC;W76Yx z0ncC4XwaY@)BVv__|LR`rTpW!e-$-m4eU^`Bby1o86i#<^*>*tGAoY&AxvbtVcKcn zA0>`1^bLsB>vfS)M-n!+icDxpaDtR{cC4gKq(y|}7IemRbyO|hX%^KI-a4W&6_#1Ea}@T&BFRjY*)gA5rm)a%b_b}G0OX8oeHjbR@UgJJyLU<}K?QCtiY zf@pF{0c+UG`qPXa0GsJ@nin`EyWo_CZI<2`A8`+kfP91cbO^LtjsCpQYgMi%I9p1; zn3_6+`8Ag|1VpLTd~(|}XPph%`AM0sIN;lvcF8$yr4wnALJicfn*U_-bo3hoAOO#` zN@X^l_iXS>=;RH+#dzS6e?t*64qzLsS&0#mv# zhiIU{a;Kn6CA=ql;q*d0fB)N?PLb4H(W{Z=rzPp+s^_j~nJU%m6xurY=AsNY z&--dgE^#~`^3HB1J#(P1{m|%6f)Wi|A|Sndk7=hzNeKL>7Uj{x_@FHn zZ!4uOKq|?k2Xy{)If-(2FTLKjyJplxsyFODQD?!JN0{=1){~7Dtjd_OFPOjv0ivm= zuEdl$db+RuEK|*Wt+CB%R=xq!#tzU(YGhC|XI+AgO78zyuh*%u`?T zSgGcH;^yT)U_2U#0Grk=UqtLPsxP?*6?qxgiZgG9WkmGy@sHo%~p^mJ0b-4))I&#-%Wy>7XtPU^I;tMx|TxU_;a zdFkI0EC=HE)OITm9$nmc`MP34M%BE+)tQ#_-?6N+<8Aw5eWz&|iT6uV^`ZPL&v#&JK37(hCS!QdYaziit>1Zv&BJEZNABG}_P`?475Sdx#F= zI4$zk`Rq!ob@)H+{oO4PAy+bN9)>s6tjjYge!05pPxpE>1$J|t6yIw7@M72ds!W~-P0hK_~1I=_Q4`{vu3RGHBX@3 zjPg>t)nosAd4Y(>a5Z4Up!;$GfWF&#%X10{3l9bn%YbG0HyLxFoYtDTo?eOn zArC4xWHi&KmXM%7F@9xA))d9SWXR`nYcOqmtM{>lxkNB38fF@a9Cq(#)JEL{TUf>a znFS691!HiAiK!rB%kO4=f-J6~*e< z0PTWJC?el)yFC`5o;9Go+}9Z$?N($hcR{`|V<^CB4RzTW_3bVP6+-c)Vry3xP955e zl@wcqPplKr717a|OKd1J=&DN4bLhLfG%GuOkK|397Zq=-=A>fpr@!9@1HAQEUMDGe6q7ni=ynHA!zhsnY zLp7`3^3X%t1MjZIpJ;<)`f7Qr8qMkwNLcM6ZFbbjaqe6=*TnBiN1Jghp;LI548?F& z*5jq>Oao?*#5@CN(?7*ZsJ7J9@8`f|liSslt<&Jq9XzaTndHk+qcBSR5_S^i{TIaDmL$qVjM&(i_DJ9QzltrKbZ@0BbbFue zZS7o_%_|ZeHk~YWIrM*?haqgVhk^;+kI$!~msgiG_h)D&8{12*sb!Y${CASKv?uQ( za#+}|!zQ1o0ewEFFIRdJ3v7Ziap9{=zXsM zNECnS%*5-kZ8WcE5)kj+SaPI_va;HN;J14@sDbonxR*|M{Ek&tnygmeVn-UA{x>Bh zk(I$oosuA-Y-j}46!JuMQF z7uXV_zImfK03U?--N*|NS)0P+b+G!0r;ULU*?dUIDbYRHP$Wzf1(dX_>xP*kO(x1GZM1uhDZ?^k-?b9ha`TKTc@Z&*?b~R1 zpYJ8Vd*n8@3KYE=BEaGtCmn6F5p(gT=-D|Q{1yElg*uicPr3^w@%2Gng`GNC#aiQ% z^?thVTNa?SVoxFM6d>TaW-Z+#zF_rLf4s~Q2kQ84#OH9Z?Pcu=h#1`pwiEL$IH4YJwJe^`e|t)XYUqslE%m6^)KmQGwzM< zzuE7V>4qm+tahUwI`I#_tNxzmL+LpIl>_m^@BHj|z6BY)Ms$+%#Ijso|7&Nz#|Lyb zFqY`)<~=`(I2q+Lr&q8FAYvqI*4;0kj;@-!d{+ilNF5g&>pBGO?7k@8n9BhlQuaG@ zhD2A8Lv(mH!KC>wf8ls=_syE^nVX|AaB)q(_sLnvUxUNTL$t|{(3dm37YxLmo+Iv) zYOG#`q{v7pNp_!?gu<8NT$4-YB^(IVq%Mgl0dGrBu84TL&%b@i>&NmyYA z%gYi*f45f4M|4MB^jDgmB?iVq)6M1SvJi1+zrK)_e@IA+#lV*d-Rl{hMD~8_auWAD zbGB(yl`8G{bz`tX)I`9m3#WCA2<+qI*>|6HXi=#1*gc8clnRZ(_y{UN1t)OY6Ow3X zfgbc6UVB&{%lI9fBH{ZH%QwUEL}my}fcM51oHHR7Q)09)ZwDwq$~M72H>2`CqY0Zj z54vb)9^QD0Cs6`~k{ zxl(Yc^*W*?s|#!Uu%>P2h3oOQ#!&r$Y-V9 z4?mnoudl$`hkw($?BN>{mb@&tgG2E=Rz|8S+LJOdJ=jX5LB2w#h z`K(BHNIeP{uB{pZ7>fOXdb~8i!<3HGbQntJ)oHe3nXfYY;E;I7DF>i2+X@z|y|mtT zn~xHeP?N8-Tt98zH<0 zJk||Z_`Me?Q&H&7_`b|jRbhec+jrKvk*6it%L@_{3mCy;spvMkBI(4|QLnF8Va1jT z&f-I-?cE5vzqkb;1XoU31mP7rIIIJjl`YfSIXd>FG%rgiqXQMkJ{b-+I4K}nR8V7z zR6RbI9mJ#L2WOl{i3XKYzxPv26w)Wp+XY0H((_{2WQ9D=&5h$qlaKw+1(-Shv{YU0 z#B$j4^C%V%igX0rk3;D_mGhV`k|LRuJadV{G>DmiPsUdJ%f;5P8Q8v>`r#2z?G)~E zJ~(a$IPu?e8P{!yod4`n^{u3k7l6!z@)6;eE@?yv6Oq)h$E$Sg#qyy7taWrPdO)zN8?6NmYY{-mbjbF0Fg_U)r4J+Yjb5nsp>U|xh<_BDVB&p(URP2-4J?d}7lR!=k-f&>qUud_0j0ygB;&wbE+mG0HdSm4T+fNH=h+s{rB) zWM6FcSjljBL;PRz-0x`B&*aou@{C_)N;KXMw&}Ay5A43$nA|MN1wO)8nvAAw9udMA z%b_=Zq?7{%T~~Y5-M5Hr*4@ryGXVev1%MS=BtMWm6+N$;!z0=W{?&$493eF+NbjW5y+_-NdVYfZA@W&pI~5isY3#A947f2LZf~1o$zDY`RJ$P|BAk6kP~YcOm0R#mBGG2_qkq z$yVU98^^{^1~NnWc#@5QJXn&b)31@_h1%tu39knkUPvvjrUneoY(u%LqJJqE!F4$b zoljSEld$EyYO|F`!)~k1&qnR_O*W&R$Chdw0wZ=fjOFhsDtW#`=C)gPi@%ai8~;+s zQLp|hEIY`cbTW?yI9j9EHMqi#?hP!{<$>|`Lg%(?XExU2TGcfpP4qnmekEsG z?Pj)Rpz6`@*)k8HiM#jLtY7W4-b))jA0&D@a*Tb{NPGtkH%LF_p%24R5a!*DJs+_p z?O|<@3%Z}}pJY>!>-%Sa`GZZG^1)B(`+&REg|Uy#fRj#X!1W1Mq8h1@_W^jkUqr5d z+SZI8W=ePe|Fr<3+>P4$9)32rvlEE57TfmDXPd29nsU|70W7j=z(c*{SbcNt?znUn z1qgLipfD_hKMYTvsuPv(KcN6fT?8w(;|D zbxbrF(8qpz8}3rJ@-Sf_4lWRR&TmG@E&K_k#9Ck9jKXwXHf1bc+)nS!0O6O8FN^U~ z>Td_LHw86?H35lR7qUE#>6Bopc@@aY=j8DwtKH!Pn+kW`45dn{Atf#rPchF_c?#P1l#2H$`W;=1wdEg8T@u zC|yk3wQQyCx6jiJ4`>gbMs06;W^RMLUW=p$wk;-A*~i$@D5>9fO1QKmVz&;rCXT;` zg@sA(t*Fwcp8>Jn2$&?}_ceyU1rR0UrwH!Un!jzzr2GO~rZVUj(>xfKd$(G5dEuc~ z3Ru0LGRxMfBr}G*l`#G=j!mCr33RCfgL!+f@oj)8+2nw*EEinqhWRHa04G<2>C?jN zl_p?8O~pf`ss6aDyjNz8i?EOLc=}JP=U$WvIyYw6QuArMYspZCrfb_G3(UQ9{$^UI zzwoP^(I>v<4T0y+tifrqpaxdRE*M+|GYP0cdO0;}C+$%YSmNPv5%`zJx)9qoW#?{ab0(j*x}x^q@k!@SyYTJ(CITIC;n74caIYExLsfmpKa)v7xet` z=X_3L?Kf^tK2yd-g=WtT7B!%Ty^GiFVo@#M+qh}}Fs^&|M@;-C*ta-tWXI^8;4JPh zd6R!0H7nk}j@7*IBd#c*YrO4ow8ipu&&1WrA=Wb!$;AAp28xLi*R=ZP>-R@V_K1do??+x4tJXpmL~b$pROiAI6;;u)nZmhJE%3bskE9CYXfB zG7!s|upUFg5un+_>Qh8;R>5Ob&D+e;uZNGaW%Iii0eE6J-+Qp#I++Cu9+sV+-~8_( z^R%T>r&*I@Kc8WQ&;#mAB2|Kcwh`kGis&dXZB_ z)7j|4^Pq450Y*yb8^_liCnUrX*v=)WUO^d7GHhGU72vz(6v^F0R6aY~L?>WjGvI@5(#dD@)Z=8mX~`RmUJzv`dCs>Eurx;l& z%Dta;;%!9t^*82M#Tg)hHU`}rE{#n0nZ0$rs;1qE+;`Ll0wUdKV{Vh(e-8u%0Ty3K>c9$zSt zp5_y%zc5etY20@la0xkh+ly>F!kzd+J?Zl@JJ;Hs+DtYh{~&zTMxq{?^?&U^>OLIl zpB@fEh=vl;0`cv}m0f-cqDO~2jOr)4?L$y>ChwoS+bDb9lj=Y}OVqA^E`1M-gjH>5I7X1ixO4Pg+=vOhI9~;AxD;&T)oRM7COl$?xh@qT#G17068fP%wZJm0l7R zvDISjDwWdb#GhAV}#$DuVUyMb7?Q4lko zv5%)&-B00)ppHba=+Gf*7b8FQ!i=RbcAGG6uVSV4VwW-*uufA@Z6Lc>CX`kl9p|~T zyOS8evnd{Z-fbOenLw;D)})}GG*9K37W-o=(R94YVe26kfO{OYk^gw&^VC&FHMHb5 zd~$sV{5T`zZ+o+W#bH=yhNv6#4zG2|&``_{AABFsF3D5ri9Ho4veWZ^>n5O4@88s? zGG~SPWqu#HSk^6?67cif+26gzBW4u4D_=)-ethnpszM@^855qz1SA{d3{-(*&cA}M%(fZ~jXeGRoj30m|7ayzW}*_d5UAaOtJs!j;ie~UK*pM5|C4w#Vlh`&y%x$UJ- zX|vbF8+0%ytJY%+;2|P$ofZQJYT%bon|0KXk7Xik8qr0PWpX>EiFt99!m92~D%;aB zp^f9Feio4EuKZ&T8lkU_s)rsAomxh|=Yy=CB-O}g2{lhDJ@U(A@egl^h&0W=PYIZM zR;A@!l`;-KVg(pZyBSP!V=L4UfTE+pyBU@zov$z74ZW_{gO2TZU_5ni%m0Y+?EVK# zKJ3YmY`9!4>q166EXHZN-$eqx?nOzBAPUDGr-kf(vw%ou9fhGQH!6I9&~WJI_@do*%1-L&-@XE<+1du2iHeXee zCKF1Y-D@12B{WY_LpM&v3gN?|i-{(Wy%v%}$8R)cMn%D{wdAym7Wbd@0EjtXQnvo+ znDfPVF(wbO!!4P1X{N(mDuSlQxYz4r3Sv7HMO=J)o2KVL2Th>i>*Gw9U#OUyy4Bft zBqQsa71JOSMh3qV5GjOVcHI~6BH6zqRYm5SnblxPX(Mulm70#1ET!ejXICY2tAuN= z9`g%#T}(M1w3NaU%g?vkp#5rHErR0fu?!O>@dz6KihyBLfZsu#RnWZkA;d^-x6-;;mc_Pe#$Z+3L96;F0no=K5fE`75jU*1j=xUqP(B%psES+jZ` zFQgmywJCKnE5jlOm*9;{iP@*;@*q^i3@@+Sje$tP6ilIntt3R&{bh82ln547Yt8_qB4b4VolIu+;Jj#C~WKVQ?x*f>6V*gs_2j z`0R({DmNz?|GcHSh*={Zp|Hqm&rlcXanUI1=GM+G75Oge0 zm&N8@c8KeG;nWAS=fpu)WVUYba}Dczsd3Gbi*LqRQ)8`ZG9}>unoST4vo9UGU#aTG zwGF46ByA@3D+az)@ijQ8I~6>J+`jJoaz4d7dGv%$$C&wSq!R{xyE?WtOgN zJ2qM@oOJcRoQEz>@la9z=~cXs;Ta-jX*h13^N>E|Sakl==52j{_U8k1S1VIm+*z6d z)91YWi?OnBYum0A^Z6VElD>i^A;=}*Aa zfilxd6n)nGhb7l2G_T12E1(`bn;Ymgcdc!f#$rL_8R-2}Nu?m3s{k8tMp_Asmb?KU z;fqB#ri0MENmJKlMqHS~X~v8xERmM0=aAF?y&grZfk-lULBFu^(T#J500k()5IUq! z&#Rr2$3gRaR7#vfTyk~q?U_ODM~^P8UnuuZ*E?)R&o*jNvthBPUJi&T)Gr%Rb+HN& zP5-%&eb`z1i(_{^|4(sT<$XK+H&<1;N@`BiDe74s+PlB$Bxi-yzzGxNo6v z2bU1PV28)$M$)2KSNifFbuZ6#NCEJSNxW+%dJ~^(DE6?jtib2#s2uG7I<+;())}!@ za(sQWt*Hnrj!I;K_@QC?c^+vCOc@=svOf8>a8$nMMKGjhOZ7i{= zn=QgEo;N6Xazi}#)-Iz2Sbs(LQj}V7^^`=&yd5ZQ{Ag35jY6Lwjl>2iCg#ybSC9da z_W`6?->mp-y>==6$-2|eErL+3#N*vH7W$tcG z&V&G5;dFpcmwN_oc1qP2lqv32?j@iSvlfh%^bN;O(;STtSEVT!qizbi$?GAsz_e*e z{5SAw8?d4s`uA1kVkj;Z_~dw$?*J!OubsWaeCWSS#JC~=FA9M@mO#bfl%P;-upG2Q z54OY!mcZt;%a#6Bh4E`wA{8Gnkj+<64A!xmUw&%9)5qIz5<7oc+!Bm?BRC%|J)Ufu z;8BIoI?jD;{=>4E=@c;?OQN9qFc+U*8#ka1tJJ`+{4fvt?Wr$$W?x=B)r2i)i}a8q z5`?>P{K)P0ZQb=lc|59#g9?V$RlE(yclDfQ0(Avmt@MO=)xyh^@FJzI6JEhR*W&7; z4{<{CXH-6a(wbd(H?>8htTHnw#uY{56soGi0bTcbug4!rMg!dscHcuvOj>dH#m<`u z@^WVwzM`>uHl*g)I4PSl8Q+^^$+o<>ymt-8`Lb3z4q9 zCq04JSzK&SpMU3f$`KFv;bp(6;|I~&AIJNKO~k2A|bCAORI)1Z(0 z7xdKG4Uo7?H@Z)MrR}SE8mW0gBbZpJ_quiFcMxt$nlf39He=*6&Tb8ND9y{t$`Z-5 z%#>-~F2nf?-mxtv!s13H_{yF)V#N*POh_}(*A10mJQSV=4Q%yrA3Fs-vje==o&k36 zOF)FEDN=+4(GoI%JSO?1!dEQBx!{8uj7~dc2SDxy8rKH<9Cu+MJ>Bp6iaI=+yfoOXhmaFp1`tACH!oSGS9IVRfGONW_mQ>NoG~>6|4e!g zr_Z;NNbZZ6Nf}{+jXZrVJ_t1=>o*rDc=B~2vEv+PA>Y9Nx@T0)3GUN7&qn%~=h>-W zwV>tedohI9*`cfV4alufwZgk}vM7g_PfxAy2d5l$+rF_)8?#KRoAInxN4T5vq>QHb zU}z6SZJkXWrl7~_`(N<_D0gFMV!m@CYtK)043ci_`@g}@Q9F~l`mE`;c|F|Dz5j*% zdB`n)zU}r&H7Y6Yu9_JC1>tP_w_I7%x?k%53Sc5+=GZ2at@41(7*uI3EMNK!Jv@8= zt$96nn|$)Nd6@eO$QPcYqeCT7NTQ|hkPSu$Nz@==q@YS-N6s(Pq0g6snB{}z_jOx! zbA?3fT^e$f1~BWUh3AtkG1hp~fM-Q3iCkQ$N(vwl#`v!>dP{5iH>QI^>WKX{|BGS@ z8)$+As#^n0X5m13d80koTNL!l=(c$>@44)P^B?buF$ zEYD5lEwT3uTY|w_FP0n*T}7+U#~)vAPXiH*0JKDaw<{w)?M$H?8N!p!h>j$QxcQY4 zpI;w0tsNksP){|n)bY}Yq+?6Jo2i+of;Ov_E-#1fJKGG|w>&mbVyvB*5Ku7Q^XL~0 zYjwm*R5T^Y=Hn_PqeV*EaRbmZqjH6uQ&?KpnnKg)$E!fvcJoz@JKA5h>>zt(1uY1; zY=`J$^=iEZy_GLK(#gHQRhBiWqo4c9(+#X6lK-A+{Wd4TOm`>4zKM3b7jj>}KGBx2 zp70gJiFSKLS=ARja`E(U`Cu{9gbCY2>rg|Z`oSyc)Cg(yIyEY7{+BU5ASF6Hm=<~O zlVs>GI|O;e7YD!?j1EAMmO=ppTv<9i0AvF4t5Vimp~P-7i2&be-+(0XU*Zv$=ajxK6n>Kzq+|D{;Ne$IBHnE>Eue0PbKZoE z|CttDV}ze-)Eo*n>*vSmPg6%H%%DT+xO^C`e`9653eH|mQhZx)_;5EIHm4i0Nt-&> zqrr0x>GCgt9DlAzlW#oeVk4ZJ)(zr*93|bU()a8)cAeT20Lo?TsM6?V-MGR33>&Wx zdlzmbcJcdSTA$l2FIy)+hvT6L`HSn+lE=(X zz8v_5ih&D)si+d)o7#m5_7)X}g6O50Kice-NrEHGwS?qj07E|`FW9T#uEh-8jUN!ahuHt&Wsp(Bt2>gcsosK`Btb}$kUQJZEV z;F4pgK(euDQ))Nms-mLaY~nC5!9c9e>gDkNPObK{gUTlRygi1RZ>JTG8kZg)LLI6< zE>vD7t5;hom95!3Gjb@~pK@%;Q)P?m;XLJ=|A3!;%@@d5^o=kq;jV0G;DP*`DkWIj zKftALp0T~`Yd;j&UOQv!{`HZGf!(v@0&6ljB=Nsg$KnM(z==o4kr6##^tcO{&bT}RBcps>MVJ^d4Mk1vL0>N|jM|2Kl7wTs&*23CYgSE)x=0ny z_ox!6$YfIzRXU-R@h1qLCD+Nn*h&G%94dTuz|@#Y)stiMnxlZ8$q1+DP~>!mLaAM{ z3E?6q+bBOk|2i_yOtkg9yec~7IiNE^y5W+^7V6nHlv?^*aL2WH&GF5bs;@q*+dIqK zJr}NrvQdcqafD$5YG6g58Sp&NU3$5b`0mN2AI}fXqR;lI)!O{te%A7lAz#v{Ey1y3|Z3EcRFC2U7vX#4fkcZsJAC^-71y=HkxxA$DKAAKf&{ zQg)m~)||V5yFEO_bb*8xovtKFSt{&J7^3ZqKqK$~rpK}oub>`OOJsV;TUJgRU<0DX zGLGQ$&p&Od8UC-gl+me$C>8{_ONB7?*r+2JbKoXu>9Uno8b`|+jao|ISQpAHII~^Q z@@CJsBQ&W>aC+&=h+@juq}n(aljom?ms%fmCkK(191j^6v3@Le92PjCIPZ$$v1*3$ zMWcfN^rm!(A}bV>Oe-maj7-ZId`3Z!KS2lh1wf|_p8SIyN{h}NM;UZ%3ZU^>$g%HF z0%4#cl4;cy1nTT|cO3jYudr3JJV#F5v*%!z7B<_n-^CvXQsqdqnf1hi?7L2i%W9nZ z&AVyK)~pS?Oq?sUd|Jn1mYkvR_oHSzPHUcc!4hs&y%a!6%v|YUT26fKM@7omUDC{; zv&~)ZVRQPtz;!(iMbL3HxkQNuL>FwNXEWQ5GOQt%lTN$9k29Jy&BE~w3*}yB0_E(% z&I=a=wI4KKerU2Gc~FNv5QgO`^BdK&QD?M&DV`hO_WPP(pC*z5Ngc)|eo&+FhOL%X zH!@=GA6EM*!GAsWRz4ElB6VKyPp7ty{-?#$fSRngW%&-y{zjg%xu!TMr9Z zi{?kt$4J7`rSosCHW9Ff^MsJ$KPKR8ie(?O)3$2_qeW#*W;U}4VSkO1i`Kycq{wHf zV-)oOr8&9C03p3&f^s}|)<8Q6yS?bFG<*U(NRKigOPd)&oHku`oha^tf#@C(t+3Oq zPPMEaV#FIt4nlR>3YLSnaVNwxFQCMkWQWd#T% zEH5&Jb08pqd-J)&RYXHSZT~)eIf6gLI|1%qMDdvCktqNn=-Bk=bTQ%hGAYp#p%}Em z62<`3DIviq06vcd03j@D07?YY&iEfva=bY^zl9qsnc(+?(nUuTv}T+?jIu9!?7ug( zlo+T=Cy^PLL8E!S7iA*NRL!q|{b(U}ff8L`??gTLW-5ML;i>MnevZu5gI2$hovc*4 z&Qhk^*6O}S0VG=vjA4c%A&n;?N74e3cW7rBFJHps41Hhjwlj^W1{Yx-w0SqY43?U8 z)f&1(mXj0g6N%T`h>kEeo&GKrp0b;jZGY$f>Sw~9Cea+A@28>OC7w#Cr`62h`BbcO zY>5TraT){KCTUN`oqqH;!TOPU$nB_Orn)(HNDI0)%i=NkfVSp4K0cIbBouIFf7GBc z?X)0B6Yp?z>_POpy@@c=jK1Cs0kP4W{=lgEU9r+((|t!alf)ukaS`2x59Xu0jXa;!8$eq9on?EjO{e9Mdw>PH5^Ibg8AbUR}p2Pnp3ZTlH zMiYXbf*5j=y`ssTAsOp@qrrX(nrMAqTUhT9=sdRPII2HXFJU41A^$ch1A)LpjyeCz zK=!$qOy+Ya;~$wXTsKmC(X{{hE=3$Sh**?Ob{INxl=+(niAMvzfZS0_3zKPl^4tBd zB+M?(eWCe$Z{wTGNDo=_w#?^_f#;uv+=hj6d>iB^YA9C&eSAzsGv1F05h62a)V>9@ zoCGBzks@Q^5&}y$$cn?!BTQl#B#?WVr4S**EgRV30CrSUf`1sK-0!*n3+3r0H_gRD z4RsqtHPhz|T)*~1yJW!U%6O}_;U0F zQc}vnG7^Yf0FXW^9aFUE#nC3K9EDT&9kNQsd!4vMY;;_gk8g=FCa9ULVawnu8;A-Z zgmW+&9l*4~LR4Ob2`E0~$G$DYzf4fB!2o$4FPCOw3)m}Z4{u)#q>T|w?!g==G@1lhQ#XXs=H&`Abu^f-`9z0)A!u{h`r1L;mwMzZ?n9; zN=%Q8s&AF5PGZ~g1;q&!e<7s%=p%XdJJpLO?`39Hm*H$nF6fB`Fedm>Ib0&z7$AWx z`H>d*+l>9ckOuw+XLlo5n|_|qJ73R=&nFx|e;HNs=SDbwzBVpr0EMg3u;w=W`Iz9q z70G_G-l3vN+$#i>K;On82fX|ZQRtO4o@fb2#V1hwKqpU>GDbj)3m}s~$ETHW?j`$2 zg=&@ipFRLBooI~U&EOm9G%Q9z=ZSJ-DZu6{f(?g=P8(UQZa`=P;sf5tY&eQyiw=H{ zOG?7Xz>%!?W#qk(F-+Y|Pw4DrA@2(`dvPwYD|1-=81un=_@S>hgF-t$r-oXHKsT}; zHq>AMp-zlHG)ZYY%vnbZHWEEUi>*bCrVYUWPDrAM5=ctmqqCFI=i)~OhX6uAbojZ5 z2wGZn3_3xymmom61UlCQ*(N%{RAVKF7#`r|{NUDf0Kgvnb?`%(20i!A@J>vfQq8lm zqdOKOH(hpTC+@YG7X$KlSJR99=;hM#0>&2A%3tD`x`qvG;druv&7 z@=_`9e0OTA_Kez(*8#NVRBlMf=Sf)MAJD=j<_L_@%h8FdjUVy~0&eRH#5>JS|I_17 zT+pJ_>N@Q>ud9ou`#K~8>=m-YCJIF-pv9L$mH>dL@QK=3kR*JZXMP(eY@QsrDt+1J z@jAk-*G8VysLw*oSdD)**^B1AUp_A<3R%wvXXhH}d`QGK^tfMN_)?7f-S4p}T+Yhy zQ_JgFT`S(}csi`w>+TvHll=0-0?J1kc=n8FQK}fdWHy#psOO^AQOceAJo@E7%oM|W zY@pv<-fEYd);ZA{jD2BeX36+J0RBJ$zi^UE=ci|8%R_@>W8!8-8N5db*erAZDUc-<5c{^J3`Al2t`gEvhT(P?_G|-O*-ZixSY%^^MArJ_RP*#@# z;4=?Sat4BM#xV$lcqBtCA%)8-#|SY1fdzm74nhbHoC4z@I49f>C>LM~j2NL4{aye-aLUHB&g>b2BQW}Im){J!ZK^k ztlDFcDVl8G+H_*6+lNPFV17)8^8K>fGc!Hko}E?u^I|ybd7kPeqobzh$p3!Zj{)Fw zU%F9NkfQ3kqF|y5Boj%IiKHuPN|Ms1!6k)Dgn&B0Py*%-T|`0IRQawaQ1YNRNOfGAXmP;iIgvZR`-3;_LOJIyrmooeU>V`Kg2 zo^j^9+c9~&)W4_86@)@fH_F2k`9isCyUj**=J?^o+1JK~hp+z2>sGz`f;X*1ezxBI zVdIWwtFyM+D36TpI(?HWB0G#~O{djfd~N^l zU)wiXsrW=PXOAocz>LizDrAB%ff$ejKoF!TZ5?{y*wgni%8LeWG&`Zkn2ytF(*iG# zlNj+1Wvjk8-Zf7wRwW*sJFM=VD47%Q969~nh2A0;2mv4jSzQKz&ptHC8E_)FK#U0j zOoBuP!X$+uggF9%#hX9?2*H6-!5Qc>m%LSnGD#{jP(3UJaNxkW;6eZg#sudA3HaT0 zdjJ5zDW|>d?80DrIGbja#4ce83b`jRPHD2PQl^u5+1oH)baA*;XZ^m^+av97&2d@U zRO&;L#1J0Llw)eVet5dQd@?`LBb7655OBvE8cD^T{=M7o0f1|-`v+B$2|*G8gh5~; zBmxOaf}}{$p6#1rLZ~EpaTJEW=Q!zfDvBe7B`nE+5KE{Rgj68Ggr+F2AH!GpP&4!}zz)?4wA|q^&6Q9XrUSw7`$Jrh5?`DD~{vvg3xYe**w0<6Lkg zP!uGXU?O1<9AShI4k>4hAz%+a_4K#?_4YF^xLD5@f>!go4_`anSD2Y=Z5%C{h9nqI zVn%dHk|p>*f(s!4T+TEk84>}2;SJlF#FD@T#&?~+>x|uJIkrQ(%ceHVSO7ImNB_kh^4Lc(IaaBFl#f92^oPbAw@z2 zLqbJmskZ#|&zaZF7^;e*rV}O+!V1zPrgPVj1t^4L@PW*A;o0N$>FJpfgI%_Fq;JDp z2Y0`}JTfE@<^UK$Mpppf^A8_q92gc{00tmPaGFSfxkMNM;RFQY0yqc&5KIUT0W&u5@wG)eWJjm5<}xL0m+4--Yiy?PEF)nZl+H?98>|LZN;L)5+>;bxw7qwKRQjkqOh?B_i?ZE%(Lyx@v+;gU$ zjcIV&&NKh+udm2w(#@vRU(Tusyk=0HYoxPsX=|@)>I7o|U_$tu0Y@2C27pal#=B9V zXjUrMGrDDL>*n2ltDeHq&U3DK_^DTqzW#hMmk%7@NM%MR#@v;Y!mkbtSsCc;*t`vy z%37@=x=>kNeR*<@=;}y+5B2;YVU82!j2w-VpFa8|0DSr5pN{gHWy)To^UldzDV4;F^K&z^%W)7W5~O>3{Ek=5Si}A6#y9rFp%56#DctOe!;_PT zCSP5wOxGK&C}1P~eb2mb0st0V)?p$=kR^ePA*6hDt+V*@T-F6o=tq?m@l1OPI+0svol_&DXj zvET@>;8+00h`kA;HxWjVz)T7O`%XkrV?7XqY6Tu>%B z2*H6#~yy!N}-X{N4hj{x*o=8XkD4v_(!+?5&*8d z{l5V~a3&Z75C|b5coHQ^#L6PvH8DU`l<>G4;DuV(p)A3~(lwgI0)*h4GC?_T1b{d& z0e~1|{Qo48>uC!9<-dO$0B*eI(gYDzmhHfeh2q-omUTn#ecuI|nOa+Da!GOg#`xI! zVtKH-QmL%Yy#DHQo|i1vnzJkIcH7~AWK-=c6|&t=m_DoH{?+liU<_H5ZUx(NW@dGWSd({nnv0%Rof1RdrQx}a0~#tF>f+3kpM30PS9AXEbo6dmvkhF5E*0$aN@@a)hsRH2{>&_ zP141bY^VT?#!=)uRVUsz+u46$b|^(II(Kh=)5WRcaPR0kNjEv5tf2zH4G&K;4jcgx zfds>Xa3MGdM34}G8AgIIfhZDKfXq4LzybNSr4!HoqO!QSVfVX>gBy}uFIRO1fC|Bc z0FF2ZE)W3x{^L6U0Kq9A`OGB=#wL(P!`ZlTBJ%vnle39{VmYOzNF-oDwswuKV}Op$ zSJ1*rwm-9qai!8Yd+P|OWZ9)-<$mzohaPxEvBzL4-RjAEXo3r6g-pWk*r0DSao z|Hcyv00bd~(VGNt!MLEjK={t}1C+&xg#kyii>+oPn53$Tj5y~2VUlo6FqSx{LQu{a zXIu!*1&^bECIMpf;UE710RHu_E)E2dwQPiVv*idOZQ0bbd1A!M4!pX5q3t){{q74) ziO(LJJaBAr$ByxKqh>eP{2*BJ6W8|`18n9@L*cd?II)maRU@XQn!2v5N>4uj4_~?+ z0OEwTy6*AGsi~Q%jic*q&u+MO@8HmCrF-y=Bkz941!_jW^RDk#re?hMd=%gdulnSB zPTx2(RL-VIx3F={I2uXkFVd*X*Ha+wnT(# znf?7^CyyW5cVI@6wQM@2DjLBg613iMn{MlY$BzQQ_inja$|yueNhr@%?BNYtOG6`0 zz+QXi&#~P&|W2!&9#UK*WU2L>RJKqdR|mTC{7zUDbmoV8IEJajc0@Q3yjMibI8Y zDU(@wOV(A&IE@(w7O~{OiNsW&&VanN2} zc%~k$NzhtZ+IIRGT#%QZ{IjlViY~=*RLB*- z@bxGVL z`3OM}dY%(TNG3xAg8+Ebw%v|n|M&NPj<9NFEJ_1Kp}K*_hphGEJ!z}IQFj*RYnc?{ z;P;gJ=I0h>XVw%+DP*#$t`Wo|h}(AHM%^F$;t&AbebY^H9||!=zEpG6{D!^dQdWbc z(x@UuH7%vSwCZ&2W^=7tUDLwI%IB}X_R5L%o9fky=SObfM{%NXOcd#sg7 zFkM_;)rnp(Q$M@+5dffEbYgydx*9a9oz=OhwWNZphAk{hp}-i$IPs9IcoAhu+@FSA zh8wvwmx%AXNlXn@Lj;aaRet*WmqIW8n@i7l%R4T&og`NpEe>x{Q@Kpa1b`bKIl=@H z956}*5*Q?e2mol9Mo|Pri#g^oQ)MOt2Z0a>(ebi!S4(Q1L+c(h^Mg`xXxV43K#UW{ zL2$ttBp?u=2R?Bc03bN!i{H331mtzXMx#4AF_cM}hmKECM$&m*>$NhOyjiiwv~0DV zyfHPe$S|^Lu-bAk=e@=JQn#rpczkG>#li6ttAvSEF01F%j%!mHCo-c>^0Ti#1OOlT z^0zT?3Sz>n2l?d7WYwM zFuU%w%Xe+szJ8p#e%M?*{Kjh?GyTeZ9kRNSBVH%gkZ@a#<*8}E+sYSu?!D*t0PvH$ zJ}oN*k|;<7$U>H((`uv(J&d9Hq9=qqLWN5=&qBFRjMdcAS{*i^ML*YTsJrKRQL2Mge=%&!*8 zgE=Gjt@|DX0LsOQb{`Q@_jm_7zMPkZ7q?rsuceA%gm`@=qtg^}6W2L1wV)NtdU>z`Igu#| zK@u)FM?xSf1n1(BPwoN$1gHG-*Z)FI>nqDG--`!_%DS!}Us%M*%#{qIY!KDZtKP6= zEUb23I03ky!I^}(*Qo_gBWoF&gmi)s7spSotkxT$A0%<2C`vZ1ZQV51H!zqhmdk@f z>&M1xjoQP{ykLhCO(>%b01!bKV}h|H2p8h#cYO^2e*e`^H#i_QySTDscl`drzVV4t zR{PIaU747`w@nle()y%`0i)#4AqPmQpRJd3YO0TNUGD`&_fhMlQKC( z5PQx_NzktAL1xc=g5t=g{m-LC5d_WW`$j%M~f={19{?f&rS zBme|4d;Fp2_P_MVP*Gv+C5T&?@PzW13*E{A4HL?%U0>1Vl!7<(r^`j6YB;8G$8r1^ z3|-e%>Ez7ffg{W9PO^F9y1%&U!?kLY$Xa<|TYh+Bv2PFnzJC8pB7vN#ND5!5wg@qa znZ}HLaC;uJ&e4iTd0vjgmQy{nxYjeg%K?HLOV0AKV~1XGnyvnkt;jH?-ad2qG^|=7 zI%ekXUNBWI2<{X=+}H5XuCiPd3`! z@ab13bA{~aME}W!`C=(!ntIK)(~>sWlZ!>Xx?1(y!9ag+wlCA_HWgjhOpVj%vwwXL z0Q}WWcS=Apr=i=d*Omz+x~2dJ&YA1bM!iEB%rozELamZ-*i-}hck z(_cGwe6?<`t<~c&0Ze3tND>j8CrL~h|H-Xi0f2jN{j5VIj;*=*g^0uE&70S6UC%r9 z{=vQ!O5lwrgHA*x+3DX<<3=H$0^xb>rXb2!^6M{|6{Nk;50>HvzHHbMdhA7u*m$qLKxI^7#zquAfYcNxZUuq};ry2@OVi7e< zNs6n+$Y2DTA4x1Wq5##}K7dSU=t;?3jxH_VDb-oAZSfS+3ifF%-1hcG!60wPO?8F0hG;nyV@Q`SX&&=6qtZv>74 zE@A-2fpHQukudbc^?LvS!72aoy=!G6w`#uchq|mHENmVUIc;QAWFR5o6vH`UgmH3c zYSFN)Y{6Wu)_Quff`~-0f@AVSTS04du;vs_h0sbLb3Gf;X?h)ts=6oLSee1bT{PK|`MsbE{nu0MXs?k#{hH;oAG;OB7 zbjNo9;OOBKVZsn+joIbA;o^>lmuMet1}8abvBzTpFf+Q5?s*s%V6(5;IciQjb|{+5yo)LhVL(xJN&2 zQ?F@YA(9}9*J_UAQ^8Rbd0xl%I#Ic|Z^N!Lu_P1K(o+-pk*$Lh8vx*s4?i$IK87Wl zvSbP2APi?$D$6Ui6b^ezg-W%#asBD_YHcx!Uzw^6mIl+N5jt(ByLSA@E3$$nHtn6B zJE`@J>%E&|VTFQF%qioHVG7_z;_>Ux0ssW3eEZv1C_qGju!j6xwmSq-#*>($#rc)9&pP|SiS$^JT9tMNYwOe5)5{qg3&(@yfT@}6SSl!)ja+Y_^y7}DA6%oa?k7L_)w2Nb z%CEmF35E%9Mj1~S;I($^iRX?!`O3n?wrzeA1B0{ooLNlgJB?aaQ_8uFRWN36001BW zNklc@LL=+5Nv1HI>q3Y`JP-dypt@>O`=`>-Qf_JPd6w?}p*mb>Xtvgq-ZC_+8#gZZM zY45;5|JX*&Fl8;LW;W@?p^e+O1HkmY$5vJrZAncTO4^WQprPAI65x?vEajMB>FhwK z<)5fKj+*Yz7E*-NT6PQLqf5(Sr?bA?<3~|UbTgF?J??Rh8d+aRT!@IW<(mDc&s_un z2u``>miL#^mMRoALx2QS$UKU(dXK;)L4VF@Y4|JYonaXAI(NuKY0o{Oyz{wYh3|dOB^$PE`t4(nH7hOKwUdMakuZ@20OK4Z%n|(Nu5SXs zH$Q&4Ljx_Fo19%Mmd4)w-uF(74{{(@u7^wPPW{LsD2WL?ePnb!n&G0(b4#IIbNyxz^`+BqDdhVQ5q9KoBxkd-phWky z?N)mIfYm#)@5G72N3*xzb~^w(|C29@I6*R!6}j84FeP{J`07vZd#>#SRwmB{S2aDI zFX#%wEGXtOdCTBDK>$3Y&31cicp!?CR=cyj)>x`Fd$Z|MMqg>x275D7!Ca~OFTFek z0Jq%yRjw%gePs+mt6DvK&js^K)x$GKXw;dXUqnWamCqCmBP#>rVNWVGXk>FCXF=!w z$(Mfh?7_F}+_vW}XZIN;hT@r})mL77anqI^9E?(*l^dkg>Am^J&j7$ne|qTU-`*jq zfQaV0VWZ=P5lyMckcF(tRIRrI%AoCvYB#7iyDOFY z8$b9Q03bN!b2q*#pHfsH*`Ac7FeU&eQ3e;7jC~$ZOqpP+jA@v-^*~jW+~`od+sWlr zFL8NHHKJLn=6en>k&<&EO*o+kryWO8u2B5Qhd=zbw_TVi7VmrP(Zh$QW5zHc91s#yyipKZWtPl?ZAyoovvrsjvk$PElG%?sR||$L*2;j%@2L~Tektg<)8VC zuBH`B9W_fA4r}G?e8eY>Xu`;a^9xVUEadiNc6I*nyUa+~l&LS$u9wj$5n&=2q>Qu~ zDZ~ytq1#F#oKBZ6cuO>~(JW0Exs>4X(W%bWA9z0i{OLO%lO-%EGGKD($ZRdhygI#j z;OKNh6IIm^V2p{8wkVBNf(%U+j7L!fDFtV;iYcSP{(?(o z8L4;Q_Y?qp?Mq*^W1lBU6bA`S&OP%23CKOa`)yt(6Pw5CEr-SEt-H3Da+&4D*`V8Y z*Ou+&Iv3%I$Zue2^SV*rCd2s&tf|O~jvbxR(>bK-r)}Su?N?}QT>qhK0N}Sj`d)Sa zZ;-?&Lre(Ir;V0x>R1x7uB(Ot4VO(;LafPoOXEqBLdjT3-!zm-DU8U(XX4nrv$EDb zZ9{&;=8Z|BAfjSD9WpfxEu}E@*7sZm0F$piuvSgvbbh&3UAJMwO1&ERemZUS6mpEw z#pN{%Nil;=Pao10-|Ph8~(-r(@qiG>Q~Xn6=lZy*W zw(WCHre6ma%ltDxr$O4l;-@!VY{?xYhMK~KQrTBJF!U|dc3ec6-Uzv3wUG^S76S)0K&lH zPTGuVz*=aaPmE1W`|gy{mnn>9v$BlX;a6tvyy5cz@Vi?+paNi7l2r4F=T9uS+SKAg zv)xH)O1@xds#R}!UPPG?06-960M0l9Arp*2gh>JjamEydL; z+FYBNc9$xQz>BL>Nm?5ykE}Gq_5G*iEZs?*#pN}t(6?^;4ma>7))xXV{^&JV0l>f9 z{0+7CgrNtaOQR%-6K?w~rOPb#HBHKA)zzlIf58VL_vfSyWy{jgK#!KUBpCt7YBxfQ zjmY)fjidR&fdK$S!$dJlolZ2j&`AwUTyfpM0Kk3!b7##)nw7DAuT(1gK?pKRr&Efm z#8DgsN!ChZA|VV!)i}mN07!Bp2-{%{5=kJ|WGpx`bv@*ykjm|!T9`U?DDuN376V&$ z-1G5E006-$pZ?5cxn4z6Sz1#x#(_&92RUM~h;-sM$6O-{0aC9!T*gSHg^?m6z?f|Z zo*|i*WH669eivk}%38=|hebrfW=cj3$uj-umG=X{$8Ne6gMd>60wx4zET?!yJ?0*- zt+nbw*zDQ~U^88WFsLuj7SgG08^$lZ;QSqDo{=pL&n_?g;{FHMR@;mtMNxoYfr$_x zSRw%Kx&7+^@U_oh=Q?&Ag+UU>OgZoTcW>XZF5lCuC^FE9hi=>MPEMVieBnT)vexc+ zg0YUX)rlSxFp3MMhQg<6Gal~ za<`=oWO=%sq!W6JcWM`a7NhfT#cXs6tT4S;*P)%Hr#lw=D zi!vA(xaasQOD!*YX7b>s&Am95PE@_~_g-A=?X7p6x|@)md=w!?i+7IpxPg212d@Tz zPk!#d+2W(?`|GlR$P0bn^6~1!t=_S}|MK?%;QCK~q}acnC>TpvR%L<_AS4ix zWm!{HRZ(?SlNA|B3P?&MNW?iuKor$XnIokhQ`3atZjv9H(ZlnCuvHo6W~##YXu}6B|?Zx6eXbu6kBRHW_H%d5?6A6WWY^LZxUiAFl4T7O z43(v7r=o*6`sCvQGIzcIvX0;W?Qed$=dBlP7@zp`hpz>IAAaI(Duh@^#4u}~*f&{? z6L;^XavU+;G|Y^Z$`3#G%&~``Ka?aA2QCB$;0Oc&V1y9}!FftivMF6vlYyeNZCy^r zsGIOOV#CGUjX!u10FLZ?)zD;)QOsx<(v{Vvr=EWD*%uFVT;6CjY3ymLS{@ob?euM% z#|O)~f+`acxG_a99C&^2_TIFQo_qbog%@9G_4TiI8pmJX-#mQ$_{{uAKl_>__lS~p0edecxEl0 zUAAlOpwS7Vgk>{UE@Si+Q&~gSfnfovW<^A+yGh?b@4wvkV*vQTW$zmqS(nM@w3H=D zmUG?T!!S2Z=(Wl&&O{6D5PV2NBa$oiaZWp+Xc4SRz`>?r`oJ zIa8N2GUjd^Gnx0Kb(Bb91dK*0T>^o_ByKsp(&%Ckl32=sy_^+>j1pscp=}Vs52;#8T=r~??Y+}bHe{u2fx)D`XSnQ(ETdmgb zec%bZR`prJI7^aP0M`|D{n)yG1Cr>@{>@&|l%(;Xxw^H)0 z|9Lk6eCdWy)|+mn(^7R+3G90Dzgv6RJX(5P%RD%yGJFt1AZ%9(;b^k;xMacH3bz zVu`D1>d@%A!a%QKXoezNveBuooUvp5*74CF-1Do^?Yq4wTz%#BXtlCncO=ca_W%9E z=tRFJPlv0wCBz{z6AhZx%tM@ zz>u0w=_xZ~nS!&BNu0=%tYRV&EE7ovLO3AGs3b{QP0}Q+5|SkJ(8BV>=n%r_*y57y zxz$E%^2B_l-t4qH0S$7yc9%Du`Q$a{0RVzi?)brnQz>APmoqdPf{-j#O?09HfakS2 ziZxBkS}7S5pL#yEHC+vz*wi#HjuXUlX+;rA;ERODrbP%*66$n3Ng-(krJ2^g?!sC;m>6S+&^+g3$AQ%j&uE>c8H; zYlo^Uc4Juu+V1+lfA9(Fwqp>S3(6_y3<;(y%208jr_kGz*9)3tSn2*lvT0*uYO>=oVQ(Y<~h60 z27rfexkg2l5}q*dqxk6LvLm8wR`*=0XzE~ZR+BWVkpJN?UwrjwC1Mx=1aJ@>5SIxt zb$Ptlf7yB4F8|c~WTV?Ud7!?usA{ri86pDLRnPy#Jpgdx_zXduF@_PMoFOCxr;gJ) zIWzUr{(~ouO?SJl;{~?k`M$>)Q)I=EB?XuvOPhyA`+BUG4jhg-f;bjlj07}!k}VZ> zU-I?~_neh7j7vUnB>-&QwRdRq#Thd+oc&|CTFlCt&X?A_R~7tFO1e-~N<)&(8h}_U~L*rB!aUYUQcRcJKb{kN++J-0_2NX~8w|lJCz=PbW#5 zYMdk)A#;wvQEp9?X3-?YJi|DiZiL{m%&ox2D_{RW%@T}YZ>1|wbEUZtc$Awp_vmo;YolE{jL890PlSN`^h;XfkGMNb;$Qk zOH-;AF1J+b4ku|&5?E$65@0-P)T?t-orT$%`I+fvyHssYqbS||*yG>&uOClFv2m_Z zD@lQ=mDZs|_nD7<2ms#xrq?mddV{e8jFL$5(p%p0D?j_P7c++Ek3Txys$`md`Rm`& zNgtI2KnKoVVk{Y(BynKmj$K=Kv`TYRGuyAd!S@5_@WCJ6@uRzLN9hBYzxc|(0>F>I z|Lv_ic3Pkx-Fe^R$4sH=_k!Ndil@&;?Z-b4*%qK7f%FGv(tI_@cl+33PsL@ zrD`eg>k4lW#S4Gt#`=vf{M2VYbI0MMd#-xkk;e|tv^#(Fhws1s`fGr(a!>+*&%fu@ z60BjA8Jk4eFiO`36UST_1o5OGfm>VUpsbD@@85NJIWCAm5Lp1r(6z!~Tc=ttZ+-do z&%NfB>9y{|k3aIvXp|Lgi|BU07M(9IOwLQJfcAGa@9*fdV0X27<@}2ta9>Myu+jYo2%gbvNByl4T*;%U}I^ z0C>)g&ncI7HCh#xot(}uUa?);qPX`&(U;TN_~PrI(>}DP-7m!VAK19-sr4wg$}3st ziahgtVYKGLo87YI(1G2dFZRqb$BtKOtvL}kThrVB^)nv;fPeb?&-N%|A}9yt z9b30v+}Jd_==b^?gOLy(IMHp zh_a!cOp4J6kTWNl44mRJn7;Tw-wOZ;&-&BPywM6_hiOTwM#*QYZdArF_c;S+OCEQS zhYTB)dVjP&$aCW0t1yq^Ns(2mK~NE)2)JWeky&6wel+QK`{R1E)vV7MK7PZCz6b#C z`Qr~UB920M9s#ukZk=sSP!gtqrKR=Wus@bkX=kT9Z9W-?fz$+^3J>kyzkAQtN~48L zJ#pg1KY#OwgV6*94}2kA-b;}Q&qMonfA)s}@b)*pHt_T$%96Ag#*sGc)>~itOTY3f z!c*su9hs_y7Q-)m^*?mnWu8K0$j}jKZM1Q=a2x9@CE;z`(!S=v6?-o~81k$>y}h^8 z``l+gJxEejQh)XJZvw!7``AY(E<3-txv{#cq-wXT?U|{WT4!eKWqbE-yZ3>6@A=`~ zaXzj$r|-G*$3>nC>4kw82Ep{q^p0IyuHQa?>uY`iON~GN*vD`G-Vd(2_S)vuY&;&` z`ZG7b`HgQ>VNflX0N~TV{pv=&5|o)Gmt|>`y6Nu;_UhU;@V`o9V{#~Hv0C* z_njZb1)^ZcfwZUi)!SOzr)y7NR3&d=_ZE2M)I;ai$8lkqkZF+{YZ+pZCjfBj{1PXZ zrHQi|0Ijt;&k!t-b7Y(~Ni-P^dxOy>(PnSd8xG?nNz$|^3dOMO`Q=i89(PuA<2vom zzJpiRJ00ZQ77mbJ_Nt!;fET^u73o0F&d*lMkv(_hh5J2A=F9i?kFDk}IaL3hH$BI% zdbdBY{=LWhtBKRjONM=fA}9&Yd72hlW2sy@bm(9hsL^P^5hjzUD6m|e@#8=U_@Pf0BpAa+rMfWN$O%dwOQQ6|@`ej!;c@^T8S3{2&SsX$E_)}Q?K8zxR?>yz!X)2&LSAm7TA zi`=@5$W}@cbQ&_KDL=E?I?4eA4>?jl?mN=WR+Ty7bKx9t9`=Tl!epaCPXv71?9L3* z*S+lP0Pwy)e6IqoJf+HIZL@7*KwQSm3q7y^lSGflkrZ;AB(u{~qA2nxSzaF~-`jWD z&Ks`1Vs7hJ$}zSqDTc84cG~S3JSyXBH0b z*|%%j-?@Ju%JO%=^o=`?9<6ukpZ|~V0KjWs`r@;zYmNzyyizJzq}iGIGpA0eu<=W8 zdeuV@KN_~%!nkv1PCk9?3FVjSwWhI}GYEYz@Pp@Fd!rxtXD^(anwtOG*Ss75*9RR-c!PiUSgwLcG=GMhgyg3~CWtFB`6uUuObeeup@lKuT zojNn@jgnAGj$$;JcmaFy^>eN;-+6FY3ie#Fr+)7f53KYj4rNd(W$DBLa>Qud2Y|

F03Q>idVlD0N(Z+?>%>BaqIj-trlgc?tk7c*_y6>``)wPePrqVZ@TK`FTOGz z4Zrm5^%E&(s+>eo*$Y}T%_1Ed1J9f|JL*q3ms_@N@61er)sT;?fl^`F^BP)AWvcR( z|M@-u*#EwNU}ax0UJ|^_sZ;i9A{&nfYomU_b*xQccxF|eX>uuNRByC=ldH-4=)`g1 zu+^RxvkN1ynnSL0laq6TyT}SrBH^Q7e&*x91po-o`oJfDzMD)$o^NT*)k_tEzvErNOjvd9|n!v>dX5O0izB4A~CetmSG(Y^r zMM8JW%`>Jj-@JV#%PYHg)lOeHzt+pS^k!z7l2c&}5P#;xIRJR*kw-i!!Z1`)DbH7) zM?~Y%FiR(7T~Xx5XdobR)>>nnvqqZ&;Syqqj+`#EHo4IT5S8?ia{v-jSxe;HD_{LO z0QloS``gt)RH=5dGU?hc-G1?G3>IY+jdPv8 z@MX7LdElBXrMvEZ_^Brz&*Dk7*=jW#KC=sTy#7FIes<>mCs+Hf(V5!uCm;R;0NDM( zFEHYgJCE5ar~OkwOC{aWU~@7~Oc4>wGtaR=8*Qx#m0W06w^zh`IH`(WC^Hj{Qz|E3 z$E#JWtehIU6B}`_n|rFBq~Mr%{7-)k01%$_Ywvk66SUCqeUb=*RGT;CT(~NDo|J-u zQ3J9fvxb_$#1xAH2`ZeA;FP>Z)drE!wwm$=aH^1@)9H?5aMVX7UyhwB3^XBHZyycgF zecPVxquxf~1$X}NM^7JpR0N&_hk!x~0MOQHZ3PqQBe(~r zy4`Lx9w{Npm68y?)6QBhgsjxchG;Zgzvb2!blROt`}NlC001BWNkl-_1Nl}C=omt9sPHaxXlko0%V);*sU&KQlK`Rcs@aQN}Zd>}vY%H?VhmZenIxj2rp zBtoE~&}oupX(|LWrm%%8bjCR7qR6rw$Z1~Qx4-jU0I=snU*nD#WyFQS6U0i>yIo)|MBG7 z#dV@w*}`eZPks9T2LOa;z3m+@qO#uG3PQq0*ut>pht%dvz{J?=jIHUYBC+IDv(V8N zF{zb2gQbjIQbf^Yz<`tS;#h&8DD5)gd%m~cABa+OV{_6dO}+0OUj~3reexp)taHSq z@cojI$`T35N}dv2AY+_+#u5TD#+8(5mN`HG;Ebn95+&&N{H9K_VqWv{1phmh#tD@$9MeTTfX$T zA6iGiKq?7{X_8sv(kxHYoO4NzrHLtV+}Eii!zWIkJ#zN!V`okPz}b_}MA;}AX3n@g zFRZgt38j2rs&Yw%fs&rjgy2E~fYByRQ)8{~`=0M{#sCo+7g8c)h>S4~K!CuIBVzz4 zqy&Kf{*G7WdQuBz7=&I}F^+Riji#Ta5phlgh-stIPGXlOsq#@M85e05Yav{@&dRNF z)`N?uqH@U?nI);xgj_m@E*_8bRDJkUcL2cob4!-21FDpQQV<|x)*4gflgXHXF41`& zPhwLe7yPV}fxoPQK)2uLOWsyy;b&8>^$$ zHGy>F-sbk2`Rbh)ZMk;k6}xZQH?^W(Ufg4QaC`7+D<@ zgxXobB@#IxWGEP8hz`&aIcuFGW1V%D$QiAzGm-JSQ`4**m3`P}A2P6`(I9(M!| zgCO+EC81Op)C#cUG|8edL(1|(d44n*Fb=+y zgrU2odnoCyp^*++_DVi04{Sh~&v zCFP`R{`OC)q+%Cy&@XkZEZ%ZhG6=s zaTTKs(Z}+qbr>IAmI3ac3CQaK4bF|70`|bkUiI{fu=DTgRMZ)G-pM~Ff;*a)D!!^C z3!0ldrdxeVr|(VaDofo&%WPUEBuf}F%h{?CT#ZG%jW~bc04PL`<6MHN<>2$@2vZ;m zB6R3KJL71S+?`aM0|%SOCv}f_F*T6G;w$T=f!)f+FTk*8L0s*;kR(bVc@(Jt2A2D+ zsn07RV83TKNs~m&Vbv{3hd30a!1j(HTq?0mS#zsWMq*N*G3_sr;AytZXNE6ua+92s z4I@NwBZP7Q)$SBpxP?sy%;xS?C=4bL1uqVuvlXDgWgO4iWPAvDJh&WvxGNmW=C@(iJdJ@Me*HP=IBOgDd~1Q*F2MQoMUTMwZatptr!}Z{hU9P@-fH@sE~Q`A z4feoN23Ge2YCA+1W;4a~S=F2yIBTJh3(q#saD_)4#0o1?A_WA+zlj4-MkWe~3WbIc z0qo{TyHPBd`f`&7z)fv4^!>k#`1{nGb;h|tz+O4c=lLYCp!T$7yOr(TjV!%TGfb55 z-Gqq8Yb-&kx92`vw&9MXO5=*qKwL?jvakgM=c4Hv+L(aH5)aef_7{b^pO3x(I<7%qe0e^x-xo7X3dh zslA1GFB2_mT&AVK*`vNX@RC?L{eu0_0rj>U%O;@Qelt=ky~+$aQy}eiF0s5z0ec0 z-6R?0=9ftSz$EjchYHmGu|)&ECvl<^N=erM@u5f}6g9W^<)}Wd_uZ;uUX~(8{H5ZO zg91A-q~Hg$8*N#sp?-O=?q6)NklEaT zFY-}H!$_FOgcj(bl@b&bT=+am63@FXoyfpGyGwfC6a%SIwr3NMkS?FN<_hj8+^plf zw88sKTJp~2N}r?E%jfIsIY)=pJVNF#ID7V>6*3=s3sDvnI!6)D`T5e{J#o3a%QaNo z7;pE4P`Jq{Pk!6eyK1SDZ>QtHi49|L?eBLb2$&7540f!%BVdOr0W~WAifrH*DHr}4 z_JCKNJd`wakIyz!_RD?8OvA^F_ou%`%(h9`_Ml)W9_Z2S3g(&QE{_tB8&uY!fa^XY zae)5rC-&)|I}Fi7wBWxL%ArK@L;A#;Nl7CuX6VCLmL(cg@MqVKAmaNEe%p(Ovm!%K zt|tKo*wQ}_gt*}A6h*8ijdRNh(JOjk6WAG8evryFU@W#8h2>%sDP~#dZ@Dh^KqnQX zu?&YHcJTbv{a*U0<^c}3ypG~y4#xDWJ!+HX}^$H8C~Q=c0zVMsF)C85-0%V(reQWzKk zDWK(*x4x2Mi1GZAXQsVL_3eaJ*XdU7-JDI^i1HbaHQDL!Yz^}z{-Va;u*ZpyA@Ny{Q(^kMHqnCv=cs=zF+Hz#?5_~Xb zYbK#16F@ELk*iyhPSH+ZoR*7+g@P_mUsS*Q*W>z|XZIIh{qeopzC1ciDN9vfi!*N) z+m=K=WE?0QDK>Gj^xLmg*_k8kgSUx=NrD6n>Wg7cC^lk$fq+?^*P2u~I(F!xi{9qf z<6Rn6R_MnuE#5BO*d;0DHxc}BBqc!tyh&R{SX3O+ z-^KxtNh|5kjv&51q0ICRzx=pV94e|9m6pCLXn;8GC4jnw{N9{E5N6XLswax z<>0ak_K WYr9ovMW5ZwnS;Faq_mB0(-RhdV)lCRc4b^@^_zP&Xj+PVkwrRi#;O& zfhSD~Qr5&1zd5tAv9i6M*5(%J^ZeaUduJ@OW9f-F^s5>(^L=?>L+y)7zq1f4S5gnQ zs*{o-|Ecsap)hDX!b7|h^L>+=xb@7R!#Hap0bWm6=y8lG2tm9yzh+tZWh$MrtV)Mj zTJ?@Ahf#BmsH*;27VTz=Ow2Cg3c~#pWy8G_9cleRQ||S63C&=fP<3*w`XKBvPjpUA z{!IAa6H0gE;rkCEE)Z$NBLJ;L_+D`@!y!3WhsU4oFxUKkIlKqm$ zEUduloROUGM?rdcCQ3&WTVp~cw?guue+s~&VDonmD))3WYDUgQs4420>6Pp?vl4T~ zs>N*F;Eo*nT@ejfWeEerB;d5uXXBK4M6b*J!m_$~{YSa5(l29CG=4t5D=MG)Ft15i zOvBX{03aBA2T>NL0RBwtXT@G>mZ_m+JIHLq*)kTWtdHfSZJ{>H0FWku%~qFlLd%@T4v{$4kk`x8jMH|<qm=%5^w~hg!)I zI%p96lV#54lJJ20U4pKpW7sgmJ!E%VBlOq;nljMeNlqH1AQ%9CmjzGO!Qp+r)F2k} z4;Y#n0G;%Jh2M{jD7i7xBeRb^XqOD&XzrdMqATAwYP30m&dqS}GaBjgE-B!Rb7YV> z=cwJxo!pMkSZP4^@HT4I@p6%nry`O0%%UGeihacVL7$cFP-C-qC6j#Kw&Z}#qPNqc zO4l^&fu&S#+;I{*Eb5My+Q7#}?qdfMyQ9HRC=@N0NG#yG8lzh!6A{vYQSem*hk;@V zJWjHW9k&0`K8Z`qdG5Du<%Qpk^a*0fK;F|O&X*>WWKy?enjLd30jFq1%8JwysKaf2 zdsi0`edb^|rzUcm_)pm#k!YeNj_a{B0H(gTSRwuZIv6$hktYR9#@xyLt|Q7ryl5WP#!=qy;ZCuWHqBQY;(?wSTq_*W<38in-n zs^#iA@Jp$+Xbw#}vVa)Ut64>LuK5Mpw#>3|TWM>S6YIr&op-2!>_e;M`WDx6%57zV ztZVvdS8jtt^o&P>1%7@Q5XR3gnI2R`r&(-9CH*3W%p|mboy`-hHnv@n2msUpFy4#Vb7GJrK2}4VpVD#%WNk` z=F5pjlH-r==TUHbomroy7eZ(=$&t(wrZrR`O{ipoOmD{|uwy3mYc&9D4&0|Cgy0hK z7=oz2&(LILEw7(e_dm2O9JA*NkPNvs^JmfMryK5$qV~nG;Q)=LfM0Frg=WNY|JESuz@h1UXrMHGCG`Q8)c)7 zTWVVP4nW>RMI+MO89%iOZ)xZ&1T&{n{whXt^JVtC<)%dFbk5nctaixaha4+!b%BP6NA%ih6prM#FX*2&h#x zyNsC`%S^$)PzO1aK91QCO@N!DYsxs#b?<)XJA|z`^WDCoO?Dint6LBjpjsu{mY+6p zk$FQIJ(IR_+?zGQBiJn@7;+n~&_-MSGxwBdMO7zW^f857ZCz|e#$3B>`d9if^f-9? z`)}l~?a6ljb=KBCB}*~D@WC>TO`M*g&5w23WL8H=a$ZZn{3nh2Q|z=zQ=vg*2DZGIZ4~)~MCi zej$3f7u<0oIcv1NW5=jF&YV2d*AFy#XnI0CL`+7;A2>g__i-#JAvq1B69jaPOOA{a|kK6t10K^O(ddpR-gt^bC8 zf&}}vJ!CXu@3|N(PqEwh% zPB1fq(yqgy0E-YHO$8Z-g#m=zrQt&X+Z@`Sfv0%59#0E|FPOvGyycUSW@jt z_7oO-wF*>%bbRYuVhe2*-W5%B#g{WAMs{6TNozfjnHO8XmW)N0+w;XY)enL~#Ig!i z?imqeLN?H_Em#0Uq~7iF;3zT;CwiJY2io4aYtMEs?R|o%Nvb_EP)D(hHg$h<=cLf9 zv3$IyruHonT(jeaZp)yeCi_O!&gsnDTEN!WqwO_9UbK zDc7jJwmfeWJoi>?{QDkfKT-;3*)@rpN~N~b{n*}p4?bU2$mF&p)j#$$u^j@C1#46* z*hm6bNvz5%77Qax&+NriQ1_Iod|iTIfsotQY7YvwFMGXMu66HlvKaqVQdy|qcs7aX zUB0_&Tv__OD&9WdP$`FWKE%+V;aCqex{P^V= zPQ{yD9)!5Bz2;a|&2OV9gs0!#)$^Qjw!Xp^FsAggZq~%uwCy;v%vc3qTsp8zXqIJx zosEr5IN*;$46v2`ic;*q3Lf@IO5xB-$9df1Bo}{00 zfHv#~OqT3M0c*h_rP{jLC{8H!5jF5;M1_;zZMhVN4prFwkpDY?JtTR|!&roM@bE}T zq2f`%W{*Bqe2m9p!;Y;oWpr|avb!uAi=KOYWuqmiUOH5IQqWyG6bXKEdZy0?Fl*z? z5ZrOw3RP^}R}u3#J`;T!Gud0+{w$%8tLp6{yr?#aFmUVC{^ODs;+K@%NP(F-q}_Lg z>mgW_Ej53RF86X_iAn#7j}UYx+bR5Zj0*fK=ze&tZ9n(Id?-nl`uL+b5`nT9)5urD z^{ZJ7&)9ZtQQ+z^BxT~lbt*eI)AR>M1lQHv_}uF1_4Bn>nGwi~AMxuR#zqL_VK1dO zeDm_#22`(O@}jh$GN@ll)l`}na&r~@z`s75dA8Vc?w(?kIbtzn*4qBs{C0nrAm&es z+&VPmmTVJAUg)ujY8_UdDClIU`D!+<^4Z7$Z75PJTmdSPL4aD{4~1-HBeSs(#Iw*( z4SUcfHK;{uNzVUvaGOa`sW57*+!776_We97kmNBKX%=Zs+;Sn{`d+rbsL*wgB7Ae6 zTs2JS(wL#dfd}~ucEy|`cTReunJxBL`#DzF9}fRU@!5Z1{749crnG*4QWLva zT&`nuvp7hH81pY=cU{qA?{rlG;&;>g5OAh=RZ#y6F``t8V7>2 zzx8qwbh;HMF}c?wW@KPQ>gcRE6394OqF%%wh3zzPJ$|d>v25Wju+E@++-Q%~CO^Xh zMy-(zjnfoWxU7D8Y6dok#DumoIyHV3WBX;L@B#6a3Ns@%Z@1M8_~6$g!wt`YD`z%l zFlPqI`^hk9@NuY2okQo=b1$sVGVv)PH^MeelH!+O+Z>PdTO%*l|37q9dy`mL0sw`$ zc=%gfA}QyPI&5XhNNX-i+OiH2TtRYnm{~k@N$=#u#d|OTqW9e$9nVVuTJuwNTq7?@;F8@0KNlx_@#3KU{qpg zT^~jKu*Y`w4cTWRXlRaE?TJhnHeC2~+@BvSIbG0yz$Vu)NY zB)C>gUWd>Bf}xzBmf=vzIoXJTf%n^wsvo8(N*2S$=pe7D;hshNH`d)1U7_aY%I^yqwk(9tczV zNlZ~tEc6(_4ioCMpz2oeqfkTB+xmB-;z8K2xuC6jg=TQwqn2HI!qPTQf*9P>B==`w{B1uvo1roDlz87DN`)F^C!NYAC^lLff-@2H3?sj&M zJpvocx4#BXQ{;C~qI>U#`$U-9NSJqQ`~L_Q`KfD5v}%pJHp~-MulY|124j?{B+`f_f>bFb2S;Jt+d54AnN(Us( zeTYsW)k=q;iDS^xk9YG$0)t3OVwg~ffh&b3DYa{JnCLFiRVX}% zteRKp0{b})w0L?nGv!Xi*Qa_V@8?hV(D~lEYmuEFmbYFmephE8!x2s@QZ$4$^S7jc zv(GPjX|xK^>N#_Y0psuAj(|p4K$uYcWZ* zCinM2?dx&{eS-Z;y1Sb2k;=GX=?Ggzm@kzRP*Q9Sol!m|*&SW>XNDt?2m;Ro1l{JK zt^RC?hbIarK}?HUs$4NMDWcp5!xaApE7iLcj-T+o8*`wE-gn$qaw7l6b`xVuMSS{= zVHij?N#o`-snuR~jjIu%r%na+@Bz={ca%M=?fhs-kAsiUy!4KOZafM4Z;wT+l!1~) zJjQ1-&72%vlP*Ek^N`(XiL9D(#ez$Q!=?UF`$n$WeI%Jwg0GjHW)x@U8%d@AV*#F@ zgn>=t^QWNQN>T(;#<9V6(GygS=A*^JW3(EZ?lZCazLB+hBRI7t3#!E+jpNCS6!Lc1i@V2Qd4vhh(RdYdMJxOM1_-OOxrZ{2_grapT;s&=sZtOgh=q#pkpXGaL5Si z`pXKrYgL7D~3oM@4iX?{(qal|!gG35M#JlPfr1nxd9aP{r)IapK}}Q1*+(*4lHSgAZ8i zgJ7-uUG&74J;j3X7&9hZWzMoxv{Z2$%&4ABBgpF0s!JCPz}D_3Q3%n^|H~L)BtNN0 zdG2YQv!_2FnUs;`YGZetrs-(s5rLRidfAu%OIbzK!~H_;j{H=XQ9D()QCcT`OoG)i zO46T`mX6j0vhlK;1N=L~=vuiGnbZSmd_ZqsJF_p*aF`R;7s1UIWEmhL&S|vYd)*(k z6shvDri*aPqMN6&BhC9*|HEn^O_!|Wa`~7!jSi_{@PJBJ(VZ?evHO(-inArG1&mYR zN6tsWOpe&Q59TkOROP<@>2_0EacL2BZ0yx2S+gtflFJR=-=6s_n%KO0Ss#EwwjExU zGJR1rbaubxlaTlEY=$(JQc?wqTogbD(?0(dMDH%gW@!ZYjKs!yJ#S>lCLzqgJP(_- zM@n%9-l;fl%=@vw4fyym0{pR*7TMVz^9HAc7UGs5xUcb1ECM#{AE2PIhlDWtGxYl@ z*xvb@1y6HHQECP{J-yn8D_EC>sx@T}B801}E9lys5qxE-&XgzcN`ng@+jE#U;oQ02 z^4#;sfH8JSqmDSh&Mi;A$qDQWcjKOP6DVuKHCTA^SFEP(22e-tT$^ zy9gQg4btQqOv5=8F`e&ODg4lzUYMnk(4o z*LBrpOZdIvEo6*wEy!5LP=6yA2?Qt1(!^{0EbSNR@-78J)!xQJV2QRU_=#H@24g`b z;qNq3uxTiESHI2C?%XI(oUf}|*Q}mmXyBE!Z6iu*JDCS@piJw1&bG2UQrb&Y^CZ6C z%6nTTf!X|>amqL7wkI^ZXHY_y1cx6+&NIf{Ic>nHib^y-)vNqLS5cyVb_8d_iRW*X zevQvZ_uW;Rq|+>r6+U;l2rZl(1@7(IqGk5epF6Qz9N>N`FR<9?cBD=Gmzzio9h~_3 zFFsh>I*~e6x_4-Z5fvC`(A#_vokEZoGOLntzU2RWKe98VChyG7<{}bwwE8C~z&Y^G z`_pnK^hA$XI%+DLSWTw;&W)EK;C^%?sCD?7CQEsc`jo}pq>>1dnq}@@+PV;VM_YB879=L>U+`rAsHhc^;jyg#xEYvZ z5s)p&xET|CRdO@KB%D6`dRE#lq>EnmTNT~(b^R`=H^}AF*@@TbEC1Wo6Q$~0md~np z5ebH)5n1|PV*r6m+iAKY?-d>Upzi(Y>FHjh(obPyVn`KB%%|#wV-&LB`;b>DK)l5s z{QZtumLgFyAD&J}v2BJL2~h<8XYNV!V+lbre6)RbGqIFapH@=i`aMOQdLgsoEunN) zhmEyOCvQA*_MO9!4vZO{{l3EE2}Fe5PB~KFDHuHWc%6Yns2d zl6f4vA~(Nq21fgF&V-^trhVfM1AN$v1Ucy!1CpP*GoDUCeY;)!2SK+fFx7ZU&@mO+;^BPy8X_pV%(1X`Tgd-BLo&H$Ym}IvITDX^T`PdPF((OE&G*(YHWc9MI~dOqCSW}Pat^vIVIs(yxAHb zALB#S*SJ4Za1jVpHi&&mIw%rIWE|hIrv>gx?p^}`fyZ7M1m)jMQ+f__BsZmrgTf2H zRh+%~f)2mo-$fZW_R>jC`D(k1)cW{@I2aZl-ST^YnvPHCzRKcbf2J&+cmDwqOzUG6 zJh}h&dPxY~s@#3V8<5Za{$~m+(oI?%JI!q4&}p8yz61xlT{&5HK;PZTWfuAZp>L(XqUwuLP^*6&{>9ERSD*DD!j|w|-+H9j75= z0Z=H`mq$L#v^Ul#h}`sa7KIls1=Xi$29z0myE|+3eQ-CGHE;B?b=&`!KF#+sqjb^H?EL47%j4xe37!Q;_Y+?}m_ z=3*TQ^|7P&JMy)YGXd}T)v@Z~X{f|Cis1f)#(~tojE&g~u zepwBbJb~IzQG+hfTvR^x53djxXo|`0qoMOb!qrv_iG4|$mJxj7w_3DhMIT#XYQ04d zc_f=C&#B&|&O5`9o4BX5G;Hf-Jmvw6b3>?1qWc`gibgZ|8$S)02F)pf5~)+)@hNhca}e3VbPf>;9z{SiB!V@qU4oZmF;3 zJGmW|1rVp`+42)<__Na1CpUXr-@gM5dcM3p&hZ1A)OpXo$q4eV{TDkzvG@c&$KuID z9`xicO~h_q>q%|CkDO$(Pd;kwb2iMJ_T|Xt4i-*M9w3UF2=Y~zY`=YDaHgTiraTgD zTK|PD2XU-6X7Irav_>R!J#{ionL`=#LQg^~ZK}ZQY4U8}6H`#5T>NU_0{FRi`}xaZ zC5x#669VZxF-}T-y{6_ww9XIN$@Yty#f}cBv`2y9e&j*0oEnftUT<5)@4ok_h6lb> z;F~tdQ7a~{V+-pjLM_td&llnH(;t)mc5oA;+D`JCbwM@u(&f^jk_t}Q9_w(hm{WM` zW9t5LRG5|p;}ZiwROOz(iXz%E&SGU-F9rpuk|p8Y zbkK$SCS$0SE&U<&rdSP8G7qg{uV?`2+mPw9#KmqjPDPNA>%=L}#4)>59Nnz3AxT9A z0a#8^^?X^of)7QQZ?9z>(Ypcf+YjxZLRHRQ-s^xUuMZf9n z%8dKladg!r!}YKSo;k>WM8MV9@@{T#ZTq+=(qMD0+#Z9ie@7=*oI&Tnn?6S0x zZSRDBHKsyAInhUW`ykr4teb2hv|u9-=G%hPja7({Iu-M z$E?j!1(ZRY;!fW+5lXJ(X(dFLxe|B$Rv{hkj$N`WSYslkyW;3rJvLq=CU@9Lu7#@S7ojr}ZYicl=5ks=a)YpUxKVgi+K=(2YCCeTPGM!}LHg@umy z@9N8Nofrn#Gs{3>>;2zWzeQ2ifbcZjo^e1DrC>@JmT_26OFs7#mGHk^aL^kpKqF)~ z$ztMh4AuFA8cK;z4_2egU^w63G|IW0V6-bDG_MBEZm;%UK8{9?G5ejtBI^?~gZv{d zylpD}wv-@azXo_NaH>ZKB+KN%P@B`Cl}c zREyLR^|KX;5X=i3cm`1lMzh=2KC8zMKD6+x<4~di8QKn$=&~ynS3+DR*-JhS4Gz{e zkduv&mui^JMX^qMI}whZBZ_b58&3zBI1#twNqND4lx*m-WH`^x9Ty9a6*sbf9ANPM zA!rn+R8eNeLoLb3SYQL4p?#Ghg#T-x{mw1xV0I|j(|MYvt;)ZE#+iDzJNo*~MfLFWp7riPA;x?jJ)O%zE=(}9&EKAo?JJiIP( z+tfE`})w;m}(+RZk&o;>{_h7E@HW>+wmKwaC>MB=YT+e%vM(pIj48Eg5HwW z((EVnrJ^>8Xx>)A-NewIZQ%RLb||U{!g(LR$c^+bo=`x8%Ygmz)Vu;e4n=->T0C@N znY#v|JhGlG3KjuDZXE}H22Tft=9~x%M&#ean>QA~G@@k$nmjj_ub8Mb&dbXqx_%0{ zC2Q&MEam0Qkl`4O)49wFdNRBj-OG7}u2`DvkdZg-J&hD;Bk2(@rR+)7oK|qzPz5ZMr$0 zOxmuJ;1v~;?mf6>Cpg-5UhS%v(?m<+@|=(;S(J8ut8$pHXMd@Nu3JOyCo7r(ET{@rkqO(19vIV{E`#5J~$saX4g2F z#TZjcJfVZ#{}(B_#S+q)X~%=Eraa?Y)4IgnKX}i@?)b?-O_>I(5krpoca+*A&=1rybIVsjW{^V^PleZJY8M7`jo1y!l%5nv3`cA-{5;7pKXm@ zl3KWMF1Qm6X%0bz(rw>O0j=?04a zQjuI!8mN>dtfca|C`963N(N-8tTtuLYkTJDhcGCL7DZ8#vIsUE@eQd5iiojw2@bHGbaRiQ&kV*z-){{?{W7n`;rI-8Quk{cHFE}Eu@8x1#;ib!n^w*^RT^{(Jn&#AdIO6d_YLI~!J3q!bhlSAii-y9M%du6 zbSmdPYnDnt7b{7L2Q93XHqbD*ZvYYpkIYAwuKSg4WFR1qpTsUxJ7Ul?+nPw=eZL## z&sCIgMPerxymcLdLK)-A_k4V@$Ax{x%jsMOAH33qnQ;P`(}M8GZ27GVXcS@hWC!## zN~G`N)Ha~E#It^HjcY zf6?TQ)>`k2ZN(y;pg*X28^?FeP5h<%`_}k(0WN78eHX*AV`{D#1Z7!+5k~P{OxY4(YEgJ`O#EH(fYg{-1LhG}caHWJ z$VTVw--u}rbm9b&%L6B{zbx@*7aPc8Z5g_0J3rKQZb@mu1S+!rQ95$eO3TgiEqL)a zpCV*dYoYI<1ob!t_0*pwSHOqKjt^TOYP|s!-J!3$EyjK4KnaZ2pi@NX$@j$l;+Cb8 zs>AS(lbRceg5o?-aI%x}9b5zjg`6V_s&W*t`!(XV|N42o4^eVL{nGUHfdJU7-|$t) zzjMhkYzwq=QeU=n&K7M9I{j0$aIXI&H-JshDj$o233j(axyV}(uN^l?beej*VY5+l%Rx4LxXsM6A;X$<@MrXaByDUhi zdSEGp8$FfJ%kUW-u7$R;B5Y>y?cXU_3;>|(ORyn6DhHlt*(X1RM3ULY0RjU^+9v>}X@rB};4batkE5RWwfRY>7DzpPOIkx7oE z!og5jbVnXFQ>SiZ|KyLu-Sf4Fi&gnz0N7E{p)%;kVFQ4^7gbg{iM|p_OHA`CDx}#$ zSV*Q6X$JY3wY9-*%qzh`zP3z9^+&@kVc$(sW^94XjBkOA9 zlP(~RXy;^a!iY&xitpXIbLw*D&7YCp)@bkGyl{PMw$mo{jm=a~%;)e6BjU@%*>oSj z8TVPnZnvX$ zvX;6ybZG>jdS|xgs}~<2l|oPzdZ8|yt}&#~I?IGd6^D~*V1`)`(|EfZNguh1hC9DF z6S9|!0o${^R$q(jRyk`=NcRczO9AYkuw*LL>lRa6)nV;>E@ex*3Ty-Z<;BH=_mx#; zHqSHD$2L!qjOA$hkrG<|6>uZHcZflI$=ItQFDZz?=6ja_!%mY%Hbe(lvUy@64!pi( zqC!Q4N=ZDB3upp7?r0?7boA5Ao^@mmSZ%Wy`}Ep)+bRB)osleCwFO;;J0s3g$^0oyjuemibabK;{3g#P zSD`5h{6w$G79}wQIG`kj;iozb*VFv&^U%lFPlKNyG;9vHBX6(A!>AE+eXdk8*jV2- zrx6|gE)R}2d0BPlI6B_285`k8@(!q0OlPgv+Zy}KMI`^u@D`fT0yorgj6LT333={! zf$6Do@P6M0Ctfq(KI0DwBgDd#cqZiP)0RY%#+AUNbEEk}VGTh`Atx9#=u>um*N2A& zpa~(RYOI~IL8V@Qx{tgAcGBoojRky8MpFDIP%BI>#X3FB-NW=Mt4&?K%jT;BW^g9# zAlaN2R0stkUr%p$OZINc@aC>+ERx!u9!>zZsyvS@NyR4)*dokbIBAGHg@ZIOyp)E2 z_ErR~mR))JXnNmJQWObg%}HeyKjrfZ;P!vKJE1-=to+3ruEiwI^!VZ=TPw2ein@KU zeLbn_0&jUqHO0lw0$Be=!fTg|{dIqvC-PsFdlELBKyYJ>f{&`i=FG6k4a>Gw zGwyITNizPC<5^-}C>!v5KFcj1z7Q(yG*71aY3QJqE4Y4$zQSG4fc0an&-$U&{shrJL(?x&<6leue+HB4(5pS`!$KiU3D1oRP1>R= zAdr_#LJn7_EhkPevy_@p5M60vkuska96FK4M8@4@#c?BK;O2YyMFa6FCd~Z7_VQng zRqSoa`O5{V6n~2`^__46cZ}@5F4kJq2i)rZP7`)%Gf`-(=@_Ls8Fo51N}hp~Ks_JQ zj0FWVDJAAB%NVf^LFWFq8ua-RpKh7c?3WYo0`L2P8eg-n6*7*R`uGl;&zJrE&@A<+ zVL%dzv7}4#kO8SU*~+excUP2?JNzm>bBjqD7dBb~g;D+5+aLHx5b`EB1}BPTeCEZr zem7M#)a_wju;xEOvf*!LewT?eMBZ|jmj&X8P3e5PYo@op>|l|Fy7+z^o>1-n@pI$v zsdv|-kLk*hG0U1d{g!kNdLu@(uf?i{Pf^68bxfPcR zm6LnYb493Y<*-S2+n&z|KEQ|acbxK7-d2>U*tC4C;{K0;|)Eh zCYfFasv!vt-uaZEf?D}PYx4)IU;$ZDnw7qzK zZA+E3rrz3yqB&q-a^;OVdSeM1Cl5=Cy>>)^Q>b`E9Y3P>GD~d52D`NnczU3BK?P% zyp4LnMLx>9qC_0^Pubef;*T1!U;B3Gp=RqI8+V7|(Jb#5mub+lW4;ZDQynslMt|L@ zMN5mZbRG#{Ru8Qy;hao?YEjFjZDYCT8KPk$ZD4v}v52x84<2g4&*;#v+d|{;5)@dx zBaZiQCCTto$OAQIl1w75vNFCZPquW|tyhr7TaT29LC2T=e#nwUnnMX=G~a2g z8>e6>rXMfyebosyBsW?hP>^Bm=Z0W2$7#onl|F8fY|(ETVW~+yaAm5q%D6Wl-(;>x zmydaqL4|c16W~up`3`{kf$Z*#IgCl9PfVW2%@~|1xhPtUx{Fw!i(Teo06jVUMq;Ae zhB;ZDgG3G~98<*2O?>YxFl7kXSV8R7Tm^{Yh4P<^i$Bvipqxht{LK8h8nP2ca@OI% z^?ME^QGM$G0kZ^z*UyrCq5Bq{2PsU9_g(-ZQac&A1%HyQ{Tcsp_{fDysP05wo zdAs%7nooDEl>o=tzc4B8StlkX%C(tD+U7pt2Yr#!ot*C# z1j52Efda5Z$1Uk|GCF_6o;S;2HnSY=oE*>fG=(5JH_EX{_bvd;CkmAER4om~QsxeN zNqA{9Yj}7{aJSkX>;j`ysJYB$xcAXbW#n<%Q2fa z+knHmUpBg3y$>C$g;At#dfQzD1J`aO47k%lTi-tTJ0A9qdgS>Xm&DcIy=`_;rjvoy z@dfaOX6$%Bcm+;h(Qb|Iu>!vP!eY7agfKY>(P&_gQj?UI-X|AdPew%5C)N<+9l%6& z#NsJ!RZ?3AeAE}ITaM^XegD?&IWbgd;nS=Z0@M2Yo~d{1D~WaVJxYw$$MQp%f2XC=_n+_P5uuTAEiyM275_9^Z_P zUS5`Smp#6iw`;GhuQGmZm7E)gas8!*{mu@X?JTSPcK_!XldQ)T$X5|vCHt^J2j*MR zD<&2c2zb?9|3~uQ*yODJmP1$L=3FP|a5{6Ok;v*- zP`@x|>VEzff(|S|Z?KXsx*uRE z7Ec{Vd&`k-t#5q?bU&4W+1hUpXCdq+q%I)+g}x$0^sd_s({s6pyY4qN=&U&JV|x#OSd$VA|)W*HFTFW2uMkHzvua{_40|e_%xh7 zXYc#o*Y&#=f#cHlli7#?O{-H@jKLF(khuT?7$lg%+(OUFYjwSw)!o&jq^9+t3^Pd) zNe+NpeVl!(ZCk4(zmd?=BZxb&xibI34;CPCU2z5JSX?>%yzbFl?`n19R9rLlsSey? zdbU2Ayb*1wSst2nsnc=wm`LI^t6)=7vYg!M$i4|l^O&6fyti>Hi}=*S6ZJ#M`Nwn2 zs1x{Zb$?d9z2{;xXlwdmFlP8mZdaLh#)JgPOoL1Yw~G@4$z0*XSNCl-XlkJw%&ezl zN3>=R{#`qeMb8r$cf`O6vZIZAy{?9>(cry`1!(+<;QU0e2-n5l7{8lLD0!bzzx=rL z$;-*hx|C-kJI$J=!Tjj*@^W{BL#6khikFGAiI200n((vSY*WKjjx1FjRy?QTRi^C( zAo|5XvmdBRMf-23_P49hjNT;L;v+MfEFF|Am`EK9a7lr_h0`%%(UJM5L7=GgYs66) z9m5?_S{Q6!10n*xHP=1^B<_y(>^46;k=Eu%vJ6$TOS1yXxgDY6K$6v^`b&dSyr=Pih>)st+wYlwfr3Kn%Z&d7Bo3V~Q&pIC* zYWNh^aN1)AR!BK*s&v!GGNL_JWAw%WF|Q0#AW74NWVRZeRzBWeF=dud@eO~t3?!UA z=>t;7CKj~QK^q3=$0bLBqdi^wU4vO=kApq|B;PeCn3fM&RS1(k>1>E193}8H7{hAG zU$KNaIh!y-ikR6)8iTL)hCs0mfd})x4H=4@P99XTEFL>Cb#ow$^&xfj={4Yfk+HI1 zH%mp1YBMNWTA)rZlmfRu`uDHQo<`Uk{3@a{tKyTwWCeT(3$*U zQLEU8K$F$On7>~F_3SR?ngi@=BMY_ZPU8o-A-Ic#iGc6Re&-E{68lX`eLLlUD~Sf@ zmCZsT^*%aSB8DV3GhK309!@QmhI$GrE6V4UTxW~`$s_3zV;kMbEY~^*q*wTuOz?M? z^|u>_I$0&2c{Av9kX)TKJUL05ZdQDCOAQ)DtW3+1*T2lwWXhKNrs0E_gS)}MkQn6bFNA|pACC()mqVfa(siG z`nCKjTa=H748f}jET&zRrWAWRY>pj%E#a`r5E)5$wd(h8c(RTqJ+oi8+{Nkhp6wBs z=QNyMJeI98wQzG$gST{-h5bf6Yq0{qjWseoTNA$$Y_Y$3`9&yQQak~Nsr^-%S*zzk zk_f9_vc81ViE%HNBo0PultKoLcxZ@PKwmB=0#HRy$h=6AgyEyWJqS<;i0L^X37Ar+ z@BD7b0ry!hnh822wQ^lrTm_n7RXOOp?+)B#miRB-4G(1N(9KkVB|Aj3T_&}_odK0+ zH4YIgajr3_&l1vvZ<-yppyp#Nr;1-c2d-}f?mbK2&#_Ay2_Kc#s$jwTBwJdV7LIUZ zsDy;utsS>cw`y44l)!$s3U16VINFs_Z<$!XCX=^HL9K-o_2|^5Ykn%?MCo$>y4dCY zuU;Gg_zpr{WmzX*V&}8Qba}oSI`cO3nbAGUDwlOk2<=hj>4TFMW;iz|NUHFc9(XBl$)_BR*?&S^jA^Dbc8iHuu9Sk%e=5ai$oH0jwdAuIi7@6Qt2W4&tK9VGXY3> zMA897(z#un6|z!9ECc7+X$1+hi3`)R&Zeg;4%53fNAqrKSYl)`Fm3dCSTs zhR?@<^$`tV3t<_<3CF@GC&TxRdYk)u{x^t6YPEZu-1_#Th(lRMfL)q+$$PPeV_v11 zxisRj@@Y#%d5C)7fPfuIb$_12Z}hNOuz1T$t6Zw#DkM&{r86-Z+eWTno)qeF~Vb?4}9W+=af z`##tn0ifvQvxG&wlY)w(H2Q)E>R>4fp#33NN_khzDLawS2Ac(03ZiWMyx`Rfg!@<;$mZ4JcRSc<3rD??hoD&7OlU9=dVW!0e1W0|%bt&tDVSKs>#bt7Tt3)~T z8TF%1O5v9cwLUAVP^-fCB`;p6a#n)EFPF3^c%IsRgr~K$)rihr+Sn5f0ET}BU~j+R zHR$Zn0_eep_mw4!}U^VTZ8XEo75b1PNVi5$X? zdz~A~#h$?LOj}ZVQf=-Ts<_l{sWB0jK}cB^4d@oG^dKZ+unuo|FU9GvRAo^T1pOUA zkWGi7!4xwS~q0B0hzma!$CH&(!1S73yUA8ry`2sGscP>minvI>mu(`>}l1P8D*T#oO0e`+swL*&kN!d7BBEU(3GN{hAyU z4q)w8w0fS$t!uDCB;9(&K#K|%)1|m|`w1#NfL%_U=rkJqYP~G+I}-;rT$X2t#4wL9 zCMq5iD`EgoXKD$sh82i zX%5gav454--0%l$NDBgB5lM5O5VHp3N(c0-4Ce*`_>iw(@#D!+uq5RJaKc#!2w^aQ zkOv?6qTUNQt|qOW_44o$CaA(7`d5~e|1C%2>z<4;HW!W3g2{bDw>V8QA%Rh~ zb^6@Iz9#-KcnZ*iD^K5_$hsT-+D&7ViByK>;=#BZ=)m^9dHv9t&H*Qumr6r$)f0W?iG9qdn#!rx2U@}wW_)$^n_QHVKQtW|(B(*aVY#xH3| zXZ;=%+tC4gzY(u&ZK0`sJJ`+ce2H#8fvjb;i0d9^Q6S_aIl6in&t+ft$|!VR?yUw{ zXU|5SJ0iON$WTA?YYEJ914>+E)w8p~s<--^YU{eW{eSihLWvW?ry!{B9~R#~e$cM> zpC8EZ=H`>52vD)gr9x##6D8dK#b4crwI5vcxgFU1T@xOu=)O*3L%btX35u_ z3y{=6g04SBMg%7S4)>k3$Aak9?2|C-Quim}Gdu7hDDuSvq|G7G!9L)_Crm zfuV3tp{Kc{Rxa@(9a7D!;lRwZI%AS(hB0I?YyAQJ!S1SK!UyRUH`+IKkkNqj+^tUJ z-PAKQSP|lg~P>3#WqykTVx^ z0LMGX#cxre3jS)k(nVm3%Zrv6B(|^KN*Vh6 z4}_41n~w(uMdl;q0iaW{JcK+#?5|Jyj2MQ&t(AV{`7Ya2LzI1)9CL4=@72ZU3+#e^a;N&A;U z!{rXQVv-j%V-%Buv!&=|yR-^GlOJ1%raIO)S?JKG*K@r1O6oYK`3853fWtO*eE^yg zl`K}FB!F@p-b17ytQK$qTF>OfnPDI6>Rd*VWpa^@ailSr0>UBX7qiL9x|$I+lXt3q zuuS*cDi-jF$6kLaL*b&s`vY1ae+&e+j0nt$pA2yM-VG>A))5Si`5n(F*tct1(1j9D zpFs~%lL2}tA2KXg^4gE?p7N4233Rx%Uu8+cp-|H*SEgfPSRnTZFJz}Bdg>)y9fZd@ zjczi?(y0mAkLC!UjM)2HG#Plzo*jNQ(2iSUqQ~b`yi^(e<_6XWs(+==#mdOROum3& zVhC}$1{BVumMa^4U|6rKTsu8nhP>XN?~hG7>b+&(=Dk_8c;q?yCQKR*jNM1u`)GyT zUh3K7f6o(CQ>Vvf1bSrdSb#OXRo{jL#pK>me2*Pm`9`jx77Y}*S}O%=!1%Y1F#6I$ z?fKtAt{$~NZvy6*XSl63IB5PkBw4eeNPdWx-8Pev1WMUyD4prhxxU zNmCSApd~QC4+jbfmP84rH8ExewzDK5!`Tr}v<|2Yr^EotTC2>T)u%tHkTppnO9Uj| z#2AJ~#0p;v#C!|T1N7iwAy6_Y94SPS2pImCCdR|VmkULD3%~##WIn*Sk__l@YkO3T zK*Y0(tK@)6O}7ubta2f&WY*K)=e{?7r2E1|5zPZC&xtGHFDJemzYHMDH&kOF ziGsoCTRE$JEG#U{i?e)3_58MO@Rr7&?pmktA#TB_gcl;=YE%?|1L-~z^_{V7*TVVj z=G7^w9UlO429p5p$o#E$&TcENN9)}RE#vcbCR4OeCo@}45pU&5!UdsmwZ<-HcQ>Fy zu--mec>b`)4Zh2Z2r&kiKPVPZImasfpDAQODh|0~7+1FF=R3T1fojJmVl9JSHgx&W z3432xg^FPiTUf3sMdlu}rt(+y+m7bd5qSIY^mz&|Km+QjlSJn%L|77GlkE^U9KYom z()_~NpwJ<%pyZ45vt`5Vp7`#U$ce>GHM0a!S{%eEjQQhqhnTX`nEGf(&3~hT| z5dwMz6a<+C4ki==ok%c?g22FEAP{^AEVpVw+d8Vyf(H@qA8t&I1acc8I(dHAgY|%M zDJdg@Juj0pju_+p+`OP>_QA!*OBTsDU&fuoIC`se8+Swky5&o^;7q2&2&JZle_3Dc zsg0}z(3Fy_Z36&Ie2j2o1BqzJnnk^MnOdSjSd^pbFt4es+=s}eYb5=#fmfIB+S1QL zP%D^O1TWHH##C<|tZ_)G z3T4q0Q^KPPm&o!}D)<~=T7#?%aUqjK?~)VI&;lJ}ejhx;`F*SQ78G=B$i^2c9C{If z-08xMKGzMLZLAui_x%imk@+duoV)wogP_H@+mRxc?Nmm zFI+}#6zdLktNnSvq1&Qe+i4K}g*YBGz@G;pWRXeufrRP9tC#5wL|w!c9Obor+1@>t zCfb6$>RLK}z8@-@TW)u#+g&;^B30O4h>;Wd#~a~}uD0zC#`+R~MEwvy>hlCf&ccms_**ouN!K0%5GbyT^~Lb_Q!9*^Cey4 z7MJif`5ptTS1~~RQAe$D0Olx}MR-35%UToG4g>sUNiZ<_wN1+9Wa)w@b=hpArA+lP z5MOx^1o5>goS=so=KCkq9_aXL1do1Y`dkNO4LOpvt^avl>D^n1PAfscvODeWvwvty zxOkP?Cm4Q*W5$9>;WuZ3k?#hR-WEi& zcuY9HHs`fXnzU4DrQ#O}T8g7pOvUciF%Rdt?8w3c*V8pNOMJ>_{Y^WKD1+Qr(UV`m$#|63Ba{l*5w40{1r6w97J^pE`)SJhKRAI|!U0;m1$l zq7DK>qDSh8Z}vO5VPe+FewN4RvDvBcW~99Am&mfO#M^?iBz$t~=K&06WY-z|Z|1^6 zg|lX-6mYlTiF7X{cX ziz!;k8drF5h{zJr7jVGNcIlby>e|2)yf({G}`?A zlTmyoyN^cTwHC%$^}=s?(kY#faBM$U-wTk2EOF#{9Fd^)yUIgZ?Ht^2`s}cwL~Xd3 zZ@gp}aVa}}IhwdIJ7|{L_HRkqh@?TT?;_4wTLC2;kVNSR&tYpNIZuqN$?vHgjS3iP zypF4ApYCIPf9O|A8biYQ7)_kZolQ>l^-KnSRK@F7)H3}XbbqnvWqg#={%znOpy+1=X)> z&Qe_($_wU$*@B3URb0U6dVjxz9tHtR>c1awwq_cNqoaq2A@}PJR%c?<6>uZC5r1Cl z>FbkIQ{wta4DrzqK{3H@==)~~bmLCIvpN!(s%Oat-=mls{VI1^rhGA9ZPFt6)@?t- z)GXb@m3au}EsNd}cM!0fu!pY%Tm8iA$g3?lXqiirnS}~mPeUbc^$S%T`FTWP{ z=8tOS)tM0pWXh7DB2-ObF=JC6W^>w}Uif4n9MnRa`!Fkw4A|Wu4klj-g}ILpO$9e( zEVgv`D=qF+$_kexGyCA9NhvkGh?lYsg0Uvam^Ycuz|1{V(b6cdlVRs%Mz*yil-7=1 zlPFe6K6@KS;ESyFiFy{wYuZ0c#|s$ulOaJmpT-hUC6#3*7svqS0u5bEx@YV0`3l7> zeUTxOY_DbgQ2-QUO!1T0Rlxt`Q`Rgb0M*(i{fcRlco9C=dYCtm_Os4I^s8uOSfUEr zPLfm-zCj+2iV8DBc|Zt|;F8zuJfFtkF`8jr{>JAbZ`&quUnyBGzy~WqeSPzL>I=AHQ;()}>lZU4VFAzOPB8J@YduKDT{c%1^VD&c0 z&P%Nn*T7+=O|^3B3-8!78qa&jPMey=#TsYja&2$}!-*=zbDZBG!Nl@fX9X7E@o zkwHO?E?Mo5^C1g$(;o-2CLw-SZ(-2F1>dOse04nK%wZjLbJMFTAzXA-tLcLa5|UK;&`-4sbV#danR?1GE}1Y4Z!A%(SLR9;1EC=>9BC6vM=OsHh4TmJ38K-rx3_QF@c{9= zwt5KnD7g&SDDfSrWl$Kk6@JD5aXiK#1rdnC+!dDPe`juMYis3Uwte|Ls*ESpff}U0 zqa2@5g7k-t8DRi|4^3byM_aTW%`mFo$gHW`9!Y*q`mgk#Pd0`;K;#rK%B^FI=FA*} zsoY^e^kU0rStp7#20NB#9Y@&Zj@s>E<4um`XO*JY5_@3sben4+hCc%V;MhO#e*U}m zi$+JPyQR|e7U>sqG`;M!++#D^z*=C3(jDqun6$CYdL}N%)kbF7+mtdn`pNXD+Nd?_ zxdSABf!32}lk5DieO9?pvbl2SYYRT)* zo%p=)cD$eN`c&|)J%Qt$TbCvpGOb+4Vf%&Qf7OQHDI7Tat*`yzjllBVPSAnx=G~#P z;q`s8e2~cF{p7-WzJmYD4IA@M>^MPE_1$C~9u?aS8~L?THKeW-q-{5~T)roiVq7q3 z85kqIC^CrZ^;^EK+H-fXxpc99WrFNeG?9~K@Yf8;OMQ#Ol8T-C-B7+Z$>LC$sA!iS{DHitr90-<@O9iYLX=dDKk=JN-WI zF3j0q36l0Y7Iy(S?ml|TTeY+3aV|s)ieV{ftiIhLd}FurbKPUCMBV8}T5>A5V&yel zQ)#H!_DtY2HRd@h`JprC;KU|B?iOG8`=s}}paFPME~*5yLZ=8u~tAs@-IYu!lE^P}kiNo|FFK;p8n z61hXRR#j3lICz#|ekb-S%JA+mRX8R4aam@u&VJ#xORFgvznUk3-(yz9=YIH`$yUk8 z%kS>riJm1+ly9Z+};pZ}0dx*aeCtc91)aR$RJt0o*8P ze10DOn}baC9(tai>O}N^$6U{9ee<0Lw+ z<-3>`5|Vxn`IhmR+o4w3h`jyNFD_atUvZS|ny$RBaR0yXl7sMk4nis4WDAp+S@B?u z3V9vf59Y~Txy6dMEogI~+EuCju68Wat;GK6*uL$ggM;O}wjDHTJbu>Q^*3_N_riK* zS1U9Mm5m%$CL;@6FJ0Ihiq$kZ6bYzPv6DM{oBga_sP71dqG)1a%F3-KsynS~TPM|} z4Zcqa?WIr5)aeeHZ}jl&Q*0&Pe@Rvntx6)jO88?n7U|gVu^l|pam`nyQSJ+U+eSY- zcx9xBO2$@WvN0N2+2T2OGgiB)qudn`QrOu3n6W=gVXfZp?3VqIedX=obXL?y`$+c~1H zZf+l`!*x{1BrXNVj^iK4+3Vv#JX#cBo6zvs6)poDpBF$?KQJ`O{b-mL5Bm%f^wHXwsOjI(9vgF44g zKi`r46fITAyl3I#Zmh4_Q$S6&_~-kgN}t=E2?iW0T$~942zuxEYnwcq>47`ocuMbx zS;H|s%TO>;KAu#tg3Gl?Q)}@`!u#p|PuFzHKw7!Tc%}X?YnHzVU4rlq|2Q&0!HN?N zRR>cnUz=#FTvKL|iabto<{j;QzY)-JapXgZw2=u;RN%afgMt(~>4+o61)rr@-kK?z zge%6Eibf}%oUAW5enO~g~{lm%Yv67Fz-!lBXs|PJt#m2!)Ku94kERw zY&~1%n*a60i*ndlIJ);~LuI1}gawGY2bay)`#ALl7;LF`5}TsGCVX~UyEXb}5^e(I zhJInh&i-L{J(3A z57#k#Y2)m#kT0w_a$@%`qMS=KBs^m5@~Yo>6|;ik9JXC%~_Yk-Zj5eGbD z0ER9D{OF;#Z+ZC$RB%$?2%Jq}Tck-iyr^P$skX1;soK zvw(e)F76rrfk~~_(=oSyt!hnlxpiHvf3gDxei!P{2u$X zj+(EuG$F_cDAPv7s5DE|>y({_tV_t+U;AYWCdY&y8if_6lk5J9!GpgA;l3VY%YARC z7Hdt+e;GEcwJ0-!fWm6#sWf#=HU@WC8qSwVLaL~^Ngm<)VfIp|!ZY8?&UOzlhv?DA ze!CylBG?!cfIz|(n_@QZ94pY?E?8b_YR;w~q%brli4IMPzPr0T%*-Z}10Mk_{}sTE z2=Mp!?edLH$y2(=wF#<;w;rqLcZ5_kSP{w}iE zymk~(ERR)-FZ-|jl~=TR>GDAZKRhl5FCw=;h~DGF+DN9)5Z9x_hP`SJ`LB%EVDvzx zXb1@zdM9}v%Fuq-FwTX5%_#!w^LO5JKfn7@g_yq=<$8>TkU3UXW!~6E9SYkO{hTa4 zVb6aq1oH*SoNvY zYE$P(WQl)1mQA3blkH4i6tHc$@50_s7QUEz-CR!++BNltHsg0&vc>XcL{PDo9y+P! z3lV#ZnTls!izJp;5)=XK*8xC@b~M((|ZTKZW#|2ppuZwG7zJ}-*>gppr0 z{4;li8AL+&ZAw-l{u%8)i@6etwFJ^oo!5iTVkS|O2&W3ChP(AJ%b&a&DPE&ZzgZnhK*9fXzTyQV-pbgO44h20Th?Z;dI z_j_t-G#oN*hmPQE-~F6JpWTdAy;TQ@1-Ib+&}NU-Ur!yP^2}^KyI*%gW<7rx^$e{M zO4G5EtZ;s!Yt#&7nfwrG@_Y4fbyt%zVS01R*F!V4-)Af#9koWDIGBRyVGJ^M{jO!p zf(eE{+Z(JzRW^1k+$7F>syZu{7i~%0v|=5njI7Q}^T7))1a)fGSFx&}4H_iG?Jsw= ziqy{uhMa!FgJyB!9`?g<0Z>SL{gJKp+urZZX|&|N*3Xg3>#(gMi*%N!H54#r!sT#U zw|P;i1|{MvgZ^&rOoaJb68~#eZDQJNu?ZT+{@-eBI4wwH!`1lS?(UWw{m)nLx&Ajq zLL`?g@6T4BHYPUkRHI`?E6W-t!3nb`t+!Z}>guaQU>skJ>5r`@v(MY-J=f!E`O76? z#D!Ki1&0j{9~M<@PYXj(vr5-7w^8kH786*2|1JOrx9#5*)Z2ZZK@u1d&F~tT*%|)S zch#j%_gk2+)vXVAYPauTb~Ci(qmRq0m?fCUyjB-z$IqV&8VJ3DmLXJ>tsLb&NzndYd1l z0C=2FptH@f$8t5F%WWiPJg))WI~~*#H-FP^=Zger%TY?lkMK3iT9%I`ey05|3IhipAIr@WvWJ$m;D;qN<=ds zQ9F3USlEwTg!u;~&L`ZE|Lff%GR8Rg15}cPZMDAf zT8q|uxyT?D?+xu*MWxqSYjd0Z$3aX|)vTtcwW`FC70aFc0ZEI3nKVOQHFM{Y#aD)# zNMd8S_GC2kes*Qnv`r@pc;ftvpDuv410(NnLLsC=S;SYmU1c5I>BbD3fk@Aw*|q}< z%)?C~ZPUJ(toyxZMid1nGnMWJuHU^seyCXd=O%1j@!^A`skbK|>P6x2UGv zXkKB&3S!*ymxZSGSl~iYvGA&;AuK+=I*pfI_j~Pr{{^PktbiZ7ORX<0i{xD0EBpAb z4DO}fEh~ps@T=ibse~1b|`;sT+=j}9=({|JJr~lW62+5hgMY=}&l(_3&L#@qSN{vB# z7fMA%X46qcMl7wbv{|DmE4jcIZ?YcnFq7WgCoFp#*+Ia((Do5^m@U6?LLi-6I1m4%!MyUQJd(iFhj(9*4%=v+0Wu zof%wOus_lk7H^-lp*2j8^(r4h{f-VkiipWxLkjDCj6)HXhCJ;A!l{eNO%>Onc{fpq z`zP;x^vEGRm$f@a`+EIdj_a-^-0wmPNcp>hpZVm_3v5jrBg- zi=Zx$d7d8h_jK?S_64F1CHk@Tf$M8r;WHu}Gm6B_Uue94CB%K{KnV@!LB+yW7sCr71jGamIL&1p9fxnx%-;6w>kUc2tcO zUWbp^m;Z#;&}`;XqG!YNVsbpe|M?=zB;5f-;h`*(z>+yp|kz|5B7@pK{LX@POmIj7H@{9AIE!6wu^44{7d3Ho06}rxT5^$v2G@B3uo3!Bw_2nl&lKj@mdg=w)A{{lwvwqj;)FmJnYV}p`< zJ%MNN6Md;hMfLQFPX(C01uqnTB~Ig(|G5&o3rY7-QE|Oe{PqPHc;;ho0mi;cGMa|> z7mAA2J8bqW-B0SKZ|B#>pR9jlJ<9Skw)goC4?l~v_^Ued{zu_!N`V_Gz#j+>mjo1n zeliB@F-K8I2-4S7T(~J#SYpQQSR1%H<{HZ~oyyd80nKWg`vHSV-Wjg4UjLrX| zjIjKBm)@w8{(eZRIRh813Lxy;g}ui?rMG7BD0)u1R5?3HXu?h-DE>5JpMwTwDNYDu zzj?ZR^n19Co@IPP<@sYdve1+?%e8}Jj@I+vx8!8v*Pordr{Cmm+-74y4w||YtlhU?Pjwql!%s=unYo9(vhKnT`_m8M* zjkXnX8iis6DYfA7LcM;?U1rVH=z>iB!}WN6EFN)~Kva~bcV&Iu4_6g_)eZBUnk~lo z7#j;u%NG7d+wOm#{5)RmyZy=V198X=?i;~eE#uD_XR&0}lbG3+YotyEcv= z@D(+}N*hN#AEZ{4#cy}p!L>@;b~#25nT0+;?-&P5r6@i4%r!LJ)rF*e9w<5Qa&%Zu zDWHOVXv%J1uh;uALPQ@cq8~brRNyfiiN^j8M+j(mvAyGc9{Gd=BqmdAgC1>YXN&Pw zk44)_^5jmG9VfR##DIPcp>s|Bvd37wNti%`XK55x>&nk=_v;P4e^tZHFKE&Db&-e8+r4ICM}QY2*$}`x@x>-ewv(hZqSBV#avSNx(%3nVbl6153zE$(!`#5I3Ai-<%mqfO4#{Jc|xwiMxxrr<)w_F{M010i}V3iTL=}e0u4v zhiQDuR_0|Rh|toDiHNjlN&y=(u2kev+39^mkZMiig0+CfwyGemzT-tsw(|gS@24y! zbaJHsS~lt(u76d)2Fikx#k6Zs3aSBgo^6hjEF@a1mPW23Z z=trpm+BQ(KIcGu(;)-K($jEcj)6&(2^$hj)J~uNDiXYeihh<#7;zvN^c@WNt*t^f4 zmzTZD(5YZdJeYYOFqv>z+~X!reRSwBPj z>{^v8!+7vY4#~Ss1{?WR^X#beD?=y#nSw~u1}X|_>SbEa%t1_rEx-uRFbgoL5E!GHnW)L4vlI-5;iM=oRp7~mEDS~B39Tp*9MS+`aVHmc4i1_>MbV`Z~rzO)-Nx$b~yD3Pk!m z^K>lKUXM|!x$ZydQ>F1!|B1oISMqU|*$VM|<#7!kxbAwkN~G=?(ScRfLeiG;q3Wg{ddI8!6Fy9D`@EO zd`p2$r+Yty2m0W|pHl5DqUd;$#&h!xo&KEf)|!{j9HNB3Fxj|>;2e8TQ|RgN=I;Ns z0KzXqcv({#DcyV3UEO=mkgQJ;y3W3KA6#vp53%8Ne{*6w&h(|C6>247dsXiOmgnGm zFd`X(<$eEX-)5Ng7q!#BX2 z57=~mu!s&JwZkyLEnf6cUJs4KBq9bxvR0)=S#Y4^5*0?Jb+VJ1t#zOsF*1~TC z58FoWc3gI1kK@!eVTYA(4qhbj!zz>Z@|pQ@35EVDO(v@(g%!~s>i*Q!vF#uL0(eXV z1UsG{hR=YtEPL-)&SikS@et}<74zY*+wzzRdFunk_^sWl-Wlx)hpM&aGF_ZxkQ4*T zk(gLRyA!1fKa%q|-F(Ui&$`${#rcdTnW$}A>D@|Op&iSXi(SFwyA{ByHgb)tIQ6~_+;=Wm8y0=yB)F^r4sjiBnBUoTjfS* zW2op}w9V2#6gkQNHEdFeC-@94(O2q{kbzt+)4;8`tB=F)Sq47Ze(Gxfcy5;I{~WRjFX&*od2Fc`|_){n5*h)g5Fq=4+3|G-&?y*={jM zP1zfuuw$Ss=ZdNM_y=i4*{i2Jt#Yw^yXiO5EGtZ8t+EWq80C!L6hxmY3v%ngh#mC9c_k-;-00B1}#rLPnW2!Oxet**5 zX8u%|T0P1`bCKNHJ3n9fxV;=pV_lmkI>qjS0(qI|l1rdW0tjxKtv0$8eeSIg4)obH zJ#*m8E=kRP$~1UiVdUIh39K!+J=_6K5L0ReeBD1$cNKb6>7%NdQ0 zdnQ=4Y*@d{m`}bDn7{e5em0Uzg%yBmAgz`@DO$4@SA1&0^H#I z{DbqSVeyL%3F%V&?ar@mT=B=AYee~2>z>)g$;ewS-3*CWH6~u)_y+~i|C=J`^c~+% z>Yjj$-{DC?A}Rur=1C-M8ABx294mY*8m1r51{)$ybSFg9VTx2<+yE1b*` zb9AshP$^w}>Rkqr$?NH@e{gk<6`{nE$-$smewvU_N$Ft?R#)rv<2;=U6UM;j^+LM zFh`FJ5K-yRJ#A&V)r*Mn3R9f>jMhaZ>96ApA_#cbs2Vp|Hy0?h)aw^7u%)yqoTyhf zl|(Jg)98`?8E#Gqk zEnCR-#hEkbzlx=M}2b}m(^v&(0?TS>)=HE23PB8)Oun~LefL;`6seq7oMEltWS z6XjB*p3A-C%XQ(upQf+EQ0fPNmA~%)TM%X1ctlP#1#zRJJWtzO{}E05Hi)6M4Q=$R zasSssg;-GtP`OYwr#mEw7dS3piCLHFvvTgyysbL2?6qni9^y6D_*3Kgd-x1Dr*KSt zLxaQdT6Y_z&2n`ZHcq#iAg%%{5!OchYRa9mv3&it!I z?MKQG@to_AZ^)U|)3*U4{RzdfqJ^G826}ZDnxuFV7XY$Z`1AV2>^jvC-W!OK?h9kz z4_0=y^OkiH+?*jt9&@f22^nU{)^yQ-uAW9f%lz$?(GZz~Z2cOQnD<_UgeWnS()^Wl zYHUhS+*=Bk67&C~=`Ew$?7FV)z=aieC{Wy`cyXslumUZv!HT=PySqD-;u2_ZcPU<6 zgS$Il?)Mwd82O(+>)dzT6?eCiwiZVjI&d zsn|#yJ(3E*i9q_WRIo7*)1j5B%_J$4X!wb8Xeyf0O?AR}tKqt_3&BPhRKij$w!=!517VvN1>uOpffTxfU#6fIuV_hr~O_DrWtGE~=!96!pRADlzF+-71yZs8flytuFD zHtm_-p(PhN4A?|Op3Ftrm9EDAqb4`PJ@9??oSuA`lf9~l%+WW`R9kFB?I>YF^{47V zv{PSgDHqw0!H4t}`SP7_>VqniHD74Z^c*G=I6u5x7OMcdqraT)4yW+e>}!@#P@-A) zX`yC!$h58K9lK*}U;O)S=E1|L`C4bgnm7u-$nxTU(&O0#wa{jNIKOT`w_m*$cpfi5 znycXaQ9(s@EgpelYJ^)OCyhXGGXpdXwO&Dq*dAz0DaFw2fo0o9{nsQeUX~!f7YH2w zCa+X6=kWU|<~c{?{FM~OX97;wbXJ|MSTD7_?_O7Y9~~(8#<=eZ-@Ch8LL9l1@-gsS z?4`s0K3RGC`t5=Jour$^(6i0L)5$9*vgz;KL&OfE{#A6`hM|bxQ~#`;@-vp6-^n8$ zdyd~$6eLm!&tzz-TGYqNxHNl0(cYD3lAYb6R~vJ|%g5R+V zo1*i6qff^P`##B=!jmT|Ogk@eSg%7+jC01mg-&5$hJ#49QgUwW)5IxJ<=+NFfe9-x-`6O|vfBudg;6?SfD&$|(yyCgfn)UkCnEme` zdIW6N4N2LoafDMjZ_h#$zMBU?_QLUqj_zFqIO&j#aWiL=+Rn2XD=Ix*!tlLj9G4KH7QA0p0`w z1cg@sAp$rviJ-I(3;vK#-4lxj#Onq|iDbb@XiWop*Udma06IwLUdM4)@Hr zR)Hu#*wTw>S@1_8M&|^Y>o2WBWQ$ZF0Q*{-|NGvExb~u}FBDx!DZPR$P5)GdpuA}Q z3wR|iaaFL7XD#4kkWvjUW}8H*;pcf4jk*S$YAtjUh3Zt9JmP`R*45G>Y@Y;t@w?fs z+j#4-t!2}jYSn0l+vq`1fw)D5WQi~M)Qh+1jCf9;y7X$EI`_S|MhA5&zG45X7&%=2 z>07+Y08?t@W)Ji`e6{eL8aQLfib^u^1OWr5GCvBjjKTj;bt5wF4xZIICGX^%FUap$`c1}FbW0O+4>lD>dmJD zOsnD5h}|`vRkqJ)xc_$vMUJZlcucVSN0Q}Bso+Qhs6o?J64Xh-av?@v0mjy_wFrSY znuRQ3a0#0w4huUxD*{3fqK1u~)5yeHz6VACY0awjY|{Zs<|@f0!PTE^%&^fdwI1t% zFhr!?&;hdFy8?y9sGnKz_B}xenb5PpF6y(Srq+pv^z>sGczPDhRioi-9Zb}7Ouk{2jcB4_k91SJ5a z1RH=hE<%Cfby|3^CVEIPXx_5DD z(YFN+4R@=1QA}P2Gd2fM3<}7|wLtu9#Rx-3WSMTV@$xC}qHZi}G@j$8r(+k!F|7d7 z<>pLBy1{EwdPr(ktu`%@LexC6=!+1*?|yOJ;G4%0a{Pd*{j4-thZQr~2f}^8;Ly>l zWs4xAa~un-(5hIJp>KJX;8X?;%};1J6Mt>YHt*#y73ZuPxfbU2B%!k;W0*hswZnh@ zv?&XS2`$uEhDUmYUaxGtceOh&zA60tV}o4X+iy;Q;OV~;YVYrga=wLEi6spFJN1tw zd0(aqMXoB7v{v7H>BY2Xs$pxXF}kKW;7RxB)k3a36T1?4lg9xZlHbV!VriBoo_x9BAaV9}OcHLc;-RbHX_HUe59YC!aibaa zR<(lF_hX=t5bxc|^eg~{D3ft4JNPzP#s9=!RF`f3j^ZER**=$+X!9SxNARK{7ue9c zL7?KY=wq{T*Tv?N^3WQ;fXESYYQ4nKS4T&1rvHBDGaXI&_rK%)UUoxt_8u$jamovD z*f;^Y22S$?Lhd0pS-H0wwQZJuSenhOV6K&;aP3-t1J>AwsO=^Z4o^2f6Hf9wc}Jn( z(p8kulh4&WLaL^6bddUmpaNd&)1RgKP+{K_>;PRpduOoPG9N(LVx{a;`q^@eo7-jK zxmd>D7u;k#Efx?JVi_7Op8#vd9HmmU#AaiSCd-7W!*58l{Zy%@p?j*<+tp&1$I%Tn z8p#@Fz*tSk;nT5UTGJxgZigMWptI>X=FDDqykZ$(kUq5eIQN&L4diAtNLNqC(E667 za*Sb-v|5b7f0 zE`rlXC1IzS_K`;K;$fz`@p*qppG$SlnrDH*Dp14eeH#31__KaLU!NaqlVSZ%r9|Wh z3XiT999})6nj!)r97;(_gA95MHM|@kB7jO7@WD?`nlcCt!L&L+j@$P&-T-bhiL>h{ z0dADM1yszV32zqr(KKLt1XzGy%lllXhVf~`ctbr@?c1H60=WQl+{B}d3d$P4p(R7L z@fSIztTK&$$DE3hr;*uxIPA|{s&+bk>xVo3*I@P>Vc)yhmJPMprHXPa^V`dGJ^$;~ zISsCFCNv2zyLk7xOZ@qEIkMi+qjUq^b+@n2H-j9N>eY9-wJ&R#CR5H@P1MR2ZqL>R zzH8OBwA8S-_oi)a`xMk(zu`DCnXuN4M$vV?zA!EOV`fiL<{vMnXV##;Ae!$COUKQgu2h%L z$!_G}_7W7T?kTK@JHl$+QfEK4F2xWZ237+b)WR$)_DedS(|Kvwx)fH>f(w0Jm}0ncXGiO zc4QTL4Nh9#h@@35A#Eo8dhhE!uSlEKV^_M^${+q_G;eoX(8gTas)aZTQKZdM`&S0W zA*K)=?FM*ikTHll005udun<9~24j^|ODalY2Lj+xD5(+Px8UI-6CyaK_5H7y0qK(( z3^GG#fpII=(G!6SdY2Lll1z-e26xrNUe3fG%hxVTbk$5K!42mwPnkr2I*bJsIfhM( z^J|)S{H&5z%Q87ZpN1;pH70ZP;bC`e8RV1s$nsHgc<1}A_gRP6(B$E7!I!xm6K^;B z&KuOJU5DP$VL9u=lSGrDA(>HYmh!Pe@5j`10fWbrLJf8nia)PMjYF8Pvxcvs62>l8 zUUv6oeC@ZVIs$(pB4D~ZTi4$WE!J;IcrIG8>c^p936Q``(>qgSR#i6N(ZI8@rIOlO zrHCwWnW6l;b{VYZ*4jO|$#iJg;)?y_Kqd_x&g9L16|lA#+Avn#JRvc|b#>?da@6^n zUY#fKyxV~#=6q3a&|=Z^G13?wCn6(LJW38vdR*Gq3wE>aULB^!4uBv<@U^0xp3qcH zylJU;VYiLGjj?N<5jB9A=QZ0(%Lx~u3G@=$I7A{#UcFn7zLQiR8FtP%RBGn5(BA9H zx2A?M7H+`rwRcg-aUM&Uqh5KLX+Y7>ci#R26WDHz`H0jn$c!5{D>5Nor8l|AumkTb zzx8(-FKV7?Cau8clRl}Mv5%Eg6I;wrN=5;s^3zZW%w8v~s@f7D+6D{OIuJ?^& zg#E0Ras9x8!-hwtm6kX}YSpf_BNvuJ^HX8B-yMHXEJiW({Z9aRcMoyWr78 z%&bPCumGxlcFZHe3T;n@%VhiE0*m}H{ap0>%zE76asikNY@%yo;&2qYqdv=Bc-%*F ziWm!)@htJ_W3j7>n+uO6a2i6xOIF^L*u5yeXRG~U(QLci@y@J4@*V}Nu4g5Cmj=d2 zLDD`OFB?JDKd-9=T3W&_UVC$vDxpkJWeLzq>~;pOV|Q8iT65%cQK~FuP`xP}HHcn= zeWt%ELi^z0a0jsNxpJ!EXjfS-|5xIIm#>yyqOAHxZ5#tp<$NAT*_vse%_O zA!|_0o1C=sT~gw!scPxE{M#}(#LzNsTQM(dZq5hsGyY2gc!BI)ps8M8=9JFMP)mKG-lLibfZ z>ljs$e8CfJpK%v4^c+<^nnccncbNE;AAXlpuvv$VfB*_a;ZCN8uY$v=HwPdDViQy9 zQNk_+xDWt!z=t|`DtMFt_>I?kO+W^-U@EX$t(x9(?jyPn;l^MxVq8eourvfaZFItb z-NA*}Blyk1r-`3WXwlBy`&Htzq;w`!f_cKP{qF%C1Oh0Kq(YdclqX7C1Ymz|7~E6+ ztv9cu?{l%mkTs#qdC?%@eY}v#N+SAx(^!M2Rk!~v^YG}bVDG=E+j6q1Bn^-gkPeBi z8VL)wEBsCadf|&e>F)BsWnoiH+Xc4G>o6g@&zN>h>lxbLc{`eNam1<+wgzL&@SH3O zH`x6O$@!IT<%SQDQA7KW`YTX6gO_DOTCu8ohzPdDco6>n*ByK=6go2;j3stc%IkP880)cF7 zKvjY|vv~|9NPWi6uD8Km?-Br5qFRTEjTY-&l8uvTNmu-xYKO;))z8BeGd!o(Qwo~FW+-}-){F3WI#s=t2V zzg1H#GB!#D{iV~h(PlO)J)qQP9WUk#swSNu^ZSNkpcP+@iH3-ropLl{{$eQQ^O&^CukWLpTAzi)*v62 zvD*y3p#;$?XC(%DJF0y~D5fcp2|^QZMs&p4fGo=5t73?#SY5L<8rkO zh3v0v{zeylZ?@Zb2{ZI)bMp|Tn4A=tCI8z6WqfL=O5I^xjKJZN9yem?t;$FNpz|+#`fVWs@*i(D zEU{T|X$;?wa_?9D7k}FBR@*->sbSaT=o*MRZ@()@yIH#+xI zTUOJuUlPM9aBv`|7@7M>wr9ju1?LviHRcSgBddBsDD|bNcij1yUTI$Cu-3 zBCW7)Q+^%wr_Cc6K*!tFbMJ2t@~FaoWs(>y9tT^BNmZs~yHz@6529j2>5HS_A}XJ@ zjo-~+*bzBTdAs%`UCLWv!lL$TJU&L{kOPnrkpcT0!oy8NQwm@U5J#X65SOlMAC3wIv-TokYr*%R#`vAs z+aYtPG7l~=mx5=oN3MA0%oRd?RJ3Pq8^rY4T#{Lv_0ZeLqZaIv)ao}s*S6557s0lD z__%pNwykqyC7x$obe36xut*mz@Au>(SZ|MOf)D^ehzQlHhh(5r;*c~i!@iN)&((87 z(cCNM3dyqug{UBG7;-5jT249w<(&`Ku(#NClqzihF_?TBzGj&EoR9ondG{wnW74iX zHseMMt2X`;e=}%A96&u&)dXV04m4{OhucJp$Ryyd-@_|cfqxh`d@okRY}Cr;Sx*_iq5#|eca9CB9^bHeRv*BBYl`Pn8G`uEN)~?pOThwkC+PlaM z%De=f|J0Ngrz~xlyGcGF+IS;`NgJZm!%3Y4L(_3HFk}65XrfyE#>%e;ja8#Yp?a`d z`~C1%fuJgJ1+q+bznNm|a;O#vz}}|7g#UnvqM=5zpQSQWv+2Z{qSv?Vr^;*_nO) zCySj=)m?4x4)t15v!=utcat$^8Dv79c3>gmGRr+njb&{WMPt+6+wap~^R$Z}D9!~? zj5dx%GY=c~TQPB1K!jd6pN&9uxcrJnut|b+Gzuj=T10mg7*rhrhPNJ$;tB|zhes;N z3C{QC4LHTS_Qk_eiV~Ful&~E!&uU2?G9!J<38Ie%?!`v2Bs3i-TXl$n_h1MNB zpRLX@a<)1W(q+;|B{5E3F^=iJZehi-!BM{ai+}ysg|ju}cQW)Ai$dgksUmg=*~RbZ zsoOZlGS-J8IA+N@&K=)By?&P0L0v7{how`S%VbEeBmz6;t?r6=Mt0OH(9!BCOS%7Y zaL4UvMni9*GA$c#2jyZ&oADd3an;w^`NCcYyNB1RNvW0KdM94_D`5rROuKb`Ep100 zZYW+c)Q54c^J7$Qe$C_$=46xEZ7PYgP@1^UHGF(=Y&oC)&3#Z(r@NTlURZ81mmvhVo;= zJ^DynV&jkrN3%PuOrK-N9O0L*S`59;N7rPnMfWg6Ds6$mlBEidOJ{~Y&i@5Oh*N67 zI5r$82q*~b)a|g(#}Ib%PKgqKP5Va$#pdReK!C?Kl4gZ1&?TTmg6Z9PKN?HfAZQrD zp-{pO0H;6#Yg(ca0L&1^rwK=(lhbs=N zKF%tFQCvLtq`V!ahb_XP%ZyCeKLc>WD~MYGsN98DP4h*>L=j*kN8cZ;-q7j4NxzCP z>0yhC0e?SK(y0i&rec88Vs3&@eAdN?*lMbf+q#k8`--vMk^ z8m@n9c0LW)m&eYsY5KJBaK!%6$kUu{)V)(~nan#bf5qEZIq7oz?4G1P#dAWbu-$X; zT_nDtB%ipH3s2K(-PHEw&>Kr>anP>)ZqQ!TW$MU_hV^lF_I8Lu06xHBI^XZ$KDtm% zltC5U2|S(q%L0~pys8pI7nYdl#AM`_K^(A`Lo1)mGsQ!rqJ&MTWUihZt(6R|Nc@Y1 zGGluyFhub(KLoe>$9t&sI$Q9Ifprs0sLtjyz3u*jx#MI=LBwEzAyb%kTFThE)&V+_ zTWNBwZPVw>k1VN`>dCXY#$qZlJA_D%b2|3-*W)ajd`N5HTXIMbcf1(f<_~rjn7Z~p z%Ds=7E9!1%sz=YF`u9*>%l6_NZq~qfjy}rx^DqnKgez+jDK$CikP@ch|-Gaj?j{` z`wHOX0Qp_-p$6=0=isBEyzK56nzaL!{nK z+8-RT#rTUJ00;sgsETvm0AgO-IKziO1s11(kd};;{ulZ6_Ord_8 zLRH&u9M*U37%Wrjwsb%9$(ytKWJs_$$ben(5|aWyzS=vBTxy)#!2OCFS2nyGjUAB6 zW1qLuUtt`ssIO~EG;3U8|MX=c=@<8KtKjakp5^sq2OfAv#PE?&Ysx0iEoD~6I;oka zbQ;vrnXpiA$3YtC;!uW6(3d_tb^>)Q2}jn9P>|8vmQ{{E_+B96wyfZi)Cm2yWH zt&~M4!;hYVSy`HBTnS~a(>jGzzcHSnorj-|VJx$bK-vG4TUUP|qGP^{MBMs{oEwaWJ^ z0w?0r@6#*i+vnR!$3k67M#VhUUm+IDG|3EpAl* zIT((GRZ^oLfCNH>RfiUf3?;P&C82bk>4u}yhwhQs&e~KEh(3Pg$2Vrp6e$F`#*=oi zRFD+Nn2VaI-zb0hKYR&1@BU6~?St_KFl!nDB@Q*VDyIXSjpt$&y1IUdQ%A2{qOg{= zaJv2mTZ)3E)mj1TC>~UWn}pp}`uaWr$0RE_7p;d;(7e_L@Hn>t`nYpf^;bLYEH zymbDTe!9eSz3+clA)E|j2AyWRi`;b6(3sqg9PL)OY-pf&Fkp!`yV_0Zm_Q->SYqBU zTJ9#kzSW4ZgJo#rzS9Pql%D3KbutaYwqQ!R-ySf_^Wa-vc?N_t9 z88}1^cd>4`e3NvlOPDr=8v5;3En_@x=IK$ie$hnxIIGlbL|t0`t+7R^x@o3V{dG|A zQ0EWMl5F0Y)@lE13;{~!z9i&3{%3LtEa8!W5a9{bu&`6Ug#V&yfl>`P~18Heki>F%xXyf4b@j0(*{?DVc;u zUM{D^Y+Vr=!uvH>CO&VRG7Wkio1S>9-S|DIRo?T*M@ ztjAPoS27`Y_Gi#lKRbNF?WJ2Pb}32J{j&-Czb5;=Gw-Qt_gFCveCmyR-DvCikN!To zu8XYA@s-!Hi~_RiZG67FukrBMI4=o+5Hy5Eaa@@l7nvEdrDoWn6 zwZUkvBINUd5c_gBk; zj)tk#@$Q43ZIhY*KXpu@ZANL z*XtGymdJVj%xFwAJk_ z7(C~$koZx91u;mTI(g4Ci@s)G)$Yr(k_p@|^A^4nj4p?JO*lR8i8Hr0U36bUT^6sJ zZ#n%Zd%$DHFDX#VIAb6yB8w;!!GTZ7Nl5AVbxL0i{TU{=bt*KcsYnm0BP693evyJ{ ziSfBt6J`~#@H4j_nLa$)PTWxyN`pbG^7GZw$J{^Kuw@QORn(i!NqE; zCGune$K5I(294X()$+-t8rh{$@sFGjhJMF^)d6SCT{m4~D`zW8*E{)-yVo$iMvNA) z1si7*L|`K*_Qz1*23**~mdDS;lGQPh!~qg`;*>$f1;prqILX>TXU!@%{HCB{ar}E* zLU~qtPL8Y*s-JMuD1?B(5B(1#kAQA9KI$N9HN{S9S*8K$IzoEx+_|P5n^-!{VS*5C zJf&LGEi%SnM?bfhLeD>3M@y)s-OvAgSL$&j z(1iTqFAm`S*#B+Ei}wO;y2mQiKbFE0X*IL)J6>(tH`75jeB0jtFV4_wY--AGY5DYCaN@dpegK)`&pX!J zi8D;V^?iD!_#mZ1wB}cvODD2;>DBj-SV)6{pm%9tKq3i0%8X@MQ+Zn5PJ+VQ*!A)S z1X0Uc64qwIkK~`iMPl0GiPT*q7iwMm__>73p9{N0o4^9L+nsicEriYoOu7-GE+-0D zC{WdTcHkn3kG}vr!DEzlC9vo;`*yq;(l`Ouksw;CVElnWCgz;Q;B6V$`P2cGCH&Ru z4#SN=PZ!gp=`0SvfB*c~A!F_G8o6fuCP||2wS4OJIKw~#DIC+=C;@;Dq6g@Tc@}HVt$WlY^W(Q{ zYhJT*3DPM$^_NIR)c&AMYh*=~f+!CT(=Ni$Tn<5ZlyM0u}4DJpI99 zq?O;r-tx);?Ju`oE<-9^Hr4en-5ndE4u^ZzHGWI(6yBI#7r&2dc{|#dwBi|gZMceg zi@8LzzCNB!Ap1bwmPtWs9H+}%d}K{sp8G9m2K!g}9_kRTF|N(_WV6Sm#(n#Dh&w!` z-c9=X^X9_?BeI_`8Rm>#<-Da!tG224nt8q2h!!Wu7f~Fq&LI?FPX%<+z0>!6X4wj& z)!{M~*O5M+4xdjO913EHoV{}}l0dT{(p__-V)VVNsRjJ&vQDr3gG*?e)#5#&Gz--$ z4Q7pYQi6}++LOD0EioHbHW|jJhY=8I%qyV*sk^ZY> zKsPodi@nwxFxi4k$w+PPgdYm5SIcq;l=}=qAi_4H{8$f9jE!SL$4eiog~Hhe43&me z6Gk0?u$OWG-DnX87npNrDeLY0l%WRsjS{zzS^ve(QUYXD5K=fGh_w7`^#bv0b(9`4 z#y)(9<$xo3dAav=WN*VKU5De}Z{wTN4bi!ZBKMQBfYpoNu$Wd-yFcfhwY7PILVN;F zYq78poV#{qZ*RAA9bGFAvhT7ij(NKu@1OccDkAG6A%~G8Zq`c{=8Kmi`fgi@PhLWxn6CR+g+fMngpq7a4%V%v zRT%ZVtnGY41f<(kTP+Ssh?p+uiuN3SX8GxvCy6iZMp-iE7kMTCcPpyPko z$1^E4p25;{Zx99wM1LyN#*N0q9}|d_T0gRCQ>zNNm>B9r-n_MMGj>xct@A0#VA0R0 zvRlbc31XbG?G5eG@!UC_XyoG4{rqfIw|*BrBe?N8fpa~;frqMjpySyQVpOgyFVTRoxdi|p54h!q^N$Fm#!A(#7&XS znehOB5!60kxpCg7cpBYOc~limR+<1^nL0uvkLtIV!+}JQagzqFMv*X$sm+%bSHIbsT6tykre%(mvQ<0N+VHM zeEF8gd5#&naM`pUU!dh;aImgv|NUTRjdARx+u7j?V)4t}kN(2MNyV0StilO(;4rZj ziMMfFaMVR`%z|(r?oX(vvjP_acE1N&NWQ?eULQWcfba7E~BZP!Fx+9FXtMddkh z#dz~_EONfr3*W{oj5^X2&o!7M>U+kw@k{{>wW`q5)zc@JnLap>&XPSfUvbY_(>z%c zgq)668fa!Qp66Y|5;W)}#{qfz%DmhtiWgeKh~W-_#|h>?2@}wZcPq*}&fgRm z%<0=XPAue*%63F8EZ@Jl?3btpbneLy2RKwl z0M_g%>?(IN!i#-ax9dw5 zXk=_+<0oae=zBQOJwvQ>K86m`rUE_Bn=><`j-*NHcR=5&<6~@_vgSeEf(~E}V52Tj zjl0v+#(c<6zv>4|sattlokQ!Qt&0wuvd#I8aky+){vT72q5RurHGNMe|tBVUGHQ8$9MKhbTt|!Vi62c4X&b$smJ>ZLviFw z9Iv8Mn!mH#Do$NmD^KZ$m^6UJg^Q%MENQGHv~A|iQAKxyS6=QJp6ebRf@+`Ze^ef` zBp2U;V<&U5gkiE~SI2E21?!sI?ZeA()pwX5f@PrvKbSKW1x+ zlK#FXjRmMkc6cS)^wMo0XQ(4q=BVzM!=q+yCGXlP+wY8;`cUb<6Gi*O{q>8f|Fv18 zyXKydOQQGNSt!uc@esx;FyV+%%6m3GcnBgdNF5J(C&vLH zUG(ZOOUrCB@}o~|b2b%qo7J2DJ=T_}oP8f`Igj(2UOh=0Js4hpP4}o-u^obTyXwzd zVJPS6{$21lrg_x3e*QeINks+}#4rDE zOuSuA+944Z{1J`9UWA4KyFv%%QM+ReJr$;jG9+52r)!dT9qYAF$t<8t`_OOppI4jx zr(%bJ9WokMQPq1v2$N|QmO};!Ukyg^e2-JT!xsEr`+KfaW%C4`1yil8Ib3Q#UJI9y zb`bfUNdRiw@90mxVe&v?5zCi5-*O#Nj8~x6M}~y@!H9E$U{>{9IgDgen+BiVT}hLU zmSruOB$u|zjx(7;t<_e~v55Y~HHTPEiI)6dBohm{3E{PVWBv%Ri6e#IIp06D$uEmQ z?ecBMkxTK``ost0j(J0GH)A#m^kedPMa9EQ`A>7(t1u;0At}RFqw4$SQz~d=*5|7K zW2uLoIlVm6I4fOv(^pt(^-}4;^2tG3rfR%EtWC>?v%ari z^<~FU9N?i3>-4R6QF-m|XyV!Jl=x$=V5h_IhxW`0kwCFj%q!X{Ai;$B=~6g(6KVCM z{F*-5S?1#G^)VQ5Eas{_c)Vo=Wsd>?8v!v z62G_^nGI1ZvTin;uVf)pk8%RpT%7tph#9)h(}i^Vt56x^1NJKGDZj&5!Mz``;VDB0 zndnN?47~3)IoYh4C1C@>s%blQNg-SR*}1a%!pqds)cR>-`C%vU!U<1c@UeEB#eSo} zpj?N3b&F5pu=V>8GQg;H=9Io7Qj+N-y*%;PfFP&ej&@dIrKDvw$5~~ZsK1Ema@0l* zq~QYE5dbKG&L}xjC;)01Ng$K(7<_<&y|=nrrK&zIZP>9kA;X8!6q!ne`RBcxJQ0Xl z`e|CPf%~t`am6W{)mDqX{28Hn*MXKa9 zTCl_2s|s9qZ2qI^7i*m~oUeu7dz%#_GsZBi%ojKm#gAxM+y^d~9Q?rl6%|TYZ|z1h z5@s3yjA2fcG1g)vKv4<19$4(yN@Z$XSXc;}JUXim_Wc4mAn->AAWS}9f+@Zt>ebv? zPUx7roBF*D#v;u#dQ>m%Fp~N~N~M8pE8|$T1MTQ}`KZ8>LiM4BVgt$$%3m59ni$IA z&&VZ`qmb|ae)5t|ELY#JqXB9=TrB8*LkQ6Yy+{TsW53Zn-MQ*&SUbk}QDt*#yOrvt z{#-CQV=11#aV1Py{j{PjO>G#I{fg|AI zU%9)A=4^faNYGv^1_oCi^)U!nw|`_mDh=xKFE)drTdgg`<-jaM$@nD8xOL<{hs@BnJSAWOXaov-<1Z-O50uRR2kO+^h{hLs+IJ} zlBpW2-`qO6i;Nc){f>Z|SnZGy}==;t+CC)%Xa`@kh-NK5i{~efp z<;QUMeC;DQfYM=7#PAj0O~*5*?3AE$s~srB%`ce6ZY~Jssag2t|0RkVUA6RpAX=$O zCDJCIIBf6(ioOYcAb=+etOL$+ZWLI-OrC3~Ho*Tu5^GoPdMqM_7y#P@*Tl+JOvJrM zdVcy{4cJMM4}&j_BO~ki^?zP~vNCQNp-&q#jk)~#zvw#tG3{}|s26-$rOfh7f;IcH zpJv@YFSRskR(#ixvicKg#V1&}VY_iz84LrJ_~)RYo|(S?T)*K7HG%lWl`Q@=`}m3Z zRwxtqVvgHIoS@Xy!0RjaoLq4YX3C?LpAX1loee*U z(dOREIMDaW7a0!b)#<{m(9-+8?)ZG#<$3VNL8wf>|D0=3yKja%-h7qWZl?u4;$rOi z_OI1=W!cd0ESktZs|ew(>;KiGno5SUSx%m?NIGqYMn`K?o!cQndd&~(_ovzxwj2TN z1JaD;PW=iV`5i5k-e2EnlS+tg`R9olQdE<5f2mQGq@Y9yGBjHvIHu5#Oua+10pIMA z!{c&9E{=!j2ay(!pp6RLRI2?Pkg`Icf^R7gjHhDslP9j&r-GdzNS%v|1AoA=EP)%H zCQfRQca0+;s3tRR$0Lr-?0dyLMz?$W-aL#bwYV`@%0U+EtevS3e91E=rA{iEA#m;~ z)t%~kY0v~3PEL)N$nywr{IzurU^(0Mu2(GCL^Un3%1w~_C&aI{y*9lSIN%B)g$B=V zk;p?&+F*rw-*uasV?sG7LHw=euyue~ZY01Om-Yz4J@!U}tSq?HBYWPlH22W|6l+|| z%?q)eZJwxYeQJnJ-U&$T7q{{1+1 zUqS6WZf^@c5wcDPhAE{&apu=ir>Xj|ra?}zs9?&i4=sA^F3;wt^&Ht2T|WUC)wsCDbnSUoP6tJ>L;wIyeJ?+&{vy z49Iki@mHUspJ9YUEa;8W)84AlAT#4G&C*MNW(m^-ci)0|<1<76n?a(O*EYMVwV}b~ z#Zm}8%W)=dM~2VQ`x3fS=a=&B!!?UdoZZagepKIUZr*vgL=ppq@0cGzTv{9e{~pgH zZvk_Lu6Df#B2#!gElH(VW!Z6zGA^RN(un^oU%4R5hK~*YTqyaUiHpy}a_uTlPRr(d z*adEoI3rwu7lRpzw!Yno|H%lqh4K;!*7N}>2?_c55k2I#d1X@AJp?<837TQcEFVxH zr^fVy8CRScftm|macAc0M)C8xPTV9f(1Q$(ZntaAkp(bb#nWeSHMfI&gYU|me#~oW zZKPG97);sZqEwcsSY*JCFX;ubX;~{n;4)}dt2RSJ{P6d-t=ZC1Nnr+`xKQuf>8$m# z$Gj~OI+)-7nhU5qO(r6V%bdF9B_z^OAP^b*wVzpb*6jWC`(IAJ7GG&bV zV3_>R>@xG^9A~LTltMlCthLrPKJdb{H&YxmSGV(Sp694KKSM65-&G&gbU*G>o0~lr zF60k>({jIxd?S&_i zlj3F#rsu+QN!glWh^o<8ZcZ*NbN8vKkNVihq80=kOyL}A7BGX(N1Y?bLA`2N2)^sg zn+zEi#1QAaU!=qAg=2Sl7UtlJz#fJ?kI7r7l@{AAo*!H&E28z->NDxby4w0Qf)?Z7 z3#~#V`C={$LCiapS1Xotm-Y5L2Xb9))m9U_^Lm+99jF*oy3Tmh*kNu`^< zNv2`lev(ZEWN6{uaL>0EV3w^l)36}S5WZOczq3+|5&(yQBKX!QJ1Oe?P*I-m`L?+= zgz|0D6Q4nk&uKscfmzj|$Ym`eIIoO5nen ze9ht)DDHpI8s=s*mucp3%ABEWiOD|A4$`vEhgAm-PP3U zRj~fSX~|F5Gx>P%cx`EF9Tq|V=Vfj3f<*!EzsJW$3<8c>w*{4(+H(quJ0xeGwm&{k z59;wTv6vVtw)%DpG~02l54&Bjt-cMf;r5$T6!K&tXzT@##(Oo+7XIt6Pxg;7JC^^+ z3~p)k{7l}R?8-+1z@9KLdnLYPxoX=xQ9Om%W$(>u@6sXBi@}g9cL$6rIA5Ui;71jHN`$M-3oyq`1gCLDa z3dqn5B`wn3-AGHPbhm(XmnbPM-8nQ!H+R0jyY4-U#ajHsf_3)n_ubFl`}0Vl%CW#H zXL(2Y`j|nVEz-;0yx)w9my)H0uu@1HRcPAt??;!f+BKo~f4Djpey$bCK>D;xq{i&s z`e~e2TnIIStQS3*Y3 zL}HK01Qg;0l=ZFc3`KPE(+wpElNtI=0Xp@%xjGu_GQxBXl8rKTr@GTvxXr#NpD9re}>XM>+e% z!87cfCi+E=R!}nV;k1AW2{1$zUlW6doKKbbdn_&^eiwUH<@ElxTU)2;6#vLt$=E2D znT1jcVI=ne3A$9Klo29g%Tyc@RvEVN{?1eP#eR8APTEsKNlZZRe6ZRo(j_P^2vUpk z4I7n3BU4J=+B$hbr$O`N(kHo(S*@&)5F4N{6=w#IkbVs{)yX~(nh`gwsU9!cYqhjU z0+0x$JF0!Rsesc0Z-0&uVmTH-36sIZ&W|?yp%YdXH>N750u@&QV1ZJo;;kOU%d-_)bws-S&pjC{x19pW;(IiG;f(rL#zLBxf4@Rs?|7<` zv*BsYu!auahoJ}VCngGe(>9VSZ?iWzHxEQ*E_YQ$jkZr32zsvFV$g7WEAqAXzNBAQ zhDS^DXTQOwyo^AaFR=H;1pMwE?YZ({P^#!w#5F{h~UOLOygASK1RRCZ?6{jr%!uJ@C5vL`KV@*5zFa-O%wem}Z;PVdLOsTKn3j+yDa zSt5E|BWs=W=fK9os^4KX&NMAp7>m`}fn86CVtRPm30~GaCgSZiGjfr*@#E9X-K8$> zqIf{*>>wGoM0FF)PNjUiJs%cCFm1(eeUz)Md%Nz7IK)%+9WDEc84;p=71^Cp`OlA} zXODW}-_6rnKT+1%zBfmxFfWTL8;SJGI`f$(P2B86<4ksJ8Ula*F>6U-@m&7gcZw_e zhMV-G4kcfw+v75D+D4N>-nH)(c26;@8GzOnH+A-!qglsFE`AEVH}{S6omS(FmK}dA z=ik*cVa_QEb`fXuS8rp-B6%umP2#>b5}IqM$B)6?{w5!^(BW-P%TS$fRaq+`0iBou z-TrcPj$3K|g5XZ6gK`^<5ry*s>1M03ZgCY=N}uZpcU_gWvrKq`ET*w~;r@ZZGA_{cbW;+= zEmMk^lpip)VKJ$kmrzg1ODm4RCZPj^z)2wF(j>VMRMh3Q^RGB#V%K^l({h_0@M9c) z`?;FajK{IZyALsO2RNGz5$h-Kp5%c6gDGXj4Ow}^5Sy@6R4qIW&ajlece9kNB#Otu zWs$+`S0oBqTb_3BpVvb3;s*vtoqcx8m`Du;8*2nRPZaI_9F77%6rZTQ502ey{ScLu zI4*d(Vh*g$*!xW6Vv0k+<-d4wQ4{)DVfj=4#xlRXz9Mn<^;I1^ah2@lrNrx+dhxl8 zy|+|rpQ+e9P9>>!+E#4{YwBJuvA9h16ybH(DP(?j6Ey5k^Iu?2F;)Gufeqa(?-8Xb zIBsUM=~J`_eO-t_t>|c6_?S_xsq+D`2B2*~7@nT?Pc4gb^mM#+eb6Y2OCT{XiL%%{ z{diBUF8PiXh44nTrIRq^Ngsa+A85Nid^Kd+f0q@d6qGv)B}8*+=BjC}-=MJhVS&)n zzi5_yoOD5fEo!M3(=vC#OA<=$w#KrK)#=t#?taQ3z^+HSGL)CK!d%^<=MbwEuPJ3< zZQ6gj>)y62V|r7^o)3*Xo#SGnXJW~q3Yt8_qyEum(q4&^Gv{9c_?Lr;g$lxu_K=3t zP1JF7f5OwXhuC{sskTj)^_LDyN9$yRRo1*$i~i9?E2QNarw$0)>Z`?log9DvhY&1I9O0msqe17-`XPHneStIvqTC{K-h7IKPsIsU6jZ_Ws zlWX3h7kT&(|1y}199|$IwDGXT!Ng&*FxV)ZoI9GXgi@dgW(2~FFSLwgp5lG+6P!FC z+D%eXEZ{`0nlJ8WNu<++1<@tYX>5HEyJGSf0ldTB@l3TK9ZO&cgmq*!h=| z#GMWd1k?N~3_BO9CX_}ZqV>guW0#j+&ceTk>W?|^mSs7FzSx#)afHmN+P{C*X0?Nd z;NofXlIHPj{rFE{>QS0U?}^^Ox&6AP$K5*z-*P$i6BN3TK9?`1=FbnzfX{jHfsGPJ z^Z`0fP)Zy#2(LHlvjocvylD5;(~IY;KEzeO)>0fMu%y1A z9B$xNaf16Ij`KcSU26T|GbK0YdT9d3~QHlpQT77t=CNSU5`pAw;u%)4X^itQlY4yw(Xx`5{?z}cI+1KRT zGvQ`n+G|j*?AUmys70aD@-jyd-rLs%Hq2~Y8#)iSYy>dOM>AdER{l@xsn2^f%g)7B zRdbSl&sM&RE1jE6y$mNB=lb5A@0LsX7P6X4e!Vvo#Q|;z!HAa*6M0-ql#ec=vVT~3 zINNWuCCd`@EQAXPDBI~YIjV_f_-~}qM?0B%n9$RzRI|$yN4w28E(La~>Nf?bDi~dR z!hLrZVz=((Pz@w8kpf%~W-Ze#GeYy6qvdMNCP_{&lgV z%shw!jh1HrdpE^j0Qt+?`$Hsa5lSed-!*ky4BMl8zN|)28oiGnTTrDz#9$TlU>I_` zom-pY)DI9+039+N2pLvb3JU^Hs^`(6CMU^%<*}qACDm%n8m&6N7WLmysL)@nx1)m= z%IwW{{nG%(pP7|8jAYpi`DVflny6KN)pTymbq7RAQ+@@*E(Jg9$(Ql{Fg?*Yxo0D1 zU2@XQ^!%P4LF)fZc#=RT>arV{^L!R2Unwnw=2Kv2Mq#wXEy7rzhW`q|we7k|`#jTt zt6yl;G!fE$o^`feuYd$e{S!hy?2nS?U8=VG`(YIF+EEhgKDzN)5B3 z6k!_Wv~81Kl@ENB2I$^7TEs0j)gLjkaD-9UH=6|w;CZ}Pg=qkb0w?7kCY5hO6{+y7 z*g5#ktjwkDSt7jrKCoA_ql@gA@V;Eg|L`HKUrTp{mk&KmKq1P~&-Zdi*bf=-uC8_O z^3`&53D+$aXCE6`tZO*-xg2uWGm2GamMg{Opf>k&wcC@DCEnS9XTOaXrCPpAPJKM= z$A5IwZoh8mYb;aMR-}2$mQ6xM#l4EB+o8#45y7SOs_T{=Sd$L`iK7yr<~)}u3kX;G zuRd>CReG(XX^C7f5xwkk?78Y_b30l7VY#s$mh;x>;?vB*FdV(uJcdAp{(!N`M%cbT z#r8w&D-ov~OU0&0gMYoVK5LiNo@j`+3sS%nEE}dTfY(1s@qTzioNqPuE|OuEkGYf? zS}?AhC>U?a+QaQSJkvZ#G5d0gjg08 z0%2!z%Q359i>s79J@q7SPe;M!SOQgiupIh;?vq=&K{6~B!J;zRY%wo&RSW*~N;hu@ zi_&?kmb9S65r-kgEdN_)LsZ}~_B=-zVCaDqPWrrZ8w8o)A6{*2``sQvsDrOGoM2!>@PnHn^cN&po}s zY0H?;&q)?oVR$KONVv+3-QBXCmivVd5&ef9O@VC>fc`Kb)J4W@xP`r@GP=EEhQU*v^+GNAZktc($iSZf%CGT0Qt)yBE<~P z^*>V|%)L5Xc&*L3NnPvCork&c82e;Cpnr|5nLDZc!;!;!J<1+3Rbfq!5|Tw`Xh6wc z+z|k()U0+bP)ZMnXjWNcERPEKpNas2_Oq{)(vI@bzZuz>jPu**4&oN`DtcpeOAEjL z^FooZ%+0VKO4#}w4}bB_-@(lJps-t?&v_Wa$7a3HhMJu&izv_0O$_rJqCkVv3$ zpQ`sd3{18*5UZyg#bEcn%_hM6K$@-pU@F{XW>?;Xtf)*m-7usOwtIRrYYzNZC;>@H z+qXj)E6-umW)%}QO>d9Baz13CV9=0SIt+Hozg}=3+)g#{+SuJYS@mvOwbV^%YUNLL zTuSYFFlC$GnR)3Qihp_zW?U9$@7Jr5^Yl{;DUggn68Pj`0ai@zOS9a2i;<;P zW!Yh{vB19>&WuGX7qKNRNk_QI%buZ@g=`vXL>Ry&w*i^KwbGrJ;Zai47%&$ZmP;fj z5xp2kXp2ALPl5aD+-pggMWAo88&UB3w$46R@xov*9asfGoyG}Lm&+>zA!8avu~(;H z(L&i(X-zYU(jDs>_h|E?;yC^H(1CF=^+lv7{ z+rv4t5p>kNRl1>!8lM*o5+prs@ia{KT6-$h$Laf(mz_`SgH4J$2lLh^%v_CZZ9_jL z6cd=5-0vJ+mcl*nZhm!-jx1i;WpMLGZN#Y^O`kl_{HHR}_*@RhY^Ugv*OQb;w55tV zTUC-`en8V>z^!*`nNM}^>kGpv=B~3NESa7Ux!sAJ=m%uC$xm7po9EE|l{1 zy16&#*){##P#JW)6c0;fM%}q*&ZoW*m~GEm-EI&iS_RB<2(L|978SUVqTJvJ*Ucr1DAp`i!Pq6w}!XJ z%fOG5F9m&D;{&0vwziogbTZqt^xN&kr}fe7icoo7H{I-=#H*X`J7Z;WPCe!f*&j7+3cD19b4O_8PSCe*_#3-g*$XHV8+2nPL6A?}Fd zyPoA)gX<9WA-g8y-JRnst%tijVUG9SvxtKDs;R^DalpjW^yulH=6S_;6<(^H-soxd zp{~v0@Xtl(d9`0Jm7()g^sM8)+s`Wmyl2mK)|UjcJsgA#prH8b)q#v9#Y=*nWtg$N zYKkqB2+AO|-^PTJ0@P(O(NIUziYLG%p+=q*D8$79p@Sle$q_4f#@M9gM$*Kg%f=HO9 z8H473sqqTfO^~R1><;h7BCk6#vXfZZ^e=+%|Z3RrA=k+w-P!%G7ph9(7)Fbh)p-@6ybZ{VdqwaY1(N zbJC9UI=~r|7U1Haw5RYAh-AqtlXaetGS$BHS{`@pza%7pvhbw55Em>#WuBE~O}5q* z53i#7qcWliD6kQCe9y~}!cSz5(xAg)o&1{r;gugXg22AS6iC;OibX5V%rF38s-U7u zsf1%9>Lv*SaVZr-RXPcT0&7?(&Vp46Ql$e3u}X;p5)509(E+X%fpEt#LgFyONi>-V zk?+Rg3gb~niZ7y+w?4qq8iR86?9|yRxs-ajR7$^RW>JmeN0}j0+`X$*ub5PjjUj(T zpXKAQmM9g3Pd&R}1AblyT6{`q;&h)E9rYL0=v-{o;7ZI1EiEk&rRJUeKc08v|A^}3 zwYv5vMZB9tZYu6YhP@Bk4DH)O)XbH9t{1!PzAK(Az)6-S9V}g(e4E6v>sM+ z0K^56Drig9EWX@ z8M%k(?OOZU9fUUK%2!xZk5>y=M{9O+3eSt3Cey570Vlq_8Z6yT4du>?Q_O^|j=c_< z5V~nnHjVzF3zD!yy-_ye#bF$f3vU@AE*8VK*%%q!$u@V&?AOK6FA|IR3*}1(GTn;$ zruw#4E*E{e$H)OVudmXh3v1I!<;@_4+8g$Fk9oUuk?K2r%Nd70d8|P;J$ufB#<4Wq z?2YiB90xP_{_Dlw!uOl@K7KY1?8XQ0r|aa!#OhCFNxo0`4R&)M!JcywgJOo?9)GXW zASmAmDSqzYCRX(Bm*Q%><=xiB%6TDFN`_WSCx&QMK(xU`8S-PXkz$v*F%~br_Hk9<#L;!&q}DT-aCQDs~)QJzF? zO_kiTReyj4VM~iW7~A#i4BUh%>%GSWBLNWcKoz-om}DFnF+E4yj*1iqMnla-OfFC$ zqp6pQvj8CR-~{Y=X6blPQ5=$i`!XKzsogxFG>$o5l;}MX1>w|YaRtICJn8TWsmb2Q z;SY<+LPzerod9Ll9+COmk;YA-gJS-e7uO}0Qt#X>w#UzgA_J0y{-(PiG%z@Ic=5zk zP7ZoNn_X8LY>RrK(rh-CU3f}sh=8P2>RZgLK)Wif`s|Em>P+*xS9XWdaN!>L^|JHq zl=92P<}w-~G0*aZZ6ZHG-=5AV#B_Sp;z_g6sK@vbHiO~6#%rYi0`zI4L?|z6a(lYL zj{G87t}*X}L@aD5ExYjNS}0sc)w;4)Fb9d6?W53F^E-@0S%|7Y%%*0Aw^vn5`cEtH z=d~2eke6Lk=ZasI(N@$&1eR}$YQQf>tl5@Ak=h+{Tx~R&u5BX<{ztYsomBn|CaF?Q5J?&GP z;Gk_G$BNE-?tJ0N{Efn(JJZR7h#f-zJx5x7%plxF{kH zV8P8Plw53B*_8|XH=67 z0S7zub-Mf)n2%4n<*rb66YX*Gr3A~2V)(X`j?xF=gB`{G8XKm=keY->)3lTlYcp0= zX!!pvKtit~h#JC>Shia-)0$9A^>G|20h~rMfw%>UzjCl z8_!~l+N$COwDUWkhYgXtP02`wkyA$-agmim?cdse@`<&)efeb}W)vr-hD&!3)c9}m zX!lOu`8+MZ#5k>g8^ZrHk3forY#!LmwxIb|EG4}E5hhc>GrTT@sWyg7Xlfmu7 z*`)9D0oVVA;N;4yS01tqm(S}W0&AEMWV%~ZE&m65F&oCR(qE%0RaAYe_wyYX_@Xs_ z9#cz)8@el3t-o&ziUI@MXcls|LyDYyUAO=Cx<4LRSVpBfeF9l!09-WHwyB3yowlDTFih&HCbBm zpzG5XJ%S(J|8z_r#Vva;#EGas>I! zFJP?Vq|%ad(wOU+ez$j2NZqZi;9;b^fpcDcS>7xip|7`;iLI9NF>p{ZRp@)zPoT69 zBdkx46V6nlUZ#+*9*XD?&BVB)&@fN4e=!Z0ErjL5q~o+`N`Y?9<5tC4ysCdiMJ*w> zMUwn*Q=Sk#gP}qM(&Xp(Uj>Fd6?O_rCUw~~!k-uz{i&q0E{V=~RF}oFNpSKixRV)o zxS81~%<5ZAMaC7?^BT&3RgG@dqdF)QyEK8tn3Ah_Pls5_wyTILv@E05E)TU2%5qT7 zY{NfvDf2Ra<=9pY5_0xaZv49xhkwJ4RuFF?E^{z)izO45HGc-ia0$}d7yA}TT8Bj? z`W^}%>XMh}X#FFtRY&`kc4Q=>%w7^cPbnS0qh>8&p$s?hJ&x|D%492w#{wz z9yf5#OiM;0GW-kMece)Zw-XA529vSBrc?dd!rl&|Vk+a<8GrPiT7F#qy&&+fTo#(Q z&Wu6YleV9qYqGyEd|pFB+#PSkP&-b|@gEj1@)AENXWgtBKKz#Ub|13(B5OswWY%!m z(RSi>_>ak^WA{42|Dhirk2t_#)6nmtZgcE)<~PyAnh_fR`#TtZw7M}7TBjv0;FLW` z&Jbl8B@Hd#OZOt6llc5GMOLkMT$SZLJT#A~OdZye0u?%tCP2&I(}OQL+Vs~1SR7TY z9)&n&TBK)>!EJl*M!w)9cYk=g`vM@QHQ&gwphyA%Z)AW0d1jzA6-+BmQ4mbWS_A?R zWK8KI5D79NHZ+}%o0fz&7^xu2=O4~2N`SAWW}`7K!*sy4v$lNASJ0bLK0BwiZ|E$v zf1PZJlbk!M{R`u0PN|!yTwJwq5@ZVM<_`Y`Cx|oWWtvj3s@KrHEe-$cr;8Hc?RUzN zyVhct6q&|jj@3Q4`_0T^*)5Txwa+z;HR-1#I|BnaR8ltROv z>ETF=aFk-xPZ^|icaQPFD-q(UJEb_vR{*~bu(Xu2kmUJGbwu@1NAOKs<6n!u;f5dE zAFz$s4L_sTZ+8E6?cXZyjePH`JzVg{Z!hKqse1;?x`y;<(YE6WKU(>vEdpK>xTSR~ue$HLu?xlreLA&EG+}>VYP&dE^xSPMT;~HA)B(z;->e z>x)iFB-T>Q7R>l{57{o8T3@!U0z9wBUiU-uTiETO#NA&hn>-g3HG+d{?|R+>OOA_T zI8sVIjXcXSIgof$?G7a^o=d$Ni{0V`NVLyr#AvhuC|AyPIkMo@?6=|?x4H`Ah2lOT z`-V102qS-#Mg^NVt~`%RdDoqY{|m&JdCT4ujTVbV`h*p~{_OD1GDIm~w$IYy8za3s zOKucps0^lyFo8gQPp1v>UE_K#kfrO`)Cx;8eBH#F90DPziQ}(HOHBQApS-iI{EP^> zJ7xd=HKisJ8A-}kgCWwk1ajWXT3H1(RUQ?~fCNUgiC;UPO$Du`j0&dr*tIf_s-lRp z2)K2I3q zVEWB#K0ewXyf1zwHV)OdyV!eFdwevePuKh=DvHprojX3tw37XbTNK_nq)gL(rwf5@ zo@RCd(VFRt37-2Eh)!bo>2txvWilb!Y(Ww-LN&-u*gx$t7rmY5ygwSXc%Fk--OmlQ zt}|bCU2a!D-m94_=ZM@LH+9~UEV{*dWLLlv!)^t-UIL+Ea;%DwfF$r_688{A0g#f1 z|ANF+?$ID=EG?!44f>;&nz*hih*wEE0W+R;*ugnljd|BrOeuap^f$SYluj!_X!*=8 zVo)B@oV)Sw$N`%sIgWw?c|uWDSY@z^aIrvOPNaMwSS28V2N@HF1d|>O@;=`F-fHt$G~Kv`K55LY*-{nAyFK2u+);$t3rEQ3PU}`+VXAD-_q|h&3%T? zEBGjgdz{}{2xk?#;;>_@zTqY#ETOP0kID#yl?)E(!;|Z7bddod&LZ;S?8#0p9C(SD z$u;bCM4?%YtKWIK!o8LoC10fAFlHI*+#Yv5BBZN_@tlzl)9MU#Fmczg^S?i4JiU-b z9nVz7=~=uoLF+$W=Ut{0dqF81(?z28U`1%zFr1>^)34u#yg?2w${m1y=cM)rBaHNB}b5m7b~^6Kq(0G;Tl8$ z3IG9MR1ngsw0X=+|JnPe;%{TWRPXC@ZgxH4Cx`WmhAp>O^L5j*ZenMRvz2T+&9bzz z>O)Nr6K%k!)s*PrN+)t0Ng<^0O;jY2{&BvM!H+dS7P;r_z)qB0#moBSJc7?df*N$1 zegZBoy0f=`|Eh3mUKXF2iOfzs+ToL+cht{g!Oe=1*8HEg?;#9HuroS25@b+00XpJN z22LUzVosK0MGYcWB}@{>MAit$WnqZ4Q~(3{N#WuN5L$79WYjx^r5zFQv~xh?y|Xp- zwI5yp!S)f0{*0=yNC;q|k)Y%ySrU`5MkEm=eKH{zsON>zDX=g-lX;)Mw6N5E75U`3 zQr$E?yWo9fP&~%P)7>4w3}(&^qMZ1+!PiI0XZ81EegunGEK_!3rWBE~68krcBq^v( zIQnl%n!Afe^Xpo#TQAR5_zMrGlgI0alM@VFBq%?_D=hkIGgN3?xXM5zMDRtb58!cu z;g0w48a$9d2jw1+LoZ~J19>4cth%%P=0#-T3brhDACbvTkH2JA@c*qV3ILd0SJquu{+R2ten+!U__Xifn>kPPis(oXD5N<& z5fw1$NnjrpoR#1)DS*F7ZPK)rFM%X#xIHS4t-24oDNNOsjEY|Ds-_B9`HalP6MDS#V`PX*(x?ME+!v)6lElf~o%dU>ua&jCk<3a{xQ~s_0_U0F6 z%U~MK5#D?&3?ExTxbt>iFT4ySD=d^tSfW6$(l@DV;kv9@jGaMuk`ocAVMsF3uEXsi zhg;f{w`CTW$lq6Yz^JIs8TRqtvhOrx?q&IKiLO#|!k7K-RD>&DX5QV9#Q@zp*57`= zc`wC?MEBk#9G3!-Kamh4lAmyUxsEUO7P)y9z-XxXnXN4x!!sK_ll}z~A@%mA$Gs~v#J2|$`iJ)=lU7>V zT$lL?$?VH)e%G1HsZsD>ihXR0a)>-3N?DGHGanAZcrq(24LkA|B$Nz^PA^zI01y{H zeSVKPp~)ts9S8`J07AXs08{`~N)qIVoc62Dt^*yhTOFCfXjW0FsXIlSQN|J;NM-TH zDGhDQaa{fNw#|a)V0+|bp^-Kvj)n&3eoFaACDU=s5>=41$Hu>c_thN-Ys9Jbm)mZ{ z(zP};bKZlxe)ow0n5EoKOYIfm7iZKef-8 zV2?giX&&4uoFMk~cY_HEm^~yKBO6KPk`sIbQoDEv(?_9``(I&Bt#rY0CP@%dCIVzD z=8$)E$U@S14Q^|dwv}+_#)2WGiFYnX=P!;|l5dWFwuyQEO@(B4fB5}}4Bg{Dqj&3%VL2_&xN<7g6^usB=V!w%LS&asF*Xkv&V(di zKEzz&BzpT_M9`pgUmNJV`>ubn+m|ay{JEtdSeBP5-7C0gwF_N3`T+gKvR=#1Qu{|; zx}kF6Ldvl)QZlmnG_iF0SlwAEuQcLZdH3j()Au=x;%hgP^o6G)`xO-6KvCCK7H(P(Mf zkM~=NM}nG{TEi$!aSoQ9<`?+lokb>GdvPkO5Uh{c)SP!`G=v`IbOGj(#o8%6qHnv* zXY*bPyN^Yap!-d}^cr!i+no!^91IA}q78E0twn6U<%s!&4<OG3}SV#vCnNY-{tt9P3 z)^)mUQ(1NB$j{ARuWAmE;YX$9`&HoACh$maY0C7qq_$+?4OV5pbKQB^Gj}r-HLN`O zDC(UO|54k%<7#GB|9(RN1>hbm9sT!<#1Rt$NqeLC8QDa-R<7|^;gcqjghg?m3v_BE z7mkz0V1$aQFlnOBoec^E%l!-vfDaczWG5joR}|xwlP2JQaW{vdpom0Vo-cT=ySj-?52|bUp$)!5gJCU9# z#X%nEe@{Xrn7vS%N?DT=j8GL#o!`?ps5^~Tp~rezeWZHs)9*qJ7-?klV1xz?tZ9b_ zgyUkyhtuMVRIeVfV7GRR-VI?7wq{jFjVbJfMkGif>_7hZ#)D9?S~24Q0yta-X>IgA z#qE!VNeBzB?fbJYhZ6-~6L3>cGKRx{tIk~eN(b6;3ZBgoQ5}@z8+}QbYgCY)u%_Rf zj(2QR&&bT=RiB}`h{+Z-n{V8rT(1AH#OxgR<01uC`uM!wSXpxcW{`&%G{}YP3 zFcEJKm5kus*YJSX$P#0Oh`)qx16dF$;pS1d8Yw{FdDMQ|ob@p}TCeD>_NxQWU{p*BF!8Hi`tsaugg^5(E&XFdq z|LYqnbYO}Z6R%hH0Wyrrh5VUW9tvW?u9(K|&=ect7T;nC3B*9Sg5P%ci&>On=c<@6 zKtBU82=p_d96J#;Q9{}zMd(Oh`gdd6YGp;1`)YREoUZ(q#)-Fyx9_R@92LfQUz|Md zp+s?mlDHRYY}?<$CKS#&pDwDA);z1*+;nZL-wYY}9ohCic~y7KU{S5P;^ASZ7>ri* zYmL5Uw{KYNK$(r%ik{qmv!a8=_`;5-ZXZR=e?P4xCZqK(Sy)snXYumrnk%bTKUqTR{jL_9uky7R*H`34O(V&tOlMmAMy93 zO0Fp3tT-rkQT`(CByrgCC#UvwUNZTP9n5@T6`Ke`xxR{Gx;L}< zW7UK)UHynL-yYjs3f&>T4SMC=RZ49*@g&uGB~T)~v-a0yv*; zXc-4_|C3n)F#XVwke-Piu#u4wKkG^T?}GqA)rW)!=YDMlBXkYF=NULkw|&PppY!5} z-Vrm_ziES(u}t6k+)^^<4|DXE*h$YLS)F3^sLTG0DMyjO(C4sWlgSV~@QbUc;{?9# z!#k^lU@}c5?q0v1X-==ZvM^lf!Nnp;(+3R!e-Pl(J4C{YOGpPz*&e^<)&JZL+h7j- zJ`^-g)0)6OYi=EC<>A{keH4h=Xo)70qk;w*6$%)bRG6KPpC}h5zj>b%j`bSBL`FC_ z!h3TbepE42bEASXaYm7XS=kc4JB-rOqdToC-z~qkgw}w~81o;s1{Te(DzoKVaW*X_ zvpfbE4tjFL8m8?%zDV@b94`upiSbwp7H`hcc%CpJt?3&+ovX&Q#+YY|PA};AuJdXA zZinM#)Us+|Yu44Fqxn|W7!B!|LUOo!gq7@h>Zz@bY7mZ6&{K&v$L-|%>z9JW(9v57 z_|_f~y6(z_vJ);z$IS_iCpvK4)X5;4j#LPN%00rrr7&W!^HqDtEo}RAWnyq%aW^&Ur;pfU^8B-+{<^GX3&u zqY&C*Tux+hadGs3D5>3+oTmcihb<;CtbD;8?abn+!SIBU(^6OpviB4SwP>5hzw>tCE~KZ9-Z8$_94b{J5r+x!_G=JP3N;Q zUj|`bnT0MuOjh|Wv7T2k?27>izn%ACp8h)P?Uigp3O-R;%W>!zKfzORxDtc=!-_L- zy8Z0&F#&=&TYX5u??VxxE)KX5fc1hA#XK$GMLJ$54#J|N-KCR&pkg5d|5j2BPyl<6 z{-ebvEvb@KJJicP3T}=MBz%l9*(cP7db2JmYk&NGu##Z$I z^4?9(V;(z?1O;XfsFxWO#H*pEk&GHfj9Ju4g}>`{*MDcj(jqwi0)DMncFZfA2KCp5M_XjhHEMm?masD|WU+y~U zP75(^O&W;8iTO%ptvE&#?s31XF!%}!8Tm!s`t|aPahk7aaJI4L^WG7nm!fQ6RnqZ`te%fQ;ZJl?WWR}%oMzMVcG5yCKcA3Y zdSmXuB?fNK_>td~x!zYIi>8N}lqPFG?w4+d$DaK-8rp2BIDRR<_s#c^j4RXaw6Is` z0W0131*id#w81DO1JnRqBvnlR@#o1^zo%@PrRCjgx1)u?ggvjkUyweoi_mujG>=<3 zdak|GxJUtObP`hH0ph^{xbwRf1c-%$c&ck9h2pf2?`k}6T4!A}Yb#~MTzSW$mz6Sr z9@x^c*|=p!%1>VCX)qS;WD*n%Hj*>TaO1SCw5?Jja*v3g1zK`6HKi$VXD%?)A; zO8Hg%w_rg07x_D!KuiS5o8rAxC?+6_P>|SA#Sy1&V6HhL=ww0T&Vu@=;9qTqPJBT7 z{z%iI7*{q<*`o8&S+joi^>Xwcu6N$UuXzMKePoQp@apHmW>V;cSB(YZue+@50yc^p z%ulRE`8;;Sk-Aw#@);$-%i^6-JI)P)*iU{B35$kzNWi!3Y<}mrr@Y}rCcEV5WE2Dg z2{ziSED0bOad0gi;^WSU0c8YOqRHvrlRCb@4E@ey5?Oeu!!dSls*y}Sj2aOiaJyUov7C7Hu`+znQIvn!Jw~BAtN!Lw z`roy!w4%#P2S2N;9v=LUt?l0T?N(GNiFG5H<;?b8kBbO{+OB^?a;l4s{YrP0OQou+ zANb!=6(JKr$xXF6U4@L{qc;0aJ*1m zR;GSFCxewEW4@2Q$)yV~RsSCsV9;pT_Fv#xhnUCeuFuWYpP-71{hZwo;0|wVC%VC_mMHYYxd|{gUmR z=?8iGXK_CWK0&f$&%_A${ky@S%bHTDNK?uL2tedONiYH4vc3QLXwK6OMx|YasK`sF z%H_QXuI97u?MMgC@B9Xr<+Dp3_mz>wbRZxQpaYB3eGb6Ywk^a2s(pUPKzQ?K!6mYj78i>0|r3rS|FsjHM(F!2?X3845kA~1<7L4 z0d$0jA{Qwj*L~TE1mQ2VK=!iJ%&m{BR@UQtZj2_?5rb)&h)~;k*;N0#9Q#FW-$&af zSu3Ls*T4MG&vMQcBp8+}FcuusB9pEC!f>M>lT`f6;$<_%IYPckdvcVKwwvDe-v7Lj zKJ_=ww;Mw&$)zmG8Hft#BZDP~q2q2p!czdc0o_5+vIgF& zsc_Nl;{6N@aRV={Vpk6$oEjv-gfcgW_PaHjw0kU z3!Khheu%1S+4dA+{2XDn7Vd*p0gLZpHn^X|^7Ol;^}}64D&uk=I`HnMyWo1|ymuJ?W#`lH zS#0D{G;VQmsXm|%^3c=edti9{U{X03ci0S zKi{BdD8j!0!KXtkph#J|6ruvvUOnos&Ybw%_kW_Nx{j2|&B*%Muwr?pm_{lHy?3To zBUMcasi4X*rY|TiyFkx!D_HkLB6B476m7m-)=1HbecUbm&Ws67F|iy%lQ=8}PL;(( z=;U#c2oN5om;oRpfRm2Shm>9l2!8&r3bMG`I8{Q`z@K(0gUj?XOBxj0H-yF%$*4U;D^V$piP}2M{Jz`Neo6`m`kcFZ zo^cd`2?X<<%ZwN&R-nf>tXsm`BWaI(OAj^oi&PB}J`m z1KHt1hrd^+CP(zwC{DY~Jtm0B^7(k+r})K)z&gH z(v922Rhzu8(MGXoM27pkf4g;is zRm4q5?JQ)ztbpmYY`O7P|HaQzt5m|*lk5$|P$W?izB0Bln!6E>YQ5JM+;3zBD0M5S zsFvNB6zgbDMvimNyawm{N3jVBi&Zm&jaUU4?63jDwv+H*AWRTuAYu~%lpQhtk(EcgBG~`SBgcrm3k`VXyU?4F^rp!s5Pm8+U%~q#@TFWz{0e&vvIBtQ8y?OeguR zO?@xamRq)fQ;mQbwXa7vQPz%fDJIshUIUQVu!zAH$ydCAlKGGt*P>Z0evC)2hD@|x zoajneLq2&-&4Z_}vCSNV`lGG$kp~d>7==HrSAQ)&M(>1& zsggsHcVxE1{5&sqfBrPpdQ~5d49Q;|d+}0`i=RicQo~06a&!IX(B03p?deLpvbA@Q zbHxf{QLg8T`pNS@BH{BZ2U zF4?jh{i)=rkj~^#3jlOJo=xx>WeE4Rq&o#Ge8zl-`9-qF;f&O=szSzptpVN7^4A8m zwSL^RP;*%0{TsI1^7Puvy5YuM?c-2cLhra1GPl)fkbrN@)io4%|m=SAauP5L?KJ&o9vd*xvqHhZrTYxDzi zXoKNR{k$!we+6ZMODu`fj$teW~KovimlH&#xMm1N$O*A^UZ)q?BwA$~yAaQ{ip|4Q$ zK)CVSU(u4#=;R8E91Gc;sy-SRF2f*}kdg~E0g119<@+^d{rD^};WrUC8-w9%k$*g~aEl1~d>BY>ER}XB40Kn-r zCffFjMpVyGbP7i}o^+x|-aUn}>biC3=jIr1%1l;fudTtK6Rx?!D44<54v+J+-#Nl( z^?BAUpB1;Q7KM+{j{?b>bnP~->COaFA0c$VHD^XqfzvKK3Mi}bQ1}-o$^d|o$ugik z$z1_3N=rA=88It4kx|l4{pND)fs$75e-hI!5)@D9jxnq(#`n7IZBgV;b&XgF|Iccj z=Pqpo4l!wqc1Yq0h03D07UmT-_RmGh3l`+R5uz{wCPQDs!2hY~EV$Zgm}q^_;#S<< z-Q8V_JCtI@-L({Gad&H>K=C5M-624VyA^kLyLrERZ?abYKxWOEnX~t^JFbq*MIL8l zKo?nOuiLL_5wj0)^IAs+`-o@3dn6v3r5_3^D4Ok;tAlb&m2$DUlP(IxTtn8FNClhq zwe$QtpmygDUmvH-sm0X{zY}kUe=SK_t8KT(EBqE^m1ixoe?Q-?TI}D5h(Y4G$p?uD zVCCqovx-^jc~UCIi+P6BFDv&DUwcQclyKhD%%c}k{;S1edKVhfd8XNplt@Jh3xkb? zLkWwGrAf#jG^Ear0g;BKmV}26l0VF0pkpLFKn?AVp#W`Y)2b&S(D{6W*{TFiBuXw9 z8L+hF_(jcS9k9g8B1K9+`l@o2!Xs}<({W@hk(WmT-S>l$z5e}L@Z&2jgC=8Eb*`&f zVs9^c#RmAQeDug%cu%N>PKV}m%Qu7-fR+n4!u-?+^O z?e{k_O`VtB#ov6hZHO+=PDA!z&t5j#Ql>@LZYG+xQ9<@}n83WbIA7=6Wmd=hzN(n- z!KPMKE}dZ}tr@PIl<)0xZv@ZYzt30^eVpe-jCf%m(&?1g@kSee+DD6P)l@g}2ROtK zS%G7~9qPvCW0*yM6XfKn$AOK4kVho$L4ZgmQc^OMk_r~Urus<8@R0kSE(*X3gPstf z`Ag96K6Nt^Hh>HuA$`C?0gyMG{_Bt~Yd7zL`EkRB%o;t0I*W!9ocObO>X*Sb>ax|= z2@8YvL7{p27Hjg~^`PH9IEQL6)&x!C>Vn?KN#Er>BVVWskdwJp-q zSUR?|p-{)Rbq^B7k2{q=kIJH1VY~zS9{Y7#^uG7~sOS6Z<>A?Yj}UmHqawg)K1t9K z7dWYhr2~_zVo04>+C9n1ru5E`LE1Spl2t{CFzCMHf0Zfz)b!@nU_gB9pJQAhm5QWm zYuFn292R}NQ7uz<+K_Eb23fXu9L73(1LKTf7-$j8ouL1ESgew}mee_Cd*)m~==T7mN{ zt6Zd?au~xHQL;DPmbW24>EZI~!=fwLygK56D-%b3cO?VS(sRlNpu5X6eld}Q4 zsO8$j{Dupo|5WGM>OY9qB?tNV{lVAjo!(tS1Zdr!7c4QISXs@yZ>|*NkKD3{qsJ0q z02xv=y{_~5kT&1fh5P<(y=n7PYv&l zBaI({qwOeDp<{2hfw=;+_bISYTs&DKGW^A@EkF9;iNbe# zl0m&(QAs{BFGEMos@ADnO@J&@*udB&51gzcjYn6KgRdkTrZsF9ylM{99k70P$a&5N z=0z7%r_Og*O0qVEWC?AU=%eFy`=ztQ)2~*}FC$GZ%Qa^caV z>MAI~&{GlM+FlfdT(kc-NSX(p*3*HHZ(3KKJ+SCtf8AVFT>OE8?S;^IM}{-pcXpTT z>!)X|4LK(dKuJuDDw>3f$tH|*6`&U2=KEDU9>Gkrj!q0n*{@REh-GdxtpdXg9SFJF z_y%j|U~vG;o2tdM=Vz*tWWTs(DdA}D{&{N2aK;J2Qzm1aR!N`u#fs+8nLlU}Cw13j zX=WcOA&t((&0~SQEFGorx7^gdDkuf|c81+M6}s|%f&pIdwnZL>J2_1}x8Fu$pf zeDG4!=0bVljEfqn&cRj^GQRQ;-i^++;S)d{pH-i&y(L?C-|8ksUf#!-{wv#FvGrn~ zx~oRb0lS3Stm6+NV3+*-ym!}}lgCosR%^ig%aEQ01u9S=k}Pey#x%b#sJcP58!Zq9 zb*@D7t)_PDT6z6S2gk4q`_2~Eyyi8wcAmYQvCfv1 zTay50f%{sT$(W`Oy;9}jijk)05$*oLDzZUB$moVJ{X4~|ktGQls<_d`HWkZf=!8Ww z`lspBn_2a?UNUU4@w7~EOv^^?e@)g`9QhGt%w6H7+^}G}^+T?qHy-FE5_)c@q{QNq z)lFaSh(B4#`uH87MM|Q=p~O~13q=YAK1lPT7x7%{X2Ec)GXg}t%g(JRK<8D5M0BAf zrS$04gAqQ%$tHcu2YufB2+hPfh1}d>C*FmBWjlKF5Jt7FOn3vEs1$0Jq~h?5beW9!#f|%W;A@XC)RHs^t)F$L0$;I-iKJ)T2x8>O zLe!Y*PArXHmZ9JlctM8Gc1!^6`7{` zJs9yzYLv;^WNvmrC~1|xufFtOJnhMG8kNlil0QkmqmbtD1pvyZl&9dw)!$p@elO@P zVJXN|Dkv#dv>z2uPA@ZF=QE}^mnPrGVCH?6FAG!Bkm3>$E8^nR@jYO1?mQ7NGiIMv zr!c?2?FxCBDdUm;nEWdM)9hkX!naHJw{A2&aJ;}T(Gqkz#Zse!tgYVnapA;DavFRKX{eLu^I*Sf3H53?N+-E6V0Rn=_k2wQ`DEOdkZ z^*%^h79TyO4l-%bsE64ya#!hbT>N6$DFizc>yr&k9_24%^l{Sl@DN!&cnJhy0KsQ6 zUD(AkJ(R}^(4cQkUUq6~Em}NHS8PKe5zlE;n&p-HqdRhmB8~VlVO@4={S!q5vxZn! z8u(PrN$W_HaJOzh{L&~p!{2`(|F0x5p*JI`u-Zpn2@twcREny{ zWux<*^h%4R4;Fm|xZ?B&Nd#PD!p==1{4v_c6?XO;QPyP%6*FcLSWrDjg)H ze*<-S93okYa}>ttmcPfHyB;@=%i`g)3y2uf$*gB-z|%Xp^VWQd7}TEZ@o?YvraNe!&#YPnPQQ?_;=ihbY0XKHs6C> z!~)%a($yux%=5i;(*;-!>qMD7EM=^FA60ZXUlviUTv1@I%^zKt(lhAI@jC}jj-Le% zEytOZX>r4RbPsk8zI*M+ARUGOK-`uuaD2DiM;XVZ0^7%`IydzEL*yDqBEE=LsqjO} z>_H2YiW62@iRq|r?xBIW?QRC7nmpvOwiD6@gYfbNo;n&DhlBv!kBZ&F!D3PWl~a|b zRhPyhFdU6}XeVgPnqCQd<)#AecYT5-@R-7;>>5~#je34JYsySn(IO?QXaC9VBLyuijNxnWEmgs2 zv}|Rni_f27=x5}B?8}5diO{uQM`o!Kb8{0ZE2Vq3Iwh4yxIZ)=i9=sUKE32<#_Mtx z1aCj@8}ONr*+quODwS6Pgmr`P?{S)Rx2~sc;S#}D%l(Gt*gtL2{-DwD=*q8Tr$H@W34-O7C7r_CaDW6n zQ1JL_%TcgI&Sz}?ayjoq+Rhuj8vE~I<$3p_lwO|0(C)4g-zYc?oD(C3AFopp8E3R+ zrTV$ikbNhcCC7NrxcvBdEnu;yLuA4`xW_}OL|WaQKF^6|E#mF*XARJ8Yo4hsZ!gG2 zZX<4=ZISi6F81FnYDM*sM>O+X5D%Owl_|ZhJp2@H()?HXV*m2UnI+}X>!wX*F+GrXqVM zkmsHG0zq#nD=HaIRfo4Vr|dDThm)G%0~WK35V42u_vZKiB$_RfVG0Uz&H}y~yELCf zaJ001sMd*w3S69*`P)41u5V4#^9fAgU+C$dqdXi^lW9x!Njohd&ttVIQ00@*NlS1t zttj&7!&E*Pqir(FbAOhZ&TzVqmrGf%V%G2Rwdh3M<;_Vz`0}sb!b9=vHR3$KbT5IKu zbyIgsaW|3n6)wR$^S}lhMZ4UTB*AX&&_qI9np9!tP+g^9q?1x~(SG0B2aer{%G=h? zL0s8-pHFIX>}i?Y6Th;Hh7ASLaCID26gF~gHIwNLaS)>l70n-W%>O&F2j2yq@v}{n zG@H;?rOmEE-!%0{m*ZU(Cq2$QcUGWAJj|f7=Ng9W4|Bk}$XMi;DQB1e43Cbjb9Nor zC{J>y8^xN^K@!)Z5J>$cnw&#i^L+750tE(c;Jx z2Q6JD??xaXM_>tZwaC<8r`T%HuMlLpWE(IF%l{NUmg{hy=dFBP`0Q+z_*?0xxcfl9 zNcrH8XB><8-DX-mNgL zi78cLj6NGyZ&4S8Vm%SBa)KK6<3z;iD;hj-`%?zX`g%XxB66Qay%-S689R{|AZ&+W zb9>grO0$D($^E5pIJw`|VP^&k6GyG_znHyyxo23v-|Xw_zUz=o-g$XR}H3wFT$)M^(1%MNd?*^)`o-!S>v5_h4se~ULxS`I}% z`a#Nt(RinnJj8c2i3bAL!O7jUeDgA{H+14cRs4h5GUdHrF~PgMsxRfZdoh;Fo^9tn z`s+x*dfQz>6xZm+^MS{*tNPve)~~O-E%`}2yy*t% zSrzg0sot$8gEsZTT4kzb-aUj&>O|%52fwD;{yg#t8(cPfsR7RCFxy4rrBz0}nuxQ1 z4%!_uSd2aVHZ3D=DTH0?p7x;0FDQ`$luBHkf}CXZcAgE^_^8W&N3_c;slwu_X%Uqt zRGErc@F_slB!5owWdY;F^kXnb#X! z3<)Z#)KO|&6|Vv6*L+G?C4MU#W5j(uX585cTShnafhj>{cN|_kBJbm@x3M7!Sb*Z` zmLZptw>8<3C5X_5oKp@${0Va0$)Hgn?D21VWUI7MLv_V-i>-~l!^+EYYwd~L_i>?! z`pPS-y6v|7@p#Kz@DSZbm3t8r3$PLWm-JIS_#MO9G zPef3C#E9~yhy;{uiO7$;jEPv4{|?B#GOKyIa(UfC?|LxKws!BKgKnEVR@`3)ms%2D zCWopQntc2$ueKK>u2@b62U~w0iP$ngfkf5;K5M?3Mx}w64#%&Jj*X zlg@IeBcP2iDxN){w*YXa+3@P+)$R(!S3Sx&#oKD2LelHAPUzlxa;ou5bPlf+)M8jR zLm8QPr>kwtlvP5cnW#zWMu#zd>gsS~;PL6HGHL$nU$)di7E8G(>md!Y1Ox>k=AGj( zP0|{u5|antM6QScWab0TfZ0gv3JTgqM^&F3AyUHef58ezOqP-oHDrt>b}tqC@PSO9 zO^voZG_cJ7Qxzy}z<3KcZIhIfX=s#DChi8<$`j zSj3<6c8$=8kE+k3+?L6jjPn7bI-gF|6^9x)9*+30+b5JgEU7O2MYqs0(I{1@;l`T7 zqnSx=_qU(>XPl{2s+T8lxVl`1)?W`4fQ_4wt6ibK1s?9NqaW@5h<`>SJ!RB1vs${p z_w;ZQx?Eh?f1ZGALpK%)+P&5|3)zTzKZNRsjMr$nJR>_G6ySYu?9X7HE;lusB@}c+ zy7w{}RYz;U8m94fdk1WQ1-3sajY1Oqi$~qjs6M3o+hwOZt~TD}eo9^WIfsm{mXvke z_R2Y$F7QpEX(frQxWe4G%&y#+ zWT|lYYKf^~?oH^W24LB$>d_e@=JV@AUcqGM?RQa@s3fbox`(`TK}|!62j!n~4*4=n zAD>C8$L)+tx5Tew3Ie_;B80o2^3{aAsW^*cp{9EIK?2bM=az>O2KBs=`SP3E5qGb0 z>Jc|Vpv?%pmsdAJ$77F)=m6HD65!UIXA2m5K2E_D*CAtCee37ZCkO25 zUjo17*VZw`WaW8u#jcOeMCPmBu@;pwlrg9G?L`f{RZCQlXKFzoZ`LiBb(ab=%9eTy z%H(T}Iy#;#be+XRf?+q_>@X)NSgS_iV7ktP z_LjTa=FMiG)_B8?hd06$Km4H@{0%dnsl+`Z%d;PwG@=~Km9pQTHD)zT_+CD)=QrhV zO*E8jq&j5*-j2V>*v9xds}M}z|VF_DBK zpucPN7oZHYo7SMVTd(wavVDqG$X!4;T2|K(C5|M}KWv2XOpx+&2P z@Ta-mey+<&29O7bozRDsMXZvc6SRreBKoGYVG)y^V7y+r*6K>Ze6Yok}G`5>3w}11;;^F zIrC3!xk(a>L_f{Xf49HoYKS0iMoKK!h;Qfyzd!^YxO?vVI`TY{b30IqDee;}FZ6&F6-{hKRZs=9cfZusSU01S>*hgsJ=INF4H zJmP(1KBcsSU?Tk(Iw4gkPsUf^LoqKgZ!2hS@DGm3YpcM^%i9AX(7ppFAYUz2wb}}? zsO) zB5>pcvCMp*-gMM26C?3)%BxIxWQobUwS7C!0nRh@`zv?#`5|0-?e4ysIJjvGv2j<7 z=dqVU<3=sTS`-DYL?g4hZJfVm4JI7d>0@GPVv`+Tb(SqV9f)v8^i->tL>`*V#cyTT zi4Z}K3u5!o`PGHf#Keq!PET)6c>JJjwaq&&QieK@{^;Qc0w;Z=GoaYXU9l_15`KmL=k+IaWtJGwE@@7 zr*G!&-6E#yK1ep?K|f@B#SH=XVwTGn|MKeKFELy+6i?{?XHQeDw#^-7R6UKSuorS4~T_R^F}>k?WO zMj{b5v#dl0g5{`BZsFf8%leL2<@8@)wCPHM56IpCpeAVAxqxW?z2>$*c~VKVzDL<^ z_TO^&*uJ^QCywTf`%=NZS=38$;Oi*0!Wot+i)gbZgwtd z#hh@*sho)S>1xZ2itUflasa}gR1Aj{e!ET zPZUCuW_Q?z!0=_ogPJOw9hKXNQqW@K9T#`O2TIGk6+aRnaJR?Y+5q0{BM3c@*_IRm zj@odzP*p6$&Z$8w_Gex4_e0Qy7M@mobh6uo<~Dpa6+CND526xz#H|- zX$@@mY?GN?eJU4;zw>|(p(I^wi&xOmo?oGIN@GfssoxEWvoD;$U~KXaJn@UwGl}yt zRNF@)ikJ#DiRw&Il9sR`%}ga8LTJ1qXsLW#iTe<}^Xeu8YO@Susp;w^7COdgJIUvt zb0Zy0qME9xgn@Uc;*w`70@1+^B5-4h|e5xK|2o%2AEb9#rZ7i^{4Z za9M0VYYI8S0Ggjt-ki!js54>hO!6?<^Ybp*#tTe^c?ufa%GW8hJ|T8&=EpQLCtRBO zOvjh0RJR`=>|ws#x4moLgN(%+I@kDL?Tot#E}K>M#zGV5B40ik9+1-9?1#K@0fC3v zdS33L)q3@>4?TzeG`Mshsk{VY!TzQOJgDA2^(}7Zrpiyf7tfpe6Ns~)+ll3aGcmrI z4OD~&XJt8BnaWpsoHkaJO5B~Ze2YGWMYId4ng#tAXI9&P@j94sF#GPC41DykLjT>uPjq%jDwHe81qE z^FS_UPdj9?(ti^tIqmzGU|Ev{c#3>;O{k07e<|3$YPx*^hg zk=*g0QTzH=Q@%j7cIx~n#C3kPKBweLU8LF%B7V654bZ8*f$jG#=7MT;wbPjx(4=cp z1(iqZKJ>oF2O@%wH^2VZ9OIhSW#_K-^yioJSV!XM<~_o(Lai$KjKErUW#byqSAH_5 zt*ZBEiC?`4g{w(sShVY>Q?}|gagBMIDdai@#6U%p3l~Y4>(FG8!24;H)d-r3AsM~t z4~~n1LaN_t+C6WtnfQ#9njBYe2ItL#?Qd4@`;VuVo@*Hj7K6qerz<9$I=3Sj3WVK0 zZH|I{Y=&VOu8oSmoXn0pPGwZ|L8DBE!7Lw?i(Kdp!9FH{r z+#$%@uF1j%LNn|1^S><|{pWrQf?KsL3I}p@yLMYPG%YKqt~c{GlOZ#@AAR(MWuHUUfX})M;g0il`CxfhN$5uz@ z`ObNa7|%l8zA!C^!NV%=CV{tq;BUdM4P!XmbXd(lB}`1;g=z$EX;NB!46RJnqG7BM zO8mNyOzu7bVzV+LMhEnS$GpNB(DwTc5L7t_p)PqgLrGM{(AY#IWUTMv!F$Uv8$AlA zT5{^JX*21R=k`)qfWBY8N-^9zPwO^gA|ZkFBK5g+S!z0sWss!~XqVXQ<23jLEFo^yu-Fz*W4`EnKLg%IL-kpZ7!f||a4px0 z%F`qLn7SSUk$~%dak1kwj-T*qGqze{Mvcp+(9erVa|Csd=J@@5+rCP72x0iyqfUR`3{H;PPXWaQ5=TD2?^>f-# z>}92P8d5MXWD0_6`WS1i3&3Dz36Ue*tAJSSe2S#E4uPO}&!hDF8{HlPVmH ztVR#2>&Hrhvi6`RfxkGw7X*2D#oL54XeJ1z*dultL1^l)EUl?4=3Wc+?r1Ii7ArK9 zjCbltgTnba;F(QzCTm<2%RFjGL_wIOOJeg8emOiY{2VhS)mifaRNi+Nu&zkt z^(_mevmJHrHb0SN5yE?n*H_7k`jkE2uGfF#Eo?+_gEBQg;oKU0HE3G^6^K%fNJX+? z&wu4j8DZR60fajo`!gw;VSp0mms2V^p2zLg4okG&KtUWy>D} zo5(44E*;JCGcp_1GraJMTLf50fqjLta(1isV($LFK~8TtX^u|CtE1)0Mf?eRy8ia` zrjz4XW)O6+Ar#eMp^?gqXgjfExz&VNwVOY<(z0o9v;D4SZCz!a-2d4FLh*dtjPKHV z8ku1xnoe899BX>pN|KN#HbtI$=ACl$-Y$8UdK<*#W8;AadS(eZ#|1=v!LlG|(8tqP z_Yd|E+k7li?C?C73eIz*IA8bj?4sAri`SoD)NonSFhonqTV^v8bj9#J*p=Wotocn$ zyB_#N^dPJ&PusuYEV9s3G%)}N78S`*BzUCm%1L0j1)%!dx zmojv1z;%nnMUvo3Q!|cOSsBY9t1@)cklAk_C(j-~vc^ZBKqFsI&$dr1rCJ@)BS2u* zJ|+10_S@Hz5GI%cCZ9EJZq$fH5S4}lNsMx4S9gh!TDk}whKgEd)3qlP7&L{{=oTrnmGcdSZ>4c_ zME92<3?2Hs9vB0zccy2R@qYAra`?LRE7at4d_t!hYKZ!yt@zC1ELq{#w+9Z&iz0rv z4=E>b9W=IP^(RYP3=<{X5uHoz7RCjbP%2f; zZA-f#_$baL_=-(n{i=wZ_=e0Z1;c;6%g*{sIG zAV4E}UvIn%O-m|hL1<|thH`_nbf9AC;bhTyJaX6~EiFT3(~_)-7*#5to;1tp6pIfx zTOU*Ls<{bF0}qS>ZniHX9*PuDnELd14=WE%Xx?|E=>qrnaO+l~_@Pw7Vc4)qHt>?& zp``4gKc|)qV1YUSK8y!eYl+YS`8yI47QjXc;J{!TNK${4Ebu*Z0?s#zD5jKQ&g&A5 zz=FREKhi25YGuf0OCvd_l|8zZX~o3?rn{UrdQT4H@>PEgDa+G)P0){KBZo|wQ0aJZ z^EZB4Eathi5CEsX^CYAYmAYQ+-0ofBkt;MLBFdp=;(pL>II`MA-RU1pe4lwEGvp4l zDgLq{zp72_+E+$4P<%O_4bhhaug55OiP-kz+1M&vyV>AL^nE>DC3G`qdoc)@ncM<8; zmZYcDt>4QPJygsrZP>H(UV9QS$j|7l<*fRu9{yNvdV#{zE(GW=f0?7};bQ2cw(Ji{Vy$)lN`-j5kEHv14 z@T@f46m~qZXJuu&n`8i&q6ePy@+B9|0;rtXP+=OUL?>2*WK)i#YrpOOX=;X)u`%T3M2^-xXz-j8v?mLZL>u zM5yf!38@>F5=Q3!KCcO`d!GCOeb-gt-Y)t2sfe+Xt7nCBO!rePVnZ&nsq!~C85TZ3 zxbujoEE=YlMs&HmlQny!m3f@*cF+FIj?AEZw?kc{@qbe0Y zN1U&Z%q(TXTW^W(OHXU{5TSMXc#DCkTcKW_$q5Fbo0I5Z;k>!u!PL~n-k!b+jpp#@ z6(m=+f>xHVkdQ|yXgcQ8u5ZU-a~dn#l_+NJl$@v75Vyzu2V*3c>49asUQ9Dl0SQ*W zw4+44Xmod?hxWrdy<-janfL-TreC;8|(0=wDr{d26IG7Ox8pt7;4GA!yxWHdYV z?#8n}2%7+(l~@JnCJaST39aNMj-np29&@Eas%l(v3p{O&+8+zPi2|WLPDsL6#&{5vnu-#Z zn8GBFz@W~*!qr!0 z-HjIB(9jM$mxobeviWkeM-B^wqq2AHY887j=?W{?^JyV`H>lm zvDJDzWj7H%vEQv~n-ND3e=sfVSgRBnWU#-0na*~VKb7eIY}nGb&7Q9Ew+2m{?N{0H zrn{HYulz6??>~H~FL>_n0M7TxNH}9#wjBN`RR>y#8HbY#>ALL?TsZ^<9PD^>m`M2l zUgdYR8lOQ7`CXg%3sm-Q@^ zLp`Scz>^m{C_6)7Rq@Cu7MhXZgA3UbT-DEMO)(o0wYs+Bi%h@Jcx#cPSN-s-^v ztax4pfBCF>AlQpd%4Sp-E^F0eL=7uHX8$1^i80ZGQl?wbgLDF`r7zPp;#=-dNCUHH z85^=gB8r2%|9P7^La4;!<@}_9n(R3G1P!zDwe3WsH#u`B;fgV^bV-I|2us z*LK~<=<5*(88xBBV&C}x&CDcsiT|T$KN&tI^#CHJq@P#PhS+w9TGJ)VPV~_*KuSg< zyG=_Sk7)148l(KjEroH%FVOw1FiC}O7bQC00pz|z`1mdCD1D@5Rp0mlqWf*W_C5u@ zcn*RmZ-G{n*BvYO&ObCV<8GK@@G$bm8t2qpT1!U91L!{9DrCRN$xSJyuP?KO&6TyY zWmJR!v;b2$_&EcWST_eY=m_GVB!~ee8b`)bQJYA@qrq&#Qag;8{fOf-awsylMoW^i z3=Q=@TZ9LiRv$g^dErh_EcgtoL=~`Ps!~6$U}I2DmscWD5yC-|21DuUA``76wpZ}R z+(>&&)M4R2e^}!F7E>-0pL|%;d94qee>3<0@ScG?6uQ$&8gEg4)u%#|{h|oSengI~ zh1LyLDElAT@a{w^02`~2uI|4|g@y7XStOuiW#D$7)?N}x>zKLm5#5xaZivh**-fj= zZZb)PR&`~S_Hl~Lv}Tnmcub2>K{>;y?EBTt%@=}qo!@2|nJ;w&)vJrGt3k8|_npgc zq=5LN!+Gz5Qhnwg=R8E)>eLNpoh)#&H8*JD;G}+|uB~($_dF=x^}h`o1}gvjKe1!FEm? P0R1S*K`lYjAH)6+L;tas literal 0 HcmV?d00001 diff --git a/official/vision/gan/README.md b/official/vision/gan/README.md new file mode 100644 index 0000000..1f2b01b --- /dev/null +++ b/official/vision/gan/README.md @@ -0,0 +1,57 @@ +Generative Adversarial Networks +--- + +This directory provides code to build, train and evaluate popular GAN models including DCGAN and WGAN. Most of the code are modified from a well-written and reproducible GAN benchmark [pytorch_mimicry](https://github.com/kwotsin/mimicry). + +We provide pretrained DCGAN and WGAN on cifar10. They use similar ResNet backbone and share the same training setting. + +![images generated by DCGAN](../../assets/dcgan.png) + +#### Training Parameters +| Resolution | Batch Size | Learning Rate | β1 | β2 | Decay Policy | ndis | niter | +|:----------:|:----------:|:-------------:|:-------------:|:-------------:|:------------:|:---------------:|------------------| +| 32 x 32 | 64 | 2e-4 | 0.0 | 0.9 | Linear | 5 | 100K | + +Their FID and Inception Score(IS) are listed below. + +#### Metrics +| Metric | Method | +|:--------------------------------:|:---------------------------------------:| +| [Inception Score (IS)](https://arxiv.org/abs/1606.03498) | 50K samples at 10 splits| +| [Fréchet Inception Distance (FID)](https://arxiv.org/abs/1706.08500) | 50K real/generated samples | +| [Kernel Inception Distance (KID)](https://arxiv.org/abs/1801.01401) | 50K real/generated samples, averaged over 10 splits.| + + +#### Cifar10 Results +| Method | FID Score | IS Score | KID Score | +| :-: | :-: | :-: | :-: | +| DCGAN | 27.2 | 7.0 | 0.0242 | +| WGAN-WC | 30.5 | 6.7 | 0.0249 | + +### Generate images with pretrained weights + +```python +import megengine.hub as hub +import megengine_mimicry.nets.dcgan.dcgan_cifar as dcgan +import megengine_mimicry.utils.vis as vis + +netG = dcgan.DCGANGeneratorCIFAR() +netG.load_state_dict(hub.load_serialized_obj_from_url("https://data.megengine.org.cn/models/weights/dcgan_cifar.pkl")) +images = dcgan_generator.generate_images(num_images=64) # in NCHW format with normalized pixel values in [0, 1] +grid = vis.make_grid(images) # in HW3 format with [0, 255] BGR images for visualization +vis.save_image(grid, "visual.png") +``` + +### Train and evaluate a DCGAN or WGAN + +```bash +# train and evaluate a DCGAN +python3 train_dcgan.py +# train and evaluate a WGAN +python3 train_wgan.py +``` + +#### Tensorboard visualization +```bash +tensorboard --logdir ./log --bind_all +``` diff --git a/official/vision/gan/megengine_mimicry/__init__.py b/official/vision/gan/megengine_mimicry/__init__.py new file mode 100644 index 0000000..76c0068 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +from . import nets, training, datasets, metrics diff --git a/official/vision/gan/megengine_mimicry/datasets/__init__.py b/official/vision/gan/megengine_mimicry/datasets/__init__.py new file mode 100644 index 0000000..7a49c58 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/datasets/__init__.py @@ -0,0 +1 @@ +from .data_utils import load_dataset diff --git a/official/vision/gan/megengine_mimicry/datasets/data_utils.py b/official/vision/gan/megengine_mimicry/datasets/data_utils.py new file mode 100755 index 0000000..7e45318 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/datasets/data_utils.py @@ -0,0 +1,77 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Script for loading datasets. +""" +import os + +import megengine.data as data +import megengine.data.transform as T + + +def load_dataset(root, name, **kwargs): + """ + Loads different datasets specifically for GAN training. + By default, all images are normalized to values in the range [-1, 1]. + + Args: + root (str): Path to where datasets are stored. + name (str): Name of dataset to load. + + Returns: + Dataset: Torch Dataset object for a specific dataset. + """ + if name == "cifar10": + return load_cifar10_dataset(root, **kwargs) + + else: + raise ValueError("Invalid dataset name {} selected.".format(name)) + + +def load_cifar10_dataset(root=None, + split='train', + download=True, + **kwargs): + """ + Loads the CIFAR-10 dataset. + + Args: + root (str): Path to where datasets are stored. + split (str): The split of data to use. + download (bool): If True, downloads the dataset. + + Returns: + Dataset: Torch Dataset object. + """ + dataset_dir = root + if dataset_dir and not os.path.exists(dataset_dir): + os.makedirs(dataset_dir) + + # Build datasets + if split == "train": + dataset = data.dataset.CIFAR10(root=dataset_dir, + train=True, + download=download, + **kwargs) + elif split == "test": + dataset = data.dataset.CIFAR10(root=dataset_dir, + train=False, + download=download, + **kwargs) + else: + raise ValueError("split argument must one of ['train', 'val']") + + return dataset diff --git a/official/vision/gan/megengine_mimicry/datasets/image_loader.py b/official/vision/gan/megengine_mimicry/datasets/image_loader.py new file mode 100644 index 0000000..3a05bad --- /dev/null +++ b/official/vision/gan/megengine_mimicry/datasets/image_loader.py @@ -0,0 +1,100 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Loads randomly sampled images from datasets for computing metrics. +""" +import os + +import numpy as np +import megengine.data.transform as T + +from . import data_utils + + +def get_random_images(dataset, num_samples): + """ + Randomly sample without replacement num_samples images. + + Args: + dataset (Dataset): Torch Dataset object for indexing elements. + num_samples (int): The number of images to randomly sample. + + Returns: + Tensor: Batch of num_samples images in np array form [N, H, W, C](0-255). + """ + choices = np.random.choice(range(len(dataset)), + size=num_samples, + replace=False) + + images = [] + for choice in choices: + img = np.array(dataset[choice][0]) + img = np.expand_dims(img, axis=0) + images.append(img) + images = np.concatenate(images, axis=0) + + return images + + +def get_cifar10_images(num_samples, root=None, **kwargs): + """ + Loads randomly sampled CIFAR-10 training images. + + Args: + num_samples (int): The number of images to randomly sample. + root (str): The root directory where all datasets are stored. + + Returns: + Tensor: Batch of num_samples images in np array form. + """ + dataset = data_utils.load_cifar10_dataset(root=root, **kwargs) + + images = get_random_images(dataset, num_samples) + + return images + + +def get_dataset_images(dataset_name, num_samples=50000, **kwargs): + """ + Randomly sample num_samples images based on input dataset name. + + Args: + dataset_name (str): Dataset name to load images from. + num_samples (int): The number of images to randomly sample. + + Returns: + Tensor: Batch of num_samples images from the specific dataset in np array form. + """ + if dataset_name == "cifar10": + images = get_cifar10_images(num_samples, **kwargs) + + elif dataset_name == "cifar10_test": + images = get_cifar10_images(num_samples, split='test', **kwargs) + + else: + raise ValueError("Invalid dataset name {}.".format(dataset_name)) + + # Check shape and permute if needed + if images.shape[1] == 3: + images = images.transpose((0, 2, 3, 1)) + + # Ensure the values lie within the correct range, otherwise there might be some + # preprocessing error from the library causing ill-valued scores. + if np.min(images) < 0 or np.max(images) > 255: + raise ValueError( + 'Image pixel values must lie between 0 to 255 inclusive.') + + return images diff --git a/official/vision/gan/megengine_mimicry/metrics/__init__.py b/official/vision/gan/megengine_mimicry/metrics/__init__.py new file mode 100644 index 0000000..75ba356 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +from . import fid, kid, inception_score, inception_model +from .compute_fid import * +from .compute_is import * +from .compute_kid import * +from .compute_metrics import * diff --git a/official/vision/gan/megengine_mimicry/metrics/compute_fid.py b/official/vision/gan/megengine_mimicry/metrics/compute_fid.py new file mode 100755 index 0000000..c5014b0 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/compute_fid.py @@ -0,0 +1,237 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +MegEngine interface for computing FID. +""" +import os +import random +import time + +import numpy as np +import tensorflow as tf + +from ..datasets.image_loader import get_dataset_images +from .fid import fid_utils +from .inception_model import inception_utils +from .utils import _normalize_images + + +def compute_real_dist_stats(num_samples, + sess, + dataset_name, + batch_size, + stats_file=None, + seed=0, + verbose=True, + log_dir='./log'): + """ + Reads the image data and compute the FID mean and cov statistics + for real images. + + Args: + num_samples (int): Number of real images to compute statistics. + sess (Session): TensorFlow session to use. + dataset_name (str): The name of the dataset to load. + batch_size (int): The batch size to feedforward for inference. + stats_file (str): The statistics file to load from if there is already one. + verbose (bool): If True, prints progress of computation. + log_dir (str): Directory where feature statistics can be stored. + + Returns: + ndarray: Mean features stored as np array. + ndarray: Covariance of features stored as np array. + """ + # Create custom stats file name + if stats_file is None: + stats_dir = os.path.join(log_dir, 'metrics', 'fid', 'statistics') + if not os.path.exists(stats_dir): + os.makedirs(stats_dir) + + stats_file = os.path.join( + stats_dir, + "fid_stats_{}_{}k_run_{}.npz".format(dataset_name, + num_samples // 1000, seed)) + + if stats_file and os.path.exists(stats_file): + print("INFO: Loading existing statistics for real images...") + f = np.load(stats_file) + m_real, s_real = f['mu'][:], f['sigma'][:] + f.close() + + else: + # Obtain the numpy format data + print("INFO: Obtaining images...") + images = get_dataset_images(dataset_name, num_samples=num_samples) + images = images[:, :, :, ::-1] # NOTE: opencv image convert to RGB + + # Compute the mean and cov + print("INFO: Computing statistics for real images...") + m_real, s_real = fid_utils.calculate_activation_statistics( + images=images, sess=sess, batch_size=batch_size, verbose=verbose) + + if not os.path.exists(stats_file): + print("INFO: Saving statistics for real images...") + np.savez(stats_file, mu=m_real, sigma=s_real) + + return m_real, s_real + + +def compute_gen_dist_stats(netG, + num_samples, + sess, + device, + seed, + batch_size, + print_every=20, + verbose=True): + """ + Directly produces the images and convert them into numpy format without + saving the images on disk. + + Args: + netG (Module): Torch Module object representing the generator model. + num_samples (int): The number of fake images for computing statistics. + sess (Session): TensorFlow session to use. + device (str): Device identifier to use for computation. + seed (int): The random seed to use. + batch_size (int): The number of samples per batch for inference. + print_every (int): Interval for printing log. + verbose (bool): If True, prints progress. + + Returns: + ndarray: Mean features stored as np array. + ndarray: Covariance of features stored as np array. + """ + # Set model to evaluation mode + netG.eval() # NOTE: in MegEngine this may has no effect + + # Inference variables + batch_size = min(num_samples, batch_size) + + # Collect all samples() + images = [] + start_time = time.time() + for idx in range(num_samples // batch_size): + # Collect fake image + fake_images = netG.generate_images(num_images=batch_size).numpy() + images.append(fake_images) + + # Print some statistics + if (idx + 1) % print_every == 0: + end_time = time.time() + print( + "INFO: Generated image {}/{} [Random Seed {}] ({:.4f} sec/idx)" + .format( + (idx + 1) * batch_size, num_samples, seed, + (end_time - start_time) / (print_every * batch_size))) + start_time = end_time + + # Produce images in the required (N, H, W, 3) format for FID computation + images = np.concatenate(images, 0) # Gives (N, 3, H, W) BGR + images = _normalize_images(images) # Gives (N, H, W, 3) RGB + + # Compute the FID + print("INFO: Computing statistics for fake images...") + m_fake, s_fake = fid_utils.calculate_activation_statistics( + images=images, sess=sess, batch_size=batch_size, verbose=verbose) + + return m_fake, s_fake + + +def fid_score(num_real_samples, + num_fake_samples, + netG, + device, + seed, + dataset_name, + batch_size=50, + verbose=True, + stats_file=None, + log_dir='./log'): + """ + Computes FID stats using functions that store images in memory for speed and fidelity. + Fidelity since by storing images in memory, we don't subject the scores to different read/write + implementations of imaging libraries. + + Args: + num_real_samples (int): The number of real images to use for FID. + num_fake_samples (int): The number of fake images to use for FID. + netG (Module): Torch Module object representing the generator model. + device (str): Device identifier to use for computation. + seed (int): The random seed to use. + dataset_name (str): The name of the dataset to load. + batch_size (int): The batch size to feedforward for inference. + verbose (bool): If True, prints progress. + stats_file (str): The statistics file to load from if there is already one. + log_dir (str): Directory where feature statistics can be stored. + + Returns: + float: Scalar FID score. + """ + start_time = time.time() + + # Make sure the random seeds are fixed + # torch.manual_seed(seed) + random.seed(seed) + np.random.seed(seed) + + # Setup directories + inception_path = os.path.join(log_dir, 'metrics', 'inception_model') + + # Setup the inception graph + inception_utils.create_inception_graph(inception_path) + + # Start producing statistics for real and fake images + if device is not None: + # Avoid unbounded memory usage + gpu_options = tf.compat.v1.GPUOptions(allow_growth=True, + per_process_gpu_memory_fraction=0.15, + visible_device_list=str(device)) + config = tf.compat.v1.ConfigProto(gpu_options=gpu_options) + + else: + config = tf.compat.v1.ConfigProto(device_count={'GPU': 0}) + + with tf.compat.v1.Session(config=config) as sess: + sess.run(tf.compat.v1.global_variables_initializer()) + + m_real, s_real = compute_real_dist_stats(num_samples=num_real_samples, + sess=sess, + dataset_name=dataset_name, + batch_size=batch_size, + verbose=verbose, + stats_file=stats_file, + log_dir=log_dir, + seed=seed) + + m_fake, s_fake = compute_gen_dist_stats(netG=netG, + num_samples=num_fake_samples, + sess=sess, + device=device, + seed=seed, + batch_size=batch_size, + verbose=verbose) + + FID_score = fid_utils.calculate_frechet_distance(mu1=m_real, + sigma1=s_real, + mu2=m_fake, + sigma2=s_fake) + + print("INFO: FID Score: {} [Time Taken: {:.4f} secs]".format( + FID_score, + time.time() - start_time)) + + return float(FID_score) diff --git a/official/vision/gan/megengine_mimicry/metrics/compute_is.py b/official/vision/gan/megengine_mimicry/metrics/compute_is.py new file mode 100644 index 0000000..cb86f98 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/compute_is.py @@ -0,0 +1,89 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +MegEngine interface for computing Inception Score. +""" +import os +import random +import time + +import numpy as np + +from .inception_model import inception_utils +from .inception_score import inception_score_utils as tf_inception_score +from .utils import _normalize_images + + +def inception_score(netG, + device, + num_samples, + batch_size=50, + splits=10, + log_dir='./log', + seed=0, + print_every=20): + """ + Computes the inception score of generated images. + + Args: + netG (Module): The generator model to use for generating images. + device (Device): Torch device object to send model and data to. + num_samples (int): The number of samples to generate. + batch_size (int): Batch size per feedforward step for inception model. + splits (int): The number of splits to use for computing IS. + log_dir (str): Path to store metric computation objects. + seed (int): Random seed for generation. + Returns: + Mean and standard deviation of the inception score computed from using + num_samples generated images. + """ + # Make sure the random seeds are fixed + random.seed(seed) + np.random.seed(seed) + + # Build inception + inception_path = os.path.join(log_dir, 'metrics/inception_model') + inception_utils.create_inception_graph(inception_path) + + # Inference variables + batch_size = min(batch_size, num_samples) + num_batches = num_samples // batch_size + + # Get images + images = [] + start_time = time.time() + for idx in range(num_batches): + fake_images = netG.generate_images(num_images=batch_size).numpy() + + fake_images = _normalize_images(fake_images) # NCHW(BGR) -> NHWC(RGB) + images.append(fake_images) + + if (idx + 1) % min(print_every, num_batches) == 0: + end_time = time.time() + print( + "INFO: Generated image {}/{} [Random Seed {}] ({:.4f} sec/idx)" + .format( + (idx + 1) * batch_size, num_samples, seed, + (end_time - start_time) / (print_every * batch_size))) + start_time = end_time + + images = np.concatenate(images, axis=0) + + IS_score = tf_inception_score.get_inception_score(images, + splits=splits, + device=device) + print("INFO: IS Score: {}".format(IS_score)) + return IS_score diff --git a/official/vision/gan/megengine_mimicry/metrics/compute_kid.py b/official/vision/gan/megengine_mimicry/metrics/compute_kid.py new file mode 100644 index 0000000..9255e25 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/compute_kid.py @@ -0,0 +1,241 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +MegEngine interface for computing KID. +""" +import os +import random +import time + +import numpy as np +import tensorflow as tf + +from ..datasets.image_loader import get_dataset_images +from .inception_model import inception_utils +from .kid import kid_utils +from .utils import _normalize_images + + +def compute_real_dist_feat(num_samples, + sess, + dataset_name, + batch_size, + seed=0, + verbose=True, + feat_file=None, + log_dir='./log'): + """ + Reads the image data and compute the real image features. + + Args: + num_samples (int): Number of real images to compute features. + sess (Session): TensorFlow session to use. + dataset_name (str): The name of the dataset to load. + batch_size (int): The batch size to feedforward for inference. + feat_file (str): The features file to load from if there is already one. + verbose (bool): If True, prints progress of computation. + log_dir (str): Directory where features can be stored. + + Returns: + ndarray: Inception features of real images. + """ + # Create custom feat file name + if feat_file is None: + feat_dir = os.path.join(log_dir, 'metrics', 'kid', 'features') + if not os.path.exists(feat_dir): + os.makedirs(feat_dir) + + feat_file = os.path.join( + feat_dir, + "kid_feat_{}_{}k_run_{}.npz".format(dataset_name, + num_samples // 1000, seed)) + + if feat_file and os.path.exists(feat_file): + print("INFO: Loading existing features for real images...") + f = np.load(feat_file) + real_feat = f['feat'][:] + f.close() + + else: + # Obtain the numpy format data + print("INFO: Obtaining images...") + images = get_dataset_images(dataset_name, num_samples=num_samples) + + # Compute the mean and cov + print("INFO: Computing features for real images...") + real_feat = inception_utils.get_activations(images=images, + sess=sess, + batch_size=batch_size, + verbose=verbose) + + print("INFO: Saving features for real images...") + np.savez(feat_file, feat=real_feat) + + return real_feat + + +def compute_gen_dist_feat(netG, + num_samples, + sess, + device, + seed, + batch_size, + print_every=20, + verbose=True): + """ + Directly produces the images and convert them into numpy format without + saving the images on disk. + + Args: + netG (Module): Torch Module object representing the generator model. + num_samples (int): The number of fake images for computing features. + sess (Session): TensorFlow session to use. + device (str): Device identifier to use for computation. + seed (int): The random seed to use. + batch_size (int): The number of samples per batch for inference. + print_every (int): Interval for printing log. + verbose (bool): If True, prints progress. + + Returns: + ndarray: Inception features of generated images. + """ + batch_size = min(num_samples, batch_size) + + # Set model to evaluation mode + netG.eval() + + # Collect num_samples of fake images + images = [] + + # Collect all samples + start_time = time.time() + for idx in range(num_samples // batch_size): + fake_images = netG.generate_images(num_images=batch_size).numpy() + + # Collect fake image + images.append(fake_images) + + # Print some statistics + if (idx + 1) % print_every == 0: + end_time = time.time() + print( + "INFO: Generated image {}/{} [Random Seed {}] ({:.4f} sec/idx)" + .format( + (idx + 1) * batch_size, num_samples, seed, + (end_time - start_time) / (print_every * batch_size))) + start_time = end_time + + # Produce images in the required (N, H, W, 3) format for kid computation + images = np.concatenate(images, 0) # Gives (N, 3, H, W) BGR + images = _normalize_images(images) # Gives (N, H, W, 3) RGB + + # Compute the kid + print("INFO: Computing features for fake images...") + fake_feat = inception_utils.get_activations(images=images, + sess=sess, + batch_size=batch_size, + verbose=verbose) + + return fake_feat + + +def kid_score(num_subsets, + subset_size, + netG, + device, + seed, + dataset_name, + batch_size=50, + verbose=True, + feat_file=None, + log_dir='./log'): + """ + Computes KID score. + + Args: + num_subsets (int): Number of subsets to compute average MMD. + subset_size (int): Size of subset for computing MMD. + netG (Module): Torch Module object representing the generator model. + device (str): Device identifier to use for computation. + seed (int): The random seed to use. + dataset_name (str): The name of the dataset to load. + batch_size (int): The batch size to feedforward for inference. + feat_file (str): The path to specific inception features for real images. + log_dir (str): Directory where features can be stored. + verbose (bool): If True, prints progress. + + Returns: + tuple: Scalar mean and std of KID scores computed. + """ + start_time = time.time() + + # Make sure the random seeds are fixed + random.seed(seed) + np.random.seed(seed) + + # Directories + inception_path = os.path.join(log_dir, 'metrics', 'inception_model') + + # Setup the inception graph + inception_utils.create_inception_graph(inception_path) + + # Decide sample size + num_samples = int(num_subsets * subset_size) + + # Start producing features for real and fake images + if device is not None: + # Avoid unbounded memory usage + gpu_options = tf.compat.v1.GPUOptions(allow_growth=True, + per_process_gpu_memory_fraction=0.15, + visible_device_list=str(device)) + config = tf.compat.v1.ConfigProto(gpu_options=gpu_options) + + else: + config = tf.compat.v1.ConfigProto(device_count={'GPU': 0}) + + with tf.compat.v1.Session(config=config) as sess: + sess.run(tf.compat.v1.global_variables_initializer()) + + real_feat = compute_real_dist_feat(num_samples=num_samples, + sess=sess, + dataset_name=dataset_name, + batch_size=batch_size, + verbose=verbose, + feat_file=feat_file, + log_dir=log_dir, + seed=seed) + + fake_feat = compute_gen_dist_feat(netG=netG, + num_samples=num_samples, + sess=sess, + device=device, + seed=seed, + batch_size=batch_size, + verbose=verbose) + + # Compute the KID score + scores = kid_utils.polynomial_mmd_averages(real_feat, + fake_feat, + n_subsets=num_subsets, + subset_size=subset_size) + + mmd_score, mmd_std = float(np.mean(scores)), float(np.std(scores)) + + print("INFO: KID: {:.4f} ± {:.4f} [Time Taken: {:.4f} secs]".format( + mmd_score, mmd_std, + time.time() - start_time)) + + return mmd_score, mmd_std diff --git a/official/vision/gan/megengine_mimicry/metrics/compute_metrics.py b/official/vision/gan/megengine_mimicry/metrics/compute_metrics.py new file mode 100644 index 0000000..4d5d2ea --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/compute_metrics.py @@ -0,0 +1,191 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Computes different GAN metrics for a generator. +""" +import os + +import numpy as np + +from . import compute_fid, compute_is, compute_kid +from ..utils import common + + +def evaluate(metric, + netG, + log_dir, + evaluate_range=None, + evaluate_step=None, + num_runs=3, + start_seed=0, + overwrite=False, + write_to_json=True, + device=None, + **kwargs): + """ + Evaluates a generator over several runs. + + Args: + metric (str): The name of the metric for evaluation. + netG (Module): Torch generator model to evaluate. + log_dir (str): The path to the log directory. + evaluate_range (tuple): The 3 valued tuple for defining a for loop. + evaluate_step (int): The specific checkpoint to load. Used in place of evaluate_range. + device (str): Device identifier to use for computation. + num_runs (int): The number of runs to compute FID for each checkpoint. + start_seed (int): Starting random seed to use. + write_to_json (bool): If True, writes to an output json file in log_dir. + overwrite (bool): If True, then overwrites previous metric score. + + Returns: + None + """ + if metric == 'kid': + if 'num_subsets' not in kwargs or 'subset_size' not in kwargs: + raise ValueError( + "num_subsets and subset_size must be provided for KID computation.") + + elif metric == 'fid': + if 'num_real_samples' not in kwargs or 'num_fake_samples' not in kwargs: + raise ValueError( + "num_real_samples and num_fake_samples must be provided for FID computation.") + + elif metric == 'inception_score': + if 'num_samples' not in kwargs: + raise ValueError("num_samples must be provided for IS computation.") + + else: + choices = ['fid', 'kid', 'inception_score'] + raise ValueError("Invalid metric {} selected. Choose from {}.".format(metric, choices)) + + if evaluate_range and evaluate_step or not (evaluate_step + or evaluate_range): + raise ValueError( + "Only one of evaluate_step or evaluate_range can be defined.") + + if evaluate_range: + if (type(evaluate_range) != tuple + or not all(map(lambda x: type(x) == int, evaluate_range))): + raise ValueError( + "evaluate_range must be a tuple of ints (start, end, step).") + + ckpt_dir = os.path.join(log_dir, 'checkpoints', 'netG') + + if not os.path.exists(ckpt_dir): + raise ValueError( + "Checkpoint directory {} cannot be found in log_dir.".format( + ckpt_dir)) + + # Decide naming convention + names_dict = { + 'fid': 'FID', + 'inception_score': 'Inception Score', + 'kid': 'KID', + } + + # Set output file and restore if available. + if metric == 'fid': + output_file = os.path.join( + log_dir, + 'fid_{}k_{}k.json'.format(kwargs['num_real_samples'] // 1000, + kwargs['num_fake_samples'] // 1000)) + + elif metric == 'inception_score': + output_file = os.path.join( + log_dir, + 'inception_score_{}k.json'.format(kwargs['num_samples'] // 1000)) + + elif metric == 'kid': + output_file = os.path.join( + log_dir, 'kid_{}k_{}_subsets.json'.format( + kwargs['num_subsets'] * kwargs['subset_size'] // 1000, + kwargs['num_subsets'])) + + if os.path.exists(output_file): + scores_dict = common.load_from_json(output_file) + scores_dict = dict([(int(k), v) for k, v in scores_dict.items()]) + + else: + scores_dict = {} + + # Evaluate across a range + start, end, interval = evaluate_range or (evaluate_step, evaluate_step, + evaluate_step) + for step in range(start, end + 1, interval): + # Skip computed scores + if step in scores_dict and write_to_json and not overwrite: + print("INFO: {} at step {} has been computed. Skipping...".format( + names_dict[metric], step)) + continue + + # Load and restore the model checkpoint + ckpt_file = os.path.join(ckpt_dir, 'netG_{}_steps.pth'.format(step)) + if not os.path.exists(ckpt_file): + print("INFO: Checkpoint at step {} does not exist. Skipping...". + format(step)) + continue + netG.restore_checkpoint(ckpt_file=ckpt_file, optimizer=None) + + # Compute score for each seed + scores = [] + for seed in range(start_seed, start_seed + num_runs): + print("INFO: Computing {} in memory...".format(names_dict[metric])) + + # Obtain only the raw score without var + if metric == "fid": + score = compute_fid.fid_score(netG=netG, + seed=seed, + device=device, + log_dir=log_dir, + **kwargs) + + elif metric == "inception_score": + score, _ = compute_is.inception_score(netG=netG, + seed=seed, + device=device, + log_dir=log_dir, + **kwargs) + + elif metric == "kid": + score, _ = compute_kid.kid_score(netG=netG, + device=device, + seed=seed, + log_dir=log_dir, + **kwargs) + + scores.append(score) + print("INFO: {} (step {}) [seed {}]: {}".format( + names_dict[metric], step, seed, score)) + + scores_dict[step] = scores + + # Print the scores in order + for step in range(start, end + 1, interval): + if step in scores_dict: + scores = scores_dict[step] + mean = np.mean(scores) + std = np.std(scores) + + print("INFO: {} (step {}): {} (± {}) ".format( + names_dict[metric], step, mean, std)) + + # Save to output file + if write_to_json: + common.write_to_json(scores_dict, output_file) + + print("INFO: {} Evaluation completed!".format(names_dict[metric])) + + return scores_dict diff --git a/official/vision/gan/megengine_mimicry/metrics/fid/__init__.py b/official/vision/gan/megengine_mimicry/metrics/fid/__init__.py new file mode 100644 index 0000000..d163bc9 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/fid/__init__.py @@ -0,0 +1 @@ +from .fid_utils import * diff --git a/official/vision/gan/megengine_mimicry/metrics/fid/fid_utils.py b/official/vision/gan/megengine_mimicry/metrics/fid/fid_utils.py new file mode 100755 index 0000000..b104c4c --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/fid/fid_utils.py @@ -0,0 +1,104 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Helper functions for calculating FID as adopted from the official FID code: +https://github.com/kwotsin/dissertation/blob/master/eval/TTUR/fid.py +""" +import numpy as np +from scipy import linalg + +from ..inception_model import inception_utils + + +def calculate_frechet_distance(mu1, sigma1, mu2, sigma2, eps=1e-6): + """ + Numpy implementation of the Frechet Distance. + The Frechet distance between two multivariate Gaussians X_1 ~ N(mu_1, C_1) + and X_2 ~ N(mu_2, C_2) is + d^2 = ||mu_1 - mu_2||^2 + Tr(C_1 + C_2 - 2*sqrt(C_1*C_2)). + + Stable version by Dougal J. Sutherland. + + Args: + mu1 : Numpy array containing the activations of the pool_3 layer of the + inception net ( like returned by the function 'get_predictions') + for generated samples. + mu2: The sample mean over activations of the pool_3 layer, precalcualted + on an representive data set. + sigma1 (ndarray): The covariance matrix over activations of the pool_3 layer for + generated samples. + sigma2: The covariance matrix over activations of the pool_3 layer, + precalcualted on an representive data set. + + Returns: + np.float64: The Frechet Distance. + """ + if mu1.shape != mu2.shape or sigma1.shape != sigma2.shape: + raise ValueError( + "(mu1, sigma1) should have exactly the same shape as (mu2, sigma2)." + ) + + mu1 = np.atleast_1d(mu1) + mu2 = np.atleast_1d(mu2) + + sigma1 = np.atleast_2d(sigma1) + sigma2 = np.atleast_2d(sigma2) + + diff = mu1 - mu2 + + # Product might be almost singular + covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False) + if not np.isfinite(covmean).all(): + print( + "WARNING: fid calculation produces singular product; adding {} to diagonal of cov estimates" + .format(eps)) + + offset = np.eye(sigma1.shape[0]) * eps + covmean = linalg.sqrtm((sigma1 + offset).dot(sigma2 + offset)) + + # Numerical error might give slight imaginary component + if np.iscomplexobj(covmean): + if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3): + m = np.max(np.abs(covmean.imag)) + raise ValueError("Imaginary component {}".format(m)) + covmean = covmean.real + + tr_covmean = np.trace(covmean) + + return diff.dot(diff) + np.trace(sigma1) + np.trace( + sigma2) - 2 * tr_covmean + + +def calculate_activation_statistics(images, sess, batch_size=50, verbose=True): + """ + Calculation of the statistics used by the FID. + + Args: + images (ndarray): Numpy array of shape (N, H, W, 3) and values in + the range [0, 255]. + sess (Session): TensorFlow session object. + batch_size (int): Batch size for inference. + verbose (bool): If True, prints out logging information. + + Returns: + ndarray: Mean of inception features from samples. + ndarray: Covariance of inception features from samples. + """ + act = inception_utils.get_activations(images, sess, batch_size, verbose) + mu = np.mean(act, axis=0) + sigma = np.cov(act, rowvar=False) + + return mu, sigma diff --git a/official/vision/gan/megengine_mimicry/metrics/inception_model/__init__.py b/official/vision/gan/megengine_mimicry/metrics/inception_model/__init__.py new file mode 100644 index 0000000..60f1c3d --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/inception_model/__init__.py @@ -0,0 +1 @@ +from .inception_utils import * diff --git a/official/vision/gan/megengine_mimicry/metrics/inception_model/inception_utils.py b/official/vision/gan/megengine_mimicry/metrics/inception_model/inception_utils.py new file mode 100644 index 0000000..b353864 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/inception_model/inception_utils.py @@ -0,0 +1,159 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Common inception utils for computing metrics, as based on the FID helper code: +https://github.com/kwotsin/dissertation/blob/master/eval/TTUR/fid.py +""" +import os +import pathlib +import tarfile +import time +from urllib import request + +import numpy as np +import tensorflow as tf + + +def _check_or_download_inception(inception_path): + """ + Checks if the path to the inception file is valid, or downloads + the file if it is not present. + + Args: + inception_path (str): Directory for storing the inception model. + + Returns: + str: File path of the inception protobuf model. + + """ + # Build file path of model + inception_url = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz' + if inception_path is None: + inception_path = '/tmp' + inception_path = pathlib.Path(inception_path) + model_file = inception_path / 'classify_image_graph_def.pb' + + # Download model if required + if not model_file.exists(): + print("Downloading Inception model") + fn, _ = request.urlretrieve(inception_url) + + with tarfile.open(fn, mode='r') as f: + f.extract('classify_image_graph_def.pb', str(model_file.parent)) + + return str(model_file) + + +def _get_inception_layer(sess): + """ + Prepares inception net for batched usage and returns pool_3 layer. + + Args: + sess (Session): TensorFlow Session object. + + Returns: + TensorFlow graph node representing inception model pool3 layer output. + + """ + # Get the output node + layer_name = 'inception_model/pool_3:0' + pool3 = sess.graph.get_tensor_by_name(layer_name) + + # Reshape to be batch size agnostic + ops = pool3.graph.get_operations() + for op_idx, op in enumerate(ops): + for o in op.outputs: + shape = o.get_shape() + if len(shape._dims) > 0: + try: + shape = [s.value for s in shape] + except AttributeError: # TF 2 uses None shape directly. No conversion needed. + shape = shape + + new_shape = [] + for j, s in enumerate(shape): + if s == 1 and j == 0: + new_shape.append(None) + else: + new_shape.append(s) + o.__dict__['_shape_val'] = tf.TensorShape(new_shape) + + return pool3 + + +def get_activations(images, sess, batch_size=50, verbose=True): + """ + Calculates the activations of the pool_3 layer for all images. + + Args: + images (ndarray): Numpy array of shape (N, C, H, W) with values ranging + in the range [0, 255]. + sess (Session): TensorFlow Session object. + batch_size (int): The batch size to use for inference. + verbose (bool): If True, prints out logging data for batch inference. + + Returns: + ndarray: Numpy array of shape (N, 2048) representing the pool3 features from the + inception model. + + """ + # Get output layer. + inception_layer = _get_inception_layer(sess) + + # Inference variables + batch_size = min(batch_size, images.shape[0]) + num_batches = images.shape[0] // batch_size + + # Get features + pred_arr = np.empty((images.shape[0], 2048)) + for i in range(num_batches): + start_time = time.time() + + start = i * batch_size + end = start + batch_size + batch = images[start:end] + pred = sess.run(inception_layer, + {'inception_model/ExpandDims:0': batch}) + pred_arr[start:end] = pred.reshape(batch_size, -1) + + if verbose: + print("\rINFO: Propagated batch %d/%d (%.4f sec/batch)" \ + % (i+1, num_batches, time.time()-start_time), end="", flush=True) + + return pred_arr + + +def create_inception_graph(inception_path): + """ + Creates a graph from saved GraphDef file. + + Args: + inception_path (str): Directory for storing the inception model. + + Returns: + None + """ + if not os.path.exists(inception_path): + os.makedirs(inception_path) + + # Get inception model file path + model_file = _check_or_download_inception(inception_path) + + # Creates graph from saved graph_def.pb. + with tf.io.gfile.GFile(model_file, 'rb') as f: + graph_def = tf.compat.v1.GraphDef() + graph_def.ParseFromString(f.read()) + _ = tf.import_graph_def(graph_def, name='inception_model') diff --git a/official/vision/gan/megengine_mimicry/metrics/inception_score/__init__.py b/official/vision/gan/megengine_mimicry/metrics/inception_score/__init__.py new file mode 100644 index 0000000..8305145 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/inception_score/__init__.py @@ -0,0 +1 @@ +from .inception_score_utils import * diff --git a/official/vision/gan/megengine_mimicry/metrics/inception_score/inception_score_utils.py b/official/vision/gan/megengine_mimicry/metrics/inception_score/inception_score_utils.py new file mode 100644 index 0000000..859cad7 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/inception_score/inception_score_utils.py @@ -0,0 +1,118 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Helper functions for computing inception score, as based on: +https://github.com/openai/improved-gan/tree/master/inception_score +""" +import time + +import numpy as np +import tensorflow as tf + +from ..inception_model import inception_utils + + +def get_predictions(images, device=None, batch_size=50, print_every=20): + """ + Get the output probabilities of images. + + Args: + images (ndarray): Batch of images of shape (N, H, W, 3). + device (Device): Torch device object. + batch_size (int): Batch size for inference using inception model. + print_every (int): Prints logging variable every n batch inferences. + + Returns: + ndarray: Batch of probabilities of equal size as number of images input. + """ + if device is not None: + # Avoid unbounded memory usage + gpu_options = tf.compat.v1.GPUOptions(allow_growth=True, + per_process_gpu_memory_fraction=0.15, + visible_device_list=str(device)) + config = tf.compat.v1.ConfigProto(gpu_options=gpu_options) + + else: + config = tf.compat.v1.ConfigProto(device_count={'GPU': 0}) + + # Inference variables + batch_size = min(batch_size, images.shape[0]) + num_batches = images.shape[0] // batch_size + + # Get predictions + preds = [] + with tf.compat.v1.Session(config=config) as sess: + # Batch input preparation + inception_utils._get_inception_layer(sess) + + # Define input/outputs of default graph. + pool3 = sess.graph.get_tensor_by_name('inception_model/pool_3:0') + w = sess.graph.get_operation_by_name( + "inception_model/softmax/logits/MatMul").inputs[1] + logits = tf.matmul(tf.squeeze(pool3, [1, 2]), w) + softmax = tf.nn.softmax(logits) + + # Predict images + start_time = time.time() + for i in range(num_batches): + batch = images[i * batch_size:(i + 1) * batch_size] + + # curr_image = np.expand_dims(images[i], axis=0) + pred = sess.run(softmax, {'inception_model/ExpandDims:0': batch}) + preds.append(pred) + + if (i + 1) % min(print_every, num_batches) == 0: + end_time = time.time() + print("INFO: Processed image {}/{}...({:.4f} sec/idx)".format( + (i + 1) * batch_size, images.shape[0], + (end_time - start_time) / (print_every * batch_size))) + start_time = end_time + + preds = np.concatenate(preds, 0) + + return preds + + +def get_inception_score(images, splits=10, device=None): + """ + Computes inception score according to official OpenAI implementation. + + Args: + images (ndarray): Batch of images of shape (N, H, W, 3), which should have values + in the range [0, 255]. + splits (int): Number of splits to use for computing IS. + device (Device): Torch device object to decide which GPU to use for TF session. + + Returns: + tuple: Tuple of mean and standard deviation of the inception score computed. + """ + if np.max(images[0] < 10) and np.max(images[0] < 0): + raise ValueError("Images should have value ranging from 0 to 255.") + + # Load graph and get probabilities + preds = get_predictions(images, device=device) + + # Compute scores + N = preds.shape[0] + scores = [] + for i in range(splits): + part = preds[(i * N // splits):((i + 1) * N // splits), :] + kl = part * (np.log(part) - + np.log(np.expand_dims(np.mean(part, 0), 0))) + kl = np.mean(np.sum(kl, 1)) + scores.append(np.exp(kl)) + + return float(np.mean(scores)), float(np.std(scores)) diff --git a/official/vision/gan/megengine_mimicry/metrics/kid/__init__.py b/official/vision/gan/megengine_mimicry/metrics/kid/__init__.py new file mode 100644 index 0000000..b8ceff3 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/kid/__init__.py @@ -0,0 +1 @@ +from .kid_utils import * diff --git a/official/vision/gan/megengine_mimicry/metrics/kid/kid_utils.py b/official/vision/gan/megengine_mimicry/metrics/kid/kid_utils.py new file mode 100644 index 0000000..79f7d93 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/kid/kid_utils.py @@ -0,0 +1,153 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Helper functions for computing FID, as based on: +https://github.com/mbinkowski/MMD-GAN/blob/master/gan/compute_scores.py +""" +import numpy as np +from sklearn.metrics.pairwise import polynomial_kernel + + +def polynomial_mmd(codes_g, codes_r, degree=3, gamma=None, coef0=1): + """ + Compute MMD between two sets of features. + + Polynomial kernel given by: + K(X, Y) = (gamma + coef0)^degree + + Args: + codes_g (ndarray): Set of features from 1st distribution. + codes_r (ndarray): Set of features from 2nd distribution. + degree (int): Power of the kernel. + gamma (float): Scaling factor of dot product. + coeff0 (float): Constant factor of kernel. + + Returns: + np.float64: Scalar MMD score between features of 2 distributions. + """ + X = codes_g + Y = codes_r + + K_XX = polynomial_kernel(X, degree=degree, gamma=gamma, coef0=coef0) + K_YY = polynomial_kernel(Y, degree=degree, gamma=gamma, coef0=coef0) + K_XY = polynomial_kernel(X, Y, degree=degree, gamma=gamma, coef0=coef0) + + return _compute_mmd2(K_XX, K_XY, K_YY) + + +def polynomial_mmd_averages(codes_g, + codes_r, + n_subsets=50, + subset_size=1000, + **kernel_args): + """ + Computes average MMD between two set of features using n_subsets, + each of which is of subset_size. + + Args: + codes_g (ndarray): Set of features from 1st distribution. + codes_r (ndarray): Set of features from 2nd distribution. + n_subsets (int): Number of subsets to compute averages. + subset_size (int): Size of each subset of features to choose. + + Returns: + list: List of n_subsets MMD scores. + """ + m = min(codes_g.shape[0], codes_r.shape[0]) + mmds = np.zeros(n_subsets) + + # Account for inordinately small subset sizes + n_subsets = min(m, n_subsets) + subset_size = min(subset_size, m // n_subsets) + + for i in range(n_subsets): + g = codes_g[np.random.choice(len(codes_g), subset_size, replace=False)] + r = codes_r[np.random.choice(len(codes_r), subset_size, replace=False)] + o = polynomial_mmd(g, r, **kernel_args) + mmds[i] = o + + return mmds + + +def _sqn(arr): + flat = np.ravel(arr) + return flat.dot(flat) + + +def _compute_mmd2(K_XX, + K_XY, + K_YY, + unit_diagonal=False, + mmd_est='unbiased'): + """ + Based on https://github.com/dougalsutherland/opt-mmd/blob/master/two_sample/mmd.py + but changed to not compute the full kernel matrix at once. + """ + if mmd_est not in ['unbiased', 'u-statistic']: + raise ValueError( + "mmd_est should be one of [unbiased', 'u-statistic] but got {}.". + format(mmd_est)) + + m = K_XX.shape[0] + if K_XX.shape != (m, m): + raise ValueError("K_XX shape should be {} but got {} instead.".format( + (m, m), K_XX.shape)) + + if K_XY.shape != (m, m): + raise ValueError("K_XX shape should be {} but got {} instead.".format( + (m, m), K_XY.shape)) + + if K_YY.shape != (m, m): + raise ValueError("K_XX shape should be {} but got {} instead.".format( + (m, m), K_YY.shape)) + + # Get the various sums of kernels that we'll use + # Kts drop the diagonal, but we don't need to compute them explicitly + if unit_diagonal: + diag_X = diag_Y = 1 + sum_diag_X = sum_diag_Y = m + sum_diag2_X = sum_diag2_Y = m + else: + diag_X = np.diagonal(K_XX) + diag_Y = np.diagonal(K_YY) + + sum_diag_X = diag_X.sum() + sum_diag_Y = diag_Y.sum() + + sum_diag2_X = _sqn(diag_X) + sum_diag2_Y = _sqn(diag_Y) + + Kt_XX_sums = K_XX.sum(axis=1) - diag_X + Kt_YY_sums = K_YY.sum(axis=1) - diag_Y + K_XY_sums_0 = K_XY.sum(axis=0) + K_XY_sums_1 = K_XY.sum(axis=1) + + Kt_XX_sum = Kt_XX_sums.sum() + Kt_YY_sum = Kt_YY_sums.sum() + K_XY_sum = K_XY_sums_0.sum() + + if mmd_est == 'biased': + mmd2 = ((Kt_XX_sum + sum_diag_X) / (m * m) + (Kt_YY_sum + sum_diag_Y) / + (m * m) - 2 * K_XY_sum / (m * m)) + else: + mmd2 = (Kt_XX_sum + Kt_YY_sum) / (m * (m - 1)) + + if mmd_est == 'unbiased': + mmd2 -= 2 * K_XY_sum / (m * m) + else: + mmd2 -= 2 * (K_XY_sum - np.trace(K_XY)) / (m * (m - 1)) + + return mmd2 diff --git a/official/vision/gan/megengine_mimicry/metrics/utils.py b/official/vision/gan/megengine_mimicry/metrics/utils.py new file mode 100644 index 0000000..9f1cca0 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/metrics/utils.py @@ -0,0 +1,46 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import numpy as np + + +def _normalize_images(images): + """ + Given a tensor of (megengine BGR) images, uses the torchvision + normalization method to convert floating point data to integers. See reference + at: https://pytorch.org/docs/stable/_modules/torchvision/utils.html#save_image + + The function uses the normalization from make_grid and save_image functions. + + Args: + images (Tensor): Batch of images of shape (N, 3, H, W). + + Returns: + ndarray: Batch of normalized (0-255) RGB images of shape (N, H, W, 3). + """ + # Shift the image from [-1, 1] range to [0, 1] range. + min_val = float(images.min()) + max_val = float(images.max()) + + images = (images - min_val) / (max_val - min_val + 1e-5) + + images = np.clip(images * 255 + 0.5, 0, 255).astype("uint8") + + images = np.transpose(images, [0, 2, 3, 1]) + + # NOTE: megengine(opencv) uses BGR, while TF uses RGB. Needs conversion. + images = images[:, :, :, ::-1] + + return images diff --git a/official/vision/gan/megengine_mimicry/nets/__init__.py b/official/vision/gan/megengine_mimicry/nets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/official/vision/gan/megengine_mimicry/nets/basemodel.py b/official/vision/gan/megengine_mimicry/nets/basemodel.py new file mode 100644 index 0000000..5eedae3 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/basemodel.py @@ -0,0 +1,164 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import os +from abc import abstractmethod + +import megengine +import megengine.jit as jit +import megengine.module as M +import numpy as np + + +class BaseModel(M.Module): + def __init__(self): + super().__init__() + self.train_step = self._reset_jit_graph(self._train_step_implementation) + self.infer_step = self._reset_jit_graph(self._infer_step_implementation) + + def _reset_jit_graph(self, impl: callable): + """create a `jit.trace` object based on abstract graph implementation""" + return jit.trace(impl) + + @abstractmethod + def _train_step_implementation(self, *args, **kwargs): + """Abstract train step function, traced at the beginning of training. + + A typical implementation for a classifier could be + ``` + class Classifier(BaseModel): + + def _train_step_implementation( + self, + image: Tensor, + label: Tensor, + opt: Optimizer = None + ): + logits = self.forward(image) + loss = F.cross_entropy_with_softmax(logits, label) + if opt is not None: + opt.zero_grad() + opt.backward(loss) + opt.step() + ``` + + This implementation is wrapped in a `megengine.jit.trace` object, which equals to + something like + ``` + @jit.trace + def train_step(image, label, opt=None): + return _train_step_implemenation(image, label, opt=opt) + ``` + + And we call `model.train_step(np_image, np_label, opt=sgd_optimizer)` to + perform the wrapped training step. + """ + raise NotImplementedError + + @abstractmethod + def _infer_step_implementation(self, *args, **kwargs): + """Abstract infer step function, traced at the beginning of inference. + + See document of `_train_step_implementation`. + """ + raise NotImplementedError + + def train(self, mode: bool = True): + # when switching mode, graph should be reset + self.train_step = self._reset_jit_graph(self._train_step_implementation) + self.infer_step = self._reset_jit_graph(self._infer_step_implementation) + super().train(mode=mode) + + def count_params(self): + r""" + Computes the number of parameters in this model. + + Args: None + + Returns: + int: Total number of weight parameters for this model. + int: Total number of trainable parameters for this model. + + """ + num_total_params = sum(np.prod(p.shape) for p in self.parameters()) + num_trainable_params = sum(np.prod(p.shape) for p in self.parameters(requires_grad=True)) + + return num_total_params, num_trainable_params + + def restore_checkpoint(self, ckpt_file, optimizer=None): + r""" + Restores checkpoint from a pth file and restores optimizer state. + + Args: + ckpt_file (str): A PyTorch pth file containing model weights. + optimizer (Optimizer): A vanilla optimizer to have its state restored from. + + Returns: + int: Global step variable where the model was last checkpointed. + """ + if not ckpt_file: + raise ValueError("No checkpoint file to be restored.") + + ckpt_dict = megengine.load(ckpt_file) + + # Restore model weights + self.load_state_dict(ckpt_dict['model_state_dict']) + + # Restore optimizer status if existing. Evaluation doesn't need this + if optimizer: + optimizer.load_state_dict(ckpt_dict['optimizer_state_dict']) + + # Return global step + return ckpt_dict['global_step'] + + def save_checkpoint(self, + directory, + global_step, + optimizer=None, + name=None): + r""" + Saves checkpoint at a certain global step during training. Optimizer state + is also saved together. + + Args: + directory (str): Path to save checkpoint to. + global_step (int): The global step variable during training. + optimizer (Optimizer): Optimizer state to be saved concurrently. + name (str): The name to save the checkpoint file as. + + Returns: + None + """ + # Create directory to save to + if not os.path.exists(directory): + os.makedirs(directory) + + # Build checkpoint dict to save. + ckpt_dict = { + 'model_state_dict': + self.state_dict(), + 'optimizer_state_dict': + optimizer.state_dict() if optimizer is not None else None, + 'global_step': + global_step + } + + # Save the file with specific name + if name is None: + name = "{}_{}_steps.pth".format( + os.path.basename(directory), # netD or netG + global_step) + + megengine.save(ckpt_dict, os.path.join(directory, name)) diff --git a/official/vision/gan/megengine_mimicry/nets/blocks.py b/official/vision/gan/megengine_mimicry/nets/blocks.py new file mode 100644 index 0000000..56306e8 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/blocks.py @@ -0,0 +1,243 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import math + +import megengine.functional as F +import megengine.module as M + + +class GBlock(M.Module): + r""" + Residual block for generator. + + Uses bilinear (rather than nearest) interpolation, and align_corners + set to False. This is as per how torchvision does upsampling, as seen in: + https://github.com/pytorch/vision/blob/master/torchvision/models/segmentation/_utils.py + + Attributes: + in_channels (int): The channel size of input feature map. + out_channels (int): The channel size of output feature map. + hidden_channels (int): The channel size of intermediate feature maps. + upsample (bool): If True, upsamples the input feature map. + num_classes (int): If more than 0, uses conditional batch norm instead. + spectral_norm (bool): If True, uses spectral norm for convolutional layers. + """ + def __init__(self, + in_channels, + out_channels, + hidden_channels=None, + upsample=False): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.hidden_channels = hidden_channels if hidden_channels is not None else out_channels + self.learnable_sc = in_channels != out_channels or upsample + self.upsample = upsample + + self.c1 = M.Conv2d(self.in_channels, + self.hidden_channels, + 3, + 1, + padding=1) + self.c2 = M.Conv2d(self.hidden_channels, + self.out_channels, + 3, + 1, + padding=1) + + self.b1 = M.BatchNorm2d(self.in_channels) + self.b2 = M.BatchNorm2d(self.hidden_channels) + + self.activation = M.ReLU() + + M.init.xavier_uniform_(self.c1.weight, math.sqrt(2.0)) + M.init.xavier_uniform_(self.c2.weight, math.sqrt(2.0)) + + # Shortcut layer + if self.learnable_sc: + self.c_sc = M.Conv2d(in_channels, + out_channels, + 1, + 1, + padding=0) + M.init.xavier_uniform_(self.c_sc.weight, 1.0) + + def _upsample_conv(self, x, conv): + r""" + Helper function for performing convolution after upsampling. + """ + return conv( + F.interpolate(x, + scale_factor=2, + mode='bilinear', + align_corners=False)) + + def _residual(self, x): + r""" + Helper function for feedforwarding through main layers. + """ + h = x + h = self.b1(h) + h = self.activation(h) + h = self._upsample_conv(h, self.c1) if self.upsample else self.c1(h) + h = self.b2(h) + h = self.activation(h) + h = self.c2(h) + + return h + + def _shortcut(self, x): + r""" + Helper function for feedforwarding through shortcut layers. + """ + if self.learnable_sc: + x = self._upsample_conv( + x, self.c_sc) if self.upsample else self.c_sc(x) + return x + else: + return x + + def forward(self, x): + r""" + Residual block feedforward function. + """ + return self._residual(x) + self._shortcut(x) + + +class DBlock(M.Module): + """ + Residual block for discriminator. + + Attributes: + in_channels (int): The channel size of input feature map. + out_channels (int): The channel size of output feature map. + hidden_channels (int): The channel size of intermediate feature maps. + downsample (bool): If True, downsamples the input feature map. + spectral_norm (bool): If True, uses spectral norm for convolutional layers. + """ + def __init__(self, + in_channels, + out_channels, + hidden_channels=None, + downsample=False): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.hidden_channels = hidden_channels if hidden_channels is not None else in_channels + self.downsample = downsample + self.learnable_sc = (in_channels != out_channels) or downsample + + # Build the layers + self.c1 = M.Conv2d(self.in_channels, self.hidden_channels, 3, 1, + 1) + self.c2 = M.Conv2d(self.hidden_channels, self.out_channels, 3, 1, + 1) + + self.activation = M.ReLU() + + M.init.xavier_uniform_(self.c1.weight, math.sqrt(2.0)) + M.init.xavier_uniform_(self.c2.weight, math.sqrt(2.0)) + + # Shortcut layer + if self.learnable_sc: + self.c_sc = M.Conv2d(in_channels, out_channels, 1, 1, 0) + M.init.xavier_uniform_(self.c_sc.weight, 1.0) + + def _residual(self, x): + """ + Helper function for feedforwarding through main layers. + """ + h = x + h = self.activation(h) + h = self.c1(h) + h = self.activation(h) + h = self.c2(h) + if self.downsample: + h = F.avg_pool2d(h, 2) + + return h + + def _shortcut(self, x): + """ + Helper function for feedforwarding through shortcut layers. + """ + if self.learnable_sc: + x = self.c_sc(x) + return F.avg_pool2d(x, 2) if self.downsample else x + + else: + return x + + def forward(self, x): + """ + Residual block feedforward function. + """ + # NOTE: to completely reproduce pytorch, we use F.relu(x) to replace x in shortcut + # since pytorch use inplace relu in residual branch. + return self._residual(x) + self._shortcut(F.relu(x)) + + +class DBlockOptimized(M.Module): + """ + Optimized residual block for discriminator. This is used as the first residual block, + where there is a definite downsampling involved. Follows the official SNGAN reference implementation + in chainer. + + Attributes: + in_channels (int): The channel size of input feature map. + out_channels (int): The channel size of output feature map. + spectral_norm (bool): If True, uses spectral norm for convolutional layers. + """ + def __init__(self, in_channels, out_channels, spectral_norm=False): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.spectral_norm = spectral_norm + + # Build the layers + self.c1 = M.Conv2d(self.in_channels, self.out_channels, 3, 1, 1) + self.c2 = M.Conv2d(self.out_channels, self.out_channels, 3, 1, 1) + self.c_sc = M.Conv2d(self.in_channels, self.out_channels, 1, 1, 0) + + self.activation = M.ReLU() + + M.init.xavier_uniform_(self.c1.weight, math.sqrt(2.0)) + M.init.xavier_uniform_(self.c2.weight, math.sqrt(2.0)) + M.init.xavier_uniform_(self.c_sc.weight, 1.0) + + def _residual(self, x): + """ + Helper function for feedforwarding through main layers. + """ + h = x + h = self.c1(h) + h = self.activation(h) + h = self.c2(h) + h = F.avg_pool2d(h, 2) + + return h + + def _shortcut(self, x): + """ + Helper function for feedforwarding through shortcut layers. + """ + return self.c_sc(F.avg_pool2d(x, 2)) + + def forward(self, x): + """ + Residual block feedforward function. + """ + return self._residual(x) + self._shortcut(x) diff --git a/official/vision/gan/megengine_mimicry/nets/dcgan/__init__.py b/official/vision/gan/megengine_mimicry/nets/dcgan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_base.py b/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_base.py new file mode 100644 index 0000000..178af5c --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_base.py @@ -0,0 +1,46 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +from .. import gan + + +class DCGANBaseGenerator(gan.BaseGenerator): + r""" + ResNet backbone generator for ResNet DCGAN. + + Attributes: + nz (int): Noise dimension for upsampling. + ngf (int): Variable controlling generator feature map sizes. + bottom_width (int): Starting width for upsampling generator output to an image. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, nz, ngf, bottom_width, loss_type='ns', **kwargs): + super().__init__(nz=nz, + ngf=ngf, + bottom_width=bottom_width, + loss_type=loss_type, + **kwargs) + + +class DCGANBaseDiscriminator(gan.BaseDiscriminator): + r""" + ResNet backbone discriminator for ResNet DCGAN. + + Attributes: + ndf (int): Variable controlling discriminator feature map sizes. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, ndf, loss_type='ns', **kwargs): + super().__init__(ndf=ndf, loss_type=loss_type, **kwargs) diff --git a/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_cifar.py b/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_cifar.py new file mode 100644 index 0000000..0371897 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/dcgan/dcgan_cifar.py @@ -0,0 +1,122 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.functional as F +import megengine.module as M + +from ..blocks import DBlock, DBlockOptimized, GBlock +from . import dcgan_base + + +class DCGANGeneratorCIFAR(dcgan_base.DCGANBaseGenerator): + r""" + ResNet backbone generator for ResNet DCGAN. + + Attributes: + nz (int): Noise dimension for upsampling. + ngf (int): Variable controlling generator feature map sizes. + bottom_width (int): Starting width for upsampling generator output to an image. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, nz=128, ngf=256, bottom_width=4, **kwargs): + super().__init__(nz=nz, ngf=ngf, bottom_width=bottom_width, **kwargs) + + # Build the layers + self.l1 = M.Linear(self.nz, (self.bottom_width**2) * self.ngf) + self.block2 = GBlock(self.ngf, self.ngf, upsample=True) + self.block3 = GBlock(self.ngf, self.ngf, upsample=True) + self.block4 = GBlock(self.ngf, self.ngf, upsample=True) + self.b5 = M.BatchNorm2d(self.ngf) + self.c5 = M.Conv2d(self.ngf, 3, 3, 1, padding=1) + self.activation = M.ReLU() + + # Initialise the weights + M.init.xavier_uniform_(self.l1.weight, 1.0) + M.init.xavier_uniform_(self.c5.weight, 1.0) + + def forward(self, x): + r""" + Feedforwards a batch of noise vectors into a batch of fake images. + + Args: + x (Tensor): A batch of noise vectors of shape (N, nz). + + Returns: + Tensor: A batch of fake images of shape (N, C, H, W). + """ + h = self.l1(x) + h = h.reshape(x.shape[0], -1, self.bottom_width, self.bottom_width) + h = self.block2(h) + h = self.block3(h) + h = self.block4(h) + h = self.b5(h) + h = self.activation(h) + + h = F.sigmoid(self.c5(h)) # sigmoid instead of tanh + + return h + + +class DCGANDiscriminatorCIFAR(dcgan_base.DCGANBaseDiscriminator): + r""" + ResNet backbone discriminator for ResNet DCGAN. + + Attributes: + ndf (int): Variable controlling discriminator feature map sizes. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, ndf=128, **kwargs): + super().__init__(ndf=ndf, **kwargs) + + # Build layers + self.block1 = DBlockOptimized(3, self.ndf) + self.block2 = DBlock(self.ndf, + self.ndf, + downsample=True) + self.block3 = DBlock(self.ndf, + self.ndf, + downsample=False) + self.block4 = DBlock(self.ndf, + self.ndf, + downsample=False) + self.l5 = M.Linear(self.ndf, 1) + self.activation = M.ReLU() + + # Initialise the weights + M.init.xavier_uniform_(self.l5.weight, 1.0) + + def forward(self, x): + r""" + Feedforwards a batch of real/fake images and produces a batch of GAN logits. + + Args: + x (Tensor): A batch of images of shape (N, C, H, W). + + Returns: + Tensor: A batch of GAN logits of shape (N, 1). + """ + h = x + h = self.block1(h) + h = self.block2(h) + h = self.block3(h) + h = self.block4(h) + h = self.activation(h) + + # Global sum pooling + h = h.sum(3).sum(2) + + output = self.l5(h) + + return output diff --git a/official/vision/gan/megengine_mimicry/nets/gan.py b/official/vision/gan/megengine_mimicry/nets/gan.py new file mode 100644 index 0000000..7829a27 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/gan.py @@ -0,0 +1,178 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Implementation of Base GAN models. +""" +import megengine +import megengine.functional as F +import megengine.module as M +import megengine.random as R +import numpy as np + +from . import losses +from .basemodel import BaseModel + + +class BaseGenerator(BaseModel): + r""" + Base class for a generic unconditional generator model. + + Attributes: + nz (int): Noise dimension for upsampling. + ngf (int): Variable controlling generator feature map sizes. + bottom_width (int): Starting width for upsampling generator output to an image. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, nz, ngf, bottom_width, loss_type, **kwargs): + super().__init__(**kwargs) + self.nz = nz + self.ngf = ngf + self.bottom_width = bottom_width + self.loss_type = loss_type + + def _train_step_implementation( + self, + real_batch, + netD=None, + optG=None): + # Produce fake images + fake_images = self._infer_step_implementation(real_batch) + + # Compute output logit of D thinking image real + output = netD(fake_images) + + # Compute loss + errG = self.compute_gan_loss(output=output) + + optG.zero_grad() + optG.backward(errG) + optG.step() + return errG + + def _infer_step_implementation(self, batch): + # Get only batch size from real batch + batch_size = batch.shape[0] + + noise = R.gaussian(shape=[batch_size, self.nz]) + + fake_images = self.forward(noise) + return fake_images + + def compute_gan_loss(self, output): + if self.loss_type == "ns": + errG = losses.ns_loss_gen(output) + + elif self.loss_type == "wasserstein": + errG = losses.wasserstein_loss_gen(output) + + else: + raise ValueError("Invalid loss_type {} selected.".format( + self.loss_type)) + + return errG + + def generate_images(self, num_images): + """Generate images of shape [`num_images`, C, H, W]. + + Depending on the final activation function, pixel values are NOT guarenteed + to be within [0, 1]. + """ + return self.infer_step(np.empty(num_images, dtype="float32")) + + +class BaseDiscriminator(BaseModel): + r""" + Base class for a generic unconditional discriminator model. + + Attributes: + ndf (int): Variable controlling discriminator feature map sizes. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, ndf, loss_type, **kwargs): + super().__init__(**kwargs) + self.ndf = ndf + self.loss_type = loss_type + + def _train_step_implementation( + self, + real_batch, + netG=None, + optD=None): + # Produce logits for real images + output_real = self._infer_step_implementation(real_batch) + + # Produce fake images + fake_images = netG._infer_step_implementation(real_batch) + fake_images = F.zero_grad(fake_images) + + # Produce logits for fake images + output_fake = self._infer_step_implementation(fake_images) + + # Compute loss for D + errD = self.compute_gan_loss(output_real=output_real, + output_fake=output_fake) + D_x, D_Gz = self.compute_probs(output_real=output_real, + output_fake=output_fake) + + # Backprop and update gradients + optD.zero_grad() + optD.backward(errD) + optD.step() + return errD, D_x, D_Gz + + def _infer_step_implementation(self, batch): + return self.forward(batch) + + def compute_gan_loss(self, output_real, output_fake): + r""" + Computes GAN loss for discriminator. + + Args: + output_real (Tensor): A batch of output logits of shape (N, 1) from real images. + output_fake (Tensor): A batch of output logits of shape (N, 1) from fake images. + + Returns: + errD (Tensor): A batch of GAN losses for the discriminator. + """ + # Compute loss for D + if self.loss_type == "gan" or self.loss_type == "ns": + errD = losses.minimax_loss_dis(output_fake=output_fake, + output_real=output_real) + + elif self.loss_type == "wasserstein": + errD = losses.wasserstein_loss_dis(output_fake=output_fake, + output_real=output_real) + + else: + raise ValueError("Invalid loss_type selected.") + + return errD + + def compute_probs(self, output_real, output_fake): + r""" + Computes probabilities from real/fake images logits. + + Args: + output_real (Tensor): A batch of output logits of shape (N, 1) from real images. + output_fake (Tensor): A batch of output logits of shape (N, 1) from fake images. + + Returns: + tuple: Average probabilities of real/fake image considered as real for the batch. + """ + D_x = F.sigmoid(output_real).mean() + D_Gz = F.sigmoid(output_fake).mean() + + return D_x, D_Gz diff --git a/official/vision/gan/megengine_mimicry/nets/losses.py b/official/vision/gan/megengine_mimicry/nets/losses.py new file mode 100644 index 0000000..8b0d1fd --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/losses.py @@ -0,0 +1,114 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.functional as F +from megengine.core.tensor_factory import zeros + + +def ns_loss_gen(output_fake): + r""" + Non-saturating loss for generator. + + Args: + output_fake (Tensor): Discriminator output logits for fake images. + + Returns: + Tensor: A scalar tensor loss output. + """ + output_fake = F.sigmoid(output_fake) + + return -F.log(output_fake + 1e-8).mean() + + +# def ns_loss_gen(output_fake): +# """numerical stable version""" +# return F.log(1 + F.exp(-output_fake)).mean() + + +def _bce_loss_with_logits(output, labels, **kwargs): + r""" + Sigmoid cross entropy with logits, see tensorflow + https://www.tensorflow.org/api_docs/python/tf/nn/sigmoid_cross_entropy_with_logits + """ + loss = F.maximum(output, 0) - output * labels + F.log(1 + F.exp(-F.abs(output))) + return loss.mean() + + +def minimax_loss_dis(output_fake, + output_real, + real_label_val=1.0, + fake_label_val=0.0, + **kwargs): + r""" + Standard minimax loss for GANs through the BCE Loss with logits fn. + + Args: + output_fake (Tensor): Discriminator output logits for fake images. + output_real (Tensor): Discriminator output logits for real images. + real_label_val (int): Label for real images. + fake_label_val (int): Label for fake images. + device (torch.device): Torch device object for sending created data. + + Returns: + Tensor: A scalar tensor loss output. + """ + # Produce real and fake labels. + fake_labels = zeros((output_fake.shape[0], 1)) + fake_label_val + real_labels = zeros((output_real.shape[0], 1)) + real_label_val + + # FF, compute loss and backprop D + errD_fake = _bce_loss_with_logits(output=output_fake, + labels=fake_labels, + **kwargs) + + errD_real = _bce_loss_with_logits(output=output_real, + labels=real_labels, + **kwargs) + + # Compute cumulative error + loss = errD_real + errD_fake + + return loss + + +def wasserstein_loss_gen(output_fake): + r""" + Computes the wasserstein loss for generator. + + Args: + output_fake (Tensor): Discriminator output logits for fake images. + + Returns: + Tensor: A scalar tensor loss output. + """ + loss = -output_fake.mean() + + return loss + + +def wasserstein_loss_dis(output_real, output_fake): + r""" + Computes the wasserstein loss for the discriminator. + + Args: + output_real (Tensor): Discriminator output logits for real images. + output_fake (Tensor): Discriminator output logits for fake images. + + Returns: + Tensor: A scalar tensor loss output. + """ + loss = -1.0 * output_real.mean() + output_fake.mean() + + return loss diff --git a/official/vision/gan/megengine_mimicry/nets/wgan/__init__.py b/official/vision/gan/megengine_mimicry/nets/wgan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/official/vision/gan/megengine_mimicry/nets/wgan/wgan_base.py b/official/vision/gan/megengine_mimicry/nets/wgan/wgan_base.py new file mode 100644 index 0000000..a36123c --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/wgan/wgan_base.py @@ -0,0 +1,94 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.functional as F +import megengine.jit as jit + +from .. import gan +from ..blocks import DBlock, DBlockOptimized + + +class WGANBaseGenerator(gan.BaseGenerator): + r""" + ResNet backbone generator for ResNet WGAN. + + Attributes: + nz (int): Noise dimension for upsampling. + ngf (int): Variable controlling generator feature map sizes. + bottom_width (int): Starting width for upsampling generator output to an image. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, nz, ngf, bottom_width, **kwargs): + super().__init__(nz=nz, + ngf=ngf, + bottom_width=bottom_width, + loss_type="wasserstein", + **kwargs) + + +class WGANBaseDiscriminator(gan.BaseDiscriminator): + r""" + ResNet backbone discriminator for ResNet WGAN. + + Attributes: + ndf (int): Variable controlling discriminator feature map sizes. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, ndf, **kwargs): + super().__init__(ndf=ndf, loss_type="wasserstein", **kwargs) + + def _reset_jit_graph(self, impl: callable): + """We override this func to attach weight clipping after default training step""" + traced_obj = jit.trace(impl) + def _(*args, **kwargs): + ret = traced_obj(*args, **kwargs) + if self.training: + self._apply_lipshitz_constraint() # dynamically apply weight clipping + return ret + return _ + + def _apply_lipshitz_constraint(self): + """Weight clipping described in [Wasserstein GAN](https://arxiv.org/abs/1701.07875)""" + for p in self.parameters(): + F.add_update(p, F.clamp(p, lower=-3e-2, upper=3e-2), alpha=0) + + +def layernorm(x): + original_shape = x.shape + x = x.reshape(original_shape[0], -1) + m = F.mean(x, axis=1, keepdims=True) + v = F.mean((x - m) ** 2, axis=1, keepdims=True) + x = (x - m) / F.maximum(F.sqrt(v), 1e-6) + x = x.reshape(original_shape) + return x + + +class WGANDBlockWithLayerNorm(DBlock): + def _residual(self, x): + h = x + h = layernorm(h) + h = self.activation(h) + h = self.c1(h) + h = layernorm(h) + h = self.activation(h) + h = self.c2(h) + if self.downsample: + h = F.avg_pool2d(h, 2) + + return h + + +class WGANDBlockOptimized(DBlockOptimized): + pass diff --git a/official/vision/gan/megengine_mimicry/nets/wgan/wgan_cifar.py b/official/vision/gan/megengine_mimicry/nets/wgan/wgan_cifar.py new file mode 100644 index 0000000..affdf50 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/nets/wgan/wgan_cifar.py @@ -0,0 +1,123 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.functional as F +import megengine.module as M + +from ..blocks import GBlock +from . import wgan_base +from .wgan_base import WGANDBlockOptimized as DBlockOptimized +from .wgan_base import WGANDBlockWithLayerNorm as DBlock + + +class WGANGeneratorCIFAR(wgan_base.WGANBaseGenerator): + r""" + ResNet backbone generator for ResNet WGAN. + + Attributes: + nz (int): Noise dimension for upsampling. + ngf (int): Variable controlling generator feature map sizes. + bottom_width (int): Starting width for upsampling generator output to an image. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, nz=128, ngf=256, bottom_width=4, **kwargs): + super().__init__(nz=nz, ngf=ngf, bottom_width=bottom_width, **kwargs) + + # Build the layers + self.l1 = M.Linear(self.nz, (self.bottom_width**2) * self.ngf) + self.block2 = GBlock(self.ngf, self.ngf, upsample=True) + self.block3 = GBlock(self.ngf, self.ngf, upsample=True) + self.block4 = GBlock(self.ngf, self.ngf, upsample=True) + self.b5 = M.BatchNorm2d(self.ngf) + self.c5 = M.Conv2d(self.ngf, 3, 3, 1, padding=1) + self.activation = M.ReLU() + + # Initialise the weights + M.init.xavier_uniform_(self.l1.weight, 1.0) + M.init.xavier_uniform_(self.c5.weight, 1.0) + + def forward(self, x): + r""" + Feedforwards a batch of noise vectors into a batch of fake images. + + Args: + x (Tensor): A batch of noise vectors of shape (N, nz). + + Returns: + Tensor: A batch of fake images of shape (N, C, H, W). + """ + h = self.l1(x) + h = h.reshape(x.shape[0], -1, self.bottom_width, self.bottom_width) + h = self.block2(h) + h = self.block3(h) + h = self.block4(h) + h = self.b5(h) + h = self.activation(h) + h = F.tanh(self.c5(h)) + + return h + + +class WGANDiscriminatorCIFAR(wgan_base.WGANBaseDiscriminator): + r""" + ResNet backbone discriminator for ResNet WGAN. + + Attributes: + ndf (int): Variable controlling discriminator feature map sizes. + loss_type (str): Name of loss to use for GAN loss. + """ + def __init__(self, ndf=128, **kwargs): + super().__init__(ndf=ndf, **kwargs) + + # Build layers + self.block1 = DBlockOptimized(3, self.ndf) + self.block2 = DBlock(self.ndf, + self.ndf, + downsample=True) + self.block3 = DBlock(self.ndf, + self.ndf, + downsample=False) + self.block4 = DBlock(self.ndf, + self.ndf, + downsample=False) + self.l5 = M.Linear(self.ndf, 1) + self.activation = M.ReLU() + + # Initialise the weights + M.init.xavier_uniform_(self.l5.weight, 1.0) + + def forward(self, x): + r""" + Feedforwards a batch of real/fake images and produces a batch of GAN logits. + + Args: + x (Tensor): A batch of images of shape (N, C, H, W). + + Returns: + Tensor: A batch of GAN logits of shape (N, 1). + """ + h = x + h = self.block1(h) + h = self.block2(h) + h = self.block3(h) + h = self.block4(h) + h = self.activation(h) + + # Global average pooling + h = h.mean(3).mean(2) + + output = self.l5(h) + + return output diff --git a/official/vision/gan/megengine_mimicry/training/__init__.py b/official/vision/gan/megengine_mimicry/training/__init__.py new file mode 100644 index 0000000..cfd08ea --- /dev/null +++ b/official/vision/gan/megengine_mimicry/training/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +from .trainer import Trainer diff --git a/official/vision/gan/megengine_mimicry/training/logger.py b/official/vision/gan/megengine_mimicry/training/logger.py new file mode 100644 index 0000000..870787b --- /dev/null +++ b/official/vision/gan/megengine_mimicry/training/logger.py @@ -0,0 +1,216 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Implementation of the Logger object for performing training logging and visualisation. +""" +import os + +import numpy as np +from tensorboardX import SummaryWriter + +from ..utils import vis as vutils + + +class Logger: + """ + Writes summaries and visualises training progress. + + Attributes: + log_dir (str): The path to store logging information. + num_steps (int): Total number of training iterations. + dataset_size (int): The number of examples in the dataset. + device (Device): Torch device object to send data to. + flush_secs (int): Number of seconds before flushing summaries to disk. + writers (dict): A dictionary of tensorboard writers with keys as metric names. + num_epochs (int): The number of epochs, for extra information. + """ + def __init__(self, + log_dir, + num_steps, + dataset_size, + flush_secs=120, + **kwargs): + self.log_dir = log_dir + self.num_steps = num_steps + self.dataset_size = dataset_size + self.flush_secs = flush_secs + # self.num_epochs = self._get_epoch(num_steps) + self.writers = {} + + # Create log directory if haven't already + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) + + # def _get_epoch(self, steps): + # """ + # Helper function for getting epoch. + # """ + # return max(int(steps / self.dataset_size), 1) + + def _build_writer(self, metric): + writer = SummaryWriter(log_dir=os.path.join(self.log_dir, 'data', + metric), + flush_secs=self.flush_secs) + + return writer + + def write_summaries(self, log_data, global_step): + """ + Tasks appropriate writers to write the summaries in tensorboard. Creates additional + writers for summary writing if there are new scalars to log in log_data. + + Args: + log_data (MetricLog): Dict-like object to collect log data for TB writing. + global_step (int): Global step variable for syncing logs. + + Returns: + None + """ + for metric, data in log_data.items(): + if metric not in self.writers: + self.writers[metric] = self._build_writer(metric) + + # Write with a group name if it exists + name = log_data.get_group_name(metric) or metric + self.writers[metric].add_scalar(name, + log_data[metric], + global_step=global_step) + + def close_writers(self): + """ + Closes all writers. + """ + for metric in self.writers: + self.writers[metric].close() + + def print_log(self, global_step, log_data, time_taken): + """ + Formats the string to print to stdout based on training information. + + Args: + log_data (MetricLog): Dict-like object to collect log data for TB writing. + global_step (int): Global step variable for syncing logs. + time_taken (float): Time taken for one training iteration. + + Returns: + str: String to be printed to stdout. + """ + # Basic information + # log_to_show = [ + # "INFO: [Epoch {:d}/{:d}][Global Step: {:d}/{:d}]".format( + # self._get_epoch(global_step), self.num_epochs, global_step, + # self.num_steps) + # ] + log_to_show = [ + "INFO: [Global Step: {:d}/{:d}]".format( + global_step, self.num_steps) + ] + + # Display GAN information as fed from user. + GAN_info = [""] + metrics = sorted(log_data.keys()) + + for metric in metrics: + GAN_info.append('{}: {}'.format(metric, log_data[metric])) + + # Add train step time information + GAN_info.append("({:.4f} sec/idx)".format(time_taken)) + + # Accumulate to log + log_to_show.append("\n| ".join(GAN_info)) + + # Finally print the output + ret = " ".join(log_to_show) + print(ret) + + return ret + + # def _get_fixed_noise(self, nz, num_images, output_dir=None): + # """ + # Produce the fixed gaussian noise vectors used across all models + # for consistency. + # """ + # if output_dir is None: + # output_dir = os.path.join(self.log_dir, 'viz') + + # if not os.path.exists(output_dir): + # os.makedirs(output_dir) + # output_file = os.path.join(output_dir, + # 'fixed_noise_nz_{}.pth'.format(nz)) + + # if os.path.exists(output_file): + # noise = torch.load(output_file) + + # else: + # noise = torch.randn((num_images, nz)) + # torch.save(noise, output_file) + + # return noise.to(self.device) + + # def _get_fixed_labels(self, num_images, num_classes): + # """ + # Produces fixed class labels for generating fixed images. + # """ + # labels = np.array([i % num_classes for i in range(num_images)]) + # labels = torch.from_numpy(labels).to(self.device) + + # return labels + + def vis_images(self, netG, global_step, num_images=64): + """ + Produce visualisations of the G(z), one fixed and one random. + + Args: + netG (Module): Generator model object for producing images. + global_step (int): Global step variable for syncing logs. + num_images (int): The number of images to visualise. + + Returns: + None + """ + img_dir = os.path.join(self.log_dir, 'images') + if not os.path.exists(img_dir): + os.makedirs(img_dir) + + # Generate random images + fake_images = netG.generate_images(num_images=num_images) + + # Generate fixed random images + # fixed_noise = self._get_fixed_noise(nz=netG.nz, + # num_images=num_images) + + # if hasattr(netG, 'num_classes') and netG.num_classes > 0: + # fixed_labels = self._get_fixed_labels(num_images, + # netG.num_classes) + # fixed_fake_images = netG(fixed_noise, + # fixed_labels).detach().cpu() + # else: + # fixed_fake_images = netG(fixed_noise).detach().cpu() + + # Map name to results + images_dict = { + 'fake': fake_images + } + + # Visualise all results + for name, images in images_dict.items(): + images_viz = vutils.make_grid(images, + padding=2, + normalize=True) + + vutils.save_image(images_viz, + '{}/{}_samples_step_{}.png'.format( + img_dir, name, global_step)) diff --git a/official/vision/gan/megengine_mimicry/training/metric_log.py b/official/vision/gan/megengine_mimicry/training/metric_log.py new file mode 100644 index 0000000..a76a9b8 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/training/metric_log.py @@ -0,0 +1,85 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +MetricLog object for intelligently logging data to display them more intuitively. +""" + + +class MetricLog: + """ + A dictionary-like object that logs data, and includes an extra dict to map the metrics + to its group name, if any, and the corresponding precision to print out. + + Attributes: + metrics_dict (dict): A dictionary mapping to another dict containing + the corresponding value, precision, and the group this metric belongs to. + """ + def __init__(self, **kwargs): + self.metrics_dict = {} + + def add_metric(self, name, value, group=None, precision=4): + """ + Logs metric to internal dict, but with an additional option + of grouping certain metrics together. + + Args: + name (str): Name of metric to log. + value (Tensor/Float): Value of the metric to log. + group (str): Name of the group to classify different metrics together. + precision (int): The number of floating point precision to represent the value. + + Returns: + None + """ + # Grab tensor values only + try: + value = value.item() + except AttributeError: + value = value + + self.metrics_dict[name] = dict(value=value, + group=group, + precision=precision) + + def __getitem__(self, key): + return round(self.metrics_dict[key]['value'], + self.metrics_dict[key]['precision']) + + def get_group_name(self, name): + """ + Obtains the group name of a particular metric. For example, errD and errG + which represents the discriminator/generator losses could fall under a + group name called "loss". + + Args: + name (str): The name of the metric to retrieve group name. + + Returns: + str: A string representing the group name of the metric. + """ + return self.metrics_dict[name]['group'] + + def keys(self): + """ + Dict like functionality for retrieving keys. + """ + return self.metrics_dict.keys() + + def items(self): + """ + Dict like functionality for retrieving items. + """ + return self.metrics_dict.items() diff --git a/official/vision/gan/megengine_mimicry/training/scheduler.py b/official/vision/gan/megengine_mimicry/training/scheduler.py new file mode 100644 index 0000000..67b0f98 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/training/scheduler.py @@ -0,0 +1,127 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ + +""" +Implementation of a specific learning rate scheduler for GANs. +""" + + +class LRScheduler: + """ + Learning rate scheduler for training GANs. Supports GAN specific LR scheduling + policies, such as the linear decay policy using in SN-GAN paper as based on the + original chainer implementation. However, one could safely ignore this class + and instead use the official PyTorch scheduler wrappers around a optimizer + for other scheduling policies. + + Attributes: + lr_decay (str): The learning rate decay policy to use. + optD (Optimizer): Torch optimizer object for discriminator. + optG (Optimizer): Torch optimizer object for generator. + num_steps (int): The number of training iterations. + lr_D (float): The initial learning rate of optD. + lr_G (float): The initial learning rate of optG. + """ + def __init__(self, lr_decay, optD, optG, num_steps, **kwargs): + if lr_decay not in [None, 'None', 'linear']: + raise NotImplementedError( + "lr_decay {} is not currently supported.") + + self.lr_decay = lr_decay + self.optD = optD + self.optG = optG + self.num_steps = num_steps + + # Cache the initial learning rate for uses later + self.lr_D = optD.param_groups[0]['lr'] + self.lr_G = optG.param_groups[0]['lr'] + + def linear_decay(self, optimizer, global_step, lr_value_range, + lr_step_range): + """ + Performs linear decay of the optimizer learning rate based on the number of global + steps taken. Follows SNGAN's chainer implementation of linear decay, as seen in the + chainer references: + https://docs.chainer.org/en/stable/reference/generated/chainer.training.extensions.LinearShift.html + https://github.com/chainer/chainer/blob/v6.2.0/chainer/training/extensions/linear_shift.py#L66 + + Note: assumes that the optimizer has only one parameter group to update! + + Args: + optimizer (Optimizer): Torch optimizer object to update learning rate. + global_step (int): The current global step of the training. + lr_value_range (tuple): A tuple of floats (x,y) to decrease from x to y. + lr_step_range (tuple): A tuple of ints (i, j) to start decreasing + when global_step > i, and until j. + + Returns: + float: Float representing the new updated learning rate. + """ + # Compute the new learning rate + v1, v2 = lr_value_range + s1, s2 = lr_step_range + + if global_step <= s1: + updated_lr = v1 + + elif global_step >= s2: + updated_lr = v2 + + else: + scale_factor = (global_step - s1) / (s2 - s1) + updated_lr = v1 + scale_factor * (v2 - v1) + + # Update the learning rate + optimizer.param_groups[0]['lr'] = updated_lr + + return updated_lr + + def step(self, log_data, global_step): + """ + Takes a step for updating learning rate and updates the input log_data + with the current status. + + Args: + log_data (MetricLog): Object for logging the updated learning rate metric. + global_step (int): The current global step of the training. + + Returns: + MetricLog: MetricLog object containing the updated learning rate at the current global step. + """ + if self.lr_decay == "linear": + lr_D = self.linear_decay(optimizer=self.optD, + global_step=global_step, + lr_value_range=(self.lr_D, 0.0), + lr_step_range=(0, self.num_steps)) + + lr_G = self.linear_decay(optimizer=self.optG, + global_step=global_step, + lr_value_range=(self.lr_G, 0.0), + lr_step_range=(0, self.num_steps)) + + elif self.lr_decay in [None, "None"]: + lr_D = self.lr_D + lr_G = self.lr_G + + else: + raise ValueError("Invalid lr_decay method {} selected.".format( + self.lr_decay)) + + # Update metrics log + log_data.add_metric('lr_D', lr_D, group='lr', precision=6) + log_data.add_metric('lr_G', lr_G, group='lr', precision=6) + + return log_data diff --git a/official/vision/gan/megengine_mimicry/training/trainer.py b/official/vision/gan/megengine_mimicry/training/trainer.py new file mode 100644 index 0000000..3349406 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/training/trainer.py @@ -0,0 +1,330 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Implementation of Trainer object for training GANs. +""" +import os +import re +import time + +import megengine + +from ..utils import common +from . import logger, metric_log, scheduler + + +class Trainer: + """ + Trainer object for constructing the GAN training pipeline. + + Attributes: + netD (Module): Torch discriminator model. + netG (Module): Torch generator model. + optD (Optimizer): Torch optimizer object for discriminator. + optG (Optimizer): Torch optimizer object for generator. + dataloader (DataLoader): Torch object for loading data from a dataset object. + num_steps (int): The number of training iterations. + n_dis (int): Number of discriminator update steps per generator training step. + lr_decay (str): The learning rate decay policy to use. + log_dir (str): The path to storing logging information and checkpoints. + logger (Logger): Logger object for visualising training information. + scheduler (LRScheduler): GAN training specific learning rate scheduler object. + params (dict): Dictionary of training hyperparameters. + netD_ckpt_file (str): Custom checkpoint file to restore discriminator from. + netG_ckpt_file (str): Custom checkpoint file to restore generator from. + print_steps (int): Number of training steps before printing training info to stdout. + vis_steps (int): Number of training steps before visualising images with TensorBoard. + flush_secs (int): Number of seconds before flushing summaries to disk. + log_steps (int): Number of training steps before writing summaries to TensorBoard. + save_steps (int): Number of training steps bfeore checkpointing. + save_when_end (bool): If True, saves final checkpoint when training concludes. + """ + def __init__(self, + netD, + netG, + optD, + optG, + dataloader, + num_steps, + log_dir='./log', + n_dis=1, + netG_ckpt_file=None, + netD_ckpt_file=None, + lr_decay=None, + **kwargs): + self.netD = netD + self.netG = netG + self.optD = optD + self.optG = optG + self.n_dis = n_dis + self.lr_decay = lr_decay + self.dataloader = dataloader + self.num_steps = num_steps + + self.log_dir = log_dir + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) + + # Obtain custom or latest checkpoint files + if netG_ckpt_file: + self.netG_ckpt_dir = os.path.dirname(netG_ckpt_file) + self.netG_ckpt_file = netG_ckpt_file + else: + self.netG_ckpt_dir = os.path.join(self.log_dir, 'checkpoints', + 'netG') + self.netG_ckpt_file = self._get_latest_checkpoint( + self.netG_ckpt_dir) # can be None + + if netD_ckpt_file: + self.netD_ckpt_dir = os.path.dirname(netD_ckpt_file) + self.netD_ckpt_file = netD_ckpt_file + else: + self.netD_ckpt_dir = os.path.join(self.log_dir, 'checkpoints', + 'netD') + self.netD_ckpt_file = self._get_latest_checkpoint( + self.netD_ckpt_dir) # can be None + + # Default parameters, unless provided by kwargs + default_params = { + 'print_steps': kwargs.get('print_steps', 1), + 'vis_steps': kwargs.get('vis_steps', 500), + 'flush_secs': kwargs.get('flush_secs', 30), + 'log_steps': kwargs.get('log_steps', 50), + 'save_steps': kwargs.get('save_steps', 5000), + 'save_when_end': kwargs.get('save_when_end', True), + } + for param in default_params: + self.__dict__[param] = default_params[param] + + # Hyperparameters for logging experiments + self.params = { + 'log_dir': self.log_dir, + 'num_steps': self.num_steps, + # 'batch_size': self.dataloader.sampler.batch_size, + 'n_dis': self.n_dis, + 'lr_decay': self.lr_decay, + # 'optD': optD.__repr__(), + # 'optG': optG.__repr__(), + } + self.params.update(default_params) + + # Log training hyperparmaeters + self._log_params(self.params) + + # Training helper objects + self.logger = logger.Logger(log_dir=self.log_dir, + num_steps=self.num_steps, + dataset_size=len(self.dataloader.dataset), + flush_secs=self.flush_secs) + + self.scheduler = scheduler.LRScheduler(lr_decay=self.lr_decay, + optD=self.optD, + optG=self.optG, + num_steps=self.num_steps) + + def _log_params(self, params): + """ + Takes the argument options to save into a json file. + """ + params_file = os.path.join(self.log_dir, 'params.json') + + # Check for discrepancy with previous training config. + if os.path.exists(params_file): + check = common.load_from_json(params_file) + + if params != check: + diffs = [] + for k in params: + if k in check and params[k] != check[k]: + diffs.append('{}: Expected {} but got {}.'.format( + k, check[k], params[k])) + + diff_string = '\n'.join(diffs) + raise ValueError( + "Current hyperparameter configuration is different from previously:\n{}" + .format(diff_string)) + + common.write_to_json(params, params_file) + + def _get_latest_checkpoint(self, ckpt_dir): + """ + Given a checkpoint dir, finds the checkpoint with the latest training step. + """ + def _get_step_number(k): + """ + Helper function to get step number from checkpoint files. + """ + search = re.search(r'(\d+)_steps', k) + + if search: + return int(search.groups()[0]) + else: + return -float('inf') + + if not os.path.exists(ckpt_dir): + return None + + files = os.listdir(ckpt_dir) + if len(files) == 0: + return None + + ckpt_file = max(files, key=lambda x: _get_step_number(x)) + + return os.path.join(ckpt_dir, ckpt_file) + + def _fetch_data(self, iter_dataloader): + """ + Fetches the next set of data and refresh the iterator when it is exhausted. + Follows python EAFP, so no iterator.hasNext() is used. + """ + real_batch = next(iter_dataloader) + + if isinstance(real_batch, (tuple, list)): # (image, label) + real_batch = real_batch[0] + + return iter_dataloader, real_batch + + def _restore_models_and_step(self): + """ + Restores model and optimizer checkpoints and ensures global step is in sync. + """ + global_step_D = global_step_G = 0 + + if self.netD_ckpt_file and os.path.exists(self.netD_ckpt_file): + print("INFO: Restoring checkpoint for D...") + global_step_D = self.netD.restore_checkpoint( + ckpt_file=self.netD_ckpt_file, optimizer=self.optD) + + if self.netG_ckpt_file and os.path.exists(self.netG_ckpt_file): + print("INFO: Restoring checkpoint for G...") + global_step_G = self.netG.restore_checkpoint( + ckpt_file=self.netG_ckpt_file, optimizer=self.optG) + + if global_step_G != global_step_D: + raise ValueError('G and D Networks are out of sync.') + else: + global_step = global_step_G # Restores global step + + return global_step + + def train(self): + """ + Runs the training pipeline with all given parameters in Trainer. + """ + # Restore models + global_step = self._restore_models_and_step() + print("INFO: Starting training from global step {}...".format( + global_step)) + + try: + start_time = time.time() + + # Iterate through data + iter_dataloader = iter(self.dataloader) + while global_step < self.num_steps: + log_data = metric_log.MetricLog() # log data for tensorboard + + # ------------------------- + # One Training Step + # ------------------------- + # Update n_dis times for D + for i in range(self.n_dis): + iter_dataloader, real_batch = self._fetch_data( + iter_dataloader=iter_dataloader) + + # ----------------------- + # Update G Network + # ----------------------- + # Update G, but only once. + if i == 0: + errG = self.netG.train_step( + real_batch, + netD=self.netD, + optG=self.optG) + log_data.add_metric("errG", errG.item(), group="loss") + + # ------------------------ + # Update D Network + # ----------------------- + errD, D_x, D_Gz = self.netD.train_step(real_batch, + netG=self.netG, + optD=self.optD) + log_data.add_metric("errD", errD.item(), group="loss") + log_data.add_metric("D_x", D_x.item(), group="prob") + log_data.add_metric("D_Gz", D_Gz.item(), group="prob") + + # -------------------------------- + # Update Training Variables + # ------------------------------- + global_step += 1 + + log_data = self.scheduler.step(log_data=log_data, + global_step=global_step) + + # ------------------------- + # Logging and Metrics + # ------------------------- + if global_step % self.log_steps == 0: + self.logger.write_summaries(log_data=log_data, + global_step=global_step) + + if global_step % self.print_steps == 0: + curr_time = time.time() + self.logger.print_log(global_step=global_step, + log_data=log_data, + time_taken=(curr_time - start_time) / + self.print_steps) + start_time = curr_time + + if global_step % self.vis_steps == 0: + self.logger.vis_images(netG=self.netG, + global_step=global_step) + + if global_step % self.save_steps == 0: + print("INFO: Saving checkpoints...") + self.netG.save_checkpoint(directory=self.netG_ckpt_dir, + global_step=global_step, + optimizer=self.optG) + + self.netD.save_checkpoint(directory=self.netD_ckpt_dir, + global_step=global_step, + optimizer=self.optD) + + # Save models at the very end of training + if self.save_when_end: + print("INFO: Saving final checkpoints...") + self.netG.save_checkpoint(directory=self.netG_ckpt_dir, + global_step=global_step, + optimizer=self.optG) + + self.netD.save_checkpoint(directory=self.netD_ckpt_dir, + global_step=global_step, + optimizer=self.optD) + + except KeyboardInterrupt: + print("INFO: Saving checkpoints from keyboard interrupt...") + self.netG.save_checkpoint(directory=self.netG_ckpt_dir, + global_step=global_step, + optimizer=self.optG) + + self.netD.save_checkpoint(directory=self.netD_ckpt_dir, + global_step=global_step, + optimizer=self.optD) + + finally: + self.logger.close_writers() + + print("INFO: Training Ended.") diff --git a/official/vision/gan/megengine_mimicry/utils/__init__.py b/official/vision/gan/megengine_mimicry/utils/__init__.py new file mode 100644 index 0000000..207af18 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/utils/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +from .common import * diff --git a/official/vision/gan/megengine_mimicry/utils/common.py b/official/vision/gan/megengine_mimicry/utils/common.py new file mode 100755 index 0000000..b35182a --- /dev/null +++ b/official/vision/gan/megengine_mimicry/utils/common.py @@ -0,0 +1,51 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +""" +Script for common utility functions. +""" +import json +import os + +import numpy as np + + +def write_to_json(dict_to_write, output_file): + """ + Outputs a given dictionary as a JSON file with indents. + + Args: + dict_to_write (dict): Input dictionary to output. + output_file (str): File path to write the dictionary. + + Returns: + None + """ + with open(output_file, 'w') as file: + json.dump(dict_to_write, file, indent=4) + + +def load_from_json(json_file): + """ + Loads a JSON file as a dictionary and return it. + + Args: + json_file (str): Input JSON file to read. + + Returns: + dict: Dictionary loaded from the JSON file. + """ + with open(json_file, 'r') as file: + return json.load(file) diff --git a/official/vision/gan/megengine_mimicry/utils/vis.py b/official/vision/gan/megengine_mimicry/utils/vis.py new file mode 100644 index 0000000..b58a2b0 --- /dev/null +++ b/official/vision/gan/megengine_mimicry/utils/vis.py @@ -0,0 +1,59 @@ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +import math + +import cv2 +import megengine + + +def normalize_image(tensor: megengine.Tensor, scale=255): + """normalize image tensors of any range to [0, scale=255]""" + mi = tensor.min() + ma = tensor.max() + tensor = scale * (tensor - mi) / (ma - mi + 1e-9) + return tensor + + +def make_grid( + tensor: megengine.Tensor, # [N,C,H,W] + nrow: int = 8, + padding: int = 2, + background: float = 0, + normalize: bool = False, +) -> megengine.Tensor: + """align [N, C, H, W] image tensor to [H, W, 3] image grids, for visualization""" + if normalize: + tensor = normalize_image(tensor, scale=255) # normalize to 0-255 scale + + c = tensor.shape[1] + assert c in (1, 3), "only support color/grayscale images, got channel = {}".format(c) + nmaps = tensor.shape[0] + xmaps = min(nrow, nmaps) + ymaps = int(math.ceil(float(nmaps) / xmaps)) + height, width = int(tensor.shape[2] + padding), int(tensor.shape[3] + padding) + num_channels = tensor.shape[1] + grid = megengine.ones((num_channels, height * ymaps + padding, width * xmaps + padding), "float32") * background + k = 0 + for y in range(ymaps): + for x in range(xmaps): + if k >= nmaps: + break + grid = grid.set_subtensor(tensor[k])[:, + y * height + padding: (y + 1) * height, + x * width + padding: (x + 1) * width] + k = k + 1 + c, h, w = grid.shape + grid = grid.dimshuffle(1, 2, 0) # [C,H,W] -> [H,W,C] + grid = grid.broadcast(h, w, 3) # [H,W,C] -> [H,W,3] + return grid + + +def save_image(image, path): + if isinstance(image, megengine.Tensor): + image = image.numpy() + cv2.imwrite(path, image) diff --git a/official/vision/gan/requirements.txt b/official/vision/gan/requirements.txt new file mode 100644 index 0000000..fc2715a --- /dev/null +++ b/official/vision/gan/requirements.txt @@ -0,0 +1,2 @@ +tensorflow>=2.0 +tensorboardX diff --git a/official/vision/gan/train_dcgan.py b/official/vision/gan/train_dcgan.py new file mode 100644 index 0000000..2c8acdb --- /dev/null +++ b/official/vision/gan/train_dcgan.py @@ -0,0 +1,85 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.data as data +import megengine.data.transform as T +import megengine.optimizer as optim + +import megengine_mimicry as mmc +import megengine_mimicry.nets.dcgan.dcgan_cifar as dcgan + +dataset = mmc.datasets.load_dataset(root=None, name='cifar10') +dataloader = data.DataLoader( + dataset, + sampler=data.Infinite(data.RandomSampler(dataset, batch_size=64, drop_last=True)), + transform=T.Compose([T.Normalize(std=255), T.ToMode("CHW")]), + num_workers=4 +) + +netG = dcgan.DCGANGeneratorCIFAR() +netD = dcgan.DCGANDiscriminatorCIFAR() +optD = optim.Adam(netD.parameters(), 2e-4, betas=(0.0, 0.9)) +optG = optim.Adam(netG.parameters(), 2e-4, betas=(0.0, 0.9)) + +LOG_DIR = "./log/dcgan_example" + +trainer = mmc.training.Trainer( + netD=netD, + netG=netG, + optD=optD, + optG=optG, + n_dis=5, + num_steps=100000, + lr_decay="linear", + dataloader=dataloader, + log_dir=LOG_DIR, + device=0) + +trainer.train() + +mmc.metrics.compute_metrics.evaluate( + metric="fid", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_real_samples=50000, + num_fake_samples=50000, + dataset_name="cifar10", +) + +mmc.metrics.compute_metrics.evaluate( + metric="inception_score", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_samples=50000, +) + +mmc.metrics.compute_metrics.evaluate( + metric="kid", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_subsets=50, + subset_size=1000, + dataset_name="cifar10", +) + diff --git a/official/vision/gan/train_wgan.py b/official/vision/gan/train_wgan.py new file mode 100644 index 0000000..554463a --- /dev/null +++ b/official/vision/gan/train_wgan.py @@ -0,0 +1,85 @@ +# Copyright (c) 2020 Kwot Sin Lee +# This code is licensed under MIT license +# (https://github.com/kwotsin/mimicry/blob/master/LICENSE) +# ------------------------------------------------------------------------------ +# MegEngine is Licensed under the Apache License, Version 2.0 (the "License") +# +# Copyright (c) 2014-2020 Megvii Inc. All rights reserved. +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# This file has been modified by Megvii ("Megvii Modifications"). +# All Megvii Modifications are Copyright (C) 2014-2019 Megvii Inc. All rights reserved. +# ------------------------------------------------------------------------------ +import megengine.data as data +import megengine.data.transform as T +import megengine.optimizer as optim + +import megengine_mimicry as mmc +import megengine_mimicry.nets.wgan.wgan_cifar as wgan + +dataset = mmc.datasets.load_dataset(root=None, name='cifar10') +dataloader = data.DataLoader( + dataset, + sampler=data.Infinite(data.RandomSampler(dataset, batch_size=64, drop_last=True)), + transform=T.Compose([T.Normalize(mean=127, std=127), T.ToMode("CHW")]), + num_workers=4 +) + +netG = wgan.WGANGeneratorCIFAR() +netD = wgan.WGANDiscriminatorCIFAR() +optD = optim.Adam(netD.parameters(), 2e-4, betas=(0.0, 0.9)) +optG = optim.Adam(netG.parameters(), 2e-4, betas=(0.0, 0.9)) + +LOG_DIR = "./log/wgan_example" + +trainer = mmc.training.Trainer( + netD=netD, + netG=netG, + optD=optD, + optG=optG, + n_dis=5, + num_steps=100000, + lr_decay="linear", + dataloader=dataloader, + log_dir=LOG_DIR, + device=0) + +trainer.train() + +mmc.metrics.compute_metrics.evaluate( + metric="fid", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_real_samples=50000, + num_fake_samples=50000, + dataset_name="cifar10", +) + +mmc.metrics.compute_metrics.evaluate( + metric="inception_score", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_samples=50000, +) + +mmc.metrics.compute_metrics.evaluate( + metric="kid", + netG=netG, + log_dir=LOG_DIR, + evaluate_step=100000, + num_runs=1, + device=0, + num_subsets=50, + subset_size=1000, + dataset_name="cifar10", +) + -- GitLab