From 0e1303af8c83946a660ea1eca79108748a922130 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 24 Jun 2021 11:00:38 -0500 Subject: [PATCH 001/150] reindex vs merge --- activitysim/core/util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/activitysim/core/util.py b/activitysim/core/util.py index 0000051df7..8d6327b67f 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -258,11 +258,7 @@ def quick_loc_df(loc_list, target_df, attribute=None): if attribute: target_df = target_df[[attribute]] - df = pd.merge(left_df, - target_df, - left_on=left_on, - right_index=True, - how="left").set_index(left_on) + df = target_df.reindex(loc_list) df.index.name = target_df.index.name From 99d6f70e7ec6153b4c75fe225b3940a2c7211c53 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 07:57:11 -0500 Subject: [PATCH 002/150] start work on benchmarking --- activitysim/abm/models/initialize.py | 12 +- activitysim/abm/models/location_choice.py | 4 + activitysim/benchmarking/__init__.py | 1 + activitysim/benchmarking/componentwise.py | 155 +++++++++++++++++++++ activitysim/benchmarking/config_editing.py | 55 ++++++++ benchpress.py | 112 +++++++++++++++ 6 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 activitysim/benchmarking/__init__.py create mode 100644 activitysim/benchmarking/componentwise.py create mode 100644 activitysim/benchmarking/config_editing.py create mode 100644 benchpress.py diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index d72a88f9da..b0f05ab42a 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -184,11 +184,11 @@ def preload_injectables(): t0 = tracing.print_elapsed_time() - # FIXME - still want to do this? - # if inject.get_injectable('skim_dict', None) is not None: - # t0 = tracing.print_elapsed_time("preload skim_dict", t0, debug=True) - # - # if inject.get_injectable('skim_stack', None) is not None: - # t0 = tracing.print_elapsed_time("preload skim_stack", t0, debug=True) + if config.setting('benchmarking', False): + # we don't want to pay for skim_dict inside any model component during + # benchmarking, so we'll preload skim_dict here. Preloading is not needed + # for regular operation, as activitysim components can load-on-demand. + if inject.get_injectable('skim_dict', None) is not None: + t0 = tracing.print_elapsed_time("preload skim_dict", t0, debug=True) return True diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index 5b3b7d1e47..bb6044268a 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -886,6 +886,10 @@ def workplace_location( # if multiprocessing.current_process().name =='mp_households_0': # raise RuntimeError(f"fake fail {process_name}") + # disable locutor for benchmarking + if config.setting('benchmarking', False): + locutor = False + iterate_location_choice( model_settings, persons_merged, persons, households, diff --git a/activitysim/benchmarking/__init__.py b/activitysim/benchmarking/__init__.py new file mode 100644 index 0000000000..d962f5a53f --- /dev/null +++ b/activitysim/benchmarking/__init__.py @@ -0,0 +1 @@ +from .componentwise import benchmark_component, run_component \ No newline at end of file diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py new file mode 100644 index 0000000000..ff8d4c04bc --- /dev/null +++ b/activitysim/benchmarking/componentwise.py @@ -0,0 +1,155 @@ +import os +import logging +import numpy as np +from ..core.pipeline import print_elapsed_time, open_pipeline, mem, run_model +from ..core import inject, tracing + +logger = logging.getLogger(__name__) + + +def benchmark_component(model, resume_after=None): + """ + + Parameters + ---------- + model : str + list of model_names + resume_after : str or None + model_name of checkpoint to load checkpoint and AFTER WHICH to resume model run + + returns: + nothing, but with pipeline open + """ + + t0 = print_elapsed_time() + + open_pipeline(resume_after) + t0 = print_elapsed_time('open_pipeline', t0) + + if resume_after: + logger.info("resume_after %s" % resume_after) + + mem.trace_memory_info('pipeline.run before preload_injectables') + + # preload any bulky injectables (e.g. skims) not in pipeline + if inject.get_injectable('preload_injectables', None): + t0 = print_elapsed_time('preload_injectables', t0) + + mem.trace_memory_info('pipeline.run after preload_injectables') + + t1 = print_elapsed_time() + run_model(model) + mem.trace_memory_info(f"pipeline.run after {model}") + + tracing.log_runtime(model_name=model, start_time=t1) + + mem.trace_memory_info('pipeline.run after run_models') + + t0 = print_elapsed_time("benchmark_component (%s)" % model, t0) + + +from ..cli.run import handle_standard_args, config, warnings, cleanup_output_files, pipeline, INJECTABLES, chunk, add_run_args + +def run_component(args): + """ + Run the models. Specify a project folder using the '--working_dir' option, + or point to the config, data, and output folders directly with + '--config', '--data', and '--output'. Both '--config' and '--data' can be + specified multiple times. Directories listed first take precedence. + + returns: + int: sys.exit exit code + """ + + # register abm steps and other abm-specific injectables + # by default, assume we are running activitysim.abm + # other callers (e.g. populationsim) will have to arrange to register their own steps and injectables + # (presumably) in a custom run_simulation.py instead of using the 'activitysim run' command + if not inject.is_injectable('preload_injectables'): + from activitysim import abm # register abm steps and other abm-specific injectables + + tracing.config_logger(basic=True) + handle_standard_args(args) # possibly update injectables + + # If you provide a resume_after argument to pipeline.run + # the pipeline manager will attempt to load checkpointed tables from the checkpoint store + # and resume pipeline processing on the next submodel step after the specified checkpoint + resume_after = config.setting('resume_after', None) + + # cleanup if not resuming + if not resume_after: + cleanup_output_files() + elif config.setting('cleanup_trace_files_on_resume', False): + tracing.delete_trace_files() + + tracing.config_logger(basic=False) # update using possibly new logging configs + config.filter_warnings() + logging.captureWarnings(capture=True) + + # directories + for k in ['configs_dir', 'settings_file_name', 'data_dir', 'output_dir']: + logger.info('SETTING %s: %s' % (k, inject.get_injectable(k, None))) + + log_settings = inject.get_injectable('log_settings', {}) + for k in log_settings: + logger.info('SETTING %s: %s' % (k, config.setting(k))) + + # OMP_NUM_THREADS: openmp + # OPENBLAS_NUM_THREADS: openblas + # MKL_NUM_THREADS: mkl + for env in ['MKL_NUM_THREADS', 'OMP_NUM_THREADS', 'OPENBLAS_NUM_THREADS']: + logger.info(f"ENV {env}: {os.getenv(env)}") + + np_info_keys = [ + 'atlas_blas_info', + 'atlas_blas_threads_info', + 'atlas_info', + 'atlas_threads_info', + 'blas_info', + 'blas_mkl_info', + 'blas_opt_info', + 'lapack_info', + 'lapack_mkl_info', + 'lapack_opt_info', + 'mkl_info'] + + for cfg_key in np_info_keys: + info = np.__config__.get_info(cfg_key) + if info: + for info_key in ['libraries']: + if info_key in info: + logger.info(f"NUMPY {cfg_key} {info_key}: {info[info_key]}") + + t0 = tracing.print_elapsed_time() + + if config.setting('multiprocess', False): + logger.info('run multiprocess simulation') + + from activitysim.core import mp_tasks + injectables = {k: inject.get_injectable(k) for k in INJECTABLES} + mp_tasks.run_multiprocess(injectables) + + assert not pipeline.is_open() + + if config.setting('cleanup_pipeline_after_run', False): + pipeline.cleanup_pipeline() + + else: + logger.info('run single process simulation') + + #pipeline.run(models=config.setting('models'), resume_after=resume_after) + benchmark_component(config.setting('benchmarking'), resume_after=resume_after) + + if config.setting('cleanup_pipeline_after_run', False): + pipeline.cleanup_pipeline() # has side effect of closing open pipeline + else: + pipeline.close_pipeline() + + mem.log_global_hwm() # main process + + chunk.consolidate_logs() + mem.consolidate_logs() + + tracing.print_elapsed_time('all models', t0) + + return 0 diff --git a/activitysim/benchmarking/config_editing.py b/activitysim/benchmarking/config_editing.py new file mode 100644 index 0000000000..be5a07e3c8 --- /dev/null +++ b/activitysim/benchmarking/config_editing.py @@ -0,0 +1,55 @@ +import sys +import os +import shutil +from pathlib import Path +from ruamel.yaml import YAML + + +def copy_to_original(filename): + """ + If a ".original" does not exist, make a copy of a file to be the original. + + Parameters + ---------- + filename : Path-like + The base file to original-ize. + + Returns + ------- + basefilename, originalfilename : Path + """ + basefilename = Path(filename) + if '.original' in basefilename.stem: + basefilename = basefilename.parent / basefilename.name.replace(".original", "") + originalfilename = basefilename.parent / (basefilename.stem+".original"+basefilename.suffix) + if basefilename.exists() and not originalfilename.exists(): + shutil.copyfile(basefilename, originalfilename) + return basefilename, originalfilename + +def modify_settings(filename, changes=None, **kwargs): + """ + Modify the settings in a yaml file. + + The file to be changed is first memorialized with `copy_to_original` + before changes are applied. Repeated changes are applied repeatedly to + the original (which is not itself changed) so they do not stack. + + Parameters + ---------- + filename : Path-like + The settings file to manipulate + changes, kwargs : Mapping + Changes to apply to the file. + + """ + yaml = YAML() + doc = Path(filename) + doc, doc2 = copy_to_original(doc) + settings = yaml.load(doc2) + if changes is not None: + kwargs.update(changes) + settings.update(kwargs) + yaml.dump(settings, doc) + yaml.dump(kwargs, sys.stdout) + + diff --git a/benchpress.py b/benchpress.py new file mode 100644 index 0000000000..d66d3f650e --- /dev/null +++ b/benchpress.py @@ -0,0 +1,112 @@ +import argparse +import os +import logging + +logger = logging.getLogger("activitysim.benchmarking") + + +from activitysim.benchmarking.config_editing import modify_settings + +benchmarking_data_directory = "/tmp/asim-bench" +benchmarking_settings_mtc = dict( + households_sample_size=20_000, + benchmarking='workplace_location', +) + +model_list_mtc = [ + 'initialize_landuse', + 'initialize_households', + 'compute_accessibility', + 'school_location', + 'workplace_location', + 'auto_ownership_simulate', + 'free_parking', + 'cdap_simulate', + 'mandatory_tour_frequency', + 'mandatory_tour_scheduling', + 'joint_tour_frequency', + 'joint_tour_composition', + 'joint_tour_participation', + 'joint_tour_destination', + 'joint_tour_scheduling', + 'non_mandatory_tour_frequency', + 'non_mandatory_tour_destination', + 'non_mandatory_tour_scheduling', + 'tour_mode_choice_simulate', + 'atwork_subtour_frequency', + 'atwork_subtour_destination', + 'atwork_subtour_scheduling', + 'atwork_subtour_mode_choice', + 'stop_frequency', + 'trip_purpose', + 'trip_destination', + 'trip_purpose_and_destination', + 'trip_scheduling', + 'trip_mode_choice', + 'write_data_dictionary', + 'track_skim_usage', + 'write_trip_matrices', + 'write_tables', +] + +def pull_mtc(): + # replicate function of `activitysim create -e example_mtc_full` + from activitysim.cli.create import get_example + get_example( + "example_mtc_full", + os.path.join(benchmarking_data_directory, "example_mtc_full") + ) + +def setup_mtc_component(component_name): + pre_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)] + modify_settings( + os.path.join(benchmarking_data_directory, "example_mtc_full", "configs", "settings.yaml"), + **benchmarking_settings_mtc, + models=pre_run_model_list, + checkpoints=pre_run_model_list[-1:], + ) + from activitysim.cli.run import run, add_run_args + cmd_line_args = [ + '--config', os.path.join(benchmarking_data_directory, "example_mtc_full", "configs"), + '--data', os.path.join(benchmarking_data_directory, "example_mtc_full", "data"), + '--output', os.path.join(benchmarking_data_directory, "example_mtc_full", "output"), + ] + parser = argparse.ArgumentParser() + add_run_args(parser) + args = parser.parse_args(cmd_line_args) + return run(args) + + +def run_mtc_component(component_name): + this_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)+1] + modify_settings( + os.path.join(benchmarking_data_directory, "example_mtc_full", "configs", "settings.yaml"), + **benchmarking_settings_mtc, + checkpoints=False, + models=this_run_model_list, + resume_after=this_run_model_list[-2], + ) + from activitysim.benchmarking.componentwise import run_component, add_run_args + cmd_line_args = [ + '--config', os.path.join(benchmarking_data_directory, "example_mtc_full", "configs"), + '--data', os.path.join(benchmarking_data_directory, "example_mtc_full", "data"), + '--output', os.path.join(benchmarking_data_directory, "example_mtc_full", "output"), + ] + parser = argparse.ArgumentParser() + add_run_args(parser) + args = parser.parse_args(cmd_line_args) + return run_component(args) + + + + + +if __name__ == '__main__': + #pull_mtc() + #setup_mtc_component("workplace_location") + run_mtc_component("workplace_location") + logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + run_mtc_component("workplace_location") + From 1ea2c39b69269fd9a36c235677fa7d53b0580b0b Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 13:00:05 -0500 Subject: [PATCH 003/150] as a suite --- activitysim/benchmarking/componentwise.py | 27 ++++- activitysim/benchmarking/config_editing.py | 2 +- benchpress.py | 125 ++++++++++++--------- 3 files changed, 97 insertions(+), 57 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index ff8d4c04bc..b9ef8fb20b 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -3,6 +3,7 @@ import numpy as np from ..core.pipeline import print_elapsed_time, open_pipeline, mem, run_model from ..core import inject, tracing +from ..cli.run import handle_standard_args, config, warnings, cleanup_output_files, pipeline, INJECTABLES, chunk, add_run_args logger = logging.getLogger(__name__) @@ -48,9 +49,17 @@ def benchmark_component(model, resume_after=None): t0 = print_elapsed_time("benchmark_component (%s)" % model, t0) -from ..cli.run import handle_standard_args, config, warnings, cleanup_output_files, pipeline, INJECTABLES, chunk, add_run_args -def run_component(args): + +def reload_settings(**kwargs): + settings = config.read_settings_file('settings.yaml', mandatory=True) + for k in kwargs: + settings[k] = kwargs[k] + inject.add_injectable("settings", settings) + return settings + + +def run_component(args, component_name): """ Run the models. Specify a project folder using the '--working_dir' option, or point to the config, data, and output folders directly with @@ -60,6 +69,9 @@ def run_component(args): returns: int: sys.exit exit code """ + reload_settings( + benchmarking=component_name, + ) # register abm steps and other abm-specific injectables # by default, assume we are running activitysim.abm @@ -74,7 +86,12 @@ def run_component(args): # If you provide a resume_after argument to pipeline.run # the pipeline manager will attempt to load checkpointed tables from the checkpoint store # and resume pipeline processing on the next submodel step after the specified checkpoint - resume_after = config.setting('resume_after', None) + models = config.setting('models') + component_index = models.index(component_name) + if component_index: + resume_after = models[models.index(component_name) - 1] + else: + resume_after = None # cleanup if not resuming if not resume_after: @@ -139,6 +156,10 @@ def run_component(args): #pipeline.run(models=config.setting('models'), resume_after=resume_after) benchmark_component(config.setting('benchmarking'), resume_after=resume_after) + # pipeline.run( + # models=[resume_after, config.setting('benchmarking')], + # resume_after=resume_after, + # ) if config.setting('cleanup_pipeline_after_run', False): pipeline.cleanup_pipeline() # has side effect of closing open pipeline diff --git a/activitysim/benchmarking/config_editing.py b/activitysim/benchmarking/config_editing.py index be5a07e3c8..da80e0ec81 100644 --- a/activitysim/benchmarking/config_editing.py +++ b/activitysim/benchmarking/config_editing.py @@ -26,7 +26,7 @@ def copy_to_original(filename): shutil.copyfile(basefilename, originalfilename) return basefilename, originalfilename -def modify_settings(filename, changes=None, **kwargs): +def modify_yaml(filename, changes=None, **kwargs): """ Modify the settings in a yaml file. diff --git a/benchpress.py b/benchpress.py index d66d3f650e..f405475cfb 100644 --- a/benchpress.py +++ b/benchpress.py @@ -5,12 +5,11 @@ logger = logging.getLogger("activitysim.benchmarking") -from activitysim.benchmarking.config_editing import modify_settings +from activitysim.benchmarking.config_editing import modify_yaml benchmarking_data_directory = "/tmp/asim-bench" benchmarking_settings_mtc = dict( - households_sample_size=20_000, - benchmarking='workplace_location', + households_sample_size=1_000, ) model_list_mtc = [ @@ -49,64 +48,84 @@ 'write_tables', ] -def pull_mtc(): - # replicate function of `activitysim create -e example_mtc_full` - from activitysim.cli.create import get_example - get_example( - "example_mtc_full", - os.path.join(benchmarking_data_directory, "example_mtc_full") - ) - -def setup_mtc_component(component_name): - pre_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)] - modify_settings( - os.path.join(benchmarking_data_directory, "example_mtc_full", "configs", "settings.yaml"), - **benchmarking_settings_mtc, - models=pre_run_model_list, - checkpoints=pre_run_model_list[-1:], - ) - from activitysim.cli.run import run, add_run_args - cmd_line_args = [ - '--config', os.path.join(benchmarking_data_directory, "example_mtc_full", "configs"), - '--data', os.path.join(benchmarking_data_directory, "example_mtc_full", "data"), - '--output', os.path.join(benchmarking_data_directory, "example_mtc_full", "output"), - ] - parser = argparse.ArgumentParser() - add_run_args(parser) - args = parser.parse_args(cmd_line_args) - return run(args) - - -def run_mtc_component(component_name): - this_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)+1] - modify_settings( - os.path.join(benchmarking_data_directory, "example_mtc_full", "configs", "settings.yaml"), - **benchmarking_settings_mtc, - checkpoints=False, - models=this_run_model_list, - resume_after=this_run_model_list[-2], - ) - from activitysim.benchmarking.componentwise import run_component, add_run_args - cmd_line_args = [ - '--config', os.path.join(benchmarking_data_directory, "example_mtc_full", "configs"), - '--data', os.path.join(benchmarking_data_directory, "example_mtc_full", "data"), - '--output', os.path.join(benchmarking_data_directory, "example_mtc_full", "output"), - ] - parser = argparse.ArgumentParser() - add_run_args(parser) - args = parser.parse_args(cmd_line_args) - return run_component(args) +class BenchSuite_MTC: + + def __init__(self, component_name="workplace_location"): + self.example_name = "example_mtc_full" + self.component_name = component_name + + def pull_files(self): + # replicate function of `activitysim create -e example_mtc_full` + from activitysim.cli.create import get_example + get_example( + example_name=self.example_name, + destination=self.example_name, + ) + + def setup_cache(self): + pre_run_model_list = model_list_mtc[:model_list_mtc.index(self.component_name)] + modify_yaml( + os.path.join("example_mtc_full", "configs", "settings.yaml"), + **benchmarking_settings_mtc, + models=pre_run_model_list, + checkpoints=pre_run_model_list[-1:], + ) + modify_yaml( + os.path.join("example_mtc_full", "configs", "network_los.yaml"), + read_skim_cache=True, + ) + from activitysim.cli.run import run, add_run_args + cmd_line_args = [ + '--config', os.path.join("example_mtc_full", "configs"), + '--data', os.path.join("example_mtc_full", "data"), + '--output', os.path.join("example_mtc_full", "output"), + ] + parser = argparse.ArgumentParser() + add_run_args(parser) + args = parser.parse_args(cmd_line_args) + return run(args) + + + def time_component(self): + this_run_model_list = model_list_mtc[:model_list_mtc.index(self.component_name)+1] + modify_yaml( + os.path.join("example_mtc_full", "configs", "settings.yaml"), + **benchmarking_settings_mtc, + benchmarking='workplace_location', + checkpoints=False, + #models=this_run_model_list, + resume_after=this_run_model_list[-2], + ) + from activitysim.benchmarking.componentwise import run_component, add_run_args + cmd_line_args = [ + '--config', os.path.join("example_mtc_full", "configs"), + '--data', os.path.join("example_mtc_full", "data"), + '--output', os.path.join("example_mtc_full", "output"), + ] + parser = argparse.ArgumentParser() + add_run_args(parser) + args = parser.parse_args(cmd_line_args) + return run_component(args, self.component_name) if __name__ == '__main__': + + os.chdir(benchmarking_data_directory) + + component_name = "workplace_location" + suite = BenchSuite_MTC(component_name) + #pull_mtc() - #setup_mtc_component("workplace_location") - run_mtc_component("workplace_location") + suite.setup_cache() + logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + logger.warning("$ 0 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + suite.time_component() logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + logger.warning("$ 1 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - run_mtc_component("workplace_location") + suite.time_component() From a96c087790a2e8127bbc570f514b390ce80798fc Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 13:46:08 -0500 Subject: [PATCH 004/150] timing and setup --- activitysim/benchmarking/componentwise.py | 13 +++-- benchpress.py | 61 ++++++++++++++++++----- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index b9ef8fb20b..da41841121 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -59,7 +59,7 @@ def reload_settings(**kwargs): return settings -def run_component(args, component_name): +def prep_component(args, component_name): """ Run the models. Specify a project folder using the '--working_dir' option, or point to the config, data, and output folders directly with @@ -137,6 +137,10 @@ def run_component(args, component_name): if info_key in info: logger.info(f"NUMPY {cfg_key} {info_key}: {info[info_key]}") + return resume_after + +def run_component(component_name, resume_after): + t0 = tracing.print_elapsed_time() if config.setting('multiprocess', False): @@ -155,7 +159,7 @@ def run_component(args, component_name): logger.info('run single process simulation') #pipeline.run(models=config.setting('models'), resume_after=resume_after) - benchmark_component(config.setting('benchmarking'), resume_after=resume_after) + benchmark_component(component_name, resume_after=resume_after) # pipeline.run( # models=[resume_after, config.setting('benchmarking')], # resume_after=resume_after, @@ -166,11 +170,6 @@ def run_component(args, component_name): else: pipeline.close_pipeline() - mem.log_global_hwm() # main process - - chunk.consolidate_logs() - mem.consolidate_logs() - tracing.print_elapsed_time('all models', t0) return 0 diff --git a/benchpress.py b/benchpress.py index f405475cfb..6197644d7f 100644 --- a/benchpress.py +++ b/benchpress.py @@ -1,6 +1,9 @@ import argparse import os import logging +import time +from datetime import timedelta +from activitysim.benchmarking.componentwise import run_component, add_run_args, prep_component logger = logging.getLogger("activitysim.benchmarking") @@ -50,9 +53,17 @@ class BenchSuite_MTC: - def __init__(self, component_name="workplace_location"): - self.example_name = "example_mtc_full" - self.component_name = component_name + example_name = "example_mtc_full" + + # any settings to override in the example's usual settings file + benchmark_settings = { + 'households_sample_size': 1_000, + } + + # the component names to be benchmarked + params = [ + "workplace_location", + ] def pull_files(self): # replicate function of `activitysim create -e example_mtc_full` @@ -63,12 +74,18 @@ def pull_files(self): ) def setup_cache(self): - pre_run_model_list = model_list_mtc[:model_list_mtc.index(self.component_name)] + last_component_to_benchmark = 0 + for component_name in self.params: + last_component_to_benchmark = max( + model_list_mtc.index(component_name), + last_component_to_benchmark + ) + pre_run_model_list = model_list_mtc[:last_component_to_benchmark] modify_yaml( os.path.join("example_mtc_full", "configs", "settings.yaml"), - **benchmarking_settings_mtc, + **self.benchmark_settings, models=pre_run_model_list, - checkpoints=pre_run_model_list[-1:], + checkpoints=True, ) modify_yaml( os.path.join("example_mtc_full", "configs", "network_los.yaml"), @@ -86,8 +103,8 @@ def setup_cache(self): return run(args) - def time_component(self): - this_run_model_list = model_list_mtc[:model_list_mtc.index(self.component_name)+1] + def setup_component(self, component_name): + this_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)+1] modify_yaml( os.path.join("example_mtc_full", "configs", "settings.yaml"), **benchmarking_settings_mtc, @@ -96,7 +113,6 @@ def time_component(self): #models=this_run_model_list, resume_after=this_run_model_list[-2], ) - from activitysim.benchmarking.componentwise import run_component, add_run_args cmd_line_args = [ '--config', os.path.join("example_mtc_full", "configs"), '--data', os.path.join("example_mtc_full", "data"), @@ -105,7 +121,10 @@ def time_component(self): parser = argparse.ArgumentParser() add_run_args(parser) args = parser.parse_args(cmd_line_args) - return run_component(args, self.component_name) + self.resume_after = prep_component(args, component_name) + + def time_component(self, component_name): + return run_component(component_name, self.resume_after) @@ -113,19 +132,35 @@ def time_component(self): if __name__ == '__main__': + t0 = time.time() os.chdir(benchmarking_data_directory) component_name = "workplace_location" - suite = BenchSuite_MTC(component_name) + suite = BenchSuite_MTC() #pull_mtc() suite.setup_cache() + t1 = time.time() logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$ 0 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - suite.time_component() + suite.setup_component(component_name) + t2a = time.time() + suite.time_component(component_name) + t2b = time.time() logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$ 1 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - suite.time_component() + suite.setup_component(component_name) + t3a = time.time() + suite.time_component(component_name) + t3b = time.time() + + logger.warning(f"Time Base Setup: {timedelta(seconds=t1-t0)}") + + logger.warning(f"Time Setup 1: {timedelta(seconds=t2a-t1)}") + logger.warning(f"Time Setup 2: {timedelta(seconds=t3a-t2b)}") + + logger.warning(f"Time Run 1: {timedelta(seconds=t2b-t2a)}") + logger.warning(f"Time Run 2: {timedelta(seconds=t3b-t3a)}") From 555967a45e4745d2b0af4e4055337f84e1099a4c Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 14:09:56 -0500 Subject: [PATCH 005/150] streamlining --- activitysim/benchmarking/componentwise.py | 81 ++++++++--------------- benchpress.py | 10 ++- 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index da41841121..ab06667ceb 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -21,32 +21,12 @@ def benchmark_component(model, resume_after=None): returns: nothing, but with pipeline open """ + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") - t0 = print_elapsed_time() - - open_pipeline(resume_after) - t0 = print_elapsed_time('open_pipeline', t0) - - if resume_after: - logger.info("resume_after %s" % resume_after) - - mem.trace_memory_info('pipeline.run before preload_injectables') - - # preload any bulky injectables (e.g. skims) not in pipeline - if inject.get_injectable('preload_injectables', None): - t0 = print_elapsed_time('preload_injectables', t0) - - mem.trace_memory_info('pipeline.run after preload_injectables') - - t1 = print_elapsed_time() - run_model(model) - mem.trace_memory_info(f"pipeline.run after {model}") - - tracing.log_runtime(model_name=model, start_time=t1) - - mem.trace_memory_info('pipeline.run after run_models') - - t0 = print_elapsed_time("benchmark_component (%s)" % model, t0) + else: # single process benchmarking + open_pipeline(resume_after) + run_model(model) @@ -137,39 +117,34 @@ def prep_component(args, component_name): if info_key in info: logger.info(f"NUMPY {cfg_key} {info_key}: {info[info_key]}") - return resume_after - -def run_component(component_name, resume_after): - - t0 = tracing.print_elapsed_time() - if config.setting('multiprocess', False): - logger.info('run multiprocess simulation') - - from activitysim.core import mp_tasks - injectables = {k: inject.get_injectable(k) for k in INJECTABLES} - mp_tasks.run_multiprocess(injectables) - - assert not pipeline.is_open() - - if config.setting('cleanup_pipeline_after_run', False): - pipeline.cleanup_pipeline() - + raise NotImplementedError("multiprocess benchmarking is not yet implemented") else: - logger.info('run single process simulation') + open_pipeline(resume_after) - #pipeline.run(models=config.setting('models'), resume_after=resume_after) - benchmark_component(component_name, resume_after=resume_after) - # pipeline.run( - # models=[resume_after, config.setting('benchmarking')], - # resume_after=resume_after, - # ) +def run_component(component_name): + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + # logger.info('run multiprocess simulation') + # + # from activitysim.core import mp_tasks + # injectables = {k: inject.get_injectable(k) for k in INJECTABLES} + # mp_tasks.run_multiprocess(injectables) + # + # assert not pipeline.is_open() + # + # if config.setting('cleanup_pipeline_after_run', False): + # pipeline.cleanup_pipeline() + else: + run_model(component_name) + return 0 +def after_component(): + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + else: if config.setting('cleanup_pipeline_after_run', False): pipeline.cleanup_pipeline() # has side effect of closing open pipeline else: pipeline.close_pipeline() - - tracing.print_elapsed_time('all models', t0) - - return 0 + return 0 \ No newline at end of file diff --git a/benchpress.py b/benchpress.py index 6197644d7f..736dddc3c9 100644 --- a/benchpress.py +++ b/benchpress.py @@ -3,7 +3,7 @@ import logging import time from datetime import timedelta -from activitysim.benchmarking.componentwise import run_component, add_run_args, prep_component +from activitysim.benchmarking.componentwise import run_component, add_run_args, prep_component, after_component logger = logging.getLogger("activitysim.benchmarking") @@ -121,11 +121,13 @@ def setup_component(self, component_name): parser = argparse.ArgumentParser() add_run_args(parser) args = parser.parse_args(cmd_line_args) - self.resume_after = prep_component(args, component_name) + prep_component(args, component_name) def time_component(self, component_name): - return run_component(component_name, self.resume_after) + return run_component(component_name) + def teardown_component(self, component_name): + after_component() @@ -148,6 +150,7 @@ def time_component(self, component_name): t2a = time.time() suite.time_component(component_name) t2b = time.time() + suite.teardown_component(component_name) logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$ 1 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") @@ -155,6 +158,7 @@ def time_component(self, component_name): t3a = time.time() suite.time_component(component_name) t3b = time.time() + suite.teardown_component(component_name) logger.warning(f"Time Base Setup: {timedelta(seconds=t1-t0)}") From daaff62248025f227a604b229e1a98da27fa7828 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 15:21:21 -0500 Subject: [PATCH 006/150] feat: add optional sha256 to example manifest this can prevent upcoming benchmarking tool from pounding relentlessly on activitysim_resources repo, by caching the large file downloads --- activitysim/cli/create.py | 44 ++++++++++++++++++---- activitysim/examples/example_manifest.yaml | 6 ++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/activitysim/cli/create.py b/activitysim/cli/create.py index a0e9f638d1..577843cc9a 100644 --- a/activitysim/cli/create.py +++ b/activitysim/cli/create.py @@ -5,6 +5,7 @@ import glob import pkg_resources import yaml +import hashlib PACKAGE = 'activitysim' EXAMPLES_DIR = 'examples' @@ -116,17 +117,22 @@ def get_example(example_name, destination): # split include string into source/destination paths items = item.split() assets = items[0] - if len(items) == 2: + if len(items) == 3: + target_path = os.path.join(dest_path, items[1]) + sha256 = items[-1] + elif len(items) == 2: target_path = os.path.join(dest_path, items[-1]) + sha256 = None else: target_path = dest_path + sha256 = None if assets.startswith('http'): - download_asset(assets, target_path) + download_asset(assets, target_path, sha256) else: for asset_path in glob.glob(_example_path(assets)): - copy_asset(asset_path, target_path) + copy_asset(asset_path, target_path, dirs_exist_ok=True) print(f'copied! new project files are in {os.path.abspath(dest_path)}') @@ -135,22 +141,46 @@ def get_example(example_name, destination): print(instructions) -def copy_asset(asset_path, target_path): +def copy_asset(asset_path, target_path, dirs_exist_ok=False): print(f'copying {os.path.basename(asset_path)} ...') if os.path.isdir(asset_path): target_path = os.path.join(target_path, os.path.basename(asset_path)) - shutil.copytree(asset_path, target_path) + shutil.copytree(asset_path, target_path, dirs_exist_ok=dirs_exist_ok) else: shutil.copy(asset_path, target_path) -def download_asset(url, target_path): +def download_asset(url, target_path, sha256=None): - print(f'downloading {os.path.basename(target_path)} ...') + if sha256 and os.path.isfile(target_path): + computed_sha256 = sha256_checksum(target_path) + if sha256 == computed_sha256: + print(f'not re-downloading existing {os.path.basename(target_path)} ...') + return + else: + print(f're-downloading existing {os.path.basename(target_path)} ...') + print(f' expected checksum {sha256}') + print(f' computed checksum {computed_sha256}') + else: + print(f'downloading {os.path.basename(target_path)} ...') with requests.get(url, stream=True) as r: r.raise_for_status() with open(target_path, 'wb') as f: for chunk in r.iter_content(chunk_size=None): f.write(chunk) + computed_sha256 = sha256_checksum(target_path) + if sha256 and sha256 != computed_sha256: + raise ValueError( + f"downloaded {os.path.basename(target_path)} has incorrect checksum\n" + f" expected checksum {sha256}\n" + f" computed checksum {computed_sha256}" + ) + +def sha256_checksum(filename, block_size=65536): + sha256 = hashlib.sha256() + with open(filename, 'rb') as f: + for block in iter(lambda: f.read(block_size), b''): + sha256.update(block) + return sha256.hexdigest() diff --git a/activitysim/examples/example_manifest.yaml b/activitysim/examples/example_manifest.yaml index 0bae5f9713..400b7053f2 100644 --- a/activitysim/examples/example_manifest.yaml +++ b/activitysim/examples/example_manifest.yaml @@ -32,16 +32,20 @@ include: - example_mtc/configs - example_mtc/configs_mp - - example_mtc/data + # example_mtc/data - example_mtc/output - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/mtc_data_full/skims.omx data/skims.omx + 04bddb2dd6b829a2ce25a27369d3276143fa9a354989ebd30ed9bba92f8e9bfb - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/mtc_data_full/households.csv data/households.csv + 77bb2870677ebb430f3d7a813528445b1aea6d66096b9c03ace7201b901a527c - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/mtc_data_full/persons.csv data/persons.csv + 9b7e1c9972d5f16e06cf5baf8d552935cb4c9745c4da1d5743f8a76a8f5f5655 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/mtc_data_full/land_use.csv data/land_use.csv + fac71207925a34c32b956632fe375814e42860624a99f88401c42317af0fc203 - name: example_mtc_sf description: San Francisco MTC dataset with 190 zones, 400k households and 900k persons From 0adc2a28ef8bee459974e5bf81f108ac30e9fd2b Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 15:22:52 -0500 Subject: [PATCH 007/150] download files in setup_cache --- benchpress.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/benchpress.py b/benchpress.py index 736dddc3c9..4baa8c2e20 100644 --- a/benchpress.py +++ b/benchpress.py @@ -4,6 +4,7 @@ import time from datetime import timedelta from activitysim.benchmarking.componentwise import run_component, add_run_args, prep_component, after_component +from activitysim.cli.create import get_example logger = logging.getLogger("activitysim.benchmarking") @@ -65,15 +66,11 @@ class BenchSuite_MTC: "workplace_location", ] - def pull_files(self): - # replicate function of `activitysim create -e example_mtc_full` - from activitysim.cli.create import get_example + def setup_cache(self): get_example( example_name=self.example_name, - destination=self.example_name, + destination='.', ) - - def setup_cache(self): last_component_to_benchmark = 0 for component_name in self.params: last_component_to_benchmark = max( @@ -140,7 +137,6 @@ def teardown_component(self, component_name): component_name = "workplace_location" suite = BenchSuite_MTC() - #pull_mtc() suite.setup_cache() t1 = time.time() logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") From 0577363a071bca5cac429c6240d04b73473c3641 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 17:54:24 -0500 Subject: [PATCH 008/150] finish suite --- activitysim/benchmarking/__init__.py | 3 +- activitysim/benchmarking/componentwise.py | 150 +++++++++++----------- activitysim/cli/create.py | 1 + 3 files changed, 78 insertions(+), 76 deletions(-) diff --git a/activitysim/benchmarking/__init__.py b/activitysim/benchmarking/__init__.py index d962f5a53f..d32ac6b672 100644 --- a/activitysim/benchmarking/__init__.py +++ b/activitysim/benchmarking/__init__.py @@ -1 +1,2 @@ -from .componentwise import benchmark_component, run_component \ No newline at end of file +from .config_editing import modify_yaml +from . import componentwise \ No newline at end of file diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index ab06667ceb..9b8fcde973 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -8,29 +8,6 @@ logger = logging.getLogger(__name__) -def benchmark_component(model, resume_after=None): - """ - - Parameters - ---------- - model : str - list of model_names - resume_after : str or None - model_name of checkpoint to load checkpoint and AFTER WHICH to resume model run - - returns: - nothing, but with pipeline open - """ - if config.setting('multiprocess', False): - raise NotImplementedError("multiprocess benchmarking is not yet implemented") - - else: # single process benchmarking - open_pipeline(resume_after) - run_model(model) - - - - def reload_settings(**kwargs): settings = config.read_settings_file('settings.yaml', mandatory=True) for k in kwargs: @@ -39,47 +16,89 @@ def reload_settings(**kwargs): return settings -def prep_component(args, component_name): +def setup_component(component_name): """ - Run the models. Specify a project folder using the '--working_dir' option, - or point to the config, data, and output folders directly with - '--config', '--data', and '--output'. Both '--config' and '--data' can be - specified multiple times. Directories listed first take precedence. + Prepare to benchmark a model component. - returns: - int: sys.exit exit code + This function sets up everything, opens the pipeline, and + reloads table state from checkpoints of prior components. + All this happens here, before the model component itself + is actually executed inside the timed portion of the loop. """ reload_settings( benchmarking=component_name, ) - # register abm steps and other abm-specific injectables - # by default, assume we are running activitysim.abm - # other callers (e.g. populationsim) will have to arrange to register their own steps and injectables - # (presumably) in a custom run_simulation.py instead of using the 'activitysim run' command + # register abm steps and other abm-specific injectables outside of + # benchmark timing loop if not inject.is_injectable('preload_injectables'): - from activitysim import abm # register abm steps and other abm-specific injectables - - tracing.config_logger(basic=True) - handle_standard_args(args) # possibly update injectables + from activitysim import abm - # If you provide a resume_after argument to pipeline.run - # the pipeline manager will attempt to load checkpointed tables from the checkpoint store - # and resume pipeline processing on the next submodel step after the specified checkpoint + # Extract the resume_after argument based on the model immediately + # prior to the component being benchmarked. models = config.setting('models') - component_index = models.index(component_name) + try: + component_index = models.index(component_name) + except ValueError: + # the last component to be benchmarked isn't included in the + # pre-checkpointed model list, we just resume from the end + component_index = len(models) if component_index: - resume_after = models[models.index(component_name) - 1] + resume_after = models[component_index - 1] else: resume_after = None - # cleanup if not resuming - if not resume_after: - cleanup_output_files() - elif config.setting('cleanup_trace_files_on_resume', False): - tracing.delete_trace_files() + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + else: + open_pipeline(resume_after) + + +def run_component(component_name): + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + # logger.info('run multiprocess simulation') + # + # from activitysim.core import mp_tasks + # injectables = {k: inject.get_injectable(k) for k in INJECTABLES} + # mp_tasks.run_multiprocess(injectables) + # + # assert not pipeline.is_open() + # + # if config.setting('cleanup_pipeline_after_run', False): + # pipeline.cleanup_pipeline() + else: + run_model(component_name) + return 0 + - tracing.config_logger(basic=False) # update using possibly new logging configs +def teardown_component(): + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + else: + pipeline.close_pipeline() + return 0 + + +def pre_run(model_working_dir): + """ + Pre-run the models, checkpointing everything. + """ + inject.add_injectable('configs_dir', os.path.join(model_working_dir, 'configs')) + inject.add_injectable('data_dir', os.path.join(model_working_dir, 'data')) + inject.add_injectable('output_dir', os.path.join(model_working_dir, 'output')) + + # register abm steps and other abm-specific injectables + if not inject.is_injectable('preload_injectables'): + from activitysim import abm # register abm steps and other abm-specific injectables + + # Always pre_run from the beginning + config.override_setting('resume_after', None) + + # cleanup + cleanup_output_files() + + tracing.config_logger(basic=True) config.filter_warnings() logging.captureWarnings(capture=True) @@ -117,34 +136,15 @@ def prep_component(args, component_name): if info_key in info: logger.info(f"NUMPY {cfg_key} {info_key}: {info[info_key]}") - if config.setting('multiprocess', False): - raise NotImplementedError("multiprocess benchmarking is not yet implemented") - else: - open_pipeline(resume_after) + t0 = tracing.print_elapsed_time() -def run_component(component_name): if config.setting('multiprocess', False): raise NotImplementedError("multiprocess benchmarking is not yet implemented") - # logger.info('run multiprocess simulation') - # - # from activitysim.core import mp_tasks - # injectables = {k: inject.get_injectable(k) for k in INJECTABLES} - # mp_tasks.run_multiprocess(injectables) - # - # assert not pipeline.is_open() - # - # if config.setting('cleanup_pipeline_after_run', False): - # pipeline.cleanup_pipeline() else: - run_model(component_name) - return 0 + logger.info('run single process simulation') + pipeline.run(models=config.setting('models')) + pipeline.close_pipeline() -def after_component(): - if config.setting('multiprocess', False): - raise NotImplementedError("multiprocess benchmarking is not yet implemented") - else: - if config.setting('cleanup_pipeline_after_run', False): - pipeline.cleanup_pipeline() # has side effect of closing open pipeline - else: - pipeline.close_pipeline() - return 0 \ No newline at end of file + tracing.print_elapsed_time('prerun required models for checkpointing', t0) + + return 0 diff --git a/activitysim/cli/create.py b/activitysim/cli/create.py index 577843cc9a..2f8c4ca0f8 100644 --- a/activitysim/cli/create.py +++ b/activitysim/cli/create.py @@ -154,6 +154,7 @@ def copy_asset(asset_path, target_path, dirs_exist_ok=False): def download_asset(url, target_path, sha256=None): + os.makedirs(os.path.dirname(target_path), exist_ok=True) if sha256 and os.path.isfile(target_path): computed_sha256 = sha256_checksum(target_path) if sha256 == computed_sha256: From 341521f1c8925af5750ddba5a2a289639702b8f7 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 1 Jul 2021 17:54:42 -0500 Subject: [PATCH 009/150] ex press --- benchpress.py | 166 -------------------------------------------------- 1 file changed, 166 deletions(-) delete mode 100644 benchpress.py diff --git a/benchpress.py b/benchpress.py deleted file mode 100644 index 4baa8c2e20..0000000000 --- a/benchpress.py +++ /dev/null @@ -1,166 +0,0 @@ -import argparse -import os -import logging -import time -from datetime import timedelta -from activitysim.benchmarking.componentwise import run_component, add_run_args, prep_component, after_component -from activitysim.cli.create import get_example - -logger = logging.getLogger("activitysim.benchmarking") - - -from activitysim.benchmarking.config_editing import modify_yaml - -benchmarking_data_directory = "/tmp/asim-bench" -benchmarking_settings_mtc = dict( - households_sample_size=1_000, -) - -model_list_mtc = [ - 'initialize_landuse', - 'initialize_households', - 'compute_accessibility', - 'school_location', - 'workplace_location', - 'auto_ownership_simulate', - 'free_parking', - 'cdap_simulate', - 'mandatory_tour_frequency', - 'mandatory_tour_scheduling', - 'joint_tour_frequency', - 'joint_tour_composition', - 'joint_tour_participation', - 'joint_tour_destination', - 'joint_tour_scheduling', - 'non_mandatory_tour_frequency', - 'non_mandatory_tour_destination', - 'non_mandatory_tour_scheduling', - 'tour_mode_choice_simulate', - 'atwork_subtour_frequency', - 'atwork_subtour_destination', - 'atwork_subtour_scheduling', - 'atwork_subtour_mode_choice', - 'stop_frequency', - 'trip_purpose', - 'trip_destination', - 'trip_purpose_and_destination', - 'trip_scheduling', - 'trip_mode_choice', - 'write_data_dictionary', - 'track_skim_usage', - 'write_trip_matrices', - 'write_tables', -] - -class BenchSuite_MTC: - - example_name = "example_mtc_full" - - # any settings to override in the example's usual settings file - benchmark_settings = { - 'households_sample_size': 1_000, - } - - # the component names to be benchmarked - params = [ - "workplace_location", - ] - - def setup_cache(self): - get_example( - example_name=self.example_name, - destination='.', - ) - last_component_to_benchmark = 0 - for component_name in self.params: - last_component_to_benchmark = max( - model_list_mtc.index(component_name), - last_component_to_benchmark - ) - pre_run_model_list = model_list_mtc[:last_component_to_benchmark] - modify_yaml( - os.path.join("example_mtc_full", "configs", "settings.yaml"), - **self.benchmark_settings, - models=pre_run_model_list, - checkpoints=True, - ) - modify_yaml( - os.path.join("example_mtc_full", "configs", "network_los.yaml"), - read_skim_cache=True, - ) - from activitysim.cli.run import run, add_run_args - cmd_line_args = [ - '--config', os.path.join("example_mtc_full", "configs"), - '--data', os.path.join("example_mtc_full", "data"), - '--output', os.path.join("example_mtc_full", "output"), - ] - parser = argparse.ArgumentParser() - add_run_args(parser) - args = parser.parse_args(cmd_line_args) - return run(args) - - - def setup_component(self, component_name): - this_run_model_list = model_list_mtc[:model_list_mtc.index(component_name)+1] - modify_yaml( - os.path.join("example_mtc_full", "configs", "settings.yaml"), - **benchmarking_settings_mtc, - benchmarking='workplace_location', - checkpoints=False, - #models=this_run_model_list, - resume_after=this_run_model_list[-2], - ) - cmd_line_args = [ - '--config', os.path.join("example_mtc_full", "configs"), - '--data', os.path.join("example_mtc_full", "data"), - '--output', os.path.join("example_mtc_full", "output"), - ] - parser = argparse.ArgumentParser() - add_run_args(parser) - args = parser.parse_args(cmd_line_args) - prep_component(args, component_name) - - def time_component(self, component_name): - return run_component(component_name) - - def teardown_component(self, component_name): - after_component() - - - - -if __name__ == '__main__': - - t0 = time.time() - os.chdir(benchmarking_data_directory) - - component_name = "workplace_location" - suite = BenchSuite_MTC() - - suite.setup_cache() - t1 = time.time() - logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - logger.warning("$ 0 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - suite.setup_component(component_name) - t2a = time.time() - suite.time_component(component_name) - t2b = time.time() - suite.teardown_component(component_name) - logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - logger.warning("$ 1 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - logger.warning("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") - suite.setup_component(component_name) - t3a = time.time() - suite.time_component(component_name) - t3b = time.time() - suite.teardown_component(component_name) - - logger.warning(f"Time Base Setup: {timedelta(seconds=t1-t0)}") - - logger.warning(f"Time Setup 1: {timedelta(seconds=t2a-t1)}") - logger.warning(f"Time Setup 2: {timedelta(seconds=t3a-t2b)}") - - logger.warning(f"Time Run 1: {timedelta(seconds=t2b-t2a)}") - logger.warning(f"Time Run 2: {timedelta(seconds=t3b-t3a)}") - From 13a9eed24df65ce79474eaf4c3cedeb327c483f9 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 11:22:36 -0500 Subject: [PATCH 010/150] component logging --- activitysim/benchmarking/componentwise.py | 48 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 9b8fcde973..f98be1e1eb 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -16,7 +16,29 @@ def reload_settings(**kwargs): return settings -def setup_component(component_name): +def component_logging(component_name): + tracing.config_logger(basic=True) + import logging.handlers + logfilename = config.log_file_path(f"asv-{component_name}.log") + if os.path.exists(logfilename): + do_roll = True + else: + do_roll = False + file_handler = logging.handlers.RotatingFileHandler( + filename=logfilename, + mode='a', maxBytes=50_000_000, backupCount=10, + ) + if do_roll: + file_handler.doRollover() + formatter = logging.Formatter( + fmt='%(asctime)s %(levelname)7s - %(name)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + ) + file_handler.setFormatter(formatter) + logging.getLogger().addHandler(file_handler) + + +def setup_component(component_name, working_dir='.', preload_injectables=()): """ Prepare to benchmark a model component. @@ -25,14 +47,26 @@ def setup_component(component_name): All this happens here, before the model component itself is actually executed inside the timed portion of the loop. """ + inject.add_injectable('configs_dir', os.path.join(working_dir, 'configs')) + inject.add_injectable('data_dir', os.path.join(working_dir, 'data')) + inject.add_injectable('output_dir', os.path.join(working_dir, 'output')) + reload_settings( benchmarking=component_name, ) + component_logging(component_name) + logger.info("connected to component logger") + config.filter_warnings() + logging.captureWarnings(capture=True) + # register abm steps and other abm-specific injectables outside of # benchmark timing loop if not inject.is_injectable('preload_injectables'): + logger.info("preload_injectables yes import") from activitysim import abm + else: + logger.info("preload_injectables no import") # Extract the resume_after argument based on the model immediately # prior to the component being benchmarked. @@ -53,8 +87,15 @@ def setup_component(component_name): else: open_pipeline(resume_after) + for k in preload_injectables: + if inject.get_injectable(k, None) is not None: + logger.info("pre-loaded %s", k) + + logger.info("setup_component completed: %s", component_name) + def run_component(component_name): + logger.info("run_component: %s", component_name) if config.setting('multiprocess', False): raise NotImplementedError("multiprocess benchmarking is not yet implemented") # logger.info('run multiprocess simulation') @@ -69,14 +110,17 @@ def run_component(component_name): # pipeline.cleanup_pipeline() else: run_model(component_name) + logger.info("run_component completed: %s", component_name) return 0 -def teardown_component(): +def teardown_component(component_name): + logger.info("teardown_component: %s", component_name) if config.setting('multiprocess', False): raise NotImplementedError("multiprocess benchmarking is not yet implemented") else: pipeline.close_pipeline() + logger.info("teardown_component completed: %s", component_name) return 0 From 522d517b759d396ec5165662d1fdc1d4f4e087c7 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 11:35:52 -0500 Subject: [PATCH 011/150] more stable logging --- activitysim/benchmarking/componentwise.py | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index f98be1e1eb..f1152da142 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -1,5 +1,6 @@ import os import logging +import logging.handlers import numpy as np from ..core.pipeline import print_elapsed_time, open_pipeline, mem, run_model from ..core import inject, tracing @@ -17,21 +18,27 @@ def reload_settings(**kwargs): def component_logging(component_name): - tracing.config_logger(basic=True) - import logging.handlers + root_logger = logging.getLogger() + + CLOG_FMT = '%(asctime)s %(levelname)7s - %(name)s: %(message)s' + logfilename = config.log_file_path(f"asv-{component_name}.log") - if os.path.exists(logfilename): - do_roll = True - else: - do_roll = False + + # avoid creation of multiple file handlers for logging components + # as we will re-enter this function for every component run + for entry in root_logger.handlers: + if (isinstance(entry, logging.handlers.RotatingFileHandler)) and \ + (entry.formatter._fmt == CLOG_FMT): + entry.doRollover() + return + + tracing.config_logger(basic=True) file_handler = logging.handlers.RotatingFileHandler( filename=logfilename, mode='a', maxBytes=50_000_000, backupCount=10, ) - if do_roll: - file_handler.doRollover() formatter = logging.Formatter( - fmt='%(asctime)s %(levelname)7s - %(name)s: %(message)s', + fmt=CLOG_FMT, datefmt='%Y-%m-%d %H:%M:%S', ) file_handler.setFormatter(formatter) From 1d8ec5d81c9f3534d4a77493edcba90d8a9299fe Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 11:48:59 -0500 Subject: [PATCH 012/150] no locutor for school location in benchmarking --- activitysim/abm/models/location_choice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index bb6044268a..5dbafb2648 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -921,6 +921,10 @@ def school_location( if estimator: write_estimation_specs(estimator, model_settings, 'school_location.yaml') + # disable locutor for benchmarking + if config.setting('benchmarking', False): + locutor = False + iterate_location_choice( model_settings, persons_merged, persons, households, From 0a3db94909454bb895a0cd242bc3b86c28fc0507 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 12:34:08 -0500 Subject: [PATCH 013/150] read-only pipeline --- activitysim/benchmarking/componentwise.py | 3 ++- activitysim/core/pipeline.py | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index f1152da142..dc039a9b90 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -60,6 +60,7 @@ def setup_component(component_name, working_dir='.', preload_injectables=()): reload_settings( benchmarking=component_name, + checkpoints=False, ) component_logging(component_name) @@ -92,7 +93,7 @@ def setup_component(component_name, working_dir='.', preload_injectables=()): if config.setting('multiprocess', False): raise NotImplementedError("multiprocess benchmarking is not yet implemented") else: - open_pipeline(resume_after) + open_pipeline(resume_after, mode='r') for k in preload_injectables: if inject.get_injectable(k, None) is not None: diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 1b2240e6d7..a09f274845 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -101,7 +101,7 @@ def close_open_files(): _PIPELINE.open_files.clear() -def open_pipeline_store(overwrite=False): +def open_pipeline_store(overwrite=False, mode='a'): """ Open the pipeline checkpoint store @@ -109,6 +109,17 @@ def open_pipeline_store(overwrite=False): ---------- overwrite : bool delete file before opening (unless resuming) + mode : {'a', 'w', 'r', 'r+'}, default 'a' + ``'r'`` + Read-only; no data can be modified. + ``'w'`` + Write; a new file is created (an existing file with the same + name would be deleted). + ``'a'`` + Append; an existing file is opened for reading and writing, + and if the file does not exist it is created. + ``'r+'`` + It is similar to ``'a'``, but the file must already exist. """ if _PIPELINE.pipeline_store is not None: @@ -125,7 +136,7 @@ def open_pipeline_store(overwrite=False): print(e) logger.warning("Error removing %s: %s" % (pipeline_file_path, e)) - _PIPELINE.pipeline_store = pd.HDFStore(pipeline_file_path, mode='a') + _PIPELINE.pipeline_store = pd.HDFStore(pipeline_file_path, mode=mode) logger.debug(f"opened pipeline_store {pipeline_file_path}") @@ -487,7 +498,7 @@ def run_model(model_name): logger.info("##### skipping %s checkpoint for %s" % (step_name, model_name)) -def open_pipeline(resume_after=None): +def open_pipeline(resume_after=None, mode='a'): """ Start pipeline, either for a new run or, if resume_after, loading checkpoint from pipeline. @@ -498,6 +509,9 @@ def open_pipeline(resume_after=None): ---------- resume_after : str or None name of checkpoint to load from pipeline store + mode : {'a', 'w', 'r', 'r+'}, default 'a' + same as for typical opening of H5Store. Ignored unless resume_after + is not None. This is here to allow read-only pipeline for benchmarking. """ if is_open(): @@ -511,7 +525,7 @@ def open_pipeline(resume_after=None): if resume_after: # open existing pipeline logger.debug("open_pipeline - open existing pipeline") - open_pipeline_store(overwrite=False) + open_pipeline_store(overwrite=False, mode=mode) load_checkpoint(resume_after) else: # open new, empty pipeline From 8d75fc2dfc3e7ea4cab7e998efe89037f454ab96 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 13:17:58 -0500 Subject: [PATCH 014/150] don't write to checkpoints when reading them --- activitysim/benchmarking/componentwise.py | 2 +- activitysim/core/pipeline.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index dc039a9b90..a9430915b1 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -150,7 +150,7 @@ def pre_run(model_working_dir): # cleanup cleanup_output_files() - tracing.config_logger(basic=True) + tracing.config_logger(basic=False) config.filter_warnings() logging.captureWarnings(capture=True) diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index a09f274845..acd281c800 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -365,8 +365,11 @@ def load_checkpoint(checkpoint_name): i = checkpoints[checkpoints[CHECKPOINT_NAME] == checkpoint_name].index[0] checkpoints = checkpoints.loc[:i] + # if the store is not open in read-only mode, # write it to the store to ensure so any subsequent checkpoints are forgotten - write_df(checkpoints, CHECKPOINT_TABLE_NAME) + store = get_pipeline_store() + if store and store._mode != 'r': + write_df(checkpoints, CHECKPOINT_TABLE_NAME) except IndexError: msg = "Couldn't find checkpoint '%s' in checkpoints" % (checkpoint_name,) From 62e97acd1338942df027ffeda6aa8cfd250e0a4c Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 13:52:23 -0500 Subject: [PATCH 015/150] don't rollover for every timing --- activitysim/benchmarking/componentwise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index a9430915b1..57c7402a15 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -29,13 +29,12 @@ def component_logging(component_name): for entry in root_logger.handlers: if (isinstance(entry, logging.handlers.RotatingFileHandler)) and \ (entry.formatter._fmt == CLOG_FMT): - entry.doRollover() return tracing.config_logger(basic=True) file_handler = logging.handlers.RotatingFileHandler( filename=logfilename, - mode='a', maxBytes=50_000_000, backupCount=10, + mode='a', maxBytes=50_000_000, backupCount=5, ) formatter = logging.Formatter( fmt=CLOG_FMT, @@ -129,6 +128,7 @@ def teardown_component(component_name): else: pipeline.close_pipeline() logger.info("teardown_component completed: %s", component_name) + logger.critical("\n\n"+("~"*88)+"\n\n") return 0 From 0d497e9883cf0ade0480328f8414f49d621fb072 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 14:28:42 -0500 Subject: [PATCH 016/150] log exceptions in benchmarks --- activitysim/benchmarking/componentwise.py | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 57c7402a15..d398f7ef1d 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -103,21 +103,25 @@ def setup_component(component_name, working_dir='.', preload_injectables=()): def run_component(component_name): logger.info("run_component: %s", component_name) - if config.setting('multiprocess', False): - raise NotImplementedError("multiprocess benchmarking is not yet implemented") - # logger.info('run multiprocess simulation') - # - # from activitysim.core import mp_tasks - # injectables = {k: inject.get_injectable(k) for k in INJECTABLES} - # mp_tasks.run_multiprocess(injectables) - # - # assert not pipeline.is_open() - # - # if config.setting('cleanup_pipeline_after_run', False): - # pipeline.cleanup_pipeline() + try: + if config.setting('multiprocess', False): + raise NotImplementedError("multiprocess benchmarking is not yet implemented") + # logger.info('run multiprocess simulation') + # + # from activitysim.core import mp_tasks + # injectables = {k: inject.get_injectable(k) for k in INJECTABLES} + # mp_tasks.run_multiprocess(injectables) + # + # assert not pipeline.is_open() + # + # if config.setting('cleanup_pipeline_after_run', False): + # pipeline.cleanup_pipeline() + else: + run_model(component_name) + except Exception as err: + logger.exception("run_component exception: %s", component_name) else: - run_model(component_name) - logger.info("run_component completed: %s", component_name) + logger.info("run_component completed: %s", component_name) return 0 @@ -127,8 +131,11 @@ def teardown_component(component_name): raise NotImplementedError("multiprocess benchmarking is not yet implemented") else: pipeline.close_pipeline() - logger.info("teardown_component completed: %s", component_name) - logger.critical("\n\n"+("~"*88)+"\n\n") + logger.critical( + "teardown_component completed: %s\n\n%s\n\n", + component_name, + "~" * 88 + ) return 0 From ca11aebe8a73d23d0dfc6ab04ec9461dee39db83 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 14:57:46 -0500 Subject: [PATCH 017/150] table cleaning for mand tours freq --- activitysim/benchmarking/componentwise.py | 12 ++++++++++++ activitysim/core/pipeline.py | 11 +++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index d398f7ef1d..79ba642cea 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -8,6 +8,11 @@ logger = logging.getLogger(__name__) +TABLE_CLEANING = dict( + mandatory_tour_frequency=['tours'], + +) + def reload_settings(**kwargs): settings = config.read_settings_file('settings.yaml', mandatory=True) @@ -127,6 +132,13 @@ def run_component(component_name): def teardown_component(component_name): logger.info("teardown_component: %s", component_name) + + # any new orca tables that were created need to be dropped so the + # next benchmark run has a clean slate + for table_name in TABLE_CLEANING.get(component_name, []): + logger.info("dropping table %s", table_name) + pipeline.drop_table(table_name) + if config.setting('multiprocess', False): raise NotImplementedError("multiprocess benchmarking is not yet implemented") else: diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index acd281c800..89c5c161cc 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -81,6 +81,14 @@ def is_open(): return _PIPELINE.is_open +def is_readonly(): + if is_open(): + store = get_pipeline_store() + if store and store._mode == 'r': + return True + return False + + def pipeline_table_key(table_name, checkpoint_name): if checkpoint_name: key = f"{table_name}/{checkpoint_name}" @@ -367,8 +375,7 @@ def load_checkpoint(checkpoint_name): # if the store is not open in read-only mode, # write it to the store to ensure so any subsequent checkpoints are forgotten - store = get_pipeline_store() - if store and store._mode != 'r': + if not is_readonly(): write_df(checkpoints, CHECKPOINT_TABLE_NAME) except IndexError: From f1b2d32d518e89ed9fe7a68ce087ada51afe16b5 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 15:11:10 -0500 Subject: [PATCH 018/150] clean sweep all tables --- activitysim/benchmarking/componentwise.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 79ba642cea..a5061cfd59 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -135,7 +135,15 @@ def teardown_component(component_name): # any new orca tables that were created need to be dropped so the # next benchmark run has a clean slate - for table_name in TABLE_CLEANING.get(component_name, []): + # for table_name in TABLE_CLEANING.get(component_name, []): + # logger.info("dropping table %s", table_name) + # pipeline.drop_table(table_name) + + # use the pipeline module to clear out all the orca tables, so + # the next benchmark run has a clean slate. + # anything needed should be reloaded from the pipeline checkpoint file + pipeline_tables = pipeline.registered_tables() + for table_name in pipeline_tables: logger.info("dropping table %s", table_name) pipeline.drop_table(table_name) From 093c66381f55c8c53c0db88a388fbbc4a5b31ae0 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 16:50:09 -0500 Subject: [PATCH 019/150] benchmark cli command --- activitysim/cli/benchmark.py | 91 ++++++++++++++++++ activitysim/cli/main.py | 5 + asv.conf.json | 172 +++++++++++++++++++++++++++++++++++ benchmarks/__init__.py | 1 + benchmarks/mtc1.py | 117 ++++++++++++++++++++++++ benchmarks/workspace.py | 45 +++++++++ 6 files changed, 431 insertions(+) create mode 100644 activitysim/cli/benchmark.py create mode 100644 asv.conf.json create mode 100644 benchmarks/__init__.py create mode 100644 benchmarks/mtc1.py create mode 100644 benchmarks/workspace.py diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py new file mode 100644 index 0000000000..c399ab50c7 --- /dev/null +++ b/activitysim/cli/benchmark.py @@ -0,0 +1,91 @@ + +import re +import sys + +def benchmark(args): + """ + Compute the airspeed velocity of an unladen activitysim. + """ + # for now we simply complete a handoff to the asv tool. + # TODO: setup workspace if not defined. + from asv.main import main + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + del sys.argv[1] + sys.exit(main()) + +def add_benchmark_args(parser): + from asv.commands import common_args + parser.add_argument( + 'range', nargs='?', default=None, + help="""Range of commits to benchmark. For a git + repository, this is passed as the first argument to ``git + log``. See 'specifying ranges' section of the + `gitrevisions` manpage for more info. Also accepts the + special values 'NEW', 'ALL', 'EXISTING', and 'HASHFILE:xxx'. + 'NEW' will benchmark all commits since the latest + benchmarked on this machine. 'ALL' will benchmark all + commits in the project. 'EXISTING' will benchmark against + all commits for which there are existing benchmarks on any + machine. 'HASHFILE:xxx' will benchmark only a specific set + of hashes given in the file named 'xxx', which must have + one hash per line. By default, will benchmark the head of + each configured of the branches.""") + parser.add_argument( + "--steps", "-s", type=common_args.positive_int, default=None, + help="""Maximum number of steps to benchmark. This is + used to subsample the commits determined by range to a + reasonable number.""") + common_args.add_bench(parser) + parser.add_argument( + "--profile", "-p", action="store_true", + help="""In addition to timing, run the benchmarks through + the `cProfile` profiler and store the results.""") + common_args.add_parallel(parser) + common_args.add_show_stderr(parser) + parser.add_argument( + "--quick", "-q", action="store_true", + help="""Do a "quick" run, where each benchmark function is + run only once. This is useful to find basic errors in the + benchmark functions faster. The results are unlikely to + be useful, and thus are not saved.""") + common_args.add_environment(parser) + parser.add_argument( + "--set-commit-hash", default=None, + help="""Set the commit hash to use when recording benchmark + results. This makes results to be saved also when using an + existing environment.""") + common_args.add_launch_method(parser) + parser.add_argument( + "--dry-run", "-n", action="store_true", + default=None, + help="""Do not save any results to disk.""") + common_args.add_machine(parser) + parser.add_argument( + "--skip-existing-successful", action="store_true", + help="""Skip running benchmarks that have previous successful + results""") + parser.add_argument( + "--skip-existing-failed", action="store_true", + help="""Skip running benchmarks that have previous failed + results""") + parser.add_argument( + "--skip-existing-commits", action="store_true", + help="""Skip running benchmarks for commits that have existing + results""") + parser.add_argument( + "--skip-existing", "-k", action="store_true", + help="""Skip running benchmarks that have previous successful + or failed results""") + common_args.add_record_samples(parser) + parser.add_argument( + "--interleave-processes", action="store_true", default=False, + help="""Interleave benchmarks with multiple processes across + commits. This can avoid measurement biases from commit ordering, + can take longer.""") + parser.add_argument( + "--no-interleave-processes", action="store_false", dest="interleave_processes") + parser.add_argument( + "--no-pull", action="store_true", + help="Do not pull the repository") + return parser + diff --git a/activitysim/cli/main.py b/activitysim/cli/main.py index 113e8b9d68..a2f1ea5b6a 100644 --- a/activitysim/cli/main.py +++ b/activitysim/cli/main.py @@ -3,6 +3,7 @@ from activitysim.cli import CLI from activitysim.cli import run from activitysim.cli import create +from activitysim.cli import benchmark from activitysim import __version__, __doc__ @@ -18,4 +19,8 @@ def main(): args_func=create.add_create_args, exec_func=create.create, description=create.create.__doc__) + asim.add_subcommand(name='benchmark', + args_func=benchmark.add_benchmark_args, + exec_func=benchmark.benchmark, + description=benchmark.benchmark.__doc__) sys.exit(asim.execute()) diff --git a/asv.conf.json b/asv.conf.json new file mode 100644 index 0000000000..60acf2df8a --- /dev/null +++ b/asv.conf.json @@ -0,0 +1,172 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "activitysim", + + // The project's homepage + "project_url": "https://activitysim.github.io/", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": ".", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + // "repo_subdir": "", + + // Customizable commands for building, installing, and + // uninstalling the project. See asv.conf.json documentation. + // + // "install_command": ["in-dir={env_dir} python -mpip install {wheel_file}"], + // "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], + // "build_command": [ + // "python setup.py build", + // "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" + // ], + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "default" (for mercurial). + // "branches": ["master"], // for git + // "branches": ["default"], // for mercurial + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "conda", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + "show_commit_url": "http://github.com/ActivitySim/activitysim/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + // "pythons": ["2.7", "3.6"], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + "conda_channels": ["conda-forge"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + "matrix": { + "pyarrow": [], + "numpy": [], + "openmatrix": [], + "pandas": [], + "pyyaml": [], + "pytables": [], + "toolz": [], + "orca": [], + "psutil": [], + "requests": [], + "numba": [], + "coverage": [], + "pytest": [], + "ruamel.yaml": [], + "cytoolz": [] + }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "six": null}, // don't run without six on conda + // ], + // + // "include": [ + // // additional env for python2.7 + // {"python": "2.7", "numpy": "1.8"}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + // "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": "../activitysim-asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": "../activitysim-asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": "../activitysim-asv/html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + // "build_cache_size": 2, + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // }, + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // }, +} diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1 @@ + diff --git a/benchmarks/mtc1.py b/benchmarks/mtc1.py new file mode 100644 index 0000000000..56ec0153bc --- /dev/null +++ b/benchmarks/mtc1.py @@ -0,0 +1,117 @@ +import argparse +import os +import logging +import time +import yaml +from datetime import timedelta +from activitysim.benchmarking import componentwise, modify_yaml +from activitysim.cli.create import get_example +from .workspace import workspace + +logger = logging.getLogger("activitysim.benchmarking") + + +class BenchSuite_MTC: + + benchmarking_directory = workspace.directory + + # name of example to load from activitysim_resources + example_name = "example_mtc_full" + + # any settings to override in the example's usual settings file + benchmark_settings = { + 'households_sample_size': 100_000, + } + + # the component names to be benchmarked + params = [ + "compute_accessibility", + "school_location", + "workplace_location", + "auto_ownership_simulate", + "free_parking", + "cdap_simulate", + "mandatory_tour_frequency", + "mandatory_tour_scheduling", + "joint_tour_frequency", + "joint_tour_composition", + "joint_tour_participation", + "joint_tour_destination", + "joint_tour_scheduling", + "non_mandatory_tour_frequency", + "non_mandatory_tour_destination", + "non_mandatory_tour_scheduling", + "tour_mode_choice_simulate", + "atwork_subtour_frequency", + "atwork_subtour_destination", + "atwork_subtour_scheduling", + "atwork_subtour_mode_choice", + "stop_frequency", + "trip_purpose", + "trip_destination", + "trip_purpose_and_destination", + "trip_scheduling", + "trip_mode_choice", + ] + + param_names = ['component_name'] + + timeout = 36000.0 # ten hours + + preload_injectables = ( + 'skim_dict', + ) + + def setup_cache(self): + get_example( + example_name=self.example_name, + destination=self.local_dir, + ) + settings_filename = os.path.join(self.working_dir, "configs", "settings.yaml") + with open(settings_filename, 'rt') as f: + self.models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') + + last_component_to_benchmark = 0 + for component_name in self.params: + last_component_to_benchmark = max( + self.models.index(component_name), + last_component_to_benchmark + ) + pre_run_model_list = self.models[:last_component_to_benchmark] + modify_yaml( + os.path.join(self.working_dir, "configs", "settings.yaml"), + **self.benchmark_settings, + models=pre_run_model_list, + checkpoints=True, + trace_hh_id=None, + chunk_training_mode='off', + ) + modify_yaml( + os.path.join(self.working_dir, "configs", "network_los.yaml"), + read_skim_cache=True, + ) + componentwise.pre_run(self.working_dir) + + def setup(self, component_name): + componentwise.setup_component( + component_name, + self.working_dir, + self.preload_injectables, + ) + + def time_component(self, component_name): + return componentwise.run_component(component_name) + + def teardown(self, component_name): + componentwise.teardown_component(component_name) + + @property + def local_dir(self): + if self.benchmarking_directory is not None: + return self.benchmarking_directory + return os.getcwd() + + @property + def working_dir(self): + return os.path.join(self.local_dir, self.example_name) + diff --git a/benchmarks/workspace.py b/benchmarks/workspace.py new file mode 100644 index 0000000000..9c1ab046b4 --- /dev/null +++ b/benchmarks/workspace.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import os +import ruamel.yaml +from pathlib import Path + + +class Workspace: + """ + Stores information about the activitysim benchmarking workspace in + ~/.asv-activitysim.yaml file. + """ + + def __init__(self, directory=None): + self.directory = directory + + @staticmethod + def get_cfg_file_path(): + return os.path.expanduser('~/.asv-activitysim.yaml') + + @classmethod + def load(cls, path=None): + settings = {} + if path is None: + path = cls.get_cfg_file_path() + if os.path.isfile(path): + yaml = ruamel.yaml.YAML() + settings = yaml.load(Path(path)) + self = cls( + directory=settings.get('directory', None) + ) + if self.directory: + os.makedirs(self.directory, exist_ok=True) + return self + + def save(self, path=None): + if path is None: + path = self.get_cfg_file_path() + yaml = ruamel.yaml.YAML() + yaml.dump({ + 'directory': self.directory, + }, Path(path)) + + +workspace = Workspace.load() + From 5aa71105b548a07c17acaf132cfeb81f0b3be09f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Fri, 2 Jul 2021 22:25:41 -0500 Subject: [PATCH 020/150] benchmark test --- activitysim/cli/benchmark.py | 10 ++++++++- asv.conf.json | 2 +- benchmarks/mtc1.py | 40 +++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index c399ab50c7..a1d0ab5778 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -1,11 +1,15 @@ - +import os import re import sys +from pathlib import Path def benchmark(args): """ Compute the airspeed velocity of an unladen activitysim. """ + + repo_dir = Path(__file__).parents[2] + os.chdir(repo_dir) # for now we simply complete a handoff to the asv tool. # TODO: setup workspace if not defined. from asv.main import main @@ -15,6 +19,9 @@ def benchmark(args): def add_benchmark_args(parser): from asv.commands import common_args + parser.add_argument( + 'asv_command', nargs='?', default='run', + ) parser.add_argument( 'range', nargs='?', default=None, help="""Range of commits to benchmark. For a git @@ -87,5 +94,6 @@ def add_benchmark_args(parser): parser.add_argument( "--no-pull", action="store_true", help="Do not pull the repository") + common_args.add_global_arguments(parser, suppress_defaults=True) return parser diff --git a/asv.conf.json b/asv.conf.json index 60acf2df8a..3540765db3 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -75,7 +75,7 @@ "pyarrow": [], "numpy": [], "openmatrix": [], - "pandas": [], + "pandas": ["1.2"], "pyyaml": [], "pytables": [], "toolz": [], diff --git a/benchmarks/mtc1.py b/benchmarks/mtc1.py index 56ec0153bc..cd24e3c98a 100644 --- a/benchmarks/mtc1.py +++ b/benchmarks/mtc1.py @@ -6,7 +6,10 @@ from datetime import timedelta from activitysim.benchmarking import componentwise, modify_yaml from activitysim.cli.create import get_example -from .workspace import workspace +try: + from .workspace import workspace +except ImportError: + from workspace import workspace logger = logging.getLogger("activitysim.benchmarking") @@ -115,3 +118,38 @@ def local_dir(self): def working_dir(self): return os.path.join(self.local_dir, self.example_name) +if __name__ == '__main__': + + benchmarking_data_directory = workspace.directory or os.getcwd() + os.chdir(benchmarking_data_directory) + + t0a = time.time() + suite = BenchSuite_MTC() + suite.setup_cache() + t0b = time.time() + + timings = {} + for component_name in suite.params: + + logger.warning(f"$$$$$$$$ {component_name} #1 $$$$$$$$") + suite.setup(component_name) + t1a = time.time() + suite.time_component(component_name) + t1b = time.time() + suite.teardown(component_name) + + logger.warning(f"$$$$$$$$ {component_name} #2 $$$$$$$$") + suite.setup(component_name) + t2a = time.time() + suite.time_component(component_name) + t2b = time.time() + suite.teardown(component_name) + + timings[component_name] = ( + str(timedelta(seconds=t1b-t1a)), + str(timedelta(seconds=t2b-t2a)), + ) + + logger.warning(f"Time Base Setup: {timedelta(seconds=t0b-t0a)}") + for component_name in suite.params: + logger.warning(f"Time {component_name}: {timings[component_name]}") From 98a0642c37d61c99b63ecc62f0b115b49b1e33ed Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 10:52:20 -0500 Subject: [PATCH 021/150] better cli wrapper for asv --- activitysim/cli/benchmark.py | 162 +++++++++++++++-------------------- activitysim/cli/cli.py | 6 +- activitysim/cli/main.py | 2 +- 3 files changed, 74 insertions(+), 96 deletions(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index a1d0ab5778..9596308115 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -1,99 +1,77 @@ import os -import re import sys -from pathlib import Path -def benchmark(args): + +def make_asv_argparser(parser): """ - Compute the airspeed velocity of an unladen activitysim. + The entry point for asv. + + Most of this work is handed off to the airspeed velocity library. """ + from asv.commands import common_args, Command, util, command_order + + def help(args): + parser.print_help() + sys.exit(0) + + common_args.add_global_arguments(parser, suppress_defaults=False) + + subparsers = parser.add_subparsers( + title='benchmarking with airspeed velocity', + description='valid subcommands') + + help_parser = subparsers.add_parser( + "help", help="Display usage information") + help_parser.set_defaults(afunc=help) + + commands = dict((x.__name__, x) for x in util.iter_subclasses(Command)) + + hide_commands = ["quickstart", ] + + for command in command_order: + if str(command) in hide_commands: + continue + subparser = commands[str(command)].setup_arguments(subparsers) + common_args.add_global_arguments(subparser) + del commands[command] + + for name, command in sorted(commands.items()): + if str(command) in hide_commands: + continue + subparser = command.setup_arguments(subparsers) + common_args.add_global_arguments(subparser) + + parser.set_defaults(afunc=benchmark) + return parser, subparsers + + + +def benchmark(args): + from asv.console import log + from asv import util + + print("benchmarking activitysim...") + + log.enable(args.verbose) + + args.config = os.path.abspath(args.config) + + # Use the path to the config file as the cwd for the remainder of + # the run + dirname = os.path.dirname(args.config) + os.chdir(dirname) + + print("working directory:", dirname) + + try: + result = args.func(args) + except util.UserError as e: + log.error(str(e)) + sys.exit(1) + finally: + log.flush() - repo_dir = Path(__file__).parents[2] - os.chdir(repo_dir) - # for now we simply complete a handoff to the asv tool. - # TODO: setup workspace if not defined. - from asv.main import main - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - del sys.argv[1] - sys.exit(main()) - -def add_benchmark_args(parser): - from asv.commands import common_args - parser.add_argument( - 'asv_command', nargs='?', default='run', - ) - parser.add_argument( - 'range', nargs='?', default=None, - help="""Range of commits to benchmark. For a git - repository, this is passed as the first argument to ``git - log``. See 'specifying ranges' section of the - `gitrevisions` manpage for more info. Also accepts the - special values 'NEW', 'ALL', 'EXISTING', and 'HASHFILE:xxx'. - 'NEW' will benchmark all commits since the latest - benchmarked on this machine. 'ALL' will benchmark all - commits in the project. 'EXISTING' will benchmark against - all commits for which there are existing benchmarks on any - machine. 'HASHFILE:xxx' will benchmark only a specific set - of hashes given in the file named 'xxx', which must have - one hash per line. By default, will benchmark the head of - each configured of the branches.""") - parser.add_argument( - "--steps", "-s", type=common_args.positive_int, default=None, - help="""Maximum number of steps to benchmark. This is - used to subsample the commits determined by range to a - reasonable number.""") - common_args.add_bench(parser) - parser.add_argument( - "--profile", "-p", action="store_true", - help="""In addition to timing, run the benchmarks through - the `cProfile` profiler and store the results.""") - common_args.add_parallel(parser) - common_args.add_show_stderr(parser) - parser.add_argument( - "--quick", "-q", action="store_true", - help="""Do a "quick" run, where each benchmark function is - run only once. This is useful to find basic errors in the - benchmark functions faster. The results are unlikely to - be useful, and thus are not saved.""") - common_args.add_environment(parser) - parser.add_argument( - "--set-commit-hash", default=None, - help="""Set the commit hash to use when recording benchmark - results. This makes results to be saved also when using an - existing environment.""") - common_args.add_launch_method(parser) - parser.add_argument( - "--dry-run", "-n", action="store_true", - default=None, - help="""Do not save any results to disk.""") - common_args.add_machine(parser) - parser.add_argument( - "--skip-existing-successful", action="store_true", - help="""Skip running benchmarks that have previous successful - results""") - parser.add_argument( - "--skip-existing-failed", action="store_true", - help="""Skip running benchmarks that have previous failed - results""") - parser.add_argument( - "--skip-existing-commits", action="store_true", - help="""Skip running benchmarks for commits that have existing - results""") - parser.add_argument( - "--skip-existing", "-k", action="store_true", - help="""Skip running benchmarks that have previous successful - or failed results""") - common_args.add_record_samples(parser) - parser.add_argument( - "--interleave-processes", action="store_true", default=False, - help="""Interleave benchmarks with multiple processes across - commits. This can avoid measurement biases from commit ordering, - can take longer.""") - parser.add_argument( - "--no-interleave-processes", action="store_false", dest="interleave_processes") - parser.add_argument( - "--no-pull", action="store_true", - help="Do not pull the repository") - common_args.add_global_arguments(parser, suppress_defaults=True) - return parser + if result is None: + result = 0 + sys.exit(result) diff --git a/activitysim/cli/cli.py b/activitysim/cli/cli.py index 1eff88406f..5756490db7 100644 --- a/activitysim/cli/cli.py +++ b/activitysim/cli/cli.py @@ -12,7 +12,7 @@ def __init__(self, version, description): version=self.version) # print help if no subcommand is provided - self.parser.set_defaults(func=lambda x: self.parser.print_help()) + self.parser.set_defaults(afunc=lambda x: self.parser.print_help()) self.subparsers = self.parser.add_subparsers(title='subcommands', help='available subcommand options') @@ -20,8 +20,8 @@ def __init__(self, version, description): def add_subcommand(self, name, args_func, exec_func, description): subparser = self.subparsers.add_parser(name, description=description) args_func(subparser) - subparser.set_defaults(func=exec_func) + subparser.set_defaults(afunc=exec_func) def execute(self): args = self.parser.parse_args() - args.func(args) + args.afunc(args) diff --git a/activitysim/cli/main.py b/activitysim/cli/main.py index a2f1ea5b6a..8f18931be5 100644 --- a/activitysim/cli/main.py +++ b/activitysim/cli/main.py @@ -20,7 +20,7 @@ def main(): exec_func=create.create, description=create.create.__doc__) asim.add_subcommand(name='benchmark', - args_func=benchmark.add_benchmark_args, + args_func=benchmark.make_asv_argparser, exec_func=benchmark.benchmark, description=benchmark.benchmark.__doc__) sys.exit(asim.execute()) From fad157225afe2e985281a94c100a7679623b376a Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 15:12:18 -0500 Subject: [PATCH 022/150] locally manage json --- .../benchmarking/asv.conf.json | 0 activitysim/cli/benchmark.py | 126 ++++++++++++++++-- 2 files changed, 116 insertions(+), 10 deletions(-) rename asv.conf.json => activitysim/benchmarking/asv.conf.json (100%) diff --git a/asv.conf.json b/activitysim/benchmarking/asv.conf.json similarity index 100% rename from asv.conf.json rename to activitysim/benchmarking/asv.conf.json diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 9596308115..838181f013 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -1,6 +1,75 @@ import os import sys - +import json + +ASV_CONFIG = { + # The version of the config file format. Do not change, unless + # you know what you are doing. + "version": 1, + + # The name of the project being benchmarked + "project": "activitysim", + + #The project's homepage + "project_url": "https://activitysim.github.io/", + + # The URL or local path of the source code repository for the + # project being benchmarked + "repo": ".", + + # The tool to use to create environments. + "environment_type": "conda", + + # the base URL to show a commit for the project. + "show_commit_url": "http://github.com/ActivitySim/activitysim/commit/", + + # The Pythons you'd like to test against. If not provided, defaults + # to the current version of Python used to run `asv`. + # "pythons": ["2.7", "3.6"], + + # The list of conda channel names to be searched for benchmark + # dependency packages in the specified order + "conda_channels": ["conda-forge"], + + # The matrix of dependencies to test. Each key is the name of a + # package (in PyPI) and the values are version numbers. An empty + # list or empty string indicates to just test against the default + # (latest) version. null indicates that the package is to not be + # installed. If the package to be tested is only available from + # PyPi, and the 'environment_type' is conda, then you can preface + # the package name by 'pip+', and the package will be installed via + # pip (with all the conda available packages installed first, + # followed by the pip installed packages). + "matrix": { + "pyarrow": [], + "numpy": [], + "openmatrix": [], + "pandas": ["1.2"], + "pyyaml": [], + "pytables": [], + "toolz": [], + "orca": [], + "psutil": [], + "requests": [], + "numba": [], + "coverage": [], + "pytest": [], + "ruamel.yaml": [], + "cytoolz": [] + }, + + # The directory (relative to the current directory) to cache the Python + # environments in. If not provided, defaults to "env" + "env_dir": "../activitysim-asv/env", + + # The directory (relative to the current directory) that raw benchmark + # results are stored in. If not provided, defaults to "results". + "results_dir": "../activitysim-asv/results", + + # The directory (relative to the current directory) that the html tree + # should be written to. If not provided, defaults to "html". + "html_dir": "../activitysim-asv/html", +} def make_asv_argparser(parser): """ @@ -33,12 +102,22 @@ def help(args): continue subparser = commands[str(command)].setup_arguments(subparsers) common_args.add_global_arguments(subparser) + subparser.add_argument( + "--workspace", "-w", + help="benchmarking workspace directory", + default=".", + ) del commands[command] for name, command in sorted(commands.items()): if str(command) in hide_commands: continue subparser = command.setup_arguments(subparsers) + subparser.add_argument( + "--workspace", "-w", + help="benchmarking workspace directory", + default=".", + ) common_args.add_global_arguments(subparser) parser.set_defaults(afunc=benchmark) @@ -50,18 +129,45 @@ def benchmark(args): from asv.console import log from asv import util - print("benchmarking activitysim...") - log.enable(args.verbose) - args.config = os.path.abspath(args.config) - - # Use the path to the config file as the cwd for the remainder of - # the run - dirname = os.path.dirname(args.config) - os.chdir(dirname) + log.info("<== benchmarking activitysim ==>") + + # workspace + args.workspace = os.path.abspath(args.workspace) + if not os.path.isdir(args.workspace): + raise NotADirectoryError(args.workspace) + log.info(f" workspace: {args.workspace}") + os.chdir(args.workspace) + + from .. import __path__ as pkg_path + log.info(f" activitysim installation: {pkg_path[0]}") + + repo_dir = os.path.normpath( + os.path.join(pkg_path[0], "..") + ) + git_dir = os.path.normpath( + os.path.join(repo_dir, ".git") + ) + local_git = os.path.exists(git_dir) + log.info(f" local git repo available: {local_git}") + + asv_config = ASV_CONFIG.copy() + if local_git: + asv_config["repo"] = os.path.relpath(args.workspace, repo_dir) + else: + asv_config["repo"] = "https://github.com/ActivitySim/activitysim.git" + + conf_file = os.path.normpath( + os.path.join(args.workspace, "asv.conf.json") + ) + with open(conf_file, 'wt') as jf: + json.dump(asv_config, jf) + + if args.config and args.config != "asv.conf.json": + raise ValueError("activitysim manages the asv config json file itself, do not use --config") + args.config = os.path.abspath(conf_file) - print("working directory:", dirname) try: result = args.func(args) From 1ba82e6b45530c73989f33966adc838a8ade180a Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 15:45:49 -0500 Subject: [PATCH 023/150] better workspace --- activitysim/benchmarking/workspace.py | 14 +++++ activitysim/cli/benchmark.py | 10 +++ benchmarks/mtc1.py | 16 ++--- benchmarks/workspace.py | 91 ++++++++++++++------------- 4 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 activitysim/benchmarking/workspace.py diff --git a/activitysim/benchmarking/workspace.py b/activitysim/benchmarking/workspace.py new file mode 100644 index 0000000000..3a741b4f31 --- /dev/null +++ b/activitysim/benchmarking/workspace.py @@ -0,0 +1,14 @@ +import os +_directory = os.getcwd() + +def get_dir(): + global _directory + return _directory + +def set_dir(directory): + global _directory + if directory: + _directory = directory + else: + _directory = os.getcwd() + diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 838181f013..c785bae0b3 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -135,11 +135,21 @@ def benchmark(args): # workspace args.workspace = os.path.abspath(args.workspace) + + if os.path.abspath(os.path.expanduser("~")) == args.workspace: + log.error("don't run benchmarks in the user's home directory \n" + "try changing directories before calling `activitysim benchmark` " + "or use the --workspace option \n") + sys.exit(1) + if not os.path.isdir(args.workspace): raise NotADirectoryError(args.workspace) log.info(f" workspace: {args.workspace}") os.chdir(args.workspace) + from ..benchmarking import workspace + workspace.set_dir(args.workspace) + from .. import __path__ as pkg_path log.info(f" activitysim installation: {pkg_path[0]}") diff --git a/benchmarks/mtc1.py b/benchmarks/mtc1.py index cd24e3c98a..3bc12fa226 100644 --- a/benchmarks/mtc1.py +++ b/benchmarks/mtc1.py @@ -4,19 +4,15 @@ import time import yaml from datetime import timedelta -from activitysim.benchmarking import componentwise, modify_yaml +from activitysim.benchmarking import componentwise, modify_yaml, workspace from activitysim.cli.create import get_example -try: - from .workspace import workspace -except ImportError: - from workspace import workspace logger = logging.getLogger("activitysim.benchmarking") class BenchSuite_MTC: - benchmarking_directory = workspace.directory + benchmarking_directory = workspace.get_dir() # name of example to load from activitysim_resources example_name = "example_mtc_full" @@ -60,6 +56,12 @@ class BenchSuite_MTC: param_names = ['component_name'] timeout = 36000.0 # ten hours + repeat = ( + 2, # min_repeat + 10, # max_repeat + 20.0, # max_time in seconds + ) + number = 1 preload_injectables = ( 'skim_dict', @@ -120,7 +122,7 @@ def working_dir(self): if __name__ == '__main__': - benchmarking_data_directory = workspace.directory or os.getcwd() + benchmarking_data_directory = workspace.get_dir() os.chdir(benchmarking_data_directory) t0a = time.time() diff --git a/benchmarks/workspace.py b/benchmarks/workspace.py index 9c1ab046b4..910485d4f8 100644 --- a/benchmarks/workspace.py +++ b/benchmarks/workspace.py @@ -1,45 +1,46 @@ -# -*- coding: utf-8 -*- -import os -import ruamel.yaml -from pathlib import Path - - -class Workspace: - """ - Stores information about the activitysim benchmarking workspace in - ~/.asv-activitysim.yaml file. - """ - - def __init__(self, directory=None): - self.directory = directory - - @staticmethod - def get_cfg_file_path(): - return os.path.expanduser('~/.asv-activitysim.yaml') - - @classmethod - def load(cls, path=None): - settings = {} - if path is None: - path = cls.get_cfg_file_path() - if os.path.isfile(path): - yaml = ruamel.yaml.YAML() - settings = yaml.load(Path(path)) - self = cls( - directory=settings.get('directory', None) - ) - if self.directory: - os.makedirs(self.directory, exist_ok=True) - return self - - def save(self, path=None): - if path is None: - path = self.get_cfg_file_path() - yaml = ruamel.yaml.YAML() - yaml.dump({ - 'directory': self.directory, - }, Path(path)) - - -workspace = Workspace.load() - +# # -*- coding: utf-8 -*- +# # import os +# # import ruamel.yaml +# # from pathlib import Path +# # +# # +# # class Workspace: +# # """ +# # Stores information about the activitysim benchmarking workspace in +# # ~/.asv-activitysim.yaml file. +# # """ +# # +# # def __init__(self, directory=None): +# # self.directory = directory +# # +# # @staticmethod +# # def get_cfg_file_path(): +# # return os.path.expanduser('~/.asv-activitysim.yaml') +# # +# # @classmethod +# # def load(cls, path=None): +# # settings = {} +# # if path is None: +# # path = cls.get_cfg_file_path() +# # if os.path.isfile(path): +# # yaml = ruamel.yaml.YAML() +# # settings = yaml.load(Path(path)) +# # self = cls( +# # directory=settings.get('directory', None) +# # ) +# # if self.directory: +# # os.makedirs(self.directory, exist_ok=True) +# # return self +# # +# # def save(self, path=None): +# # if path is None: +# # path = self.get_cfg_file_path() +# # yaml = ruamel.yaml.YAML() +# # yaml.dump({ +# # 'directory': self.directory, +# # }, Path(path)) +# # +# # +# # workspace = Workspace.load() +# +# directory = None \ No newline at end of file From 1c1990262db0f05abf76e3d3cf4942e7b93901f7 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 16:08:03 -0500 Subject: [PATCH 024/150] benchmarks are in the package --- .../benchmarking/benchmarks}/__init__.py | 0 .../benchmarking/benchmarks}/mtc1.py | 16 +++---- activitysim/cli/benchmark.py | 11 +++++ activitysim/cli/run.py | 4 +- benchmarks/workspace.py | 46 ------------------- 5 files changed, 21 insertions(+), 56 deletions(-) rename {benchmarks => activitysim/benchmarking/benchmarks}/__init__.py (100%) rename {benchmarks => activitysim/benchmarking/benchmarks}/mtc1.py (90%) delete mode 100644 benchmarks/workspace.py diff --git a/benchmarks/__init__.py b/activitysim/benchmarking/benchmarks/__init__.py similarity index 100% rename from benchmarks/__init__.py rename to activitysim/benchmarking/benchmarks/__init__.py diff --git a/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py similarity index 90% rename from benchmarks/mtc1.py rename to activitysim/benchmarking/benchmarks/mtc1.py index 3bc12fa226..f207323951 100644 --- a/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -70,9 +70,9 @@ class BenchSuite_MTC: def setup_cache(self): get_example( example_name=self.example_name, - destination=self.local_dir, + destination=os.path.join(self.local_dir, "models"), ) - settings_filename = os.path.join(self.working_dir, "configs", "settings.yaml") + settings_filename = os.path.join(self.model_dir, "configs", "settings.yaml") with open(settings_filename, 'rt') as f: self.models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') @@ -84,7 +84,7 @@ def setup_cache(self): ) pre_run_model_list = self.models[:last_component_to_benchmark] modify_yaml( - os.path.join(self.working_dir, "configs", "settings.yaml"), + os.path.join(self.model_dir, "configs", "settings.yaml"), **self.benchmark_settings, models=pre_run_model_list, checkpoints=True, @@ -92,15 +92,15 @@ def setup_cache(self): chunk_training_mode='off', ) modify_yaml( - os.path.join(self.working_dir, "configs", "network_los.yaml"), + os.path.join(self.model_dir, "configs", "network_los.yaml"), read_skim_cache=True, ) - componentwise.pre_run(self.working_dir) + componentwise.pre_run(self.model_dir) def setup(self, component_name): componentwise.setup_component( component_name, - self.working_dir, + self.model_dir, self.preload_injectables, ) @@ -117,8 +117,8 @@ def local_dir(self): return os.getcwd() @property - def working_dir(self): - return os.path.join(self.local_dir, self.example_name) + def model_dir(self): + return os.path.join(self.local_dir, "models", self.example_name) if __name__ == '__main__': diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index c785bae0b3..6d4c9935b7 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -1,6 +1,7 @@ import os import sys import json +import shutil ASV_CONFIG = { # The version of the config file format. Do not change, unless @@ -168,6 +169,16 @@ def benchmark(args): else: asv_config["repo"] = "https://github.com/ActivitySim/activitysim.git" + # copy the benchmarks to the workspace + import activitysim.benchmarking.benchmarks + benchmarks_dir = os.path.dirname(activitysim.benchmarking.benchmarks.__file__) + shutil.copytree( + benchmarks_dir, + os.path.join(args.workspace, "benchmarks"), + dirs_exist_ok=True, + ) + + # write the asv config to the workspace conf_file = os.path.normpath( os.path.join(args.workspace, "asv.conf.json") ) diff --git a/activitysim/cli/run.py b/activitysim/cli/run.py index 272a8d61fd..5ca6f10f04 100644 --- a/activitysim/cli/run.py +++ b/activitysim/cli/run.py @@ -88,10 +88,10 @@ def inject_arg(name, value): assert name in INJECTABLES inject.add_injectable(name, value) - if args.working_dir: + if args.model_dir: # activitysim will look in the current working directory for # 'configs', 'data', and 'output' folders by default - os.chdir(args.working_dir) + os.chdir(args.model_dir) if args.settings_file: inject_arg('settings_file_name', args.settings_file) diff --git a/benchmarks/workspace.py b/benchmarks/workspace.py deleted file mode 100644 index 910485d4f8..0000000000 --- a/benchmarks/workspace.py +++ /dev/null @@ -1,46 +0,0 @@ -# # -*- coding: utf-8 -*- -# # import os -# # import ruamel.yaml -# # from pathlib import Path -# # -# # -# # class Workspace: -# # """ -# # Stores information about the activitysim benchmarking workspace in -# # ~/.asv-activitysim.yaml file. -# # """ -# # -# # def __init__(self, directory=None): -# # self.directory = directory -# # -# # @staticmethod -# # def get_cfg_file_path(): -# # return os.path.expanduser('~/.asv-activitysim.yaml') -# # -# # @classmethod -# # def load(cls, path=None): -# # settings = {} -# # if path is None: -# # path = cls.get_cfg_file_path() -# # if os.path.isfile(path): -# # yaml = ruamel.yaml.YAML() -# # settings = yaml.load(Path(path)) -# # self = cls( -# # directory=settings.get('directory', None) -# # ) -# # if self.directory: -# # os.makedirs(self.directory, exist_ok=True) -# # return self -# # -# # def save(self, path=None): -# # if path is None: -# # path = self.get_cfg_file_path() -# # yaml = ruamel.yaml.YAML() -# # yaml.dump({ -# # 'directory': self.directory, -# # }, Path(path)) -# # -# # -# # workspace = Workspace.load() -# -# directory = None \ No newline at end of file From 48f4340c1288a59e2a6c07797a1e0c756f6d9256 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 18:02:04 -0500 Subject: [PATCH 025/150] local dir --- activitysim/benchmarking/workspace.py | 7 ++++--- activitysim/cli/benchmark.py | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/activitysim/benchmarking/workspace.py b/activitysim/benchmarking/workspace.py index 3a741b4f31..b346d35070 100644 --- a/activitysim/benchmarking/workspace.py +++ b/activitysim/benchmarking/workspace.py @@ -1,8 +1,10 @@ import os -_directory = os.getcwd() +_directory = None def get_dir(): global _directory + if _directory is None and "ASV_CONF_DIR" in os.environ: + _directory = os.environ.get("ASV_CONF_DIR", None) return _directory def set_dir(directory): @@ -10,5 +12,4 @@ def set_dir(directory): if directory: _directory = directory else: - _directory = os.getcwd() - + _directory = os.environ.get("ASV_CONF_DIR", None) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 6d4c9935b7..2034db89b0 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -61,15 +61,15 @@ # The directory (relative to the current directory) to cache the Python # environments in. If not provided, defaults to "env" - "env_dir": "../activitysim-asv/env", + # "env_dir": "../activitysim-asv/env", # The directory (relative to the current directory) that raw benchmark # results are stored in. If not provided, defaults to "results". - "results_dir": "../activitysim-asv/results", + # "results_dir": "../activitysim-asv/results", # The directory (relative to the current directory) that the html tree # should be written to. If not provided, defaults to "html". - "html_dir": "../activitysim-asv/html", + # "html_dir": "../activitysim-asv/html", } def make_asv_argparser(parser): @@ -165,8 +165,11 @@ def benchmark(args): asv_config = ASV_CONFIG.copy() if local_git: - asv_config["repo"] = os.path.relpath(args.workspace, repo_dir) + repo_dir_rel = os.path.relpath(repo_dir, args.workspace) + log.info(f" local git repo: {repo_dir_rel}") + asv_config["repo"] = repo_dir_rel else: + log.info(f" local git repo available: {local_git}") asv_config["repo"] = "https://github.com/ActivitySim/activitysim.git" # copy the benchmarks to the workspace From 52dd0e762d00503543c8eab810d01939c833a2dd Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 18:13:47 -0500 Subject: [PATCH 026/150] ASIM_ASV_WORKSPACE --- activitysim/benchmarking/benchmarks/mtc1.py | 5 +++++ activitysim/benchmarking/workspace.py | 6 ++++-- activitysim/cli/benchmark.py | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index f207323951..eb0093e0f7 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -68,6 +68,11 @@ class BenchSuite_MTC: ) def setup_cache(self): + if workspace.get_dir() is None: + from asv.console import log + for k,v in os.environ.items(): + log.error(f" env {k}: {v}") + raise RuntimeError("workspace unavailable") get_example( example_name=self.example_name, destination=os.path.join(self.local_dir, "models"), diff --git a/activitysim/benchmarking/workspace.py b/activitysim/benchmarking/workspace.py index b346d35070..877167cb4f 100644 --- a/activitysim/benchmarking/workspace.py +++ b/activitysim/benchmarking/workspace.py @@ -3,7 +3,9 @@ def get_dir(): global _directory - if _directory is None and "ASV_CONF_DIR" in os.environ: + if _directory is None: + _directory = os.environ.get("ASIM_ASV_WORKSPACE", None) + if _directory is None: _directory = os.environ.get("ASV_CONF_DIR", None) return _directory @@ -12,4 +14,4 @@ def set_dir(directory): if directory: _directory = directory else: - _directory = os.environ.get("ASV_CONF_DIR", None) + _directory = os.environ.get("ASIM_ASV_WORKSPACE", None) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 2034db89b0..b6a17bb8ab 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -147,6 +147,7 @@ def benchmark(args): raise NotADirectoryError(args.workspace) log.info(f" workspace: {args.workspace}") os.chdir(args.workspace) + os.environ["ASIM_ASV_WORKSPACE"] = str(args.workspace) from ..benchmarking import workspace workspace.set_dir(args.workspace) From a060c71cd80c3fb674fe9423301d202eeeb644f5 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 18:35:33 -0500 Subject: [PATCH 027/150] fix dir for download --- activitysim/benchmarking/benchmarks/mtc1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index eb0093e0f7..ec898d2103 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -75,7 +75,7 @@ def setup_cache(self): raise RuntimeError("workspace unavailable") get_example( example_name=self.example_name, - destination=os.path.join(self.local_dir, "models"), + destination=self.model_dir, ) settings_filename = os.path.join(self.model_dir, "configs", "settings.yaml") with open(settings_filename, 'rt') as f: From fa4600d1e8947fba802f6a1a2bfc3b84054764de Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 18:52:44 -0500 Subject: [PATCH 028/150] fix models dir --- activitysim/benchmarking/benchmarks/mtc1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index ec898d2103..82ef0214b7 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -73,9 +73,10 @@ def setup_cache(self): for k,v in os.environ.items(): log.error(f" env {k}: {v}") raise RuntimeError("workspace unavailable") + os.makedirs(os.path.join(self.local_dir, "models"), exist_ok=True) get_example( example_name=self.example_name, - destination=self.model_dir, + destination=os.path.join(self.local_dir, "models"), ) settings_filename = os.path.join(self.model_dir, "configs", "settings.yaml") with open(settings_filename, 'rt') as f: From 79f672395a4386208bb93ba3c96404646d9853c3 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 18:52:57 -0500 Subject: [PATCH 029/150] single thread flags --- activitysim/cli/main.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/activitysim/cli/main.py b/activitysim/cli/main.py index 8f18931be5..323122c003 100644 --- a/activitysim/cli/main.py +++ b/activitysim/cli/main.py @@ -1,14 +1,25 @@ import sys +import os -from activitysim.cli import CLI -from activitysim.cli import run -from activitysim.cli import create -from activitysim.cli import benchmark -from activitysim import __version__, __doc__ +def main(): + # set all these before we import numpy or any other math library + if len(sys.argv) > 1 and sys.argv[1] == "benchmark": + os.environ['MKL_NUM_THREADS'] = '1' + os.environ['OMP_NUM_THREADS'] = '1' + os.environ['OPENBLAS_NUM_THREADS'] = '1' + os.environ['NUMBA_NUM_THREADS'] = '1' + os.environ['VECLIB_MAXIMUM_THREADS'] = '1' + os.environ['NUMEXPR_NUM_THREADS'] = '1' + + from activitysim.cli import CLI + from activitysim.cli import run + from activitysim.cli import create + from activitysim.cli import benchmark + + from activitysim import __version__, __doc__ -def main(): asim = CLI(version=__version__, description=__doc__) asim.add_subcommand(name='run', From 1754103781818bfbb8859ffad285ec5c4207de69 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 19:58:53 -0500 Subject: [PATCH 030/150] allow asv to be missing --- activitysim/cli/benchmark.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index b6a17bb8ab..79ae039902 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -78,7 +78,11 @@ def make_asv_argparser(parser): Most of this work is handed off to the airspeed velocity library. """ - from asv.commands import common_args, Command, util, command_order + try: + from asv.commands import common_args, Command, util, command_order + except ImportError: + return + def help(args): parser.print_help() @@ -127,6 +131,12 @@ def help(args): def benchmark(args): + try: + import asv + except ModuleNotFoundError: + print("airspeed velocity is not installed") + print("try `conda install asv -c conda-forge` if you want to run benchmarks") + sys.exit(1) from asv.console import log from asv import util From b8ce055d6b0590115cfb5078b696f2fb393d87a3 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 4 Jul 2021 20:26:13 -0500 Subject: [PATCH 031/150] fix args --- activitysim/cli/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitysim/cli/run.py b/activitysim/cli/run.py index 5ca6f10f04..272a8d61fd 100644 --- a/activitysim/cli/run.py +++ b/activitysim/cli/run.py @@ -88,10 +88,10 @@ def inject_arg(name, value): assert name in INJECTABLES inject.add_injectable(name, value) - if args.model_dir: + if args.working_dir: # activitysim will look in the current working directory for # 'configs', 'data', and 'output' folders by default - os.chdir(args.model_dir) + os.chdir(args.working_dir) if args.settings_file: inject_arg('settings_file_name', args.settings_file) From 881b56c2729f23fbd2ed4596577028b9daf1962a Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 11:53:09 -0500 Subject: [PATCH 032/150] change suite to module --- activitysim/benchmarking/benchmarks/mtc1.py | 220 ++++++++++---------- activitysim/cli/benchmark.py | 4 + 2 files changed, 118 insertions(+), 106 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 82ef0214b7..290a03486a 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -1,4 +1,5 @@ import argparse +import sys import os import logging import time @@ -10,121 +11,128 @@ logger = logging.getLogger("activitysim.benchmarking") -class BenchSuite_MTC: - - benchmarking_directory = workspace.get_dir() - - # name of example to load from activitysim_resources - example_name = "example_mtc_full" - - # any settings to override in the example's usual settings file - benchmark_settings = { - 'households_sample_size': 100_000, - } - - # the component names to be benchmarked - params = [ - "compute_accessibility", - "school_location", - "workplace_location", - "auto_ownership_simulate", - "free_parking", - "cdap_simulate", - "mandatory_tour_frequency", - "mandatory_tour_scheduling", - "joint_tour_frequency", - "joint_tour_composition", - "joint_tour_participation", - "joint_tour_destination", - "joint_tour_scheduling", - "non_mandatory_tour_frequency", - "non_mandatory_tour_destination", - "non_mandatory_tour_scheduling", - "tour_mode_choice_simulate", - "atwork_subtour_frequency", - "atwork_subtour_destination", - "atwork_subtour_scheduling", - "atwork_subtour_mode_choice", - "stop_frequency", - "trip_purpose", - "trip_destination", - "trip_purpose_and_destination", - "trip_scheduling", - "trip_mode_choice", - ] - - param_names = ['component_name'] - - timeout = 36000.0 # ten hours - repeat = ( - 2, # min_repeat - 10, # max_repeat - 20.0, # max_time in seconds +benchmarking_directory = workspace.get_dir() + +# name of example to load from activitysim_resources +example_name = "example_mtc_full" + +# any settings to override in the example's usual settings file +benchmark_settings = { + 'households_sample_size': 1_000, +} + +# the component names to be benchmarked +component_names = [ + # "compute_accessibility", + "school_location", + "workplace_location", + # "auto_ownership_simulate", + # "free_parking", + # "cdap_simulate", + # "mandatory_tour_frequency", + # "mandatory_tour_scheduling", + # "joint_tour_frequency", + # "joint_tour_composition", + # "joint_tour_participation", + # "joint_tour_destination", + # "joint_tour_scheduling", + # "non_mandatory_tour_frequency", + # "non_mandatory_tour_destination", + # "non_mandatory_tour_scheduling", + # "tour_mode_choice_simulate", + # "atwork_subtour_frequency", + # "atwork_subtour_destination", + # "atwork_subtour_scheduling", + # "atwork_subtour_mode_choice", + # "stop_frequency", + # "trip_purpose", + # "trip_destination", + # "trip_purpose_and_destination", + # "trip_scheduling", + # "trip_mode_choice", +] + +#param_names = ['component_name'] + +timeout = 36000.0 # ten hours +repeat = ( + 2, # min_repeat + 10, # max_repeat + 20.0, # max_time in seconds +) +number = 1 + +preload_injectables = ( + 'skim_dict', +) + +def setup_cache(): + + if workspace.get_dir() is None: + from asv.console import log + for k,v in os.environ.items(): + log.error(f" env {k}: {v}") + raise RuntimeError("workspace unavailable") + os.makedirs(os.path.join(local_dir(), "models"), exist_ok=True) + get_example( + example_name=example_name, + destination=os.path.join(local_dir(), "models"), ) - number = 1 + settings_filename = os.path.join(model_dir(), "configs", "settings.yaml") + with open(settings_filename, 'rt') as f: + models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') + + last_component_to_benchmark = 0 + for component_name in component_names: + last_component_to_benchmark = max( + models.index(component_name), + last_component_to_benchmark + ) + pre_run_model_list = models[:last_component_to_benchmark] + modify_yaml( + os.path.join(model_dir(), "configs", "settings.yaml"), + **benchmark_settings, + models=pre_run_model_list, + checkpoints=True, + trace_hh_id=None, + chunk_training_mode='off', + ) + modify_yaml( + os.path.join(model_dir(), "configs", "network_los.yaml"), + read_skim_cache=True, + ) + componentwise.pre_run(model_dir()) - preload_injectables = ( - 'skim_dict', +def _setup_component(self, component_name): + componentwise.setup_component( + component_name, + model_dir(), + self.preload_injectables, ) - def setup_cache(self): - if workspace.get_dir() is None: - from asv.console import log - for k,v in os.environ.items(): - log.error(f" env {k}: {v}") - raise RuntimeError("workspace unavailable") - os.makedirs(os.path.join(self.local_dir, "models"), exist_ok=True) - get_example( - example_name=self.example_name, - destination=os.path.join(self.local_dir, "models"), - ) - settings_filename = os.path.join(self.model_dir, "configs", "settings.yaml") - with open(settings_filename, 'rt') as f: - self.models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') - - last_component_to_benchmark = 0 - for component_name in self.params: - last_component_to_benchmark = max( - self.models.index(component_name), - last_component_to_benchmark - ) - pre_run_model_list = self.models[:last_component_to_benchmark] - modify_yaml( - os.path.join(self.model_dir, "configs", "settings.yaml"), - **self.benchmark_settings, - models=pre_run_model_list, - checkpoints=True, - trace_hh_id=None, - chunk_training_mode='off', - ) - modify_yaml( - os.path.join(self.model_dir, "configs", "network_los.yaml"), - read_skim_cache=True, - ) - componentwise.pre_run(self.model_dir) +def _time_component(self, component_name): + return componentwise.run_component(component_name) - def setup(self, component_name): - componentwise.setup_component( - component_name, - self.model_dir, - self.preload_injectables, - ) +def _teardown_component(self, component_name): + componentwise.teardown_component(component_name) + +def local_dir(): + if benchmarking_directory is not None: + return benchmarking_directory + return os.getcwd() - def time_component(self, component_name): - return componentwise.run_component(component_name) +def model_dir(): + return os.path.join(local_dir(), "models", example_name) - def teardown(self, component_name): - componentwise.teardown_component(component_name) - @property - def local_dir(self): - if self.benchmarking_directory is not None: - return self.benchmarking_directory - return os.getcwd() +for component_name in component_names: + f = lambda: _time_component(component_name) + f.__name__ = f"time_{component_name}" + f.setup = lambda: _setup_component(component_name) + f.teardown = lambda: _teardown_component(component_name) + #sys._getframe(0).f_globals[f.__name__] = f + globals()[f.__name__] = f - @property - def model_dir(self): - return os.path.join(self.local_dir, "models", self.example_name) if __name__ == '__main__': diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 79ae039902..08ec0a167d 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -70,6 +70,10 @@ # The directory (relative to the current directory) that the html tree # should be written to. If not provided, defaults to "html". # "html_dir": "../activitysim-asv/html", + + # List of branches to benchmark. If not provided, defaults to "master" + # (for git) or "default" (for mercurial). + "branches": ["master", "performance1"] } def make_asv_argparser(parser): From 2c5fe278bb31a55b8248f2f43824f88ff006e4ce Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 11:56:07 -0500 Subject: [PATCH 033/150] no self --- activitysim/benchmarking/benchmarks/mtc1.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 290a03486a..020c0b645f 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -103,17 +103,17 @@ def setup_cache(): ) componentwise.pre_run(model_dir()) -def _setup_component(self, component_name): +def _setup_component(component_name): componentwise.setup_component( component_name, model_dir(), - self.preload_injectables, + preload_injectables, ) -def _time_component(self, component_name): +def _time_component(component_name): return componentwise.run_component(component_name) -def _teardown_component(self, component_name): +def _teardown_component(component_name): componentwise.teardown_component(component_name) def local_dir(): From c55e14ac27a78fb60d6213666477f8801d011456 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 12:05:32 -0500 Subject: [PATCH 034/150] partial --- activitysim/benchmarking/benchmarks/mtc1.py | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 020c0b645f..516c46a393 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -5,6 +5,7 @@ import time import yaml from datetime import timedelta +from functools import partial from activitysim.benchmarking import componentwise, modify_yaml, workspace from activitysim.cli.create import get_example @@ -126,11 +127,10 @@ def model_dir(): for component_name in component_names: - f = lambda: _time_component(component_name) + f = partial(_time_component, component_name) f.__name__ = f"time_{component_name}" - f.setup = lambda: _setup_component(component_name) - f.teardown = lambda: _teardown_component(component_name) - #sys._getframe(0).f_globals[f.__name__] = f + f.setup = partial(_setup_component, component_name) + f.teardown = partial(_teardown_component, component_name) globals()[f.__name__] = f @@ -140,26 +140,26 @@ def model_dir(): os.chdir(benchmarking_data_directory) t0a = time.time() - suite = BenchSuite_MTC() - suite.setup_cache() + setup_cache() t0b = time.time() timings = {} - for component_name in suite.params: + for component_name in component_names: logger.warning(f"$$$$$$$$ {component_name} #1 $$$$$$$$") - suite.setup(component_name) + f = globals()[f"time_{component_name}"] + f.setup(component_name) t1a = time.time() - suite.time_component(component_name) + f(component_name) t1b = time.time() - suite.teardown(component_name) + f.teardown(component_name) logger.warning(f"$$$$$$$$ {component_name} #2 $$$$$$$$") - suite.setup(component_name) + f.setup(component_name) t2a = time.time() - suite.time_component(component_name) + f(component_name) t2b = time.time() - suite.teardown(component_name) + f.teardown(component_name) timings[component_name] = ( str(timedelta(seconds=t1b-t1a)), @@ -167,5 +167,5 @@ def model_dir(): ) logger.warning(f"Time Base Setup: {timedelta(seconds=t0b-t0a)}") - for component_name in suite.params: + for component_name in component_names: logger.warning(f"Time {component_name}: {timings[component_name]}") From 2b3e2e6a85162b6d4e561a0d703961eae8b86b60 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 12:45:50 -0500 Subject: [PATCH 035/150] discoverable --- activitysim/benchmarking/benchmarks/mtc1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 516c46a393..d12a8071a4 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -127,7 +127,8 @@ def model_dir(): for component_name in component_names: - f = partial(_time_component, component_name) + f = lambda: partial(_time_component, component_name) + # benchmark discovery fails on partial, so we wrap in a lambda f.__name__ = f"time_{component_name}" f.setup = partial(_setup_component, component_name) f.teardown = partial(_teardown_component, component_name) From 23c7496f8106a252e3509be37bf1552b83ea4237 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 12:57:29 -0500 Subject: [PATCH 036/150] remote intermediate funcs --- activitysim/benchmarking/benchmarks/mtc1.py | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index d12a8071a4..b54eaa073c 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -27,9 +27,9 @@ # "compute_accessibility", "school_location", "workplace_location", - # "auto_ownership_simulate", - # "free_parking", - # "cdap_simulate", + "auto_ownership_simulate", + "free_parking", + "cdap_simulate", # "mandatory_tour_frequency", # "mandatory_tour_scheduling", # "joint_tour_frequency", @@ -53,8 +53,6 @@ # "trip_mode_choice", ] -#param_names = ['component_name'] - timeout = 36000.0 # ten hours repeat = ( 2, # min_repeat @@ -67,6 +65,7 @@ 'skim_dict', ) + def setup_cache(): if workspace.get_dir() is None: @@ -104,34 +103,29 @@ def setup_cache(): ) componentwise.pre_run(model_dir()) -def _setup_component(component_name): - componentwise.setup_component( - component_name, - model_dir(), - preload_injectables, - ) - -def _time_component(component_name): - return componentwise.run_component(component_name) - -def _teardown_component(component_name): - componentwise.teardown_component(component_name) def local_dir(): if benchmarking_directory is not None: return benchmarking_directory return os.getcwd() + def model_dir(): return os.path.join(local_dir(), "models", example_name) for component_name in component_names: - f = lambda: partial(_time_component, component_name) + f = lambda: partial(componentwise.run_component, component_name) # benchmark discovery fails on partial, so we wrap in a lambda f.__name__ = f"time_{component_name}" - f.setup = partial(_setup_component, component_name) - f.teardown = partial(_teardown_component, component_name) + f.setup = partial( + componentwise.setup_component, + component_name, model_dir(), preload_injectables, + ) + f.teardown = partial(componentwise.teardown_component, component_name) + f.repeat = repeat + f.number = number + f.timeout = timeout globals()[f.__name__] = f From e1a423ccd20beebe29eb3ce7d1a9665a251170f5 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 13:22:17 -0500 Subject: [PATCH 037/150] call the partial --- activitysim/benchmarking/benchmarks/mtc1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index b54eaa073c..ecaaf9f3e7 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -115,8 +115,8 @@ def model_dir(): for component_name in component_names: - f = lambda: partial(componentwise.run_component, component_name) - # benchmark discovery fails on partial, so we wrap in a lambda + p = partial(componentwise.run_component, component_name) + f = lambda: p() # benchmark discovery fails on partial, so we wrap in a lambda f.__name__ = f"time_{component_name}" f.setup = partial( componentwise.setup_component, From f05e1cf29fa51c0629242ffbcd3eaa17d9ef612b Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 13:33:59 -0500 Subject: [PATCH 038/150] more parts --- activitysim/benchmarking/benchmarks/mtc1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index ecaaf9f3e7..945ef5a2fe 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -30,8 +30,8 @@ "auto_ownership_simulate", "free_parking", "cdap_simulate", - # "mandatory_tour_frequency", - # "mandatory_tour_scheduling", + "mandatory_tour_frequency", + "mandatory_tour_scheduling", # "joint_tour_frequency", # "joint_tour_composition", # "joint_tour_participation", From 1bd9a05b99f1d358a19da470a5595d81c6b8361c Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 13:46:20 -0500 Subject: [PATCH 039/150] reraise --- activitysim/benchmarking/componentwise.py | 1 + 1 file changed, 1 insertion(+) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index a5061cfd59..101c43dda4 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -125,6 +125,7 @@ def run_component(component_name): run_model(component_name) except Exception as err: logger.exception("run_component exception: %s", component_name) + raise else: logger.info("run_component completed: %s", component_name) return 0 From 685c2a8bd2b87b6dc443c7fe7e97e197db1c52c1 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 19:36:51 -0500 Subject: [PATCH 040/150] refactor --- activitysim/benchmarking/benchmarks/mtc1.py | 64 ++++++++++++--------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 945ef5a2fe..bc6d0721fc 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -10,20 +10,18 @@ from activitysim.cli.create import get_example logger = logging.getLogger("activitysim.benchmarking") - - benchmarking_directory = workspace.get_dir() # name of example to load from activitysim_resources -example_name = "example_mtc_full" +EXAMPLE_NAME = "example_mtc_full" # any settings to override in the example's usual settings file -benchmark_settings = { +BENCHMARK_SETTINGS = { 'households_sample_size': 1_000, } # the component names to be benchmarked -component_names = [ +COMPONENT_NAMES = [ # "compute_accessibility", "school_location", "workplace_location", @@ -53,15 +51,17 @@ # "trip_mode_choice", ] -timeout = 36000.0 # ten hours -repeat = ( +# benchmarking configuration +TIMEOUT = 36000.0 # ten hours +REPEAT = ( 2, # min_repeat 10, # max_repeat 20.0, # max_time in seconds ) -number = 1 +NUMBER = 1 -preload_injectables = ( +# any injectables to preload in setup (so loading isn't counted in time) +PRELOAD_INJECTABLES = ( 'skim_dict', ) @@ -75,7 +75,7 @@ def setup_cache(): raise RuntimeError("workspace unavailable") os.makedirs(os.path.join(local_dir(), "models"), exist_ok=True) get_example( - example_name=example_name, + example_name=EXAMPLE_NAME, destination=os.path.join(local_dir(), "models"), ) settings_filename = os.path.join(model_dir(), "configs", "settings.yaml") @@ -83,7 +83,7 @@ def setup_cache(): models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') last_component_to_benchmark = 0 - for component_name in component_names: + for component_name in COMPONENT_NAMES: last_component_to_benchmark = max( models.index(component_name), last_component_to_benchmark @@ -91,7 +91,7 @@ def setup_cache(): pre_run_model_list = models[:last_component_to_benchmark] modify_yaml( os.path.join(model_dir(), "configs", "settings.yaml"), - **benchmark_settings, + **BENCHMARK_SETTINGS, models=pre_run_model_list, checkpoints=True, trace_hh_id=None, @@ -111,22 +111,30 @@ def local_dir(): def model_dir(): - return os.path.join(local_dir(), "models", example_name) + return os.path.join(local_dir(), "models", EXAMPLE_NAME) -for component_name in component_names: - p = partial(componentwise.run_component, component_name) - f = lambda: p() # benchmark discovery fails on partial, so we wrap in a lambda - f.__name__ = f"time_{component_name}" - f.setup = partial( - componentwise.setup_component, - component_name, model_dir(), preload_injectables, - ) - f.teardown = partial(componentwise.teardown_component, component_name) - f.repeat = repeat - f.number = number - f.timeout = timeout - globals()[f.__name__] = f +def generate_component_timings(component_name): + + class ComponentTiming: + component_name = component_name + repeat = REPEAT + number = NUMBER + timeout = TIMEOUT + def setup(self): + componentwise.setup_component(self.component_name, model_dir(), PRELOAD_INJECTABLES) + def teardown(self): + componentwise.teardown_component(self.component_name) + def time_component(self): + componentwise.run_component(self.component_name) + + ComponentTiming.__name__ = f"time_{component_name}" + + return ComponentTiming + + +for component_name in COMPONENT_NAMES: + globals()[f"time_{component_name}"] = generate_component_timings(component_name) if __name__ == '__main__': @@ -139,7 +147,7 @@ def model_dir(): t0b = time.time() timings = {} - for component_name in component_names: + for component_name in COMPONENT_NAMES: logger.warning(f"$$$$$$$$ {component_name} #1 $$$$$$$$") f = globals()[f"time_{component_name}"] @@ -162,5 +170,5 @@ def model_dir(): ) logger.warning(f"Time Base Setup: {timedelta(seconds=t0b-t0a)}") - for component_name in component_names: + for component_name in COMPONENT_NAMES: logger.warning(f"Time {component_name}: {timings[component_name]}") From 26442968d3086be4bf2b4b465b92cb58cc9e4ef2 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 20:05:54 -0500 Subject: [PATCH 041/150] names --- activitysim/benchmarking/benchmarks/mtc1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index bc6d0721fc..6e3fd6eac1 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -121,6 +121,8 @@ class ComponentTiming: repeat = REPEAT number = NUMBER timeout = TIMEOUT + benchmark_name = f"{__name__}.time_component.{component_name}" + pretty_name = f"{EXAMPLE_NAME}:{component_name}" def setup(self): componentwise.setup_component(self.component_name, model_dir(), PRELOAD_INJECTABLES) def teardown(self): From 97881234456234577980ca44fe073298cd400eb8 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 20:53:39 -0500 Subject: [PATCH 042/150] to tour mode --- activitysim/benchmarking/benchmarks/mtc1.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 6e3fd6eac1..d646cce998 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -30,15 +30,15 @@ "cdap_simulate", "mandatory_tour_frequency", "mandatory_tour_scheduling", - # "joint_tour_frequency", - # "joint_tour_composition", - # "joint_tour_participation", - # "joint_tour_destination", - # "joint_tour_scheduling", - # "non_mandatory_tour_frequency", - # "non_mandatory_tour_destination", - # "non_mandatory_tour_scheduling", - # "tour_mode_choice_simulate", + "joint_tour_frequency", + "joint_tour_composition", + "joint_tour_participation", + "joint_tour_destination", + "joint_tour_scheduling", + "non_mandatory_tour_frequency", + "non_mandatory_tour_destination", + "non_mandatory_tour_scheduling", + "tour_mode_choice_simulate", # "atwork_subtour_frequency", # "atwork_subtour_destination", # "atwork_subtour_scheduling", From 59f562ae1d1368334b3a44a7ee92ac94b236eb8f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 21:18:30 -0500 Subject: [PATCH 043/150] tinker names --- activitysim/benchmarking/benchmarks/mtc1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index d646cce998..cff85929ae 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -39,7 +39,7 @@ "non_mandatory_tour_destination", "non_mandatory_tour_scheduling", "tour_mode_choice_simulate", - # "atwork_subtour_frequency", + "atwork_subtour_frequency", # "atwork_subtour_destination", # "atwork_subtour_scheduling", # "atwork_subtour_mode_choice", @@ -121,14 +121,14 @@ class ComponentTiming: repeat = REPEAT number = NUMBER timeout = TIMEOUT - benchmark_name = f"{__name__}.time_component.{component_name}" - pretty_name = f"{EXAMPLE_NAME}:{component_name}" def setup(self): componentwise.setup_component(self.component_name, model_dir(), PRELOAD_INJECTABLES) def teardown(self): componentwise.teardown_component(self.component_name) def time_component(self): componentwise.run_component(self.component_name) + time_component.benchmark_name = f"{__name__}.time_component.{component_name}" + time_component.pretty_name = f"{EXAMPLE_NAME}:{component_name}" ComponentTiming.__name__ = f"time_{component_name}" From f1bb8376abdc599a06c0f0973c723fd86934479f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 5 Jul 2021 22:03:33 -0500 Subject: [PATCH 044/150] just pretty --- activitysim/benchmarking/benchmarks/mtc1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index cff85929ae..704aa37e5f 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -127,7 +127,7 @@ def teardown(self): componentwise.teardown_component(self.component_name) def time_component(self): componentwise.run_component(self.component_name) - time_component.benchmark_name = f"{__name__}.time_component.{component_name}" + #time_component.benchmark_name = f"{__name__}.time_component.{component_name}" time_component.pretty_name = f"{EXAMPLE_NAME}:{component_name}" ComponentTiming.__name__ = f"time_{component_name}" From b0c7dbad6d86d4be6de51b1fc3ea7cf1dbd2b72b Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 8 Jul 2021 10:37:14 -0500 Subject: [PATCH 045/150] generic cleaning --- activitysim/benchmarking/componentwise.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 101c43dda4..d4bfb3a70e 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -8,11 +8,6 @@ logger = logging.getLogger(__name__) -TABLE_CLEANING = dict( - mandatory_tour_frequency=['tours'], - -) - def reload_settings(**kwargs): settings = config.read_settings_file('settings.yaml', mandatory=True) @@ -134,12 +129,6 @@ def run_component(component_name): def teardown_component(component_name): logger.info("teardown_component: %s", component_name) - # any new orca tables that were created need to be dropped so the - # next benchmark run has a clean slate - # for table_name in TABLE_CLEANING.get(component_name, []): - # logger.info("dropping table %s", table_name) - # pipeline.drop_table(table_name) - # use the pipeline module to clear out all the orca tables, so # the next benchmark run has a clean slate. # anything needed should be reloaded from the pipeline checkpoint file @@ -176,7 +165,7 @@ def pre_run(model_working_dir): config.override_setting('resume_after', None) # cleanup - cleanup_output_files() + #cleanup_output_files() tracing.config_logger(basic=False) config.filter_warnings() From 5df7b420fda05bb70902ab07f1e45725e01ad5bf Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sat, 17 Jul 2021 16:40:52 -0500 Subject: [PATCH 046/150] define mtc1 as 20K households --- activitysim/benchmarking/benchmarks/mtc1.py | 20 ++++++++++---------- conda-environments/activitysim-dev.yml | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index 704aa37e5f..fbbd78c9e2 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -17,7 +17,7 @@ # any settings to override in the example's usual settings file BENCHMARK_SETTINGS = { - 'households_sample_size': 1_000, + 'households_sample_size': 20_000, } # the component names to be benchmarked @@ -40,15 +40,15 @@ "non_mandatory_tour_scheduling", "tour_mode_choice_simulate", "atwork_subtour_frequency", - # "atwork_subtour_destination", - # "atwork_subtour_scheduling", - # "atwork_subtour_mode_choice", - # "stop_frequency", - # "trip_purpose", - # "trip_destination", - # "trip_purpose_and_destination", - # "trip_scheduling", - # "trip_mode_choice", + "atwork_subtour_destination", + "atwork_subtour_scheduling", + "atwork_subtour_mode_choice", + "stop_frequency", + "trip_purpose", + "trip_destination", + "trip_purpose_and_destination", + "trip_scheduling", + "trip_mode_choice", ] # benchmarking configuration diff --git a/conda-environments/activitysim-dev.yml b/conda-environments/activitysim-dev.yml index 8cd394996a..64b3026843 100644 --- a/conda-environments/activitysim-dev.yml +++ b/conda-environments/activitysim-dev.yml @@ -28,6 +28,8 @@ dependencies: - git - gh - bump2version +- asv +- ruamel.yaml - pip: - -e .. \ No newline at end of file From 43723e1364dd33837501381c63f7db2433d12133 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sat, 17 Jul 2021 17:57:58 -0500 Subject: [PATCH 047/150] ready for win --- activitysim/benchmarking/benchmarks/mtc1.py | 2 +- activitysim/cli/benchmark.py | 2 +- docs/benchmarking.rst | 63 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 docs/benchmarking.rst diff --git a/activitysim/benchmarking/benchmarks/mtc1.py b/activitysim/benchmarking/benchmarks/mtc1.py index fbbd78c9e2..24247c1baf 100644 --- a/activitysim/benchmarking/benchmarks/mtc1.py +++ b/activitysim/benchmarking/benchmarks/mtc1.py @@ -17,7 +17,7 @@ # any settings to override in the example's usual settings file BENCHMARK_SETTINGS = { - 'households_sample_size': 20_000, + 'households_sample_size': 100_000, } # the component names to be benchmarked diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 08ec0a167d..6b6e2ec30a 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -73,7 +73,7 @@ # List of branches to benchmark. If not provided, defaults to "master" # (for git) or "default" (for mercurial). - "branches": ["master", "performance1"] + # "branches": ["master", "performance1"] } def make_asv_argparser(parser): diff --git a/docs/benchmarking.rst b/docs/benchmarking.rst new file mode 100644 index 0000000000..cb445fab2a --- /dev/null +++ b/docs/benchmarking.rst @@ -0,0 +1,63 @@ + +.. _benchmarking : + +Benchmarking +------------ + +ActivitySim includes the ability to run performance benchmarks using a tool +called `airspeed velocity `__. + +The benchmarking process is closely tied to ActivitySim's *git* repository, +so it is recommended that you use Git to clone the repository from GitHub. + + + +Interim Directions +~~~~~~~~~~~~~~~~~~ + +Clone the repo, and setup a conda environment for benchmarking. + +:: + + conda create -n ASIM-BENCH git gh -c conda-forge --override-channels + conda activate ASIM-BENCH + gh auth login # <--- (only needed if gh is not logged in) + gh repo clone jpn--/activitysim # FUTURE: use main repo + cd activitysim + git switch performance1 # FUTURE: use master branch + conda env update --file=conda-environments/activitysim-dev.yml + cd .. + gh repo clone jpn--/activitysim_benchmarks # FUTURE: use org repo + cd activitysim_benchmarks + +Define machine specs by running this command:: + + activitysim benchmark machine + +This will start an interactive questions and answer session to describe your +computer. Don't be afraid, just answer the questions. The tool may make +suggestions, but they are not always correct, so check them first and don't just +accept all. For example, under "arch" it may suggest "AMD64", but for consistency +yo can change that to "x86_64", which is the same thing by a different name. + +To run benchmarks on only the most recent commit in the main ActivitySim repo:: + + activitysim benchmark run "HEAD^!" --verbose + + +Threading Limits +~~~~~~~~~~~~~~~~ + +When you run benchmarking using the `activitysim benchmark` command, the +following environment variable are set automatically before benchmarking begins:: + + MKL_NUM_THREADS = 1 + OMP_NUM_THREADS = 1 + OPENBLAS_NUM_THREADS = 1 + NUMBA_NUM_THREADS = 1 + VECLIB_MAXIMUM_THREADS = 1 + NUMEXPR_NUM_THREADS = 1 + +This ensures that all benchmarking operations run processes in single-threaded +mode. This still allows ActivitySim itself to spin up multiple processes if the +item being timed is a multiprocess benchmark. \ No newline at end of file From c91f17cf5e3227750907b9b90b5d1cc648fddc54 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Sun, 18 Jul 2021 13:10:39 -0500 Subject: [PATCH 048/150] pb branches --- activitysim/cli/benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 6b6e2ec30a..79045d806f 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -73,7 +73,7 @@ # List of branches to benchmark. If not provided, defaults to "master" # (for git) or "default" (for mercurial). - # "branches": ["master", "performance1"] + "branches": ["master", "performance1", "performance2"] } def make_asv_argparser(parser): From 20ec9f1204d6ce53a86d6086323272ccb9399f5c Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 19 Jul 2021 09:13:51 -0500 Subject: [PATCH 049/150] more aggressive chunkless --- activitysim/core/chunk.py | 52 ++++++++++++++++++++++++--------------- docs/benchmarking.rst | 2 +- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 5d27689549..4ae26873c6 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -594,11 +594,11 @@ def log_rss(trace_label, force=False): def log_df(trace_label, table_name, df): - assert len(CHUNK_LEDGERS) > 0, f"log_df called without current chunker." - if chunk_training_mode() in (MODE_PRODUCTION, MODE_CHUNKLESS): return + assert len(CHUNK_LEDGERS) > 0, f"log_df called without current chunker." + op = 'del' if df is None else 'add' hwm_trace_label = f"{trace_label}.{op}.{table_name}" @@ -637,23 +637,26 @@ def __init__(self, chunk_tag, trace_label, num_choosers=0, chunk_size=0): self.depth = len(CHUNK_SIZERS) + 1 - if chunk_metric() == USS: - self.rss, self.uss = mem.get_rss(force_garbage_collect=True, uss=True) - else: - self.rss, _ = mem.get_rss(force_garbage_collect=True, uss=False) - self.uss = 0 + if chunk_training_mode() != MODE_CHUNKLESS: + if chunk_metric() == USS: + self.rss, self.uss = mem.get_rss(force_garbage_collect=True, uss=True) + else: + self.rss, _ = mem.get_rss(force_garbage_collect=True, uss=False) + self.uss = 0 - if self.depth > 1: - # nested chunkers should be unchunked - assert chunk_size == 0 + if self.depth > 1: + # nested chunkers should be unchunked + assert chunk_size == 0 - # if we are in a nested call, then we must be in the scope of active Ledger - # so any rss accumulated so far should be attributed to the parent active ledger - assert len(CHUNK_SIZERS) == len(CHUNK_LEDGERS) - parent = CHUNK_SIZERS[-1] - assert parent.chunk_ledger is not None + # if we are in a nested call, then we must be in the scope of active Ledger + # so any rss accumulated so far should be attributed to the parent active ledger + assert len(CHUNK_SIZERS) == len(CHUNK_LEDGERS) + parent = CHUNK_SIZERS[-1] + assert parent.chunk_ledger is not None - log_rss(trace_label) # give parent a complementary log_rss reading entering sub context + log_rss(trace_label) # give parent a complementary log_rss reading entering sub context + else: + self.rss, self.uss = 0, 0 self.chunk_tag = chunk_tag self.trace_label = trace_label @@ -857,6 +860,11 @@ def adaptive_rows_per_chunk(self, i): @contextmanager def ledger(self): + # don't do anything in chunkless mode + if chunk_training_mode() == MODE_CHUNKLESS: + yield + return + mem_monitor = None # nested chunkers should be unchunked @@ -922,7 +930,8 @@ def chunk_log(trace_label, chunk_tag=None, base=False): yield - chunk_sizer.adaptive_rows_per_chunk(1) + if chunk_training_mode() != MODE_CHUNKLESS: + chunk_sizer.adaptive_rows_per_chunk(1) chunk_sizer.close() @@ -963,7 +972,8 @@ def adaptive_chunked_choosers(choosers, chunk_size, trace_label, chunk_tag=None) offset += rows_per_chunk - rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) + if chunk_training_mode() != MODE_CHUNKLESS: + rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) chunk_sizer.close() @@ -1057,7 +1067,8 @@ def adaptive_chunked_choosers_and_alts(choosers, alternatives, chunk_size, trace offset += rows_per_chunk alt_offset = alt_end - rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) + if chunk_training_mode() != MODE_CHUNKLESS: + rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) chunk_sizer.close() @@ -1097,6 +1108,7 @@ def adaptive_chunked_choosers_by_chunk_id(choosers, chunk_size, trace_label, chu offset += rows_per_chunk - rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) + if chunk_training_mode() != MODE_CHUNKLESS: + rows_per_chunk, estimated_number_of_chunks = chunk_sizer.adaptive_rows_per_chunk(i) chunk_sizer.close() diff --git a/docs/benchmarking.rst b/docs/benchmarking.rst index cb445fab2a..6ff6b8f895 100644 --- a/docs/benchmarking.rst +++ b/docs/benchmarking.rst @@ -42,7 +42,7 @@ yo can change that to "x86_64", which is the same thing by a different name. To run benchmarks on only the most recent commit in the main ActivitySim repo:: - activitysim benchmark run "HEAD^!" --verbose + activitysim benchmark run "HEAD^!" --verbose --append-samples Threading Limits From e4e5657a4f4942e5318256b855c5b4df77357c6f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 19 Jul 2021 14:56:47 -0500 Subject: [PATCH 050/150] asv commit logging --- activitysim/core/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index b85a02e81a..87acd56d01 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -333,6 +333,12 @@ def log_file_path(file_name, prefix=True): output_dir = inject.get_injectable('output_dir') + # - check if running asv and if so, log to commit-specific subfolder + asv_commit = os.environ.get('ASV_COMMIT', None) + if asv_commit: + output_dir = os.path.join(output_dir, f'log-{asv_commit}') + os.makedirs(output_dir, exist_ok=True) + # - check for optional log subfolder if os.path.exists(os.path.join(output_dir, 'log')): output_dir = os.path.join(output_dir, 'log') From 675f588ec98a3f6ece8a134a668d05b013da2168 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 09:05:47 -0500 Subject: [PATCH 051/150] asv sandag1 --- .../benchmarks/componentewise_template.py | 89 +++++++++++++++++++ .../benchmarking/benchmarks/sandag1.py | 72 +++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 activitysim/benchmarking/benchmarks/componentewise_template.py create mode 100644 activitysim/benchmarking/benchmarks/sandag1.py diff --git a/activitysim/benchmarking/benchmarks/componentewise_template.py b/activitysim/benchmarking/benchmarks/componentewise_template.py new file mode 100644 index 0000000000..239628d431 --- /dev/null +++ b/activitysim/benchmarking/benchmarks/componentewise_template.py @@ -0,0 +1,89 @@ +import argparse +import sys +import os +import logging +import time +import yaml +from datetime import timedelta +from functools import partial +from activitysim.benchmarking import componentwise, modify_yaml, workspace +from activitysim.cli.create import get_example + +logger = logging.getLogger("activitysim.benchmarking") +benchmarking_directory = workspace.get_dir() + +def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS): + + if workspace.get_dir() is None: + from asv.console import log + for k,v in os.environ.items(): + log.error(f" env {k}: {v}") + raise RuntimeError("workspace unavailable") + os.makedirs(os.path.join(local_dir(), "models"), exist_ok=True) + get_example( + example_name=EXAMPLE_NAME, + destination=os.path.join(local_dir(), "models"), + ) + settings_filename = os.path.join(model_dir(EXAMPLE_NAME), "configs", "settings.yaml") + with open(settings_filename, 'rt') as f: + models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') + + last_component_to_benchmark = 0 + for cname in COMPONENT_NAMES: + last_component_to_benchmark = max( + models.index(cname), + last_component_to_benchmark + ) + pre_run_model_list = models[:last_component_to_benchmark] + modify_yaml( + os.path.join(model_dir(EXAMPLE_NAME), "configs", "settings.yaml"), + **BENCHMARK_SETTINGS, + models=pre_run_model_list, + checkpoints=True, + trace_hh_id=None, + chunk_training_mode='off', + ) + modify_yaml( + os.path.join(model_dir(EXAMPLE_NAME), "configs", "network_los.yaml"), + read_skim_cache=True, + ) + componentwise.pre_run(model_dir(EXAMPLE_NAME)) + + +def local_dir(): + if benchmarking_directory is not None: + return benchmarking_directory + return os.getcwd() + + +def model_dir(example_name): + return os.path.join(local_dir(), "models", example_name) + + +def generate_component_timings( + componentname, + EXAMPLE_NAME, + PRELOAD_INJECTABLES, + REPEAT, + NUMBER, + TIMEOUT, +): + + class ComponentTiming: + component_name = componentname + repeat = REPEAT + number = NUMBER + timeout = TIMEOUT + def setup(self): + componentwise.setup_component(self.component_name, model_dir(EXAMPLE_NAME), PRELOAD_INJECTABLES) + def teardown(self): + componentwise.teardown_component(self.component_name) + def time_component(self): + componentwise.run_component(self.component_name) + #time_component.benchmark_name = f"{__name__}.time_component.{componentname}" + time_component.pretty_name = f"{EXAMPLE_NAME}:{componentname}" + + ComponentTiming.__name__ = f"time_{componentname}" + + return ComponentTiming + diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py new file mode 100644 index 0000000000..1c9c833c91 --- /dev/null +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -0,0 +1,72 @@ + +from functools import partial +from .componentewise_template import f_setup_cache, generate_component_timings + +# name of example to load from activitysim_resources +EXAMPLE_NAME = "example_sandag_1_zone_full" + +# any settings to override in the example's usual settings file +BENCHMARK_SETTINGS = { + 'households_sample_size': 100_000, +} + +# the component names to be benchmarked +COMPONENT_NAMES = [ + # "compute_accessibility", + "school_location", + # "workplace_location", + # "auto_ownership_simulate", + # "free_parking", + # "cdap_simulate", + # "mandatory_tour_frequency", + # "mandatory_tour_scheduling", + # "joint_tour_frequency", + # "joint_tour_composition", + # "joint_tour_participation", + # "joint_tour_destination", + # "joint_tour_scheduling", + # "non_mandatory_tour_frequency", + # "non_mandatory_tour_destination", + # "non_mandatory_tour_scheduling", + # "tour_mode_choice_simulate", + # "atwork_subtour_frequency", + # "atwork_subtour_destination", + # "atwork_subtour_scheduling", + # "atwork_subtour_mode_choice", + # "stop_frequency", + # "trip_purpose", + # "trip_destination", + # "trip_purpose_and_destination", + # "trip_scheduling", + # "trip_mode_choice", +] + +# benchmarking configuration +TIMEOUT = 36000.0 # ten hours +REPEAT = ( + 2, # min_repeat + 10, # max_repeat + 20.0, # max_time in seconds +) +NUMBER = 1 + +# any injectables to preload in setup (so loading isn't counted in time) +PRELOAD_INJECTABLES = ( + 'skim_dict', +) + +# benchmarking implementation + +setup_cache = partial( + f_setup_cache, EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, +) + +for cname in COMPONENT_NAMES: + globals()[f"time_{cname}"] = generate_component_timings( + cname, + EXAMPLE_NAME, + PRELOAD_INJECTABLES, + REPEAT, + NUMBER, + TIMEOUT, + ) From 761319f7b821593de0357e8357fe952b497210d5 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 09:22:05 -0500 Subject: [PATCH 052/150] setup_cache as func --- activitysim/benchmarking/benchmarks/sandag1.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py index 1c9c833c91..68c08fac85 100644 --- a/activitysim/benchmarking/benchmarks/sandag1.py +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -55,11 +55,12 @@ 'skim_dict', ) + # benchmarking implementation -setup_cache = partial( - f_setup_cache, EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, -) +def setup_cache(): + f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS) + for cname in COMPONENT_NAMES: globals()[f"time_{cname}"] = generate_component_timings( From 1a7ba61d7c4176fe2067124eff4d817c84cbbb4a Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 10:04:40 -0500 Subject: [PATCH 053/150] fix sandag1 benchmarks --- ..._template.py => componentwise_template.py} | 28 +++++++++++++------ .../benchmarking/benchmarks/sandag1.py | 5 ++-- activitysim/examples/example_manifest.yaml | 8 +++--- 3 files changed, 25 insertions(+), 16 deletions(-) rename activitysim/benchmarking/benchmarks/{componentewise_template.py => componentwise_template.py} (73%) diff --git a/activitysim/benchmarking/benchmarks/componentewise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py similarity index 73% rename from activitysim/benchmarking/benchmarks/componentewise_template.py rename to activitysim/benchmarking/benchmarks/componentwise_template.py index 239628d431..b74ea8867e 100644 --- a/activitysim/benchmarking/benchmarks/componentewise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -12,7 +12,7 @@ logger = logging.getLogger("activitysim.benchmarking") benchmarking_directory = workspace.get_dir() -def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS): +def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs",)): if workspace.get_dir() is None: from asv.console import log @@ -24,9 +24,15 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS): example_name=EXAMPLE_NAME, destination=os.path.join(local_dir(), "models"), ) - settings_filename = os.path.join(model_dir(EXAMPLE_NAME), "configs", "settings.yaml") - with open(settings_filename, 'rt') as f: - models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') + models = None + for config_settings_dir in CONFIGS_DIRS: + settings_filename = os.path.join(model_dir(EXAMPLE_NAME), config_settings_dir, "settings.yaml") + if os.path.exists(settings_filename): + with open(settings_filename, 'rt') as f: + models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') + break + if models is None: + raise ValueError("missing list of models from configs/settings.yaml") last_component_to_benchmark = 0 for cname in COMPONENT_NAMES: @@ -36,17 +42,21 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS): ) pre_run_model_list = models[:last_component_to_benchmark] modify_yaml( - os.path.join(model_dir(EXAMPLE_NAME), "configs", "settings.yaml"), + settings_filename, **BENCHMARK_SETTINGS, models=pre_run_model_list, checkpoints=True, trace_hh_id=None, chunk_training_mode='off', ) - modify_yaml( - os.path.join(model_dir(EXAMPLE_NAME), "configs", "network_los.yaml"), - read_skim_cache=True, - ) + for config_network_los_dir in CONFIGS_DIRS: + network_los_filename = os.path.join(model_dir(EXAMPLE_NAME), config_network_los_dir, "network_los.yaml") + if os.path.exists(network_los_filename): + modify_yaml( + network_los_filename, + read_skim_cache=True, + ) + break componentwise.pre_run(model_dir(EXAMPLE_NAME)) diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py index 68c08fac85..c0ca64a4b3 100644 --- a/activitysim/benchmarking/benchmarks/sandag1.py +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -1,6 +1,5 @@ -from functools import partial -from .componentewise_template import f_setup_cache, generate_component_timings +from .componentwise_template import f_setup_cache, generate_component_timings # name of example to load from activitysim_resources EXAMPLE_NAME = "example_sandag_1_zone_full" @@ -59,7 +58,7 @@ # benchmarking implementation def setup_cache(): - f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS) + f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs_1_zone", "example_mtc/configs")) for cname in COMPONENT_NAMES: diff --git a/activitysim/examples/example_manifest.yaml b/activitysim/examples/example_manifest.yaml index a91a61dcf8..5f82f86ab0 100644 --- a/activitysim/examples/example_manifest.yaml +++ b/activitysim/examples/example_manifest.yaml @@ -631,16 +631,16 @@ - example_sandag/output_1 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/households.csv data_1/households.csv - 2b0b19a1a0b00901e80a533fc6418a4dbeb3d0b017d9feadae71324f9145dcec + 9b384c1893c383de3b87dba0eef8bfed5e6fd18d18d4e21a64b5f473667f0250 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/persons.csv data_1/persons.csv - 7864364ef70ab3f490570768da12984593da81921a0cd3f8e4b0d724ae436f7a + 957d9393fadf020a9efa9083678b6baf5a8682527d4d4713f535f7802ae3b35a - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/land_use.csv data_1/land_use.csv - 2088fd1375a6a5ee253cc11da5f1b9171a9e0b788896589197ecd41ded35edd0 + 11554a33283ed9ac0d8e67eb319f3300f33a12059aaaaaeee82baa0c476185e6 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/skims1.omx data_1/skims1.omx - 33cd691c51b205e0c2bd09dba5612cd05862127f3c8ffc83c4fd533e83a0817b + 4a133e566785fa8789e039de199254b928e3953f4930df7f0951ccc21fe3a7d5 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/skims2.omx data_1/skims2.omx e650d70aeb932a1452f636088af599abe6e66dd0843a045130d8e683601b5a82 From 4e7d041e243eec78ef0399f0d701cac36ce562d9 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 10:11:15 -0500 Subject: [PATCH 054/150] allow other configs dirs in prerun --- .../benchmarking/benchmarks/componentwise_template.py | 2 +- activitysim/benchmarking/componentwise.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index b74ea8867e..b27432307b 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -57,7 +57,7 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIR read_skim_cache=True, ) break - componentwise.pre_run(model_dir(EXAMPLE_NAME)) + componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS) def local_dir(): diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index d4bfb3a70e..76f36d03d8 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -149,11 +149,15 @@ def teardown_component(component_name): return 0 -def pre_run(model_working_dir): +def pre_run(model_working_dir, configs_dirs=None): """ Pre-run the models, checkpointing everything. """ - inject.add_injectable('configs_dir', os.path.join(model_working_dir, 'configs')) + if configs_dirs is None: + inject.add_injectable('configs_dir', os.path.join(model_working_dir, 'configs')) + else: + configs_dirs_ = [os.path.join(model_working_dir, i) for i in configs_dirs] + inject.add_injectable('configs_dir', configs_dirs_) inject.add_injectable('data_dir', os.path.join(model_working_dir, 'data')) inject.add_injectable('output_dir', os.path.join(model_working_dir, 'output')) From 4bacdceaee57b20d24043661521ca954c3177ce4 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 10:19:37 -0500 Subject: [PATCH 055/150] data and output definable --- .../benchmarking/benchmarks/componentwise_template.py | 4 ++-- activitysim/benchmarking/benchmarks/sandag1.py | 7 ++++++- activitysim/benchmarking/componentwise.py | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index b27432307b..a9d8f65f3b 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -12,7 +12,7 @@ logger = logging.getLogger("activitysim.benchmarking") benchmarking_directory = workspace.get_dir() -def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs",)): +def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs",), DATA_DIR='data', OUTPUT_DIR='output'): if workspace.get_dir() is None: from asv.console import log @@ -57,7 +57,7 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIR read_skim_cache=True, ) break - componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS) + componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR) def local_dir(): diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py index c0ca64a4b3..c028bfd584 100644 --- a/activitysim/benchmarking/benchmarks/sandag1.py +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -58,7 +58,12 @@ # benchmarking implementation def setup_cache(): - f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs_1_zone", "example_mtc/configs")) + f_setup_cache( + EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, + CONFIGS_DIRS=("configs_1_zone", "example_mtc/configs"), + DATA_DIR="data_1", + OUTPUT_DIR="output_1", + ) for cname in COMPONENT_NAMES: diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 76f36d03d8..569ebf3921 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -149,7 +149,7 @@ def teardown_component(component_name): return 0 -def pre_run(model_working_dir, configs_dirs=None): +def pre_run(model_working_dir, configs_dirs=None, data_dir='data', output_dir='output'): """ Pre-run the models, checkpointing everything. """ @@ -158,8 +158,8 @@ def pre_run(model_working_dir, configs_dirs=None): else: configs_dirs_ = [os.path.join(model_working_dir, i) for i in configs_dirs] inject.add_injectable('configs_dir', configs_dirs_) - inject.add_injectable('data_dir', os.path.join(model_working_dir, 'data')) - inject.add_injectable('output_dir', os.path.join(model_working_dir, 'output')) + inject.add_injectable('data_dir', os.path.join(model_working_dir, data_dir)) + inject.add_injectable('output_dir', os.path.join(model_working_dir, output_dir)) # register abm steps and other abm-specific injectables if not inject.is_injectable('preload_injectables'): From e677f098f021cafa81cf67378ca93446487154df Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 10:35:38 -0500 Subject: [PATCH 056/150] checksums were correct --- activitysim/core/skim_dict_factory.py | 2 +- activitysim/examples/example_manifest.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/activitysim/core/skim_dict_factory.py b/activitysim/core/skim_dict_factory.py index 51b1bc59ab..5605e939b4 100644 --- a/activitysim/core/skim_dict_factory.py +++ b/activitysim/core/skim_dict_factory.py @@ -122,7 +122,7 @@ def load_skim_info(self, skim_tag): if self.omx_shape is None: self.omx_shape = tuple(int(i) for i in omx_file.shape()) # sometimes omx shape are floats! else: - assert (self.omx_shape == tuple(int(i) for i in omx_file.shape())) + assert (self.omx_shape == tuple(int(i) for i in omx_file.shape())), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}" for skim_name in omx_file.listMatrices(): assert skim_name not in self.omx_manifest, \ diff --git a/activitysim/examples/example_manifest.yaml b/activitysim/examples/example_manifest.yaml index 5f82f86ab0..a807b7a232 100644 --- a/activitysim/examples/example_manifest.yaml +++ b/activitysim/examples/example_manifest.yaml @@ -32,7 +32,7 @@ include: - example_mtc/configs - example_mtc/configs_mp - # example_mtc/data + # example_mtc/data # load data from activitysim_resources instead - example_mtc/output - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/mtc_data_full/skims.omx data/skims.omx @@ -627,20 +627,20 @@ - example_sandag/../example_mtc/configs example_mtc - example_sandag/configs_1_zone - - example_sandag/data_1 + # example_sandag/data_1 # load data from activitysim_resources instead - example_sandag/output_1 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/households.csv data_1/households.csv - 9b384c1893c383de3b87dba0eef8bfed5e6fd18d18d4e21a64b5f473667f0250 + 2b0b19a1a0b00901e80a533fc6418a4dbeb3d0b017d9feadae71324f9145dcec - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/persons.csv data_1/persons.csv - 957d9393fadf020a9efa9083678b6baf5a8682527d4d4713f535f7802ae3b35a + 7864364ef70ab3f490570768da12984593da81921a0cd3f8e4b0d724ae436f7a - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/land_use.csv data_1/land_use.csv - 11554a33283ed9ac0d8e67eb319f3300f33a12059aaaaaeee82baa0c476185e6 + 2088fd1375a6a5ee253cc11da5f1b9171a9e0b788896589197ecd41ded35edd0 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/skims1.omx data_1/skims1.omx - 4a133e566785fa8789e039de199254b928e3953f4930df7f0951ccc21fe3a7d5 + 33cd691c51b205e0c2bd09dba5612cd05862127f3c8ffc83c4fd533e83a0817b - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_1_zone_data_full/skims2.omx data_1/skims2.omx e650d70aeb932a1452f636088af599abe6e66dd0843a045130d8e683601b5a82 From 4e19b845dfde1ade6ae7c402572779dad077e0c5 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 10:51:45 -0500 Subject: [PATCH 057/150] dirs in component setup --- .../benchmarks/componentwise_template.py | 8 +++++++- activitysim/benchmarking/benchmarks/sandag1.py | 10 +++++++--- activitysim/benchmarking/componentwise.py | 17 +++++++++++++---- activitysim/examples/example_manifest.yaml | 4 ++-- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index a9d8f65f3b..d86082bdf2 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -73,6 +73,9 @@ def model_dir(example_name): def generate_component_timings( componentname, EXAMPLE_NAME, + CONFIGS_DIRS, + DATA_DIR, + OUTPUT_DIR, PRELOAD_INJECTABLES, REPEAT, NUMBER, @@ -85,7 +88,10 @@ class ComponentTiming: number = NUMBER timeout = TIMEOUT def setup(self): - componentwise.setup_component(self.component_name, model_dir(EXAMPLE_NAME), PRELOAD_INJECTABLES) + componentwise.setup_component( + self.component_name, model_dir(EXAMPLE_NAME), PRELOAD_INJECTABLES, + CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, + ) def teardown(self): componentwise.teardown_component(self.component_name) def time_component(self): diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py index c028bfd584..4342d09d69 100644 --- a/activitysim/benchmarking/benchmarks/sandag1.py +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -3,6 +3,9 @@ # name of example to load from activitysim_resources EXAMPLE_NAME = "example_sandag_1_zone_full" +CONFIGS_DIRS = ("configs_1_zone", "example_mtc/configs") +DATA_DIR = "data_1" +OUTPUT_DIR = "output_1" # any settings to override in the example's usual settings file BENCHMARK_SETTINGS = { @@ -60,9 +63,7 @@ def setup_cache(): f_setup_cache( EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, - CONFIGS_DIRS=("configs_1_zone", "example_mtc/configs"), - DATA_DIR="data_1", - OUTPUT_DIR="output_1", + CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, ) @@ -70,6 +71,9 @@ def setup_cache(): globals()[f"time_{cname}"] = generate_component_timings( cname, EXAMPLE_NAME, + CONFIGS_DIRS, + DATA_DIR, + OUTPUT_DIR, PRELOAD_INJECTABLES, REPEAT, NUMBER, diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 569ebf3921..6f57496dcc 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -44,7 +44,14 @@ def component_logging(component_name): logging.getLogger().addHandler(file_handler) -def setup_component(component_name, working_dir='.', preload_injectables=()): +def setup_component( + component_name, + working_dir='.', + preload_injectables=(), + configs_dirs=('configs'), + data_dir='data', + output_dir='output', +): """ Prepare to benchmark a model component. @@ -53,9 +60,11 @@ def setup_component(component_name, working_dir='.', preload_injectables=()): All this happens here, before the model component itself is actually executed inside the timed portion of the loop. """ - inject.add_injectable('configs_dir', os.path.join(working_dir, 'configs')) - inject.add_injectable('data_dir', os.path.join(working_dir, 'data')) - inject.add_injectable('output_dir', os.path.join(working_dir, 'output')) + if isinstance(configs_dirs, str): + configs_dirs = [configs_dirs] + inject.add_injectable('configs_dir', [os.path.join(working_dir, i) for i in configs_dirs]) + inject.add_injectable('data_dir', os.path.join(working_dir, data_dir)) + inject.add_injectable('output_dir', os.path.join(working_dir, output_dir)) reload_settings( benchmarking=component_name, diff --git a/activitysim/examples/example_manifest.yaml b/activitysim/examples/example_manifest.yaml index a807b7a232..fce4a59035 100644 --- a/activitysim/examples/example_manifest.yaml +++ b/activitysim/examples/example_manifest.yaml @@ -680,7 +680,7 @@ - example_sandag/../example_psrc/configs example_psrc - example_sandag/configs_2_zone - - example_sandag/data_2 + # example_sandag/data_2 # load data from activitysim_resources instead - example_sandag/output_2 - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_2_zone_data_full/households.csv data_2/households.csv @@ -743,7 +743,7 @@ # activitysim run -c configs_3_zone -c example_mtc/configs -d data_3 -o output_3 -s settings_mp.yaml # cd .. include: - - example_sandag/data_3 + # example_sandag/data_3 # load data from activitysim_resources instead - example_sandag/../example_mtc/configs example_mtc - example_sandag/configs_3_zone From e7eaaedad7137802d80961c5a7dde914381edbdc Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 11:32:29 -0500 Subject: [PATCH 058/150] only use 20K hh for sandag tests --- .../benchmarking/benchmarks/componentwise_template.py | 1 + activitysim/benchmarking/benchmarks/sandag1.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index d86082bdf2..596db75b7f 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -55,6 +55,7 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIR modify_yaml( network_los_filename, read_skim_cache=True, + write_skim_cache=True, ) break componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR) diff --git a/activitysim/benchmarking/benchmarks/sandag1.py b/activitysim/benchmarking/benchmarks/sandag1.py index 4342d09d69..3773364bd5 100644 --- a/activitysim/benchmarking/benchmarks/sandag1.py +++ b/activitysim/benchmarking/benchmarks/sandag1.py @@ -9,17 +9,17 @@ # any settings to override in the example's usual settings file BENCHMARK_SETTINGS = { - 'households_sample_size': 100_000, + 'households_sample_size': 20_000, } # the component names to be benchmarked COMPONENT_NAMES = [ # "compute_accessibility", "school_location", - # "workplace_location", - # "auto_ownership_simulate", - # "free_parking", - # "cdap_simulate", + "workplace_location", + "auto_ownership_simulate", + "free_parking", + "cdap_simulate", # "mandatory_tour_frequency", # "mandatory_tour_scheduling", # "joint_tour_frequency", From 6484ebd7f58524361757343ce6768609da563fb8 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 12:02:25 -0500 Subject: [PATCH 059/150] documentation updates --- activitysim/cli/benchmark.py | 4 ++-- docs/benchmarking.rst | 45 ++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index 79045d806f..f5f6a07508 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -45,7 +45,7 @@ "pyarrow": [], "numpy": [], "openmatrix": [], - "pandas": ["1.2"], + "pandas": [], "pyyaml": [], "pytables": [], "toolz": [], @@ -73,7 +73,7 @@ # List of branches to benchmark. If not provided, defaults to "master" # (for git) or "default" (for mercurial). - "branches": ["master", "performance1", "performance2"] + "branches": ["master", "develop", "performance2"] } def make_asv_argparser(parser): diff --git a/docs/benchmarking.rst b/docs/benchmarking.rst index 6ff6b8f895..f28d3a1c9b 100644 --- a/docs/benchmarking.rst +++ b/docs/benchmarking.rst @@ -11,25 +11,30 @@ The benchmarking process is closely tied to ActivitySim's *git* repository, so it is recommended that you use Git to clone the repository from GitHub. - -Interim Directions +Benchmarking Setup ~~~~~~~~~~~~~~~~~~ -Clone the repo, and setup a conda environment for benchmarking. +The first step in running benchmarks is to have a conda environment for +benchmarking, as well as a local clone of the main ActivitySim repository, +plus one of the `activitysim_benchmarks` repository. -:: +If this isn't already set up on your performance benchmarking machine, you can +do so by following these steps:: conda create -n ASIM-BENCH git gh -c conda-forge --override-channels conda activate ASIM-BENCH gh auth login # <--- (only needed if gh is not logged in) gh repo clone jpn--/activitysim # FUTURE: use main repo cd activitysim - git switch performance1 # FUTURE: use master branch + git switch performance2 # FUTURE: use develop branch conda env update --file=conda-environments/activitysim-dev.yml cd .. gh repo clone jpn--/activitysim_benchmarks # FUTURE: use org repo cd activitysim_benchmarks +Next, we'll want to declare the specs of our benchmarking machine. Some of +these can be determined quasi-automatically, but we want to confirm the specs +we'll use as they are written with our benchmark results into the database. Define machine specs by running this command:: activitysim benchmark machine @@ -38,12 +43,38 @@ This will start an interactive questions and answer session to describe your computer. Don't be afraid, just answer the questions. The tool may make suggestions, but they are not always correct, so check them first and don't just accept all. For example, under "arch" it may suggest "AMD64", but for consistency -yo can change that to "x86_64", which is the same thing by a different name. +you can change that to "x86_64", which is the same thing by a different name. + +Running Benchmarks +~~~~~~~~~~~~~~~~~~ + +ActivitySim automates the process of running many benchmarks. It can also easily +aggregate benchmark results across many different machines, as long as the +benchmarks are all run in the same (relative) place. So before running benchmarks, +change your working directory (at the command prompt) into the top directory of +the `activitysim_benchmarks` repository, if you're not already there. -To run benchmarks on only the most recent commit in the main ActivitySim repo:: +To run all of the benchmarks on the most recent commit in the main ActivitySim repo:: activitysim benchmark run "HEAD^!" --verbose --append-samples +In this command, `"HEAD^!"` instructs the benchmarking to run only on the "HEAD" +(most recent) commit in the repository, `--verbose` will generate a stream of output +to the console (otherwise, the process may appear stalled as it can run a very long +time without writing anything visible) and `--append-samples` will record the runtime +of each attempt on each model component, instead of merely recording some statistical +measurements. + +To run only benchmarks from a certain example, we can +use the `--bench` argument, which allows us to write a "regular expression" that +filters the benchmarks actually executed. This is handy if you are interested in +benchmarking a particular model or component, as running *all* the benchmarks can +take a very long time. For example, to run only the SANDAG 1-Zone benchmarks, +run:: + + activitysim benchmark run HEAD^! --verbose --append-samples --bench sandag1 + + Threading Limits ~~~~~~~~~~~~~~~~ From 1c13c610e775ea378fca21e0f376f3802c63850d Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 12:06:41 -0500 Subject: [PATCH 060/150] sandag2 --- .../benchmarking/benchmarks/sandag2.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 activitysim/benchmarking/benchmarks/sandag2.py diff --git a/activitysim/benchmarking/benchmarks/sandag2.py b/activitysim/benchmarking/benchmarks/sandag2.py new file mode 100644 index 0000000000..bf4ee1e2ae --- /dev/null +++ b/activitysim/benchmarking/benchmarks/sandag2.py @@ -0,0 +1,81 @@ + +from .componentwise_template import f_setup_cache, generate_component_timings + +# name of example to load from activitysim_resources +EXAMPLE_NAME = "example_sandag_2_zone_full" +CONFIGS_DIRS = ("configs_2_zone", "example_mtc/configs") +DATA_DIR = "data_2" +OUTPUT_DIR = "output_2" + +# any settings to override in the example's usual settings file +BENCHMARK_SETTINGS = { + 'households_sample_size': 20_000, +} + +# the component names to be benchmarked +COMPONENT_NAMES = [ + # "compute_accessibility", + "school_location", + "workplace_location", + "auto_ownership_simulate", + "free_parking", + "cdap_simulate", + # "mandatory_tour_frequency", + # "mandatory_tour_scheduling", + # "joint_tour_frequency", + # "joint_tour_composition", + # "joint_tour_participation", + # "joint_tour_destination", + # "joint_tour_scheduling", + # "non_mandatory_tour_frequency", + # "non_mandatory_tour_destination", + # "non_mandatory_tour_scheduling", + # "tour_mode_choice_simulate", + # "atwork_subtour_frequency", + # "atwork_subtour_destination", + # "atwork_subtour_scheduling", + # "atwork_subtour_mode_choice", + # "stop_frequency", + # "trip_purpose", + # "trip_destination", + # "trip_purpose_and_destination", + # "trip_scheduling", + # "trip_mode_choice", +] + +# benchmarking configuration +TIMEOUT = 36000.0 # ten hours +REPEAT = ( + 2, # min_repeat + 10, # max_repeat + 20.0, # max_time in seconds +) +NUMBER = 1 + +# any injectables to preload in setup (so loading isn't counted in time) +PRELOAD_INJECTABLES = ( + 'skim_dict', +) + + +# benchmarking implementation + +def setup_cache(): + f_setup_cache( + EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, + CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, + ) + + +for cname in COMPONENT_NAMES: + globals()[f"time_{cname}"] = generate_component_timings( + cname, + EXAMPLE_NAME, + CONFIGS_DIRS, + DATA_DIR, + OUTPUT_DIR, + PRELOAD_INJECTABLES, + REPEAT, + NUMBER, + TIMEOUT, + ) From 91cc417adc3fbb1b01e99ac95bdad85f6d9cc3ed Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 12:23:13 -0500 Subject: [PATCH 061/150] submitting benchmarks --- docs/benchmarking.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/benchmarking.rst b/docs/benchmarking.rst index f28d3a1c9b..18ce0ddf38 100644 --- a/docs/benchmarking.rst +++ b/docs/benchmarking.rst @@ -32,6 +32,11 @@ do so by following these steps:: gh repo clone jpn--/activitysim_benchmarks # FUTURE: use org repo cd activitysim_benchmarks +If you want to submit your benchmarking results to the common database of +community results, you should also fork the `activitysim_benchmarks` repository:: + + gh repo fork --remote=true + Next, we'll want to declare the specs of our benchmarking machine. Some of these can be determined quasi-automatically, but we want to confirm the specs we'll use as they are written with our benchmark results into the database. @@ -91,4 +96,18 @@ following environment variable are set automatically before benchmarking begins: This ensures that all benchmarking operations run processes in single-threaded mode. This still allows ActivitySim itself to spin up multiple processes if the -item being timed is a multiprocess benchmark. \ No newline at end of file +item being timed is a multiprocess benchmark. + +Submitting Benchmarks +~~~~~~~~~~~~~~~~~~~~~ + +One of the useful features of the airspeed velocity benchmarking engine is the +opportunity to compare performance benchmarks across different machines. The +ActivitySim community is interested in aggregating such results from a number +of participants, so once you have successfully run a set of benchmarks, you +should submit those results to our repository. + +To do so, assuming you have run the benchmark tool inside the `activitysim_benchmarks` +repository as noted above, you simply need to commit any new or changed files +in the `activitysim_benchmarks/results` directory. You can then open a pull request +against the community `activitysim_benchmarks` to submit those results. From ff0345c72f3bd4a8d20041208c363e885575f785 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 17:19:47 -0500 Subject: [PATCH 062/150] fix sandag2 benchmarks --- activitysim/benchmarking/benchmarks/sandag2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/benchmarking/benchmarks/sandag2.py b/activitysim/benchmarking/benchmarks/sandag2.py index bf4ee1e2ae..2e33de1bdf 100644 --- a/activitysim/benchmarking/benchmarks/sandag2.py +++ b/activitysim/benchmarking/benchmarks/sandag2.py @@ -3,7 +3,7 @@ # name of example to load from activitysim_resources EXAMPLE_NAME = "example_sandag_2_zone_full" -CONFIGS_DIRS = ("configs_2_zone", "example_mtc/configs") +CONFIGS_DIRS = ("configs_2_zone", "example_psrc/configs") DATA_DIR = "data_2" OUTPUT_DIR = "output_2" From dae575bc5229eb31e9e3132aae0ce13e2c25b7e1 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 18:02:09 -0500 Subject: [PATCH 063/150] note about escapes --- docs/benchmarking.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/benchmarking.rst b/docs/benchmarking.rst index 18ce0ddf38..40538d8624 100644 --- a/docs/benchmarking.rst +++ b/docs/benchmarking.rst @@ -63,6 +63,9 @@ To run all of the benchmarks on the most recent commit in the main ActivitySim r activitysim benchmark run "HEAD^!" --verbose --append-samples +Note that the literal quotation marks are neccessary on Windows, as the carat character +preceding the exclamation mark is otherwise interpreted as an escape character. +In most other shells (e.g. on Linux or macOS) the literal quotation marks are unneccessary. In this command, `"HEAD^!"` instructs the benchmarking to run only on the "HEAD" (most recent) commit in the repository, `--verbose` will generate a stream of output to the console (otherwise, the process may appear stalled as it can run a very long From b7fa7699ccafe13460e22f7f14cb9a8634f5dcbd Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Wed, 8 Sep 2021 18:03:11 -0500 Subject: [PATCH 064/150] pycodestyle --- activitysim/cli/create.py | 1 + 1 file changed, 1 insertion(+) diff --git a/activitysim/cli/create.py b/activitysim/cli/create.py index 0dab8aeada..b9e0fe33d1 100644 --- a/activitysim/cli/create.py +++ b/activitysim/cli/create.py @@ -178,6 +178,7 @@ def download_asset(url, target_path, sha256=None): f" computed checksum {computed_sha256}" ) + def sha256_checksum(filename, block_size=65536): sha256 = hashlib.sha256() with open(filename, 'rb') as f: From 579917ee653e27fac22cbebe1973e976957eacb2 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 9 Sep 2021 09:42:38 -0500 Subject: [PATCH 065/150] sandag3 --- .../benchmarking/benchmarks/sandag3.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 activitysim/benchmarking/benchmarks/sandag3.py diff --git a/activitysim/benchmarking/benchmarks/sandag3.py b/activitysim/benchmarking/benchmarks/sandag3.py new file mode 100644 index 0000000000..32ee4b736d --- /dev/null +++ b/activitysim/benchmarking/benchmarks/sandag3.py @@ -0,0 +1,81 @@ + +from .componentwise_template import f_setup_cache, generate_component_timings + +# name of example to load from activitysim_resources +EXAMPLE_NAME = "example_sandag_3_zone_full" +CONFIGS_DIRS = ("configs_3_zone", "example_mtc/configs") +DATA_DIR = "data_3" +OUTPUT_DIR = "output_3" + +# any settings to override in the example's usual settings file +BENCHMARK_SETTINGS = { + 'households_sample_size': 20_000, +} + +# the component names to be benchmarked +COMPONENT_NAMES = [ + # "compute_accessibility", + "school_location", + "workplace_location", + "auto_ownership_simulate", + "free_parking", + "cdap_simulate", + # "mandatory_tour_frequency", + # "mandatory_tour_scheduling", + # "joint_tour_frequency", + # "joint_tour_composition", + # "joint_tour_participation", + # "joint_tour_destination", + # "joint_tour_scheduling", + # "non_mandatory_tour_frequency", + # "non_mandatory_tour_destination", + # "non_mandatory_tour_scheduling", + # "tour_mode_choice_simulate", + # "atwork_subtour_frequency", + # "atwork_subtour_destination", + # "atwork_subtour_scheduling", + # "atwork_subtour_mode_choice", + # "stop_frequency", + # "trip_purpose", + # "trip_destination", + # "trip_purpose_and_destination", + # "trip_scheduling", + # "trip_mode_choice", +] + +# benchmarking configuration +TIMEOUT = 36000.0 # ten hours +REPEAT = ( + 2, # min_repeat + 10, # max_repeat + 20.0, # max_time in seconds +) +NUMBER = 1 + +# any injectables to preload in setup (so loading isn't counted in time) +PRELOAD_INJECTABLES = ( + 'skim_dict', +) + + +# benchmarking implementation + +def setup_cache(): + f_setup_cache( + EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, + CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, + ) + + +for cname in COMPONENT_NAMES: + globals()[f"time_{cname}"] = generate_component_timings( + cname, + EXAMPLE_NAME, + CONFIGS_DIRS, + DATA_DIR, + OUTPUT_DIR, + PRELOAD_INJECTABLES, + REPEAT, + NUMBER, + TIMEOUT, + ) From 1db6e1c764871714c958e1c00319301f451e44f8 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 09:27:13 -0500 Subject: [PATCH 066/150] extra cli run args --- activitysim/cli/run.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/activitysim/cli/run.py b/activitysim/cli/run.py index a7fdd8fc4a..2ef8b458b8 100644 --- a/activitysim/cli/run.py +++ b/activitysim/cli/run.py @@ -58,6 +58,13 @@ def add_run_args(parser, multiprocess=True): type=int, metavar='BYTES', help='chunk size') + parser.add_argument('--chunk_training_mode', + type=str, + help='chunk training mode, one of [training, adaptive, production, disabled]') + parser.add_argument('--households_sample_size', + type=int, + metavar='N', + help='households sample size') if multiprocess: parser.add_argument('-m', '--multiprocess', @@ -129,6 +136,10 @@ def inject_arg(name, value): if args.chunk_size: config.override_setting('chunk_size', int(args.chunk_size)) + if args.chunk_training_mode is not None: + config.override_setting('chunk_training_mode', args.chunk_training_mode) + if args.households_sample_size is not None: + config.override_setting('households_sample_size', args.households_sample_size) for injectable in ['configs_dir', 'data_dir', 'output_dir']: validate_injectable(injectable) From cbe00f877dc51355951fe61af45079317a32491a Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 10:37:42 -0500 Subject: [PATCH 067/150] extra logging --- activitysim/abm/models/accessibility.py | 3 +++ .../benchmarking/benchmarks/componentwise_template.py | 10 ++++++++-- activitysim/core/assign.py | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 16d0e85597..328744374f 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -52,6 +52,7 @@ def compute_accessibilities_for_zones( trace_od_rows = None # merge land_use_columns into od_df + logger.info(f"{trace_label}: merge land_use_columns into od_df") od_df = pd.merge(od_df, land_use_df, left_on='dest', right_index=True).sort_index() chunk.log_df(trace_label, "od_df", od_df) @@ -69,11 +70,13 @@ def compute_accessibilities_for_zones( if network_los.zone_system == los.THREE_ZONE: locals_d['tvpb'] = network_los.tvpb + logger.info(f"{trace_label}: assign.assign_variables") results, trace_results, trace_assigned_locals \ = assign.assign_variables(assignment_spec, od_df, locals_d, trace_rows=trace_od_rows, trace_label=trace_label, chunk_log=True) chunk.log_df(trace_label, "results", results) + logger.info(f"{trace_label}: have results") # accessibility_df = accessibility_df.copy() for column in results.columns: diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index 596db75b7f..68c799240a 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -12,7 +12,14 @@ logger = logging.getLogger("activitysim.benchmarking") benchmarking_directory = workspace.get_dir() -def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIRS=("configs",), DATA_DIR='data', OUTPUT_DIR='output'): +def f_setup_cache( + EXAMPLE_NAME, + COMPONENT_NAMES, + BENCHMARK_SETTINGS, + CONFIGS_DIRS=("configs",), + DATA_DIR='data', + OUTPUT_DIR='output', +): if workspace.get_dir() is None: from asv.console import log @@ -33,7 +40,6 @@ def f_setup_cache(EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, CONFIGS_DIR break if models is None: raise ValueError("missing list of models from configs/settings.yaml") - last_component_to_benchmark = 0 for cname in COMPONENT_NAMES: last_component_to_benchmark = max( diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index afa8cc46dd..d42062bd51 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -256,7 +256,7 @@ def to_series(x): logger.warning("assign_variables target obscures local_d name '%s'", str(target)) if trace_label: - logger.debug(f"{trace_label}.assign_variables {target} = {expression}") + logger.info(f"{trace_label}.assign_variables {target} = {expression}") if is_temp_scalar(target) or is_throwaway(target): try: From 6c52cd43e69771e03c8da44aed085bc8dff81199 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 10:39:19 -0500 Subject: [PATCH 068/150] performanceTest --- activitysim/cli/benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/cli/benchmark.py b/activitysim/cli/benchmark.py index f5f6a07508..5ec185f582 100644 --- a/activitysim/cli/benchmark.py +++ b/activitysim/cli/benchmark.py @@ -73,7 +73,7 @@ # List of branches to benchmark. If not provided, defaults to "master" # (for git) or "default" (for mercurial). - "branches": ["master", "develop", "performance2"] + "branches": ["master", "develop", "performance2", "performanceTest"] } def make_asv_argparser(parser): From db11669ed9d43348c5daf517b24fa44e51967140 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 10:47:18 -0500 Subject: [PATCH 069/150] elapsed time logging --- activitysim/core/tracing.py | 13 +++++++++++++ .../examples/example_mtc/configs/logging.yaml | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 4f6a5da36f..ebb305ce4d 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -29,6 +29,19 @@ logger = logging.getLogger(__name__) + +class ElapsedTimeFormatter(logging.Formatter): + def format(self, record): + duration_milliseconds = record.relativeCreated + hours, rem = divmod(duration_milliseconds / 1000, 3600) + minutes, seconds = divmod(rem, 60) + if hours: + record.elapsedTime = ("{:0>2}:{:0>2}:{:05.2f}".format(int(hours), int(minutes), seconds)) + else: + record.elapsedTime = ("{:0>2}:{:05.2f}".format(int(minutes), seconds)) + return super(ElapsedTimeFormatter, self).format(record) + + def extend_trace_label(trace_label, extension): if trace_label: trace_label = "%s.%s" % (trace_label, extension) diff --git a/activitysim/examples/example_mtc/configs/logging.yaml b/activitysim/examples/example_mtc/configs/logging.yaml index 71ac15cc1f..43a70cb58e 100644 --- a/activitysim/examples/example_mtc/configs/logging.yaml +++ b/activitysim/examples/example_mtc/configs/logging.yaml @@ -36,7 +36,7 @@ logging: console: class: logging.StreamHandler stream: ext://sys.stdout - formatter: simpleFormatter + formatter: elapsedFormatter level: NOTSET formatters: @@ -52,3 +52,7 @@ logging: format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' + elapsedFormatter: + (): activitysim.core.tracing.ElapsedTimeFormatter + format: '[{elapsedTime}] {levelname:s}: {message:s}' + style: '{' From 064942ec975c9d3028ee50577648a8a0f25bcfdb Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 10:55:28 -0500 Subject: [PATCH 070/150] elapsed time logging --- .../examples/example_sandag/configs_3_zone/logging.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/activitysim/examples/example_sandag/configs_3_zone/logging.yaml b/activitysim/examples/example_sandag/configs_3_zone/logging.yaml index 7742c3ece3..93cf6cea93 100644 --- a/activitysim/examples/example_sandag/configs_3_zone/logging.yaml +++ b/activitysim/examples/example_sandag/configs_3_zone/logging.yaml @@ -36,7 +36,7 @@ logging: console: class: logging.StreamHandler stream: ext://sys.stdout - formatter: simpleFormatter + formatter: elapsedFormatter level: INFO formatters: @@ -52,3 +52,7 @@ logging: format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' + elapsedFormatter: + (): activitysim.core.tracing.ElapsedTimeFormatter + format: '[{elapsedTime}] {levelname:s}: {message:s}' + style: '{' From 52887e6b4a82e1be7767de3492e07172d93004ae Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 13 Sep 2021 17:57:56 -0500 Subject: [PATCH 071/150] use cached accessibility --- .../benchmarks/componentwise_template.py | 7 +- .../benchmarking/benchmarks/sandag3.py | 3 +- activitysim/benchmarking/componentwise.py | 16 +- activitysim/examples/example_manifest.yaml | 2 + .../configs_3_zone/settings_benchmarking.yaml | 149 ++++++++++++++++++ .../data_3/cached_accessibility.csv.gz | Bin 0 -> 1663439 bytes 6 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 activitysim/examples/example_sandag/configs_3_zone/settings_benchmarking.yaml create mode 100644 activitysim/examples/example_sandag/data_3/cached_accessibility.csv.gz diff --git a/activitysim/benchmarking/benchmarks/componentwise_template.py b/activitysim/benchmarking/benchmarks/componentwise_template.py index 68c799240a..5a746cb552 100644 --- a/activitysim/benchmarking/benchmarks/componentwise_template.py +++ b/activitysim/benchmarking/benchmarks/componentwise_template.py @@ -19,6 +19,7 @@ def f_setup_cache( CONFIGS_DIRS=("configs",), DATA_DIR='data', OUTPUT_DIR='output', + SETTINGS_FILENAME="settings.yaml", ): if workspace.get_dir() is None: @@ -33,13 +34,13 @@ def f_setup_cache( ) models = None for config_settings_dir in CONFIGS_DIRS: - settings_filename = os.path.join(model_dir(EXAMPLE_NAME), config_settings_dir, "settings.yaml") + settings_filename = os.path.join(model_dir(EXAMPLE_NAME), config_settings_dir, SETTINGS_FILENAME) if os.path.exists(settings_filename): with open(settings_filename, 'rt') as f: models = yaml.load(f, Loader=yaml.loader.SafeLoader).get('models') break if models is None: - raise ValueError("missing list of models from configs/settings.yaml") + raise ValueError(f"missing list of models from configs/{SETTINGS_FILENAME}") last_component_to_benchmark = 0 for cname in COMPONENT_NAMES: last_component_to_benchmark = max( @@ -64,7 +65,7 @@ def f_setup_cache( write_skim_cache=True, ) break - componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR) + componentwise.pre_run(model_dir(EXAMPLE_NAME), CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, SETTINGS_FILENAME) def local_dir(): diff --git a/activitysim/benchmarking/benchmarks/sandag3.py b/activitysim/benchmarking/benchmarks/sandag3.py index 32ee4b736d..d1b9332292 100644 --- a/activitysim/benchmarking/benchmarks/sandag3.py +++ b/activitysim/benchmarking/benchmarks/sandag3.py @@ -6,6 +6,7 @@ CONFIGS_DIRS = ("configs_3_zone", "example_mtc/configs") DATA_DIR = "data_3" OUTPUT_DIR = "output_3" +SETTINGS_FILENAME = "settings_benchmarking.yaml" # any settings to override in the example's usual settings file BENCHMARK_SETTINGS = { @@ -63,7 +64,7 @@ def setup_cache(): f_setup_cache( EXAMPLE_NAME, COMPONENT_NAMES, BENCHMARK_SETTINGS, - CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, + CONFIGS_DIRS, DATA_DIR, OUTPUT_DIR, SETTINGS_FILENAME, ) diff --git a/activitysim/benchmarking/componentwise.py b/activitysim/benchmarking/componentwise.py index 6f57496dcc..f0889f2dae 100644 --- a/activitysim/benchmarking/componentwise.py +++ b/activitysim/benchmarking/componentwise.py @@ -9,8 +9,8 @@ logger = logging.getLogger(__name__) -def reload_settings(**kwargs): - settings = config.read_settings_file('settings.yaml', mandatory=True) +def reload_settings(settings_filename, **kwargs): + settings = config.read_settings_file(settings_filename, mandatory=True) for k in kwargs: settings[k] = kwargs[k] inject.add_injectable("settings", settings) @@ -51,6 +51,7 @@ def setup_component( configs_dirs=('configs'), data_dir='data', output_dir='output', + settings_filename='settings.yaml', ): """ Prepare to benchmark a model component. @@ -67,6 +68,7 @@ def setup_component( inject.add_injectable('output_dir', os.path.join(working_dir, output_dir)) reload_settings( + settings_filename, benchmarking=component_name, checkpoints=False, ) @@ -158,7 +160,13 @@ def teardown_component(component_name): return 0 -def pre_run(model_working_dir, configs_dirs=None, data_dir='data', output_dir='output'): +def pre_run( + model_working_dir, + configs_dirs=None, + data_dir='data', + output_dir='output', + settings_file_name=None, +): """ Pre-run the models, checkpointing everything. """ @@ -169,6 +177,8 @@ def pre_run(model_working_dir, configs_dirs=None, data_dir='data', output_dir='o inject.add_injectable('configs_dir', configs_dirs_) inject.add_injectable('data_dir', os.path.join(model_working_dir, data_dir)) inject.add_injectable('output_dir', os.path.join(model_working_dir, output_dir)) + if settings_file_name is not None: + inject.add_injectable('settings_file_name', settings_file_name) # register abm steps and other abm-specific injectables if not inject.is_injectable('preload_injectables'): diff --git a/activitysim/examples/example_manifest.yaml b/activitysim/examples/example_manifest.yaml index fce4a59035..6c3a24fa3f 100644 --- a/activitysim/examples/example_manifest.yaml +++ b/activitysim/examples/example_manifest.yaml @@ -749,6 +749,8 @@ - example_sandag/configs_3_zone - example_sandag/configs_skip_accessibility - example_sandag/output_3 + - example_sandag/data_3/cached_accessibility.csv.gz + data_3/cached_accessibility.csv.gz - https://media.githubusercontent.com/media/activitysim/activitysim_resources/master/sandag_3_zone_data_full/taz_skims1.omx data_3/taz_skims1.omx 5b56d0e79ec671e37f8c71f7fedd741d7bf32d2bced866ab1f03f3973fccce8c diff --git a/activitysim/examples/example_sandag/configs_3_zone/settings_benchmarking.yaml b/activitysim/examples/example_sandag/configs_3_zone/settings_benchmarking.yaml new file mode 100644 index 0000000000..16a13be598 --- /dev/null +++ b/activitysim/examples/example_sandag/configs_3_zone/settings_benchmarking.yaml @@ -0,0 +1,149 @@ +inherit_settings: True + +# - tracing + +# trace household id; comment out or leave empty for no trace +# households with all tour types +trace_hh_id: + +# trace origin, destination in accessibility calculation; comment out or leave empty for no trace +trace_od: + +# input tables +input_table_list: + - tablename: households + filename: households.csv + index_col: household_id + rename_columns: + HHID: household_id + PERSONS: hhsize + workers: num_workers + VEHICL: auto_ownership + MAZ: home_zone_id + keep_columns: + - home_zone_id + - income + - hhsize + - HHT + - auto_ownership + - num_workers + - tablename: persons + filename: persons.csv + index_col: person_id + rename_columns: + PERID: person_id + keep_columns: + - household_id + - age + - PNUM + - sex + - pemploy + - pstudent + - ptype + - tablename: land_use + filename: land_use.csv + index_col: zone_id + rename_columns: + MAZ: zone_id + COUNTY: county_id + keep_columns: + - TAZ + - DISTRICT + - SD + - county_id + - TOTHH + - TOTPOP + - TOTACRE + - RESACRE + - CIACRE + - TOTEMP + - AGE0519 + - RETEMPN + - FPSEMPN + - HEREMPN + - OTHEMPN + - AGREMPN + - MWTEMPN + - PRKCST + - OPRKCST + - area_type + - HSENROLL + - COLLFTE + - COLLPTE + - TOPOLOGY + - TERMINAL + - access_dist_transit + - tablename: accessibility + filename: cached_accessibility.csv.gz + index_col: zone_id + keep_columns: + - auPkRetail + - auPkTotal + - auOpRetail + - auOpTotal + - trPkRetail + - trPkTotal + - trOpRetail + - trOpTotal + - nmRetail + - nmTotal + +output_tables: + h5_store: False + action: include + prefix: final_ + sort: True + tables: + - checkpoints + - accessibility + - land_use + - households + - persons + - tours + - trips + +models: + - initialize_landuse + - initialize_households + # compute_accessibility # use cached table, otherwise overwhelms benchmark runtime + # --- STATIC cache prebuild steps + # single-process step to create attribute_combination list + - initialize_los + # multi-processable step to build STATIC cache + # (this step is a NOP if cache already exists and network_los.rebuild_tvpb_cache setting is False) + - initialize_tvpb + # --- + - school_location + - workplace_location + - auto_ownership_simulate + - free_parking + - cdap_simulate + - mandatory_tour_frequency + - mandatory_tour_scheduling + - joint_tour_frequency + - joint_tour_composition + - joint_tour_participation + - joint_tour_destination + - joint_tour_scheduling + - non_mandatory_tour_frequency + - non_mandatory_tour_destination + - non_mandatory_tour_scheduling + - tour_mode_choice_simulate + - atwork_subtour_frequency + - atwork_subtour_destination + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + - write_data_dictionary + - track_skim_usage + - write_trip_matrices + - write_tables + + + + diff --git a/activitysim/examples/example_sandag/data_3/cached_accessibility.csv.gz b/activitysim/examples/example_sandag/data_3/cached_accessibility.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..f5bafec20f9a0c2d92698de28bee5cfb85562735 GIT binary patch literal 1663439 zcmV(pK=8jGiwFqp%sgQL17l%hXk}zyVPj)ub8~58X>4h9c`jpfb^xrs%MK(;6FZZL&{SXJvYLxZ7jfwr&64fBX0U z{D1zJ|Lf1c{m=jH|Nh_q^Z)p_|MLI%kN?Mi`ycXul~XR`oFX%|7w5UfA;BpjP}g^v~P|-=bvxAedpinjOXMN zXRiLWG1qhU*?s-_{(1UYXMgK?_w&v7&;Dn<-}mlw=JTv~y!p&}+tb?WV~_Q<&1dox zb3C)RXRYpM`g&G-*ZB78?a%X1KlyY!edKHT_@9wKdiMC*+^|Px_WO<2zh~#qKGr_6e|wK?Nz1ObKl7igp?%NTvyHQ!ahE>W z_SNRhgJq+izFqq0`SdfsvwGh?9vjv2jV)W!4oiRXQ!T&yWPeW2!uA^Z@O*cB^586N z<~On{ZyQg`r|RA059c$pH+eE%V7@YI&!0A)bpGl8wDs&~JUu@?dAa$cx4-P-msdNo zfp_&YD|_FwS6wM^2E6d_twYdgM%P#LX zt9|oVAGbbhdY<#0@jY2;K9Uvee8Kp>l_%xYwB7oXea~OcY064>{v^-#eEFr(drnPG zR`z7%Ti!hR*YDyNi{IbKk(r(C9Ut4eTMkFd>zsRitUWuEhyLc%=NPPfJlO?*c6lJO z%$%Pb)ZClTRlOrSn4{I_oSdrsF7H0ik{x__{OoG3+UBg~NbDS$Cu=|F$@b2CR~9yZ z$J?XZ=QrM!6P~}B6Y>4ixEd>0B&*GdPUr)Zb=SAc$amhok55-Z4Lx%Bjyc=h<=y-dLV2tYv5Oqd5n8hwta1 z&o9Q9y(6~s;eE9B=J@3hW*K=Q9vBEhDJN7bfBb$?_8@V4lFCzyc`oXXUH}5HHd*eoON?z}KPG=q*+m=H!&&h|flRalY zR;5~;=I-uXgfW}5)_}zt`XN|kdZOA9rpY~5SJ@!3cn*ZfxtHwVK=NDto znTcE6cl9{WJUfd$`Oz$U=NsRA|H&T4{En@IooD86WuYCT%x8Aq{PVpS?KlN&+{{0J zvEZ>(Z(G`ml^dKlyL0wloM&87b~OesJ2|xBE64r8_To9{x^p&>o`81PW!}JXB`-@ zMKF+e5P#2Sa$24d%btU>H@6c99UuDU0kVlB8_f>HF|U_HmTQ)^=RGXE-OhWBF}rKm z%6$)SE$`)h-^bgTqaAaQMdhmLN}e3j?A?duh+E||v1fUCF>Tq6!NRieXJZ8Oy5ng% z z(2tfo?ja5*->}#8PO{m#<~e{kZ%%bwRsx9Flid0^K&;`p&##FO*F8_8YRS$*~+KlKos#P8?z$|D?UZ0pbh}HZyi8zv5=+2%c$?hvTyeYE~X6P9^65BV5T_y8hny_}Mvyd0{INa%`Qx zJinO>olEj;H!M3D%aAh{(-)`dsll`7$r4P)yRonwEp93HG4_M+iO=36U!M4O42^vv zr<|jl{WuE`Xe@u8FMBpQmARog0+VAl5|(67v+ukWz9RN|W$*9O%@K0#K8{QGE*Xi( z5uak1_?#wYO*;g}156W4J2xapraZ<#8@tA4v)a6EYI>C3Hc z#2q-;%{|GsXKi^i2?yia8fi@@K3#uSRx@K@7goA|cilM8MD;QLx%s)=L?}eYi63ys z@nusI6>+>f!$mYg@=d>Li^&Xm^M7rNL z;@`;tvT+OBIGUBUw_d_B=?C)bN9%jPBNxSQU2%{d_-S3o;GjvE>iod?M}6DMr#%NYqs z@}56@jR9V~LvGk-Vlcd;Z@zkQd2^bzpg9v07lF&FyO2OFXFgYkXj;G7NxKrk#<*wE zIL^5D&F^f&-@JH2u!NhO;JE30Pi2y^53eUUlXaST|HTmn}=NDXpu;{eo`u35ta^S>Ugh-N71N&f4z-B(k} zxS>2V=M}@0v@C zSA-NXig(3J*dg~L9zN^MiZuwi!g_05VZrQm}X+q9`c6d zvFvW1inKcqpA;{r%h)%YI&kb8&*MqjTKY)>l{>o#0FsLu^XF|RAmW5$)pA{PnsXuY zg?IJbT`tPeF|uVzZF=oDaq`X*Flnzb84*IxW$gRRJ;waRN`Gb^8yj25 zN}R&x_+`18sO)o=pL2P76H9nwD6>U5fsM2*tB+q!1hH@v`B%P+Pk}vY6>6bT4?eJ8WX~2uXB(ZnGX*hTU?7DS~`9``;$YKD`GH}J&$uKav-de{2O} zdPNhJl4}824y|@BM%+)XXg(TC7w4Z1z>&mWA* zR>Dh;F(=80CgTMa3sJFoD3H9K97f+O9zCk$RpNLQI+D+SlE}#5g_6c{-;)&27T+A_BO)z{2IpXK@m_8m6RgPfG z9jzl1Bi~JCd6;DkH1HTnBsnn?JiO!=M?i>ly>R>ikxHR#iRbgOUju?cT1pkTIaHJ)PPiEyc+ZdcYY6@b3UV_Lc9gJl!5KB-006hqq=U^nZ z1qC70GN{_V(%6f}iVRNTy~!(MBFEpG_zDYdb|J4bFH0`sv_8PQK-0QEBW+IRKuDCQkE4sfeT)wpDbHpdC#DVH0N?on0dd+AW#-xL z;>{Uz&NsPmZYNN2vZ^>TGF}W)+yFZt3m9Ackhjm~uX>V!b8&QR!UNKmY$|)f|K&3j zcRnD=e67)FLW$&oxy}iws9zjmmIk~_)K82{StKSYhhtJe=!RGT-ngyYu-t`fS>~e| z%>LzX18UZ5my5@s~yZ=CW?N^4VUMJ*ooje>!L`O@5!r+DNU}& zyFJTjlmIgxl{%pbSKf^%X0{~pg8Q63cnYkDHC}-IIj-2c!NGrj#kW}*PFB@@qhKET z&X&!}-^EsF7Zd-ST&$RuR!&coH7QFZ465?uv%nMc2Y_p?o#dYIgF~WiNcfwCkBjpV z`W*uCY-Yl#6-xqMo>VQKhceLs>HsxL))I5heaoY)8<@{336eQ;oEOQ727OR=1-r-h z>pH*DD4^zvsGSN-B|6Miw2nGGSVUS$p2-uBTR=)Q!3+=nz^Xya-Afi3(>@K4vH>|i zg!no2F@BA2CU5HCYw^3F{rPmBp0Fh`$N3qXsvl5Y1WqE5ICWo%KsK>*o?n2cCl-w{ zCa7+r8ViR@aV^2nwcx-JgD`m%r2diibM+6AmwpW}xKnIg$Bid~&Qd`;J}y@WSe!%` z*_(tm4HzRYX@YA~MTptCOBZ;fTug=WBjXuWN6vmOfX8X55tEZ_E0H6?jLwJPkrZMO zpr3M3hr(J+LQa-7JrH>BP!g&_eG}3FzfiQ!QJXrk0iu(4PoC%60i&lSRsh`1+H;XQ zFwO#=|DBrkF9a6Brn8$5xK5$IFBp6RWQyzGdqeqzD90NsJePl-x?XFJ#9Q(7c?j_9 z%@NPi4~bPCiE=q-{;vJ?#K#ig?y!|Q7Amy~zG9qWd$w`z1MolunyZK*JtF^cYxy!y z@(Y}yYoOwpPw-&N7?$dZp8*~JK)7>2^-cq`5hES40>LOIA( z@eFt2EQ*3oqQ_i|59rY}Eru!9oZ4Bwv{#fHvzNc0^;I2{LzHD6Zfv|bZ!cDe+>4y= zF@go{A=uB$0^Ja!1Z~Ym<+#P@3aK>+6y&13Bvy}Ah~dd!%Ih$u!3bls9?D+d@5l&l z6G-zC31GzNsNZPCtXB&nlNjVa4+`q_fE{3J8a^)Tw7KtMWy$)7k#J1FfmxCCzJMZg zja;BLiHHp@4v%_>5??)2Vs4z*N5Cl%4R7QT5TRu9Tes=j=W!&4?iFWEl9G6|0U+Y_ zS|CMcs+Q{0oWj~j?n*Q=5!p$A?kg@L+%QsP#91tGx;oOlr0-0g^s7uEV(2vP{Je~zAs(JXo1B~2psyQh+B^;gnN!3P^sm92TY!dm+HIU1|G~f8L_^k7S z7;qLu8x6(;F%qC~N`N;mp(Tg1xj`@ubP`zV*k($V&0_;Jn*fcs6PIXtEy+i=9% zkSgM6vvVF2v5@Ey{=NVxiEHwmme3QVzuEFZpx=K5yCt;CGab1zajIV}`YT6w1f(36H^M2bD0hBgS#>Hw2RBg*eu~3iIQgu%qH{ z;H1{WXCwK8Yyd%rVm#ypva@S!)-urA*jtbf06=4c273SDy^F&`OgT9t@934*a*kfU zsaJTL`?Y~5viCsfkO(-{4}sSbAMx~o=wa33oY^I)DbQx}|4|PxBp^wM`yg#ShGN-l z)76hU`_DVwqxw>>8Ws|a5sm%1ta*6?cY|&{jpQ zW47|bz4%rW(gejhmb#l~CP_g;A`qY^KZGzUE+yZD^?CG6UDL5U0NVJiy!>i*`>~#y z;Gviv+?zal9{(-oT+YG=?*qqaS+7mmV!FOuMl4{F4-zjCtQx&P3B-Ao$ya7&9vxtV zS{E4_2QM$`#ph?6lY;>skqJZ78M+V?k4dCMeI0Jw&q`J)k#cwr34A1hLN<>amikn} zg1U#mmllZA!-S&zJvMSxFH@J08IX3er+`VMO~jS?8^Byr9nN z$#L;ngNmtXQ_^LU?fiFu0=@^-=aa+sW%(~3A_$w13iB5qW8mx$)xM7mm2;h)PrQ-W zz%LD|)_3(~w;(IP%AqFrRTveQM^+C*(c8MDbHzREf}n*|fyjw}1nXgDZG6w7YK9GA zfw|=a=yDe~GIt@yjsJ+u)5+|Ejb?p{yO*TGYgU*bZdYL8MafW*+6UQj(y&u2Z9Q z-=Ug~AMBV{ocM>TMuUlUS6lQ+oHrhnB8`Wdf)KW!bjvfH*BOiPEssK7$Bxy^SNt^- zx$n^O;Hgdg3Krnuo2mZ0`4+E%E|A*8QiZ1C2H7!ns=Z4eEwp{1) z>NqD>GY_6~ol{{FOnp*BB8Ec7q6O*fXd;a{FC_}hRzQ*O{|G0js87j%T(plxEbSM3 z87K%;-IHYE37h<`_-pum@L1OrJlYS0?UiIbnUPe>4sJZGfv+2Ez3f)r11QRDBwoeZ zj_L1@oTYWdRHubcexc7^e6MT@NNEz1n2LhNfL~sAddMePBn4#XvBfY}v6I{KeKWq& ze)m&3E6#2=>*wcP(*1I<7C9ZnfKdz2&ioFy?v7KdyND00f(nq51xxaRR=4aiw);S% z{0zsjH!Da>`+cOEW(|!1Le8ePy%VZD0DVA5S=7f7ikET^@KvKY5fozyask;vn;$Y+ zxl!4(H?OUcOXUFH#c%m1Id1U1!MsFGDleJ%>s!xT1QY_?d(??ZK>uQC^P&9PkcP8y zB+hb1ymgSa^QRlIzl)Y)@$lx97{A&n4srrpkXUPra5rFS!P6W^*SW|f6Qu$w#Pr|Q z-x%=Y{>I2ssbXaxNg`|p_=3X00E>qtBYt94G(wdbMgfVuZZWP0awfJYW-4mAT5N>ghX%Ot-DZS$Q6be15NLD#*0_fHGtGeiVc(lnOok7i$2X8 zILQVD{JLmEw)Lfkay+1KIW5bfxV51%rrrl}m{QxmjLm#2RHXs;oTbtb@mcnRi+#C@ zr`XyA$A@E9OWiVXq19s?mxiv=&I4O6=9YS@8*9A`Z}V0~3-Z8*AsCA-X>hPdVzxkD zf{v8R$Vyke-FybF6-gj0=>jGfbqR_~c$^{jNd~_PdY8&i)Sa*0*e8OSyY|Z@0)>7W zh(V>`kQuGbQ8_8@68tb+MV943%)OvqmU9hj*~Apa7a>-5rzoFtmE>nodMkkJg?J=q zm&^-B2Otcn_eXh${P@Kb!Q#m!`T*tPjvA)?kOBU9+gnFTk)uJJos{4&v=3o|+Rh`+ z3FQIKA;4u3mZ4YyLG)Y#`EbJ`M1#(?FH2#1c#mYMA6yE^X|hyxL=GO{;Sg!WMG~TO zS146HW}L8cDU`f)Q2dx@dz4GGIfB@4*)T&p$F%hZ(s;WBE~xZGJiCBuqN!%c{eCi6 zo8)|e=f<({=n=Vtlq9YaFs1wnG6|e~W)bSSu1$_!?2B~@0qS^Q_ zj{Ly4AR8skHnk<9fJ_3_kzjD$g-uK)bkkl z02DOnJ@^Z`tn|k`d^zS|5XF+WbL>8T|3=`uc$;+0$h6~io3>H8q=yP-hT!-IF@S`g zjK#E#6fhPiH?J35$Zlt+^Rj^7ID0r-I4BU-;B-N)%R9v$+La~x}dG0r<56C1N z{XwT6Ynl6WT2Xp4lK(A!*CA{or%+Ht_Mxk6+&L*v0j9)gu|jl26;towy<@*2q07|n z%}VE?KW*`RU`g;SKB}jmWUfVi4MTZhY4{8#A@Kp=1)Q2LxreNiAf51mS{JWn)8R3H z@cFJ~(Vt?~XB0((RIN#;&fyYrgKic#--%b}Mbq;Av2+tq;1i^U{<)0O^1{5%@IoHQ zH-OdrmR$_6wurf^I7L&KuRL$P8A^tf|8IeGR}C)l0*^_;Vt7xk zMRd~mIt>Wt6Z(7JzE20WFDAGX*DUD^7!G3YgLNtxW6&P~t1HVgX(9O?G~dBF7d~&& z^2N0{YJe0_Ru9&h16^OIHXGWMT4;^PYK%()>dewzJCVM6^5&X9238Hc@ZVGHV$r%Kgg6L$w z#RqwbsES)e+yDujjvhY4t4tHd;Ou||ES)f;3MOx!_ZQ$7^q3=SUhnsWzMBq9JC(p}?2D+ktKmb%tl^v=$zy}mtq5n$mu~tf$DJlBa+gA3;FE2)GgQLv^VS z&47#J$Az{gllTjIbOLIMX_yZvb3~FB`q^`UzeYzw_Jdor$u*z_<{7)p45+yzJ8|Zs7H_5U=Gd7M5>YH5%#2HWl0CPkp!;M*DK^Mz%I_8 zzcO!~YfbwSHy%x<9ajgW7k<*II@bUsZn1;gVmIIdL|xJoYx+cxaNCHH;D$a^!(OaZ zR-*BwZHM*e^bV60z)eH)7vE-OOt>VYr z9b1!Nx=4@$qoB7k=Mzh4zbF|n9Xb>Tz=WtRJWT5+nxE*~2q?I^BB1E2emO(9_l;Vk5W82wU(n^gSXW*H0Dl~)Ss^fci`Xv;A%xzLl2(JBv<*aIH% zm$K9fA?U2;;1K@8uOI#Ka%HU;%OCfhicZ@yt?Hl%Y5G~|FErT(Kn3WsFy zGU<`8VsZ?2#gxe!m-d1FrY+({>7)pcGEo57bLBAaxm(ajC}aa}Y9rU8a0i?=dwmA5 zZc?U8c2+H!ANd8vqujgpORT9UHXJyq`0tPl1bvjD4LI?nzuFHzmN>rxkHo;{rL>BC z^)s|JLwSTL`RR^`dR{8__(W@~!vvowk}vl&o{|Tspx<5?OD-RsiLr56DyZh59UwUa z)k)~Z%eF*)*!;x+hBmPqPyi(wv|ijT0x5gSAmY?Ls7`dczBDffuL?mZ2k_yh(F0BA z?<1?7rNi=Hvfk)c>SAg#wXJM(zAhMiSgL`1}`5CJU|$)4FYRgILv!m7Zx$6p|PIx zCv$=V$-E0ZP#>cmjPN)v$7uw~zMiNUnW zS|PU@|Iv6UR<>X~k)LMQ;q`!)VE}Xa@7mYD3|w5vtCCq2cUqv(5D}&aiSGmEz%mHv zaq`6U&f6lO&B48wv(tzSEDg5tYqQWiC)5L=LpNd<_Cg4Vt>kM*p>G&x(AZa) z$cU^7_mcfeji-@X@-PI)RS_99I6c8 z*bi+Bxu~T+K?lu`YGL?B|B|j4QnQUt(BbLEL7YJ2OblIu88qasvhrm52VSlDIH=+S zCyZ80Z*R*5S=`%(IeqJZcN&@DJJ#d1f=$H34;XIF18`m5gEejmF7%MVCK=QVuJifI zV8HEwM+2ZFvF%2laN%!BeMw}=!@!<0)}Semsw|mu(ZKTqvUg+Mtm4xrK$~20@MBz% zh)F(YtUB#4L^Z_=4#7NUxEW!Q-ip2-T%ycjo{@2UjhVBQg zVe9aWQY6}Z&~s}?v&@%DMt|%kp+}FCOwZdc0wl35xeCqVz354x#pt(SUn_e~572yYwQv7kOW-xmtc=0j#FM?HxsB zAiJ0*C|(FkDyJA5-!>?Dsm(6gAwVWL8LESu>VGr+CpP0P9Lk)SvfO{Dj=49TWD_q0 zP=qcnRY;1brw; z9~5Ii0kU+eVHk$*`c*W9`ghx9U1A!h+Ap~+=Fu2RppS%EOGP4cCe8zpQz`7w zVrmJI#6zO2F)VS&9gL?r{z~U+_%}x>udcBrBKdb(@U8P!x24F&sM>2M9 z^1Q1ruI_()K-8~Q#AA}sPR992T=rs>}?Pn=oVl(WsgYy}MHzF~SAEu9fd9)-5av&0aO(S50 zx(@9Vq))w2P`p2#!W;vm1_Cag4!GOT(7Quz29xlVW(PO$)%ly!_T9GatGhy6ZzWXC8Vq_p*f-2w^BG_{lhu$xi9h4%gq>cdcE3TX#^T zL`^+?X8H3%;9r}YK8opEK$2n{4jI6B?{WEwz}d;C!?I&`{i}O8CiVQdiC@FonxSBNhD=mmmr{!Y@6g z|Az~CfOumgKFhZVi<$d3DQkFZ#$s$_W-bi)qymnha|Aqa@^}KxnA53-%QDiN}v4r@5 zPz^;H+(0bG;Vvq(z>xr10OTUy?L=ENo3eH=kX)Erd6$UGU&2Y_5PDrRC?e8*F;-RE zc#3U7Yn>}cL<(9H@*miE}OQGMedhBM7VVQbFC=SbA*a_?LxYfOaUQZe(0;v z6Qx2bSy_qvzzzwLHpGd%Rdpgz=78QD4_j(2)XV6UR(z!B092a=pWy;SK;h|e@$N|9@KRqQoG@W(5XzyipU`>qD2pK6?y2hwFU6l^oy+r zN~hheN*w6!P&}&wJ69F{wT$|g9;Nr1Uk+gmCD$TrrwL6;S z(4y+uWc}KK?q!Z)MC*eH&P_&$1JVwp8Sqs3{=i}FQ?Bv{9Q7h}Ks-iU?buib<(z^X zS`-!)1v-|#2xi8+ZVCvt?Sd-OgHKCDyF@UR*9b44TiS)NzSQZgW;+UB=6CsGF0e565gUTyJ({Oz`M|Vp#%zzb^JpF zNw+@n{rCJVOr@fPg1LR4DZEh#WW@(|u_lXDRHag3xnZQzX+Y1_d;p0GO8^L$T?kBU z(O2oYfz({}z;~U;AbM4dQLF@_=yjw(mqG=p6|i__RWTHN%CRKof|Egaq44ypjT8Q=im zx*~AsQ|Q~5f)fRAI8{1$)pz3^e<}k=!Y4DE{r{FibGQ^RnN;GNam`XS|sAAByUKENU0}XddB4N)iSMI#m z=K~&5vqBAPm?eS*e2rF~1ej<#wC^syv;mPWfSn0iQtitC10Rnx>Njm81=+=S6|(`b zQ6Yfk;~{m2mJ@G#N^C&T8*Br9#1xrD;eHuxs}aa&imxv9VJlm6ET+NDZlk&%keM^G zSg^70!V48?GG+PWuS%N7s}7M4VM*CNl&4AI&33Jm3;RC?Fx3+EFunq@O%$Pqf;1RY z!R@K5;B>$bAB4BtjxYLffJ(q%K03a}v3O;(!Vtr&7H@!v2MSv)y>yN=Sn6^=)yG7> zsTxTx$S!H9M;U4gM(nf(0>uk^7uzes&IwY{1`fV?H0=Y5z98&TVo#1nq0=9d3)}Q6 z>1k+8eu-2qjs7SB#3&H~v3OJpkZn9l_uq}LwsFKZU%m7Dnur0!)2^bXi}cNpoVG2+ zarRfH6jhMpkc0w{s2}C*(I&sEMSWULUqJB&m$d8=`;i5xVr&!St4ScCL+=%%O8xaE zn}|s!*XBm-rkLs9xWJYpP8EGY$p>)>r8MBvumx|4tiHr%OW})B@&Mqbnc%>`l;GGT z`=O~0JSa8>4oyUWYF^QO4>bjvkcJ&J^a9CJAiiDPQ!W%bpo7+fN6e1qXJ&acilpDK zuu+^*KSQJCQ@k%&Cvy5`%?(i0GV6u{Rp}1W=16Nax!d*(lW+rdV>t~;8Awo49M937 zFjfVq^^@9UuK-j}5U8c}1;k9e5L9Kx@^pn?DOuzEMSlLKeASp}gDPGLkl(<%YYe9x zkV;pXGl6O*SavC4WIc;_dK4uBB|zhVE>AiZdQAW*L5*W7WwGxd!9w%+18I-dH~k3u ztIEX=a(J?=K~h1&q*f4z3>o<*)%&hqEO9%B0t`3Haq*vr7BKwNaMRT^h|V5c=PGl+ zj7>WzG&6CZ>cFDWQ`%VAJlT;*5E?4egxCpB3EiuC3&jV5djv-?GdTg9CeXg6gb+y* zjlzi{hHAgsuSp~fE_ip?LEK+!Moxrh6E@&eU2IaXS%~QRK%pyYT?&RSqgu#rZIDj!N zO#{>{u?p0Z8BuUoUkc3Jx4a67R?k&+nUAdr(guE_4XfhL|v6!9Mg-6v4Dt^n-ZEHSg=G z@Gn0B=Lyv0y|90;LhoD8cNB5NM+<9L(-GaHhv;*Q$#ZQ`=%CxIwF=Hb)0=%{!+V1o zt|agceq7n(f$4CtW56?1F<)7T4EeI|`w@WiJ!D+_hGFWNFcg6b$t*fEXe9UoD>u`t z=JcsLD*BhLmDoSYH{trGxFMKKB}@ebFc|B;N;WawT4U1{|rT93ebfjivd? zFAd1Pr1=_s_Ex5#yF|&M?im6EdPKQ%aK}B2s ztl#P-9YLHU8>iQGR6s**kp_wEeRGNgz8ouyiCNXL0g@xKe4J!qcdFu1_#`@`EOjje z)xK&EzV#kRC7&nGy8EFfg^uc+a?#MT10$h&uEZV*U%NDk()FNPZ`)oCMH|js zBTEK(@ut0~Zcy-<7DFH>%g1fdRhI-NydJWvHMKzM8V#(#thT+W**w)t=r%%5n>C40 zF)*{lz^ib$JG%zT?k6>9rY$|^QHT22ce)D`uh zQ3*_m3rcY0T!4cbq%2y1=gsATAKzeKR7^%f$gBo3C$eh_dq|m0<=Rm^#JfJ+Xt67=Bghnl=&`ZY{f_DJfpWm_}5gaOX9Nm^LFAM008rfGVN)CHQMLj}aogvTx5Q zq{FCO;mJ#<0}^JGABlC0weYQH*(EwlR?`V*5UXtsGdV;} z2E^O^S0C-|ata|7X}`1!We6MP9t z&jsu;RHUl4zX}QN_L~tk3m3wmvzPd-It|>E4oP~d-S2JQcJG5P_FlpmaP#ELA>6>S zsGK8rN?BbIj*Q~*dHbETZKY5+}r6yVK&?9e`9th7}J4X<(jyx^q zhUTck<1FbBp45PnQKClU*w!g#hwq{uNi+FU0vUEVF({-9dnwq?y{ap0ik^^vefCml z27Nvvl>;{~%f*v&{a=LKKSd~1!|{YMOzHbc@;94#pmjol*bGyw+=(s>gce#a)-0q3 z8U)Y}6FlqWo~t>l??_x|4n63^o2|Ni81SXK86Epg0&pkXc^wWpY$^Z^{6iXkWBrtZR2b_xn#h|H0{8HVT!sgwvfs$G&L|Tra zF8JH_DxV@6A+wU^UxDl+zW`O;(oIIhi%ClWl%uHKR6C?0KqXBZ%z0S>G#B1I(xZG~r6HJs9Odw$fOTL&ToqAtpiyy9;1(h+vnqnJ5CgGTvC zsH5aBHo%g7lkao8CWbnPOrxoSJ8db9BtcS842McPyjV>AC(bifApq=vP)u!)oN9a` zkRV68CFPm5F%WR?>MOEiLMr`iSTojzme+HX+1;%~QyyoiI5e{P}PxRr349Ez|tc@_7fa}=wfMk3L@f*vh1olCsL zISa2dDVc9K{(01S0w$&wS2EFH>-^6Q)42=ZH7dr!4180l;?@K>icCM?T^Q{2oDfba z8K0Ff;zq>Ff#RgCJ(Tued!*4Ytm#wUhTPV*3e=Nb=O`C7(rw>E5{K?Oq*g3azFT&_ za^FTWb}Cbl;^y3&S{g~s2FTax#j^zYu!J!G)XmlqQU0NZ0O6;297xHbUBYw~*^Ug8 zfNoBYU`24oxg)-)XQ3GM%GIkGr~~ zt%|vy^WVNtcI8_c6P(0KJ(CGPifYXYr?>rI+~q_ z$3c|N@#4mOg76BU(wn<1TfTo=B6f+?FV712N7eWQ5D5Z@a%%)(`9Z*IxgpluIp;a> z8#eK=004)gOHd+gZJu}MWx^+7{&V+3u=3hzg_Qascpg&1nE^zR4@EfVF*DTKoa3m! z!P!Q$`!2p%OX?_tip?4y-2m{mT_vLgzt{rXW3CD=<$hag{F|E?|B27-)C{m$sQ4Hz2Qn6*&o4vqW6KI7Q8QPWWYYI$nd9eru>h9c9=T}loL^;nl!+ED2%br+1qjZfaCmD{n6)Vo> zQ*=|U;V3q67{c`y<$%~72^&v_VehCzvs z3fOzKLD9i|OMa$b)tjo@!taJjNp+R_c|S^EmUIkVlnmZOmoj&})~-wv{jt;@Nmg5x z<9Q7EP@srK-lx&^ty#p+C73u517O_Le^#n_K3j#ksoT+74DYFc4>8iz-R z9@1-+NEb+?JtGjnAEnU5-ig#`NJGK%*2%;e2_{e#Ad@^L#k0srQT0XqTz z4BYe9w#9Z4Gc;58pMg9D0UufgnjU!8Ttmtgv|~)QTL>~Ozp9M>J=HE7uR-oM9E#)G z!GG=cIzHZKJ@FO#dJUo(&*yv$u*`OW4Qj&R9Aq$rx{n=!-Fhj%9`iUK&yMZ=zk-eV zGu-+j(@}`3T2q!lk1`GYrRGR~!)uqzv)of~u3RmY#7NyRel&`ZTH3vB2>FzO0cWiC z7AI{Vc#C)Sy%DESQ0P?&@^6yO$^7a|@q3DCet4v0B+Ra<~P z$@RmO+YiivBkZEF&URc-Ccpv<9-ydq^{Xgcj%Z-6>A8bSlxHK>ds;~yNVq}^3L<}X z4KuC)-Cw3~Nc06$=-4)4XEPJZF!Vr;R*GOBuq`AF(!%-#bY5&UQoe-l%`lxZFcl=L zla$|}zR@ZCvoWQ_lQ)P4CG2@c`jK&9q}1cYs;}ztKnRqo%&C#6Cn_O#-K!M8&~*HS ze<@{(Wirr<#UmtiO8lAv&+D|OPc4q4>^y2*T_HHBkdSS+6ZcNV?r?iN5#?B z@EK0iZuc?NB|JH{@2HbB3DJh_sPVuo9idpbEhympf`M}f$e=||_wr>D^OaA@I7Ygg7o9y$Bc@B1 z1frI%MiOf7$`0kFJk@Z;xEPWs<~AMCQ*_^$SFM&k;3%1uD|t!@M53Yxr{Bh^Jv za!?pmfC_APt`kWS(rnuiq&Bm$Vesfs^!YiZ%Mif)BccE&%o_-$c{$sOdQ}l?7*p;T z^>a0xEDXT{`*pUXIK&X}Ddo|K9HftKt%=fMF;@8>nbC-xPt})x4u;LwVIB^)*~fr5QgxKbdK*DKQd|Lkp|?%J6#2u31$tZcCnt0 z7eq-{9TSK@F*k#DFTaTWu=597ZLkVm67tyKn(}>Xe+&^Q{~WSmQ+0Z3$13@kWu}g)CBP`#QP@WaOFl#Sy(k@Ye-E|2;!+G zatC^;C&nO9;d{m}L3^Q4)9vnrs(^2LW(^KiYu?kRM*3ZQ6_6Oog$YJSF1S^U{_BqH~sk=s$2B?9{jnx_*gWF^bjMo6J;)0W_)L{yp_~f(&C1>4pXosoB@cDFGGio2pW9!q~0i4Ukmy z$*JxnxoubRfsICn*BR0*4YPuY1->hJGn+4+Z80^;%h8NEa?q9~&!}p4D59YbVT%v- z0@T{N8OzQ1E^6d(+UKnqODw*XbUIwwaTz<0O>5NNTCZviOq#q3^nygZs2x%J=lkEn z)#1&8wPwdkJ(#I{K<%W(-dJq@{?z(Vaj>54aLV@wba#O`8j{V)%@6t~o1##R#69PHay6>{b|o)iGG}Wa$+ zsHL}U$BOg-9C7V=6t^8$Dzmp$>uRe`+RkLfl7u0HMDt0}vG=N;b41~ML*+MbBICqQ zafG$tsjk{2G$OxC*i$XjTkLyw}H)^SEq*w}?^)Rd;u}u2P z9`zrYGfQ9FTNWOzSH^HKqwBZrw|YlQ>7Qv#{!?8x%&)gWbXC(Bk|Fz` z@b^hDwAKI;Wm!t7I7fsifhjqo#)T8GHCXKPcH6^#MXwXK;43sx2Bt>M>jYqx(*U{hkn;Xhx(voKxR%Bp^ z-7ZSPkD-?r_4|+#XQGE<)|U8C4(}p;O+nEvp_;^VC?DglF8Qbh7~UQzJKvRg+81${ zK)7_5P1Py1q+mLnN{g#U3kf^YG2M6V?+@|*Q+o*H9HPjurM&;Ms~RIZwbtzrb@S+)lyST_)ed~`)EZwl5KW3oSayS zZ9{Lr_;uHY6vc$nJ}9uE765FonE$al2_b{74Dz*Qwhe~AoZ^Bt=yjMWrXjvLst1<7 z#i=MeNT2Bd@B)H0sNr5OK#vZT90bfbGepH`(mNXfoxZ7_I1MB~%CrCWy~eZI#bkp` zyNDf(4l2B8@E8fB)bgmt!lb`C@U#hRyIdST3`S)FHk|pwo_;8FqXV@j@yL-VZ!e zGbnB$)P#8z*^oY_534EJF95@0>4zD1=85rc^2H`uK{f^*@%9wWDhs3N|9GfQjd9_= ztLX~@p>sk-V>pVH>dReuQ`naisyHEI>kFL#?tvUPFPs49(77FytBUi3=B_%I^G@j? z$>WAKjBQ>7m84zb3qu8>($2&35pWU;9X~W5sjUmrzi1MI16`^UlHAS&qO%@)t0SOp zSRfmzTb-u7@%CaMx9)7ijgUe~n-3I7;U5yV%`97ls=n3n^+m}H|^hjcUXf-QpuzvCe7yY^mXUsMAOh~PfUzH8MY zp`NT{3DPs(Hxn~jb#GGVh?_}hV(5V$2S_@JDMM3DaJM}a!`T@_^ma|PZ;^n$K~?=wuvBgi#1u3k^54lGy>CRqFyHD&49e-UE_$-qK{E+99O&Xoft*FIyX z{ZjvOrYJbY_uG533xMzj);ulC!;K`K_*6ff!b2QU(q3%-bIFkesu<7Q8OzpKtB<+T zPJF4%PCXJzv9lE1ZP{3+Hg7fQlJ_o!#gR^MrAH_QREJ#Hc#=Xbjn62pzQ4tx-nE9j8ja@uq(xKqKX79?4AT_|2innUYl6d^*HqC*^0Q~tJCH_F2{ z8w!h1Uwzc-$tH-z!wDJi0#KEZL)MDR2ai3wd>3D8h3FG7ZO4J}rUs4Mft=>EYwX`d z&Wkc69NX#&+lXS?7zriZCb;GZ`i*8wyHpMM@MvaX{%Up@8id1XLOA}av(i^!oDLR0 zY^%2hLkYFUI2#{BH*F3?yX!Bu?<~<0CM+x6OQJ|maSTB(8aYzN&Srd+C*X}w$}^O! z*xp?%LcnXsL+(qF+)jn%>y_ny(5qOEK?0LB+-H6ynGkIs!*LwnteGCzjMBCM-cxJQ zG~llN{JFwq3^xXmoJDy9nP#$5$NXthkMkU1f%6jnC`+W-AWPbJR1o_IO=ew=D_Zp+ ztz`HX;3y#h*+Hfp^7l!}DcvcWV?X7qQUX&GX_!|f;)OIyDhqaYZqIxdXu&E;MkOs1 z^L*%}fsTc&L5-532KO-*#wk}Y0yr=ze#4Yh^88WF9C4wdnzI4Z*+Uc#jE7%mm2&PG z>RKj=MLQJQu|$kPuW6{$#x0(YFp*T8zVu)6+G`*UA=1&EzT;-5)Ib?u-1aSl^9OK> z2`xkfNGKVvIf{(QZIlW^j7Yxo6_p0@lh1aCX~`1%iiSDpX{)Jg`f9X{nD%9+O*V&R zcix5dR?q_aW>7iGO78O8zYL=kDMRRrVL@B{Ce&q5!Ff-K*3z~?clKxRY-x~|1WcFS zptJ}f8PrsQqEFH5C7X;!H2l_Gy4nu#@Lo;?e5)$}#4;FNsG65>K1JYzvX;eU*jJ9I zpO;fts5pXTSiYikfaNmQ*bxF&is7iM-v>%ll#b<`9O~&5D526ImAkYGr0uqWAU$UH z$hmzJy_d!>3^b%TU}%U>0XSM`M)+2}MrE)=C$R#TyeUM^-i%bsX*>#OaY*mN5^Vq3^N93Mqx6*2gQ zLXWZ+n3~L8d`U0S$-pF6#&c?xtb$O0<&9K#_eazNr+{RwOtX#p6Q$LX;)3wql%|GG zgA1b_t~qA*h(;mL`?2=(YX`Nzuo^SBlW7UUHv|%;+pX<%a8}X_QufPF#W0(AbMd$b z{oMJkh*$u_jZwuC`h=Z%-7jN%e@aebu)7BaDi&h`9%W_at!7qqt~{ zBm^~cmIA}$p5sZ-0mcKoPN@NCwd@J|@3yOys-`N6WWt}4Y9bJr`&Ion=gwsPYB)U_ z?|+=&#ygNkew2Yl+2y0KLYvz2i^uF?K*ZXh2lfgN*BozK4uB?CB+6j~7>!v7dz2HW z(OD|F`PK{T46ybtUQqa`xP`XQyb`}#iC9W1ttRFYiV+0UqTh=2)@=c#cXpSe4e%~9 ztwYqO`r-i2z&9kXID>CEpMk13v9Nx1e5Ix9NGIZW&?rKCNC9sREhfxIYNuvh)#``x z`gB;)FigvhiQAwb0Ou|vzsTg&QVDTL-%fQ3(W2O-If)9hB*?9RX`*e%$sDiz^Ut71 zhTHaUQe)t%(s@JefdN3B^A6#HM%=ENP}}w&h$Vssea~;{vY610fjIkUA^ueRK~tFL zjG_E(lkpbhnr)giqHiJg z7Xs(=cj@xX7d;9znSfl?@Gf{FFgoFSIEfuJh-p2a0|Jd^V0aIw@@Pb;yTcd4lRmv> zE#ThZ>URaMNV%aYFb1ACBb#Pq2OKg9IiC|Vl|aB2%Z=v(U=JuU1FBys#$LnoP;*Ds z<~SPn+CL_{cPj^0246f9Fxt6{6e^?)+f|orSeQeC#HtmV;wB?X^KFjkR)^DXCGK)C ziU_e?5#HksJI2@7iD1BH&4Y<>oZV7+f2Lvv8w*>>!!a*8BqBoY>Gm6>AUdw15 zM)9=mOf~m@Jna4(K_d}^CbUyy(rO+w4H7jL8){+R_b{5Q2{a2~WBApWDrfz?Q~5b2 zqzI)FTtM7=V3+sWS(@zuv6+X*N5Df60j!AIb_pRBG&?-fs*UDJ#*qJ%cXL;6^6%k= z4n}fK=2RatH{{4$@UoR*1#dcY1ognm(Dw8g(L-SE*z>d1NcW~ z#dB}RzC%AzdSwm*jgN3qwym$4A=;uK^FLH(z=6n#uSXhH8F0uPq7sn!3*;Ss`cBk= z?f}UO6>u<6OGO}Hk0oW>onBj#!R!zj21H;RAkau2N}}p-J!&=Us$V6ek*V9TeK=SB zIHP{hm59ptbY$5iGgEI8U4!=*wb$&ML6;2&MfK%UVWejHQV-deB+oH&U?^N=v+#}9 zfa3(Z%FG=Jf#p~~r2RN0C{iMeIx$5o9+45a*ihcPhwV5-i$IrWIx0muwm^w%#`ifV zK*Px4!T{9br&usj{<*M>`{WC$A7BBDK2~9kU@HhtKd8!;YB2*FlslFsaTEi zc3_B=5Tl9Dlt$X6k7K1o#oAYsV3z|(=|vVNL4xiE12yAahuFzUPjSRwEE~rJ9h>Ut zt06Zu&Jj&xPGztRNt@ptctuf~rj`eJRnZ$Hn(4?g@~VC#DjEpkV1?uX&<}zgMq*)X zbuG|WV{(}S&E27lc@Waz>iNZw4^Z!;{d~)I(=*kbbtAV=StDzFNvK11UNR7pmQVz zPXC_x8rIcq=W>?cw@MOK(l8?}U{I!$eXS6k$8FpkVPRdQA0@wxtKUMsRy?kGavpK52O z(08!A+dyLw?@_;PASNz_zYoPVYXhY9$iwaMX2&mKa%D$?8o~sr}?n*PUB)&fAcB?WQ%RzKt1ndu_y`pD;ff20p}{?yD#6EO z2xA>bgY{me9w`(`;4_uwqi_u&5aBRXENnMIv>$maeyBdv71pIaw-;rb7X$B_u1^Sx zoo;GM>Nv`=3fn+=smp_Y|6}J>T#}*V6drjqnp{VM&9jtA%!g2q?w@`oh0&F;^O%Kd&9mm2w68v2r<6F$0M{*!IDx<*dUVTLq-m18hUoY!45a~GI z;__%o(f!E96~!Z1q>+cTL<$PI7mUZHh!G(wI+l!ag~37AuazCcH2%n%+l zhu#U)ejJhF&^<^0(3p2hyc`*xO+Ajj9ketv9fDD)bYOtSz~&oNy1~a1zXJ`RI<8RT z{vzmA+;3F8$X%Rx$iaj{fJd*~gKlL={q*4dK$)6= zxfu6zoMh4}0;cT_@-N~}<#W2JKQrLInmonacW4gpWslmSNp}Xvt(W4(5gx(4375T` zY{|Mv51POr_e(5b+vGNOU!P;1(RmtnB9=%sG0XRBo9L?)eX%@{B=GYkYDrfjGlZec ziLWw>8O!!)+lmN**o6{8rP56i3osFV(=9e25p;TWtLO13b%E96;8vEvfC|Q#;A9ys zO(l#NC{N6|6!B94?i%uphR0p|F1{>kgNiPkQ)*_5;VJtVU=0WdzL$|e(f3o% z@iQ^5DIcw5!p3(={?oo#Mm-)$88*Z8@mUu#m0a;C$3R@Q`| z9+Vx@a`|ybFi z?)_7&VHydhQnt46zQ*cQonVC{y+kU5;<&23H3aTcr@Db`*RaW~+Nt-$ny$4y$(a}i z&~68-A~OcJ^e72)cm6GHK_}7Ec)2RVP@|oIh&IG>VckKJ4nWP&N~}D1N!1ALGvImE zkJ>0tVc3y01=1}Gs|o$9se@32IuLaK${rVLT>#ul$@rc-oO>qWC{;zj5QCDsr20|P zKf{6??tCd6b{B8XDtU~lDcH8E$D?qI^0yI&9FCWM7aGFebq7*3^+ME@^yb4feRW!OiauN zRsdU{cwW8G;?uw>QB}greb;V+v5}ji09G(NN1o9-D8bNuar^z@$=S5{-HHutUZTl% zN(7i8K5&ca5GhwibK$pbf5OA9M%h~r~g3z4JASw#b(FAhP z_I`c?VroiL-YQIWI4*DH7_?Q|lZ^DCzOW;1$j}aaXlWTj+g?C)<13wK2JfP`58btR z9K=TqA*12=)I=d-E|jH!9#0)V8&w#%$RoF!H=rOK)X%5XJW2TFzrvQOweP1`Jw)?1 zFU2sMJ)j{DYX>!L%b2tqM$P=SC|l+qEcfM$p?wN7>FjdB0;jjS zO^>;rjpv{FtvtEkT~$4{JGF#J@kO84#D_^S$P6Qm7{5`eKH)kap{9S3D;;a}JA#FvjiI1~lAL$IGQ_EA;l&xq(I(q~gfqBb zzn!=7y1nY}HMGO}sDsKBc~9B5yk^5$nJ_t;!(cv#NieM*LhZY&X_Aw{WHyOHR@|vE zP0LL*+{uXBMXr75+pYpKMS0{(i7V|80@(mlg5;La#w%#{2q129s%B8QRO%?V{A_s(IGEb@|QP* zJQfFg6c{MHhHMRffM6HS2Lu?lCIZGb1<*Fan4vKo?^3E>)N7oUk?58)zSVO7!36eb4o*t?cN}P;Sz4sOPBqdAGgE*H{o8M46XcM$f$r~HTW=8_N8s2%Xf)JBx)^>zIBLkl zbcDR4vy=7=TZY*(O4macLv$5Y_-=Co3H)P}4yWk1^XI-4GJtJ-s&fDV!yQ37ld-Dmt20EW0q1a?p%llkkT7b&3^r*IK&* zOaOJwpP_k3LkUO>JCAup<`0VW7xVVhKp=_3$a+;BeLM(akn=bVk4PsuJwb;B-fHuv z9brKE>hjOw)jFg7bsxn*RLg1*b}_1Ucx3Fs!+hLzbZ=6EXEX zYHU3pD)XmEDC{BQEIy0NC53^s$#AYqiP4Zp!BQe0DV72cN@xRiuugTb53wOwP;usD zf7l3MB3nO-yYhI^=^7h9vRkq6mJuY~2fGB0nFsROiVp(d+I{Iq3)O0bEO}w^e3f{$ zKf%lv9j|ZHL{N;&OFQ8z61nBhu2L#=-3sd9!BDS~8Ys7u=?ds@9R}_Y&@iKjayzm| z?YHkm2$ow#E7tqoq6-;`syQ{75>ZYDXi!b2bwuyQ%nqIs-~Gpd6R-%DypfXhg0X3z z9ZCqJG67;uMvb;d;^9{sX2vgWuX)HrFOB~?RVK%;I4hOb<6UKP=lf84vpw!bY=c4rh4QoQ z&^Io}gdAHV8L+Y)I7OTX$9-4Ez0`f7l=R&}PW?{F&Tml20U)tf*}R1I$dlaF12RUIanzRlIq4_E z`9bzwm*J*_TvbxqsSP-%x`&S<9@RMS%m#GS$s^SxX9^@OzMQsCEp_@Xh=_oDe~zgf zCX<_AYtvuxMeWxHMzWtsAfwr6^YSKY$VwNftqpvCvQ{-;r!&o?Qd0ErAeZc3Gd1kW zK2C0UO4yvh3t2!3tM+S?e*=*y9omKg0om7bc23{F>v|iUKZUy-({X!k=*EUL0u@Cp(*~z=&$=wq9@o`4F{@W?MtAi7w3hL??sbzE{f! z>VouM(tc1AVhFKS$A$;{b4^aSaRM?`QC0V?KM7%hl@tERojP_<6CqU#(_<^KT$ob_ z>v2*jffft(0Ubt4HyV1~b+kfte0kl#Yq_X~xqF!pTGIIz3gqtI`6qNG0nv#eVylSw$0B0cO z)>nu)(VZgQJMR6m!HfI|LQM_aMpmae5B5EU4SN;Ircb3$xCa_3r}`k*A%fk0iURM8 zPj+j2Xt+ZEZh)9IK@VxcuclURm9R$@b#L-odQ8eCeiT<)za?WnONbfMT1Q8nG4OXI z{8hm+*fx0`j>XRaeS&{LSt*iL%~(FF%T;+=CM6PNKu6M$1w-KaKoO=O0dwTMiR4lD7xp3ci0F6r^@hK)0xXXw-&Zfatr`97WoYA7=!v0fNqRiL-axNqyzZfU@{SrnrfD&`L|SW2|aQf%l7)D=(l#Qf2pb3c!}c z{LFdP9^YvCTb%7&2xzUS!aE%l{hJL8X4dgmlNn?!=Ipe;FxmGbMatAf?M5S@zyh^L zyE*h^uf)HlB1%=OsBT*}w7(c49WzsCPNUeN^JhvZQ!8;L+AZdLTJg4cLzn1brV6wG ze6ORxEY6}5*3`fvji+D;VxOyg*Ig}GG?R1Z*hPm(P(G5VfZ$2>B@R^Z0bt#e>TRKe z7X}r>!Bppk_`KC!Te}8)5H~hEv1r6m;^Bp^Lnd~Z2n0yx)GpXEow2so6PO8X&y;FI z!?9yf=RSINf4g3TcR=za#?hi$p!VxLS*GW;RucedhTOJh{vve<%FHNxX0_;}fep$B z^4;uKQ^yD-dSQtbadSQnZwhq_`AqaFFd+j9)A*#o&1#N*6p14;gZE5*{U{{;V|^Cf zPI^kA(XOHzz75wLX`lCY6`^iU`TQ%{KRUfB!f%r0UC`sOE{$o!(6KOP%)`e9X+5G7 z<0~0Jr?AQ3LA&|!WgUQEbw*y~&@g{=;@g>smILtt2-`2VDK)0!tCbJJ6K zmxZ8;O6v&oikf*j>%V*I?D=`46&QK^%Ir2#-WcF-)OtD}v0#dOjNjS{;lfCNdDbs08d z;k}(AtGou7OwUh^8f(C504RM0-n*9LGkvY5|5=?B@C<%TtPA)8YFVXDUZcIGyNYhE zuI{O|Qf5*hqQ;--m@0WE7d$m+4N?VVPv&DL*ts3VlxjaZ^UI{zU$j7E?6C3GfDsV~ z>HCZwIrI-=39}j|o-GNIu7TX0-`e%62NDAf|8|pVKuVHY28@-ZuKa=F*7+&}iCyZL z9C)tgPn2P%y2nouaS_K*XBFtGfaQf$ri#vVnpg43lny>sbvogLd3<8!QT$JrTCBF# zen+N!Doy)z0CXlF6x6G9H5agQKeSVrQK1;}Eey&reU+Z72CSd#eibelI!+mdK0fLM zmfx~}Owo{LYhn&ZUQK+Yc$(8=AD|}1pIMIRHZ#Y$Y|K^t&2t*cFcAPhj*3Qp)=2WG zz`ltcPtD}E(Uh5KKsd$lQ+gAe1ure$&6LC=m%ik35OfBSp_El}iXcfa?HdXHF4P~D zE4cjl%vwFr14ufm2CbO4)2Yk=2e*4@XtZGQU``(qOcj3t(s)fsB-o{eRPwa(k+Mh8 zuIG_1vmXP*tqwT@#`t^U7`O{;0HA!eh<^-Kn_^f+%x3y1pz|sj2`a7CX!J-FF_L-I zp(b{R5c*qr(V5^J+K&aPRgXhhi*W$dHvp!Rp+G3TO*ILU>itIV@*PS6DfwFco}&^J zmFv1{TW?L6pAwb=sNwqm`k*r^bS)n?bJ^EH$D0u4+2NHqWU+12&KX3po>s2$^;Qn3$xW zHS({-RS^K(a0_5Ha8jve11!+;0#ba8(ffZmC8IjVG7t$+B68Rvh0bX|G!2R7CX73u zR3oO13mDP4tNH2o#)>j+eRKK>}K7GvG%7RJJneYNU3 zrH(QjPeD}qS<7`^1%X~IzCC4Spm=|!cViqwrJV_%Oh;||M(*hT)$BOW@~dVRBW#wW zo0my9U0(ibHyB84h(;TZqXwVSqlqqwVU=`v&;Ur5#1)?bBuy7^06e$qj8HNLC#6-2 zR_eR>Qa40TLq4$bZ*V3j`U`*$v)cSXSYJ=zKw{tc7s8XZJ~(Za50Sw@!+BnBJRISr zd{aL)Q5U#W#xS#gv}iz-;O!2a+s(E_w6Y15oxJn?;IPq(yhQz&a2izX&R0!xO(77H z+bQ-Eh+A&bbd+CVz%Xzb`Rp(p25um)&on~;TgiAx2{e#;ET)?c$<4-M?y^xal|mZf zP^zU-2>nBNV1u9?C41}*w70Ecp@e*wUt_%_$qq8Hsz;*?Mhd5#eTk9_WfHw{C`GUd zx**;hdFZYTQEFx=Z7!WFQxy{(yY`g2@3P-W)*wAq#EpbE$k+*D+EU46-N%{dMp>Z1 z{8vyp5URWOl60jX5qnZ2t_Z(CE4LZ@u?Gm|I=ZP2NiI5Nx3)^T3uwtaLeEQF>J!rl zrBVfCUZ@U!hI$-KKSW+%pU=^5bo@B{0Nxk@rXz@^fg{af7&6&Rm*qKHu^cRGaRIRq zf2v)DivYo5N$Dj_Wx^l|%;n<=)fCYB?3U5I5S*ZUPEWIz;~`cBH_lFI$*8<5<=lzz zR52pSM&1t8c{#QLB$J}wL+>czn8j`8`8uraF5R7I4ghB!n3qV5L^X?}h=55mu7}+F z+{t<*^ON2Zgf1-`e2(5^p!AxBLIM{{nH@C4*hbID;qB~Bh=sPOPg#ZLl!!Gsyg zT+!2Qi+q=W0~doxI9k7=gbBRSe{`<|`V2#Q%THAc z3piof?fL(+b|*-ZBuR2bFO<||`*Qz_!QU5)nhO9!80xNx%CvA-Qxz3q{yZgUF?X_Y z4ZicS!fE9gaLitl9(RewCOcXF*mQeT99g8Olx+T2SM?NJOE6c{7C%Fq403D!v^|>B+uMy>LGYqy@4u!U7p7mCG4q49(RHCUTc|gh-o%{ zJ(ZW;>)U3Aw)B3}yQSts1sDPDnNNrm%?JN`)fo`=u~-2sz)JrtkYJmF{0Vb)h4Y?D zXKRoh7-oR}UgW0Ws0^d-N-eL;;F>}WPsbhh-i7+TyMlzA1|6Z0|DHYr>hCaFYyiLS zdmT8-n$Iu4(2?Ls&-+F+I>o)!_C$JDftYMf^gMmB&**nP_2uYh5XPtl2I7$8<02D;-_fCd5z=53BQc{H~EVQj=Ry?A>oeiSN<3QCsuM`HVGK>u6~Ip@&e-b|txT zb7F~c%k@$K4de-+BD9U1ITr1e>(Es-W7uyyJ0@dLPi3)jUfZNdb~U(**A5b};sS}# z^A|WQuY^Bz08%xnXTP_#8`|#_^$|X05uDS|JexhgQ6+}*DU5t_JGTU5zrB`SO92df zK6{`@J=@KsWhQqa{1LR)Ro)9j7{k;)(_=2!7py=>FsxfN4^))j2hECosP=GKhZ^#+ zmiBEzoSjz>0~OtEh8r^&gZ1-##^*`_brqa@EPs@%-@!y^z3nactDrluckb{&QHPO< zfyeR=j`GYMxTfsfYeSGsd%4cF&|3S<@4LKQ2ZVwy0|AHQYaCfP>OC3)Gm zG}LQb!8V8D7r|{RXk&s1%z-dm9dSRe zxL5iF5p0}=-~HqY9-BNnfnjV;|4hlWe$PIB8mR}aQNN?C^2lw|AW+Z#m`oZS3)1Dq zAgb=;@+R>zfNbP2a8ff)sZo65BN z9rim2n(+#(|64EJB)CwqE0}IVLa((fpM1>wXtAKG=<$H^drg<2`GiVrr!xApE15Oe z-L;bZnp6X3goEGLX;b~MhfWNwdcVzGqIiC+$hH+B2nnwjRo#zrq|vhem15h#Xba?h z>q3r(oYqHq{`*_DYnpgE6__}BtJ8f#0hvG?FSX772n0)9btThyg?LY% zk{pk@o}}QTO)$a8lg8z3dOBbAm#7IHfzF2V89nm%iWIvMLZC!VSsF&_E8|C*?1d7o zsFx*!JF-Mpn6>VI$5Nmr1AEifQg@G`bBR(){XfZvGPt4Iy;Gn27G*^H^mphQX{C51 zSHkxAk{=l78DYFPSCq=8e8%*S$O*S}VT;Zvt2D>z-n{LtM17zksQp#$<|NmDk6zdJ zO1qiVV)j1cYNooaasNaCshh6+^RPIzxabFXsbqooi8l2#8rpsOp;&b`p)Xv9Hzqui=;>#x35#A;@P$nwv_5jmYH#xud_we0) zlOO76!d{H$AU>U*TXboU#9&WNz(!RJTiczRyUBjRt=gpvB(W0iZTyzrDU#CE$S~uZl?I@Xtf( zET%HQGN}~PRicc&=J@;6=hjYJQK})fea-gO(BvKHdzaNTD=HKamN9m(kq3)*oY{+8 z{T`tQes7|lbwTlLxJWA0Y#&P^KND;}#L{M>M~&O#AKoK%x>T72;%izncGrCV-^1V2 znsHNH=m+lo>*<<*qadtH{Y%}=^)1Kt8L`r$sWkH|v*A>V1pGeEb^=z;E!nfA!E z{fd_m)dOf$Ni2L782`xV_C0$y_eDczO`9w-slhq;C_0_n#XbFIFNInwH~AAH?zc2S ztIV3ry=XH5mrOSMcHE-!VrU4}bX_Wk^&EW~g_{FA8`)zfzeywAH5rE7U?FH+_+&(- zg2e-xr$3fy6J$HF@r*x6B3yz2&ff_p*1334j;SA~wS)xrNM(4wAU>KAqT2aavvT;q zllI6GPufu`i?$2OP3wlWE1Yga%_3Ip^|l!RvS)Oi8ly5~=qx^v`hJYhMt8|HmT+=B zs;5hJH1Bo9pUJ;?I7-v>Gf#s0du~q&^g_1q5AdFNt|O>XVA4r)I%k-U;LTtEx7$dCof88z}>nWY<>sr+3VqO1*f>o}V5t%bMhjWDfyqiKqCX)(M+1 z)AcggM4n`ySa(MO&8IB-3byHH^D^NML0l$I)YRg4mo{Ox6zT8x5&tchTDO#$o7NK9 z!!;wjLeaKo^bC*Y77L{bd^foiKR7Q+m99fNm!K7mEpo7!C7kaRxHW9;oROi##XKn@ zU57TD^fR?v{_JxI^?)d)fv(uOzYl|;`ucM{?@b%^`3PQqPdb$z1IC4T)L-M*dw z*#G03x}J|BX`1n-e~$5V2E5Hb`%du9Bcg?Zd4I-Y>E1&EPr&<3Z%UvS)fU~!HJUbv zG|8Nvu-lnBaM7HxaH?mNBgMRs!HzaePDPU4_rHVbn7TZ_@s-C{x|q>`6<^e@7Yi`?&qwIp6G=>b@7K!J$V(x3Yt&@Xf5CsRn{kOY^ zTY8yF2?>rrF8W?3BMBA@k)~tzR5{uW@;E+_3!1~+6~rwK-jO0O%TnJk$6ds8o3xczWQ%it)WZ=iZY72F zaSx68O$20Gm3cYJlNVd)^IaGdqC3TBj;M126s6K^=TKqa$?tVx4_*hDlE^sJvKE%i zsKok4pwaVR1-*txBOsihb@@}E?n7Kc`pBMl5VJKDmROtK{aD9vmVR;RU-{w%g~$B? zy+SY^I$4|<5d1Z!ljxrXtZe%@lXoxbw&mIzkTfW-6d5}sd&+r^K4dM8Z0@TCvs%Mo zN#BhxIEh}Tu2PKr<^j^6QoyfqE8Dw~67Wlxb0{GynYeLbTZ73Mfw^G1P_L6o65qC$ zX(_LMi}S*Lxe811M!W-?AvC_CxDd1n{sULDH4;LsNtjhGzt`uhBG0*~O`9&O{yFIj ziyr#ius)ahh`i2(J5|8ORL&f665$?7?d$A$n-IN?leZ#S-gngE4~?tNk~!m20pjH# zM<{FMJhMay?V@==Y&ecu1>FdgrHmlo)3;Rjv{Hzp_cLo%Lzr}Gk7yoHlqcdTyK)`3 z@J95Y14Mi}=}g#6_I+ZZ$>ud^L~O^uKT0NUq=t~mDWV# zike=f5>j1p(fOH1dCq+ZqbfCc4B=zprnukery#7hVE?=Ee*qO5Z?UUD>u3jzL zi84O_<=mA0ms)uG{_bwju4^%D^@bdCrT`e|={cWkvSzR&kI$!VDhkZ@7}WV0wbK#L zCbd9;2htfu4JL9^8l?Qvj($HL$LVILpmYJa{vN(_UsvqbMyaH%tf8biYokrF#Y$2= z+$iJvjwtd~GneZ#kY9n#wjw6?FB8wO$ryW|btA*bs;V3E#X!l@z7in)jvsraIVcfh zrk3o_dJwKe^!M-I#A(1wiZ8r7eSoiPhyOsz>*(!5?h**kOl$g#2zV{SeXnq8AB@|| z3taH(J1J5m<$DadeuflJwNc7LKg5YbEMUk~)H&@|SqC4ep$Wo`@b5jpKWEK;jU^au z$hoj3L3(q#huTgeu}$UUB;X8nOii0(Yug8a04qS$zv;VS3?SDtm0u=|RfF0}B0YTu z+HsNRYNvb+rXBxy?-Unvpcbx(Wx7q*7xNVjNfr&X)!zX-&?T9jw%sL0Up+@6{Fy$y zBk3d;cL5x?uoYjKfJr8y?3u=7C9W3a{-Ft&=D8ak8I38uRGDEQ`G-u9Zi^p507|*f zt7@6%<@gGd5cek(gpU`UG^Xs~o-Sy!>94p{sH9h);&L3fanas-1K#eQ}C zFqh=lS#W4w*YKiPquCXs@F%5BV)t7!bF%--=O#6Gn=3`bK1I*bO$9WruAS|vyMtl9 zpV5Mpz2cVLwl={#iwr}fVV}gO zOU{JFoaueXu6>POgC~0vS@AvkGZp2gRLhYyZ{mt0jpt3s)ewO~Nq-x#;v*XY98POe z?sCNcNCLLdDXI{)?fgPh_yHBi&YW zI^yIxDCo@5E^Qx)RJpRh({%!CN!f8Lk-8d*^udc3FjLK%)%QGKx(VadMX#wef>O%egvqJEX z4Y7|noV||#Hpysjq7wC>b-KzFXo#R|?`)9-RWW~*w?|4j;Rk0}`zs*DzRmbr#4!Y| zT;w6$3nY@dKn6OvkvrIaUq`4bF|f!z{)hHngU>LBz?2U6LllVIFpAWy^R;j-4)!0V z#LbW_ee!t5tnZhZTZZxJnH5HjUtfXWtn{;^90G4eBGzixi73`_--?hhKx?}tmv{Bw zG%XlCR?}c}7GkPFW7Cw1EdIqYv!^k!rwA50=FM!@v8wyGZ5 zkaa|3ka~9t#jRKs1N;Os6c8U9dw)_pnf^}H)gJhD3snn?IO6d)9N-<=@c5$PMktHc z^G+tg_14gPa+

o@<}7wVX@U7Cp30e0AYmImW)bKW+NM3sfoOGWceHZFbYuMXPTg zOMhh6^Ujl@q|hvU;E#NzG)10Eg{5T_yT6un_0+N^8bep^Clek)cS`oUr!ShX} z9X!%N6uny`)QUtK1omHYFYQ~Mwx1!z<-zb2V;xxWIdNFC`z)y{n)M;tiheW*UV zH-XDGXpQ{|y=X;Gz{HV|jy&`_=Or=Hm14alV%WM{JV;p%Bi7wt1#^c0=(zfAjNhYo zkUk~O9EGWmg_Ats@(b+wXPhAfg8Fzz$W{v2r<_E}0W!M?7Cedcl(bT9cTbT?d~El? z{}Njj$UKCO%yA+&?2vI6*RbAYg^N0?>NQ$dyxzj&|CUTg17xv*FCAoExNu}_Ce)b_O<%W4&5&cZl znxjU;(-e8Cj^N&B0ex@qilaI2TjEm(5%Z3VKZ^C*dRNlt7?5yM!G4%sfC^sSK^uAK z94Ry_MATlqPIZxbUy{(6KK{Def{?r zt4#)Rmkp{dtzcqjNHF@Z^j$E*`Aqhi85CiI<5P`T2(f2++rYBDvoXHXEG~cElWR}y z0gz$AeT{*+@RmhPkKn@}-WG$R@cQTL)14zF-qM}U#9NGy|6hB^s|pRfuSI-56Kbc{ zclo+Yhg}mN;yvlQ3A*zmTx*1uqO@YYOS~8K9EtFKN@C{`ix)@=_dR;KI1At;(YtX% zy`;4whueM-qs_k?YlM*RXdnBo(rmoJ+#qa_{9|s2UP2qY^9R$hnx9YpT@p+@GZs<_ z9i6aJFLN3!4R22Of;;v@M}q$P6VUy8_IBJI_35Y;b?o0^u}vtXOpG>VNOu=Y5YZ7Y zd*v5X`DNH_=NnKp|L->Mvf_@n!IU( zCid9+r|eb}1~v3Yy@h9f(WaH%8vEr*wcyi}UFomal1$%jVwNb$&EKjSM2Y?{(XXi3 z^o;i7>tir31HNK=SJ|=h)v(YKbgEE{_2(GCDPQ;W)yoW;+?$9{wX2$XQ!)N-`Pdxq z^*7;j9eZQY*So%mv>kGV^lro*GaNr{8|@J^V~oDHkSa;`pL*G#1GMZ8nqD&KUDJ zHrI8ea2Ov7B$K}p{=cKYJ&72XT28-X0vJ3xh#61Jm`K^k zT7S=?pA%KO(q+atPyfM{cpI7!!t2hV`ObL&V?2f{XJpt)d=ozdYGBxSBkj$bamMmI zmH>JGvJx!XVaS1ZLO%WN$;5j1$r&<)F>o^IyAwjlgT5M=+Sj%WWkr;2Iro3R{P*zP zZ|VUkUyW@>NM*=EykeS^9@7MSX8iZSiJ}{Cv2X7`2}*)O)iPRmtzC$i6!P2ACl6yz zJxO6XBPskZeC}Lwy`cTg-BYP7!UA{65lN&p{~mo(n`sJY7<)i4zs|^I$>|4 z3+VXc{$eJ$9&S1?c=yINul6@`y(vMm$BklY`xKyQBky{d!%%$h(MD>ZO%R?cKs-r~ z3f-f-bM3SXkG1Vc$^SilCo468ddL-Y^5N+)%y%5PUzElbzRZqr4{rWRZM9U@N8a#x z`S0iUh22_*<*Z|x)ZW%Op#*W%&0H9&pcxWq_;W`MS+7RD`W;&eJ^WiVH-m@YO8B1r z_%JLN=Nt6v;C|^p`C{)b54D`^Tl;9W2chTgq!2ju^IH)9Kp2dVyi+EIm*ArJO8N{ z;2styX|ubh=JxZEl>kl@V46Xb-F(#0#ALkMDXH$F^w1AUc}!W>!gzY=GekUQAg za>?Rrf6rb6an=aj#&7N1(@m*7@KH{zr%S4C8IT2L6875_GcdxkMf;tS}S&$7OU z3B1k$esv>Yhbv>XkR!FQddp;IBiM%|AZ%v2G2`A=5*k~<*&%Y-uD@sR{J||glZG5$ zcCk*>7Ps!29d56qY@iB_IWr;@7k%~pGjzA&=?sG!CfgJjzvo#yXUX5^=OLM{N?G%a zI%-;#cGWQ9HBEqS+U;{Uys_bx9{qRs8}XyWGFPTf!TP^wrnWwH`Gh(aiDt36rq$b} zH`x*ha9FgznxKAo5lA*>FJ8fD|E4xK2wpUI#JN9W{2@P z9uWl0cSQZ(=bWi)VT5(kM^r!@Xn~)t-_bV-I|zd`wqYmhdPl2vaCsMjJIx1w|9cJt zw|7kwJ%IZhjgU0Br2}A66V&Y4&V`f21qMdBKJ3(<-m}JL$*J^q+xP5)TakORxoG%p zM*%4Z(}sYxGy?uO!D{JsI=AbTogPEWrU!GLQX6k`yh4fauCg)iBhDhyIPOsP^XMQ- zH;=>=S}Qyl3C**`kY9(oPiW+#WE}eUx!3r+ycBCI!i zTy;0Z9njH?EAHV=*O&5ttWo1oxsIrdEgARvPUP*JM7ijeDLKxznMIF~px#(ofI(O* zyRVSa*ec(XmuN1VhI-W8@}78w(|NFyaNYM%ZnD^$*4?Rp7)c&ViJJ%bU#Zc10XV;> zct=0pzP;DZ&r?u|aV%d7=*LHNd#F{B&+WOot)YzmKt33}%3|mb=TIr~o0;b5|)Z^767S!~B#TOy2Xvdl;JZTab+J zep+#*BT$ts;Vg2I>6qrGi22I0WO(HI`uo$qXK%}$k%$0BV)cJBjz`0?TWvMS%6DJR zdGRKWUHTJm>v|9%?bPhV7gZm=oHqbJxXPh z(XfQiqH%sdX5r@_$Bahdz-Aa~&0jgIv@yPAEhP12K32L?TgE}H5*1HQAF^lfw? z6ifa)LBh2pHVzJ9QCw@}#ku@8fm593p%edcn1+*kR8n{M7F| zp4%^I2ZunOz;q@-shZ#KvBXeFLPjuKzkeG{Cw=vxdR7(tF?H#c6I4$nvt_n37l&u5 z5HsOVX}*1lD_Q_fa-!~a8%HhW8!)Jlkglx1uZRSi-g#oI-QZ_?q*NML2bc2M4ZL(b zQf9+od=d((zqQ_xDjfPL`3N>}KnG?V02A9sqvEL815@a{wwAq-zinp~MpvI+d2-1x zgVUMI^qZN6^Lm+LP38>CArEem6nhSv29X4vP)9vsLOJG0> zpxR9Tio-!%M||o3a(>U=lzOLJL!-JsaWS9Z&?d%&!!^AohL}P}_cgVetKvAatQC%m z$D5dm*?Q&y%4+{aD-5clu4UY|*1O4_!cdRm)JXESZq#c0uz5_YTF%Qb_P*yHS|Lz# z&MkhlDuLhY?Q(tNaL$KCX(>@r|DL_m)Zg$ZK3uq(iLVjy^pMq*em@dv(#+qkb2p>cA9D<{-xmlV zZ-$o7-SsNErovyTy%+BFFVmJVL~iJH`Lb(Xf|1DHN(-O6e%zhe4_h?pIJtg<;H*D=aMTS2Q6m<-{cSS-&7N%sXEW(`pl(!xrUdj!+PLQ109=L9w-W;0lKK z+dk_?LQs!2ihmme?#ycaJ^K^pCQK;y>U#{vhu`&$!ShVIjq{DnLGDa|x)|rk=X(T- zuFn%pDU{EK)|)6e`uDGB#*D`Nj#X-?M$1qZn0~9dACqdW)U$v-{aIG2Ot)nFeyS_> zbNp?QEs1ou`veU<(u`}E1%oph7)kwUj>rRwi-K-)KZ7t_| z```2(!%sd#6I^QH*9!kV(D&Tyhb6ec>d7}pzrwt59#Ll2xeMt)5|lt6kE>OQ>IkC2 zdOwk{Aqa=}{Ta79q=v3|jLVd8!og5macnyB>&{|^1{%HT;}xfa)!N*npg^6D72fE( zU=Lym%N8}9qnH^!6BGB)L95ZtFv{$$!>J)YF@5sf#6G#CavJPZRd-eAOMZ-%3*o)T; zA^HvZjxR#}?z_KHJX3S=g&e`~j16o3RtGpjuP>@e2Gd?X$~k%^Q))medFj_A#+8Qa zf=Vs)FGTP$sx9V^X$5Ky5l!f(GL|n{FER9e@kMxCdnyghfL&tW~(Q9^^fY@7llSL z6C@@0fM%lX4$kihowglhL^7rib%pLu0Fh$e?^Zuzuep~B(&lF}0iRMIQ*dMDY z2a$jAMwumc1sy;UK2e6%lK|sT+R#!^^d2K5P)gTE3;_JhI(QYNl9Z`Dyn=25hbys3WFBGYBXM&_@RT;UhJm z+3qT+{HmFb_~LFLm0n%i9{s zL))+!?PD#J$>X=nLgTAZlJ4=>o|Ya-HOiGOY-Cg`*qul*u=3H?o}p&AIhXGUfkeA8 zgY-z(vWAO;rbCbLb5`oFMIU}d+GC%A>LTlTS6t8fb7i1ruv>pwF8Ch})FJx2SZiH4 z@Is$Y1>^eHr5Nh9`imQkw2hNE5x?>As12wr{*_N$BgPu~ojE89N<5fET>iINHhydY zYoJS?)cPo;MJ$7T*SGyfJFrVo=-oKgj z{@!13V2{$on0@0tXM#JHC_Skd$NH1NN`mqfw#Ho*zT}!Zeq4@!M%!b;ZyfvvEBb(Q zB1yK#xgY?V6KO&d(Rc6JLRt6GI?7+XKXbt|G-V~7>l|=A_pE)CbuaB+d_c<}te5-w6vt{%&ug#43CA6(gLjU@6UpaneOrLNc z0-=jEZ~bmAxiU@%hlmIYjlCMpl)}^k)Oama%khjBwU3APLCgs%uDEG; zzdArMP&`^!S>`cJVKT;=18xy}TgAD4jGs4GSD7DGz3!P?aMZ=x`5yh8i_jqn$>43s z&`ZEL`4F82bq7|8y{#_iRP49F^Q%zCyXnQdhesr9dWXGnm6?ruxWD%V$U@fCeqU#> zbW#p!+ls^MIpK=lt^%pVe;=G2UsHPXyY=tMuUW|s9WadN(=MaRu(<(nKI4Ek6@HWO zoipeTUvw6lJ!+VPRcS(y*JUiHGp|F)RsB}ajV zUGvUCRi6`ivkslHqBpe#sc=hfB9X() zL4q^P*{>tky2>(UnD7yBb^*OOnRGHU=JD$jjsO1IKW?6@@A8b0@cxF)PY5XhJs(-@~{2A=$)^+E_;Vwr@7cE9}^PPu$<#$#}WRJ(F`%W^KN- z32?U?NH*CMeWx3Ul&;($|B86E4{bO^>6j4^{N3aj?QC*X51ae+hB9;_eI5~N)ySvD z#^3OL3iIC!a+{+<93K;BuP-T(r*Mk;l)$8Ba?5AOs{y$*e93_5XD$!`W%jRJS!RBA zp@@iZ+9u?L7!1bdj)u9-1x7`W;S8YUhHZaLPobso+znH~zLV4Wj=rZKBq92XoZh3} zLh%-$z)wtrYeMH%Y3dGwb8c4U0;oy!X(y$b=(1>*<(cls5!pyjnjCr3o+}RF3TE(d zbnHTeQsHIta5o8y4x_{EZNA@#%h#`=QVoo@A){k!h2kBQLQ6*ZBgGm%$D7WBGz0Fb%cMO^&8C zs&NOsWx0NP{9r+TZ(s;#`j5}9q$*)$S(ZBe-F)s|fCgGiV&9aEzmGT}eynwxrF|I) zn&nCknZUp4(jiY|n{1sy*nz(H(b%fEf@E3xMb1j1GO-fAZb^SFqj_K)gyI=9uCjZW`%0yl0ZqTnskoE{YgO zqw-$trM#;(uEfUW=$@(R#t$%5G;%<^X&sU8**jJ%!Cbd~>n65-_|whcNE3R#7D0Hu z%lFI*YTk#9qbKPzAmIghr_}zlo$d(WVmK+VFa%OzBurQm2a;23_G+sZ-tt_9} zlHtSIEklsS<@nr8M_i>xG4P&wbk>oPs0+8UMb4bVLdf}#e9!wfq;V!*9bUk~!5A!& zOG%TuyO;{P5IByPd0p1p=ElBgFlBuFE*GwwERW?J?oR}Ko+t57JOA1}yOMtM*ONJ; zA}F?xL1rSCc|(<9Ivqazambz1GA$Xi(ta(%6}XDJyaV%usVU=X^o=@W`EuQ7_cAYhx2@G z?WN$_r99SsNa}qn4YAf7L4?JR_^`K`oO3B9w}%A<8a=o;F+H1=Cy;{Fd}ipdQRUp( zLI_5-{^ae-US)_|*Fb~J@gZpGcdC3(AG0vXiv}l*?~L{|3mfvRgF3d0u7wR-1szUi zI!`HS;Es(Pxm8p+dpWs`LC?q=o43?nxYMY>-O=s$Z>{x4VXG&mhoXT9xX|w#!I*_co&4r@0NV_Nil=arXYKUo{G& z<$!Lk8NGnUDGhXSM)-4Omn9_m>xrEO(jbBqK_hlEQsksiaB5l>eKmp!H0FKbAmnYzc^wcZu;M-vDj*45(-rk>lSZDf@ z7jn(^H^{!`WHpJ-nPCt<(pFOgHTqfFs}H_$ zyYx;5o(?ez;1h~h3t{+Ej^9V=ckmpk#^BrBX-O*ezJBExMWlofLy5ly|3Q_+1JWAS zDCra1(y?(`J~PLDivD}~+Q$dHbQ6ORdCq$@*>E&I26+I(4>Uk_+r6R^`k@<$;7u)m zPjC+nbj=WdRO#eGP-B@I$R!uMBE?I5FNNuQ_`Yt#CqCGSwVx}Tp-QgqG1<(zoi&dj zr_NccQBVCv?=@=pty>}d*fY7{K{NDob;2i5e$6jYfiXfQr3M(-RM0#I5z|3My`5}^x zr=IbGs}N1?1kSxHuKmv zY#HRGwQjnAXy>hQ{zUxwnYR?-Gfn6>O?%doReU`8@KL#VpQ(FC^NKs{-fQ9rEUTLY zhoUv-(vkjGJ^BcCxXWF%jo0{!)d)G@pqOAD0)=Of_j87R9%h+M9)lxd@LXbDd$^6;Z+F5*8}KuK z7r9nZ!gOitAo2HvS!vy+C!^YxoX6q8IqDXo7`%tx`**aq9=4CZaOob-I0kNd*J`%S z^;I*b#=23(R7a`p1d~ABOyklAFPV7_t~D=(#qW|VUT_2+Tsm%Rk-Sw;XP3^r!6B6t zqkGv!!KQV#Q*Mh^BGUUF{Ua)^>4I0q_v|2OpJGOx7=w$*VShB+n^)o)GCFVBn-*(g zXAi>jWArP|e8aT%-j3+H35E?z@)|#*Z4w|jFkE%8#@v>H-!N#02e=%4D*(YC^6!YK zq*J>mju$~N0yk%a8W>}^Es_A4w;47!!bnOc_XGy@&H%mSz1a4q__n{*yX}Blh5=_@ zNj8URSkO9=zH6=mcXw&k9snvr2IhINVD1*Dv_dD>_ofb|SgB&8bX}}WRB!-clj+h= z7q#vlEz?)lPW9MRI}0J|E%M2Z*#uwmS|v@O9A&TZ{!NYX!W@uuE*Y82vJoz=R@f(z zmsz7EwhMR4QA-3?mJi6(`hlz_ZTB71KRv6{r!(_RUlWRKx_)JpfEWBddc(+dk5HK} zXG_PhrSE-{`04R-XZ+sG`)#0@HpC?&ZDBuC5>Bb$O8^egc@akK*=wh2Ix0qDqUSF& z^Qd)%$wd>(LODb23cKI=j(~lR>RMy7o{e#BrAkgPK@Z2UZ378290i^(S^c@EQD0h} z)oz66RWMOn>K$%TdXD77%QM{WHe&n;u_O2QrnZ+*aV&pJrk#yZWa%ccY?E2>Bm`AK zmj#_sOGX(qMDWGAT%Bvo8{Notcsydjac~g~>&m;^wP)Di5(cD*-J*^RWcR4M5FafG zt40`Re#5$B!22g|{!Nbs!}qsn1}c}YO<`N&PcQ3bquZ_mzy77F^FF_8$&VwjIp>s9 zI`y*Nz!~mR(&I|=)cr*Qy6wWH{N8KqUNaMBXBmmFJ%)Ep!%CQNcZ~~yVtYz&*mYy} z*62(#zio2jiJ96bQO`i}yK)op-T94swBOli8}j}y!`7p(lLFQ*vCYa+e8&%1zPk)S z0REFUdE>cQ+ir9k;0a#NxD{N1tgdEe5+v%lvd9>|A7jXcEnOY#e=BErh39yg3}~*8 z_#Wuq&Og*(jnD&$Um<^gNhI{ia1ZNTAv4vcAbo4IdK*JJG~?g9L^{so?=FkRNDsfH zO}?pVbxqd)6^P|ZSxkA{JPwb&_-EX-KN-!)d<903LE<>h4T_bI@(S8=4&nvml zfL)t(6D_c)`RI=Zdz16@lm{=`@GXvce+H#?w2`Qt7w7>i2Q9e!f?{<4t(+{%r3Iyl z-(z~8-IBNZ)_<3f@8qC32>Z{OKYEU%_@0A0s55v_)%tsL2K)Y{dI8{!^x$Iuy2@L- z-%K2PjG(^mUKTPMEpHwrDAhXgMn^}QNtNC23Q|O+$PcbW`@DSHEqbzI=DWE*b90^0 zCpwhT>Mcb!DAZ%Kf34Ny%uWHQKT950+v%dNZ~|NX+UL!f=BU$f664GH)|-E^+itz2 z&onuodU+Amcx@N5f+*tw<<~XXtN%>YRFLDBrE)xELSC#Axz?@pqTrbP(Tg&6cmRP=WlmEsg+51wZP(6=^rFD z1HKvZ=`KpTT$CbMi~dj$g%}vw2b*_BjK#t41|?qwR@~nWI-%oHhiUDlI{!+_VJ*E8 zvX+8GhfBb4rI_aO=-ETS??nNh*=*K3(&3(Hem;VoUbg7R0bv!$rzh3Fzmg4IKW0 zp63+mG756}UB(Og1V8Dy zP{x#G*OSF4mflA{)z*M~$ju0-TtkkUNX~ZCgg@XSIG*+(-6vi1_qf-HBi)tx+CSyv zc1(y6ZreQ`;vGUBEHPSKP0Op61Yq)?R*bgp;db~P8^Q4mvvTnp8q@`bLeOhtHr_^A z&by=WN9~Z1!}(+wd+E9qyTjqQMZlo5_odB&O~xu7h5H6}`8h~s)byLV zUvk@UTz8`7gO>a}hZ*8nIp5kBBYO8tnolKG-JM5wh-C60ll3BnT@Tm1fWO7?J^aug z>ZER}n{}JY+=0{?#01)Bs%3WM>mDhoyPhc@N92%LkQ%HpK;J9#id1xTFn8=@lA4r7 zN@?F#z3}}lUC(YH38@G&NKh;`jjn5-ZseEe>*V9zX(Irv+{&@)E%o-BWn$j7K3r_BQxm{0O|MkxC89C2FKcy`5gQij$@+HYOm zWBR=6oJszu`t|Sh?4%`IshRi0f>JKrz2sL(&0greUJ1FLhJ+c%p(F9zQDM#d*m>p+ zzsS$8zr$(Nk6^p{miFB_r^*IiO~yQF>HrTHjynFvLnN-IQYLlX@d_A*Frq_)Y3 zRqV6JOq(Fqf`XI}l0>zET3$mbc;04Rf<@Zp?^Z8!Y?%n~h<981j^%G;GYv1w>V|#R z-E_Q;;{$~7#!hp(<*vSe=cZaR${W6F0Pk%2yR02*;K^FB`BFc1TFiJ=85vCE9JP+K z8pwNsE|e^}hs0+87MtVPSf0h1Sy0R6Z_UhlP~)R;Hgz5ovyaBi_zDMZYHf6SapU+7 z{sm3qw^n`eY4B*RZ^w=Z+ zj{b|F%%(J#B~7niA+U#rXyVQv0#>zLG<$d%% zA=IgoVJ*~S*kEc0eu;0|bS`tQN{n(e5KLFJ<8j+3U0Nd0(Sg03X7}<8dY|t_YkCWb zlY$0tZ$!!W_;CZPo++Q?g8CUk?7f%{S6Dxzljf^5Q3#<+rESp}!EG(~HxpG6<-g+Z z?_=La$6^1QhV0Y1s?V_$Q?%Wqi1U}(8%l-jzJU)w-u(6#cxmiQ%cN6|?v^=8ee00= zJ_6&|BY*8yYl8~`bwhJiSSeAeg{p7-mQ>l{beCEmH}Uy<_NlZ^xQvamcReSadEO@kAYq^yNPMy`_n%#f*F&c_*udf zW47iCTb{Ytu+K=dI?-;^iv5|E?4~m7OLo1hR=zd;mOT8=MV(hz=~29`cA-xmV%K0v zF5;ga;#{T=ymqc!?YrNs3{F;m8Z78vPQ6!KOYyl6es?xVl*w!2x-pRULNfi@mCAMt z?Mp-0N;Z+Fr_7!wCDBY!PIcy<5IXK3(;(awKF;sw-9W^L0vg=pqSyEE6W7d;Ho~Lx zlQCEU-tO>>GoYfnW z;MP8vB>Q{z=Mx%kC4Z8|#){mhU_n6{AoCIn+&gWoHuLrzwF;eWK2sB-_^5A}#Oa5j z+s^pl8s)Gq_c5Wf;pZ|u(+(A)?rnYIPcapS`91!Zi%$c=d$Rtm_aQ+TKSs~$H`kSI zc6ua1e5VS20@mvWjy8)q<|@VG{*(g0|3txNPIb>22fIF%%B*(Yfn@-&_W-=jXBt@- zu3A8^+rmjn?0e2LY=p!Yq|dEiGtVAgNpOxC(UlCmRSN#g`1hDlv`W>G8chZ4{wpo5 z${RrSOAaM99mz3^t1IbfKygN!(NLHj%DbzX z^I@&dbq-d_z%Vw?B#VTpUmJA1Q*{0k;Yi>5PTNNSVm26j4FWlq)R0Igh+v7ax}`K)=U`gDQ7L3(Ez zpkZuisGpUxsWej&w$xz9;KcZaqNZ;j`xP7|2#3B-+dJin8;hIS922sexG9WApA{0&`wG&ahkt&qgEKgXbZ7+?5yjDr#fIHAQomt3sH!sY@NWvo ze^D3UKGrV&Bh5z`GDj1&(q*(XEuG11Gyh_=zr$t@JuqDy{28qVT`tR=cv5HZ+V}wO zHg~z*=_Lq>zGu(8y3{&YtF=B`9AfCMcTCmQoC&qx2xT&K02e}NKKQxGGpJiG?MXaE zKa)`*%E>;d&zq=Q89|Lw>w#z->UtHYK*F&=_Gl4{pK@v1it>WC`yKs%FKN*=C!yns zzCiGpdf}cSjuuVb$VLL!BrI+F$ou*2@otv;o(T+~W--$AGZhuaH0*!QoVpPNR1RTt zwQgsG>sNzzF(+C3!+=raF*+7FiJdY!JGIC6=;?XL;h|=AZj&n&D0K#-W6QLEnltPZ zym|~IhmS`8TH`E2Bo&r{k|}f+6acRn!sR?oQzqVqItF44!B+w{C#Mhh2`fLw- zr~Xyy+~uZC;)jVFkOzV7o*df5%L3b2-V<>$IBGMTB>Be{idM_->7d~@{+@p9t1>dk z;wEHZ2#=)V8K@@L5!r7ondiKw@=K8Jv}S}>+UgeN-ZX6;W)(WIoBd8N5!O&h0UEAK zeY*JlonQ0pqCx-iQ=*%On&$<-z{#F(XF;lN3)-6Tb$>xl05!{vB(N)rMyw zgmM7QusDu!;*&Y($y!9r!HFH6mpIl&TEy#nxpx%^aMIO%rQTIckRak= z<5Xd%d&q5l8~@M4Z%LMp81=tP2{O<#7d}t@q{*F?t#7Imaz0`YXp2 zC`#Lm2%HDrg__HO>X}hR{P7X@HMkU6xC*$9-_utb8qH`h|IVGeTekZ7E%)@u9WmU6 zbtx-Y&%ahAubc?ThEHWvyq)ZK=gL(7Sx*-RiB{zfAoaWY0Q%NRJo6*08E>m_uJxc0A#DYNUAQ!^Vkbvuo)q~l}yNoY;Q zn|;F?P8JT>g(1tQ{;W+K4P2>Kvj>^9=(6e7-+#B%ZDP?amJ89<-^j}ozU=p1hq85o98v&%cOchdNo|^~Lcv;L%Od=I+Q@BhdfOeb9|mNvV*R zfK{mJb{JG-XLAwgS5iZcET@;-M5e5jjBzdF^G?1=r>_Tb#g0xSCYxTXQa0%Rhl4n` z>FqMh)CgSWI&l==;rNgF(4k7@pzcaYb56_r5$)m7oCF`bKE&ry;7$I^zNb4vkGrO8 zrs9lmL3(h^MfLZt5oMSnzqJRns_fC+I?q_0jp??~Chy;>c?Nbsy7$}kG!4H(1e&l2 ziV34NaUhttA$&2QgsA{p;W1O2t@_kUiFGp>Hi{KZ8Hc6$Ia=*Cfc@GG&;0Gt(mX*~ z7^K;T$@FaT#I!Dx9seHed+@n|x|j#9*8}ShV;|MqyUQ<8@VK4o<_pCd1nKEE%5pj) zCG$gZBr&pE58fCOv!Y6?GSgrajlW-Iiy!CSyZuhUfEX2~b;@|TrdhUQ%O`NTml;ja z*>wp&2gs$5kL4Hey8V&aWb=j+Bp(4pN^W&^k>L5KYa=~1m&_z7)eT~)Zy5(GPkpM2XbBoP;cfW~(&q?&%0-KMQaC)rcY-HYE`WXg{iU~XEg_hG1 zW5ys8kPP}LS5_-0XwIjUIft;Mk*?`+AhRdwwBJywRr8<{H-!p?|-~nv7?iEF?`~E6+>% zPLLSw2I9gS$aMCqH#cm~{@rDdB$e2>xb6FshCTUKQ6Ki}@9As);yn1Q{VBDum{EyI zWzb$NXb|FVwdUKC`r_C@b06bolMJYwU-8S4k>!@A)(S6x%m*Kp$4vb9I@dyR#?uU7 zHB8t#y1XeK+le;8q<4JjywY~Q+IOG8nu~nRmh|kCJIPCV7CjD<*-OE0OdhyK*sS`+ z%w}(}-=F^y{NI~q*_74mtT zEHpBJ@8+`-&9r~|<-rn~|LMA5dLQeDRcL>ey0o>CU=je<5m#=*z9tfKyP?S5#wKsK zP);yLw4B$H#1&ER(!NG4^<*?ZC!Ou+_y3)lk7+_;tJ+)6Yr98%Yo9Ga_x-TCj~hdw`f#u@y(%PnF0u2^Ri6co3| zMZUqyXkXQhPcx8rt{xQDS04oON{s|j2ncmnYTvO#@It^O44Omyw^^Xh)WW-?pB!ox zF%?U*`seEZFGOSWPD{jIqH)MtU2S&ZZ3-ZhXCM#yPxEZ zZCPS^({@JFNWeYmV!CAa{&5gjZ&NYjsTY3_`K}3?*O)!)xAno5^6At8KIY})%)^8m@FiY2LEur;`s0QBB5?;>_KKa{vbna}>HkP0&CPhY_ zQDV9Upc^Q_x;lmcAQ&$no;&CwGj=mmlx}oDYjfU6@4t8Md-84QTuSz2e;ekl9BO)Qscz>X4On*#<@cB37XCaJ*qn&?WrJm;6Xr+K*efF&q+ z>k7t#37hzwYi~HO{*DTYLHYZVzo)MQyif|Sct507sYiW5dyuD$EX;DK%$DIc4x2Mr zL6FFIx|$^NhgPD-_L8PLd7juQ7KA?dl+SPZ0C$&dIC}!lL@eVc&KIT)1CBCS)|kJvLIYyjJyQp3g9rZU& z?~(Z1uFPqrGh+b%-L<1bkw^`++@jG@}Kf6r5{w zno<7#t_YHYeD>}TU)|fNM*-EL=sf=ph`wbSM`v7r4P3c~1&#cSjLgBe#;n8MupW6j zp;1N%N$7B#rTp3nXKxjt!F+yCkR}v7naGvlj}s`bAukRt)>#c>qU~Jpl;`uet#m=f zA%2R8SU#-A!DB0{FEs6y5(1OXkd=3}XR`g~5lF{d9DS7J{_wmdVR zQ+fEs$s;!#->vV=5>{#2MPW)1yg-}w#%?he>FgSrB?$$pUc8;@}RHi z$kKd0rev2Lv$`Fvqv;ho6+Wn|Pm(ri4(C&^09HV$zvAw-c@pqV*6gGKxUWR7fK(Zz zH*Ci5zxV~#`j5+<;*H?psfXv=54&M^{M!_3H3U+IfP4c^^`>4ClZiK$<|+pzkKjRH z@Z>n2-FM(Ancdxjejmva{;TeL+m)8s(LHiK21e`h=Cs#Y>SwkxK$;$&<{3*+*lwM> zlIJAwt+xdW!^n>$M;MD<@BZ@G9}X{V@?8 z+OVyk%JBM#6ncH9-$%E>ryfJ{a)b!)>Qi?FcscJU5vwF288s*lGt$e~w05ulIGApx znwqY^lR$>A+gdy`p}JZdQD~>Ap0qA@852vuX%O-EZ_06g><2;yF@xwuT{URF_1c1Y z)Un_Vi(d3p(3=HFG72XdcfJ@U{Te|`bsimJCkcyvruHOB1Qocz(M(s5d4U0McjI$Z zCOO;n3f7{H@=ZP_BRFqgS71CFjIzxASKgOdfNO!|E-TCE;9pslUP7HeV;s^j&BVKuC-%=ry)+BKBIje9)4g``dPGp zCF9^n;1`Xeu??m#V8pSlV-g%G^-;8)j< zt2?%&%ud_E5=(sin@4%q+CqO6R>G=e`BbNa(J2z|}v zZm>tOyc4sgslgxr_WR)tbPom0S~Jv~^$|>=@2W_UC_XC7VE^ni=U|oe1u~^2wd`h% za9{eBo}aHqto2oP&9rQpw^!Yz4$@@&K>DmCRPEJ7ZwJ#cwe!D;gLx#(p*wAZC1RRb zh?g7}$*MR<$gGJ4){6H`(c%elu)Yd%?)yf-0N>#csvvFbCI^y%KLgnlZzqJ-aDat3 zIS_!4$EDy7b%#u1U+R`*#Dxuj2VL`f8@zTSymRy>2cZ%D{F9___sxiMKB_gKY4q@n zy`LH!e^#KgVPfsEuz1Jko#KszN?m4SZOW^$T@3d-#*PWc(y}*o%eb(SUJmEYsl3xfL|eqbQtd)A^p?k}!}ILL$|q+&N~S!t%RCU5-= z=m#+l9skTq58&9{MfRP=r#|9Sc)IvaRdDp)xU-HT`+{YwNBYimwbW0lJIGp z38R1=M-B1!$#-4#)fZEw8$Uz65Mt5h=T=PNy*6{wf}FOfBp%HkACzVVgM;~!Mla0Z zWBjcsMEbN!kRK?n^tdhnV@s`E38I#=JMV7S{MdS>t|)wtf11s1K}*fO-ydC+$*gb% z6L!Ki5dW1nk?>}ZIo(;Rub0S#gd=*1hP<>~$CwA%D5X`i{KH+ zZUq#pVWbfX=F-avUPYv!%>(Fj|22BYGE1AQ!PE$+xeT+JQZN@r2PyW8zRlUZVO~xX zRr3{qE`N@`BWXZwyFv3mh1Z`YpUh1veVPfmY^3LZrq(iesEstpxBQ-%mltdCoy24} zSoC6;u|Ln>!`CNtK9$=olQc1yasrc#KV*Krl!bK*XOvgcy_QT-m3136&*AO2dgmHi zh=s<2bX;LzN?oU_cUq`7xR^V;-Vr&1R~esK@s|=QmvFUbgb0>~uk5{>qQ(^w6<nePgq%+Qux0Cj>qaZ9^BNBb?E8gSR>v|2B0Rn{&{Rte0ltko9 zl?4Sf^eMco(l%Wv+O@IdX83RDQ@_RDsFbqx5N7#T_<79kw=T=xU^GCfykgT&tqo8E zL&RsK2KPSv8&nc=h)At>mf!P9rBw@E;qfpS>)!lzT?UCC6W8Hh?oFkwH`L;!nHV*O z8IG>=su$m~W{@QbH7T$%k{6)-1Qt&dn68hD-fjNs3qcp+_#Y(*egsm5yjVx|eAJXD zZv=LTrtR-xzqE}dh1_pB4R!))Qj4P~m0?OiWNg#{9IUqKv#)K{j(I9@277?4Qdq|~ zh^X0pl-=lOjKOr6x1&1qvF8xTQJLjQou`!wxnx!c%=uGCHPo)N$G9u1w(JarccxRn z+~l4(kO>%p`1+YVOJyh;uWr2$$`JCW6!nIq%vG+*^uj61BkejU4M(eA2+@T~rRe$2 z6lj9b61NZWZ}gc8lIDP$5boq1Qv?mZqYH8A{>E5!P7rx+I+d>e6l2`%Oy}XLeqV^{ zZtE%^Il&B}f&OoIKHpE#dZbm5x0#Bgg)_IvFqEpEe7$gbB+UO(~rK!=(cF8$y0 zk5*S_D+KTI{;?RblDItq?&s!fT|6_RoD68KLvD*aSTo-hOdYN3iVhUuT^veAof>9r zs70`w{d5u>?eF}XT6cdRx3P>m3)bmNC7h%8eMfiulwmk4@L$YyW|+%N3ObrmK!6!^qu@$BDjaug+r zZ~H~f>DIgy0V>fN3`7BB@OygJH!qy1X^DU1L|P*&5yX9I;@dInKOW*6fbxiy@VgEj zgBss4lTm$L?D_1zd1tUA0QoR6F4y;j?p=Y~e}~^Pw&IX>Zy(?F0fP)ah=_mt&^BUw z=3rwHT(Qahx!-hGZhQ%+1nlD~)V zP2|hc1pTeMr_ozC!>9buXGG82-V}4oL-ctl>oS&rI+jk7kG3?%ntZ@{##OswC#tW^ zsY#Jdss`<+4}=@GJ>T2v3Nt~}mO}O}t|YCPkz#$1KF$hT^G+|aPSlX$cP^~on=z_j z^9pipH`{F*g1Y6uMW!eIe8GzPlDq+$^8J2i^IX&@NB*!Z!LkDva`f~E>AK!Uv?;l& zPI#@EUXNwS)XKJBjhvt7`6yH8Y$U>3oFt(3Gl+D|xyH)L-G<;#ZuYwUX~zB=X&hs_ z@z2DUxCl}~%0ju`A%ARiPo@t~$Dj3-G1tyo^CuR{(l8nc&0U`7h5~yl45%SFHNH*l z%)R(Md)w|4ulbVjwvI|%u_O*2ML8P!kwhXhS71UMktiKNj4G{I!+-@en>*Mn#<=8YfRin& z)uNdK6ONTk$eR|o2O@Jdar>S9&rlLa04^&Kjs2{(NEy3RM@zPP@8T)%dTutt2JH6RFasWMCYWsz%zw8=natV&9ZrAMcK7PL z4!>MdkJOzJ*28;`^*;>$r>Tz(@E)~{$>ee58lW6K?b0xMHh15pRvD^Ediw{0+bMk) zNu~@mv>OxA6Q_^>&V0kuE{QhkZD3DpqJ7Uhe6``wY1F)fTiUEELlFXqS;TIoV(Ycj zCya7yZw=!SZQB`oBrv)rGQ+)drb6Fm9+pXhLmPVP z!{0lH)cJ}JJ^t=t??=>krDD-~C%n@{5u|L7w|9PbwZpaLRBrZ)Dlvzb$NqZ?dE&sn zN=13?IZf$+%~kS83nQk%64&cvEkwB@e8O8i{yyrs$C;C8wZF>g9rCS^0!%Ag(fGaD zBu;9RNxL^DX3ev{3pUkvJYwhP@E;O{u&*}-IT%8LQ;;~Y1n8fh->_Z6ro|Y>nRgvD+pP}}h^t88Jt6Mh6 zk-6#t=^#VPhn@iM=BrZ$RvjFv#?Fdz=Q)=roH{vGwkB4>kwc*WYD+TP-VAiUda zl+xzyhyo>F{K}Q>EWpeb^E_t_M1+G7x`Z)22BV9b>|qqd+(Vv zZsxLT5Su5~<(-ts8r4t`db^IC{Vs;b71^E0Quoua`WMZ>&|YC7Z|zwffWLF(af=t& zt6TFOdGGYQ*s*H5Mi=X_!iQ5K-*2kIo0pfq03`Hzr(OcdN!amQxuwqT! zRYV5yN<{l}mhd%bA$K@kHKUegmK8bK^)?poGDqFBa44Sjs=Sns{$)4ZZ4{zrI)8%h z>OG0@P@Gp}+^*tHAIer)hIB4A>)F8p&vBYA?-!6xB5)5&i`MXRk?v07En+N^8zqB!jwJ&|9J1{ZK;B<50cp4w0whm>}m zu;O|aW)K%-k$-SMf2)G6{YXcDQ3!m`J4)i<#9JNTFC7(kB;4IGP*@uo^%K78Ib1^+ za~CrWms$fUu=vT{YDU%jJExpU7h&4~vF)&K8pg9DVUsYRz7T}+=q@scQ$L*)frG(S9=Rzels+qwKdiwy zm1(<^4g2b(aSt(n+-sVD9$NA%*{wAM7Mo1IZ6sK=Xr){;DUb-Pqra!0k!Q@Ll6&`0 z!+_OiVu6S~msqAWjf?V-x=Yz)H0$W``*z+EJ0&yk%lrsdpJ*Lz?@#CL?ql%HXfdBu z6Qc~l$6ep1kjq+gx>{o@F_vLq=9K%My(1B%{I{mW0^J`84drmFdA_U@t=Wqs(f~|k zx=z$t;f9}%R!!xA^pDKwlmd5|ZD&ab+n_h8h_w()t8e*1G$YaQ->iQ-wdNR1U0#Fj z?{OlO%86yWu@p_I@fcV!s>^zxC9$@xrR z1^e`3xzuX2U&{v#B+xkW(du{>Ss{k7rp!gNRt|`L-J5V130kRb+3K7RR_)QhdH|tf zzbc}u6@Rxx=F$lKB}p?Qu{6|t`kwyzbX#W48w>7S3mXWnwA1GxwQEbmk$yD$GQu-w zly!H0{!OA~f&kcAs!%7_WLM<6+t*(;8e| z&9ja#S@c>)c3#Runu{-oRg4DwgE>W*Q#M0Kj+x76P(0JZnsTyQSU4Ev}fZuM>a z8jc0b@$Md!s?1a=RvWM(a+DVTmRY)zrb^i%e0QsLQ)H~5MTFMAK0m`X6rrxNO$ljr zYM!e&<{N_9n^)?rwcee)M4y?)&0u?5xxcqlS<@!P`EFZRtBhlX zCRqSnq=$_Y18BdHjH9;BU31b5gRqMz_`}FwafA$CaZ+PRUL&;3FwwATzo$8=m6iDN zqqUQ}6285O-R8EY;iofo)e*~KTiMH_yfwH6)1ZE(7%?bt1qILX-e`rsbH_Cky|-fg zvf%$dHq@dud&cnvduT8lM z;npE#+Mbhu7ENL`sjFi9VXfSpq0hii&5E<-VROCJ<2OKx4|*Lj@&RIsK`GMZbS6RH z9;scqhOY=irZR>L<_qs%D(#i`rg5=`-(A+$kbC7#FXfSpjeP-Z0ms(|Y_00)ExobD zbX=LK3P4(;y>x{SX`}*wChJ%`p`3U~s*jR#X|SbQ8$+Lp&h6#&pv?Xe~jybf>2x9FhhfN$tLO9hP+87m&Cg zo_$JnoSF7kt^2gk*7hM+Cf$Ir`d_)s=Ed&zd#ApqA0$7Ep^nU&^lR3^TF||G+cBy= z#yFaLdV25BrY1C{Wv}YzI2P8AlyS``D_gJdck*Y@CMd6T!$XHrR z<@>im&XClHbMk&{bJ&TZ=BQvq9cF&{Od5y2gvf~xRseOUoqMkE$K(@RKF>9$73EFh zW1?=25@B`-4ac98h9k0I8_59wRgap}cV_-f?Oz?REiHAvNg|_q$Qeo4WK~Lcvtc?T z5)bHqxln!9gyc)$mh}s}rii|SJ)S9Eh5GGt-TG2IR8&Ye+?V6PQZud?Ybe64lqTFI z=}u(3?ObbSD^L;(;d}OV?q%wL#>w|!n+(dzj4P_t<1%8Uas%B9 z&6Lzz>gQ$Xn4qbKOZtOY>6&t1JlpjL4U!0~4~iS#8(ESL>HRw0J&ycWyu~cqQT9;D zH+Wkn?WxwCvHPk)W4L653}}u0H}VsPd(tqNp9E{W7;n>c&9`EEP}8@G;MS~&#lzn( z51lOZZ;^~O6rg^)XRK4pLQV8sd5mhzaYPkByNVWlnz%7Glgv8`btD8^rGuB-(4Z+9Y+oLr!VbE|s zlvZz{(c?Ws4D-hw=QropiM2zVam)1)0>n$Vbg(o=!|CHW?XEo&OX-;^DNAXlM(Fr& zRN8ke43JlMa=mw-Y%`2LU2%i-@6qF+Np($z@`Y;fmWybD z1IS9mdna4O!Pq#$^dhXkoZvq4VulyEp=7OSsm%lCMog0s9tjE+5nFHOKa#c>8!{;n>$+5YODH%df zL&?L8>4gLe`-fUzy=+Omk<4i~KmSSSd@iX?2^eSP%A84L_3p9m-Q+G|0_vtUgvyp` z8|T2~@4Hy+W5>)c>ya(hxhj7-8|2NSdAsXx>kWJ{3c8?IP{H48(izztUuP5zW#l9# zD9O;&!2OQ2yC$#hO2g;TFLDG-NP5Z8Pk>k~X91i?Igk`FNN3Auz_^AQq|{-?NVn13 z3chH!*d-eN%g3!iI->}UNcwiSA+%#jJf{-p6$@xLN}t~$B0uCZbGSPs=+81|Y9EY; z;p7KhCyM=!$$*_?O8zvM>{@gc_9Xms{d0a_BRuD%_c7nhM3Zm3aNqtuTGGLszV!Qc z;o_SI_$jH2Oi!Nj7gfURs5IDzZ$o4s_n7MOo{#-4Qh~qj+{Xt{24#8gL&HD>vHKq0 z%yk>jj_?h}h6=;);b#NLI=|NtxJUQ&@N9E&j}WHxpHL_+$-{&*R-+MEGi>QY+5Kc1 zU&qjI35Wav>gErmXnLyk^&DR;9eA+UH_{(@DY22{$>042hpITjjjz;Hb>gv2)hUx*c@`>J?`C`d-qmx8nYB8bD}5lHY%W_FRhZr1 zW0A#n?fBxj4J8*@z*n|?zG7<9V^nsAnYXip#)~JtpRuj(1$MtfNZ@5ykHJzud10j@ zA1`;6L#9eB3>4+m8Kv@j_OWLkj}X#xMi?8tn0w2ejfs@#oGE0 zx?=|kQX>?)F=OZ+8VCK-`{N0$(+PmSZMQ`tZp$QDX@M9)SqNv@zzz z@g}u<5ZA|b8Nf@Y`IX(@LN@fSxY^&KdI#|DLKO$d;6MD4cPe{!c)I#)&=DN}C`3x|A9kq19F_RB75pBQ_0}Id$7M!_# z#1q6b9T3~j#LrzoM^br=^xg#4S0KV$r1(~9QcVJi9w*~(#eI)H(^BY#Tw?A7jmPh! ztD!OCzmUQp143+vI=sqLF(%_GYQhK8sR6)nv2|dQuKgYzWbYp6{4Lgj-AdxgXz9fh z8hGE+?zTf-1t&&psqtsfOK1Qluqv~^N1q&)b-IhBbnh_N1H#-1ruL**){);x%0U;R z0xPs9XJ_mqU?Xt;9j2vydt1lXo?c>pg)peq^N8tN_F0}q zQJ_b{y!FGxE_o&?xX5ArOfrPC$**Dl!gJ~7(=!F&o>>hf&k^384x5LDRCi??o}$BR3+g!`IAHlw7PSS| z8FV@}dBKG48nJKsg^j~~_}<_H2cukht(@bpbvG(346*$Px_^H#PWob<@0$*;s;5ls z2Cga08t1<+qqmv(XI4_orqJ#3)xNmDz704Q||` z-_I$61F13)8Vr8VJ~Tf5K3}W@&97y7^6M#DFJ@pwBvyoOg_cG5he?% zvu|y4IpQGDIQEB4VkB+?pOgOT@S3y$%sq9@jwrpQHJHWt9EP8XCFZTGRq@$*|K_Ch zuKpExy?tg9OJTNW;irn_ybBWg%#GS`QW@I3KJ%53HG^UqAp57OX5-QifRFtb_HL&f z=)PV)YBj*p>^tq+sy~}%7LU^4Out~sX{MRM*Z-9@-zZC5t{)(D{^*jpP0q%ApOs|T zsU@VIEv=na6nN$ep6=j}Ekm7c7=BDh8`SHWck&u&BrT`vk4Toy#18=g9Hp!)+{-mH z6gs{C_=N!*wiu&hSp0isXMlU9fqKpIcdh_*s(H)HrG31kc}r!p%2`ko`h z@JS~P+6EDA9Q50=nhHHZ#m}?7km~B)hXtY)-YslB5`zlj^nz~18M%K?`{sz6N&fkJ z@8|Jj32ovhiL&IB5BykRL*2Vnj3^Nv4*sVr*|bsP%%Kw!aa3yXeEfbX#h(kFU-1ch zkp<<$O`v7EsTyM(e=*dyAm0Xt!}7mkYWQ@aPJeZ)ngPZ#7asP&XAv5Cw;Lc@+8x|z zuCD}bx-Wtub>@bv3by917OBt8MaRxr_y=%Dz+YFbclcT5 zkxfC*NseD^2r;pV>0aK(JD? z5g)S^YBnAvSnd@#Z)%D)=hXo9%n8yL7%(TljaK5Gd6=&$ydYOZ29l7Zhr4QMYm6^n zCPZSZG!mC-ykdq_hv4Tf6id?pq22cO#9jHGJRH~Z1fPN2dehE5Mnr488w1HEXdycY z@nCbN^c?@TV`|Kmx;@LDM0uNO3~qFw6!X7fr@ukCOqr$;XxzT%HL7!OwkN#fY28qF z?-|2M^6nC$jd;GNAFhYpLcLVF z!PkK$FL8-~qAT_{iB77BYNGFlyz7~8Cg5r_g|7m&rM|E;MHC`l-eLNlStXbQZo2_Hrjzy(@f7?8m*0mM{P9!(tF#Qn=lv@V zy1yaY99$jJJSz+xe^Z9n{a@Q09d)ti2(2KnCQb;JwCIRw+ z%<>BC1>?8~tqyeM==bDbNcY`U08nvcRt}LueXmhDLPuhIa7=v|Bd%eP1*5#dn*WY% z11{wPie-lwkv8Q+!m0^>Z`=k70@IvUkcfG8A6ElVp^_5~kkqGX+koSMTH5=ITrnMWS_1THsS=g#F;!2)Ka@ zC?-vZb8LM;R;sZqMaQ`P$qT9JGn0PyC7(Zc{bjlAiPST;I=@$6l_-bvl|W27H= z0MiTX#hmcVfpn_=pM54!fH2MzVxK=7t%+pk>`Wblb$3phGC_zDSc=ci8Q8*)C_?sI`ZFL36xm$*I=&>eUY9Zj#;%IbfaBxRybYTl(S41@fZX!7mpNgH_4F>x z^_1Ty%f<p`zZjyv)|&!txu`4=ht&C z15zp*0p9z&e3_P8g!S&_@vt`kHn+kCXF)nWz3(Q!g%~*Mac5?>DFazAjOa%k-VQ9Hk8x;Bykz8HOAFpGH+mV zWRmEB<{kdKD0F4t;v0JzyvPY$*H71A2$4|?=G@SPe;5vcbhdpFk3 z?L(2)R)&F*w?~<8T$1ZYhZ#w?pP@sJBmmIS^-*~;9J{$rKG8j}{vIR>G3D>yDKi#C zIn@{7xlG72H4#29YRL19d+4rAUW!auzW^MUD8ons`Y~3s)*`l#aidAe+fnqE)0Pb+ z^5O5C$Kh&(mVtLTO__+zau_xutbMXGMhS?+48gYLZ`*t~tM-h^@jm&a-UOsvMC-|< zJjy*_%w<>~oMS;tY4BSwbmogK;X*JN6EBnp@o=P_w6rhiPsqV#S5 zgQd8!6@z@V+*a{K%qEwf#kcl9>K8zi>UbMrqvbHIYNNG3>d&6E(qx9D$N7fsKW0^27TF0VLLn=4+)~EvC z;?MD*Il?Hz|NXKeB$ZkSAp_bu=VvurYGeG$CN6syy77v_oFpx>RD!Y-io%KZBs z7RA9jRh{?SDvc4n@kZ&1TwG6vD5MSe$J;dB4hO_Re7oO;@hyI^VT1&UE4+;dF1T5B zzYP*b#$g*882TbdGT+u+qJ@Q=J^_z(SnHlwq-xONy#A%C#4Hk@4W{pfZusS*c=88f zS-s|wWGYTIUWJ45F;s#>zSXxOQiw`_;d{Rg zw;|w1Yr0NCPPuuID4NC3$9EZt7*ILWwXMW8Np5$g5s>&UWQmQ&Kzl1@^j%oi@J81) z8$}RcVt^4(7ngEY{&csprQx%?$F3Qj1k49B#|zq=%1mIVHNCHK zPX=i5T&q&`)!^$I+p!=HxHP=EjM}c0eF4knGiQmI@AXmSJ+yPwg$hZkY~2Wsux(HY zeB}Y<$o^u)hV`9)ShG)+X3)@Gx&`pMg4YEm4yp_G9IJDt^?lDA-aANDGZ3bMPxPWM zHWPFkH`>>t`-9kTjfkhhM|DEuF9(jZ6)-pHwdCa3Kz9;()=Jmkyf_Oui3o3*2L4S0 z)s6ns7KN2fNE+}|jl;^Jg)=YWJL=+TIwSNMW9g|jQ;A^<0KBZO#)@Lb*9KC46Fb@*>qfOFE6X2~fp?34*J~sddJsGZBkgOr zK&)zT%u=*S^K$xe)PFy>Z3eH>;?=YeiLQ~YBez80vFU=wtyknD2CLiT@IDEekA#Kd zEp?4dVwFRG9bY@SOkI2eH~bR^>E<>D9shj+V}v^!1SicWYiUj4(Lz4uGgbg4m|Ok~ z4Zdnw*7nWtq+D53z>NF>0S*@Ey|!QA)XsQ&qEov(YShFIglOA-sOs;eP}ng*F` zdKI4*kKKfAqtiUr&kp5i=)_}wQc+R+HRX7`^AOUbT(DC-WJGgK@BNz<8V;!28r<4A=s?5Z~*_uabO#70W=z&Sl==eO)Q?6(L$QI+Pit(kT8O%3Tms=$S?W z=8n|0f0Q86VCZSA)AHb?Lk=&J%>#JiywNN}#;Ubs(_0)v3!2_bLWJZFQchc8Y+z%^qyg^H*1 zZ_xctbc>j$f0+=?LS;Pr0-A0dDQ}Ap815whCN8M}vKqhn{_*Zp zvALu)N@p2AGk=S`C?d!$w68h}ncxKpQ=%j3| zPhPuC=NXpZ`9#8v)@vNT~tbO4?{^z=iS?5O2>2m**?11 zR{+1mItI^Da)ART?eBd$_>`9;Sff_-tf`u&L5X9a%v7dWn3b!1Tm>^i)g$6N;5FW( zSs1#=$p#@pmvpD>GIbhTUMGCO)6k$Nc;uADBW3M!@qJf{El%do@vodW*Pb0Cw0-RV7ih-G*T*_yh{f&VQX55q|5I6mq* z5`YL3*i7{(EQ-YWATId=(^l^=gOzjQ)^m49I{cjpPZ3LNQI2~vytPT+w&pR0B(eXE zo8F0Si7R(yfH{R2YV9epT9wnvHhD%)6C74Fx%!6+l zZL}&R8fbe**~K>a;agjukEJJjW*k4yHFh&@=(6_Z{9f_zN*%+bocHhv=dcB?)F~aUefbzpqHreQJ@Kab+HaSP#RmECI&6Sz@&jNrOF7u)wPPNpBPIdd zhA}zbsH!t}{ki8U?r6;?@%-mV@D6`TQkX(J?0-y%-|d+}=81fk3 zfajW7=h$S3gPjsF?70$(7>bW!_J`6Sj{lzi#IbYQF^1y5N8d*@Cr#sV0pJY)U7G;0t;OfQkcZHB$lT+tv=pXk{l9JT`5moU`m<7;@_x(MsJri+ zK_DW2GC^2L1n1wMr$G#B{q5Om=Wp%99^t!3br0yNXVL}fhe;X64ttqbf*`oL$U3>we7%%d0HYOU$z%S^ z$uVJ;yz;qGMmgYHiNCtnwdQNwHO|PI z`Q=Z6zhb+?t@1nX&NLe2d6k-OTu9hGXxU@-{+GkE>$=I+FrYIKi#UB2p?i-YV^Lq6 z<#F8j()qw~WBFV8H9LYsEoz^Ib#U#uyW%MgJ#a%2+b0Rr{GY;;tok={{ODy zM_bjvN(`>STkZMeIfL@@?U;`TGkx;Bk7>a9ggZa(q?xO1IWWOq!Eea7KYvF}66Z%= z=n)d}c0Hk+c#22Jv`1`#Uu*|jCv98u<;A2{ceIm>Cdk7XJd&3pI17{R? z%)s)k#5=w-jCP^o87fF*70*A|b1EqyCQ0L0M&22ETuLKHkcuS#-t1n^9496NgpEES zFv;HZ;`wt5Hq!t$kl)MT#Q$8k?`9>`0nc#TmWwIyPmYJdxK$o<$@qFmIdO>4C?8oU z=23w>V{X*(pJTdDcV^~sMIxUmnYr6G>Bqn4%S^0n7<8&i$WE`Uv7uz&0l9|%9)rz| z8hif^$hD+rxLV}Ta|vj0eoR*q7)`zht+_uhK=W>o?ui}yW$Zl}*&f3e+tx`!^INuk z5L@uqTuevSGPH7>&kTNoiE#WFBl{Sr{JBG+1`0c9OPS>$L1Gj2OVXo?Wi+4$(R1n78 z-Kt9DC6mS9zuEqdnb{92a3UAfp8Yco&ID*FlWtroOBBcCTeq2Nv)%B$29ym^fH;DZ z?ii7UvjnBru|0?wc!hb`ozdStb3k`4Yu=hWmW4_Wz{!0s!-J2ktEB^#E#KYrXBvHQ zeafx%9NCcbo?8h~XTwMBr)Jt<;FX|cb#sV|WV=}uZ4EVF09 z!W-#JBvijS#YhvIx8*ir;(@f%T(}mZlTRt@_Y~i=KglPq(HgU#nw+L*{k+epLRcMg znN!VRSTh`sv%G9?wHhfqn=8`R~Gi8biwE-A;%aqA+CHD_EJmk*Wc2|1>cyF zhvJzAnXnfL_2!F%gBDb&QyZ2473;@X&6qk<(GNm})xdI?rVhsV3f76*H&^qMhnt(7 zhr!y<wtnD zG3H*T5YyZl5RqpERZ_n3r@WX^hd9|75mL=zCq-7+NQnbqT;>r-z3dr(l6?Gj;hnX@ zQ6xeys;)eUYuo?kd-vF=vvFJlX7InqOzbtR%C%sc6U7_eW(=48Gxoa`kj$_`NgjF< zRvs^(-nmnMcf=Ee?D#TNktAM1RT#lss&{0nl9C0SGiWysemcGRWJ|QX%GfQFivVwv3MvvRz z?q+ifD`3N{$xm&_M7?Ndam2Z#TA-7ffLPd@y23`hi}wEg45Lpk+>Om*f4+qZ8&L8~ z+s0A(RuPL1-E)-U#(jO6f*hB=#eX>prXWx0zhA;A{Wfe64BBcDjh7Jx?=_tR7_Ta|CTmbuTFPdk8q=9I=a=uHo0ScW&xb>AaelLG* z+Lo#DoRKOhb&>Nu<_WV^W0ozvY5{c z10sd!rXxxaaAc~J;BVm(1g<)7CL-6=ep_dPqnqL6m`U2fC4TK+cGHotg=T5o zN%H4|ry5{Sa`{vhtVe!bMdZQKoRmoHgQ0M>R^B@C}6@4ZO3HZACfl*WY`ZGx${mC63CwWG;8R z8ZZa0qQyt!q+`)@X=HHS>`CUD|+P@b!&Jeli-E5AVawu6A?RC_ z{qiLNGr46VDpKa2^aygmz}MxAeL(+FM>R@GxUH`NA{jG*@b# z_+_g&kMec*t?o}IF@a{#FNua|D|>|sYqxHxHE~CK=FUlv?th{$Y?UJEfw7YqLK{sT zNwV2J!de@R-}K?hw2Qq0lL*<3Y(pEVCi##IC5d8IzYklfK?30{3GbAV*~Ernj}h*j z=8KKYHX@ZJ>DY1&x31eBGlbkGH|F@)RQLiI8(L+je#zf}vLDv~$!Bhr%M2e5hJ~iC zNsEz&_hj2T>H0*QXZ>6JM9Usx+jJ$_UkZ|;y*Lyee8*$`l;@+6A`Y!5R1$MJXV1~L zCk1torK#$Chd9B*&OcHoW7tOJD>se#gk5q^&hO9hmx)=RMwbIq!H?AT`%a==9~rD3 zzP>Tz(F5JU+88lCY(S(tcRMEnzySv4HJ99GR*)wudfbE{0L$?noJ(VXe-?4Kau3P$IFL#Ynj`wearq z&ddKd(lz=e=L%7KpFH9oqlPChio5oAg4N*D(rCpNe=0fIs+^mjECjXlW5$?&=AS~0 z;m;NAaLAI};Ep18*M>n!C!&*z`5KiRg&_QC4VO2eF#xn4+=t(pJZKViHqRAvp>Of$ zK-NqXuo>mEr^D)$1}Gfkj04cV@NqXN{$N1LS*g44I!;ly+jTEw>BFSp5=Wz!_xfDY zw;U|9tqJ8kdf`&5*ucyGA}g2|U2AQo!jY?DXdYF{-=687R8S#&?BxTwk=Utm@*)Yy z9Q7%D`fN(Fe6ew8K5M(-{RCoE9+uj@{8e9ao}jCpEbMLNy}~n|hCw6J;drL?R|bfe zRu`k&jo$MCFN&^6Z1*+S`#l#D#F)4Xk;>BbzLx+B(}(7fN3pl%l#Ty>V=OVXEQz|0 z>~#s4Z&*mR74^woD)+J<<6BCU=vUYfqfna+YmtE{5{--OpR?Y#R zJa^D9Q%XXo_3ya{Q@ARbD!s;mQ^d%GXx?^Az{|l34PpEa$I6_GnGU7zIhHiZ;88R9 zIPX)%V8x@J|GtgEyE&mP_50`8*2G&8FymY9Xk4|rx=Jjkjef~@ zbRybT{`@AM#p_vg;yqu&lzngfg7I<7^@_@WC-^wE=(T(9<6g55u?pSx6bEIvI zO@WRYNp~I3Tp`=QU|gzYB%H_&As%;b-6UZ`A=s2$f2}utJNQ$!VP79s-U*iBeZCbE z`#Wl>l@eN4vy;U<%{$&d*T{Q(l@wCQa?$&vKVskrK>q!Lv0C1JT2iC|>D$QC+UsiM z8I=}rP}T5AbGl|v53ZoCo`H_sK-kHq?FLhER=-*F8~NX^THgAC^R6%Yuuc`{kJ@rA zZtm}D8Yy0>Fp*LH)-fRYN_3a$NB`qYPoKr7ab&-|Jh{9d9~h69%Xw2#g&Z+B5-!7$ z)73Ye@ALyK4PnK6z`gKZIqP*fIZ@%#8?yfqW%kPV!z=bHD!$csvBLf3b_!-bV_Bf= z665lQ8a3XU%jWp0Pk?Q4q2?~-+K%#fM}$@#?!!WEmWNM`s=obcCCzJ04%P^0JldSK zD|69Nq&Rf4pugn7=NN)50#{H@e`_CHk+zjeK6CzanxRDos5rp!Fk?Q>=Y?u)9RtG@ zK<3#1o2uWHZrlmK9pgm<%z-u#e(hd@0oW3*4wE4p1isnk!V~|C6PvIX1nz^I1f%}7W?lmknnl*tC8JWqBfsPQ?}=vpg%kIDVfO5kEpeM9%RIeJ*tm<$ z*_Pq?74nz6Fl=Wb-!r~{2hYs|iADTNimQ0_nG0u85hcSYygrjEcPh(EQ<)%tc zBOgcF`8NJtMlmWC=8byWyl+>;CtqHb%wNb%G*= z-1h_ltRtK~KT}E6*eBCmW@c@dMw*DbUlJDDY4&;)iSG#i7`0!+7|D?}g9@e_2n?gh zWUW}b4I=neqz8$ki{$Kl-fG9LCno^Y{BP+afdm9K!`7eGGhtg&+tSVj$#}mRTv7f; z@JTVwEEYmsr{g{|r==^W*RdF2kw(W*QF6D%fV(Fo zz(yu$;_H%Ch`M4etIj|5>KeI7N6Me+dsW9}xl5q9&rcV zf0nsi0$Fw4W6%4h6_Bh&j@M-OSE0?sYUs?|(zP^LRqMn=v*34d1B6$k*`n1M?Pc~~ zIs+CRCJfZ=SH^!!?|Hh2&uVJv2g9k-`ECUf%(?#NAoe^(>h_)*(oo-v!28_(gg@va z7y$>K5>M&t4L<6!al~x|4Y?w;=^dH48PeKXAo z%rLgHO{Nw=WCM`BLre}oK$LclUT@ap2=X{z&%ZR1`}z`W-r{btYW^O3CNN_uISHUw zgsOG_GZ+(!?3}@HUe}SP-j29@w%b%sncS@Xb2sW%dO6YiF9pQ@S5!Jy%JkLyJt7)c zwA)lE8QGU9(#BYuzu@26tGOYk9X&5%(_meyB%B3=&(2VWv6rsx`AEoX-b`urK!l1z zJZBu`?f!^(Oy|=$!na6xJWEbL-SNWZBy~rYaUY2rc zS@^~kYw630+;kvqoG}CiUVMS`dPcAWgeuo}BO5$J{@XM4!=ohIB(}cNQ9k^#$c>i5 z!b`?Tlu^FQiqU-huUW|N__?aFzt*FQnm&6JZBT)s1psFZVRl#ZeB=0KBFG$Ip=WP& z@+*JGWyHg-NVO>2v`PmCiqa@TB5lps+Vof}$31krDP4CC^NF zkL&j@o&3&K34NEWJ*LapwKjG!c-=qqFG+l6rCaW?=M2EefH-dMGZDyvMTR0xQji_W zb0Lg^se3gHB1p^ykbGFehHsI9(vr9U%a0o%vg=Yv17o_fm59>*5Xs>NplSbJWdhJ^ zR7_lfyX}N>M9^nP#%+a(>b$YeQc+5&UhOr!sa1){ttjjzpKD101oLm=iAz!~wZD_U zBMHTeUfxsN39?oiA&e%`48W#^GxF*R1+jiB|BMRb0h!K!@oIDn?DpuX)(;gUMS|QL zP=>eSbMWnXjLJI&vBcFb`+a>3_gA8Gp)JokRq!1<_f-frz`mv*!bLWnEOF0AhY;ou zem6P*{5;#68^m`~4ncFpLkyy30T(W|0x5x)+m!I+N(O0ymq((tpT)T-{#fWnD!9XPNdVipWmM`gb2bg4<^9q~N<>2<=~*h=P@v|53(T%X>J12iv5UIc%eWkQX8c@s2v ztnh4ZrOwDq#|Bg)%H7?3o~7qVFV; z7ozCkDH7D*zL~bZDs(Lxkj+)+Uehu0HNSH+Pda;1eT($cW|yW?ydyI3fMa7^E4I8z<< zOf;Tp{tFOK`MQrmdSH>d{VA!SB3Zt%kuKgdc_)G-|8%Fxo-&m&4D({3*>@jhqC~Il zT29{{dd{(*V^_$`(MZ+*qUnJ1JfAN;^3Bf1^=)A6ec%IVT1A`={#~I`NQC#`W&VE> zp_oelPSF3vlO9(J!AIMnWir@m==F)C9rSB?>(nv7efDjau~Fjd!T$lZr#la7;nZVv zagG_0iUzb`hS0V3J2oXYOVVIB*~u6OE(6nvPq#4+Gr@d9O)Et6 zuA+DH#~Vj~K0J{(Vdi!jW$;(SWTZ614-|!sY(BoUSwo^}ptR?u+ImWD@XGu>u%#lV zxkc%^1-lq!J#EE%T1?|gmLPs6NuCA~I2n%vM>-3v_9sS_H~vezr^HteyUBjvyB*!)(@X>yr* zV=|oZ%zC7gGkV|r8xzm)n!@5${_>gj}zdU6^3I=rdozvZ9(*}{k2AuKICd$uwNZ7kS^6r^F1+r`CywZCBX zjy7-D`Zs-SC`2F5J z`LKp}@YFJ{@uPnK1cCM{p-I<aAzbcwivazznS);E|`_2JUFl8GI{r)y&z@APgC7+y5XJ6`qW|Ge3tCxaOqb4 zrcPa&?A;S#%chFD6Fah70rnPWLEU$L6xv_4Xz(=8;U6wG+S4<|8Q4iE-aLRv3MHOQ z0$T>SEcy3!6u4L79nK`MLT9(uMMK6vaQXPXSnqHA4;b7DZH@j8Xg_DvO5%!VpCxcR zg>ikUQnasYBfqN=%u%$~^Y@l^ctfUjTp2C2Q+{O^-w`35`^*5HZAuzF*N_eNocwU4 zb}X1H6Ix#l#Qn+-5GT}`^XgxtTVNp?NIo77XoeHfo*NMH9mR&K12_Z-{N>YGTwI&=7yd7FFY#A{2W z&JJVf1Wz9Nux?b3^;<0+24rNPY4^9>Y58>Br~fcjuSn)!vIS z;aXFzTL#jZcyyh}Zy_$k59vtf;KrPynl*!+Oap-r$^zyGgZ-bl5Aeh-AzFcOnO4Ze z`^e))5omk)o~l^hl1-0B`Q-rbvio>Hy=4%;f5WgbO>zsX`#=XB6|_o5Y-_nA5p+w$ z`YoJcVt}g0@o3%qHLwSGqT*w7kQ zLk#YXTWPka^~aV74mYo5NuvhTZu1TDf0dw8y78kSqTOEO4Zlqx#SO5P)YM(YOcIBAvVu&0 zugACe+#SwrQSu$U;;dh356^;UrlcXcqyk(dV^$3_n8K5LV?l1iV<@lGu<&g0jK1Qe z3syP5QNua@4~hP#?D^f6(IXs0nVt@(!mQN2%1ZpsG|^`MHG-}ES?0>bPmzUtt)nQx zr$m~ivN!M>$;>GZT?XN?%>#2@qjG(h$=T4-TWI<++2_jBbQn>~pHWjTxPe)5U-<>-GZ~ zo9!C@nS~4fh^O&}4>>ijbt3JqJgf~>hBDA4Y0h9K97J6ZEiq963cuz^56tXOiM!_+ zT0kvXw&C748+h8MUh0(!9+uu7G_9o4q32t^>@FGsMh4e2OzeED)+N7~dol~y7e^rg zibZCF55q|O!^Q`yU6Y*v$S5WxNP13MlCv>2E22Vn=ykSa9EVJ+y9i7+80WY8X-w}m z;DQmyb9Dwvk3zMom1*@txIH8~;-pGq#Z@!u{>B!1%ggVEv|f{zV^@QS86u7?G`-sr zd-%9MKR}9i1QL}Fs6rASj zK_9TD$ws;IM#l{}mcO-Ep_t?yI(3$ZbN8^da))sK8X`>*KdTyd>t#)GoSHrpmv+Q4 zo+qlLUPS)fcLOJqx&TZeA>-=~+IDz4j)39H!Z}qi_ zWlizH`KZ=_3jN3eR+|UEq=q=1#m`+^1p35B1zAoure~88nh?t|smEe4+m|@~2WFoT zfZyFb8P0}=j>Ws2#|%vv_rXkGq=2iQ6i@#NV6TYy7C!-ag07$HF9D~$#K~Lql9F&{`TFLosgORW~@{jgw%y$_NT z(%xSb42A_yZQ)W!uzqvXXZ@~99tyr~x{sERks%)8+9#x2eZ(Jzw`rBFI5B2phS-xH z=y2PT7LELJjt;FFNbede+u!HSfagiC`Q>O-a~wr--(UAEI^}W>GNk#c-De%pxku>c z*v?Gdb^4U`F6+a}-Ne=m0`p)+7 zwe<@MK8t44<}Alwv!8tdK?M@{*MdID)atYkD%wKBYcHWe(0MfCng7z0n~~Bk{v%La zjZshxIv}Lw+~v<}%F?X-Y6bBLM!v_m>#_AQ1}O`ExCau|P+8br3_7vGN66yTZ3jB! zx8MOvwxa4Tv)WO42{}Bax@{Tvl;Y`&UlJuu7rK{ppRw&)YamaC^R6pt9fK9jIR)C1 z9bXj~@+BM zvb2$jR~@Zi$#cr|bAh zX}S{NT+C`f(MLS+7f)|5k?H#P*oWkQ=L46%d=7~4QM-EE-bvixC~}25&n$%&!@mUr z#%N!g=I9;XNnkcb-3%7kx({&%HnT;vekX8)zs%LlsW^?ch)F1dV|Ga{=5MLf)bzGK zY%M=o*D_&TaPzZK+5#aI^z^$))^lP#vMcic3Vi%^^2q<0{2`%v%x9w#GKZNk3HiKy zf~%Xb0puf};Nmd(1h(ab%}$`ilt~RETG0~1{kQ$^_~`rB7&vncnVVn|>hNKmXw7E0 zj(W_m;Z8Y)jo?u+7$K&%$H!bd@=z!Qh<9pI> zv^VX_BUkWS?PS>UKEE6c_xLi9O#1N`FxmIH=E=9Si#{hcP9-^>Gpg~CedmRQKe>Fw zMJ&gBjn$IzLW(w8%F6rF^)j2!#^55#G%@j7a>huNM1cPy|6Vy3{hiZWmm)V#?t)md z_rw%46M-rj9|v;6wJ|5<%#M-WlHKN>Sbm*QTIC5^s~##N)~k%`%nm<~T*0Yq%=`V( zAGunal#3YMCXBFzk|Vv;orVFL&b#v|*z8+-0Bsh2_)A4#Sv!~ufc6;e)j(efv22rb z1ncbi-(oj>6TOXDzoEm@;>?Yx3PIpG_vwxoHlMs*Fgiy)4woJkruo|{gW}2X%=m0V ziXp&~&9)xZR|akah5YtI`qwcfea80y`R2rX3sLd6Nom$(o>NIJd5+(Tn7FQ?7A6t(nAV?+fUD}h=eRO$(}GjmUU3boF5YyICI3(XDF-PIRC!M(f_Yj<3KOCQ`4cOnzK zv(W)r3e^YQ`Ap}i2A`SKyuA+yU!?Y>8zsuzsjJdRJijHjF5NYfkBjmC<0En6pjExP zn+{?Y1TEcN{wd`RZE-uOH->QeL@|Xu{&vFOb|0E9Q-E#qV%BcnThIt0kmGACGf_`L zJ9GT>-CTPj_nPpDMnHScNRCYZzjV5?EMPz94AnujW`*o5-e)Aidf*IQuj8}v*bFmm zNQk5k<~vzFgCnDTMd6*3WmPbj_O7!@j9!u8y$ZNa)62uyIazGIft5(&cv&8sqeNXj z@y`hrq?zZ5i1C1E!8Iel!@Y!s*BJ0V{^9tlp#t9#THcS2@N7fJWj(lbnod+lv142B z$ubTvDbpaw{46J%qTyQP#92hU7g|fH0-LA2Yli6?@uGiE^IL6S zxYvVbS2*S})K&UgRHh+G_U-_Q8D~*kk2|xflwTq4Q3W_z27?fh+2gk3e%0z^!8?aP zj$+T9_OBE4jZK0HvtHkFw@Q;*AQ-||?j_haJ|jSXm%ntlp*vvRpxSyUH!)vH?qY9; z(@tGS(QXLzc2xnr=@cI&#na)AK|SX0Azxmr|=n5BKS!TDctZ$%xvn27QMoefC zuHgQWrDE(ESNd1^Zx{@s9yo2r1_vPio>}{;8+@e96G3h|Q19)-GD@T_{IThW+s+$; z81|lprrMTLpuxFGT$OppY}mNC@bf2gvWk|E<1i zV6ml#Jx@B?-3;!g`F6mJd%;{=Brs$P|4n8JqaOhYZ~pG(Z?e3`gIFdnelJIt6@6Fy zEzx@~H*g*O(=At)B8m&!;TEJRGkx8%Tc%coh45Cw^Rf~5Pu}!=Mnxor%7d`FN80RUz}#bRwm0#*|!NF0=!vqCe9Vr^5pbtd*kESkjp?0h_GLLrN<@3 zL<2r>)V&XQ|NFvhJmZgXonFr@H*!RlRx#f~KZ>vdhhN zkAadrH!+S_BgYv2i=rhh}JyDQrRhseV4)X=~`!4*{#oL`?E z?srO3%{d*V>AU_ml+NONCmxK5vM1aDb3Xi|b z$_@hjt>IgK(@8F#>7(8$w~&))zX`@-VNubYlc+d~xUzzF6K^BTd(!#JID3yo)V-WM zDIcB!;z(0?X~hXMwe#;Ml!ULSKaFv(VVs&hAK;iLF-lS#B&{ZS-*q(Ne60=pD`68y z6%e;Gs0d@-&#-7s4DIl~1B@8~19U_{Z$TkPE8mzzgHpS4w>9mFf~+4Ayj<*!#&v$p zK8i5o=S{mKRZ;ezHbVF?p&tGcUDQ%J{;j?9Ze@K02oQvfD1dVorrOs`;0xbacSCiQhe z8DBv2WbxTW-<4WXjS!lx%)UeT!8U^ED z#l|O7z#EKFS1T7Tiwouq8p~OhMJE&kQ8|kc4wzSN&BocaBfPF_k+&8~aqJ2B4u6L+ zBK8r2om}Vrlvhvu<4n7p3jc4sl_`%2e1`CiO0gWDBOBk0kKit>o>`DgE@Bh?mv4*z zSqlLrW|*ZGo5-)Ca~cKq$v14fBUdjbO|=-=vFNduKhIWLuHUOgBHj1v>W%(G z(AU*>6}=MJwl>}2`kPyjGV)YD8oAcpMCkfz^l=~(#4BKhIA>@T&?9L)z{gQ&FGXKs zlfeAOPgj`Mx=b#`%-p@$aJK7BF33jY+M4g}e-FOSls=@%I->lfc{7uF^Y2@q$p~V=jaR^FJtnGUalXFyV|+$I z-m2JKKq5pUIYY@eI8unhRdUB!oq0CWGQ2a|SU4h_pNRQn@XVUxw{v;{SiH&YwuY~@ z%Z3OfjGuZIe-oSg%-B2@-a0ca)Hjqc6O~wM)G=!TA=fq+YLjb}p|flWZ{fD*P{xem z4I<{Kw70T#yj#-}?trnaam%hr7o;1THM=97f zU~FsCY*iU3IjGJFJooX0%5L|Qrv5JChIgjBFM>at-3lxqslj-vwbZM$RqL#`(mj_Z zY$q;F6^F4h`EXZT5kf)DB zv~`pR={((E&H)3q6wfHaug)@g+~yv7i`j8~+na)@? zGHcg?9t>997f5|!=9ZP97=v$;`}HYYb*hy*71MB z-7<1M{sO|65Nfgxi_Jxt$7sE2Jr!nR2CKU0?iIdroZ!k5OmVXmh_L4`G1G-N zbChv}uXM6RgmkcJuZ_=wC`69A7-AezOSCRz?R=ah!Xq3MQQX_}Eq=&ReA%B$mZ`?r zc*$}iF}v2;T4$9dc2l-F+ELr^jXXA2G}lUQeh1@??lAAWqXh;sX`84V@E#pR1oflS z+ChD7YBBzvh<9A0e+MXxfV~tEST-2z-*a~aBI?_ITg;S5w0wyRZ8FWAwhAuH2Qcb( z@MM{}22$(u>7jl*6>((i4h!Q!s_d7*E&U7?AjeArjF1K5J)|SB2A^B0fKyB?8C6yo zgJ!sLDAqY}eXE~V^{MtY1>Qy|foLyB1ZQG$VRhA4t^(KK8NB58B3C;z3^=bPC-*V> zt2_s)@M+jTa|vY-BSdPZXHt9bJ%%`D@B=o*cV-NLQ-Jue~~@t%Hd9lZfysYotPm*v``_3?tF%`(l+Dk=rHT|Zt#+*u{B zLtpR7Tg;S@M zIvFR}GeE(B99{GhxLY?5Sl|af>+!k#;HYMW^!wkw#Sat=WVrHgK1VkBK>sMy&#!X3 z8{#akDAuADX2KDgo?@`;;=&YXKVx71SFDhUt?{UFT-d&=wW?DC5iqg4BiAew;sF53 z#Db{o%|4WA>a;Z)Fg@S5_|Qfk$Shg4&u8t?n{IFP-Hmb#Llh5NE`sq%7@Yd1)YtEr zyOR#NS2`_gF$ghJ@E05s5eEl==pn7no6j#9U=ot69ghG4cjkV5dtJ5DxLAg-)YRo&$hfrm|m?WpR2Z#=Po*2J-V432c(I$xy!s-#^zpc>|)!IlA>jW9?|IzVHMW|7lUWv-%uDVj>M_P-Bq@KHhEFcad-nM$AVpGpX>fyL2JLw!|B5waxDo__C*6ZdR`R6& z{F>MJrF6iC*a^$d=w2>qSbTflHN$oX@v^Cwyhvh7RpXhE42g@%Gpi%yx?dS-?*3A_ zu{T;il;ioF5?DNdY2IdFq6zRtm$X;y`I1u{4oUVVD^u_7vWN-sUqWDgI$KXXL@U$o zuvZ~ljS*>QhpDSJ&Ty(^j5up<`uGmys_uwF*`4<(wcWslE#`Ukt$!^3erq`^kFIB& z9zs3$3Ad~SWX0q@hNBA?M=gZBcy~D=0@pm~J*7?LCd3ZKCF4nfIh#3dT8e z*AYr>guNRlE1}LF{hq zFeysf2c+%8AzUZm7?H3=8`oVwUZYPuy>}Vg1f8fIp5^|3nZU3#@ab}#SfT$sJQKeC zc8ibmxT@aR!oO4XJ1fVJ_~2G%;C-fHu_fTb1mthSgFBlYG`uHKO-H3~Q28g}uGs-o z#jy4uzW)kWV|It-hPB&dSVi;Vm?+@6>8ww$v!kDE zmgd3fHk|q7ovPK8nhTN!BMgU#d-na~IXrWpsz#n}iesIGIy&!}^?gyc*trzi_wo&3 zjD`6QX7h4SMxT+^=}6i}HUaj0c$T6vRGZc4phjH98BBqSP`)LETXMiL>W9riFx5|y z*Y1jBpvXT!>A603^F23~)+&~y_`c4nYn-20c=6vRJM8$pO9x$3(_UR%QhoO)rz#y~ zN8>oGqnKRdb98XU%!@hij3;HI@zGWA#5K&M$lT7eot4w-Np;9xX_NmB#%WO<1AI^0e*sB;*gs=s2t0&t zm69T}V=KQG%qCK+BbCx0AM5;Qf!ryYjasYJ3|}?|+roTY&dL&5S~FjAhrG zRUbVyC);$Yf-LrTg0h=;p+;h~rtrSTd7l3jrrOJGelVoH^oJ55qsm?8#YC8EH*9S8 zb6iN4`~DO1#doBInN;*|ttGhRbCSvM^@Z{kg=bp({xM}qrpH`}tOj+!J*dS7 zY+%DV+6VATOZ!n-qG@hgt z{)U*(a$>gR*fwaP*GIjdvpKg1ckvF@^eV@YW zFxM?S@BUa`>;DkZwJRCG{{G6h+FoG>WG+J`@8DIc8VzuqkDfp9WV_2lKSv^T2P8&h zulq&&eHaxIZB95kJ3ENgzSTRRQ+P#q{64{Y2oT5QpdU+$PS&e%C1Fm4rP)vMD&4i- z>iciyLpPfCY+LhM?s71?*~im3a!LF+Px*J~OlZ5J0F&w}T51NLa8gkQ`xiQX)T9;U z@_u1u&8pRQv;9LYN?aOkG6rBIFBDF!T}2!6E4!${X>QKnghY*Auo3W`UiJ-U&qD) zc6TIkZUP55-J@!;mH4us?_!tNb2v+>^Wga=J4+UaMaG!a}qDcwhmEvFGf2&sBu#B))yUMAB$%+1s-+wt^o!U2w)jcGP;xm>pZl>l#2B5q)AITWy?_Bw#?b;54sm^=c z4*5iwq$t9qCXiLp<@xf zwc0mZp&}r*@(K8WRNt}tr&DGIG1DKgnzHQ~Ep2(NL?wqhYHTXhfQvT%8il}?j^_$T z-nuUMs2)dMTzqvRWN|z@lOP(IqJFQs*4hV?!e8;dCjat}ZiG z^i3Wty;hLo5QKych#hn5bfhnZTiLb85KZqj=I=BY^8tNV?rrgq_c!fOhBh`G7U|w} zprOEGsJLW6A>f8M$H}tz=Fbw@@4xExUA}>6X=_-O`zLZD$<<;Qe%15zPkb?8z;)7( zn4*#RRD3(HH+|~XKyUqqcMLVQpsDj3Nt+mPeohMq&AQZcPsR;e$^>jJ%mimzbWit~ zXj^8kF6HC=TYVcMA5oRW?VPpielo1 zw~nU}EAYDRa^+2}Qpn|UiPB8t?C*#r<1_O91+~2aJ=n_2=vns}7fP-bxF7wJ>nAUQcvd#$VbN4=ilFL<|?i?8tYjT23?Jk4#LfvHd_ zgQw)xaZJh7_*3wM+;Z5Y2Eu`-g&)qnGn&&8jl`9u!^Ny)FTIDe+%VM-wrB?h?7ID} z-J8$uPzog{TE-)Mi`Y7r&a{;eTf5%DXPm_J zEhCDrp#Ck=fjEi?eIeo78g#UBW1*7qZ3>>B`FZfS&~!N3qC+2t0+BM1$=Gl?=Yuir#u+%AhQ-CqiZAtspcnYw-n7m(?o3gsgIQ##*+p z1cCdLG`{O(bF*#d7RZ___)Ve>GmzhQUhXjnBpuvzx613-!U%|kHaz>_C~3WPUYSxN zK&56fMlpWL2K3OZ3Rx$U?MId--^y;YbXSv1ZDlMH-EZ?aXZ(UQ6EcLbZ^y4gJY%7> zkU{=7!B>DyGQE7B??SxYG6TcjlYqh+(!Qu<50XV!d<$DTB6a4rtnXi%CB-#8rr1;? z1&$G!pa9aAXGwstr)b#a-#xvp$u-MBWM=jJEj}deEg>E;PU1}cMuKbg(A8;V-hz{5 z@BTKerE?}j(kw?YK@@`;oP;xEuMAWb5U%f8NNmiail_J-M&%VnT*g*;Jnha}z5A zuH38I(yN)L;U8b)zUN1-Zq-Y0-Dh>7K2&j-b7a)Q>mJ7EZ~*VC+L@3`JLgXG{2i2G zP2hyh%bzNKos)7K;{YW|SFjW?ew%j${6MSXtLuAtZ?rT@yBs&q_nkaqh?Pr@r(C}N z+Z}xZnc>LwjwfE^JLBxtD@($5nY%5YPsq;u@yO`XQ26(Cf6G5((Y)4`7es*5lRz4tA=lGv4oqJTIpc zr@cI#OG(b#_MX6*rHCBvMk@}&xBhlDb2s)CyWbo%kJu+hw#>$=ShlIkGLVj~Ipl?-aD}IO zXZ!c$CsFQc1mw1uW%+t=FTm*AP}C3vvbCffduoXY7efm!GZhRMRa0`dgVwd9mgw_bq*1MrEc~^A!>l z28*sM(sL0GRh!~G#r1pQ)I^Gbmc_g=gE194oidkG`=q~;}ncU30I5T8_=lJlRtD`B}kqiUPdz3cC2 zGn&Dq#QpLi-|f3#^$5iO`xpMGG$xMcb}+!xoe%YspU?6|haE*jh?^Q67;#J$^B5Jw z+*LmrD8*s(xjXMY(0BXTvh)UIU0bxqXDi3cA@6!DsPHLyg|jBB+3&H3GD4}ez?%S! zKW~aFX_>xrq(qs}yqTvDoJVEDvh^+@vJ-HpR`f+|L$150&qYW3wEZMfl3ZL2;~t6N z`z8eD%M+Cj-4|n2TWUvR{tazCMK45vfx_S)Rl*o$*ER3N zX(Yjeel2&xRGyCB?57i`ikbP?!J`n^+{kD{oI&mszdMur3%Zv}*%}wNLZv{5y@W8T zGZB{Hf9(bEI`cV+zw3(n@KfSBs4RO zc=u}=RXn_E%mEInrr(BS5#nu4DfIx3&O~cq?_(VFGx0x(ijSN_aw_;v_eLR_&l7^o z?)o0Nl(PV4yya(i{jY>DS0Z1us!jr3O4yr-apWAfD24Sgq0?yanZPYsvJBIzU?S7$ zHU5ug*`2~LDZj_veq9j)$)QauaMMQ2|GU@UKYx#R`#3plHi`PKk*EWAM`2M)MkSf~ zA%>dQae_xpc+OESd~Pc;LMS|V8*Vex<~FH)Hs74$2)vUE*z7%?7D>^+H;GciYf^wY ztfNt#Y+{p)G%El0Oym1PhX6WxQ?^sD0;XXEM;ag;q}36Nti4XGQsb#NN49>%oE9SP zm7F&R0<0`#X&bC^w1Hsdhc0KbVDO#vx2iiJ@WNCB+XJN)Z%4OqV_^V&cx@x?2e2w z;D1g}s3A{DWq) zIpS4hcz94$Twh*noLB`4_pMRQkV6+|GL6obuwYNo_?OtMFQ#AEu4XS%gFVqh1NRNl zk8ryS{MbuQm(Pd?nQ0$D%)WZ^Qu)SWba~!SX%FA_&hqFgo7DkF5RgOD_QgB5r{9@` ziut;maF>eY&+*yS^PgiyP@;fN(vg%?zw29k-Sy?ph~gOBL+60t+N1+~B2Zrihy`hqHFrb-IlD zl;PEk3LidNerC->%xxEx{J>IFXjD(^1K0m`irmFjkpD&Mh9a$^Ez6(zmOkgwUZ{r> z`E8bjFRF1@Tn%?j)w$|q`mTC;u_<8jw;TFPE8LQQq~gz)N1l)TayaSOuR$tI;&aK0 zbpIGmYY{rsDpMUq}oETe$FY`$p_TEfeDkio!yG>FuHG^sG zL~kZO)=dGnjYpw_0(6$~%O@-a=AchHc0W-VinNrMeJE&bIb?Fd{PXOsCrW77<+}u-*n;#S+(3ui&rcjN|f3wOx;@; zqwb=6^K6n~_rW_%1YMfTwFq@BJo@;Pn|?OucW8f(K)MV{_|&%J9C z*ic}t0Gs+qy74v7qjM71)Plfc=q1yTaHf3wa$FbqIuE2I%BoB?|=Q8`c`AR$EpR#?c zZAKA-Mp1PAl0;q&87kmc2#S1_HRm-Y9M7 z@I$1EoI~gA^y@QW*Dm>8Wz{6+4Hs@tWO5AOhn;mfBU?lfL>Ybna$YE~ z{*Axhw;pc!>v@i`VSs69-r0f7>1PO+ecSn*-`gXW#4o-+lZoQSgPo6|60lcFUAD~L zfz$xJ979iF0-LL$^6Yza*57}$Oh6Ov-d`M*twMtPB$oXvp+@sl-Y(n}`=!&)_#_S& z`qSTWAWP8u{Vr2Rvph2#B1i5@M76obB*-PpR zM|x(eBf-Ity2_j4{_q{d!0G4dFm@R^;#Fz7nvr)omu}8~{E8Ev2{h1{Y~_I!qq%nA zqe&jLmf!L}Mpcon6g_tm4xwJs_wY(47-M^PF!{R!C=UIB){z5O(v_)moV*9V-lzp+yiPBsa8i)Pm#|IWGY zaKQOBncq(ix@XBDqD+#wHJSIvRgk2++x5pZBV!V)1b4XpJ=`HfWPXh!?})DnrA}`E z?jZCH9*OAeBUIiOkDX-K;qaz(^%4HhnJ&eJo88Fo1i0Mdvzl>!2THu-i z2*_COL@mZ|59}fa6MkGl0cI%Q((j@4ojHh9HyuOxM%zBI9wru@)YXAoQd_1gK)W|5 zH}n;{vfo~Ag>EddEB#E06xhV`{~H5gSA|gH^sho{h@lOo zjplCn^c#wqt3Vbj6{!!#iX%f86mQI9*tOab!^XYuL z`N3m~TvRfa#1dpAQjibLm4N$RCueU+JQBHjXy5ARuHtEg3_$!cOaoBRS?LkT2g$qg zLSYXJNI z%MD^xK`^;vI5Re2KDud58OXys41U3EJks?SfH1*zxrJqBq&RR~t9`F;YrGjF0A2TS z>Tt#bpqV}mFojymTNToB&kzt!$x*i>oWq90jQR=b@++%DA+K~4^oVl0rmW^{?3KG_ zc8taGfINKl(DOGAl!$E6}sA6fioDD$w2hururiI*i3R+_)Mm_O@p z@xOp&XUz2_FpX;&JDy{o5zze(S^)ahke}D2b21;3W8&o8XVU#Uw1F6n7{^UcAZ=px zzuFu~;3Czy&uA3C?wpPNq6I~No^qpdi*Ym6dQQ}i&*I^SVogoEU@U! z(`E=AO3pm1d;H!KCqTW=^VubzTT{Dp!qx7-i{%}RpX5nm+x>Ce+<%YDjg~) z)<@$#;6+uoH^&%L$D^D-mE3vGe7|||Bb?-Ql@!~g!aeHNLTIwchefbT`_7MRB2zG; z9B1bQbEx{31a$sPTpE2e?F`9O8XZ=vP=s}=rPgke#GjJTs&AwE3d_PrtUdNcLFb4S ztKSuR2Qsj}=r5B?TYXHXoLk`UIrkK|_>kPj_rZEiYK!hoxz{bojYg>}FJ@fU_M+$r zceP~oXi82vsQo|l`pRi{y~{Boh!NB(^646%PIQ>Zq;(M_X^c=<7VW^Nc&S+DEP7 z?-Kr7ebY`s0wP@Aai{(8V+CdiZ}_ld9t4e)Gg_xBVsr72zLTa%I4$YKMmOdj6=cSm zaUxk7?G)9vd^WQ_F?Ti;yJP(qcCN&10(t)FWJbuXVm!8N^gmA!9wBV`C`$GT_!TY* zhN(lY!9kovo-^QxJu`PSZcPv4(|MG zwZ|H8To21&6tDyTEe|B=8#4AfsRHG?)5}pSCg6mWVp1K&-e?dte?`GwSzFih3ixzV z=K1@re2hf$IcLc6$%IX%p|zJ$L}u4Z;Lw_qcT#kC{uu5`NbNeQjAu0v+8;a6)2&7| z@tiWS7Fiy>NqbX`EO?B`y}FSB>wR;kn7tpT#TMN?BR7t5s2Aj+POty(`2K~ zz$>5F>yZ_d$j`*N92w^pWT}?_;)~v_ftcJ?n>!2N8AcZ(FvgNYZLBS0pI}V#@lI_O z>5bPAM)Af0O9;&GHnvPqR2@HfxOQGHHZRENwv*zuqevTaC{ApeQgRtju^~F*Ws=sf z0*zqal73 z*2?}Oq`!-Ftbs^_AXxzbRZlDvI=G%3jb-1wnNfkW?|JU0F~OPUrOxPg&JuZ@L79oX z1lGZ7xIT`v&T>3Cxx5YIMNcTnV${lpDE@%fU)~3VYg{r7K@eUHvH#5x#xW1|!vOI> zC5W3Y?8ig^&T>rZ$I6rGcKy6#$4lR4CPE=NhTj3=Cg9HR30ltBQgil>&$<`ODCEts zIzclqyu@QBp#n-wx|D^&GK#=F{uh9`l2Ad@{`Snck0SH%t4lsPizijXzyE^kVBg>r z%5v6(c^+~uGJf!{>ifpP*A#Y{Ap(MQl6K0iu0aeEgm4_vcMc=h0BYy+MudfxY7XE_ z=}jdonNWZWmM^CB!wXXXp4BN}B9YXkFS^Bx|aUwT7K^^JW+{r)r zUQT{3?Pl?@S*wTb+aJu{8FJ4g%?V;6>|ms4doX@6e;0>QTSii0TADIs{z$y{46lHt zluPfG4A%4)NDM>R#>Y>U23KmLgG-5Sj4lPVC8vo{u%h1H-YjOq!W(2u#l< zHE9w$kuqV)|8-%@SD0D(6=!@*7{QezE#l?h!pFl!+y=8{T7w ztAJu};Htm&%TsFND4Z#>%`m@rG6?Nz=Kl6REuTl?KEh4voq!KbKL}8c&oSQAFqV&5Z6cK9^)hOX-j* z7(4>X%r{4P0gdK>$SUH-zLk%`fe8bD+-4;qJO(=W=iJCh~{LggUjKalFJec!wqYJfj6u^t=$HFIMmhds@)wud@&m-T+$`th-nPGh(4b^r#ZQ zexLeV{0#PxmUn18gQ6Bs#h)kkGDA-$NnU8PpFxP~vzD0`F58%ku?gQQH@=A2s5Yf{ z*}}7D_Azdj)HLeNjYB*n8aq!tRf5`H+oSS6xpDK+Hwoc(JZpc$YmKGW{L9w+Lp{Oi zLS}Y;hGB$g`3`5chhr&?yuIeTd{(!=TcU?EiqAew-57)?*l^9uJ3ANDh+lQPM&zKM zVm5V@0h2FAIJOc2*FHYC9Hid|$U^5|BHSBG&nJINETk`v?brDi?{?f)cDv=NS&G~h z{)cI%C6-S-u$hqnyUVd!C#%=fpgV)SCJvqfgkt{vrpnZ0`!l17vv#Pb$`k<;WluW7 zh6!4Zf^;u`i;vyID2w&7RR(8wXG?11yeUB@z@GmL)po ztleaouIruaXr)0`Fe&7w1Hg@=DweY6=f~G%DYERLRdbIP~`V-RNt|iDL-3J zw^P-LTt)kNW3b)vP05$@6^Em5YJA1T#JR`vnWhkHo|O1k=2uQUIT$&g@1s)M^tntR z86gu}cq(vBdwtv4q;9|en(BA)Uk4kVnS*E#u-LxEkLJ?Qmq3_8v(oBr%qT+X8Z0sc zanqio8C8$Fxr34+s4=@|GR&XfDHm4rTW#YMQFoNL<}lkN!>E#PrBcoB*IA5U zrO4?0i3-!i#vm4$%HQl$Hj12y^f$?I5Ny+~d?#~|aEifVUP=Rn+af!N=+}4Ll7W*bqlf zc-n(o7GT?F!q;gOJjc7P?gDGM%2)C0xf*D@U2*EPShP(DR_ppZW8@Rk5%N0?UUCvGSNB66&^i=JAGM14~J% z_uFdN;E`BbJ|uM<4%T6lULL}vJ>BKYE9Sr)bHnZsO#C6OdTANa<`Pyt?RnW5MtjPk zYyErjU9fwuvrDSO>uV>Bo1P}%F=|bcP!-7oMg)H9Rqep1$zxQuW;%N*l&&BA#HHwH zF+u+oNkK`;FX?+-CmID$m>4On5#2ch3IwmIYXr|_7WK?S=U)f+e_9U)y=rR4m5gh;B9`06Hz!5>$97>suY!zmuiU3VU3%6{=KG5G!+HGsgZX-j(^~Do{js^k3eTC zHlzsL^bi_0zC3I0%9H=fPS7x1v09S~ka6+Rag0gdWZ4kJ8v#v8cYSN$hiJMt;=u;( zfS}j3i*~BR02ute*fZr}HU}bsA+Az54>g#(k)_4)9sYrKkkZMpY^sneHHIxeRF=zR zq$VO`rv)grPeKVo#j2y?2)_hmT?~N#_WUqu@3TblvB<7Y(sM!)#gz5soc(K_kN4;k zcnku{-BW2$H2%cDCHg2875%eYTSk>4WAMTrTb$`0xG zEo<-dZJwd}`XzYS3rx@K4gQA%Rzg=^nwuzzj zH9i3`zg;?^4>Wf<1f$*cx0J>R@-+4L&_XaD35lZ+fMu9>mDQ+L{9F6vt7Hw7xBEJS zeA>ge@voD?cWuwU4cC~x)ToqT;(0Q3j5kc9X^&G@MR?t_&V(niA&Ha{La;U4j$X&L zJg~D1*P!}$Ymm=CrxXTh?sTY{27tsjV<%M(qE`Qzp|TzA=9$TaYWn6u(- z%`ITAQCrUR3=;7hDe(NvwRB3dn!d9N^JYvyBwzE1cpN_LE_RRyy;YdBVK0jSq2Hx{ z@hZx|I3cktRJphPqd$DOLlBenPbQZ4Gl&xFI%dck=SD`JhR3OO{5FhB;6M#v2AYo2 zy-XS=0IQ8s!nP|mpVE6h|IHnZ8t51;jD8o~+mtEe@3|8^u|E^(Xc-RNV-Wp$B94i6 z<&AFR%Zw$`mD3_U8q1wbmCToOd>WbD(>ebl3U#T4OzD#vcXcWN3B%6jTXWqhd z1_kF5UDjGitN-2RfrQXFo^ct*H!Cnh4@4w_5SD&kVpkoCb4I zyf?kALXi*jwiK;gz7#^K@qz2HQ!Jf_*k-rIm&vkl^zy4{4!Uya2yy<%$4FD6a__gCdg zki04uh$9SbogUY=|HPCd6pI`AOqO8NtITBQ}&WtZrWJZfmbt8K!?nU0U4X_wIq!B7?Pg`Fs0Z0+1|a@n#Ea zYI}69yJ!noaz<{rV$8Mgi`C+;1M z5_FyKh#bvr6^KBifv7 z27=^y+^?A*8@JOCA7xr)rD+)%$<6JzriN(7desY`5h;|mOOuR)qisK$;vLW{jL_sO*LS2R#gc(+ z=AVCIXq*Ji?*vHOk2T zm&1cI0z4C)4@e$YR2!c!ghO_PrQ zUC1(VTLD>*bYdwZ2jF-Wsg9L#4|hAjo9VYrU^1D|lAbzewZ9D@&~V|&&NNm$`(Sro zIevzZBIPk5ytm$8l|I=C=4afo3ZZ7!B7@7oG@Dx|HiwKFB9c4rN#p|&NLz8A6FVz{ z0nkj1JzKmOL|bWgsXp%3qgkdOnF>Z*o~8WzJ%49pK`y3E4RoYQ!OWaLgc~U<@2hM8o4KXs z$7ZH@26GCbt*ydrTZ4Oj*S?J?x9!{QM4Xm0AQv>jXegdh2^ytNV`ng(Mt~rHy3#=c zy8SY2SwPu6`%5;{$@)@OkiE*Z+(&PT?5Ahi*yD=ovFpH=1Ipr#TO;)RJ;DU%mH4iG z-mG^v8hWGiGfB#`FyBf}G0Q(X0CjltfabYNZ4J)-Y)8G>o;BTKCI#YQmC77;EiUf_ zJ{+`&`IRthC>_t3b@^6gswZ5vO)SvkIw25ZNRInEBHMf!rz{rdmgd_aNdawrVdwUI z3-|RPXcEG3%p~S6ZDz|b0&;na?2MC4p|hToWFA?`H$j4*-VPCkbxrA8g$D2wm<6aZ z9u|0$e4EoY$}IA(z64JCys7Plf*_ia-)Ui4tX+B@2YrT<+k3#Q2JvC zlFm+Z1m^RUE}xdfv&Hf4*A6R|ITW{Q)6eWr5e44B0%p%NU>LazQtI%^%8L< z+2de3DhdAz_iB1;ekR_0RR<8vmAp4nQ9jeg0kAe_ChhkFIR0IIuhjzXs0f4|w0H0m zW(PW+(a*n~p)#nCYxHU;eJC&_fQ@sg!zIAf%;gV_@qRxoiHDnKME`_pdXp5aL|o7q z*HBC)xjS{+6>wJdS~YurgAwAFeV0DJ&#-&R4ZX~Z6~QXjQqwbXwOU4+KfBUhX*>K0`)^U8YJ;|h^Zy*lic?8s%bC)7Nqs} z-^(6q5cXaCJ0)T{-luudKKX}B$Zjv<32nAV^;D#F8V;WTY^j1q$}_R|7hC&j4_ky_ z7kJ_Cx}#2G0YfVo+%l2xXot!v>#3s;-opM&fJDijo?xW{o;l(2JKgtX#zW^@(u#Vf zzRA%2XoGcKL;ka>qMP3k2m0eM-u6gu8EaWipnK%$EZ27{IO|my~yJzy_ z9o!3sm^`=V=N3lvvn937`a8S6iw(F-C@j@@>-$txRJM_z@1M*GdWVWYs@sg=)*Sr& z1;VrX5y0H7Fp^QED>f{4)yvmR)-EO{d5Tb%m5oB6w?NGt+@G}}S)K+tHr(%%ofk0z zof9mDcPq@*d=BQ5gtPhGL&0yl zH`>`AoMD4j7`G397Cb+|jr4#5=PTy&p}slFs&jc&G5*Z4rmMpjyN z-TZUkbD_*K)Q!jKgIn;$f}5hh%SfSm0d0)rlAUl z)*Sb_8zOOHsMC%V#t=`ZodwG5SpdWS$*umHYXawmxCH?juI_B$MGT!*U8%r&o)Y5t zj*|%o4rK@ZcNyo8$?5MRFM~i{=Mh%FpockrrxqmZd-@&-g@(^#QjtQR;LAZbvLMZ= zX;JfZO&eHZE};&?2F5rz%lYh|hi>^UFc*`;2kq3Z{hn-+r%1<6l8SLsKg)Q%^}yg> z!N^v;@BZQNjS3H`6aJ`Hk4-jJbNgJ|X?M97ym@>;4jgz7-S+%Ewl~d~iME*W>vtb? zpvv~6n8N8>zu6(I!==wy$DiI?O)(;yWJcuN3OOM@hrZD(pnsk1z|qSPWIn8az0(#C z+y8pMj94n*qG}n8T7c&IzSw3B^T#`{x16!d(*4QCOc_9ay_c`< z7JIe;6U!jhD|03a@wo%nm^<0{F#B)Elr*cYjCHh(hK5?>m~oEc8~WW0mn(BKo?qhZSf{F=j^rY|ICNjY4#te-B=GQ8R z464hNDfbME_Txc1ZjsollI-M6Ht*b^W1-4_nT=nc;lsD3H(T{Sn|a%wQZWnTC}t z=0@>iLL`=u^xfs-TUny7kYKxET8UX8fb2mk^p>G9JKd40$Eu4$21!eP>r?!#0&x2i zIx1EQB>%IW!#97jRhryW$xlj^()THUuXw2s4byPal(-0%-<`3KvurP@J5giDnxRB3 z?>k^3Y6BIjK7uQJa!1oDoXL(my{W85i8`B39Xp9Jy5=luc+B{9+_>u~!5SWNWL$-c z|KCS*G=RHsk>`tRofUY-BMhx)?RI&lqA=&kmRcP}mAg_3j=r%F`-RW@oyJmKCo|Hi z|6D^vwb{E0aXqJ3+WkF7n6tiGEVYa&O2ySQ(3e;$zrX~k{P-%Egd$%NCv^3uvAg`Gb$v_#E zzhe5B#M%H)-nHL5{EVh5Nsl8sOGqNVC)nDa?Jp}BlSiHD6aI&cV7>_E=s_dkq^esqmQ zzTlXD*PGnI=aCFt>ok$LOb36@rS)8W75hGHkm>lWBOTgGkGICYve*sbf%$v#hP?b3 zXAft5s1!?wqW)_~=Wg1}A32=9z2j zI-5Ho5+>wzVvuBkKK8PD|BII4wda7pKAL~`fTWjF$X}MF!{@3xIY#j&yN1wE3*Y?a zO`1i?$Wrm6vZACJ>8yoQe)q+>j{Q!)=N^jxdgc*a>2Ppb2Qq_f0T8oipj}+ zFyS{!eh0c!YN(}rr8E>r`K%+{H+0dKKHoe_FW8^7rsT`W%n8~sVJR5=j7DKxMTvWU zx5)WLBHa1LNtut|%5sra!rG7mOdo%fRq_h<*A>ydgo2&@P3uI8dgIfEHAmznuwbnC zSY^Jf<@oBXWBPLjEo^7o!1FuoETtbsprg(}B>FkhP4@@+u}lUs5*FS zv=Fez3dp(ox~qx5YtTcgUc>M{CXb+!ch&UnAHC^KeC{R~<(oW56H4mG_1g!>K&Z?! zz-5X4u6Ew_=HhSgqlv1&E%g4rWL|}dh>`B7yDXh)bf;@BoF|dp=<}^uinzP)V)2Yu z|4IUKt#zn$cbo(h_ShpiI#@qD@ddR1dy{xE8rJ(r?r&Idj!4$P=YTss=ATj7t%+=3 zxVTxnSoQ5io(^gY_cPn?_Q8^uy!fIJp3hDe=u!lUSMsPSWU{dM>)KC!)Y7 zwxvo~mu#6#%Cwe+^v1(-`bv=e9mQQ2Rkaf638`K6pdc5l^7nY2(_0(`OD}dC6J>ckwoU|@SW(Xr9Yd8V5k%3XuHV}u z`8B5LKMONWlWNgVWXS#YNE#uJXMIxn7xG3P=|k2{#EAmDd191OHGo} z;O~Di=Ca0p#_5XN&@e)AG>$oesV07V!_>5v>Z<}$Cmic$&s{Y)%yNBDYb#Nq%kOv{ z(wy%f!hAFPY6nce%#7mFedrI=ID^FV-S1I>L?>wdqgu{bD_7=O`py%iNrH={UV_!< z_?GLmr9viAGbC9!z<2u_aE(mr(#^J}=Y&>9UokE))`>H>d0_9!_<3XXPd{ZfcF8nk z+x(5X2AbosEr`#^cSHf+6?Ogf(#WDz4x!?C+*eV7kNlP%}-{rPYBy&xoh|ks*8j2jVm%efa zzrKI@6`J_j|I2thMjwAYeYQPU=a$U)o}1iO^W!W{l8D9K-#tvn7S!anM1D-Kw#atFujNs{*2({(yj4}kwxl$;(U%YchyfX_K z?Akz_%|LuTyacKEJ#B}ZYqtdPGte=5$#ZIuZ(612KUJD-E^E!4r_Tv^O#RdT$jAql$Z8=p8F?8P%=C^}lZY zC<&?CI#gnMAoXv^B7*38rWa_%^JV4)s(6&D`MXrj5Q!Ch=K1dlYbO}qeJS)1ub$FI z-i-0z^;|;VA}Rn9=($e!NKPKewb`k&*_pw_dn{XGC~ z6~+XOv&Q56w=^+5v~C(f%6c zI!6(O$(Yq9!@H*DpgGLWJ_>C5QVBOqGXZ@7>^6ILM^y+7uK<=ZHmunMKgsGYk#y=^abs=qg2gD&m;2XViX!zi=#n!nN2-Am0h^w7KGx#j+P!UjPWKi9m|NCk+e zNf+yUtw3DrQw7m>Lyh(n_&93LH?jv^>Uqv4MGNE`Hr)70i;Tt^p@haWfNazK6)nJud=@oEnoxycpEN3T29aZgOq&3~5S$I9TI%$v3WFF?goss9*DFD%C|Cu4>h59sj*29LQ47WhW$6fpda>Q4iOmOc2$r8|#|R zj!iZ&4aD;CZ)gc9eYTe0{4oYa+ad?78g>cY%d*OYQrNtF#jUIlf9+o}LPX`azUHjw zcVfrxanM{tKCumUc#(jHp3h=-C!_vdw#D+ra_LO&#gcJa2D}WgqA;klg%XcV7?_pn*R*KC$@|Y{D zjV0gTD=1yLqC>Wm(z8lJ52<^t17{ZeBwjLxou4_X z&P0ZJSC5v567!A*IbeEq)!I&Nt$#G!Qxx8@?;Q9IGv?XqxD2h*6%FM^ELi)ysZ0{X3%x7lH^NN-sl` z^yqpE%yQ5ex6=h&QM=~S1NPV-yq(v^uE$p^p{cBst=5Fg@kDh8^w^4My=Pm#MJh7_ zTA*eAp2u->2(6qtSI05!OM4R&wK4LgTojVXVx8?|om@|&5#!6T2k-IvsI|WH@R@=M%$yDA)FJey);?wJUqjWDg!&mpSvufzmg7vWn7&{vyGQ9c6xDN z`ID4i$cvvbN@Uwt{s-HD3=tQ;6wk3F=HPSOV`T!ne%C&}4L;C?QR$gj%1gxwJ z|9w#SJ&gWQ+3$s9pn7>b^!f+vqVUe8lc{rw*sW&vPVODF7q4^Hy7a|BTp#H!m~qvE z-rVmI3Xu>*d#=kQjX4eLzqaQ2sLO5Vkqpk@!DZoXbXZqO6678RAWIn#14@VRR}Z{T z0+-(GX3b}mI@%#2;RfY|y|QLTrSBf{DlaVS80O>Sb&}r0g0D;wN375@A74PTrEK88 z&%wLxH%)kkR`yqzl(Ir*^_j&&rj^~cc0hu!xO;JP*X3gmil`;v4<)wvPxl|;^9u<# zprsnnQIBe7&Ja|RCVk8Tyem&@lE{Nwk@W7q(k%{9DzjUzV_7%frGE=)*4SaoNB2_dX|h}wYUB>KT4%1$)f*mRWZF3 zWPG5m0+vke3J<5l3MI!lrAs*y>3o-;%Kj$U_$$Ttpqjk^A)TZeq{i>tG0V7Sv@l!b zO*KfGbX4Z885hsQfQg96DpFy|XhqG!|0|IPIuu+3q_Wn1$z8&lSdI>m z&e3mj-)_H~zKx)kTtjXKFxw#lB<&t0Ovd$vtWJ>X6~jO2o2ejG6oEoNlYOr+3qy>Q zdTC%H$1M97&TNtDP8N#Y+u4N2N$>zWyLen_IHx`3TW;nv?x8~5d;NF9w`wHj;`=3< zZ5LGblv7vH85e>bJZ(d{)?Ga5hI{$a12@v=F65-(*chO^G#RQW!iK8U2S&uP#xT%Q z_pLN*?Z`W|OjZ~09jeh%!nJdI+e)6KJ^QZx8|)c9+m?OgV9guX{ZoH@e~?(Pk||3204Sbryzihn+TSa3tS6pmU1wuky(T0=jBM&=pj+k2;GSVhOZ6y<+!jBFiuK6baY zK20gJhgy0GMd;~05Bjym>C1>CwaRj9J|ldX8H7&_rc3RdEXd6q(D~=a z+6pE%2Ykx&6^%=Z;b;C{?b7$a)s_UHyWI^&i;2hibTq=xuV8B}y!iG=_X-Xt5L~|F zrLiAim~6+(7xO6t$P5csjG^7RfO-E{h*;4~1|FsGl>lw|UR_H#N%P%rWtgxrF|=o7 zL?cN94B^H!D&`1p1nqe<%pl2Se;S1I8|Z=Uj*^81GYC=c+s2% zcWu{qrlHtS8_>7+?I=&Tt<3TWPGbNjEOGR02qAl{2dRzqgvP%3C(&=X59_#!{%)zb=7Y@ZBSP3r}79 zObK|OrlaBou+j)bCe)SF@x=dqQ(662;=tAWdNg&;lQO$Cy;oahJ64)#*^Ez0(eK&^ zBorPhK*GBt4QP^tt3;nYgbV9{uwk6aI^1)9#sV{wAs&*S3A~HiqMdunItMe` z!9Ft}^%}{{;zhV!U(NUqpxygwaXy)E&8X4M3%O%blxiRY$hPIX`|8TTKD)ZQvHfg0 zGEI+u+jfgLoBaA{%7bWlY9UeVkNKfIbu5k2AAIur#G=&uh?=SWxH?=}m{140`Zapb zZSvvW{xs6=*r2GBznCqs?NnpTwOE?`5f%*g;CsZNv2*OnlHa`lO|w3i*K;d!?eeh+~4f_JFt5tB0pjv&ty6!3*lO4lsnofhrJKqWpPGHm%5fjElYpwXUu4D2PG zSwLU)TR7-OOk72oeZ;;gtr*iM;!(9TGELIHZ5r-7gj*bu-P7q5z;rbE3n5zgF!Uwt z;_|!l=}v80NRdDACo@~Z*7(blS|GPV4CG93J&GOO?W^yJeJ)+24`I6pJ)n5eEHE@A zOR+rvYeYCCmMI^33<+In71E`|r4=+rvV6DZs9stGZxV!l#ENzKB}Macf&$(%IwwDu z0zC#XyvGIuZkSJ;twI3(ui5x-u%_NHl)=Xs+-GOHms(rpyWO*D5>&7@$bSK)O%n=4 z_&-UcUhBuw%H;g!cIzxxOf#c!y`@x`0!-UX+_Uv5#+1}Ua{_dvT`PrBbM*Xf6Xql_$t-=J40K21Ws7EsX9`DXj^TF>a5D~_ z7tK57zA*+hn{bpT9JZ-wL__fSgaTS-MWMn7_Vw3nj7~PEKr!C;KFeP3FfyAGBK#U9 zqu-^%UsHH$TzMv!r$YWq({;ZfNO%IiS<@|IKMi+m1Mxhroh0OG1v{5rCvEyw6rQAV(ne>b#%Gb2TF?qg`|>u@5?VXQ1W3T8q9uk8Eg zNS+e%Y|A62^*RQJ6tcp^Xc=>OnXIedK$AURMxo}SY{;(rr(m!Pk|R*{GkUVEv|#pG zJS5-d7@tp5baceKWLZA#2D0D9*Lgp-V;2N1N^2i9Iqs=li%E&mw@6mAxYt)r)3rco zGMFBNk}v<`{aVlZj&03s*H0c?#JJ->IQXO(=tMH$C{iq#%p!YN z{yz9)#-SK8K9c22s1xOcRi}wh*}3G~3A_AGSS_g+=!y)Ysj&QRq#!-q6p(+nX^_Gr z0RQ{RAjO1$fgEG8)j#<9j!aCQe9i$nOv=9UGdA0mvjr9s=K6W8*ALZ`TI;}ZTdvQv zqT9vmJ<|NjcSHJI^4JXmH{&S+C4fk;6WZ*(Qn!}4m_*!Qbw@9*z?r6Fe^RQE5$ehkiDDh`nP6nDh}if_(dsMoJ@)0Fnat+IB(E&w{dby7 zvWSFa;RH(cOz7>ti+izTZuo$QMs+1euvy)!6@3()254^b>&>DdcDI7pMT&5yO)$49 z=E>maF3NA}t{w0hnM4a-!(*x|#^{9S+$TMWJluTAbNiE>u5k%SYnCvJ7wCQC?f_@p zD4-W@sMmJ(>Yd}+W`Oo+DgQ38f6HC4)V$#K&lEVkV_6SYiv5HJojf-!@@Qp9=RN+4 z_~;X>35TJf892wa2H7r$Y54bmPVf10uEhMm@@d*!&`gYZy;q?^f@;Jnye7#p>GcuW zSGlO;J7aXC# zK*IDcDBj>KIIEsttl}sZpH;T!g1ZW)M4Iv!;q-mOO8}Slr1fyS?Q$Gbz9;2>S)Ix6E=dIf?mf)sQFEi6LbN~ zMBS!-{(Jq>0(^aE)zrznfbhb~I6ySylD7}~?{19&Rv7pRv*{)D4Z5!=4HN|Hv_}cJ zKu4CrWRdE-cZ(`T-VrnhzNRQ^LHr7!n%J!@o>3e zkM8_n07t$%rqXLp#aVhCF`2U7C0Z_8SY)&>@CgG+M2yyIkhrA?)fe7HYzXu-6}V@t z_Fehdo2U+xX0Gd0xfBPC{23BD|8_z&)TUqV(ZuA z!AkZ#Gj`t6^Fv7HZ&L~dJ*3JUnO%WFsv%O&?083#foA4+sDY)pK2S>uIeUq$EKLw1 zduqDx${XFD(XyK4PmcZd#QLBqy!KB@xS;Iu2h}%+cfc9ghR(fwsA}0;$I7BZPPS zGzE?HKA}h7&R_|1v|F+ASY5_HEzIKsM>3K_c70ng6sW{cX^Wg4Rqk^om5QZIc-IhXI&%zJAUu*%*LCvuq?b;{pqhB-;@`K_Z`w@7mu)QI;eYA3#Ol za&FxRInMq`_p9+5dMOgW@z~#E^i!ligQqsu=m>Rmgc2|Lq+WVYuqVmnHr(e~gfSrP z%OEkkmcRFXil8<7v#Vb^{;v!ZJ+C4xUU%l*OQKW6<>wv0s?LeiR=dnT*qNhPO|x0@!4jeZ7-Y4Kd}APZ-v#_71px znO-#E!!(I|-jwzpU4bIo|KI1sgXmA+w4$z84PckLPkz(2gcnv_QwB?uTz}2QJ{czT zTcuxp+AHOyOwYBPyk16!B?+VVx^$cPV!hD|QF{g-y5czVX&$40cI!n@DU=p-_&8WeWKDj=7zwI{Q6-61?q+C`e~>W#DbAdmaLe4(fvF!%?5ZyQ&S zF7?EKG_F#?s1e%2BHpdPQC$QdmXwTehPKCKBW#8XHSy0ki=>r5igmxwFDGmYd&YzK z`>l7bGvDe^8m=!RUAWuytqxNIjOx3#{|el{zbCUUXs$rQmi=BsF<18gQT4GB;G0|@ z8DRJ<1wSOtJHMBuk<6_c#u54CbH)|WNtF1V>*%g#?C1LBP$Qr*1&~XQt4N5vnKljm zq=guTWSR4QHHUl!ZySTSY`iri=j+YPUO>1~?#5 z(q$OgaSqq#yQh}O1uo^Dxu*&lT-VfYWDRgKI@-6IaqDfQV>N6V*}f9XdmP565uG5u z^oobcavbnP`bc@WPTz@sjIT55r$h(!e)qA%-VjiEI{OR~@JQ|RHloXadYS&SV(TZ) zxEII3k<3oE5ZCzeb$DKTwhaQ4rm3NC^E`9*Uq)VkJ%+AGgQ+3D3LV&y9t`r$Q-)$ejSf8=dOqlm=EaQHmx$<|JZ>_)kWf+T1pWZSw$9H%w0S?|;s!^ry zWp})VkxT?g8*m!+yXcMZykHTww>0-R^*35A z1gAR71x~_7zIRg4#?pXmEFy{x^B3C4ET_*F057vBUWtUlt>*J94 z>F3swZvGuTemv&&M6Vcbz_(X!Z z7u^Qs%O|osN2kg`JXuq0HT;`)`fpZKAA3oT7R%QdmDWj}>(8idVFBMMUlvmC-f*PN zN4mQT;*6;G)nKy!hfPsG>8Xv(RHnt=$0NNZ5^EQ9tYW!wa8ZMa2a3CUz5#3mLz_xWWm25!Bk?!BCpxbsc?kB?&0kJ?l3pWUVi+T zsE+N2*D2)VFnU8d{?5Yr71?~uqJ?aIk*xK>XKIB>V}baH#dnS`kVoa*o=Kp42L{Ee z!pa_TuNS(8sY0GQr*dU2f|aIwgmnmOcnSCu%ZJrm%xVs=F(2ghxk5f}?UVMxq}w~9 z8%yHA2C+^2R?&Kr>%tn|U#ba?FaijT=ZCj4L^cEi+^g}tqH|4i(88{Oq}>Ck3hC7a zjdn4;P(Yra@mU|NQj37S{q$L%R2GNBd;ISMX!#8@Ln*0Qx@*@o2JSA&j_c$X@^7HJ zUSUM4Oiqy|1vb7D8tVq*FYYg>G&yiI3t`>AqKlfxV-HMrno#)YK;~4z9-O{^$wWw= z*YktHuAzabl?&uh$_=fW5D`r5@!(B6jnpAN)lYPnGBKNF?Wti$NW_qFfk)EMY=rvP(cY64fMf~JpNj1);gJf(dn{3D{8jwRQ!guZS3KY-Sx~2f!59%<{ z3i~sQfTwDOcI^D}Gyfh??>dyjb(!$bXdLPJ_r9DqNmU;SPwJ)3^%S^PSEJ6_)>agB z!HtGCQ*5>*W}nr*oWhwj1{6wI&v)_nw>jYqx?|$>p0QoXhL3;rZc0DuSp6Y&v;Y{GTCUO*06+6knaMp{u$=hz!?Z#f8WST5`QLTJNDp$ z8*}#h0X+2SKp~=&(g#-XE|3LoKP?u`0 z!$uk9r`sN${Wayqs)W3+uQJr<;;)(E26|d%6yIyhlwvFEI!Au|Ex=J})8D(|4j^TT z_E}c{9ukl0-Ozlja9Qjpq3*6PwE!iE)wd0?O54<0nM-H6XZbwryRsdlPZ>w}orJRt z7T?9UN#U2L3;TO3e<^^`qIr005Ify!1C5v^q0BPrDbS*_xypn-5r4`GH677$wBquz zXl>GFk7JjR4q_*Fu5WU1h5YAd*neLkGn5fj_Ejx7EVBT>Rw}bJUT{H_)4*m+2{@>k>x3k| z<7l}$j*u*t-?(BNAI5uEaT3Nwy`bC@OHqdt$zEQq0WXba4HFwD1@=7Lc>);h45Bg1 z2cV4{DbNZ8B4yZm)#%tn-*H7Y;~2v%r|_j1aN4_PP-$&=iqY_Y8){gd{?jg8Gk~*l z2#k4-rb}GkQ^>CWpiT#p#&Nyfspk@M+U$4*YgGDNx2Bgy-&*y4DCO?5cixX>Ly69* zq@O!R1#wNfw72f_6*Gs$kwF``?C*RW@=$v@wZZafIb~Fu-@5tk_j54vqLh;`N=1$( z4)+w>`|QpC{s5Ea_=&Neee`TWY+(B=!JdlngY0#3KtN*w#d(YZC2_JeAq8V07vJf8 z{w~JWj|}r`euhPqDTWaF1^D}~bokI!$R2Mm0OCw2Gi&l=5*P(y=!k-xPPF}-a6C2k z_=nDzG@I4PP~G1K!qoeD7yQf`F(*skU$ysl@nx9pEcL=**2E)dmDT<)tmyYrttt8c z*GSPs+kcPlWQ=umx+a}3#Ar_`Cco{mj(W_3`W)*K$n$T&^+u_@kirs;?Mz|!Ax>*a z1^iunhB(TM_|oweE7?ytp=Zj%uKf-x2QT)%lQe+>2{Y#bPxfqbt<*7t*c&MVo#9_D zy^B*uBV`1jn|LtKY81{%q5SKEUUuF+)dt01b0fFG8f6#bdM^ELlnc&xHe!9J1*0G9 z6)g`mh@J?C%#K3j#uyrCb1hLUrGOK@F>08nib#eA1f5;UIU)zK6;D32)PZ*6fcH*r z@9*`0d3qR!xRc+UAp%Xo6py`#wC~b$wXjJ29Xqsk&-=rGiLG$+ZXcCgOTUClT*etf zzO?0n#=ZY9rUC~tM%O-w(I<>92#Hv<)KeiD>IZ3VTKxJNUYSP6D5KonN_WWhw`Hl? z^qrC%Wahi}Z4O2wh?AkcE{`~S>B=2^$2ITNyZqr(snWy~yRTtKI5nq;!rPwwUK>h~Ft-+{23 z8+X&xrCm?dtu$LplvaF5HB++~V7w=j-OfAghW9$jB)1F^0=KC-6`5tI66Oep4D)%4(fa&tUlRNf8QZ29g>$CW12cQZoMG_gvbjE)r%xir1*1kZu7_i1|_JC2N1a~lKPfG|Ux&%AJr5qr)Ar_!-Pcq9LRYO(_)P$R|m zoTtmBV}>I*vFy<;Lbu>5kWDK=;?$=jVyS%}8Yj$`BdU3-NJN5l4x z1b14!;!pRox+{#UrdA7hoQulw3KS}u!kn7p(n@jiKUJh9&Pg4i8rXi=gTE`R=)Lao zcM;l6cBg$!#xuhp`4UjuYGcbgg{?GsRl^s2yFSyY%GGrGvIV?Tr|`TGalV7ozvHNj z58AIE1)^t*{OEX-g>1%%w=@R|vZ>kEy)Se9Y>hMue-ATbr7=fmjByZcuy_9Z)x4%E zY)*VO`_I=p`)`^uns?ULS$4|&J-jv9b^={NTs?(oV_oGF^3HkC3;ZE1MDG~d-pAWH zh-1S^A_Wt#j`D-bj1ZaR%#B1v=eD^rK^XS~8HpS7U}S{aC^=253!6NC488UFO2W`+ zYqIHTroVDiC9)d4&5X13uxBU<-I`WK(P@{7GW|RLxR^^{JaqSqi~&ocNz%OLhv-CB z08}EHA=L<2a$}^3R-Tv0WTq`qt3(dITJCuogX0&7xK8l2!MM52Gl{whq;~(NFMa*3 zs~xMs0_Fhvl$90_`wY0wB>1*)d&Rh55p67|oIK7CvQxq2I3=Pt21AGN z-*B%lla0yFsn%XAlzPG3TSrvZXo0Xd`}P50lp@Z0^qvo7fEi$mSSGV$L+3VezKZStcFl{vZT> z!L)DW)sjd5YAzLeBEb=Uc_TgfYqiX;dAjO#5;1~e&}MmB9qdm?za!$!IyweniN<<_ z6Mx_Rck#~%a?a*PJcskN9znFoy;$}uVjL;$EUwuu-k^wxy2hZDG4WJ3YQOwwy_9Iz z&9pU$4fnS@$plmMpn7HMuEyLV>SO3I1pIw+mh(R9E41ES=q;_q`!jPtlM;U$U=VMU zSX7SlXKKNkf2Mg1hqaq$o|9L3WkYH_b#_k}TM23_(%|nW){<*g8t3se2_!neXA)^K zZ$Ef8FD(%=Mon9QsPN1}&+PAa8+~+ayV*pBbM3*4#4y+}v1_I>tspYEvjyX|f4`mdxkPBWCV%_1TA0Qm4x;f6iAhN+$MM5gzeqq# zP{spaG}~H8DpY<55(DPH6Qk9}D`s)3wmxY#pE7a{?-cbuvuOCcT2DYz<|9qe|igvP%48mqCp>>C}i_|@P7yE zg>|oC?luu!yQf3mXQ{M@jx@2y%y4F-QVMbed_Cu#8*f)lh2-&HxdL062;#H;g`LTg zQMMWf(UpIfJCohNd)KOd&r;bAT?(av*i%prUnU)xKAj1=Bj@krkkHvEai;TeP?pd- z`!iX9EvF#M#ox3Npuuh{HoZ8+(#|kRz|uaWdJl(Wn`Bl)0dF$omA#yo-+dAyci(G( z?0;x>A{Q|9h(D)9HOT^{5yQPUO)zO+rT}7KPs{8 zw(pq_FZGHE)uZTAK#5fGCi1&-OqFwJ1jZ-w^Nh}to?ONx%EMHYOB%qZM};rBn)hjf z_7Ky(!yTEdvEY595r64%+o-Nt!@+`75eygisJc8Aa;kMR(_fLrm)~vP^Jy&<|4XSn zGRD439RlAm!Yei6{kewCy`H}pzYrJq6f4y*(-`iQUsfEddt3pmI(mhR#qbZ{R~+KP)u3a z+0(*Ss447)wh||3t<+p$D|g}2&JG3bfVfhm2xfTqTyyId6{ z>z?i4ot5OB-ODo|Q$tg#KTl>n^hMnP-^q^Jc%Smer($SOn^{8MRx*^qu$p_*qy@21 ztli(mM|bhFNT>!l1F|-+GnG`iZE5MT`$~sz#qn9vgb*74J%i*RY|eZ@9P^C&_AvH| zkTevy=yXLGt}`@LBB8`^gqsv>6uJOIlZ*8>NfG`{6Hc#iJim*t$E;Sj$tJuz=2gt^ z-SlGH246s3>5XR?bB;^|PLYI%s=cO3m^yY{Q}1ReWUvZx3X=@tgpG}Ww{|zPo+hdC z(U5HXUA&hw^ZTSQHhd;&ql7lG_-*92Wrj2Tt0np_15IF+;bq+3U$7+r8ZDxec}RQ= zV1g129V>s`sQct;A9vURLjBf<%k#{X!Cg5)X6{K|U3pJmGb zG;;9pFCU3}TfSIH%RJa}@Sl#T=d-729ICmg#QAPnX6|F6%mY=v4)H5X*%sBsWT20~ zA%>LCuwJ~@<3$>y(T z6d7eQUt6t3{Jv9mKUKNBuYV8CWU*fS4`*f1EGQ&$sXd`O_iH5COv!>f9LW8U8g0_l zm+A0*+%PVGpYz`{)j|%$C=#qAx;}ngXON2-^?VD8ZJE01XdGo*|DisoGBi0+ws-QK zQTFNCaH%B?~c5vpUD#bOo>bZ{z?82 zEVg!e6ff`Z!yYdSMzyyJ=GMH4%p2|~wEG0h%EK^fUu|mFIq>zbU=;7VbQPr3<^m)jsRi~NBYFAu%+Ik>30$SjZ|aGP}Ey&zd|sJYq3WejFn>kyY|8K zl{N@#ju9z?=XBm5XY4f(!21)6sXP}i?kQR)21Na5NeIi zf2uPuv|hd^2>^RbDp^5Cx!3+;_sIuAfYOWFI_DKyV zQ>PcLHZ#C<9`5`ueunv1H9%q(r1EX?=O4S-MRGGkpz!?{j)(Mq1C!aipoCkJCE z+&!8K@i~|46jYe7f|Dk%k~Gm7RczZ3a+f$ULeXcdVn7EW^ClE%bVxRaO`;}f!o0R4 z02H_I}^Oz1kt#t@Jvv9yo^ z&5EHFf5Uh6qb6F59cJ!-KN5l3?;Q})F@hXKg`zzWr~aGBXrf^s>{d-)1jlplI%PF%_Imd-6ULEQ`h-mjmh`3GxHy@pmRBRhwm{NOxdw}i*HGizi^Q`w z|4x?al&>L1sBQLMNA@-+U6XZOeWWtD7P;`OnJ(fH>iw_p&C)2Vm(){OYZyFGVRmPa1 zp(*1oup&iIPw|0bezE7jrOfj+RJzdy-sYn=TYAF7p7dKZ8S+}6d{&;`g}zPuLx ztjES|X#>b{bF}nTg9g@ygaTgM*D-lf!i4l9O{^95!XZhP3!PZbvaa?1J$bc87+2le z^NiW@yYyO7UQ9j}+pnn{ebZ$bt;cZn-jaCu%B(ih&#Vq~-qI^`rz0A=}Opb98zti*sUE?)BfHIJ(uNtWlQnInsruL;sVh+`Zx(JS=-w zv}b&_O%{Ujq`GLAQ?Q zJrXJExJs&1+svhZz#@CgrCuks@Lf|uK-@U6nL&wSW?cL-V-%FEw>R3ozlMW7NL-*z z@!Q;s#W*(a6knG!1|VQd4w{PLWvy|$uKCUraoY?w-7A|M%#P-SWC=ll=`th4xX12r-a)Mz|Uagt~7VCD++{F+Sbe zj#OZ?>GFDiFgo!Fwn=17d&8Z*GT{`w{eCl)RAla|*wKbE5`K_eq?MOz@pyZ$ONdny z!0*z(;FiC#+W-S;Ie9`a84iz4kWBK=)gOFwC)^+O+T_w8B-Vx(PYydw3I>vEL|Rhp zSO`3#iPeuh5)U=W)^_PN?9{DOJ3r!n$tn4I&TOmU#l*%w!sj*p-;jC&k&Utb+E<(B z!4qD-hjSY-RaL#Bms|d%U>F^jDVj1t&9!ZFJ0A%a)HU~%h1RFMXBM*~m@+@(2wL|E zi`=bIiYJo!_``9tnSiZYSRcP>z(`c~QhMG}*oO|a-!+X8?aQP=^xENxG)Ee+zhR$g zsEucF2~>K3uqMiL6TAVxC^>N2dlHn&GE?`wq8%;~k_6KA-Tzg(rq5Yl#6<0hw|Gw@ z6y%uuU48@e>v{8?eItT+MEsC8s>kFw{8a{2^->E5xkVUY8NGExSAtXDE9Up~lXIq% z%s0Dx=RtT2Y3ePzjcwT^#H2!FzYzSLQAVF8gJxw~>ce91?_kvU-oI-f_V$y8NtHK5 zcI+c$*5~=Qqy7|ca2A(i8*d8On#G;Ha~%>tb#rzVL0Cq?#eX-zS)koo7X@_7=E@Q_ zZ(Nc_FCXi~a7*D_q#bxWXiM1_V0D6guD=p&|JqXKTlJ07`kaQI)TW4i5*Z z>ExCz#cYDtTyW#$=SKm3ONm%nSg7g&C}?S~=>e~Be;t{3(K9EX`QI(1$hzZlz3d-5 zy%$V)f(4P9c7tp9>#r1uSY%|8`*3&CeW9p#@PDmjo3|c7WvS=g{D{=I+w5i`Mq zI70O?a@yZvq>8Q;)4Y!b?TU7dgQ&&P>BaN^JAna!?$0Z|jRxc(eKLi8DxHG5~yV06)yYG!MCp>1y<{)Neqg$(&HmWZ(HS-%QEv^VuaI zl7sowOG(n~yp!0-q$132?@(%>v40Cpnv_odiIZ1RoL2Wc*SdH^L>O6t#kl<)k%UF( z4B{Kt5`0nadRu;;Gjh{lR_n+GsJ{(Cn4FW(JZLT3GuG$(YpisD z=X@Pok{8WQO7J^Z@d1$kw6rEnK7OBleh__~8%GED-sA1M5&{d#I%*geVDo(fYUBa^ zm(Tf@(YL?<;s8=|^7RGfZ6%Ge#aaPiw|;W9Tf2_#vb_`ku1VbD6FMCJ%1Ea#-v{v# z+yFrB1#n%_wY|U7rc=BSBrUK&gb#fwsgQqQ9Vx9<(NfiIWcW>feKBXg+IePKE;AP| zThFztz7Qf1Li&FEy?4o;$8a+AO#~|xdem6t|CCDG=Yx}_tv-k!ar24)^}4?gf9$z= z2Ru6(n+i$w#!(u}P-kAlF5=nUbaLn^9tv8+Vmew$i&*~aHMX!09rS7$2`=%#8vQ=i z=ENq`^tqd@i%;djmj`ed2obEh$2hybtNoG`9W!^$`M?!ztw{M_+nVkaGgZlD4^`uY zv>1bvYAUefbBsvuG(;c9#SK;&ui;rIGN7rgIfgp;_*Q=SmZNkFEvp zP>z%$(n#a>N}(c4JcZ)r3kF4OL11xpA_8$=O*jlGKacMw84021&^)O7cGNi`1y#p7ejm}+^h1yXGwdfA@qS7?f%^NH4G0XYJ0B&$Pi?9^Jpz z)EHFuz8cmd62|4M^xf+Ek^ZZVy=AzF)iG>N1E zbb}kcaIn}=%{ln@pCx{mBoI->&fDhp<;{MIaAzwCg?ajLtxaRBh06GZm)mfS2L4Yz z`VL++3d`O(e$At)HvIcyS2U=5A25bpitKgO$j?sCdd$AFPdYp~kA-$dmt)pBb$wQx zLy^b#@YE0+?Rz6tOz+m2+a?`2xUHtj&S-chb5;OjV#|09F8Lco>x zKl|@eM_>u_XQRyAQ_!Y2D#@SC=I9S?Y;Gws_VfQ`wzb{Mu6)cQGMt-sFGk@`Nks1a zev1uLqzFAn|8IS>^axyBcv(F^q&L_FZDAFPF!wDGl2OF>j!cmy;iP+XnXCjW3|f|T zDIRS9m@g&W@KArlq!Mz`bok)<8OUGp)C_kYfXimYE^7HUjAp=B<{Fa839?RS9gfT= z^mlt8gXQC69g;T(Kt~8Vh!B6Em)B658FYp zYcpil<7e#L?SzE~chItG9r={^BCx%BCIj9Pwj{=X zKU93rA;3Xz@I&V<-}SAd1^s*K&FhPmXkTGFu}jok&o9c#ipzGA$Ud7jkl)_NyS^Fd z>{*+6ISfXHgof-9MIu>h3TF1(~PNr}d@c5l{OK#_r{DWX7apIZf%Fpr7^cElLb?%%QZSVhdI>AnZ~8JUj^;o zmEh=2dw3)rZ_PNtTG&%*`_*lOh+z0|p1HWJ@FgRQd z0dG8{degMJ#U1q zYA0ewFU+rPX$fODdbK_$Djgs5A=JDWlJg$8u@Qw3-m8XJyCHo7SSHz$fhBa*p6zmY8OEGC>t;6#e%`!>4FY5Q-lJiH*n+@eO!oxKwwwXCLvZPc^lOcTD2d3m<|6cI9B|E*EavVrm)j0dMq~<_&{q zLd`^i8pmA$biX5`;AAiT+#BEJPG*yoZX7Ixjq&xU?|FD_A?87F$DL*)$2QVcrn4#^ z1fsUFf{^(RtixNi_Wjf8crN=HkrsNiCzW8$Eg|k-Jx%%^T=;ILc}1x=XHG3t_!_Sy zNOULIe`X~a_UM@D81__V`Hj(*09qC$r0yMvh1PfiX0YG7fS2{#Jqc9a6zSqA;HB6L3kn=F3#MBxErSctv@MSsUD-kKKC* z7`}alIsN(uOWcM6P%w&#^(BJy5R|N z*2KI5938?6+nU|K@^-8~7yBQCn`wGg(Vi#?O}y+y(=aud7*XAI(f#>ke4Pu)3{vm&y*J;rA-ChpZIFbOyTzNZ zVQczI8%?b6K6}j`4i8ZGpeeyiB~_~I`x!IoSo&ra>;5wvG$&m81^==Tx85VPUEW~L z8b1}=+6Qh*vJXhVrCn0IXM9#pzrz6^oO^Pt?A6<5QDXl6o*rh+&NvV}MOTw4$I3FX zn&de=d5{%k8vj?-G(7#+S$JL<5hrHKdB|&|0sPX-wviA&w#%L3TOU(5>NU~L4MPIN z0$`7*MqUPw(>2NJ&4x{sEn%w0A?xX^3Gi_rzbDUe$T7-lbbp~(AB)*rtYgR*NbiNijBYL#iZo@*=JESg_xEhg*J5;zDvHlU zV)FB*-tVAxoBVxr-q2RV^EJDUzxR%1Z7Ud~<5x3wd`>0Yd_V*`;}2lNDx3k>zTJ;_ zTn8L_k~A!>9JX~JUh!&(dY0M7G7o=G*|YSN6|7Y)@D82sw7?}V3*p$ZS1E0hZxS?j z-;p{jr|Z1hg0g84*I)jQ$nip32~PSw<;BaI*gh^|OASj{ljrMq6Z?PmJ}Jt-uk;@Z zP?&)?yV00gWWI>fA|by%s~Y1HTXqRhKIk0&`7GCaG&_cbDV4!E1{D34VPNm_HiyJV zNOQeqEEr?SUr%K!Lj3WV3E0W3lPg$eig_2gm@XJ$?sNWcjs8H)?|OwRZWmXKW>8V+ z2=6m8+&Ureh09cc>ZuGgC|hP@k=*X@hQ2|v+60}25km&n9*BoUhcMkeKfEMLEM-r= z2xa~Y$qhvna)aWi2|@FCpqk$Y-+=UKzQ#3b$k53kT4QGcUB^RC{d^w*2k#k?Lx;a1 zV9ZD>M}+~txnkFvA1Hn}hU1K3UEzP0?jfk@>=oq5YD?_Sk3|F8NYqadduBEHd&U8h zRjpQ}-=%+iyQsV&zblhHaF_rOP2E@ao3Z^pB+F!n{2k6a=tzR8B=&4)FFO)5ZZdV6 zlHN=^_4$;dh~?&%f8Sn`>I+5(>8G)ddz*MST_5$#-HwQIfr!8({*@bJm{%Pi#x6Rv z5$LLW&wQTOGhnTeh=&u}W-~(tK&_X$AeXnStXMKzK#+Nr=;G0g#BCa6Gg*Oh{AWdL#2hY-=-{kN5%+N*=*+=GTv#^mgVjKVxWjb>BWs= zr}0rS^CT}ozgb^y*mL=I&Y59^ z15$G1Q-AR3Ed=J-g8p-kT@I@DSwj%S$y1)J!C1RH8{v4F!W%n7w9(#8NDG08&pWSK z`oQz2kqY*cAjxYB7i>Z!9kD!MOtbIcO#W`Zzr(J$%(U_k?f{M$*m0sfInCUw7dnzB zg3Lk1Dx36acDS=2@G?8TZg+bxTS~j5xOjbxoj=*}HfUdVO{G=Z9ZZO?5$L+^z^oVj zpuOR-v{)czl6bE?%O8VcwI?SGpLAhsZ z0@vRX9BmEsUPBe2@zc{D+suv#rONKG>@hBu@f-0kmsnMk5eN;O9Yx%&a!w}76^(0# zQog2^gQQIxx1tF;L<6c|v1S$fdnnm!`Ca=yI}%iNwYzS+8u2Ov$@1#&^zctWHr+*( zh|K#hLWN0!GSV?^jmO&0V8QM(Xt}}kz4;WbzqiKmzrxJ564<*F2E@R--oDld1Ixw2I42EJN)_^#QgQJyIWrle@$EHCGo^tzQUXt!=X-Z4}VJZv$ke#3O0 zt4yTE0;955IUB&1zufpai*i@On%pRLGVt!4;_SK`KZaYOkh+%HV z+dKnbQ^#f`ImZSwY5oLvchCD~Y)Ia!2!~a(CNC?rxrVX{;lZPRG=Q)+ac;WzWatDMG?SEeFnDBMOSi^)p> zF>dkeJ@CtT=Zbp5+M>K%zb`b8?lRH8Yo@Sg@F8bI=la{e42htTu*9}I8Lh;F zQ&fxZ&qu;zn2sKSPoZ3p^V)KZ7C(-y`)~aGuYJl@qkH~7?Pfbg!qW77AMYMr$EGO# z@x8Yfv)^?m8cae0vZmMR8%vMTNd6zZQJX>4R+a}WNW0Lw4LuOqx7xFU=V5#bbx=KGiS{2s29vx;f0ob{l5M4+X7c*DAdLZL^ok| ztmYX@rGtBt)q5+9X$}vvS~SX8laQQ3h(zfZE@E z5;j2&mfzI^lcT-*?tZkpeHp1sP7XIP^+uB9l$@y8_MCG#mhicwL$71pzl%|n%JFQ^ z|JQfv^1p0-%Ngc{^!oc}8#6K@6QTZneWRThlL8fX3Tc{`sXKJfx`b}f=eYAqtbw35 zUl({#ulc*-*?%c`*=#PR$)n@)Y1=N$_pe5LXY8*wG>VZ8`ztt>aB`Xkb>C-Z_&c9E z-wQzE@_N4Nh{dUPRed;NIO5lj63+GkG#XKgD%=M{OV>$Is`|QS6nsQBSbJz17L$}% zlNJT1vhnxR4X?@92c&l6A3bF%WDPu%Fq7x2e$Ma0hXQ9^UP9D0`J4u8>;YRt_?bkK z2Pste_MUH-QgX=IROP=|YibkHo%Kwnc#2E>Yx#~cXiIj@fuj&L-wQSDUD`YhuJ6YO zv=LiFU`z<{ce(Re6oszo_zcBPOmYtUyNL&gle!dNx(xHhNA{Lzxpc9V-~uC!?2c!O znh@D(S%yr@>L#ZGWaz#|NvlGYNlZ}p6YAWwIA>v`3grE}*nGHFA!nxiZ3;p%vyq7op(0B^4}+`=qURHl*lY}7O=#KC5^s@yA{@3Y9X=V%S58?q!c2` zdsVs3PoC^!ne^NMNkF#0mgb9%GL6*}=r}I~d8Vh`*YK~{J|p<+5ua~Axv%rDh&E-5 z6zS100mNG_PJHFzN7&!c9N;J&^IC9-7zVx5?dtH9hzggty}L1e!H#LRn)X}%w8^+< z#_q-F6{Jdhx8H$wWfr1s)jJ=*4>yFdQMBrq%I6%MpM~50rcpbeVP8^KFc4*~o|UWY zaOT)^4%$eRWO!#&1i3LmQ+cPishuFC^j#aiP(nN6N8TzDZ?2=?Q-&1xSI#-1_WO65 zH9*?FRA3{8TB9{S%u`HQKXNzbTU&cAO*nuQ5C;kln~;0nL=_e(&a`@Dx*V{_HL@gD z5jtc5ytS|B3`Q$LHHH39t;%Er3WLJKiAWuY3CuSxw?G z_H?K}8ugy-^eB0P_S!%o*7(aVve{XO6fOf~$-sjl{Z6ioy$ldk7@zpzPmT}MEpnR? zyc*u9%bqd+Wxm?>8S?aJd(r>BJdzX7jk`hYoMcj-p!ExM+PBF-{=0#j*4|zq-3nr8 zDz!Y-fTxH^h<49Sr!R_rT`2vZy+0AK`|E|Cjz5FshR_&-f0&n=w%C>J4IKG9z^#VOd zJFypKQS|a`kn%R!8~%NoUvRNGoXigT-tJoGfMuX7-Pd6X5gX21rO(xKvoOf%6Uv#` z@PkIyshD5Q{0xF`$DSo+Thu$KqQe8{^SkzaYBOnbDiAxu(cxUBmOy6g6O06~&j)Sq zJ;B)Ys<5d?)_Bw?QbK&Jv5*A20JBhuHKRJz5_d4jre8=-TD4v+ZXD&#-*@TTJly?b z_w~2&E-Pt|&xE%Ji`k8l;eL&L=LL2Sf8r7|aSU&sf|vZn;w>?@QGpKG?EaWkNwGl< z8rS0(u(@43f~FbaEbX5fF9xG$9^&U2m9U-E_WYupB`H!)5zRP)zUysdPEk*DSg*E! z7d$SEy|X1nhrU>+ZPGI+mJKXGczENu^bSq&)ie2;s9g$iuxiyer^Vd7?>j%PPyF>) zqP@%8xAOiUF~iYkK^?jNx`jd#Y|++W2KLBb+UEEZ^JC26hS_0t=^&d>RhOCyTvet8 z>@?Diry95z11d3;M6|Y*J zK!GpN6>FJ9zY-0w@b7X|`QH4$tDX3$!l$I6zcUy;Qf%I7I;V&@$62{2-@fKvK#|^) z9=)G0#}sj|p>7CidkUcBZTJzJ4`~XrTehP5`f%0Fd70OLBP2&ti{dY{%!o7`< z4es9ttmS#}^R4N)nlqT(@=XgMX;a0+yvFkrnb5vY=(<&A8%5^?W;&D>M`cHLu^X4w zpRrHVhy(uBAd?X5-;Hamd{J=|X#Sfa4BrLl)%uEpYRWIcP8eMKvrNl(6vTYY4w7rYs}J1seba^%xhp+Biws?k z67CLcfr0;B{l`ip$;@l@;dL#98LJr|k1x3hQ_}V@v~Bf(a{JD5mY6MU7S)z9-q@a# z9UJ2i$+S6mQp{k~c>kq(KGV|m|BWDlv43Y|<(({t$#XBxOTqQ`8@`LrlHPkXo6S4n z)o1;rM^e>OU#EljUmUn;l?W{3xhGy48opY`*X6kC{ZRRR!c9^_7CXHsApb^&XojR2 zJ-%vGfh@xGuL;@q!5d&hnfcUhx)d$XBSvRf2o5)G$lnv`o9T=^8gRe>UE>8espo&A z%5yS(?`{Dh)^Ts3z3*Zt(E{bk&2)WBsM6mboGhmEQ6h^GxFYZl5`-jkWkxi}N{P!V ziIIUtcprnq%6ZDiag9Y{tEJlB&W5ZTy>hJ$bXv!+(s+BZxDV*rKC0`!&miI{jylWx zc=yGAa~S;FoQ^V1!5#PkBfE zVy%6OQWLQU{(ZPhzu5=LKmPl4CmGvYGIMFjJ4M7(RtE0J{*1&EC;|MoY@CXHq-PUB z?Nw{2SN7&#HA1rdEsX4!m&P9J=$x}O!y~~X%)AzroY34mvWzMQMUbPXnfOHflHUk# zl|S(9srTwy(nq_MBRIl{PlbP&sFI0RumN64#uZW+3REu4OMGeWHa=$bg}3{10uDHv z*#(l*^oSdUSyj&X*qOgs0}#p>h;0P`h!+pcEx6(>BCi}EYQq!Y>w5~1p>_MF$UOcI zp*;3R{D_!OyXtQK7^7LbF@OInjhcqlhPwu!r`hn~prX2;xv>vBhSKyV`$>l2k)+wf z`X!TiR~3Ez?FPL4y<0_DlSiaIv)9poo@~V5i9H?Q(JbRcC?C#Q=~Eh}I<3hhA`CRP zv>QH$4P`-fX9;^JvSxPVm|sTYHG#OBx+p;IT7Y8@0GhFQ|L(3;nCkJAZ2WDu^1Snr z=&65KTeugfqCikizpvGz*Nsd+@f1oHJb4(xn zmQgh^b-(HwwBE#=;r_4j`L4H@;le7({q&^^S;C*Gy%;3_D{hV$(fOyD{l+rSn&8SC z{~2NVE~5xO8Xh)fv%C6HGMyh;5w~zNPuuoS*SbZ}Hur%~`rR0IM+e<3g~V5`7O??V zqH@tLjPYoAZ^*>>Y|o6nqVyK<(G!-8qHr&wyO|ESg-w3wXZvs`bhi-U^Fu5}8E^P7 z!-|Ultr(0M3!*jd^-vLb!_cm8ccCaDW%9rXdvZZojZ+k>mE?Eyd zJlEXns;Gc^g@nxWgg73&_x|FU*e~rKnv&+bsaS`=DDnL_Q#{idcS?K4zqd;e2@=JU zIwiZ6w6Wvh?im_A<<(AfoqU*gwCYELr~XD8KAx(&z^XP8j5eK={6tP9{qr`3-yZnz z9st5fjY8G@on7N$J;%$u6;RSM7j|r>QRnsVbZbFu1Xf4#rYQcqa!Sxp5n7jyl-qmj zLUo*NDOuirOdidV4F0f(c3d#`45}KaXb+Qg56)WVfpt6v!m^E*4n8%@&T;(eIpg0Y z>Gu{1{kzx%L*5x`Z(_S%;wKB~QG&D;R3`+?jstC@Set$}izsmSzJ94+n8bUx%RTi3 zJQ82rd(FLjFdAYQoUEN*L!raXhE3CiD7+uO#+ zcZ4!Dmy@ zfvCSU`Gi4zCU(_x>Q)JEspF+HUK6aX6)GSA%HzfT(~1!^ci30}=RF-xT*MmFk>mFs z0nYu_5ze3AhucWB^ac5Q+jrKTl2uH81$WO_Z=KvUGN0$3e+J?c#fe?V*`9q z;nsa`&m5$Y2>weKd*`b>-0bUYmL!nuDXFgC-p}1CP6WHdVewt=Bn)xv-JPPxK)LBe ze3q^CpWZgHC&N!v_p^4A9v>;qC$wdu3#SV(#Ynzl)kDi7+9X!Mbw{c_QSPr%=$cP| z6vThAJeiO?Dgo^O9T|Pim+V`&=&?^^!{5L1##<}Vw3m&|It>|j?5v5k`D%Tw`-E!$ zy$K|Y>|TCjvbMf@lS%t;jlA|Wmfp89tS(EJW9}(o>nCF1zMz3|b3WJazP+aavQHNI z6}{2L{yhRmEeMB2qZ-vWU(@r>RL^p8ONa6IZ{_j}@q9Nc7cA0I+Y=EhW^8k1W4a~% zceZa$uXq5bZ3g)>NYT>|(c%3}e)~Df#K+PNW#+Tj+V6As^Wlvjfbm|BagX3{9-$!i zq$^JrPm07d@bOkw1)mNR{88N2krzu1#LOLu7NvZg~_ohpfrUL?8fn zb2HVeb>eo@%TM7wxg}yhkR$40;&3)>7k7WwLte6k8^~)vjjAkE7g_`a*teS4Ucp^q z#-L)t$L8yBh*Y=U?b9lhpn}HvZ=&72xA~zBy5{2JrD)=O-s#OXLkX4=VRuUo#EBZE zqT-TTf6Z#6GWr7}y-rX^B;vc5WU_Zog482m**Xt3@2Xz-zGrigdObPL~ zH`vLLst-`{^B4;lG91&=bC0wdi*KktiUVLDsL0We7H{c9%tflVAnp1J8EwXL{q<;a zM<`(~-2Wvvgcp=YIwPMAxcAc#vj1t~Guxmeg{}BG-f%Y-Ja5k~kt2>k2?7nfmx030 z(JJ=bXE=>{m-d28bIiWm;C;1nDbiJH@8!CdB*2JE5<4s1L{_H!JfllDkoy!2x7!s` z;y^%wGC#GZ`;pi%t5V@jU4C?hJp=5~{miFr^r;28zH0$P)){Z{Pe!2rgE6Ey+J7}h z#u}A~T13<3Zm2mFhXetX679BIHfUqbt2Ue6q2lkFG;S+3km{K+?x|xTGWvWv|D6MD z0VX!EEg)%PmdAdv8eaVg9*>$kAhU(nydr`T+0a1ll4GB97BA9CS)@jVacX;QB%8dM=9ru)^mBY>-TVIl+M>&@8izbGR(^JeGFv5!~$fi zUn@B-r3|^79Jt?e{PM2X*t+=A$uV~1;f-=@SWssp#t&2CIq{d8!M^{Sx8Y5t!O?}N z)9o$_$oRDK$9Hzj%+0e_fx5+4pbpq&D1MfbdlrJLy~ghMqwBmTF~Hg9hlKO{-S}x( zK*v$}C|pKUWY#8uNnfE$p9DZ6H|<~R2hM_mOcN$r8N^NU4e=^_U4pG+-giIOR%`{q zsn`lFe3r4HY>ItLz^Wqo^C-L*IMY=&a*GjAbKmGgU4xQzgOqpwscfDtqw5U1R6`&Z zlhC52L*d{XJl{N#GbuKFPHJ`0o%z6H-!(*(`z!7UKr@mGEi+4<#f_2JeqO(#*8T<5xrV`8Y6 z^FRqic{o+6a7%CQqqYBxsK6xrG3)y=_m)ncOH8Xjb-yc%RG8QFeKHb5AA?iy=Uy)@ zX;&($?VLDuRtF*xuF*MI&`$Sd5>Ip!l@y-=R}F0QdpqR^ZXz>}AJ@-HYkRooMnE5v1Xw1-I~%Ua zTDKC^DpXJ4A}P@rO<7r8{5|Rb#cRV_$Rrb-{RbZTP{)rtn7I9iUfQlveFD-fZR<~& zPJ6_2y;G;3`t+9Tvsl?z+PoSz?OUhX`ekQ!+BLM}Y@QeM%#y!k9d`q50qDl(Ri?D2 z!@blz2yXqB{PgPnqP72eewSOTdq-KvS1R?Oc_jmy^-aUC3g{;0(mPxD;ofRwW(qx+G54jOA=)+{XKy0$3fFSU2EE zznXs<3mcIy0H$`E9i7Z-9x_sq<(o7LhUjwqj6D36M2W7rfAutQT%H z{8G81{XY%m?IxAYh8HBNHny_tMz<4bGZ6+ zCZU%W4t3mYrmaZX$ z@aNZFHZPOLDr~hkQ)Pgv!DaVmzQn1tiUpYuvGue=Vc&JNhh~^*W%I`OawR3}QIj#N zfWFW1*>EKCmmKDa>RNQ{frSt)@8XuR+W4sh(^wlA=716q zw_0@4YLd4`NBrx<`O6HZM-0~YVw?1tB|CkH4r`O!Tcwn@oD<`+P!~A0Z}62Jmzwty z+`|7llZ&=p!07`>1cWF>ZhE5U3pZl4o}?XUYJ~v}!2O!3oVEC#xzm=4 zuv&bC!&#b(p+|;zw4rgb=&&e|6MG&b11omyoR{a0kT8+uuQatY z0@eR)&row95#x^>F`SGICR~2W_Lb!D=R|}Lv*j~dA`~oHtTqUUgT~3oc#Raa6`am= z+0IulcTXSFdSqoK)?1TKu=w(#>3~gQ$8$`Q_hl*b`J{$M?a;*?XUAT`mZR&@Q8vQy z5lW5Iw35)r5bpI!lGTfV3xWGDPvvKaGsY6;=JqXrD_LVb{6cv5yG}!YhYS*b5!z!< zPhp<<{clNOxyMjpSrW-JWctp zOe2}N@@U!+-iytohR@}^w`Ojd)4Q>(TWKy=Z+#0q30`H%Zw)d;)37n)il1PTX9)kk_ksNJeNfVZH=iVxd7M|CnWFP+l(^Yx zYL8~Mm;!8Vs~RgZ3lA(mBb}7CHjfVt**Wzv3!%`glTT|D7hux1l{Ds!8*lp<_FPrx zKczA3B!#@U_b*13Y=>AEWTgY*$0A#lrIhG*6Oq(uqgdiS|8DKHRzDd_95ct#06?+@ zMf^S)v$K95;9GUNu_OGGoyla;RECni)nG*+5-zJnKi%^pbVy*%&M*dgEyXx%9S}Ds zYP^Nj`2HB^jW%*BbMVg+=B`Mp@b!P})Gbr^SuaLRZ^Xk29YeG-5o)X_*k-f*coElE zvHd+U>}gaI_ks4otAHu)lQ$2C#He}9fDRf=Du!kXqspYZe@k~yI1cFNhXXb}c#a_V z$DeO9mB)FHV~M|SzVibfELWZQoyjO5in~d|4XN5!=qnFp1k*x=!;|EGvXiP+sdluP zBv0leOd+78d^4GEJpk;N*bdL<*9oFe2YbRjX3u*scMd@rS{uLM} zd4@D2Mwj2+owNE1qGID&I?k_YO^=BG(@U|G5+=iI+ye&r48mLb7HG!7gG%`S#QwNi zDVqa3aum!R5;f^rw6;n4aPN`u6};8(|G=ZVdW+>F)V{oN+;^vmwB@X{=6^ z!l#3^A;1|BUk>;)y)3GC`iOHK#1r_%zLj(^h=24vwy7n#t8d7*NN59gmE-;{`wg7$ zR3m(>aNj>&^s>6pq=~HC;strVoxjyB-^eim(|c#39RB#tWxfX>_=YqeTqAj zb~p<;DgkD)6UWY;4`o!h9(^S2M-Q@}`DBVPc{pBT{ilgDyUbrLmomOH{HaSmwcNf8 z<4e7%p0_k5&@!Z3wQC&4dvM6W%+ESk&*4MQ_{q}Ibl$&yx7nTv^AO*SsFv`TqtqZB zw!%Uh?vuHggXK)O_Ty7*d_r_t^gG|1_<1k(Gdmc3mwIp+L4BDkW&zA9&H2c(>jd*O zzQRHbn4sGQqa20LX}5?BIs+$ZZWkf!ST{t#G|)Nf9P`l6z->Dx5Ltj2e0#g&t4+d zhku&s_81vWCR3~Pp3;++G{?Q8(m9P(Bf7nA{!Agsj?6Qp51vC2f9i?GS>N9p?K%o$ zp+g^(r?IYb@N7JPzSlnIJX#d;h`sDQn3ThNVV2cNuvVrll<&dzyxIm2R-BHx4B3^h zCFiKJqaIk!>2{pvX%A=3muqvB*`T$hY|CLFLi>m_`74qwpja5$vVM1Z+6slua^nVt znC;e=ZMMA)B8-yfDx#f(s{=nt10mj41D5yyOf)3+!ch8qXCd$Qf-W2#&lneGmb3RS z$7CHBK<8YNkN5RQnZpsR(F@+c%k2|?W(sLWbbba@kN~2X?$4l%H zaLY0UuZ~7}tKv!~4uOo&Abbfv()>wIqPxDnQ#*RjhC_rL=iRw*Zz404&aKKfcKmE5f zkA@@kq`rn=w-#rjWZ5OL2oh_RFU%pEuDtO`sY|+80D}KaP9%c)2puNe%&`2%i(*OW zIoF?cbSMJJGmLmZ8ZsoWzw2ec+zP2EU;e$`wm~oJw|Jzn+t+EsQ9mejq!-jg)sTyL zbZsNRPp_#2{W)N_6#jmBb#W~jkFYw;do`z>=tWno(0!D-QMAbno94Xp(W|~d>+=2^ zDD0mpA7YV|9{zN-2Prxx>z#eG(V1i3qqTKU#yGZEmE~24N;5~YgsaqL-e;rey9{LZ z{wo&Y<~tz}ImvkLvF)R`#WnwNaS)}9tDS7ftvCoCocnpLBUUrX=1|$YYa2IU zdxt%ld}!-qjv1pQGyKZWSi}W|5eb@8ER7LZdg5MTMV~3BskH6OD{jgD`T_QudF6^c zYxJFK{kfg)_H4n69XpS_qH2|EJ|S@hpqkXvPcxk})3f7|hrHr{?`7g?B*uLovD%}i zy5kh(z5&@&ivQZ21Xb3o+*fNvvgdD5_hNcU@tJ zF}h~1#`o$w@P*yc&2X!$E+;Zq)Y~(|3^odX&FuDjiEXgFk^HNqmf@WySw6g?7N2f6 zRf6MzHDPok#%=zOT~6=W!RzrAq#GZq&;RRD)?4P7i~sdz>#$KO`C4qlTpOv<=f z!^mmPbwAnNo&i2eIo2W!&GW_(t50LhA4z4A_xV|i`WwK!?!>lMF)#T@uMwE_h~po}~@?GS!<$_*kZd3rsR;7YZ znO0~mbg8u_Bj%ae*uYJAz>$durQpgXJ$82qqm(t4Vgi5JI-a8i@BgB?v!Yyl5>IB- z?*Z(6P}YHK3XXSxG4rdI{*?M2x#^7p7ZRHR&k<-EmISEUkW3Dedx~5kMwQTS&YgWoH-BC8onC6qkC(*S zb|(;Hzb;DoDvBY2$mR5~4n<3)uggeUcg+;{uU6h135g{iPxE`VLxA>Iuump@tSJH| zt6>l4Kqb#blw!>eH)?O$Yn*qs9fB=I(_SB6Y38NVb7##1PS0zO6Q#vqE7^}B4B*UZ zU#AwNq;>>}N6pUcTvA@Wt@=ue`EHEsc={acIBFiuwHE7-Qfz;<*i)K(3M;>GJ=OPAOlO)7z5aD}Um#T`GGyVJ zwhog|VKGZ3NEujx?WqA_F0%z+DVjf{hoC3}=X-R@Berfr0IfN#kc}$E8GKS^KEL7L zUS7571Jm()rBy=DfQOwJT5b$E436R;q%4#%>8vVWXiK3=cIrtYxVXEku`0h8OlrFi z9EygHjsGe7vSEa|1;DtJ?Pv7$X?rwXuU})%`S z?R=IXQPNxo*yzO$tB|Ta2urYE!^r2(3?P*(WWY`g@4ar0ZmB>pnCJ;`#CH=J-yJGl zYSU*q=RG1!Q_OorXS$XG$+Y;l0kB(Xt%kUK%+7_4oc@{Uv!(2HUEK-jz1iX@?+m2=iGX)juTIK!BZ%7>f;e&ebzkh( z>dTTiR}=I1dMgQv;JJiEozIXpy+y^OP(aRr-?G{G;~b(dn$i=vX3Ex26lC&R@Q`A3 zO3MJnbW&&u`ZTMxEK}Rf4}~TBC_x!9^|fSEFsiP`bEjzfWsHJC5VY&w)5`YKcPX5NgQ{z(4_Cig zx0W&g-p0{LckfJbj0Y|3PZO&3P9eSQVEnP~6(HW|uETgBX{`#)WJZML$%5FAICaZ#}jS5Y+;@H`fQ&_QAT;k#A1P>KPkEF zg#FNxKY9-bnhpO#I3Iv5Jl;>XRV=KH_9-G_u6JMR#eJW(jWdlrrl-~eMOyk$(#PJ~dT1t!*Ba{Pc0{Pk!x z2OGW;>fLX*sA;t6v>t}8>!>BRBAy|y_%bT`0pfJT_pJxP6T%E^HZ#=KtTcOLo^$To zjv!r)pK>UMwuYr{N~rbLw2 z;iP(Qk-)Z$A^RIao)bO`vG|4Wy=64kJXSc?(a_i~=q|SEHGSFiD2JiijCANuwqAn0 zF{N=3d#9u<%X&uPDTuYUI6@;NBR=IlhWet<=%{%d~;bn2Y@DS}vqK~)6| zm>*mEhlivZVjnE~(^UWAVtgHly|*@ylEbT3`JwRq)?74yFvO;pTzKDe^g9EtHWv@Q z#8bDiG_GM`wUq+0O$-xZ+7ZF)8O6@z)odu_-`ja5QNUOth1OkN>!V*!*j(+w3g=aW zh)=;(yB}Gs%yfUxyDc-$v|QZ@GAfaZ(B&l6V_tsGbX5v)v~A~nMOw01zqgrhiMYw` z^u)LgKwKgI>AY*vl$Q2Iv7|O5 zLe;A=L!^lCnRqp-ZOQ_kb1Uk)j!YngU&B_ev>BpNx|i?0;KqFrq2hH=f7_wNEnoG( z4CV$@`bp9)>F9WaqM?W#98GHWF2=JJZA-#yZE3ejFT$&n# zoICx{nN0i+Jr<>AWm#}aT01vf+wXplMw8A45N0(suC2{n!z1wI4xsnoDvZpnYPl3A%Z*%wiCo!w2B|mJCOMo;BP_Mtx2%eg&*L zQOtZBL_94tR~#&MQ#z%wh6B{t(3e0(9}7(lf`Bk+B4*qX;aQ69d2{i{l0!_)$y!dw zM3z$V3GEFY_)oE}9}R$B-PkSG!@USuyK+CnP!=W!ocl9+ohN-T;c(`n>Q1eHEUnJT z!ZR#OU~L4J>%STjB|5c=NpT<}malb-dHNj=kIC%ZYn3w0?L)xvV=E4i?Jazy!HIwo z0?;u%Ih-3=ElKd&WvI2#X}}nAenxlgG<;3E@&y2^Q)nh#Fl{+1m+YpM2Y_~G38~3! zOzEs|u4CdW_%3@A8QzOuU!CmPtLmAb9tsWO0~ouGUsf!lmOsb(evO&_Q|T?(YRSFl zApeQeK*D?s$-z8Ce@Ix`_-(TPJbudkkUwT56d7lHdt5b*B7VSKtGr!2LGOT8A)^r3j{I z%*>XNPYw8^u)M5PT7^KY=P!w1(}knal!vSd_0}h~;~dTzhVEj)e=XzisFsh<0GhRx ziO+ZuW^+Et7PlIO_h4cR0>5zZdBtmxYMv5h$N_!d&k_V2Ubgwto+t!Ws}Zg@k~$Bq z5hL9|=~XgR>Bo-W;c&ENDUzIdunMO1a0ICrcMVG=?Tph_^v>$9Ya4{U2)K0sZN>aR z<}JUFFe2Bz%HH9v=Z!3-ymDn`hh!FvqBzLD7jfAy+* zr3_ztM5gIW5DUqy{8Xa~=Ex_^l{UwJxu$Eq)9W)g{zaZNBWKm>&`lsM_?1zXuT5369U-8#x+L4KYO{Izk1Cx~%QxnC!yHtxF$jpbR9} zU>lrRPg}v~Y;X7IKlR6#Ph?)vRG(4HaK9++t?8eUaL_cesXD3h3%@2W!U!i8?tr%r zJjO721n~;SbdUHO%PuDCGw~b7^m`zsU1n*;RC#q}Ha*kvC+`4fdid62+;O()@SkZM z^D;I$rDDS;I(tiD=G-=o=QbRml1-Cg8W!INn$*@EmY2L9zh`>1W+iGIMl_#!9nr^J zM=ap<4A(tbfF>Rz7+2BLTG(cyq}>9>Q0s4rkOvG%=hX)^9XzJd9woc6jA;>v0lzkZi>6`&L?!{_ak*-S8;oXb$@>upJJ*+7hGAs zAk`XwX${SUokU5o!w+(O-oIplHp2e=B~E*zkWk8We4|1~>Y&$8Ze(~j%(zb`xzg9S zZDPxbM%ao1Eh+(wSxc67QmHmga!R11pqs7RA=2ner?L61RbiCtcVtQnke@KW`_$BP zkBKh~o(o)HV)djv4vj`pRQxaW$o~HO|lFq*t}8$0g(U9<|kU?FE~`C^#uJREq-h7C@|J$XWo+fL<~GkOa0kDwlPUaCfd(8`(e6_!^%c9qbjuWBj;RRCsCN z@DZ6dW|)pv`6G=`9y1l*$kLVVXIw!_G1)qq$6zdR;aG=$O-Ms4Ei}Y-_wK#k0m-@H z{;Wpd5WIC;NJ@_%Vl0#tlhoF;TDi2?4KO6kxHR32)jxOSmN_4fs0SI2=`oY4FKQQf zR;se&PWrkhHsrs1WzaW3JK)opq3`ux(|BEA8ruh6f3Tr2E}doT%#s6GIBaD}ORLal--|v2GcUM*q?~fx`h%vAeH4my*V0y!m zqPd}Ke~SKrzuIOkmf8~QFRM$)O~lu0&^|j+xjX;HUuY^WuPt$}UH921YFGwyamniM z06tfcE2K-kRFHR1697$~jgyHO`Ud;?q;YTZ+yn zhhvGG=c=`44dB1;Y5`QhBr8~zu6{pslu&v*bKM<`xwgc=TaGY1zMI~6p%PwUuQxq4 zbFr{FOwr`MJ4>@G7GFR>?X3Hiz}mnH+or*u(9SJ^D_q96 zX0A5c&WR_H!FIj5)cF9m(n31Lb-Gkq(j3>_Qo z;mz-PZhQEIP&OjFMuSo^z}lJOcm`PEu}3q6xm$*NlebFrh%C8GdyIErf22fZUVNXx z0_&~VyEd_tLV-6L;C~0PajRTujL5{-x$P?Dku$3i-v~Kgsqa$qjO=KycXU=iKlK%B zDyPU>%YkleZYo36b#4>G_=r&>-7azdeYtY^b?)8%9BkQ8he7!F6GYN0I^82^RK@iRgR7L(q44ji&4@LS1{tW}TIkbLm^g6;@4;xqv}GEyNza>oa@>gT`$r zmq*p~ZJR#P6IE>Q)z6Xo!up6zbWT;ti*l7b;UDN88#00{8UiGgB9T*IIhj>!C~B0P zdqU}s0aQMm$|%|;Y8%=C#%yJvw(o}sC39lr-ICUzryo@arv}J@6|yOUjnRKP!h7_k z95Puf&xcCVv;zA+LYZbHA< zeD*0uuDS_Zh(POsZNv=D2=jVv^c)+WBhgjKnBLhrrGj~SsXoWq>Fa;u>Ys>Vs{q3N z`~gnJ=v?apkV`ngah)fodi=+RM?h@YIwI#L`$Ir zY6XE~UD@}2e@d65Pwm`VuQ3>nwrh{f@Th%s=~o@AY@5 zCnjWd1$1H0M5*A-8(`mwK-W<_)yZCK9Z#gFPDDs!!LX@D)`M^?eK<^n-tm^~ttz0*8gW%|BYG(K5e(_()$lz-pDF z*7W_FmQ{$_Jraq(_cv)Q0vAuZRH(K>!y#gOM3ZeeBPJqGfqQ!ulFnHLnl9v)hpa?N z5@>OT{~Z6?^Df0$RiM6?-epzN82~)5{N?*7rS;HWSO-}~i?|~P$X=VGYQG14~tER#|%mB)Wv->GkwBkwI@EM zM_jdcueK+@K`AYf_n33cCv)Up*xihFmO|s1E`T~;)eyUS6I{l#mkhZ-uR3g9Ul`?} z{!|j=J-AqNh~ZEwF}(K1KLg4(2274NqDND|%{13kr-k`Qs12I(T;nR)TjA-kwm0r9 zlavvYp6MHEU&sd+{-=EI_gY(Uu!Cq+=m_UDwF+>8N>ham16B3}h`Zi=1$2uzpy6>| zgDTi%P2Gtt?#QD;s3-555Z`SH4lcY+(`3Zxu)B7s{}C3!S`9G?PbKH!z1YGcm<`I1 zWUWe?C&a_~@!~Ihrp5Op&N2}^)>pa$in^3VN+e{}-y1%BpU3D&0tCe+a6y0OXO<2U z)#F=1FVYPHMH&+Cf0xU;7E=L~_$SefDzIw?XjChgX5*mtGSjbkfJ{IHcRm(i;FIgZNvC1xEJvdv_~*5)KP5f#8%KU3a;h4a|{q_Asn zev|m;-vE&oP_y~HNspf1yG0+xl~b|MI_kq%5&v(Ez%_{Qrc|Q4vl+AZp<7*4i+t4_|%1~`^;4L+m zTG~p@c8*t9wn?(C*tO_h22=0QTu8)T`>o#KCkNp(p-N|QHQ%)u**c@Ih^5sU{g`-V zUA{(dp*??G(>`Cr>_9ar*f%X@sG2>{-2FZ!(-oAmXDFaLUnVMErcNPE-$%ZmfuXK&-UWb`;WE7#hG7fG!<55P86qq{Uw+*yQxmf?)K^&m&q0RbU zepfo;yCYN5tajbxrT8|u3`|`0rJ>OCL=CLLN*HVr?7D3P2}vc`l-LPz{00b zD|lT(3Lko*enu;3Uxy+8XVJ3(EbPRKmhfwn9ZMpkw>7~_y}#o={!lUL>hj*cu;uPG zKuqPCms6TCEMfh)n~qFDz3jsxDv6AooNi@0K z0U;iX#>r#IUmT9Ot#zA3yJwnWta@7ESIsncnqib&N3!1;OypZt`!cjvjV0f!?`2a} z-KCT9e9t-3#s~h3P*Z1vSVz;s-AxClba7(&SQ@(e=Zxjcta!xJLLJ&B+gF!2jiP}B zBIs3ObCx|i!?E3MKl+z;^4j?PQ>x^9xf8MJ`(&#$zj}gSdOg3=_ zbj^)s1+)|ia1_z&w(2K2-?6ptj5t=ZNt5sb7R_=FOMNY4o1vGT&#+L*o-?2MWF$WZ za>k5g=1u_b#dii?B3)TiUrM)FzWf#s%xIHA5>*a>Ov%w=jFYHe&%&)6K0G!t&ZbDXnU$FFvFi6Fvg0)jlP@Zf{DIL$OV5{nbrJM;~-9stSgV zp^#^2L7<6fHpeVSD{GrV?7!|-px*97W>LnpPz z=Q_v!nQD|8=V&um>j)&^sPp`kBymw+^Idoa-mA}pNc+zSx-?AcO#HAf-np@dt3(cD z6#|XB2OU!s@U|xCO_xAjNa=jm!}DaRc%Gt`%;f$VW?$h%(mSWt$|p?EDly_MsEHQu zo*5hY?`mVvXd)~OA7)lHI=EqCSTTR+|7>!K#!wkLScK>oD&}R*k<=Fy6~u4Hd?Adf zKH!piV4bl!78@QFhfP~W(wLk&&-xpQlSHed)EURCL^5gHN` zX~qFFmUHAC{dg7%JlE}5obA5#?W+tw!qDs$Eu0XhY;&eHE=MB!u4maz%!M|s9w)p? z*2vnjF|L=f>fC|`W7s%WMHa05XO@tJ17&U+!tHM0N;FuZ7 zC%1$~Su3+J;Q+kGiLWTx(DicUAP3TyF)zcX`MwSsn7WG-_@bBMop>+3#^~XGh>O@_ z(bh8e?BEdKShWNe-{_QSB)!id@wE!7`lbiAG=TG(o^J)R%M4p<8CQr&Z3X!KesaEm zf5s$f;25~-&m`HI5z@tx?46|?nN9an#_*I1C~Im4X&01*%}6GqVS#L_b$Ueq#H_=NYXzjU+y=4gh*BI#oq6! zkGkWGAl}QLe0cM?*&f={ygf7H`HVV=&cW-YFSO}}kU}GH+9_^};{Hk{7*aKL4 zXd@tZ)+15BDEFv@x1I}}3ZeGPXzsOU6bM!GoSN6~$(T6Z6i3giRA`uD0H52*vN1cK z!y3*IBi^L{1^_ZPyOudBj`HQZP8?I*k+&Mr5KO+t9is@vfGO@|ZxM7!VUPx-WeyQ0 z&>0BSs!he~8wKETa$R06>Gyf#!+MM|mnCK7aO1vr0zc3@IXvH!oSgJwA7Xe)i92G= zHfoR1`RF>fkQPmyq1z^roVVg;Z@|RiPhRm)0WtJF-W5!&-+{|GQu%Xf^qjwo4ZcQO zV;|h=rq)*~DSZclxVIs`6$4JmtV{7vNtsR*^vQ-hJ`stISs<0jF^E{FfB~jH*_2Ur zvC3^9qoR=^7QL5WZvD#?@JNbqm+ZK|B-&$E-Ysp~!p=HiCi^E}uxZTD_RPPVIePJ$ zw%{l?{=KIOmQ4;x3U#^TVT3=HCZ3-y%%pIr0`yWFC2BHz1(L>${?$WX4hHtM6xZL< z9B^hSh-Rf8*@wEXn|jx6Jcg``Om{wY5E=K}ejW@ypk}FUji;`YUIm_4PXcQ4q`s{q z2JonDu?;Yt-+)5qC_NCvvEN~qQy&!A?sfe=3GI9Jv9u%hZ7LnX^qT!osVv>x6q-cB zyKsWp8v`OE{ydZHI0-Ge0P}#8jw(`#cAoL%?%xOM*;>=@LE5q&Zd5 z%`8GlByWSP<&ecqlN9=E;X%Jik5i~~zV-9@J+kd`@Fq``Bqh##eM>;j%!}LGE;VgV zGd+x7bq>1$EyHNLmNEw@f`PAFs8+(I(g2=`w9e$J$s~RzV-)Jkhr4+z$X7o@LlW1T znj)ndkDktR5~g=@GDc!n42G&wzSaR!-%Qb0{JO~i<4E{^v3Hb$o;^p1B6zkJ)SyP7 z7(bP^h)T*IAG!@ZWChcy@mb85(ptLprNOXebqIQQ+^dY6U3@vpD@ zUH|x}JDB}OuSfJ5cA7N((v|z})+gRhjEKBsCHX7_4A~eY)d7cDBg$Q}f zxf{>wa@iqF1gqn0Vsfm$&+ssf&puE|s7@ZgA(h}dJO$^SS7UNiuPB@yvos->ck|r{ zD9l0!q!5zVI>uu-^8n^912lW>;KaB**-x87cIva|v;5jihR>Dd(?H<7>WQElwx*p9 zjsIp9o0osGNiX;B&h~sd%?fyXqPPurb4;QMDu*z+t#!LG)sI-&(CjoYPTmt;xTR7n z!+97{*ac5$o|b{ki;8)ZUk~0r&_A`nQ`q%SZ$!W2*n%3jdVAh2Z{Uh11X%=atnJU1 z_=LGBHS63-eKzig=~-y7JnvKxhLpPcIPE=J5+V$x4$_G&%WRbg-3e!&uF%JjVyxBYju z$1D)*nBm!8nq4~V=za}vlT{~@-&M3<%QA`FsUa-)XSy3h^J3A)dg-w+CAxZjM?8=x zblQuQW51_D)Ajo}t$!#9Lu%lPTjl!OtS2?7{+E?u+22UJ?|T`(a=@e#y{SMz?v>eFp)*&T^-^RX(S_5;uvVJ0kc)Y4Yhiuw&i6cr8-H#;zVBL_*$%E4 zeMRI000e@`ZMK(lX_?X<-^1zbLzahaD%l4`c;GUg(i)b}KJw6i%s?LLxdoCayy8{+$7sQnLKt_Vcx_h0Jw5nhHYG67t?)uFLO zdi@aIekKB98Q*fyx?`Gb#<_OBQAL(y17x~|WT|TfMoK~8DXQrmbrVz3&}}T6% z0qGr!VJbeJP(-PHhx>Z2iIQNOr>_$peU4ve@6F6(Y7e-C@2WE#+_vIReY=OV-*?-| zY)(J=X?jb<^#@LNEa09a@Y{d)cAMJ8SXYth?)?+ec3d{}KVufFMUR8HMipau-hh zGqq*0+e1gqu!FRsc~O_pddZ*n(cSoqr&?(jW6Y{W-s?@@>9}i*nwauUZ-Th;on6cC zcAsJIVu|(M1=hp|-qv;K%KJOejjT5lLa{Z5_D;VuOg=kv+T&9su#N38s41%cXk_#i z`3n~7$M5c*b8aEo+|rb!Z4`(a-VdgH=0e|vLz{`Yy;Kp+65G`G+_8;}Qp-PzU~SuJ zDT9f4RzBy%NrePmr~G(cofj7|p7fFke%-bbu)xQp%G&6@8L(KO;PP!pAifE}C(?NvIm=e@@_y zKq!pA58+v|QWwuQQ&s3h_ANj7y_O;!m|V}_xM{!Z;;@+B5mG0cPn-VAxym^8CT zG|N4!?1+N!{W$x@8I7=ty?9TrvHqn7>}jS{_n`vbGX7X*G!|Irs$iI6_-fH42bk#9 z{msE4=hUR^i9OV366Xwj(+@wwHk`k=1QO?#z<}3mA0aFlX~|!DI`7}oR^KK(e*2~G znOZ$+L=4RDoBUpGP0V;LTgie)vfibApQs69|IHP7(vc;|uhFTsqb99;HNoCj#k`5` zXUO5Y7Um*G#S=|=N;O4YkYCd)h`UYb5<%csvZ*JOezXMJZ|}=QGg5N3V2t<7_b#J6 z(o#%{*&Z#ll0y*&myw6<#f5AjrRk?U)oA1lOV_UmLSb17z0n$YP2@+_*qUqi@-1qx z`F1LQgvzL$XJDqWkN;|(HF(yG5_DY)$4Y)GZo`K7w812Jpl9JSq7F(`B|PCyqt#f@ zd~a`{QLuMqg8*|qen2x*tM2`IgJZ%`tul8=?PV_O^chE_V+;*uY12?0dB}m(ro@(1mQROY+DzDu;Z$ zDJt@_6+nx05G=O^SwTO}NL)2XEvxCc+xN247JP1iJRhj>m_;E;dgmkv$H|=h9X&oC zmE)fSU`x+6As1@C}ICuPz;cD{Ae&OyFAFCBv|Uz2kio3!*p!%psVh*YPMX!jL#2YZS1H@4VzZcuM?py8=^r#G8ekq?To4vEs z_$vXO9YiX+hZ82ijCIr?F;gJq<6#?9Q8tU)T3G_b&)LH)0t@r_l#Gl(&J%FdaioM; z{&huj=D_EMy~;LpN~ZQ}6L}B(=QtZIzQrt)9@l$lnIa=`M((k&gj|~3;%!{JaArzK zGPKZG*8Zlp8o`x7Yq^GqhFb13jugW4!Ijzud!;AW{PrGG(hA6!;(3Vto0-_EpAJh4?@y#1~*dXp{8y%4S*D;$!!oBu->|zewm$DYu%5=cGD($|^H8Iv zem7LBmf+3b<&Ij-l`)AJUnR~s6R*ck7%R29s_8_Gy7#cA4E=-*8KRMtIQd|OQSHaj z5Hwnz!tS3W_9jpYJIB8>+3%CC*iek(sgbjs2z^w0Wd2&Qz~o+PgWpbgzI0QmMP`c{ z6n%csEc99B>K`sY{3ub-(3EX)O~zt@{+|2f6uMWs;cJw?%cOZ17drW0>LFCTP$xaO-%F9wqwAjDj`6J! z^n{(&Zy#zB!!e0Gkp;*f3C=CswhbS}_XItPWnve+inK3B^jo03*FK?1iU-Nu+bbvB z+uP-o7$^@lNVV|P!=BR{--ga1-p5k)d9u`VWP)g=xlW2Kg(#Eaw7nd;T^5~6jhKT3 z%|Yf{aM?#FJ#{DlPxUowRa-U;Le45j^M+(tw(tYv+2@rRa6NpcyAqpf~crn*#g1@SO}3+9>v|- ztn>`=X~eWH)99c-QM-z~9m8ruWEX70d=-}=EjVQS}1 z4@}W=aTqUd}Tk zRAU6Sch;WC+c7PY8Eu8MA+51-h<(q(%oA>(UQ4b+zMU(!`)x}2@8^2L1+$z)(-}K0 z=2&xd0{uGE6eRxX72vT3%XMW<)}^>2;nEbVRB<7fa%%Nf4KO>^L^1cE-h~D!m}fr{ zC}{}-Yyb5@`L_#{r;%BH^2l0knzk!(AvbunD8Q}2Fp+szCwz1gr<(6qxW0G?OQv#u zkCfw5h{CjFQ&whQAba>Mx$F01%C20xnnu^eEEPGi2pn-(`?WRh(vLN?ua_V&hmlWc z&N6eBPc0bgu(&**LW0r&Kpz~budZ{h)$b>2cm;Qc30g~Eo_s}UJC465xwa-}F z9ZnqO%|xr4nX1g4kj6d}C8*b+7+BUpYpy&USD*-Pl*}VkSbM9`uV1v)qK_rv-1T1S z2-@({?y`dBQaRI`zUs9#`?-r^slWH;Rgnv?+R1xmrbEkG-u8V@%0|#ZXMo+qxTd)w z=bxxrGN0GffR@8>r~Leb;+;`(ECt)I%^AsMuhH!YWy`hre&`Ts5uSA>W@po7{3=J& z`hAU7y?08ZD`l28#8Nf?(kivr3>s#6X2{c9LLD7UZ%`f%X8-Enz|5Ae!r#*9ziCuQ zAQwmweYa1&x4NXhsi(s<^>jtCY;Oh)^7m=TIs^$WyI!Pj!OFwPSqAC-+46z$cz=KQ z)OuR7lr1FF=eR*3X81kTz3G`AM7i6hLUmF^nWl8+30Bef;@5Y~z&d}WOdD(?I0Iuf zvrt8-;~ZjvK#6N!r7gN%P?0%pEz%Pv0mkr!zcaL<)F*fSmUGolyB5wOSK zwgEZ!hP5Txf3J3%Hl({eB)(oY!+9fuV$jAqw{%VPBKsTsWf1;7IQ)$rIq>Gdrgm{9 z;8U+~W*JoCIYD=4lcGuvSeLCl3*6^l=f!ECCLV0=m%De|Gkw{zQ8*%MZ)2k(aPeY4}WA3+mpyU7G4B#TsC0 z>7VN_0Y!V(l)}y0e-X5^Nvc`;GS&!@|Kym|`o6f&wj`I7i($dNnl*~5PRQR_OKPOl zLsj#g0M!-1Te!hbQ>tde)DLSiw`zbUk&af5e9dJ43Ey| z?9-8T9;r%O8yYwV+_73e*%8byRtJl-f;2nwnn@PTHtI<)6vCvo#gUL1XoeDND}HP1 zJhGIK#GwOVZBxUX+DdV2p3O9Ajg(liYc#*dk=QNeJ-~@rm&OCDA#o+zEB`kzhQEgn zqHw7~a-wEjaU|}#9GSN{@UEHKC~h#0Xff8hEkJ&TIzpR9HDE&J<7`cYs*4@eKV||{ zJ7;9RZE89MrRCk9XIgObJ?OkDY`QKgcqKIY_&yC`4mM+-$!_{Sw^&a196#g2dBzK8 zJCjLkP3&R(;zjia(#4~!3$i(%33)YqWE?P+tXFaMK}*P?1Zo4^edXWB*hu~NLXLEk z8f|ITw+&*_!wdXo;ys(SKBN&_POS8u%&e@2xyAQML)wA{DTJ@yNGvJ#!JKS$&hss5 z$#wAkjGY`gg8`}x?w6V)2E@3_OMbtR1xI9;r`ZZ(TdZ=9Jq(wsvMavs zCaY74taYZ1zs~mi{ROvJO2l4(aF+%za@@H#0tsA{(^hfT*|FRDPGqm7Xh}p#wIqRy z?aFeExb>(A5BI+;00pnw=Hqz(ds1d!{C-4Om+>-L=mbtLeMiqwW}G2Zb1~2M*BD2m z;FMF^nLR)c%aAVc$IxBiU?ly#YD_~fUTZgNpRbU8rn4U%!||J8LxW0B+r|X_UheoA zP*!PG(6>g4`biroS$v|2B8GXL=Il6viW3u<40eQ7xv&B!q*STQ+ygPlYij^%r%aRu zi1tj+n&{Z(tO@>$fcox3hxWlIDEIH)&gf4t6f)N{EEJOvU?{VmX}_~;gA(5I6N8@N z=y>6*;_aM{U+N(U8fD z?Hd7s#gV?|oCHv^5$yue(j(+l8LKg!_!^$DEoO66SYzpvu z*M!cN90c%a$*g>Q_MxhfJl@1Y!7gU{ronrc-*8$(&W$N|=yV!8hPpYiU>{30n-W$){VggXB@`}9{2}NpzH#?;L}jOn-n8eeMFSd10{uW1 z7YTuKC;uRJPK%w6GKBJ78uq93SMSPp4W0G#Q{^h71H%O=?4AZRRp-yPOHFLf86 z;PLWp05?sXu^hLJ?855n-t(u44t;XdzQU(EEu`4nC=R@PoB@tSZz_V8hm-1h{3`1x zV0sPutj9|w{g!=|s?C?A0XS9mPI}cow=Hud;V0B(ve3dZnst6W)?}!6=Kx*<%O;D_ zQblpZT1HX)zN4k{K?#1EV^dbvM<_Wl4>^q2uURBVoj@uq((huYZxR{y)(Kl)MlQyC zX;b#o*$3^F@JaM#y-b`HPkJ#fQbX}YSR#3$r6YE2aVn1dd_rk~bW_)X-D$TNzn#;` zGc}F`g7Bxe386{xWh`i<@p&(PdIFdI|8o`O+P*Sm!;gv3n9smP^+mmg_b(=R zF*(X)_S0Vs8tt-!^Jt>n4KWdIl*XRG$zagTwmBszs7;?1eVrVR)4p8 zuOYKP9n4EzPj^Ei|EYys|2*^PSsivhpSNuw%GBz<42qVzpAE-$S{{kWm31#r#y_kk z;a2#OxOX9Aq9n6T5(9Hc@Miji}sBa7_y!=nmr)vi6nlkZs zxf4H)Kz2K!Q3}kvzpA2@3+G6Bqz1dV!?RWIuj%L~5mvDT)n*SX_!G9q%B&S;_+W>N z3QLfx;{uvKWiL4qWs6{^Oc*Qzz%V*|S$RLSxZH8}e`<)|#W$o>BOd8%u2HR)^~U&n zUDxDmd+nSjyzFm(EOEvs(%Q0*W{aprN{uowGRc2!hAwP(1EtF?gm2C#VLE+TePRUiU$|TLf_39D#5E>pTFieQYK-_L-GpzHZ zHge)`5GV^^0}zuKhoY-zvi&-^XA~C%E+pMh-oa473ly=n9DgUSMI>sLgWe@4hC3mRltpu;GdZ>k4vZ0Gzl zKl*#;KO=`hx+@NM8>s3TW{WOsoyIcocuGy?`YD$DE4ot0fQ}o7kuD~UPoF8yiTj=N zDd~i2Ob_TK=h}%>Qf&T&M!y)O4E)w^BZTKo+&=4fYISbq5$U0RrZhrxAcru?u0U4w zch;?~Qsj{ByR-K>-UJeBL(Q1@l~4Fe<3!#+Z~t{om2*8~e6hmo8xy5`?jIljGwp;E zzyyNv<;_f-;-?_>0=Ud)I^Qz`9rEz&Ha#yj`m94&xO9h{0(%ZtQP%2m86kpjRvB_|=-^-P-=D2-5(0$}|6OKHKZ)X$`^0u_CWP^02!;*Fogn0&ex(^R0& z^I$6)6~b0i>E8via6Nw)wfE8|!Qm0r7ngJ$6xP*lZ#;d6NRstsz<>|A5P^9lmw`;k1wY%U_ogO>_PP;qPqW;rw9k;X zJ0)68GD(g%D;3615V`Ly)vvkALq0pRNStuR-q_z`(kV3!ZZ;`giMiLYf@Qf zN`;76M!1N2u1pYI#>)Y9K<-YxB z)HJP6VVV2$yW2yDuX0ZqzkFV;rv1UhiNA2*x!Yduh~U(>sk=m-h*;T?SR*TJx5&GS z*hWUBSAnb79Qmx#s7{2XXCOgbEYQ7L)qhT&PomSEW%v5qO5U6;)^sPM&k%#Gw4XpX zcCgByhg$9i6fUVlvkvqF*V4+@KB4@)*a^+8g3u6LDftXluZj8;$*na`e*6|0w4Qiu z(CdG`a9zw=mxKdzwd-_3T^6$*8GMZ%2?7*vloWL^cUnJEcELu;{hUCstL66er8eD> zOAdy!45?8VUse7+gDhfFdIO<{XBb!w@9!}qR35p_7NKn`8rz0|dEj3PSE$hgL?bz+vb<Tn7hYd0oz5Yw{W!xN;!$`gmaKDp4|Tzzh(-Xz%oM%1*`yY@3^K}Maa0ZT^UYdfZ+@ZBO0 zH#^Y`4CFw$e>b~spU-nQ;rUFo4V{6gX|a&|2m`Xfm0h|FrzTZ~LJhjGzCbVj*;d_& zUArX)Kj8;`K)q}d*E5a(mH?A1gKCIGf8YO-ZFF$k(vG-e@8!-o4gQgeu9i(Z_xFDz zCGj9!GvXmoy-GPvPmykJoL1XA&eRAKmyn;+A$pA>bB0~{K1Y6rlo`&q(55+#d+6Bs zp~8EBC}-)7^%O?nM015Ia;xv(jGJ@jQzb+@SIcK^Td(IM3`Irp)+p8A%dnj%oTekEHI8B&`lv+e=bTbGG%5f6E7q#<#9Ye)nCJ-fNu% zIeKK(suP3zemLhy%$&K@Lum9`TcHZL*yqn|hbKWZ)T~&I&fq&it-bXL?HlB9x zgma?gntO%-xv04izrUrkv^tE|W=S#C&xrEzJUj()HEk{cs&K9!jd7y~ErTEs-V5PA zgSB{e8j@b07xI78k1t$YCeD)tOXWp_4cDFgW;Ll@tZ$=|_&n2Yg14DASZ}4$2(oV( zMyG*m;BE+!e&}G2$4eO{?7meobupc|)rqv?sOAl^DD;6FzlgKjof6dL*HnM-_f|(?6=0 z$ISzc@VVk(`09;qv=_{*um62{eUaL#1ZdAWCm-YLWg9ltgtOFLw)K5FQTIGJx7|G3 zsl`Tw=G0+AwNK-Uwns1&S6xqL(rlvqB*le(Jy2K&V{Iq`@NCYb zE57?Fl&EE>5=9#+u7AhFvyfSFIOimlS(~=ZuBmyNACW5k&*7)79==BSMpMrFcP!PBtuFcO#E(OV;TbuoLD*{cY5?auEc2bMGTM;nB zhwMq8k0b}LQ1cASQZ9Qh-SyhV)2C^NF&(TkL_{sb@!yhV(kf6;8F%s9M`keh+UY>d z$(#(z@!TtQzu!wiG>y9UIXV@oyL#t`8?^n2COX-g*NG$lQ3|H@DTGP0Xj-3x^c7Vv z256sFVo=qRs=gQ82{FVhH>|YTwy)KM+Go)Rn^lCF-4TBSZ~SKnN}a4IFh&c`7-C)x z0M+`1)s4q1Vw;MpcazK{-tgOS zQ7Ik4_a;?@{XyE?b-Y8iCGmIQTgJgW5o9KoV|Xt|+*?QmHU(f(ABUmLZFbV^2=D7N ztWaM*|L?v#`k_l@if15hbmVBw#VLjeqLKj{VdyoZ|$osFIOM3#<5@1sSqbSTq@coy=Tpaa$I%Ro_jWV8fpD;LGj8947%jCx} zlaa;JhYji*e$WA1bj$T)J0_g*D$z-AFE;Z$MqmYuV%!>#cQZ9n{~V~6H;b+yntsx5(4%OGRM*e5mUsfNF!A~~Hk;d*;WjqYUUrQ3Wj zv|b$2uKIl(W&gx0JTOAf5@*J_xwo}%NrBhdf(g-cJZ(KT&Oq*!gZvZ2ui`E>jiHCV zF>Mi<)svpF**zHur!{lUFRSnlCrt6g)Af4_CZ$=(yohdX^@Pt>gOAeiDDKDtr>`Le z_$N=JcT}nsBlQQ~|3CjxAsS@nka+HQL^k7Evy<>9k zaorQAZ#y#Z{hz<7<-~vzrql5+*5byZ>Q?=w1-`%Dxc+;rWE2?-2-Cy5hsd4SpKI%T zUNmth=yJ~;p^q<$$z~&+Cu@c+XktK=YsG~CG^lj{RW_l5fbez zqc2%_lo9YJmsX{cIi?z>GKe*E*{AGPl#nt$4u`1l1|~SikjS_3*62BT!3w_Ks-@1P zDW4gZ^m}$1JJl z$P~y_`dZ&~>2M?`Zx*68jNtpKca_NzF^O`IzaU9rka6ZiovVrhcnhro{)SHm`-T)7 z<}ID>HWMx@hOSB4??bhzt9Yze@Zzfjlymj7D=@ptjW8VdjzFXQ5v z$ios0d9N!bO`q)aPD>Cwy)f#3LHP-E=k_slq+U`E?SGCszw9^lR28HPox05R@;b{u<;a(@rO z8`C)n(pUD-ga_@Jwgx4FTQ?ExH?h%Qv|?$fDSt;oum4Pl`-|_ca!v%Hi`w(|?z3Fz zN~RkT?%U$6KV7r5fTGU%Jz&yhIotH0;CGJ~_T&H0w2c#P0kBazwzG%Tkq6 zs2B@f`+G4%HsPb5SNWN5X?4zOOtWV_hMBE*ZT(^Mg~bhINyy0Qsv;gIa*;e+unWy~ z3WOM8p8_E{x#&yHms=yaS5ql0(Oy60p|yGP;nf9GrTn__dW&kuJFeKTyD@wY0MiTA z{(ku!GPb3W65@@QK?z$(s_JX>+;@VW#q^^yhBO2{tycGEh6e`qzD)!7PhSCW9tB7P zc0Xg_c+OV;S^n42aE^W7nv+7dsKN1SnD*L^?jR590;|WWLaMWF+(XN;W+Eku0iG~% zvVGL9F?k}T5;R3pd??G=OSU!pVRJD@KgZ$g$vpIWx{Gz$63zbx1=pa3pMg3(QDUaV z3g<<5IN8Hjen*D~dX7=@VMu5Gbi?l)5O~7sy@T#Kpx>T(1`lae{{uKvgcwLu77{DZL^YZ9WnXRwJJLt{MxS6aL8;1!S=Uqyz)-eMXig1)e-afWV+ks>1(v z%JLnCyed< zebn7C_x-CiTIr6?GjPMY4MhodvnI>^YN~()4Vz2gsdv0K<9X=Ielwc?{hyr-WJdB8 z%D>NAO-iBo&cxR@)W;w=Pq3)Z=+c9L^o-QKAPu8uU;w~J(Vd2k?w!N}`vkb=;n5Ldnh2&G|PlMkgmFGJK4N}g3 zD(VRp3>KfLQaGk0S7vsx;h*1t=^jk=281qu;`sr(bdJ_kN0WeY6Ltb7C?S(fXpWtw z?3$)320uwxwPUZirle|gqfat&tBMt>E}Fip2eTr z<6|2(U|naQFxMp03PqG6=ZY7_G&M28MPBbkm+ybksQ>4CvMntIH=|blzNffzwMMlC zB~1@iJF|w&+-Km)Cs+f4Jm>Nd!HFxF#y-aE<9+KiGb{nFzdu{ug~q#O;*D`- zs45?e>YJW#@-H>LZ=sT43R8CQaUIiM|8GyF{28POFT(Od-OdzEoV&Yi08_LNvSn+x z7B3@cj`c54{A1;wKN`y07Pp@Nr2w!iDsfOMHHm*NjEwEI*E=6y4jnCHSkF=;%2s-x zR15}c^K~|Cl2e2fhnN4K#=$dZ=?QJ!OL6KYmL75s&hLD2&XP;S9Uz%c{uM`0-nY9k z;eTGGIi7`HW%j;*wZ=26nphh$eH+jaqP63^EYjlpczi$JTg}dA(DYa51`NaHzpkE& zzt0)bGK0qZXD8rii(RRtJ!hBmTC&kENzgg~e0P>B2l+iwPv7N0Mp9rj~W@JxzDxy^;WCq)%;3f3o~q&)>J9NMa?||9xja|8h6W zJt;Wh-eZxyF|v<|!wW+_MK`9fw8`n6E?GWgTrhxl!)1xx#~YJ z9I|;pe!kU}wQl<<`CYj@9RGh@oCRvtez9}jMPJEXdU{D4J~PG znE}Uyd>eM*kiu}P35F{{v$)@VH~*ybzfy_Qs~@4p_2yL^q5zgZLcZU1AK?dOQoq+b zjZtJHmNT2sIGX+4?_(&C?*(PV>ZRVi2BLpZ=I+C@cC9``uD!_|pi@`xJL+dG2GeaZQC z=6jn4pts{_jK7Qs;qyJB$t&N=vmZ{IwQse_wg~7)aHZd$G^tI#lxaue7%Hw`2`yU+={xs<0R3g*}>AmCQ3b>nG4xH=W#S_Mu>O+}FmO;Q#y>p)p&Z zwHzalY~#^zCXEk!vPJeY2i`)AFjYgJ-^^_7P<|{w^d!y!m4XKIx9p#b&A9BfVWdI8 zpHyuKBHAfnkU@e|W)V}BLpRMgd8Vr@!A_Z~SG_o`xP6vX&H$OG0z*1A+|8bLUH@ZZ z>#3y3XpZoCKQ%7S3;7jcS`XPBM)>1z*mRw9gn4x9)i5(*3^9G`A**vbQdjcYR8s8J;?BsPl-P5lNoss zH>?G}o6B;lOjk{nPp(&MF94ch{iZ%P0VQPD?Zv9 z0IqdIr!pofPh0!%!t5NGEPCnpzusHB4yU-51ACCeg)0$bro*}fj&%+W@O`Hy{3nhb z8;oZNGQC;4wUdwc`+>{pz((<1%1o+++zEg#1(B|Cg&XA!o>HT&UoP>TuAd^1=k<4` zL!nYw>_hL|Mi%y6>VowBn964ZRvMAQr{^sMQw2MxNsc99fL1@@9;wKr1@SlSU#a;>wANC5(=(Y?*PLfxoG8{0~yp#9Y7feNLAvROHBBlQWd)zYnt^m z(@NTXfQ>rtJMZW3>gO&REPhCeX!S;iPps?-7Nu2DMJMi={Rm!qMVZ{PY-wqwu0p0Q z_VFl|jzZDVzAG~5!4GGkih=in|Bdam++%RWk2wZF#KnCUnC(31Xyn}97ps~0>i@=S z#DQ%xzD0})t&3-wdafG>>m^#g&9%^v(`z*6C_b)#wriJm|9`@~1TSvKNh&7d^ zV1$Axm-`s7?!Cr8(W?G?0ZAA{gg}9*c;Abi3G0pZ2OUKE-IQR_H}G_S55vieINKIN zhpTG%pP;U(0vv9S;k^0(JjI3epiS`>5(%gTq?<6+``q^SzipU8q`$ z?Rqr5_r3i!cwFq=2dQEAySG*UN4+I-OlO3agr!p|tM1^^7QG?{s;t8u7>~UDAcx!7 z|Lv9JEmxso-T$Kq6j2uIwEE6s<<0vg^!OR`{2F2ealb?Ik_L>W>$_mS+uS`e-4V#;zJWj^a942%ntP`q1_ntSEJqP-LsfGb=&HQv~Oh zNqbD)MZDoijRLgK{?rruy+8)OJQ8Nf%-)K__u#=5T<#>UQctyygSK<v;oII?&ot+2Skr zb@(+_a6>Ug(RCQa*71sTp7eoy ztCbp`99+v%+C(4dGgIvE2eMsE=%}LZTSRzW)|amg0JiOQ=~7hRPrXE~sDGP3*)k23 z2-uh3)lRpjsUB+6^%;W<0m7XM7yq_)4O4`2+K@QG#CNqcOoPqe_OUR2tJm+3NPKKQ zYghF~D+aa~d7+HJ6saw*_ z;cq9XBKY69F7C61RD*9afspRgk&<=ubZyp{4#S4z=Cw}Xosu|-&4e6>KUoDi1~$&^ zJ2KJE7U$f}@omPM5Kz6ZFvX&Ok8j(6Djil0&DOj;s@x|mz(GSgM<$)Z2O~oRVRXH0 zj?B{ExA52oa^qGzte9j+WUW(8l2hLNlxHLc#N>kAm>oZaT+Lh1=WVQO1~Yern?lc<%pW?M{+hN0)1jPUu(~Q3Laz*yH*fjN4N0 zH6)WVBO^fYFWk59qSRN#=lQz1=k+P#^g2hYs8HaG6GbL-r&DC;!=gnNS%RO>5<&iE z_mrC~nXWS*L!i<49bCxGWyxIySIbQ#;!SIIAHPL2mXwn$#4YB~LG%WF%5Wzb#0CjoLmX$Wh#W8jKQaElcvgXBn?{?cvjK4ao5M zZ?vPge_y~16mzRa<_eAL|JD}?o+nByi>%^gTR3P^sAR`7_IA3QMJ;R`IcDHbso{9f z-!f{zGdl;M_-%0#TOogH`Q`f*TI!|Wq$>`;?1fg*l3NLo32c7l(2@ChQ1owy#yXF+ zV;-vkU&f~CQ+zX*n7f~md;k6Px|`3qtV@fhn|A+h0T8*ieP1#awF2L0k@&Y>AI$K$su65FSCJpQW( zyM_NtD>ovn!6+(|+fW#1Yu&T>6owe%xdt2{&ooh}=DQ{@!b6T032sH6ZlO*11gD_X zO8)t85DI(4)ZkjoU-5lfcOLHp(D6!p(4ALFOzx-d>PY|IfVVfTS$LM=V~&4nGlFpM zaGuh+%}D-@#N}K`TphA)-fX7~kZ^;e$J8J0*4gLyGni$cbmpum&+bk2y!LVC<#sgA zX8WiJb)G5m_*jq0T0@=BxD@v&AECNTw^+H<*M^O|T&0yvgchXPFuHKQ;9CkT#urPt zv1x}5#|>>U3Hs4E*w&tTVSbOU-=^!mS@?HQl)-N+L7Aq@splR71p|~5J8>Ugk;1CP zQKxO{m1f~I>E}AC^aFBD@Irqu{-UK!VYaA!X5#w&okkG>r0)Qu#E2j4+EUl5?O=sje7`F zSS5^+%3;hjIemY&$P9pYYBr93%j;ix?E_C`ur5-1ZMBhQ-h2UWZBkIc1R3VDwKr+f z%I~q18Qu@ff}fs|#5lV6GGJ=9;oWT595!5CebRkNTbEaSW)bYB@BGhz^5{1=8@<5O zC!QL>e;$3r&!q3Gby#6jYiitr4RCCezBHLttkIZN)E#}Qy!aqpowIvz9M7re<+UH; zSrM?b?~QM_r=vlg=w!*S{>nb#$zOl}HBe{-B$W=r?f0;eP!2L?M&F1l4ZH$ZzE=}e zaWo78{da!1VYJMFR_IV8>YM<4F*rK17{&xlyf&;`ReuI*n*4u?_cF$-+cQe}@aR8( znP5vsD)hRimj8N1I#9r~$Q6bhhW9yof8aN1VR1Q^h zzSApnp;Z4wFEOv=VnT2W$ZbE(suEB)sU$}d=CbXsX{L*_R_heldUPln&|%s--)af- zRy@<)uiNi1ld1)J;;Q-G%kSgX%9$-e;mYfLrAx)>`5o6ae&uJmwRYxs?J<}BShhsN z>x-8#v(t$lBm80oXxxp1z2Bo-E~(M@=A77C72RGA`?~x>A@c55OuH@qW-`s@<3KYy z(Al2@p^j97joWn$$L_52${KoHfb|2VVH$C2O+Ia5f5) zq1k!U^8FBO_TQz(Q;G7Gzz^|n~|0P#SF?}Ys#fWCFd7rf|56x8F!PF9gb%Xtw> z8#~whH-$~Pz{Yt`o2bWEdpVS)#{{LxELx=Bfq%wcH2sf^Bj9>F9P=^{5tZAoJcEwN z*SmB1{H=|IeWEdPd;eS`f`{=ThJ5#_dUUSa13Mg+I}+0rBq1Tt%bW#9E$ z31jnJWqSK;JuOZok6V`mDWaVDOQ;==L>IH^*Db}(=spDEN#YAza6rx{iXh~;K^*x5 zUY*NF9u3F=@6$#QDX;npuf5-1_`XVne2T2mAYmN{GK(> zGXc{M1;x6ee!_@126UTo%1g!tQ3a7IV*X|^cW&D@GD7P6v=f=F6k&faLB5;obQfnQ z@jNpP4V3||DH@g?+wOdQ?6$A@SbcsxBhnn(XM?*Eo0Sf&A+HlbtZ&eDZaY*H|Da7c8@UF&qR9pwn0;L zZXlU8A4Z%>zh@_bDM+*IiN96c)z0ku?(q4p$p$J)cq62=ZetgD7Ot|(be{AF$xGQO zxGLWuqc z1*HC_p_8aVNO+X>vAr~<6GN~{!(NVO*_9>Vs90f$p zUBtQgH>rOl%0w0;P-p(%A#?Fauw#g{7 zoMjRf+2~0bvO5O(e+mzbj(!XsR71bUy{VRes6fSk`pxUe^1f$^;bFYO|A}p6jb)U( z{md7x`I0woA^d1e51xTq^}!$X*_SDP#44fL97840p__UyBl{WVH4B${mD}*z#}A0p zSf-F`q^uyp&oM?gQiYtM(P)1?-74SiEJyM;wuyGVB0S};_1Nqc@+=oW`3LQhb%lxo zB#%gG?G*3cx$VoE6v@57_oOz@tE0-v2fTaDOmZoLK$^sNfp70jqHKB_;`vh*Xc({V zw!HPPqY9#_?U+3Ts_YfDHBkN`R3hl@^tB~ixXB@P2rLnW(C0{9mZa7o(oZTi2 z4|&ASrQ+KLUSg<;zxJaP!w`<-d&YD_F#Og~+UK+~6JyS}O3ibQJHXib5z=k@k1)YT zMbZJwlb-}lGBY=A(n?3r`?OuM?7PIqJ>?(wa<>9)r;!@&Oy22_Ru;3O8-&jUDE=Jo z%%w2;CZ4I=qQpMeqoIJxA!P&J>^S0M^k&|EY8B>2$E<4D#r zmFy-!<_rpitH}Exu1Fs`sRcEsnod>9yQp(GC~L24;C;7u_YaD_ct+7W%Q(7yvLWGb zaId-Tt&GQ%(1-)uKC{e2Bd&LZg8NW0qA=GH!_b9k|q-?n0lR~T}i%#(bgkJSnd1GFF$zhnmQJ%>}Ep)>7b)4YuH zswx{b_W6T;#)ac4E8%Q>yuL{$Mb)gUqLlQ;;{6uDsOjUhpXnIM{N^sLgu-@ z>-H*pFeY*U2lngd9>4BVKxm`^Qy1M~YmSk$6A>*XN*Fy|?tP{;Mvv=%ye%=ekdMCm z_V@5reBZaX3pt7X)LXvEKnUweP-;$540TQ~%lX5RY0HUWT3lf@r;J{0{*wDPt2HN& zlWbduP{AV{BMiV<#H}zg#?T-8;ZH-2l6~ai{CVARuKBt@lE8_T&(E+cBXnY&-6ih% zDhsqbLFsqsP9vwsXwvEdd|wn{*d-iPm-fCVY>T{Ga2xl`hgqq4=a4ZoeW=2X?+Jzr|fCU9Z=nshBfLdA8Z6xdSyFI?i+XTn8T zpDfSNvVJ@6s&|4(QrrE2?R)I$Q5wNp%U$erw(^S(+;FId+JfDYkFYG)iNyL8>!Kli zUKgUf@*TjvCxzZdh8dY;nTfg?okrFX9mcG#{~jtwK@{7sE7}avp2U_k-0x+PafUP* zDR6L$v2SrkT}6QMJ*&L*P-KpDefJ=fpICrg$2M%-2ZO(oP6c_tX$CT4E;$LV+2g-F zXk`u=(N6Nh-K0t%gyr>zwN{X*Jo%}-E$0MXk@uz5uNN%bk0kXss zPkUkUX=!A7|6k4o(lUn8t^NI#iNg}9<7L>-)nb-lbi_}h(Klk@m6X9CPhbnbeuRZF|*ovuMu`_?8g=1!5YEik~Jc0;d^AKdQcU+ zI=;L0c`ZDyF}H@CH)gnxQM+NT;6zVeQdO|vk~Q0M!o(^-1{6SKacFf0Vwp=%Mj_G} zd@{EcMl=;=;B1w zK>3>c*MxIRNIhVUmR#lmTkOyO<=&*(*J181>ih5e9nsoTmcUx<)Tqtur2P7H?g0?2 z13wWU*Zm#&Aa|f2E!+R)u5y;1G9hy7FBkoq5vI}3CO@q>$Np*tpKM&VH^dz+7WUgr zcxvE_?&S>E$~%eh51~fIw9f1q4S|=rs&}kmSCCVOoQvHhO;*Dx5IR~j-j?{fxp zNUvr1ea5&3U&w*=eKi@!9W4yUAd9-Qf+D&VH-xheyq}>s5)64|Qy@K$;}Z4?8^Rxi zl5d3^U?u2}*b$<)V-BDuu;35WfT`R|*<>{2{}N}f>0R|*BY=mNZzZ^WD}d+(8FY|| z#MB+j1y>PY5RP1pc3kOhM{MP32w4F?x%zxhgwkYIoHL$>j2+O%8~>%v1dmALS=@nHdteB3t@H z?+##H@e0w3Wl}sNlH`J@^^VE?W1D06F+$OE|2^$CpSPH{WrBxW#=e9~*Am$^^&&9A7~(P`j}W3>+?Kv`gBo_^_z!sV_BXVY&e#4c8MqpkXwLht3l@6UTPs0qioy`SMX!x_3-6bllFnhRZzuhIJGX&69Wr1}4*2y?z-Nr(sL+-A59cR9qS9#pk$4U{d ziqA6P$$S^Zf zm7XW|Nm3CVPZpC6oA(uVomks7fiC;Rd;8TClw{uiEzp&+pc?MPVkCRP^h1F1zXa58LKDC(sw_o;q)Zs76g{73e-)s?n z2Kjhh-blq9HUSm`eaV;&C}P;QA7`6Q8@vyPS|95=7`HN9yyYbnmzPZMFG0{=+fhkB zTj%z1{hE9B6?dF6LgJTpa4!Rh3VBAQb8%6bm(&xl^gZj zHS9S}9B>d}I~jIPUpXDhYX6sCyBcYn8Zre_Mp-^G{?@`_^j9>8DuQpn`>oVU2<6&` zAM&Ds_Y60YWLc3b;VZ&~nWf#ZU&jtdeSS8C1w!pJsom zOcRpU>62(=W|8hDcNfFk9QW`0&-*K@8#toipwF9=a4#cE8`op^@CfbtjSLWU^O679 zOes?L@}-9}teL<>REn==5NJN-s)t`xo+Dmfdt{5y{pFD@{6)$ zLj9}0V0Z_uleBp6Zzq>upCW7GQ1M+FW8$1oNJud%yB|AV`=M{uZJA38nxGCz((9cG z#q)>#eOQXwnRvy=i?-Z3jeO6?_5{FcX3|vwT#%w{U55onQq>*r_=W?XU`H3|qSSb>U8sdH05tbbQ1BN(6 zgk2P^jfgR8{Fw{sh^L>4#Wj_usk+6d!ad&`N9~l4(2#5@7SyM3_KDVtrHJkHbN#w% z-kfR2|DsIc-Vl(S-j3JG@?Kvfav}%FQLleUItM!0ge+9E|e;Okb+?A&3bUMBZcYQ%MxK4DT zk!Eq`{uYV^C1Z?@ne)9Amu~@oj*AYRHU~_m>Gv}bbzE9T4zaZ!cdSk?kCvky!NlZ~ z@jKF?YYmk(Hjs4Sg*PpY&-d?^hxafjGbv$8y`GV8-Y4JRs_ZPW?0(aSck#+u037@H zY%#zqTZFgn%iv3kL(cJnCs@@hm3RF9z>_L@DMLI(G6KM3;8(`)th(&UStzRGvj46L z44IR9NsP@Vuq1Wfl9z+ zY)ooi3qru`;{%ag1JA^e`{;P_{g^-sXQq%jewGq3C-Je+gI-_p>M>nfv-1oC1D)VYQ)kANy1p4CL@yr;8 zITGHShO^Y49ja=oE9m$dXC(kIr5FD-6-J>h*wPxcYqURRk}#%=*Zgl;6qy(hr33#R z7ID9sOGvd|3-j=_pA>^;?StVg5vd`IfVA+YORR3-6%-iRX{DnpYWNr;8udbRJ!tG9 zm2F=MI2r~ zxy$pJZ2z z5ADlk1k;)T3pp5(J^HdyH9iO6`(Km*j3;F&Q?~IJTaClnbN3bPJDO8`hWkjH!GMKc*;cE`iT8vmC*ZnGpx{bS0j05RqOa!)=YUk8H!GlBHK*YP4z=zCa49($*6Y)N4MuQt}d6$IYDy<&nJn)v%~oukilgqH!5>A;W_^3;4;gmMzd$ zIc&&rF>yH=zPZud-15NOyiBW?-{x%YBe@kLoT_Hbi0}gGBuaMgg~?M@^?p()OoG?< zogZ0}`+-LT$>5N_fY%>tkQN_8U(iz9%}Ry z(ZpaET%mKL6-Lco@hniaCy;Elj(l=a4L*?e6`zZp$`*eEuE-tI4LaJowZM_ibxQy1 zzX?vBl&Gg%=QAc}+6F<#+Pj@WsS{pxQc$w*>&zwY`}Mp}-ooU|5oC%ppUL8p0vm`K z%`lTBWgRz9XnAYI#PG`gHa{SIFC7>D!4t!0ixT%qT0B-uI)denCX)>&y)WxS0!h&E z!pTRQ10EFgb9|4Ly|{~I>4x}KOP&9>6^QA~8hAi}?@w+|T=aWf5S-bcvRU`n=F@l* z`=uU*x~F&@Yo^i-;@$~huslvwkB#;kW$~l@c+xX; z^s=`6orsf8UD&!ooD+EncHr727W-$mCTjv4-Fv=L`Vas@;c)xLR|TVxL}y<^n=OYM z)?yju+7izO@x}!Isra*Gn9Js5SV^%kfiu_h57r)X%Y;x-Wd8JB+wZZse>$ozw${&@ ziv9I8EgMoS{IZVwmK1FoJBR*&5h(U$$5*u73GA}7MCBctv?If~%fhdeS@%+uby|JT zPbSj-EB5@~&e9-9zD&YKcw4fM)eN(26GV~U*|HA@lGPQoZ{G(qUPXkfq3U=CW_1T^ z<=MA&E`H0HtvRke^oH7|qQgYL$Ef-+ZTKWDp6^g!&*$}ic^~!w+Kv+Hpjg4q&FPTQ zRoi!v$y@&M_G1^`0}qp&su9#Y>y$<3QGUYp^0s}+e9*K2XJ>Vx4NzoW$GP&krm53; zOdb0xcSBBA>y1qk!ZWW{HiM9%P9l@f6?GJq6SbhjdqcgF^?6WfNd_TY_`bE@f_W90 zE)T3-tavxGzTwjepReaNLo3nTXG#W?F%_!zn@+7JD^{`Yt!|_R;}242j=3%JNtKIf z8Vh{S&L4T9L9>hhToY5AE>B7xYwdm9xy??n4r>VW@U)4laQEq%yOxinPvt81DT&SV zqG(x$kA7?)X`5po%u|LhA!GoK)JudiKclbZBod^3j-u~5CIcmpp&=WfBk+5%R{oXX zn!yeR_aET6kLHf}+TH^s-$S&(j9rTVe&*COV%_ts+xOR6GG@M!)SUa@l^$D`6l(N+ z4CAr(@HXCizaoIA^~T$+LyAdBSr5-o_P^y0=Qm|RvHhNQJSSXcod1m9@7W}TNhTim zr0(7Maf95A1J@~^fKWW2w#jxeyJN;IZ|NK|p>r|b02`(_0@@>U!<;HW0*TEia%WDHS>ACysd6HFj?Z)Ix9FfZKm#)Z|;u zTn^w6060%+|!hB#t1}4_aAFY_J~h&<$X%XEm?&2xiOZ5emlFJcdP=>?q6VY zP`X^>jb-BceR~?EC9yA;CFE-q+|TtJTe0P_45`d|vry__qbj2q)LAR5zSBPb-yvZ=aK`*84C4JSqtsjV1yT>F(xmAR99$hKDNy zIEU)Osu7B38nUCo0km~USf%sqIh>eLvDX6UtrN@1H%c+QL=g!w2ZcLqzl$}=ZX$Nk z);MqLaM+)?qHN#KZM-k8e8WG0(R{6>jO3|O?Yb6dzoea&kC(ys2M;(` z2nCvCqfK2}y+DU0n?#c1tCQY+Aa(gJy`{Q*LIJ7n z=F2g6+KfD+HEfMKAqS`iuAY6)mOaNzTy~|~`}jk&HiyarC^1Af2IM|h6U{+rZ0`{v zUG+e&*T=D9pY}`mfPQ#RAD_uVF52s<-EA;Y)s6m>Ib{d1eT?<98P3uC5k2IA7asAHCZP>ODqCU|pd{X{EV_b9@nh$R z=!QlCbhOY2c6Nqh@5(Cjd9{0aVDGa>6U9X|7VO7; zIE*GJaAyi?7I43qqPM0t*t2NtG%?{ZUSqb{X<`V93D4B+e2uCQB@ceqpV*px5JPzg z-b?e?J0G9+5Gm2`)Na~XxTOZ=en#Z2od7v4YXL^Bp1rAP-(aIeiD+9YUsI$mZsQlX z|9yU@!IUk+URY;4@ge5ny=>I_&PTg_!_ERWC6f6r2Z<1Hw40oNE3qO84Lv0#XWlcW7mv@3SU~o%LsilKqUP-v*_BR z$A5~=+tYWxRjI(6JZqXZwWBKpLecCdn{zNTAfLOzqMB2X(KjdppL=--3D&og0orLk$ z>m0MwWDp*?fh=zo%#*y2e2>)^?X`d4viq#lP(8RW%QUbv($(~=YM?iw1-2gpTkJ=+ zB|DNP-rPF!f;CFc34-6`i~>}do}SDX)*p8j8fx@g#-E+DFj^DWU-`wOSM z*=xMd9szeTVBa#w`mEzz%l>yr-p^#WF!fInaKDzT_zirfRkM54JUI$ELOf3R4t5=+ z2tCTD38lu|^l5RwJ8f=al4{qFbCP)pKS&y;gMa>}v@C9J0kZ$r`!#b>&#Bh8C#zkJ z{dP|3Np79J0aGrfG@Uz?T9UnGCe*2!ENRj z5jDKxxhN5BVEfk-L^$6aIWw>Aa}p^b9aq60!G+j8Pi={>**uy0w7?>h#P6|uJ0~wC z>jp@>GGTrnJ}lop&;b|#mqxZ!^s`tH>@cRwCPVknym+E@d*%T_DGOfP9d4b=*;TZd!Bm4Ee#~r%d)?7}77qHN{Ql2w& zR(&m%`*_Vs7!E4tGS$rYuixEND}&N+)+wQKpO=Akb+I9D1l_L4wu|4Y)+-Znd3PV> z&>bIe)iP{?Slj+Nhuux!X_NP#^wj^&bu-{@0}ZWH1VFqW(r0S2-vG*#>IWT9dT z=8SmH$ZNbgsgsmK@s(oVnqfguOl8R`wh($EI2D(+)%HICD|v9B__x_stHr`S|N! za4KMYzvn$Z!;l9!NwP=}LYHU=3fZJ>g%#m*Rp0F|zqL)xJm@l2u;P3c=#EXqAZq(B z)QH46McIXUxiO_O5RZY`ci#-Mq0*2oBaG;tI9blvJGMa>)xJD!XwhR9-(@=pII^;P9paNfToetCK?89g+bmDB=-Vy zpL0+PKBHUGkk_8?9z@)vn41ltW#;2gb{HV1b67!-H?{YXl**Bh_K*`<}Zb^ar{>A$j#D13++mmwt)=1~oHN=wfUGmZ!M8^8LJP~Ez+mwbT>o*Ox=p%3o_+e)Q^ z|7Ruum-*uy-*uzi6}}VcS#fz#3fWy_y47iZ%FM=xR5DCM*I-tY7IptLE@|v-v7+0SjYYXbjIVN)ZQh@vsvrE3AEqO zJaNv~o2L0n(MEQum-olE3hFyBGSS*~4eX;h2udg0wlKm{QoW%n*Zl&w+fA zbAG>xi(>qJc0Kq7g4! z&PmF0nRc%o@8d3rI~jI=#vR(!i9MXxGJbyLwx+s=&KR*O{yCV)<|(b<#2fNg2w#`q z6_drC5)L>0eb#x5D@KYW=g<5N_X!7JU3_#%)J_Rj4~xA}MIl&qk{a?@v|BPN^KoZd zVqsO9TU33W2U4=_&sv4?DTK#dk{Y;L;cB!wEJ}`l#NkyYxIPnyMz{19BKKIR#pzgu z7@q6&P5d~7p*pehU&*NEN^UV9km={ze={SxBwrw@$}(9>28bv2MOeFde-No5vv)*5Ii86wQ~}ao&_X z`OQmP)bg~HD?X3?I5^`~QtH#a{j5PRJnse?U_HdHrlxK<>t!3k&`^{7D6RIIfKbjzK&57W*1|oueW;j{GbvH9qZrH;@4>EgQn4)$`WTU zYU)%01(g_zl+-gF$K8y}NgpWIeVGt#a}DB@tj1rpV+yOw`rG~s9D^e8YJoVw{Y7-%!Tn#QLVNgU)>hhTMgDk#N-RXBIGpTu#=yeLF;2ydtdG|>8ZWR^*I3Mqm$E9U9;yYz4aJ?aeeBW0TzE8s1@|_^0d-2hI9njw# zG(K~l;QYxy-%{(hJ<<%K-4U%0CzB3G6sI=!4QV{BUS0y%PmD~%w&q)dcl9<0qtraI z@lqaM1A=^tnnXp4dFNSS?N7!}*Yx4;?$S_n1mP_YMT>K1Kc*a0BU8AZWvvZjF&&p8 ziUDNPtdhIe7-WAbC5=TMM&(xRuMd0Pq>~KS4$#@;Vy}VT-!q5QbRCE??xWe=GGi!q z>Q5$_&)AXk-e0<*68l;^*~WaoXf5UuczjjTB;$yOqABDd*WDJwfDjlp0$Agp-4gPO za}kGm7^H0PRa0vR0>dw$x?QWsmNM*@viY``CADV%8fR&;%-O1IwM)F}TYGSBQ+iVw zj^N8u;pT?0G4eErg^$A;I{`8B1UazSYNdJ8JJVCZwhKE$N*p{-jmshYQN_mZ~tbWbdqCDi^Cu~&1$}C z(_ShJoY>!o8Y&l}DDHqN{ux0DF^@o3#}(-r|9*WCL(sKhJ-5WAMX|n9g(vG91TSP2 zX3f_%iSTwRsWa^LdU+DMgzuY$u5{epKx&R+Mt`wlx))L0-D#65u6tJtT{?X0x)4+b zWO(KF3US&Jx>1jlWKSPjU;I5ncxxiC5%^M1BW zx%@GxU(Q)cTL$I@u>{z&K0>(wG<#6_TIQp`seEVUEM$JC=+UFT&pk5rn|nb~+WR1t z+RrTmBp%%jFdiaGX4c!}Q<$ikP@^R5%j9&T0p%$KR*PGGH|rdh&c77Qyu{UE_XKB~ zt578w(($(Zu*2oi7pYt`L@*BH@5<&zTgMZz-|CJDqxnD?8e|xTpgHtdB(fLDS$p z>cmdLNnr_k`8|5asFN2imS6BbotLP)RYvQ14j{;0$qa*G^H_r{XxuDsD=V@m5@;vOnc*ovfashaH$=EVDEOU|1FI@o z3AVGc59{PUwLMWm2NE`B%S0#gPiKIF;T#cNw3&thN)OZZYoF;+`H>GBr!Abyj^(0- zj6B5%<|F)PDcWWVJsFRti;?l9TfC1u?@{sp!Cs59*d?=+w)7Dx&;^;Ya_N_>Px$S& zjg^Q1p*3!CR)xgyYi4dXYH1_4$s_7uWg0829T9gQ+{rI3#$09nB;fhpbZFzd>0QQ; zhh~zKB@@qgr1U*AYNWtGU~5g_jOw;(f0g8nca`Jauu@}su078*Wt@Pt(H6KZ0QC1~ z29e(`68)%T#%E|upe^9r(NJ?b4^N^lFe`UUY}^hz)8zohiM-$ zcTFO#&N{o14AnkpI14jYO-0NlVLe{r)%W=p_#_nj4${b@S$U z_EfvB-CY|~jqjIyrX)0m0A0G^Ot?RSSC-%Q3~1%jK>UTbw|riAk0UntBHwB_kwDl; z4xe!&U=d}8cH}a#&7e+P#gYZ~umcU%3=;wE>nOfsf`9L(zQvIlT-Xw&CK=Hn!=6ZS zh*O(s^=6WWLRcL3Zk7|~buMlHQnB$saWpRQ-8JQhcLgzS(@PM7RP`3P@ep2$D z3=+;f$cLYC*Bz#@7!y~6A_L^_B{F|Bb&o-TduK?pJWRJOyZ>p25z2jeydavm9O=gu zujuPzDkKWw?%Gw3t;OxocM{f^R++p{icRPLch&yRmQYnRKg<`sU$1q|w1~Key?vZ} z$1cc8b8^>$Zn;f;@>suT(6j3$bdKK7Gp!LNH#X)n6P0x`ff-3&b1)jEs2bHfSBmK^ zjPQHDvE)C1m)qSiSv%hh{eAkFJ-LP{-GWvd99Ro$;`3|%C}T*jQEICJpq3(Z-3Cl< zI~o0&qNR;h)T;-vA}L0plupK@(e$AhKO_GTI3gE+zgo0G4d)(3;3>mpLmXI2c=~{q zY7v%D)L|B!zPZ z%t%RC*1g4NSJwp;KVy)A&YJbGQT=BtK2u1D26M|KAgTRcrZ;P_Y+6sAf?3$8jYS1m z$XO|dSbkqNFrMDpyS_q=X;xU!y#2&F!7tpf?&MKxq8PxCS9ym)b^c>VFnMT#m^GcS zP7I0M>N1|ExGaMC+Zkl6nkS^>5{40lX7lvcM8iLes{2%B6>9!WN;o>nF~;&FuXiSv z{2A`-1g~F_`{t;b(MXSAfY;T24e`jQdH_j4w!d44<%s=HxIAi|HDDuzdzs8|ej$zG zlkiRAFUK7U_25=N!!nuKPMHFIJ5-&r5hx3OnDG&{=sCRS`{hkNGc+{?P!OMCP3_2| z*)fOjS+c7F_Z|kx6}py>-z@WSk4VzPheyl!<9ggs zsgPW=F8V$CITRd#@g+TadwY9#VJ-9b=v!YT(Se1IFrN+#U=AF^u5Dh*2tvN7#dR7g zN5prRA?)^RKI3$5U9AJW9jQBSE$IH!2HqLu?8tw9|NE>HJxk8LwAJ<&w0JQlQyCkN zP!)uzDr|t{B(bhkY$k%+;N7VU(%}woU^h+C<|6KA2!aVF(8)zK_vIZAZ|1ev@Y79d z?O$xn{8E$j39DL zPAV>=__=1iMDlwopR#MFr&=LLi@LrQ2a9(I}yVTZrBrRqcKB zF|<%;6iBEg7?tt&jH0OJ-EqA4Gb*AF$WUo`1H+;g05iiH{(OYq65j{@my~?v&@sjs z>HKqpJJ*p-r8xB6?S1T@S0EHHhY!S+{k?C-?Pn>}z+f;~?Ves`vI~rfV!3?JJBkul zLMLtXkV2>PO0*Yav2RV9))|;m26N_FQ}Swehgilvr&u?m3rH2zqhuTRMpjjK#9gGA zPc9F%D1G+#$*YaoqEk7O#&#?rMhH>04c0e7m8UBq<1zvq=9l-lruvfpU8%?r#?Yac zmIzQ@r*YNhrb1>VVT%{|S-Z_M#8`vxVPFuq@U8!(bJLPnMWcEw+_e6-<~FV7_uYJ= ztcv36mFAcr9y2sEa?ufET_(mh)@iLd#(_u)JiS>QuV_v=iMuF0^Li!d#QTwt!7Ncc zpwLD?!kwG4jI?h3F#_{nUCJ+lV!aO=7`s)>e=ICx-x(7~SYii6xzSpD^7#?*vdw_I zN}EvCX%TV6VLVwKJecbq`S7KubcAa_v;(w&cTmw-UCeZgV0*uRqnuA3+&mny6_Ta?4Cb&MT?BO#37)Ctv9RBDd}&rPfed^83y}#d1H;>Rs`-03O2*6;O6dRj;)`g@8z&yog1s>wfO}Wtwc=CTA=&M@p zTYuPfW=;lr##(-r#VDqAD>z5!|$RrUR z^bKBhyQM|i<}I4AT_)Uj;*fT!q!qqZFXRR}9QRX0`WJ=13j1pCK42)rc+6(tc8<@@ z@WAW^Xb)BAyO{UDk9dP(^a&i*ihw467-7IyKewIrHOGR#DK&0bc_qn0_17|87_Sk**Z z>Iy)Ff!u^szt27gGS1VJz~y`$^|XOcb`N>wBt|&yLTT2f^yarnlWyrxHw;^l!q=8u zp&d5dYJ8M4xnHOWI3vXFRe{L(qXT8~e3i_AbA9AP2)$aHM}Lbt9<#<9?}ImLQCpcr z*WXR^m3+<8a>I7;m=s$Ld_KB9b^H2iy3L8GDO?SoY~N`(+!A} zd*9ZKx4F@i-Ca>M;bExms9@BP9`->K#NR{OowuxK)N|dR0al%bxY(Oq5@h@h8Xz0* zuKPNc_t8gavc-zC?B>&3Oy=5}=W!Y2T71Z@GnyMJ%u=Alr!s7Ge59cKyUuOK)1LxP z(;{G2OtV~g9#O~>ofat!!wWS1`Lm@uX%*m}=9Ay|_de~29XA5c-FLXW^h7iL#;b<< zeugor{ zd+ruK+$M58Sa&eolsW27Nck$A7+`Sbsw)%o!c(5wgB$RZKi?G*dR@weczK{thCScI zm3g9wX~k7cz9Yx%Ph8{3mmLDCVN~a8%IH?E;^Q?WJ$?K5?dvh#GOWu!`1bI~mc5K^ zuCALw^>~(s{o~PR8nU*!kjL0c(8J~DC}h>}b|Er-AHA`L$Wtss)S~J-KB4~n-kR*v zi{WO+LlLVFi9NJ_$wy=J?>Xuqk>Ig`3%b)+;k67$rq#X1;P*~E>W54du+t6hKV-q_ ztA*jwAYc8yk&z>d?`0LeKH~Z1 zABp35!8CPLP+nWS*Jb$eVk8`A-}iBhUrYXO?jK|_SBu#J7-@CQHk#}LUYcF3FYEG| zG2Q=Rl_ zpLL?4&6Hof@0i*W!jLEQH3F<=9NHm7>}0{a$v@9*1I@QY84`s6r-6LF7q!#>d!x9k z(vs&;LT4&YFw(gII|D&n-#J_a07`q3;dT8IuVV0)G6Yb2E7v@Guo4*&9r>o^g{Bt0 ziljMaI{m#xl&B`r_c)FUZd~7&ws$CVm)K*O+!^&-D9ycZOU#JU<4T?tj6^8dO`E?> z)}woFGkXG=Pho#c*{Zk$I0AQew9D^yf6atInfLfI2Kp(dKDXZnBezPT|fJv zNKZ~t>g67*Lbm+75|G0DYKr&q3ymDf8=phPRxout^p<@^i#!t6Ut(9nfE)w&=eMkX z;OZTk#wYhx8XE4xI46K{ossY80bfcyjAiqZ9Bl)IOE2G82cM%NG=L}v79@Dj1gki> z8%&j%9_sg=b&esRUUFZ%t7XUtJsc8aXZv@HrrLO)z47!!wBf#N=RS9?L3&XpV>~UV ztn?$o{c#~#I}5uohaN9+>PGKcXlZ9nXDTg|gVjs@o7g&a;<60+cA$JnCU439&g~C#uU6smTR`=zi+nQap_RWz4*tB#QH1jyUuJvn z8EuO339GyCka&~Wllyox)NURb;hiCWhp&N52G=uc68rTnJ0_!t#-y-hcDZ+|^qCl4q`r>QI)?(ocD%Gy*{zEm}~kONdh8T73)tY zo46Yn&?k2N|AJTWI}(%PqP|ZXW=e@{0a`z=32s)6Zlf&ct`c9)3_`EsAcHB3f%*6X z-V$!dKjUr+xNFr4y+GgP{8e=TGBw1y3cS z;r{#d`^$dBVP4Zkz2hlRM|Ilr%)9oTvOi?)FatUy`0t6aNgRnSrKUCF5&T%^ikPh& z!FIaNA*B$O`z?k8&rp}HoK9g)*L&&t4)MdNbbl`6Pbg+h%NpP1zQ*s#wD*b@O!PcP z4JH-d-E$4)~#_#AgzDG`OfGNP$NpeMkYo?i8}M54?sOYg~?qgjiuP zjyl5$+^~Sglyil05Fh951cM}eY@^juW$ zv7C5-V~<0?d~$jHza6jz23W5%I%aT6?+cB2Q8IR6#>q!0pvyr57#q2L+$8q#3Wdy_ z>!btk$;}zV59@A>VY9MUgKA}@S+mJnbDSx-D(DKNh#MjVqX@#StB-J~;vVVEqMs{{pjw6~LZUe!q~5 z?>=jY#hD?67$^N8fW0(QZeH6ELd8Z^l?m8ukvcli=V_4z+y8 zY^hJ4qsJiq;D0&lpqz-^U?Xq$-FXY`;+ah(G30zc1p(V8rC-zjd*-1y=)Hq*9~vP( zxu0Jwq~hVs5+7sWN02K;(QF*L((9Papv|08J!2W|!!g5b!z1-}Th@Ie-^KX-MC1I) z!O1&wioQ0bPy_>MfZ23)E+yWF*K2d+6mUc@GpenhT}vr(Y3yUk@ZByj47DcP@=Mb=$`TXoFpSSb#>A03S}%cX=YWl+4t$kj^Mkz47cx~dUFT}jIsVUZnz4U)itEbdqcY+L;DEw z>s*d>D(Tp-Nw?S&`HsYRi{2*KYhC-A5NiGvi=qN@Q(+$1p^7;=)Z9v*LVuDa=Iyur zRa)3wYyLUjE~YLo+MBDDaROc6s|tAro#XiHUclncJ98+A=QY$ zmwse>QyV|)`y)qPmHd%yW*n>^A4&tcz@LgliPz(Uh>Z0`5@#n5@V>CIVc4Bfe3kL-dXatJhdnuky$brOxO6DQ~&MnkHD~>+o3t zwdkIE*A)1VrSo@p8&)qF&oQXD(m%&&iP=KQ{Sl5Rzbp7}zq)D^MBA7e<%4*X)BK5d z6kjZ3Rxsj^w>`bpKDT9wp4%`jcw18s9}O+xf8SSyU{uQ3KAn*6ss!{&;?<}p_Xi~g zO0CiVPqg#>`2EPnEwmgRZ@zc{Rm8H5Tg`@wc9aG{AzeJ3FQN=B2ClzHpWu*d88b)7 z*!@~7)ip4g7!Jk0Hu9kUyV6XUXn+^bA$_=r5mnFBrQU2WRTEC5MWUYy7R15O5P*#} z}2wV1r z-}+4aSU^eWp=tB|`k0?j{v4m?A}=W$9;bg+yNzqwr4Vk@l>w=0h1d*4 zt0q!iFc=>lh$`N=;=Pyd*NLupEfD%TEI`@D`gDcH8+}C`W4cVnSt+K2v6`7mjd>7$ zX8z94>x7A$Lv=_r_ZkeL!?G!5z!NaCg_OY&uBBt%=pVMRM}FR;(68}P3Ns~}c^^K4 zfNDTJsMCpAchAc}(!D0^^@`)lVwb(jq`L`}ee&^WTPrB9qEm7l1=X72JYnJ^V=o|f zVK=hpcV_X0@SYJLU!KX9HHk13ZOHc{z0aCZ=otUMJ;-^+=dciDkY#*!_;3`^V*v0B zlYwURX0nsu7x`SCl99$RzvCl(t{Lp^v%t+tk7CE}pmiR9)SgFTFo>(!ZKOmc%^r0W z9epo=9Cz$}@a|H@mg2g9<}Pp=Dhx8DoUx7$O0{ zKT%K2gRc1=);Vs(t0=xJhsLkMZ7Eyk!w#cs%4h-rL1%lvw!0-_zYxwCnm(fvwbZY( z-a_=XwrUUe-6s@<36#fOIXTMnFDQUzT1Bh zcJpa;IQ+PbqnciZF~BSI^9)vhiWlMr>T0g)f|PAJH%ztknSZID^pA~n+}I!0*mhp? zR*kv=d@8if&EYWl>%LKzz z^X;XojwqD+*so$4|64l4?f(?OMBXzFhIvSt@R^vw8PdJ=I=a~ zMvXwr>lAHcMjAk`5Lsw_`O4^nxtDe2 zkBI71!O6aeRkAasnWQ!3l!S+7eO)Y;gX4F~bZZJ(C=peI|8srFe|?1KP1QE4P`K^# zwq(fX@QL%xX0X#*sI6l(`KhPBCiR1mZAd|2NC%3ZaGg`;p}0PgEk%0Mg|KnsOJ_cN z$_PO1xQf=*+P|G$YB5-X2|>Abzubu6+%s(U>I2^A*nS~=HGnbAKoC9jGMz-{QjV0% zY1@W4Rt;|oZ%`Oc#SUe`$^Y>Ep5iDDg>Q62#Y0eW-((LVOmvUs2`3k8x&5XU&ujWh z&dZE)pFK)I58;9&tor~oUEp*u$TjtUOGq>qtvz~KIbXcMBXy z>Z*XcyPfjPDtd<7lc(n+-zd|5C7pr$_H_fvoknIs?+B3$0OwhRPZLr1EIzOJ)wPo^ zlw1ROFXLhENIiy+`J6KYaFy71d20C)9F5h}#mFcn07fOKm7(0;-4rbw#d4|gejmQx z^eL`2V}m`P9#rfl8YHUUOk%0TxQxqk8z$TpOY+LCZA!2cain0n$kK=e8$*sgkwklX z<6Yw3cQbU@q}8McJMLc`6PLhT_zL1%-e+ykkX7&8QxddQk)OFW7EgG^DbWEo}VeJ6;|m3eP(L28<$ZlY~xj^bgI7q_?2}1DK!8zNwxCa zRRJ_6dc-Sd?f2;ecQbJQo{&a?bL`nZxZyZ#Z;}Em>0x#QXb9!_RIQk#6n6tiy7K~D zsq4bLe0SFCn|1P}{l7})-G{h9yX5O|{hpua&yH_LWzo;jz`hUrdOv3^%p~YD-b{wO z5YfeFZ(L+N*?ue)d_rp?!8H|;C1%1S? z?!Q}_+@^9DzM^P`d}ZSxT1h7OGFejV_)5SHxfNaLO@lJ$>B|6!z`Ycu;2LUS>MYBo zI62hFW3(tW0knf2bbUq`}Y(x8PMy0dz2i_*DD}i(vJiXWWOJobS_~ zgNes~$!uusNa4i=fIr(crk~u&j4dhCtMi~XYhX4QV@xBvFXV@u!`wVvK@2s?(K=#I z6{nspx^S%~PWVx_E3^+GHj#$^@G_j-8NqY&rQp6_3pTmJuTMe_q2G^ie8%$XHiJE@ zRhAK)AWS_hOn7bon=H2~ZlkV|V{lB$VS_AXxVsqD@c2G|P_mcB z#ddfdx9+vol=tWa0LiHWQ#~P&ov)Ru#>Em}#`d*Kjo(Q+hOo_}$|oJpI*xH(9s*g0 zt$yYcR}g?AdwW5jw-f!v*0pDcf~8)B#lO#wu)LV9*fuYs~=>q3Hy=#bjU{8CI~hecouVlS&>R7;_~gax3HrFN?#mCB3n~t30E> z0aB8H;?={Jbn0ic?qx9PjgP)@7`rKVcJc0hXaqud1M&&qf`bvAg7( z&z~!!`^xr=@m;~b<_pR!2-d8$gj6F|ORmTQ!hfGC86k z*I0W#19XCbPvrWu3kSrp_FFoUepM8HT2NzR)BSzgNJ|JRVl0ePm%M1>O&+z6>y%M0 zCp0F7#89_z>V(ngA!3n9Fk&8}B_6D6?I>erXSGgPq4tuyGd=Ax+%3M4R5S+iYi9a5 zebDGT%ffH3;EsrAWPbf#Ib+}RfHAir1FU{(jBaXf`FtP?#`8uJXlMnCbqoVa0P%-)W8Loffh!gvZW(s zl6P%o{AdSDZF`44d`SKcw^U-qVX8Sj&6cE8(exQ@*OhJzU^o2lPhb3})V901O2TvP zWooi<*7%;Y+*ueqv|~#Sjv3}W7%q*g9^31cLNj0CRIq&}b&!6D_V&q< z-Z`;j$>IckZ_b<_u7WL$c#mwLoF$7eg%5emw_Ez%F{nLwMPf;g-_!OaR^s!Nske^q z@5(0S_~a#0%PK>tD6XMq*iS;ht()yL_&r`jDT4Rblm|4gt>t)fkG>$DbrG$|k-iN4 z59VNQiNoaVq0ORLW3O_J)y)BbwwJUI|LuFMQ*(drSxC-UHf9)s z!>rk(x#ARUwLn)Hb$lWA_qJg)t2ceoX%j&Sji_~ zAJ-?vL!hB~5r;Rkm*L*VMQpiWKWw}gv9*!ddtJPtM9pQSZYu0K4}*-VOaX7XzY#mh5EDe*Cga^;K#6*DF;Z3 zRN;OtrZF-Fre0GihwzD)_%s2|;0gSHhK0slQwPpmh1(F$idVJMW`kL#02cC5_4Vk@}Js`xWAmvZ>T? zFZ}-a#twY?Y0TIdV?3zF_&d87UR;6E6ugr#xWtq*y^JT%k8c97b1qr(atYE=gJw|- zqKu~UsX*qS11Ef+d>nusN6_zE-22?-NJVI5>-ZB;dU9krpG;?F(4#0m;^%g)cwW*BW{TBBH|uCNQ6n(_*@I`HoLw@Weu) z5LmJrsW+sEVWJ&~ay(WA&ZfbM)^qUJM#x(v@0Xz}sem^9c!or6pg(@VH z-prX2@UEk?DjB=2>-*xjH@DI}Bvr@uyb|jrJ2zH+&%|1ZKOih7=?y#10P@N~!IT^- z$j!Ep)zMJH8185>Nrpo$_^h(ZmbMVi#%8kggr~med#rC)^hIpn<*HStS;Qn3JHPzU zpNaOCwV`^=Q+2A_yKvPtl7S_|mSgld=MbNZG2=IdmOF6*V#|Fsj1=6vUoO;R6VD=%8W&PI(BC-oMX2C8uFDFKl})yc)eA(`?{+ zV{H6u6cFT$fs!#C*U+Ug;KIaC41-J4SK|ua3d=1!vOQrSszYhE)|D&9lxQ9^^yg3h z)c$8jZ4bQ6h$`G+eGG z>`)T|IEqHFqM;$>>h;pCxDaEg!RGHl2+y->ww}4RZY^i9B!dl~BOWv#N8iEa47WL# z27T-HcZP3oR^L)am47iWG4~5hD}Ysi33}n~){wB?`vFIPd!u|L$GGqDregXmlMJcv z;L(ZJJ|t&t>>c;*9Gc#zE)bt(kGQw|CnAbhS1AK%E{B!$^all|?7wH9s2I&@#6!q* z{fm%9ThiEgQtvTwB{D$DyEymK(G2ehzNb~rp925))$GELx1-Q()8%jrn@N1z=gp3G z)8{M5jVeE6yrj7IXRPHdaw@$hf1up7-=~klwVT7A#s|S3WQbZ^Av(wnqr6mdj>*ZK z6lYcyOAeyzM9Z^YAej9qQR0rPHi2(wEulz!8n&!)-W-^xU8l_51+acvo<_-qxt;~% z#Z2DZe(ZkV0(UR)`Qkb4x=|R7d{tJd4(+)9?x-tv`ii?bL&s~uk24^Ov_L;$opot# zZ|)!&-;7g+4oFVHzn4;#FroCN|P}*gx}A?Fc0Bm8SIa^B1$wjdfsJtnEXbM@)sippr0(w z2d|_OZl|?q`a9A$ng`iR-^#%$!HHf-@wcB2Uj528 zprUT@^E5S_m^yRpy4gF~O-2B0la`1yJ-mFzJ+|l4cuCpZK!@3?cR=5CIq!!mUDpqp zP=VetdeUA|Y}{Uipr5xq&-Dj2gm*a(dCs+z$%Ls#kr)xp54_bi%GWt`L-Mc>oywwH z8p)%w0PU3?<`@UDMqwD+SMT6%lwoRuAev&9xA9TB2`~BR`$kje#4`gn_}i9q<}T#6 z`ab+mqr&Y|aQFU<^k>Z}DP?!mPd@*uva@~kIx=ZE7=-A6$71T?b7op}@2^8R2^YUM zjx+TrLpNR6`n|j_=CD5phx<2%EeMj$!1v(&zMc1RkI}I0#a&^GOrRz2(ws<(4=l~F zjkmHTI^F^PHuH)7ryJX_mUtH+5zVP+p=Ni2!rIReq;507I%dGc-V4_<3h4W={7Oex z&QRsW4Dt?vYB}UDAJGV z#RQS5raq-EHcU8TrK5n((S8U3*4UtQz0(2BIM(NP1&^}M4Q>8{Hu@gzS8=jCaQYq8-npFKW%5RqQ2cN8W)m2A+qf`j z!8kQ2#cwKR=u`bj(3(q}_OSSxJ!^k+w{PWDq9yxY4oOPV{axTkm*t*a825*Df76iB zeBnzX9)^t%%`1l)kL%w!8)>?aB0wh~gv5t)$%X1Thfr?ePma&Q1e5cG+k3ri5oJ2i zOzrl+pnER{2jR#G@_o<8*r><;KyPx-rukAj=`pJBX1F=l*DQx-Hj5LX(JHO5uAbO5 zpqS%XN1Ta!l8AnmwPtd3XFc zxm;5GvP>RiCi`NBD=2Z9!)9^rWsstb!Z*h=6x?tO-o7UBQ0hH8y4fC)je-I{t?OM$ zscSUM%%*Z}0(!n61Oke-0?b^3$_5ugV%Xm#N&KJp;kP;YC83;mV%l*a)Z(0p5!gFb z$`zRhp;fcbSA6GhBER+ZzsAp!FK*D*g1cXDXC+H7o_6jGT1N})wmM5l8TU{_W7;f7 z+g0BC8dIF9H-@F};yqXrHCPFaOr3t8b21CBh{739kM99m2_&SM#P~8l5Ezfi%5Grf zGgR%n`=3_Uq`y2qHziHw=gsxhK}Hqu z6wj8Fm;`0&)L}OIu|y*slEMG1OKl7<bpe zeVK-FD`{E?cjlE~+Idc+yo8Z&u|86R=Id@~@;#@=+9J|-!DGg<$f82xV=qj?=DCwP${k6nTQ1JymG2r1Y9s7L`64{ihD1*m>rmtBxW7T zojL}@GMo(I%y%fXk#w{?L`2(YOaEcJk{shk-%PaL=5w5ZNV-+xf{e#SdlJeRm$l+c zq;fW7Ec=c-wb@DPo-+*`&2?^u-^V`^1l7Elu&@gnXBk#DtS-=GxAF76(ro55)SPz7 zkV|F*j%JLqe6*Id6W~tx9~QsRroqY^hUdz$;~!koGN9MLyQ@=JRM^!eJz>z7jd`I)`5`LA((>-c)CF z^V{(X*;1qRGYN%H44v;kPm_}WeNJ~GHG*^R`iAGoq2u@Pzl1>eCs*BRp3%rQ{#3^> z5diBl-7sF^EfXRAeKn~dxH~P)YJ|(Kk_qOXGcv?F_>^5cz3$d+bZyy>e}6{#tuvrn z{C+0a>rww*KJuY_<=iSo7Eg@g=)GOv6YgQ3fHsdJR;gh~=2d41JI@~PNaemAZ;|OR zb=95|H6$?5UCU$UQ;ZkEEnQ(YPr(n1sf_z;S8J4GLXa-^RHR&O2Gjp@QWz$naQ!a} zesI5*=6tW$MwCiCx$7#4SJ#CkrF~%T5xje}R`@;NeqN`VW62$*1bimP(H z)h*>|%>9}8tIrYJJs%{RZNq@^si8IC3XOSK zxy82bGZvQjGiLA0r0}{h$Gjm6x41A0zNW@Ud^0>JxBmFyyOxd1ypuJTA^#1QDLiIO zNAY_}VU(Hgv(Gt_&xF=@oe7sIF!HmrWuL5L3{h8xvuCm;_Hsr2lb5dh;Sj>sM3BIw zMPz$<$ffv8^}01VWbBh(;v5#Jg50TvnUh_}tUOPK6snmm>3~QE zC++{rncRlXk_GW^zDsLrS1(aVkAG5RJ;dcKW;H{bsWsh%3>NRv_)KM#b;&|aYv&!Q_?tsp1p8Rr|= z#V+hJxLnuQjo{evUZy^kX#U|OXnlsdP^Q#zEidaeuiHIhp5dl=5TK; zN!N-uz5e{6_oVSB={mO6`?PV!zAKEt)`;-!jPIuMIdXh{(}XQK%a*s1mtPj_Wq`N6 zY$pVC_7X@g;}S%d&X9Y3jaYD8)u{|~hMR4S*r5S?E@JF|aeuDs_weToSv8|Emxk#+ z_K7wto0Dd>=P08^OPphemM1znija+l$QPQ?(*WpGop5kwm)vqjPn89~w|Yi=c?pQfs`& zbqa@F9^sFwH37@_WC54+bxm{!f^;EGAgO;7r)F$Ab`-uBPMjFSxFdE%zOtr@SUXjZh}_i0BM7uc|AS@-b-G++|io1(pk z{ccFdAUEj@5A)JfuN!BKD?Y0pki^2XuxG`Xd2#|fw=u-7vf#*aysm+^JjB!9Ncg># z_=7ssuVYPkzU>MUY1tE*3O|e@nskeI=iXG~>r;NOfddv(Cr{vI%uv z_Sxd!?Gv&LV;TmR=s0ZX?}ksRe?~3fhW6X8r5YyJ`B*z3ZHf2c4&+PPtGXbGYOI_2iZ|CwE)g$&gO)#ipqoz zumcU;M>0Q|bb#4W7{-x<3&A=w0JZ{PL1zAP2f`>MfnnTH{XV~0?Y#~OBqpMQ@6+C- zdp1ogX0v*qA0B`D_v3Uugoi#a2A#(lI#ZIg>@ z$X=Z->YU}ns@cq;1fv8G%r*CihDJLWL|>EhtA&Q5g-GOOg*#7~vqp%mW(i>;l-9Yx z(uQ1)60s^eFBE`U3shC>lf2%7f1)TFeXqF)WhcZRV9n$7{E{G)i*64M*Jz~gw7>j{3e_ zrDY~u1hOKzV+fHYGXRJvb&ZpkDGM+_p49|^O<Ra0 z(v!}g{ED1He~&+=wiB*h2-?KE$0Xs($w4Q=j*?Bi%voqEw7i*}c)68a2{BFE(jj%! z2zk3r_H;YjI|$vcf_Tv3`g)}K9p3sG^J~o<(7aq#vwz;%BH^IxP&@xtjuQ$8j#!F$ z0m9e4?zs@BS_%r}5V&Te$a{Nisi`YS>I|#K9sWMEXfDs-$Hie7*5|2JmcUe9kK~_0 zi-Vgt6YB7q;_Z`fSVlw(PfhQAjhnOUcYk_>dqPph8uQG~^BUOaQV8z_hy^;L;jt8F zc~{-&SPW^BD{OLseLQ8(hQvivsfEl~rg+bKVP_ z{!?`!G|Rio59FNi^2?a~%p-EzgP6R!G}vz1+Q=(74|g_~oxz0%6?nt~|M&Fc0-x2E z@rylY$DV~vqo;qD+O%L?F;!{b8?J9h9y9lHa=cV?=oU-!jx8{O2J%y0?;&fu_O*Z- z;BU#Wx!?gcq~{ol=c|r3!Vw1^LV%Kc1~=_x)Q8V+EInZlHZq6;17-$oMvwoGwL8m| zB@2=(JE1_p`eyzUD_>jfMiMOYk3c0*ne=$q-261EqS7omv%Syfy|9v~x`ytlF*`~j zs}fh!nK=wFmKB6Ebr)UQ&gB0;)&g)G-g~msuc2L_9>+!3p8j6u#yX#E_#2z+e;^eC z4l=gy**n6k?Hv;k;*Y!StiAyF$2SAz6GgM$9Ko~i;mOF$SKGqhdLnB7dBJo&;{E@Y z$(l@o1f}x1p;`uD24$FC^!w*FX;6GgDOJc!K93wg(UEn=_1&)Tn9yRwKBn?BH`C1G7~do207X1q@cTWX2hYFp1o6L?*g)UIPScj% z+V6PC^)1DMHObF za+EfknyQY2si(D$irhjQHa5Y+F?DZdk7249Khd`$u`)>9kK;>6Lfa{!lk#>gKi-@^ zF~pM_5@!689(${Y{T{w65N|uJ$o(e)Eo6uTO~1)pllU%cWd z$$pO72_jxDA#T%cX2V=CFe7qH>HLk^wK_WE`PDc{rb5*g_=kvY;IrmTM5FLM{W~`t z=s<0A%ov=v0QZ#QLwo$F ztmv09TxAMIQtDhNMG>#z!OhwcE!Ui=7?!0);k>J~w$No;BHCepBX$Cm`m z!G7{Z<8QskoM>oJpy97%32tUWsC5R&@(TSu80T9xS+le)!#GN`jP?X4&4&)r3-?j2 z`^rS%knCz4&&W#@?a9)G`NLxHynau=*2z6cyBtmP0D%yg_!Sb#lWO_}c_Zu^Q86N! zoErZ`E$ip$X}r~NabM0!oHitDR=<4;T+lOK5d!hl_kUcV0P3jGi1R&%k$CdJ=aJaYw@0+3kwAIkoSW3LZTFj3|(l&q3Box@L4y zGME^Mb*u^G3KaysA78Dy=6POT-jc--o5QCpW?|8(o)y8(l0KnecpCx6p<`IIA5rMRaN5Ha9 z)`XVNN^~YvUzIPdA4c*UdWeB&NTLXnk70_BoFCd8mUbI*r)ht@Tth;$VR*aXQ-Zo9 zKK{~(?Z5qcP7lrNoZIX@e-rBSl&tudfnoWx_+!>&Zc@hw|1^KaKnNB)1%j+H=9lju zqI*0QX>Ve(@|mE*v@iSFWw+;n9pO}hl}W&s0*SxQxdi%xQOZ`#1?&DEK2odjnK@VS zgRdHGl5YykAp9HdFTPIL-`QVGOv6sQMz9V07eP8L$ zj-g@mUIXwy$>m0$4Hxj@jo71*XqVBP_7Z}8M(E~M)XC=qLuXFt$DcF^tTe4Ih+LJx zk>C|(Seo?hZ@Uid2z!IUNQPy#*=VGtD}2JJLho~0mD+QBlMt4hzTG?Ss66H88%~s; zF%(O@tr~OG(}er3wu9IDC{vAIcJY7eYB-YpugKyEu@D!x4vonqs(<68b<(bI{sEpl z*NGuGI^k8|w{m?b3g=|H)FF*8o|<4&9^W6kC$3OQgzxxL<5-s*Q)cW31(mIQ^|JbB zRK#?0&lh@$JpeXi{`xFq6Hadn+r$z&J}Le0(T3v4felLXj)^0Kk1e3!I%g6hehYnX z?KMrwRY171ruF5=?}Wmw=!;!5QRQvt8{o!5_Zm~9Z1eAH;s*v>mpidJzH%uD z((^@%xC9vflczR*Ba0-Hf~sNpt=@Df`abS`ie@aiRiM%L_+75rPqrVNHH@>c{+x=YflhtK`hp$b z(`LN`-+xbZl2-?{W+jHGf0z1H0~J0L`9D)rn9EXjRxr~f7?g3A)5#EoR2@0 z?YJmT$T+vcrW&NM=xEyihs>fS@u1S9@~`vjJY^PQ84GzD_$IZqMa22wBRF8Sz{6}; zu-^B)Vk8dgyB3{I?o6u6}$w+%o4hSj0Uqxxbe^iWqKzOIpw&1SBOC z$j)wZ$XgTEbm9R%Z7o>tfC=XL$DqTwLPz;CRVW=9d+fXSlX893Sfz?jPpLv==_1Izoqdvt()y8@Zh&vz=3;KNYb@34G*m=fC*_ zf<87gkndpJMu&5hZL9j--jNxxWYzB{X8dOq3Y3DxT60%6r-5-6m}bio-rQq@Nwzcq zfVt?s<{J^gacK>4IuO-!hV!YW89m(ziHR;HW01t~%(7;rx|r8mHOF5qPNv(z#u>7Ti$2F74_^@02s_)~FYr5KktIyA^{!QMa|kB=*0Pr=2J#xJ##5e7Sx(nX z6nr_HN4%&bLm+sKjRpEUQ+wwR{bu<~?eU%J+qf_2gb8uqzuZ={F>~5$n5HQ&_gq0t zgQ~7ru1K9hH&q9~ro=@*%Q~x zTpwFbM%-GE2^|FgRgT*jmGX>T&*8Wym^)))gI=!5s>G4Ki=Ga4`Df?>HhM_U6wi9-ksbe>Vxc%k0R~^09jMRz7`W ztQ)QKdkz;h{lCKAv0^vyqvr*uLY-%st^MrY2%2XYEb_WFBe-p9V%V%(D&Fq3IBnXA*IEx)4_;duEBg^NPCEc5;yeG!`e_;4T}H#DaJy0cAfw8zAS`^Q6N z_jgQ=I|qX-_b!w*N`?6)UpN^xA8~V)PQD~qm}oKO-8<2aZws17nhDo+q_D52Lb{`n zLg<;ADW6$o?u>eL}I+%xbJkRawMezNs;q(CqAk`Cdc+bjF8@gQv)0dOA;_HX+y zST_gg@S3aj0fQC|GL*0EdJ2W&)Q@8)--bm+Is*Z&-J zgAYIA!7|9!6XHIoxpMfIl$PRNM;&X z`Vpl2Z8*3Sk6HE}U*tjMJu2zr^?bDOSMbOI!rI>1PLHDz2cRe}^CNehz}m$mU9&%Eg11DCPp=PuUjdzh?$8Td`;FH{M%^ps3HR8hv6uI%-iRY`kby z|CC~1x@itygQKkJ5q@_QWPZ#Ha*@(XE*`CxAB&NimKttmfUE$3Au`4W^#x3eR)J*Z ze#uiIk~8`qXd*=1d7DPA+cU?*9__=-gk!eMdtjkMr&s-nl9* zH*?DZdM4k@5pU-8=H}hW$+W$1@Ba8_+;L1C$>2Bud-5h|_6g73Gp7av768Xu1mO4V z4Nmsd*j(Po_p4+Y2*c!v^ZR~w0LN-{(Qy>=FS&m94o zlIksqZ?-rqu{Ef1*bWuLbzbFO{@#%RE8tYKbc>Wrh4wvqn_nA(^Vu*rx;a}~kzpMg z)24zLfH&(h1>`RCktwi1hg#6izVhC|S3cuq9>Y22PG47z!RNl(ycwN0P6Hp^4XESz zX1V~)>q_2|P)Uv}#Sks~{(JaP->q=sG3i-91@Ef1a7WKa1pKmk0oRUT&gnw=r{Eq2 zF~;UL{s+$p`1Vl)Qdw-1+bXNw`@Y~OM}W`JRw0B*=@Ib1RMeV|u; z&eqrrcgjgLhFtDRzdNhxZql=m;ylT=X{jh3tRZmM+_vWXD9ZO(z?jv^Rw*YaT;sQ% zs&AK%HYAS|L!>@v{SSCdg+tN@40o$6$K2hU+iME_$qjQL^Fcz%oP5sTG9OghF@mHIY=i=z&~aP{TF~FJ z{2qQnV$On?BynDm{MBpv0{&y?IxU(tiYmgTtNxlx8pQD7Rr*g3fs3=lZ0^2?Ol@?l z<prIzJ^EU7Q~+!WwvSi}yo;-6Ctngg4wqFYmtTD>9?{${Bo!Q3S8aV6*5asO2q z;Br;Yi1fx@#SrXr)5I1R;kSyXn7BC&@!4c^`A`iQQgNm@9-TwlMdC}};hso3BsE0b z^r(<_?ngr442*vFbHt6K-!9tUw*I~M;U=+m*b(4+)QV^Z;tv7r`i%2D-j{g*1MV?M zt&g)m6vv((GlkBZI;H?c z{2K8vLYc+O zXyi!6a|faN?IxA*{h#B!0Y20nUdUVPi-hIG{v^9@;&~P-cP5=2OX(q5u_t-IJsuS* zUEVQZ0DbE~a65EMTc0K6RnTrB zm9IHg+@+H4nxjIKVm@GSZnr8mexDf!Ddp?XNGrN2y-s4zYx%Q8C!{@4&?Igu05gjm_1#~e8KNc>qQ3sc>LTW7e1Em_ zm!)CjVL*tR8*6uD;62{gI+hlmDFb=UN3^rUND54_qK}*30iJh`7`V+(b>yDFl^8O6 z@$cd1=sp9y$c)~xU{v{_^w*hH@0!Y)&E3nCseB7~5R|dCf8lHunG!>;kDpDm!+F>! zu#1WO^UVEz8@bD#YX<*z3RB(OONfIp?pRtA&|I$?0s#^t^z|9%WW1PbN^#*CGl1h- z_#!szIY7%L>VeLFmb&JQ^m!3}h+|%id{RD@ z+V49WNMC>Uz`spmz;k9?T!(i=ixHgR#_2OGzCodMWaV*#j(k}`8b{uIW{VmKWZ1)^ zxik_+&KCUd0RJ9-uqg`GtjgJX&pKQ)SXKp2?2DtCevDbrxUXRIUU_S56ZA{N%dh%~ zQT0#r+%214X%1Z#XhNhvY^pyr1`!^!x^jz)lm7SSs|wwUGy9zV7K?~Qh9cjyztiH0 zHdsvq&Y@+Vgli2fw)vx|GKd_}4F>0P8SC%z9M~&+5zTU7KgaM;z~aZQ%AKy(dg7T$ zpL_9ve80bYq@swr*>>=_#wF{OC}k`EeMh|c>o%?^NeBT+z10`vPI6|6Z&WrWKyPy1 z1KU%WU=`aHJqx*G`4Er2z6sod3I@4JgpV&H*db%fOg%Af}wv4 z{1%mcjGYikav@g)fZw|x2ZN9Sa?xvs=6nl}r(8dPo{!*29+5EK(cblg&3|5YVBv5@>zSvx#np3nMKL-rOte0&+JS%a*(BmHHe zqbM~~;+DDm6~CDqXHK3Z{99?qCzl%T@%SYe<#bt>tK_w zdd_R?XG^~z`NyXo-}J4rSHkaI)RJnnpuXiw1X`#}v)4Em6A_lGnWMd;c%07Lt=8i^ zbJNsQY!qMlUq@uGSCj#Ydnr7=PE>!|c)s=9-^0Iyzpl{zdHb0lokX-KcqmPo^B!4GIZqr`8rUy$d5xQ(36hs2 zt86B)>sD55Nb)ry2ulhCCgvZb1>e#I%@em%#S-QlqdA(P>Y2y&d5%eJ39pa^Di`v< zDN?D0&+KM7fHuiH(PFOMI8b8?bb^LOB{VK=tO1fc8 zW8*h9U~1aki^!bpd+;)!{}$5m`2_l=TdC<8At(d90Y0K7^7)m~ZnH z-~=F3;oS%|R^$6uu@ATI46)%QDv_m~$YFp11wnt*U1X$%w#L+Lq>m2(l1^(|b1WEr zrNU7)^aag3aS(^!Ng@}HyHD*Y7qv@!N4#sI?qc6vabt^z7JeW;;>tm`HEzdW0W-<% zHr=S^eB+97uX9O5zRhF?#Ph@pUzYLCvc}?<-^=(sO&Ez>Rg<-<0K8e=#H2no;p#*3 zv5i;D1eNivL!^@gkSV`EdGXj$^;|uBNQh;C(esC)j$_vzt9eVCHFmv<-`@q9Ma?-x z*jEQazq5VEq{>2(zZLYLhr3(8+g_|#9K41(^h{Sjmzfy_<@MIQPi=`+x|B@lWQM_* zDGGuhfV!W|dJDv4Z5>62Un@piX$%0Z#zFiBScTZ`|0 zdYYjsMG@gL(253n!wa@abdJz-P~~gON&=dl$ncJv&@!{GEerS0;X!xJA$l?VO42$= z9#>F8Ip=H8_3N5Yvatg+bQj3tu3%JeVZcyDjMBW(vj08#I_KX**$`v)uVAOAB#P22 zgrRDIm&hxlAzurnj9dK{xerrK?nB+P`% z>9^ujY#JLq8A=tY_|F6j>ed1B$Bc==?iZN>BF+7Y+zcI~Fy6g-{2H_&q#%Z{^!WT;0jAYw@)6(2eh?c|PS#o_2dLcFuU0 z6@?*=rn&Q*{_L=dqpoXX~=WC!>Do=61dNJYs(a0k#NNax8T(#w|_O^g5}Ht zwoR@ZoMP<03(8iLUc)q3eI)ISd5spfq}VdXe4W8)4}?GnccE#2?|#_6TtK!$cdw?V zaggbVx6)dB_jr~}5%W{+_wm9syRwYjJ&XDGuBCP&bl8-KhKeHeqpFR#>S)NltkbEl zS%mF8NRz)~tM8QtjBJtzJ9*B5ji{}^XCJ|-8aaOuP^;$xk9tlTk}-h9urb?}R6bVt zQ>`5{Drr6`vYf%)Ug%o1fZuHrZTOPN5QrLo4p)f8rfpHMdvO;yXiFvy+}e;aqeyC? zq%sNv*pvHwPd~xmm)Z2)@G_jo+E50Ip#H6=PMZYv#Tq>WZlvBv+F?N09b6`q1hN+G zKRN0^vlqs|!vsnY@suyT3^f;SfWZsOKZ>K?Gpk;*`_&dGY_9Q7LhJpj4c=wuV7gYx zI?$MZKIbEJh*s-9U=T+lu1Whb+IE!qy^P0g<_1tL^AmHoW0Y=!p4Q?@j=9I~_&6@+ zi#>V!qRQXN?#1Kc!zww@|9z#@&meB7?YU;c2mc`e%#_OH@ z^ZkR!$(69wXV`~4ZOv>5dt;KD^&wp-hShuI_oIEUE82>Gq$)yvR zXeDeTXLE2uyih{cW%2@z|Lm0wG}lpTf1&bA&*=WeFy7v}>amwesVm0kBq*CR4JE)s zr2nf1A@{)%&*XY&z8PwAs5{f7Dn5~ve%~0Z{5|?O6XH_kb+OK{7H<)srvb+?ey&f( zr2w7}#(B#b*uEe~c6dU|xEOY!T(#30WAkVq5X^#cY}va6dWo)coW4z@4>l zdtSkR>$|`h?}cbU4fi}>GRsm-IxE--k}f%bzx!v!tR`d*Qea#}CNh_1(VD<(`qI)o=E5P)Xxxv!0Rr(g`JYi8&VJak4 zK?dea$(ltEoH>P>*k(wK34u>}bM5&`--$k68djE3f1E=gxTpv0#d8#T*d7!v-6i*5 zd`oE^HpWn4o%Xe8(+NelTB4t_9;`-tm{NxI1Tz_dq*y{eI<|u1Wp%uTNeUG+(bNAH z8z;)?&jsh5slN(Yd$}|~`K)#kD%p~af%rqL#d7P8GO5R=bQepCLsA##68&A3FJbA- zjP~NY6VW}(XeZAYwc9iieF$R?kvBVnQRr=oItZ@9v)En%ZD^-U2l)5&?{q^N-CYCw ze7YI7St;Z8dlbUpdbk#Bbp(h4?ABymd;iRvp}Bs}Ig<~*2yAJYEY*@2JTq#ZddfExbaqG;=xUTn!bF34W_;t8{ zUzX035%LjKHmoW*tt_A`vhtictnd&4XjbY&-mR8s3^0|8Qd#un?i)l^=Q>RAOuyBj z0w+rS5O&>S)cic9iwXam$CBkaMq*TbtgA^WMk>g0-xed!?-=l%*m)3Q{tC4tR z#5@zXbzoIZRBN%h$@EYx5aVDPpXmC_N95LVUr?Im7)(ts!Kffk+dna6vZ5JL$U6lI z#%Mt~6X=qV`7Wf@fzqKKUz%xtULju7A`+4u<6tBd6K01YTbbY}{}+C)$ekfZ?7(($f= z;fD43g)ySUFE!T7XG^@0Au<(T&T;U=M|Kcy*#^AF-^2DV0+naK@Zge2%^26;eA zB>+1Nm-NI?1DTfIXhKvkbND;BCv@`>U~M#GaVdUXLz=ZW(9>O#4TWHM z1c~2K@Rx|UC63!N_oICm99Kc1c4lscTt4|SsyjR!UBc|Y-@4+CdntW~myJ_#!_ZTQ zSFst`f>%f@#+r(RLO^y$_&>SkG!iV6Wqm$-`NfC`Xri!L{(IYXmt53b5GyB#3jG#Ks?cSiTy=&tyc zned-s{Iw)RRu78-A?TTf-*KBZ23o_BV?N59NqOKeUD+IS4(P-;H9dJ8{klKCH+$Zd z=GPf_dprBLOx8(_c>=P@Jn+1-@&xz_J(sbb{n8qkjZm=`k1GIac=K#ftxt%XG5#KfpV^W+@gT5e7MfPryu(o z_@EMzQP!WR65|za0cYs2>sKJxSiVwN9wN9u{b7?9--t=eDlo&70nx+U?m6$%d%oOs z+-l?CbF=nBWIYGojV$PkjK zg}U-%JK~)G{=cn2VDy~dQ7rYQaOKcXkDW8?I&)JKE3J)j`WO@`(+OlL736_UrNuEr zR8M-#4<6dqiIH)ul}VnleRWp)0wj`2ZcTOjlt<`hta3DGAQUaHw{FRjO0f1j0G_O~ z7thI)c1fR*Tc#vF-sydUmr_0pW-(6!u(jB6d+GOo1GsqYLQwdg{oO&CAW=qQ09bPQ zd0T)a{b#e4Dt3f1o>7;UVg$=LV{00byBENAFh936_Y;Ar`K4xOGM-N`(+9|`vXKLFPgkPdkk~6_m6WK(-BK64Q8HqN}zUT7h;BU&9H&2l_QhMO?Q-#H`Jw<6eR-bUgVk52~Kks z0b?oFtlxhtX8KR7wqe#eZ6oWM1zG7H!($thui%#9)j1A?GJQ8H|LYvUR3Hb&G)DI_ zQgN^@b4_I0cUXvPp|wPDA3-0*AL9RO0uoGjr~s3Cq-f%HQnntL^;QsIs0 zyPwS?-m4M+$L-r>f})c6|^l;r%r{9gnkAl-HXwS8RN#dv2Gp|woK8dgk`MK?^1lT#qFsYA6Tf%ji~v>mUlQTQRwY${thfdcnQIaH?Ahbst-6o7FH!^n z7ySIJqc>{QS4gsH{%2Z|X>7=)KTe!9TpiPe3 z=b%iu-whF&Abv7HaEU2jMwIWzV*%=mf<8iZ#d9rVa;~$;2Wb7$)#%f-nj)Gw&5o3u zk~eSPKWE0rRbM{Ukp2xfA0bR;$uqX=?p^jqj$a%KOblg=-S>`g*-l1DM}q{4fq zynd_Dul#Dy65I7;_;hSHM&6`1eH+{2o10f5q=CQaDU{rSCqXM`Ay4NwqTRMhA{TC< zI!D}{z;DMSrnv#YC&K_ zV2cXLNcp-a5O}SrWtaDRf_ll^;b_s7p}teyHtt7Jvt`6S#RdNs=IDghh+uEH%h@D4 z+Oq&9)ix#F-_y?&Ktk^lkoipF4rvC=Vibf;HWQPJyDD!b*VkhVYxcp);nz|MSgUR2 z))Cf31E;6=Y7Z>M2}nB2H!ivhwho_E5R3YlI>Y-yYMAGC@Pt0C*`h}_)%6rdKOP?vz;E$U>mTR=@w-PT2n7EgcUq=xv=8q3|B_Xj4E}?> zp7VQVeGT^Bp6vDy^sG=tWak5A__`L{WO%l+^~mG9?{vQG4`54gvk@i48d_J@(EsHv zaS@b)=g8dt{L!~TJ$iL(@a^O(6n7IBd`t58KxRPRO9ji6S#f(ISFI5MIrWwHan071 zrs~SyBypL=_Qv?QCptUmk+&ZsOI*$EZyo*KASXe5LoTo1qZWotF1b$qzxV9{vSLaZ zeQbq>nktvCM3O!uH6eGBG8?UInPOzCEG{+psv#y*eMZ%GA{R<{^AUBO9>Xq~*$juw<;;@H+;=n@fEgm(v}OIg4NnYQ zmjA=KG5=F`5625#?4P;HmLbo^@as`gA+l$Bh4`SI(?f1^kD^ra#3%Q0pk;gG6)r58 z_W#?Tg(EB#GHkTIqHBC|lu-l=$iQuMTOz`~rZIxlgNGHB~-D@o_3~)zd90?!70qnh_VZw+gpc z?~5v?bAzaOzm*~(WACdd<6oIcbAUST)Q?>m&1q%6#FS2rlMVSNVx(5MRhA-VXQ=t*bLkXd6E-TjRfDkRa;G&YQ0Id)gYB zRG?Sp!{Zd#_)HlUulq)sW5oFWt@x;hNnDGhOZy(Blvq8 z8spMFJ_naAzUk;zrqDl=TCd5cTKH<{l6T1l{|eV$Rs{jcCglJse{ zV2(R*>}8>~wT4Bl$M4~c<1_0UYGCBIl!m~uF$OEC`}%%xHnfqhAm6v&7x*5x!|qMe zN~L?RGrqRSjBQCdYI(R&fYh(BYx1<;u}8)maIb)NNs@>#;~yy(P)uSd@za(V1J5lp zbIxD+)8rU8Yz!%s8U&Op0z+f(t;ketwL6bx7FOSrw>V#ipTI_wm(+?4pNz!zKl0Zq zoo6k2ZT-yHj^H;rZu>dcE)+>s#+9&y+u!kVjjfQG!_gl*O0ahU`h#6po{ew4Q{WBK z&S0CL3}=_}P{Q%dGOhwIzvr*NV@E7#_RK1= zw*bp!og{fxh-2zKCV;9szX5tIE4}|kUKF8KPf?cb-_iqHz~WuNMJt3$xqBH&Vk(zL7FqFM6H z$5*V};Z6FxIxwatS8pl(z1H~YUUo)gws>KzOySX}Cy2kHnGDV@J_&}T z3hf7HoxROpPaU!1(hFtb+F;dw-DJ?6P!N(Aw6(Xx=*Dm|k|*{Ex`uKV?~U8Ee@H?p zezf&Ve9djCZ*k+j+hQLCDM&%pF1#@j@0Vh_?}=s@2T7)tj=n3~1Ec=tzH zr)xesin8zNk0!g83e;8YI5wW9Qzhe_-8e4g=qDL5cV_PusG9w&2`is+B^f@H$gJnz zFp*4UYnl4YT3#Lt_$wInw;A&$P_ee~t@ymq4s!g}gIvu=rGu$?p zALDE4iZkQx?fBy^(6^J?ouxutd4rpU}$s1S-|tSYBGkfO~g#ZF|wK_q6)J z&J}ETf2Yy^{y$PDkMxRaPr1_HpY0N}xX}&(9?Q%&Nu!wxtA|^`A`D$&?>!VVT2Go3 z*{jR{xr4!%HT^lGB0gEiAVb;;mFs^%Y}PS;b*4%fRC|L%uK4;kIL5dKqbKY#XN^6& zOs|!%fuZuR!ka^XbkR9}{N8alf4m;t!jv;uh*?wyF%I89Y7S&7@0EWG;+ zZlivXX~ZDouk}2qCnnOoq|XS7spd3*6zSl09McDSHO`)QbKcyN^djoMFYJ_7kuT9Z zi25j*LXFLYg;mb299?d2S&uD%yl@o2{7L>68t3IqkeC~OV&z{IP?Z&p%jG{APdd$Y zOi`Xmi9-?PYou>j2HV-+#5N2K#wkAg`GVn|ss7#^)^h6gU@{gVXg1bf1FP@du3KWdyW#2QsIrZf+BTbnWav7+ zQ4S4wvc~6YQ4j(*_U4=*{wZPm-Jo5dtl8(R=(z>q;9dh}&>xCqWO`p$h3U&oMC0W% zM`ZxN@hgG+#>0w5ZW$;{_SA#|g~*vAT7S#9n}Xj2EiIz>(m77>Z+wbje&nP^4aS-4 zt<8w8h;e`AAa%@*apYzDHZ;Dfy>Xry`=PMZVbTH~g(2)+=c0)aTaLy@n24*9Bx!YQ;}!jJ%cgq$A0VRVI@oKRGoEN+-Z~uxNy?gSAVN_2C7ep`iOlBa zNLTv9cLrJHk6IJZ3B)MxE408#J9DUm}Wh8PP zb1FlG@6n?Kxg@m&f?8_$xN{U|3$hC|d-4(A%4AcV_VD(}uZ1@nc}MV|`8nf9T>;6G zXn9(vC7Adwy{P`}CI77bkl221kdW*FzPW_tc4A0}#QC=!`*|OpQKlgPuhGmn(~ZZ$ ze${4dhiP=~$ukvOy_-$vi(&e-=7U1u@%>&47Yat$l=qp@J?+(I^f=)@lR|Ejeuf|O zJu0aK=g+h3zrPxy?nQdW9x6v2KV=EQt{7TV-!3;W3JTu3O~+t{~&tk&^B zSzabh_Ex)%OQ_I~-)Eje-jqh$Xmnnon|FR5h#Z*QX9`7H^E1iK#vR2%)8ibRb?4!8 ze7VlR2wOQc1e*-i<6Z&}s$X$IsFpVa#Ho?!_VAkWBd-%iQRkK|rYp)Bk4wTYM2Ka& zWvyhmyyv$B(T@R381wlw8ekc8?KN2W@;WK9vsw249QU?@Q!0)bg1&vTo;?~)a=AOe z(od+BS>kSfoskE6v27a|KS;<$VbXyUBB@&ZUrG-F zw~){`>*w#}YZ2htnUxsdUp3Pc+jY!YQKjU@qJNhVdknt%kiswx-v1MH{2yj8`6X>p z0>yoPf7Y%Bh}jfp*&7|WP2A0S#Lo6y3_TD!?MTvx0c7u?UHN~QOd zG}c(-fN>cse%w@`+y_fTM~bY=C2sh(+4?ZjUnY~Wv*vIYv7#AP$tjZCe2&A({vRiS zExqdLRFr=WV%MW9XZ`pS>xejZ6~(d*FYt@ViQsl;+0yM>o%JrdTK+-+;R&64LN5Y=-KoQqf2#H^7g zA!gF$O*$vH<&3mqGN&_X#!r#1+aj2L|7QGmFAVGmNGpBtZqL4dI>U9U-mO$!3zT8} z{y}fT1s^5PAcy~_;nS6gG);UXv%gJT@-PHQ3wNVSj&xacwP|vT>7Oqn*HM{LYs`Mk zWqjpp95YCX?DDc)jxoo^7C%d@p5*Ab+FXgZh+ciBWY*i{`kwlw%wA`lI&s@`PytNi zk1(D2F4e}DD3@>P8$jxH4(XLKPL)X|4hO#{ucEah>1jf%ZI@1+y;)rY%ZaVY;6E8A z7fba{x@RD6>F^i?&!B!G;R#Eysg99i5IB3wr-=YX;Kk_43G!di_HSL){c=I5Q3tnu z>k!B6S0M{%{U=@QH=HXquh|7ujHF7zc??TZ1{Jg!p(J|am8|Wz70)pNW2B?@7;JRp z){-1DdXsoF&ah7NW6QlWdsU7rCQii_{gsVlp#m4<)YlYkia9f8&OYz+1*)_oZ0K0T zgOziThT5#-D@Q+{v5yecak@vlXbd9Nd_051{iLMX$JTZim$Vr`@uaB>!W(%~>k?aF zbTWOxjd`E!Aisy-zOnI+P|PejBJO!jzBUI*E1X@)$G`L#Lt!n)-)nCe8^j7%L!sz$s#$<&M*Xk#uPNp> zX$}y=a0-FMinSE22RHhNkCu^K$Q*dA82`s}ygtwz?vI4?d4RQtpek2Ix#Vqlrnm8~ZBsUY2tPg07sj~9(B(Lt(zF!W#cCWwd(>2#3&b{Dc!21(DI4q@ox#9Z$SbuqVLhi;}b8$ zlV@?&8Cju%&wNjcwi!sgzq=lFE`9Su7738BrV~IP@QPu{aK}#*4D~U5WkmR#MCCdt zO*-JY#y+J?T6w9CV$$wIkaobYK?klS=3%s?b)gKN0n`uTywl#tq< ze8508BPm>TgKN$mpR@04+Ql2?3zs+1c;{G<7!{YF>;HvO>z*FT&sFRc&+Kda8xIBk zqiKp-`>UUZgltd?t?w?Z7k(@=?T9oQ#Lg@I)I)vR_+h4i&6jp9zi&FCUw3CQvA|*ag62&M-&5p&$Y&boh7gyd&-A0AOT*qFUE^HSMEEXcCbN@|9QT@8G^>%=u)3jlP?DJ?k~`JxrwB zzTcR@w{z)F1R^OTMjTgz`!m|g*zO1~jEcM^n34Zvz0lP&f@MZ}*<)YE8Xq%S#?{pjLOmtl1=ncFd$s{pLysy}*?+Jn!wauwfmoSH}} z*ym>0+Tx-7GarA9M%O-Gi z#({N9oxL6NV=o9g=-gdKie#jHvGlBRn^=onWOI57LfzTdQ3~faZ}`nuj__VW^j^rY z1LZpM|0^ird-e*ahv6B78J)UbiLxEDa;;==(-h|l)S!S35--4R3mJHZTyN=O>G2Ke zHUf3$kM<^!^XQqpJjk7cFukgqql|jGi0+yjuSXLFjm)~9?2c*Bbk~22yb&EmUG_-# ztly_fP5vPr`4EycyDGsy65?Yak&Z1+%&BQLPEQ%|TgNqDRLvOD(fTCHUPWzpNM{G+j1Dm|1R@!et;-;CZR>lVe2B_=Z2U1O1CB2L)_0KIVHDAC zQ(k7Y`qb_HVf|s4m_8UyWK6S`**YwZ?NQ!*KUP=!SX7D3^z8F`nd zbWDcG`UF%M!wjwMP1{85nT1JhfsWk-Z@niHP}oP)4Pl*`XmQAl3>zCQeF>uOk_0w_ z^JabW9G_ObQ}EY$xo->GLTvjUzFXib2L$l^PX7%wfB+vksI|T?BZ5t|_7-vVsVfG8 zFQ3ia&7JPT;?r)Z@E~)B9m=MuzLmQoXAS=5Rby^j@Jo3ATAGJ?8ml(T_A+3WEW1dT zahktppM8g76tDjoTVvfJ8@M5b@83xG9DmyLPdbf*dF19W_OpOyRMoXC&EETI^}@`) zv^?rKmuaq$`zIo+wl?kGqkCd-nHBEP?B5=$Wi#=?_3Ogu*?$W*Ohfj>!DhAS1kZ&k z2QGgWPjG>xO^jRFrq@j2?Ha!hC$;E}YEW0oQryAx+;`PL zKKc9dN83|GueKiv;LLXX7Ha8R^ zI4J;;&#!OoeO|3=qqZZmTFSa7e*nb?4aE2CbJ#}4xvre_n@v4paSLmnyOM21u1bpY ziMG|3qWmn)&D5eC5FBhKlapG@>A}#$d?AU-Q6@@3TI=UNkwpeznMx#60=7$g=0OR& z_kYKN(0|Aw?Bi?iab$r60&YMa@+GI(c53h(`>ewrIQe7r?#Q|aI9wDUJC~aFDA0<=JLsRtlKXf$F8l%*(IBCS*pkf`TxZ(qP&(?h7fn{t) zvC?T^*RsTG-(P(BU!!oza6G3Z?me1boQ79q0K~#{r>ulN4LXsh?{z(YM@yZR;_JyY z4wbego{a;;8sD{kc%UT|I^8F%Yd9@T$c1f9kJLqNR0mBb|< zkuBp6{=#{~wq>PQW9lj#|8o-Cvq3>0-m73>v$a6Q5s-5#S!R)^yw&eOF53uN;|5Rb z*?q6U;tz(weXJ9D&1;|2_V0ZJYq@WT2Xr%Ur;F=;Nss|cgDtlk_MCz?gn7knlD;b#>)l~jdF0J{5uiftMaxru$z>shsNtEe21(CM;mgFY6 zV%(qF9Y_7D&bnt_B7|Ap39P&SXhBFAq(zjw%U@u`KDH+`JOD^EgKZ|MWG-SaWo!pk z7=C)*d*bQK-Y1IW5j5lzxqFC^St)`#NKP7cc6|K+b|ysryjmzlWvP zzQ1J3=TqzK$$X)_S21h@!iqU zh3VoM{~UZ&_U@Qe^BGR6T-1II6&RDZJ>E!>tG~nX(6y`z8I9_nuy7yO6VWUZ&g8A! zL;ek%dwhX?#((?TL3@#aHD%153F`6tddE6Z;)JESFAD+cB)RFDxW=s$9@#X-Z>7j@ z!zM4$8%t_i*0N8I>{@>1vqs%Hm=UPv>>|kj?#eaW%_DOLxhDCqTdU~ZXBjAC661PC zc4G53d3Cy2=LBnpG5i(cQMMWH>p4e=GfI*B$h*q_tCE5|Wx~lE^Xvu=%ks||hg$Tv zfu($!&3;ci-Kh$aqlsz5*TYLveFD(OTm;)oH2k}MiSxO~u^tx#RnxYmBY;}Fh_9I+ zU@tf;7|HpJQ~${lWu3e={-AF*T)z!-oZsW-TVIMM{o?(1(6zUg9Kn#SxACG~?qL4q zI@YE)cY!y#uc{75A&b1j@RLpJ7omG+bIIWRFF#S)Ppz?+sApwhUprWlMaFpk&OBf# zJpTM(K-ucCP1xPN%ir4_T!zbfurJQLR*vR-v|=U~WG+`9M7$;RPHT;CGlcdeh<1>4cz<1-8$K9SP{6fTyppOts0 zf(95jcK}B7o!3kxoloo#w6<3OB5uy}=5{i?io-8H<`U1B07|ZQOzAUNG5VO2=VHeSyBRbWUq)_#mN4Z|+lS&FUvd+m0@D8SgQ_MnfYi6qm+{Q2U} z_yQe;i+wqS`!+44`bSa_63Nh*2(lrD*ur9v-7PB{Qawxw&5Hg)pAA}Z=%XiBvp6J zI>Qh;uRSL%b~AqpB~+KE+=g)$c~Npo<$J!k_t*^k_-TH`*si6g?J(op{oW>3?!G55 z37ZRlA&BwX^*#LX?7YIAChKC$QJqH{LIXfo46R3ER`2nu zxvYys<|CXs!}?~}%lp`zBGCYUP)t$QXFOK2mSJF*NBvu>>G!V1mJkgyOI-in^>-15 zI7$TilBb9$;4^ei!5mBhpDm^3y^cYVvxJGtaf}EbNv7fZn=i|_1k<4Vv#jrOPk;j> z7MOna59#Gd3;a;qf6PH0f;cDd>-ns6 zSvWRdi1c`;HQ!2V`b?c*hBvS%E@ zthmBXq?2C%w}77DKBW%Kp6mTqEwZhi6duf927zhuvCE1<4ZkD2jL9r>u8f0zt3kh66EwZ+C!@ zz1(be^~1K1qnF&r#3NI{ClH=P6w4_j9zm4?MBZ<$p^^AL-U4pKcYhNnaomhpKJtV=PqDHe}a&XG4@NS_CbuVM4?I)PsXF!PxNHl(rl;Bs# z`hAb8+L>=kNya#MQ{NVeyOy`+JP`fuqS@7Hnj!b=cXs4wl4V!#*yMCVNS&G*U4c$n zl5$E@ByUo@>Iru;0q+e1_8I`239XqjH<3U`9J`VEKUvG&DKR_fuEEr<(!!pq{qr;1 zHHLg-0f8L1Kk#55zmC#|)?%>l;d$V`S>R-@&98=nu$3qu$LM-vVB#8LJLkbA#L7-R}F~RO=K+#+({D%h;#&twQRND(Y1#+8iU~o?_8CZ{>1m}ql}rP zL?gYAy8X5!Y}9KG@`nilh}M$6C&~$_C-Tfa*)TITZ~o0vI;e_0SWa;;;gB6CsXfeP z>@UBK$08=r7|bZXkek!mE(uyoX5g`ToA`ZY-agKd6!mWdah~0#Q-J6z>*QyS#%-=# zu-!4Zhp8dTH_7tU6%V1)?+}nL(X2Piqk07Fx_yuXYNFq{4Ih~VK{0z~-lf3VjLwGA zDUm4S?g()@q|-kEx8K7b{|~V}yPGpkA*p}yl-Qd{uGck{ysY6k?)wKzl0L4%-}`@Y z3`fpJixedY>~~1Von?=;bo2xuKYj4 z_CopLa{Nz1Na2mA=z`-KT7=gjXF4B`r1-^TSJ_74@tXa_IJxB_oka(C2uH?#KVPSP zzyBX1dkl*Wuhb30nu0Y;ic)5d6J*JZRgDa5_gP7Axi?ytC7mR(|5+Z_=e0I5&b!oK zqR4IgTjSMtCS3UYWj~LBH6pwmxTSp4;>xzj@h<#v7`J`Uy`TyMPD9WrTJ=-`3V6yJ zX{jY%&nMCfFzO^bg9xoL9a%J@bX<3OF@{T5g?UY6_RH!jl+5qpbI){T8{RIF{@g)+ zxdq>E`@$Bym)|QAGV%@y&~%t9X{Y-fjaEP6j|8I6CmHDnEA&aIlJmo$rYfA!wq-UQvK1q^df8L1sor|Po{*i`txxa83J6&5y5{L0G{zatjGw+(K zYv971Oj%_>MQ@v&%;#96`XB<%yd2=e7y0h>_@jm7xF}s!rA7eUr8*ds zPul=g0i7we=1_lR7i_T)^tb&(K8DisvLtoy>y|MkK&?k+9A(g|E=vRXrq^S${MK-s zW)*?^3Ay}5_0jTataTrzC=sgm>Cw)VWXRtE)#?@Q(i+6iWwTho-&L_vIEHQd@kjo) z%S25pYyks_$w}IscV8^A1|(W)At!3#>xHpKxeq3*VCNf ztvvZ8KZ67_n#2)#2Xj>quaizLE^m;S152YA`OG5Tz`3QOMMQ)JFHfAB8Kvg$Zq4Q2 zgqZW&@9p5iHSsKEh_>n&?fgj3^cCQq*7Any{OjLiS3w2Olpi>bhy%{QKQh*uo?85> z5e-mi+XfZ8OT2}cE^Ic`={MgFozF!%_&nXMg_bYbM15|^qnCkKbGrEbJEG*fGRVzY zWPlz1?iF-6ZTz}VDi1Ed7jQ@-^K&}pW zS)8S;75w=<{4-lo6UV{(R~>|S@ZK!2`IvG$H~E9uKyeY)w-l;nN;9U3L~!DbX(TGz zO@wcX1XF{4zDop*b>f;I`kHIY%Di)0NjHBsgyIv!1=?$8Gbm+4QrY`g6&w?E^c22? zi1U8d>O?SPnFBb&m5ZM2;CegT*2K~LM96f;>ozJqG|4gl&#;rq2E@fK=!kci@R$b( zRLdXH^Bfmw$m2%CjiiO9EpR#q&m&=Xy1l;{ceXUDjH6JOm0O!z^9*~tvqT+jm;QoT z>oH+~#ACRG-0ilRTd5=YbFT53ih_oHI;z&e-_+%W0Yt962JuD3j7`kT-=xg%$V@;-NtR{74E z4qI);zmtj0SEH=s{d?yV+11{W zbcQ4__ngCA6Lfr&AY#{g7yPuZ3Mpxu&}RZyP+_r8c}U8aLI{3Eq8R1m7<{d{YKui|#odD|h{5dnGTjk%S_jl0Tb1yB|1S37m@fEK3oIdE)<5x|?V<0X4b+#Eh zUGno{5+||-MYeT*Z9@dhs05nMO)D_^{jB;KnG9h?t9en`Id1Z0FeTP<$`ne#8cnH4 z<#71#;fLZiQBkxt;<0-$D)K99*?UrsaGevqWvSTBTQM7j?hfzkl%X$lNuC4W?u7{R zT}a)R@*Y6vhMHVB-Z4xWt}?H7LK($DarM~qYzHVqr8O{*|9kX0QAK+7U99C+&5G#( z&OauH*RMLp)#`#T0hGL|dSEhkkKD&JtjHZRz>tqF*P$<1F>_@RgTA!?DIJ%}WTsaN zF#}F4#W!JXmnyIX&r<8Xe0DXzv!oR-bk~uKVE=xdw!>kiuNjr5PIzxJ{tVY+eWQrT zTRhHm9Zrki=lrfrcm8QcZ;l^CKY70X#3|pxxVEA}betds$tL(zrk$=vKl0^!lmL-P zrYyPT`g``Vy*^9musyDKOn)qft;Ebdp~NGe`qZs%*vK8V@d%ZDzT6WY|I&z*&F&l6d!$DDsL% zU;*sSuYHEHy|&0ZIsvu5Buq@mduD4gd!(=CSR6Da`BQ+>v&>Y;g>Xu-r*Rr;oaM~_ zmExc>nP)?mv*w+pDC{Ltq!8xN*IDC79mnzW58@cF6N7Ad4$TwCqCuwtSj+%vG#t5E ze5oU&Q_9PZmog?}V`V;eWNU0jw(8`Qk;P_=b5|Q#%>3Ns6`cWS+%lArMzJ)|jDwbu z5idvMX2ybBA!bOv`xjWg=f#r^BF+@wg=K6=6l3adL!X(d^a!GakIyeP@7;=pwNm;` zxO4PdFb!?lXgl80c(z73rnu&CdlKnRthYG7&;e<jh|2FKH1i2r6{V(3bxG|Lg zZYE^-`WeyOww>sSwO{9`mv^|X6L5j0fKP?|arW(=qLj#W%EI;;qbKBTbb)c4K&FWc z$*3ltYF>43-PDsK>~6^uj=%WK{GY$4kNGOk>S{Yb6A6e)n}X|%#^}^% zoHMO&cEwt(bqYXW5C$alZ;S+t4phGmjI$h$y9rh#;l^8*{Fauv$nEt{e9&|*>q&%| zF)D9K(0Z`V6Ja?aNO-);Zlmu}CSi@M)Y#sg1JhIs9lRM*lR~M@#>?Cr<{w|Lv!Ko6 zn4)Yg=<1?Gimp!pc?Ib1d5WEEVo-gu2YLQVvb=h*O;!=#dsMz5F%fxkw|Ixvs|a=B z6`|WXvS{CnPPsm~5h=Z-5qhbK`82+}i>tg_Ge_I^DhzDG{3IMI6qj+zbV0dX?{s-U zZ}*cF9KZ!s`R{fO)WY~Oh4l9B`(qn81$isJBN`|2A{Sss-^qNaRk)L1B?<+6fh*J8 z`_18voAJ?;7x3LUnNjT-P3eSX_nETLZ?c>U-#u?QFeL+pHOYsu;SFTvwfCp0-a7yR z7^t%jfM-f!^rdL5SoAHKyA+c(zG|fbq==rIRnvI5|5`{EA5-v8zT@8f4uqhi@R@9G z*`+)sz$!Qqwz8I1S`?9ZbQMvVU_ErXhFalBsMfxR-@keiO;+zVh}=PTj!0@)eohSd zU3wV|cWwI7&J_e<1E=QzaWo9aLpkT)VJT+#$*3f}pqN%d&i6ZQCqFMW^cGo||0a*v zvvk+r@(S4_XqAnx7`8r=jk zDkZ_XeInpt?@^RLL+{o=p%}>=SSvcwnSOdL)2MsroK z0L>2t?2IGz<`z%F?igh0g!)Wrxp_}2YlkEvFMTP<_75_cNo4mSlBFdzUC&vz@FN9v zl)To{7U2n%!J`|oqu8H~SUN?{98K;ph>%`bp9?|9wU3R|)!2PYZrLG;7QL3z`D90` z$-A9;SJpmINr#oI0?@eZe=8=ujRi(lKx1!p{op@Ll1`LKIf<@RA!OXOET{g*<41)sgw z=q{Dplz$fSK5r>#9CClt~Ktd6wtCOf87UdfLpZ4*TT#B;!(UAAI^oUfG-OlV$%n%R6B6B~`54+mG^RU76V-KI^jjJX+73z_=_T$ah{BR5 z=C8i^cIxx#R+M9<1zy3p0hn&L*iDrLM|g~{t;OmLRp~%lBxXc8)>y;-f3JR28b>h@ zisT$V`9^3CQVES*CntbCqM6U>ep+3jKqk9P*mr6v@0&A6692R(eUEx>m#R zrXNogi_5~q^n3W2VDDALCkE91&Jkg66dZL;j09z#b5x?i08!>-v>s`MJo;7U*5;}e z@td@kNMnoRz63wZ^8Fov4yAK;tDy~34QW)5Q<6FE*0_Ul%<};qqU1TNZGQ~=szkI# z4D>5Rc8XV4TvyTaAKBmW@LCZ>J^NgTcc#PEVqiTgFs5|ww?puui|vhao9LWz1=k%Q znY5p`RG?eyqdo&A%1$Gkp9~V%DnnT`isf%GKN^c)(S!I-?>)uLkvADz7`7Vglww}q zQDprHt`OVh=_xuo;O_)jOek`e?aHKw{OyxRh}H*>8B(Q8TCUMA680DzSPg+RP&y?r zL-M^mJ1;;Vt4CP>DzR=v!`xNA^;3O20`vfT*pl=?yn8b*e0qI?T$A9)i{XVM zEcF1L%%Zg*JJkPAU7 zEn4D_?tnr&HOS~*sXdR{q&EiTG3A=8aY9b7q<)$kQAA-QqtW8mqM~7ihyIR zygk1`(31Ji78&nbGVe0)T@P-_JDEn^t1erzV}17zxg;gq$(oKP7qL(tn_6s0B`|e6 z_;8n${(SxTt%~A1Fk^;uiy=`W)A14ZX&M}C#d8)ahPCQ<#}yuDnF5rygd2Y)%{!L1 zT>RzcDI%hXV-&a^*JnZiF>j$A#f4?V6h7n2>CSo0r>NXZ9(Y*qw%vH+iDU zLD%{HMoIgf@CZ^MqrFVWzh#qY-ywgCJ(-4Wj5TzROHRMD>wic5ZsZLPLIA_IvtJJ5z7mf&ELfRt<9h z+N$?`;z`-OToo%HOWDNekXi|L{O2ChHGRorvOgqMG+JE0e0pjP%auNYRl}5181Y`3 zfTDG#aS+3&Oq2L}227?xgK2$_UZn+CYj9gO$e8(>mF$L|+t;(Q0osN@evPS^ST>S@ z3#+duyRuj{#!Oe!Nr}9X8q46kr$>>eaDr}xT%Jiunws2cUXR>unHmzV-jP&~xKh_- zKmKV-yYzr)&!|#hpBBY#(!T2;f%CJ|s=f%6m)`4%kPgcJJ?{P)E@K`w1sZb1j&^@V zH|Q4Qa3*x^yvoFOB$v*Rb^H|zh}N+*&NcM5qec$TGt7nG+WsE=nWJVa#c0i2|1%Xq zb${Bg!#4yCAdR6*U*oTI_w#0!7c9w3RKr=O-f;hL0QGpA@pM2H$HCZeE}-dU_GO=K zEj1RqcdNePKZ-~^F4l+3y0f*<#8f9|6sn&YgzW^=jaF0t+c;B9}A@& z4;XBTMo$@%@&ac&_Rd~prob!$INdEKXiL$|8hI>Ge=k_EvoUnL3c4lpy}~7SIm>&> zUWGv{h~sNre}6438=9zn^kX%@Edv5Qa&I^l=+2@vY5Mao%R_r2fB#pae&EkgPx*UmeKn1m4DabtM+e*a0FQuE1_t` zCH?euw4T0o?9X7DgdBWu>GKD2G%Ltws8Nnyp|y$rnsE;24ZU9Ek}^P zq94#u4mnf*lM;ax!QT?jvChD<*b;@6^5*^?ezbEdLR=#Iuj_U{;$Md=?cWT6g)AP; za(}&_2syBYlDU%7)DnLam_gl`(45%b4O0}W{84I*4IGaPu?{Rr86{@RE zQCTLcW-@Y_6f$P`cqvDRS5R}|9I*6z?jK<}GEA27*RXR;#3_cp#R}=e<41_QB5U0} z)`H&p#?8Ud&zAWqRKiDC2`pEm?BV7l=sz7eP43Fh{8f9eYst7I!h|`YR)yWOdh~nx zkQiqumcm{s+d6czymxH3Yc7Mlm4N2Y+MF~43(ZM1wP~VQ=e{-nJUBJ4FGwH*spXvx z5SVNX@^2VK$*WxLGc21Lr(Gu49g9kcPF#KM9gWPQ!SK*5 z?BB!RbN`T$W7+O=rz2y!(e}GY?0y?;PKt=7iEcbOCu;%hQ@OA4yU!r`nOHWsJ7b8N zHTuj}kc9jjHH6RfpD%L;|E?5t;FR0C8dSw|COA*5w}l16H}^X-H3DWM)zJKXb6~7E z3=(%^J#JHHOWIXCTup~m379bhM)rg$k2EIfAX3_J>RWWZp^qmY~YPO;?0bxw%pX* zpNg{{?8x@Ydb_8X2ihtW5Zpfp!)RyEC{YKm33+v;%JZoeX|{T16Zww)#1f4KdQzs* zJp4KhEXT7m521I0z!J$4*AuVH=8qV@#>8z46o1XmFqK; zx@HW`v+%2JGL=wmlrwf%2)R-I_JE%^&7K^leUH20DY5>^ZQz4!j`~hY1bKV; za-@!pi0O#s)^YvarG z9^T%%fit?gSg^a@T2@LC1n$OUJXjEBkrK{gS>R+vZM;z<=f}A^dz&Nuik)=krx~QE z;$5~atSpr_8+8KVzx~?vsz$#9>aV%>$=gjzRcm8bVjwBA`gAvs{Ww-IXB^__0r4ou zT%;uZYfq+`QR)nO8&*cy>W(a4H3R}6o48F#@Tn7?K_JbNXMDEgNo)SnW zAOOX)DQn(o-BDXV7tb$)-z?G(b4GrTBoC@#+PAC0>+d1IC;wzPMqGGwcR&Q>mx#sb za>jFy(e(Qzl#gnu%3(|gLGXE6HbY~Y@aE4f9<}A*sC_0Wa5}W#peCbWNq~%I zhbZx8Rt{j@|gYV`Gs1R%nQB_j~vnSki1$ zA>@a1oI2Q>DMA>9_WfSNN-e~gk+(x0$We30(!t7CP`i*@C%ioa(_i4=HS^i#xek{m zO)49L*R>1RHOu<=2)Pg{VnYC-j!urZG~(-T9yQ8SJ}>83nct%dIgThkWsbbTzg&H# zDVIBO&K#);zW9ese#`;S8wPQg{WP&OcY@cEWRI2Hp6pKw#&}PP;}(W?y0w?F&auwV zy`f7((!awo(-5_pss#&W*=Mc-h~&8N1c*9>jw8}DQr?%`V?&aZWl`o)gsf#=FCKLq zhu&@!*xOvHS92I?S@?F5=m;Y@c#Z#k=s_o4^}WNxm3Gx{Tj3SJ_mol=v?ml4;R_ATE69PtLq8j~LW zoMzE%!9BHV!7%d0hZ&TYX!KirtRiU$AYF}mZ@A)ddrRH?AsX#S! z(Id8Pv^+UIM+VHFE)!hj*Nj|=o2@at%T*c^LQ9^?-UqQ>cK z+P)H%UT^t4XCBU}|IH8J3I@dty)rm-`hjWKErQ5`8J2wl8J9_aU*euvAxG=rZ? z3@rPOLd9vk@{oB8$u!hkW-b4k-SZtu?p6qn5`7Y!;6x41emi5l?Jowse9h8}PNbKY zbQUK*RU|SL%XD*{MonY)<}_(9ja;q+9W4Gk!oEk3`GAOHJ@Fu$;Rd52NmfQkIQLtWBf5uzDwZ@^y;!zG&81n3cpbILrL;%{^| z%?zh%P4E?H*KN8taJlNhyG<7#UKIX=KQWTSLK9T050jMgt11zJkkN!T?)v50> zjvhrxsZ6tEVzElZplD!w6=~$oS8^ZaKWM?jn|6H#I)T)wR@sA6!V@1nUXDfCi*iwy&2A39d zt|cr8h-@L(y-u|QSm7B{iW|t8(I3TD)QKgQ8?Bv&sKo0h0}_8*-D8_x6NZZtx}AS( zeFAltOvAND*BsjGxNf4qs`Lk)c}mYx5|4&)vKgOz|)SKW%O>w$jq0pQeo(aO#Dr&~*SnXCsK(L3y(Jqw^NWnGiya4rHk|S3sV1b%|L_SZJ31T zAK6Do+ejku%&JQ!*4Q14ALBjHk#hFwtY)%WD=^k5oXWx1dDOL)Yanbw*gdeGE4Om&GJwAjK9W95!dnB>J z)!p=z1B`kHIwQlZWDAqm=3Ub3o>`=?pT&(3+ZTEV7Mo;L!;d#V@AH9lby)eEwla%{ zj>~F7^iUlj_{g4DQ~Wo#IJ!Hxz$`~8MX~3rQM2OR&!nb33DUW10PE}ftP|X+8U~x!g$w)5w(|l4^k+c?04}?m-N^`B{bsYmpb4OemC6* zKsiz|5}(oXW3XY6$b`{RqEV8K2bZ+xTw(8&nUgPVH=!ph`SQ*jeKkRPKS95F_fx9o zF+~jmtvRzQM!noxilTLyQ1;D3g3QP^wl~M#wzOd!h8#*z1OL68&m4*kPzw^>L(8{wVpzef8Z96K6!+tC|PfzWY#&|GEjg4kvMD|QOcz;L&yVWcBjCXVVZ>O$i zFR|$(cfrA_tJbz&-qSm|ln$NkV0FqBA=?~0eGw$XU3{1Lg0BSV1p_2yK5bMqacW0t z>@EmhR%@``@ZW&(J*JB;2hL`BnY=snRYQ=5W?%H^mYrTfzKlJ>K!_Q5-zM*~;%;f{ zrWTl;wco-azhzdS$Fl;9Ub+5NUc*?jLgB(c?*9S#|QfX#&M}Mwwa9q4c4t7 z|Ke7F`dywXpm`hkuRH#YiD!%C&9DHu83FUifh+Xi*S46?@yV_^t8Z?wT=7pa>S+;D zj(?xNS(TYFhzaUokEvaw)?R?s%-QxfshH7Aa$F9HQ8anH1}TjyYVghAIFdbL+0Lot zm;LqcV#SwzLS6iI3}yMQjCS6?B3H$*5U>!6{_T0s33Iq%x47eUYBOD=CW5isCR2i3 zj^~2-`cM4bO856Qexx1avmwcAoVVF|+05Hk9Fr+W_5#^(mHSa|?MWmZB(+WCn1J1~ ziEo#MzxGxrIXb|9@8&rqt}=v5l}ILP23IJP_3}GM^&}hTI~iT~u1UfJbX4{T>>2(c zMUK&j>md4*Q6(&0o;TBRlW3aub}hhdM!ke_Il~wfqGk-sNm7yT6a#aUdbj zHb-652tmkbqytDkw(wpkg;Bl(wE4@y&jq?*oL5y@&{=(Q;OWID~D&}gr&I1-Hl ztgeX?$A*X~u{RakyX}QevV`EmwvjW9A>=Kb=C60|FTIxKoyhLmC-qOVG()uT+Vo$< zz8)HjxY29sF|TNrAbClQWD~@`cB70gO-Fa@QxjdcBCs7glH{CU@L{HK5DQIwUp(E3 z8_x(!fg4L=F<5Y-exH4G!^$hk41cDo;rg_QdjE{_vCFKa)(<=!{*spcF5JXCmkfT_ z=)dR36;&2R)~CZMq=)D1cQ@T7A8+4hr#FyEXaL-rR5J)S48m|EU$d<&!FH2W_kH}q zK5V0*eL3WtgV^f7$NNv4(-k*gFDYLGLrK!Up__oGoFx!SOo%ad1d`VGJAZ0>8) zW-`rD26#q!mCGHQ4Gf{jnnQJd!OQGu21*K`89{D=gYGn29{EyTpCFIFH_AEA2C*j+ z#%cN6VobjRYXg(FrNTUC)dq!o&-h7U%&A^I!Os*0P z9II~SFp-+v1L=QRW=yfBgI|D?@4rCd;~K^)NwVn4l1@J{pfNBgfd}4)ygVA~QAV+`CkZl<3L5}tv z^vS=zjXJlI)BXJfpCa9CbJZJ?Ft`7iAgoE!QO@7sXC|e0l;HEf7qv^LQM>X&={ZYK zGhx{czw0ap$T|e443N_$Fv!_xdcN1_Kpd!-6?C(_?QiNOPG-xVnC1?_L~~%cIowwd zJSnOMs~zt?&>9uu)ii}r#%zu_kMTCh*5Q0DuM0^#Ul!Q3o|@FDykLuk|O2Ki)~!_dx5tgJI%*K z=vpU3!+(g#qp!=E(&NCXI!0^IAee_eb(x>RM&xUHLxc3lJk2v!%D!pfoN*O^WVj7* zlbT=Sqixp6(R0a@7}ct3nD2b-jG@D%ZQTK zQa{bf&BEFXnfbBl?9XI11=Ph5y^99>SF&&y64_~ImFCcNik$8DqgK2a2i$9dxjY5u z|1zM#O*q#6RQUHXS`v=bkEt`6d~f7oRIqIs-UH+08h!bhkO*(Y@15dxTL)apAdYr} zPj!vulmg8=@(ItMw6a(m*t#6Yx16lW+iKcmq)6;v5yn}Dr^XF8p0%BzEEh!oCpc9x}1zN45s%IkR}o{+iC zq#J1*nSib$eH(%~XZgQxKBl!uHVtB56ph=T3_nDzCjZJP!74GL*UCN#nb&Z3>7sYF z;#hgs@~})H zo4{bc4cXs6@zGF}h+i!6n7hV5j)t!OeoJ~Ncf?zY82t>9-LeH%>m8g@&oSL(F;B<- z4ED`%=$g*;88pEibYy~kCL8GiJTiIAqq_dDb_Wx=<_xCfwWZG+;zggBynwseOrWK;s z%-M~}6y-XyJYr;tsz>-Wyo+oG;m$qN|l?uB) zv;HymL9v`)$fwd*fZ089S3Ji}#?P5leQM=LT9+8uZTn`Xp1rj=t#OazCwq_pM%PA4 zWc{Yb`7XXo>2aT~5{pI1fK`kHT<}vmdPR87fu$_lSdnMCs@c<1pV&~2V~}NqJehjx zod(<20r!nF^DnLP)F`X;lTlXD_|Y@f7m4JAV6{Co3WAR`tcHkUV+?}*Ju+ceNF4<| zzMr?X9#!oIn%8d9baB)RW7ISP+kz=eH>wGaHoUYk8pvE6d36<+-aVQy|2Rk%QE_DB zwB5ud=8SNbo53IP0n(SjJ2}RQVF>(%ZS#HbyfWTDGEhc?cFMozqlL1u_cp;NSHN4Ii$8=}*{Z!Q#1m5Lnm2-M%8J~e?8{hKa9|9v zK<*nGD|9GbjjR30((npqdK(WG5(rz}?`^Zy;jeP{6j3)qCT;6kTBR_H-~Ige#ScSH zy>&Gu9lrKjDIh9oi0jmNcz=`$zk}w$h$<{zZ>=WoHF7@kZKG}Cz>fnCO-AG%_yG9 z)QHA{b{|ZI<*(xs&U&9ADzyT228UxaMrv0dv`~g-d8NW?maqQ%4g8?5%b#-g3YI!u zsdxbx2ekJCwDFx@|BW>|+w?_3#I{I96U87d+h7Q2aE6s=@`1yq3s)7QJm}J1Z$PP| zkhii5&N1wBto$lS%8Mqb7TyzBw|r3Tq8_>dFFaY}fwx86k~Abdx}d+HZWYDf2Oq@4 z7F8sC%UnypUvek15$tl#_zgl68m&a)@8DkBrNxM9IJ(786z^gj`1?YQ#cU#VgoOCM zb-NtE`~(0|P>tpa+;Hdl)}JJuulbmQ+K11I-l_I`r~bb7Nzi^G<^6hg_r`oOV~^vG z16wUaH&a4Lp8V!-xz3A}YyGhZ8cO7)o68OG`>x*fYex})F;E=~=Bs3xLvRJkR)$jR za-r^;7=$2fmQhHp~uk@+w60@&+h}Gu5zsTNNQWbiB zy;A%n_Gh8Q3qJo=ReM855TM?8 zVhw3QnF+xtb@BM+F?Rl22s;aQ$UQKHju)9jS{pHO4?No$hj*vxDa5;U>enSU6txhE4_nT4{ zR4xO188v|DwTt%IiQU;p=jy-181OY!8-JeB|Murn-R?>2aP%r5yNf+Ic)u~jgv+ky zB#8c5G-evjNK_$26VxE@3f$b3Vjb6VseL9rQ88QQEHf;WY(LWUb%_aGBi9fT*O|Ta z%%C;X(;FG)Ot?I*Y!V`+(XXal%Bo4=r{as$KN;ODQ}yB0Am14|{G>x2co@x@8_jF@ z7+Yxr#|{5R)F!&Uz6>25&UM&@dK&-5W)+CCpUsM@+-pLKc*tMtJ2q3bn7TxO4O#E< zV2Iy|aelpCw9>0g@xll7dcb}+9~t-l_t|Hux};OXxnCv?OA4Nw?ma(v1(1>(h4x}X znD5@X^PNOUcWQfb7!hOU&`*k5JpbRP-=}CGMT}E(3@V34#TSPwvZdgaYQ?ksLPWKS zS!ib2%ncU)&h~ebRXb`ph1Suvz^T5DBa@$j=hb~hd{7u)x^Gh*x|!>?Y~$?l<&lxvV)b8jj4#)@11K7Fm+Th8oh zSi1eiu&cQuLJk98GGEiuB&>spVYCP2fHzQ@d$6LU=1Fr%S_WD866#dFzk&3B-weG% z2RvgxJop-6g1Uk0#}z;wF*_tvxy!%T?)UGLHyb6%CB{C?k)E&b02|_Yep|mFm#bV+ zfd;0r7?~t4TKS030Tnlqy;@qNHJHLTHT#7z%zu}scMsfdqhHG5M^$9op zMktq+>kSy_$UWU0@DJ7Zb4*hx5X940DDl6fCgH8`W)uwsw6gH>+#yS2q0+D|8YQBB z^NruH>uENfWCS?hvQ=0!$@+|DoZ=(zc(yd$iCC)e*EgJ)#r$j)a6tB%(eg`9(2%|B zHVHo?fhhFk%^GOh++^+eA_1MTN2-`BedS)dqE_1mNsU*S=10go^L^1%J`TM$8j}d` zct^E6lTH^)ojqSSI7hQ#1X+4|yoXyHtoNzTASzIYf$Xk7O(ORbSb`Sk}cU0&R zbB}t1f*#VyNq-+1XG2oR#ZhE_AHCTw*fCD(u{!hjI(roIH&ArRXXQi4&zBE z<~Y5_pkyR!?P8cXh!}smw}0-L{oku*R1)h*lb(-@~8K$-nm5eLJ$Jj0X3Dp5Hjjzrs)NR~xOXcpF1(BTweN z-=f4&L``?eEEF|$T|SOrkHwIm$6g%?oZv9TI8zE?;tMa{hs$&j!A{lcJSK)mV2wxy5im;{ znIZdYCS%8j$iZWB*vWC<`uAKB?P4^Hk;{^jx*Lwavz75^$-|WQhhugv0_u3;k2v$u zLso$xchM8~_u-TLI%e#|5VD`~I38tLiwzU7oU`K!m&X}500M!F!1C5N%cL&Zwyb|~ ziQ|TSOqX>ic!>W-(=3-FvUVr3;G&0*bWJ9s1G&a+MH^>g=W2bZo26nA*Z&=p_bA*x zemCV~J`U6!WS(FfLnn&qnZIpKy<;B|bbP0f+`m%)3EUNtBgx@XQFNoyO~;U$&TVXe zugXyZsuq?6uck2yc(?uurZm{2yT}kD`sG7yoy)joCZT3iMQ236GBu(SJhNHQ&>Fju zU-{QPZDP0$@h`X8oVd5u>UcI2?*#GDl$0C4BhA!Xmjcq%7eF!b`v>SigMef`?I{41?OE^{;3>qA1|LjjmhcsPC~0*SYQgi~aLn6N0Bc)P8^q?XG$ImG5#2 znnV$}6bWl0?(cX->c8STc$i_0Y3x>uM*N5Q(>u53heGF-g8wPkFcEW-61IFZyQVL>`1HO{8zw3H63C0YvPG}S<1uz= zS5rGQ?*S15Zoo5Vhq|8Bim+yuuIbCNW^}i?<5@ltTGvKAE~Oa(g44~NR}}q4nxz8_51Yad`CE#kL`#i(tV>aE&4Okhu(srzA-5 z)6(OBI9ooE`x=*2<3~8yW1K*LNxC=szLOR6e z%u#!abu4Easff#L^b>pNmEVCx2{T$OXf=@yebl-0Er! zXjs+EGf-xhzrJZegEmBrzf{;0QgeF?=m9=v3hg$(jS=trp%s7+(nvuL&z$~Gw$bM;rSQByoGD?3PU0PnSX6O=P&Vk- za8Xq#HlcDA0#sSe^W+31$_Doi8lWh)?W0$ssrXiG$4qG@*2%1^t$Cf#5ps2kBrE-rL54I(U)KN`okf3M7Q1-GU~)2 z0w0Oqk1tYv&)M{S_-RFVi(n=S4c77>2R7oYN3V0m zn%D5(Zn-x=C=f~8qxkJuvHV9(0Gk|+gH9zl!e@lr!&I1fK&%Wvj&a5#vzf{D_tM~s z5plt_k>sgbpmg^8o|psTbPpAQ;NKei>^nS}b5lf!#-6tMO`K1`_coH+)2^wtS=8hBo3!t z+Yj6x+XKSnzx1nfo825N*3_!wA5WwthVRC%fy5f!)D<`UHh59x+ zC5Qd^=5}$8*H8gNw(>bnbd7aZD~59v($anJXW~3vM7~b%Ugm~@`PbX-e;&hgn~f^V zqG`_i1tkQJjJUTxQ#0%^VEIDV^&1tDEtL&qP~95}nDC^}Rm=Y?P+JK~&oZXDlShSE z#dFX&DVD4(+494D&z;%?mW-Vhe)+KRt*zq!?xQyUc__^a!Rq%BQ&Sr6wf^@zzxh@$ zw9VNuj-lTN@PyzdZNtQV%U1FmHcRonCw?Lu*yau0Cb527r$p#p^LJux`F+_<@j}IN zkUY68v?!j78qA1Vf8Tqj-d;c%+v5Ih131A+R*x)Vos7=Y@FfC6degKWZa71cO7GiU z4ElaPqfB(`yonpl>@)PWa*0>|D`o~2Qg@(sHdu0*o+4IZhCpKf4#qgcuEW14K7pt9 zzl=eWiYWuUt(Iiu5;Kyyw@i)ZlC??5QKdd4S|C9xeY{3#{xWw0BPeLSZk58>PM2Y3 z8+`<-IUt<4m(z7*L`wRTl#6xs1kG2{;H;?)=acWtH1ydC?;&+GPiI{v=TpLu&YimA z&?NE-{7d?vZI9eBIDE3Kzz6#$1FzjZa?CyVfGMssaill?oFag~3TgrTF8A1?yx^Q) z$Fq>0CORz@=~wpd4Sj9d7!>a!<{W>5b*gipPgYjM0_KMEYzQG;(_)~~Dk}ch1}Gda zQ#Hbr(#1dxe}woo$!F$@Nf{(AyU=_o^0z^m{r(CD*gHYNGu=4ho~o51<|}#kZxZ1d zH{+y)oaaS=+V6kGoxEn9aAmYv_57I&M6qPrTmMY^5pgUn$Ps5}M*@nrO9|ZlWq-Lf zSbSQ4p0W+7V!c)8mUfm>GNgKA`7)T|M^f3Wf5wP&AATAGLcWLVKL?PnhOWm4PTCQ! z>;a7%6vo=f|8~8P07>V6q>XU)&3+Ir-*z~Ksy^G#_e}om}hR1Tf+D+zgGQDl$ucbKSFeSM-9vxiqZF?@|_cGWix^pgwrtWSQ{G z*%^ZIy)Av9!#p;>zpFu3U%YII%b3Kb#?NFccwh2pnmNmoD>R7Z_N|wWf;Wf?t9i`F7gKn2a)jjye4?*zVH zSB$JEa(C$xURQ^%8ML|N`uE6>QBYX?vlSjUq1z##^B{Um{*5sJ$lriz?{t&5D{$%8 z106GHF(N!CUf>?fMrfBN=QJ_xREuIaH38-9XVR$wubUB=1yJdtrrO^==}w+E1Mu&@ zwo?B-`lk*M%&@{8y+Z_pMf=NmO0%nT62ng2`V2rs-ivmim2$rv&jsfEy^JTvyk8pA zrp4-K96YjL8RgfJ>TZFiO=T=*StSjhoW*upt(VAdm;7v1*xyI5Ht*u+&T}?TY@t6w zsFV@VCgo#_Hl&$CllLNawLMNuaAeA7KH5^-EQ!m@T6pXHfo(I0ur~Ul@P3Hf64Xr1 zN*BYD5XX7MgrDYt<^0Z)_?_;@(y3L3+I3{GZ2@n1muQ!5{olkr#&5w2^75!uS2En2Bs(fLDeFm1kY%SH z_pfI1BcDaGfxbQ z1iW{D`#=Pk@=K(cIUR|%Ga>Zbqz*GJTF!9w_pB^Uzx8EG2IEs>Ue;2M-fNQFwq*V! zrr{aCkN+JXe)C%t%6g6xGK0G!{P(Zz0w6}Tt~N;dAZrZlYNq4E+j44IZ~%68b-l(H zpe1gfhU#Q97D>RgGeU+1@aKacx-+Pj>S=nc)3__dl>a1`kK+GtD|U=&!$7;u3Wnyu zbzse((8+BPodSwKySV3`D9m%z_1eOq$a!mUu1SQ)L2iUs9mr+$2X|E7T=ietE?x6XzgV5e+HgN)7uACk3@Nnak0$;jBN3r2h zs;xQ(tLT-~vQ6Q&P7CUoRTrN#*TzzjbF`N4Jfe<0l`I+V`hjn(rQC+mU2L$y!W1k^ zJ?{6}>)6r34sLwBxi6aF3}>m+nD`l&q;l(A#u@=$9&O{Y)Si~(3T{cT@qY7AbB>%B z!}3gTW`wYcx1CUh+M3tt9Dkdr{VK>CFe(ym@QSUd1rU<&laK1xQxX4s8M7LhEI89c zE9swc;>uQ`s4fP= z084oaF_2h^@M{UK^iGEI_tCe}I`Clf#<^E}UN}uOY40<+E=49J^jfOEwv3K|aR;(r zg5?FCwHfX*ZU<=`XC@D`kQDq2uO&CU{mSuOTz4=tA{}ftLTG<&Ck)J=ubonQv9Qqj zeb4$itk{7!{(h%l**`;#xBvh`_o%=TB-!lzFQB8gm197;4YsUfLox&T#zl8m`YLBEB6Sbp)n{ru?_p$`5DIk`{v!q9_IBU67~t`_hEadSK2c8 zxl5=E&B4Ziu}2k{!L@WH=X_8cMES3!=r5s5@X%g(mZ zMPCjUmG5Z436`xx3@%N+d!Mz;G>Kl0iSz~$)7rj?=sLN#jx|Nz+HZF&`u$=?)wN}G zdVWz7#xvaJB9u2T^>L1mJjG44C4|JkaeJCk-aJ$sBHO)-$r{jvlOS$RcbeASy~_(b z%Xf-NK0vK)GP`Xe@;FcLzi_LkY-7wlOe=>Za`=uUw~Zym><@x$ExIFU(leA0K8pAVFM{mae1CvgnYhgzW9D}g@SAjMzb=~7NN_t-t_ zUz%_X8_!>kW`3+Ar3lY<^`kF@WW$)GXjGmnIf-uXC$iZYKmJHo_1~6bC`#7c&tN+E zUMhWG1pr^|HFp!(tVh(I`kp8t!{C+CW#0xFV_JKaJ}9a28~vKmoOF>5HK@j{(P(Qw zg@s%jhmT6#V+e?@R`f=HPo%6Q?I2SHH|x)w9jh~7!77Kh5Zxx5?E8=X>PyI zP|*862C}u#S4E#IKNl7)zCW&8=RLn$)8lKu?9-;aJ9<6YHi<=?N0z=bL6Cs{ z_s_1u*W#^{04B{*DEuvjCjl?embod_M-R?rFzKCuJ)D>kX<<}%@23hdq+m}HDf_%L z-rYT}?~H`&q6^8WaagZmpVAR5VMVTXd+$Ihz4a!G2j!g-|7caZq_;*cSZjh?E_@$- zQmZI&X zTS8#bJa03r?BRGm-_6@G8PRSp?(E#gfl3zPWT}nyLwg*xKD&#qCrcAeb!QY7*%!uf zz2br{&Gda`zX$uu9q~6?y2kmtm-Qiy?(D+buJT>fNBZ3n38=iDUf_Kc4yFrCxDHcK zbKqx-3Dc(;*A*RetC`qJ`ym}IWJ{^`gKxKtc*-^8;4e)nZ>B9bv*n%3mj2sBgEH|O zUysH9l7bFlM9=hn_g7{qhts4_Q%@Z2^0$c!ZKojcj8&)%xcwfyqFMMhp_csJ_T4DX zcg>&VNSuwW^n?-WJCb$?7+Bi>i9#h{>5QrB=#B73H>}&smDy5@&qD&m$I{wTb z_LO)#%+M3Q`!EtHinXsl$FUiz)5ZiAU0TC$q2GD$-`q@JacWUBg)cu)>L5Wm|C637CYc`$2p2|hA4YZ+5`us_a;ho z8BE`CGrgVRvRKHB;*&kaC<6B*sH0bQMMknVHKe>J6?=C}LFrKA{eK)v#&6a422|PK7iqH^0Bd_uA&DXy~FR}Xo7Nsy+ z{#(LoVy`x^{_DSIzLSWogH1d>q~n7-R|w7er((Wm7)*F7#0S%_Va@MJy(16W;wBdu zAjt3kJwKD5=m>^DbF+yo7c#%KhtUUJljim`ko)~7>{|Qkn>+j0JGni5D!mZfO5=YM-Ak+UL?#y%tAl$FHTzvGh%`yI*;N|IQj z{g|lQbTn&01ugdCH<1CLWwx?6(GYVX-OeJ@$vI+mZFgGn~NIqFUaPCymy>kKc5-=y&K^ zgMjNZx>?t%Fcl(|#Y>4mj9ruCe6R;ogoWX1SFlFkPfYr@K`gslfEI4%z<>ox8Z73S zObA4#zp<|>2S+(}LKqZYc+Ell=9&sf@`rLe%P!jk%m64FtJk8U1*n_=+NNV60*2L@ z>0kd%QUC_^V{rVKUpw=LiODmUKXE}YuR?SCp>9M0b*3=hj^bJ1;8&U8L=l>LVGVkW zS+eIBbiXU=zK{N~d|uu+7vIrjV9Uh1S^|#jdkd9;X^t~6o?An~;79AshR;GM5-F?= z)bb3zR}m&ND>FDL$pBfPzssF))lkhjUmp2dC<$W(d$QTVg5==cXKDXFc-O1P>?{&U z&(p|;57QrD9AK|yp|b2QajHpRE_m*8WR!>lnwO&^OE}8ZTW(Y8olgSPx-?Rf^K;(S zQ6`dE|NcQ=IoVwBA6@%4ez83Tl2o9-Mx?20!|Lb`dCP9SdQktkeQ3VY60_3vGT&B0DH@M~qmJ_@t9cH*SnpSTWW2DQ%+Kzq zryWYsVnkB=GZV_9Nv@LT+u}%j`kjsBXL3%ykDhkvVIqD;3DMf`?J$2#NY2U6*@Ctv zfqUK&5i-BTSMtYfFkG-+S-J1bVVHvA=4MA4q?k`BrMRq;$qz0qp?i8C`bi^KoHYvV zb$s4}ZsV>T1@n?#j*On|Q=oVto32 z!y_)eKY$3!@u@P@*k!^qHD#6A=I~QMcBuW%!wJw4ov{{8lhLLxmFm_tPBk}4JkM}% zN-~&`PbfpTo8oINzTS@iYV2hZrUUV>uupy?9Dg7FQ!o{Tu48^CnBK_LTnd@s9L;ZA z@;wjsEOGjE+eFw__TldqbRle7{OxJaQ31nyJ%u;XcLy>_(c$2Du}0{|9gtQw=kg^z z|KFsMU01gKH&-1ko0DKT=zNUh8~?rD+HRw%vC1)gfvs&HEy8H=D2Q~8IA}Q82A6VE z)a}quh@sr)Rb7LAjxp3aI;-knIKE+}GG~eSu_Q ze9oga2Pw1n`vTVACOS;J#_7F=Y)071mcc_re!aZY#si zXXh@r828X6W)v4t4o$vTqr zxRoGidrg}FyhK_eB^^XL4uapOyLH9Vl>Z)n!!zrT+?6;EC6sP`>FzQ?t>vnf;g4)euSZyNYqHev_;P65kTgvN5wy&BsD)}vRH2%7ZnG)wgVN~e-n$vR z&^2Tocrt5^4jJtmTAQ2Y`aXJHDhfMSyS8mSpOok@f$rI6@XRWa+)n54Y|TU`%Qi4U zZ}ein#7FapDw#Tk=1=~Hg1?_ZE-%4Yb2``V;;-{2+(}!oHSE57`EKv&^|W)h>V?_= zK6>ql3B6a6K6iXseo1oas{jW-^KQZRxlq=QP_?(J(vR-LN?zc5a!&Wj0IW)k%N_pQ%Wd(NKE=b& za!-+B!0?57i43xRScxZkA&BJ>mhcW|dT_jvvY}+XBsqjq+w8=kx<9fAD=UUtdYkk6 z@bd~go81nmB6BBUvutHtPvFs(!CP&lHxOlw>`$M0j9xv>F_MacZZL&7fqMXkHLii?<`;2lBkYNTfJwprCl z^S&)vuL(lm++GJ!o?Z0*efE2rJ@FMKq|f8N7U;H3eN-hQ6XexS^Ws0&UFg`Aac*Y{v+fw0Wa2r=l=K6 zKM2H_7Oa#2H1 zz3-x&o9S!CRd3E!cn`P1S%}d0uC8U)EJlk&ER-2Vsv`4;DjGv1TbXNzw+G0n(M=4i z?aNBJX7Q`-uqEdM_#n*mJ-YbkZ#zuYq7*HaZpvTya=n7)!(2v>j@d~AaZHGw^{2ur zTHD{JpWv${^-^n^k1=m;K@`|&-PYrGVM!mrHa{o)#kKJJMsQchSbM=^lMXIubwh&C zdzGyHp=qwDKZa+V{S~9a9f&ttp^-{1i9O~fx>ESookW^9^!y&1$)xcf*g`+7Mq@va z!se)?Wr!zcs{aCS&k1GQ~>8NZUL;o&veP-V0AIQgm446oivLg ze>@dLK(c3I4^f=woRG~?`MI{L|2})%!k1dao&*eIj>!^6F#1&}&BUY}!NpL=_WRiy zddgot-ToaUt~?1y15ve(JYrnGG4#Pa3E_SZlI*roFmWH7qZUCtaTq)tNL7Z9X~bMt zeCZ_4$M?|(zee^HixI83L8LLf8TqDP(0zQ>SVx8 zuRSf>-4fTt&uCg;6P{ucnnkvFn495ONmw$QY&3aNx(vp6|Vgj9m-&{fvW%LjNqUr^e|FykGZK)+h8K%k|nr@(f87 z1)wY)sI~92*R3*uTJ@RTeB=Q!+;Sk~UB_bVZwAA*;SSf!WenEp0?YP|Yz@JUUb!!9ldan2={2A7#->2_(g|5&J z920PF8hNJIe7>kM$QiSnU*E)I^X<_nDQS)+)y}kzu+u()&~zFj*`AO`ix#qee9wd- zhkLnaE=D-T;fxgbyYjeLp9D-YYu|w@YWbz2qZQZ6d&_fI$7dQ)((E9YoVrKQED28X-rYZS-tKQ1w@B#Yo zdYgSD8_<+*f@k`+ua4h}ut}(#~g z)_6x(^cmqj9wWEMREjO3st=juDDKJX(kb;Baec-Gl0hU$`ZY(zxiw9dgZEA@GRwnL z+z7qNj0*^l`3wB3`G=1hTkvJsW?@{kZTH9%zDdXRS)rQm4jA|uIxU>G*~+%>LuOs! z-w=I`o(63ppt|6Meeya)cO2zb6~FDUQGbz5t^2$b9GXhDjn{vMG`yhxD_ZD1#(-ni zJoKITK2;ovEG5@|e^o)fGM|Cy0+{a!G!&q{w}$R-aNjR}hfS5BoL7CIG_* zqD)Jva(Yec4R-*oJ1@dAp~8U=k5r5?qzj|Ru0x~y`_j)D0vJsZH|r;Qj5S&QYRcTn zb2>|&J(o}RV{MyG=862fh3;q+gM>QGc997we(z5SujO4qr3q#FvzSP!`5pMqM!&nU zAM&P!9Q2IjcCK89*WY74unN?!J2>J*GX8!;Y9W}tb>bFdHartqJiWHRf&Aa&zi1bW zg~Jl$?Wt}U*}gpi`!??}T;%U4sSP^kQuX?-GKzw@?wFh6H4Grmrjc;pSm%Fv9TR@xQRRzZ}GGr&lnzqE_q=DL)$%IrYvdgMoO6L~?D_a0*=t+XJe32WLK zw#tIub4)BBauM|GY-szdFCbjC(OXD}*fP19AjE^+kSBegeXdmU{Ki`X&YCpd{Hfd? zF3ey|&R;c%abyrIgR@3l(w7GOr`Z7|F)x`97Ssta_+lmL-Ys`4YU_J^`zZT2BL>^M z#9|Ef8a;H>5wi2Ym>%?ueOKya7f?QWW@=uVi`krcp#+z=7Pg3{4f;LNf+qjv4}OFttoqqW z=d(v!y4`^V`IY6n7U|tvna`e?@Q;T>5y!nF{UaBU!Sz zFKM*rXGo%!mUic7Ma$rWOvag&DU?rAoN?%zPk^#X7D9}hQH6~ct}aYU58rfgQ>>%%KGAQnb0h9 z5qxdmJ{dLwguV25vrPf0NuPp-&Ub2HiYA5tSeH3YsVevspJH^Uyv3IBUnY>5FWz|J zqFx!Le>oZSXQu1-)XDIAMH>^O+{xV7DybI831&v*+)>Yy<$Uk!K$i$2r!`X2R3iF2 zs8VQHJF0$PAN}AZL9?M`VN>dIv;WQ!!}L9X$FbgDERd6t`@V@;A6_^9y5pCR{bLW> z(C+s;?j>UsEdYse)@A2FfNEKD&S{;$Pv1w|s-c2tPkt+(WgendK|@^+W=T=HjuIf~ z>YgQ+ZzFF6F=Vi3>(1|)KY@%(d+Ue%#r8rqy$Mw{$^y5dzsY2hL{0(@iZ+@$z1`6_>eII_Xxuq-jm%D3cs0ZKDWq_!sE#DK0Y6WNV%KBzt z_Igiy=&9|38N%@IUsxbplnb2yrk`Q?!-L$jw)^a;WB1h=wdEz|*4nNB&Fw1t&!1Or zkkftkefVK4ddFU)x@`m&DCV4N_{!%tSAh-ML?qusv85^3bf@8N6!5rtWVvGjxjPLy z^eN2o9hK!gCbOgz-Df#Pfi6Hr;x*^+X&c(TpXk zvnB)4mCNs2Jy~rZf%doiQzEA2bTY2h4`wMAkKyUZ7tu?gV9^WK)v|?96?5RmN4R}G z*jd+}`X{VvaFH|Tg35sYjhca$`wzx^HTgQMU@D$(gXUT~3o#vvEka{g+1#5i@R3|K zhGTxt*6hbRidy};hmEuUtJC-VQCGyfN~R>P^dpNqr!+tSM{!uUfzJi>eGjWiTM0`U~${x7azql8aL&csggjhue_J z+s5X_6!Mlv_6k%wR%x;Sk&*TP$K5y|NL{N1@(%jje@u%jY`&`=HCO;pSOgu-q@ zK8~D)e9xIB@G&(C_YX#O^QCii16Ki1^onD`i#<=+@mYb(QFcr@*W)YZP3vBOp zLPEx;A&lS%o&Y4JE_$btxf*veR!rt2=+0_zK-Wl*LTOCJnqnl5$l5s6FDTji>%5Q2 zh@%j%=DUX#qXRfy#Kv3mTF6=2njBD*W}U{uNwsxB5qO8U=RmF!-?^;9d=tBWuWJ-0?-!Nb&fB+?nthj?YkTSZ z>H6gw078=2(HF1bynnB2sIVD1dQA$7K0mwUUe%w5WqoH1f66?aub|SXh@@9WK^a^&N!n!D)@c>E5_EdLll_OmY8yp2x+Zi$U}NgT3gJ} z&kTAE7M1)306I(Q2sVzI&!TO>``&Yk^C4x=n+XfLXDP(Az|snWBwy z5`9Syk@^8s&U_Ajm0=}ggWyU2o-fo4M@@+fS#Q=9_J~B+Ujs5ACe~)!SxEgag`sV{ z`gGYQxycuCA(r1mNw@lc+gM7rXu`L2_mROtMNHf3V6>39woe3KPIAz+nN1$SjJ31B zf4{qFt?~q4&>~Q)E-ACmRt@jp0$~zIw0SA#l1|Km+v9(xSoq}Fq83bz%e1&&+Di`^E@;H4mHQdWbY$!OC4kcyR%`PTaQeehG(L~kVp6dNQNWn3FP?t^l4 zZhjwq(rm8)C7beV^jEZTWHSUD6GM~e(GaWfiW-!ow7cq+EZ|G8S`ztp&;jbHH6t}m zKr0l__B{^7cY<>bi_GlkW;OuQOWm|L0e*Az2^DyXBED>jzmGm%MU@0v_6*&rQADRp znf3Fnk+*YLPDiDrkG`aEXK$bktQbSQIYBJf;luR4x2tu{RgJx!#C%Y~eLU>$Q>`zg zeQ}7(AjNMEg7ck-XT&uVB7Bj*C#DI=TIswOz}V=sA<8k*^_i{xiiw@k?SQh2eKt8I zD~ZL&hUL)iZ?8bg;N(TV^G7V>yniz4utXLp*&~BRT*uCo-9-58S>1xoHDDS_h1@?H z^&Xmg4|QaAZRG9dyEeX&uO!N{m<?`EVPQ0E_$qi4Q1BvT-nYcA#$7fu~LV|qpUQZhi69~Z>X;7Qbf z=jMNIOv~ZjL!j)TAZawZ#qom8oI z`JW`1T8>Q0#AjAEl<>3%TDxhgwjtWBnf#1^%iI=x3JILT$K!NF-$@kmefDfmchlZA zMtM56aigDc1^0308o0oeYfDGN;Uz}C*gV`llOVCan?uHD4s0h-!u^$&wx8*f|4L;= zdOm?3R$ff2i8LJ6E%(=0CM8^>V3eoz{z*70M++CXVyqh&D@TdpFpc{-Z0Hm1EQ6ud z<g4X{FnQO=0h@UG_i}v39A|EFfCXjbPg%fwm6Ye=^MMX&9%@P`D0qf@FY|xI~q% zydFCk?D3x>xQznimeIYFb4XlB$u}VIdxjLaQmtKyLvfF0hQK<-%CY*ta6sA*-J$1? z2KYRj(RxLHHi~F@Lzi1XDl{Eo9(Pc!t)L0&<*Bn`3T^prMaTTTr(LMK%le7> zLhh>(a?^{8#L3-e7RVn@v2%UR5;ndg(<@as27`k<@_RZg_4t@TAnwvg_wr`(DN04CwW1-~yJ?{?g7*y`2fLP_g+MZy1fC=>r3XQ5~ zG=I8AU15|0Sc|1U=7xG?2f89+>!T$%u`NVd3!ck>co!h~PVKR#lICE&73jf#syvs( z5Zq{|QKZ2cyXpH!@%-H;$qYf3j{bbdHGUPb8*D`av$Bk^GQm*zUDHSwQSL^#I)b`b z4ZsEc{T`b!eMCDF8(}_~R7KssPtf9Wex4oSGb$mM9)G{xcnmSy(ig7 zyfJHIIQ%|-Xe^IJdQ&Wk;5q(&m%{<`J;H8sN+1Hnq032b=;e(C~( z-pndwr-f6}Y9XCBN}dfue&*o%vN4w76+XRFJHWBOX4M1T$jXJWp6k384Ud=go!Q5CJI6?ez z3qG0M)>t-c`#mhI+(Qso9M!mNhezMdB5p?iEK_4 z1E}+hAit8Y+5c=UMjZDJPW9(Hc0{Lkv1#^e19rUvF#P^gqz6stYm$yUWitRSldH_A zGnbQnlJ-_Qf*im4;9p6@xz?rp_q?}S6;i&ZvRAJ+WIPIktjXc;nSGBS*-- z_na|VG6-zPmn`|qwy-ek2kx?J%Pd2l-}3o8=n9+|0v}ZT{UrztqL0l0LXtOgX;UFO z<4iAhKOcWV0tPp(u~_Zu@B0(}Lq&JbjB%J0_Ia+8URC$ro;CSw(_r@seoD|jn%kC_ zewp8El+JMC(~D^{4GDnh56Q?)U-LaM<03~?q>z2!wr3A>b#YtAO(2`Vx{!{#jq4i{ z*fTRoJfEk4*mM_}tc~gHd&Bqr{$dqUEVCh;o3ZF2pJNb^|U4P(1@ z42bt&myX*8t)bFVbnhcu^4sfLd|OYp35D7=X1{kD{Ush8n2=xS`|w??C|N~7rDSVo zfUaF}_D<3*u9d+ac+(gq`LGQtauW`-&E;}rBQscTpfKcc_tfY0%qjJL7Ula-|>V0Q+Pff7s8!tmA+gv6=KSH3l&_qG{F|+9_D>%gZFP@ zHOyM8dDF2l84(xBDY&OKdNL`vwP5|yyP124tl1Jzc92RwgpkCjJQGPC=1(B`jl{<^ zVTJQ#D*ZysuFlUAZQ{r#F|Wu47i!jbjYj_JrpfL?gE~kK5(h_G98Sweb53G6%FhpT zvT{>PuVbUCZ@u+`+JB~q>+^i@sH+RDStHsjT0LQ=aZg(uBr1qczvWj@R_?TOV1KsY66~lZ9!n+EhiyM(Z%wV@wBG^M?kp0OH!3!65qcnRNXVn z@y9fxmOHF6qJtpgax<6~F*EM_%geQkK<{`>SZ3NCArBFU99DnhrlYtkHb-2Mbi%HI9U3QkWDZ3e0NUy0v@ znCFvPyc=rEN_N10%*1gWrBEGX|Pen8tvQN|R zFz44qrHM32R9RgXn$Xb#u@d(`^P(3q8GwRZ} zTT@b?Yj~sj<%|PCCp=md3M0k93c<;f-Sk~il^cOC%*q)}bE_Ht`iIZ5DJLk*#CW?? zd{#~QYIi0v80Q(mNVk99>gT(qQz)Rb?w?}5pYd<78FL|w?T8rI_>`9LIGZSWuI;G| zg?zivm9M0mUZ(GcYA`3sQ%|pH@%0GJ1feMlY5!wJ)qJXUu34r#A*|d90JplV18Ysv zkyY}Hdl9x=e|VjJQATR^h=uM@xf-j|AbCzV_N z%pZ+R-DRx7i1|k}%i;$#6m*0gN9xA4%oFy{ex_x^Qxr_G&;D)@9lqKaaK9IfRY;At z4p%RT?_$MMXQ;u^9`Pn`4N@Vf#yS#K1MX`rTXcMv+zN&>S>=aBU?}CCG1*M-OV{9p z31~S*ye3$;l4Ky6jhEK$IVS+%M%P)fZWA>GI!@rbL zig9nMY>!!66N8M_?r6-paUUt9VXccx>}zszc;tYMS9ns(>!`TGg`LTh#}Y8=;| zyQwWVxF?GUqrs2&M^&KowV-O+ac6+AQ%AubBN`(z;j4MTQ^-vT>ak-3zF$?;YI|Fl z=et?&dRnQG;iFAV#`aM>`R|xICs^9hfY6XAgrlId+cy4n$dgX@Dhi}rm+^ru&)EBD zq3?841OHpmX z=N`Gz#c%(+htWl4ye*LmOimTy^gh40>ig`iThUdJmR=IuKsh+NB+B5#BP_EsD?{#_ zxm;3SGC7)tvhT25YRMgiANcCwZGdt5QZZ2{kK%CnfA>$Vmx6k$Q8$~!EFkE`?V43F zTqHI#c>W*9ej5Evr}?N~HRFTQ5#R)BOaKy~Z5xZ8oXvNJfPyg-K0T8c@I;NFXf~mp z{miePBa?)P*aigDk+$-A)XvTOdY2LM=;B;ZCwIoy@)jfQ_ua*utAWRdIld3yOqicp z#mX>yk92@40R7Ckp>ctY`ZbQ4A!F0}?F^Ov!s|H-$T&l+L;Dxevu;gLz!ly(kSH)n z^M4=pt|$c{Q53)3fl3;Iirh;=Y3?=sl@YjC+~xgI;O`Jjc^&es02|D#L!30t%Ta8* zIiQ8dYY4NgpPy)KBKrGx86eF%GvA#Wph3G6Yc-<%n<`l0TViMb$Y7yUc#6no(l48J zYso||yH5BsR7I~&i|X%*9{^J{XTu8#7#$;_of#J1iQP`?Yu>3N3^~TsG76=Ht7zGXi&mWp(tbAWSg!1U z0%jpD{iW_9u2WUoz(|l^HGY&eWH+%IeCbAeZxE8TotWY7@B2E+ zSvg16k-_=}Y@bpDIfLiFbeqJt+8kphaDc6~tqPHX{LR_!N31EYLV`U;Rg#LQg&P5{ zq&IaRMwB?1Oq0g`#!VCiza!=QmL&Fj zrWG*XM?a@XBuV$z{yiSgG_3Sxl&Kal9Wnv%Cn;=RWz7QeN8afcktgstu}s5xn%S#J zbH)4dj(M)gT<-c?RG%LrlRQYCqxl$eX!Qeo-CqsPZt$4aV3hpf_oqH~WEQZ9^*HwD z9INoUOd}PNt(eX?^-OOe2)tTVbjDIAT{O0Yv9K&(l$Lz;7~C`){fbBUP}<;+TMO<; z6P4X1$^}1ugILGrGGW9(d{&2~3x7{d%>`63p|#_10HY*Q23yG1pN}=7C1=J=jD>FS z2i#3LkJ0QMDfX?7zVe*?AL;pbfsM{{S4vwtHe8uRTvxGL!8uT~8MOZ`Jy%c^bZ)><+(k3;AjWsUsPi;)vRQA;~8(_U@ESo)Pe+95yM>n#q#iZhac^q-_2ZCmU zvKPIlY@Ehv---*>I4`rTM*n;Zbblsz`z%G{%?y&Xz;r<^g~Vjs*;FT+=B`0L&cE+H zE6NtNUO)By3$q(R2I<}=@Cj<~_xclv%tSI(qC@jWFKg!qucqO9I>6YoWPQQi<0uF6 zRNRn~p9$utF;r6M+yuXsut|BE$#|c~+7zuMbq&NfO&y9cC0{rOTb&VCmDXe>>&D7D z_x8VVeJzvrR#Fwd0ebt`;_x=K0_XWXSCAzm$D*Q5J)9O6zNq9}?*u|b0UMy79vXr; zP&hjH_p?)apq@1L%il5cO}x~nrstOQ9WzOYy@ba*mT$u6pqG-*nz#CsI19D{ztf+3 zn_@v5H}#dC>8R{##`f}17A;L2woeRtw)X~poG@IbfA!EBRk?G9Fh7U#HfZnfvu8#j z_;s%x*^0VXF*kmfSSrmVM6XJkV2RnNS%MCTF6jaJp0PzR(YXQ)t)J=8 zta)~(ySU0`vQPbTjg8on{}qQE7@s!zPNz-q@kHx_v%kJipHM|v(B=!?W1=&yzi7xP z1v@4KkK*F&+f*yY?cQL40#7T{vtv>(=Mip-^VH(5HHK}86>pbU80?|$i^wgYAWh0K z4}44Z71n*k?MG56Gqh*<``$B%wgk;ObIza<%y%ygZkaw?!)|l9VjuX4=kw-RdkzBO z08-taRI%qs`0?q%?Xf+#QK&twt?WYfPb-h9u>kpWF=-mzo#jrAv(i$~rqV_EJbzz# zFJV4mIt44gi36X^UU4-OY;TkzQU(v@FWuU_=1prgSX|4g^tSv<;Ri$_|Gxc;bhcy2 zeEg~``dU5feMiDOZR&32(R`Dv7urb~mgd@r&6M_i@(-?mmybv-HL&sUnn!5#*`?<} zK^#ojJjXv+Xj=2D1GGqvl$haXt5Gg%?mO4lSfB{gWCGq$aB5=!am z*NBw^(BTJU4d;9PdXBhD9^$L(axdRS9|s>6l6%``Rb_R9=q;b{nr=Z&8rk4jCgF(9 znBvnPW}SCoYYsEiNIxS9GTxza(IlrDCepqhiB))4jc66ue}3@|3hPoY8$f;Ifqjz5 zu%}9^x&Cg{&2=f5FZ1d|VZx?(b&fW$M(isn$`zrjll>qPzP6lf11xKg8n+|Br$Vco ztMd6qCWXAJFoS*FBJJM1nMvc9Z_F!#%(6=?@89{dk9tYVwARNg&!>bC^Q?+IzvviR zAA&)DkOAz}oWk!?0;NoH`-q!Gd`*l~9JQJH?=26;dz-RaF=_!usPgJ#ndD`W799}F zV9bIU7%${s@7k<>|Ju}KRjh;CM6HS9sKb8i#l69AL~;=^7;g&R8Pg2_!spyZK|=tv zlwp8gJ#~ypM~~v(*Ip}wB>Q_H&f)J9s*x_B9qyc}-Lw(({hw{E$V^^@GC9n@`*a`C zDRepykezq;{8B#TQ{HP$n$=24B|P!lhk-W**lUjNF(MuePFImK%LE)@IG9f7*q=J) zAV`4MTY5STeJS{_N`fuF3NHmPI^ci}c_J)S{HuySc zi)1(Yl#4!Z>N(+g@g{&mO|fVBMNsu1&dhT_1b*$Q#CTCl4L49mI!Ec