From fe1d520d0cf3c7be3e69558979be83a092bb7bf9 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 21 Jan 2023 01:38:25 +0100 Subject: [PATCH] WIP: some bug fixes, tests for deep mode - some refinement needed. --- src/db/db/dbShapeCollection.cc | 3 +- src/db/db/gsiDeclDbRecursiveShapeIterator.cc | 85 +++++++++++++++++++ src/drc/drc/built-in-macros/_drc_engine.rb | 27 +++++- src/drc/drc/built-in-macros/_drc_layer.rb | 33 ++++--- src/drc/drc/built-in-macros/_drc_source.rb | 43 +++++++--- src/drc/drc/built-in-macros/_drc_tags.rb | 13 +++ src/drc/unit_tests/drcSimpleTests.cc | 5 ++ testdata/drc/drcSimpleTests_70.drc | 33 ++++--- testdata/drc/drcSimpleTests_au70.gds | Bin 65794 -> 66238 bytes testdata/drc/drcSimpleTests_au70d.gds | Bin 0 -> 47154 bytes 10 files changed, 199 insertions(+), 43 deletions(-) create mode 100644 testdata/drc/drcSimpleTests_au70d.gds diff --git a/src/db/db/dbShapeCollection.cc b/src/db/db/dbShapeCollection.cc index ea79c2b4b..4d5a3a0fc 100644 --- a/src/db/db/dbShapeCollection.cc +++ b/src/db/db/dbShapeCollection.cc @@ -57,9 +57,10 @@ DeepShapeCollectionDelegateBase::apply_property_translator (const db::Properties db::Shapes &shapes = c->shapes (m_deep_layer.layer ()); db::Shapes new_shapes (shapes.is_editable ()); - new_shapes.assign (shapes, pt); shapes.swap (new_shapes); + shapes.assign (new_shapes, pt); + } } diff --git a/src/db/db/gsiDeclDbRecursiveShapeIterator.cc b/src/db/db/gsiDeclDbRecursiveShapeIterator.cc index 092ec6502..e97f17bfe 100644 --- a/src/db/db/gsiDeclDbRecursiveShapeIterator.cc +++ b/src/db/db/gsiDeclDbRecursiveShapeIterator.cc @@ -172,6 +172,32 @@ static db::Region complex_region (const db::RecursiveShapeIterator *iter) } } +static void enable_properties (db::RecursiveShapeIterator *c) +{ + c->apply_property_translator (db::PropertiesTranslator::make_pass_all ()); +} + +static void remove_properties (db::RecursiveShapeIterator *c) +{ + c->apply_property_translator (db::PropertiesTranslator::make_remove_all ()); +} + +static void filter_properties (db::RecursiveShapeIterator *c, const std::vector &keys) +{ + if (c->layout ()) { + std::set kf; + kf.insert (keys.begin (), keys.end ()); + c->apply_property_translator (db::PropertiesTranslator::make_filter (const_cast (c->layout ())->properties_repository (), kf)); + } +} + +static void map_properties (db::RecursiveShapeIterator *c, const std::map &map) +{ + if (c->layout ()) { + c->apply_property_translator (db::PropertiesTranslator::make_key_mapper (const_cast (c->layout ())->properties_repository (), map)); + } +} + Class decl_RecursiveShapeIterator ("db", "RecursiveShapeIterator", gsi::constructor ("new", &new_si1, gsi::arg ("layout"), gsi::arg ("cell"), gsi::arg ("layer"), "@brief Creates a recursive, single-layer shape iterator.\n" @@ -547,6 +573,20 @@ Class decl_RecursiveShapeIterator ("db", "RecursiveS "\n" "This method has been introduced in version 0.25.3." ) + + gsi::method ("prop_id", &db::RecursiveShapeIterator::prop_id, + "@brief Gets the effective properties ID\n" + "The shape iterator supports property filtering and translation. This method will deliver " + "the effective property ID after translation. The original property ID can be obtained from " + "'shape.prop_id' and is not changed by installing filters or mappers.\n" + "\n" + "\\prop_id is evaluated by \\Region objects for example, when they are created " + "from a shape iterator.\n" + "\n" + "See \\enable_properties, \\filter_properties, \\remove_properties and \\map_properties for " + "details on this feature.\n" + "\n" + "This attribute has been introduced in version 0.28.4." + ) + gsi::method ("shape", &db::RecursiveShapeIterator::shape, "@brief Gets the current shape\n" "\n" @@ -592,6 +632,51 @@ Class decl_RecursiveShapeIterator ("db", "RecursiveS "@brief Comparison of iterators - inequality\n" "\n" "Two iterators are not equal if they do not point to the same shape.\n" + ) + + gsi::method_ext ("enable_properties", &enable_properties, + "@brief Enables properties for the given iterator.\n" + "Afer enabling properties, \\prop_id will deliver the effective properties ID for the current shape. " + "By default, properties are not enabled and \\prop_id will always return 0 (no properties attached). " + "Alternatively you can apply \\filter_properties " + "or \\map_properties to enable properties with a specific name key.\n" + "\n" + "Note that property filters/mappers are additive and act in addition (after) the currently installed filter.\n" + "\n" + "This feature has been introduced in version 0.28.4." + ) + + gsi::method_ext ("remove_properties", &remove_properties, + "@brief Removes properties for the given container.\n" + "This will remove all properties and \\prop_id will deliver 0 always (no properties attached).\n" + "Alternatively you can apply \\filter_properties " + "or \\map_properties to enable properties with a specific name key.\n" + "\n" + "Note that property filters/mappers are additive and act in addition (after) the currently installed filter.\n" + "So effectively after 'remove_properties' you cannot get them back.\n" + "\n" + "This feature has been introduced in version 0.28.4." + ) + + gsi::method_ext ("filter_properties", &filter_properties, gsi::arg ("keys"), + "@brief Filters properties by certain keys.\n" + "Calling this method will reduce the properties to values with name keys from the 'keys' list.\n" + "As a side effect, this method enables properties.\n" + "As with \\enable_properties or \\remove_properties, this filter has an effect on the value returned " + "by \\prop_id, not on the properties ID attached to the shape directly.\n" + "\n" + "Note that property filters/mappers are additive and act in addition (after) the currently installed filter.\n" + "\n" + "This feature has been introduced in version 0.28.4." + ) + + gsi::method_ext ("map_properties", &map_properties, gsi::arg ("key_map"), + "@brief Maps properties by name key.\n" + "Calling this method will reduce the properties to values with name keys from the 'keys' hash and " + "renames the properties. Property values with keys not listed in the key map will be removed.\n" + "As a side effect, this method enables properties.\n" + "As with \\enable_properties or \\remove_properties, this filter has an effect on the value returned " + "by \\prop_id, not on the properties ID attached to the shape directly.\n" + "\n" + "Note that property filters/mappers are additive and act in addition (after) the currently installed filter.\n" + "\n" + "This feature has been introduced in version 0.28.4." ), "@brief An iterator delivering shapes recursively\n" "\n" diff --git a/src/drc/drc/built-in-macros/_drc_engine.rb b/src/drc/drc/built-in-macros/_drc_engine.rb index 7e5ebecbd..2c166ee4f 100644 --- a/src/drc/drc/built-in-macros/_drc_engine.rb +++ b/src/drc/drc/built-in-macros/_drc_engine.rb @@ -323,6 +323,30 @@ module DRC def negative DRCNegative::new end + + def enable_props + DRCPropertySelector::new(:enable_properties) + end + + def remove_props + DRCPropertySelector::new(:remove_properties) + end + + def select_props(*keys) + self._context("select_props") do + keys.each do |k| + k.is_a?(String) || k.is_a?(1.class) || raise("Key values need to be integers or strings (got '#{k.inspect}')") + end + DRCPropertySelector::new(:filter_properties, keys) + end + end + + def map_props(hash) + self._context("map_props") do + hash.is_a?(Hash) || raise("Argument needs to be a hash (got '#{hash.inspect}')") + DRCPropertySelector::new(:map_properties, hash) + end + end def pattern(p) self._context("pattern") do @@ -2806,7 +2830,7 @@ CODE end end - def _input(layout, cell_index, layers, sel, box, clip, overlapping, shape_flags, global_trans, cls) + def _input(layout, cell_index, layers, sel, box, clip, overlapping, shape_flags, global_trans, prop_sel, cls) if layers.empty? && ! @deep @@ -2819,6 +2843,7 @@ CODE else iter = RBA::RecursiveShapeIterator::new(layout, layout.cell(cell_index), layers) end + prop_sel.each { |p| p.apply_to(iter) } iter.shape_flags = shape_flags iter.global_dtrans = global_trans diff --git a/src/drc/drc/built-in-macros/_drc_layer.rb b/src/drc/drc/built-in-macros/_drc_layer.rb index 70890edcb..2ff441a03 100644 --- a/src/drc/drc/built-in-macros/_drc_layer.rb +++ b/src/drc/drc/built-in-macros/_drc_layer.rb @@ -4783,37 +4783,35 @@ CODE # %DRC% # @name select_props - # @brief Enables or selects properties from original or property-annotated layers - # @synopsis layer.select_props + # @brief Enables or selects properties from a property-annotated layer # @synopsis layer.select_props(keys) # - # This method will enable user properties or select specific property keys - # from layers. It returns a new layer with properties enabled. The + # This method will select specific property keys + # from layers. It returns a new layer with the new properties. The # original layer is not modified. # - # When used on original layers, this method will enable properties on input. - # By default, properties are not read: - # - # @code - # layer1 = input(1, 0) - # layer1_with_props = input(1, 0).select_props - # @/code - # # You can specify the user property keys (names) to use. As user properties - # in general are a set of key/value pairs and may carry multiple information + # in general are a set of key/value pairs and may carry multiple values # under different keys, this feature can be handy to filter out a specific # aspect. To get only the values from key 1 (integer), use: # # @code - # layer1_with_props = input(1, 0).select_props(1) + # layer1 = input(1, 0, enable_properties) + # layer1_filtered = layer1.select_props(1) # @/code # # To get the combined key 1 and 2 properties, use: # # @code - # layer1_with_props = input(1, 0).select_props(1, 2) + # layer1 = input(1, 0, enable_properties) + # layer1_filtered = layer1.select_props(1, 2) # @/code # + # Without any arguments, this method will remove all properties. + # Note that you can directly filter or map properties on input + # which is more efficient than first loading all and then selecting some + # properties. See \DRCSource#input for details. + # # \map_props is a way to change property keys and \remove_props # will entirely remove all user properties. @@ -4822,7 +4820,7 @@ CODE # @brief Selects properties with certain keys and allows key mapping # @synopsis layer.map_props({ key => key_new, .. }) # - # Similar to \select_props, this method will enable user properties + # Similar to \select_props, this method will map or filter properties and # and take the values from certain keys. In addition, this method allows # mapping keys to new keys. Specify a hash argument with old to new keys. # @@ -4835,7 +4833,8 @@ CODE # use: # # @code - # layer1_with_props = input(1, 0).map_props({ 2 => 1 }) + # layer1 = input(1, 0, enable_properties) + # layer1_mapped = layer1.map_props({ 2 => 1 }) # @/code # # See also \select_props and \remove_props. diff --git a/src/drc/drc/built-in-macros/_drc_source.rb b/src/drc/drc/built-in-macros/_drc_source.rb index 2e969e463..6cade1e21 100644 --- a/src/drc/drc/built-in-macros/_drc_source.rb +++ b/src/drc/drc/built-in-macros/_drc_source.rb @@ -374,6 +374,7 @@ CODE # @synopsis source.input(layer, datatype) # @synopsis source.input(layer_into) # @synopsis source.input(filter, ...) + # @synopsis source.input(props_spec, ...) # Creates a layer with the shapes from the given layer of the source. # The layer can be specified by layer and optionally datatype, by a RBA::LayerInfo # object or by a sequence of filters. @@ -410,12 +411,27 @@ CODE # # "input" without any arguments will create a new, empty original layer. # + # If you want to use user properties - for example with properties constraints in DRC checks - + # you need to enable properties on input: + # + # @code + # input1_with_props = input(1, 0, enable_props) + # @/code + # + # You can also filter or map property keys, similar to the functions available on + # layers (\DRCLayer#map_props, \DRCLayer#select_props). For example to select + # property values with key 17 (numerical) only, use: + # + # @code + # input1_with_props = input(1, 0, select_props(17)) + # @/code + # # Use the global version of "input" without a source object to address the default source. def input(*args) @engine._context("input") do - layers = parse_input_layers(*args) - DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SAll, @global_trans, RBA::Region)) + layers, prop_selectors = parse_input_layers(*args) + DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SAll, @global_trans, prop_selectors, RBA::Region)) end end @@ -441,8 +457,8 @@ CODE def labels(*args) @engine._context("labels") do - layers = parse_input_layers(*args) - DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::STexts, @global_trans, RBA::Texts)) + layers, prop_selectors = parse_input_layers(*args) + DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::STexts, @global_trans, prop_selectors, RBA::Texts)) end end @@ -467,8 +483,8 @@ CODE def polygons(*args) @engine._context("polygons") do - layers = parse_input_layers(*args) - DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SBoxes | RBA::Shapes::SPaths | RBA::Shapes::SPolygons | RBA::Shapes::SEdgePairs, @global_trans, RBA::Region)) + layers, prop_selectors = parse_input_layers(*args) + DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SBoxes | RBA::Shapes::SPaths | RBA::Shapes::SPolygons | RBA::Shapes::SEdgePairs, @global_trans, prop_selectors, RBA::Region)) end end @@ -496,8 +512,8 @@ CODE def edges(*args) @engine._context("edges") do - layers = parse_input_layers(*args) - DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SBoxes | RBA::Shapes::SPaths | RBA::Shapes::SPolygons | RBA::Shapes::SEdgePairs | RBA::Shapes::SEdges, @global_trans, RBA::Edges)) + layers, prop_selectors = parse_input_layers(*args) + DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SBoxes | RBA::Shapes::SPaths | RBA::Shapes::SPolygons | RBA::Shapes::SEdgePairs | RBA::Shapes::SEdges, @global_trans, prop_selectors, RBA::Edges)) end end @@ -525,8 +541,8 @@ CODE def edge_pairs(*args) @engine._context("edge_pairs") do - layers = parse_input_layers(*args) - DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SEdgePairs, @global_trans, RBA::EdgePairs)) + layers, prop_selectors = parse_input_layers(*args) + DRCLayer::new(@engine, @engine._cmd(@engine, :_input, @layout_var, @cell.cell_index, layers, @sel, @box, @clip, @overlapping, RBA::Shapes::SEdgePairs, @global_trans, prop_selectors, RBA::EdgePairs)) end end @@ -581,7 +597,10 @@ CODE def parse_input_layers(*args) layers = [] - + prop_selectors = args.select { |a| a.is_a?(DRCPropertySelector) } + + args = args.select { |a| !a.is_a?(DRCPropertySelector) } + if args.size == 0 li = @layout.insert_layer(RBA::LayerInfo::new) @@ -615,7 +634,7 @@ CODE end - layers + [ layers, prop_selectors ] end diff --git a/src/drc/drc/built-in-macros/_drc_tags.rb b/src/drc/drc/built-in-macros/_drc_tags.rb index 317f882b4..aabb84c52 100644 --- a/src/drc/drc/built-in-macros/_drc_tags.rb +++ b/src/drc/drc/built-in-macros/_drc_tags.rb @@ -158,6 +158,19 @@ module DRC def initialize end end + + # Property selector for "input" + class DRCPropertySelector + attr_accessor :method + attr_accessor :args + def apply_to(iter) + iter.send(self.method, *self.args) + end + def initialize(method, *args) + self.method = method + self.args = args + end + end # A wrapper for a pair of limit values # This class is used to identify projection limits for DRC diff --git a/src/drc/unit_tests/drcSimpleTests.cc b/src/drc/unit_tests/drcSimpleTests.cc index 7622b3217..107aa58de 100644 --- a/src/drc/unit_tests/drcSimpleTests.cc +++ b/src/drc/unit_tests/drcSimpleTests.cc @@ -1401,3 +1401,8 @@ TEST(70_props) { run_test (_this, "70", false); } + +TEST(70d_props) +{ + run_test (_this, "70", true); +} diff --git a/testdata/drc/drcSimpleTests_70.drc b/testdata/drc/drcSimpleTests_70.drc index 57071a3e3..28c67a740 100644 --- a/testdata/drc/drcSimpleTests_70.drc +++ b/testdata/drc/drcSimpleTests_70.drc @@ -8,28 +8,37 @@ if $drc_test_deep deep end -l1 = input(1, 0) -l2 = input(2, 0) -l3 = input(3, 0) -l4 = input(4, 0) +# properties on input +l1 = input(1, 0) +l2 = input(2, 0) +l3_wp = input(3, 0, enable_props) +l3_wp1_input = input(3, 0, select_props(1)) +l3_wp2as1_input = input(3, 0, map_props({ 2 => 1 })) +l3 = input(3, 0) +l4_wp = input(4, 0, enable_props) +l4 = input(4, 0) + +# derived properties +l3_wp1 = l3_wp.select_props(1) +l3_wp2as1 = l3_wp.map_props({ 2 => 1 }) +l3_nowp = l3_wp.remove_props + +# dump to output l1.output(1, 0) l2.output(2, 0) l3.output(3, 0) l4.output(4, 0) -l3_wp = l3.select_props -l3_wp1 = l3.select_props(1) -l3_wp2as1 = l3.map_props({ 2 => 1 }) -l3_nowp = l3_wp.remove_props - -l4_wp = l4.select_props - l3_wp.output(10, 0) l3_wp1.output(11, 0) l3_wp2as1.output(12, 0) l3_nowp.output(13, 0) -l4_wp.output(14, 0) +l3_wp1_input.output(14, 0) +l3_wp2as1_input.output(15, 0) +l4_wp.output(16, 0) + +# booleans with properties constraints l3_wp.and(l4_wp, props_eq).output(20, 0) l3_wp1.and(l4_wp, props_eq).output(21, 0) diff --git a/testdata/drc/drcSimpleTests_au70.gds b/testdata/drc/drcSimpleTests_au70.gds index 8efd555a01cc731fd090c84ae83b47d884888415..bd9736eb5166db690a3e22f62a85ce3f582167b4 100644 GIT binary patch delta 235 zcmZo_V%gWqqQt<)z{C{6Aj8PS{+xl4K@^Bp86=R|6O~kju?TOB>f)W8!(}|#Uxa6J zJ|FL79ZvPh|9Lnk-{I#6^JTd08TltCaDvDN9&aG2z-bIrA;Bmxc>$2c%?DyV0J6eC ztjz^{9k!Ea_ykP;;k{t<3!e#$lO_BECRg|^nVjRlU~)`=&t#5(fXyoc3e+ZNlz*5k wQ68{4qGAKf=7?Gc#>pJD0h?#k39wA|Xa*Y46fk*4^Mc7MngTXkw4AB|08V>G=Kufz delta 183 zcmdnj%F@)tqQt<)z{C{6Aj8PS{+xl4L4-k^L6?CMnLSZSRTzu##;7jd$!j>(Cm-jR zn4HhYJNXU2J($+vQr{de5NbP_#c#pnExrMpm-tO!oZJ$yWU@to&t#RrC6gtBd?p_W z4A`6!RG>EbNM*p}D-|C$i&Sl3nat7EA)yc?B*M?aQVeX65)ep835W$GHU!1Nh9KfV76=a#3zRr3kj?q()OT;4y0@xN zRd-ryf8(k?U!DJ|dtdh*w!?aTXLw+>-n}xchWq8c8!pp-5Bv4@$#*|K43}r;Kk?YF zJ@Ukt|LA|-`BQ)Kwr76t{Z9=KoR{Uc+wEh+u-Om8_MWxyZtq_lT2=Y%4!}?3Z_6N3iJSm_5Wq1SrhBx9W z>*(_*>GSi;pvkb44{W|LjEHeqM%><(&pjv3iby%ZCH@dod={KQ#BzUYLXHat`Dlgc zH^o%4aM(VZTCw!0-3Nn~N#vugPj}?k_90}{{^s_vo5+sDbA~O(Ts*3e6fpSdvlC(_&W~(&s=CFj8-QRF4~_`7IPYYOc|QQbCN(x9*y6mhUA?*e;dj41 z*Y0Oe8VAF6bQ}!Z(Qzqn={j#Y)-#B*>c_TEQ<3ZTitKYnsl<|x)mza$>ywAC)<<}n)QY1Rkr1w z(&R`J-_i-*usM6{Xlp%xn=;N}tF!aeeE0V!tyaUbq0U?KTBa!2RMR}h0xEmo;AD!TQ)=`XZPER}8oPJlf6y=@$x+u?j10sJ}4~$xzFUqrCSU<=e@Bs0O^F?`Q4=&2H-dKO^ zC#;A#UzDHXt$MSbnP+i6m4DIX&zLVNpRXU~$LyY+3=EsonhVAJ3GHk6mJd1>3JG#CMTdQuf+Ba-RcVNSIbhR3` zrs`VG;C{ZPYBg*}SF2%b%3p`AC#cqcw$-ZV8~hi$A!0+EX3X`6k>~k@=LKXj=Zov}e8lq-;srezw>Y25f1|^H<=$@CoZfV@ zIej~_%{q$F&FN_;o73;gwpDZv9r*ZTN~x4PQ}~)%S=TPuLD-oFv(I z)uz>V)yDN-5;>mO>fmeAZ)q1ZaXouQwK#i4wc7j%k>iPW2icnHA+z<0Ts+G+(7qL` zb^Dg}upai;^m}NR;k!}1_7oN6wNO=uIkL{##eqF5*}J+p_;=sfdq<8bHxJvAb=oeD zZ2#-#fIlKv7f06Tm7_UBvfa3oB|F=U_Dp-h;n*NY@QV4?#gUycYS#Q)rD}LrY;aJqfbc4ejrEHyfwW0^Fq~f!V9|_^C*;wdmiY3TYNOw#>Euv7Phtr*)r=Q6^@cmTR7bNcSR3(H>+?! z57!x>!r|_S6^@2;M1`fHuU1$ZP9qhThVEQpS+r^F{fr`wi#+sXRcu;(Sp)%cNoTn#u$5it|PJ;ji>_Kt?#d(Lk`UJnLLcZ)inD9QD-gRG@^`}UwT%Jd9zPSD*ulk2|#SX52 zNWPlq;+HsITt8-}WGHijsP@PCf;>Kxyvx~8kRK~UD@5cAm6OUplID+7|8i~{wkaCb zXgb-Pv2n2_UWUiZo_4Xt&JsF_&(c|kF@Bq(pt=>JsJbog-YdiBtm@HLuXfCBBD^~0 zpj*2su4-+eXN}s;I-s$2$~k2#bbyhqZtJ9*+GzH@sUOsAm3?LUh4K^$U1c(cfFz4R5cNn(GbIMZay`yfkI2g7j&mGVW)+FzqYBjpGS~|-S!}k4(spfA- z?H9CDzQT?^MzwBhQ^qZKCZpYyC8}L>?Hjf!uFBS2`-W}OP3xhh_76NaV87iy_0#W_ z%NTt=fc#4Qf`2cPE2FSGk2BjJ#9w_c9yove{epOf&tCr&I&FHAvd=<*?fBI{It)&z z_}#oB#{(uO0V|J4KptISI^n_-9IwkGZy^t_L4NZjk>hoFoP!|`S-kN(enpN?@)x3( zJ_};{-9N5qa8)rq%RzhIV19$|^wQYh@7&b5m44Ij^qQN`=|wl6)2nViT*vZ_=v?`L zMhVDqK{wy!pM|Imz$>s;j9-n< zJkw!)N6$bw6M-9NEJcn_XQO!PG4eda;m#c7a7I*A`8c2EgOSJG`*0Q$#J{mVk>i3z zdEC9n=YWvI8I#EInmq1)<8wgBL4P90Yx4M;EItQ>9QHIK$7}NO>^%g0HJ(a{a!Oe^ zn;ZXO9(U3M-c@=0Wd?ORj^C*G4^QTLNzc@X8~mrp@p}C6S=b4L-S{_=4+{BT zt4G5Zo|N{|!Fag&ob_GfGtVhtY=L+e>ebEXtUouOGauc2AupMS<@{8>(h01X?62^BJ8Mlt!{^*--Fzpg6rI+x))L8mc%C+IYW?*tiW_(J9}(@v6sC+S@? z9gpr^GsdHPmtj0X?=pPwB6}2j7LnrzdzX10MtV&U&P{jX`6+gcIG>(5j67uc#u+Pp zb6l_}kNday9Pi`i_hI6EQQmnCSCr>j2?*VJUzum8IA4@^Uh@^@asL?h^FA`~OY`g- z=Zo^rYs8{FueEp|n)kJN?G@*X@@f9Sf8^m=?@<1v@naq+>X|>z7uQey54kCxDj(;I z@~QtA2St^S^F{fvH%q7FS-o&)H^bgH=naU z-F#`hp%d$Vf6K(%%{RrnTEA|-G@qa!`vOlqao)}6tZz48ny=80eS;^SIPd10@=g6$ zsDC$Kn$OU$&=1^vX}%zTr+n1AJ4wm9I4(&VHi)E1mt#lYZEV z8EJNQY98m)4rSz>XPBZqD~F!u&dSIx+?6xyJI~(5^_^$qqP+90UX;g~&2dgG%CoYW zSv(mqljD3*9$AS>1g5^k7{>*R^3IccRi0;4o>hq)7p%%VPUTg3$JKNyZ=Q|hte;v( zGknf?y7`>-RpT>Hzj5ZfpYQm)l14vgKD+fxYXbj9eA#8SZxkg6EzEfv3&&D}HW*fc}RG#5GL1!?0 zC+Mt}?^)e((P3dbPC`U0Nh2|9z}J4t7lF&Mpb^v&A(${?-$JpzPs9Qg0hPnBs z3aNGsH(weLR6wBvxcO55fv?b!+_?dD7K1^OXh6#Mc8Imb76(l^Hi-TF-( zM7atbwZ>`g{qST$=mCU&@E?)mE&AaZdyoTy2mT{+ zyoGQ7+sX$7&y}B<5PWgo?Z4}v6uyV#J1jtXa^K~jC&=-K4t$5jWFVN%uPY;-$Oy&} zIexvB@$5lm#1kXII3mX%Y-QZ-lo3xq1mlPt-?cE}8IY7q-R9=cABzuWDIz4H5yPVe)(H%hjRw7~ed4R|Kp*!G zRpDtkIaXL2_PGiReB(ZUR(Rl>!~5l4ZfM98FTS1L+g`sXTmug4_rw!p_P3olpWYic z^4Z=voPAn(&OWWYalRuLMTx_hjNvYW*D;`*D%RnN`i`q^wZ9O8UYKHK+(^N%Xe`A3yE z&KKpwjwg0zJD=`=9Ix09x$!M_hrN$D%@x-VyPT|(?RNS%HjY=;kJkkHt6o_*>~}<2 zKh77|4?CNzlkIT&OcIV)){j>cdQ~CohCPlb>&N-x`se`3ZZ=%Xvp&PG(SY3L>=S!T z{$2l7{anCTwOc2<+7Rcf>nFQcSU>zK7tb8M)LmAvC=Y&oF5t_$ezJ?{ctt+jt-|`@ zSIx5ixQpGWuaD1R{qZ_z+O>4NBA@L}VSVf}h{L#R*l~{P)q$k+kt}J;rpXvKX)dKHGWt>e zbSH`QjFAy#gozwK80i@!BgzO9IeswGhVcZEHhl0RBgzO9IWE{3mt!M6oTW7lwtN3Y z^XJ+#!|;ZuhJna&!K5e5XpULa5?+`$d%b>xUTs2 zf{`n;QU0P8`w;u?H7&0bE3m zr#X>vy*afJzK0AD9N34698bA2-y1IE1Z)Aqw*9RMIiA{><-`x=dLdrW3-N8R(=mmtHhhpv37xqJ6AoGyO@gD!ee&`E?zT4Xqa@^_P2Tz!%GcM=?+o7W) z2hu&raCO9|$OV1qd&Yvu@hm1qF6hOYV+|5HUa7^IjaVO?(8p1Wsa`ia;#ib-+EtWC zUc!DL{DJ%=a$K+|kM&!8k@X_u3+MnYAp8nlh#VIj_s|*_a;GJyvtBmSi?xdSfL=t7 z3yykaTyHrt-bTJ7UPd1Ag3dtLQ0Sj&yp258r_J;Mk>i3z`706^#1hC_CURV`DBqFq zMt^6%7T1T~uoHOgnPRUj$~$vBm49nT{*1Ypoxu#3)0ZwTXI{FvoH0td@b9IwjP>*19A_a$GQtX_X841uo=UyIfkw-=zv3K~Tx<2Hg$2Hme3TN)3JnZIP#=VWmalxW|M;*@W zcjSI?edrJSfv~@DO$R8u$ zkv}u}#{4nz9rw(dw|~#;96DD{PqCXy>d9SztZ2!aaDecVffPT@`DHG xSw-_*em7tG+YIpFJ9&!A7w6r4CwNMZu@}FM;2U}&o