From 28d4b02660f3f5c682538acaf4768218d9a8b40a Mon Sep 17 00:00:00 2001 From: HadoopMarc Date: Mon, 21 May 2018 14:03:54 +0200 Subject: [PATCH 1/2] Merged vtslab recipe for connected components --- .../src/recipes/connected-components.asciidoc | 123 ++++++++++++------ 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/docs/src/recipes/connected-components.asciidoc b/docs/src/recipes/connected-components.asciidoc index 70abdbd1c4c..edbeec5735c 100644 --- a/docs/src/recipes/connected-components.asciidoc +++ b/docs/src/recipes/connected-components.asciidoc @@ -14,17 +14,31 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. //// + +// @author Daniel Kuppitz (anwer on gremlin user list) +// @author Robert Dale (answer on gremlin user list) +// @author Marc de Lignie + [[connected-components]] == Connected Components Gremlin can be used to find link:https://en.wikipedia.org/wiki/Connected_component_(graph_theory)[connected components] -in a graph. As of TinkerPop 3.4.0, the process has been simplified to the `connectedComponent()`-step which is -described in more detail in the link: -link:http://tinkerpop.apache.org/docs/x.y.z/reference/#connectedcomponent-step[Reference Documentation]. +in a graph. In a directed graph like in TinkerPop, components can be weakly or strongly connected. This recipe is +restricted to finding link:https://en.wikipedia.org/wiki/Directed_graph#Directed_graph_connectivity[weakly +connected components], in which the direction of edges is not taken into account. + +Depending on the size of the graph, three solution regimes can be discriminated: + +1. Small graphs that fit in the memory of a single machine + +2. Medium graphs backed by storage for which a linear scan is still feasible. This regime is left to third party +TinkerPop implementations, since TinkerPop itself has no storage-backed reference implementations. The idea is that +component membership is stored in the graph, rather than in memory. -The `connectedComponent()`-step replaces the original recipe described below from earlier versions of TinkerPop, -however the algorithm from that old recipe remains interesting for educational purposes and has thus not been removed. -The original recipe considers the following graph which has three connected components: +3. Large graphs requiring an OLAP approach to yield results in a reasonable time. + + +These regimes are discussed separately using the following graph with three weakly connected components: image:connected-components.png[width=600] @@ -41,46 +55,75 @@ g.addV().property(id, "A").as("a"). addE("link").from("d").to("e").iterate() ---- -One way to detect the various subgraphs would be to do something like this: + +===== Small graphs + +Connected components in a small graph can be determined with both an OLTP traversal and the OLAP +`connectedComponent()`-step. The `connectedComponent()`-step is available as of TinkerPop 3.4.0 and is +described in more detail in the +link:http://tinkerpop.apache.org/docs/x.y.z/reference/#connectedcomponent-step[Reference Documentation]. + +A straightforward way to detect the various subgraphs with an OLTP traversal is to do this: [gremlin-groovy,existing] ---- -g.V().emit(cyclicPath().or().not(both())).repeat(both()).until(cyclicPath()). <1> - path().aggregate("p"). <2> - unfold().dedup(). <3> - map(__.as("v").select("p").unfold(). <4> - filter(unfold().where(eq("v"))). - unfold().dedup().order().by(id).fold()). - dedup() <5> +g.V().emit(cyclicPath().or().not(both())). <1> + repeat(__.where(without('a')).store('a').both()).until(cyclicPath()). <2> + group().by(path().unfold().limit(1)). <3> + by(path().unfold().dedup().fold()). <4> + select(values).unfold() <5> ---- -<1> Iterate all vertices and repeatedly traverse over both incoming and outgoing edges (TinkerPop doesn't support -unidirectional graphs directly so it must be simulated by ignoring the direction with `both`). Note the use of `emit` -prior to `repeat` as this allows for return of a single length path. -<2> Aggregate the `path()` of the emitted vertices to "p". It is within these paths that the list of connected -components will be identified. Obviously the paths list are duplicative in the sense that they contains different -paths traveled over the same vertices. -<3> Unroll the elements in the path list with `unfold` and `dedup`. -<4> Use the first vertex in each path to filter against the paths stored in "p". When a path is found that has the -vertex in it, dedup the vertices in the path, order it by the identifier. Each path output from this `map` step -represents a connected component. -<5> The connected component list is duplicative given the nature of the paths in "p", but now that the vertices within -the paths are ordered, a final `dedup` will make the list of connective components unique. - -NOTE: This is a nice example of where running smaller pieces of a large Gremlin statement make it easier to see what -is happening at each step. Consider running this example one line at a time (or perhaps even in a step at a time) to -see the output at each point. - -While the above approach returns results nicely, the traversal doesn't appear to work with OLAP. A less efficient -approach, but one more suited for OLAP execution looks quite similar but does not use `dedup` as heavily (thus -`GraphComputer` is forced to analyze far more paths): +<1> The initial emit() step allows for output of isolated vertices, in addition to the discovery of +components as described in (2). + +<2> The entire component to which the first returned vertex belongs, is visited. To allow for components of any +structure, a repeat loop is applied that only stops for a particular branch of the component when it detects a cyclic +path. Collection `'a'` is used to keep track of visited vertices, for both subtraversals within a component +and new traversals resulting from the `g.V()` linear scan. + +<3> While `'a'` nicely keeps track of vertices already visited, the actual components need to be extracted from the +path information of surviving traversers. The `path().unfold().limit(1)` closure provides the starting vertex +of surviving traversers, which can be used to group the components. + +<4> This clause collects the unique vertices from all paths with the same starting vertex, thus from the same +weak component. + +<5> The values of the groupby map contain the lists of vertices making up the requested components. + +This algorithm completes in linear time with the number of vertices and edges, because a traversal is started for each +vertex and each edge with its associated out-vertex is visited exactly once. + + +==== Large graphs + +Large graphs require an OLAP solution with a custom VertexProgram that can be run using a graph implementation's +GraphComputer, in particular `SparkGraphComputer` on a `HadoopGraph`. The OLAP solution also runs faster for most +medium-sized graphs, that is when these graph have a 'natural' structure with a limited maximum path length. + +The TinkerPop library of vertex programs contains the `WeakComponentsVertexProgram` which can be run in the same +way as the link:http://tinkerpop.apache.org/docs/x.y.z/reference/#peerpressurevertexprogram[PeerPressureVertexProgram]: [gremlin-groovy,existing] ---- -g.withComputer().V().emit(cyclicPath().or().not(both())).repeat(both()).until(cyclicPath()). - aggregate("p").by(path()).cap("p").unfold().limit(local, 1). - map(__.as("v").select("p").unfold(). - filter(unfold().where(eq("v"))). - unfold().dedup().order().by(id).fold() - ).toSet() +result = g.getGraph().compute(). + program(WeakComponentsVertexProgram.build().maxIterations(100).create()). + mapReduce(ClusterPopulationMapReduce.build().create()). + mapReduce(ClusterCountMapReduce.build().create()). + submit().get() +result.memory().clusterPopulation +gResult = result.graph().traversal() +gResult.V().valueMap(true) ---- + +The vertex program has interconnected vertices exchange id's and store the lowest id until no vertex receives a +lower id. This algorithm is commonly applied in +link:https://en.wikipedia.org/wiki/Bulk_synchronous_parallel[bulk synchronous parallel] systems, e.g. in +link:https://spark.apache.org/graphx[Apache Spark GraphX]. + +==== Scalability + +ToDo: + - limits and run time regime 1 + - test of friendster graph regime 3 + - discuss: link:http://www.vldb.org/pvldb/vol7/p1821-yan.pdf[http://www.vldb.org/pvldb/vol7/p1821-yan.pdf] \ No newline at end of file From b087822708707013f7f0cd3b5abaf6d0f574a72e Mon Sep 17 00:00:00 2001 From: HadoopMarc Date: Sun, 10 Jun 2018 15:17:17 +0200 Subject: [PATCH 2/2] Extended the connected-components recipe --- .../src/recipes/connected-components.asciidoc | 94 +++++++++++------- docs/static/images/cc-scale-ratio.png | Bin 0 -> 14393 bytes docs/static/images/cc-scale-size.png | Bin 0 -> 12220 bytes 3 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 docs/static/images/cc-scale-ratio.png create mode 100644 docs/static/images/cc-scale-size.png diff --git a/docs/src/recipes/connected-components.asciidoc b/docs/src/recipes/connected-components.asciidoc index edbeec5735c..850c31f1cde 100644 --- a/docs/src/recipes/connected-components.asciidoc +++ b/docs/src/recipes/connected-components.asciidoc @@ -31,11 +31,11 @@ Depending on the size of the graph, three solution regimes can be discriminated: 1. Small graphs that fit in the memory of a single machine -2. Medium graphs backed by storage for which a linear scan is still feasible. This regime is left to third party +2. Medium-sized graphs backed by storage for which an OLTP linear scan is still feasible. This regime is left to third party TinkerPop implementations, since TinkerPop itself has no storage-backed reference implementations. The idea is that component membership is stored in the graph, rather than in memory. -3. Large graphs requiring an OLAP approach to yield results in a reasonable time. +3. Large graphs requiring an approach with `HadoopGraph` and `SparkGraphComputer` to yield results in a reasonable time. These regimes are discussed separately using the following graph with three weakly connected components: @@ -55,16 +55,21 @@ g.addV().property(id, "A").as("a"). addE("link").from("d").to("e").iterate() ---- +==== Small graph traversals -===== Small graphs - -Connected components in a small graph can be determined with both an OLTP traversal and the OLAP +Connected components in a small graph can be determined with either an OLTP traversal or the OLAP `connectedComponent()`-step. The `connectedComponent()`-step is available as of TinkerPop 3.4.0 and is described in more detail in the link:http://tinkerpop.apache.org/docs/x.y.z/reference/#connectedcomponent-step[Reference Documentation]. +The traversal looks like: +[gremlin-groovy,existing] +---- +g.withComputer().V().connectedComponent(). + group().by('gremlin.connectedComponentVertexProgram.component'). + select(values).unfold() +---- A straightforward way to detect the various subgraphs with an OLTP traversal is to do this: - [gremlin-groovy,existing] ---- g.V().emit(cyclicPath().or().not(both())). <1> @@ -73,7 +78,6 @@ g.V().emit(cyclicPath().or().not(both())). <1 by(path().unfold().dedup().fold()). <4> select(values).unfold() <5> ---- - <1> The initial emit() step allows for output of isolated vertices, in addition to the discovery of components as described in (2). @@ -83,7 +87,7 @@ path. Collection `'a'` is used to keep track of visited vertices, for both subt and new traversals resulting from the `g.V()` linear scan. <3> While `'a'` nicely keeps track of vertices already visited, the actual components need to be extracted from the -path information of surviving traversers. The `path().unfold().limit(1)` closure provides the starting vertex +path information. The `path().unfold().limit(1)` closure provides the starting vertex of surviving traversers, which can be used to group the components. <4> This clause collects the unique vertices from all paths with the same starting vertex, thus from the same @@ -91,39 +95,57 @@ weak component. <5> The values of the groupby map contain the lists of vertices making up the requested components. -This algorithm completes in linear time with the number of vertices and edges, because a traversal is started for each -vertex and each edge with its associated out-vertex is visited exactly once. - -==== Large graphs -Large graphs require an OLAP solution with a custom VertexProgram that can be run using a graph implementation's -GraphComputer, in particular `SparkGraphComputer` on a `HadoopGraph`. The OLAP solution also runs faster for most -medium-sized graphs, that is when these graph have a 'natural' structure with a limited maximum path length. +==== Small graph scalability -The TinkerPop library of vertex programs contains the `WeakComponentsVertexProgram` which can be run in the same -way as the link:http://tinkerpop.apache.org/docs/x.y.z/reference/#peerpressurevertexprogram[PeerPressureVertexProgram]: +The scalability of the OLTP traversal and the `connectedComponent()`-step for in-memory graphs is shown in the figures +below. -[gremlin-groovy,existing] ----- -result = g.getGraph().compute(). - program(WeakComponentsVertexProgram.build().maxIterations(100).create()). - mapReduce(ClusterPopulationMapReduce.build().create()). - mapReduce(ClusterCountMapReduce.build().create()). - submit().get() -result.memory().clusterPopulation -gResult = result.graph().traversal() -gResult.V().valueMap(true) ----- +[[cc-scale-size]] +.Run times for finding connected components in a randomly generated graph with 10 components of equal size and with an edge/vertex ratio of 6 +image::cc-scale-size.png[width=600, side=bottom] -The vertex program has interconnected vertices exchange id's and store the lowest id until no vertex receives a -lower id. This algorithm is commonly applied in +In general, the `connectedComponent()`-step is almost a factor two faster than the OLTP traversal. Only, for very +small graphs the overhead of running the ConnectedComponentVertexProgram is larger than that of the OLTP traversal. +The vertex program works by having interconnected vertices exchange id's and store the lowest id until no vertex +receives a lower id. This algorithm is commonly applied in link:https://en.wikipedia.org/wiki/Bulk_synchronous_parallel[bulk synchronous parallel] systems, e.g. in -link:https://spark.apache.org/graphx[Apache Spark GraphX]. +link:https://spark.apache.org/graphx[Apache Spark GraphX]. Overhead for the vertex program arises because it has to run +as many cycles as the largest length of the shortest paths between any two vertices in a component of the graph. In +every cycle each vertex has to be checked for being +"halted". Overhead of the OLTP traversal consists of each traverser having to carry complete path information. For +pure depth-first-search or breadth-first-search implementations, connected-component algotithms should scale +as [.big]##O##(V+E). For the traversals in the figure above this is almost the case. + + +[[cc-scale-ratio]] +.Run times for finding connected components in a randomly generated graph with 10 components, each consisting of 6400 vertices +image::cc-scale-ratio.png[width=600] + +The random graphs used for the scalability tests can be modulated with the edge/vertex ratio. For small ratios the +components generated are more lint-like and harder to process by the `connectedComponent()`-step. For high ratios +the components are more mesh-like and the ConnectedComponentVertexProgram needs few cycles to process the graph. These +characteristics show clearly from the graph. Indeed, for a given number of vertices, the run time of the +`connectedComponent()`-step does not depend on the number of edges, but rather on the maximum shortest path length in +the graph. + + +==== Large graphs + +Large graphs in TinkerPop require distributed processing by `SparkGraphComputer` to get results in a reasonable time (OLAP +approach). This means that the graph must be available as `HadoopGraph` (third party TinkerPop implementations often +allow to make a graph available as an `HadoopGraph` by providing an Hadoop `InputFormat`). Running the +`connectedComponent()`-step on +an `HadoopGraph` works the same as for a small graph, provided that `SparkGraphComputer` is specified as the graph computer, +either with the `gremlin.hadoop.defaultGraphComputer` property or as part of the `withComputer()`-step. + +Scalability of the the `connectedComponent()`-step with `SparkGraphComputer` is high, but note that: + +* the graph should fit in the memory of the Spark cluster to allow the VertexProgram to run its cycles without spilling +intermediate results to disk and loosing most of the gains from the distributed processing -==== Scalability +* as discussed for small graphs, the BSP algorithm does not play well with graphs having a large shortest path between +any pair of vertices. Overcoming this limitation is still a +link:http://www.vldb.org/pvldb/vol7/p1821-yan.pdf[subject of academic research]. -ToDo: - - limits and run time regime 1 - - test of friendster graph regime 3 - - discuss: link:http://www.vldb.org/pvldb/vol7/p1821-yan.pdf[http://www.vldb.org/pvldb/vol7/p1821-yan.pdf] \ No newline at end of file diff --git a/docs/static/images/cc-scale-ratio.png b/docs/static/images/cc-scale-ratio.png new file mode 100644 index 0000000000000000000000000000000000000000..33a842d045b6fe302f79dede18f52766136efb4b GIT binary patch literal 14393 zcmd73cUV*3)-Jk05d{GS1nHuJbVTV@kRrWz0tnJe=rt5YR6sy_3B8BjTPUK^dkr1w zy%RzUIq|pm-QWJs-TV9QJ?D>e{@}^;tgOsAnQM-ByzdzA3Q<#$BO;(C004kU{*AN- z0N{cE00(*-7kkCIq4Ev(9?>Uyxp*F2`p>KGAFg}volA&!4X#g^7i++70>HO^Jp>s5yowx>0RU;Y z`v7nsbQ5?bO^E~C8~!(!d}i9K6Zdvb;oih6Pa0R8AFm!*;3l8AMF=cm?yy}c9{Hyz)y`Tg3KD^PQG zo4D(!6mchst9)egTxRzEdcO)}#W5Zp&*qG&BJ3-MWE|80Kv!-)h`gA(6>h27Syz;- zc=K4+zqxR5iYn%*Du~K)sg?cFw*BCU`N`cuyteJZW{Lck^O!h#SsOi5x0&@~Jm3|V z*lw15ZY>&81-9r&zhhW^p^~l6Y0t3J_-$#nd22aCU&qcm0QGxZi?Plr2{j-J|Go;1 z2rk0|0^g{@6P4Wq6lX&HTdk-YZWa@exbQ@rGipDt;|aJL%NXnH&`OGq%_IHT`J_kd zyo#_ZhLq5R5CEdYJ=M(?d;?@qelzhdQf7v%|4|f_U@x^urk~2Rgy- z%a{S^pn-CMxjXt`JQgP+ek(@){Jh{&g|pD`iyf6ny#)Lfrxj6Pre$r)H=V5^2}Yj&Q42Vue;|y+cyO?8O4~*42JUV8_&&Jbnut(aXhl7 z#h$<$*oX9pbJYIoB4!z}P7}{}XP%tyYU~?q`-{H#FhP>~1SKgEp*8)VTm5#I2;YQg zyQR*yMI;L7HnJ=nS^>}?4xlC#8_0zVWbpleJKXNKXjHXtt_3g_;ED;QY2P&e)MO>aYXE_ z#jIso#C6vk$Dl#a$Df3_H>~7-<><$kZIh8Ux|Kt7rI$?)_wNJWfejnV2LcmFls=oG z8w<}6=kc$mx~2=|lTdxcBOVol0QtVdig8|5!?=2hNh?Q#uEn#rT$}1uB5o(>rm2vm zI1*8jGdE=%z)MLLUho1Y^lpm87`-+51}`(yWWj(jlah4AZeEeJn9lwyfx-6Mu^uNf z3scz(}kTE@In2aPY@lIrZYKVhd0Z&@r* z2=$Als2Zsos3+5nE}3h*#cj+#)oWb*ZmszxrA7)|*Uw0_*Oq8^pS}q)dN|cq1jEJR zt93>`s4vdnZ>GNVW$gZLng&A2XFS&qW6XCW|E|q0i$vv6SqutTKfZshqS{lS6qir1 z#=jPsE)ttOmB^;KA|EnH%$y6mx!A4wH7Y8~r9pJTV;6gSXUhq|dx2|rG zpvdOZBeE1+GN8wYk;%LkT6hT-^3YibrbDDcHum3LdU?ZSs9YKOShblZE%{WFqQ9}# zRK~^md8k>&ue%GRrKO$o7e{2()uDN8%VNdg9sRKOT<{V%jxTko<6tSs3iF`6d@LQ5EYI7XSO|noFa1R=vW<1y+Vx*tEUu`k5<+2ym*utrPS3}U^74lQ z3u${Vw(}&#w8gB7=^s$6B-1uLTCqlc&*k!ni@14%&mECRE09~G>u zXYYyB6gV=LW;wq%7#P(5LrKy+)V;YOTIodL2>NtHxADchp+*}uRqa8lBDr9^24*>~ z+d>9nPOz|~7mUIj93B02`EaFWTx^_`EUs<*8L{0r|mu`9k@bYHL2#c>lN;q+mZBjp@Oyk%mhu4#b9R;L89(bNZuX;a+0d|2C~-CAN#V$Tx=6L} zu`VOHv5XdO*-y^x`-Ib;=+7K9ASEF<>ixbd<*o4=?+YHy5Ey#f;Z~phvczUz^0)AH zjV1$qySH`#&?G#)k>$Rve{7gL*GgrzXEY$~mz?Obi{ccyRw)}9xh%q9>GOa&vL}Jj+pzLisUj&Kh=8`^gFP62aC4`u;pwG@22lymK z)2|=`U-~JgJVBpsj8J+bf=1@!i%nb|yJ84PFb}6pPf(N>eFYAM!QlX)cDH(@-R>DZ z9|NKxI^@y2`730;Z+O0Y@uLS%D?iO`J>_amX2U+Iz|5f@^AuTT(qO6Fmdqy4=Q|2~ z3mq1fCV|;~eOmHfmydz(f!5h!@mvmckpm`ZT{4ODXrX{{>Xsg%ZQ}=ZCdTLVPU~3{WWXSTsd*!Rh+L_8MW|Ayi;O=J~dbzl+ zWWTeSW|W2oo-1L@4!#PL1vQbOyP3`9#i|#(#NXy6AyK+KQ@~K&ur&-*Wlk2X+g!Oi ztKAx&LNb8894?+Ul`#Mr>(#?Dgox8hRd~JsCZFw(@_9N@qn$KSx8(btha2;p3mgX5 zzEc@PH53Mu%in0dGGsYjIw~#_-*;VXGUqSwvZQAf;&|3vFQ<`j*&?ZV%CV{5 z89zvb4{S&{_)&yw>A=^k-No`t;}WHfxyL~Q>$uuPWJk- zvfo*&zIZ@}*TGaS7NY*7n8yxoJjd6t?79>vqN*2V++H0<6Hn*x!Y42+iGm*&c*Qd$ zOUXc&GUPY)tJk;L0grK$5;L(ta_#EpOC$M4tkvg~t%wmDET+$^(Sn69kk`AHFa^#9 zb^vG(TK`A-GT6#`!F1Yz+;)-t*2!$eygsk8h@<@q+{KVy8gOPG$2JCR#S*kyo^^hQm$5yw23LbI(`azeseCB7}l_$n#$}oY~bD}An#@(Zy8te(BXy5U>DKBzrvahvZ$Cqg7Tge9%Ip zKR~@8V8G!x-rNcanslD){*2|pDss6L7Jb2EIa+b1&~Z@<1*yWZ9vz+2Daj=LG!|J| z*`C!S$P57q31V#w2V^*1wzh$!Bu2Ytd%NqKn!oh_@&1GL?zv2e&H1Of&LWI}#QF51 z!#2WS$aL?(Euroir-`VvV)~7I1-L8QRx(I4vS*6qJ=@FCI9W?(Z(~=-o!`ctTNjH( ztT$*m$HRID<>owX)qC}Sn!1OHqP90Tlfa5wCRFqD^9D)084}aAVXX<@ip4F)kkY0P z6?P@ksxgf;?b*NT>+wmJR#^$`Ub@#+pE~6}-QA&V0jc2a&b^$gvR{f?effi&Zi2s3 zgIW4>aC`mvsat}+0MQm~OUN8qQ2q$9(uKz;SA@^yUQv|u_M2{Y7I~q1Me#*IV;Ww@ ze)WWm#Zk@cw|2T^#B(lBn;jzqB^fYR4u-ljxF*6EqRwx3Q+?3M@qU-md@;@+etust zZ!!7bBgR;WL8j-v#vhz<&4=_lqGcULDTnK`rWDhk#vh+EY5!z5`hPKIj}J`$nJ{C4 z-iGogzcfhq4#Lyl+iX2S)$e<_`V1Y3|E|tQluc7$tey;_n{LO;n-$BZZ8nb`M{3aC z0^Hv#YfZiv&EEntVuxgF$PY2JLUj2!nJh#;9$q~LGJxX% z(ThDqtNhasy}!!_N~n#e7#Hqnx||f(_OIWTWcM`MnJUOqpG}%GQF*vDb+waLz_i=5 z3g?zTzYbu#u$qhB_bHMJ{J9tKTCBvSCGMoi^xdc{SVdABCg2Yp?S)^OCWMLjiYNqQ zzWhP{G94B2aRDthM$RXrdAywSo;QI3N&#ygNdl1`HmCX={L)oE8 zxR6lI7eruGqDUQ-e8l^0dVf9x(qqgmqA&`Z6nBQYsxrlb@c_{$0--|W3fexO9&Z;P z!WXsa7$o}6Hpb#vmTL`Xgja-MF_XMLopnpPGbHoSCEb&fqF9BtuCX&2aaJuFzA9TG ziQ-&}J$yiI2~xim33;-`YXE)do_=nA(!)4)qc9d9HEPk1{l8KV)}napyhe=(^I}OS9}9BELWKIXy(yLZ5on04L0cVs4> zn$ia6kg@F1t9&}YoJ~U=`t(xF*s_d@DGtEY7h(}a6K>$n##|d>*Pg=Sm^jV3k2^vj zMloPC!h2BSOl6{sbXa;vjv7p}Ao~pAB)oQiLqxXtR?_oI0ULIC+QKB%#uCf0ij$cD ze*xc3|ICqUT&`Dm0E?Ik7m59Im+=bMx{F#^40q@h$LzwK=MSNGJ+qE{x7zavWmjjX zPob`+qu-cog-G77dAso{?oL@@lD8}`6!HqgxstW)$Fn{6bPHqI;cNJ+Ni^Dg*67V7 zzqxeuJzC(^CRy|2=ZkM`sq9?)7vl90dlLKk*T=k&-n_ZBr{U{*m31CW5?yugK6Dg` zGy^5#tpz?_j@!k~ZHj{k=$Xt($6gJG$x4z6Rs-mok|RTE8w->~Yy8&|?VBuifin_#b$$vg- zDO>1jG8&f!6Qv67^edFD?J{B6IjMm+Cd>6RP7rAH_$!OXUfPsctK!cEgf!;&`U!4A z5VnGuUw!H=F!(isXlp3{8h;^mpQo9B{VM?6INVC3j1{ilTPD4ni_?0Ktz$L`fW*9? zSs-Ty=!)#l)b?W)p;lipUj6%&%P+~02PdWeMG0PE2i0O+=AdxUXOc_t7{| zLiCtGfg#@8bQ&sld+7rX3B7FiI0o35$RL#uugfK(j6O87tgx#I< z(mJ-Af;-m&W5Nt{*YkiPLuMW(QH%|17R#k6zVrp#Xvz2Rh21)pe z#yqA6^YL?ZIZ$Q^i6d{!_GTS5-mC}v@u&Nb{Tbh10js0#1{5hpE*wYpY~evDTE1&` z;QzpR#TqXVYReYa_%+(mu+)AZJv~$NE>f<9n)o#S^uVvztmpPZgfZuqPHkQoD6veP+II1A^3 z*hzpKKi8%0auy>4o{?(VWFL6!tI)njiLK5gXdQkQ`^Y&RJs5J8kgkzCcWpi_cgfM{ zScF;Nx>X#`rJ&@nOUE0a2vbuCuXdup3p`gcPoCr!_Tk=#X|7ZA`{$8=xE~^K!DD zx@4rmVC5kf%gSpB0e1Nbi-!Tmg5GQ0ZLRjmwPSUhjB10SmMp1U15$U1XW&d%MRzwTwW{r}6rp)hP!EJ;=9T(aLcxl8QbT#qzu($H_zoVLC-Kv3s+v|#W<;X@0L3h}j((xXBJ zI%cjb_F}M~A^mGS;Pw^;8{S*==0~gS0?RGZCcDVzn?c`w7>wfBnPhe1s6D6}-S@{D zgT(Rt>g+5DXd&XDA zqn-CO7v>@X)n3Lu%K7yJLHhW~lhvcE^yh!o;!yZL8wmx9q#=!8^y|XIpIUH+uY<3$ zLs#NU*YPsW9;#9PlkSM){Y!T!STeIha18aQu!H^8Qj@2X0fYC=CYd&%$w&s?+ZSA# znY_^oG>Af{tEJs9Lf6V5wm+JSICgsU#*MA4iXdOQqV3ORd3T+v$#2G8LB{;@DaJX| zs$OxfW1Ey;X4z(B%4oAxvSA@n!lC$-Bu_(M*_gk=c0QrEkXp!;3Lh=KZ+0##AHHRe z-$-aoWF^b``NM1$;@jBE%Vnx7KWfl8Pq|T&!s}nYg@U0CNM$m}M-W^c!8M*+NIQem zU9xedjyuFR0wV7qQ~uj}-QT-%1UV|^lYPcmr2)(hA-Mjc_oGb0$v45F#QwIw#@v5# zq+18~v1}8D>`dYDlKiRgwRjxj<@{o6RM|7_d^U+KJ^f$Uo6Fd^jAv^yw=+>rgHIZO*dhEx4S zTs!G+9k@4Sv1MgVPVv0d;*DRH%J)~|R5S&svGBCWScu(VG?KRkIQ~BBDB%({rs`qks+|m9I-ORN-Kl&1lRn&A-)DY*W%x zgb{giG%#$~Jzw)~22!-;BRhoK_(ivO`p4L$UUCvl%d)P*d>L_?T3@&=k7JOnVvc9G zymEK7KJOG8($zs+OXJZB!OgB0wZ7mWt#58!5ldeKby7I8o>EO`q?G0Z4%l_9Hf?F2 zBag4Qc=yDd-7Q}gCEYOC^pOSB9$_~kLuFZFfmnCJ$$8CW0yv=tN%LfW0q*|kh5b)n zgD6RN9vfAX&H)h(msSs`xA4Ui&ni(x7BXV)+jmpw?#Ec_x0I^B*%)baj3*5bYM1Z@ z0&$)faQ$;x$EJuBg@4(nOejPv(Z?ia>+w0FC0#&-2MbzO`01srYDcVEY^XA>99Av( zg^p|0@;U>{H%52$!vo`J(_K?@tsz{JY@JWgvweatVY=#*hhjfnB@ZrKvuA7{LIPC$ zj+at|Gc>nY8cPKOgv3E?_qPIE7ssAcj@%~y^{p{6iTsj<(=r#F>in$qsUh5*$hgVA zGND?5^0o9-nN(|2|^JLzpFE*tZPn?_*TiySOP zKQkI> zvI2L6+mp~&l6MQjqN}S+&OC%3RC^A(4OWK8#Sh-rbkm#ZQb@W-M?tZ?HZuycoZ~IX zFI@jRp8ey4QOpy{kF+K9^<(X&(VryKbR})FRkEYBzFeV(j|SqtJXX7n#pOrG2LBu7 z5QWrCq8{hmSHBzX!oogOf0aAv&}^egr{;C1ZR#+Fj{2um(3w) zE6b9#BD6F{Wb*J1KMPF{Sx8fD6TifXr=9cXD<==Tr4h zM`&~%cevZ=I&We25|0zDms<|sBGPG>wok8g9n z=4SB8zh!e%fyVDjyA%WQZ;B_A;R|l$XOo{n@>t2%Z+PfXcXKG`l7?ugt>#p`&bGZ& zG3Hsb>A+o)R_|)L6|5mc{eI*N7e%lqClsyAVEI|r+NfT_a%kG)8YaB8k@@!=C0r(Oq)6Qt*AYsehOTY523+>aQYrlwCyS&gvAae-DW=56o`JANIh&>A(MH^y~>%z{5T@FKsDIBC2 z$82mG{j>xtjoek0!xl1MRb++9kVm7pT_)Nl^B!;#lN7?XH$^`ShmW)0)mX6J(U8=2 zRV%y+n-5@7Zxyx}!)WIp@+HB9LluR!splqD5HYxF#T#Z=FGYcP^K<@h`CD#!(lzWM zRAps?R&O7R36{$`;JeZdtTeccoLz(KLLRFL54R_nv8az>A15e2y;rO7MQrG;1>>*u z28x)`L`^v$_a5k1QCw_hUr5$Bv|6F`W8SO1C5&wS1Y7TT3$XFz58`)7Cm&Np(Q>A8 z5*w+uTs{xt!W~j|tEfJEyyc&Iw6<$f#dWP*1}Pdj011q_+k@JV4#H#S9W4yBYuT=C zfbRj4l#0PPBBf;*lP2kV%>15-3#Dc}V6k=T!!2I=#;B3ragP|)II}bWxbk$XSn2(< z7Ah{n$XU&WA$e-LlCNYM4{+SiuY>@faISbKRP+O^OXaQEQ|)|p5UYSlm04OhR)@7W z$p9?4jRl^C5qiE7;;f>-*834P2NADnG(-COHq<|9WNPUrzcHy35TS$GzAZ+9%kcra z%&?1g@3Um6F1Qd}oqF$LCd<=UJbbNVq&M5hjzc0nI#VnCNny5^n^S*bZ4(7Kz?&WB z^Hd;IKUTpZcX)n8FSTRZ1XNF(UK(L`LoO^Kl?8}3a;r$R7lP__&KDmHdN;VEOV5HU zDl#;PK!=^HOn%1x+grRHDh22x)D3`%9>hW1TxWcFXp|*=PYSNhwSt!f_B@@Wa(=!Y zUr`YpXQ42XjrTsST|f~4KpeM()X6Ism=6aAitXkpVms)T#o&Y=gu}6=>@5~YD9uo| zq;qQ~E}$UPcxIbg9}`U`!^opjUd~%v{_tuPnO0gmk(VstEcz_GRW{WGRVtuLx)*h81TL)BdBIfS)v_ ziGqSbUaU1Tyjn?!=6HefuJucs)C#R+y7rj>uDfa^6zifm0EQ04@z|Z5B)$K_KG~eoQGkKc z;0?9mxU)`v#T=YQ#keUuBI`3$lShRNI568VQ$I;OH&-DKE*`Yggbm_~sG#5Or?lX4 zmZ_LSCZ&-?ipI`Z91J%Zl~HdW)?tfc(Pl$B_!<%_zKnsQVhBuvAL9l}{!VaL=b##A zJUT&+>YDZHCG~Lh?>);SKN7@>ufU=9$DzjeCrwuhTg~?@**;H#CeowtOb;BESxLkF_|FxBM|NU{lS1u$7srf3TImtVIETpel#X}qM-v{2AUR=D>x znB$SE0>>ZMh4ig4nSXK(F)9D$8j4PmCouW0qf>iO^RcVzO@mPy*nDwuvArC7(*3Zy zh4-Ix{D+L_^vK5jv8={+SBiqwtCX6Xs;Yw!Lk+p%nHjj?DabQd9ojiAacbB2sBk;z1{lYgHNetDBe4CYA6b8BM^czb)BvI;E8e zr+I%ALE(LR%eWspBmsaI%OMF!^<#c354Wj?CpAVSu9B5}qk2#sxc1k##ay|U)6JdHs4MRX(gqVIm;9Tjy+g5otsQCdU)ToYb{?3}k0{vS@AvmIg`{LhV3358$pN6Y`r2vf2p);|r>Z58I%khI=F)akt02>vLY z+s42hei>&x`aLp1d)X6VzjfxJ?HXb_-&LlI6cRCyUV+^TC=dO_wdS=a6G^0Psj-t? zofy~R-jP}#ZNEFg=XF@wZG3v`B0-nU)s9gU<0>|&hTTu<)1s%eg8#FX?=2+?ik(pT za<$;a!Lo%0%t2yd$1v&8>IX$h%c!*$;S4_f?zQOyB2eGqY#2Qa8!S!{aouctONDA= zTHXI$rpdE?oPqTxSE+Z`x$PGPoo+osU(`B-tXLLi#Bxi!TW1sp4tuIhEJuo%etW_8 zz&3`%`zWO+34z*+l3X1;60s>kNU068h;`$WNwuVZNyE-<8%NNn5bi}E@5lsmzzbKhjOq-uKCN4Knit(!zR*U3kl}_;?3sTTS zpI^KL*}SuSJdUic{(K{UF!nG&#csjQ6FOv`?hgBghUdMbEwe)P4n-zt`1{PPRdMO<|Fye21lh$r^Aep$)gf^Bbq@AG7)-3s{4 zyN5twKS)Wq<{_8Kl~q3lRAt$2${Jq`{KjP2%$I@hUV3#;a}Z-7ZfXl`Rc5116mbSO zAypPlMX=nWKgT&UtU+O8H7wUc^e^jQ!M3;cYoT-#4Q=WUBJ}qMSd{Yr-E(@ToVn>l zb#cgM-HIJizL%G#^?KzDdHzXp4cxw-iRJNs^JM&^QkV6=e(5=jvxLty^7s4w(`;BguRIAR+doE_S74KMgeYi zNoO=onE;|=UY_3bqASr>^*ZgHvEfvYU+#j5Z{bZHuX*0FoYk!pFVFVS>-G6kVbQHk zx+pJ*ua8Cne4)y-@`q)q+$H*F#5I3uyv-?d>+Yu9#wPK$;~O<=U@{+ zQ5|nyh3v8&^y1`_OI$qXjdx#5y|8mpj>*5Z?9v(ja*2Bn<3C+~%E@&NxeDXAcv^V2 z&`}4uTwgn$xN3%5JQ%Hi38%QrFlL$2FClTt&-r`)rsM;L8nk0olzCaina_6I{TRDd zh{W-9ZQ0-i-Kt^#(=G2&waB6o^GL{We$_slJ|Uv;6RZ^qv91qGf7csaW}s)uePv#- zR-#Ho*WA231cqO~DExG?mdC?zEn+=WyQZ3zh#^_*)?xNBBJdk!zJ$Us#ZwxVTPH3w zts+z-D@5yWsq4}p8`c9LR)!tAxvZJ8lC`qSg*1w|k2IerAc6&UVMX_!wc6|IumQOw zAn7|#cv!bboR_B4HKDxFGxS+DS>gP}Tib%E*q;lLC01!jbSRNez;#jQhL)V%6ULVB zzTRi3T0%a*X`~96e|eOFD*|dPvOUI$K8gpI@uD-JRZ;h}e_TS~LsLW2gbtLQ_los5S?Ly?c0Jm*a9^a6gg3{t|iroM!i3 zup`G4=96akiP6Y1YRwsr?QLpsgdut~=z@N~B+1PULgX)*LB7;U&lIiSZ;USLn|FS} zGkF~#kynoor4DB?pCuF$QYRbZXzH}EVYuPf>>cGlg9!gys?RHY+H}G2A&Ep02 zny~9?`k@0np7Wi%_EL9hLgC3V%!WB434VbrYI=qKEKvFh^w#i}0(0Z-*D;W_IfK0Y zHUCY~qoMOv&NTfVJstHt1)p!lZ^FZLCk3Z0F*)quvVfxN@~+QY(D;4|dQ5v+pzq6; zT7i+`(JutDq@(=i!c5_Mem>Jbn4<$GN>z8Nv-+2{$a%x_J9=C1R|{T^=EZYYb5AK6 zHeOze1RVL#TDRUxF4X(2qjea?PCQ0^+K{dFxJjt#IySC#et&6O?O-`G^m9cB+l+!m z+J&;IMy&#s_K)Vz3T*=g-}&B0hjRTGvWBQ|@nJNmQ0DXxMYO7UYMofX(N0LoPio_D zz6oCxW8e;rTf^_8zo+&TaCQ1xAFMW; zwA^j2_EkK8#>DG)a7>r}UIp*daMyH2@l)~b!Tc<*j>P+(oNZmDABa@Bv$V3pSfZja zb^bqheKcvhZx%I*ZO>g=&%q+8z4h*B5W)$zeu-AU_brDEf;0`=-o_Dfxr+h;%QclP~K!f%Ge>^D^$H~X9vm0J#f z5iohF*mNSd(WgH1k>Z9up>_LD&A01zC-w&`RBw(OgYe@wpZhgRQq}2EBD`D>(yH$iX1E--@WwdNsfPSaz+bjtxf94J$97w@Vh(; z*lIz%K;4ni9}Q$q$isLsIc0rv9ii7-zq=od$$6A1QIsB{8)YUDn^ zQWe?L4}CGzn-<*Rqi$dZN9%NCGEK$1Jg(~?I`QPE9D3_!7gshbjGX&oRz1(cI|kG5 ziNT{fj>U*PU0k5(Gb)tq4m8E}lQs5*1?ZvO%uWBPRuMd)yWJ$6HvjWq@D9t z^oxFc{)W>wl7tQ=U(j*7<-`jI3cA~#X=nZn2|RTpHwmpGjoaXlJ6qeP9xJdn-VV|j z5YWgSIUz#fc2rSKwennF-dJwMl%Y;-TJu|YVM2$owo3si9ZwcSO_Cd%QMyOIsx6iF z=g4px%%aL;hOgbnaWd$nfX>0bIwWrG$j1H|tLj|buTr_QAbKH1v1WJVJI8r{$V|Ny xoRua*teIKO_8(&Hzket0zcm#9yRk6%KtYzX`id?u_FX_gUPeW_RLb*7qPGc#wigBwA$jl1NBGln`O`F3ONZ^v)>JB}5Rt1<}V~hS7T&B++}D z=%O<^Lm0z5a^HKO``z!k@4D+-@A{VIKWqR0=j?O#KKtym_ivvNbyay9Dkdrb06+s) zc%}&ekU{_el9Ws2#5boCAXefJ`D-QlXMnTwzl?_5SmGN>M+JQs0D$%@@xO^=T?U^^ zd`STXtH@EzkyHI~gZz)lZ5RN+1^_>Ms_ikpG3D+}?|Ry{b7&DOEyEUl`{TRJu-kH1 z;^1tT={25Q@Jq|%^33JVeMiOZ_)c@AEG`g_zoMa%O$KGNpq7H(4aXHpM)yL*%CsEL!vWXQNc{0S5!K4gHz$0edTe@R}h z{l{07_sfR(TYj-=vXLHj;HHE%BgmXkCB&LDj;Vyq_mxDFq_7 zhJ-ZGruO{3^P`V-z)zJj_cn%61O7pX?YSexk*`X?c8STU3cKo7-r6ov!V1lyYEjI5 zGM`3O9|UyRPPXh)ZiP7wIO?~2z!c6H3Lll7*2c1nk60_XuSKH`nja79^th|+g3IuU zlF+~)2mL}Ogp*a=T)CjvOKT$)yP=r$E^z3RMk`FhgySSp2DU@}lm3Swf{NxRWi>-= z=2X7osm66d7#%E^TQmBeX8B-Q@Wrs_HdVqgWQrOC^C@n6Y&=KR%VYp)K?+oxNgu5d zp5jXVZsNAgHq(2Jdy0C;f;@83p`)tcfbu(Xh!-IlS#-6hSni=#a#PbtFG=AtCD8Zc z5E6j%cQc=qJcV*4VQ>tEJKyIDt>$j89^1v}gMiIb%I_S|)H`YVm8FEd^hWL?ohn^T zW&+~P_ktHNVZ^m6u;%!VilLcSP^Vb$k;3MwYec$s_jnEdji>~miS?Bo1py{uz^@eu zlhAyb*+L3z4>MZb+>0ph%XEG#pINP@m+#5sYdX7ew?TC8o|1>S=NPqT#@6~8JVC_x zu!2#fxOU3C0qB0WBSO^fdv=dJ1?ws)fK88%-Fwg>{&s0^qD>@BIZ03*E^;8mk}U7K z+oj0QJwx@mkX9h9Ge>s3LaVC1ir!?ey0(h9s7HguEFVgMx~3jwO@n?^ zN-y*VxAFZEx;R(oUl`M)B$y(o4ry!cY`bXKj^0mK) zNdPa?-}lc`6cJ7K&&}{p)_=ZL{v_F;#CHDyDZN5U+~EIc{3q$(e-QxqpUH#LX38l$ z#}?t6wRr*~=S=)BUlauQDIhX4)NXrRA?{2f1HA*l{dd4j;;vq36sJAzf(R3`GU#$1 zobT-@!_t;oFjZ$asAeI(OGkB=oj&+{T;~ueO`8v;Ee0J^pfkDBF3{Gx4>lrG?7YwA z&{?wsz5u>6%3Pw0SSXtXL8L*utP%f^;kS1F6zLx%n~g|$QUx=k8t7asHthgpPt+*Y z3ZL0AeHg1)YUsJd(%htp6ZGH&iM+?n)aRo)r%cFGTtKb+Btgt~Wbx{^&UM-@0MCgj zrM-Sv$2s<$I5+M25DN+UbPuD-6*Uu#&SLB17s$M=XMWOC{N@o-_b&NhyCz1LJA>1U zOfNk^oBS>05s28SjVOy_A%Xpm^Z8z`ot0yVN_X_U0s#D}e!#iv{)+sYWBkY0e@p(8 z+dOalPvoC=^dI}!b1Pd&a8x8JA=0+Lg-2LiDQUVHkAbMlMPZy^)PSfnQcP5!&>Kdf zRihxocM`S51*G+S$?tD5_?e=AnzNkmpzsz%#AxmDTJXN?j+p2x(_xpgp>rsM7 zv|#+qo_!Tqemf>0HV zkBjq15Qd}4`eU^{1wWe%Ds`g^h%Q6@dSI~I0pp8t6FmuBw5M;QX3r=b7#n)wY8cXu67BJWpTqU0B&YrHI%PTq@z<@>4N zVUcZ(i*{lBV&ky7+!uv6QQS`0Ut8`f+sDUnY2a^zFdrfV6!-BM$+*L?GG{Y_=|5ar zspYQOg(`w$L|+V3V8#Vrxh?q|_{A29D zuwiBbtGyLBgz*r8^eZ+pRk8gnEH2Z5_-1{+dr{XIv~>dys^3w?@^5$Dc737Mpe1b) z+!K4pjQ5EaNo09iQl@tT)*@EefBcnV%^yqsjGFFveP)5{*TVc zk;P!=#NhnTx@AnnX*>niazHF>RF=K=y~FMa7*2WwH+sWu&!((R_GlB$3YihnDim=I z2JY@jD3|h>5deUz_X9tZ>HBMGnOCJ*wvg;~Y8c5q{uaTtD}sJvQx%8WbCni_N2_IM zZp1wSNp~KkI>Q$)IH0}fFs_->1YE1FslF>WU1cwUAU$~eR%;~!+EWJqUO0cP5|mww zAF{%eCfhvn7ndI17B-U`4< z@r88`uSYe5g^{MR*Tgj0+2{@?`qPZdo92A2&-U-&(rUGV4&9#;TI}SVKwV|pydXz7 zRQ05)wiZ9nq#;sya)Gza;})A%?xUM&U=V zY7ba`hH^Ou_^3{Q-(n$^mR$D1mcyu|r-8o%qSwM!aLxnsT}&OHN9rUMgSQ-dnY+X-gJTx zks$h{U5>ifKNv>c)OC1C&izg%l~G}EeNKiSqPuvJQL5k?1BrT`Od^j&Mv;`DjX8tV zse##0Jd5x4%<~npDDi-<^#p>0#NhRJKz|vAT1D&z@|BqN*d`_a1zt}no_erYjYER33{-ye7SYgvZJKd7(NHRx7QoWNXPvSw`kl7`n zHE3Bkm!zqENxGR1f_tE$`R(4s^s3CiM)8)h=DoUM(`2~*a@8^mtD(slBr9PxKiR=; zHLu7GCpP{_6f~=ygSwq^$87Cz#$F}XDqumMD$qic4RBEkx(MPKm$3G%L(_Qdr6$n4 zjy%mC7r!>_YV-__!ZSW^*0 zejVJe=-RE#d()Z|doLv3{Y9=|dsp;8VP5e9gU@!wYjd}J*yk^dugy1p?EClM%O(d< zX{~hXKAEoC|A=vsfGzvt`{#_kU&$;NF{)IXdLkQr)?4Zl)^P0ZUJ!vkj`aBZUAmR5 zxFO(T#oDMMBHJXs;4?#*_H$Q+Eka#i{yOkHi+Jkde0Ho5&^5(Xwn_q!IU@~^$c8Akq53^o7m@a1=DLTta#4?W z_tohLD)?F5DruRqHT( zT7kxTMbh9(539!3`b+bS#9$C{*RF47h@%%e8@ffT*A`ed1r7fyjbQPa+>ro+H&n;5 z@BzyQ2ATHjF1xyA>+v!8rHQuQN>co;@*I@3$^RDp)kQn!3Uh0 zD_${Oc5m3CdBZL--1R$)82gQ$^&meCdYR8Br_NJfE{A*q_r%amzfe>>vI zyU^8N@eGt5imZ%@6js^cwhCcfeX{XV=iG;2LKO7gOMh-9g$-3j(t zb>ol9y*aAg$N7ziZI(nfRE6rNf2dDL?2a5i)l)bBog)w*?Lqq%{w@24)y8iN5(rg( zuOqI-Rcj_6bzChfe5Ol0in-fmJYjrd#ZPn{XqmZJhi)0NZRa24u_K8*Um#zdm5$1y zD@EV)vbwg|>ApTO+30{NDJ2M(|y+KCI@wwuv4eQnWQDxzuJAk={2#&>T6y6n($)csZQiwvl0iEK_GMM zXpMz=*uz$s6gq3JGqJ2Qyml84Og`JHL?ipYJ+6G3U6-QPz>5fgm1~1m^L5OBu#1ElF*(Np^6$$f*@5{MEPo7Z;LM%lWZ(3E-9>uHRYF1<8&&nI*zm_Huj>GL)>K46+YI6N9A91A(H3J9h z-mbOYO&2dNsS1CwZDDM)B7=ESS3PAV+Oy8RQSLTmnMyM2dNrxi4g6*!GZpU6#$TJ$cwFnc3 zlS3+zNS0`zi3PVqM6K~4-{bIVoUn*tmG)t``VF3pr?&P0XOIV2An!ty8h$%YQib35 zhi%8r&i4*W^wp)g%5@9(Y$RS`)CjEUr&WfHUh?jVdY_@Ka^l0s)0hY4J?MIvoAm}s zF6CC#mfx?pvalVxbgW4E>KpWPm&4e~;{ANxql#tR`fQ}BAz^Sg(_v&I{qUDbsF0xY z&G@7Wm%|PyF;2mMxs~P;MiMP)D((5yi1jCz8INJ%uLa{Z{I?5JduYY8NmLu_N`Y}a zXY~S<5&$SxdrD)(#SYb<%{^T^*>dt+!+mqpIL4B_A1*s8GX}5P>Jnq-yOfX>#p2FB zC1GodQG}c}LAt zm$@A!;{KfOcCr)*M&e-ejrf_(E z9ULHcmH_!-lRIv*QWP7}l% zK5;~d8M9kkppU@meEHUrVHZ~E)+_Hxf_<24v~x0NLAhb7ec1Zfg=@zhB~AIfww;_h z(=g3dDy^$2H;Ct^q@xed-cpQWmv=pl!4LK}O1<&7MUu#>Y2-shy8|nHq`On|+Xo9@ zt0As!O*@)1Xcnl>J#F>Xru}AUsz=S>@XtMFXQ9o_C9R@RYc?JM9k8d_#IJ@S=KfI$ zJKx4e%&Q^p;>{!G!s83Z2sn^awOD{RF<2%ssNyc4eQ-H#VZnagRKZs?i%xnW^JaYS zAgL%Z?xpgXt+hP^B+?ht*=}d42CIC@3N6!P9Lv_-aXDFQ7m2~^LkM9LB$#^O~4i)nYyE>NyyFYsIUzOom$#od`bejCG3l zyqo9W5~^^}<`Bj}%*W*Rt+buu<7urdjP!N4w)mIIjxjjVE7W6?L^FZ~CJ z;bLecEG-|;IQy8So_P}-SZwMyk37aKV%oY0Thm3G zPY+Kfk4r`PteNeLFT}^+(b@at_SHvo%ic4mC3)4W+(~U}($hZpV%c!`D@pg4rVclq zWH8ymf>65>=W8bY3ula21F5BT7@Y%{W%&77s-#^Dhub{!MmZeU?m{7mUJHol&D#30 zKvey=W~5rGq%*-VrmJE_AwVhSwwm78ERG_<;%E`K>J zAGH_eDfLd^{oaR>mDoH;Hh@0qn($lOj*wNe8;zFyg7eqYL|Mf3PeuC%RG??yJul+_ zEQJii3~?E}v`aB^?7dmm+UxU8y!EuIfAeyU_+1(1YG1>e7n&;IB&xF3(^c79D&#+w z+2}42~uykJQ@7yW_q+X(yo9E|6r8=Alb3=YyJDkM>|UK zJ?hWDHKKb8vKVZ%21a>_2*Sdyn^t z5n~6c6til(JU-}zVSV9__&Yqxn$;~~7?>D9JxVrl(xTepSYzUoa^KDeOMq$~M#aan z9M&rw7RO`s^E>j0>u_7k5TITx3-s4wtWSPJuQfkqakU!2wsk4@f;7n=#ss{D=0!le z;iExfSo%Lha2v)gs zk-k{;0x{%U=3_jF|no5i;)*U{-rh0Z8RIt7yVGCS}(AUDVP^2SKbsKuOpsjqni*o)?IVM%0P3 zq3*FIGjwgTNf4E3l6v1Fkt83AS3ZxVSka6QRB4w^|{IZWOcnN7?C zFV{v*1auLOXD}7=m+Q}$n7yi|O5)B3GX*NeS7bc2=S7KyG2Ic|_&u3E_n3G1GhrW29%YsVd158*T##SG z!G;Q_g|Liniw*`MJ%{vh_eVra^zRQcqA~Vi0v{B<0d|Z` z4|9etr(K+`^NETmyfn!$ORqT$0HBjNzYCCVMy*UGm3c&*Iz7BU{;{EeC*82@kXTPz zj*5#C%6C6E*Sc{mC%bWc1X~>P&@k!HerS?b$sU2J&k|PkkRSWmHZ_JO2zE^=TjjQwk+YP2#AV2E3=*WCDK!w$ zo*(4?ZUs4L8m>+rhl8XNhcaK~CvPP$#F`%p5x9uOZGd{UOd{zjPpI~rPxOWR>#v%# z2F43NE%<9{@83D+MKAL+XjX(g`*j>@AEFt0o1ri@hSUlis$FvRsT`~Kl&*bGTeo05 zeeBSu?gHi%mUHx-wDsR1QgfnUMe6UwxERZEQP4fT8%}Xc>X^oH&L^06fjMYS=2#Q= z;H0%_;+3b}l4-)W+kPo7kEFpn*{t z>VtJ85rjgvI^>ta{l>nWYr~O#X)~zdLgneb@Ca%Y&5(aJ7P8wf7(R3dKp@Z9qJRD~ zPWm?_#H8W&Hjj*Gm)zgjKmT?VnQe5&L=JzN(0Re@@8qL*e;NtVFl^5R9{w97xBnB_ z3g13Qzut=cQAFKPM6R7?(ttpP5pc46eKb@N^Y@z*76}H%de0_YXVxPtR`IWq>AL9T z(24v3af-m#x3KS@n`E*M5E*5kV7Oa$Dj7npa^1qo`L%<9eR^vFsiqT-;X2SnyW@eI zo2ka2HW9rn`}8_%yGyMd5;3ka*+u=4mir9H8mHEi(Wf3ddyrV8vWfxTdT9Tpzc8#I zn=S|L)QNdtK_QV(adEwhBtAjD7mle2A8l%>bZCaO==0mJ^LUXQF+%XSa*vCd-+DWB*p9mE~5 zqHcGn9B;OPj;KyM&dDj|PL78AxdJnYV(d;J7F#N%Mn!M=HfQ@Gl^pot8@0P@tqFD!oC@feh%!7o@4Rbx-+kGUc#%l@qm#=2y3=aP7wzjrLxUO&t3Ks? zPD)8#(k+~)x>6e{I10b?Bsu<7v#^5ZI-UgBAcd3_H9xE6nXOl4VHj1_ z@LlQ&Q;P@VuiX252Vu7jY{QG_%6bl3BQWlbc4sK)N@>re;7x3qv!}7Sy>9D9#kVS2 zp#-_U>hDVicuLAjV-Sc>GOND9VaVNm)EPN3hsf)Z5A= z#Z=@2h+wgHFTtxk8GXNa3<+G`f8j!HGcWyUk7$C*Wsaax3&ics6( zSR0eBcBdM7d-0hO4v_02uTM>GMO_yLxh8;ks7p<_nx20^<@Gww`_xJaZ(C2 z*yn7X*r0fAJl|KzB8SDMrp<6q&e}V*{Hep{udRTJv9s}OuH#&1xjMWUNm+NlVcEVv z9&KQ!YuA2;RX|*W;nqo*eN%}<(_zm(y->6aNY84x*LRYgzA);+7_czx| zJ?ARgd_02BenIp=qAcswwGx>Li^82>imWxRxg1}Ij@l)=D9ID@a-&$ob+-(m&plw>ziccqlDu_%0YpSVphuMk+GDi38XJb<;y zUGq?!M6sCv1HGJ6daNWyeuxf38F7(4wi0)BP-nCyV zNiIE+f~IiPmN?w|t%T&9zF!I9{&d5cL(hk!wlhx!Jx0Svdy;mFYG-8?v}o=7AEskJ z!ydwC?8J3qj4u-Jph(kdj$+pWOPTMx4p`08GC4(X5#~5B9wKH7`_(-h!|UsGC7e9t%znyOqkZmAvhqV<;w%l9r(e^R)PdbsssOdtbqIJE$Zd@yx|U+XK-;>#73 z60%{r^kawJ`uk)%T<6jXWudm@>){-qVOeS~V-zyC9yk0wc~^RmCMmgcfiX5zQ5QP) zVCvXqkD?6f6wSj`igGSEDsi6cvz9_@2TNGLIL=^WaxL^rnTs{XEk)a$c^CSq;g*=R zHun-q4V;>I%m7E%g7KE+>JP?_UnLj1q=82{BVF9hlr@%@zQ^_DN zfKl%a^z~v%W-Jw3kMK(st2Y{oYB_YdJJb$Sc}0$|77q8j#xAh5ukO!bBp#FoG7;dB zL-q%c*M4*_@LpZ?N^GcNh&EDb`}HapI2C^yf}pi5NEDRRx*4j7qDstJgGZ*13bCXP zt$zPDu;!H3JB`3pNjlG3)qgcfF=E&x0B>y{SCBbw8?_h+B?+u3Vopy;kD`nFC!7D(beyEMMbReF~q^M z(9yT?9QG7#8t=15$03c<0x0#>9#5&&E5{7s=apctX{1Ic!Z~?Xa?QYsdhyymag#$+0%q?vtEJ_xj^G;mBpTaag9nSm2*?oY+@LBw zEy{`4CF7@<#|T7+$v2yn-YA$tp&;BQOv=jKUZny%IFRV7uqJYHu_FAI0QiQlTk;3h zo80*b7^1>De~HW8I!U;6(Ao1qu4rF}U~xRj$11u* zr@I^?xmLHZS%MtS>Apr0A+}wy;UI;6V{BMP_eJo&4#U;#d0~be<}vx+P)=p7dpdkI z#Vya{Pj~CeAR^hzTPcl3S`FTiPOR7d#=*ojv|gfKtoXXdkh2i$mUE>3o~f|hcS~fY zV3+ppfSNoDw&qsZsK&SkkEa(r#=UKz$p0jqn8GUTC^F5k?GP^YjJw6&$&b$UNu5jK zQtlTryUozOdha`GAaD7s@>Ose>(rDk))uUoUc|?mI$&AX*kOzI%axF~Gw8nc3)$FH zE!M`2UlIz#&$}Mi#PNVpj(Q`Wb(212bq>b$FH*YNEF&igQFmWqgZ26N9u}0@={SLT zeZm~-kEEEk6$sz>3#Uh4wgtpGRLtE^8>*8vO@~fRZF43Lsudf-efr)6jMRy!YB{7b zPHWPAIrOs_9fBps4QQ&&laFuP5kO#9oKIwmPZcMjXzGj;;WOHExTOp6@w-jko4?dn z<11xd?BX5Rc$^aku9TAYeHD(?f1kKfwhwRf=x8gzHP7^4ASSy}+3m;2vpXqOD4o8P z%~JbVud(I5*J#!^%k_?_#&s^%`I@|j-a9{mK3>*eQ-+a??uTYpEgzaqI$JQ_`E`lL zgM}a00(-`#^i_Ng`<9SlmdCr|?n`TBZjps~U47HLyGT#l@mNu{l4LuLvf#B%uI1E( z940fgAP!$XzMn?!etj&jfP8YK%f@OkL{Q|3!A(Utidqk$h)5Ii*yE7U)Si;WoP=-q zdMcV8mNPwufUyRi7~x2BA@K%#@$e^v?9Eq);e6LnZmu(uJ5BD6J#>Gl%R|sW?EB`2 zo4@qwW=`CvL>eQ*uN$sSoVw}EOedEY4+uQWb6#kbGPFq)jz`2`H5G%A9?`D*ylm<7 zxA?*tTPl^!rt1wHj67;%KsBLpZaVgL%7@*JdYf0|t5ap8SvN<#Qwc8-k~VHX{U3p> zf7JFZvOL;OE}>DgDeXKQliZG#@(c?!ofK}-*++_t6oB8_@!hVWU*es(tMO3vw67Th z74Rd>^Ti{+p!9jCo3ZsSf!L%w7|4u3tk(dc>qJ#g6q985K(HpU4qH_{TRClcr+GMu>Zr&0O~^s z?D4EU010kna>u;6LOCT;W#2+!VR_