From bd10ce553a85bc0d2c2634274e90c32236f93c8a Mon Sep 17 00:00:00 2001 From: Yury Kurlykov Date: Thu, 18 Jun 2020 13:19:27 +1000 Subject: [PATCH] Add 12th lab --- CMakeLists.txt | 3 +- README.md | 2 + lab12/.execme | 10 + lab12/CMakeLists.txt | 36 ++++ lab12/README.md | 53 +++++ lab12/cache.c | 72 +++++++ lab12/g1.png | Bin 0 -> 44223 bytes lab12/graph_data.py | 20 ++ lab12/thinkplot.py | 504 +++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 699 insertions(+), 1 deletion(-) create mode 100755 lab12/.execme create mode 100644 lab12/CMakeLists.txt create mode 100644 lab12/README.md create mode 100644 lab12/cache.c create mode 100644 lab12/g1.png create mode 100644 lab12/graph_data.py create mode 100644 lab12/thinkplot.py diff --git a/CMakeLists.txt b/CMakeLists.txt index e8f7be2..39330da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,4 +21,5 @@ define_lab(lab6) define_lab(lab7) define_lab(lab8) define_lab(lab9) -define_lab(lab10) \ No newline at end of file +define_lab(lab10) +define_lab(lab12) \ No newline at end of file diff --git a/README.md b/README.md index 9a22440..70a150e 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ - [Лабораторная работа 5](lab5/README.md) - [Лабораторная работа 6](lab6/README.md) - [Лабораторная работа 7](lab7/README.md) +- [Лабораторная работа 8](lab8/README.md) - [Лабораторная работа 9](lab9/README.md) - [Лабораторная работа 10](lab10/README.md) - [Лабораторная работа 11](lab11/README.md) +- [Лабораторная работа 12](lab12/README.md) ## Запуск diff --git a/lab12/.execme b/lab12/.execme new file mode 100755 index 0000000..dad8c12 --- /dev/null +++ b/lab12/.execme @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail +IFS=$'\n\t' + +pushd "$1" > /dev/null + +./lab12_cache.c_run > ./data && lscpu && cat /proc/cpuinfo && python2 ./graph_data.py + +popd > /dev/null \ No newline at end of file diff --git a/lab12/CMakeLists.txt b/lab12/CMakeLists.txt new file mode 100644 index 0000000..fd72e9a --- /dev/null +++ b/lab12/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) +set(CMAKE_C_STANDARD 11) + +# Lab name +set(LAB_NAME "lab12") + +# Lab tasks +list(APPEND SOURCE_FILES + cache.c + ) +list(APPEND NON_COMPILABLE_SRC + .execme + graph_data.py + thinkplot.py + ) + +### Here goes the template + +project("${LAB_NAME}" C) + +add_custom_target("${LAB_NAME}") + +foreach (file IN LISTS SOURCE_FILES) + add_executable("${LAB_NAME}_${file}_run" "${file}") + add_dependencies("${LAB_NAME}" "${LAB_NAME}_${file}_run") +endforeach () + +foreach (file IN LISTS NON_COMPILABLE_SRC) + add_custom_command( + TARGET "${LAB_NAME}" POST_BUILD + DEPENDS "${file}" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/${file}" + "${CMAKE_CURRENT_BINARY_DIR}/${file}" + ) +endforeach () \ No newline at end of file diff --git a/lab12/README.md b/lab12/README.md new file mode 100644 index 0000000..44c278f --- /dev/null +++ b/lab12/README.md @@ -0,0 +1,53 @@ +# Лабораторная работа №12 + +> Проанализируйте cache.c и с ее использованием исследуйте параметры кэша на вашем компьютере. Для этого +> 1. постройте графики времени доступа как функции длины массива, шага выборки и размера буфера. +> 2. на их основе сформулируйте обоснованные гипотезы о размере кэша, размере блока, наличию кэша более высокого уровня. +> 3. сравните свои оценки с реальными значениями, полученными через вызов системных функций или из технического описания вашего компьютера. + +График: + +![](g1.png) + +Как видно из графика, стремительный рост access time происходит на 2^22 B, что примерно равно 4 мегабайтам. +Из этого можно предположить, что размер кэша -- 4Мб. На размере блока выше 64 байт происходит увеличение access time, +что может быть связано с тем, что физический размер блока -- 64 байта. Также наблюдаются ускорения при размере 2^20 и +2^21, что может говорить о существовании некоторых кэшей размером в 1 и 2 Мб. + +Вывод `cat /proc/cpuinfo`: +```text +... +cache size : 3072 KB +bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds +bogomips : 3792.26 +clflush size : 64 +cache_alignment : 64 +address sizes : 39 bits physical, 48 bits virtual +... +``` + +Вывод `lscpu`: +```text +... +L1d cache: 64 KiB +L1i cache: 64 KiB +L2 cache: 512 KiB +L3 cache: 3 MiB +Vulnerability Itlb multihit: KVM: Mitigation: Split huge pages +Vulnerability L1tf: Mitigation; PTE Inversion; VMX conditional cache flushes, SMT vulnerable +Vulnerability Mds: Mitigation; Clear CPU buffers; SMT vulnerable +Vulnerability Meltdown: Mitigation; PTI +Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp +Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization +Vulnerability Spectre v2: Mitigation; Full generic retpoline, IBPB conditional, IBRS_FW, STIBP conditional, RSB filling +Vulnerability Srbds: Mitigation; Microcode +Vulnerability Tsx async abort: Not affected +... +``` + +Исходя из этих данных, можно предположить, что в связи с патчами для устранения уязвимостей процессора +(Spectre, Meltdown, L1TF и прочие) график может не вполне корректно отражать реальное положение дел. + +Но выводы оказались достаточно приближены к действительности: мы видим два L1-кэша размера 64 Кб (не видно +на графике, т.к. 2^16 Б меньше левой границы графика), L2-кэш размера 512 Кб (2^19 Б) и L3-кэш размера 3 Мб +(~2^(21.6) Б). diff --git a/lab12/cache.c b/lab12/cache.c new file mode 100644 index 0000000..fc630ed --- /dev/null +++ b/lab12/cache.c @@ -0,0 +1,72 @@ +/****************************************************************** + * CACHE project * + * * + * Using this program, on as many different kinds of computers as * + * possible, investigate these cache parameters: * + * -- total cache size * + * -- cache width * + * -- cache replacement policy * + ******************************************************************/ + +/* I got this program from Brian Harvey, who teaches CS61C at + Berkeley. He didn't put a copyright on it, but he should + at least get credit for it. Thanks, Brian! */ + +#include +#include +#include +#include +#include + +#define CACHE_MIN (32*1024) /* smallest cache */ +#define CACHE_MAX (32*1024*1024) /* largest cache */ +#define SAMPLE 10 /* to get a larger time sample */ + +int x[CACHE_MAX]; /* array going to stride through */ +long clk_tck; + +double get_seconds() { /* routine to read time */ + struct tms rusage; + times(&rusage); /* UNIX utility: time in clock ticks */ + return (double) (rusage.tms_utime)/clk_tck; +} + +int main() { + int register i, index, stride, limit, temp; + int steps, tsteps, csize; + double sec0, sec; /* timing variables */ + + clk_tck = sysconf(_SC_CLK_TCK); + + for (csize=CACHE_MIN; csize <= CACHE_MAX; csize=csize*2) + for (stride=1; stride <= csize/2; stride=stride*2) { + sec = 0; /* initialize timer */ + limit = csize-stride+1; /* cache size this loop */ + + steps = 0; + do { /* repeat until collect 1 second */ + sec0 = get_seconds(); /* start timer */ + for (i=SAMPLE*stride;i!=0;i=i-1) /* larger sample */ + for (index=0; index < limit; index=index+stride) + x[index] = x[index] + 1; /* cache access */ + steps = steps + 1; /* count while loop iterations */ + sec = sec + (get_seconds() - sec0);/* end timer */ + } while (sec < 1.0); /* until collect 1 second */ + + /* Repeat empty loop to loop subtract overhead */ + tsteps = 0; /* used to match no. while iterations */ + do { /* repeat until same no. iterations as above */ + sec0 = get_seconds(); /* start timer */ + for (i=SAMPLE*stride;i!=0;i=i-1) /* larger sample */ + for (index=0; index < limit; index=index+stride) + temp = temp + index; /* dummy code */ + tsteps = tsteps + 1; /* count while iterations */ + sec = sec - (get_seconds() - sec0);/* - overhead */ + } while (tstepsUZSC)=YZd3SogtC z#B*Y}!MA%(Qu3Nu;L8*1Edu=ef&B|zCp0tyW7O|Exe_^+;DOdXvp?QH3IIC(hv+375to$ZC6KK-8yIPDzGpT6j4&O}3_Lz9O| zYPzLtO?$YJ9M?(go%U>r{hg*yw&tYw!$N%gnEL`!Hix00o?elYBc>%Zq+ylyQZWL4 zq2V!{alF)cq?sNBNDxJ zaV#oIaFIHlNKm=g*K>Mtj*Ubj>7d|?2Tgq!9fA6C=T0GlKp>IET>tO;|67Ou3wDrj z#7P(JvNi69N#lm*ak3?8V)8`5ZYC6-uf^cC8}@7lAxG;)a&yse6QXZhZ8|=tMuLHd zhi5OGN_g)vJc}3t8NBx$$%^iW*45QT7hG$O^;TC9@4kNmuQ~o`fv$gkalQM%sm)9+ zg8DyTd0y&GsrNlCrV{Yu$W@MY9V3_P~WtByzY zxvfSEjJ+5s!=RL5Cw2Ju(9zK1yW8v)U@%xqbDl4gy<>~~PQ#6Nf)Znaex>br+TS0> zcc*M!X9>iIn81@M#R7&GPqzO02hw^!R2AKCMbvv#FNTZE9iIR5{xdR?RiXf{$_NUZ zKdO2*<93&WgCn<~;M4E7AAi4v@M2Nj ziJpPWU(=)#)9$$W39I6^5{6DKQ+RI!vn9blai8Rj9$ep*T0^);pD&r8~6$ABR$cBgkH0{ z?#Ez$xO1QCaW_&;HtYqRi{5&!d8ItDSc#}v-S@XO_W)2Jf}oD@afLG$+v0)3e{HUOfo|8%QnUG8ZXFBgseknOjfAs zF-A+C$`mK2k^73ruQ4vFcq4Zf@yk8w-f^()5xjkZcPpn|9{IC?(PrU-OJV9%gKy}? zgi=*f-jC`gm87Il0kgQX2p&~Oq=4HyulH%0t|x<3Yw&)^k}tfhiS_T}2WGZ#<-4~5 zM7`_$jZH2A&Bp>pG&Eyp0=Ed}7e5qSbYw)>qpsZCwm)zLg);vQ_Br5VEpE8}HN!v% ziM;b$0Ua;BWi7ZI|J+C>7NX+JgjdAaG)1h*Aj00UTXynE=Ml_r_&4MKUaO!TH}Y@{ z0=lBscF&?ulyXpr9P>P_eGj{yajf+Z1ilV~{%opth{YnG-vi&<4Q_|2oAP~rH$Y>f zZ*uj41&=2jzyr2P+em&8a<9T{i)jdZv*9ja8-p9 zYsmVQkCY@Su;McseXQf4cEEslnl`vCJ6?XKt_Yo-oRRj}a`9+?ttC z@b~B^SgjXh&}K(ZJ>O$Di|yoD)`W)!c%Nk7c z-;?x}DLtm*U9}2lL4F+HugR}7=64ruWNha$e6CIkTcU$H+Bb5`fBE`6^S6hv;i9)4S?c*o$^+*OV`n!sm4M z&_2p*_1{IWJ;8RfA^p`m%@<^r0OO@%j5jlY!|DI%sJ@PsMzAvNRJAX4BFM~i89b2P z+4K)pf)EG`H6${Fcl2ZX0E8JSI#&fR&4jKxk1MNCBOfKvVR7MTCCIS7cCiXi)kz=~ zrZH;q3MqS|;M`dKP=)tD-W{dD%uR(uBSkk8dE}N}h}(AZd$EG$`Zvzc6tnG;na{fF z)z1oKUlXzaBS|ua)#1=BMw?Q&PQNDy$nwMuC5&_8YdO2zcgd+LrXu#e2 zI(h#*-Lj0X4+09*IboBpg=4^zY(p?*P8^lG65CxgD4Iob_`YB4Hqa&u*fdA0iU;Zb zG&2S$SatnxbKa$<#CBS^uKNizgM{+k#h|AZw*oc%%RN~v1%m6|jK=^VaM-kqT7ya1 z(9krpm746l4bG zWuOFTO8JtRr(QXnNN$Ughvaz;-GZ1=M`##9kNbNeQnM)lN_8c1d z7!sWG+RVFThQSo$Kc5GnK7ar1uARUbG+`~V;X=!bn$_tr(yC(3;BRXODih&TvA-BR z!f7{g`&mskGCeW#!$GD!xtCu@%lnr~tSP~N9`w!pV$dIp(NjL$@nW##r6%dk#=Ex3 zLOF=W{hjZoM6eiP12uSTLx&{@M=;pi*Qn9{$}rxtKuVg7ryaS;gg*)i&YvsQ;Wf~8 zAv)&tZ4X2RsV=r+n656xJ;_F#r?p&jVmFto5)LD9?WS#)CrnJ3hbz4;gPC&gbK?*e zZ?KqtsZU-#*zm`UzCSFbPmx{SXUs*-pbP&|SxD1A<{876FhS4D$&wnpyv+Mhi5KCj zVyW}7iEgDQ(VyRHbZ#TRn8yBcZ@xvR%mPyXZ=`r3Py&QxPIB?b3) z=ECP}%UmKP?fNLk%S0nbr59!i_EMH>Ue2lt-_LFJ+wS~HrU|je+uBKYTv}HFCPU2? zK79vuc~Vx#hnL6A4S$8zNl5+d4aO)_TzFv)ThYHrWtv`HACP;V*37n*ns)Eac?lgG?m zHp_E`MB$I9KLyxuui3%iba)K-a^5ayfQb3l)dVo{f$8ZmVBG|#Q%(e^eAN1HDTekd zFa8Rf$pBTKf5R-d8=wv+ZTdvOF`tRW81=OE4w^hy`hdU#8hw`CxbTF!u4 zt)M+xG?VIR9xD`Tq6R0ucEi+bmV?NbPFp^hCupx*wDV@uRVo69zE|z}J_Yy!kdkFc{ z03`k;rcWtwzt_D<{1iw8qEkl`E~8z5!4WCky43k?_vZ@%H=9_HcF4I$$bARhY~_Q4UnM$OMIhy~TSRSt4kvYP*BhiPl2)P+jnQMktgXPV;z-#{z=jC< z`S6V`_%?8??|X(&lgDeOw>A|Tc09U;FLgCd48uXQGFbTIBHUW5v2MoYMyq1q?k!_x zh3Aa+<3z-$;dh) zPNzH4mcjQH)N2Pd{eQjI9$I8@VdCV>ld2#2I^V?{dpqO+Np(|aSn>d6p4@_F`kxq_ z;zA-7Cvk8iGcq_rIJu6ON<)HmQ8WT#derlR9T?o3o~~1#E-)%7{Z!z>#ZpCb|%u5 z|0Xu!Tk)cWMcT6HFCG}PkeTcK{BC@_#}Y1lS}o+NGgosxqEA=I;f<`^?Mkpq4Ul!B ze^FcTO4pR4D>$pVEXJb-RALK z?;bFOgDFmAhDm=KLU-FHwNDs-#ybMcJ-x~kY-war=cE{Wk&L>T^NuN0CN~uuBIs9^l4#Y_rGeM+NRmmq8WyS?Hgui-EabHE?dMP8*?*&z_8?C{<}d`;A{1Ljn;bOUk?q15}tfI&1WFvl}vzg(>NLy-8Ac zK$;?J&Yp5~hU*FuUoQLlv4aU)I>ik9;!M>ZP$sWSK9; z-;KBl7ujh%SngKW)eU-1=Zh+V0NWv4VLOct29u>24<*5#U(ZPknBCn?o>0@&yeGeq zNeNXbeqKx5@r!^i9pBS=i%Up7lGp!GL8E|;tO?>Hf`2}VX*l=im5d4v8L4VOce4bJ zFn_>s^wIrce@ucmlNC05Zlaboam-jPMxoy;SrhNSTch=U|ElYHcSoK{aB(BfCb&x$Z3T1y6s|dRfiEYUR#Kn&( zvAG2KvTvZqUrNG!)uYfttkCMPbVfDQM7eUh%A@aro;U1|o-bzgFZSieTe#r#e>0q4 zu0?AbBl-LOw3ld4>(8LK>E@uPEm#+wS~rLG4SHxsQ!3ImZ`i>1AZ48^XmRm(3`Az> z9`Y`=;MVSQ=}w=nFdX;g1ongt_D0QkOqmL*`!6Y@3$SyfrG)Xxz2@1md3=vxudZ)i5jx3as=*wLB^r*RdfIa|jycN} zTr`(Je2K}IHeJ+~QnxRw!3n(F7Np$zSiman-40dAbtHS8)a}m`7~&T*1oB(JPb$LU zU^_sGjuAE0+YMl%$AJ>&mns<`twjowCj0SOzoZ?N*4UWR^TV7QtqD-M`UT_9Tp zy`hxnwReFPmb$Jpr`CF{MLx%Dmaib#R{i9AK=ll4dqPw)0nUYHvEs%(aXykQ z^|E++2fd3*Md|$qwPki++t#Kv`t;&nplt5)7-55pJYu6u;x{1<^Z@Z*;o%|iI^KczHopDATZ_Rs6KZZlA&sOMU?zSJN2D_Cq#w z+RKA(W>(hbCV$Mxv^0siy5whe?SS0)Tw2OQ&B~CmPdDiv&)Okv&J2-ehDcm5eoyOW z@)l2#J^O8cp_cUGq00M=T-J|4>B+P?8sYMkqdPRGvpy26tgL$=X8*D)KqB8YUTkb` zIvn;sGua$11U#~gf)ztXka6csmeBAQa|+rzMN+uT*>_)xP%D2kJ2X6|g_a3?=kH*t zlr;fM@__N})W1HtU!!}C80JluH2;blL!Q+RVeUKcJ(Nqi4bG)VnPdGT?b z-2cC`;T)$7Tm0j;ZkVJ-@(Iw1g2deSR4GF`ALLqMrRFqmOS3!zwbYkzDxr_c$pXfp z@;IA0tq(*I>0rG6ZEu=@pWZo|GUQKG(y6pf?HeJA96UPWER@v${&;;Ks^7LhleLbg z=OCkUpmVjf_t$Ba7fMj)|MHiv3^5fW;0jqz{#e>V@lCmSn%Z4Nm6{S&(D6V zdZVKyh`4f!|66-|HxyLd)GqE~L87G?_$*1$5AO;G)g#yMbSPY23-Z%q%QTp%lDF z%!vqRB87WuV|BFLrgH*GTED}CQ+NUY`@Jr_NatQLKb6X_*T3lz_6h8r6i{b9nIMf= zgfo};5M=$`Ozns9+7G-tRg3tSYjP4&z`p=jh5$sN?CubvlP+MxQ13N|WL_rglR8C{ z;LrwY5#NLin=w~Obqup*(iZk$+*H_oz$vvjwvlSj1+x`BJh>3+k74!zb}}8YQ)P!D zz`0n=g{R`0L;qwTSu38=Q2dy^1n9R)V*f}pWB-;)ljR`Xd=4W0+xM?87{uJ;>t_cM zPyFzEb^J#BRO7X{6NtiY*?qn@(#M>~6Kr$kSq-l1zc+r>?5S*W_a?ykjMu-fbA=F_ zuxVykH=?SfCq%I=f)iIyVNpk%{It7AcVf`)#K$Oqp3Jx@(tz=K zqPR;wcJS!;i8x#=ck2`{LfCk&wR{L=;KW1!)dHx?h?KuJQWpaJ9FHCjBh2FCJ@As~Yv0;QG5u zr4vr&Z1J@geGWN(6EgMYp@U)Ld35FshY(a8x<5i$^oUT2ce;iz@wsSl1JmC-8&Rcs zor|a|04Ih(LQ<3X~4-Xb;dnbPNWPHk69yX2@R1CAom}@Xg1w=rusQs}Oqb|en zk6k{{bf)T#f2!)<18TBZZKJ_Zv4hShRdXM4?2p#eLC-a#un-TEfTndZLL&U{PxHHI zJ2kuFmkZ<#ji5pPDJm+e>h{nfn0}PW=~G z%F8a9|0xx4LZJ^&(?w>oDA-D4 zrHPI&cHd|_ap*y(lS+b9L{nbn?(*G*DiHJuWyJ({ES`ahV*zgfKg zf@8PURM#Ki1qL6MV?p-&r{oQEBMZrz)Xbm69c>ZNkv%K$EzP-NE=_#9nLYk*uwqxX z;~kR0d!RqhGWyC)Ndh7_HkQp>+IlcEoUNzcQF8*dp z`&04`xM#%&CA$>u z@PAjZ;}Ub*qXPzdKcXX3hz9#J8d^r~mG=u08Rp=qQelLJvnHIHYwQnHFVc=+CX~!7|7(uEA42pv9Z-aoO%kf zZBMZU6A!C2;?JbU~4?&yxkxLDA1E9p!3=P9b*|gSBmo#dSm(U(PYDOObQ2|D>@P-z4n|VPqw&9N-?XK$DoybkYJ$K^=~Ywkv(9 zIA}Tz-eTY{A*kmbA0NBzw~;$+jdKImSBRN|WBa1SYK#-@xv}x%3hTcgfv&*7@i&9u z&E;W<)2hnlx~fkzDE^NwvmmBoKukb2IKvN8<|WS0gTfls%ky|RVCe* zfti4Q54ch|BMiYdQJv~O#gE6ZfP%NT@S5ib3sI~je;rY{y^cakX2svg(9u2aUY+pY zVFnsTvYTp}q{RYMQmzdcYUa!Fb3i&KYxNjrNakp%B? zgIU3!o#vcc12mbKFjRtFswNeb$^WvL!YK*oi-{xNLC}%lnpI0+s&Y~ZRWwGta?*YI zKmje-cZut6CTN#bVWf$grY>scDT9;~P)WdXjE2g8L}W#0w=@Ch*S#HQH|SXd<}Hc4 zGKBOz z4{HUVsHU~wz}|x18w{pP&nPzl8R^?+|3TKm`j=O$rKa$`AlZRsd+>I3)epg+!`O(< z+ZI{r3#Tfw#vtnHdsurDY`iX4E!Qqvn*%B=_Akr~j093~z9F#CKvD;~e~-BAW<&v0 z0N*aqE5}BqMz!Yq3vGn=Bo7zcAK6ja7XcXw`cL!zfL=5IvCX!Lax32BZn`dah|s4Z zoqV$%6SmK~WoAqNs0Rrvp1mV~9z@v~(Ytyi*L|9Z55ehP^Hj8Th?R>2V!51j4fFBDU_V7Zm z?%AW7$xWEFFc}P2zsCxdEB0Y4<@5<^7O63jR=bw@9fPmq%?di>{AJoSc~YM_E}}RYyaL4rhDD zN~yx6!5MUbOfr|aporr%#6~;owe$7|t6@D^fd?8f-D}1E%=)=V;nziZ@@0)CY|0nq zBALe4R-zdc#B_6|^21-O`g)-3P)eUO8#cVjXAS4dwczRF6*i9m|EqdFbAGsb7Y(0U z_#PTF$M$)IJ?Kn&?Xs}4N&zekrx8oa4h~`_+|7&kL_@yxjGjKcS?0PWI{#Kw>sHfC@5Ymcxu^p5p9AwGY9@ zc}aL*OwOXCv6JCkU!OLZtPf?O)Cha1$_seN>i(+4jjq$zoUiWKz%^0a=N^bHlx9CE z^ee3W{)&oA2@kquAhDBLCRlj2e*1|ks_PZ&PfmZHwwb_C6Zrk98<>!;(yk^tH0hz+ zYD~a)@#yo0IlIr_YR>ewIHRVZnvM zU1CH5;{c_2HbpmoO{7(y;w1mmOMVwUf z<|3xdu$XhLMWk?o%bHYKrwc>|K84%I!9cv>$S`Et>usAaq$b_%-kH_e(u6RXGj* ziOh*&Q+PE-!X}I2`7wgI<}EyJ7$qG5I?=|jVjctI<8`^A z-5{l{O-{yXfIu-1pzFd?T~99$_;P7QMI5xod3*bkMmiE1Nh>QhDtt_(?1Fz*3Vf`f z^WVGp{y9kZ6637A#{Y&&U%T|`0GhMSG-uz65)!7XbyZA@d!O69kE%fT=Y7y3KlWXe zxLQ};|Cx4$i)!ltt&shgp{w!VQq!}gXw}z0v&^nF<>giNV`qF7{E205HI4m%U{V(f z4Y&9Swnh&(|E=nI`G&uD?MTM-hc2jsj$Iy4;ng1X+6JSP!4{A9rlTK3Ny zWaN!JWV(up5pw&d-XhQMG)4{Ob^gp2$0AmD0o<)SE65uy-o0x!jW-;9w&GI5?Q`u3 zu=wU7Uy5v|IxKUre$2bUM`tXWKFEnB$f5@tMEC1WVG?&lA+8yB`Q&Ft5)4pYz+j`^ z8eXVk&tWC!NDme>?WVs`m7UFq_T0gt#H_Zes%lq<2eb-&C%1P1Js`z4A_ZB8n6R~ZgfntfJu8+x(V|;gbCiQ)?jm}G9Qt0X@V)bC|6q>ObwY+NGn%r;I z)zx4GTKS}$@rsB32xj7cv;495h_P+*>Z1Qb@Qk&8o(P*sg2H$Ghe8U4EG)0X-W{mZ zNEH+m_JD-sB<%dkyY}j2Qs>8OzwlU4JpZBImEzhN4*jrA`sLG~UQ=y2sYvp>^do(= z@~SO95*=!m8hps$@6wIQng6qhb|FsRwn^-C%71-0=d?VEm5puqNQwbEl#E1LV2@d} z_hevTj5ehmgtp)Dc|_SVA@lX;p`v}#Pr9tJ$53HiAkLkxaW(lJ#o$#uR{V+pO{dPC z|0_r$P|6V*&`SgH&bO$DlU=V23qYT|ZUg`!&Akp0LKSDLcF-0^u+&;nMiRT_%ZLQd zSXL}tX8hr-fI>G^)vEnID1hl!P1^$8?UP{0h|z$PVqWb&W=$Ix{sWz`|8-%2CADGp zZrh@uUBYXkS4v#NTbOaR)P_WzmHQcPs}M@G^+LM4l^q*xSd{+qzlAqF>IFsGPrCW$g{~Mwf5e*R?S(;o7QAm1 zV7#MFV|?%HWF^xN!7GiJt z(<#_0R8{_-F9wUg4tbgUwjE<87&Q}%D%4DH+0$DG$OilFzSkACPk8An=VYgU7d6?D8x8~109uQ8?F!qRO0FMXqFxz!dr>av5PQHL zQ5~`yjnm61^cABVXykcQP#C%lR#h&2IZh!0A&crHbbixokxTzdLWP0FT#-{maee0% zu&6t|<5a3F2ZblMBm90lLT1*$L5R(0=(4`z>qu)BAs?8^T%p#st%k}^QYSG}79k19 zpap6PDb$>FYTS(oVQ$-b`eHXO-Rhud#PkK(-)pk0+XwR9K9DhGE#T>BHU(Q*Tr<1g zOULVMHME<9@^|e>M;x`ps_KC6%9NKAtTXI)Ns5`dd~Fjmk7TJkZ`1r ze@#YBCkq=jojYfX+gRPxN!+M9n(O50@U6aXV$LeQr|KK?`@%c~;^k+sH!sY^oRrJw z2cdtz*va=A4Aj{iuc^TJP0|juni&&cI(h@IMKyAUbb)+EU8n$9n6k?6>l0AWh_6`; zg~kdQBnMiu`x6=X{UiQzc_Dj!Yz=!`Qm~BA{U)9T<10?ts?JI5yk#^+~w~uQ^J&qV?M2U)qLxq%7mXZ!TJa_D& z;g$O3u5S&^*!qsYXPdL$l08I__3s=*zMm+^LSx4@4m+>5M}=WapUkX{_em09-hY{( zCX3oEP$+e$0=f-k+g2IKVH|sQxYKI5NgBOh<(qBsnco2%XuGE^G{YGsXC9KYT6*{v zJ{n3o$=2a%1O$IS^JIC=$Qao7RA>&XgAhD^sFJ${$kMk5@AoEjFZfL_Q5{xGY*|Rj z{nsAMx)b`9VKEO-yG$`kl;`g_M01L!W=(sM+6$=6{|_AwG=_W9KvY1P7!Nb5k{uLi z>#uY-j;1mZKTYl}f007sjsd*tJ2uoKiD*y|wKg%5K+crQ#NS3v*J$3^gcY zSV0+s$chs&#FPjagL(jLnNT>hH>m{O>H&L{jnZlu3(t&o(O~Fb2Hq7?c<6d94Can< z>aH)>lQF>`5zveAyGt__?#@hal1fxRuo*`ynTS6F2N`aegXe;Sc&`eSc~?r}n%Ki_ zNWDDhKM{zOIq7s$p)jJ|*$4`)LqVeHbEhh;Lp>sTl8N?;*xU@{_+Nv&%io0xqUaVf z7XEDU$iTj10$<7C!)f=zYAp|m#kN`I3R>Y#J@qZdQ=a!VvsRI0IVd>cPK`&rP(`hx z<>(D4gt#&Xv;?x90*DPZ$bmaE0M#um`n-Q;;MQ>BV<$`)z zG@{d@z3t?&Tm+2_I8Gy#P8b{<%w;vo4p`8-ZXjINlnQ!6xCxS0{dSizXKh6eBIMAi zp096+LiY2wsv6Lw=eUnltlqu*0ZJE9pso~j*?OpX42!k|0*MgLUs%tk?C*--ocBvF zZD>My9=<9d-H`9y$H21J50>?+zqO+Bx|jVuhs{>~)Wq%|~%um6oTycx`pL8+CwJx;7q>I?UHCD`{tiV^W4S6$IBt%K79 zi2t!VPG(kGn-4HpfN7^lj*yU$pyLvj=vO}vX^%Q16f;+~6s5Qa1U|GNcaStRq+aPw z&XLZ7!S2csNq$#u)eL;aN`k`LQ57-Zo4vQFh@J4nE=YKnf)TzvwiB*3Lr(FVmP zs^~)-7#aBtD(*#4khUV>`KT>O&3^(NQW%}&>lgl|rF!92Xn}8)gPEeC7A%_Y_2(Eg zsZ-}$)`3*PVNizr4%%0r>klO4 zW1BY|THPa%vdHpC!wZ;}`I$W>_UAcg;X7zPph&ep9?_O2k@4KnNCT&`3j0PNkkl`= zmFsIeN%hlufw@rwp&YDVaf$U;!Fe9@vtmaePip~!Xy0GIgsrE4kt)V>VxX0kl{GFq zqTWCF;I~IIS@@YK=;EJT=<%Cqq|#_Et%Oy}LZO+wo`Q8?%lBtN{{jd)uvhxiA^_Qo zwpJY??7CxsS^?qo8&02KIg`BZMGVrWoP;#1mw;lq5Gg`G+>-jh%koEm@R!o|#+; z1lTkb;wkt2C8t1>*bS`O;Ls4dj5=RO1eH*RhA9jth7yl9$o>03wu^9&MD>l5t-h$K zdWI3xy;k2RL2Ownz;s*p=8qb#Hycn5Y*g12kj|!ymO#(217jizuBf!U#_~uWb#`5_ z_@T%vz}t^gEaut+Hb0ChfFS2-!zIr(P&B=sZ~lxrwHBjk-awr$J2A7WX%>8Wh60N-*03gx?Q^fCiuE zuoy@1e2MG*2#3>Y*C!;{>xS5nL9}`u{ivC2nU-tMTkKBaGl{0MReFDr>_io-rII5dLq-fzx%o zmDdHhu$cI`#j;o?gv=NKG)2t}IEE7e_=$PYLTUx8^uy!S*1+fL*uG{O$RP~dH|U@y zhi!V8M*_W%WbHJ3*l)9x5~x1D*-BV}B}l+wW^49ImNhQh&0VoeUlp`F6~c@%wTX(%{hLq2BU)cY+hNH!B8o9R`2dWFTGGLsG1E^t(! zH$j@m#7O-^*Cw#Q-G?fw2Q{--`2C`XxL>_abbFHcaRATO!cjT(5h!K~jOv8f%>}H- zeQB@v0x61O5J%tscR2V-CP}Dr1^jK3`yTN`g$7PAQw!_9B9}515(&*_Ms#6AOF!mj z&_#WLz)4z3Fc>9$&j)PAA#GFOawBte?9ObP7lLD>8 ziTVY@!< zsFa%tbR0rQItD&UD@L6X+7Z$A7QMsn`wfX9C2$xB zPfbwtRh9i?)WL3)VHg#Y?>5|s)A(FEh=P_MOU)=~aR-4sZJF@kXD4K%QBK(l}u z3Nov*@^Xgr)-`av)_ST60+d2Z>mPzFE;mC3tc+@U&T{~;hsfA}n>8FxJa4m4P?;u( zm6g-d^c+}k>080~93p*jKGk9Unn*^6p@c~-7-}8md|B@N#vtmo$aTF2^-pSo@6yUZ z%S0NC-3rGjb%a(i{sAXMADO}FHJ4jAqr-nAgR7!0$8XGc`&yQeo1Xg({X+1Zlfw87 zFGZ|RU(h0%zxY zm1n|I<1;x^{9c2b6mb|D>_%}d_P@y9iKhg#7>9<3XA~Fr3tR)i$G6PPOogA&j4&AQ zSRVT%p^St>Xq^0yEvd8X8a_2iYPfto!l~%6&&5^HtNsb)&0j0Q=(z-;f!e zX_(sOd>K#YWW>hmf{kBBrg!nElIcGwrI(DO-?Q7D$UC?Hg%Xnh7IST5D@RAx2ul?GDiwCtW7pfWt^Y&r6Wg{2@#tI=LKq@%<9hYwFcsBy|c38zfcW>QNP zG7BQ+y-V5q1;LuW(gGm`Vm#O@*e%J6PGB?_u|xyX*5- zG&^G3%h|)@G?g>=e!O$na?B1g&si9MChkxCbA4FcTca^hj010cgd{*QZ}6fO=B&U^ ztFrpu)itxGreE?MCAI(3!MCHwwO^oQ8Ch%#YNtSD(0#eXNB`SKFdcUBOF(&Ia^lBa4ls;c5#)Xw!~J;h(&AX{BdHN*4RNTXwkt9 zGhN1U&PvPr&1KpR@vF0D^4qo+O+?PbFc{Yx%L`^1N4hy{=8JI(4*x|4_^_6i@@mR& zhm2tQ`KWfnB3sK=jK>llABb$8^WXgbFs%G+9#i7%PR1a!yNAcT^TabU6#BxGC?JK} zQiv%1f5zn$2RwDk-Ed`BzOteXDy!*48m5T_hec%EeFKQi^G4l6i6n#t>BuEhqfO;> z0P4ResGM{Jao!iTBx9#y^*{yOr*?sG?wuqj16|7Qod;#~nu z&r44xDB62M6=0%ii>TH)T-y-7okuu$UwvZo4^HI*+r{{5+}41$DTWC2QT{vkijJWA zA|c%3%oQJ8$RK6@x$0-YW8d>{dZ+4-5Wvz%coBCbz~M7yCahimZexNDu0g}30LxEb zk&wK(-o2qgoyOb`v-7-LS64Uk6(9Ik&?O1G@osswf!*+Gc7RQT>+kUQulSc8GGJvB zzyxO8Hw!!8@}MM(wMKTo19B4+6HUL$!<<(Z6m8crQ}9eZl2w+61S2ON!iUcnVYFcH zf3gXx{Modh>tc=|X2|%dahR5Ia~5%I{DX^Z*?jQVat+LHCAj%SJd>aq%sy*`u1ObkYss8k?(TXR4dial)a02b=OA zQkWrdJ>`Jmw>-L2_hOa4{(H4K#V!MjrTfHOoz(zlWZtc*Tv?onwCv;C&m?{VoZ9e2 z7&WL7&FFbPqM+B#&fug7$-I5l7N_pxd-U=jKO~id{|19CQXs1H>ADRir<0-&PqOSy z6mp#*%w{wDojZGCdjl0krEd$w(mIorgV zOT!*D4b5b8w&6r9JpC}`G%Q^)TSJP;860Cb@JdxlagJPi@vLpY?eB03rw#7>Xs_Yk z;SnQL&y@e1#i^HnjKpVpY%CC*9EjS;BSiQ2_qP(`hw24l*D2>%n?U&W4yt)0mD43i zE5i(j2JXX)c9H9DS}m^lrqu;fGUx&BmhDy7%)h;1 zDzGvaU=n@B9EPm13QgN)H=uv&{{f-#5m+vk^7&db1{_mTb`+N;>W6#jU#}!k zog;a40WFXlhE5V2dOVZJ{OAP$kNJC2gwh%suw>2k29vl>$u91=Pz4Ao*XIb36bf_g zou+lB)OnP^k3?)Gf3-}zcTu9`q2fp$P0El}SUBe-)T;0V8bNxsT5mNjc^I<7UV)oV zJ-2SxMeUOEma03{{@Z^{a#MwIRir-KZ^RiZH}xRCE6wZ&Rx8`AxEe-aIi4`DsU}YX zvag=2=s20v8y{tE!S$nd`LeaT!mltl7stcNib9;66@2>|6vtulZ|Kjx;*NumkhkDw zV4&4yQ*hjEB2bZcocw63#DOs*H|5G*w0cB`ASjl?-jK_}y|l0pP^3Fc8x~@N4l=3s zZxT4a^>G~9&woI-F-_9JFb6mEf^CZpOJEy5KF{?PKHbz4`ToO zVCX$Bs|mIPsPr9Pjj|Ucdr`8~PI*l6=8#`6RY*1qroC)nl*e<}v1@lrV|!<&sSuja z53;S}P=dh)0VzVS6n1qyIUacoj*?{l>UNq10fnj9uNCx<7dzCkHxopv!w$p_;;rN< z@GUGu5g}HaHdkt&M%-KP4DLa9JICWAz)l0_BilO=Jz;b|+ABPAPHrxseDa#Qs+I6I(c~Oi+*~p*our)S8G#mB~2yd_oN5Dr<_!z zX)Fy}?m{P)dUq$i+ivZ{OF0mACY#6F$CVXoSnn2G8!Se>YTg#T9t#!=6!pGb!F2SH zc4Lx*xXGcYe$==201~rgQYgyRlfJyjpRzy=2!=A9Oa^dio9jaCqY+QRb7{JWR3Yk? zHf0#)fuIfT+;x>V4z#u>)?d!sK7#|`o;kbPWHq`hA$1TuS4FakmAVS1by-at@-6C^ zYD^ZSLj46j2%hG|b@3F_DkHA!_qr&FF9B#<)u8ZZV7F$*FkJj5PP`pZTEL(@uoxQd zUq>g*?x#`UPNk_`@DoaXPes~v&#(U}TbL__a*z?RirgDKSR1l;>3!OM)-=B7tYz{E z0*O}@&iQQab7*VS2@6g7`{iM*AF`&VCI+)EqoToAzmI)2(kXGL;y!}ppH21bf~#O$ zm({1~Pro$_bFCNK>aefYjiNBVBdW>AIvH4^i&dGTe`Us<5|#fhJQ_8k%bf&yNgzf0 zO@gz}r7+Y+UNNQZ-EFzOo$Ymv(*QdP%}HE<7H93t)LTmSzs)xvUcPth+lI!qpA$)u$Lsa(Wq;VpVYxcMlJ&eoyEu%8 zF3StwXky{dF$S@B3YKMTtuY!wVeS-0Zln3C924|(Dls}9GgnKN{G>sF-I$Xh%Pj}E z9(iIH%=B?V!>Gtu6m3jmr>L-P6?9%hJOcD1{}fCHC~eKJI%^1qGiQZcjxs*pIJX&S!V#Y{b9GgUgN5BxeNhr2?t**&0be1KFk4zTlv=# z*?cN{u-HTlnihSWT?lZ;ce~CmW3+bc=i%T;2I#rcmF7y1?FyBr{@}r5M=P!|A((Qq z7H!-Ht-?77myw$gyRYGo->RRjs%vPNag)Nebz+N0VEe3qqy`iU{U8$$ipYUD488G0 zUMVa=l(%ufFe(#|V3+t#xyla01h(QttMPG|W)Nl>ORep{h|~pyhggXg-Z*)IPSaHw z@($olPR=J))?vgEa4UdHV&~wX_`idYd@8dcoed>#$I0>{CxTsLp@7p=QH+*cG+U*N z^nZF5!}nGX;&v-?y>cUJb@h=+47bFeuXXD@<8e1*#YxZ!FqEu;z8)d)4=@dahTSF5 zr=jKf38Id@mi*hMkj23xzD?gq?aun0WFETSbh%CPH0FIq=fm~L>SL@ii#*=aJZ^V} zQFBoH_YnSwwB`D2h3Peb*MwQ| zhQUyefXJYJCo3ElBNdwT!whTL8>qo}rSSPz<*Vdkz9}|zaZ-}bPP(|4Gd8#XZJ{_U z{pnA;hE&r`-P=FF7~O?(dm`bg zN-9=qDWU&XA|KCS(c(MQt|_#pEnA1(UIZCz4O=nh%|&^bj&`QuFKKCiVs%&xQuOTX zyu7`=wOi+CK9O?X7Q-EX4-5SMFusWy)KnTp=Pw-)aUJYbrPy}Qw>iD-N+aqbSmF5aHd9o0`LXc3L;3smH;fjee)g7Ge(JF%7+SpOp?d8R5 zKj`JJPo@oB?|u{di_>OZWC2^$4BUQKH#ZG$GKe~I>`fBzZ6x`^dV8W~duR)hQ{XX+ zxPvWo*BHouF8!JAGJXqjC4(I<(PK~SmitYP#E4~ZIeLKjPA`ZQSG7d15qIDi!-PJL zxgfc88tb9jVMUHf*3Q%wh%Tc=FG)&UA8%t%0Tvbu{H0B30fE#M z17ACaCv2PXQ21P{%Es%d?WKj=BL62BBjmqm} z5cR>eWZ#kkR^}h$W_mR?zk7QV6LF&-&# z&4*Wa$9T(+QyZ*H?pIz}W88 z4tH@?F+;5{3k%dft>%z=bK1Llj+5u#-(0A@#uTR{sfq-q&%@N)rF$7>Qj2b!(BNRD zPgoy3ES9tR!N5unVmUrm@eiJN^XF5unH+&_qP@YH9J|)HkHZG6GKd|O{!fbc%3D3{5S$HP@#Wip&rQokoX7Xq;?0mnga`Bu{h6eDG5zrBp=UU$Cl zHoAWf_#Ee9zNC$0X5FroAR*3=A$c5al&oQg7hd z9v|6w43;?{$P#gnNrKSJO{ zNN8zkbuT-5c#yyn2V~^rK#R&vd$B$}r!F)Jcicrs8QNp4-0JWewWz~hY#Gp0GS#IL z*O{2C9^UV%fmQZgM{HU?qQL=bgBQ1B!I}@gA1X*-#1^2S6zKr~p<{plMgTGkYP^Db zMl=wtC6wbB{aH}w!Bn)xG9G7pFi=V(EK7bo>Eh&_Y+2m{A*fRy-_x-#ZC0`t@hZW~ zgZUREBobndj+kd7?k!axj!!o^{7DTA3}D6da8nRZ5_Vdp=MPc9STJgE16LMG%3lM^Asl14Ji!68=JFbCrP+PQh(;J5=2uu;pucRp+P zVd&C-|67n){H9;!q_*xi=CSXwH;f@6D?1wjR^vF$^S{j_^v0$wOzrNCx5*9o&fbZG zUU&ZL_cX6%a{YGw?>y_a-L@w0*~#xA`R{EWss6xD_W8wy$%hZ{Ak7XdWKww_a2)^! zxoDC9-pDNPU!SgD19(6b$O^8&@-G<=a~Z32FK$p;?iP>P?@FeYS}A!+=29U8CMc=*P&~Om=n*tcB{rIq2UBSVQ6S^aiO{9z{dvghoZp9TfZPxQxZJ&%n<)NV=YP`_mfx~JTR{tMg88|HbraE9~~0Yf4)te{g~GOF)jl4nY^)9^E}~Eymda> zGVeoeMYQ8`1iDl}*l%Q9d;KBI9B@c=m8GRS3x8k2XnV;Wo1Lc&GeSm&eHR|bRxVIfu(H+r|PCqFYFRzl%@OY3&c3H5E01=#M5 zpZgx(q76tS=|Rc3oe0E2?L>qP` zn1t@z97~zl!04s5;OM?m=kv9)g2&hKR+$vvvzC72rU{dZ)h57bG(ykO(_$!c;yLdj`t`}}bWd2b7(m02!lcPUgUBt57uP^D zw0G%p;5G<(JKBju1V^1C%$3tHz8n6td%djB+y7}62V;;05I+X$@w-=ob*jb#gqx}c z3Jqt`s4=%dU<8+K6UakUf0G;GrG^{Y(iqw>7-M@5$5Jcp|73PoODFe;kR=0=*Z2w*G@BsMtE% zWaltko)5Nq{)HLt?~Hhu&f?PK`IZu;)2p{K;BaZeO z$1TnrOuF32PCU)I-kk&hMEM}H-Bo<_+!#AU^bzH<`kZ>jRm08@YQDN0HRqk&Nbvlr zr0gS;5}*{GgNZ$CAc<7bA{b{>1>WT_BcU>|t(bk)?5*UylZo}qi`VK0*1M8MKQq_P z?xy@IE;{kW zU^wmF=0Mp)>X7yI%}Tv}@!)nl`Ft|Vqah$5`vlZzrx=wcX2KMx9*sS{%8hm%Ui<2= z(9H7*%G>9_rzv$fl&)H+BU1~)fd^SK_W${b|87GIpbz7u_)-K5lAA+AmCF?;4jd!70q8iHtZrAM$I9d^OZ#IV0vgBf+%KZ2$t^))9HYK z6!rE}PZM0OLV=VWwmZ*a_*i}{P0ZO68w1gh(_qNXJt+aDn1*jZaqzSrfy z>!3Gv#Xy_I1CvFbxweo+HJ9SHtZ@i;xXiYoIib=KmdAJ5gA9tEZyV#4(PzT?^@TOa zqK|zxN^@I}kE--%b{f8b@Ady?1YCM9|CxCf{Z&*cG{goIC<&WF9k1f1EN6CPJKk1h zvt6aT8!M=}+qW{hE$Lu(evj&95nm>W5wF?*su5;})Lr8hC@>P;V#7Wq$S z!(m4?(87W|iVg&%KI6OJoCb;9V{_SdoxtipPi6{>YFwCXOitmFAb z;!V*4{Dt{ZXowy$ly!QBQ-r9U@djg~D=UI4y#Fsw?9Hy4*vK(P_T1w03_27w z6cHZ2K;k?(lX%q1!s32d28Z9SEKu z4+x$%he`QnHfBE1!ktMZs^jMJYk?ru3-QojhJG%8wj}62)wy==Yu_PV@c>8B062<^ z`#pN|DKkFo5J>e$a3;yWntJ*5C`n_=d7BSlp3x=~j8X{P(%c)}aq=mM&nRsBYKpkP;CO02=+D<_(bw z9JK-(nSs5PYh5REk;^C{i1gbY`z1F@CNSS~)9X*nLenA-ZEIHxQkPYjP9`FLqxrbm z((xnDIQ)JEEFL9PEsKzo6@64069l4xqnTOV>vDhA<*F96rf@znDh{?q@I#sH5ux!$ zm(Eyop^RF{WcnrVHiDu^$QQqbVZ-(FV2S}X}P7$Z!6+ovF=)vISHN0 z{Sz@U5V3M69(*gMD0IClh-aPYTTj0_hQw-ud-k9nMiCp8_!ng@r2|BPVvz}1JW{&Q z>Ti(gHwbyNo@E0LK&fy()kq|4tkpXtc{tdRa12MpFE$*Q zQ##jq1=5beqy5psv{_zlDz;f_I8rO2r;i^v*&46{nAsx1D{(z&Siqq>Ab-IKqZsH^ zVP)f#1`Q(bD?OAfFPfP2y@ZDkRNcWAD#Tw#dSc%#W`vn8&&m&xZ3>9kqw=qYSqU^_ zZNGjZWig?SZ+DahRUy63XW$%r*&4x4EI9LN9mkfk;h8QuA9Di&TqzKC1wY?l%1~9; z6BicIO!+En$SP>}B{Q3yn^g$egiYjL<*m6u#}1AHmvPxqbF4+9nHUqWH$gH;tL8Fm zQcF1U062$^y-+(*CCbVOJ$|=o2t!Iph$eLPyA^3!57X)S#!q`iXdqiAqvAIAk7@2a zyLaL$62`KF;I$Iph4{Inj9+yntVZEz1f)VbBFFwzdjlM96`@^OXb#o+anygI!H*9s2I7#pT2|4 zjjjkd;67F#i<|~L&4zV;Mbh)~xP)oxrL!`h2*IBSeK~pkp)F3&5B2 zGTCpOhPO-iC0$0!=zUT0FmRdC`JF4BFutR?`bltXBX)cnc>;>H$eoFf4mEEtSDsq2 z4#1HZi224xXr_ex5el!vJ$^7kx7VN9y)N;GuCwk-wv<{U)OY=%J3(6^NCO+(gKU~X zwi-*-Ctug4Yq>He1D@s=4FAcb0muaYXA%+>i4S3w!br&Re?d-4ddp$%)95rmlaki3 zZlx5M?raBQ5fj(i?)fR7aS^{~+etu`*5(ZQ%uH{FiCmfcC3RO~L>+E++XNLodrl|9 z!ZO^{FL-d;y=Huq&A|}(1?{sW>1h;D_5nu^FC|l9b_c7X227RBdpjqL$=BXfSBs+d zCsi;vZE=lB(B1=h99o@noF#JtlzdIT9tyogFDL$^8OCF*lpP?F9p=tFR;PdCG=S!# z{K6*+r#Xt+%mZfg5U+cZetLz+)H9)G z8km;G?1^RHXbl3`Ot2iIy{Vp*-$4|=x;5ZSR|oAM)tNd{E8oV~Nk-`H*Cq4F9gmyg z_F}W;(b>|7yuvLa|LD}X#!|PhpL7b>Y5v(pX`=y^(O?4!qw-9_QBpS3eT=NhGTM~;Qow(x53xy<6Sx11pLpj1Ann% z*n#QGiQ$+`Iag=LkUMSMDpkAGE|IzGv2^m8UixpZgi^KIb3c1e0RkB;D$95SAv|9j zdWqH5V`r+Y1B#f$vkvxo7l=sjgN;MtFF4tnqXvm&4Z`Zy2u17GB1a*&sGB≠xgZyP#dQ5VS9(SIA383y{80wfPtdL9;T?3H06fF^+xs&x5du&k*$cuheM(V7?nh{? zQg_uFn+bS62A6&FhI1u7v5Glc}#~2uM8I`+Jjba zi{S8aRXt&XS2R<*o;n$iRZA(gI_gPyhYu%F$BXd`i^g;)K?GFwe-64oBS`Xhn@)C! zzvR6TB!F33UCV3y1pWw!STJ%hR^s_pv*1LS0@(r_9JnqY|Imzjp#XfnF-XAwnm6xw0b@M6}`!b}k*i_O9w>&pEK>GH~W>qSNr|T_^Wh|LC&` z*>pou^`lFsm-l$u8X?U1D+7UCfawU+d};*B{>ejg-XHiId^~J>iBL>_io2m$)9L)D z-^vrnzN{%|w!|++97Log#JKfkdkse@esF03QI9>K&lO4e68Xw0e zdCO$C4&164w=`@uGbf)*!X5MJME(ZhvAx~R+X+HEDQfc{KeP!%h_xi$Y|Z7$N+BsT zk>rgq|J~|07#!fXzuPZKA+HRJNAYLe?Sm|OxKhR5SGK5s3B+0^g6NOeq}nX@ky&s# zMUDDqJUvV-Wyj-briFOvhcp_1C7sOYa_YaeuRVgW|mk`vizx0XR@6nsP^*^=Bk-(W%iSXW_ga#b4v^%#d zy`U6Hus~@Ou)uAYyRCe_&_n;m!^Qgr_m=#xb4b67PZV|IgWhV)R86vlMv{~gy>%$0DTjO2;38=Sapj4M)kH*taliB&?P2PTyQykif$7ZKRdp{??v*l^t|<{ z-Fw?UDBw4Yt-R0V{EUY?{@NJq2u;$=NqD9<>{xf@Zf@~C`O+8Ri(MIf2_&smzd|R zYRJaexrTCZi;w85BEz>6bfE-(LW0oF&_6bcO^hCtXut`9EEb9ax&I9T^Aj|{wd=;_ znB-x+qd|YJ@2nWFWQS&j(nJxHfH)aa^!#vXI7X$S<^0-?3=}3hU!Bfrq+Gp)O_7LQ ziJ=$V=x~=Y6k#Ox`)#F|S5 zxZ$!fzB!4bX`t|>An?;MBX7NKBBtp>0fwItC-iH@#AMbMU!z9qi*w@66`yODH=AG9 z*LvSLz4@3_5yaVWU9(ko<^iTi&0CQu7qFw!oXqY3Z*BdI&dOu;jb`t^_Cjc?W z<;Y8bI}9x3{0;`ml@LPJ3SafcPI`R!00+0?vSW4ei$+1>om)FIN8lL0;|nr_!`h1+ zP#$U}q3Df^t1y=?m|1OG5X$n9)}tGB1k5}OEguN)>iOAG3Bab?;9zLrD zu@FD1Y0@LZj+r|&d^BbJKM!Lo3UD17v*EuL#d_{&?bwqtpSKuskS(jLF`ojO{MK>%5N{+V`;X zfRBz+;vj#kA=5*yn{qN8_3~c%kq?aADB?B)jfjp^^ACzBwxK4Xtj|pc1}S z?KySk)MPlLJMaiYjj*CuzEv=94F8w;hi0%IRIeQOLum7bW=>RKleAv7+xO+22>71~ zfJ#0L1Y0N?k0}qk7d%k#)^<*E!rT8a52EC=3(_NdArQB3+jXlT_nwHSdp~cbNPkJL z2;v!mYQEL*vId;v!+lr+mbOr}IF)@J#sUOmuF>~7?Vd^>fSREO7)4V+>6L!=mk{?7 z%1e#yLiJ8FyM4u|$Q4#sztNasRvqm3#RdY{FiaOQ+1*NVVY9U?LszRK)LDW6A;-f% zXfg+2Oau6AC|zLNaSxVshzVjUdZt@tyzEJ&G}AY(h{`e=dAw4)sFhn# zTWU}sKLMlNRP8?qvcU>zbj698#gALq`62JQILA5G)pMOq9)=!7{3tN29 ze8F+2FCTl-n9EsuY$FpMoC$~t^OrbN+5fdm0)DVwFp#DytJgLeNog{y6{Nk3_-fo# zPy!MyuuP^&KGV*Xy*4AJqB>o@E;fAZhhw)zTkk2=toB?i5&;dS0IY_Q7Y@pYYk&&s zrQb!7QTw5xPTS2yVoM;5Zmme9>S7d+$S{hst(w2qOrN?y__VA31#`BK%e?}bw1=G5 zDa9vJga-uI+81>uC5y#dl7$^>D5fmMU{`G9^~d#sxm+r&{A${Yy>oe zI2cGXc1IVAb-WZ|t$_u9F}9ves{_HEAj`v&o)*@IlM~5MEBD1i&i$3w{ZdUDCoZl= z5CbAc(MgUTJk*-7Wz=_52|f2mE8|bSgu`T$xr{PBB1Hvi$B}5g@KQp<)@#FrHp-^P zaq?0-8u}zfz)j61z}hjeOABTWr8oni;leDeodR&Ht?^{sD9Up_2h;4HCx+XSk80|w ztj!)sDsEfQl;gY&f{S7l6(qsVEMId49vIxx_+psSGb4>HMQ{xz2(U%u@V5lpVbXCI#6gHn3&ZPcM-CzL=OxuTtBpcjL=91S6+9bmiATLfZsf*NTZt}>( zYWep1Ap+TSF37kMph7`*(^;|)TRt|y--LGlr0Xx+Vzad*59@}~v*GHWdPMii$h1^U zf3ecVtKLhY44JuMa1?&QVsasowyYEI^fT% z9|PSuqub>k{VuE=>unLpJU3wKsHh(YY}3L!oyg@B7UF$02o<`gCzU@ zY}KU{P3Y<_RrOJD2TT(BqL#1{!yrh2TF5H>jkqT@OuwE{;oWu%ap|BFOHeOBUtE!4 zS+9C3>qx1DEE-P(dfk$w>h!Kxr(Z*?Nl7|BSMMPA8P5lpRvKGsU}2@P0>SJoV-OQyg4Q>lk{q8zT?*6Q$7>WePH>EMT#Z=#gzS^K%ryR z2ax2(4ML^O$7?>|q@l-E1~y(l1t>?ae%DMh*%c%Z4;w!~;hhX*)rr~m)7F8*VoD_$ z6K{@t#)`Z&UGcok(boJWLm{353Pti; z(`91}un?|qI%(8uY+>{z^U98(7&^4U<(d363}UF+O~J$1FCh!4!Oi!UnG9&h0^EA} zUPKP9#9wwiyxjO{3&J9z8ph^U-7`g3q%6lG`CHLx62B5E6aqqTUBbx*>t!if zE1lHB5I#qaxKpQFNZM5nvFj7Y=$v?i!Lf4(C}J^tsq8_S6hp(~Qg?*ax( z?T~Gvj(x>XtFLOUd~%)^(wLtG1X@sMk_racbsM$L6=j(o)p}QukvU~{Zwr}sCs>sL z2TO(|0o)W(A7}*-J6Qt9yW`)({TO2YF)iZsisfF}SVe(@|2cV~plwzpun!{Q30?To zTn8_di|e^(kK9e%wGh39I%g+V7CGx27O_Z6{D^fQ^2Cf~1VFKyf3e(B_G`59HH*=qIk>Y^9j+w>WTwVwRPadv3l#Z5l320 z#Sn&J>TATsw~`6P4euHyh}+)GKbQ7cQAc}v@A&ZswCIrYKiB4BD6CdrYMZ1(6%oVf zL^rK^Wtiw;*cHB*W^!;8_H&=Nu6cK;KUV!OoEu95&X3`g?h~Q3VLE1UZb_n)*FdNQQ3w8ktF9DEGpFe1_H*3)N-Vjg4$G16oaq-5 zZ9zn3*b8(ef*i9b!9zh;ka*7Y>60#FiaY3*rPs=)CT z09*iPSSTmaQug%i8qR$bg}(X6T;|WGsbS5#om<9qpdjeEpPnxTwRD*kAn9#sCyDCe ze&rR#GB`U&)^=2i!tRJov$EDJf;&a*XQPVxf7xb)3N2hso)eBUqyFkGi4lwsa?--J zLD?t54Gxhz`a>C}t&T6DzF(csnKY-@Cvy5`r$En6nn`o5C?a-r1bvneD2vD>Td%AP z31am`Wwyu5nk6w~)mrK^mSyaHTdhs0aRK6d9Wk~wNXyDL1hsi|O6s_bmMY+c!X0T&V6Fvj{` z0;Sb|*E658Ap4M2iaUaIlCV`^+H$L23HOSveOenB;kifLtCNT385QDyYBw(x$budS4r zKhUVKEMnIuFnFCEN*T&F)D)$;{Wq2AeK2i^{= zMgt^I_NAqGPFgdLdgD4zC&g-qk2{WF%aU<=L=x&v53yo~PP*BhmfU5!!b`z5=Fseo z(Eds&eN_XxE<-;VVY6G$g3%#f{8?2Occ2sDu^{e8KbsNTA(mHpQ<2kP3&)VlL1+jZ zc!)K4$ggw+#26mTz&{vX_eexf*I=#}jdZM8;2)v`n|ZXvjyThh5lxU#F5`$?5jUjd z@b-1-(rY#!g+1dfS86F;xZ~n}R(XRwj}n+wk@D4Zp_)(+eR>q1>3def=~zCZYn#ZCaOCbea~EYQVUT9vn$K&Z-3^0mLQ;bW(^b zCT!i<4&&=C!xZT=GYkZ**THNwl!dIul3!?uKFz<14g5*g7S*STs8Q-pC#2;$+2L+P z(enU+x`zI}2`?R5e<83_F$3&d4~gn-0=ou!4iDRCF(EA0!Z)W5mU)(gOuJ(bd212I>^ZWSqwbu-0m##DB{)2x?lj? z9sls)@u~VgHxGNn-fL|U^7da3IbO_Mjs*&1ZZLBz>z+Jxh0=zEB#rl`A8+>VA^T7ID00Mds_5?C72tE8Z7wZg+x~9P_#g24U z9s+kX1w$dqaJkb}c<1%*xBpVoA9wCwIyC)4DC}r;0d?jsCtYNE`HrAfMha!tKs8i} zwq1$5GD?F-C8q(AkKgm(EB92@@Au>HsC;+u3ad}MzTOnAFOEz}2jxEmpdsLUI-R@_ zt);(?W>W8k{^TfsZYcM2UX4yyfR*NCvNP^Fk$BM2tFc)$FuFPF-^cL5$KIJCv!mFz zYhTje!l&SBX2ZlKSy3ESb|4D=m&JGe1%k^pHLc666xdfyBX(L-expZ40~~uSz{|N7 zZ++3h5v82)`>_;V_aq>Ep#kik73RODCRdS!b_134He+tBk0tGgT}-7r-8m)g|5}%T zHD$=c?8bwCy4gc)xtrj0Rrs#onnf28-CT-HLo7}e8d4aL(E_P9QMCN;I{4w?`oUv2 zmno5UUAkiA2Bf?2N`rV#;R@ULZqa4vMZI zJJ}fO_?t7(h>D|icQ@Cbt=`;Qn1Z_qa3a-2f#0z=zq-r&cvKKa!d|soFjusOuXX-G zj`tnRB#h4ia;mIGkW}$DBJF(=<%F*q%FQckqOCX868N_QQa>Af)y1TtjZ=4(^se@P zuvt`~yWzN!y}6+-04Ci%92i6`z8A^jq{b2{W@0$jmYDbUq~O)<7?G2P2|%dB%3Kh2 zV%R!G#6yBr6chF17*GZ5ZM*N-ZPsC%B?LpJ@X4VBc#O1#dJr+K>7*)}olJP3sm=NH zo2L9`5%ZMcsC5C-8*hYh$!Ww3uwgK|2vQBMn2ChPg;FSY@w~Ch`TaJrF}La2i>0aO ztpm~A0HFOPV+SCi0&#u}>;=8G5(&pY#Zc~XAxR!?XQ^6vR2RW|4&EFjaKT?XVF#qD z3j?97eT0TSLL822eP{yHBP<6TJ_xBTB;Q|m5&pKi__qcx*jz%(VLW*JT_0zAE1D1s zuH*sfm7&7SO%RU!Ho?B_pgM<&0H?b9d8Alv2+*ir@MJ5Q2~T_`d>8WXYow`)8&2Hm zGSh-rHZS3-Z}o$4gkH#YQdQp=#E?QlL-+}R%bRz$}wy zM$P<9gDs+*Lt6&&S{H!U%nCulswZn$9y}wakH%4F`+mNXK6BA1A4()`HTyMRYt!Bn zEP<$#Sv3enRok2yl6fomWc!Ru0J>J{YWsK^{vy1=C|qW=v*T<_pn6oi<$6};*xnnv z!lU^_^fj?8dff)kkC8pXn<`}WV79=^AkiHxua%E||L|(_f-qQzDRA6C#2Ss+<>bnU zxro<&Vig+nt^xODY+&in1x?gLbkDIB=Sng#se}-u#MNar1y0Z3z(-$>t~89(M_}TK z5!*TmAJXt1NLsdG=5^S-1j#5WP(6PQuAsAQc@ZJbuMeH5R$C2aO~uHCtl7dFwTrRG zsGc~Vgo{9DF75nh5@l;?9=3o)MvGIcCyaE8Fs)zm_cpN%sCaPJM>8>SI`H&wTueXH z%g4}5KzXlH8@A;?t0j!R_SX|rN67{eJwa0422cWiSd|oH1v7Vfsdh8k*Abk%Y9<}g z-kXy)&3~C0DP{8tG$WsQywQ|CR5f!X&y8mJD>TplwrqMHAhKJrAR9@ADwDv>S`lsDb-O zo@E`K`=`*{l)inU|DdRy1nkPjP9BAm>_-{*U zR{!4i_sRWDZ}GGRL?&THGya1y8{*szr=C!Ylkbs2yh3TwS^I23Tjm?v|FD&55HZxK z9e<3uqI)hvmde8dM4z7O+_xn^et-Vd*6KSlG;##~J4i(h$HpMwl_JIrQs_TJe2ZA3 zHJCwyGbjab6Z#=DNJ=&LS2?6Szav|ss0~jQBNs7_SY9fG`rE!tU|=A9imA!5>Cus& zpZ_@*81|u6DLg5B_n?j`y7=Ha8pmX^-J~L}QfK1%j5~$Aanbu&*zd&ohT4YKQujxG zE1nM7>%PVXLisOQZ)7^lvijZyr*@)tjBp)oZ;a9q`=omp7xrmaa7N`e_I2Gqv(^S7 z{W#WDUd5q=KrQ7?N9N=CkEqu*1xQ6(Q|ldDLQze>tl4aT?CcHx%;tz3X~BpkrCX+Z zO%EaaD)lSKb(-B#SIto>(;Ah20twIB^!*5j!l!G@-J7<(%Z>R}{#Man%wv3{&`COL zUq*wP}#U}|yDwG@cP=dYU z;X4aP!N3%!{gS#hFi#}FgakIG0LD%a<2eLmQ=6q!&-zG+425l$Dg0-?wQz|dw6OnL zjXT>b&&_$p)SHd*i$Gfn<($doMq%GK>C2L`fXCKbAMB;_(w`Ol5%Vm=sNXIRE<;M^ z_jUcI@0@73H@wS`4kWQ+>F3(tYP{o-U+olUW=(lT5x0+B{iyZmfN*)Os??7RF;K(# z2%mFAo~Vn1G36sMlvsBRc~?p$A^(w)v)>p#(?nhh@nEI_&f3wj7mU>^2c33cl7)?8 zFKF2S+-R9~b;O{3Ax^(@x4j&3$!fpz@~gr1cBbL4h;g}IIlkw_Lp2Dy);u;IFH~5B zX%0~N9mWn0s(L*?ozEvOE*|P+SGTLFne%pcgv>z4waH~oSp##m5l`nq;rejOB);_z zt`goBH1pbf5Cw36JcGl;#OzGyH{%9;7fW77M$5luY=z1L+k_7hX8nGm3+sB1Wl;J~ zNWA&xe3VL}IcnP$rc*P9FfA-Ssp8KPJagr0JadEvd=h+L)B2z}$a_N6(_~IE(~%IIujPkO%RS6w$t&Zi$9{$l;^z) zwMT9|d2cHqq?Wx*VH8iF%2z}Lj^jz_92dlc<2?%<4I-A0$Y+Bl;gjRVLyxi=_lX`d zka_`Mbvj?Zct$#SXu zeiT^ex#fov#NhPzj0X(mZvx7OT^^g}l=YdwYhj{z6-LXoC$WQ$(nZSInkLG_-=<9& zM{np+^UJV2f2Uk>w^Bt1jlZM(a6(@Q*IX=4&^i?k8+yZUKOYDQ+f#o%L_SPeKR#B9-2D-^u3GG1-8Qi0^~a@$K3>&Z1+D3F*5v%q*I=EBNtbx) z_i3ubiCvh?7)h146uFI5KngGL=vXlF#H3JQ<5oSkk}S)?2+yGm@z%c@g)c>SQy;2x zzZ!V7@^IyvZq3M)dl`OxP`tBw#FN-G(Ir}PJgo7SyQ*GqlQV5fWH(k36f%`W7%}`C z$V0^YlZyyi`tceSciah>7Kh&#nj$wFakKT#bT&3NW7E?}fT90G2;{oH($qv3zA5VR z!(fZ@{r(${A+8A8Bn`@rJx@&SsrS(dBnxQ+o#IKm7uZ6aoKUW@(p+s4OAd#Qxo~pl z`A|%hU5so9#I&0$PporT+ZW7%0OQ6RxzfIx%Gm|qP0m)9;dA)RhL)c180|&kQoh=r zGM#GnVztR%J1}8+4P@_^+Gg9d&?}lGpFp4bm!eZgnc6-{b=bcX!pk^>DP6JsC;0Wl z^)F0(pt$AcModfA$1iGpB1_lehTXImCqA1iO~u@pD>Hl&iAyS^BhzHH>jF!w_@KJ} zYq1%ybTopR{yJbWIx3~FJ=kxXMp(>VqBhbp^QmmWD%#>RzCo>d_j=*S+XD|qo|>WJ zp1JUYrE!Mg3R5q2MnEPA;+H6S$DX$EJK@u--)Vxg0=IT|-I%Jy1~|QY2}tUvhQHLM z5*SV%GpU*{A5WDmguGShlJ^9W0H;nR7i{3X)XGJznBCG$s227`8;Lmy#QsfpHz=vv zNzy5ABnrj91BXsPY^Tlk*o7N-7~9SCtVOx`;dCc1#X?qEyNNnVVXJpsJty%e zly|WqF6r@i9zxmg(SA^|iwDHvB2p+mV`*JmzG<_cexir|eHoWxMoP>-vswwhi$5D$ zb%;P4&ow3*BjZTB&RVrY+Lz@`{nf1JGp)iqdell*$!~`_g?*Qe-WlpgGNwWM$4ZxT;y zfiH~I7A;F3t>Jh^gE*mszlcD3j$t;w9ypvHSasy!erYaylk=f4hJKbtqq4J@RpCv< zYI*~^vIQ+|a`F$>TxJE7<_lGE{^R{NaqJ6f%SaKiGFhz(a5>l#cUAmsDTMX+N}>F! zWb{HhSW*%&yJS50Jhzx3sX`lWnAhvlJnB$}C1a`&RtL z54sY3)2vjY#2R*olK5czdbTw!xxJB#3qb8dBs)rG<5L-@?0MVX!e@zdO~~ARN}X-5 z#|4u@L!uIK;CCYe%(#OCuLfwpl_&PAk1Sk?&#*}&lyxWyK_{)LZFN01-(iEbccr|z zkT%{hoXBONOk@jLpSW=zih#aVA2c2yW4w)h4eqxc6^rr2Ay3JwUeshm93?XZLP6=| zFOB(8jW$<=Qf}H`KYuny{qfZhT*0W%CRzVlpgo(c{%>C<$|Vu%f>oBv{QtT;uc)TF zc2NfukSe_dks_iX5|vPu9;y@z5fCX6kY1!H0@4ITP(TD?=tTi(QUnF0iwMC`r9%iv zZ(<|}q3pT7{eNTZbFr_^%^4>*i;*#sHM8cL@0_gnozG)?n^#&i-MV+y>;ldib3!gX zR2Nxlky754$0t^$Cr;jA;8e0{mY!IM>Z?!{{aqQ$dm%jq*5*t%zF_Dv7Ld)nZ(;YP zGF6(iSL{!PudQq4W*gtPeGX~@-mLHf-A zed1DjgW+6)fq>pyGT!%VXpvN5oJ(QcvX*sDLaVi_vV>CGp8yGlr#goG#zDoe zxx(*JmA+YW8FU-tv2l3yA!yixP;^AtH(s^YS;LA)#*)1wHHChV{z5mm-RoPb*LF4Y z`L1LnED?vW-7j_;v2S_%&|iwSf)E85QUyA$&b+x7rMCb{d2~Tm6CbOp^)CuJY z2B2YrJ0duF=r2P7y@E9d7rkj|=`;g@Z!dn2kSeZmkMiAOKH`Qf%Bu@ld=QvLZ!|YD zo$Y;C>QE)&t;9TrqY7l?0l zD%0AUc29jG|DWqRH&Qj5^KDK8= z+09O3Y1K?m90-~ct|Hi~1X)A-OZP}r#=q?jMT}3;sA6AKG13(6Tu5o~bom)EePjCL z4ENB>?toWmW1P6__$<}7%n9F?iGYZ~&L(N82z6agc&QNM2^W*;Yr3?$mtQSzZTZqW zKc5Fvb##>6;4SCE*yU&?l{61TK~qPSOp#B@Uu2T8;Nqe|Zq>PG6oR<2G8rF7;8s?C zHSjw&So)H{5!~}ILRsH?%3rX*%n6OL6_T8weGpwEP1E+l7T1__dOllJf*>EFgzv)2 z8Xnfk){LF^WaN)Z7A~4`+a_#W&cZ~8udQ$9JG3Cv$#LEaJY7DAMyA@x4Pum_bRIe& zxIV<@%Ol|mywOt||L{1HZ%ikS*IK1N_B&O1Q^%56hy^uI$}$lmT5jFRW2{RZncPQhLB>NXl0o=5E*aaFF84qMMO)c{-) zgWQHwO{xj}JL`Nm^%YCRIQv%}P}=A(!C80eQB$8gLRa zET2|0&zRrWT#GRBFmPl&v8f7v)zM@PE=CqxH7J^VLl2TB>Rc zwj;0AFUY>RKSR%BAYQe&`a7~Gu9bbPFPX)6BUxNOIs%OngBOWOwTiO7EO!a9c#5Vz z{xZc&=^CXLrTcCF_0kg}Bkrc0PB5H{Iyw}F;yrT2!ZYw78IgvgZS!&R2Q$tv&tfXA zQ5AN_uXd5@l)TQ2z1{NG=V4aaW)auLbYp?|20#AyLQ6sw-Ji#f$*6T^PMGqkQXC_< zPgl}mXDr9pPWEkHsSo;WuU{F6DBdIWGp2ycA$=?Z4%QE*Q-R4k7=B*m`kTfhhS^oJ zaz85rB^{}C+}adPyAjs-C*f}eB_?eJ7+vYYWI1cv3d^tyd@>d(pRX>=Cd(R%2Vts3 zn%O>SeDp789rGWrU~gSt^0P^SMSy6GA9u7^SEV6^AdW_qKjqU;{Mlg7*gFARjQEmJ zCc=FqdqqmKF2+kw`|KO#!xSW{6|Px#gJ1fw#-%sKy~xqaW+ZBmQlwg-Gbi|+9N6#i ze5?f==$6!!fIBtW9I=95Rckvdk1Fr>TW>hJ2GTwqW9S`wseX90N9XyX)V#cQ-voDi z`q#5F)j6UVS@`^0ez@9pWm!`}Ro=k*lJ9VR-N4zow+qXKabNv;;In8a_=l3%$c78b zlY!Alw1$CWwwjkDt0=7Aw!VF_?EF3nr^5x!Iz#+(FtqnjC-COmcbnvh0YLu<`NysN{0JavI= zj)z`zNTbN2Z&NA>=ZT+ge>5_}|NeTUe%5Wx507J?U*+3bTdz~l8381k?Z6P5c0!ho zaL>*#(U*>wkR{hhUvS?}F+)uC;^ioFJ?-~ClBrdG3 z?)i;a0+PJJ+9vh)#);2F2rr|BK*YY!BO4wP;4A|I^T*a)$X~x$Ud@U3CXXoY7d9@c~YK@PTK086ZMyo6z zg?VJ6s-N#YRT|9+YGf??8;NfN>%|`OR;rzV=TD9&WWBqi_+(@)>})z|Rdrj{JjPme ztCShvV9I185GcSXc}&+HCB~#F$|%a(>#Kf~&VDo^l(B8GrNc^styAI-xYq!i6&=jC&Yu zPOXci#>{3Eh(!%Z-W@1r*rf7p-xYx_cCjYKzqj_q^^F^Q;ELZH($xtr^< zT1?-d@7XyM!>y_E~^Zg#X2w--PQa}>qxqhcvP`2LMK;wR$?-{a0RX@8shcg zNp6{vXg_Nr;bI-1%%mSHDIrki$Hz>fgvm^vztMoEyd%Ttb3v!7$Q=1sa<=?C`vQ(3 z9@mr*+?Q!af<@HJrh{-LG^ZSor+pJ3ct_|uUae%YNXh<2IeKo}u~H15kl`M4Q|Ffb z#zLjlEg40^xrL;yEUgrA;*LXM<94MbgN;%;@ zSt5BZ_~e%aWtu0;KaG#72g`?J`NY+`-tUB?D%>M;uK(uY4|;9-OSG})7vlHfW>d9R zDWY2wHkxiBe)P6hTTTe2bdEW`?PymEruuFzmolswwn`boH;RrfB<(*dP0erabM`-=_A|*Ws5MD|g6JbT%bLxr)@ct>oQCe>7Tv60}~M zZy=YB_{Ki07;(RR?F?rcZ z>FP!MNjCphzQ#zzC*c*Ay9?$->P(06h%ou}x0u!MA~Nz?N^e}RWsKsdSRBw{>m^jj zli((-tSeLcIKKJ)0VQL2s$i3H8M&sHc=S7N-voJUZ}^h-8!GkXeMI~*^)6fjOPlq) zp@^fpyrDWwVwKNGv@N1@W;|wtK|&s8K&Rq!W7Ap=E%}Lip3*xPIBUP)eW%f}x86-+ zVR-VYYtehNijjrUkIkJ7h8rmgsev}Sr6Sh?KGyV^(rbz3N``|4SK+(qT#4gXjLotf zmP55Fw~7koBv7rj{Cg`$8lyj&=!%z!p6Jh6DSF&F%$Rg=VX$ZaYKP@^^_u>$G(*>K zcL_aVY!-aKB%HStm%K$h(Ea<>Z)~BQKKY}g=lHA=IZO1KzcnUCLurJuL7UbStSl#r zCwXI-P_4$ld}!aOu}5i*6=M3BaRfXw4$~mk(F_WXUw}v%WByqrZoq_fJU;QJ!-C`q z!l{*X-JVD-CR6lg=2w!;nyr&f_z%-|OEZQBv-DquQWU#oQ(~R6|K!86cO&>)URTBN zd@iK4i=Ly{u6-_C1p9+RmeEU1=!x7QZ(cy&e%pSoaQ3H&_MB;InlZS_l zj}ds^sc3OyLsy#j+(S894bJDwTwRsYCzQO8%E`$kh`*co!I<9IsESlj$g8gA0S|WP z$_O7?YY>QBtuM~?uUGSDfuAg%tt9B*_6&C6zKJbtXAN*(QC z8{LK+m%xMD*8leEed^jrvOpVG3HWi~OfAq`08cX;AF^lHasW9ufsWApgA+&a{)!Fu zT}ra+8;bDSMpRsn`IrLEFc;9J9!}iX6>u>h)p0!piUM{%D_;&^-HjPAzRdazm1`*X zR)pmCSPBQK&2yWYgq!x)JU0P)F>oBb?gLy63FJVIBaPaC+2>dDk>k^QU$i85y>zu{P&HoHrn|2|jZ(Jt_xc6^{rRyiIAv~(3n#XMJZS6lGi{v_M8p!VYj3lA!GppC6GgfLjdGMJq17d1Y{Iw(HUm z!jAO-!)BAm0-Z5znq=0i_g>`QE7vFpIHsKde~KBMF&|69KG#q#UCqS!{Vf zu6lnP;I@{RU4Y(0&b;C-T`*u2Ve6K;udx&$F(5G$W$D?#^ZxU1CP?jmN9HMzX~!u? z71q}arpddvLYPOuXN7DnppGALz4Tm%`B+nNF*C%13*N!oq*UZ5FZqvWg(!5an~w(r z`;+-+|BJi`>pO8SX3y>1L)tF<>c5I0z0^I~0{Y*?ZM2dOXsIMSgu}vh*o_k7)eg*I z?0^78u4(u?6FalJ@*Z(^d+?^oGYD`twF_Xj2mnaTN-D`rcJZPX3^+q7)lDECDa)R~ zBbuZEMD4ccK4g&bpLur1;P$Wqa)=`AF8qRMK7J5Lq-KIdLYb4ZGsmX?9UzK{0cweY z6X>$D;CcacS`7HP0V}|v()8)@fO_?|8IuGhz5mV9+8Uj0{3*D;kB=+_<@Z$v*S-Qg z!k4xHdwXF%WZfSAGsF|E4L?o9ks1~C%t zkPrbP%tLI@iCQ1LRfhA^f8!vQZ#+wac4JhX%VU3cYvhA77qGSygpD1VDNuCX`6p(Y zZ~>#D#B+AqkCP7sA*&Ch(E=K1#KYu#o_|9y2&^HgPSBnEza~D!9LlWb?5Nh>!-PPWzgtr%G1oTLAb*1Y=D z86u6jzR&p6V^QtGlNt28^S%%n#JyP%>mnR1UYVW~%k2kS{>9+!008L&;ecQ53iPN_ zrB!zqPXz(?_;yVOxUa#Mu|#o=>)hR$DuOmUU1q%?eVAN36LT)x84kR$&-6=$Ij+Bh zCWaScATe21=5a&qA6`G8c^$Km*{cYGXtj%dd5~xV63x018-+d+g99~iP!j=JE_HC1 zgCiNTUvh*30XV!M;{jak<*}JfML;~iAD*YER&3Lr1Ef2rb118wheMdV z!0q*n&=e6dDFi6yNu>a#dJME5>(f$x+Area0iWeZ4@l_5Ao?Yt5}ZXb;7{Rp!0ZbJ z_V|tpmmgBFX2|^3&SN$W;zq}~XVVL{J<@Riyk!`AZ=<%lurX3`1dT>3&L!Q{DOmtY zu1x^dxTO=%QU)cm%7xS!Zf#=$GYxxI6WP94CML3*( za(o6bH&6DHAA#@$bwMwZHQ!k*!*~`1h<0+1nwtIVmVZ^}Q0wChB%`Cd;#3My$h8dM zXo75hUSrM$B%dtm325sYkD)T0V5Be^ac~d}<^HD3`v9Ji8AMV=;1_Hdp2OO(;VXZE zv+}B#NF__bZ_`X)Cho9J1(A&)4YEHek`D$0afHWkK>94-GZa7wc91(H#Fzje0CWPk zc1z0n|IWigvoCPqRM>YWV%o%EkOJ&4m3SBj+2g@{HAM*vqxpxs4al}j4Zy{4es#2p z;Y^0_jXobad3iR$C_zEN3NMl-q@@FJzKam_8k$THjeqZI)B;8WzW9RVL_^tS>C~kAGPE<#kdc1Gkm9q9H(Un-$ z^UB`qZZ3Y`7(<~@a=DVg5=CgjtBv&^U$@)nB_RBvf%#hLYH1w-0Bz%RfZT&p#vYA3 z_fUD)q3Q*|$7KdvByG6&_rQ7bDk?Z2gkxFb?sE9!{r!C>SJx-(w56q`f7c&2#R8-D z9Z>pVjWe@;M3-~z*g{sw!ODCS0N^hQod5s; literal 0 HcmV?d00001 diff --git a/lab12/graph_data.py b/lab12/graph_data.py new file mode 100644 index 0000000..c85c2ad --- /dev/null +++ b/lab12/graph_data.py @@ -0,0 +1,20 @@ +import thinkplot +import matplotlib.pyplot as pyplot + +d = {} +for line in open('data'): + t = line.split() + size, stride, time = int(t[1]), int(t[3]), float(t[5]) + d.setdefault(stride, []).append((size, time)) + + +thinkplot.PrePlot(num=7) +for stride in sorted(d.keys()): + if stride >= 512: continue + + xs, ys = zip(*d[stride]) + thinkplot.plot(xs, ys, label=str(stride)) + print stride, len(d[stride]) + +pyplot.xscale('log', basex=2) +thinkplot.show(xlabel='size (B)', ylabel='access time (ns)') diff --git a/lab12/thinkplot.py b/lab12/thinkplot.py new file mode 100644 index 0000000..ed01a2c --- /dev/null +++ b/lab12/thinkplot.py @@ -0,0 +1,504 @@ +"""This file contains code for use with "Think Stats", +by Allen B. Downey, available from greenteapress.com + +Copyright 2010 Allen B. Downey +License: GNU GPLv3 http://www.gnu.org/licenses/gpl.html +""" + +import math +import matplotlib +import matplotlib.pyplot as pyplot +import numpy as np + +# customize some matplotlib attributes +#matplotlib.rc('figure', figsize=(4, 3)) + +#matplotlib.rc('font', size=14.0) +#matplotlib.rc('axes', labelsize=22.0, titlesize=22.0) +#matplotlib.rc('legend', fontsize=20.0) + +#matplotlib.rc('xtick.major', size=6.0) +#matplotlib.rc('xtick.minor', size=3.0) + +#matplotlib.rc('ytick.major', size=6.0) +#matplotlib.rc('ytick.minor', size=3.0) + + +class Brewer(object): + """Encapsulates a nice sequence of colors. + + Shades of blue that look good in color and can be distinguished + in grayscale (up to a point). + + Borrowed from http://colorbrewer2.org/ + """ + color_iter = None + + colors = ['#081D58', + '#253494', + '#225EA8', + '#1D91C0', + '#41B6C4', + '#7FCDBB', + '#C7E9B4', + '#EDF8B1', + '#FFFFD9'] + + # lists that indicate which colors to use depending on how many are used + which_colors = [[], + [1], + [1, 3], + [0, 2, 4], + [0, 2, 4, 6], + [0, 2, 3, 5, 6], + [0, 2, 3, 4, 5, 6], + [0, 1, 2, 3, 4, 5, 6], + ] + + @classmethod + def Colors(cls): + """Returns the list of colors. + """ + return cls.colors + + @classmethod + def ColorGenerator(cls, n): + """Returns an iterator of color strings. + + n: how many colors will be used + """ + for i in cls.which_colors[n]: + yield cls.colors[i] + raise StopIteration('Ran out of colors in Brewer.ColorGenerator') + + @classmethod + def InitializeIter(cls, num): + """Initializes the color iterator with the given number of colors.""" + cls.color_iter = cls.ColorGenerator(num) + + @classmethod + def ClearIter(cls): + """Sets the color iterator to None.""" + cls.color_iter = None + + @classmethod + def GetIter(cls): + """Gets the color iterator.""" + return cls.color_iter + + +def PrePlot(num=None, rows=1, cols=1): + """Takes hints about what's coming. + + num: number of lines that will be plotted + """ + if num: + Brewer.InitializeIter(num) + + # TODO: get sharey and sharex working. probably means switching + # to subplots instead of subplot. + # also, get rid of the gray background. + + if rows > 1 or cols > 1: + pyplot.subplots(rows, cols, sharey=True) + global SUBPLOT_ROWS, SUBPLOT_COLS + SUBPLOT_ROWS = rows + SUBPLOT_COLS = cols + + +def SubPlot(rows, cols, plot_number): + """Configures the number of subplots and changes the current plot. + + rows: int + cols: int + plot_number: int + """ + pyplot.subplot(rows, cols, plot_number) + + +class InfiniteList(list): + """A list that returns the same value for all indices.""" + def __init__(self, val): + """Initializes the list. + + val: value to be stored + """ + list.__init__(self) + self.val = val + + def __getitem__(self, index): + """Gets the item with the given index. + + index: int + + returns: the stored value + """ + return self.val + + +def Underride(d, **options): + """Add key-value pairs to d only if key is not in d. + + If d is None, create a new dictionary. + + d: dictionary + options: keyword args to add to d + """ + if d is None: + d = {} + + for key, val in options.iteritems(): + d.setdefault(key, val) + + return d + + +def Clf(): + """Clears the figure and any hints that have been set.""" + Brewer.ClearIter() + pyplot.clf() + + +def Figure(**options): + """Sets options for the current figure.""" + Underride(options, figsize=(6, 8)) + pyplot.figure(**options) + + +def Plot(xs, ys, style='', **options): + """Plots a line. + + Args: + xs: sequence of x values + ys: sequence of y values + style: style string passed along to pyplot.plot + options: keyword args passed to pyplot.plot + """ + color_iter = Brewer.GetIter() + + if color_iter: + try: + options = Underride(options, color=color_iter.next()) + except StopIteration: + print 'Warning: Brewer ran out of colors.' + Brewer.ClearIter() + + options = Underride(options, linewidth=3, alpha=0.8) + pyplot.plot(xs, ys, style, **options) + + +def Scatter(xs, ys, **options): + """Makes a scatter plot. + + xs: x values + ys: y values + options: options passed to pyplot.scatter + """ + options = Underride(options, color='blue', alpha=0.2, + s=30, edgecolors='none') + pyplot.scatter(xs, ys, **options) + + +def Pmf(pmf, **options): + """Plots a Pmf or Hist as a line. + + Args: + pmf: Hist or Pmf object + options: keyword args passed to pyplot.plot + """ + xs, ps = pmf.Render() + if pmf.name: + options = Underride(options, label=pmf.name) + Plot(xs, ps, **options) + + +def Pmfs(pmfs, **options): + """Plots a sequence of PMFs. + + Options are passed along for all PMFs. If you want different + options for each pmf, make multiple calls to Pmf. + + Args: + pmfs: sequence of PMF objects + options: keyword args passed to pyplot.plot + """ + for pmf in pmfs: + Pmf(pmf, **options) + + +def Hist(hist, **options): + """Plots a Pmf or Hist with a bar plot. + + The default width of the bars is based on the minimum difference + between values in the Hist. If that's too small, you can override + it by providing a width keyword argument, in the same units + as the values. + + Args: + hist: Hist or Pmf object + options: keyword args passed to pyplot.bar + """ + # find the minimum distance between adjacent values + xs, fs = hist.Render() + width = min(Diff(xs)) + + if hist.name: + options = Underride(options, label=hist.name) + + options = Underride(options, + align='center', + linewidth=0, + width=width) + + pyplot.bar(xs, fs, **options) + + +def Hists(hists, **options): + """Plots two histograms as interleaved bar plots. + + Options are passed along for all PMFs. If you want different + options for each pmf, make multiple calls to Pmf. + + Args: + hists: list of two Hist or Pmf objects + options: keyword args passed to pyplot.plot + """ + for hist in hists: + Hist(hist, **options) + + +def Diff(t): + """Compute the differences between adjacent elements in a sequence. + + Args: + t: sequence of number + + Returns: + sequence of differences (length one less than t) + """ + diffs = [t[i+1] - t[i] for i in range(len(t)-1)] + return diffs + + +def Cdf(cdf, complement=False, transform=None, **options): + """Plots a CDF as a line. + + Args: + cdf: Cdf object + complement: boolean, whether to plot the complementary CDF + transform: string, one of 'exponential', 'pareto', 'weibull', 'gumbel' + options: keyword args passed to pyplot.plot + + Returns: + dictionary with the scale options that should be passed to + Config, Show or Save. + """ + xs, ps = cdf.Render() + scale = dict(xscale='linear', yscale='linear') + + for s in ['xscale', 'yscale']: + if s in options: + scale[s] = options.pop(s) + + if transform == 'exponential': + complement = True + scale['yscale'] = 'log' + + if transform == 'pareto': + complement = True + scale['yscale'] = 'log' + scale['xscale'] = 'log' + + if complement: + ps = [1.0-p for p in ps] + + if transform == 'weibull': + xs.pop() + ps.pop() + ps = [-math.log(1.0-p) for p in ps] + scale['xscale'] = 'log' + scale['yscale'] = 'log' + + if transform == 'gumbel': + xs.pop(0) + ps.pop(0) + ps = [-math.log(p) for p in ps] + scale['yscale'] = 'log' + + if cdf.name: + options = Underride(options, label=cdf.name) + + Plot(xs, ps, **options) + return scale + + +def Cdfs(cdfs, complement=False, transform=None, **options): + """Plots a sequence of CDFs. + + cdfs: sequence of CDF objects + complement: boolean, whether to plot the complementary CDF + transform: string, one of 'exponential', 'pareto', 'weibull', 'gumbel' + options: keyword args passed to pyplot.plot + """ + for cdf in cdfs: + Cdf(cdf, complement, transform, **options) + + +def Contour(obj, pcolor=False, contour=True, imshow=False, **options): + """Makes a contour plot. + + d: map from (x, y) to z, or object that provides GetDict + pcolor: boolean, whether to make a pseudocolor plot + contour: boolean, whether to make a contour plot + imshow: boolean, whether to use pyplot.imshow + options: keyword args passed to pyplot.pcolor and/or pyplot.contour + """ + try: + d = obj.GetDict() + except AttributeError: + d = obj + + Underride(options, linewidth=3, cmap=matplotlib.cm.Blues) + + xs, ys = zip(*d.iterkeys()) + xs = sorted(set(xs)) + ys = sorted(set(ys)) + + X, Y = np.meshgrid(xs, ys) + func = lambda x, y: d.get((x, y), 0) + func = np.vectorize(func) + Z = func(X, Y) + + x_formatter = matplotlib.ticker.ScalarFormatter(useOffset=False) + axes = pyplot.gca() + axes.xaxis.set_major_formatter(x_formatter) + + if pcolor: + pyplot.pcolormesh(X, Y, Z, **options) + if contour: + cs = pyplot.contour(X, Y, Z, **options) + pyplot.clabel(cs, inline=1, fontsize=10) + if imshow: + extent = xs[0], xs[-1], ys[0], ys[-1] + pyplot.imshow(Z, extent=extent, **options) + + +def Pcolor(xs, ys, zs, pcolor=True, contour=False, **options): + """Makes a pseudocolor plot. + + xs: + ys: + zs: + pcolor: boolean, whether to make a pseudocolor plot + contour: boolean, whether to make a contour plot + options: keyword args passed to pyplot.pcolor and/or pyplot.contour + """ + Underride(options, linewidth=3, cmap=matplotlib.cm.Blues) + + X, Y = np.meshgrid(xs, ys) + Z = zs + + x_formatter = matplotlib.ticker.ScalarFormatter(useOffset=False) + axes = pyplot.gca() + axes.xaxis.set_major_formatter(x_formatter) + + if pcolor: + pyplot.pcolormesh(X, Y, Z, **options) + + if contour: + cs = pyplot.contour(X, Y, Z, **options) + pyplot.clabel(cs, inline=1, fontsize=10) + + +def Config(**options): + """Configures the plot. + + Pulls options out of the option dictionary and passes them to + the corresponding pyplot functions. + """ + names = ['title', 'xlabel', 'ylabel', 'xscale', 'yscale', + 'xticks', 'yticks', 'axis'] + + for name in names: + if name in options: + getattr(pyplot, name)(options[name]) + + loc = options.get('loc', 0) + legend = options.get('legend', True) + if legend: + pyplot.legend(loc=loc) + + +def Show(**options): + """Shows the plot. + + For options, see Config. + + options: keyword args used to invoke various pyplot functions + """ + # TODO: figure out how to show more than one plot + Config(**options) + pyplot.show() + + +def Save(root=None, formats=None, **options): + """Saves the plot in the given formats. + + For options, see Config. + + Args: + root: string filename root + formats: list of string formats + options: keyword args used to invoke various pyplot functions + """ + Config(**options) + + if formats is None: + formats = ['pdf', 'eps'] + + if root: + for fmt in formats: + SaveFormat(root, fmt) + Clf() + + +def SaveFormat(root, fmt='eps'): + """Writes the current figure to a file in the given format. + + Args: + root: string filename root + fmt: string format + """ + filename = '%s.%s' % (root, fmt) + print 'Writing', filename + pyplot.savefig(filename, format=fmt, dpi=300) + + +# provide aliases for calling functons with lower-case names +preplot = PrePlot +subplot = SubPlot +clf = Clf +figure = Figure +plot = Plot +scatter = Scatter +pmf = Pmf +pmfs = Pmfs +hist = Hist +hists = Hists +diff = Diff +cdf = Cdf +cdfs = Cdfs +contour = Contour +pcolor = Pcolor +config = Config +show = Show +save = Save + + +def main(): + color_iter = Brewer.ColorGenerator(7) + for color in color_iter: + print color + +if __name__ == '__main__': + main()