From 5db2e8db391c2d12aa7c151abbaf912777eccb25 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 30 Jul 2025 11:50:42 +0200 Subject: [PATCH] v2/feature/[Creation of Cortex.Vectors] #107 Add Cortex.Vectors project with vector implementations This commit introduces the `Cortex.Vectors` project to the solution, featuring: - `BitVector` for fixed-length bit-packed vectors. - `DenseVector` for contiguous dense mathematical vectors. - `SparseVector` for dictionary-backed sparse vectors. - An interface `IVector` defining core vector operations. - Extension methods for cosine similarity and vector conversions. - Updated project file to target .NET 8.0 and 9.0. - Enhanced documentation in `README.md` with usage examples. - Added license file and project image. --- Cortex.sln | 6 + src/Cortex.Vectors/Abstractions/IVector.cs | 26 ++++ src/Cortex.Vectors/Assets/andyX.png | Bin 0 -> 63537 bytes src/Cortex.Vectors/Assets/license.md | 20 +++ src/Cortex.Vectors/BitVector.cs | 124 +++++++++++++++++ src/Cortex.Vectors/Cortex.Vectors.csproj | 59 ++++++++ src/Cortex.Vectors/DenseVector.cs | 151 +++++++++++++++++++++ src/Cortex.Vectors/README.md | 123 +++++++++++++++++ src/Cortex.Vectors/SparseVector.cs | 145 ++++++++++++++++++++ src/Cortex.Vectors/VectorExtensions.cs | 21 +++ 10 files changed, 675 insertions(+) create mode 100644 src/Cortex.Vectors/Abstractions/IVector.cs create mode 100644 src/Cortex.Vectors/Assets/andyX.png create mode 100644 src/Cortex.Vectors/Assets/license.md create mode 100644 src/Cortex.Vectors/BitVector.cs create mode 100644 src/Cortex.Vectors/Cortex.Vectors.csproj create mode 100644 src/Cortex.Vectors/DenseVector.cs create mode 100644 src/Cortex.Vectors/README.md create mode 100644 src/Cortex.Vectors/SparseVector.cs create mode 100644 src/Cortex.Vectors/VectorExtensions.cs diff --git a/Cortex.sln b/Cortex.sln index 025b0b9..adaf7b7 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.Elasticsearc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Types", "src\Cortex.Types\Cortex.Types.csproj", "{64E12D4C-FBB2-4004-8316-C886CBFC614B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Vectors", "src\Cortex.Vectors\Cortex.Vectors.csproj", "{268BA5C7-C6FB-4A6B-875A-492659ED4573}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -170,6 +172,10 @@ Global {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Debug|Any CPU.Build.0 = Debug|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.ActiveCfg = Release|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.Build.0 = Release|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Cortex.Vectors/Abstractions/IVector.cs b/src/Cortex.Vectors/Abstractions/IVector.cs new file mode 100644 index 0000000..f0a3b44 --- /dev/null +++ b/src/Cortex.Vectors/Abstractions/IVector.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace Cortex.Vectors +{ + /// + /// Common contract for vector collections. + /// Provides dimension metadata and core linear‑algebra operations. + /// + /// Any IEEE‑754 floating‑point numeric type (float, double, Half, decimal). + public interface IVector : IReadOnlyList where T : IFloatingPointIeee754 + { + /// Gets the number of components in the vector. + int Dimension { get; } + + /// Dot (inner) product with another vector. + /// Thrown when vector dimensions differ. + T Dot(IVector other); + + /// Euclidean norm (L2). + T Norm(); + + /// Returns a unit‑length copy of this vector. + IVector Normalize(); + } +} diff --git a/src/Cortex.Vectors/Assets/andyX.png b/src/Cortex.Vectors/Assets/andyX.png new file mode 100644 index 0000000000000000000000000000000000000000..101a1fb10887915ba6cd81f7493120090cfab590 GIT binary patch literal 63537 zcmZ^K1yG#bvhCn5gAN`fxVuAOu!La2-5~@B?gV#t2yO}PK@)6nhu{)CxCHmSkN=!= z?|D`4?J9}^s&{vI@Ee~(vP<9_oU@vo z6sT&9d=K~m(dyOPS0GSr9L9qQ67VycgS?J22*lm_`~}BouDb*RZGse_uhcyZ4<8ri z*qTxp!tHumUMplBZ@UQnQov(KAb7d0l9Hh@q;8cGW-rlJuGKM*)2LPLbxJic6L+Os zwa;8_ali6m!SIuVmZh7kQi@t?a%nF{fFfn5UI4CGQoX7WO zg={rqe%>1auBYq776dj~yXg!L41FHi0a!?3p}w^FzAPW5fdM<4$Ef?|3cqxD#>Wec zLrS+qfA^G$-|TbCPo&S|cmFa}sHsC?{w2+4wN$1?tmknbz=SB=>qcaoS>kjyJ~_!a z^U)d@^9IRy7O|gV#}4O_g|@dcrW6LlRLp)(0HcN?IdMJyNrjYF&WgzPb9Ct0o8puq z>8<^L)Y?+DAf`{Tq`>4CZRhDoiq9ekoh>+D%neMw|7Tuyr*Mv9{G^>BV@YwcRQdwy zzb1>*W-<8<|2Xw18Hp(Ex#t*R%MXnhT$DMteHOBw7lGgxkJ!58=jH!1Og`~;+x_b= zAkuaIXLosRK`{v`5vzcZ2t_3w6u7c!vB0GDU<)7&q9$30yZ^0zRmO}$n=#- zi#T!b?rS%bE1u^!Y2tz^ntT=?Tfm8fH4r#mg4DKn1lD8Y?b1_eCgwjW5*_!)zN`Dk z;*~B4Nyr}8)JRGQAP;eJ0jU-7Fsx(8+bN1ei=^wc7bn_fU;p|CIL|FSl}N@gdA`ex zwvRO17;#DAiKF_#dXkM?rmVN*bvV~%{r?1|EsB7tH=JvShu*B`>Ncr`mQ~q(ziV+I&|1&O$0k;uzK3^H9{@Zck_9ZU!<*vR2yNdid@-Io_=w>D z?I5ll3v{}^x%$wtyA|*lPxlT=_J-;Xv8*^<|AYZtxltOdC*PPq0s`rO8mD?nFz`Jz zg1fsE?;J4aX3hnIXF2Am z*w?Q+cAqV5_>&=MTLGF`l_dG z5W~N}=l(v1Df?`$POL-nvF&sP8W3cfee|KNll4{9%b0aBpA0{pNZiscW#63qYYytk z8DNgy?hx(yM_$_|hpXM)wyU7)zeiylKDl9^zuMyp%?1zG*-pjGsxYl=p0|6;Nim;e@tq&?wE;Z`N7TH$wI?svV8zxvPwhM!pyrWsz)1d|{Z4cAc? z@M%jme>3-9@b!C`A({bvz@(k}0flDvN+D?v*oik9`Y$+K92h+s`Q2^_{56n_rXJki zqoM^xYF70;S<~Lc8m~iV~;yB;Oj%eJXC-S6_ zOnUvdjjTzBGwm;Q&+*Zk66^rpQ+M0bu`iNaj`>)+d(OZ9B7)@Ya?!~_d4K0eNIf%e z740CsA(rn#UBIAI?)$1i#tBUz$o97WA6Y~bVmxBDxD1}`g%wWz&kMf-MdIo2l!HUFYs21SEHnRuqFrLjVw|Z`w}rzOwY)~smi+Z*+9?Sc=&)IzqLoWZ z=$QFrv>TO)27&EwGlowm`0jtZ{&Gn*Q~&AX$M*?+|2<@P&)>rBVgb5#fnUn{3_la4YC9kFx(hq;Nio!^3q1lo4o2PPD?$OHM=Ms|e+lu-sjE&Ave4sD{|3k6r8#eU1`x}S{9#9QwpC+ke>Z}MCU4Gof- zsJ5+;$VneO+|F8A7oF%OWi>3}E%B+@Rq{EEqLOf*JBf|ARkTFDl4Ef~U$@zf9%D&nKOW)n9deuQyB3(k=(QP@u~5tD9(3U#T6?PPZAx*u_bZ5ZmjI$mYb93H#K z1c^eQonTA8NiKLV&$%^4A$&_GFygnkr7J&HE8`$d2)b%Kwe`94yBHx}(a6df# za*-6W)Y54)umW)4?p4qhcydt(>D*dqT3t@`h7-M14`=2x&lCAm%keO~Ao7Xf#UjC< z&;GVLVcR-bUBM!RQ+K%!%V|$boVCCVX<(P-sOxQf?sVgQX_3!|71$Kvv2Smo*R^qe?A(52ONzlBm$kAUBb;2_Z<)U4DgT%T3Y!TR z;*TlCY;~#>G2`NlIGwC5X{)(mC(b{TxH)Fc7G+5LddO*((fZOYjdQsQJ*Wc2Z5kGU z;^4LNmU)41Q;x+M2qNiQbX0?fE>awhao`*+huD0^#v@`Ey39qzK>w(*=1_-sV^Q4p z*4F7{U0M#cAI(EHH>#L=%uy`lEz=a|d3MiHo5FWhltg0V)up+ye!@s6zj13z^97T^ zjOhVgZ)k4D!VEfXOC0n~Hq7dRe0o~P*Mr3Z*!>(R^F4Bv>nUsH`IGn9WZRiM-czq* zlYGguvpEDRVHvK^Tz~tW#>Ar{9J3-h^EqqCSl=*nM$IPbaODtHTOsZV9&q3B-;>9Lcd&v8csLw*EP1hN-0e}GPNVsw8VU1+f5 zkF#XK_UHMSCMgX&R9QVaEf>dN&eBYOVfr{T5^f15JjQjoEs4&z){1K9L1ig7KR~0S zrs0Vchzd*Dd9e(1NDKKL=fy?`p@*SP?fpA9U;1y zbi;@0jnHEh2a9a^FjfMT!tHWzx+xb8eHO#6{q$ZIY9JS)7hHwFtOadYMO6mafz@2I zj@F|@mmIc7`lCUuJET)#+yv0bG(E?7MVK4>kLXP3CTd%AgF_VRP$7SjALscAXoX!Td3D8JXa$!t!x6<&eSFK&=EU*W( zzF^dD$ZA70=)@x69bsO+!qMM9O#)MfLEaDpgaLx6iugn2N{a^tNlZVWOgd#!(92$@ zM$&Krb*KhICQ^#nwhKN~hc#=3QZm=mm2E_(c?JnsaP&*6dn>Xv0_x#4!k7ykFP5(m z$&hzGeqX61Dh&(4AZj&Y%oKyqebi>#+V@P4dhXs4%jQ0RLtt^x%-dJ7;Kg*?YdJ#W zN7cx)UT8!!$jbm^vKJnI+we>n^^}hO>mM0a-42eGZf~TF{-$haFfc8TWFvN z;0Jzh(uk&XX|ZHg*>#hhJEvwfK5m4Vj+xadq$fF8ap+6Ko+J&P>aVbnV$s#X^+W`d z&I|aqs97A_6y)|eJjBqp%t<@3?(U+-AFs6MPn%Jl@$98xT*3R2mPc>Uvj|IQBC-1Z zkpX2{O;BdChoTsSMl zJ3-?Ir7)b>x1xg1&8rL)9g@O0>n(V&(10ugTb5SHHe+GwsH`?E+?B!0W!PXNwf9dr z&)~CS%HZ0#*H4gWS&9cmi3XF5=IY32t-E2WXp}zdK?b{MYZ%qzB4rx~Fc-|fh{-9> zF*YfA;LqmsA99iE<7tM3jv4=omckRW3~ohz27)eMxO(BPPLZMamXNGy@>vP047rWF zb9#Cyf3*u&^eqqSpbLWnpXiZL6IQ|V9uB8Vz%iA>;&+qCyV3lAD|uD&oj-%kf2fB0 zXT)#V#MoC0T)h3+a6ZxEtRu+@0JyH8JGb#Gi*c94(%&k!xN;|=-hoK@XN%^Ii8>ac z_@kQ>N?(O=6kN@ENQopGM`JmIrtF-b$po7Cdp3F|Hu4~4RVGA0)-&n+6Q31vW}$Rt zG>@CdU=017_lpQtgsYag!|sAV6~26yu6;!=CLq=&rE+V_ z{05YwV-lKT37b$lApaPw4>Q#y6yhacj70(9Aa^d|xMy9xS7gYr2`3SGIX)Ww2_^q* zK|S?bwr8;p>7q&YPZ>0RRzp=T=|YDOr~oC_)1_R|>o0IJl(=WeBa?rgp0Tk*U;eJz zBv&`@p%1ZqKP`ee=&Bhw(>ug&Xsu$tjGG6xf95Q!<(>d{ws{$~<^Hsnz2&J8`jCs6 zud{_LA-&`7!!X5|Qk98gT_hp16$u{!06dH}o|BViW*R>k?E ziHbO3xq7?`b}3C&scqGuP^4#vAd;@!N%xr}N-Wa7)IWD{z1Y11Gx8revI!+U=_?G4 z@#L}#dr{qR^Uq3N!9t%U{PTsgb?$O8QDkljn7*#Jqz_u*InV9k3I+u#qR(tHj=MK9 zYFde<`<4inEZ5KiVRgt;p_tfeXRMbVR&iuhi!IcuAn~Ek=RVjL5kTiLzJyN_AIi2( zGbXzN1H{J0ER+6R{Bg)ZC0RMaiCm0WtSdUbO*~8`{Z+mf)!~cP!9%*tU`Te`_w(Et8QQ6>XHQ>tXCIFZyG0zPd;;kWXrqoWzzHd#CC_{asErBj|sf#4)xVv>Z zZ}|#;__I3D-R#I2(vU-Z{)S4?vfN1){T&&i#q6-N5`i#>`gK>ICpLXT#TawON=Y{* zZH2$XusTK)f;8=Ns95NorqYv>r4Nip9bnMr>4?>78laOq3U8<4bLlU+_CL0CWFeaJ z%tzqv^N1`xM(ifDSU{(D}|@#`<7k+^EZ_A``E`NH#pvMDlRb=afDk4$j#yh0VSp|TuF`5W<-kT5I5WJkDx6SZS!3ahf-1S?UY&oP1cN$5b^ zA^i9~ zNKIA|wopd^kc*(s1S#$-a(fj5_%)i6=iEY#6ynEamaT+G)emULYtqe42KX4D_)Xz? z`gaaN;o=;PNYH||4V7frgqWa0hiCEvvN5mT$6Q6B?so+sd*UX zi;i=VLM+`$EL`+h11(Z zT3O2;-HJVuz&S>?A~^u1YY)9pU}yUDiwC7iU6k2Z~`C zHsWW|k@7^L%T7|fjzOqHwJg0uq3jOLc4#JCzvutS3ccXgMFxW#l0FjVq>g}`JwyrB zlDg4-;qxq>8J{hZQRRju_#I_Ehvp?zjJ5#yu!U_~?`LM~1>RjL-qFNBto&5#lQ&M! z2F7YYTOZx#GRA-^dxowHZ8)sk;m@d{M!fylCRjLuq+e5$e?RYr`Aj2>ypKyo9EPEP z#qFO#rbf)x z;FFE67YSl57JL>f$ip8HkJC37EFW@SHWr?&t4XVYEAeU4iDq;Ouw+!W&au$8z(PyRU3DEf4?UT6i9Zj6r&cn* zJmT1mcwM2darD{dZWUse%Wv6#dLat3<~Iue#Q2{%6s<^@#rqzK@Shz<>he@;Ut*NP zX;s8W$8XJ8v*AI-5V2Td1IVe3yB}EHe#L{yqt2bOajM906A}0@`kTMLa8TVDA}}ve zu>GJ;X4{HN=>o@$0d6gEsmJUNEN+C!02M4GuU&9vH6IK+Jh4huBYy^5Ol7@(oEn8B zHu9WIVBV-eqb+mf7pw=~`usI9(D*4f^F1pC0i?bFNKO9i`o(&R?^FadZ{Lc@Jefnf zx4*gT6PRtMc>mKo9q)Xzv<+^7wc9ONc~g$T&GEJUFb;)<9 zb=4POB%v|EAEM@hLn?o`Y-=Q-!?(5vZaG7WETj8*JWJYNuz~?HyWt6M_J9r2Rw`CwRg^} zZi8V7<~_kw{j2eWZDAZC-^g7Iju&k`0=Rpeoyq z*#!aqs-U?RYZ^7shSCm(vlfeg=UB>G6+Pq09L8iIVaA3bE%_E~i}H)v>L=)gD3G`y zM_JTib{E9ip#%cz2~}48_c>tU>+;+*|7$%_8j0B3&`4O#*%R(jXdOO*W#2v${q4jU z@u0DIunbm{StKAEqOzL0&oTDjvN+RGBeNONf=pvp+AUx^)G;8I?+}IkA zC_+9L@qd*|M*mj9^6H<@enV5$s7(2Hh+`f`HrNaGX$QqaA1!3QsS>NMkEnRs(ufSw z@e~5Hu>NkQY?ZGk$}C#&iJB+!3;8A#VfL88gQMt z4&5nBMEMg86zGA~HV5U%MYDe!HHy1bK0nFfp9&fecNeJcrzIOLn+Vo+OBa+rrujV$pMWAyS8))Mqo8KP}&{}w7+_Y8mBQmH8-rs2Qsrvmr7U(-R=)+sVe;6k4b=B3tg=ZzQk^M)hQy>?}fI&~W>7X+` zW${#hV$81Ukj1^7;%Mqt^a_f$;A9r82h#_r8xQiGEbI9AAAEXjtFVKfNdOy^uY=W8 z+ZJ5vUm)@nRq?JdoB#i$;y-HoP0N5^JV7^Mvb)r(UnJa>HncVSC+@jD0jq9b?j0MdZ)x4BxFZX22 zKpdLPMEMJjpvfax+z-&nk85X*DE&W#Sirs-pfqA0huNjOQ;!23NkZ`!jHk3F&eX<$ zlUukY!92{ws1S<5b#9XTGtWoM+;M&Rt;71m=#P?_m~7!XOa&rg0e#k@naVu9Y{?Zu z*#^ZrRqkR`t#EBrp%*Tb@90X=^l7%IiK6l-GNiv2*uUOfx9(tN@z_I2Tlk1V zKzntF#=<|l21%7{GeTKRxx{ksZOQ;eq!cr3Ei)zAM2gUZFIm<)&4}ujlRoGE=HB>} z5D*5mfkIe@8=kaJrXVHXlwO_2Fg{~O7@*eHb9$cL|DD3n5qb{V-NsG=JvWlo_|0C?T}l zBnnpvnj^6`-hnv|$c!EM^@6Tv=*sG0cb9M@IyOLGEN(ka@`s{Ac^ty?yYF3p5BW4liew`7B9dXB>^&K9}i zBJ_rZVbK}sn)b~a1qN^H$a_tTqEstrbR&iNx=3ZZ3+f97TwLJ?)$CRx@S8mo%7-$C zMl$8v)x^`3Ar#V9YuWCf&~bcr{?w4VWBLS?u%(IOV8%GavF+MawU?k^q9l^nCHr{L z6JCj(rb#5HnHIT26x<-1BQ%SK6vdA-(zn@@k2@##nEJl^%+cm>H9W|2b;g`tDY3d> zPC0);{j5myh&4JXtWe*V%$Go{A_yy#Txx{#3r7>yP2Sd!x8!IC2sy$77e!}{#M$hG?z7DtlQ%mY(>MRNq1Pksb#lQ_{0`-nk4>Mg)TbMo%%_zgW7*nBXscgVe+(gfUw2yJ~J+OrkS&O^8JTk-Duh7 ze(W+>Mfd;5fM-M=ivUDs7DkT$bP)+eno;XHPzd8-Z2nB++~7fum=rS(9E>bbr_rUJ zh(*;1jieZ);j&45*^RNIZie{ATuipcWGk}0rNZlHfT`62 z2SVNKRv8*Z7$L8~HmB*+??K}ABb~uiGJ#iBSoQhfsgGCOg>F!@FSxRMr~ zH6cC11$dVmBh>U<-@}4I``+MS#7K;< zC9W6DUdQBrz6)Plz8FgL(>~oK%RgA#*c|Wr87D!kxsQ6Y_;ITh!#ddeRAsfIjAOxn z^53|W)*QV`=%>RJ960WriqwG~WB|uU{yt;Ji|zwBjiAmJ9mUz6sG?w1Hr(3160HQ9 zBvLU%*yv8xa#CUVv zML|#s?Z@cPj!7mTB3%;lR_xx0CLb1P`an=rZYih_tRdq_!uq^dzl)pJ5Bx51%8(6w zDX0Rslk{!=Ss3W1Wz=28upn4;Lgq|D9&^6=b_82yiRIu;po_l4$d+lyd9Ta~9$L+^ zM;Y>5_00|apkx?;Lf*1pGJc9{gFEOZ8;&{{P7AaxiaiuR48qW&3x<{C(|snFYYl^u zc%x%rOT33~u0bM!@2ZiK{V42()wEjWch7#p3)}@z_t@*}L15xoK1Uo+zjP?byS(_g zYo0rCO760%nSaL-Mft}Bf25FbG2Eu@>ic5WItn61#M@!sX*(~zG>^Yxywni)ka%)djMFqCQ0lxb17=f8^0A_ytP(J zyeRuB`zOG1ApOBwnlFKAEqb!N=jir6HF%ccLGi@meHZ>`u@&vArg2NU?n% zB_#j5WK@K049m6F->=oM%<7J9tu3WMjxY1GxCP+jR{`v2WgmPytzQgMzZvX$$I);U z(7yI=1ycwj?7aECFDueYhy_*r5PmN|3d7FbNf39?do?RF&n1b z22;oEwg`qfH=+~AbH!rJStco7awg#YISqAkHDUwGWhD!#?6DyHEUX?{75N>wsvdIS zN@Dm^K&!&cozz~&!QvIkD>LU$P4Uai6^2+YKR+db05Yz`^o1at-dyJf_jgWE-HG1SvM$vUS_Ox|PFC|f5}$6L ziZHr^u^7~!v^o?!W$$L(j3c7K$e0EYqWa>7ekvb$O21RIugzcwxQj;4?nAXDI3R+* zf~rQ!FdByB^Cx~ihtFillLtAvzT?8NxZ-xvqzmz1$0D?2>Su#VX~S8SFV&pC4@+0` zPO~^Sxg5l)I@g~~pc`r&hME}!D@c!oa_{-m==jj{B-P?t_wTF?y|mcLw~FyMukFX& z55%tIYJk%oOCmVUMUoDVRY0!j9q*?EEL%cd*vHr0;biD$?LLAOf{zW&hGs-eMG~{} z*I@t9>X4y@gp(3r)ive6Sq1-m1ajdCf03k|^PRliVfl)@n*Y|?c(e3&KN5Aca)OrWh+5pXVWD#|NX=oGJ6c-@w>7C~x&4t5H|6|)7|4Q^ zXjd)0k4EqBGrH$AqDpuBkUh>mdOb1bSFm~rBWhrD*El9%AtO>wGX z9J;c;Z`6R4m9~9r5-Xo^8PKgp%I*e@V(3D&iSDUwCV32k3rUoHx;#Pa?|L7YvMj$- z?)Q`%c%jyeCqq_Dkl#k4&o2m=W;OC?0l5??`%h5ISE4T{5frc;i)-E-eHQ=b?{h1Br<-BK{D-`V!WcEpA!oijf!ZKT}idHC6*B_gyNOTPt$1BLDj3l)Xt`S>XGDl?;Tndwsk!(&WFm(%oJ{raZ5C<2{C8&I*Z?xMsZqH9_SrcecR{@ z){E)qE4w!LJdC1!VG>SKd8ugTo6~6wJMVO-F;08I#OQFAl!T)!BZo7-%wez`g2K*N zPUyLnz2wVS)0d<#g$u1`A+8_dSS>eD1nEkr^8#>bmz%dOU$K^$`3Ckp+?Eit+DI?jyzbKl_>HJjnK$0kQ)D zZahJ4o?s$d>m?%qHNHNyl=9Vzw4(^|JJ4&Kn=To9Q3+a1eIp>wKE%fPQbVjwDv)a^8>&TW3hC zme%D383)T_`LDMbW{^c#V}0Qn)Rn0#U_~5MWja>zQ5>M2{}$i&jfk@C?;p&%ZG5tl z2QF*iA=^k$4d=R>=me_!M@eFfnQuZs!3~CJ{EKs(e1T+~;H?EyX!Q_|L;i3}U=)obj10AiUV|4`2B)aKur&|TFO24sRt1k;=(CPM6p12@nh^31_n>c1Kg zyeW!|Dsq4=1$r3C{5Z?eX?N{kfiEI{mk}!)hjpuykAu4kEs!jF>s&>C!;Mc$sYG9Z z6&B^JK;)Wb*j$x?URYqzumjXK4{}Ps7j$}6hY9G)CIy}pE?@MRI)4GSjhPLFh#HIh zFJ3im<|5&S6g`j0&j&{KMfwN|(Mb#(Va^vm=9+R+FyCnIKQaiS?kvHtaq1;=1;{q|| zg+zTb*`6p$q7;w;Vro=L`ErfSYJAYdI}I^cP53Jp>(hNNjo{q;mzFhEhz15uHx0oPd z{j3@U9I#}E>?^vFsI`|O*_j`3S}oxSG96k;xL7rejT;K3F7m95p z>cktYniKS?P=z-7m?GIF;vD%TV5Z7wKm7Bw;aDlKDzGfd8xK}&eaSx37rm00Yl{cG0iSK1Ugb)oGKLzvbR?Y7p+YWF?Vre6S3)Cv1i5U&og#Xm4aIbs=oF)1t!LvcPDoT^rZD9}xHGTa; zO}Ze_%z4PAKU$=Gn|(-})*zpmQJ2401MTS184Y8E3s*s}w;c>W z|EoA0Xzgb_8Z<9CN2V?S8=v*dPubSl)?PKykM}bY_qum4&z6x*{xFzv^WEx;*E+H zm#v#%(9!i|<$iiY*JfC4@H89v?Sscg7sI{z@TKN7ve|@^m8*Vi4Wk~DTDs@)^A+og z41o~>0y2U^xGEA=fuZ#rOv*g-k|L5N8n5!Xq+_4)EErV#VaiT1Uq$<@VL zJ6dqzF|#F_)@dMOl4H5waAJDyhS2E7nkHB$@Ymiz=M~Ec@breqfkUvZ-XU_*B4 zA}|v8zpM*17l~}1Vg$Ca%#ztXtiHYeK>*j*avI&wACL@pp&VEa zljXXCguumT;W_2Zfbx-sGT-fbtEx2IL~dSqSyWy_S+PIE)_(XLA>pk`!j~?Ycc0|twB&QN{|l3fbmZd6^# z-Jg7aWcS(0yp|iUj%!v^5vIyfSZ>GMGOH_xLqSmR?MZQzIdk<$ak&v8M@vLap|lRT zZy53jk?z0wP34s?BRBq;QZ&{jNg(OiEG+{*t%DUPT~D_j+)k&Rv$v;io`Zv`YM?P z@`+-8)%+AVaKn!k&FcWOn*CfAWdE(3;i*EiZUE`X`j_3eK>KfjReG07a0U?)*xrRoy=#Bo$z z3BPSR@YIPc%rBthe0J&d=p!Objq(pN^bK8+Yp}LOEY?qvI7o(%)&|Pp$8Aa#<7ip~ znPgil&6|c7)N?7`>bdc>34D0W`1}%D#I6>vu!a$wcFrwm`l&$reb~p}@0FT2tCoH+ zpMy`J*paqTcS8+lK;a1jvaCN}AAxt|4aS#X#jcLautmJEID;ixxTIf^G!`^A8Et$Z z%U%T22FM#=TH6_d42WCiWMx9Jcz}&^V_wyL)2bI)&JT}-Ed=v&aXuz$IA`d(uq3_WA)}D6 z42^!}g~|(xTtVuDil{&eRk!JU8xgtoSdTwUP@jqdV`?pmg?;PrhaACI7CQMHXF)}A z7Oe+4BI~i^zt)*maZgmP;%rfb`-25z8@Q9$95~-he!V6rt?|`-XqNfBGc)&@dP6^< zo$DsNKX!neaJRMZ0|g_^x72E1ToRRrQnd8JYPp6r35JDNNvvQt5s*yX?370y)nI`z zW%Uth&drmI$Hx7I!DZ)BO4W%CLE9|yf-Dx9B<*sHlsmb2j_2KbkKgYfWt5WGK))>4 zg@tFR)fo0_DM9+(DgBDDO*dp^5zH_Hp9uwJ#w`Go%LyFXkVAHj^Ukedf9zFMJ>YBU`aw?Q_p znv9xkfj$%b7Gv_@XqH1!^jHaA+Q&N=AxZczOmzN>tVg=jg0vu6H4_Y#HA0iwl z6De)~Y+dJs0$ySYZQQ+`b0myhw%KM4dJN9L%C0+7c?6bB)RHcq#m9`VIe}5ill9tg zUgihF(&VzUusZoV;r(Lyrk`|P@FQiga5XFxkwiKvb2ZCuO=0qsvl@=1LV783;j2Ln z`CCMMf$c$64jeIObH%5(bCbA&X(>Z?r{CKz#-C8D+SY}uDNPE^-x%$e+N)tl-bI`A zCTSTUU;pw=4GpOsAtJAJLSay4F)$EjO>Vsh8l4}<0y%>>rI`WF}_4vZvWDRXYX4#*cj z$7gq;qIEQM+BY3nI^pb?nocE7F^-CK-5xz;Zcd>6iqGSI)Zay;zhqF;J(*(V36{Vl zTT0>L<&hyjqcY6|E?rFL)oP%I!)vKxBqcPEsbW~pVv#EbUKC-<43pQe0uhv8U|*ve|ahMrM36;FibpHvArPn(r`fHC~W9IfpM-;-Uzg*s}b!E;KQ@c43q# zaJ^b1+9%m}=vpf?9h!4e-S+C^rk2}p`U?V^OE%2H543&Ugbq)s^B&AHw8^J_1lKi4 zqJ!2B{QlVO054EKD==9yoghnbi}e)=0}r600@<544zRIuKAkxp4uC|MkEd(;5#wyz z2!9dvm`5QhN>@{`n{rF3G{{HF6NK3+cSa5*45sb4x%Suynrgt>?E|LKb~ULA8{(MY zEoiP>wTcQ0HR#oeN$$Z;bE+iiMQyPKYuHMwF0>Dme;)zlJO1u#QA~P)Bd8V%^hZ~mcH}7-#yj?Q9e|g&c=6pjT={a3K~ro(uAbz$AZ7SHnDE*t3M%$Fk@6n!##03e3hlp z4I8g_#n7Lw;l=_FNin}Rs?@ysu17D+ClDn)R79KbH7Dxj4<#H4X9C6YtPp=s!Nu*( z(MdOo6{le7(I-xBigVlsN~h!SY2`JWLvO!(@#dZLYc-0N{nx1IU0dLxH}RZG;hO{R z#Jvw(9W+;-5d6=}rK0cTGDq6*Jclk24<_)xSPPO^JOnA@S1yqc(!iO4yRFC%W(L${ zG&9J1X{wC6;Rz)}AUOzjnXZ?acI0NQeTGMaPGw*FbdCb0F%i+M=L*KZ} zj9YY@k%ut!_-?t+T7CS1J@(6s?K1OXGn$Cs#4i%MPb9O~vPfts@>w00@+X^2{20vk zlZiM@OZpvp5~ocIiV;Yg6R14JNKh8cH*`@%k=An`$hql_JgV9DbbYJYCcN#~L@y_J zPN${E)r5EwX6VXj+>m1Q1%L;)^+D{*8wc*4ogmT6`NGpJv$%;g1d%s~(i(CocU?$T zNj*x*!YcGe^qd*Py|FIe(2sx2ICrq*e09xiZz4iVMVrPt@1xmJ8r^BV_g~4X- zWnqq@wOJ0f6(f;t&AmSwhlv>(Pf?VtvQ9Ua%XSM=AJgioCa}4k^-YM?NHy{njf}4# zw|@!u4~btnbrg&N`fIXPvk%resUi;aCMQ^ui8wN8t%hdMVMuzeMIvFrH+%ckcfDi@ zY~oZI4-0(S7^lRwgKCSIq+RwDF%DKTM>3+uUlVhDw!j__>tQPL<{xUm z_Ec^X2OX@7(+TT4zLxH{k=H_fonSrp2iFldMXg~kd@{k{fuCBY3J=U8W$1XZf3f`I zd*t^$6cES0lN%)FAY3c_5i#||x#Q7O(04PYRhGs7Qfyig1_7vfKcE?0Ue${E96!oa z94=RM(Q9;;c0&gXF7{;fs@#E7E+ORlf_2L)lm@|}3A`q=o85Zpi3EbOSk|jXgtvPy_$cUYDPxS?(PJFaI`!w= zdT?qRyDh2m@vz9Pr^`L68Zu{BQt*re0w5$X(0aE^EE zum3jwR@-Kjw&O6VIn;eS`8B>Q|31Q#kY8@aK+Zy#y4*U3xXb-?{IAwua-~&|AS304 zwbqJW|3AR(yRA&~e%bJvbFLTyh}Q@6rW1{}kkURs32t|M2KoPR^%ibX_D}Ti0!m1C zF0qUBQUcPsxF8+UAtjA;cQ-7J(jAi0-6`E2(nxoR?>^t(8`t&z33JWdpE)yg&bduT zZF@{v59NOqpbbiw8uEv|a(cp^%+1_Z!E|7}l0NzVU4X;HSY@k*C>?b9rwLDD4->j~WWlA;(!>`*~ zk-o+&c>imyxe--mhoun@-{?&#rIhI?WcZ7onNi1T}*$w*Jlsa_P-oW$KdZD@j@U`P*o^vUdFF;)}ty+MJzWc~? zZ0QySQME|C?hdV_lz3R)H&rhLvZ;^cCiXwZFGUZHNbGzY!K%{RE|{OYa;WxYA|^G+ zhBuY-XHIr(rJiqTVGUHT@AjR7WZdQ?8o5U}+-~Lb#yeG%`nDH3hR=B+kB5SwqgaqF z#g_r60dDWpMzQ@edm_vPTasGh*LQeJBXP_OGQN+A5|=#J08HxJ-IDr571TSaUx$wi zZ}x*k54d@*!Bxix<}{o2jHa}tOhvVn3Uwc217A%gRKn^!@skvF)e5ZFSl|4ikiDmQ zx%3@j^C!0sR0%PwxuYMwNnOSvjInPF4T!R$m1PvY{d?w=3IdWdB*scyCM!+?$PT(F za)v^LOhBT|ovKxe*r9GA;g;^-WR}*dQ$LO_o=9o736 z{JPn-L+Z)hLtjGHSBtC`P+CHhb82J*Z`XNST@MY!TL2SN@NN6z%-pWI1M5FMULyx} z{wqgcioLzde=}-yCX;f^(g0NA>B+=eXdVkh7W>-vLh*d^?_O;B-IuvlA~$w%pXB8l z#GaS>=iuP|AqezG*kLGM$d;6A%Y0C3Xw`$#_X|5vjR%s+fh&8-`DLGEQD#VmFbD8FZayomS~*5|sCJZ;(%N*DizcArqESy`p&y#oXiwu64)q z?;Rjn<>dZqZM}8&??~vSy}?<~GMa@gsGU~3^1J;CL632??SBM6*_?S6yE|IIz8WmZ1P3qzeCz$8lodRzILQOo9Utks9ABo3_*y+N zXn0$sXl+xRY6yL|PW85VhQ;u7dy>ERISR$@W3dH^!bR)FwbVp60@XxGx(Q-Lv;2z7 zWYDg}ZHEMNeqXf>Lr6#{>LEg(-^cSLxgXYRZcU-lLp?npEPB^UcX&#^gkUMWYcv?G2+yWoLWZd4(=a}M@_LTdkuC%1r82?9zS)!l z&9bA$`bJHo76`KwxOlvO(GOcErduFs(x>7Seh9AYJwa%!+*tNnI8py%F(3oF@^OU$xMKEVFWbdK8vF~} zy-N>W=Xu0gA5GfPmvnv)kr*oDOp;6a@VN}3+FodY1y8ZCZxT2}i-h|W_a4u)l&MFq z!Jm19*&6ddH>U+(EW+8VeFvRppnoJefvc?WN^qDUeKesl4RL!E62BK_LPc(aqn-M9 zXZeQb{BT*gFKvymd@B4+W5DydaqX_=KG%}*Ez#l#GdZoKdgDErLY*RWkVU%PVK>Q0 z*r%3`jMMshk}ThQ0ry80&Dc@K z7vDx)fpx{}R?)y4Pi$*y4c&Zi+Y6)<68rEmF7`8%D$BW=*Ln&gndQ1fD6?-3@|;gX1d1tXaLRw@aJvPnR6!cKyj0Y0)W!?-8EayhEmX81Z8PIZfdo?(}^ad zZK8&vgIf-(#A#q%4254kxsMHgh7L0nO0wh^cWa+S~^~8hrO{U zbrWIh|N5t}Qq;#-ChVblvmg12qBLLRVTsInX8MJQlP&D$wIuSdP=gPE|Kj$Dwn^-| zT1Bsh^Q9UR=!KGx5VU;j-laq6`nasB@~{`l{b4jsG}Mo$ZZ*HPXQUE}XUTx4cPPBwP zQ7$Jk%_v<7dRG@-_8T9oQ10;&mK+ax+Pxi!Lh|3YjG+|$AjQ)9y7`lgp}Bi{Vfg$4 zVcrc$TXD}ibK`B?!aW55yh6XUeN)HZWOv}XL4EJdm?YJjc5u{$5IR93*=vz@0}9|! znmnt`DKu*T1w-F-8B&b-X$~LjL+KSNasX%h$5+2hM@8$Y6DfVC!tv614#&0!&d;mC zhQb9Vjy7GJ=b+Ponw=HduE}aXz=uwmH%YPb;-)L;nfOMpx~5w&px35}ZsX34teiLLu1 zoD{kdaAam_lWF=O%$RY8#ZG}2V;?21xuQq9rsoT~KNkC|=6kVlsU2Yl8JOAEc61JoAcao6>MrL~|&1w8B{k1{@@QT_v zf(nuAD1XyTF-MLm{jPFG(fmK^PN5bqvP@kHXzWxeuF6y%!!bB6L*QeVA}$fL+CGyCs#wv4 zOHNxDunzQk>!vHrx#`|=hy)8A{tn~wD;ur>RjG9*hPCm{4v;PbQS>C!bYUYC*Ws%3 zsKWU!VpE|0NCY}b_}f0F>Zt5HvF-@T5cD9|qUGMDmx7Ks0!v+~`lmB0pPRa4CcbO3 z9+xy63w~qYe9UJl>Xc{DL$#BzGq<8<>3EWW1<^WhR#r*m?Z#ZQ!C6P(!50v3=aWj6 zeVUg?q-1SZ@LZ5=%F!36O{a4HL5!mBQd&a^wv7mdHj%U~e~xfGPr`we`fonJ8?k4v zMw!U4lyv74ig_6SLdYG{rAXT~f(pjHF`Ai09qZ~->f9z%oiPx1)ceF1Kq3{95qGHe zOxESZ?asFL9{4+qj=U&6b`RM7`4yJvQi~5r*Hedt0AEz^_rUkB!Cs<%@3U3>Wq>fK zgDER0>2XQ7GUQ#OUJx%W{D%(H09;tQeI<;ML)dyZZNG!dNs=#_a!%`8(VOzB7x9O=mzl2dd#- zx@^99XvH4ZTtFg(RD7Jr=eE~Z>yU5IQTDC3?%%7ildVq*A6kaVC zUTm~@Sg2_b5Z3V*80l;&5kal^rS^5K|5|(7GnS4Vt7wP~aYRDkP{44C+?NO@e-Ld2 z=^`wyB6S;|1#mXQSqpdg0>aC1f)a!9vx=|xxE}D~GQxMh7Y5QT@W+_&?I8^))6LKK zhM4AdE~cYCM6gJAU-7t!otdJ?&*X%h!|666swdJNe}Xg<2$xhZ*v`n_|A^z3FD!C{ z44ATNMlpqxoU-J-ZahlENLOGxk9y<(L5CAtlU@v$l*R-9FGolPzAX~MIlxBHc>qDZ zQOw7;V~?!-4l)Zt7E&4e>9G@lwz>7yhjRWH%rE$e9+oy&%{{)cEz-h%X$H*LaLLSQ z*+oCsuAjcJBzXPJIuVDDR#z&Mbkp1&3zeyMS3vUAn;AC>`Wd?gs^;b&c7O{ue17#v zl#sin(c(XQ0lR-Jc3T|UY z+z74zu@9?`^)8KZS0Or&W3*C}vXweHt3+Bjm8?iHD2fZqE{W8dfeN@Wk*?y<>U+{6(z0MfTJ z?^4(ofS4GvuxKx`*ZF=Z?z#l)J=;TlwIcXYQ3o;ueNa>DgXUj;&z<)KdVakwLDF~d zig4wl=g*k1hO)Nn-vMELA^yYxpsmZ5PLl}l2$`7NCl90uvSY03*wP&N zn7(cR)x)K%*0&i(+dI07!{r%kla#xNl(&wMQv8&txQ=S9CA$oZcPb?YXyc~fbRZ>9 zk3t}OZJu?7jCSroSFD3~egx}Mk* zc-uCIHdjL3`AH&dUL!)Kp9V@BwZ1yKTMyxYzZc(K5W|T^%CPvr4ZYmCqQ6XT){YD| z>qxOFKs0lU>#F0(Y?gtW;N9-q&1JrO3D$&%G;|f;XVVY$1-+W6Bdc-DsjROSzJSi} zJWa)QRW4NT8SUQSQ%$wExcUS)qE7j=HI*%G$JZ8`&k`V^jQzb7=5USX-)K$wR791j zY)&2zacfv@9^pFn0OtI3_|J^516O@wDGzS#eS42 zC$`bWK!XUPuPnt|+ypm~Z5PkeEiqqpKKv9*T?w9t@$~)5J_e`n@0O6tE=hOXx9Rwl zPksyi{`bjMfABDT0odw%SRz&X$xni$O_0W*p!}->cU?iM%q%iI8A)$x<-rsah*La* zxq>a0i6MJ5@Lj1l^~KyTeD@%?S|9o5XQKgHab*kDZrvl9DZ2LBhJ5dU{E+;QwlfVn zYhc#z{;RGlv}ro-B9N?enQzW{h3My+N(jOkFqv=Kc1W;|^1$6sTVZ135%z1Y`+^dl`C;6$#G{0nmUkcl(ZKFRmQ52kWnm!(2S@iqcum#GQi0XwKWmYD7~ z(FfeJw1=BqWc0diUIXy2gY-bGap?jt*Jav;bHFR_`K+{nfa9p?($H$(5F6Prvy{{NWRm=bG@>~c5AmIp73wpE zps&wkcLxS57>i_6Uvk8GY5q9l?|Yy}Rqt-wP{!DcOn%~A=;u5z=bQ>FAz5_K85azc ztLi}YyDIhzEl)lqq05+Y!7Ph)cbLQ9;0|uYXigrr<9&zw*P&pP<0Ie{iZuJiTsUQ1 z$8C1@@9nfY>5qv~X$htV$M1MLw8N^wU*d)1{bHh=u_viYzfHWYbr}R^bX<7e|Jgsa zPu~UQd7l2dvA_M}B3VL~nV+{sXT~tFJ|H!~PT)ES&3aB^E@c@B!yscSBR(|}x<;ZU zK1i&vqj{_rpE@VX!!}Uta@64j{5PK^(b%X&=+o(Fj4F=}f)uZEej5BhH$rv#-_~+i zhDS?M59>nh*nahNy1c3zd?zL&H3X}E3CBnwRlvtkt2@r>mxov4aZRis2yk^KhqmnA z9BJk%+)GE}sbL91UCk;YdgY<% zLNndwXRU;%1k+>U1>}lZZOG%9?FQk5#yhg6m^mYZW_(12x9g9+g2GAB`&M(_JZ`*g z-&S^~sy@Ax2BAfAHUr)5vWEHUj_Z@Hos_8YPAJ^(7k6*=3f;DhPe7&m;N_(*9CH^z ztjt}6%_5G;if=xCr9Z{b5{0NFVylyna6iR3k$VyM`xi7b_od3XO^cZFGzZbtuY{oK z!d!E&ovhAZDjZsRrC6JSD%2o#P{Nex86Lj1>WRz1R%L+-+SnV^xR~4*9hrIQ7IDkx z6p%=EluA`DQ<~uk40rHA@V~_Qx1P4I5i^8X{H44y z)m01`kR%_Tx@Ubj7%5zOhERX)T>}HDP!r_3X9PACiRy#;TB=%&^f*L zv9c1oMx!AOw@A;em$~Wqij;rQn~G|x_U95+QSQ%g8D<9-B*k6`Zz;LY=gZ{s*K}pC z`RLxr(|DbGog-Bk+$fevVEyu=?t3_UBC~k5%|+tAn8UFkig?x4aSP6A%b9D_ zX{hzE$E&GFx1Q{mYZUi~-A4MOR}|CzF%jz0OGK}K8?(>O3Y~i+7O>sGuG$bhcUoW^LS`NT9R2NF*aD2H(PFJ zRH}jMgd5Iv=%SBi_-NjVi=a1!I;w^9Nf_Surfvk%{Jn=%C_-5>GzXX7*4L(DY04072LCYgK0mwKi&(> z7Gks%`SWIKJWxBczgt_~EI+0^lM0ikXE(|OyfE~EJKbK<;(Rd(xBHl*Gq880Ej*G$ z^+J-lP2m7ct}zhuNqA6B=|`>gwqgfq>gn8TZ{y@+3wZEfoO$V9_nn>Gtold2zeZ+s zmJ!aMVDhYAyZxappGpru*=Pln6;CC20D$j(e&Z&Lqf`1p zjT}3b)P+-Wm%H^rhts*`8^3m{A`lQ3-=DfIfnr0rs*C9@&q2RQBO!jHbi)K4wPod2 z=%OFQ3~c``X2!alOO0RgBM^;#&H5|9wBQ>wiMl@)B<4OAchcIyq0$3`)G_wu@l8Vn z#(n<>vtp>&98vzao+dqCjx)#|N+MO>(qkv6(&c%J&k27A82?X!P5%R|&oIqdn>;uA zjVDNkR7?-{$0zjeVA?1D00s{ecEwxuW>*#)+mY*IFfGr69rzpU!W!2mS@wj^+7(8o zVCn~3VPc+yzSo!EC2O~7!u1Y)I#b#)YB(QI26F4R(-+hC$}Xa@*HZszyp6&nQvR-` zyhAzPE?xV!C8^=!CGv;Ag-4`BXt~F${s-gpqdT$tM6a#o8AcVEeIc=f9x}<$kJMkB zX3<*N9jjs@u_!_Od%>e3ADL(*;f?}*KDMIlk?E0D&taY7c_M;&sZ^6~5D|wFoo~lx zstvOe(;d$1M42+?Wr7-L(q#r{r?8fmnE|P}2;EP%BaJok)%T0^qqu{d!r4(fZ~U_z zSK?s}GX9^_Rs*+mX1Wh4tAfOy6aDBP1b5yK;riY)wB4$8Z$=GlJJ84I7^ zhk|t@!m)9yoam!1<-e+$tH19{k}o%C!GquzJ=fzS<18<|6%uH{xZY7dEJ(C zK&x!tVwpTQZ%=P<8%SZ_w;b5A*c}E)R8aBgt__8;FD=Jw3>+U6t zKdG%~pA)nDa<#J19C9UG**1$9hO8m$=Mq3h>Q+Sx!_cnG`lGh=0_N9ES7r)pnyoH% zP_k7Ym79d2!$wbPwu}tO^MJ9--GC@AYO8bOX{I!kb<$SbrKe>=7PV|xB zd8Zw>M9(xXaipnyd;#+S>rM4FQEp4>jMZ|_zYirzg_4=7X9+ScEgD?9bG(ie+CvwH0_By8nX*D{g?>nyrGSh4LileTxLzQsy zj1KDml*gvi;Fk)jD1_#ri9;){oz1D@OThAVjowz!N&pqzHXd{2YQ2#^Sobt7TCDHa zSrV^IM<{iL1)^g-u9|O-w~B>yu$-@Z(RB6Vc7}TxOC0FUN-|qI(Q6Ym(;4ge?^-3% z@pii~CimRz97W&2wd&=UJ|n+Hqh}cr^xF@6OE;DGYXOO`S4>e6>N9qu*<*g%CcCr~ zxZ(yNXX#D*lM@F@J*5!EsrDC3b81l7P9r#tCbOyP?ju+EuGF#i8&?E5PW0Y~bZjJ_ zCf&l}4cH`%ch+!Br9fq<(~O}b^xhJnU=x`s z$Y4|feUk$wMjv5{QCSLJ&MhY?a;M6-Bss~%x|8A6lFlWln;J_?J9hYw`1jjiDs6ZW zdOzM$F1f!_FP1`lb_1qpqKo<|qoCc)kiw~=gfSl_SIoh9`%=2dW$^#6HYFsn0Ox&D zaXpApa>wpTKrmsStx8Pwsu{*FA3bOSAO@r_0G)ZlIhDC))<=eKM`NgjZ*UJ(G5LqxqD}Sa#&u& zmVe+zB=#^ou~kTFh_T)1mqG0F(iJ=VOD0ry=bRnRu{ySZ^7z2#jMGsXd7hMQfzjEH5?E=6-Z=0B+%Ba@PvY zs;URL#+=R8yDG$K6*-@S^?hHsMLsPYJL{W|0XcxI%`mosl5xGNw~i{DA=O&gp_I~B^@G43TRFm`d)%6jx)tBK^zxP4TGbi`cQB~kW{3?T0EP}vGLFoO1Iy^D=|DJo`JytkQED48G zuonCquOT&EaY3&T->Tqry>D~Cf+d=1xsVlRtT$LC0~%+ruh??g zZ@yg*;6{ryJ+LFpGx@gBpFB#m<;qIGoZJdqAoKUT+}$9jUE^8eHt^o!b-xH%Q12YM z>zNjvix9o_z>!tZ2vE}(@TqDL_Pg)xt$G&~&BQ(HaHx`Ls?JJ6@AL%Pr?DO(A$AeM zqzrnENov9Yog|r>!!GyO%Jgqi#0fqu<-wJ1hP>(b&zgr- zhg!3?`{(lW?W-ylM`!xq@O_GnMf!gSgJH#AEq%Sm;B1BEuK0c_qDxYf=tEvR-+4_0 zGsFtL#8PBn{|+2m65d6u2^c+i=e_#!(*vc#A6(f@hPbF0?Ezw-( z?8A>wP9EbYl@(!kn_j&juv88__Lhw!IPjYcxMEiMCIOaBd<#S1KN%8#AxC=MQc9H1 zF_a%rV|~-TBciK1FDUMV#9lyVilKjxfPGOD?QhOHt~!Jmqx=f8=Dj=glv)(EcZNu$ z?@@;Ql8V44RjzPiE~Pk`QvQTk$6rEuzhgc9^qU3F#1&x-&-Hzf?PR3Qt4=4@jJC%_ z9c&2&?J(H^9BsSWB)&k==F<<&x333-?ez!K;t(J_noM5)vl`#%Cad2CO}8&}Lg!0u zy){3GpyBcW_I`&=g`-%Tm7~7V`c!@4@?gk7fKJv#vVfl8Uh*j7q}dA8^ZQ+Y{^WQ4nq88(!|k86``yW1X%D zvlYn<(;Bwl)xpK3W;DV?4M#>3}xnL2yQirKX&bTt!ML%Qzjh$ zFLaF;8PN9dNO?Zj`K(K&+Ca4AsO|-gDl_%H15#>?&)HETSlx z5`_qL65Ums3ZKMjc~`wH9jnY zsrU@4Aktk3unLi~+jujKa7&2Khy_CK%PrvIt2ObgeZwST?pts$xv;g|{sf#Ia9+** zqH=efhc#z`PYnOz^(OiI!fwf3td}UZn>XK8W+ipCu$afbkNde0X~=!GV!)}&-lLA1 zsoX1>{5Om2b)XE*K;6jjWZaLm+|q&b(8#jqeDt@mnwf@YVG^5CJ1Ed7g)L_`ElUz@ zzAvkyDEp*qh2A%F*}+cx;IE_7?Ac?XyA9GVIFI$~np`nf5Rh2Sr|guEY|Vfk^Q)L~bgk3hB&Yc$xifhRI{$(dmlbg7nRfB$)}eS+rK& z>$;^od)5Zl@-cHX*vg;W6wEhoy+mIx7^KPpeA=E)CpJUO&v_&H_2qVk=v(%TOj}%{ zd}lc?w&Tb^q9BdYM$@SbPSm!_X`lo0e4aW<@xF9@tcAXi*2~4KB$~r&BJ1wJWC3^H z>#rrYQ2gT!>6Q|Y+f`|Tf$4&Zl}Z<0j9}#(*>Y2CE|2T&1yCYKHXyItO4++& zCBsUwbE;|~%@$N*{)F95#}}d{(EAymW`ZfzIx_&N+Hlk{_7Me&-+8tVekD2mn(uc2 z{{<%EvE>lHilkI5QbB%EG4aYr;c@J`DY)K2!%n2qp!mng?drk=U8dBh#kK-BNUWhF zS?8JY{!*FN9>j-CYH>G^O*22C+~IuAG9N_oAMaYaV%++*#F{B;Rdg(fmcN((=I6G3 zzE5Q?ee;jxf0o7ReEk(elT~2XQ@FQ0Qo0n=qT&AW>H8J zjp*i4iCFM2{>%89(8tw%O01l}SNC=>#8+{1F#TF#vwBP$9 zi{NN(d;)krlYwi`yxkg?+oqDfmMsvN>3_oiiCRYU2nRTez+SxQQ0<4-&N>4t*N;yir5K6$~Q zWlTBXPaVYP{7SGi5UV}_uL|ktQf8jKX@C7V!kLv;RE_LY8uk;f-ml$g+prjvx2wP) zJs22m)O(9Vu7pqOC`@(7e z7hAJe>}A?VJqy~-&=-w}k9gf@-&|FxmRtKM&bMvSm{*T8(Q!$;H$2jjs(c8CqMVq&|PbJtJ+@_nSUX>8A zzRf8T;+qWoOUAv|GPx3n{&QrBFC2wludF*SQvQEU28yOKIvZycSo#@<2JeyF?J@i1 z(fVljhJAXs#Kk8V^T&_=Ph<1bdBC;dV+2lRZe9svYDq9MXHa}L3CK$8gXvIHTdVSg zW_~U{$q1ZdO@+WzXuFE#k>+s?V~=h;jZ8*ygJ!jFFF#E|2mn=j zd*M-n-ly04aY|vc67M3)=u{q{uML&lp5ZbJI-BPoyR$WS4v5o~g@R5A;E)fU{&y$e za4Lm--q`*7qrEHE+9G|?Z*JJz$cZ$Uxauk0XyhP*oB;X(L9FpZQ!8`|qg`_Cz{ok_ z>6hq7^NAO6;N)1ZvJU1o_dR>E`zw+N)qeMhWU483<%9)Pd4v_J? z+W|1=Cwfzug*K^`;|2bH$LTy$CV>g(LEA4>RD*ch#%7%c< zc}{7^w=Ar~iUc9WP;kG5GahP>wsEJ2CLQwS@`~{EdcrHcu;>9IJSab~1mw^ZrGCky zPdg0CB*jg|i>i8VX4VzTj1~%<){|f?PZCvcy{zWc9tzf~LT%jB()ZmgD;tkDZzr;X zR?Y;a(v`pD+ehAij^2;w_i#vJOY3>;9ZFu7!e;%Ajxux*nDsuDMq;Q(>(jMK=rNVR zR35ZGOlaWR-RCEk5^QBFKXHz*$ld5Wn}yWIfMcdH%EhA@RYcB8@Bvx==T$L}qu=(& z6{(3%f#hZXy8E=NbrE^i^;pmKDE#g-5P*NmP~FX1z+Y}0RvY^!Za3mXyH&Nj z*PCI5FcF~-weyr87_gs}?)Ht*`g*fb?VkP!G@bl}wgiRW>HUY7?x_M zy-zHe=)_~Q$|8S!#SS|c6s@xnAfk+sf5_hZ75DT+?v zub*dnRqjJl_orbv9Qu3%T@~h);?psj%to9J+^&q@cCUW$Z%o!rJ}Wzj2g^ZpW~&h? zZbIMAA6WW-9;~`ok9;1S=d&g-@N z#e25(RL+CI0}M+wBF5sF!BQF0t~h6|?rju~E)g8nJnbw!9^oAufW;8GAWGwg3r=No zW=P$F?p} zxbNShrGzHD9oe3%pPiyEjm^fGN0s`HwB(t|&oapT5YlCz~P);)Ls*i z>3T7Kq^?)N=*KIQRwyeY4MRb$!kb?dIHC5XbNd$00^aCW8Ks6hOH6!2Vf&tvq9fO( zgB(4SRfdjV$+ls_)D;(Ix7PIf(au~gj#1?n1}fC6T|44R8b!q8O>a_Vf`#mkG9btO zJCA8XPV)t#)BvX1Ad7-gVCOwIJJP;m&CGlM-XZQjS=+CI-`v!Dv;N&@56Ha#DT37% zHDMS?n(enXwofp1L}%(fo%%Y5MpW`gPQ>EaS<7p+Q?%&1u-WW#ZSjU%>*%4yIgxqt zk)%v5ej$MHd%3|LZPW;om6V8-HhhOx8h(E<|7to5XL3VHhtZ1E?qto&Ag;Z^+whdi z`qlrjN3b2J(6>@R4v1elhIjw3^W1UfD@aBkFusA&Pc;rpJ)`xBdGG4lAJ)qu7JB{M zoqfJ1dk)>yTa;m@SsHGS{I6J|0@Tqsp#CCFa$7ih#b}`%XP^uTF$J2@pR0GhUd~Dt;29Es>5wCUnwM5 zrqtIGUAsW?Tjk#_7S-34UeiY3EqJhazKEX$`t^}aM66Dm&_tMsc!pmK9UN_LB^DBKPP8VFcMsW$P*Z^SW5GSDnFY z{QA_a&Q}PFgm(g82RWFvS#=^+jW>Fc)gU@W(F0o+5oEQ?YCV^T@1kfTv4 zQl|P7Qbee7aEpTfuix;QC1On61bcsw&-VP2cgMr>-t-TMwON5F+}VK+*X2^aj3?r$pCGcxx-S1hBLxOz zSRZO-`wPqcnpoTDg94rQ87j5!IokQD(Af+w1xQ{;Cd8>h>+4I+1_R(ln6uN1)Ua4) zAA!OBiRxY?_(oewzA ze>FP}*Ak;%wV`?iv!?DEOo1c&=Ak3cp>(nF%|G`nuvfVNE_=`X=tp%ILDt=IUwE5C z2X!XOrDLlF%C=2VSRO&hucdlNh-}Ftzm#tOP`PR@0)zPM*KRbs%}<;9cgaCIXTFTL z=RsmQg6dL3+>F%E2S^#mE@zMdXO9$Kt!+h+xxzQ@NB?>VZwT_Qs6zG5OH%!O5v}Kd z-jfm47BSM}Wclkj8Wdq)C0pY{>L#}5AXKc0eF7CiI)p`X8LJp-<3`p64@<}x0VKOi z#y8#SO3J*gUeX5WrPx#`!(;#7Zpj(1Ufu`SV`nFSF$eANSyJ9re{b{T{YF4BO>208 zm-8!_b;itGRN;AI_+r^0fn$<%X4P>#koirn{q?TP%j(ka>XLHefA3@)JmE>xNFdz^ zXUH9wd$g%srpSR}HdyEJ72@dY)chICd#_S=8*vGJEWx9GS(j5Eq-qZB`*`Ep=TxKa z2K|+w2*%dBxdvcMN1vw9{&0i+$QZZn<$jSUsJoA&5Ou{ceS0&}QYJh5Go?mQ4|=>D zqAsvp7-kHhJN{A6**k3$tY#~Efc5^{#kffvMku z6)5s~?89x-ByuN9pAo1{Mkf<;C!aS3&BGzZ_=ca?DDHn{zVAikG#xX(9>&>3c3P zJF(m$W5SF-fn+_gU-_e*GES|Wm?LuDu9q9awzsTYv1{zTPX^V)qVQBKt(vW10r&gl zt##u#j7FjeOgcLD!21#hJ~d}be1U61mpdK~t8p|3gzfO=AuJoN8_ZeT{}=3gIXkrs z$;pFBaC6!3+j(>ojj}Z{s#%JhCadU**6StO-ZRTuIpFx8K7R@x%}xo8Ot%pN-~Dl1 zD+KBKe8(%JyuZ$D>?;1+DWI~ik{fon{ukhT-b1OyC|M?xbl42N}&6vBN$HeA+@EUjp;hL3whtU$-feUecf^yDwJ0U;4lRy2uD=gso zS{CJ_6b))JfN8oPq30)bghMLx%v=HYYa5*+v}?ZYI9p*H?bu@O_{SOf+K6+p+JpfK z`Pf$AuhJ`AQiEnHw&+@!c)fpVP>NGrI8!6=@Fyk9~&!b;epNZ5SfJm-Jm~`g86sd{D3L%?IJsW!2nl!7xOfzDi z=Czmp7I6ZJU&_YiU^rL!%9YH!w7NjHH;LM6o zGs-CaIj>mI2{QQ4*+yTzFrnt7mb89LitFlIQ%*rOIK&?e0MKgzmlDVYxX%w)z8W+8 zq#%OqKm>VVOP)7KynQ-|Iq6-gj}{lYL-)lamHpp6l1{Vy%?B}Vwz-U1RARDP+1O3(yF;AH6(#e`8%hOaPBHFmg)A#!Tl1`Pj7oDfSb zyJ+|;=q|t!cl4@Zx?2vm!l+Aca#UAnayLBh zM>xn7a)VovugN}xro2;ryok>?TEYPQiOOAjRb=ZiIEhPdZrz2f)ds|D8u4)7*gh8{ ztWf`PaGp;iD`V^UWSgB9;4F~;s({LrZ4K{}@}NdGEKM_KU$wfo7zjM1NI{~RocxJ|(`=vr9$VjQE&06P@0$5i2jGkj z0K|zrvmmymXxCBZ&EydT zYMT1-&7`k7IZUMVcf&Qc*3qx9Aq|?=7oT7ofwk(P!)MdreF$GDwA$pL;V-C(-C9Z)v^P}JG@T1oO(|n-YACSJVCcT9| zqn1)7MBU30yJm|b4%$mpdi2P*dOfhD`n$=EW-F3C^K(QVV7>_DhV>`z9 ztybub@$k9UJ+Abd?(dx>TO8#gSBvFvt+h-{k2P4q5_l+0ir-!=ZTJP?8%`RW2SJMV8U@~kvUPzZboAF-t!!B1K`)1 z3EvK}s&+n0eIO~qe?XqtpKhPZS(eFynw3_vHBj=QRjDxetg(x`4A_#)3oX>yk>=di zsVk*nn8o=jC!2}GE7e=d)fjXEk>Rpo$KPz_!{F@L?+jgb2RGkkQ-Jj?`}jX3jCPHG zaXQG+pV$_4CbJ6*5CN^K(o-1tqYhT%4k%6xVxFrH)>xndRi`P&4dVOB{tS%hnUmhrYJ9>``C|I77_^gDGWJP>X2zMhbe0_Q>_b&;$!9dwxgdY^n6e1-0 z#C*gr&~npQ-=C|XS0bfC>ax7b*!q$#=pbvKDT1+zOrub;&UJHPB^FFr8fGv#Vn&At zQoH-?qtLnw1WMf$#RVUyt`MSh^=>@nCW*((HbkfTT~oI`2i>)qYhTH|Jx}{e zH!T+21Ux{)pQv$a{h^F(|hC8VX1?(PtfmJaEZ^ZNh3b1u%s+|KXad#$~CJ5 zhzSh#`YXKG`8sy3NX(2-{LWtbMsT|SJ>??J(p$OO=Qmt5#&ibcVNG6fcG1yaLj*oX zY%GW#t~l@2SDPi72G`^4!=9n?Gnz9TEIPA?#Wk>&ji8x7;`tO?Q2K5e*d(C z$=b4gSuYRBcC2qc1bTB90@xX%|!?yILV= zFjLC1{mG-pgrYrlSKy}!rfi$fKs`=%Phyygzb&GU;rp}W%DnFP48mEP3S@+2RG-MH zfilch5vWn=1t-;#@^(=UkZ9BvZ*y*-ivMpm_XMR{@^v%h$Vv+>tLQ>JkJ(vCVfECw z9C!K2p>Of8ALKY(9#h_nPS3f#qxpo6f5;+Eepp00L4f1|!=dUL$b;oS%FYhG+ipnT zEZzTwBMjRhMKFCk#QBQx&ocdKV)G$J!s4F8CVOF$cjV2p`YM!&s70um4VTB8%v)|Q zdT?7L^hZ9G3-Vp_tygSBpfB3HV|k(k`xVAnwx~qk55n6y)wHHRe4gis-cuiH_wD+d z4yv9+VMbw+ftNq_x)XU(;zptVu~bd=E(f9qcWEGE+OQ<&U4cNvRJZ2xI}ych4h>*V zKpt6AwZ!XYRwrRerL_C+^%vz^-!IB2Z@RZUmnZiMxxIWpT86K`;rl#$eH|pfq`Hz3 zQw8fQQdnq>W|(iq?&TW0xb4gc(~UmWfy62HB(zt<1;4_R=db`5bzX=XSa zRa#;AyruKe5$jg(S0^N3N>zqIK`(`N$T5H4PI9+|HJ6LGHkj6|exSJE3JX}a(jDQDE54*0D$l)B4P$I4*Rh?+@Qr_7rI zY{_TT(KqGc*|WBV_~-eF4==G9;B|~lH#PRb?j(p!r*yT^_HPG}FbS4TdrQOefBw}t zWh`X4icp5^a%CKBmRtPvLivr~fCEH;QaM8Ih|jk~POgawZ4hdSc-18HBa%}X5&*IjO|6klp5%GXOV z-F_rXvQyJCwdEH0ZZ*2!v+AEi6O0j*ti@ja%wG!NiRL zq&~YG$-h2TVkD^i(G_yJlqB_&tKyx@ZJOOz8DRyU;0)uZ%H4a={oDi7r?jFdV?sd$ zbf6}s;dMd9QQvKRtKK+V4z2AX_tvs2g;!8^#IPyX42^~Z&VZN# zrs`|wlNhE>e(znI8Bs-8AjQ~pozkrO*JEtKBa~X~B|%{h4T}1b%lBFKG8#g5jOU3} zlW%M9MjhwbAwt;2ahN|vF78w`+Imakn9RsPu!(1^;(k;oS%j+P6BGtusOYzGfF6GT z462*1)GKk-dVd`0@6{^*wu~w}!uH2)G5;Hd_+W$0uWD-T-VQUW!T?75_;(G{`KEkqAyRc{n4$s)=f{qAZUe35!b641sq~}O{;YeB{SvLN zn(O9eS%*cvYD=Uz+Xw6qMCwCIMPAhPOfw3xtfEQ8w=(>MYYtK-k*S&1 zm@ombeewGxa<2N@+Aw#&3?toadQxfE(K7z<18mwxlr)8;<=@N$DS(#niwv2m+J5R^ zT7FPspGlGQm!H(XFSt?Q_owp0T&-%pj%Tz82zr@>EEaYxpK?FH>4KLrz!z>!HK8`=KtK?qsB7a!&vBW(mF3S3yN;RjL zr!9QPpnx$@$(_VpOf~(qjT=*BQ=k`?&i!)jPRP+Up6Y_1G9;kOt>~>6vg` z1)*i=U%`1&kV~~}B!2z5h;Pl19U;H7wle2@c4E_vrYXdAClUCI)9i76_qd+hrDOtR zsF8#H%bNLyD1r3px4Ci&xLNvFeGhmyho>73EUM`>&(0XCuARDt_=q*$X?aI0mlc*U0n|Z=6?1iEixq8m_J86Hm7}iN zH=o&S43ReFmt?xbUN3H#A<;vFn_Rp;a@#+gJw)HPow-m-Hf>9x47<130Uu!t}-*-S{d?3m=gVCo-`3CPb&lYcpnAu z{L(yMf9=x7?&@we&{HaPI!@Yxx;t^O=k*FTX%3at9D%xnQss5341Yt*4XWLhzqh?; z8XOYIm+{wkbdSP`62phbuAqyz^s7Tyb;@CHc0glh0g+3XeE9&ZAL9p$w=wnu4_hGn zjAb0NArzhJQ6)Njylodvp^7(S(i`5pm81nuym48as+m2lykEE0GLJuRbKl!oZ=uTm z!07vt$JnqISx0qr;D$Vb5az$~E8Py0IVt}nkta@A|K8V9-<|9GP=^@+W6e{8FY+ZHEnx6rFK$LUAkt$UKUdt4s=Kt=vFKKT z8=XXsH+h?L2Wv8f;%NglqRzDaJWniZ4=yrOx>PeKuZxTz{s&J!2>`g$YHfbK;Tmb5 zM1>idbKdW#Ai7u>YqDpQ9i=KI+wm-G3s%OV@kjd5cv4_nmVA-=3=4C)(+yw~y4e9Z zzMt)-q1iZ3#M(Y*t#>{8=*I!p>)bLek7?9NfAIP`%vJkY-NT!4-mPoN97za72)DRzKy7q83zA|5J#Li)?+ zMbzsyl1i;j%AGL-fI$GDRC;))I%>4p%pt6xinU_J62-m=fQ4+!YLkiz_t52j)cr6b z!AK_%vEcEmK1Kw8?SlH{sl6Wdg*KZF&7D04%#SP*XVsO2PFvbehKDU;^3x#LmTffj zeaDB`gU15&&l=Wr?I<1*&WdV2G|;1H{V#v%AeRvl{Lxu(5PL9q5YRido&c3vq@1;Y zj)yQ>!-7J=IG6GzICCL7t&<7O_sfDpMsh=8OMSH!ILtR=zr-kP)JxtQjFlEIQnWT) z+%2#xQ-ZEU%L+C?c3V}7=RKaUb32o$#cjMw%k=$?_YcTaJq=SYYcdVb%#|5h_+wDJ zNc%w4#UIOgBg9^fyxI&}bW1s;VYl+BiUbKv&G5pi5Z3a-Dms^-q^kkhrTSC=c9|qw zxvS&XkDupJtZas!6Cf-3Mc%hcXt7YDp*S24z6+z~hU+8r!gIq%SWJ~Fr{qut-kyLL z00ur>gORfjOa5mrSVN|>qXogZ;*1h*3L2fCcD+7oX`Ul~DA0ljZ9)99+(TL{q+OvU zlYz0M^;Ghcc9lj{rXs8V6LieQZmZqDSrM9DE^jOT`XG1l(~e4^{|Id0hZE*=3xHQo z?WzATTi9fmlpjPvw|ga|q0#mNmOb?-RryI^iLJXpW)=ooTC4f*-p>gl>v8s zqi}e{=l`hB*I|N=k<)og+j5o_e|hYK>t>1tt3HFc`n#6)pLwUJ|7YII<6Lk?Xu&_( zY|#>&T>owA-i(ZCy-`G$k1%}T2D-&H0?BXS0g$Z~nI_@~4{LZ?ff!mdgsG>(5Dx;~ zZM-j~lX${*Un@@C@6K7vCUf_qG;fSj>_^U4jbjrW_;4Lkaqbe!iOYWY9qL^CJnnK! zWE1uGp4c7Z%oGsn9ZoGacH$;5DM2T{F%xYUk`3you|gq@TJ2q^oUR$p@Sz}yPMhU^ zG%`f@st>!|?|PHl(|=MeNM{#geJDpWtx;a!3UapEyyla0Ugs;{+9TC^xaITyY`V3V?dNPZ%`8;h+xH-K;Z(u|x^a^ALq#>9vqEBT?<}>yGD|652Je6`W`f zpK#XPc}d)HHTcQ&sAERHsZwd6EGuw{^M%yt(`mX?)Q=&F%Ra9A4Dg5fp#kW9I|Nku zq5Z*-Gz`gvqCgW@-WKKFA zub4%72vgnlo3`Zy&-Uh06J>yUNz3ITbsWs5$)(@ zxbr(HkdiW>%UFW;A`Sq*wzXE}^%RejZz?5&c3q@Tsc>6l0#6~=7m%Nk9_2X0oe@W* zIQOtXs9_a%0t zd+OIKO1{`Y&2@1za$uZcRr}tMOlL(d+afn!O*_53bt;O0HRs}%e7d~m$#BhBWJhA0 zmy1TLTNa@NCFiL0XwG|^4nv7fv^eWO`a=kvw}I8C4b8qN>7S)IOGf+OgV=LhnhC&f z;vIzJ*zq_Prst!wY&px^G@*oQaexuEKm%BoNef(??}v++;uN$>e!6?H?T$*1@~YLg_GJ zgutGTXmXW35&)sDHR`tJ&KeaXNP2NMgG~YNZf(Q}$z_gQOAaoRwkWdvkmfox3Qmn- z$=U4sy0q?oJhf^ImQQlv1D_(;)Wx%vhUfz;vY|x&GKtj&s3|1*) zeO!yr#bMwNMyiMfXG?puaoQA#`n4@EKMkgT|4esRTR)62CrfuFL^{|8i~2nm8#b6p zyZHQ|`cU7kvt^N9wKc9xrA`StIQC=RoV`p7oTi(z&#$uzX%A*S;_{>ZI~KyF1LudwSh?CXswesWNQ9W0J6GxP zqj#E+Bo~$HgyN|83%#u6u1e1QYe{LclwNV^KWAQ;q^Q;wpVRkbfpH>XwwSSG)U;|v z^lMd&Ho`>f*1t@#os82YEzk9|06B6(ykQ6QHl=*mq}wT@lKLi$WHSG7gKYEE*}Bjgj{Ox#3VW^<_0 zhUr-jhWyts*=;gr|4q``?*o@3Z@Us;KM zct{N*#X!E7E&X|Rl2fZ6UoRpLsEma!yA~R{=XB_2Vm&1YnNmE@=hol6dO@R)B%8@; z4WdD-&rq_u=!nN*bt#Beaj&HBMGq;*hsOxYFiDX3XZ=cJp8$k0>1h-8I5=K4_-WDh zdD*nEgps(AdB}H_t9EPT_sF{8flrT|XP0X{W0_tv%}B3hcvMK@u>(U1`uYPWexmiF zR96+206WZ!UCZq*XX8_-BqFihhEn5B%qi~Kd|2j$Uq!J3KJ?iCGiu)Fzy@2$rD~C@ zRkd7wDZ<=LNJ>H#K*yvc7!*2b0$Fv3S&wog0Ix*t`>%I!SO(@MuDPymjmak3uZyrH zw&L%dYSnh5wN!>A9NNMfF;uSX zIe+a(HyseICi^24unWOI&uo6c)I>VZDlD{MBb4n9tG2j=CqE^v(Eh6y`3?rgd7|97 zhc7y*8cN9^V4Vuvh!oBaaNB#UGp96C1cQ z+YCu6#O?hIb;z9Odtx!)ihI>|`$MF0LXAUL!E0NfM**KD;5qw+c+ETvO`^ zJy9m!cYVT>aE33^1u+KQ8!I*9>8rm+A&>nXH*##Cx#};OMEhqqk7lV}LYJifnIhT7&yA)GX}8!JL-Y2-M#D{g(b*#^MXKfn2Fyt(LI)A{p$Y8-Zmhv~ z$43WRP0oq@PgCn1GUutDkRqibYXt;P~m_&f?4Vf+@P zMy02K+F0g{e@WVo^2^H=Q~c>c?z?`5Hj9DbE_8`7Uwr^NCZyDnb~aQ>W6EIGfs|3! zs$A{>_48D2CalA3N-agy9v-@mN6Gz6d-do#;gYM{?J_5hzsa{(O+G(hr-{dJJmk9@ zL3Do$hH2S*%-2e&cERtHzr37AL}`8*AOsR$`vOUz*vKRYVQx7S4TZB>h}fTDH1I2j z!KEd3u6LAiiGhSB+&F(6Qc2+fNNs5~4V4D^2CR=A;o;bdd#ryZVKw9;VEB@@saLh_ zFNQ^8q#^DqB8*y^jpTB@D>0{}UA2Y$WCS14Dl!)m62t|So1<{PSYw{Q8g4|lBMG?l zkKUcM$te27r9<%-;INs;xE~UjGFPLss-n5C-w!ZFw>VGga;1-@!`J!c zghL`TrvOp!KN8DGa=xBhTEotPLZViM67(~E>L2=<@)TBF}VN z%G&ih$QVhFe@LT_Mz8J|&`yhI9!$A4Eh;Wc-y_`fh}|vYgaO({d0G1Mc#EHl_O=}H z69aeU=Iu1Iw0FL_HO|~AL6J6-slz*>xNLE^|apKBGw6k+H~JcMi;^ zSJ=UDNx<_3(ZCLDK$zrYc&v1rW&H;`PZwvxmP%Lp02VtZEm8cwpXH%?X76ew-XG!Z zjr>x{sy(%hm@@;}i2Z(@Y41AyiT;jN8lwh#+`B;(c;Xc}zp1rHwY_C^ad~i*nY4V!BouKCBuYHWJOjSb93cuz07Sg5%M!+ zQbxs?cAotvrUrEVn+mY$Ys zgK?BZRkfn0=+K9?t=lo9IGhYo(bGV9rs1!~n+IVH2Qk6;O5q9(ynhrn{v0!lTSi^1 zyV?%{YBv^@dj(D`s5zrdwRgL#eLi|LQ(8#MQ9@hNk$4W5cU{;J< zn4<3Vs-Ghai;TzfYhc?wrWOZ%?|q6~VaAP(%{$=qtL5~>_v8r!BMrxNfMW=_KRhL& zNt}+Xo=i^?jZkBoa=SERngKOmA4|n!Cs;%oH)DoSxF<0N5_p*I2iLD#N69*6(R?`? zTCPpMwwb`I1RF@u1Ac11dqfb=`lm5yoQF>#zJn7eQ~dv5{<#+)kHI9#~;GU*xoE)+WIacJS=iI#NOM2 z7mxM$Aa;akHT_*mu@>DGbww$azMWBw%xOGSh3;KQ6wl9QY6}VE9Au+-sWk~7-^Qv3 zp8@4p%;S-u>|;^)y{WDFJ#p8Kg_WK-qL07ACpj8VVfp1-cuj0eqdu1xtG}qHJaR5D z;%7dL?oZ}~HCVkzA3<9=O6`*seHV3Bw`aZJ#&8(~7w7vCmY`b6vVIZa>~=+bnw;>g z?O@Sahpq&jWzao0g%&Q(EETJ~ki3>xTS&gF;~0!<6(QDjlPWiN_Y*+*T$0xmh!Px5 z`}13v_;8e~4pNA(Mp-VS$rEw{R;m?1+XAaEdiJx{z<-}lks73SpnSyHE?JEVQThZ7 z_tb}ot@APN+Wz?|z2P>pMKzOtJP*gss=z+HTXWZU2^=syIsIiEm7%Z9f#!gXKVkg~ zC@&5crZ(*pG1Xv(RtZb(*OGs{fr7Jrs)Q>9=|FJ+8P_*L|4XMy*w${uaF2@S1_`o? z#k3wm=?!+?C18TLo>#~spv*x~T!d~xUCPIs-*(LP7V3OR+pZC9&3!5}-_dW3WK;+hY6*7rqhvN;8igE>WMaT6F4HwRiNvt-;5HYOhF`fMUUIY(Avb+0jMa)A22(6|{(qSO6s)>-I! z%b)Y%QBCN_=Y8z!Dba_(!ElzhmR^J-d|?2YVBj#F(C@JX_xquc(EOKemis*?F%It( z+HcJybp^+UzV{2SQ(Z6tv_Hkv^S5rvU|P^%{A8ZGzMQ^=xoBG(uhLg>fbnf@`}Xm* z7WJ*hQeRTYstQVHJql3gzbo+6bW>Cc`hWgF0486%2{AJlD zt^L7>Wm$*~J`GO9&}CN?vjFGK%L#`+eR|+M0LN5uD&#YZFM0u{Gq_7YmxkKG zT(|8|yAWItYmZTwK0>3V{evRn821ObO}19*R{bl~R^u`~GHuN>WScrfX`W>Y0sW0^&npLAS0tTtOz-28~*3g&T)Ch^2*a7iJ~m|BHn^BO-#M5 z`gc3<01aUi58K*|M(O;I#*|+lHOKBYE~gs`?>1cMjzK~j9{qHy{@x<#p^E3$x`|)h z0$^=?Ou$;rch~AREpZlUzi}e%^Sw{PN5O%uLOYpj{V$NvU}7JrIpr4trdnm-<1P!U z2ObhG(s9z^yZ9Oe=BpW6(J7QYTJV-(w$5NmauvwzZ9Y?Rqg`-M_;Uc5eB@&O=U-tk zri19{1mct)e(C1hYkeQPn;O+977D<6M>dz3km$Q0#ne5VhN?v7FRfgOKe2qpnepzbm@1j_+W#aOnxeI8|>Vy_o{LAtKvtr{8hlHe3N{h4{Y*y zGup=rZ+(^DV586RRfVQd>PuNTohjWj=d(p(K5-1lwZyJMzn$cblhok!>xvYV)kd;D zN^3ocAn^i(_huafnwjw*$BD7e+j#p*9GmWlGN4F+a&+{uCPu>|pb@zi%KwEq!bcnXOMhTG|q0Fo@#%^CzZ-c$Dg> z^m}TwzPFz?rL&~Xy(Y6?Fa8Dxgl-~{SDj6!o*FtM;eP3RrovbpBh6N01s?#givf?RoLq{WH*SyeVfjq(ftAgC^~TzK3MVPhNMJ z(-^&d6Qjlz1ZNx6z=+HlzHxOxnpDf zR|x-xe#(CWB(S`6JsI)f&+zgF7rD^+?iVfR08=p7l>J`9EMM z^SdHZ@G%orGs)MdYyEyuim$<0A1wcfkys%K4y?cs6(+$Ie)#hC4nRlTYc=3wiTPNQ zolbCYusBT7!uIE`_h50g=gEFgUZyiG{_v%2<8d{d$hTIe_pGwWTNdg_LPLfYXoTqy8YWK(F1!1lv`0iUt;+)CEt+;7Hov`b@@_qDXp{B3+? zc;rK)541Qk!x6C;C;hz7J-XNsVQnPJ49m#f%`U^?up`FEeH^M_Bh==OvxtEwGMmQO ziGV%Ojci1{B6fA4h|ZA|iIM6Fk)T9-Wm7YXnXB-@UJh)mxIVT1jY40Ga9 zvFerexD{Jzx*hQx>y?vHKLz|#=l$T3G+|r@Et>b7^)lOTcT;UN&}N*d@a#yND=yF|r**mj zTCO_vLYOzNuwO`Uqn6vOn0E??+Iqes$$k@>kZHCXC8E{$)nV@DjqhxMtzS>U)Lc=f zUF~if)(Ue`r_W3Xf4Bb)^v8UATSSx8D*GQYlSg6nk;8mCh67o@)ISNT$7|Rl*JbR0 z0Z4v9-@l!!Fe3E>g@zOp3L(d$cNxaG=Vx_@($eE1*Wr^`i7^4`%=o#M8!pWm`GRNOZKZk zQsU9flkL)~S_GXFq3G{;8&3&6(w&ptQaX!3tx35G(E@KtiNX1H$_g#>uS;e2k5=xJwPYLeNxz(#_Z&g#)gaxcb=9OziVg zA`ce*m3?(1u(AE}!RvP&mB17E_>7uW>#*vmDgbSVR?EJc`4flpT1`!iC8j~So**5O zs85v)f(rCgVYbvQpiA$X%*6UF%Fjuz0S#xf_utqTQ)W?WB2Ojt{TP5j!21q7a_Rfc zz&*z1SCqwNcuDM|3wk0qOd2X-*PG$StshPDL9PW{(D6{$iqPk;g-Mxt!gVZgsIme{F zzZxU~VmpQZagrTZRmxeQWi<#@aFNNCbQ?o~{>rIpNVrS9yh;oVOREZXdT~HpA@p1X zy+r&!xh+!v`;uVhx0C7s#7u}3l>wETZUJ21UG#01#JJn;+#hOnD9fr_Lo zEdJIbMA1iS@Y)%T&cKg3kd%jEyOCnkK`Lv$PoB_Op5zV{_F=n@c1TqP+6E5SX1Ee< zgo2}sn4$9 zr6nP%cR1V1+^toh=$_m!8?c3D>Ww{Y5lt}~JQk?J&Tl`&{MkUc#VL4Rr=D?%03 z?e-*zr$jEn9iN%6dI*zo1eYR)hsvbj_&oUdr<=z-l7W6$G}@KPh1}+w3_p^J&$7Ol zG>W2%Kq{#{lw%^zCf)KBHBcR?K6G`MFa8O*Dt|l}nUs3>EaE;>)0~D==XWknC@Z2J zaQWHa&YSlTHMQ|LWu_&dM~EO7iF}p`91zdiv}K5id5F?Xr8`EfVj-|frZkho0k9ES z5K(;w<$0gbU+M%178R2i!t_61BNS)2GYO(+dH{_dF6@QYES#y1tNg3H{ zPM$w=*V}e)p6>+JoK><$76IZhJMn zZtIricV6g_vB)@{IQ~kO^L<=ipi=6NDCSB9jiu88QO7}3Gb3FLk4O;>vq-=z?j{%TiLy`kTGgk0(V4Ex>4WLz|;mo|`T2xvYSd&GG_JqF~*G?WM|5tQ< z&g!#bnF-w@k6+&#!4geAsU-G(6=qH%xO9gcP0U<6h&~_4#<|H*p=yJnFY@wxc=tvI z@S012>_-w{F{3j46}g&uMl`jZVG2XKjR@ncBM^!uVz-)S(` z1aPi-0{f4TE}JjkmAY+-_2g=Wd`_KpS7f>&!>l(-P_X%FGLc@O4_5slhHf-Zv5?T3 zXJZgEizC4Yil3LJ`C;6WGIe>)5jlA*H{->eO!eyvSaijPqN#Z%_zg16D~>$>^F5mR z#j>Xlwi!HqY71(iKF(^P)?^BG1|jW44#dn*h-OInEo|q7w_FqM2+zzca4ABf6ouR7 zB}`JMF?W;(3I?`I=43pp6%%fQ@2ggmF=}tz&boB)ER$$idN25q|5&=;zeL_U`(z$-2bpw-)*hRr=Hhp($YzI zDNfcB!?$W#N8kGaCcCEH3}S8K)0+Ac>iV{aPsbRsTje9T_XaiX)>~kC^HhlZ=8~KP zj}MEcGIfL;vl4|wh_pJb&|;j*>74rquUx%5dZE&AVq9i!X$;elZ6c!$-%UOH##!G? z4VjA(eD_Y3>SKg5Rb=HuC~t(y)=Yq?)W3$><+j1J2EyNv@k<2;Fjf z&u|=Z>G{-)&H_T-Ub3&P_k!W{+nXwJ4RMdNKs>9?)rgdA7o7-=VXCDwZf7yKk zh(O?h8{>YLdL*5vtWwITi_-W2U6#=*>J`CnPL=KF2b0PM_7ohMmSAyfLGZWrf>QPV0BKdwtEH(DM zM{K!2h|X(2j8zMqVUr^Ovj9=dQSmB_p5wphFN*8%8QcV#;o`v@PXIoU0^>Dv#fea0 zBdDYCUFZXc33>=wkCCQ+Ndjnw;p5iYg!9((L_PILe!lxmHKdJgM_xOTMXvq*P;+ox zc!H#Td}oiLH8-tdhOKI-JL(b>$i}>(;ptHo6k!>K?iYU8AKriIa$7nB8&=iQ1Z4$#+8i^4@?(w$R{i?+~e@8&Zu!d->Zm4}< z%~dWLqw!_;^U*xP#lX{IY+`JQs zR-55*xb-6iaXi%hJ1IH3{NPNL`&DgDz?X0P*tERf#=h2Hh6xLbXgL=)F#fOT{-s(1 z47mpXDc2WCb<8e?t48~ zR1yr#;#nbK$?h*CD7)`YP1pX(*P<3)&V{LN436WX)$T8YG9VV7G9k`?4^n|v!&}vX zi<>I!*o?tnt@brgghw+DuSuol9rSxb8T25m?4Kk(Y_B?<{{jo$S(S8c{VJ5QX9-u< zAUtf^9VsTcHsT++Go)GJrDfXo^L3Q(n~-#<&GW<5xXxVkVsg08%t^8TJbpc`bAUm0 zSSZBv`n22LOqz-HDw*t1^XpU49(UGWnG}KrzdO_*I;P{FK!YN7i9n@^t^pm_Lc^b<45T(DeLP(S_4j&Qh-F-39(meVtrIpOJTSj zo>YoKc7cc=BgVZLm(kvtT3D(w0Qx|qoJ&(w3BzxEl6!Z;cG=$tSX zXOt=2@J_p~;IV~J=YO;3{V$Ujh+4IRk*VfM{g|hT0GoVwO z;OvQ5N{))+H&@y4`sOI|$dc0AS-6=ROn+^DPV}ZP}z`Wcku4BDl z5-KR=0GCC{c5@D;kn0?M-zg0E6&}BnX^z79hz$LeN~;pmSv9tga3(4%K{Pxg z1dzC&cId`q>?q|?9voG)n?zZ>qgZ1~7$sdCI!;0lc~*#Z@NK|BS>NFGz?y10B~l|D)Yh9;-^NM6W|L=r$cL zANczdF zSg|lJB%yk9`6|D^p6d&k3M{cP8H%>3)mWz7ax8Uxn{GqbIX3xszMORnFlBhyY>D5t z3IU!>81s8E!zg?DOLx~=m&yb&B=ASDmRzUX44MBQ+_XM)xfeG(HB0ty>sRvqCTIjk2fYg}sTXG>DT%gVx|iTbVxRoXQ=1=2!Dx~gft&4oru0;Za!;1L2$cecMF-O)r3 zZn+O#Od{J^vGIG)rA}D8>*%cqUa2gRXS+m%M5mitPT5kU`B6%*C08;G#$=_Y#`Xm{ zu>Pf8)jzr>M#>ebZ$CZ-nc{mMl|GUjw4R_M{eLno$k$2C#@H)~B2gF*P88<_Z#ji+ zKmJ?1K9#mg2G)wm{)HCSu<4&6JdqJc2_Dy>PiGfGcr;ox@_fGb&R)b`G|3Dmd6inA z8b9Nz!~2yXjy67|eW;pTI-05EMAz+EOsO)6UaloB;pXC_*U9av4+RDz{6qcO$JoW* ztUT~dY?rsHLcMFmd@&GL_eoxB7WbO=b4-%rw~hcsc;L^C>_OCZBW#*=Kln9n9 zt#nwl(;X8#rhWL?2zUXG+|fJWYMg%|8-!&1oQchfXCcQb`br8s#WrQQcJN!7QM5)6 zgom@q&qyX(5- zW^!4eQ*`;xAe`boS!Ikx$>9O5j7Sf6vuQXkrZ&^sB_F;yf{H6s;f}oje2BcMLPDqE%}bO@Ww-t3W>^86XHOW&tWSG0&N>3}>uh7P($e)a3!l zTZ?6dFN%0)mMWHFkWmT{O%keJ3*ZR_{LdH!E#XROn^l&O3h#?ebs@!S*6LSh{o6tRDq%ZZNUvGhuEWt zp|Aa})~6qU)WX!(Tk-xOk2VEl@hmKB7PN`}yDgH@f87Zx*}vlf5mb0Y>83?^{P~-` z;T{HxO}WpES-)TwRtqE?{<76nS7ENxDAfn3>iWIuSH+@>z~&L zPMFPzruccQmKR`j2rj^Q=5LM76DCYY9IMdYmbXNH2dwJh4GD2KmaAZIYy>$H9;hg( zkQGJV0w0(jM%iLyVsW|9Oo{_juiGa}e4KXV0;zp)Wcqd$VfJr%G9p9$c6(B^hzcZ# z!2tFFvHVy1+(L(^}%ZP zNzO!+>Ku}?15pZLXE_<{?PziY48nm9X`iya`!F8vPJri@l6)$aQB1tBkGpKqKB79V z$}Ic8e{^YjM6Q`7*k_9@9E*tRY0O$#vPR#gX#Tt2>rCNu`q4SB!c?QCKOV|xidH|U z6j0C>7~4rkym!VxErVD$&R*GAGYoB~LS&_E+4;Pd8ZaUJOA$MLBlKnb||Y=SN0-|B$*W)n~7Do9*DtR*SQB zra0^P>~?@jnGF8RJ$VYjZVf4lEBx}hA|l};rYC=2(veW zEaTD(tu`5cf3-Mh{nUrGC$tT*3`1ref|tp+7*yCYaU>%hnZcd3UXRR<{F|_6Gh}3_GVTz+8GQVt>aqY$A-R|B!=`Tx8yUbt69t7ayCz5CG)mI-n&7P#mmlr)okfvwcL zTknuPv$l{&%M2(wr{euebxA4OO^j#{p-2C4)UN99a$P4%ety7TJL17zM64W#He|Z1 zLjPYswkdmsXbqi8xxdrn=@RUmZA?htM2bVU4jo(!K~8mB}<^00DA4P0_=!pOhfY=CcpTV>J7lr6SpkSoVJ(LWl)BPZvV z1`-ub+ptl$6lhXmb|J1@BaKj~9 zKMS{-3Ket{yg|B;UZFWJmFV9g8$ z<-KK-8dRT)VV`3fCjL8_DU$7Jfs?_>=q^cY1@w_Qa6bv#a?*V0V?jteYitYa9@9W4 z;G>t#_)>w=5kuCH+QIiDa$9>@9Cs3exPLKephI1yQm`8EB|OBZv%&%%0;;{a%U?9? z--abgr5$LSnBF}wvB|6L>lUGtO>@eWzk*&8cVnsBF;=4HxR|rMTb6ag0!cK;w3vch zc5+ERpEIX1u0bgn@xV+o>@tz(80#3R0u`7byoqDAq>qrdU>9N~a^+t(C$9geuD_0o z>iyowVLFEH29Z`6O1hL10qJG{NohtJk?xiTkrqTcharR+N@*lVh8Pr(k`yU{-{JNC zJZpXbdCpo4EY6&B_P+MDv>1=baq9jd#7;=Cw>ajZCoAS0t#hv^Y8zQ zMz%(I(%LRS_4gzjIUvvY+le4-4ih;5Tkf-^-lye8^XE5)xYs%JFg^9T{Ki`J)=*!IL3sXmiJ#Dzfac^Lcuf8#5fNmv&i0P}o$g?Bbb)-ngEAgZXGPT+ zTVZdVzo~~boAtb<;1^!~+3nn|K6t2b2Omm&&+g*s$fJz+K=6xmYgX!`)I1f6q@p7E|Dw|t7W+$i^y(&Kb=s`g~blc8troYhTjCqlz;)c{)~ zHS-R}qh+mq;*7?N>R0aQ(CVm!O)LQp{WT(2_Y$w%-@GcKpE9(+M&;@L5htQKoBW}A ztoY-QC;*g^@=hE2RWG4@5h~kV+isTn%&d7y>k_QyV(zcqtM|FDq_5JbcLERCXy+>7 z-uA=%w}hg_TYSVcXN8mteW~sR1F1Co$!#ktpAWvXeCpvx_uDzLzmm`B<_A)aWYh1H zBCgx-Z^kfHUkBs8#bokD$jc}FB8&M^FELV_6KWb|==-yxf=}VYiCVnU4ponmZQ~xk zic2y{OV8W^9QB!dNYktlZEi0CZjfqbSs&FbD%{I--A+{||GhS2T%OJ$RZPN_cKnyd z(GRlG8$@TQvL8FcLqPh}Gf1Ugj?tez%H3j@610*Uxt%uODC=Zxi{{aHMg3 zbMID_mx3?N&gI&G9|-K(DNG=-D3WkarQzsTj5p%QxVZQRpv8`NhU2f-=;y5TYfj}o zPs$jSKX&GWgB2kY*_Z$ojLZve>)F@W69=EYt1+09)_XRJ|U+sY2Iefg&RB) zc%WUbr;zB*!IcOAs>~ca{=mJQ=$`|8oXZ029NVve6pQY9#i|qKU_T-d3^QaMPe}Jp z=JulC!e@ELKE(agCGVZz5!CzB4X>VIC3684w-qmqn8;v`?HeVeEvchwaG*40-}2MF zZTz;hLGaE$Kn2E@;-NkAnXeFs#B3SWZ?si0T>Tp&Ko3D7 zyuv|(=A<psaPdd2MXO4w_m$vw7hab^s=<10*FXOf zCQ*r9yI{w+z%0oJy=t2%{Nuctin|)9buN4q*qyj5e|UKsrQf$4^={X8SWI|3n4+=e zqmn^m`ua7Jz3}U78S{l5KEa8597s= zX2&L+u7B5g^@xvN@TvX>v$8HNq7jAH%;lg{{%S8Xm>?F*Aw_l&DT+HYgV@C=?@xMI zh!Fjpj>t04m@b?wE=hplA1&I$c3$k49C~+cUEu3T@MD*I0wn4;v-V{dwQrlX1&m(E zVO;&o99uw!9Eoac}b^(ArC%L|G z>j@=_9L~&(R?d?Zf63_C6@`0mDZ%eu>+?G%af=R0CaArq&iV5>4fXAkQE0wOSs{_j zr=r+Vw&c;!VMK_eSWZ)@d#fZPF2y+T zB>gX0MxC9;%=VeAJUC+b469PBM420zG#*5zD`o4!a6P9_+HloH^QdV|z+#I`tm1uE zpsR($t|CGOa_apX)19(B1BD`w(sE@#3Pbny7y-v3J30LFi_{0R<>zsc)01LsddwOl%Gn{= z`kbH>%2Q>zC)NePqMW%1&xl;wGUrnNry2WH9v1Ist30S|??(gri;5MO*v-I`2{K0X zcLgWGIxzeXc@-m?<%z*@WK{cW#t?A|TfOK;0 z9zoWBkt}8>B)R>;wd0q^2M*b_{W)WxsR&Q@T_E&D{#4 zFf8%T>VV2UYA|o+?|a&*)BiB;QtwZ1Xgno%+$2Mh9yq6to+#?SO~d}3W4JLa8Y6w` zdo5YCpwpCppTe89;PGc6#2b|5t^idxag$dtnA45sXJ@Ha9Mq^}r|B3VU_Ke7{_NPp+eVu~7$N(d4uEhu{7 z)rbw)Q_DCoc9k@nyAmbo&Qy0K?_MJd%RXR@&rx^)!Ro&-O8+@KnmQ;XqOApB~m4a{T>*(#p^&uLnX z?uw9%zuiIORK9mR_#yPT^im9!piFD0;<%zGIa3Pi92Yx)tmvotkbSX8DqcH z#XW6JwpWIe1;+@iwh8pN&}j30r?(wW2EEb!^h=-goLyt2ACK_vzY{TgV#)lxcjMc` zu?Hc0#LU1NKc-RmMy$0H^_;dK$0$zKH}`6)0N4Jhb`>&yYX+>FNQk- z%aFe~nz7`Midp)rpQ#^{l4pb>zu#A4LHlflfSz}$V#0IvUyj|7cVIo}n!fsgf&6lv znU`w|?nc#?0wUC_ZlDzX{j{BmCnbH|J-)bu73Tu9_<&L1$__2j%!8_eDkzrLF2xIS(;xOGFX3`=qi>9;+~e&Z z!?kQ%>O!QQPhr9Q&Y3uKQHY)*uMbNhL-G76Wr)hhevFA$xtxpBRc%H#Q$+2hwkBh% zERVDY*0P5Z-ux zQ6Q|}``eb9WmQKKF6vE(vI#x(R_8lDpz#@`6{nSH5uxYV*h$R^n9(CJccmp`0ogX0KYA}{fi}`T@%i(Vba8R4-i}-0o2(CgNMQcEEsm3}v{#5k0V{PW7+qkq(nI`O$G&tiEx@hRI$ zE)F=wi=~mE)Rou9Pun?P#o~>~YQtVD(Qn88#S*9Voc(lt$PlcQ#-h+<9~Dv4T59Xn z_;TTBY;gHnF;%&(isFzLYqlG) zz@ef_U^t-XBFV&(;wgUoRnM07^PxzC-uwqei$};?W~*MDs!c{@xZR(V7-wjVvzC6&LrKvF{h;{N&sf)Jm7gOrW;}@5Eap zLs{F-L#xJB6_02ZA7&>{Y~Ue%XT6_q#v5*|O3+h{Eb(E&EX=U*pHR^W-_$729|=#r zuFof(9lbSjw97a@#Ww5d?tZ|$a@2rZ+ws~-t_;69ACS8BV6pIYemu}4LPj>qsq;AV z9+=@!sOpHVUFP-fm#~|HgBwT2r2!6A3f24%f!o&kj$e)k5CJ@3xBt^ zZl)U75R|}1P$q?L{Y$~dv_m$6yzcEv0f5;@#kV13oX)p57u*zqXh9IM5*;`q(YlXeO-TzQA z!a}ENn>0ewv#LLX9b;{v`&L~)|E#WCL^r?uM^z)ROdLcUHh@{Es-=jrs!%b^VGC z7Gj5{`1(4m<8qzuHf01HLG19BlJj4*Gg-Xm^fSB<1s>G=yuUMu$%`~&dtNBBQ7n@2 z&gZsQ`B+tV>p!7Qfl3h2zJ8|ggjsDPCTN6}?rBU2<3%R%bV}ZztNM8A4+HwP9D^J9 zvy)=a_<08Y_S--pxBslH6Ihef?HyRSmfu2DzQ1c;57DutS$jPQO2sz#D54B6+0%AV zQirR2c=!#B_5OUo>h`?X+{%CBw&;!eTeLH!(&xK16;-##Mj1N)TT&5q$<>A)8j1bE zr`ZVvmRFj3y44DI4K`9hWzGN_OQy7Th^P4<2#*yAkra^!xHsJLG2N<*VUMYvBz*3VvT9if@N5g-J6bGQ;xg_8 zj*o@mX--u4-jzKV{h?^D_4I)K^Xfx2n(R1=gVp+jm46*UXAHgH%An?*+3wkaGOK&+ zDW~#h3f5%D3hR6Y0!yr-7SqN*)V5dD2U-|bLvK%ZHfoAJdxK8V`%DyR8SN(V>iUb# zz4yWhhjNZ7k@fAtBoaASBQb=3Eo3jB>YOWh?z@56!&c30>VI`M2x^(|aq2Vo@y`GvsTabjjCJjh(4W7F+j1ux_7P_T>27Aa zvqh?fKJhDx_Q@Uet&D$>>~qC!MQuBRkYCcq@H#{b^cJhnxY$iK^k|Y240^DUV=(s5 z|7`O$4b`M=(S@nVB-P1F$_~Ow2hLxG$K*i&EFY1&&Jnu0)A@IzLl6=RB8t}eZ2t7D z^82g&L8+008lUVwFwUhVEl?d1tEZlV6_pjIdQSKMg)tY~tA=g|fTaAzNZ!))dNi}1 zJRK64rUfUpQ+w_YQ#mH}4a%(-dJPuR+(68K%1-{8rTrW-#ONc?N?8fzm^2pp1^L39 z%7pFpo=Ff*3w{b>s7T0gT(eV{{D=2)IVns?qq%zBg7ZUx539_fN2U+GliRwYJm0T6 zgq#oA0&R7@>od1;MteoR2kBLv)jY0bJT>a6w68Q7eBSI1b~DTT@`;OwbHuLw+*f>_ zL+Kd~(X->1$l@H)A8w_t+xgjYZVq(5f7md-G1a58JH&W0E5n_xvMaRDd652_{|ToB zIgfFzam25WP$i^-mJ^ATvSxcm9*Fj5sq-^>+{wZB2ay3rG81WQv)91IwE4mrk^sV99ZChouAIs^8?D&OK7*RH! zjvG_u5TuRikVzFK?PuOM^KON5(dU9UjiD3=UihmIk*sU7lXMCSz+*aIC2K%++lqH( zo~|9S#>8i=chk926w4@H=|{6PRr&g?L&oP66$@|=W=AYB=0lA3n?92F@e zW2GCo7rDMyy4yco8uwgTf8RIG>i7}I(R{ddBq;3R?6T3ysAzDq=%&^hZAaB-{ti|d zQ#&J>r0icytBHY+*3`8>)OfSj=;3TIgK~o{kpUXjZ=Ox~)dR$NH-q1->&HGX4om=X z=jjg{66Y*j2+~oZrIxO&R@Pbj$T-Dli>Y}@jK__I|6+Wk4|Qa`SE`xplVv!{yQ?N7 zXj!zHBy^EZ9x8ql!S}u27_F&~XqoqapK?9^~)$MZj$)ZDoV;!r|a^M?C?GQSj6$c7@ zMJc3A10ZS#`FU~5aavwetXGZvFUa_-;_5XEF;#b)SEVM+c~CAVEvXZkV=_)i{hE0` zAEaO6Lp6(ivN<`uMNEdxTX&xUi-{nGg~s~L2VY*vZPRX-^`&g(fG70d$(Y{XVEzl6 z_}VdIDFKL(8-pL$SFQRr-H%@MKtIThwF9`jP6n7H%W=`rl-b{_OUoUX04QXTL<*VSqKybY>T-2shC`a}Sz|jqJRfF6d4@f4|Qjp&skgHf= z;ocm*t};Af&@EXsw!{A$udXH>SmjCqB%s)6p2&x9C7^BGRZZRqXbKurvZ^`tr!#^<`ePo?KoCDBlDDkBmo;%$Nj1Tq8{E zT5IHPi6cdbPQ?uIIBtxfE^u?YI^pZ)ulNEo)g~Q4>35fID&e_ zJR(~L#T!xcSdJd9(sUaBu<1FL={-40G*ih2>Cy+vIX!~?GK7b>j0}$Fnzy$iTxN8* z1M{HjlDcGQ1l#nvpj*9^~zr->{0)k`vSD*|4nd4pgaob5DsuHt{NMk@> z@oN0~$m)i@ZI~n+RNWJ6w~Ku?)l@-F3&E41FJrF8hq@_Hx2i(v9LSai?tm%8Ut$Gx z3fLYUwsn6=7~td_P$&VNQ8rSkV}jtDjUi4SDZ!~{%|4;(DIpR6uOwQHY#dAdh`;WS zaHRxPC5*T*AR`qf_$a<<-;P!!vl};nB@+guXTK3%^LQ8E^oc|kmV}8G=6+?9cT-CE z!?zwxmI*seIvfX$2%*j}IWBmnJasD$6u-Fw&_0&BD%OJe|FCGh!@?D7X>Vg40;O~Q zg4MpgZGwDcBH{kC@zR0{4r(-`g5%n}Uc*0=hMh!^pvbZRXFn}GhDJaenvx-l`*0Z) z0fKJI=|vYIgXr^5k}!OX2@*zXj%!-N?0OH5vp9sL`wIKdbZSui>Qe81EWN-&b<2W|Z9XA`d z9;c20f^TB;9h3S;`fa!*E?NN|?@~KdiMBvJfac^7?H>T7o5KO#90R~44hJ<8c-plg z)QoV6_3VXMArUn>k-wKPfgNRcn3^UxK4Uye= znNcz%1|3BJgAzC1!J} z*W->m04V|fm`3>X!^R_PN_N~hQ3fEm6c`(FNk$5h2p%K}Z|f0Qz5B;xZVP0B|98+=$>frb77WQVjiX9qSoiIbPV8 zS93ea>qeY?34BbCCU$OZE$;rOq=t*LH&CisvpZJ^@CN9U67?{Akk+|?0Pr2e^IE#Q zlM+pUOEbCi)LQ(q3P4X{FInPXtlwjU^S5ww6@&uh}v7eR-x1GNI?5|8Ry-&i+q%Fe6&Gcu}k zJBxi5r2v=0g6Cqx8OE@|F&LoSBLT~Owt$|Yz<5j3+x}o^nr)Z=ojz>{KnR}4DH1qv z95g93PLOXB1de1LrPy~srQ+9zKjI;FaaDkatz)0XX0`TD>or;A$+Dq!3)wXC=%dh&l>Sau`|UL+G4AVE~}v>El(hY8j;fH|#% zXB4lYUi-*vtnlDrrwr>3FfKR1*kS|V=r3m?2(kQIZ!d<|_lKz?>@`Q9DS@*M)g z^aKHaHW8jl0WyozLdcdW;eS{t;SGxsHCRdG*!AJ_2xFSY%l`r-;yM4z{#k0k;XK$O z_!e~NMa*SxCJzwt?ZYjTJp=&$S%4Zn8@m7f=|u!*5kp>&(nef`Vx1KhjBm05KOW?8 z;OW;AfMeDZgRhlwmNFS2uhlWtb0Pq|Q8*0xWR7>7&MUKjbz zwwl4*8o>k$A=_#rxs#ch4o-B?XMq-@q2r&D>aQsx_8#0Z{LZx@xD0Mscbo_KALa4z z|A%A}jw!Yz%i`6Uhh#yT^qJQ(+er7_O^J{jFQG?5hUe@X_n}Nycncx)xk#RE8=vFf z2UqVN`x`*9Epeqpfdn;;z;}-e4<<$91A)#;6mBe~4P*wu(N&?DAJLo`Shxo@{OOdz zweRTYumDS?avI)|mT+KPGXYqbj{+xG8QShuu!H0|3Ok)uTln`8U;vo|#>w6dQ%?hl z_??JkYqotLySy1$1K1643sa@m*`S5xo21arjg58GwWUXYR+oy*UsBI$Dg27aq%?w~ z7JyS;NQI!AE=9o-2`DZToB*7Y2qZaBZ4X`*kO9WAO(W(^je(qF?9PBykMO?_WQHDE z;G?<~)~{~lCQoOC6v+q9ynG%ay}8O> zrMycNq)X3`|L6bQcX=C6VxAhw=F&-r7;5^Rn-SDGxIDc7^Ks9{`t(&hTqTGS-jia8 zzJg!tV)d4Sg;)UM>@Ok}TmUkf-ANS}X&HSdZmT|&r&R%V+X;|oDc|!|K=T#ZlhO@` z?l3vpC3~O>@%ot+VBT>X^?CmeyW9vCx3&KqR2zsH2%t=&2i1qq<>Gi?!>&WnmW74G zfd});f`eU|%ff{L#l0I>?XBgM;F|$?S`%{79a$SuphpfUO5mv+3e7xN zt;RfnyN=`6KZX%mT4)Y!&xib_|7aNiC?11<&Ia!o9gae5e2F4jx|SkbYxce+44p|J zS-nb&!`^>R;OPgY0?MxXh#E&sJu$>go!5Q&3Ks3LCsVH%nMdL znja0lmo5Qy1U2SG)HvKlM~S%E|mfc-Fn^c|G`W;&z$P_9E}2g|%|!@>c=W zv4NOUz;4gheZNjNN11#I4)`a-`m1=#1GCKv$3^ewO3$Zs>Hf?Nc1;BLTOKGKAgB=EesMzhtoU%$WqBG`f0bfVCS$)d=4 zo6LYzNa0eYEB&HKNEZf%`L51~yln2$Ph2gKcjz1^$R#IR&Jf7;7yLObcKxX7bMfwz zTpjXjRb{>;9p*xL3+9qyi>(jS!xhC&TdFg}A8B+sutF$lL-=V!9@B=Hv4(~JnW4U( zjsBK_uZA9LLcjOa-6J1uu8oOV5EVLm9lxNN*iaFevf+}=XF`zub5{~KX{NTzSDSw7 zSX4zzXR2JO`$=MMI+bOQ*Qjs@qQYS*KRFG@1>=fJB>-`7fc6XsdXYdv4O=Q>BL-S+Q_ObCc1vHj@6` zA{Lzw7R;5T)hsr>OlAS^)SNG@g`NP9_kne@|2WrvVD`sd|itifgf{^ex3F!4&dmT%Ak>-0~D+)IwlYpm|y0OIrDfW zAamM*{)SNVU1l4FM`)@nEURvSg-;Z$WSRcG2N!Y|`CqKmlL)W=^gHsyc8X@QBm4RO zoFhWGs9tEad7jz&Or+a$b6~+GHXBfVCOvSF_wc zSmD3{Bl0|=!dZ~o=>)u!b_R%gMN!K`z$>+7^g?9{Q5R_dV-to7Zy#JYogbXLXs6U=l( zFG|n1Eyc)}dduOET3om5jaTm4MupQ*t`3YuB!u!$1Qa~P7*-l8(J6_k z@w+=Dki`0~bWm;{mG$Lz^qugsP0_-- zJwzg0V@HI8{2$lLJj`<(H{uqIZT2PJ={uS+nRX5t?q`T{jk1&lmid*=Rayi{d1s;k zG~jXu;&6ej&NR}a5UYSizuf#+KPqTO&X3oVu*CaV!}OB9J^z*7K_S-dw7G6wY-Pk` zQuJb*Iqhrb4B!+rt=(_-blTw2d;99@?rjwg129>*Z#3fhlZ6l#y3$>+=t=7?SbT=~ zCjbLCKuckqF(UB+)-W?tpOkZsIKY65eY?w3OK!~t!ONR&GCwts07hmIzDb)ebSAd& zsNUdlIQv%2+f;!YmTBO7vBB6qkjzpI2@Lx5z3?@$4cQCzJkHJYesB8{fN`}heY=eX z!Q-2|@T3|NLEYwL!3)IHvFhlV>++|0D`hZ}o~$+$SoF+dyh^M#xrU%NR#3N<$YaE)AnLauhWttHN2>r8 zI$`my!%jotAEN+Rq@rIqX#3m&=*MZSbC=H-#}|Ca%*4V451;IMCVC&tRoabWbb6AL z32!Q8wqkSgUM>2qov!TyfOH)Zu9w|i*_It#=VT<))8mG?-@K?=^CSt?*_CeCSV#(y zJ(MCZ9X&j@;dfk|I~*Kdz|W)FWVB~+6uP;0r}^cY1fx#_dS7CBa1P%@Wu8z-r;QmV zF#0t!N{jGjlex3UMT)?S2mlONk`)~~R{^3<^CEG>+f>T&~0LVTcWcc{P<38+8ONxfI_!^R0itn6L zU~-7!p>RoQ(qZf&88&O5w8X!+clTObYm=1dhIK96GB#ICfvGLe5WU(0J3zxK}Rpw=3tLt$2sE(C<=Sc`K7&diZCAbCm&87(5&{zUL`(H5bW4x{c2 zvq{qMt)rBxbZ?77UbJE@?hYCusqUd%r>8$6_$B~)HjBuq%O9@aNtVM#!x}h!r^RXe V*Ppl8Hi;(v?^jgV{@+>5{|{p6e{lc+ literal 0 HcmV?d00001 diff --git a/src/Cortex.Vectors/Assets/license.md b/src/Cortex.Vectors/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Vectors/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Vectors/BitVector.cs b/src/Cortex.Vectors/BitVector.cs new file mode 100644 index 0000000..69c9c99 --- /dev/null +++ b/src/Cortex.Vectors/BitVector.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + /// + /// Fixed‑length bit‑packed vector that implements for any IEEE‑754 type . + /// Each bit encodes 0 → , 1 → . + /// + public sealed class BitVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly ulong[] _blocks; + + public int Dimension { get; } + public int Count => Dimension; + + #region Construction + public BitVector(int dimension) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + Dimension = dimension; + _blocks = new ulong[(dimension + 63) >> 6]; + } + + /// Create from indices that should be set to 1. + public BitVector(int dimension, IEnumerable oneIndices) : this(dimension) + { + foreach (var idx in oneIndices) SetBit(idx, true); + } + + /// Create from a span of bools. + public BitVector(ReadOnlySpan bits) : this(bits.Length) + { + for (int i = 0; i < bits.Length; i++) if (bits[i]) SetBit(i, true); + } + #endregion + + #region Bit helpers + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int blk, int off) Loc(int idx) => (idx >> 6, idx & 63); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetBit(int idx) + { + if ((uint)idx >= Dimension) throw new IndexOutOfRangeException(); + var (b, o) = Loc(idx); + return (_blocks[b] & (1UL << o)) != 0UL; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBit(int idx, bool value) + { + if ((uint)idx >= Dimension) throw new IndexOutOfRangeException(); + var (b, o) = Loc(idx); + if (value) _blocks[b] |= 1UL << o; else _blocks[b] &= ~(1UL << o); + } + public int PopCount() + { + int c = 0; foreach (var v in _blocks) c += BitOperations.PopCount(v); return c; + } + #endregion + + #region IVector Implementation + public T this[int index] + { + get => GetBit(index) ? T.One : T.Zero; + } + + public T Dot(IVector other) + { + if (other is BitVector bv) + { + ValidateSameDimension(bv); + int count = 0; + for (int i = 0; i < _blocks.Length; i++) count += BitOperations.PopCount(_blocks[i] & bv._blocks[i]); + return T.CreateTruncating(count); + } + else + { + ValidateSameDimension(other); + T sum = T.Zero; + for (int i = 0; i < Dimension; i++) sum += this[i] * other[i]; + return sum; + } + } + + public T Norm() => T.Sqrt(T.CreateTruncating(PopCount())); + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) throw new InvalidOperationException("Cannot normalize zero bit‑vector."); + var inv = T.One / n; + var data = new T[Dimension]; + for (int i = 0; i < Dimension; i++) if (GetBit(i)) data[i] = inv; // zeros already default + return new DenseVector(data); + } + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Dimension; i++) yield return this[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + #region Equality & HashCode + public bool Equals(BitVector? other) + { + if (other is null || other.Dimension != Dimension) return false; + for (int i = 0; i < _blocks.Length; i++) if (_blocks[i] != other._blocks[i]) return false; + return true; + } + public override bool Equals(object? obj) => obj is BitVector bv && Equals(bv); + public override int GetHashCode() => HashCode.Combine(Dimension, _blocks.Length > 0 ? _blocks[0] : 0UL); + #endregion + + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != Dimension) throw new ArgumentException("Vector dimensions must match.", nameof(other)); + } + } +} diff --git a/src/Cortex.Vectors/Cortex.Vectors.csproj b/src/Cortex.Vectors/Cortex.Vectors.csproj new file mode 100644 index 0000000..8e1bda8 --- /dev/null +++ b/src/Cortex.Vectors/Cortex.Vectors.csproj @@ -0,0 +1,59 @@ + + + + net9.0;net8.0 + + 2.0.0 + 2.0.0 + Buildersoft Cortex Framework + Buildersoft + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + Cortex Data Framework is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. + + + https://github.com/buildersoftio/cortex + cortex;machine‑learning;vector;ai;streaming + + 2.0.0 + license.md + andyX.png + Cortex.Vectors + True + True + True + git + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + README.md + + + + + + + + + + True + \ + Always + + + + + True + + + + True + + + + + + + + + diff --git a/src/Cortex.Vectors/DenseVector.cs b/src/Cortex.Vectors/DenseVector.cs new file mode 100644 index 0000000..15c71e8 --- /dev/null +++ b/src/Cortex.Vectors/DenseVector.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + /// + /// Contiguous dense representation of a mathematical vector. + /// Suitable for small‑to‑medium dimensions (< 10⁶) that are mostly non‑zero. + /// + /// Floating‑point element type. + public sealed class DenseVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly T[] _data; + + #region Construction + + public DenseVector(ReadOnlySpan span) + { + _data = span.ToArray(); + } + + public DenseVector(params T[] values) + { + _data = values.Length == 0 + ? throw new ArgumentException("Vector must have at least one component.", nameof(values)) + : (T[])values.Clone(); + } + + /// Creates a zero‑filled vector of given dimension. + public static DenseVector Zeros(int dimension) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + return new DenseVector(new T[dimension]); + } + + /// Creates a vector where every component equals . + public static DenseVector Filled(int dimension, T value) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + var data = new T[dimension]; + Array.Fill(data, value); + return new DenseVector(data); + } + + #endregion + + #region IVector & IReadOnlyList Implementation + + public int Dimension => _data.Length; + + public int Count => _data.Length; // IReadOnlyCollection implementation + + public T this[int index] => _data[index]; + + public T Dot(IVector other) + { + ValidateSameDimension(other); + T sum = T.Zero; + for (int i = 0; i < _data.Length; i++) + sum += _data[i] * other[i]; + return sum; + } + + public T Norm() + { + T sumSq = T.Zero; + foreach (var v in _data) + sumSq += v * v; + return T.Sqrt(sumSq); + } + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) + throw new InvalidOperationException("Cannot normalize the zero vector."); + var scaled = new T[_data.Length]; + for (int i = 0; i < _data.Length; i++) + scaled[i] = _data[i] / n; + return new DenseVector(scaled); + } + + #endregion + + #region Arithmetic Operators + + public static DenseVector operator +(DenseVector left, DenseVector right) + { + left.ValidateSameDimension(right); + var result = new T[left.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = left._data[i] + right._data[i]; + return new DenseVector(result); + } + + public static DenseVector operator -(DenseVector left, DenseVector right) + { + left.ValidateSameDimension(right); + var result = new T[left.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = left._data[i] - right._data[i]; + return new DenseVector(result); + } + + public static DenseVector operator *(DenseVector vector, T scalar) + { + var result = new T[vector.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = vector._data[i] * scalar; + return new DenseVector(result); + } + + public static DenseVector operator *(T scalar, DenseVector vector) => vector * scalar; + + #endregion + + #region Equality & Hash + + public bool Equals(DenseVector? other) + { + if (other is null || other.Dimension != Dimension) return false; + for (int i = 0; i < Dimension; i++) + if (_data[i] != other._data[i]) return false; + return true; + } + + public override bool Equals(object? obj) => obj is DenseVector v && Equals(v); + + public override int GetHashCode() => HashCode.Combine(Dimension, _data[0], _data[^1]); + + #endregion + + #region IEnumerable Implementation + + public IEnumerator GetEnumerator() => ((IEnumerable)_data).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _data.GetEnumerator(); + + #endregion + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != Dimension) + throw new ArgumentException($"Vector dimensions must match (this: {Dimension}, other: {other.Dimension}).", nameof(other)); + } + } + +} diff --git a/src/Cortex.Vectors/README.md b/src/Cortex.Vectors/README.md new file mode 100644 index 0000000..773ac55 --- /dev/null +++ b/src/Cortex.Vectors/README.md @@ -0,0 +1,123 @@ +# Cortex.Vectors 🧠 + +**Cortex.Vectors** is a High‑performance vector types—Dense, Sparse, and Bit—for AI & for .NET. + + +Built as part of the [Cortex Data Framework](https://github.com/buildersoftio/cortex), this library offers High‑performance vector types—Dense, Sparse, and Bit—for AI for: + + +- ✨ Generic‑math powered (IFloatingPointIeee754): works with float, double, decimal, … +- 🟢 DenseVector – contiguous storage, SIMD‑friendly operations +- 🔵 SparseVector – dictionary‑backed, memory‑efficient for huge, mostly‑zero spaces +- 🟡 BitVector – bit‑packed booleans with popcount & logical ops +- ⚙️ Core ops out‑of‑the‑box: dot product, L2 norm, cosine similarity, scaling, +/‑ + +--- + +[![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Vectors?label=Cortex.Vectors)](https://www.nuget.org/packages/Cortex.Vectors) +[![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +## 🚀 Getting Started + +### Install via NuGet + +```bash +dotnet add package Cortex.Vectors +``` + +## DenseVector +```csharp +using Cortex.Vectors; + +// (1, 2, 3) +var a = new DenseVector(1f, 2f, 3f); + +// (0.5, 0.5, 0.5) +var b = DenseVector.Filled(3, 0.5f); + +float dot = a.Dot(b); // = 3.0 +var normA = a.Norm(); // ≈ 3.7417 +var unitA = a.Normalize(); // unit length +float cosine = a.CosineSimilarity(b); +``` + +## SparseVector +```csharp +using Cortex.Vectors; +using System.Collections.Generic; + +// 1‑million‑dimensional vector with two non‑zeros +var sv = new SparseVector( + dimension: 1_000_000, + nonZero: new[] + { + new KeyValuePair(42, 1.0), + new KeyValuePair(123456, 2.5) + }); + +double l2 = sv.Norm(); // √(1² + 2.5²) +var unit = sv.Normalize(); +``` + +## BitVector + +```csharp +using Cortex.Vectors; + +// length 128, bits 0, 3, and 5 set to 1 +var bv = new BitVector(128, new[] { 0, 3, 5 }); + +int ones = bv.PopCount(); // 3 +float selfDot = bv.Dot(bv); // 3.0 (generic type ⇒ float) +var l2 = bv.Norm(); // √3 +``` + +## 💬 Contributing +We welcome contributions from the community! Whether it's reporting bugs, suggesting features, or submitting pull requests, your involvement helps improve Cortex for everyone. + +### 💬 How to Contribute +1. **Fork the Repository** +2. **Create a Feature Branch** +```bash +git checkout -b feature/YourFeature +``` +3. **Commit Your Changes** +```bash +git commit -m "Add your feature" +``` +4. **Push to Your Fork** +```bash +git push origin feature/YourFeature +``` +5. **Open a Pull Request** + +Describe your changes and submit the pull request for review. + +## 📄 License +This project is licensed under the MIT License. + +## 📚 Sponsorship +Cortex is an open-source project maintained by BuilderSoft. Your support helps us continue developing and improving Cortex. Consider sponsoring us to contribute to the future of resilient streaming platforms. + +### How to Sponsor +* **Financial Contributions**: Support us through [GitHub Sponsors](https://github.com/sponsors/buildersoftio) or other preferred platforms. +* **Corporate Sponsorship**: If your organization is interested in sponsoring Cortex, please contact us directly. + +Contact Us: cortex@buildersoft.io + + +## Contact +We'd love to hear from you! Whether you have questions, feedback, or need support, feel free to reach out. + +- Email: cortex@buildersoft.io +- Website: https://buildersoft.io +- GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. + +Built with ❤️ by the Buildersoft team. diff --git a/src/Cortex.Vectors/SparseVector.cs b/src/Cortex.Vectors/SparseVector.cs new file mode 100644 index 0000000..bea7c51 --- /dev/null +++ b/src/Cortex.Vectors/SparseVector.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + public sealed class SparseVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly int _dimension; + private readonly Dictionary _values; + + #region Construction + public SparseVector(int dimension) : this(dimension, Enumerable.Empty>()) { } + + public SparseVector(int dimension, IEnumerable> nonZero) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + _dimension = dimension; + _values = new Dictionary(); + foreach (var (i, val) in nonZero) + { + if (i < 0 || i >= dimension) throw new ArgumentOutOfRangeException(nameof(nonZero), "Index out of range."); + if (val != T.Zero) _values[i] = val; + } + } + + /// Creates a sparse vector where the provided indices hold the same . + public static SparseVector FromIndices(int dimension, IEnumerable indices, T value) + => new SparseVector(dimension, indices.Select(i => new KeyValuePair(i, value))); + #endregion + + #region IReadOnlyList Implementation + public int Dimension => _dimension; + public int Count => _dimension; // total logical length + public int NonZeroCount => _values.Count; + + public T this[int index] + { + get + { + if ((uint)index >= _dimension) throw new IndexOutOfRangeException(); + return _values.TryGetValue(index, out var v) ? v : T.Zero; + } + } + #endregion + + #region Core Vector Operations + public T Dot(IVector other) + { + ValidateSameDimension(other); + T sum = T.Zero; + foreach (var (i, v) in _values) sum += v * other[i]; + return sum; + } + + public T Norm() + { + T sumSq = T.Zero; + foreach (var v in _values.Values) sumSq += v * v; + return T.Sqrt(sumSq); + } + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) throw new InvalidOperationException("Cannot normalize zero vector."); + var scaled = _values.Select(kv => new KeyValuePair(kv.Key, kv.Value / n)); + return new SparseVector(_dimension, scaled); + } + #endregion + + #region Operators + public static SparseVector operator +(SparseVector a, SparseVector b) + { + a.ValidateSameDimension(b); + var result = new Dictionary(a._values); + foreach (var (i, v) in b._values) + { + if (result.TryGetValue(i, out var existing)) + { + var sum = existing + v; + if (sum == T.Zero) result.Remove(i); else result[i] = sum; + } + else result[i] = v; + } + return new SparseVector(a._dimension, result); + } + + public static SparseVector operator -(SparseVector a, SparseVector b) + { + a.ValidateSameDimension(b); + var result = new Dictionary(a._values); + foreach (var (i, v) in b._values) + { + if (result.TryGetValue(i, out var existing)) + { + var diff = existing - v; + if (diff == T.Zero) result.Remove(i); else result[i] = diff; + } + else if (v != T.Zero) result[i] = -v; + } + return new SparseVector(a._dimension, result); + } + + public static SparseVector operator *(SparseVector vector, T scalar) + { + if (scalar == T.Zero) return new SparseVector(vector._dimension); + var result = vector._values.ToDictionary(k => k.Key, k => k.Value * scalar); + return new SparseVector(vector._dimension, result); + } + + public static SparseVector operator *(T scalar, SparseVector vector) => vector * scalar; + #endregion + + #region Equality & Hashing + public bool Equals(SparseVector? other) + { + if (other is null || other._dimension != _dimension || other._values.Count != _values.Count) return false; + foreach (var kv in _values) + if (!other._values.TryGetValue(kv.Key, out var v) || v != kv.Value) return false; + return true; + } + + public override bool Equals(object? obj) => obj is SparseVector sv && Equals(sv); + public override int GetHashCode() => HashCode.Combine(_dimension, _values.Count); + #endregion + + #region Enumeration + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _dimension; i++) yield return this[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != _dimension) throw new ArgumentException("Vector dimensions must match.", nameof(other)); + } + } +} diff --git a/src/Cortex.Vectors/VectorExtensions.cs b/src/Cortex.Vectors/VectorExtensions.cs new file mode 100644 index 0000000..6ee463e --- /dev/null +++ b/src/Cortex.Vectors/VectorExtensions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Cortex.Vectors +{ + public static class VectorExtensions + { + public static T CosineSimilarity(this IVector a, IVector b) where T : IFloatingPointIeee754 + { + var denom = a.Norm() * b.Norm(); + return denom == T.Zero ? T.Zero : a.Dot(b) / denom; + } + + public static SparseVector ToSparse(this DenseVector v) where T : IFloatingPointIeee754 + => new SparseVector(v.Dimension, + Enumerable.Range(0, v.Dimension) + .Where(i => v[i] != T.Zero) + .Select(i => new KeyValuePair(i, v[i]))); + } +}