From 74006b620888bc05a4c9ca0b8e8c122d83bc5677 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 17 Feb 2019 17:34:31 +0100 Subject: [PATCH] Hierarchical implementation of extended method for edges --- src/db/db/dbAsIfFlatEdges.cc | 319 +++++++++++++------------- src/db/db/dbAsIfFlatEdges.h | 27 ++- src/db/db/dbDeepEdges.cc | 93 +++++++- src/db/db/dbFlatRegion.h | 4 +- src/db/db/dbLocalOperationUtils.h | 27 +++ src/db/unit_tests/dbDeepEdgesTests.cc | 47 ++++ testdata/algo/deep_edges_au6.gds | Bin 0 -> 5410 bytes 7 files changed, 349 insertions(+), 168 deletions(-) create mode 100644 testdata/algo/deep_edges_au6.gds diff --git a/src/db/db/dbAsIfFlatEdges.cc b/src/db/db/dbAsIfFlatEdges.cc index 717ef2935..991c6c9b2 100644 --- a/src/db/db/dbAsIfFlatEdges.cc +++ b/src/db/db/dbAsIfFlatEdges.cc @@ -27,17 +27,141 @@ #include "dbEdges.h" #include "dbEdgeBoolean.h" #include "dbBoxConvert.h" -#include "dbBoxScanner.h" #include "dbRegion.h" #include "dbFlatRegion.h" #include "dbPolygonTools.h" #include "dbShapeProcessor.h" +#include "dbEdgeProcessor.h" +#include "dbPolygonGenerators.h" +#include "dbPolygon.h" +#include "dbPath.h" #include namespace db { +// ------------------------------------------------------------------------------------------------------------- +// JoinEdgesCluster implementation + +JoinEdgesCluster::JoinEdgesCluster (db::PolygonSink *output, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i) + : mp_output (output), m_ext_b (ext_b), m_ext_e (ext_e), m_ext_o (ext_o), m_ext_i (ext_i) +{ + // .. nothing yet .. +} + +void +JoinEdgesCluster::finish () +{ + std::multimap objects_by_p1; + std::multimap objects_by_p2; + for (iterator o = begin (); o != end (); ++o) { + if (o->first->p1 () != o->first->p2 ()) { + objects_by_p1.insert (std::make_pair (o->first->p1 (), o)); + objects_by_p2.insert (std::make_pair (o->first->p2 (), o)); + } + } + + while (! objects_by_p2.empty ()) { + + tl_assert (! objects_by_p1.empty ()); + + // Find the beginning of a new sequence + std::multimap::iterator j0 = objects_by_p1.begin (); + std::multimap::iterator j = j0; + do { + std::multimap::iterator jj = objects_by_p2.find (j->first); + if (jj == objects_by_p2.end ()) { + break; + } else { + j = objects_by_p1.find (jj->second->first->p1 ()); + tl_assert (j != objects_by_p1.end ()); + } + } while (j != j0); + + iterator i = j->second; + + // determine a sequence + // TODO: this chooses any solution in case of forks. Choose a specific one? + std::vector pts; + pts.push_back (i->first->p1 ()); + + do { + + // record the next point + pts.push_back (i->first->p2 ()); + + // remove the edge as it's taken + std::multimap::iterator jj; + for (jj = objects_by_p2.find (i->first->p2 ()); jj != objects_by_p2.end () && jj->first == i->first->p2 (); ++jj) { + if (jj->second == i) { + break; + } + } + tl_assert (jj != objects_by_p2.end () && jj->second == i); + objects_by_p2.erase (jj); + objects_by_p1.erase (j); + + // process along the edge to the next one + // TODO: this chooses any solution in case of forks. Choose a specific one? + j = objects_by_p1.find (i->first->p2 ()); + if (j != objects_by_p1.end ()) { + i = j->second; + } else { + break; + } + + } while (true); + + bool cyclic = (pts.back () == pts.front ()); + + if (! cyclic) { + + // non-cyclic sequence + db::Path path (pts.begin (), pts.end (), 0, m_ext_b, m_ext_e, false); + std::vector hull; + path.hull (hull, m_ext_o, m_ext_i); + db::Polygon poly; + poly.assign_hull (hull.begin (), hull.end ()); + mp_output->put (poly); + + } else { + + // we have a loop: form a contour by using the polygon size functions and a "Not" to form the hole + db::Polygon poly; + poly.assign_hull (pts.begin (), pts.end ()); + + db::EdgeProcessor ep; + db::PolygonGenerator pg (*mp_output, false, true); + + int mode_a = -1, mode_b = -1; + + if (m_ext_o == 0) { + ep.insert (poly, 0); + } else { + db::Polygon sized_poly (poly); + sized_poly.size (m_ext_o, m_ext_o, 2 /*sizing mode*/); + ep.insert (sized_poly, 0); + mode_a = 1; + } + + if (m_ext_i == 0) { + ep.insert (poly, 1); + } else { + db::Polygon sized_poly (poly); + sized_poly.size (-m_ext_i, -m_ext_i, 2 /*sizing mode*/); + ep.insert (sized_poly, 1); + mode_b = 1; + } + + db::BooleanOp2 op (db::BooleanOp::ANotB, mode_a, mode_b); + ep.process (pg, op); + + } + + } +} + // ------------------------------------------------------------------------------------------------------------- // AsIfFlagEdges implementation @@ -219,148 +343,13 @@ AsIfFlatEdges::selected_not_interacting (const Region &other) const namespace { -template -struct JoinEdgesCluster - : public db::cluster, - public db::PolygonSink -{ - typedef db::Edge::coord_type coord_type; - - JoinEdgesCluster (OutputContainer *output, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i) - : mp_output (output), m_ext_b (ext_b), m_ext_e (ext_e), m_ext_o (ext_o), m_ext_i (ext_i) - { - // .. nothing yet .. - } - - void finish () - { - std::multimap objects_by_p1; - std::multimap objects_by_p2; - for (iterator o = begin (); o != end (); ++o) { - if (o->first->p1 () != o->first->p2 ()) { - objects_by_p1.insert (std::make_pair (o->first->p1 (), o)); - objects_by_p2.insert (std::make_pair (o->first->p2 (), o)); - } - } - - while (! objects_by_p2.empty ()) { - - tl_assert (! objects_by_p1.empty ()); - - // Find the beginning of a new sequence - std::multimap::iterator j0 = objects_by_p1.begin (); - std::multimap::iterator j = j0; - do { - std::multimap::iterator jj = objects_by_p2.find (j->first); - if (jj == objects_by_p2.end ()) { - break; - } else { - j = objects_by_p1.find (jj->second->first->p1 ()); - tl_assert (j != objects_by_p1.end ()); - } - } while (j != j0); - - iterator i = j->second; - - // determine a sequence - // TODO: this chooses any solution in case of forks. Choose a specific one? - std::vector pts; - pts.push_back (i->first->p1 ()); - - do { - - // record the next point - pts.push_back (i->first->p2 ()); - - // remove the edge as it's taken - std::multimap::iterator jj; - for (jj = objects_by_p2.find (i->first->p2 ()); jj != objects_by_p2.end () && jj->first == i->first->p2 (); ++jj) { - if (jj->second == i) { - break; - } - } - tl_assert (jj != objects_by_p2.end () && jj->second == i); - objects_by_p2.erase (jj); - objects_by_p1.erase (j); - - // process along the edge to the next one - // TODO: this chooses any solution in case of forks. Choose a specific one? - j = objects_by_p1.find (i->first->p2 ()); - if (j != objects_by_p1.end ()) { - i = j->second; - } else { - break; - } - - } while (true); - - bool cyclic = (pts.back () == pts.front ()); - - if (! cyclic) { - - // non-cyclic sequence - db::Path path (pts.begin (), pts.end (), 0, m_ext_b, m_ext_e, false); - std::vector hull; - path.hull (hull, m_ext_o, m_ext_i); - db::Polygon poly; - poly.assign_hull (hull.begin (), hull.end ()); - put (poly); - - } else { - - // we have a loop: form a contour by using the polygon size functions and a "Not" to form the hole - db::Polygon poly; - poly.assign_hull (pts.begin (), pts.end ()); - - db::EdgeProcessor ep; - db::PolygonGenerator pg (*this, false, true); - - int mode_a = -1, mode_b = -1; - - if (m_ext_o == 0) { - ep.insert (poly, 0); - } else { - db::Polygon sized_poly (poly); - sized_poly.size (m_ext_o, m_ext_o, 2 /*sizing mode*/); - ep.insert (sized_poly, 0); - mode_a = 1; - } - - if (m_ext_i == 0) { - ep.insert (poly, 1); - } else { - db::Polygon sized_poly (poly); - sized_poly.size (-m_ext_i, -m_ext_i, 2 /*sizing mode*/); - ep.insert (sized_poly, 1); - mode_b = 1; - } - - db::BooleanOp2 op (db::BooleanOp::ANotB, mode_a, mode_b); - ep.process (pg, op); - - } - - } - } - - virtual void put (const db::Polygon &polygon) - { - mp_output->insert (polygon); - } - -private: - OutputContainer *mp_output; - coord_type m_ext_b, m_ext_e, m_ext_o, m_ext_i; -}; - -template struct JoinEdgesClusterCollector - : public db::cluster_collector > + : public db::cluster_collector { typedef db::Edge::coord_type coord_type; - JoinEdgesClusterCollector (OutputContainer *output, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i) - : db::cluster_collector > (JoinEdgesCluster (output, ext_b, ext_e, ext_o, ext_i), true) + JoinEdgesClusterCollector (db::PolygonSink *output, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i) + : db::cluster_collector (JoinEdgesCluster (output, ext_b, ext_e, ext_o, ext_i), true) { // .. nothing yet .. } @@ -368,20 +357,45 @@ struct JoinEdgesClusterCollector void add (const db::Edge *o1, size_t p1, const db::Edge *o2, size_t p2) { if (o1->p2 () == o2->p1 () || o1->p1 () == o2->p2 ()) { - db::cluster_collector >::add (o1, p1, o2, p2); + db::cluster_collector::add (o1, p1, o2, p2); } } }; } +db::Polygon +AsIfFlatEdges::extended_edge (const db::Edge &edge, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i) +{ + db::DVector d; + if (edge.is_degenerate ()) { + d = db::DVector (1.0, 0.0); + } else { + d = db::DVector (edge.d ()) * (1.0 / edge.double_length ()); + } + + db::DVector n (-d.y (), d.x ()); + + db::Point pts[4] = { + db::Point (db::DPoint (edge.p1 ()) - d * double (ext_b) + n * double (ext_o)), + db::Point (db::DPoint (edge.p2 ()) + d * double (ext_e) + n * double (ext_o)), + db::Point (db::DPoint (edge.p2 ()) + d * double (ext_e) - n * double (ext_i)), + db::Point (db::DPoint (edge.p1 ()) - d * double (ext_b) - n * double (ext_i)), + }; + + db::Polygon poly; + poly.assign_hull (pts + 0, pts + 4); + return poly; +} + RegionDelegate * AsIfFlatEdges::extended (coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i, bool join) const { if (join) { std::auto_ptr output (new FlatRegion ()); - JoinEdgesClusterCollector cluster_collector (output.get (), ext_b, ext_e, ext_o, ext_i); + db::ShapeGenerator sg (output->raw_polygons (), false); + JoinEdgesClusterCollector cluster_collector (&sg, ext_b, ext_e, ext_o, ext_i); db::box_scanner scanner (report_progress (), progress_desc ()); scanner.reserve (size ()); @@ -401,29 +415,8 @@ AsIfFlatEdges::extended (coord_type ext_b, coord_type ext_e, coord_type ext_o, c } else { std::auto_ptr output (new FlatRegion ()); - for (EdgesIterator e (begin_merged ()); ! e.at_end (); ++e) { - - db::DVector d; - if (e->is_degenerate ()) { - d = db::DVector (1.0, 0.0); - } else { - d = db::DVector (e->d ()) * (1.0 / e->double_length ()); - } - - db::DVector n (-d.y (), d.x ()); - - db::Point pts[4] = { - db::Point (db::DPoint (e->p1 ()) - d * double (ext_b) + n * double (ext_o)), - db::Point (db::DPoint (e->p2 ()) + d * double (ext_e) + n * double (ext_o)), - db::Point (db::DPoint (e->p2 ()) + d * double (ext_e) - n * double (ext_i)), - db::Point (db::DPoint (e->p1 ()) - d * double (ext_b) - n * double (ext_i)), - }; - - db::Polygon poly; - poly.assign_hull (pts + 0, pts + 4); - output->insert (poly); - + output->insert (extended_edge (*e, ext_b, ext_e, ext_o, ext_i)); } return output.release (); diff --git a/src/db/db/dbAsIfFlatEdges.h b/src/db/db/dbAsIfFlatEdges.h index 3a5fe50e7..d67b87080 100644 --- a/src/db/db/dbAsIfFlatEdges.h +++ b/src/db/db/dbAsIfFlatEdges.h @@ -25,11 +25,35 @@ #define HDR_dbAsIfFlatEdges #include "dbCommon.h" - +#include "dbBoxScanner.h" #include "dbEdgesDelegate.h" +#include +#include + namespace db { +class PolygonSink; + +/** + * @brief A helper class to turn joined edge sequences into polygons + * + * This object is an edge cluster so it can connect to a cluster collector + * driven by a box scanner. + */ +struct JoinEdgesCluster + : public db::cluster +{ + typedef db::Edge::coord_type coord_type; + + JoinEdgesCluster (db::PolygonSink *output, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i); + void finish (); + +private: + db::PolygonSink *mp_output; + coord_type m_ext_b, m_ext_e, m_ext_o, m_ext_i; +}; + /** * @brief Provides default flat implementations */ @@ -160,6 +184,7 @@ protected: void update_bbox (const db::Box &box); void invalidate_bbox (); EdgePairs run_check (db::edge_relation_type rel, const Edges *other, db::Coord d, bool whole_edges, metrics_type metrics, double ignore_angle, distance_type min_projection, distance_type max_projection) const; + static db::Polygon extended_edge (const db::Edge &edge, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i); private: AsIfFlatEdges &operator= (const AsIfFlatEdges &other); diff --git a/src/db/db/dbDeepEdges.cc b/src/db/db/dbDeepEdges.cc index 55aee834b..ad95737c8 100644 --- a/src/db/db/dbDeepEdges.cc +++ b/src/db/db/dbDeepEdges.cc @@ -31,6 +31,7 @@ #include "dbCellMapping.h" #include "dbLayoutUtils.h" #include "dbLocalOperation.h" +#include "dbLocalOperationUtils.h" #include "dbHierProcessor.h" #include "dbEmptyEdges.h" @@ -807,8 +808,96 @@ EdgesDelegate *DeepEdges::outside_part (const Region &other) const RegionDelegate *DeepEdges::extended (coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i, bool join) const { - // TODO: implement - return AsIfFlatEdges::extended (ext_b, ext_e, ext_o, ext_i, join); + ensure_merged_edges_valid (); + + std::auto_ptr res (new db::DeepRegion (m_merged_edges.derived ())); + + db::Layout &layout = const_cast (m_merged_edges.layout ()); + db::Cell &top_cell = const_cast (m_merged_edges.initial_cell ()); + + db::MagnificationReducer red; + db::cell_variants_collector vars (red); + vars.collect (m_merged_edges.layout (), m_merged_edges.initial_cell ()); + + std::map > to_commit; + + if (join) { + + // In joined mode we need to create a special cluster which connects all joined edges + db::DeepLayer joined = m_merged_edges.derived (); + + db::hier_clusters hc; + db::Connectivity conn (db::Connectivity::EdgesConnectByPoints); + conn.connect (m_merged_edges); + // TODO: this uses the wrong verbosity inside ... + hc.build (layout, m_merged_edges.initial_cell (), db::ShapeIterator::Edges, conn); + + // TODO: iterate only over the called cells? + for (db::Layout::iterator c = layout.begin (); c != layout.end (); ++c) { + + const std::map &vv = vars.variants (c->cell_index ()); + for (std::map::const_iterator v = vv.begin (); v != vv.end (); ++v) { + + db::Shapes *out; + if (vv.size () == 1) { + out = & c->shapes (res->deep_layer ().layer ()); + } else { + out = & to_commit [c->cell_index ()][v->first]; + } + + const db::connected_clusters &cc = hc.clusters_per_cell (c->cell_index ()); + for (db::connected_clusters::all_iterator cl = cc.begin_all (); ! cl.at_end (); ++cl) { + + if (cc.is_root (*cl)) { + + PolygonRefToShapesGenerator prgen (&layout, out); + polygon_transformation_filter ptrans (&prgen, v->first.inverted ()); + JoinEdgesCluster jec (&ptrans, ext_b, ext_e, ext_o, ext_i); + + std::list heap; + for (db::recursive_cluster_shape_iterator rcsi (hc, m_merged_edges.layer (), c->cell_index (), *cl); ! rcsi.at_end (); ++rcsi) { + heap.push_back (rcsi->transformed (v->first * rcsi.trans ())); + jec.add (&heap.back (), 0); + } + + jec.finish (); + + } + + } + + } + + } + + } else { + + for (db::Layout::iterator c = layout.begin (); c != layout.end (); ++c) { + + const std::map &vv = vars.variants (c->cell_index ()); + for (std::map::const_iterator v = vv.begin (); v != vv.end (); ++v) { + + db::Shapes *out; + if (vv.size () == 1) { + out = & c->shapes (res->deep_layer ().layer ()); + } else { + out = & to_commit [c->cell_index ()][v->first]; + } + + for (db::Shapes::shape_iterator si = c->shapes (m_merged_edges.layer ()).begin (db::ShapeIterator::Edges); ! si.at_end (); ++si) { + out->insert (extended_edge (si->edge ().transformed (v->first), ext_b, ext_e, ext_o, ext_i).transformed (v->first.inverted ())); + } + + } + + } + + } + + // propagate results from variants + vars.commit_shapes (layout, top_cell, res->deep_layer ().layer (), to_commit); + + return res.release (); } EdgesDelegate *DeepEdges::start_segments (length_type length, double fraction) const diff --git a/src/db/db/dbFlatRegion.h b/src/db/db/dbFlatRegion.h index 8f84c8d66..6ad08639d 100644 --- a/src/db/db/dbFlatRegion.h +++ b/src/db/db/dbFlatRegion.h @@ -175,6 +175,8 @@ public: } } + db::Shapes &raw_polygons () { return m_polygons; } + protected: virtual void merged_semantics_changed (); virtual Box compute_bbox () const; @@ -185,8 +187,6 @@ private: friend class AsIfFlatRegion; friend class Region; - db::Shapes &raw_polygons () { return m_polygons; } - FlatRegion &operator= (const FlatRegion &other); bool m_is_merged; diff --git a/src/db/db/dbLocalOperationUtils.h b/src/db/db/dbLocalOperationUtils.h index 8665eec2c..6342f3ccc 100644 --- a/src/db/db/dbLocalOperationUtils.h +++ b/src/db/db/dbLocalOperationUtils.h @@ -36,6 +36,33 @@ namespace db { +template +class polygon_transformation_filter + : public PolygonSink +{ +public: + /** + * @brief Constructor specifying an external vector for storing the polygons + */ + polygon_transformation_filter (PolygonSink *output, const Trans &tr) + : mp_output (output), m_trans (tr) + { + // .. nothing yet .. + } + + /** + * @brief Implementation of the PolygonSink interface + */ + virtual void put (const db::Polygon &polygon) + { + mp_output->put (polygon.transformed (m_trans)); + } + +private: + db::PolygonSink *mp_output; + const Trans m_trans; +}; + class PolygonRefGenerator : public PolygonSink { diff --git a/src/db/unit_tests/dbDeepEdgesTests.cc b/src/db/unit_tests/dbDeepEdgesTests.cc index 2c14840b6..cf4c00e68 100644 --- a/src/db/unit_tests/dbDeepEdgesTests.cc +++ b/src/db/unit_tests/dbDeepEdgesTests.cc @@ -247,3 +247,50 @@ TEST(5_Filters) } } +TEST(6_Extended) +{ + db::Layout ly; + { + std::string fn (tl::testsrc ()); + fn += "/testdata/algo/deep_region_area_peri_l1.gds"; + tl::InputStream stream (fn); + db::Reader reader (stream); + reader.read (ly); + } + + db::cell_index_type top_cell_index = *ly.begin_top_down (); + db::Cell &top_cell = ly.cell (top_cell_index); + + db::DeepShapeStore dss; + + unsigned int l2 = ly.get_layer (db::LayerProperties (2, 0)); + + db::Region r2 (db::RecursiveShapeIterator (ly, top_cell, l2), dss); + db::Edges e2 = r2.edges (); + + db::Layout target; + unsigned int target_top_cell_index = target.add_cell (ly.cell_name (top_cell_index)); + + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (2, 0)), r2); + + db::EdgeLengthFilter elf1 (0, 40000, false); + db::Edges e2f = e2.filtered (elf1); + + db::Region e2e1; + e2.extended (e2e1, 100, 200, 300, 50); + db::Region e2e2; + e2f.extended (e2e2, 0, 0, 300, 0); + db::Region e2e3; + e2.extended (e2e3, 100, 200, 300, 50, true); + db::Region e2e4; + e2f.extended (e2e4, 0, 0, 300, 0, true); + + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (10, 0)), e2e1); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (11, 0)), e2e2); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (12, 0)), e2e3); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (13, 0)), e2e4); + + CHECKPOINT(); + db::compare_layouts (_this, target, tl::testsrc () + "/testdata/algo/deep_edges_au6.gds"); +} + diff --git a/testdata/algo/deep_edges_au6.gds b/testdata/algo/deep_edges_au6.gds new file mode 100644 index 0000000000000000000000000000000000000000..1bd8172107a9004ae1944735c6d51210cd4198c2 GIT binary patch literal 5410 zcmbW4L1>g$5XWaXo6R;x(KM7c2}uquc!-Li(S{(#dTESFu|cEl!9yt??4hL;1@Rc+CPN_A6NMEW;3iEJNn?*7N5$es@HU!&QTl`XQ8-nkcd zLIW1@1o&Jves3xuv%v%|38`75yK$zXf`7n%abBZ^`Ms_Wmvs zpCdu5#J<(8%*2m;ox^oxQ~5pDz;C13%LV@%V*at;HlIJ`U99sbejAZLe?6~f-UjZ5 z{Pp?!#{$ic_0sqg|MVB)C#MiUv~@Cme?8T=h4|0a>xcKDezXo456_+FiTvw%2C^<# zU!EaPxTg7H{&`+Or<{hZ{=t3fN14X6F~l>pxykG6{;?P4FXn^$VZH)GoCC%BwDo*{ z`2G7ltdSpVVYN=Iq7!2mm6ufkPSHoph~tN+7^)-Pb)x5PkQGG7-KAPP&2U}yYeZS& zyx1AzEKG}S_uvOzvF#qF#2Mf1o@Q63aVC14&Owpg=yGyk4*G;<-+iG_tp)uePjA#Y za_lMcvM8Rx{R7yE3Eqb;LY}dMb2-*&Fho`1g5dJ!<}!b(dYw z#qa%EH)`%s=SH)melzhSU-0Rt6Te68IBKFkwWjiV_JQU(^h;bX+8gp;n0CxR^~^u> z#XJ~UKdeWi);FJqtn+rnhjZpNPyd~I+DPUhe$0n3ogd@6~q-kGij}Ns-?D^?e}+cBOZ}a~Gfb zD;qRBxKewyjQ74~>1Iy4Cf(AV{Ze}Wv68>tD?w4uC%u%e!3#WTob@Xqb6w5DeSTNV zvR?ia`+wtds>4~WtUFDN2)nJ z;=L`>bH;PzrpN~stQTl@Fzxdse&{|&HEr_j!~9u7*BH$XX7w+oo%?%Aj4#2lM*d&dnfa$HDm-?myD(U`CI;qi1G@JRe*~{_)*t z_L-raUZk_%EuC5Pe7+Su^OajNnjOsehwdM@w6NXt`4uT$pNBrs>|jn``g#HWr$o6h zBc&S?STE4*U`{WcSD{D#$+Oqs|4O9U!K}Wg!?|NPck&Wyr}wi+vx9j(`sX5Qrw6>; zA*J$Eq}joozEt`K{xQ+WOQX;)Mw%VW>CrpR9YybwlY`LjiZnZz>I?5J+W(jz?EjIy zHb>%I@z?WuKX<&2{m=PhjO*p}{(d|S>T^r`f3KedX1AwBm|sc!=v%z&&YvW4c34OI zS7~--X8-hyemUjIb+rEi&E7CS6aPL>>@z=yi|d)aq5l%S@4K_`$39#D`Ghl?9n8gF zy1o_leiC&LLg)J@(Cn+|weBa8Sk^zDy~tst*}<&;(nHi+4fP8`uk~v*dxPHI8*lQj zQq~vqr1gjA2%kTEZp;q)ImWrjd8YFb^Of_O^Ht|H=RTevI_E*^Y#n``p&nUJtVfd1 z1=feBpW8fd&S}!eK^FFF@qWlR5S^EtH#{%QJI*%|l^DMquH&W@cvz?$bZSE B&k+Cs literal 0 HcmV?d00001