From a6b4023c5071655a17d3e63a73a5b0cfdebed974 Mon Sep 17 00:00:00 2001 From: Pablo Vasconez
aibPm7ux@)K4HXsD@#Ed=MWQ|I z#P;=j++BLn&Il3DO~ez9xrMno@awftXFuiT@iH;>p^0cZ*r8N|4up!JrLDcOu>mNI zl`4O<-{|K{`K{y2*f9nMhA-visYcsU`BE4!I|WnenV8&EjS{qHV;TUy6c!e)sHgzk zdG+d5APvWA`eCKJHIY1eU;R*wq$0R+R3EwgMOEZIdv^5Ovy*slE3U}=wx!8V{Wb#;09$y29JUA}x-Kwu@r6?ql8b{5w? zIEcClGzfG-bLh~>=;+4!e4WxUQtvE_PF{jvQPJA{9lHUCeF%5-^!@;I0z(Qg)c$&N zOS5Hob#-Zy{q*V3=;&gr9$_|~>({U6sq~9*>+0$X$1-APyN{U|z4QY~(X*d7%6;_L zn_RP2(%ri=)w8dgr=_Q#i+LY@PW{jD@R1`&@(sQpG^G#{j_t`dssmtGyye}9g8b>< zD|Kl!>(1W2dmBXC+S^yw) t)r(2rYM7m9DI=2IPr8 zd-lwv>&PXC>F%zoDkH#yAV!f_JZ52SEi|Ry?dJx*>*(lUVH1bjHPzJQ%!2u@sj9aA z_%ZhR{tlexU%!4e*?L4qMj}lf-q1F s|%xwo-1PqPG0915NHUDmJ_rav7Y@?@$x0Du5J$?r?Qfg-B^=9a2uoW@1H*( z>pT*Zk*Te!8lD@hzH{f!>C>lU1nubrYzETRvxPm@M+OE?MGl BQ>_N z(p69(WbBbZBNWc5D ;ko-a@WXcxUcVm-AFyB z`hkk(W_wiPjBY*>s{ZNeQiJc$K_&DQ*d~8k+iqbt-TkRaiJ2e)`eeiL)CND-5$rt) z$*Ho|*8H+MkrV&1V#c=k;D&~V1=at)_R2d TfvW-gHa|b#Zk~rKeE QLqHdHs3)#!+b6?FRGlYyZuEGB_{T0Cc7)D2fvR2Ui< zSQltfP&`4U2cYttGX)V8{^G@@yimbhS>Ek4xkSmsYbiV?=96AU0N_HyX)rRV%in<3 z_AnuL$7j!hg+qsq>jHck2N7pephftGB9%gR9BaEaU0Uii`@r&sj*bUdTeH@8Z$vLM z834V=zPc>nxmoP%>ub!Xr>8drNOAi!3E0uX;4x7tsh_c)o6eIRJaKXop@5z`AFZ$5 zxN#%L_xbbZphf+JoA}M!-V+<#c|o`JzxT=?QY=sRBnJ^9E}8rqZA?zB+XC!ER9RS9 z06Mbe75Vz2DG*muLN`gH`~md^WoNRdRG0P8RC{*v+qVH!dUx-tXll|QJ$kb>PWUk$ z51>yV)d&45N-HZ6gxn34&?UB3$2swqS>7dXE58sEmQMg2!@+Ovqvi5CU*I_XDm!}~ zdp-J`@|d7`SK%TkTZ`35T#Jb7(u9fmI5uFYF1E*RFNrP_u|yghVgIx5*8KSSbD|-k z>Q6;rk=^L?XV1W$W|=m>t#l}`?jsl&Bx0TX{cjRAh5A?#PfIg1O>J!pV`F18v$T{H z+GEGuu~tWppjUbhAmeuP44+ju1G+cQ&2=zPhiGVU21{HPf7I5VKXJnA_|%&rWkdi7 zb56}Xsav SSuiGs z;*FrYT(p;u%ig+G4cZ^e(B9sT&bYO)@uUw(eT7Ak$+#D8$0;poY3b9a-vFXOq7ZhT z1LMU@q 8ICY9Yy01CN%OB9LX>7Cw=OD#0KOE}`is}7> z2Lvsx-{3=Wf9I&kB5UiBa`3J+?C03o*YHa|gt(lXg>bE-UJlS2{!X3KE_^}74TKXm zc^(Lh?4G^7MVHPseKZ+Web!Ra(y0~MX94v9&_Q1^P!c2Id@Qd8o$?42ftFezNOTAB z;(<<=k86WH)dt9mi#x!`D0`%%tqm#$AaNEA PCrlG{1cMG2vKH@jv>kLD;EkYO-sT zqHWB{d9fP(>{%QRXk1+1_&B@98Ge2>w7{yW`5@{&dLlm$+Mu6 ^86w4D_5?RCWef+W*&+01`m7%loa6`;$!p+yXreCLQA@OBa&>P zoT6g(<_Sx0kR{HSUxEC;!W6aS+ZeOBun@+n+r7TJnCy;>i%#Gn6|-Q~5ONjbBRD)9 zRK;QXCt)EW zxKS;zPA#hxaPGrXqDoDme$Q z)6DIxgKW<{|IXZB)qqeK9W6z6RLCJ+O&T?2h=G70x^*k9u&@JX?ELw65w@z4`d|GH z#4GkiiLxF-W8vcL9K(O_4 *< z{A2Z8py=9}A) MWrO0M5)b){d_{tM~RN? V01ULFX4`b1qTyRq Yx4Uu@3|c6#>CA zau8{oK*qCMy9og#lD)R2i`oI8vX`7(K|$dwvlC#+UOFY5^{lK2{(JES1uMu5NK2_? zGWI1NGf*<^=e<3sQv&5YO+6G8o~!vIVTH1*f?vW`P;YrbQhD&;0ZKsqA2L84ZWK1u zI `3kxI9 zbF#8p+uE9W2!T6PR!~@-=_^M`5wFJOLixN-1`u&jyRFF4iXb5&;b&X}xSQoJ()!h( zdJhv|6-`VA93rFnW0x|@NFb7eXb>YyO-+rHOc|7f*381|8>ulqSq*ylxl8^D@Tax0 zBAhC=zY=dkDY5D)*+8C(G_G%H38VTmI=X^dM9O|?qxydJAQ12Z#0V4&VW8q <>fCh zGwVUw1cm1l93RiTd-o$BpPdc%2kAqiqc0;K_U{iu5teIAC#ns9zWTSdsqsc#x%-p$ zZ6x06;lt~OhJ$aF(;!4@tEz^-dIfIO2{2woJ~y zNv#V3thKeZARmYX7Q|k_u!~!qg*;wX|GEYVB@d4h=sUp4L~iiBR$F)13LBIhFaZ`g zyWhWmr=+A*f@<0K?Bor+VHE@ik>0iwU?ifWqe0>Uo(KvEWVM^KDd1?XA(O<*QqUbe z%Ar&I7hgOA p!4kbc9O*j8BtRyJrIBpI|JwU(UDG{34YU!n0o3232H4&$A zn)0B-0WqMGey^@pTzOt|x9H=?`srxEIYFnHUVuWdp(9%7IXOK6mvAh8vFGHpf#h%* zZ^?lCcuwo1oQ6hZg&ufQ`^k<)#4{8Py- iJMRnpv5s}gxQaGq@-@Qvp%Dx!!Gs#n+*kv(*>UnIem7$@sC%cZ^{+@rvr|;i? z{>*v*{@afqb4yE`P^`fN>K=eu<@4vyGcz*_3wF>GcI?=J?#Xm63!To0=_%~iMP6P@ zYo^OAAqB3>mN2lOPlJ#GvL5FxG9seAqoYMPS0Ag2p6(OCAGk9tC*awUb85k{v85<6 z_|1TpM4fa+&y3JJ ?HtM6FsG#Fs1}mB_!SBOY+>> zY>FCy;|BGhK+7`UvI{i1QBCBVgoKkCjR3Bj8*7lxng;2(Zhyu?6H&U6QPrT0c&use zhY!CR8V Vi{g_#L6xraDN-ZgS> ?#m+ z7=jLNC%NkW*J=mJ)wDjF0%V3IC{XZ!+1iS3l9G6l-Z_LfwO3M6fr;h{7gs$#6{UBw z>ln;WE>LVqavq>~!JC1WHdfHy6rcOw58o1h=#~nF$jodU(kH5EXGce#qaz3tvq#PW zBqXsMoSZ?M@I7^+0|PTZ*OH-0ypeha;&R8H;pChi_#V Gft+9bYlg=mNMIRtubTaTWDqMpuZibiyKYzZe zuix)5aTt!61Y(E?qr|>b%ObEII!L(wgzhk0ZU+urxcl?v(iAu;x3rlDE+k&E{OE8^ zO~?EWoZS4|A9PkR3FaBBUev$nBtl-SDULCH9}KA)d-1_n6~VbY@b)IU7D;Bb|H1Pl zyNR}}Peq``dZK8aJ}7lvt}QEzvoHFUv~&A5?=pD*GP*y=v6=_o-PU{Svis^nHN61* zXliP0;rTmAL?Tfv;48~@ U#;;!!vz7o$p?-ZCw0T59KQua; z`ucS}xe*yjh6P{*d?G7ft~-DOvK#%SGP<8+U9QdC!PRkFi37*uUg*o^eL0q@bme{4 zh%)4TBykbwE|2yzj*X05QYk1dj;`u=c6FsD(C(|$_1wsX6jRBDV~Gqr4W}L|zYo=} zUA|y-o*Wa(%UVUcLFUvt?>O-78C+)opt^Q?B=vWZb4rV!p;SvuY>i-7AI(`viH#zw zA$CzOfz|Qp02TCU1*kGe@rtpiM1?3oERmtlpRYnHgSk}FT|3`Wn|eivi_02Gn}>(z z US9ez{6VO|5Tb1%{H| zEE^tDU_-EtO@CsbRG~7OzrX9`^cM~rxIiB6XMDO&LEK&4KT-Z@55OUj?%HKP(Kb6+ z9gfaN@2nQ 4d1}igW!O
LcjV-PA(+FOhD=~ 2^OOTI`kMIp|8514tF!g&Sc5D3+)t|934G~{>O=TjuPYVhnt~xq9I{|{BTH-F< zggwh1?1BpT#*D}$eeu}?BVi(?be+aO-NiV<>f8fr0`d$%1mFx}tEQ%Aer}G8>FHCV zO@fc }16Jf=PPb7sxC`|^}4;W(&U}QL|$d~|*+e+MvjBDUY zU7xLDM1#AvZj1Uj;JLB}b*$;{GS+ EH8>caDYV$=?8aMv=Uc^IJz;(Co&b8b$B%ykA>crP zE(Tx(^QdMhAW$IWI3)$%^wK3e)Ds^nKzmQmEtwd>+zkQZ>V4kd7saqE`7wil4RB%; zdN3iygf5B>h@7a4dzcUz6qJ-<&z~1M&HjORgFYQnX(^O$Q+an%Z`_|JdWu|%!u`Z` z5lOM)e?V+jK&|*t@kVK@squTP|3%> Pkw985zfDX#wDMv937&%Brfeh~i6^loW EqChJyU;=lS|pplgN& z3_^2?;m*__2=LAI6#z{=?g*m9F5+1NTmoqujZL1pfYZ#0E^`-`!iWfU^dn(mEhw)! zXmf~8p_%1nK3?9WR{3Y^E5t#F&yGX%fxQdlJs@TU+*dD!mYziu$Y=RG7^I$VQu*-w za7~mW+CWn8zM-MK+hyAcWZq@qn7)GZ|MzimD%75{e|+&_Iy%QgOGnlFeW+l{0a%BR zs6NgA*54Pz!08p5$4Otk5)Pk+An3o4v)#^MN!WaEh>1y|>jHO!6N{YzbCsW;pPY6f z;wS93=v0GTl7|pxH9voXU&0n0r=#mgxUoO$E=2zI^>tEM(MuJxGwKczSFc3BM{1`C z-aoVy5PWcIN=L*~5&V+%%A6r@)Ug8x`UVFpZgXvJu1%xK2~dWWS`do~Yz(hscDAjI z%+sW#yGYdNX6eoI&_R}!m64K?!t EZ0ji1J#09?IF^mw7vre|be?=;5U{sq^K znZ@hYIMHC(W>?k$6SWF#e8IuWM`(0@t*LoJ^#M&GycTcW`o8!ok(b` +4{e$e57Xk!s;H24M!L#7CD!(?<7%*e bPS HjJj3qm|m!tcVuRE&*No<5}v{eqrK zWca@Vyn7KV#~bwtD<}KZ)5yq3u}&8hgnx;(&SQ9gK}33&&CJc=iVhyzqJiVX5slBq zi`m#Rh?uArm)z~3`r$JvC@4%lh#)&z=3mwGu*0QWB_SX0-n|>aFx^#@KS~6hu$ZuV zm(dE^8B;d{E5Y^wtNxFFGQmCc=Sfij?s&67Ykq1LDR@%0Ca8(4?ZqgiX!h8R9YiW# zk&3oJtAKq8$IHwg6c?|9-2eoEXXkEhv@_lYTg)S03TO$Q9i|g~F51`zE8_e7WpbZS z=#3o@S&53r#KZ8gk)GbKRK+tehRe1 H<#Le-aN{-pEOMHC!faaVUImV#=E$sXt=i-3T z(bJ
Tkfvk)XF&>ne#y{MKzd+(6h#(7;)?V*g$I9(n~gT%Rb2RjE9#Eyq1 zZBfyZh!XYmfNx=#UiFnGwwX8~v6Kw_RE&&BvCez<1};tAO1tOaFf{7~a%v5V3;b3Q zQY^&rp<$q$b2Q_q#1L9To;|yh-Janw0z)h^5UjB7`wv{^=l_d_w#f)@1Ypz{rKhhi zN+6`vLHGa}v5*8%OFV+HXxfeR^&8hu5*10$tyinX)_q^V?RZ>UCMJuLE4UA{>GL^a z@B+cx3QY~x|HCC`@g*{1+xt5ZeJ!h|cyz~a%n-*jz#$3nAU}Zn_dvkGODX~Wf90PA zlnlg4iS4iqO<={K4YDC8T^Z66nve E3qF} ziIe+wn=A4i3l$l-!1u_E1V7vYCSQmIy(zL2ULY_R`M~r+^ix$<&V3yMRv77^W<3?3 zOfut3yb7dfQPFY^&Ag@_q bORXD(a}+~+S~76h4}mHmklTt zI0-Fh7yD+NK GclSTKPyIUK?dklXWm-8v1J1LEWVoJsC(ZEnu3i2D~|1Rw^qmLM9`3v3Lr8e)7n zc>tnFPlQV`kU>xu{j2&xFYi0boe1umho|}b_fdp3s$ra>6H*K2WWuX+Vc1_>B#u&{ z>V3%0#<(2HeadXjmoEgif`bH@J5u+94h1)lmVtWmJN9N`Vgl_LP(2(Vj~`26gb$_^ z6&0FeWw`5H#42x8MqJzlm>d55`9o}BAaNi`gWDXk=I8jxk4_UDCwevz2t>z4eVi!d z64<{1moX;*R_qoTWPRY4KQ;wERKl^bp`oWcw)1FdDl3bJ<6NL|1MY!yktHM*<6FkY zYPg2bP|(IaXeS!THhZ= 2(7 zYIkMA^QgyMeD%cZ%|$NSeK-Ka^C%e|?=BLfc`^k_9Zph`5JW0K+3mAm&w-l>YO^ z1hZiu+H*{(|Fv}u@#0Dbk}@(ndFJYvK|o}*rmLUqAo7(^&7q {Vgz`FT#G+~5BBxYDI85W-dDhnr>SJP%=BJEVnP@nJ$f`EI@;XCggDj( zEwX`z=vo4W3F@@4sHo8w;s>Ni99ryN@{LAcLLYh%@##~)k*eXv#n}S(TtnZKa%UQ! z4^Gj^#U=9POQ~4l7%CfF1Td-*+B>4v287UF8AT=f>(&S(ROzfD`gj!OJxnOnIGb$B z@yW@p4jaU @tS0=% z3sCaKjTmN}im^2H0-7>1?{;;q!LWm7>EXVkHgMH6RHC27Ycvy?mAX?N76nW 6~-zAIm@Y2L{x<)q-p>2x<$-n5+&}VG5@4EQM+T; zK|i4D{3lPresC>3i1xx4boIz15N2O9JB^!m9+n5(19a}Oi-Cg$ADb*)M7+=XsU(@0 zLeMHOFrX)Ch~q_hLitB|g2opoNxpZlzMdW#6Hdt?v$C&Weaibldn6_$(b3Y@4 vFu{avEkzaH*2aX<9*VM$sjwMD)HEQo> z5C4on+Ioe~Jxtu%lxG3Q0^<&sctu&TK^HG7f~JGBK`#m-0C5Gt!v6WAxBBX;naP&; zp6TBO^N{<;=FvHJTDt!oa>X{Id31AkZ~XNODHB}|Dg_ERR^cN=T`YapD$LtWq9i`Z zd{O9Jcw20)Unh0 SImvJ8sXMt zgp>%nBs45cr@&?qP`!yg0khN?dIE9V`X`OJvKg>2w8JnCk`M@cA;*f5k@5c3+oq 7@!a$!3icmV8L`e6X>k?16rb@{d;@F6xG!JV)%;Smz6bphF!#E;Tz2Pf`Y)q zO6H0i&t>CZ9i4=}x?wNjvOFcd$@j8rJN6GOeC^MlpehDZ vYYxVz z5&V$1!NH8>U*#ajAn5$D>(-v0o-XoV{$<0R7*^5E(i8XaxKsB60QA4-0+b*F$gvt^ zw>M=l91jYANqmg}?L$z?GK#=B$J66iBxAln??kgsXSP)*vW0oP^cOD
}~z(2(sk=hZ}Nq{5gN$UkMww&DUuacK0?CgHgQalp#PrZ7JWbKVuRmUE^q zR{|>(ps!(=d~L~sU5A-?C@-MMvORz}6CoLmMfIU-U?L886C|XiYoN^0o82&1&gWDQ z5L s9oo?wX%tn>wd{{|2b9Q|Hz2{pL`l_m` z^G?My>y^PFA#2M$o@fa$sew_9nclJ;1PO2uDA$lPE|0A$p3zZKYFbb0V(Soj5W;fg z^o#e1W1L<@KDjU9co5#}2FH6=)Ma+|>f2nHBuBx;ghCL50AV^8I0CDN%mC&Gf9Kz= zeNb6Z(YEi)F8l#apt8F9Xm9U{LVLhN_{ea|(57Jmt9lT2KvM5Ay)0xc2>Et)5Rx#x zWQRH>?r6Y83tDjdeW=y9L83NkJVC${=e8u>v36b#d?4n53_+`17ZZb}a0p2Nv#%=h z@>w}KiQ{tRo@lW#w)!Mj^orb#wE?~QSTWgNKt^*t`$_Or7$Cz8lB1(zmh(x_!A C111F~#XkaQ#$8g5fQf(=PWQ4xb{ z$UvCvr88ea#Wv@Sa%l`kq>gO0;hVrakXFEj LA@w_y8ot z!}lIONkvASk=%am=1qT)Y)A~S!#O((*jEvsqtOwAIOZ9Sa@pBPG%j*uY95%mkaBV2 z#3IWy {4cBPeE@z?_ZQ_S;yo#P9ts{Z-Yd?@QHOoh}0y5BK{ zA+TsPeh4!`%>wU)c5>g|y`6>j>`Y&~x?<=O65lNWW}!= Pcr6`C;7p&oUC?u z9;IHm_`qT;*I!9iR#u`95{&{3Qc&GcTt7NYYM{$TwnZlhsPIyXC8`G0T=Ixc>BbWM z;xa~25oB*@G7i};jL47eh^^DjzDseX&tJn7I^fgq1}#lZ`{1$z{SD*kC(Nhh=R3ka z3$qVa4ug^i0hnJv6nS3>N4LTacU9!Qd-re!AWJ;vhor%Xv~qnqj%8zQ`f&y^1Op2$ zn#HHBqa#429@ *ua#`EQ zv=<(HH$aC~SydHiQFyPPlxYCT%v8O4@dE8*XceJU3IJaeS^MdE)6vJ_#NcGE4osnf zGjBx+%=>n5J%H#A_<4llrnK~%q<%LG^vMx8&6xgy&?{}O2!x3xgG(-T+bG1wh9kC5 z>G@=C-b~HQ3#++{zh3pMfRzX#fSbI3RZdy?bYW#Z3I-xZ?VO6eeJ&V=t5>~}l2|^4 zmfdda?8F3n94ugw2W^opRrqtc9>bIhg=`aBqJEX9dU! zvMs*)ej~3{w-gzu>KW{81+RQ2F>+GG4uCz4KHC9!Vwj}{xB-{m;QWf6Ge`*JkI`Cc z0uBj~?Fc#iGFvNko{{ZuNQDGI&=bB*mu~5(sY!%y!88k602u_)WvO0`x7klgNps~0 z!LPJbgh@ uW5^d!}3VOP9^V_%Idv0x_DP%y32cr*52Q1rlKYzN-4eWVT9;YMUo~bJW z{s*)J-UXdGJ`m&)3oGll&!4@@`(W70nRqa<;W^P|boG#Gx$t_ndpM &Ai^kJFJ{|V;)$dB0Px|NN6Z|hUw0)*wmVCd%Awf@EZ0EII^4DCb6V2~bZ k@7$_f+DFz|GJa)p&YLoF+Q)k) )j?~9NZ&XOlLC0Gwi6_5s z=1pHmjYPkLb{?2w61C?L`RRk-s|(L4P4irzfr@9kIdd&Go{r_obyO)|;0;Vn8yFd} zhe|q#>Ek&cPpVOA9DeCZqlv&ApWVSw8^t3UCu1b9a* zEHWC52cY+7&jZn?r5`Tm!dbwqd~)Lz_tmDqu?Y&P4BQ41$379If|)|141+qCV;>en z`TH~AShwr+Cn$2009Y^^<;bywP#onJ^CY*9kQy2rlQg2d&;n-?)bIM#0E$YXj`sF~ z!opvhJ?UV}8Y(zpXal2S92XT8wHe8SH3iK(iW3fx+#V){)TX++>7wb9D6q$<51ah* z(CJXrJDd|-AW7juh`WGaA31cvhYDr}92YoJN2kxy1Y#16A}Q$vaccgkZW>Z5E)OaL zzvC2xW*dS(vt(6Lgc}1{v7Va+Af2F=1_cE{(mt=bC7*D8v_~kYYVV#sAgynKM%B@I zb!}~x-TBD@E#_xCNl8o0uf#>xt=48iwU&+M&q`01N^ab<6YRlN`j?|Bz!-UFO3HqQ z!S0p+|VYIs{1xj>dFaR!ViAU|fWE&bAL&5^Zn7#8Q5*q4B zZT!W6yKYmJ*$ZkzN)q*WPeijnzI @aT&jN^jx#$ zYc!`5jFXGX?U|uezwqgp 83j{n$YY znZe=V Ws-$tC7@j+K z$RFd5GL^^L&tl>Y6Ol-Vupl1~GIHyGk&!X!*yFBR9`&|zJmy6`{7H;P;pTGiXTVr} zDpYdxvX|r=g*OTfI*gZm8@0|qlOSWN?0O3c=2n$5)MY>^;!|Vl(?zI4YS(Lye-VBj zQ1K`7GRr~CIeVz9M_glysVFWJZo>*)oqQP;g@?gB><|2q?Oyu0pg^G0Qti=Ky?Zg7 zktpJr7*C6K{EbKD9OiR(q5oRkOCjdru?aivNO1?bFUnOs+FsmQK1OKqWRB4#ST2Yu z>%ea{gc=YE);&=nRU< 7Ak2d&9eV_^`LU? zL;d2Uf7JS=tA4#(#Fm`-nct9Agp>n(2mKz$pz*+sls (LCjYX2Wf%-N??u8RvO;_{MHM+!^Rl_?T!Xru+KJ`e&O=d1CAM zE?yLZnfzGSKTZFV9B%XxW!WhobmDBTQ9oZ@=|tBW^LoY*dT4?Y8&Z%QtJ_TPHjbuI zJ2aoXQ)4y#v+Elh?Z^Aj GJw(5{;ZWxg&r8PsNYK@I8 zf)<)xCtL(ek4$pHt!$M9=_p?8U3m2%^D_zNXYbkdWL{k^_^| lkINLkR+w z{c)QyL&X&%J5X+*m}boz{?jI-$lS}eWFh%rc3mbp zLR?DUS5JUC5FJFC;+Bc&rATB}mFz7rSr}$f$<*Np#W_k9W0p^A2JTBbrjL9CN)rQE zqjt^b_=$j;#LA~|+V4yMEFx4PH64vzACqaWMye9qA@Y9d1tsNoTd8uaFC^d0uJZH9 ztE=M&Pl{gP5hlp$iNIpT#abCEjjvAO*4`+f^RA8LF@t@zxhd@QL4PnCnF^_7Op @4g3n+eyS*f=;OWukPYmX2atUN_JDs5>gJ h9ZWok1aFb{JyHlP6KD^RqI+N#vl~@_@d7OjIt%Eoj~-QdZQCKdK2wg8KElXM z69_7=yyV@i>wg#jP<2o+aE7xB_6opF7-Tdu@)8qIojmES{a`l P^wHk5MB>S-OI+G6T?c$ zrl1&=1PhMlJ109kZCa9eF;~kUsLT{Hg-X^%3yA7=wzXZd>i+%f*9VU@Vq}w0P%}Gq zfp=hZo4MxRN_ D=e)Y+>L7Py?)6XOl6-?Y)2Q46eCp8$u zx8zH36M|h%b8kj33C{;eqiBAAa<3F(M5qBR+p*vK08CT}J0CgL?iX3-LR!W!4z1NH z*hoCkWld(V6m%3I@)CN`MePH)DMvD9{$g8jF0c}YMn#zwkAfBe0gf)?ck9~{XhS!| z#X%sZm$2iCXDErbK)iYnkzOu~0-N;*y#pXJPIhHwWktmsbTgNa*E!XJH939i6wqsD zdpo{Y)6o&xl_ iROyh(FMMO%3e&M1j8j z!(er7SWJvNYFen|;GaLga3S}v&V0CnDCuVL>zyIR@WC{8NDdpTizf(pE=_=k)XW>e zY7!N9+hIygoTy~!D5B{jauNx&9~Fm>1J5TzqF%_wvk*Zt?xvs<^ZboxIiP(QF~b-m z7!g&)e4y7%wJc^Kp(u{5++6J}y$ZO)+YjbybOBluBrtplmMiF((DiEP%iXBkU@~36 z*g`i0wFsu @=@FaGm*{G6*?2ftjs^$EJzHB4730 92UfcQT;S|<`IN#_m1}=LXQmPUgbTny6Nlb4bhwC{Oxv%AwmU?2(;VuDU!+OO= zN5gW8M{*2-7lg1MNp%Seou( zgqH@`6Mz(t8g#|4!JJiHQ?mhf$`Z;i2;vP1 VjB9)4hfu!06NRTVlyjk<;A2J2Q%zM`$d!CE z){;SA&IO6Cq2~+?^l(h>D;K+QBik4B(Y|sYU*Ds;W>}bg +@OHLeK_u32Nnx}Utq+QCO<2#z zd){|w@jMJX6)evh-O|2tXpZ8gav3UhF+$;YUgBhD`J&{+htDrBtFncfJKw&2qMQp` zqL|ifPJC_)?L#y`fwLT{XeVzi44^my{H#U+R( 6yx*U=(3Ok!J*^|MLV1(hCpdLoTapFcr+T5*j9dD(Vp@P zad8P7vil;P_Amjt<_v~X!O4tuhk~}Zqof)~qWy@j+cmO16cicr0dV=IHk-oJ1}r)f z9z@6U7W^6}dn!ZdYa=c`x D-z<~1ACCVYN)H{8aEKqnju_`5GxYM z{gH6L3cXblIlMyX(;rHlinls LVQ3fd(S8?rp2xS0w*dTywmCw;Qg6zg&!GpXSVvW3O`Q{*BZ-Vq9ebm_C z0V2{~ll{r{kcfzzWD!D+?`E4$=Y$kq%f7@UR=Oven};VODf(EG#PDQ`X)bA5czkvw z?eBzHfjvxMAXk$!cpl!_H&qCF8Dt>UA6!;(DQ0vrOf6&_%KM0Z`g{K@!?9yXFq?*p zxI#L9F>B)yfN~_gB3u;Q72QajKs>0ATJXUd#k%L=VT*J-NB1$Ia(cAOeGz0FxUr^2 zO~CI}%2XP?Mt@5Rly5X+IC##*8@D;Zz*0%aXPLg$Zx+)VB+N9TukRjO)3(5&z!thH zq*{ zsj}5M`G2%`=HXbcZ@Yh1BT+O @9pSMd>3|Qm><*Dk`Fi zN`zb8eya5K>!?+6l|id!MhDH*Xs!G`P|ucmr 8iK5 z$_|i|Gpq>RVo+Z&T}_Rdj h*=QS}BXCC({tWW*8CsM9;Ay?c zmu!f1jv?w^JA8g&WibC<{y8(KQu7kde;NEfQjY@i_ulgcXjUGsgABrlww#W-Hu_6C zoKJg-6@7gbY$8sblK5#i3R(eOG=7AnlsjonWfTLTFhZe1?uHt*$|c??Tu>g6z8xJL z%xsJbEFEF(!)PdB*r2YS@^;-2qUih7)G` xuyG!k26C z6${Zi0X)#DzP2$8)Z1V9;0trHknYQbmvFFBs$bvD+!lAp|CUKGMk4m!aA^_9-#%Ld zab}3jt)irlX5K}l^P}KZk@qW*RBDxMB *0aF7ajxxDb~hDSc?a0f~kH-j~>ljvV-9~&;0rG zmDa)br KKzJn^cTs!HSG`H2ufZ{%LYncpelNQ`sr2beKaK?yj_Fd)bV3s zE87NtUkW}4UOo~mP;6jQzB?-U(MDuyVDf*{8*#_HOX>y~sY%JnLRm<@6k@#JT=rQ~ zm2IV+q{#d_x*@OtA=gdq-f)LJv^MkuJeRhR0V2`ufzW>hq~yhmyNBD)*z|7gBIJoI z{=;?Z*t#wjnn-l@KTPIJ2MJyUd_w+%z&P6tDHosrYz+V9^! bHlsWrG;UZAY)!NdPnopV4Jsj$YARO#rs&Nw zx<~~D1$Z7?N(P69LI_&EJcIltXO7=3gbb)n^P6J3-R^QvA^_@0)+BcOE@<4ai2*c< zjfvSQmtI_)^1SHJmBI-GxF?dTxY}{vE9cxUWN^U9cyJ%L a8Wbc)El)*m9dt)JT zVk%2NF3j1O3Hd1+;x^^$OD)9UhFK+W!@dp2?OWohMdam|Slm8J&9fQTQaY_hDN<$% zKm(Ja`+vaq=F;X_lpZsl8uG^s-~Heyf-7VnH&;w{1^|pC?BxgeMO}dI(N8aip$%x= zL3*~ZP cz}cjF}~BX-O)% zW_@mJZS82t)!iP|j7Unr=I!n07Kuq!SVu!5?Bz2LbQuIja6GzfiRSO5q7$nBovXld z_%9Q@o>312N)H|};;w*i&mU<{DK~u9{tM4`)*ijBHy-{mztN#tc?wh@VOIaX9e@Nz z^>NutSFa{fdqI()S=BtHHy%^$7nE;V*M9Ay# $th0t2jnTABdtrqH4c&_c9M{%bR=zyn} jWKG>rTA zPoDCPo}c>mT@j@-_lIXn+piX!W@ MW6YZx$}|^NrUXerWN3(5I;? zO(8l#0%RCQX(n_%iQa$yd2Zx_Ryfn3wP<54tgX@RqZoL{SHZCbJn_sJc=idrERZLj z3$!uf#jOlRs8=$|1%-8@%`|+J7%{RxLj3P^eg#7TdqK~(6m?C4%{N@o{QvyZ5^^YS z1F!1gSv>|&y@7i1bRO)>KYzXw;ov=(CJ?njv}r_=lall&u18uc{4SjU``UNYM|x(A z9QiLq$Ap{G)ZW&XQ5+-<5NoG~?B(2Ij&7N&Y=Dj$ftN;}u{nczG-U!gn=Q6I%9iJD zc^wD^0O3Q5W8uq};Y`tEUL%Vhdp#3%kpPA<+KPYp5Ud=jQZ3X&WFW
J-Mkkh}~>km%UsOoN1$x z5!3Py5VxH^5En2=;GlpFjaDLCQ%HWD-_vAhFnE@PY}4hdt}i$`3*|EKDoAc9pxQ|p z2KxEanBxG1aWlgm9Ub>-A-)8JUbyxC6nWPR-0Qvmzqnnt5)Ji^xRH=dv9~L_Zz|*X zD)KZj++5s#c~%cuO<)ejC=p;WaE7YCS#x8s4Wd13x=u8(52tU=zJzii*XDPC7|`({ zATX =!Bk57V|jm=di*M_`2eaLR~>)aI*7kD)S zx-w-{Ltm7>kHJ!WF)`$y*a=2X9lu%_#ZdPTSIek`7QcPpt{3_FL;L^Q9N7K#I)QJN zrVId)V;0+50U`!cR_jUF*(R5cTLB%}6u}-ZCs-;?nRT#T3O(L4WdR@eh-}Q9CRUcd zPv=#>8fZ&84J6cU%9` T zc$gtQ6KJ-5CPEezo0UGW`TAmIXmSN~rP4=6UgeaDTzlItkcYD-SMW$fk#*k m87nQ|Ct?pdc>-JE3vW9&jwAkDv3^gXKLO>*KrzwM$Gi0rOMK!?V z!ndwd8k+k?nCtZM3}&H&3iMtcsk;f(NSeGQO`JM}Mesp_tDL)JzkVrg8o-5?C+YDh zClcc0VXLWkoxK2}Sa)_>$~L)MWRTX*$S_MHg}X_?g9A1H{1eSA84M6n3A?)Auo g16E?96kjKcm*EKbnPSWn&f< #3sH1+BXp?r%7;j+P|6%@I+Z#$k#D@qR4OXj3Ais z9I~fnu}!S2-|VG?LoLA2yaoP!Rpb>4f%TmS!QD$#592sP3Bp3d@t|79S&DOVyksv? zS5O|Ao+{PL*z@?Vdg6Q-NDRtHH3~!62LFpwLc=7SN+12!x_K)suzU5w&QgBCIarS` zzm`Bws_#M|s$~#Bxs_)Y>(ug>mKOkalHJTrGk`4QoBF=Gu{Kmd_>{GU{ATide22?P zCsoKs?=KlG{|=vf_ 6OvxQc-X=h7W28`vf!eh1c@+UZH zUw>?7J5LoC4fEdesl(hsjIukw0?+-`Sx!WFGQ?n5YG0$p Cf(MYD=0^NBtMm0y`mGTRQLyei_1(y6% rl`RWqq&IM`8?7cj5 |M45!a#*!`e{ 2hAc zr}X$2^8 ^#!QSOZ9Fy%EN4pC7;>HKc*BgHU zUASJLGJ_n|epv8UkSNSu7L;u)2rTfh)N`2$H-hD~-foWlx7JGk(30aJZ^5pNcWJ@9 zT0OSgv W0gL5+T}oqG%!K9WCxm zTs3|W{*OMc7YLIZoVR%=C$Y4(BpBp}J_{zvT=DWM2T4}m?f&+R>|shQzpE!@XCO{# zSH%5ioc5K45>PmMOu;>Y8fL0zMq8G1N02xKV<+~Mq!Qzlu(H@K?kPLiwQm$9)5_~o zI$HN>K4sSf$Mo&zt7d+!!;@m&)O>m87e|^35F6|h+THIe1?V&Zfqh7_yHEP!Ju7$g z?P)&AAxAHRatym3FM>UwF-kN66ROGo<|3p1`uZx@WpT_OWT0X^_qwfr2b2jQYwq25 z1SJV2XciA`>u#owEl7;IcklI$s|t0`Zq7VjSt7MG?D@|@li_JinevA#6TQc_ca_?m z)Z(wM7&GHq=i{85OR_ihCx(e*{gj;w{#8rLdyIwS$i+>Yhd58L5^L>lxUpemXaDwi zLE9benMfZM6Y~ZvdaP-8TeV^5Mk=|!9{G+mPsYjX>?$Zx13+eEFkO{DsNEp6tbD=h zYQ}Hmo9XXBx}-Fp@^WI04m7S?)n9YW0Lelx|D_p~C5bkk9V>eKKqipWh_b1DcTpBU zF$4JMHCn!uqE%}54s|vXKZt5DqN%AO8D#B$Nc8Don)x6sF4fo&P!dPhapO*=Oi`uw z*Wf=g9BBtcv})TVBTNWx?-+1eIBQ}2_)0Pj=rJyMf+~liw|lp47 hh6Ixg9=;WlX^pQLW6EysAZ;xf`)~u=X zTo=TZJZ|1}{(Lco!kloW0PiR9zAnEa6&8i$LX*#%dm^`9TswfWj54Lym*1&1dL>4c z>`wRJ qvonvX9Ty#1G)ne#5B2ZXi+h|*7 VJsY3h(9dMs(44;X2= ObbA3I1EG2d1u(-us(O-E*OQ+zCJuT2XO zG*kuZ?V)s6oehWiF~6;Tw>k+K?X>5S#^$0?OTzN#L0vu@&UL-0UpU?ZY5)}Uyubez zaDPse+3r 3@Oan-6PBKKD2dawe7GY$)z%L3^0BQJs*Q$2cm>O$wo&ub7KE5FB|JlVVJ zd8@V*{?R_`ZgixDs{c7VTU%sscwAg|ktn7|2K9y9k&&MM_COEk;7xCrhaAYBhLCBj zKt)}D-$*u~dLM&>UW$U~?3~Rlap5_C=gJI-6CQpJA4G>AIv|y`RCzM|0Hc(c#Gm4j z+$#1O{pqc(e1ld;eoNV|-Z@TL6on5LsePr3_uCpU>k+(?UOjpwt&Q#b@zW Fj%4u(3b+n>?veVee!3PGAxr`s| z_pQ6FWIGfOtDzJct0V%YKFDffzrE++!44(>5V3vQcX=&6x?;G^o3Wj3H+O#C8Gelu zG`JwPe+zQ3xi9Kse>64%j@PhRgaJs7y8hyZqT6LtYTAUMLvv|&mjRlme0=gVTMI9A z#ySma*axqVw4z$<+i`6w(ENBsGtbB9;=l>5l42o98rR_|1$QJh G xczYtDBTOn zTsQxd%DQEwEwH9Zsi=FW6Y8yZ+S3fvTEh5ND{)x7%1_OlPMmrM#iLQI*;1d{K$jhr zx0sTEb%BJmf_1^v$&&>dp7)W!*A9Xfi}EgeX&b#E!&sUi zSRdLNoW_+yVz{3vDS$OVQksDPcy5uc29F9uZr|QRW5wu=S;B~4vDGi5?do@<*_-v- z&_RP 11kUz zxymBYYwt5q5cXKk4OleoNWuh^)A8N2Af7O jU~H_pBBe6#Ob$We8i_X~KFbs`;gZlm!I9&R(tfH-qG$ z)(D+6$Y8r>yoY_nd;9XwKR<-m iW-m$qAN>DFtr=%|JL&7n~{uej0a!l!3}x@Ct`y_2-`rbUi+o zFN5)|*EoUPBNg5|Yl_qlOmq3!&KbV7VUa1LU1JUuzD-ZZ6~LT@FLa=)s;G+~#Eo{P znXH4w#-mf1wf`MrL8G{XG~UU$6ans09=$HU6qyGu#IB}lAQiJ84&ECq^+r5THZ}14 z%j+5f%8BX$IQnU3=1euUZ(znCB;4Pg*D=aKKiG!BnEuAofWxL4_!^XlZQW~Y2ItnY zH{rqBLDSys;t??HSVp;Emd?AQh~}q_!Y>~fE%-bxTqyeD_F~5L=^U2KLe%7>2X+D4 zH7bda5L2!Tfbd1cVco95GRWg0&qq{RhDJsY4i**MfMGpz)To7>(lPyDBtjz>yfneH zK^Hj8C!czs2M!1S%7Jds%wg{;UrW3oo5LZS7gv1z`ZZ!}JS7zX4p<-H-rm>(`Kfw> zrXENePbm1eQHFwfDSS}*UrXQR<*D+^#6IUwpMJ(G<>A;;PT+0;uF1bU*F6~ux(9^U z%fo~sO^9~9w!M$X*-O3Q=t#!o8edWMy!I^QY$2izt2fCX*a&GwfS1=1U|H7Fy_B%r zQi&cXKuu7Gtm({-gM>jyTkm9bkI6$y%O`(9c#Cu%Df z94*hQ<3-r?^YVI14S~%3WYhtoMUKYB_TOQ~v6NYmtHo!3ap0MBFEdzToJea6MnWcH zjqJ)~Q)2gH-#+#LeF6_@%H{->sZ-CZuQA-XF +hRP9{b*?T#i6XypA;Pw_B6>+Vpd34P1fuDT52*?`B `nj_?64D3#cXT)`=ky~t#k>BK`fc6A zUXAf?(o5OY{;9*`xd-N1aCiYZ^Y2Y>47jP7T9Y_cWYxX@)xUd~h(vlCh) 2&XEQUNo#XkGWxIv;Di&8DLkHz_hhE{d4#D|F7DbJd4JZ8qg zQ35|o?#6TzeRpR-Z|nbTXEde9Tbn7O4rhw`hP(}18}V8o@5NqG=nO#L^DCn7N0IIF z8^5D0urTHML0uWn3IyH#p(aYYFO2n%Nx`x!PO>AQ^^uU6ZCQCie`5GI`QJouh70U( z+b>XUfob>!DzFWx;^prS!_PG$)n^pEgDt!uuoub{V+MP~u9`&n2#qp0rxqK*YGuF_ z@#MI?$E{bB>R(NqFah`liAFWqO?#*er0u)sCBOfCiXwmd_9c$_Hy431PMSRVm(MuP zOhaEA>D8JPNHs|S|KWVZot;SmmXVPGlpq+JTj@^|*}9lmY4?G8ZY~qNcG&RYn@VDB zkAhkAd1EuB+6zg5cn&O!qQb&97AcvI+9)Cn _h?h=KY!LNYdre_bDcCl zA|z56tF>g2K7}}C&h4}_{3ZqE2}<~&O)?5Ma5h}SzF~PS9EID$Xx_s@Z{a 2(*Th4q6ygznNpR_5^e3f+ z^%A|d`1j`+r4q!h%0?4W0N-LrF%X08Gd|0A7wrI;nL2*F^7wP7PurR1n=BMg+cDzA zPh8@Bw8!!=ipXQFP2Zdex@Z#eY?4JcQV&^J$Z>wMQy($ul3J5ww&3lZkzK#;z@FFw z)*&Jl@0Ac5M`1M!DpeFC~&6jf4(}gdmF9OMgwWAh;25z_-?j4aov&|5(90{K_mX zRg^yBP$!+nsJXSd*@1GL3jw-n+&oXT#{&B(c|}FxoV~M8{t<2n&oiEP LyjZyM4M%8%*$z4R;LEHk zLONozL+y;I<<$#hb&@la^^h^JM+QL3=cL7OgPN~j1;!co@5FG DxmHYBU${rktF8`iiQj`ChPxX)60c;Z`i;@rlrqY%wj z24#Z)Gcq6L(0$<)j!6}mPn2Yi8Ak(UEiuzR=12WSEw-ei*D<4@vNi?9vs%KEgOfEA zKImSu0B$qYiK82(71oG~HJM#Ooa)*UjB)a91{0TZtEjFchV*`5mUm|A-Ay&C#0ryS zk`DLCg28w8>`{gFza4YNEXF^jlK9j9wnuS4`YHgMh;K%sr0L8X+c==HB)0S^g^V!O zFHi`}tzwd3P_v3E^Cl$FSoNgirODL;fm f*WhuphFNx$jk4QKO@Vui$BpYmzz086QVA?> z%$Ub--o)tq>*e=N{+domSX%CcL1)PF*vHJnb1gpJaH2OU^M~zxX5kRq3D?fwQ+rb| zAfd6cU8A{zWkG{Wz<``)5Jn~KDjRMPmc57WzUJcq{;uF3)k)vY42g vwPKhH`^wg&1|TF#9!oII$*d37xjwF`TaKhKyA`9dd?N$NgHCFCb`8GhPh%j<0A zJpi=%OW_8n)9vVGp_qbKX|ABsro`TuxQcp9D|qekg`DxjNATR{6T8k-VGXkfdfMMF zuGk|Lw=9T3n@XQ9L#y!@S8zI$l50l!;2}fa3cr!IgfM%vX!D>xZfip=0d{0WRH{JP z)Q7UCmmoEyYUvl*Wre6GWi>Dru{72xs0kl$kK()0@_FB(LPM?Z!AW3QS%{qvqE)BK zTox3Vyrf#s);4~{804l%y{LO=k5HD>z6IS!n2aDfZnMDu7KzM44vua{n*iU~a59W- zA|5{jkP)?HbaqJyVukth9iW|X#a4&c2&b($D#=P9#tOUMC14ZD`z4$%C8$y1%HGr) z59ZB&I+)~%K^hZbKKu9YUoHO^N0*>t!RmI=pK5%Ic4EA&ro5_I!}Hk4eW=u7^>jP_ zsGuO>RqtyCx`BF3{As)C V}K(Tz v1^ zPb 0q4gd0$oLr^Fr~Qme>Fm^OmDhH9{v&g5NGUW $-;I-D}u00}KmBCXSr2il9xHE#u_!=gL36s3{H^G7IUpUs9Ka+4q&z zHwGIAW#7&-^Q&)mNHLxoS!4(EAE<6npPrr>OfYsNo}*r+8xW@Z0K6zUYdb)Usj*as z E0a9Peqm!z@K@)ns;(bDXyTwV z3~DRQZ{DAdnUK!-L1&(9cQ@Jd&>)QEEqK0j! 9$=?gKUIvLI%MzFa(g20~br z-~mU`p+>*`*ELo@vfr|0eR_05I+HNy{L0|Dl+aUj1}G>LVK))H7SazI+%Ac%UZ_5+ zjipAD1ayZwhB`M_V`Y;@!*asIQAwBfzRx>@DN q2P@P~bL_b!S{;UFv{ zBhKXe%!vA}h8a!K+yI@4-bg*MQdXx?eeN}G>{u8?uV1`CeniP505nDVSh`RHK>p$Q zV=}K$v_I*qh>R!Ayz`n!NP$U?CqW1x}#wtd-l=3 zwQ7#3M;j^+)x4bo3piBY9u@)ZwsYH;I8JT57ZnxytPGMCQYZc{ks{BGjc{lEB#tjc zs?Pf$4zCh~pfFwV9h=SbD##87l8Om=3=)OBdT;k5OvM<4XzETHayHM*%G!FCLZntx z-OrzI#g;-1@KHf;qPM(9I&<*M%rHiY%pNE^fh~~JUm>2X44&c3q^gpM%@-6?N2{P+ z6p5Wnx;oS`{0E5ks_FtozRKbJ;fKG{WHfdb-nW`t1jB=O3+)>_1Y9+uqXCgTDXR?d zh~_hV_`dDiM|(Zve3EY6x N}`A3rAvgS ocw zqy|de>C%ha=H7?cvpBAxpaA *OmzcuE4rKEz?U<(zppIq8 8h&9Ry9KstGI+(D==C =CPPVs#@l`kT5d^l% zm2)S0n=v;c9psh-B6}mN5HDE%qc75?z&5(2b7%7gFwxJfFS6x(TQ3ifm)s)LW1I_? zj0OY0s}pHu2j$K|T^t>~cJh$SiXS}rVIw+V01$El^=|7=+&gey;@&4`29r}V%ArI! z0p#$OX3AMZXRAbVk9$9L!^IU1ETLf=bfJayI6-LWpagF9nj^SeFz}8&swQN(8}?QY zR=xr`77**B129<8l<1;%#SFa99$6V&aL>dAXC}IeMj7a%$Bm~BJ^O?T7ICn+vpurz z8T;Z(SsDIvm7b7%tk!>-Yr@;nySX;>TBqlVF=*L_ed!fs5Qa(KyCM#MrYovEBITI@ z7t<@%j;mR*71b}7eP3xFJCH*uCVFF=vi|2bCD#`aSI*y|3?QMX6(7&~Tw|HQq#^Iq z1EJ`?VNkvQxIKrM0PHXqrAa$LV?teo>OjRNC*nozo+pq&6f~S17(+EYVhBFBrxiyf zxVX54h3)hH-1*T<*QvP-?>oKmNw~dkUZ2>HR#Px2XU*%bZg>7twDD9xMe3uv`!7FU zeSh`Y_6H9ZEeSg@U4CuQ@iFh-Ez`dcf6;&0(nUd9OL|P3ESW0hb9rIt8AGE@XI{TO zb-QP<$KF$ty;kpE|3UenQg*hq*42u;M~dq=&v!VfyyI3~=11RKCEsjRKP7z*_ CIk53)rv@XTq)xI{=L0zy^QcO<#2Y9L@b8E+p7ei&v&hQ_8$JOl$#8j9NozGV?9 z#nTT;j>NgrQIHtV+7E68ru>=xvDL1^JFeJ1jANGeE6AC(k-bX9&d!LKCwf tQ4R=VHc6W 4=Y4CT-o`bQu z^)OeF-*NT8``p}tQ_%q8-#u>cyG}KYw)a`r?5jfwT$+IwbV6{uBj!^Co2fcf-`lV) zes*oKZOqZH563lF&hmGJN|@F&cx{!Z^RFGV|KaWQPn-^ocJ_$7Gr7-uaivk51K<`p z^`UWu;pFqpd0Lm2sEY@Rj>&myBqk($O{fvLkee;Dmr~`KbkxyL+-jf? zhwAq^yIOc{(AorH)l0W4ApzC}fM4gGR?Q9QSN}H!Lt9&$?#^F9S+9DCO8+{OAN~32 zkypV!TUtN)n`0Nu37jZ)+?~ID{d(})0Xw$Lj^uW}PCDw!7_G{pPFdagbxYp)(ryUn zCrop0$*eS&(7JSPO+uBWJs<#srESewTidHQAX0A0mYPnbGVh-+fF^y#fB3(|fvZ8W zvpK`oX$-vYTVAUDD>ga86*AXJlc*n$Qvbc_b%oX$KAFiZs-Vw%dqgJxG_qX 6X$~Z*0W_JMp(zP*t<(D6zfF2%CP7kR?CvpIImSZ;jS$%NvLNDiPE`X@ zak)6kAZ!6k$z;3bL~kR+Wp$oYu4kR}ee0{e`|;YRho*BH{qCpyZJ_ $L`oCWWZIso zKIZXd?&XSw%y81m>p6E9BvUoL?L#9B2#YM{9K7iBwy}|s)3krztKO(_8J6R`{j%3y z?uI!2caw*GGWBag_D~+<@PqM6lj~mWzR$S1gVy!D)#ou ziE&!d05SU_9)3Dn(pJo@IF2EYLowDV{IO$(g3Slpau1WV3uCmNR|tnq` XG0!Rv zYhB{Bw$>+%p36PdpbrsGt}#RG?4i-dAC54%I9BU0WuEwF!M>M10_MLh^@M%$mQa1o zEuN0o`EeX`2(Q!*_hj!gv4NgfV&6{DL*ffJkjl#BNRFn*19e9vtK8obDP|?g%>Vuf z3UEZ?*8~dGYo+6QinbY_D?I!{IiD2wt2JNjL>N0iVh)7{<%j;{A#C> qrEuJf ziir4s&IlGwv9a{`8az5+;C?e-Aq`<%)sos<@#@pw!bNViuL>Tc^`pfhRKN9^uG0)6 z5_M&AT%6j~`HI$S_P@G0ERCAwXo+JJ5r?)|e`4Mqm1VU{ZDVF;&98?mB(8}_q_n#I zonf9cqe^z|7o5wH{3Cfs0tp!>BsZfVWQ6EgbNc5AwzJi58HKxh+^%o=MM9^3!|!Yk z9U4zoGf-BZ<1siEqyoTC{?N&i&W NB#+db@AZuR302x>i~U~u zyAmr~e!fqQJP_HtrSq;cnQ!4;?FjFf?}YcmMhs(#a!2>$6}kKVdl(82Vq#n`P4!;6 zqt?bLh5+gQxuVK4fhchH3FVi^p>}H3k#-+1(!mm5XkxQI KJ31)BH~x&xyViPQBj)r63`PIp0Lh(>#;-QlC_ zI9yV=IY#4dXHUAj1KlQXnMO~vHWZcwT?eFwhaWGkTGsiKE-5m CbzHx1FJ@-_ z!ZHFD+dp(6T;F$< ^P1%fz=_%-o?pjWoajg_7#G-9Q&+_!*)#Nz(~Z^cf!(VCw6S@Y~6sgwYlYQ zsc9q1FCIaIgEIktQXgsotR?K^`HBblMK8Tvc*isBy;1?D8+=W#y=QY!^)5i>G4s{z z@;hM>3ANFg(eG;yowvXnl=i~p?P_P|PBN5m-eCDKYLfyWVd1z=PSNC>8@gObvV3QL zJ|Q(Vz9DCoFm(OLKb?0#0rGbiVj+g9^Izfi_~`WT&pkdbnNKeTgbd#V>s!uVgurvH zvj1-)<&K*pU#7_sut(aK9{(Pnki7@%Sd*K}dx#!sl}%_~g8D>n_WBU9xYI`5R CS(DIpTA(P9vFYNap((H}5EB)WO2R3wu1?)rf- zAnZ&20L&gWQIh=lKR^0EPvHN9C-AUb+#uuJoJQrMTdYT2x>Tbz+w+>;C*RW}dxl*4 zcA*qpndmgnZt-uT*@N3lduv>&RI)fTT{y7u|K2_+X~y5FvS5@1Bn6`UJ_95$D|>P2 vAg_Ze@&gy(gwpK%cnLGj|M%OkU1rXMz-|{+i>K2Qi?o*NEQ!$EcH(~laIW7e literal 0 HcmV?d00001 diff --git a/docs/tree/reference.rst b/docs/tree/reference.rst new file mode 100644 index 0000000..82133e6 --- /dev/null +++ b/docs/tree/reference.rst @@ -0,0 +1,24 @@ +.. _reference: + +Reference +========= + +.. _settlementRodMeasurement: +Settlement Rod Measurement +---------------------------- + +A settlement rod device consists of a rod and a (bottom) settlement plate (see figure below). The measurements are taken at the top of the rod (i.e. the `measurement point`) +and since the `vertical offset` between the measurement point is known, the settlement of at the bottom of the plate `plate bottom z` can be also derived. Optionally, +the `ground surface z` can be measured by performing for instance radar measurements. + +.. image:: figures/settlement_rod.png + +The class `SettlementRodMeasurement` presented below stores the data of a single settlement rod measurement. + +.. autoclass:: baec.measurements.settlement_rod_measurements.SettlementRodMeasurement + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + diff --git a/pyproject.toml b/pyproject.toml index 38b13f5..a9fd8e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ ensure_newline_before_comments = true line_length = 88 [tool.mypy] -files = ["src/pypilecore"] +files = ["src/baec"] mypy_path = 'src' namespace_packages = true show_error_codes = true @@ -85,10 +85,8 @@ module = [ "matplotlib.*", "requests.*", "nuclei.*", - "pygef.*", - "natsort.*", - "shapely.*", "pytest.*", "scipy.*", + "pyproj.*", ] ignore_missing_imports = true diff --git a/src/baec/measurements/settlement_rod_measurements.py b/src/baec/measurements/settlement_rod_measurements.py index 19b73f9..5535b84 100644 --- a/src/baec/measurements/settlement_rod_measurements.py +++ b/src/baec/measurements/settlement_rod_measurements.py @@ -7,52 +7,7 @@ class SettlementRodMeasurement: """ - Represents the measurement of a settlement rod. - - Attributes - ---------- - date_time : datetime.datetime - The date and time of the measurement. - rod_id : str - The ID of the settlement rod. - point_id : str - The ID of the measurement point. - coordinate_epsg_code : int - The EPSG code of the coordinate reference system used. - EPSG codes can be found in https://epsg.io/ . - point_x : float - The X-coordinate of the measurement point. - point_y : float - The Y-coordinate of the measurement point. - point_z : float - The Z-coordinate of the measurement point. - It is the top of the settlement rod. - rod_length : float - The length of the settlement rod. - Note that the settlement rod connects the measurement point with the ground plate, - thus this value is in principle the vertical distance between these two points. - ground_plate_z : float - The corrected Z-coordinate of the ground plate. - Note that this plate is in contact with the original ground surface. - ground_surface_z : float | None, optional - The Z-coordinate of the ground surface, or None if unknown (default: None). - Notes: - - This value in principle corresponds to the top of the fill, if present. - - This value will be typically measured using radar measurements. - temperature : float or None, optional - The temperature at the time of measurement in [°C], or None if unknown (default: None). - voltage : float or None, optional - The voltage measured in [mV], or None if unknown (default: None). - comment : str, optional - Additional comment about the measurement (default: "No comment"). - - Properties - ---------- - coordinate_reference_system : pyproj.CRS - The coordinate reference system used. - ground_plate_z_uncorrected : float - The uncorrected Z-coordinate of the ground plate. - Computed as the difference between point_z and rod_length. + Represents a single settlement rod measurement. """ def __init__( @@ -61,12 +16,12 @@ def __init__( rod_id: str, point_id: str, coordinate_epsg_code: int, - point_x: float, - point_y: float, - point_z: float, - rod_length: float, - ground_plate_z: float, - ground_surface_z: float, + x: float, + y: float, + z: float, + vertical_offset: float, + plate_bottom_z: float, + ground_surface_z: float | None = None, temperature: float | None = None, voltage: float | None = None, comment: str = "No comment", @@ -84,42 +39,46 @@ def __init__( The ID of the measurement point. coordinate_epsg_code : int The EPSG code of the coordinate reference system used. - EPSG codes can be found in https://epsg.io/ . - point_x : float - The X-coordinate of the measurement point. - point_y : float - The Y-coordinate of the measurement point. - point_z : float + EPSG codes can be found in https://epsg.io/. + x : float + The X-coordinate of the measurement point. Units are according to the `coordinate_reference_system`. + y : float + The Y-coordinate of the measurement point. Units are according to the `coordinate_reference_system`. + z : float The Z-coordinate of the measurement point. It is the top of the settlement rod. - rod_length : float - The length of the settlement rod. - Note that the settlement rod connects the measurement point with the ground plate, - thus this value is in principle the vertical distance between these two points. - ground_plate_z : float - The corrected Z-coordinate of the ground plate. - Note that this plate is in contact with the original ground surface. + Units are according to the `coordinate_reference_system`. + vertical_offset : float + The vertical offset distance between the measurement point and the bottom of the settlement plate. + It is in principle the rod length plus the plate thickness. + Units are according to the `coordinate_reference_system`. + plate_bottom_z : float + The corrected Z-coordinate at the bottom of the settlement plate. + Note that the bottom of the plate is in principle the original ground surface. + Units are according to the `coordinate_reference_system`. ground_surface_z : float | None, optional - The Z-coordinate of the ground surface, or None if unknown (default: None). - Notes: - - This value in principle corresponds to the top of the fill, if present. - - This value will be typically measured using radar measurements. + The Z-coordinate of the ground surface, or None if unknown (default: None). + Notes: + - This value in principle corresponds to the top of the fill, if present. + - This value will be typically measured using radar measurements. + Units are according to the `coordinate_reference_system`. temperature : float or None, optional The temperature at the time of measurement in [°C], or None if unknown (default: None). voltage : float or None, optional The voltage measured in [mV], or None if unknown (default: None). comment : str, optional - Additional comment about the measurement (default: "No comment"). + Comment about the measurement (default: "No comment"). Raises ------ TypeError If the input types are incorrect. ValueError - - If empty string for `rod_id` or `point_id`. - - If negative value for `coordinate_epsg_code` or `rod_length`. + If empty string for `rod_id` or `point_id`. + ValueError + If negative value for `coordinate_epsg_code` or `vertical_offset`. pyproj.exceptions.CRSError - If no valid coordinate reference system is found for the given EPSG code. + If no valid coordinate reference system is found for the given `coordinate_epsg_code`. """ # Initialize all attributes using private setters. @@ -127,11 +86,11 @@ def __init__( self._set_rod_id(rod_id) self._set_point_id(point_id) self._set_coordinate_epsg_code(coordinate_epsg_code) - self._set_point_x(point_x) - self._set_point_y(point_y) - self._set_point_z(point_z) - self._set_rod_length(rod_length) - self._set_ground_plate_z(ground_plate_z) + self._set_x(x) + self._set_y(y) + self._set_z(z) + self._set_vertical_offset(vertical_offset) + self._set_plate_bottom_z(plate_bottom_z) self._set_ground_surface_z(ground_surface_z) self._set_temperature(temperature) self._set_voltage(voltage) @@ -182,79 +141,72 @@ def _set_coordinate_epsg_code(self, value: int) -> None: ) self._coordinate_epsg_code = value - def _set_point_x(self, value: float) -> None: + def _set_x(self, value: float) -> None: """ - Private setter for point_x attribute. + Private setter for x attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'point_x' attribute.") - self._point_x = value + raise TypeError("Expected 'float' type for 'x' attribute.") + self._x = value - def _set_point_y(self, value: float) -> None: + def _set_y(self, value: float) -> None: """ - Private setter for point_y attribute. + Private setter for y attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'point_y' attribute.") - self._point_y = value + raise TypeError("Expected 'float' type 'y' attribute.") + self._y = value - def _set_point_z(self, value: float) -> None: + def _set_z(self, value: float) -> None: """ - Private setter for point_z attribute. + Private setter for z attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'point_z' attribute.") - self._point_z = value + raise TypeError("Expected 'float' type for 'z' attribute.") + self._z = value - def _set_rod_length(self, value: float) -> None: + def _set_vertical_offset(self, value: float) -> None: """ - Private setter for rod_length attribute. + Private setter for vertical_offset attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'rod_length' attribute.") + raise TypeError("Expected 'float' type for 'vertical_offset' attribute.") if value < 0: - raise ValueError("Negative value not allowed for 'rod_length' attribute.") - self._rod_length = value - - def _set_ground_surface_z(self, value: float) -> None: - """ - Private setter for ground_surface_z attribute. - """ - if isinstance(value, int): - value = float(value) - if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'ground_surface_z' attribute.") - self._ground_surface_z = value + raise ValueError( + "Negative value not allowed for 'vertical_offset' attribute." + ) + self._vertical_offset = value - def _set_ground_plate_z(self, value: float) -> None: + def _set_plate_bottom_z(self, value: float) -> None: """ - Private setter for ground_plate_z attribute. + Private setter for plate_bottom_z attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'ground_plate_z' attribute.") - self._ground_plate_z = value + raise TypeError("Expected 'float' type for 'plate_bottom_z' attribute.") + self._plate_bottom_z = value - def _set_ground_plate_z_uncorrected(self, value: float) -> None: + def _set_ground_surface_z(self, value: float | None) -> None: """ - Private setter for ground_plate_z_uncorrected attribute. + Private setter for ground_surface_z attribute. """ - if isinstance(value, int): - value = float(value) - if not isinstance(value, float): - raise TypeError( - "Expected 'float' type for 'ground_plate_z_uncorrected' attribute." - ) - self._ground_plate_z_uncorrected = value + if value is not None: + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError( + "Expected 'float' or 'None' type for 'ground_surface_z' attribute." + ) + self._ground_surface_z = value def _set_temperature(self, value: float | None) -> None: """ @@ -315,7 +267,7 @@ def point_id(self) -> str: def coordinate_epsg_code(self) -> int: """ The EPSG code of the coordinate reference system used. - EPSG codes can be found in https://epsg.io/ . + EPSG codes can be found in https://epsg.io/. """ return self._coordinate_epsg_code @@ -323,50 +275,62 @@ def coordinate_epsg_code(self) -> int: def coordinate_reference_system(self) -> pyproj.CRS: """ The coordinate reference system used. + It is a `pyproj.CRS` object (see https://pyproj4.github.io/pyproj/stable/api/crs/crs.html). + It is determined based on the `coordinate_epsg_code`. """ return self._coordinate_reference_system @property - def point_x(self) -> float: + def x(self) -> float: """ The X-coordinate of the measurement point. + Units are according to the `coordinate_reference_system`. """ - return self._point_x + return self._x @property - def point_y(self) -> float: + def y(self) -> float: """ The Y-coordinate of the measurement point. + Units are according to the `coordinate_reference_system`. """ - return self._point_y + return self._y @property - def point_z(self) -> float: + def z(self) -> float: """ The Z-coordinate of the measurement point. + It is the top of the settlement rod. + Units are according to the `coordinate_reference_system`. """ - return self._point_z + return self._z @property - def rod_length(self) -> float: + def vertical_offset(self) -> float: """ - The length of the settlement rod. + The vertical offset distance between the measurement point and the bottom of the settlement plate. + It is in principle the rod length plus the plate thickness. + Units are according to the `coordinate_reference_system`. """ - return self._rod_length + return self._vertical_offset @property - def ground_plate_z(self) -> float: + def plate_bottom_z(self) -> float: """ - The corrected Z-coordinate of the ground plate. + The corrected Z-coordinate at the bottom of the settlement plate. + Note that the bottom of the plate is in principle the original ground surface. + Units are according to the `coordinate_reference_system`. """ - return self._ground_plate_z + return self._plate_bottom_z @property - def ground_plate_z_uncorrected(self) -> float: + def plate_bottom_z_uncorrected(self) -> float: """ - The uncorrected Z-coordinate of the ground plate. + The uncorrected Z-coordinate at the bottom of the settlement plate. + It is computed as the difference beteen the Z-coordinate of the measurement point and the vertical offset. + Units are according to the `coordinate_reference_system`. """ - return self.point_z - self.rod_length + return self.z - self.vertical_offset @property def ground_surface_z(self) -> float | None: @@ -392,6 +356,6 @@ def voltage(self) -> float | None: @property def comment(self) -> str: """ - Additional comment about the measurement. + Comment about the measurement. """ return self._comment diff --git a/tests/measurements/test_settlement_rod_measurements.py b/tests/measurements/test_settlement_rod_measurements.py index 8e39f2b..fd1e654 100644 --- a/tests/measurements/test_settlement_rod_measurements.py +++ b/tests/measurements/test_settlement_rod_measurements.py @@ -12,13 +12,13 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: rod_id = "BR_003" point_id = "ZB-02" coordinate_epsg_code = 28992 - point_x = 123340.266 - point_y = 487597.154 - point_z = 0.807 - rod_length = 2.0 + x = 123340.266 + y = 487597.154 + z = 0.807 + vertical_offset = 2.0 ground_surface_z = 0.419 - ground_plate_z = -1.193 - ground_plate_z_uncorrected = -1.193 + plate_bottom_z = -1.193 + plate_bottom_z_uncorrected = -1.193 temperature = 12.0 voltage = 4232 comment = "No comment" @@ -29,12 +29,12 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: rod_id=rod_id, point_id=point_id, coordinate_epsg_code=coordinate_epsg_code, - point_x=point_x, - point_y=point_y, - point_z=point_z, - rod_length=rod_length, + x=x, + y=y, + z=z, + vertical_offset=vertical_offset, ground_surface_z=ground_surface_z, - ground_plate_z=ground_plate_z, + plate_bottom_z=plate_bottom_z, temperature=temperature, voltage=voltage, comment=comment, @@ -44,13 +44,13 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: assert measurement.rod_id == rod_id assert measurement.point_id == point_id assert measurement.coordinate_epsg_code == coordinate_epsg_code - assert measurement.point_x == point_x - assert measurement.point_y == point_y - assert measurement.point_z == point_z - assert measurement.rod_length == rod_length + assert measurement.x == x + assert measurement.y == y + assert measurement.z == z + assert measurement.vertical_offset == vertical_offset assert measurement.ground_surface_z == ground_surface_z - assert measurement.ground_plate_z == ground_plate_z - assert measurement.ground_plate_z_uncorrected == ground_plate_z_uncorrected + assert measurement.plate_bottom_z == plate_bottom_z + assert measurement.plate_bottom_z_uncorrected == plate_bottom_z_uncorrected assert measurement.temperature == temperature assert measurement.voltage == voltage assert measurement.comment == comment @@ -66,12 +66,12 @@ def test_settlement_rod_measurement_init_with_invalid_date_time() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -87,12 +87,12 @@ def test_settlement_rod_measurement_init_with_invalid_rod_id() -> None: rod_id=None, point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -105,12 +105,12 @@ def test_settlement_rod_measurement_init_with_invalid_rod_id() -> None: rod_id="", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -126,12 +126,12 @@ def test_settlement_rod_measurement_init_with_invalid_point_id() -> None: rod_id="BR_003", point_id=None, coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -144,12 +144,12 @@ def test_settlement_rod_measurement_init_with_invalid_point_id() -> None: rod_id="BR_003", point_id="", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -165,12 +165,12 @@ def test_settlement_rod_measurement_init_with_invalid_coordinate_epsg_code() -> rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=None, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -183,12 +183,12 @@ def test_settlement_rod_measurement_init_with_invalid_coordinate_epsg_code() -> rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=-28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -204,12 +204,12 @@ def test_settlement_rod_measurement_init_with_invalid_point_x() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x="123340.266", - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x="123340.266", + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -225,12 +225,12 @@ def test_settlement_rod_measurement_init_with_invalid_point_y() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=None, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=None, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -246,12 +246,12 @@ def test_settlement_rod_measurement_init_with_invalid_point_z() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z="0.807", - rod_length=2.0, + x=123340.266, + y=487597.154, + z="0.807", + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -267,12 +267,12 @@ def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length="2.0", + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset="2.0", ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -285,12 +285,12 @@ def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=-2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=-2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -306,12 +306,12 @@ def test_settlement_rod_measurement_init_with_invalid_ground_surface_z() -> None rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z="-0.419", - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", @@ -327,12 +327,12 @@ def test_settlement_rod_measurement_init_with_invalid_ground_plate_z() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z="1.193", + plate_bottom_z="1.193", temperature=12.0, voltage=4232, comment="No comment", @@ -348,12 +348,12 @@ def test_settlement_rod_measurement_init_with_invalid_temperature() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature="12.0", voltage=4232, comment="No comment", @@ -369,12 +369,12 @@ def test_settlement_rod_measurement_init_with_invalid_voltage() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage="4232", comment="No comment", @@ -390,12 +390,12 @@ def test_settlement_rod_measurement_init_with_invalid_comment() -> None: rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992, - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment=123, @@ -412,12 +412,12 @@ def test_settlement_rod_measurement_init_with_invalid_coordinate_reference_syste rod_id="BR_003", point_id="ZB-02", coordinate_epsg_code=28992111, # no coordinate system corresponds to this epsg code. - point_x=123340.266, - point_y=487597.154, - point_z=0.807, - rod_length=2.0, + x=123340.266, + y=487597.154, + z=0.807, + vertical_offset=2.0, ground_surface_z=0.419, - ground_plate_z=-1.193, + plate_bottom_z=-1.193, temperature=12.0, voltage=4232, comment="No comment", From 993ec083598e5c3176eb58a6f6fa69f68bcd5138 Mon Sep 17 00:00:00 2001 From: Pablo Vasconez Date: Thu, 6 Jun 2024 16:53:15 +0200 Subject: [PATCH 3/7] refactor(settlement_rod_measurement): refactor SettlementRodMeasurement according to comments from Michel (CRUX) and Daniel (BouwRisk) --- .github/workflows/lint_test.yaml | 30 +- pyproject.toml | 3 +- requirements.txt | 92 ++++- src/baec/measurements/measurement_device.py | 90 +++++ ...ments.py => settlement_rod_measurement.py} | 231 ++++++----- src/baec/project.py | 87 +++++ tests/measurements/test_measurement_device.py | 55 +++ ....py => test_settlement_rod_measurement.py} | 362 +++++++++--------- tests/test_project.py | 50 +++ 9 files changed, 703 insertions(+), 297 deletions(-) create mode 100644 src/baec/measurements/measurement_device.py rename src/baec/measurements/{settlement_rod_measurements.py => settlement_rod_measurement.py} (58%) create mode 100644 src/baec/project.py create mode 100644 tests/measurements/test_measurement_device.py rename tests/measurements/{test_settlement_rod_measurements.py => test_settlement_rod_measurement.py} (53%) create mode 100644 tests/test_project.py diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index d1c864f..fea3ebc 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -38,21 +38,21 @@ jobs: - name: Test run: coverage run -m pytest - - name: Post coverage results - uses: coverallsapp/github-action@v2 - with: - flag-name: run-${{ matrix.python-version }} - parallel: true - - finish: - needs: test - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true + # - name: Post coverage results + # uses: coverallsapp/github-action@v2 + # with: + # flag-name: run-${{ matrix.python-version }} + # parallel: true + + # finish: + # needs: test + # if: ${{ always() }} + # runs-on: ubuntu-latest + # steps: + # - name: Coveralls Finished + # uses: coverallsapp/github-action@v2 + # with: + # parallel-finished: true lint: name: Formatting check diff --git a/pyproject.toml b/pyproject.toml index a9fd8e3..bdca795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ docs = [ "sphinx-autodoc-typehints==1.22", "ipython==8.11.0", "asteroid-sphinx-theme==0.0.3", - "sphinx_rtd_theme==1.2.0", + "sphinx_rtd_theme>1.2,<2", + "enum-tools[sphinx]>0.12,<0.13", ] # lint dependencies from github super-linter v5 # See https://github.com/super-linter/super-linter/tree/main/dependencies/python diff --git a/requirements.txt b/requirements.txt index 74d7557..4ba8d2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,18 +6,30 @@ # alabaster==0.7.16 # via sphinx +apeye==1.4.1 + # via sphinx-toolbox +apeye-core==1.1.5 + # via apeye asteroid-sphinx-theme==0.0.3 # via baec (pyproject.toml) asttokens==2.4.1 # via stack-data +autodocsumm==0.2.12 + # via sphinx-toolbox babel==2.15.0 # via sphinx backcall==0.2.0 # via ipython +beautifulsoup4==4.12.3 + # via sphinx-toolbox black[jupyter]==23.10.1 # via # baec (pyproject.toml) # black +cachecontrol[filecache]==0.14.0 + # via + # cachecontrol + # sphinx-toolbox cems-nuclei[client]==0.5.5 # via # baec (pyproject.toml) @@ -42,26 +54,49 @@ coverage[toml]==7.5.3 # coveralls coveralls==4.0.1 # via baec (pyproject.toml) +cssutils==2.11.1 + # via dict2css cycler==0.12.1 # via matplotlib decorator==5.1.1 # via ipython +dict2css==0.3.0.post1 + # via sphinx-toolbox docopt==0.6.2 # via coveralls docutils==0.18.1 # via # sphinx + # sphinx-prompt # sphinx-rtd-theme + # sphinx-tabs + # sphinx-toolbox +domdf-python-tools==3.8.1 + # via + # apeye + # apeye-core + # dict2css + # sphinx-toolbox +enum-tools[sphinx]==0.12.0 + # via baec (pyproject.toml) exceptiongroup==1.2.1 # via pytest executing==2.0.1 # via stack-data +filelock==3.14.0 + # via + # cachecontrol + # sphinx-toolbox flake8==6.0.0 # via baec (pyproject.toml) fonttools==4.53.0 # via matplotlib +html5lib==1.1 + # via sphinx-toolbox idna==3.7 - # via requests + # via + # apeye-core + # requests imagesize==1.4.1 # via sphinx importlib-metadata==7.1.0 @@ -83,13 +118,17 @@ isort==5.12.0 jedi==0.19.1 # via ipython jinja2==3.1.4 - # via sphinx + # via + # sphinx + # sphinx-jinja2-compat jupyterlab-widgets==3.0.11 # via ipywidgets kiwisolver==1.4.5 # via matplotlib markupsafe==2.1.5 - # via jinja2 + # via + # jinja2 + # sphinx-jinja2-compat matplotlib==3.9.0 # via baec (pyproject.toml) matplotlib-inline==0.1.7 @@ -98,6 +137,10 @@ mccabe==0.7.0 # via # baec (pyproject.toml) # flake8 +more-itertools==10.2.0 + # via cssutils +msgpack==1.0.8 + # via cachecontrol mypy==1.6.1 # via baec (pyproject.toml) mypy-extensions==1.0.0 @@ -105,6 +148,8 @@ mypy-extensions==1.0.0 # baec (pyproject.toml) # black # mypy +natsort==8.4.0 + # via domdf-python-tools numpy==1.26.4 # via # baec (pyproject.toml) @@ -140,6 +185,7 @@ pillow==10.3.0 # via matplotlib platformdirs==3.5.1 # via + # apeye # baec (pyproject.toml) # black pluggy==1.5.0 @@ -160,8 +206,11 @@ pyflakes==3.0.1 # flake8 pygments==2.18.0 # via + # enum-tools # ipython # sphinx + # sphinx-prompt + # sphinx-tabs pyjwt==2.6.0 # via cems-nuclei pyparsing==3.1.2 @@ -178,26 +227,52 @@ pytz==2024.1 # via pandas requests==2.32.3 # via + # apeye + # cachecontrol # cems-nuclei # coveralls # sphinx +ruamel-yaml==0.18.6 + # via sphinx-toolbox +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml six==1.16.0 # via # asttokens + # html5lib # python-dateutil snowballstemmer==2.2.0 # via sphinx +soupsieve==2.5 + # via beautifulsoup4 sphinx==6.1.3 # via # asteroid-sphinx-theme + # autodocsumm # baec (pyproject.toml) + # enum-tools # sphinx-autodoc-typehints + # sphinx-prompt # sphinx-rtd-theme + # sphinx-tabs + # sphinx-toolbox # sphinxcontrib-jquery sphinx-autodoc-typehints==1.22 + # via + # baec (pyproject.toml) + # sphinx-toolbox +sphinx-jinja2-compat==0.2.0.post1 + # via + # enum-tools + # sphinx-toolbox +sphinx-prompt==1.6.0 + # via sphinx-toolbox +sphinx-rtd-theme==2.0.0 # via baec (pyproject.toml) -sphinx-rtd-theme==1.2.0 - # via baec (pyproject.toml) +sphinx-tabs==3.4.5 + # via sphinx-toolbox +sphinx-toolbox==3.5.0 + # via enum-tools sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -214,6 +289,8 @@ sphinxcontrib-serializinghtml==1.1.10 # via sphinx stack-data==0.6.3 # via ipython +tabulate==0.9.0 + # via sphinx-toolbox tokenize-rt==5.2.0 # via black tomli==2.0.1 @@ -241,13 +318,18 @@ typing-extensions==4.7.1 # via # baec (pyproject.toml) # black + # domdf-python-tools + # enum-tools # mypy + # sphinx-toolbox tzdata==2024.1 # via pandas urllib3==2.2.1 # via requests wcwidth==0.2.13 # via prompt-toolkit +webencodings==0.5.1 + # via html5lib widgetsnbextension==4.0.11 # via ipywidgets zipp==3.19.1 diff --git a/src/baec/measurements/measurement_device.py b/src/baec/measurements/measurement_device.py new file mode 100644 index 0000000..930c770 --- /dev/null +++ b/src/baec/measurements/measurement_device.py @@ -0,0 +1,90 @@ +from __future__ import annotations + + +class MeasurementDevice: + """ + Represents a measurement device. + """ + + def __init__( + self, + id_: str, + qr_code: str | None = None, + ) -> None: + """ + Initializes a MeasurementDevice object. + + Parameters + ---------- + id_ : str + The ID of the measurement device. + qr_code : str | None, optional + The QR code of the measurement device, or None if unknown (default: None). + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If empty string for `id_` or `qr_code`. + """ + + # Initialize all attributes using private setters. + self._set_id(id_) + self._set_qr_code(qr_code) + + def _set_id(self, value: str) -> None: + """ + Private setter for id attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'id' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'id' attribute.") + self._id = value + + def _set_qr_code(self, value: str | None) -> None: + """ + Private setter for qr_code attribute. + """ + if value is not None: + if not isinstance(value, str): + raise TypeError( + "Expected 'str' or 'None' type for 'qr_code' attribute." + ) + if value == "": + raise ValueError("Empty string not allowed for 'qr_code' attribute.") + self._qr_code = value + + @property + def id(self) -> str: + """ + The ID of the measurement device. + """ + return self._id + + @property + def qr_code(self) -> str | None: + """ + The QR-code of the measurement device. + """ + return self._qr_code + + def __eq__(self, other: object) -> bool: + """ + Check if two MeasurementDevice objects are equal. + It compares the `id` attribute. + + Parameters + ---------- + other : object + The object to compare. + + Returns + ------- + bool + True if the objects are equal, False otherwise. + """ + if not isinstance(other, MeasurementDevice): + return False + return self.id == other.id diff --git a/src/baec/measurements/settlement_rod_measurements.py b/src/baec/measurements/settlement_rod_measurement.py similarity index 58% rename from src/baec/measurements/settlement_rod_measurements.py rename to src/baec/measurements/settlement_rod_measurement.py index 5535b84..e3bc184 100644 --- a/src/baec/measurements/settlement_rod_measurements.py +++ b/src/baec/measurements/settlement_rod_measurement.py @@ -1,9 +1,27 @@ from __future__ import annotations import datetime +from enum import Enum import pyproj +from baec.measurements.measurement_device import MeasurementDevice +from baec.project import Project + + +class SettlementRodMeasurementStatus(Enum): + """Represents the status of a settlement rod measurement.""" + + OK = "ok" + DISTURBED = "disturbed" + EXPIRED = "expired" + RELOCATED = "relocated" + ROD_IS_EXTENDED = "rod_is_extended" + CROOKED = "crooked" + DESELECTED = "deselected" + FICTIONAL = "fictional" + UNKNOWN = "unknown" + class SettlementRodMeasurement: """ @@ -12,34 +30,40 @@ class SettlementRodMeasurement: def __init__( self, + project: Project, + device: MeasurementDevice, + object_id: str, date_time: datetime.datetime, - rod_id: str, - point_id: str, - coordinate_epsg_code: int, + coordinate_reference_system: pyproj.CRS, x: float, y: float, z: float, - vertical_offset: float, + rod_length: float, plate_bottom_z: float, - ground_surface_z: float | None = None, + ground_surface_z: float, + status: SettlementRodMeasurementStatus, temperature: float | None = None, voltage: float | None = None, - comment: str = "No comment", + comment: str = "", ) -> None: """ Initializes a SettlementRodMeasurement object. Parameters ---------- + project : Project + The project which the measurement belongs to. + device : MeasurementDevice + The measurement device. + date_time : datetime.datetime + The date and time of the measurement. + object_id : str + The ID of the measured object. date_time : datetime.datetime The date and time of the measurement. - rod_id : str - The ID of the settlement rod. - point_id : str - The ID of the measurement point. - coordinate_epsg_code : int - The EPSG code of the coordinate reference system used. - EPSG codes can be found in https://epsg.io/. + coordinate_reference_system : pyproj.CRS + The coordinate reference system of the spatial measurements. + It is a `pyproj.CRS` object (see https://pyproj4.github.io/pyproj/stable/api/crs/crs.html). x : float The X-coordinate of the measurement point. Units are according to the `coordinate_reference_system`. y : float @@ -48,98 +72,98 @@ def __init__( The Z-coordinate of the measurement point. It is the top of the settlement rod. Units are according to the `coordinate_reference_system`. - vertical_offset : float - The vertical offset distance between the measurement point and the bottom of the settlement plate. - It is in principle the rod length plus the plate thickness. + rod_length : float + The length of the settlement rod including the thickness of the settlement plate. + It is in principle the vertical distance between the measurement point and the bottom of the settlement plate. Units are according to the `coordinate_reference_system`. plate_bottom_z : float The corrected Z-coordinate at the bottom of the settlement plate. Note that the bottom of the plate is in principle the original ground surface. Units are according to the `coordinate_reference_system`. - ground_surface_z : float | None, optional - The Z-coordinate of the ground surface, or None if unknown (default: None). - Notes: - - This value in principle corresponds to the top of the fill, if present. - - This value will be typically measured using radar measurements. + ground_surface_z : float + The Z-coordinate of the ground surface. + It is in principle the top of the fill, if present. Units are according to the `coordinate_reference_system`. + status: SettlementRodMeasurementStatus + The status of the measurement. temperature : float or None, optional The temperature at the time of measurement in [°C], or None if unknown (default: None). voltage : float or None, optional The voltage measured in [mV], or None if unknown (default: None). comment : str, optional - Comment about the measurement (default: "No comment"). + Additional comment about the measurement (default: ""). Raises ------ TypeError If the input types are incorrect. ValueError - If empty string for `rod_id` or `point_id`. - ValueError - If negative value for `coordinate_epsg_code` or `vertical_offset`. - pyproj.exceptions.CRSError - If no valid coordinate reference system is found for the given `coordinate_epsg_code`. + If empty string for `object_id`. + If negative value for `rod_length`. """ # Initialize all attributes using private setters. + self._set_project(project) + self._set_device(device) + self._set_object_id(object_id) self._set_date_time(date_time) - self._set_rod_id(rod_id) - self._set_point_id(point_id) - self._set_coordinate_epsg_code(coordinate_epsg_code) + self._set_coordinate_reference_system(coordinate_reference_system) self._set_x(x) self._set_y(y) self._set_z(z) - self._set_vertical_offset(vertical_offset) + self._set_rod_length(rod_length) self._set_plate_bottom_z(plate_bottom_z) self._set_ground_surface_z(ground_surface_z) + self._set_status(status) self._set_temperature(temperature) self._set_voltage(voltage) self._set_comment(comment) - # Set the coordinate reference system based on the EPSG code - self._coordinate_reference_system = pyproj.CRS.from_user_input( - coordinate_epsg_code - ) + def _set_project(self, value: Project) -> None: + """ + Private setter for project attribute. + """ + if not isinstance(value, Project): + raise TypeError("Expected 'Project' type for 'project' attribute.") + self._project = value - def _set_date_time(self, value: datetime.datetime) -> None: + def _set_device(self, value: MeasurementDevice) -> None: """ - Private setter for date attribute. + Private setter for device attribute. """ - if not isinstance(value, datetime.datetime): - raise TypeError("Expected 'datetime.datetime' type for 'date' attribute.") - self._date_time = value + if not isinstance(value, MeasurementDevice): + raise TypeError("Expected 'MeasurementDevice' type for 'device' attribute.") + self._device = value - def _set_rod_id(self, value: str) -> None: + def _set_object_id(self, value: str) -> None: """ - Private setter for rod_id attribute. + Private setter for object_id attribute. """ if not isinstance(value, str): - raise TypeError("Expected 'str' type for 'rod_id' attribute.") + raise TypeError("Expected 'str' type for 'object_id' attribute.") if value == "": - raise ValueError("Empty string not allowed for 'rod_id' attribute.") - self._rod_id = value + raise ValueError("Empty string not allowed for 'object_id' attribute.") + self._object_id = value - def _set_point_id(self, value: str) -> None: + def _set_date_time(self, value: datetime.datetime) -> None: """ - Private setter for point_id attribute. + Private setter for date_time attribute. """ - if not isinstance(value, str): - raise TypeError("Expected 'str' type for 'point_id' attribute.") - if value == "": - raise ValueError("Empty string not allowed for 'point_id' attribute.") - self._point_id = value + if not isinstance(value, datetime.datetime): + raise TypeError( + "Expected 'datetime.datetime' type for 'date_time' attribute." + ) + self._date_time = value - def _set_coordinate_epsg_code(self, value: int) -> None: + def _set_coordinate_reference_system(self, value: pyproj.CRS) -> None: """ - Private setter for coordinate_epsg_code attribute. + Private setter for coordinate_reference_system attribute. """ - if not isinstance(value, int): - raise TypeError("Expected 'int' type for 'coordinate_epsg_code' attribute.") - if value < 0: - raise ValueError( - "Negative value not allowed for 'coordinate_epsg_code' attribute." + if not isinstance(value, pyproj.CRS): + raise TypeError( + "Expected 'pyproj.CRS' type for 'coordinate_reference_system' attribute." ) - self._coordinate_epsg_code = value + self._coordinate_reference_system = value def _set_x(self, value: float) -> None: """ @@ -171,19 +195,17 @@ def _set_z(self, value: float) -> None: raise TypeError("Expected 'float' type for 'z' attribute.") self._z = value - def _set_vertical_offset(self, value: float) -> None: + def _set_rod_length(self, value: float) -> None: """ - Private setter for vertical_offset attribute. + Private setter for rod_length attribute. """ if isinstance(value, int): value = float(value) if not isinstance(value, float): - raise TypeError("Expected 'float' type for 'vertical_offset' attribute.") + raise TypeError("Expected 'float' type for 'rod_length' attribute.") if value < 0: - raise ValueError( - "Negative value not allowed for 'vertical_offset' attribute." - ) - self._vertical_offset = value + raise ValueError("Negative value not allowed for 'rod_length' attribute.") + self._rod_length = value def _set_plate_bottom_z(self, value: float) -> None: """ @@ -195,19 +217,26 @@ def _set_plate_bottom_z(self, value: float) -> None: raise TypeError("Expected 'float' type for 'plate_bottom_z' attribute.") self._plate_bottom_z = value - def _set_ground_surface_z(self, value: float | None) -> None: + def _set_ground_surface_z(self, value: float) -> None: """ Private setter for ground_surface_z attribute. """ - if value is not None: - if isinstance(value, int): - value = float(value) - if not isinstance(value, float): - raise TypeError( - "Expected 'float' or 'None' type for 'ground_surface_z' attribute." - ) + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + raise TypeError("Expected 'float' type for 'ground_surface_z' attribute.") self._ground_surface_z = value + def _set_status(self, value: SettlementRodMeasurementStatus) -> None: + """ + Private setter for status attribute. + """ + if not isinstance(value, SettlementRodMeasurementStatus): + raise TypeError( + "Expected 'SettlementRodMeasurementStatus' type for 'status' attribute." + ) + self._status = value + def _set_temperature(self, value: float | None) -> None: """ Private setter for temperature attribute. @@ -243,40 +272,38 @@ def _set_comment(self, value: str) -> None: self._comment = value @property - def date_time(self) -> datetime.datetime: + def project(self) -> Project: """ - The date and time of the measurement. + The project which the measurement belongs to. """ - return self._date_time + return self._project @property - def rod_id(self) -> str: + def device(self) -> MeasurementDevice: """ - The ID of the settlement rod. + The measurement device. """ - return self._rod_id + return self._device @property - def point_id(self) -> str: + def object_id(self) -> str: """ - The ID of the measurement point. + The ID of the measured object. """ - return self._point_id + return self._object_id @property - def coordinate_epsg_code(self) -> int: + def date_time(self) -> datetime.datetime: """ - The EPSG code of the coordinate reference system used. - EPSG codes can be found in https://epsg.io/. + The date and time of the measurement. """ - return self._coordinate_epsg_code + return self._date_time @property def coordinate_reference_system(self) -> pyproj.CRS: """ - The coordinate reference system used. + The coordinate reference system of the spatial measurements. It is a `pyproj.CRS` object (see https://pyproj4.github.io/pyproj/stable/api/crs/crs.html). - It is determined based on the `coordinate_epsg_code`. """ return self._coordinate_reference_system @@ -306,13 +333,13 @@ def z(self) -> float: return self._z @property - def vertical_offset(self) -> float: + def rod_length(self) -> float: """ - The vertical offset distance between the measurement point and the bottom of the settlement plate. - It is in principle the rod length plus the plate thickness. + The length of the settlement rod including the thickness of the settlement plate. + It is in principle the vertical distance between the measurement point and the bottom of the settlement plate. Units are according to the `coordinate_reference_system`. """ - return self._vertical_offset + return self._rod_length @property def plate_bottom_z(self) -> float: @@ -330,15 +357,23 @@ def plate_bottom_z_uncorrected(self) -> float: It is computed as the difference beteen the Z-coordinate of the measurement point and the vertical offset. Units are according to the `coordinate_reference_system`. """ - return self.z - self.vertical_offset + return self.z - self.rod_length @property - def ground_surface_z(self) -> float | None: + def ground_surface_z(self) -> float: """ - The Z-coordinate of the ground surface, or None if unknown. + The Z-coordinate of the ground surface. + It is in principle the top of the fill, if present. """ return self._ground_surface_z + @property + def status(self) -> SettlementRodMeasurementStatus: + """ + The status of the measurement. + """ + return self._status + @property def temperature(self) -> float | None: """ @@ -356,6 +391,6 @@ def voltage(self) -> float | None: @property def comment(self) -> str: """ - Comment about the measurement. + Additional comment about the measurement. """ return self._comment diff --git a/src/baec/project.py b/src/baec/project.py new file mode 100644 index 0000000..0336834 --- /dev/null +++ b/src/baec/project.py @@ -0,0 +1,87 @@ +from __future__ import annotations + + +class Project: + """ + Represents a project. + """ + + def __init__( + self, + id_: str, + name: str, + ) -> None: + """ + Initializes a MeasurementDevice object. + + Parameters + ---------- + id_ : str + The ID of the project. + name : str + The name of the project. + + Raises + ------ + TypeError + If the input types are incorrect. + ValueError + If empty string for `id_` or `name`. + """ + + # Initialize all attributes using private setters. + self._set_id(id_) + self._set_name(name) + + def _set_id(self, value: str) -> None: + """ + Private setter for id attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'id' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'id' attribute.") + self._id = value + + def _set_name(self, value: str) -> None: + """ + Private setter for name attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'name' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'name' attribute.") + self._name = value + + @property + def id(self) -> str: + """ + The ID of the project. + """ + return self._id + + @property + def name(self) -> str: + """ + The name of the project. + """ + return self._name + + def __eq__(self, other: object) -> bool: + """ + Check if two MeasurementDevice objects are equal. + It compares the `id` attribute and `name` attribute. + + Parameters + ---------- + other : object + The object to compare. + + Returns + ------- + bool + True if the objects are equal, False otherwise. + """ + if not isinstance(other, Project): + return False + return self.id == other.id and self.name == other.name diff --git a/tests/measurements/test_measurement_device.py b/tests/measurements/test_measurement_device.py new file mode 100644 index 0000000..9c4d85c --- /dev/null +++ b/tests/measurements/test_measurement_device.py @@ -0,0 +1,55 @@ +import pytest + +from baec.measurements.measurement_device import MeasurementDevice + + +def test_measurement_device_with_valid_input() -> None: + """Test initialization of MeasurementDevice with valid input.""" + # With QR code + device = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + assert device.id == "device_1" + assert device.qr_code == "qr_code_1" + + # Without QR code + device = MeasurementDevice(id_="device_1") + assert device.id == "device_1" + assert device.qr_code is None + + +def test_measurement_device_init_with_invalid_id() -> None: + """Test initialization of MeasurementDevice with invalid ID.""" + # Invalid id: None + with pytest.raises(TypeError, match="id"): + MeasurementDevice(id_=None) + + # Invalid id: Empty string + with pytest.raises(ValueError, match="id"): + MeasurementDevice(id_="") + + +def test_measurement_device_init_with_invalid_qr_code() -> None: + """Test initialization of MeasurementDevice with invalid QR-code.""" + # Invalid id: Integer value + with pytest.raises(TypeError, match="qr_code"): + MeasurementDevice(id_="device_1", qr_code=1) + + # Invalid id: Empty string + with pytest.raises(ValueError, match="qr_code"): + MeasurementDevice(id_="device_1", qr_code="") + + +def test_measurement_device__eq__method() -> None: + """Test the __eq__ method of MeasurementDevice.""" + device_1 = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + device_2 = MeasurementDevice(id_="device_1", qr_code="qr_code_1") + device_3 = MeasurementDevice(id_="device_2", qr_code="qr_code_2") + device_4 = MeasurementDevice(id_="device_2", qr_code="qr_code_1") + + assert device_1 == device_2 + assert device_1 != device_3 + assert device_1 != device_4 + assert device_2 != device_3 + assert device_3 == device_4 + + assert device_1 == device_1 + assert device_1 != "device_1" diff --git a/tests/measurements/test_settlement_rod_measurements.py b/tests/measurements/test_settlement_rod_measurement.py similarity index 53% rename from tests/measurements/test_settlement_rod_measurements.py rename to tests/measurements/test_settlement_rod_measurement.py index fd1e654..455caa2 100644 --- a/tests/measurements/test_settlement_rod_measurements.py +++ b/tests/measurements/test_settlement_rod_measurement.py @@ -3,255 +3,269 @@ import pyproj import pytest -from baec.measurements.settlement_rod_measurements import SettlementRodMeasurement +from baec.measurements.measurement_device import MeasurementDevice +from baec.measurements.settlement_rod_measurement import ( + SettlementRodMeasurement, + SettlementRodMeasurementStatus, +) +from baec.project import Project def test_settlement_rod_measurement_init_with_valid_input() -> None: """Test initialization of settlement rod measurement with valid input.""" + project = Project(id_="P-001", name="Project 1") + device = MeasurementDevice(id_="BR_003", qr_code="QR-003") + object_id = "ZB-02" date_time = datetime.datetime(2024, 4, 9, 4, 0, 0) - rod_id = "BR_003" - point_id = "ZB-02" - coordinate_epsg_code = 28992 + coordinate_reference_system = pyproj.CRS.from_user_input(28992) x = 123340.266 y = 487597.154 z = 0.807 - vertical_offset = 2.0 - ground_surface_z = 0.419 + rod_length = 2.0 plate_bottom_z = -1.193 - plate_bottom_z_uncorrected = -1.193 + ground_surface_z = 0.419 + status = SettlementRodMeasurementStatus.OK temperature = 12.0 voltage = 4232 comment = "No comment" - coordinate_reference_system = pyproj.CRS.from_user_input(coordinate_epsg_code) + plate_bottom_z_uncorrected = -1.193 measurement = SettlementRodMeasurement( + project=project, + device=device, + object_id=object_id, date_time=date_time, - rod_id=rod_id, - point_id=point_id, - coordinate_epsg_code=coordinate_epsg_code, + coordinate_reference_system=coordinate_reference_system, x=x, y=y, z=z, - vertical_offset=vertical_offset, - ground_surface_z=ground_surface_z, + rod_length=rod_length, plate_bottom_z=plate_bottom_z, + ground_surface_z=ground_surface_z, + status=status, temperature=temperature, voltage=voltage, comment=comment, ) + assert measurement.project == project + assert measurement.device == device + assert measurement.object_id == object_id assert measurement.date_time == date_time - assert measurement.rod_id == rod_id - assert measurement.point_id == point_id - assert measurement.coordinate_epsg_code == coordinate_epsg_code + assert measurement.coordinate_reference_system == coordinate_reference_system assert measurement.x == x assert measurement.y == y assert measurement.z == z - assert measurement.vertical_offset == vertical_offset + assert measurement.rod_length == rod_length assert measurement.ground_surface_z == ground_surface_z assert measurement.plate_bottom_z == plate_bottom_z + assert measurement.status == status assert measurement.plate_bottom_z_uncorrected == plate_bottom_z_uncorrected assert measurement.temperature == temperature assert measurement.voltage == voltage assert measurement.comment == comment - assert measurement.coordinate_reference_system == coordinate_reference_system -def test_settlement_rod_measurement_init_with_invalid_date_time() -> None: - """Test initialization of settlement rod measurement with invalid date_time.""" - # Invalid date_time: None - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_project() -> None: + """Test initialization of settlement rod measurement with invalid project.""" + # Invalid project: None + with pytest.raises(TypeError, match="project"): SettlementRodMeasurement( - date_time=None, - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + project=None, + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_rod_id() -> None: - """Test initialization of settlement rod measurement with invalid rod_id.""" - # Invalid rod_id: None - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_device() -> None: + """Test initialization of settlement rod measurement with invalid device.""" + # Invalid device: None + with pytest.raises(TypeError, match="device"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=None, + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id=None, - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - # Invalid rod_id: Empty string - with pytest.raises(ValueError): - SettlementRodMeasurement( - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="", - point_id="ZB-02", - coordinate_epsg_code=28992, - x=123340.266, - y=487597.154, - z=0.807, - vertical_offset=2.0, ground_surface_z=0.419, - plate_bottom_z=-1.193, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_point_id() -> None: - """Test initialization of settlement rod measurement with invalid point_id.""" +def test_settlement_rod_measurement_init_with_invalid_object_id() -> None: + """Test initialization of settlement rod measurement with invalid object_id.""" # Invalid point_id: None - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="object_id"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id=None, date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id=None, - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) # Invalid rod_id: Empty string - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="object_id"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_coordinate_epsg_code() -> None: - """Test initialization of settlement rod measurement with invalid coordinate_epsg_code.""" - # Invalid coordinate_epsg_code: None - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_date_time() -> None: + """Test initialization of settlement rod measurement with invalid date_time.""" + # Invalid date_time: None + with pytest.raises(TypeError, match="date_time"): SettlementRodMeasurement( - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=None, + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", + date_time=None, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) - # Invalid coordinate_epsg_code: Negative value - with pytest.raises(ValueError): + +def test_settlement_rod_measurement_init_with_invalid_coordinate_reference_system() -> ( + None +): + """Test initialization of settlement rod measurement with invalid coordinate reference system.""" + # Invalid coordinate_reference_system: None + with pytest.raises(TypeError, match="coordinate_reference_system"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=-28992, + coordinate_reference_system=None, x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_point_x() -> None: - """Test initialization of settlement rod measurement with invalid point_x.""" - # Invalid point_x: String value - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_x() -> None: + """Test initialization of settlement rod measurement with invalid x.""" + # Invalid x: String value + with pytest.raises(TypeError, match="x"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x="123340.266", y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_point_y() -> None: - """Test initialization of settlement rod measurement with invalid point_y.""" - # Invalid point_y: None - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_y() -> None: + """Test initialization of settlement rod measurement with invalid y.""" + # Invalid y: None + with pytest.raises(TypeError, match="y"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=None, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_point_z() -> None: - """Test initialization of settlement rod measurement with invalid point_z.""" - # Invalid point_z: String value - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_z() -> None: + """Test initialization of settlement rod measurement with invalid z.""" + # Invalid z: String value + with pytest.raises(TypeError, match="z"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z="0.807", - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", @@ -261,36 +275,40 @@ def test_settlement_rod_measurement_init_with_invalid_point_z() -> None: def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: """Test initialization of settlement rod measurement with invalid rod_length.""" # Invalid point_z: String value - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="rod_length"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset="2.0", - ground_surface_z=0.419, + rod_length="2.0", plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) # Invalid rod_length: Negative value - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="rod_length"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=-2.0, - ground_surface_z=0.419, + rod_length=-2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", @@ -300,39 +318,43 @@ def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: def test_settlement_rod_measurement_init_with_invalid_ground_surface_z() -> None: """Test initialization of settlement rod measurement with invalid ground_surface_z.""" # Invalid ground_surface_z: String value - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="ground_surface_z"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z="-0.419", + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z="0.419", + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", ) -def test_settlement_rod_measurement_init_with_invalid_ground_plate_z() -> None: - """Test initialization of settlement rod measurement with invalid ground_plate_z.""" - # Invalid ground_plate_z: String value - with pytest.raises(TypeError): +def test_settlement_rod_measurement_init_with_invalid_plate_bottom_z() -> None: + """Test initialization of settlement rod measurement with invalid plate_bottom_z.""" + # Invalid plate_bottom_z: String value + with pytest.raises(TypeError, match="plate_bottom_z"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, + rod_length=2.0, + plate_bottom_z="-1.193", ground_surface_z=0.419, - plate_bottom_z="1.193", + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, comment="No comment", @@ -342,18 +364,20 @@ def test_settlement_rod_measurement_init_with_invalid_ground_plate_z() -> None: def test_settlement_rod_measurement_init_with_invalid_temperature() -> None: """Test initialization of settlement rod measurement with invalid temperature.""" # Invalid temperature: String value - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="temperature"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature="12.0", voltage=4232, comment="No comment", @@ -363,18 +387,20 @@ def test_settlement_rod_measurement_init_with_invalid_temperature() -> None: def test_settlement_rod_measurement_init_with_invalid_voltage() -> None: """Test initialization of settlement rod measurement with invalid voltage.""" # Invalid voltage: String value - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="voltage"): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, + ground_surface_z=0.419, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage="4232", comment="No comment", @@ -386,39 +412,19 @@ def test_settlement_rod_measurement_init_with_invalid_comment() -> None: # Invalid comment: Integer value with pytest.raises(TypeError): SettlementRodMeasurement( + project=Project(id_="P-001", name="Project 1"), + device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), + object_id="ZB-02", date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992, + coordinate_reference_system=pyproj.CRS.from_user_input(28992), x=123340.266, y=487597.154, z=0.807, - vertical_offset=2.0, - ground_surface_z=0.419, + rod_length=2.0, plate_bottom_z=-1.193, - temperature=12.0, - voltage=4232, - comment=123, - ) - - -def test_settlement_rod_measurement_init_with_invalid_coordinate_reference_system() -> ( - None -): - """Test initialization of settlement rod measurement with invalid coordinate reference system.""" - with pytest.raises(pyproj.exceptions.CRSError): - SettlementRodMeasurement( - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - rod_id="BR_003", - point_id="ZB-02", - coordinate_epsg_code=28992111, # no coordinate system corresponds to this epsg code. - x=123340.266, - y=487597.154, - z=0.807, - vertical_offset=2.0, ground_surface_z=0.419, - plate_bottom_z=-1.193, + status=SettlementRodMeasurementStatus.OK, temperature=12.0, voltage=4232, - comment="No comment", + comment=123, ) diff --git a/tests/test_project.py b/tests/test_project.py new file mode 100644 index 0000000..9e67ec7 --- /dev/null +++ b/tests/test_project.py @@ -0,0 +1,50 @@ +import pytest + +from baec.project import Project + + +def test_project_with_valid_input() -> None: + """Test initialization of Project with valid input.""" + project = Project(id_="P001", name="name_1") + assert project.id == "P001" + assert project.name == "name_1" + + +def test_project_init_with_invalid_id() -> None: + """Test initialization of Project with invalid ID.""" + # Invalid id: None + with pytest.raises(TypeError, match="id"): + Project(id_=None, name="name_1") + + # Invalid id: Empty string + with pytest.raises(ValueError, match="id"): + Project(id_="", name="name_1") + + +def test_project_init_with_invalid_name() -> None: + """Test initialization of Project with invalid name.""" + # Invalid name: None + with pytest.raises(TypeError, match="name"): + Project(id_="P001", name=None) + + # Invalid name: Empty string + with pytest.raises(ValueError, match="name"): + Project(id_="P001", name="") + + +def test_project__eq__method() -> None: + """Test the __eq__ method of Project.""" + project_1 = Project(id_="P001", name="name_1") + project_2 = Project(id_="P001", name="name_1") + project_3 = Project(id_="P002", name="name_2") + project_4 = Project(id_="P002", name="name_1") + + assert project_1 == project_2 + assert project_1 != project_3 + assert project_1 != project_4 + assert project_2 != project_3 + assert project_3 != project_4 + + assert project_1 == project_1 + assert project_1 != None + assert project_1 != "P001" From 75d4e1e3c9291da12e5a93f14009dfda1f3fc2b8 Mon Sep 17 00:00:00 2001 From: Pablo Vasconez Date: Thu, 6 Jun 2024 16:54:56 +0200 Subject: [PATCH 4/7] feat(settlement_rod_measurement_series): add SettlementRodMeasurementSeries class to represent a series of measurements for a single settlement rod and update docs --- docs/conf.py | 1 + docs/tree/figures/settlement_rod.png | Bin 40356 -> 39160 bytes docs/tree/reference.rst | 49 ++++++- .../settlement_rod_measurement_series.py | 137 ++++++++++++++++++ tests/measurements/conftest.py | 57 ++++++++ .../test_settlement_rod_measurement_series.py | 115 +++++++++++++++ 6 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 src/baec/measurements/settlement_rod_measurement_series.py create mode 100644 tests/measurements/conftest.py create mode 100644 tests/measurements/test_settlement_rod_measurement_series.py diff --git a/docs/conf.py b/docs/conf.py index faa0f36..13c05ba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,7 @@ "sphinx.ext.mathjax", "sphinx_autodoc_typehints", "matplotlib.sphinxext.plot_directive", + "enum_tools.autoenum", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/tree/figures/settlement_rod.png b/docs/tree/figures/settlement_rod.png index d6ccb4aad090faaa2643c586b0c90252129ecf2d..6702a751a532aff294d66e0be983b042cc38be75 100644 GIT binary patch literal 39160 zcmeFZX*8B$+ctbPYm&?*$&i`K96~h6P%_U-<{^ yGSI`-V5?) zR7s>QUrD6RwPc&|lfw2r3;440hP=*A5{djX@!w71Jmd@{(h<^yGpE&EpN;pp>DPAm zOHT{dDYYo*JmjLGWT`&L&&{I8AHmzfU;K2q`0L-isR(x`I{rc%(S+B>!qustByrk4 z4GVtq*C~2%q0-HxuWzYd{fQ`*VMDoD{Yq)Wvjx4l6}P&moAoiQp;ctK1QN;Pkg$f! zzwdrn-@dY!`0oAYt=9j3LVHAe3-L`3>BB+d>+Nl1?Zj6SuT0K%;_H=7cZG?sq}>#m zn~1O4|EImhkF?@{x0udM>0ez_v*c0s^y$;|mCZ(59ufEcUUam4V?8fFe{OD0-Rb-f zo;&{@QCv(!M8{`;s(bhDY4n5yIn0l?`BQPM|LwL*pCl8aj@(78a-5!ngM+$yYcR8% zdUxO}gA#u2oOrdAi}MnvczAfYxfQguB6 Ax-~c4l<1f4zR74Gu>gxt zB;96SmX?+}jkOmS7aub$een1(KR35k_nV3LceWJi^Jj&|#d)tS&ySCf)AH$xiHWre z#cOM7I*+wy(H%3CPf}G=Yndurxu~Y5R;26Z=4N9vKGu=lSLWIM@&XkPPe$~1v4DVp zsh*OB<@vGKuLV3kJy}^3CwodVGBR2cRnz9063_bt*T+g^q^-rJMcVb2R#jD BY8|*4d#3VId)2Uf!3gmP$%WR#sLT8f`(+>tA{Ec#a(ldi?m}rAra=Z0zjpeVz-k zZ{ED|$fNXUJ8>d}!r0Wb;^&RSlCIO!KjmcQJ6;)NWo1QKSz21UO!e~d@(PKHG9AA{ z3exfv`C3&~g!}I F3)krHB7Z< z#e|2`sAau)@#{~2rLV8APjE-JQDtRi%gnjJgHcgY8Y@jc!7pCCn45Ft AvY)hOc(odb`4*ybF$-%}J_VlTUb$g~!xfh?_8}-!Cn3$F2dFxZBPEk=& z>9WuUgg$vf#%xgR$n5jEvQpY(G3UjLj~_q2!0*q9a&U2tv}I_`eT>RbQBxcHUUXAg zYm<@m0R&XUr>3UMoYeRI{Ic~5>%V=|UX 4KRwUvcJyFNqCl$%5K<_*s^Qb#5y`TGtEI}CiqJ@`hS4>~L?Dyo%gdHwqOKoym4 zLVkW2t){v<=d%*GS+fL{xA@;=HUWVrhUyLuGq*QwiQH~f>Ux^zkfdwQwaUjcai*4* z2Pi0x^x0g!%6tmfG4bmci;>%hyW2E0G{VEf6`%5+5D+k~_S?(wG$JCU@m+IsbCP1j z8>=>jvTg3}EBO2>Es2}IAE|IU+S|L$4PQol4f1TvwOnEed7!DNq(rT0zC1S~yp~vF z^7_Y*E4PLaa2@a5U6!a$NlHe3Ie?gw^CJ(>HnXuQwdoY_jhUL5;G^C}$FKi2%*5P$ zx34UMa&>i;$$Ry3*|r@!{Is$SPY7f6!m~eq|6cku<97Dpb4Wi2gI~XXT^Ynw6T~iC zu)aK&y|}n&*^=xu`TeA*Xh(Oq#L1IOLvh}R1 u4~yNnUD>Z3BU1O}%l(?c-cq-gzCLd}6b~uy#p$0)bqGj3UEQwE z&Zy{UM1FEg%F6QcmoHx~MhlIMwq?}*u*c4SNW L9W7^n*R?b^Z{56!*`)F@qxb3`zvSfKj}8cCW@c{PeA>}* z4&jW))!N$n 3wF`z)2Cm(dc`R2__exP z{Kk*>pZxZ9Wa`W=^mz*oJv{RQsj9c!yYJh#Zxt0u1|`neCPVeH0goRa7QL>2<3>(e zTH31l;K0D@Vt>%NbLXBve}4b|eZ-@em)B53oL$GOo+n2Y$(Xlq-=3kFC4Td_5&{cJ zE;5R~V2_qy?>l$VYhU&p42co7TmSLl9s+n}Wd&=0%Kh*0HUR+v+<>08wuPnTNzdgu zv%2V`Y;3OX?o1NSygWQ($bg5)hu*~vj*dn@e_nRu=f^j;-DYNH%ZrP+%eN&ZapF!7 zH5ajvc70`x)YPpBayx!MpIusAc(cbyP=mvrL<+_S*=($@v7Ub{Bq*q;prGpHa!^Rz zxF=mB1N$xc_{oz| abAnLZ-@6(cz^Pyq7jlpG&*Tm-XBuP9i2fhUytv zSy{2tz9Ea)v^#x2UW^sTPHLhw`&^9Jo#}7%BWHZEkdDu)y Rmhb(5tAcf2LMQQr)+2UxLc;=x8-{^M30D6*0SB$z1K)u#J_ea$K-=$1Bye zFfPZNH>q~{N=Zq*E>2)=mrc0QntFL=Lnb#j7w;>ZFpVUx^fQc0%||vNFE4L3>v2$! zT~Bd=3a6ygD7Bh)%L9Bn-dA3}s*D}`vOJiLgG104dt2k}&gsBU&UmcpmkkZQQ0QoQ zG_`eg@%Y;7>L#$*5yy-szvU*Tm;BvEFX5aS7`S)#4Z_foHrv;?;;nsuq=SKh0S5<% z?ALoc_C?Ct+1bf{Mf47EtW(PB>+2Ugjywqsz1ja|Z?ei&v8NO-5)w|hOzO+Zo0zbv z$@?Yk-Mg2D`)1MiphF+OzqL>Nn5S6!?wzSX6iU>`V+{56^;qrNh6X$&Een&8=A`B2 z h=;tpFR}dwQ-J6ECUPT1LSMa(>*|92o(I z?{0C0(K9fhpe>Ads{PdRx_0fF?AH>PsbM|;Gn9;s%4E5Pg<99!zkFdwEJ#W9e)w>Q zi#nhJ(W ?+qI$=d3bn8y$%5aYCWYL5f*0CSGE@Cy`J`WBeJcn4bOEgWNqn}{<~WRULSOq zc*Qd_^e>7or>3XBu-m k zKWA#^8V^>7)6&vz*+G6t#3s_gVeN=)g38sQKorE)RS($*0Rf82=1se1BJ0l4>+0%m z+qSLkDOFMT{LG-Dth|m+)XdjYa_i0^qw71#m>o+flRIx&cfL-VnX^E)ko}548rEA5 zI$N&7Wzm*S!~7B;B|ct;d5!1C4Y6ubzbuqFdZ2BOme#hmF}iUiQmHv=M#k7W{N?wz zC;0gvlk1*8e_k<3^_Gydv^SQmja<&FV)T?(8inYADT6R}O8?>A92_apf5XQb6SjEn zr=uGh9aSKU60>Jv)VFI*-OtR%%>1FQPI;aBjtm7oz3ZP}6*UpZ`rg{J3KOq+pib=U zo)VYr`}e>2>3`o)?OhKVDMv`GO73v0?ta(iSYpL(uCA#`DSHs0tlWDedX%1?-a{pZ zDX~j2xiF%t#5gySEAtJzT8eYroNO5*HmTXVg*3 5*cq&1aQ~|_uD9q2#?AMuZ2X>ZyfCsR?wjw=m-nK2)C-L@m zcI6TUvIMF`z`rd0Q1bT~X=x)PBc2}(?>+;9U$%WTJ2#@Xw2j1+o}675cMuDCyIRP) z-DY(WX(Xtx(2j}mOK+Ze5H9ZHR-G#$$;oEG0l=6nEG)eYnZO2Q%+;Z{q6$3QUIdYi z08t`Mb!6$$(a@Z|S8)IS_PB=Z^z`cb`u3)#5b2|MT#wg={ eZjPJDOy-#>&a#mnIU90Y*98ycr{G&2AJb#Z(E5 zmHc6S-`VaD1HrnDeI8QkUS8|pa<2^*s;sL1FefLG+7dr~14_7h^=iTDZ}QAYAxlc; zfvNJ1`l_nSQrsc(k@@F24)G|hxXu3Fo409OVwc5Pzl($WbhNc8WM$>#1~V!?fA(lR zN0q;^vM`nHyr{_%(rWr#j&Wpcti@JC BmjG1b-89i^-?@?JY?gjOw#KO@!j)|y}Xf`-h*EBzu$8XAoSBZq_JXsT}C_pSfs zd&zvXH4V|!P+#xt>@0Qj_a4tT&ZefO9v-WxlOs o=_MkUv+dg?quf#d4tjx>A X)c*xu_iHOJ;2j^26f4J5U)xai~OcmC|zrj+cCSBQLM-=3nI=9e#H|8j9z&J0wY zJb4n}Z*hHP%90Tk&aCdB?1RF>g})QsXn0HobYx{e4pfDrtoh3R9Pi8<>o7N+=q?;` zDDwCB*DJi9q%+-~rDr3&&WHT?M4k=nnm+k;$0v0PG^KB*msU5{R~mYHPVw^E{QT&< z>pZ%^$nTQqm45vA0Z^%vXV%oy$wqkrMF242T;Xf8u!x8qTZvcW-za;1WT~Z~8%|Dp zckV3m-dO)t5fJr}OD!1?hkMWG_Pdq+xcFfpb#i>VogXFH)^VxTY*djuckTdBz4Ki8 z>$8)J`f>XTR{}8J+`>Y+*P6JXcMl?eGRk20&6~5Rm)HBsdTMGE4GdymJrooXk#e1m zUoB)jf4&m=GTW$py+L|ob+$ozFnjQ82rI9b3vNRHwMi9 DL;!uiI zQB{3y)yBQ#@wd-= d_SV{ zc6WET82lt-M!E&4XKAy&kqhko kEWW=dAN~TQc@CJ0}2rOvh;KX zPU?dU4BefbRhML@TGLu^`{_MHcr<*{(s7f{=l{4$qQgQjhK+%>dy}4?&L1R{a%u{f zdgaO$UEOGee^%B+zGce*lc8MQvy&~oy=Y}a(dq-Rh#WsY+*iH h$T-mh246y-=*KtOeoVRm|&UeHWQRkaxm2gsFa`Pm>kI=b`TP6_*zvTA0B7|0(h zv8y>Tkx1(A0ShWCQ*b>@c|1t_9WTxXXiqVaILUVIj1soiEWF;sum3iFwCsIKfoTn` zc$6OnB?Cjo>dWW%_uTx)bHyi+?b?<8cb;z3e~7VzKIl&)Bk6zh{f7^e@$|WW|6zr( zRNSS)sB+I0k)DO$ivOXG_8Q>6E wQOP6aM>!A6(4^DfJ;;q DYFc_N((~!TKL#$<(AfAFI^+{p zt(?gwHBb3OL_~}lX`ShF{}FX<$cy9Oi}GX>N=jDfs54I(|6}^nQ&!nnM|a;kx99(K z9`FAeLh^rZ i1t~z&MP>}cPqCG$la)tZCWEO6BS|@-1mMvS9m6exf z9zJ|HK5lbIrt!tu-O0hpm(XYON5q7L*aNQ2%*^0A=YYqPoxA`UcJADHh&%;lxxKxe z=@t)~)bMA|p2f!24b8>~SvCWkEUi AkO!_p zIa;Wxt&NY1s|bAM#fuj~VS@-p7k|^ifs7f<+}uHQAba=h8TL2MT}0_X4MWZBXl)&D zP73h#J jwO3KP5jw3HhmDE^5K7aX=oRs8$h6RllKro)jLT@12 zE9C6x$IXiFcB~|(egBI39x%kxYjZ6GQ}JZemhI!c?@GWk`2?d)5!mSp8t}x46X^e3 z|BiP8-||!XlQ9DoCVmhX5&0Wo @0T}MX;Os1f3{yua$OB02C7oSUVg;rS<6%{Rou%552tsT(&+R$M1_C~@g<$FBQ zHDDE|(tSsd9t9y= ;BVE}>loyXp*h)Y*!66|{AT6T=%>g#BFiVSzwQ grsY45a~Nqp3fxo` z$`KPCt-$sKazP)ij ma>B3hEZa&K?+m9zoMN=ndLfUC=~Lb=x}frjo-7~9w= zjRB7zICv1N*a?bJOKSo01jof^Z(XLI1It4S7(D*~%^X2}>g(H>nf?Ct%h=59DKrJ( zFbF9_zkds~d3btiWojoRC;wer=yMrsSJu;$KwLp*LVp&akW|2Nje#L%+iqG;tP$;D z31=nt(6o#U@7mZ+izET$q-KEmoy;J_GW$k0-?>fkYjCjQdUiZJa3IYtU$i=odFG?I zvRH8^%3Z!74>L0#aWM&+?cv))My6t6KK tL)zN7>H&7F &lp
-$lfH7fVpZov3vFocJbcmYS&w36x_^XKk z+Pt8V0rWA0=BFd0qsb3TMp8Izta~O;V|9IkeRtEnvFUumB_txEdH6<_rm1O?HZLvN z*4i`XU(R1-oor0_gnoX^(8x`^K<0?{+Z)WxXS|-)%#U?2Nqc1`C4qWQI_{8}me%>! z{@U^~b|6d0Fk;Zf!Qn}EKbo%LVM6r-TT{HC%pMB4Y8*Qo_YDHjL#ljoXtT#2LQu3A zhGZ~8SuEwb%z5ZVXiSEwwRMJy!@{J2tUS_ExQlTzRhp&mE#%%9*`0NrppX*n@MtNi zsCF`g_EJx^G&k3Z2(oBR4e|HC;O178kzs9PW5Z}3)Or?7=0Hkhhp!U5qP+a{!U8A_ zC?*?-WN&P`;0>MX3q7T7p%lh;c6~1|gz82=eDsLT2hWd_X|(s91VKF|Oft^S% s~uf$Y_>%Z z*;%cH3)Pi{AUHiO`UZCT%KB15dy$pb(4awZO@Jg-wPgx(*9*;p_!cW6Dnz{0(N(p# zJ2%Fk{&atDOGn2>WPQiGEv?8T5qx?`=E)up_`P2mmU&ztv+RCjJCS8-W=1Ki32FhI zwXdAxge6~$7pK_G-?hN!TQ`3|_Zb#(d9#Nqp_cDuTr)s@?SU}7`wYwFQ+ErL*jatl z-HrL~W$x-82-(>AwtCQqqBw=2?#BGzUd2Sl9}Y$4{8zdE+Y8|MxJ>en%= zB#N9ID<7XN_KZN$gu?LAjxrAw_D~IitSfezGHmgAipy_Lx`v9R^V(!5^XHnH0CJ}- z9vA=JM)VmLdvw-+{$Oi9j){p$FPY7?>S`@=b`<>m OLt{EVz z7*!M7399f&qq}k}o&TAcPgV2I(8sH*6$U3(0f&-RLVKILTzG%p5W98j7D6>f+I!ul z&r@s4@Rawu2huxBh(*Oi8m3vsE%CjCJ>eieTF7Zs-5Wi10PO@?Nx78wUcx|3(L$C_ zA|hO{N~iB_uVPw`*J;AC`vixDQRixBQvB)f@88KBE8$|M;OOWG^7eXRDaWPf9# >tZEftmAJ>iK`hi9ID$8~!8-T8)< zRi4kR4>z&;Qd(P&fdEAL0D{w9zin${^9tpDqO0K6jxhj~G_{o9LqnIMg{pdb-gyO* zJlKdlWf70Hg@QRTH8q9AJ2*IBmuw|V=J#m{sf~=ee0|y4&F!Sx^U%;dV2|QL!5cqh z3=DekeXN+hY?&kn#}FE9pI~IL _A*I5qj31~e$&ztr9jjp zw>OFt>46oJhqJ-#_yj{S)s-zAcb)!m=VPl$aX@EfOJm~^1!+1IrNf|G6Mh4ipqFiK zX$fMK&{Vt!T;fB46p5&?;%Qy?lwqT+dwyzVX?6%y1PG6nm1CqIEkxgA@c>!;{+ &=H=!>8t3NY zi$&<+1Hf#+aRCmRz^2u`H*tF$J`mM5py~+Ce5t4?di%CfkNXmyMN3O4#p_%1qkUd0 zf$N)9fXSGcP!UHZTqYZ>kcAi|U2oXi|A{ d=>34Q}W z6fFcuH@EpwjV~J&`P-+{u5y!+WbTDR!ygzRyd>yHl;flf+IlQ0@bCdnPMX(0`~3L~ z#4=qATU)45lL&Zwu|tD{x7$GZx`RggaBm0hKr2hPVrbS< Y?QTL2(cT-zdl z;erk<7F`9_Mf$IvKBZL-gqRMrSgTfDq(6yljEh6FQ}xmo2p#37Hbyk4GwEfo*Xo~2 z %mFrTTm*z=h*Z zfm7zN;Bz%WuU=ip=7?o~m7J`(vTz5`o>CTi57JJ$2X&B;Ujsq##b2%Z`t^pRqsO0L zXJusGql#sZP6{1CQB6ol0QU=xT%W&?0Qc?-$)UH<=dhHsR${$i;<-FQYX?sN=uZb{ zXYxIJ(2HcHr&~3>@O}609Tsz@o(BImiUfo`0D#nkvMwP}0 A1UXH}#toWcs(a=F@|M_A&a}LgIq0K*DJ>3=$h{z{uQiwBu;2zm&Tq;zB|~ zkBzdxrnC9`caM)gJAuAtgeS(w`HjlP`}^hWBErMl0EN5rE&IW^g4*u5O&Cwml6iR* zm0H;)%N#-dmIgJgt*w3aY69Ri=sBpNFI7iyDZ}jd_K^Ga|NMEvVL%?abjQBKXzoNU zn_q%vMCUdmm=1z-MC&nj4F@~>)3C7Nkr80yzr#(}h8q(I`gQQlqr<0)pW4b<6$_Et z?FWWjH<3If^bHI=Jv?sx9j{uYUptMu%+15|uCx@o-e==%B-TTWjNqdoSZ3rX>`Nb< zn0N>27SF9KWqR{_y3fG$2)}H@cdES%Wp_RuBFO$<&}g=7-$UVlCL|<8zO5SDljg7x zAX+ifi|kh{mwGD~>Gp#DDBN 6tS3luS@cd9HV81{r zLMX9O`a=-lC?BRE$!HM8MSTkO3YiWqm)0x&tL<@j?joYmIBZ}aD)~Qs+72^^W~TP# zpp=uFNjc;QF_{YFceC2atEQ$ X2LqkQy**1cK`Fr&`6X`w2 zpWk&1znxIp0l(3JI>5#A(u#CDn}%p5R{$N*5N|Ygz&wOB+fY}BUhiROXsDG0_|7=; zf4}6lG)NaBe}=3Gbq{(D(lf*~EL+N`*Cu>|53b?;>(ta#)L&~WE5j1!x(^>tqvsM3 zXzuFLZCl$+x_yZ-=iOE|&UJ;-0Vx0*7P(gtP0_=LuXA!#oXSb0AECs JW)~JZnwq$d;c5Ku-$C%(VLd;WUX+&~hK&k3H+T{d@#@{8&{kTBNYcjEx~#6w zW+Z?Tix@zb@A~#FHC>BDnsy@$nK`Vuz`njXw{GYdV7vY9-^bpP^L>Zw{g=CLZ$p2O z66f`#%ETwwYi-%r&kq{zwfDC-rBBkK50`R%c@maQFM#ubfdO;_nfgTz@Qb8RLd#1f z;n}^fytOnlh;FYwPC7^)OT|ar;-WUx$$w(dHvAs_Z(NH H z+F0Bx&-M#Rs^os3e%{cw(_s(wd}y}m$2CF^?q@iO5^Z8KP+7Qz)EA9b-l0feV_~!Q z-<|iIphN=iNExh 96O *~)_lWO8z!&`p2q>@+noX;r^j z_2ekjmpP|~=cnj+wN(`J^!XRCuUb1gQtV(ZQ&&tU-@BLb)GbFKDs*_k>BIt*aT#68 zVfS$yZv2T$AWU-qDV1x>1rCf+N+RCttH1&+g=i$uGK5x{2p2%=6*K<)06vN0&H*Gw zI88IM4$#mDBmV(Vf|bMOCTP_ 3irUIRKAR9|VOtN2} z^68<$Jx{iTvJ8%3$Vv~2&q0WW!01B4!p97Xg&>3YpAmo$O EA!_}R|`T366IH q!Q*Y~=m59jbmN9A kxU%$#bInBdR97uoSE`_WSd`-{H#_&s&ve$cxF0rezA<- zSna^>BsP@Oc57LAXj{W!kAND0>i7*y7dwo+kNRYSAVSKT5yqvKIt(hvezhxK17#sF zBb~MkyylpBYv5~$lIcHJ$ncLJDjFJtzkbaC1(Gv}WWIeH8FhEYEt~S+<9vAOZO`-S z&aN()>Yzhcz^YVPNhSLfMbm5T)?hR~DqJB+!g>5#fpvt;k++^J&S%bSjcZs~Sa5N6 z)=UM>;f7obgn#bL8D68ZXB5r2)ZpM?I0ZeItqEiG?d_u(jJe6ne<$!Z+RMeDX&g<8 zb 9Rdp6pksijeQ2_9DOE-^XOAuKrSAhmA6jNm}aEDO?GK z?CRQDp>4OOZ5}L#pq#zH!~oJlmc51VbaYOlHJP5CzH|%OnnY?0_AK~t3v22qKL70G zP2`vs t% z{kuMV;f;-r@B{ey_{^a@DJ}It7!x*;v1Q`3;;$}=-xJO&uLMQ_{ds9e0?6xzSc#N` zg#9p?iGEECN2+X1QzL$soa_wZ5Q&?B-gqEB!2lQ#8$&HcgoWFBd!-~Kx<7v&z(WQ> z=o3tI`1sV}rhA==1G7fQh|f#?u0@|2#h7MZ7wv?;GdevDjtbo5xg!+EgB(HksU<6& zbem!G`_zew2&I^e`4G7|dPhV*!dE-@?xfe+;-72qBO` u(8r#OeenljY z?xbRey_F}$$_g7AapfVkfkyzs9X?lt7t+IXSz#;}+FE=%e6o8PL`uEZQ1iuDSY9Bz zwuyA3 i=WP)6bYjXaVXn|xF=H~|;iVD=%!BT_qQZxb& z=LI&TO|##j2yI7mdwa4S9GTcHdWv}fB>Q&n9>Yqa_QJzE+51i}Taifew~sYwQD(wR zAg?wL@f}drwY2LwCq# L=Siv~cpfOe)>A>S6uP>)0(PP^AczA7hLt}Z|5EJU z?{rL=m!3 &&0?w*^U z$507q041`^T3Y-5AH~FKiXpH04<5t4?RyoJluSlm5K9%$y!`s!JT~BG<3HeD&>< ^S;MUW4b= wg9PV5is4Sg!AP~Z^I^wC+s>(T@N63&l+Kc+jZ zrQ*sxmSz?nYnYpvHMc!PEA+(sOwnd7Ev;k6j-ltAnnE|lx|D0>hU|;pC#SFwrZ)$B z`=T2^WhdgUTUkL-f~t7NJQDrEZ_ob -_T?6NK{DS?e?3c@9P zAO-3$*cC(YKzRLA3BH3?vbJmg{#}xV1?(xxTAB}ClW%-}`!svmW@#kf^ zccL0 WcOm?y#9NJvQhR 0v%HhHep!d&e3T0?O#!t+Pgu>)xR#Gc|b<*S@%%#;&~qH4(yxQ(9VfwlvBL3f(F+ z58y1Af#D#N(gtUmWmNvWvfuyA3q0qV9b?e8ZNI<03;~;$cHaSkfqQr7ktwm)ss|l1 z($g_#L-c%j6{soafMo?0-+x1_Kr2)s326DbgbP3O896z*^XF4uzg8`ZM!A9w)80OJ zh8ZmYLLSD*TaKzoWk)Dl|NL74l1IO~2o0{suV%2_FeN>Gbk^~o;pPWxn(JeyvuC%# zmpPKE&OP|Y)raDrHx zNzZ@NY1R@JK0zBw(->{9K`f!hg-wV~jSB@C? z`ua7suS-&4{zEZ8Oi?#xe=9E&b7~mfxNPf9#*Aqetd72liVEb2)2Ht!v9~leS%Ni0 z`-b#p306a;ucihAZvkb{> Xxr6Ev#xXjCy1^6}@5 zm#UaS>i`?>LrqQnOw9gcN5?|RRGEa=>RZ@jSGfQifmj=xni6JPAuCIRhZeVut?ak_ zCvK;tq~y5<5aC&N+UQ&kd%^X>g{V`m(?^aS17&Q)9tvw%dOCNp(~*dpw#G)-!oi=S zQUbCfwTRwqN6tf