From b198f33df14ff63c7859bb31b6d14241bc823b40 Mon Sep 17 00:00:00 2001 From: Manolis Papadakis Date: Sat, 16 Nov 2024 19:22:53 -0500 Subject: [PATCH] 24.11.00 release --- .clang-format | 3 +- .gitattributes | 2 +- .github/CODEOWNERS | 4 + .github/ISSUE_TEMPLATE/bug_report.yml | 33 +- .github/actions/download-artifacts/action.yml | 48 - .github/workflows/ci-gh.yml | 28 - .github/workflows/gh-build-and-test.yml | 92 - .github/workflows/gh-build.yml | 101 - .github/workflows/gh-test.yml | 91 - .github/workflows/require-labels.yml | 15 - .gitignore | 8 +- .pre-commit-config.yaml | 11 +- CMakeLists.txt | 36 +- CONTRIBUTING.md | 12 +- LICENSES_bundled.txt | 39 - MANIFEST.in | 4 +- README.md | 154 +- cmake/Modules/cpm_helpers.cmake | 4 +- cmake/Modules/cuda_arch_helpers.cmake | 8 +- cmake/Modules/set_cpu_arch_flags.cmake | 2 +- cmake/generate_install_info_py.cmake | 10 +- cmake/thirdparty/get_cutensor.cmake | 2 +- cmake/thirdparty/get_legate.cmake | 104 + cmake/thirdparty/get_legate_core.cmake | 103 - cmake/thirdparty/get_nccl.cmake | 2 +- cmake/thirdparty/get_openblas.cmake | 38 +- cmake/thirdparty/get_tblis.cmake | 30 +- cmake/versions.json | 13 +- conda/conda-build/build.sh | 39 +- conda/conda-build/conda_build_config.yaml | 7 +- conda/conda-build/meta.yaml | 124 +- continuous_integration/dot-gitconfig | 3 - .../scripts/build-cunumeric-all | 38 - .../scripts/build-cunumeric-conda | 85 - .../scripts/build-cunumeric-cpp | 33 - .../scripts/build-cunumeric-wheel | 27 - continuous_integration/scripts/entrypoint | 46 - continuous_integration/scripts/test-cunumeric | 61 - cunumeric/_ufunc/comparison.py | 122 - cunumeric/config.py | 788 -- cunumeric/install_info.py.in | 43 - cunumeric/linalg/cholesky.py | 272 - cunumeric/linalg/solve.py | 62 - cunumeric/module.py | 8284 ----------------- cunumeric/random/legacy.py | 203 - cunumeric_cpp.cmake | 505 - {cunumeric => cupynumeric}/__init__.py | 21 +- cupynumeric/_array/__init__.py | 15 + {cunumeric => cupynumeric/_array}/array.py | 1502 +-- cupynumeric/_array/flags.py | 82 + cupynumeric/_array/thunk.py | 356 + cupynumeric/_array/util.py | 221 + cupynumeric/_module/__init__.py | 146 + .../_module}/_unary_red_utils.py | 4 +- cupynumeric/_module/array_basic.py | 81 + cupynumeric/_module/array_dimension.py | 443 + cupynumeric/_module/array_joining.py | 702 ++ cupynumeric/_module/array_rearrange.py | 289 + cupynumeric/_module/array_shape.py | 119 + cupynumeric/_module/array_splitting.py | 246 + cupynumeric/_module/array_tiling.py | 232 + cupynumeric/_module/array_transpose.py | 136 + .../_module/binary_bit_packing.py | 22 +- cupynumeric/_module/creation_data.py | 177 + cupynumeric/_module/creation_matrices.py | 192 + cupynumeric/_module/creation_ranges.py | 355 + cupynumeric/_module/creation_shape.py | 406 + cupynumeric/_module/indexing.py | 1243 +++ cupynumeric/_module/io_numpy.py | 75 + cupynumeric/_module/linalg_mvp.py | 935 ++ cupynumeric/_module/logic_array_contents.py | 114 + .../_module/logic_array_type.py | 121 +- cupynumeric/_module/logic_comparison.py | 201 + cupynumeric/_module/logic_truth.py | 138 + cupynumeric/_module/math_complex.py | 122 + cupynumeric/_module/math_extrema.py | 179 + cupynumeric/_module/math_misc.py | 174 + cupynumeric/_module/math_rounding.py | 57 + cupynumeric/_module/math_sum_prod_diff.py | 1338 +++ cupynumeric/_module/sets_making.py | 104 + cupynumeric/_module/ssc_counting.py | 57 + cupynumeric/_module/ssc_searching.py | 357 + cupynumeric/_module/ssc_sorting.py | 291 + cupynumeric/_module/stats_avgs_vars.py | 518 ++ cupynumeric/_module/stats_correlating.py | 187 + cupynumeric/_module/stats_histograms.py | 388 + cupynumeric/_module/stats_order.py | 1063 +++ {cunumeric => cupynumeric/_module}/window.py | 8 +- .../_sphinxext/__init__.py | 2 +- .../_sphinxext/_comparison_config.py | 7 +- .../_sphinxext/_comparison_util.py | 18 +- .../_sphinxext/_cupynumeric_directive.py | 8 +- .../_sphinxext/_templates.py | 2 +- .../_templates/comparison_table.rst | 6 +- .../_sphinxext/comparison_table.py | 6 +- .../_sphinxext/implemented_index.py | 12 +- .../_sphinxext/missing_refs.py | 44 +- .../_sphinxext/ufunc_formatter.py | 4 +- cupynumeric/_thunk/__init__.py | 15 + .../sort.py => cupynumeric/_thunk/_sort.py | 51 +- {cunumeric => cupynumeric/_thunk}/deferred.py | 1391 +-- {cunumeric => cupynumeric/_thunk}/eager.py | 454 +- {cunumeric => cupynumeric/_thunk}/thunk.py | 174 +- {cunumeric => cupynumeric}/_ufunc/__init__.py | 2 +- .../_ufunc/bit_twiddling.py | 5 +- cupynumeric/_ufunc/comparison.py | 212 + {cunumeric => cupynumeric}/_ufunc/floating.py | 5 +- {cunumeric => cupynumeric}/_ufunc/math.py | 5 +- .../_ufunc/trigonometric.py | 5 +- {cunumeric => cupynumeric}/_ufunc/ufunc.py | 227 +- cupynumeric/_utils/__init__.py | 19 + cupynumeric/_utils/array.py | 113 + {cunumeric => cupynumeric/_utils}/coverage.py | 69 +- .../utils.py => cupynumeric/_utils/linalg.py | 133 +- cupynumeric/_utils/stack.py | 52 + cupynumeric/_utils/structure.py | 37 + {cunumeric => cupynumeric}/_version.py | 4 +- cupynumeric/config.py | 835 ++ {cunumeric => cupynumeric}/fft/__init__.py | 8 +- {cunumeric => cupynumeric}/fft/fft.py | 156 +- cupynumeric/install_info.py.in | 47 + {cunumeric => cupynumeric}/linalg/__init__.py | 9 +- cupynumeric/linalg/_cholesky.py | 305 + .../linalg/_exception.py | 2 +- cupynumeric/linalg/_qr.py | 50 + cupynumeric/linalg/_solve.py | 108 + cupynumeric/linalg/_svd.py | 60 + {cunumeric => cupynumeric}/linalg/linalg.py | 237 +- {cunumeric => cupynumeric}/ma/__init__.py | 8 +- .../ma/_masked_array.py | 16 +- {cunumeric => cupynumeric}/patch.py | 12 +- {cunumeric => cupynumeric}/py.typed | 0 {cunumeric => cupynumeric}/random/__init__.py | 18 +- .../random/_bitgenerator.py | 82 +- .../random/_generator.py | 90 +- .../random/_random.py | 189 +- {cunumeric => cupynumeric}/runtime.py | 401 +- {cunumeric => cupynumeric}/settings.py | 56 +- {cunumeric => cupynumeric}/types.py | 12 +- cupynumeric_cpp.cmake | 565 ++ ...c_python.cmake => cupynumeric_python.cmake | 52 +- docs/cunumeric/source/api/settings.rst | 8 - .../source/developer/CONTRIBUTING.md | 1 - docs/cunumeric/source/developer/building.rst | 71 - docs/cunumeric/source/index.rst | 39 - docs/cunumeric/source/user/configuration.rst | 108 - docs/cunumeric/source/user/installation.rst | 19 - docs/cunumeric/source/user/notebooks.rst | 9 - docs/cunumeric/source/user/usage.rst | 143 - docs/cunumeric/source/versions.rst | 14 - docs/{cunumeric => cupynumeric}/Makefile | 9 +- docs/{cunumeric => cupynumeric}/make.bat | 0 .../source/_images/developer-build.png | Bin .../source/_implemented.rst | 2 +- .../source/_static/.keep | 0 .../cupynumeric/source/_templates/layout.html | 7 + .../source/api/_bitgenerator.rst | 4 +- .../source/api/_generator.rst | 4 +- .../source/api}/_grouped.rst | 0 .../source/api/_ndarray.rst | 5 +- .../source/api/binary.rst | 2 +- .../source/api/broadcast.rst | 4 +- .../source/api/classes.rst | 0 docs/cupynumeric/source/api/comparison.rst | 12 + .../source/api/creation.rst | 3 +- docs/cupynumeric/source/api/datatype.rst | 12 + .../source/api/fft.rst | 16 +- .../source/api/index.rst | 3 +- .../source/api/indexing.rst | 4 +- docs/cupynumeric/source/api/io.rst | 11 + .../source/api/linalg.rst | 8 +- .../source/api/logic.rst | 2 +- .../source/api/manipulation.rst | 7 +- .../source/api/math.rst | 6 +- .../source/api/ndarray.rst | 7 +- .../source/api/random.rst | 4 +- .../source/api/routines.rst | 2 + .../source/api/set.rst | 2 +- docs/cupynumeric/source/api/settings.rst | 8 + .../source/api/sorting.rst | 2 +- .../source/api/statistics.rst | 34 +- .../source/api/window.rst | 2 +- .../{cunumeric => cupynumeric}/source/conf.py | 52 +- .../source/developer/CONTRIBUTING.md | 72 + .../cupynumeric/source/developer/building.rst | 71 + .../source/developer/index.rst | 0 .../source/developer/testing.rst | 2 +- .../source/examples}/black_scholes.ipynb | 8 +- .../source/examples}/cholesky.ipynb | 12 +- .../examples/compact_finite_difference.ipynb | 336 + .../source/examples/edge_detection.ipynb | 210 + docs/cupynumeric/source/examples/image.png | Bin 0 -> 305442 bytes docs/cupynumeric/source/examples/index.rst | 14 + docs/cupynumeric/source/examples/kmeans.ipynb | 402 + .../source/examples/newton_raphson_2d.ipynb | 264 + .../source/examples}/stencil.ipynb | 6 +- .../source/examples/torchswe.ipynb | 219 + docs/cupynumeric/source/faqs.rst | 206 + docs/cupynumeric/source/index.rst | 34 + docs/cupynumeric/source/installation.rst | 63 + docs/cupynumeric/source/oss-licenses.rst | 123 + docs/cupynumeric/source/user/advanced.rst | 42 + docs/cupynumeric/source/user/differences.rst | 67 + .../source/user/howtos/benchmarking.rst | 56 + docs/cupynumeric/source/user/howtos/index.rst | 9 + .../source/user/howtos/measuring.rst} | 44 +- .../source/user/howtos/patching.rst | 35 + .../source/user/index.rst | 6 +- .../source/user/practices.rst | 164 +- docs/cupynumeric/source/user/usage.rst | 50 + docs/cupynumeric/switcher.json | 7 + examples/benchmark.py | 19 +- examples/black_scholes.py | 2 +- examples/black_scholes_greeks.py | 2 +- examples/cg.py | 2 +- examples/cholesky.py | 74 +- examples/cpp/stencil/CMakeLists.txt | 31 + examples/cpp/stencil/build.sh | 22 + examples/cpp/stencil/stencil.cc | 101 + examples/einsum.py | 2 +- examples/gemm.py | 2 +- examples/indexing_routines.py | 2 +- examples/ingest.py | 96 - examples/jacobi.py | 2 +- examples/kmeans.py | 2 +- examples/kmeans_slow.py | 2 +- examples/kmeans_sort.py | 228 - examples/linreg.py | 2 +- examples/logreg.py | 2 +- examples/lstm_backward.py | 2 +- examples/lstm_forward.py | 2 +- examples/lstm_full.py | 419 - examples/qr.py | 94 + examples/quantiles.py | 2 +- examples/richardson_lucy.py | 21 +- examples/scan.py | 4 +- examples/solve.py | 69 +- examples/sort.py | 2 +- examples/stencil.py | 2 +- examples/svd.py | 131 + examples/wgrad.py | 107 - install.py | 183 +- pyproject.toml | 6 +- scripts/api_compare.py | 20 +- scripts/build-install.sh | 37 - scripts/build-no-install.sh | 36 - scripts/build-separately-no-install.sh | 53 - scripts/build-with-legate-no-install.sh | 37 - ...build-with-legate-separately-no-install.sh | 54 - scripts/conda-build.sh | 13 +- scripts/hooks/enforce_boilerplate.py | 2 +- scripts/hooks/legate_defined.sh | 43 + scripts/util/build-caching.sh | 17 - scripts/util/compiler-flags.sh | 12 - scripts/util/read-legate-core-root.sh | 16 - ...global-legion-legate-core-and-cunumeric.sh | 10 - setup.cfg | 14 +- setup.py | 14 +- src/cunumeric/arg_redop_register.h | 56 - src/cunumeric/binary/binary_op.h | 46 - src/cunumeric/binary/binary_red.h | 46 - src/cunumeric/cunumeric.cc | 62 - src/cunumeric/cunumeric_c.h | 345 - src/cunumeric/index/zip.h | 67 - src/cunumeric/mapper.cc | 251 - src/cunumeric/random/bitgenerator_util.h | 98 - src/cunumeric/random/curand_help.h | 46 - .../random/randutil/generator_create.inl | 50 - .../random/randutil/generator_integers.inl | 59 - src/cunumeric/random/randutil/randutil.h | 230 - src/cunumeric/scan/scan_global_util.h | 58 - src/cunumeric/stat/bincount.h | 43 - src/cupynumeric.h | 19 + src/{cunumeric => cupynumeric}/arg.h | 8 +- src/{cunumeric => cupynumeric}/arg.inl | 6 +- .../arg_redop_register.cc | 27 +- .../arg_redop_register.cu | 10 +- src/cupynumeric/arg_redop_register.h | 50 + .../binary/binary_op.cc | 18 +- .../binary/binary_op.cu | 26 +- src/cupynumeric/binary/binary_op.h | 46 + .../binary/binary_op_omp.cc | 18 +- .../binary/binary_op_template.inl | 30 +- src/cupynumeric/binary/binary_op_util.cc | 49 + .../binary/binary_op_util.h | 300 +- .../binary/binary_red.cc | 17 +- .../binary/binary_red.cu | 34 +- src/cupynumeric/binary/binary_red.h | 46 + .../binary/binary_red_omp.cc | 25 +- .../binary/binary_red_template.inl | 34 +- .../bits/bits_util.h | 12 +- .../bits/packbits.cc | 14 +- .../bits/packbits.cu | 22 +- .../bits/packbits.h | 24 +- .../bits/packbits_omp.cc | 14 +- .../bits/packbits_template.inl | 35 +- .../bits/unpackbits.cc | 12 +- .../bits/unpackbits.cu | 20 +- .../bits/unpackbits.h | 24 +- .../bits/unpackbits_omp.cc | 12 +- .../bits/unpackbits_template.inl | 30 +- .../cephes/chbevl.cc | 0 src/{cunumeric => cupynumeric}/cephes/i0.cc | 6 +- .../convolution/convolve.cc | 86 +- .../convolution/convolve.cu | 415 +- .../convolution/convolve.h | 29 +- .../convolution/convolve_omp.cc | 90 +- .../convolution/convolve_template.inl | 180 +- src/{cunumeric => cupynumeric}/cuda_help.h | 332 +- src/{cunumeric => cupynumeric}/cudalibs.cu | 211 +- src/{cunumeric => cupynumeric}/cudalibs.h | 23 +- src/cupynumeric/cupynumeric.cc | 82 + src/cupynumeric/cupynumeric.cu | 45 + src/cupynumeric/cupynumeric_c.h | 355 + .../cupynumeric_task.h} | 17 +- .../device_scalar_reduction_buffer.h | 18 +- src/{cunumeric => cupynumeric}/divmod.h | 18 +- .../indexing/parallel_loop.cuh | 22 +- .../execution_policy/indexing/parallel_loop.h | 12 +- .../indexing/parallel_loop_omp.h | 16 +- .../reduction/scalar_reduction.cuh | 20 +- .../reduction/scalar_reduction.h | 12 +- .../reduction/scalar_reduction_omp.h | 22 +- src/{cunumeric => cupynumeric}/fft/fft.cu | 102 +- src/{cunumeric => cupynumeric}/fft/fft.h | 26 +- .../fft/fft_template.inl | 45 +- src/{cunumeric => cupynumeric}/fft/fft_util.h | 50 +- .../index/advanced_indexing.cc | 44 +- .../index/advanced_indexing.cu | 47 +- .../index/advanced_indexing.h | 28 +- .../index/advanced_indexing_omp.cc | 44 +- .../index/advanced_indexing_template.inl | 23 +- .../index/choose.cc | 22 +- .../index/choose.cu | 34 +- src/cupynumeric/index/choose.h | 42 + .../index/choose_omp.cc | 18 +- .../index/choose_template.inl | 26 +- .../index/putmask.cc | 12 +- .../index/putmask.cu | 14 +- src/cupynumeric/index/putmask.h | 43 + .../index/putmask_omp.cc | 14 +- .../index/putmask_template.inl | 40 +- .../index/repeat.cc | 33 +- .../index/repeat.cu | 57 +- src/{cunumeric => cupynumeric}/index/repeat.h | 28 +- .../index/repeat_omp.cc | 38 +- .../index/repeat_template.inl | 38 +- .../index/select.cc | 28 +- .../index/select.cu | 51 +- src/{cunumeric => cupynumeric}/index/select.h | 26 +- .../index/select_omp.cc | 28 +- .../index/select_template.inl | 32 +- src/{cunumeric => cupynumeric}/index/wrap.cc | 20 +- src/{cunumeric => cupynumeric}/index/wrap.cu | 38 +- src/{cunumeric => cupynumeric}/index/wrap.h | 47 +- .../index/wrap_omp.cc | 30 +- .../index/wrap_template.inl | 40 +- src/{cunumeric => cupynumeric}/index/zip.cc | 18 +- src/{cunumeric => cupynumeric}/index/zip.cu | 52 +- src/cupynumeric/index/zip.h | 69 + .../index/zip_omp.cc | 34 +- .../index/zip_template.inl | 43 +- src/{cunumeric => cupynumeric}/item/read.cc | 12 +- src/{cunumeric => cupynumeric}/item/read.cu | 16 +- .../putmask.h => cupynumeric/item/read.h} | 28 +- .../item/read_template.inl | 18 +- src/{cunumeric => cupynumeric}/item/write.cc | 20 +- src/{cunumeric => cupynumeric}/item/write.cu | 30 +- .../item/read.h => cupynumeric/item/write.h} | 22 +- .../item/write_template.inl | 27 +- src/cupynumeric/mapper.cc | 229 + src/{cunumeric => cupynumeric}/mapper.h | 31 +- .../matrix/batched_cholesky.cc | 24 +- .../matrix/batched_cholesky.cu | 24 +- .../matrix/batched_cholesky.h | 24 +- .../matrix/batched_cholesky_omp.cc | 20 +- .../matrix/batched_cholesky_template.inl | 51 +- .../matrix/contract.cc | 14 +- .../matrix/contract.cu | 142 +- .../matrix/contract.h | 28 +- .../matrix/contract_omp.cc | 14 +- .../matrix/contract_template.inl | 56 +- src/{cunumeric => cupynumeric}/matrix/diag.cc | 26 +- src/{cunumeric => cupynumeric}/matrix/diag.cu | 34 +- src/{cunumeric => cupynumeric}/matrix/diag.h | 26 +- .../matrix/diag_omp.cc | 22 +- .../matrix/diag_template.inl | 36 +- src/{cunumeric => cupynumeric}/matrix/dot.cc | 16 +- src/{cunumeric => cupynumeric}/matrix/dot.cu | 21 +- src/{cunumeric => cupynumeric}/matrix/dot.h | 28 +- .../matrix/dot_omp.cc | 24 +- .../matrix/dot_template.inl | 20 +- src/{cunumeric => cupynumeric}/matrix/gemm.cc | 14 +- src/{cunumeric => cupynumeric}/matrix/gemm.cu | 18 +- src/{cunumeric => cupynumeric}/matrix/gemm.h | 22 +- .../matrix/gemm_omp.cc | 12 +- .../matrix/gemm_template.inl | 28 +- .../matrix/matmul.cc | 18 +- .../matrix/matmul.cu | 24 +- .../matrix/matmul.h | 28 +- .../matrix/matmul_cpu.inl | 22 +- .../matrix/matmul_omp.cc | 14 +- .../matrix/matmul_template.inl | 84 +- .../matrix/matvecmul.cc | 18 +- .../matrix/matvecmul.cu | 44 +- .../matrix/matvecmul.h | 28 +- .../matrix/matvecmul_cpu.inl | 12 +- .../matrix/matvecmul_omp.cc | 14 +- .../matrix/matvecmul_template.inl | 26 +- src/cupynumeric/matrix/mp_potrf.cu | 146 + .../matrix/mp_potrf.h} | 17 +- src/cupynumeric/matrix/mp_potrf_template.inl | 167 + src/cupynumeric/matrix/mp_solve.cu | 247 + src/cupynumeric/matrix/mp_solve.h | 33 + src/cupynumeric/matrix/mp_solve_template.inl | 195 + .../matrix/potrf.cc | 30 +- .../matrix/potrf.cu | 22 +- src/{cunumeric => cupynumeric}/matrix/potrf.h | 22 +- .../matrix/potrf_omp.cc | 28 +- .../matrix/potrf_template.inl | 22 +- src/cupynumeric/matrix/qr.cc | 40 + src/cupynumeric/matrix/qr.cu | 190 + .../solve.h => cupynumeric/matrix/qr.h} | 22 +- src/cupynumeric/matrix/qr_cpu.inl | 131 + src/cupynumeric/matrix/qr_omp.cc | 31 + src/cupynumeric/matrix/qr_template.inl | 102 + .../matrix/solve.cc | 16 +- .../matrix/solve.cu | 24 +- src/cupynumeric/matrix/solve.h | 38 + .../matrix/solve_cpu.inl | 22 +- .../matrix/solve_omp.cc | 14 +- .../matrix/solve_template.inl | 38 +- src/cupynumeric/matrix/svd.cc | 40 + src/cupynumeric/matrix/svd.cu | 194 + .../choose.h => cupynumeric/matrix/svd.h} | 28 +- src/cupynumeric/matrix/svd_cpu.inl | 257 + src/cupynumeric/matrix/svd_omp.cc | 31 + src/cupynumeric/matrix/svd_template.inl | 137 + src/{cunumeric => cupynumeric}/matrix/syrk.cc | 14 +- src/{cunumeric => cupynumeric}/matrix/syrk.cu | 16 +- .../write.h => cupynumeric/matrix/syrk.h} | 22 +- .../matrix/syrk_omp.cc | 12 +- .../matrix/syrk_template.inl | 24 +- src/{cunumeric => cupynumeric}/matrix/tile.cc | 12 +- src/{cunumeric => cupynumeric}/matrix/tile.cu | 20 +- src/{cunumeric => cupynumeric}/matrix/tile.h | 26 +- .../matrix/tile_omp.cc | 12 +- .../matrix/tile_template.inl | 21 +- .../matrix/transpose.cc | 25 +- .../matrix/transpose.cu | 36 +- .../matrix/transpose.h | 26 +- .../matrix/transpose_omp.cc | 21 +- .../matrix/transpose_template.inl | 20 +- .../matrix/trilu.cc | 29 +- .../matrix/trilu.cu | 32 +- src/{cunumeric => cupynumeric}/matrix/trilu.h | 26 +- .../matrix/trilu_omp.cc | 24 +- .../matrix/trilu_template.inl | 41 +- src/{cunumeric => cupynumeric}/matrix/trsm.cc | 14 +- src/{cunumeric => cupynumeric}/matrix/trsm.cu | 16 +- src/{cunumeric => cupynumeric}/matrix/trsm.h | 22 +- .../matrix/trsm_omp.cc | 12 +- .../matrix/trsm_template.inl | 24 +- src/{cunumeric => cupynumeric}/matrix/util.cc | 57 +- src/{cunumeric => cupynumeric}/matrix/util.h | 10 +- src/cupynumeric/ndarray.cc | 1903 ++++ src/cupynumeric/ndarray.h | 175 + src/cupynumeric/ndarray.inl | 33 + .../nullary/arange.cc | 15 +- .../nullary/arange.cu | 20 +- .../nullary/arange.h | 29 +- .../nullary/arange_omp.cc | 15 +- .../nullary/arange_template.inl | 27 +- src/{cunumeric => cupynumeric}/nullary/eye.cc | 16 +- src/{cunumeric => cupynumeric}/nullary/eye.cu | 20 +- src/{cunumeric => cupynumeric}/nullary/eye.h | 24 +- .../nullary/eye_omp.cc | 16 +- .../nullary/eye_template.inl | 22 +- .../nullary/fill.cc | 16 +- .../nullary/fill.cu | 24 +- src/{cunumeric => cupynumeric}/nullary/fill.h | 27 +- .../nullary/fill_omp.cc | 16 +- .../nullary/fill_template.inl | 41 +- .../nullary/window.cc | 20 +- .../nullary/window.cu | 24 +- .../nullary/window.h | 22 +- .../nullary/window_omp.cc | 20 +- .../nullary/window_template.inl | 27 +- .../nullary/window_util.h | 18 +- src/{cunumeric => cupynumeric}/omp_help.h | 6 +- src/cupynumeric/operators.cc | 614 ++ src/cupynumeric/operators.h | 200 + src/cupynumeric/operators.inl | 38 + src/{cunumeric => cupynumeric}/pitches.h | 21 +- .../random/bitgenerator.cc | 58 +- .../random/bitgenerator.cu | 43 +- .../random/bitgenerator.h | 34 +- .../random/bitgenerator_curand.inl | 294 +- .../random/bitgenerator_template.inl | 40 +- src/cupynumeric/random/bitgenerator_util.h | 98 + src/cupynumeric/random/curand_help.h | 64 + .../random/philox.h | 6 +- src/{cunumeric => cupynumeric}/random/rand.cc | 16 +- src/{cunumeric => cupynumeric}/random/rand.cu | 24 +- src/{cunumeric => cupynumeric}/random/rand.h | 28 +- .../random/rand_omp.cc | 16 +- .../random/rand_template.inl | 38 +- .../random/rand_util.h | 32 +- .../random/randutil/generator.cuh | 43 +- .../random/randutil/generator.h | 92 +- .../random/randutil/generator_beta.inl | 2 +- .../random/randutil/generator_binomial.inl | 2 +- .../random/randutil/generator_cauchy.inl | 18 +- .../random/randutil/generator_chisquare.inl | 2 +- .../random/randutil/generator_create.inl | 51 + .../random/randutil/generator_device.cu | 2 +- .../randutil/generator_device_advanced.cu | 2 +- .../generator_device_straightforward.cu | 2 +- .../random/randutil/generator_exponential.inl | 8 +- .../random/randutil/generator_f.inl | 2 +- .../random/randutil/generator_gamma.inl | 2 +- .../random/randutil/generator_geometric.inl | 2 +- .../random/randutil/generator_gumbel.inl | 18 +- .../random/randutil/generator_host.cc | 83 +- .../randutil/generator_host_advanced.cc | 168 +- .../generator_host_straightforward.cc | 126 +- .../randutil/generator_hypergeometric.inl | 2 +- .../random/randutil/generator_integers.inl | 56 + .../random/randutil/generator_laplace.inl | 26 +- .../random/randutil/generator_logistic.inl | 18 +- .../random/randutil/generator_lognormal.inl | 8 +- .../random/randutil/generator_logseries.inl | 2 +- .../randutil/generator_negative_binomial.inl | 6 +- .../random/randutil/generator_normal.inl | 8 +- .../random/randutil/generator_pareto.inl | 12 +- .../random/randutil/generator_poisson.inl | 6 +- .../random/randutil/generator_power.inl | 8 +- .../random/randutil/generator_raw.inl | 6 +- .../random/randutil/generator_rayleigh.inl | 8 +- .../random/randutil/generator_standard_t.inl | 2 +- .../random/randutil/generator_triangular.inl | 36 +- .../random/randutil/generator_uniform.inl | 10 +- .../random/randutil/generator_vonmises.inl | 2 +- .../random/randutil/generator_wald.inl | 22 +- .../random/randutil/generator_weibull.inl | 20 +- .../random/randutil/generator_zipf.inl | 2 +- .../random/randutil/random_distributions.h | 160 +- src/cupynumeric/random/randutil/randomizer.h | 143 + src/cupynumeric/random/randutil/randutil.h | 232 + .../random/randutil/randutil_curand.h | 9 +- .../random/randutil/randutil_impl.h | 2 +- src/cupynumeric/random/rnd_aliases.h | 75 + src/cupynumeric/random/rnd_types.h | 63 + src/cupynumeric/runtime.cc | 178 + src/cupynumeric/runtime.h | 73 + .../scan/scan_global.cc | 14 +- .../scan/scan_global.cu | 24 +- .../scan/scan_global.h | 28 +- .../scan/scan_global_omp.cc | 14 +- .../scan/scan_global_template.inl | 19 +- .../scan/scan_local.cc | 24 +- .../scan/scan_local.cu | 36 +- .../scan/scan_local.h | 30 +- .../scan/scan_local_omp.cc | 24 +- .../scan/scan_local_template.inl | 24 +- .../scan/scan_local_util.h | 16 +- src/cupynumeric/scan/scan_util.h | 90 + .../search/argwhere.cc | 22 +- .../search/argwhere.cu | 32 +- .../search/argwhere.h | 26 +- .../search/argwhere_omp.cc | 34 +- .../search/argwhere_template.inl | 14 +- .../search/nonzero.cc | 25 +- .../search/nonzero.cu | 37 +- .../search/nonzero.cuh | 15 +- .../search/nonzero.h | 26 +- .../search/nonzero_omp.cc | 39 +- .../search/nonzero_template.inl | 22 +- src/{cunumeric => cupynumeric}/set/unique.cc | 20 +- src/{cunumeric => cupynumeric}/set/unique.cu | 49 +- src/{cunumeric => cupynumeric}/set/unique.h | 22 +- .../set/unique_omp.cc | 20 +- .../set/unique_reduce.cc | 12 +- .../set/unique_reduce.h} | 21 +- .../set/unique_reduce_omp.cc | 12 +- .../set/unique_reduce_template.inl | 24 +- .../set/unique_template.inl | 24 +- src/cupynumeric/slice.h | 27 + .../sort/cub_sort.cuh | 24 +- .../sort/cub_sort.h | 6 +- .../sort/cub_sort_bool.cu | 8 +- .../sort/cub_sort_double.cu | 8 +- .../sort/cub_sort_float.cu | 8 +- .../sort/cub_sort_half.cu | 8 +- .../sort/cub_sort_int16.cu | 8 +- .../sort/cub_sort_int32.cu | 8 +- .../sort/cub_sort_int64.cu | 8 +- .../sort/cub_sort_int8.cu | 8 +- .../sort/cub_sort_uint16.cu | 8 +- .../sort/cub_sort_uint32.cu | 8 +- .../sort/cub_sort_uint64.cu | 8 +- .../sort/cub_sort_uint8.cu | 8 +- .../sort/searchsorted.cc | 28 +- .../sort/searchsorted.cu | 40 +- .../sort/searchsorted.h | 28 +- .../sort/searchsorted_omp.cc | 28 +- .../sort/searchsorted_template.inl | 30 +- src/{cunumeric => cupynumeric}/sort/sort.cc | 21 +- src/{cunumeric => cupynumeric}/sort/sort.cu | 324 +- src/{cunumeric => cupynumeric}/sort/sort.h | 30 +- .../sort/sort_cpu.inl | 261 +- .../sort/sort_omp.cc | 21 +- .../sort/sort_template.inl | 42 +- .../sort/thrust_sort.cuh | 18 +- .../sort/thrust_sort.h | 6 +- .../sort/thrust_sort_bool.cu | 8 +- .../sort/thrust_sort_complex128.cu | 8 +- .../sort/thrust_sort_complex64.cu | 8 +- .../sort/thrust_sort_double.cu | 8 +- .../sort/thrust_sort_float.cu | 8 +- .../sort/thrust_sort_half.cu | 8 +- .../sort/thrust_sort_int16.cu | 8 +- .../sort/thrust_sort_int32.cu | 8 +- .../sort/thrust_sort_int64.cu | 8 +- .../sort/thrust_sort_int8.cu | 8 +- .../sort/thrust_sort_uint16.cu | 8 +- .../sort/thrust_sort_uint32.cu | 8 +- .../sort/thrust_sort_uint64.cu | 8 +- .../sort/thrust_sort_uint8.cu | 8 +- .../stat/bincount.cc | 18 +- .../stat/bincount.cu | 48 +- src/cupynumeric/stat/bincount.h | 44 + .../stat/bincount_omp.cc | 34 +- .../stat/bincount_template.inl | 32 +- .../stat/histogram.cc | 18 +- .../stat/histogram.cu | 22 +- .../stat/histogram.cuh | 12 +- .../stat/histogram.h | 30 +- .../stat/histogram_cpu.h | 17 +- .../stat/histogram_gen.h | 6 +- .../stat/histogram_impl.h | 35 +- .../stat/histogram_omp.cc | 18 +- .../stat/histogram_template.inl | 18 +- .../ternary/where.cc | 17 +- .../ternary/where.cu | 26 +- .../ternary/where.h | 30 +- .../ternary/where_omp.cc | 17 +- .../ternary/where_template.inl | 22 +- .../transform/flip.cc | 17 +- .../transform/flip.cu | 30 +- .../transform/flip.h | 26 +- .../transform/flip_omp.cc | 17 +- .../transform/flip_template.inl | 22 +- src/cupynumeric/typedefs.h | 28 + .../unary/convert.cc | 20 +- .../unary/convert.cu | 28 +- .../unary/convert.h | 28 +- .../unary/convert_omp.cc | 20 +- .../unary/convert_template.inl | 25 +- .../unary/convert_util.h | 77 +- src/{cunumeric => cupynumeric}/unary/isnan.h | 8 +- .../unary/scalar_unary_red.cc | 12 +- .../unary/scalar_unary_red.cu | 16 +- .../unary/scalar_unary_red.h | 32 +- .../unary/scalar_unary_red_omp.cc | 14 +- .../unary/scalar_unary_red_template.inl | 115 +- .../unary/unary_op.cc | 24 +- .../unary/unary_op.cu | 44 +- .../unary/unary_op.h | 56 +- .../unary/unary_op_omp.cc | 24 +- .../unary/unary_op_template.inl | 55 +- .../unary/unary_op_util.h | 436 +- .../unary/unary_red.cc | 18 +- .../unary/unary_red.cu | 74 +- .../unary/unary_red.h | 30 +- .../unary/unary_red_omp.cc | 29 +- .../unary/unary_red_template.inl | 42 +- .../unary/unary_red_util.h | 102 +- src/cupynumeric/utilities/repartition.cc | 71 + src/cupynumeric/utilities/repartition.cu | 1357 +++ src/cupynumeric/utilities/repartition.h | 95 + .../utilities/thrust_allocator.h | 6 +- .../utilities/thrust_util.h | 2 +- src/env_defaults.h | 8 +- test.py | 34 +- tests/cpp/.gitignore | 1 + tests/cpp/CMakeLists.txt | 75 + tests/cpp/cmake/thirdparty/get_nccl.cmake | 34 + tests/cpp/integration/common_utils.cc | 171 + tests/cpp/integration/common_utils.h | 196 + tests/cpp/integration/test_amax.cc | 339 + tests/cpp/integration/test_amin.cc | 339 + tests/cpp/integration/test_arange.cc | 91 + tests/cpp/integration/test_argsort.cc | 477 + tests/cpp/integration/test_argwhere.cc | 298 + tests/cpp/integration/test_bincount.cc | 106 + tests/cpp/integration/test_convolve.cc | 135 + tests/cpp/integration/test_diagonal.cc | 311 + tests/cpp/integration/test_dot.cc | 73 + tests/cpp/integration/test_eye.cc | 296 + tests/cpp/integration/test_fill.cc | 91 + tests/cpp/integration/test_flip.cc | 666 ++ tests/cpp/integration/test_logical.cc | 450 + tests/cpp/integration/test_moveaxis.cc | 150 + tests/cpp/integration/test_msort.cc | 398 + tests/cpp/integration/test_nonzero.cc | 273 + tests/cpp/integration/test_put.cc | 292 + tests/cpp/integration/test_repartition.cc | 459 + tests/cpp/integration/test_repeat.cc | 352 + tests/cpp/integration/test_reshape.cc | 232 + tests/cpp/integration/test_sort.cc | 632 ++ tests/cpp/integration/test_sort_complex.cc | 442 + tests/cpp/integration/test_squeeze.cc | 212 + tests/cpp/integration/test_swapaxes.cc | 103 + tests/cpp/integration/test_transpose.cc | 139 + tests/cpp/integration/test_trilu.cc | 123 + tests/cpp/integration/test_unique.cc | 96 + tests/cpp/integration/test_where.cc | 273 + tests/cpp/integration/test_window.cc | 196 + tests/cpp/integration/test_zeros.cc | 108 + tests/cpp/integration/util.inl | 247 + tests/cpp/main.cc | 43 + tests/cpp/run.py | 169 + tests/cpp/run.sh | 13 + tests/integration/test_0d_store.py | 4 +- tests/integration/test_advanced_indexing.py | 48 +- tests/integration/test_allclose.py | 12 +- tests/integration/test_amax_amin.py | 12 +- tests/integration/test_angle.py | 104 + tests/integration/test_append.py | 4 +- tests/integration/test_arg_reduce.py | 9 +- tests/integration/test_argsort.py | 20 +- tests/integration/test_array.py | 8 +- tests/integration/test_array_creation.py | 54 +- tests/integration/test_array_dunders.py | 4 +- tests/integration/test_array_equal.py | 8 +- tests/integration/test_array_fallback.py | 8 +- tests/integration/test_array_split.py | 4 +- tests/integration/test_astype.py | 16 +- tests/integration/test_atleast_nd.py | 12 +- tests/integration/test_average.py | 103 + tests/integration/test_binary_op_broadcast.py | 4 +- tests/integration/test_binary_op_complex.py | 4 +- tests/integration/test_binary_op_typing.py | 38 +- tests/integration/test_binary_ufunc.py | 498 +- tests/integration/test_bincount.py | 4 +- tests/integration/test_bits.py | 12 +- tests/integration/test_block.py | 26 +- tests/integration/test_broadcast.py | 20 +- tests/integration/test_cholesky.py | 6 +- tests/integration/test_clip.py | 12 +- tests/integration/test_complex_ops.py | 8 +- tests/integration/test_compress.py | 14 +- tests/integration/test_concatenate_stack.py | 10 +- tests/integration/test_contains.py | 4 +- tests/integration/test_convolve.py | 28 +- tests/integration/test_copy.py | 4 +- tests/integration/test_corner_quantiles.py | 379 + tests/integration/test_data_interface.py | 6 +- tests/integration/test_diag_indices.py | 4 +- tests/integration/test_diff.py | 66 + tests/integration/test_digitize.py | 166 + tests/integration/test_dot.py | 8 +- tests/integration/test_einsum.py | 24 +- tests/integration/test_einsum_path.py | 8 +- tests/integration/test_exp.py | 10 +- tests/integration/test_expand_dims.py | 92 + tests/integration/test_extract.py | 12 +- tests/integration/test_eye.py | 14 +- tests/integration/test_fallback.py | 15 +- tests/integration/test_fft_c2c.py | 37 +- tests/integration/test_fft_c2r.py | 36 +- tests/integration/test_fft_hermitian.py | 4 +- tests/integration/test_fft_r2c.py | 4 +- tests/integration/test_fftshift.py | 70 + tests/integration/test_file.py | 48 + tests/integration/test_fill.py | 16 +- tests/integration/test_fill_diagonal.py | 7 +- tests/integration/test_flags.py | 4 +- tests/integration/test_flatten.py | 6 +- tests/integration/test_flip.py | 11 +- tests/integration/test_floating.py | 9 +- tests/integration/test_get_item.py | 4 +- tests/integration/test_gradient.py | 164 + tests/integration/test_histogram.py | 171 +- tests/integration/test_identity.py | 4 +- tests/integration/test_index_routines.py | 68 +- tests/integration/test_indices.py | 6 +- tests/integration/test_ingest.py | 101 - .../test_inlinemap-keeps-region-alive.py | 4 +- tests/integration/test_inner.py | 6 +- tests/integration/test_input_output.py | 4 +- tests/integration/test_intra_array_copy.py | 6 +- tests/integration/test_item.py | 10 +- tests/integration/test_itemset.py | 17 +- tests/integration/test_jacobi.py | 4 +- tests/integration/test_length.py | 4 +- tests/integration/test_linspace.py | 16 +- tests/integration/test_logic.py | 13 +- tests/integration/test_logical.py | 8 +- tests/integration/test_logical_reduction.py | 61 + tests/integration/test_lstm_backward_test.py | 4 +- tests/integration/test_lstm_simple_forward.py | 4 +- tests/integration/test_map_reduce.py | 4 +- tests/integration/test_mask.py | 4 +- tests/integration/test_mask_indices.py | 10 +- tests/integration/test_matmul.py | 95 +- tests/integration/test_matrix_power.py | 8 +- tests/integration/test_mean.py | 6 +- tests/integration/test_median.py | 223 + tests/integration/test_meshgrid.py | 71 + tests/integration/test_min_on_gpu.py | 4 +- tests/integration/test_moveaxis.py | 13 +- tests/integration/test_msort.py | 10 +- tests/integration/test_multi_dot.py | 8 +- tests/integration/test_nan_reduction.py | 65 +- tests/integration/test_nanarg_reduction.py | 36 +- tests/integration/test_nanmean.py | 6 +- tests/integration/test_nanpercentiles.py | 102 + tests/integration/test_nanquantiles.py | 102 + tests/integration/test_nd_convolve.py | 85 + tests/integration/test_ndim.py | 4 +- tests/integration/test_negaxes_quantiles.py | 112 + tests/integration/test_nonzero.py | 53 +- tests/integration/test_norm.py | 12 +- tests/integration/test_ones.py | 10 +- tests/integration/test_outer.py | 4 +- tests/integration/test_overlap.py | 14 +- tests/integration/test_overwrite_slice.py | 4 +- tests/integration/test_partition.py | 8 +- tests/integration/test_percentiles.py | 8 +- tests/integration/test_prod.py | 41 +- tests/integration/test_put.py | 6 +- tests/integration/test_put_along_axis.py | 6 +- tests/integration/test_putmask.py | 8 +- tests/integration/test_qr.py | 110 + tests/integration/test_quantiles.py | 306 +- tests/integration/test_randint.py | 4 +- tests/integration/test_random.py | 109 + tests/integration/test_random_advanced.py | 27 +- tests/integration/test_random_beta.py | 22 +- tests/integration/test_random_bitgenerator.py | 26 +- tests/integration/test_random_creation.py | 104 +- tests/integration/test_random_gamma.py | 22 +- .../test_random_straightforward.py | 33 +- tests/integration/test_reduction.py | 33 +- tests/integration/test_repeat.py | 11 +- tests/integration/test_reshape.py | 12 +- tests/integration/test_roll.py | 103 + tests/integration/test_rot90.py | 76 + tests/integration/test_round.py | 131 + tests/integration/test_scan.py | 12 +- tests/integration/test_searchsorted.py | 14 +- tests/integration/test_set_item.py | 4 +- tests/integration/test_setflags.py | 8 +- tests/integration/test_shape.py | 4 +- tests/integration/test_singleton_access.py | 30 +- tests/integration/test_slicing.py | 4 +- tests/integration/test_solve.py | 12 +- tests/integration/test_sort.py | 20 +- tests/integration/test_sort_complex.py | 8 +- tests/integration/test_split.py | 24 +- tests/integration/test_squeeze.py | 9 +- tests/integration/test_stack.py | 70 + tests/integration/test_stats.py | 255 +- tests/integration/test_svd.py | 132 + tests/integration/test_swapaxes.py | 4 +- tests/integration/test_take.py | 10 +- tests/integration/test_take_along_axis.py | 6 +- tests/integration/test_tensordot.py | 8 +- tests/integration/test_tile.py | 4 +- tests/integration/test_trace.py | 10 +- tests/integration/test_transpose.py | 32 +- tests/integration/test_tri.py | 20 +- tests/integration/test_trilu.py | 8 +- tests/integration/test_trilu_indices.py | 20 +- .../test_unary_functions_2d_complex.py | 4 +- tests/integration/test_unary_ufunc.py | 10 +- tests/integration/test_unique.py | 6 +- tests/integration/test_unravel_index.py | 166 + tests/integration/test_update.py | 4 +- tests/integration/test_vdot.py | 8 +- tests/integration/test_view.py | 4 +- tests/integration/test_where.py | 12 +- tests/integration/test_window.py | 4 +- tests/integration/utils/__init__.py | 2 +- tests/integration/utils/comparisons.py | 12 +- tests/integration/utils/contractions.py | 4 +- tests/integration/utils/generators.py | 2 +- tests/integration/utils/random.py | 8 +- tests/integration/utils/utils.py | 20 +- tests/todo/2d_reduction_complex.py | 4 +- tests/todo/assign_slice.py | 4 +- tests/todo/complex_test.py | 4 +- tests/todo/dot.py | 4 +- tests/todo/indirect.py | 4 +- tests/todo/kmeans_test.py | 4 +- tests/todo/lstm_batch.py | 4 +- tests/todo/lstm_simple_backward.py | 4 +- tests/unit/__init__.py | 16 + tests/unit/cupynumeric/__init__.py | 16 + tests/unit/cupynumeric/_array/__init__.py | 16 + tests/unit/cupynumeric/_array/test_util.py | 238 + tests/unit/cupynumeric/_sphinxext/__init__.py | 17 + .../_sphinxext/test__comparison_util.py | 6 +- tests/unit/cupynumeric/_utils/__init__.py | 16 + tests/unit/cupynumeric/_utils/test_array.py | 101 + .../_utils}/test_coverage.py | 76 +- .../_utils/test_linalg.py} | 129 +- tests/unit/cupynumeric/random/__init__.py | 16 + .../random/test_bitgenerator.py | 6 +- .../{cunumeric => cupynumeric}/test_config.py | 104 +- tests/unit/cupynumeric/test_nptest.py | 39 + .../{cunumeric => cupynumeric}/test_patch.py | 6 +- .../test_settings.py | 12 +- tests/unit/util.py | 34 + versioneer.py | 2 +- 917 files changed, 50029 insertions(+), 24643 deletions(-) create mode 100644 .github/CODEOWNERS delete mode 100644 .github/actions/download-artifacts/action.yml delete mode 100644 .github/workflows/ci-gh.yml delete mode 100644 .github/workflows/gh-build-and-test.yml delete mode 100644 .github/workflows/gh-build.yml delete mode 100644 .github/workflows/gh-test.yml delete mode 100644 .github/workflows/require-labels.yml delete mode 100644 LICENSES_bundled.txt create mode 100644 cmake/thirdparty/get_legate.cmake delete mode 100644 cmake/thirdparty/get_legate_core.cmake delete mode 100644 continuous_integration/dot-gitconfig delete mode 100755 continuous_integration/scripts/build-cunumeric-all delete mode 100755 continuous_integration/scripts/build-cunumeric-conda delete mode 100755 continuous_integration/scripts/build-cunumeric-cpp delete mode 100755 continuous_integration/scripts/build-cunumeric-wheel delete mode 100755 continuous_integration/scripts/entrypoint delete mode 100755 continuous_integration/scripts/test-cunumeric delete mode 100644 cunumeric/_ufunc/comparison.py delete mode 100644 cunumeric/config.py delete mode 100644 cunumeric/install_info.py.in delete mode 100644 cunumeric/linalg/cholesky.py delete mode 100644 cunumeric/linalg/solve.py delete mode 100644 cunumeric/module.py delete mode 100644 cunumeric/random/legacy.py delete mode 100644 cunumeric_cpp.cmake rename {cunumeric => cupynumeric}/__init__.py (69%) create mode 100644 cupynumeric/_array/__init__.py rename {cunumeric => cupynumeric/_array}/array.py (72%) create mode 100644 cupynumeric/_array/flags.py create mode 100644 cupynumeric/_array/thunk.py create mode 100644 cupynumeric/_array/util.py create mode 100644 cupynumeric/_module/__init__.py rename {cunumeric => cupynumeric/_module}/_unary_red_utils.py (95%) create mode 100644 cupynumeric/_module/array_basic.py create mode 100644 cupynumeric/_module/array_dimension.py create mode 100644 cupynumeric/_module/array_joining.py create mode 100644 cupynumeric/_module/array_rearrange.py create mode 100644 cupynumeric/_module/array_shape.py create mode 100644 cupynumeric/_module/array_splitting.py create mode 100644 cupynumeric/_module/array_tiling.py create mode 100644 cupynumeric/_module/array_transpose.py rename cunumeric/bits.py => cupynumeric/_module/binary_bit_packing.py (92%) create mode 100644 cupynumeric/_module/creation_data.py create mode 100644 cupynumeric/_module/creation_matrices.py create mode 100644 cupynumeric/_module/creation_ranges.py create mode 100644 cupynumeric/_module/creation_shape.py create mode 100644 cupynumeric/_module/indexing.py create mode 100644 cupynumeric/_module/io_numpy.py create mode 100644 cupynumeric/_module/linalg_mvp.py create mode 100644 cupynumeric/_module/logic_array_contents.py rename cunumeric/logic.py => cupynumeric/_module/logic_array_type.py (51%) create mode 100644 cupynumeric/_module/logic_comparison.py create mode 100644 cupynumeric/_module/logic_truth.py create mode 100644 cupynumeric/_module/math_complex.py create mode 100644 cupynumeric/_module/math_extrema.py create mode 100644 cupynumeric/_module/math_misc.py create mode 100644 cupynumeric/_module/math_rounding.py create mode 100644 cupynumeric/_module/math_sum_prod_diff.py create mode 100644 cupynumeric/_module/sets_making.py create mode 100644 cupynumeric/_module/ssc_counting.py create mode 100644 cupynumeric/_module/ssc_searching.py create mode 100644 cupynumeric/_module/ssc_sorting.py create mode 100644 cupynumeric/_module/stats_avgs_vars.py create mode 100644 cupynumeric/_module/stats_correlating.py create mode 100644 cupynumeric/_module/stats_histograms.py create mode 100644 cupynumeric/_module/stats_order.py rename {cunumeric => cupynumeric/_module}/window.py (96%) rename {cunumeric => cupynumeric}/_sphinxext/__init__.py (95%) rename {cunumeric => cupynumeric}/_sphinxext/_comparison_config.py (98%) rename {cunumeric => cupynumeric}/_sphinxext/_comparison_util.py (87%) rename cunumeric/_sphinxext/_cunumeric_directive.py => cupynumeric/_sphinxext/_cupynumeric_directive.py (86%) rename {cunumeric => cupynumeric}/_sphinxext/_templates.py (95%) rename {cunumeric => cupynumeric}/_sphinxext/_templates/comparison_table.rst (69%) rename {cunumeric => cupynumeric}/_sphinxext/comparison_table.py (92%) rename {cunumeric => cupynumeric}/_sphinxext/implemented_index.py (86%) rename {cunumeric => cupynumeric}/_sphinxext/missing_refs.py (69%) rename {cunumeric => cupynumeric}/_sphinxext/ufunc_formatter.py (94%) create mode 100644 cupynumeric/_thunk/__init__.py rename cunumeric/sort.py => cupynumeric/_thunk/_sort.py (72%) rename {cunumeric => cupynumeric/_thunk}/deferred.py (75%) rename {cunumeric => cupynumeric/_thunk}/eager.py (84%) rename {cunumeric => cupynumeric/_thunk}/thunk.py (81%) rename {cunumeric => cupynumeric}/_ufunc/__init__.py (95%) rename {cunumeric => cupynumeric}/_ufunc/bit_twiddling.py (94%) create mode 100644 cupynumeric/_ufunc/comparison.py rename {cunumeric => cupynumeric}/_ufunc/floating.py (97%) rename {cunumeric => cupynumeric}/_ufunc/math.py (98%) rename {cunumeric => cupynumeric}/_ufunc/trigonometric.py (96%) rename {cunumeric => cupynumeric}/_ufunc/ufunc.py (77%) create mode 100644 cupynumeric/_utils/__init__.py create mode 100644 cupynumeric/_utils/array.py rename {cunumeric => cupynumeric/_utils}/coverage.py (87%) rename cunumeric/utils.py => cupynumeric/_utils/linalg.py (51%) create mode 100644 cupynumeric/_utils/stack.py create mode 100644 cupynumeric/_utils/structure.py rename {cunumeric => cupynumeric}/_version.py (99%) create mode 100644 cupynumeric/config.py rename {cunumeric => cupynumeric}/fft/__init__.py (80%) rename {cunumeric => cupynumeric}/fft/fft.py (91%) create mode 100644 cupynumeric/install_info.py.in rename {cunumeric => cupynumeric}/linalg/__init__.py (77%) create mode 100644 cupynumeric/linalg/_cholesky.py rename cunumeric/linalg/exception.py => cupynumeric/linalg/_exception.py (94%) create mode 100644 cupynumeric/linalg/_qr.py create mode 100644 cupynumeric/linalg/_solve.py create mode 100644 cupynumeric/linalg/_svd.py rename {cunumeric => cupynumeric}/linalg/linalg.py (78%) rename {cunumeric => cupynumeric}/ma/__init__.py (80%) rename {cunumeric => cupynumeric}/ma/_masked_array.py (87%) rename {cunumeric => cupynumeric}/patch.py (80%) rename {cunumeric => cupynumeric}/py.typed (100%) rename {cunumeric => cupynumeric}/random/__init__.py (67%) rename cunumeric/random/bitgenerator.py => cupynumeric/random/_bitgenerator.py (89%) rename cunumeric/random/generator.py => cupynumeric/random/_generator.py (83%) rename cunumeric/random/random.py => cupynumeric/random/_random.py (90%) rename {cunumeric => cupynumeric}/runtime.py (60%) rename {cunumeric => cupynumeric}/settings.py (75%) rename {cunumeric => cupynumeric}/types.py (83%) create mode 100644 cupynumeric_cpp.cmake rename cunumeric_python.cmake => cupynumeric_python.cmake (68%) delete mode 100644 docs/cunumeric/source/api/settings.rst delete mode 120000 docs/cunumeric/source/developer/CONTRIBUTING.md delete mode 100644 docs/cunumeric/source/developer/building.rst delete mode 100644 docs/cunumeric/source/index.rst delete mode 100644 docs/cunumeric/source/user/configuration.rst delete mode 100644 docs/cunumeric/source/user/installation.rst delete mode 100644 docs/cunumeric/source/user/notebooks.rst delete mode 100644 docs/cunumeric/source/user/usage.rst delete mode 100644 docs/cunumeric/source/versions.rst rename docs/{cunumeric => cupynumeric}/Makefile (87%) rename docs/{cunumeric => cupynumeric}/make.bat (100%) rename docs/{cunumeric => cupynumeric}/source/_images/developer-build.png (100%) rename docs/{cunumeric => cupynumeric}/source/_implemented.rst (73%) rename docs/{cunumeric => cupynumeric}/source/_static/.keep (100%) create mode 100644 docs/cupynumeric/source/_templates/layout.html rename docs/{cunumeric => cupynumeric}/source/api/_bitgenerator.rst (65%) rename docs/{cunumeric => cupynumeric}/source/api/_generator.rst (65%) rename docs/{cunumeric/source/comparison => cupynumeric/source/api}/_grouped.rst (100%) rename docs/{cunumeric => cupynumeric}/source/api/_ndarray.rst (95%) rename docs/{cunumeric => cupynumeric}/source/api/binary.rst (90%) rename docs/{cunumeric => cupynumeric}/source/api/broadcast.rst (52%) rename docs/{cunumeric => cupynumeric}/source/api/classes.rst (100%) create mode 100644 docs/cupynumeric/source/api/comparison.rst rename docs/{cunumeric => cupynumeric}/source/api/creation.rst (93%) create mode 100644 docs/cupynumeric/source/api/datatype.rst rename docs/{cunumeric => cupynumeric}/source/api/fft.rst (67%) rename docs/{cunumeric => cupynumeric}/source/api/index.rst (67%) rename docs/{cunumeric => cupynumeric}/source/api/indexing.rst (92%) create mode 100644 docs/cupynumeric/source/api/io.rst rename docs/{cunumeric => cupynumeric}/source/api/linalg.rst (82%) rename docs/{cunumeric => cupynumeric}/source/api/logic.rst (95%) rename docs/{cunumeric => cupynumeric}/source/api/manipulation.rst (91%) rename docs/{cunumeric => cupynumeric}/source/api/math.rst (96%) rename docs/{cunumeric => cupynumeric}/source/api/ndarray.rst (97%) rename docs/{cunumeric => cupynumeric}/source/api/random.rst (93%) rename docs/{cunumeric => cupynumeric}/source/api/routines.rst (91%) rename docs/{cunumeric => cupynumeric}/source/api/set.rst (79%) create mode 100644 docs/cupynumeric/source/api/settings.rst rename docs/{cunumeric => cupynumeric}/source/api/sorting.rst (93%) rename docs/{cunumeric => cupynumeric}/source/api/statistics.rst (64%) rename docs/{cunumeric => cupynumeric}/source/api/window.rst (85%) rename docs/{cunumeric => cupynumeric}/source/conf.py (59%) create mode 100644 docs/cupynumeric/source/developer/CONTRIBUTING.md create mode 100644 docs/cupynumeric/source/developer/building.rst rename docs/{cunumeric => cupynumeric}/source/developer/index.rst (100%) rename docs/{cunumeric => cupynumeric}/source/developer/testing.rst (97%) rename docs/{cunumeric/source/user/notebooks => cupynumeric/source/examples}/black_scholes.ipynb (99%) rename docs/{cunumeric/source/user/notebooks => cupynumeric/source/examples}/cholesky.ipynb (86%) create mode 100644 docs/cupynumeric/source/examples/compact_finite_difference.ipynb create mode 100644 docs/cupynumeric/source/examples/edge_detection.ipynb create mode 100644 docs/cupynumeric/source/examples/image.png create mode 100644 docs/cupynumeric/source/examples/index.rst create mode 100644 docs/cupynumeric/source/examples/kmeans.ipynb create mode 100644 docs/cupynumeric/source/examples/newton_raphson_2d.ipynb rename docs/{cunumeric/source/user/notebooks => cupynumeric/source/examples}/stencil.ipynb (99%) create mode 100644 docs/cupynumeric/source/examples/torchswe.ipynb create mode 100644 docs/cupynumeric/source/faqs.rst create mode 100644 docs/cupynumeric/source/index.rst create mode 100644 docs/cupynumeric/source/installation.rst create mode 100644 docs/cupynumeric/source/oss-licenses.rst create mode 100644 docs/cupynumeric/source/user/advanced.rst create mode 100644 docs/cupynumeric/source/user/differences.rst create mode 100644 docs/cupynumeric/source/user/howtos/benchmarking.rst create mode 100644 docs/cupynumeric/source/user/howtos/index.rst rename docs/{cunumeric/source/comparison/index.rst => cupynumeric/source/user/howtos/measuring.rst} (50%) create mode 100644 docs/cupynumeric/source/user/howtos/patching.rst rename docs/{cunumeric => cupynumeric}/source/user/index.rst (62%) rename docs/{cunumeric => cupynumeric}/source/user/practices.rst (71%) create mode 100644 docs/cupynumeric/source/user/usage.rst create mode 100644 docs/cupynumeric/switcher.json create mode 100644 examples/cpp/stencil/CMakeLists.txt create mode 100755 examples/cpp/stencil/build.sh create mode 100644 examples/cpp/stencil/stencil.cc delete mode 100644 examples/ingest.py delete mode 100644 examples/kmeans_sort.py delete mode 100644 examples/lstm_full.py create mode 100644 examples/qr.py create mode 100644 examples/svd.py delete mode 100644 examples/wgrad.py delete mode 100755 scripts/build-install.sh delete mode 100755 scripts/build-no-install.sh delete mode 100644 scripts/build-separately-no-install.sh delete mode 100644 scripts/build-with-legate-no-install.sh delete mode 100755 scripts/build-with-legate-separately-no-install.sh create mode 100755 scripts/hooks/legate_defined.sh delete mode 100755 scripts/util/build-caching.sh delete mode 100755 scripts/util/compiler-flags.sh delete mode 100755 scripts/util/read-legate-core-root.sh delete mode 100755 scripts/util/uninstall-global-legion-legate-core-and-cunumeric.sh delete mode 100644 src/cunumeric/arg_redop_register.h delete mode 100644 src/cunumeric/binary/binary_op.h delete mode 100644 src/cunumeric/binary/binary_red.h delete mode 100644 src/cunumeric/cunumeric.cc delete mode 100644 src/cunumeric/cunumeric_c.h delete mode 100644 src/cunumeric/index/zip.h delete mode 100644 src/cunumeric/mapper.cc delete mode 100644 src/cunumeric/random/bitgenerator_util.h delete mode 100644 src/cunumeric/random/curand_help.h delete mode 100644 src/cunumeric/random/randutil/generator_create.inl delete mode 100644 src/cunumeric/random/randutil/generator_integers.inl delete mode 100644 src/cunumeric/random/randutil/randutil.h delete mode 100644 src/cunumeric/scan/scan_global_util.h delete mode 100644 src/cunumeric/stat/bincount.h create mode 100644 src/cupynumeric.h rename src/{cunumeric => cupynumeric}/arg.h (95%) rename src/{cunumeric => cupynumeric}/arg.inl (98%) rename src/{cunumeric => cupynumeric}/arg_redop_register.cc (71%) rename src/{cunumeric => cupynumeric}/arg_redop_register.cu (64%) create mode 100644 src/cupynumeric/arg_redop_register.h rename src/{cunumeric => cupynumeric}/binary/binary_op.cc (81%) rename src/{cunumeric => cupynumeric}/binary/binary_op.cu (85%) create mode 100644 src/cupynumeric/binary/binary_op.h rename src/{cunumeric => cupynumeric}/binary/binary_op_omp.cc (80%) rename src/{cunumeric => cupynumeric}/binary/binary_op_template.inl (78%) create mode 100644 src/cupynumeric/binary/binary_op_util.cc rename src/{cunumeric => cupynumeric}/binary/binary_op_util.h (77%) rename src/{cunumeric => cupynumeric}/binary/binary_red.cc (83%) rename src/{cunumeric => cupynumeric}/binary/binary_red.cu (80%) create mode 100644 src/cupynumeric/binary/binary_red.h rename src/{cunumeric => cupynumeric}/binary/binary_red_omp.cc (75%) rename src/{cunumeric => cupynumeric}/binary/binary_red_template.inl (80%) rename src/{cunumeric => cupynumeric}/bits/bits_util.h (74%) rename src/{cunumeric => cupynumeric}/bits/packbits.cc (87%) rename src/{cunumeric => cupynumeric}/bits/packbits.cu (88%) rename src/{cunumeric => cupynumeric}/bits/packbits.h (85%) rename src/{cunumeric => cupynumeric}/bits/packbits_omp.cc (87%) rename src/{cunumeric => cupynumeric}/bits/packbits_template.inl (79%) rename src/{cunumeric => cupynumeric}/bits/unpackbits.cc (84%) rename src/{cunumeric => cupynumeric}/bits/unpackbits.cu (83%) rename src/{cunumeric => cupynumeric}/bits/unpackbits.h (75%) rename src/{cunumeric => cupynumeric}/bits/unpackbits_omp.cc (83%) rename src/{cunumeric => cupynumeric}/bits/unpackbits_template.inl (71%) rename src/{cunumeric => cupynumeric}/cephes/chbevl.cc (100%) rename src/{cunumeric => cupynumeric}/cephes/i0.cc (99%) rename src/{cunumeric => cupynumeric}/convolution/convolve.cc (85%) rename src/{cunumeric => cupynumeric}/convolution/convolve.cu (84%) rename src/{cunumeric => cupynumeric}/convolution/convolve.h (59%) rename src/{cunumeric => cupynumeric}/convolution/convolve_omp.cc (82%) rename src/{cunumeric => cupynumeric}/convolution/convolve_template.inl (69%) rename src/{cunumeric => cupynumeric}/cuda_help.h (68%) rename src/{cunumeric => cupynumeric}/cudalibs.cu (69%) rename src/{cunumeric => cupynumeric}/cudalibs.h (67%) create mode 100644 src/cupynumeric/cupynumeric.cc create mode 100644 src/cupynumeric/cupynumeric.cu create mode 100644 src/cupynumeric/cupynumeric_c.h rename src/{cunumeric/cunumeric.h => cupynumeric/cupynumeric_task.h} (72%) rename src/{cunumeric => cupynumeric}/device_scalar_reduction_buffer.h (74%) rename src/{cunumeric => cupynumeric}/divmod.h (97%) rename src/{cunumeric => cupynumeric}/execution_policy/indexing/parallel_loop.cuh (78%) rename src/{cunumeric => cupynumeric}/execution_policy/indexing/parallel_loop.h (80%) rename src/{cunumeric => cupynumeric}/execution_policy/indexing/parallel_loop_omp.h (73%) rename src/{cunumeric => cupynumeric}/execution_policy/reduction/scalar_reduction.cuh (88%) rename src/{cunumeric => cupynumeric}/execution_policy/reduction/scalar_reduction.h (86%) rename src/{cunumeric => cupynumeric}/execution_policy/reduction/scalar_reduction_omp.h (68%) rename src/{cunumeric => cupynumeric}/fft/fft.cu (81%) rename src/{cunumeric => cupynumeric}/fft/fft.h (56%) rename src/{cunumeric => cupynumeric}/fft/fft_template.inl (68%) rename src/{cunumeric => cupynumeric}/fft/fft_util.h (52%) rename src/{cunumeric => cupynumeric}/index/advanced_indexing.cc (75%) rename src/{cunumeric => cupynumeric}/index/advanced_indexing.cu (86%) rename src/{cunumeric => cupynumeric}/index/advanced_indexing.h (59%) rename src/{cunumeric => cupynumeric}/index/advanced_indexing_omp.cc (78%) rename src/{cunumeric => cupynumeric}/index/advanced_indexing_template.inl (81%) rename src/{cunumeric => cupynumeric}/index/choose.cc (78%) rename src/{cunumeric => cupynumeric}/index/choose.cu (80%) create mode 100644 src/cupynumeric/index/choose.h rename src/{cunumeric => cupynumeric}/index/choose_omp.cc (81%) rename src/{cunumeric => cupynumeric}/index/choose_template.inl (78%) rename src/{cunumeric => cupynumeric}/index/putmask.cc (76%) rename src/{cunumeric => cupynumeric}/index/putmask.cu (66%) create mode 100644 src/cupynumeric/index/putmask.h rename src/{cunumeric => cupynumeric}/index/putmask_omp.cc (66%) rename src/{cunumeric => cupynumeric}/index/putmask_template.inl (76%) rename src/{cunumeric => cupynumeric}/index/repeat.cc (82%) rename src/{cunumeric => cupynumeric}/index/repeat.cu (82%) rename src/{cunumeric => cupynumeric}/index/repeat.h (53%) rename src/{cunumeric => cupynumeric}/index/repeat_omp.cc (78%) rename src/{cunumeric => cupynumeric}/index/repeat_template.inl (65%) rename src/{cunumeric => cupynumeric}/index/select.cc (78%) rename src/{cunumeric => cupynumeric}/index/select.cu (77%) rename src/{cunumeric => cupynumeric}/index/select.h (54%) rename src/{cunumeric => cupynumeric}/index/select_omp.cc (78%) rename src/{cunumeric => cupynumeric}/index/select_template.inl (74%) rename src/{cunumeric => cupynumeric}/index/wrap.cc (83%) rename src/{cunumeric => cupynumeric}/index/wrap.cu (89%) rename src/{cunumeric => cupynumeric}/index/wrap.h (57%) rename src/{cunumeric => cupynumeric}/index/wrap_omp.cc (76%) rename src/{cunumeric => cupynumeric}/index/wrap_template.inl (74%) rename src/{cunumeric => cupynumeric}/index/zip.cc (88%) rename src/{cunumeric => cupynumeric}/index/zip.cu (88%) create mode 100644 src/cupynumeric/index/zip.h rename src/{cunumeric => cupynumeric}/index/zip_omp.cc (80%) rename src/{cunumeric => cupynumeric}/index/zip_template.inl (74%) rename src/{cunumeric => cupynumeric}/item/read.cc (80%) rename src/{cunumeric => cupynumeric}/item/read.cu (76%) rename src/{cunumeric/index/putmask.h => cupynumeric/item/read.h} (53%) rename src/{cunumeric => cupynumeric}/item/read_template.inl (73%) rename src/{cunumeric => cupynumeric}/item/write.cc (63%) rename src/{cunumeric => cupynumeric}/item/write.cu (59%) rename src/{cunumeric/item/read.h => cupynumeric/item/write.h} (55%) rename src/{cunumeric => cupynumeric}/item/write_template.inl (58%) create mode 100644 src/cupynumeric/mapper.cc rename src/{cunumeric => cupynumeric}/mapper.h (50%) rename src/{cunumeric => cupynumeric}/matrix/batched_cholesky.cc (81%) rename src/{cunumeric => cupynumeric}/matrix/batched_cholesky.cu (85%) rename src/{cunumeric => cupynumeric}/matrix/batched_cholesky.h (52%) rename src/{cunumeric => cupynumeric}/matrix/batched_cholesky_omp.cc (84%) rename src/{cunumeric => cupynumeric}/matrix/batched_cholesky_template.inl (81%) rename src/{cunumeric => cupynumeric}/matrix/contract.cc (97%) rename src/{cunumeric => cupynumeric}/matrix/contract.cu (67%) rename src/{cunumeric => cupynumeric}/matrix/contract.h (56%) rename src/{cunumeric => cupynumeric}/matrix/contract_omp.cc (96%) rename src/{cunumeric => cupynumeric}/matrix/contract_template.inl (88%) rename src/{cunumeric => cupynumeric}/matrix/diag.cc (81%) rename src/{cunumeric => cupynumeric}/matrix/diag.cu (86%) rename src/{cunumeric => cupynumeric}/matrix/diag.h (54%) rename src/{cunumeric => cupynumeric}/matrix/diag_omp.cc (85%) rename src/{cunumeric => cupynumeric}/matrix/diag_template.inl (82%) rename src/{cunumeric => cupynumeric}/matrix/dot.cc (83%) rename src/{cunumeric => cupynumeric}/matrix/dot.cu (88%) rename src/{cunumeric => cupynumeric}/matrix/dot.h (52%) rename src/{cunumeric => cupynumeric}/matrix/dot_omp.cc (79%) rename src/{cunumeric => cupynumeric}/matrix/dot_template.inl (84%) rename src/{cunumeric => cupynumeric}/matrix/gemm.cc (92%) rename src/{cunumeric => cupynumeric}/matrix/gemm.cu (90%) rename src/{cunumeric => cupynumeric}/matrix/gemm.h (55%) rename src/{cunumeric => cupynumeric}/matrix/gemm_omp.cc (92%) rename src/{cunumeric => cupynumeric}/matrix/gemm_template.inl (78%) rename src/{cunumeric => cupynumeric}/matrix/matmul.cc (71%) rename src/{cunumeric => cupynumeric}/matrix/matmul.cu (94%) rename src/{cunumeric => cupynumeric}/matrix/matmul.h (52%) rename src/{cunumeric => cupynumeric}/matrix/matmul_cpu.inl (95%) rename src/{cunumeric => cupynumeric}/matrix/matmul_omp.cc (71%) rename src/{cunumeric => cupynumeric}/matrix/matmul_template.inl (50%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul.cc (71%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul.cu (93%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul.h (51%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul_cpu.inl (95%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul_omp.cc (71%) rename src/{cunumeric => cupynumeric}/matrix/matvecmul_template.inl (88%) create mode 100644 src/cupynumeric/matrix/mp_potrf.cu rename src/{cunumeric/set/unique_reduce.h => cupynumeric/matrix/mp_potrf.h} (61%) create mode 100644 src/cupynumeric/matrix/mp_potrf_template.inl create mode 100644 src/cupynumeric/matrix/mp_solve.cu create mode 100644 src/cupynumeric/matrix/mp_solve.h create mode 100644 src/cupynumeric/matrix/mp_solve_template.inl rename src/{cunumeric => cupynumeric}/matrix/potrf.cc (79%) rename src/{cunumeric => cupynumeric}/matrix/potrf.cu (86%) rename src/{cunumeric => cupynumeric}/matrix/potrf.h (55%) rename src/{cunumeric => cupynumeric}/matrix/potrf_omp.cc (79%) rename src/{cunumeric => cupynumeric}/matrix/potrf_template.inl (85%) create mode 100644 src/cupynumeric/matrix/qr.cc create mode 100644 src/cupynumeric/matrix/qr.cu rename src/{cunumeric/matrix/solve.h => cupynumeric/matrix/qr.h} (57%) create mode 100644 src/cupynumeric/matrix/qr_cpu.inl create mode 100644 src/cupynumeric/matrix/qr_omp.cc create mode 100644 src/cupynumeric/matrix/qr_template.inl rename src/{cunumeric => cupynumeric}/matrix/solve.cc (74%) rename src/{cunumeric => cupynumeric}/matrix/solve.cu (87%) create mode 100644 src/cupynumeric/matrix/solve.h rename src/{cunumeric => cupynumeric}/matrix/solve_cpu.inl (82%) rename src/{cunumeric => cupynumeric}/matrix/solve_omp.cc (71%) rename src/{cunumeric => cupynumeric}/matrix/solve_template.inl (74%) create mode 100644 src/cupynumeric/matrix/svd.cc create mode 100644 src/cupynumeric/matrix/svd.cu rename src/{cunumeric/index/choose.h => cupynumeric/matrix/svd.h} (53%) create mode 100644 src/cupynumeric/matrix/svd_cpu.inl create mode 100644 src/cupynumeric/matrix/svd_omp.cc create mode 100644 src/cupynumeric/matrix/svd_template.inl rename src/{cunumeric => cupynumeric}/matrix/syrk.cc (90%) rename src/{cunumeric => cupynumeric}/matrix/syrk.cu (88%) rename src/{cunumeric/item/write.h => cupynumeric/matrix/syrk.h} (55%) rename src/{cunumeric => cupynumeric}/matrix/syrk_omp.cc (91%) rename src/{cunumeric => cupynumeric}/matrix/syrk_template.inl (79%) rename src/{cunumeric => cupynumeric}/matrix/tile.cc (86%) rename src/{cunumeric => cupynumeric}/matrix/tile.cu (84%) rename src/{cunumeric => cupynumeric}/matrix/tile.h (53%) rename src/{cunumeric => cupynumeric}/matrix/tile_omp.cc (85%) rename src/{cunumeric => cupynumeric}/matrix/tile_template.inl (87%) rename src/{cunumeric => cupynumeric}/matrix/transpose.cc (74%) rename src/{cunumeric => cupynumeric}/matrix/transpose.cu (80%) rename src/{cunumeric => cupynumeric}/matrix/transpose.h (52%) rename src/{cunumeric => cupynumeric}/matrix/transpose_omp.cc (75%) rename src/{cunumeric => cupynumeric}/matrix/transpose_template.inl (77%) rename src/{cunumeric => cupynumeric}/matrix/trilu.cc (77%) rename src/{cunumeric => cupynumeric}/matrix/trilu.cu (79%) rename src/{cunumeric => cupynumeric}/matrix/trilu.h (54%) rename src/{cunumeric => cupynumeric}/matrix/trilu_omp.cc (78%) rename src/{cunumeric => cupynumeric}/matrix/trilu_template.inl (73%) rename src/{cunumeric => cupynumeric}/matrix/trsm.cc (90%) rename src/{cunumeric => cupynumeric}/matrix/trsm.cu (88%) rename src/{cunumeric => cupynumeric}/matrix/trsm.h (55%) rename src/{cunumeric => cupynumeric}/matrix/trsm_omp.cc (91%) rename src/{cunumeric => cupynumeric}/matrix/trsm_template.inl (79%) rename src/{cunumeric => cupynumeric}/matrix/util.cc (80%) rename src/{cunumeric => cupynumeric}/matrix/util.h (91%) create mode 100644 src/cupynumeric/ndarray.cc create mode 100644 src/cupynumeric/ndarray.h create mode 100644 src/cupynumeric/ndarray.inl rename src/{cunumeric => cupynumeric}/nullary/arange.cc (77%) rename src/{cunumeric => cupynumeric}/nullary/arange.cu (81%) rename src/{cunumeric => cupynumeric}/nullary/arange.h (52%) rename src/{cunumeric => cupynumeric}/nullary/arange_omp.cc (75%) rename src/{cunumeric => cupynumeric}/nullary/arange_template.inl (69%) rename src/{cunumeric => cupynumeric}/nullary/eye.cc (74%) rename src/{cunumeric => cupynumeric}/nullary/eye.cu (79%) rename src/{cunumeric => cupynumeric}/nullary/eye.h (55%) rename src/{cunumeric => cupynumeric}/nullary/eye_omp.cc (72%) rename src/{cunumeric => cupynumeric}/nullary/eye_template.inl (83%) rename src/{cunumeric => cupynumeric}/nullary/fill.cc (80%) rename src/{cunumeric => cupynumeric}/nullary/fill.cu (83%) rename src/{cunumeric => cupynumeric}/nullary/fill.h (53%) rename src/{cunumeric => cupynumeric}/nullary/fill_omp.cc (81%) rename src/{cunumeric => cupynumeric}/nullary/fill_template.inl (63%) rename src/{cunumeric => cupynumeric}/nullary/window.cc (71%) rename src/{cunumeric => cupynumeric}/nullary/window.cu (83%) rename src/{cunumeric => cupynumeric}/nullary/window.h (55%) rename src/{cunumeric => cupynumeric}/nullary/window_omp.cc (70%) rename src/{cunumeric => cupynumeric}/nullary/window_template.inl (69%) rename src/{cunumeric => cupynumeric}/nullary/window_util.h (90%) rename src/{cunumeric => cupynumeric}/omp_help.h (93%) create mode 100644 src/cupynumeric/operators.cc create mode 100644 src/cupynumeric/operators.h create mode 100644 src/cupynumeric/operators.inl rename src/{cunumeric => cupynumeric}/pitches.h (90%) rename src/{cunumeric => cupynumeric}/random/bitgenerator.cc (55%) rename src/{cunumeric => cupynumeric}/random/bitgenerator.cu (63%) rename src/{cunumeric => cupynumeric}/random/bitgenerator.h (70%) rename src/{cunumeric => cupynumeric}/random/bitgenerator_curand.inl (87%) rename src/{cunumeric => cupynumeric}/random/bitgenerator_template.inl (81%) create mode 100644 src/cupynumeric/random/bitgenerator_util.h create mode 100644 src/cupynumeric/random/curand_help.h rename src/{cunumeric => cupynumeric}/random/philox.h (97%) rename src/{cunumeric => cupynumeric}/random/rand.cc (80%) rename src/{cunumeric => cupynumeric}/random/rand.cu (78%) rename src/{cunumeric => cupynumeric}/random/rand.h (54%) rename src/{cunumeric => cupynumeric}/random/rand_omp.cc (79%) rename src/{cunumeric => cupynumeric}/random/rand_template.inl (72%) rename src/{cunumeric => cupynumeric}/random/rand_util.h (89%) rename src/{cunumeric => cupynumeric}/random/randutil/generator.cuh (80%) rename src/{cunumeric => cupynumeric}/random/randutil/generator.h (57%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_beta.inl (96%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_binomial.inl (96%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_cauchy.inl (66%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_chisquare.inl (97%) create mode 100644 src/cupynumeric/random/randutil/generator_create.inl rename src/{cunumeric => cupynumeric}/random/randutil/generator_device.cu (98%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_device_advanced.cu (98%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_device_straightforward.cu (98%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_exponential.inl (85%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_f.inl (97%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_gamma.inl (96%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_geometric.inl (95%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_gumbel.inl (78%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_host.cc (71%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_host_advanced.cc (64%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_host_straightforward.cc (68%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_hypergeometric.inl (95%) create mode 100644 src/cupynumeric/random/randutil/generator_integers.inl rename src/{cunumeric => cupynumeric}/random/randutil/generator_laplace.inl (75%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_logistic.inl (76%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_lognormal.inl (84%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_logseries.inl (95%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_negative_binomial.inl (86%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_normal.inl (83%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_pareto.inl (70%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_poisson.inl (85%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_power.inl (81%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_raw.inl (86%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_rayleigh.inl (80%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_standard_t.inl (96%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_triangular.inl (67%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_uniform.inl (77%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_vonmises.inl (96%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_wald.inl (77%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_weibull.inl (75%) rename src/{cunumeric => cupynumeric}/random/randutil/generator_zipf.inl (95%) rename src/{cunumeric => cupynumeric}/random/randutil/random_distributions.h (89%) create mode 100644 src/cupynumeric/random/randutil/randomizer.h create mode 100644 src/cupynumeric/random/randutil/randutil.h rename src/{cunumeric => cupynumeric}/random/randutil/randutil_curand.h (90%) rename src/{cunumeric => cupynumeric}/random/randutil/randutil_impl.h (94%) create mode 100644 src/cupynumeric/random/rnd_aliases.h create mode 100644 src/cupynumeric/random/rnd_types.h create mode 100644 src/cupynumeric/runtime.cc create mode 100644 src/cupynumeric/runtime.h rename src/{cunumeric => cupynumeric}/scan/scan_global.cc (90%) rename src/{cunumeric => cupynumeric}/scan/scan_global.cu (86%) rename src/{cunumeric => cupynumeric}/scan/scan_global.h (53%) rename src/{cunumeric => cupynumeric}/scan/scan_global_omp.cc (90%) rename src/{cunumeric => cupynumeric}/scan/scan_global_template.inl (85%) rename src/{cunumeric => cupynumeric}/scan/scan_local.cc (87%) rename src/{cunumeric => cupynumeric}/scan/scan_local.cu (84%) rename src/{cunumeric => cupynumeric}/scan/scan_local.h (51%) rename src/{cunumeric => cupynumeric}/scan/scan_local_omp.cc (87%) rename src/{cunumeric => cupynumeric}/scan/scan_local_template.inl (85%) rename src/{cunumeric => cupynumeric}/scan/scan_local_util.h (86%) create mode 100644 src/cupynumeric/scan/scan_util.h rename src/{cunumeric => cupynumeric}/search/argwhere.cc (76%) rename src/{cunumeric => cupynumeric}/search/argwhere.cu (75%) rename src/{cunumeric => cupynumeric}/search/argwhere.h (53%) rename src/{cunumeric => cupynumeric}/search/argwhere_omp.cc (71%) rename src/{cunumeric => cupynumeric}/search/argwhere_template.inl (83%) rename src/{cunumeric => cupynumeric}/search/nonzero.cc (77%) rename src/{cunumeric => cupynumeric}/search/nonzero.cu (77%) rename src/{cunumeric => cupynumeric}/search/nonzero.cuh (92%) rename src/{cunumeric => cupynumeric}/search/nonzero.h (52%) rename src/{cunumeric => cupynumeric}/search/nonzero_omp.cc (70%) rename src/{cunumeric => cupynumeric}/search/nonzero_template.inl (74%) rename src/{cunumeric => cupynumeric}/set/unique.cc (80%) rename src/{cunumeric => cupynumeric}/set/unique.cu (85%) rename src/{cunumeric => cupynumeric}/set/unique.h (55%) rename src/{cunumeric => cupynumeric}/set/unique_omp.cc (85%) rename src/{cunumeric => cupynumeric}/set/unique_reduce.cc (75%) rename src/{cunumeric/matrix/syrk.h => cupynumeric/set/unique_reduce.h} (57%) rename src/{cunumeric => cupynumeric}/set/unique_reduce_omp.cc (72%) rename src/{cunumeric => cupynumeric}/set/unique_reduce_template.inl (74%) rename src/{cunumeric => cupynumeric}/set/unique_template.inl (78%) create mode 100644 src/cupynumeric/slice.h rename src/{cunumeric => cupynumeric}/sort/cub_sort.cuh (94%) rename src/{cunumeric => cupynumeric}/sort/cub_sort.h (97%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_bool.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_double.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_float.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_half.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_int16.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_int32.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_int64.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_int8.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_uint16.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_uint32.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_uint64.cu (88%) rename src/{cunumeric => cupynumeric}/sort/cub_sort_uint8.cu (88%) rename src/{cunumeric => cupynumeric}/sort/searchsorted.cc (77%) rename src/{cunumeric => cupynumeric}/sort/searchsorted.cu (82%) rename src/{cunumeric => cupynumeric}/sort/searchsorted.h (52%) rename src/{cunumeric => cupynumeric}/sort/searchsorted_omp.cc (77%) rename src/{cunumeric => cupynumeric}/sort/searchsorted_template.inl (82%) rename src/{cunumeric => cupynumeric}/sort/sort.cc (83%) rename src/{cunumeric => cupynumeric}/sort/sort.cu (90%) rename src/{cunumeric => cupynumeric}/sort/sort.h (76%) rename src/{cunumeric => cupynumeric}/sort/sort_cpu.inl (83%) rename src/{cunumeric => cupynumeric}/sort/sort_omp.cc (82%) rename src/{cunumeric => cupynumeric}/sort/sort_template.inl (79%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort.cuh (92%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort.h (98%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_bool.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_complex128.cu (89%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_complex64.cu (89%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_double.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_float.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_half.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_int16.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_int32.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_int64.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_int8.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_uint16.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_uint32.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_uint64.cu (88%) rename src/{cunumeric => cupynumeric}/sort/thrust_sort_uint8.cu (88%) rename src/{cunumeric => cupynumeric}/stat/bincount.cc (79%) rename src/{cunumeric => cupynumeric}/stat/bincount.cu (90%) create mode 100644 src/cupynumeric/stat/bincount.h rename src/{cunumeric => cupynumeric}/stat/bincount_omp.cc (85%) rename src/{cunumeric => cupynumeric}/stat/bincount_template.inl (76%) rename src/{cunumeric => cupynumeric}/stat/histogram.cc (83%) rename src/{cunumeric => cupynumeric}/stat/histogram.cu (81%) rename src/{cunumeric => cupynumeric}/stat/histogram.cuh (92%) rename src/{cunumeric => cupynumeric}/stat/histogram.h (50%) rename src/{cunumeric => cupynumeric}/stat/histogram_cpu.h (87%) rename src/{cunumeric => cupynumeric}/stat/histogram_gen.h (97%) rename src/{cunumeric => cupynumeric}/stat/histogram_impl.h (89%) rename src/{cunumeric => cupynumeric}/stat/histogram_omp.cc (90%) rename src/{cunumeric => cupynumeric}/stat/histogram_template.inl (88%) rename src/{cunumeric => cupynumeric}/ternary/where.cc (83%) rename src/{cunumeric => cupynumeric}/ternary/where.cu (84%) rename src/{cunumeric => cupynumeric}/ternary/where.h (50%) rename src/{cunumeric => cupynumeric}/ternary/where_omp.cc (83%) rename src/{cunumeric => cupynumeric}/ternary/where_template.inl (82%) rename src/{cunumeric => cupynumeric}/transform/flip.cc (79%) rename src/{cunumeric => cupynumeric}/transform/flip.cu (76%) rename src/{cunumeric => cupynumeric}/transform/flip.h (54%) rename src/{cunumeric => cupynumeric}/transform/flip_omp.cc (79%) rename src/{cunumeric => cupynumeric}/transform/flip_template.inl (76%) create mode 100644 src/cupynumeric/typedefs.h rename src/{cunumeric => cupynumeric}/unary/convert.cc (79%) rename src/{cunumeric => cupynumeric}/unary/convert.cu (82%) rename src/{cunumeric => cupynumeric}/unary/convert.h (52%) rename src/{cunumeric => cupynumeric}/unary/convert_omp.cc (78%) rename src/{cunumeric => cupynumeric}/unary/convert_template.inl (86%) rename src/{cunumeric => cupynumeric}/unary/convert_util.h (73%) rename src/{cunumeric => cupynumeric}/unary/isnan.h (89%) rename src/{cunumeric => cupynumeric}/unary/scalar_unary_red.cc (74%) rename src/{cunumeric => cupynumeric}/unary/scalar_unary_red.cu (63%) rename src/{cunumeric => cupynumeric}/unary/scalar_unary_red.h (51%) rename src/{cunumeric => cupynumeric}/unary/scalar_unary_red_omp.cc (65%) rename src/{cunumeric => cupynumeric}/unary/scalar_unary_red_template.inl (66%) rename src/{cunumeric => cupynumeric}/unary/unary_op.cc (84%) rename src/{cunumeric => cupynumeric}/unary/unary_op.cu (90%) rename src/{cunumeric => cupynumeric}/unary/unary_op.h (62%) rename src/{cunumeric => cupynumeric}/unary/unary_op_omp.cc (85%) rename src/{cunumeric => cupynumeric}/unary/unary_op_template.inl (82%) rename src/{cunumeric => cupynumeric}/unary/unary_op_util.h (72%) rename src/{cunumeric => cupynumeric}/unary/unary_red.cc (82%) rename src/{cunumeric => cupynumeric}/unary/unary_red.cu (89%) rename src/{cunumeric => cupynumeric}/unary/unary_red.h (51%) rename src/{cunumeric => cupynumeric}/unary/unary_red_omp.cc (84%) rename src/{cunumeric => cupynumeric}/unary/unary_red_template.inl (73%) rename src/{cunumeric => cupynumeric}/unary/unary_red_util.h (88%) create mode 100644 src/cupynumeric/utilities/repartition.cc create mode 100644 src/cupynumeric/utilities/repartition.cu create mode 100644 src/cupynumeric/utilities/repartition.h rename src/{cunumeric => cupynumeric}/utilities/thrust_allocator.h (91%) rename src/{cunumeric => cupynumeric}/utilities/thrust_util.h (95%) create mode 100644 tests/cpp/.gitignore create mode 100644 tests/cpp/CMakeLists.txt create mode 100644 tests/cpp/cmake/thirdparty/get_nccl.cmake create mode 100644 tests/cpp/integration/common_utils.cc create mode 100644 tests/cpp/integration/common_utils.h create mode 100644 tests/cpp/integration/test_amax.cc create mode 100644 tests/cpp/integration/test_amin.cc create mode 100644 tests/cpp/integration/test_arange.cc create mode 100644 tests/cpp/integration/test_argsort.cc create mode 100644 tests/cpp/integration/test_argwhere.cc create mode 100644 tests/cpp/integration/test_bincount.cc create mode 100644 tests/cpp/integration/test_convolve.cc create mode 100644 tests/cpp/integration/test_diagonal.cc create mode 100644 tests/cpp/integration/test_dot.cc create mode 100644 tests/cpp/integration/test_eye.cc create mode 100644 tests/cpp/integration/test_fill.cc create mode 100644 tests/cpp/integration/test_flip.cc create mode 100644 tests/cpp/integration/test_logical.cc create mode 100644 tests/cpp/integration/test_moveaxis.cc create mode 100644 tests/cpp/integration/test_msort.cc create mode 100644 tests/cpp/integration/test_nonzero.cc create mode 100644 tests/cpp/integration/test_put.cc create mode 100644 tests/cpp/integration/test_repartition.cc create mode 100644 tests/cpp/integration/test_repeat.cc create mode 100644 tests/cpp/integration/test_reshape.cc create mode 100644 tests/cpp/integration/test_sort.cc create mode 100644 tests/cpp/integration/test_sort_complex.cc create mode 100644 tests/cpp/integration/test_squeeze.cc create mode 100644 tests/cpp/integration/test_swapaxes.cc create mode 100644 tests/cpp/integration/test_transpose.cc create mode 100644 tests/cpp/integration/test_trilu.cc create mode 100644 tests/cpp/integration/test_unique.cc create mode 100644 tests/cpp/integration/test_where.cc create mode 100644 tests/cpp/integration/test_window.cc create mode 100644 tests/cpp/integration/test_zeros.cc create mode 100644 tests/cpp/integration/util.inl create mode 100644 tests/cpp/main.cc create mode 100755 tests/cpp/run.py create mode 100755 tests/cpp/run.sh create mode 100644 tests/integration/test_angle.py create mode 100644 tests/integration/test_average.py create mode 100644 tests/integration/test_corner_quantiles.py create mode 100644 tests/integration/test_diff.py create mode 100644 tests/integration/test_digitize.py create mode 100644 tests/integration/test_expand_dims.py create mode 100644 tests/integration/test_fftshift.py create mode 100644 tests/integration/test_file.py create mode 100644 tests/integration/test_gradient.py delete mode 100644 tests/integration/test_ingest.py create mode 100644 tests/integration/test_logical_reduction.py create mode 100644 tests/integration/test_median.py create mode 100644 tests/integration/test_meshgrid.py create mode 100644 tests/integration/test_nanpercentiles.py create mode 100644 tests/integration/test_nanquantiles.py create mode 100644 tests/integration/test_nd_convolve.py create mode 100644 tests/integration/test_negaxes_quantiles.py create mode 100644 tests/integration/test_qr.py create mode 100644 tests/integration/test_random.py create mode 100644 tests/integration/test_roll.py create mode 100644 tests/integration/test_rot90.py create mode 100644 tests/integration/test_round.py create mode 100644 tests/integration/test_stack.py create mode 100644 tests/integration/test_svd.py create mode 100644 tests/integration/test_unravel_index.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/cupynumeric/__init__.py create mode 100644 tests/unit/cupynumeric/_array/__init__.py create mode 100644 tests/unit/cupynumeric/_array/test_util.py create mode 100644 tests/unit/cupynumeric/_sphinxext/__init__.py rename tests/unit/{cunumeric => cupynumeric}/_sphinxext/test__comparison_util.py (92%) create mode 100644 tests/unit/cupynumeric/_utils/__init__.py create mode 100644 tests/unit/cupynumeric/_utils/test_array.py rename tests/unit/{cunumeric => cupynumeric/_utils}/test_coverage.py (86%) rename tests/unit/{cunumeric/test_utils.py => cupynumeric/_utils/test_linalg.py} (60%) create mode 100644 tests/unit/cupynumeric/random/__init__.py rename tests/unit/{cunumeric => cupynumeric}/random/test_bitgenerator.py (95%) rename tests/unit/{cunumeric => cupynumeric}/test_config.py (55%) create mode 100644 tests/unit/cupynumeric/test_nptest.py rename tests/unit/{cunumeric => cupynumeric}/test_patch.py (81%) rename tests/unit/{cunumeric => cupynumeric}/test_settings.py (91%) create mode 100644 tests/unit/util.py diff --git a/.clang-format b/.clang-format index 262238254..6d5353f99 100644 --- a/.clang-format +++ b/.clang-format @@ -8,7 +8,7 @@ AlignEscapedNewlines: Left AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true -AllowShortBlocksOnASingleLine: true +AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: true @@ -72,6 +72,7 @@ IndentCaseLabels: true IndentPPDirectives: None IndentWidth: 2 IndentWrappedFunctionNames: false +InsertBraces: true JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: false diff --git a/.gitattributes b/.gitattributes index 8ae3c8012..1215d42fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -cunumeric/_version.py export-subst +cunpyumeric/_version.py export-subst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..5ac9b710d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Code Ownership +.github @marcinz @m3vaz @sandeepd-nv @mag1cp1n +continuous_integration @marcinz @m3vaz @sandeepd-nv @mag1cp1n +conda @marcinz @m3vaz @sandeepd-nv @mag1cp1n diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8bf716ed1..74fb1d45b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: value: "# Bug report" - type: markdown attributes: - value: Thank you for reporting a bug and helping us improve Cunumeric! + value: Thank you for reporting a bug and helping us improve cuPyNumeric! - type: markdown attributes: value: > @@ -25,19 +25,19 @@ body: description: >- Run `legate-issue` and paste the output here. placeholder: | - Python : 3.10.11 | packaged by conda-forge | (main, May 10 2023, 18:58:44) [GCC 11.3.0] - Platform : Linux-5.14.0-1042-oem-x86_64-with-glibc2.31 - Legion : v23.11.00.dev-16-g2499f878 - Legate : 23.11.00.dev+17.gb7b50313 - Cunumeric : (ImportError: cannot import name 'LogicalArray' from 'legate.core') - Numpy : 1.24.4 - Scipy : 1.10.1 - Numba : (not installed) - CTK package : cuda-version-11.8-h70ddcb2_2 (conda-forge) - GPU Driver : 515.65.01 - GPU Devices : - GPU 0: Quadro RTX 8000 - GPU 1: Quadro RTX 8000 + Python : 3.12.4 | packaged by conda-forge | (main, Jun 17 2024, 10:23:07) [GCC 12.3.0] + Platform : Linux-6.8.0-40-generic-x86_64-with-glibc2.35 + Legion : (failed to detect) + Legate : 24.05.00+255.g2656afbd + cuPynumeric : 24.05.00+132.gc4741d57 + Numpy : 1.26.4 + Scipy : 1.13.1 + Numba : (failed to detect) + CTK package : cuda-version-12.0-hffde075_3 (conda-forge) + GPU driver : 535.183.06 + GPU devices : + GPU 0: NVIDIA RTX A5000 + GPU 1: NVIDIA RTX A5000 validations: required: true - type: input @@ -76,10 +76,9 @@ body: attributes: label: Example code or instructions description: > - Please provide detailed instructions to reproduce the issue. Ideally this includes a + Please provide detailed instructions to reproduce the issue. Ideally this includes a [Complete, minimal, self-contained example code](https://stackoverflow.com/help/minimal-reproducible-example) - given here or as a link to code in another repository. - render: Python + given here or as a link to code in another repository. validations: required: true - type: markdown diff --git a/.github/actions/download-artifacts/action.yml b/.github/actions/download-artifacts/action.yml deleted file mode 100644 index 640dc143a..000000000 --- a/.github/actions/download-artifacts/action.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: setup-legate-conda - -description: Download dependencies (artifacts) - -inputs: - device: {type: string, required: true} - git_sha: {type: string, required: true} - -runs: - using: composite - steps: - - - id: cache - name: Cache conda artifacts - uses: actions/cache@v3 - with: - key: "nv-legate/legate.core@${{ inputs.git_sha }}-${{ inputs.device }}" - path: .artifacts - - - if: steps.cache.outputs.cache-hit != 'true' - name: Download conda artifacts - uses: dawidd6/action-download-artifact@v2 - with: - path: .artifacts-dl - repo: nv-legate/legate.core - commit: ${{ inputs.git_sha }} - workflow_conclusion: success - workflow: "ci-gh.yml" - name: "legate.core-${{ inputs.device }}-[0-9a-z]{40}" - name_is_regexp: true - - - if: steps.cache.outputs.cache-hit != 'true' - name: Move conda artifacts into cached dir - shell: bash --noprofile --norc -xeo pipefail {0} - run: | - mkdir -p .artifacts; - find .artifacts-dl/legate.core-${{ inputs.device }}-*/ \ - -maxdepth 2 -type d -name legate_core -exec mv {} .artifacts/ \; - find .artifacts-dl/legate.core-${{ inputs.device }}-*/ \ - -maxdepth 2 -type f -name "environment*.yaml" -exec mv {} .artifacts/ \; - - - name: Copy and change cache dir ownership - shell: bash --noprofile --norc -xeo pipefail {0} - run: | - # Copy and change directory ownership - cp -ar .artifacts /home/coder/.artifacts; - chown -R coder:coder /home/coder/.artifacts; - ls -R /home/coder/.artifacts diff --git a/.github/workflows/ci-gh.yml b/.github/workflows/ci-gh.yml deleted file mode 100644 index ffb77c10e..000000000 --- a/.github/workflows/ci-gh.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build and test cunumeric on GH - -concurrency: - group: ci-build-and-test-on-${{ github.event_name }}-from-${{ github.ref_name }} - cancel-in-progress: true - -on: - push: - branches: - - "pull-request/[0-9]+" - - "branch-*" - -jobs: - build-and-test: - strategy: - fail-fast: false - matrix: - include: - - device: "gpu" - image: "rapidsai/devcontainers:23.06-cpp-mambaforge-ubuntu22.04" - - - device: "cpu" - image: "rapidsai/devcontainers:23.06-cpp-mambaforge-ubuntu22.04" - uses: - ./.github/workflows/gh-build-and-test.yml - with: - device: ${{ matrix.device }} - image: ${{ matrix.image }} diff --git a/.github/workflows/gh-build-and-test.yml b/.github/workflows/gh-build-and-test.yml deleted file mode 100644 index 3766a07ee..000000000 --- a/.github/workflows/gh-build-and-test.yml +++ /dev/null @@ -1,92 +0,0 @@ -on: - workflow_call: - inputs: - image: - type: string - required: true - device: - type: string - required: true - - -jobs: - build: - name: "Build cunumeric (with ${{ inputs.device }} legate) on GH" - uses: - ./.github/workflows/gh-build.yml - with: - device: ${{ inputs.device }} - image: ${{ inputs.image }} - runs-on: ${{ github.repository_owner == 'nv-legate' && 'linux-amd64-32cpu' || 'ubuntu-latest' }} - - test: - needs: - - build - strategy: - fail-fast: false - matrix: - include: - - name: 1 CPU test - options: test --cpus 1 --unit --debug - runner: ${{ inputs.device == 'gpu' && 'linux-amd64-gpu-v100-latest-1' || 'linux-amd64-cpu4' }} - has-gpu: false - enabled: true - - - name: 2 CPUs test - options: test --cpus 2 --debug - runner: ${{ inputs.device == 'gpu' && 'linux-amd64-gpu-v100-latest-1' || 'linux-amd64-cpu8' }} - has-gpu: false - enabled: true - - - name: GPU test - options: test --use cuda --gpus 1 --debug - runner: linux-amd64-gpu-v100-latest-1 - has-gpu: true - enabled: ${{ inputs.device == 'gpu' }} - - - name: 2 GPUs test - options: test --use cuda --gpus 2 --debug - runner: linux-amd64-2gpu - has-gpu: true - enabled: ${{ inputs.device == 'gpu' }} - - - name: OpenMP test - options: test --use openmp --omps 1 --ompthreads 2 --debug - runner: ${{ inputs.device == 'gpu' && 'linux-amd64-gpu-v100-latest-1' || 'linux-amd64-32cpu' }} - has-gpu: ${{ inputs.device == 'gpu' }} - enabled: true - - - name: 2 NUMA OpenMPs test - options: test --use openmp --omps 2 --ompthreads 2 --numamem 2048 --debug - runner: ${{ inputs.device == 'gpu' && 'linux-amd64-gpu-v100-latest-1' || 'linux-amd64-32cpu' }} - has-gpu: ${{ inputs.device == 'gpu' }} - enabled: true - - - name: Eager execution test - options: test --use eager --debug - runner: ${{ inputs.device == 'gpu' && 'linux-amd64-gpu-v100-latest-1' || 'linux-amd64-cpu4' }} - has-gpu: ${{ inputs.device == 'gpu' }} - enabled: true - - - name: mypy - options: mypy - runner: linux-amd64-cpu4 - has-gpu: false - enabled: true - - - name: documentation - options: docs - runner: linux-amd64-32cpu - has-gpu: false - enabled: ${{ inputs.device == 'gpu' }} - - uses: - ./.github/workflows/gh-test.yml - with: - name: ${{ matrix.name }} - device: ${{ inputs.device }} - image: ${{ inputs.image }} - runs-on: ${{ matrix.runner }} - has-gpu: ${{ matrix.has-gpu }} - test-options: ${{ matrix.options }} - enabled: ${{ matrix.enabled }} diff --git a/.github/workflows/gh-build.yml b/.github/workflows/gh-build.yml deleted file mode 100644 index 030dad1ad..000000000 --- a/.github/workflows/gh-build.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: Build - -on: - workflow_call: - inputs: - image: - type: string - required: true - device: - required: true - type: string - runs-on: - required: true - type: string - -jobs: - build: - name: build-${{ inputs.device }}-sub-workflow - - permissions: - id-token: write # This is required for configure-aws-credentials - contents: read # This is required for actions/checkout - - runs-on: ${{ inputs.runs-on }} - - container: - options: -u root - image: "${{ inputs.image }}" - env: - CUDA_VERSION: "12.2" - CUDA_VERSION_MAJOR: "12" - CUDA_VERSION_MINOR: "2" - SCCACHE_REGION: "us-east-2" - SCCACHE_BUCKET: "rapids-sccache-devs" - SCCACHE_S3_KEY_PREFIX: "legate-cunumeric-dev" - USE_CUDA: "${{ inputs.device == 'gpu' && 'ON' || 'OFF' }}" - GH_TOKEN: "${{ env.GH_TOKEN }}" - GITHUB_TOKEN: "${{ env.GITHUB_TOKEN }}" - VAULT_HOST: "${{ github.repository_owner != 'nv-legate' && 'https://vault.ops.k8s.rapids.ai' || '' }}" - defaults: - run: - shell: su coder {0} - working-directory: /home/coder - - steps: - - name: Checkout cunumeric (= this repo) - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: cunumeric - persist-credentials: false - - - name: Dump environment - run: | - env - - - name: Copy source folder - run: | - set -x - pwd - cp -r $GITHUB_WORKSPACE/cunumeric . - chown -R coder:coder cunumeric; - ls -R - - - name: Copy .gitconfig - run: cp ~/cunumeric/continuous_integration/dot-gitconfig ~/.gitconfig - - - id: legate_core_info - name: Read legate.core SHA - shell: bash --noprofile --norc -xeo pipefail {0} - run: | - git_tag="$(jq -r '.packages.legate_core.git_tag' cunumeric/cmake/versions.json)"; - - echo "git_tag=$git_tag" | tee -a "${GITHUB_OUTPUT}"; - - - name: Download dependencies (artifacts) - uses: ./cunumeric/.github/actions/download-artifacts - with: - device: "${{ inputs.device }}" - git_sha: "${{ steps.legate_core_info.outputs.git_tag }}" - - - if: github.repository_owner == 'nv-legate' - name: Get AWS credentials for sccache bucket - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-region: us-east-2 - role-duration-seconds: 28800 # 8 hours - role-to-assume: arn:aws:iam::279114543810:role/gha-oidc-nv-legate - - - name: Build cunumeric - run: | - export PATH="/home/coder/cunumeric/continuous_integration/scripts:$PATH" - build-cunumeric-all - - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: "cunumeric-${{ inputs.device }}-${{ github.sha }}" - path: | - /tmp/out - /tmp/conda-build diff --git a/.github/workflows/gh-test.yml b/.github/workflows/gh-test.yml deleted file mode 100644 index 675f27e9b..000000000 --- a/.github/workflows/gh-test.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Test cunumeric on GH - -on: - workflow_call: - inputs: - name: - required: true - type: string - image: - type: string - required: true - device: - required: true - type: string - runs-on: - required: true - type: string - has-gpu: - required: true - type: boolean - description: "The runner has GPU(s)." - test-options: - required: true - type: string - enabled: - required: true - type: boolean - -env: - build_artifact_name: "cunumeric-${{ inputs.device }}-${{ github.sha }}" - -jobs: - test: - name: ${{ inputs.name }} - if: inputs.enabled && github.repository_owner == 'nv-legate' - runs-on: ${{ inputs.runs-on }} - - container: - options: -u root - image: "${{ inputs.image }}" - env: - # CUDA_VERSION: "${{ inputs.CUDA }}" - NVIDIA_VISIBLE_DEVICES: ${{ env.NVIDIA_VISIBLE_DEVICES }} - - defaults: - run: - shell: su coder {0} - working-directory: /home/coder - - steps: - - if: inputs.has-gpu - name: Run nvidia-smi to make sure GPU is working - run: nvidia-smi - - - name: Install numactl - run: | - export DEBIAN_FRONTEND=noninteractive && \ - sudo apt-get update && \ - sudo apt-get install -y numactl - - - name: Checkout cunumeric - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: cunumeric - persist-credentials: false - - - name: Copy source folder - run: | - set -x - pwd - cp -r $GITHUB_WORKSPACE/cunumeric . - chown -R coder:coder cunumeric; - ls -R - - - name: Download build artifacts - uses: actions/download-artifact@v3 - with: - name: ${{ env.build_artifact_name }} - path: /home/coder/.artifacts - - - name: Run cunumeric test / analysis - shell: su coder {0} - run: | - set -x - sudo chown -R coder:coder /home/coder/.artifacts - - export PATH="/home/coder/cunumeric/continuous_integration/scripts:$PATH" - - set -eo pipefail - test-cunumeric ${{ inputs.test-options }} diff --git a/.github/workflows/require-labels.yml b/.github/workflows/require-labels.yml deleted file mode 100644 index 9b2704f70..000000000 --- a/.github/workflows/require-labels.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Pull Request Labels -on: - pull_request: - types: [opened, labeled, unlabeled, synchronize] -jobs: - label: - runs-on: ubuntu-latest - steps: - - name: Check Labels - uses: mheap/github-action-required-labels@v3 - with: - mode: exactly - count: 1 - labels: "category:new-feature, category:improvement, category:bug-fix, category:task, category:documentation" - \ No newline at end of file diff --git a/.gitignore b/.gitignore index 84244ce82..d4ccc950a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,11 +27,11 @@ legion gasnet* legion_defines.h realm_defines.h -cunumeric/install_info.py +cupynumeric/install_info.py /build/* -/docs/cunumeric/build -/docs/cunumeric/source/api/generated -/docs/cunumeric/source/comparison/comparison_table.rst.inc +/docs/cupynumeric/build +/docs/cupynumeric/source/api/generated +/docs/cupynumeric/source/comparison/comparison_table.rst.inc *.egg-info .cache .vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03cfc8b1c..8c9820450 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: mypy language: system pass_filenames: false - args: ['cunumeric'] + args: ['cupynumeric'] - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: @@ -32,6 +32,15 @@ repos: entry: python scripts/hooks/enforce_boilerplate.py language: python pass_filenames: false + - id: legate-defined + name: legate-defined + description: 'Find uses of ifdef LEGATE_ that should be using LegateDefined()' + entry: ./scripts/hooks/legate_defined.sh + language: script + 'types_or': [c++, c, cuda] + require_serial: false + stages: [pre-commit] + exclude: '^src/cupynumeric/cupynumeric_c\.h$' ci: skip: [mypy] diff --git a/CMakeLists.txt b/CMakeLists.txt index 18b121f50..b13fe7d60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,11 +41,19 @@ if(POLICY CMP0132) set(CMAKE_POLICY_DEFAULT_CMP0132 NEW) endif() +set(CMAKE_CXX_STANDARD 17 CACHE STRING "" FORCE) +set(CMAKE_CXX_STANDARD_REQUIRED ON CACHE STRING "" FORCE) + +set(CMAKE_CUDA_STANDARD 17 CACHE STRING "" FORCE) +set(CMAKE_CUDA_STANDARD_REQUIRED ON CACHE STRING "" FORCE) + ############################################################################## # - Download and initialize RAPIDS CMake helpers ----------------------------- +set(rapids-cmake-version 24.04) +set(rapids-cmake-sha "365322aca32fd6ecd7027f5d7ec7be50b7f3cc2a") if(NOT EXISTS ${CMAKE_BINARY_DIR}/RAPIDS.cmake) - file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-23.08/RAPIDS.cmake + file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-${rapids-cmake-version}/RAPIDS.cmake ${CMAKE_BINARY_DIR}/RAPIDS.cmake) endif() include(${CMAKE_BINARY_DIR}/RAPIDS.cmake) @@ -55,7 +63,7 @@ include(rapids-cuda) include(rapids-export) include(rapids-find) -set(cunumeric_version 24.01.00) +set(cupynumeric_version 24.11.00) # For now we want the optimization flags to match on both normal make and cmake # builds so we override the cmake defaults here for release, this changes @@ -70,40 +78,40 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g") set(CMAKE_CUDA_FLAGS_RELWITHDEBINFO "-O2 -g") if(NOT SKBUILD) - project(cunumeric VERSION ${cunumeric_version} LANGUAGES C CXX) - include(cunumeric_cpp.cmake) + project(cupynumeric VERSION ${cupynumeric_version} LANGUAGES C CXX) + include(cupynumeric_cpp.cmake) else() project( - cunumeric_python - VERSION ${cunumeric_version} + cupynumeric_python + VERSION ${cupynumeric_version} LANGUAGES # TODO: Building Python extension modules via the python_extension_module requires the C # language to be enabled here. The test project that is built in scikit-build to verify # various linking options for the python library is hardcoded to build with C, so until # that is fixed we need to keep C. C CXX) - include(cunumeric_python.cmake) + include(cupynumeric_python.cmake) endif() if(CMAKE_GENERATOR STREQUAL "Ninja") - function(add_touch_cunumeric_ninja_build_target) + function(add_touch_cupynumeric_ninja_build_target) set(_suf ) if(SKBUILD) set(_suf "_python") endif() - add_custom_target("touch_cunumeric${_suf}_ninja_build" ALL + add_custom_target("touch_cupynumeric${_suf}_ninja_build" ALL COMMAND ${CMAKE_COMMAND} -E touch_nocreate "${CMAKE_CURRENT_BINARY_DIR}/build.ninja" COMMENT "touch build.ninja so ninja doesn't re-run CMake on rebuild" VERBATIM ) - foreach(_dep IN ITEMS cunumeric cunumeric_python - legion_core legion_core_python + foreach(_dep IN ITEMS cupynumeric cupynumeric_python + legate legate_python Legion LegionRuntime Realm RealmRuntime Regent) if(TARGET ${_dep}) - add_dependencies("touch_cunumeric${_suf}_ninja_build" ${_dep}) + add_dependencies("touch_cupynumeric${_suf}_ninja_build" ${_dep}) endif() endforeach() endfunction() - add_touch_cunumeric_ninja_build_target() + add_touch_cupynumeric_ninja_build_target() endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e083cc3c0..b4ac11a6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,16 @@ -# Contributing to cuNumeric +# Contributing to cuPyNumeric -CuNumeric is an open-source project released under the [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0). We welcome any and all contributions, and we hope that you can help us develop a strong community. +CuPyNumeric is an open-source project released under the [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0). We welcome any and all contributions, and we hope that you can help us develop a strong community. ## How to begin -Most of the time, the best thing is to begin by [opening an issue](https://github.com/nv-legate/cunumeric/issues). This gives us a chance to discuss the contribution and to define the problem or feature that it addresses. Often, opening of the issue first may help prevent you from doing unnecessary work or to enhance and further develop your idea. +Most of the time, the best thing is to begin by [opening an issue](https://github.com/nv-legate/cupynumeric/issues). This gives us a chance to discuss the contribution and to define the problem or feature that it addresses. Often, opening of the issue first may help prevent you from doing unnecessary work or to enhance and further develop your idea. Once you are ready to start development, we ask you to work on a [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) of our repository. The next step is to create a (pull request)[https://help.github.com/en/articles/about-pull-requests]. Feel free to open the pull request as soon as you begin your development (just mark it [as a draft](https://github.blog/2019-02-14-introducing-draft-pull-requests/)) or when you are ready to have your contribution merged. ## The Legalese: Developer Certificate of Origin -CuNumeric is released under the open-source [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0), and is free to use, modify, and redistribute. To ensure that the license can be exercised without encumbrance, we ask you that you only contribute your own work or work to which you have the intellectual rights. To that end, we employ the Developer's Certificate of Origin (DCO), which is the lightweight mechanism for you to certify that you are legally able to make your contribution. Here is the full text of the certificate (also available at [DeveloperCertificate.org](https://developercertificate.org/): +CuPyNumeric is released under the open-source [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0), and is free to use, modify, and redistribute. To ensure that the license can be exercised without encumbrance, we ask you that you only contribute your own work or work to which you have the intellectual rights. To that end, we employ the Developer's Certificate of Origin (DCO), which is the lightweight mechanism for you to certify that you are legally able to make your contribution. Here is the full text of the certificate (also available at [DeveloperCertificate.org](https://developercertificate.org/): ```` Developer Certificate of Origin @@ -61,12 +61,12 @@ Please use your real name and a valid email address at which you can be reached. ## Review Process -We are really grateful that you are thinking of contributing to cuNumeric. We will make every effort to review your contributions as soon as possible. +We are really grateful that you are thinking of contributing to cuPyNumeric. We will make every effort to review your contributions as soon as possible. As we suggested at the beginning of this document, it will be really helpful to start with an issue unless your proposed change is really trivial. An issue will help to save work in the review process (e.g., maybe somebody is already working on exactly the same thing you want to work on). After you open your pull request (PR), there usually will be a community feedback that often will require further changes to your contribution (the usual open-source process). Usually, this will conclude in the PR being merged by a maintainer, but on rare occasions a PR may be rejected. This may happen, for example, if the PR appears abandoned (no response to the community feedback) or if the PR does not seem to be approaching community acceptance in a reasonable time frame. In any case, an explanation will always be given why a PR is closed. Even if a PR is closed for some reason, it may always be reopened if the situation evolves (feel free to comment on closed PRs to discuss reopening them). ## Code Formatting Requirements -CuNumeric has a set of coding standards that are expected from all the code merged into the project. The coding standards are defined by the set of tools we use to format our code. We use the [pre-commit](https://pre-commit.com/) framework to run our formatting tools. The easiest way to meet the coding standards is to simply use the pre-commit framework to run all the checks for you. Please visit the [pre-commit project page](https://pre-commit.com/) for pre-commit installation and usage instructions. Once pre-commit is installed in the cuNumeric repo, all the checks and formatting will be run on every commit, but one can also run the checks explicitly as detailed in pre-commit documentation. +CuPyNumeric has a set of coding standards that are expected from all the code merged into the project. The coding standards are defined by the set of tools we use to format our code. We use the [pre-commit](https://pre-commit.com/) framework to run our formatting tools. The easiest way to meet the coding standards is to simply use the pre-commit framework to run all the checks for you. Please visit the [pre-commit project page](https://pre-commit.com/) for pre-commit installation and usage instructions. Once pre-commit is installed in the cuPyNumeric repo, all the checks and formatting will be run on every commit, but one can also run the checks explicitly as detailed in pre-commit documentation. We hope that the automation of our formatting checks will make it easy to comply with our coding standards. If you encounter problems with code formatting, however, please let us know in a comment on your PR, and we will do our best to help. diff --git a/LICENSES_bundled.txt b/LICENSES_bundled.txt deleted file mode 100644 index d18691fd7..000000000 --- a/LICENSES_bundled.txt +++ /dev/null @@ -1,39 +0,0 @@ -The cuNumeric repository and source distributions bundle several libraries that are -compatibly licensed. We list these here. - - -Name: Cephes -Files: src/cunumeric/cephes/* -License: 3-clause BSD - Distributed under 3-clause BSD license with permission from the author, - see https://lists.debian.org/debian-legal/2004/12/msg00295.html - - Cephes Math Library Release 2.8: June, 2000 - Copyright 1984, 1995, 2000 by Stephen L. Moshier - - This software is derived from the Cephes Math Library and is - incorporated herein by permission of the author. - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index 8f77ed200..3eb2279b7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include versioneer.py -include cunumeric/_version.py -include cunumeric/py.typed +include cupynumeric/_version.py +include cupynumeric/py.typed diff --git a/README.md b/README.md index cec00b052..945124602 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# cuNumeric +# cuPyNumeric -cuNumeric is a [Legate](https://github.com/nv-legate/legate.core) library +cuPyNumeric is a [Legate](https://github.com/nv-legate/legate.core) library that aims to provide a distributed and accelerated drop-in replacement for the [NumPy API](https://numpy.org/doc/stable/reference/) on top of the -[Legion](https://legion.stanford.edu) runtime. Using cuNumeric you do things like run +[Legion](https://legion.stanford.edu) runtime. Using cuPyNumeric you can do things like run [the final example of the Python CFD course](https://github.com/barbagroup/CFDPython/blob/master/lessons/15_Step_12.ipynb) -completely unmodified on 2048 A100 GPUs in a [DGX SuperPOD](https://www.nvidia.com/en-us/data-center/dgx-superpod/) and achieve good weak scaling. +completely unmodified on 2048 A100 GPUs in a +[DGX SuperPOD](https://www.nvidia.com/en-us/data-center/dgx-superpod/) +and achieve good weak scaling. drawing -cuNumeric works best for programs that have very large arrays of data +cuPyNumeric works best for programs that have very large arrays of data that cannot fit in the memory of a single GPU or a single node and need to span multiple nodes and GPUs. While our implementation of the current NumPy API is still incomplete, programs that use unimplemented features will still work (assuming enough memory) by falling back to the canonical NumPy implementation. -If you have questions, please contact us at legate(at)nvidia.com. - ## Installation -cuNumeric is available [on conda](https://anaconda.org/legate/cunumeric). -Create a new environment containing cuNumeric: - -``` -mamba create -n myenv -c nvidia -c conda-forge -c legate cunumeric -``` - -or install it into an existing environment: - -``` -mamba install -c nvidia -c conda-forge -c legate cunumeric -``` - -Only linux-64 packages are available at the moment. - -The default package contains GPU support, and is compatible with CUDA >= 11.8 -(CUDA driver version >= r520), and Volta or later GPU architectures. There are -also CPU-only packages available, and will be automatically selected when -installing on a machine without GPUs. You can force installation of a CPU-only -package by requesting it as follows: - -``` -mamba ... cunumeric=*=*_cpu -``` - -See the build instructions at https://nv-legate.github.io/cunumeric for details -about building cuNumeric from source. - -## Usage and Execution - -Using cuNumeric as a replacement for NumPy is easy. Users only need -to replace: - -``` -import numpy as np -``` - -with: - -``` -import cunumeric as np -``` - -These programs can then be run by the Legate driver script described in the -[Legate Core](https://github.com/nv-legate/legate.core) documentation. - -``` -legate cunumeric_program.py -``` - -For execution with multiple nodes (assuming Legate Core is installed with networking support) -users can supply the `--nodes` option. For execution with GPUs, users can use the -`--gpus` flags to specify the number of GPUs to use per node. We encourage all users -to familiarize themselves with these resource flags as described in the Legate Core -documentation or simply by passing `--help` to the `legate` driver script. - -You can use `test.py` to run the test suite. Invoke the script directly or through -standard `python`; the script will invoke the `legate` driver script internally. -Check out `test.py --help` for further options. - -## Supported and Planned Features - -cuNumeric is currently a work in progress and we are gradually adding support for -additional NumPy operators. Unsupported NumPy operations will provide a -warning that we are falling back to canonical NumPy. Please report unimplemented -features that are necessary for attaining good performance so that we can triage -them and prioritize implementation appropriately. The more users that report an -unimplemented feature, the more we will prioritize it. Please include a pointer -to your code if possible too so we can see how you are using the feature in context. - -## Supported Types and Dimensions - -cuNumeric currently supports the following NumPy types: `float16`, `float32`, -`float64`, `int16`, `int32`, `int64`, `uint16`, `uint32`, `uint64`, `bool`, -`complex64`, and `complex128`. - -cuNumeric supports up to 4D arrays by default, you can adjust this setting by -installing legate.core with a larger `--max-dim`. +cuPyNumeric is available from [conda](https://docs.conda.io/projects/conda/en/latest/index.html) +on the [legate channel](https://anaconda.org/legate/cupynumeric). +See https://docs.nvidia.com/cupynumeric/latest/installation.html for +details about different install configurations, or building +cuPyNumeric from source. ## Documentation -The cuNumeric documentation can be found -[here](https://nv-legate.github.io/cunumeric). +The cuPyNumeric documentation can be found +[here](https://docs.nvidia.com/cupynumeric). -## Future Directions +## Contributing -There are three primary directions that we plan to investigate -with cuNumeric going forward: +See the discussion on contributing in [CONTRIBUTING.md](CONTRIBUTING.md). -* More features: we plan to identify a few key lighthouse applications - and use the demands of these applications to drive the addition of - new features to cuNumeric. -* We plan to add support for sharded file I/O for loading and - storing large data sets that could never be loaded on a single node. - Initially this will begin with native support for hdf5 and zarr, - but will grow to accommodate other formats needed by our lighthouse - applications. -* Strong scaling: while cuNumeric is currently implemented in a way that - enables weak scaling of codes on larger data sets, we would also like - to make it possible to strong-scale Legate applications for a single - problem size. This will require leveraging some of the more advanced - features of Legion from inside the Python interpreter. +## Contact -We are open to comments, suggestions, and ideas. +For technical questions about cuPyNumeric and Legate-based tools, please visit +the [community discussion forum](https://github.com/nv-legate/discussion). -## Contributing +If you have other questions, please contact us at legate(at)nvidia.com. -See the discussion of contributing in [CONTRIBUTING.md](CONTRIBUTING.md). - -## Known Issues - - * When using certain operations with high scratch space requirements (e.g. - `einsum` or `convolve`) you might run into the following error: - ``` - LEGION ERROR: Failed to allocate DeferredBuffer/Value/Reduction in task [some task] because [some memory] is full. This is an eager allocation ... - ``` - Currently, Legion splits its memory reservations between two pools: the - "deferred" pool, used for allocating cuNumeric `ndarray`s, and the "eager" - pool, used for allocating scratch memory for operations. The above error - message signifies that not enough memory was available for an operation's - scratch space requirements. You can work around this by allocating more - memory overall to cuNumeric (e.g. adjusting `--sysmem`, `--numamem` or - `--fbmem`), and/or by adjusting the split between the two pools (e.g. by - passing `-lg:eager_alloc_percentage 60` on the command line to allocate 60% - of memory to the eager pool, up from the default of 50%). - * cuNumeric can exercise a bug in OpenBLAS when it is run with - [multiple OpenMP processors](https://github.com/xianyi/OpenBLAS/issues/2146) - * On Mac OSX, cuNumeric can trigger a bug in Apple's implementation of libc++. - The [bug](https://bugs.llvm.org/show_bug.cgi?id=43764) has since been fixed but - likely will not show up on most Apple machines for quite some time. You may have - to manually patch your implementation of libc++. If you have trouble doing this - please contact us and we will be able to help you. +## Note +*This project, i.e., cuPyNumeric, is separate and independent of the CuPy project. CuPy is a registered trademark of Preferred Networks.* diff --git a/cmake/Modules/cpm_helpers.cmake b/cmake/Modules/cpm_helpers.cmake index 9fc28633d..acdb5d0d5 100644 --- a/cmake/Modules/cpm_helpers.cmake +++ b/cmake/Modules/cpm_helpers.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ function(get_cpm_git_args _out_var) endif() if(GIT_REPOSITORY MATCHES "github\.com") # If retrieving from github use `.zip` URL to download faster - set(cpm_git_args URL "${GIT_REPOSITORY}/archive/refs/${gh_tag_prefix}/${repo_tag}.zip") + set(cpm_git_args URL "${GIT_REPOSITORY}/archive/${repo_tag}.zip") elseif(GIT_REPOSITORY MATCHES "gitlab\.com") # GitLab archive URIs replace slashes with dashes string(REPLACE "/" "-" archive_tag "${repo_tag}") diff --git a/cmake/Modules/cuda_arch_helpers.cmake b/cmake/Modules/cuda_arch_helpers.cmake index 9a2206f69..01a192a43 100644 --- a/cmake/Modules/cuda_arch_helpers.cmake +++ b/cmake/Modules/cuda_arch_helpers.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,6 +44,9 @@ function(set_cuda_arch_from_names) if(CMAKE_CUDA_ARCHITECTURES MATCHES "ampere") list(APPEND cuda_archs 80) endif() + if(CMAKE_CUDA_ARCHITECTURES MATCHES "ada") + list(APPEND cuda_archs 89) + endif() if(CMAKE_CUDA_ARCHITECTURES MATCHES "hopper") list(APPEND cuda_archs 90) endif() @@ -65,7 +68,7 @@ function(set_cuda_arch_from_names) endfunction() function(add_cuda_architecture_defines defs) - message(VERBOSE "legate.core: CMAKE_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES}") + message(VERBOSE "legate: CMAKE_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES}") set(_defs ${${defs}}) @@ -86,6 +89,7 @@ function(add_cuda_architecture_defines defs) add_def_if_arch_enabled("70" "VOLTA_ARCH") add_def_if_arch_enabled("75" "TURING_ARCH") add_def_if_arch_enabled("80" "AMPERE_ARCH") + add_def_if_arch_enabled("89" "ADA_ARCH") add_def_if_arch_enabled("90" "HOPPER_ARCH") set(${defs} ${_defs} PARENT_SCOPE) diff --git a/cmake/Modules/set_cpu_arch_flags.cmake b/cmake/Modules/set_cpu_arch_flags.cmake index ff3e35ca3..3ee6953e8 100644 --- a/cmake/Modules/set_cpu_arch_flags.cmake +++ b/cmake/Modules/set_cpu_arch_flags.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cmake/generate_install_info_py.cmake b/cmake/generate_install_info_py.cmake index 2fb14cbcb..724640cbb 100644 --- a/cmake/generate_install_info_py.cmake +++ b/cmake/generate_install_info_py.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ execute_process( COMMAND ${CMAKE_C_COMPILER} -E -DLEGATE_USE_PYTHON_CFFI - -I "${CMAKE_CURRENT_LIST_DIR}/../src/cunumeric" - -P "${CMAKE_CURRENT_LIST_DIR}/../src/cunumeric/cunumeric_c.h" + -I "${CMAKE_CURRENT_LIST_DIR}/../src/cupynumeric" + -P "${CMAKE_CURRENT_LIST_DIR}/../src/cupynumeric/cupynumeric_c.h" ECHO_ERROR_VARIABLE OUTPUT_VARIABLE header COMMAND_ERROR_IS_FATAL ANY @@ -26,6 +26,6 @@ execute_process( set(libpath "") configure_file( - "${CMAKE_CURRENT_LIST_DIR}/../cunumeric/install_info.py.in" - "${CMAKE_CURRENT_LIST_DIR}/../cunumeric/install_info.py" + "${CMAKE_CURRENT_LIST_DIR}/../cupynumeric/install_info.py.in" + "${CMAKE_CURRENT_LIST_DIR}/../cupynumeric/install_info.py" @ONLY) diff --git a/cmake/thirdparty/get_cutensor.cmake b/cmake/thirdparty/get_cutensor.cmake index e1829d429..d271d8a7c 100644 --- a/cmake/thirdparty/get_cutensor.cmake +++ b/cmake/thirdparty/get_cutensor.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cmake/thirdparty/get_legate.cmake b/cmake/thirdparty/get_legate.cmake new file mode 100644 index 000000000..68db8207b --- /dev/null +++ b/cmake/thirdparty/get_legate.cmake @@ -0,0 +1,104 @@ +#============================================================================= +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#============================================================================= + +function(find_or_configure_legate) + set(oneValueArgs VERSION REPOSITORY BRANCH EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + include("${rapids-cmake-dir}/export/detail/parse_version.cmake") + rapids_export_parse_version(${PKG_VERSION} legate PKG_VERSION) + + include("${rapids-cmake-dir}/cpm/detail/package_details.cmake") + rapids_cpm_package_details(legate version git_repo git_branch shallow exclude_from_all) + + set(version ${PKG_VERSION}) + set(exclude_from_all ${PKG_EXCLUDE_FROM_ALL}) + if(PKG_BRANCH) + set(git_branch "${PKG_BRANCH}") + endif() + if(PKG_REPOSITORY) + set(git_repo "${PKG_REPOSITORY}") + endif() + + set(FIND_PKG_ARGS + GLOBAL_TARGETS legate::legate + BUILD_EXPORT_SET cupynumeric-exports + INSTALL_EXPORT_SET cupynumeric-exports) + + # First try to find legate via find_package() + # so the `Legion_USE_*` variables are visible + # Use QUIET find by default. + set(_find_mode QUIET) + # If legate_DIR/legate_ROOT are defined as something other than empty or NOTFOUND + # use a REQUIRED find so that the build does not silently download legate. + if(legate_DIR OR legate_ROOT) + set(_find_mode REQUIRED) + endif() + rapids_find_package(legate ${version} EXACT CONFIG ${_find_mode} ${FIND_PKG_ARGS}) + + if(legate_FOUND) + message(STATUS "CPM: using local package legate@${version}") + else() + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/cpm_helpers.cmake) + get_cpm_git_args(legate_cpm_git_args REPOSITORY ${git_repo} BRANCH ${git_branch}) + + message(VERBOSE "cupynumeric: legate version: ${version}") + message(VERBOSE "cupynumeric: legate git_repo: ${git_repo}") + message(VERBOSE "cupynumeric: legate git_branch: ${git_branch}") + message(VERBOSE "cupynumeric: legate exclude_from_all: ${exclude_from_all}") + message(VERBOSE "cupynumeric: legate legate_cpm_git_args: ${legate_cpm_git_args}") + + rapids_cpm_find(legate ${version} ${FIND_PKG_ARGS} + CPM_ARGS + ${legate_cpm_git_args} + FIND_PACKAGE_ARGUMENTS EXACT + EXCLUDE_FROM_ALL ${exclude_from_all} + ) + endif() + + set(Legion_USE_CUDA ${Legion_USE_CUDA} PARENT_SCOPE) + set(Legion_USE_OpenMP ${Legion_USE_OpenMP} PARENT_SCOPE) + set(Legion_BOUNDS_CHECKS ${Legion_BOUNDS_CHECKS} PARENT_SCOPE) + + message(VERBOSE "Legion_USE_CUDA=${Legion_USE_CUDA}") + message(VERBOSE "Legion_USE_OpenMP=${Legion_USE_OpenMP}") + message(VERBOSE "Legion_BOUNDS_CHECKS=${Legion_BOUNDS_CHECKS}") +endfunction() + +foreach(_var IN ITEMS "cupynumeric_LEGATE_VERSION" + "cupynumeric_LEGATE_BRANCH" + "cupynumeric_LEGATE_REPOSITORY" + "cupynumeric_EXCLUDE_LEGATE_FROM_ALL") + if(DEFINED ${_var}) + # Create a cupynumeric_LEGATE_BRANCH variable in the current scope either from the existing + # current-scope variable, or the cache variable. + set(${_var} "${${_var}}") + # Remove cupynumeric_LEGATE_BRANCH from the CMakeCache.txt. This ensures reconfiguring the same + # build dir without passing `-Dcupynumeric_LEGATE_BRANCH=` reverts to the value in versions.json + # instead of reusing the previous `-Dcupynumeric_LEGATE_BRANCH=` value. + unset(${_var} CACHE) + endif() +endforeach() + +if(NOT DEFINED cupynumeric_LEGATE_VERSION) + set(cupynumeric_LEGATE_VERSION "${cupynumeric_VERSION}") +endif() + +find_or_configure_legate(VERSION ${cupynumeric_LEGATE_VERSION} + REPOSITORY ${cupynumeric_LEGATE_REPOSITORY} + BRANCH ${cupynumeric_LEGATE_BRANCH} + EXCLUDE_FROM_ALL ${cupynumeric_EXCLUDE_LEGATE_FROM_ALL} +) diff --git a/cmake/thirdparty/get_legate_core.cmake b/cmake/thirdparty/get_legate_core.cmake deleted file mode 100644 index 6bd7dea04..000000000 --- a/cmake/thirdparty/get_legate_core.cmake +++ /dev/null @@ -1,103 +0,0 @@ -#============================================================================= -# Copyright 2022-2023 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#============================================================================= - -function(find_or_configure_legate_core) - set(oneValueArgs VERSION REPOSITORY BRANCH EXCLUDE_FROM_ALL) - cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - include("${rapids-cmake-dir}/export/detail/parse_version.cmake") - rapids_export_parse_version(${PKG_VERSION} legate_core PKG_VERSION) - - include("${rapids-cmake-dir}/cpm/detail/package_details.cmake") - rapids_cpm_package_details(legate_core version git_repo git_branch shallow exclude_from_all) - - set(version ${PKG_VERSION}) - set(exclude_from_all ${PKG_EXCLUDE_FROM_ALL}) - if(PKG_BRANCH) - set(git_branch "${PKG_BRANCH}") - endif() - if(PKG_REPOSITORY) - set(git_repo "${PKG_REPOSITORY}") - endif() - - set(FIND_PKG_ARGS - GLOBAL_TARGETS legate::core - BUILD_EXPORT_SET cunumeric-exports - INSTALL_EXPORT_SET cunumeric-exports) - - # First try to find legate_core via find_package() - # so the `Legion_USE_*` variables are visible - # Use QUIET find by default. - set(_find_mode QUIET) - # If legate_core_DIR/legate_core_ROOT are defined as something other than empty or NOTFOUND - # use a REQUIRED find so that the build does not silently download legate.core. - if(legate_core_DIR OR legate_core_ROOT) - set(_find_mode REQUIRED) - endif() - rapids_find_package(legate_core ${version} EXACT CONFIG ${_find_mode} ${FIND_PKG_ARGS}) - - if(legate_core_FOUND) - message(STATUS "CPM: using local package legate_core@${version}") - else() - include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/cpm_helpers.cmake) - get_cpm_git_args(legate_core_cpm_git_args REPOSITORY ${git_repo} BRANCH ${git_branch}) - - message(VERBOSE "cunumeric: legate.core version: ${version}") - message(VERBOSE "cunumeric: legate.core git_repo: ${git_repo}") - message(VERBOSE "cunumeric: legate.core git_branch: ${git_branch}") - message(VERBOSE "cunumeric: legate.core exclude_from_all: ${exclude_from_all}") - - rapids_cpm_find(legate_core ${version} ${FIND_PKG_ARGS} - CPM_ARGS - ${legate_core_cpm_git_args} - FIND_PACKAGE_ARGUMENTS EXACT - EXCLUDE_FROM_ALL ${exclude_from_all} - ) - endif() - - set(Legion_USE_CUDA ${Legion_USE_CUDA} PARENT_SCOPE) - set(Legion_USE_OpenMP ${Legion_USE_OpenMP} PARENT_SCOPE) - set(Legion_BOUNDS_CHECKS ${Legion_BOUNDS_CHECKS} PARENT_SCOPE) - - message(VERBOSE "Legion_USE_CUDA=${Legion_USE_CUDA}") - message(VERBOSE "Legion_USE_OpenMP=${Legion_USE_OpenMP}") - message(VERBOSE "Legion_BOUNDS_CHECKS=${Legion_BOUNDS_CHECKS}") -endfunction() - -foreach(_var IN ITEMS "cunumeric_LEGATE_CORE_VERSION" - "cunumeric_LEGATE_CORE_BRANCH" - "cunumeric_LEGATE_CORE_REPOSITORY" - "cunumeric_EXCLUDE_LEGATE_CORE_FROM_ALL") - if(DEFINED ${_var}) - # Create a cunumeric_LEGATE_CORE_BRANCH variable in the current scope either from the existing - # current-scope variable, or the cache variable. - set(${_var} "${${_var}}") - # Remove cunumeric_LEGATE_CORE_BRANCH from the CMakeCache.txt. This ensures reconfiguring the same - # build dir without passing `-Dcunumeric_LEGATE_CORE_BRANCH=` reverts to the value in versions.json - # instead of reusing the previous `-Dcunumeric_LEGATE_CORE_BRANCH=` value. - unset(${_var} CACHE) - endif() -endforeach() - -if(NOT DEFINED cunumeric_LEGATE_CORE_VERSION) - set(cunumeric_LEGATE_CORE_VERSION "${cunumeric_VERSION}") -endif() - -find_or_configure_legate_core(VERSION ${cunumeric_LEGATE_CORE_VERSION} - REPOSITORY ${cunumeric_LEGATE_CORE_REPOSITORY} - BRANCH ${cunumeric_LEGATE_CORE_BRANCH} - EXCLUDE_FROM_ALL ${cunumeric_EXCLUDE_LEGATE_CORE_FROM_ALL} -) diff --git a/cmake/thirdparty/get_nccl.cmake b/cmake/thirdparty/get_nccl.cmake index 1aee52b6f..3208de846 100644 --- a/cmake/thirdparty/get_nccl.cmake +++ b/cmake/thirdparty/get_nccl.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cmake/thirdparty/get_openblas.cmake b/cmake/thirdparty/get_openblas.cmake index 82bef489d..aa7030ca5 100644 --- a/cmake/thirdparty/get_openblas.cmake +++ b/cmake/thirdparty/get_openblas.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ function(find_or_configure_OpenBLAS) set(BLAS_name "OpenBLAS") set(BLAS_target "openblas") - # cuNumeric presently requires OpenBLAS + # cuPyNumeric presently requires OpenBLAS set(BLA_VENDOR OpenBLAS) # TODO: should we find (or build) 64-bit BLAS? @@ -35,8 +35,8 @@ function(find_or_configure_OpenBLAS) set(FIND_PKG_ARGS ${PKG_VERSION} GLOBAL_TARGETS ${BLAS_target} - BUILD_EXPORT_SET cunumeric-exports - INSTALL_EXPORT_SET cunumeric-exports) + BUILD_EXPORT_SET cupynumeric-exports + INSTALL_EXPORT_SET cupynumeric-exports) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/cpm_helpers.cmake) if(PKG_BRANCH) @@ -105,35 +105,35 @@ function(find_or_configure_OpenBLAS) FINAL_CODE_BLOCK code_string) # Do `CPMFindPackage(BLAS)` in build dir - rapids_export_package(BUILD BLAS cunumeric-exports + rapids_export_package(BUILD BLAS cupynumeric-exports VERSION ${PKG_VERSION} GLOBAL_TARGETS ${BLAS_target}) # Tell cmake where it can find the generated blas-config.cmake include("${rapids-cmake-dir}/export/find_package_root.cmake") - rapids_export_find_package_root(BUILD BLAS [=[${CMAKE_CURRENT_LIST_DIR}]=] cunumeric-exports) + rapids_export_find_package_root(BUILD BLAS [=[${CMAKE_CURRENT_LIST_DIR}]=] EXPORT_SET cupynumeric-exports) endif() endfunction() -if(NOT DEFINED cunumeric_OPENBLAS_VERSION) +if(NOT DEFINED cupynumeric_OPENBLAS_VERSION) # Before v0.3.18, OpenBLAS's throws CMake errors when configuring - set(cunumeric_OPENBLAS_VERSION "0.3.20") + set(cupynumeric_OPENBLAS_VERSION "0.3.20") endif() -if(NOT DEFINED cunumeric_OPENBLAS_BRANCH) - set(cunumeric_OPENBLAS_BRANCH "") +if(NOT DEFINED cupynumeric_OPENBLAS_BRANCH) + set(cupynumeric_OPENBLAS_BRANCH "") endif() -if(NOT DEFINED cunumeric_OPENBLAS_TAG) - set(cunumeric_OPENBLAS_TAG v${cunumeric_OPENBLAS_VERSION}) +if(NOT DEFINED cupynumeric_OPENBLAS_TAG) + set(cupynumeric_OPENBLAS_TAG v${cupynumeric_OPENBLAS_VERSION}) endif() -if(NOT DEFINED cunumeric_OPENBLAS_REPOSITORY) - set(cunumeric_OPENBLAS_REPOSITORY https://github.com/xianyi/OpenBLAS.git) +if(NOT DEFINED cupynumeric_OPENBLAS_REPOSITORY) + set(cupynumeric_OPENBLAS_REPOSITORY https://github.com/xianyi/OpenBLAS.git) endif() -find_or_configure_OpenBLAS(VERSION ${cunumeric_OPENBLAS_VERSION} - REPOSITORY ${cunumeric_OPENBLAS_REPOSITORY} - BRANCH ${cunumeric_OPENBLAS_BRANCH} - PINNED_TAG ${cunumeric_OPENBLAS_TAG} - EXCLUDE_FROM_ALL ${cunumeric_EXCLUDE_OPENBLAS_FROM_ALL} +find_or_configure_OpenBLAS(VERSION ${cupynumeric_OPENBLAS_VERSION} + REPOSITORY ${cupynumeric_OPENBLAS_REPOSITORY} + BRANCH ${cupynumeric_OPENBLAS_BRANCH} + PINNED_TAG ${cupynumeric_OPENBLAS_TAG} + EXCLUDE_FROM_ALL ${cupynumeric_EXCLUDE_OPENBLAS_FROM_ALL} ) diff --git a/cmake/thirdparty/get_tblis.cmake b/cmake/thirdparty/get_tblis.cmake index b02afbd7d..164923601 100644 --- a/cmake/thirdparty/get_tblis.cmake +++ b/cmake/thirdparty/get_tblis.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,14 +34,14 @@ function(find_or_configure_tblis) HEADER_NAMES "tblis/tblis.h" LIBRARY_NAMES "libtblis${lib_suffix}" NO_CONFIG - BUILD_EXPORT_SET cunumeric-exports - INSTALL_EXPORT_SET cunumeric-exports + BUILD_EXPORT_SET cupynumeric-exports + INSTALL_EXPORT_SET cupynumeric-exports ) rapids_cpm_find(tblis ${PKG_VERSION} GLOBAL_TARGETS tblis::tblis - BUILD_EXPORT_SET cunumeric-exports - INSTALL_EXPORT_SET cunumeric-exports + BUILD_EXPORT_SET cupynumeric-exports + INSTALL_EXPORT_SET cupynumeric-exports CPM_ARGS ${tblis_cpm_git_args} EXCLUDE_FROM_ALL ${PKG_EXCLUDE_FROM_ALL} @@ -95,8 +95,8 @@ function(find_or_configure_tblis) set(ENV{CC} "${_CC}") set(ENV{CXX} "${_CXX}") - message(VERBOSE "cunumeric: ENV{CC}=\"$ENV{CC}\"") - message(VERBOSE "cunumeric: ENV{CXX}=\"$ENV{CXX}\"") + message(VERBOSE "cupynumeric: ENV{CC}=\"$ENV{CC}\"") + message(VERBOSE "cupynumeric: ENV{CXX}=\"$ENV{CXX}\"") set(tblis_verbosity "--enable-silent-rules") if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.25") @@ -167,20 +167,20 @@ function(find_or_configure_tblis) endif() set(tblis_BINARY_DIR ${tblis_BINARY_DIR} PARENT_SCOPE) - set(cunumeric_INSTALL_TBLIS ${should_build_tblis} PARENT_SCOPE) + set(cupynumeric_INSTALL_TBLIS ${should_build_tblis} PARENT_SCOPE) endfunction() -if(NOT DEFINED cunumeric_TBLIS_BRANCH) - set(cunumeric_TBLIS_BRANCH master) +if(NOT DEFINED cupynumeric_TBLIS_BRANCH) + set(cupynumeric_TBLIS_BRANCH arm-build) endif() -if(NOT DEFINED cunumeric_TBLIS_REPOSITORY) - set(cunumeric_TBLIS_REPOSITORY https://github.com/devinamatthews/tblis.git) +if(NOT DEFINED cupynumeric_TBLIS_REPOSITORY) + set(cupynumeric_TBLIS_REPOSITORY https://github.com/nv-legate/tblis.git) endif() find_or_configure_tblis(VERSION 1.2.0 - REPOSITORY ${cunumeric_TBLIS_REPOSITORY} - BRANCH ${cunumeric_TBLIS_BRANCH} - EXCLUDE_FROM_ALL ${cunumeric_EXCLUDE_TBLIS_FROM_ALL} + REPOSITORY ${cupynumeric_TBLIS_REPOSITORY} + BRANCH ${cupynumeric_TBLIS_BRANCH} + EXCLUDE_FROM_ALL ${cupynumeric_EXCLUDE_TBLIS_FROM_ALL} USE_OPENMP ${Legion_USE_OpenMP} ) diff --git a/cmake/versions.json b/cmake/versions.json index 80128714e..b96665e2c 100644 --- a/cmake/versions.json +++ b/cmake/versions.json @@ -1,11 +1,16 @@ { "packages" : { - "legate_core" : { - "version": "24.01.00", - "git_url" : "https://github.com/nv-legate/legate.core.git", + "legate" : { + "repo": "legate.core.internal", + "artifact_name": "${{ inputs.platform }}-${{ inputs.build-type }}-<>-python${{ inputs.python-version }}-${{ inputs.target-device }}-release-with_tests-${{ inputs.network }}-<>", + "org": "nv-legate", + "artifact_workflow": "ci-gh.yml", + "nightly_workflow": "ci-gh-nightly-release.yml", + "version": "24.11.00", + "git_url" : "git@github.com:nv-legate/legate.core.internal.git", "git_shallow": false, "always_download": false, - "git_tag" : "b84e86e9e3518d7102cdf531b5b733dc09c0e8d9" + "git_tag" : "583cbc0bae357de2eb88b57a88e911750b82ca4f" } } } diff --git a/conda/conda-build/build.sh b/conda/conda-build/build.sh index b78740090..2a7b6589f 100644 --- a/conda/conda-build/build.sh +++ b/conda/conda-build/build.sh @@ -1,5 +1,9 @@ #!/bin/bash +echo -e "\n\n--------------------- CONDA/CONDA-BUILD/BUILD.SH -----------------------\n" + +set -xeo pipefail; + # Rewrite conda's -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY to # -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH CMAKE_ARGS="$(echo "$CMAKE_ARGS" | sed -r "s@_INCLUDE=ONLY@_INCLUDE=BOTH@g")" @@ -7,40 +11,42 @@ CMAKE_ARGS="$(echo "$CMAKE_ARGS" | sed -r "s@_INCLUDE=ONLY@_INCLUDE=BOTH@g")" # Add our options to conda's CMAKE_ARGS CMAKE_ARGS+=" --log-level=VERBOSE --DBUILD_MARCH=haswell" +-DBUILD_SHARED_LIBS=ON +-DBUILD_MARCH=${BUILD_MARCH} +-DCMAKE_BUILD_TYPE=Release +-DCMAKE_VERBOSE_MAKEFILE=ON +-DCMAKE_BUILD_PARALLEL_LEVEL=${JOBS:-$(nproc --ignore=1)}" # We rely on an environment variable to determine if we need to build cpu-only bits if [ -z "$CPU_ONLY" ]; then # cutensor, relying on the conda cutensor package CMAKE_ARGS+=" -Dcutensor_DIR=$PREFIX --DCMAKE_CUDA_ARCHITECTURES:LIST=60-real;70-real;75-real;80-real;90 -" +-DCMAKE_CUDA_ARCHITECTURES=all-major" else # When we build without cuda, we need to provide the location of curand CMAKE_ARGS+=" --Dcunumeric_cuRAND_INCLUDE_DIR=$PREFIX -" +-Dcupynumeric_cuRAND_INCLUDE_DIR=$PREFIX/targets/x86_64-linux/include" fi -# Do not compile with NDEBUG until Legion handles it without warnings -export CFLAGS="-UNDEBUG" -export CXXFLAGS="-UNDEBUG" -export CPPFLAGS="-UNDEBUG" -export CUDAFLAGS="-UNDEBUG" export CMAKE_GENERATOR=Ninja export CUDAHOSTCXX=${CXX} +export OPENSSL_DIR="$PREFIX" + +echo "Environment" +env echo "Build starting on $(date)" +CUDAFLAGS="-isystem ${PREFIX}/include -L${PREFIX}/lib" +export CUDAFLAGS cmake -S . -B build ${CMAKE_ARGS} -DCMAKE_BUILD_PARALLEL_LEVEL=$CPU_COUNT -cmake --build build -j$CPU_COUNT +cmake --build build -j$CPU_COUNT --verbose cmake --install build CMAKE_ARGS=" --DFIND_CUNUMERIC_CPP=ON --Dcunumeric_ROOT=$PREFIX -" +-DFIND_CUPYNUMERIC_CPP=ON +-Dcupynumeric_ROOT=$PREFIX" SKBUILD_BUILD_OPTIONS=-j$CPU_COUNT \ $PYTHON -m pip install \ @@ -48,12 +54,9 @@ $PYTHON -m pip install \ --no-deps \ --prefix "$PREFIX" \ --no-build-isolation \ + --upgrade \ --cache-dir "$PIP_CACHE_DIR" \ --disable-pip-version-check \ . -vv echo "Build ending on $(date)" - -# Legion leaves an egg-info file which will confuse conda trying to pick up the information -# Remove it so the legate-core is the only egg-info file added -rm -rf $SP_DIR/legion*egg-info diff --git a/conda/conda-build/conda_build_config.yaml b/conda/conda-build/conda_build_config.yaml index a508a6ed1..4ec5498e9 100644 --- a/conda/conda-build/conda_build_config.yaml +++ b/conda/conda-build/conda_build_config.yaml @@ -2,13 +2,16 @@ gpu_enabled: - true - false +upload_build: + - true + - false + python: - - 3.9 - 3.10 - 3.11 numpy_version: - - ">=1.22" + - ">=1.22,<2" cmake_version: - ">=3.20.1,!=3.23.0" diff --git a/conda/conda-build/meta.yaml b/conda/conda-build/meta.yaml index 85ea88d74..2aaddb22b 100644 --- a/conda/conda-build/meta.yaml +++ b/conda/conda-build/meta.yaml @@ -1,4 +1,4 @@ -{% set name = "cunumeric" %} +{% set name = "cupynumeric" %} {% if gpu_enabled == "true" %} {% set gpu_enabled_bool = true %} {% elif gpu_enabled == "false" %} @@ -7,10 +7,18 @@ {# We need to have a default value for the initial pass over the recipe #} {% set gpu_enabled_bool = false %} {% endif %} +{% if upload_build == "true" %} + {% set upload_build_bool = true %} +{% elif upload_build == "false" %} + {% set upload_build_bool = false %} +{% else %} + {# We need to have a default value for the initial pass over the recipe #} + {% set upload_build_bool = false %} +{% endif %} ## The placeholder version is strictly for making two-pass conda build process. ## It should not be used for any other purpose, and this is not a default version. {% set placeholder_version = '0.0.0.dev' %} -{% set default_cuda_version = '12.0' %} +{% set default_cuda_version = '12.2.2' %} {% set cuda_version='.'.join(environ.get('CUDA', default_cuda_version).split('.')[:2]) %} {% set cuda_major=cuda_version.split('.')[0]|int %} {% set py_version=environ.get('CONDA_PY', '') %} @@ -21,7 +29,7 @@ {% endif %} {% if package_version is defined %} {% set version = package_version %} - {% set core_version = version %} + {% set legate_version = version %} ## The tag must be in one of the two formats (where N is a digit): ## vNN.NN.NN ## vNN.NN.NN.dev @@ -29,10 +37,10 @@ ## Note: default values are only given to make conda build work. They should not be necessary in principle. {% elif 'dev' in environ.get('GIT_DESCRIBE_TAG', placeholder_version) %} {% set version = (environ.get('GIT_DESCRIBE_TAG', placeholder_version) ~ environ.get('GIT_DESCRIBE_NUMBER', '')).lstrip('v') %} - {% set core_version = (version.rsplit('.',1)[0] ~ ".dev" ~ "|>=" ~ version.rsplit('.',1)[0]) %} + {% set legate_version = (version.rsplit('.',1)[0] ~ ".dev" ~ "|>=" ~ version.rsplit('.',1)[0]) %} {% else %} {% set version = environ.get('GIT_DESCRIBE_TAG', placeholder_version).lstrip('v') %} - {% set core_version = version %} + {% set legate_version = version %} {% endif %} package: @@ -53,20 +61,18 @@ build: number: {{ build_number }} missing_dso_whitelist: - '*libcuda.so*' -{% if use_local_path is not defined %} -# use git hash -{% if not gpu_enabled_bool %} - string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ GIT_DESCRIBE_HASH }}_{{ PKG_BUILDNUM }}_cpu" +{% if gpu_enabled_bool %} +{% set cpu_gpu_tag='_gpu' %} {% else %} - string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ GIT_DESCRIBE_HASH }}_{{ PKG_BUILDNUM }}" +{% set cpu_gpu_tag='_cpu' %} {% endif %} +{% set upload_tag='' if upload_build_bool else '_with_tests' %} +{% if use_local_path is not defined %} +# use git hash + string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ GIT_DESCRIBE_HASH }}_{{ PKG_BUILDNUM }}{{ cpu_gpu_tag }}{{ upload_tag }}" {% else %} # do not use git hash -{% if not gpu_enabled_bool %} - string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ PKG_BUILDNUM }}_cpu" -{% else %} - string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ PKG_BUILDNUM }}" -{% endif %} + string: "cuda{{ cuda_major }}_py{{ py_version }}_{{ PKG_BUILDNUM }}{{ cpu_gpu_tag }}{{ upload_tag }}" {% endif %} script_env: - SCCACHE_BUCKET @@ -76,22 +82,21 @@ build: - SCCACHE_S3_KEY_PREFIX - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - - CMAKE_C_COMPILER_LAUNCHER - - CMAKE_CXX_COMPILER_LAUNCHER - - CMAKE_CUDA_COMPILER_LAUNCHER {% if not gpu_enabled_bool %} - CPU_ONLY=1 + # The CPU-only packages having more track_features than the GPU builds helps + # the solver to prefer the GPU builds when both are viable candidates. + # ref: https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#track-features track_features: - cpu_only -{% else %} -# prevent nccl from pulling in cudatoolkit - ignore_run_exports: - - cudatoolkit - ignore_run_exports_from: - - cuda-nvcc - - legate-core {% endif %} +ignore_run_exports_from: + # scikit-build should really be a part of the build env, but then it installs its own Python. Conda build stacks + # the build environment on the host environment, and the build python takes over causing paths havoc. So, we put + # scikit-build into the host env, but we ignore any exports it may bring. + - scikit-build + requirements: build: - make @@ -99,73 +104,60 @@ requirements: - cmake {{ cmake_version }} - {{ compiler('c') }} =11.2 - {{ compiler('cxx') }} =11.2 - host: # the nvcc requirement is necessary because it contains crt/host_config.h used by cuda runtime. This is a packaging bug that has been reported. - - cuda-nvcc ={{ cuda_version }} - # libcurand is used both in CPU and GPU builds - - libcurand-dev + - cuda-nvcc # cudart needed for CPU and GPU builds because of curand - - cuda-cudart-dev ={{ cuda_version }} + - cuda-cudart-dev + - cuda-version ={{ cuda_version }} + - libcurand-dev + + host: - python - scikit-build - openblas =* =*openmp* -{% if not gpu_enabled_bool %} - - legate-core ={{ core_version }} =*_cpu -{% else %} - - legate-core ={{ core_version }} - - cuda-nvcc ={{ cuda_version }} - - cuda-cccl ={{ cuda_version }} - - cuda-cudart ={{ cuda_version }} - - cuda-cudart-static ={{ cuda_version }} - - cuda-driver-dev ={{ cuda_version }} - - cuda-cudart-dev ={{ cuda_version }} - - cuda-nvtx ={{ cuda_version }} - # - libcutensor-dev >=1.3 - - cutensor >=1.3,<2.0 =*_* +{% if gpu_enabled_bool %} + - legate >={{ legate_version }} =*_gpu* + - cuda-cccl + - cutensor >=2.0 =*_* - libcublas-dev - libcusolver-dev - libcufft-dev - - nccl + - libcurand-dev + - libcufile-dev + - cuda-version ={{ cuda_version }} +{% else %} + - legate >={{ legate_version }} =*_cpu* {% endif %} run: - numpy {{ numpy_version }} - - libopenblas =* =*openmp* -{% if not gpu_enabled_bool %} - - legate-core ={{ core_version }} =*_cpu -{% else %} - - legate-core ={{ core_version }} - - cuda-cudart >={{ cuda_version }},<{{ cuda_major+1 }} - - cuda-version >={{ cuda_version }},<{{ cuda_major+1 }} - - cutensor >=1.3 =*_* - - libcublas - - libcusolver >=11.4.1.48-0 - - libcufft + - opt_einsum >=3.3 + - scipy + - openblas =* =*openmp* +{% if gpu_enabled_bool %} - libnvjitlink - libcusparse + - cutensor >=2.0 =*_* + # Pin to all minor versions of CUDA newer than the one built against, within the same major version. + # cuda-version constrains the CUDA runtime version and ensures a compatible driver is available + - {{ pin_compatible('cuda-version', min_pin='x.x', max_pin='x') }} + - __cuda >={{ cuda_version }} {% endif %} - - opt_einsum >=3.3 - - scipy - - typing_extensions run_constrained: - __glibc >=2.17 # [linux] - - python != 3.9.7 -{% if gpu_enabled_bool %} - - __cuda >={{ cuda_version }} -{% endif %} about: - home: https://github.com/nv-legate/cunumeric + home: https://github.com/nv-legate/cupynumeric license: Apache-2.0 license_file: LICENSE summary: 'Drop-in Replacment for NumPy' description: | - cuNumeric is a Legate library that aims to provide + cuPyNumeric is a Legate library that aims to provide a distributed and accelerated drop-in replacement for the NumPy API on top of the Legion runtime. - doc_url: https://github.com/nv-legate/cunumeric - dev_url: https://github.com/nv-legate/cunumeric + doc_url: https://github.com/nv-legate/cupynumeric + dev_url: https://github.com/nv-legate/cupynumeric extra: recipe-maintainers: diff --git a/continuous_integration/dot-gitconfig b/continuous_integration/dot-gitconfig deleted file mode 100644 index 91ac79c70..000000000 --- a/continuous_integration/dot-gitconfig +++ /dev/null @@ -1,3 +0,0 @@ -[user] - email = users.noreply.github.com - name = anon \ No newline at end of file diff --git a/continuous_integration/scripts/build-cunumeric-all b/continuous_integration/scripts/build-cunumeric-all deleted file mode 100755 index 66f5ccb6e..000000000 --- a/continuous_integration/scripts/build-cunumeric-all +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -setup_env() { - yaml_file=$(find ~/.artifacts -name "environment*.yaml" | head -n 1) - - echo "YAML file..." - cat "${yaml_file}" - - mkdir -p /tmp/out; - - cp "${yaml_file}" /tmp/out - - mamba env create -n legate -f "$yaml_file" - - mamba uninstall -yn legate numpy - - mamba install -yn legate -c ~/.artifacts/legate_core -c conda-forge -c nvidia legate-core - - mamba activate legate -} - -build_cunumeric_all() { - set -xeo pipefail - - setup_env; - cd ~/cunumeric; - conda info; - - set -xeuo pipefail; - printf "\n\n\n\n********* BUILDING CUNUMERIC CPP *********\n" - build-cunumeric-cpp; - printf "\n\n\n\n********* BUILDING CUNUMERIC WHEEL *********\n" - build-cunumeric-wheel; - printf "\n\n\n\n********* BUILDING CUNUMERIC CONDA *********\n" - build-cunumeric-conda; -} - -(build_cunumeric_all "$@"); diff --git a/continuous_integration/scripts/build-cunumeric-conda b/continuous_integration/scripts/build-cunumeric-conda deleted file mode 100755 index ee4efefcb..000000000 --- a/continuous_integration/scripts/build-cunumeric-conda +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash - -build_cunumeric_conda_package() { - set -xeuo pipefail; - - local python_version="${PYTHON_VERSION:-}"; - - if [ -z "${python_version}" ]; then - python_version="$(python3 --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f3 --complement)"; - fi - - mkdir -p /tmp/conda-build /tmp/out - cp -r ~/.artifacts/legate_core /tmp/conda-build/ - - local conda_build_args=(); - conda_build_args+=(--override-channels); - conda_build_args+=(-c conda-forge); - conda_build_args+=(-c nvidia); - conda_build_args+=(-c file:///tmp/conda-build/legate_core); - conda_build_args+=(--croot /tmp/conda-build/cunumeric); - conda_build_args+=(--numpy 1.22); - conda_build_args+=(--python ${python_version}); - conda_build_args+=(--no-test); - conda_build_args+=(--no-verify); - conda_build_args+=(--no-build-id); - conda_build_args+=("--build-id-pat=''"); - conda_build_args+=(--no-include-recipe); - conda_build_args+=(--no-anaconda-upload); - - GPU_ENABLED=true - [ "${USE_CUDA:-}" = "OFF" ] && GPU_ENABLED=false - - conda_build_args+=(--variants "{gpu_enabled:${GPU_ENABLED},python:${python_version}}"); - - rm -rf /tmp/conda-build/cunumeric; - mkdir -p /tmp/conda-build/cunumeric; - - # Synthesize new cunumeric conda-build build.sh script - - cat < ~/cunumeric/conda/conda-build/conda_build_config.yaml -gpu_enabled: - - "${GPU_ENABLED}" - -python: - - "${python_version}" - -numpy_version: - - ">=1.22" - -cmake_version: - - ">=3.20.1,!=3.23.0" - -use_local_path: - - "true" - -numpy: - - 1.22 - -package_version: - - "$(git -C ~/cunumeric describe --abbrev=0 --tags | sed 's/[a-zA-Z]//g' | cut -d '.' -f -2).00" -EOF - - cat <<"EOF" > ~/cunumeric/conda/conda-build/build.sh -# Install cunumeric C++ libs -tar -C "$PREFIX" --exclude="*.a" --strip-components=1 -xvf /tmp/out/cunumeric-*-Linux.tar.gz; - -# Install cunumeric Python wheel -pip install --no-deps --root / --prefix "$PREFIX" /tmp/out/cunumeric-*.whl; -EOF - - git -C ~/cunumeric add .; - git -C ~/cunumeric commit --allow-empty --allow-empty-message -n -m ""; - - # Build cuNumeric conda package - CUDA=${CUDA_VERSION_MAJOR}.${CUDA_VERSION_MINOR} \ - conda mambabuild ${conda_build_args[@]} ~/cunumeric/conda/conda-build; - - git -C ~/cunumeric reset --hard HEAD~1; - - cp /tmp/conda-build/cunumeric/linux-64/cunumeric-*.tar.bz2 /tmp/out/; - - { set +x; } 2>/dev/null; -} - -(build_cunumeric_conda_package "$@"); diff --git a/continuous_integration/scripts/build-cunumeric-cpp b/continuous_integration/scripts/build-cunumeric-cpp deleted file mode 100755 index fd08ceac2..000000000 --- a/continuous_integration/scripts/build-cunumeric-cpp +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -build_cunumeric_cpp() { - set -xeuo pipefail; - - # Build + package cuNumeric C++ libs - local cmake_args=(${CMAKE_ARGS:-}); - cmake_args+=(-DBUILD_SHARED_LIBS=ON); - cmake_args+=(-DBUILD_MARCH=${BUILD_MARCH:-haswell}); - cmake_args+=(-DCMAKE_BUILD_TYPE=Release); - cmake_args+=(-DCMAKE_CUDA_ARCHITECTURES=RAPIDS); - cmake_args+=(-DCMAKE_BUILD_PARALLEL_LEVEL=${JOBS:-$(nproc --ignore=1)}); - cmake_args+=(${@}); - - cmake -S ~/cunumeric -B ~/cunumeric/build ${cmake_args[@]} -GNinja; - - sccache --show-stats; - - time cmake --build ~/cunumeric/build --verbose --parallel ${JOBS:-$(nproc --ignore=1)}; - - sccache --show-stats; - - ( - mkdir -p /tmp/out; - cd ~/cunumeric/build; - cpack -G TGZ; - cp ./*-Linux.tar.gz /tmp/out/; - ); - - { set +x; } 2>/dev/null; -} - -(build_cunumeric_cpp "$@"); diff --git a/continuous_integration/scripts/build-cunumeric-wheel b/continuous_integration/scripts/build-cunumeric-wheel deleted file mode 100755 index 5f55136eb..000000000 --- a/continuous_integration/scripts/build-cunumeric-wheel +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -build_cunumeric_wheel() { - set -xeuo pipefail; - - mkdir -p /tmp/out; - - local pip_args=(-vv); - pip_args+=(--wheel-dir /tmp/out); - - if type conda 2>&1 >/dev/null; then - pip_args+=(--no-deps); - pip_args+=(--no-build-isolation); - fi - - local cmake_args=(${CMAKE_ARGS:-}); - cmake_args+=("-DFIND_CUNUMERIC_CPP=ON"); - cmake_args+=("-Dcunumeric_ROOT=$HOME/cunumeric/build"); - - # Build + package cuNumeric Python wheel - CMAKE_ARGS="${cmake_args[@]}" \ - pip wheel ${pip_args[@]} ~/cunumeric; - - { set +x; } 2>/dev/null; -} - -(build_cunumeric_wheel "$@"); diff --git a/continuous_integration/scripts/entrypoint b/continuous_integration/scripts/entrypoint deleted file mode 100755 index 298fc1c7a..000000000 --- a/continuous_integration/scripts/entrypoint +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -sccache_stop_server_and_show_stats() { - sccache --stop-server || true && sccache --show-stats; -} - -init_devcontainer() { - # disable xtrace and history - local xtrace_enabled=$(echo "${SHELLOPTS:-}" | grep -q 'xtrace'; echo $?); - local history_enabled=$(echo "${SHELLOPTS:-}" | grep -q 'history'; echo $?); - { set +xo history; } 2>/dev/null; - eval "export $(find /run/secrets/ -type f -exec bash -c 'echo ${0/\/run\/secrets\//}=$(<${0})' {} \;)"; - if [ "${history_enabled}" -eq "0" ]; then { set -o history; } 2>/dev/null; fi; - if [ "${xtrace_enabled}" -eq "0" ]; then { set -o xtrace; } 2>/dev/null; fi; - - set -xeo pipefail - - . devcontainer-utils-post-attach-command; - - sleep 10; - . devcontainer-utils-vault-s3-test; - . devcontainer-utils-vault-s3-export 0; -} - -entrypoint() { - set -x - - mkdir -p /home/coder/.cache; - - local secrets_dir=/run/secrets - - if [ -d "$secrets_dir" ] && [ "$(ls -A $secrets_dir)" ]; then - init_devcontainer - else - sccache_stop_server_and_show_stats - fi - - echo AWS_REGION=${AWS_REGION:-} - echo AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} - echo AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-} - echo AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-} - - exec "$@"; -} - -entrypoint "$@"; diff --git a/continuous_integration/scripts/test-cunumeric b/continuous_integration/scripts/test-cunumeric deleted file mode 100755 index 698179b31..000000000 --- a/continuous_integration/scripts/test-cunumeric +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -setup_env() { - mamba create -yn legate -c ~/.artifacts/conda-build/legate_core -c ~/.artifacts/conda-build/cunumeric -c conda-forge -c "nvidia/label/cuda-12.0.0" legate-core cunumeric -} - -setup_test_env() { - mamba install -y "clang-tools>=8" "clang>=8" colorama coverage mock pre-commit pytest-cov pytest-lazy-fixture pytest-mock "pytest<8" types-docutils pynvml - - pip install tifffile -} - -setup_docs_env() { - mamba install -y pandoc doxygen - pip install ipython jinja2 "markdown<3.4.0" "pydata-sphinx-theme>=0.13" myst-parser nbsphinx sphinx-copybutton "sphinx>=4.4.0" - -} - -setup_mypy_env() { - mamba install -y "mypy>=0.961" jinja2 nbsphinx sphinx-copybutton "sphinx>=4.4.0" types-docutils -} - -test-cunumeric() { - set -xeo pipefail - - setup_env; - - set +u - mamba activate legate; - conda info; - - cd ~/cunumeric; - - case "$1" in - "test") - echo "Executing tests..." - shift; - setup_test_env; - ./test.py --verbose "$@" - ;; - "mypy") - echo "Installing and executing mypy..." - shift; - setup_mypy_env; - mypy cunumeric - ;; - "docs") - echo "Building docs..." - shift; - setup_docs_env; - cd docs/cunumeric - make clean html - ;; - *) - echo "Invalid command: $1" - return 1 - ;; - esac -} - -(test-cunumeric "$@"); diff --git a/cunumeric/_ufunc/comparison.py b/cunumeric/_ufunc/comparison.py deleted file mode 100644 index d1df4e8de..000000000 --- a/cunumeric/_ufunc/comparison.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from cunumeric.config import BinaryOpCode, UnaryOpCode, UnaryRedCode - -from .ufunc import ( - all_dtypes, - create_binary_ufunc, - create_unary_ufunc, - float_dtypes, - integer_dtypes, - predicate_types_of, - relation_types_of, -) - -greater = create_binary_ufunc( - "Return the truth value of (x1 > x2) element-wise.", - "greater", - BinaryOpCode.GREATER, - relation_types_of(all_dtypes), -) - -greater_equal = create_binary_ufunc( - "Return the truth value of (x1 >= x2) element-wise.", - "greater_equal", - BinaryOpCode.GREATER_EQUAL, - relation_types_of(all_dtypes), -) - -less = create_binary_ufunc( - "Return the truth value of (x1 < x2) element-wise.", - "less", - BinaryOpCode.LESS, - relation_types_of(all_dtypes), -) - -less_equal = create_binary_ufunc( - "Return the truth value of (x1 =< x2) element-wise.", - "less", - BinaryOpCode.LESS_EQUAL, - relation_types_of(all_dtypes), -) - -not_equal = create_binary_ufunc( - "Return (x1 != x2) element-wise.", - "not_equal", - BinaryOpCode.NOT_EQUAL, - relation_types_of(all_dtypes), -) - -equal = create_binary_ufunc( - "Return (x1 == x2) element-wise.", - "equal", - BinaryOpCode.EQUAL, - relation_types_of(all_dtypes), -) - -logical_and = create_binary_ufunc( - "Compute the truth value of x1 AND x2 element-wise.", - "logical_and", - BinaryOpCode.LOGICAL_AND, - relation_types_of(all_dtypes), -) - -logical_or = create_binary_ufunc( - "Compute the truth value of x1 OR x2 element-wise.", - "logical_or", - BinaryOpCode.LOGICAL_OR, - relation_types_of(all_dtypes), -) - -logical_xor = create_binary_ufunc( - "Compute the truth value of x1 XOR x2, element-wise.", - "logical_xor", - BinaryOpCode.LOGICAL_XOR, - relation_types_of(all_dtypes), -) - -logical_not = create_unary_ufunc( - "Compute bit-wise inversion, or bit-wise NOT, element-wise.", - "invert", - UnaryOpCode.LOGICAL_NOT, - ( - ["??"] - + predicate_types_of(integer_dtypes) - + predicate_types_of(float_dtypes) - ), - overrides={"?": UnaryOpCode.LOGICAL_NOT}, -) - -maximum = create_binary_ufunc( - "Element-wise maximum of array elements.", - "maximum", - BinaryOpCode.MAXIMUM, - all_dtypes, - red_code=UnaryRedCode.MAX, -) - -fmax = maximum - -minimum = create_binary_ufunc( - "Element-wise minimum of array elements.", - "minimum", - BinaryOpCode.MINIMUM, - all_dtypes, - red_code=UnaryRedCode.MIN, -) - -fmin = minimum diff --git a/cunumeric/config.py b/cunumeric/config.py deleted file mode 100644 index c18d36f4b..000000000 --- a/cunumeric/config.py +++ /dev/null @@ -1,788 +0,0 @@ -# Copyright 2021-2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import os -from abc import abstractmethod -from enum import IntEnum, unique -from typing import TYPE_CHECKING, Union, cast - -import numpy as np -from legate.core import Library, get_legate_runtime - -if TYPE_CHECKING: - import numpy.typing as npt - - from .runtime import Runtime - - -class _CunumericSharedLib: - CUNUMERIC_ADVANCED_INDEXING: int - CUNUMERIC_ARANGE: int - CUNUMERIC_ARGWHERE: int - CUNUMERIC_BATCHED_CHOLESKY: int - CUNUMERIC_BINARY_OP: int - CUNUMERIC_BINARY_RED: int - CUNUMERIC_BINCOUNT: int - CUNUMERIC_BINOP_ADD: int - CUNUMERIC_BINOP_ARCTAN2: int - CUNUMERIC_BINOP_BITWISE_AND: int - CUNUMERIC_BINOP_BITWISE_OR: int - CUNUMERIC_BINOP_BITWISE_XOR: int - CUNUMERIC_BINOP_COPYSIGN: int - CUNUMERIC_BINOP_DIVIDE: int - CUNUMERIC_BINOP_EQUAL: int - CUNUMERIC_BINOP_FLOAT_POWER: int - CUNUMERIC_BINOP_FLOOR_DIVIDE: int - CUNUMERIC_BINOP_FMOD: int - CUNUMERIC_BINOP_GCD: int - CUNUMERIC_BINOP_GREATER: int - CUNUMERIC_BINOP_GREATER_EQUAL: int - CUNUMERIC_BINOP_HYPOT: int - CUNUMERIC_BINOP_ISCLOSE: int - CUNUMERIC_BINOP_LCM: int - CUNUMERIC_BINOP_LDEXP: int - CUNUMERIC_BINOP_LEFT_SHIFT: int - CUNUMERIC_BINOP_LESS: int - CUNUMERIC_BINOP_LESS_EQUAL: int - CUNUMERIC_BINOP_LOGADDEXP2: int - CUNUMERIC_BINOP_LOGADDEXP: int - CUNUMERIC_BINOP_LOGICAL_AND: int - CUNUMERIC_BINOP_LOGICAL_OR: int - CUNUMERIC_BINOP_LOGICAL_XOR: int - CUNUMERIC_BINOP_MAXIMUM: int - CUNUMERIC_BINOP_MINIMUM: int - CUNUMERIC_BINOP_MOD: int - CUNUMERIC_BINOP_MULTIPLY: int - CUNUMERIC_BINOP_NEXTAFTER: int - CUNUMERIC_BINOP_NOT_EQUAL: int - CUNUMERIC_BINOP_POWER: int - CUNUMERIC_BINOP_RIGHT_SHIFT: int - CUNUMERIC_BINOP_SUBTRACT: int - CUNUMERIC_BITGENERATOR: int - CUNUMERIC_BITGENOP_DISTRIBUTION: int - CUNUMERIC_BITGENTYPE_DEFAULT: int - CUNUMERIC_BITGENTYPE_XORWOW: int - CUNUMERIC_BITGENTYPE_MRG32K3A: int - CUNUMERIC_BITGENTYPE_MTGP32: int - CUNUMERIC_BITGENTYPE_MT19937: int - CUNUMERIC_BITGENTYPE_PHILOX4_32_10: int - CUNUMERIC_BITGENDIST_INTEGERS_16: int - CUNUMERIC_BITGENDIST_INTEGERS_32: int - CUNUMERIC_BITGENDIST_INTEGERS_64: int - CUNUMERIC_BITGENDIST_UNIFORM_32: int - CUNUMERIC_BITGENDIST_UNIFORM_64: int - CUNUMERIC_BITGENDIST_LOGNORMAL_32: int - CUNUMERIC_BITGENDIST_LOGNORMAL_64: int - CUNUMERIC_BITGENDIST_NORMAL_32: int - CUNUMERIC_BITGENDIST_NORMAL_64: int - CUNUMERIC_BITGENDIST_POISSON: int - CUNUMERIC_BITGENDIST_EXPONENTIAL_32: int - CUNUMERIC_BITGENDIST_EXPONENTIAL_64: int - CUNUMERIC_BITGENDIST_GUMBEL_32: int - CUNUMERIC_BITGENDIST_GUMBEL_64: int - CUNUMERIC_BITGENDIST_LAPLACE_32: int - CUNUMERIC_BITGENDIST_LAPLACE_64: int - CUNUMERIC_BITGENDIST_LOGISTIC_32: int - CUNUMERIC_BITGENDIST_LOGISTIC_64: int - CUNUMERIC_BITGENDIST_PARETO_32: int - CUNUMERIC_BITGENDIST_PARETO_64: int - CUNUMERIC_BITGENDIST_POWER_32: int - CUNUMERIC_BITGENDIST_POWER_64: int - CUNUMERIC_BITGENDIST_RAYLEIGH_32: int - CUNUMERIC_BITGENDIST_RAYLEIGH_64: int - CUNUMERIC_BITGENDIST_CAUCHY_32: int - CUNUMERIC_BITGENDIST_CAUCHY_64: int - CUNUMERIC_BITGENDIST_TRIANGULAR_32: int - CUNUMERIC_BITGENDIST_TRIANGULAR_64: int - CUNUMERIC_BITGENDIST_WEIBULL_32: int - CUNUMERIC_BITGENDIST_WEIBULL_64: int - CUNUMERIC_BITGENDIST_BYTES: int - CUNUMERIC_BITGENDIST_BETA_32: int - CUNUMERIC_BITGENDIST_BETA_64: int - CUNUMERIC_BITGENDIST_F_32: int - CUNUMERIC_BITGENDIST_F_64: int - CUNUMERIC_BITGENDIST_LOGSERIES: int - CUNUMERIC_BITGENDIST_NONCENTRAL_F_32: int - CUNUMERIC_BITGENDIST_NONCENTRAL_F_64: int - CUNUMERIC_BITGENDIST_CHISQUARE_32: int - CUNUMERIC_BITGENDIST_CHISQUARE_64: int - CUNUMERIC_BITGENDIST_GAMMA_32: int - CUNUMERIC_BITGENDIST_GAMMA_64: int - CUNUMERIC_BITGENDIST_STANDARD_T_32: int - CUNUMERIC_BITGENDIST_STANDARD_T_64: int - CUNUMERIC_BITGENDIST_HYPERGEOMETRIC: int - CUNUMERIC_BITGENDIST_VONMISES_32: int - CUNUMERIC_BITGENDIST_VONMISES_64: int - CUNUMERIC_BITGENDIST_ZIPF: int - CUNUMERIC_BITGENDIST_GEOMETRIC: int - CUNUMERIC_BITGENDIST_WALD_32: int - CUNUMERIC_BITGENDIST_WALD_64: int - CUNUMERIC_BITGENDIST_BINOMIAL: int - CUNUMERIC_BITGENDIST_NEGATIVE_BINOMIAL: int - CUNUMERIC_BITGENOP_CREATE: int - CUNUMERIC_BITGENOP_DESTROY: int - CUNUMERIC_BITGENOP_RAND_RAW: int - CUNUMERIC_BITORDER_BIG: int - CUNUMERIC_BITORDER_LITTLE: int - CUNUMERIC_CHOOSE: int - CUNUMERIC_CONTRACT: int - CUNUMERIC_CONVERT: int - CUNUMERIC_CONVERT_NAN_NOOP: int - CUNUMERIC_CONVERT_NAN_PROD: int - CUNUMERIC_CONVERT_NAN_SUM: int - CUNUMERIC_CONVOLVE: int - CUNUMERIC_DIAG: int - CUNUMERIC_DOT: int - CUNUMERIC_EYE: int - CUNUMERIC_FFT: int - CUNUMERIC_FFT_C2C: int - CUNUMERIC_FFT_C2R: int - CUNUMERIC_FFT_D2Z: int - CUNUMERIC_FFT_FORWARD: int - CUNUMERIC_FFT_INVERSE: int - CUNUMERIC_FFT_R2C: int - CUNUMERIC_FFT_Z2D: int - CUNUMERIC_FFT_Z2Z: int - CUNUMERIC_FILL: int - CUNUMERIC_FLIP: int - CUNUMERIC_GEMM: int - CUNUMERIC_HISTOGRAM: int - CUNUMERIC_LOAD_CUDALIBS: int - CUNUMERIC_MATMUL: int - CUNUMERIC_MATVECMUL: int - CUNUMERIC_MAX_MAPPERS: int - CUNUMERIC_MAX_REDOPS: int - CUNUMERIC_MAX_TASKS: int - CUNUMERIC_NONZERO: int - CUNUMERIC_PACKBITS: int - CUNUMERIC_POTRF: int - CUNUMERIC_PUTMASK: int - CUNUMERIC_RAND: int - CUNUMERIC_READ: int - CUNUMERIC_RED_ALL: int - CUNUMERIC_RED_ANY: int - CUNUMERIC_RED_ARGMAX: int - CUNUMERIC_RED_ARGMIN: int - CUNUMERIC_RED_CONTAINS: int - CUNUMERIC_RED_COUNT_NONZERO: int - CUNUMERIC_RED_MAX: int - CUNUMERIC_RED_MIN: int - CUNUMERIC_RED_NANARGMAX: int - CUNUMERIC_RED_NANARGMIN: int - CUNUMERIC_RED_NANMAX: int - CUNUMERIC_RED_NANMIN: int - CUNUMERIC_RED_NANPROD: int - CUNUMERIC_RED_NANSUM: int - CUNUMERIC_RED_PROD: int - CUNUMERIC_RED_SUM: int - CUNUMERIC_RED_SUM_SQUARES: int - CUNUMERIC_RED_VARIANCE: int - CUNUMERIC_REPEAT: int - CUNUMERIC_SCALAR_UNARY_RED: int - CUNUMERIC_SCAN_GLOBAL: int - CUNUMERIC_SCAN_LOCAL: int - CUNUMERIC_SCAN_PROD: int - CUNUMERIC_SCAN_SUM: int - CUNUMERIC_SEARCHSORTED: int - CUNUMERIC_SELECT: int - CUNUMERIC_SOLVE: int - CUNUMERIC_SORT: int - CUNUMERIC_SYRK: int - CUNUMERIC_TILE: int - CUNUMERIC_TRANSPOSE_COPY_2D: int - CUNUMERIC_TRILU: int - CUNUMERIC_TRSM: int - CUNUMERIC_TUNABLE_MAX_EAGER_VOLUME: int - CUNUMERIC_TUNABLE_NUM_GPUS: int - CUNUMERIC_TUNABLE_NUM_PROCS: int - CUNUMERIC_UNARY_OP: int - CUNUMERIC_UNARY_RED: int - CUNUMERIC_UNIQUE: int - CUNUMERIC_UNIQUE_REDUCE: int - CUNUMERIC_UNLOAD_CUDALIBS: int - CUNUMERIC_UNPACKBITS: int - CUNUMERIC_UOP_ABSOLUTE: int - CUNUMERIC_UOP_ARCCOS: int - CUNUMERIC_UOP_ARCCOSH: int - CUNUMERIC_UOP_ARCSIN: int - CUNUMERIC_UOP_ARCSINH: int - CUNUMERIC_UOP_ARCTAN: int - CUNUMERIC_UOP_ARCTANH: int - CUNUMERIC_UOP_CBRT: int - CUNUMERIC_UOP_CEIL: int - CUNUMERIC_UOP_CLIP: int - CUNUMERIC_UOP_CONJ: int - CUNUMERIC_UOP_COPY: int - CUNUMERIC_UOP_COS: int - CUNUMERIC_UOP_COSH: int - CUNUMERIC_UOP_DEG2RAD: int - CUNUMERIC_UOP_EXP2: int - CUNUMERIC_UOP_EXP: int - CUNUMERIC_UOP_EXPM1: int - CUNUMERIC_UOP_FLOOR: int - CUNUMERIC_UOP_FREXP: int - CUNUMERIC_UOP_GETARG: int - CUNUMERIC_UOP_IMAG: int - CUNUMERIC_UOP_INVERT: int - CUNUMERIC_UOP_ISFINITE: int - CUNUMERIC_UOP_ISINF: int - CUNUMERIC_UOP_ISNAN: int - CUNUMERIC_UOP_LOG10: int - CUNUMERIC_UOP_LOG1P: int - CUNUMERIC_UOP_LOG2: int - CUNUMERIC_UOP_LOG: int - CUNUMERIC_UOP_LOGICAL_NOT: int - CUNUMERIC_UOP_MODF: int - CUNUMERIC_UOP_NEGATIVE: int - CUNUMERIC_UOP_POSITIVE: int - CUNUMERIC_UOP_RAD2DEG: int - CUNUMERIC_UOP_REAL: int - CUNUMERIC_UOP_RECIPROCAL: int - CUNUMERIC_UOP_RINT: int - CUNUMERIC_UOP_SIGN: int - CUNUMERIC_UOP_SIGNBIT: int - CUNUMERIC_UOP_SIN: int - CUNUMERIC_UOP_SINH: int - CUNUMERIC_UOP_SQRT: int - CUNUMERIC_UOP_SQUARE: int - CUNUMERIC_UOP_TAN: int - CUNUMERIC_UOP_TANH: int - CUNUMERIC_UOP_TRUNC: int - CUNUMERIC_WHERE: int - CUNUMERIC_WINDOW: int - CUNUMERIC_WINDOW_BARLETT: int - CUNUMERIC_WINDOW_BLACKMAN: int - CUNUMERIC_WINDOW_HAMMING: int - CUNUMERIC_WINDOW_HANNING: int - CUNUMERIC_WINDOW_KAISER: int - CUNUMERIC_WRAP: int - CUNUMERIC_WRITE: int - CUNUMERIC_ZIP: int - - @abstractmethod - def cunumeric_has_curand(self) -> int: - ... - - @abstractmethod - def cunumeric_register_reduction_op( - self, type_uid: int, elem_type_code: int - ) -> None: - ... - - -# Load the cuNumeric library first so we have a shard object that -# we can use to initialize all these configuration enumerations -class CuNumericLib(Library): - def __init__(self, name: str) -> None: - self.name = name - self.runtime: Union[Runtime, None] = None - self.shared_object: Union[_CunumericSharedLib, None] = None - - def get_name(self) -> str: - return self.name - - def get_shared_library(self) -> str: - from cunumeric.install_info import libpath - - return os.path.join( - libpath, "libcunumeric" + self.get_library_extension() - ) - - def get_c_header(self) -> str: - from cunumeric.install_info import header - - return header - - def get_registration_callback(self) -> str: - return "cunumeric_perform_registration" - - def initialize(self, shared_object: _CunumericSharedLib) -> None: - assert self.runtime is None - self.shared_object = shared_object - - def set_runtime(self, runtime: Runtime) -> None: - assert self.runtime is None - assert self.shared_object is not None - self.runtime = runtime - - def destroy(self) -> None: - if self.runtime is not None: - self.runtime.destroy() - - -CUNUMERIC_LIB_NAME = "cunumeric" -cunumeric_lib = CuNumericLib(CUNUMERIC_LIB_NAME) -cunumeric_context = get_legate_runtime().register_library(cunumeric_lib) -_cunumeric = cast(_CunumericSharedLib, cunumeric_lib.shared_object) - - -# Match these to CuNumericOpCode in cunumeric_c.h -@unique -class CuNumericOpCode(IntEnum): - ADVANCED_INDEXING = _cunumeric.CUNUMERIC_ADVANCED_INDEXING - ARANGE = _cunumeric.CUNUMERIC_ARANGE - ARGWHERE = _cunumeric.CUNUMERIC_ARGWHERE - BATCHED_CHOLESKY = _cunumeric.CUNUMERIC_BATCHED_CHOLESKY - BINARY_OP = _cunumeric.CUNUMERIC_BINARY_OP - BINARY_RED = _cunumeric.CUNUMERIC_BINARY_RED - BINCOUNT = _cunumeric.CUNUMERIC_BINCOUNT - BITGENERATOR = _cunumeric.CUNUMERIC_BITGENERATOR - CHOOSE = _cunumeric.CUNUMERIC_CHOOSE - CONTRACT = _cunumeric.CUNUMERIC_CONTRACT - CONVERT = _cunumeric.CUNUMERIC_CONVERT - CONVOLVE = _cunumeric.CUNUMERIC_CONVOLVE - DIAG = _cunumeric.CUNUMERIC_DIAG - DOT = _cunumeric.CUNUMERIC_DOT - EYE = _cunumeric.CUNUMERIC_EYE - FFT = _cunumeric.CUNUMERIC_FFT - FILL = _cunumeric.CUNUMERIC_FILL - FLIP = _cunumeric.CUNUMERIC_FLIP - GEMM = _cunumeric.CUNUMERIC_GEMM - HISTOGRAM = _cunumeric.CUNUMERIC_HISTOGRAM - LOAD_CUDALIBS = _cunumeric.CUNUMERIC_LOAD_CUDALIBS - MATMUL = _cunumeric.CUNUMERIC_MATMUL - MATVECMUL = _cunumeric.CUNUMERIC_MATVECMUL - NONZERO = _cunumeric.CUNUMERIC_NONZERO - PACKBITS = _cunumeric.CUNUMERIC_PACKBITS - POTRF = _cunumeric.CUNUMERIC_POTRF - PUTMASK = _cunumeric.CUNUMERIC_PUTMASK - RAND = _cunumeric.CUNUMERIC_RAND - READ = _cunumeric.CUNUMERIC_READ - REPEAT = _cunumeric.CUNUMERIC_REPEAT - SCALAR_UNARY_RED = _cunumeric.CUNUMERIC_SCALAR_UNARY_RED - SCAN_GLOBAL = _cunumeric.CUNUMERIC_SCAN_GLOBAL - SCAN_LOCAL = _cunumeric.CUNUMERIC_SCAN_LOCAL - SEARCHSORTED = _cunumeric.CUNUMERIC_SEARCHSORTED - SELECT = _cunumeric.CUNUMERIC_SELECT - SOLVE = _cunumeric.CUNUMERIC_SOLVE - SORT = _cunumeric.CUNUMERIC_SORT - SYRK = _cunumeric.CUNUMERIC_SYRK - TILE = _cunumeric.CUNUMERIC_TILE - TRANSPOSE_COPY_2D = _cunumeric.CUNUMERIC_TRANSPOSE_COPY_2D - TRILU = _cunumeric.CUNUMERIC_TRILU - TRSM = _cunumeric.CUNUMERIC_TRSM - UNARY_OP = _cunumeric.CUNUMERIC_UNARY_OP - UNARY_RED = _cunumeric.CUNUMERIC_UNARY_RED - UNIQUE = _cunumeric.CUNUMERIC_UNIQUE - UNIQUE_REDUCE = _cunumeric.CUNUMERIC_UNIQUE_REDUCE - UNLOAD_CUDALIBS = _cunumeric.CUNUMERIC_UNLOAD_CUDALIBS - UNPACKBITS = _cunumeric.CUNUMERIC_UNPACKBITS - WHERE = _cunumeric.CUNUMERIC_WHERE - WINDOW = _cunumeric.CUNUMERIC_WINDOW - WRAP = _cunumeric.CUNUMERIC_WRAP - WRITE = _cunumeric.CUNUMERIC_WRITE - ZIP = _cunumeric.CUNUMERIC_ZIP - - -# Match these to CuNumericUnaryOpCode in cunumeric_c.h -@unique -class UnaryOpCode(IntEnum): - ABSOLUTE = _cunumeric.CUNUMERIC_UOP_ABSOLUTE - ARCCOS = _cunumeric.CUNUMERIC_UOP_ARCCOS - ARCCOSH = _cunumeric.CUNUMERIC_UOP_ARCCOSH - ARCSIN = _cunumeric.CUNUMERIC_UOP_ARCSIN - ARCSINH = _cunumeric.CUNUMERIC_UOP_ARCSINH - ARCTAN = _cunumeric.CUNUMERIC_UOP_ARCTAN - ARCTANH = _cunumeric.CUNUMERIC_UOP_ARCTANH - CBRT = _cunumeric.CUNUMERIC_UOP_CBRT - CEIL = _cunumeric.CUNUMERIC_UOP_CEIL - CLIP = _cunumeric.CUNUMERIC_UOP_CLIP - CONJ = _cunumeric.CUNUMERIC_UOP_CONJ - COPY = _cunumeric.CUNUMERIC_UOP_COPY - COS = _cunumeric.CUNUMERIC_UOP_COS - COSH = _cunumeric.CUNUMERIC_UOP_COSH - DEG2RAD = _cunumeric.CUNUMERIC_UOP_DEG2RAD - EXP = _cunumeric.CUNUMERIC_UOP_EXP - EXP2 = _cunumeric.CUNUMERIC_UOP_EXP2 - EXPM1 = _cunumeric.CUNUMERIC_UOP_EXPM1 - FLOOR = _cunumeric.CUNUMERIC_UOP_FLOOR - FREXP = _cunumeric.CUNUMERIC_UOP_FREXP - GETARG = _cunumeric.CUNUMERIC_UOP_GETARG - IMAG = _cunumeric.CUNUMERIC_UOP_IMAG - INVERT = _cunumeric.CUNUMERIC_UOP_INVERT - ISFINITE = _cunumeric.CUNUMERIC_UOP_ISFINITE - ISINF = _cunumeric.CUNUMERIC_UOP_ISINF - ISNAN = _cunumeric.CUNUMERIC_UOP_ISNAN - LOG = _cunumeric.CUNUMERIC_UOP_LOG - LOG10 = _cunumeric.CUNUMERIC_UOP_LOG10 - LOG1P = _cunumeric.CUNUMERIC_UOP_LOG1P - LOG2 = _cunumeric.CUNUMERIC_UOP_LOG2 - LOGICAL_NOT = _cunumeric.CUNUMERIC_UOP_LOGICAL_NOT - MODF = _cunumeric.CUNUMERIC_UOP_MODF - NEGATIVE = _cunumeric.CUNUMERIC_UOP_NEGATIVE - POSITIVE = _cunumeric.CUNUMERIC_UOP_POSITIVE - RAD2DEG = _cunumeric.CUNUMERIC_UOP_RAD2DEG - REAL = _cunumeric.CUNUMERIC_UOP_REAL - RECIPROCAL = _cunumeric.CUNUMERIC_UOP_RECIPROCAL - RINT = _cunumeric.CUNUMERIC_UOP_RINT - SIGN = _cunumeric.CUNUMERIC_UOP_SIGN - SIGNBIT = _cunumeric.CUNUMERIC_UOP_SIGNBIT - SIN = _cunumeric.CUNUMERIC_UOP_SIN - SINH = _cunumeric.CUNUMERIC_UOP_SINH - SQRT = _cunumeric.CUNUMERIC_UOP_SQRT - SQUARE = _cunumeric.CUNUMERIC_UOP_SQUARE - TAN = _cunumeric.CUNUMERIC_UOP_TAN - TANH = _cunumeric.CUNUMERIC_UOP_TANH - TRUNC = _cunumeric.CUNUMERIC_UOP_TRUNC - - -# Match these to CuNumericUnaryRedCode in cunumeric_c.h -@unique -class UnaryRedCode(IntEnum): - ALL = _cunumeric.CUNUMERIC_RED_ALL - ANY = _cunumeric.CUNUMERIC_RED_ANY - ARGMAX = _cunumeric.CUNUMERIC_RED_ARGMAX - ARGMIN = _cunumeric.CUNUMERIC_RED_ARGMIN - CONTAINS = _cunumeric.CUNUMERIC_RED_CONTAINS - COUNT_NONZERO = _cunumeric.CUNUMERIC_RED_COUNT_NONZERO - MAX = _cunumeric.CUNUMERIC_RED_MAX - MIN = _cunumeric.CUNUMERIC_RED_MIN - NANARGMAX = _cunumeric.CUNUMERIC_RED_NANARGMAX - NANARGMIN = _cunumeric.CUNUMERIC_RED_NANARGMIN - NANMAX = _cunumeric.CUNUMERIC_RED_NANMAX - NANMIN = _cunumeric.CUNUMERIC_RED_NANMIN - NANPROD = _cunumeric.CUNUMERIC_RED_NANPROD - NANSUM = _cunumeric.CUNUMERIC_RED_NANSUM - PROD = _cunumeric.CUNUMERIC_RED_PROD - SUM = _cunumeric.CUNUMERIC_RED_SUM - SUM_SQUARES = _cunumeric.CUNUMERIC_RED_SUM_SQUARES - VARIANCE = _cunumeric.CUNUMERIC_RED_VARIANCE - - -# Match these to CuNumericBinaryOpCode in cunumeric_c.h -@unique -class BinaryOpCode(IntEnum): - ADD = _cunumeric.CUNUMERIC_BINOP_ADD - ARCTAN2 = _cunumeric.CUNUMERIC_BINOP_ARCTAN2 - BITWISE_AND = _cunumeric.CUNUMERIC_BINOP_BITWISE_AND - BITWISE_OR = _cunumeric.CUNUMERIC_BINOP_BITWISE_OR - BITWISE_XOR = _cunumeric.CUNUMERIC_BINOP_BITWISE_XOR - COPYSIGN = _cunumeric.CUNUMERIC_BINOP_COPYSIGN - DIVIDE = _cunumeric.CUNUMERIC_BINOP_DIVIDE - EQUAL = _cunumeric.CUNUMERIC_BINOP_EQUAL - FLOAT_POWER = _cunumeric.CUNUMERIC_BINOP_FLOAT_POWER - FLOOR_DIVIDE = _cunumeric.CUNUMERIC_BINOP_FLOOR_DIVIDE - FMOD = _cunumeric.CUNUMERIC_BINOP_FMOD - GCD = _cunumeric.CUNUMERIC_BINOP_GCD - GREATER = _cunumeric.CUNUMERIC_BINOP_GREATER - GREATER_EQUAL = _cunumeric.CUNUMERIC_BINOP_GREATER_EQUAL - HYPOT = _cunumeric.CUNUMERIC_BINOP_HYPOT - ISCLOSE = _cunumeric.CUNUMERIC_BINOP_ISCLOSE - LCM = _cunumeric.CUNUMERIC_BINOP_LCM - LDEXP = _cunumeric.CUNUMERIC_BINOP_LDEXP - LEFT_SHIFT = _cunumeric.CUNUMERIC_BINOP_LEFT_SHIFT - LESS = _cunumeric.CUNUMERIC_BINOP_LESS - LESS_EQUAL = _cunumeric.CUNUMERIC_BINOP_LESS_EQUAL - LOGADDEXP = _cunumeric.CUNUMERIC_BINOP_LOGADDEXP - LOGADDEXP2 = _cunumeric.CUNUMERIC_BINOP_LOGADDEXP2 - LOGICAL_AND = _cunumeric.CUNUMERIC_BINOP_LOGICAL_AND - LOGICAL_OR = _cunumeric.CUNUMERIC_BINOP_LOGICAL_OR - LOGICAL_XOR = _cunumeric.CUNUMERIC_BINOP_LOGICAL_XOR - MAXIMUM = _cunumeric.CUNUMERIC_BINOP_MAXIMUM - MINIMUM = _cunumeric.CUNUMERIC_BINOP_MINIMUM - MOD = _cunumeric.CUNUMERIC_BINOP_MOD - MULTIPLY = _cunumeric.CUNUMERIC_BINOP_MULTIPLY - NEXTAFTER = _cunumeric.CUNUMERIC_BINOP_NEXTAFTER - NOT_EQUAL = _cunumeric.CUNUMERIC_BINOP_NOT_EQUAL - POWER = _cunumeric.CUNUMERIC_BINOP_POWER - RIGHT_SHIFT = _cunumeric.CUNUMERIC_BINOP_RIGHT_SHIFT - SUBTRACT = _cunumeric.CUNUMERIC_BINOP_SUBTRACT - - -@unique -class WindowOpCode(IntEnum): - BARLETT = _cunumeric.CUNUMERIC_WINDOW_BARLETT - BLACKMAN = _cunumeric.CUNUMERIC_WINDOW_BLACKMAN - HAMMING = _cunumeric.CUNUMERIC_WINDOW_HAMMING - HANNING = _cunumeric.CUNUMERIC_WINDOW_HANNING - KAISER = _cunumeric.CUNUMERIC_WINDOW_KAISER - - -# Match these to RandGenCode in rand_util.h -@unique -class RandGenCode(IntEnum): - UNIFORM = 1 - NORMAL = 2 - INTEGER = 3 - - -# Match these to CuNumericTunable in cunumeric_c.h -@unique -class CuNumericTunable(IntEnum): - NUM_GPUS = _cunumeric.CUNUMERIC_TUNABLE_NUM_GPUS - NUM_PROCS = _cunumeric.CUNUMERIC_TUNABLE_NUM_PROCS - MAX_EAGER_VOLUME = _cunumeric.CUNUMERIC_TUNABLE_MAX_EAGER_VOLUME - - -# Match these to CuNumericScanCode in cunumeric_c.h -@unique -class ScanCode(IntEnum): - PROD = _cunumeric.CUNUMERIC_SCAN_PROD - SUM = _cunumeric.CUNUMERIC_SCAN_SUM - - -# Match these to CuNumericConvertCode in cunumeric_c.h -@unique -class ConvertCode(IntEnum): - NOOP = _cunumeric.CUNUMERIC_CONVERT_NAN_NOOP - PROD = _cunumeric.CUNUMERIC_CONVERT_NAN_PROD - SUM = _cunumeric.CUNUMERIC_CONVERT_NAN_SUM - - -# Match these to BitGeneratorOperation in cunumeric_c.h -@unique -class BitGeneratorOperation(IntEnum): - CREATE = _cunumeric.CUNUMERIC_BITGENOP_CREATE - DESTROY = _cunumeric.CUNUMERIC_BITGENOP_DESTROY - RAND_RAW = _cunumeric.CUNUMERIC_BITGENOP_RAND_RAW - DISTRIBUTION = _cunumeric.CUNUMERIC_BITGENOP_DISTRIBUTION - - -# Match these to BitGeneratorType in cunumeric_c.h -@unique -class BitGeneratorType(IntEnum): - DEFAULT = _cunumeric.CUNUMERIC_BITGENTYPE_DEFAULT - XORWOW = _cunumeric.CUNUMERIC_BITGENTYPE_XORWOW - MRG32K3A = _cunumeric.CUNUMERIC_BITGENTYPE_MRG32K3A - MTGP32 = _cunumeric.CUNUMERIC_BITGENTYPE_MTGP32 - MT19937 = _cunumeric.CUNUMERIC_BITGENTYPE_MT19937 - PHILOX4_32_10 = _cunumeric.CUNUMERIC_BITGENTYPE_PHILOX4_32_10 - - -# Match these to BitGeneratorDistribution in cunumeric_c.h -@unique -class BitGeneratorDistribution(IntEnum): - INTEGERS_16 = _cunumeric.CUNUMERIC_BITGENDIST_INTEGERS_16 - INTEGERS_32 = _cunumeric.CUNUMERIC_BITGENDIST_INTEGERS_32 - INTEGERS_64 = _cunumeric.CUNUMERIC_BITGENDIST_INTEGERS_64 - UNIFORM_32 = _cunumeric.CUNUMERIC_BITGENDIST_UNIFORM_32 - UNIFORM_64 = _cunumeric.CUNUMERIC_BITGENDIST_UNIFORM_64 - LOGNORMAL_32 = _cunumeric.CUNUMERIC_BITGENDIST_LOGNORMAL_32 - LOGNORMAL_64 = _cunumeric.CUNUMERIC_BITGENDIST_LOGNORMAL_64 - NORMAL_32 = _cunumeric.CUNUMERIC_BITGENDIST_NORMAL_32 - NORMAL_64 = _cunumeric.CUNUMERIC_BITGENDIST_NORMAL_64 - POISSON = _cunumeric.CUNUMERIC_BITGENDIST_POISSON - EXPONENTIAL_32 = _cunumeric.CUNUMERIC_BITGENDIST_EXPONENTIAL_32 - EXPONENTIAL_64 = _cunumeric.CUNUMERIC_BITGENDIST_EXPONENTIAL_64 - GUMBEL_32 = _cunumeric.CUNUMERIC_BITGENDIST_GUMBEL_32 - GUMBEL_64 = _cunumeric.CUNUMERIC_BITGENDIST_GUMBEL_64 - LAPLACE_32 = _cunumeric.CUNUMERIC_BITGENDIST_LAPLACE_32 - LAPLACE_64 = _cunumeric.CUNUMERIC_BITGENDIST_LAPLACE_64 - LOGISTIC_32 = _cunumeric.CUNUMERIC_BITGENDIST_LOGISTIC_32 - LOGISTIC_64 = _cunumeric.CUNUMERIC_BITGENDIST_LOGISTIC_64 - PARETO_32 = _cunumeric.CUNUMERIC_BITGENDIST_PARETO_32 - PARETO_64 = _cunumeric.CUNUMERIC_BITGENDIST_PARETO_64 - POWER_32 = _cunumeric.CUNUMERIC_BITGENDIST_POWER_32 - POWER_64 = _cunumeric.CUNUMERIC_BITGENDIST_POWER_64 - RAYLEIGH_32 = _cunumeric.CUNUMERIC_BITGENDIST_RAYLEIGH_32 - RAYLEIGH_64 = _cunumeric.CUNUMERIC_BITGENDIST_RAYLEIGH_64 - CAUCHY_32 = _cunumeric.CUNUMERIC_BITGENDIST_CAUCHY_32 - CAUCHY_64 = _cunumeric.CUNUMERIC_BITGENDIST_CAUCHY_64 - TRIANGULAR_32 = _cunumeric.CUNUMERIC_BITGENDIST_TRIANGULAR_32 - TRIANGULAR_64 = _cunumeric.CUNUMERIC_BITGENDIST_TRIANGULAR_64 - WEIBULL_32 = _cunumeric.CUNUMERIC_BITGENDIST_WEIBULL_32 - WEIBULL_64 = _cunumeric.CUNUMERIC_BITGENDIST_WEIBULL_64 - BYTES = _cunumeric.CUNUMERIC_BITGENDIST_BYTES - BETA_32 = _cunumeric.CUNUMERIC_BITGENDIST_BETA_32 - BETA_64 = _cunumeric.CUNUMERIC_BITGENDIST_BETA_64 - F_32 = _cunumeric.CUNUMERIC_BITGENDIST_F_32 - F_64 = _cunumeric.CUNUMERIC_BITGENDIST_F_64 - LOGSERIES = _cunumeric.CUNUMERIC_BITGENDIST_LOGSERIES - NONCENTRAL_F_32 = _cunumeric.CUNUMERIC_BITGENDIST_NONCENTRAL_F_32 - NONCENTRAL_F_64 = _cunumeric.CUNUMERIC_BITGENDIST_NONCENTRAL_F_64 - CHISQUARE_32 = _cunumeric.CUNUMERIC_BITGENDIST_CHISQUARE_32 - CHISQUARE_64 = _cunumeric.CUNUMERIC_BITGENDIST_CHISQUARE_64 - GAMMA_32 = _cunumeric.CUNUMERIC_BITGENDIST_GAMMA_32 - GAMMA_64 = _cunumeric.CUNUMERIC_BITGENDIST_GAMMA_64 - STANDARD_T_32 = _cunumeric.CUNUMERIC_BITGENDIST_STANDARD_T_32 - STANDARD_T_64 = _cunumeric.CUNUMERIC_BITGENDIST_STANDARD_T_64 - HYPERGEOMETRIC = _cunumeric.CUNUMERIC_BITGENDIST_HYPERGEOMETRIC - VONMISES_32 = _cunumeric.CUNUMERIC_BITGENDIST_VONMISES_32 - VONMISES_64 = _cunumeric.CUNUMERIC_BITGENDIST_VONMISES_64 - ZIPF = _cunumeric.CUNUMERIC_BITGENDIST_ZIPF - GEOMETRIC = _cunumeric.CUNUMERIC_BITGENDIST_GEOMETRIC - WALD_32 = _cunumeric.CUNUMERIC_BITGENDIST_WALD_32 - WALD_64 = _cunumeric.CUNUMERIC_BITGENDIST_WALD_64 - BINOMIAL = _cunumeric.CUNUMERIC_BITGENDIST_BINOMIAL - NEGATIVE_BINOMIAL = _cunumeric.CUNUMERIC_BITGENDIST_NEGATIVE_BINOMIAL - - -# Match these to fftType in fft_util.h -class FFTType: - def __init__( - self, - name: str, - type_id: int, - input_dtype: npt.DTypeLike, - output_dtype: npt.DTypeLike, - single_precision: bool, - complex_type: Union[FFTType, None] = None, - ) -> None: - self._name = name - self._type_id = type_id - self._complex_type = self if complex_type is None else complex_type - self._input_dtype = input_dtype - self._output_dtype = output_dtype - self._single_precision = single_precision - - def __str__(self) -> str: - return self._name - - def __repr__(self) -> str: - return str(self) - - @property - def type_id(self) -> int: - return self._type_id - - @property - def complex(self) -> FFTType: - return self._complex_type - - @property - def input_dtype(self) -> npt.DTypeLike: - return self._input_dtype - - @property - def output_dtype(self) -> npt.DTypeLike: - return self._output_dtype - - @property - def is_single_precision(self) -> bool: - return self._single_precision - - -FFT_C2C = FFTType( - "C2C", - _cunumeric.CUNUMERIC_FFT_C2C, - np.complex64, - np.complex64, - True, -) - -FFT_Z2Z = FFTType( - "Z2Z", - _cunumeric.CUNUMERIC_FFT_Z2Z, - np.complex128, - np.complex128, - False, -) - -FFT_R2C = FFTType( - "R2C", - _cunumeric.CUNUMERIC_FFT_R2C, - np.float32, - np.complex64, - True, - FFT_C2C, -) - -FFT_C2R = FFTType( - "C2R", - _cunumeric.CUNUMERIC_FFT_C2R, - np.complex64, - np.float32, - True, - FFT_C2C, -) - -FFT_D2Z = FFTType( - "D2Z", - _cunumeric.CUNUMERIC_FFT_D2Z, - np.float64, - np.complex128, - False, - FFT_Z2Z, -) - -FFT_Z2D = FFTType( - "Z2D", - _cunumeric.CUNUMERIC_FFT_Z2D, - np.complex128, - np.float64, - False, - FFT_Z2Z, -) - - -class FFTCode: - @staticmethod - def real_to_complex_code(dtype: npt.DTypeLike) -> FFTType: - if dtype == np.float64: - return FFT_D2Z - elif dtype == np.float32: - return FFT_R2C - else: - raise TypeError( - ( - "Data type for FFT not supported " - "(supported types are float32 and float64)" - ) - ) - - @staticmethod - def complex_to_real_code(dtype: npt.DTypeLike) -> FFTType: - if dtype == np.complex128: - return FFT_Z2D - elif dtype == np.complex64: - return FFT_C2R - else: - raise TypeError( - ( - "Data type for FFT not supported " - "(supported types are complex64 and complex128)" - ) - ) - - -@unique -class FFTDirection(IntEnum): - FORWARD = _cunumeric.CUNUMERIC_FFT_FORWARD - INVERSE = _cunumeric.CUNUMERIC_FFT_INVERSE - - -# Match these to CuNumericBitorder in cunumeric_c.h -@unique -class Bitorder(IntEnum): - BIG = _cunumeric.CUNUMERIC_BITORDER_BIG - LITTLE = _cunumeric.CUNUMERIC_BITORDER_LITTLE - - -@unique -class FFTNormalization(IntEnum): - FORWARD = 1 - INVERSE = 2 - ORTHOGONAL = 3 - - @staticmethod - def from_string(in_string: str) -> Union[FFTNormalization, None]: - if in_string == "forward": - return FFTNormalization.FORWARD - elif in_string == "ortho": - return FFTNormalization.ORTHOGONAL - elif in_string == "backward" or in_string is None: - return FFTNormalization.INVERSE - else: - return None - - @staticmethod - def reverse(in_string: Union[str, None]) -> str: - if in_string == "forward": - return "backward" - elif in_string == "backward" or in_string is None: - return "forward" - else: - return in_string diff --git a/cunumeric/install_info.py.in b/cunumeric/install_info.py.in deleted file mode 100644 index 74f1cf994..000000000 --- a/cunumeric/install_info.py.in +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -# See the LICENSE file for details. -# - -# IMPORTANT: -# * install_info.py is a generated file and should not be modified by hand - -def get_libpath(): - import os, sys, platform - join = os.path.join - exists = os.path.exists - dirname = os.path.dirname - cn_path = dirname(dirname(__file__)) - so_ext = { - "": "", - "Java": ".jar", - "Linux": ".so", - "Darwin": ".dylib", - "Windows": ".dll" - }[platform.system()] - - def find_libcunumeric(libdir): - if exists(join(libdir, f"libcunumeric{so_ext}")): - return libdir - return None - - return ( - find_libcunumeric(join(cn_path, "build", "lib")) or - find_libcunumeric(join(dirname(dirname(dirname(cn_path))), "lib")) or - find_libcunumeric(join(dirname(dirname(sys.executable)), "lib")) or - "" - ) - - -libpath: str = get_libpath() -header: str = """@header@""" diff --git a/cunumeric/linalg/cholesky.py b/cunumeric/linalg/cholesky.py deleted file mode 100644 index 8eccd2944..000000000 --- a/cunumeric/linalg/cholesky.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright 2023 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from typing import TYPE_CHECKING - -from legate.core import Rect, types as ty -from legate.core.shape import Shape -from legate.settings import settings - -from cunumeric.config import CuNumericOpCode - -from .exception import LinAlgError - -if TYPE_CHECKING: - from legate.core.context import Context - from legate.core.store import Store, StorePartition - - from ..deferred import DeferredArray - from ..runtime import Runtime - - -def transpose_copy_single( - context: Context, input: Store, output: Store -) -> None: - task = context.create_auto_task(CuNumericOpCode.TRANSPOSE_COPY_2D) - task.add_output(output) - task.add_input(input) - # Output has the same shape as input, but is mapped - # to a column major instance - - task.add_broadcast(output) - task.add_broadcast(input) - - task.execute() - - -def transpose_copy( - context: Context, - launch_domain: Rect, - p_input: StorePartition, - p_output: StorePartition, -) -> None: - task = context.create_manual_task( - CuNumericOpCode.TRANSPOSE_COPY_2D, - launch_domain=launch_domain, - ) - task.add_output(p_output) - task.add_input(p_input) - # Output has the same shape as input, but is mapped - # to a column major instance - - task.execute() - - -def potrf_single(context: Context, output: Store) -> None: - task = context.create_auto_task(CuNumericOpCode.POTRF) - task.throws_exception(LinAlgError) - task.add_output(output) - task.add_input(output) - task.execute() - - -def potrf(context: Context, p_output: StorePartition, i: int) -> None: - launch_domain = Rect(lo=(i, i), hi=(i + 1, i + 1)) - task = context.create_manual_task( - CuNumericOpCode.POTRF, launch_domain=launch_domain - ) - task.throws_exception(LinAlgError) - task.add_output(p_output) - task.add_input(p_output) - task.execute() - - -def trsm( - context: Context, p_output: StorePartition, i: int, lo: int, hi: int -) -> None: - if lo >= hi: - return - - rhs = p_output.get_child_store(i, i) - lhs = p_output - - launch_domain = Rect(lo=(lo, i), hi=(hi, i + 1)) - task = context.create_manual_task( - CuNumericOpCode.TRSM, launch_domain=launch_domain - ) - task.add_output(lhs) - task.add_input(rhs) - task.add_input(lhs) - task.execute() - - -def syrk(context: Context, p_output: StorePartition, k: int, i: int) -> None: - rhs = p_output.get_child_store(k, i) - lhs = p_output - - launch_domain = Rect(lo=(k, k), hi=(k + 1, k + 1)) - task = context.create_manual_task( - CuNumericOpCode.SYRK, launch_domain=launch_domain - ) - task.add_output(lhs) - task.add_input(rhs) - task.add_input(lhs) - task.execute() - - -def gemm( - context: Context, - p_output: StorePartition, - k: int, - i: int, - lo: int, - hi: int, -) -> None: - if lo >= hi: - return - - rhs2 = p_output.get_child_store(k, i) - lhs = p_output - rhs1 = p_output - - launch_domain = Rect(lo=(lo, k), hi=(hi, k + 1)) - task = context.create_manual_task( - CuNumericOpCode.GEMM, launch_domain=launch_domain - ) - task.add_output(lhs) - task.add_input(rhs1, proj=lambda p: (p[0], i)) - task.add_input(rhs2) - task.add_input(lhs) - task.execute() - - -MIN_CHOLESKY_TILE_SIZE = 2048 -MIN_CHOLESKY_MATRIX_SIZE = 8192 - - -# TODO: We need a better cost model -def choose_color_shape(runtime: Runtime, shape: Shape) -> Shape: - if settings.test(): - num_tiles = runtime.num_procs * 2 - return Shape((num_tiles, num_tiles)) - - extent = shape[0] - # If there's only one processor or the matrix is too small, - # don't even bother to partition it at all - if runtime.num_procs == 1 or extent <= MIN_CHOLESKY_MATRIX_SIZE: - return Shape((1, 1)) - - # If the matrix is big enough to warrant partitioning, - # pick the granularity that the tile size is greater than a threshold - num_tiles = runtime.num_procs - max_num_tiles = runtime.num_procs * 4 - while ( - (extent + num_tiles - 1) // num_tiles > MIN_CHOLESKY_TILE_SIZE - and num_tiles * 2 <= max_num_tiles - ): - num_tiles *= 2 - - return Shape((num_tiles, num_tiles)) - - -def tril_single(context: Context, output: Store) -> None: - task = context.create_auto_task(CuNumericOpCode.TRILU) - task.add_output(output) - task.add_input(output) - task.add_scalar_arg(True, ty.bool_) - task.add_scalar_arg(0, ty.int32) - # Add a fake task argument to indicate that this is for Cholesky - task.add_scalar_arg(True, ty.bool_) - - task.execute() - - -def tril(context: Context, p_output: StorePartition, n: int) -> None: - launch_domain = Rect((n, n)) - task = context.create_manual_task( - CuNumericOpCode.TRILU, launch_domain=launch_domain - ) - - task.add_output(p_output) - task.add_input(p_output) - task.add_scalar_arg(True, ty.bool_) - task.add_scalar_arg(0, ty.int32) - # Add a fake task argument to indicate that this is for Cholesky - task.add_scalar_arg(True, ty.bool_) - - task.execute() - - -def _batched_cholesky(output: DeferredArray, input: DeferredArray) -> None: - # the only feasible implementation for right now is that - # each cholesky submatrix fits on a single proc. We will have - # wildly varying memory available depending on the system. - # Just use a fixed cutoff to provide some sensible warning. - # TODO: find a better way to inform the user dims are too big - context: Context = output.context # type: ignore - task = context.create_auto_task(CuNumericOpCode.BATCHED_CHOLESKY) - task.add_input(input.base) - task.add_output(output.base) - ndim = input.base.ndim - task.add_broadcast(input.base, (ndim - 2, ndim - 1)) - task.add_broadcast(output.base, (ndim - 2, ndim - 1)) - task.add_alignment(input.base, output.base) - task.throws_exception(LinAlgError) - task.execute() - - -def cholesky( - output: DeferredArray, input: DeferredArray, no_tril: bool -) -> None: - runtime = output.runtime - context: Context = output.context - if len(input.base.shape) > 2: - if no_tril: - raise NotImplementedError( - "batched cholesky expects to only " - "produce the lower triangular matrix" - ) - size = input.base.shape[-1] - # Choose 32768 as dimension cutoff for warning - # so that for float64 anything larger than - # 8 GiB produces a warning - if size > 32768: - runtime.warn( - "batched cholesky is only valid" - " when the square submatrices fit" - f" on a single proc, n > {size} may be too large", - category=UserWarning, - ) - return _batched_cholesky(output, input) - - if runtime.num_procs == 1: - transpose_copy_single(context, input.base, output.base) - potrf_single(context, output.base) - if not no_tril: - tril_single(context, output.base) - return - - shape = output.base.shape - initial_color_shape = choose_color_shape(runtime, shape) - tile_shape = (shape + initial_color_shape - 1) // initial_color_shape - color_shape = (shape + tile_shape - 1) // tile_shape - n = color_shape[0] - - p_input = input.base.partition_by_tiling(tile_shape) - p_output = output.base.partition_by_tiling(tile_shape) - transpose_copy(context, Rect(hi=color_shape), p_input, p_output) - - for i in range(n): - potrf(context, p_output, i) - trsm(context, p_output, i, i + 1, n) - for k in range(i + 1, n): - syrk(context, p_output, k, i) - gemm(context, p_output, k, i, k + 1, n) - - if no_tril: - return - - tril(context, p_output, n) diff --git a/cunumeric/linalg/solve.py b/cunumeric/linalg/solve.py deleted file mode 100644 index cec277c4e..000000000 --- a/cunumeric/linalg/solve.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from typing import TYPE_CHECKING, cast - -from cunumeric.config import CuNumericOpCode - -from .cholesky import transpose_copy_single -from .exception import LinAlgError - -if TYPE_CHECKING: - from legate.core.context import Context - from legate.core.store import Store - - from ..deferred import DeferredArray - - -def solve_single(context: Context, a: Store, b: Store) -> None: - task = context.create_auto_task(CuNumericOpCode.SOLVE) - task.throws_exception(LinAlgError) - task.add_input(a) - task.add_input(b) - task.add_output(a) - task.add_output(b) - - task.add_broadcast(a) - task.add_broadcast(b) - - task.execute() - - -def solve(output: DeferredArray, a: DeferredArray, b: DeferredArray) -> None: - from ..deferred import DeferredArray - - runtime = output.runtime - context = output.context - - a_copy = cast( - DeferredArray, - runtime.create_empty_thunk(a.shape, dtype=a.base.type, inputs=(a,)), - ) - transpose_copy_single(context, a.base, a_copy.base) - - if b.ndim > 1: - transpose_copy_single(context, b.base, output.base) - else: - output.copy(b) - - solve_single(context, a_copy.base, output.base) diff --git a/cunumeric/module.py b/cunumeric/module.py deleted file mode 100644 index 0a8132a81..000000000 --- a/cunumeric/module.py +++ /dev/null @@ -1,8284 +0,0 @@ -# Copyright 2021-2023 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -import math -import operator -import re -from collections import Counter -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Iterable, - Literal, - Optional, - Sequence, - Tuple, - Union, - cast, -) - -import numpy as np -import opt_einsum as oe # type: ignore [import] -from numpy.core.multiarray import ( # type: ignore [attr-defined] - normalize_axis_index, -) -from numpy.core.numeric import ( # type: ignore [attr-defined] - normalize_axis_tuple, -) - -from cunumeric.coverage import is_implemented - -from ._ufunc.comparison import maximum, minimum -from ._ufunc.floating import floor, isnan -from ._ufunc.math import add, multiply -from ._unary_red_utils import get_non_nan_unary_red_code -from .array import ( - add_boilerplate, - check_writeable, - convert_to_cunumeric_ndarray, - ndarray, -) -from .config import BinaryOpCode, ScanCode, UnaryRedCode -from .runtime import runtime -from .settings import settings as cunumeric_settings -from .types import NdShape, NdShapeLike, OrderType, SortSide -from .utils import AxesPairLike, inner_modes, matmul_modes, tensordot_modes - -if TYPE_CHECKING: - from typing import Callable - - import numpy.typing as npt - - from ._ufunc.ufunc import CastingKind - from .types import BoundsMode, ConvolveMode, SelectKind, SortType - -_builtin_abs = abs -_builtin_all = all -_builtin_any = any -_builtin_max = max -_builtin_min = min -_builtin_sum = sum -_builtin_range = range - -casting_kinds: tuple[CastingKind, ...] = ( - "no", - "equiv", - "safe", - "same_kind", - "unsafe", -) - -######################### -# Array creation routines -######################### - -# From shape or value - - -def empty(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: - """ - empty(shape, dtype=float) - - Return a new array of given shape and type, without initializing entries. - - Parameters - ---------- - shape : int or tuple[int] - Shape of the empty array. - dtype : data-type, optional - Desired output data-type for the array. Default is `cunumeric.float64`. - - Returns - ------- - out : ndarray - Array of uninitialized (arbitrary) data of the given shape and dtype. - - See Also - -------- - numpy.empty - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return ndarray(shape=shape, dtype=dtype) - - -@add_boilerplate("a") -def empty_like( - a: ndarray, - dtype: Optional[npt.DTypeLike] = None, - shape: Optional[NdShapeLike] = None, -) -> ndarray: - """ - - empty_like(prototype, dtype=None) - - Return a new array with the same shape and type as a given array. - - Parameters - ---------- - prototype : array_like - The shape and data-type of `prototype` define these same attributes - of the returned array. - dtype : data-type, optional - Overrides the data type of the result. - shape : int or tuple[int], optional - Overrides the shape of the result. - - Returns - ------- - out : ndarray - Array of uninitialized (arbitrary) data with the same shape and type as - `prototype`. - - See Also - -------- - numpy.empty_like - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - shape = a.shape if shape is None else shape - if dtype is not None: - dtype = np.dtype(dtype) - else: - dtype = a.dtype - return ndarray(shape, dtype=dtype, inputs=(a,)) - - -def eye( - N: int, - M: Optional[int] = None, - k: int = 0, - dtype: Optional[npt.DTypeLike] = np.float64, -) -> ndarray: - """ - - Return a 2-D array with ones on the diagonal and zeros elsewhere. - - Parameters - ---------- - N : int - Number of rows in the output. - M : int, optional - Number of columns in the output. If None, defaults to `N`. - k : int, optional - Index of the diagonal: 0 (the default) refers to the main diagonal, - a positive value refers to an upper diagonal, and a negative value - to a lower diagonal. - dtype : data-type, optional - Data-type of the returned array. - - Returns - ------- - I : ndarray - An array of shape (N, M) where all elements are equal to zero, except - for the `k`-th diagonal, whose values are equal to one. - - See Also - -------- - numpy.eye - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if dtype is not None: - dtype = np.dtype(dtype) - if M is None: - M = N - k = operator.index(k) - result = ndarray((N, M), dtype) - result._thunk.eye(k) - return result - - -def identity(n: int, dtype: npt.DTypeLike = float) -> ndarray: - """ - - Return the identity array. - - The identity array is a square array with ones on - the main diagonal. - - Parameters - ---------- - n : int - Number of rows (and columns) in `n` x `n` output. - dtype : data-type, optional - Data-type of the output. Defaults to ``float``. - - Returns - ------- - out : ndarray - `n` x `n` array with its main diagonal set to one, and all other - elements 0. - - See Also - -------- - numpy.identity - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return eye(N=n, M=n, dtype=dtype) - - -def ones(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: - """ - - Return a new array of given shape and type, filled with ones. - - Parameters - ---------- - shape : int or tuple[int] - Shape of the new array. - dtype : data-type, optional - The desired data-type for the array. Default is `cunumeric.float64`. - - Returns - ------- - out : ndarray - Array of ones with the given shape and dtype. - - See Also - -------- - numpy.ones - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return full(shape, 1, dtype=dtype) - - -def ones_like( - a: ndarray, - dtype: Optional[npt.DTypeLike] = None, - shape: Optional[NdShapeLike] = None, -) -> ndarray: - """ - - Return an array of ones with the same shape and type as a given array. - - Parameters - ---------- - a : array_like - The shape and data-type of `a` define these same attributes of the - returned array. - dtype : data-type, optional - Overrides the data type of the result. - shape : int or tuple[int], optional - Overrides the shape of the result. - - Returns - ------- - out : ndarray - Array of ones with the same shape and type as `a`. - - See Also - -------- - numpy.ones_like - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - usedtype = a.dtype - if dtype is not None: - usedtype = np.dtype(dtype) - return full_like(a, 1, dtype=usedtype, shape=shape) - - -def zeros(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: - """ - zeros(shape, dtype=float) - - Return a new array of given shape and type, filled with zeros. - - Parameters - ---------- - shape : int or tuple[int] - Shape of the new array. - dtype : data-type, optional - The desired data-type for the array. Default is `cunumeric.float64`. - - Returns - ------- - out : ndarray - Array of zeros with the given shape and dtype. - - See Also - -------- - numpy.zeros - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if dtype is not None: - dtype = np.dtype(dtype) - return full(shape, 0, dtype=dtype) - - -def zeros_like( - a: ndarray, - dtype: Optional[npt.DTypeLike] = None, - shape: Optional[NdShapeLike] = None, -) -> ndarray: - """ - - Return an array of zeros with the same shape and type as a given array. - - Parameters - ---------- - a : array_like - The shape and data-type of `a` define these same attributes of - the returned array. - dtype : data-type, optional - Overrides the data type of the result. - shape : int or tuple[int], optional - Overrides the shape of the result. - - Returns - ------- - out : ndarray - Array of zeros with the same shape and type as `a`. - - See Also - -------- - numpy.zeros_like - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - usedtype = a.dtype - if dtype is not None: - usedtype = np.dtype(dtype) - return full_like(a, 0, dtype=usedtype, shape=shape) - - -def full( - shape: NdShapeLike, - value: Any, - dtype: Optional[npt.DTypeLike] = None, -) -> ndarray: - """ - - Return a new array of given shape and type, filled with `fill_value`. - - Parameters - ---------- - shape : int or tuple[int] - Shape of the new array. - fill_value : scalar - Fill value. - dtype : data-type, optional - The desired data-type for the array The default, None, means - `cunumeric.array(fill_value).dtype`. - - Returns - ------- - out : ndarray - Array of `fill_value` with the given shape and dtype. - - See Also - -------- - numpy.full - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if dtype is None: - val = np.array(value) - else: - dtype = np.dtype(dtype) - val = np.array(value, dtype=dtype) - result = empty(shape, dtype=val.dtype) - result._thunk.fill(val) - return result - - -def full_like( - a: ndarray, - value: Union[int, float], - dtype: Optional[npt.DTypeLike] = None, - shape: Optional[NdShapeLike] = None, -) -> ndarray: - """ - - Return a full array with the same shape and type as a given array. - - Parameters - ---------- - a : array_like - The shape and data-type of `a` define these same attributes of - the returned array. - fill_value : scalar - Fill value. - dtype : data-type, optional - Overrides the data type of the result. - shape : int or tuple[int], optional - Overrides the shape of the result. - - Returns - ------- - out : ndarray - Array of `fill_value` with the same shape and type as `a`. - - See Also - -------- - numpy.full_like - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if dtype is not None: - dtype = np.dtype(dtype) - else: - dtype = a.dtype - result = empty_like(a, dtype=dtype, shape=shape) - val = np.array(value, dtype=result.dtype) - result._thunk.fill(val) - return result - - -# From existing data - - -def array( - obj: Any, - dtype: Optional[np.dtype[Any]] = None, - copy: bool = True, - order: Union[OrderType, Literal["K"]] = "K", - subok: bool = False, - ndmin: int = 0, -) -> ndarray: - """ - array(object, dtype=None, copy=True) - - Create an array. - - Parameters - ---------- - object : array_like - An array, any object exposing the array interface, an object whose - __array__ method returns an array, or any (nested) sequence. - dtype : data-type, optional - The desired data-type for the array. If not given, then the type will - be determined as the minimum type required to hold the objects in the - sequence. - copy : bool, optional - If true (default), then the object is copied. Otherwise, a copy will - only be made if __array__ returns a copy, if obj is a nested sequence, - or if a copy is needed to satisfy any of the other requirements - (`dtype`, `order`, etc.). - order : ``{'K', 'A', 'C', 'F'}``, optional - Specify the memory layout of the array. If object is not an array, the - newly created array will be in C order (row major) unless 'F' is - specified, in which case it will be in Fortran order (column major). - If object is an array the following holds. - - ===== ========= =================================================== - order no copy copy=True - ===== ========= =================================================== - 'K' unchanged F & C order preserved, otherwise most similar order - 'A' unchanged F order if input is F and not C, otherwise C order - 'C' C order C order - 'F' F order F order - ===== ========= =================================================== - - When ``copy=False`` and a copy is made for other reasons, the result is - the same as if ``copy=True``, with some exceptions for 'A', see the - Notes section. The default order is 'K'. - subok : bool, optional - If True, then sub-classes will be passed-through, otherwise - the returned array will be forced to be a base-class array (default). - ndmin : int, optional - Specifies the minimum number of dimensions that the resulting - array should have. Ones will be pre-pended to the shape as - needed to meet this requirement. - - Returns - ------- - out : ndarray - An array object satisfying the specified requirements. - - See Also - -------- - numpy.array - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if not isinstance(obj, ndarray): - thunk = runtime.get_numpy_thunk(obj, share=(not copy), dtype=dtype) - result = ndarray(shape=None, thunk=thunk) - else: - result = obj - if dtype is not None and result.dtype != dtype: - result = result.astype(dtype) - elif copy and obj is result: - result = result.copy() - if result.ndim < ndmin: - shape = (1,) * (ndmin - result.ndim) + result.shape - result = result.reshape(shape) - return result - - -def asarray(a: Any, dtype: Optional[np.dtype[Any]] = None) -> ndarray: - """ - Convert the input to an array. - - Parameters - ---------- - a : array_like - Input data, in any form that can be converted to an array. This - includes lists, lists of tuples, tuples, tuples of tuples, tuples - of lists and ndarrays. - dtype : data-type, optional - By default, the data-type is inferred from the input data. - - Returns - ------- - out : ndarray - Array interpretation of `a`. No copy is performed if the input is - already an ndarray with matching dtype. If `a` is a subclass of - ndarray, a base class ndarray is returned. - - See Also - -------- - numpy.asarray - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if not isinstance(a, ndarray): - thunk = runtime.get_numpy_thunk(a, share=True, dtype=dtype) - writeable = a.flags.writeable if isinstance(a, np.ndarray) else True - array = ndarray(shape=None, thunk=thunk, writeable=writeable) - else: - array = a - if dtype is not None and array.dtype != dtype: - array = array.astype(dtype) - return array - - -@add_boilerplate("a") -def copy(a: ndarray) -> ndarray: - """ - - Return an array copy of the given object. - - Parameters - ---------- - a : array_like - Input data. - - Returns - ------- - arr : ndarray - Array interpretation of `a`. - - See Also - -------- - numpy.copy - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - result = empty_like(a, dtype=a.dtype) - result._thunk.copy(a._thunk, deep=True) - return result - - -# Numerical ranges - - -def arange( - start: Union[int, float] = 0, - stop: Optional[Union[int, float]] = None, - step: Optional[Union[int, float]] = 1, - dtype: Optional[npt.DTypeLike] = None, -) -> ndarray: - """ - arange([start,] stop[, step,], dtype=None) - - Return evenly spaced values within a given interval. - - Values are generated within the half-open interval ``[start, stop)`` - (in other words, the interval including `start` but excluding `stop`). - For integer arguments the function is equivalent to the Python built-in - `range` function, but returns an ndarray rather than a list. - - When using a non-integer step, such as 0.1, the results will often not - be consistent. It is better to use `cunumeric.linspace` for these cases. - - Parameters - ---------- - start : int or float, optional - Start of interval. The interval includes this value. The default - start value is 0. - stop : int or float - End of interval. The interval does not include this value, except - in some cases where `step` is not an integer and floating point - round-off affects the length of `out`. - step : int or float, optional - Spacing between values. For any output `out`, this is the distance - between two adjacent values, ``out[i+1] - out[i]``. The default - step size is 1. If `step` is specified as a position argument, - `start` must also be given. - dtype : data-type - The type of the output array. If `dtype` is not given, infer the data - type from the other input arguments. - - Returns - ------- - arange : ndarray - Array of evenly spaced values. - - For floating point arguments, the length of the result is - ``ceil((stop - start)/step)``. Because of floating point overflow, - this rule may result in the last element of `out` being greater - than `stop`. - - See Also - -------- - numpy.arange - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if stop is None: - stop = start - start = 0 - - if step is None: - step = 1 - - if dtype is None: - dtype = np.result_type(start, stop, step) - else: - dtype = np.dtype(dtype) - - N = math.ceil((stop - start) / step) - result = ndarray((_builtin_max(0, N),), dtype) - result._thunk.arange(start, stop, step) - return result - - -@add_boilerplate("start", "stop") -def linspace( - start: ndarray, - stop: ndarray, - num: int = 50, - endpoint: bool = True, - retstep: bool = False, - dtype: Optional[npt.DTypeLike] = None, - axis: int = 0, -) -> Union[ndarray, tuple[ndarray, Union[float, ndarray]]]: - """ - - Return evenly spaced numbers over a specified interval. - - Returns `num` evenly spaced samples, calculated over the - interval [`start`, `stop`]. - - The endpoint of the interval can optionally be excluded. - - Parameters - ---------- - start : array_like - The starting value of the sequence. - stop : array_like - The end value of the sequence, unless `endpoint` is set to False. - In that case, the sequence consists of all but the last of ``num + 1`` - evenly spaced samples, so that `stop` is excluded. Note that the step - size changes when `endpoint` is False. - num : int, optional - Number of samples to generate. Default is 50. Must be non-negative. - endpoint : bool, optional - If True, `stop` is the last sample. Otherwise, it is not included. - Default is True. - retstep : bool, optional - If True, return (`samples`, `step`), where `step` is the spacing - between samples. - dtype : data-type, optional - The type of the output array. If `dtype` is not given, infer the data - type from the other input arguments. - axis : int, optional - The axis in the result to store the samples. Relevant only if start - or stop are array-like. By default (0), the samples will be along a - new axis inserted at the beginning. Use -1 to get an axis at the end. - - Returns - ------- - samples : ndarray - There are `num` equally spaced samples in the closed interval - ``[start, stop]`` or the half-open interval ``[start, stop)`` - (depending on whether `endpoint` is True or False). - step : float or ndarray, optional - Only returned if `retstep` is True - - Size of spacing between samples. - - See Also - -------- - numpy.linspace - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if num < 0: - raise ValueError("Number of samples, %s, must be non-negative." % num) - div = (num - 1) if endpoint else num - - common_kind = np.result_type(start.dtype, stop.dtype).kind - dt = np.complex128 if common_kind == "c" else np.float64 - if dtype is None: - dtype = dt - - delta = stop - start - y = arange(0, num, dtype=dt) - - out: tuple[Any, ...] # EllipsisType not even in typing_extensions yet - - # Reshape these arrays into dimensions that allow them to broadcast - if delta.ndim > 0: - if axis is None or axis == 0: - # First dimension - y = y.reshape((-1,) + (1,) * delta.ndim) - # Nothing else needs to be reshaped here because - # they should all broadcast correctly with y - if endpoint and num > 1: - out = (-1,) - elif axis == -1 or axis == delta.ndim: - # Last dimension - y = y.reshape((1,) * delta.ndim + (-1,)) - if endpoint and num > 1: - out = (Ellipsis, -1) - # Extend everything else with extra dimensions of 1 at the end - # so that they can broadcast with y - delta = delta.reshape(delta.shape + (1,)) - start = start.reshape(start.shape + (1,)) - elif axis < delta.ndim: - # Somewhere in the middle - y = y.reshape((1,) * axis + (-1,) + (1,) * (delta.ndim - axis)) - # Start array might be smaller than delta because of broadcast - startax = start.ndim - len(delta.shape[axis:]) - start = start.reshape( - start.shape[0:startax] + (1,) + start.shape[startax:] - ) - if endpoint and num > 1: - out = (Ellipsis, -1) + (slice(None, None, None),) * len( - delta.shape[axis:] - ) - delta = delta.reshape( - delta.shape[0:axis] + (1,) + delta.shape[axis:] - ) - else: - raise ValueError( - "axis " - + str(axis) - + " is out of bounds for array of dimension " - + str(delta.ndim + 1) - ) - else: - out = (-1,) - # else delta is a scalar so start must be also - # therefore it will trivially broadcast correctly - - step: Union[float, ndarray] - if div > 0: - step = delta / div - if delta.ndim == 0: - y *= step - else: - y = y * step - else: - # sequences with 0 items or 1 item with endpoint=True (i.e. div <= 0) - # have an undefined step - step = np.NaN - if delta.ndim == 0: - y *= delta - else: - y = y * delta - - y += start.astype(y.dtype, copy=False) - - if endpoint and num > 1: - y[out] = stop.astype(y.dtype, copy=False) - - if np.issubdtype(dtype, np.integer): - floor(y, out=y) - - if retstep: - return y.astype(dtype, copy=False), step - else: - return y.astype(dtype, copy=False) - - -# Building matrices - - -@add_boilerplate("v") -def diag(v: ndarray, k: int = 0) -> ndarray: - """ - - Extract a diagonal or construct a diagonal array. - - See the more detailed documentation for ``cunumeric.diagonal`` if you use - this function to extract a diagonal and wish to write to the resulting - array; whether it returns a copy or a view depends on what version of numpy - you are using. - - Parameters - ---------- - v : array_like - If `v` is a 2-D array, return a copy of its `k`-th diagonal. - If `v` is a 1-D array, return a 2-D array with `v` on the `k`-th - diagonal. - k : int, optional - Diagonal in question. The default is 0. Use `k>0` for diagonals - above the main diagonal, and `k<0` for diagonals below the main - diagonal. - - Returns - ------- - out : ndarray - The extracted diagonal or constructed diagonal array. - - See Also - -------- - numpy.diag - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if v.ndim == 0: - raise ValueError("Input must be 1- or 2-d") - elif v.ndim == 1: - return v.diagonal(offset=k, axis1=0, axis2=1, extract=False) - elif v.ndim == 2: - return v.diagonal(offset=k, axis1=0, axis2=1, extract=True) - else: - raise ValueError("diag requires 1- or 2-D array, use diagonal instead") - - -def tri( - N: int, - M: Optional[int] = None, - k: int = 0, - dtype: npt.DTypeLike = float, - *, - like: Optional[ndarray] = None, -) -> ndarray: - """ - An array with ones at and below the given diagonal and zeros elsewhere. - - Parameters - ---------- - N : int - Number of rows in the array. - M : int, optional - Number of columns in the array. - By default, `M` is taken equal to `N`. - k : int, optional - The sub-diagonal at and below which the array is filled. - `k` = 0 is the main diagonal, while `k` < 0 is below it, - and `k` > 0 is above. The default is 0. - dtype : dtype, optional - Data type of the returned array. The default is float. - like : array_like - Reference object to allow the creation of arrays which are not NumPy - arrays. If an array-like passed in as `like` supports the - `__array_function__` protocol, the result will be defined by it. In - this case it ensures the creation of an array object compatible with - that passed in via this argument. - - Returns - ------- - tri : ndarray of shape (N, M) - Array with its lower triangle filled with ones and zero elsewhere; - in other words ``T[i,j] == 1`` for ``j <= i + k``, 0 otherwise. - - See Also - -------- - numpy.tri - - Notes - ----- - `like` argument is currently not supported - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - # TODO: add support for `like` (see issue #418) - if like is not None: - raise ValueError("like parameter is currently not supported") - - if M is None: - M = N - - out = ones((N, M), dtype=dtype) - return tril(out, k) - - -@add_boilerplate("m") -def trilu(m: ndarray, k: int, lower: bool) -> ndarray: - if m.ndim < 1: - raise TypeError("Array must be at least 1-D") - shape = m.shape if m.ndim >= 2 else m.shape * 2 - result = ndarray(shape, dtype=m.dtype, inputs=(m,)) - result._thunk.trilu(m._thunk, k, lower) - return result - - -def tril(m: ndarray, k: int = 0) -> ndarray: - """ - - Lower triangle of an array. - - Return a copy of an array with elements above the `k`-th diagonal zeroed. - - Parameters - ---------- - m : array_like - Input array of shape (M, N). - k : int, optional - Diagonal above which to zero elements. `k = 0` (the default) is the - main diagonal, `k < 0` is below it and `k > 0` is above. - - Returns - ------- - tril : ndarray - Lower triangle of `m`, of same shape and data-type as `m`. - - See Also - -------- - numpy.tril - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return trilu(m, k, True) - - -def triu(m: ndarray, k: int = 0) -> ndarray: - """ - - Upper triangle of an array. - - Return a copy of a matrix with the elements below the `k`-th diagonal - zeroed. - - Please refer to the documentation for `tril` for further details. - - See Also - -------- - numpy.triu - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return trilu(m, k, False) - - -############################# -# Array manipulation routines -############################# - -# Basic operations - - -@add_boilerplate("a") -def ndim(a: ndarray) -> int: - """ - - Return the number of dimensions of an array. - - Parameters - ---------- - a : array_like - Input array. If it is not already an ndarray, a conversion is - attempted. - - Returns - ------- - number_of_dimensions : int - The number of dimensions in `a`. Scalars are zero-dimensional. - - See Also - -------- - ndarray.ndim : equivalent method - shape : dimensions of array - ndarray.shape : dimensions of array - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return 0 if a is None else a.ndim - - -@add_boilerplate("a") -def shape(a: ndarray) -> NdShape: - """ - - Return the shape of an array. - - Parameters - ---------- - a : array_like - Input array. - - Returns - ------- - shape : tuple[int, ...] - The elements of the shape tuple give the lengths of the - corresponding array dimensions. - - See Also - -------- - numpy.shape - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.shape - - -# Changing array shape - - -@add_boilerplate("a") -def ravel(a: ndarray, order: OrderType = "C") -> ndarray: - """ - Return a contiguous flattened array. - - A 1-D array, containing the elements of the input, is returned. A copy is - made only if needed. - - Parameters - ---------- - a : array_like - Input array. The elements in `a` are read in the order specified by - `order`, and packed as a 1-D array. - order : ``{'C','F', 'A', 'K'}``, optional - The elements of `a` are read using this index order. 'C' means - to index the elements in row-major, C-style order, - with the last axis index changing fastest, back to the first - axis index changing slowest. 'F' means to index the elements - in column-major, Fortran-style order, with the - first index changing fastest, and the last index changing - slowest. Note that the 'C' and 'F' options take no account of - the memory layout of the underlying array, and only refer to - the order of axis indexing. 'A' means to read the elements in - Fortran-like index order if `a` is Fortran *contiguous* in - memory, C-like order otherwise. 'K' means to read the - elements in the order they occur in memory, except for - reversing the data when strides are negative. By default, 'C' - index order is used. - - Returns - ------- - y : array_like - y is an array of the same subtype as `a`, with shape ``(a.size,)``. - Note that matrices are special cased for backward compatibility, if `a` - is a matrix, then y is a 1-D ndarray. - - See Also - -------- - numpy.ravel - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.ravel(order=order) - - -@add_boilerplate("a") -def reshape( - a: ndarray, newshape: NdShapeLike, order: OrderType = "C" -) -> ndarray: - """ - - Gives a new shape to an array without changing its data. - - Parameters - ---------- - a : array_like - Array to be reshaped. - newshape : int or tuple[int] - The new shape should be compatible with the original shape. If - an integer, then the result will be a 1-D array of that length. - One shape dimension can be -1. In this case, the value is - inferred from the length of the array and remaining dimensions. - order : ``{'C', 'F', 'A'}``, optional - Read the elements of `a` using this index order, and place the - elements into the reshaped array using this index order. 'C' - means to read / write the elements using C-like index order, - with the last axis index changing fastest, back to the first - axis index changing slowest. 'F' means to read / write the - elements using Fortran-like index order, with the first index - changing fastest, and the last index changing slowest. Note that - the 'C' and 'F' options take no account of the memory layout of - the underlying array, and only refer to the order of indexing. - 'A' means to read / write the elements in Fortran-like index - order if `a` is Fortran *contiguous* in memory, C-like order - otherwise. - - Returns - ------- - reshaped_array : ndarray - This will be a new view object if possible; otherwise, it will - be a copy. Note there is no guarantee of the *memory layout* (C- or - Fortran- contiguous) of the returned array. - - See Also - -------- - numpy.reshape - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.reshape(newshape, order=order) - - -# Transpose-like operations - - -@add_boilerplate("a") -def swapaxes(a: ndarray, axis1: int, axis2: int) -> ndarray: - """ - - Interchange two axes of an array. - - Parameters - ---------- - a : array_like - Input array. - axis1 : int - First axis. - axis2 : int - Second axis. - - Returns - ------- - a_swapped : ndarray - If `a` is an ndarray, then a view of `a` is returned; otherwise a new - array is created. - - See Also - -------- - numpy.swapaxes - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.swapaxes(axis1, axis2) - - -@add_boilerplate("a") -def transpose(a: ndarray, axes: Optional[list[int]] = None) -> ndarray: - """ - - Permute the dimensions of an array. - - Parameters - ---------- - a : array_like - Input array. - axes : list[int], optional - By default, reverse the dimensions, otherwise permute the axes - according to the values given. - - Returns - ------- - p : ndarray - `a` with its axes permuted. A view is returned whenever - possible. - - See Also - -------- - numpy.transpose - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.transpose(axes=axes) - - -@add_boilerplate("a") -def moveaxis( - a: ndarray, source: Sequence[int], destination: Sequence[int] -) -> ndarray: - """ - Move axes of an array to new positions. - Other axes remain in their original order. - - Parameters - ---------- - a : ndarray - The array whose axes should be reordered. - source : int or Sequence[int] - Original positions of the axes to move. These must be unique. - destination : int or Sequence[int] - Destination positions for each of the original axes. These must also be - unique. - - Returns - ------- - result : ndarray - Array with moved axes. This array is a view of the input array. - - See Also - -------- - numpy.moveaxis - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - source = normalize_axis_tuple(source, a.ndim, "source") - destination = normalize_axis_tuple(destination, a.ndim, "destination") - if len(source) != len(destination): - raise ValueError( - "`source` and `destination` arguments must have the same number " - "of elements" - ) - order = [n for n in range(a.ndim) if n not in source] - for dest, src in sorted(zip(destination, source)): - order.insert(dest, src) - return a.transpose(order) - - -# Changing number of dimensions - - -def _reshape_recur(ndim: int, arr: ndarray) -> tuple[int, ...]: - if arr.ndim < ndim: - cur_shape: tuple[int, ...] = _reshape_recur(ndim - 1, arr) - if ndim == 2: - cur_shape = (1,) + cur_shape - else: - cur_shape = cur_shape + (1,) - else: - cur_shape = arr.shape - return cur_shape - - -def _atleast_nd( - ndim: int, arys: Sequence[ndarray] -) -> Union[list[ndarray], ndarray]: - inputs = list(convert_to_cunumeric_ndarray(arr) for arr in arys) - # 'reshape' change the shape of arrays - # only when arr.shape != _reshape_recur(ndim,arr) - result = list(arr.reshape(_reshape_recur(ndim, arr)) for arr in inputs) - # if the number of arrays in `arys` is 1, - # the return value is a single array - if len(result) == 1: - return result[0] - return result - - -def atleast_1d(*arys: ndarray) -> Union[list[ndarray], ndarray]: - """ - - Convert inputs to arrays with at least one dimension. - Scalar inputs are converted to 1-dimensional arrays, - whilst higher-dimensional inputs are preserved. - - Parameters - ---------- - *arys : array_like - One or more input arrays. - - Returns - ------- - ret : ndarray - An array, or list of arrays, each with a.ndim >= 1. - Copies are made only if necessary. - - See Also - -------- - numpy.atleast_1d - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return _atleast_nd(1, arys) - - -def atleast_2d(*arys: ndarray) -> Union[list[ndarray], ndarray]: - """ - - View inputs as arrays with at least two dimensions. - - Parameters - ---------- - *arys : array_like - One or more array-like sequences. - Non-array inputs are converted to arrays. - Arrays that already have two or more dimensions are preserved. - - Returns - ------- - res, res2, … : ndarray - An array, or list of arrays, each with a.ndim >= 2. - Copies are avoided where possible, and - views with two or more dimensions are returned. - - See Also - -------- - numpy.atleast_2d - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return _atleast_nd(2, arys) - - -def atleast_3d(*arys: ndarray) -> Union[list[ndarray], ndarray]: - """ - - View inputs as arrays with at least three dimensions. - - Parameters - ---------- - *arys : array_like - One or more array-like sequences. - Non-array inputs are converted to arrays. - Arrays that already have three or more dimensions are preserved. - - Returns - ------- - res, res2, … : ndarray - An array, or list of arrays, each with a.ndim >= 3. - Copies are avoided where possible, and - views with three or more dimensions are returned. - For example, a 1-D array of shape (N,) becomes - a view of shape (1, N, 1), and a 2-D array of shape (M, N) - becomes a view of shape (M, N, 1). - - See Also - -------- - numpy.atleast_3d - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return _atleast_nd(3, arys) - - -@add_boilerplate("a") -def squeeze(a: ndarray, axis: Optional[NdShapeLike] = None) -> ndarray: - """ - - Remove single-dimensional entries from the shape of an array. - - Parameters - ---------- - a : array_like - Input data. - axis : None or int or tuple[int], optional - Selects a subset of the single-dimensional entries in the - shape. If an axis is selected with shape entry greater than - one, an error is raised. - - Returns - ------- - squeezed : ndarray - The input array, but with all or a subset of the - dimensions of length 1 removed. This is always `a` itself - or a view into `a`. - - Raises - ------ - ValueError - If `axis` is not None, and an axis being squeezed is not of length 1 - - See Also - -------- - numpy.squeeze - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.squeeze(axis=axis) - - -def broadcast_shapes( - *args: Union[NdShapeLike, Sequence[NdShapeLike]] -) -> NdShape: - """ - - Broadcast the input shapes into a single shape. - - Parameters - ---------- - `*args` : tuples of ints, or ints - The shapes to be broadcast against each other. - - Returns - ------- - tuple : Broadcasted shape. - - See Also - -------- - numpy.broadcast_shapes - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - # TODO: expected "Union[SupportsIndex, Sequence[SupportsIndex]]" - return np.broadcast_shapes(*args) # type: ignore [arg-type] - - -def _broadcast_to( - arr: ndarray, - shape: NdShapeLike, - subok: bool = False, - broadcasted: bool = False, -) -> ndarray: - # create an array object w/ options passed from 'broadcast' routines - arr = array(arr, copy=False, subok=subok) - # 'broadcast_to' returns a read-only view of the original array - out_shape = broadcast_shapes(arr.shape, shape) - if out_shape != shape: - raise ValueError( - f"cannot broadcast an array of shape {arr.shape} to {shape}" - ) - result = ndarray( - shape=out_shape, - thunk=arr._thunk.broadcast_to(out_shape), - writeable=False, - ) - return result - - -@add_boilerplate("arr") -def broadcast_to( - arr: ndarray, shape: NdShapeLike, subok: bool = False -) -> ndarray: - """ - - Broadcast an array to a new shape. - - Parameters - ---------- - arr : array_like - The array to broadcast. - shape : tuple or int - The shape of the desired array. - A single integer i is interpreted as (i,). - subok : bool, optional - This option is ignored by cuNumeric. - - Returns - ------- - broadcast : array - A readonly view on the original array with the given shape. - It is typically not contiguous. - Furthermore, more than one element of a broadcasted array - may refer to a single memory location. - - See Also - -------- - numpy.broadcast_to - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - return _broadcast_to(arr, shape, subok) - - -def _broadcast_arrays( - arrs: list[ndarray], - subok: bool = False, -) -> list[ndarray]: - # create an arry object w/ options passed from 'broadcast' routines - arrays = [array(arr, copy=False, subok=subok) for arr in arrs] - # check if the broadcast can happen in the input list of arrays - shapes = [arr.shape for arr in arrays] - out_shape = broadcast_shapes(*shapes) - # broadcast to the final shape - arrays = [_broadcast_to(arr, out_shape, subok) for arr in arrays] - return arrays - - -def broadcast_arrays( - *args: Sequence[Any], subok: bool = False -) -> list[ndarray]: - """ - - Broadcast any number of arrays against each other. - - Parameters - ---------- - `*args` : array_likes - The arrays to broadcast. - - subok : bool, optional - This option is ignored by cuNumeric - - Returns - ------- - broadcasted : list of arrays - These arrays are views on the original arrays. - They are typically not contiguous. - Furthermore, more than one element of a broadcasted array - may refer to a single memory location. - If you need to write to the arrays, make copies first. - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - arrs = [convert_to_cunumeric_ndarray(arr) for arr in args] - return _broadcast_arrays(arrs, subok=subok) - - -class broadcast: - """Produce an object that broadcasts input parameters against one another. - It has shape and nd properties and may be used as an iterator. - - Parameters - ---------- - `*arrays` : array_likes - The arrays to broadcast. - - Returns - ------- - b: broadcast - Broadcast the input parameters against one another, and return an - object that encapsulates the result. Amongst others, it has shape - and nd properties, and may be used as an iterator. - - """ - - def __init__(self, *arrays: Sequence[Any]) -> None: - arrs = [convert_to_cunumeric_ndarray(arr) for arr in arrays] - broadcasted = _broadcast_arrays(arrs) - self._iters = tuple(arr.flat for arr in broadcasted) - self._index = 0 - self._shape = broadcasted[0].shape - self._size = np.prod(self.shape, dtype=int) - - def __iter__(self) -> broadcast: - self._index = 0 - return self - - def __next__(self) -> Any: - if self._index < self.size: - result = tuple(each[self._index] for each in self._iters) - self._index += 1 - return result - - def reset(self) -> None: - """Reset the broadcasted result's iterator(s).""" - self._index = 0 - - @property - def index(self) -> int: - """current index in broadcasted result""" - return self._index - - @property - def iters(self) -> Tuple[Iterable[Any], ...]: - """tuple of iterators along self’s "components." """ - return self._iters - - @property - def numiter(self) -> int: - """Number of iterators possessed by the broadcasted result.""" - return len(self._iters) - - @property - def nd(self) -> int: - """Number of dimensions of broadcasted result.""" - return self.ndim - - @property - def ndim(self) -> int: - """Number of dimensions of broadcasted result.""" - return len(self.shape) - - @property - def shape(self) -> NdShape: - """Shape of broadcasted result.""" - return self._shape - - @property - def size(self) -> int: - """Total size of broadcasted result.""" - return self._size - - -# Joining arrays - - -class ArrayInfo: - def __init__( - self, ndim: int, shape: NdShape, dtype: np.dtype[Any] - ) -> None: - self.ndim = ndim - self.shape = shape - self.dtype = dtype - - -def convert_to_array_form(indices: Sequence[int]) -> str: - return "".join(f"[{coord}]" for coord in indices) - - -def check_list_depth(arr: Any, prefix: NdShape = (0,)) -> int: - if not isinstance(arr, list): - return 0 - elif len(arr) == 0: - raise ValueError( - f"List at arrays{convert_to_array_form(prefix)} cannot be empty" - ) - - depths = list( - check_list_depth(each, prefix + (idx,)) for idx, each in enumerate(arr) - ) - - if len(set(depths)) != 1: # this should be one - # If we're here elements don't have the same depth - first_depth = depths[0] - for idx, other_depth in enumerate(depths[1:]): - if other_depth != first_depth: - raise ValueError( - "List depths are mismatched. First element was at depth " - f"{first_depth}, but there is an element at" - f" depth {other_depth}, " - f"arrays{convert_to_array_form(prefix+(idx+1,))}" - ) - - return depths[0] + 1 - - -def check_shape_with_axis( - inputs: list[ndarray], - func_name: str, - axis: int, -) -> None: - ndim = inputs[0].ndim - shape = inputs[0].shape - - axis = normalize_axis_index(axis, ndim) - if ndim >= 1: - if _builtin_any( - shape[:axis] != inp.shape[:axis] - or shape[axis + 1 :] != inp.shape[axis + 1 :] - for inp in inputs - ): - raise ValueError( - f"All arguments to {func_name} " - "must have the same " - "dimension size in all dimensions " - "except the target axis" - ) - return - - -def check_shape_dtype_without_axis( - inputs: Sequence[ndarray], - func_name: str, - dtype: Optional[npt.DTypeLike] = None, - casting: CastingKind = "same_kind", -) -> tuple[list[ndarray], ArrayInfo]: - if len(inputs) == 0: - raise ValueError("need at least one array to concatenate") - - inputs = list(convert_to_cunumeric_ndarray(inp) for inp in inputs) - ndim = inputs[0].ndim - shape = inputs[0].shape - - if _builtin_any(ndim != inp.ndim for inp in inputs): - raise ValueError( - f"All arguments to {func_name} " - "must have the same number of dimensions" - ) - - # Cast arrays with the passed arguments (dtype, casting) - if dtype is None: - dtype = np.result_type(*[inp.dtype for inp in inputs]) - else: - dtype = np.dtype(dtype) - - converted = list(inp.astype(dtype, casting=casting) for inp in inputs) - return converted, ArrayInfo(ndim, shape, dtype) - - -def _block_collect_slices( - arr: Union[ndarray, Sequence[ndarray]], cur_depth: int, depth: int -) -> tuple[list[Any], list[tuple[slice, ...]], Sequence[ndarray]]: - # collects slices for each array in `arr` - # the outcome will be slices on every dimension of the output array - # for each array in `arr` - if cur_depth < depth: - sublist_results = list( - _block_collect_slices(each, cur_depth + 1, depth) for each in arr - ) - # 'sublist_results' contains a list of 3-way tuples, - # for arrays, out_shape of the sublist, and slices - arrays, outshape_list, slices = zip(*sublist_results) - max_ndim = _builtin_max( - 1 + (depth - cur_depth), *(len(each) for each in outshape_list) - ) - outshape_list = list( - ((1,) * (max_ndim - len(each)) + tuple(each)) - for each in outshape_list - ) - leading_dim = _builtin_sum( - each[-1 + (cur_depth - depth)] for each in outshape_list - ) - # flatten array lists from sublists into a single list - arrays = list(chain(*arrays)) - # prepares the out_shape of the current list - out_shape = list(outshape_list[0]) - out_shape[-1 + cur_depth - depth] = leading_dim - offset = 0 - updated_slices = [] - # update the dimension in each slice for the current axis - for shape, slice_list in zip(outshape_list, slices): - cur_dim = shape[-1 + cur_depth - depth] - updated_slices.append( - list( - (slice(offset, offset + cur_dim),) + each - for each in slice_list - ) - ) - offset += cur_dim - # flatten lists of slices into a single list - slices = list(chain(*updated_slices)) - else: - arrays = list(convert_to_cunumeric_ndarray(inp) for inp in arr) - common_shape = arrays[0].shape - if len(arr) > 1: - arrays, common_info = check_shape_dtype_without_axis( - arrays, block.__name__ - ) - common_shape = common_info.shape - check_shape_with_axis(arrays, block.__name__, axis=-1) - # the initial slices for each arr on arr.shape[-1] - out_shape, slices, arrays = _collect_outshape_slices( - arrays, common_shape, axis=-1 + len(common_shape) - ) - - return arrays, out_shape, slices - - -def _block_slicing(arrays: Sequence[ndarray], depth: int) -> ndarray: - # collects the final slices of input arrays and assign them at once - arrays, out_shape, slices = _block_collect_slices(arrays, 1, depth) - out_array = ndarray(shape=out_shape, inputs=arrays) - - for dest, inp in zip(slices, arrays): - out_array[(Ellipsis,) + tuple(dest)] = inp - - return out_array - - -def _collect_outshape_slices( - inputs: Sequence[ndarray], common_shape: NdShape, axis: int -) -> tuple[list[Any], list[tuple[slice, ...]], Sequence[ndarray]]: - leading_dim = _builtin_sum(arr.shape[axis] for arr in inputs) - out_shape = list(common_shape) - out_shape[axis] = leading_dim - post_idx = (slice(None),) * len(out_shape[axis + 1 :]) - slices = [] - offset = 0 - # collect slices for arrays in `inputs` - inputs = list(inp for inp in inputs if inp.size > 0) - for inp in inputs: - slices.append((slice(offset, offset + inp.shape[axis]),) + post_idx) - offset += inp.shape[axis] - - return out_shape, slices, inputs - - -def _concatenate( - inputs: Sequence[ndarray], - common_info: ArrayInfo, - axis: int = 0, - out: Optional[ndarray] = None, - dtype: Optional[npt.DTypeLike] = None, - casting: CastingKind = "same_kind", -) -> ndarray: - if axis < 0: - axis += len(common_info.shape) - out_shape, slices, inputs = _collect_outshape_slices( - inputs, common_info.shape, axis - ) - - if out is None: - out_array = ndarray( - shape=out_shape, dtype=common_info.dtype, inputs=inputs - ) - else: - out = convert_to_cunumeric_ndarray(out) - if not isinstance(out, ndarray): - raise TypeError("out should be ndarray") - elif list(out.shape) != out_shape: - raise ValueError( - f"out.shape({out.shape}) is not matched " - f"to the result shape of concatenation ({out_shape})" - ) - out_array = out - - for dest, src in zip(slices, inputs): - out_array[(Ellipsis,) + dest] = src - - return out_array - - -def append( - arr: ndarray, values: ndarray, axis: Optional[int] = None -) -> ndarray: - """ - - Append values to the end of an array. - - Parameters - ---------- - arr : array_like - Values are appended to a copy of this array. - values : array_like - These values are appended to a copy of arr. It must be of the correct - shape (the same shape as arr, excluding axis). If axis is not - specified, values can be any shape and will be flattened before use. - axis : int, optional - The axis along which values are appended. If axis is not given, both - `arr` and `values` are flattened before use. - - Returns - ------- - res : ndarray - A copy of arr with values appended to axis. - - See Also - -------- - numpy.append - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - # Check to see if we can build a new tuple of cuNumeric arrays - inputs = list(convert_to_cunumeric_ndarray(inp) for inp in [arr, values]) - return concatenate(inputs, axis) - - -def block(arrays: Sequence[Any]) -> ndarray: - """ - Assemble an nd-array from nested lists of blocks. - - Blocks in the innermost lists are concatenated (see concatenate) - along the last dimension (-1), then these are concatenated along - the second-last dimension (-2), and so on until the outermost - list is reached. - - Blocks can be of any dimension, but will not be broadcasted using - the normal rules. Instead, leading axes of size 1 are inserted, - to make block.ndim the same for all blocks. This is primarily useful - for working with scalars, and means that code like np.block([v, 1]) - is valid, where v.ndim == 1. - - When the nested list is two levels deep, this allows block matrices - to be constructed from their components. - - Parameters - ---------- - arrays : nested list of array_like or scalars - If passed a single ndarray or scalar (a nested list of depth 0), - this is returned unmodified (and not copied). - - Elements shapes must match along the appropriate axes (without - broadcasting), but leading 1s will be prepended to the shape as - necessary to make the dimensions match. - - Returns - ------- - block_array : ndarray - The array assembled from the given blocks. - The dimensionality of the output is equal to the greatest of: * the - dimensionality of all the inputs * the depth to which the input list - is nested - - Raises - ------ - ValueError - If list depths are mismatched - for instance, [[a, b], c] is - illegal, and should be spelt [[a, b], [c]] - If lists are empty - for instance, [[a, b], []] - - See Also - -------- - numpy.block - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - # arrays should concatenate from innermost subarrays - # the 'arrays' should be balanced tree - # check if the 'arrays' is a balanced tree - depth = check_list_depth(arrays) - - result = _block_slicing(arrays, depth) - return result - - -def concatenate( - inputs: Sequence[ndarray], - axis: Union[int, None] = 0, - out: Optional[ndarray] = None, - dtype: Optional[npt.DTypeLike] = None, - casting: CastingKind = "same_kind", -) -> ndarray: - """ - - concatenate((a1, a2, ...), axis=0, out=None, dtype=None, - casting="same_kind") - - Join a sequence of arrays along an existing axis. - - Parameters - ---------- - a1, a2, ... : Sequence[array_like] - The arrays must have the same shape, except in the dimension - corresponding to `axis` (the first, by default). - axis : int, optional - The axis along which the arrays will be joined. If axis is None, - arrays are flattened before use. Default is 0. - out : ndarray, optional - If provided, the destination to place the result. The shape must be - correct, matching that of what concatenate would have returned if no - out argument were specified. - dtype : str or data-type - If provided, the destination array will have this dtype. Cannot be - provided together with `out`. - casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional - Controls what kind of data casting may occur. Defaults to 'same_kind'. - - Returns - ------- - res : ndarray - The concatenated array. - - See Also - -------- - numpy.concatenate - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if dtype is not None and out is not None: - raise TypeError( - "concatenate() only takes `out` or `dtype` as an argument," - "but both were provided." - ) - - if casting not in casting_kinds: - raise ValueError( - "casting must be one of 'no', 'equiv', " - "'safe', 'same_kind', or 'unsafe'" - ) - - # flatten arrays if axis == None and concatenate arrays on the first axis - if axis is None: - # Reshape arrays in the `array_list` to handle scalars - reshaped = _atleast_nd(1, inputs) - if not isinstance(reshaped, list): - reshaped = [reshaped] - inputs = list(inp.ravel() for inp in reshaped) - axis = 0 - - # Check to see if we can build a new tuple of cuNumeric arrays - cunumeric_inputs, common_info = check_shape_dtype_without_axis( - inputs, concatenate.__name__, dtype, casting - ) - check_shape_with_axis(cunumeric_inputs, concatenate.__name__, axis) - - return _concatenate( - cunumeric_inputs, - common_info, - axis, - out, - dtype, - casting, - ) - - -def stack( - arrays: Sequence[ndarray], axis: int = 0, out: Optional[ndarray] = None -) -> ndarray: - """ - - Join a sequence of arrays along a new axis. - - The ``axis`` parameter specifies the index of the new axis in the - dimensions of the result. For example, if ``axis=0`` it will be the first - dimension and if ``axis=-1`` it will be the last dimension. - - Parameters - ---------- - arrays : Sequence[array_like] - Each array must have the same shape. - - axis : int, optional - The axis in the result array along which the input arrays are stacked. - - out : ndarray, optional - If provided, the destination to place the result. The shape must be - correct, matching that of what stack would have returned if no - out argument were specified. - - Returns - ------- - stacked : ndarray - The stacked array has one more dimension than the input arrays. - - See Also - -------- - numpy.stack - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if type(axis) is not int: - raise TypeError("The target axis should be an integer") - - arrays, common_info = check_shape_dtype_without_axis( - arrays, stack.__name__ - ) - shapes = {inp.shape for inp in arrays} - if len(shapes) != 1: - raise ValueError("all input arrays must have the same shape for stack") - - axis = normalize_axis_index(axis, common_info.ndim + 1) - shape = common_info.shape[:axis] + (1,) + common_info.shape[axis:] - arrays = [arr.reshape(shape) for arr in arrays] - common_info.shape = tuple(shape) - return _concatenate(arrays, common_info, axis, out=out) - - -def vstack(tup: Sequence[ndarray]) -> ndarray: - """ - - Stack arrays in sequence vertically (row wise). - - This is equivalent to concatenation along the first axis after 1-D arrays - of shape `(N,)` have been reshaped to `(1,N)`. Rebuilds arrays divided by - `vsplit`. - - This function makes most sense for arrays with up to 3 dimensions. For - instance, for pixel-data with a height (first axis), width (second axis), - and r/g/b channels (third axis). The functions `concatenate`, `stack` and - `block` provide more general stacking and concatenation operations. - - Parameters - ---------- - tup : Sequence[ndarray] - The arrays must have the same shape along all but the first axis. - 1-D arrays must have the same length. - - Returns - ------- - stacked : ndarray - The array formed by stacking the given arrays, will be at least 2-D. - - See Also - -------- - numpy.vstack - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # Reshape arrays in the `array_list` if needed before concatenation - reshaped = _atleast_nd(2, tup) - if not isinstance(reshaped, list): - reshaped = [reshaped] - tup, common_info = check_shape_dtype_without_axis( - reshaped, vstack.__name__ - ) - check_shape_with_axis(tup, vstack.__name__, 0) - return _concatenate( - tup, - common_info, - axis=0, - dtype=common_info.dtype, - ) - - -def hstack(tup: Sequence[ndarray]) -> ndarray: - """ - - Stack arrays in sequence horizontally (column wise). - - This is equivalent to concatenation along the second axis, except for 1-D - arrays where it concatenates along the first axis. Rebuilds arrays divided - by `hsplit`. - - This function makes most sense for arrays with up to 3 dimensions. For - instance, for pixel-data with a height (first axis), width (second axis), - and r/g/b channels (third axis). The functions `concatenate`, `stack` and - `block` provide more general stacking and concatenation operations. - - Parameters - ---------- - tup : Sequence[ndarray] - The arrays must have the same shape along all but the second axis, - except 1-D arrays which can be any length. - - Returns - ------- - stacked : ndarray - The array formed by stacking the given arrays. - - See Also - -------- - numpy.hstack - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # Reshape arrays in the `array_list` to handle scalars - reshaped = _atleast_nd(1, tup) - if not isinstance(reshaped, list): - reshaped = [reshaped] - - tup, common_info = check_shape_dtype_without_axis( - reshaped, hstack.__name__ - ) - check_shape_with_axis( - tup, hstack.__name__, axis=(0 if common_info.ndim == 1 else 1) - ) - # When ndim == 1, hstack concatenates arrays along the first axis - return _concatenate( - tup, - common_info, - axis=(0 if common_info.ndim == 1 else 1), - dtype=common_info.dtype, - ) - - -def dstack(tup: Sequence[ndarray]) -> ndarray: - """ - - Stack arrays in sequence depth wise (along third axis). - - This is equivalent to concatenation along the third axis after 2-D arrays - of shape `(M,N)` have been reshaped to `(M,N,1)` and 1-D arrays of shape - `(N,)` have been reshaped to `(1,N,1)`. Rebuilds arrays divided by - `dsplit`. - - This function makes most sense for arrays with up to 3 dimensions. For - instance, for pixel-data with a height (first axis), width (second axis), - and r/g/b channels (third axis). The functions `concatenate`, `stack` and - `block` provide more general stacking and concatenation operations. - - Parameters - ---------- - tup : Sequence[ndarray] - The arrays must have the same shape along all but the third axis. - 1-D or 2-D arrays must have the same shape. - - Returns - ------- - stacked : ndarray - The array formed by stacking the given arrays, will be at least 3-D. - - See Also - -------- - numpy.dstack - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # Reshape arrays to (1,N,1) for ndim ==1 or (M,N,1) for ndim == 2: - reshaped = _atleast_nd(3, tup) - if not isinstance(reshaped, list): - reshaped = [reshaped] - tup, common_info = check_shape_dtype_without_axis( - reshaped, dstack.__name__ - ) - check_shape_with_axis(tup, dstack.__name__, 2) - return _concatenate( - tup, - common_info, - axis=2, - dtype=common_info.dtype, - ) - - -def column_stack(tup: Sequence[ndarray]) -> ndarray: - """ - - Stack 1-D arrays as columns into a 2-D array. - - Take a sequence of 1-D arrays and stack them as columns - to make a single 2-D array. 2-D arrays are stacked as-is, - just like with `hstack`. 1-D arrays are turned into 2-D columns - first. - - Parameters - ---------- - tup : Sequence[ndarray] - 1-D or 2-D arrays to stack. All of them must have the same - first dimension. - - Returns - ------- - stacked : ndarray - The 2-D array formed by stacking the given arrays. - - See Also - -------- - numpy.column_stack - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # Reshape arrays in the `array_list` to handle scalars - reshaped = _atleast_nd(1, tup) - if not isinstance(reshaped, list): - reshaped = [reshaped] - - tup, common_info = check_shape_dtype_without_axis( - reshaped, column_stack.__name__ - ) - - if common_info.ndim == 1: - tup = list(inp.reshape((inp.shape[0], 1)) for inp in tup) - common_info.shape = tup[0].shape - check_shape_with_axis(tup, column_stack.__name__, 1) - return _concatenate( - tup, - common_info, - axis=1, - dtype=common_info.dtype, - ) - - -row_stack = vstack - - -# Splitting arrays - - -def split( - a: ndarray, indices: Union[int, ndarray], axis: int = 0 -) -> list[ndarray]: - """ - - Split an array into multiple sub-arrays as views into `ary`. - - Parameters - ---------- - ary : ndarray - Array to be divided into sub-arrays. - indices_or_sections : int or ndarray - If `indices_or_sections` is an integer, N, the array will be divided - into N equal arrays along `axis`. If such a split is not possible, - an error is raised. - - If `indices_or_sections` is a 1-D array of sorted integers, the entries - indicate where along `axis` the array is split. For example, - ``[2, 3]`` would, for ``axis=0``, result in - - - ary[:2] - - ary[2:3] - - ary[3:] - - If an index exceeds the dimension of the array along `axis`, - an empty sub-array is returned correspondingly. - axis : int, optional - The axis along which to split, default is 0. - - Returns - ------- - sub-arrays : list[ndarray] - A list of sub-arrays as views into `ary`. - - Raises - ------ - ValueError - If `indices_or_sections` is given as an integer, but - a split does not result in equal division. - - See Also - -------- - numpy.split - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return array_split(a, indices, axis, equal=True) - - -def array_split( - a: ndarray, - indices: Union[int, tuple[int], ndarray, npt.NDArray[Any]], - axis: int = 0, - equal: bool = False, -) -> list[ndarray]: - """ - - Split an array into multiple sub-arrays. - - Please refer to the ``split`` documentation. The only difference - between these functions is that ``array_split`` allows - `indices_or_sections` to be an integer that does *not* equally - divide the axis. For an array of length l that should be split - into n sections, it returns l % n sub-arrays of size l//n + 1 - and the rest of size l//n. - - See Also - -------- - numpy.array_split - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - array = convert_to_cunumeric_ndarray(a) - split_pts = [] - if axis >= array.ndim: - raise ValueError( - f"array({array.shape}) has less dimensions than axis({axis})" - ) - - if isinstance(indices, int): - if indices <= 0: - raise ValueError("number sections must be larger than 0.") - res = array.shape[axis] % indices - if equal and res != 0: - raise ValueError("array split does not result in an equal divison") - - len_subarr = array.shape[axis] // indices - end_idx = array.shape[axis] - first_idx = len_subarr - - # the requested # of subarray is larger than the size of array - # -> size of 1 subarrays + empty subarrays - if len_subarr == 0: - len_subarr = 1 - first_idx = len_subarr - end_idx = indices - else: - if res != 0: - # The first 'res' groups have len_subarr+1 elements - split_pts = list( - range( - len_subarr + 1, (len_subarr + 1) * res, len_subarr + 1 - ) - ) - first_idx = (len_subarr + 1) * res - split_pts.extend(range(first_idx, end_idx + 1, len_subarr)) - - elif isinstance(indices, (list, tuple)) or ( - isinstance(indices, (ndarray, np.ndarray)) and indices.dtype == int - ): - split_pts = list(indices) - # adding the size of the target dimension. - # This helps create dummy or last subarray correctly - split_pts.append(array.shape[axis]) - - else: - raise ValueError("Integer or array for split should be provided") - - result = [] - start_idx = 0 - end_idx = 0 - out_shape = [] - in_shape: list[Union[int, slice]] = [] - - for i in range(array.ndim): - if i != axis: - in_shape.append(slice(array.shape[i])) - out_shape.append(array.shape[i]) - else: - in_shape.append(1) - out_shape.append(1) - - for pts in split_pts: - if type(pts) is not int: - raise ValueError( - "Split points in the passed `indices` should be integer" - ) - end_idx = pts - # For a split point, which is larger than the dimension for splitting, - # The last non-empty subarray should be copied from - # array[last_elem:array.shape[axis]] - if pts > array.shape[axis]: - end_idx = array.shape[axis] - out_shape[axis] = (end_idx - start_idx) + 1 - in_shape[axis] = slice(start_idx, end_idx) - new_subarray = None - if start_idx < array.shape[axis] and start_idx < end_idx: - new_subarray = array[tuple(in_shape)].view() - else: - out_shape[axis] = 0 - new_subarray = ndarray( - tuple(out_shape), dtype=array.dtype, writeable=array._writeable - ) - result.append(new_subarray) - start_idx = pts - - return result - - -def dsplit(a: ndarray, indices: Union[int, ndarray]) -> list[ndarray]: - """ - - Split array into multiple sub-arrays along the 3rd axis (depth). - - Please refer to the `split` documentation. `dsplit` is equivalent - to `split` with ``axis=2``, the array is always split along the third - axis provided the array dimension is greater than or equal to 3. - - See Also - -------- - numpy.dsplit - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return split(a, indices, axis=2) - - -def hsplit(a: ndarray, indices: Union[int, ndarray]) -> list[ndarray]: - """ - - Split an array into multiple sub-arrays horizontally (column-wise). - - Please refer to the `split` documentation. `hsplit` is equivalent - to `split` with ``axis=1``, the array is always split along the second - axis regardless of the array dimension. - - See Also - -------- - numpy.hsplit - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return split(a, indices, axis=1) - - -def vsplit(a: ndarray, indices: Union[int, ndarray]) -> list[ndarray]: - """ - - Split an array into multiple sub-arrays vertically (row-wise). - - Please refer to the ``split`` documentation. ``vsplit`` is equivalent - to ``split`` with `axis=0` (default), the array is always split along the - first axis regardless of the array dimension. - - See Also - -------- - numpy.vsplit - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return split(a, indices, axis=0) - - -# Tiling arrays - - -@add_boilerplate("A") -def tile( - A: ndarray, reps: Union[int, Sequence[int], npt.NDArray[np.int_]] -) -> ndarray: - """ - Construct an array by repeating A the number of times given by reps. - - If `reps` has length ``d``, the result will have dimension of ``max(d, - A.ndim)``. - - If ``A.ndim < d``, `A` is promoted to be d-dimensional by prepending new - axes. So a shape (3,) array is promoted to (1, 3) for 2-D replication, - or shape (1, 1, 3) for 3-D replication. If this is not the desired - behavior, promote `A` to d-dimensions manually before calling this - function. - - If ``A.ndim > d``, `reps` is promoted to `A`.ndim by pre-pending 1's to it. - Thus for an `A` of shape (2, 3, 4, 5), a `reps` of (2, 2) is treated as - (1, 1, 2, 2). - - Parameters - ---------- - A : array_like - The input array. - reps : 1d array_like - The number of repetitions of `A` along each axis. - - Returns - ------- - c : ndarray - The tiled output array. - - See Also - -------- - numpy.tile - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - computed_reps: tuple[int, ...] - if isinstance(reps, int): - computed_reps = (reps,) - else: - if np.ndim(reps) > 1: - raise TypeError("`reps` must be a 1d sequence") - computed_reps = tuple(reps) - # Figure out the shape of the destination array - out_dims = _builtin_max(A.ndim, len(computed_reps)) - # Prepend ones until the dimensions match - while len(computed_reps) < out_dims: - computed_reps = (1,) + computed_reps - out_shape: NdShape = () - # Prepend dimensions if necessary - for dim in range(out_dims - A.ndim): - out_shape += (computed_reps[dim],) - offset = len(out_shape) - for dim in range(A.ndim): - out_shape += (A.shape[dim] * computed_reps[offset + dim],) - assert len(out_shape) == out_dims - result = ndarray(out_shape, dtype=A.dtype, inputs=(A,)) - result._thunk.tile(A._thunk, computed_reps) - return result - - -def repeat(a: ndarray, repeats: Any, axis: Optional[int] = None) -> ndarray: - """ - Repeat elements of an array. - - Parameters - ---------- - a : array_like - Input array. - repeats : int or ndarray[int] - The number of repetitions for each element. repeats is - broadcasted to fit the shape of the given axis. - axis : int, optional - The axis along which to repeat values. By default, use the - flattened input array, and return a flat output array. - - Returns - ------- - repeated_array : ndarray - Output array which has the same shape as a, except along the - given axis. - - Notes - ----- - Currently, repeat operations supports only 1D arrays - - See Also - -------- - numpy.repeat - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if repeats is None: - raise TypeError( - "int() argument must be a string, a bytes-like object or a number," - " not 'NoneType'" - ) - - if np.ndim(repeats) > 1: - raise ValueError("`repeats` should be scalar or 1D array") - - # axes should be integer type - if axis is not None and not isinstance(axis, int): - raise TypeError("Axis should be of integer type") - - # when array is a scalar - if np.ndim(a) == 0: - if axis is not None and axis != 0 and axis != -1: - raise np.AxisError( - f"axis {axis} is out of bounds for array of dimension 0" - ) - if np.ndim(repeats) == 0: - if not isinstance(repeats, int): - runtime.warn( - "converting repeats to an integer type", - category=UserWarning, - ) - repeats = np.int64(repeats) - return full((repeats,), cast(Union[int, float], a)) - elif np.ndim(repeats) == 1 and len(repeats) == 1: - if not isinstance(repeats, int): - runtime.warn( - "converting repeats to an integer type", - category=UserWarning, - ) - repeats = np.int64(repeats) - return full((repeats[0],), cast(Union[int, float], a)) - else: - raise ValueError( - "`repeat` with a scalar parameter `a` is only " - "implemented for scalar values of the parameter `repeats`." - ) - - # array is an array - array = convert_to_cunumeric_ndarray(a) - if np.ndim(repeats) == 1: - repeats = convert_to_cunumeric_ndarray(repeats) - - # if no axes specified, flatten array - if axis is None: - array = array.ravel() - axis = 0 - - axis_int: int = normalize_axis_index(axis, array.ndim) - - # If repeats is on a zero sized axis_int, then return the array. - if array.shape[axis_int] == 0: - return array.copy() - - if np.ndim(repeats) == 1: - if repeats.shape[0] == 1 and repeats.shape[0] != array.shape[axis_int]: - repeats = repeats[0] - - # repeats is a scalar. - if np.ndim(repeats) == 0: - # repeats is 0 - if repeats == 0: - empty_shape = list(array.shape) - empty_shape[axis_int] = 0 - return ndarray(shape=tuple(empty_shape), dtype=array.dtype) - # repeats should be integer type - if not isinstance(repeats, int): - runtime.warn( - "converting repeats to an integer type", - category=UserWarning, - ) - result = array._thunk.repeat( - repeats=np.int64(repeats), - axis=axis_int, - scalar_repeats=True, - ) - # repeats is an array - else: - # repeats should be integer type - repeats = repeats._warn_and_convert(np.int64) - if repeats.shape[0] != array.shape[axis_int]: - raise ValueError("incorrect shape of repeats array") - result = array._thunk.repeat( - repeats=repeats._thunk, axis=axis_int, scalar_repeats=False - ) - return ndarray(shape=result.shape, thunk=result) - - -# Rearranging elements - - -@add_boilerplate("m") -def flip(m: ndarray, axis: Optional[NdShapeLike] = None) -> ndarray: - """ - Reverse the order of elements in an array along the given axis. - - The shape of the array is preserved, but the elements are reordered. - - Parameters - ---------- - m : array_like - Input array. - axis : None or int or tuple[int], optional - Axis or axes along which to flip over. The default, axis=None, will - flip over all of the axes of the input array. If axis is negative it - counts from the last to the first axis. - - If axis is a tuple of ints, flipping is performed on all of the axes - specified in the tuple. - - Returns - ------- - out : array_like - A new array that is constructed from `m` with the entries of axis - reversed. - - See Also - -------- - numpy.flip - - Availability - -------- - Single GPU, Single CPU - - Notes - ----- - cuNumeric implementation doesn't return a view, it returns a new array - """ - return m.flip(axis=axis) - - -@add_boilerplate("m") -def flipud(m: ndarray) -> ndarray: - """ - Reverse the order of elements along axis 0 (up/down). - - For a 2-D array, this flips the entries in each column in the up/down - direction. Rows are preserved, but appear in a different order than before. - - Parameters - ---------- - m : array_like - Input array. - - Returns - ------- - out : array_like - A new array that is constructed from `m` with rows reversed. - - See Also - -------- - numpy.flipud - - Availability - -------- - Single GPU, Single CPU - - Notes - ----- - cuNumeric implementation doesn't return a view, it returns a new array - """ - if m.ndim < 1: - raise ValueError("Input must be >= 1-d.") - return flip(m, axis=0) - - -@add_boilerplate("m") -def fliplr(m: ndarray) -> ndarray: - """ - Reverse the order of elements along axis 1 (left/right). - - For a 2-D array, this flips the entries in each row in the left/right - direction. Columns are preserved, but appear in a different order than - before. - - Parameters - ---------- - m : array_like - Input array, must be at least 2-D. - - Returns - ------- - f : ndarray - A new array that is constructed from `m` with the columns reversed. - - See Also - -------- - numpy.fliplr - - Availability - -------- - Single GPU, Single CPU - - Notes - ----- - cuNumeric implementation doesn't return a view, it returns a new array - """ - if m.ndim < 2: - raise ValueError("Input must be >= 2-d.") - return flip(m, axis=1) - - -################### -# Binary operations -################### - -# Elementwise bit operations - - -################### -# Indexing routines -################### - -# Generating index arrays - - -@add_boilerplate("arr", "mask", "vals") -def place(arr: ndarray, mask: ndarray, vals: ndarray) -> None: - """ - Change elements of an array based on conditional and input values. - - Parameters - ---------- - arr : array_like - Array to put data into. - mask : array_like - Mask array. Must have the same size as `arr`. - vals : 1-D sequence - Values to put into `arr`. Only the first N elements are used, - where N is the number of True values in mask. If vals is smaller - than N, it will be repeated, and if elements of a are to be masked, - this sequence must be non-empty. - - See Also - -------- - numpy.copyto, numpy.put, numpy.take, numpy.extract - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if arr.size == 0: - return - - check_writeable(arr) - - if mask.size != arr.size: - raise ValueError("arr array and condition array must be of same size") - - if vals.ndim != 1: - raise ValueError("vals array has to be 1-dimensional") - - if mask.shape != arr.shape: - mask_reshape = reshape(mask, arr.shape) - else: - mask_reshape = mask - - num_values = int(count_nonzero(mask_reshape)) - if num_values == 0: - return - - if vals.size == 0: - raise ValueError("vals array cannot be empty") - - if num_values != vals.size: - reps = (num_values + vals.size - 1) // vals.size - vals_resized = tile(A=vals, reps=reps) if reps > 1 else vals - vals_resized = vals_resized[:num_values] - else: - vals_resized = vals - - if mask_reshape.dtype == bool: - arr._thunk.set_item(mask_reshape._thunk, vals_resized._thunk) - else: - bool_mask = mask_reshape.astype(bool) - arr._thunk.set_item(bool_mask._thunk, vals_resized._thunk) - - -@add_boilerplate("condition", "arr") -def extract(condition: ndarray, arr: ndarray) -> ndarray: - """ - - Return the elements of an array that satisfy some condition. - - Parameters - ---------- - condition : array_like - An array whose nonzero or True entries indicate the elements - of `arr` to extract. - arr : array_like - Input array of the same size as `condition`. - - Returns - ------- - result : ndarray - Rank 1 array of values from arr where `condition` is True. - - See Also - -------- - numpy.extract - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if condition.size != arr.size: - raise ValueError("arr array and condition array must be of same size") - - if condition.shape != arr.shape: - condition_reshape = reshape(condition, arr.shape) - else: - condition_reshape = condition - - if condition_reshape.dtype == bool: - thunk = arr._thunk.get_item(condition_reshape._thunk) - else: - bool_condition = condition_reshape.astype(bool) - thunk = arr._thunk.get_item(bool_condition._thunk) - - return ndarray(shape=thunk.shape, thunk=thunk) - - -@add_boilerplate("a") -def nonzero(a: ndarray) -> tuple[ndarray, ...]: - """ - - Return the indices of the elements that are non-zero. - - Returns a tuple of arrays, one for each dimension of `a`, - containing the indices of the non-zero elements in that - dimension. - - Parameters - ---------- - a : array_like - Input array. - - Returns - ------- - tuple_of_arrays : tuple - Indices of elements that are non-zero. - - See Also - -------- - numpy.nonzero - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.nonzero() - - -@add_boilerplate("a") -def flatnonzero(a: ndarray) -> ndarray: - """ - - Return indices that are non-zero in the flattened version of a. - - This is equivalent to `np.nonzero(np.ravel(a))[0]`. - - Parameters - ---------- - a : array_like - Input array. - - Returns - ------- - res : ndarray - Output array, containing the indices of the elements of - `a.ravel()` that are non-zero. - - See Also - -------- - numpy.flatnonzero - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return nonzero(ravel(a))[0] - - -@add_boilerplate("a", "x", "y") -def where( - a: ndarray, x: Optional[ndarray] = None, y: Optional[ndarray] = None -) -> Union[ndarray, tuple[ndarray, ...]]: - """ - where(condition, [x, y]) - - Return elements chosen from `x` or `y` depending on `condition`. - - Parameters - ---------- - condition : array_like, bool - Where True, yield `x`, otherwise yield `y`. - x, y : array_like - Values from which to choose. `x`, `y` and `condition` need to be - broadcastable to some shape. - - Returns - ------- - out : ndarray - An array with elements from `x` where `condition` is True, and elements - from `y` elsewhere. - - See Also - -------- - numpy.where - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if x is None or y is None: - if x is not None or y is not None: - raise ValueError( - "both 'x' and 'y' parameters must be specified together for" - " 'where'" - ) - return nonzero(a) - return ndarray._perform_where(a, x, y) - - -@add_boilerplate("a") -def argwhere(a: ndarray) -> ndarray: - """ - argwhere(a) - - Find the indices of array elements that are non-zero, grouped by element. - - Parameters - ---------- - a : array_like - Input data. - - Returns - ------- - index_array : ndarray - Indices of elements that are non-zero. Indices are grouped by element. - This array will have shape (N, a.ndim) where N is the number of - non-zero items. - - See Also - -------- - numpy.argwhere - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - thunk = a._thunk.argwhere() - return ndarray(shape=thunk.shape, thunk=thunk) - - -# Indexing-like operations -def indices( - dimensions: Sequence[int], dtype: npt.DTypeLike = int, sparse: bool = False -) -> Union[ndarray, tuple[ndarray, ...]]: - """ - Return an array representing the indices of a grid. - Compute an array where the subarrays contain index values 0, 1, ... - varying only along the corresponding axis. - - Parameters - ---------- - dimensions : Sequence[int] - The shape of the grid. - dtype : data-type, optional - Data type of the result. - sparse : bool, optional - Return a sparse representation of the grid instead of a dense - representation. Default is False. - - Returns - ------- - grid : ndarray or Tuple[ndarray, ...] - If sparse is False returns one array of grid indices, - ``grid.shape = (len(dimensions),) + tuple(dimensions)``. - If sparse is True returns a tuple of arrays, with - ``grid[i].shape = (1, ..., 1, dimensions[i], 1, ..., 1)`` with - dimensions[i] in the ith place - - See Also - -------- - numpy.indices - - Notes - ----- - The output shape in the dense case is obtained by prepending the number - of dimensions in front of the tuple of dimensions, i.e. if `dimensions` - is a tuple ``(r0, ..., rN-1)`` of length ``N``, the output shape is - ``(N, r0, ..., rN-1)``. - The subarrays ``grid[k]`` contains the N-D array of indices along the - ``k-th`` axis. Explicitly: - - grid[k, i0, i1, ..., iN-1] = ik - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # implementation of indices routine is adapted from NumPy - dimensions = tuple(dimensions) - N = len(dimensions) - shape = (1,) * N - if sparse: - res_tuple: tuple[ndarray, ...] = () - for i, dim in enumerate(dimensions): - idx = arange(dim, dtype=dtype).reshape( - shape[:i] + (dim,) + shape[i + 1 :] - ) - res_tuple += (idx,) - return res_tuple - else: - out_shape = (N,) + dimensions - res_array: ndarray = empty(out_shape, dtype=dtype) - for i, dim in enumerate(dimensions): - idx = arange(dim, dtype=dtype).reshape( - shape[:i] + (dim,) + shape[i + 1 :] - ) - res_array[i] = idx - return res_array - - -def mask_indices( - n: int, mask_func: Callable[[ndarray, int], ndarray], k: int = 0 -) -> tuple[ndarray, ...]: - """ - Return the indices to access (n, n) arrays, given a masking function. - - Assume `mask_func` is a function that, for a square array a of size - ``(n, n)`` with a possible offset argument `k`, when called as - ``mask_func(a, k)`` returns a new array with zeros in certain locations - (functions like :func:`cunumeric.triu` or :func:`cunumeric.tril` - do precisely this). Then this function returns the indices where - the non-zero values would be located. - - Parameters - ---------- - n : int - The returned indices will be valid to access arrays of shape (n, n). - mask_func : callable - A function whose call signature is similar to that of - :func:`cunumeric.triu`, :func:`cunumeric.tril`. - That is, ``mask_func(x, k)`` returns a boolean array, shaped like `x`. - `k` is an optional argument to the function. - k : scalar - An optional argument which is passed through to `mask_func`. Functions - like :func:`cunumeric.triu`, :func:`cunumeric,tril` - take a second argument that is interpreted as an offset. - - Returns - ------- - indices : tuple of arrays. - The `n` arrays of indices corresponding to the locations where - ``mask_func(np.ones((n, n)), k)`` is True. - - See Also - -------- - numpy.mask_indices - - Notes - ----- - WARNING: `mask_indices` expects `mask_function` to call cuNumeric functions - for good performance. In case non-cuNumeric functions are called by - `mask_function`, cuNumeric will have to materialize all data on the host - which might result in running out of system memory. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # this implementation is based on the Cupy - a = ones((n, n), dtype=bool) - if not is_implemented(mask_func): - runtime.warn( - "Calling non-cuNumeric functions in mask_func can result in bad " - "performance", - category=UserWarning, - ) - return mask_func(a, k).nonzero() - - -def diag_indices(n: int, ndim: int = 2) -> tuple[ndarray, ...]: - """ - Return the indices to access the main diagonal of an array. - - This returns a tuple of indices that can be used to access the main - diagonal of an array a with a.ndim >= 2 dimensions and - shape (n, n, …, n). For a.ndim = 2 this is the usual diagonal, - for a.ndim > 2 this is the set of indices to - access a[i, i, ..., i] for i = [0..n-1]. - - Parameters - ---------- - n : int - The size, along each dimension, of the arrays for which the - returned indices can be used. - ndim : int, optional - The number of dimensions. - - See Also - -------- - numpy.diag_indices - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - idx = arange(n, dtype=int) - return (idx,) * ndim - - -@add_boilerplate("arr") -def diag_indices_from(arr: ndarray) -> tuple[ndarray, ...]: - """ - Return the indices to access the main diagonal of an n-dimensional array. - - See diag_indices for full details. - - Parameters - ---------- - arr : array_like - at least 2-D - - See Also - -------- - numpy.diag_indices_from, numpy.diag_indices - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if not arr.ndim >= 2: - raise ValueError("input array must be at least 2-d") - # For more than d=2, the strided formula is only valid for arrays with - # all dimensions equal, so we check first. - for i in range(1, arr.ndim): - if arr.shape[i] != arr.shape[0]: - raise ValueError("All dimensions of input must be of equal length") - - return diag_indices(arr.shape[0], arr.ndim) - - -def tril_indices( - n: int, k: int = 0, m: Optional[int] = None -) -> tuple[ndarray, ...]: - """ - Return the indices for the lower-triangle of an (n, m) array. - - Parameters - ---------- - n : int - The row dimension of the arrays for which the returned - indices will be valid. - k : int, optional - Diagonal offset (see :func:`cunumeric.tril` for details). - m : int, optional - The column dimension of the arrays for which the returned - indices will be valid. - By default `m` is taken equal to `n`. - - Returns - ------- - inds : tuple of arrays - The indices for the lower-triangle. The returned tuple contains two - arrays, each with the indices along one dimension of the array. - - See also - -------- - numpy.tril_indices - - Notes - ----- - - Availability - ------------ - Multiple GPUs, Multiple CPUs - """ - - tri_ = tri(n, m, k=k, dtype=bool) - return nonzero(tri_) - - -@add_boilerplate("arr") -def tril_indices_from(arr: ndarray, k: int = 0) -> tuple[ndarray, ...]: - """ - Return the indices for the lower-triangle of arr. - - See :func:`cunumeric.tril_indices` for full details. - - Parameters - ---------- - arr : array_like - The indices will be valid for arrays whose dimensions are - the same as arr. - k : int, optional - Diagonal offset (see :func:`cunumeric.tril` for details). - - Returns - ------- - inds : tuple of arrays - The indices for the lower-triangle. The returned tuple contains two - arrays, each with the indices along one dimension of the array. - - See Also - -------- - numpy.tril_indices_from - - Notes - ----- - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - # this implementation is taken from numpy - if arr.ndim != 2: - raise ValueError("input array must be 2-d") - return tril_indices(arr.shape[-2], k=k, m=arr.shape[-1]) - - -def triu_indices( - n: int, k: int = 0, m: Optional[int] = None -) -> tuple[ndarray, ...]: - """ - Return the indices for the upper-triangle of an (n, m) array. - - Parameters - ---------- - n : int - The size of the arrays for which the returned indices will - be valid. - k : int, optional - Diagonal offset (see :func:`cunumeric.triu` for details). - m : int, optional - The column dimension of the arrays for which the returned - arrays will be valid. - By default `m` is taken equal to `n`. - - Returns - ------- - inds : tuple of arrays - The indices for the upper-triangle. The returned tuple contains two - arrays, each with the indices along one dimension of the array. - - See also - -------- - numpy.triu_indices - - Notes - ----- - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - tri_ = ~tri(n, m, k=k - 1, dtype=bool) - return nonzero(tri_) - - -@add_boilerplate("arr") -def triu_indices_from(arr: ndarray, k: int = 0) -> tuple[ndarray, ...]: - """ - Return the indices for the upper-triangle of arr. - - See :func:`cunumeric.triu_indices` for full details. - - Parameters - ---------- - arr : ndarray, shape(N, N) - The indices will be valid for arrays whose dimensions are - the same as arr. - k : int, optional - Diagonal offset (see :func:`cunumeric.triu` for details). - - Returns - ------- - inds : tuple of arrays - The indices for the upper-triangle. The returned tuple contains two - arrays, each with the indices along one dimension of the array. - - See Also - -------- - numpy.triu_indices_from - - Notes - ----- - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - # this implementation is taken from numpy - if arr.ndim != 2: - raise ValueError("input array must be 2-d") - return triu_indices(arr.shape[-2], k=k, m=arr.shape[-1]) - - -@add_boilerplate("a") -def take( - a: ndarray, - indices: ndarray, - axis: Optional[int] = None, - out: Optional[ndarray] = None, - mode: BoundsMode = "raise", -) -> ndarray: - """ - Take elements from an array along an axis. - When axis is not None, this function does the same thing as “fancy” - indexing (indexing arrays using arrays); however, it can be easier - to use if you need elements along a given axis. A call such as - `np.take(arr, indices, axis=3)` is equivalent to `arr[:,:,:,indices,...]`. - - Parameters - ---------- - a : array_like `(Ni…, M, Nk…)` - The source array. - indices : array_like `(Nj…)` - The indices of the values to extract. - Also allow scalars for indices. - axis : int, optional - The axis over which to select values. By default, the flattened input - array is used. - out : ndarray, optional `(Ni…, Nj…, Nk…)` - If provided, the result will be placed in this array. It should be of - the appropriate shape and dtype. - mode : ``{'raise', 'wrap', 'clip'}``, optional - Specifies how out-of-bounds indices will behave. - 'raise' - raise an error (default) - 'wrap' - wrap around - 'clip' - clip to the range - 'clip' mode means that all indices that are too large are replaced by - the index that addresses the last element along that axis. - Note that this disables indexing with negative numbers. - - Returns - ------- - out : ndarray `(Ni…, Nj…, Nk…)` - The returned array has the same type as a. - - Raises - ------ - - See Also - -------- - numpy.take - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.take(indices=indices, axis=axis, out=out, mode=mode) - - -def _fill_fancy_index_for_along_axis_routines( - a_shape: NdShape, axis: int, indices: ndarray -) -> tuple[ndarray, ...]: - # the logic below is base on the cupy implementation of - # the *_along_axis routines - ndim = len(a_shape) - fancy_index = [] - for i, n in enumerate(a_shape): - if i == axis: - fancy_index.append(indices) - else: - ind_shape = (1,) * i + (-1,) + (1,) * (ndim - i - 1) - fancy_index.append(arange(n).reshape(ind_shape)) - return tuple(fancy_index) - - -@add_boilerplate("a", "indices") -def take_along_axis( - a: ndarray, indices: ndarray, axis: Union[int, None] -) -> ndarray: - """ - Take values from the input array by matching 1d index and data slices. - - This iterates over matching 1d slices oriented along the specified axis in - the index and data arrays, and uses the former to look up values in the - latter. These slices can be different lengths. - - Functions returning an index along an axis, like - :func:`cunumeric.argsort` and :func:`cunumeric.argpartition`, - produce suitable indices for this function. - - Parameters - ---------- - arr : ndarray (Ni..., M, Nk...) - Source array - indices : ndarray (Ni..., J, Nk...) - Indices to take along each 1d slice of `arr`. This must match the - dimension of arr, but dimensions Ni and Nj only need to broadcast - against `arr`. - axis : int - The axis to take 1d slices along. If axis is None, the input array is - treated as if it had first been flattened to 1d, for consistency with - :func:`cunumeric.sort` and :func:`cunumeric.argsort`. - - Returns - ------- - out: ndarray (Ni..., J, Nk...) - The indexed result. It is going to be a view to `arr` for most cases, - except the case when `axis=Null` and `arr.ndim>1`. - - See Also - -------- - numpy.take_along_axis - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if not np.issubdtype(indices.dtype, np.integer): - raise TypeError("`indices` must be an integer array") - - computed_axis = 0 - if axis is None: - if indices.ndim != 1: - raise ValueError("indices must be 1D if axis=None") - if a.ndim > 1: - a = a.ravel() - else: - computed_axis = normalize_axis_index(axis, a.ndim) - - if a.ndim != indices.ndim: - raise ValueError( - "`indices` and `a` must have the same number of dimensions" - ) - return a[ - _fill_fancy_index_for_along_axis_routines( - a.shape, computed_axis, indices - ) - ] - - -@add_boilerplate("a", "indices", "values") -def put_along_axis( - a: ndarray, indices: ndarray, values: ndarray, axis: Union[int, None] -) -> None: - """ - Put values into the destination array by matching 1d index and data slices. - - This iterates over matching 1d slices oriented along the specified axis in - the index and data arrays, and uses the former to place values into the - latter. These slices can be different lengths. - - Functions returning an index along an axis, like :func:`cunumeric.argsort` - and :func:`cunumeric.argpartition`, produce suitable indices for - this function. - - Parameters - ---------- - a : ndarray (Ni..., M, Nk...) - Destination array. - indices : ndarray (Ni..., J, Nk...) - Indices to change along each 1d slice of `arr`. This must match the - dimension of arr, but dimensions in Ni and Nj may be 1 to broadcast - against `arr`. - values : array_like (Ni..., J, Nk...) - values to insert at those indices. Its shape and dimension are - broadcast to match that of `indices`. - axis : int - The axis to take 1d slices along. If axis is None, the destination - array is treated as if a flattened 1d view had been created of it. - `axis=None` case is currently supported only for 1D input arrays. - - Note - ---- - Having duplicate entries in `indices` will result in undefined behavior - since operation performs asynchronous update of the `arr` entries. - - See Also - -------- - numpy.put_along_axis - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - - if a.size == 0: - return - - check_writeable(a) - - if not np.issubdtype(indices.dtype, np.integer): - raise TypeError("`indices` must be an integer array") - - computed_axis = 0 - if axis is None: - if indices.ndim != 1: - raise ValueError("indices must be 1D if axis=None") - if a.ndim > 1: - # TODO call a=a.flat when flat is implemented - raise ValueError("a.ndim>1 case is not supported when axis=None") - if (indices.size == 0) or (values.size == 0): - return - if values.shape != indices.shape: - values = values._wrap(indices.size) - else: - computed_axis = normalize_axis_index(axis, a.ndim) - - if a.ndim != indices.ndim: - raise ValueError( - "`indices` and `a` must have the same number of dimensions" - ) - ind = _fill_fancy_index_for_along_axis_routines( - a.shape, computed_axis, indices - ) - a[ind] = values - - -@add_boilerplate("a") -def choose( - a: ndarray, - choices: Sequence[ndarray], - out: Optional[ndarray] = None, - mode: BoundsMode = "raise", -) -> ndarray: - """ - Construct an array from an index array and a list of arrays to choose from. - - Given an "index" array (`a`) of integers and a sequence of ``n`` arrays - (`choices`), `a` and each choice array are first broadcast, as necessary, - to arrays of a common shape; calling these *Ba* and *Bchoices[i], i = - 0,...,n-1* we have that, necessarily, ``Ba.shape == Bchoices[i].shape`` - for each ``i``. Then, a new array with shape ``Ba.shape`` is created as - follows: - - * if ``mode='raise'`` (the default), then, first of all, each element of - ``a`` (and thus ``Ba``) must be in the range ``[0, n-1]``; now, suppose - that ``i`` (in that range) is the value at the ``(j0, j1, ..., jm)`` - position in ``Ba`` - then the value at the same position in the new array - is the value in ``Bchoices[i]`` at that same position; - - * if ``mode='wrap'``, values in `a` (and thus `Ba`) may be any (signed) - integer; modular arithmetic is used to map integers outside the range - `[0, n-1]` back into that range; and then the new array is constructed - as above; - - * if ``mode='clip'``, values in `a` (and thus ``Ba``) may be any (signed) - integer; negative integers are mapped to 0; values greater than ``n-1`` - are mapped to ``n-1``; and then the new array is constructed as above. - - Parameters - ---------- - a : ndarray[int] - This array must contain integers in ``[0, n-1]``, where ``n`` is the - number of choices, unless ``mode=wrap`` or ``mode=clip``, in which - cases any integers are permissible. - choices : Sequence[ndarray] - Choice arrays. `a` and all of the choices must be broadcastable to the - same shape. If `choices` is itself an array (not recommended), then - its outermost dimension (i.e., the one corresponding to - ``choices.shape[0]``) is taken as defining the "sequence". - out : ndarray, optional - If provided, the result will be inserted into this array. It should - be of the appropriate shape and dtype. Note that `out` is always - buffered if ``mode='raise'``; use other modes for better performance. - mode : ``{'raise', 'wrap', 'clip'}``, optional - Specifies how indices outside ``[0, n-1]`` will be treated: - - * 'raise' : an exception is raised (default) - * 'wrap' : value becomes value mod ``n`` - * 'clip' : values < 0 are mapped to 0, values > n-1 are mapped to n-1 - - Returns - ------- - merged_array : ndarray - The merged result. - - Raises - ------ - ValueError: shape mismatch - If `a` and each choice array are not all broadcastable to the same - shape. - - See Also - -------- - numpy.choose - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.choose(choices=choices, out=out, mode=mode) - - -def select( - condlist: Sequence[npt.ArrayLike | ndarray], - choicelist: Sequence[npt.ArrayLike | ndarray], - default: Any = 0, -) -> ndarray: - """ - Return an array drawn from elements in choicelist, depending on conditions. - - Parameters - ---------- - condlist : list of bool ndarrays - The list of conditions which determine from which array in `choicelist` - the output elements are taken. When multiple conditions are satisfied, - the first one encountered in `condlist` is used. - choicelist : list of ndarrays - The list of arrays from which the output elements are taken. It has - to be of the same length as `condlist`. - default : scalar, optional - The element inserted in `output` when all conditions evaluate to False. - - Returns - ------- - output : ndarray - The output at position m is the m-th element of the array in - `choicelist` where the m-th element of the corresponding array in - `condlist` is True. - - See Also - -------- - numpy.select - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if len(condlist) != len(choicelist): - raise ValueError( - "list of cases must be same length as list of conditions" - ) - if len(condlist) == 0: - raise ValueError("select with an empty condition list is not possible") - - condlist_ = tuple(convert_to_cunumeric_ndarray(c) for c in condlist) - for i, c in enumerate(condlist_): - if c.dtype != bool: - raise TypeError( - f"invalid entry {i} in condlist: should be boolean ndarray" - ) - - choicelist_ = tuple(convert_to_cunumeric_ndarray(c) for c in choicelist) - common_type = np.result_type(*choicelist_, default) - args = condlist_ + choicelist_ - choicelist_ = tuple( - c._maybe_convert(common_type, args) for c in choicelist_ - ) - default_ = np.array(default, dtype=common_type) - - out_shape = np.broadcast_shapes( - *(c.shape for c in condlist_), - *(c.shape for c in choicelist_), - ) - out = ndarray(shape=out_shape, dtype=common_type, inputs=args) - out._thunk.select( - tuple(c._thunk for c in condlist_), - tuple(c._thunk for c in choicelist_), - default_, - ) - return out - - -@add_boilerplate("condition", "a") -def compress( - condition: ndarray, - a: ndarray, - axis: Optional[int] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return selected slices of an array along given axis. - - When working along a given axis, a slice along that axis is returned - in output for each index where condition evaluates to True. - When working on a 1-D array, compress is equivalent to numpy.extract. - - Parameters - ---------- - condition, 1-D array of bools - Array that selects which entries to return. If `len(c)` is less than - the size of a along the given axis, then output is truncated to the - length of the condition array. - - a : array_like - Array from which to extract a part. - - axis: int, optional - Axis along which to take slices. If None (default), - work on the flattened array. - - out : ndarray, optional - Output array. Its type is preserved and it must be of the right - shape to hold the output. - - Returns - ------- - compressed_array : ndarray - A copy of `a` without the slices along `axis` for which condition - is false. - - Raises - ------ - ValueError : dimension mismatch - If condition is not 1D array - ValueError : shape mismatch - If condition contains entries that are out of bounds of array - ValueError : shape mismatch - If output array has a wrong shape - - See Also - -------- - numpy.compress, numpy.extract - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - return a.compress(condition, axis=axis, out=out) - - -@add_boilerplate("a") -def diagonal( - a: ndarray, - offset: int = 0, - axis1: int = 0, - axis2: int = 1, - extract: bool = True, -) -> ndarray: - """ - diagonal(a: ndarray, offset=0, axis1=None, axis2=None) - - Return specified diagonals. - - If `a` is 2-D, returns the diagonal of `a` with the given offset, - i.e., the collection of elements of the form ``a[i, i+offset]``. If - `a` has more than two dimensions, then the axes specified by `axis1` - and `axis2` are used to determine the 2-D sub-array whose diagonal is - returned. The shape of the resulting array can be determined by - removing `axis1` and `axis2` and appending an index to the right equal - to the size of the resulting diagonals. - - Parameters - ---------- - a : array_like - Array from which the diagonals are taken. - offset : int, optional - Offset of the diagonal from the main diagonal. Can be positive or - negative. Defaults to main diagonal (0). - axis1 : int, optional - Axis to be used as the first axis of the 2-D sub-arrays from which - the diagonals should be taken. Defaults to first axis (0). - axis2 : int, optional - Axis to be used as the second axis of the 2-D sub-arrays from - which the diagonals should be taken. Defaults to second axis (1). - - Returns - ------- - array_of_diagonals : ndarray - If `a` is 2-D, then a 1-D array containing the diagonal and of the - same type as `a` is returned unless `a` is a `matrix`, in which case - a 1-D array rather than a (2-D) `matrix` is returned in order to - maintain backward compatibility. - - If ``a.ndim > 2``, then the dimensions specified by `axis1` and `axis2` - are removed, and a new axis inserted at the end corresponding to the - diagonal. - - Raises - ------ - ValueError - If the dimension of `a` is less than 2. - - Notes - ----- - Unlike NumPy's, the cuNumeric implementation always returns a copy - - See Also - -------- - numpy.diagonal - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - return a.diagonal(offset=offset, axis1=axis1, axis2=axis2, extract=extract) - - -@add_boilerplate("a", "indices", "values") -def put( - a: ndarray, indices: ndarray, values: ndarray, mode: str = "raise" -) -> None: - """ - Replaces specified elements of an array with given values. - The indexing works as if the target array is first flattened. - - Parameters - ---------- - a : array_like - Array to put data into - indices : array_like - Target indices, interpreted as integers. - WARNING: In case there are repeated entries in the - indices array, Legate doesn't guarantee the order in - which values are updated. - - values : array_like - Values to place in `a` at target indices. If values array is shorter - than indices, it will be repeated as necessary. - mode : {'raise', 'wrap', 'clip'}, optional - Specifies how out-of-bounds indices will behave. - 'raise' : raise an error. - 'wrap' : wrap around. - 'clip' : clip to the range. - - See Also - -------- - numpy.put - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - a.put(indices=indices, values=values, mode=mode) - - -@add_boilerplate("a", "mask", "values") -def putmask(a: ndarray, mask: ndarray, values: ndarray) -> None: - """ - putmask(a, mask, values) - Changes elements of an array based on conditional and input values. - Sets ``a.flat[n] = values[n]`` for each n where ``mask.flat[n]==True``. - If `values` is not the same size as `a` and `mask` then it will repeat. - This gives behavior different from ``a[mask] = values``. - - Parameters - ---------- - a : ndarray - Target array. - mask : array_like - Boolean mask array. It has to be the same shape as `a`. - values : array_like - Values to put into `a` where `mask` is True. If `values` is smaller - than `a` it will be repeated. - - See Also - -------- - numpy.putmask - - Availability - ------------ - Multiple GPUs, Multiple CPUs - """ - if not a.shape == mask.shape: - raise ValueError("mask and data must be the same size") - - check_writeable(a) - - mask = mask._warn_and_convert(np.dtype(bool)) - - if a.dtype != values.dtype: - values = values._warn_and_convert(a.dtype) - - try: - np.broadcast_shapes(values.shape, a.shape) - except ValueError: - values = values._wrap(a.size) - values = values.reshape(a.shape) - - a._thunk.putmask(mask._thunk, values._thunk) - - -@add_boilerplate("a", "val") -def fill_diagonal(a: ndarray, val: ndarray, wrap: bool = False) -> None: - """ - Fill the main diagonal of the given array of any dimensionality. - - For an array a with a.ndim >= 2, the diagonal is the list of locations with - indices a[i, ..., i] all identical. This function modifies the input - array in-place, it does not return a value. - - Parameters - ---------- - - a : array, at least 2-D. - Array whose diagonal is to be filled, it gets modified in-place. - val : scalar or array_like - Value(s) to write on the diagonal. If val is scalar, the value is - written along the diagonal. - If array-like, the flattened val is written along - the diagonal, repeating if necessary to fill all diagonal entries. - wrap : bool - If true, the diagonal "wraps" after N columns, for tall 2d matrices. - - Raises - ------ - ValueError - If the dimension of `a` is less than 2. - - Notes - ----- - - See Also - -------- - numpy.fill_diagonal - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - if val.size == 0 or a.size == 0: - return - - check_writeable(a) - - if a.ndim < 2: - raise ValueError("array must be at least 2-d") - - n = _builtin_min(a.shape) - - if a.ndim > 2: - for s in a.shape: - if s != n: - raise ValueError( - "All dimensions of input must be of equal length" - ) - - len_val = n - - if a.ndim == 2 and wrap and a.shape[0] > a.shape[1]: - len_val = a.shape[0] - (a.shape[0] // (a.shape[1] + 1)) - - if (val.size != len_val and val.ndim > 0) or val.ndim > 1: - val = val._wrap(len_val) - - if a.ndim == 2 and wrap and a.shape[0] > a.shape[1]: - idx0_tmp = arange(a.shape[1], dtype=int) - idx0 = idx0_tmp.copy() - while idx0.size < len_val: - idx0_tmp = idx0_tmp + (a.shape[1] + 1) - idx0 = hstack((idx0, idx0_tmp)) - idx0 = idx0[0:len_val] - idx1 = arange(len_val, dtype=int) % a.shape[1] - a[idx0, idx1] = val - else: - idx = arange(n, dtype=int) - indices = (idx,) * a.ndim - - a[indices] = val - - -################ -# Linear algebra -################ - -# Matrix and vector products - - -@add_boilerplate("a", "b") -def inner(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: - """ - Inner product of two arrays. - - Ordinary inner product of vectors for 1-D arrays (without complex - conjugation), in higher dimensions a sum product over the last axes. - - Parameters - ---------- - a, b : array_like - out : ndarray, optional - Output argument. This must have the exact shape that would be returned - if it was not present. If its dtype is not what would be expected from - this operation, then the result will be (unsafely) cast to `out`. - - Returns - ------- - output : ndarray - If `a` and `b` are both - scalars or both 1-D arrays then a scalar is returned; otherwise - an array is returned. - ``output.shape = (*a.shape[:-1], *b.shape[:-1])`` - If `out` is given, then it is returned. - - Notes - ----- - The cuNumeric implementation is a little more liberal than NumPy in terms - of allowed broadcasting, e.g. ``inner(ones((1,)), ones((4,)))`` is allowed. - - See Also - -------- - numpy.inner - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if a.ndim == 0 or b.ndim == 0: - return multiply(a, b, out=out) - (a_modes, b_modes, out_modes) = inner_modes(a.ndim, b.ndim) - return _contract( - a_modes, - b_modes, - out_modes, - a, - b, - out=out, - casting="unsafe", - ) - - -@add_boilerplate("a", "b") -def dot(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: - """ - Dot product of two arrays. Specifically, - - - If both `a` and `b` are 1-D arrays, it is inner product of vectors - (without complex conjugation). - - - If both `a` and `b` are 2-D arrays, it is matrix multiplication, - but using ``a @ b`` is preferred. - - - If either `a` or `b` is 0-D (scalar), it is equivalent to - :func:`multiply` and using ``cunumeric.multiply(a, b)`` or ``a * b`` is - preferred. - - - If `a` is an N-D array and `b` is a 1-D array, it is a sum product over - the last axis of `a` and `b`. - - - If `a` is an N-D array and `b` is an M-D array (where ``M>=2``), it is a - sum product over the last axis of `a` and the second-to-last axis of - `b`:: - - dot(a: ndarray, b)[i,j,k,m] = sum(a[i,j,:] * b[k,:,m]) - - Parameters - ---------- - a : array_like - First argument. - b : array_like - Second argument. - out : ndarray, optional - Output argument. This must have the exact shape and dtype that would be - returned if it was not present. - - Returns - ------- - output : ndarray - Returns the dot product of `a` and `b`. If `out` is given, then it is - returned. - - Notes - ----- - The cuNumeric implementation is a little more liberal than NumPy in terms - of allowed broadcasting, e.g. ``dot(ones((3,1)), ones((4,5)))`` is allowed. - - Except for the inner-product case, only floating-point types are supported. - - See Also - -------- - numpy.dot - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.dot(b, out=out) - - -@add_boilerplate("a", "b") -def matmul( - a: ndarray, - b: ndarray, - /, - out: Optional[ndarray] = None, - *, - casting: CastingKind = "same_kind", - dtype: Optional[np.dtype[Any]] = None, -) -> ndarray: - """ - Matrix product of two arrays. - - Parameters - ---------- - a, b : array_like - Input arrays, scalars not allowed. - out : ndarray, optional - A location into which the result is stored. If provided, it must have - a shape that matches the signature `(n,k),(k,m)->(n,m)`. - casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional - Controls what kind of data casting may occur. - - * 'no' means the data types should not be cast at all. - * 'equiv' means only byte-order changes are allowed. - * 'safe' means only casts which can preserve values are allowed. - * 'same_kind' means only safe casts or casts within a kind, - like float64 to float32, are allowed. - * 'unsafe' means any data conversions may be done. - - Default is 'same_kind'. - dtype : data-type, optional - If provided, forces the calculation to use the data type specified. - Note that you may have to also give a more liberal `casting` - parameter to allow the conversions. Default is None. - - Returns - ------- - output : ndarray - The matrix product of the inputs. - This is a scalar only when both a, b are 1-d vectors. - If `out` is given, then it is returned. - - Notes - ----- - The behavior depends on the arguments in the following way. - - - If both arguments are 2-D they are multiplied like conventional - matrices. - - If either argument is N-D, N > 2, it is treated as a stack of - matrices residing in the last two indexes and broadcast accordingly. - - If the first argument is 1-D, it is promoted to a matrix by - prepending a 1 to its dimensions. After matrix multiplication - the prepended 1 is removed. - - If the second argument is 1-D, it is promoted to a matrix by - appending a 1 to its dimensions. After matrix multiplication - the appended 1 is removed. - - ``matmul`` differs from ``dot`` in two important ways: - - - Multiplication by scalars is not allowed, use ``*`` instead. - - Stacks of matrices are broadcast together as if the matrices - were elements, respecting the signature ``(n,k),(k,m)->(n,m)``: - - >>> a = ones([9, 5, 7, 4]) - >>> c = ones([9, 5, 4, 3]) - >>> dot(a: ndarray, c).shape - (9, 5, 7, 9, 5, 3) - >>> matmul(a: ndarray, c).shape - (9, 5, 7, 3) - >>> # n is 7, k is 4, m is 3 - - The cuNumeric implementation is a little more liberal than NumPy in terms - of allowed broadcasting, e.g. ``matmul(ones((3,1)), ones((4,5)))`` is - allowed. - - Only floating-point types are supported. - - See Also - -------- - numpy.matmul - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if a.ndim == 0 or b.ndim == 0: - raise ValueError("Scalars not allowed in matmul") - - (a_modes, b_modes, out_modes) = matmul_modes(a.ndim, b.ndim) - - return _contract( - a_modes, - b_modes, - out_modes, - a, - b, - out=out, - casting=casting, - dtype=dtype, - ) - - -@add_boilerplate("a", "b") -def vdot(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: - """ - Return the dot product of two vectors. - - The vdot(`a`, `b`) function handles complex numbers differently than - dot(`a`, `b`). If the first argument is complex the complex conjugate - of the first argument is used for the calculation of the dot product. - - Note that `vdot` handles multidimensional arrays differently than `dot`: - it does *not* perform a matrix product, but flattens input arguments - to 1-D vectors first. Consequently, it should only be used for vectors. - - Parameters - ---------- - a : array_like - If `a` is complex the complex conjugate is taken before calculation - of the dot product. - b : array_like - Second argument to the dot product. - out : ndarray, optional - Output argument. This must have the exact shape that would be returned - if it was not present. If its dtype is not what would be expected from - this operation, then the result will be (unsafely) cast to `out`. - - Returns - ------- - output : ndarray - Dot product of `a` and `b`. If `out` is given, then it is returned. - - Notes - ----- - The cuNumeric implementation is a little more liberal than NumPy in terms - of allowed broadcasting, e.g. ``vdot(ones((1,)), ones((4,)))`` is allowed. - - See Also - -------- - numpy.vdot - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return inner(a.ravel().conj(), b.ravel(), out=out) - - -@add_boilerplate("a", "b") -def outer(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: - """ - Compute the outer product of two vectors. - - Given two vectors, ``a = [a0, a1, ..., aM]`` and ``b = [b0, b1, ..., bN]``, - the outer product is:: - - [[a0*b0 a0*b1 ... a0*bN ] - [a1*b0 . - [ ... . - [aM*b0 aM*bN ]] - - Parameters - ---------- - a : (M,) array_like - First input vector. Input is flattened if not already 1-dimensional. - b : (N,) array_like - Second input vector. Input is flattened if not already 1-dimensional. - out : (M, N) ndarray, optional - A location where the result is stored. If its dtype is not what would - be expected from this operation, then the result will be (unsafely) - cast to `out`. - - Returns - ------- - output : (M, N) ndarray - ``output[i, j] = a[i] * b[j]`` - If `out` is given, then it is returned. - - See Also - -------- - numpy.outer - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return multiply( - a.ravel()[:, np.newaxis], b.ravel()[np.newaxis, :], out=out - ) - - -@add_boilerplate("a", "b") -def tensordot( - a: ndarray, - b: ndarray, - axes: AxesPairLike = 2, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Compute tensor dot product along specified axes. - - Given two tensors, `a` and `b`, and an array_like object containing - two array_like objects, ``(a_axes, b_axes)``, sum the products of - `a`'s and `b`'s elements (components) over the axes specified by - ``a_axes`` and ``b_axes``. The third argument can be a single non-negative - integer_like scalar, ``N``; if it is such, then the last ``N`` dimensions - of `a` and the first ``N`` dimensions of `b` are summed over. - - Parameters - ---------- - a, b : array_like - Tensors to "dot". - - axes : int or array_like - * integer_like - If an int N, sum over the last N axes of `a` and the first N axes - of `b` in order. - * (2,) array_like - Or, a list of axes to be summed over, first sequence applying to `a`, - second to `b`. Both elements array_like must be of the same length. - out : ndarray, optional - Output argument. This must have the exact shape that would be returned - if it was not present. If its dtype is not what would be expected from - this operation, then the result will be (unsafely) cast to `out`. - - Returns - ------- - output : ndarray - The tensor dot product of the inputs. If `out` is given, then it is - returned. - - Notes - ----- - The cuNumeric implementation is a little more liberal than NumPy in terms - of allowed broadcasting, e.g. ``tensordot(ones((3,1)), ones((1,4)))`` is - allowed. - - Except for the inner-product case, only floating-point types are supported. - - See Also - -------- - numpy.tensordot - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - (a_modes, b_modes, out_modes) = tensordot_modes(a.ndim, b.ndim, axes) - - return _contract( - a_modes, - b_modes, - out_modes, - a, - b, - out=out, - casting="unsafe", - ) - - -# Trivial multi-tensor contraction strategy: contract in input order -class NullOptimizer(oe.paths.PathOptimizer): # type: ignore [misc,no-any-unimported] # noqa - def __call__( - self, - inputs: list[set[str]], - outputs: set[str], - size_dict: dict[str, int], - memory_limit: Union[int, None] = None, - ) -> list[tuple[int, int]]: - return [(0, 1)] + [(0, -1)] * (len(inputs) - 2) - - -def _maybe_cast_input( - arr: ndarray, to_dtype: np.dtype[Any], casting: CastingKind -) -> ndarray: - if arr.dtype == to_dtype: - return arr - if not np.can_cast(arr.dtype, to_dtype, casting=casting): - raise TypeError( - f"Cannot cast input array of type {arr.dtype} to {to_dtype} with " - f"casting rule '{casting}'" - ) - return arr.astype(to_dtype) - - -# Generalized tensor contraction -def _contract( - a_modes: list[str], - b_modes: list[str], - out_modes: list[str], - a: ndarray, - b: Optional[ndarray] = None, - out: Optional[ndarray] = None, - casting: CastingKind = "same_kind", - dtype: Optional[np.dtype[Any]] = None, -) -> ndarray: - # Sanity checks - if len(a_modes) != a.ndim: - raise ValueError( - f"Expected {len(a_modes)}-d input array but got {a.ndim}-d" - ) - - if b is None: - if len(b_modes) != 0: - raise ValueError("Missing input array") - elif len(b_modes) != b.ndim: - raise ValueError( - f"Expected {len(b_modes)}-d input array but got {b.ndim}-d" - ) - - if out is not None and len(out_modes) != out.ndim: - raise ValueError( - f"Expected {len(out_modes)}-d output array but got {out.ndim}-d" - ) - - if len(set(out_modes)) != len(out_modes): - raise ValueError("Duplicate mode labels on output") - - if len(set(out_modes) - set(a_modes) - set(b_modes)) > 0: - raise ValueError("Unknown mode labels on output") - - makes_view = b is None and len(a_modes) == len(out_modes) - if dtype is not None and not makes_view: - c_dtype = dtype - elif out is not None: - c_dtype = out.dtype - elif b is None: - c_dtype = a.dtype - else: - c_dtype = ndarray.find_common_type(a, b) - - a = _maybe_cast_input(a, c_dtype, casting) - - if b is not None: - b = _maybe_cast_input(b, c_dtype, casting) - - out_dtype = out.dtype if out is not None else c_dtype - - # Handle duplicate modes on inputs - c_a_modes = Counter(a_modes) - for mode, count in c_a_modes.items(): - if count > 1: - axes = [i for (i, m) in enumerate(a_modes) if m == mode] - a = a._diag_helper(axes=axes) - # diagonal is stored on last axis - a_modes = [m for m in a_modes if m != mode] + [mode] - c_b_modes = Counter(b_modes) - for mode, count in c_b_modes.items(): - if count > 1: - axes = [i for (i, m) in enumerate(b_modes) if m == mode] - b = b._diag_helper(axes=axes) # type: ignore [union-attr] - # diagonal is stored on last axis - b_modes = [m for m in b_modes if m != mode] + [mode] - - # Drop modes corresponding to singleton dimensions. This handles cases of - # broadcasting. - for dim in reversed(range(a.ndim)): - if a.shape[dim] == 1: - a = a.squeeze(dim) - a_modes.pop(dim) - if b is not None: - for dim in reversed(range(b.ndim)): - if b.shape[dim] == 1: - b = b.squeeze(dim) - b_modes.pop(dim) - - # Sum-out modes appearing on one argument, and missing from the result - # TODO: If we supported sum on multiple axes we could do the full sum in a - # single operation, and avoid intermediates. - for dim, mode in reversed(list(enumerate(a_modes))): - if mode not in b_modes and mode not in out_modes: - a_modes.pop(dim) - a = a.sum(axis=dim) - - for dim, mode in reversed(list(enumerate(b_modes))): - if mode not in a_modes and mode not in out_modes: - b_modes.pop(dim) - b = b.sum(axis=dim) # type: ignore [union-attr] - - # Compute extent per mode. No need to consider broadcasting at this stage, - # since it has been handled above. - mode2extent: dict[str, int] = {} - for mode, extent in chain( - zip(a_modes, a.shape), zip(b_modes, b.shape) if b is not None else [] - ): - prev_extent = mode2extent.get(mode) - if prev_extent is not None and extent != prev_extent: - raise ValueError( - f"Incompatible sizes between matched dimensions: {extent} vs " - f"{prev_extent}" - ) - mode2extent[mode] = extent - - # Any modes appearing only on the result must have originally been present - # on one of the operands, but got dropped by the broadcast-handling code. - out_shape = ( - out.shape - if out is not None - else tuple(mode2extent.get(mode, 1) for mode in out_modes) - ) - c_modes = [] - c_shape: NdShape = () - c_bloated_shape: NdShape = () - for mode, extent in zip(out_modes, out_shape): - if mode not in a_modes and mode not in b_modes: - c_bloated_shape += (1,) - else: - assert extent > 1 - c_modes.append(mode) - c_shape += (extent,) - c_bloated_shape += (extent,) - - # Verify output array has the right shape (input arrays can be broadcasted - # up to match the output, but not the other way around). There should be no - # unknown or singleton modes on the result at this point. - for mode, extent in zip(c_modes, c_shape): - prev_extent = mode2extent[mode] - assert prev_extent != 1 - if extent != prev_extent: - raise ValueError("Wrong shape on output array") - - # Test for fallback to unary case - if b is not None: - if len(a_modes) == 0: - a = a * b - a_modes = b_modes - b = None - b_modes = [] - elif len(b_modes) == 0: - a = a * b - b = None - - if b is None: - # Unary contraction case - assert len(a_modes) == len(c_modes) and set(a_modes) == set(c_modes) - if len(a_modes) == 0: - # NumPy doesn't return a view in this case - c = copy(a) - elif a_modes == c_modes: - c = a - else: - # Shuffle input array according to mode labels - axes = [a_modes.index(mode) for mode in c_modes] - assert _builtin_all(ax >= 0 for ax in axes) - c = a.transpose(axes) - - else: - # Binary contraction case - # Create result array, if output array can't be directly targeted - if out is not None and out_dtype == c_dtype and out_shape == c_shape: - c = out - else: - c = ndarray( - shape=c_shape, - dtype=c_dtype, - inputs=(a, b), - ) - # Perform operation - c._thunk.contract( - c_modes, - a._thunk, - a_modes, - b._thunk, - b_modes, - mode2extent, - ) - - # Postprocess result before returning - if out is c: - # We already decided above to use the output array directly - return out - if out_dtype != c_dtype or out_shape != c_bloated_shape: - # We need to broadcast the result of the contraction or switch types - # before returning - if not np.can_cast(c_dtype, out_dtype, casting=casting): - raise TypeError( - f"Cannot cast intermediate result array of type {c_dtype} " - f"into output array of type {out_dtype} with casting rule " - f"'{casting}'" - ) - if out is None: - out = ndarray( - shape=out_shape, - dtype=out_dtype, - inputs=(c,), - ) - out[...] = c.reshape(c_bloated_shape) - return out - if out_shape != c_shape: - # We need to add missing dimensions, but they are all of size 1, so - # we don't need to broadcast - assert c_bloated_shape == out_shape - if out is None: - return c.reshape(out_shape) - else: - out[...] = c.reshape(out_shape) - return out - if out is not None: - # The output and result arrays are fully compatible, but we still - # need to copy - out[...] = c - return out - return c - - -def einsum( - expr: str, - *operands: ndarray, - out: Optional[ndarray] = None, - dtype: Optional[np.dtype[Any]] = None, - casting: CastingKind = "safe", - optimize: Union[bool, Literal["greedy", "optimal"]] = True, -) -> ndarray: - """ - Evaluates the Einstein summation convention on the operands. - - Using the Einstein summation convention, many common multi-dimensional, - linear algebraic array operations can be represented in a simple fashion. - In *implicit* mode `einsum` computes these values. - - In *explicit* mode, `einsum` provides further flexibility to compute - other array operations that might not be considered classical Einstein - summation operations, by disabling, or forcing summation over specified - subscript labels. - - Parameters - ---------- - subscripts : str - Specifies the subscripts for summation as comma separated list of - subscript labels. An implicit (classical Einstein summation) - calculation is performed unless the explicit indicator '->' is - included as well as subscript labels of the precise output form. - operands : list[array_like] - These are the arrays for the operation. - out : ndarray, optional - If provided, the calculation is done into this array. - dtype : data-type, optional - If provided, forces the calculation to use the data type specified. - Note that you may have to also give a more liberal `casting` - parameter to allow the conversions. Default is None. - casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional - Controls what kind of data casting may occur. - - * 'no' means the data types should not be cast at all. - * 'equiv' means only byte-order changes are allowed. - * 'safe' means only casts which can preserve values are allowed. - * 'same_kind' means only safe casts or casts within a kind, - like float64 to float32, are allowed. - * 'unsafe' means any data conversions may be done. - - Default is 'safe'. - optimize : ``{False, True, 'greedy', 'optimal'}``, optional - Controls if intermediate optimization should occur. If False then - arrays will be contracted in input order, one at a time. True (the - default) will use the 'greedy' algorithm. See ``cunumeric.einsum_path`` - for more information on the available optimization algorithms. - - Returns - ------- - output : ndarray - The calculation based on the Einstein summation convention. - - Notes - ----- - For most expressions, only floating-point types are supported. - - See Also - -------- - numpy.einsum - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - operands_list = [convert_to_cunumeric_ndarray(op) for op in operands] - - if out is not None: - out = convert_to_cunumeric_ndarray(out, share=True) - - if optimize is True: - optimize = "greedy" - elif optimize is False: - optimize = NullOptimizer() - - # This call normalizes the expression (adds the output part if it's - # missing, expands '...') and checks for some errors (mismatch on number - # of dimensions between operand and expression, wrong number of operands, - # unknown modes on output, a mode appearing under two different - # non-singleton extents). - computed_operands, contractions = oe.contract_path( - expr, *operands_list, einsum_call=True, optimize=optimize - ) - for indices, _, sub_expr, _, _ in contractions: - assert len(indices) == 1 or len(indices) == 2 - a = computed_operands.pop(indices[0]) - b = computed_operands.pop(indices[1]) if len(indices) == 2 else None - if b is None: - m = re.match(r"([a-zA-Z]*)->([a-zA-Z]*)", sub_expr) - if m is None: - raise NotImplementedError("Non-alphabetic mode labels") - a_modes = list(m.group(1)) - b_modes = [] - out_modes = list(m.group(2)) - else: - m = re.match(r"([a-zA-Z]*),([a-zA-Z]*)->([a-zA-Z]*)", sub_expr) - if m is None: - raise NotImplementedError("Non-alphabetic mode labels") - a_modes = list(m.group(1)) - b_modes = list(m.group(2)) - out_modes = list(m.group(3)) - sub_result = _contract( - a_modes, - b_modes, - out_modes, - a, - b, - out=(out if len(computed_operands) == 0 else None), - casting=casting, - dtype=dtype, - ) - computed_operands.append(sub_result) - - assert len(computed_operands) == 1 - return computed_operands[0] - - -def einsum_path( - expr: str, - *operands: ndarray, - optimize: Union[bool, list[Any], tuple[Any, ...], str] = "greedy", -) -> tuple[list[Union[str, int]], str]: - """ - Evaluates the lowest cost contraction order for an einsum expression by - considering the creation of intermediate arrays. - - Parameters - ---------- - expr : str - Specifies the subscripts for summation. - *operands : Sequence[array_like] - These are the arrays for the operation. - optimize : ``{bool, list, tuple, 'greedy', 'optimal'}`` - Choose the type of path. If a tuple is provided, the second argument is - assumed to be the maximum intermediate size created. If only a single - argument is provided the largest input or output array size is used - as a maximum intermediate size. - - * if a list is given that starts with ``einsum_path``, uses this as the - contraction path - * if False no optimization is taken - * if True defaults to the 'greedy' algorithm - * 'optimal' An algorithm that combinatorially explores all possible - ways of contracting the listed tensors and chooses the least costly - path. Scales exponentially with the number of terms in the - contraction. - * 'greedy' An algorithm that chooses the best pair contraction - at each step. Effectively, this algorithm searches the largest inner, - Hadamard, and then outer products at each step. Scales cubically with - the number of terms in the contraction. Equivalent to the 'optimal' - path for most contractions. - - Default is 'greedy'. - - Returns - ------- - path : list[Tuple[int,...]] - A list representation of the einsum path. - string_repr : str - A printable representation of the einsum path. - - Notes - ----- - The resulting path indicates which terms of the input contraction should be - contracted first, the result of this contraction is then appended to the - end of the contraction list. This list can then be iterated over until all - intermediate contractions are complete. - - See Also - -------- - numpy.einsum_path - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - computed_operands = [convert_to_cunumeric_ndarray(op) for op in operands] - memory_limit = _builtin_max(op.size for op in computed_operands) - if isinstance(optimize, tuple): - if len(optimize) != 2: - raise ValueError("einsum_path expects optimize tuples of size 2") - optimize, memory_limit = optimize - if optimize is True: - optimize = "greedy" - elif optimize is False: - optimize = [tuple(range(len(computed_operands)))] - elif optimize in ["greedy", "optimal"]: - pass - elif ( - isinstance(optimize, list) - and len(optimize) > 1 - and optimize[0] == "einsum_path" - ): - optimize = optimize[1:] - else: - raise ValueError( - f"einsum_path: unexpected value for optimize: {optimize}" - ) - path, info = oe.contract_path( - expr, *computed_operands, optimize=optimize, memory_limit=memory_limit - ) - return ["einsum_path"] + path, info - - -@add_boilerplate("a") -def trace( - a: ndarray, - offset: int = 0, - axis1: Optional[int] = None, - axis2: Optional[int] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return the sum along diagonals of the array. - - If a is 2-D, the sum along its diagonal with the given offset is - returned, i.e., the sum of elements a[i,i+offset] for all i. - If a has more than two dimensions, then the axes specified by axis1 - and axis2 are used to determine the 2-D sub-arrays whose traces - are returned. The shape of the resulting array is the same as that - of a with axis1 and axis2 removed. - - Parameters - ---------- - a : array_like - Input array, from which the diagonals are taken. - offset : int, optional - Offset of the diagonal from the main diagonal. Can be both - positive and negative. Defaults to 0. - axis1, axis2 : int, optional - Axes to be used as the first and second axis of the 2-D sub-arrays - from which the diagonals should be taken. Defaults are the - first two axes of a. - dtype : data-type, optional - Determines the data-type of the returned array and of the - accumulator where the elements are summed. If dtype has the value - None and a is of integer type of precision less than the default - integer precision, then the default integer precision is used. - Otherwise, the precision is the same as that of a. - - out : ndarray, optional - Array into which the output is placed. Its type is preserved and - it must be of the right shape to hold the output. - - Returns - ------- - sum_along_diagonals : ndarray - If a is 2-D, the sum along the diagonal is returned. If a has - larger dimensions, then an array of sums along diagonals is returned. - - Raises - ------ - ValueError - If the dimension of `a` is less than 2. - - See Also - -------- - numpy.diagonal - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.trace( - offset=offset, axis1=axis1, axis2=axis2, dtype=dtype, out=out - ) - - -################# -# Logic functions -################# - -# Truth value testing - - -@add_boilerplate("a") -def all( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Test whether all array elements along a given axis evaluate to True. - - Parameters - ---------- - a : array_like - Input array or object that can be converted to an array. - axis : None or int or tuple[int], optional - Axis or axes along which a logical AND reduction is performed. - The default (``axis=None``) is to perform a logical AND over all - the dimensions of the input array. `axis` may be negative, in - which case it counts from the last to the first axis. - - If this is a tuple of ints, a reduction is performed on multiple - axes, instead of a single axis or all the axes as before. - out : ndarray, optional - Alternate output array in which to place the result. - It must have the same shape as the expected output and its - type is preserved (e.g., if ``dtype(out)`` is float, the result - will consist of 0.0's and 1.0's). See `ufuncs-output-type` for more - details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `all` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - Returns - ------- - all : ndarray, bool - A new boolean or array is returned unless `out` is specified, - in which case a reference to `out` is returned. - - See Also - -------- - numpy.all - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.all(axis=axis, out=out, keepdims=keepdims, where=where) - - -@add_boilerplate("a") -def any( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Test whether any array element along a given axis evaluates to True. - - Returns single boolean unless `axis` is not ``None`` - - Parameters - ---------- - a : array_like - Input array or object that can be converted to an array. - axis : None or int or tuple[int], optional - Axis or axes along which a logical OR reduction is performed. - The default (``axis=None``) is to perform a logical OR over all - the dimensions of the input array. `axis` may be negative, in - which case it counts from the last to the first axis. - - If this is a tuple of ints, a reduction is performed on multiple - axes, instead of a single axis or all the axes as before. - out : ndarray, optional - Alternate output array in which to place the result. It must have - the same shape as the expected output and its type is preserved - (e.g., if it is of type float, then it will remain so, returning - 1.0 for True and 0.0 for False, regardless of the type of `a`). - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `any` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - Returns - ------- - any : bool or ndarray - A new boolean or `ndarray` is returned unless `out` is specified, - in which case a reference to `out` is returned. - - See Also - -------- - numpy.any - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.any(axis=axis, out=out, keepdims=keepdims, where=where) - - -# Array contents - - -# Logic operations - - -# Comparison - - -@add_boilerplate("a", "b") -def allclose( - a: ndarray, - b: ndarray, - rtol: float = 1e-5, - atol: float = 1e-8, - equal_nan: bool = False, -) -> ndarray: - """ - - Returns True if two arrays are element-wise equal within a tolerance. - - The tolerance values are positive, typically very small numbers. The - relative difference (`rtol` * abs(`b`)) and the absolute difference - `atol` are added together to compare against the absolute difference - between `a` and `b`. - - NaNs are treated as equal if they are in the same place and if - ``equal_nan=True``. Infs are treated as equal if they are in the same - place and of the same sign in both arrays. - - Parameters - ---------- - a, b : array_like - Input arrays to compare. - rtol : float - The relative tolerance parameter (see Notes). - atol : float - The absolute tolerance parameter (see Notes). - equal_nan : bool - Whether to compare NaN's as equal. If True, NaN's in `a` will be - considered equal to NaN's in `b` in the output array. - - Returns - ------- - allclose : ndarray scalar - Returns True if the two arrays are equal within the given - tolerance; False otherwise. - - Notes - ----- - If the following equation is element-wise True, then allclose returns - True. - - absolute(`a` - `b`) <= (`atol` + `rtol` * absolute(`b`)) - - See Also - -------- - numpy.allclose - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if equal_nan: - raise NotImplementedError( - "cuNumeric does not support `equal_nan` yet for allclose" - ) - args = (np.array(rtol, dtype=np.float64), np.array(atol, dtype=np.float64)) - return ndarray._perform_binary_reduction( - BinaryOpCode.ISCLOSE, - a, - b, - dtype=np.dtype(bool), - extra_args=args, - ) - - -@add_boilerplate("a", "b") -def isclose( - a: ndarray, - b: ndarray, - rtol: float = 1e-5, - atol: float = 1e-8, - equal_nan: bool = False, -) -> ndarray: - """ - - Returns a boolean array where two arrays are element-wise equal within a - tolerance. - - Parameters - ---------- - a, b : array_like - Input arrays to compare. - rtol : float - The relative tolerance parameter (see Notes). - atol : float - The absolute tolerance parameter (see Notes). - equal_nan : bool - Whether to compare NaN's as equal. If True, NaN's in `a` will be - considered equal to NaN's in `b` in the output array. - - Returns - ------- - y : array_like - Returns a boolean array of where `a` and `b` are equal within the - given tolerance. If both `a` and `b` are scalars, returns a single - boolean value. - - Notes - ----- - For finite values, isclose uses the following equation to test whether - two floating point values are equivalent. - - absolute(`a` - `b`) <= (`atol` + `rtol` * absolute(`b`)) - - See Also - -------- - numpy.isclose - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if equal_nan: - raise NotImplementedError( - "cuNumeric does not support `equal_nan` yet for isclose" - ) - - out_shape = np.broadcast_shapes(a.shape, b.shape) - out = empty(out_shape, dtype=bool) - - common_type = ndarray.find_common_type(a, b) - a = a.astype(common_type) - b = b.astype(common_type) - - out._thunk.isclose(a._thunk, b._thunk, rtol, atol, equal_nan) - return out - - -@add_boilerplate("a1", "a2") -def array_equal( - a1: ndarray, a2: ndarray, equal_nan: bool = False -) -> Union[bool, ndarray]: - """ - - True if two arrays have the same shape and elements, False otherwise. - - Parameters - ---------- - a1, a2 : array_like - Input arrays. - equal_nan : bool - Whether to compare NaN's as equal. If the dtype of a1 and a2 is - complex, values will be considered equal if either the real or the - imaginary component of a given value is ``nan``. - - Returns - ------- - b : ndarray scalar - Returns True if the arrays are equal. - - See Also - -------- - numpy.array_equal - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if equal_nan: - raise NotImplementedError( - "cuNumeric does not support `equal_nan` yet for `array_equal`" - ) - - if a1.shape != a2.shape: - return False - return ndarray._perform_binary_reduction( - BinaryOpCode.EQUAL, a1, a2, dtype=np.dtype(np.bool_) - ) - - -######################## -# Mathematical functions -######################## - -# Trigonometric functions - - -# Hyperbolic functions - - -# Rounding - - -# Sums, products, differences - - -@add_boilerplate("a") -def prod( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Return the product of array elements over a given axis. - - Parameters - ---------- - a : array_like - Input data. - axis : None or int or tuple[int], optional - Axis or axes along which a product is performed. The default, - axis=None, will calculate the product of all the elements in the - input array. If axis is negative it counts from the last to the - first axis. - - If axis is a tuple of ints, a product is performed on all of the - axes specified in the tuple instead of a single axis or all the - axes as before. - dtype : data-type, optional - The type of the returned array, as well as of the accumulator in - which the elements are multiplied. The dtype of `a` is used by - default unless `a` has an integer dtype of less precision than the - default platform integer. In that case, if `a` is signed then the - platform integer is used while if `a` is unsigned then an unsigned - integer of the same precision as the platform integer is used. - out : ndarray, optional - Alternative output array in which to place the result. It must have - the same shape as the expected output, but the type of the output - values will be cast if necessary. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left in the - result as dimensions with size one. With this option, the result - will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `prod` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - initial : scalar, optional - The starting value for this product. See `~cunumeric.ufunc.reduce` for - details. - - where : array_like[bool], optional - Elements to include in the product. See `~cunumeric.ufunc.reduce` for - details. - - Returns - ------- - product_along_axis : ndarray, see `dtype` parameter above. - An array shaped as `a` but with the specified axis removed. - Returns a reference to `out` if specified. - - See Also - -------- - numpy.prod - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return multiply.reduce( - a, - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -@add_boilerplate("a") -def sum( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Sum of array elements over a given axis. - - Parameters - ---------- - a : array_like - Elements to sum. - axis : None or int or tuple[int], optional - Axis or axes along which a sum is performed. The default, - axis=None, will sum all of the elements of the input array. If - axis is negative it counts from the last to the first axis. - - If axis is a tuple of ints, a sum is performed on all of the axes - specified in the tuple instead of a single axis or all the axes as - before. - dtype : data-type, optional - The type of the returned array and of the accumulator in which the - elements are summed. The dtype of `a` is used by default unless `a` - has an integer dtype of less precision than the default platform - integer. In that case, if `a` is signed then the platform integer - is used while if `a` is unsigned then an unsigned integer of the - same precision as the platform integer is used. - out : ndarray, optional - Alternative output array in which to place the result. It must have - the same shape as the expected output, but the type of the output - values will be cast if necessary. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `sum` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - initial : scalar, optional - Starting value for the sum. See `~cunumeric.ufunc.reduce` for details. - - where : array_like[bool], optional - Elements to include in the sum. See `~cunumeric.ufunc.reduce` for - details. - - Returns - ------- - sum_along_axis : ndarray - An array with the same shape as `a`, with the specified - axis removed. If `a` is a 0-d array, or if `axis` is None, a scalar - is returned. If an output array is specified, a reference to - `out` is returned. - - See Also - -------- - numpy.sum - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return add.reduce( - a, - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -@add_boilerplate("a") -def cumprod( - a: ndarray, - axis: Optional[int] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return the cumulative product of the elements along a given axis. - - Parameters - ---------- - a : array_like - Input array. - - axis : int, optional - Axis along which the cumulative product is computed. The default (None) - is to compute the cumprod over the flattened array. - - dtype : dtype, optional - Type of the returned array and of the accumulator in which the elements - are multiplied. If dtype is not specified, it defaults to the dtype of - a, unless a has an integer dtype with a precision less than that of the - default platform integer. In that case, the default platform integer is - used. - out : ndarray, optional - Alternative output array in which to place the result. It must have the - same shape and buffer length as the expected output but the type will - be cast if necessary. See Output type determination for more details. - - Returns - ------- - cumprod : ndarray - A new array holding the result is returned unless out is specified, in - which case a reference to out is returned. The result has the same size - as a, and the same shape as a if axis is not None or a is a 1-d array. - - See Also - -------- - numpy.cumprod - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - with floating point and complex types. For example, when boundary values - such as inf occur they may not propagate as expected. Consider the float32 - array ``[3e+37, 1, 100, 0.01]``. NumPy's cumprod will return a result of - ``[3e+37, 3e+37, inf, inf]``. However, cuNumeric might internally partition - the array such that partition 0 has ``[3e+37, 1]`` and partition 1 has - ``[100, 0.01]``, returning the result ``[3e+37, 3e+37, inf, 3e+37]``. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return ndarray._perform_scan( - ScanCode.PROD, - a, - axis=axis, - dtype=dtype, - out=out, - nan_to_identity=False, - ) - - -@add_boilerplate("a") -def cumsum( - a: ndarray, - axis: Optional[int] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return the cumulative sum of the elements along a given axis. - - Parameters - ---------- - a : array_like - Input array. - - axis : int, optional - Axis along which the cumulative sum is computed. The default (None) is - to compute the cumsum over the flattened array. - - dtype : dtype, optional - Type of the returned array and of the accumulator in which the elements - are summed. If dtype is not specified, it defaults to the dtype of a, - unless a has an integer dtype with a precision less than that of the - default platform integer. In that case, the default platform integer is - used. - out : ndarray, optional - Alternative output array in which to place the result. It must have the - same shape and buffer length as the expected output but the type will - be cast if necessary. See Output type determination for more details. - - Returns - ------- - cumsum : ndarray. - A new array holding the result is returned unless out is specified, in - which case a reference to out is returned. The result has the same size - as a, and the same shape as a if axis is not None or a is a 1-d array. - - See Also - -------- - numpy.cumsum - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - with floating point and complex types. For example, when boundary values - such as inf occur they may not propagate as expected. For more explanation - check cunumeric.cumprod. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return ndarray._perform_scan( - ScanCode.SUM, a, axis=axis, dtype=dtype, out=out, nan_to_identity=False - ) - - -@add_boilerplate("a") -def nancumprod( - a: ndarray, - axis: Optional[int] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return the cumulative product of the elements along a given axis treating - Not a Numbers (NaNs) as one. The cumulative product does not change when - NaNs are encountered and leading NaNs are replaced by ones. - - Ones are returned for slices that are all-NaN or empty. - - Parameters - ---------- - a : array_like - Input array. - - axis : int, optional - Axis along which the cumulative product is computed. The default (None) - is to compute the nancumprod over the flattened array. - - dtype : dtype, optional - Type of the returned array and of the accumulator in which the elements - are multiplied. If dtype is not specified, it defaults to the dtype of - a, unless a has an integer dtype with a precision less than that of the - default platform integer. In that case, the default platform integer is - used. - out : ndarray, optional - Alternative output array in which to place the result. It must have the - same shape and buffer length as the expected output but the type will - be cast if necessary. See Output type determination for more details. - - Returns - ------- - nancumprod : ndarray. - A new array holding the result is returned unless out is specified, in - which case a reference to out is returned. The result has the same size - as a, and the same shape as a if axis is not None or a is a 1-d array. - - See Also - -------- - numpy.nancumprod - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - with floating point and complex types. For example, when boundary values - such as inf occur they may not propagate as expected. For more explanation - check cunumeric.cumprod. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return ndarray._perform_scan( - ScanCode.PROD, a, axis=axis, dtype=dtype, out=out, nan_to_identity=True - ) - - -@add_boilerplate("a") -def nancumsum( - a: ndarray, - axis: Optional[int] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, -) -> ndarray: - """ - Return the cumulative sum of the elements along a given axis treating Not a - Numbers (NaNs) as zero. The cumulative sum does not change when NaNs are - encountered and leading NaNs are replaced by zeros. - - Zeros are returned for slices that are all-NaN or empty. - - Parameters - ---------- - a : array_like - Input array. - - axis : int, optional - Axis along which the cumulative sum is computed. The default (None) is - to compute the nancumsum over the flattened array. - - dtype : dtype, optional - Type of the returned array and of the accumulator in which the elements - are summed. If dtype is not specified, it defaults to the dtype of a, - unless a has an integer dtype with a precision less than that of the - default platform integer. In that case, the default platform integer is - used. - out : ndarray, optional - Alternative output array in which to place the result. It must have the - same shape and buffer length as the expected output but the type will - be cast if necessary. See Output type determination for more details. - - Returns - ------- - nancumsum : ndarray. - A new array holding the result is returned unless out is specified, in - which case a reference to out is returned. The result has the same size - as a, and the same shape as a if axis is not None or a is a 1-d array. - - See Also - -------- - numpy.nancumsum - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - with floating point and complex types. For example, when boundary values - such as inf occur they may not propagate as expected. For more explanation - check cunumeric.cumprod. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return ndarray._perform_scan( - ScanCode.SUM, a, axis=axis, dtype=dtype, out=out, nan_to_identity=True - ) - - -@add_boilerplate("a") -def nanargmax( - a: ndarray, - axis: Any = None, - out: Union[ndarray, None] = None, - *, - keepdims: bool = False, -) -> ndarray: - """ - Return the indices of the maximum values in the specified axis ignoring - NaNs. For empty arrays, ValueError is raised. For all-NaN slices, - ValueError is raised only when CUNUMERIC_NUMPY_COMPATIBILITY - environment variable is set, otherwise identity is returned. - - Warning: results cannot be trusted if a slice contains only NaNs - and -Infs. - - Parameters - ---------- - a : array_like - Input array. - axis : int, optional - By default, the index corresponds to the flattened array, otherwise - along the specified axis. - out : ndarray, optional - If provided, the result will be inserted into this array. It should - be of the appropriate shape and dtype. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the array. - - Returns - ------- - index_array : ndarray[int] - Array of indices into the array. It has the same shape as `a.shape` - with the dimension along `axis` removed. - - See Also - -------- - numpy.nanargmin, numpy.nanargmax - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if a.size == 0: - raise ValueError("attempt to get nanargmax of an empty sequence") - - if cunumeric_settings.numpy_compat() and a.dtype.kind == "f": - if any(all(isnan(a), axis=axis)): - raise ValueError("Array/Slice contains only NaNs") - - unary_red_code = get_non_nan_unary_red_code( - a.dtype.kind, UnaryRedCode.NANARGMAX - ) - - return a._perform_unary_reduction( - unary_red_code, - a, - axis=axis, - out=out, - keepdims=keepdims, - res_dtype=np.dtype(np.int64), - ) - - -@add_boilerplate("a") -def nanargmin( - a: ndarray, - axis: Any = None, - out: Union[ndarray, None] = None, - *, - keepdims: bool = False, -) -> ndarray: - """ - Return the indices of the minimum values in the specified axis ignoring - NaNs. For empty arrays, ValueError is raised. For all-NaN slices, - ValueError is raised only when CUNUMERIC_NUMPY_COMPATIBILITY - environment variable is set, otherwise identity is returned. - - Warning: results cannot be trusted if a slice contains only NaNs - and -Infs. - - Parameters - ---------- - a : array_like - Input array. - axis : int, optional - By default, the index corresponds to the flattened array, otherwise - along the specified axis. - out : ndarray, optional - If provided, the result will be inserted into this array. It should - be of the appropriate shape and dtype. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the array. - - Returns - ------- - index_array : ndarray[int] - Array of indices into the array. It has the same shape as `a.shape` - with the dimension along `axis` removed. - - See Also - -------- - numpy.nanargmin, numpy.nanargmax - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if a.size == 0: - raise ValueError("attempt to get nanargmin of an empty sequence") - - if cunumeric_settings.numpy_compat() and a.dtype.kind == "f": - if any(all(isnan(a), axis=axis)): - raise ValueError("Array/Slice contains only NaNs") - - unary_red_code = get_non_nan_unary_red_code( - a.dtype.kind, UnaryRedCode.NANARGMIN - ) - - return a._perform_unary_reduction( - unary_red_code, - a, - axis=axis, - out=out, - keepdims=keepdims, - res_dtype=np.dtype(np.int64), - ) - - -@add_boilerplate("a") -def nanmin( - a: ndarray, - axis: Any = None, - out: Union[ndarray, None] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Return minimum of an array or minimum along an axis, ignoring any - NaNs. When all-NaN slices are encountered, a NaN is returned - for that slice only when CUNUMERIC_NUMPY_COMPATIBILITY environment - variable is set, otherwise identity is returned. - Empty slices will raise a ValueError - - Parameters - ---------- - a : array_like - Array containing numbers whose minimum is desired. If a is not an - array, a conversion is attempted. - - axis : {int, tuple of int, None}, optional - Axis or axes along which the minimum is computed. The default is to - compute the minimum of the flattened array. - - out : ndarray, optional - Alternative output array in which to place the result. Must - be of the same shape and buffer length as the expected output. - See `ufuncs-output-type` for more details. - - keepdims : bool, Optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `amin` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - initial : scalar, optional - The maximum value of an output element. Must be present to allow - computation on empty slice. See `~cunumeric.ufunc.reduce` for details. - - where : array_like[bool], optional - Elements to compare for the minimum. See `~cunumeric.ufunc.reduce` - for details. - - Returns - ------- - nanmin : ndarray or scalar - Minimum of `a`. If `axis` is None, the result is a scalar value. - If `axis` is given, the result is an array of dimension - ``a.ndim - 1``. - - Notes - ----- - CuNumeric's implementation will not raise a Runtime Warning for - slices with all-NaNs - - See Also - -------- - numpy.nanmin, numpy.nanmax, numpy.min, numpy.max, numpy.isnan, - numpy.maximum - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - unary_red_code = get_non_nan_unary_red_code( - a.dtype.kind, UnaryRedCode.NANMIN - ) - - out_array = a._perform_unary_reduction( - unary_red_code, - a, - axis=axis, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - if cunumeric_settings.numpy_compat() and a.dtype.kind == "f": - all_nan = all(isnan(a), axis=axis, keepdims=keepdims, where=where) - putmask(out_array, all_nan, np.nan) # type: ignore - - return out_array - - -@add_boilerplate("a") -def nanmax( - a: ndarray, - axis: Any = None, - out: Union[ndarray, None] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Return the maximum of an array or maximum along an axis, ignoring any - NaNs. When all-NaN slices are encountered, a NaN is returned - for that slice only when CUNUMERIC_NUMPY_COMPATIBILITY environment - variable is set, otherwise identity is returned. - Empty slices will raise a ValueError - - Parameters - ---------- - a : array_like - Array containing numbers whose maximum is desired. If a is not - an array, a conversion is attempted. - - axis : None or int or tuple[int], optional - Axis or axes along which to operate. By default, flattened input is - used. - - If this is a tuple of ints, the maximum is selected over multiple axes, - instead of a single axis or all the axes as before. - - out : ndarray, optional - Alternative output array in which to place the result. Must - be of the same shape and buffer length as the expected output. - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `amax` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - initial : scalar, optional - The minimum value of an output element. Must be present to allow - computation on empty slice. See `~cunumeric.ufunc.reduce` for details. - - where : array_like[bool], optional - Elements to compare for the maximum. See `~cunumeric.ufunc.reduce` - for details. - - Returns - ------- - nanmax : ndarray or scalar - An array with the same shape as `a`, with the specified axis - removed. If `a` is 0-d array, of if axis is None, an ndarray - scalar is returned. The same dtype as `a` is returned. - - Notes - ----- - CuNumeric's implementation will not raise a Runtime Warning for - slices with all-NaNs - - See Also - -------- - numpy.nanmin, numpy.amax, numpy.isnan, numpy.fmax, numpy.maximum, - numpy.isfinite - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - unary_red_code = get_non_nan_unary_red_code( - a.dtype.kind, UnaryRedCode.NANMAX - ) - - out_array = a._perform_unary_reduction( - unary_red_code, - a, - axis=axis, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - if cunumeric_settings.numpy_compat() and a.dtype.kind == "f": - all_nan = all(isnan(a), axis=axis, keepdims=keepdims, where=where) - putmask(out_array, all_nan, np.nan) # type: ignore - - return out_array - - -@add_boilerplate("a") -def nanprod( - a: ndarray, - axis: Any = None, - dtype: Any = None, - out: Union[ndarray, None] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Return the product of array elements over a given axis treating - Not a Numbers (NaNs) as ones. - - One is returned for slices that are all-NaN or empty. - - Parameters - ---------- - a : array_like - Input array. - axis : int, optional - Axis or axes along which the product is computed. The - default is to compute the product of the flattened array. - dtype : data-type, optional - The type of the returned array and of the accumulator in - which the elements are summed. By default, the dtype of a - is used. An exception is when a has an integer type with - less precision than the platform (u)intp. In that case, - the default will be either (u)int32 or (u)int64 depending - on whether the platform is 32 or 64 bits. For inexact - inputs, dtype must be inexact. - out : ndarray, optional - Alternate output array in which to place the result. The - default is None. If provided, it must have the same shape as - the expected output, but the type will be cast if necessary. - See Output type determination for more details. The casting of - NaN to integer can yield unexpected results. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left in the - result as dimensions with size one. With this option, the result - will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `prod` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - initial : scalar, optional - The starting value for this product. See `~cunumeric.ufunc.reduce` for - details. - where : array_like[bool], optional - Elements to include in the product. See `~cunumeric.ufunc.reduce` for - details. - - Returns - ------- - nanprod: ndarray, see `dtype` parameter above. - A new array holding the result is returned unless out is - specified, in which case it is returned. - - See Also - -------- - numpy.prod, numpy.isnan - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - - # Note: if the datatype of the input array is int and less - # than that of the platform int, then a convert task is launched - # in np.prod to take care of the type casting - - if a.dtype == np.complex128: - raise NotImplementedError( - "operation is not supported for complex128 arrays" - ) - - if a.dtype.kind in ("f", "c"): - unary_red_code = UnaryRedCode.NANPROD - else: - unary_red_code = UnaryRedCode.PROD - - return a._perform_unary_reduction( - unary_red_code, - a, - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -@add_boilerplate("a") -def nansum( - a: ndarray, - axis: Any = None, - dtype: Any = None, - out: Union[ndarray, None] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - Return the sum of array elements over a given axis treating - Not a Numbers (NaNs) as ones. - - Zero is returned for slices that are all-NaN or empty. - - Parameters - ---------- - a : array_like - Array containing numbers whose product is desired. If a is not - an array, a conversion is attempted. - - axis : None or int or tuple[int], optional - Axis or axes along which a sum is performed. The default, - axis=None, will sum all of the elements of the input array. - If axis is negative it counts from the last to the first axis. - - If axis is a tuple of ints, a sum is performed on all of the - axes specified in the tuple instead of a single axis or all - the axes as before. - - dtype : data-type, optional - The type of the returned array and of the accumulator in which - the elements are summed. The dtype of `a` is used by default - unless `a` has an integer dtype of less precision than the - default platform integer. In that case, if `a` is signed then - the platform integer is used while if `a` is unsigned then an - unsigned integer of the same precision as the platform integer - is used. - - out : ndarray, optional - Alternative output array in which to place the result. It must - have the same shape as the expected output, but the type of - the output values will be cast if necessary. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - initial : scalar, optional - Starting value for the sum. See `~cunumeric.ufunc.reduce` for - details. - - where : array_like[bool], optional - Elements to include in the sum. See `~cunumeric.ufunc.reduce` for - details. - - Returns - ------- - nansum : ndarray, see `dtype` parameter above. - A new array holding the result is returned unless out is - specified, in which case it is returned. The result has the - same size as a, and the same shape as a if axis is not None or - a is a 1-d array. - - See Also - -------- - numpy.nansum, numpy.isnan, numpy.isfinite - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - return a._nansum( - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -# Exponents and logarithms - - -# Arithmetic operations - - -# Handling complex numbers - - -@add_boilerplate("val") -def real(val: ndarray) -> ndarray: - """ - Return the real part of the complex argument. - - Parameters - ---------- - val : array_like - Input array. - - Returns - ------- - out : ndarray or scalar - The real component of the complex argument. If `val` is real, the type - of `val` is used for the output. If `val` has complex elements, the - returned type is float. - - See Also - -------- - numpy.real - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return val.real - - -@add_boilerplate("val") -def imag(val: ndarray) -> ndarray: - """ - - Return the imaginary part of the complex argument. - - Parameters - ---------- - val : array_like - Input array. - - Returns - ------- - out : ndarray or scalar - The imaginary component of the complex argument. If `val` is real, - the type of `val` is used for the output. If `val` has complex - elements, the returned type is float. - - See Also - -------- - numpy.imag - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return val.imag - - -# Extrema Finding - - -@add_boilerplate("a") -def amax( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Return the maximum of an array or maximum along an axis. - - Parameters - ---------- - a : array_like - Input data. - axis : None or int or tuple[int], optional - Axis or axes along which to operate. By default, flattened input is - used. - - If this is a tuple of ints, the maximum is selected over multiple axes, - instead of a single axis or all the axes as before. - out : ndarray, optional - Alternative output array in which to place the result. Must - be of the same shape and buffer length as the expected output. - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `amax` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - initial : scalar, optional - The minimum value of an output element. Must be present to allow - computation on empty slice. See `~cunumeric.ufunc.reduce` for details. - - where : array_like[bool], optional - Elements to compare for the maximum. See `~cunumeric.ufunc.reduce` - for details. - - Returns - ------- - amax : ndarray or scalar - Maximum of `a`. If `axis` is None, the result is a scalar value. - If `axis` is given, the result is an array of dimension - ``a.ndim - 1``. - - See Also - -------- - numpy.amax - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return maximum.reduce( - a, - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -max = amax - - -@add_boilerplate("a") -def amin( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Return the minimum of an array or minimum along an axis. - - Parameters - ---------- - a : array_like - Input data. - axis : None or int or tuple[int], optional - Axis or axes along which to operate. By default, flattened input is - used. - - If this is a tuple of ints, the minimum is selected over multiple axes, - instead of a single axis or all the axes as before. - out : ndarray, optional - Alternative output array in which to place the result. Must - be of the same shape and buffer length as the expected output. - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `amin` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - initial : scalar, optional - The maximum value of an output element. Must be present to allow - computation on empty slice. See `~cunumeric.ufunc.reduce` for details. - - where : array_like[bool], optional - Elements to compare for the minimum. See `~cunumeric.ufunc.reduce` - for details. - - Returns - ------- - amin : ndarray or scalar - Minimum of `a`. If `axis` is None, the result is a scalar value. - If `axis` is given, the result is an array of dimension - ``a.ndim - 1``. - - See Also - -------- - numpy.amin - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return minimum.reduce( - a, - axis=axis, - dtype=dtype, - out=out, - keepdims=keepdims, - initial=initial, - where=where, - ) - - -min = amin - -# Miscellaneous - - -@add_boilerplate("a", "v") -def convolve(a: ndarray, v: ndarray, mode: ConvolveMode = "full") -> ndarray: - """ - - Returns the discrete, linear convolution of two ndarrays. - - If `a` and `v` are both 1-D and `v` is longer than `a`, the two are - swapped before computation. For N-D cases, the arguments are never swapped. - - Parameters - ---------- - a : (N,) array_like - First input ndarray. - v : (M,) array_like - Second input ndarray. - mode : ``{'full', 'valid', 'same'}``, optional - 'same': - The output is the same size as `a`, centered with respect to - the 'full' output. (default) - - 'full': - The output is the full discrete linear convolution of the inputs. - - 'valid': - The output consists only of those elements that do not - rely on the zero-padding. In 'valid' mode, either `a` or `v` - must be at least as large as the other in every dimension. - - Returns - ------- - out : ndarray - Discrete, linear convolution of `a` and `v`. - - See Also - -------- - numpy.convolve - - Notes - ----- - The current implementation only supports the 'same' mode. - - Unlike `numpy.convolve`, `cunumeric.convolve` supports N-dimensional - inputs, but it follows NumPy's behavior for 1-D inputs. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if mode != "same": - raise NotImplementedError("Need to implement other convolution modes") - - if a.ndim != v.ndim: - raise RuntimeError("Arrays should have the same dimensions") - elif a.ndim > 3: - raise NotImplementedError(f"{a.ndim}-D arrays are not yet supported") - - if a.ndim == 1 and a.size < v.size: - v, a = a, v - - if a.dtype != v.dtype: - v = v.astype(a.dtype) - out = ndarray( - shape=a.shape, - dtype=a.dtype, - inputs=(a, v), - ) - a._thunk.convolve(v._thunk, out._thunk, mode) - return out - - -@add_boilerplate("a") -def clip( - a: ndarray, - a_min: Union[int, float, npt.ArrayLike, None], - a_max: Union[int, float, npt.ArrayLike, None], - out: Union[npt.NDArray[Any], ndarray, None] = None, -) -> ndarray: - """ - - Clip (limit) the values in an array. - - Given an interval, values outside the interval are clipped to - the interval edges. For example, if an interval of ``[0, 1]`` - is specified, values smaller than 0 become 0, and values larger - than 1 become 1. - - Parameters - ---------- - a : array_like - Array containing elements to clip. - a_min : scalar or array_like or None - Minimum value. If None, clipping is not performed on lower - interval edge. Not more than one of `a_min` and `a_max` may be - None. - a_max : scalar or array_like or None - Maximum value. If None, clipping is not performed on upper - interval edge. Not more than one of `a_min` and `a_max` may be - None. If `a_min` or `a_max` are array_like, then the three - arrays will be broadcasted to match their shapes. - out : ndarray, optional - The results will be placed in this array. It may be the input - array for in-place clipping. `out` must be of the right shape - to hold the output. Its type is preserved. - **kwargs - For other keyword-only arguments, see the - :ref:`ufunc docs `. - - Returns - ------- - clipped_array : ndarray - An array with the elements of `a`, but where values - < `a_min` are replaced with `a_min`, and those > `a_max` - with `a_max`. - - See Also - -------- - numpy.clip - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.clip(a_min, a_max, out=out) - - -################################## -# Set routines -################################## - - -@add_boilerplate("ar") -def unique( - ar: ndarray, - return_index: bool = False, - return_inverse: bool = False, - return_counts: bool = False, - axis: Optional[int] = None, -) -> ndarray: - """ - - Find the unique elements of an array. - Returns the sorted unique elements of an array. There are three optional - outputs in addition to the unique elements: - * the indices of the input array that give the unique values - * the indices of the unique array that reconstruct the input array - * the number of times each unique value comes up in the input array - - Parameters - ---------- - ar : array_like - Input array. Unless `axis` is specified, this will be flattened if it - is not already 1-D. - return_index : bool, optional - If True, also return the indices of `ar` (along the specified axis, - if provided, or in the flattened array) that result in the unique - array. - Currently not supported. - return_inverse : bool, optional - If True, also return the indices of the unique array (for the specified - axis, if provided) that can be used to reconstruct `ar`. - Currently not supported. - return_counts : bool, optional - If True, also return the number of times each unique item appears - in `ar`. - Currently not supported. - axis : int or None, optional - The axis to operate on. If None, `ar` will be flattened. If an integer, - the subarrays indexed by the given axis will be flattened and treated - as the elements of a 1-D array with the dimension of the given axis, - see the notes for more details. Object arrays or structured arrays - that contain objects are not supported if the `axis` kwarg is used. The - default is None. - Currently not supported. - - Returns - ------- - unique : ndarray - The sorted unique values. - unique_indices : ndarray, optional - The indices of the first occurrences of the unique values in the - original array. Only provided if `return_index` is True. - unique_inverse : ndarray, optional - The indices to reconstruct the original array from the - unique array. Only provided if `return_inverse` is True. - unique_counts : ndarray, optional - The number of times each of the unique values comes up in the - original array. Only provided if `return_counts` is True. - - See Also - -------- - numpy.unique - - Availability - -------- - Multiple GPUs, Multiple CPUs - - Notes - -------- - Keyword arguments for optional outputs are not yet supported. - `axis` is also not handled currently. - - """ - if _builtin_any((return_index, return_inverse, return_counts, axis)): - raise NotImplementedError( - "Keyword arguments for `unique` are not yet supported" - ) - - return ar.unique() - - -################################## -# Sorting, searching, and counting -################################## - -# Sorting - - -@add_boilerplate("a") -def argsort( - a: ndarray, - axis: Union[int, None] = -1, - kind: SortType = "quicksort", - order: Optional[Union[str, list[str]]] = None, -) -> ndarray: - """ - - Returns the indices that would sort an array. - - Parameters - ---------- - a : array_like - Input array. - axis : int or None, optional - Axis to sort. By default, the index -1 (the last axis) is used. If - None, the flattened array is used. - kind : ``{'quicksort', 'mergesort', 'heapsort', 'stable'}``, optional - Default is 'quicksort'. The underlying sort algorithm might vary. - The code basically supports 'stable' or *not* 'stable'. - order : str or list[str], optional - Currently not supported - - Returns - ------- - index_array : ndarray[int] - Array of indices that sort a along the specified axis. It has the - same shape as `a.shape` or is flattened in case of `axis` is None. - - See Also - -------- - numpy.argsort - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - result = ndarray(a.shape, np.int64) - result._thunk.sort( - rhs=a._thunk, argsort=True, axis=axis, kind=kind, order=order - ) - return result - - -def msort(a: ndarray) -> ndarray: - """ - - Returns a sorted copy of an array sorted along the first axis. - - Parameters - ---------- - a : array_like - Input array. - - Returns - ------- - out : ndarray - Sorted array with same dtype and shape as `a`. - - See Also - -------- - numpy.msort - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return sort(a, axis=0) - - -@add_boilerplate("a") -def searchsorted( - a: ndarray, - v: Union[int, float, ndarray], - side: SortSide = "left", - sorter: Optional[ndarray] = None, -) -> Union[int, ndarray]: - """ - - Find the indices into a sorted array a such that, if the corresponding - elements in v were inserted before the indices, the order of a would be - preserved. - - Parameters - ---------- - a : 1-D array_like - Input array. If sorter is None, then it must be sorted in ascending - order, otherwise sorter must be an array of indices that sort it. - v : scalar or array_like - Values to insert into a. - side : ``{'left', 'right'}``, optional - If 'left', the index of the first suitable location found is given. - If 'right', return the last such index. If there is no suitable index, - return either 0 or N (where N is the length of a). - sorter : 1-D array_like, optional - Optional array of integer indices that sort array a into ascending - order. They are typically the result of argsort. - - Returns - ------- - indices : int or array_like[int] - Array of insertion points with the same shape as v, or an integer - if v is a scalar. - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.searchsorted(v, side, sorter) - - -@add_boilerplate("a") -def sort( - a: ndarray, - axis: Union[int, None] = -1, - kind: SortType = "quicksort", - order: Optional[Union[str, list[str]]] = None, -) -> ndarray: - """ - - Returns a sorted copy of an array. - - Parameters - ---------- - a : array_like - Input array. - axis : int or None, optional - Axis to sort. By default, the index -1 (the last axis) is used. If - None, the flattened array is used. - kind : ``{'quicksort', 'mergesort', 'heapsort', 'stable'}``, optional - Default is 'quicksort'. The underlying sort algorithm might vary. - The code basically supports 'stable' or *not* 'stable'. - order : str or list[str], optional - Currently not supported - - Returns - ------- - out : ndarray - Sorted array with same dtype and shape as `a`. In case `axis` is - None the result is flattened. - - - See Also - -------- - numpy.sort - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - result = ndarray(a.shape, a.dtype) - result._thunk.sort(rhs=a._thunk, axis=axis, kind=kind, order=order) - return result - - -@add_boilerplate("a") -def sort_complex(a: ndarray) -> ndarray: - """ - - Returns a sorted copy of an array sorted along the last axis. Sorts the - real part first, the imaginary part second. - - Parameters - ---------- - a : array_like - Input array. - - Returns - ------- - out : ndarray, complex - Sorted array with same shape as `a`. - - See Also - -------- - numpy.sort_complex - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - result = sort(a) - # force complex result upon return - if np.issubdtype(result.dtype, np.complexfloating): - return result - elif ( - np.issubdtype(result.dtype, np.integer) and result.dtype.itemsize <= 2 - ): - return result.astype(np.complex64, copy=True) - else: - return result.astype(np.complex128, copy=True) - - -# partition - - -@add_boilerplate("a") -def argpartition( - a: ndarray, - kth: Union[int, Sequence[int]], - axis: Union[int, None] = -1, - kind: SelectKind = "introselect", - order: Optional[Union[str, list[str]]] = None, -) -> ndarray: - """ - - Perform an indirect partition along the given axis. - - Parameters - ---------- - a : array_like - Input array. - kth : int or Sequence[int] - axis : int or None, optional - Axis to partition. By default, the index -1 (the last axis) is used. If - None, the flattened array is used. - kind : ``{'introselect'}``, optional - Currently not supported. - order : str or list[str], optional - Currently not supported. - - Returns - ------- - out : ndarray[int] - Array of indices that partitions a along the specified axis. It has the - same shape as `a.shape` or is flattened in case of `axis` is None. - - - Notes - ----- - The current implementation falls back to `cunumeric.argsort`. - - See Also - -------- - numpy.argpartition - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - result = ndarray(a.shape, np.int64) - result._thunk.partition( - rhs=a._thunk, - argpartition=True, - kth=kth, - axis=axis, - kind=kind, - order=order, - ) - return result - - -@add_boilerplate("a") -def partition( - a: ndarray, - kth: Union[int, Sequence[int]], - axis: Union[int, None] = -1, - kind: SelectKind = "introselect", - order: Optional[Union[str, list[str]]] = None, -) -> ndarray: - """ - - Returns a partitioned copy of an array. - - Parameters - ---------- - a : array_like - Input array. - kth : int or Sequence[int] - axis : int or None, optional - Axis to partition. By default, the index -1 (the last axis) is used. If - None, the flattened array is used. - kind : ``{'introselect'}``, optional - Currently not supported. - order : str or list[str], optional - Currently not supported. - - Returns - ------- - out : ndarray - Partitioned array with same dtype and shape as `a`. In case `axis` is - None the result is flattened. - - Notes - ----- - The current implementation falls back to `cunumeric.sort`. - - See Also - -------- - numpy.partition - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - result = ndarray(a.shape, a.dtype) - result._thunk.partition( - rhs=a._thunk, kth=kth, axis=axis, kind=kind, order=order - ) - return result - - -# Searching - - -@add_boilerplate("a") -def argmax( - a: ndarray, - axis: Optional[int] = None, - out: Optional[ndarray] = None, - *, - keepdims: bool = False, -) -> ndarray: - """ - - Returns the indices of the maximum values along an axis. - - Parameters - ---------- - a : array_like - Input array. - axis : int, optional - By default, the index is into the flattened array, otherwise - along the specified axis. - out : ndarray, optional - If provided, the result will be inserted into this array. It should - be of the appropriate shape and dtype. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the array. - - Returns - ------- - index_array : ndarray[int] - Array of indices into the array. It has the same shape as `a.shape` - with the dimension along `axis` removed. - - See Also - -------- - numpy.argmax - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - when the array contains NaN(s). - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.argmax(axis=axis, out=out, keepdims=keepdims) - - -@add_boilerplate("a") -def argmin( - a: ndarray, - axis: Optional[int] = None, - out: Optional[ndarray] = None, - *, - keepdims: bool = False, -) -> ndarray: - """ - - Returns the indices of the minimum values along an axis. - - Parameters - ---------- - a : array_like - Input array. - axis : int, optional - By default, the index is into the flattened array, otherwise - along the specified axis. - out : ndarray, optional - If provided, the result will be inserted into this array. It should - be of the appropriate shape and dtype. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the array. - - Returns - ------- - index_array : ndarray[int] - Array of indices into the array. It has the same shape as `a.shape` - with the dimension along `axis` removed. - - See Also - -------- - numpy.argmin - - Notes - ----- - CuNumeric's parallel implementation may yield different results from NumPy - when the array contains NaN(s). - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.argmin(axis=axis, out=out, keepdims=keepdims) - - -# Counting - - -@add_boilerplate("a") -def count_nonzero( - a: ndarray, axis: Optional[Union[int, tuple[int, ...]]] = None -) -> Union[int, ndarray]: - """ - - Counts the number of non-zero values in the array ``a``. - - Parameters - ---------- - a : array_like - The array for which to count non-zeros. - axis : int or tuple, optional - Axis or tuple of axes along which to count non-zeros. - Default is None, meaning that non-zeros will be counted - along a flattened version of ``a``. - - Returns - ------- - count : int or ndarray[int] - Number of non-zero values in the array along a given axis. - Otherwise, the total number of non-zero values in the array - is returned. - - See Also - -------- - numpy.count_nonzero - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a._count_nonzero(axis) - - -############ -# Statistics -############ - -# Averages and variances - - -@add_boilerplate("a") -def mean( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Compute the arithmetic mean along the specified axis. - - Returns the average of the array elements. The average is taken over - the flattened array by default, otherwise over the specified axis. - `float64` intermediate and return values are used for integer inputs. - - Parameters - ---------- - a : array_like - Array containing numbers whose mean is desired. If `a` is not an - array, a conversion is attempted. - axis : None or int or tuple[int], optional - Axis or axes along which the means are computed. The default is to - compute the mean of the flattened array. - - If this is a tuple of ints, a mean is performed over multiple axes, - instead of a single axis or all the axes as before. - dtype : data-type, optional - Type to use in computing the mean. For integer inputs, the default - is `float64`; for floating point inputs, it is the same as the - input dtype. - out : ndarray, optional - Alternate output array in which to place the result. The default - is ``None``; if provided, it must have the same shape as the - expected output, but the type will be cast if necessary. - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - If the default value is passed, then `keepdims` will not be - passed through to the `mean` method of sub-classes of - `ndarray`, however any non-default value will be. If the - sub-class' method does not implement `keepdims` any - exceptions will be raised. - - where : array_like of bool, optional - Elements to include in the mean. - - Returns - ------- - m : ndarray - If `out is None`, returns a new array of the same dtype a above - containing the mean values, otherwise a reference to the output - array is returned. - - See Also - -------- - numpy.mean - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.mean( - axis=axis, dtype=dtype, out=out, keepdims=keepdims, where=where - ) - - -@add_boilerplate("a") -def nanmean( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - keepdims: bool = False, - where: Optional[ndarray] = None, -) -> ndarray: - """ - - Compute the arithmetic mean along the specified axis, ignoring NaNs. - - Returns the average of the array elements. The average is taken over - the flattened array by default, otherwise over the specified axis. - `float64` intermediate and return values are used for integer inputs. - - Parameters - ---------- - a : array_like - Array containing numbers whose mean is desired. If `a` is not an - array, a conversion is attempted. - axis : None or int or tuple[int], optional - Axis or axes along which the means are computed. The default is to - compute the mean of the flattened array. - - If this is a tuple of ints, a mean is performed over multiple axes, - instead of a single axis or all the axes as before. - dtype : data-type, optional - Type to use in computing the mean. For integer inputs, the default - is `float64`; for floating point inputs, it is the same as the - input dtype. - out : ndarray, optional - Alternate output array in which to place the result. The default - is ``None``; if provided, it must have the same shape as the - expected output, but the type will be cast if necessary. - See `ufuncs-output-type` for more details. - - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - - - where : array_like of bool, optional - Elements to include in the mean. - - Returns - ------- - m : ndarray - If `out is None`, returns a new array of the same dtype as a above - containing the mean values, otherwise a reference to the output - array is returned. - - See Also - -------- - numpy.nanmean - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a._nanmean( - axis=axis, dtype=dtype, out=out, keepdims=keepdims, where=where - ) - - -@add_boilerplate("a") -def var( - a: ndarray, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, - ddof: int = 0, - keepdims: bool = False, - *, - where: Union[ndarray, None] = None, -) -> ndarray: - """ - Compute the variance along the specified axis. - - Returns the variance of the array elements, a measure of the spread of - a distribution. The variance is computed for the flattened array - by default, otherwise over the specified axis. - - Parameters - ---------- - a : array_like - Array containing numbers whose variance is desired. If `a` is not an - array, a conversion is attempted. - axis : None or int or tuple[int], optional - Axis or axes along which the variance is computed. The default is to - compute the variance of the flattened array. - - If this is a tuple of ints, a variance is performed over multiple axes, - instead of a single axis or all the axes as before. - dtype : data-type, optional - Type to use in computing the variance. For arrays of integer type - the default is float64; for arrays of float types - it is the same as the array type. - out : ndarray, optional - Alternate output array in which to place the result. It must have the - same shape as the expected output, but the type is cast if necessary. - ddof : int, optional - “Delta Degrees of Freedom”: the divisor used in the calculation is - N - ddof, where N represents the number of elements. By default - ddof is zero. - keepdims : bool, optional - If this is set to True, the axes which are reduced are left - in the result as dimensions with size one. With this option, - the result will broadcast correctly against the input array. - where : array_like of bool, optional - A boolean array which is broadcasted to match the dimensions of array, - and selects elements to include in the reduction. - - Returns - ------- - m : ndarray, see dtype parameter above - If `out=None`, returns a new array of the same dtype as above - containing the variance values, otherwise a reference to the output - array is returned. - - See Also - -------- - numpy.var - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - return a.var( - axis=axis, - dtype=dtype, - out=out, - ddof=ddof, - keepdims=keepdims, - where=where, - ) - - -# Histograms - - -@add_boilerplate("x", "weights") -def bincount( - x: ndarray, weights: Optional[ndarray] = None, minlength: int = 0 -) -> ndarray: - """ - bincount(x, weights=None, minlength=0) - - Count number of occurrences of each value in array of non-negative ints. - - The number of bins (of size 1) is one larger than the largest value in - `x`. If `minlength` is specified, there will be at least this number - of bins in the output array (though it will be longer if necessary, - depending on the contents of `x`). - Each bin gives the number of occurrences of its index value in `x`. - If `weights` is specified the input array is weighted by it, i.e. if a - value ``n`` is found at position ``i``, ``out[n] += weight[i]`` instead - of ``out[n] += 1``. - - Parameters - ---------- - x : array_like - 1-D input array of non-negative ints. - weights : array_like, optional - Weights, array of the same shape as `x`. - minlength : int, optional - A minimum number of bins for the output array. - - Returns - ------- - out : ndarray[int] - The result of binning the input array. - The length of `out` is equal to ``cunumeric.amax(x)+1``. - - Raises - ------ - ValueError - If the input is not 1-dimensional, or contains elements with negative - values, or if `minlength` is negative. - TypeError - If the type of the input is float or complex. - - See Also - -------- - numpy.bincount - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if x.ndim != 1: - raise ValueError("the input array must be 1-dimensional") - if weights is not None: - if weights.shape != x.shape: - raise ValueError("weights array must be same shape for bincount") - if weights.dtype.kind == "c": - raise ValueError("weights must be convertible to float64") - # Make sure the weights are float64 - weights = weights.astype(np.float64) - if not np.issubdtype(x.dtype, np.integer): - raise TypeError("input array for bincount must be integer type") - if minlength < 0: - raise ValueError("'minlength' must not be negative") - # Note that the following are non-blocking operations, - # though passing their results to `int` is blocking - max_val, min_val = amax(x), amin(x) - if int(min_val) < 0: - raise ValueError("the input array must have no negative elements") - minlength = _builtin_max(minlength, int(max_val) + 1) - if x.size == 1: - # Handle the special case of 0-D array - if weights is None: - out = zeros((minlength,), dtype=np.dtype(np.int64)) - # TODO: Remove this "type: ignore" once @add_boilerplate can - # propagate "ndarray -> ndarray | npt.ArrayLike" in wrapped sigs - out[x[0]] = 1 # type: ignore [assignment] - else: - out = zeros((minlength,), dtype=weights.dtype) - index = x[0] - out[index] = weights[0] - else: - # Normal case of bincount - if weights is None: - out = ndarray( - (minlength,), - dtype=np.dtype(np.int64), - inputs=(x, weights), - ) - out._thunk.bincount(x._thunk) - else: - out = ndarray( - (minlength,), - dtype=weights.dtype, - inputs=(x, weights), - ) - out._thunk.bincount(x._thunk, weights=weights._thunk) - return out - - -# Quantiles - - -# account for 0-based indexing -# there's no negative numbers -# arithmetic at this level, -# (pos, k) are always positive! -# -def floor_i(k: int | float) -> int: - j = k - 1 if k > 0 else 0 - return int(j) - - -# Generic rule: if `q` input value falls onto a node, then return that node - - -# Discontinuous methods: -# -# 'inverted_cdf' -# q = quantile input \in [0, 1] -# n = sizeof(array) -# -def inverted_cdf(q: float, n: int) -> tuple[float, int]: - pos = q * n - k = math.floor(pos) - - g = pos - k - gamma = 1.0 if g > 0 else 0.0 - - j = int(k) - 1 - if j < 0: - return (0.0, 0) - else: - return (gamma, j) - - -# 'averaged_inverted_cdf' -# -def averaged_inverted_cdf(q: float, n: int) -> tuple[float, int]: - pos = q * n - k = math.floor(pos) - - g = pos - k - gamma = 1.0 if g > 0 else 0.5 - - j = int(k) - 1 - if j < 0: - return (0.0, 0) - elif j >= n - 1: - return (1.0, n - 2) - else: - return (gamma, j) - - -# 'closest_observation' -# -def closest_observation(q: float, n: int) -> tuple[float, int]: - # p = q*n - 0.5 - # pos = 0 if p < 0 else p - - # weird departure from paper - # (bug?), but this fixes it: - # also, j even in original paper - # applied to 1-based indexing; we have 0-based! - # numpy impl. doesn't account that the original paper used - # 1-based indexing, 0-based j is still checked for evennes! - # (see proof in quantile_policies.py) - # - p0 = q * n - 0.5 - p = p0 - 1.0 - - pos = 0 if p < 0 else p0 - k = math.floor(pos) - - j = floor_i(k) - gamma = 1 if k < pos else (0 if j % 2 == 0 else 1) - - return (gamma, j) - - -# Continuous methods: -# -# Parzen method: -# 'interpolated_inverted_cdf' -# -def interpolated_inverted_cdf(q: float, n: int) -> tuple[float, int]: - pos = q * n - k = math.floor(pos) - # gamma = pos-k - # this fixes it: - # - gamma = 0.0 if k == 0 else pos - k - j = floor_i(k) - return (gamma, j) - - -# Hazen method: -# 'hazen' -# -def hazen(q: float, n: int) -> tuple[float, int]: - pos = q * n + 0.5 - k = math.floor(pos) - # gamma = pos-k - # - # this fixes it: - # (when pos > n: this actually selects the right point, - # which is the correct choice, because right = arr[n] - # gets invalidated) - # - gamma = 0.0 if (pos < 1 or pos > n) else pos - k - - j = floor_i(k) - return (gamma, j) - - -# Weibull method: -# 'weibull' -# -def weibull(q: float, n: int) -> tuple[float, int]: - pos = q * (n + 1) - - k = math.floor(pos) - # gamma = pos-k - # - # this fixes it: - # (when pos > n: this actually selects the right point, - # which is the correct choice, because right = arr[n] - # gets invalidated) - # - gamma = 0.0 if (pos < 1 or pos > n) else pos - k - - j = floor_i(k) - - if j >= n: - j = n - 1 - - return (gamma, j) - - -# Gumbel method: -# 'linear' -# -def linear(q: float, n: int) -> tuple[float, int]: - pos = q * (n - 1) + 1 - k = math.floor(pos) - # gamma = pos-k - # - # this fixes it: - # (when pos > n: this actually selects the right point, - # which is the correct choice, because right = arr[n] - # gets invalidated) - # - gamma = 0.0 if (pos < 1 or pos > n) else pos - k - - j = floor_i(k) - return (gamma, j) - - -# Johnson & Kotz method: -# 'median_unbiased' -# -def median_unbiased(q: float, n: int) -> tuple[float, int]: - fract = 1.0 / 3.0 - pos = q * (n + fract) + fract - k = math.floor(pos) - - # gamma = pos-k - # - # this fixes it: - # (when pos > n: this actually selects the right point, - # which is the correct choice, because right = arr[n] - # gets invalidated) - # - gamma = 0.0 if (pos < 1 or pos > n) else pos - k - - j = floor_i(k) - return (gamma, j) - - -# Blom method: -# 'normal_unbiased' -# -def normal_unbiased(q: float, n: int) -> tuple[float, int]: - fract1 = 0.25 - fract2 = 3.0 / 8.0 - pos = q * (n + fract1) + fract2 - k = math.floor(pos) - - # gamma = pos-k - # - # this fixes it: - # (when pos > n: this actually selects the right point, - # which is the correct choice, because right = arr[n] - # gets invalidated) - # - gamma = 0.0 if (pos < 1 or pos > n) else pos - k - - j = floor_i(k) - return (gamma, j) - - -# `lower` -# -def lower(q: float, n: int) -> tuple[float, int]: - gamma = 0.0 - pos = q * (n - 1) - k = math.floor(pos) - - j = int(k) - return (gamma, j) - - -# `higher` -# -def higher(q: float, n: int) -> tuple[float, int]: - pos = q * (n - 1) - k = math.floor(pos) - - # Generic rule: (k == pos) - gamma = 0.0 if (pos == 0 or k == pos) else 1.0 - - j = int(k) - return (gamma, j) - - -# `midpoint` -# -def midpoint(q: float, n: int) -> tuple[float, int]: - pos = q * (n - 1) - k = math.floor(pos) - - # Generic rule: (k == pos) - gamma = 0.0 if (pos == 0 or k == pos) else 0.5 - - j = int(k) - return (gamma, j) - - -# `nearest` -# -def nearest(q: float, n: int) -> tuple[float, int]: - pos = q * (n - 1) - - # k = floor(pos) - # gamma = 1.0 if pos - k >= 0.5 else 0.0 - - k = np.round(pos) - gamma = 0.0 - - j = int(k) - return (gamma, j) - - -# for the case when axis = tuple (non-singleton) -# reshuffling might have to be done (if tuple is non-consecutive) -# and the src array must be collapsed along that set of axes -# -# args: -# -# arr: [in] source nd-array on which quantiles are calculated; -# axes_set: [in] tuple or list of axes (indices less than arr dimension); -# -# return: pair: (minimal_index, reshuffled_and_collapsed source array) -def reshuffle_reshape( - arr: ndarray, axes_set: Iterable[int] -) -> tuple[int, ndarray]: - ndim = len(arr.shape) - - sorted_axes = tuple(sorted(axes_set)) - - min_dim_index = sorted_axes[0] - num_axes = len(sorted_axes) - reshuffled_axes = tuple(range(min_dim_index, min_dim_index + num_axes)) - - non_consecutive = sorted_axes != reshuffled_axes - if non_consecutive: - arr_shuffled = moveaxis(arr, sorted_axes, reshuffled_axes) - else: - arr_shuffled = arr - - # shape_reshuffled = arr_shuffled.shape # debug - collapsed_shape = np.prod([arr_shuffled.shape[i] for i in reshuffled_axes]) - - redimed = tuple(range(0, min_dim_index + 1)) + tuple( - range(min_dim_index + num_axes, ndim) - ) - reshaped = tuple( - [ - collapsed_shape if k == min_dim_index else arr_shuffled.shape[k] - for k in redimed - ] - ) - - arr_reshaped = arr_shuffled.reshape(reshaped) - return (min_dim_index, arr_reshaped) - - -# args: -# -# arr: [in] source nd-array on which quantiles are calculated; -# preccondition: assumed sorted! -# q_arr: [in] quantile input values nd-array; -# axis: [in] axis along which quantiles are calculated; -# method: [in] func(q, n) returning (gamma, j), -# where = array1D.size; -# keepdims: [in] boolean flag specifying whether collapsed axis -# should be kept as dim=1; -# to_dtype: [in] dtype to convert the result to; -# qs_all: [in/out] result pass through or created (returned) -# -def quantile_impl( - arr: ndarray, - q_arr: npt.NDArray[Any], - axis: Optional[int], - axes_set: Iterable[int], - original_shape: tuple[int, ...], - method: Callable[[float, int], tuple[float, int]], - keepdims: bool, - to_dtype: np.dtype[Any], - qs_all: Optional[ndarray], -) -> ndarray: - ndims = len(arr.shape) - - if axis is None: - n = arr.size - - if keepdims: - remaining_shape = (1,) * len(original_shape) - else: - remaining_shape = () # only `q_arr` dictates shape; - # quantile applied to `arr` seen as 1D; - else: - n = arr.shape[axis] - - # arr.shape -{axis}; if keepdims use 1 for arr.shape[axis]: - # (can be empty []) - # - if keepdims: - remaining_shape = tuple( - 1 if k in axes_set else original_shape[k] - for k in range(0, len(original_shape)) - ) - else: - remaining_shape = tuple( - arr.shape[k] for k in range(0, ndims) if k != axis - ) - - # compose qarr.shape with arr.shape: - # - # result.shape = (q_arr.shape, arr.shape -{axis}): - # - qresult_shape = (*q_arr.shape, *remaining_shape) - - # construct result NdArray, non-flattening approach: - # - if qs_all is None: - qs_all = zeros(qresult_shape, dtype=to_dtype) - else: - # implicit conversion from to_dtype to qs_all.dtype assumed - # - if qs_all.shape != qresult_shape: - raise ValueError("wrong shape on output array") - - for index, q in np.ndenumerate(q_arr): - (gamma, j) = method(q, n) - (left_pos, right_pos) = (j, j + 1) - - # (N-1) dimensional ndarray of left, right - # neighbor values: - # - # non-flattening approach: - # - # extract values at index=left_pos; - arr_1D_lvals = arr.take(left_pos, axis) - arr_vals_shape = arr_1D_lvals.shape - - if right_pos >= n: - # some quantile methods may result in j==(n-1), - # hence (j+1) could surpass array boundary; - # - arr_1D_rvals = zeros(arr_vals_shape, dtype=arr_1D_lvals.dtype) - else: - # extract values at index=right_pos; - arr_1D_rvals = arr.take(right_pos, axis) - - # vectorized for axis != None; - # (non-flattening approach) - # - if len(index) == 0: - left = (1.0 - gamma) * arr_1D_lvals.reshape(qs_all.shape) - right = gamma * arr_1D_rvals.reshape(qs_all.shape) - qs_all[...] = left + right - else: - left = (1.0 - gamma) * arr_1D_lvals.reshape(qs_all[index].shape) - right = gamma * arr_1D_rvals.reshape(qs_all[index].shape) - qs_all[index] = left + right - - return qs_all - - -@add_boilerplate("a") -def quantile( - a: ndarray, - q: Union[float, Iterable[float], ndarray], - axis: Union[None, int, tuple[int, ...]] = None, - out: Optional[ndarray] = None, - overwrite_input: bool = False, - method: str = "linear", - keepdims: bool = False, -) -> ndarray: - """ - Compute the q-th quantile of the data along the specified axis. - - Parameters - ---------- - a : array_like - Input array or object that can be converted to an array. - q : array_like of float - Quantile or sequence of quantiles to compute, which must be between - 0 and 1 inclusive. - axis : {int, tuple of int, None}, optional - Axis or axes along which the quantiles are computed. The default is - to compute the quantile(s) along a flattened version of the array. - out : ndarray, optional - Alternative output array in which to place the result. It must have - the same shape as the expected output. - overwrite_input : bool, optional - If True, then allow the input array `a` to be modified by - intermediate calculations, to save memory. In this case, the - contents of the input `a` after this function completes is - undefined. - method : str, optional - This parameter specifies the method to use for estimating the - quantile. The options sorted by their R type - as summarized in the H&F paper [1]_ are: - 1. 'inverted_cdf' - 2. 'averaged_inverted_cdf' - 3. 'closest_observation' - 4. 'interpolated_inverted_cdf' - 5. 'hazen' - 6. 'weibull' - 7. 'linear' (default) - 8. 'median_unbiased' - 9. 'normal_unbiased' - The first three methods are discontinuous. NumPy further defines the - following discontinuous variations of the default 'linear' (7.) option: - * 'lower' - * 'higher', - * 'midpoint' - * 'nearest' - keepdims : bool, optional - If this is set to True, the axes which are reduced are left in - the result as dimensions with size one. With this option, the - result will broadcast correctly against the original array `a`. - - Returns - ------- - quantile : scalar or ndarray - If `q` is a single quantile and `axis=None`, then the result - is a scalar. If multiple quantiles are given, first axis of - the result corresponds to the quantiles. The other axes are - the axes that remain after the reduction of `a`. If the input - contains integers or floats smaller than ``float64``, the output - data-type is ``float64``. Otherwise, the output data-type is the - same as that of the input. If `out` is specified, that array is - returned instead. - - Raises - ------ - TypeError - If the type of the input is complex. - - See Also - -------- - numpy.quantile - - Availability - -------- - Multiple GPUs, Multiple CPUs - - References - ---------- - .. [1] R. J. Hyndman and Y. Fan, - "Sample quantiles in statistical packages," - The American Statistician, 50(4), pp. 361-365, 1996 - """ - - dict_methods = { - "inverted_cdf": inverted_cdf, - "averaged_inverted_cdf": averaged_inverted_cdf, - "closest_observation": closest_observation, - "interpolated_inverted_cdf": interpolated_inverted_cdf, - "hazen": hazen, - "weibull": weibull, - "linear": linear, - "median_unbiased": median_unbiased, - "normal_unbiased": normal_unbiased, - "lower": lower, - "higher": higher, - "midpoint": midpoint, - "nearest": nearest, - } - - real_axis: Optional[int] - axes_set: Iterable[int] = [] - original_shape = a.shape - - if axis is not None and isinstance(axis, Iterable): - if len(axis) == 1: - real_axis = axis[0] - a_rr = a - else: - (real_axis, a_rr) = reshuffle_reshape(a, axis) - # What happens with multiple axes and overwrite_input = True ? - # It seems overwrite_input is reset to False; - overwrite_input = False - axes_set = axis - else: - real_axis = axis - a_rr = a - if real_axis is not None: - axes_set = [real_axis] - - # covers both array-like and scalar cases: - # - q_arr = np.asarray(q) - - # in the future k-sort (partition) - # might be faster, for now it uses sort - # arr = partition(arr, k = floor(nq), axis = real_axis) - # but that would require a k-sort call for each `q`! - # too expensive for many `q` values... - # if no axis given then elements are sorted as a 1D array - # - if overwrite_input: - a_rr.sort(axis=real_axis) - arr = a_rr - else: - arr = sort(a_rr, axis=real_axis) - - if arr.dtype.kind == "c": - raise TypeError("input array cannot be of complex type") - - # return type dependency on arr.dtype: - # - # it depends on interpolation method; - # For discontinuous methods returning either end of the interval within - # which the quantile falls, or the other; arr.dtype is returned; - # else, logic below: - # - # if is_float(arr_dtype) && (arr.dtype >= dtype('float64')) then - # arr.dtype - # else - # dtype('float64') - # - # see https://github.com/numpy/numpy/issues/22323 - # - if method in [ - "inverted_cdf", - "closest_observation", - "lower", - "higher", - "nearest", - ]: - to_dtype = arr.dtype - else: - to_dtype = np.dtype("float64") - - # in case dtype("float128") becomes supported: - # - # to_dtype = ( - # arr.dtype - # if (arr.dtype == np.dtype("float128")) - # else np.dtype("float64") - # ) - - res = quantile_impl( - arr, - q_arr, - real_axis, - axes_set, - original_shape, - dict_methods[method], - keepdims, - to_dtype, - out, - ) - - if out is not None: - # out = res.astype(out.dtype) -- conversion done inside impl - return out - else: - return res - - -@add_boilerplate("a") -def percentile( - a: ndarray, - q: Union[float, Iterable[float], ndarray], - axis: Union[None, int, tuple[int, ...]] = None, - out: Optional[ndarray] = None, - overwrite_input: bool = False, - method: str = "linear", - keepdims: bool = False, -) -> ndarray: - """ - Compute the q-th percentile of the data along the specified axis. - - Parameters - ---------- - a : array_like - Input array or object that can be converted to an array. - q : array_like of float - Percentile or sequence of percentiles to compute, which must be between - 0 and 100 inclusive. - axis : {int, tuple of int, None}, optional - Axis or axes along which the percentiles are computed. The default is - to compute the percentile(s) along a flattened version of the array. - out : ndarray, optional - Alternative output array in which to place the result. It must have - the same shape as the expected output. - overwrite_input : bool, optional - If True, then allow the input array `a` to be modified by - intermediate calculations, to save memory. In this case, the - contents of the input `a` after this function completes is - undefined. - method : str, optional - This parameter specifies the method to use for estimating the - percentile. The options sorted by their R type - as summarized in the H&F paper [1]_ are: - 1. 'inverted_cdf' - 2. 'averaged_inverted_cdf' - 3. 'closest_observation' - 4. 'interpolated_inverted_cdf' - 5. 'hazen' - 6. 'weibull' - 7. 'linear' (default) - 8. 'median_unbiased' - 9. 'normal_unbiased' - The first three methods are discontinuous. NumPy further defines the - following discontinuous variations of the default 'linear' (7.) option: - * 'lower' - * 'higher', - * 'midpoint' - * 'nearest' - keepdims : bool, optional - If this is set to True, the axes which are reduced are left in - the result as dimensions with size one. With this option, the - result will broadcast correctly against the original array `a`. - - Returns - ------- - percentile : scalar or ndarray - If `q` is a single percentile and `axis=None`, then the result - is a scalar. If multiple percentiles are given, first axis of - the result corresponds to the percentiles. The other axes are - the axes that remain after the reduction of `a`. If the input - contains integers or floats smaller than ``float64``, the output - data-type is ``float64``. Otherwise, the output data-type is the - same as that of the input. If `out` is specified, that array is - returned instead. - - Raises - ------ - TypeError - If the type of the input is complex. - - See Also - -------- - numpy.percentile - - Availability - -------- - Multiple GPUs, Multiple CPUs - - References - ---------- - .. [1] R. J. Hyndman and Y. Fan, - "Sample quantiles in statistical packages," - The American Statistician, 50(4), pp. 361-365, 1996 - """ - - q_arr = np.asarray(q) - q01 = q_arr / 100.0 - - return quantile( - a, - q01, - axis, - out=out, - overwrite_input=overwrite_input, - method=method, - keepdims=keepdims, - ) - - -@add_boilerplate("x", "weights") -def histogram( - x: ndarray, - bins: Union[ndarray, npt.ArrayLike, int] = 10, - range: Optional[Union[tuple[int, int], tuple[float, float]]] = None, - weights: Optional[ndarray] = None, - density: bool = False, -) -> tuple[ndarray, ndarray]: - """ - Compute the histogram of a dataset. - - Parameters - ---------- - a : array_like - Input data. The histogram is computed over the flattened array. - bins : int or sequence of scalars, optional - If `bins` is an int, it defines the number of equal-width bins in the - given range (10, by default). If `bins` is a sequence, it defines a - monotonically increasing array of bin edges, including the rightmost - edge, allowing for non-uniform bin widths. - range : (float, float), optional - The lower and upper range of the bins. If not provided, range is simply - ``(a.min(), a.max())``. Values outside the range are ignored. The first - element of the range must be smaller than the second. This argument is - ignored when bin edges are provided explicitly. - weights : array_like, optional - An array of weights, of the same shape as `a`. Each value in `a` only - contributes its associated weight towards the bin count (instead of 1). - If `density` is True, the weights are normalized, so that the integral - of the density over the range remains 1. - density : bool, optional - If ``False``, the result will contain the number of samples in each - bin. If ``True``, the result is the value of the probability *density* - function at the bin, normalized such that the *integral* over the range - is 1. Note that the sum of the histogram values will not be equal to 1 - unless bins of unity width are chosen; it is not a probability *mass* - function. - - Returns - ------- - hist : array - The values of the histogram. See `density` and `weights` for a - description of the possible semantics. - bin_edges : array - Return the bin edges ``(length(hist)+1)``. - - See Also - -------- - numpy.histogram - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - result_type: np.dtype[Any] = np.dtype(np.int64) - - if np.ndim(bins) > 1: - raise ValueError("`bins` must be 1d, when an array") - - # check isscalar(bins): - # - if np.ndim(bins) == 0: - if not isinstance(bins, int): - raise TypeError("`bins` must be array or integer type") - - num_intervals = bins - - if range is not None: - assert isinstance(range, tuple) and len(range) == 2 - if range[0] >= range[1]: - raise ValueError( - "`range` must be a pair of increasing values." - ) - - lower_b = range[0] - higher_b = range[1] - elif x.size == 0: - lower_b = 0.0 - higher_b = 1.0 - else: - lower_b = float(min(x)) - higher_b = float(max(x)) - - step = (higher_b - lower_b) / num_intervals - - bins_array = asarray( - [lower_b + k * step for k in _builtin_range(0, num_intervals)] - + [higher_b], - dtype=np.dtype(np.float64), - ) - - bins_orig_type = bins_array.dtype - else: - bins_as_arr = asarray(bins) - bins_orig_type = bins_as_arr.dtype - - bins_array = bins_as_arr.astype(np.dtype(np.float64)) - num_intervals = bins_array.shape[0] - 1 - - if not all((bins_array[1:] - bins_array[:-1]) >= 0): - raise ValueError( - "`bins` must increase monotonically, when an array" - ) - - if x.ndim != 1: - x = x.flatten() - - if weights is not None: - if weights.shape != x.shape: - raise ValueError( - "`weights` array must be same shape for histogram" - ) - - result_type = weights.dtype - weights_array = weights.astype(np.dtype(np.float64)) - else: - # case weights == None cannot be handled inside _thunk.histogram, - # bc/ of hist ndarray inputs(), below; - # needs to be handled here: - # - weights_array = ones(x.shape, dtype=np.dtype(np.float64)) - - if x.size == 0: - return ( - zeros((num_intervals,), dtype=result_type), - bins_array.astype(bins_orig_type), - ) - - hist = ndarray( - (num_intervals,), - dtype=weights_array.dtype, - inputs=(x, bins_array, weights_array), - ) - hist._thunk.histogram( - x._thunk, bins_array._thunk, weights=weights_array._thunk - ) - - # handle (density = True): - # - if density: - result_type = np.dtype(np.float64) - hist /= sum(hist) - hist /= bins_array[1:] - bins_array[:-1] - - return hist.astype(result_type), bins_array.astype(bins_orig_type) diff --git a/cunumeric/random/legacy.py b/cunumeric/random/legacy.py deleted file mode 100644 index 4070653e8..000000000 --- a/cunumeric/random/legacy.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright 2021-2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Union - -import numpy as np -import numpy.random as nprandom - -from cunumeric.array import ndarray -from cunumeric.runtime import runtime - -if TYPE_CHECKING: - import numpy.typing as npt - - from ..types import NdShapeLike - - -def seed(init: Union[int, None] = None) -> None: - if init is None: - init = 0 - runtime.set_next_random_epoch(int(init)) - - -def rand(*shapeargs: int) -> Union[float, ndarray]: - """ - rand(d0, d1, ..., dn) - - Random values in a given shape. - - Create an array of the given shape and populate it with random samples from - a uniform distribution over ``[0, 1)``. - - Parameters - ---------- - d0, d1, ..., dn : int, optional - The dimensions of the returned array, must be non-negative. - If no argument is given a single Python float is returned. - - Returns - ------- - out : ndarray, shape ``(d0, d1, ..., dn)`` - Random values. - - See Also - -------- - numpy.random.rand - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if shapeargs == (): - return nprandom.rand() - result = ndarray(shapeargs, dtype=np.dtype(np.float64)) - result._thunk.random_uniform() - return result - - -def randint( - low: int, - high: Union[int, None] = None, - size: Union[NdShapeLike, None] = None, - dtype: Union[np.dtype[Any], type, None] = int, -) -> Union[int, ndarray, npt.NDArray[Any]]: - """ - Return random integers from `low` (inclusive) to `high` (exclusive). - - Parameters - ---------- - low : int or array_like[int] - Lowest (signed) integers to be drawn from the distribution (unless - ``high=None``, in which case this parameter is one above the - *highest* such integer). - high : int or array_like[int], optional - If provided, one above the largest (signed) integer to be drawn - from the distribution (see above for behavior if ``high=None``). - If array-like, must contain integer values - size : int or tuple[int], optional - Output shape. If the given shape is, e.g., ``(m, n, k)``, then - ``m * n * k`` samples are drawn. Default is None, in which case a - single value is returned. - dtype : data-type, optional - Desired dtype of the result. Byteorder must be native. - The default value is int. - - Returns - ------- - out : int or ndarray[int] - `size`-shaped array of random integers from the appropriate - distribution, or a single such random int if `size` not provided. - - See Also - -------- - numpy.random.randint - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if not isinstance(low, int): - raise NotImplementedError("'low' must be an integer") - if high is not None and not isinstance(high, int): - raise NotImplementedError("'high' must be an integer or None") - - if size is None: - return nprandom.randint(low=low, high=high, size=size, dtype=dtype) - - if dtype is not None: - dtype = np.dtype(dtype) - else: - dtype = np.dtype(np.int64) - # TODO: randint must support unsigned integer dtypes as well - if dtype.kind != "i": - raise NotImplementedError( - "cunumeric.random.randint must be given an integer dtype" - ) - if isinstance(size, int): - size = (size,) - result = ndarray(size, dtype=dtype) - if high is None: - if low <= 0: - raise ValueError( - "bound must be strictly greater than 0 for randint" - ) - result._thunk.random_integer(low=0, high=low) - else: - if low >= high: - raise ValueError( - "'high' bound must be strictly greater than 'low' " - "bound for randint" - ) - result._thunk.random_integer(low=low, high=high) - return result - - -def randn(*shapeargs: int) -> Union[float, ndarray]: - """ - randn(d0, d1, ..., dn) - - Return a sample (or samples) from the "standard normal" distribution. - - Parameters - ---------- - d0, d1, ..., dn : int, optional - The dimensions of the returned array, must be non-negative. - If no argument is given a single Python float is returned. - - Returns - ------- - Z : ndarray or float - A ``(d0, d1, ..., dn)``-shaped array of floating-point samples from - the standard normal distribution, or a single such float if - no parameters were supplied. - - See Also - -------- - numpy.random.randn - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - - if shapeargs == (): - return nprandom.randn() - result = ndarray(shapeargs, dtype=np.dtype(np.float64)) - result._thunk.random_normal() - return result - - -def random(size: Union[NdShapeLike, None] = None) -> Union[float, ndarray]: - """ - random(size=None) - - Return random floats in the half-open interval [0.0, 1.0). - - See Also - -------- - numpy.random.random - - Availability - -------- - Multiple GPUs, Multiple CPUs - """ - if size is None: - return nprandom.random() - result = ndarray(size, dtype=np.dtype(np.float64)) - result._thunk.random_uniform() - return result diff --git a/cunumeric_cpp.cmake b/cunumeric_cpp.cmake deleted file mode 100644 index be5c0fbe6..000000000 --- a/cunumeric_cpp.cmake +++ /dev/null @@ -1,505 +0,0 @@ -#============================================================================= -# Copyright 2022 NVIDIA Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#============================================================================= - -############################################################################## -# - User Options ------------------------------------------------------------ - -option(BUILD_SHARED_LIBS "Build cuNumeric shared libraries" ON) -option(cunumeric_EXCLUDE_TBLIS_FROM_ALL "Exclude tblis targets from cuNumeric's 'all' target" OFF) -option(cunumeric_EXCLUDE_OPENBLAS_FROM_ALL "Exclude OpenBLAS targets from cuNumeric's 'all' target" OFF) -option(cunumeric_EXCLUDE_LEGATE_CORE_FROM_ALL "Exclude legate.core targets from cuNumeric's 'all' target" OFF) - -############################################################################## -# - Project definition ------------------------------------------------------- - -# Write the version header -rapids_cmake_write_version_file(include/cunumeric/version_config.hpp) - -# Needed to integrate with LLVM/clang tooling -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -############################################################################## -# - Build Type --------------------------------------------------------------- - -# Set a default build type if none was specified -rapids_cmake_build_type(Release) - -############################################################################## -# - conda environment -------------------------------------------------------- - -rapids_cmake_support_conda_env(conda_env MODIFY_PREFIX_PATH) - -# We're building python extension libraries, which must always be installed -# under lib/, even if the system normally uses lib64/. Rapids-cmake currently -# doesn't realize this when we're going through scikit-build, see -# https://github.com/rapidsai/rapids-cmake/issues/426 -if(TARGET conda_env) - set(CMAKE_INSTALL_LIBDIR "lib") -endif() - -############################################################################## -# - Dependencies ------------------------------------------------------------- - -# add third party dependencies using CPM -rapids_cpm_init(OVERRIDE ${CMAKE_CURRENT_SOURCE_DIR}/cmake/versions.json) - -find_package(OpenMP) - -option(Legion_USE_CUDA "Use CUDA" ON) -option(Legion_USE_OpenMP "Use OpenMP" ${OpenMP_FOUND}) -option(Legion_BOUNDS_CHECKS "Build cuNumeric with bounds checks (expensive)" OFF) - -### -# If we find legate.core already configured on the system, it will report -# whether it was compiled with bounds checking (Legion_BOUNDS_CHECKS), -# CUDA (Legion_USE_CUDA), and OpenMP (Legion_USE_OpenMP). -# -# We use the same variables as legate.core because we want to enable/disable -# each of these features based on how legate.core was configured (it doesn't -# make sense to build cuNumeric's CUDA bindings if legate.core wasn't built -# with CUDA support). -### -include(cmake/thirdparty/get_legate_core.cmake) - -if(Legion_USE_CUDA) - include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/cuda_arch_helpers.cmake) - # Needs to run before `rapids_cuda_init_architectures` - set_cuda_arch_from_names() - # Needs to run before `enable_language(CUDA)` - rapids_cuda_init_architectures(cunumeric) - enable_language(CUDA) - # Since cunumeric only enables CUDA optionally we need to manually include - # the file that rapids_cuda_init_architectures relies on `project` calling - if(CMAKE_PROJECT_cunumeric_INCLUDE) - include("${CMAKE_PROJECT_cunumeric_INCLUDE}") - endif() - - # Must come after enable_language(CUDA) - # Use `-isystem ` instead of `-isystem=` - # because the former works with clangd intellisense - set(CMAKE_INCLUDE_SYSTEM_FLAG_CUDA "-isystem ") - - rapids_find_package( - CUDAToolkit REQUIRED - BUILD_EXPORT_SET cunumeric-exports - INSTALL_EXPORT_SET cunumeric-exports - ) - - include(cmake/thirdparty/get_nccl.cmake) - include(cmake/thirdparty/get_cutensor.cmake) -endif() - -include(cmake/thirdparty/get_openblas.cmake) - -include(cmake/thirdparty/get_tblis.cmake) - -############################################################################## -# - cuNumeric ---------------------------------------------------------------- - -set(cunumeric_SOURCES "") -set(cunumeric_CXX_DEFS "") -set(cunumeric_CUDA_DEFS "") -set(cunumeric_CXX_OPTIONS "") -set(cunumeric_CUDA_OPTIONS "") - -include(cmake/Modules/set_cpu_arch_flags.cmake) -set_cpu_arch_flags(cunumeric_CXX_OPTIONS) - -# Add `src/cunumeric.mk` sources -list(APPEND cunumeric_SOURCES - src/cunumeric/ternary/where.cc - src/cunumeric/scan/scan_global.cc - src/cunumeric/scan/scan_local.cc - src/cunumeric/binary/binary_op.cc - src/cunumeric/binary/binary_red.cc - src/cunumeric/bits/packbits.cc - src/cunumeric/bits/unpackbits.cc - src/cunumeric/unary/scalar_unary_red.cc - src/cunumeric/unary/unary_op.cc - src/cunumeric/unary/unary_red.cc - src/cunumeric/unary/convert.cc - src/cunumeric/nullary/arange.cc - src/cunumeric/nullary/eye.cc - src/cunumeric/nullary/fill.cc - src/cunumeric/nullary/window.cc - src/cunumeric/index/advanced_indexing.cc - src/cunumeric/index/choose.cc - src/cunumeric/index/putmask.cc - src/cunumeric/index/repeat.cc - src/cunumeric/index/select.cc - src/cunumeric/index/wrap.cc - src/cunumeric/index/zip.cc - src/cunumeric/item/read.cc - src/cunumeric/item/write.cc - src/cunumeric/matrix/batched_cholesky.cc - src/cunumeric/matrix/contract.cc - src/cunumeric/matrix/diag.cc - src/cunumeric/matrix/gemm.cc - src/cunumeric/matrix/matmul.cc - src/cunumeric/matrix/matvecmul.cc - src/cunumeric/matrix/dot.cc - src/cunumeric/matrix/potrf.cc - src/cunumeric/matrix/solve.cc - src/cunumeric/matrix/syrk.cc - src/cunumeric/matrix/tile.cc - src/cunumeric/matrix/transpose.cc - src/cunumeric/matrix/trilu.cc - src/cunumeric/matrix/trsm.cc - src/cunumeric/matrix/util.cc - src/cunumeric/random/rand.cc - src/cunumeric/search/argwhere.cc - src/cunumeric/search/nonzero.cc - src/cunumeric/set/unique.cc - src/cunumeric/set/unique_reduce.cc - src/cunumeric/stat/bincount.cc - src/cunumeric/convolution/convolve.cc - src/cunumeric/transform/flip.cc - src/cunumeric/arg_redop_register.cc - src/cunumeric/mapper.cc - src/cunumeric/cephes/chbevl.cc - src/cunumeric/cephes/i0.cc - src/cunumeric/stat/histogram.cc -) - -if(Legion_USE_OpenMP) - list(APPEND cunumeric_SOURCES - src/cunumeric/ternary/where_omp.cc - src/cunumeric/scan/scan_global_omp.cc - src/cunumeric/scan/scan_local_omp.cc - src/cunumeric/binary/binary_op_omp.cc - src/cunumeric/binary/binary_red_omp.cc - src/cunumeric/bits/packbits_omp.cc - src/cunumeric/bits/unpackbits_omp.cc - src/cunumeric/unary/unary_op_omp.cc - src/cunumeric/unary/scalar_unary_red_omp.cc - src/cunumeric/unary/unary_red_omp.cc - src/cunumeric/unary/convert_omp.cc - src/cunumeric/nullary/arange_omp.cc - src/cunumeric/nullary/eye_omp.cc - src/cunumeric/nullary/fill_omp.cc - src/cunumeric/nullary/window_omp.cc - src/cunumeric/index/advanced_indexing_omp.cc - src/cunumeric/index/choose_omp.cc - src/cunumeric/index/putmask_omp.cc - src/cunumeric/index/repeat_omp.cc - src/cunumeric/index/select_omp.cc - src/cunumeric/index/wrap_omp.cc - src/cunumeric/index/zip_omp.cc - src/cunumeric/matrix/batched_cholesky_omp.cc - src/cunumeric/matrix/contract_omp.cc - src/cunumeric/matrix/diag_omp.cc - src/cunumeric/matrix/gemm_omp.cc - src/cunumeric/matrix/matmul_omp.cc - src/cunumeric/matrix/matvecmul_omp.cc - src/cunumeric/matrix/dot_omp.cc - src/cunumeric/matrix/potrf_omp.cc - src/cunumeric/matrix/solve_omp.cc - src/cunumeric/matrix/syrk_omp.cc - src/cunumeric/matrix/tile_omp.cc - src/cunumeric/matrix/transpose_omp.cc - src/cunumeric/matrix/trilu_omp.cc - src/cunumeric/matrix/trsm_omp.cc - src/cunumeric/random/rand_omp.cc - src/cunumeric/search/argwhere_omp.cc - src/cunumeric/search/nonzero_omp.cc - src/cunumeric/set/unique_omp.cc - src/cunumeric/set/unique_reduce_omp.cc - src/cunumeric/stat/bincount_omp.cc - src/cunumeric/convolution/convolve_omp.cc - src/cunumeric/transform/flip_omp.cc - src/cunumeric/stat/histogram_omp.cc - ) -endif() - -if(Legion_USE_CUDA) - list(APPEND cunumeric_SOURCES - src/cunumeric/ternary/where.cu - src/cunumeric/scan/scan_global.cu - src/cunumeric/scan/scan_local.cu - src/cunumeric/binary/binary_op.cu - src/cunumeric/binary/binary_red.cu - src/cunumeric/bits/packbits.cu - src/cunumeric/bits/unpackbits.cu - src/cunumeric/unary/scalar_unary_red.cu - src/cunumeric/unary/unary_red.cu - src/cunumeric/unary/unary_op.cu - src/cunumeric/unary/convert.cu - src/cunumeric/nullary/arange.cu - src/cunumeric/nullary/eye.cu - src/cunumeric/nullary/fill.cu - src/cunumeric/nullary/window.cu - src/cunumeric/index/advanced_indexing.cu - src/cunumeric/index/choose.cu - src/cunumeric/index/putmask.cu - src/cunumeric/index/repeat.cu - src/cunumeric/index/select.cu - src/cunumeric/index/wrap.cu - src/cunumeric/index/zip.cu - src/cunumeric/item/read.cu - src/cunumeric/item/write.cu - src/cunumeric/matrix/batched_cholesky.cu - src/cunumeric/matrix/contract.cu - src/cunumeric/matrix/diag.cu - src/cunumeric/matrix/gemm.cu - src/cunumeric/matrix/matmul.cu - src/cunumeric/matrix/matvecmul.cu - src/cunumeric/matrix/dot.cu - src/cunumeric/matrix/potrf.cu - src/cunumeric/matrix/solve.cu - src/cunumeric/matrix/syrk.cu - src/cunumeric/matrix/tile.cu - src/cunumeric/matrix/transpose.cu - src/cunumeric/matrix/trilu.cu - src/cunumeric/matrix/trsm.cu - src/cunumeric/random/rand.cu - src/cunumeric/search/argwhere.cu - src/cunumeric/search/nonzero.cu - src/cunumeric/set/unique.cu - src/cunumeric/stat/bincount.cu - src/cunumeric/convolution/convolve.cu - src/cunumeric/fft/fft.cu - src/cunumeric/transform/flip.cu - src/cunumeric/arg_redop_register.cu - src/cunumeric/cudalibs.cu - src/cunumeric/stat/histogram.cu - ) -endif() - -# Add `src/cunumeric/sort/sort.mk` sources -list(APPEND cunumeric_SOURCES - src/cunumeric/sort/sort.cc - src/cunumeric/sort/searchsorted.cc -) - -if(Legion_USE_OpenMP) - list(APPEND cunumeric_SOURCES - src/cunumeric/sort/sort_omp.cc - src/cunumeric/sort/searchsorted_omp.cc - ) -endif() - -if(Legion_USE_CUDA) - list(APPEND cunumeric_SOURCES - src/cunumeric/sort/sort.cu - src/cunumeric/sort/searchsorted.cu - src/cunumeric/sort/cub_sort_bool.cu - src/cunumeric/sort/cub_sort_int8.cu - src/cunumeric/sort/cub_sort_int16.cu - src/cunumeric/sort/cub_sort_int32.cu - src/cunumeric/sort/cub_sort_int64.cu - src/cunumeric/sort/cub_sort_uint8.cu - src/cunumeric/sort/cub_sort_uint16.cu - src/cunumeric/sort/cub_sort_uint32.cu - src/cunumeric/sort/cub_sort_uint64.cu - src/cunumeric/sort/cub_sort_half.cu - src/cunumeric/sort/cub_sort_float.cu - src/cunumeric/sort/cub_sort_double.cu - src/cunumeric/sort/thrust_sort_bool.cu - src/cunumeric/sort/thrust_sort_int8.cu - src/cunumeric/sort/thrust_sort_int16.cu - src/cunumeric/sort/thrust_sort_int32.cu - src/cunumeric/sort/thrust_sort_int64.cu - src/cunumeric/sort/thrust_sort_uint8.cu - src/cunumeric/sort/thrust_sort_uint16.cu - src/cunumeric/sort/thrust_sort_uint32.cu - src/cunumeric/sort/thrust_sort_uint64.cu - src/cunumeric/sort/thrust_sort_half.cu - src/cunumeric/sort/thrust_sort_float.cu - src/cunumeric/sort/thrust_sort_double.cu - src/cunumeric/sort/thrust_sort_complex64.cu - src/cunumeric/sort/thrust_sort_complex128.cu - ) -endif() - -# Add `src/cunumeric/random/random.mk` sources -if(Legion_USE_CUDA OR cunumeric_cuRAND_INCLUDE_DIR) - list(APPEND cunumeric_SOURCES - src/cunumeric/random/bitgenerator.cc - src/cunumeric/random/randutil/generator_host.cc - src/cunumeric/random/randutil/generator_host_straightforward.cc - src/cunumeric/random/randutil/generator_host_advanced.cc - ) - if(Legion_USE_CUDA) - list(APPEND cunumeric_SOURCES - src/cunumeric/random/bitgenerator.cu - src/cunumeric/random/randutil/generator_device.cu - src/cunumeric/random/randutil/generator_device_straightforward.cu - src/cunumeric/random/randutil/generator_device_advanced.cu - ) - endif() -endif() - -list(APPEND cunumeric_SOURCES - # This must always be the last file! - # It guarantees we do our registration callback - # only after all task variants are recorded - src/cunumeric/cunumeric.cc -) - -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - list(APPEND cunumeric_CXX_DEFS DEBUG_CUNUMERIC) - list(APPEND cunumeric_CUDA_DEFS DEBUG_CUNUMERIC) -endif() - -if(Legion_BOUNDS_CHECKS) - list(APPEND cunumeric_CXX_DEFS BOUNDS_CHECKS) - list(APPEND cunumeric_CUDA_DEFS BOUNDS_CHECKS) -endif() - -list(APPEND cunumeric_CUDA_OPTIONS -Xfatbin=-compress-all) -list(APPEND cunumeric_CUDA_OPTIONS --expt-extended-lambda) -list(APPEND cunumeric_CUDA_OPTIONS --expt-relaxed-constexpr) -list(APPEND cunumeric_CXX_OPTIONS -Wno-deprecated-declarations) -list(APPEND cunumeric_CUDA_OPTIONS -Wno-deprecated-declarations) - -add_library(cunumeric ${cunumeric_SOURCES}) -add_library(cunumeric::cunumeric ALIAS cunumeric) - -if (CMAKE_SYSTEM_NAME STREQUAL "Linux") - set(platform_rpath_origin "\$ORIGIN") -elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - set(platform_rpath_origin "@loader_path") -endif () - -set_target_properties(cunumeric - PROPERTIES BUILD_RPATH "${platform_rpath_origin}" - INSTALL_RPATH "${platform_rpath_origin}" - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON - POSITION_INDEPENDENT_CODE ON - INTERFACE_POSITION_INDEPENDENT_CODE ON - CUDA_STANDARD 17 - CUDA_STANDARD_REQUIRED ON - LIBRARY_OUTPUT_DIRECTORY lib) - -target_link_libraries(cunumeric - PUBLIC legate::core - $ - PRIVATE BLAS::BLAS - tblis::tblis - # Add Conda library and include paths - $ - $ - $ - $ - $ - $) - -if(NOT Legion_USE_CUDA AND cunumeric_cuRAND_INCLUDE_DIR) - list(APPEND cunumeric_CXX_DEFS CUNUMERIC_CURAND_FOR_CPU_BUILD) - target_include_directories(cunumeric PRIVATE ${cunumeric_cuRAND_INCLUDE_DIR}) -endif() - -# Change THRUST_DEVICE_SYSTEM for `.cpp` files -if(Legion_USE_OpenMP) - list(APPEND cunumeric_CXX_OPTIONS -UTHRUST_DEVICE_SYSTEM) - list(APPEND cunumeric_CXX_OPTIONS -DTHRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_OMP) -elseif(NOT Legion_USE_CUDA) - list(APPEND cunumeric_CXX_OPTIONS -UTHRUST_DEVICE_SYSTEM) - list(APPEND cunumeric_CXX_OPTIONS -DTHRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CPP) -endif() - -target_compile_options(cunumeric - PRIVATE "$<$:${cunumeric_CXX_OPTIONS}>" - "$<$:${cunumeric_CUDA_OPTIONS}>") - -target_compile_definitions(cunumeric - PUBLIC "$<$:${cunumeric_CXX_DEFS}>" - "$<$:${cunumeric_CUDA_DEFS}>") - -target_include_directories(cunumeric - PRIVATE - $ - INTERFACE - $ -) - -if(Legion_USE_CUDA) - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld" -[=[ -SECTIONS -{ -.nvFatBinSegment : { *(.nvFatBinSegment) } -.nv_fatbin : { *(.nv_fatbin) } -} -]=]) - - # ensure CUDA symbols aren't relocated to the middle of the debug build binaries - target_link_options(cunumeric PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld") -endif() - -############################################################################## -# - install targets----------------------------------------------------------- - -include(CPack) -include(GNUInstallDirs) -rapids_cmake_install_lib_dir(lib_dir) - -install(TARGETS cunumeric - DESTINATION ${lib_dir} - EXPORT cunumeric-exports) - -install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/include/cunumeric/version_config.hpp - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cunumeric) - -if(cunumeric_INSTALL_TBLIS) - install(DIRECTORY ${tblis_BINARY_DIR}/lib/ DESTINATION ${lib_dir}) - install(DIRECTORY ${tblis_BINARY_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -endif() - -############################################################################## -# - install export ----------------------------------------------------------- - -set(doc_string - [=[ -Provide targets for cuNumeric, an aspiring drop-in replacement for NumPy at scale. - -Imported Targets: - - cunumeric::cunumeric - -]=]) - -string(JOIN "\n" code_string - "set(Legion_USE_CUDA ${Legion_USE_CUDA})" - "set(Legion_USE_OpenMP ${Legion_USE_OpenMP})" - "set(Legion_BOUNDS_CHECKS ${Legion_BOUNDS_CHECKS})" -) - -if(DEFINED Legion_USE_Python) - string(APPEND code_string "\nset(Legion_USE_Python ${Legion_USE_Python})") -endif() - -if(DEFINED Legion_NETWORKS) - string(APPEND code_string "\nset(Legion_NETWORKS ${Legion_NETWORKS})") -endif() - -rapids_export( - INSTALL cunumeric - EXPORT_SET cunumeric-exports - GLOBAL_TARGETS cunumeric - NAMESPACE cunumeric:: - DOCUMENTATION doc_string - FINAL_CODE_BLOCK code_string) - -# build export targets -rapids_export( - BUILD cunumeric - EXPORT_SET cunumeric-exports - GLOBAL_TARGETS cunumeric - NAMESPACE cunumeric:: - DOCUMENTATION doc_string - FINAL_CODE_BLOCK code_string) diff --git a/cunumeric/__init__.py b/cupynumeric/__init__.py similarity index 69% rename from cunumeric/__init__.py rename to cupynumeric/__init__.py index 3ad86dd9f..01037dfa3 100644 --- a/cunumeric/__init__.py +++ b/cupynumeric/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # """ -cuNumeric +cuPyNumeric ===== Provides a distributed task-parallel implementation of the Numpy interface @@ -24,18 +24,15 @@ """ from __future__ import annotations -import os - import numpy as _np -from cunumeric import linalg, random, fft, ma -from cunumeric.array import maybe_convert_to_np_ndarray, ndarray -from cunumeric.bits import packbits, unpackbits -from cunumeric.module import * -from cunumeric._ufunc import * -from cunumeric.logic import * -from cunumeric.window import bartlett, blackman, hamming, hanning, kaiser -from cunumeric.coverage import clone_module +from . import linalg, random, fft, ma +from ._array.array import ndarray +from ._array.util import maybe_convert_to_np_ndarray +from ._module import * +from ._ufunc import * +from ._utils.array import is_supported_dtype +from ._utils.coverage import clone_module clone_module(_np, globals(), maybe_convert_to_np_ndarray) diff --git a/cupynumeric/_array/__init__.py b/cupynumeric/_array/__init__.py new file mode 100644 index 000000000..31d8d448c --- /dev/null +++ b/cupynumeric/_array/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations diff --git a/cunumeric/array.py b/cupynumeric/_array/array.py similarity index 72% rename from cunumeric/array.py rename to cupynumeric/_array/array.py index 8bfc5178a..1787affe6 100644 --- a/cunumeric/array.py +++ b/cupynumeric/_array/array.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,61 +16,64 @@ import operator import warnings -from functools import reduce, wraps -from inspect import signature -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Sequence, - TypeVar, - Union, - cast, -) +from functools import reduce +from typing import TYPE_CHECKING, Any, Sequence, cast +import legate.core.types as ty import numpy as np -from legate.core import Array, Field +from legate.core import Field, LogicalArray, Scalar from legate.core.utils import OrderedSet -from numpy.core.multiarray import ( # type: ignore [attr-defined] - normalize_axis_index, -) -from numpy.core.numeric import ( # type: ignore [attr-defined] - normalize_axis_tuple, -) -from typing_extensions import ParamSpec -from .config import ( - BinaryOpCode, - ConvertCode, +from .. import _ufunc +from .._utils import is_np2 +from .._utils.array import ( + calculate_volume, + max_identity, + min_identity, + to_core_type, +) +from .._utils.coverage import FALLBACK_WARNING, clone_class, is_implemented +from .._utils.linalg import dot_modes +from .._utils.structure import deep_apply +from ..config import ( FFTDirection, FFTNormalization, FFTType, ScanCode, + TransferType, UnaryOpCode, UnaryRedCode, ) -from .coverage import FALLBACK_WARNING, clone_class, is_implemented -from .runtime import runtime -from .types import NdShape -from .utils import ( - calculate_volume, - deep_apply, - dot_modes, - to_core_dtype, +from ..runtime import runtime +from ..types import NdShape +from .flags import flagsobj +from .thunk import perform_scan, perform_unary_op, perform_unary_reduction +from .util import ( + add_boilerplate, + broadcast_where, + check_writeable, + convert_to_cupynumeric_ndarray, + maybe_convert_to_np_ndarray, + sanitize_shape, tuple_pop, ) +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + from numpy.core.numeric import normalize_axis_tuple # type: ignore + if TYPE_CHECKING: from pathlib import Path import numpy.typing as npt - from .thunk import NumPyThunk - from .types import ( + from .._thunk.thunk import NumPyThunk + from ..types import ( BoundsMode, CastingKind, - NdShapeLike, OrderType, SelectKind, SortSide, @@ -79,187 +82,6 @@ from math import prod -R = TypeVar("R") -P = ParamSpec("P") - - -def add_boilerplate( - *array_params: str, -) -> Callable[[Callable[P, R]], Callable[P, R]]: - """ - Adds required boilerplate to the wrapped cunumeric.ndarray or module-level - function. - - Every time the wrapped function is called, this wrapper will: - * Convert all specified array-like parameters, plus the special "out" - parameter (if present), to cuNumeric ndarrays. - * Convert the special "where" parameter (if present) to a valid predicate. - """ - keys = OrderedSet(array_params) - assert len(keys) == len(array_params) - - def decorator(func: Callable[P, R]) -> Callable[P, R]: - assert not hasattr( - func, "__wrapped__" - ), "this decorator must be the innermost" - - # For each parameter specified by name, also consider the case where - # it's passed as a positional parameter. - indices: OrderedSet[int] = OrderedSet() - where_idx: Optional[int] = None - out_idx: Optional[int] = None - params = signature(func).parameters - extra = keys - OrderedSet(params) - assert len(extra) == 0, f"unknown parameter(s): {extra}" - for idx, param in enumerate(params): - if param == "where": - where_idx = idx - elif param == "out": - out_idx = idx - elif param in keys: - indices.add(idx) - - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> R: - assert (where_idx is None or len(args) <= where_idx) and ( - out_idx is None or len(args) <= out_idx - ), "'where' and 'out' should be passed as keyword arguments" - - # Convert relevant arguments to cuNumeric ndarrays - args = tuple( - convert_to_cunumeric_ndarray(arg) - if idx in indices and arg is not None - else arg - for (idx, arg) in enumerate(args) - ) - for k, v in kwargs.items(): - if v is None: - continue - elif k == "out": - kwargs[k] = convert_to_cunumeric_ndarray(v, share=True) - if not kwargs[k].flags.writeable: - raise ValueError("out is not writeable") - elif (k in keys) or (k == "where"): - kwargs[k] = convert_to_cunumeric_ndarray(v) - - return func(*args, **kwargs) - - return wrapper - - return decorator - - -def convert_to_cunumeric_ndarray(obj: Any, share: bool = False) -> ndarray: - # If this is an instance of one of our ndarrays then we're done - if isinstance(obj, ndarray): - return obj - # Ask the runtime to make a numpy thunk for this object - thunk = runtime.get_numpy_thunk(obj, share=share) - writeable = ( - obj.flags.writeable if isinstance(obj, np.ndarray) and share else True - ) - return ndarray(shape=None, thunk=thunk, writeable=writeable) - - -def maybe_convert_to_np_ndarray(obj: Any) -> Any: - """ - Converts cuNumeric arrays into NumPy arrays, otherwise has no effect. - """ - from .ma import MaskedArray - - if isinstance(obj, (ndarray, MaskedArray)): - return obj.__array__() - return obj - - -def check_writeable(arr: Union[ndarray, tuple[ndarray, ...], None]) -> None: - """ - Check if the current array is writeable - This check needs to be manually inserted - with consideration on the behavior of the corresponding method - """ - if arr is None: - return - check_list = (arr,) if not isinstance(arr, tuple) else arr - if any(not arr.flags.writeable for arr in check_list): - raise ValueError("array is not writeable") - - -def broadcast_where( - where: Union[ndarray, None], shape: NdShape -) -> Union[ndarray, None]: - if where is not None and where.shape != shape: - from .module import broadcast_to - - where = broadcast_to(where, shape) - return where - - -class flagsobj: - """ - Information about the memory layout of the array. - - These flags don't reflect the properties of the cuNumeric array, but - rather the NumPy array that will be produced if the cuNumeric array is - materialized on a single node. - """ - - def __init__(self, array: ndarray) -> None: - # prevent infinite __setattr__ recursion - object.__setattr__(self, "_array", array) - - def __repr__(self) -> str: - return f"""\ - C_CONTIGUOUS : {self["C"]} - F_CONTIGUOUS : {self["F"]} - OWNDATA : {self["O"]} - WRITEABLE : {self["W"]} - ALIGNED : {self["A"]} - WRITEBACKIFCOPY : {self["X"]} -""" - - def __eq__(self, other: Any) -> bool: - flags = ("C", "F", "O", "W", "A", "X") - if not isinstance(other, (flagsobj, np.core.multiarray.flagsobj)): - return False - - return all(self[f] == other[f] for f in flags) # type: ignore [index] - - def __getattr__(self, name: str) -> Any: - if name == "writeable": - return self._array._writeable - flags = self._array.__array__().flags - return getattr(flags, name) - - def __setattr__(self, name: str, value: Any) -> None: - if name == "writeable": - self._check_writeable(value) - self._array._writeable = bool(value) - else: - flags = self._array.__array__().flags - setattr(flags, name, value) - - def __getitem__(self, key: Any) -> bool: - if key == "W": - return self._array._writeable - flags = self._array.__array__().flags - return flags[key] - - def __setitem__(self, key: str, value: Any) -> None: - if key == "W": - self._check_writeable(value) - self._array._writeable = bool(value) - else: - flags = self._array.__array__().flags - flags[key] = value - - def _check_writeable(self, value: Any) -> None: - if value and not self._array._writeable: - raise ValueError( - "non-writeable cunumeric arrays cannot be made writeable" - ) - - NDARRAY_INTERNAL = { "__array_finalize__", "__array_function__", @@ -278,19 +100,19 @@ def __init__( self, shape: Any, dtype: npt.DTypeLike = np.float64, - buffer: Union[Any, None] = None, + buffer: Any | None = None, offset: int = 0, - strides: Union[tuple[int], None] = None, - order: Union[OrderType, None] = None, - thunk: Union[NumPyThunk, None] = None, - inputs: Union[Any, None] = None, + strides: tuple[int] | None = None, + order: OrderType | None = None, + thunk: NumPyThunk | None = None, + inputs: Any | None = None, writeable: bool = True, ) -> None: - # `inputs` being a cuNumeric ndarray is definitely a bug + # `inputs` being a cuPyNumeric ndarray is definitely a bug assert not isinstance(inputs, ndarray) if thunk is None: assert shape is not None - sanitized_shape = self._sanitize_shape(shape) + sanitized_shape = sanitize_shape(shape) if not isinstance(dtype, np.dtype): dtype = np.dtype(dtype) if buffer is not None: @@ -304,7 +126,7 @@ def __init__( order=order, ) self._thunk = runtime.find_or_create_array_thunk( - np_array, share=False + np_array, TransferType.SHARE ) else: # Filter the inputs if necessary @@ -314,57 +136,32 @@ def __init__( for inp in inputs if isinstance(inp, ndarray) ] - core_dtype = to_core_dtype(dtype) + core_dtype = to_core_type(dtype) self._thunk = runtime.create_empty_thunk( sanitized_shape, core_dtype, inputs ) else: self._thunk = thunk - self._legate_data: Union[dict[str, Any], None] = None + self._legate_data: dict[str, Any] | None = None self._writeable = writeable - @staticmethod - def _sanitize_shape( - shape: Union[NdShapeLike, Sequence[Any], npt.NDArray[Any], ndarray] - ) -> NdShape: - seq: tuple[Any, ...] - if isinstance(shape, (ndarray, np.ndarray)): - if shape.ndim == 0: - seq = (shape.__array__().item(),) - else: - seq = tuple(shape.__array__()) - elif np.isscalar(shape): - seq = (shape,) - else: - seq = tuple(cast(NdShape, shape)) - try: - # Unfortunately, we can't do this check using - # 'isinstance(value, int)', as the values in a NumPy ndarray - # don't satisfy the predicate (they have numpy value types, - # such as numpy.int64). - result = tuple(operator.index(value) for value in seq) - except TypeError: - raise TypeError( - "expected a sequence of integers or a single integer, " - f"got {shape!r}" - ) - return result - # Support for the Legate data interface @property def __legate_data_interface__(self) -> dict[str, Any]: if self._legate_data is None: # If the thunk is an eager array, we need to convert it to a # deferred array so we can extract a legate store - deferred_thunk = runtime.to_deferred_array(self._thunk) + deferred_thunk = runtime.to_deferred_array( + self._thunk, read_only=False + ) # We don't have nullable data for the moment # until we support masked arrays dtype = deferred_thunk.base.type - array = Array(dtype, [None, deferred_thunk.base]) + array = LogicalArray.from_store(deferred_thunk.base) self._legate_data = dict() self._legate_data["version"] = 1 - field = Field("cuNumeric Array", dtype) + field = Field("cuPyNumeric Array", dtype) self._legate_data["data"] = {field: array} return self._legate_data @@ -389,7 +186,7 @@ def __legate_data_interface__(self) -> dict[str, Any]: def __array_function__( self, func: Any, types: Any, args: tuple[Any], kwargs: dict[str, Any] ) -> Any: - import cunumeric as cn + import cupynumeric as cn what = func.__name__ @@ -400,19 +197,19 @@ def __array_function__( return NotImplemented # We are wrapping all NumPy modules, so we can expect to find every - # NumPy API call in cuNumeric, even if just an "unimplemented" stub. + # NumPy API call in cuPyNumeric, even if just an "unimplemented" stub. module = reduce(getattr, func.__module__.split(".")[1:], cn) cn_func = getattr(module, func.__name__) - # We can't immediately forward to the corresponding cuNumeric + # We can't immediately forward to the corresponding cuPyNumeric # entrypoint. Say that we reached this point because the user code - # invoked `np.foo(x, bar=True)` where `x` is a `cunumeric.ndarray`. If - # our implementation of `foo` is not complete, and cannot handle + # invoked `np.foo(x, bar=True)` where `x` is a `cupynumeric.ndarray`. + # If our implementation of `foo` is not complete, and cannot handle # `bar=True`, then forwarding this call to `cn.foo` would fail. This # goes against the semantics of `__array_function__`, which shouldn't # fail if the custom implementation cannot handle the provided # arguments. Conversely, if the user calls `cn.foo(x, bar=True)` - # directly, that means they requested the cuNumeric implementation + # directly, that means they requested the cuPyNumeric implementation # specifically, and the `NotImplementedError` should not be hidden. if is_implemented(cn_func): try: @@ -435,7 +232,7 @@ def __array_function__( def __array_ufunc__( self, ufunc: Any, method: str, *inputs: Any, **kwargs: Any ) -> Any: - from . import _ufunc + from .. import _ufunc # Check whether we should handle the arguments array_args = inputs @@ -488,30 +285,31 @@ def T(self) -> ndarray: See Also -------- - cunumeric.transpose + cupynumeric.transpose ndarray.transpose """ return self.transpose() @property - def base(self) -> Union[npt.NDArray[Any], None]: + def base(self) -> npt.NDArray[Any] | None: """ - Returns dtype for the base element of the subarrays, - regardless of their dimension or shape. - - See Also - -------- - numpy.dtype.subdtype - + Base object if memory is from some other object. """ - return self.__array__().base + raise NotImplementedError( + "cupynumeric.ndarray doesn't keep track of the array view " + "hierarchy yet" + ) @property def data(self) -> memoryview: """ Python buffer object pointing to the start of the array's data. + Notes + ----- + This causes the entire (potentially distributed) array to be collected + into one memory. """ return self.__array__().data @@ -534,9 +332,9 @@ def flags(self) -> Any: """ Information about the memory layout of the array. - These flags don't reflect the properties of the cuNumeric array, but - rather the NumPy array that will be produced if the cuNumeric array is - materialized on a single node. + These flags don't reflect the properties of the cuPyNumeric array, but + rather the NumPy array that will be produced if the cuPyNumeric array + is materialized on a single node. Attributes ---------- @@ -618,7 +416,7 @@ def flat(self) -> np.flatiter[npt.NDArray[Any]]: flatten : Return a copy of the array collapsed into one dimension. Availability - -------- + ------------ Single CPU """ @@ -794,9 +592,7 @@ def __abs__(self) -> ndarray: """ # Handle the nice case of it being unsigned - from ._ufunc import absolute - - return absolute(self) + return _ufunc.absolute(self) def __add__(self, rhs: Any) -> ndarray: """a.__add__(value, /) @@ -808,9 +604,7 @@ def __add__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import add - - return add(self, rhs) + return _ufunc.add(self, rhs) def __and__(self, rhs: Any) -> ndarray: """a.__and__(value, /) @@ -822,12 +616,10 @@ def __and__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_and - - return bitwise_and(self, rhs) + return _ufunc.bitwise_and(self, rhs) def __array__( - self, dtype: Union[np.dtype[Any], None] = None + self, dtype: np.dtype[Any] | None = None ) -> npt.NDArray[Any]: """a.__array__([dtype], /) @@ -877,12 +669,13 @@ def __contains__(self, item: Any) -> ndarray: args = (np.array(item, dtype=self.dtype),) if args[0].size != 1: raise ValueError("contains needs scalar item") - return self._perform_unary_reduction( + core_dtype = to_core_type(self.dtype) + return perform_unary_reduction( UnaryRedCode.CONTAINS, self, axis=None, res_dtype=bool, - args=args, + args=(Scalar(args[0].squeeze()[()], core_dtype),), ) def __copy__(self) -> ndarray: @@ -902,7 +695,7 @@ def __copy__(self) -> ndarray: result._thunk.copy(self._thunk, deep=False) return result - def __deepcopy__(self, memo: Union[Any, None] = None) -> ndarray: + def __deepcopy__(self, memo: Any | None = None) -> ndarray: """a.__deepcopy__(memo, /) Deep copy of array. @@ -941,7 +734,7 @@ def __divmod__(self, rhs: Any) -> ndarray: """ raise NotImplementedError( - "cunumeric.ndarray doesn't support __divmod__ yet" + "cupynumeric.ndarray doesn't support __divmod__ yet" ) def __eq__(self, rhs: object) -> ndarray: # type: ignore [override] @@ -954,9 +747,7 @@ def __eq__(self, rhs: object) -> ndarray: # type: ignore [override] Multiple GPUs, Multiple CPUs """ - from ._ufunc import equal - - return equal(self, rhs) + return _ufunc.equal(self, rhs) def __float__(self) -> float: """a.__float__(/) @@ -976,9 +767,7 @@ def __floordiv__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import floor_divide - - return floor_divide(self, rhs) + return _ufunc.floor_divide(self, rhs) def __format__(self, *args: Any, **kwargs: Any) -> str: return self.__array__().__format__(*args, **kwargs) @@ -993,14 +782,12 @@ def __ge__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import greater_equal - - return greater_equal(self, rhs) + return _ufunc.greater_equal(self, rhs) # __getattribute__ def _convert_key(self, key: Any, first: bool = True) -> Any: - # Convert any arrays stored in a key to a cuNumeric array + # Convert any arrays stored in a key to a cuPyNumeric array if isinstance(key, slice): key = slice( operator.index(key.start) if key.start is not None else None, @@ -1017,9 +804,9 @@ def _convert_key(self, key: Any, first: bool = True) -> Any: elif isinstance(key, tuple) and first: return tuple(self._convert_key(k, first=False) for k in key) else: - # Otherwise convert it to a cuNumeric array, check types + # Otherwise convert it to a cuPyNumeric array, check types # and get the thunk - key = convert_to_cunumeric_ndarray(key) + key = convert_to_cupynumeric_ndarray(key) if key.dtype != bool and not np.issubdtype(key.dtype, np.integer): raise TypeError("index arrays should be int or bool type") if key.dtype != bool: @@ -1047,12 +834,10 @@ def __gt__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import greater - - return greater(self, rhs) + return _ufunc.greater(self, rhs) def __hash__(self) -> int: - raise TypeError("unhashable type: cunumeric.ndarray") + raise TypeError("unhashable type: cupynumeric.ndarray") def __iadd__(self, rhs: Any) -> ndarray: """a.__iadd__(value, /) @@ -1064,9 +849,7 @@ def __iadd__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import add - - return add(self, rhs, out=self) + return _ufunc.add(self, rhs, out=self) def __iand__(self, rhs: Any) -> ndarray: """a.__iand__(value, /) @@ -1078,9 +861,7 @@ def __iand__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_and - - return bitwise_and(self, rhs, out=self) + return _ufunc.bitwise_and(self, rhs, out=self) def __idiv__(self, rhs: Any) -> ndarray: """a.__idiv__(value, /) @@ -1104,9 +885,7 @@ def __ifloordiv__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import floor_divide - - return floor_divide(self, rhs, out=self) + return _ufunc.floor_divide(self, rhs, out=self) def __ilshift__(self, rhs: Any) -> ndarray: """a.__ilshift__(value, /) @@ -1118,9 +897,21 @@ def __ilshift__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import left_shift + return _ufunc.left_shift(self, rhs, out=self) + + def __imatmul__(self, rhs: Any) -> ndarray: + """a.__imatmul__(value, /) + + Return ``self@=value``. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + from .._module.linalg_mvp import matmul - return left_shift(self, rhs, out=self) + return matmul(self, rhs, out=self) def __imod__(self, rhs: Any) -> ndarray: """a.__imod__(value, /) @@ -1132,9 +923,7 @@ def __imod__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import remainder - - return remainder(self, rhs, out=self) + return _ufunc.remainder(self, rhs, out=self) def __imul__(self, rhs: Any) -> ndarray: """a.__imul__(value, /) @@ -1146,9 +935,7 @@ def __imul__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import multiply - - return multiply(self, rhs, out=self) + return _ufunc.multiply(self, rhs, out=self) def __index__(self) -> int: return self.__array__().__index__() @@ -1171,15 +958,11 @@ def __invert__(self) -> ndarray: Multiple GPUs, Multiple CPUs """ - if self.dtype == np.bool_: + if self.dtype == bool: # Boolean values are special, just do logical NOT - from ._ufunc import logical_not - - return logical_not(self) + return _ufunc.logical_not(self) else: - from ._ufunc import invert - - return invert(self) + return _ufunc.invert(self) def __ior__(self, rhs: Any) -> ndarray: """a.__ior__(/) @@ -1191,9 +974,7 @@ def __ior__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_or - - return bitwise_or(self, rhs, out=self) + return _ufunc.bitwise_or(self, rhs, out=self) def __ipow__(self, rhs: float) -> ndarray: """a.__ipow__(/) @@ -1205,9 +986,7 @@ def __ipow__(self, rhs: float) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import power - - return power(self, rhs, out=self) + return _ufunc.power(self, rhs, out=self) def __irshift__(self, rhs: Any) -> ndarray: """a.__irshift__(/) @@ -1219,9 +998,7 @@ def __irshift__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import right_shift - - return right_shift(self, rhs, out=self) + return _ufunc.right_shift(self, rhs, out=self) def __iter__(self) -> Any: """a.__iter__(/)""" @@ -1237,9 +1014,7 @@ def __isub__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import subtract - - return subtract(self, rhs, out=self) + return _ufunc.subtract(self, rhs, out=self) def __itruediv__(self, rhs: Any) -> ndarray: """a.__itruediv__(/) @@ -1251,9 +1026,7 @@ def __itruediv__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import true_divide - - return true_divide(self, rhs, out=self) + return _ufunc.true_divide(self, rhs, out=self) def __ixor__(self, rhs: Any) -> ndarray: """a.__ixor__(/) @@ -1265,9 +1038,7 @@ def __ixor__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_xor - - return bitwise_xor(self, rhs, out=self) + return _ufunc.bitwise_xor(self, rhs, out=self) def __le__(self, rhs: Any) -> ndarray: """a.__le__(value, /) @@ -1279,9 +1050,7 @@ def __le__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import less_equal - - return less_equal(self, rhs) + return _ufunc.less_equal(self, rhs) def __len__(self) -> int: """a.__len__(/) @@ -1301,9 +1070,7 @@ def __lshift__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import left_shift - - return left_shift(self, rhs) + return _ufunc.left_shift(self, rhs) def __lt__(self, rhs: Any) -> ndarray: """a.__lt__(value, /) @@ -1315,9 +1082,7 @@ def __lt__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import less - - return less(self, rhs) + return _ufunc.less(self, rhs) def __matmul__(self, value: Any) -> ndarray: """a.__matmul__(value, /) @@ -1329,7 +1094,9 @@ def __matmul__(self, value: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - return self.dot(value) + from .._module.linalg_mvp import matmul + + return matmul(self, value) def __mod__(self, rhs: Any) -> ndarray: """a.__mod__(value, /) @@ -1341,9 +1108,7 @@ def __mod__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import remainder - - return remainder(self, rhs) + return _ufunc.remainder(self, rhs) def __mul__(self, rhs: Any) -> ndarray: """a.__mul__(value, /) @@ -1355,9 +1120,7 @@ def __mul__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import multiply - - return multiply(self, rhs) + return _ufunc.multiply(self, rhs) def __ne__(self, rhs: object) -> ndarray: # type: ignore [override] """a.__ne__(value, /) @@ -1369,9 +1132,7 @@ def __ne__(self, rhs: object) -> ndarray: # type: ignore [override] Multiple GPUs, Multiple CPUs """ - from ._ufunc import not_equal - - return not_equal(self, rhs) + return _ufunc.not_equal(self, rhs) def __neg__(self) -> ndarray: """a.__neg__(value, /) @@ -1383,9 +1144,7 @@ def __neg__(self) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import negative - - return negative(self) + return _ufunc.negative(self) # __new__ @@ -1395,11 +1154,11 @@ def nonzero(self) -> tuple[ndarray, ...]: Return the indices of the elements that are non-zero. - Refer to :func:`cunumeric.nonzero` for full documentation. + Refer to :func:`cupynumeric.nonzero` for full documentation. See Also -------- - cunumeric.nonzero : equivalent function + cupynumeric.nonzero : equivalent function Availability -------- @@ -1421,9 +1180,7 @@ def __or__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_or - - return bitwise_or(self, rhs) + return _ufunc.bitwise_or(self, rhs) def __pos__(self) -> ndarray: """a.__pos__(value, /) @@ -1436,9 +1193,7 @@ def __pos__(self) -> ndarray: """ # the positive opeartor is equivalent to copy - from ._ufunc import positive - - return positive(self) + return _ufunc.positive(self) def __pow__(self, rhs: float) -> ndarray: """a.__pow__(value, /) @@ -1450,9 +1205,7 @@ def __pow__(self, rhs: float) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import power - - return power(self, rhs) + return _ufunc.power(self, rhs) def __radd__(self, lhs: Any) -> ndarray: """a.__radd__(value, /) @@ -1464,9 +1217,7 @@ def __radd__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import add - - return add(lhs, self) + return _ufunc.add(lhs, self) def __rand__(self, lhs: Any) -> ndarray: """a.__rand__(value, /) @@ -1478,9 +1229,7 @@ def __rand__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_and - - return bitwise_and(lhs, self) + return _ufunc.bitwise_and(lhs, self) def __rdiv__(self, lhs: Any) -> ndarray: """a.__rdiv__(value, /) @@ -1492,9 +1241,7 @@ def __rdiv__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import true_divide - - return true_divide(lhs, self) + return _ufunc.true_divide(lhs, self) def __rdivmod__(self, lhs: Any) -> ndarray: """a.__rdivmod__(value, /) @@ -1507,12 +1254,10 @@ def __rdivmod__(self, lhs: Any) -> ndarray: """ raise NotImplementedError( - "cunumeric.ndarray doesn't support __rdivmod__ yet" + "cupynumeric.ndarray doesn't support __rdivmod__ yet" ) - def __reduce__( - self, *args: Any, **kwargs: Any - ) -> Union[str, tuple[str, ...]]: + def __reduce__(self, *args: Any, **kwargs: Any) -> str | tuple[str, ...]: """a.__reduce__(/) For pickling. @@ -1522,7 +1267,7 @@ def __reduce__( def __reduce_ex__( self, *args: Any, **kwargs: Any - ) -> Union[str, tuple[str, ...]]: + ) -> str | tuple[str, ...]: return self.__array__().__reduce_ex__(*args, **kwargs) def __repr__(self) -> str: @@ -1547,9 +1292,21 @@ def __rfloordiv__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import floor_divide + return _ufunc.floor_divide(lhs, self) + + def __rmatmul__(self, lhs: Any) -> ndarray: + """a.__rmatmul__(value, /) + + Return ``value@self``. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + from .._module.linalg_mvp import matmul - return floor_divide(lhs, self) + return matmul(lhs, self) def __rmod__(self, lhs: Any) -> ndarray: """a.__rmod__(value, /) @@ -1561,9 +1318,7 @@ def __rmod__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import remainder - - return remainder(lhs, self) + return _ufunc.remainder(lhs, self) def __rmul__(self, lhs: Any) -> ndarray: """a.__rmul__(value, /) @@ -1575,9 +1330,7 @@ def __rmul__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import multiply - - return multiply(lhs, self) + return _ufunc.multiply(lhs, self) def __ror__(self, lhs: Any) -> ndarray: """a.__ror__(value, /) @@ -1589,9 +1342,7 @@ def __ror__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_or - - return bitwise_or(lhs, self) + return _ufunc.bitwise_or(lhs, self) def __rpow__(self, lhs: Any) -> ndarray: """__rpow__(value, /) @@ -1603,9 +1354,7 @@ def __rpow__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import power - - return power(lhs, self) + return _ufunc.power(lhs, self) def __rshift__(self, rhs: Any) -> ndarray: """a.__rshift__(value, /) @@ -1617,9 +1366,7 @@ def __rshift__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import right_shift - - return right_shift(self, rhs) + return _ufunc.right_shift(self, rhs) def __rsub__(self, lhs: Any) -> ndarray: """a.__rsub__(value, /) @@ -1631,9 +1378,7 @@ def __rsub__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import subtract - - return subtract(lhs, self) + return _ufunc.subtract(lhs, self) def __rtruediv__(self, lhs: Any) -> ndarray: """a.__rtruediv__(value, /) @@ -1645,9 +1390,7 @@ def __rtruediv__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import true_divide - - return true_divide(lhs, self) + return _ufunc.true_divide(lhs, self) def __rxor__(self, lhs: Any) -> ndarray: """a.__rxor__(value, /) @@ -1659,9 +1402,7 @@ def __rxor__(self, lhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_xor - - return bitwise_xor(lhs, self) + return _ufunc.bitwise_xor(lhs, self) # __setattr__ @add_boilerplate("value") @@ -1713,9 +1454,7 @@ def __sub__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import subtract - - return subtract(self, rhs) + return _ufunc.subtract(self, rhs) def __str__(self) -> str: """a.__str__(/) @@ -1739,9 +1478,7 @@ def __truediv__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import true_divide - - return true_divide(self, rhs) + return _ufunc.true_divide(self, rhs) def __xor__(self, rhs: Any) -> ndarray: """a.__xor__(value, /) @@ -1753,35 +1490,33 @@ def __xor__(self, rhs: Any) -> ndarray: Multiple GPUs, Multiple CPUs """ - from ._ufunc import bitwise_xor - - return bitwise_xor(self, rhs) + return _ufunc.bitwise_xor(self, rhs) @add_boilerplate() def all( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.all(axis=None, out=None, keepdims=False, initial=None, where=True) Returns True if all elements evaluate to True. - Refer to :func:`cunumeric.all` for full documentation. + Refer to :func:`cupynumeric.all` for full documentation. See Also -------- - cunumeric.all : equivalent function + cupynumeric.all : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.ALL, self, axis=axis, @@ -1796,27 +1531,27 @@ def all( def any( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.any(axis=None, out=None, keepdims=False, initial=None, where=True) Returns True if any of the elements of `a` evaluate to True. - Refer to :func:`cunumeric.any` for full documentation. + Refer to :func:`cupynumeric.any` for full documentation. See Also -------- - cunumeric.any : equivalent function + cupynumeric.any : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.ANY, self, axis=axis, @@ -1831,18 +1566,18 @@ def any( def argmax( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, ) -> ndarray: """a.argmax(axis=None, out=None) Return indices of the maximum values along the given axis. - Refer to :func:`cunumeric.argmax` for full documentation. + Refer to :func:`cupynumeric.argmax` for full documentation. See Also -------- - cunumeric.argmax : equivalent function + cupynumeric.argmax : equivalent function Availability -------- @@ -1853,7 +1588,7 @@ def argmax( raise ValueError("output array must have int64 dtype") if axis is not None and not isinstance(axis, int): raise ValueError("axis must be an integer") - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.ARGMAX, self, axis=axis, @@ -1866,18 +1601,18 @@ def argmax( def argmin( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, ) -> ndarray: """a.argmin(axis=None, out=None) Return indices of the minimum values along the given axis. - Refer to :func:`cunumeric.argmin` for detailed documentation. + Refer to :func:`cupynumeric.argmin` for detailed documentation. See Also -------- - cunumeric.argmin : equivalent function + cupynumeric.argmin : equivalent function Availability -------- @@ -1888,7 +1623,7 @@ def argmin( raise ValueError("output array must have int64 dtype") if axis is not None and not isinstance(axis, int): raise ValueError("axis must be an integer") - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.ARGMIN, self, axis=axis, @@ -1903,7 +1638,7 @@ def astype( order: OrderType = "C", casting: CastingKind = "unsafe", subok: bool = True, - copy: bool = True, + copy: bool = False, ) -> ndarray: """a.astype(dtype, order='C', casting='unsafe', subok=True, copy=True) @@ -1939,10 +1674,14 @@ def astype( array. copy : bool, optional - By default, astype always returns a newly allocated array. If this - is set to false, and the `dtype`, `order`, and `subok` - requirements are satisfied, the input array is returned instead - of a copy. + By default, astype does not returns a newly allocated array. If + this is set to True, a copy is made and returned, instead of the + input array. + + Notes + ----- + The default value for the ``copy`` argument is the opposite of Numpy. + Avoiding copies reduces memory pressure. Returns ------- @@ -1995,18 +1734,18 @@ def take( self, indices: Any, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, mode: BoundsMode = "raise", ) -> ndarray: """a.take(indices, axis=None, out=None, mode="raise") Take elements from an array along an axis. - Refer to :func:`cunumeric.take` for full documentation. + Refer to :func:`cupynumeric.take` for full documentation. See Also -------- - cunumeric.take : equivalent function + cupynumeric.take : equivalent function Availability -------- @@ -2016,7 +1755,7 @@ def take( if not np.isscalar(indices): # if indices is a tuple or list, bring sub-tuples to the same shape # and concatenate them - indices = convert_to_cunumeric_ndarray(indices) + indices = convert_to_cupynumeric_ndarray(indices) if axis is None: self = self.ravel() @@ -2075,18 +1814,18 @@ def take( def choose( self, choices: Any, - out: Union[ndarray, None] = None, + out: ndarray | None = None, mode: BoundsMode = "raise", ) -> ndarray: """a.choose(choices, out=None, mode='raise') Use an index array to construct a new array from a set of choices. - Refer to :func:`cunumeric.choose` for full documentation. + Refer to :func:`cupynumeric.choose` for full documentation. See Also -------- - cunumeric.choose : equivalent function + cupynumeric.choose : equivalent function Availability -------- @@ -2104,12 +1843,12 @@ def choose( dtypes = [ch.dtype for ch in choices] ch_dtype = np.result_type(*dtypes) choices = tuple( - convert_to_cunumeric_ndarray(choices[i]).astype(ch_dtype) + convert_to_cupynumeric_ndarray(choices[i]).astype(ch_dtype) for i in range(n) ) else: - choices = convert_to_cunumeric_ndarray(choices) + choices = convert_to_cupynumeric_ndarray(choices) n = choices.shape[0] ch_dtype = choices.dtype choices = tuple(choices[i, ...] for i in range(n)) @@ -2177,17 +1916,17 @@ def compress( self, condition: ndarray, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, ) -> ndarray: """a.compress(self, condition, axis=None, out=None) Return selected slices of an array along given axis. - Refer to :func:`cunumeric.compress` for full documentation. + Refer to :func:`cupynumeric.compress` for full documentation. See Also -------- - cunumeric.compress : equivalent function + cupynumeric.compress : equivalent function Availability -------- @@ -2236,9 +1975,9 @@ def compress( @add_boilerplate() def clip( self, - min: Union[int, float, npt.ArrayLike, None] = None, - max: Union[int, float, npt.ArrayLike, None] = None, - out: Union[npt.NDArray[Any], ndarray, None] = None, + min: int | float | npt.ArrayLike | None = None, + max: int | float | npt.ArrayLike | None = None, + out: ndarray | None = None, ) -> ndarray: """a.clip(min=None, max=None, out=None) @@ -2246,41 +1985,68 @@ def clip( One of max or min must be given. - Refer to :func:`cunumeric.clip` for full documentation. + Refer to :func:`cupynumeric.clip` for full documentation. See Also -------- - cunumeric.clip : equivalent function + cupynumeric.clip : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ + min = max_identity(self.dtype) if min is None else min + max = min_identity(self.dtype) if max is None else max + args = ( np.array(min, dtype=self.dtype), np.array(max, dtype=self.dtype), ) if args[0].size != 1 or args[1].size != 1: runtime.warn( - "cuNumeric has not implemented clip with array-like " + "cuPyNumeric has not implemented clip with array-like " "arguments and is falling back to canonical numpy. You " "may notice significantly decreased performance for this " "function call.", category=RuntimeWarning, ) - if isinstance(out, np.ndarray): - self.__array__().clip(args[0], args[1], out=out) - return convert_to_cunumeric_ndarray(out, share=True) - elif isinstance(out, ndarray): + if out is not None: self.__array__().clip(args[0], args[1], out=out.__array__()) return out else: - return convert_to_cunumeric_ndarray( + return convert_to_cupynumeric_ndarray( self.__array__().clip(args[0], args[1]) ) - return self._perform_unary_op( - UnaryOpCode.CLIP, self, out=out, extra_args=args + core_dtype = to_core_type(self.dtype) + extra_args = (Scalar(min, core_dtype), Scalar(max, core_dtype)) + return perform_unary_op( + UnaryOpCode.CLIP, self, out=out, extra_args=extra_args + ) + + @add_boilerplate() + def round( + self, + decimals: int = 0, + out: ndarray | None = None, + ) -> ndarray: + """a.round(decimals=0, out=None) + + Return a with each element rounded to the given number of decimals. + + Refer to :func:`cupynumeric.round` for full documentation. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + extra_args = ( + Scalar(decimals, ty.int64), + Scalar(10 ** abs(decimals), ty.int64), + ) + return perform_unary_op( + UnaryOpCode.ROUND, self, out=out, extra_args=extra_args ) def conj(self) -> ndarray: @@ -2288,11 +2054,11 @@ def conj(self) -> ndarray: Complex-conjugate all elements. - Refer to :func:`cunumeric.conjugate` for full documentation. + Refer to :func:`cupynumeric.conjugate` for full documentation. See Also -------- - cunumeric.conjugate : equivalent function + cupynumeric.conjugate : equivalent function Availability -------- @@ -2310,11 +2076,11 @@ def conjugate(self) -> ndarray: Return the complex conjugate, element-wise. - Refer to :func:`cunumeric.conjugate` for full documentation. + Refer to :func:`cupynumeric.conjugate` for full documentation. See Also -------- - cunumeric.conjugate : equivalent function + cupynumeric.conjugate : equivalent function Availability -------- @@ -2333,17 +2099,17 @@ def copy(self, order: OrderType = "C") -> ndarray: Multiple GPUs, Multiple CPUs """ - # We don't care about dimension order in cuNumeric + # We don't care about dimension order in cuPyNumeric return self.__copy__() @add_boilerplate() def cumsum( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ) -> ndarray: - return self._perform_scan( + return perform_scan( ScanCode.SUM, self, axis=axis, @@ -2356,10 +2122,10 @@ def cumsum( def cumprod( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ) -> ndarray: - return self._perform_scan( + return perform_scan( ScanCode.PROD, self, axis=axis, @@ -2372,10 +2138,10 @@ def cumprod( def nancumsum( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ) -> ndarray: - return self._perform_scan( + return perform_scan( ScanCode.SUM, self, axis=axis, @@ -2388,10 +2154,10 @@ def nancumsum( def nancumprod( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ) -> ndarray: - return self._perform_scan( + return perform_scan( ScanCode.PROD, self, axis=axis, @@ -2407,11 +2173,11 @@ def nancumprod( def _diag_helper( self, offset: int = 0, - axes: Union[Any, None] = None, + axes: Any | None = None, extract: bool = True, trace: bool = False, - out: Union[ndarray, None] = None, - dtype: Union[np.dtype[Any], None] = None, + out: ndarray | None = None, + dtype: np.dtype[Any] | None = None, ) -> ndarray: # _diag_helper can be used only for arrays with dim>=1 if self.ndim < 1: @@ -2540,11 +2306,11 @@ def diagonal( Return specified diagonals. - Refer to :func:`cunumeric.diagonal` for full documentation. + Refer to :func:`cupynumeric.diagonal` for full documentation. See Also -------- - cunumeric.diagonal : equivalent function + cupynumeric.diagonal : equivalent function Availability -------- @@ -2566,11 +2332,11 @@ def put( """ Replaces specified elements of the array with given values. - Refer to :func:`cunumeric.put` for full documentation. + Refer to :func:`cupynumeric.put` for full documentation. See Also -------- - cunumeric.put : equivalent function + cupynumeric.put : equivalent function Availability -------- @@ -2622,18 +2388,18 @@ def trace( offset: int = 0, axis1: Any = None, axis2: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ) -> ndarray: """a.trace(offset=0, axis1=None, axis2=None, dtype = None, out = None) Return the sum along diagonals of the array. - Refer to :func:`cunumeric.trace` for full documentation. + Refer to :func:`cupynumeric.trace` for full documentation. See Also -------- - cunumeric.trace : equivalent function + cupynumeric.trace : equivalent function Availability -------- @@ -2665,28 +2431,27 @@ def trace( return res @add_boilerplate("rhs") - def dot(self, rhs: ndarray, out: Union[ndarray, None] = None) -> ndarray: + def dot(self, rhs: ndarray, out: ndarray | None = None) -> ndarray: """a.dot(rhs, out=None) Return the dot product of this array with ``rhs``. - Refer to :func:`cunumeric.dot` for full documentation. + Refer to :func:`cupynumeric.dot` for full documentation. See Also -------- - cunumeric.dot : equivalent function + cupynumeric.dot : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - from .module import _contract # work around circular import + # work around circular import + from .._module.linalg_mvp import _contract if self.ndim == 0 or rhs.ndim == 0: - from ._ufunc import multiply - - return multiply(self, rhs, out=out) + return _ufunc.multiply(self, rhs, out=out) (self_modes, rhs_modes, out_modes) = dot_modes(self.ndim, rhs.ndim) return _contract( @@ -2699,12 +2464,12 @@ def dot(self, rhs: ndarray, out: Union[ndarray, None] = None) -> ndarray: casting="unsafe", ) - def dump(self, file: Union[str, Path]) -> None: + def dump(self, file: str | Path) -> None: """a.dump(file) Dump a pickle of the array to the specified file. - The array can be read back with pickle.load or cunumeric.load. + The array can be read back with pickle.load or cupynumeric.load. Parameters ---------- @@ -2763,7 +2528,7 @@ def _normalize_axes_shape( def fft( self, s: Any, - axes: Union[Sequence[int], None], + axes: Sequence[int] | None, kind: FFTType, direction: FFTDirection, norm: Any, @@ -2773,7 +2538,7 @@ def fft( Return the ``kind`` ``direction`` FFT of this array with normalization ``norm``. - Common entrypoint for FFT functionality in cunumeric.fft module. + Common entrypoint for FFT functionality in cupynumeric.fft module. Notes ----- @@ -2781,7 +2546,7 @@ def fft( See Also -------- - cunumeric.fft : FFT functions for different ``kind`` and + cupynumeric.fft : FFT functions for different ``kind`` and ``direction`` arguments Availability @@ -2928,7 +2693,7 @@ def flatten(self, order: OrderType = "C") -> ndarray: def getfield(self, dtype: np.dtype[Any], offset: int = 0) -> None: raise NotImplementedError( - "cuNumeric does not currently support type reinterpretation " + "cuPyNumeric does not currently support type reinterpretation " "for ndarray.getfield" ) @@ -2993,73 +2758,75 @@ def item(self, *args: Any) -> Any: assert result.shape == () return result._thunk.__numpy_array__() - def itemset(self, *args: Any) -> None: - """a.itemset(*args) - - Insert scalar into an array (scalar is cast to array's dtype, - if possible) - - There must be at least 1 argument, and define the last argument - as *item*. Then, ``a.itemset(*args)`` is equivalent to but faster - than ``a[args] = item``. The item should be a scalar value and `args` - must select a single item in the array `a`. - - Parameters - ---------- - \\*args : - If one argument: a scalar, only used in case `a` is of size 1. - If two arguments: the last argument is the value to be set - and must be a scalar, the first argument specifies a single array - element location. It is either an int or a tuple. - - Notes - ----- - Compared to indexing syntax, `itemset` provides some speed increase - for placing a scalar into a particular location in an `ndarray`, - if you must do this. However, generally this is discouraged: - among other problems, it complicates the appearance of the code. - Also, when using `itemset` (and `item`) inside a loop, be sure - to assign the methods to a local variable to avoid the attribute - look-up at each loop iteration. - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - if len(args) == 0: - raise KeyError("itemset() requires at least one argument") - value = args[-1] - args = args[:-1] - key = self._convert_singleton_key(args) - self[key] = value + if not is_np2: + + def itemset(self, *args: Any) -> None: + """a.itemset(*args) + + Insert scalar into an array (scalar is cast to array's dtype, + if possible) + + There must be at least 1 argument, and define the last argument + as *item*. Then, ``a.itemset(*args)`` is equivalent to but faster + than ``a[args] = item``. The item should be a scalar value and + `args` must select a single item in the array `a`. + + Parameters + ---------- + \\*args : + If one argument: a scalar, only used in case `a` is of size 1. + If two arguments: the last argument is the value to be set + and must be a scalar, the first argument specifies a single + array element location. It is either an int or a tuple. + + Notes + ----- + Compared to indexing syntax, `itemset` provides some speed increase + for placing a scalar into a particular location in an `ndarray`, + if you must do this. However, generally this is discouraged: + among other problems, it complicates the appearance of the code. + Also, when using `itemset` (and `item`) inside a loop, be sure + to assign the methods to a local variable to avoid the attribute + look-up at each loop iteration. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + if len(args) == 0: + raise KeyError("itemset() requires at least one argument") + value = args[-1] + args = args[:-1] + key = self._convert_singleton_key(args) + self[key] = value @add_boilerplate() def max( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.max(axis=None, out=None, keepdims=False, initial=, where=True) Return the maximum along a given axis. - Refer to :func:`cunumeric.amax` for full documentation. + Refer to :func:`cupynumeric.amax` for full documentation. See Also -------- - cunumeric.amax : equivalent function + cupynumeric.amax : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.MAX, self, axis=axis, @@ -3069,19 +2836,17 @@ def max( where=where, ) - def _count_nonzero(self, axis: Any = None) -> Union[int, ndarray]: + def _count_nonzero(self, axis: Any = None) -> int | ndarray: if self.size == 0: return 0 - return ndarray._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.COUNT_NONZERO, self, res_dtype=np.dtype(np.uint64), axis=axis, ) - def _summation_dtype( - self, dtype: Optional[np.dtype[Any]] - ) -> np.dtype[Any]: + def _summation_dtype(self, dtype: np.dtype[Any] | None) -> np.dtype[Any]: # Pick our dtype if it wasn't picked yet if dtype is None: if self.dtype.kind != "f" and self.dtype.kind != "c": @@ -3096,7 +2861,7 @@ def _normalize_summation( axis: Any, ddof: int = 0, keepdims: bool = False, - where: Union[ndarray, None] = None, + where: ndarray | None = None, ) -> None: dtype = sum_array.dtype if axis is None: @@ -3132,20 +2897,20 @@ def _normalize_summation( def mean( self, axis: Any = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, keepdims: bool = False, - where: Union[ndarray, None] = None, + where: ndarray | None = None, ) -> ndarray: """a.mean(axis=None, dtype=None, out=None, keepdims=False) Returns the average of the array elements along given axis. - Refer to :func:`cunumeric.mean` for full documentation. + Refer to :func:`cupynumeric.mean` for full documentation. See Also -------- - cunumeric.mean : equivalent function + cupynumeric.mean : equivalent function Availability -------- @@ -3154,7 +2919,7 @@ def mean( """ if axis is not None and not isinstance(axis, int): raise NotImplementedError( - "cunumeric.mean only supports int types for " + "cupynumeric.mean only supports int types for " "`axis` currently" ) @@ -3190,15 +2955,13 @@ def mean( def _nanmean( self, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, keepdims: bool = False, - where: Union[ndarray, None] = None, + where: ndarray | None = None, ) -> ndarray: - from . import _ufunc - - if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.bool_): + if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, bool): return self.mean( axis=axis, dtype=dtype, out=out, keepdims=keepdims, where=where ) @@ -3219,23 +2982,23 @@ def _nanmean( @add_boilerplate() def var( self, - axis: Optional[Union[int, tuple[int, ...]]] = None, - dtype: Optional[np.dtype[Any]] = None, - out: Optional[ndarray] = None, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, ddof: int = 0, keepdims: bool = False, *, - where: Union[ndarray, None] = None, + where: ndarray | None = None, ) -> ndarray: """a.var(a, axis=None, dtype=None, out=None, ddof=0, keepdims=False) Returns the variance of the array elements along given axis. - Refer to :func:`cunumeric.var` for full documentation. + Refer to :func:`cupynumeric.var` for full documentation. See Also -------- - cunumeric.var : equivalent function + cupynumeric.var : equivalent function Availability -------- @@ -3244,7 +3007,7 @@ def var( """ if axis is not None and not isinstance(axis, int): raise NotImplementedError( - "cunumeric.var only supports int types for `axis` currently" + "cupynumeric.var only supports int types for `axis` currently" ) # this could be computed as a single pass through the array @@ -3254,7 +3017,7 @@ def var( # directly as <(x-mu)^2>, which then requires two passes through the # data to first compute the mean and then compute the variance # see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance - # TODO(https://github.com/nv-legate/cunumeric/issues/590) + # TODO(https://github.com/nv-legate/cupynumeric/issues/590) dtype = self._summation_dtype(dtype) # calculate the mean, but keep the dimensions so that the @@ -3267,7 +3030,7 @@ def var( if axis is None or calculate_volume(tuple_pop(self.shape, axis)) == 1: # this is a scalar reduction and we can optimize this as a single # pass through a scalar reduction - result = self._perform_unary_reduction( + result = perform_unary_reduction( UnaryRedCode.VARIANCE, self, axis=axis, @@ -3275,10 +3038,13 @@ def var( out=out, keepdims=keepdims, where=where_array, - args=(mu,), + # FIXME(wonchanl): the following code blocks on mu to convert + # it to a Scalar object. We need to get rid of this blocking by + # allowing the extra arguments to be Legate stores + args=(Scalar(mu.__array__(), to_core_type(self.dtype)),), ) else: - # TODO(https://github.com/nv-legate/cunumeric/issues/591) + # TODO(https://github.com/nv-legate/cupynumeric/issues/591) # there isn't really support for generic binary reductions # right now all of the current binary reductions are boolean # reductions like allclose. To implement this a single pass would @@ -3288,7 +3054,7 @@ def var( # delta*delta in second pass delta = self - mu - result = self._perform_unary_reduction( + result = perform_unary_reduction( UnaryRedCode.SUM_SQUARES, delta, axis=axis, @@ -3312,28 +3078,28 @@ def var( def min( self, axis: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.min(axis=None, out=None, keepdims=False, initial=, where=True) Return the minimum along a given axis. - Refer to :func:`cunumeric.amin` for full documentation. + Refer to :func:`cupynumeric.amin` for full documentation. See Also -------- - cunumeric.amin : equivalent function + cupynumeric.amin : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.MIN, self, axis=axis, @@ -3346,20 +3112,20 @@ def min( @add_boilerplate() def partition( self, - kth: Union[int, Sequence[int]], + kth: int | Sequence[int], axis: Any = -1, kind: SelectKind = "introselect", - order: Union[OrderType, None] = None, + order: OrderType | None = None, ) -> None: """a.partition(kth, axis=-1, kind='introselect', order=None) Partition of an array in-place. - Refer to :func:`cunumeric.partition` for full documentation. + Refer to :func:`cupynumeric.partition` for full documentation. See Also -------- - cunumeric.partition : equivalent function + cupynumeric.partition : equivalent function Availability -------- @@ -3374,20 +3140,20 @@ def partition( @add_boilerplate() def argpartition( self, - kth: Union[int, Sequence[int]], + kth: int | Sequence[int], axis: Any = -1, kind: SelectKind = "introselect", - order: Union[OrderType, None] = None, + order: OrderType | None = None, ) -> ndarray: """a.argpartition(kth, axis=-1, kind='introselect', order=None) Returns the indices that would partition this array. - Refer to :func:`cunumeric.argpartition` for full documentation. + Refer to :func:`cupynumeric.argpartition` for full documentation. See Also -------- - cunumeric.argpartition : equivalent function + cupynumeric.argpartition : equivalent function Availability -------- @@ -3409,41 +3175,31 @@ def argpartition( def prod( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.prod(axis=None, dtype=None, out=None, keepdims=False, initial=1, where=True) Return the product of the array elements over the given axis - Refer to :func:`cunumeric.prod` for full documentation. + Refer to :func:`cupynumeric.prod` for full documentation. See Also -------- - cunumeric.prod : equivalent function + cupynumeric.prod : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - if self.dtype.type == np.bool_: - temp = ndarray( - shape=self.shape, - dtype=np.dtype(np.int32), - inputs=(self,), - ) - temp._thunk.convert(self._thunk) - self_array = temp - else: - self_array = self - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.PROD, - self_array, + self, axis=axis, dtype=dtype, out=out, @@ -3457,11 +3213,11 @@ def ravel(self, order: OrderType = "C") -> ndarray: Return a flattened array. - Refer to :func:`cunumeric.ravel` for full documentation. + Refer to :func:`cupynumeric.ravel` for full documentation. See Also -------- - cunumeric.ravel : equivalent function + cupynumeric.ravel : equivalent function ndarray.flat : a flat iterator on the array. Availability @@ -3476,11 +3232,11 @@ def reshape(self, *args: Any, order: OrderType = "C") -> ndarray: Returns an array containing the same data with a new shape. - Refer to :func:`cunumeric.reshape` for full documentation. + Refer to :func:`cupynumeric.reshape` for full documentation. See Also -------- - cunumeric.reshape : equivalent function + cupynumeric.reshape : equivalent function Availability @@ -3551,15 +3307,15 @@ def setfield( self, val: Any, dtype: npt.DTypeLike, offset: int = 0 ) -> None: raise NotImplementedError( - "cuNumeric does not currently support type reinterpretation " + "cuPyNumeric does not currently support type reinterpretation " "for ndarray.setfield" ) def setflags( self, - write: Union[bool, None] = None, - align: Union[bool, None] = None, - uic: Union[bool, None] = None, + write: bool | None = None, + align: bool | None = None, + uic: bool | None = None, ) -> None: """a.setflags(write=None, align=None, uic=None) @@ -3622,10 +3378,10 @@ def setflags( @add_boilerplate() def searchsorted( self: ndarray, - v: Union[int, float, ndarray], + v: int | float | ndarray, side: SortSide = "left", - sorter: Optional[ndarray] = None, - ) -> Union[int, ndarray]: + sorter: ndarray | None = None, + ) -> int | ndarray: """a.searchsorted(v, side='left', sorter=None) Find the indices into a sorted array a such that, if the corresponding @@ -3659,7 +3415,7 @@ def searchsorted( raise ValueError("Dimension mismatch: self must be a 1D array") # this is needed in case v is a scalar - v_ndarray = convert_to_cunumeric_ndarray(v) + v_ndarray = convert_to_cupynumeric_ndarray(v) a = self # in case we have different dtypes we ned to find a common type @@ -3697,17 +3453,17 @@ def sort( self, axis: Any = -1, kind: SortType = "quicksort", - order: Union[OrderType, None] = None, + order: OrderType | None = None, ) -> None: """a.sort(axis=-1, kind=None, order=None) Sort an array in-place. - Refer to :func:`cunumeric.sort` for full documentation. + Refer to :func:`cupynumeric.sort` for full documentation. See Also -------- - cunumeric.sort : equivalent function + cupynumeric.sort : equivalent function Availability -------- @@ -3721,17 +3477,17 @@ def argsort( self, axis: Any = -1, kind: SortType = "quicksort", - order: Union[OrderType, None] = None, + order: OrderType | None = None, ) -> ndarray: """a.argsort(axis=-1, kind=None, order=None) Returns the indices that would sort this array. - Refer to :func:`cunumeric.argsort` for full documentation. + Refer to :func:`cupynumeric.argsort` for full documentation. See Also -------- - cunumeric.argsort : equivalent function + cupynumeric.argsort : equivalent function Availability -------- @@ -3749,11 +3505,11 @@ def squeeze(self, axis: Any = None) -> ndarray: Remove axes of length one from `a`. - Refer to :func:`cunumeric.squeeze` for full documentation. + Refer to :func:`cupynumeric.squeeze` for full documentation. See Also -------- - cunumeric.squeeze : equivalent function + cupynumeric.squeeze : equivalent function Availability -------- @@ -3779,41 +3535,31 @@ def squeeze(self, axis: Any = None) -> ndarray: def sum( self, axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: """a.sum(axis=None, dtype=None, out=None, keepdims=False, initial=0, where=None) Return the sum of the array elements over the given axis. - Refer to :func:`cunumeric.sum` for full documentation. + Refer to :func:`cupynumeric.sum` for full documentation. See Also -------- - cunumeric.sum : equivalent function + cupynumeric.sum : equivalent function Availability -------- Multiple GPUs, Multiple CPUs """ - if self.dtype.type == np.bool_: - temp = ndarray( - shape=self.shape, - dtype=np.dtype(np.int32), - inputs=(self,), - ) - temp._thunk.convert(self._thunk) - self_array = temp - else: - self_array = self - return self._perform_unary_reduction( + return perform_unary_reduction( UnaryRedCode.SUM, - self_array, + self, axis=axis, dtype=dtype, out=out, @@ -3826,10 +3572,10 @@ def _nansum( self, axis: Any = None, dtype: Any = None, - out: Union[ndarray, None] = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Optional[Union[int, float]] = None, - where: Optional[ndarray] = None, + initial: int | float | None = None, + where: ndarray | None = None, ) -> ndarray: # Note that np.nansum and np.sum allow complex datatypes # so there are no "disallowed types" for this API @@ -3839,7 +3585,7 @@ def _nansum( else: unary_red_code = UnaryRedCode.SUM - return self._perform_unary_reduction( + return perform_unary_reduction( unary_red_code, self, axis=axis, @@ -3855,11 +3601,11 @@ def swapaxes(self, axis1: Any, axis2: Any) -> ndarray: Return a view of the array with `axis1` and `axis2` interchanged. - Refer to :func:`cunumeric.swapaxes` for full documentation. + Refer to :func:`cupynumeric.swapaxes` for full documentation. See Also -------- - cunumeric.swapaxes : equivalent function + cupynumeric.swapaxes : equivalent function Availability -------- @@ -3957,7 +3703,7 @@ def tolist(self) -> Any: Return a copy of the array data as a (nested) Python list. Data items are converted to the nearest compatible builtin Python - type, via the `~cunumeric.ndarray.item` function. + type, via the `~cupynumeric.ndarray.item` function. If ``a.ndim`` is 0, then since the depth of the nested list is 0, it will not be a list at all, but a simple Python scalar. @@ -3974,7 +3720,7 @@ def tolist(self) -> Any: Notes ----- - The array may be recreated via ``a = cunumeric.array(a.tolist())``, + The array may be recreated via ``a = cupynumeric.array(a.tolist())``, although this may sometimes lose precision. Availability @@ -4090,8 +3836,8 @@ def flip(self, axis: Any = None) -> ndarray: def view( self, - dtype: Union[npt.DTypeLike, None] = None, - type: Union[type, None] = None, + dtype: npt.DTypeLike | None = None, + type: type | None = None, ) -> ndarray: """ New view of array with the same data. @@ -4110,7 +3856,7 @@ def view( Notes ----- - cuNumeric does not currently support type reinterpretation, or + cuPyNumeric does not currently support type reinterpretation, or conversion to ndarray sub-classes; use :func:`ndarray.__array__()` to convert to `numpy.ndarray`. @@ -4124,11 +3870,11 @@ def view( """ if dtype is not None and dtype != self.dtype: raise NotImplementedError( - "cuNumeric does not currently support type reinterpretation" + "cuPyNumeric does not currently support type reinterpretation" ) if type is not None: raise NotImplementedError( - "cuNumeric does not currently support conversion to ndarray " + "cuPyNumeric does not currently support conversion to ndarray " "sub-classes; use __array__() to convert to numpy.ndarray" ) return ndarray( @@ -4143,11 +3889,11 @@ def unique(self) -> ndarray: Find the unique elements of an array. - Refer to :func:`cunumeric.unique` for full documentation. + Refer to :func:`cupynumeric.unique` for full documentation. See Also -------- - cunumeric.unique : equivalent function + cupynumeric.unique : equivalent function Availability -------- @@ -4157,44 +3903,6 @@ def unique(self) -> ndarray: thunk = self._thunk.unique() return ndarray(shape=thunk.shape, thunk=thunk) - @classmethod - def _get_where_thunk( - cls, where: Union[None, ndarray], out_shape: NdShape - ) -> Union[None, NumPyThunk]: - if where is None: - return where - if ( - not isinstance(where, ndarray) - or where.dtype != np.bool_ - or where.shape != out_shape - ): - raise RuntimeError("should have converted this earlier") - return where._thunk - - @staticmethod - def find_common_type(*args: ndarray) -> np.dtype[Any]: - """Determine common type following NumPy's coercion rules. - - Parameters - ---------- - *args : ndarray - A list of ndarrays - - Returns - ------- - datatype : data-type - The type that results from applying the NumPy type promotion rules - to the arguments. - """ - array_types = list() - scalars = list() - for array in args: - if array.ndim == 0: - scalars.append(array.dtype.type(0)) - else: - array_types.append(array.dtype) - return np.result_type(*array_types, *scalars) - def _maybe_convert(self, dtype: np.dtype[Any], hints: Any) -> ndarray: if self.dtype == dtype: return self @@ -4212,313 +3920,6 @@ def _warn_and_convert(self, dtype: np.dtype[Any]) -> ndarray: else: return self - # For performing normal/broadcast unary operations - @classmethod - def _perform_unary_op( - cls, - op: UnaryOpCode, - src: ndarray, - out: Union[Any, None] = None, - extra_args: Any = None, - dtype: Union[np.dtype[Any], None] = None, - out_dtype: Union[np.dtype[Any], None] = None, - ) -> ndarray: - if out is not None: - # If the shapes don't match see if we can broadcast - # This will raise an exception if they can't be broadcast together - if np.broadcast_shapes(src.shape, out.shape) != out.shape: - raise ValueError( - f"non-broadcastable output operand with shape {out.shape} " - f"doesn't match the broadcast shape {src.shape}" - ) - else: - # No output yet, so make one - out_shape = src.shape - - if dtype is not None: - out = ndarray( - shape=out_shape, - dtype=dtype, - inputs=(src,), - ) - elif out_dtype is not None: - out = ndarray( - shape=out_shape, - dtype=out_dtype, - inputs=(src,), - ) - else: - out = ndarray( - shape=out_shape, - dtype=src.dtype - if src.dtype.kind != "c" - else np.dtype(np.float32) - if src.dtype == np.dtype(np.complex64) - else np.dtype(np.float64), - inputs=(src,), - ) - - if out_dtype is None: - if out.dtype != src.dtype and not ( - op == UnaryOpCode.ABSOLUTE and src.dtype.kind == "c" - ): - temp = ndarray( - out.shape, - dtype=src.dtype, - inputs=(src,), - ) - temp._thunk.unary_op( - op, - src._thunk, - True, - extra_args, - ) - out._thunk.convert(temp._thunk) - else: - out._thunk.unary_op( - op, - src._thunk, - True, - extra_args, - ) - else: - if out.dtype != out_dtype: - temp = ndarray( - out.shape, - dtype=out_dtype, - inputs=(src,), - ) - temp._thunk.unary_op( - op, - src._thunk, - True, - extra_args, - ) - out._thunk.convert(temp._thunk) - else: - out._thunk.unary_op( - op, - src._thunk, - True, - extra_args, - ) - return out - - # For performing reduction unary operations - @classmethod - def _perform_unary_reduction( - cls, - op: UnaryRedCode, - src: ndarray, - axis: Any = None, - dtype: Union[np.dtype[Any], None] = None, - res_dtype: Union[npt.DTypeLike, None] = None, - out: Union[ndarray, None] = None, - keepdims: bool = False, - args: Union[Any, None] = None, - initial: Union[int, float, None] = None, - where: Union[ndarray, None] = None, - ) -> ndarray: - # When 'res_dtype' is not None, the input and output of the reduction - # have different types. Such reduction operators don't take a dtype of - # the accumulator - if res_dtype is not None: - assert dtype is None - dtype = src.dtype - else: - # If 'dtype' exists, that determines both the accumulation dtype - # and the output dtype - if dtype is not None: - res_dtype = dtype - elif out is not None: - dtype = out.dtype - res_dtype = out.dtype - else: - dtype = src.dtype - res_dtype = src.dtype - - # TODO: Need to require initial to be given when the array is empty - # or a where mask is given. - if ( - op - in ( - UnaryRedCode.ARGMAX, - UnaryRedCode.ARGMIN, - UnaryRedCode.MAX, - UnaryRedCode.MIN, - ) - and src.dtype.kind == "c" - ): - raise NotImplementedError( - "(arg)max/min not supported for complex-type arrays" - ) - - if axis is None: - axes = tuple(range(src.ndim)) - else: - axes = normalize_axis_tuple(axis, src.ndim) - - out_shape: NdShape = () - for dim in range(src.ndim): - if dim not in axes: - out_shape += (src.shape[dim],) - elif keepdims: - out_shape += (1,) - - if out is None: - out = ndarray( - shape=out_shape, dtype=res_dtype, inputs=(src, where) - ) - elif out.shape != out_shape: - errmsg = ( - f"the output shapes do not match: expected {out_shape} " - f"but got {out.shape}" - ) - raise ValueError(errmsg) - - if dtype != src.dtype: - src = src.astype(dtype) - - if out.dtype == res_dtype: - result = out - else: - result = ndarray( - shape=out_shape, dtype=res_dtype, inputs=(src, where) - ) - - where_array = broadcast_where(where, src.shape) - result._thunk.unary_reduction( - op, - src._thunk, - cls._get_where_thunk(where_array, src.shape), - axis, - axes, - keepdims, - args, - initial, - ) - - if result is not out: - out._thunk.convert(result._thunk) - - return out - - @classmethod - def _perform_binary_reduction( - cls, - op: BinaryOpCode, - one: ndarray, - two: ndarray, - dtype: np.dtype[Any], - extra_args: Union[tuple[Any, ...], None] = None, - ) -> ndarray: - args = (one, two) - - # We only handle bool types here for now - assert dtype is not None and dtype == np.dtype(np.bool_) - # Collapsing down to a single value in this case - # Check to see if we need to broadcast between inputs - if one.shape != two.shape: - broadcast = np.broadcast_shapes(one.shape, two.shape) - else: - broadcast = None - - common_type = cls.find_common_type(one, two) - one_thunk = one._maybe_convert(common_type, args)._thunk - two_thunk = two._maybe_convert(common_type, args)._thunk - - dst = ndarray(shape=(), dtype=dtype, inputs=args) - dst._thunk.binary_reduction( - op, - one_thunk, - two_thunk, - broadcast, - extra_args, - ) - return dst - - @classmethod - def _perform_where( - cls, mask: ndarray, one: ndarray, two: ndarray - ) -> ndarray: - args = (mask, one, two) - - mask = mask._maybe_convert(np.dtype(np.bool_), args) - - common_type = cls.find_common_type(one, two) - one = one._maybe_convert(common_type, args) - two = two._maybe_convert(common_type, args) - - # Compute the output shape - out_shape = np.broadcast_shapes(mask.shape, one.shape, two.shape) - out = ndarray(shape=out_shape, dtype=common_type, inputs=args) - out._thunk.where(mask._thunk, one._thunk, two._thunk) - return out - - @classmethod - def _perform_scan( - cls, - op: ScanCode, - src: ndarray, - axis: Any = None, - dtype: Union[npt.DTypeLike, None] = None, - out: Union[ndarray, None] = None, - nan_to_identity: bool = False, - ) -> ndarray: - if src.dtype.kind != "c" and src.dtype.kind != "f": - nan_to_identity = False - if dtype is None: - if out is None: - if src.dtype.kind == "i": - # Set dtype to default platform integer - dtype = np.int_ - else: - dtype = src.dtype - else: - dtype = out.dtype - # flatten input when axis is None - if axis is None: - axis = 0 - src_arr = src.ravel() - else: - axis = normalize_axis_index(axis, src.ndim) - src_arr = src - if out is not None: - if dtype != out.dtype: - # if out array is specified, its type overrules dtype - dtype = out.dtype - if out.shape != src_arr.shape: - raise NotImplementedError( - "Varried output shape not supported. Output must have " - "same shape as input (same size if no axis is provided" - ) - else: - out = ndarray(shape=src_arr.shape, dtype=dtype) - - if dtype != src_arr.dtype: - if nan_to_identity: - if op is ScanCode.SUM: - nan_op = ConvertCode.SUM - else: - nan_op = ConvertCode.PROD - # If convert is called, it will handle NAN conversion - nan_to_identity = False - else: - nan_op = ConvertCode.NOOP - # convert input to temporary for type conversion - temp = ndarray(shape=src_arr.shape, dtype=dtype) - temp._thunk.convert(src_arr._thunk, nan_op=nan_op) - src_arr = temp - - out._thunk.scan( - op, - src_arr._thunk, - axis=axis, - dtype=dtype, - nan_to_identity=nan_to_identity, - ) - return out - def _wrap(self, new_len: int) -> ndarray: if new_len == 1: idxs = tuple(0 for i in range(self.ndim)) @@ -4531,3 +3932,46 @@ def _wrap(self, new_len: int) -> ndarray: ) out._thunk._wrap(src=self._thunk, new_len=new_len) return out + + def stencil_hint( + self, + low_offsets: tuple[int, ...], + high_offsets: tuple[int, ...], + ) -> None: + """ + Inform cuPyNumeric that this array will be used in a stencil + computation in the following code. + + This allows cuPyNumeric to allocate space for the "ghost" elements + ahead of time, rather than discovering the full extent of accesses + incrementally, and thus avoid intermediate copies. + + For example, let's say we have a 1-D array A of size 10 and we want to + partition A across two GPUs. By default, A would be partitioned equally + and each GPU gets an instance of size 5 (GPU0 gets elements 0-4, and + GPU1 gets 5-9 inclusive). Suppose we use A in the stencil computation + `B = A[:9] + A[1:]`. The runtime would now need to adjust the + partitioning such that GPU0 has elements 0-5 and GPU1 has elements 4-9 + inclusive. Since the original instance on GPU0 does not cover index 5, + cuPyNumeric needs to allocate a full new instance that covers 0-5, + leading to an extra copy. In this case, if the code calls + `A.stencil_hint([1], [1])` to pre-allocate instances that contain the + extra elements before it uses A, the extra copies can be avoided. + + Parameters + ---------- + low_offsets: tuple[int] + Stencil offsets towards the negative direction. + high_offsets: tuple[int] + Stencil offsets towards the positive direction. + + Notes + ----- + This function currently does not behave as expected in the case where + multiple CPU/OpenMP processors use the same system memory. + + Availability + -------- + Multiple CPUs, Multiple GPUs + """ + self._thunk.stencil_hint(low_offsets, high_offsets) diff --git a/cupynumeric/_array/flags.py b/cupynumeric/_array/flags.py new file mode 100644 index 000000000..d58a5480a --- /dev/null +++ b/cupynumeric/_array/flags.py @@ -0,0 +1,82 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .array import ndarray + + +class flagsobj: + """ + Information about the memory layout of the array. + + These flags don't reflect the properties of the cuPyNumeric array, but + rather the NumPy array that will be produced if the cuPyNumeric array is + materialized on a single node. + """ + + def __init__(self, array: ndarray) -> None: + # prevent infinite __setattr__ recursion + object.__setattr__(self, "_array", array) + + def __repr__(self) -> str: + return f"""\ + C_CONTIGUOUS : {self["C"]} + F_CONTIGUOUS : {self["F"]} + OWNDATA : {self["O"]} + WRITEABLE : {self["W"]} + ALIGNED : {self["A"]} + WRITEBACKIFCOPY : {self["X"]} +""" + + def __eq__(self, other: Any) -> bool: + flags = ("C", "F", "O", "W", "A", "X") + return all(self[f] == other[f] for f in flags) # type: ignore [index] + + def __getattr__(self, name: str) -> Any: + if name == "writeable": + return self._array._writeable + flags = self._array.__array__().flags + return getattr(flags, name) + + def __setattr__(self, name: str, value: Any) -> None: + if name == "writeable": + self._check_writeable(value) + self._array._writeable = bool(value) + else: + flags = self._array.__array__().flags + setattr(flags, name, value) + + def __getitem__(self, key: Any) -> bool: + if key == "W": + return self._array._writeable + flags = self._array.__array__().flags + return flags[key] + + def __setitem__(self, key: str, value: Any) -> None: + if key == "W": + self._check_writeable(value) + self._array._writeable = bool(value) + else: + flags = self._array.__array__().flags + flags[key] = value + + def _check_writeable(self, value: Any) -> None: + if value and not self._array._writeable: + raise ValueError( + "non-writeable cupynumeric arrays cannot be made writeable" + ) diff --git a/cupynumeric/_array/thunk.py b/cupynumeric/_array/thunk.py new file mode 100644 index 000000000..d7f351f86 --- /dev/null +++ b/cupynumeric/_array/thunk.py @@ -0,0 +1,356 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np +from legate.core import Scalar + +from .._utils import is_np2 +from ..config import ( + BinaryOpCode, + ConvertCode, + ScanCode, + UnaryOpCode, + UnaryRedCode, +) +from ..types import NdShape +from .util import broadcast_where, find_common_type + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + from numpy.core.numeric import normalize_axis_tuple # type: ignore + +if TYPE_CHECKING: + import numpy.typing as npt + + from .._thunk.thunk import NumPyThunk + from .array import ndarray + + +def get_where_thunk( + where: ndarray | None, out_shape: NdShape +) -> NumPyThunk | None: + from .array import ndarray + + if where is None: + return where + if ( + not isinstance(where, ndarray) + or where.dtype != bool + or where.shape != out_shape + ): + raise RuntimeError("should have converted this earlier") + return where._thunk + + +def perform_unary_op( + op: UnaryOpCode, + src: ndarray, + out: ndarray | None = None, + extra_args: Any = None, +) -> ndarray: + from .array import ndarray + + if out is not None: + # If the shapes don't match see if we can broadcast + # This will raise an exception if they can't be broadcast together + if np.broadcast_shapes(src.shape, out.shape) != out.shape: + raise ValueError( + f"non-broadcastable output operand with shape {out.shape} " + f"doesn't match the broadcast shape {src.shape}" + ) + else: + # No output yet, so make one + out_shape = src.shape + + if op == UnaryOpCode.ANGLE: + dtype = np.dtype(np.float64) + elif op == UnaryOpCode.ROUND or src.dtype.kind != "c": + dtype = src.dtype + else: + if src.dtype == np.dtype(np.complex64): + dtype = np.dtype(np.float32) # type: ignore + else: + dtype = np.dtype(np.float64) + + out = ndarray( + shape=out_shape, + dtype=dtype, + inputs=(src,), + ) + + if out.dtype != src.dtype: + if ( + op == UnaryOpCode.ABSOLUTE and src.dtype.kind == "c" + ) or op == UnaryOpCode.ANGLE: + out._thunk.unary_op( + op, + src._thunk, + True, + extra_args, + ) + else: + temp = ndarray( + out.shape, + dtype=src.dtype, + inputs=(src,), + ) + temp._thunk.unary_op( + op, + src._thunk, + True, + extra_args, + ) + out._thunk.convert(temp._thunk) + else: + out._thunk.unary_op( + op, + src._thunk, + True, + extra_args, + ) + return out + + +def _need_upcast_for_reduction(op: UnaryRedCode, dtype: np.dtype[Any]) -> bool: + return op in (UnaryRedCode.SUM, UnaryRedCode.PROD) and dtype.kind in ( + "b", + "i", + "u", + ) + + +def perform_unary_reduction( + op: UnaryRedCode, + src: ndarray, + axis: Any = None, + dtype: np.dtype[Any] | None = None, + res_dtype: npt.DTypeLike | None = None, + out: ndarray | None = None, + keepdims: bool = False, + args: tuple[Scalar, ...] = (), + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + from .array import ndarray + + # When 'res_dtype' is not None, the input and output of the reduction + # have different types. Such reduction operators don't take a dtype of + # the accumulator + if res_dtype is not None: + assert dtype is None + dtype = src.dtype + else: + if dtype is not None: + # If 'dtype' exists, that determines both the accumulation dtype + # and the output dtype + pass + elif out is not None: + dtype = out.dtype + elif _need_upcast_for_reduction(op, src.dtype): + # upcast to conserve precision + dtype = np.dtype(np.uint64 if src.dtype.kind == "u" else np.int64) + else: + dtype = src.dtype + res_dtype = dtype + + # TODO: Need to require initial to be given when the array is empty + # or a where mask is given. + if ( + op + in ( + UnaryRedCode.ARGMAX, + UnaryRedCode.ARGMIN, + UnaryRedCode.MAX, + UnaryRedCode.MIN, + ) + and src.dtype.kind == "c" + ): + raise NotImplementedError( + "(arg)max/min not supported for complex-type arrays" + ) + + if axis is None: + axes = tuple(range(src.ndim)) + else: + axes = normalize_axis_tuple(axis, src.ndim) + + out_shape: NdShape = () + for dim in range(src.ndim): + if dim not in axes: + out_shape += (src.shape[dim],) + elif keepdims: + out_shape += (1,) + + if out is None: + out = ndarray(shape=out_shape, dtype=res_dtype, inputs=(src, where)) + elif out.shape != out_shape: + errmsg = ( + f"the output shapes do not match: expected {out_shape} " + f"but got {out.shape}" + ) + raise ValueError(errmsg) + + if dtype != src.dtype: + src = src.astype(dtype) + + if out.dtype == res_dtype: + result = out + else: + result = ndarray(shape=out_shape, dtype=res_dtype, inputs=(src, where)) + + where_array = broadcast_where(where, src.shape) + result._thunk.unary_reduction( + op, + src._thunk, + get_where_thunk(where_array, src.shape), + axis, + axes, + keepdims, + args, + initial, + ) + + if result is not out: + out._thunk.convert(result._thunk) + + return out + + +def perform_binary_reduction( + op: BinaryOpCode, + one: ndarray, + two: ndarray, + dtype: np.dtype[Any], + extra_args: tuple[Scalar, ...] = (), +) -> ndarray: + from .array import ndarray + + args = (one, two) + + # We only handle bool types here for now + assert dtype is not None and dtype == np.dtype(bool) + + # Collapsing down to a single value in this case + # Check to see if we need to broadcast between inputs + if one.shape != two.shape: + broadcast = np.broadcast_shapes(one.shape, two.shape) + else: + broadcast = None + + common_type = find_common_type(one, two) + one_thunk = one._maybe_convert(common_type, args)._thunk + two_thunk = two._maybe_convert(common_type, args)._thunk + + dst = ndarray(shape=(), dtype=dtype, inputs=args) + dst._thunk.binary_reduction( + op, + one_thunk, + two_thunk, + broadcast, + extra_args, + ) + return dst + + +def perform_where(mask: ndarray, one: ndarray, two: ndarray) -> ndarray: + from .array import ndarray + + args = (mask, one, two) + + mask = mask._maybe_convert(np.dtype(bool), args) + + common_type = find_common_type(one, two) + one = one._maybe_convert(common_type, args) + two = two._maybe_convert(common_type, args) + + # Compute the output shape + out_shape = np.broadcast_shapes(mask.shape, one.shape, two.shape) + out = ndarray(shape=out_shape, dtype=common_type, inputs=args) + out._thunk.where(mask._thunk, one._thunk, two._thunk) + return out + + +def perform_scan( + op: ScanCode, + src: ndarray, + axis: Any = None, + dtype: npt.DTypeLike | None = None, + out: ndarray | None = None, + nan_to_identity: bool = False, +) -> ndarray: + from .array import ndarray + + if src.dtype.kind != "c" and src.dtype.kind != "f": + nan_to_identity = False + + if dtype is None: + if out is None: + if src.dtype.kind == "i": + # Set dtype to default platform integer + dtype = np.int_ + else: + dtype = src.dtype + else: + dtype = out.dtype + + # flatten input when axis is None + if axis is None: + axis = 0 + src_arr = src.ravel() + else: + axis = normalize_axis_index(axis, src.ndim) + src_arr = src + + if out is not None: + if dtype != out.dtype: + # if out array is specified, its type overrules dtype + dtype = out.dtype + if out.shape != src_arr.shape: + raise NotImplementedError( + "Varried output shape not supported. Output must have " + "same shape as input (same size if no axis is provided" + ) + else: + out = ndarray(shape=src_arr.shape, dtype=dtype) + + if dtype != src_arr.dtype: + if nan_to_identity: + if op is ScanCode.SUM: + nan_op = ConvertCode.SUM + else: + nan_op = ConvertCode.PROD + # If convert is called, it will handle NAN conversion + nan_to_identity = False + else: + nan_op = ConvertCode.NOOP + # convert input to temporary for type conversion + temp = ndarray(shape=src_arr.shape, dtype=dtype) + temp._thunk.convert(src_arr._thunk, nan_op=nan_op) + src_arr = temp + + out._thunk.scan( + op, + src_arr._thunk, + axis=axis, + dtype=dtype, + nan_to_identity=nan_to_identity, + ) + return out diff --git a/cupynumeric/_array/util.py b/cupynumeric/_array/util.py new file mode 100644 index 000000000..e0096db85 --- /dev/null +++ b/cupynumeric/_array/util.py @@ -0,0 +1,221 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import operator +from functools import wraps +from inspect import signature +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ParamSpec, + Sequence, + TypeVar, + cast, +) + +import numpy as np + +from ..runtime import runtime +from ..types import NdShape + +if TYPE_CHECKING: + import numpy.typing as npt + + from ..types import NdShapeLike + from .array import ndarray + + +R = TypeVar("R") +P = ParamSpec("P") + + +def add_boilerplate( + *array_params: str, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """ + Adds required boilerplate to the wrapped cupynumeric.ndarray or + module-level function. + + Every time the wrapped function is called, this wrapper will convert all + specified array-like parameters to cuPyNumeric ndarrays. Additionally, any + "out" or "where" arguments will also always be automatically converted. + """ + to_convert = set(array_params) + assert len(to_convert) == len(array_params) + + def decorator(func: Callable[P, R]) -> Callable[P, R]: + assert not hasattr( + func, "__wrapped__" + ), "this decorator must be the innermost" + + params = signature(func).parameters + extra = to_convert - set(params) + assert len(extra) == 0, f"unknown parameter(s): {extra}" + + # we also always want to convert "out" and "where" + # even if they are not explicitly specified by the user + to_convert.update(("out", "where")) + + out_idx = -1 + indices = set() + for idx, param in enumerate(params): + if param == "out": + out_idx = idx + if param in to_convert: + indices.add(idx) + + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> R: + # convert specified non-None positional arguments, making sure + # that any out-parameters are appropriately writeable + converted_args = [] + for idx, arg in enumerate(args): + if idx in indices and arg is not None: + if idx == out_idx: + arg = convert_to_cupynumeric_ndarray(arg, share=True) + if not arg.flags.writeable: + raise ValueError("out is not writeable") + else: + arg = convert_to_cupynumeric_ndarray(arg) + converted_args.append(arg) + args = tuple(converted_args) + + # convert specified non-None keyword arguments, making sure + # that any out-parameters are appropriately writeable + for k, v in kwargs.items(): + if k in to_convert and v is not None: + if k == "out": + kwargs[k] = convert_to_cupynumeric_ndarray( + v, share=True + ) + if not kwargs[k].flags.writeable: + raise ValueError("out is not writeable") + else: + kwargs[k] = convert_to_cupynumeric_ndarray(v) + + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def broadcast_where(where: ndarray | None, shape: NdShape) -> ndarray | None: + if where is not None and where.shape != shape: + from .._module import broadcast_to + + where = broadcast_to(where, shape) + return where + + +def convert_to_cupynumeric_ndarray(obj: Any, share: bool = False) -> ndarray: + from .array import ndarray + + # If this is an instance of one of our ndarrays then we're done + if isinstance(obj, ndarray): + return obj + # Ask the runtime to make a numpy thunk for this object + thunk = runtime.get_numpy_thunk(obj, share=share) + writeable = ( + obj.flags.writeable if isinstance(obj, np.ndarray) and share else True + ) + return ndarray(shape=None, thunk=thunk, writeable=writeable) + + +def maybe_convert_to_np_ndarray(obj: Any) -> Any: + """ + Converts cuPyNumeric arrays into NumPy arrays, otherwise has no effect. + """ + from ..ma import MaskedArray + from .array import ndarray + + if isinstance(obj, (ndarray, MaskedArray)): + return obj.__array__() + return obj + + +def check_writeable(arr: ndarray | tuple[ndarray, ...] | None) -> None: + """ + Check if the current array is writeable + This check needs to be manually inserted + with consideration on the behavior of the corresponding method + """ + if arr is None: + return + check_list = (arr,) if not isinstance(arr, tuple) else arr + if any(not arr.flags.writeable for arr in check_list): + raise ValueError("array is not writeable") + + +def sanitize_shape( + shape: NdShapeLike | Sequence[Any] | npt.NDArray[Any] | ndarray, +) -> NdShape: + from .array import ndarray + + seq: tuple[Any, ...] + if isinstance(shape, (ndarray, np.ndarray)): + if shape.ndim == 0: + seq = (shape.__array__().item(),) + else: + seq = tuple(shape.__array__()) + elif np.isscalar(shape): + seq = (shape,) + else: + seq = tuple(cast(NdShape, shape)) + try: + # Unfortunately, we can't do this check using + # 'isinstance(value, int)', as the values in a NumPy ndarray + # don't satisfy the predicate (they have numpy value types, + # such as numpy.int64). + result = tuple(operator.index(value) for value in seq) + except TypeError: + raise TypeError( + "expected a sequence of integers or a single integer, " + f"got {shape!r}" + ) + return result + + +def find_common_type(*args: ndarray) -> np.dtype[Any]: + """Determine common type following NumPy's coercion rules. + + Parameters + ---------- + *args : ndarray + A list of ndarrays + + Returns + ------- + datatype : data-type + The type that results from applying the NumPy type promotion rules + to the arguments. + """ + array_types = list() + scalars = list() + for array in args: + if array.ndim == 0: + scalars.append(array.dtype.type(0)) + else: + array_types.append(array.dtype) + return np.result_type(*array_types, *scalars) + + +T = TypeVar("T") + + +def tuple_pop(tup: tuple[T, ...], index: int) -> tuple[T, ...]: + return tup[:index] + tup[index + 1 :] diff --git a/cupynumeric/_module/__init__.py b/cupynumeric/_module/__init__.py new file mode 100644 index 000000000..e96566d91 --- /dev/null +++ b/cupynumeric/_module/__init__.py @@ -0,0 +1,146 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import Any +from warnings import warn + +# The CLLR functions for the numpy module are broken up more or less according +# to the docs at https://numpy.org/doc/stable/reference/routines.html +# +# There are some discrepencies since some functions are repeated in multiple +# locations and since indexing routines are listed elsehwere for some reason. +# Sections/sub-modules that are currently missing are noted in comments. + +# --- Array Creation Routines +# https://numpy.org/doc/stable/reference/routines.array-creation.html + +from .creation_shape import * # From shape or value +from .creation_data import * # From existing data +from .creation_ranges import * # Numerical Ranges +from .creation_matrices import * # Building Matrices + +# --- Array manipulation routines +# https://numpy.org/doc/stable/reference/routines.array-manipulation.html +# +# from .array_kind import * # Changing kind of array +# from .array_add_remove import * # Adding and removing elements + +from .array_basic import * # Basic operations +from .array_shape import * # Changing array shape +from .array_transpose import * # Transpose-like operations +from .array_dimension import * # Changing number of dimensions +from .array_joining import * # Joining arrays +from .array_splitting import * # Splitting arrays +from .array_tiling import * # Tiling arrays +from .array_rearrange import * # Rearranging elements + +# --- Binary operations +# https://numpy.org/doc/stable/reference/routines.bitwise.html +# +# from .binary_elementwise_bit_ops import * # Elementwise bit operations +# from .binary_output import * # Output formatting + +from .binary_bit_packing import * # Bit packing + +# --- Indexing routines +# +# These routines in the numpy module are a bit odd, they are documented under +# the array ref: https://numpy.org/doc/stable/reference/arrays.indexing.html + +from .indexing import * + +# --- Input and output +# https://numpy.org/doc/stable/reference/routines.io.html +# +# from .io_text import * # Text files +# from .io_raw import * # Raw binary files +# from .io_string import * # String formatting +# from .io_memory import * # Memory mapping files +# from .io_text import * # Text formatting options +# from .io_base import * # Base-n representations +# from .io_data import * # Data sources +# from .io_binary import * # Binary format description + +from .io_numpy import * # NumPy binary files (NPY, NPZ) + +# --- Linear Algebra +# https://numpy.org/doc/stable/reference/routines.linalg.html + +from .linalg_mvp import * # Matrix and vector products + +# --- Logic functions +# https://numpy.org/doc/stable/reference/routines.logic.html +# +# from .logic_ops import * # Logical operations + +from .logic_truth import * # Truth value testing +from .logic_array_contents import * # Array contents +from .logic_array_type import * # Array type testing +from .logic_comparison import * # Comparison + +# --- Mathematical functions +# https://numpy.org/doc/stable/reference/routines.math.html +# +# from .math_trig import * # Trigonometric functions +# from .math_hyp import * # Hyperbolic functions +# from .math_exp_log import * # Exponents and logarithms +# from .math_other import * # Other special funtions +# from .math_floating import * # Floating point routines +# from .math_arithmetic import * # Arithmetic opertations + +from .math_rounding import * # Rounding +from .math_sum_prod_diff import * # Sums, products, differences +from .math_complex import * # Handling complex numbers +from .math_extrema import * # Extrema finding +from .math_misc import * # Miscellaneous + +# --- Set routines +# https://numpy.org/doc/stable/reference/routines.set.html +# +# from .sets_boolean import * # Boolean operations + +from .sets_making import * # Making proper sets + +# --- Sorting, searching, and counting +# https://numpy.org/doc/stable/reference/routines.sort.html + +from .ssc_sorting import * # Sorting +from .ssc_searching import * # Searching +from .ssc_counting import * # Counting + +# --- Statistics +# https://numpy.org/doc/stable/reference/routines.statistics.html +# + +from .stats_order import * # Order statistics +from .stats_avgs_vars import * # Averages and variances +from .stats_correlating import * # Correlating +from .stats_histograms import * # Histograms + +# --- Window functions +# https://numpy.org/doc/stable/reference/routines.window.html + +from .window import * # Various windows + +# --- numpy.test (disabled) + + +def test(*args: Any, **kw: Any) -> None: + warn( + "cuPyNumeric cannot execute numpy.test() due to reliance " + "on Numpy internals. For information about running the " + "cuPyNumeric test suite, see: https://docs.nvidia.com/cupynumeric/latest/developer/index.html" + ) diff --git a/cunumeric/_unary_red_utils.py b/cupynumeric/_module/_unary_red_utils.py similarity index 95% rename from cunumeric/_unary_red_utils.py rename to cupynumeric/_module/_unary_red_utils.py index c115b50d6..17c493499 100644 --- a/cunumeric/_unary_red_utils.py +++ b/cupynumeric/_module/_unary_red_utils.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ from __future__ import annotations -from .config import UnaryRedCode +from ..config import UnaryRedCode # corresponding non-nan unary reduction ops for nan unary reduction ops _EQUIVALENT_NON_NAN_OPS: dict[UnaryRedCode, UnaryRedCode] = { diff --git a/cupynumeric/_module/array_basic.py b/cupynumeric/_module/array_basic.py new file mode 100644 index 000000000..5fed9e38b --- /dev/null +++ b/cupynumeric/_module/array_basic.py @@ -0,0 +1,81 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import add_boilerplate + +if TYPE_CHECKING: + from .._array.array import ndarray + from ..types import NdShape + + +@add_boilerplate("a") +def ndim(a: ndarray) -> int: + """ + + Return the number of dimensions of an array. + + Parameters + ---------- + a : array_like + Input array. If it is not already an ndarray, a conversion is + attempted. + + Returns + ------- + number_of_dimensions : int + The number of dimensions in `a`. Scalars are zero-dimensional. + + See Also + -------- + ndarray.ndim : equivalent method + shape : dimensions of array + ndarray.shape : dimensions of array + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return 0 if a is None else a.ndim + + +@add_boilerplate("a") +def shape(a: ndarray) -> NdShape: + """ + + Return the shape of an array. + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + shape : tuple[int, ...] + The elements of the shape tuple give the lengths of the + corresponding array dimensions. + + See Also + -------- + numpy.shape + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.shape diff --git a/cupynumeric/_module/array_dimension.py b/cupynumeric/_module/array_dimension.py new file mode 100644 index 000000000..01629b2cb --- /dev/null +++ b/cupynumeric/_module/array_dimension.py @@ -0,0 +1,443 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Iterable, Sequence + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate, convert_to_cupynumeric_ndarray +from .._utils import is_np2 +from .creation_data import array + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import normalize_axis_tuple # type: ignore + +if TYPE_CHECKING: + from ..types import NdShape, NdShapeLike + + +def _reshape_recur(ndim: int, arr: ndarray) -> tuple[int, ...]: + if arr.ndim < ndim: + cur_shape: tuple[int, ...] = _reshape_recur(ndim - 1, arr) + if ndim == 2: + cur_shape = (1,) + cur_shape + else: + cur_shape = cur_shape + (1,) + else: + cur_shape = arr.shape + return cur_shape + + +def _atleast_nd(ndim: int, arys: Sequence[ndarray]) -> list[ndarray] | ndarray: + inputs = list(convert_to_cupynumeric_ndarray(arr) for arr in arys) + # 'reshape' change the shape of arrays + # only when arr.shape != _reshape_recur(ndim,arr) + result = list(arr.reshape(_reshape_recur(ndim, arr)) for arr in inputs) + # if the number of arrays in `arys` is 1, + # the return value is a single array + if len(result) == 1: + return result[0] + return result + + +def atleast_1d(*arys: ndarray) -> list[ndarray] | ndarray: + """ + + Convert inputs to arrays with at least one dimension. + Scalar inputs are converted to 1-dimensional arrays, + whilst higher-dimensional inputs are preserved. + + Parameters + ---------- + *arys : array_like + One or more input arrays. + + Returns + ------- + ret : ndarray + An array, or list of arrays, each with a.ndim >= 1. + Copies are made only if necessary. + + See Also + -------- + numpy.atleast_1d + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return _atleast_nd(1, arys) + + +def atleast_2d(*arys: ndarray) -> list[ndarray] | ndarray: + """ + + View inputs as arrays with at least two dimensions. + + Parameters + ---------- + *arys : array_like + One or more array-like sequences. + Non-array inputs are converted to arrays. + Arrays that already have two or more dimensions are preserved. + + Returns + ------- + res, res2, … : ndarray + An array, or list of arrays, each with a.ndim >= 2. + Copies are avoided where possible, and + views with two or more dimensions are returned. + + See Also + -------- + numpy.atleast_2d + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return _atleast_nd(2, arys) + + +def atleast_3d(*arys: ndarray) -> list[ndarray] | ndarray: + """ + + View inputs as arrays with at least three dimensions. + + Parameters + ---------- + *arys : array_like + One or more array-like sequences. + Non-array inputs are converted to arrays. + Arrays that already have three or more dimensions are preserved. + + Returns + ------- + res, res2, … : ndarray + An array, or list of arrays, each with a.ndim >= 3. + Copies are avoided where possible, and + views with three or more dimensions are returned. + For example, a 1-D array of shape (N,) becomes + a view of shape (1, N, 1), and a 2-D array of shape (M, N) + becomes a view of shape (M, N, 1). + + See Also + -------- + numpy.atleast_3d + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return _atleast_nd(3, arys) + + +@add_boilerplate("a") +def squeeze(a: ndarray, axis: NdShapeLike | None = None) -> ndarray: + """ + + Remove single-dimensional entries from the shape of an array. + + Parameters + ---------- + a : array_like + Input data. + axis : None or int or tuple[int], optional + Selects a subset of the single-dimensional entries in the + shape. If an axis is selected with shape entry greater than + one, an error is raised. + + Returns + ------- + squeezed : ndarray + The input array, but with all or a subset of the + dimensions of length 1 removed. This is always `a` itself + or a view into `a`. + + Raises + ------ + ValueError + If `axis` is not None, and an axis being squeezed is not of length 1 + + See Also + -------- + numpy.squeeze + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.squeeze(axis=axis) + + +def broadcast_shapes(*args: NdShapeLike | Sequence[NdShapeLike]) -> NdShape: + """ + + Broadcast the input shapes into a single shape. + + Parameters + ---------- + `*args` : tuples of ints, or ints + The shapes to be broadcast against each other. + + Returns + ------- + tuple : Broadcasted shape. + + See Also + -------- + numpy.broadcast_shapes + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + # TODO: expected "Union[SupportsIndex, Sequence[SupportsIndex]]" + return np.broadcast_shapes(*args) # type: ignore [arg-type] + + +def _broadcast_to( + arr: ndarray, + shape: NdShapeLike, + subok: bool = False, + broadcasted: bool = False, +) -> ndarray: + # create an array object w/ options passed from 'broadcast' routines + arr = array(arr, copy=False, subok=subok) + # 'broadcast_to' returns a read-only view of the original array + out_shape = broadcast_shapes(arr.shape, shape) + if out_shape != shape: + raise ValueError( + f"cannot broadcast an array of shape {arr.shape} to {shape}" + ) + result = ndarray( + shape=out_shape, + thunk=arr._thunk.broadcast_to(out_shape), + writeable=False, + ) + return result + + +@add_boilerplate("arr") +def broadcast_to( + arr: ndarray, shape: NdShapeLike, subok: bool = False +) -> ndarray: + """ + + Broadcast an array to a new shape. + + Parameters + ---------- + arr : array_like + The array to broadcast. + shape : tuple or int + The shape of the desired array. + A single integer i is interpreted as (i,). + subok : bool, optional + This option is ignored by cuPyNumeric. + + Returns + ------- + broadcast : array + A readonly view on the original array with the given shape. + It is typically not contiguous. + Furthermore, more than one element of a broadcasted array + may refer to a single memory location. + + See Also + -------- + numpy.broadcast_to + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + return _broadcast_to(arr, shape, subok) + + +def _broadcast_arrays( + arrs: list[ndarray], + subok: bool = False, +) -> list[ndarray]: + # create an arry object w/ options passed from 'broadcast' routines + arrays = [array(arr, copy=False, subok=subok) for arr in arrs] + # check if the broadcast can happen in the input list of arrays + shapes = [arr.shape for arr in arrays] + out_shape = broadcast_shapes(*shapes) + # broadcast to the final shape + arrays = [_broadcast_to(arr, out_shape, subok) for arr in arrays] + return arrays + + +def broadcast_arrays(*args: Any, subok: bool = False) -> list[ndarray]: + """ + + Broadcast any number of arrays against each other. + + Parameters + ---------- + `*args` : array_likes + The arrays to broadcast. + + subok : bool, optional + This option is ignored by cuPyNumeric + + Returns + ------- + broadcasted : list of arrays + These arrays are views on the original arrays. + They are typically not contiguous. + Furthermore, more than one element of a broadcasted array + may refer to a single memory location. + If you need to write to the arrays, make copies first. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + arrs = [convert_to_cupynumeric_ndarray(arr) for arr in args] + return _broadcast_arrays(arrs, subok=subok) + + +class broadcast: + """Produce an object that broadcasts input parameters against one another. + It has shape and nd properties and may be used as an iterator. + + Parameters + ---------- + `*arrays` : array_likes + The arrays to broadcast. + + Returns + ------- + b: broadcast + Broadcast the input parameters against one another, and return an + object that encapsulates the result. Amongst others, it has shape + and nd properties, and may be used as an iterator. + + """ + + def __init__(self, *arrays: Any) -> None: + arrs = [convert_to_cupynumeric_ndarray(arr) for arr in arrays] + broadcasted = _broadcast_arrays(arrs) + self._iters = tuple(arr.flat for arr in broadcasted) + self._index = 0 + self._shape = broadcasted[0].shape + self._size = np.prod(self.shape, dtype=int) + + def __iter__(self) -> broadcast: + self._index = 0 + return self + + def __next__(self) -> Any: + if self._index < self.size: + result = tuple(each[self._index] for each in self._iters) + self._index += 1 + return result + raise StopIteration + + def reset(self) -> None: + """Reset the broadcasted result's iterator(s).""" + self._index = 0 + + @property + def index(self) -> int: + """current index in broadcasted result""" + return self._index + + @property + def iters(self) -> tuple[Iterable[Any], ...]: + """tuple of iterators along self's "components." """ + return self._iters + + @property + def numiter(self) -> int: + """Number of iterators possessed by the broadcasted result.""" + return len(self._iters) + + @property + def nd(self) -> int: + """Number of dimensions of broadcasted result.""" + return self.ndim + + @property + def ndim(self) -> int: + """Number of dimensions of broadcasted result.""" + return len(self.shape) + + @property + def shape(self) -> NdShape: + """Shape of broadcasted result.""" + return self._shape + + @property + def size(self) -> int: + """Total size of broadcasted result.""" + return self._size + + +@add_boilerplate("a") +def expand_dims( + a: ndarray, axis: int | tuple[int, ...] | list[int] +) -> ndarray: + """ + Expand the shape of an array. + + Insert a new axis that will appear at the `axis` position in the expanded + array shape. + + Parameters + ---------- + a : array_like + Input array. + axis : int or tuple of ints + Position in the expanded axes where the new axis (or axes) is placed. + + Returns + ------- + result : ndarray + View of `a` with the number of dimensions increased. + + See Also + -------- + squeeze : The inverse operation, removing singleton dimensions + reshape : Insert, remove, and combine dimensions, and resize existing ones + atleast_1d, atleast_2d, atleast_3d + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if isinstance(axis, int): + axis = (axis,) + + out_ndim = len(axis) + a.ndim + normalized_axis = normalize_axis_tuple(axis, out_ndim) + + shape_it = iter(a.shape) + shape = [ + 1 if ax in normalized_axis else next(shape_it) + for ax in range(out_ndim) + ] + + return a.reshape(shape) diff --git a/cupynumeric/_module/array_joining.py b/cupynumeric/_module/array_joining.py new file mode 100644 index 000000000..fbdf2adda --- /dev/null +++ b/cupynumeric/_module/array_joining.py @@ -0,0 +1,702 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from itertools import chain +from typing import TYPE_CHECKING, Any, Sequence + +import numpy as np + +from .._array.array import ndarray +from .._array.util import convert_to_cupynumeric_ndarray +from .._utils import is_np2 +from .array_dimension import _atleast_nd + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + +if TYPE_CHECKING: + import numpy.typing as npt + + from .._ufunc.ufunc import CastingKind + from ..types import NdShape + +_builtin_any = any +_builtin_max = max +_builtin_sum = sum + +casting_kinds: tuple[CastingKind, ...] = ( + "no", + "equiv", + "safe", + "same_kind", + "unsafe", +) + + +class ArrayInfo: + def __init__( + self, ndim: int, shape: NdShape, dtype: np.dtype[Any] + ) -> None: + self.ndim = ndim + self.shape = shape + self.dtype = dtype + + +def convert_to_array_form(indices: Sequence[int]) -> str: + return "".join(f"[{coord}]" for coord in indices) + + +def check_list_depth(arr: Any, prefix: NdShape = (0,)) -> int: + if not isinstance(arr, list): + return 0 + elif len(arr) == 0: + raise ValueError( + f"List at arrays{convert_to_array_form(prefix)} cannot be empty" + ) + + depths = list( + check_list_depth(each, prefix + (idx,)) for idx, each in enumerate(arr) + ) + + if len(set(depths)) != 1: # this should be one + # If we're here elements don't have the same depth + first_depth = depths[0] + for idx, other_depth in enumerate(depths[1:]): + if other_depth != first_depth: + raise ValueError( + "List depths are mismatched. First element was at depth " + f"{first_depth}, but there is an element at" + f" depth {other_depth}, " + f"arrays{convert_to_array_form(prefix + (idx + 1,))}" + ) + + return depths[0] + 1 + + +def check_shape_with_axis( + inputs: list[ndarray], + func_name: str, + axis: int, +) -> None: + ndim = inputs[0].ndim + shape = inputs[0].shape + + axis = normalize_axis_index(axis, ndim) + if ndim >= 1: + if _builtin_any( + shape[:axis] != inp.shape[:axis] + or shape[axis + 1 :] != inp.shape[axis + 1 :] + for inp in inputs + ): + raise ValueError( + f"All arguments to {func_name} " + "must have the same " + "dimension size in all dimensions " + "except the target axis" + ) + return + + +def check_shape_dtype_without_axis( + inputs: Sequence[ndarray], + func_name: str, + dtype: npt.DTypeLike | None = None, + casting: CastingKind = "same_kind", +) -> tuple[list[ndarray], ArrayInfo]: + if len(inputs) == 0: + raise ValueError("need at least one array to concatenate") + + inputs = list(convert_to_cupynumeric_ndarray(inp) for inp in inputs) + ndim = inputs[0].ndim + shape = inputs[0].shape + + if _builtin_any(ndim != inp.ndim for inp in inputs): + raise ValueError( + f"All arguments to {func_name} " + "must have the same number of dimensions" + ) + + # Cast arrays with the passed arguments (dtype, casting) + if dtype is None: + dtype = np.result_type(*[inp.dtype for inp in inputs]) + else: + dtype = np.dtype(dtype) + + converted = list(inp.astype(dtype, casting=casting) for inp in inputs) + return converted, ArrayInfo(ndim, shape, dtype) + + +def _block_collect_slices( + arr: ndarray | Sequence[ndarray], cur_depth: int, depth: int +) -> tuple[list[Any], list[tuple[slice, ...]], Sequence[ndarray]]: + # collects slices for each array in `arr` + # the outcome will be slices on every dimension of the output array + # for each array in `arr` + if cur_depth < depth: + sublist_results = list( + _block_collect_slices(each, cur_depth + 1, depth) for each in arr + ) + # 'sublist_results' contains a list of 3-way tuples, + # for arrays, out_shape of the sublist, and slices + arrays, outshape_list, slices = zip(*sublist_results) + max_ndim = _builtin_max( + 1 + (depth - cur_depth), *(len(each) for each in outshape_list) + ) + outshape_list = list( + ((1,) * (max_ndim - len(each)) + tuple(each)) + for each in outshape_list + ) + leading_dim = _builtin_sum( + each[-1 + (cur_depth - depth)] for each in outshape_list + ) + # flatten array lists from sublists into a single list + arrays = list(chain(*arrays)) + # prepares the out_shape of the current list + out_shape = list(outshape_list[0]) + out_shape[-1 + cur_depth - depth] = leading_dim + offset = 0 + updated_slices = [] + # update the dimension in each slice for the current axis + for shape, slice_list in zip(outshape_list, slices): + cur_dim = shape[-1 + cur_depth - depth] + updated_slices.append( + list( + (slice(offset, offset + cur_dim),) + each + for each in slice_list + ) + ) + offset += cur_dim + # flatten lists of slices into a single list + slices = list(chain(*updated_slices)) + else: + arrays = list(convert_to_cupynumeric_ndarray(inp) for inp in arr) + common_shape = arrays[0].shape + if len(arr) > 1: + arrays, common_info = check_shape_dtype_without_axis( + arrays, block.__name__ + ) + common_shape = common_info.shape + check_shape_with_axis(arrays, block.__name__, axis=-1) + # the initial slices for each arr on arr.shape[-1] + out_shape, slices, arrays = _collect_outshape_slices( + arrays, common_shape, axis=-1 + len(common_shape) + ) + + return arrays, out_shape, slices + + +def _block_slicing(arrays: Sequence[ndarray], depth: int) -> ndarray: + # collects the final slices of input arrays and assign them at once + arrays, out_shape, slices = _block_collect_slices(arrays, 1, depth) + out_array = ndarray(shape=out_shape, inputs=arrays) + + for dest, inp in zip(slices, arrays): + out_array[(Ellipsis,) + tuple(dest)] = inp + + return out_array + + +def _collect_outshape_slices( + inputs: Sequence[ndarray], common_shape: NdShape, axis: int +) -> tuple[list[Any], list[tuple[slice, ...]], Sequence[ndarray]]: + leading_dim = _builtin_sum(arr.shape[axis] for arr in inputs) + out_shape = list(common_shape) + out_shape[axis] = leading_dim + post_idx = (slice(None),) * len(out_shape[axis + 1 :]) + slices = [] + offset = 0 + # collect slices for arrays in `inputs` + inputs = list(inp for inp in inputs if inp.size > 0) + for inp in inputs: + slices.append((slice(offset, offset + inp.shape[axis]),) + post_idx) + offset += inp.shape[axis] + + return out_shape, slices, inputs + + +def _concatenate( + inputs: Sequence[ndarray], + common_info: ArrayInfo, + axis: int = 0, + out: ndarray | None = None, + dtype: npt.DTypeLike | None = None, + casting: CastingKind = "same_kind", +) -> ndarray: + if axis < 0: + axis += len(common_info.shape) + out_shape, slices, inputs = _collect_outshape_slices( + inputs, common_info.shape, axis + ) + + if out is None: + out_array = ndarray( + shape=out_shape, dtype=common_info.dtype, inputs=inputs + ) + else: + out = convert_to_cupynumeric_ndarray(out) + if not isinstance(out, ndarray): + raise TypeError("out should be ndarray") + elif list(out.shape) != out_shape: + raise ValueError( + f"out.shape({out.shape}) is not matched " + f"to the result shape of concatenation ({out_shape})" + ) + out_array = out + + for dest, src in zip(slices, inputs): + out_array[(Ellipsis,) + dest] = src + + return out_array + + +def append(arr: ndarray, values: ndarray, axis: int | None = None) -> ndarray: + """ + + Append values to the end of an array. + + Parameters + ---------- + arr : array_like + Values are appended to a copy of this array. + values : array_like + These values are appended to a copy of arr. It must be of the correct + shape (the same shape as arr, excluding axis). If axis is not + specified, values can be any shape and will be flattened before use. + axis : int, optional + The axis along which values are appended. If axis is not given, both + `arr` and `values` are flattened before use. + + Returns + ------- + res : ndarray + A copy of arr with values appended to axis. + + See Also + -------- + numpy.append + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + # Check to see if we can build a new tuple of cuPyNumeric arrays + inputs = list(convert_to_cupynumeric_ndarray(inp) for inp in [arr, values]) + return concatenate(inputs, axis) + + +def block(arrays: Sequence[Any]) -> ndarray: + """ + Assemble an nd-array from nested lists of blocks. + + Blocks in the innermost lists are concatenated (see concatenate) + along the last dimension (-1), then these are concatenated along + the second-last dimension (-2), and so on until the outermost + list is reached. + + Blocks can be of any dimension, but will not be broadcasted using + the normal rules. Instead, leading axes of size 1 are inserted, + to make block.ndim the same for all blocks. This is primarily useful + for working with scalars, and means that code like np.block([v, 1]) + is valid, where v.ndim == 1. + + When the nested list is two levels deep, this allows block matrices + to be constructed from their components. + + Parameters + ---------- + arrays : nested list of array_like or scalars + If passed a single ndarray or scalar (a nested list of depth 0), + this is returned unmodified (and not copied). + + Elements shapes must match along the appropriate axes (without + broadcasting), but leading 1s will be prepended to the shape as + necessary to make the dimensions match. + + Returns + ------- + block_array : ndarray + The array assembled from the given blocks. + The dimensionality of the output is equal to the greatest of: * the + dimensionality of all the inputs * the depth to which the input list + is nested + + Raises + ------ + ValueError + If list depths are mismatched - for instance, [[a, b], c] is + illegal, and should be spelt [[a, b], [c]] + If lists are empty - for instance, [[a, b], []] + + See Also + -------- + numpy.block + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + # arrays should concatenate from innermost subarrays + # the 'arrays' should be balanced tree + # check if the 'arrays' is a balanced tree + depth = check_list_depth(arrays) + + result = _block_slicing(arrays, depth) + return result + + +def concatenate( + inputs: Sequence[ndarray], + axis: int | None = 0, + out: ndarray | None = None, + dtype: npt.DTypeLike | None = None, + casting: CastingKind = "same_kind", +) -> ndarray: + """ + + concatenate((a1, a2, ...), axis=0, out=None, dtype=None, + casting="same_kind") + + Join a sequence of arrays along an existing axis. + + Parameters + ---------- + a1, a2, ... : Sequence[array_like] + The arrays must have the same shape, except in the dimension + corresponding to `axis` (the first, by default). + axis : int, optional + The axis along which the arrays will be joined. If axis is None, + arrays are flattened before use. Default is 0. + out : ndarray, optional + If provided, the destination to place the result. The shape must be + correct, matching that of what concatenate would have returned if no + out argument were specified. + dtype : str or data-type + If provided, the destination array will have this dtype. Cannot be + provided together with `out`. + casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional + Controls what kind of data casting may occur. Defaults to 'same_kind'. + + Returns + ------- + res : ndarray + The concatenated array. + + See Also + -------- + numpy.concatenate + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if dtype is not None and out is not None: + raise TypeError( + "concatenate() only takes `out` or `dtype` as an argument," + "but both were provided." + ) + + if casting not in casting_kinds: + raise ValueError( + "casting must be one of 'no', 'equiv', " + "'safe', 'same_kind', or 'unsafe'" + ) + + # flatten arrays if axis == None and concatenate arrays on the first axis + if axis is None: + # Reshape arrays in the `array_list` to handle scalars + reshaped = _atleast_nd(1, inputs) + if not isinstance(reshaped, list): + reshaped = [reshaped] + inputs = list(inp.ravel() for inp in reshaped) + axis = 0 + + # Check to see if we can build a new tuple of cuPyNumeric arrays + cupynumeric_inputs, common_info = check_shape_dtype_without_axis( + inputs, concatenate.__name__, dtype, casting + ) + check_shape_with_axis(cupynumeric_inputs, concatenate.__name__, axis) + + return _concatenate( + cupynumeric_inputs, + common_info, + axis, + out, + dtype, + casting, + ) + + +def stack( + arrays: Sequence[ndarray], axis: int = 0, out: ndarray | None = None +) -> ndarray: + """ + + Join a sequence of arrays along a new axis. + + The ``axis`` parameter specifies the index of the new axis in the + dimensions of the result. For example, if ``axis=0`` it will be the first + dimension and if ``axis=-1`` it will be the last dimension. + + Parameters + ---------- + arrays : Sequence[array_like] + Each array must have the same shape. + + axis : int, optional + The axis in the result array along which the input arrays are stacked. + + out : ndarray, optional + If provided, the destination to place the result. The shape must be + correct, matching that of what stack would have returned if no + out argument were specified. + + Returns + ------- + stacked : ndarray + The stacked array has one more dimension than the input arrays. + + See Also + -------- + numpy.stack + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if type(axis) is not int: + raise TypeError("The target axis should be an integer") + + arrays, common_info = check_shape_dtype_without_axis( + arrays, stack.__name__ + ) + shapes = {inp.shape for inp in arrays} + if len(shapes) != 1: + raise ValueError("all input arrays must have the same shape for stack") + + axis = normalize_axis_index(axis, common_info.ndim + 1) + shape = common_info.shape[:axis] + (1,) + common_info.shape[axis:] + arrays = [arr.reshape(shape) for arr in arrays] + common_info.shape = tuple(shape) + return _concatenate(arrays, common_info, axis, out=out) + + +def vstack(tup: Sequence[ndarray]) -> ndarray: + """ + + Stack arrays in sequence vertically (row wise). + + This is equivalent to concatenation along the first axis after 1-D arrays + of shape `(N,)` have been reshaped to `(1,N)`. Rebuilds arrays divided by + `vsplit`. + + This function makes most sense for arrays with up to 3 dimensions. For + instance, for pixel-data with a height (first axis), width (second axis), + and r/g/b channels (third axis). The functions `concatenate`, `stack` and + `block` provide more general stacking and concatenation operations. + + Parameters + ---------- + tup : Sequence[ndarray] + The arrays must have the same shape along all but the first axis. + 1-D arrays must have the same length. + + Returns + ------- + stacked : ndarray + The array formed by stacking the given arrays, will be at least 2-D. + + See Also + -------- + numpy.vstack + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # Reshape arrays in the `array_list` if needed before concatenation + reshaped = _atleast_nd(2, tup) + if not isinstance(reshaped, list): + reshaped = [reshaped] + tup, common_info = check_shape_dtype_without_axis( + reshaped, vstack.__name__ + ) + check_shape_with_axis(tup, vstack.__name__, 0) + return _concatenate( + tup, + common_info, + axis=0, + dtype=common_info.dtype, + ) + + +def hstack(tup: Sequence[ndarray]) -> ndarray: + """ + + Stack arrays in sequence horizontally (column wise). + + This is equivalent to concatenation along the second axis, except for 1-D + arrays where it concatenates along the first axis. Rebuilds arrays divided + by `hsplit`. + + This function makes most sense for arrays with up to 3 dimensions. For + instance, for pixel-data with a height (first axis), width (second axis), + and r/g/b channels (third axis). The functions `concatenate`, `stack` and + `block` provide more general stacking and concatenation operations. + + Parameters + ---------- + tup : Sequence[ndarray] + The arrays must have the same shape along all but the second axis, + except 1-D arrays which can be any length. + + Returns + ------- + stacked : ndarray + The array formed by stacking the given arrays. + + See Also + -------- + numpy.hstack + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # Reshape arrays in the `array_list` to handle scalars + reshaped = _atleast_nd(1, tup) + if not isinstance(reshaped, list): + reshaped = [reshaped] + + tup, common_info = check_shape_dtype_without_axis( + reshaped, hstack.__name__ + ) + check_shape_with_axis( + tup, hstack.__name__, axis=(0 if common_info.ndim == 1 else 1) + ) + # When ndim == 1, hstack concatenates arrays along the first axis + return _concatenate( + tup, + common_info, + axis=(0 if common_info.ndim == 1 else 1), + dtype=common_info.dtype, + ) + + +def dstack(tup: Sequence[ndarray]) -> ndarray: + """ + + Stack arrays in sequence depth wise (along third axis). + + This is equivalent to concatenation along the third axis after 2-D arrays + of shape `(M,N)` have been reshaped to `(M,N,1)` and 1-D arrays of shape + `(N,)` have been reshaped to `(1,N,1)`. Rebuilds arrays divided by + `dsplit`. + + This function makes most sense for arrays with up to 3 dimensions. For + instance, for pixel-data with a height (first axis), width (second axis), + and r/g/b channels (third axis). The functions `concatenate`, `stack` and + `block` provide more general stacking and concatenation operations. + + Parameters + ---------- + tup : Sequence[ndarray] + The arrays must have the same shape along all but the third axis. + 1-D or 2-D arrays must have the same shape. + + Returns + ------- + stacked : ndarray + The array formed by stacking the given arrays, will be at least 3-D. + + See Also + -------- + numpy.dstack + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # Reshape arrays to (1,N,1) for ndim ==1 or (M,N,1) for ndim == 2: + reshaped = _atleast_nd(3, tup) + if not isinstance(reshaped, list): + reshaped = [reshaped] + tup, common_info = check_shape_dtype_without_axis( + reshaped, dstack.__name__ + ) + check_shape_with_axis(tup, dstack.__name__, 2) + return _concatenate( + tup, + common_info, + axis=2, + dtype=common_info.dtype, + ) + + +def column_stack(tup: Sequence[ndarray]) -> ndarray: + """ + + Stack 1-D arrays as columns into a 2-D array. + + Take a sequence of 1-D arrays and stack them as columns + to make a single 2-D array. 2-D arrays are stacked as-is, + just like with `hstack`. 1-D arrays are turned into 2-D columns + first. + + Parameters + ---------- + tup : Sequence[ndarray] + 1-D or 2-D arrays to stack. All of them must have the same + first dimension. + + Returns + ------- + stacked : ndarray + The 2-D array formed by stacking the given arrays. + + See Also + -------- + numpy.column_stack + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # Reshape arrays in the `array_list` to handle scalars + reshaped = _atleast_nd(1, tup) + if not isinstance(reshaped, list): + reshaped = [reshaped] + + tup, common_info = check_shape_dtype_without_axis( + reshaped, column_stack.__name__ + ) + + if common_info.ndim == 1: + tup = list(inp.reshape((inp.shape[0], 1)) for inp in tup) + common_info.shape = tup[0].shape + check_shape_with_axis(tup, column_stack.__name__, 1) + return _concatenate( + tup, + common_info, + axis=1, + dtype=common_info.dtype, + ) + + +row_stack = vstack diff --git a/cupynumeric/_module/array_rearrange.py b/cupynumeric/_module/array_rearrange.py new file mode 100644 index 000000000..7f2707583 --- /dev/null +++ b/cupynumeric/_module/array_rearrange.py @@ -0,0 +1,289 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import itertools +from typing import TYPE_CHECKING, Sequence + +from .._array.util import add_boilerplate +from .._utils import is_np2 +from .array_dimension import broadcast +from .array_transpose import transpose +from .creation_shape import empty_like + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import normalize_axis_tuple # type: ignore + +if TYPE_CHECKING: + from .._array.array import ndarray + from ..types import NdShapeLike + + +@add_boilerplate("m") +def flip(m: ndarray, axis: NdShapeLike | None = None) -> ndarray: + """ + Reverse the order of elements in an array along the given axis. + + The shape of the array is preserved, but the elements are reordered. + + Parameters + ---------- + m : array_like + Input array. + axis : None or int or tuple[int], optional + Axis or axes along which to flip over. The default, axis=None, will + flip over all of the axes of the input array. If axis is negative it + counts from the last to the first axis. + + If axis is a tuple of ints, flipping is performed on all of the axes + specified in the tuple. + + Returns + ------- + out : array_like + A new array that is constructed from `m` with the entries of axis + reversed. + + See Also + -------- + numpy.flip + + Availability + -------- + Single GPU, Single CPU + + Notes + ----- + cuPyNumeric implementation doesn't return a view, it returns a new array + """ + return m.flip(axis=axis) + + +@add_boilerplate("m") +def flipud(m: ndarray) -> ndarray: + """ + Reverse the order of elements along axis 0 (up/down). + + For a 2-D array, this flips the entries in each column in the up/down + direction. Rows are preserved, but appear in a different order than before. + + Parameters + ---------- + m : array_like + Input array. + + Returns + ------- + out : array_like + A new array that is constructed from `m` with rows reversed. + + See Also + -------- + numpy.flipud + + Availability + -------- + Single GPU, Single CPU + + Notes + ----- + cuPyNumeric implementation doesn't return a view, it returns a new array + """ + if m.ndim < 1: + raise ValueError("Input must be >= 1-d.") + return flip(m, axis=0) + + +@add_boilerplate("m") +def fliplr(m: ndarray) -> ndarray: + """ + Reverse the order of elements along axis 1 (left/right). + + For a 2-D array, this flips the entries in each row in the left/right + direction. Columns are preserved, but appear in a different order than + before. + + Parameters + ---------- + m : array_like + Input array, must be at least 2-D. + + Returns + ------- + f : ndarray + A new array that is constructed from `m` with the columns reversed. + + See Also + -------- + numpy.fliplr + + Availability + -------- + Single GPU, Single CPU + + Notes + ----- + cuPyNumeric implementation doesn't return a view, it returns a new array + """ + if m.ndim < 2: + raise ValueError("Input must be >= 2-d.") + return flip(m, axis=1) + + +@add_boilerplate("m") +def rot90(m: ndarray, k: int = 1, axes: Sequence[int] = (0, 1)) -> ndarray: + """ + Rotate an array by 90 degrees in the plane specified by axes. + + Rotation direction is from the first towards the second axis. + This means for a 2D array with the default `k` and `axes`, the + rotation will be counterclockwise. + + Parameters + ---------- + m : array_like + Array of two or more dimensions. + k : integer + Number of times the array is rotated by 90 degrees. + axes : (2,) array_like + The array is rotated in the plane defined by the axes. + Axes must be different. + + Returns + ------- + y : ndarray + A rotated copy of `m`. + + See Also + -------- + flip : Reverse the order of elements in an array along the given axis. + fliplr : Flip an array horizontally. + flipud : Flip an array vertically. + + Availability + -------- + Single GPU, Single CPU + """ + axes = tuple(axes) + if len(axes) != 2: + raise ValueError("len(axes) must be 2.") + + if axes[0] == axes[1] or abs(axes[0] - axes[1]) == m.ndim: + raise ValueError("Axes must be different.") + + if ( + axes[0] >= m.ndim + or axes[0] < -m.ndim + or axes[1] >= m.ndim + or axes[1] < -m.ndim + ): + raise ValueError( + f"Axes={axes} out of range for array of ndim={m.ndim}." + ) + + k %= 4 + + if k == 0: + return m.copy() + if k == 2: + return flip(flip(m, axes[0]), axes[1]) + + axes_list = list(range(0, m.ndim)) + (axes_list[axes[0]], axes_list[axes[1]]) = ( + axes_list[axes[1]], + axes_list[axes[0]], + ) + + if k == 1: + return transpose(flip(m, axes[1]), axes_list) + else: + # k == 3 + return flip(transpose(m, axes_list), axes[1]) + + +@add_boilerplate("a") +def roll( + a: ndarray, + shift: int | tuple[int, ...], + axis: int | tuple[int, ...] | None = None, +) -> ndarray: + """ + Roll array elements along a given axis. + + Elements that roll beyond the last position are re-introduced at + the first. + + Parameters + ---------- + a : array_like + Input array. + shift : int or tuple of ints + The number of places by which elements are shifted. If a tuple, + then `axis` must be a tuple of the same size, and each of the + given axes is shifted by the corresponding number. If an int + while `axis` is a tuple of ints, then the same value is used for + all given axes. + axis : int or tuple of ints, optional + Axis or axes along which elements are shifted. By default, the + array is flattened before shifting, after which the original + shape is restored. + + Returns + ------- + res : ndarray + Output array, with the same shape as `a`. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + Notes + ----- + Supports rolling over multiple dimensions simultaneously. + """ + if axis is None: + return roll(a.ravel(), shift, 0).reshape(a.shape) + + normalized_axis: tuple[int, ...] = normalize_axis_tuple( + axis, a.ndim, allow_duplicate=True + ) + broadcasted = broadcast(shift, normalized_axis) + if broadcasted.ndim > 1: + raise ValueError( + "'shift' and 'axis' should be scalars or 1D sequences" + ) + shifts = {ax: 0 for ax in range(a.ndim)} + for sh, ax in broadcasted: + shifts[ax] += sh + + rolls: list[tuple[tuple[slice, ...], ...]] + rolls = [((slice(None), slice(None)),)] * a.ndim + for ax, offset in shifts.items(): + offset %= a.shape[ax] or 1 # If `a` is empty, nothing matters. + if offset: + # (original, result), (original, result) + rolls[ax] = ( + (slice(None, -offset), slice(offset, None)), + (slice(-offset, None), slice(None, offset)), + ) + + result = empty_like(a) + for indices in itertools.product(*rolls): + arr_index, res_index = zip(*indices) + result[res_index] = a[arr_index] + + return result diff --git a/cupynumeric/_module/array_shape.py b/cupynumeric/_module/array_shape.py new file mode 100644 index 000000000..078dc78a9 --- /dev/null +++ b/cupynumeric/_module/array_shape.py @@ -0,0 +1,119 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import add_boilerplate + +if TYPE_CHECKING: + from .._array.array import ndarray + from ..types import NdShapeLike, OrderType + + +@add_boilerplate("a") +def ravel(a: ndarray, order: OrderType = "C") -> ndarray: + """ + Return a contiguous flattened array. + + A 1-D array, containing the elements of the input, is returned. A copy is + made only if needed. + + Parameters + ---------- + a : array_like + Input array. The elements in `a` are read in the order specified by + `order`, and packed as a 1-D array. + order : ``{'C','F', 'A', 'K'}``, optional + The elements of `a` are read using this index order. 'C' means + to index the elements in row-major, C-style order, + with the last axis index changing fastest, back to the first + axis index changing slowest. 'F' means to index the elements + in column-major, Fortran-style order, with the + first index changing fastest, and the last index changing + slowest. Note that the 'C' and 'F' options take no account of + the memory layout of the underlying array, and only refer to + the order of axis indexing. 'A' means to read the elements in + Fortran-like index order if `a` is Fortran *contiguous* in + memory, C-like order otherwise. 'K' means to read the + elements in the order they occur in memory, except for + reversing the data when strides are negative. By default, 'C' + index order is used. + + Returns + ------- + y : array_like + y is an array of the same subtype as `a`, with shape ``(a.size,)``. + Note that matrices are special cased for backward compatibility, if `a` + is a matrix, then y is a 1-D ndarray. + + See Also + -------- + numpy.ravel + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.ravel(order=order) + + +@add_boilerplate("a") +def reshape( + a: ndarray, newshape: NdShapeLike, order: OrderType = "C" +) -> ndarray: + """ + + Gives a new shape to an array without changing its data. + + Parameters + ---------- + a : array_like + Array to be reshaped. + newshape : int or tuple[int] + The new shape should be compatible with the original shape. If + an integer, then the result will be a 1-D array of that length. + One shape dimension can be -1. In this case, the value is + inferred from the length of the array and remaining dimensions. + order : ``{'C', 'F', 'A'}``, optional + Read the elements of `a` using this index order, and place the + elements into the reshaped array using this index order. 'C' + means to read / write the elements using C-like index order, + with the last axis index changing fastest, back to the first + axis index changing slowest. 'F' means to read / write the + elements using Fortran-like index order, with the first index + changing fastest, and the last index changing slowest. Note that + the 'C' and 'F' options take no account of the memory layout of + the underlying array, and only refer to the order of indexing. + 'A' means to read / write the elements in Fortran-like index + order if `a` is Fortran *contiguous* in memory, C-like order + otherwise. + + Returns + ------- + reshaped_array : ndarray + This will be a new view object if possible; otherwise, it will + be a copy. Note there is no guarantee of the *memory layout* (C- or + Fortran- contiguous) of the returned array. + + See Also + -------- + numpy.reshape + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.reshape(newshape, order=order) diff --git a/cupynumeric/_module/array_splitting.py b/cupynumeric/_module/array_splitting.py new file mode 100644 index 000000000..4462ee5e6 --- /dev/null +++ b/cupynumeric/_module/array_splitting.py @@ -0,0 +1,246 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import convert_to_cupynumeric_ndarray + +if TYPE_CHECKING: + import numpy.typing as npt + + +def split(a: ndarray, indices: int | ndarray, axis: int = 0) -> list[ndarray]: + """ + + Split an array into multiple sub-arrays as views into `ary`. + + Parameters + ---------- + ary : ndarray + Array to be divided into sub-arrays. + indices_or_sections : int or ndarray + If `indices_or_sections` is an integer, N, the array will be divided + into N equal arrays along `axis`. If such a split is not possible, + an error is raised. + + If `indices_or_sections` is a 1-D array of sorted integers, the entries + indicate where along `axis` the array is split. For example, + ``[2, 3]`` would, for ``axis=0``, result in + + - ary[:2] + - ary[2:3] + - ary[3:] + + If an index exceeds the dimension of the array along `axis`, + an empty sub-array is returned correspondingly. + axis : int, optional + The axis along which to split, default is 0. + + Returns + ------- + sub-arrays : list[ndarray] + A list of sub-arrays as views into `ary`. + + Raises + ------ + ValueError + If `indices_or_sections` is given as an integer, but + a split does not result in equal division. + + See Also + -------- + numpy.split + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return array_split(a, indices, axis, equal=True) + + +def array_split( + a: ndarray, + indices: int | tuple[int] | ndarray | npt.NDArray[Any], + axis: int = 0, + equal: bool = False, +) -> list[ndarray]: + """ + + Split an array into multiple sub-arrays. + + Please refer to the ``split`` documentation. The only difference + between these functions is that ``array_split`` allows + `indices_or_sections` to be an integer that does *not* equally + divide the axis. For an array of length l that should be split + into n sections, it returns l % n sub-arrays of size l//n + 1 + and the rest of size l//n. + + See Also + -------- + numpy.array_split + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + array = convert_to_cupynumeric_ndarray(a) + split_pts = [] + if axis >= array.ndim: + raise ValueError( + f"array({array.shape}) has less dimensions than axis({axis})" + ) + + if isinstance(indices, int): + if indices <= 0: + raise ValueError("number sections must be larger than 0.") + res = array.shape[axis] % indices + if equal and res != 0: + raise ValueError("array split does not result in an equal divison") + + len_subarr = array.shape[axis] // indices + end_idx = array.shape[axis] + first_idx = len_subarr + + # the requested # of subarray is larger than the size of array + # -> size of 1 subarrays + empty subarrays + if len_subarr == 0: + len_subarr = 1 + first_idx = len_subarr + end_idx = indices + else: + if res != 0: + # The first 'res' groups have len_subarr+1 elements + split_pts = list( + range( + len_subarr + 1, (len_subarr + 1) * res, len_subarr + 1 + ) + ) + first_idx = (len_subarr + 1) * res + split_pts.extend(range(first_idx, end_idx + 1, len_subarr)) + + elif isinstance(indices, (list, tuple)) or ( + isinstance(indices, (ndarray, np.ndarray)) and indices.dtype == int + ): + split_pts = list(indices) + # adding the size of the target dimension. + # This helps create dummy or last subarray correctly + split_pts.append(array.shape[axis]) + + else: + raise ValueError("Integer or array for split should be provided") + + result = [] + start_idx = 0 + end_idx = 0 + out_shape = [] + in_shape: list[int | slice] = [] + + for i in range(array.ndim): + if i != axis: + in_shape.append(slice(array.shape[i])) + out_shape.append(array.shape[i]) + else: + in_shape.append(1) + out_shape.append(1) + + for pts in split_pts: + if type(pts) is not int: + raise ValueError( + "Split points in the passed `indices` should be integer" + ) + end_idx = pts + # For a split point, which is larger than the dimension for splitting, + # The last non-empty subarray should be copied from + # array[last_elem:array.shape[axis]] + if pts > array.shape[axis]: + end_idx = array.shape[axis] + out_shape[axis] = (end_idx - start_idx) + 1 + in_shape[axis] = slice(start_idx, end_idx) + new_subarray = None + if start_idx < array.shape[axis] and start_idx < end_idx: + new_subarray = array[tuple(in_shape)].view() + else: + out_shape[axis] = 0 + new_subarray = ndarray( + tuple(out_shape), dtype=array.dtype, writeable=array._writeable + ) + result.append(new_subarray) + start_idx = pts + + return result + + +def dsplit(a: ndarray, indices: int | ndarray) -> list[ndarray]: + """ + + Split array into multiple sub-arrays along the 3rd axis (depth). + + Please refer to the `split` documentation. `dsplit` is equivalent + to `split` with ``axis=2``, the array is always split along the third + axis provided the array dimension is greater than or equal to 3. + + See Also + -------- + numpy.dsplit + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return split(a, indices, axis=2) + + +def hsplit(a: ndarray, indices: int | ndarray) -> list[ndarray]: + """ + + Split an array into multiple sub-arrays horizontally (column-wise). + + Please refer to the `split` documentation. `hsplit` is equivalent + to `split` with ``axis=1``, the array is always split along the second + axis regardless of the array dimension. + + See Also + -------- + numpy.hsplit + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return split(a, indices, axis=1) + + +def vsplit(a: ndarray, indices: int | ndarray) -> list[ndarray]: + """ + + Split an array into multiple sub-arrays vertically (row-wise). + + Please refer to the ``split`` documentation. ``vsplit`` is equivalent + to ``split`` with `axis=0` (default), the array is always split along the + first axis regardless of the array dimension. + + See Also + -------- + numpy.vsplit + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return split(a, indices, axis=0) diff --git a/cupynumeric/_module/array_tiling.py b/cupynumeric/_module/array_tiling.py new file mode 100644 index 000000000..6dca2939d --- /dev/null +++ b/cupynumeric/_module/array_tiling.py @@ -0,0 +1,232 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence, cast + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate, convert_to_cupynumeric_ndarray +from .._utils import is_np2 +from ..runtime import runtime +from .creation_shape import full + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + +if TYPE_CHECKING: + import numpy.typing as npt + + from ..types import NdShape + +if is_np2: + from numpy.exceptions import AxisError # type: ignore +else: + from numpy import AxisError # type: ignore + +_builtin_max = max + + +@add_boilerplate("A") +def tile( + A: ndarray, reps: int | Sequence[int] | npt.NDArray[np.int_] +) -> ndarray: + """ + Construct an array by repeating A the number of times given by reps. + + If `reps` has length ``d``, the result will have dimension of ``max(d, + A.ndim)``. + + If ``A.ndim < d``, `A` is promoted to be d-dimensional by prepending new + axes. So a shape (3,) array is promoted to (1, 3) for 2-D replication, + or shape (1, 1, 3) for 3-D replication. If this is not the desired + behavior, promote `A` to d-dimensions manually before calling this + function. + + If ``A.ndim > d``, `reps` is promoted to `A`.ndim by pre-pending 1's to it. + Thus for an `A` of shape (2, 3, 4, 5), a `reps` of (2, 2) is treated as + (1, 1, 2, 2). + + Parameters + ---------- + A : array_like + The input array. + reps : 1d array_like + The number of repetitions of `A` along each axis. + + Returns + ------- + c : ndarray + The tiled output array. + + See Also + -------- + numpy.tile + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + computed_reps: tuple[int, ...] + if isinstance(reps, int): + computed_reps = (reps,) + else: + if np.ndim(reps) > 1: + raise TypeError("`reps` must be a 1d sequence") + computed_reps = tuple(reps) + # Figure out the shape of the destination array + out_dims = _builtin_max(A.ndim, len(computed_reps)) + # Prepend ones until the dimensions match + while len(computed_reps) < out_dims: + computed_reps = (1,) + computed_reps + out_shape: NdShape = () + # Prepend dimensions if necessary + for dim in range(out_dims - A.ndim): + out_shape += (computed_reps[dim],) + offset = len(out_shape) + for dim in range(A.ndim): + out_shape += (A.shape[dim] * computed_reps[offset + dim],) + assert len(out_shape) == out_dims + result = ndarray(out_shape, dtype=A.dtype, inputs=(A,)) + result._thunk.tile(A._thunk, computed_reps) + return result + + +def repeat(a: ndarray, repeats: Any, axis: int | None = None) -> ndarray: + """ + Repeat elements of an array. + + Parameters + ---------- + a : array_like + Input array. + repeats : int or ndarray[int] + The number of repetitions for each element. repeats is + broadcasted to fit the shape of the given axis. + axis : int, optional + The axis along which to repeat values. By default, use the + flattened input array, and return a flat output array. + + Returns + ------- + repeated_array : ndarray + Output array which has the same shape as a, except along the + given axis. + + Notes + ----- + Currently, repeat operations supports only 1D arrays + + See Also + -------- + numpy.repeat + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if repeats is None: + raise TypeError( + "int() argument must be a string, a bytes-like object or a number," + " not 'NoneType'" + ) + + if np.ndim(repeats) > 1: + raise ValueError("`repeats` should be scalar or 1D array") + + # axes should be integer type + if axis is not None and not isinstance(axis, int): + raise TypeError("Axis should be of integer type") + + # when array is a scalar + if np.ndim(a) == 0: + if axis is not None and axis != 0 and axis != -1: + raise AxisError( + f"axis {axis} is out of bounds for array of dimension 0" + ) + if np.ndim(repeats) == 0: + if not isinstance(repeats, int): + runtime.warn( + "converting repeats to an integer type", + category=UserWarning, + ) + repeats = np.int64(repeats) + return full((repeats,), cast(int | float, a)) + elif np.ndim(repeats) == 1 and len(repeats) == 1: + if not isinstance(repeats, int): + runtime.warn( + "converting repeats to an integer type", + category=UserWarning, + ) + repeats = np.int64(repeats) + return full((repeats[0],), cast(int | float, a)) + else: + raise ValueError( + "`repeat` with a scalar parameter `a` is only " + "implemented for scalar values of the parameter `repeats`." + ) + + # array is an array + array = convert_to_cupynumeric_ndarray(a) + if np.ndim(repeats) == 1: + repeats = convert_to_cupynumeric_ndarray(repeats) + + # if no axes specified, flatten array + if axis is None: + array = array.ravel() + axis = 0 + + axis_int: int = normalize_axis_index(axis, array.ndim) + + # If repeats is on a zero sized axis_int, then return the array. + if array.shape[axis_int] == 0: + return array.copy() + + if np.ndim(repeats) == 1: + if repeats.shape[0] == 1 and repeats.shape[0] != array.shape[axis_int]: + repeats = repeats[0] + + # repeats is a scalar. + if np.ndim(repeats) == 0: + # repeats is 0 + if repeats == 0: + empty_shape = list(array.shape) + empty_shape[axis_int] = 0 + return ndarray(shape=tuple(empty_shape), dtype=array.dtype) + # repeats should be integer type + if not isinstance(repeats, int): + runtime.warn( + "converting repeats to an integer type", + category=UserWarning, + ) + result = array._thunk.repeat( + repeats=np.int64(repeats), + axis=axis_int, + scalar_repeats=True, + ) + # repeats is an array + else: + # repeats should be integer type + repeats = repeats._warn_and_convert(np.int64) + if repeats.shape[0] != array.shape[axis_int]: + raise ValueError("incorrect shape of repeats array") + result = array._thunk.repeat( + repeats=repeats._thunk, axis=axis_int, scalar_repeats=False + ) + return ndarray(shape=result.shape, thunk=result) diff --git a/cupynumeric/_module/array_transpose.py b/cupynumeric/_module/array_transpose.py new file mode 100644 index 000000000..f16f1963f --- /dev/null +++ b/cupynumeric/_module/array_transpose.py @@ -0,0 +1,136 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence + +from .._array.util import add_boilerplate +from .._utils import is_np2 + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import normalize_axis_tuple # type: ignore + + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a") +def swapaxes(a: ndarray, axis1: int, axis2: int) -> ndarray: + """ + + Interchange two axes of an array. + + Parameters + ---------- + a : array_like + Input array. + axis1 : int + First axis. + axis2 : int + Second axis. + + Returns + ------- + a_swapped : ndarray + If `a` is an ndarray, then a view of `a` is returned; otherwise a new + array is created. + + See Also + -------- + numpy.swapaxes + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.swapaxes(axis1, axis2) + + +@add_boilerplate("a") +def transpose(a: ndarray, axes: list[int] | None = None) -> ndarray: + """ + + Permute the dimensions of an array. + + Parameters + ---------- + a : array_like + Input array. + axes : list[int], optional + By default, reverse the dimensions, otherwise permute the axes + according to the values given. + + Returns + ------- + p : ndarray + `a` with its axes permuted. A view is returned whenever + possible. + + See Also + -------- + numpy.transpose + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.transpose(axes=axes) + + +@add_boilerplate("a") +def moveaxis( + a: ndarray, source: Sequence[int], destination: Sequence[int] +) -> ndarray: + """ + Move axes of an array to new positions. + Other axes remain in their original order. + + Parameters + ---------- + a : ndarray + The array whose axes should be reordered. + source : int or Sequence[int] + Original positions of the axes to move. These must be unique. + destination : int or Sequence[int] + Destination positions for each of the original axes. These must also be + unique. + + Returns + ------- + result : ndarray + Array with moved axes. This array is a view of the input array. + + See Also + -------- + numpy.moveaxis + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + source = normalize_axis_tuple(source, a.ndim, "source") + destination = normalize_axis_tuple(destination, a.ndim, "destination") + if len(source) != len(destination): + raise ValueError( + "`source` and `destination` arguments must have the same number " + "of elements" + ) + order = [n for n in range(a.ndim) if n not in source] + for dest, src in sorted(zip(destination, source)): + order.insert(dest, src) + return a.transpose(order) diff --git a/cunumeric/bits.py b/cupynumeric/_module/binary_bit_packing.py similarity index 92% rename from cunumeric/bits.py rename to cupynumeric/_module/binary_bit_packing.py index 1cb3995c0..b84e0f07a 100644 --- a/cunumeric/bits.py +++ b/cupynumeric/_module/binary_bit_packing.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,19 +14,19 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING -from .array import add_boilerplate -from .module import empty +from .._array.util import add_boilerplate +from .creation_shape import empty if TYPE_CHECKING: - from .array import ndarray - from .types import BitOrder + from .._array.array import ndarray + from ..types import BitOrder def _sanitize_arguments( - a: ndarray, axis: Optional[int], bitorder: BitOrder -) -> Tuple[ndarray, int]: + a: ndarray, axis: int | None, bitorder: BitOrder +) -> tuple[ndarray, int]: if axis is None: if a.ndim > 1: a = a.ravel() @@ -49,7 +49,7 @@ def _sanitize_arguments( @add_boilerplate("a") def packbits( - a: ndarray, axis: Optional[int] = None, bitorder: BitOrder = "big" + a: ndarray, axis: int | None = None, bitorder: BitOrder = "big" ) -> ndarray: """ @@ -108,8 +108,8 @@ def packbits( @add_boilerplate("a") def unpackbits( a: ndarray, - axis: Optional[int] = None, - count: Optional[int] = None, + axis: int | None = None, + count: int | None = None, bitorder: BitOrder = "big", ) -> ndarray: """ diff --git a/cupynumeric/_module/creation_data.py b/cupynumeric/_module/creation_data.py new file mode 100644 index 000000000..03869905c --- /dev/null +++ b/cupynumeric/_module/creation_data.py @@ -0,0 +1,177 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Literal + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from ..runtime import runtime +from .creation_shape import empty_like + +if TYPE_CHECKING: + from ..types import OrderType + + +def array( + obj: Any, + dtype: np.dtype[Any] | None = None, + copy: bool = True, + order: OrderType | Literal["K"] = "K", + subok: bool = False, + ndmin: int = 0, +) -> ndarray: + """ + array(object, dtype=None, copy=True) + + Create an array. + + Parameters + ---------- + object : array_like + An array, any object exposing the array interface, an object whose + __array__ method returns an array, or any (nested) sequence. + dtype : data-type, optional + The desired data-type for the array. If not given, then the type will + be determined as the minimum type required to hold the objects in the + sequence. + copy : bool, optional + If true (default), then the object is copied. Otherwise, a copy will + only be made if __array__ returns a copy, if obj is a nested sequence, + or if a copy is needed to satisfy any of the other requirements + (`dtype`, `order`, etc.). + order : ``{'K', 'A', 'C', 'F'}``, optional + Specify the memory layout of the array. If object is not an array, the + newly created array will be in C order (row major) unless 'F' is + specified, in which case it will be in Fortran order (column major). + If object is an array the following holds. + + ===== ========= =================================================== + order no copy copy=True + ===== ========= =================================================== + 'K' unchanged F & C order preserved, otherwise most similar order + 'A' unchanged F order if input is F and not C, otherwise C order + 'C' C order C order + 'F' F order F order + ===== ========= =================================================== + + When ``copy=False`` and a copy is made for other reasons, the result is + the same as if ``copy=True``, with some exceptions for 'A', see the + Notes section. The default order is 'K'. + subok : bool, optional + If True, then sub-classes will be passed-through, otherwise + the returned array will be forced to be a base-class array (default). + ndmin : int, optional + Specifies the minimum number of dimensions that the resulting + array should have. Ones will be pre-pended to the shape as + needed to meet this requirement. + + Returns + ------- + out : ndarray + An array object satisfying the specified requirements. + + See Also + -------- + numpy.array + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if not isinstance(obj, ndarray): + thunk = runtime.get_numpy_thunk(obj, share=(not copy), dtype=dtype) + result = ndarray(shape=None, thunk=thunk) + else: + result = obj + if dtype is not None and result.dtype != dtype: + result = result.astype(dtype) + elif copy and obj is result: + result = result.copy() + if result.ndim < ndmin: + shape = (1,) * (ndmin - result.ndim) + result.shape + result = result.reshape(shape) + return result + + +def asarray(a: Any, dtype: np.dtype[Any] | None = None) -> ndarray: + """ + Convert the input to an array. + + Parameters + ---------- + a : array_like + Input data, in any form that can be converted to an array. This + includes lists, lists of tuples, tuples, tuples of tuples, tuples + of lists and ndarrays. + dtype : data-type, optional + By default, the data-type is inferred from the input data. + + Returns + ------- + out : ndarray + Array interpretation of `a`. No copy is performed if the input is + already an ndarray with matching dtype. If `a` is a subclass of + ndarray, a base class ndarray is returned. + + See Also + -------- + numpy.asarray + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if not isinstance(a, ndarray): + thunk = runtime.get_numpy_thunk(a, share=True, dtype=dtype) + writeable = a.flags.writeable if isinstance(a, np.ndarray) else True + array = ndarray(shape=None, thunk=thunk, writeable=writeable) + else: + array = a + if dtype is not None and array.dtype != dtype: + array = array.astype(dtype) + return array + + +@add_boilerplate("a") +def copy(a: ndarray) -> ndarray: + """ + + Return an array copy of the given object. + + Parameters + ---------- + a : array_like + Input data. + + Returns + ------- + arr : ndarray + Array interpretation of `a`. + + See Also + -------- + numpy.copy + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + result = empty_like(a, dtype=a.dtype) + result._thunk.copy(a._thunk, deep=True) + return result diff --git a/cupynumeric/_module/creation_matrices.py b/cupynumeric/_module/creation_matrices.py new file mode 100644 index 000000000..540276c53 --- /dev/null +++ b/cupynumeric/_module/creation_matrices.py @@ -0,0 +1,192 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from .creation_shape import ones + +if TYPE_CHECKING: + import numpy.typing as npt + + +@add_boilerplate("v") +def diag(v: ndarray, k: int = 0) -> ndarray: + """ + + Extract a diagonal or construct a diagonal array. + + See the more detailed documentation for ``cupynumeric.diagonal`` if you use + this function to extract a diagonal and wish to write to the resulting + array; whether it returns a copy or a view depends on what version of numpy + you are using. + + Parameters + ---------- + v : array_like + If `v` is a 2-D array, return a copy of its `k`-th diagonal. + If `v` is a 1-D array, return a 2-D array with `v` on the `k`-th + diagonal. + k : int, optional + Diagonal in question. The default is 0. Use `k>0` for diagonals + above the main diagonal, and `k<0` for diagonals below the main + diagonal. + + Returns + ------- + out : ndarray + The extracted diagonal or constructed diagonal array. + + See Also + -------- + numpy.diag + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if v.ndim == 0: + raise ValueError("Input must be 1- or 2-d") + elif v.ndim == 1: + return v.diagonal(offset=k, axis1=0, axis2=1, extract=False) + elif v.ndim == 2: + return v.diagonal(offset=k, axis1=0, axis2=1, extract=True) + else: + raise ValueError("diag requires 1- or 2-D array, use diagonal instead") + + +def tri( + N: int, + M: int | None = None, + k: int = 0, + dtype: npt.DTypeLike = float, + *, + like: ndarray | None = None, +) -> ndarray: + """ + An array with ones at and below the given diagonal and zeros elsewhere. + + Parameters + ---------- + N : int + Number of rows in the array. + M : int, optional + Number of columns in the array. + By default, `M` is taken equal to `N`. + k : int, optional + The sub-diagonal at and below which the array is filled. + `k` = 0 is the main diagonal, while `k` < 0 is below it, + and `k` > 0 is above. The default is 0. + dtype : dtype, optional + Data type of the returned array. The default is float. + like : array_like + Reference object to allow the creation of arrays which are not NumPy + arrays. If an array-like passed in as `like` supports the + `__array_function__` protocol, the result will be defined by it. In + this case it ensures the creation of an array object compatible with + that passed in via this argument. + + Returns + ------- + tri : ndarray of shape (N, M) + Array with its lower triangle filled with ones and zero elsewhere; + in other words ``T[i,j] == 1`` for ``j <= i + k``, 0 otherwise. + + See Also + -------- + numpy.tri + + Notes + ----- + `like` argument is currently not supported + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + # TODO: add support for `like` (see issue #418) + if like is not None: + raise ValueError("like parameter is currently not supported") + + if M is None: + M = N + + out = ones((N, M), dtype=dtype) + return tril(out, k) + + +@add_boilerplate("m") +def trilu(m: ndarray, k: int, lower: bool) -> ndarray: + if m.ndim < 1: + raise TypeError("Array must be at least 1-D") + shape = m.shape if m.ndim >= 2 else m.shape * 2 + result = ndarray(shape, dtype=m.dtype, inputs=(m,)) + result._thunk.trilu(m._thunk, k, lower) + return result + + +def tril(m: ndarray, k: int = 0) -> ndarray: + """ + + Lower triangle of an array. + + Return a copy of an array with elements above the `k`-th diagonal zeroed. + + Parameters + ---------- + m : array_like + Input array of shape (M, N). + k : int, optional + Diagonal above which to zero elements. `k = 0` (the default) is the + main diagonal, `k < 0` is below it and `k > 0` is above. + + Returns + ------- + tril : ndarray + Lower triangle of `m`, of same shape and data-type as `m`. + + See Also + -------- + numpy.tril + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return trilu(m, k, True) + + +def triu(m: ndarray, k: int = 0) -> ndarray: + """ + + Upper triangle of an array. + + Return a copy of a matrix with the elements below the `k`-th diagonal + zeroed. + + Please refer to the documentation for `tril` for further details. + + See Also + -------- + numpy.triu + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return trilu(m, k, False) diff --git a/cupynumeric/_module/creation_ranges.py b/cupynumeric/_module/creation_ranges.py new file mode 100644 index 000000000..d04d94b53 --- /dev/null +++ b/cupynumeric/_module/creation_ranges.py @@ -0,0 +1,355 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from .._module.array_dimension import broadcast_arrays +from .._module.creation_data import asarray +from .._ufunc.floating import floor + +if TYPE_CHECKING: + import numpy.typing as npt + + +_builtin_max = max + + +def arange( + start: int | float = 0, + stop: int | float | None = None, + step: int | float | None = 1, + dtype: npt.DTypeLike | None = None, +) -> ndarray: + """ + arange([start,] stop[, step,], dtype=None) + + Return evenly spaced values within a given interval. + + Values are generated within the half-open interval ``[start, stop)`` + (in other words, the interval including `start` but excluding `stop`). + For integer arguments the function is equivalent to the Python built-in + `range` function, but returns an ndarray rather than a list. + + When using a non-integer step, such as 0.1, the results will often not + be consistent. It is better to use `cupynumeric.linspace` for these cases. + + Parameters + ---------- + start : int or float, optional + Start of interval. The interval includes this value. The default + start value is 0. + stop : int or float + End of interval. The interval does not include this value, except + in some cases where `step` is not an integer and floating point + round-off affects the length of `out`. + step : int or float, optional + Spacing between values. For any output `out`, this is the distance + between two adjacent values, ``out[i+1] - out[i]``. The default + step size is 1. If `step` is specified as a position argument, + `start` must also be given. + dtype : data-type + The type of the output array. If `dtype` is not given, infer the data + type from the other input arguments. + + Returns + ------- + arange : ndarray + Array of evenly spaced values. + + For floating point arguments, the length of the result is + ``ceil((stop - start)/step)``. Because of floating point overflow, + this rule may result in the last element of `out` being greater + than `stop`. + + See Also + -------- + numpy.arange + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if stop is None: + stop = start + start = 0 + + if step is None: + step = 1 + + if dtype is None: + dtype = np.result_type(start, stop, step) + else: + dtype = np.dtype(dtype) + + N = math.ceil((stop - start) / step) + result = ndarray((_builtin_max(0, N),), dtype) + result._thunk.arange(start, stop, step) + return result + + +@add_boilerplate("start", "stop") +def linspace( + start: ndarray, + stop: ndarray, + num: int = 50, + endpoint: bool = True, + retstep: bool = False, + dtype: npt.DTypeLike | None = None, + axis: int = 0, +) -> ndarray | tuple[ndarray, float | ndarray]: + """ + + Return evenly spaced numbers over a specified interval. + + Returns `num` evenly spaced samples, calculated over the + interval [`start`, `stop`]. + + The endpoint of the interval can optionally be excluded. + + Parameters + ---------- + start : array_like + The starting value of the sequence. + stop : array_like + The end value of the sequence, unless `endpoint` is set to False. + In that case, the sequence consists of all but the last of ``num + 1`` + evenly spaced samples, so that `stop` is excluded. Note that the step + size changes when `endpoint` is False. + num : int, optional + Number of samples to generate. Default is 50. Must be non-negative. + endpoint : bool, optional + If True, `stop` is the last sample. Otherwise, it is not included. + Default is True. + retstep : bool, optional + If True, return (`samples`, `step`), where `step` is the spacing + between samples. + dtype : data-type, optional + The type of the output array. If `dtype` is not given, infer the data + type from the other input arguments. + axis : int, optional + The axis in the result to store the samples. Relevant only if start + or stop are array-like. By default (0), the samples will be along a + new axis inserted at the beginning. Use -1 to get an axis at the end. + + Returns + ------- + samples : ndarray + There are `num` equally spaced samples in the closed interval + ``[start, stop]`` or the half-open interval ``[start, stop)`` + (depending on whether `endpoint` is True or False). + step : float or ndarray, optional + Only returned if `retstep` is True + + Size of spacing between samples. + + See Also + -------- + numpy.linspace + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if num < 0: + raise ValueError("Number of samples, %s, must be non-negative." % num) + div = (num - 1) if endpoint else num + + common_kind = np.result_type(start.dtype, stop.dtype).kind + dt = np.complex128 if common_kind == "c" else np.float64 + if dtype is None: + dtype = dt + + delta = stop - start + y = arange(0, num, dtype=dt) + + out: tuple[Any, ...] # EllipsisType not even in typing_extensions yet + + # Reshape these arrays into dimensions that allow them to broadcast + if delta.ndim > 0: + if axis is None or axis == 0: + # First dimension + y = y.reshape((-1,) + (1,) * delta.ndim) + # Nothing else needs to be reshaped here because + # they should all broadcast correctly with y + if endpoint and num > 1: + out = (-1,) + elif axis == -1 or axis == delta.ndim: + # Last dimension + y = y.reshape((1,) * delta.ndim + (-1,)) + if endpoint and num > 1: + out = (Ellipsis, -1) + # Extend everything else with extra dimensions of 1 at the end + # so that they can broadcast with y + delta = delta.reshape(delta.shape + (1,)) + start = start.reshape(start.shape + (1,)) + elif axis < delta.ndim: + # Somewhere in the middle + y = y.reshape((1,) * axis + (-1,) + (1,) * (delta.ndim - axis)) + # Start array might be smaller than delta because of broadcast + startax = start.ndim - len(delta.shape[axis:]) + start = start.reshape( + start.shape[0:startax] + (1,) + start.shape[startax:] + ) + if endpoint and num > 1: + out = (Ellipsis, -1) + (slice(None, None, None),) * len( + delta.shape[axis:] + ) + delta = delta.reshape( + delta.shape[0:axis] + (1,) + delta.shape[axis:] + ) + else: + raise ValueError( + "axis " + + str(axis) + + " is out of bounds for array of dimension " + + str(delta.ndim + 1) + ) + else: + out = (-1,) + # else delta is a scalar so start must be also + # therefore it will trivially broadcast correctly + + step: float | ndarray + if div > 0: + step = delta / div + if delta.ndim == 0: + y *= step + else: + y = y * step + else: + # sequences with 0 items or 1 item with endpoint=True (i.e. div <= 0) + # have an undefined step + step = np.nan + if delta.ndim == 0: + y *= delta + else: + y = y * delta + + y += start.astype(y.dtype, copy=False) + + if endpoint and num > 1: + y[out] = stop.astype(y.dtype, copy=False) + + if np.issubdtype(dtype, np.integer): + floor(y, out=y) + + if retstep: + return y.astype(dtype, copy=False), step + else: + return y.astype(dtype, copy=False) + + +@add_boilerplate() +def meshgrid( + *xi: ndarray, copy: bool = True, sparse: bool = False, indexing: str = "xy" +) -> tuple[ndarray, ...]: + """ + Return a tuple of coordinate matrices from coordinate vectors. + + Make N-D coordinate arrays for vectorized evaluations of + N-D scalar/vector fields over N-D grids, given + one-dimensional coordinate arrays x1, x2,..., xn + + Parameters + ---------- + x1, x2,..., xn : array_like + 1-D arrays representing the coordinates of a grid. + indexing : {'xy', 'ij'}, optional + Cartesian ('xy', default) or matrix ('ij') indexing of output. + See Notes for more details. + sparse : bool, optional + If True the shape of the returned coordinate array for dimension *i* + is reduced from ``(N1, ..., Ni, ... Nn)`` to + ``(1, ..., 1, Ni, 1, ..., 1)``. These sparse coordinate grids are + intended to be use with broadcasting. When all + coordinates are used in an expression, broadcasting still leads to a + fully-dimensonal result array. + + Default is False. + copy : bool, optional + If False, a view into the original arrays are returned in order to + conserve memory. Default is True. Please note that + ``sparse=False, copy=False`` will likely return non-contiguous + arrays. Furthermore, more than one element of a broadcast array + may refer to a single memory location. If you need to write to the + arrays, make copies first. + + Returns + ------- + X1, X2,..., XN : tuple of ndarrays + For vectors `x1`, `x2`,..., `xn` with lengths ``Ni=len(xi)``, + returns ``(N1, N2, N3,..., Nn)`` shaped arrays if indexing='ij' + or ``(N2, N1, N3,..., Nn)`` shaped arrays if indexing='xy' + with the elements of `xi` repeated to fill the matrix along + the first dimension for `x1`, the second for `x2` and so on. + + Notes + ----- + This function supports both indexing conventions through the indexing + keyword argument. Giving the string 'ij' returns a meshgrid with + matrix indexing, while 'xy' returns a meshgrid with Cartesian indexing. + In the 2-D case with inputs of length M and N, the outputs are of shape + (N, M) for 'xy' indexing and (M, N) for 'ij' indexing. In the 3-D case + with inputs of length M, N and P, outputs are of shape (N, M, P) for + 'xy' indexing and (M, N, P) for 'ij' indexing. The difference is + illustrated by the following code snippet:: + + xv, yv = np.meshgrid(x, y, indexing='ij') + for i in range(nx): + for j in range(ny): + # treat xv[i,j], yv[i,j] + + xv, yv = np.meshgrid(x, y, indexing='xy') + for i in range(nx): + for j in range(ny): + # treat xv[j,i], yv[j,i] + + In the 1-D and 0-D case, the indexing and sparse keywords have no effect. + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + ndim = len(xi) + + if indexing not in ["xy", "ij"]: + raise ValueError("Valid values for `indexing` are 'xy' and 'ij'.") + + s0 = (1,) * ndim + output = [ + asarray(x).reshape(s0[:i] + (-1,) + s0[i + 1 :]) + for i, x in enumerate(xi) + ] + + if indexing == "xy" and ndim > 1: + # switch first and second axis + output[0] = output[0].swapaxes(0, 1) + output[1] = output[1].swapaxes(0, 1) + + if not sparse: + # Return the full N-D matrix (not only the 1-D vector) + output = broadcast_arrays(*output, subok=True) + + if copy: + output = [x.copy() for x in output] + + return tuple(output) diff --git a/cupynumeric/_module/creation_shape.py b/cupynumeric/_module/creation_shape.py new file mode 100644 index 000000000..d14aa7298 --- /dev/null +++ b/cupynumeric/_module/creation_shape.py @@ -0,0 +1,406 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import operator +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from ..types import NdShapeLike + +if TYPE_CHECKING: + import numpy.typing as npt + + +def empty(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: + """ + empty(shape, dtype=float) + + Return a new array of given shape and type, without initializing entries. + + Parameters + ---------- + shape : int or tuple[int] + Shape of the empty array. + dtype : data-type, optional + Desired output data-type for the array. Default is + ``cupynumeric.float64``. + + Returns + ------- + out : ndarray + Array of uninitialized (arbitrary) data of the given shape and dtype. + + See Also + -------- + numpy.empty + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return ndarray(shape=shape, dtype=dtype) + + +@add_boilerplate("a") +def empty_like( + a: ndarray, + dtype: npt.DTypeLike | None = None, + shape: NdShapeLike | None = None, +) -> ndarray: + """ + + empty_like(prototype, dtype=None) + + Return a new array with the same shape and type as a given array. + + Parameters + ---------- + prototype : array_like + The shape and data-type of `prototype` define these same attributes + of the returned array. + dtype : data-type, optional + Overrides the data type of the result. + shape : int or tuple[int], optional + Overrides the shape of the result. + + Returns + ------- + out : ndarray + Array of uninitialized (arbitrary) data with the same shape and type as + `prototype`. + + See Also + -------- + numpy.empty_like + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + shape = a.shape if shape is None else shape + if dtype is not None: + dtype = np.dtype(dtype) + else: + dtype = a.dtype + return ndarray(shape, dtype=dtype, inputs=(a,)) + + +def eye( + N: int, + M: int | None = None, + k: int = 0, + dtype: npt.DTypeLike | None = np.float64, +) -> ndarray: + """ + + Return a 2-D array with ones on the diagonal and zeros elsewhere. + + Parameters + ---------- + N : int + Number of rows in the output. + M : int, optional + Number of columns in the output. If None, defaults to `N`. + k : int, optional + Index of the diagonal: 0 (the default) refers to the main diagonal, + a positive value refers to an upper diagonal, and a negative value + to a lower diagonal. + dtype : data-type, optional + Data-type of the returned array. + + Returns + ------- + I : ndarray + An array of shape (N, M) where all elements are equal to zero, except + for the `k`-th diagonal, whose values are equal to one. + + See Also + -------- + numpy.eye + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if dtype is not None: + dtype = np.dtype(dtype) + if M is None: + M = N + k = operator.index(k) + result = ndarray((N, M), dtype) + result._thunk.eye(k) + return result + + +def identity(n: int, dtype: npt.DTypeLike = float) -> ndarray: + """ + + Return the identity array. + + The identity array is a square array with ones on + the main diagonal. + + Parameters + ---------- + n : int + Number of rows (and columns) in `n` x `n` output. + dtype : data-type, optional + Data-type of the output. Defaults to ``float``. + + Returns + ------- + out : ndarray + `n` x `n` array with its main diagonal set to one, and all other + elements 0. + + See Also + -------- + numpy.identity + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return eye(N=n, M=n, dtype=dtype) + + +def ones(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: + """ + + Return a new array of given shape and type, filled with ones. + + Parameters + ---------- + shape : int or tuple[int] + Shape of the new array. + dtype : data-type, optional + The desired data-type for the array. Default is `cupynumeric.float64`. + + Returns + ------- + out : ndarray + Array of ones with the given shape and dtype. + + See Also + -------- + numpy.ones + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return full(shape, 1, dtype=dtype) + + +def ones_like( + a: ndarray, + dtype: npt.DTypeLike | None = None, + shape: NdShapeLike | None = None, +) -> ndarray: + """ + + Return an array of ones with the same shape and type as a given array. + + Parameters + ---------- + a : array_like + The shape and data-type of `a` define these same attributes of the + returned array. + dtype : data-type, optional + Overrides the data type of the result. + shape : int or tuple[int], optional + Overrides the shape of the result. + + Returns + ------- + out : ndarray + Array of ones with the same shape and type as `a`. + + See Also + -------- + numpy.ones_like + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + usedtype = a.dtype + if dtype is not None: + usedtype = np.dtype(dtype) + return full_like(a, 1, dtype=usedtype, shape=shape) + + +def zeros(shape: NdShapeLike, dtype: npt.DTypeLike = np.float64) -> ndarray: + """ + zeros(shape, dtype=float) + + Return a new array of given shape and type, filled with zeros. + + Parameters + ---------- + shape : int or tuple[int] + Shape of the new array. + dtype : data-type, optional + The desired data-type for the array. Default is `cupynumeric.float64`. + + Returns + ------- + out : ndarray + Array of zeros with the given shape and dtype. + + See Also + -------- + numpy.zeros + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if dtype is not None: + dtype = np.dtype(dtype) + return full(shape, 0, dtype=dtype) + + +def zeros_like( + a: ndarray, + dtype: npt.DTypeLike | None = None, + shape: NdShapeLike | None = None, +) -> ndarray: + """ + + Return an array of zeros with the same shape and type as a given array. + + Parameters + ---------- + a : array_like + The shape and data-type of `a` define these same attributes of + the returned array. + dtype : data-type, optional + Overrides the data type of the result. + shape : int or tuple[int], optional + Overrides the shape of the result. + + Returns + ------- + out : ndarray + Array of zeros with the same shape and type as `a`. + + See Also + -------- + numpy.zeros_like + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + usedtype = a.dtype + if dtype is not None: + usedtype = np.dtype(dtype) + return full_like(a, 0, dtype=usedtype, shape=shape) + + +def full( + shape: NdShapeLike, + value: Any, + dtype: npt.DTypeLike | None = None, +) -> ndarray: + """ + + Return a new array of given shape and type, filled with `fill_value`. + + Parameters + ---------- + shape : int or tuple[int] + Shape of the new array. + fill_value : scalar + Fill value. + dtype : data-type, optional + The desired data-type for the array The default, None, means + `cupynumeric.array(fill_value).dtype`. + + Returns + ------- + out : ndarray + Array of `fill_value` with the given shape and dtype. + + See Also + -------- + numpy.full + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if dtype is None: + val = np.array(value) + else: + dtype = np.dtype(dtype) + val = np.array(value, dtype=dtype) + if np.dtype(dtype).itemsize == 1 and value > 255: + raise OverflowError(f"Value {value} out of bounds for {dtype}") + result = empty(shape, dtype=val.dtype) + result._thunk.fill(val) + return result + + +def full_like( + a: ndarray, + value: int | float, + dtype: npt.DTypeLike | None = None, + shape: NdShapeLike | None = None, +) -> ndarray: + """ + + Return a full array with the same shape and type as a given array. + + Parameters + ---------- + a : array_like + The shape and data-type of `a` define these same attributes of + the returned array. + fill_value : scalar + Fill value. + dtype : data-type, optional + Overrides the data type of the result. + shape : int or tuple[int], optional + Overrides the shape of the result. + + Returns + ------- + out : ndarray + Array of `fill_value` with the same shape and type as `a`. + + See Also + -------- + numpy.full_like + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if dtype is not None: + dtype = np.dtype(dtype) + else: + dtype = a.dtype + if np.dtype(dtype).itemsize == 1 and value > 255: + raise OverflowError(f"Value {value} out of bounds for {dtype}") + result = empty_like(a, dtype=dtype, shape=shape) + val = np.array(value).astype(dtype) + result._thunk.fill(val) + return result diff --git a/cupynumeric/_module/indexing.py b/cupynumeric/_module/indexing.py new file mode 100644 index 000000000..3af462256 --- /dev/null +++ b/cupynumeric/_module/indexing.py @@ -0,0 +1,1243 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence + +import numpy as np + +from .._array.array import ndarray +from .._array.util import ( + add_boilerplate, + check_writeable, + convert_to_cupynumeric_ndarray, +) +from .._utils import is_np2 +from .._utils.array import calculate_volume +from .._utils.coverage import is_implemented +from ..runtime import runtime +from ..types import NdShape +from .array_joining import hstack +from .array_shape import reshape +from .array_tiling import tile +from .creation_data import asarray +from .creation_matrices import tri +from .creation_ranges import arange +from .creation_shape import empty, ones +from .ssc_counting import count_nonzero +from .ssc_searching import nonzero + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + +if TYPE_CHECKING: + from typing import Callable + + import numpy.typing as npt + + from ..types import BoundsMode, OrderType + +_builtin_min = min + + +@add_boilerplate("arr", "mask", "vals") +def place(arr: ndarray, mask: ndarray, vals: ndarray) -> None: + """ + Change elements of an array based on conditional and input values. + + Parameters + ---------- + arr : array_like + Array to put data into. + mask : array_like + Mask array. Must have the same size as `arr`. + vals : 1-D sequence + Values to put into `arr`. Only the first N elements are used, + where N is the number of True values in mask. If vals is smaller + than N, it will be repeated, and if elements of a are to be masked, + this sequence must be non-empty. + + See Also + -------- + numpy.copyto, numpy.put, numpy.take, numpy.extract + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if arr.size == 0: + return + + check_writeable(arr) + + if mask.size != arr.size: + raise ValueError("arr array and condition array must be of same size") + + if vals.ndim != 1: + raise ValueError("vals array has to be 1-dimensional") + + if mask.shape != arr.shape: + mask_reshape = reshape(mask, arr.shape) + else: + mask_reshape = mask + + num_values = int(count_nonzero(mask_reshape)) + if num_values == 0: + return + + if vals.size == 0: + raise ValueError("vals array cannot be empty") + + if num_values != vals.size: + reps = (num_values + vals.size - 1) // vals.size + vals_resized = tile(A=vals, reps=reps) if reps > 1 else vals + vals_resized = vals_resized[:num_values] + else: + vals_resized = vals + + if mask_reshape.dtype == bool: + arr._thunk.set_item(mask_reshape._thunk, vals_resized._thunk) + else: + bool_mask = mask_reshape.astype(bool) + arr._thunk.set_item(bool_mask._thunk, vals_resized._thunk) + + +# Indexing-like operations +def indices( + dimensions: Sequence[int], dtype: npt.DTypeLike = int, sparse: bool = False +) -> ndarray | tuple[ndarray, ...]: + """ + Return an array representing the indices of a grid. + Compute an array where the subarrays contain index values 0, 1, ... + varying only along the corresponding axis. + + Parameters + ---------- + dimensions : Sequence[int] + The shape of the grid. + dtype : data-type, optional + Data type of the result. + sparse : bool, optional + Return a sparse representation of the grid instead of a dense + representation. Default is False. + + Returns + ------- + grid : ndarray or tuple[ndarray, ...] + If sparse is False returns one array of grid indices, + ``grid.shape = (len(dimensions),) + tuple(dimensions)``. + If sparse is True returns a tuple of arrays, with + ``grid[i].shape = (1, ..., 1, dimensions[i], 1, ..., 1)`` with + dimensions[i] in the ith place + + See Also + -------- + numpy.indices + + Notes + ----- + The output shape in the dense case is obtained by prepending the number + of dimensions in front of the tuple of dimensions, i.e. if `dimensions` + is a tuple ``(r0, ..., rN-1)`` of length ``N``, the output shape is + ``(N, r0, ..., rN-1)``. + The subarrays ``grid[k]`` contains the N-D array of indices along the + ``k-th`` axis. Explicitly: + + grid[k, i0, i1, ..., iN-1] = ik + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # implementation of indices routine is adapted from NumPy + dimensions = tuple(dimensions) + N = len(dimensions) + shape = (1,) * N + if sparse: + res_tuple: tuple[ndarray, ...] = () + for i, dim in enumerate(dimensions): + idx = arange(dim, dtype=dtype).reshape( + shape[:i] + (dim,) + shape[i + 1 :] + ) + res_tuple += (idx,) + return res_tuple + else: + out_shape = (N,) + dimensions + res_array: ndarray = empty(out_shape, dtype=dtype) + for i, dim in enumerate(dimensions): + idx = arange(dim, dtype=dtype).reshape( + shape[:i] + (dim,) + shape[i + 1 :] + ) + res_array[i] = idx + return res_array + + +def mask_indices( + n: int, mask_func: Callable[[ndarray, int], ndarray], k: int = 0 +) -> tuple[ndarray, ...]: + """ + Return the indices to access (n, n) arrays, given a masking function. + + Assume `mask_func` is a function that, for a square array a of size + ``(n, n)`` with a possible offset argument `k`, when called as + ``mask_func(a, k)`` returns a new array with zeros in certain locations + (functions like :func:`cupynumeric.triu` or :func:`cupynumeric.tril` + do precisely this). Then this function returns the indices where + the non-zero values would be located. + + Parameters + ---------- + n : int + The returned indices will be valid to access arrays of shape (n, n). + mask_func : callable + A function whose call signature is similar to that of + :func:`cupynumeric.triu`, :func:`cupynumeric.tril`. + That is, ``mask_func(x, k)`` returns a boolean array, shaped like `x`. + `k` is an optional argument to the function. + k : scalar + An optional argument which is passed through to `mask_func`. Functions + like :func:`cupynumeric.triu`, :func:`cupynumeric,tril` + take a second argument that is interpreted as an offset. + + Returns + ------- + indices : tuple of arrays. + The `n` arrays of indices corresponding to the locations where + ``mask_func(np.ones((n, n)), k)`` is True. + + See Also + -------- + numpy.mask_indices + + Notes + ----- + WARNING: ``mask_indices`` expects ``mask_function`` to call cuPyNumeric + functions for good performance. In case non-cuPyNumeric functions are + called by ``mask_function``, cuPyNumeric will have to materialize all data + on the host which might result in running out of system memory. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # this implementation is based on the Cupy + a = ones((n, n), dtype=bool) + if not is_implemented(mask_func): + runtime.warn( + "Calling non-cuPyNumeric functions in mask_func can result in bad " + "performance", + category=UserWarning, + ) + return mask_func(a, k).nonzero() + + +@add_boilerplate("indices") +def unravel_index( + indices: ndarray, + shape: NdShape, + order: OrderType = "C", +) -> tuple[ndarray, ...] | ndarray: + """ + Converts a flat index or array of flat indices into a tuple + of coordinate arrays. + + Parameters + ---------- + indices : array_like + An integer array whose elements are indices into the flattened + version of an array of dimensions ``shape``. + shape : tuple of ints + The shape of the array to use for unraveling ``indices``. + + order : {'C', 'F'}, optional + Determines whether the indices should be viewed as indexing in + row-major (C-style) or column-major (Fortran-style) order. + + Returns + ------- + unraveled_coords : tuple of ndarray + Each array in the tuple has the same shape as the ``indices`` + array. + + See Also + -------- + numpy.unravel_index + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if order not in ( + "F", + "C", + ): + raise ValueError("order is not understood") + + if indices is None or not np.can_cast( + indices.dtype, np.int64, "same_kind" + ): + raise TypeError("only int indices permitted") + + size = calculate_volume(shape) + + if (indices < 0).any() or (indices > size).any(): + raise ValueError("indices have out-of-bounds value(s)") + + if indices.size == 0: + unraveled_coords = tuple( + empty(indices.shape, dtype=indices.dtype) + for dim in range(len(shape)) + ) + return unraveled_coords + + unraveled_coords = tuple() + for dim in shape[::-1] if order == "C" else shape: + unraveled_coords = ( + (indices % dim,) + unraveled_coords + if order == "C" + else unraveled_coords + (indices % dim,) + ) + indices = indices // dim + return unraveled_coords + + +def diag_indices(n: int, ndim: int = 2) -> tuple[ndarray, ...]: + """ + Return the indices to access the main diagonal of an array. + + This returns a tuple of indices that can be used to access the main + diagonal of an array a with a.ndim >= 2 dimensions and + shape (n, n, …, n). For a.ndim = 2 this is the usual diagonal, + for a.ndim > 2 this is the set of indices to + access a[i, i, ..., i] for i = [0..n-1]. + + Parameters + ---------- + n : int + The size, along each dimension, of the arrays for which the + returned indices can be used. + ndim : int, optional + The number of dimensions. + + See Also + -------- + numpy.diag_indices + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + idx = arange(n, dtype=int) + return (idx,) * ndim + + +@add_boilerplate("arr") +def diag_indices_from(arr: ndarray) -> tuple[ndarray, ...]: + """ + Return the indices to access the main diagonal of an n-dimensional array. + + See diag_indices for full details. + + Parameters + ---------- + arr : array_like + at least 2-D + + See Also + -------- + numpy.diag_indices_from, numpy.diag_indices + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if not arr.ndim >= 2: + raise ValueError("input array must be at least 2-d") + # For more than d=2, the strided formula is only valid for arrays with + # all dimensions equal, so we check first. + for i in range(1, arr.ndim): + if arr.shape[i] != arr.shape[0]: + raise ValueError("All dimensions of input must be of equal length") + + return diag_indices(arr.shape[0], arr.ndim) + + +def tril_indices( + n: int, k: int = 0, m: int | None = None +) -> tuple[ndarray, ...]: + """ + Return the indices for the lower-triangle of an (n, m) array. + + Parameters + ---------- + n : int + The row dimension of the arrays for which the returned + indices will be valid. + k : int, optional + Diagonal offset (see :func:`cupynumeric.tril` for details). + m : int, optional + The column dimension of the arrays for which the returned + indices will be valid. + By default `m` is taken equal to `n`. + + Returns + ------- + inds : tuple of arrays + The indices for the lower-triangle. The returned tuple contains two + arrays, each with the indices along one dimension of the array. + + See also + -------- + numpy.tril_indices + + Notes + ----- + + Availability + ------------ + Multiple GPUs, Multiple CPUs + """ + + tri_ = tri(n, m, k=k, dtype=bool) + return nonzero(tri_) + + +@add_boilerplate("arr") +def tril_indices_from(arr: ndarray, k: int = 0) -> tuple[ndarray, ...]: + """ + Return the indices for the lower-triangle of arr. + + See :func:`cupynumeric.tril_indices` for full details. + + Parameters + ---------- + arr : array_like + The indices will be valid for arrays whose dimensions are + the same as arr. + k : int, optional + Diagonal offset (see :func:`cupynumeric.tril` for details). + + Returns + ------- + inds : tuple of arrays + The indices for the lower-triangle. The returned tuple contains two + arrays, each with the indices along one dimension of the array. + + See Also + -------- + numpy.tril_indices_from + + Notes + ----- + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + # this implementation is taken from numpy + if arr.ndim != 2: + raise ValueError("input array must be 2-d") + return tril_indices(arr.shape[-2], k=k, m=arr.shape[-1]) + + +def triu_indices( + n: int, k: int = 0, m: int | None = None +) -> tuple[ndarray, ...]: + """ + Return the indices for the upper-triangle of an (n, m) array. + + Parameters + ---------- + n : int + The size of the arrays for which the returned indices will + be valid. + k : int, optional + Diagonal offset (see :func:`cupynumeric.triu` for details). + m : int, optional + The column dimension of the arrays for which the returned + arrays will be valid. + By default `m` is taken equal to `n`. + + Returns + ------- + inds : tuple of arrays + The indices for the upper-triangle. The returned tuple contains two + arrays, each with the indices along one dimension of the array. + + See also + -------- + numpy.triu_indices + + Notes + ----- + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + tri_ = ~tri(n, m, k=k - 1, dtype=bool) + return nonzero(tri_) + + +@add_boilerplate("arr") +def triu_indices_from(arr: ndarray, k: int = 0) -> tuple[ndarray, ...]: + """ + Return the indices for the upper-triangle of arr. + + See :func:`cupynumeric.triu_indices` for full details. + + Parameters + ---------- + arr : ndarray, shape(N, N) + The indices will be valid for arrays whose dimensions are + the same as arr. + k : int, optional + Diagonal offset (see :func:`cupynumeric.triu` for details). + + Returns + ------- + inds : tuple of arrays + The indices for the upper-triangle. The returned tuple contains two + arrays, each with the indices along one dimension of the array. + + See Also + -------- + numpy.triu_indices_from + + Notes + ----- + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # this implementation is taken from numpy + if arr.ndim != 2: + raise ValueError("input array must be 2-d") + return triu_indices(arr.shape[-2], k=k, m=arr.shape[-1]) + + +@add_boilerplate("a") +def take( + a: ndarray, + indices: ndarray, + axis: int | None = None, + out: ndarray | None = None, + mode: BoundsMode = "raise", +) -> ndarray: + """ + Take elements from an array along an axis. + When axis is not None, this function does the same thing as “fancy” + indexing (indexing arrays using arrays); however, it can be easier + to use if you need elements along a given axis. A call such as + `np.take(arr, indices, axis=3)` is equivalent to `arr[:,:,:,indices,...]`. + + Parameters + ---------- + a : array_like `(Ni…, M, Nk…)` + The source array. + indices : array_like `(Nj…)` + The indices of the values to extract. + Also allow scalars for indices. + axis : int, optional + The axis over which to select values. By default, the flattened input + array is used. + out : ndarray, optional `(Ni…, Nj…, Nk…)` + If provided, the result will be placed in this array. It should be of + the appropriate shape and dtype. + mode : ``{'raise', 'wrap', 'clip'}``, optional + Specifies how out-of-bounds indices will behave. + 'raise' - raise an error (default) + 'wrap' - wrap around + 'clip' - clip to the range + 'clip' mode means that all indices that are too large are replaced by + the index that addresses the last element along that axis. + Note that this disables indexing with negative numbers. + + Returns + ------- + out : ndarray `(Ni…, Nj…, Nk…)` + The returned array has the same type as a. + + Raises + ------ + + See Also + -------- + numpy.take + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.take(indices=indices, axis=axis, out=out, mode=mode) + + +def ix_(*args: Any) -> tuple[ndarray, ...]: + """ + Construct an open mesh from multiple sequences. + + This function takes N 1-D sequences and returns N outputs with N + dimensions each, such that the shape is 1 in all but one dimension + and the dimension with the non-unit shape value cycles through all + N dimensions. + + Using `ix_` one can quickly construct index arrays that will index + the cross product. ``a[np.ix_([1,3],[2,5])]`` returns the array + ``[[a[1,2] a[1,5]], [a[3,2] a[3,5]]]``. + + Parameters + ---------- + args : 1-D sequences + Each sequence should be of integer or boolean type. + Boolean sequences will be interpreted as boolean masks for the + corresponding dimension (equivalent to passing in + ``np.nonzero(boolean_sequence)``). + + Returns + ------- + out : tuple of ndarrays + N arrays with N dimensions each, with N the number of input + sequences. Together these arrays form an open mesh. + + See Also + -------- + ogrid, mgrid, meshgrid + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + out = [] + nd = len(args) + + for k, new in enumerate(args): + if not isinstance(new, ndarray): + new = asarray(new) + if new.size == 0: + # Explicitly type empty arrays to avoid float default + new = new.astype(np.intp) + + if new.ndim != 1: + raise ValueError("Cross index must be 1 dimensional") + + if np.issubdtype(new.dtype, bool): + (new,) = new.nonzero() + + new = new.reshape((1,) * k + (new.size,) + (1,) * (nd - k - 1)) + out.append(new) + + return tuple(out) + + +def _fill_fancy_index_for_along_axis_routines( + a_shape: NdShape, axis: int, indices: ndarray +) -> tuple[ndarray, ...]: + # the logic below is base on the cupy implementation of + # the *_along_axis routines + ndim = len(a_shape) + fancy_index = [] + for i, n in enumerate(a_shape): + if i == axis: + fancy_index.append(indices) + else: + ind_shape = (1,) * i + (-1,) + (1,) * (ndim - i - 1) + fancy_index.append(arange(n).reshape(ind_shape)) + return tuple(fancy_index) + + +@add_boilerplate("a", "indices") +def take_along_axis(a: ndarray, indices: ndarray, axis: int | None) -> ndarray: + """ + Take values from the input array by matching 1d index and data slices. + + This iterates over matching 1d slices oriented along the specified axis in + the index and data arrays, and uses the former to look up values in the + latter. These slices can be different lengths. + + Functions returning an index along an axis, like + :func:`cupynumeric.argsort` and :func:`cupynumeric.argpartition`, + produce suitable indices for this function. + + Parameters + ---------- + arr : ndarray (Ni..., M, Nk...) + Source array + indices : ndarray (Ni..., J, Nk...) + Indices to take along each 1d slice of `arr`. This must match the + dimension of arr, but dimensions Ni and Nj only need to broadcast + against `arr`. + axis : int + The axis to take 1d slices along. If axis is None, the input array is + treated as if it had first been flattened to 1d, for consistency with + :func:`cupynumeric.sort` and :func:`cupynumeric.argsort`. + + Returns + ------- + out: ndarray (Ni..., J, Nk...) + The indexed result. It is going to be a view to `arr` for most cases, + except the case when `axis=Null` and `arr.ndim>1`. + + See Also + -------- + numpy.take_along_axis + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if not np.issubdtype(indices.dtype, np.integer): + raise TypeError("`indices` must be an integer array") + + computed_axis = 0 + if axis is None: + if indices.ndim != 1: + raise ValueError("indices must be 1D if axis=None") + if a.ndim > 1: + a = a.ravel() + else: + computed_axis = normalize_axis_index(axis, a.ndim) + + if a.ndim != indices.ndim: + raise ValueError( + "`indices` and `a` must have the same number of dimensions" + ) + return a[ + _fill_fancy_index_for_along_axis_routines( + a.shape, computed_axis, indices + ) + ] + + +@add_boilerplate("a", "indices", "values") +def put_along_axis( + a: ndarray, indices: ndarray, values: ndarray, axis: int | None +) -> None: + """ + Put values into the destination array by matching 1d index and data slices. + + This iterates over matching 1d slices oriented along the specified axis in + the index and data arrays, and uses the former to place values into the + latter. These slices can be different lengths. + + Functions returning an index along an axis, like + :func:`cupynumeric.argsort` and :func:`cupynumeric.argpartition`, produce + suitable indices for this function. + + Parameters + ---------- + a : ndarray (Ni..., M, Nk...) + Destination array. + indices : ndarray (Ni..., J, Nk...) + Indices to change along each 1d slice of `arr`. This must match the + dimension of arr, but dimensions in Ni and Nj may be 1 to broadcast + against `arr`. + values : array_like (Ni..., J, Nk...) + values to insert at those indices. Its shape and dimension are + broadcast to match that of `indices`. + axis : int + The axis to take 1d slices along. If axis is None, the destination + array is treated as if a flattened 1d view had been created of it. + `axis=None` case is currently supported only for 1D input arrays. + + Note + ---- + Having duplicate entries in `indices` will result in undefined behavior + since operation performs asynchronous update of the `arr` entries. + + See Also + -------- + numpy.put_along_axis + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + + if a.size == 0: + return + + check_writeable(a) + + if not np.issubdtype(indices.dtype, np.integer): + raise TypeError("`indices` must be an integer array") + + computed_axis = 0 + if axis is None: + if indices.ndim != 1: + raise ValueError("indices must be 1D if axis=None") + if a.ndim > 1: + # TODO call a=a.flat when flat is implemented + raise ValueError("a.ndim>1 case is not supported when axis=None") + if (indices.size == 0) or (values.size == 0): + return + if values.shape != indices.shape: + values = values._wrap(indices.size) + else: + computed_axis = normalize_axis_index(axis, a.ndim) + + if a.ndim != indices.ndim: + raise ValueError( + "`indices` and `a` must have the same number of dimensions" + ) + ind = _fill_fancy_index_for_along_axis_routines( + a.shape, computed_axis, indices + ) + a[ind] = values + + +@add_boilerplate("a") +def choose( + a: ndarray, + choices: Sequence[ndarray], + out: ndarray | None = None, + mode: BoundsMode = "raise", +) -> ndarray: + """ + Construct an array from an index array and a list of arrays to choose from. + + Given an "index" array (`a`) of integers and a sequence of ``n`` arrays + (`choices`), `a` and each choice array are first broadcast, as necessary, + to arrays of a common shape; calling these *Ba* and *Bchoices[i], i = + 0,...,n-1* we have that, necessarily, ``Ba.shape == Bchoices[i].shape`` + for each ``i``. Then, a new array with shape ``Ba.shape`` is created as + follows: + + * if ``mode='raise'`` (the default), then, first of all, each element of + ``a`` (and thus ``Ba``) must be in the range ``[0, n-1]``; now, suppose + that ``i`` (in that range) is the value at the ``(j0, j1, ..., jm)`` + position in ``Ba`` - then the value at the same position in the new array + is the value in ``Bchoices[i]`` at that same position; + + * if ``mode='wrap'``, values in `a` (and thus `Ba`) may be any (signed) + integer; modular arithmetic is used to map integers outside the range + `[0, n-1]` back into that range; and then the new array is constructed + as above; + + * if ``mode='clip'``, values in `a` (and thus ``Ba``) may be any (signed) + integer; negative integers are mapped to 0; values greater than ``n-1`` + are mapped to ``n-1``; and then the new array is constructed as above. + + Parameters + ---------- + a : ndarray[int] + This array must contain integers in ``[0, n-1]``, where ``n`` is the + number of choices, unless ``mode=wrap`` or ``mode=clip``, in which + cases any integers are permissible. + choices : Sequence[ndarray] + Choice arrays. `a` and all of the choices must be broadcastable to the + same shape. If `choices` is itself an array (not recommended), then + its outermost dimension (i.e., the one corresponding to + ``choices.shape[0]``) is taken as defining the "sequence". + out : ndarray, optional + If provided, the result will be inserted into this array. It should + be of the appropriate shape and dtype. Note that `out` is always + buffered if ``mode='raise'``; use other modes for better performance. + mode : ``{'raise', 'wrap', 'clip'}``, optional + Specifies how indices outside ``[0, n-1]`` will be treated: + + * 'raise' : an exception is raised (default) + * 'wrap' : value becomes value mod ``n`` + * 'clip' : values < 0 are mapped to 0, values > n-1 are mapped to n-1 + + Returns + ------- + merged_array : ndarray + The merged result. + + Raises + ------ + ValueError: shape mismatch + If `a` and each choice array are not all broadcastable to the same + shape. + + See Also + -------- + numpy.choose + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.choose(choices=choices, out=out, mode=mode) + + +def select( + condlist: Sequence[npt.ArrayLike | ndarray], + choicelist: Sequence[npt.ArrayLike | ndarray], + default: Any = 0, +) -> ndarray: + """ + Return an array drawn from elements in choicelist, depending on conditions. + + Parameters + ---------- + condlist : list of bool ndarrays + The list of conditions which determine from which array in `choicelist` + the output elements are taken. When multiple conditions are satisfied, + the first one encountered in `condlist` is used. + choicelist : list of ndarrays + The list of arrays from which the output elements are taken. It has + to be of the same length as `condlist`. + default : scalar, optional + The element inserted in `output` when all conditions evaluate to False. + + Returns + ------- + output : ndarray + The output at position m is the m-th element of the array in + `choicelist` where the m-th element of the corresponding array in + `condlist` is True. + + See Also + -------- + numpy.select + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if len(condlist) != len(choicelist): + raise ValueError( + "list of cases must be same length as list of conditions" + ) + if len(condlist) == 0: + raise ValueError("select with an empty condition list is not possible") + + condlist_ = tuple(convert_to_cupynumeric_ndarray(c) for c in condlist) + for i, c in enumerate(condlist_): + if c.dtype != bool: + raise TypeError( + f"invalid entry {i} in condlist: should be boolean ndarray" + ) + + choicelist_ = tuple(convert_to_cupynumeric_ndarray(c) for c in choicelist) + common_type = np.result_type(*choicelist_, default) + args = condlist_ + choicelist_ + choicelist_ = tuple( + c._maybe_convert(common_type, args) for c in choicelist_ + ) + default_ = np.array(default, dtype=common_type) + + out_shape = np.broadcast_shapes( + *(c.shape for c in condlist_), + *(c.shape for c in choicelist_), + ) + out = ndarray(shape=out_shape, dtype=common_type, inputs=args) + out._thunk.select( + tuple(c._thunk for c in condlist_), + tuple(c._thunk for c in choicelist_), + default_, + ) + return out + + +@add_boilerplate("condition", "a") +def compress( + condition: ndarray, + a: ndarray, + axis: int | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return selected slices of an array along given axis. + + When working along a given axis, a slice along that axis is returned + in output for each index where condition evaluates to True. + When working on a 1-D array, compress is equivalent to numpy.extract. + + Parameters + ---------- + condition, 1-D array of bools + Array that selects which entries to return. If `len(c)` is less than + the size of a along the given axis, then output is truncated to the + length of the condition array. + + a : array_like + Array from which to extract a part. + + axis: int, optional + Axis along which to take slices. If None (default), + work on the flattened array. + + out : ndarray, optional + Output array. Its type is preserved and it must be of the right + shape to hold the output. + + Returns + ------- + compressed_array : ndarray + A copy of `a` without the slices along `axis` for which condition + is false. + + Raises + ------ + ValueError : dimension mismatch + If condition is not 1D array + ValueError : shape mismatch + If condition contains entries that are out of bounds of array + ValueError : shape mismatch + If output array has a wrong shape + + See Also + -------- + numpy.compress, numpy.extract + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + return a.compress(condition, axis=axis, out=out) + + +@add_boilerplate("a") +def diagonal( + a: ndarray, + offset: int = 0, + axis1: int = 0, + axis2: int = 1, + extract: bool = True, +) -> ndarray: + """ + diagonal(a: ndarray, offset=0, axis1=None, axis2=None) + + Return specified diagonals. + + If `a` is 2-D, returns the diagonal of `a` with the given offset, + i.e., the collection of elements of the form ``a[i, i+offset]``. If + `a` has more than two dimensions, then the axes specified by `axis1` + and `axis2` are used to determine the 2-D sub-array whose diagonal is + returned. The shape of the resulting array can be determined by + removing `axis1` and `axis2` and appending an index to the right equal + to the size of the resulting diagonals. + + Parameters + ---------- + a : array_like + Array from which the diagonals are taken. + offset : int, optional + Offset of the diagonal from the main diagonal. Can be positive or + negative. Defaults to main diagonal (0). + axis1 : int, optional + Axis to be used as the first axis of the 2-D sub-arrays from which + the diagonals should be taken. Defaults to first axis (0). + axis2 : int, optional + Axis to be used as the second axis of the 2-D sub-arrays from + which the diagonals should be taken. Defaults to second axis (1). + + Returns + ------- + array_of_diagonals : ndarray + If `a` is 2-D, then a 1-D array containing the diagonal and of the + same type as `a` is returned unless `a` is a `matrix`, in which case + a 1-D array rather than a (2-D) `matrix` is returned in order to + maintain backward compatibility. + + If ``a.ndim > 2``, then the dimensions specified by `axis1` and `axis2` + are removed, and a new axis inserted at the end corresponding to the + diagonal. + + Raises + ------ + ValueError + If the dimension of `a` is less than 2. + + Notes + ----- + Unlike NumPy's, the cuPyNumeric implementation always returns a copy + + See Also + -------- + numpy.diagonal + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + return a.diagonal(offset=offset, axis1=axis1, axis2=axis2, extract=extract) + + +@add_boilerplate("a", "indices", "values") +def put( + a: ndarray, indices: ndarray, values: ndarray, mode: str = "raise" +) -> None: + """ + Replaces specified elements of an array with given values. + The indexing works as if the target array is first flattened. + + Parameters + ---------- + a : array_like + Array to put data into + indices : array_like + Target indices, interpreted as integers. + WARNING: In case there are repeated entries in the + indices array, Legate doesn't guarantee the order in + which values are updated. + + values : array_like + Values to place in `a` at target indices. If values array is shorter + than indices, it will be repeated as necessary. + mode : {'raise', 'wrap', 'clip'}, optional + Specifies how out-of-bounds indices will behave. + 'raise' : raise an error. + 'wrap' : wrap around. + 'clip' : clip to the range. + + See Also + -------- + numpy.put + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + a.put(indices=indices, values=values, mode=mode) + + +@add_boilerplate("a", "mask", "values") +def putmask(a: ndarray, mask: ndarray, values: ndarray) -> None: + """ + putmask(a, mask, values) + Changes elements of an array based on conditional and input values. + Sets ``a.flat[n] = values[n]`` for each n where ``mask.flat[n]==True``. + If `values` is not the same size as `a` and `mask` then it will repeat. + This gives behavior different from ``a[mask] = values``. + + Parameters + ---------- + a : ndarray + Target array. + mask : array_like + Boolean mask array. It has to be the same shape as `a`. + values : array_like + Values to put into `a` where `mask` is True. If `values` is smaller + than `a` it will be repeated. + + See Also + -------- + numpy.putmask + + Availability + ------------ + Multiple GPUs, Multiple CPUs + """ + if not a.shape == mask.shape: + raise ValueError("mask and data must be the same size") + + check_writeable(a) + + mask = mask._warn_and_convert(np.dtype(bool)) + + if a.dtype != values.dtype: + values = values._warn_and_convert(a.dtype) + + try: + np.broadcast_shapes(values.shape, a.shape) + except ValueError: + values = values._wrap(a.size) + values = values.reshape(a.shape) + + a._thunk.putmask(mask._thunk, values._thunk) + + +@add_boilerplate("a", "val") +def fill_diagonal(a: ndarray, val: ndarray, wrap: bool = False) -> None: + """ + Fill the main diagonal of the given array of any dimensionality. + + For an array a with a.ndim >= 2, the diagonal is the list of locations with + indices a[i, ..., i] all identical. This function modifies the input + array in-place, it does not return a value. + + Parameters + ---------- + + a : array, at least 2-D. + Array whose diagonal is to be filled, it gets modified in-place. + val : scalar or array_like + Value(s) to write on the diagonal. If val is scalar, the value is + written along the diagonal. + If array-like, the flattened val is written along + the diagonal, repeating if necessary to fill all diagonal entries. + wrap : bool + If true, the diagonal "wraps" after N columns, for tall 2d matrices. + + Raises + ------ + ValueError + If the dimension of `a` is less than 2. + + Notes + ----- + + See Also + -------- + numpy.fill_diagonal + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + if val.size == 0 or a.size == 0: + return + + check_writeable(a) + + if a.ndim < 2: + raise ValueError("array must be at least 2-d") + + n = _builtin_min(a.shape) + + if a.ndim > 2: + for s in a.shape: + if s != n: + raise ValueError( + "All dimensions of input must be of equal length" + ) + + len_val = n + + if a.ndim == 2 and wrap and a.shape[0] > a.shape[1]: + len_val = a.shape[0] - (a.shape[0] // (a.shape[1] + 1)) + + if (val.size != len_val and val.ndim > 0) or val.ndim > 1: + val = val._wrap(len_val) + + if a.ndim == 2 and wrap and a.shape[0] > a.shape[1]: + idx0_tmp = arange(a.shape[1], dtype=int) + idx0 = idx0_tmp.copy() + while idx0.size < len_val: + idx0_tmp = idx0_tmp + (a.shape[1] + 1) + idx0 = hstack((idx0, idx0_tmp)) + idx0 = idx0[0:len_val] + idx1 = arange(len_val, dtype=int) % a.shape[1] + a[idx0, idx1] = val + else: + idx = arange(n, dtype=int) + indices = (idx,) * a.ndim + + a[indices] = val diff --git a/cupynumeric/_module/io_numpy.py b/cupynumeric/_module/io_numpy.py new file mode 100644 index 000000000..42d4ebdf5 --- /dev/null +++ b/cupynumeric/_module/io_numpy.py @@ -0,0 +1,75 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .._array.array import ndarray + from os import PathLike + from typing import BinaryIO + +import numpy as np + +from .creation_data import array + + +def load( + file: str | bytes | PathLike[Any] | BinaryIO, + *, + max_header_size: int = 10000, +) -> ndarray: + """ + Load an array from a ``.npy`` file. + + Parameters + ---------- + file : file-like object, string, or pathlib.Path + The file to read. File-like objects must support the + ``seek()`` and ``read()`` methods and must always + be opened in binary mode. + max_header_size : int, optional + Maximum allowed size of the header. Large headers may not be safe + to load securely and thus require explicitly passing a larger value. + See :py:func:`ast.literal_eval()` for details. + + Returns + ------- + result : array + Data stored in the file. + + Raises + ------ + OSError + If the input file does not exist or cannot be read. + + See Also + -------- + numpy.load + + Notes + ----- + cuPyNumeric does not currently support ``.npz`` and pickled files. + + Availability + -------- + Single CPU + """ + return array( + np.load( + file, + max_header_size=max_header_size, # type: ignore [call-arg] + ) + ) diff --git a/cupynumeric/_module/linalg_mvp.py b/cupynumeric/_module/linalg_mvp.py new file mode 100644 index 000000000..8650b1b00 --- /dev/null +++ b/cupynumeric/_module/linalg_mvp.py @@ -0,0 +1,935 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import re +from collections import Counter +from itertools import chain +from typing import TYPE_CHECKING, Any, Literal + +import numpy as np +import opt_einsum as oe # type: ignore [import] + +from .._array.array import ndarray +from .._array.util import ( + add_boilerplate, + convert_to_cupynumeric_ndarray, + find_common_type, +) +from .._ufunc.math import multiply +from .._utils.linalg import ( + AxesPairLike, + inner_modes, + matmul_modes, + tensordot_modes, +) +from ..types import NdShape +from .creation_data import copy + +if TYPE_CHECKING: + from .._ufunc.ufunc import CastingKind + +_builtin_all = all +_builtin_max = max + + +@add_boilerplate("a", "b") +def inner(a: ndarray, b: ndarray, out: ndarray | None = None) -> ndarray: + """ + Inner product of two arrays. + + Ordinary inner product of vectors for 1-D arrays (without complex + conjugation), in higher dimensions a sum product over the last axes. + + Parameters + ---------- + a, b : array_like + out : ndarray, optional + Output argument. This must have the exact shape that would be returned + if it was not present. If its dtype is not what would be expected from + this operation, then the result will be (unsafely) cast to `out`. + + Returns + ------- + output : ndarray + If `a` and `b` are both + scalars or both 1-D arrays then a scalar is returned; otherwise + an array is returned. + ``output.shape = (*a.shape[:-1], *b.shape[:-1])`` + If `out` is given, then it is returned. + + Notes + ----- + The cuPyNumeric implementation is a little more liberal than NumPy in terms + of allowed broadcasting, e.g. ``inner(ones((1,)), ones((4,)))`` is allowed. + + See Also + -------- + numpy.inner + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if a.ndim == 0 or b.ndim == 0: + return multiply(a, b, out=out) + (a_modes, b_modes, out_modes) = inner_modes(a.ndim, b.ndim) + return _contract( + a_modes, + b_modes, + out_modes, + a, + b, + out=out, + casting="unsafe", + ) + + +@add_boilerplate("a", "b") +def dot(a: ndarray, b: ndarray, out: ndarray | None = None) -> ndarray: + """ + Dot product of two arrays. Specifically, + + - If both `a` and `b` are 1-D arrays, it is inner product of vectors + (without complex conjugation). + + - If both `a` and `b` are 2-D arrays, it is matrix multiplication, + but using ``a @ b`` is preferred. + + - If either `a` or `b` is 0-D (scalar), it is equivalent to + :func:`multiply` and using ``cupynumeric.multiply(a, b)`` or ``a * b`` is + preferred. + + - If `a` is an N-D array and `b` is a 1-D array, it is a sum product over + the last axis of `a` and `b`. + + - If `a` is an N-D array and `b` is an M-D array (where ``M>=2``), it is a + sum product over the last axis of `a` and the second-to-last axis of + `b`:: + + dot(a: ndarray, b)[i,j,k,m] = sum(a[i,j,:] * b[k,:,m]) + + Parameters + ---------- + a : array_like + First argument. + b : array_like + Second argument. + out : ndarray, optional + Output argument. This must have the exact shape and dtype that would be + returned if it was not present. + + Returns + ------- + output : ndarray + Returns the dot product of `a` and `b`. If `out` is given, then it is + returned. + + Notes + ----- + The cuPyNumeric implementation is a little more liberal than NumPy in terms + of allowed broadcasting, e.g. ``dot(ones((3,1)), ones((4,5)))`` is allowed. + + Except for the inner-product case, only floating-point types are supported. + + See Also + -------- + numpy.dot + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.dot(b, out=out) + + +@add_boilerplate("a", "b") +def matmul( + a: ndarray, + b: ndarray, + /, + out: ndarray | None = None, + *, + casting: CastingKind = "same_kind", + dtype: np.dtype[Any] | None = None, +) -> ndarray: + """ + Matrix product of two arrays. + + Parameters + ---------- + a, b : array_like + Input arrays, scalars not allowed. + out : ndarray, optional + A location into which the result is stored. If provided, it must have + a shape that matches the signature `(n,k),(k,m)->(n,m)`. + casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional + Controls what kind of data casting may occur. + + * 'no' means the data types should not be cast at all. + * 'equiv' means only byte-order changes are allowed. + * 'safe' means only casts which can preserve values are allowed. + * 'same_kind' means only safe casts or casts within a kind, + like float64 to float32, are allowed. + * 'unsafe' means any data conversions may be done. + + Default is 'same_kind'. + dtype : data-type, optional + If provided, forces the calculation to use the data type specified. + Note that you may have to also give a more liberal `casting` + parameter to allow the conversions. Default is None. + + Returns + ------- + output : ndarray + The matrix product of the inputs. + This is a scalar only when both a, b are 1-d vectors. + If `out` is given, then it is returned. + + Notes + ----- + The behavior depends on the arguments in the following way. + + - If both arguments are 2-D they are multiplied like conventional + matrices. + - If either argument is N-D, N > 2, it is treated as a stack of + matrices residing in the last two indexes and broadcast accordingly. + - If the first argument is 1-D, it is promoted to a matrix by + prepending a 1 to its dimensions. After matrix multiplication + the prepended 1 is removed. + - If the second argument is 1-D, it is promoted to a matrix by + appending a 1 to its dimensions. After matrix multiplication + the appended 1 is removed. + + ``matmul`` differs from ``dot`` in two important ways: + + - Multiplication by scalars is not allowed, use ``*`` instead. + - Stacks of matrices are broadcast together as if the matrices + were elements, respecting the signature ``(n,k),(k,m)->(n,m)``: + + >>> a = ones([9, 5, 7, 4]) + >>> c = ones([9, 5, 4, 3]) + >>> dot(a: ndarray, c).shape + (9, 5, 7, 9, 5, 3) + >>> matmul(a: ndarray, c).shape + (9, 5, 7, 3) + >>> # n is 7, k is 4, m is 3 + + The cuPyNumeric implementation is a little more liberal than NumPy in terms + of allowed broadcasting, e.g. ``matmul(ones((3,1)), ones((4,5)))`` is + allowed. + + Only floating-point types are supported. + + See Also + -------- + numpy.matmul + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if a.ndim == 0 or b.ndim == 0: + raise ValueError("Scalars not allowed in matmul") + + (a_modes, b_modes, out_modes) = matmul_modes(a.ndim, b.ndim) + + return _contract( + a_modes, + b_modes, + out_modes, + a, + b, + out=out, + casting=casting, + dtype=dtype, + ) + + +@add_boilerplate("a", "b") +def vdot(a: ndarray, b: ndarray, out: ndarray | None = None) -> ndarray: + """ + Return the dot product of two vectors. + + The vdot(`a`, `b`) function handles complex numbers differently than + dot(`a`, `b`). If the first argument is complex the complex conjugate + of the first argument is used for the calculation of the dot product. + + Note that `vdot` handles multidimensional arrays differently than `dot`: + it does *not* perform a matrix product, but flattens input arguments + to 1-D vectors first. Consequently, it should only be used for vectors. + + Parameters + ---------- + a : array_like + If `a` is complex the complex conjugate is taken before calculation + of the dot product. + b : array_like + Second argument to the dot product. + out : ndarray, optional + Output argument. This must have the exact shape that would be returned + if it was not present. If its dtype is not what would be expected from + this operation, then the result will be (unsafely) cast to `out`. + + Returns + ------- + output : ndarray + Dot product of `a` and `b`. If `out` is given, then it is returned. + + Notes + ----- + The cuPyNumeric implementation is a little more liberal than NumPy in terms + of allowed broadcasting, e.g. ``vdot(ones((1,)), ones((4,)))`` is allowed. + + See Also + -------- + numpy.vdot + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return inner(a.ravel().conj(), b.ravel(), out=out) + + +@add_boilerplate("a", "b") +def outer(a: ndarray, b: ndarray, out: ndarray | None = None) -> ndarray: + """ + Compute the outer product of two vectors. + + Given two vectors, ``a = [a0, a1, ..., aM]`` and ``b = [b0, b1, ..., bN]``, + the outer product is:: + + [[a0*b0 a0*b1 ... a0*bN ] + [a1*b0 . + [ ... . + [aM*b0 aM*bN ]] + + Parameters + ---------- + a : (M,) array_like + First input vector. Input is flattened if not already 1-dimensional. + b : (N,) array_like + Second input vector. Input is flattened if not already 1-dimensional. + out : (M, N) ndarray, optional + A location where the result is stored. If its dtype is not what would + be expected from this operation, then the result will be (unsafely) + cast to `out`. + + Returns + ------- + output : (M, N) ndarray + ``output[i, j] = a[i] * b[j]`` + If `out` is given, then it is returned. + + See Also + -------- + numpy.outer + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return multiply( + a.ravel()[:, np.newaxis], b.ravel()[np.newaxis, :], out=out + ) + + +@add_boilerplate("a", "b") +def tensordot( + a: ndarray, + b: ndarray, + axes: AxesPairLike = 2, + out: ndarray | None = None, +) -> ndarray: + """ + Compute tensor dot product along specified axes. + + Given two tensors, `a` and `b`, and an array_like object containing + two array_like objects, ``(a_axes, b_axes)``, sum the products of + `a`'s and `b`'s elements (components) over the axes specified by + ``a_axes`` and ``b_axes``. The third argument can be a single non-negative + integer_like scalar, ``N``; if it is such, then the last ``N`` dimensions + of `a` and the first ``N`` dimensions of `b` are summed over. + + Parameters + ---------- + a, b : array_like + Tensors to "dot". + + axes : int or array_like + * integer_like + If an int N, sum over the last N axes of `a` and the first N axes + of `b` in order. + * (2,) array_like + Or, a list of axes to be summed over, first sequence applying to `a`, + second to `b`. Both elements array_like must be of the same length. + out : ndarray, optional + Output argument. This must have the exact shape that would be returned + if it was not present. If its dtype is not what would be expected from + this operation, then the result will be (unsafely) cast to `out`. + + Returns + ------- + output : ndarray + The tensor dot product of the inputs. If `out` is given, then it is + returned. + + Notes + ----- + The cuPyNumeric implementation is a little more liberal than NumPy in terms + of allowed broadcasting, e.g. ``tensordot(ones((3,1)), ones((1,4)))`` is + allowed. + + Except for the inner-product case, only floating-point types are supported. + + See Also + -------- + numpy.tensordot + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + (a_modes, b_modes, out_modes) = tensordot_modes(a.ndim, b.ndim, axes) + + return _contract( + a_modes, + b_modes, + out_modes, + a, + b, + out=out, + casting="unsafe", + ) + + +# Trivial multi-tensor contraction strategy: contract in input order +class NullOptimizer(oe.paths.PathOptimizer): # type: ignore [misc,no-any-unimported] # noqa + def __call__( + self, + inputs: list[set[str]], + outputs: set[str], + size_dict: dict[str, int], + memory_limit: int | None = None, + ) -> list[tuple[int, int]]: + return [(0, 1)] + [(0, -1)] * (len(inputs) - 2) + + +def _maybe_cast_input( + arr: ndarray, to_dtype: np.dtype[Any], casting: CastingKind +) -> ndarray: + if arr.dtype == to_dtype: + return arr + if not np.can_cast(arr.dtype, to_dtype, casting=casting): + raise TypeError( + f"Cannot cast input array of type {arr.dtype} to {to_dtype} with " + f"casting rule '{casting}'" + ) + return arr.astype(to_dtype) + + +# Generalized tensor contraction +def _contract( + a_modes: list[str], + b_modes: list[str], + out_modes: list[str], + a: ndarray, + b: ndarray | None = None, + out: ndarray | None = None, + casting: CastingKind = "same_kind", + dtype: np.dtype[Any] | None = None, +) -> ndarray: + # Sanity checks + if len(a_modes) != a.ndim: + raise ValueError( + f"Expected {len(a_modes)}-d input array but got {a.ndim}-d" + ) + + if b is None: + if len(b_modes) != 0: + raise ValueError("Missing input array") + elif len(b_modes) != b.ndim: + raise ValueError( + f"Expected {len(b_modes)}-d input array but got {b.ndim}-d" + ) + + if out is not None and len(out_modes) != out.ndim: + raise ValueError( + f"Expected {len(out_modes)}-d output array but got {out.ndim}-d" + ) + + if len(set(out_modes)) != len(out_modes): + raise ValueError("Duplicate mode labels on output") + + if len(set(out_modes) - set(a_modes) - set(b_modes)) > 0: + raise ValueError("Unknown mode labels on output") + + makes_view = b is None and len(a_modes) == len(out_modes) + if dtype is not None and not makes_view: + c_dtype = dtype + elif out is not None: + c_dtype = out.dtype + elif b is None: + c_dtype = a.dtype + else: + c_dtype = find_common_type(a, b) + + a = _maybe_cast_input(a, c_dtype, casting) + + if b is not None: + b = _maybe_cast_input(b, c_dtype, casting) + + out_dtype = out.dtype if out is not None else c_dtype + + # Handle duplicate modes on inputs + c_a_modes = Counter(a_modes) + for mode, count in c_a_modes.items(): + if count > 1: + axes = [i for (i, m) in enumerate(a_modes) if m == mode] + a = a._diag_helper(axes=axes) + # diagonal is stored on last axis + a_modes = [m for m in a_modes if m != mode] + [mode] + c_b_modes = Counter(b_modes) + for mode, count in c_b_modes.items(): + if count > 1: + axes = [i for (i, m) in enumerate(b_modes) if m == mode] + b = b._diag_helper(axes=axes) # type: ignore [union-attr] + # diagonal is stored on last axis + b_modes = [m for m in b_modes if m != mode] + [mode] + + # Drop modes corresponding to singleton dimensions. This handles cases of + # broadcasting. + for dim in reversed(range(a.ndim)): + if a.shape[dim] == 1: + a = a.squeeze(dim) + a_modes.pop(dim) + if b is not None: + for dim in reversed(range(b.ndim)): + if b.shape[dim] == 1: + b = b.squeeze(dim) + b_modes.pop(dim) + + # Sum-out modes appearing on one argument, and missing from the result + # TODO: If we supported sum on multiple axes we could do the full sum in a + # single operation, and avoid intermediates. + for dim, mode in reversed(list(enumerate(a_modes))): + if mode not in b_modes and mode not in out_modes: + a_modes.pop(dim) + a = a.sum(axis=dim) + + for dim, mode in reversed(list(enumerate(b_modes))): + if mode not in a_modes and mode not in out_modes: + b_modes.pop(dim) + b = b.sum(axis=dim) # type: ignore [union-attr] + + # Compute extent per mode. No need to consider broadcasting at this stage, + # since it has been handled above. + mode2extent: dict[str, int] = {} + for mode, extent in chain( + zip(a_modes, a.shape), zip(b_modes, b.shape) if b is not None else [] + ): + prev_extent = mode2extent.get(mode) + if prev_extent is not None and extent != prev_extent: + raise ValueError( + f"Incompatible sizes between matched dimensions: {extent} vs " + f"{prev_extent}" + ) + mode2extent[mode] = extent + + # Any modes appearing only on the result must have originally been present + # on one of the operands, but got dropped by the broadcast-handling code. + out_shape = ( + out.shape + if out is not None + else tuple(mode2extent.get(mode, 1) for mode in out_modes) + ) + c_modes = [] + c_shape: NdShape = () + c_bloated_shape: NdShape = () + for mode, extent in zip(out_modes, out_shape): + if mode not in a_modes and mode not in b_modes: + c_bloated_shape += (1,) + else: + assert extent > 1 + c_modes.append(mode) + c_shape += (extent,) + c_bloated_shape += (extent,) + + # Verify output array has the right shape (input arrays can be broadcasted + # up to match the output, but not the other way around). There should be no + # unknown or singleton modes on the result at this point. + for mode, extent in zip(c_modes, c_shape): + prev_extent = mode2extent[mode] + assert prev_extent != 1 + if extent != prev_extent: + raise ValueError("Wrong shape on output array") + + # Test for fallback to unary case + if b is not None: + if len(a_modes) == 0: + a = a * b + a_modes = b_modes + b = None + b_modes = [] + elif len(b_modes) == 0: + a = a * b + b = None + + if b is None: + # Unary contraction case + assert len(a_modes) == len(c_modes) and set(a_modes) == set(c_modes) + if len(a_modes) == 0: + # NumPy doesn't return a view in this case + c = copy(a) + elif a_modes == c_modes: + c = a + else: + # Shuffle input array according to mode labels + axes = [a_modes.index(mode) for mode in c_modes] + assert _builtin_all(ax >= 0 for ax in axes) + c = a.transpose(axes) + + else: + # Binary contraction case + # Create result array, if output array can't be directly targeted + if out is not None and out_dtype == c_dtype and out_shape == c_shape: + c = out + else: + c = ndarray( + shape=c_shape, + dtype=c_dtype, + inputs=(a, b), + ) + # Perform operation + c._thunk.contract( + c_modes, + a._thunk, + a_modes, + b._thunk, + b_modes, + mode2extent, + ) + + # Postprocess result before returning + if out is c: + # We already decided above to use the output array directly + return out + if out_dtype != c_dtype or out_shape != c_bloated_shape: + # We need to broadcast the result of the contraction or switch types + # before returning + if not np.can_cast(c_dtype, out_dtype, casting=casting): + raise TypeError( + f"Cannot cast intermediate result array of type {c_dtype} " + f"into output array of type {out_dtype} with casting rule " + f"'{casting}'" + ) + if out is None: + out = ndarray( + shape=out_shape, + dtype=out_dtype, + inputs=(c,), + ) + out[...] = c.reshape(c_bloated_shape) + return out + if out_shape != c_shape: + # We need to add missing dimensions, but they are all of size 1, so + # we don't need to broadcast + assert c_bloated_shape == out_shape + if out is None: + return c.reshape(out_shape) + else: + out[...] = c.reshape(out_shape) + return out + if out is not None: + # The output and result arrays are fully compatible, but we still + # need to copy + out[...] = c + return out + return c + + +def einsum( + expr: str, + *operands: ndarray, + out: ndarray | None = None, + dtype: np.dtype[Any] | None = None, + casting: CastingKind = "safe", + optimize: bool | Literal["greedy", "optimal"] = True, +) -> ndarray: + """ + Evaluates the Einstein summation convention on the operands. + + Using the Einstein summation convention, many common multi-dimensional, + linear algebraic array operations can be represented in a simple fashion. + In *implicit* mode `einsum` computes these values. + + In *explicit* mode, `einsum` provides further flexibility to compute + other array operations that might not be considered classical Einstein + summation operations, by disabling, or forcing summation over specified + subscript labels. + + Parameters + ---------- + subscripts : str + Specifies the subscripts for summation as comma separated list of + subscript labels. An implicit (classical Einstein summation) + calculation is performed unless the explicit indicator '->' is + included as well as subscript labels of the precise output form. + operands : list[array_like] + These are the arrays for the operation. + out : ndarray, optional + If provided, the calculation is done into this array. + dtype : data-type, optional + If provided, forces the calculation to use the data type specified. + Note that you may have to also give a more liberal `casting` + parameter to allow the conversions. Default is None. + casting : ``{'no', 'equiv', 'safe', 'same_kind', 'unsafe'}``, optional + Controls what kind of data casting may occur. + + * 'no' means the data types should not be cast at all. + * 'equiv' means only byte-order changes are allowed. + * 'safe' means only casts which can preserve values are allowed. + * 'same_kind' means only safe casts or casts within a kind, + like float64 to float32, are allowed. + * 'unsafe' means any data conversions may be done. + + Default is 'safe'. + optimize : ``{False, True, 'greedy', 'optimal'}``, optional + Controls if intermediate optimization should occur. If False then + arrays will be contracted in input order, one at a time. True (the + default) will use the 'greedy' algorithm. See + ``cupynumeric.einsum_path`` for more information on the available + optimization algorithms. + + Returns + ------- + output : ndarray + The calculation based on the Einstein summation convention. + + Notes + ----- + For most expressions, only floating-point types are supported. + + See Also + -------- + numpy.einsum + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + operands_list = [convert_to_cupynumeric_ndarray(op) for op in operands] + + if out is not None: + out = convert_to_cupynumeric_ndarray(out, share=True) + + if optimize is True: + optimize = "greedy" + elif optimize is False: + optimize = NullOptimizer() + + # This call normalizes the expression (adds the output part if it's + # missing, expands '...') and checks for some errors (mismatch on number + # of dimensions between operand and expression, wrong number of operands, + # unknown modes on output, a mode appearing under two different + # non-singleton extents). + computed_operands, contractions = oe.contract_path( + expr, *operands_list, einsum_call=True, optimize=optimize + ) + for indices, _, sub_expr, _, _ in contractions: + assert len(indices) == 1 or len(indices) == 2 + a = computed_operands.pop(indices[0]) + b = computed_operands.pop(indices[1]) if len(indices) == 2 else None + if b is None: + m = re.match(r"([a-zA-Z]*)->([a-zA-Z]*)", sub_expr) + if m is None: + raise NotImplementedError("Non-alphabetic mode labels") + a_modes = list(m.group(1)) + b_modes = [] + out_modes = list(m.group(2)) + else: + m = re.match(r"([a-zA-Z]*),([a-zA-Z]*)->([a-zA-Z]*)", sub_expr) + if m is None: + raise NotImplementedError("Non-alphabetic mode labels") + a_modes = list(m.group(1)) + b_modes = list(m.group(2)) + out_modes = list(m.group(3)) + sub_result = _contract( + a_modes, + b_modes, + out_modes, + a, + b, + out=(out if len(computed_operands) == 0 else None), + casting=casting, + dtype=dtype, + ) + computed_operands.append(sub_result) + + assert len(computed_operands) == 1 + return computed_operands[0] + + +def einsum_path( + expr: str, + *operands: ndarray, + optimize: bool | list[Any] | tuple[Any, ...] | str = "greedy", +) -> tuple[list[str | int], str]: + """ + Evaluates the lowest cost contraction order for an einsum expression by + considering the creation of intermediate arrays. + + Parameters + ---------- + expr : str + Specifies the subscripts for summation. + *operands : Sequence[array_like] + These are the arrays for the operation. + optimize : ``{bool, list, tuple, 'greedy', 'optimal'}`` + Choose the type of path. If a tuple is provided, the second argument is + assumed to be the maximum intermediate size created. If only a single + argument is provided the largest input or output array size is used + as a maximum intermediate size. + + * if a list is given that starts with ``einsum_path``, uses this as the + contraction path + * if False no optimization is taken + * if True defaults to the 'greedy' algorithm + * 'optimal' An algorithm that combinatorially explores all possible + ways of contracting the listed tensors and chooses the least costly + path. Scales exponentially with the number of terms in the + contraction. + * 'greedy' An algorithm that chooses the best pair contraction + at each step. Effectively, this algorithm searches the largest inner, + Hadamard, and then outer products at each step. Scales cubically with + the number of terms in the contraction. Equivalent to the 'optimal' + path for most contractions. + + Default is 'greedy'. + + Returns + ------- + path : list[tuple[int,...]] + A list representation of the einsum path. + string_repr : str + A printable representation of the einsum path. + + Notes + ----- + The resulting path indicates which terms of the input contraction should be + contracted first, the result of this contraction is then appended to the + end of the contraction list. This list can then be iterated over until all + intermediate contractions are complete. + + See Also + -------- + numpy.einsum_path + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + computed_operands = [convert_to_cupynumeric_ndarray(op) for op in operands] + memory_limit = _builtin_max(op.size for op in computed_operands) + if isinstance(optimize, tuple): + if len(optimize) != 2: + raise ValueError("einsum_path expects optimize tuples of size 2") + optimize, memory_limit = optimize + if optimize is True: + optimize = "greedy" + elif optimize is False: + optimize = [tuple(range(len(computed_operands)))] + elif optimize in ["greedy", "optimal"]: + pass + elif ( + isinstance(optimize, list) + and len(optimize) > 1 + and optimize[0] == "einsum_path" + ): + optimize = optimize[1:] + else: + raise ValueError( + f"einsum_path: unexpected value for optimize: {optimize}" + ) + path, info = oe.contract_path( + expr, *computed_operands, optimize=optimize, memory_limit=memory_limit + ) + return ["einsum_path"] + path, info + + +@add_boilerplate("a") +def trace( + a: ndarray, + offset: int = 0, + axis1: int | None = None, + axis2: int | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return the sum along diagonals of the array. + + If a is 2-D, the sum along its diagonal with the given offset is + returned, i.e., the sum of elements a[i,i+offset] for all i. + If a has more than two dimensions, then the axes specified by axis1 + and axis2 are used to determine the 2-D sub-arrays whose traces + are returned. The shape of the resulting array is the same as that + of a with axis1 and axis2 removed. + + Parameters + ---------- + a : array_like + Input array, from which the diagonals are taken. + offset : int, optional + Offset of the diagonal from the main diagonal. Can be both + positive and negative. Defaults to 0. + axis1, axis2 : int, optional + Axes to be used as the first and second axis of the 2-D sub-arrays + from which the diagonals should be taken. Defaults are the + first two axes of a. + dtype : data-type, optional + Determines the data-type of the returned array and of the + accumulator where the elements are summed. If dtype has the value + None and a is of integer type of precision less than the default + integer precision, then the default integer precision is used. + Otherwise, the precision is the same as that of a. + + out : ndarray, optional + Array into which the output is placed. Its type is preserved and + it must be of the right shape to hold the output. + + Returns + ------- + sum_along_diagonals : ndarray + If a is 2-D, the sum along the diagonal is returned. If a has + larger dimensions, then an array of sums along diagonals is returned. + + Raises + ------ + ValueError + If the dimension of `a` is less than 2. + + See Also + -------- + numpy.diagonal + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.trace( + offset=offset, axis1=axis1, axis2=axis2, dtype=dtype, out=out + ) diff --git a/cupynumeric/_module/logic_array_contents.py b/cupynumeric/_module/logic_array_contents.py new file mode 100644 index 000000000..e5bb9bd9e --- /dev/null +++ b/cupynumeric/_module/logic_array_contents.py @@ -0,0 +1,114 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import convert_to_cupynumeric_ndarray +from .._ufunc.comparison import logical_and +from .._ufunc.floating import isinf, signbit + +if TYPE_CHECKING: + from .._array.array import ndarray + + +def isneginf(x: ndarray, out: ndarray | None = None) -> ndarray: + """ + + Test element-wise for negative infinity, return result as bool array. + + Parameters + ---------- + x : array_like + The input array. + out : array_like, optional + A location into which the result is stored. If provided, it must have a + shape that the input broadcasts to. If not provided or None, a + freshly-allocated boolean array is returned. + + Returns + ------- + out : ndarray + A boolean array with the same dimensions as the input. + If second argument is not supplied then a numpy boolean array is + returned with values True where the corresponding element of the + input is negative infinity and values False where the element of + the input is not negative infinity. + + If a second argument is supplied the result is stored there. If the + type of that array is a numeric type the result is represented as + zeros and ones, if the type is boolean then as False and True. The + return value `out` is then a reference to that array. + + See Also + -------- + numpy.isneginf + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + x = convert_to_cupynumeric_ndarray(x) + if out is not None: + out = convert_to_cupynumeric_ndarray(out, share=True) + rhs1 = isinf(x) + rhs2 = signbit(x) + return logical_and(rhs1, rhs2, out=out) + + +def isposinf(x: ndarray, out: ndarray | None = None) -> ndarray: + """ + + Test element-wise for positive infinity, return result as bool array. + + Parameters + ---------- + x : array_like + The input array. + out : array_like, optional + A location into which the result is stored. If provided, it must have a + shape that the input broadcasts to. If not provided or None, a + freshly-allocated boolean array is returned. + + Returns + ------- + out : ndarray + A boolean array with the same dimensions as the input. + If second argument is not supplied then a boolean array is returned + with values True where the corresponding element of the input is + positive infinity and values False where the element of the input is + not positive infinity. + + If a second argument is supplied the result is stored there. If the + type of that array is a numeric type the result is represented as zeros + and ones, if the type is boolean then as False and True. + The return value `out` is then a reference to that array. + + See Also + -------- + numpy.isposinf + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + x = convert_to_cupynumeric_ndarray(x) + if out is not None: + out = convert_to_cupynumeric_ndarray(out, share=True) + rhs1 = isinf(x) + rhs2 = ~signbit(x) + return logical_and(rhs1, rhs2, out=out) diff --git a/cunumeric/logic.py b/cupynumeric/_module/logic_array_type.py similarity index 51% rename from cunumeric/logic.py rename to cupynumeric/_module/logic_array_type.py index 667ae1d13..1e39754a7 100644 --- a/cunumeric/logic.py +++ b/cupynumeric/_module/logic_array_type.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,110 +14,19 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any import numpy as np -from ._ufunc.comparison import logical_and -from ._ufunc.floating import isinf, signbit -from .array import convert_to_cunumeric_ndarray, ndarray -from .module import full +from .._array.array import ndarray +from .._array.util import convert_to_cupynumeric_ndarray +from .creation_shape import full if TYPE_CHECKING: import numpy.typing as npt -def isneginf(x: ndarray, out: Union[ndarray, None] = None) -> ndarray: - """ - - Test element-wise for negative infinity, return result as bool array. - - Parameters - ---------- - x : array_like - The input array. - out : array_like, optional - A location into which the result is stored. If provided, it must have a - shape that the input broadcasts to. If not provided or None, a - freshly-allocated boolean array is returned. - - Returns - ------- - out : ndarray - A boolean array with the same dimensions as the input. - If second argument is not supplied then a numpy boolean array is - returned with values True where the corresponding element of the - input is negative infinity and values False where the element of - the input is not negative infinity. - - If a second argument is supplied the result is stored there. If the - type of that array is a numeric type the result is represented as - zeros and ones, if the type is boolean then as False and True. The - return value `out` is then a reference to that array. - - See Also - -------- - numpy.isneginf - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - x = convert_to_cunumeric_ndarray(x) - if out is not None: - out = convert_to_cunumeric_ndarray(out, share=True) - rhs1 = isinf(x) - rhs2 = signbit(x) - return logical_and(rhs1, rhs2, out=out) - - -def isposinf(x: ndarray, out: Union[ndarray, None] = None) -> ndarray: - """ - - Test element-wise for positive infinity, return result as bool array. - - Parameters - ---------- - x : array_like - The input array. - out : array_like, optional - A location into which the result is stored. If provided, it must have a - shape that the input broadcasts to. If not provided or None, a - freshly-allocated boolean array is returned. - - Returns - ------- - out : ndarray - A boolean array with the same dimensions as the input. - If second argument is not supplied then a boolean array is returned - with values True where the corresponding element of the input is - positive infinity and values False where the element of the input is - not positive infinity. - - If a second argument is supplied the result is stored there. If the - type of that array is a numeric type the result is represented as zeros - and ones, if the type is boolean then as False and True. - The return value `out` is then a reference to that array. - - See Also - -------- - numpy.isposinf - - Availability - -------- - Multiple GPUs, Multiple CPUs - - """ - x = convert_to_cunumeric_ndarray(x) - if out is not None: - out = convert_to_cunumeric_ndarray(out, share=True) - rhs1 = isinf(x) - rhs2 = ~signbit(x) - return logical_and(rhs1, rhs2, out=out) - - -def iscomplex(x: Union[ndarray, npt.NDArray[Any]]) -> ndarray: +def iscomplex(x: ndarray | npt.NDArray[Any]) -> ndarray: """ Returns a bool array, where True if input element is complex. @@ -144,14 +53,14 @@ def iscomplex(x: Union[ndarray, npt.NDArray[Any]]) -> ndarray: Multiple GPUs, Multiple CPUs """ - x = convert_to_cunumeric_ndarray(x) + x = convert_to_cupynumeric_ndarray(x) if x.dtype.kind != "c": return full(x.shape, False, dtype=bool) else: return x.imag != 0 -def iscomplexobj(x: Union[ndarray, npt.NDArray[Any]]) -> bool: +def iscomplexobj(x: ndarray | npt.NDArray[Any]) -> bool: """ Check for a complex type or an array of complex numbers. @@ -184,7 +93,7 @@ def iscomplexobj(x: Union[ndarray, npt.NDArray[Any]]) -> bool: return np.iscomplexobj(x) -def isreal(x: Union[ndarray, npt.NDArray[Any]]) -> ndarray: +def isreal(x: ndarray | npt.NDArray[Any]) -> ndarray: """ Returns a bool array, where True if input element is real. @@ -212,7 +121,7 @@ def isreal(x: Union[ndarray, npt.NDArray[Any]]) -> ndarray: Multiple GPUs, Multiple CPUs """ - x = convert_to_cunumeric_ndarray(x) + x = convert_to_cupynumeric_ndarray(x) if x.dtype.kind != "c": return full(x.shape, True, dtype=bool) else: @@ -249,7 +158,7 @@ def isrealobj(x: ndarray) -> bool: return not iscomplexobj(x) -def isscalar(x: Union[ndarray, npt.NDArray[Any]]) -> bool: +def isscalar(x: ndarray | npt.NDArray[Any]) -> bool: """ Returns True if the type of `element` is a scalar type. @@ -270,7 +179,7 @@ def isscalar(x: Union[ndarray, npt.NDArray[Any]]) -> bool: Notes ----- - This function falls back to NumPy for all object types but cuNumeric's + This function falls back to NumPy for all object types but cuPyNumeric's ndarray, which always returns `False`. Availability @@ -278,9 +187,9 @@ def isscalar(x: Union[ndarray, npt.NDArray[Any]]) -> bool: Multiple GPUs, Multiple CPUs """ - # Since the input can be any value, we can't just convert it to cunumeric - # ndarray. Instead we check if the input is cunumeric ndarray and, if not, - # fall back to Numpy + # Since the input can be any value, we can't just convert it to cupynumeric + # ndarray. Instead we check if the input is cupynumeric ndarray and, if + # not, fall back to Numpy if isinstance(x, ndarray): return False else: diff --git a/cupynumeric/_module/logic_comparison.py b/cupynumeric/_module/logic_comparison.py new file mode 100644 index 000000000..46c6410a4 --- /dev/null +++ b/cupynumeric/_module/logic_comparison.py @@ -0,0 +1,201 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from legate.core import Scalar, types as ty + +from .._array.thunk import perform_binary_reduction +from .._array.util import add_boilerplate, find_common_type +from ..config import BinaryOpCode +from .creation_shape import empty + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a", "b") +def allclose( + a: ndarray, + b: ndarray, + rtol: float = 1e-5, + atol: float = 1e-8, + equal_nan: bool = False, +) -> ndarray: + """ + + Returns True if two arrays are element-wise equal within a tolerance. + + The tolerance values are positive, typically very small numbers. The + relative difference (`rtol` * abs(`b`)) and the absolute difference + `atol` are added together to compare against the absolute difference + between `a` and `b`. + + NaNs are treated as equal if they are in the same place and if + ``equal_nan=True``. Infs are treated as equal if they are in the same + place and of the same sign in both arrays. + + Parameters + ---------- + a, b : array_like + Input arrays to compare. + rtol : float + The relative tolerance parameter (see Notes). + atol : float + The absolute tolerance parameter (see Notes). + equal_nan : bool + Whether to compare NaN's as equal. If True, NaN's in `a` will be + considered equal to NaN's in `b` in the output array. + + Returns + ------- + allclose : ndarray scalar + Returns True if the two arrays are equal within the given + tolerance; False otherwise. + + Notes + ----- + If the following equation is element-wise True, then allclose returns + True. + + absolute(`a` - `b`) <= (`atol` + `rtol` * absolute(`b`)) + + See Also + -------- + numpy.allclose + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if equal_nan: + raise NotImplementedError( + "cuPyNumeric does not support `equal_nan` yet for allclose" + ) + args = (Scalar(rtol, ty.float64), Scalar(atol, ty.float64)) + return perform_binary_reduction( + BinaryOpCode.ISCLOSE, + a, + b, + dtype=np.dtype(bool), + extra_args=args, + ) + + +@add_boilerplate("a", "b") +def isclose( + a: ndarray, + b: ndarray, + rtol: float = 1e-5, + atol: float = 1e-8, + equal_nan: bool = False, +) -> ndarray: + """ + + Returns a boolean array where two arrays are element-wise equal within a + tolerance. + + Parameters + ---------- + a, b : array_like + Input arrays to compare. + rtol : float + The relative tolerance parameter (see Notes). + atol : float + The absolute tolerance parameter (see Notes). + equal_nan : bool + Whether to compare NaN's as equal. If True, NaN's in `a` will be + considered equal to NaN's in `b` in the output array. + + Returns + ------- + y : array_like + Returns a boolean array of where `a` and `b` are equal within the + given tolerance. If both `a` and `b` are scalars, returns a single + boolean value. + + Notes + ----- + For finite values, isclose uses the following equation to test whether + two floating point values are equivalent. + + absolute(`a` - `b`) <= (`atol` + `rtol` * absolute(`b`)) + + See Also + -------- + numpy.isclose + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if equal_nan: + raise NotImplementedError( + "cuPyNumeric does not support `equal_nan` yet for isclose" + ) + + out_shape = np.broadcast_shapes(a.shape, b.shape) + out = empty(out_shape, dtype=bool) + + common_type = find_common_type(a, b) + a = a.astype(common_type) + b = b.astype(common_type) + + out._thunk.isclose(a._thunk, b._thunk, rtol, atol, equal_nan) + return out + + +@add_boilerplate("a1", "a2") +def array_equal( + a1: ndarray, a2: ndarray, equal_nan: bool = False +) -> bool | ndarray: + """ + + True if two arrays have the same shape and elements, False otherwise. + + Parameters + ---------- + a1, a2 : array_like + Input arrays. + equal_nan : bool + Whether to compare NaN's as equal. If the dtype of a1 and a2 is + complex, values will be considered equal if either the real or the + imaginary component of a given value is ``nan``. + + Returns + ------- + b : ndarray scalar + Returns True if the arrays are equal. + + See Also + -------- + numpy.array_equal + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if equal_nan: + raise NotImplementedError( + "cuPyNumeric does not support `equal_nan` yet for `array_equal`" + ) + + if a1.shape != a2.shape: + return False + return perform_binary_reduction( + BinaryOpCode.EQUAL, a1, a2, dtype=np.dtype(bool) + ) diff --git a/cupynumeric/_module/logic_truth.py b/cupynumeric/_module/logic_truth.py new file mode 100644 index 000000000..89ca0244d --- /dev/null +++ b/cupynumeric/_module/logic_truth.py @@ -0,0 +1,138 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import add_boilerplate + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a") +def all( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + where: ndarray | None = None, +) -> ndarray: + """ + Test whether all array elements along a given axis evaluate to True. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + axis : None or int or tuple[int], optional + Axis or axes along which a logical AND reduction is performed. + The default (``axis=None``) is to perform a logical AND over all + the dimensions of the input array. `axis` may be negative, in + which case it counts from the last to the first axis. + + If this is a tuple of ints, a reduction is performed on multiple + axes, instead of a single axis or all the axes as before. + out : ndarray, optional + Alternate output array in which to place the result. + It must have the same shape as the expected output and its + type is preserved (e.g., if ``dtype(out)`` is float, the result + will consist of 0.0's and 1.0's). + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `all` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + Returns + ------- + all : ndarray, bool + A new boolean or array is returned unless `out` is specified, + in which case a reference to `out` is returned. + + See Also + -------- + numpy.all + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.all(axis=axis, out=out, keepdims=keepdims, where=where) + + +@add_boilerplate("a") +def any( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + where: ndarray | None = None, +) -> ndarray: + """ + Test whether any array element along a given axis evaluates to True. + + Returns single boolean unless `axis` is not ``None`` + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + axis : None or int or tuple[int], optional + Axis or axes along which a logical OR reduction is performed. + The default (``axis=None``) is to perform a logical OR over all + the dimensions of the input array. `axis` may be negative, in + which case it counts from the last to the first axis. + + If this is a tuple of ints, a reduction is performed on multiple + axes, instead of a single axis or all the axes as before. + out : ndarray, optional + Alternate output array in which to place the result. It must have + the same shape as the expected output and its type is preserved + (e.g., if it is of type float, then it will remain so, returning + 1.0 for True and 0.0 for False, regardless of the type of `a`). + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `any` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + Returns + ------- + any : bool or ndarray + A new boolean or `ndarray` is returned unless `out` is specified, + in which case a reference to `out` is returned. + + See Also + -------- + numpy.any + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.any(axis=axis, out=out, keepdims=keepdims, where=where) diff --git a/cupynumeric/_module/math_complex.py b/cupynumeric/_module/math_complex.py new file mode 100644 index 000000000..29f3787f7 --- /dev/null +++ b/cupynumeric/_module/math_complex.py @@ -0,0 +1,122 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from legate.core import Scalar + +from .._array.thunk import perform_unary_op +from .._array.util import add_boilerplate +from ..config import UnaryOpCode + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("val") +def real(val: ndarray) -> ndarray: + """ + Return the real part of the complex argument. + + Parameters + ---------- + val : array_like + Input array. + + Returns + ------- + out : ndarray or scalar + The real component of the complex argument. If `val` is real, the type + of `val` is used for the output. If `val` has complex elements, the + returned type is float. + + See Also + -------- + numpy.real + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return val.real + + +@add_boilerplate("val") +def imag(val: ndarray) -> ndarray: + """ + + Return the imaginary part of the complex argument. + + Parameters + ---------- + val : array_like + Input array. + + Returns + ------- + out : ndarray or scalar + The imaginary component of the complex argument. If `val` is real, + the type of `val` is used for the output. If `val` has complex + elements, the returned type is float. + + See Also + -------- + numpy.imag + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return val.imag + + +@add_boilerplate("z") +def angle(z: ndarray, deg: bool = False) -> ndarray: + """ + Return the angle of the complex argument. + + Parameters + ---------- + z : array_like + A complex number or sequence of complex numbers. + deg : bool, optional + Return angle in degrees if True, radians if False (default). + + Returns + ------- + angle : ndarray or scalar + The counterclockwise angle from the positive real axis on the complex + plane in the range ``(-pi, pi]``, with dtype as numpy.float64. + + See Also + -------- + numpy.angle + + Notes + ----- + This function passes the imaginary and real parts of the argument to + `arctan2` to compute the result; consequently, it follows the convention + of `arctan2` when the magnitude of the argument is zero. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if z is None: + raise TypeError("can't compute 'angle' for None") + extra_args = (Scalar(deg),) + return perform_unary_op(UnaryOpCode.ANGLE, z, extra_args=extra_args) diff --git a/cupynumeric/_module/math_extrema.py b/cupynumeric/_module/math_extrema.py new file mode 100644 index 000000000..0b576684d --- /dev/null +++ b/cupynumeric/_module/math_extrema.py @@ -0,0 +1,179 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.util import add_boilerplate +from .._ufunc.comparison import maximum, minimum + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a") +def amax( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + + Return the maximum of an array or maximum along an axis. + + Parameters + ---------- + a : array_like + Input data. + axis : None or int or tuple[int], optional + Axis or axes along which to operate. By default, flattened input is + used. + + If this is a tuple of ints, the maximum is selected over multiple axes, + instead of a single axis or all the axes as before. + out : ndarray, optional + Alternative output array in which to place the result. Must + be of the same shape and buffer length as the expected output. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `amax` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + initial : scalar, optional + The minimum value of an output element. Must be present to allow + computation on empty slice. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to compare for the maximum. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + amax : ndarray or scalar + Maximum of `a`. If `axis` is None, the result is a scalar value. + If `axis` is given, the result is an array of dimension + ``a.ndim - 1``. + + See Also + -------- + numpy.amax + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return maximum.reduce( + a, + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +max = amax + + +@add_boilerplate("a") +def amin( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + + Return the minimum of an array or minimum along an axis. + + Parameters + ---------- + a : array_like + Input data. + axis : None or int or tuple[int], optional + Axis or axes along which to operate. By default, flattened input is + used. + + If this is a tuple of ints, the minimum is selected over multiple axes, + instead of a single axis or all the axes as before. + out : ndarray, optional + Alternative output array in which to place the result. Must + be of the same shape and buffer length as the expected output. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `amin` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + initial : scalar, optional + The maximum value of an output element. Must be present to allow + computation on empty slice. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to compare for the minimum. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + amin : ndarray or scalar + Minimum of `a`. If `axis` is None, the result is a scalar value. + If `axis` is given, the result is an array of dimension + ``a.ndim - 1``. + + See Also + -------- + numpy.amin + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return minimum.reduce( + a, + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +min = amin diff --git a/cupynumeric/_module/math_misc.py b/cupynumeric/_module/math_misc.py new file mode 100644 index 000000000..a91e3facc --- /dev/null +++ b/cupynumeric/_module/math_misc.py @@ -0,0 +1,174 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from ..config import ConvolveMethod + +if TYPE_CHECKING: + import numpy.typing as npt + + from ..types import ConvolveMethod as ConvolveMethodType, ConvolveMode + + +@add_boilerplate("a", "v") +def convolve( + a: ndarray, + v: ndarray, + mode: ConvolveMode = "full", + method: ConvolveMethodType = "auto", +) -> ndarray: + """ + + Returns the discrete, linear convolution of two ndarrays. + + If `a` and `v` are both 1-D and `v` is longer than `a`, the two are + swapped before computation. For N-D cases, the arguments are never swapped. + + Parameters + ---------- + a : (N,) array_like + First input ndarray. + v : (M,) array_like + Second input ndarray. + mode : ``{'full', 'valid', 'same'}``, optional + 'same': + The output is the same size as `a`, centered with respect to + the 'full' output. (default) + + 'full': + The output is the full discrete linear convolution of the inputs. + + 'valid': + The output consists only of those elements that do not + rely on the zero-padding. In 'valid' mode, either `a` or `v` + must be at least as large as the other in every dimension. + method : ``{'auto', 'direct', 'fft'}``, optional + A string indicating which method to use to calculate the convolution. + + 'auto': + Automatically chooses direct or Fourier method based on an estimate of + which is faster (default) + + 'direct': + The convolution is determined directly from sums, the definition of + convolution + + 'fft': + The Fourier Transform is used to perform the convolution + + Returns + ------- + out : ndarray + Discrete, linear convolution of `a` and `v`. + + See Also + -------- + numpy.convolve + + Notes + ----- + The current implementation only supports the 'same' mode. + + Unlike `numpy.convolve`, `cupynumeric.convolve` supports N-dimensional + inputs, but it follows NumPy's behavior for 1-D inputs. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if mode != "same": + raise NotImplementedError("Only support mode='same'") + + if a.ndim != v.ndim: + raise RuntimeError("Arrays should have the same dimensions") + elif a.ndim > 3: + raise NotImplementedError(f"{a.ndim}-D arrays are not yet supported") + + if a.ndim == 1 and a.size < v.size: + v, a = a, v + + if not hasattr(ConvolveMethod, method.upper()): + raise ValueError( + "Acceptable method flags are 'auto', 'direct', or 'fft'." + ) + + if a.dtype != v.dtype: + v = v.astype(a.dtype) + out = ndarray( + shape=a.shape, + dtype=a.dtype, + inputs=(a, v), + ) + out._thunk.convolve(a._thunk, v._thunk, mode, method) + return out + + +@add_boilerplate("a") +def clip( + a: ndarray, + a_min: int | float | npt.ArrayLike | None, + a_max: int | float | npt.ArrayLike | None, + out: ndarray | None = None, +) -> ndarray: + """ + + Clip (limit) the values in an array. + + Given an interval, values outside the interval are clipped to + the interval edges. For example, if an interval of ``[0, 1]`` + is specified, values smaller than 0 become 0, and values larger + than 1 become 1. + + Parameters + ---------- + a : array_like + Array containing elements to clip. + a_min : scalar or array_like or None + Minimum value. If None, clipping is not performed on lower + interval edge. Not more than one of `a_min` and `a_max` may be + None. + a_max : scalar or array_like or None + Maximum value. If None, clipping is not performed on upper + interval edge. Not more than one of `a_min` and `a_max` may be + None. If `a_min` or `a_max` are array_like, then the three + arrays will be broadcasted to match their shapes. + out : ndarray, optional + The results will be placed in this array. It may be the input + array for in-place clipping. `out` must be of the right shape + to hold the output. Its type is preserved. + **kwargs + For other keyword-only arguments, see the + :ref:`ufunc docs `. + + Returns + ------- + clipped_array : ndarray + An array with the elements of `a`, but where values + < `a_min` are replaced with `a_min`, and those > `a_max` + with `a_max`. + + See Also + -------- + numpy.clip + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.clip(a_min, a_max, out=out) diff --git a/cupynumeric/_module/math_rounding.py b/cupynumeric/_module/math_rounding.py new file mode 100644 index 000000000..15b754fa1 --- /dev/null +++ b/cupynumeric/_module/math_rounding.py @@ -0,0 +1,57 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from .._array.array import ndarray +from .._array.util import add_boilerplate + + +@add_boilerplate("a") +def round( + a: ndarray, + decimals: int = 0, + out: ndarray | None = None, +) -> ndarray: + """ + Evenly round to the given number of decimals. + + Parameters + ---------- + a : array_like + Input data. + decimals : int, optional + Number of decimal places to round to (default: 0). If + decimals is negative, it specifies the number of positions to + the left of the decimal point. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output, but the type of the output + values will be cast if necessary. + + Returns + ------- + rounded_array : ndarray + An array of the same type as `a`, containing the rounded values. + Unless `out` was specified, a new array is created. A reference to + the result is returned. + + The real and imaginary parts of complex numbers are rounded + separately. The result of rounding a float is a float. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.round(decimals, out=out) diff --git a/cupynumeric/_module/math_sum_prod_diff.py b/cupynumeric/_module/math_sum_prod_diff.py new file mode 100644 index 000000000..6027fb1c1 --- /dev/null +++ b/cupynumeric/_module/math_sum_prod_diff.py @@ -0,0 +1,1338 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import collections.abc +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.thunk import perform_scan, perform_unary_reduction +from .._array.util import add_boilerplate +from .._ufunc.comparison import not_equal +from .._ufunc.floating import isnan +from .._ufunc.math import add, multiply, subtract +from .._utils import is_np2 +from ..config import ScanCode, UnaryRedCode +from ..settings import settings as cupynumeric_settings +from ._unary_red_utils import get_non_nan_unary_red_code +from .array_dimension import broadcast_to +from .array_joining import concatenate +from .creation_data import asarray +from .creation_shape import empty, empty_like +from .indexing import putmask +from .logic_truth import all, any + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a") +def prod( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + + Return the product of array elements over a given axis. + + Parameters + ---------- + a : array_like + Input data. + axis : None or int or tuple[int], optional + Axis or axes along which a product is performed. The default, + axis=None, will calculate the product of all the elements in the + input array. If axis is negative it counts from the last to the + first axis. + + If axis is a tuple of ints, a product is performed on all of the + axes specified in the tuple instead of a single axis or all the + axes as before. + dtype : data-type, optional + The type of the returned array, as well as of the accumulator in + which the elements are multiplied. The dtype of `a` is used by + default unless `a` has an integer dtype of less precision than the + default platform integer. In that case, if `a` is signed then the + platform integer is used while if `a` is unsigned then an unsigned + integer of the same precision as the platform integer is used. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output, but the type of the output + values will be cast if necessary. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in the + result as dimensions with size one. With this option, the result + will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `prod` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + initial : scalar, optional + The starting value for this product. See `~cupynumeric.ufunc.reduce` + for details. + + where : array_like[bool], optional + Elements to include in the product. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + product_along_axis : ndarray, see `dtype` parameter above. + An array shaped as `a` but with the specified axis removed. + Returns a reference to `out` if specified. + + See Also + -------- + numpy.prod + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if isinstance(initial, list): + exc = TypeError if is_np2 else ValueError # type: ignore [unreachable] + raise exc("initial should not be a list") + + return multiply.reduce( + a, + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +@add_boilerplate("a") +def sum( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + + Sum of array elements over a given axis. + + Parameters + ---------- + a : array_like + Elements to sum. + axis : None or int or tuple[int], optional + Axis or axes along which a sum is performed. The default, + axis=None, will sum all of the elements of the input array. If + axis is negative it counts from the last to the first axis. + + If axis is a tuple of ints, a sum is performed on all of the axes + specified in the tuple instead of a single axis or all the axes as + before. + dtype : data-type, optional + The type of the returned array and of the accumulator in which the + elements are summed. The dtype of `a` is used by default unless `a` + has an integer dtype of less precision than the default platform + integer. In that case, if `a` is signed then the platform integer + is used while if `a` is unsigned then an unsigned integer of the + same precision as the platform integer is used. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output, but the type of the output + values will be cast if necessary. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `sum` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + initial : scalar, optional + Starting value for the sum. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to include in the sum. See `~cupynumeric.ufunc.reduce` for + details. + + Returns + ------- + sum_along_axis : ndarray + An array with the same shape as `a`, with the specified + axis removed. If `a` is a 0-d array, or if `axis` is None, a scalar + is returned. If an output array is specified, a reference to + `out` is returned. + + See Also + -------- + numpy.sum + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return add.reduce( + a, + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +@add_boilerplate("a") +def cumprod( + a: ndarray, + axis: int | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return the cumulative product of the elements along a given axis. + + Parameters + ---------- + a : array_like + Input array. + + axis : int, optional + Axis along which the cumulative product is computed. The default (None) + is to compute the cumprod over the flattened array. + + dtype : dtype, optional + Type of the returned array and of the accumulator in which the elements + are multiplied. If dtype is not specified, it defaults to the dtype of + a, unless a has an integer dtype with a precision less than that of the + default platform integer. In that case, the default platform integer is + used. + out : ndarray, optional + Alternative output array in which to place the result. It must have the + same shape and buffer length as the expected output but the type will + be cast if necessary. See Output type determination for more details. + + Returns + ------- + cumprod : ndarray + A new array holding the result is returned unless out is specified, in + which case a reference to out is returned. The result has the same size + as a, and the same shape as a if axis is not None or a is a 1-d array. + + See Also + -------- + numpy.cumprod + + Notes + ----- + cuPyNumeric's parallel implementation may yield different results from + NumPy with floating point and complex types. For example, when boundary + values such as inf occur they may not propagate as expected. Consider the + float32 array ``[3e+37, 1, 100, 0.01]``. NumPy's cumprod will return a + result ofc``[3e+37, 3e+37, inf, inf]``. However, cuPyNumeric might + internally partition the array such that partition 0 has ``[3e+37, 1]`` + and partition 1 has ``[100, 0.01]``, returning the result + ``[3e+37, 3e+37, inf, 3e+37]``. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return perform_scan( + ScanCode.PROD, + a, + axis=axis, + dtype=dtype, + out=out, + nan_to_identity=False, + ) + + +@add_boilerplate("a") +def cumsum( + a: ndarray, + axis: int | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return the cumulative sum of the elements along a given axis. + + Parameters + ---------- + a : array_like + Input array. + + axis : int, optional + Axis along which the cumulative sum is computed. The default (None) is + to compute the cumsum over the flattened array. + + dtype : dtype, optional + Type of the returned array and of the accumulator in which the elements + are summed. If dtype is not specified, it defaults to the dtype of a, + unless a has an integer dtype with a precision less than that of the + default platform integer. In that case, the default platform integer is + used. + out : ndarray, optional + Alternative output array in which to place the result. It must have the + same shape and buffer length as the expected output but the type will + be cast if necessary. See Output type determination for more details. + + Returns + ------- + cumsum : ndarray. + A new array holding the result is returned unless out is specified, in + which case a reference to out is returned. The result has the same size + as a, and the same shape as a if axis is not None or a is a 1-d array. + + See Also + -------- + numpy.cumsum + + Notes + ----- + CuPyNumeric's parallel implementation may yield different results from + NumPy with floating point and complex types. For example, when boundary + values such as inf occur they may not propagate as expected. For more + explanation check cupynumeric.cumprod. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return perform_scan( + ScanCode.SUM, a, axis=axis, dtype=dtype, out=out, nan_to_identity=False + ) + + +@add_boilerplate("a") +def nancumprod( + a: ndarray, + axis: int | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return the cumulative product of the elements along a given axis treating + Not a Numbers (NaNs) as one. The cumulative product does not change when + NaNs are encountered and leading NaNs are replaced by ones. + + Ones are returned for slices that are all-NaN or empty. + + Parameters + ---------- + a : array_like + Input array. + + axis : int, optional + Axis along which the cumulative product is computed. The default (None) + is to compute the nancumprod over the flattened array. + + dtype : dtype, optional + Type of the returned array and of the accumulator in which the elements + are multiplied. If dtype is not specified, it defaults to the dtype of + a, unless a has an integer dtype with a precision less than that of the + default platform integer. In that case, the default platform integer is + used. + out : ndarray, optional + Alternative output array in which to place the result. It must have the + same shape and buffer length as the expected output but the type will + be cast if necessary. See Output type determination for more details. + + Returns + ------- + nancumprod : ndarray. + A new array holding the result is returned unless out is specified, in + which case a reference to out is returned. The result has the same size + as a, and the same shape as a if axis is not None or a is a 1-d array. + + See Also + -------- + numpy.nancumprod + + Notes + ----- + CuPyNumeric's parallel implementation may yield different results from + NumPy with floating point and complex types. For example, when boundary + values such as inf occur they may not propagate as expected. For more + explanation check cupynumeric.cumprod. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return perform_scan( + ScanCode.PROD, a, axis=axis, dtype=dtype, out=out, nan_to_identity=True + ) + + +@add_boilerplate("a") +def nancumsum( + a: ndarray, + axis: int | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, +) -> ndarray: + """ + Return the cumulative sum of the elements along a given axis treating Not a + Numbers (NaNs) as zero. The cumulative sum does not change when NaNs are + encountered and leading NaNs are replaced by zeros. + + Zeros are returned for slices that are all-NaN or empty. + + Parameters + ---------- + a : array_like + Input array. + + axis : int, optional + Axis along which the cumulative sum is computed. The default (None) is + to compute the nancumsum over the flattened array. + + dtype : dtype, optional + Type of the returned array and of the accumulator in which the elements + are summed. If dtype is not specified, it defaults to the dtype of a, + unless a has an integer dtype with a precision less than that of the + default platform integer. In that case, the default platform integer is + used. + out : ndarray, optional + Alternative output array in which to place the result. It must have the + same shape and buffer length as the expected output but the type will + be cast if necessary. See Output type determination for more details. + + Returns + ------- + nancumsum : ndarray. + A new array holding the result is returned unless out is specified, in + which case a reference to out is returned. The result has the same size + as a, and the same shape as a if axis is not None or a is a 1-d array. + + See Also + -------- + numpy.nancumsum + + Notes + ----- + CuPyNumeric's parallel implementation may yield different results from + NumPy with floating point and complex types. For example, when boundary + values such as inf occur they may not propagate as expected. For more + explanation check cupynumeric.cumprod. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return perform_scan( + ScanCode.SUM, a, axis=axis, dtype=dtype, out=out, nan_to_identity=True + ) + + +@add_boilerplate("a") +def nanargmax( + a: ndarray, + axis: Any = None, + out: ndarray | None = None, + *, + keepdims: bool = False, +) -> ndarray: + """ + Return the indices of the maximum values in the specified axis ignoring + NaNs. For empty arrays, ValueError is raised. For all-NaN slices, + ValueError is raised only when CUPYNUMERIC_NUMPY_COMPATIBILITY + environment variable is set, otherwise identity is returned. + + Warning: results cannot be trusted if a slice contains only NaNs + and -Infs. + + Parameters + ---------- + a : array_like + Input array. + axis : int, optional + By default, the index corresponds to the flattened array, otherwise + along the specified axis. + out : ndarray, optional + If provided, the result will be inserted into this array. It should + be of the appropriate shape and dtype. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the array. + + Returns + ------- + index_array : ndarray[int] + Array of indices into the array. It has the same shape as `a.shape` + with the dimension along `axis` removed. + + See Also + -------- + numpy.nanargmin, numpy.nanargmax + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if a.size == 0: + raise ValueError("attempt to get nanargmax of an empty sequence") + + if cupynumeric_settings.numpy_compat() and a.dtype.kind == "f": + if any(all(isnan(a), axis=axis)): + raise ValueError("Array/Slice contains only NaNs") + + unary_red_code = get_non_nan_unary_red_code( + a.dtype.kind, UnaryRedCode.NANARGMAX + ) + + return perform_unary_reduction( + unary_red_code, + a, + axis=axis, + out=out, + keepdims=keepdims, + res_dtype=np.dtype(np.int64), + ) + + +@add_boilerplate("a") +def nanargmin( + a: ndarray, + axis: Any = None, + out: ndarray | None = None, + *, + keepdims: bool = False, +) -> ndarray: + """ + Return the indices of the minimum values in the specified axis ignoring + NaNs. For empty arrays, ValueError is raised. For all-NaN slices, + ValueError is raised only when CUPYNUMERIC_NUMPY_COMPATIBILITY + environment variable is set, otherwise identity is returned. + + Warning: results cannot be trusted if a slice contains only NaNs + and -Infs. + + Parameters + ---------- + a : array_like + Input array. + axis : int, optional + By default, the index corresponds to the flattened array, otherwise + along the specified axis. + out : ndarray, optional + If provided, the result will be inserted into this array. It should + be of the appropriate shape and dtype. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the array. + + Returns + ------- + index_array : ndarray[int] + Array of indices into the array. It has the same shape as `a.shape` + with the dimension along `axis` removed. + + See Also + -------- + numpy.nanargmin, numpy.nanargmax + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + if a.size == 0: + raise ValueError("attempt to get nanargmin of an empty sequence") + + if cupynumeric_settings.numpy_compat() and a.dtype.kind == "f": + if any(all(isnan(a), axis=axis)): + raise ValueError("Array/Slice contains only NaNs") + + unary_red_code = get_non_nan_unary_red_code( + a.dtype.kind, UnaryRedCode.NANARGMIN + ) + + return perform_unary_reduction( + unary_red_code, + a, + axis=axis, + out=out, + keepdims=keepdims, + res_dtype=np.dtype(np.int64), + ) + + +@add_boilerplate("a") +def nanmin( + a: ndarray, + axis: Any = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + Return minimum of an array or minimum along an axis, ignoring any + NaNs. When all-NaN slices are encountered, a NaN is returned + for that slice only when CUPYNUMERIC_NUMPY_COMPATIBILITY environment + variable is set, otherwise identity is returned. + Empty slices will raise a ValueError + + Parameters + ---------- + a : array_like + Array containing numbers whose minimum is desired. If a is not an + array, a conversion is attempted. + + axis : {int, tuple of int, None}, optional + Axis or axes along which the minimum is computed. The default is to + compute the minimum of the flattened array. + + out : ndarray, optional + Alternative output array in which to place the result. Must + be of the same shape and buffer length as the expected output. + + keepdims : bool, Optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `amin` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + initial : scalar, optional + The maximum value of an output element. Must be present to allow + computation on empty slice. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to compare for the minimum. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + nanmin : ndarray or scalar + Minimum of `a`. If `axis` is None, the result is a scalar value. + If `axis` is given, the result is an array of dimension + ``a.ndim - 1``. + + Notes + ----- + CuPyNumeric's implementation will not raise a Runtime Warning for + slices with all-NaNs + + See Also + -------- + numpy.nanmin, numpy.nanmax, numpy.min, numpy.max, numpy.isnan, + numpy.maximum + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + unary_red_code = get_non_nan_unary_red_code( + a.dtype.kind, UnaryRedCode.NANMIN + ) + + out_array = perform_unary_reduction( + unary_red_code, + a, + axis=axis, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + if cupynumeric_settings.numpy_compat() and a.dtype.kind == "f": + all_nan = all(isnan(a), axis=axis, keepdims=keepdims, where=where) + putmask(out_array, all_nan, np.nan) # type: ignore + + return out_array + + +@add_boilerplate("a") +def nanmax( + a: ndarray, + axis: Any = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + Return the maximum of an array or maximum along an axis, ignoring any + NaNs. When all-NaN slices are encountered, a NaN is returned + for that slice only when CUPYNUMERIC_NUMPY_COMPATIBILITY environment + variable is set, otherwise identity is returned. + Empty slices will raise a ValueError + + Parameters + ---------- + a : array_like + Array containing numbers whose maximum is desired. If a is not + an array, a conversion is attempted. + + axis : None or int or tuple[int], optional + Axis or axes along which to operate. By default, flattened input is + used. + + If this is a tuple of ints, the maximum is selected over multiple axes, + instead of a single axis or all the axes as before. + + out : ndarray, optional + Alternative output array in which to place the result. Must + be of the same shape and buffer length as the expected output. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `amax` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + initial : scalar, optional + The minimum value of an output element. Must be present to allow + computation on empty slice. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to compare for the maximum. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + nanmax : ndarray or scalar + An array with the same shape as `a`, with the specified axis + removed. If `a` is 0-d array, of if axis is None, an ndarray + scalar is returned. The same dtype as `a` is returned. + + Notes + ----- + CuPyNumeric's implementation will not raise a Runtime Warning for + slices with all-NaNs + + See Also + -------- + numpy.nanmin, numpy.amax, numpy.isnan, numpy.fmax, numpy.maximum, + numpy.isfinite + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + unary_red_code = get_non_nan_unary_red_code( + a.dtype.kind, UnaryRedCode.NANMAX + ) + + out_array = perform_unary_reduction( + unary_red_code, + a, + axis=axis, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + if cupynumeric_settings.numpy_compat() and a.dtype.kind == "f": + all_nan = all(isnan(a), axis=axis, keepdims=keepdims, where=where) + putmask(out_array, all_nan, np.nan) # type: ignore + + return out_array + + +@add_boilerplate("a") +def nanprod( + a: ndarray, + axis: Any = None, + dtype: Any = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + Return the product of array elements over a given axis treating + Not a Numbers (NaNs) as ones. + + One is returned for slices that are all-NaN or empty. + + Parameters + ---------- + a : array_like + Input array. + axis : int, optional + Axis or axes along which the product is computed. The + default is to compute the product of the flattened array. + dtype : data-type, optional + The type of the returned array and of the accumulator in + which the elements are summed. By default, the dtype of a + is used. An exception is when a has an integer type with + less precision than the platform (u)intp. In that case, + the default will be either (u)int32 or (u)int64 depending + on whether the platform is 32 or 64 bits. For inexact + inputs, dtype must be inexact. + out : ndarray, optional + Alternate output array in which to place the result. The + default is None. If provided, it must have the same shape as + the expected output, but the type will be cast if necessary. + See Output type determination for more details. The casting of + NaN to integer can yield unexpected results. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in the + result as dimensions with size one. With this option, the result + will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `prod` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + initial : scalar, optional + The starting value for this product. See `~cupynumeric.ufunc.reduce` + for details. + where : array_like[bool], optional + Elements to include in the product. See `~cupynumeric.ufunc.reduce` + for details. + + Returns + ------- + nanprod: ndarray, see `dtype` parameter above. + A new array holding the result is returned unless out is + specified, in which case it is returned. + + See Also + -------- + numpy.prod, numpy.isnan + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + + # Note: if the datatype of the input array is int and less + # than that of the platform int, then a convert task is launched + # in np.prod to take care of the type casting + + if a.dtype == np.complex128: + raise NotImplementedError( + "operation is not supported for complex128 arrays" + ) + + if a.dtype.kind in ("f", "c"): + unary_red_code = UnaryRedCode.NANPROD + else: + unary_red_code = UnaryRedCode.PROD + + return perform_unary_reduction( + unary_red_code, + a, + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +@add_boilerplate("a") +def nansum( + a: ndarray, + axis: Any = None, + dtype: Any = None, + out: ndarray | None = None, + keepdims: bool = False, + initial: int | float | None = None, + where: ndarray | None = None, +) -> ndarray: + """ + Return the sum of array elements over a given axis treating + Not a Numbers (NaNs) as ones. + + Zero is returned for slices that are all-NaN or empty. + + Parameters + ---------- + a : array_like + Array containing numbers whose product is desired. If a is not + an array, a conversion is attempted. + + axis : None or int or tuple[int], optional + Axis or axes along which a sum is performed. The default, + axis=None, will sum all of the elements of the input array. + If axis is negative it counts from the last to the first axis. + + If axis is a tuple of ints, a sum is performed on all of the + axes specified in the tuple instead of a single axis or all + the axes as before. + + dtype : data-type, optional + The type of the returned array and of the accumulator in which + the elements are summed. The dtype of `a` is used by default + unless `a` has an integer dtype of less precision than the + default platform integer. In that case, if `a` is signed then + the platform integer is used while if `a` is unsigned then an + unsigned integer of the same precision as the platform integer + is used. + + out : ndarray, optional + Alternative output array in which to place the result. It must + have the same shape as the expected output, but the type of + the output values will be cast if necessary. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + initial : scalar, optional + Starting value for the sum. See `~cupynumeric.ufunc.reduce` for + details. + + where : array_like[bool], optional + Elements to include in the sum. See `~cupynumeric.ufunc.reduce` for + details. + + Returns + ------- + nansum : ndarray, see `dtype` parameter above. + A new array holding the result is returned unless out is + specified, in which case it is returned. The result has the + same size as a, and the same shape as a if axis is not None or + a is a 1-d array. + + See Also + -------- + numpy.nansum, numpy.isnan, numpy.isfinite + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + return a._nansum( + axis=axis, + dtype=dtype, + out=out, + keepdims=keepdims, + initial=initial, + where=where, + ) + + +@add_boilerplate("a", "prepend", "append") +def diff( + a: ndarray, + n: int = 1, + axis: int = -1, + prepend: ndarray | None = None, + append: ndarray | None = None, +) -> ndarray: + """ + Calculate the n-th discrete difference along the given axis. + The first difference is given by ``out[i] = a[i+1] - a[i]`` along + the given axis, higher differences are calculated by using `diff` + recursively. + + Parameters + ---------- + a : array_like + Input array + n : int, optional + The number of times values are differenced. If zero, the input + is returned as-is. + axis : int, optional + The axis along which the difference is taken, default is the + last axis. + prepend, append : array_like, optional + Values to prepend or append to `a` along axis prior to + performing the difference. Scalar values are expanded to + arrays with length 1 in the direction of axis and the shape + of the input array in along all other axes. Otherwise the + dimension and shape must match `a` except along axis. + + Returns + ------- + diff : ndarray + The n-th differences. The shape of the output is the same as `a` + except along `axis` where the dimension is smaller by `n`. The + type of the output is the same as the type of the difference + between any two elements of `a`. This is the same as the type of + `a` in most cases. + + See Also + -------- + numpy.diff + + Notes + ----- + Type is preserved for boolean arrays, so the result will contain + `False` when consecutive elements are the same and `True` when they + differ. + + For unsigned integer arrays, the results will also be unsigned. This + should not be surprising, as the result is consistent with + calculating the difference directly:: + + >>> u8_arr = np.array([1, 0], dtype=np.uint8) + >>> np.diff(u8_arr) + array([255], dtype=uint8) + >>> u8_arr[1,...] - u8_arr[0,...] + 255 + If this is not desirable, then the array should be cast to a larger + integer type first: + >>> i16_arr = u8_arr.astype(np.int16) + >>> np.diff(i16_arr) + array([-1], dtype=int16) + Examples + -------- + >>> x = np.array([1, 2, 4, 7, 0]) + >>> np.diff(x) + array([ 1, 2, 3, -7]) + >>> np.diff(x, n=2) + array([ 1, 1, -10]) + >>> x = np.array([[1, 3, 6, 10], [0, 5, 6, 8]]) + >>> np.diff(x) + array([[2, 3, 4], + [5, 1, 2]]) + >>> np.diff(x, axis=0) + array([[-1, 2, 0, -2]]) + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if n == 0: + return a + if n < 0: + raise ValueError("order must be non-negative but got " + repr(n)) + + nd = a.ndim + if nd == 0: + raise ValueError( + "diff requires input that is at least one dimensional" + ) + axis = normalize_axis_index(axis, nd) + + combined = [] + if prepend is not None: + if prepend.ndim == 0: + shape = list(a.shape) + shape[axis] = 1 + prepend = broadcast_to(prepend, tuple(shape)) + combined.append(prepend) + + combined.append(a) + + if append is not None: + if append.ndim == 0: + shape = list(a.shape) + shape[axis] = 1 + append = broadcast_to(append, tuple(shape)) + combined.append(append) + + if len(combined) > 1: + a = concatenate(combined, axis) + + # Diffing with n > shape results in an empty array. We have + # to handle this case explicitly as our slicing routines raise + # an exception with out-of-bounds slices, while NumPy's dont. + if a.shape[axis] <= n: + shape = list(a.shape) + shape[axis] = 0 + return empty(shape=tuple(shape), dtype=a.dtype) + + slice1l = [slice(None)] * nd + slice2l = [slice(None)] * nd + slice1l[axis] = slice(1, None) + slice2l[axis] = slice(None, -1) + slice1 = tuple(slice1l) + slice2 = tuple(slice2l) + + op = not_equal if a.dtype == bool else subtract + for _ in range(n): + a = op(a[slice1], a[slice2]) + + return a + + +@add_boilerplate("f") +def gradient( + f: ndarray, *varargs: Any, axis: Any = None, edge_order: int = 1 +) -> Any: + """ + Return the gradient of an N-dimensional array. + + The gradient is computed using second order accurate central differences + in the interior points and either first or second order accurate one-sided + (forward or backwards) differences at the boundaries. + The returned gradient hence has the same shape as the input array. + + Parameters + ---------- + f : array_like + An N-dimensional array containing samples of a scalar function. + varargs : list of scalar or array, optional + Spacing between f values. Default unitary spacing for all dimensions. + Spacing can be specified using: + + 1. single scalar to specify a sample distance for all dimensions. + 2. N scalars to specify a constant sample distance for each dimension. + i.e. `dx`, `dy`, `dz`, ... + 3. N arrays to specify the coordinates of the values along each + dimension of F. The length of the array must match the size of + the corresponding dimension + 4. Any combination of N scalars/arrays with the meaning of 2. and 3. + + If `axis` is given, the number of varargs must equal the number of + axes. Default: 1. + + edge_order : {1, 2}, optional + Gradient is calculated using N-th order accurate differences + at the boundaries. Default: 1. + + axis : None or int or tuple of ints, optional + Gradient is calculated only along the given axis or axes + The default (axis = None) is to calculate the gradient for all the axes + of the input array. axis may be negative, in which case it counts from + the last to the first axis. + + Returns + ------- + gradient : ndarray or list of ndarray + A list of ndarrays (or a single ndarray if there is only one dimension) + corresponding to the derivatives of f with respect to each dimension. + Each derivative has the same shape as f. + + See Also + -------- + numpy.gradient + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + N = f.ndim # number of dimensions + + if axis is None: + axes = tuple(range(N)) + elif isinstance(axis, collections.abc.Sequence): + axes = tuple(normalize_axis_index(a, N) for a in axis) + else: + axis = normalize_axis_index(axis, N) + axes = (axis,) + + len_axes = len(axes) + if not varargs: + n = 0 + else: + n = len(varargs) + + if n == 0: + # no spacing argument - use 1 in all axes + dx = [asarray(1.0)] * len_axes + elif n == 1 and np.ndim(varargs[0]) == 0: + # single scalar for all axes + dx = list(asarray(varargs)) * len_axes + elif n == len_axes: + # scalar or 1d array for each axis + dx = list(asarray(v) for v in varargs) + for i, distances in enumerate(dx): + if distances.ndim == 0: + continue + elif distances.ndim != 1: + raise ValueError("distances must be either scalars or 1d") + if len(distances) != f.shape[axes[i]]: + raise ValueError( + "when 1d, distances must match " + "the length of the corresponding dimension" + ) + if np.issubdtype(distances.dtype, np.integer): + # Convert numpy integer types to float64 to avoid modular + # arithmetic in np.diff(distances). + distances = distances.astype(np.float64) + diffx = diff(distances) + dx[i] = diffx + else: + raise TypeError("invalid number of arguments") + + if edge_order > 2: + raise ValueError("'edge_order' greater than 2 not supported") + if edge_order < 0: + raise ValueError("invalid 'edge_order'") + + # use central differences on interior and one-sided differences on the + # endpoints. This preserves second order-accuracy over the full domain. + + outvals = [] + + # create slice objects --- initially all are [:, :, ..., :] + slice1 = [slice(None)] * N + slice2 = [slice(None)] * N + slice3 = [slice(None)] * N + slice4 = [slice(None)] * N + + otype = f.dtype + if not np.issubdtype(otype, np.inexact): + # All other types convert to floating point. + # First check if f is a numpy integer type; if so, convert f to float64 + # to avoid modular arithmetic when computing the changes in f. + if np.issubdtype(otype, np.integer): + f = f.astype(np.float64) + otype = np.dtype(np.float64) + + for axis, ax_dx in zip(axes, dx): + if f.shape[axis] < edge_order + 1: + raise ValueError( + "Shape of array too small to calculate a numerical gradient, " + "at least (edge_order + 1) elements are required." + ) + # result allocation + out = empty_like(f, dtype=otype) + + # spacing for the current axis + uniform_spacing = np.ndim(ax_dx) == 0 + + # Numerical differentiation: 2nd order interior + slice1[axis] = slice(1, -1) + slice2[axis] = slice(None, -2) + slice3[axis] = slice(1, -1) + slice4[axis] = slice(2, None) + + if uniform_spacing: + out[tuple(slice1)] = (f[tuple(slice4)] - f[tuple(slice2)]) / ( + 2.0 * ax_dx + ) + else: + dx1 = ax_dx[0:-1] + dx2 = ax_dx[1:] + a = -(dx2) / (dx1 * (dx1 + dx2)) + b = (dx2 - dx1) / (dx1 * dx2) + c = dx1 / (dx2 * (dx1 + dx2)) + # fix the shape for broadcasting + shape = list(1 for i in range(N)) + shape[axis] = -1 + a = a.reshape(shape) + b = b.reshape(shape) + c = c.reshape(shape) + # 1D equivalent -- out[1:-1] = a * f[:-2] + b * f[1:-1] + c * f[2:] + out[tuple(slice1)] = ( + a * f[tuple(slice2)] + + b * f[tuple(slice3)] + + c * f[tuple(slice4)] + ) + + # Numerical differentiation: 1st order edges + if edge_order == 1: + slice1[axis] = 0 # type: ignore + slice2[axis] = 1 # type: ignore + slice3[axis] = 0 # type: ignore + dx_0 = ax_dx if uniform_spacing else ax_dx[0] + # 1D equivalent -- out[0] = (f[1] - f[0]) / (x[1] - x[0]) + out[tuple(slice1)] = (f[tuple(slice2)] - f[tuple(slice3)]) / dx_0 + + slice1[axis] = -1 # type: ignore + slice2[axis] = -1 # type: ignore + slice3[axis] = -2 # type: ignore + dx_n = ax_dx if uniform_spacing else ax_dx[-1] + # 1D equivalent -- out[-1] = (f[-1] - f[-2]) / (x[-1] - x[-2]) + out[tuple(slice1)] = (f[tuple(slice2)] - f[tuple(slice3)]) / dx_n + + # Numerical differentiation: 2nd order edges + else: + slice1[axis] = 0 # type: ignore + slice2[axis] = 0 # type: ignore + slice3[axis] = 1 # type: ignore + slice4[axis] = 2 # type: ignore + if uniform_spacing: + a = -1.5 / ax_dx + b = 2.0 / ax_dx + c = -0.5 / ax_dx + else: + dx1 = ax_dx[0] + dx2 = ax_dx[1] + a = -(2.0 * dx1 + dx2) / (dx1 * (dx1 + dx2)) + b = (dx1 + dx2) / (dx1 * dx2) + c = -dx1 / (dx2 * (dx1 + dx2)) + # 1D equivalent -- out[0] = a * f[0] + b * f[1] + c * f[2] + out[tuple(slice1)] = ( + a * f[tuple(slice2)] + + b * f[tuple(slice3)] + + c * f[tuple(slice4)] + ) + + slice1[axis] = -1 # type: ignore + slice2[axis] = -3 # type: ignore + slice3[axis] = -2 # type: ignore + slice4[axis] = -1 # type: ignore + if uniform_spacing: + a = 0.5 / ax_dx + b = -2.0 / ax_dx + c = 1.5 / ax_dx + else: + dx1 = ax_dx[-2] + dx2 = ax_dx[-1] + a = (dx2) / (dx1 * (dx1 + dx2)) + b = -(dx2 + dx1) / (dx1 * dx2) + c = (2.0 * dx2 + dx1) / (dx2 * (dx1 + dx2)) + # 1D equivalent -- out[-1] = a * f[-3] + b * f[-2] + c * f[-1] + out[tuple(slice1)] = ( + a * f[tuple(slice2)] + + b * f[tuple(slice3)] + + c * f[tuple(slice4)] + ) + + outvals.append(out) + + # reset the slice object in this dimension to ":" + slice1[axis] = slice(None) + slice2[axis] = slice(None) + slice3[axis] = slice(None) + slice4[axis] = slice(None) + + if len_axes == 1: + return outvals[0] + else: + return outvals diff --git a/cupynumeric/_module/sets_making.py b/cupynumeric/_module/sets_making.py new file mode 100644 index 000000000..4e1c6ef4d --- /dev/null +++ b/cupynumeric/_module/sets_making.py @@ -0,0 +1,104 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import add_boilerplate + +if TYPE_CHECKING: + from .._array.array import ndarray + +_builtin_any = any + + +@add_boilerplate("ar") +def unique( + ar: ndarray, + return_index: bool = False, + return_inverse: bool = False, + return_counts: bool = False, + axis: int | None = None, +) -> ndarray: + """ + + Find the unique elements of an array. + Returns the sorted unique elements of an array. There are three optional + outputs in addition to the unique elements: + * the indices of the input array that give the unique values + * the indices of the unique array that reconstruct the input array + * the number of times each unique value comes up in the input array + + Parameters + ---------- + ar : array_like + Input array. Unless `axis` is specified, this will be flattened if it + is not already 1-D. + return_index : bool, optional + If True, also return the indices of `ar` (along the specified axis, + if provided, or in the flattened array) that result in the unique + array. + Currently not supported. + return_inverse : bool, optional + If True, also return the indices of the unique array (for the specified + axis, if provided) that can be used to reconstruct `ar`. + Currently not supported. + return_counts : bool, optional + If True, also return the number of times each unique item appears + in `ar`. + Currently not supported. + axis : int or None, optional + The axis to operate on. If None, `ar` will be flattened. If an integer, + the subarrays indexed by the given axis will be flattened and treated + as the elements of a 1-D array with the dimension of the given axis, + see the notes for more details. Object arrays or structured arrays + that contain objects are not supported if the `axis` kwarg is used. The + default is None. + Currently not supported. + + Returns + ------- + unique : ndarray + The sorted unique values. + unique_indices : ndarray, optional + The indices of the first occurrences of the unique values in the + original array. Only provided if `return_index` is True. + unique_inverse : ndarray, optional + The indices to reconstruct the original array from the + unique array. Only provided if `return_inverse` is True. + unique_counts : ndarray, optional + The number of times each of the unique values comes up in the + original array. Only provided if `return_counts` is True. + + See Also + -------- + numpy.unique + + Availability + -------- + Multiple GPUs, Multiple CPUs + + Notes + -------- + Keyword arguments for optional outputs are not yet supported. + `axis` is also not handled currently. + + """ + if _builtin_any((return_index, return_inverse, return_counts, axis)): + raise NotImplementedError( + "Keyword arguments for `unique` are not yet supported" + ) + + return ar.unique() diff --git a/cupynumeric/_module/ssc_counting.py b/cupynumeric/_module/ssc_counting.py new file mode 100644 index 000000000..879c22083 --- /dev/null +++ b/cupynumeric/_module/ssc_counting.py @@ -0,0 +1,57 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._array.util import add_boilerplate + +if TYPE_CHECKING: + from .._array.array import ndarray + + +@add_boilerplate("a") +def count_nonzero( + a: ndarray, axis: int | tuple[int, ...] | None = None +) -> int | ndarray: + """ + + Counts the number of non-zero values in the array ``a``. + + Parameters + ---------- + a : array_like + The array for which to count non-zeros. + axis : int or tuple, optional + Axis or tuple of axes along which to count non-zeros. + Default is None, meaning that non-zeros will be counted + along a flattened version of ``a``. + + Returns + ------- + count : int or ndarray[int] + Number of non-zero values in the array along a given axis. + Otherwise, the total number of non-zero values in the array + is returned. + + See Also + -------- + numpy.count_nonzero + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a._count_nonzero(axis) diff --git a/cupynumeric/_module/ssc_searching.py b/cupynumeric/_module/ssc_searching.py new file mode 100644 index 000000000..c64261eb2 --- /dev/null +++ b/cupynumeric/_module/ssc_searching.py @@ -0,0 +1,357 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +from .._array.array import ndarray +from .._array.thunk import perform_where +from .._array.util import add_boilerplate +from .array_shape import ravel, reshape + +if TYPE_CHECKING: + import numpy.typing as npt + + from ..types import SortSide + + +@add_boilerplate("a") +def searchsorted( + a: ndarray, + v: int | float | ndarray, + side: SortSide = "left", + sorter: ndarray | None = None, +) -> int | ndarray: + """ + + Find the indices into a sorted array a such that, if the corresponding + elements in v were inserted before the indices, the order of a would be + preserved. + + Parameters + ---------- + a : 1-D array_like + Input array. If sorter is None, then it must be sorted in ascending + order, otherwise sorter must be an array of indices that sort it. + v : scalar or array_like + Values to insert into a. + side : ``{'left', 'right'}``, optional + If 'left', the index of the first suitable location found is given. + If 'right', return the last such index. If there is no suitable index, + return either 0 or N (where N is the length of a). + sorter : 1-D array_like, optional + Optional array of integer indices that sort array a into ascending + order. They are typically the result of argsort. + + Returns + ------- + indices : int or array_like[int] + Array of insertion points with the same shape as v, or an integer + if v is a scalar. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.searchsorted(v, side, sorter) + + +@add_boilerplate("a") +def argmax( + a: ndarray, + axis: int | None = None, + out: ndarray | None = None, + *, + keepdims: bool = False, +) -> ndarray: + """ + + Returns the indices of the maximum values along an axis. + + Parameters + ---------- + a : array_like + Input array. + axis : int, optional + By default, the index is into the flattened array, otherwise + along the specified axis. + out : ndarray, optional + If provided, the result will be inserted into this array. It should + be of the appropriate shape and dtype. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the array. + + Returns + ------- + index_array : ndarray[int] + Array of indices into the array. It has the same shape as `a.shape` + with the dimension along `axis` removed. + + See Also + -------- + numpy.argmax + + Notes + ----- + cuPyNumeric's parallel implementation may yield different results from + NumPy when the array contains NaN(s). + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.argmax(axis=axis, out=out, keepdims=keepdims) + + +@add_boilerplate("a") +def argmin( + a: ndarray, + axis: int | None = None, + out: ndarray | None = None, + *, + keepdims: bool = False, +) -> ndarray: + """ + + Returns the indices of the minimum values along an axis. + + Parameters + ---------- + a : array_like + Input array. + axis : int, optional + By default, the index is into the flattened array, otherwise + along the specified axis. + out : ndarray, optional + If provided, the result will be inserted into this array. It should + be of the appropriate shape and dtype. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the array. + + Returns + ------- + index_array : ndarray[int] + Array of indices into the array. It has the same shape as `a.shape` + with the dimension along `axis` removed. + + See Also + -------- + numpy.argmin + + Notes + ----- + cuPyNumeric's parallel implementation may yield different results from + NumPy when the array contains NaN(s). + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.argmin(axis=axis, out=out, keepdims=keepdims) + + +@add_boilerplate("a") +def flatnonzero(a: ndarray) -> ndarray: + """ + + Return indices that are non-zero in the flattened version of a. + + This is equivalent to `np.nonzero(np.ravel(a))[0]`. + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + res : ndarray + Output array, containing the indices of the elements of + `a.ravel()` that are non-zero. + + See Also + -------- + numpy.flatnonzero + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return nonzero(ravel(a))[0] + + +@overload +def where(a: npt.ArrayLike | ndarray, x: None, y: None) -> tuple[ndarray, ...]: + ... + + +@overload +def where( + a: npt.ArrayLike | ndarray, + x: npt.ArrayLike | ndarray, + y: npt.ArrayLike | ndarray, +) -> ndarray: + ... + + +@add_boilerplate("a", "x", "y") # type: ignore [misc] +def where( + a: ndarray, x: ndarray | None = None, y: ndarray | None = None +) -> ndarray | tuple[ndarray, ...]: + """ + where(condition, [x, y]) + + Return elements chosen from `x` or `y` depending on `condition`. + + Parameters + ---------- + condition : array_like, bool + Where True, yield `x`, otherwise yield `y`. + x, y : array_like + Values from which to choose. `x`, `y` and `condition` need to be + broadcastable to some shape. + + Returns + ------- + out : ndarray + An array with elements from `x` where `condition` is True, and elements + from `y` elsewhere. + + See Also + -------- + numpy.where + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if x is None or y is None: + if x is not None or y is not None: + raise ValueError( + "both 'x' and 'y' parameters must be specified together for" + " 'where'" + ) + return nonzero(a) + return perform_where(a, x, y) + + +@add_boilerplate("a") +def argwhere(a: ndarray) -> ndarray: + """ + argwhere(a) + + Find the indices of array elements that are non-zero, grouped by element. + + Parameters + ---------- + a : array_like + Input data. + + Returns + ------- + index_array : ndarray + Indices of elements that are non-zero. Indices are grouped by element. + This array will have shape (N, a.ndim) where N is the number of + non-zero items. + + See Also + -------- + numpy.argwhere + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + thunk = a._thunk.argwhere() + return ndarray(shape=thunk.shape, thunk=thunk) + + +@add_boilerplate("condition", "arr") +def extract(condition: ndarray, arr: ndarray) -> ndarray: + """ + + Return the elements of an array that satisfy some condition. + + Parameters + ---------- + condition : array_like + An array whose nonzero or True entries indicate the elements + of `arr` to extract. + arr : array_like + Input array of the same size as `condition`. + + Returns + ------- + result : ndarray + Rank 1 array of values from arr where `condition` is True. + + See Also + -------- + numpy.extract + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if condition.size != arr.size: + raise ValueError("arr array and condition array must be of same size") + + if condition.shape != arr.shape: + condition_reshape = reshape(condition, arr.shape) + else: + condition_reshape = condition + + if condition_reshape.dtype == bool: + thunk = arr._thunk.get_item(condition_reshape._thunk) + else: + bool_condition = condition_reshape.astype(bool) + thunk = arr._thunk.get_item(bool_condition._thunk) + + return ndarray(shape=thunk.shape, thunk=thunk) + + +@add_boilerplate("a") +def nonzero(a: ndarray) -> tuple[ndarray, ...]: + """ + + Return the indices of the elements that are non-zero. + + Returns a tuple of arrays, one for each dimension of `a`, + containing the indices of the non-zero elements in that + dimension. + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + tuple_of_arrays : tuple + Indices of elements that are non-zero. + + See Also + -------- + numpy.nonzero + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.nonzero() diff --git a/cupynumeric/_module/ssc_sorting.py b/cupynumeric/_module/ssc_sorting.py new file mode 100644 index 000000000..4f32d0194 --- /dev/null +++ b/cupynumeric/_module/ssc_sorting.py @@ -0,0 +1,291 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from .._utils import is_np2 + +if TYPE_CHECKING: + from ..types import SelectKind, SortType + + +@add_boilerplate("a") +def argsort( + a: ndarray, + axis: int | None = -1, + kind: SortType = "quicksort", + order: str | list[str] | None = None, +) -> ndarray: + """ + + Returns the indices that would sort an array. + + Parameters + ---------- + a : array_like + Input array. + axis : int or None, optional + Axis to sort. By default, the index -1 (the last axis) is used. If + None, the flattened array is used. + kind : ``{'quicksort', 'mergesort', 'heapsort', 'stable'}``, optional + Default is 'quicksort'. The underlying sort algorithm might vary. + The code basically supports 'stable' or *not* 'stable'. + order : str or list[str], optional + Currently not supported + + Returns + ------- + index_array : ndarray[int] + Array of indices that sort a along the specified axis. It has the + same shape as `a.shape` or is flattened in case of `axis` is None. + + See Also + -------- + numpy.argsort + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + result = ndarray(a.shape, np.int64) + result._thunk.sort( + rhs=a._thunk, argsort=True, axis=axis, kind=kind, order=order + ) + return result + + +if not is_np2: + + def msort(a: ndarray) -> ndarray: + """ + + Returns a sorted copy of an array sorted along the first axis. + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + out : ndarray + Sorted array with same dtype and shape as `a`. + + See Also + -------- + numpy.msort + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return sort(a, axis=0) + + +@add_boilerplate("a") +def sort( + a: ndarray, + axis: int | None = -1, + kind: SortType = "quicksort", + order: str | list[str] | None = None, +) -> ndarray: + """ + + Returns a sorted copy of an array. + + Parameters + ---------- + a : array_like + Input array. + axis : int or None, optional + Axis to sort. By default, the index -1 (the last axis) is used. If + None, the flattened array is used. + kind : ``{'quicksort', 'mergesort', 'heapsort', 'stable'}``, optional + Default is 'quicksort'. The underlying sort algorithm might vary. + The code basically supports 'stable' or *not* 'stable'. + order : str or list[str], optional + Currently not supported + + Returns + ------- + out : ndarray + Sorted array with same dtype and shape as `a`. In case `axis` is + None the result is flattened. + + + See Also + -------- + numpy.sort + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + result = ndarray(a.shape, a.dtype) + result._thunk.sort(rhs=a._thunk, axis=axis, kind=kind, order=order) + return result + + +@add_boilerplate("a") +def sort_complex(a: ndarray) -> ndarray: + """ + + Returns a sorted copy of an array sorted along the last axis. Sorts the + real part first, the imaginary part second. + + Parameters + ---------- + a : array_like + Input array. + + Returns + ------- + out : ndarray, complex + Sorted array with same shape as `a`. + + See Also + -------- + numpy.sort_complex + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + + result = sort(a) + # force complex result upon return + if np.issubdtype(result.dtype, np.complexfloating): + return result + elif ( + np.issubdtype(result.dtype, np.integer) and result.dtype.itemsize <= 2 + ): + return result.astype(np.complex64, copy=True) + else: + return result.astype(np.complex128, copy=True) + + +# partition + + +@add_boilerplate("a") +def argpartition( + a: ndarray, + kth: int | Sequence[int], + axis: int | None = -1, + kind: SelectKind = "introselect", + order: str | list[str] | None = None, +) -> ndarray: + """ + + Perform an indirect partition along the given axis. + + Parameters + ---------- + a : array_like + Input array. + kth : int or Sequence[int] + axis : int or None, optional + Axis to partition. By default, the index -1 (the last axis) is used. If + None, the flattened array is used. + kind : ``{'introselect'}``, optional + Currently not supported. + order : str or list[str], optional + Currently not supported. + + Returns + ------- + out : ndarray[int] + Array of indices that partitions a along the specified axis. It has the + same shape as `a.shape` or is flattened in case of `axis` is None. + + + Notes + ----- + The current implementation falls back to `cupynumeric.argsort`. + + See Also + -------- + numpy.argpartition + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + result = ndarray(a.shape, np.int64) + result._thunk.partition( + rhs=a._thunk, + argpartition=True, + kth=kth, + axis=axis, + kind=kind, + order=order, + ) + return result + + +@add_boilerplate("a") +def partition( + a: ndarray, + kth: int | Sequence[int], + axis: int | None = -1, + kind: SelectKind = "introselect", + order: str | list[str] | None = None, +) -> ndarray: + """ + + Returns a partitioned copy of an array. + + Parameters + ---------- + a : array_like + Input array. + kth : int or Sequence[int] + axis : int or None, optional + Axis to partition. By default, the index -1 (the last axis) is used. If + None, the flattened array is used. + kind : ``{'introselect'}``, optional + Currently not supported. + order : str or list[str], optional + Currently not supported. + + Returns + ------- + out : ndarray + Partitioned array with same dtype and shape as `a`. In case `axis` is + None the result is flattened. + + Notes + ----- + The current implementation falls back to `cupynumeric.sort`. + + See Also + -------- + numpy.partition + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + result = ndarray(a.shape, a.dtype) + result._thunk.partition( + rhs=a._thunk, kth=kth, axis=axis, kind=kind, order=order + ) + return result diff --git a/cupynumeric/_module/stats_avgs_vars.py b/cupynumeric/_module/stats_avgs_vars.py new file mode 100644 index 000000000..c463e174f --- /dev/null +++ b/cupynumeric/_module/stats_avgs_vars.py @@ -0,0 +1,518 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from .._utils import is_np2 +from .creation_shape import full +from .logic_truth import any +from .stats_order import nanquantile, quantile + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import normalize_axis_tuple # type: ignore + +if TYPE_CHECKING: + import numpy.typing as npt + + +@add_boilerplate("a", "weights") +def average( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + weights: ndarray | None = None, + returned: bool = False, + *, + keepdims: bool = False, +) -> ndarray | tuple[ndarray, ndarray]: + """ + Compute the weighted average along the specified axis. + + Parameters + ---------- + a : array_like + Array containing data to be averaged. If `a` is not an array, a + conversion is attempted. + axis : None or int or tuple of ints, optional + Axis or axes along which to average `a`. The default, + axis=None, will average over all of the elements of the input array. + If axis is negative it counts from the last to the first axis. + If axis is a tuple of ints, averaging is performed on all of the axes + specified in the tuple instead of a single axis or all the axes as + before. + weights : array_like, optional + An array of weights associated with the values in `a`. Each value in + `a` contributes to the average according to its associated weight. + The weights array can either be 1-D (in which case its length must be + the size of `a` along the given axis) or of the same shape as `a`. + If `weights=None`, then all data in `a` are assumed to have a + weight equal to one. The 1-D calculation is:: + + avg = sum(a * weights) / sum(weights) + + The only constraint on `weights` is that `sum(weights)` must not be 0. + returned : bool, optional + Default is `False`. If `True`, the tuple (`average`, `sum_of_weights`) + is returned, otherwise only the average is returned. + If `weights=None`, `sum_of_weights` is equivalent to the number of + elements over which the average is taken. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the original `a`. + + Returns + ------- + retval, [sum_of_weights] : array_type or double + Return the average along the specified axis. When `returned` is `True`, + return a tuple with the average as the first element and the sum + of the weights as the second element. `sum_of_weights` is of the + same type as `retval`. The result dtype follows a general pattern. + If `weights` is None, the result dtype will be that of `a` , or + ``float64`` if `a` is integral. Otherwise, if `weights` is not None and + `a` is non-integral, the result type will be the type of lowest + precision capable of representing values of both `a` and `weights`. If + `a` happens to be integral, the previous rules still applies but the + result dtype will at least be ``float64``. + + Raises + ------ + ZeroDivisionError + When all weights along axis are zero. + ValueError + When the length of 1D `weights` is not the same as the shape of `a` + along axis. + + See Also + -------- + numpy.average + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + clean_axis: tuple[int, ...] | None = None + if axis is not None: + clean_axis = normalize_axis_tuple(axis, a.ndim, argname="axis") + + scl: npt.ArrayLike | ndarray = 1 + if weights is None: + scl = ( + a.size + if clean_axis is None + else math.prod([a.shape[i] for i in clean_axis]) + ) + if a.dtype.kind == "i": + scl = np.float64(scl) + avg = a.sum(axis=clean_axis, keepdims=keepdims) / scl + elif weights.shape == a.shape: + scl = weights.sum( + axis=clean_axis, + keepdims=keepdims, + dtype=(np.dtype(np.float64) if a.dtype.kind == "i" else None), + ) + if any(scl == 0): + raise ZeroDivisionError("Weights along axis sum to 0") + avg = (a * weights).sum(axis=clean_axis, keepdims=keepdims) / scl + else: + if clean_axis is None: + raise ValueError( + "a and weights must share shape or axis must be specified" + ) + if weights.ndim != 1 or len(clean_axis) != 1: + raise ValueError( + "Weights must be either 1 dimension along single " + "axis or the same shape as a" + ) + if weights.size != a.shape[clean_axis[0]]: + raise ValueError("Weights length does not match axis") + + scl = weights.sum( + dtype=(np.dtype(np.float64) if a.dtype.kind == "i" else None) + ) + project_shape = [1] * a.ndim + project_shape[clean_axis[0]] = -1 + weights = weights.reshape(project_shape) + if any(scl == 0): + raise ZeroDivisionError("Weights along axis sum to 0") + avg = (a * weights).sum(axis=clean_axis[0], keepdims=keepdims) / scl + + if returned: + if not isinstance(scl, ndarray) or scl.ndim == 0: + scl = full(avg.shape, scl) + return avg, scl + else: + return avg + + +@add_boilerplate("a") +def mean( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + where: ndarray | None = None, +) -> ndarray: + """ + + Compute the arithmetic mean along the specified axis. + + Returns the average of the array elements. The average is taken over + the flattened array by default, otherwise over the specified axis. + `float64` intermediate and return values are used for integer inputs. + + Parameters + ---------- + a : array_like + Array containing numbers whose mean is desired. If `a` is not an + array, a conversion is attempted. + axis : None or int or tuple[int], optional + Axis or axes along which the means are computed. The default is to + compute the mean of the flattened array. + + If this is a tuple of ints, a mean is performed over multiple axes, + instead of a single axis or all the axes as before. + dtype : data-type, optional + Type to use in computing the mean. For integer inputs, the default + is `float64`; for floating point inputs, it is the same as the + input dtype. + out : ndarray, optional + Alternate output array in which to place the result. The default + is ``None``; if provided, it must have the same shape as the + expected output, but the type will be cast if necessary. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + If the default value is passed, then `keepdims` will not be + passed through to the `mean` method of sub-classes of + `ndarray`, however any non-default value will be. If the + sub-class' method does not implement `keepdims` any + exceptions will be raised. + + where : array_like of bool, optional + Elements to include in the mean. + + Returns + ------- + m : ndarray + If `out is None`, returns a new array of the same dtype a above + containing the mean values, otherwise a reference to the output + array is returned. + + See Also + -------- + numpy.mean + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.mean( + axis=axis, dtype=dtype, out=out, keepdims=keepdims, where=where + ) + + +@add_boilerplate("a") +def nanmean( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + keepdims: bool = False, + where: ndarray | None = None, +) -> ndarray: + """ + + Compute the arithmetic mean along the specified axis, ignoring NaNs. + + Returns the average of the array elements. The average is taken over + the flattened array by default, otherwise over the specified axis. + `float64` intermediate and return values are used for integer inputs. + + Parameters + ---------- + a : array_like + Array containing numbers whose mean is desired. If `a` is not an + array, a conversion is attempted. + axis : None or int or tuple[int], optional + Axis or axes along which the means are computed. The default is to + compute the mean of the flattened array. + + If this is a tuple of ints, a mean is performed over multiple axes, + instead of a single axis or all the axes as before. + dtype : data-type, optional + Type to use in computing the mean. For integer inputs, the default + is `float64`; for floating point inputs, it is the same as the + input dtype. + out : ndarray, optional + Alternate output array in which to place the result. The default + is ``None``; if provided, it must have the same shape as the + expected output, but the type will be cast if necessary. + + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + + + where : array_like of bool, optional + Elements to include in the mean. + + Returns + ------- + m : ndarray + If `out is None`, returns a new array of the same dtype as a above + containing the mean values, otherwise a reference to the output + array is returned. + + See Also + -------- + numpy.nanmean + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a._nanmean( + axis=axis, dtype=dtype, out=out, keepdims=keepdims, where=where + ) + + +@add_boilerplate("a") +def var( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, + ddof: int = 0, + keepdims: bool = False, + *, + where: ndarray | None = None, +) -> ndarray: + """ + Compute the variance along the specified axis. + + Returns the variance of the array elements, a measure of the spread of + a distribution. The variance is computed for the flattened array + by default, otherwise over the specified axis. + + Parameters + ---------- + a : array_like + Array containing numbers whose variance is desired. If `a` is not an + array, a conversion is attempted. + axis : None or int or tuple[int], optional + Axis or axes along which the variance is computed. The default is to + compute the variance of the flattened array. + + If this is a tuple of ints, a variance is performed over multiple axes, + instead of a single axis or all the axes as before. + dtype : data-type, optional + Type to use in computing the variance. For arrays of integer type + the default is float64; for arrays of float types + it is the same as the array type. + out : ndarray, optional + Alternate output array in which to place the result. It must have the + same shape as the expected output, but the type is cast if necessary. + ddof : int, optional + “Delta Degrees of Freedom”: the divisor used in the calculation is + N - ddof, where N represents the number of elements. By default + ddof is zero. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the input array. + where : array_like of bool, optional + A boolean array which is broadcasted to match the dimensions of array, + and selects elements to include in the reduction. + + Returns + ------- + m : ndarray, see dtype parameter above + If `out=None`, returns a new array of the same dtype as above + containing the variance values, otherwise a reference to the output + array is returned. + + See Also + -------- + numpy.var + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + return a.var( + axis=axis, + dtype=dtype, + out=out, + ddof=ddof, + keepdims=keepdims, + where=where, + ) + + +@add_boilerplate("a") +def median( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + keepdims: bool = False, +) -> ndarray: + """ + Compute the median along the specified axis. + + Returns the median of the array elements. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + axis : {int, sequence of int, None}, optional + Axis or axes along which the medians are computed. The default, + axis=None, will compute the median along a flattened version of + the array. + If a sequence of axes, the array is first flattened along the + given axes, then the median is computed along the resulting + flattened axis. + out : ndarray, optional + Alternative output array in which to place the result. It must + have the same shape and buffer length as the expected output, + but the type (of the output) will be cast if necessary. + overwrite_input : bool, optional + If True, then allow use of memory of input array `a` for + calculations. The input array will be modified by the call to + `median`. This will save memory when you do not need to preserve + the contents of the input array. Treat the input as undefined, + but it will probably be fully or partially sorted. Default is + False. If `overwrite_input` is ``True`` and `a` is not already an + `ndarray`, an error will be raised. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the original `arr`. + + Returns + ------- + median : ndarray + A new array holding the result. If the input contains integers + or floats smaller than ``float64``, then the output data-type is + ``np.float64``. Otherwise, the data-type of the output is the + same as that of the input. If `out` is specified, that array is + returned instead. + + See Also + -------- + numpy median + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + if a is None: + raise TypeError("'None' is not suported input to 'median'") + return quantile( + a, + 0.5, + axis=axis, + out=out, + overwrite_input=overwrite_input, + keepdims=keepdims, + method="midpoint", + ) + + +@add_boilerplate("a") +def nanmedian( + a: ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + keepdims: bool = False, +) -> ndarray: + """ + Compute the median along the specified axis, while ignoring NaNs + + Returns the median of the array elements. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + axis : {int, sequence of int, None}, optional + Axis or axes along which the medians are computed. The default, + axis=None, will compute the median along a flattened version of + the array. + If a sequence of axes, the array is first flattened along the + given axes, then the median is computed along the resulting + flattened axis. + out : ndarray, optional + Alternative output array in which to place the result. It must + have the same shape and buffer length as the expected output, + but the type (of the output) will be cast if necessary. + overwrite_input : bool, optional + If True, then allow use of memory of input array `a` for + calculations. The input array will be modified by the call to + `median`. This will save memory when you do not need to preserve + the contents of the input array. Treat the input as undefined, + but it will probably be fully or partially sorted. Default is + False. If `overwrite_input` is ``True`` and `a` is not already an + `ndarray`, an error will be raised. + keepdims : bool, optional + If this is set to True, the axes which are reduced are left + in the result as dimensions with size one. With this option, + the result will broadcast correctly against the original `arr`. + + Returns + ------- + median : ndarray + A new array holding the result. If the input contains integers + or floats smaller than ``float64``, then the output data-type is + ``np.float64``. Otherwise, the data-type of the output is the + same as that of the input. If `out` is specified, that array is + returned instead. + + See Also + -------- + numpy median + + Availability + -------- + Multiple GPUs, Multiple CPUs + + """ + if a is None: + raise TypeError("'None' is not suported input to 'nanmedian'") + return nanquantile( + a, + 0.5, + axis=axis, + out=out, + overwrite_input=overwrite_input, + keepdims=keepdims, + method="midpoint", + ) diff --git a/cupynumeric/_module/stats_correlating.py b/cupynumeric/_module/stats_correlating.py new file mode 100644 index 000000000..ac01d6a2c --- /dev/null +++ b/cupynumeric/_module/stats_correlating.py @@ -0,0 +1,187 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from .array_joining import concatenate +from .creation_data import array +from .creation_shape import empty +from .linalg_mvp import dot +from .math_misc import clip +from .math_sum_prod_diff import sum +from .stats_avgs_vars import average + + +@add_boilerplate("m", "y", "fweights", "aweights") +def cov( + m: ndarray, + y: ndarray | None = None, + rowvar: bool = True, + bias: bool = False, + ddof: int | None = None, + fweights: ndarray | None = None, + aweights: ndarray | None = None, + *, + dtype: np.dtype[Any] | None = None, +) -> ndarray: + """ + Estimate a covariance matrix, given data and weights. + + Covariance indicates the level to which two variables vary together. + If we examine N-dimensional samples, :math:`X = [x_1, x_2, ... x_N]^T`, + then the covariance matrix element :math:`C_{ij}` is the covariance of + :math:`x_i` and :math:`x_j`. The element :math:`C_{ii}` is the variance + of :math:`x_i`. + + Parameters + ---------- + m : array_like + A 1-D or 2-D array containing multiple variables and observations. + Each row of `m` represents a variable, and each column a single + observation of all those variables. Also see `rowvar` below. + y : array_like, optional + An additional set of variables and observations. `y` has the same form + as that of `m`. + rowvar : bool, optional + If `rowvar` is True (default), then each row represents a + variable, with observations in the columns. Otherwise, the relationship + is transposed: each column represents a variable, while the rows + contain observations. + bias : bool, optional + Default normalization (False) is by ``(N - 1)``, where ``N`` is the + number of observations given (unbiased estimate). If `bias` is True, + then normalization is by ``N``. These values can be overridden by using + the keyword ``ddof``. + ddof : int, optional + If not ``None`` the default value implied by `bias` is overridden. + Note that ``ddof=1`` will return the unbiased estimate, even if both + `fweights` and `aweights` are specified, and ``ddof=0`` will return + the simple average. The default value is ``None``. + fweights : array_like, int, optional + 1-D array of integer frequency weights; the number of times each + observation vector should be repeated. + aweights : array_like, optional + 1-D array of observation vector weights. These relative weights are + typically large for observations considered "important" and smaller for + observations considered less "important". If ``ddof=0`` the array of + weights can be used to assign probabilities to observation vectors. + dtype : data-type, optional + Data-type of the result. By default, the return data-type will have + at least `float64` precision. + + Returns + ------- + out : ndarray + The covariance matrix of the variables. + + See Also + -------- + numpy.cov + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # Check inputs + if ddof is not None and not isinstance(ddof, int): + raise ValueError("ddof must be integer") + + # Handles complex arrays too + if m.ndim > 2: + raise ValueError("m has more than 2 dimensions") + + if y is not None and y.ndim > 2: + raise ValueError("y has more than 2 dimensions") + + if dtype is None: + if y is None: + dtype = np.result_type(m.dtype, np.float64) + else: + dtype = np.result_type(m.dtype, y.dtype, np.float64) + + X = array(m, ndmin=2, dtype=dtype) + if not rowvar and X.shape[0] != 1: + X = X.T + if X.shape[0] == 0: + return empty((0, 0)) + if y is not None: + y = array(y, copy=False, ndmin=2, dtype=dtype) + if not rowvar and y.shape[0] != 1: + y = y.T + # TODO(mpapadakis): Could have saved on an intermediate copy of X in + # this case, if it was already of the right shape. + X = concatenate((X, y), axis=0) + + if ddof is None: + if not bias: + ddof = 1 + else: + ddof = 0 + + # Get the product of frequencies and weights + w: ndarray | None = None + if fweights is not None: + if fweights.ndim > 1: + raise RuntimeError("cannot handle multidimensional fweights") + if fweights.shape[0] != X.shape[1]: + raise RuntimeError("incompatible numbers of samples and fweights") + if any(fweights < 0): + raise ValueError("fweights cannot be negative") + w = fweights + if aweights is not None: + if aweights.ndim > 1: + raise RuntimeError("cannot handle multidimensional aweights") + if aweights.shape[0] != X.shape[1]: + raise RuntimeError("incompatible numbers of samples and aweights") + if any(aweights < 0): + raise ValueError("aweights cannot be negative") + if w is None: + w = aweights + else: + # Cannot be done in-place with *= when aweights.dtype != w.dtype + w = w * aweights + + avg, w_sum = average(X, axis=1, weights=w, returned=True) + + # Determine the normalization + fact: ndarray | float = 0.0 + if w is None: + fact = X.shape[1] - ddof + elif ddof == 0: + fact = w_sum + elif aweights is None: + fact = w_sum - ddof + else: + fact = w_sum - ddof * sum(w * aweights) / w_sum + + # TODO(mpapadakis): @add_boilerplate should extend the types of array + # arguments from `ndarray` to `npt.ArrayLike | ndarray`. + fact = clip(fact, 0.0, None) # type: ignore[arg-type] + + X -= avg[:, None] + if w is None: + X_T = X.T + else: + X_T = (X * w).T + c = dot(X, X_T.conj()) + # Cannot be done in-place with /= when the dtypes differ + c = c / fact + + return c.squeeze() diff --git a/cupynumeric/_module/stats_histograms.py b/cupynumeric/_module/stats_histograms.py new file mode 100644 index 000000000..05ab4e928 --- /dev/null +++ b/cupynumeric/_module/stats_histograms.py @@ -0,0 +1,388 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.array import ndarray +from .._array.util import add_boilerplate +from ..types import SortSide +from .creation_data import asarray +from .creation_shape import ones, zeros +from .math_extrema import amax, amin +from .ssc_searching import searchsorted + +if TYPE_CHECKING: + import numpy.typing as npt + +_builtin_max = max +_builtin_range = range + + +@add_boilerplate("x", "weights") +def bincount( + x: ndarray, weights: ndarray | None = None, minlength: int = 0 +) -> ndarray: + """ + bincount(x, weights=None, minlength=0) + + Count number of occurrences of each value in array of non-negative ints. + + The number of bins (of size 1) is one larger than the largest value in + `x`. If `minlength` is specified, there will be at least this number + of bins in the output array (though it will be longer if necessary, + depending on the contents of `x`). + Each bin gives the number of occurrences of its index value in `x`. + If `weights` is specified the input array is weighted by it, i.e. if a + value ``n`` is found at position ``i``, ``out[n] += weight[i]`` instead + of ``out[n] += 1``. + + Parameters + ---------- + x : array_like + 1-D input array of non-negative ints. + weights : array_like, optional + Weights, array of the same shape as `x`. + minlength : int, optional + A minimum number of bins for the output array. + + Returns + ------- + out : ndarray[int] + The result of binning the input array. + The length of `out` is equal to ``cupynumeric.amax(x)+1``. + + Raises + ------ + ValueError + If the input is not 1-dimensional, or contains elements with negative + values, or if `minlength` is negative. + TypeError + If the type of the input is float or complex. + + See Also + -------- + numpy.bincount + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + if x.ndim != 1: + raise ValueError("the input array must be 1-dimensional") + if weights is not None: + if weights.shape != x.shape: + raise ValueError("weights array must be same shape for bincount") + if weights.dtype.kind == "c": + raise ValueError("weights must be convertible to float64") + # Make sure the weights are float64 + weights = weights.astype(np.float64) + if not np.issubdtype(x.dtype, np.integer): + raise TypeError("input array for bincount must be integer type") + if minlength < 0: + raise ValueError("'minlength' must not be negative") + # Note that the following are non-blocking operations, + # though passing their results to `int` is blocking + max_val, min_val = amax(x), amin(x) + if int(min_val) < 0: + raise ValueError("the input array must have no negative elements") + minlength = _builtin_max(minlength, int(max_val) + 1) + if x.size == 1: + # Handle the special case of 0-D array + if weights is None: + out = zeros((minlength,), dtype=np.dtype(np.int64)) + # TODO: Remove this "type: ignore" once @add_boilerplate can + # propagate "ndarray -> ndarray | npt.ArrayLike" in wrapped sigs + out[x[0]] = 1 # type: ignore [assignment] + else: + out = zeros((minlength,), dtype=weights.dtype) + index = x[0] + out[index] = weights[0] + else: + # Normal case of bincount + if weights is None: + out = ndarray( + (minlength,), + dtype=np.dtype(np.int64), + inputs=(x, weights), + ) + out._thunk.bincount(x._thunk) + else: + out = ndarray( + (minlength,), + dtype=weights.dtype, + inputs=(x, weights), + ) + out._thunk.bincount(x._thunk, weights=weights._thunk) + return out + + +@add_boilerplate("x", "weights") +def histogram( + x: ndarray, + bins: ndarray | npt.ArrayLike | int = 10, + range: tuple[int, int] | tuple[float, float] | None = None, + weights: ndarray | None = None, + density: bool = False, +) -> tuple[ndarray, ndarray]: + """ + Compute the histogram of a dataset. + + Parameters + ---------- + a : array_like + Input data. The histogram is computed over the flattened array. + bins : int or sequence of scalars, optional + If `bins` is an int, it defines the number of equal-width bins in the + given range (10, by default). If `bins` is a sequence, it defines a + monotonically increasing array of bin edges, including the rightmost + edge, allowing for non-uniform bin widths. + range : (float, float), optional + The lower and upper range of the bins. If not provided, range is simply + ``(a.min(), a.max())``. Values outside the range are ignored. The first + element of the range must be smaller than the second. This argument is + ignored when bin edges are provided explicitly. + weights : array_like, optional + An array of weights, of the same shape as `a`. Each value in `a` only + contributes its associated weight towards the bin count (instead of 1). + If `density` is True, the weights are normalized, so that the integral + of the density over the range remains 1. + density : bool, optional + If ``False``, the result will contain the number of samples in each + bin. If ``True``, the result is the value of the probability *density* + function at the bin, normalized such that the *integral* over the range + is 1. Note that the sum of the histogram values will not be equal to 1 + unless bins of unity width are chosen; it is not a probability *mass* + function. + + Returns + ------- + hist : array + The values of the histogram. See `density` and `weights` for a + description of the possible semantics. + bin_edges : array + Return the bin edges ``(length(hist)+1)``. + + See Also + -------- + numpy.histogram + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + result_type: np.dtype[Any] = np.dtype(np.int64) + + if np.ndim(bins) > 1: + raise ValueError("`bins` must be 1d, when an array") + + # check isscalar(bins): + # + if np.ndim(bins) == 0: + if not isinstance(bins, int): + raise TypeError("`bins` must be array or integer type") + + num_intervals = bins + + if range is not None: + assert isinstance(range, tuple) and len(range) == 2 + if range[0] >= range[1]: + raise ValueError( + "`range` must be a pair of increasing values." + ) + + lower_b = range[0] + higher_b = range[1] + elif x.size == 0: + lower_b = 0.0 + higher_b = 1.0 + else: + lower_b = float(min(x)) + higher_b = float(max(x)) + + step = (higher_b - lower_b) / num_intervals + + bins_array = asarray( + [lower_b + k * step for k in _builtin_range(0, num_intervals)] + + [higher_b], + dtype=np.dtype(np.float64), + ) + + bins_orig_type = bins_array.dtype + else: + bins_as_arr = asarray(bins) + bins_orig_type = bins_as_arr.dtype + + bins_array = bins_as_arr.astype(np.dtype(np.float64)) + num_intervals = bins_array.shape[0] - 1 + + if not all((bins_array[1:] - bins_array[:-1]) >= 0): + raise ValueError( + "`bins` must increase monotonically, when an array" + ) + + if x.ndim != 1: + x = x.flatten() + + if weights is not None: + if weights.shape != x.shape: + raise ValueError( + "`weights` array must be same shape for histogram" + ) + + result_type = weights.dtype + weights_array = weights.astype(np.dtype(np.float64)) + else: + # case weights == None cannot be handled inside _thunk.histogram, + # bc/ of hist ndarray inputs(), below; + # needs to be handled here: + # + weights_array = ones(x.shape, dtype=np.dtype(np.float64)) + + if x.size == 0: + return ( + zeros((num_intervals,), dtype=result_type), + bins_array.astype(bins_orig_type), + ) + + hist = ndarray( + (num_intervals,), + dtype=weights_array.dtype, + inputs=(x, bins_array, weights_array), + ) + hist._thunk.histogram( + x._thunk, bins_array._thunk, weights=weights_array._thunk + ) + + # handle (density = True): + # + if density: + result_type = np.dtype(np.float64) + hist /= sum(hist) + hist /= bins_array[1:] - bins_array[:-1] + + return hist.astype(result_type), bins_array.astype(bins_orig_type) + + +@add_boilerplate("x", "bins") +def digitize( + x: ndarray, + bins: ndarray, + right: bool = False, +) -> ndarray | int: + """ + Return the indices of the bins to which each value in input array belongs. + + ========= ============= ============================ + `right` order of bins returned index `i` satisfies + ========= ============= ============================ + ``False`` increasing ``bins[i-1] <= x < bins[i]`` + ``True`` increasing ``bins[i-1] < x <= bins[i]`` + ``False`` decreasing ``bins[i-1] > x >= bins[i]`` + ``True`` decreasing ``bins[i-1] >= x > bins[i]`` + ========= ============= ============================ + + If values in `x` are beyond the bounds of `bins`, 0 or ``len(bins)`` is + returned as appropriate. + + Parameters + ---------- + x : array_like + Input array to be binned. Doesn't need to be 1-dimensional. + bins : array_like + Array of bins. It has to be 1-dimensional and monotonic. + right : bool, optional + Indicating whether the intervals include the right or the left bin + edge. Default behavior is (right==False) indicating that the interval + does not include the right edge. The left bin end is open in this + case, i.e., bins[i-1] <= x < bins[i] is the default behavior for + monotonically increasing bins. + + Returns + ------- + indices : ndarray of ints + Output array of indices, of same shape as `x`. + + Raises + ------ + ValueError + If `bins` is not monotonic. + TypeError + If the type of the input is complex. + + See Also + -------- + numpy.digitize + + Notes + ----- + If values in `x` are such that they fall outside the bin range, + attempting to index `bins` with the indices that `digitize` returns + will result in an IndexError. + For monotonically *increasing* `bins`, the following are equivalent:: + + np.digitize(x, bins, right=True) + np.searchsorted(bins, x, side='left') + + Note that as the order of the arguments are reversed, the side must be too. + The `searchsorted` call is marginally faster, as it does not do any + monotonicity checks. Perhaps more importantly, it supports all dtypes. + + Examples + -------- + >>> x = np.array([0.2, 6.4, 3.0, 1.6]) + >>> bins = np.array([0.0, 1.0, 2.5, 4.0, 10.0]) + >>> inds = np.digitize(x, bins) + >>> inds + array([1, 4, 3, 2]) + >>> for n in range(x.size): + ... print(bins[inds[n]-1], "<=", x[n], "<", bins[inds[n]]) + ... + 0.0 <= 0.2 < 1.0 + 4.0 <= 6.4 < 10.0 + 2.5 <= 3.0 < 4.0 + 1.0 <= 1.6 < 2.5 + >>> x = np.array([1.2, 10.0, 12.4, 15.5, 20.]) + >>> bins = np.array([0, 5, 10, 15, 20]) + >>> np.digitize(x,bins,right=True) + array([1, 2, 3, 4, 4]) + >>> np.digitize(x,bins,right=False) + array([1, 3, 3, 4, 5]) + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + # here for compatibility, searchsorted below is happy to take this + if np.issubdtype(x.dtype, np.complexfloating): + raise TypeError("x may not be complex") + + if bins.ndim > 1: + raise ValueError("bins must be one-dimensional") + + increasing = (bins[1:] >= bins[:-1]).all() + decreasing = (bins[1:] <= bins[:-1]).all() + if not increasing and not decreasing: + raise ValueError("bins must be monotonically increasing or decreasing") + + # this is backwards because the arguments below are swapped + side: SortSide = "left" if right else "right" + if decreasing: + # reverse the bins, and invert the results + return len(bins) - searchsorted(bins.flip(), x, side=side) + else: + return searchsorted(bins, x, side=side) diff --git a/cupynumeric/_module/stats_order.py b/cupynumeric/_module/stats_order.py new file mode 100644 index 000000000..7d7564a3d --- /dev/null +++ b/cupynumeric/_module/stats_order.py @@ -0,0 +1,1063 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, Any, Iterable, Sequence + +import numpy as np + +from .._utils import is_np2 + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import ( # type: ignore + normalize_axis_tuple, + ) + +from .._array.util import add_boilerplate +from .._ufunc.comparison import logical_not +from .._ufunc.floating import isnan +from .array_transpose import moveaxis +from .creation_data import asarray +from .creation_shape import zeros +from .ssc_counting import count_nonzero +from .ssc_searching import where +from .ssc_sorting import sort + +if TYPE_CHECKING: + from typing import Callable + + import numpy.typing as npt + + from .._array.array import ndarray + + +# for the case when axis = tuple (non-singleton) +# reshuffling might have to be done (if tuple is non-consecutive) +# and the src array must be collapsed along that set of axes +# +# args: +# +# arr: [in] source nd-array on which quantiles are calculated; +# axes_set: [in] tuple or list of axes (indices less than arr dimension); +# +# return: pair: (minimal_index, reshuffled_and_collapsed source array) +def _reshuffle_reshape( + arr: ndarray, axes_set: Sequence[int] +) -> tuple[int, ndarray]: + ndim = len(arr.shape) + + sorted_axes = tuple(sorted(axes_set)) + + min_dim_index = sorted_axes[0] + num_axes = len(sorted_axes) + reshuffled_axes = tuple(range(min_dim_index, min_dim_index + num_axes)) + + non_consecutive = sorted_axes != reshuffled_axes + if non_consecutive: + arr_shuffled = moveaxis(arr, sorted_axes, reshuffled_axes) + else: + arr_shuffled = arr + + # shape_reshuffled = arr_shuffled.shape # debug + collapsed_shape = np.prod([arr_shuffled.shape[i] for i in reshuffled_axes]) + + redimed = tuple(range(0, min_dim_index + 1)) + tuple( + range(min_dim_index + num_axes, ndim) + ) + reshaped = tuple( + [ + collapsed_shape if k == min_dim_index else arr_shuffled.shape[k] + for k in redimed + ] + ) + + arr_reshaped = arr_shuffled.reshape(reshaped) + return (min_dim_index, arr_reshaped) + + +# Define the gamma and index position for each of the distributions based +# on the paper/NumPy definition. +# +# `pos` is the "virtual index" at which we wish to sample, this is adjusted +# based on the alpha and beta parameters of the methods (which adjust for +# the fact that the distribution is sampled). +# +# `gamma` is weight for each the samples taken into account. Some methods +# are non-interpolating. `gamma` may be calculated to pick a side, but +# we forward `None` to indicate the non-interpolating nature of the method +# (the result dtype is for example identical to the input one). +# +# `pos` is (to keep with the paper) 1-based index, thus we always subtract 1 +# in the following step. + +# Discontinuous methods: + + +# q = quantile input \in [0, 1] +# n = sizeof(array) +# pos = virtual index (often 1 based, to keep with paper) +def _inverted_cdf(q: float, n: int) -> tuple[float, int]: + pos = q * n + left = int(pos) + + g = pos - left + gamma = 1.0 if g > 0 else 0.0 + + return (gamma, left - 1) + + +def _averaged_inverted_cdf(q: float, n: int) -> tuple[float, int]: + pos = q * n + left = int(pos) + + g = pos - left + gamma = 1.0 if g > 0 else 0.5 + + return (gamma, left - 1) + + +_desired_mod_2: int = int(np.lib.NumpyVersion(np.__version__) < "2.0.1") + + +def _closest_observation(q: float, n: int) -> tuple[None, int]: + pos = q * n - 0.5 + left = int(pos) + + # The calculation is done in a way that we should to take the next index + # (gamme = 1) except if we hit it exactly. + # If we do, we use round-to-even: The final index `left + gamma` should + # be an even number. But on older versions of numpy this was an odd + # number (due to 0 based vs. 1 based indexing used in the rounding). + if left != pos: + gamma = 1 + elif left % 2 != _desired_mod_2: + gamma = 1 + else: + gamma = 0 + + return (None, left - 1 + gamma) + + +# Continuous methods: + + +# Parzen method +def _interpolated_inverted_cdf(q: float, n: int) -> tuple[float, int]: + pos = q * n + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +# Hazen method +def _hazen(q: float, n: int) -> tuple[float, int]: + pos = q * n + 0.5 + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +# Weibull method +def _weibull(q: float, n: int) -> tuple[float, int]: + pos = q * (n + 1) + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +# Gumbel method +def _linear(q: float, n: int) -> tuple[float, int]: + pos = q * (n - 1) + 1 + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +# Johnson & Kotz method +def _median_unbiased(q: float, n: int) -> tuple[float, int]: + fract = 1.0 / 3.0 + pos = q * (n + fract) + fract + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +# Blom method +def _normal_unbiased(q: float, n: int) -> tuple[float, int]: + fract1 = 0.25 + fract2 = 3.0 / 8.0 + pos = q * (n + fract1) + fract2 + left = int(pos) + + gamma = pos - left + return (gamma, left - 1) + + +def _lower(q: float, n: int) -> tuple[None, int]: + pos = q * (n - 1) # 0 based here + left = int(pos) + return (None, left) + + +def _higher(q: float, n: int) -> tuple[None, int]: + pos = q * (n - 1) # 0 based here + left = int(math.ceil(pos)) + return (None, left) + + +def _midpoint(q: float, n: int) -> tuple[float, int]: + pos = q * (n - 1) # 0 based here + left = int(pos) + # Mid-point, unless pos is exact then we use that point. + gamma = 0.5 if pos != left else 0.0 + + return (gamma, left) + + +def _nearest(q: float, n: int) -> tuple[None, int]: + pos = np.round(q * (n - 1)) # 0 based here + left = int(pos) + + return (None, left) + + +# args: +# +# arr: [in] source nd-array on which quantiles are calculated; +# preccondition: assumed sorted! +# q_arr: [in] quantile input values nd-array; +# axis: [in] axis along which quantiles are calculated; +# method: [in] func(q, n) returning (gamma, j), +# where = array1D.size; +# keepdims: [in] boolean flag specifying whether collapsed axis +# should be kept as dim=1; +# to_dtype: [in] dtype to convert the result to; +# qs_all: [in/out] result pass through or created (returned) +# +def _quantile_impl( + arr: ndarray, + q_arr: npt.NDArray[Any], + axis: int | None, + axes_set: Sequence[int], + original_shape: tuple[int, ...], + method: Callable[[float, int], tuple[float | None, int]], + keepdims: bool, + to_dtype: np.dtype[Any], + qs_all: ndarray | None, +) -> ndarray: + ndims = len(arr.shape) + + if axis is None: + n = arr.size + + if keepdims: + remaining_shape = (1,) * len(original_shape) + else: + remaining_shape = () # only `q_arr` dictates shape; + # quantile applied to `arr` seen as 1D; + else: + n = arr.shape[axis] + + # arr.shape -{axis}; if keepdims use 1 for arr.shape[axis]: + # (can be empty []) + # + if keepdims: + remaining_shape = tuple( + 1 if k in axes_set else original_shape[k] + for k in range(0, len(original_shape)) + ) + else: + remaining_shape = tuple( + arr.shape[k] for k in range(0, ndims) if k != axis + ) + + # compose qarr.shape with arr.shape: + # + # result.shape = (q_arr.shape, arr.shape -{axis}): + # + qresult_shape = (*q_arr.shape, *remaining_shape) + + # construct result NdArray, non-flattening approach: + # + if qs_all is None: + qs_all = zeros(qresult_shape, dtype=to_dtype) + else: + # implicit conversion from to_dtype to qs_all.dtype assumed + # + if qs_all.shape != qresult_shape: + raise ValueError("wrong shape on output array") + + for index, q in np.ndenumerate(q_arr): + gamma, left_pos = method(q, n) + # Note that gamma may be None, in which case `right_pos` has no + # meaning since use the exact index. + right_pos = left_pos + 1 + + # The virtual pos, which was used to calculate `left`, can be outside + # the range, so fix all indices to be in range here. + if left_pos >= n - 1: + left_pos = right_pos = n - 1 + elif left_pos < 0: + left_pos = right_pos = 0 + + # If gamma is None, we only have to extract the correct values + if gamma is None: + qs_all[index] = arr.take(left_pos, axis).reshape(remaining_shape) + else: + # (N-1) dimensional ndarray of left, right + # neighbor values: + # + # non-flattening approach: + # + # extract values at left and right position; + arr_1D_lvals = arr.take(left_pos, axis).reshape(remaining_shape) + arr_1D_rvals = arr.take(right_pos, axis).reshape(remaining_shape) + + # TODO: We may want to use a more precise interpolation formula + # like NumPy here (or implement an `lerp` function to use). + # + # vectorized for axis != None; + # (non-flattening approach) + left = (1.0 - gamma) * arr_1D_lvals + right = gamma * arr_1D_rvals + qs_all[index] = left + right + + return qs_all + + +_ORDER_FUNCS = { + "inverted_cdf": _inverted_cdf, + "averaged_inverted_cdf": _averaged_inverted_cdf, + "closest_observation": _closest_observation, + "interpolated_inverted_cdf": _interpolated_inverted_cdf, + "hazen": _hazen, + "weibull": _weibull, + "linear": _linear, + "median_unbiased": _median_unbiased, + "normal_unbiased": _normal_unbiased, + "lower": _lower, + "higher": _higher, + "midpoint": _midpoint, + "nearest": _nearest, +} + + +@add_boilerplate("a") +def quantile( + a: ndarray, + q: float | Iterable[float] | ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + method: str = "linear", + keepdims: bool = False, +) -> ndarray: + """ + Compute the q-th quantile of the data along the specified axis. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + q : array_like of float + Quantile or sequence of quantiles to compute, which must be between + 0 and 1 inclusive. + axis : {int, tuple of int, None}, optional + Axis or axes along which the quantiles are computed. The default is + to compute the quantile(s) along a flattened version of the array. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output. + overwrite_input : bool, optional + If True, then allow the input array `a` to be modified by + intermediate calculations, to save memory. In this case, the + contents of the input `a` after this function completes is + undefined. + method : str, optional + This parameter specifies the method to use for estimating the + quantile. The options sorted by their R type + as summarized in the H&F paper [1]_ are: + 1. 'inverted_cdf' + 2. 'averaged_inverted_cdf' + 3. 'closest_observation' + 4. 'interpolated_inverted_cdf' + 5. 'hazen' + 6. 'weibull' + 7. 'linear' (default) + 8. 'median_unbiased' + 9. 'normal_unbiased' + The first three methods are discontinuous. NumPy further defines the + following discontinuous variations of the default 'linear' (7.) option: + * 'lower' + * 'higher', + * 'midpoint' + * 'nearest' + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in + the result as dimensions with size one. With this option, the + result will broadcast correctly against the original array `a`. + + Returns + ------- + quantile : scalar or ndarray + If `q` is a single quantile and `axis=None`, then the result + is a scalar. If multiple quantiles are given, first axis of + the result corresponds to the quantiles. The other axes are + the axes that remain after the reduction of `a`. If the input + contains integers or floats smaller than ``float64``, the output + data-type is ``float64``. Otherwise, the output data-type is the + same as that of the input. If `out` is specified, that array is + returned instead. + + Raises + ------ + TypeError + If the type of the input is complex. + + See Also + -------- + numpy.quantile + + Availability + -------- + Multiple GPUs, Multiple CPUs + + References + ---------- + .. [1] R. J. Hyndman and Y. Fan, + "Sample quantiles in statistical packages," + The American Statistician, 50(4), pp. 361-365, 1996 + """ + + real_axis: int | None + axes_set: Sequence[int] = () + original_shape = a.shape + + if axis is not None and isinstance(axis, Iterable): + nrm_axis = normalize_axis_tuple(axis, a.ndim) + if len(axis) == 1: + real_axis = nrm_axis[0] + a_rr = a + else: + # reshuffling requires non-negative axes: + (real_axis, a_rr) = _reshuffle_reshape(a, nrm_axis) + # What happens with multiple axes and overwrite_input = True ? + # It seems overwrite_input is reset to False; + overwrite_input = False + axes_set = nrm_axis + else: + real_axis = axis + a_rr = a + if real_axis is not None: + axes_set = normalize_axis_tuple(real_axis, a.ndim) + real_axis = axes_set[0] + + # covers both array-like and scalar cases: + # + q_arr = np.asarray(q) + + # in the future k-sort (partition) + # might be faster, for now it uses sort + # arr = partition(arr, k = floor(nq), axis = real_axis) + # but that would require a k-sort call for each `q`! + # too expensive for many `q` values... + # if no axis given then elements are sorted as a 1D array + # + if overwrite_input: + a_rr.sort(axis=real_axis) + arr = a_rr + else: + arr = sort(a_rr, axis=real_axis) + + if arr.dtype.kind == "c": + raise TypeError("input array cannot be of complex type") + + # return type dependency on arr.dtype: + # + # it depends on interpolation method; + # For discontinuous methods returning either end of the interval within + # which the quantile falls, or the other; arr.dtype is returned; + # else, logic below: + # + # if is_float(arr_dtype) && (arr.dtype >= dtype('float64')) then + # arr.dtype + # else + # dtype('float64') + # + # see https://github.com/numpy/numpy/issues/22323 + # + if method in [ + "inverted_cdf", + "closest_observation", + "lower", + "higher", + "nearest", + ]: + to_dtype = arr.dtype + else: + to_dtype = np.dtype("float64") + + # in case dtype("float128") becomes supported: + # + # to_dtype = ( + # arr.dtype + # if (arr.dtype == np.dtype("float128")) + # else np.dtype("float64") + # ) + + res = _quantile_impl( + arr, + q_arr, + real_axis, + axes_set, + original_shape, + _ORDER_FUNCS[method], + keepdims, + to_dtype, + out, + ) + + if out is not None: + # out = res.astype(out.dtype) -- conversion done inside impl + return out + else: + return res + + +@add_boilerplate("a") +def percentile( + a: ndarray, + q: float | Iterable[float] | ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + method: str = "linear", + keepdims: bool = False, +) -> ndarray: + """ + Compute the q-th percentile of the data along the specified axis. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array. + q : array_like of float + Percentile or sequence of percentiles to compute, which must be between + 0 and 100 inclusive. + axis : {int, tuple of int, None}, optional + Axis or axes along which the percentiles are computed. The default is + to compute the percentile(s) along a flattened version of the array. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output. + overwrite_input : bool, optional + If True, then allow the input array `a` to be modified by + intermediate calculations, to save memory. In this case, the + contents of the input `a` after this function completes is + undefined. + method : str, optional + This parameter specifies the method to use for estimating the + percentile. The options sorted by their R type + as summarized in the H&F paper [1]_ are: + 1. 'inverted_cdf' + 2. 'averaged_inverted_cdf' + 3. 'closest_observation' + 4. 'interpolated_inverted_cdf' + 5. 'hazen' + 6. 'weibull' + 7. 'linear' (default) + 8. 'median_unbiased' + 9. 'normal_unbiased' + The first three methods are discontinuous. NumPy further defines the + following discontinuous variations of the default 'linear' (7.) option: + * 'lower' + * 'higher', + * 'midpoint' + * 'nearest' + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in + the result as dimensions with size one. With this option, the + result will broadcast correctly against the original array `a`. + + Returns + ------- + percentile : scalar or ndarray + If `q` is a single percentile and `axis=None`, then the result + is a scalar. If multiple percentiles are given, first axis of + the result corresponds to the percentiles. The other axes are + the axes that remain after the reduction of `a`. If the input + contains integers or floats smaller than ``float64``, the output + data-type is ``float64``. Otherwise, the output data-type is the + same as that of the input. If `out` is specified, that array is + returned instead. + + Raises + ------ + TypeError + If the type of the input is complex. + + See Also + -------- + numpy.percentile + + Availability + -------- + Multiple GPUs, Multiple CPUs + + References + ---------- + .. [1] R. J. Hyndman and Y. Fan, + "Sample quantiles in statistical packages," + The American Statistician, 50(4), pp. 361-365, 1996 + """ + + q_arr = np.asarray(q) + q01 = q_arr / 100.0 + + return quantile( + a, + q01, + axis, + out=out, + overwrite_input=overwrite_input, + method=method, + keepdims=keepdims, + ) + + +# args: +# +# arr: [in] source nd-array on which quantiles are calculated; +# NaNs ignored; precondition: assumed sorted! +# q_arr: [in] quantile input values nd-array; +# axis: [in] axis along which quantiles are calculated; +# method: [in] func(q, n) returning (gamma, j), +# where = array1D.size; +# keepdims: [in] boolean flag specifying whether collapsed axis +# should be kept as dim=1; +# to_dtype: [in] dtype to convert the result to; +# qs_all: [in/out] result pass through or created (returned) +# +def nanquantile_impl( + arr: ndarray, + q_arr: npt.NDArray[Any], + non_nan_counts: ndarray, + axis: int | None, + axes_set: Sequence[int], + original_shape: tuple[int, ...], + method: Callable[[float, int], tuple[float | None, int]], + keepdims: bool, + to_dtype: np.dtype[Any], + qs_all: ndarray | None, +) -> ndarray: + ndims = len(arr.shape) + + if axis is None: + if keepdims: + remaining_shape = (1,) * len(original_shape) + else: + remaining_shape = () # only `q_arr` dictates shape; + # quantile applied to `arr` seen as 1D; + else: + # arr.shape -{axis}; if keepdims use 1 for arr.shape[axis]: + # (can be empty []) + # + if keepdims: + remaining_shape = tuple( + 1 if k in axes_set else original_shape[k] + for k in range(0, len(original_shape)) + ) + else: + remaining_shape = tuple( + arr.shape[k] for k in range(0, ndims) if k != axis + ) + + # compose qarr.shape with arr.shape: + # + # result.shape = (q_arr.shape, arr.shape -{axis}): + # + qresult_shape = (*q_arr.shape, *remaining_shape) + + # construct result Ndarray, non-flattening approach: + # + if qs_all is None: + qs_all = zeros(qresult_shape, dtype=to_dtype) + else: + # implicit conversion from to_dtype to qs_all.dtype assumed + # + if qs_all.shape != qresult_shape: + raise ValueError("wrong shape on output array") + + assert non_nan_counts.shape == remaining_shape + + arr_gammas = zeros(remaining_shape, dtype=arr.dtype) + arr_lvals = zeros(remaining_shape, dtype=arr.dtype) + arr_rvals = zeros(remaining_shape, dtype=arr.dtype) + + # Similar to the non-nan implementation except that it needs to make + # `n` depend on the number of non-nan-counts. + for qindex, q in np.ndenumerate(q_arr): + assert qs_all[qindex].shape == remaining_shape + + # TODO(aschaffer): Vectorize this operation, see + # github.com/nv-legate/cupynumeric/pull/1121#discussion_r1484731763 + gamma = None + for aindex, n in np.ndenumerate(non_nan_counts): + # TODO (2024-08): `n` should be an integral type, but wasn't: + n = int(n) + if n == 0: + # Cannot define a quantile over an empty range, return NaN + # TODO(mpapadakis): mypy mysteriously complains that + # expression has type "float", target has type "ndarray" + arr_lvals[aindex] = np.nan # type: ignore[assignment] + arr_rvals[aindex] = np.nan # type: ignore[assignment] + continue + + gamma, left_pos = method(q, n) + + right_pos = left_pos + 1 + if left_pos >= n - 1: + left_pos = right_pos = n - 1 + elif left_pos < 0: + left_pos = right_pos = 0 + + # assumption: since `non_nan_counts` has the same + # shape as `remaining_shape` (checked above), + # `aindex` are the same indices as those needed + # to access `a`'s remaining shape slices; + # + full_l_index = (*aindex[:axis], left_pos, *aindex[axis:]) + arr_lvals[aindex] = arr[full_l_index] + if gamma is not None: + # TODO(mpapadakis): As above, mypy complains about assignment + arr_gammas[aindex] = gamma # type: ignore[assignment] + + full_r_index = (*aindex[:axis], right_pos, *aindex[axis:]) + arr_rvals[aindex] = arr[full_r_index] + + if gamma is None: + # Note that gamma can only be always None or never + qs_all[qindex] = arr_lvals + else: + left = (1 - arr_gammas) * arr_lvals + right = arr_gammas * arr_rvals + qs_all[qindex] = left + right + + return qs_all + + +@add_boilerplate("a") +def nanquantile( + a: ndarray, + q: float | Iterable[float] | ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + method: str = "linear", + keepdims: bool = False, +) -> ndarray: + """ + Compute the q-th quantile of the data along the specified axis, + while ignoring nan values. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array, + containing nan values to be ignored. + q : array_like of float + Quantile or sequence of quantiles to compute, which must be between + 0 and 1 inclusive. + axis : {int, tuple of int, None}, optional + Axis or axes along which the quantiles are computed. The default is + to compute the quantile(s) along a flattened version of the array. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output. + overwrite_input : bool, optional + If True, then allow the input array `a` to be modified by + intermediate calculations, to save memory. In this case, the + contents of the input `a` after this function completes is + undefined. + method : str, optional + This parameter specifies the method to use for estimating the + quantile. The options sorted by their R type + as summarized in the H&F paper [1]_ are: + 1. 'inverted_cdf' + 2. 'averaged_inverted_cdf' + 3. 'closest_observation' + 4. 'interpolated_inverted_cdf' + 5. 'hazen' + 6. 'weibull' + 7. 'linear' (default) + 8. 'median_unbiased' + 9. 'normal_unbiased' + The first three methods are discontinuous. NumPy further defines the + following discontinuous variations of the default 'linear' (7.) option: + * 'lower' + * 'higher', + * 'midpoint' + * 'nearest' + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in + the result as dimensions with size one. With this option, the + result will broadcast correctly against the original array `a`. + + Returns + ------- + quantile : scalar or ndarray + If `q` is a single quantile and `axis=None`, then the result + is a scalar. If multiple quantiles are given, first axis of + the result corresponds to the quantiles. The other axes are + the axes that remain after the reduction of `a`. If the input + contains integers or floats smaller than ``float64``, the output + data-type is ``float64``. Otherwise, the output data-type is the + same as that of the input. If `out` is specified, that array is + returned instead. + + Raises + ------ + TypeError + If the type of the input is complex. + + See Also + -------- + numpy.nanquantile + + Availability + -------- + Multiple GPUs, Multiple CPUs + + References + ---------- + .. [1] R. J. Hyndman and Y. Fan, + "Sample quantiles in statistical packages," + The American Statistician, 50(4), pp. 361-365, 1996 + """ + + real_axis: int | None + axes_set: Sequence[int] = () + original_shape = a.shape + + if axis is not None and isinstance(axis, Iterable): + nrm_axis = normalize_axis_tuple(axis, a.ndim) + if len(axis) == 1: + real_axis = nrm_axis[0] + a_rr = a + else: + (real_axis, a_rr) = _reshuffle_reshape(a, nrm_axis) + # What happens with multiple axes and overwrite_input = True ? + # It seems overwrite_input is reset to False; + # But `overwrite_input` doesn't matter for the NaN version of this + # function + # overwrite_input = False + axes_set = nrm_axis + else: + real_axis = axis + a_rr = a + if real_axis is not None: + axes_set = normalize_axis_tuple(real_axis, a.ndim) + real_axis = axes_set[0] + + # ndarray of non-NaNs: + # + non_nan_counts = asarray( + count_nonzero( + logical_not(isnan(a_rr)), + axis=real_axis, + ) + ) + + # covers both array-like and scalar cases: + # + q_arr = np.asarray(q) + + # in the future k-sort (partition) + # might be faster, for now it uses sort + # arr = partition(arr, k = floor(nq), axis = real_axis) + # but that would require a k-sort call for each `q`! + # too expensive for many `q` values... + # if no axis given then elements are sorted as a 1D array + # + # replace NaN's by dtype.max: + # + arr = where(isnan(a_rr), np.finfo(a_rr.dtype).max, a_rr) + arr.sort(axis=real_axis) + + if arr.dtype.kind == "c": + raise TypeError("input array cannot be of complex type") + + # return type dependency on arr.dtype: + # + # it depends on interpolation method; + # For discontinuous methods returning either end of the interval within + # which the quantile falls, or the other; arr.dtype is returned; + # else, logic below: + # + # if is_float(arr_dtype) && (arr.dtype >= dtype('float64')) then + # arr.dtype + # else + # dtype('float64') + # + # see https://github.com/numpy/numpy/issues/22323 + # + if method in [ + "inverted_cdf", + "closest_observation", + "lower", + "higher", + "nearest", + ]: + to_dtype = arr.dtype + else: + to_dtype = np.dtype("float64") + + # in case dtype("float128") becomes supported: + # + # to_dtype = ( + # arr.dtype + # if (arr.dtype == np.dtype("float128")) + # else np.dtype("float64") + # ) + + res = nanquantile_impl( + arr, + q_arr, + non_nan_counts, + real_axis, + axes_set, + original_shape, + _ORDER_FUNCS[method], + keepdims, + to_dtype, + out, + ) + + if out is not None: + # out = res.astype(out.dtype) -- conversion done inside impl + return out + else: + return res + + +@add_boilerplate("a") +def nanpercentile( + a: ndarray, + q: float | Iterable[float] | ndarray, + axis: int | tuple[int, ...] | None = None, + out: ndarray | None = None, + overwrite_input: bool = False, + method: str = "linear", + keepdims: bool = False, +) -> ndarray: + """ + Compute the q-th percentile of the data along the specified axis, + while ignoring nan values. + + Parameters + ---------- + a : array_like + Input array or object that can be converted to an array, + containing nan values to be ignored. + q : array_like of float + Percentile or sequence of percentiles to compute, which must be between + 0 and 100 inclusive. + axis : {int, tuple of int, None}, optional + Axis or axes along which the percentiles are computed. The default is + to compute the percentile(s) along a flattened version of the array. + out : ndarray, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output. + overwrite_input : bool, optional + If True, then allow the input array `a` to be modified by + intermediate calculations, to save memory. In this case, the + contents of the input `a` after this function completes is + undefined. + method : str, optional + This parameter specifies the method to use for estimating the + percentile. The options sorted by their R type + as summarized in the H&F paper [1]_ are: + 1. 'inverted_cdf' + 2. 'averaged_inverted_cdf' + 3. 'closest_observation' + 4. 'interpolated_inverted_cdf' + 5. 'hazen' + 6. 'weibull' + 7. 'linear' (default) + 8. 'median_unbiased' + 9. 'normal_unbiased' + The first three methods are discontinuous. NumPy further defines the + following discontinuous variations of the default 'linear' (7.) option: + * 'lower' + * 'higher', + * 'midpoint' + * 'nearest' + keepdims : bool, optional + If this is set to True, the axes which are reduced are left in + the result as dimensions with size one. With this option, the + result will broadcast correctly against the original array `a`. + + Returns + ------- + percentile : scalar or ndarray + If `q` is a single percentile and `axis=None`, then the result + is a scalar. If multiple percentiles are given, first axis of + the result corresponds to the percentiles. The other axes are + the axes that remain after the reduction of `a`. If the input + contains integers or floats smaller than ``float64``, the output + data-type is ``float64``. Otherwise, the output data-type is the + same as that of the input. If `out` is specified, that array is + returned instead. + + Raises + ------ + TypeError + If the type of the input is complex. + + See Also + -------- + numpy.nanpercentile + + Availability + -------- + Multiple GPUs, Multiple CPUs + + References + ---------- + .. [1] R. J. Hyndman and Y. Fan, + "Sample quantiles in statistical packages," + The American Statistician, 50(4), pp. 361-365, 1996 + """ + + q_arr = np.asarray(q) + q01 = q_arr / 100.0 + + return nanquantile( + a, + q01, + axis, + out=out, + overwrite_input=overwrite_input, + method=method, + keepdims=keepdims, + ) diff --git a/cunumeric/window.py b/cupynumeric/_module/window.py similarity index 96% rename from cunumeric/window.py rename to cupynumeric/_module/window.py index 9f25f7e03..366f772e2 100644 --- a/cunumeric/window.py +++ b/cupynumeric/_module/window.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ import numpy as np -from .array import ndarray -from .config import WindowOpCode -from .module import empty, ones +from .._array.array import ndarray +from ..config import WindowOpCode +from .creation_shape import empty, ones def _create_window(M: int, op_code: WindowOpCode, *args: Any) -> ndarray: diff --git a/cunumeric/_sphinxext/__init__.py b/cupynumeric/_sphinxext/__init__.py similarity index 95% rename from cunumeric/_sphinxext/__init__.py rename to cupynumeric/_sphinxext/__init__.py index e6a7812ea..ddfbe0329 100644 --- a/cunumeric/_sphinxext/__init__.py +++ b/cupynumeric/_sphinxext/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cunumeric/_sphinxext/_comparison_config.py b/cupynumeric/_sphinxext/_comparison_config.py similarity index 98% rename from cunumeric/_sphinxext/_comparison_config.py rename to cupynumeric/_sphinxext/_comparison_config.py index dc2f71d6b..3623a61a0 100644 --- a/cunumeric/_sphinxext/_comparison_config.py +++ b/cupynumeric/_sphinxext/_comparison_config.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -136,7 +136,6 @@ class SectionConfig: "asarray_chkfinite", "asarray", "ascontiguousarray", - "asfarray", "asfortranarray", "asmatrix", "atleast_1d", @@ -218,7 +217,6 @@ class SectionConfig: "identity", "linspace", "logspace", - "mat", "meshgrid", "ones_like", "ones", @@ -252,7 +250,6 @@ class SectionConfig: "savez_compressed", "savez", "set_printoptions", - "set_string_function", ) IO_ND = ("tofile", "tolist") @@ -283,7 +280,6 @@ class SectionConfig: "prod", "real_if_close", "real", - "round_", "sinc", "sum", "trapz", @@ -300,7 +296,6 @@ class SectionConfig: "extract", "flatnonzero", "lexsort", - "msort", "nanargmax", "nanargmin", "nonzero", diff --git a/cunumeric/_sphinxext/_comparison_util.py b/cupynumeric/_sphinxext/_comparison_util.py similarity index 87% rename from cunumeric/_sphinxext/_comparison_util.py rename to cupynumeric/_sphinxext/_comparison_util.py index 8e2adc6a8..a76d1cf67 100644 --- a/cunumeric/_sphinxext/_comparison_util.py +++ b/cupynumeric/_sphinxext/_comparison_util.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ from dataclasses import dataclass from types import ModuleType -from typing import TYPE_CHECKING, Any, Iterable, Iterator, Type, Union +from typing import TYPE_CHECKING, Any, Iterable, Iterator, Type -from ..coverage import is_implemented, is_multi, is_single +from .._utils.coverage import is_implemented, is_multi, is_single from ._comparison_config import MISSING_NP_REFS, SKIP if TYPE_CHECKING: @@ -66,7 +66,7 @@ def _lgref(name: str, obj: Any, implemented: bool) -> str: if isinstance(obj, ModuleType): full_name = f"{obj.__name__}.{name}" else: - full_name = f"cunumeric.{obj.__name__}.{name}" + full_name = f"cupynumeric.{obj.__name__}.{name}" role = "meth" if "ndarray" in full_name else "obj" @@ -75,7 +75,7 @@ def _lgref(name: str, obj: Any, implemented: bool) -> str: def filter_names( obj: Any, - types: Union[tuple[Type[Any], ...], None] = None, + types: tuple[Type[Any], ...] | None = None, skip: Iterable[str] = (), ) -> Iterator[str]: names = (n for n in dir(obj)) # every name in the module or class @@ -106,15 +106,15 @@ def get_item(name: str, np_obj: Any, lg_obj: Any) -> ItemDetail: ) -def get_namespaces(attr: Union[str, None]) -> tuple[Any, Any]: +def get_namespaces(attr: str | None) -> tuple[Any, Any]: import numpy - import cunumeric + import cupynumeric if attr is None: - return numpy, cunumeric + return numpy, cupynumeric - return getattr(numpy, attr), getattr(cunumeric, attr) + return getattr(numpy, attr), getattr(cupynumeric, attr) def generate_section(config: SectionConfig) -> SectionDetail: diff --git a/cunumeric/_sphinxext/_cunumeric_directive.py b/cupynumeric/_sphinxext/_cupynumeric_directive.py similarity index 86% rename from cunumeric/_sphinxext/_cunumeric_directive.py rename to cupynumeric/_sphinxext/_cupynumeric_directive.py index ef6402f6c..593d25b24 100644 --- a/cunumeric/_sphinxext/_cunumeric_directive.py +++ b/cupynumeric/_sphinxext/_cupynumeric_directive.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,14 +15,14 @@ from __future__ import annotations from docutils import nodes -from docutils.statemachine import ViewList +from docutils.statemachine import StringList from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import nested_parse_with_titles -class CunumericDirective(SphinxDirective): +class CupynumericDirective(SphinxDirective): def parse(self, rst_text: str, annotation: str) -> list[nodes.Node]: - result = ViewList() + result = StringList() for line in rst_text.split("\n"): result.append(line, annotation) node = nodes.paragraph() diff --git a/cunumeric/_sphinxext/_templates.py b/cupynumeric/_sphinxext/_templates.py similarity index 95% rename from cunumeric/_sphinxext/_templates.py rename to cupynumeric/_sphinxext/_templates.py index e19316ee4..39aa3a002 100644 --- a/cunumeric/_sphinxext/_templates.py +++ b/cupynumeric/_sphinxext/_templates.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cunumeric/_sphinxext/_templates/comparison_table.rst b/cupynumeric/_sphinxext/_templates/comparison_table.rst similarity index 69% rename from cunumeric/_sphinxext/_templates/comparison_table.rst rename to cupynumeric/_sphinxext/_templates/comparison_table.rst index 3a4211100..55d1d583f 100644 --- a/cunumeric/_sphinxext/_templates/comparison_table.rst +++ b/cupynumeric/_sphinxext/_templates/comparison_table.rst @@ -3,13 +3,13 @@ {{ section.title }} {{ "~" * section.title|length }} -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric .. autosummary:: :toctree: generated/ .. csv-table:: - :header: NumPy, cunumeric, single-GPU/CPU, multi-GPU/CPU + :header: NumPy, cupynumeric, single-GPU/CPU, multi-GPU/CPU {% for item in section.items -%} {{ item.np_ref }}, {{ item.lg_ref }}, {{ item.single }}, {{ item.multi }} @@ -19,6 +19,6 @@ Number of NumPy functions: {{ section.np_count }} -Number of functions covered by cunumeric: {{ section.lg_count }} +Number of functions covered by cupynumeric: {{ section.lg_count }} {% endfor %} \ No newline at end of file diff --git a/cunumeric/_sphinxext/comparison_table.py b/cupynumeric/_sphinxext/comparison_table.py similarity index 92% rename from cunumeric/_sphinxext/comparison_table.py rename to cupynumeric/_sphinxext/comparison_table.py index f37a14229..baa62a53d 100644 --- a/cunumeric/_sphinxext/comparison_table.py +++ b/cupynumeric/_sphinxext/comparison_table.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,13 +22,13 @@ from . import PARALLEL_SAFE, SphinxParallelSpec from ._comparison_config import GROUPED_CONFIGS, NUMPY_CONFIGS from ._comparison_util import generate_section -from ._cunumeric_directive import CunumericDirective +from ._cupynumeric_directive import CupynumericDirective from ._templates import COMPARISON_TABLE log = getLogger(__name__) -class ComparisonTable(CunumericDirective): +class ComparisonTable(CupynumericDirective): has_content = False required_arguments = 0 optional_arguments = 1 diff --git a/cunumeric/_sphinxext/implemented_index.py b/cupynumeric/_sphinxext/implemented_index.py similarity index 86% rename from cunumeric/_sphinxext/implemented_index.py rename to cupynumeric/_sphinxext/implemented_index.py index 3d70f763b..f0e9598bc 100644 --- a/cunumeric/_sphinxext/implemented_index.py +++ b/cupynumeric/_sphinxext/implemented_index.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ from sphinx.application import Sphinx from sphinx.util.logging import getLogger -import cunumeric as cn +import cupynumeric as cn -from ..coverage import is_implemented +from .._utils.coverage import is_implemented from . import PARALLEL_SAFE, SphinxParallelSpec -from ._cunumeric_directive import CunumericDirective +from ._cupynumeric_directive import CupynumericDirective log = getLogger(__name__) @@ -45,7 +45,7 @@ def _filter(x: Any) -> bool: ) -class ImplementedIndex(CunumericDirective): +class ImplementedIndex(CupynumericDirective): has_content = False required_arguments = 0 optional_arguments = 0 @@ -59,7 +59,7 @@ def run(self) -> list[nodes.Node]: if _filter(x) ] refs += [ - f"* :obj:`cunumeric.ndarray.{x.__name__}`" + f"* :obj:`cupynumeric.ndarray.{x.__name__}`" for x in cn.ndarray.__dict__.values() if _filter(x) ] diff --git a/cunumeric/_sphinxext/missing_refs.py b/cupynumeric/_sphinxext/missing_refs.py similarity index 69% rename from cunumeric/_sphinxext/missing_refs.py rename to cupynumeric/_sphinxext/missing_refs.py index 94266deff..99938b80d 100644 --- a/cunumeric/_sphinxext/missing_refs.py +++ b/cupynumeric/_sphinxext/missing_refs.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,25 +28,25 @@ log = getLogger(__name__) SKIP = ( - "cunumeric.cast", - "cunumeric.ndarray.__array_function__", - "cunumeric.ndarray.__array_ufunc__", - "cunumeric.ndarray.__format__", - "cunumeric.ndarray.__hash__", - "cunumeric.ndarray.__iter__", - "cunumeric.ndarray.__radd__", - "cunumeric.ndarray.__rand__", - "cunumeric.ndarray.__rdivmod__", - "cunumeric.ndarray.__reduce_ex__", - "cunumeric.ndarray.__rfloordiv__", - "cunumeric.ndarray.__rmod__", - "cunumeric.ndarray.__rmul__", - "cunumeric.ndarray.__ror__", - "cunumeric.ndarray.__rpow__", - "cunumeric.ndarray.__rsub__", - "cunumeric.ndarray.__rtruediv__", - "cunumeric.ndarray.__rxor__", - "cunumeric.ndarray.__sizeof__", + "cupynumeric.cast", + "cupynumeric.ndarray.__array_function__", + "cupynumeric.ndarray.__array_ufunc__", + "cupynumeric.ndarray.__format__", + "cupynumeric.ndarray.__hash__", + "cupynumeric.ndarray.__iter__", + "cupynumeric.ndarray.__radd__", + "cupynumeric.ndarray.__rand__", + "cupynumeric.ndarray.__rdivmod__", + "cupynumeric.ndarray.__reduce_ex__", + "cupynumeric.ndarray.__rfloordiv__", + "cupynumeric.ndarray.__rmod__", + "cupynumeric.ndarray.__rmul__", + "cupynumeric.ndarray.__ror__", + "cupynumeric.ndarray.__rpow__", + "cupynumeric.ndarray.__rsub__", + "cupynumeric.ndarray.__rtruediv__", + "cupynumeric.ndarray.__rxor__", + "cupynumeric.ndarray.__sizeof__", ) MISSING: list[tuple[str, str]] = [] @@ -62,7 +62,7 @@ def run(self, **kwargs: Any) -> None: def _check_target(self, node: Any) -> None: target = node["reftarget"] - if not target.startswith("cunumeric.") or target in SKIP: + if not target.startswith("cupynumeric.") or target in SKIP: return domain = self.env.domains[node["refdomain"]] @@ -85,7 +85,7 @@ def _check_target(self, node: Any) -> None: if uri is None: loc = get_node_location(node) log.warning( - f"Cunumeric reference missing a target: {loc}: {target}", + f"cuPyNumeric reference missing a target: {loc}: {target}", type="ref", ) diff --git a/cunumeric/_sphinxext/ufunc_formatter.py b/cupynumeric/_sphinxext/ufunc_formatter.py similarity index 94% rename from cunumeric/_sphinxext/ufunc_formatter.py rename to cupynumeric/_sphinxext/ufunc_formatter.py index 60d8145ab..6f574d754 100644 --- a/cunumeric/_sphinxext/ufunc_formatter.py +++ b/cupynumeric/_sphinxext/ufunc_formatter.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ from sphinx.application import Sphinx from sphinx.ext.autodoc import FunctionDocumenter -from cunumeric import ufunc +from cupynumeric import ufunc from . import PARALLEL_SAFE, SphinxParallelSpec diff --git a/cupynumeric/_thunk/__init__.py b/cupynumeric/_thunk/__init__.py new file mode 100644 index 000000000..31d8d448c --- /dev/null +++ b/cupynumeric/_thunk/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations diff --git a/cunumeric/sort.py b/cupynumeric/_thunk/_sort.py similarity index 72% rename from cunumeric/sort.py rename to cupynumeric/_thunk/_sort.py index a0503bf92..82ab73847 100644 --- a/cunumeric/sort.py +++ b/cupynumeric/_thunk/_sort.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,17 +14,21 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, cast -from legate.core import types as ty -from numpy.core.multiarray import ( # type: ignore [attr-defined] - normalize_axis_index, -) +from legate.core import get_legate_runtime, types as ty -from .config import CuNumericOpCode +from .._utils import is_np2 +from ..config import CuPyNumericOpCode +from ..runtime import runtime + +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore +else: + from numpy.core.multiarray import normalize_axis_index # type: ignore if TYPE_CHECKING: - from .deferred import DeferredArray + from .._thunk.deferred import DeferredArray def sort_flattened( @@ -35,11 +39,11 @@ def sort_flattened( # run sort flattened -- return 1D solution sort_result = cast( "DeferredArray", - output.runtime.create_empty_thunk( + runtime.create_empty_thunk( flattened.shape, dtype=output.base.type, inputs=(flattened,) ), ) - sort(sort_result, flattened, argsort, stable=stable) + sort_deferred(sort_result, flattened, argsort, stable=stable) output.base = sort_result.base output.numpy_array = None @@ -58,7 +62,7 @@ def sort_swapped( swapped_copy = cast( "DeferredArray", - output.runtime.create_empty_thunk( + runtime.create_empty_thunk( swapped.shape, dtype=input.base.type, inputs=(input, swapped) ), ) @@ -68,17 +72,17 @@ def sort_swapped( if argsort is True: sort_result = cast( "DeferredArray", - output.runtime.create_empty_thunk( + runtime.create_empty_thunk( swapped_copy.shape, dtype=output.base.type, inputs=(swapped_copy,), ), ) - sort(sort_result, swapped_copy, argsort, stable=stable) + sort_deferred(sort_result, swapped_copy, argsort, stable=stable) output.base = sort_result.swapaxes(input.ndim - 1, sort_axis).base output.numpy_array = None else: - sort(swapped_copy, swapped_copy, argsort, stable=stable) + sort_deferred(swapped_copy, swapped_copy, argsort, stable=stable) output.base = swapped_copy.swapaxes(input.ndim - 1, sort_axis).base output.numpy_array = None @@ -86,23 +90,24 @@ def sort_swapped( def sort_task( output: DeferredArray, input: DeferredArray, argsort: bool, stable: bool ) -> None: - task = output.context.create_auto_task(CuNumericOpCode.SORT) + legate_runtime = get_legate_runtime() + task = legate_runtime.create_auto_task( + output.library, CuPyNumericOpCode.SORT + ) - uses_unbound_output = output.runtime.num_procs > 1 and input.ndim == 1 + uses_unbound_output = runtime.num_procs > 1 and input.ndim == 1 task.add_input(input.base) if uses_unbound_output: - unbound = output.runtime.create_unbound_thunk( - dtype=output.base.type, ndim=1 - ) + unbound = runtime.create_unbound_thunk(dtype=output.base.type, ndim=1) task.add_output(unbound.base) else: task.add_output(output.base) task.add_alignment(output.base, input.base) - if output.runtime.num_gpus > 1: + if runtime.num_gpus > 1: task.add_nccl_communicator() - elif output.runtime.num_gpus == 0 and output.runtime.num_procs > 1: + elif runtime.num_gpus == 0 and runtime.num_procs > 1: task.add_cpu_communicator() task.add_scalar_arg(argsort, ty.bool_) # return indices flag @@ -115,11 +120,11 @@ def sort_task( output.numpy_array = None -def sort( +def sort_deferred( output: DeferredArray, input: DeferredArray, argsort: bool, - axis: Union[int, None] = -1, + axis: int | None = -1, stable: bool = False, ) -> None: if axis is None and input.ndim > 1: diff --git a/cunumeric/deferred.py b/cupynumeric/_thunk/deferred.py similarity index 75% rename from cunumeric/deferred.py rename to cupynumeric/_thunk/deferred.py index 9d9fa963d..b94a16aed 100644 --- a/cunumeric/deferred.py +++ b/cupynumeric/_thunk/deferred.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,55 +20,75 @@ from enum import IntEnum, unique from functools import reduce, wraps from inspect import signature -from itertools import chain, product +from itertools import chain from typing import ( TYPE_CHECKING, Any, Callable, - Dict, - Optional, + ParamSpec, Sequence, TypeVar, - Union, cast, ) import legate.core.types as ty import numpy as np -from legate.core import Annotation, Future, ReductionOp, Store -from legate.core.store import RegionField +from legate.core import ( + Annotation, + LogicalStore, + ReductionOpKind, + Scalar, + align, + bloat, + broadcast, + constant, + dimension, + get_legate_runtime, + scale, +) from legate.core.utils import OrderedSet -from numpy.core.numeric import ( # type: ignore [attr-defined] - normalize_axis_tuple, +from legate.settings import settings as legate_settings + +from .._utils import is_np2 +from .._utils.array import ( + is_advanced_indexing, + max_identity, + min_identity, + to_core_type, ) -from typing_extensions import ParamSpec - -from .config import ( +from ..config import ( BinaryOpCode, BitGeneratorDistribution, BitGeneratorOperation, Bitorder, ConvertCode, - CuNumericOpCode, + ConvolveMethod, + CuPyNumericOpCode, RandGenCode, UnaryOpCode, UnaryRedCode, ) -from .linalg.cholesky import cholesky -from .linalg.solve import solve -from .sort import sort +from ..linalg._cholesky import cholesky_deferred +from ..linalg._qr import qr_deferred +from ..linalg._solve import solve_deferred +from ..linalg._svd import svd_deferred +from ..runtime import runtime +from ._sort import sort_deferred from .thunk import NumPyThunk -from .utils import is_advanced_indexing, to_core_dtype + +if is_np2: + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.numeric import normalize_axis_tuple # type: ignore if TYPE_CHECKING: import numpy.typing as npt - from legate.core import FieldID, Region - from legate.core.operation import AutoTask, ManualTask + from legate.core import LogicalStorePartition - from .config import BitGeneratorType, FFTDirection, FFTType, WindowOpCode - from .runtime import Runtime - from .types import ( + from ..config import BitGeneratorType, FFTDirection, FFTType, WindowOpCode + from ..types import ( BitOrder, + ConvolveMethod as ConvolveMethodType, ConvolveMode, NdShape, OrderType, @@ -91,12 +111,17 @@ def _prod(tpl: Sequence[int]) -> int: R = TypeVar("R") P = ParamSpec("P") +legate_runtime = get_legate_runtime() + def auto_convert( *thunk_params: str, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Converts all named parameters to DeferredArrays. + + This function makes an immutable copy of any parameter that wasn't already + a DeferredArray. """ keys = OrderedSet(thunk_params) assert len(keys) == len(thunk_params) @@ -116,16 +141,15 @@ def decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> R: # Convert relevant arguments to DeferredArrays - self = args[0] args = tuple( - self.runtime.to_deferred_array(arg) + runtime.to_deferred_array(arg, read_only=True) if idx in indices and arg is not None else arg for (idx, arg) in enumerate(args) ) for k, v in kwargs.items(): if k in keys and v is not None: - kwargs[k] = self.runtime.to_deferred_array(v) + kwargs[k] = runtime.to_deferred_array(v, read_only=True) return func(*args, **kwargs) @@ -134,83 +158,29 @@ def wrapper(*args: Any, **kwargs: Any) -> R: return decorator -# This is a dummy object that is only used as an initializer for the -# RegionField object above. It is thrown away as soon as the -# RegionField is constructed. -class _CuNumericNDarray(object): - __slots__ = ["__array_interface__"] - - def __init__( - self, - shape: NdShape, - field_type: Any, - base_ptr: Any, - strides: tuple[int, ...], - read_only: bool, - ) -> None: - # See: https://docs.scipy.org/doc/numpy/reference/arrays.interface.html - self.__array_interface__ = { - "version": 3, - "shape": shape, - "typestr": field_type.str, - "data": (base_ptr, read_only), - "strides": strides, - } - - -_UNARY_RED_TO_REDUCTION_OPS: Dict[int, int] = { - UnaryRedCode.SUM: ReductionOp.ADD, - UnaryRedCode.SUM_SQUARES: ReductionOp.ADD, - UnaryRedCode.VARIANCE: ReductionOp.ADD, - UnaryRedCode.PROD: ReductionOp.MUL, - UnaryRedCode.MAX: ReductionOp.MAX, - UnaryRedCode.MIN: ReductionOp.MIN, - UnaryRedCode.ARGMAX: ReductionOp.MAX, - UnaryRedCode.ARGMIN: ReductionOp.MIN, - UnaryRedCode.NANARGMAX: ReductionOp.MAX, - UnaryRedCode.NANARGMIN: ReductionOp.MIN, - UnaryRedCode.NANMAX: ReductionOp.MAX, - UnaryRedCode.NANMIN: ReductionOp.MIN, - UnaryRedCode.NANPROD: ReductionOp.MUL, - UnaryRedCode.NANSUM: ReductionOp.ADD, - UnaryRedCode.CONTAINS: ReductionOp.ADD, - UnaryRedCode.COUNT_NONZERO: ReductionOp.ADD, - UnaryRedCode.ALL: ReductionOp.MUL, - UnaryRedCode.ANY: ReductionOp.ADD, +_UNARY_RED_TO_REDUCTION_OPS: dict[int, int] = { + UnaryRedCode.SUM: ReductionOpKind.ADD, + UnaryRedCode.SUM_SQUARES: ReductionOpKind.ADD, + UnaryRedCode.VARIANCE: ReductionOpKind.ADD, + UnaryRedCode.PROD: ReductionOpKind.MUL, + UnaryRedCode.MAX: ReductionOpKind.MAX, + UnaryRedCode.MIN: ReductionOpKind.MIN, + UnaryRedCode.ARGMAX: ReductionOpKind.MAX, + UnaryRedCode.ARGMIN: ReductionOpKind.MIN, + UnaryRedCode.NANARGMAX: ReductionOpKind.MAX, + UnaryRedCode.NANARGMIN: ReductionOpKind.MIN, + UnaryRedCode.NANMAX: ReductionOpKind.MAX, + UnaryRedCode.NANMIN: ReductionOpKind.MIN, + UnaryRedCode.NANPROD: ReductionOpKind.MUL, + UnaryRedCode.NANSUM: ReductionOpKind.ADD, + UnaryRedCode.CONTAINS: ReductionOpKind.ADD, + UnaryRedCode.COUNT_NONZERO: ReductionOpKind.ADD, + UnaryRedCode.ALL: ReductionOpKind.MUL, + UnaryRedCode.ANY: ReductionOpKind.ADD, } -def max_identity( - ty: np.dtype[Any], -) -> Union[int, np.floating[Any], bool, np.complexfloating[Any, Any]]: - if ty.kind == "i" or ty.kind == "u": - return np.iinfo(ty).min - elif ty.kind == "f": - return np.finfo(ty).min - elif ty.kind == "c": - return np.finfo(np.float64).min + np.finfo(np.float64).min * 1j - elif ty.kind == "b": - return False - else: - raise ValueError(f"Unsupported dtype: {ty}") - - -def min_identity( - ty: np.dtype[Any], -) -> Union[int, np.floating[Any], bool, np.complexfloating[Any, Any]]: - if ty.kind == "i" or ty.kind == "u": - return np.iinfo(ty).max - elif ty.kind == "f": - return np.finfo(ty).max - elif ty.kind == "c": - return np.finfo(np.float64).max + np.finfo(np.float64).max * 1j - elif ty.kind == "b": - return True - else: - raise ValueError(f"Unsupported dtype: {ty}") - - -_UNARY_RED_IDENTITIES: Dict[UnaryRedCode, Callable[[Any], Any]] = { +_UNARY_RED_IDENTITIES: dict[UnaryRedCode, Callable[[Any], Any]] = { UnaryRedCode.SUM: lambda _: 0, UnaryRedCode.SUM_SQUARES: lambda _: 0, UnaryRedCode.VARIANCE: lambda _: 0, @@ -255,14 +225,13 @@ class DeferredArray(NumPyThunk): def __init__( self, - runtime: Runtime, - base: Store, - numpy_array: Optional[npt.NDArray[Any]] = None, + base: LogicalStore, + numpy_array: npt.NDArray[Any] | None = None, ) -> None: - super().__init__(runtime, base.type.to_numpy_dtype()) + super().__init__(base.type.to_numpy_dtype()) assert base is not None - assert isinstance(base, Store) - self.base = base # a Legate Store + assert isinstance(base, LogicalStore) + self.base: LogicalStore = base # a Legate Store self.numpy_array = ( None if numpy_array is None else weakref.ref(numpy_array) ) @@ -270,16 +239,6 @@ def __init__( def __str__(self) -> str: return f"DeferredArray(base: {self.base})" - @property - def storage(self) -> Union[Future, tuple[Region, Union[int, FieldID]]]: - storage = self.base.storage - if self.base.kind == Future: - assert isinstance(storage, Future) - return storage - else: - assert isinstance(storage, RegionField) - return (storage.region, storage.field.field_id) - @property def shape(self) -> NdShape: return tuple(self.base.shape) @@ -293,7 +252,7 @@ def _copy_if_overlapping(self, other: DeferredArray) -> DeferredArray: return self copy = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( self.shape, self.base.type, inputs=[self], @@ -302,6 +261,13 @@ def _copy_if_overlapping(self, other: DeferredArray) -> DeferredArray: copy.copy(self, deep=True) return copy + def _copy_if_partially_overlapping( + self, other: DeferredArray + ) -> DeferredArray: + if self.base.equal_storage(other.base): + return self + return self._copy_if_overlapping(other) + def __numpy_array__(self) -> npt.NDArray[Any]: if self.numpy_array is not None: result = self.numpy_array() @@ -312,34 +278,13 @@ def __numpy_array__(self) -> npt.NDArray[Any]: # and type return np.empty(shape=self.shape, dtype=self.dtype) - if self.scalar: - result = np.full( - self.shape, - self.get_scalar_array(), - dtype=self.dtype, - ) - else: - alloc = self.base.get_inline_allocation() - - def construct_ndarray( - shape: NdShape, address: Any, strides: tuple[int, ...] - ) -> npt.NDArray[Any]: - initializer = _CuNumericNDarray( - shape, self.dtype, address, strides, False - ) - result = np.asarray(initializer) - if self.shape == (): - result = result.reshape(()) - return result - - result = cast("npt.NDArray[Any]", alloc.consume(construct_ndarray)) - - self.numpy_array = weakref.ref(result) - return result + return np.asarray( + self.base.get_physical_store().get_inline_allocation() + ) # TODO: We should return a view of the field instead of a copy def imag(self) -> NumPyThunk: - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( self.shape, dtype=_COMPLEX_FIELD_DTYPES[self.base.type], inputs=[self], @@ -349,14 +294,13 @@ def imag(self) -> NumPyThunk: UnaryOpCode.IMAG, self, True, - [], ) return result # TODO: We should return a view of the field instead of a copy def real(self) -> NumPyThunk: - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( self.shape, dtype=_COMPLEX_FIELD_DTYPES[self.base.type], inputs=[self], @@ -366,13 +310,12 @@ def real(self) -> NumPyThunk: UnaryOpCode.REAL, self, True, - [], ) return result def conj(self) -> NumPyThunk: - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( self.shape, dtype=self.base.type, inputs=[self], @@ -382,7 +325,6 @@ def conj(self) -> NumPyThunk: UnaryOpCode.CONJ, self, True, - [], ) return result @@ -391,25 +333,17 @@ def conj(self) -> NumPyThunk: @auto_convert("rhs") def copy(self, rhs: Any, deep: bool = False) -> None: if self.scalar and rhs.scalar: - self.base.set_storage(rhs.base.storage) + legate_runtime.issue_fill(self.base, rhs.base) return self.unary_op( UnaryOpCode.COPY, rhs, True, - [], ) @property def scalar(self) -> bool: - return self.base.scalar - - def get_scalar_array(self) -> npt.NDArray[Any]: - assert self.scalar - assert isinstance(self.base.storage, Future) - buf = self.base.storage.get_buffer(self.dtype.itemsize) - result = np.frombuffer(buf, dtype=self.dtype, count=1) - return result.reshape(()) + return self.base.has_scalar_storage and self.base.size == 1 def _zip_indices( self, start_index: int, arrays: tuple[Any, ...] @@ -424,7 +358,7 @@ def _zip_indices( new_arrays: tuple[Any, ...] = tuple() # check array's type and convert them to deferred arrays for a in arrays: - a = self.runtime.to_deferred_array(a) + a = runtime.to_deferred_array(a, read_only=True) data_type = a.dtype if data_type != np.int64: raise TypeError("index arrays should be int64 type") @@ -434,7 +368,7 @@ def _zip_indices( # find a broadcasted shape for all arrays passed as indices shapes = tuple(a.shape for a in arrays) if len(arrays) > 1: - from .module import broadcast_shapes + from .._module import broadcast_shapes b_shape = broadcast_shapes(*shapes) else: @@ -485,10 +419,10 @@ def _zip_indices( # dtype, to store N-dimensional index points, to be used as the # indirection field in a copy. N = self.ndim - pointN_dtype = self.runtime.get_point_type(N) + pointN_dtype = ty.point_type(N) output_arr = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( shape=out_shape, dtype=pointN_dtype, inputs=[self], @@ -496,26 +430,25 @@ def _zip_indices( ) # call ZIP function to combine index arrays into a singe array - task = self.context.create_auto_task(CuNumericOpCode.ZIP) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.ZIP + ) task.throws_exception(IndexError) - task.add_output(output_arr.base) + p_out = task.add_output(output_arr.base) task.add_scalar_arg(self.ndim, ty.int64) # N of points in Point task.add_scalar_arg(key_dim, ty.int64) # key_dim task.add_scalar_arg(start_index, ty.int64) # start_index task.add_scalar_arg(self.shape, (ty.int64,)) for a in arrays: - task.add_input(a) - task.add_alignment(output_arr.base, a) + p_in = task.add_input(a) + task.add_constraint(align(p_out, p_in)) task.execute() return output_arr def _copy_store(self, store: Any) -> DeferredArray: - store_to_copy = DeferredArray( - self.runtime, - base=store, - ) - store_copy = self.runtime.create_empty_thunk( + store_to_copy = DeferredArray(base=store) + store_copy = runtime.create_empty_thunk( store_to_copy.shape, self.base.type, inputs=[store_to_copy], @@ -524,7 +457,9 @@ def _copy_store(self, store: Any) -> DeferredArray: return cast(DeferredArray, store_copy) @staticmethod - def _slice_store(k: slice, store: Store, dim: int) -> tuple[slice, Store]: + def _slice_store( + k: slice, store: LogicalStore, dim: int + ) -> tuple[slice, LogicalStore]: start = k.start end = k.stop step = k.step @@ -612,7 +547,7 @@ def _has_single_boolean_array( shift = 0 store = lhs.base for dim, k in enumerate(new_key): - if np.isscalar(k): + if isinstance(k, int): if k < 0: # type: ignore [operator] k += store.shape[dim + key_dim + shift] store = store.project(dim + key_dim + shift, k) @@ -628,7 +563,7 @@ def _has_single_boolean_array( "Unsupported entry type passed to advanced ", "indexing operation", ) - lhs = DeferredArray(self.runtime, store) + lhs = DeferredArray(store) return True, lhs, key[transpose_index] @@ -640,11 +575,11 @@ def _advanced_indexing_with_boolean_array( self, key: Any, is_set: bool = False, - set_value: Optional[Any] = None, + set_value: Any | None = None, ) -> tuple[bool, Any, Any, Any]: rhs = self if not isinstance(key, DeferredArray): - key = self.runtime.to_deferred_array(key) + key = runtime.to_deferred_array(key, read_only=True) # in case when boolean array is passed as an index, shape for all # its dimensions should be the same as the shape of @@ -673,7 +608,7 @@ def _advanced_indexing_with_boolean_array( out = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( out_shape, rhs.base.type, inputs=[rhs], @@ -693,10 +628,7 @@ def _advanced_indexing_with_boolean_array( # and avoid calling Copy has_set_value = set_value is not None and set_value.size == 1 if has_set_value: - mask = DeferredArray( - self.runtime, - base=key_store, - ) + mask = DeferredArray(base=key_store) rhs.putmask(mask, set_value) return False, rhs, rhs, self else: @@ -706,26 +638,26 @@ def _advanced_indexing_with_boolean_array( # indirect copy operation if is_set: N = rhs.ndim - out_dtype = rhs.runtime.get_point_type(N) + out_dtype = ty.point_type(N) # TODO : current implementation of the ND output regions # requires out.ndim == rhs.ndim. This will be fixed in the # future - out = rhs.runtime.create_unbound_thunk(out_dtype, ndim=rhs.ndim) + out = runtime.create_unbound_thunk(out_dtype, ndim=rhs.ndim) key_dims = key.ndim # dimension of the original key - task = rhs.context.create_auto_task( - CuNumericOpCode.ADVANCED_INDEXING + task = legate_runtime.create_auto_task( + self.library, + CuPyNumericOpCode.ADVANCED_INDEXING, ) task.add_output(out.base) - task.add_input(rhs.base) - task.add_input(key_store) + p_rhs = task.add_input(rhs.base) + p_key = task.add_input(key_store) task.add_scalar_arg(is_set, ty.bool_) task.add_scalar_arg(key_dims, ty.int64) - task.add_alignment(rhs.base, key_store) - task.add_broadcast( - rhs.base, axes=tuple(range(1, len(rhs.base.shape))) - ) + task.add_constraint(align(p_rhs, p_key)) + if rhs.base.ndim > 1: + task.add_constraint(broadcast(p_rhs, range(1, rhs.base.ndim))) task.execute() # TODO : current implementation of the ND output regions @@ -740,7 +672,7 @@ def _advanced_indexing_with_boolean_array( out_shape = tuple(out.shape[i] for i in range(0, out_dim)) out = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( out_shape, out_dtype, inputs=[out], @@ -759,7 +691,7 @@ def _create_indexing_array( self, key: Any, is_set: bool = False, - set_value: Optional[Any] = None, + set_value: Any | None = None, ) -> tuple[bool, Any, Any, Any]: is_bool_array, lhs, bool_key = self._has_single_boolean_array( key, is_set @@ -843,8 +775,8 @@ def _create_indexing_array( elif isinstance(k, slice): k, store = self._slice_store(k, store, dim + shift) elif isinstance(k, NumPyThunk): - if not isinstance(computed_key, DeferredArray): - k = self.runtime.to_deferred_array(k) + if not isinstance(k, DeferredArray): + k = runtime.to_deferred_array(k, read_only=True) if k.dtype == bool: for i in range(k.ndim): if k.shape[i] != store.shape[dim + i + shift]: @@ -869,7 +801,7 @@ def _create_indexing_array( # to apply all the transformations done to `store` to `self` # as well before creating a copy if is_set: - self = DeferredArray(self.runtime, store) + self = DeferredArray(store) # after store is transformed we need to to return a copy of # the store since Copy operation can't be done on # the store with transformation @@ -918,10 +850,7 @@ def _get_view(self, key: Any) -> DeferredArray: else: assert False - return DeferredArray( - self.runtime, - base=store, - ) + return DeferredArray(base=store) def _broadcast(self, shape: NdShape) -> Any: result = self.base @@ -947,15 +876,12 @@ def _convert_future_to_regionfield( shape: NdShape = (1,) else: shape = self.shape - store = self.context.create_store( + store = legate_runtime.create_store( self.base.type, shape=shape, optimize_scalar=False, ) - thunk_copy = DeferredArray( - self.runtime, - base=store, - ) + thunk_copy = DeferredArray(base=store) thunk_copy.copy(self, deep=True) return thunk_copy @@ -971,34 +897,28 @@ def get_item(self, key: Any) -> NumPyThunk: ) = self._create_indexing_array(key) if copy_needed: - if rhs.base.kind == Future: + if rhs.base.has_scalar_storage: rhs = rhs._convert_future_to_regionfield() result: NumPyThunk - if index_array.base.kind == Future: + if index_array.base.has_scalar_storage: index_array = index_array._convert_future_to_regionfield() - result_store = self.context.create_store( + result_store = legate_runtime.create_store( self.base.type, shape=index_array.shape, optimize_scalar=False, ) - result = DeferredArray( - self.runtime, - base=result_store, - ) + result = DeferredArray(base=result_store) else: - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( index_array.base.shape, self.base.type, inputs=[self], ) - copy = self.context.create_copy() - copy.set_source_indirect_out_of_range(False) - copy.add_input(rhs.base) - copy.add_source_indirect(index_array.base) - copy.add_output(result.base) # type: ignore - copy.execute() + legate_runtime.issue_gather( + result.base, rhs.base, index_array.base # type: ignore + ) else: return index_array @@ -1008,11 +928,13 @@ def get_item(self, key: Any) -> NumPyThunk: if result.shape == (): input = result - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( (), self.base.type, inputs=[self] ) - task = self.context.create_auto_task(CuNumericOpCode.READ) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.READ + ) task.add_input(input.base) task.add_output(result.base) # type: ignore @@ -1023,8 +945,12 @@ def get_item(self, key: Any) -> NumPyThunk: @auto_convert("rhs") def set_item(self, key: Any, rhs: Any) -> None: assert self.dtype == rhs.dtype + # Check to see if this is advanced indexing or not if is_advanced_indexing(key): + # copy if a self-copy might overlap + rhs = rhs._copy_if_overlapping(self) + # Create the indexing array ( copy_needed, @@ -1048,29 +974,22 @@ def set_item(self, key: Any, rhs: Any) -> None: # the case when rhs is a scalar and indices array contains # a single value # TODO this logic should be removed when copy accepts Futures - if rhs_store.kind == Future: - rhs_tmp = DeferredArray( - self.runtime, - base=rhs_store, - ) + if rhs_store.has_scalar_storage: + rhs_tmp = DeferredArray(base=rhs_store) rhs_tmp2 = rhs_tmp._convert_future_to_regionfield() rhs_store = rhs_tmp2.base - if index_array.base.kind == Future: + if index_array.base.has_scalar_storage: index_array = index_array._convert_future_to_regionfield() - if lhs.base.kind == Future: + if lhs.base.has_scalar_storage: lhs = lhs._convert_future_to_regionfield() if lhs.base.transformed: lhs = lhs._copy_store(lhs.base) if index_array.size != 0: - copy = self.context.create_copy() - copy.set_target_indirect_out_of_range(False) - - copy.add_input(rhs_store) - copy.add_target_indirect(index_array.base) - copy.add_output(lhs.base) - copy.execute() + legate_runtime.issue_scatter( + lhs.base, index_array.base, rhs_store + ) # TODO this copy will be removed when affine copies are # supported in Legion/Realm @@ -1084,7 +1003,9 @@ def set_item(self, key: Any, rhs: Any) -> None: # We're just writing a single value assert rhs.size == 1 - task = self.context.create_auto_task(CuNumericOpCode.WRITE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.WRITE + ) # Since we pass the view with write discard privilege, # we should make sure that the mapper either creates a fresh # instance just for this one-element view or picks one of the @@ -1096,27 +1017,22 @@ def set_item(self, key: Any, rhs: Any) -> None: # In Python, any inplace update of form arr[key] op= value # goes through three steps: 1) __getitem__ fetching the object # for the key, 2) __iop__ for the update, and 3) __setitem__ - # to set the result back. In cuNumeric, the object we + # to set the result back. In cuPyNumeric, the object we # return in step (1) is actually a subview to the array arr # through which we make updates in place, so after step (2) is # done, the effect of inplace update is already reflected # to the arr. Therefore, we skip the copy to avoid redundant # copies if we know that we hit such a scenario. - # TODO: We should make this work for the advanced indexing case # NOTE: Neither Store nor Storage have an __eq__, so we can # only check that the underlying RegionField/Future corresponds # to the same Legion handle. - if ( - view.base.has_storage - and rhs.base.has_storage - and view.base.storage.same_handle(rhs.base.storage) - ): + if view.base.equal_storage(rhs.base): return view.copy(rhs, deep=False) def broadcast_to(self, shape: NdShape) -> NumPyThunk: - return DeferredArray(self.runtime, base=self._broadcast(shape)) + return DeferredArray(base=self._broadcast(shape)) def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: assert isinstance(newshape, Iterable) @@ -1125,8 +1041,8 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: if order != "C": # If we don't have a transform then we need to make a copy - self.runtime.warn( - "cuNumeric has not implemented reshape using Fortran-like " + runtime.warn( + "cuPyNumeric has not implemented reshape using Fortran-like " "index order and is falling back to canonical numpy. You may " "notice significantly decreased performance for this " "function call.", @@ -1135,9 +1051,9 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: numpy_array = self.__numpy_array__() # Force a copy here because we know we can't build a view result_array = numpy_array.reshape(newshape, order=order).copy() - result = self.runtime.get_numpy_thunk(result_array) + result = runtime.get_numpy_thunk(result_array) - return self.runtime.to_deferred_array(result) + return runtime.to_deferred_array(result, read_only=True) if self.shape == newshape: return self @@ -1231,7 +1147,7 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: else: tmp_shape += tgt_g - result = self.runtime.create_empty_thunk( + result = runtime.create_empty_thunk( tmp_shape, dtype=self.base.type, inputs=[self] ) @@ -1262,8 +1178,8 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: assert src.shape == tgt.shape - src_array = DeferredArray(self.runtime, src) - tgt_array = DeferredArray(self.runtime, tgt) + src_array = DeferredArray(src) + tgt_array = DeferredArray(tgt) tgt_array.copy(src_array, deep=True) if needs_delinearization and needs_linearization: @@ -1275,8 +1191,8 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: src_dim += len(tgt_g) assert src.shape == newshape - src_array = DeferredArray(self.runtime, src) - result = self.runtime.create_empty_thunk( + src_array = DeferredArray(src) + result = runtime.create_empty_thunk( newshape, dtype=self.base.type, inputs=[self] ) result.copy(src_array, deep=True) @@ -1301,13 +1217,11 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: src_dim += diff - result = DeferredArray(self.runtime, src) + result = DeferredArray(src) return result - def squeeze( - self, axis: Optional[Union[int, tuple[int, ...]]] - ) -> DeferredArray: + def squeeze(self, axis: int | tuple[int, ...] | None) -> DeferredArray: result = self.base if axis is None: shift = 0 @@ -1328,7 +1242,7 @@ def squeeze( ) if result is self.base: return self - return DeferredArray(self.runtime, result) + return DeferredArray(result) def swapaxes(self, axis1: int, axis2: int) -> DeferredArray: if self.size == 1 or axis1 == axis2: @@ -1340,8 +1254,7 @@ def swapaxes(self, axis1: int, axis2: int) -> DeferredArray: dims[axis1], dims[axis2] = dims[axis2], dims[axis1] result = self.base.transpose(tuple(dims)) - - return DeferredArray(self.runtime, result) + return DeferredArray(result) # Convert the source array to the destination array @auto_convert("rhs") @@ -1357,8 +1270,8 @@ def convert( assert lhs_array.dtype != rhs_array.dtype if warn: - self.runtime.warn( - "cuNumeric performing implicit type conversion from " + runtime.warn( + "cuPyNumeric performing implicit type conversion from " + str(rhs_array.dtype) + " to " + str(lhs_array.dtype), @@ -1368,50 +1281,45 @@ def convert( lhs = lhs_array.base rhs = rhs_array.base - task = self.context.create_auto_task(CuNumericOpCode.CONVERT) - task.add_output(lhs) - task.add_input(rhs) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.CONVERT + ) + p_lhs = task.add_output(lhs) + p_rhs = task.add_input(rhs) task.add_scalar_arg(nan_op, ty.int32) - task.add_alignment(lhs, rhs) + task.add_constraint(align(p_lhs, p_rhs)) task.execute() - if temporary: - lhs.set_linear() - - @auto_convert("v", "lhs") - def convolve(self, v: Any, lhs: Any, mode: ConvolveMode) -> None: - input = self.base - filter = v.base - out = lhs.base - - task = self.context.create_auto_task(CuNumericOpCode.CONVOLVE) - - offsets = (filter.shape + 1) // 2 - stencils: list[tuple[int, ...]] = [] - for offset in offsets: - stencils.append((-offset, 0, offset)) - stencils = list(product(*stencils)) - stencils.remove((0,) * self.ndim) - - p_out = task.declare_partition(out) - p_input = task.declare_partition(input) - p_stencils = [] - for _ in stencils: - p_stencils.append(task.declare_partition(input, complete=False)) - - task.add_output(out, partition=p_out) - task.add_input(filter) - task.add_input(input, partition=p_input) - for p_stencil in p_stencils: - task.add_input(input, partition=p_stencil) - task.add_scalar_arg(self.shape, (ty.int64,)) + @auto_convert("input", "filter") + def convolve( + self, + input: Any, + filter: Any, + mode: ConvolveMode, + method: ConvolveMethodType, + ) -> None: + if method != "auto" and runtime.num_gpus == 0: + runtime.warn(f"the method {method} is ignored on CPUs") + + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.CONVOLVE + ) + + offsets = tuple((ext + 1) // 2 for ext in filter.shape) + + p_out = task.add_output(self.base) + p_filter = task.add_input(filter.base) + p_in = task.add_input(input.base) + p_halo = task.declare_partition() + task.add_input(input.base, p_halo) + task.add_scalar_arg(input.shape, (ty.int64,)) + task.add_scalar_arg(getattr(ConvolveMethod, method.upper()), ty.int32) - task.add_constraint(p_out == p_input) - for stencil, p_stencil in zip(stencils, p_stencils): - task.add_constraint(p_input + stencil <= p_stencil) # type: ignore - task.add_broadcast(filter) + task.add_constraint(align(p_out, p_in)) + task.add_constraint(bloat(p_out, p_halo, offsets, offsets)) + task.add_constraint(broadcast(p_filter)) task.execute() @@ -1425,21 +1333,23 @@ def fft( ) -> None: lhs = self # For now, deferred only supported with GPU, use eager / numpy for CPU - if self.runtime.num_gpus == 0: - lhs_eager = lhs.runtime.to_eager_array(lhs) - rhs_eager = rhs.runtime.to_eager_array(rhs) + if runtime.num_gpus == 0: + lhs_eager = runtime.to_eager_array(lhs) + rhs_eager = runtime.to_eager_array(rhs) lhs_eager.fft(rhs_eager, axes, kind, direction) - lhs.base = lhs.runtime.to_deferred_array(lhs_eager).base + lhs.base = runtime.to_deferred_array( + lhs_eager, read_only=True + ).base else: input = rhs.base output = lhs.base - task = self.context.create_auto_task(CuNumericOpCode.FFT) - p_output = task.declare_partition(output) - p_input = task.declare_partition(input) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.FFT + ) - task.add_output(output, partition=p_output) - task.add_input(input, partition=p_input) + p_output = task.add_output(output) + p_input = task.add_input(input) task.add_scalar_arg(kind.type_id, ty.int32) task.add_scalar_arg(direction.value, ty.int32) task.add_scalar_arg( @@ -1451,34 +1361,40 @@ def fft( for ax in axes: task.add_scalar_arg(ax, ty.int64) - if input.ndim > len(OrderedSet(axes)): - task.add_broadcast(input, axes=OrderedSet(axes)) + if input.shape == output.shape: + task.add_constraint(align(p_output, p_input)) + if input.ndim > len(OrderedSet(axes)): + task.add_constraint(broadcast(p_input, OrderedSet(axes))) + else: + task.add_constraint(broadcast(p_input)) else: - task.add_broadcast(input) - task.add_constraint(p_output == p_input) + # TODO: We need the relaxed alignment to avoid serializing the + # task here. Batched FFT was relying on the relaxed alignment. + task.add_constraint(broadcast(p_output)) + task.add_constraint(broadcast(p_input)) task.execute() - # Fill the cuNumeric array with the value in the numpy array - def _fill(self, value: Any) -> None: - assert value.scalar + # Fill the cuPyNumeric array with the value in the numpy array + def _fill(self, value: LogicalStore | Scalar) -> None: assert self.base is not None - if self.scalar: - # Handle the 0D case special - self.base.set_storage(value.storage) - elif self.dtype.kind != "V" and self.base.kind is not Future: - # Emit a Legion fill - self.context.issue_fill(self.base, value) + if not self.base.transformed: + # Emit a Legate fill + legate_runtime.issue_fill(self.base, value) else: + if isinstance(value, Scalar): + value = legate_runtime.create_store_from_scalar(value) + # Arg reductions would never fill transformed stores + assert self.dtype.kind != "V" # Perform the fill using a task # If this is a fill for an arg value, make sure to pass # the value dtype so that we get it packed correctly - argval = self.dtype.kind == "V" - task = self.context.create_auto_task(CuNumericOpCode.FILL) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.FILL + ) task.add_output(self.base) task.add_input(value) - task.add_scalar_arg(argval, ty.bool_) task.execute() def fill(self, numpy_array: Any) -> None: @@ -1489,11 +1405,7 @@ def fill(self, numpy_array: Any) -> None: # Have to copy the numpy array because this launch is asynchronous # and we need to make sure the application doesn't mutate the value # so make a future result, this is immediate so no dependence - value = self.runtime.create_scalar(numpy_array.data) - store = self.context.create_store( - self.base.type, shape=(1,), storage=value, optimize_scalar=True - ) - self._fill(store) + self._fill(Scalar(numpy_array.tobytes(), self.base.type)) @auto_convert("rhs1_thunk", "rhs2_thunk") def contract( @@ -1583,7 +1495,7 @@ def contract( # of tensor cores. In the general-purpose tensor contraction case # below the tasks do this adjustment internally. if blas_op is not None and lhs_thunk.dtype == np.float16: - lhs_thunk = self.runtime.create_empty_thunk( + lhs_thunk = runtime.create_empty_thunk( lhs_thunk.shape, ty.float32, inputs=[lhs_thunk] ) @@ -1598,19 +1510,22 @@ def contract( # The underlying libraries are not guaranteed to work with stride # values of 0. The frontend should therefore handle broadcasting # directly, instead of promoting stores. - assert not lhs.has_fake_dims() - assert not rhs1.has_fake_dims() - assert not rhs2.has_fake_dims() + # TODO: We need a better API for this + # assert not lhs.has_fake_dims() + # assert not rhs1.has_fake_dims() + # assert not rhs2.has_fake_dims() # Special cases where we can use BLAS if blas_op is not None: if blas_op == BlasOperation.VV: # Vector dot product - task = self.context.create_auto_task(CuNumericOpCode.DOT) - task.add_reduction(lhs, ReductionOp.ADD) - task.add_input(rhs1) - task.add_input(rhs2) - task.add_alignment(rhs1, rhs2) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.DOT + ) + task.add_reduction(lhs, ReductionOpKind.ADD) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) + task.add_constraint(align(p_rhs1, p_rhs2)) task.execute() elif blas_op == BlasOperation.MV: @@ -1629,12 +1544,14 @@ def contract( rhs2 = rhs2.promote(0, m) lhs = lhs.promote(1, n) - task = self.context.create_auto_task(CuNumericOpCode.MATVECMUL) - task.add_reduction(lhs, ReductionOp.ADD) - task.add_input(rhs1) - task.add_input(rhs2) - task.add_alignment(lhs, rhs1) - task.add_alignment(lhs, rhs2) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.MATVECMUL + ) + p_lhs = task.add_reduction(lhs, ReductionOpKind.ADD) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) + task.add_constraint(align(p_lhs, p_rhs1)) + task.add_constraint(align(p_lhs, p_rhs2)) task.execute() elif blas_op == BlasOperation.MM: @@ -1662,17 +1579,97 @@ def contract( assert m == rhs1.shape[0] assert n == rhs2.shape[1] assert k == rhs2.shape[0] - lhs = lhs.promote(1, k) - rhs1 = rhs1.promote(2, n) - rhs2 = rhs2.promote(0, m) - task = self.context.create_auto_task(CuNumericOpCode.MATMUL) - task.add_reduction(lhs, ReductionOp.ADD) - task.add_input(rhs1) - task.add_input(rhs2) - task.add_alignment(lhs, rhs1) - task.add_alignment(lhs, rhs2) - task.execute() + def rounding_divide( + lhs: tuple[int, ...], rhs: tuple[int, ...] + ) -> tuple[int, ...]: + return tuple( + (lh + rh - 1) // rh for (lh, rh) in zip(lhs, rhs) + ) + + # TODO: better heuristics + def choose_2d_color_shape( + shape: tuple[int, int] + ) -> tuple[int, int]: + # 1M elements, we should probably even go larger + MIN_MATRIX_SIZE = 1 << 20 + # If the matrix is too small don't partition it at all + if (not legate_settings.test()) and shape[0] * shape[ + 1 + ] <= MIN_MATRIX_SIZE: + return (1, 1) + + # start with 1D and re-balance by powers of 2 + # (don't worry about other primes) + color_shape = (runtime.num_procs, 1) + while ( + shape[0] / color_shape[0] + < 2 * shape[1] / color_shape[1] + and color_shape[0] % 2 == 0 + ): + color_shape = (color_shape[0] // 2, color_shape[1] * 2) + + return color_shape + + # TODO: better heuristics? + def choose_batchsize( + tilesize: tuple[int, int], k: int, itemsize: int + ) -> int: + # default corresponds to 128MB (to store A and B tile) + from ..settings import settings + + max_elements_per_tile = ( + settings.matmul_cache_size() // itemsize + ) + total_elements_rhs = (tilesize[0] + tilesize[1]) * k + num_batches = rounding_divide( + (total_elements_rhs,), (max_elements_per_tile,) + )[0] + batch_size = rounding_divide((k,), (num_batches,))[0] + + return batch_size + + # choose color-shape/k_batch_size + initial_color_shape = choose_2d_color_shape((m, n)) + tile_shape = rounding_divide((m, n), initial_color_shape) + color_shape = rounding_divide((m, n), tile_shape) + k_batch_size = choose_batchsize( + tile_shape, k, rhs1_thunk.dtype.itemsize # type: ignore + ) + k_color = rounding_divide((k,), (k_batch_size,)) + + # initial partition of lhs defined py tile-shape + tiled_lhs = lhs.partition_by_tiling(tile_shape) + tiled_rhs1 = rhs1.partition_by_tiling( + (tile_shape[0], k_batch_size) + ) + tiled_rhs2 = rhs2.partition_by_tiling( + (k_batch_size, tile_shape[1]) + ) + + def run_matmul_for_batch( + tiled_lhs: LogicalStorePartition, + tiled_rhs1: LogicalStorePartition, + tiled_rhs2: LogicalStorePartition, + i: int, + ) -> None: + manual_task = legate_runtime.create_manual_task( + self.library, CuPyNumericOpCode.MATMUL, color_shape + ) + + manual_task.add_output(tiled_lhs) + manual_task.add_input(tiled_lhs) + manual_task.add_input( + tiled_rhs1, (dimension(0), constant(i)) + ) + manual_task.add_input( + tiled_rhs2, (constant(i), dimension(1)) + ) + + manual_task.execute() + + for i in range(0, k_color[0]): + run_matmul_for_batch(tiled_lhs, tiled_rhs1, tiled_rhs2, i) else: assert False @@ -1693,8 +1690,8 @@ def contract( # Transpose arrays according to alphabetical order of mode labels def alphabetical_transpose( - store: Store, modes: Sequence[str] - ) -> Store: + store: LogicalStore, modes: Sequence[str] + ) -> LogicalStore: perm = tuple( dim for (_, dim) in sorted(zip(modes, range(len(modes)))) ) @@ -1712,7 +1709,7 @@ def alphabetical_transpose( extent = mode2extent[mode] def add_mode( - store: Store, modes: Sequence[str], dim_mask: list[bool] + store: LogicalStore, modes: Sequence[str], dim_mask: list[bool] ) -> Any: if mode not in modes: dim_mask.append(False) @@ -1728,37 +1725,41 @@ def add_mode( assert lhs.shape == rhs2.shape # Prepare the launch - task = self.context.create_auto_task(CuNumericOpCode.CONTRACT) - task.add_reduction(lhs, ReductionOp.ADD) - task.add_input(rhs1) - task.add_input(rhs2) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.CONTRACT + ) + p_lhs = task.add_reduction(lhs, ReductionOpKind.ADD) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) task.add_scalar_arg(tuple(lhs_dim_mask), (ty.bool_,)) task.add_scalar_arg(tuple(rhs1_dim_mask), (ty.bool_,)) task.add_scalar_arg(tuple(rhs2_dim_mask), (ty.bool_,)) - task.add_alignment(lhs, rhs1) - task.add_alignment(lhs, rhs2) + task.add_constraint(align(p_lhs, p_rhs1)) + task.add_constraint(align(p_lhs, p_rhs2)) task.execute() # Create array from input array and indices def choose(self, rhs: Any, *args: Any) -> None: # convert all arrays to deferred - index_arr = self.runtime.to_deferred_array(rhs) - ch_def = tuple(self.runtime.to_deferred_array(c) for c in args) + index_arr = runtime.to_deferred_array(rhs, read_only=True) + ch_def = tuple( + runtime.to_deferred_array(c, read_only=True) for c in args + ) out_arr = self.base # broadcast input array and all choices arrays to the same shape - index = index_arr._broadcast(out_arr.shape.extents) - ch_tuple = tuple(c._broadcast(out_arr.shape.extents) for c in ch_def) - - task = self.context.create_auto_task(CuNumericOpCode.CHOOSE) - task.add_output(out_arr) - task.add_input(index) - for c in ch_tuple: - task.add_input(c) + index = index_arr._broadcast(tuple(out_arr.shape)) + ch_tuple = tuple(c._broadcast(tuple(out_arr.shape)) for c in ch_def) - task.add_alignment(index, out_arr) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.CHOOSE + ) + p_out = task.add_output(out_arr) + p_ind = task.add_input(index) + task.add_constraint(align(p_ind, p_out)) for c in ch_tuple: - task.add_alignment(index, c) + p_c = task.add_input(c) + task.add_constraint(align(p_ind, p_c)) task.execute() def select( @@ -1767,19 +1768,23 @@ def select( choicelist: Iterable[Any], default: npt.NDArray[Any], ) -> None: - condlist_ = tuple(self.runtime.to_deferred_array(c) for c in condlist) + condlist_ = tuple( + runtime.to_deferred_array(c, read_only=True) for c in condlist + ) choicelist_ = tuple( - self.runtime.to_deferred_array(c) for c in choicelist + runtime.to_deferred_array(c, read_only=True) for c in choicelist ) - task = self.context.create_auto_task(CuNumericOpCode.SELECT) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.SELECT + ) out_arr = self.base task.add_output(out_arr) for c in chain(condlist_, choicelist_): c_arr = c._broadcast(self.shape) task.add_input(c_arr) task.add_alignment(c_arr, out_arr) - task.add_scalar_arg(default, to_core_dtype(default.dtype)) + task.add_scalar_arg(default, to_core_type(default.dtype)) task.execute() # Create or extract a diagonal from a matrix @@ -1835,17 +1840,19 @@ def _diag_helper( else: diag = diag.promote(0, matrix.shape[0]) - task = self.context.create_auto_task(CuNumericOpCode.DIAG) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.DIAG + ) if extract: - task.add_reduction(diag, ReductionOp.ADD) - task.add_input(matrix) - task.add_alignment(matrix, diag) + p_diag = task.add_reduction(diag, ReductionOpKind.ADD) + p_mat = task.add_input(matrix) + task.add_constraint(align(p_mat, p_diag)) else: - task.add_output(matrix) - task.add_input(diag) - task.add_input(matrix) - task.add_alignment(diag, matrix) + p_mat = task.add_output(matrix) + p_diag = task.add_input(diag) + task.add_input(matrix, p_mat) + task.add_constraint(align(p_diag, p_mat)) task.add_scalar_arg(naxes, ty.int32) task.add_scalar_arg(extract, ty.bool_) @@ -1854,15 +1861,15 @@ def _diag_helper( @auto_convert("indices", "values") def put(self, indices: Any, values: Any, check_bounds: bool) -> None: - if indices.base.kind == Future or indices.base.transformed: - change_shape = indices.base.kind == Future + if indices.base.has_scalar_storage or indices.base.transformed: + change_shape = indices.base.has_scalar_storage indices = indices._convert_future_to_regionfield(change_shape) - if values.base.kind == Future or values.base.transformed: - change_shape = values.base.kind == Future + if values.base.has_scalar_storage or values.base.transformed: + change_shape = values.base.has_scalar_storage values = values._convert_future_to_regionfield(change_shape) - if self.base.kind == Future or self.base.transformed: - change_shape = self.base.kind == Future + if self.base.has_scalar_storage or self.base.transformed: + change_shape = self.base.has_scalar_storage self_tmp = self._convert_future_to_regionfield(change_shape) else: self_tmp = self @@ -1876,10 +1883,10 @@ def put(self, indices: Any, values: Any, check_bounds: bool) -> None: # (indices.size,) shape and is used to copy data from values # to the target ND array (self) N = self_tmp.ndim - pointN_dtype = self.runtime.get_point_type(N) + pointN_dtype = ty.point_type(N) indirect = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( shape=indices.shape, dtype=pointN_dtype, inputs=[indices], @@ -1887,24 +1894,21 @@ def put(self, indices: Any, values: Any, check_bounds: bool) -> None: ) shape = self_tmp.shape - task = self.context.create_auto_task(CuNumericOpCode.WRAP) - task.add_output(indirect.base) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.WRAP + ) + p_indirect = task.add_output(indirect.base) task.add_scalar_arg(shape, (ty.int64,)) task.add_scalar_arg(True, ty.bool_) # has_input task.add_scalar_arg(check_bounds, ty.bool_) - task.add_input(indices.base) - task.add_alignment(indices.base, indirect.base) + p_indices = task.add_input(indices.base) + task.add_constraint(align(p_indices, p_indirect)) task.throws_exception(IndexError) task.execute() - if indirect.base.kind == Future: + if indirect.base.has_scalar_storage: indirect = indirect._convert_future_to_regionfield() - copy = self.context.create_copy() - copy.set_target_indirect_out_of_range(False) - copy.add_input(values.base) - copy.add_target_indirect(indirect.base) - copy.add_output(self_tmp.base) - copy.execute() + legate_runtime.issue_scatter(self_tmp.base, indirect.base, values.base) if self_tmp is not self: self.copy(self_tmp, deep=True) @@ -1912,19 +1916,20 @@ def put(self, indices: Any, values: Any, check_bounds: bool) -> None: @auto_convert("mask", "values") def putmask(self, mask: Any, values: Any) -> None: assert self.shape == mask.shape - values = values._copy_if_overlapping(self) + values = values._copy_if_partially_overlapping(self) if values.shape != self.shape: values_new = values._broadcast(self.shape) else: values_new = values.base - - task = self.context.create_auto_task(CuNumericOpCode.PUTMASK) - task.add_input(self.base) - task.add_input(mask.base) - task.add_input(values_new) - task.add_output(self.base) - task.add_alignment(self.base, mask.base) - task.add_alignment(self.base, values_new) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.PUTMASK + ) + p_self = task.add_input(self.base) + p_mask = task.add_input(mask.base) + p_values = task.add_input(values_new) + task.add_output(self.base, p_self) + task.add_constraint(align(p_self, p_mask)) + task.add_constraint(align(p_self, p_values)) task.execute() # Create an identity array with the ones offset from the diagonal by k @@ -1941,7 +1946,9 @@ def eye(self, k: int) -> None: # privilege, then, is not appropriate for this call, as it essentially # tells the runtime that it can throw away the previous contents of the # entire region. - task = self.context.create_auto_task(CuNumericOpCode.EYE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.EYE + ) task.add_input(self.base) task.add_output(self.base) task.add_scalar_arg(k, ty.int32) @@ -1953,30 +1960,21 @@ def arange(self, start: float, stop: float, step: float) -> None: if self.scalar: # Handle the special case of a single value here assert self.shape[0] == 1 - array = np.array(start, dtype=self.dtype) - future = self.runtime.create_scalar(array.data) - self.base.set_storage(future) + legate_runtime.issue_fill(self.base, Scalar(start, self.base.type)) return - def create_scalar(value: Any, dtype: np.dtype[Any]) -> Any: - array = np.array(value, dtype) - return self.runtime.create_wrapped_scalar( - array.data, - array.dtype, - shape=(1,), - ).base - - task = self.context.create_auto_task(CuNumericOpCode.ARANGE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.ARANGE + ) task.add_output(self.base) - task.add_input(create_scalar(start, self.dtype)) - task.add_input(create_scalar(stop, self.dtype)) - task.add_input(create_scalar(step, self.dtype)) + task.add_scalar_arg(start, self.base.type) + task.add_scalar_arg(step, self.base.type) task.execute() # Tile the src array onto the destination array @auto_convert("rhs") - def tile(self, rhs: Any, reps: Union[Any, Sequence[int]]) -> None: + def tile(self, rhs: Any, reps: Any | Sequence[int]) -> None: src_array = rhs dst_array = self assert src_array.ndim <= dst_array.ndim @@ -1985,36 +1983,40 @@ def tile(self, rhs: Any, reps: Union[Any, Sequence[int]]) -> None: self._fill(src_array.base) return - task = self.context.create_auto_task(CuNumericOpCode.TILE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.TILE + ) task.add_output(self.base) - task.add_input(rhs.base) + p_rhs = task.add_input(rhs.base) - task.add_broadcast(rhs.base) + task.add_constraint(broadcast(p_rhs)) task.execute() # Transpose the matrix dimensions def transpose( - self, axes: Union[None, tuple[int, ...], list[int]] + self, axes: tuple[int, ...] | list[int] | None ) -> DeferredArray: computed_axes = tuple(axes) if axes is not None else () result = self.base.transpose(computed_axes) - return DeferredArray(self.runtime, result) + return DeferredArray(result) @auto_convert("rhs") def trilu(self, rhs: Any, k: int, lower: bool) -> None: lhs = self.base rhs = rhs._broadcast(lhs.shape) - task = self.context.create_auto_task(CuNumericOpCode.TRILU) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.TRILU + ) - task.add_output(lhs) - task.add_input(rhs) + p_lhs = task.add_output(lhs) + p_rhs = task.add_input(rhs) task.add_scalar_arg(lower, ty.bool_) task.add_scalar_arg(k, ty.int32) - task.add_alignment(lhs, rhs) + task.add_constraint(align(p_lhs, p_rhs)) task.execute() @@ -2022,10 +2024,34 @@ def trilu(self, rhs: Any, k: int, lower: bool) -> None: def repeat( self, repeats: Any, axis: int, scalar_repeats: bool ) -> DeferredArray: - out = self.runtime.create_unbound_thunk(self.base.type, ndim=self.ndim) - task = self.context.create_auto_task(CuNumericOpCode.REPEAT) - task.add_input(self.base) - task.add_output(out.base) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.REPEAT + ) + if scalar_repeats: + out_shape = tuple( + self.shape[dim] * repeats if dim == axis else self.shape[dim] + for dim in range(self.ndim) + ) + out = cast( + DeferredArray, + runtime.create_empty_thunk( + out_shape, + dtype=self.base.type, + inputs=[self], + ), + ) + p_self = task.declare_partition() + p_out = task.declare_partition() + task.add_input(self.base, p_self) + task.add_output(out.base, p_out) + factors = tuple( + repeats if dim == axis else 1 for dim in range(self.ndim) + ) + task.add_constraint(scale(factors, p_self, p_out)) + else: + out = runtime.create_unbound_thunk(self.base.type, ndim=self.ndim) + p_self = task.add_input(self.base) + task.add_output(out.base) # We pass axis now but don't use for 1D case (will use for ND case task.add_scalar_arg(axis, ty.int32) task.add_scalar_arg(scalar_repeats, ty.bool_) @@ -2033,18 +2059,18 @@ def repeat( task.add_scalar_arg(repeats, ty.int64) else: shape = self.shape - repeats = self.runtime.to_deferred_array(repeats).base + repeats = runtime.to_deferred_array(repeats, read_only=True).base for dim, extent in enumerate(shape): if dim == axis: continue repeats = repeats.promote(dim, extent) - task.add_input(repeats) - task.add_alignment(self.base, repeats) + p_repeats = task.add_input(repeats) + task.add_constraint(align(p_self, p_repeats)) task.execute() return out @auto_convert("rhs") - def flip(self, rhs: Any, axes: Union[None, int, tuple[int, ...]]) -> None: + def flip(self, rhs: Any, axes: int | tuple[int, ...] | None) -> None: input = rhs.base output = self.base @@ -2053,19 +2079,21 @@ def flip(self, rhs: Any, axes: Union[None, int, tuple[int, ...]]) -> None: else: axes = normalize_axis_tuple(axes, self.ndim) - task = self.context.create_auto_task(CuNumericOpCode.FLIP) - task.add_output(output) - task.add_input(input) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.FLIP + ) + p_out = task.add_output(output) + p_in = task.add_input(input) task.add_scalar_arg(axes, (ty.int32,)) - task.add_broadcast(input) - task.add_alignment(input, output) + task.add_constraint(broadcast(p_in)) + task.add_constraint(align(p_in, p_out)) task.execute() # Perform a bin count operation on the array @auto_convert("rhs", "weights") - def bincount(self, rhs: Any, weights: Optional[NumPyThunk] = None) -> None: + def bincount(self, rhs: Any, weights: NumPyThunk | None = None) -> None: weight_array = weights src_array = rhs dst_array = self @@ -2075,39 +2103,37 @@ def bincount(self, rhs: Any, weights: Optional[NumPyThunk] = None) -> None: assert src_array.shape == weight_array.shape or ( src_array.size == 1 and weight_array.size == 1 ) - else: - weight_array = self.runtime.create_wrapped_scalar( - np.array(1, dtype=np.int64), - np.dtype(np.int64), - shape=(), - ) dst_array.fill(np.array(0, dst_array.dtype)) - task = self.context.create_auto_task(CuNumericOpCode.BINCOUNT) - task.add_reduction(dst_array.base, ReductionOp.ADD) - task.add_input(src_array.base) - task.add_input(weight_array.base) # type: ignore - - task.add_broadcast(dst_array.base) - if not weight_array.scalar: - task.add_alignment(src_array.base, weight_array.base) # type: ignore # noqa + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.BINCOUNT + ) + p_dst = task.add_reduction(dst_array.base, ReductionOpKind.ADD) + p_src = task.add_input(src_array.base) + task.add_constraint(broadcast(p_dst)) + if weight_array is not None: + p_weight = task.add_input(cast(DeferredArray, weight_array).base) + if not weight_array.scalar: + task.add_constraint(align(p_src, p_weight)) task.execute() def nonzero(self) -> tuple[NumPyThunk, ...]: results = tuple( - self.runtime.create_unbound_thunk(ty.int64) - for _ in range(self.ndim) + runtime.create_unbound_thunk(ty.int64) for _ in range(self.ndim) ) - task = self.context.create_auto_task(CuNumericOpCode.NONZERO) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.NONZERO + ) - task.add_input(self.base) + p_self = task.add_input(self.base) for result in results: task.add_output(result.base) - task.add_broadcast(self.base, axes=range(1, self.ndim)) + if self.ndim > 1: + task.add_constraint(broadcast(p_self, range(1, self.ndim))) task.execute() return results @@ -2116,10 +2142,12 @@ def bitgenerator_random_raw( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: - task = self.context.create_auto_task(CuNumericOpCode.BITGENERATOR) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.BITGENERATOR + ) task.add_output(self.base) @@ -2138,14 +2166,16 @@ def bitgenerator_distribution( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, distribution: BitGeneratorDistribution, intparams: tuple[int, ...], floatparams: tuple[float, ...], doubleparams: tuple[float, ...], ) -> None: - task = self.context.create_auto_task(CuNumericOpCode.BITGENERATOR) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.BITGENERATOR + ) task.add_output(self.base) @@ -2168,7 +2198,7 @@ def bitgenerator_integers( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: int, high: int, @@ -2192,7 +2222,7 @@ def bitgenerator_uniform( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: float, high: float, @@ -2226,7 +2256,7 @@ def bitgenerator_lognormal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -2260,7 +2290,7 @@ def bitgenerator_normal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -2294,7 +2324,7 @@ def bitgenerator_poisson( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, ) -> None: @@ -2320,7 +2350,7 @@ def bitgenerator_exponential( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, scale: float, ) -> None: @@ -2353,7 +2383,7 @@ def bitgenerator_gumbel( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -2387,7 +2417,7 @@ def bitgenerator_laplace( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -2421,7 +2451,7 @@ def bitgenerator_logistic( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -2455,7 +2485,7 @@ def bitgenerator_pareto( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -2488,7 +2518,7 @@ def bitgenerator_power( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -2521,7 +2551,7 @@ def bitgenerator_rayleigh( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, sigma: float, ) -> None: @@ -2554,7 +2584,7 @@ def bitgenerator_cauchy( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, x0: float, gamma: float, @@ -2588,7 +2618,7 @@ def bitgenerator_triangular( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -2623,7 +2653,7 @@ def bitgenerator_weibull( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, k: float, @@ -2657,7 +2687,7 @@ def bitgenerator_bytes( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: if self.dtype == np.uint8: @@ -2679,7 +2709,7 @@ def bitgenerator_beta( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -2713,7 +2743,7 @@ def bitgenerator_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -2747,7 +2777,7 @@ def bitgenerator_logseries( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -2770,7 +2800,7 @@ def bitgenerator_noncentral_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -2805,7 +2835,7 @@ def bitgenerator_chisquare( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, nonc: float, @@ -2839,7 +2869,7 @@ def bitgenerator_gamma( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, k: float, theta: float, @@ -2873,7 +2903,7 @@ def bitgenerator_standard_t( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, ) -> None: @@ -2906,7 +2936,7 @@ def bitgenerator_hypergeometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ngood: int, nbad: int, @@ -2934,7 +2964,7 @@ def bitgenerator_vonmises( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, kappa: float, @@ -2968,7 +2998,7 @@ def bitgenerator_zipf( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -2992,7 +3022,7 @@ def bitgenerator_geometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -3018,7 +3048,7 @@ def bitgenerator_wald( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, scale: float, @@ -3052,7 +3082,7 @@ def bitgenerator_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -3080,7 +3110,7 @@ def bitgenerator_negative_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -3104,15 +3134,18 @@ def bitgenerator_negative_binomial( doubleparams, ) - def random(self, gen_code: Any, args: Any = ()) -> None: - task = self.context.create_auto_task(CuNumericOpCode.RAND) + def random(self, gen_code: Any, args: tuple[Scalar, ...] = ()) -> None: + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.RAND + ) task.add_output(self.base) task.add_scalar_arg(gen_code.value, ty.int32) - epoch = self.runtime.get_next_random_epoch() + epoch = runtime.get_next_random_epoch() task.add_scalar_arg(epoch, ty.uint32) task.add_scalar_arg(self.compute_strides(self.shape), (ty.int64,)) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) task.execute() @@ -3126,13 +3159,12 @@ def random_normal(self) -> None: def random_integer( self, - low: Union[int, npt.NDArray[Any]], - high: Union[int, npt.NDArray[Any]], + low: int | npt.NDArray[Any], + high: int | npt.NDArray[Any], ) -> None: assert self.dtype.kind == "i" - low = np.array(low, self.dtype) - high = np.array(high, self.dtype) - self.random(RandGenCode.INTEGER, [low, high]) + args = (Scalar(low, self.base.type), Scalar(high, self.base.type)) + self.random(RandGenCode.INTEGER, args) # Perform the unary operation and put the result in the array @auto_convert("src") @@ -3141,26 +3173,30 @@ def unary_op( op: UnaryOpCode, src: Any, where: Any, - args: Any, - multiout: Optional[Any] = None, + args: tuple[Scalar, ...] = (), + multiout: Any | None = None, ) -> None: lhs = self.base - src = src._copy_if_overlapping(self) + src = src._copy_if_partially_overlapping(self) rhs = src._broadcast(lhs.shape) with Annotation({"OpCode": op.name}): - task = self.context.create_auto_task(CuNumericOpCode.UNARY_OP) - task.add_output(lhs) - task.add_input(rhs) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.UNARY_OP + ) + p_lhs = task.add_output(lhs) + p_rhs = task.add_input(rhs) task.add_scalar_arg(op.value, ty.int32) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) - task.add_alignment(lhs, rhs) + task.add_constraint(align(p_lhs, p_rhs)) if multiout is not None: for out in multiout: - task.add_output(out.base) - task.add_alignment(out.base, rhs) + out_def = runtime.to_deferred_array(out, read_only=False) + p_out = task.add_output(out_def.base) + task.add_constraint(align(p_out, p_rhs)) task.execute() @@ -3172,13 +3208,13 @@ def unary_reduction( op: UnaryRedCode, src: Any, where: Any, - orig_axis: Union[int, None], + orig_axis: int | None, axes: tuple[int, ...], keepdims: bool, - args: Any, + args: tuple[Scalar, ...], initial: Any, ) -> None: - lhs_array: Union[NumPyThunk, DeferredArray] = self + lhs_array: NumPyThunk | DeferredArray = self rhs_array = src assert lhs_array.ndim <= rhs_array.ndim @@ -3190,8 +3226,8 @@ def unary_reduction( ) if argred: - argred_dtype = self.runtime.get_argred_type(rhs_array.base.type) - lhs_array = self.runtime.create_empty_thunk( + argred_dtype = runtime.get_argred_type(rhs_array.base.type) + lhs_array = runtime.create_empty_thunk( lhs_array.shape, dtype=argred_dtype, inputs=[self], @@ -3217,8 +3253,8 @@ def unary_reduction( lhs = lhs.project(0, 0) with Annotation({"OpCode": op.name, "ArgRed?": str(argred)}): - task = self.context.create_auto_task( - CuNumericOpCode.SCALAR_UNARY_RED + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.SCALAR_UNARY_RED ) task.add_reduction(lhs, _UNARY_RED_TO_REDUCTION_OPS[op]) @@ -3230,7 +3266,8 @@ def unary_reduction( task.add_input(where.base) task.add_alignment(rhs_array.base, where.base) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) task.execute() @@ -3262,10 +3299,14 @@ def unary_reduction( ) with Annotation({"OpCode": op.name, "ArgRed?": str(argred)}): - task = self.context.create_auto_task(CuNumericOpCode.UNARY_RED) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.UNARY_RED + ) - task.add_input(rhs_array.base) - task.add_reduction(result, _UNARY_RED_TO_REDUCTION_OPS[op]) + p_rhs = task.add_input(rhs_array.base) + p_result = task.add_reduction( + result, _UNARY_RED_TO_REDUCTION_OPS[op] + ) task.add_scalar_arg(axis, ty.int32) task.add_scalar_arg(op, ty.int32) task.add_scalar_arg(is_where, ty.bool_) @@ -3273,9 +3314,10 @@ def unary_reduction( task.add_input(where.base) task.add_alignment(rhs_array.base, where.base) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) - task.add_alignment(result, rhs_array.base) + task.add_constraint(align(p_result, p_rhs)) task.execute() @@ -3284,7 +3326,6 @@ def unary_reduction( UnaryOpCode.GETARG, lhs_array, True, - [], ) def isclose( @@ -3292,8 +3333,8 @@ def isclose( ) -> None: assert not equal_nan args = ( - np.array(rtol, dtype=np.float64), - np.array(atol, dtype=np.float64), + Scalar(rtol, ty.float64), + Scalar(atol, ty.float64), ) self.binary_op(BinaryOpCode.ISCLOSE, rhs1, rhs2, True, args) @@ -3305,25 +3346,28 @@ def binary_op( src1: Any, src2: Any, where: Any, - args: Any, + args: tuple[Scalar, ...], ) -> None: lhs = self.base - src1 = src1._copy_if_overlapping(self) + src1 = src1._copy_if_partially_overlapping(self) rhs1 = src1._broadcast(lhs.shape) - src2 = src2._copy_if_overlapping(self) + src2 = src2._copy_if_partially_overlapping(self) rhs2 = src2._broadcast(lhs.shape) with Annotation({"OpCode": op_code.name}): # Populate the Legate launcher - task = self.context.create_auto_task(CuNumericOpCode.BINARY_OP) - task.add_output(lhs) - task.add_input(rhs1) - task.add_input(rhs2) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.BINARY_OP + ) + p_lhs = task.add_output(lhs) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) task.add_scalar_arg(op_code.value, ty.int32) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) - task.add_alignment(lhs, rhs1) - task.add_alignment(lhs, rhs2) + task.add_constraint(align(p_lhs, p_rhs1)) + task.add_constraint(align(p_lhs, p_rhs2)) task.execute() @@ -3333,13 +3377,13 @@ def binary_reduction( op: BinaryOpCode, src1: Any, src2: Any, - broadcast: Union[NdShape, None], - args: Any, + broadcast: NdShape | None, + args: tuple[Scalar, ...], ) -> None: lhs = self.base rhs1 = src1.base rhs2 = src2.base - assert lhs.scalar + assert lhs.has_scalar_storage if broadcast is not None: rhs1 = rhs1._broadcast(broadcast) @@ -3347,19 +3391,22 @@ def binary_reduction( # Populate the Legate launcher if op == BinaryOpCode.NOT_EQUAL: - redop = ReductionOp.ADD + redop = ReductionOpKind.ADD self.fill(np.array(False)) else: - redop = ReductionOp.MUL + redop = ReductionOpKind.MUL self.fill(np.array(True)) - task = self.context.create_auto_task(CuNumericOpCode.BINARY_RED) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.BINARY_RED + ) task.add_reduction(lhs, redop) - task.add_input(rhs1) - task.add_input(rhs2) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) task.add_scalar_arg(op.value, ty.int32) - self.add_arguments(task, args) + for arg in args: + task.add_scalar_arg(arg) - task.add_alignment(rhs1, rhs2) + task.add_constraint(align(p_rhs1, p_rhs2)) task.execute() @@ -3371,48 +3418,36 @@ def where(self, src1: Any, src2: Any, src3: Any) -> None: rhs3 = src3._broadcast(lhs.shape) # Populate the Legate launcher - task = self.context.create_auto_task(CuNumericOpCode.WHERE) - task.add_output(lhs) - task.add_input(rhs1) - task.add_input(rhs2) - task.add_input(rhs3) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.WHERE + ) + p_lhs = task.add_output(lhs) + p_rhs1 = task.add_input(rhs1) + p_rhs2 = task.add_input(rhs2) + p_rhs3 = task.add_input(rhs3) - task.add_alignment(lhs, rhs1) - task.add_alignment(lhs, rhs2) - task.add_alignment(lhs, rhs3) + task.add_constraint(align(p_lhs, p_rhs1)) + task.add_constraint(align(p_lhs, p_rhs2)) + task.add_constraint(align(p_lhs, p_rhs3)) task.execute() def argwhere(self) -> NumPyThunk: - result = self.runtime.create_unbound_thunk(ty.int64, ndim=2) + result = runtime.create_unbound_thunk(ty.int64, ndim=2) - task = self.context.create_auto_task(CuNumericOpCode.ARGWHERE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.ARGWHERE + ) task.add_output(result.base) - task.add_input(self.base) - task.add_broadcast(self.base, axes=range(1, self.ndim)) + p_self = task.add_input(self.base) + if self.ndim > 1: + task.add_constraint(broadcast(p_self, range(1, self.ndim))) task.execute() return result - # A helper method for attaching arguments - def add_arguments( - self, - task: Union[AutoTask, ManualTask], - args: Optional[Sequence[npt.NDArray[Any]]], - ) -> None: - if args is None: - return - for numpy_array in args: - assert numpy_array.size == 1 - scalar = self.runtime.create_wrapped_scalar( - numpy_array.data, - numpy_array.dtype, - shape=(1,), - ) - task.add_input(scalar.base) - @staticmethod def compute_strides(shape: NdShape) -> tuple[int, ...]: stride = 1 @@ -3423,12 +3458,20 @@ def compute_strides(shape: NdShape) -> tuple[int, ...]: return result @auto_convert("src") - def cholesky(self, src: Any, no_tril: bool = False) -> None: - cholesky(self, src, no_tril) + def cholesky(self, src: Any) -> None: + cholesky_deferred(self, src) + + @auto_convert("q", "r") + def qr(self, q: Any, r: Any) -> None: + qr_deferred(self, q, r) @auto_convert("a", "b") def solve(self, a: Any, b: Any) -> None: - solve(self, a, b) + solve_deferred(self, a, b) + + @auto_convert("u", "s", "vh") + def svd(self, u: Any, s: Any, vh: Any) -> None: + svd_deferred(self, u, s, vh) @auto_convert("rhs") def scan( @@ -3436,12 +3479,12 @@ def scan( op: int, rhs: Any, axis: int, - dtype: Optional[npt.DTypeLike], + dtype: npt.DTypeLike | None, nan_to_identity: bool, ) -> None: # local sum # storage for local sums accessible - temp = self.runtime.create_unbound_thunk( + temp = runtime.create_unbound_thunk( dtype=self.base.type, ndim=self.ndim ) @@ -3451,33 +3494,37 @@ def scan( else: # swap axes, always performing scan along last axis swapped = rhs.swapaxes(axis, rhs.ndim - 1) - input = self.runtime.create_empty_thunk( + input = runtime.create_empty_thunk( swapped.shape, dtype=rhs.base.type, inputs=(rhs, swapped) ) input.copy(swapped, deep=True) output = input - task = output.context.create_auto_task(CuNumericOpCode.SCAN_LOCAL) - task.add_output(output.base) - task.add_input(input.base) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.SCAN_LOCAL + ) + p_out = task.add_output(output.base) + p_in = task.add_input(input.base) task.add_output(temp.base) task.add_scalar_arg(op, ty.int32) task.add_scalar_arg(nan_to_identity, ty.bool_) - task.add_alignment(input.base, output.base) + task.add_constraint(align(p_in, p_out)) task.execute() # Global sum # NOTE: Assumes the partitioning stays the same from previous task. # NOTE: Each node will do a sum up to its index, alternatively could # do one centralized scan and broadcast (slightly less redundant work) - task = output.context.create_auto_task(CuNumericOpCode.SCAN_GLOBAL) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.SCAN_GLOBAL + ) task.add_input(output.base) - task.add_input(temp.base) + p_temp = task.add_input(temp.base) task.add_output(output.base) task.add_scalar_arg(op, ty.int32) - task.add_broadcast(temp.base) + task.add_constraint(broadcast(p_temp)) task.execute() @@ -3488,45 +3535,49 @@ def scan( self.copy(swapped, deep=True) def unique(self) -> NumPyThunk: - result = self.runtime.create_unbound_thunk(self.base.type) + result = runtime.create_unbound_thunk(self.base.type) - task = self.context.create_auto_task(CuNumericOpCode.UNIQUE) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.UNIQUE + ) task.add_output(result.base) task.add_input(self.base) - if self.runtime.num_gpus > 0: + if runtime.num_gpus > 0: task.add_nccl_communicator() task.execute() - if self.runtime.num_gpus == 0 and self.runtime.num_procs > 1: - result.base = self.context.tree_reduce( - CuNumericOpCode.UNIQUE_REDUCE, result.base + if runtime.num_gpus == 0 and runtime.num_procs > 1: + result.base = legate_runtime.tree_reduce( + self.library, CuPyNumericOpCode.UNIQUE_REDUCE, result.base ) return result @auto_convert("rhs", "v") def searchsorted(self, rhs: Any, v: Any, side: SortSide = "left") -> None: - task = self.context.create_auto_task(CuNumericOpCode.SEARCHSORTED) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.SEARCHSORTED + ) is_left = side == "left" if is_left: self.fill(np.array(rhs.size, self.dtype)) - task.add_reduction(self.base, ReductionOp.MIN) + p_self = task.add_reduction(self.base, ReductionOpKind.MIN) else: self.fill(np.array(0, self.dtype)) - task.add_reduction(self.base, ReductionOp.MAX) + p_self = task.add_reduction(self.base, ReductionOpKind.MAX) task.add_input(rhs.base) - task.add_input(v.base) + p_v = task.add_input(v.base) # every partition needs the value information - task.add_broadcast(v.base) - task.add_broadcast(self.base) - task.add_alignment(self.base, v.base) + task.add_constraint(broadcast(p_v)) + task.add_constraint(broadcast(p_self)) + task.add_constraint(align(p_self, p_v)) task.add_scalar_arg(is_left, ty.bool_) task.add_scalar_arg(rhs.size, ty.int64) @@ -3537,9 +3588,9 @@ def sort( self, rhs: Any, argsort: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SortType = "quicksort", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: if kind == "stable": stable = True @@ -3548,37 +3599,39 @@ def sort( if order is not None: raise NotImplementedError( - "cuNumeric does not support sorting with 'order' as " + "cuPyNumeric does not support sorting with 'order' as " "ndarray only supports numeric values" ) if axis is not None and (axis >= rhs.ndim or axis < -rhs.ndim): raise ValueError("invalid axis") - sort(self, rhs, argsort, axis, stable) + sort_deferred(self, rhs, argsort, axis, stable) @auto_convert("rhs") def partition( self, rhs: Any, - kth: Union[int, Sequence[int]], + kth: int | Sequence[int], argpartition: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SelectKind = "introselect", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: if order is not None: raise NotImplementedError( - "cuNumeric does not support partitioning with 'order' as " + "cuPyNumeric does not support partitioning with 'order' as " "ndarray only supports numeric values" ) if axis is not None and (axis >= rhs.ndim or axis < -rhs.ndim): raise ValueError("invalid axis") # fallback to sort for now - sort(self, rhs, argpartition, axis, False) + sort_deferred(self, rhs, argpartition, axis, False) def create_window(self, op_code: WindowOpCode, M: int, *args: Any) -> None: - task = self.context.create_auto_task(CuNumericOpCode.WINDOW) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.WINDOW + ) task.add_output(self.base) task.add_scalar_arg(op_code, ty.int32) task.add_scalar_arg(M, ty.int64) @@ -3587,70 +3640,69 @@ def create_window(self, op_code: WindowOpCode, M: int, *args: Any) -> None: task.execute() @auto_convert("src") - def packbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder - ) -> None: + def packbits(self, src: Any, axis: int | None, bitorder: BitOrder) -> None: bitorder_code = getattr(Bitorder, bitorder.upper()) - task = self.context.create_auto_task(CuNumericOpCode.PACKBITS) - p_out = task.declare_partition(self.base) - p_in = task.declare_partition(src.base) - task.add_output(self.base, partition=p_out) - task.add_input(src.base, partition=p_in) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.PACKBITS + ) + p_out = task.declare_partition() + p_in = task.declare_partition() + task.add_output(self.base, p_out) + task.add_input(src.base, p_in) task.add_scalar_arg(axis, ty.uint32) task.add_scalar_arg(bitorder_code, ty.uint32) - scale = tuple(8 if dim == axis else 1 for dim in range(src.ndim)) - task.add_constraint(p_in <= p_out * scale) # type: ignore + factors = tuple(8 if dim == axis else 1 for dim in range(src.ndim)) + task.add_constraint(scale(factors, p_out, p_in)) # type: ignore task.execute() @auto_convert("src") def unpackbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder + self, src: Any, axis: int | None, bitorder: BitOrder ) -> None: bitorder_code = getattr(Bitorder, bitorder.upper()) - task = self.context.create_auto_task(CuNumericOpCode.UNPACKBITS) - p_out = task.declare_partition(self.base) - p_in = task.declare_partition(src.base) - task.add_output(self.base, partition=p_out) - task.add_input(src.base, partition=p_in) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.UNPACKBITS + ) + p_out = task.declare_partition() + p_in = task.declare_partition() + task.add_output(self.base, p_out) + task.add_input(src.base, p_in) task.add_scalar_arg(axis, ty.uint32) task.add_scalar_arg(bitorder_code, ty.uint32) - scale = tuple(8 if dim == axis else 1 for dim in range(src.ndim)) - task.add_constraint(p_out <= p_in * scale) # type: ignore + factors = tuple(8 if dim == axis else 1 for dim in range(src.ndim)) + task.add_constraint(scale(factors, p_in, p_out)) # type: ignore task.execute() @auto_convert("src") def _wrap(self, src: Any, new_len: int) -> None: - if src.base.kind == Future or src.base.transformed: - change_shape = src.base.kind == Future + if src.base.has_scalar_storage or src.base.transformed: + change_shape = src.base.has_scalar_storage src = src._convert_future_to_regionfield(change_shape) # first, we create indirect array with PointN type that # (len,) shape and is used to copy data from original array # to the target 1D wrapped array N = src.ndim - pointN_dtype = self.runtime.get_point_type(N) + pointN_dtype = ty.point_type(N) indirect = cast( DeferredArray, - self.runtime.create_empty_thunk( + runtime.create_empty_thunk( shape=(new_len,), dtype=pointN_dtype, inputs=[src], ), ) - task = self.context.create_auto_task(CuNumericOpCode.WRAP) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.WRAP + ) task.add_output(indirect.base) task.add_scalar_arg(src.shape, (ty.int64,)) task.add_scalar_arg(False, ty.bool_) # has_input task.add_scalar_arg(False, ty.bool_) # check bounds task.execute() - copy = self.context.create_copy() - copy.set_target_indirect_out_of_range(False) - copy.add_input(src.base) - copy.add_source_indirect(indirect.base) - copy.add_output(self.base) - copy.execute() + legate_runtime.issue_gather(self.base, src.base, indirect.base) # Perform a histogram operation on the array @auto_convert("src", "bins", "weights") @@ -3669,14 +3721,25 @@ def histogram(self, src: Any, bins: Any, weights: Any) -> None: dst_array.fill(np.array(0, dst_array.dtype)) - task = self.context.create_auto_task(CuNumericOpCode.HISTOGRAM) - task.add_reduction(dst_array.base, ReductionOp.ADD) - task.add_input(src_array.base) - task.add_input(bins_array.base) - task.add_input(weight_array.base) + task = legate_runtime.create_auto_task( + self.library, CuPyNumericOpCode.HISTOGRAM + ) + p_dst = task.add_reduction(dst_array.base, ReductionOpKind.ADD) + p_src = task.add_input(src_array.base) + p_bins = task.add_input(bins_array.base) + p_weight = task.add_input(weight_array.base) - task.add_broadcast(bins_array.base) - task.add_broadcast(dst_array.base) - task.add_alignment(src_array.base, weight_array.base) + task.add_constraint(broadcast(p_bins)) + task.add_constraint(broadcast(p_dst)) + task.add_constraint(align(p_src, p_weight)) task.execute() + + def stencil_hint( + self, + low_offsets: tuple[int, ...], + high_offsets: tuple[int, ...], + ) -> None: + legate_runtime.prefetch_bloated_instances( + self.base, low_offsets, high_offsets, False + ) diff --git a/cunumeric/eager.py b/cupynumeric/_thunk/eager.py similarity index 84% rename from cunumeric/eager.py rename to cupynumeric/_thunk/eager.py index 4e6e504c2..74a02214f 100644 --- a/cunumeric/eager.py +++ b/cupynumeric/_thunk/eager.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,14 @@ # from __future__ import annotations -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence, cast import numpy as np +from legate.core import Scalar -from .config import ( +from .._utils import is_np2 +from .._utils.array import is_advanced_indexing +from ..config import ( FFT_C2R, FFT_D2Z, FFT_R2C, @@ -37,22 +30,22 @@ ConvertCode, FFTDirection, ScanCode, + TransferType, UnaryOpCode, UnaryRedCode, WindowOpCode, ) +from ..runtime import runtime from .deferred import DeferredArray from .thunk import NumPyThunk -from .utils import is_advanced_indexing, is_supported_type if TYPE_CHECKING: import numpy.typing as npt - from legate.core import FieldID, Future, Region - from .config import BitGeneratorType, FFTType - from .runtime import Runtime - from .types import ( + from ..config import BitGeneratorType, FFTType + from ..types import ( BitOrder, + ConvolveMethod, ConvolveMode, NdShape, OrderType, @@ -62,7 +55,7 @@ ) -_UNARY_OPS: Dict[UnaryOpCode, Any] = { +_UNARY_OPS: dict[UnaryOpCode, Any] = { UnaryOpCode.ABSOLUTE: np.absolute, UnaryOpCode.ARCCOS: np.arccos, UnaryOpCode.ARCCOSH: np.arccosh, @@ -109,7 +102,7 @@ # Unary reduction operations that don't return the argument of the # reduction operation -_UNARY_RED_OPS_WITHOUT_ARG: Dict[UnaryRedCode, Any] = { +_UNARY_RED_OPS_WITHOUT_ARG: dict[UnaryRedCode, Any] = { UnaryRedCode.ALL: np.all, UnaryRedCode.ANY: np.any, UnaryRedCode.MAX: np.max, @@ -124,14 +117,14 @@ # Unary reduction operations that return the argument of the # reduction operation -_UNARY_RED_OPS_WITH_ARG: Dict[UnaryRedCode, Any] = { +_UNARY_RED_OPS_WITH_ARG: dict[UnaryRedCode, Any] = { UnaryRedCode.ARGMIN: np.argmin, UnaryRedCode.ARGMAX: np.argmax, UnaryRedCode.NANARGMAX: np.nanargmax, UnaryRedCode.NANARGMIN: np.nanargmin, } -_BINARY_OPS: Dict[BinaryOpCode, Any] = { +_BINARY_OPS: dict[BinaryOpCode, Any] = { BinaryOpCode.ADD: np.add, BinaryOpCode.ARCTAN2: np.arctan2, BinaryOpCode.BITWISE_AND: np.bitwise_and, @@ -168,12 +161,10 @@ BinaryOpCode.SUBTRACT: np.subtract, } -_WINDOW_OPS: Dict[ +_WINDOW_OPS: dict[ WindowOpCode, - Union[ - Callable[[float], npt.NDArray[Any]], - Callable[[float, float], npt.NDArray[Any]], - ], + Callable[[float], npt.NDArray[Any]] + | Callable[[float, float], npt.NDArray[Any]], ] = { WindowOpCode.BARLETT: np.bartlett, WindowOpCode.BLACKMAN: np.blackman, @@ -219,30 +210,21 @@ class EagerArray(NumPyThunk): def __init__( self, - runtime: Runtime, - array: npt.NDArray[Any], - parent: Optional[EagerArray] = None, - key: Optional[tuple[Any, ...]] = None, + val: npt.ArrayLike, + parent: EagerArray | None = None, + key: tuple[Any, ...] | None = None, ) -> None: - super().__init__(runtime, array.dtype) + array = np.asarray(val) + super().__init__(array.dtype) self.array: npt.NDArray[Any] = array - self.parent: Optional[EagerArray] = parent + self.parent: EagerArray | None = parent self.children: list[EagerArray] = [] - self.key: Optional[tuple[Any, ...]] = key + self.key: tuple[Any, ...] | None = key #: if this ever becomes set (to a DeferredArray), we forward all #: operations to it - self.deferred: Optional[Union[DeferredArray, NumPyThunk]] = None + self.deferred: DeferredArray | None = None self.escaped = False - @property - def storage(self) -> Union[Future, tuple[Region, Union[int, FieldID]]]: - if self.deferred is None: - self.to_deferred_array() - - assert self.deferred is not None - - return self.deferred.storage - @property def shape(self) -> NdShape: return self.array.shape @@ -265,96 +247,120 @@ def check_eager_args(self, *args: Any) -> None: if self.deferred is not None: return for arg in args: - if self.runtime.is_eager_array(arg): + if runtime.is_eager_array(arg): if arg.deferred is not None: - self.to_deferred_array() + self.to_deferred_array(read_only=False) break - elif self.runtime.is_deferred_array(arg): - self.to_deferred_array() + elif runtime.is_deferred_array(arg): + self.to_deferred_array(read_only=False) break elif arg is None or not isinstance(arg, NumPyThunk): pass else: raise RuntimeError("bad argument type") - def _convert_children(self) -> None: + def _convert_subtree(self) -> None: + assert self.deferred is None + if self.parent is None: + transfer = ( + TransferType.SHARE + if self.escaped + # We can donate the base array, since it hasn't escaped to the + # user, and we won't be using it anymore. + else TransferType.DONATE + ) + deferred = runtime.find_or_create_array_thunk( + self.array, transfer=transfer, defer=True + ) + else: + parent = self.parent.deferred + assert self.key is not None + func = getattr(parent, self.key[0]) + args = self.key[1:] + deferred = func(*args) + self.deferred = cast(DeferredArray, deferred) + for child in self.children: + child._convert_subtree() + + def _convert_tree(self) -> None: """ - Traverse down our children and convert them to deferred arrays. + Convert the entire array tree to deferred arrays. + + We have to convert the whole tree when we convert even one node, to + make sure any future use of any array in the tree will go through the + deferred path, rather than use the original eager NumPy array, that we + donated. """ - assert self.runtime.is_deferred_array(self.deferred) - for child in self.children: - if child.deferred is None: - assert child.key is not None - func = getattr(self.deferred, child.key[0]) - args = child.key[1:] - child.deferred = func(*args) - # After we've made all the deferred views for each child then - # we can traverse down. Do it this way so we can get partition - # coalescing where possible - for child in self.children: - child._convert_children() - - def to_deferred_array(self) -> DeferredArray: - """This is a really important method. It will convert a tree of - eager NumPy arrays into an equivalent tree of deferred arrays that - are mirrored by an equivalent logical region tree. To be consistent - we always do this from the root, so once any array in the tree needs - to be converted then we do it for all of them. - :meta private: + if self.parent is None: + self._convert_subtree() + else: + self.parent._convert_tree() + + def to_deferred_array(self, read_only: bool) -> DeferredArray: """ - # Check to see if we already have our deferred array - # or whether we need to go up the tree to have it made - if self.deferred is None: - if self.parent is None: - assert is_supported_type(self.array.dtype) - # We are at the root of the tree so we need to - # actually make a DeferredArray to use - if self.array.size == 1: - self.deferred = self.runtime.create_wrapped_scalar( - self.array.data, - dtype=self.array.dtype, - shape=self.shape, - ) - else: - self.deferred = self.runtime.find_or_create_array_thunk( - self.array, - share=self.escaped, - defer=True, - ) - self._convert_children() - else: - # Traverse up the tree to make the deferred array - self.parent.to_deferred_array() - assert self.deferred is not None - return cast(DeferredArray, self.deferred) + Convert this EagerArray into a DeferredArray. + + If `read_only` is `False`, the EagerArray's buffer is donated to + initialize the DeferredArray, and the returned DeferredArray is used + in place of the EagerArray going forward. + """ + if self.deferred is not None: + return self.deferred + if read_only: + deferred = cast( + DeferredArray, + runtime.find_or_create_array_thunk( + self.array, + transfer=TransferType.MAKE_COPY, + read_only=True, + defer=True, + ), + ) + else: + self._convert_tree() + deferred = cast(DeferredArray, self.deferred) + return deferred def imag(self) -> NumPyThunk: if self.deferred is not None: return self.deferred.imag() - return EagerArray(self.runtime, self.array.imag) + return EagerArray(self.array.imag) def real(self) -> NumPyThunk: if self.deferred is not None: return self.deferred.real() - return EagerArray(self.runtime, self.array.real) + return EagerArray(self.array.real) def conj(self) -> NumPyThunk: if self.deferred is not None: return self.deferred.conj() - return EagerArray(self.runtime, self.array.conj()) + return EagerArray(self.array.conj()) - def convolve(self, v: Any, out: Any, mode: ConvolveMode) -> None: - self.check_eager_args(v, out) + def convolve( + self, + input: Any, + filter: Any, + mode: ConvolveMode, + method: ConvolveMethod, + ) -> None: + self.check_eager_args(input, filter) if self.deferred is not None: - self.deferred.convolve(v, out, mode) + self.deferred.convolve(input, filter, mode, method) else: if self.ndim == 1: - out.array = np.convolve(self.array, v.array, mode) + if method != "auto": + runtime.warn( + f"the method {method} is ignored " + "for the 1D convolution" + ) + self.array[:] = np.convolve(input.array, filter.array, mode) else: from scipy.signal import convolve # type: ignore [import] - out.array = convolve(self.array, v.array, mode) + self.array[...] = convolve( + input.array, filter.array, mode, method + ) def fft( self, @@ -384,7 +390,13 @@ def fft( elif res.dtype == np.float64: self.array[:] = res.astype(np.float32) else: - raise RuntimeError("Unsupported data type in eager FFT") + if not is_np2: + raise RuntimeError( + f"Unsupported data type {res.dtype!r} in eager FFT" + ) + else: + self.array[:] = res + else: self.array[:] = res @@ -406,11 +418,6 @@ def scalar(self) -> bool: return self.deferred.scalar return self.array.size == 1 - def get_scalar_array(self) -> npt.NDArray[Any]: - if self.deferred is not None: - return self.deferred.get_scalar_array() - return self.array.reshape(()) - def _create_indexing_key(self, key: Any) -> Any: if key is None or key is Ellipsis: return key @@ -424,7 +431,7 @@ def _create_indexing_key(self, key: Any) -> Any: result += (self._create_indexing_key(k),) return result assert isinstance(key, NumPyThunk) - return self.runtime.to_eager_array(key).array + return runtime.to_eager_array(key).array def get_item(self, key: Any) -> NumPyThunk: if self.deferred is not None: @@ -432,12 +439,10 @@ def get_item(self, key: Any) -> NumPyThunk: if is_advanced_indexing(key): index_key = self._create_indexing_key(key) out = self.array[index_key] - result = EagerArray(self.runtime, out) + result = EagerArray(out) else: child = self.array[key] - result = EagerArray( - self.runtime, child, parent=self, key=("get_item", key) - ) + result = EagerArray(child, parent=self, key=("get_item", key)) self.children.append(result) return result @@ -464,10 +469,9 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: child = self.array.reshape(newshape, order=order) # See if we are aliased or not if child.base is None: - result = EagerArray(self.runtime, child) + result = EagerArray(child) else: result = EagerArray( - self.runtime, child, parent=self, key=("reshape", newshape, order), @@ -475,7 +479,7 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: self.children.append(result) return result - def squeeze(self, axis: Optional[int]) -> NumPyThunk: + def squeeze(self, axis: int | tuple[int, ...] | None) -> NumPyThunk: if self.deferred is not None: return self.deferred.squeeze(axis) # See https://github.com/numpy/numpy/issues/22019 @@ -485,9 +489,7 @@ def squeeze(self, axis: Optional[int]) -> NumPyThunk: return self # Should be aliased with parent region assert child.base is not None - result = EagerArray( - self.runtime, child, parent=self, key=("squeeze", axis) - ) + result = EagerArray(child, parent=self, key=("squeeze", axis)) self.children.append(result) return result @@ -497,9 +499,7 @@ def swapaxes(self, axis1: int, axis2: int) -> NumPyThunk: child = self.array.swapaxes(axis1, axis2) # Should be aliased with parent region assert child.base is not None - result = EagerArray( - self.runtime, child, parent=self, key=("swapaxes", axis1, axis2) - ) + result = EagerArray(child, parent=self, key=("swapaxes", axis1, axis2)) self.children.append(result) return result @@ -535,18 +535,14 @@ def fill(self, value: Any) -> None: else: self.array.fill(value) - def transpose( - self, axes: Union[None, tuple[int, ...], list[int]] - ) -> NumPyThunk: + def transpose(self, axes: tuple[int, ...] | list[int]) -> NumPyThunk: if self.deferred is not None: return self.deferred.transpose(axes) # See https://github.com/numpy/numpy/issues/22019 child = self.array.transpose(cast(Any, axes)) # Should be aliased with parent region assert child.base is not None - result = EagerArray( - self.runtime, child, parent=self, key=("transpose", axes) - ) + result = EagerArray(child, parent=self, key=("transpose", axes)) self.children.append(result) return result @@ -566,9 +562,9 @@ def repeat( array = np.repeat(self.array, repeats.array, axis) else: array = np.repeat(self.array, repeats, axis) - return EagerArray(self.runtime, array) + return EagerArray(array) - def flip(self, rhs: Any, axes: Union[None, int, tuple[int, ...]]) -> None: + def flip(self, rhs: Any, axes: int | tuple[int, ...] | None) -> None: self.check_eager_args(rhs) if self.deferred is not None: self.deferred.flip(rhs, axes) @@ -585,9 +581,7 @@ def broadcast_to(self, shape: NdShape) -> NumPyThunk: child = np.broadcast_to(self.array, shape) # Should be aliased with parent region assert child.base is not None - result = EagerArray( - self.runtime, child, parent=self, key=("broadcast_to", shape) - ) + result = EagerArray(child, parent=self, key=("broadcast_to", shape)) self.children.append(result) return result @@ -705,14 +699,14 @@ def arange(self, start: float, stop: float, step: float) -> None: else: self.array = np.arange(start, stop, step, self.dtype) - def tile(self, rhs: Any, reps: Union[int, Sequence[int]]) -> None: + def tile(self, rhs: Any, reps: int | Sequence[int]) -> None: self.check_eager_args(rhs) if self.deferred is not None: self.deferred.tile(rhs, reps) else: self.array[:] = np.tile(rhs.array, reps) - def bincount(self, rhs: Any, weights: Optional[NumPyThunk] = None) -> None: + def bincount(self, rhs: Any, weights: NumPyThunk | None = None) -> None: self.check_eager_args(rhs, weights) if self.deferred is not None: self.deferred.bincount(rhs, weights=weights) @@ -730,7 +724,7 @@ def nonzero(self) -> tuple[NumPyThunk, ...]: arrays = self.array.nonzero() result: tuple[NumPyThunk, ...] = () for array in arrays: - result += (EagerArray(self.runtime, array),) + result += (EagerArray(array),) return result def searchsorted(self, rhs: Any, v: Any, side: SortSide = "left") -> None: @@ -744,9 +738,9 @@ def sort( self, rhs: Any, argsort: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SortType = "quicksort", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: self.check_eager_args(rhs) if self.deferred is not None: @@ -761,7 +755,7 @@ def bitgenerator_random_raw( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: if self.deferred is not None: @@ -784,7 +778,7 @@ def bitgenerator_integers( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: int, high: int, @@ -804,7 +798,7 @@ def bitgenerator_lognormal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -824,7 +818,7 @@ def bitgenerator_normal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -844,7 +838,7 @@ def bitgenerator_uniform( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: float, high: float, @@ -864,7 +858,7 @@ def bitgenerator_poisson( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, ) -> None: @@ -883,7 +877,7 @@ def bitgenerator_exponential( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, scale: float, ) -> None: @@ -902,7 +896,7 @@ def bitgenerator_gumbel( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -922,7 +916,7 @@ def bitgenerator_laplace( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -942,7 +936,7 @@ def bitgenerator_logistic( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -962,7 +956,7 @@ def bitgenerator_pareto( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -981,7 +975,7 @@ def bitgenerator_power( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -1000,7 +994,7 @@ def bitgenerator_rayleigh( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, sigma: float, ) -> None: @@ -1019,7 +1013,7 @@ def bitgenerator_cauchy( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, x0: float, gamma: float, @@ -1039,7 +1033,7 @@ def bitgenerator_triangular( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -1060,7 +1054,7 @@ def bitgenerator_weibull( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, k: float, @@ -1080,7 +1074,7 @@ def bitgenerator_bytes( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: if self.deferred is not None: @@ -1100,7 +1094,7 @@ def bitgenerator_beta( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -1120,7 +1114,7 @@ def bitgenerator_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -1145,7 +1139,7 @@ def bitgenerator_logseries( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -1164,7 +1158,7 @@ def bitgenerator_noncentral_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -1187,7 +1181,7 @@ def bitgenerator_chisquare( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, nonc: float, @@ -1215,7 +1209,7 @@ def bitgenerator_gamma( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, k: float, theta: float, @@ -1241,7 +1235,7 @@ def bitgenerator_standard_t( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, ) -> None: @@ -1260,7 +1254,7 @@ def bitgenerator_hypergeometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ngood: int, nbad: int, @@ -1283,7 +1277,7 @@ def bitgenerator_vonmises( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, kappa: float, @@ -1303,7 +1297,7 @@ def bitgenerator_zipf( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -1322,7 +1316,7 @@ def bitgenerator_geometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -1341,7 +1335,7 @@ def bitgenerator_wald( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, scale: float, @@ -1361,7 +1355,7 @@ def bitgenerator_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -1381,7 +1375,7 @@ def bitgenerator_negative_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -1402,11 +1396,11 @@ def bitgenerator_negative_binomial( def partition( self, rhs: Any, - kth: Union[int, Sequence[int]], + kth: int | Sequence[int], argpartition: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SelectKind = "introselect", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: self.check_eager_args(rhs) if self.deferred is not None: @@ -1437,8 +1431,8 @@ def random_normal(self) -> None: def random_integer( self, - low: Union[int, npt.NDArray[Any]], - high: Union[int, npt.NDArray[Any]], + low: int | npt.NDArray[Any], + high: int | npt.NDArray[Any], ) -> None: if self.deferred is not None: self.deferred.random_integer(low, high) @@ -1455,8 +1449,8 @@ def unary_op( op: UnaryOpCode, rhs: Any, where: Any, - args: Any, - multiout: Optional[Any] = None, + args: tuple[Scalar, ...] = (), + multiout: Any | None = None, ) -> None: if multiout is None: self.check_eager_args(rhs, where) @@ -1486,13 +1480,26 @@ def unary_op( else where.array, ) elif op == UnaryOpCode.CLIP: - np.clip(rhs.array, out=self.array, a_min=args[0], a_max=args[1]) + np.clip( + rhs.array, + out=self.array, + a_min=args[0].value(), + a_max=args[1].value(), + ) elif op == UnaryOpCode.COPY: self.array[:] = rhs.array[:] elif op == UnaryOpCode.IMAG: self.array = np.imag(rhs.array) elif op == UnaryOpCode.REAL: self.array = np.real(rhs.array) + elif op == UnaryOpCode.ROUND: + np.round( + rhs.array, + out=self.array, + decimals=args[0].value(), + ) + elif op == UnaryOpCode.ANGLE: + self.array = np.angle(rhs.array, args[0].value()) else: raise RuntimeError("unsupported unary op " + str(op)) @@ -1501,10 +1508,10 @@ def unary_reduction( op: UnaryRedCode, rhs: Any, where: Any, - orig_axis: Union[int, None], + orig_axis: int | None, axes: tuple[int, ...], keepdims: bool, - args: Any, + args: tuple[Scalar, ...], initial: Any, ) -> None: self.check_eager_args(rhs, where) @@ -1560,7 +1567,7 @@ def unary_reduction( ) elif op == UnaryRedCode.VARIANCE: (mu,) = args - centered = np.subtract(rhs.array, mu) + centered = np.subtract(rhs.array, np.asarray(mu)) squares = np.square(centered) np.sum( squares, @@ -1572,7 +1579,7 @@ def unary_reduction( out=self.array, ) elif op == UnaryRedCode.CONTAINS: - self.array.fill(args[0] in rhs.array) + self.array.fill(args[0].value() in rhs.array) elif op == UnaryRedCode.COUNT_NONZERO: self.array[()] = np.count_nonzero(rhs.array, axis=orig_axis) else: @@ -1585,7 +1592,7 @@ def isclose( if self.deferred is not None: self.deferred.isclose(rhs1, rhs2, rtol, atol, equal_nan) else: - self.array[:] = np.isclose( + self.array[...] = np.isclose( rhs1.array, rhs2.array, rtol=rtol, @@ -1594,7 +1601,12 @@ def isclose( ) def binary_op( - self, op: BinaryOpCode, rhs1: Any, rhs2: Any, where: Any, args: Any + self, + op: BinaryOpCode, + rhs1: Any, + rhs2: Any, + where: Any, + args: tuple[Scalar, ...], ) -> None: self.check_eager_args(rhs1, rhs2, where) if self.deferred is not None: @@ -1617,8 +1629,8 @@ def binary_reduction( op: BinaryOpCode, rhs1: Any, rhs2: Any, - broadcast: Union[NdShape, None], - args: Any, + broadcast: NdShape | None, + args: tuple[Scalar, ...], ) -> None: self.check_eager_args(rhs1, rhs2) if self.deferred is not None: @@ -1627,7 +1639,10 @@ def binary_reduction( if op == BinaryOpCode.ISCLOSE: self.array = np.array( np.allclose( - rhs1.array, rhs2.array, rtol=args[0], atol=args[1] + rhs1.array, + rhs2.array, + rtol=args[0].value(), + atol=args[1].value(), ) ) elif op == BinaryOpCode.EQUAL: @@ -1648,7 +1663,7 @@ def argwhere(self) -> NumPyThunk: if self.deferred is not None: return self.deferred.argwhere() else: - return EagerArray(self.runtime, np.argwhere(self.array)) + return EagerArray(np.argwhere(self.array)) def trilu(self, rhs: Any, k: int, lower: bool) -> None: self.check_eager_args(rhs) @@ -1660,21 +1675,34 @@ def trilu(self, rhs: Any, k: int, lower: bool) -> None: else: self.array[:] = np.triu(rhs.array, k) - def cholesky(self, src: Any, no_tril: bool) -> None: + def cholesky(self, src: Any) -> None: self.check_eager_args(src) if self.deferred is not None: - self.deferred.cholesky(src, no_tril) + self.deferred.cholesky(src) else: try: result = np.linalg.cholesky(src.array) except np.linalg.LinAlgError as e: - from .linalg import LinAlgError + from ..linalg import LinAlgError raise LinAlgError(e) from e - if no_tril: - result = np.triu(result.T.conj(), k=1) + result + self.array[:] = result + def qr(self, q: Any, r: Any) -> None: + self.check_eager_args(q, r) + if self.deferred is not None: + self.deferred.qr(q, r) + else: + try: + result_q, result_r = np.linalg.qr(self.array) + except np.linalg.LinAlgError as e: + from ..linalg import LinAlgError + + raise LinAlgError(e) from e + q.array[:] = result_q + r.array[:] = result_r + def solve(self, a: Any, b: Any) -> None: self.check_eager_args(a, b) if self.deferred is not None: @@ -1683,17 +1711,37 @@ def solve(self, a: Any, b: Any) -> None: try: result = np.linalg.solve(a.array, b.array) except np.linalg.LinAlgError as e: - from .linalg import LinAlgError + from ..linalg import LinAlgError raise LinAlgError(e) from e self.array[:] = result + def svd(self, u: Any, s: Any, vh: Any) -> None: + self.check_eager_args(u, s, vh) + if self.deferred is not None: + self.deferred.svd(u, s, vh) + else: + try: + full_matrices = ( + u.shape[0] == u.shape[1] and vh.shape[0] == vh.shape[1] + ) + result_u, result_s, result_vh = np.linalg.svd( + self.array, full_matrices + ) + except np.linalg.LinAlgError as e: + from ..linalg import LinAlgError + + raise LinAlgError(e) from e + u.array[:] = result_u + s.array[:] = result_s + vh.array[:] = result_vh + def scan( self, op: int, rhs: Any, axis: int, - dtype: Optional[npt.DTypeLike], + dtype: npt.DTypeLike | None, nan_to_identity: bool, ) -> None: self.check_eager_args(rhs) @@ -1717,7 +1765,7 @@ def unique(self) -> NumPyThunk: if self.deferred is not None: return self.deferred.unique() else: - return EagerArray(self.runtime, np.unique(self.array)) + return EagerArray(np.unique(self.array)) def create_window(self, op_code: WindowOpCode, M: int, *args: Any) -> None: if self.deferred is not None: @@ -1726,9 +1774,7 @@ def create_window(self, op_code: WindowOpCode, M: int, *args: Any) -> None: fn = _WINDOW_OPS[op_code] self.array[:] = fn(M, *args) - def packbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder - ) -> None: + def packbits(self, src: Any, axis: int | None, bitorder: BitOrder) -> None: self.check_eager_args(src) if self.deferred is not None: self.deferred.packbits(src, axis, bitorder) @@ -1738,7 +1784,7 @@ def packbits( ) def unpackbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder + self, src: Any, axis: int | None, bitorder: BitOrder ) -> None: self.check_eager_args(src) if self.deferred is not None: @@ -1774,3 +1820,11 @@ def histogram(self, rhs: Any, bins: Any, weights: Any) -> None: cast(EagerArray, bins).array, weights=cast(EagerArray, weights).array, ) + + def stencil_hint( + self, + low_offsets: tuple[int, ...], + high_offsets: tuple[int, ...], + ) -> None: + if self.deferred is not None: + self.deferred.stencil_hint(low_offsets, high_offsets) diff --git a/cunumeric/thunk.py b/cupynumeric/_thunk/thunk.py similarity index 81% rename from cunumeric/thunk.py rename to cupynumeric/_thunk/thunk.py index 68aafb6c9..37f157881 100644 --- a/cunumeric/thunk.py +++ b/cupynumeric/_thunk/thunk.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,16 +15,17 @@ from __future__ import annotations from abc import ABC, abstractmethod, abstractproperty -from typing import TYPE_CHECKING, Any, Iterable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Iterable, Sequence -from .config import ConvertCode +from ..config import ConvertCode +from ..runtime import runtime if TYPE_CHECKING: import numpy as np import numpy.typing as npt - from legate.core import FieldID, Future, Region + from legate.core import Scalar - from .config import ( + from ..config import ( BinaryOpCode, BitGeneratorType, FFTDirection, @@ -33,9 +34,9 @@ UnaryRedCode, WindowOpCode, ) - from .runtime import Runtime - from .types import ( + from ..types import ( BitOrder, + ConvolveMethod, ConvolveMode, NdShape, OrderType, @@ -48,14 +49,13 @@ class NumPyThunk(ABC): """This is the base class for NumPy computations. It has methods for all the kinds of computations and operations that can be done - on cuNumeric ndarrays. + on cuPyNumeric ndarrays. :meta private: """ - def __init__(self, runtime: Runtime, dtype: np.dtype[Any]) -> None: - self.runtime = runtime - self.context = runtime.legate_context + def __init__(self, dtype: np.dtype[Any]) -> None: + self.library = runtime.library self.dtype = dtype @property @@ -73,11 +73,6 @@ def size(self) -> int: # Abstract methods - @abstractproperty - def storage(self) -> Union[Future, tuple[Region, Union[int, FieldID]]]: - """Return the Legion storage primitive for this NumPy thunk""" - ... - @abstractproperty def shape(self) -> NdShape: ... @@ -99,13 +94,19 @@ def conj(self) -> NumPyThunk: ... @abstractmethod - def convolve(self, v: Any, out: Any, mode: ConvolveMode) -> None: + def convolve( + self, + input: Any, + filter: Any, + mode: ConvolveMode, + method: ConvolveMethod, + ) -> None: ... @abstractmethod def fft( self, - out: Any, + rhs: Any, axes: Sequence[int], kind: FFTType, direction: FFTDirection, @@ -127,10 +128,6 @@ def repeat( def scalar(self) -> bool: ... - @abstractmethod - def get_scalar_array(self) -> npt.NDArray[Any]: - ... - @abstractmethod def get_item(self, key: Any) -> NumPyThunk: ... @@ -144,7 +141,7 @@ def reshape(self, newshape: NdShape, order: OrderType) -> NumPyThunk: ... @abstractmethod - def squeeze(self, axis: Optional[int]) -> NumPyThunk: + def squeeze(self, axis: int | tuple[int, ...] | None) -> NumPyThunk: ... @abstractmethod @@ -166,13 +163,11 @@ def fill(self, value: Any) -> None: ... @abstractmethod - def transpose( - self, axes: Union[None, tuple[int, ...], list[int]] - ) -> NumPyThunk: + def transpose(self, axes: tuple[int, ...] | list[int]) -> NumPyThunk: ... @abstractmethod - def flip(self, rhs: Any, axes: Union[None, int, tuple[int, ...]]) -> None: + def flip(self, rhs: Any, axes: int | tuple[int, ...] | None) -> None: ... @abstractmethod @@ -223,7 +218,7 @@ def arange(self, start: float, stop: float, step: float) -> None: ... @abstractmethod - def tile(self, rhs: Any, reps: Union[Any, Sequence[int]]) -> None: + def tile(self, rhs: Any, reps: Any | Sequence[int]) -> None: ... @abstractmethod @@ -231,7 +226,7 @@ def trilu(self, rhs: Any, k: int, lower: bool) -> None: ... @abstractmethod - def bincount(self, rhs: Any, weights: Optional[NumPyThunk] = None) -> None: + def bincount(self, rhs: Any, weights: NumPyThunk | None = None) -> None: ... @abstractmethod @@ -243,7 +238,7 @@ def bitgenerator_random_raw( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: ... @@ -253,7 +248,7 @@ def bitgenerator_integers( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: int, high: int, @@ -265,7 +260,7 @@ def bitgenerator_uniform( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, low: float, high: float, @@ -277,7 +272,7 @@ def bitgenerator_lognormal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -289,7 +284,7 @@ def bitgenerator_normal( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, sigma: float, @@ -301,7 +296,7 @@ def bitgenerator_poisson( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, ) -> None: @@ -312,7 +307,7 @@ def bitgenerator_exponential( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, scale: float, ) -> None: @@ -323,7 +318,7 @@ def bitgenerator_gumbel( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -335,7 +330,7 @@ def bitgenerator_laplace( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -347,7 +342,7 @@ def bitgenerator_logistic( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, beta: float, @@ -359,7 +354,7 @@ def bitgenerator_pareto( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -370,7 +365,7 @@ def bitgenerator_power( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -381,7 +376,7 @@ def bitgenerator_rayleigh( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, sigma: float, ) -> None: @@ -392,7 +387,7 @@ def bitgenerator_cauchy( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, x0: float, gamma: float, @@ -404,7 +399,7 @@ def bitgenerator_triangular( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -417,7 +412,7 @@ def bitgenerator_weibull( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, lam: float, k: float, @@ -429,7 +424,7 @@ def bitgenerator_bytes( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ) -> None: ... @@ -439,7 +434,7 @@ def bitgenerator_beta( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, a: float, b: float, @@ -451,7 +446,7 @@ def bitgenerator_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -463,7 +458,7 @@ def bitgenerator_logseries( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -474,7 +469,7 @@ def bitgenerator_noncentral_f( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, dfnum: float, dfden: float, @@ -487,7 +482,7 @@ def bitgenerator_chisquare( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, nonc: float, @@ -499,7 +494,7 @@ def bitgenerator_gamma( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, k: float, theta: float, @@ -511,7 +506,7 @@ def bitgenerator_standard_t( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, df: float, ) -> None: @@ -522,7 +517,7 @@ def bitgenerator_hypergeometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ngood: int, nbad: int, @@ -535,7 +530,7 @@ def bitgenerator_vonmises( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mu: float, kappa: float, @@ -547,7 +542,7 @@ def bitgenerator_zipf( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, alpha: float, ) -> None: @@ -558,7 +553,7 @@ def bitgenerator_geometric( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, p: float, ) -> None: @@ -569,7 +564,7 @@ def bitgenerator_wald( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, mean: float, scale: float, @@ -581,7 +576,7 @@ def bitgenerator_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -593,7 +588,7 @@ def bitgenerator_negative_binomial( self, handle: int, generatorType: BitGeneratorType, - seed: Union[int, None], + seed: int | None, flags: int, ntrials: int, p: float, @@ -608,11 +603,11 @@ def random_uniform(self) -> None: def partition( self, rhs: Any, - kth: Union[int, Sequence[int]], + kth: int | Sequence[int], argpartition: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SelectKind = "introselect", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: ... @@ -623,8 +618,8 @@ def random_normal(self) -> None: @abstractmethod def random_integer( self, - low: Union[int, npt.NDArray[Any]], - high: Union[int, npt.NDArray[Any]], + low: int | npt.NDArray[Any], + high: int | npt.NDArray[Any], ) -> None: ... @@ -637,9 +632,9 @@ def sort( self, rhs: Any, argsort: bool = False, - axis: Union[int, None] = -1, + axis: int | None = -1, kind: SortType = "quicksort", - order: Union[None, str, list[str]] = None, + order: str | list[str] | None = None, ) -> None: ... @@ -649,8 +644,8 @@ def unary_op( op: UnaryOpCode, rhs: Any, where: Any, - args: Any, - multiout: Optional[Any] = None, + args: tuple[Scalar, ...] = (), + multiout: Any | None = None, ) -> None: ... @@ -660,10 +655,10 @@ def unary_reduction( op: UnaryRedCode, rhs: Any, where: Any, - orig_axis: Union[int, None], + orig_axis: int | None, axes: tuple[int, ...], keepdims: bool, - args: Any, + args: tuple[Scalar, ...], initial: Any, ) -> None: ... @@ -676,7 +671,12 @@ def isclose( @abstractmethod def binary_op( - self, op: BinaryOpCode, rhs1: Any, rhs2: Any, where: Any, args: Any + self, + op: BinaryOpCode, + rhs1: Any, + rhs2: Any, + where: Any, + args: tuple[Scalar, ...], ) -> None: ... @@ -686,8 +686,8 @@ def binary_reduction( op: BinaryOpCode, rhs1: Any, rhs2: Any, - broadcast: Union[NdShape, None], - args: Any, + broadcast: NdShape | None, + args: tuple[Scalar, ...], ) -> None: ... @@ -704,20 +704,28 @@ def where(self, rhs1: Any, rhs2: Any, rhs3: Any) -> None: ... @abstractmethod - def cholesky(self, src: Any, no_tril: bool) -> None: + def cholesky(self, src: Any) -> None: + ... + + @abstractmethod + def qr(self, q: Any, r: Any) -> None: ... @abstractmethod def solve(self, a: Any, b: Any) -> None: ... + @abstractmethod + def svd(self, u: Any, s: Any, vh: Any) -> None: + ... + @abstractmethod def scan( self, op: int, rhs: Any, axis: int, - dtype: Optional[npt.DTypeLike], + dtype: npt.DTypeLike | None, nan_to_identity: bool, ) -> None: ... @@ -731,14 +739,12 @@ def create_window(self, op_code: WindowOpCode, M: Any, *args: Any) -> None: ... @abstractmethod - def packbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder - ) -> None: + def packbits(self, src: Any, axis: int | None, bitorder: BitOrder) -> None: ... @abstractmethod def unpackbits( - self, src: Any, axis: Union[int, None], bitorder: BitOrder + self, src: Any, axis: int | None, bitorder: BitOrder ) -> None: ... @@ -749,3 +755,11 @@ def _wrap(self, src: Any, new_len: int) -> None: @abstractmethod def histogram(self, src: Any, bins: Any, weights: Any) -> None: ... + + @abstractmethod + def stencil_hint( + self, + low_offsets: tuple[int, ...], + high_offsets: tuple[int, ...], + ) -> None: + ... diff --git a/cunumeric/_ufunc/__init__.py b/cupynumeric/_ufunc/__init__.py similarity index 95% rename from cunumeric/_ufunc/__init__.py rename to cupynumeric/_ufunc/__init__.py index 3887e13f4..edd62f1b9 100644 --- a/cunumeric/_ufunc/__init__.py +++ b/cupynumeric/_ufunc/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cunumeric/_ufunc/bit_twiddling.py b/cupynumeric/_ufunc/bit_twiddling.py similarity index 94% rename from cunumeric/_ufunc/bit_twiddling.py rename to cupynumeric/_ufunc/bit_twiddling.py index dc70fc2ad..2943257df 100644 --- a/cunumeric/_ufunc/bit_twiddling.py +++ b/cupynumeric/_ufunc/bit_twiddling.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,7 @@ # from __future__ import annotations -from cunumeric.config import BinaryOpCode, UnaryOpCode - +from ..config import BinaryOpCode, UnaryOpCode from .ufunc import create_binary_ufunc, create_unary_ufunc, integer_dtypes bitwise_and = create_binary_ufunc( diff --git a/cupynumeric/_ufunc/comparison.py b/cupynumeric/_ufunc/comparison.py new file mode 100644 index 000000000..148854fad --- /dev/null +++ b/cupynumeric/_ufunc/comparison.py @@ -0,0 +1,212 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np + +from .._array.util import convert_to_cupynumeric_ndarray +from ..config import BinaryOpCode, UnaryOpCode, UnaryRedCode +from .ufunc import ( + all_dtypes, + create_binary_ufunc, + create_unary_ufunc, + float_dtypes, + integer_dtypes, + predicate_types_of, + relation_types_of, +) + +if TYPE_CHECKING: + from .._array.array import ndarray + + +def _post_resolution_check( + arr_x: ndarray, + arr_y: ndarray, + obj_x: Any, + obj_y: Any, + op_code: BinaryOpCode, +) -> tuple[ndarray, ndarray, BinaryOpCode]: + """When we were passed Python integers, they may not fit into the operation + dtype. In that case, however, we can just define the result. + Note that as of now, we don't try to do this if both operands were Python + ints. + """ + truthiness = None # cannot guess the truthiness based on the scalar value + + if type(obj_x) is int and type(obj_y) is int: + # No special behavior currently, check if values fit operation + if arr_x.dtype.kind in "iu": + # Check if original Python integer fits first operand. + arr_x.dtype.type(obj_x) + if arr_y.dtype.kind in "iu": + # Check if original Python integer fits second operand. + arr_y.dtype.type(obj_y) + + elif type(obj_x) is int and arr_x.dtype.kind in "iu": + iinfo = np.iinfo(arr_x.dtype) + if obj_x < iinfo.min: + truthiness = op_code in { + BinaryOpCode.NOT_EQUAL, + BinaryOpCode.LESS, + BinaryOpCode.LESS_EQUAL, + } + elif obj_x > iinfo.max: + truthiness = op_code in { + BinaryOpCode.NOT_EQUAL, + BinaryOpCode.GREATER, + BinaryOpCode.GREATER_EQUAL, + } + + if truthiness is not None: + # Replace with an always-true/always-false operation + arr_x = convert_to_cupynumeric_ndarray( + np.array(iinfo.min, dtype=arr_x.dtype) + ) + op_code = ( + BinaryOpCode.GREATER_EQUAL if truthiness else BinaryOpCode.LESS + ) + + elif type(obj_y) is int and arr_y.dtype.kind in "iu": + iinfo = np.iinfo(arr_y.dtype) + if iinfo.min > obj_y: + truthiness = op_code in { + BinaryOpCode.NOT_EQUAL, + BinaryOpCode.GREATER, + BinaryOpCode.GREATER_EQUAL, + } + elif iinfo.max < obj_y: + truthiness = op_code in { + BinaryOpCode.NOT_EQUAL, + BinaryOpCode.LESS, + BinaryOpCode.LESS_EQUAL, + } + + if truthiness is not None: + # Replace with an always-true/always-false operation + arr_y = convert_to_cupynumeric_ndarray( + np.array(iinfo.min, dtype=arr_y.dtype) + ) + op_code = ( + BinaryOpCode.GREATER_EQUAL if truthiness else BinaryOpCode.LESS + ) + + return arr_x, arr_y, op_code + + +greater = create_binary_ufunc( + "Return the truth value of (x1 > x2) element-wise.", + "greater", + BinaryOpCode.GREATER, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +greater_equal = create_binary_ufunc( + "Return the truth value of (x1 >= x2) element-wise.", + "greater_equal", + BinaryOpCode.GREATER_EQUAL, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +less = create_binary_ufunc( + "Return the truth value of (x1 < x2) element-wise.", + "less", + BinaryOpCode.LESS, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +less_equal = create_binary_ufunc( + "Return the truth value of (x1 =< x2) element-wise.", + "less", + BinaryOpCode.LESS_EQUAL, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +not_equal = create_binary_ufunc( + "Return (x1 != x2) element-wise.", + "not_equal", + BinaryOpCode.NOT_EQUAL, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +equal = create_binary_ufunc( + "Return (x1 == x2) element-wise.", + "equal", + BinaryOpCode.EQUAL, + relation_types_of(all_dtypes), + post_resolution_check=_post_resolution_check, +) + +logical_and = create_binary_ufunc( + "Compute the truth value of x1 AND x2 element-wise.", + "logical_and", + BinaryOpCode.LOGICAL_AND, + relation_types_of(all_dtypes), + red_code=UnaryRedCode.ALL, +) + +logical_or = create_binary_ufunc( + "Compute the truth value of x1 OR x2 element-wise.", + "logical_or", + BinaryOpCode.LOGICAL_OR, + relation_types_of(all_dtypes), + red_code=UnaryRedCode.ANY, +) + +logical_xor = create_binary_ufunc( + "Compute the truth value of x1 XOR x2, element-wise.", + "logical_xor", + BinaryOpCode.LOGICAL_XOR, + relation_types_of(all_dtypes), +) + +logical_not = create_unary_ufunc( + "Compute bit-wise inversion, or bit-wise NOT, element-wise.", + "invert", + UnaryOpCode.LOGICAL_NOT, + ( + ["??"] + + predicate_types_of(integer_dtypes) + + predicate_types_of(float_dtypes) + ), + overrides={"?": UnaryOpCode.LOGICAL_NOT}, +) + +maximum = create_binary_ufunc( + "Element-wise maximum of array elements.", + "maximum", + BinaryOpCode.MAXIMUM, + all_dtypes, + red_code=UnaryRedCode.MAX, +) + +fmax = maximum + +minimum = create_binary_ufunc( + "Element-wise minimum of array elements.", + "minimum", + BinaryOpCode.MINIMUM, + all_dtypes, + red_code=UnaryRedCode.MIN, +) + +fmin = minimum diff --git a/cunumeric/_ufunc/floating.py b/cupynumeric/_ufunc/floating.py similarity index 97% rename from cunumeric/_ufunc/floating.py rename to cupynumeric/_ufunc/floating.py index 0dceb691f..6c3644f8f 100644 --- a/cunumeric/_ufunc/floating.py +++ b/cupynumeric/_ufunc/floating.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,7 @@ # from __future__ import annotations -from cunumeric.config import BinaryOpCode, UnaryOpCode - +from ..config import BinaryOpCode, UnaryOpCode from .ufunc import ( create_binary_ufunc, create_multiout_unary_ufunc, diff --git a/cunumeric/_ufunc/math.py b/cupynumeric/_ufunc/math.py similarity index 98% rename from cunumeric/_ufunc/math.py rename to cupynumeric/_ufunc/math.py index 3f79c84ed..dc161b949 100644 --- a/cunumeric/_ufunc/math.py +++ b/cupynumeric/_ufunc/math.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,7 @@ # from __future__ import annotations -from cunumeric.config import BinaryOpCode, UnaryOpCode, UnaryRedCode - +from ..config import BinaryOpCode, UnaryOpCode, UnaryRedCode from .ufunc import ( all_but_boolean, all_dtypes, diff --git a/cunumeric/_ufunc/trigonometric.py b/cupynumeric/_ufunc/trigonometric.py similarity index 96% rename from cunumeric/_ufunc/trigonometric.py rename to cupynumeric/_ufunc/trigonometric.py index 0687cc13e..87cc394e7 100644 --- a/cunumeric/_ufunc/trigonometric.py +++ b/cupynumeric/_ufunc/trigonometric.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,7 @@ # from __future__ import annotations -from cunumeric.config import BinaryOpCode, UnaryOpCode - +from ..config import BinaryOpCode, UnaryOpCode from .ufunc import ( create_binary_ufunc, create_unary_ufunc, diff --git a/cunumeric/_ufunc/ufunc.py b/cupynumeric/_ufunc/ufunc.py similarity index 77% rename from cunumeric/_ufunc/ufunc.py rename to cupynumeric/_ufunc/ufunc.py index 3bba6b8ad..5b2eb11e9 100644 --- a/cunumeric/_ufunc/ufunc.py +++ b/cupynumeric/_ufunc/ufunc.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Sequence, TypeAlias import numpy as np from legate.core.utils import OrderedSet -from ..array import ( +from .._array.thunk import perform_unary_reduction +from .._array.util import ( add_boilerplate, check_writeable, - convert_to_cunumeric_ndarray, - ndarray, + convert_to_cupynumeric_ndarray, ) from ..config import BinaryOpCode, UnaryOpCode, UnaryRedCode from ..types import NdShape @@ -31,8 +31,20 @@ if TYPE_CHECKING: import numpy.typing as npt + from .._array.array import ndarray from ..types import CastingKind + PostResolutionCheckFunc: TypeAlias = Callable[ + [ + ndarray, + ndarray, + Any, + Any, + BinaryOpCode, + ], + tuple[ndarray, ndarray, BinaryOpCode], + ] + _UNARY_DOCSTRING_TEMPLATE = """{} @@ -67,7 +79,7 @@ numpy.{} Availability --------- +------------ Multiple GPUs, Multiple CPUs """ @@ -105,7 +117,7 @@ numpy.{} Availability --------- +------------ Multiple GPUs, Multiple CPUs """ @@ -143,7 +155,7 @@ numpy.{} Availability --------- +------------ Multiple GPUs, Multiple CPUs """ @@ -183,8 +195,58 @@ def to_dtypes(chars: str) -> tuple[np.dtype[Any], ...]: return tuple(np.dtype(char) for char in chars) +def _get_kind_score(kind: type) -> int: + if issubclass(kind, np.bool_): + return 0 + if issubclass(kind, (np.integer, int)): + return 1 + if issubclass(kind, (np.inexact, float, complex)): + return 2 + # unknown type, assume higher score + return 3 + + +def _check_should_use_weak_scalar(key: tuple[str | type, ...]) -> bool: + """Helper function for promotion, where we need to check whether we + should use weak promotion for python floats/integers (NEP 50/NumPy 2). + """ + max_scalar_kind = -1 + max_array_kind = -1 + + for in_t in key: + if isinstance(in_t, str): + kind = _get_kind_score(np.dtype(in_t).type) + max_array_kind = max(max_array_kind, kind) + else: + kind = _get_kind_score(in_t) + max_scalar_kind = max(max_scalar_kind, kind) + + all_scalars_or_arrays = max_scalar_kind == -1 or max_array_kind == -1 + return not all_scalars_or_arrays and max_array_kind >= max_scalar_kind + + +def _default_post_resolution_check( + arr_x: ndarray, + arr_y: ndarray, + obj_x: Any, + obj_y: Any, + op_code: BinaryOpCode, +) -> tuple[ndarray, ndarray, BinaryOpCode]: + """Check whether Python integers fit into integer operand dtypes. + This check is overloaded by comparisons to always succeed. + """ + if type(obj_x) is int and arr_x.dtype.kind in "iu": + # Check if original Python integer fits first operand. + arr_x.dtype.type(obj_x) + if type(obj_y) is int and arr_y.dtype.kind in "iu": + # Check if original Python integer fits second operand. + arr_y.dtype.type(obj_y) + + return arr_x, arr_y, op_code + + class ufunc: - _types: Dict[Any, str] + _types: dict[Any, str] _nin: int _nout: int @@ -231,12 +293,14 @@ def _maybe_cast_input( def _maybe_create_result( self, - out: Union[ndarray, None], + out: ndarray | None, out_shape: NdShape, res_dtype: np.dtype[Any], casting: CastingKind, inputs: tuple[ndarray, ...], ) -> ndarray: + from .._array.array import ndarray + if out is None: return ndarray(shape=out_shape, dtype=res_dtype, inputs=inputs) elif out.dtype != res_dtype: @@ -251,34 +315,34 @@ def _maybe_create_result( return out @staticmethod - def _maybe_cast_output( - out: Union[ndarray, None], result: ndarray - ) -> ndarray: + def _maybe_cast_output(out: ndarray | None, result: ndarray) -> ndarray: if out is None or out is result: return result out._thunk.convert(result._thunk, warn=False) return out @staticmethod - def _maybe_convert_output_to_cunumeric_ndarray( - out: Union[ndarray, npt.NDArray[Any], None] - ) -> Union[ndarray, None]: + def _maybe_convert_output_to_cupynumeric_ndarray( + out: ndarray | npt.NDArray[Any] | None, + ) -> ndarray | None: + from .._array.array import ndarray + if out is None: return None if isinstance(out, ndarray): return out if isinstance(out, np.ndarray): - return convert_to_cunumeric_ndarray(out, share=True) + return convert_to_cupynumeric_ndarray(out, share=True) raise TypeError("return arrays must be of ArrayType") def _prepare_operands( self, *args: Any, - out: Union[ndarray, tuple[ndarray, ...], None], + out: ndarray | tuple[ndarray, ...] | None, where: bool = True, ) -> tuple[ Sequence[ndarray], - Sequence[Union[ndarray, None]], + Sequence[ndarray | None], tuple[int, ...], bool, ]: @@ -290,7 +354,7 @@ def _prepare_operands( ) inputs = tuple( - convert_to_cunumeric_ndarray(arr) for arr in args[: self.nin] + convert_to_cupynumeric_ndarray(arr) for arr in args[: self.nin] ) if len(args) > self.nin: @@ -310,7 +374,7 @@ def _prepare_operands( computed_out = out outputs = tuple( - self._maybe_convert_output_to_cunumeric_ndarray(arr) + self._maybe_convert_output_to_cupynumeric_ndarray(arr) for arr in computed_out ) @@ -402,11 +466,11 @@ def _resolve_dtype( def __call__( self, *args: Any, - out: Union[ndarray, None] = None, + out: ndarray | None = None, where: bool = True, casting: CastingKind = "same_kind", order: str = "K", - dtype: Union[np.dtype[Any], None] = None, + dtype: np.dtype[Any] | None = None, **kwargs: Any, ) -> ndarray: (x,), (out,), out_shape, where = self._prepare_operands( @@ -432,7 +496,7 @@ def __call__( ) op_code = self._overrides.get(x.dtype.char, self._op_code) - result._thunk.unary_op(op_code, x._thunk, where, ()) + result._thunk.unary_op(op_code, x._thunk, where) return self._maybe_cast_output(out, result) @@ -487,11 +551,11 @@ def _resolve_dtype( def __call__( self, *args: Any, - out: Union[ndarray, tuple[ndarray, ...], None] = None, + out: ndarray | tuple[ndarray, ...] | None = None, where: bool = True, casting: CastingKind = "same_kind", order: str = "K", - dtype: Union[np.dtype[Any], None] = None, + dtype: np.dtype[Any] | None = None, **kwargs: Any, ) -> tuple[ndarray, ...]: (x,), outs, out_shape, where = self._prepare_operands( @@ -529,14 +593,17 @@ def __call__( class binary_ufunc(ufunc): + _post_resolution_check: PostResolutionCheckFunc + def __init__( self, name: str, doc: str, op_code: BinaryOpCode, types: dict[tuple[str, str], str], - red_code: Union[UnaryRedCode, None] = None, + red_code: UnaryRedCode | None = None, use_common_type: bool = True, + post_resolution_check: PostResolutionCheckFunc | None = None, ) -> None: super().__init__(name, doc) @@ -548,15 +615,21 @@ def __init__( self._op_code = op_code self._resolution_cache: dict[ - tuple[str, ...], tuple[np.dtype[Any], ...] + tuple[str | type, ...], tuple[np.dtype[Any], ...] ] = {} self._red_code = red_code self._use_common_type = use_common_type + if post_resolution_check is None: + self._post_resolution_check = _default_post_resolution_check + else: + self._post_resolution_check = post_resolution_check @staticmethod def _find_common_type( arrs: Sequence[ndarray], orig_args: Sequence[Any] ) -> np.dtype[Any]: + from .._array.array import ndarray + all_ndarray = all(isinstance(arg, ndarray) for arg in orig_args) unique_dtypes = OrderedSet(arr.dtype for arr in arrs) # If all operands are ndarrays and they all have the same dtype, @@ -567,8 +640,10 @@ def _find_common_type( scalar_types = [] array_types = [] for arr, orig_arg in zip(arrs, orig_args): - if arr.ndim == 0: - # Make sure all scalar arguments are NumPy arrays + if type(orig_arg) in (int, float, complex): + scalar_types.append(orig_arg) + elif arr.ndim == 0: + # NumPy 1.x needs a 0-D NumPy array for value-based promotion scalar_types.append(np.asarray(orig_arg)) else: array_types.append(arr.dtype) @@ -578,19 +653,29 @@ def _find_common_type( def _resolve_dtype( self, arrs: Sequence[ndarray], - orig_args: Sequence[np.dtype[Any]], + orig_args: Sequence[Any], casting: CastingKind, precision_fixed: bool, ) -> tuple[Sequence[ndarray], np.dtype[Any]]: to_dtypes: tuple[np.dtype[Any], ...] - key: tuple[str, ...] + key: tuple[str | type, ...] if self._use_common_type: common_dtype = self._find_common_type(arrs, orig_args) to_dtypes = (common_dtype, common_dtype) key = (common_dtype.char, common_dtype.char) else: to_dtypes = tuple(arr.dtype for arr in arrs) - key = tuple(arr.dtype.char for arr in arrs) + key = tuple( + arr.dtype.char + if type(orig) not in (int, float, complex) + else type(orig) + for orig, arr in zip(orig_args, arrs) + ) + # When all inputs are scalars, cannot use weak logic below. + # (Using arr.dtype.char may be off for huge integers that map to + # an unsigned int. But NumPy should mostly do the same currently.) + if not _check_should_use_weak_scalar(key): + key = tuple(arr.dtype.char for arr in arrs) if key in self._types: arrs = [ @@ -611,10 +696,18 @@ def _resolve_dtype( chosen = None if not precision_fixed: for in_dtypes in self._types.keys(): - if all( - np.can_cast(arr.dtype, to_dtype) - for arr, to_dtype in zip(arrs, in_dtypes) - ): + for in_t, to_dtype in zip(key, in_dtypes): + # Break if `to_dtype` doesn't work. + if isinstance(in_t, str): + if not np.can_cast(in_t, to_dtype): + break + else: + # In NumPy 2, the value doesn't matter. In NumPy 1.x + # it could matter (but caching wouldn't work anyway). + if np.result_type(in_t(0), to_dtype) != to_dtype: + break + else: + # dtypes OK (no break), choose them and break outer chosen = in_dtypes break @@ -622,10 +715,23 @@ def _resolve_dtype( # try to find a match based on the leading operand if chosen is None and not self._use_common_type: for in_dtypes in self._types.keys(): - if np.can_cast(arrs[0].dtype, in_dtypes[0]) and all( - np.can_cast(arr, to_dtype, casting=casting) - for arr, to_dtype in zip(arrs[1:], in_dtypes[1:]) - ): + if not np.can_cast(arrs[0].dtype, in_dtypes[0]): + # Check next in_dtypes + continue + + for in_t, to_dtype in zip(key[1:], in_dtypes[1:]): + # Break if `to_dtype` doesn't work. + if isinstance(in_t, str): + if not np.can_cast( + in_t, to_dtype, casting=casting + ): + break + elif casting != "unsafe": + # Same-kind/safe can use result_type (see above) + if np.result_type(in_t(0), to_dtype) != to_dtype: + break + else: + # dtypes OK (no break), choose them and break outer chosen = in_dtypes break @@ -646,11 +752,11 @@ def _resolve_dtype( def __call__( self, *args: Any, - out: Union[ndarray, None] = None, + out: ndarray | None = None, where: bool = True, casting: CastingKind = "same_kind", order: str = "K", - dtype: Union[np.dtype[Any], None] = None, + dtype: np.dtype[Any] | None = None, **kwargs: Any, ) -> ndarray: arrs, (out,), out_shape, where = self._prepare_operands( @@ -677,11 +783,17 @@ def __call__( arrs, orig_args, casting, precision_fixed ) + # Check python integers operands. For comparisons, this may return + # new values and op_code when the integer is out-of-bounds. x1, x2 = arrs + x1, x2, op_code = self._post_resolution_check( + x1, x2, orig_args[0], orig_args[1], self._op_code + ) + result = self._maybe_create_result( out, out_shape, res_dtype, casting, (x1, x2) ) - result._thunk.binary_op(self._op_code, x1._thunk, x2._thunk, where, ()) + result._thunk.binary_op(op_code, x1._thunk, x2._thunk, where, ()) return self._maybe_cast_output(out, result) @@ -689,12 +801,12 @@ def __call__( def reduce( self, array: ndarray, - axis: Union[int, tuple[int, ...], None] = 0, - dtype: Union[np.dtype[Any], None] = None, - out: Union[ndarray, None] = None, + axis: int | tuple[int, ...] | None = 0, + dtype: np.dtype[Any] | None = None, + out: ndarray | None = None, keepdims: bool = False, - initial: Union[Any, None] = None, - where: Optional[ndarray] = None, + initial: Any | None = None, + where: ndarray | None = None, ) -> ndarray: """ reduce(array, axis=0, dtype=None, out=None, keepdims=False, initial= binary_ufunc: doc = _BINARY_DOCSTRING_TEMPLATE.format(summary, name) types_dict = dict(_parse_binary_ufunc_type(ty) for ty in types) @@ -823,4 +947,5 @@ def create_binary_ufunc( types_dict, red_code=red_code, use_common_type=use_common_type, + post_resolution_check=post_resolution_check, ) diff --git a/cupynumeric/_utils/__init__.py b/cupynumeric/_utils/__init__.py new file mode 100644 index 000000000..626ef7aae --- /dev/null +++ b/cupynumeric/_utils/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import numpy as np + +is_np2 = np.lib.NumpyVersion(np.__version__) >= "2.0.0b1" diff --git a/cupynumeric/_utils/array.py b/cupynumeric/_utils/array.py new file mode 100644 index 000000000..eda134045 --- /dev/null +++ b/cupynumeric/_utils/array.py @@ -0,0 +1,113 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from functools import reduce +from typing import Any + +import legate.core.types as ty +import numpy as np + +from ..types import NdShape + +SUPPORTED_DTYPES = { + np.dtype(bool): ty.bool_, + np.dtype(np.int8): ty.int8, + np.dtype(np.int16): ty.int16, + np.dtype(np.int32): ty.int32, + np.dtype(np.int64): ty.int64, + np.dtype(np.uint8): ty.uint8, + np.dtype(np.uint16): ty.uint16, + np.dtype(np.uint32): ty.uint32, + np.dtype(np.uint64): ty.uint64, + np.dtype(np.float16): ty.float16, + np.dtype(np.float32): ty.float32, + np.dtype(np.float64): ty.float64, + np.dtype(np.complex64): ty.complex64, + np.dtype(np.complex128): ty.complex128, +} + + +def is_supported_dtype(dtype: str | np.dtype[Any]) -> bool: + """ + Whether a NumPy dtype is supported by cuPyNumeric + + Parameters + ---------- + dtype : data-type + The dtype to query + + Returns + ------- + res : bool + True if `dtype` is a supported dtype + """ + return np.dtype(dtype) in SUPPORTED_DTYPES + + +def to_core_type(dtype: str | np.dtype[Any]) -> ty.Type: + core_dtype = SUPPORTED_DTYPES.get(np.dtype(dtype)) + if core_dtype is None: + raise TypeError(f"cuPyNumeric does not support dtype={dtype}") + return core_dtype + + +def is_advanced_indexing(key: Any) -> bool: + if key is Ellipsis or key is None: # np.newdim case + return False + if np.isscalar(key): + return False + if isinstance(key, slice): + return False + if isinstance(key, tuple): + return any(is_advanced_indexing(k) for k in key) + # Any other kind of thing leads to advanced indexing + return True + + +def calculate_volume(shape: NdShape) -> int: + if len(shape) == 0: + return 0 + return reduce(lambda x, y: x * y, shape) + + +def max_identity( + ty: np.dtype[Any], +) -> int | np.floating[Any] | bool | np.complexfloating[Any, Any]: + if ty.kind == "i" or ty.kind == "u": + return np.iinfo(ty).min + elif ty.kind == "f": + return np.finfo(ty).min + elif ty.kind == "c": + return np.finfo(np.float64).min + np.finfo(np.float64).min * 1j + elif ty.kind == "b": + return False + else: + raise ValueError(f"Unsupported dtype: {ty}") + + +def min_identity( + ty: np.dtype[Any], +) -> int | np.floating[Any] | bool | np.complexfloating[Any, Any]: + if ty.kind == "i" or ty.kind == "u": + return np.iinfo(ty).max + elif ty.kind == "f": + return np.finfo(ty).max + elif ty.kind == "c": + return np.finfo(np.float64).max + np.finfo(np.float64).max * 1j + elif ty.kind == "b": + return True + else: + raise ValueError(f"Unsupported dtype: {ty}") diff --git a/cunumeric/coverage.py b/cupynumeric/_utils/coverage.py similarity index 87% rename from cunumeric/coverage.py rename to cupynumeric/_utils/coverage.py index a8e57285f..d0d0f0eba 100644 --- a/cunumeric/coverage.py +++ b/cupynumeric/_utils/coverage.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,36 +17,21 @@ import warnings from dataclasses import dataclass from functools import WRAPPER_ASSIGNMENTS, wraps -from types import ( - BuiltinFunctionType, - FunctionType, - MethodDescriptorType, - MethodType, - ModuleType, -) -from typing import ( - Any, - Callable, - Container, - Iterable, - Mapping, - Optional, - Union, - cast, -) +from types import BuiltinFunctionType, FunctionType, ModuleType +from typing import Any, Callable, Container, Iterable, Mapping, Protocol, cast from legate.core import track_provenance from legate.core.utils import OrderedSet -from typing_extensions import Protocol -from .runtime import runtime -from .settings import settings -from .utils import deep_apply, find_last_user_frames, find_last_user_stacklevel +from ..runtime import runtime +from ..settings import settings +from .stack import find_last_user_frames, find_last_user_stacklevel +from .structure import deep_apply __all__ = ("clone_module", "clone_class") FALLBACK_WARNING = ( - "cuNumeric has not implemented {what} " + "cuPyNumeric has not implemented {what} " + "and is falling back to canonical NumPy. " + "You may notice significantly decreased performance " + "for this function call." @@ -60,7 +45,7 @@ def filter_namespace( ns: Mapping[str, Any], *, - omit_names: Optional[Container[str]] = None, + omit_names: Container[str] | None = None, omit_types: tuple[type, ...] = (), ) -> dict[str, Any]: omit_names = omit_names or OrderedSet() @@ -84,7 +69,7 @@ class CuWrapperMetadata: class CuWrapped(AnyCallable, Protocol): - _cunumeric: CuWrapperMetadata + _cupynumeric: CuWrapperMetadata __wrapped__: AnyCallable __name__: str __qualname__: str @@ -131,7 +116,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: multi = "Multiple GPUs" in (getattr(func, "__doc__", None) or "") single = "Single GPU" in (getattr(func, "__doc__", None) or "") or multi - wrapper._cunumeric = CuWrapperMetadata( + wrapper._cupynumeric = CuWrapperMetadata( implemented=True, single=single, multi=multi ) @@ -148,7 +133,7 @@ def unimplemented( prefix: str, name: str, reporting: bool = True, - fallback: Union[Callable[[Any], Any], None] = None, + fallback: Callable[[Any], Any] | None = None, ) -> CuWrapped: name = f"{prefix}.{name}" @@ -156,7 +141,7 @@ def unimplemented( # all array-like arguments to `numpy.ndarray` through `__array__()` (taking # some care to skip the `__array_function__` dispatch logic, to avoid # infinite loops). However, it appears that this behavior is inconsistent - # in NumPy, so we will instead convert any `cunumeric.ndarray`s manually + # in NumPy, so we will instead convert any `cupynumeric.ndarray`s manually # before calling into NumPy. wrapper: CuWrapped @@ -194,13 +179,13 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) wrapper.__doc__ = f""" - cuNumeric has not implemented this function, and will fall back to NumPy. + cuPyNumeric has not implemented this function, and will fall back to NumPy. See Also -------- {name} """ - wrapper._cunumeric = CuWrapperMetadata(implemented=False) + wrapper._cupynumeric = CuWrapperMetadata(implemented=False) return wrapper @@ -208,7 +193,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: def clone_module( origin_module: ModuleType, new_globals: dict[str, Any], - fallback: Union[Callable[[Any], Any], None] = None, + fallback: Callable[[Any], Any] | None = None, include_builtin_function_type: bool = False, ) -> None: """Copy attributes from one module to another, excluding submodules @@ -224,7 +209,7 @@ def clone_module( new_globals : dict A globals() dict for the new module to clone into - fallback : Union[Callable[[Any], Any], None] + fallback :Callable[[Any], Any] | None A function that will be applied to each argument before calling into the original module, to handle unimplemented functions. The function will be called recursively on list/tuple/dict containers, and should @@ -251,7 +236,7 @@ def clone_module( reporting = settings.report_coverage() - from ._ufunc.ufunc import ufunc as lgufunc + from .._ufunc.ufunc import ufunc as lgufunc for attr, value in new_globals.items(): # Only need to wrap things that are in the origin module to begin with @@ -315,13 +300,19 @@ def clone_module( def should_wrap(obj: object) -> bool: - return isinstance(obj, (FunctionType, MethodType, MethodDescriptorType)) + # custom callables, e.g. cython used in np2, do not inherit anything. See + # https://github.com/nv-legate/cupynumeric.internal/issues/179#issuecomment-2423813051 + return ( + callable(obj) + and hasattr(obj, "__get__") + and not hasattr(obj, "__set__") + ) def clone_class( origin_class: type, - omit_names: Union[Iterable[str], None] = None, - fallback: Union[Callable[[Any], Any], None] = None, + omit_names: Iterable[str] | None = None, + fallback: Callable[[Any], Any] | None = None, ) -> Callable[[type], type]: """Copy attributes from one class to another @@ -373,12 +364,12 @@ def _clone_class(cls: type) -> type: def is_implemented(obj: Any) -> bool: - return hasattr(obj, "_cunumeric") and obj._cunumeric.implemented + return hasattr(obj, "_cupynumeric") and obj._cupynumeric.implemented def is_single(obj: Any) -> bool: - return hasattr(obj, "_cunumeric") and obj._cunumeric.single + return hasattr(obj, "_cupynumeric") and obj._cupynumeric.single def is_multi(obj: Any) -> bool: - return hasattr(obj, "_cunumeric") and obj._cunumeric.multi + return hasattr(obj, "_cupynumeric") and obj._cupynumeric.multi diff --git a/cunumeric/utils.py b/cupynumeric/_utils/linalg.py similarity index 51% rename from cunumeric/utils.py rename to cupynumeric/_utils/linalg.py index 8c2d70140..5aa0b292c 100644 --- a/cunumeric/utils.py +++ b/cupynumeric/_utils/linalg.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,108 +14,12 @@ # from __future__ import annotations -import traceback -from functools import reduce from string import ascii_lowercase, ascii_uppercase -from types import FrameType -from typing import Any, Callable, List, Sequence, Tuple, TypeVar, Union +from typing import Sequence -import legate.core.types as ty -import numpy as np from legate.core.utils import OrderedSet -from .types import NdShape - -SUPPORTED_DTYPES = { - np.dtype(np.bool_): ty.bool_, - np.dtype(np.int8): ty.int8, - np.dtype(np.int16): ty.int16, - np.dtype(np.int32): ty.int32, - np.dtype(np.int64): ty.int64, - np.dtype(np.uint8): ty.uint8, - np.dtype(np.uint16): ty.uint16, - np.dtype(np.uint32): ty.uint32, - np.dtype(np.uint64): ty.uint64, - np.dtype(np.float16): ty.float16, - np.dtype(np.float32): ty.float32, - np.dtype(np.float64): ty.float64, - np.dtype(np.complex64): ty.complex64, - np.dtype(np.complex128): ty.complex128, -} - - -def is_supported_type(dtype: Union[str, np.dtype[Any]]) -> bool: - return np.dtype(dtype) in SUPPORTED_DTYPES - - -def to_core_dtype(dtype: Union[str, np.dtype[Any]]) -> ty.Dtype: - core_dtype = SUPPORTED_DTYPES.get(np.dtype(dtype)) - if core_dtype is None: - raise TypeError(f"cuNumeric does not support dtype={dtype}") - return core_dtype - - -def is_advanced_indexing(key: Any) -> bool: - if key is Ellipsis or key is None: # np.newdim case - return False - if np.isscalar(key): - return False - if isinstance(key, slice): - return False - if isinstance(key, tuple): - return any(is_advanced_indexing(k) for k in key) - # Any other kind of thing leads to advanced indexing - return True - - -def find_last_user_stacklevel() -> int: - stacklevel = 1 - for frame, _ in traceback.walk_stack(None): - if not frame.f_globals["__name__"].startswith("cunumeric"): - break - stacklevel += 1 - return stacklevel - - -def get_line_number_from_frame(frame: FrameType) -> str: - return f"{frame.f_code.co_filename}:{frame.f_lineno}" - - -def find_last_user_frames(top_only: bool = True) -> str: - for last, _ in traceback.walk_stack(None): - if "__name__" not in last.f_globals: - continue - name = last.f_globals["__name__"] - if not any(name.startswith(pkg) for pkg in ("cunumeric", "legate")): - break - - if top_only: - return get_line_number_from_frame(last) - - frames: list[FrameType] = [] - curr: Union[FrameType, None] = last - while curr is not None: - if "legion_top.py" in curr.f_code.co_filename: - break - frames.append(curr) - curr = curr.f_back - return "|".join(get_line_number_from_frame(f) for f in frames) - - -def calculate_volume(shape: NdShape) -> int: - if len(shape) == 0: - return 0 - return reduce(lambda x, y: x * y, shape) - - -T = TypeVar("T") - - -def tuple_pop(tup: Tuple[T, ...], index: int) -> Tuple[T, ...]: - return tup[:index] + tup[index + 1 :] - - -Modes = Tuple[List[str], List[str], List[str]] +Modes = tuple[list[str], list[str], list[str]] def dot_modes(a_ndim: int, b_ndim: int) -> Modes: @@ -165,14 +69,9 @@ def matmul_modes(a_ndim: int, b_ndim: int) -> Modes: Axes = Sequence[int] -AxesPair = Tuple[Axes, Axes] -AxesPairLikeTuple = Union[ - Tuple[int, int], - Tuple[int, Axes], - Tuple[Axes, int], - Tuple[Axes, Axes], -] -AxesPairLike = Union[int, AxesPairLikeTuple] +AxesPair = tuple[Axes, Axes] +AxesPairLikeTuple = tuple[int | Axes, int | Axes] +AxesPairLike = int | AxesPairLikeTuple def tensordot_modes(a_ndim: int, b_ndim: int, axes: AxesPairLike) -> Modes: @@ -229,23 +128,3 @@ def check_axes(a_axes: Axes, b_axes: Axes) -> None: ] return (a_modes, b_modes, a_out + b_out) - - -def deep_apply(obj: Any, func: Callable[[Any], Any]) -> Any: - """ - Apply the provided function to objects contained at any depth within a data - structure. - - This function will recurse over arbitrary nestings of lists, tuples and - dicts. This recursion logic is rather limited, but this function is - primarily meant to be used for arguments of NumPy API calls, which - shouldn't nest their arrays very deep. - """ - if isinstance(obj, list): - return [deep_apply(x, func) for x in obj] - elif isinstance(obj, tuple): - return tuple(deep_apply(x, func) for x in obj) - elif isinstance(obj, dict): - return {k: deep_apply(v, func) for k, v in obj.items()} - else: - return func(obj) diff --git a/cupynumeric/_utils/stack.py b/cupynumeric/_utils/stack.py new file mode 100644 index 000000000..f5e714a3c --- /dev/null +++ b/cupynumeric/_utils/stack.py @@ -0,0 +1,52 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import traceback +from types import FrameType + + +def find_last_user_stacklevel() -> int: + stacklevel = 1 + for frame, _ in traceback.walk_stack(None): + if not frame.f_globals["__name__"].startswith("cupynumeric"): + break + stacklevel += 1 + return stacklevel + + +def get_line_number_from_frame(frame: FrameType) -> str: + return f"{frame.f_code.co_filename}:{frame.f_lineno}" + + +def find_last_user_frames(top_only: bool = True) -> str: + for last, _ in traceback.walk_stack(None): + if "__name__" not in last.f_globals: + continue + name = last.f_globals["__name__"] + if not any(name.startswith(pkg) for pkg in ("cupynumeric", "legate")): + break + + if top_only: + return get_line_number_from_frame(last) + + frames: list[FrameType] = [] + curr: FrameType | None = last + while curr is not None: + if "legion_top.py" in curr.f_code.co_filename: + break + frames.append(curr) + curr = curr.f_back + return "|".join(get_line_number_from_frame(f) for f in frames) diff --git a/cupynumeric/_utils/structure.py b/cupynumeric/_utils/structure.py new file mode 100644 index 000000000..14bf6a3a4 --- /dev/null +++ b/cupynumeric/_utils/structure.py @@ -0,0 +1,37 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import Any, Callable + + +def deep_apply(obj: Any, func: Callable[[Any], Any]) -> Any: + """ + Apply the provided function to objects contained at any depth within a data + structure. + + This function will recurse over arbitrary nestings of lists, tuples and + dicts. This recursion logic is rather limited, but this function is + primarily meant to be used for arguments of NumPy API calls, which + shouldn't nest their arrays very deep. + """ + if isinstance(obj, list): + return [deep_apply(x, func) for x in obj] + elif isinstance(obj, tuple): + return tuple(deep_apply(x, func) for x in obj) + elif isinstance(obj, dict): + return {k: deep_apply(v, func) for k, v in obj.items()} + else: + return func(obj) diff --git a/cunumeric/_version.py b/cupynumeric/_version.py similarity index 99% rename from cunumeric/_version.py rename to cupynumeric/_version.py index 7c006fdc1..9d0505089 100644 --- a/cunumeric/_version.py +++ b/cupynumeric/_version.py @@ -43,8 +43,8 @@ def get_config(): cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "v" - cfg.parentdir_prefix = "cunumeric-" - cfg.versionfile_source = "cunumeric/_version.py" + cfg.parentdir_prefix = "cupynumeric-" + cfg.versionfile_source = "cupynumeric/_version.py" cfg.verbose = False return cfg diff --git a/cupynumeric/config.py b/cupynumeric/config.py new file mode 100644 index 000000000..b3cac0573 --- /dev/null +++ b/cupynumeric/config.py @@ -0,0 +1,835 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +import os +import platform +from abc import abstractmethod +from ctypes import CDLL, RTLD_GLOBAL +from enum import IntEnum, unique +from typing import TYPE_CHECKING, Any, cast + +import cffi # type: ignore +import numpy as np + +if TYPE_CHECKING: + import numpy.typing as npt + + +class _ReductionOpIds: + argmax_redop_id: int + argmin_redop_id: int + + +class _CupynumericSharedLib: + CUPYNUMERIC_ADVANCED_INDEXING: int + CUPYNUMERIC_ARANGE: int + CUPYNUMERIC_ARGWHERE: int + CUPYNUMERIC_BATCHED_CHOLESKY: int + CUPYNUMERIC_BINARY_OP: int + CUPYNUMERIC_BINARY_RED: int + CUPYNUMERIC_BINCOUNT: int + CUPYNUMERIC_BINOP_ADD: int + CUPYNUMERIC_BINOP_ARCTAN2: int + CUPYNUMERIC_BINOP_BITWISE_AND: int + CUPYNUMERIC_BINOP_BITWISE_OR: int + CUPYNUMERIC_BINOP_BITWISE_XOR: int + CUPYNUMERIC_BINOP_COPYSIGN: int + CUPYNUMERIC_BINOP_DIVIDE: int + CUPYNUMERIC_BINOP_EQUAL: int + CUPYNUMERIC_BINOP_FLOAT_POWER: int + CUPYNUMERIC_BINOP_FLOOR_DIVIDE: int + CUPYNUMERIC_BINOP_FMOD: int + CUPYNUMERIC_BINOP_GCD: int + CUPYNUMERIC_BINOP_GREATER: int + CUPYNUMERIC_BINOP_GREATER_EQUAL: int + CUPYNUMERIC_BINOP_HYPOT: int + CUPYNUMERIC_BINOP_ISCLOSE: int + CUPYNUMERIC_BINOP_LCM: int + CUPYNUMERIC_BINOP_LDEXP: int + CUPYNUMERIC_BINOP_LEFT_SHIFT: int + CUPYNUMERIC_BINOP_LESS: int + CUPYNUMERIC_BINOP_LESS_EQUAL: int + CUPYNUMERIC_BINOP_LOGADDEXP2: int + CUPYNUMERIC_BINOP_LOGADDEXP: int + CUPYNUMERIC_BINOP_LOGICAL_AND: int + CUPYNUMERIC_BINOP_LOGICAL_OR: int + CUPYNUMERIC_BINOP_LOGICAL_XOR: int + CUPYNUMERIC_BINOP_MAXIMUM: int + CUPYNUMERIC_BINOP_MINIMUM: int + CUPYNUMERIC_BINOP_MOD: int + CUPYNUMERIC_BINOP_MULTIPLY: int + CUPYNUMERIC_BINOP_NEXTAFTER: int + CUPYNUMERIC_BINOP_NOT_EQUAL: int + CUPYNUMERIC_BINOP_POWER: int + CUPYNUMERIC_BINOP_RIGHT_SHIFT: int + CUPYNUMERIC_BINOP_SUBTRACT: int + CUPYNUMERIC_BITGENERATOR: int + CUPYNUMERIC_BITGENOP_DISTRIBUTION: int + CUPYNUMERIC_BITGENTYPE_DEFAULT: int + CUPYNUMERIC_BITGENTYPE_XORWOW: int + CUPYNUMERIC_BITGENTYPE_MRG32K3A: int + CUPYNUMERIC_BITGENTYPE_MTGP32: int + CUPYNUMERIC_BITGENTYPE_MT19937: int + CUPYNUMERIC_BITGENTYPE_PHILOX4_32_10: int + CUPYNUMERIC_BITGENDIST_INTEGERS_16: int + CUPYNUMERIC_BITGENDIST_INTEGERS_32: int + CUPYNUMERIC_BITGENDIST_INTEGERS_64: int + CUPYNUMERIC_BITGENDIST_UNIFORM_32: int + CUPYNUMERIC_BITGENDIST_UNIFORM_64: int + CUPYNUMERIC_BITGENDIST_LOGNORMAL_32: int + CUPYNUMERIC_BITGENDIST_LOGNORMAL_64: int + CUPYNUMERIC_BITGENDIST_NORMAL_32: int + CUPYNUMERIC_BITGENDIST_NORMAL_64: int + CUPYNUMERIC_BITGENDIST_POISSON: int + CUPYNUMERIC_BITGENDIST_EXPONENTIAL_32: int + CUPYNUMERIC_BITGENDIST_EXPONENTIAL_64: int + CUPYNUMERIC_BITGENDIST_GUMBEL_32: int + CUPYNUMERIC_BITGENDIST_GUMBEL_64: int + CUPYNUMERIC_BITGENDIST_LAPLACE_32: int + CUPYNUMERIC_BITGENDIST_LAPLACE_64: int + CUPYNUMERIC_BITGENDIST_LOGISTIC_32: int + CUPYNUMERIC_BITGENDIST_LOGISTIC_64: int + CUPYNUMERIC_BITGENDIST_PARETO_32: int + CUPYNUMERIC_BITGENDIST_PARETO_64: int + CUPYNUMERIC_BITGENDIST_POWER_32: int + CUPYNUMERIC_BITGENDIST_POWER_64: int + CUPYNUMERIC_BITGENDIST_RAYLEIGH_32: int + CUPYNUMERIC_BITGENDIST_RAYLEIGH_64: int + CUPYNUMERIC_BITGENDIST_CAUCHY_32: int + CUPYNUMERIC_BITGENDIST_CAUCHY_64: int + CUPYNUMERIC_BITGENDIST_TRIANGULAR_32: int + CUPYNUMERIC_BITGENDIST_TRIANGULAR_64: int + CUPYNUMERIC_BITGENDIST_WEIBULL_32: int + CUPYNUMERIC_BITGENDIST_WEIBULL_64: int + CUPYNUMERIC_BITGENDIST_BYTES: int + CUPYNUMERIC_BITGENDIST_BETA_32: int + CUPYNUMERIC_BITGENDIST_BETA_64: int + CUPYNUMERIC_BITGENDIST_F_32: int + CUPYNUMERIC_BITGENDIST_F_64: int + CUPYNUMERIC_BITGENDIST_LOGSERIES: int + CUPYNUMERIC_BITGENDIST_NONCENTRAL_F_32: int + CUPYNUMERIC_BITGENDIST_NONCENTRAL_F_64: int + CUPYNUMERIC_BITGENDIST_CHISQUARE_32: int + CUPYNUMERIC_BITGENDIST_CHISQUARE_64: int + CUPYNUMERIC_BITGENDIST_GAMMA_32: int + CUPYNUMERIC_BITGENDIST_GAMMA_64: int + CUPYNUMERIC_BITGENDIST_STANDARD_T_32: int + CUPYNUMERIC_BITGENDIST_STANDARD_T_64: int + CUPYNUMERIC_BITGENDIST_HYPERGEOMETRIC: int + CUPYNUMERIC_BITGENDIST_VONMISES_32: int + CUPYNUMERIC_BITGENDIST_VONMISES_64: int + CUPYNUMERIC_BITGENDIST_ZIPF: int + CUPYNUMERIC_BITGENDIST_GEOMETRIC: int + CUPYNUMERIC_BITGENDIST_WALD_32: int + CUPYNUMERIC_BITGENDIST_WALD_64: int + CUPYNUMERIC_BITGENDIST_BINOMIAL: int + CUPYNUMERIC_BITGENDIST_NEGATIVE_BINOMIAL: int + CUPYNUMERIC_BITGENOP_CREATE: int + CUPYNUMERIC_BITGENOP_DESTROY: int + CUPYNUMERIC_BITGENOP_RAND_RAW: int + CUPYNUMERIC_BITORDER_BIG: int + CUPYNUMERIC_BITORDER_LITTLE: int + CUPYNUMERIC_CHOOSE: int + CUPYNUMERIC_CONTRACT: int + CUPYNUMERIC_CONVERT: int + CUPYNUMERIC_CONVERT_NAN_NOOP: int + CUPYNUMERIC_CONVERT_NAN_PROD: int + CUPYNUMERIC_CONVERT_NAN_SUM: int + CUPYNUMERIC_CONVOLVE: int + CUPYNUMERIC_CONVOLVE_AUTO: int + CUPYNUMERIC_CONVOLVE_DIRECT: int + CUPYNUMERIC_CONVOLVE_FFT: int + CUPYNUMERIC_DIAG: int + CUPYNUMERIC_DOT: int + CUPYNUMERIC_EYE: int + CUPYNUMERIC_FFT: int + CUPYNUMERIC_FFT_C2C: int + CUPYNUMERIC_FFT_C2R: int + CUPYNUMERIC_FFT_D2Z: int + CUPYNUMERIC_FFT_FORWARD: int + CUPYNUMERIC_FFT_INVERSE: int + CUPYNUMERIC_FFT_R2C: int + CUPYNUMERIC_FFT_Z2D: int + CUPYNUMERIC_FFT_Z2Z: int + CUPYNUMERIC_FILL: int + CUPYNUMERIC_FLIP: int + CUPYNUMERIC_GEMM: int + CUPYNUMERIC_HISTOGRAM: int + CUPYNUMERIC_LOAD_CUDALIBS: int + CUPYNUMERIC_MATMUL: int + CUPYNUMERIC_MATVECMUL: int + CUPYNUMERIC_MAX_MAPPERS: int + CUPYNUMERIC_MAX_REDOPS: int + CUPYNUMERIC_MAX_TASKS: int + CUPYNUMERIC_MP_POTRF: int + CUPYNUMERIC_MP_SOLVE: int + CUPYNUMERIC_NONZERO: int + CUPYNUMERIC_PACKBITS: int + CUPYNUMERIC_POTRF: int + CUPYNUMERIC_PUTMASK: int + CUPYNUMERIC_QR: int + CUPYNUMERIC_RAND: int + CUPYNUMERIC_READ: int + CUPYNUMERIC_RED_ALL: int + CUPYNUMERIC_RED_ANY: int + CUPYNUMERIC_RED_ARGMAX: int + CUPYNUMERIC_RED_ARGMIN: int + CUPYNUMERIC_RED_CONTAINS: int + CUPYNUMERIC_RED_COUNT_NONZERO: int + CUPYNUMERIC_RED_MAX: int + CUPYNUMERIC_RED_MIN: int + CUPYNUMERIC_RED_NANARGMAX: int + CUPYNUMERIC_RED_NANARGMIN: int + CUPYNUMERIC_RED_NANMAX: int + CUPYNUMERIC_RED_NANMIN: int + CUPYNUMERIC_RED_NANPROD: int + CUPYNUMERIC_RED_NANSUM: int + CUPYNUMERIC_RED_PROD: int + CUPYNUMERIC_RED_SUM: int + CUPYNUMERIC_RED_SUM_SQUARES: int + CUPYNUMERIC_RED_VARIANCE: int + CUPYNUMERIC_REPEAT: int + CUPYNUMERIC_SCALAR_UNARY_RED: int + CUPYNUMERIC_SCAN_GLOBAL: int + CUPYNUMERIC_SCAN_LOCAL: int + CUPYNUMERIC_SCAN_PROD: int + CUPYNUMERIC_SCAN_SUM: int + CUPYNUMERIC_SEARCHSORTED: int + CUPYNUMERIC_SELECT: int + CUPYNUMERIC_SOLVE: int + CUPYNUMERIC_SORT: int + CUPYNUMERIC_SVD: int + CUPYNUMERIC_SYRK: int + CUPYNUMERIC_TILE: int + CUPYNUMERIC_TRANSPOSE_COPY_2D: int + CUPYNUMERIC_TRILU: int + CUPYNUMERIC_TRSM: int + CUPYNUMERIC_UNARY_OP: int + CUPYNUMERIC_UNARY_RED: int + CUPYNUMERIC_UNIQUE: int + CUPYNUMERIC_UNIQUE_REDUCE: int + CUPYNUMERIC_UNLOAD_CUDALIBS: int + CUPYNUMERIC_UNPACKBITS: int + CUPYNUMERIC_UOP_ABSOLUTE: int + CUPYNUMERIC_UOP_ANGLE: int + CUPYNUMERIC_UOP_ARCCOS: int + CUPYNUMERIC_UOP_ARCCOSH: int + CUPYNUMERIC_UOP_ARCSIN: int + CUPYNUMERIC_UOP_ARCSINH: int + CUPYNUMERIC_UOP_ARCTAN: int + CUPYNUMERIC_UOP_ARCTANH: int + CUPYNUMERIC_UOP_CBRT: int + CUPYNUMERIC_UOP_CEIL: int + CUPYNUMERIC_UOP_CLIP: int + CUPYNUMERIC_UOP_CONJ: int + CUPYNUMERIC_UOP_COPY: int + CUPYNUMERIC_UOP_COS: int + CUPYNUMERIC_UOP_COSH: int + CUPYNUMERIC_UOP_DEG2RAD: int + CUPYNUMERIC_UOP_EXP2: int + CUPYNUMERIC_UOP_EXP: int + CUPYNUMERIC_UOP_EXPM1: int + CUPYNUMERIC_UOP_FLOOR: int + CUPYNUMERIC_UOP_FREXP: int + CUPYNUMERIC_UOP_GETARG: int + CUPYNUMERIC_UOP_IMAG: int + CUPYNUMERIC_UOP_INVERT: int + CUPYNUMERIC_UOP_ISFINITE: int + CUPYNUMERIC_UOP_ISINF: int + CUPYNUMERIC_UOP_ISNAN: int + CUPYNUMERIC_UOP_LOG10: int + CUPYNUMERIC_UOP_LOG1P: int + CUPYNUMERIC_UOP_LOG2: int + CUPYNUMERIC_UOP_LOG: int + CUPYNUMERIC_UOP_LOGICAL_NOT: int + CUPYNUMERIC_UOP_MODF: int + CUPYNUMERIC_UOP_NEGATIVE: int + CUPYNUMERIC_UOP_POSITIVE: int + CUPYNUMERIC_UOP_RAD2DEG: int + CUPYNUMERIC_UOP_REAL: int + CUPYNUMERIC_UOP_RECIPROCAL: int + CUPYNUMERIC_UOP_RINT: int + CUPYNUMERIC_UOP_ROUND: int + CUPYNUMERIC_UOP_SIGN: int + CUPYNUMERIC_UOP_SIGNBIT: int + CUPYNUMERIC_UOP_SIN: int + CUPYNUMERIC_UOP_SINH: int + CUPYNUMERIC_UOP_SQRT: int + CUPYNUMERIC_UOP_SQUARE: int + CUPYNUMERIC_UOP_TAN: int + CUPYNUMERIC_UOP_TANH: int + CUPYNUMERIC_UOP_TRUNC: int + CUPYNUMERIC_WHERE: int + CUPYNUMERIC_WINDOW: int + CUPYNUMERIC_WINDOW_BARLETT: int + CUPYNUMERIC_WINDOW_BLACKMAN: int + CUPYNUMERIC_WINDOW_HAMMING: int + CUPYNUMERIC_WINDOW_HANNING: int + CUPYNUMERIC_WINDOW_KAISER: int + CUPYNUMERIC_WRAP: int + CUPYNUMERIC_WRITE: int + CUPYNUMERIC_ZIP: int + + @abstractmethod + def cupynumeric_has_cusolvermp(self) -> bool: + ... + + @abstractmethod + def cupynumeric_max_eager_volume(self) -> int: + ... + + @abstractmethod + def cupynumeric_register_reduction_ops(self, code: int) -> _ReductionOpIds: + ... + + +def dlopen_no_autoclose(ffi: Any, lib_path: str) -> Any: + # Use an already-opened library handle, which cffi will convert to a + # regular FFI object (using the definitions previously added using + # ffi.cdef), but will not automatically dlclose() on collection. + lib = CDLL(lib_path, mode=RTLD_GLOBAL) + return ffi.dlopen(ffi.cast("void *", lib._handle)) + + +# Load the cuPyNumeric library first so we have a shard object that +# we can use to initialize all these configuration enumerations +class CuPyNumericLib: + def __init__(self, name: str) -> None: + self.name = name + + shared_lib_path = self.get_shared_library() + assert shared_lib_path is not None + header = self.get_c_header() + ffi = cffi.FFI() + if header is not None: + ffi.cdef(header) + # Don't use ffi.dlopen(), because that will call dlclose() + # automatically when the object gets collected, thus removing + # symbols that may be needed when destroying C++ objects later + # (e.g. vtable entries, which will be queried for virtual + # destructors), causing errors at shutdown. + shared_lib = dlopen_no_autoclose(ffi, shared_lib_path) + self.shared_object = cast(_CupynumericSharedLib, shared_lib) + + def register(self) -> None: + from legate.core import get_legate_runtime + + # We need to make sure that the runtime is started + get_legate_runtime() + + callback = getattr( + self.shared_object, "cupynumeric_perform_registration" + ) + callback() + + def get_shared_library(self) -> str: + from .install_info import libpath + + return os.path.join( + libpath, "libcupynumeric" + self.get_library_extension() + ) + + def get_c_header(self) -> str: + from .install_info import header + + return header + + @staticmethod + def get_library_extension() -> str: + os_name = platform.system() + if os_name == "Linux": + return ".so" + elif os_name == "Darwin": + return ".dylib" + raise RuntimeError(f"unknown platform {os_name!r}") + + +CUPYNUMERIC_LIB_NAME = "cupynumeric" +cupynumeric_lib = CuPyNumericLib(CUPYNUMERIC_LIB_NAME) +cupynumeric_lib.register() +_cupynumeric = cupynumeric_lib.shared_object + + +# Match these to CuPyNumericOpCode in cupynumeric_c.h +@unique +class CuPyNumericOpCode(IntEnum): + ADVANCED_INDEXING = _cupynumeric.CUPYNUMERIC_ADVANCED_INDEXING + ARANGE = _cupynumeric.CUPYNUMERIC_ARANGE + ARGWHERE = _cupynumeric.CUPYNUMERIC_ARGWHERE + BATCHED_CHOLESKY = _cupynumeric.CUPYNUMERIC_BATCHED_CHOLESKY + BINARY_OP = _cupynumeric.CUPYNUMERIC_BINARY_OP + BINARY_RED = _cupynumeric.CUPYNUMERIC_BINARY_RED + BINCOUNT = _cupynumeric.CUPYNUMERIC_BINCOUNT + BITGENERATOR = _cupynumeric.CUPYNUMERIC_BITGENERATOR + CHOOSE = _cupynumeric.CUPYNUMERIC_CHOOSE + CONTRACT = _cupynumeric.CUPYNUMERIC_CONTRACT + CONVERT = _cupynumeric.CUPYNUMERIC_CONVERT + CONVOLVE = _cupynumeric.CUPYNUMERIC_CONVOLVE + DIAG = _cupynumeric.CUPYNUMERIC_DIAG + DOT = _cupynumeric.CUPYNUMERIC_DOT + EYE = _cupynumeric.CUPYNUMERIC_EYE + FFT = _cupynumeric.CUPYNUMERIC_FFT + FILL = _cupynumeric.CUPYNUMERIC_FILL + FLIP = _cupynumeric.CUPYNUMERIC_FLIP + GEMM = _cupynumeric.CUPYNUMERIC_GEMM + HISTOGRAM = _cupynumeric.CUPYNUMERIC_HISTOGRAM + LOAD_CUDALIBS = _cupynumeric.CUPYNUMERIC_LOAD_CUDALIBS + MATMUL = _cupynumeric.CUPYNUMERIC_MATMUL + MATVECMUL = _cupynumeric.CUPYNUMERIC_MATVECMUL + MP_POTRF = _cupynumeric.CUPYNUMERIC_MP_POTRF + MP_SOLVE = _cupynumeric.CUPYNUMERIC_MP_SOLVE + NONZERO = _cupynumeric.CUPYNUMERIC_NONZERO + PACKBITS = _cupynumeric.CUPYNUMERIC_PACKBITS + POTRF = _cupynumeric.CUPYNUMERIC_POTRF + PUTMASK = _cupynumeric.CUPYNUMERIC_PUTMASK + QR = _cupynumeric.CUPYNUMERIC_QR + RAND = _cupynumeric.CUPYNUMERIC_RAND + READ = _cupynumeric.CUPYNUMERIC_READ + REPEAT = _cupynumeric.CUPYNUMERIC_REPEAT + SCALAR_UNARY_RED = _cupynumeric.CUPYNUMERIC_SCALAR_UNARY_RED + SCAN_GLOBAL = _cupynumeric.CUPYNUMERIC_SCAN_GLOBAL + SCAN_LOCAL = _cupynumeric.CUPYNUMERIC_SCAN_LOCAL + SEARCHSORTED = _cupynumeric.CUPYNUMERIC_SEARCHSORTED + SELECT = _cupynumeric.CUPYNUMERIC_SELECT + SOLVE = _cupynumeric.CUPYNUMERIC_SOLVE + SORT = _cupynumeric.CUPYNUMERIC_SORT + SVD = _cupynumeric.CUPYNUMERIC_SVD + SYRK = _cupynumeric.CUPYNUMERIC_SYRK + TILE = _cupynumeric.CUPYNUMERIC_TILE + TRANSPOSE_COPY_2D = _cupynumeric.CUPYNUMERIC_TRANSPOSE_COPY_2D + TRILU = _cupynumeric.CUPYNUMERIC_TRILU + TRSM = _cupynumeric.CUPYNUMERIC_TRSM + UNARY_OP = _cupynumeric.CUPYNUMERIC_UNARY_OP + UNARY_RED = _cupynumeric.CUPYNUMERIC_UNARY_RED + UNIQUE = _cupynumeric.CUPYNUMERIC_UNIQUE + UNIQUE_REDUCE = _cupynumeric.CUPYNUMERIC_UNIQUE_REDUCE + UNLOAD_CUDALIBS = _cupynumeric.CUPYNUMERIC_UNLOAD_CUDALIBS + UNPACKBITS = _cupynumeric.CUPYNUMERIC_UNPACKBITS + WHERE = _cupynumeric.CUPYNUMERIC_WHERE + WINDOW = _cupynumeric.CUPYNUMERIC_WINDOW + WRAP = _cupynumeric.CUPYNUMERIC_WRAP + WRITE = _cupynumeric.CUPYNUMERIC_WRITE + ZIP = _cupynumeric.CUPYNUMERIC_ZIP + + +# Match these to CuPyNumericUnaryOpCode in cupynumeric_c.h +@unique +class UnaryOpCode(IntEnum): + ABSOLUTE = _cupynumeric.CUPYNUMERIC_UOP_ABSOLUTE + ANGLE = _cupynumeric.CUPYNUMERIC_UOP_ANGLE + ARCCOS = _cupynumeric.CUPYNUMERIC_UOP_ARCCOS + ARCCOSH = _cupynumeric.CUPYNUMERIC_UOP_ARCCOSH + ARCSIN = _cupynumeric.CUPYNUMERIC_UOP_ARCSIN + ARCSINH = _cupynumeric.CUPYNUMERIC_UOP_ARCSINH + ARCTAN = _cupynumeric.CUPYNUMERIC_UOP_ARCTAN + ARCTANH = _cupynumeric.CUPYNUMERIC_UOP_ARCTANH + CBRT = _cupynumeric.CUPYNUMERIC_UOP_CBRT + CEIL = _cupynumeric.CUPYNUMERIC_UOP_CEIL + CLIP = _cupynumeric.CUPYNUMERIC_UOP_CLIP + CONJ = _cupynumeric.CUPYNUMERIC_UOP_CONJ + COPY = _cupynumeric.CUPYNUMERIC_UOP_COPY + COS = _cupynumeric.CUPYNUMERIC_UOP_COS + COSH = _cupynumeric.CUPYNUMERIC_UOP_COSH + DEG2RAD = _cupynumeric.CUPYNUMERIC_UOP_DEG2RAD + EXP = _cupynumeric.CUPYNUMERIC_UOP_EXP + EXP2 = _cupynumeric.CUPYNUMERIC_UOP_EXP2 + EXPM1 = _cupynumeric.CUPYNUMERIC_UOP_EXPM1 + FLOOR = _cupynumeric.CUPYNUMERIC_UOP_FLOOR + FREXP = _cupynumeric.CUPYNUMERIC_UOP_FREXP + GETARG = _cupynumeric.CUPYNUMERIC_UOP_GETARG + IMAG = _cupynumeric.CUPYNUMERIC_UOP_IMAG + INVERT = _cupynumeric.CUPYNUMERIC_UOP_INVERT + ISFINITE = _cupynumeric.CUPYNUMERIC_UOP_ISFINITE + ISINF = _cupynumeric.CUPYNUMERIC_UOP_ISINF + ISNAN = _cupynumeric.CUPYNUMERIC_UOP_ISNAN + LOG = _cupynumeric.CUPYNUMERIC_UOP_LOG + LOG10 = _cupynumeric.CUPYNUMERIC_UOP_LOG10 + LOG1P = _cupynumeric.CUPYNUMERIC_UOP_LOG1P + LOG2 = _cupynumeric.CUPYNUMERIC_UOP_LOG2 + LOGICAL_NOT = _cupynumeric.CUPYNUMERIC_UOP_LOGICAL_NOT + MODF = _cupynumeric.CUPYNUMERIC_UOP_MODF + NEGATIVE = _cupynumeric.CUPYNUMERIC_UOP_NEGATIVE + POSITIVE = _cupynumeric.CUPYNUMERIC_UOP_POSITIVE + RAD2DEG = _cupynumeric.CUPYNUMERIC_UOP_RAD2DEG + REAL = _cupynumeric.CUPYNUMERIC_UOP_REAL + RECIPROCAL = _cupynumeric.CUPYNUMERIC_UOP_RECIPROCAL + RINT = _cupynumeric.CUPYNUMERIC_UOP_RINT + ROUND = _cupynumeric.CUPYNUMERIC_UOP_ROUND + SIGN = _cupynumeric.CUPYNUMERIC_UOP_SIGN + SIGNBIT = _cupynumeric.CUPYNUMERIC_UOP_SIGNBIT + SIN = _cupynumeric.CUPYNUMERIC_UOP_SIN + SINH = _cupynumeric.CUPYNUMERIC_UOP_SINH + SQRT = _cupynumeric.CUPYNUMERIC_UOP_SQRT + SQUARE = _cupynumeric.CUPYNUMERIC_UOP_SQUARE + TAN = _cupynumeric.CUPYNUMERIC_UOP_TAN + TANH = _cupynumeric.CUPYNUMERIC_UOP_TANH + TRUNC = _cupynumeric.CUPYNUMERIC_UOP_TRUNC + + +# Match these to CuPyNumericUnaryRedCode in cupynumeric_c.h +@unique +class UnaryRedCode(IntEnum): + ALL = _cupynumeric.CUPYNUMERIC_RED_ALL + ANY = _cupynumeric.CUPYNUMERIC_RED_ANY + ARGMAX = _cupynumeric.CUPYNUMERIC_RED_ARGMAX + ARGMIN = _cupynumeric.CUPYNUMERIC_RED_ARGMIN + CONTAINS = _cupynumeric.CUPYNUMERIC_RED_CONTAINS + COUNT_NONZERO = _cupynumeric.CUPYNUMERIC_RED_COUNT_NONZERO + MAX = _cupynumeric.CUPYNUMERIC_RED_MAX + MIN = _cupynumeric.CUPYNUMERIC_RED_MIN + NANARGMAX = _cupynumeric.CUPYNUMERIC_RED_NANARGMAX + NANARGMIN = _cupynumeric.CUPYNUMERIC_RED_NANARGMIN + NANMAX = _cupynumeric.CUPYNUMERIC_RED_NANMAX + NANMIN = _cupynumeric.CUPYNUMERIC_RED_NANMIN + NANPROD = _cupynumeric.CUPYNUMERIC_RED_NANPROD + NANSUM = _cupynumeric.CUPYNUMERIC_RED_NANSUM + PROD = _cupynumeric.CUPYNUMERIC_RED_PROD + SUM = _cupynumeric.CUPYNUMERIC_RED_SUM + SUM_SQUARES = _cupynumeric.CUPYNUMERIC_RED_SUM_SQUARES + VARIANCE = _cupynumeric.CUPYNUMERIC_RED_VARIANCE + + +# Match these to CuPyNumericBinaryOpCode in cupynumeric_c.h +@unique +class BinaryOpCode(IntEnum): + ADD = _cupynumeric.CUPYNUMERIC_BINOP_ADD + ARCTAN2 = _cupynumeric.CUPYNUMERIC_BINOP_ARCTAN2 + BITWISE_AND = _cupynumeric.CUPYNUMERIC_BINOP_BITWISE_AND + BITWISE_OR = _cupynumeric.CUPYNUMERIC_BINOP_BITWISE_OR + BITWISE_XOR = _cupynumeric.CUPYNUMERIC_BINOP_BITWISE_XOR + COPYSIGN = _cupynumeric.CUPYNUMERIC_BINOP_COPYSIGN + DIVIDE = _cupynumeric.CUPYNUMERIC_BINOP_DIVIDE + EQUAL = _cupynumeric.CUPYNUMERIC_BINOP_EQUAL + FLOAT_POWER = _cupynumeric.CUPYNUMERIC_BINOP_FLOAT_POWER + FLOOR_DIVIDE = _cupynumeric.CUPYNUMERIC_BINOP_FLOOR_DIVIDE + FMOD = _cupynumeric.CUPYNUMERIC_BINOP_FMOD + GCD = _cupynumeric.CUPYNUMERIC_BINOP_GCD + GREATER = _cupynumeric.CUPYNUMERIC_BINOP_GREATER + GREATER_EQUAL = _cupynumeric.CUPYNUMERIC_BINOP_GREATER_EQUAL + HYPOT = _cupynumeric.CUPYNUMERIC_BINOP_HYPOT + ISCLOSE = _cupynumeric.CUPYNUMERIC_BINOP_ISCLOSE + LCM = _cupynumeric.CUPYNUMERIC_BINOP_LCM + LDEXP = _cupynumeric.CUPYNUMERIC_BINOP_LDEXP + LEFT_SHIFT = _cupynumeric.CUPYNUMERIC_BINOP_LEFT_SHIFT + LESS = _cupynumeric.CUPYNUMERIC_BINOP_LESS + LESS_EQUAL = _cupynumeric.CUPYNUMERIC_BINOP_LESS_EQUAL + LOGADDEXP = _cupynumeric.CUPYNUMERIC_BINOP_LOGADDEXP + LOGADDEXP2 = _cupynumeric.CUPYNUMERIC_BINOP_LOGADDEXP2 + LOGICAL_AND = _cupynumeric.CUPYNUMERIC_BINOP_LOGICAL_AND + LOGICAL_OR = _cupynumeric.CUPYNUMERIC_BINOP_LOGICAL_OR + LOGICAL_XOR = _cupynumeric.CUPYNUMERIC_BINOP_LOGICAL_XOR + MAXIMUM = _cupynumeric.CUPYNUMERIC_BINOP_MAXIMUM + MINIMUM = _cupynumeric.CUPYNUMERIC_BINOP_MINIMUM + MOD = _cupynumeric.CUPYNUMERIC_BINOP_MOD + MULTIPLY = _cupynumeric.CUPYNUMERIC_BINOP_MULTIPLY + NEXTAFTER = _cupynumeric.CUPYNUMERIC_BINOP_NEXTAFTER + NOT_EQUAL = _cupynumeric.CUPYNUMERIC_BINOP_NOT_EQUAL + POWER = _cupynumeric.CUPYNUMERIC_BINOP_POWER + RIGHT_SHIFT = _cupynumeric.CUPYNUMERIC_BINOP_RIGHT_SHIFT + SUBTRACT = _cupynumeric.CUPYNUMERIC_BINOP_SUBTRACT + + +@unique +class WindowOpCode(IntEnum): + BARLETT = _cupynumeric.CUPYNUMERIC_WINDOW_BARLETT + BLACKMAN = _cupynumeric.CUPYNUMERIC_WINDOW_BLACKMAN + HAMMING = _cupynumeric.CUPYNUMERIC_WINDOW_HAMMING + HANNING = _cupynumeric.CUPYNUMERIC_WINDOW_HANNING + KAISER = _cupynumeric.CUPYNUMERIC_WINDOW_KAISER + + +# Match these to RandGenCode in rand_util.h +@unique +class RandGenCode(IntEnum): + UNIFORM = 1 + NORMAL = 2 + INTEGER = 3 + + +# Match these to CuPyNumericScanCode in cupynumeric_c.h +@unique +class ScanCode(IntEnum): + PROD = _cupynumeric.CUPYNUMERIC_SCAN_PROD + SUM = _cupynumeric.CUPYNUMERIC_SCAN_SUM + + +# Match these to CuPyNumericConvertCode in cupynumeric_c.h +@unique +class ConvertCode(IntEnum): + NOOP = _cupynumeric.CUPYNUMERIC_CONVERT_NAN_NOOP + PROD = _cupynumeric.CUPYNUMERIC_CONVERT_NAN_PROD + SUM = _cupynumeric.CUPYNUMERIC_CONVERT_NAN_SUM + + +# Match these to BitGeneratorOperation in cupynumeric_c.h +@unique +class BitGeneratorOperation(IntEnum): + CREATE = _cupynumeric.CUPYNUMERIC_BITGENOP_CREATE + DESTROY = _cupynumeric.CUPYNUMERIC_BITGENOP_DESTROY + RAND_RAW = _cupynumeric.CUPYNUMERIC_BITGENOP_RAND_RAW + DISTRIBUTION = _cupynumeric.CUPYNUMERIC_BITGENOP_DISTRIBUTION + + +# Match these to BitGeneratorType in cupynumeric_c.h +@unique +class BitGeneratorType(IntEnum): + DEFAULT = _cupynumeric.CUPYNUMERIC_BITGENTYPE_DEFAULT + XORWOW = _cupynumeric.CUPYNUMERIC_BITGENTYPE_XORWOW + MRG32K3A = _cupynumeric.CUPYNUMERIC_BITGENTYPE_MRG32K3A + MTGP32 = _cupynumeric.CUPYNUMERIC_BITGENTYPE_MTGP32 + MT19937 = _cupynumeric.CUPYNUMERIC_BITGENTYPE_MT19937 + PHILOX4_32_10 = _cupynumeric.CUPYNUMERIC_BITGENTYPE_PHILOX4_32_10 + + +# Match these to BitGeneratorDistribution in cupynumeric_c.h +@unique +class BitGeneratorDistribution(IntEnum): + INTEGERS_16 = _cupynumeric.CUPYNUMERIC_BITGENDIST_INTEGERS_16 + INTEGERS_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_INTEGERS_32 + INTEGERS_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_INTEGERS_64 + UNIFORM_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_UNIFORM_32 + UNIFORM_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_UNIFORM_64 + LOGNORMAL_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LOGNORMAL_32 + LOGNORMAL_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LOGNORMAL_64 + NORMAL_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_NORMAL_32 + NORMAL_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_NORMAL_64 + POISSON = _cupynumeric.CUPYNUMERIC_BITGENDIST_POISSON + EXPONENTIAL_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_EXPONENTIAL_32 + EXPONENTIAL_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_EXPONENTIAL_64 + GUMBEL_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_GUMBEL_32 + GUMBEL_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_GUMBEL_64 + LAPLACE_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LAPLACE_32 + LAPLACE_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LAPLACE_64 + LOGISTIC_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LOGISTIC_32 + LOGISTIC_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_LOGISTIC_64 + PARETO_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_PARETO_32 + PARETO_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_PARETO_64 + POWER_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_POWER_32 + POWER_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_POWER_64 + RAYLEIGH_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_RAYLEIGH_32 + RAYLEIGH_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_RAYLEIGH_64 + CAUCHY_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_CAUCHY_32 + CAUCHY_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_CAUCHY_64 + TRIANGULAR_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_TRIANGULAR_32 + TRIANGULAR_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_TRIANGULAR_64 + WEIBULL_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_WEIBULL_32 + WEIBULL_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_WEIBULL_64 + BYTES = _cupynumeric.CUPYNUMERIC_BITGENDIST_BYTES + BETA_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_BETA_32 + BETA_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_BETA_64 + F_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_F_32 + F_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_F_64 + LOGSERIES = _cupynumeric.CUPYNUMERIC_BITGENDIST_LOGSERIES + NONCENTRAL_F_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_NONCENTRAL_F_32 + NONCENTRAL_F_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_NONCENTRAL_F_64 + CHISQUARE_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_CHISQUARE_32 + CHISQUARE_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_CHISQUARE_64 + GAMMA_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_GAMMA_32 + GAMMA_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_GAMMA_64 + STANDARD_T_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_STANDARD_T_32 + STANDARD_T_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_STANDARD_T_64 + HYPERGEOMETRIC = _cupynumeric.CUPYNUMERIC_BITGENDIST_HYPERGEOMETRIC + VONMISES_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_VONMISES_32 + VONMISES_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_VONMISES_64 + ZIPF = _cupynumeric.CUPYNUMERIC_BITGENDIST_ZIPF + GEOMETRIC = _cupynumeric.CUPYNUMERIC_BITGENDIST_GEOMETRIC + WALD_32 = _cupynumeric.CUPYNUMERIC_BITGENDIST_WALD_32 + WALD_64 = _cupynumeric.CUPYNUMERIC_BITGENDIST_WALD_64 + BINOMIAL = _cupynumeric.CUPYNUMERIC_BITGENDIST_BINOMIAL + NEGATIVE_BINOMIAL = _cupynumeric.CUPYNUMERIC_BITGENDIST_NEGATIVE_BINOMIAL + + +# Match these to CuPyNumericConvolveMethod in cupynumeric_c.h +@unique +class ConvolveMethod(IntEnum): + AUTO = _cupynumeric.CUPYNUMERIC_CONVOLVE_AUTO + DIRECT = _cupynumeric.CUPYNUMERIC_CONVOLVE_DIRECT + FFT = _cupynumeric.CUPYNUMERIC_CONVOLVE_FFT + + +@unique +class TransferType(IntEnum): + DONATE = 0 + MAKE_COPY = 1 + SHARE = 2 + + +# Match these to fftType in fft_util.h +class FFTType: + def __init__( + self, + name: str, + type_id: int, + input_dtype: npt.DTypeLike, + output_dtype: npt.DTypeLike, + single_precision: bool, + complex_type: FFTType | None = None, + ) -> None: + self._name = name + self._type_id = type_id + self._complex_type = self if complex_type is None else complex_type + self._input_dtype = input_dtype + self._output_dtype = output_dtype + self._single_precision = single_precision + + def __str__(self) -> str: + return self._name + + def __repr__(self) -> str: + return str(self) + + @property + def type_id(self) -> int: + return self._type_id + + @property + def complex(self) -> FFTType: + return self._complex_type + + @property + def input_dtype(self) -> npt.DTypeLike: + return self._input_dtype + + @property + def output_dtype(self) -> npt.DTypeLike: + return self._output_dtype + + @property + def is_single_precision(self) -> bool: + return self._single_precision + + +FFT_C2C = FFTType( + "C2C", + _cupynumeric.CUPYNUMERIC_FFT_C2C, + np.complex64, + np.complex64, + True, +) + +FFT_Z2Z = FFTType( + "Z2Z", + _cupynumeric.CUPYNUMERIC_FFT_Z2Z, + np.complex128, + np.complex128, + False, +) + +FFT_R2C = FFTType( + "R2C", + _cupynumeric.CUPYNUMERIC_FFT_R2C, + np.float32, + np.complex64, + True, + FFT_C2C, +) + +FFT_C2R = FFTType( + "C2R", + _cupynumeric.CUPYNUMERIC_FFT_C2R, + np.complex64, + np.float32, + True, + FFT_C2C, +) + +FFT_D2Z = FFTType( + "D2Z", + _cupynumeric.CUPYNUMERIC_FFT_D2Z, + np.float64, + np.complex128, + False, + FFT_Z2Z, +) + +FFT_Z2D = FFTType( + "Z2D", + _cupynumeric.CUPYNUMERIC_FFT_Z2D, + np.complex128, + np.float64, + False, + FFT_Z2Z, +) + + +class FFTCode: + @staticmethod + def real_to_complex_code(dtype: npt.DTypeLike) -> FFTType: + if dtype == np.float64: + return FFT_D2Z + elif dtype == np.float32: + return FFT_R2C + else: + raise TypeError( + ( + "Data type for FFT not supported " + "(supported types are float32 and float64)" + ) + ) + + @staticmethod + def complex_to_real_code(dtype: npt.DTypeLike) -> FFTType: + if dtype == np.complex128: + return FFT_Z2D + elif dtype == np.complex64: + return FFT_C2R + else: + raise TypeError( + ( + "Data type for FFT not supported " + "(supported types are complex64 and complex128)" + ) + ) + + +@unique +class FFTDirection(IntEnum): + FORWARD = _cupynumeric.CUPYNUMERIC_FFT_FORWARD + INVERSE = _cupynumeric.CUPYNUMERIC_FFT_INVERSE + + +# Match these to CuPyNumericBitorder in cupynumeric_c.h +@unique +class Bitorder(IntEnum): + BIG = _cupynumeric.CUPYNUMERIC_BITORDER_BIG + LITTLE = _cupynumeric.CUPYNUMERIC_BITORDER_LITTLE + + +@unique +class FFTNormalization(IntEnum): + FORWARD = 1 + INVERSE = 2 + ORTHOGONAL = 3 + + @staticmethod + def from_string(in_string: str) -> FFTNormalization | None: + if in_string == "forward": + return FFTNormalization.FORWARD + elif in_string == "ortho": + return FFTNormalization.ORTHOGONAL + elif in_string == "backward" or in_string is None: + return FFTNormalization.INVERSE + else: + return None + + @staticmethod + def reverse(in_string: str | None) -> str: + if in_string == "forward": + return "backward" + elif in_string == "backward" or in_string is None: + return "forward" + else: + return in_string diff --git a/cunumeric/fft/__init__.py b/cupynumeric/fft/__init__.py similarity index 80% rename from cunumeric/fft/__init__.py rename to cupynumeric/fft/__init__.py index d0d3eb28d..0bc511d57 100644 --- a/cunumeric/fft/__init__.py +++ b/cupynumeric/fft/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ import numpy.fft as _npfft -from cunumeric.array import maybe_convert_to_np_ndarray -from cunumeric.fft.fft import * -from cunumeric.coverage import clone_module +from .._array.util import maybe_convert_to_np_ndarray +from .._utils.coverage import clone_module +from .fft import * clone_module(_npfft, globals(), maybe_convert_to_np_ndarray) diff --git a/cunumeric/fft/fft.py b/cupynumeric/fft/fft.py similarity index 91% rename from cunumeric/fft/fft.py rename to cupynumeric/fft/fft.py index b10ce8c98..7576f3dd4 100644 --- a/cunumeric/fft/fft.py +++ b/cupynumeric/fft/fft.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,21 +14,22 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Sequence, Union +from typing import TYPE_CHECKING, Sequence import numpy as np +from .._array.util import add_boilerplate +from .._module.array_rearrange import roll from ..config import FFT_C2C, FFT_Z2Z, FFTCode, FFTDirection, FFTNormalization -from ..module import add_boilerplate if TYPE_CHECKING: - from ..array import ndarray + from .._array.array import ndarray def _sanitize_user_axes( a: ndarray, - s: Union[Sequence[int], None], - axes: Union[Sequence[int], None], + s: Sequence[int] | None, + axes: Sequence[int] | None, is_c2r: bool = False, ) -> tuple[list[int], Sequence[int]]: if s is None: @@ -58,9 +59,9 @@ def _operate_by_axes(a: ndarray, axes: Sequence[int]) -> bool: @add_boilerplate("a") def fft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the one-dimensional discrete Fourier Transform. @@ -103,7 +104,7 @@ def fft( numpy.fft.fft Availability - -------- + ------------ Multiple GPUs """ s = (n,) if n is not None else None @@ -114,9 +115,9 @@ def fft( @add_boilerplate("a") def fft2( a: ndarray, - s: Union[Sequence[int], None] = None, + s: Sequence[int] | None = None, axes: Sequence[int] = (-2, -1), - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the 2-dimensional discrete Fourier Transform. @@ -172,9 +173,9 @@ def fft2( @add_boilerplate("a") def fftn( a: ndarray, - s: Union[Sequence[int], None] = None, - axes: Union[Sequence[int], None] = None, - norm: Union[str, None] = None, + s: Sequence[int] | None = None, + axes: Sequence[int] | None = None, + norm: str | None = None, ) -> ndarray: """ Compute the N-dimensional discrete Fourier Transform. @@ -248,9 +249,9 @@ def fftn( @add_boilerplate("a") def ifft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the one-dimensional inverse discrete Fourier Transform. @@ -319,9 +320,9 @@ def ifft( @add_boilerplate("a") def ifft2( a: ndarray, - s: Union[Sequence[int], None] = None, + s: Sequence[int] | None = None, axes: Sequence[int] = (-2, -1), - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the 2-dimensional inverse discrete Fourier Transform. @@ -384,9 +385,9 @@ def ifft2( @add_boilerplate("a") def ifftn( a: ndarray, - s: Union[Sequence[int], None] = None, - axes: Union[Sequence[int], None] = None, - norm: Union[str, None] = None, + s: Sequence[int] | None = None, + axes: Sequence[int] | None = None, + norm: str | None = None, ) -> ndarray: """ Compute the N-dimensional inverse discrete Fourier Transform. @@ -470,9 +471,9 @@ def ifftn( @add_boilerplate("a") def rfft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the one-dimensional discrete Fourier Transform for real input. @@ -528,9 +529,9 @@ def rfft( @add_boilerplate("a") def rfft2( a: ndarray, - s: Union[Sequence[int], None] = None, + s: Sequence[int] | None = None, axes: Sequence[int] = (-2, -1), - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the 2-dimensional FFT of a real array. @@ -573,9 +574,9 @@ def rfft2( @add_boilerplate("a") def rfftn( a: ndarray, - s: Union[Sequence[int], None] = None, - axes: Union[Sequence[int], None] = None, - norm: Union[str, None] = None, + s: Sequence[int] | None = None, + axes: Sequence[int] | None = None, + norm: str | None = None, ) -> ndarray: """ Compute the N-dimensional discrete Fourier Transform for real input. @@ -670,9 +671,9 @@ def rfftn( @add_boilerplate("a") def irfft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Computes the inverse of `rfft`. @@ -738,9 +739,9 @@ def irfft( @add_boilerplate("a") def irfft2( a: ndarray, - s: Union[Sequence[int], None] = None, + s: Sequence[int] | None = None, axes: Sequence[int] = (-2, -1), - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Computes the inverse of `rfft2`. @@ -785,9 +786,9 @@ def irfft2( @add_boilerplate("a") def irfftn( a: ndarray, - s: Union[Sequence[int], None] = None, - axes: Union[Sequence[int], None] = None, - norm: Union[str, None] = None, + s: Sequence[int] | None = None, + axes: Sequence[int] | None = None, + norm: str | None = None, ) -> ndarray: """ Computes the inverse of `rfftn`. @@ -895,9 +896,9 @@ def irfftn( @add_boilerplate("a") def hfft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the FFT of a signal that has Hermitian symmetry, i.e., a real @@ -960,9 +961,9 @@ def hfft( @add_boilerplate("a") def ihfft( a: ndarray, - n: Union[int, None] = None, + n: int | None = None, axis: int = -1, - norm: Union[str, None] = None, + norm: str | None = None, ) -> ndarray: """ Compute the inverse FFT of a signal that has Hermitian symmetry. @@ -1012,3 +1013,80 @@ def ihfft( return rfftn( a=a, s=s, axes=computed_axis, norm=FFTNormalization.reverse(norm) ).conjugate() + + +integer_types = (int, np.integer) + + +@add_boilerplate("x") +def fftshift(x: ndarray, axes: int | tuple[int, ...] | None = None) -> ndarray: + """ + Shift the zero-frequency component to the center of the spectrum. + + This function swaps half-spaces for all axes listed (defaults to all). + Note that ``y[0]`` is the Nyquist component only if ``len(x)`` is even. + + Parameters + ---------- + x : array_like + Input array. + + axes : int or shape tuple, optional + Axes over which to shift. Default is None, which shifts all axes. + + Returns + ------- + y : ndarray + The shifted array. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + shift: int | tuple[int, ...] + if axes is None: + axes = tuple(range(x.ndim)) + shift = tuple(dim // 2 for dim in x.shape) + elif isinstance(axes, integer_types): + shift = x.shape[axes] // 2 + else: + shift = tuple(x.shape[ax] // 2 for ax in axes) + + return roll(x, shift, axes) + + +@add_boilerplate("x") +def ifftshift( + x: ndarray, axes: int | tuple[int, ...] | None = None +) -> ndarray: + """ + The inverse of `fftshift`. Although identical for even-length `x`, the + functions differ by one sample for odd-length `x`. + + Parameters + ---------- + x : array_like + Input array. + + axes : int or shape tuple, optional + Axes over which to calculate. Defaults to None, which shifts all axes. + + Returns + ------- + y : ndarray + The shifted array. + + Availability + -------- + Multiple GPUs, Multiple CPUs + """ + shift: int | tuple[int, ...] + if axes is None: + axes = tuple(range(x.ndim)) + shift = tuple(-(dim // 2) for dim in x.shape) + elif isinstance(axes, integer_types): + shift = -(x.shape[axes] // 2) + else: + shift = tuple(-(x.shape[ax] // 2) for ax in axes) + + return roll(x, shift, axes) diff --git a/cupynumeric/install_info.py.in b/cupynumeric/install_info.py.in new file mode 100644 index 000000000..c582492df --- /dev/null +++ b/cupynumeric/install_info.py.in @@ -0,0 +1,47 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# IMPORTANT: +# * install_info.py is a generated file and should not be modified by hand + +def get_libpath(): + import os, sys, platform + join = os.path.join + exists = os.path.exists + dirname = os.path.dirname + cn_path = dirname(dirname(__file__)) + so_ext = { + "": "", + "Java": ".jar", + "Linux": ".so", + "Darwin": ".dylib", + "Windows": ".dll" + }[platform.system()] + + def find_libcupynumeric(libdir): + if exists(join(libdir, f"libcupynumeric{so_ext}")): + return libdir + return None + + return ( + find_libcupynumeric(join(cn_path, "build", "lib")) or + find_libcupynumeric(join(dirname(dirname(dirname(cn_path))), "lib")) or + find_libcupynumeric(join(dirname(dirname(sys.executable)), "lib")) or + "" + ) + + +libpath: str = get_libpath() +header: str = """@header@""" diff --git a/cunumeric/linalg/__init__.py b/cupynumeric/linalg/__init__.py similarity index 77% rename from cunumeric/linalg/__init__.py rename to cupynumeric/linalg/__init__.py index 6904bf72c..9ba64b37d 100644 --- a/cunumeric/linalg/__init__.py +++ b/cupynumeric/linalg/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,10 +16,9 @@ import numpy.linalg as _nplinalg -from cunumeric.array import maybe_convert_to_np_ndarray -from cunumeric.linalg.linalg import * -from cunumeric.linalg.exception import * -from cunumeric.coverage import clone_module +from .._array.util import maybe_convert_to_np_ndarray +from .._utils.coverage import clone_module +from .linalg import * clone_module(_nplinalg, globals(), maybe_convert_to_np_ndarray) diff --git a/cupynumeric/linalg/_cholesky.py b/cupynumeric/linalg/_cholesky.py new file mode 100644 index 000000000..a99ae6811 --- /dev/null +++ b/cupynumeric/linalg/_cholesky.py @@ -0,0 +1,305 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from legate.core import ( + broadcast, + constant, + dimension, + get_legate_runtime, + types as ty, +) +from legate.settings import settings + +from ..config import CuPyNumericOpCode +from ..runtime import runtime +from ._exception import LinAlgError + +legate_runtime = get_legate_runtime() + +if TYPE_CHECKING: + from legate.core import Library, LogicalStore, LogicalStorePartition + + from .._thunk.deferred import DeferredArray + from ..runtime import Runtime + + +def transpose_copy_single( + library: Library, input: LogicalStore, output: LogicalStore +) -> None: + task = legate_runtime.create_auto_task( + library, CuPyNumericOpCode.TRANSPOSE_COPY_2D + ) + p_out = task.add_output(output) + p_in = task.add_input(input) + # Output has the same shape as input, but is mapped + # to a column major instance + + task.add_constraint(broadcast(p_out)) + task.add_constraint(broadcast(p_in)) + + task.execute() + + +def transpose_copy( + library: Library, + launch_domain: tuple[int, ...], + p_input: LogicalStorePartition, + p_output: LogicalStorePartition, +) -> None: + task = legate_runtime.create_manual_task( + library, + CuPyNumericOpCode.TRANSPOSE_COPY_2D, + launch_domain, + ) + task.add_output(p_output) + task.add_input(p_input) + # Output has the same shape as input, but is mapped + # to a column major instance + + task.execute() + + +def potrf_single(library: Library, output: LogicalStore) -> None: + task = legate_runtime.create_auto_task(library, CuPyNumericOpCode.POTRF) + task.throws_exception(LinAlgError) + task.add_output(output) + task.add_input(output) + task.execute() + + +def mp_potrf( + library: Library, + n: int, + nb: int, + input: LogicalStore, + output: LogicalStore, +) -> None: + task = legate_runtime.create_auto_task(library, CuPyNumericOpCode.MP_POTRF) + task.throws_exception(LinAlgError) + task.add_input(input) + task.add_output(output) + task.add_alignment(output, input) + task.add_scalar_arg(n, ty.int64) + task.add_scalar_arg(nb, ty.int64) + task.add_nccl_communicator() # for repartitioning + task.add_cal_communicator() + task.execute() + + +def potrf(library: Library, p_output: LogicalStorePartition, i: int) -> None: + task = legate_runtime.create_manual_task( + library, CuPyNumericOpCode.POTRF, (i + 1, i + 1), lower_bounds=(i, i) + ) + task.throws_exception(LinAlgError) + task.add_output(p_output) + task.add_input(p_output) + task.execute() + + +def trsm( + library: Library, p_output: LogicalStorePartition, i: int, lo: int, hi: int +) -> None: + if lo >= hi: + return + + rhs = p_output.get_child_store(i, i) + lhs = p_output + + task = legate_runtime.create_manual_task( + library, CuPyNumericOpCode.TRSM, (hi, i + 1), lower_bounds=(lo, i) + ) + task.add_output(lhs) + task.add_input(rhs) + task.add_input(lhs) + task.execute() + + +def syrk( + library: Library, p_output: LogicalStorePartition, k: int, i: int +) -> None: + rhs = p_output.get_child_store(k, i) + lhs = p_output + + task = legate_runtime.create_manual_task( + library, CuPyNumericOpCode.SYRK, (k + 1, k + 1), lower_bounds=(k, k) + ) + task.add_output(lhs) + task.add_input(rhs) + task.add_input(lhs) + task.execute() + + +def gemm( + library: Library, + p_output: LogicalStorePartition, + k: int, + i: int, + lo: int, + hi: int, +) -> None: + if lo >= hi: + return + + rhs2 = p_output.get_child_store(k, i) + lhs = p_output + rhs1 = p_output + + task = legate_runtime.create_manual_task( + library, CuPyNumericOpCode.GEMM, (hi, k + 1), lower_bounds=(lo, k) + ) + task.add_output(lhs) + task.add_input(rhs1, (dimension(0), constant(i))) + task.add_input(rhs2) + task.add_input(lhs) + task.execute() + + +MIN_CHOLESKY_TILE_SIZE = 2 if settings.test() else 2048 +MIN_CHOLESKY_MATRIX_SIZE = 4 if settings.test() else 8192 + + +# TODO: We need a better cost model +def choose_color_shape( + runtime: Runtime, shape: tuple[int, ...] +) -> tuple[int, ...]: + extent = shape[0] + + # If there's only one processor or the matrix is too small, + # don't even bother to partition it at all + if runtime.num_procs == 1 or extent <= MIN_CHOLESKY_MATRIX_SIZE: + return (1, 1) + + # If the matrix is big enough to warrant partitioning, + # pick the granularity that the tile size is greater than a threshold + num_tiles = runtime.num_procs + max_num_tiles = runtime.num_procs * 4 + while ( + (extent + num_tiles - 1) // num_tiles > MIN_CHOLESKY_TILE_SIZE + and num_tiles * 2 <= max_num_tiles + ): + num_tiles *= 2 + + return (num_tiles, num_tiles) + + +def tril_single(library: Library, output: LogicalStore) -> None: + task = legate_runtime.create_auto_task(library, CuPyNumericOpCode.TRILU) + task.add_output(output) + task.add_input(output) + task.add_scalar_arg(True, ty.bool_) + task.add_scalar_arg(0, ty.int32) + # Add a fake task argument to indicate that this is for Cholesky + task.add_scalar_arg(True, ty.bool_) + + task.execute() + + +def tril(library: Library, p_output: LogicalStorePartition, n: int) -> None: + task = legate_runtime.create_manual_task( + library, CuPyNumericOpCode.TRILU, (n, n) + ) + + task.add_output(p_output) + task.add_input(p_output) + task.add_scalar_arg(True, ty.bool_) + task.add_scalar_arg(0, ty.int32) + # Add a fake task argument to indicate that this is for Cholesky + task.add_scalar_arg(True, ty.bool_) + + task.execute() + + +def _rounding_divide( + lhs: tuple[int, ...], rhs: tuple[int, ...] +) -> tuple[int, ...]: + return tuple((lh + rh - 1) // rh for (lh, rh) in zip(lhs, rhs)) + + +def _batched_cholesky( + library: Library, output: DeferredArray, input: DeferredArray +) -> None: + # the only feasible implementation for right now is that + # each cholesky submatrix fits on a single proc. We will have + # wildly varying memory available depending on the system. + # Just use a fixed cutoff to provide some sensible warning. + # TODO: find a better way to inform the user dims are too big + task = legate_runtime.create_auto_task( + library, CuPyNumericOpCode.BATCHED_CHOLESKY + ) + task.add_input(input.base) + task.add_output(output.base) + ndim = input.base.ndim + task.add_broadcast(input.base, (ndim - 2, ndim - 1)) + task.add_broadcast(output.base, (ndim - 2, ndim - 1)) + task.add_alignment(input.base, output.base) + task.throws_exception(LinAlgError) + task.execute() + + +def cholesky_deferred(output: DeferredArray, input: DeferredArray) -> None: + library = runtime.library + if len(input.base.shape) > 2: + size = input.base.shape[-1] + # Choose 32768 as dimension cutoff for warning + # so that for float64 anything larger than + # 8 GiB produces a warning + if size > 32768: + runtime.warn( + "batched cholesky is only valid" + " when the square submatrices fit" + f" on a single proc, n > {size} may be too large", + category=UserWarning, + ) + return _batched_cholesky(library, output, input) + + if runtime.num_procs == 1: + transpose_copy_single(library, input.base, output.base) + potrf_single(library, output.base) + tril_single(library, output.base) + return + + shape = tuple(output.base.shape) + tile_shape: tuple[int, ...] + if ( + runtime.has_cusolvermp + and runtime.num_gpus > 1 + and shape[0] >= MIN_CHOLESKY_MATRIX_SIZE + ): + mp_potrf( + library, shape[0], MIN_CHOLESKY_TILE_SIZE, input.base, output.base + ) + + tril_single(library, output.base) + else: + initial_color_shape = choose_color_shape(runtime, shape) + tile_shape = _rounding_divide(shape, initial_color_shape) + color_shape = _rounding_divide(shape, tile_shape) + n = color_shape[0] + + p_input = input.base.partition_by_tiling(tile_shape) + p_output = output.base.partition_by_tiling(tile_shape) + transpose_copy(library, color_shape, p_input, p_output) + + for i in range(n): + potrf(library, p_output, i) + trsm(library, p_output, i, i + 1, n) + for k in range(i + 1, n): + syrk(library, p_output, k, i) + gemm(library, p_output, k, i, k + 1, n) + + tril(library, p_output, n) diff --git a/cunumeric/linalg/exception.py b/cupynumeric/linalg/_exception.py similarity index 94% rename from cunumeric/linalg/exception.py rename to cupynumeric/linalg/_exception.py index 9a086edb2..38e38552e 100644 --- a/cunumeric/linalg/exception.py +++ b/cupynumeric/linalg/_exception.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/cupynumeric/linalg/_qr.py b/cupynumeric/linalg/_qr.py new file mode 100644 index 000000000..4b20d5fe6 --- /dev/null +++ b/cupynumeric/linalg/_qr.py @@ -0,0 +1,50 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from legate.core import get_legate_runtime + +from cupynumeric.config import CuPyNumericOpCode + +from ._exception import LinAlgError + +if TYPE_CHECKING: + from legate.core import Library, LogicalStore + + from .._thunk.deferred import DeferredArray + + +def qr_single( + library: Library, a: LogicalStore, q: LogicalStore, r: LogicalStore +) -> None: + task = get_legate_runtime().create_auto_task(library, CuPyNumericOpCode.QR) + task.throws_exception(LinAlgError) + task.add_input(a) + task.add_output(q) + task.add_output(r) + + task.add_broadcast(a) + task.add_broadcast(q) + task.add_broadcast(r) + + task.execute() + + +def qr_deferred(a: DeferredArray, q: DeferredArray, r: DeferredArray) -> None: + library = a.library + + qr_single(library, a.base, q.base, r.base) diff --git a/cupynumeric/linalg/_solve.py b/cupynumeric/linalg/_solve.py new file mode 100644 index 000000000..325fe301d --- /dev/null +++ b/cupynumeric/linalg/_solve.py @@ -0,0 +1,108 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import legate.core.types as ty +from legate.core import broadcast, get_legate_runtime + +from ..config import CuPyNumericOpCode +from ..runtime import runtime +from ._cholesky import transpose_copy_single +from ._exception import LinAlgError + +if TYPE_CHECKING: + from legate.core import Library, LogicalStore + + from .._thunk.deferred import DeferredArray + + +def solve_single(library: Library, a: LogicalStore, b: LogicalStore) -> None: + task = get_legate_runtime().create_auto_task( + library, CuPyNumericOpCode.SOLVE + ) + task.throws_exception(LinAlgError) + p_a = task.add_input(a) + p_b = task.add_input(b) + task.add_output(a, p_a) + task.add_output(b, p_b) + + task.add_constraint(broadcast(p_a)) + task.add_constraint(broadcast(p_b)) + + task.execute() + + +MIN_SOLVE_TILE_SIZE = 512 +MIN_SOLVE_MATRIX_SIZE = 2048 + + +def mp_solve( + library: Library, + n: int, + nrhs: int, + nb: int, + a: LogicalStore, + b: LogicalStore, + output: LogicalStore, +) -> None: + task = get_legate_runtime().create_auto_task( + library, CuPyNumericOpCode.MP_SOLVE + ) + task.throws_exception(LinAlgError) + task.add_input(a) + task.add_input(b) + task.add_output(output) + task.add_alignment(output, b) + task.add_scalar_arg(n, ty.int64) + task.add_scalar_arg(nrhs, ty.int64) + task.add_scalar_arg(nb, ty.int64) + task.add_nccl_communicator() # for repartitioning + task.add_cal_communicator() + task.execute() + + +def solve_deferred( + output: DeferredArray, a: DeferredArray, b: DeferredArray +) -> None: + from .._thunk.deferred import DeferredArray + + library = output.library + + if ( + runtime.has_cusolvermp + and runtime.num_gpus > 1 + and a.base.shape[0] >= MIN_SOLVE_MATRIX_SIZE + ): + n = a.base.shape[0] + nrhs = b.base.shape[1] + mp_solve( + library, n, nrhs, MIN_SOLVE_TILE_SIZE, a.base, b.base, output.base + ) + return + + a_copy = cast( + DeferredArray, + runtime.create_empty_thunk(a.shape, dtype=a.base.type, inputs=(a,)), + ) + transpose_copy_single(library, a.base, a_copy.base) + + if b.ndim > 1: + transpose_copy_single(library, b.base, output.base) + else: + output.copy(b) + + solve_single(library, a_copy.base, output.base) diff --git a/cupynumeric/linalg/_svd.py b/cupynumeric/linalg/_svd.py new file mode 100644 index 000000000..a9be94924 --- /dev/null +++ b/cupynumeric/linalg/_svd.py @@ -0,0 +1,60 @@ +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import annotations + +from typing import TYPE_CHECKING + +from legate.core import get_legate_runtime + +from cupynumeric.config import CuPyNumericOpCode + +from ._exception import LinAlgError + +if TYPE_CHECKING: + from legate.core import Library, LogicalStore + + from .._thunk.deferred import DeferredArray + + +def svd_single( + library: Library, + a: LogicalStore, + u: LogicalStore, + s: LogicalStore, + vh: LogicalStore, +) -> None: + task = get_legate_runtime().create_auto_task( + library, CuPyNumericOpCode.SVD + ) + task.throws_exception(LinAlgError) + task.add_input(a) + task.add_output(u) + task.add_output(s) + task.add_output(vh) + + task.add_broadcast(a) + task.add_broadcast(u) + task.add_broadcast(s) + task.add_broadcast(vh) + + task.execute() + + +def svd_deferred( + a: DeferredArray, u: DeferredArray, s: DeferredArray, vh: DeferredArray +) -> None: + library = a.library + + svd_single(library, a.base, u.base, s.base, vh.base) diff --git a/cunumeric/linalg/linalg.py b/cupynumeric/linalg/linalg.py similarity index 78% rename from cunumeric/linalg/linalg.py rename to cupynumeric/linalg/linalg.py index d1c0498b2..25f9ed964 100644 --- a/cunumeric/linalg/linalg.py +++ b/cupynumeric/linalg/linalg.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,25 +14,29 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Sequence, Union +from typing import TYPE_CHECKING, Sequence import numpy as np -from numpy.core.multiarray import ( # type: ignore [attr-defined] - normalize_axis_index, -) -from numpy.core.numeric import ( # type: ignore [attr-defined] - normalize_axis_tuple, -) -from cunumeric._ufunc.math import add, sqrt as _sqrt -from cunumeric.array import add_boilerplate, convert_to_cunumeric_ndarray -from cunumeric.module import dot, empty_like, eye, matmul, ndarray +from .._utils import is_np2 -from .exception import LinAlgError +if is_np2: + from numpy.lib.array_utils import normalize_axis_index # type: ignore + from numpy.lib.array_utils import normalize_axis_tuple # type: ignore +else: + from numpy.core.multiarray import ( # type: ignore + normalize_axis_index, + ) + from numpy.core.numeric import ( # type: ignore + normalize_axis_tuple, + ) -if TYPE_CHECKING: - from typing import Optional +from .._array.util import add_boilerplate, convert_to_cupynumeric_ndarray +from .._module import dot, empty_like, eye, matmul, ndarray +from .._ufunc.math import add, sqrt as _sqrt +from ._exception import LinAlgError +if TYPE_CHECKING: import numpy.typing as npt @@ -82,11 +86,63 @@ def cholesky(a: ndarray) -> ndarray: elif shape[-1] != shape[-2]: raise ValueError("Last 2 dimensions of the array must be square") - return _cholesky(a) + return _thunk_cholesky(a) + + +@add_boilerplate("a") +def qr(a: ndarray) -> tuple[ndarray, ...]: + """ + Compute the qr factorization of a matrix. + + Factor the matrix a as qr, where q is orthonormal + and r is upper-triangular. + + Parameters + ---------- + a : (M, N) array_like + Array like, at least dimension 2. + + Returns + ------- + q : (M, K) array_like + A matrix with orthonormal columns. K = min(M, N). + r : (K, N) array_like + The uppoer triangular matrix. + + Raises + ------ + LinAlgError + If factoring fails. + + Notes + ----- + Currently does not support the parameter 'mode' from numpy 1.8. + + See Also + -------- + numpy.linalg.qr + + Availability + -------- + Single GPU, Single CPU + """ + shape = a.shape + if len(shape) < 2: + raise LinAlgError( + f"{len(shape)}-dimensional array given. " + "Array must be at least two-dimensional" + ) + if len(shape) > 2: + raise NotImplementedError( + "cuPyNumeric does not yet support stacked 2d arrays" + ) + if np.dtype("e") == a.dtype: + raise TypeError("array type float16 is unsupported in linalg") + return _thunk_qr(a) @add_boilerplate("a", "b") -def solve(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: +def solve(a: ndarray, b: ndarray, out: ndarray | None = None) -> ndarray: """ Solve a linear matrix equation, or system of linear scalar equations. @@ -112,13 +168,17 @@ def solve(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: LinAlgError If `a` is singular or not square. + Notes + ------ + Multi-GPU usage is only available when compiled with cusolverMP. + See Also -------- numpy.linalg.solve Availability -------- - Single GPU, Single CPU + Multiple GPUs, Single CPU """ if a.ndim < 2: raise LinAlgError( @@ -134,7 +194,7 @@ def solve(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: raise TypeError("array type float16 is unsupported in linalg") if a.ndim > 2 or b.ndim > 2: raise NotImplementedError( - "cuNumeric does not yet support stacked 2d arrays" + "cuPyNumeric does not yet support stacked 2d arrays" ) if a.shape[-2] != a.shape[-1]: raise LinAlgError("Last 2 dimensions of the array must be square") @@ -154,7 +214,64 @@ def solve(a: ndarray, b: ndarray, out: Optional[ndarray] = None) -> ndarray: if a.size == 0 or b.size == 0: return empty_like(b) - return _solve(a, b, out) + return _thunk_solve(a, b, out) + + +@add_boilerplate("a") +def svd(a: ndarray, full_matrices: bool = True) -> tuple[ndarray, ...]: + """ + Singular Value Decomposition. + + Parameters + ---------- + a : (M, N) array_like + Array like, at least dimension 2. + full_matrices : bool, optional + If True (default), u and vh are of shape (M, M), (N, N). + If False, the shapes are (M, K) and (K, N), where K = min(M, N). + + Returns + ------- + u : (M, M) array_like + Unitary array(s). + s : (K) array_like + The singular values, sorted in descending order + vh : (N, N) array_like + Unitary array(s). + + Raises + ------ + LinAlgError + If SVD computation does not converge. + + Notes + ----- + Currently does not support the parameters 'full_matrices', 'compute_uv', + and 'hermitian'. + + See Also + -------- + numpy.linalg.svd + + Availability + -------- + Single GPU, Single CPU + """ + shape = a.shape + if len(shape) < 2: + raise LinAlgError( + f"{len(shape)}-dimensional array given. " + "Array must be at least two-dimensional" + ) + if len(shape) > 2: + raise NotImplementedError( + "cuPyNumeric does not yet support stacked 2d arrays" + ) + if shape[0] < shape[1]: + raise NotImplementedError("cuPyNumeric only supports M >= N") + if np.dtype("e") == a.dtype: + raise TypeError("array type float16 is unsupported in linalg") + return _thunk_svd(a, full_matrices) # This implementation is adapted closely from NumPy @@ -206,7 +323,7 @@ def matrix_power(a: ndarray, n: int) -> ndarray: # Invert if necessary if n < 0: - # TODO: Add this once cunumeric.inv is implemented + # TODO: Add this once cupynumeric.inv is implemented # a = inv(a) # n = abs(n) raise NotImplementedError("Negative exponent in matrix_power") @@ -222,8 +339,8 @@ def matrix_power(a: ndarray, n: int) -> ndarray: # Use binary decomposition to reduce the number of matrix multiplications. # Here, we iterate over the bits of n, from LSB to MSB, raise `a` to # increasing powers of 2, and multiply into the result as needed. - z: Union[ndarray, None] = None - result: Union[ndarray, None] = None + z: ndarray | None = None + result: ndarray | None = None while n > 0: z = a if z is None else matmul(z, z) n, bit = divmod(n, 2) @@ -237,7 +354,7 @@ def matrix_power(a: ndarray, n: int) -> ndarray: # This implementation is adapted closely from NumPy def multi_dot( - arrays: Sequence[ndarray], *, out: Union[ndarray, None] = None + arrays: Sequence[ndarray], *, out: ndarray | None = None ) -> ndarray: """ Compute the dot product of two or more arrays in a single function call, @@ -268,9 +385,9 @@ def multi_dot( -------- Multiple GPUs, Multiple CPUs """ - arrays = [convert_to_cunumeric_ndarray(x) for x in arrays] + arrays = [convert_to_cupynumeric_ndarray(x) for x in arrays] if out is not None: - out = convert_to_cunumeric_ndarray(out, share=True) + out = convert_to_cupynumeric_ndarray(out, share=True) n = len(arrays) # optimization only makes sense for len(arrays) > 2 @@ -314,7 +431,7 @@ def multi_dot( def _multi_dot_three( - A: ndarray, B: ndarray, C: ndarray, out: Union[ndarray, None] = None + A: ndarray, B: ndarray, C: ndarray, out: ndarray | None = None ) -> ndarray: """ Find the best order for three arrays and do the multiplication. @@ -359,7 +476,7 @@ def _multi_dot_matrix_chain_order( for l_ in range(1, n): for i in range(n - l_): j = i + l_ - m[i, j] = np.Inf + m[i, j] = np.inf for k in range(i, j): q = m[i, k] + m[k + 1, j] + p[i] * p[k + 1] * p[j + 1] if q < m[i, j]: @@ -374,7 +491,7 @@ def _multi_dot( order: npt.NDArray[np.int64], i: int, j: int, - out: Union[ndarray, None] = None, + out: ndarray | None = None, ) -> ndarray: """Actually do the multiplication with the given order.""" if i == j: @@ -394,10 +511,10 @@ def _multi_dot( @add_boilerplate("x") def norm( x: ndarray, - ord: Union[str, int, float, None] = None, - axis: Union[int, tuple[int, int], None] = None, + ord: str | int | float | None = None, + axis: int | tuple[int, int] | None = None, keepdims: bool = False, -) -> Union[float, ndarray]: +) -> float | ndarray: """ Matrix or vector norm. @@ -583,7 +700,7 @@ def norm( raise ValueError("Improper number of dimensions to norm") -def _cholesky(a: ndarray, no_tril: bool = False) -> ndarray: +def _thunk_cholesky(a: ndarray) -> ndarray: """Cholesky decomposition. Return the Cholesky decomposition, `L * L.H`, of the square matrix `a`, @@ -627,12 +744,33 @@ def _cholesky(a: ndarray, no_tril: bool = False) -> ndarray: dtype=input.dtype, inputs=(input,), ) - output._thunk.cholesky(input._thunk, no_tril=no_tril) + output._thunk.cholesky(input._thunk) return output -def _solve( - a: ndarray, b: ndarray, output: Optional[ndarray] = None +def _thunk_qr(a: ndarray) -> tuple[ndarray, ...]: + if a.dtype.kind not in ("f", "c"): + a = a.astype("float64") + + k = min(a.shape[0], a.shape[1]) + + out_q = ndarray( + shape=(a.shape[0], k), + dtype=a.dtype, + inputs=(a,), + ) + out_r = ndarray( + shape=(k, a.shape[1]), + dtype=a.dtype, + inputs=(a,), + ) + + a._thunk.qr(out_q._thunk, out_r._thunk) + return out_q, out_r + + +def _thunk_solve( + a: ndarray, b: ndarray, output: ndarray | None = None ) -> ndarray: if a.dtype.kind not in ("f", "c"): a = a.astype("float64") @@ -666,3 +804,32 @@ def _solve( ) out._thunk.solve(a._thunk, b._thunk) return out + + +def _thunk_svd(a: ndarray, full_matrices: bool) -> tuple[ndarray, ...]: + if a.dtype.kind not in ("f", "c"): + a = a.astype("float64") + + k = min(a.shape[0], a.shape[1]) + + out_u = ndarray( + shape=(a.shape[0], a.shape[0] if full_matrices else k), + dtype=a.dtype, + inputs=(a,), + ) + + real_dtype = a.dtype.type(0).real.dtype + + out_s = ndarray( + shape=(k,), + dtype=real_dtype, + inputs=(a,), + ) + out_vh = ndarray( + shape=(a.shape[1] if full_matrices else k, a.shape[1]), + dtype=a.dtype, + inputs=(a,), + ) + + a._thunk.svd(out_u._thunk, out_s._thunk, out_vh._thunk) + return out_u, out_s, out_vh diff --git a/cunumeric/ma/__init__.py b/cupynumeric/ma/__init__.py similarity index 80% rename from cunumeric/ma/__init__.py rename to cupynumeric/ma/__init__.py index 14a9e0d46..830910f51 100644 --- a/cunumeric/ma/__init__.py +++ b/cupynumeric/ma/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ import numpy.ma as _ma -from cunumeric.array import maybe_convert_to_np_ndarray -from cunumeric.coverage import clone_module -from cunumeric.ma._masked_array import MaskedArray +from .._array.util import maybe_convert_to_np_ndarray +from .._utils.coverage import clone_module +from ._masked_array import MaskedArray masked_array = MaskedArray diff --git a/cunumeric/ma/_masked_array.py b/cupynumeric/ma/_masked_array.py similarity index 87% rename from cunumeric/ma/_masked_array.py rename to cupynumeric/ma/_masked_array.py index 4420bdf1c..1884d2cea 100644 --- a/cunumeric/ma/_masked_array.py +++ b/cupynumeric/ma/_masked_array.py @@ -1,4 +1,4 @@ -# Copyright 2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Any, Type, Union +from typing import TYPE_CHECKING, Any, Type if TYPE_CHECKING: import numpy.typing as npt @@ -23,8 +23,8 @@ import numpy as _np -from ..array import maybe_convert_to_np_ndarray -from ..coverage import clone_class +from .._array.util import maybe_convert_to_np_ndarray +from .._utils.coverage import clone_class NDARRAY_INTERNAL = { "__array_finalize__", @@ -37,7 +37,7 @@ "__array_wrap__", } -MaskType = _np.bool_ +MaskType = bool nomask = MaskType(0) @@ -51,8 +51,8 @@ def __new__(cls: Type[Any], *args: Any, **kw: Any) -> MaskedArray: def __init__( self, data: Any = None, - mask: _np.bool_ = nomask, - dtype: Union[npt.DTypeLike, None] = None, + mask: bool = nomask, + dtype: npt.DTypeLike | None = None, copy: bool = False, subok: bool = True, ndmin: int = 0, @@ -60,7 +60,7 @@ def __init__( keep_mask: Any = True, hard_mask: Any = None, shrink: bool = True, - order: Union[str, None] = None, + order: str | None = None, ) -> None: self._internal_ma = _np.ma.MaskedArray( # type: ignore data=maybe_convert_to_np_ndarray(data), diff --git a/cunumeric/patch.py b/cupynumeric/patch.py similarity index 80% rename from cunumeric/patch.py rename to cupynumeric/patch.py index d47ab24e3..b92a7f7e3 100644 --- a/cunumeric/patch.py +++ b/cupynumeric/patch.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,12 +13,12 @@ # limitations under the License. # """ This module may be imported in order to globably replace NumPy with -CuNumeric. +cuPyNumeric. In order to function properly, this module must be imported early (ideally at the very start of a script). The ``numpy`` module in ``sys.modules`` -will be replaced with ``cunumeric`` so that any subsequent use of the -``numpy`` module will use ``cunumeric`` instead. +will be replaced with ``cupynumeric`` so that any subsequent use of the +``numpy`` module will use ``cupynumeric`` instead. This module is primarily intended for quick demonstrations or proofs of concept. @@ -28,6 +28,6 @@ import sys -import cunumeric +import cupynumeric -sys.modules["numpy"] = cunumeric +sys.modules["numpy"] = cupynumeric diff --git a/cunumeric/py.typed b/cupynumeric/py.typed similarity index 100% rename from cunumeric/py.typed rename to cupynumeric/py.typed diff --git a/cunumeric/random/__init__.py b/cupynumeric/random/__init__.py similarity index 67% rename from cunumeric/random/__init__.py rename to cupynumeric/random/__init__.py index 0f397a0c5..09e5054ff 100644 --- a/cunumeric/random/__init__.py +++ b/cupynumeric/random/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,16 +16,13 @@ import numpy.random as _nprandom -from cunumeric.array import maybe_convert_to_np_ndarray -from cunumeric.coverage import clone_module -from cunumeric.runtime import runtime +from .._array.util import maybe_convert_to_np_ndarray +from .._utils.coverage import clone_module +from ..runtime import runtime -if runtime.has_curand: - from cunumeric.random.random import * - from cunumeric.random.bitgenerator import * - from cunumeric.random.generator import * -else: - from cunumeric.random.legacy import * +from ._random import * +from ._bitgenerator import * +from ._generator import * clone_module( _nprandom, @@ -36,4 +33,5 @@ del maybe_convert_to_np_ndarray del clone_module +del runtime del _nprandom diff --git a/cunumeric/random/bitgenerator.py b/cupynumeric/random/_bitgenerator.py similarity index 89% rename from cunumeric/random/bitgenerator.py rename to cupynumeric/random/_bitgenerator.py index 8d99e61aa..55ecbea8e 100644 --- a/cunumeric/random/bitgenerator.py +++ b/cupynumeric/random/_bitgenerator.py @@ -1,4 +1,4 @@ -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ import time from abc import abstractproperty -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import numpy as np -from ..array import ndarray +from .._array.array import ndarray from ..config import BitGeneratorType from ..runtime import runtime @@ -33,7 +33,7 @@ class BitGenerator: def __init__( self, - seed: Union[int, None] = None, + seed: int | None = None, forceBuild: bool = False, ) -> None: """ @@ -44,7 +44,7 @@ def __init__( Parameters ---------- - seed : {None, int}, optional + seed : {int, None}, optional A seed to initialize the `BitGenerator`. If None, then fresh, unpredictable entropy will be pulled from the OS. @@ -53,7 +53,7 @@ def __init__( numpy.random.BitGenerator Availability - -------- + ------------ Multiple GPUs, Multiple CPUs """ if type(self) is BitGenerator: @@ -76,7 +76,7 @@ def __del__(self) -> None: runtime.bitgenerator_destroy(self.handle, disposing=True) # when output is false => skip ahead - def random_raw(self, shape: Union[NdShapeLike, None] = None) -> ndarray: + def random_raw(self, shape: NdShapeLike | None = None) -> ndarray: if shape is None: shape = (1,) if not isinstance(shape, tuple): @@ -90,8 +90,8 @@ def random_raw(self, shape: Union[NdShapeLike, None] = None) -> ndarray: def integers( self, low: int, - high: Union[int, None] = None, - shape: Union[NdShapeLike, None] = None, + high: int | None = None, + shape: NdShapeLike | None = None, type: npt.DTypeLike = np.int64, endpoint: bool = False, ) -> ndarray: @@ -112,9 +112,9 @@ def integers( def random( self, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, - res: Union[ndarray, None] = None, + res: ndarray | None = None, ) -> ndarray: if shape is None: shape = (1,) @@ -131,7 +131,7 @@ def lognormal( self, mean: float = 0.0, sigma: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -148,7 +148,7 @@ def normal( self, mean: float = 0.0, sigma: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -165,7 +165,7 @@ def uniform( self, low: float = 0.0, high: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -178,9 +178,7 @@ def uniform( ) return res - def poisson( - self, lam: float, shape: Union[NdShapeLike, None] = None - ) -> ndarray: + def poisson(self, lam: float, shape: NdShapeLike | None = None) -> ndarray: if shape is None: shape = (1,) if not isinstance(shape, tuple): @@ -194,7 +192,7 @@ def poisson( def exponential( self, scale: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -211,7 +209,7 @@ def gumbel( self, mu: float = 0.0, beta: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -228,7 +226,7 @@ def laplace( self, mu: float = 0.0, beta: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -245,7 +243,7 @@ def logistic( self, mu: float = 0.0, beta: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -261,7 +259,7 @@ def logistic( def pareto( self, alpha: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -277,7 +275,7 @@ def pareto( def power( self, alpha: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -293,7 +291,7 @@ def power( def rayleigh( self, sigma: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -310,7 +308,7 @@ def cauchy( self, x0: float, gamma: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -328,7 +326,7 @@ def triangular( a: float, b: float, c: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -345,7 +343,7 @@ def weibull( self, lam: float, k: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -358,7 +356,7 @@ def weibull( ) return res - def bytes(self, length: Union[int, tuple[int, ...]]) -> ndarray: + def bytes(self, length: int | tuple[int, ...]) -> ndarray: if not isinstance(length, tuple): length = (length,) res = ndarray(length, dtype=np.dtype(np.uint8)) @@ -374,7 +372,7 @@ def beta( self, a: float, b: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -391,7 +389,7 @@ def f( self, dfnum: float, dfden: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -412,7 +410,7 @@ def f( def logseries( self, p: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: @@ -430,7 +428,7 @@ def noncentral_f( dfnum: float, dfden: float, nonc: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -453,7 +451,7 @@ def chisquare( self, df: float, nonc: float = 0.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -470,7 +468,7 @@ def gamma( self, k: float, theta: float = 1.0, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -486,7 +484,7 @@ def gamma( def standard_t( self, df: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -504,7 +502,7 @@ def hypergeometric( ngood: int, nbad: int, nsample: int, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: @@ -527,7 +525,7 @@ def vonmises( self, mu: float, kappa: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -543,7 +541,7 @@ def vonmises( def zipf( self, alpha: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: @@ -559,7 +557,7 @@ def zipf( def geometric( self, p: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: @@ -576,7 +574,7 @@ def wald( self, mean: float, scale: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: if shape is None: @@ -593,7 +591,7 @@ def binomial( self, ntrials: int, p: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: @@ -610,7 +608,7 @@ def negative_binomial( self, ntrials: int, p: float, - shape: Union[NdShapeLike, None] = None, + shape: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: if shape is None: diff --git a/cunumeric/random/generator.py b/cupynumeric/random/_generator.py similarity index 83% rename from cunumeric/random/generator.py rename to cupynumeric/random/_generator.py index 7145de662..4736bd898 100644 --- a/cunumeric/random/generator.py +++ b/cupynumeric/random/_generator.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import numpy as np -from cunumeric.random.bitgenerator import XORWOW, BitGenerator +from ._bitgenerator import XORWOW, BitGenerator if TYPE_CHECKING: import numpy.typing as npt - from ..array import ndarray + from .._array.array import ndarray from ..types import NdShapeLike @@ -43,8 +43,8 @@ def __init__(self, bit_generator: BitGenerator) -> None: then an array with that shape is filled and returned. - The function :func:`cunumeric.random.default_rng` will instantiate - a `Generator` with cuNumeric's default `BitGenerator`. + The function :func:`cupynumeric.random.default_rng` will instantiate + a `Generator` with cuPyNumeric's default `BitGenerator`. Parameters ---------- @@ -57,7 +57,7 @@ def __init__(self, bit_generator: BitGenerator) -> None: default_rng : Recommended constructor for `Generator`. Availability - -------- + ------------ Multiple GPUs, Multiple CPUs """ @@ -67,7 +67,7 @@ def beta( self, a: float, b: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.beta(a=a, b=b, shape=size, dtype=dtype) @@ -76,21 +76,21 @@ def binomial( self, ntrials: int, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.binomial( ntrials=ntrials, p=p, shape=size, dtype=dtype ) - def bytes(self, length: Union[int, tuple[int, ...]]) -> ndarray: + def bytes(self, length: int | tuple[int, ...]) -> ndarray: return self.bit_generator.bytes(length=length) def cauchy( self, x0: float, gamma: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.cauchy( @@ -100,7 +100,7 @@ def cauchy( def chisquare( self, df: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.chisquare( @@ -110,7 +110,7 @@ def chisquare( def exponential( self, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.exponential( @@ -121,7 +121,7 @@ def f( self, dfnum: float, dfden: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.f( @@ -132,7 +132,7 @@ def gamma( self, shape: float, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.gamma( @@ -142,7 +142,7 @@ def gamma( def geometric( self, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.geometric(p=p, shape=size, dtype=dtype) @@ -151,7 +151,7 @@ def gumbel( self, loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.gumbel( @@ -163,7 +163,7 @@ def hypergeometric( ngood: int, nbad: int, nsample: int, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.hypergeometric( @@ -173,8 +173,8 @@ def hypergeometric( def integers( self, low: int, - high: Union[int, None] = None, - size: Union[NdShapeLike, None] = None, + high: int | None = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.int64, endpoint: bool = False, ) -> ndarray: @@ -184,7 +184,7 @@ def laplace( self, loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.laplace( @@ -195,7 +195,7 @@ def logistic( self, loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.logistic( @@ -206,7 +206,7 @@ def lognormal( self, mean: float = 0.0, sigma: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.lognormal(mean, sigma, size, dtype) @@ -214,7 +214,7 @@ def lognormal( def logseries( self, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.logseries(p=p, shape=size, dtype=dtype) @@ -223,7 +223,7 @@ def negative_binomial( self, ntrials: int, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.negative_binomial( @@ -234,7 +234,7 @@ def noncentral_chisquare( self, df: float, nonc: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.chisquare( @@ -246,7 +246,7 @@ def noncentral_f( dfnum: float, dfden: float, nonc: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.noncentral_f( @@ -257,7 +257,7 @@ def normal( self, loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.normal( @@ -267,29 +267,29 @@ def normal( def pareto( self, a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.pareto(alpha=a, shape=size, dtype=dtype) def poisson( - self, lam: float = 1.0, size: Union[NdShapeLike, None] = None + self, lam: float = 1.0, size: NdShapeLike | None = None ) -> ndarray: return self.bit_generator.poisson(lam, size) def power( self, a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.power(alpha=a, shape=size, dtype=dtype) def random( self, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, - out: Union[ndarray, None] = None, + out: ndarray | None = None, ) -> ndarray: if out is not None: if size is not None and out.shape != size: @@ -306,7 +306,7 @@ def random( def rayleigh( self, scale: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.rayleigh( @@ -315,14 +315,14 @@ def rayleigh( def standard_cauchy( self, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.cauchy(0.0, 1.0, size, dtype) def standard_exponential( self, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.exponential(1.0, size, dtype) @@ -330,7 +330,7 @@ def standard_exponential( def standard_gamma( self, shape: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.gamma(shape=shape, scale=1.0, size=size, dtype=dtype) @@ -338,7 +338,7 @@ def standard_gamma( def standard_t( self, df: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.standard_t(df=df, shape=size, dtype=dtype) @@ -348,7 +348,7 @@ def triangular( left: float, mode: float, right: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.triangular( @@ -359,7 +359,7 @@ def uniform( self, low: float = 0.0, high: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.uniform(low, high, size, dtype) @@ -368,7 +368,7 @@ def vonmises( self, mu: float, kappa: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.vonmises( @@ -379,7 +379,7 @@ def wald( self, mean: float, scale: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.wald(mean, scale, shape=size, dtype=dtype) @@ -387,7 +387,7 @@ def wald( def weibull( self, a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: return self.bit_generator.weibull(lam=1, k=a, shape=size, dtype=dtype) @@ -395,14 +395,14 @@ def weibull( def zipf( self, a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: return self.bit_generator.zipf(alpha=a, shape=size, dtype=dtype) def default_rng( - seed: Union[None, int, BitGenerator, Generator] = None + seed: int | BitGenerator | Generator | None = None, ) -> Generator: """ Construct a new Generator with the default BitGenerator (XORWOW). diff --git a/cunumeric/random/random.py b/cupynumeric/random/_random.py similarity index 90% rename from cunumeric/random/random.py rename to cupynumeric/random/_random.py index d877d391d..6879e9053 100644 --- a/cunumeric/random/random.py +++ b/cupynumeric/random/_random.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ # from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any import numpy as np -from cunumeric.array import ndarray -from cunumeric.coverage import clone_class -from cunumeric.random import generator -from cunumeric.runtime import runtime +from .._array.array import ndarray +from .._utils.coverage import clone_class +from ..runtime import runtime +from ._generator import default_rng, get_static_generator # NOQA if TYPE_CHECKING: import numpy.typing as npt @@ -29,10 +29,7 @@ from ..types import NdShapeLike -default_rng = generator.default_rng - - -def seed(init: Union[int, None] = None) -> None: +def seed(init: int | None = None) -> None: """ Reseed the legacy random number generator. @@ -54,7 +51,7 @@ def seed(init: Union[int, None] = None) -> None: def beta( a: float, b: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -100,13 +97,13 @@ def beta( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().beta(a, b, size, dtype) + return get_static_generator().beta(a, b, size, dtype) def binomial( ntrials: int, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -145,7 +142,7 @@ def binomial( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().binomial(ntrials, p, size, dtype) + return get_static_generator().binomial(ntrials, p, size, dtype) def bytes(length: int) -> ndarray: @@ -172,12 +169,12 @@ def bytes(length: int) -> ndarray: -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().bytes(length) + return get_static_generator().bytes(length) def chisquare( df: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -218,12 +215,12 @@ def chisquare( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().chisquare(df, size, dtype) + return get_static_generator().chisquare(df, size, dtype) def exponential( scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -278,13 +275,13 @@ def exponential( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().exponential(scale, size, dtype) + return get_static_generator().exponential(scale, size, dtype) def f( dfnum: float, dfden: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -326,13 +323,13 @@ def f( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().f(dfnum, dfden, size, dtype) + return get_static_generator().f(dfnum, dfden, size, dtype) def gamma( shape: float, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -369,12 +366,12 @@ def gamma( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().gamma(shape, scale, size, dtype) + return get_static_generator().gamma(shape, scale, size, dtype) def geometric( p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -416,13 +413,13 @@ def geometric( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().geometric(p, size, dtype) + return get_static_generator().geometric(p, size, dtype) def gumbel( loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -458,14 +455,14 @@ def gumbel( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().gumbel(loc, scale, size, dtype) + return get_static_generator().gumbel(loc, scale, size, dtype) def hypergeometric( ngood: int, nbad: int, nsample: int, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -508,7 +505,7 @@ def hypergeometric( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().hypergeometric( + return get_static_generator().hypergeometric( ngood, nbad, nsample, size, dtype ) @@ -516,7 +513,7 @@ def hypergeometric( def laplace( loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -555,13 +552,13 @@ def laplace( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().laplace(loc, scale, size, dtype) + return get_static_generator().laplace(loc, scale, size, dtype) def logistic( loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -597,13 +594,13 @@ def logistic( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().logistic(loc, scale, size, dtype) + return get_static_generator().logistic(loc, scale, size, dtype) def lognormal( mean: float = 0.0, sigma: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -641,12 +638,12 @@ def lognormal( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().lognormal(mean, sigma, size, dtype) + return get_static_generator().lognormal(mean, sigma, size, dtype) def logseries( p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -679,13 +676,13 @@ def logseries( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().logseries(p, size, dtype) + return get_static_generator().logseries(p, size, dtype) def negative_binomial( n: int, p: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -723,15 +720,13 @@ def negative_binomial( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().negative_binomial( - n, p, size, dtype - ) + return get_static_generator().negative_binomial(n, p, size, dtype) def noncentral_chisquare( df: float, nonc: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -767,16 +762,14 @@ def noncentral_chisquare( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().noncentral_chisquare( - df, nonc, size, dtype - ) + return get_static_generator().noncentral_chisquare(df, nonc, size, dtype) def noncentral_f( dfnum: float, dfden: float, nonc: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -816,15 +809,13 @@ def noncentral_f( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().noncentral_f( - dfnum, dfden, nonc, size, dtype - ) + return get_static_generator().noncentral_f(dfnum, dfden, nonc, size, dtype) def normal( loc: float = 0.0, scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -873,12 +864,12 @@ def normal( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().normal(loc, scale, size, dtype) + return get_static_generator().normal(loc, scale, size, dtype) def pareto( a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -926,12 +917,10 @@ def pareto( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().pareto(a, size, dtype) + return get_static_generator().pareto(a, size, dtype) -def poisson( - lam: float = 1.0, size: Union[NdShapeLike, None] = None -) -> ndarray: +def poisson(lam: float = 1.0, size: NdShapeLike | None = None) -> ndarray: """ poisson(lam=1.0, size=None) @@ -964,12 +953,12 @@ def poisson( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().poisson(lam, size) + return get_static_generator().poisson(lam, size) def power( a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1007,10 +996,10 @@ def power( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().power(a, size, dtype) + return get_static_generator().power(a, size, dtype) -def rand(*shapeargs: int) -> Union[float, ndarray]: +def rand(*shapeargs: int) -> float | ndarray: """ rand(d0, d1, ..., dn) @@ -1044,10 +1033,10 @@ def rand(*shapeargs: int) -> Union[float, ndarray]: def randint( low: int, - high: Union[int, None] = None, - size: Union[NdShapeLike, None] = None, - dtype: Union[np.dtype[Any], type] = int, -) -> Union[int, ndarray, npt.NDArray[Any]]: + high: int | None = None, + size: NdShapeLike | None = None, + dtype: np.dtype[Any] | type = int, +) -> int | ndarray | npt.NDArray[Any]: """ randint(low, high=None, size=None, dtype=int) @@ -1100,10 +1089,10 @@ def randint( elif low >= high: raise ValueError("low >= high") - return generator.get_static_generator().integers(low, high, size, dtype) + return get_static_generator().integers(low, high, size, dtype) -def randn(*shapeargs: int) -> Union[float, ndarray]: +def randn(*shapeargs: int) -> float | ndarray: """ randn(d0, d1, ..., dn) @@ -1140,8 +1129,8 @@ def randn(*shapeargs: int) -> Union[float, ndarray]: def random( - size: Union[NdShapeLike, None] = None, -) -> Union[float, ndarray]: + size: NdShapeLike | None = None, +) -> float | ndarray: """ random(size=None) @@ -1156,16 +1145,16 @@ def random( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().random(size) + return get_static_generator().random(size) # deprecated in numpy from version 1.11.0 def random_integers( low: int, - high: Union[int, None] = None, - size: Union[NdShapeLike, None] = None, - dtype: Union[np.dtype[Any], type] = int, -) -> Union[int, ndarray, npt.NDArray[Any]]: + high: int | None = None, + size: NdShapeLike | None = None, + dtype: np.dtype[Any] | type = int, +) -> int | ndarray | npt.NDArray[Any]: """ random_integers(low, high=None, size=None) @@ -1214,8 +1203,8 @@ def random_integers( def random_sample( - size: Union[NdShapeLike, None] = None, dtype: npt.DTypeLike = np.float64 -) -> Union[float, ndarray]: + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64 +) -> float | ndarray: """ random_sample(size=None) @@ -1256,7 +1245,7 @@ def random_sample( def rayleigh( scale: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1289,14 +1278,14 @@ def rayleigh( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().rayleigh(scale, size, dtype) + return get_static_generator().rayleigh(scale, size, dtype) sample = random_sample def standard_cauchy( - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1326,11 +1315,11 @@ def standard_cauchy( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().standard_cauchy(size, dtype) + return get_static_generator().standard_cauchy(size, dtype) def standard_exponential( - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1361,12 +1350,12 @@ def standard_exponential( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().standard_exponential(size, dtype) + return get_static_generator().standard_exponential(size, dtype) def standard_gamma( shape: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1399,12 +1388,12 @@ def standard_gamma( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().standard_gamma(shape, size, dtype) + return get_static_generator().standard_gamma(shape, size, dtype) def standard_t( df: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1439,14 +1428,14 @@ def standard_t( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().standard_t(df, size, dtype) + return get_static_generator().standard_t(df, size, dtype) def triangular( left: float, mode: float, right: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1487,15 +1476,13 @@ def triangular( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().triangular( - left, mode, right, size, dtype - ) + return get_static_generator().triangular(left, mode, right, size, dtype) def uniform( low: float = 0.0, high: float = 1.0, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1537,13 +1524,13 @@ def uniform( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().uniform(low, high, size, dtype) + return get_static_generator().uniform(low, high, size, dtype) def vonmises( mu: float, kappa: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1583,13 +1570,13 @@ def vonmises( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().vonmises(mu, kappa, size, dtype) + return get_static_generator().vonmises(mu, kappa, size, dtype) def wald( mean: float, scale: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1630,12 +1617,12 @@ def wald( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().wald(mean, scale, size, dtype) + return get_static_generator().wald(mean, scale, size, dtype) def weibull( a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.float64, ) -> ndarray: """ @@ -1675,12 +1662,12 @@ def weibull( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().weibull(a, size, dtype) + return get_static_generator().weibull(a, size, dtype) def zipf( a: float, - size: Union[NdShapeLike, None] = None, + size: NdShapeLike | None = None, dtype: npt.DTypeLike = np.uint32, ) -> ndarray: """ @@ -1718,7 +1705,7 @@ def zipf( -------- Multiple GPUs, Multiple CPUs """ - return generator.get_static_generator().zipf(a, size, dtype) + return get_static_generator().zipf(a, size, dtype) def _random_state_fallback(obj: Any) -> Any: @@ -1726,7 +1713,7 @@ def _random_state_fallback(obj: Any) -> Any: # wrapped vanilla NumPy RandomState if isinstance(obj, RandomState): return obj._np_random_state - # eagerly convert any cuNumeric ndarrays to NumPy + # eagerly convert any cuPyNumeric ndarrays to NumPy if isinstance(obj, ndarray): return obj.__array__() return obj @@ -1746,5 +1733,5 @@ class RandomState: Random seed used to initialize the pseudo-random number generator. """ - def __init__(self, seed: Union[int, None] = None): + def __init__(self, seed: int | None = None): self._np_random_state = np.random.RandomState(seed or 0) diff --git a/cunumeric/runtime.py b/cupynumeric/runtime.py similarity index 60% rename from cunumeric/runtime.py rename to cupynumeric/runtime.py index a07c0847b..7af064d46 100644 --- a/cunumeric/runtime.py +++ b/cupynumeric/runtime.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,122 +14,134 @@ # from __future__ import annotations -import struct +import math import warnings -from functools import reduce -from typing import TYPE_CHECKING, Any, Optional, Sequence, Union +from functools import lru_cache, reduce +from typing import TYPE_CHECKING, Any, Sequence, TypeGuard import legate.core.types as ty import numpy as np -from legate.core import LEGATE_MAX_DIM, ProcessorKind, Rect, get_legate_runtime -from legate.core.context import Context as LegateContext +from legate.core import LEGATE_MAX_DIM, Scalar, TaskTarget, get_legate_runtime from legate.settings import settings as legate_settings -from typing_extensions import TypeGuard +from ._utils.array import calculate_volume, is_supported_dtype, to_core_type +from ._utils.stack import find_last_user_stacklevel from .config import ( BitGeneratorOperation, - CuNumericOpCode, - CuNumericTunable, - cunumeric_context, - cunumeric_lib, + CuPyNumericOpCode, + TransferType, + cupynumeric_lib, ) -from .deferred import DeferredArray -from .eager import EagerArray -from .settings import settings -from .thunk import NumPyThunk -from .types import NdShape -from .utils import calculate_volume, find_last_user_stacklevel, to_core_dtype + +# We need to be careful about importing from other cupynumeric modules. The +# runtime is global and used in many places, but also depends on many of the +# other modules. Things like config and utils are OK, but imports for thunks, +# array types, etc. need to be deferred in order to avoid circular imports. + if TYPE_CHECKING: import numpy.typing as npt - from legate.core._legion.future import Future - from legate.core.operation import AutoTask, ManualTask - - from .array import ndarray + from legate.core import AutoTask, ManualTask + from ._array.array import ndarray + from ._thunk.deferred import DeferredArray + from ._thunk.eager import EagerArray + from ._thunk.thunk import NumPyThunk + from .types import NdShape DIMENSION = int +legate_runtime = get_legate_runtime() + + +def thunk_from_scalar( + bytes: bytes, shape: NdShape, dtype: np.dtype[Any] +) -> DeferredArray: + from ._thunk.deferred import DeferredArray + + store = legate_runtime.create_store_from_scalar( + Scalar(bytes, to_core_type(dtype)), + shape=shape, + ) + return DeferredArray(store) + + +@lru_cache +def cached_thunk_from_scalar( + bytes: bytes, shape: NdShape, dtype: np.dtype[Any] +) -> DeferredArray: + return thunk_from_scalar(bytes, shape, dtype) + class Runtime(object): - def __init__(self, legate_context: LegateContext) -> None: - self.legate_context = legate_context - self.legate_runtime = get_legate_runtime() + def __init__(self) -> None: + self.library = legate_runtime.find_library(cupynumeric_lib.name) self.current_random_epoch = 0 self.current_random_bitgenid = 0 self.current_random_bitgen_zombies: tuple[Any, ...] = () self.destroyed = False self.api_calls: list[tuple[str, str, bool]] = [] - self.max_eager_volume = int( - self.legate_context.get_tunable( - CuNumericTunable.MAX_EAGER_VOLUME, - ty.int32, - ) + max_eager_volume = ( + cupynumeric_lib.shared_object.cupynumeric_max_eager_volume() + ) + self.max_eager_volume = int(np.asarray(max_eager_volume)) + + assert cupynumeric_lib.shared_object is not None + self.cupynumeric_lib = cupynumeric_lib.shared_object + self.has_cusolvermp = ( + cupynumeric_lib.shared_object.cupynumeric_has_cusolvermp() ) - # Make sure that our CuNumericLib object knows about us so it can - # destroy us - cunumeric_lib.set_runtime(self) - assert cunumeric_lib.shared_object is not None - self.cunumeric_lib = cunumeric_lib.shared_object - self.has_curand = cunumeric_lib.shared_object.cunumeric_has_curand() + from .settings import settings settings.warn = settings.warn() or legate_settings.test() if self.num_gpus > 0 and settings.preload_cudalibs(): self._load_cudalibs() - # Maps dimensions to point types - self._cached_point_types: dict[DIMENSION, ty.Dtype] = dict() # Maps value types to struct types used in argmin/argmax - self._cached_argred_types: dict[ty.Dtype, ty.Dtype] = dict() + self._cached_argred_types: dict[ty.Type, ty.Type] = dict() @property def num_procs(self) -> int: - return len(self.legate_runtime.machine) + return len(legate_runtime.machine) @property def num_gpus(self) -> int: - return self.legate_runtime.machine.count(ProcessorKind.GPU) - - def get_point_type(self, dim: DIMENSION) -> ty.Dtype: - cached = self._cached_point_types.get(dim) - if cached is not None: - return cached - point_dtype = ty.array_type(ty.int64, dim) if dim > 1 else ty.int64 - self._cached_point_types[dim] = point_dtype - return point_dtype + return legate_runtime.machine.count(TaskTarget.GPU) def record_api_call( self, name: str, location: str, implemented: bool ) -> None: + from .settings import settings + assert settings.report_coverage() self.api_calls.append((name, location, implemented)) def _load_cudalibs(self) -> None: - task = self.legate_context.create_manual_task( - CuNumericOpCode.LOAD_CUDALIBS, - launch_domain=Rect(lo=(0,), hi=(self.num_gpus,)), + task = legate_runtime.create_manual_task( + self.library, + CuPyNumericOpCode.LOAD_CUDALIBS, + [self.num_gpus], ) task.execute() - self.legate_runtime.issue_execution_fence(block=True) + legate_runtime.issue_execution_fence(block=True) - def _unload_cudalibs(self) -> None: - task = self.legate_context.create_manual_task( - CuNumericOpCode.UNLOAD_CUDALIBS, - launch_domain=Rect(lo=(0,), hi=(self.num_gpus,)), - ) - task.execute() - - def get_argred_type(self, value_dtype: ty.Dtype) -> ty.Dtype: + def get_argred_type(self, value_dtype: ty.Type) -> ty.Type: cached = self._cached_argred_types.get(value_dtype) if cached is not None: return cached argred_dtype = ty.struct_type([ty.int64, value_dtype], True) self._cached_argred_types[value_dtype] = argred_dtype - self.cunumeric_lib.cunumeric_register_reduction_op( - argred_dtype.uid, value_dtype.code + ids = self.cupynumeric_lib.cupynumeric_register_reduction_ops( + value_dtype.code + ) + argred_dtype.record_reduction_op( + ty.ReductionOpKind.MAX, ids.argmax_redop_id + ) + argred_dtype.record_reduction_op( + ty.ReductionOpKind.MIN, ids.argmin_redop_id ) return argred_dtype @@ -138,13 +150,15 @@ def _report_coverage(self) -> None: implemented = sum(int(impl) for (_, _, impl) in self.api_calls) if total == 0: - print("cuNumeric API coverage: 0/0") + print("cuPyNumeric API coverage: 0/0") else: print( - f"cuNumeric API coverage: {implemented}/{total} " + f"cuPyNumeric API coverage: {implemented}/{total} " f"({implemented / total * 100}%)" ) + from .settings import settings + if (dump_csv := settings.report_dump_csv()) is not None: with open(dump_csv, "w") as f: print("function_name,location,implemented", file=f) @@ -152,46 +166,20 @@ def _report_coverage(self) -> None: print(f"{func_name},{loc},{impl}", file=f) def destroy(self) -> None: + from .settings import settings + assert not self.destroyed - if self.num_gpus > 0: - self._unload_cudalibs() if settings.report_coverage(): self._report_coverage() self.destroyed = True - def create_scalar( - self, - array: Union[memoryview, npt.NDArray[Any]], - shape: Optional[NdShape] = None, - ) -> Future: - data = array.tobytes() - buf = struct.pack(f"{len(data)}s", data) - return self.legate_runtime.create_future(buf, len(buf)) - - def create_wrapped_scalar( - self, - array: Union[memoryview, npt.NDArray[Any]], - dtype: np.dtype[Any], - shape: NdShape, - ) -> DeferredArray: - future = self.create_scalar(array, shape) - assert all(extent == 1 for extent in shape) - core_dtype = to_core_dtype(dtype) - store = self.legate_context.create_store( - core_dtype, - shape=shape, - storage=future, - optimize_scalar=True, - ) - return DeferredArray(self, store) - def bitgenerator_populate_task( self, - task: Union[AutoTask, ManualTask], + task: AutoTask | ManualTask, taskop: int, generatorID: int, generatorType: int = 0, - seed: Union[int, None] = 0, + seed: int | None = 0, flags: int = 0, ) -> None: task.add_scalar_arg(taskop, ty.int32) @@ -203,15 +191,16 @@ def bitgenerator_populate_task( def bitgenerator_create( self, generatorType: int, - seed: Union[int, None], + seed: int | None, flags: int, forceCreate: bool = False, ) -> int: self.current_random_bitgenid = self.current_random_bitgenid + 1 if forceCreate: - task = self.legate_context.create_manual_task( - CuNumericOpCode.BITGENERATOR, - launch_domain=Rect(lo=(0,), hi=(self.num_procs,)), + task = legate_runtime.create_manual_task( + self.library, + CuPyNumericOpCode.BITGENERATOR, + (self.num_procs,), ) self.bitgenerator_populate_task( task, @@ -226,7 +215,7 @@ def bitgenerator_create( ) self.current_random_bitgen_zombies = () task.execute() - self.legate_runtime.issue_execution_fence() + legate_runtime.issue_execution_fence() return self.current_random_bitgenid def bitgenerator_destroy( @@ -237,10 +226,11 @@ def bitgenerator_destroy( self.current_random_bitgen_zombies += (handle,) else: # with explicit destruction, do schedule a task - self.legate_runtime.issue_execution_fence() - task = self.legate_context.create_manual_task( - CuNumericOpCode.BITGENERATOR, - launch_domain=Rect(lo=(0,), hi=(self.num_procs,)), + legate_runtime.issue_execution_fence() + task = legate_runtime.create_manual_task( + self.library, + CuPyNumericOpCode.BITGENERATOR, + (self.num_procs,), ) self.bitgenerator_populate_task( task, BitGeneratorOperation.DESTROY, handle @@ -261,9 +251,9 @@ def get_next_random_epoch(self) -> int: def get_numpy_thunk( self, - obj: Union[ndarray, npt.NDArray[Any]], + obj: ndarray | npt.NDArray[Any], share: bool = False, - dtype: Optional[np.dtype[Any]] = None, + dtype: np.dtype[Any] | None = None, ) -> NumPyThunk: # Check to see if this object implements the Legate data interface if hasattr(obj, "__legate_data_interface__"): @@ -277,13 +267,14 @@ def get_numpy_thunk( raise ValueError("Legate data must be array-like") field = next(iter(data)) array = data[field] - stores = array.stores() - if len(stores) != 2: - raise ValueError("Legate data must be array-like") - if stores[0] is not None: - raise NotImplementedError("Need support for masked arrays") - store = stores[1] - return DeferredArray(self, store) + if array.nested or array.nullable: + raise NotImplementedError( + "Array must be non-nullable and not nested" + ) + + from ._thunk.deferred import DeferredArray + + return DeferredArray(array.data) # See if this is a normal numpy array # Make sure to convert numpy matrices to numpy arrays here # as the former doesn't behave quite like the latter @@ -292,21 +283,40 @@ def get_numpy_thunk( if share: obj = np.asarray(obj, dtype=dtype) else: + from ._array.array import ndarray + from ._module.array_joining import stack + + if ( + any( + ( + isinstance(obj, tuple), + isinstance(obj, list), + ) + ) + and len(obj) > 1 + and all( + (isinstance(o, ndarray) or isinstance(o, np.ndarray)) + for o in obj + ) + and math.prod(obj[0].shape) != 0 + ): + obj = stack(obj) # type: ignore + return obj._thunk obj = np.array(obj, dtype=dtype) elif dtype is not None and dtype != obj.dtype: obj = obj.astype(dtype) elif not share: obj = obj.copy() - return self.find_or_create_array_thunk(obj, share=share) - - def has_external_attachment(self, array: Any) -> bool: - assert array.base is None or not isinstance(array.base, np.ndarray) - return self.legate_runtime.has_attachment(array.data) + # We can't attach NumPy ndarrays in shared mode unless they are + # writeable + share = share and obj.flags["W"] + transfer = TransferType.SHARE if share else TransferType.MAKE_COPY + return self.find_or_create_array_thunk(obj, transfer) @staticmethod def compute_parent_child_mapping( array: npt.NDArray[Any], - ) -> Union[tuple[Union[slice, None], ...], None]: + ) -> tuple[slice | None, ...] | None: # We need an algorithm for figuring out how to compute the # slice object that was used to generate a child array from # a parent array so we can build the same mapping from a @@ -331,7 +341,7 @@ def compute_parent_child_mapping( offsets.append((ptr_diff % mod) // div) assert div == array.dtype.itemsize # Now build the view and dimmap for the parent to create the view - key: tuple[Union[slice, None], ...] = () + key: tuple[slice | None, ...] = () child_idx = 0 child_strides = tuple(array.strides) parent_strides = tuple(array.base.strides) @@ -375,102 +385,121 @@ def compute_parent_child_mapping( return key def find_or_create_array_thunk( - self, array: npt.NDArray[Any], share: bool = False, defer: bool = False + self, + array: npt.NDArray[Any], + transfer: TransferType, + read_only: bool = False, + defer: bool = False, ) -> NumPyThunk: + from ._thunk.deferred import DeferredArray + assert isinstance(array, np.ndarray) + if not is_supported_dtype(array.dtype): + raise TypeError( + f"cuPyNumeric does not support dtype={array.dtype}" + ) + # We have to be really careful here to handle the case of # aliased numpy arrays that are passed in from the application # In case of aliasing we need to make sure that they are # mapped to the same logical region. The way we handle this # is to always create the thunk for the root array and # then create sub-thunks that mirror the array views - if array.base is not None and isinstance(array.base, np.ndarray): + if ( + transfer == TransferType.SHARE + and array.base is not None + and isinstance(array.base, np.ndarray) + ): key = self.compute_parent_child_mapping(array) if key is None: # This base array wasn't made with a view - if not share: - return self.find_or_create_array_thunk( - array.copy(), - share=False, - defer=defer, - ) raise NotImplementedError( - "cuNumeric does not currently know " + "cuPyNumeric does not currently know " + "how to attach to array views that are not affine " + "transforms of their parent array." ) parent_thunk = self.find_or_create_array_thunk( array.base, - share=share, - defer=defer, + transfer, + read_only, + defer, ) - # Don't store this one in the ptr_to_thunk as we only want to - # store the root ones return parent_thunk.get_item(key) # Once it's a normal numpy array we can make it into one of our arrays # Check to see if it is a type that we support for doing deferred # execution and big enough to be worth off-loading onto Legion - dtype = to_core_dtype(array.dtype) - if ( - defer - or not self.is_eager_shape(array.shape) - or self.has_external_attachment(array) - ): - if array.size == 1 and not share: - # This is a single value array - # We didn't attach to this so we don't need to save it - return self.create_wrapped_scalar( - array.data, - array.dtype, - array.shape, - ) + if defer or not self.is_eager_shape(array.shape): + if array.size == 1 and transfer != TransferType.SHARE: + # This is a single value array that we're not attaching to. + # We cache these, but only if the user has promised not to + # write-through them. + # TODO(mpapadakis): Also mark the Store as read-only, whenever + # Legate supports that. + if read_only: + return cached_thunk_from_scalar( + array.tobytes(), array.shape, array.dtype + ) + else: + return thunk_from_scalar( + array.tobytes(), array.shape, array.dtype + ) - # This is not a scalar so make a field - store = self.legate_context.create_store( - dtype, - shape=array.shape, - optimize_scalar=False, - ) - store.attach_external_allocation( - array.data, - share, + # This is not a scalar so make a field. + # We won't try to cache these bigger arrays. + store = legate_runtime.create_store_from_buffer( + to_core_type(array.dtype), + array.shape, + array.copy() if transfer == TransferType.MAKE_COPY else array, + # This argument should really be called "donate" + read_only=(transfer != TransferType.SHARE), ) return DeferredArray( - self, store, - numpy_array=array if share else None, + numpy_array=( + array if transfer == TransferType.SHARE else None + ), ) - # Make this into an eager evaluated thunk - return EagerArray(self, array) + from ._thunk.eager import EagerArray + + # Make this into an eagerly evaluated thunk + return EagerArray( + array.copy() if transfer == TransferType.MAKE_COPY else array + ) def create_empty_thunk( self, shape: NdShape, - dtype: ty.Dtype, - inputs: Optional[Sequence[NumPyThunk]] = None, + dtype: ty.Type, + inputs: Sequence[NumPyThunk] | None = None, ) -> NumPyThunk: + from ._thunk.deferred import DeferredArray + if self.is_eager_shape(shape) and self.are_all_eager_inputs(inputs): return self.create_eager_thunk(shape, dtype.to_numpy_dtype()) - store = self.legate_context.create_store( + store = legate_runtime.create_store( dtype, shape=shape, optimize_scalar=True ) - return DeferredArray(self, store) + return DeferredArray(store) def create_eager_thunk( self, shape: NdShape, dtype: np.dtype[Any], ) -> NumPyThunk: - return EagerArray(self, np.empty(shape, dtype=dtype)) + from ._thunk.eager import EagerArray + + return EagerArray(np.empty(shape, dtype=dtype)) def create_unbound_thunk( - self, dtype: ty.Dtype, ndim: int = 1 + self, dtype: ty.Type, ndim: int = 1 ) -> DeferredArray: - store = self.legate_context.create_store(dtype, ndim=ndim) - return DeferredArray(self, store) + from ._thunk.deferred import DeferredArray + + store = legate_runtime.create_store(dtype, ndim=ndim) + return DeferredArray(store) def is_eager_shape(self, shape: NdShape) -> bool: volume = calculate_volume(shape) @@ -485,7 +514,9 @@ def is_eager_shape(self, shape: NdShape) -> bool: if len(shape) > LEGATE_MAX_DIM: return True - # CUNUMERIC_FORCE_THUNK == "eager" + from .settings import settings + + # CUPYNUMERIC_FORCE_THUNK == "eager" if settings.force_thunk() == "eager": return True @@ -500,7 +531,10 @@ def is_eager_shape(self, shape: NdShape) -> bool: return volume <= self.max_eager_volume @staticmethod - def are_all_eager_inputs(inputs: Optional[Sequence[NumPyThunk]]) -> bool: + def are_all_eager_inputs(inputs: Sequence[NumPyThunk] | None) -> bool: + from ._thunk.eager import EagerArray + from ._thunk.thunk import NumPyThunk + if inputs is None: return True for inp in inputs: @@ -511,35 +545,54 @@ def are_all_eager_inputs(inputs: Optional[Sequence[NumPyThunk]]) -> bool: @staticmethod def is_eager_array(array: NumPyThunk) -> TypeGuard[EagerArray]: + from ._thunk.eager import EagerArray + return isinstance(array, EagerArray) @staticmethod def is_deferred_array( - array: Optional[NumPyThunk], + array: NumPyThunk | None, ) -> TypeGuard[DeferredArray]: + from ._thunk.deferred import DeferredArray + return isinstance(array, DeferredArray) def to_eager_array(self, array: NumPyThunk) -> EagerArray: + from ._thunk.eager import EagerArray + if self.is_eager_array(array): return array elif self.is_deferred_array(array): - return EagerArray(self, array.__numpy_array__()) + return EagerArray(array.__numpy_array__()) else: raise RuntimeError("invalid array type") - def to_deferred_array(self, array: NumPyThunk) -> DeferredArray: + def to_deferred_array( + self, + array: NumPyThunk, + read_only: bool, + ) -> DeferredArray: if self.is_deferred_array(array): return array elif self.is_eager_array(array): - return array.to_deferred_array() + return array.to_deferred_array(read_only) else: raise RuntimeError("invalid array type") def warn(self, msg: str, category: type = UserWarning) -> None: + from .settings import settings + if not settings.warn(): return stacklevel = find_last_user_stacklevel() warnings.warn(msg, stacklevel=stacklevel, category=category) -runtime = Runtime(cunumeric_context) +runtime = Runtime() + + +def _shutdown_callback() -> None: + runtime.destroy() + + +legate_runtime.add_shutdown_callback(_shutdown_callback) diff --git a/cunumeric/settings.py b/cupynumeric/settings.py similarity index 75% rename from cunumeric/settings.py rename to cupynumeric/settings.py index 9faa52302..d73eee261 100644 --- a/cunumeric/settings.py +++ b/cupynumeric/settings.py @@ -1,4 +1,4 @@ -# Copyright 2023 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,21 +25,21 @@ __all__ = ("settings",) -class CunumericRuntimeSettings(Settings): +class CupynumericRuntimeSettings(Settings): preload_cudalibs: PrioritizedSetting[bool] = PrioritizedSetting( "preload_cudalibs", - "CUNUMERIC_PRELOAD_CUDALIBS", + "CUPYNUMERIC_PRELOAD_CUDALIBS", default=False, convert=convert_bool, help=""" Preload and initialize handles of all CUDA libraries (cuBLAS, cuSOLVER, - etc.) used in cuNumeric. + etc.) used in cuPyNumeric. """, ) warn: PrioritizedSetting[bool] = PrioritizedSetting( "warn", - "CUNUMERIC_WARN", + "CUPYNUMERIC_WARN", default=False, convert=convert_bool, help=""" @@ -49,27 +49,27 @@ class CunumericRuntimeSettings(Settings): report_coverage: PrioritizedSetting[bool] = PrioritizedSetting( "report_coverage", - "CUNUMERIC_REPORT_COVERAGE", + "CUPYNUMERIC_REPORT_COVERAGE", default=False, convert=convert_bool, help=""" - Print an overall percentage of cunumeric coverage. + Print an overall percentage of cupynumeric coverage. """, ) report_dump_callstack: PrioritizedSetting[bool] = PrioritizedSetting( "report_dump_callstack", - "CUNUMERIC_REPORT_DUMP_CALLSTACK", + "CUPYNUMERIC_REPORT_DUMP_CALLSTACK", default=False, convert=convert_bool, help=""" - Print an overall percentage of cunumeric coverage with call stack info. + Print an overall percentage of cupynumeric coverage with a call stack. """, ) report_dump_csv: PrioritizedSetting[str | None] = PrioritizedSetting( "report_dump_csv", - "CUNUMERIC_REPORT_DUMP_CSV", + "CUPYNUMERIC_REPORT_DUMP_CSV", default=None, help=""" Save a coverage report to a specified CSV file. @@ -78,11 +78,11 @@ class CunumericRuntimeSettings(Settings): numpy_compat: PrioritizedSetting[bool] = PrioritizedSetting( "numpy_compat", - "CUNUMERIC_NUMPY_COMPATIBILITY", + "CUPYNUMERIC_NUMPY_COMPATIBILITY", default=False, convert=convert_bool, help=""" - cuNumeric will issue additional tasks to match numpy's results + cuPyNumeric will issue additional tasks to match numpy's results and behavior. This is currently used in the following APIs: nanmin, nanmax, nanargmin, nanargmax """, @@ -90,7 +90,7 @@ class CunumericRuntimeSettings(Settings): fast_math: EnvOnlySetting[int] = EnvOnlySetting( "fast_math", - "CUNUMERIC_FAST_MATH", + "CUPYNUMERIC_FAST_MATH", default=False, convert=convert_bool, help=""" @@ -105,7 +105,7 @@ class CunumericRuntimeSettings(Settings): min_gpu_chunk: EnvOnlySetting[int] = EnvOnlySetting( "min_gpu_chunk", - "CUNUMERIC_MIN_GPU_CHUNK", + "CUPYNUMERIC_MIN_GPU_CHUNK", default=65536, # 1 << 16 test_default=2, convert=convert_int, @@ -121,7 +121,7 @@ class CunumericRuntimeSettings(Settings): min_cpu_chunk: EnvOnlySetting[int] = EnvOnlySetting( "min_cpu_chunk", - "CUNUMERIC_MIN_CPU_CHUNK", + "CUPYNUMERIC_MIN_CPU_CHUNK", default=1024, # 1 << 10 test_default=2, convert=convert_int, @@ -137,7 +137,7 @@ class CunumericRuntimeSettings(Settings): min_omp_chunk: EnvOnlySetting[int] = EnvOnlySetting( "min_omp_chunk", - "CUNUMERIC_MIN_OMP_CHUNK", + "CUPYNUMERIC_MIN_OMP_CHUNK", default=8192, # 1 << 13 test_default=2, convert=convert_int, @@ -153,15 +153,15 @@ class CunumericRuntimeSettings(Settings): force_thunk: EnvOnlySetting[str | None] = EnvOnlySetting( "force_thunk", - "CUNUMERIC_FORCE_THUNK", + "CUPYNUMERIC_FORCE_THUNK", default=None, test_default="deferred", help=""" - Force cuNumeric to always use a specific strategy for backing + Force cuPyNumeric to always use a specific strategy for backing ndarrays: "deferred", i.e. managed by the Legate runtime, which enables distribution and accelerated operations, but has some up-front offloading overhead, or "eager", i.e. falling back to - using a vanilla NumPy array. By default cuNumeric will decide + using a vanilla NumPy array. By default cuPyNumeric will decide this on a per-array basis, based on the size of the array and the accelerator in use. @@ -169,5 +169,21 @@ class CunumericRuntimeSettings(Settings): """, ) + matmul_cache_size: EnvOnlySetting[int] = EnvOnlySetting( + "matmul_cache_size", + "CUPYNUMERIC_MATMUL_CACHE_SIZE", + default=134217728, # 128MB + test_default=4096, # 4KB + convert=convert_int, + help=""" + Force cuPyNumeric to keep temporary task slices during matmul + computations smaller than this threshold. Whenever the temporary + space needed during computation would exceed this value the task + will be batched over 'k' to fulfill the requirement. + + This is a read-only environment variable setting used by the runtime. + """, + ) + -settings = CunumericRuntimeSettings() +settings = CupynumericRuntimeSettings() diff --git a/cunumeric/types.py b/cupynumeric/types.py similarity index 83% rename from cunumeric/types.py rename to cupynumeric/types.py index 1e3d032b0..f2fbf8311 100644 --- a/cunumeric/types.py +++ b/cupynumeric/types.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,17 +14,15 @@ # from __future__ import annotations -from typing import Literal, Tuple, Union - -from typing_extensions import TypeAlias +from typing import Literal, TypeAlias BoundsMode: TypeAlias = Literal["raise", "wrap", "clip"] CastingKind: TypeAlias = Literal["no", "equiv", "safe", "same_kind", "unsafe"] -NdShape: TypeAlias = Tuple[int, ...] +NdShape: TypeAlias = tuple[int, ...] -NdShapeLike: TypeAlias = Union[int, NdShape] +NdShapeLike: TypeAlias = int | NdShape SortSide: TypeAlias = Literal["left", "right"] @@ -36,4 +34,6 @@ ConvolveMode: TypeAlias = Literal["full", "valid", "same"] +ConvolveMethod: TypeAlias = Literal["auto", "direct", "fft"] + SelectKind: TypeAlias = Literal["introselect"] diff --git a/cupynumeric_cpp.cmake b/cupynumeric_cpp.cmake new file mode 100644 index 000000000..f9d7cbb01 --- /dev/null +++ b/cupynumeric_cpp.cmake @@ -0,0 +1,565 @@ +#============================================================================= +# Copyright 2024 NVIDIA Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#============================================================================= + +############################################################################## +# - User Options ------------------------------------------------------------ + +option(BUILD_SHARED_LIBS "Build cuPyNumeric shared libraries" ON) +option(cupynumeric_EXCLUDE_TBLIS_FROM_ALL "Exclude tblis targets from cuPyNumeric's 'all' target" OFF) +option(cupynumeric_EXCLUDE_OPENBLAS_FROM_ALL "Exclude OpenBLAS targets from cuPyNumeric's 'all' target" OFF) +option(cupynumeric_EXCLUDE_LEGATE_FROM_ALL "Exclude legate targets from cuPyNumeric's 'all' target" OFF) + +############################################################################## +# - Project definition ------------------------------------------------------- + +# Write the version header +rapids_cmake_write_version_file(include/cupynumeric/version_config.hpp) + +# Needed to integrate with LLVM/clang tooling +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +############################################################################## +# - Build Type --------------------------------------------------------------- + +# Set a default build type if none was specified +rapids_cmake_build_type(Release) + +############################################################################## +# - conda environment -------------------------------------------------------- + +rapids_cmake_support_conda_env(conda_env MODIFY_PREFIX_PATH) + +# We're building python extension libraries, which must always be installed +# under lib/, even if the system normally uses lib64/. Rapids-cmake currently +# doesn't realize this when we're going through scikit-build, see +# https://github.com/rapidsai/rapids-cmake/issues/426 +if(TARGET conda_env) + set(CMAKE_INSTALL_LIBDIR "lib") +endif() + +############################################################################## +# - Dependencies ------------------------------------------------------------- + +# add third party dependencies using CPM +rapids_cpm_init(OVERRIDE ${CMAKE_CURRENT_SOURCE_DIR}/cmake/versions.json) + +rapids_find_package(OpenMP GLOBAL_TARGETS OpenMP::OpenMP_CXX) + +option(Legion_USE_CUDA "Use CUDA" ON) +option(Legion_USE_OpenMP "Use OpenMP" ${OpenMP_FOUND}) +option(Legion_BOUNDS_CHECKS "Build cuPyNumeric with bounds checks (expensive)" OFF) + +# If legate has CUDA support, then including it in a project will automatically call +# enable_language(CUDA). However, this does not play nice with the rapids-cmake CUDA utils +# which support a wider range of values for CMAKE_CUDA_ARCHITECTURES than cmake does. You +# end up with the following error: +# +# CMAKE_CUDA_ARCHITECTURES: +# +# RAPIDS +# +# is not one of the following: +# +# * a semicolon-separated list of integers, each optionally +# followed by '-real' or '-virtual' +# * a special value: all, all-major, native +# +set(cmake_cuda_arch_backup "${CMAKE_CUDA_ARCHITECTURES}") +set(cmake_cuda_arch_cache_backup "$CACHE{CMAKE_CUDA_ARCHITECTURES}") +if(("${CMAKE_CUDA_ARCHITECTURES}" STREQUAL "RAPIDS") OR ("${CMAKE_CUDA_ARCHITECTURES}" STREQUAL "NATIVE")) + unset(CMAKE_CUDA_ARCHITECTURES) + unset(CMAKE_CUDA_ARCHITECTURES CACHE) +endif() + +### +# If we find legate already configured on the system, it will report +# whether it was compiled with bounds checking (Legion_BOUNDS_CHECKS), +# CUDA (Legion_USE_CUDA), and OpenMP (Legion_USE_OpenMP). +# +# We use the same variables as legate because we want to enable/disable +# each of these features based on how legate was configured (it doesn't +# make sense to build cuPyNumeric's CUDA bindings if legate wasn't built +# with CUDA support). +### +include(cmake/thirdparty/get_legate.cmake) + +set(CMAKE_CUDA_ARCHITECTURES "${cmake_cuda_arch_cache_backup}" CACHE STRING "" FORCE) +set(CMAKE_CUDA_ARCHITECTURES "${cmake_cuda_arch_backup}") +unset(cmake_cuda_arch_backup) +unset(cmake_cuda_arch_cache_backup) + +if(Legion_USE_CUDA) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/cuda_arch_helpers.cmake) + # Needs to run before `rapids_cuda_init_architectures` + set_cuda_arch_from_names() + # Needs to run before `enable_language(CUDA)` + rapids_cuda_init_architectures(cupynumeric) + enable_language(CUDA) + # Since cupynumeric only enables CUDA optionally we need to manually include + # the file that rapids_cuda_init_architectures relies on `project` calling + if(CMAKE_PROJECT_cupynumeric_INCLUDE) + include("${CMAKE_PROJECT_cupynumeric_INCLUDE}") + endif() + + # Must come after enable_language(CUDA) + # Use `-isystem ` instead of `-isystem=` + # because the former works with clangd intellisense + set(CMAKE_INCLUDE_SYSTEM_FLAG_CUDA "-isystem ") + + rapids_find_package( + CUDAToolkit REQUIRED + BUILD_EXPORT_SET cupynumeric-exports + INSTALL_EXPORT_SET cupynumeric-exports + ) + + include(cmake/thirdparty/get_nccl.cmake) + include(cmake/thirdparty/get_cutensor.cmake) +endif() + +include(cmake/thirdparty/get_openblas.cmake) + +include(cmake/thirdparty/get_tblis.cmake) + +############################################################################## +# - cuPyNumeric ---------------------------------------------------------------- + +set(cupynumeric_SOURCES "") +set(cupynumeric_CXX_DEFS "") +set(cupynumeric_CUDA_DEFS "") +set(cupynumeric_CXX_OPTIONS "") +set(cupynumeric_CUDA_OPTIONS "") + +include(cmake/Modules/set_cpu_arch_flags.cmake) +set_cpu_arch_flags(cupynumeric_CXX_OPTIONS) + +# Add `src/cupynumeric.mk` sources +list(APPEND cupynumeric_SOURCES + src/cupynumeric/ternary/where.cc + src/cupynumeric/scan/scan_global.cc + src/cupynumeric/scan/scan_local.cc + src/cupynumeric/binary/binary_op.cc + src/cupynumeric/binary/binary_op_util.cc + src/cupynumeric/binary/binary_red.cc + src/cupynumeric/bits/packbits.cc + src/cupynumeric/bits/unpackbits.cc + src/cupynumeric/unary/scalar_unary_red.cc + src/cupynumeric/unary/unary_op.cc + src/cupynumeric/unary/unary_red.cc + src/cupynumeric/unary/convert.cc + src/cupynumeric/nullary/arange.cc + src/cupynumeric/nullary/eye.cc + src/cupynumeric/nullary/fill.cc + src/cupynumeric/nullary/window.cc + src/cupynumeric/index/advanced_indexing.cc + src/cupynumeric/index/choose.cc + src/cupynumeric/index/putmask.cc + src/cupynumeric/index/repeat.cc + src/cupynumeric/index/select.cc + src/cupynumeric/index/wrap.cc + src/cupynumeric/index/zip.cc + src/cupynumeric/item/read.cc + src/cupynumeric/item/write.cc + src/cupynumeric/matrix/batched_cholesky.cc + src/cupynumeric/matrix/contract.cc + src/cupynumeric/matrix/diag.cc + src/cupynumeric/matrix/gemm.cc + src/cupynumeric/matrix/matmul.cc + src/cupynumeric/matrix/matvecmul.cc + src/cupynumeric/matrix/dot.cc + src/cupynumeric/matrix/potrf.cc + src/cupynumeric/matrix/qr.cc + src/cupynumeric/matrix/solve.cc + src/cupynumeric/matrix/svd.cc + src/cupynumeric/matrix/syrk.cc + src/cupynumeric/matrix/tile.cc + src/cupynumeric/matrix/transpose.cc + src/cupynumeric/matrix/trilu.cc + src/cupynumeric/matrix/trsm.cc + src/cupynumeric/matrix/util.cc + src/cupynumeric/random/bitgenerator.cc + src/cupynumeric/random/randutil/generator_host.cc + src/cupynumeric/random/randutil/generator_host_straightforward.cc + src/cupynumeric/random/randutil/generator_host_advanced.cc + src/cupynumeric/random/rand.cc + src/cupynumeric/search/argwhere.cc + src/cupynumeric/search/nonzero.cc + src/cupynumeric/set/unique.cc + src/cupynumeric/set/unique_reduce.cc + src/cupynumeric/stat/bincount.cc + src/cupynumeric/convolution/convolve.cc + src/cupynumeric/transform/flip.cc + src/cupynumeric/utilities/repartition.cc + src/cupynumeric/arg_redop_register.cc + src/cupynumeric/mapper.cc + src/cupynumeric/ndarray.cc + src/cupynumeric/operators.cc + src/cupynumeric/runtime.cc + src/cupynumeric/cephes/chbevl.cc + src/cupynumeric/cephes/i0.cc + src/cupynumeric/stat/histogram.cc +) + +if(Legion_USE_OpenMP) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/ternary/where_omp.cc + src/cupynumeric/scan/scan_global_omp.cc + src/cupynumeric/scan/scan_local_omp.cc + src/cupynumeric/binary/binary_op_omp.cc + src/cupynumeric/binary/binary_red_omp.cc + src/cupynumeric/bits/packbits_omp.cc + src/cupynumeric/bits/unpackbits_omp.cc + src/cupynumeric/unary/unary_op_omp.cc + src/cupynumeric/unary/scalar_unary_red_omp.cc + src/cupynumeric/unary/unary_red_omp.cc + src/cupynumeric/unary/convert_omp.cc + src/cupynumeric/nullary/arange_omp.cc + src/cupynumeric/nullary/eye_omp.cc + src/cupynumeric/nullary/fill_omp.cc + src/cupynumeric/nullary/window_omp.cc + src/cupynumeric/index/advanced_indexing_omp.cc + src/cupynumeric/index/choose_omp.cc + src/cupynumeric/index/putmask_omp.cc + src/cupynumeric/index/repeat_omp.cc + src/cupynumeric/index/select_omp.cc + src/cupynumeric/index/wrap_omp.cc + src/cupynumeric/index/zip_omp.cc + src/cupynumeric/matrix/batched_cholesky_omp.cc + src/cupynumeric/matrix/contract_omp.cc + src/cupynumeric/matrix/diag_omp.cc + src/cupynumeric/matrix/gemm_omp.cc + src/cupynumeric/matrix/matmul_omp.cc + src/cupynumeric/matrix/matvecmul_omp.cc + src/cupynumeric/matrix/dot_omp.cc + src/cupynumeric/matrix/potrf_omp.cc + src/cupynumeric/matrix/qr_omp.cc + src/cupynumeric/matrix/solve_omp.cc + src/cupynumeric/matrix/svd_omp.cc + src/cupynumeric/matrix/syrk_omp.cc + src/cupynumeric/matrix/tile_omp.cc + src/cupynumeric/matrix/transpose_omp.cc + src/cupynumeric/matrix/trilu_omp.cc + src/cupynumeric/matrix/trsm_omp.cc + src/cupynumeric/random/rand_omp.cc + src/cupynumeric/search/argwhere_omp.cc + src/cupynumeric/search/nonzero_omp.cc + src/cupynumeric/set/unique_omp.cc + src/cupynumeric/set/unique_reduce_omp.cc + src/cupynumeric/stat/bincount_omp.cc + src/cupynumeric/convolution/convolve_omp.cc + src/cupynumeric/transform/flip_omp.cc + src/cupynumeric/stat/histogram_omp.cc + ) +endif() + +if(Legion_USE_CUDA) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/ternary/where.cu + src/cupynumeric/scan/scan_global.cu + src/cupynumeric/scan/scan_local.cu + src/cupynumeric/binary/binary_op.cu + src/cupynumeric/binary/binary_red.cu + src/cupynumeric/bits/packbits.cu + src/cupynumeric/bits/unpackbits.cu + src/cupynumeric/unary/scalar_unary_red.cu + src/cupynumeric/unary/unary_red.cu + src/cupynumeric/unary/unary_op.cu + src/cupynumeric/unary/convert.cu + src/cupynumeric/nullary/arange.cu + src/cupynumeric/nullary/eye.cu + src/cupynumeric/nullary/fill.cu + src/cupynumeric/nullary/window.cu + src/cupynumeric/index/advanced_indexing.cu + src/cupynumeric/index/choose.cu + src/cupynumeric/index/putmask.cu + src/cupynumeric/index/repeat.cu + src/cupynumeric/index/select.cu + src/cupynumeric/index/wrap.cu + src/cupynumeric/index/zip.cu + src/cupynumeric/item/read.cu + src/cupynumeric/item/write.cu + src/cupynumeric/matrix/batched_cholesky.cu + src/cupynumeric/matrix/contract.cu + src/cupynumeric/matrix/diag.cu + src/cupynumeric/matrix/gemm.cu + src/cupynumeric/matrix/matmul.cu + src/cupynumeric/matrix/matvecmul.cu + src/cupynumeric/matrix/dot.cu + src/cupynumeric/matrix/potrf.cu + src/cupynumeric/matrix/qr.cu + src/cupynumeric/matrix/solve.cu + src/cupynumeric/matrix/svd.cu + src/cupynumeric/matrix/syrk.cu + src/cupynumeric/matrix/tile.cu + src/cupynumeric/matrix/transpose.cu + src/cupynumeric/matrix/trilu.cu + src/cupynumeric/matrix/trsm.cu + src/cupynumeric/random/rand.cu + src/cupynumeric/search/argwhere.cu + src/cupynumeric/search/nonzero.cu + src/cupynumeric/set/unique.cu + src/cupynumeric/stat/bincount.cu + src/cupynumeric/convolution/convolve.cu + src/cupynumeric/fft/fft.cu + src/cupynumeric/transform/flip.cu + src/cupynumeric/utilities/repartition.cu + src/cupynumeric/arg_redop_register.cu + src/cupynumeric/cudalibs.cu + src/cupynumeric/stat/histogram.cu + ) +endif() + +# Add `src/cupynumeric/sort/sort.mk` sources +list(APPEND cupynumeric_SOURCES + src/cupynumeric/sort/sort.cc + src/cupynumeric/sort/searchsorted.cc +) + +if(Legion_USE_OpenMP) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/sort/sort_omp.cc + src/cupynumeric/sort/searchsorted_omp.cc + ) +endif() + +if(Legion_USE_CUDA) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/sort/sort.cu + src/cupynumeric/sort/searchsorted.cu + src/cupynumeric/sort/cub_sort_bool.cu + src/cupynumeric/sort/cub_sort_int8.cu + src/cupynumeric/sort/cub_sort_int16.cu + src/cupynumeric/sort/cub_sort_int32.cu + src/cupynumeric/sort/cub_sort_int64.cu + src/cupynumeric/sort/cub_sort_uint8.cu + src/cupynumeric/sort/cub_sort_uint16.cu + src/cupynumeric/sort/cub_sort_uint32.cu + src/cupynumeric/sort/cub_sort_uint64.cu + src/cupynumeric/sort/cub_sort_half.cu + src/cupynumeric/sort/cub_sort_float.cu + src/cupynumeric/sort/cub_sort_double.cu + src/cupynumeric/sort/thrust_sort_bool.cu + src/cupynumeric/sort/thrust_sort_int8.cu + src/cupynumeric/sort/thrust_sort_int16.cu + src/cupynumeric/sort/thrust_sort_int32.cu + src/cupynumeric/sort/thrust_sort_int64.cu + src/cupynumeric/sort/thrust_sort_uint8.cu + src/cupynumeric/sort/thrust_sort_uint16.cu + src/cupynumeric/sort/thrust_sort_uint32.cu + src/cupynumeric/sort/thrust_sort_uint64.cu + src/cupynumeric/sort/thrust_sort_half.cu + src/cupynumeric/sort/thrust_sort_float.cu + src/cupynumeric/sort/thrust_sort_double.cu + src/cupynumeric/sort/thrust_sort_complex64.cu + src/cupynumeric/sort/thrust_sort_complex128.cu + ) +endif() + +# Add `src/cupynumeric/random/random.mk` sources +if(Legion_USE_CUDA) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/random/bitgenerator.cu + src/cupynumeric/random/randutil/generator_device.cu + src/cupynumeric/random/randutil/generator_device_straightforward.cu + src/cupynumeric/random/randutil/generator_device_advanced.cu +) +endif() + +# add sources for cusolverMp +if(Legion_USE_CUDA AND CUSOLVERMP_DIR) + list(APPEND cupynumeric_SOURCES + src/cupynumeric/matrix/mp_potrf.cu + src/cupynumeric/matrix/mp_solve.cu + ) +endif() + +list(APPEND cupynumeric_SOURCES + # This must always be the last file! + # It guarantees we do our registration callback + # only after all task variants are recorded + src/cupynumeric/cupynumeric.cc +) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND cupynumeric_CXX_DEFS DEBUG_CUPYNUMERIC) + list(APPEND cupynumeric_CUDA_DEFS DEBUG_CUPYNUMERIC) +endif() + +if(Legion_BOUNDS_CHECKS) + list(APPEND cupynumeric_CXX_DEFS BOUNDS_CHECKS) + list(APPEND cupynumeric_CUDA_DEFS BOUNDS_CHECKS) +endif() + +list(APPEND cupynumeric_CUDA_OPTIONS -Xfatbin=-compress-all) +list(APPEND cupynumeric_CUDA_OPTIONS --expt-extended-lambda) +list(APPEND cupynumeric_CUDA_OPTIONS --expt-relaxed-constexpr) +list(APPEND cupynumeric_CXX_OPTIONS -Wno-deprecated-declarations) +list(APPEND cupynumeric_CUDA_OPTIONS -Wno-deprecated-declarations) + +add_library(cupynumeric ${cupynumeric_SOURCES}) +add_library(cupynumeric::cupynumeric ALIAS cupynumeric) + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(platform_rpath_origin "\$ORIGIN") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(platform_rpath_origin "@loader_path") +endif () + +set_target_properties(cupynumeric + PROPERTIES BUILD_RPATH "${platform_rpath_origin}" + INSTALL_RPATH "${platform_rpath_origin}" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + INTERFACE_POSITION_INDEPENDENT_CODE ON + CUDA_STANDARD 17 + CUDA_STANDARD_REQUIRED ON + LIBRARY_OUTPUT_DIRECTORY lib) + +target_link_libraries(cupynumeric + PUBLIC legate::legate + $ + PRIVATE BLAS::BLAS + tblis::tblis + # Add Conda library and include paths + $ + $ + $ + $ + $ + $) + +if(NOT Legion_USE_CUDA AND cupynumeric_cuRAND_INCLUDE_DIR) + list(APPEND cupynumeric_CXX_DEFS CUPYNUMERIC_CURAND_FOR_CPU_BUILD) + target_include_directories(cupynumeric PRIVATE ${cupynumeric_cuRAND_INCLUDE_DIR}) +endif() + +if(Legion_USE_CUDA AND CUSOLVERMP_DIR) + message(VERBOSE "cupynumeric: CUSOLVERMP_DIR ${CUSOLVERMP_DIR}") + list(APPEND cupynumeric_CXX_DEFS CUPYNUMERIC_USE_CUSOLVERMP) + list(APPEND cupynumeric_CUDA_DEFS CUPYNUMERIC_USE_CUSOLVERMP) + target_include_directories(cupynumeric PRIVATE ${CUSOLVERMP_DIR}/include) + target_link_libraries(cupynumeric PRIVATE ${CUSOLVERMP_DIR}/lib/libcusolverMp.so) +endif() + +target_compile_options(cupynumeric + PRIVATE "$<$:${cupynumeric_CXX_OPTIONS}>" + "$<$:${cupynumeric_CUDA_OPTIONS}>") + +target_compile_definitions(cupynumeric + PUBLIC "$<$:${cupynumeric_CXX_DEFS}>" + "$<$:${cupynumeric_CUDA_DEFS}>") + +target_include_directories(cupynumeric + PUBLIC + $ + INTERFACE + $ +) + +if(Legion_USE_CUDA) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld" +[=[ +SECTIONS +{ +.nvFatBinSegment : { *(.nvFatBinSegment) } +.nv_fatbin : { *(.nv_fatbin) } +} +]=]) + + # ensure CUDA symbols aren't relocated to the middle of the debug build binaries + target_link_options(cupynumeric PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld") +endif() + +############################################################################## +# - install targets----------------------------------------------------------- + +include(CPack) +include(GNUInstallDirs) +rapids_cmake_install_lib_dir(lib_dir) + +install(TARGETS cupynumeric + DESTINATION ${lib_dir} + EXPORT cupynumeric-exports) + +install( + FILES src/cupynumeric.h + ${CMAKE_CURRENT_BINARY_DIR}/include/cupynumeric/version_config.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cupynumeric) + +install( + FILES src/cupynumeric/cupynumeric_c.h + src/cupynumeric/ndarray.h + src/cupynumeric/ndarray.inl + src/cupynumeric/operators.h + src/cupynumeric/operators.inl + src/cupynumeric/runtime.h + src/cupynumeric/slice.h + src/cupynumeric/typedefs.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cupynumeric/cupynumeric) + +if(cupynumeric_INSTALL_TBLIS) + install(DIRECTORY ${tblis_BINARY_DIR}/lib/ DESTINATION ${lib_dir}) + install(DIRECTORY ${tblis_BINARY_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +endif() + +############################################################################## +# - install export ----------------------------------------------------------- + +set(doc_string + [=[ +Provide targets for cuPyNumeric, an aspiring drop-in replacement for NumPy at scale. + +Imported Targets: + - cupynumeric::cupynumeric + +]=]) + +string(JOIN "\n" code_string + "set(Legion_USE_CUDA ${Legion_USE_CUDA})" + "set(Legion_USE_OpenMP ${Legion_USE_OpenMP})" + "set(Legion_BOUNDS_CHECKS ${Legion_BOUNDS_CHECKS})" +) + +if(DEFINED Legion_USE_Python) + string(APPEND code_string "\nset(Legion_USE_Python ${Legion_USE_Python})") +endif() + +if(DEFINED Legion_NETWORKS) + string(APPEND code_string "\nset(Legion_NETWORKS ${Legion_NETWORKS})") +endif() + +rapids_export( + INSTALL cupynumeric + EXPORT_SET cupynumeric-exports + GLOBAL_TARGETS cupynumeric + NAMESPACE cupynumeric:: + DOCUMENTATION doc_string + FINAL_CODE_BLOCK code_string) + +# build export targets +rapids_export( + BUILD cupynumeric + EXPORT_SET cupynumeric-exports + GLOBAL_TARGETS cupynumeric + NAMESPACE cupynumeric:: + DOCUMENTATION doc_string + FINAL_CODE_BLOCK code_string) + +if(cupynumeric_BUILD_TESTS) + include(CTest) + + add_subdirectory(tests/cpp) +endif() diff --git a/cunumeric_python.cmake b/cupynumeric_python.cmake similarity index 68% rename from cunumeric_python.cmake rename to cupynumeric_python.cmake index 8139d6d3a..1be5b35c6 100644 --- a/cunumeric_python.cmake +++ b/cupynumeric_python.cmake @@ -1,5 +1,5 @@ #============================================================================= -# Copyright 2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,25 +17,25 @@ ############################################################################## # - User Options ------------------------------------------------------------ -option(FIND_CUNUMERIC_CPP "Search for existing cuNumeric C++ installations before defaulting to local files" +option(FIND_CUPYNUMERIC_CPP "Search for existing cuPyNumeric C++ installations before defaulting to local files" OFF) ############################################################################## # - Dependencies ------------------------------------------------------------- -# If the user requested it we attempt to find cunumeric. -if(FIND_CUNUMERIC_CPP) +# If the user requested it we attempt to find cupynumeric. +if(FIND_CUPYNUMERIC_CPP) include("${rapids-cmake-dir}/export/detail/parse_version.cmake") - rapids_export_parse_version(${cunumeric_version} cunumeric parsed_ver) - rapids_find_package(cunumeric ${parsed_ver} EXACT CONFIG - GLOBAL_TARGETS cunumeric::cunumeric - BUILD_EXPORT_SET cunumeric-python-exports - INSTALL_EXPORT_SET cunumeric-python-exports) + rapids_export_parse_version(${cupynumeric_version} cupynumeric parsed_ver) + rapids_find_package(cupynumeric ${parsed_ver} EXACT CONFIG + GLOBAL_TARGETS cupynumeric::cupynumeric + BUILD_EXPORT_SET cupynumeric-python-exports + INSTALL_EXPORT_SET cupynumeric-python-exports) else() - set(cunumeric_FOUND OFF) + set(cupynumeric_FOUND OFF) endif() -if(NOT cunumeric_FOUND) +if(NOT cupynumeric_FOUND) set(SKBUILD OFF) set(Legion_USE_Python ON) set(Legion_BUILD_BINDINGS ON) @@ -51,9 +51,9 @@ add_custom_target("generate_install_info_py" ALL VERBATIM ) -add_library(cunumeric_python INTERFACE) -add_library(cunumeric::cunumeric_python ALIAS cunumeric_python) -target_link_libraries(cunumeric_python INTERFACE legate::core) +add_library(cupynumeric_python INTERFACE) +add_library(cupynumeric::cupynumeric_python ALIAS cupynumeric_python) +target_link_libraries(cupynumeric_python INTERFACE legate::legate) # ############################################################################ # - conda environment -------------------------------------------------------- @@ -75,37 +75,37 @@ include(CPack) include(GNUInstallDirs) rapids_cmake_install_lib_dir(lib_dir) -install(TARGETS cunumeric_python +install(TARGETS cupynumeric_python DESTINATION ${lib_dir} - EXPORT cunumeric-python-exports) + EXPORT cupynumeric-python-exports) ############################################################################## # - install export ----------------------------------------------------------- set(doc_string [=[ -Provide Python targets for cuNumeric, an aspiring drop-in replacement for NumPy at scale. +Provide Python targets for cuPyNumeric, an aspiring drop-in replacement for NumPy at scale. Imported Targets: - - cunumeric::cunumeric_python + - cupynumeric::cupynumeric_python ]=]) set(code_string "") rapids_export( - INSTALL cunumeric_python - EXPORT_SET cunumeric-python-exports - GLOBAL_TARGETS cunumeric_python - NAMESPACE cunumeric:: + INSTALL cupynumeric_python + EXPORT_SET cupynumeric-python-exports + GLOBAL_TARGETS cupynumeric_python + NAMESPACE cupynumeric:: DOCUMENTATION doc_string FINAL_CODE_BLOCK code_string) # build export targets rapids_export( - BUILD cunumeric_python - EXPORT_SET cunumeric-python-exports - GLOBAL_TARGETS cunumeric_python - NAMESPACE cunumeric:: + BUILD cupynumeric_python + EXPORT_SET cupynumeric-python-exports + GLOBAL_TARGETS cupynumeric_python + NAMESPACE cupynumeric:: DOCUMENTATION doc_string FINAL_CODE_BLOCK code_string) diff --git a/docs/cunumeric/source/api/settings.rst b/docs/cunumeric/source/api/settings.rst deleted file mode 100644 index abc807f0b..000000000 --- a/docs/cunumeric/source/api/settings.rst +++ /dev/null @@ -1,8 +0,0 @@ -Settings -======== - -cuNumeric has a number of runtime settings that can be configured through -environment variables. - -.. settings:: settings - :module: cunumeric.settings \ No newline at end of file diff --git a/docs/cunumeric/source/developer/CONTRIBUTING.md b/docs/cunumeric/source/developer/CONTRIBUTING.md deleted file mode 120000 index 069558fad..000000000 --- a/docs/cunumeric/source/developer/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -../../../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/cunumeric/source/developer/building.rst b/docs/cunumeric/source/developer/building.rst deleted file mode 100644 index 674c9f34f..000000000 --- a/docs/cunumeric/source/developer/building.rst +++ /dev/null @@ -1,71 +0,0 @@ -.. _building cunumeric from source: - -Building from source -==================== - -Basic build ------------ - -Users must have a working installation of the `Legate Core`_ library prior to -installing cuNumeric. -**Installing cuNumeric by itself will not automatically install Legate Core.** - -As for other dependencies, the Dependencies section on the -`Legate Core build instructions`_ also covers cuNumeric, so no additional -packages are required. - -Once Legate Core is installed, you can simply invoke ``./install.py`` from the -cuNumeric top-level directory. The build will automatically pick up the -configuration used when building Legate Core (e.g. the CUDA Toolkit directory). - -Advanced topics ---------------- - -Building through pip & cmake -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -cuNumeric uses the same cmake/scikit-build-based build workflow as Legate Core. -See the `Legate Core build instructions`_ for an overview. - -There are several examples in the ``scripts`` folder. We walk through the steps in -``build-with-legate-separately-no-install.sh`` here. - -We assume a pre-existing Legate Core build. For details on building Legate Core, -consult the `Legate Core repository`_. - -First, the CMake build needs to be configured: - -.. code:: sh - - $ cmake -S . -B build -GNinja -D legate_core_ROOT:STRING=path/to/legate/build - -We point cuNumeric to the Legate *build* tree, not an installation. -This generates all build-dependent headers and Python files. - -Once configured, we can build the C++ libraries: - -.. code:: sh - - $ cmake --build build - -This will invoke Ninja (or make) to execute the build. -Once the C++ libraries are available, we can do an editable (development) pip installation. - -.. code:: sh - - $ SKBUILD_BUILD_OPTIONS="-D FIND_CUNUMERIC_CPP=ON -D cunumeric_ROOT=$(pwd)/build" \ - python3 -m pip install \ - --root / --no-deps --no-build-isolation - --editable . - -The Python source tree and CMake build tree are now available with the environment Python -for running cuNumeric programs. The diagram below illustrates the -complete workflow for building both Legate core and cuNumeric. - -.. image:: /_images/developer-build.png - :width: 600 - :alt: "notional diagram of cunumeric build process" - -.. _Legate Core: https://github.com/nv-legate/legate.core -.. _Legate Core build instructions: https://github.com/nv-legate/legate.core/blob/HEAD/BUILD.md -.. _Legate Core repository: https://github.com/nv-legate/legate.core \ No newline at end of file diff --git a/docs/cunumeric/source/index.rst b/docs/cunumeric/source/index.rst deleted file mode 100644 index ace34f0e9..000000000 --- a/docs/cunumeric/source/index.rst +++ /dev/null @@ -1,39 +0,0 @@ -:html_theme.sidebar_secondary.remove: - -Welcome to cuNumeric's documentation! -===================================== - -cuNumeric is a `Legate`_ library that aims to provide a distributed and -accelerated drop-in replacement for the `NumPy API`_ on top of the `Legion`_ -runtime. - -Using cuNumeric you do things like run the final example of the -`Python CFD course`_ completely unmodified on 2048 A100 GPUs in a -`DGX SuperPOD`_ and achieve good weak scaling. - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - user/index - comparison/index - api/index - developer/index - -.. toctree:: - :maxdepth: 1 - - versions - - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`search` - -.. _DGX SuperPOD: https://www.nvidia.com/en-us/data-center/dgx-superpod/ -.. _Legate: https://github.com/nv-legate/legate.core -.. _Legion: https://legion.stanford.edu/ -.. _Numpy API: https://numpy.org/doc/stable/reference/ -.. _Python CFD course: https://github.com/barbagroup/CFDPython/blob/master/lessons/15_Step_12.ipynb \ No newline at end of file diff --git a/docs/cunumeric/source/user/configuration.rst b/docs/cunumeric/source/user/configuration.rst deleted file mode 100644 index a63a6ae89..000000000 --- a/docs/cunumeric/source/user/configuration.rst +++ /dev/null @@ -1,108 +0,0 @@ -.. _config: - -Configuration -============= - -The underlying Legate runtime has many options for configuring the details of -execution. - -How to specify configuration ----------------------------- - -.. _config_legate: - -Legate driver -~~~~~~~~~~~~~ - -When using the ``legate`` driver, it is possible to pass most configuration -options via the command line, as long as they appear *before* the script to -run: - -.. code-block:: sh - - legate script.py + +{% endblock %} \ No newline at end of file diff --git a/docs/cunumeric/source/api/_bitgenerator.rst b/docs/cupynumeric/source/api/_bitgenerator.rst similarity index 65% rename from docs/cunumeric/source/api/_bitgenerator.rst rename to docs/cupynumeric/source/api/_bitgenerator.rst index 32854eff9..e269e9872 100644 --- a/docs/cunumeric/source/api/_bitgenerator.rst +++ b/docs/cupynumeric/source/api/_bitgenerator.rst @@ -1,7 +1,7 @@ -cunumeric.random.BitGenerator +cupynumeric.random.BitGenerator ============================= -.. currentmodule:: cunumeric.random +.. currentmodule:: cupynumeric.random .. autoclass:: BitGenerator diff --git a/docs/cunumeric/source/api/_generator.rst b/docs/cupynumeric/source/api/_generator.rst similarity index 65% rename from docs/cunumeric/source/api/_generator.rst rename to docs/cupynumeric/source/api/_generator.rst index 539a3c001..c73481232 100644 --- a/docs/cunumeric/source/api/_generator.rst +++ b/docs/cupynumeric/source/api/_generator.rst @@ -1,7 +1,7 @@ -cunumeric.random.Generator +cupynumeric.random.Generator ========================== -.. currentmodule:: cunumeric.random +.. currentmodule:: cupynumeric.random .. autoclass:: Generator diff --git a/docs/cunumeric/source/comparison/_grouped.rst b/docs/cupynumeric/source/api/_grouped.rst similarity index 100% rename from docs/cunumeric/source/comparison/_grouped.rst rename to docs/cupynumeric/source/api/_grouped.rst diff --git a/docs/cunumeric/source/api/_ndarray.rst b/docs/cupynumeric/source/api/_ndarray.rst similarity index 95% rename from docs/cunumeric/source/api/_ndarray.rst rename to docs/cupynumeric/source/api/_ndarray.rst index 3320f0857..ea6b57a32 100644 --- a/docs/cunumeric/source/api/_ndarray.rst +++ b/docs/cupynumeric/source/api/_ndarray.rst @@ -1,7 +1,7 @@ -cunumeric.ndarray +cupynumeric.ndarray ================= -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric .. autoclass:: ndarray @@ -31,7 +31,6 @@ cunumeric.ndarray ~ndarray.dump ~ndarray.dumps ~ndarray.fill - ~ndarray.find_common_type ~ndarray.flatten ~ndarray.flip ~ndarray.getfield diff --git a/docs/cunumeric/source/api/binary.rst b/docs/cupynumeric/source/api/binary.rst similarity index 90% rename from docs/cunumeric/source/api/binary.rst rename to docs/cupynumeric/source/api/binary.rst index 237fdc071..38b0260ab 100644 --- a/docs/cunumeric/source/api/binary.rst +++ b/docs/cupynumeric/source/api/binary.rst @@ -1,7 +1,7 @@ Binary operations ================= -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Elementwise bit operations -------------------------- diff --git a/docs/cunumeric/source/api/broadcast.rst b/docs/cupynumeric/source/api/broadcast.rst similarity index 52% rename from docs/cunumeric/source/api/broadcast.rst rename to docs/cupynumeric/source/api/broadcast.rst index 50d329a2e..9e093e79e 100644 --- a/docs/cunumeric/source/api/broadcast.rst +++ b/docs/cupynumeric/source/api/broadcast.rst @@ -1,6 +1,6 @@ -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric -cunumeric.broadcast +cupynumeric.broadcast =================== .. autoclass:: broadcast diff --git a/docs/cunumeric/source/api/classes.rst b/docs/cupynumeric/source/api/classes.rst similarity index 100% rename from docs/cunumeric/source/api/classes.rst rename to docs/cupynumeric/source/api/classes.rst diff --git a/docs/cupynumeric/source/api/comparison.rst b/docs/cupynumeric/source/api/comparison.rst new file mode 100644 index 000000000..eda6dddec --- /dev/null +++ b/docs/cupynumeric/source/api/comparison.rst @@ -0,0 +1,12 @@ +Project comparisons +=================== + +Here is a list of NumPy APIs and corresponding cuPyNumeric implementations. + +A dot in the cupynumeric column denotes that cuPyNumeric implementation +is not provided yet. We welcome contributions for these functions. + +NumPy vs cuPyNumeric APIs +------------------------- + +.. comparison-table:: diff --git a/docs/cunumeric/source/api/creation.rst b/docs/cupynumeric/source/api/creation.rst similarity index 93% rename from docs/cunumeric/source/api/creation.rst rename to docs/cupynumeric/source/api/creation.rst index ba43a2138..e35f6ab4c 100644 --- a/docs/cunumeric/source/api/creation.rst +++ b/docs/cupynumeric/source/api/creation.rst @@ -1,7 +1,7 @@ Array creation routines ======================= -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric From shape or value ------------------- @@ -41,6 +41,7 @@ Numerical ranges arange linspace + meshgrid Building matrices diff --git a/docs/cupynumeric/source/api/datatype.rst b/docs/cupynumeric/source/api/datatype.rst new file mode 100644 index 000000000..bb5667fd0 --- /dev/null +++ b/docs/cupynumeric/source/api/datatype.rst @@ -0,0 +1,12 @@ +Data type routines +================== + +.. currentmodule:: cupynumeric + +Data type testing +----------------- + +.. autosummary:: + :toctree: generated/ + + find_common_type \ No newline at end of file diff --git a/docs/cunumeric/source/api/fft.rst b/docs/cupynumeric/source/api/fft.rst similarity index 67% rename from docs/cunumeric/source/api/fft.rst rename to docs/cupynumeric/source/api/fft.rst index 8ca9dcc17..8a656a253 100644 --- a/docs/cunumeric/source/api/fft.rst +++ b/docs/cupynumeric/source/api/fft.rst @@ -1,6 +1,6 @@ -.. module:: cunumeric.fft +.. module:: cupynumeric.fft -Discrete Fourier Transform (:mod:`cunumeric.fft`) +Discrete Fourier Transform (:mod:`cupynumeric.fft`) ================================================== Standard FFTs @@ -36,6 +36,16 @@ Hermitian FFTs .. autosummary:: :toctree: generated/ - + hfft ihfft + + +Helper routines +--------------- + +.. autosummary:: + :toctree: generated/ + + fftshift + ifftshift \ No newline at end of file diff --git a/docs/cunumeric/source/api/index.rst b/docs/cupynumeric/source/api/index.rst similarity index 67% rename from docs/cunumeric/source/api/index.rst rename to docs/cupynumeric/source/api/index.rst index c6a5243c4..d57ccba21 100644 --- a/docs/cunumeric/source/api/index.rst +++ b/docs/cupynumeric/source/api/index.rst @@ -1,7 +1,7 @@ API Reference ============= -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric .. toctree:: :maxdepth: 2 @@ -9,3 +9,4 @@ API Reference classes routines settings + comparison diff --git a/docs/cunumeric/source/api/indexing.rst b/docs/cupynumeric/source/api/indexing.rst similarity index 92% rename from docs/cunumeric/source/api/indexing.rst rename to docs/cupynumeric/source/api/indexing.rst index e1a3358cb..3468e893e 100644 --- a/docs/cunumeric/source/api/indexing.rst +++ b/docs/cupynumeric/source/api/indexing.rst @@ -1,7 +1,7 @@ Indexing routines ================= -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Generating index arrays ----------------------- @@ -9,6 +9,7 @@ Generating index arrays .. autosummary:: :toctree: generated/ + unravel_index diag_indices diag_indices_from mask_indices @@ -17,6 +18,7 @@ Generating index arrays triu_indices triu_indices_from indices + ix_ nonzero flatnonzero where diff --git a/docs/cupynumeric/source/api/io.rst b/docs/cupynumeric/source/api/io.rst new file mode 100644 index 000000000..a5ba6f670 --- /dev/null +++ b/docs/cupynumeric/source/api/io.rst @@ -0,0 +1,11 @@ +Input and output +================ + +.. currentmodule:: cupynumeric + +NumPy binary files (npy, npz) +----------------------------- +.. autosummary:: + :toctree: generated/ + + load diff --git a/docs/cunumeric/source/api/linalg.rst b/docs/cupynumeric/source/api/linalg.rst similarity index 82% rename from docs/cunumeric/source/api/linalg.rst rename to docs/cupynumeric/source/api/linalg.rst index 78394ead0..0ccd9d4e6 100644 --- a/docs/cunumeric/source/api/linalg.rst +++ b/docs/cupynumeric/source/api/linalg.rst @@ -1,9 +1,9 @@ -.. module:: cunumeric.linalg +.. module:: cupynumeric.linalg -Linear algebra (:mod:`cunumeric.linalg`) +Linear algebra (:mod:`cupynumeric.linalg`) ======================================== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Matrix and vector products -------------------------- @@ -29,6 +29,8 @@ Decompositions :toctree: generated/ linalg.cholesky + linalg.qr + linalg.svd Norms and other numbers ----------------------- diff --git a/docs/cunumeric/source/api/logic.rst b/docs/cupynumeric/source/api/logic.rst similarity index 95% rename from docs/cunumeric/source/api/logic.rst rename to docs/cupynumeric/source/api/logic.rst index abc016c65..1ab6c7873 100644 --- a/docs/cunumeric/source/api/logic.rst +++ b/docs/cupynumeric/source/api/logic.rst @@ -1,7 +1,7 @@ Logic functions =============== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Truth value testing ------------------- diff --git a/docs/cunumeric/source/api/manipulation.rst b/docs/cupynumeric/source/api/manipulation.rst similarity index 91% rename from docs/cunumeric/source/api/manipulation.rst rename to docs/cupynumeric/source/api/manipulation.rst index 86010e721..b1d3f54c3 100644 --- a/docs/cunumeric/source/api/manipulation.rst +++ b/docs/cupynumeric/source/api/manipulation.rst @@ -1,7 +1,7 @@ Array manipulation routines =========================== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Basic operations ---------------- @@ -32,7 +32,7 @@ Transpose-like operations swapaxes transpose -See also :attr:`cunumeric.ndarray.T` property. +See also :attr:`cupynumeric.ndarray.T` property. Changing number of dimensions ----------------------------- @@ -46,6 +46,7 @@ Changing number of dimensions broadcast_arrays broadcast_shapes broadcast_to + expand_dims squeeze Changing kind of array @@ -105,3 +106,5 @@ Rearranging elements flip fliplr flipud + roll + rot90 diff --git a/docs/cunumeric/source/api/math.rst b/docs/cupynumeric/source/api/math.rst similarity index 96% rename from docs/cunumeric/source/api/math.rst rename to docs/cupynumeric/source/api/math.rst index a4f71d1cf..5764a9372 100644 --- a/docs/cunumeric/source/api/math.rst +++ b/docs/cupynumeric/source/api/math.rst @@ -1,7 +1,7 @@ Mathematical functions ====================== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Trigonometric functions ----------------------- @@ -43,6 +43,7 @@ Rounding .. autosummary:: :toctree: generated/ + round rint floor ceil @@ -59,10 +60,12 @@ Sums, products, differences sum cumprod cumsum + diff nancumprod nancumsum nanprod nansum + gradient Exponents and logarithms @@ -136,6 +139,7 @@ Handling complex numbers real imag + angle conj conjugate diff --git a/docs/cunumeric/source/api/ndarray.rst b/docs/cupynumeric/source/api/ndarray.rst similarity index 97% rename from docs/cunumeric/source/api/ndarray.rst rename to docs/cupynumeric/source/api/ndarray.rst index aca3b9ce0..a9d17a648 100644 --- a/docs/cunumeric/source/api/ndarray.rst +++ b/docs/cupynumeric/source/api/ndarray.rst @@ -1,6 +1,6 @@ -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric -The N-Dimensional array (:class:`cunumeric.ndarray`) +The N-Dimensional array (:class:`cupynumeric.ndarray`) ==================================================== Constructing arrays @@ -59,7 +59,6 @@ Data Type :toctree: generated/ ndarray.dtype - ndarray.find_common_type Other Attributes ~~~~~~~~~~~~~~~~ @@ -239,6 +238,8 @@ Matrix Multiplication: :toctree: generated/ ndarray.__matmul__ + ndarray.__imatmul__ + ndarray.__rmatmul__ Special methods --------------- diff --git a/docs/cunumeric/source/api/random.rst b/docs/cupynumeric/source/api/random.rst similarity index 93% rename from docs/cunumeric/source/api/random.rst rename to docs/cupynumeric/source/api/random.rst index 0cf5a61a9..22036b534 100644 --- a/docs/cunumeric/source/api/random.rst +++ b/docs/cupynumeric/source/api/random.rst @@ -1,6 +1,6 @@ -.. module:: cunumeric.random +.. module:: cupynumeric.random -Random sampling (:mod:`cunumeric.random`) +Random sampling (:mod:`cupynumeric.random`) ========================================= Random Generator diff --git a/docs/cunumeric/source/api/routines.rst b/docs/cupynumeric/source/api/routines.rst similarity index 91% rename from docs/cunumeric/source/api/routines.rst rename to docs/cupynumeric/source/api/routines.rst index e85a5c65b..166cb4450 100644 --- a/docs/cunumeric/source/api/routines.rst +++ b/docs/cupynumeric/source/api/routines.rst @@ -8,7 +8,9 @@ Routines creation manipulation binary + datatype indexing + io linalg logic math diff --git a/docs/cunumeric/source/api/set.rst b/docs/cupynumeric/source/api/set.rst similarity index 79% rename from docs/cunumeric/source/api/set.rst rename to docs/cupynumeric/source/api/set.rst index c4299e870..e797379d1 100644 --- a/docs/cunumeric/source/api/set.rst +++ b/docs/cupynumeric/source/api/set.rst @@ -1,7 +1,7 @@ Set routines ============ -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Making proper sets ------------------ diff --git a/docs/cupynumeric/source/api/settings.rst b/docs/cupynumeric/source/api/settings.rst new file mode 100644 index 000000000..6a424f0fb --- /dev/null +++ b/docs/cupynumeric/source/api/settings.rst @@ -0,0 +1,8 @@ +Settings +======== + +cuPyNumeric has a number of runtime settings that can be configured through +environment variables. + +.. settings:: settings + :module: cupynumeric.settings \ No newline at end of file diff --git a/docs/cunumeric/source/api/sorting.rst b/docs/cupynumeric/source/api/sorting.rst similarity index 93% rename from docs/cunumeric/source/api/sorting.rst rename to docs/cupynumeric/source/api/sorting.rst index 86d8e65dc..ab5570cfd 100644 --- a/docs/cunumeric/source/api/sorting.rst +++ b/docs/cupynumeric/source/api/sorting.rst @@ -1,7 +1,7 @@ Sorting, searching, and counting ================================ -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Sorting ------- diff --git a/docs/cunumeric/source/api/statistics.rst b/docs/cupynumeric/source/api/statistics.rst similarity index 64% rename from docs/cunumeric/source/api/statistics.rst rename to docs/cupynumeric/source/api/statistics.rst index 48f10f19c..9430ea324 100644 --- a/docs/cunumeric/source/api/statistics.rst +++ b/docs/cupynumeric/source/api/statistics.rst @@ -1,7 +1,18 @@ Statistics ========== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric + +Order statistics +---------------- + +.. autosummary:: + :toctree: generated/ + + quantile + percentile + nanquantile + nanpercentile Averages and variances ---------------------- @@ -9,26 +20,27 @@ Averages and variances .. autosummary:: :toctree: generated/ + average mean nanmean var + median + nanmedian - -Histograms ----------- +Correlating +----------- .. autosummary:: :toctree: generated/ - bincount - histogram - + cov -Order statistics ----------------- +Histograms +---------- .. autosummary:: :toctree: generated/ - quantile - percentile + bincount + histogram + digitize diff --git a/docs/cunumeric/source/api/window.rst b/docs/cupynumeric/source/api/window.rst similarity index 85% rename from docs/cunumeric/source/api/window.rst rename to docs/cupynumeric/source/api/window.rst index 28058d21f..e50dc5898 100644 --- a/docs/cunumeric/source/api/window.rst +++ b/docs/cupynumeric/source/api/window.rst @@ -1,7 +1,7 @@ Window functions ====================== -.. currentmodule:: cunumeric +.. currentmodule:: cupynumeric Various windows ----------------------- diff --git a/docs/cunumeric/source/conf.py b/docs/cupynumeric/source/conf.py similarity index 59% rename from docs/cunumeric/source/conf.py rename to docs/cupynumeric/source/conf.py index 93a2bfcd0..a0dcd2af8 100644 --- a/docs/cunumeric/source/conf.py +++ b/docs/cupynumeric/source/conf.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 NVIDIA Corporation +# Copyright 2024 NVIDIA Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,11 +13,21 @@ # limitations under the License. # +from os import getenv + +from cupynumeric import __version__ + +SWITCHER_PROD = "https://docs.nvidia.com/cupynumeric/switcher.json" +SWITCHER_DEV = "http://localhost:8000/switcher.json" +JSON_URL = SWITCHER_DEV if getenv("SWITCHER_DEV") == "1" else SWITCHER_PROD + # -- Project information ----------------------------------------------------- -project = "cuNumeric" -copyright = "2021-2023, NVIDIA" -author = "NVIDIA" +project = "NVIDIA cuPyNumeric" +if "dev" in __version__: + project += f" ({__version__})" +copyright = "2024, NVIDIA" +author = "NVIDIA Corporation" # -- General configuration --------------------------------------------------- @@ -32,10 +42,10 @@ "myst_parser", "nbsphinx", "legate._sphinxext.settings", - "cunumeric._sphinxext.comparison_table", - "cunumeric._sphinxext.implemented_index", - "cunumeric._sphinxext.missing_refs", - "cunumeric._sphinxext.ufunc_formatter", + "cupynumeric._sphinxext.comparison_table", + "cupynumeric._sphinxext.implemented_index", + "cupynumeric._sphinxext.missing_refs", + "cupynumeric._sphinxext.ufunc_formatter", ] source_suffix = {".rst": "restructuredtext", ".md": "markdown"} @@ -43,27 +53,25 @@ # -- Options for HTML output ------------------------------------------------- html_context = { - "default_mode": "light", + # "default_mode": "light", "AUTHOR": author, - "DESCRIPTION": "cuNumeric documentation site.", + "DESCRIPTION": "cuPyNumeric documentation site.", } html_static_path = ["_static"] -html_theme = "pydata_sphinx_theme" +html_theme = "nvidia_sphinx_theme" html_theme_options = { - "footer_start": ["copyright"], - "github_url": "https://github.com/nv-legate/cunumeric", - # https://github.com/pydata/pydata-sphinx-theme/issues/1220 - "icon_links": [], - "logo": {"text": project, "link": "https://nv-legate.github.io/cunumeric"}, - "navbar_align": "left", - "navbar_end": ["navbar-icon-links", "theme-switcher"], - "primary_sidebar_end": ["indices.html"], - "secondary_sidebar_items": ["page-toc"], - "show_nav_level": 2, - "show_toc_level": 2, + "switcher": { + "json_url": JSON_URL, + "navbar_start": ["navbar-logo", "version-switcher"], + "version_match": ".".join(__version__.split(".", 2)[:2]), + }, + "extra_footer": [ + "This project, i.e., cuPyNumeric, is separate and independent of the CuPy project. CuPy is a registered trademark of Preferred Networks.", # NOQA + '', # NOQA + ], } templates_path = ["_templates"] diff --git a/docs/cupynumeric/source/developer/CONTRIBUTING.md b/docs/cupynumeric/source/developer/CONTRIBUTING.md new file mode 100644 index 000000000..8dacfa72c --- /dev/null +++ b/docs/cupynumeric/source/developer/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing to cuPyNumeric + +cuPyNumeric is an open-source project released under the [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0). We welcome any and all contributions, and we hope that you can help us develop a strong community. + +## How to begin + +Most of the time, the best thing is to begin by [opening an issue](https://github.com/nv-legate/cupynumeric/issues). This gives us a chance to discuss the contribution and to define the problem or feature that it addresses. Often, opening of the issue first may help prevent you from doing unnecessary work or to enhance and further develop your idea. + +Once you are ready to start development, we ask you to work on a [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) of our repository. The next step is to create a (pull request)[https://help.github.com/en/articles/about-pull-requests]. Feel free to open the pull request as soon as you begin your development (just mark it [as a draft](https://github.blog/2019-02-14-introducing-draft-pull-requests/)) or when you are ready to have your contribution merged. + +## The Legalese: Developer Certificate of Origin + +cuPyNumeric is released under the open-source [Apache license, version 2.0](https://www.apache.org/licenses/LICENSE-2.0), and is free to use, modify, and redistribute. To ensure that the license can be exercised without encumbrance, we ask you that you only contribute your own work or work to which you have the intellectual rights. To that end, we employ the Developer's Certificate of Origin (DCO), which is the lightweight mechanism for you to certify that you are legally able to make your contribution. Here is the full text of the certificate (also available at [DeveloperCertificate.org](https://developercertificate.org/): + +```` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +```` + +### How Do I Sign the DCO? + +Fortunately, it does not take much work to sign the DCO. The only thing that you have to do is to mark all your commits with a `Signed-off-by` line that looks like that: + +```` +Signed-off-by: Your Name +```` + +Please use your real name and a valid email address at which you can be reached. For legal reasons, we will not be able to accept contributions that use pseudonyms in the signature. You can simply add this line at the end of all your commits manually, or you can use the `-s` or the `--signoff` options provided by Git to automatically tack on the signature. + +## Review Process + +We are really grateful that you are thinking of contributing to cuPyNumeric. We will make every effort to review your contributions as soon as possible. + +As we suggested at the beginning of this document, it will be really helpful to start with an issue unless your proposed change is really trivial. An issue will help to save work in the review process (e.g., maybe somebody is already working on exactly the same thing you want to work on). After you open your pull request (PR), there usually will be a community feedback that often will require further changes to your contribution (the usual open-source process). Usually, this will conclude in the PR being merged by a maintainer, but on rare occasions a PR may be rejected. This may happen, for example, if the PR appears abandoned (no response to the community feedback) or if the PR does not seem to be approaching community acceptance in a reasonable time frame. In any case, an explanation will always be given why a PR is closed. Even if a PR is closed for some reason, it may always be reopened if the situation evolves (feel free to comment on closed PRs to discuss reopening them). + +## Code Formatting Requirements + +cuPyNumeric has a set of coding standards that are expected from all the code merged into the project. The coding standards are defined by the set of tools we use to format our code. We use the [pre-commit](https://pre-commit.com/) framework to run our formatting tools. The easiest way to meet the coding standards is to simply use the pre-commit framework to run all the checks for you. Please visit the [pre-commit project page](https://pre-commit.com/) for pre-commit installation and usage instructions. Once pre-commit is installed in the cuPyNumeric repo, all the checks and formatting will be run on every commit, but one can also run the checks explicitly as detailed in pre-commit documentation. + +We hope that the automation of our formatting checks will make it easy to comply with our coding standards. If you encounter problems with code formatting, however, please let us know in a comment on your PR, and we will do our best to help. diff --git a/docs/cupynumeric/source/developer/building.rst b/docs/cupynumeric/source/developer/building.rst new file mode 100644 index 000000000..9c4f74765 --- /dev/null +++ b/docs/cupynumeric/source/developer/building.rst @@ -0,0 +1,71 @@ +.. _building cupynumeric from source: + +Building from source +==================== + +Basic build +----------- + +Users must have a working installation of the `Legate`_ library prior to +installing cuPyNumeric. +**Installing cuPyNumeric by itself will not automatically install Legate.** + +As for other dependencies, the Dependencies section on the +`Legate build instructions`_ also covers cuPyNumeric, so no additional +packages are required. + +Once Legate is installed, you can simply invoke ``./install.py`` from the +cuPyNumeric top-level directory. The build will automatically pick up the +configuration used when building Legate (e.g. the CUDA Toolkit directory). + +Advanced topics +--------------- + +Building through pip & cmake +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +cuPyNumeric uses the same cmake/scikit-build-based build workflow as Legate. +See the `Legate build instructions`_ for an overview. + +There are several examples in the ``scripts`` folder. We walk through the steps in +``build-with-legate-separately-no-install.sh`` here. + +We assume a pre-existing Legate build. For details on building Legate, +consult the `Legate repository`_. + +First, the CMake build needs to be configured: + +.. code:: sh + + $ cmake -S . -B build -GNinja -D legate_ROOT:STRING=path/to/legate/build + +We point cuPyNumeric to the Legate *build* tree, not an installation. +This generates all build-dependent headers and Python files. + +Once configured, we can build the C++ libraries: + +.. code:: sh + + $ cmake --build build + +This will invoke Ninja (or make) to execute the build. +Once the C++ libraries are available, we can do an editable (development) pip installation. + +.. code:: sh + + $ SKBUILD_BUILD_OPTIONS="-D FIND_CUPYNUMERIC_CPP=ON -D cupynumeric_ROOT=$(pwd)/build" \ + python3 -m pip install \ + --root / --no-deps --no-build-isolation + --editable . + +The Python source tree and CMake build tree are now available with the environment Python +for running cuPyNumeric programs. The diagram below illustrates the +complete workflow for building both Legate and cuPyNumeric. + +.. image:: /_images/developer-build.png + :width: 600 + :alt: "notional diagram of cupynumeric build process" + +.. _Legate: https://github.com/nv-legate/legate.core +.. _Legate build instructions: https://github.com/nv-legate/legate.core/blob/HEAD/BUILD.md +.. _Legate repository: https://github.com/nv-legate/legate.core diff --git a/docs/cunumeric/source/developer/index.rst b/docs/cupynumeric/source/developer/index.rst similarity index 100% rename from docs/cunumeric/source/developer/index.rst rename to docs/cupynumeric/source/developer/index.rst diff --git a/docs/cunumeric/source/developer/testing.rst b/docs/cupynumeric/source/developer/testing.rst similarity index 97% rename from docs/cunumeric/source/developer/testing.rst rename to docs/cupynumeric/source/developer/testing.rst index f5485b787..55aa39e36 100644 --- a/docs/cunumeric/source/developer/testing.rst +++ b/docs/cupynumeric/source/developer/testing.rst @@ -4,7 +4,7 @@ Running tests Basic usage ----------- -The simplest way to run the cuNumeric test suite is to use the ``test.py`` +The simplest way to run the cuPyNumeric test suite is to use the ``test.py`` test driver script. .. code-block:: sh diff --git a/docs/cunumeric/source/user/notebooks/black_scholes.ipynb b/docs/cupynumeric/source/examples/black_scholes.ipynb similarity index 99% rename from docs/cunumeric/source/user/notebooks/black_scholes.ipynb rename to docs/cupynumeric/source/examples/black_scholes.ipynb index b77e60386..e5868463a 100644 --- a/docs/cunumeric/source/user/notebooks/black_scholes.ipynb +++ b/docs/cupynumeric/source/examples/black_scholes.ipynb @@ -19,7 +19,7 @@ "License\n", "
\n",
     "\n",
-    "Copyright 2023 NVIDIA Corporation\n",
+    "Copyright 2024 NVIDIA Corporation\n",
     "\n",
     "Licensed under the Apache License, Version 2.0 (the \"License\");\n",
     "you may not use this file except in compliance with the License.\n",
@@ -41,7 +41,7 @@
    "id": "5b787e94-e440-4e1c-bd66-29faf9b59041",
    "metadata": {},
    "source": [
-    "To get started, `import cunumeric as np` (just the same way we would import `numpy`)"
+    "To get started, `import cupynumeric as np` (just the same way we would import `numpy`)"
    ]
   },
   {
@@ -51,7 +51,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "import cunumeric as np  # instead of numpy"
+    "import cupynumeric as np  # instead of numpy"
    ]
   },
   {
@@ -162,7 +162,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "/home/bryan/work/legate.core/legate/core/context.py:280: RuntimeWarning: cuNumeric has not implemented numpy.result_type and is falling back to canonical numpy. You may notice significantly decreased performance for this function call.\n",
+      "/home/bryan/work/legate.core/legate/core/context.py:280: RuntimeWarning: cuPyNumeric has not implemented numpy.result_type and is falling back to canonical numpy. You may notice significantly decreased performance for this function call.\n",
       "  result = func(*args, **kwargs)\n",
       "Elapsed Time: 45.659 ms\n"
      ]
diff --git a/docs/cunumeric/source/user/notebooks/cholesky.ipynb b/docs/cupynumeric/source/examples/cholesky.ipynb
similarity index 86%
rename from docs/cunumeric/source/user/notebooks/cholesky.ipynb
rename to docs/cupynumeric/source/examples/cholesky.ipynb
index e0bcfa21d..ee39c6ec0 100644
--- a/docs/cunumeric/source/user/notebooks/cholesky.ipynb
+++ b/docs/cupynumeric/source/examples/cholesky.ipynb
@@ -9,13 +9,13 @@
     "\n",
     "A [Cholesky decomposition](https://en.wikipedia.org/wiki/Cholesky_decomposition) is a useful factorization of Hermitian, positive-definite matrices into the product of a lower triangular matrix $L$ with its conjugate transpose $L^{*}$.\n",
     "\n",
-    "Numpy has a function [numpy.linalg.cholesky](https://numpy.org/doc/stable/reference/generated/numpy.linalg.cholesky.html) built-in for computing Cholesky decompositions. Cunumeric also implements this function, and it can be used as an immediate drop-in replacement.\n",
+    "Numpy has a function [numpy.linalg.cholesky](https://numpy.org/doc/stable/reference/generated/numpy.linalg.cholesky.html) built-in for computing Cholesky decompositions. cuPyNumeric also implements this function, and it can be used as an immediate drop-in replacement.\n",
     "\n",
     "
\n", "License\n", "
\n",
     "\n",
-    "Copyright 2023 NVIDIA Corporation\n",
+    "Copyright 2024 NVIDIA Corporation\n",
     "\n",
     "Licensed under the Apache License, Version 2.0 (the \"License\");\n",
     "you may not use this file except in compliance with the License.\n",
@@ -37,7 +37,7 @@
    "id": "389cd191-ccda-4597-8e08-8d01ac226bee",
    "metadata": {},
    "source": [
-    "To get started, `import cunumeric as np` (just the same way we would import `numpy`)\n"
+    "To get started, `import cupynumeric as np` (just the same way we would import `numpy`)\n"
    ]
   },
   {
@@ -49,7 +49,7 @@
    },
    "outputs": [],
    "source": [
-    "import cunumeric as np  # instead of numpy"
+    "import cupynumeric as np  # instead of numpy"
    ]
   },
   {
@@ -57,7 +57,7 @@
    "id": "9ef2bc57-e703-40ce-8aaa-d45408259c7a",
    "metadata": {},
    "source": [
-    "At this point we can call `np.linalg.cholesky`, exactly how we would with Numpy, but will get the result computed by Cunumeric's `cholesky` function. Let's quickly try it out with a simple identitity matrix:"
+    "At this point we can call `np.linalg.cholesky`, exactly how we would with Numpy, but will get the result computed by cuPyNumeric's `cholesky` function. Let's quickly try it out with a simple identitity matrix:"
    ]
   },
   {
@@ -96,7 +96,7 @@
     "tags": []
    },
    "source": [
-    "We'd like to get some information about how well Cunumeric's `cholesky` function performs. In order to obtain accurate timings, we need to use the `time` function from `legate.timing`. Let's define a helper function `cholesky_timed` that calls the `time` function for us, and prints out the results as well:"
+    "We'd like to get some information about how well cuPyNumeric's `cholesky` function performs. In order to obtain accurate timings, we need to use the `time` function from `legate.timing`. Let's define a helper function `cholesky_timed` that calls the `time` function for us, and prints out the results as well:"
    ]
   },
   {
diff --git a/docs/cupynumeric/source/examples/compact_finite_difference.ipynb b/docs/cupynumeric/source/examples/compact_finite_difference.ipynb
new file mode 100644
index 000000000..6c77763a4
--- /dev/null
+++ b/docs/cupynumeric/source/examples/compact_finite_difference.ipynb
@@ -0,0 +1,336 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "a040718f-95ed-4e39-8e96-76529df1811a",
+   "metadata": {},
+   "source": [
+    "# Compact Finite Difference Scheme"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b8ced087-d285-4a55-97c6-fc78ad69d186",
+   "metadata": {},
+   "source": [
+    "## Learning Outcomes\n",
+    "\n",
+    "This examples teaches how to compute derivative of a function using Compact Finite Difference scheme as described in the paper by [Lele](https://www.sciencedirect.com/science/article/abs/pii/002199919290324R).\n",
+    "\n",
+    "In this example, you will learn:\n",
+    "* how to convert stencil expressions from discretization to NumPy slicing operations\n",
+    "* how to create a tridiagonal matrix (dense)\n",
+    "* how to solve the resulting tridiagonal matrix using `linalg.solve`\n",
+    "* how to compute the L2 norm error between exact solution and computed solution using `linalg.norm`\n",
+    "\n",
+    "Note that a more optimal way of solving tridiagonal matrices is by using the TDMA algorithm. Here, we show how this can be solved using NumPy's `solve` API.\n",
+    "\n",
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "adb9700f-d3d5-4003-a532-123c1c55e340",
+   "metadata": {},
+   "source": [
+    "## Background\n",
+    "\n",
+    "Compact finite difference schemes approximate the first derivative by including the information of derivative of function at neighboring points in addition to including the value of function themselves, as shown below:\n",
+    "\n",
+    "$\\alpha f^{'}_{i-1} + f^{'}_{i-1} + \\alpha f^{'}_{i+1} = a_{1} f_{j+1} + a_{2} f_{j+2} - a_{2} f_{j-2} - a_{1} f_{j-1}$\n",
+    "\n",
+    "This can be represented more compactly in the following form,\\\n",
+    "$\\mathbf{A} f' = \\mathbf{B} f$\n",
+    "\n",
+    "where the matrix $\\mathbf{A}$ is tridiagonal and $\\mathbf{B}$ is pentadiagonal. In this example, we store the matrix, $\\mathbf{A}$, as a dense matrix and explicitly compute $\\mathbf{B} f$ instead of storing B to save memory. To compute the derivative, $f'$, of a function, $f$, using a sixth order compact finite difference, we solve a linear system of equations\n",
+    "\n",
+    "The main and off-diagonal elements of matrix $\\mathbf{A}$ are 1.0 and 1.0/3 respectively. For this example, we consider a sine function, $f=\\sin({k x})$\n",
+    "\n",
+    "\n",
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "df0cdc69-4bf6-4ecd-bb46-4df1b95437c4",
+   "metadata": {},
+   "source": [
+    "The domain extends from 0 to $2 \\pi$ and is discretized using N points. Since the stecil for the RHS extends 2 points on either side, we may have to create arrays of size (N+4) to accomodate storing the values of points outside the domain."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9475f334-b2ab-4cad-8a1f-8803ecf97371",
+   "metadata": {},
+   "source": [
+    "## Implementation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "39bc4d10-25e2-4ef8-a70a-2ce270c8eb5f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "from matplotlib import pyplot as plt"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "7dc1892b-842f-4200-abf6-769095caa861",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# number of points used in discretization\n",
+    "npoints: int = 100\n",
+    "\n",
+    "# number of stencil points to compute the right-hand side\n",
+    "n_stencil: int = 2\n",
+    "\n",
+    "# length of the domain\n",
+    "length = 2.0*np.pi\n",
+    "\n",
+    "# wavenumber of the initial profile\n",
+    "wavenumber = 10"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "ee7c5c26-ffcb-4e6a-acb6-73540e0c735c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# compute the spacing\n",
+    "h = length/npoints\n",
+    "\n",
+    "# generate the discretized points\n",
+    "x = np.linspace(0, length, npoints, endpoint=False)\n",
+    "\n",
+    "# compute the function and exact derivative\n",
+    "f_interior = np.sin(wavenumber*x)\n",
+    "derivative_exact = wavenumber* np.cos(wavenumber*x)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "17b3c8ce-22dd-4a26-8ec7-ceb51632e59f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# For sixth order, the stencil should be of size 2\n",
+    "assert n_stencil == 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "816d739e-c4d3-4559-a097-af896eb75228",
+   "metadata": {},
+   "source": [
+    "Compute the function values including the left and right-hand side boundaries"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "59649243-9f42-4344-bbd7-17051d303d1d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "function_values = np.zeros(npoints + n_stencil*2)\n",
+    "function_values[n_stencil:-n_stencil] = f_interior\n",
+    "\n",
+    "# set the RHS boundary values using periodic boundary condition\n",
+    "function_values[npoints + n_stencil] = f_interior[0]\n",
+    "function_values[npoints + n_stencil + 1] = f_interior[1]\n",
+    "\n",
+    "# set the LHS boundary values using periodic boundary condition\n",
+    "function_values[0] = f_interior[npoints - n_stencil]\n",
+    "function_values[1] = f_interior[npoints - n_stencil + 1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3dbc59ce-7099-4f49-ba55-d2f918eb3adc",
+   "metadata": {},
+   "source": [
+    "Form the matrix $\\mathbf{A}$"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "9a11d32c-1568-4a30-aeb6-52480aca34c6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "A = np.zeros((npoints, npoints))\n",
+    "\n",
+    "# Eqn (2.1.7) from Compact Finite Difference with Spectral-like Accuracy, Lele, 1992, JCP.\n",
+    "alpha = 1.0/3.0\n",
+    "\n",
+    "# generate the tridiagonal matrix using np.diag\n",
+    "main = np.ones((1, npoints))[0]\n",
+    "diagonal = alpha*np.ones((1, npoints - 1))[0]\n",
+    "A = np.diag(main, 0) + np.diag(diagonal, -1) + np.diag(diagonal, 1)\n",
+    "\n",
+    "# Apply periodic boundary condition\n",
+    "A[0, -1] = alpha\n",
+    "A[-1, 0] = alpha"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "204c132c-b4b2-494e-96cc-1b7ee1c55e83",
+   "metadata": {},
+   "source": [
+    "Form the right-hand side"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "d939df3a-6e6c-4e88-b671-18ab0b1e58b9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Generate the right-hand side\n",
+    "a1 = 7.0/(9.0*h)\n",
+    "a2 = 1.0/(36.*h)\n",
+    "\n",
+    "# note how $a_{1} f_{j+1} + a_{2} f_{j+2} - a_{2} f_{j-2} - a_{1} f_{j-1}$\n",
+    "# gets converted to slicing operations on the array function_values.\n",
+    "\n",
+    "# It is important to derive the right start and end indices for the slices corresponding to each term.\n",
+    "# Since the stencil size on the RHS is 2 (n_stencil), the index j in the equation starts from the second point (j=2)\n",
+    "# and ends at j=(npoints+2). This translates to the following slices;\n",
+    "\n",
+    "# f_{j+2} - f_{j-2} -> (function_values[4:npoints+4] - function_values[0:npoints])\n",
+    "# f_{j+1} - f_{j-1} -> (function_values[3:npoints+3] - function_values[1:npoints+1])\n",
+    "rhs = np.zeros(npoints)\n",
+    "rhs[0:npoints] = a1*(function_values[3:npoints+3] - function_values[1:npoints+1]) \\\n",
+    "               + a2*(function_values[4:npoints+4] - function_values[0:npoints])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "77ee7783-933e-4be0-88c7-984396cdaeef",
+   "metadata": {},
+   "source": [
+    "Compute the derivative and the L2 norm error"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "87df4961-25af-4876-8e46-2762429a3418",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "L2 norm error: 0.0021705301478625403\n"
+     ]
+    }
+   ],
+   "source": [
+    "derivative = np.linalg.solve(A, rhs)\n",
+    "error = np.linalg.norm(derivative - derivative_exact)\n",
+    "print(f\"L2 norm error: {error}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1015951c-29a9-4be1-b65e-5f3d19efdb9c",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "Compare the exact and computed derivative of the function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "c1d82ac2-d584-451e-8bc7-b618e3a0f9af",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       ""
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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hw5kzpw5If9eDdmyZQsZO3as8N7Nz88n559/fsDx8j/nEELITTfdRGw2G/n555+F2zweDzn77LNJTk4OOXr0aMRzUyzH33+fcv7W69atI0OGDCEJCQnksssuI4QQcuTIEXLppZeSpKQkkpKSQiZOnEg2bNgQ9O+7efNmMn78eJKenk5sNhvp168fWbx4ccA27HWvWrWKXHfddSQrK4sAILW1tUFfQ21tLZk+fTrp27cvSUlJIenp6eT0008ny5cvD/p6X375ZdK3b19it9tJamoqGTx4cMC5iBBCPvjgA3L66acTh8NBHA4H6du3b8C5rOHf0/84DR8+XPidff4WLlxIpk+fTlq1akUMBgPZtWsXKS4uJrfccgvp3r07cTgcJDs7m4wcOZJ8//33wuPZ56Thj/++Yzl3Sjkeevxu/+ijj8ioUaNIXl4esdvtpFu3buT+++8nVVVVjV7jxo0bybhx40hGRgax2WykY8eO5K677grYZ8OfUN0Uwb4D2d8ilnNQw++ASN+19fX15MknnxT+JllZWeTaa69tdJxCMX/+fHLKKacI75f33nsvZPwNvyv8f9jnoOHt/p8BJT7zH330ETn99NNJYmIicTgc5Nxzz21UHWPVjT179pAxY8YQh8NB2rRpQ6ZPnx5QCSOEkLq6OvL444+Tbt26EZvNRjIyMsiIESPI//73P2Ebr9dL5s6dK3w20tLSyCWXXEL++uuviMdXyncWIYSsWLGCnH322SQlJYUkJCSQbt26kaeffjqq1yT1PcHWFp9//jnp16+f8Plh78n58+eTbt26kcTERHLaaaeRzZs3N3p9Uv6maiE5sXG73SQxMZEMHjxY8pPfeOONBAC5/fbbycqVK8nrr79OsrOzSdu2bQMWg8OHDyeZmZnCIm3VqlVk3LhxBAB5/PHHSe/evcmiRYvIV199RU4//XRis9nIkSNHhMezD1f79u3JvffeS1atWkVeeOEF4nA4SP/+/QMW4lISm+LiYvL0008TAGTu3Llkw4YNZMOGDcKb4b///S8xGAzkwgsvJEuXLiWff/45GTduHDGZTBGTm6KiIpKTk0Nat25N5s+fT7766ity1VVXkXbt2jU6eUrdj//rv//++8nq1auFL/CGC7FPP/2UAGj0Ydq1axcBQF5++WXhtrvvvpvMmzePrFy5knz33XfkxRdfJFlZWeS6664Ttvn999/JGWecQfLy8oTj5N/G1PB49+/fn5xxxhmNjsvEiRNJTk4OcblcwvOmpqaS3r17k4ULF5Kvv/6a3HPPPcRoNJJZs2aFPcZer5eMHj2amM1m8sgjj5Cvv/6aPP/888L7gX34N2zYQM4//3ySkJDQ6G8cjHnz5pHZs2eTzz77jKxbt4689957pG/fvqRr164B77GGsJYCAGTChAkBx0huYtOmTRvSo0cPsnDhQrJq1Spy6aWXEgBk3bp1wnaHDx8m+fn5JCsri7zwwgvkm2++IYsXLybXX3892bVrFykvLxee/+GHHxbiYYuSYDHJ/Sx36dKFvP7662T16tXk1ltvJQDIe++9F+avJi+xGT16NMnOziZvvvkmWbt2LVm+fDl59NFHyUcffSRsE+pLuaCggAwaNIh8/PHH5KuvviIjRowgZrM54MtpxYoVxGg0khEjRpBly5aRTz75hAwePJgUFBRETGyqqqpIZmYmGThwIPn444/JunXryOLFi8nNN99Mdu7cKWzXcCFcW1tL+vXrRzp27Cgk4I8++igxGo3k66+/JoREPjfFcvwZcv7WGRkZpG3btuSVV14ha9asIevWrSM1NTWke/fuJDU1lbzyyitk1apV5M477xTOcf5/3++++45YrVYybNgwsnjxYrJy5Upy7bXXNtqOvV9bt25NbrzxRrJixQryf//3f8Ttdgd9DWVlZeTaa68l//3vf8l3331HVq5cSWbMmEGMRmOj4zBp0iRiMBjIlClTyKeffkpWrFhBnnrqKfLSSy8J2zzyyCMEALn44ovJJ598Qr7++mvywgsvkEceeSTk39P/OAVLbFq3bk0mTJhAPvvsM/LFF1+QkpIS8scff5BbbrmFfPTRR2Tt2rXkiy++IDfccAMxGo3Cd0NdXR1ZuXIlAUBuuOEG4T3A2hpjOXdKPR56/G5/8sknyYsvvki+/PJLsnbtWvL666+TDh06kJEjRwa8vpUrVxKLxUL69OlDFixYQL777jvy7rvvkssvv5wQQltx77jjDgKALF26VDi+5eXlQY/X77//Th5++GHhPev/t4jlHNTwOyDcd63H4yHnnXcecTgc5PHHHyerV68mb7/9NmndujXp0aMHqampCfs3Z/v65z//ST7//HPy/vvvk86dO5O2bduGTWwOHTpEli5dSgCQO+64g2zYsEFosZ07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QZ9rx44daNOmDbKysmTHoQW8FU1hHnvsMVitVowaNQoejwejR4/G5s2bceqpp2odWosm2eYAnPGtIrAraekKtWMArD0q/omNki5MajgaKV0dA8SqVbyqCPFIbFgS7YljFSEe1TFbnKsI7PNiVLDFUqgixDGxEapjVuXiTohzFUEY/Ok10vZZBfA314iXvrDGVQ3YlE1sHFZHXPWFTpcHsNQB4ImNEmzbtg0XXHBB0PuiaUV79tlnkZubi9NOOw0mkwlvv/020tPThURi+/btMJlMQrsX+90/uTpw4ABOnjwpJDajR4/GzJkzcfLkSaSnpwvbVVVVYe/evcLv+/btw7Zt25CRkYF27doBAKZPn45JkyZh4MCBGDJkCN58800cPHgQN998M/bv34+xY8figQcewKRJk9CjRw+cdtpp+OWXXzBgwICA1/XDDz/g3HPPlXUstIRXbBSksLAQd955J5588kn06NEDFosFl19+OU9qdEBqAm3/iacWgfWZZyskoAWoyBqInxZB7DNXLuZ0BxNZx7/PXKnhi4AoIi6rjU/cTOOgZHUsMzn+Q1ydBmWHLwKA3RjfIa4llUw7plzM2cyEwFIbtypCnZfGzQZUKkGiz/QhXvpCph2DK4m2zypAWpKd6gsBFJfFJ26mHUtSaKAyACT7nite+kL/6liuQqYYLRWv14vffvstZMUmGurq6vD0009jwIABOPPMM7Fnzx589913QkKyfft2dOvWTejgYb8zDQtA29DS0tKEYZy9e/fGwIED8fHHHwfs6+eff0b//v2Fas/06dPRv39/PProo8I2l112GebMmYMnnngC/fr1w/fff4+vvvoKycnJGDNmDC644AI8+OCDAIABAwZg/PjxeOihhxq9pmXLlmHq1KmKHad4wys2CtKxY0d069YNn3zyCcaOHYtVq1Zh4sSJOHz4MO6//36tw2vRpPjs7Z2kCigsBEwmICdHseePR585amth8SaiDkD58WM07pQUWXaWkYhHn3ma1wvAV0UoLKR9z7m5svufw6F4n3l9PWxeKtCsPHmCxp2UBIRwOYwGZgGrZBKZSegCm5h9xxqgx9qo3DUrxatjbjcSCNW9VFeV0rgTEoC0NGWeH37aMQWrY9lwCf8/sWcfWqU7gOxswKzc16hT6eGLXi8cXhpfTV0ZPdY2GxCFKDoUQnVMQct4Y3UV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",
+      "text/plain": [
+       "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(x, f_interior, color='red', label=f'$f = sin({wavenumber}x)$')\n", + "plt.plot(x, derivative, color='blue', label=f'Computed $f\\'(x)$')\n", + "plt.plot(x, derivative_exact, color='green', label=f'Exact f\\'(x)')\n", + "\n", + "plt.xlabel(f'x')\n", + "plt.ylabel(f'$f(x), f\\'(x)$')\n", + "plt.title(f'Compute derivative of a function using sixth-order accurate compact finite difference scheme')\n", + "plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))" + ] + }, + { + "cell_type": "markdown", + "id": "4f05eea3-5e1e-4567-9d7a-6ef8143f2723", + "metadata": {}, + "source": [ + "We see that the curves for the computed and the exact derivative overlap (green and blue) and the L2 error is of the order of 1e-3. If you are curious,\n", + "\n", + "* Try increasing the wavenumber, $k$, to see how this affects the accuracy of the solution. Plot error vs wavenumber to see how it varies\n", + "* Try reducing the resolution of the grid (n_points) and plot the error vs number of points.\n", + "* Try plotting the modified wavenumber and see if you can match the curve in the paper by [Lele](https://www.sciencedirect.com/science/article/abs/pii/002199919290324R)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9573de0a-be2a-4781-ae79-3ca9dcb17f05", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3 (legate) *", + "language": "python", + "name": "conda-env-legate-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/cupynumeric/source/examples/edge_detection.ipynb b/docs/cupynumeric/source/examples/edge_detection.ipynb new file mode 100644 index 000000000..c83093f02 --- /dev/null +++ b/docs/cupynumeric/source/examples/edge_detection.ipynb @@ -0,0 +1,210 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0c2fc84f-fb0b-4fb2-b356-14a75a1f9dba", + "metadata": {}, + "source": [ + "# Edge Detection" + ] + }, + { + "cell_type": "markdown", + "id": "ceb11e7b-85d7-4598-826b-2a91ad751092", + "metadata": {}, + "source": [ + "## Learning Outcomes\n", + "This example identifies edges in an image using Sobol edge detection algorithm and is implemented using NumPy and SciPy. An edge is defined as an abrupt change in intensity of the image. The Sobol edge detection algorithm uses a kernel in each direction to compute derivative of intensity of the image. The gradient of the intensity will help us determine the locations where changes in intensity are abrupt, which can then be used to detect edges in an image.\n", + "\n", + "This example uses the following packages in addition to NumPy/cuPyNumeric: Scipy, Matplotlib, PIL" + ] + }, + { + "cell_type": "markdown", + "id": "15f468c7-5e09-4456-a770-03ee0e713546", + "metadata": {}, + "source": [ + "## Background\n", + "For more information on edge detection, check this [material](https://www.cs.auckland.ac.nz/compsci373s1c/PatricesLectures/Edge%20detection-Sobel_2up.pdf)." + ] + }, + { + "cell_type": "markdown", + "id": "ed8b8b45-a02d-42a0-8092-d232ef3da30f", + "metadata": {}, + "source": [ + "## Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98e3e73e-e500-433c-94a7-9e0a5593e3c1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from numpy import ndarray\n", + "from scipy import ndimage\n", + "from scipy.signal import convolve\n", + "from matplotlib import pyplot as plt\n", + "from PIL import Image" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "94310f80-baf0-4cd3-b0bc-b49dd62ccfbb", + "metadata": {}, + "outputs": [], + "source": [ + "# Intensity varies between 0 and 255 in the image.\n", + "intensity_min = 0.0\n", + "intensity_max = 255.0" + ] + }, + { + "cell_type": "markdown", + "id": "78273013-cea0-4c28-a376-c3c40e681276", + "metadata": {}, + "source": [ + "Since NumPy's `convolve` API does not allow two-dimensional arrays and our image is represented in an two-dimensional array, we will use the `convolve` API from SciPy for this example. cuPyNumeric's implementation of `convolve` permits two-dimensional array and will be used if `cuPyNumeric` is imported instead of `NumPy`. Try changing the import statement from \"import numpy as np\" to \"import cupynumeric as np\"!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "64c779b4-6167-4854-97af-ac74469d772c", + "metadata": {}, + "outputs": [], + "source": [ + "def convolve_nd(array: ndarray, kernel: ndarray, mode: str = \"same\"):\n", + " \"\"\"\n", + " array: ndarray\n", + " Input array corresponding to a grayscale image\n", + " kernel: ndarray\n", + " Kernel to compute the gradient in x or y as per Sobel Edge Detector\n", + " mode: str\n", + " The default convolution mode. Note that cuPyNumeric only\n", + " supports the convolution mode \"same\".\n", + "\n", + " Notes:\n", + " Check https://homepages.inf.ed.ac.uk/rbf/HIPR2/sobel.htm\n", + " for more information on Sobel Edge Detector\n", + "\n", + " The image was taken from:\n", + " https://docs.nvidia.com/vpi/algo_canny_edge_detector.html\n", + " \"\"\"\n", + " if np.__name__ == \"cupynumeric\":\n", + " return np.convolve(array, kernel, mode)\n", + " return convolve(array, kernel, mode)" + ] + }, + { + "cell_type": "markdown", + "id": "a93b9fb7-f792-48ac-9a80-f5d33d800c9a", + "metadata": {}, + "source": [ + "Read the image and compute the gradient by performing a convolution operation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4edeed3a-9729-4a2c-a52e-95be99e2e5a3", + "metadata": {}, + "outputs": [], + "source": [ + "# Read the image\n", + "image = np.array(Image.open(\"image.png\"))\n", + "\n", + "# Sobol kernels in x and y to compute the derivatives\n", + "kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])\n", + "kernel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])\n", + "\n", + "# Apply the Sobel kernels and compute the gradient\n", + "grad_x = convolve_nd(image, kernel_x, mode=\"same\")\n", + "grad_y = convolve_nd(image, kernel_y, mode=\"same\")\n", + "\n", + "# Normalize the gradients and scale to the max intensity, which defines the edge\n", + "edges = np.sqrt(grad_x**2 + grad_y**2)\n", + "edges *= intensity_max / np.max(edges)\n", + "edges = edges.astype(int)" + ] + }, + { + "cell_type": "markdown", + "id": "2d10238c-50f0-457a-be84-f50791d8989f", + "metadata": {}, + "source": [ + "Now that we have computed the gradient and the edges, we can plot the edges and see if they actually pick up the edges in the original image." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ebe45839-78bf-42bb-ac1b-24eff249d39e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABRAAAAGgCAYAAADSGUviAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9d5RtR30lAO+qE2++nfvlp6eMUACEUDBBKJAz37LBgGzABmR/HgbPZ4KxZTM4IA9jGBYyM4MNTnjAAYGxLSMhkAFJWBH5KUsvp8433xOrvj+qftV172shCcNokM9eq1d3n3tCnUr31D57/35MSilRoECBAgUKFChQoECBAgUKFChQoECBAhuAP9UFKFCgQIECBQoUKFCgQIECBQoUKFCgwP+7KAjEAgUKFChQoECBAgUKFChQoECBAgUKPCYKArFAgQIFChQoUKBAgQIFChQoUKBAgQKPiYJALFCgQIECBQoUKFCgQIECBQoUKFCgwGOiIBALFChQoECBAgUKFChQoECBAgUKFCjwmCgIxAIFChQoUKBAgQIFChQoUKBAgQIFCjwmCgKxQIECBQoUKFCgQIECBQoUKFCgQIECj4mCQCxQoECBAgUKFChQoECBAgUKFChQoMBjoiAQCxQoUKBAgQIFChQoUKBAgQIFChQo8JgoCMQCBQo8KXz+858HY8z8uK6LTZs24Wd+5mfw8MMPP9XFe1L44he/iDPOOAOlUgmMMdx9990b7kf3fPvtt//fLWCBAgUKFChQoMCPEePPdeM/3/rWtx73HIwx/NZv/daPvawF/t/Dhz/8YWzfvh2u66LZbD7mfj/3cz+HarX6f69gBQoU+LHAfaoLUKBAgZ9MfO5zn8Npp52GKIrw3e9+F7/zO7+Db37zm3jggQcwMTHxVBfvcbG0tIS3vvWteOlLX4prrrkGQRDglFNOeaqLVaBAgQIFChQo8H8d9Fw3jmc84xlPQWkK/CTgK1/5Cn7nd34Hv/7rv46XvexlCILgqS5SgQIFfswoCMQCBQr8UHjmM5+Jc889FwDwohe9CHme46qrrsK1116Ln//5n3+KS/f4eOihh5CmKd7ylrfghS984VNdnAIFChQoUKBAgacM9nNdgacew+EQYRiCMfZUF+UxsXv3bgDAr/zKr2B2dvYpLk2BAgX+b6CwMBcoUOBHAnroXFhYGNn+1a9+FRdccAHK5TJqtRouu+wy3HLLLebze++9F4wx/PVf/7XZdscdd4AxhjPOOGPkXK9+9avxnOc853HL8njX/Lmf+zn81E/9FADgp3/6p8EYw4te9KLHPW+328V73vMeTE9PY2pqCq9//etx5MiRkX2EELj66qtx2mmnIQgCzM7O4m1vexsOHTo0st/OnTvxcz/3c8dd40UvetFIWYQQ+OhHP4pTTz0VpVIJzWYTZ511Fj75yU+OHPfwww/jzW9+M2ZnZxEEAU4//XR8+tOfPq5sT+RcBQoUKFCgQIEC4+h0OviFX/gFTE1NoVqt4qUvfSkeeuihDff9yle+grPOOgtBEGDXrl345Cc/id/6rd86jhCTUuKaa67BOeecg1KphImJCbzxjW/Enj17Rva766678MpXvtI852zevBmveMUrjnu+2gh/8id/grPPPhthGGJychKve93rcP/994/sQxbbRx55BC9/+ctRrVaxbds2/Oqv/iriOH7ca3zxi1/E5Zdfjk2bNqFUKuH000/HBz7wAfT7/cc9lmzkX//61/H2t78dMzMzKJfLiOP4CT1XfvrTnwbnHIuLi2bbxz/+cTDG8Eu/9EtmmxACExMT+NVf/dUfWJ4ncs2dO3fiwx/+MABgbm7uCdvYn0j9rq6u4sorr8SWLVvg+z527dqFX//1Xx/Zb9++fWCM4fOf//xx1xgvy9LSEn7xF38R27ZtQxAEmJmZwUUXXYQbbrhh5LgbbrgBl1xyCer1OsrlMi666CJ84xvfGNnniZ6rQIGnKwoCsUCBAj8S7N27FwBGbMBf+MIX8JrXvAb1eh1/9Vd/hT/+4z/G2toaXvSiF+E73/kOAOCMM87Apk2bRr54b7jhBpRKJdx3332GoMuyDDfddBMuvfTSH1iOJ3LN3/iN3zDk2u/+7u/illtuwTXXXPO49/jOd74TnufhC1/4Aq6++mp861vfwlve8paRfd7znvfg/e9/Py677DJ89atfxX/9r/8V1113HS688EIsLy8/7jXGcfXVV+O3fuu38KY3vQn/8A//gC9+8Yt4xzvegVarZfa577778NznPhe7d+/Gxz/+cXzta1/DK17xCvzKr/wKfvu3f/tJnatAgQIFChQo8B8PeZ4jy7KRnzzPzedSSrz2ta/Fn//5n+NXf/VX8eUvfxnnn38+Xvaylx13ruuuuw6vf/3rMTU1hS9+8Yu4+uqr8Vd/9Vf40z/90+P2fde73oX3vve9uPTSS3Httdfimmuuwb333osLL7zQvJTu9/u47LLLsLCwgE9/+tO4/vrr8YlPfALbt29Ht9v9gff1e7/3e3jHO96BM844A3/3d3+HT37yk7jnnntwwQUXHBe7O01TvPrVr8Yll1yCr3zlK3j729+OP/zDP8THPvaxx62/hx9+GC9/+cvxx3/8x7juuuvw3ve+F1/60pfwqle96nGPJbz97W+H53n48z//c/zN3/wNPM97Qs+Vl156KaSUI2QXPUtff/31Ztvtt9+OVqv1uM/ST+SaX/7yl/GOd7wDgGrvW265Be985zt/4HmfSP1GUYSLL74Yf/Znf4b3ve99+Id/+Ae85S1vwdVXX43Xv/71T7gubbz1rW/Ftddei9/8zd/E17/+dXz2s5/FpZdeipWVFbPPX/zFX+Dyyy9HvV7Hn/7pn+JLX/oSJicn8ZKXvGSkXp/IuQoUeFpDFihQoMCTwOc+9zkJQN56660yTVPZ7XblddddJ+fn5+ULXvACmaaplFLKPM/l5s2b5ZlnninzPDfHd7tdOTs7Ky+88EKz7S1veYvctWuX+f/SSy+Vv/ALvyAnJibkn/7pn0oppfzud78rAcivf/3rj1m2J3PNb37zmxKA/Ou//usnfM9XXnnlyParr75aApBHjx6VUkp5//33b7jf9773PQlAfuhDHzLbduzYIa+44orjrvXCF75QvvCFLzT/v/KVr5TnnHPODyzfS17yErl161bZbrdHtv/yL/+yDMNQrq6uPuFzFShQoECBAgX+44CecTb6cRzH7PdP//RPEoD85Cc/OXL87/zO70gA8qqrrjLbnvvc58pt27bJOI7Ntm63K6empqS9/LzlllskAPnxj3985JwHDx6UpVJJ/tqv/ZqUUsrbb79dApDXXnvtk7q3tbU1WSqV5Mtf/vKR7QcOHJBBEMg3v/nNZtsVV1whAcgvfelLI/u+/OUvl6eeeuqTuq4QQqZpKm+66SYJQH7/+9//gftTG7ztbW8b2f5kniu3bt0q3/72t0sppYzjWFYqFfn+979fApD79++XUqq28jxP9nq9xyzLk7nmVVddJQHIpaWlH3h/Uj7x+v3MZz6z4X4f+9jHRtYBe/fulQDk5z73ueOuNd4fq9WqfO973/uYZev3+3JyclK+6lWvGtme57k8++yz5XnnnfeEz1WgwNMdhQKxQIECPxTOP/98eJ6HWq2Gl770pZiYmMBXvvIVuK4Krfrggw/iyJEjeOtb3wrO16eaarWKN7zhDbj11lsxGAwAAJdccgn27NmDvXv3IooifOc738FLX/pSXHzxxebN6Q033IAgCIz1eCM8mWv+MHj1q1898v9ZZ50FANi/fz8A4Jvf/CYAHGdNPu+883D66acfZ4N4IjjvvPPw/e9/H1deeSX++Z//GZ1OZ+TzKIrwjW98A6973etQLpdHlAMvf/nLEUURbr311id0rgIFChQoUKDAf0z82Z/9GW677baRn+9973vmc3rG+dmf/dmR49785jeP/N/v93H77bfjta99LXzfN9ur1epxaryvfe1rYIzhLW95y8jzy/z8PM4++2yTAfqkk07CxMQE3v/+9+Mzn/kM7rvvvid0T7fccguGw+Fxz2Xbtm3Di1/84uOeyxhjx5XxrLPOMs95Pwh79uzBm9/8ZszPz8NxHHieZ2Jsj9ulHwtveMMbRv5/Ms+Vl1xyiXHz3HzzzRgMBnjf+96H6enpkWfpCy64AJVK5THL8ON4liU8kfq98cYbUalU8MY3vnFkPyrPD/ss/fnPfx4f/ehHceuttyJN05HPb775ZqyuruKKK64Y6YdCCLz0pS/FbbfdZqzoj3euAgWe7igIxAIFCvxQoAfNG2+8Ee9617tw//33401vepP5nKT8mzZtOu7YzZs3QwiBtbU1ADBWihtuuAHf+c53kKYpXvziF+PSSy81Dwo33HADLrroIpRKpccs05O55g+Dqampkf8p29xwOHxC1/9h7A0f/OAH8d/+23/Drbfeipe97GWYmprCJZdcgttvv91cM8syfOpTn4LneSM/L3/5ywHA2E0e71wFChQoUKBAgf+YOP3003HuueeO/Nhxp1dWVuC67nHPQvPz8yP/r62tQUqJubm5464xvm1hYcHsO/4Mc+utt5rnl0ajgZtuugnnnHMOPvShD+GMM87A5s2bcdVVV/1AAufJPpeVy2WEYTiyLQgCRFH0mNcAgF6vh+c///n43ve+h49+9KP41re+hdtuuw1/93d/B2D9OfHxMF7OJ1P+Sy+9FAcOHMDDDz+MG264Ac961rMwOzuLF7/4xbjhhhswHA5x8803P659+cfxLEt4IvW7srKC+fn542Jl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B2VhBiD/9//+32bfVqsl3/72t8tmsynL5bK87LLL5AMPPHBc1lsppfzyl78szzzzTPO88fu///vyV37lV+TExMRxZfiTP/kT+bznPU9WKhVZKpXkiSeeKN/2trfJ22+/XUop5QMPPCDf9KY3yRNPPFGWSiXZaDTkeeedJz//+c8/oXv87Gc/K8866yzp+75sNBryNa95jbz33ntH9rniiitkpVI57ljKNPx4uPnmm+UFF1wgy+WynJmZke985zvlnXfe+ZiZgm38oGfrJ/pcKaWUX/nKVyQAedlll41s/4Vf+AUJQP6P//E/Hvc+nsw1n2wW5idavysrK/Ld73633LRpk3RdV+7YsUN+8IMflFEUjezXbrflO9/5Tjk3NycrlYp81ateJfft2zfSH6Moku9+97vlWWedJev1uiyVSvLUU0+VV111lez3+yPnu+mmm+QrXvEKOTk5KT3Pk1u2bJGveMUrzFrhyZyrQIGnK5iUls+vQIECBQoUKFCgQIECBQoU+BEiTVOcc8452LJlC77+9a8/1cUpUKBAgQI/BAoLc4ECBQoUKFCgQIECBQoU+JHhHe94By677DJs2rQJx44dw2c+8xncf//9+OQnP/lUF61AgQIFCvyQKAjEAgUKFChQoECBAgUKFCjwI0O328V/+S//BUtLS/A8D89+9rPxj//4j4+bEK9AgQIFCvy/i8LCXKBAgQIFChQoUKBAgQIFChQoUKBAgccEfyovfs011+CEE05AGIZ4znOeg29/+9tPZXEKFChQoECBAgUKFChQoECBAgUKFCgwhqeMQPziF7+I9773vfj1X/913HXXXXj+85+Pl73sZThw4MBTVaQCBQoUKFCgQIECBQoUKFCgQIECBQqM4SmzMD/vec/Ds5/9bPzRH/2R2Xb66afjta99LX7v937vqShSgQIFChQoUKBAgQIFChQoUKBAgQIFxvCUJFFJkgR33HEHPvCBD4xsv/zyy3HzzTc/7vFCCBw5cgS1Wg2MsR9XMQsUKFCgQIECBX5skFKi2+1i8+bN4PwpjSpT4IdE8UxaoECBAgUKFPhJxpN5Hn1KCMTl5WXkeY65ubmR7XNzczh27Nhx+8dxjDiOzf+HDx/GM57xjB97OQsUKFCgQIECBX7cOHjwILZu3fpUF6PAD4EjR45g27ZtT3UxChQoUKBAgQIF/l14Is+jTwmBSBh/Uyul3PDt7e/93u/ht3/7t4/bftK7fhNOEEJ4AMYOkwxgEpAcYGJ0u9lXru9zXNmE3q4N3kyOHTtysdF9Nvo9XiaMGceP22f8Omzsbzm2TVj/0+ePdbzE6DnkBvuM399j3Td9Nv65tH5b9WvXx0bnp/se3+exto9cy2rXDcvzWNvk2Ocb1d8G7WW30UZtZpd1vO3t/Y/rL9T3NrjeY9ULGMByfRzb+Br2/nZ/ZgLgid7mACzT42a8rzL1OaDHlAR4tl53Qn/G8/VrSXfjMQcak3x9uz2OIHU9ONYxWN/H3KddB/z48Sbc9TKbusz1PWdAHqzfz0j7sfU2oHsdb6eResR6vdvnGPl7ozG6Ud8dP5Y2idH73ag8j3XscWUbG5c/DEbq2urXPFX9KQ/VNicZ29++R913TB/R9S51W470j8ebn8Z/259jg20btZe0+naq+zfWyzgyjvT+0ln/nuGpdS5h/U2X5ev9imfrfVM6avwIb/08LFfjB1B/U7l5rvo1z3T/5nrcivU5wO4bdF4bLAec1LoXqH1zT52LZ6oNoccQmL43hg3n4o3GOMtHP6N6tSE51Ljl6+UfmW/sPqr7rJlnstF9hbs+30hPfcATpu4DVpnk8XMCnWN8TI6MOXvsYn1fM9/Z84WwxiW36kys9wH6PI8jPPI/P4JarbZBoQr8JMBuu2q1ipNOOgmNRgNCCDiOA8bYyA+Bcw7HUZ2RMQYppXkOZoxBCNUBhRDmb1sVMH5OIQTyPIcNO0rR+Plp27jSgI4Zfx7nnJtt9jnoeMYYXNcdKTPdn31O2p+uTZ/R3/TDOR/Z1y7zRusHuu54PVNdjm9/snisehmPBGXfD/1v1zv9b+9v/1Bb2+ei7fZndA3GGNI0HWnH8f1o20btPl63G5XVbvfxMtN+VBa6zvh5Hqu+7HMCQLvdxp49ezAcDk1/kVIiz3O4ros8z5EkCRqNBiYmJgAAa2trGA6HkFKaPkf14/s+AOV+S5IEQghzHiml+dy+1nh7CCFGzu04DjzPg+M4ph5pvyzLzHXovIwxOI6DcrmMSqWCIAhMfeV5jn6/jzzPcfLJJ6PZbI7UD5XDLrvdB6mMnueNzBvj/c8ea3Q8zRee55k6tvvORuOXrpFlmbnOeHns+rfnCaqv8X3svjI+dsfxWFzB+NwopUSv18NDDz2E4XCIbdu2YWVlBYcOHUKSJCPzlX2vcRxDSokgCBAEASYmJrBjxw7UajWzP/WF8fGxUTk3mnPtz+x6tetPSok4jjEYDDAcDnHs2DEMBgOEYQghBOI4NmMiTVP0ej0wxlAul03ZKpWKKY/neciyDFmWQUqJNE0xGAyQJAk8z4Pv+xgOhxBCYHJyEqeccgrm5+cxHA7xyCOPYHFx0YyVwWAAAMiyDIwx87tcLmNiYgKTk5MYDodYWVkZqRPHcbBjxw5s2rTJ1LcQAktLS3j00UfNmKfxQuO71+shjmO4rgvf95FlGaIoGhmz4/VOfTSOY2RZZvo29UOqI6oPz/PgeR4458jzHGmajnxm91/XdY8bM3mem2vV63WcdtppRskXRRFWV1dx7NgxrK6uIo5jM0/a85v9/UjntPsD9XPqO/QdS2UfHzu+78P3fdNGSZIgTVNzb/Zckee5mQ+eyPPoU0IgTk9Pw3Gc49SGi4uLx6kSAeCDH/wg3ve+95n/O50Otm3bBicI4Tkhapcfw7OnD2KY+/B4Dl+v+hwIlPQqyWECHstR5glyMHgsR6pXEY5eMUTCg8MEIuEhlY45JtcrA4/lcPSKwmM5unlozkGIhQshGVwuzN9CclTcGLFQ1R3wDJngKDkphGQYCB9lnoDrFRldg0PC06swAYYyTxAJD2UnNmWo8BgcAql0kUoHIU/BIRBJHw4EPJYh1KvaSHio6NUU7Uvbc3CELIXHMn09Dq5XwPbfNhwm0BcBfJabMtDx9LfPciS6LkeOhUQOZuo+B0Mu+ch+kfDhsey4Y1O9quYQEODmOCqjakN/pCwOE0istoqEZ44FgIEIUOYxBiJAKh14LMdA+Ij0ytthArnkCHkKj+VYSOtoOEPE0gWHhABDJDx4LIfQq8WB8BHovhgLF2WejPQXziSEZBjmPkpOAo/lpo/Q55ypPjDMPX0eDwFPTRlLTopOFsJjOTiTSKWDMk8QC9ccT+d0IMxxqs45HAjctrwD7X/aBCeW6G8CyocZvK5EfxNDXpHgGYPXAeIJCX5aD2nigh0M4XUZnB7gRvoLMAUgAb+r/k+qDFmFQThAVtKEhQSShlxfSDvrC25amLOcwYnUNqm+q1A6JuFEAM8keGZ9UTgMuc+QlYA8YMgDa4EOIK9KZDUBWc3AHAnGJLwgQ7JQhtfiOP0Fe3DZzH2mXqndHEj8W28LhrmHs2qH4TBVd9TWqXTQcIYIeIpeHiKXHDUnMu1L43SgGcqyEyOVDoTk6OYhtgcrGAgfqXQwyAMz70x7XQjJkUrH9De6xkD44JBwmFB9QLgQetwA63OEx3Lk+u8Kj/UcxiDA4bEcHsvUmIAwfWF9XsvM+Wjb+P8ED7mZG2icD2SAD//x25BOSrz2lbfgS987D817XAhPES08kaad9TSGeIIhC4GsLJFVJEQ1x9uedzNODBZQ0fO1kBw1Z4hcckTSQ8hSM65pLqDt9NsetzQfRVKNI1/XQy75yDzQFSFu6ZwIjwn84y3nYPO/AOFKCuFxJDUHecAQtHNkJY7ODgdOBHS3C0gXqO7jmLk/Qh5wSM4QrMVI6z6Eq244K3H05xwkVcDrA5XlXJ0jBnpbJZ59wUO4ctM3sZjX8IGb3wjvoI9kNgNLObw2h3ABr8uQu5rcE0AyJZFPpXjxMx7Av+w5CVnLHyGGWc5wzpl78CtbvmHNKwL/Z+V8/PN3zlFjiANwJFjGEG7rIs854tUSWMogfQEW5pBDFzzikK6E5BIsZ5rgk+AJh3QkWMrgJAxun8EdANx6AUBtLzmQlRmEr0jNPFTbnCGD1wbg0nZVrjzQ3w8lCVESYCkDyxl4wpBLwG8zQwaf8pI9eNeWb6EvAtzd34FvHjsZx/ZOwWs5imzMGIQrDfnKE72ACiSYYGC5RRhbLzOEA/OCxh0oItWJJdxIGkJccsBJJLyBgBMJRQyGDtIKRxaq+819/bsEiBDg0TqJC2y8UCrwkwFqO9d1sWvXLlxwwQXo9/sjpAotDGgBQgskzvkIOeE4zsiDvP2AT3/bi/pxIsAmoGwygT7L83xkgUJwXddcgzEGz/NGrmcvcGgRTQQCkaR0zXFShcpP+9gLR6qXJ9L/baLSvmcCleMHEW2cc0O2bbS4txfxG53HLge12zgpTKDFqr3Ys7fTNvta42QuLXztNqBFo02a2te0Ye9j1zPVvd1XbMLMJm83qptxgtsmf6iMtMC2iXS7zWhxPH7eMAzheR4OHToExhgmJyexsrICxhj6/b7ZZ/v27ajVaiP3TJ+laTriXBNCwPd9VCoVVCqVERLCJlHSNIUQwpBH42R2kiRmHNNinHOOSqWCMAxHyDJq0zRNzbirVquGhImiCHEcI01TeJ6HcrmMubk5DAYDNJtNhGE4QiK1221Uq1Xs2rULvu+bslHdUl3bJB/V6fhYsfsvgA2JPZtkzrLM1AGNf3uOomvaRLbd7kR8EvlJ5bLnDruPUR3aGH9ZshHpRvvR3NRoNLBr1y6zT6fTwfXXX4+1tTVDqBGJ7Ps+OOfwPA95nqNarWJiYgInnXQSTjzxxJGXQTZJbb8QoPoZJ/7te7L7xjhpZM/z9H+328VwOMSznvUs3HvvvWi1WqYvJkmCMAwBAAcOHEAYhpiYmECWZdi+fTuGwyEGg4Ep13A4NH2cSNJOp4OpqSlT3jAM8exnPxvnnXeeuc9ms4lbb73VtHkYhkiSZKT9iLjcsmULpqamMBwOTf3SPc3OzuLFL36x6b/Uj2iMHDp0aITko/Fjj8c4jsEYQ7VaHekj9Hccx4aQt0k06hP2WAmCYOQehsMhAKBUKgEAfN8fIQ+JrKO+7jgOpqamUK1Wsba2hoMHD2I4HGL79u14/vOfj1arhf3796PT6Zj5l8ZStVpFEARmXuv1emYcEiFLLxromYDu0XEc8yKCxmOapiN9meoriqLj2sJ1XQRBYO6FiOVOp/OYhPg4nhIC0fd9POc5z8H111+P173udWb79ddfj9e85jXH7U8dZ0NIoOrHmPc7SKWDmhMh4KlZHFe4+gLJwfVimshFCQ6BkKcQelFKRFcuuVkQp9JFqEk3OkaAoy8C5GDwWY5IqGPthSiwvniNpAfHIuA4k6ZchAqPEQlPE1RqgZ9LDo9lqPAYDiQ6IoTDBCos0fekCDiPZfp3bog5j4n1v61rp+Co6DrgAFIwlDVxmUrAY4rLCcc6T05fMowhlxIpAE//n0gJB0AOwNef0/aQMXSFRIWz4wRPDhg4YxBSItcrNrHBtdbrkyGSErkEHF08EtbQOsyzjssBOAAGUt2rw4C+4Ig1yZOAw4dAAtVGFZagLxVp6GtihfpFyFKkcBAJ9cW/2VuziDhmCFwifagtc00mptJFhcfoiBJ8liESniGMhWSGTLRJ6pCl6ItAk6hy5FzURzyWQYAjkS58TYgoMjjR5ffNdiJPRttA4JvxyYAESssCg3kHaR2oHc7BUw7hMghfamUTQ5Q6yBOOUp+hdlCit5VhOA+wDGCCgafAcIYZBZMbSYQrarEtXKYImFVNEgQMTEAvrgERqGOykkTSAIQvwRMGN2IYzjCUj6pzSM7gxhI8U+Rj0M4RrgFMSEjO1D6OKi8A5B6DdHxFUlUYch/wNGH5wLdOxD2TO+FORwhLCTwnhxAcw9hDeqQCEQq86uLvo+n04UAaEjqVLvYn0/jmyql49czdONFfRF8ESOGMkFienh9UG63PESFP4CFHJFVfo2NoDkqlizJTc0SdRwBgxn8qXeRgqLAEK3nVzE+pdFHjQzNXhCw1BKIPochoCAg9P+SSIbRIQRr7XI9jrseoGl8SHIDH+Mh4dbA+T6SQ+MixF6N2QCD8+aNwIFB91AUTiqDhiYTwFeHjxBI8lUhqijwUviKNnM0DnDq3jBdUH0CNR/AhUOMpPGs6SvV4jiRHkwszL9DY7wsJj6mxT/NQqqcRuls6HwcwkAwhU/NKk3PMuB08HM9j+2kLaD+0GeFyCn9pCJ4EyEoOwsUBkqkSIB3kASBCCQigtCTgrQ6Qb6rCbyVgaQ6WSXAhIR3VL7OyItW8rkRW4mACyMpAPpniP22+HheFHEezw3D8HMIHkDPIQCCZkHD6DuIpAUigcoijd2IO6Uh45QTbS6vIIhesnENKAAkHXAmUBH5x87/gRaXRReU3/C5EPQNSDpYxRQz6AoOFCuALgEvIQAKCAT0PcCREWS9AuITMlRSRJQw8ZvC6XKkaYxgijgkJKRmEC6QVIJ/UpKOjSEOWMzhDPVekQFZRimARSKUe1vOOCKRSFErAnY7wzC1HcHJtCV/63nnw265RHx5qN/Cx6GU4uGcGwaILJwJKDs01EnlDAI7qCMGiAydR8450FMEpHSDz1csNHjNwrVT2IsDrSXgDqfptLrWKkYHlEn4nM4RjVnYQTbrIQo60AgiPIQ9V+bKKXFdkA2DpujqywNMDtKhJ0xTXX3892u32iIrAVmPRQhVYJ1LsRSftSyqB8QW3rSazF1u08LCVDbToSNN05DP7vDapRee0SSWbrBgnzmi/cRLCXiDbShubALWJNLo3WuhQOQCM3Je9nRa0dE/2gpzKaZeJyk/Xt0nMcTLNPi9da1whYtezTdzY5xkndux7s1VMdA6buKM6tO/HJqZtFdt4mexyUp2Ot9E4QWardIgIscknars0XX/7QeWySWi7jITxthwnwIQQ2LRpE37mZ34GYRii0WggDENMT09jZWUFQRBgamoKvV4PKysrqFarpn4mJiZQKpUQxzGiKEKlUkGj0Rgh5eM4RpIkRv1DC+lNmzYhCAL4vo8kSQzhQvfS6XQMWUB1VC6XjbKI7inLMqRpakjGcrmMMAyNYozGfBAEZvFP/ZFUSbOzs9i1a5dpo+FwiF6vhyiKUKvVUK1Wsbq6ikceeQRRFBkFWrVaxZYtW1Cv17F3717ccccdhjCh+cXzPFNHSZKY9gdgXhjY7W33Abv/2Mot+wXB+NiiPky/iUAkEmi839njkz6z5zV7jhwfX/bcQNfZsWMH3v/+9+PBBx/Etm3bsLa2Btd1MRgMRu6T+sdgMMDk5KS5RhAE2LVrFxzHwfXXX489e/aMkJMbkZk2+U7XGJ+DqA5sZSuRQPY5hRAolUqYnp7GYDDAa1/7Wpxxxhm46aabcPjwYTQaDbiui06ng9nZWTiOg8nJSbTbbUxNTWFmZga33HKLqT8hBAaDAaIoQhAE2Lp1K1zXHWnTPM8xOzuLrVu3Ys+ePfj7v/97vPCFL8Tk5KR5KdbtdhEEAXq9HsIwRL1eRxiGcF0Xy8vLmJubw8LCAtI0xb59+0bmv/n5eezfvx/XXXcdkiRBtVrFpk2bcOaZZ2J1dRULCwumrTnnqNfrYIxhOBwijmM4jgPXdY0al152+b6PcrmMUqmEUqmEIAiM+jCKItMuNM7puzeOYzO+SK1M7Vev1813Jp3LcRzMzMygXC5jamoKu3btQr1ex3A4xNLSEvbu3QvGmCH9t2zZgomJCRw8eBBHjx41cwONN1Jy0rxAxCT1a5pTSfEYBAFKpZIhkHu9ntmfvluzLEOv1wOglIRhGKJUKpn7IAVimqaGXCTimI5/InjKLMzve9/78Na3vhXnnnsuLrjgAvyv//W/cODAAbz73e9+cidiwCD10csDlLWKi5DrRTugVHSLWR0hS7T6JEfIEwih9vGZWsyHSI1aTS3WgVZeQY0P0dUqNYcJOEygm5chWArOBBxwpEIph4hMKvMYOfQ26HJIjoCn6OahIYsSvX+ilXUdGRoCKwdDmrsjxKchD0QFFZbAgUQkPb09RZll6EoPDiQcSAjG0Jeu+b8PFyHLEUmOVHJ4PEUkOXx9v0TO2UThQAJlUg4xhlRKRBII9cqpJTgqXCCSEpFkCKHIw0hKOAzrhJ5FMKpVGBBJAU+fkx5J+oKjxgUE1gkDT5MYERTpCAAtqe6FyJAc64rSXK4TiwJAZJGHuSY++tJDKl0k0kEXoanjjlGjKsKwK0qGBKY2JULIVoDm4Ehzx6gBI+FpAln9z5k07UzK01S6CFiKgQjAmUDIUuTgiOCZaxFS6aLmDCGoX+lzAjBqSc4k2nlFE5tKKRdrVa2tvAWAMo+xtlBHMwdKyym8noO0CgiXKYWNkIiaHNJRysJe20ew5Ch1IAOCVQmeMmP1y33AHQJpTRECkQMwwZVdkyk7q9+S8LsS3qJQpKJPlkqJLGBIKwxpVXkG8wDw24oEiCcYwhUJnq+rG3mqzg+hyEqeKiWQGwuwTCIPGBwJ8FyitCIg+Tq5mJaZsYtmYRnCLSMrMYgA8BzA40Duc3z4m28Ar6TwwwyhnyITHMNBgHzgIjjqAhcD/2XLP6t5AYrgzyXHsayJ73ZOxunlozg5OKbmE6H6iSc5HKbUhDkY+tI3Lw2EVg6nUHNJNyuhwmO08jImnZ5pu5W8avoEvWyIpAcPau5I4eBY1kTT6aPJh8glQwTVZ2pczRtt4aHMM3iQaAvHvJQIWY4aV2OUSEIBIJbCvAig7S0hsJCXkIPhn298NsItDK/ftBv/857no7kqkZfYiD2VZ9LYmoWnSCXhSYhA4OITHsUvzH4LIcsxEB44z9EVHjwm4DGh5qsxL2xXSPT1PODpeSHWH3MpkYIhlww+E4Y4bAkXFZYhB1Pn5wkEgK4UaPIBjiZNPG9mH/5m1zxqB114a0P4iz34tDCeL8MdSEQzDJJLuAMH5YUEYAx5yBEupBCh9fXKGLISA7RV1+9L9Oc53KFEfxKYnuvgfP2OzGMMfpAiFSVFeDEJluuFYEnAX3KMUo4POcL5FN9b3Qk2dCDLGZDrm5SAX0rxgrCLnhC4MwnxL73T8FB/Fv+2uBnMkZCpblgJINPHJeTFBeBKSFes23tzBt5z4fYY3AFbt3nrcSQdRQIq0owhL0sIh2zZDE4MyJwZIi0PpCIOSxI8UucTrlYEZgz5dIIzTjiC5089ghOCRZwfHsYmp4S/7U3jr5PnmTAJeUkiuXkKq9CkYSART0mIQH95CEVYskiVm0lFahJZmftqHnMHSj3pDiTcoZqTWK5IRZrjnEjCiXNDImahg6zCEdc5srKaz/JAKWqVYlM3B1Wrti5vGI6jwE80aFFtWyVtSyAtYsZJRdrHJsdsdYa9aKbz0PWAdeWBTTCNqwhsYspeINDCnhbxdJxNctkLd5tMspWTUkqz2BpXFtoLSFtpZyugaHEzrnwYJ7rob7qmTXzax9t1QMSfTbCO1+m4Qssm+2yiwyYebILPJmrturG3bUSAJEliymdf196HCMTxtiHljq3osut7/Lr0mV1++t8mesdJT5sQpbLYKqpx4pTKNk482tew+xsd57qusVz2ej3UajVMTEwgTVOEYYh+v2+IEM45lpaWsHnzZgwGAywsLKBerxtygZSKQRCYspAyh5Rntg2RFvaDwQCdTge+7xuVFeccMzMzaDabSJIErVYLg8EAeZ7D8zwMh0P4vo9+vw/XdVEqleB53ghZRoRdnufIsuw4MpKUQDfddBNuu+02VCoVc448z9HtdrG2toYdO3bgvPPOw7Oe9awRC2ocx2i321hdXcWznvUsOI6DhYUF0/6u62JychLz8/M4duwY7rrrLnS7XdN2ZA+dmZkB5xwrKyuGeBnvP3Y/tklnew6y+7PdJ6gsNrlJY8Tud9RHxl96EHFtjyP7b9d1US6XEQSBIaW2bduGpaUlNBoNfPOb3xxRm9pEu614I7Kr2WzizjvvxIEDB8y1x1+22CQq3TvNMzYhT/eQJMlxZbbHEt07zVeAst/fc889eOELX4jt27djeXkZrVYLjDEzZjzPQxiGWFpawtzcHFqtFhzHQa/XM/2IvitIlBVFkSHper0efN/H9PQ0KpUKHnroIXS7XVMfdA2qLxojExMThsyyFbUHDx40415KpYyr1WrIsgznn38+qtUqyuUyGGN49NFHsba2dty8vba2NjIn0lgmMrxSqRjlI42zdruNKIrg+z4cxzEqPluB1+/3jQKY5gG6H1JD0vc4kYXbtm3Drl27MDU1hSRJsH//fqysrMD3fVSrVVPfnHMcPXoU3/jGN1CtVtHtdtHtds0LB1KM2iEZqBxxHBs7Ms2HpJ4k8m91dXXDMUcvJYIgMIQu9R0iYKl96DfN0aVSaURV+0TwlBGIP/3TP42VlRV85CMfwdGjR/HMZz4T//iP/4gdO3Y8uRMxpeDyeG4spIqEyeAhQwSlwOIQqPAYNT40KqBccqRQC+++CFDhsbLHylG7nmPsuetfhhyK6OFMQEiOXDJFOGkiL+CKECKsW5GVDZVre3SirYQA4LMMHVFCyBIk0kEkfUxpwoAIKscZwgOMwogIUo9l8FiOliihzGP4UAvuSBOTQnI4LDeL8IF09UIc6OrFd6BVO4BalKv7VGodjymFYSTWJz0BRSwCQIUL5BIYSAc1niOV0EQhEGmFnQMgkdKoFDljyKHIRDpXjSvSUZWBrJPKJ9bVJCURIR4EaixTtceUIqnCFcHRFZqggUQiubJ6Wj9E6OVYt2eSEtVhqVF6Ud3m2qLssxwOhCEgc8mR6GHEmQSH+txW+wnJNRHqGIsxtb9SHwrEuu/mtqJLuuBMYKDVrg1nqKya2nIuJFf7S2hrvtpG3AopUoF1wjTSfYHGSVeE8Bdd8ETC6afwugGiaSCucyR1RfykVaUQ8ztA814XTiTR2wZ0djF4PSBpKgWWO2Twuorsc2IdVzFn4KmE8BjSqrIyZyWGeEIpc7yesgXmoSIfAfV/uCoRruWQDkNSVfbNaJIhrTEEawI8VfedlWDIjazKIJkiKnh6fIA/nnK4Q8DvCbiRAE8YmO7PXl9AOkzbn5VqKg+U2rHxKId0AuR+iNwHHJ+hQrHrOHDvDafgTVtPgFeLwRjgebmygfZ9uEd8/MtpJ+K/nfU3RjnI9TyxJ5nF3x89C/3Ex//vpK8j5AlCphTFXVFShCDNaVot3RWlETs+qaK5HsvU3nUeoSXK2J9MoxYO0RUhOBOos1i/KOCoqQZCCjX++tJFkyeIpINIOujnDDWWocJh1MT22AckBIBV4eOOaCfu7OzA5L1A9Po1rKUV+PeWlVqNKXJX2dUleKJIxCxQfUCSRXQywUWNh/XY5HoOFxCMIWQ5EgolAYlIcvPCIJIOUsnVCxJrjk6kemFQ5krB6zH1UmFJBPChSDEPUqm1mXrB0JcckzxCwxliIHxsesYieg/NIVgN4XZjQAiIso/BjAsnBqJppWorHWPw2wmyWgAnlkAmIAId20wqFWxaVv3KHSoCSToAUiCtSVy25QE4TM9FYKiECdYYwIcORC2D2+fIygJ8wOF3GLo7lRJRehLdpSoeaJUhHanIw4wDrgDzBKRkeOm9P43DS01IwcC4hEg53CCHFHqh7WpbOdfMlrYE6yYGMg4+5PC6HF5vPT6jiYfIlS03rWvSTJOkbo/B7a2rjCVTn1OUBhFKiFIOPnDAI/0SggN5M8NFz3gEPz/3bex02wiYUole3z8N7370Ijx0ZA5yIUC4ypHVNPmQA2lVqut7cj0cgrZU83y970IoNSAk4EQMPFZzjjeQ8PpKZTgSDxOA183AM6UkzUOOaMpDFjL1U1JzmwiUeprqTbrSzBHK8q3GgJTQcRELy/LTETZ5BowScOOE2/gCwFbwjJNChPFFuq3go8/txT4tRG3ChkDHjxN3NrFnk4BUFrLU0oJjXEFpk0n2At0+BxE444vvcZJsXCFoK3RsBd9GCr9xwmPcWmmfb5xQHG8DWzFEsO+NzmHfh63As9tofBspR8bb0SaUbfKO7sEmHOx6Ge8b4/dvE8WkQLJJbPsadt3Q/nYdjCsMx89h96nxdnUcx1gFiSTcsWMHBoMB0jQ1C+h9+/aZtqbYdbVaDa1WC/1+3/ThdrttPmeMGeVfuVzGcDg0i3iy/RGRReozGoNpmmJyctKQE0T+LS8vI4oiQywAikQhUiZNU7TbbdNeFCORFvN2exMBQvVHaiA6FxHLRLoQCXLkyBF89atfNbHgfN83MRRJSTQ9PY3LL7/clIlUVkQwnnnmmfA8D4uLi6Yta7UapqamMD8/jyiKcOedd+LgwYOGsKUxT8SGbYPdiFgfJxzpGqVSCVJKYxW1z22T/eNhGogUIbKU+qs97u06B4DTTz8du3fvximnnIK77roLgLL5Unlt9S+gbLk0NsrlMrZv347Dhw8bWyqVxw49Qf1+/CWC/dKIjrPnN6onm4wfJ12JMO12u6jX63jkkUdw5plnYufOndizZw+WlpaQJAlKpRJ6vR7K5bIp+/T0NHbv3m2INCKJaVxVq1XT7xuNhlGiNRoNNJtNMMawd+9eQ7a5rmvINbLgl8tlNBoNdLtdeJ6H1dVVMMZw6NAhE6aO+gUR9aurq5idnUW5XMbevXuxuLiITqczQijbL6HI8k5joVwuG+IRgHlZQP2fCPyJiQl4noc4jo19mMpAcR5JCUzjM8sydLtdCCEMyTs1NYUXvvCF2LJlCxzHwf33348HH3wQz3nOc3DOOedgOBwiz3Osrq7i7rvvNi8UpFRKzV6vh8FggDiOUS6XzfxJSmfqt2Qpnp6eNiEc8jw3sRNJsWt/F2ZZhkqlYlSXREwCMCrE4XBoxgwRhht9x1AZngye0iQqV155Ja688sp/30n0d34uOYa5WmzXHCUpJ2VWNy9p5WBqFuYAEGpFUI2nyPXiLZdcKwrXHyaI2CECoM4i5ABqzhCR8KFCuHGtAlLqRp9lhngcaBVbwFNwMBOfy6jLGLRtOoOQDClchDxFiAR94cNnOXIoktKQWsiN1ZrIg9W8aqyQA8lR45FRoQFAIn04iBHpcyRQpNkMH8JjSukXQ5GFiSbjUkvFZ1Q9AEKmCL+Aqe0cimDMwRBJTdCB6dWSIiYjSSomiYABqf4stdSNkVQqQyIkhT6nsosLtIQLDwKhRchG0tHqywx9sR4PThHBdA2uz+8iZBm6IjSxMPvSN8rOXAfPyyU31uWBCBQxLDki6Su7ufSN9djXBDQRemWewIEwRDGRzNQHcp0dgGzIqS4/9R1SLALAWlZBmScQkiMWipwWUCrWnv7fgUQ7LylVrW5/pTYViKUHLhXZfSidxO72ZlS9GHUvQi6VldVvMXh9AR5n8HtSxavLlLXQxCILJdBhSCtAWlEKnjyQKC0BpUW1TbpqIZ81laIHDPA7ivQTviIU/Y62OKaA7AN+W8LvCyRVjv4mhrQmkTSBaAbopq5JNuAMgXBFqjhiHoM3lEirDMxZT/IgocmaQBEWJvmJTiyRldVCP5py4MTEAELHN5MmGYLXk3BjCW81B8slhK+s3O5QGtWQslIrYqh6WAIPOBBuGcJTlty8xODqBBf53Q28Z+2tAJdgDGCugBi6QMrgrzkQDnDd1Jk4vXJUx2lVKudWXsYgD/CNxVPxgplHcEHlYbRkGQ7WY/jRnOVYc1YKB5BQseC6W7HVX4HPcjT5AC1ZQk1boiNNrKu5SB17MKvDYzmOpBM4kk7g2aV9mHSUxH6SZwg12U9jVc2jOU4LjuAT//JKVBoMrz3hHvyfB56D2oKObxcrK2oeMm0BVfUudMKO3JfIfeD0rcewzVtBJF1U9BxJduuAUcxTBYcpEjHV5faYQNmaE4hsXCf6GfpapWzs3JpQrLBMq5W5qcOzy/vxT2tn49yZA/j7U2dRO+QBUoInOZJmgCxU6jNZUjEKawdzIBNIp0P4rQRwOYTDwaSEhCKa8pIi0byORFZSaru0xiDrCV7bvEPN6lLAYxzz1S5W3SkwnSgkqwo4PQ6/zTCcFxCVHN6qi3QiB4s4WN+BrGWK+GJqQmZcIl0s4eBKCIQC3MshMw5kHFnsrBOF9jqPS7BEEYZODDixstm6w3UyTcUm1Mo/X80NTKqYgl6XGbUxoMZhWpZKYepBKRqDHDzQg63nwetwCE8iL0u4fYZ3nPcd/Gzjdtwc7cBfLF2Ie1fmsXRwAqVDLvKSBHMBb6iVq64iPRlXKlYeM7CYGcUjxVaVXBF6PFWxDv2WpTSMdDxDAaM25IkAy/VCN+BIGi7SMkdWUqrZrKQU1sKVEL5U5C3Wr8UEDFGqklHJddEsW6+fAk9PjCvfxtVsAIxVyCbBaDuRY8BovCZb4WIr4mwyzP57XA1kq/7sMo2XzSYtx0kxOg99RvuPL4BtSxyAkbhp9uLavj+6Fi3IN1L5jN+bXd7xctt1bJNzthrIVgc9FhE3Tv7RdTdSXNnn34hQGS/nOJlok750n3Q+IkbsfjB+zzZscoU+t+/pseqe2mkcNmFM5RknBW0C0b4ebaP9XdfF3NzciE3adV1MT08b1RFZibvdLkqlkrFf0kKZiCyyAzLGTCw3ik9I6r0sywxpQrEF6XyAUlY1Gg2jkAKAVqtl7Jq1Wg2+74/YJ9vttlF3EVFJBCApzSjOIhEhpHCj8hMJSERurVbb8AUEqYcAmAQIZG20Y446joObbroJd9xxByqVivmMyM3BYIAgCHDhhRdCynXFdKfTwfLyMu69917Mz8/jJS95CZaWltDtdg0B4nkeqtUqGo0GVldXccstt6Db7Y5Y+22SlPocle2EE05AqVRCp9MxpJGtZKX+QX/TnOH7PhqNhrm/MAyNeozqhPogKcfOP/98eJ6Hubk53HfffXjmM5+JG2+80Si97JiOdp+luH5zc3MIwxB79+4dUT3aL0foenQe6tv22LHnVnsets9F1x5/EUNl6/V6aDQaSNMU9957L57//OdjdnbWEMClUgnLy8uYnZ1Fr9fD1NSUIZWjKEKj0UCv1zPkuu/7ZjwRGdjv9yGEwOzsLCYmJtBqtYyCtdPpGDLfTqRCpLfv+1hbWzNKuZWVlZF4kdTXAeD+++/Hvffea0hFxtbjZtqWeJtkp7ihVMdxHBvylOYNUhtSW9C4pNiidG6K+0ffD3mem8QtlUoF27dvR6/XQ6fTQb/fx9TUFKanp3H//fdjenoap5xyCubm5rCysoKHHnoIBw4cwMLCwkjb0733+/2ROIM0J5EVmizJ9vySpimGwyFardYI4WerpUl5SZZn6kekRqa4rkS0b9QXqT+POx+eDJ5SAvFHAgljF6Xf3Tw0CUI8lmHS7WmyJFMqM51MhMi4SHomJmEENTCU4syHwwRqfKitgQN0RcmQiQ5TSQpUnDpF/LTyMhwIsx+gSKNYKCKnl4fw3Nwk6SAVWipdOFwtbEn96LMcfRGoe5HKWqwUaD48PjRltzNbcm2jjKSHvvSVxVkr6JRdmFR1lDxB2x2lUiARCacW6EzH3Vu3LeZWzEQiDyPpgCOHgLZYa4UgkXy59msRSUm2ZED97TFFLqq4ZkyrOUeJP9U2TCmTeI5UcnBN+vWliyndfilIpSQQSwcDbd1uiZKxB6dGtZkrNR5yYzH3WKZizUEi1Wq9RDrgEEig1KK2VRjQyXe0oimXDmL9uSIkVeIMm5SOpAuPJ1jNqpogyQ3J6LAUHs9HVKpELqqkLKr8yiYtALluZaZGIsVjJF20szIm3T4CnmJ3ezOWhxUc6dUBALOVHvqpD7evVHkA4PUVqyAdoHo0R1JRsbywAgRtgWiSgwmJLGUQU0BnFxCsMmPPE2VF9vlt1fedWCqVDjRhxNTiOpuSEAEw2AxU9yvVpNcHvB4zWZqZlEjqqixZGUgTBq8nkVYUmRe01d/SUdcRHkPcAKSn5gXKBsvT9QW7IrTWMwQLHya+IyV8iCcYWAbwzDG2W0cn/3Bi9bejE0IoJeO6YkkRCCrWY+4z5J4iMxp7XGShGoN5qKzZlD2e5cD3vnQ2vls+G2lFIi8LyFAAuUoY4a9yXHt2GZeeuRt1HiHRNmRKJLIiqgAyeJp0oxFzZ28Hvv3AyZg5q4fXTNw5EtvTgUSs58yQ5TiWV7CU1U3M1XsG2wAAh70JfG9wIi6oPIwZ3jMxEgGY+KZ96eJvV5+L5v3A8vmpUtjeV9UxKZWyK/dVvDhVr1KTMLpROJA3MrxsdrdWhbuoOQN0NUHOJcOqdEwsVzU3KdUhJIxqEnouGWi7ciK5CW/QlR4qWq0csFzZmQH0tZU8hyISSa28y13FrtIS2nkJzZNWMbx7Cn6XQTocWVnFLhzO677R4ygtqflHBAzuSg95s2zmLclhVLROCvg9ieEMB08k4q3A5k1r2OkmADw4jINLjrKbQLqA3+IQgQNZyZF5AllT9SF/2TWZlaUvlNot5SbII0s4hMsBTyc8GegAFo7uq67eMVMMPcs4eMTh9jjcAYxll2VqDDEBRYwHWr3HtMJwqF4SSK76MykT05pUNmZfKhu20Io8DjilHDOTHTx35gA8luNr1z1PvVRIGLKKxJ/d+zz8qTwf+WoAp8dVLFQAWVUqwhBAPKkUxG6fr2eQ5/otAlmGHYAyIPMMcDsMbl/CjdQY5qk098YTCSfKwXOJPHCQlVXCnLSs+mlW0nE6A53Yha+Pd+FJQ66qysdIlmU76QzLqVxylLgt8LTC+IO5vSgeJxfHSavx+ID09/i+NmE1rgSifW37mF22cYUe2fnsYwEcRwjY5bTPZ5ORdpZSex+bTKVzjBOr44vqx7qOvdAZX3xvVC824WVfY5yctOvcbg+7nHTN8Xqyy0mw1Ujj7WLvO04y2MpUuh+bjBy3mm/UvjaBM04wbkQ82oQqncNOdjN+bptUtcnH8WtRnZPVkXNu7MWu6xrVEufcWE8PHz5sbLSknqHEELRwllIaQkRKiXq9jk6nYxRJq6urJmZgqVSCEMIQeUQ+kcLQVqSSzdHOcJskCRYXFwEoYtL3fTSbTdTrdUNEJUmCSqWCmZkZsx8t/u2YgjaZSIQCERh2BnMi0DjnhmQkQpDuneqX6puIz36/j5WVFWM9pdhwRLTce++9ePTRR02/ovFDluh2u41yuYz5+XnMzs4aArLT6aDVauHQoUN45jOfiSAIcPDgQayurhrlVqlUQrPZxGAwMNmOqW137NgBzjmazSaOHDliLNS2oooUoo1Gw1g7qb2pnKTkIiuyHZIhCAJs374dF154IVqtFiqVCjqdDhqNBvbv32/mAc/zMBgMDPlcqVSMPbZSqRjbM7U7kdV2f6fxMK5KHn9RQX/b+9uEE401ey6jfkP9pN1uo9FoYN++fTjnnHNw0kknGXKT6sL3fayuruKUU07B2toa+v0+wjBEEASmP1N/YEzFFaT6IbVes9lEo9HAXXfdNRLnlOogiiI0m01EUWT6IakzAUUqNhqNke84ih86rlwl2PuQoo6SgJA1udPpmAzHZEum8AT29wVlZq5Wq6a8lLGdSFV6iTA5OWlCE1QqFdRqNXieh4ceegi7d+9Gq9XC6uoqDh8+jK1btxrr/8rKCvr9vhnnFMvRzoJuxxm0Yxp6nmfitVIsQyLxifSjsUxzFMVRJRLVfjkXRRGiKDL2cWoDm4gdV7jaczr10Y2+Wx8PP/kEImASUPR1xtPQTU2sN66zm1KylBxKtRdqArHp9LUKzDVJByhRCmUAJqKxK0qYcnommQHZnsneGklf20aFITMpo2vAU5ORlWLfEYlHGXS7IjTbOJStruYM9T0KUwYiCO0yeMjha0JMQCXyCFmqz5np+nCRAjq5gzDkHFn9PCZ0uCgGIRnKLDeL9bJFBraseGKUsGWgjzdxyqCsxqQcTLEey5C6p50YwWNAVyqCMJIOysiM+ogslRWWqXuBipVI5KADia5OSiL0NZVlUZEELW0lJ9KFSLm+UNlvUzgmG6vaz4XAOhGbSBehJospUQpl1C3zGCk8nSzDMeRdmcfIpWfIyoDl6OXqrSll1Q01GUjkYaqJRkiYtgOAkGUmI/R63ESVETiVjsrw7KiYdrHwsJqpeBjfXdgFCeCU5hImvT5CN0XVj9FLAiS5g04c4tDDs5hMdSIAxrRVz9ckHQPPJNyhspoO5nQsxDbgJxJZhSGtSng9CSdWVueowpBXNbnWZ8hntfInUaQiJQ1wh8wQeEQ+0AIdgCba1B/OkCFcUWohlgPBQIAJCW8A+F1F3MZ1DkiJyhGtjAqYSdIhfKUGcmJFFuTBevwxpYpS2yk0ZO4A8DRxotWLPGea1NREBVOKSumocvF0PQYjz5Rt1+sLhJGOu+gy+F2lXpIdTVhyundFiAJAFjJIziFdnb3VU2VMb5nEz93/S8hmU1QnBthc76DkpuhnPh49OItN82v4+Cl/rV50sAz/0D4HD/dmwToeblvegS1BC2eX9qtYnyI0YQ4i6WBfOok9ySyW0xq2Bys4EE/hge4cotxD1YmxlqmXIvelDcw7Pcw4ArmU8JhSI17beg6+/q1nYQLAi85+AF/a9yxUDmnLcg6j7pJcqdNYDqQNRQoDqm3mt61ip79kXuT0tdqbSMNUE31c25s9Y0EWek6RWBU+JnkCDxIDTTCazPbaqpxo6zOFTOhKDzWkmkRU80Y7D9DgMZ4ZHsQ3umfgefMH8PVnTqG86MJPE0RNRSAmTQGWcpSPMLjtGHnFgxNJYK0DOVPTSjOGrOogK6v29ltSqy4VOZfVBC7f9ABmnYqZC5dFgn99dKex4XptjjziEKG6Z6fPlRW4mYL1XbjTEUTuQHZV+A3pSkBnFZauUr5Jrok8UsoJBt53VMy/vhqXTPfv9X4Ok9wmK0mtBGYIl7UtWWdLThpSKZQBeF1F6KVNRYLzrgMJhsbOFn5m1x14Xf1uTHJSjkv8TecsZCU1x0hNwssDZRUT1SYCXYlcxzLkKYO/xnU2ZPW5CmUgdYZmqPGpP/d6Eu5AwtFjkwkJHkvwVIDnEpIx5IGyJacVpmM46uQ+Howtml5+sFyVlUG/ENEveE2Mw2y93Izia2giU7rqXqBfTBR4esFW4QDrJIq97bHInHHyziadbLJtnHgjsoFUPDbhtdFClmAv4MaJOlv9RAtaO7i6fX/j6jh7MTdeH3SMTeaN15V9vJ1QwSZKxhVOVFb6f/ycBCq/re60STBbCWrX1zhZZx9nk8MEWz1pk3N2+TZSLtFvu3wbXX+8/u17tcs9ThKO19f4/RPRZbcFkS32PqQOeiwi1K4XsubVajVDIExNTSGKIrTbbdOedhzBpaUlYwvs9XqYnp42fYHiE1IyAaqbKIrgeR663a4hC8jeS5boLMtMrLNqtYosy4ztMYoiVKtVsyCnhTypgOgeaNFPtkhSHbqui7W1teOs1pRwgWyWlUplxCqYpil6vZ4ZZ9Q241mkyYJp1zP1O4p9R+RbGIbG4kntQUSbbYckGzepm6jNVldXcd11140QNZQ1l+LMlctlPO95z8P5559vyA9SdBJxedttt+HQoUMIggBzc3NI0xRLS0vYunUrzjrrLOzevRu9Xm9kjExMTKDZbAKAUcg1Gg3Tl4hUrFar2Lx5M7rdLlZWVsy4mJiYwBlnnIEkSfDggw+i2WziGc94Bm666SasrKyYRBFUX0mSGHVdt9uFlBJzc3Oo1+u47bbbjssibI9xe37b6CXCRgrg8X3o3OPzj93ujuOg1WphYmIC7XYbd911F17wghfglFNOwd69ezEYDIzVXkqJWq2G++67DwCMxZjGKvUVO5YooIiw6elpTExMQEqJRx991PSR0047zSQuYowZdR8pgbMsw/T0tBlPZJkmUpr6LMX/pPoiwrhcLhsFHtUbxVQkCy61GV2TrMMUu6/b7ZrESbRPu91GlmUjWc+3bNmCZzzjGdiyZYu5Tr/fx549e+A4Ds455xyzLwAsLS3hxhtvPC52rq22ppinRGBSf6Y5npIpERlI5+/3+yPWZCIOfd/H1NTUSFxCIprJak5kI71I2Eghv9FLJPuZgNph/AXRE8XTgkBMBUcsXJQ0sdK1iBoAxv7rMGhyT5ikA11RQk2r+UhpRtl2m04ffREAen+KcUgZeQFlj6ZszUIylHlsYh8SecSZULHvnBix8IwdNdCkZgpHW+pyEw9xIAI4TmTIKs+yLFJcPM5UXMeQpcaCS+VLhIeUOajxCAMRIGSZ2l9n5x0ID6E+HxFxnl5Y51Cxzwghy63srEIlLIBAVygSS4ChxnPzP4eEw6RRCSo7oSYCtOVW1Y+yQkdSWacdHXSqL11TJrLo+jruYCQdxFKixlP0tf3XxJeUTKn7WI6u8JHAsRKfqPMMRKDaS5OGPpTKk9SetD/FouT6fwGOrma3KMO3gDBxBwUEyjzBQPgmkY6KyZaZuINlniDSZeZsPY4ixT/0eIZBHkCAoYdQE4UckXSxnFbRyULU3QiD3Mcw9+Hy3FilV9IKHCZRcWI82ptGPw3QHoZolCLceWwrykGCqdIAh9sNxLELxgBeG8BfccBTFf9Qeg7cYa6IjQrAliiDsYo/qCyDyr7rRADFCOtvYQiXgKShFs/+UGcwzYE8HV20CE/FShOuzrA8BKJp9ZkTAcEaVMxEhyEvqYQEwlOES1JTZWFSZXIurQgMp5QKTamKNEEQKXIvD4GgreKaSabKrUgPprO8Al5Xx3h01YKfxwDPFSEhHLme7CBi64rGRJcp0CSLu25HhVDHpxW1r9une1OJQ5xEZXHliTSEJSJtjdZKRyZ09llPW18DReQ2H5bIfRfCbWC53EBWVsqokAMLS3P4L/j/YLbcxXypi329STzwb9sAR+LI7jn8tXgWLj39PqPMpbAAe5JZ/M3ic7A4qGFrtYU9/Wl00hArwzJavTLKboId5VX80bEX4+TKIl5TvwuRVJFDQzAkkuFo3EDzQWDlHImTy4u49fYzUU8lshBwtfrT6wtETRdOohSpZGsXnkTWyPHSzffrqvDQ5EMIHQaBxgi9mMjBVFZmnYBJ6PiNDpOosdRYnDmkyrIuBQbSQwIHnhTwIExSFUCpFz2dTIbrmKmcCQyki1mnp17E+Cnqp66if/8keCqM6lX4OXjEUTkmwISKeRisRMDMBLKSA57rfuxz6CgWKmZoVfWXpMmAWoqfqj6o5igp4DCObw12gi/6ECWJPNCqzRzgXa4UfBFDPB8DOQdLGbKlkiK1XKmIQg6Yzgqthks5+IASmajxq2JRWgSYDgcgSpQIRV3biRj8jiINMx3rMA9U2UQoAE/CKWdgh0I4MZDUVfKQ6nQfrz/v+3hZ7R48N2DIkOP7iY9/7p2E77ZOxL2L8+gfqcFNtIKZE9GsfxyyFQMsZXB0nESplcCQ62NQZX1X9ep3FJHvDSWcRIBlirjjiQTPJCDUGEyrLgZ1jrjBkZd0RmauErIIX/3YSmYqm2JXNXkomFYZQtvHYYjG9X3Z+m8AyNioYrHA0wqkHLGVf6Qusi1q4w/u9NsmhOwF3vgiYJxItAmm8cUuAEMCEmxlzGMp2myCySb0xkkr+/e4yoEWKqSmsheQ9kJrI2WiXcbHUhnaSh6beNuoXsZVjfa92IQn3ed4spTxerOJD7qmTTjYBIPdtnTe8XsdJwjtmJh2/dK57DqxiVq7POPH2QvMjYjW8XPZ9lLqQ6Rks1VSdE07UQZZD4nM63Q6GAwGqFarhuQiVRBZFfv9vlHlURyv6elpdLtd+L5vCD8ARvlEmc+llCOEkJQSS0tLANYTPtiJSSj+oeu6xiJLmWXpc7IXk+2ViAT6XS6XTfKWo0ePGvKS+qRtAU6SxGREpWzAnHPMz88bEpMUVXR+UhFHUWTUVzY5QfXQ6XRM25Gdk9qKc25IjsnJSaNYo+vRfdF1qZ9T0gZSjBJZ6TgOdu/ejYceegiTk5Oo1+vGet3pdLC0tIQzzjgDr3vd64w9utPp4P7778fRo0chhMAFF1yAUqmEw4cPG5KWLOzNZhN5nuPgwYPodrtYXV0182Wj0YCU0thSqVxEYpbLZezatQsPPvggnv3sZ+PgwYOIoggPPPDAyIsWIppWV1cNuZxlGYIgwLZt29Dr9XD06FEzLuw5yp5j7TE+PkfYY2tc2TVOPFKbUvva4QHo2kSSPfzww3jWs56FE088EWtrazh8+LCxL8/Pz0MIgcXFxZH5luL+UT+I4xiccwyHQ5RKJbTbbWzbtg0TExNYXFxEq9UyY2t+fh533303er0ems2miadJ9nkiz+I4BmMq9IBNilK/sucOO9kHlbPT6RgCjuqOCH9Ct9sFAKMcpnlmcnLSkP9RFMFxHExNTeHIkSMmPMELXvACTExMYGlpCcvLy1hdXYXnedi2bRsuuugiDIdD7NmzB7t37zb1RfMK3Sd9PxDJPx5fkIhOuj87YRCRr6SKpDqh+qA+SccQyUjXsH/b9TtOGo7P+/bLuvHvbgDm5cFGYSt+EH7yCUSmiCNAZ10WLjKhSKWqjoUoJIOvA5yFPDWEn89igGXaNueo7KlgEJIbJQxZlQW4zo7LAB3vMBlLwkHEESnhiNhLpaMzm6ZwuBxRnZElFZKs0r5W0WkrrolzxpBLZUGs82gkc7SJhagTu6j4g9wkYACAllCBiiOo+GIq87KjkgkASMDBtU1QSIa2HM18CqhFOQAT3zCkpB9y3ZKcgkNIhgDqMyIYScXYt5I/eEwgEsxkjC4zpTpsauIs10rIVMeKDJEptaPwwKU0hCMAdGSgk5soS7KnrYvK+hsYu7KdfAI6GQXZOtcVmzlC3eYcwiThofiCZBsPmVISKkLaQy8LDSls5UIBoFSnVSeCENwk+Umlg1hnSSbSmwhRoWNXDoQPkQdYiGsQkiMTqvyxcFFyUgxz9UYyEQ56aYDD7QZ8N0e7FyKLXXhOjiR1kaQu2v0SksRFpRyj1wux2ikjbGvSLReQDgdPcng9RUy5A6EyKpc5ysd0bEFP2Yj9vkTX4YpoKEl4fdUUaU1nWBXrSRWEB3h9lWBFOoq0Y5la7DupBM80Uegq4o1nSgWksiWvEx1kAVT2SkUG5D7XikNlY84D9T9PAbevVEdZoDJKK+uxyibtRgJpWSWK8boM3kAaC6ZkgJxmEL4iityeUssJXxEaDlMEqbFFO4BgWnnkAlLbFAUHhKPICydSpI101mPEkdrL0WSiZIoocrTF0klVLEYnkXAHAuCq7JLr+S5Qyqm0xCCOAcm9c9hTm8fDJWU3DaW2V2YMK3fN4q3xz2O+pr58GZNoxyE6gxCDlTL8RRfDczxMlgZgTGJTpYN2v4T97UnsbU1iGPvAZuDz2UWY9brYESzj+aX9OJhN4I5jW1HpS5x37kP4zsqJqBxWMQ2pnbKSim/ZPidB9T4fXk9l5pWOUq7N71jBs8r7jJoYgDWHqnAFJvuypJinwsRvTCQ3LzaMYhHKvkzfDXUWQ0gGwZTKucIytISPEDm6en6mlyqUnTmSDs4oHcLd/R04e/YIbj5hEu7QA8uBpKn6oztgKC/GgFQJNpyjq8i2qhhMLJOQgaPax4HJED6Y5/C6Kg7i7HQHFwRD5NKFwzhyKXA0nYAIVVw9uWOItOPDbTmQvmpL4UrwRSWjFSVFGkpPKlusTozCcgbkDM6Awx2q7MfGjqwJSVIApxX1W/hSE+AMLAW8rur/eVki07xDVpKQpRxePYYY+Cr2oi8gJcCFSkaSVgC4Ep85+y9wqjfETcNN+N+LZ+HOha1ot8tAx4Pb5YpY1u+p8lBqq7G6P3eoys6MSlma8cYEU+Rmru7L6yibPE+lGZdMAkErA0+EthlzZCWOtMKRhRxJgyGpqbkr9xXpKrkEhSIw5L7UzxiutiFjVHUoPJ00Rr8wka5SfVKCGtVF18lDnqq5T90HCjzNMP72nh7KaXEMYCTzJy0ibWLAPnacCKPFwHhcv40UdfT/ODlkE3D2onecmNsoJtf4eQCMxBMD1tV3NiFoE5vjSkhbQbkRAWkvJjciBG21I5V7nJi1yz1OwNH/dlKYcZWGfQ+2mtD+nOphXI003hc2qsfxeh9XN9I5ab/xdt0oFqF9vF0G+j3erx5r4WmTYURMEYlE9lSqAzspCqAUZIDK8En7cs4NOUELcrIvhmGITqcDxpixCPZ6PWzfvh1ra2smQQKNmWazOZKchMi+TqdjVFZkhwRg9qXkBXRPUkpDXNrnIpKKlHWkhCL7IZElq6urmJmZMfVPZbfJY7IhE4FFsfrsbM6kOvQ8D1NTU4bgyvMcU1NTI3bdOI4N2WJnkibSgwgdKgcptsgaHQSBIVupTaVcz85rjw/qE1Re+x4OHjxo2rjRaJhkDvv27UO32zWkLWXzrdVqaLfbaLfbuPzyy436lOIZdrtdLCwsoNPpYOvWrThy5IhRe/Z6PXMt13UxMTFh6lsIgeXlZUxPT2N6ehp79uwxlvf77rsPrVbLEI2cczQaDQyHQ9TrdSwtLZmxS3Ew77nnHlN/NkFujz/75QORzLbt9we9YNloXNrXsMlEml/b7Tamp6cxHA7x4IMP4uyzzzbEZ6lUwsLCAk4//XR0Oh3keY5arWbUopSxOQxDMKYsw9SH0zQ11uVms4n7778fSZJgcnISJ510EqIowtLSkrmPiYkJQ/DT3EkkLwCj5BRCGMWjTajRNaMoMmEHiEgjmy6R5vSSgL43KTs5kZH0koHGWb1ex7Zt20zdLSwswPM8bN68GUEQoN/vm4RB27dvx8LCAh555BEcPnwYi4uLI+OKc24IPZoLiTSktrRVlOVy2ZSf+nOv10OapsaGTwrNarVqkrhQf6bxRYlXKMQBveiwSWx7vqbv0HGVu03g0n72Swb7+YT60fh35g/CTz6BKNVikzOJbhqi4sbgTCIWLnp5iKoTIeQpOqIEIRmaGBhSjVRpgCKUKGZgrhehHsvRdPomeUYCBw4T6IhQKf+QIhKesTjPe2108hCpCKz4eK5RpnV1bD2Kh5dKB4NMxbUT4BgIRYJWeKySe4gADpRVmQgxIrA4hEnyQcpDUlLmOlELxfvLNQFY18kTOjJAiFRbGF1NaOXGJghAKyLXyUNPl7krPATGZq0agOIQGgKAKWVQaMrsoMaVOoiuAwCreYgyTw2JObAUhXS9SMd7U+2jiDsHEseyGuo8AmcCfelDSGXbriBFAm7sv2RT5lhPqEDb7Wy3RA5yCG0PFoaApTI5msDwtB1ZsPWHRAfSWIvpGFKg2ipSABjkARxHGoI1ki5i6WKQ+SomHVdx5Lp5iIW4DiEZDvYm4HKBwMkwGfQxyDwsDatgTGKpX0HoZQjdDLO1HvYcnYboeYAn0O6U0agPkOnkMlsm2jjSqivSd+ih3lWkGo8ySM8BhIDfAnrbJdKKItjSiiL7SitCtbfHkFQYgjW1uo6nJLo7Gdy+Wni7fUAlUFHxEU0cslQirSuyUJYV2RDNMLP49rtKqZeVGFyhlX76xVMeqiyrYIqMkw5DUuGoHsmRVDnyUC3OAYnMVdfIA1WmPAQirkg/ty+VApFzZWuMAO4oki5oS8QNjqzMUDki4Q0FkoqybYuOJjQdBnegYi/moSIqhIsR0kG4MHZFqT37aXVdWcUztQ+pDvNUqzt1jEg3grKYZqo+lPJrXQEJqLp0hxJeL0e4IjQZxBG0ubFSSkfXg4616H2/gdVK02SYzgOAc6Cqn2OG35nGvopEVpMQjRSs42Hoaatw28F3V07G5OY2Tmiu4k5sw0P1eUy4fYhbJ7B6hsQVEw/iv/3TqzCZqXvRAnDknrqPyoM+HG0vBbR605d4wfyjqPDYqKX70ocv118C0IuLCsvMscfyMvoiwBa3o+ZZqWKukn2ZEhxR0iS1jwuu40Qmkus5hWLCCpRZiibPsJorNeJABJh32wCATUEblbNX0RlOobQoEU+oMpUWGby1CKLswxnmkFEE4XI4qc7q7eh2dYFgTScBcgHpMmRliVdv/TfEMsM9iYNbhzvxT8eeiUeOzECGOZBwZJHqKJrXgggk8ppO2mEThhkDBIMzVBZiJ2bGsk/JhKiP5iGQaIUhOMBSwEkYHJ2YJPclRBnapqzJbS4hSwJOOUMeO+BcYn7TGn5qbg/+v9Pfxn3JFH756NuRVtT4hCvwzjuuQNQJ4Ky5cGKlznUCTbRzGtcSTDCwjMHJFIFImaBVSIN1FSLdp9dXc4vXE3BSRagr1Z8i3Xks4Q7VAUnDxXDSQVpl+iWAiuEoXGHGpiHzARipPfVRTcATecgyBnCpx7YqO2WvNkJ7xtbPl1kJXXT9S09/SD7oAk87kFLHVuDZ6i16YN+INLRVb+P2XzpmPEMp/U3npjLQOWjhQMkebMJnnPwh5RYRR0SqkHVwnOCzF7rjVl5Sltgqw3FbNV17PAsxnWsjxRwdT79tctImK6ke6fe4Em+cPB0nAO2y2fZnOo5IA1u1N07i2fVsE5R2eW014zgBaStL6FibON3o/u1j6X/bHmkvMDciimgb2RSpHxDZRu1N/YkyKNM5SqWSsbJSTLJer4csy0ymWPs+wzA08eiIXCR7Ii2+W63WSPITOicAVCoVcK6SrbiuiziOTUICQCn07M/r9ToYY0YFZKsobZKDFvJEWtKin6yaZNV1XdeoKIfDIZrNplEQUd+neH009kmJSLZMIhDoZzgcGrUSKapIOdZut+H7Pqanpw05SNmj7bolVRS1K1ksaVzSnDI+vogsIcKYxrDv+4a8IcKR+iLVO9ky7fmP7OY0Xnzfx+7du3Ho0CFUKhUzVogwovs+6aSTcMYZZ5h7O3z4sCFWyFYexzFarRaazSa2b9+OE044AYcOHcJ5552H3bt3Y/PmzbjzzjvNGKJ2X1lZwdzcHBYWFgw5xRjD5s2bEUURjhw5YspFijN7frHnYLovAtnX7Tlq/OXJuIKczklkuv0ZXZus9dVqFffeey82b96Mbdu2odVqmSRB9Xodd911FxqNhrkukV5EHEspTTIiypY8NTVlsjFXq1W8+c1vxuzsLAaDAXbv3o2lpSV4nmeIbvv7heYiGk9EsBOhP65mpf0omQsRmaRCJOKRCG+y/1JfJYKy0WigUqmYRCfz8/MjGaIXFhZM/19aWkKtVsPc3BwWFxdx99134/Dhw1hbWzP93ibo6OUG3RdjzIQxiKIIQqi4mXYyE9t2bfcRsi0HQYDNmzebeYzqYXl52SiVqZ7Gv+vGSWj7e4Ngux6I9LRfMNjz3Lh6ffzl2RPBTz6BCPUMHvAMQ55jmPsIeIaym6wTOFoVV9bE3HpmTq6C5/MMkECirW52RlNlowOaTt/Yg2nxQ8RhhcdIpGOSaxDZlOjFq9CLVduyCgB7htO4f20eZS/BbKmLQeaj5sXweYamN8Qp4TFTzkS68FmGMo/R0Wo1n+UQOtZhrhO3lFkMAY6+dHTMPtcQZH3pI2RKIdeVoVYBZqixzNiBFcEptSLHRcByo2Ys64W40MRrV9u2af9Ix0Fctyhzs/gHYBIdEHFb5qk5nqyVfZ1Bluoqkp6xXR9Op5UVnMcmBqQj5ci+A4ojqOvNBsU5NCpEJuCxDJHwINj69lxyJHBNQhulBAwQ8NTERaSMyWRPjzSBSv8b+7q+f+p3uSY6qa8IMKVihEQnC7ESV1DzYlScBBU3xkqsU9XnDtrDEBPlIVqsjMV+FXHqYrbaQ7cfwqsPsNSrIM85Jpt9LPcbahHed9HhJWydbmGQehimHjiXKFUS9BcrgATcoYBkDPFsGf7SEH5XQgQ6hp22OAoP6G5Vyso8gEmWwHLAGTBkNYnKIUWUCE9la9ZiSTgpAAnEU4pE8HqAvyQViSYUycVoca0TpuShItqceF3F5/YVeQAociMrA0EXCNsCos+UNTRTceZI3cNzQEQw9kzJdebYQKklnUgTmdNaERkpYi4rA3nATaw3yVWSF54KJDVFCgQtpdjKA6aJDCiCiEhPiqsoAOlZZdDlYBmMLZQJrbRyJJxEEzkOIHz9wKGJIp6tE5FOrO+TqeQ3lESCZ1q1OBTw+kqZRTEXnYSv2ywlDNmYhevKyDxgEJ6vk0dwncEWcPseWv1J3FGvA47EfbV5ZKmD5lGJ8s8exY2rp6G2l0M4yoLtJIpshiZFwz05spAjmlLEmnQBOMBWf80ohSnRUYVlOlyBo8m99ZAKRPqFPDUK51QnTHEY0NfpbytQSsIEXKsKc6MyDLkwmdrLPEOu47qm+oUUAP0SJsOJ4SIeGG7C+Zv244b6JIJVQIQCLGWoHFXZlwebS8paG22G9PTi02FIqo5JFuMOoBSvPa3mLQtcd/QZ+OqhM7FwtKmSoOgYhWzIwRMG1nMgfImsLAyxJV0BCAYWcWVJHjK4A0XE8wwmBh/ZaEWwnvyDMqWzjKl+JhWJm9akJr319XNAMkVKilIOcKA+08PF2x7GmeVDeH75UZziVfBo2sOt0RZcu/RsZf0v6/4rGbK9VQSRHuOBhPTUtbhkOjYmA7SaVjg6NmQgjeKPp6pPun1VZ26k+rQ7lEbdqu5ZxURlmcqYnpU4hjMukipTKuiSIl7p/mj8GFcxl5BaaQhHjzUdT5FlzIwXkEIRapzSmDRfcUIRoCzWHKgDdZ6MGSUjjwEmuMlcXeDpCdtCSguJcQUeLQTsBf5GZJJNAgHr9tfHUiwSbHJsnHC0yTzXddFsNlGtVjE9reKJkOqKFsC0OG632yYIPSkWyHJJ56dFl70osbdT/VAZx4mwjWzS9uLHPt6+T1vRadcPkXUbYZyctH/GyVebIKTFJC0EbfWGTQBTOWzC0G4ru51tFYmd2GIjxdI4qWj3D7vc43Ztuw/Y+4yThlRvdpB/qk8i0Og4O3Ye3SvZbYkESJLEkGt0XlqU02I5DEM0Gg0cO3YMnudhenoa27Ztw8rKiumnBLo+JV0hYgXASJZdik9ISh5qMzuJC90T3TsAQ1LZ90VqN1I7kWqQ7M5EMBK5SP2C+jQRElQnpBAkWyIpsOr1ulE4UcxDWxlJx/b7ffM5qQsrlQqq1aohWRhjhsghEpjGLwBDtlASF3scUL+kvk0xJimrrJ1QhNqHFGS2MpQSXxC5aNs/Dx8+PDJ+iYijfR544AFDYpNllfodJQjZsWMHdu3aZfrY5OQkut2uIXgfeughUxd2LNeFhQWjSqT+whgz90ZqTjvsgz2GxhWENFbtlzrjv+lc4/FkibCl628051EdxXGMqakpLC0t4fDhwzj55JOxd+9erK2tmTiEURRh586dqNfrGA6HuPPOO4360Pd9o/hjTGU0bjQaqNfrOHr0KKrVKmZnZ7GwsIC77roLR48eNUS4lBLtdtvUI6njaAyROtBW23e7XcRxbPoIqd9sBRypU2m7TVzTiyv6u1wuY9OmTTj55JMxPz9vVMuU1ZteNDiOY+zylLV5eXnZnM9WGdJ8bFt9qX9T/6AkKdVqdYSAp7FmZz2ml3Fkz65Wqyb2aRRFJgkL9TPqawBGvk9tctZ+4WJ/Z42/eKPyjr+ssvuq3V/r9fpxL9CeKJ4WBKLL1yuHM4FYuMjBUWWxTk7hGEtyV4SoaSWeo2MbEtHkM5WIxFaqqSQgyioMKOUaoMjDvlj35QtwRFLZnAciwEAEqDlDOFwYC20qfOyPJnFkWMfRfh0r7YoKfi8AvlVioVdFyVOL2SRzcGd5G2bCHlyeY9If4LTSUXTzEjZ7a+gLX8d25EjgIJIqtmLZiU1ZFWmpFuQATCIVIvBICRlpgoz2E5JB6BhplFClpi3Pno7dlwrXxFELoWIO0jXBMEKYOjrxAf2fSg4BiYDl6MKDEOsPg6QEFMJDWcd3TKWDlijjSDph9js5OGaISkWg8nWLslQksGoTDzO8g64o6XNzQ/zSb1JdDrRy1NiIJUNfW6NV3EnfZFRWZKCLBh+ahCohT5BLV5GtjupvpIKt8UjFyWTcKBI7WYheHiARLmaDLoTkONCZwM7GqkkWsTiooRsFCLwMw9iH6wgMUg9pznHi5DIWBzXUKhEcLiAlQ55z5IIhmIjgugJZxpGlLo6sNJClDsJyAs/N0R8ECI+5EK6EM8whqj72vpFh6ra6UhoylUG4vJQjTjiEp4g+JwGG0yqWoHSUStAfqqQYg00MfgdI6orIcCPA62pircTAYwBMEX8sZ0gmgEyroLyuynyc1pgim1akUU6ltfUsqLGnypBWpLYuO/A7iowUpO5y1HWFr1R2TAB+W8KL1LW9vkTQWicRVNIFdY2syRCuqDh9SUNdyx0osm4wy8AzlfDEHUrwDPAGAsJliOtKBRWuCvg9gaSmkqCohBlKrRguS2RlhripyDMuFSmZNBRJKXSG3NzXRKG2NdtqPgBwh1r1yNeVWvGEIghJ7cQENKGh7dBEojr6njNNwAh1D35PxbsTPjNxGgGo+H2uugdIoL4XyEqeylBbDVDuA4N54A2b7sVnbn4RZjsq9iGP14lOakfprGefVmSqhPQEHo1mcGH5YaRw1hMc6bihjmbDaIzmZj7SsVyli5DlKHOVQTmS66pyNTdkcCBR4zlyoVTRFNM1ZDkaPEesyZ2+dDEQHiadCLl1jdOCI3g0msWk10dpgSGagrYvW9mXXYYjl+aoPlzD9D0JnFhA+FwpVUuqjzk68ZDflhhsVuU79NAs3OlIk1MSLObgMdOxCCWkIxSpKGE+Y5kLd7BuS1aJe1SZiCCXriLO8kApjFmukqWQIjIP5TqZ5mtFoyajIYFw0UVaFXBihq1nL+Lqk/8a806MSe5iKc/wld4z8Z6jZ+HA4iSyjraHMUAGiqxzlzyVfbopAAfgQz12upq4hO7DWCf1SCXpDBlKHQa/o+KXgq2/EHCHAu4gh3SYimnqM6ShGmtpmSErM6Q1dd/CteKMcq2kdNV9SlcqTk8yldBkPUyvVjtq5bQrgXydxKfjaT8mASfipuzgMNncVRxYBp6sP084sWoXSiIl1z8q8DTERmQULaTshTfZiIDRRcNGygP7vOOLBpuMpHPQgoEWp47jGHJwcnLSqJ/K5TIGgwGOHTtm7Ga0CKdkCpOTk9i8ebNZgJEShGxXNrFll2+cOLPJOLtebBJtXD1nk4523WxEABJsW/S42se2JVPZ7AWkXfbx89JC0s5eTeTO+AJsPNmC3Rfo3sYt1+OEoL0vLf7Gy2Sf295G9zSuqhxXbFIiDdviSGQbET6kzrPby1bo0MKVVDR0z5QhtlqtAoA5J12P1DxENDHGsHPnTtRqNdO/pqamIIQw2ZVpEU8Ehp25lQg2yuJK+xNhQuRMlmWYmJgwCkJSZkVRZKy8mzZtAucqc3Sv1zNKPDq3Tep1u10zjqi+Scln/09KMPqbyAwac1RGildHbbK2tmaIIFKUkW2W7JVUJ7YKyp5rNm3aZOqI7Mj1et2MB9taTaScTXBRUgu6B8dRWXLJrk4EEZE49ksSSsZC+1B27HHVF5E8SZIYconKTyrpcrmM4XCIvXv3GnVjq9XCsWPHcN555+Hb3/42LrjgAnzpS18yhBm1OY2pfr9v4i0yxjA3N2fKS+o5W208bk8eTyI0HkuW5il7+/iLHprbaIwSqU1WXAAm23IQBCiXy6hUKmi326hWq8jzHI1GA61WCzMzMyZJzszMDNI0RbVaRa1WM+NBSoler2diSdrE8IEDB3Do0CHkeY7hcGi+G2wSir6rfN83iU9IYUdjgQg1+t4g1TG9XCBCu9frmVAB9guxcYLPHlNTU1O47LLLIKXE3XffjenpaczNzWFqasooZBcWFnDvvfdiaWkJQggTFiHLshESk/o8xYO0VbwUp9VWCpNdmghNGi9UNorjSklT6HueSEP6nqQXIHaICHsOt/uY/V1B3wdU1/Z8bhOyRIDa+9L3G81f1B79fn9k3v4PRyAyHS8r4ErtV3IjOFaMrJClSrUmPETCQ41HOnOyb+y+Kg7eukoMTAXcJ0JRxTtUluGQp1jNq+b6IU/BpbK89kWg4vCxDIeSSTzQm8fO8gpqToS7OtuwMKih4UfoRQHSng+vkoK5Evfv2wTuCsSVBFICp84sopuGuG3/DtSqQ0jJsDhdUwlL3BgXNR5GnQ/QyUP4TMVUDHmKgQxQZmpRm0hFLEa5Z2IkpnB0cg+lRIw0yUbZiTsygK/rIZUuIpahwlIThwza1lxmGVIVOErHJFML9wQcFOdvIAI0+RAeJV5hSgFU1mRkX9u/6dqpjsnYEqq8qY4xuZTVcSSdwN3dbWinIZ43sVe3BzPEHKkHBbguu7rPJh8oohUMHREi0nEkE+nAgSIRlVU5M3HXKjxGLjkGMoSQHFUnQiQoNqHUfUYlpVnLKuDaYp5KB6lwdQxFlTmZQ+qkLqH6jGfGGt1Ky1hLSliLy+hnPuZDZcdciSoQAUPNi5EJjmHkYRgp5eAw8VDyU8Sph/3tSQxiD4GXoRcFiIY+8sTRYwIQgmG63sexlQbAJPwwxXCxjKF+viz1oG29GSRjmL7FRdAV8Ho5WO4irTCEbSBs5ch9rqzCgY4hOFQ2WieRSMsc0TSQVSVq+5VyKa0pki8L1UI8D9UC3mszlBbV3+Ey02ofHQeQA4M5BlGX4A0VM5HIEJYr26ITqaypLFcqJieWCDrKwpvUuEm0Ilxm1HW5r87LU0WeMKHiDLqRVPHpmoqUKy/ohwBPlae0CEjOwKTKMs1LlJGWobQkkVaAwZxSzLoRELQE8oBhUKJtimh0YkV2JhUOngGl5XW1ImVpTeoA44DfYvBbKh5jVmGGIEoaFAtOKR/doVJ6AlAka1WrrHSSDa+rlGdZBYBUyWoARdrkoSJGAUVU8Yyr62TqXMoyqhRSKpOthN8V4JlSMwpf1X1vkwOeAckLu1hOq2js9rStU8W2pDZwEsCNJXIPyMrMqOH41gF+5rS7cG5lr4oRqwm7VHIs5lXMu11EUidmkgwD6SKSriH4UjPW119OUNxEeqER6nAILc1WqfEI9AXHkihjNa/i4XgOD/Q24V+PboeUDFecciuOxQ2sphW0kxBH+3VMlQZYi0oIViW6O7V9+ZjKvixDF04sUDrgKRLPUX2Gkg5JDgRdbV92VLtJRyp7qwTE4ZISvvmaQK4I064sY0qNGKtYnU6sE4hAE1hCqUuVxV8pAKndeKISpqi+AkRTEnlFqDbKGKQvAVeA+QKuq15A1GsDtDtliBUHPFXlPbG+jKW8hr9YuRDfOnQSep0S2IoPJ2aQnoSX6DEX6MRIfa2gDSR4yjXJpvoamIo5KInIThSp6PWg1MkDCW8gwFOp6lD3RycWYLk02ZITnS05ra33J6Xg1QtxS3wuPXUesg4zxoyLQAoJJjkgpFIfasUhSI0poAlMpZ5mmVIlkjVcOpo71PfGM60EJZLUVwldKPs7kaZc28sLPP1gEwUbqdhsFYGtuqNtNmw1m70AtfejBah9HXuhSp+TqisMQwyHQ0MwLC8vGxKn0+nA8zwkSWKCulMG2zAM0ev1cOTIEXDOUa/X0Ww2zQIkDEN0u12zOBpP7jGuqrAX2rbazlby0L3bZB9Zijci9uw6HLeCb0Qg2uTK+LlsUmNcRVgul02dep6HUqmEVqtllHWk2LPPTe1ObT1uJ7fb+QdZmqkOx8nZcbKZyItxwoKOpcUtkVi0gCY7LJETRDYR+UCEHhFWRAqSaojakhRjFHOObMf0f6VSMf+naWpIEUAp4g4ePIhTTz3VnIMUjBRTjGItuq6L4XCISqViyHEir/r9Pqanp43t11ZN2slUVldXTZ+iWH4ARggDItVtu2K/3zeJN+yxPzExYQhUIhfpf7LfUv8jdRYpKYkUJGKC6pzGJJG7VD6yRlPbEzlCBBupp4gApSQxnKuYep7nYWZmxozffr9vMiHTOE7T1IwDWx1H8w4ReLaqmdSOREwTKUnlI/WVTZDaZAapuOi8VCdki6e5hsYaZSHetWsXPM/DiSeeiAMHDuDw4cMjVmIikajv0t+NRgNnnXUWoijCvn37TFxFmkPpOvZLHxqfdnvapC0wSt4AMGQo9UGywTYaDUxPT2NmZgbVatVYswFFYttZgPv9Pnzfx/z8PO666y6jhm00Gvi3f/s3DIdDLC8vY+vWrVheXjZjk9qH+n4URYbEXV1dNQphejlCiVJIZUzjlohtUs9RW9jzKinbSKFH1mY7o/vExIQh0YjIDMMQU1NTmJmZwZYtWxAEAY4dO4bbbrvNZH+m+ealL30pVldXcfDgQTz44IM4evQo2u22USPT2KS+bBPApFoeVwdTpnW6DoUIWF5eHlFWU31WKhXMzc2Zfkt13Ov10O/3zQsJUhWPf0eNJ+ex52z7Z/ylE409+7uQxoP9/UHjh8hzO+wEjQs6hlSVTxQ/+QQiAyTZYHUwNVKRUbISAGjlFTSdvlGlkbJt0ulpu/G6ddmj7MjShaPVbRT/DoC288aAo+yulL3XYRICAmtpBQejCXTTEA+szGLQ9OBygdmgh4Y3RD8LMF3tI00dSMkw3ejhSGsKaHkQpRRzjS5Ori1hb38KrXoJa50ypGC4O92CZ28+hJKTYDWrYiFtIJUOtvvLaFrZnz1nvRMIWikhhwBHhSVGoQjA1A8A9KVvshADKoFLDq5IRbkeNxKATmyyvnAfPxchwbqNWdmVhUlsoNSKmbm2kBx96eNY1oAAx05v2ViCF9I6YuHicK+BtWoFS04dqXSMErErSnDYehIZin1I7drNS+BMmIQpAIwV3NFkZ8hVW/dFoBSFLEWEdVt6qmNOpiKAAOBBZfpeTOqY9TuIhYfFtIZpr4flrIqAZQh5ilgT19S/HCawP5pExY2xENXAmcTe1hSimofZSg+PLk1DNBky4SDNOVxXIElcOE6ONHUgBEOWOSpBSuxhojJUMUBbqp8kA+0JBpCVI+QJh1fNMFnr48hyCQhyuIsqmYUTAzzNIQIXU/d0wDKBrBaAx56y2jKGpMqRlZTCJw/X1U1ki6XsxcKT6G1VNmMnAfwOU4SfozIfkxVYeEyTHWqR7gwYmCQyS1kbnaHOSFxiJpmKcAHmKbJNJTRRGYql46C8KBB0BLwBQ+5pck3boslyzTNlIc1DTawxptV1imBJasoyKTwYUtAZSghPqQadWMJvq6oVDpGaKtMuzxVhltSVhZllAGspsjH3NUkaK0Ix1yo/sjQHaxKVo1rJ2ASShiIoiGhNakrZqUgVmKyvzlCRJlxoAmZASimlssxTldBGuIDX09fTlmKeqG25TvAhAigldKbbxWFwB4BkEkldKawoJiNMchgVV+61J96DL977HMysCmOFhq4jyowtubJGKyWpSp7ynK2H8VPVh1TWeKhr5GDwmMCUo7zqAxEgYgIzfADOJMr6ZQaHHEmSQipprngelJ0hHAZ0hYOWUHPKnmQWjwzmsK8/iX2rk+ivlcD6DnjCkJcFeD0FY8Cf3H8hOBdI4vV5sh/76O1voFFhkEEOljNUD+dgeY60WUb58AA79mYYbq3B66SAwyA8hqCj7t3vSAynOLKqRFqHziTMIDwdLoDIrZyBxRxuXyn1WKpCAPBEK2VpNtdZg1Win/UEJH5bta/wSWWoM6rnynYryzmYtuzWJwY4YWIVF0zuwanhUZzsLWHOEXjlv12B1p5QxVwMJb55+xm4KXomnCGD9ACUhb6mIvKVNVqRkm5P9VXhKrItD+T6CwSmYy3GStXrRhIsl3AjRbLRiwS3n6/3c0clCkpqLpIaM5nHha/Uy8IXigiVqu5GFH2uXFcGMmWlllwlnaHtTDLAkevZq5kENOlJBC7XMUrpPJRxmUI88ByQUp3fGag6EmRFF9DqaP3gGCiFcNKU8FtME/kFnm4YV4bZygJ6ULeVZQSbfAQwQvzYf9N56Ry2Ks8mpih+1NzcnMlUS2oMIhgqlYpRLgVBYBYWRDSSCoNibOW5yprbbrcN0dLtds2ikZQdpEIBRhfRNsk5vqAaJwPtRdM4cWEv4G3V4kbnHVd30t+2ssom8qi+aeFGBBuRgxTfjc5PWUztbKHjRK9NKFBZ7aQq47bucXWkfbx9TiJkaJFIdUFt7Pu+UaRRjC0Apg+SMpXOR21kL66J4KJkIkSgOI5j4vPleY5er2dUZmQLpTqr1WqGKCIyjFQyFBOw1WqhXC6j0+ng6NGjWFxcRJZlOOuss0y/IgUNJRShxS9ZIMvlsiGYqNytVgtxHJuEHmQFtW2IpKQiIssmtKvVqrlHUmdRXRHpSPVE6kWyJtsxFW1FFZGC1LdslSqRTLayi46lOiNil9SI1M9pLNokI8WKpDITEUd2VooNSUTH6uqqGcvdbteQvuPkA2XRpv+pH42PK+qX4yo2m3wDMGLlpDFOJCORrVEUGes6EY30d5qmOPPMM3H06FHs3LkTX/7ylw3pZKu/qTx2+WdmZhAEAW699VasrKyMkCn0UsQmbOx2secTOj/1KRoH1WrVxOybnJzE1NQUarUayuWyiW3ZarWwuLiIPXv2GDU4ABMqgeoujmOTdRkAOp2OUeiSKvzo0aOG5F5ZWTH9nqzvdI7V1VVEUYTV1VVDArbbbfMSgYhcW7VGyW5IWUz7ELkdx7GZb+gFCwAT59N+uUaZtzdt2oRms2nUiFTuMAxx0kkn4Y477kCe52i1WnjggQdQq9Xwr//6rzh27Bg6nc5IvE968UHltuPwUvIXsruPz1c0z1O2ZzqGFLdEMJbLZUMykuKa4h/SNex4hjZJOP7CiPoP9Xn63x4LNgluf8eQtZvmEPslgx37kFTi9ks4sqbTS4AgCEzIiCeCn3wCEWrxmAmOofBRcWKUeWISjwzyAA4kAh13jkOoxB7wRjItKwWd0P+rjMx96aMvfR3Mn68r15gwSTg8BrTSMjyWIZUuDiRT+N7KThxYnYDnKjJzNaqgPQzBpyVOrS7g6yunoe7H8LwcOybWsG91ErycQTgS9XKEXbUV/OvyDhxcnITIGeTAhVNPICVDxVVxHNt5Ce2shIc6s/iOPBHPm9qHWa+DpjPQSV88tPIKHJ0UJOQpmo6WmGtLrkqw4oxkFw5Zilx/RpbmOotV4hWtWqRM1QkcU2cOJITkimjTqyw6fqDj/T2UTWPebSMCUGEJUrARYq4lyhhIZf9ezmrwWI4troqPtpRU8Wh7ClHiYSB87I1nMOd1TBbqkCeIhK+UL2DwWAYB1WYdnVm5L3xE8BDyVJPD0mRqTpg7krU60PEZheTo5aHJ4hzBw0JaRystoekN0XCGODCcRCsrYzGq4tHVaYR+ii3VNs6sHzF177EcK2kFnp+bayzGNfSSAJngyHKOg60mTpxcRpY6WGjXMCx7KHkZhOCI+j7g5WAMiGMP080eelEAx83hcoHlXgW15gBp5mDYCeGseRChRLeqSOUkctFxQyAUijzsKPLPG+RgmYAsMbBcAozBiTI4kYpj6PVzMMn14lslGAEAin+YVjUJOCQLskTlKBBzRRjm2kYMrm24Ekhr6u9wRf1muUqckpcU6ZTUJVyHIXEY8pJSAbkDlQRFeEot6KwynYlZEVOSA7m3rjpMq+r6TiThDtQiPisxeF2JcBUmBqDQls6swowCKppUZU4CwHWZIfqysiL23AGMNdIdSK22A+KGIs+ClmWhcgF3qP+XKi5eWmWKvHSAcE0Re3FdxVvkmbJvk9pQJV1RZI2TKNIorSqSkohEEejFkgP4awxwVExHyVRdsVyRkpTR2m9LReaVGXgm4ffVdRUhpdoF0HHzQopDp8jLrKS25QFQOSKx+MIUvTxA+e4ShI6fx3U8SEXaSEOiELkqOSAmUlw48aiZixydTMhjOVoiRJ3F6AofZa7CUAhLaVhjKSpcoMwYyixFWwfb/NbgJKzmFbyp/n18qXMWvrN6Ih5emUFvtQzedlVyD6aIbhFKIBCQlRx5GYAj4QcZGJNm3mZMqdvT1MFgEKC2hyOaUe3otpV9WXoOspID/5j622+nkJwhCx0Mph2wXLVxbyvHYH6dKDREl7at8wGH11N2V2NJBtnr1f5pXSUSEp7quxR/lCfrpGEyITVpp2zJzpDDSXVWcw7MzLXxhu1345W1ezDJc5S5Aw8OBjLFQs5x03ATVjtlgJS6Qwa3r4jPrEKxB9VYEZq44wkD7+sb4kA0LU14AGhLvdtXYQXcocosTmPD60s4sX5ISgSky5HquJFpmZl5gbK9C0fVoSH89FwkdT0aGzZZkiVMXEcag0xXPkvZ+nE0remJQZJ9WeiFvA4nwIQKt5AH6lxOouuCq3lKvSyRYClMrNOsLFRYAZ0sSvgSeUlA9hwgRoGnGUi1A8ConWhhYqvYbHUagBGVDR07ThrS9vG/bfKJFjVCqEyytLA8duyYsRk7jmPiipFyodVqjXxG9sZKpYLFxUWUy2WsrKwYIpBIRzoHWQ3Jfjc5OYnhcIhWq4W1tTWz6KfyUqwuO46W/WMTaOMErG17HCchaN+NrF90TiIPfN83llGqM4Kt7CBFW61Ww3A4hOd5I0k6oigytj4iG2yVJZ17nHiw1SY2sWqXmf63CU277xChRiSilOvWV1IHUnseOnTIqGhsSxvdIxEIFMuLCGMhxMiimIg4u68CMKQgkQqdTseUPY5jk9U4TVMsLy+jVCqhVqsZNVOv18Pk5CRWV1chhMDa2pqJozY5OQkppbFjUl+nxTKpA8lOS8qpfr9vYpCtrq4aRa1NsE5NTZk+TCQWkQqUVIjs/ER+Un3TvZEaUUppYsHReCO1Gam/SM1GpB3FYGy320bdaydcIas8ESNpmmIwGJjjqP7pnERq0time6ZxR/vRvETqUOpfpAgjhdnU1JQhVejcjDGjWKZjySocRSpMGJG+pEi1Y+NR+YgEIcUzEVRkgbVVsZVKBbVazdw/JcshJV0Yhti0aROWlpawsLCABx544DiLMM0BpIAdDocmucXS0hJardbIHG7PI+NWUPulDdUdxdncsmULzjnnHERRhHvuuQfT09M4/fTTUa1WcezYMRw7dgx79+7F4uKiIeOHw6FJblKtVk27UVtTnMHp6Wk0Gg088sgjcBwHvV4PJ554IpaWltDpdMz8/PDDDxuSsdlsmvnJcRzMzs6i3W5jcXHR3Af1TzszMI1XStSSZZmxkNP8QMmG7GzgdJ5GowHXdY0iLwxDozTevn07TjvtNNMX1tbW8OCDD4IxZgjFarWKlZUVo+BLkgR33XWXmQdorgIwoipO03RE7Ukk+/T09MiLFXq5YocRSNPUJF4ql8uYm5szLxioTmxlIX2P0csSe/6269bGRi+JNtpO8yt9N9O8QN8BNE/RPE1jnghTmovtOJ+keqb+THMSKev/4xCIUhGILhfwkensmT5qTqRJIG0bgNBxCSND9tmWXg+K2CGlWF+qN4key+Ejh8PFSGxBAMYCTfH21DYPs6Uudu1YwXJcwSOr03C4wOXbH8DXD5ymLHZuhuVBGSdMrmL/2gR8N0N8tA53Rx+TpQH6uY8oc3HO9oO4a/82sCFH7ngIp1LcfPgETFUGeM7UAQDAMPMQZS6+cfRUZILjvNn9eHHjfhxKpvDgYA79TD2wbSm1EPIUZ5QOoc4jrOZV1PhQxYBkynJM1mf7HlPpoA/PJA/xkevkK8oSDQA+hK53VT8hyxBJFYfSl0ACFWNxIW2iwmNFzsI37ZdKF11RwqPJLLZ4a3hguAlHhg0shjXM+x2k0kE3DRElHlxHYDmuYqIywDZ/RccTdJFKpSQkQnHdUu2oz7Guxuzm6m1IhcfoyxKEDoafa7XkQPg6iYyPQe6j6sTwnNwoLCfdPh7uzmB/bxLNYIjdC5tQDhL0hgGGnRCIObpzIWbDHnqZj01hB8PcQyYdPDKYxUnlRTS9IY4OGwicDKnwFak99LE73oSwlMBzcvSiAIGXotcPUW0M4TCJ1kIN4USEYeLBdzMALtYGJfRbJXBfqYsggbyZgXddDHsBnCBHHrmIhj6QMfgtRVKAA+FKqiyXQkK6qn5YksEdQNuQHW3DVKq56hGloHOHilCKIqW08juKeBtslhjOKPVa1FALaK8HkxEWAJIakDQU6ebEamGdl5SCyYmBYE2rB10VK4wStqgEH2qRnlYVMbGerICjciwHS9dJq7jBjHqQ50BaUSSiE0MnSmFGHRYuS+ShIjzDZT1xM8DvSWQBQ9JYT/biDqRJlpKVdfkDbbf2VEISJ6ZywSSiIUKBJ+ocYEAW6m1aWa/IIyV1ykNFOAYdZR9OKjokw4qyjlNG3zxU9SV8dV+k8JSuqie/A2RaZSlrEqLLkNakuh8ALAW8ns7KnMJYvZOGBKNkUkOVcIYIWiIzX3/Onbjh4KkoLWtiKye7JtNxFgGeK5u78LUaywVO2r6Ik4NjKlwAuEk65EAiZCk4k6ixBAPhYcZR/msOCV/HH304bWBfMoNHo1nc19mEw90GWvdOIZtN8KZLvo+7u1tx97/tQmWfA75ZqLquaaIq4vDWGJzIUeoxBgx2ZJAVIFouI45VAhOVYIchiACnpBSb/e2qn5YWGNxuDBG4qr2yHKJRUlZYl0H4XBHoNdV3BpuEVvRqi7gEeMzgRhw81krNVBOLmgwUjuqzeVlCeCqpiKNjHxp1bUkRe0To5yUBVlEPMIwB6AdwBwwiU33hqlP/HpeX+tibCdwWz2J/MoNvr52Eh1dm0G6XIfsueMwplwu8DkNaV0lcWK4GoVFD6j4nfKV0BACZKLuyG6ls7E6sLPxEGrJc9V1S8/NUQHhcZT8vuevjqqTs2Jm2c5skSy6R5Vp1KBigYw3axKF0JBiYiVdoyEb9GX33mP8ZDOnKYr6uNvQEkCkLeR6ScpHGgtTKTq22DJWyluVQBGyqQlvyZD1pEBM6+7W0ylbgaYXxBSUt0m3Cy1YeELEAHK9osdWL9g/BJpLowZ8WEjYx5TgOms0mJicnjcqLiB5asBJJRLEOO50Odu7cibW1NQCKJKvVagBUIolyuWwC6tvEYKlUwsrKClzXRaPRwOzsrFmIEdlAysdGo2FslaSKsa2f47BVXBvFhbJJxnEFoF2ftBCjxTTVvX2eiYkJQ4DQYpWsghTHjYgqiiFJJM+44gQYJSTperaS0iYH7fal+GT2wpIWf7RYtZNPTExMGOKHlFSe52FyctIkmCBiiuqCrG20zU6UYSviiGi0425RvVGsTGo3IoAAmKy8RG7QQta21K2urhoFG6l/aOx0Oh3Mzc0hz3NjuSSiyU6wQSobsujW63Wsrq6iUqmg1WoZIozane6RrIZkJ6U+YMfgIyKqWq0aqzplSyaCjCyxtuqK1EGUfIbIZtpOi/skSYwijsh7u09Q8gYqG5WT4iWScpQyxBLBSbHqiACl8UpjkhSF1GdtkpdUdGT3pG22/V1KiXq9bjJmkx2YiL7l5eWRcUUWZJojSdlGKilKJENESb/fN4QVKdMonqaduIIxhhe96EUm1t+tt95q6ti2/9I1wzA0bbFt2zZUq1Xcf//95mXC+Fi05wki7MvlMsrlMur1OrZs2YLZ2VnU63VDkrVaLczPz5t5cv/+/z97fxZjW5afd2K/tfZ45hPjjbhzZlberMysqmSxiqRIiiIldUskNNhq04InGPaDARsNow3IhoF+6Re7AXf7xYAND0DDsmEI3XK7W5I1sS26m+IgFllDDpVz5p3vjTnizGePa/lh7f+KHcFSN/VY5drAxb034sQ5e6+91tqxfuv7/t8THj586BWf7bl6OBzy6quvekWpvEaCQgTsy7wglnxp2+FwyMOHD/19FfgusFbaD2A0GnF6esrJyYnvmzI/i/26KAqfiCzKNgHH16GhbDpJ2woIlpqmeZ6zubnJ3bt3Afjyyy/J85w7d+6ws7PD+++/TxzH3Lx5k9u3b/tnxGq14smTJ37jQ+D0YDDwsP+6glrgZXu89Ho9rw4XVbQkfcu8Jkpg2TTb2tq6Eggjz0e5bhl3bQfA9eeN/P2jnmft8d1WILe/L/21rdwXZaH0EwGG7b4g91DaRfpNe6OpbatuA/MftWn5Lzt+/AGigl5UMAgyjO0QYIh07RVjNYp+szqXdE2XTtwQa2ofplI26kIJ5RDFXaRqUllIK8vSxvR0ztxoMpOgm1qJGRFvdl7yduc553Wfr3Q0z+ZjZllK3hQ/erEYUdYB03mXQVKQ5RHFJCHJ4MHeMd8cP+OkGPDQbPH+81tobak3C5SCbB2zMVryyvCMvWTKaTng4GJIVQVsjpZ0o5JJ2eEHq3t87+IuR4sBqzzila1zni/GLu35xow4qvnu8hUepAfcii54Vm6xF058KnPd2LzlyKyrGxmIhBpF0ViOXSJyCA0oxNKEjGgfPlKamM/zPVYmdiE0GlJKxnrtfhZXr/HD5S0+VXs8XW5ynnXZTpYclwO+mO9wvOxTVAHdpCDRFYEyTOoukaoZ6PWVOoixqslMgkFUlopAWQeMbejs6MDMuAd8aUOvyFyZmM+WewzDNbmJWNcRizDnpR2zl0yJVM2zbJPTdZ9lEfH5y12UgtU8QQWWIKmJBjmrWcofHtyjG5cUJqSoA1ZVzKuDM1Ym5rzosZUseVpuMM8SlnNnVahrTVUFbGxNKaOK81mPugjIVEy/58IW8nVEJylYZTH5IiEd5N6ynCQlqyxERzUm1YRHCeam2w2sTxPUqMTqyEEIAzqvQWtU2ZrcGgtsOXQwrOw5qzDWKfTCtfV2wsYF3iyq3eJ5vWcYfeZA5WrfJRqjLPFUE6zdqcbTRmlVNH2qsTibsAFdygEE21iF0/PL4BZVQzSnqS2ompARS7gyRMuKshtCBzrnFqustyTrwlmMq66zJ2PdZ+XjS1VlHYPuOkgTLZztVJeW9MxcWqAT5ZSHjX3Z1R3Et0e0tMQz6z6ro6h7DrjUaWPttFDUqqUwdNcUrpo6h4GDC/HchZnko2YTJHOBGDZw90XZRhG1bJKjm3uFdfbneO7CXOoE0nP3OmiUX/NLOzPWqSbzsQOPJnLQ10SKqu/gFdal4dapa6fuoeX865a9ZMr64zFJ0w0EFEl4impUl9JPlIV6YPjzO5/50gNucm5+afMqcM2mLshUwCflNi/KTd5d3OXjyQ2OZ31Wxz2ii4Bo4ezb+QbE35hydzQnAG4kc6LtNfrzPhsfKdefO5psW1GMLMVOzWBvzv5gzuvDEx4utvjsYJfRR2ETzoMPsKlTB4HrVPn03d5RY8VJQ8J5iaoNGIuua8iAQcR6xyloxYIfrFVTl1CUt/jUZGVdLcOqD8XYwXVdNxbZ3CUtm8iB8apnqQbGwTTtzkeUd8nWmgc3TviVrc8ZBWv+/X/wXyNcQ9Z1yr3/84tf4/9gNE8vNlgtEuw6QOWaIHPQ0Gq8Ui4oXB8LcoUudVPDEN+HUE3dw6Y+Y7TA1/z0dTSNsyVHi7qpJ9qUMOi5gBkbBNRRE3yS4iGc1C0UVaG1Cp+C3NQidB0NryD0SkMBg8pCk/RucanKzQMMGxn3GbXyKlCfqoz7t8qV3/RILtxGhFMPSuCQxST4xGhJwjZRo5ZMcWpJr27E2b4NXqH70+Mn72gDRIFEbfUh/EmVwXX7KlyCp+twTJRX7feSr7cXKqIGkcW6qJs2NzeBy+RVgSMCRsbjsYcfm5ubfP7554xGI6bTqV+0yeeLskuUQuPx2NdFlFp1oiySsAJR/Bnjwhvk3KMo8rZqOW85N1k8tRWEbRgnAKUNCaVt2l+T14kaStpOFFeyWBRFkCyW2ws1eU8BSKJuiePYAy6pJda+L9If5Gvtv+Uarv+MLGQF4AyHQ6+OkX4jC940Tb19UECdtLeAzp2dnT+x4JTQEgFZ0gZiQ5TFeBuiifJLa02WZR6EiSVWPleARZIkLJdLBoOBt3NKuwm4E6WgtGsbfrdr48m9FRAo19wGi3KPpLbhfD73qkk5R1G+Ss3E0Wjk77f0PwGmYiWWPiE/J+NMAIUAofPzc690EvAh59qeEwR+SB8XcCLgLooiXxuurf5sq0Ql/VjgiSiZpU0kJVo2DK4r6MTKLZBaFIaSKC3nKeElAjDa/U/OX8C09ClRj8pnSr9brVZ0Oh02NjY8qAU8fJOxLzBRVLNSa1XaT85N6gG+8cYb/M7v/A7vvPMOjx8/9u17Xe0tAFTmp1u3bnF6esrx8bGHSW0oFAQBm5ub3L59m62tLfb399nd3WUwGPjNk9VqxenpKY8fP/YwXFKeBdbcuHHjCsza2Nhgb2/Pz7uDwcCr9d59910P9Z8/f85qtWK5XPqyEgKssyxje3ubuq45Pj6+ojZrB9bs7e0xm804OTlhsVh4FWL7WkX5KfBJYHjb6i/nIRsbMgak5IVsAEmK+tbWlr9n0ucePnxIkiQcHx+zvb3NN77xDebzOaenp3z55ZdcXFwwmUy86lk2pmTcy/OjPR5lbGxubnp1qbw+yzK/ASTjW55jaZp6dafMf+3+IRsIYk0WdW37+XNd8d7ua9f7nry+DT3bmzft+9H+HaINIDudjt/MOD099XOjQFBRHrZrPsr1tjd55Lkt9+z6ef5XHT/+ABFnM9NYEu1qznV14dViGkhVQWZjYuUSjmur6aoc4wx0PoE4pva2WzBevXhSD+jq/Ep9QFEwBhhqAr7Ib6CxvMzHfPfkDoMk543hMbf6Ux5NNvlwsk9RBWRFj26aszFc8frohL3ejD86fR1VK2Jd0dUF6zpCK8vuxpyzeQ9jFN1ezrf3n3G3c852uKC0AQ/SQ5L4TZSCsgoY92e8WI75fLLD2aRPdZJiBxV623I86zPqrVnUKYeMOCn6jMIR07rHvE7ZCWaUaM7qPnvhhK7OfRAJ4Oy7NvoTNROLOmAcrJoEY6cA1LhAmVSXLOuEx+UOp1Wfw9z9svhGesCtYMpx3cfgkpNTVXIjnvHHF/cwVnG26HLa63G8GnC66FHkIaPB2n0v77GsY1Zdpw58NTn2wFeOXhOa4yznzqpc2tCrT+XIWwEqx+WQf378FXY6C9Z1xLuHtxh0Mr66cUysKw7zEeNoRahrTmZ9srMOeq0xgVucmnEJWUAdhKhCMz3fYLJdUNQBWekUgKE2PJxvkdchtdEsi4hsHaMCi9aGKg8JAsPFqsO9jQuGcc7T8w2yecIkD9FpjSk1nbgkKyJ0XFPkIUFSE8cVYWAYbC6ZH/dRucaEFrOISLfWZHkH1iHlwBItGgi2yKn7CTqvvCJGGUM8syzuKoLSEp0YsnHgFsaJgx7lQHk1mQmdUqpzpIhmLvAj23JBI91D1agHVZMG65SKVdde1oGLLeCUXZFT27uwltmlWlBVlrLfAK4EysDVfysHTs1TdhXKhISr4GrdwczZdevUKQ07J9bXHwxyqPJGLVe6YJJ8Q5SFjaoocfZmEzuVZb6hXMrwSBFkqqnp2NRuXLp6hnUCZc+9Pr24hHZVqvy56NLBoWwHqo5F1w6oVh2auoXW2YcbNSA4lZWuLiGkqC9dyITyqq5o6WBi1WlUfZkDJFYrr84MclzNyKWhalJs03PD8Jmh6Lmal0EBwVP3c1a5YJSq44CrsvD1bz3inx6+Tf/pJVBSNSTTmrUOms+DKlHO9tzUhkt2V7zVeUFmIzaDBTG1n08mdZdP831yE/F3Pvs2+fM+yYkmmdDUj4REQRRKeAasdy3l7YLXxjNudOZk1imntbZeUWkVnL9j+O/8uT/g53tf8nZ8zKbWaKUw1vLvHP0qnzzZZ713qYRVNUQTV28wXEK+6d4nOdekp7mDVKFClzXUNeHRBLSmujFicTPAJJYgaxR7ZRNk0wAvXTn47GzHri+DU8OFK+Xt/nXqYJQoE61u/t2vINeo2HBz74K/uP8p/63RH7PXWOtLLP94+QomsQ7UhWA6NT987577nKZmYdCoeE10aTsOV5p46j6v6jSW70btKGAxzFyqejxzauR26Imum+TvxprsVMBhMy7x6ehV18FQE1hMx9UYVpW6tCgDqtRNCnUDShsQaBWudmFgne3Y4gGhbTZT5Os2sKjyEjiqSjWhJ0AbVGrrPs/9MEL+nP3eEmaKIm2AeoyvpRjNFcmFg6DFhiFYKbovmjqnWrt7mOCUnAMXYqPXrmTBTyHiT97RVhEI8GinG7YhiCy824truAoN/2UgSmCHfGZbrdjr9Tycu3nzJqPRyC8OLy4umE6nXtEFXAF1xhi2tra8SiHPc7a2tryySGyZr7zyioczBwcHfoEn1jBZVI5GI4IgYDqdkmWZX1BOp1N6vR5hGLKxseHVQAI8rgPD67CtnWTdVoK0lUbSbu1D7L2ymBT1jCwqJWE3z/MrFt5+v38Fukoir6jPZGEm90jqS8nPt+2+ck3tQxahbRt7r9fzCh5RYVnrQhTKsvSKNbEbSuCHWGZlcS0ARmqhyTUo5dKO1+s1BwcHHh62lZWiJGqrYaReoSxYpb6aLF6lP8l1tmvWyb0TiHR8fMy9e/c8LJLPbgdTCEBUSjEej1mtVh5GykJfLJACLaUN0jRlNpsxHA5Zr9e+NudwOPQwWSCPwDQBb2JnLMvSqwQF4Ityq10bTu55G0bIuF4ul171KH20bRWW9m2HViil6Pf7vm9LG7fVT71ej/l87lVk0m7Sv6TtJMlW2lHA5HA49F9rq8/gst6eQOI2pJb3bFvcoyjyUFDG1vb2trdHyrnK+7U/QxRhgE9gFpAJeOW0KPIk1ELq9D148IDJZMKbb77Jhx9+yGKx8Ocsc5vAIAGpgFdJv/fee36cyxza6/XY2dnhxo0bBEHAjRs3uH37tp/PPvvsMz7//HMODw+J45iNjQ06nY6fY168eEGn07kCvMIw5Pnz59y5c4e/+Bf/oq8ne3p6ygcffMDR0REAr7/+ugd+AiTFUg/4TZDZbMYbb7zh4Wdb9St9amNjw/cFeS7IJojURRXgKBtCUj8R8HOyMcaHkohtX2CYMYbd3V3efvtt3weMMZydnfH973+fzc1Nvv71r3vbcRzHfP755xwdHfkSGu3aiDKvSx1DsakPBoMrGwCS5t1O/c6yzKeFy9wAlxCt2+0yHo+9JVnmbhnDYmtuJyZLf24r9a4/a9rPZmlj+Vr7+S1jXFTTct8EvMv7tjfP5BkvwL1dQ1bGk0BFUedKP9/d3fVtKNcn84o8S2QD4V/l+IkAiKE2RLqiC15dFuCCVBJdNnDLMg5WFNbZadOg9GqX6wq2SFVedQf4wJWgsc8FWGoMgXL1887qPn90cZ9YV5yu+xwdjZgPMx4ebBOENb1OwRef7qOsItjIOT8bEwwL/tmLtwgGJfRLcgtvDw/4cLHP55MdQm14cbBBELuF1du7h5zlPeKmONZ3zu4TasN2f8mToy3KMuDj7AbFNPHtojSwDjhfd4nCmtpoPlzs8+V0m9pokt2KfhPpKfUPB3p9GTKDC6HJTOTB4FjnZDbCWM1AZx4s1igKE3mYmDaqz5KAp/kW52XPqQCVYaDXnJkuqS45rEY8KbZdvUoT83I2ZLboUM9izK5CKct6FWNWIcFoydmkz/msR7+bMStS3hwe0g1GpKrkxA5Ilav1KEnYAZraKmJVs2qCVXo6Z2kSchPxZbbDbjQnUIZFlTCKM3phweP5JkURUESuT1wUXRZlwl/aPeKH05vkK1eIzKSG9DCkfLDml195xB8+eoW60FijiKaa6izmJB+jIgPa8tEP72J7FXt7E2arFGsVSVoShxXzZUqUVlij6MQlj8832e4vUUp2MmA0WDNfpqzymPUkBavY3p+yzGKMUayziE5aEo9yzHmPum8IhwX5YZd0b0V21nGKogripfV1wtDO+6eqxpa+MFgVUPQ03eOKzlntFEdNsm64dKEhdQpxfqk2Si5crbZ80xCu3CJ5veOgiLJQDR2YiueNusc4oNBWHwaZ+7scONDXOXb1GE1kCdeKeNpAMfD136QmoW5Si03o1Il16mo7qtqpDyWBWezLydQSzx08KXtOqaYvGmVgDWrq6iwqC0FhgIDlfgOvRhAtHfCrIhdWkRnlAV0xbNRmlW3VT7TEJ5dzV7ygUUJehkxIQnO0dMCmlGTpwNWRq3rKp1NjaWCMRVJ/q7lTDqrafT9cKV/zUdWXCihn+9SE68v7ud5ooGMT7iDp2HXk2jHIFL0Dy+xVxS9vfcH/5R//JTbWjXKzdJ+x3grIx5r0wrifTRsQlTi77S/cfE5tNS/LDT41+xwUI16uR5ys+yyLmOn3tyl23Tw3+lIzelShasvqRsjytgNZdbdG9SritGTUy7g9mJDVEfvpFHeXDMY0oT/ahX18+2c/5W+MvseLasx/Mn/Hjeuyx6zq8C8OHVhLj5W3Wjt45wDvxhEs7gDa0j20xNMCmwSYUKHKGpsmUNfYfoe6ExKuof/EQSbdKPrAtVPVU+SdJmAEoFEnOtu+oupY6k0XTGIT49RwlSK6aNJbQwjTkv/Rt3+f//bwPW6HfRYm44dFwnv5Nn+4eI0/PrnL4enI1320GsKLEHSjrLQWAleLT+e6gZbNgjuwrPdcsjGhRWfOOh0tNPHsUmWoK9fHdN38Xbk+ZELXX7JR6FS9CR6cl31LnRoHxa9ZibEKkxiUVSijLmsUyvq/UQpa1SgHDSjUFTuzzhVWOQipKvc9Nahc25YalQf4DVb5Xck06sZKufdurNC6aTsCS929rEsJzX0tFPG0KV2yY0nOFP2nqkm8hsU9i0lrb5/WK0041w7GDmpMrH06+k+Pn5yjvRBvLxyuqwvbajdZnMjPw1ULUxumyWuuAyk5tNaMRiPiOGY2m7FYLDg9PfWL47ZKTiyNohC5desWWmsmkwm3bt3i4OCA7e1tVquVX4TKInaxWPhrE5VhFEWcnp5egX6yYJSFqqhvRG0lQQKyIJ1MJl7dIlDxuh1Z2kfOp60WagO6NiyTBZks4kXpJZCnbetsp46K+kyUaALpZMEqdd9kQS8LNAE6UttO4JLcs+vXJO8hirR2TUVRpJ2dnTEcDun1ej6YxBjD9vY2i8WC+XzOaDSiKAreeOMNb4M8ODggz3NfizJNU79YPTs78+EAYu1rK69kEd4GU3K/2vciTVMPAcUWK8ohgXJijRbYV5alr20IDu6+ePHiivrQWuuVSAIRlsulvzcCMARStetTijJSKcXR0RGj0ci/RqBXEAReKZamqYexEkjSDmaQ2nR5nl+BKKL0EYsg4KHecrn01yfK0XbQQbtPClAQ+7eoJUXpJW0tykbp4wKpxJ4p7Se2Uxl/AohkvhFQIn1S+rmAC1GeyX2T8dZWSsn4BPw4k59r22OttZycnBCGIePx2CsyBcy17ZzSVu0gGDmn4+NjlFIeXs1mM3Z2dnjjjTd47733+MY3vsGHH37o+wo4GJfnOU+ePGE2m3n7p1iHRbEr9fjEml8UBYPBgK2tLVarFf/iX/wLdnZ2fG3Bu3fv0uv1eO211/x9l3n/8PDwikJWaozO53Pm87kHeX//7/993nzzTb7yla/Q7/e5ceMG8/mcly9feottO+le+qjcz8FgwGAw4IsvvvDnLXON9AEJxhE1tQBigVntWpVi55d5SIBh29K9WCy8FV5e+2f/7J9lOBxyfHzM0dERZ2dndDod9vb2+LVf+zWstZyenvLJJ594hXLbrt9OMZZ+LeNQYHKv1/P9o9/v+5IaolgVGC99uv2eYRgyGAz8GJXnrYQMCXSU+Vvapz0Xyddk/m6DwvZmnrxOxk5bvS79ROB/GIZe0dnexJP3absU5PvSp0SVK+NoOByilOLi4sKrzW/duuXV/mIBlzYVFbWoEOU17TqO/2XHTwRAbB+11XR14dOBI1W79GWrfRBKZjVz0/G23aWNvQ1ZjlQ58OjUaYa6IRSZjVxgCNor2iJV0w9zTrI+u9051b7m1dEpn5zfYF1ELJYpelhiL2LevHXIB7O7dDoFi1kMLzpEBRQbNR9Mb/JstkFWhiwO+hBZVFLT6Rd8fr6NMZpuWHAnvaAbFnzw/BZKGzrdnPUqocxDt7BKDBRNJ+5VnM175KuIolvyONzi5GIAynI+7nFsB4yiNV/ke9yLTxkHS2qrnXqrgbAG7RWJZasdUhpIaAMHBKoNHha73I9PwcKk7nrF4uF6QD/KOS6GfKRuk9mQnXDOSTXgg/ktAmXJ65CsiDClJpgH/PBgHwCTB6hSc3w8QoUGM404n8dkRcQwdpB3K1ryyfIGnaDka72XjIIltY3IbEyqCgob+LqNM9NhUadM6w6H2ZCX6xFaWYo64MlkgzfuHhHripfnI7SCXlBwlveY5SkfLm7x+ek2+iSm7htoAO+9G2e8e3gLc5qgnaDOWRdjS3QaUg0MwapJmlUhh0+23AK5VzvF4DKlyiLCtMQYzbqInPI0KviZ/Rf8UX6feh04EDyPmFsFlUZ1Ki5mXepZ3ChzLFXdYXBnxuJWBucJb9465IflTbJZgl5roqVb7MfzZsKrDBjjQFtlsFoTzWt0FTjrqXa1A8uedgo55cI3gtwSNEo6GzpoVjcJxDThI8mFg3cOOOAW6w1UUAafygsN6NJOmRQtoXPi4E+QWaJFE8IRuc/ANnX+mjqDGAfdrIL0whDmiipxICheWuqo+flmxgvXlmLkgkzKJhjGxFDh1F9Bbsk7Chu44JVkarGps3F2TppgkEhgpbO3Vh33/yBzkM+EUIwb62/cWI87yifOWuVsy/HcqcSqrjs3CVOp06aeRY5Ps7bKWavT3AFF1QDEYngZINM9smRbinzT+pTscAn5lm1UbZYgv0yszbcaC/VCedusWDFp1Ie6dIEsZU8xfFIT/8yMjxY3GX6JD/XQlTuns3cUdVoTvK+dGrLTSrvu1Xxytsv/+uw3KKqQutYekBujCAJDPFOMPw85/vMF6z1LMgkIM8PitkI9mLPZy3iwccKtzoTNcIlWhlRV/JPjtzFW0VVuU0nWZq5WINzrnvNlucP/8j//mww/iZi9XRAPCnqdnFUWY+smQbuGoHbXa7Vrl6rj0pKx0D0xqKKi3Oo2cE479Z3WYC06q+mcVhTDwNX0i1zwTTF0UE7XztIeFI36rwGrEq7R2V1RZxF2HaLTirRbsDruebWuMvCzd57zP9v4jB8UCf+702/yaLnFx8c3WC8SmEZEM03Y3N9yeAnh6tTNQ+FKoQvtbb8mgqpjQIMqGwv/1M0V4copOcPcuL5ZWsLMoHODDRRVN6Do66YOqPJKxKqrqHpQ9iw2NFdqGIqaUoltWLl+b+GyRmFbodd+vWleqxVYi2qedYSWelBDrdBNHcu6b7GFpre55tZoyqOTTcrzFL0KsLFt6h9aPy+hnfJQVQ46C1hUTa1MZ3sGnWvSU0WxYQkXiq33Latdxdm3DGpUkHYK+oGhqjVhYDDWbYZlWYQpQtQsQhde5PjT4yfkuL6D3wZF7ZpIomq4bq0VZZzAxuuQ6bpy7boiQj5PFo69Xs8v8Obz+ZUgiLIsvXU0iiLG4zFRFPH8+XPW67W3OmdZ5gu1i/VNlIcbGxteBROGIefn5x6giqJLVEoCHZbLJbPZjDiOfaqvLOraUFRAiRTcl0VgW3F2faEl7Sb1yQTUCECRcIAgCK4kRIvqRqCTfF4bsiil2N7eBvB1sETVJioqscnKwlUWhVtbW0yn0yuAp61akZpsAvbEQig2ydls5i2pAnKAK0X9JSm3vdAUdZwAZ7mH0+nU20Vns5m3Qwo8E7gnUFNApiQza6096BA7rVh+AX+NgFc31XXNaDTyysl2/xCFllLKAwXp37LwzrLM242l37dBdTvNWBb7ckgSs1g+V6uV72ui7myDaEkql6+JGhHg/PzcKxattX7RLgt6ATBaawaDAf1+3/fh4XDoVYdtsCbnLFb+tsK4HY7QVlfKPZHrkT4jtvp2errYQK/X3BQwLEolgSkCrAVQiEKxnfgqh/Q/OVeBxe26kHJd10OiBOKWZenr2kmKdK/X82NL/i8bHQLo5bN3dnaoqooHDx7w6aefcnR05PtIW4kr0FfUd7LJ8uzZM6Io8snHooSsqoq9vT16vR6np6e+78vc+c4773BxccHp6SnPnj3zmyTdbte3jdjQZe4SgCep5jdu3ODrX/86L1684LPPPuPg4MDbZff29jxME7AlfUc2MzY2Nq70KYFo8jyQxGQBojJvyP1vlxyQdhU7tRzymtFo5ANcsizju9/9rrcO7+7u+g2K8XjM3t4eWZYxm8148uQJT58+ZbVaXVHxyvi6vgnWVk5vbGz4oCUZ+zInT6dTb1GWeozy3Lve32WDQ5R+0+nUA0O5vvZzVP4tf7c3AeX7Mk+1FYKyMSibMkmSsF6vmc1mHoSPx2MePHjAxsYGh4eHvPvuu/453S4x0bYtS/9pP+NlHMs9k/qgd+7cIQxDbt68SZIknJyc8PDhQ19ao13/ttPpMBwOrygp/7THjz1AVAZMk6RrlKtXGKkKjfayDxeMYpsE5sCr4zIb0VNuQEkxf1Ebzk3HhYygmdRdYlV723Kqc2Ympatyzus+Q73mr2+/S2YjTqoBF70ej1db7PYWmK7icb1JWYTUg4oPPr9NMCj5i3c/4/RGn99/9wG6CkhOQj492eXmeMajj2+jEkM4DShrRX9jzjDO+ZnN52xHrrjwqoqpspC3X33B4/NNtwi3imRrTVmE2HVAsNLUQ6hrRRAZxv0V6zIiTirKMuCLiQt4eWP/iMfZFpvhgq1gQd20zV6w4KTuUTe1BF1SauSDDgTSljbkk3yfF/kGo3DNedXn1eSYw2rEH1y8RlZHrKuINKhY1gm/P3mNWZFyqztlGK55Ot+kNNqr6tQ6cCEY5x10pl1d/NSgJxHBjTUmsFBoyjLg8/Ntwi3DUT7k+WJMoA0nWZ+3hocMgowb0ZSVSfx5QsK8Tnm03mZaphyvBmymSy7yLqeLHtUPxvzHs28SRDXqix6n+zH/vHqVxayD0pbjSZ8kqdj46jnTeYdyHlN3LA+/2EP3S6LZpfornrjk4rpjCZaaIGvq1tXu3ya2FJElzyMGvYy5VVTHHbCK9TZ0ujmnqx6xrkFZ1Drg5PmYYFASBAY9MmyOlkyXHQb7M7pJQVkHXEx7zCdd4m5BVaQczIeowKJ0jTIhgXs2EqwNqqwg1FjlFuM2UKi8IppmBHnigSHKQcCyr7EasvGlZbZOLxVW4RrKcU24dKmzdSyAwIEUVeEDFjw0WCkPCsJVU4uv5+rauXN1teSCFUQrB9DqSBHP3M879ZELD5H6aliIlwYTKIqBe//0wvjX17Einrv/F/0GfqYOkAFNsIyDc/mmIt9w4E9qMLrQC1cL0gZNEMvSAcI6VUQLe6kuamBynTgFpy4t0cpSNrUWdW1Jzy3rUFN2HcjUlTsnEwAp2I1L4KcrURBenQvjuSQsQ3pq6R3YJgXbNrUSm8TqQJGcW8qBs2PXiVNbRnM8xJR6k3XHqTlV7eo5xnOY3wn4b7zyHn/7j3+Jncz9jFMfOhA6eAR1HBAU1ttWlXWgjNgwuegRJhVBY9sPAvcQzrII0IQVhGsDpW5qYQJKU399wf/mm/8p/2zyNv/Zl29QV6+Qdlw6/bCbEQd1M4drKqN922Md5L2VXDDWK/Sy2UmMDUlS0o1L1nnsa+xVvUZ911iIx5/BelthY0M406THa2yoKUYhyUWJqmsHsqoaG0foyhCtKha3QtY7zi7roLSzmteJU9fK/Su2aoJBidYujKrfybm7ecFfufFD/nL/I24GAT/zO/8Tgkch5dBdz3cf3+PbJ/89Zkd9dz3ahZdEJS7Ip+/gmKtPqH1f0tWlirHqWmfbrV0oSLzWhCtX59SpXy3KuJRxB5Xd/02sKQYBxV7YJJQ39Umb2pGuzIBTZ8scgdQftFwGmiBKPu2s2R4uij9fNfZledY37xE0kLEGjMImhmBYkCRu8yVfR1gTIc7F8c6C//3X/w5vRRn/VvfX+f3DtzykdLbnVhpzffkLlA0ctBdrsw3c+QdrTXKuKMaW3nNFMjEc/QLE+3NioCpD1suEIDQkaUknLjFWYa0i6FjKuCYYrskmY8L1TwniT+IhC01ZeLQXSO1FR/vrstiFy9pibRVWe1Ej/28v5tsLgPPzc19bTtQKch5i7ZxMJhRF4QMLZBG0vb3Nxx9/zIcffsju7i6bm5sYY3j58iW9Xo9XX33Vh6iUZcnZ2RlpmrK3t4dSiul0ekWtt16vrxTwb9eful43TezDAhySJPE2MgFEcp5tQCWHLKhETSbqsDRNGQwGjEYj+v2+hxdlWTKdTv3naq09wBLoIjUbBXxKDS4BcuCAjigaBXKICq6ua58eO5/PvfJPrkNUPnJtog4BuHPnDuv12tdIE6gnwOTk5MRbICW8QKDB0dHRlcViW7UmNl6xKIq1fTwee4vfcrn0tbbaik5ZbN++fZuLiwt/3wSiimVya2vLA57ZbMbx8THT6dRbD+W1bTAt7Sd18mSsyH0QyC33VmCCwFuBEW37c1ul9/z5c39tAvgE2gvIEvu9BKIAvhaaAFRR9gG+f4iqVa5fzqOtMF6tVl7Baa319RHFijgaja5YhQWQt+eK6XTqoa+MHVEw1nXtLbjt2pBiXxTw3lbNCriEy/IDApIETEn7Cnhr17+TcSKlCuTn5XvHx8d0Oh3m87kPthCVqyiXRUUltfraltI2RGmrzGT+2NjY4M/8mT/DZDJhc3OT73//+x6eSxjJ6enpFau3wN84jjk9PeX58+d+k6StcJNxLLVZAQ+ebt68yYMHD/jt3/5tBoMB3/72t8myjC+//JKjoyPOz899KJXMv211uSib9/f3Wa/XPH782JeXWCwWVyyl0vYCWrMs4+bNmzx9+pTd3V3Ozs586QApL9DeNBCr9/b2NmdnZ36jR0CvwN3VauXr2kq41o0bNxgOh2xvb18p7bC1tcX3vvc9Op0Os9mMhw8f+lqM7777LmdnZ1xcXDCfz70Std3v2uUzpL/Jc0jmaumDct9F/d0uGSBju53y3S4ZItDQGFebVDbSrtf7a98fOdpgUP4vR3vzCi5hsdxXeWYIBG+/7u2336bT6XBwcMBXv/pVVqsVH3/8sb8Xcn5tcCl/2ucqc9dgMPDzt9iV79y5w5MnT/jiiy8YjUaA+71E6mzK/CNlHXZ2dhiPx7x8+ZI/7fFjDxABumHhAk+aNOFUlxQ29DULA2UJGoVhbbWrXSg1EpUhpWRiuvRU4ZN6xdY80BkEeOVdbTUT0wBFAnbCGZO6x9I4FeMXq10iZfj58SNyEzGtOgzjjB8e7VOFNVpb1sddBz11jTKuHlygIT/v8nAdE1QQLQOCNVgdcPr+Lqs3JqTbJZ8t97goOnz28W3CheZDdRPWAcEywA5q8nWIXmmCSlFtlURRjTWautKcz1zthDILCSJDUQXs9tf8s4M3uNGd8+3+IwobEKuawgY8q8aXwSTUpHpNYRJqINUlKTU1iqVJ+Gy1R20V+/GUeZ3yg9U9Hi23+ODgJtkiJkxqyu2AXlhwkXV5er7BYpjwc9tPAFgXEbNZBxUbbKXd4jfTTgnVWDXNqMSWAdROIVKuI0yn4GA15HThrs1YRdYJ6YYlvzj+0iVrc7lje1r1mVYdpmXKrEiZZQl53dzbWpPdrIiiGmM0OoTwLGJ1PqZzrljdrTAqQp0GrFNLPawJpm4RrboV9/fOeHJwy8Gdpsi/blJJy4Elnri/dakwiaXYqhnvzxh312RVSNopWOjUFeivFFUVUIcVL+YjdjbmHM5iVKHpdnNWyxRTOvXWzY0pW+mSNChZlAlpWHE0GZAvYxjUTBcptll8B6UDWC60o8R2YvLdLvF5hkWj6hobhajKuBqBHQcOwpXBxIrkwlClbtEd5G5xn280AKEJBLGpodIQzDU2hOEj9zOi0jMh2C4e0FkNQelgYdl34C5aOGDgU31XAl8dVyiHrZ/NQNdO5SagxtXk014daAIHDXXVBFGkivTckkxqrA4oewoTKuKWZbmOQVeXyr5waak7l7X3sA4qFj0oaL6eXtqlgybbxjTqzHBlnZIwhHyoHahZuXCWKlUkF5Z42ry+AZRlX/lkWrEvOzs3EFuCtYOrJoZy1ChF+w4WxlPXVqs95aFnuAKMq2MYrh30hOb9FKSnzraMcjbUquugLLj70DuoOf3NNUfFkMHHsQu8UMon60rqdiersYGiGOqmpqP7eaUtKrBUeUhZBFAqp4hbaqe4fn1xaVkNRempqCvL/saMN6Jj/hef/yY7/yBldk+zfBCwuTtjs7NiUSSN+tw6BaJAROWuL1Wlm98bS69StimrYB3ELLVPFw5X7prr2Kk/8y0Dxtnpg6zCJGHTJy7rrrovaEzoEoVN5IC62POLTetqfyaGYKGJly6pORiU3Ltxxs9sPOed3lN+pfOYV6I+pa35uAz4f8xeo84C8rHFhNYpF1+mLEkJJaE8dOEjJA3gXiuCXDd1Gi3loIHREpJSQTJ1wR9Bbgkzp0QW9Wu4MgSFaQJwNFVPNTU8HTC8rI/YWMpTpyS2DYS1QSu0xCv78GphcBsp6MvAFPlZwkYxqVpqRXW1PqHtNbvFgXU5QLUiDmvKGuwydOAxcGBw1MnYC1YMdZf/+f5/RvVzAX/4+auwDNm+f87Ji3GjMlSXcLMRJrowlmZuGhl01tQ7HFoHyhM4/tdKOoOM9dzNs/2NFctnA37x5z5mGOYkuiQ3EZ3ABYB9Mr/Bk+km6wZI/vT4yTrkl/62XbVtR5ZFQHuxIq9vg4+2nem6GvG6RVq+JwtnUVHJYkwWaYC3wo3HYxaLhVf2iBoI8EBIFFaPHj3y8ODi4oJ+v8+9e/f44IMPCIKAW7dueUA4HA59gIosVkSZtb29zcXFhVeXtIvdt+vs9ft9D4kkFVasV/Kadg21toIrDEP29/d9kvTZ2ZkHkltbW0iCqNh/i6LwlkZRqIniTezXnU7HW4RFOdNWLUodq3ZhfmutB0ZS23AwGHjAJxZAqd+1Wq28AlKUeufn51eSkZVSLBYL3z6yuN3e3vaLfoGCAnI2NjZ8KrYsviV9V+6pgJI2wIbLgJCTkxO/YBXbryh+AA9nBJKenp56uN1WXApshss6iwIlhsMhy+WSt956i16vx6effsrz5899v5ZFrgBpAWKieOv1el7d1B5Hg8HA9/GnT5+ytbXFzs6OVwyKGlH6lgD2xWLhrZMCsEWpI8CrbdltWwivf09gl7SXwEqB5TL+pd8JQJM2lPssrxWwK99vq7ja84YozQTWSv289vzRtiC375WkLst4lPcXICnXIlBZ7LqinhLQKkEecn/FBi5jQ0JW4jgmz3NfK1PArKh64TJkqm1l7Xa7vo+/ePGCly9fejAt8OnJkydeiShjTebmOI7Z3NxkOBx6VSPgVYjSZqIkkzmjPW/+0i/9Et/5znfIsowvvvjC1/MT9ayUbpD5X8ab9BtRJbbvh/TFyWQC4GsrylwlMG44HPL06dMr1mW5b20AJWNX5t3BYODfSxTAMg5fe+01H0ayXC558eKFh8NlWXLr1i0PuEXN/p3vfMfPhwLNBDTLeGwHIQkQFfAn1mw5ZFND2kQUo3JvZVNIngFyP0WRJ9BRVI+ikv1Rz19pq/Zz9jr0a4+r9nNenpdtmDgcDv0mynq95pNPPvGbFqL6Ozo64v3336fX6/EzP/Mz1HXtlbO9Xo/Dw0N/bdJn2n/kudrr9ZjNZvT7fcbjMfv7+xhj+MEPfsByueTevXskScJ0OuXu3bteJSr3arFYeHWmKDz/tMePPUAUVU7UyDmiBhIaq0h1RaCcDRfllIjdJu0z4qp8Z0svKQgoCJrk5apJJFaunlYT9gHO2qsxFDaiJKRGsRUu+DS7SaQM75/fZBiuebzaIqtDLvIu/TTn+GSIXYekxyEPF9scL/tOddGrCPYL9Od9wkVE1XP10PJN62rYzTSzsx6/138NYxWTVYdwql29t+cxxU5NPagJhwW3dy54/HQHZiEbO3OsVUynTlakm8L0NgsIOiWjTsbJssc6jxklGY/yXR7lu4zCFV9Pn2GsU2QGyhA1CkxwdSbP6z47wYyJ6fJHy9f4589eY9RdEyjLeeEA6aPJlgsayTR1EHIYOrXfNE/JJikXYc2k7LIqo8a63MDKjYwq6zqLZ6bQAZidgjiuWE+bONdhhdKWogp4drJBVQR0BzlFHpKFFZOiw+9ffIVbnQn9IGcQZJxXPT5b7PJ4ukkaVpQNYDg5H9Lp5pgGspWzhDv3Tnl+kbgFZKdmNVbEw5xeJ2dSjukcBAQvnUVztaeosoBnp2Nnxw3cSrnqOlhY9t0CuRzapg4erG4bCA3LVcJimZJ2CoadDHNzQfZoAIElX8Z005xhmnEwGaJXGi1gcR6xc+eCUZIxyTocTgfkeUS3m1MUobOzG4XKNeUqdqnMeYAu8Mq4YJ5z8vObnH/Dcve3OqQHCz8eVFERrKHahLKvKbsuDEAZZ/9N5pai54BCuIIws03giWG1F9L/6gWLjzfINwzrhUbnUAya1NkmnVdZdy7h6jLAwQZOFVcOAdMEYIRunLvPdgAjWDu1nQ0ulZBWO/Ue1tU21JVTJVoNNnWv06WDdbqyjdowINvQTfqxs12Kws+E7vOihbNwujReS2fqFI+uoZwV1YRNXcmpsyKb2AVE6MYOWnWauouqsfuWUIwUygZoYVC2UakVDhyawL1ntLQEF05NGE8t6Zl7uWmUWHUq4RTOXp2cNzUMlbNim1iRbVuqwN0vlyALVa68opLGgoy6hN8C2nTl4GUdK4q+5l9/9VN+6/M3Gc6dQkuLMq1pA3DXWEXNeYUOLpnYYlch/S9C95nqsu5lneDgVgON1tsaF5d7eV93u3OC5vtlV7n2TWq0At0EJfWDnKmxpLr049n9vGUzXPigpTp28CmJXLAWAKGlGF2GhWAhWjTANrWoQtF/4ezLph8T5MallwsoCJySV9VN7cdEUYxcDcVqs0IlNTYLILAoEzSvsfzKa1/w79/6pwx0TG0tj6qAv3Xws3zn5D6zLCFQFpUFvp6hBJ7Y0Hrlri4U4dzVDkS550eVujZHuRCXYK0asGx98jO48RzmliAzvo5h2dNkm1EDDJ1duE5cP3Ypyc15iBpQcwnDxHqs8ApQmvPCAs3vJyqnSSJ3IScK5a7J4jaJjPJBJUg/Vc4+XMWWYFhwZ+eCn918xtd7z/mF9DF/++KX+Lvn34ZKYyPj2lpZIgWB0rwdxfytm7/Fb/7w3yRcaM4uXNiUjRy09DbpplakwNliy232paeacmAZfgnrXUXxzpJIW9Yv+4TbGf/dt/6Ys7LH//vsHd7uH/Af/PCXqE5T9wwc1Py1b/+A39z9HrPtDv/eJ3/9UlX50+Mn6mgvJtqKw+v2KHnd9dfIArytQrz+/badST6zvcBp13+ShbcsgAVC7e3t+XCImzdvMpvNeP78+ZUF7vHxsV8AizUvyzLOz8+90jBNUx4+fMiLFy/Y3Nz0lkdZ7PV6PV+zq9/ve+urKMRksdmufyZQcGdnh7IsOTk5uVJkvq3OknZN09Rbc0XBNh6PUUoxmUx8ivT5+bm32gmMENu2WCdFaSUL3rZ6SICV2D1FfSgLO+BKKI5Auo2NDfr9vgeTYreT65H3kcW5KEQknVjq152dnV2xSQrAm0wmGGO4e/euDynJ85zRaOSVegB7e3tenSkQT2zpsnCW0JjRaOShcJ7nDAYDvygXqCgLY4HXAg3aara2Tb/dxwV4dDodJpMJX//615nP59y7d4+DgwMP9kSJKamjSZJcUbrJ9YqyFPDj5tVXX+X73/8+Ozs7TKdTtre3WS6Xvo5eW40nNdQECgvklWsTACx1CttBKhLSIZ8t8Hc8HjOfz1ksFl7tKjBEQIoEHMgYFpWcKP201l7J1rYSt/uZ9M82YJbra5cRkDHcVoFKf5f+KH1FxqSo89q1WwVCiBJMQI8EkLRBs4BjUbiKOlfmKdkg6HQ6vhZnO9zlumJM1JG/8Ru/wdHREf1+n9/5nd/xbd8OglHKBU+MRiP/PtL+g8HA9yu5DmkfgS+z2cxfm7S5QGEZQ48fP2Zra8vfC+n/AuZECSf3rV2nU17XVgzLfCIhSgJOlXKhGJPJxG+ITKdTD85kDpE2kzYWO7v0P1EGSzvIBsV4POZrX/sa3/3udwHY3Nxka2uL0WjkVeUS7CKKZqVcLUoZ01L7VezFMkdIv97Y2PDzF+D7glyHbKbJeJA6uaKckzaU/iE1UCVxXtS37XEhz9z28+P61+XfbaWf/Gk/u+V78tper8f9+/fZ2tpia2vLW85nsxn37t3j8PCQs7MzP1eJjXo+n/Pd736XnZ0dtre3efbsGb1e70/Yidvn1ob9g8HAQ/kkSfjKV77C6empVxG+9dZbXp2+sbFBURSMx2OvLt3e3mYymXBwcMDOzg43b968UiLjv+r4sQeIKKhM4ANTjNVN+IerU2gsHhrGqvYqlJVNGKi1tyenOiNr4k4jVZGqiqWNKU3og1Ni5RR3caO8E+WiJBWPghX3OqfoLcMXyx3udC7YjedkJmJlYka31/zhxSt8ENzh89+/T3knR3UqUJCfdVC3CpL3E3SpWN6vsaFl794Zhy826G2sefR8hzCpMC86xEtFNbBUXUO8kRGGNdkqZpYlpMOcYh6yWCU82DthNndUf9xfMVulEFqMURxNB9S15p1bLwD4YHaTRZnwzsYLbkYXDJsQGaCpfxi6cBm1ZqDXHFZjPs32CZThwfYxX55v84PTW8xWKePemjisHAw86KIqRX7e4XG6yTDN0Z2K1TLho4sbTBcdrIW4V1BXAcU0QRvQBg/hzFnCOopRaY3FYJu6V2uTogKDLTWrhbONBdry8HQLYxRPhxtEQc0ozlhXESfLHpNJjzCuUApMrTGlppcUFEFAvFcxv+jy8nSMXivqbiNBqRXlcYeLXoweF6w6AeFphDpUxBOwOiL6LCJo7Jy6vgRQrs6fszQ7S6yzNNcKoo01G701R+dDVsuE0XDFsl9DERB1SqazHtYqijzCdAxqEbCepRC5Ce6Lg11MrbCFJuoX5HlEMU1cXbDYEE80VRVS9x180IXCBJbOhcEqRf+gIigDwmUFsrAxxlmWC+sTgHVlqbTyIQnKwOrG5UI7WiqimcWEmmih2B/O+HhjgF4FlD0YnLlQEwFoLpXYvXfVaaBd0ACrEpKJnMslxCq7rkaf2FolmMSBP6GITgGHgnysCTIHE9WFcfXZDMRz40IdYnc90dJSV+48lHWq0arZCKtTp1wMcryF0cT4JFer3bmm5zV17Gy+4Rpvd8ZaX6dQAmiihbMPizJNNVBVwGPZUKE6bZycGsK1vkw6bkJawEHEcOUs1CaEfKSIVs52GhQuPTdaWHRxCT2DwrWlq83XqCOXriak1UDgbOZVirf/1rGi99Jy/Ms1naAg+qjrbKeB8tCpTrjyOU6Vhgdl0e6aYpZcKjiV9aEuQXapCkTB8hb0Po0v2yBQ3OxMCXBAyDaqQueudypCORIFpWntoikHDGU+U7W7N1pZyqYGY6ANVIr0FEwsQTaQnrnadhiIJ5ruwRobh1SdgGhZOat77e4zQYCylqobsd4KnfXaOvgY9Cru7J7z53a/4HeOXufwxU2XrhyAxvKPl/f4j4++zYvZkOU6oVhHMI8IltrVH9QO2IVLV06gjq3/bLl0Vx+02R0tXVhRcuHqGAaZA+oC4qMmOTlcG7CNLXkYuE2BtAntaSzJVd9Sx40l+TrvElVh084eENpLGGe1xf8uVCusMs34aVKUiyZpWQJ+TKMSjVo1cSrlgXa1VfE//jP/BX918D43A0uktCtZQujLk7TPJwkqBkpTW0OgNP9g9k1UoTG3MtRRgk1FbijAuYGIiuaanfqx8yKg6sDgUaPq/fYUk0XUZwlvf/0pb48O+A8/+xbZWQeSmv3oAqUdXNUlMA84zgf8u89/nc3u2td+/enxk3f8yxSHbfvt9ZpPbZuyLP7aakZZVLUtu9fr/7XtVrJwDYLAK36kzp3AkG63y61bt1iv10wmE69gk0WnWEk7nY6vwSWBHU+fPmW5XDIYDFgsFkwmE78YFTurQEOplSigTkIJRGk5m838YlmA5PHxsQ+8EOjQrh0JXFGJSFtlWcbJyQmj0ehK0rOk98p1TSYTnxJ9cXGBtZbj42O/0JWgElHvtANNZMEri9k2DF0ul/51ssgVFaOomobD4ZVUTAlxENWUgB6BhxKYkmWZrzXWfv8nT55QlqVXUD558sTXtmsnKm9ubqKU8vZiwKt8xCIskFLggNyHTqfjr3c8HgN4yCNWzXaIiwAFsYaKZVJsigINpf/K+RweHvp6XtfBrag6pU+I+qm9yJefk/snsDqKIrrdLkdHR76OmyhBBexIDTuBofI5WZZxdnbm71EQBB5GCfQR4CcKRgE18v8sy3xQjQAlgY1hGHpILPXxBGjKvCCqxnZZBIHkAlek7wiMEwUe4C2heZ6zXq/9Z7YVYe0arPI+Mh/JHCRKJanVKHZvUU/KXDSdTj3oFRWe/FtqbIpSV0BpEARX3lsAtNxjUXRKO7zxxhu89tpr/NZv/Rb379/3ilWZd9thLgLtptMpWmt2d3e9HXcymbBYLLyVvp3SXFUVjx494vDw0EN1uc8CFeW82vC6PUcLBJX3A7ziUuZiaSPpU20LrgDM9r1YrVa8/vrrHB8f+3Eo7yltJYEdOzs7pGnKy5cvWS6X7Ozs8Oqrr7K7u+s/8zvf+Y6vg7hYLPjmN79JnuecnZ0xmUx4+fKlt1dba68ko0v/k3sj/Vg2dAQM93o9by2XP3Jd8vySNux2ux5Etp+VomqU85TSAm11ocwp8ly4fkgfkedu+zXXayG27cPXz0UUud/61rd47bXXePHiBZ988gkHBwfM53N6vR6//uu/fkVBLH1dNolEkXhxcUEYhpydnfn+3n6+tfuRKMtF8T4cDnnzzTd5+vSp31y6ffs20+mU27dvc3h4yM7ODqenp0RRxNOnT316d6fT4caNG/53hH+V48cfIFqIg4rSOtA3DlbM6w4j7WT8opozVrO0IWlQenWhqBYLG1CjGOiMpY3JbMTSJB44Lm1M0YSJpKr0SsSSwCf6zusOqS7ZUXPuDM4p+pdW4EBZMhNR2oBvjp/Rfyvnu4M7JJ/0yfZLZ80KLOHLmCCH1b5Fr52Nd7LoOqukdguZ6rhDMtPYyNnUqj4U8xg9yun0XD2wulaY1GAmKTv3F3xkFHYRkg9DemnBynQxRpOtQnRgCZUh1DXvHd2imxS8WI/5ZlczMylDnVHagEigqSp9u9YotqM5X2a7xEHNqJPx/GgDmwWsTnoQGtQyvFTALDQXL0csxhlKu0X/ZNGlWMaodYDVlv7egiKsqeddr/5QubOX2cjCPPRrPRtYVMdga9d+tlboxLBYJ6znCRSaU6CqAp4DQVgTxxVhVFNljozZpij/xbzLjfGcKKgZpjmHH+6iasXeK2fURjN5f5uq58Jp9HEEsavZlW1Beuqg0fJuTby3Yn3UpXMQEK7cYj2ZKNS5qzu33lGEa0XZdzbG1bRDGBjipGQ17TCbd51yMKxJ0pL1OmaVxZiLmCBT1Jsl6SAnO+1wejrAZoFbgCeGcp6gMu2TfFUVOKWfKMrqBoSFEC5rbBIQn2ekB5fw0L3OYLqJW8yHDnx1TivC3KXOYiEoLXUSUAwaAZa/J846uygStm9NmXy4Rb5tsKEmnrjXyfsqg7coR0uncGuGqw8QqROuQEdf9085OBtPHIQpe5dAr46bem+lg3J16izvLijCAcMwc3Xe8lFjXZ5bkqmzbNqgAVrNtevKetWeCRtgJAolnB3YhNqBl6YNlGlUbgJAy0vlZb7hgGl61lg3g0ubs9ilg6J535gmSAbCGrJti1hyg7WzxFc9Z/02iXUhOmvFeqdRG1rQuQOCwdrd42yrsTTnluR5Y7GNID13SksTAY2NXJfunuUbDqj8wte/4HcPX6Nz0qhGawcnfR1M1cDzwMFUsc2ajZLbWxMenew7a3ftrssFazRtEENZBoTWzRfx1JLFl+BoFK5dPVSjPTSzFmqjqIxTnQOkSlHawNv2oWmHxjurmqRsNwc5kGiaenv52L1eV06JiHH1ClGuXwfrsjlvRbBqktryEpSi2uph4oDVjZjVvqgSHQj7937u/8Vf7Z2RqIg/e/CG+7kaCOGfP/wK//nF21Apb9/VhfJw1UTutdGiqcPZlEgwsVM3yqFrSE4bYJjbJuW8CW0pLdHaqQyVwasMi0FI2W3uRdiMlxjqrqVOrAsacY3nLL7X1YXtfzfPYywo3H3DgJKBovAwzqmNbQMGnfW5xYD9/O5UyK4WJhZsZLh554x/c+MD+rpDaWtyW2K4/MUx6NTUM+3gZWxIgwqtFIFyg2s7XHDvq4e8OB1Tdo2zV9fq6nVUTUp8A7ST04C6Y+k9dzVRq3cWlLMUmwX8hZ/9iIP1kL/7xz9HOAnRocWmtYeZNjSg3PP8VjrhQ7vHsx/ukV40mwLXXPA/PX58j/biXhYHbaVg254mkE4USG01ovxbFottG1P756+rFNpqvPaCVhQHYi8T+ADw8uVLbt26xb179zg/P+fw8NADoMFg4IHT4eGhD0wRQLG7u0sYhr6GWLfbZbVaeVWI1L1qgwABUHJObWut2C7FOt1u07a1qm1Zu15bTMCpLE5Fcdnr9ej3+76Oodaa8/NzxuOxbxupkSaf0QZAAmAFqEpwiahfpHYeXKoPRTEjiiixeLYXpwIg2so1sd5KcvLm5qYHqwA3btzwiilRism9FWVkr9djb2/PL7DleofD4ZUgCWnffr9Pt9tle3ub2WzmgWBbCbdcLj1kkXCVjY0ND+3qumYymfhz6vV6V95HLMFtdawouIxxabPf//73r4RGSP8HV1dsNptx69YtTk9PPeRr24+l7qfcBwGD9+7d4+joiJ2dHX/fpX9KX2nDSFFNtvuzwCVZ3LeBIlwu8tuJuwJZ2koygS3yM1IyQOzJAink/KWvSACQQMi2mrMdkCCvk/cXICz/F4jbtv8LZJa+1AaK8pr29behnlhn29ck6kw518Fg4AG/3B/ph221ZhtAtes/XldWx3HM/fv3efbsGb/wC7/A7/3e7/n2bgM4uZ9tKDkajdjd3eXly5ceKgvglvOVuqQyRmXDYzqdXoFdop5ub+y0IVO7zdvWeukDbRW1zGFyCKAVq7RYj+WeDAYDHj165Ptqe+NKgHlVVWxtbXF2dubDrl577TV2dnZ48eIFw+GQ/f19f27z+Zx33333iipT1Ipt+N0OQZE2ktf2+31Go5EPxxLVriic5VrF5i+qO9lIEKVl2/IsfUJq90o/kXZqA8Pr7dmGhW3Lf7ud5T7Ja9qbc9dVie2Nvhs3bnDnzh1++MMf8r3vfc+rJgUuitW80+n4GrYyVuSenZyccOPGDV/XdbVaMZ1O/4TLQP4ej8e+3be3t3nttdd49uwZFxcX3L1719/L0WjERx99xGw24+bNm1dU4TK3Sl/59NNP/Qbfn/b4sQeIykJlNIkuWdTu4eoSgxuZdVPUKGpgILg6hnCZqKwxGKvp6YKuWvGydr76pY2pUf71Ag9LXOpwoIxLZLaaVJfuaxi0MjzPd3mZj+mFObkJeS09IdEl2+GCRZrwN9644NP9G7z34T1XnH8SEs3dorB+JQNlMbMY09hQF0+HxBdBU4vJUm5V6E5F8DKlCi35yx5oy/5bL9ntL/isuIEtNF9Ot+Eipnt7znTWxcwj9FpT1wpiQ9Jb82S+wWydUpYBm70VldW8LDfo6pw0chbmEheiUtjAA9ShznhZbjAIMiqjKeqAjY0F58/HTV2zgKrXBDBsVOh5ABbql11sbDH9inISEAxLzCpA55rFQd+pP2JLNNWYyFJul4TdCnue+AWe1NcyReDscaFBaYupNXWtUcsQq62z+2YhYVpRrGJn4xb/LDgKZBRaW/pxzsmyT6AN9aBGdyqqOuD0+RidWGxqiIY5ZtH1YEAXinLYAKKNgiKLsIF10MEq1rdrrLIMPw2pOi5gJZop4gunLKpOY1bbESa2hJVC3yuweYKJNNk6BqvYGi45A8rnPTZ3Z5yfDhw0rCOIDTY23q6sSwdfUM5qWCeWIFNUTd1IF6agiBYVNgrQawc/sJbWGhybOLszOLVatNIN8GuCOWJN58TQPWqUe006q7LOkny+7PLW7iF/PBgTrDVV19J71qjf+qBzp3CsUwezikaRBw6kibJQNynPunQ2aYGOrhM4tVs20JR9vDW46kHVU6SnliBrIGTslHpWN4nHHUWYNTAuhmzb2aJtoHxASZBbkqXxwSjx3KKrxp4aNW3cJM/aoLFiB866ahooJtZcFwTCpfXUOCgnVllnhb5U4jVTGemZs4bXifvMftOGtrn2sn+ZHKzP3PnnYxCra9KoXosB6NQpOKsuHuyoxq4tikZdu+/592yUodHcMnkAPz96zPd/9w2GAkaLy+uS+6Rq6yzvMS7QI7Xcv33KIMpd+w/c+evK/YyA9jqBOK4J106tWnUVYmFGQVcXRAq0vgzDUQrisHZ1DJXlZnzBvFFA2vqyfqWJna15Unc9lLPGJePWRmMblXEyhbLnwmOihXI23dCCUfQPnFqv7sYOKpdNeEpeYoZdqm6IiTTrLd3UDm3GUmRYmph/+/AX+MOT+7x8tkXc1PvTFfCkQ1S78UrhQJNVToEd5O4eCTR2ac3u2nXhNiPiibMhO7t/80tz6b4WL2p0bjCRpuxriq3QKUMbW7KD6q68ggDDK3AQB1wRaB410E++37b92mY+DRvoCM56LD9vnMLXNknHVwJVNNjAzWN+jq+Un5N0BSaxdHZX/Ntf+cf0ddr8mMJVNW5+ucMSJyXrKMBWGmpFHFREBNTWveZ/OPqUdzpP+Fv536TuZSxWCeVF6s7ZNucbWIQ/hxchGEjPHezMv7l0m1bTiP/+r/0uXy53+OSjO0RTB0aroQuPWZnEPW+sugStOMWsjS5DdH56/OQdbcWJHLIwke9fX+DIwrK9iGnb92Sxef192xblH2XvFSWIQA2BO2maejBzdHREnuc+/GFrawtwEO3GjRterSXASuqGGWN8WIfYBKVWlaRNwqX9UuxWsugWm5lAP8DXgAOuwDZRZckCRxZXbauegAVJQhWVjNiWBeAITKjrmpOTE1//TKxl8rmz2czXFpT7IQosCc5o31+5H6KskwW3gNW2ikqAl4AcWSgLuOh2uywWrqxM29IssE/+LbBD+ofWmnv37nmgIYmjo9Hoil1Va+3ru7Xr7YVhyM7Ojv/cyWTirY+ilBRFpfQXCcmQcxRIIGnVAtWkD4laShRTos5cLBY+MVwWzm2I1A4XaNdFE4BzcHDg7dPWWg+oZ7MZ+/v7PHz4kLt373pAsrW1xcuXL4nj2MNXAW7L5fIKNJB+I2NK+gjgwSvgYapYxMGB1K2tLR/qIX1Uxre0GeDvpdhkBTbIuBZwJ/UTZbxIO8k9FTgjAEreS2oLikJN4N31hG+t9ZUkXoHyci8EZq/X6yshOlJbrx0w1IagURRRlqVvJwH0MjcIDBWF4vUaenLd3W6XBw8e8OjRI3q9Hl988cUVJbcAfYHUUgtW1Ie9Xs8HuLRDkdqwVz5PrJ7SJ9oAES5Ds+SPjGkJbBF4K/O9QOPT01O/mSLvJaCpvTkkgE3geJZl7OzsYK1lMpn4mqYCneq69qE2+/v7RFHE6empb1MZO7dv38YYw6effspkMkFrzcXFBavV6grsk7EoMFDaVvqLKN03NjbodDq+zQUaAldA+3K59JCxvUnWbj95FkgQl8xR7XH3oxSEP+p7Mm7bUFmesW043e777fF5ffNOruPu3bv8/M//PMvlko8++ojFYnEFVrbnCpk35XzkHk8mE7773e/yzjvvkKap34yTcdPuT3IO3W7Xl2K4f/8+k8mE6XTK5uamH7NSe1HGZztZvL0p155b2xtZf5rjxx4gAlQ2IMDV6ZPwlBqnSOnqnMxETYpygNYGQ0RX5d56nOpGZWgCZ09uVp0RNZl1AyRVJTOT+iAWg4OFS5Ogm0CW0gZEqubj7CZfrnbI65BhuCZSNZ+vd8lNyBezHU4WPVarhHoZES4CkjNnNwMou3Bze0I/znmSbLA66dE5DIgWeMthHTvbl6kjbOqUbG7FCQ9fbvPX3vqAh8dbVJMOh+dD4v0lZRlgz2OUxqlJOhW20qwXCdkqJkoqbm1OiXTN8/mYXnCTXxp9QWlD5iYiUhU9nft0aoPmrO7Q1QXGao5XA07OByRpiV5rTGwpm7pY8UyBDZ01VGlnq+1a7DqmHtTUi9AtOhu7FzUwqIheOPpSJIYqDwhXmnq3wBYaPQswqYVco3ON6UI0zikWMZTaBTAUCnucQr+imsXo3uXAiDslxTpCrQN0prBjOFr0sbYh8qch5Q6cHowI5k7JZypFOU9gs0LPQuygouwoOttL8vfHpB91KEcuzEAXTi0UXbhE4GzHNmEXlmLbkByFdI4bNdxaMXpcsdwNWc77aAVMYrLbgFGcRT3y8w5Rrjh/NkZVitH9Ces8Ip+mqExfWZCK5VfnLunZqfyUUzVVDozp3AVBqMq4OoLg0mTrBpKkYROM0Vga9WUQSTHwK2E3/ppgEXBAIrmA1Tyhd7Pg/utHPP/BTerEkm1r+i8Mddrs7GiFbtKPHXjE11hEOTuuifDBEFVXUac0tQYdWLPKhYG4f+NDN8q+A3mdsxoJBDEhRCtFvGjqTSlFmBmqjr4MZFlbqtR9bp0q6twBjTqFderaUGCOqp1yMlw5KGmVC6OIFs4iXXXwbVulzkrsgkWUB4ThSizulqrrgJG7F+5aVl0HGetOo9KqFcm5+1nT1DNMzx34kATleAbx3DahMtZbpW2oCNauRmLVbe6ZgnDq3ssEzXkum/buOHiIgt5Ly/1ffM4/O/kqwy9dewaFJWjUh6KwlLqTVRdfu7Ia1/wbN9/lt07eQueaaI63Y5sIquYpZAPHsZWBYA2rm/ayvTX0gwwDDvbhflZrQ21c6nIvKrgZXngLONYpPU3krKg9VfC43kaVrj37vYzbgwk76YLvHt1haRx8dX3IBdqsd5wFN1oo0tMCDMzvJsQLVwJAAbaTUGx2WO9GTB5osrsF6eO4UXa6Z8y/83v/dVTmavHGE+3PXxSqVrlNEV01myON7LZOLGbg7rvOm6TlWaMoza1XiCrramfq0hJmLvDFJIp8GFD2Q2fXjy7VplXXlb8wHXM17KTGWZBFaagbJWCjZPb1AbXANu2mAeXmKncfBR42yr7qMgDJJPYy6bj5HP/6a6BNzsl9vrM8/7vv/D1+vbMCnCW5amoZ11iMtRgUda2h0k61Dg1cVhgskQroq5SYmnURYYyiXMaXn6la5xTgAOTEbZB0Tg0nfyV3KtjDlNffec7T9SaFCQgWTvld9hvLs3KKV6UshMYNLuvOxcp1/vT4iTvkF305/mWqlPbipQ1J4HLhI/bP65aqNkgU2NF+r7bKQhYzspAVpZWc18bGhq/5V1UVFxcXHh6dn59zcXHBvXv3vPJMVG1ZlvHy5UtfS7HX63nVhdQ4BJjP59y/f5+Liwtu3rzJixcvWK1WrFYrD+ZEFQOXaql27Typw9dWfsn1C1hot2FZlmxsbHgrrSh3BE6JkkyUHgLh2qrNdojMYrFAa83e3h5lWfprlcWzhGFIGIeAYAlpEWDXTq+VxXAcxyyXS4qioNvtXgmdSJKE8XjMdDql3+97hV4Yhpyenvq+JjBALMxtFdnp6amvCbZcLn0tsqqqPHgQCCfg4vnz54zHY0ajkf/axcWFh9+ibFqv18xmM5/kKpBULJnS1+q69uExAhYEkErtxnZ4jRxtFZBAD+lvAnFE0dOup2et9SnH8j7Pnj1jf3/fL55v3rzJ+fk5d+/eJY5jhsMhw+HwStCCLPilr8m5txfyosRs12MU9aTUexTgMJlMGA6Hvr1kU0DgqijSBK5L/xboK0CwXRtV4KD01bYiUBSA8lly7tI35RraY6lty5fXS19qz0mieMuyjG636+eZ6zUk27Vbxboq5zYYDK5Amo2NDX/+AuAF5MqYELi/tbXF7u4uZVmyv7/PD37wAx/Q075/8h7t8+10Oty7d4+zszNfS1BUqm2o0m6bjY0Nbt68ycHBgQeM0mbtub5tQ5b7KsElAmakj4v1fzwe0+/3+cpXvsLOzo5vv4uLC66HyQi4ffHiBa+88oq3q969exetNQ8fPuTo6Iher+dtyzdv3uThw4f+ugAePXrkbdGnp6ce9AtMbQc3tYG91CGVtHc5HxnfRVF4cN7ekBDlqdjm2wo66TvS/6WMhsyLP+r4UfbjtmJQXtOuvSgAFLgC9URNe13tJz93vWRIHMd+Y+y1116j0+nwW7/1W5yenvpzar++/eyWr7VrkhZFwdOnT/n5n/95kiTxJSukj8h5ybjc2NhgPp/7Gr6dTocvvvjC98Hnz59z//59lsulDzyT56A8V9vKabkP7b75pz1+7AGiuKNqnApxWncYBWtSVWDUZSHrGheMIsrEFQmFDdgKFhirqa0msxFbeklP55zVfXo6d/DHRiytszUsTeLfMzNuktPKeOgIcDs+ZxSs0cqwqFOeZZs8XGwxjtf0ooJwYKj6S45mA/SOIft4zOhzZy2sO5bpOmWYZBij2XgvQNWWbMuFBtSpJb6zZGe4cEnOQc2L0zGdToGxiuXTIX9w+AphaChDSzmPncJurbH9GpXUmFUIy9ApPIYWswwJugVlHXA4HRAEhnG0orQBmXXw8KQaEoRTShswNx1SVdLTOQO95p8uvsHhxQCziMgqTVgot6zTzmapS5dqWieunqGyDpQU27Urcr/WRDNNOTIuNyG0qGXYAAkFkSXpFRSrEKYRDCoHe+YBptN0/FxTriOnchHLbqOwcipDMGVAEViSxE3kaa8gm0foQlEcd8mTlO7mip3elPMIVFLDRYzpGtSogGWIXgbOHh4b1DRCBZbFPCXUsL5Vk5y4haJJLMuxZfS5boIHGkBQQL7j4NJqz1InOCBkXKhG9lrurOyFQq0Clza96BE2acDJaUC+XbNYpk6dGhmHwSu3sFeFohrXEBr0IqRxZmJjZydX1hIvGvVUFHjAhcHBQ0BVNXUaNLX2HOwKMkOgXGCIrjVVqii77meDBgJasavGwDSitooHo2Me72yTPkm8lXf7/dyPXRNp1tvO5l67uZ0wsxQD5RNhg4xGUel+PigcHLTBJRAxAT71uEobFV8C602nnASpKah84rOJYN1v6iROjFdkSRm1Om7SkCtLhqbqNSq5mcVqdw+rjnLKxtDBQldT0NlB6+RSQRmuLmFieuaUmlLfMGqSkOtEETSqyKpzqSyM5q4+YdVzmwdV140hE4FNHYgVizc0Xw8bmNNA/CADVVlviQ5Xlmh+CfmUhTjjMlQFSCauTwaZZX5X8zdufMz/8b/419hoNiF05aCVKFCDwj0gy45TeLpzsWzfnvB6csg/Ml/zFnNdAzUEwlYC91oJetK1pdisSY9Cp0IMYSeckVlFXepGveZgTFldKnJWNuGj4gZ50xgm9t8iwPIrncf8N//a77EfT/jlzhdsByUfFRu8f3bTfW7pVH7g7lmxYbChpXOoCdYVaFjuK843NTfLHr3PzrFJyHo3cjU6C1CrgDp19Qp1pdCHob8XNsSr6oLMqQhlXNjQtYGDvU6lKnUMw5WDwVbZpq0dQIwXBlU3YUGBokoUZS/ENJbwOm5gesfNNSZxikrbtLOq1aWCsJmzfR3D5jwd0LxsR6usm1QqhU9X1mC7NSoyqHl4+TrtUqeplNvwSWr0JHJKR4GUQaN6rNTlv41LUFZG8cobBzz6aB+0JVKXv8wFSmOspaYmwAHCadXB1Np/vqqUt7YDlLYmUq5kyai75s/vfc7fef/nsLW6BKCSBg1EF866PP7Mcvxt6HZzVo+GbD4459HJJssy5htbLzGRpRIFJqBC48JvGiWmlN3wAPGqw+mnx0/Q0VYWtpWE7cVme7Ei6gZZbMDlwqNt22svJNo25vbr2iqL9t/r9dqr7pRSPrF2uVx6hYIsuKW4+unpqa+ZBnDr1i12d3c5ODjwCkRJBRW1npzrnTt3GI/HBEHA0dERg8HAQwUBXKImk/9fX2hKImS/3/8T8Equud2mgIdVN27cuKKYkppv19UWAmOHw6F/v7quvVoO8FZrAX4CyLIsu5L22b4noh5r10kT+Cn3oixLoihiNBp5O7moGyV9We6VWOEECAnQgku7tHxNFGUCSUVJJaBwuVx6VaBARGkr6acSrrOxscH5+TlhGLJarVgulz6Z9fDw0NvDd3Z2fLCKLLLbSbACjwReymuk/lyv12M6nV5ZWLdButh65/O5V6kJUJH3FAWpjB9R24rNUpSdT5484cGDByyXSyaTCbdu3bpSB1HusfQxCfSQf0vAjyzGz8/PPYiQ+yC1EwVMiHVdlJJih5X2blsi24pimU/a/5f2E6v9jwIpogKTPty2mrbnovb80lYmSf+RftdWcYnSuK38FIgrbSO1DgUkSYmAti1Zxmv78+T+tcs/SPvIz3S7XebzOb/5m7/JYrEgSRLef//9K5ZdGeNtW7iMk+3tbcbjMZ999plXWsv8IvNOe/6UviYK0iiKfPiIqAKvW0wFAIuaUiCgnJuMC601n332GZubm15t3O12PXiUZ4j0b9nMiKKIwWDARx99RJIk7O3tsV6v+epXv+rnTwHuh4eHHB4e+nMTKDuZTDxElGttK/XkXsjmymg08rVTBRRLyJNcS1sl3U4mVsrZrdtzlPQZURjKJovAs3Y7tZ+VbZV9+97JObTbsD3XS01UmWvDMKTT6VwJlGnbeuVoA8EoinzgCeBTrE9OTq6cW/v1MmbbCkt55rUVjXmes7W1Rbfb5ezszKsZ2+U6ZJwKPHzllVd47733rozh7e1tv/HRLjUg46z9O4ncg+tt+qc9fuwBIkCoagIMc5MS0KQuExOriszGjIOlCwAxET1dOChGxVBnTOqeA4U4a/PEdH29Q18/EQcYU10AV+stikKxbmzMhQ1c3USVcFy6Gi6hrrnVnbITz/liucM3Ri+4Ec34fLzLF/Mdjr5Ws5pvM3hqKPua7MMxX9RjkgvF7FWLvr3CWqemsLWiKgMm65QkdB1rb3PGX7n5Q07LPh+MbvL5u3e8siScBZQbFSYxBN2KutAQGaITd94mj6mHFatJh9Wkg45rXr15yofTfUobsBvPWdUxo3DNUq8Z6KxpZ0OgDM/KLZ6vxiRJhZ0E6CpAVwol6bkNyBFVnAHyB2uCqEY/69G9NWN+MHAL6SbNc/v2hNPjIWUvcNbDQc7WYMnLLMKuQiiVq81lXHonOHWMyQIfLqIKpxYzsSWYhJiugVWAnYasOjHErp4hob0sZq8s2Trm88UuyVJRv0yaQA2F6Wm/wCQ2qGVAuFboXKOOHUgONnPKLKX3QlP2cDbV8SVEMj1LsWlgVDpomwXEE0Uxthz8ckixXRHEtVOcNVbCcKkaOOdsjeHS1TusZrFTd25m1FGAmTSkREF8EmDiAJNYgtyBKBr7stWKeFZhQ92ofZT7IxOHtc6O2dHoyqJzB+TKvnZKvUY1FS8M6cSFZ1SJs1K6RFVILww2CJi906EKA77+6gs+nN4nOdHEs5r4bN18vgJrSU8vP98G2v2JNSbUlIPG9h4rVjuBa4vGclt1LmsHxkt37Va52ohOyeVqHUrwitWuP1SpwoT2sgZfAFUv8CBG7NhB7uysyjqVYzx3tR+rRKGMJVq4n9WFg2f5WDfWUKdkNIFLIbaNwtLbcVEeapqoUYuWTdJx6r4Xza0bL40VOjl3ysE6dediIpcmbcPGrrqyFEPllZGSYF02DhR3no3yUbmaiWJPlrwRLXZqi6/jqEvoHhuKv3rOR4ub9B8G7pyU+7nOaYmyEUXfqVStdu/tAmEsVc/yCzeeEKmKsKnj6gJumsVtEyITLZoADau8Ki8cF9iT0J2LdSrw2ips2YRsBA7ofH33Ja91T/ntwzfITMT96NSHqogdnSZoZWUD/pMv3iFbxfyfBr8CuLqLUVSDcX1HGVdzsuxbrwwcPK/RWYXpNBsbBaw3A3rWYuOQ7lFBmIWsboQEK6dGc/MShKWDzShXL1KXbmPDRK6NsIoqacBprojWTbL52qmHTeTaTFWQzoyzK1eurxQD7UB+A6Ml5Kjsu9qNJjbYxoYtkNBBSPmZy3/T2MKdxI5LRaK6/JpYpKncRo3t1sSDgjQpWWcR5Som2Cy4uT3h/vCM3/3hG+7nIuM2lGKD3l9RnXUcvAxcTUQbG4haUFIOA0ezgQPW3YqI2tcyBGdhNhhqLHNrWNeRY3ONFdlqyyDM/WvBQcRNnfNvvfrbfCt5wX8YfJu6qYVLY5f3z5TYMniimN/RDF87Z/pkRHhzxXTeoTpJyfsZpQkwXYOumo7aOlQDSGWzYBSsPVT86fGTebSVBnC1vtKPqsd0HQxeV8AJ1JL3vr64aVurri8EZFG1Wq2uJCZLImkb4l1cXHgb487ODn/5L/9lfv/3f5/T01M2NzfZ3Nzk4OCALMvY39/34SMSXCCw8eDgwIMnrTU3b95kuVx68AgwmUx8fTRRpYid+XpYSdv221aryCGWZllYi512MBh4laNYr8WK264VJ+EiAAcHBz4wRKzHErQiNcHaKkuBIm1QJYt+sXMKHJFzb6tCRVEoih5ZSEtghgDUMAx90MNyuWQ8HnvoK2oosQGfn5976Ch1sAQqyAJfLMSiIBMLrSziHz9+zO7uLtPp1IeHSEKqtKMoLwUOCogWuNYO9RCVIeDVRe0AFQHQ7QVte7x0Oh2Wy6W31baBuJxHVbkU1rb9XYIYRNW2vb3NH/3RH3Hz5k2++tWvegXXxx9/7MODBEQJkJTz63a7bG5u+usVkCJANs9zD9HEzivtLMC2DeOkrQTEiDJJXiNgXlSIAqPbKcoy/gEPPwXWCXhtH0qpK9AHLqGl2H3bELWtZhYFs1z3YrHw40cUx6JGFHWk9FF577ZlVMZBW60mc4Ccm2wiyNdE6fbqq68yGo347LPPPCiTeyfwVuBdkiRe7RcEAbdv32Y2m135XKnfKpsqUuOvbR3d3Ny8EhLTHtNlWfLn/tyfY3Nz09udp9Mpn332mQ8OaY8HATrGGPb29njnnXc4OTnh8ePHnJ2d8fLlS87Oznj11Vd9P9TapbhPJhO2t7epa5fGPh6Pr7yvqIMPDw/Z2Ni4UitQNi7Oz8/9c6UNuaQfAL70xfb2todTeZ4zmUz85ocoZyX9uK3kFeAlY0BKaKzXax/80lb+tTcN/mXwvN1n20BMYFgbWsrcur+/z9e+9jVu3LjBdDrl3Xff9f1GwKyot1er1RVY3QaC7bEGeIgsz9D2a9obAu3fB9oQsH0YY3jy5AlpmrK/v+/Bslxf+9k+GAwYj8fcvXuXP/7jP74yRyVJwmAw8Js57d8V2htcbfVxG9b/qx4//gBRwaqKKW2AsU6FCJCbiDioiBvVYW0VQfOL+zhYesWhsx1X1FZT2qRJaY4Y65Wvd1jYwNudY+Vszca6kBFjNZGqCZQhMxGFDchsTKpKIlXz/uwWe+mMnXhOjeZXNr5gZWIeZ1tMyg53ehf86vbn/F2+SXW8hS4hPXRQYb3tVCrFNOGdN5/QDQt+eLJHlkUsJl1WkeFrt18S6ppEl5Q24E5vwvgX1nznk1ehDAgyhVkE1D2DOYshcgnAQdbsRowMKtdYYyGtURpOFz3G3TX9ICfRJc+yDc7KHtvhnKzpZJO6CzVMqy6v9485X3fJ1Mhb7UzPLXxVU/dKFU5NmG9azDJCDS31sGJ+OHDqsq4lnAfUeznrInIL2aYmVX7Y5YVYk2MDmUbJAt3gJVTBQlN3G7VdETiVTvO6MlQEe2vKLERPI1g3v9x3jFOG5Io61JiFJpw2RMVCMXI2weAobhbzoGehrx1XJ67OnFWgnnQIM0WVup9DW/RMkW1b6kFNdBE4pc7a1Xu0EeR90yzkXQ3DehpjNg3BXLsFbGPf1c11mKSxbRcaVSrKsuPUPJFxPk+cijVaaEyJBy3B2qUjuxTgln1Za1Rdo8omEKKqMXHoE4d15WCEwCFRt9WpU0UVI+VgKZBcOAtvnSiiueVk3WO/V2OsIsgUG58buk9nYIyzl4dSoA6Uac5F16AUNtMEQHzqAlgw0H+isVHg1HW1pU4DykHY1GR0sK7sXyboBpmiGLoxlMwN4UVTvzECZSzKOJhoddNGBVRYV8MwUL6Om9iRsQ5WmqbmoS6dLdkEUAydqk9LvboQ9IUD4nWkCHKLDfFJx6pyn1cM8Gm3dXyZMs1QeeAoijPVJHsXI6fqDXIIlqKGdGBRz5rraNSaycw276NcQnCiqDuuj8ZTB6aK4WWtQGUcNEWDCVzfXu4H/PXbH/F//94vMs6ba2uEH8v9iNl9F9zRPTJUqaLu0CQCQ3prwS8Ov3Bzbd3YWRRE85ZCUrv3NJGlqrQHmGFUX1FqxapmL4D/6Z/5/3L0rSF/ZfQuMTWPy20mdZfaKgbBmoEuXEBWE0KCxc17WP4/yzcJ/2BItGVZ37dsjJZsdNdM1ylEzkZejCyjz2D+iruX0UyTnqwgUJSDiBvfzahjTTSTkAzXeDq3JBP3UNJ1Y8mvXD3FQIMtlL/Wum+becfZkcNz5dKya+vbSNcOggczB8KxbjNjvRX4VG9lnC2+6kIxdJshdWodsGvgJwZXM7Vdm7ABhgp1FRg237ORadqvCa+SmpnN5o3q1NAKPgmDmm/cOuEXNh7xl/sf8kYU8HFh+L1PX8dWTZ1aZamXIXpYYyNDemPN+rR7aaFu5lys8qpHVSuyL4fY7YJv33/KzyQToOdfbrDUTSfRwLJKsLXy10tiKK2mtpak+eUvALYCy2vRCb+7fhX/e5O+VGTKoQtX+3TxF5asjgYQW4LAkp0nhJkmrwIqq69eQ2BR2hKIhLNlWS5tQFkGf0LV+dPjJ+e4/ot4u05WG/C1F6Ltf7cXKQKk2l//UQuc63BS1G5tS5QsItsLSnnter0mTVMWi4UvhP8bv/Eb3Lx509d2evjwIdvb215x0bb3np+7uhqj0cjXE5Sk4NlsxptvvunreLWtcnLeoswT8CAwBrhSR7ENWGWBL2AgTVM6nY5/nxs3brBYLJjNZv6zBGS0LdOdTofJZAJchjpct8GJglHgVxAE9Ho9Tk9Pr9QvlPssCzWBm6LCkwVzG0hI3TGBN+3FHbiF8IsXL3xdQK010+nUA8J2UrBcm6h62gX7BYqWZenVVKJykXOWeoqz2YyLiwuvGhL1ncBdgUzgoIQkswpYEKAhwKKuXbhKGxKsVitms5lXsYm6q20/FTAtdRHbqij5vtwzUcOKolLGgKSbHhwcMBwOvX1PbJ9ffvml75MCzbIs89BP/oiNW86h0+nQ7/c9SJH+1w7rADxMFDgrY1AUl23oB3iFbRsKiXJP5oR2vU/5mijsBIKI/bWdet1Oh5b+f72mnFLK1yCUcSr9UcJZ2vU/BcLIOcvPSX+X/iNKtnY4ifQfKQsg1ygqWmmD9obC7u4u3/rWt/j+97/P3bt3+Xt/7+95sLlarbh79y47OzvMZjM+//xz3x+ltMHW1haffvqph+DyPcBfr4B1+WwpXSDArA1Wnz9/zpMnT3j99dd9wNTLly99mwooFlWizN8yn7z11lu89957fPrpp77EQxtQSr3H2WzGeDzm4OCABw8ecHx87MsUfPrpp4xGI7/BIPOiBK4IvGtvNsn4kbYTxXjbdlxVlQd9cj6irG4HHck9lTlCNl4EPC8WC05OTjxol74nz0E52pD5OiyUr7X7s4BYAd69Xo9er8dwOGRjY8OHfE0mE773ve/x+uuvs7e3x+eff+4/czgcevB4584dP9+2Fahyv7Ms4+joiMViwd27dxkOh7z//vt+rLefxXKPpf/L9+Xc67r2c5bUXpX+cnZ2duU+yfe01mxvb3tLvNba329rrZ//2jU12xuW7X7bvj7pC9fvx3/V8eMPEC3EQU2N9rUIAWoUmYnQyoWkxMq9JrMRGIhURdakNmQ2IjMxgTLMTYcaxYtqw4NGqXvoFIodn+A8Nykaw2E1Yi+cAvA7k68S6pp76TldXfD24IDn2QYfT/fYSRcMgoz3Z7fQyjIrUmJdc5L1yYqItLGPlX2ngFm9VoBRhJOQ9768w/07J06JaDRxt6CuAt5/dJvOIOOrgyNG4ZoAw8P5FkGnInweU3WtA3m18n+CTPl6asHaQSpjFdZCnQeswpokqjgpBqxMzLPlBlpZyn5AbVztx0f5Do+WW8S6JtEVsyyhGtSo2ikQdXGpshIQpVoKFnucOiVf5VR80cwp8dRFzLLSzr7bKEjCpcbkkYNXg0tyLymlVjuw5j5TYevG+ix2shpsYOj3MhaklFJjrFaEk4C6a7AdZ7NTnRo1CShGxoG7xs4aZK4GV5A1bRVYqn6NzjRWNUo2DSZs2rVwCiBJ4g2Wri5kMNcgChrj1Ei6wKfq6llAsVNjYutUpEYTlI21tgms0IVGZw1sChUUgHVQOMjca33waQN6FE0Ntab+oQ01qjQNPGgtfgETSo3Cpg5ec8+6RxXr7bBRLUGYGfLNwIdcFCPlbZPhClZ5TDyo+eTkBuNPoP+0iYjXGvICvbpqP/Jn0agirVYQBCitwFjQChuH/jXBUhGfOfiIckDShrpRFuoGgmlMrMFaUIo6Di7rHsYQ5q5unEDIcNVAwbBRBlbWhUz0lIeMoqytUjBBU2cvxqsjBYBJ4nQ8t409VREuL5VlVmzXxrVZUDhQU3UdhLPKJf8KkDPppUrQBcXggE5j/wXH3KO58nbmZNLYtWOoY+3SeOdOPVnHrtE7J/bKOYcr93e+oUjPLCe/WrCoEnqfXtaK06X7vPW2JturqaeaznEDAgN3Ija0/PKdR2wFC2oUhQkgcHZamRt0rhoFJxSbCtWAsypwyfOm6RjSnzNr+CdHb/PlD2/xn+6+Q5y4+eDuxgVp6DaCtJCZSvn+r6Oarqox1pUUqLqWIKgJtCGrQmqjUWtNPHGKQGWdetK1DwR5jYkCZ/+e5URljaotqqpRZY1WChVruicGXblk7zqBut9A98C2oLwiPXWQPcibtlL4+p9B4dKSbaAaeK3IR9rPdXUsCcxQjqBOjFdvi4JQiU22mXOviN40bsMowMGuLHBWY1EXSiqxBrVRuNT60w4E7jqIDGk/560bh/zM6Dnf7D7m9eiM20FEV8fUNiZQmpoCFRqsdRsn1Iq/8u33+O3HD6jWAX/h3uf848XXsOvgUnWonALRNsBSjjCp+V/d+Qds6NSHoQRKk9urBZ8XZYIpA1RTtxEFSUO7381zHldbfJ7f4HfPXudwMWC2TDHziGCp3XMgsF4lK8rX9bZGa0s4Cek8mDA/6aMqV/pgkEo5BntpfbYKaxSTuuuAeKkuN8SaS7RXp9yfHj9BR1tV2D6uqwLbwQJtMAH4BbO8X9vm3A4WaVs2BSRJbTmpaygqJFGQyEJD/pbFqsAkUfCdnp5eCfkYjUbeIij1EEVhIosUWeRub2+zs7PD48ePAafsE+WFLJytdbXqgiBgPp+zXq/9NYrqRur+iapF1EyyKE+SxP8ZDofeapymKavVyiut5L3addHatQpFkRQEgQ9Oaaui0jRlOp16hdTx8bG/Z3K/r9djbBf8by/aZBG8ubnJy5cvrwCcNE39YlFgk4AReY0s+CX4oV1rr9PpeMWbKFAEggwGA/86KcBvreX8/PzKwrINL8W+LaomAQti67yeLN2uxyhQSfp+W3km4HM2mzEcDlmtVh70yBiR/t+GGVJX0lrr2280Gl1JFW2PGVHAGWM4ODjgzp07pGnKrVu3yLKMFy9ecHx87FV+bbts294r/Ubgu0C0dv3OtlKwrYRL0/SK9V1Ai9TNbKuw2rDi+vu2x77W+opCUSyhojpsj/U28Gr/LdBM2lzgktSzlPMU+7fASpl35P7IWBSYJONCIKPAK1H/yjwgY6GtxmtvErThoyjrAHZ2dhiNRjx9+pTDw0O/eSEbIVJjVZSkAoQFEBljePbs2ZUAofa9ljEm1yL3vW2VbR+j0YibN2+ilGI+n/u5RNSK0s7STu3agDLnTadT/3pRFMv4EaWzKOREEf306VM/rx8cHPDw4UOvvBQVYV3XHB8f+6/Ls0CeDVL/U/oo4OfNtrVV1IoCutoKaZmP5XkgmwEnJycsFgsPguW6f9T4bivy23X52s9HwCfFAywWiyv1aV9//XW2t7f9/Hd6esoPfvADfx5FUfDKK69ceQZsbW35MCgBm6KUlnEnfUvOS8bgG2+84eHtdUAoY6QNVtuQMc9z7ty5wxtvvMF4PPbQbzKZ8OLFLnN/bQABAABJREFUC05PTz2ova5WBLepNpvN/Dwg7SwwVZ7rbUgozyM5V2lj+Vob6v9pjx97gKisszADXoWolUtVrpWmNA1IoGIzWDCpu2REDTSMCJQhMnUDHBMfwAKXac3yt1iaU1UyNx0+Xt/k2WqDh9MtHmwcsxMv6AQlH09vsK4jxtGadR1TWc1WuqSymt87fY07vQkvViPA1U/c68wptkKedUcuLCWGbBvQkI4yim7IsJ/x9HATkwdEvZIyDzFFQNipWD8f8HeWP8fPvvKUp7MNAm2IohoTWsKFq41FIYmeCpNYVN4ooaxbxKsSqoFBx64t74/OebzY5EZnzo3OnIPVkD+YvsZr3VMA3ru4xbxIWOUxi2VKfZaga+XDU+KpcnWlGguhBAaY1KLSGjUPiOcBddf67zuoCfFh5BOEFY2NN27UPKGztNrILZiNdnWuVOmuywbW1XsMpN5bY8Xccr+M1ZVGrQMXOtKpYBYQZJpagRoXqMPUQUDDZX242i2cg8zBmrJboypF50XoX6cqSC4UxdA6++VCEc0DFxajYetdp2KbvYqHMEGuqBsYFy2Ut8PqgxCrIe/VSE3HaqckOmtqBc1du7q0ZdcOen2paFHGDYw6uVSKhSungouWlmCRYzoRylqsaSzMYQBlhe0klKMYZXD2yJEhWDuIEc8gndSUXd2o5hTdQ4NqLMa6dNciNfHOFh1W45jN3op5d0QxiknK2iU/xxE2aRWoEwVkWeElQUEIdY3K68YLCGrRRMxrhY2bxDhRC0Shf52Nmh3sBoaqvKba6LDYd6/xKcmpApSvqegCPJzqtOyBiTXhytI5FaXoJVSuEwGAtkmRVtj4UgEoNRpz7YCypEyb6PIXEGdfvqqGDHLrglGUsykHJb4endzPquPaXFkHnbLNBip1HfwMM8i2HEHRtetXNnBwMt9sau4FlyAjzC5Viy4N2lm2rYa/9PZH/JNHb9GZuHOEyzEdriyb7zbAOWjVPgzB7uX86xs/BGCsV5wvuwRzTeeoCalJwCRQppaieRKVZYCuLFYpjNHu/Bq4GamKZ3XCo3dvsf2B4vTbKWyvGfUzuqEb37GqyW3ApOw6FZqoOBtmvzKx40MG6irAWEVVa5bTFKVgta+Ipw6emsgQZJrhkwqMoe7GBIWrOYjWUNdOTVs7uJ2PQ/KRA6KrG41a2DgVtBt/zpoe5safV1A2dvnCbVjUiaLsaVY7oZ8nxJpc9mlCmoxXBWIa4Ca1Axto5lORG5holXVALnLKaEp9Ccoi42Gdm0udRTy9M6eqApKkZJ4kPvxksLXk//rO/403I0hU6GCh7Tg1oL38BSRSxp+DzFN/cHDffXOj4B998HXUKoDE4ENYtHV/hxbbWPHtTsn+5ox7YextyAYL1hCpAG1dOEquMr+J6GFoofl0ssu/sfwbvJiOWJx3Ideu1mJkoVQES908J1rqQyMKSLcxUFea/hsX3BjM+fSkT7DQmBj6UcG6jpw6s3bPIDd11f73CELr72VuQrRY+X96/P/FIYtiOdowUL5/XVUor2tDRAEFnU7HL+Jk8dvv9/3CR5JFpa7WZDLxycliEx0MBh4KidUP8IsepRTT6fSKui5NU46OjphOp75mYJ7nXvEksFIWxPfu3WN7e5uXL1/6xZosWmQhKoCwrTgR++Xm5qavdyZ190SFJ2moUodRFnUHBweMx2M2Nzc9/BDgJovY62ER1lqvrBNIeX2hJQnD8jkC52SRJxbgdl276/a79v2SNFWpiycLVFHHtesWCkAE/oQyR0CdhLOUZclsNvMqSQG+4JK2xXK6tbXF/v4+AF9++SVnZ2d+8S+wRM5drL1tK2W/3/cAYT6f+4WuWG9l4d9W0bUXqW0L9927d3n+/Llvo/YiXIC2BLLs7Ox40B3H8ZWacKL8Oz8/Z2Njw1uNpS3Fki7tub297YMzBLK1IXMbdrTvo0CeNgxuQ7D1es1yubyiXpSj3RZpmrKxscFsNmOxWPgajKJUE3itlPLQRMZmW2koc4PUCpW+K7X85BrEaluWpe/vAqkFRgrskn7XBmpyD+Tz5Dxl7pDNAJm3xDosfVWSY4Er40N+VqCh9CGZzwTsiEJVQkEePHjAP/yH/9CHA8n9Oz4+9u8rFmZR8e3v73NwcMB8Pmd/f9/DTYGMMp4FwEhbtmFrG3QB3Lt3jyRJ+Nt/+2/T7/d59OiRP2eBZzKur9fCE2gocEf6lJyXqBGtdTUIDw4O2N/f98paee1qtfIBMdIOkjI9n8+pqsrXVex0OozHY3Z3d/05rNdrP9/LvCZQWJS27Q2Abrfry0VIX5SgretJ1e2xfP3f8myUz5Q2kLlDxrlS6gqgbIPwOI752te+RpIkvPfeexwfHzOdTq8khrc32wRaS98dj8ecnp76pPp2+nr75+GyRq1sqnz55ZfMZrM/cX1yvwUCy7NFFJknJye+Tu/Z2RlffvmlD7wS9WDbUdDuMzKPvXjxgp2dHX+tgN84as83cs1SHkH6cnvzq32v/lWOH3uAaBVktbMUl41sKKsTIl1hWsEmTjHYYWUSBkHWfM09WJcmJlCudmIvyDmr+j5w5WW5wVE5ZDNccjO6YKDXvL++y/uzWwzCnJfLEdNlh+/M77M5XLLVWXGjM2cnXtAPcyJVuzqCJqY0Ae+WtznJ+vSjnC/Ot1nnMR+zR76MGeBgwLqvyHcqKDTZeYruVmhl+cXXHvHZxQ4nz8cMbiwYpjlJWPFMbVCdpnzw8iZaW+paUR90CQKaEAqFLhWmUVeIfVkWS0GhyLdq+hsrVsuEMKw5XA6ZZQkayzRPiYOaD5d7zIYd4qBiknUwVpEVEfZlSjrVrq5XE/ygjIMDJrQE1gGUqgPJSYA9a8Bs4lSEulH6JWeaqmebIAdnC9YNfNd1s7ica2xoqbumWXA6mKgqpyjUldSwU00yrFMhrecRF+uBS+e0EM4CTOYAhS4dRDXT2NkGm/elwCvFTNyEHuSQHgRNaIb7ui7dtYIDgSZ06sNw5RaVnSPoHeSYSJNvRr7mnI1ANecIEDbwWFJVo/PQBXJoSzALvI1ZzsmrWBrVVrBWhGtJK25q/+kmQKNZ18dLA2WF0s4OrKQIeaPQs6FuoJqztta9Gl0EjYpKUaUuRbjsu3ZVFZc1+bY0poFw4cpiLmLMbcV2Z8HRLcvGF6DXJSorsNMZajhwSsEodAATsOGlCsNZl0NXH7ESEhqgWmEvV466cK/TClWUkMQUtzdBKXTkXhvm1luBy76DfuHKogtLOXR9ruo6VWHd3NOq465Xl3joHq6sTyo2oSZcW8K19T8TF+51YlfWtVP8mdD1rXBpfRJy1XWwom7s6nVHUfXUZQ3G2r1XkEHZ9I8gs0QLSzlw9yPIXbiLC5yxjRLSwaxo6ayxJmhUcU0dRbnHUrsyXDkoWHUg31QMv7ScfQP2khm8N3T9VjdKWOv6Q1BYD0HzUZO+3NT2s0ZR2pCxXqGVYblKILQU48ZWa1z/DwvXl1Z9Q3PaLoSm0gR1o6KNYKAzAix1x1ClAVbbBgI6VfAsTymsC8hIdHWp+LKgtQsBKm3ggWQQGKKgpqo1UaeknkRES0gm1oF+q4iminBZY6PA2ZbnJVZgdllhw8Db6l0NSkWYWTrHTbs0tTh15ZSFLo3Y/V9gX9nVlL2AOnI1LU3o4HCdumTtqudCm2zs1M00Gxo07F/U5D4xuZnbfZgUDUT0acfN1yrtag42alFVNtbh5j2DwPDm7hEfH99w8LBJ/uklBXfCkq6+tBK7QJNr4/H6oeDi+Qg9KNGBxZ6G7ppyjU1doJZXQzc1FhlW9Mcr/tL+J4Rczg0CEjWajILnVcn3s9scrQYobd0lGiA1PP9i19d6DNbKg0KrwcaWuteye4sCsQZlNFiXPq8DQy8p6IYFOq2wQQCVIgkrBmHuFIhBA3WVJQwNW8GCMDTkokzUMArX1LV2avnm/v/0+Mk62ouk9v+vB0S0rUXto61Uaysg5L3adiepmbexseHVLpIqKmDJGHMlJEFUJ7KAEDWd1MuS1wiwA3wacFVVHh7JQltsdYC3gFZVxbvvvkuSJOzs7LCxscHJyQnz+dyHBchCqa3YExgUBAGj0circQSUiTWvvQAU+CNBFwIwZ7OZt9QJvBOVjbRtlmX++tu1rETFIwuuLMv8zwmkEdDYrvfVVsGJOkdAhPxbIJBYjCW9WUBDu5afWNSkT4iCS85T6kiKek+uQeyt8jppLwEYojIVu6HUNWvDVsCfl4BngYOicJLzaod1iCqmXYsQnJJK2l1+vg1itre3OT8/9+0hUCGOY/I89/3t/Pz8ytgSO7ksngWkSkiLqNCWyyVZltHr9Xjy5AlvvfUW3/zmN/ne977nFUoC0QSWycJb7nt7PLbhwnUw3e5j8h6dTse3u0D8NrSUeyaWWvm56+Ej0hZSN1TGYts2LXNNWx0rUEO+Jv2zXevv+rwkSkbAj3s5Z+mH7VICbdgpsErgrYxNgZfSf0R1K0BaxpcoPeXapf3eeOMNDg4OODs748mTJ15pKudWFAXPnj0jCAI2NzevWLAFPiulPHAViHy9/aStRFUrIFHaUsCknLsAv7aiS+B0W+Uo7y99rf066TdtRa8kd4tyTeZbmXekj7RVqNImcpRlSafT8TVZpd8I0GoD43ZatEBhgWHteolZlnF2duZTl6XPtMHXdRuuQOP2M6+t7pPx1f55ObednR3efPNNyrLk+PiYjz/+2M9FW1tbfPzxx3zwwQd/4nnaPo/2GJbNEem3URRxdnbmny/tPiVj0RjD/fv32d3dJcsyHyQlbSRzzmg0Yn9/n93dXRaLhe+DSikWiwVffPGFrx0q850copaVMdlWDrfnUwnmkTaUeyPzZvta2yrP63Oa/PxwOPRws30+/2XHjz1ARLnFYy3KhAYapg0cTHRJbqTWYc0gyEhVyaTuUtoArSyxqogpqVEsTcIgcDsqSxMzr1MOshEvGHOa9HnQOfQf/fl0h9oqbozmZFXIyfmQahRAH251JhzmLkTltz5/k7rWbA6WRNpQBjU/fH6TOCl5ffeEvAr57NztBppQUWwYlFHEJwHl0Nn4JmWf550xtwcTLgY91quENKoo6oAqb3aJLtyOU3IUQmSpO5ZQOTsxyqnQJAXU1XBz8M6EEF9olqOUMKrJs5hnq5jhcE0nLFHK8tnTPVRo6MUlWRWyka55er5BvowhurwXqnKLf9P8WxsH9IqhdbZi4wCDr+2WN4O5qVUW5C6tGfC24LYKycRuER7OA5fautZOUVk71Z+NXF1CiqZWW9nAl6XGxAqMgKAmgEWUOcpBRJd+6l4j52Mip9gUS7MNQYQ2tqVUrBNLaFWjSlOXIQkoZvdjp17sCURofhGplFcj2ZDLxX3pRE5YV4crbNpI1Q2cae6fqlx9w3ChiFbufXwYAg4o1KkiOWvA16KmvDHk7Gsdtj7MCOe5A3TWgjHYKMA0acE2gP4XIVXfQaUgq1FWO4tpGrj7E0PWc9dadS9VUHWqCGcaYxVaW771a5/wh4MHvPL3BiSHc1TZxQYaM+iirIXKOJgpcLCs3FsFGlrWLrR2kFFfbg74o6wg0KjaYKYz9O62szQHiulrKdHSvXfZc+ArmViUMRR9jTYuHdk6QSK6dICvSt21BZl1tvSeA26SEm0i165Zz5UdqBN8knG0dDehGrh77GoSGurIqQWD4jKIRRn3s5K+bJWzUjt1pMBH/GfWqXvPciB9wd37znET3BK0QH4MRersy/HcEq2cZduErm6hMm7esRoPCbNNRbw03PmZQ3774A3SE+tqPyp33u2+qit33+tOkz6dupqfWlvmdcooycmbudZWTYBIAnVgPWQNMnc9xjRzUkQzNi/ngABXx1DGKzilF4BWlrxy8+DSxoS6dpsQzXnqwBCBK2shAK45KqMJAkulm/unGphrID236KKmTkMHIiuDjbSroVmUmH636a+G/vOKuqMxofLp3W7MWqJF3ahMA6qeJh8F3iZvG05edRXFAKqBpeoYF76hLVKjUOChVxri/lZGefWeqpSvf2gjLoNKpKbgKnBtGjrgpQIuP6Ny7WIji+6V/Ec/8x/Q04a/Of8fsKLnQF9kGcY50TXyVTdqwCvD0WpMHjTn4E727beecb93zj/63juwUUIe+CAsqX8INOUpFFs7M/63b/4/eStaAs5+k9uKR1XN3MR8L7vPu/O7vHd6k9PTATZrzkG5DRG1DIhmbqOo7tgmhdo9X70q0162k7N9K9qJzMoobHNeWR2hdDO+KtBYB6tFwRhY2v5kpdzzRJVO3TmvU8b9NTd//pB333+V4WdX2+ynx4//IYvFtnKhDRXbNQ8Bv/CTBXv7F/+2mgHwi2ix6d64cYP5fO5Bl0AUCQJZrVZekQdwfn7ui+PLIXUN20EYaZqyXC6ZTqdeSdRWRUgCsdiRRSUm179YLPziSGpkSf0rWTi326YN3mSRnuc5/X7fK+rk2oMg8ApIY4y3Fnc6Hfb29gA8hFssFr7tZLEsCylZ2AugE0ghqhyxnQlkaYOBNjyRcxcgIyo+eU37ugQYiPJIgmba0ECSQSWsRBbYAogFqMiCtl1fUFSCYm8WECqwQM5ntVrxxhtvcHFx4cGwfH77etpKnHYdPoEM8n5t1aaoouq69io5gcICX8QiKzbOd955h93dXR4+fMh3vvMd308EXMj1LRYL9vb2OD4+BvD1DmWhLW0sATbT6dTXN9zd3fUW+y+//JLvf//7/Oqv/ipZlvHDH/7QQ8e2+kyAh4BBAUkyLq8ri9u23Pb4l3u+tbXFbDbzSsh2SEK7XQWiic1UgJpY9a+rneTzpT1lA6Gt6BJI0p5bBDK3IZwot67fA7n3bcWlnIfUDRQlrChDBczJmJa+IHC+XUdR+rioRmWMyrmlacqdO3e8GveDDz7wqsH2XClt2S7dkCQJt2/fvqKua0Nw2TRpA8I2/JXxLO8Hl8pUeY/rzwCB+vLz8po2zK3r2sOe9nNCNgWiKGJjY4PVasVisaDf7xPHse//AhVl7Ev7ylwrc5jMw+05QzaDJDBIvi7zXZIkHgaXZekBvNifpV+14db1Pt+GidI/f5Rdvf1e7fEDeDXqgwcP6Ha7nJyccPv2bT7//HPfx+Sarx/XAabcc7lnm5ubvPXWW1hr+eSTT7xVvB1Q1N4M7PV6PHjwgCzLeO+995jNZuzv73Pjxg12d3cZDocMBgO01sznc46Pjzk4OPgTSuTT01Pfl9oq4natTznn62Ud2hsUAq7bwO86HLwOVGXelXatKpfcfP/+fW89Pzs7+xNt+aOOH3+ACFirWNUJpQ3IbUhXF2Q2JMAyUGvSoGiSkzWZiZhb98tTV+ccVSNSVXFkRj5J+UU+BuC87PHldJtFHhMGhj/K7vLK1i1+afMhplkklHVAUYXcGjhp9tHjTc6qDT6M7xAMSkbDJVFQs7zocA4UDeyLk4qyCPngw7uoUhMtnUUtXEO00OjzRmkWKoqOQi00J/Mex7M+Slmqk5Rzq0g7Bb3RmqoXkM0TlLbot1coqwg+HDTJprJgdtBA6oLZoAFhqoFULxPKce0sxhcRq7gm1DXTlZOSK22vwMPiRc/Z85aKYA0uMAJfE8+DsZpGSec+V1VO6RHkDtRJ4IKkFQcoqo71QKZOnZpHVWB7ro6gU4Fp6o69XLQpp45SdZMQ2qhJql6jaoqa2nO18uCyGlpvLY0ncoLWQ9c6sT6IwAZQC5CQmo6NEFIh0NCFWjjrmyVcu2s5+8WS9EnskpBDLi3R6lId2Hy0O8fI3ROvllqLvdV9Tp1aTGiJZtoFbFTNz2l3H8D9P1i5UJWgUY2Fy4psN+HiGzXRKmXru0un9NOO6Nbdps6JhuGTmiA3HH8rohg5O2/ZC6hSF/iAagCaExSQb7qAElQT8JErLrIO/SigE5bsfuWM9c428XkI3RTTTSi2UmygHIQCB2ZK01jrxdbs6sxhrVNPFgYbaGd3Vgq7zsBYVBhAJ8WGAaqTYuMIXRrKJHThIwPtwihoQF0fF3ojKbbKqSmthmKo0YUlvXCArUocjEqmzm5sQuUhYx27+xQ21ue641RLdeoUgHXi7mvZh6rvbo7UKAxXzrJcx64Wqc4hObMNpHVKx6hpX4Hubry4/pBcuHY3DTi3TduXfddvyqF1tTXL/x97fxpzWZKfd2JPRJzlru973zX3yqrsrLWXanarKVJbU6Q8oCiOTBkcEdRoxoIFw1/mgzfYA3n8yQaEAQYDjDGw4LE9MxoLGMGWxiNBIkWaQ40aTbI3dnd1dXVXV3VVZVZm5fLudz/3nBMR/vCPJ268t6qp9scu9wESmfm+95wTJ7Zz4xfP//8IoGq2Qt8Mm5PVnop9Egjh2Q7oP/I4fcXgb155E//57/5F8XTR69/bjkBYFXIyNgMBnS6XfvnJFx/iVw+/g2eLE6y8QeUzNMtcvH5yec7MqrXC1wC+66JbddsRdR3HBLSExC6cGCx5pYBMlF6AAETnARMGE+dnhGhRYxw6SvK3EhIr7dFYg9xYCZfOHQYPlIQvlxZmqdF/KiHKrlDQjYNXCgoe8F7Uh51M6nzRoNM4rHZLGf9O+pnLRdE7u1GImtStx7stAzDsyWaPKxwk/2KYDIIiTjVBEc3vCE7JWFAJCE1UiKrRH1YiWskv6TvBciSE3Hrj18o/DfiuRTlc4TPXH+Hloof32xkaG2S3RsbJTmeBgQ5J05N8hJvH3AsJ94pgDvj8zvv4md59/HP3KlBr5NsrNLMCaDViHkQtgNrnHof9Gfb0Et9r+njQ7OG91QG+eXELPzg+xGqVwy4yoNZirtWKuZapNNqt4FprPOodG99HVHvL/CjPo6DWIc+se4UYsu2ytVrVuhDW1qqQGkXBITF9cXKe95IDschaDPfmyA4c/uKNt/Hv7nwF/8bW63i3PsRr7SfW9/vp8bE4+GU9hQzA5dC3dFHCL/5cQFG5pZSKecfatsXOzk503mWYHvNh3bp1C0+fPo1Ow1wAcsHJe1NhQkUZw5qNMVFRwUVvXde4uLiI6gjChLqusbe3F5Up29vb0YCECqrlcom9vT0cHh7i6OgoOt3u7e3BGIMnT57ExVzqFMnFERey5+fn6PV6GI1GcfE5Go3w9OlT9Ho9nJ+fR/iR5zlu3ryJ8Xgc3WGBy0qxdLGaAtx0QZZCMP6O6keqo9KcdamSiEqw1EQkrXtCLoK4FDowRx6wDuNNXXrTMqX9iTkl07ISSPPf/AxDegmj3nrrLYzHY4zH4wgkUxUa+zGPzcU++xMBD+smDR0nCGEd8D69Xi9CcIa7X1xcYDQaodvtXsq/RrUmIO7dPB9AzF1G92OaN/B+eZ5Hh2uOre3tbVhr8bWvfQ03b97E4eFhrDvC+TRfG5+N7c/fs+9RMcixzXFAmEVVKB3BCXkIuNn/CMEJDzmWObbYZ6ii4/1SdSuhHOcO9hvCiTTXY13X0cSBodda65hnkv2Y4J3KUqozafxCyJkaFI1Gozg+0nsyxJ4QmwpY9mVCTYbicjMEkJx3v/ALv4B3330XvV4P3/ve92Lf4zyS/mE/8t7j6tWruHPnDt577z08efIkQjxCQ4J6pg1wzmFrayuCbwI8jh9uFqSh2KliMVVWpyovHpz/UjjF8cY/xpiY97AoCjx48AC3bt2KBjy8L1VoKfx3zmEymUTVNYE+YSrDcLmBwvcN8yEyLPn8/ByTyeSS0z37egqm0jk1Df1PVW+c3zZVdenmVDpnEjAeHByg3+9jf38f3/zmNyOkYx1zHmIZP+p9zLJvvgMGgwH29/dxfn6OZ599NuaTBHDp3cTnKssy1uPdu3fx6U9/OoJ6brj98Ic/jJtehPnpnEpozLbbTH3Btmc98BlSpXA6D7GuWEZ+p0hDtvl/qjXv3r2LnZ0dWGtxfHyM+/fv4/bt23Es/rjHxwIgKuXhoDCzpYQIBcMT6BbzkNeQ+Qu1cjBwWLgSD+tdnDV9dHUNC413Z/t4eesJHix38K33b6GtMqiFgS8dyu0Kq2mJNy5u4Gg+QNVksFbDewWlPO7bHbROIxvV0O90YTuA2qpxfj6ANh5YadQf9OEVUJ5pWThve2C7hdcOemzkZ2ptYqAsUFwoZDPJh+VOt7HadWIkknl4D3zmirgw1y7Dd59ew/y8i+V5F9AeeSmLR5d7+Bwwy3Xus5gDLST2NysJZVbWQFlRl9WnHbzb3cP56QBlv8aN3TGeToZiPDAtYRol4YRDj+JUHJ+5OPZ6nTOQKiBx8RVwKCFjAXzpYKzgVTShkPBfhNAwyILXCTADADiBhc2Wh6l0yPsXwk4VoL2AQbNawxo6N2dLCWWzpUceoCHDg6l2SsvsA+xUIfcdIV/bQ3wOmljQPMblVBdJHsfuuwVMI+eoBjAhRFuFiDnm1XMBwKkWwWBBBIKmRvy8qaQ+XC7nZfMAloJCLZsBcJKPEkHdpVupL906lGcNnvtvNLL5ak1pWgtkBrYrk46pBYQe/UyOat+Jyc3QYLWtRMkZgFkxkbx4rpB+Wy4FbGSVR9NTmFUlenmDXlbDaAeJN3XweQbXydD2TcwxydBt36EqJ48gRjceynuo1kNZB904qFUDtWqB2Rx+WcFbC10ewm/1YA9uwUxW0LWFqTV6J1bckBsPZbUoJkuBgAz7hgrhy8EgxHYVqnwNCqOyMKgG7baW+l96ZGceLpc+my0T0wovbWdzBV1KfkUooBkKZGz6oS2DwtCW8rto4hDCYLOFqEDFaENUbfCSuzFbyD3hIPBFA+YsqAqnAmd1K2WAFihpyxDCnUlfJ1Rsu9JP8g8cdv7cMb47vY7uUx0he1b5aC6hHCL4bfoqqiTttsVfPvwubhWnyFWLiS9RwEIXFrop4qYB1dDarlMVuNpgeaDhcg/7fj8GrXojCsQaJoQ/B4DmFYz24vStPUZ6AQOPXNk4JyAARACY2VKUxJVCU2WoygyZdmhbjXxnBa97WI3kvGyuUJ43gFZoewbFuIGyDsp5yeNpDFxuoLyHKwz0qkUxVqi3C8yv5XHDhPMIc1bWWwEadlwEV7pRUbnmtY+DnekUvPLBTEjaOIbcBoioVhIWS2WhbnU0VvGZlzx8QFRuCoQV6OgLJ+kfKgW91+Du4Qn+o2f+CYABrAdaK/kSfIC1XdPAwcEmSjvr3YcgooETsOl0DLkuVYtb2Rny3QpKAW1jgmHLGh5GRWDu8O7JHv7W7H+M6aJE22SwKyMhyq0ScKgEmKpKh3oCbM+FFBgBDHgFD75fApCNk28CYR1kPlTh35lsuvkcMNqj0BYtcxiEcG4Nj6XNgVY2C9ocQGmRZRa/2H8Lz758AguFt1dX8f35Nfy/J5/Ff/ban4N52EH3XMY9avz0+JgdaQhbqsLg7wDEBSYXEIPBAFtbErXCRTqBQKpOZF6qtm1xfn6Osizx7LPPxtBlhu5yIQhI/juGwxIGUrHCHFoM22JeQ7pvUn2SLr6YdJ/PQEiyt7cHADg9PY0LVUJFLhJ5pIvHVBnDumvbNrpRprkDCTuLoojQk4t5Ou4aYzAajWKoXRrCxT8EAcDlBTAgocGEJoR+m9AGQAw1ZhsCuGQ8Q0VPuuDm5xh+TeDDfGepYpKwj0CHR6pk5YKU/YQ/AxDhIRexLMNisYghialZBOt5UxUF4NL1CdVSRR5Vail8TaEWcw/yGlVV4fDwEOfn53jttdfw3HPP4eLiIqpE0zBCpRRu3LiB27dv4/DwEK+99locI1VVxTyNaU4+1gFVqwxFJ3xIw9rTfrdcLi+F17Ovsu3SPwRB/X4/1i/HLRWDo9EoPg8NPxaLxaVwXYaLbqrP0vZkn2PbEEISbqcqzBQSEuCk7aK15LFkeDGfjWrCFGTx+Vg3NGqiypHzCPsBgS7zKqahtinM73Q6ETpyDgDWRjUpjNva2sKnP/3pmFrh6dOnlxzTU4DivY9tTgXyc889h9PTU7z++uuxPHSoZpg0gXM613LszGYzvP766wAQ84uy3DwnBYSsZyq9UxVx+m7gvJVlGba3t2PuQ+bRTMeucw77+/txAyZVbaZzTwqVqE5kKDlV6hyXDKXnOKNbMlNjcA5J+86mqi8FoPw/FZ50ced9NxV0BNc8l/2AdaOUwvb2Np599ll88MEHeO+99/C5z30ulot1kI6PH3V8lIKP74XT01PcuXMnGrMwr2GqsGZfZu5BpguZzWY4Pz+PaSDS9wjfaTwXwCWF7ea44Nw7HA5jiP3Z2Vmc39KohTRdAp8nbRtjDO7evYtr165FU5b9/f0IQpfLJabTKbIswyc+8Qk8evQIf/zHf4zJZPIn1mN6/OQDRA+MVx2cNAPkymJhC2glC8gc4pa8cEU0V7HeYGx7eLQa4d58FxdVF1tlhY5p0DqNN8bX4LxCXrTQ73ahG3F0baZ9oOtgZgbjx/vwGmhGFuha6Mxh6RX8SvJr4UqL8nGOxir0hxVmF11Rkligc7KeZEwFZMsshj3SaIRKM0IEHUCbN0A+UWiQwZcOo+05tPI4Wg5xNBvAWg1VGeiFAMp4fqMk1JbAMCyqgXCveq0CQrg/nITGjt/ZgXEA+jUy5fDSwVM8nI5iTixfOiDzaLYVlJNFrKkAbwSQMUzPZWuoyHBhAi9JSblWe6whZKinlVqH7a5UVPEwv6NZhNDljuTyYkJ8CaUNYc8WMJWWhaGTBXwMCQ6KQAI55QMQDPfwCrAhFFmvpJ1cz4cFu/yebtYRrNSAZ+ie8/E6NBVIvAZEfaU+3C4RCC6lHFSNCaSSaF9TCxCS3GkhzNoLNJAQTsnF6IxHuQwhut6jfDKP91erYFzivTj1Wo+nXzCor7To783gj/qS8wsSlltrwGsVTT9sR4nhSA5gJHWiVwr5HJhOunhxX+T2s6rE0ENyLzZW+ogOC3kbTH3oGm2DgioLuzZm/ZICgpKrLVBcrGBWfahOB/AezY1dLK6WyGcW2cUS3jk0/Qx65VH3NdqOhm4ljDdfyM9M7aPyEEER2nZCOxvJK2lWkrtP8luGcOVglNL25Fm9Xj9DVvkwrsPuz9KjmAH1QIBQeRbUiiUBNIKiU8EESFfthzD44MAt9/NohoiwiX24GYQXPJ1+G8Tcic1AQmrLc8nDZwPo7B4JQNGt5Ef0RkKzdQNMbxr8zZuv4f/85V/CaIG1wU+DkP/Sx9yOTU/FugCA67dO8VL5SBzvIXkHF75Ef1ChRlfqKsw3thPGKHPPNeKSXIwVMBanacnRKP2hgIQm6wZRIea9wrTuoGkNRnqFqSuwdEUwGZKq6hcNcqVxtZhg9nyDl1/4AH/92jfw2fIBKp/hN//l/wy9dwoxxBnJ4Ow98dArC1eEnTwXwLD1QN3Ad0ppXy99XuYNE41tbCmqzHoo6nJX+hgi7o1fq9Xo0BwMRJjewOcAGJKMAAI9oFc6gkG10pLfdtii2K7QKRpUdY7qaV/yCgalHEIorUw4aj3PZD4YqgCu42HPS/zlV7+LZzJRnVdei0N1G8LBjccwq6Bx+ctyehAmXrheeE+sfze2XdzJWvzdz/23+O7yJv7+t38uQFHpt3x+yd+oUT3tY2l6AhUbJUrvVokBSjpfFuH/fO8x72Ayz/oAZGVuFEiplwrFhRjnmMbj/GUP13GIOXD9+h65sagb9oX1ZlnXNOjdmuLu3gn+1vU/wH8/eQn/6uFdvFkf4P/wg1/FyfEQ+dMCtuPxG1/8Q7hlBhR+vXnz0+NjdTCElQsGKu2A9SJpU52WKqeUUhiNRhEW0j23aZqYR5CL76qq8Pjx46hKomqGi7L5fB4XdcYY7OzsRNBINU1VVdEIg4sQKnG4iE8VIVRWAIiKqaIoIkjgzwm8hsMhFotFVCqmCi4AHxk+CCCGoqaGFQ8fPozGILu7u9GhdHt7O6q5vPeYTqcxxDpV5qSKn1SZkS7e0nBNADHUlNdIHVnTED7WTwraCOhSBQtVgWlfYF5ILoS5kOY10gVmql5lzskUivLcVCGVqs/Yr2hesxlCzYOLVB7p4jUFTaxXAqcsyyKAS3ObseypQyyVc1SqVlV1Kck/FW+7u7v43Oc+h8Viga9+9asRoHS7XYzH40t1TqjDMUjVFUM/X3zxxaieS8O9ebC/83nZTlQMbSrd0nYnbGNYfFmWGAwGOD8/j2GrbE+Or3Qjgc/NscB8jqmKlO1PME1gyHpnnsc0dx/HAtuDdcXwbvZrKl4BgUW9Xi+qoFNTCPb3qqpi/afK6jTtAftYmm+RcG4zdJ7zSjqm+v0+BoMBXnzxRXzpS1/Cyy+/jH/8j/9xhJRpv2Y9djqdaGx09epVbG1t4Tvf+U4EqoDkmlssFhgOh7FczFXJvsF+v1qtImxJ00ywzdinWZfsRzS04njk/dNcgmdnZ/hTf+pPoaoqPHr0KNanMQb37t2L/2dbnJ6eXuqf7DMMSea45rhbrVaxP3BjhZ+n8y+hbxouyzGWhv6m8w/7EGEt33veS57eGzduIMuyOO/zvcRrpjA5Pdjvd3d3oxr12rVr+Kf/9J9G6Mv24rvDe38pLcZHHYT+KVTnuH7w4AGKosD29jauXr2Ki4uL2Je4ocOx9t577+HevXtxfFFVnM5vnDN+lKEJr5eC3729PYxGIwyHwwieaTq0XC4vgXK2D/sc/1y5ciWao/3Mz/wMtre3cXx8jPl8Huv9+vXrsNbiyZMn6Pf7ePLkCYqiwBtvvPGhcOh/3fGTDxABrJoMrTMY2y4y5XCzc46eEQOT91d76OkaQ1NJGJ3LMLZdPFyMcFF1cXQ+xGO7DVcZqMLB11qcIb2C7vioDCnGCjbkwTOVLKbMkww+M2i2PPywhem1cBcFvPFYHbYo7nWw7JZAz0FXGvCimMtma4URnFxPWYFBzGWm2wAMgsGB9kGNpBXyiSxUzzDC12ZdaOOwOusCTsJwCcWk7IhKHIYgtr2gDnSADvnyvA4L1rCQczmQT4ISqATqKsd7R3vodmssFuLIafcaYGmgag2zkDxTVBASGipH+BYUjiHnF5PKu1wUdKtdKZOAmDVQ0SHMkfWu6/AcIVy3uJC6tADQAeAEltjSw4dcimjFXMVrv865GBViBJ4CCPRKiZFLJmDTFhLmajPJR9b2RR2pnYINEJHQl6pRb9bPrsTTQ+4Z/k9IFoFlyFOna0QTmhhyvBD1ncvluQjWpI3WAC+Gl4a6U62KC2qpM4XyrIY3Grp14sBsFLxTEvoLwCsVIVDb8yi2Vli9vYXOUqEZOjQ9hXLigvhGFuhm5dG0OqromAOz6QWRz1kB95yUpWlNANsBkGQ6QgCq/gBEoxGaVaSqKeUForhMQSkNlxvoIhdjlczAdgyyyqHzdAFV1XA7PdRDgYSm9mj6Ck0ZTGDCGAPC+FtJ/dtcBagoN6XbcufER/MauZ6ELxPwKbeGhoS9bUfK33ZD382kfZq+gEkxp5F6yxY+giev5X66DuOgJUiX+2UzqYvVSELUVatQXkjossuBtpC5yxbSl1eFRzPQAmcCWGT4MbTMEwxl7hw5HP/qCq9Pb2Dr+xmosi1mXvIrquAeHMZghJfaoxlZ/JXrb+C43cJBNkHjM/T0KsLEtuexcmsHeNXKOMsbhfo6AO1RnstzcUOFc2WeOOxyg6BeZajzFt2sgfMKc5/BQpRhHpJSIasAox0W3uLv7P8Af+ff/EHy9ihxYudQmUP32GO5r4DMQS8VBo8soBTaroGpLPSqhc80dC0qRNfNAaPgLcPaNeA9dOOwGiksDyXVgA+5HLlREaYdcVIO/dsVDjq4RpuVgst8hJKSk289DlzhoLca+En4UtdxuH7jDIWxqK3BeNwDBg2wzIBM8iHKHy9AMXNAoyX/Xyu/2745FvB43MUb8xtotu/FnIbzk150dkfmsJMvkCsTw5cdPDTUh8KZ566MgFQAoMZZ3UflHT6RH+Nbi9vQuZO5m6pL5m/1gJnp+MzehE2SwsF3mDfCJ7syCKrFYAbDlwKBeq3jXJ1PFTonHsVMzGxs4bHa0qj2ghIysAO90pKWIvOA1dHp+2Bniqpf4cX9I/z7138b27rBX9n5Nu7VB/jPH/05fO+Dq9geLvBufYiT4yHUNCiKthtsZ0uowkpu1p/yw4/lQQVNmiuMbpVcjHHBxoU/ARyBxnQ6jedQ8TKbzVBVFdq2xcnJSQyTpJMjAGxtbWF7exvdbjeqk1IFUFVVGAwGMRfiphIGQARRvD4XRsBaUQEghiQSKBE6Mhk7y9o0TQR9qeKIUIKLoVTVQzVVWk9c/DE3ovceh4eHABDdP+naTECUqjRSlRAXoB+Vhw5ADIFkHkL+bDN8OYVhBFFUK6buyvw8n4EL1xRm8tqp+ibNkZaG9fJgmCuvnypf0lBIPm+qAk3rJQ3ZZXlS8Ml7ZlmGbreLqqpgjInQOAUPLDPzcqYquzTMk+GAi8UCeZ5HM4Y0BJKKs36/j7Ztcf/+fXQ6nag0PTg4iGWhIpb1mpoNGWMuga7RaISzs7M4rtIww7TO0mdPwQOwDo9nu61WK3Q6HfT7/dg27IdpePpoNIqu3qlRA/sSgSHrlu3KsFJCLiowqSwjIKKKOQW0bEvCQNZ/CvFo0EMYxLZkCC3hD0OxmRdwZ2cn1lPax9inCLFYZ+wHVC9qrWOqhOFwGPsk+2ld19GB9+bNm3jnnXfw5MmTeC6V1ZzDCDkJUp9//nmcnJzg6OjoQ8CXQLgoiphLlvMeP8cw77Ozs9je7Cubqre0PtJNI/ZBqpo5Ruu6xm//9m/j2rVrOD8/x8OHD7FarfBX/+pfxfXr1/GNb3wD+/v7ePDgAW7evBmNrdK8suncQgDMPpxuYh0cHODi4gLz+Rzz+Tz2Z6r40s2PdIMndQVnXy3LMqYGIDQHgMlkgqOjI/R6vbiJQLfzyWRyaY75KPVcmuaAClmaPbE9qJjkOGQf58bDjzpSiMt3D9/VzANMYM+xzTrg/Mbckix/Oj+k77UUpnO+S1XFvV4Pw+EwOlwzB6UxBvP5HCcnJ9jb27s052xvb0flYKrI11rjhRdewK1bt2CtxYMHDzCdTnF8fIz9/X1cu3YtbvSdnZ3h9ddfx+PHj9E0DX7+538+vptZR6mK8V93/OQDRAUsqgJHqwEKbQEN3FvuoW9k8J7Ufbw8eILH9TZWLsMs2KS+e76L6XlP8iaVFmplEhWYisnt254sfphnDiDcElOQbCYmJas9i16/gu3U8F6hrjM0rkR+LknpXC4ATddhwe8klJZhy6aWhbSYUQjIMCtgHcMX1m8LyZ3lcw8z03DzLtrSwyxlRa5r5hoMoWyNShROHmgkZ6FqAePWgJLhmcgkDFeACKBDXTTHBZxTmOVduO0Gnb0lmsbAVgZ6qeWaHojuqiksIzAK0JDATcs4hO0K7DRU9wWHY17Pa8R8gAISw/NkVDYCbV8UgyaEZovyyCObrl1XmScxDQ+lg6lugLZESLIvQAiQxbxqARNcTPmcPg9qSIs1tPPrfHfeXFa0AeG5GRqdY52zEPJMhIDKrYuqW8mHp61f50sMQMetgHoo55iVF0jVstzyjL4VUKWtR7aUhatarV96yjn52WIFe7glYbhOnrs+76CsFKrDFrrScIWAMFsEl+IAwlQLdM5dDNN0mSj3iqlHcS5GKg4Kq2UuABDSJ7xOwrjbAB8qK1BGKdTbGbKlgzMKpg4TeSFmOMwPGuGi0fBFDrNsUT5aQE3mcPvbcIVG50yeVzcepjFoOxouA/KFx8qJ4s0VQVFLSJPLsyof8vEFEL8OQRZTknzuL5lu5HNAWymzch75POTxLBTyuRcFY1/BdgVOm5Wo7FwZyoAk5N6qNWAO/cWsgLwSyKicQnnhoY8QVVVmhfgMuvFo+8HNtyv9oDwXt2RbAq4rSlaC/7YbytNR+It338LvvfYK9hYSiq5bSFtkOjoIK+tFXRdUgtDA7q0LvNq7D+s1pq6LXLXoqxpOSbqHbCn35LhlKLMtEBVy3VOH1Y5B25P5i+WzXmHiOlCtAGCzXeNTNx7jF/Z/gJXL8Wi2hZGu8cT2cVb34lgAgFy7D2nmrHf4wC7wjyafQXGvI07Sex6wshFTjFfwmYLPFMzUxnBlWMl/6HIN1YZx2Tq4bgZYj2aQoR6q2I5AUBkHBSFzoEKv5x+6zkPJZ10RQJgHXN9h58oEg7KG9ZIWwAOYNjqqCU8nffzvX/0tfLZ8iP9V8ev4wQ+vQw8auEUm0LBrw2dVcF6W+dFUGq7jMX64Dd+1yHcrjLJ1Muqpz6FWoeZCx9IfkbiPEDE9Fq4EnabpfPzt4xv424u/jg/G25iOuzC5E3VjgRjK7A2gGgXXEUMXBEU1w5sFeieKQyNglKpD3ssspNydM4XyzMOsZB60ubiPT/YVmi2Ptu+ArQr5wzK8rzzM1GD4nkI9AqyIMdExDf7XN/4F9vQKc5/hH48/j3vtHv7Bk5/DH3/3DrJJyHe3baG3FsiVhSkt7MrAL6T+Gu7QcJD+eN/Tfnr8hBze+5iLMM1DR9djLry56GLIMBcU/P18PsdkMomLOy6y+W8u/NM8TVyoHx0d4fDwEDs7O5hOp3HBw8VkappB1VKqIGLYHNVwqbsjQ5m5uCIoVUphPB5jOp1G1Q0T93Mhmaqn+AyEFCwPgR+hyWaOP8Iia210sx0MBrH+ueBnTiyGOfJ8XpsLfIIjHukCOVWYpeqPVAGSnp9+ls+ahofyWWmQkoYycnEOrFVMvG6q7GPYYQouuIBOw8BTIMq/N3Njsa2BNeTYBCzsB4QlVJWx//G6/B1D6wlzWA8peCbkIIhcLpcRCLOOCTKKosBkMonPDiCqg7hgTxf7aZ5PKtT4WYaHZlmGra2tmIuPCtcUghF6piYHDPVnf2cfoVI2VcKx3Zjzb3d3F7PZDFtbW3H8sb37/X7cVGBIPttoU8lEBSPbheHXH2VqkSryCL149Pv9WD6CYPabtN9RNWutjeqvTQCTgtcURKcQm+1C6EiTp3QO2wTgnO9u376NO3fu4OnTp9jZ2cHv/u7vRtVxOpb5/AScbdvi8PAQe3t7+MpXvhLTKKSQalPJxg2DtA1YV9evX4/5E/mMnN/KssSv/MqvYLlc4uzsDFVVRWjpveTa63a7ODo6iiCL9zw/P8fp6SkODw/x8ssv4/DwEM8991w09WAd7ezsxPsTIKXzY9qXN8fG9vY26rrG0dFRBIwcm5tqwI/aTOHnRqMRdnZ20Ov1oJTCxcVFvK7WEhbP99wLL7wQc2GORqMYHs3QdsLUdNOAoL2u62iK8swzz8Swa601er1eDDln30thH/vvJgRLVaN81sVigXfeeQdXr16FUgpPnz7FbDaLcyr7Yhpqzg0hvi/SOTKtM/7hJoExBnt7e7h27RrKsoz5emm6wudP0z0QrGeZmEBxs5DPsLe3h/fffx+3b99GURTxHfyJT3wC4/EYR0dHeOedd3BxcYFXX301lpnzAjcWUjXrpiL0Tzo+FgBxddbFk+0tHPamOF4OcF510csb7Hbm2C4qPFpt43TVx+P5Fva6CxzNB1jMO6IM8AreQ1QZrZJFVuajai26ypYJ+Gok3JWLP5cDOC0xGxdQ2zVcbUThAQFbuhZFjDMAcub0kz95JX+vE+sjmHWIyoYqHR8UbW0PcFlQJoXfZzMdxBiS71AHSBhDvBQBVYBDQcVnTQjRDYt5FcxOaBgCrEN6AVFMmEqhWRaoDhTQamQTgyyYNLgCgAG8B3wIbxT1R6i/ACsRVHpqhZBDDnEh5QPc85mHd4Bpw5czt4ZnQFBohpBsV8iCO825qFoFlQn4IKT1GdBsOQmzXsmive3JIjUfK2RTUQyJ23K4b1Do6Bpw5XrR1wzF5EUFdZBu5eeu9OtFYcg/yPITKESIqBCALSLssEV4Tr0OF3UZYtinqXzIgSfQUPUTcxUboGWoJKuDUqwN4aZ0k3UePg8NpRRU66BaC1eGBXChoO7MgXkhAL1roZYabU+hmIS+r9bKR98NBh0DoB2EBX9wLy3GwKItoNUK3quovgTW8Jp9X9x8w+RvQqh666GdRz6poeoWy+sD2FLDrNy6f/NFkRlk5wvg5AIY9LB4ZhgMaBxsV6MZKGRLh964RjPIsNoSU5VyslYWAmK24jIpSjlx0D0lOQszAKWoEwFRgK4KBAWZnNt2gWIi9SFuvAIRdePXzr7nHjjDWs3IXJdGIVsKCG6GYQxqIJt7rErp280QMTRdwjxVVC1HNWcL5FNgtROqZY7goiyfK8fSH2RTQ8rQDBXqLWDrvsPjX3AodYvB2znabuj/AWQXU4cqF+MeIPTPQuakZuTwl26+FduX8LCjLJ74At6LMlOXCIpLXBob+XCF5rwTlZzNtkN5YsAwZKM8vlCO8c/+2n8MB4UD49BTBgPdwb9YlPjt/JMYaeCb1Q6+8f4z6JyrkFPW42zRxT+YfBI/XFzB25MDvP3wEPq4QHGuUUyAblAI2q7M6RK+3KIdFOvQZQXoZQtVN/CdPMzbDjCJcsMDq20dcrEmX2BCSgKq5pijTzkVzVL4O597+J5Ff2+B2zvnaJ3Goinw5HwIYzxu752hanNMT/shFFqh16nxK70H2NId/J8+8f/Ev5/9Gr715rNA7jC6MsfF0yFinkGGMxcOlorxpYbPFH79xW/jPzj4BnIlHfrC9kCHZygPpdZGNVQaGnzYTKXxFpXL16HJAFA4jKddnDzdkntOMjj2W5qoKCkL00j4PKgNQ/7F9HNRrdiuw77NTJyiXdchnxpc+UaNai9DNdKYPeNR74gLMwpOOvJMOpP3tOV4zmX8RHU5AOc1/tMnv4Q/eu8O7HEHplJ47y+8gZPlIDpJ274DcgelPJzXMMbBEmyGPJzeSd7knx4fv4NhgZPJBNvb2xG8EI6li28u4MqyRLfbxXQ6jcokAJfCtVKIkyqgAFxSslHhRmMM5iHkgpoqotQsJF1wEXpyEc17UKlDeJWqErXWsazeexwfH1/Kz8fFcpprL1WLcFHH33HBlIIwfp7lTR2dF4sFdnd3LwHP2Wx2CdKlobapMipVFxK4sOyEPClcTd2EN0N60/KlIaqs2xS2UjHEZ0shDuFdGuLKtkkVYynkoJFEqiAhVGL9pnCAv9+sEx5pLjU+b6qK7Ha7MUdmqqykKoxQg6HEKXxlf6IBCtshbRcCO8LGW7duXeqb6VggiGTZ2BcBgT/e+xgCSLixs7MT84KmYDMFgCmA3QwbJDQgVDk6Oopwgc/KazvncH5+joODA8xmswhMCYAAREiaGoLQCZ3qI7YV54QUdjGcmHCI/RVAdHxOxyPHMH9OBV86v/T7/dhH+ZzsmxzDzKmaAhfmPWW9GWOigpTAlUo6lo2pFph7lEqzbreL+XyOl19+GY8ePcL9+/fx8OHDS/Pd9vY2AMR8hwRpRVHg1q1bGI/HePDgwSWlHsc664gmMISaW1tbODg4AICoBL979y6qqsLp6Wk8v6oqfOc738HBwQH29vbw8OFDvPPOO5jNZvjUpz4VQd1LL72Eoijw6NGj+A44Pz/Hr/3ar0UnZD5TVVX49re/ja985StRGcmw1uPj40sh58A6xUJRFBG6pfPGcDhEWZY4PT29pATmuyMd55ubO+w/g8EgQvPz8/OY8qKuazzzzDNxs+rhw4dxrrl58yYePnyI73znO/gzf+bP4Od+7ufwta99LfY55vbjuEtTaHDMPP/887hz5w6+853v4OTkBN1uN24qbAI7HptzMdWuh4eHcM7FuYD95N69e3jw4EFUzbNO0g2XNBXDcrmM9cSfse44B/FcvscYYXDnzh2cn5/De49r165FUHhxcYHT01MsFou4ibi7uxvD61nXTF3CeYigdnt7G6enpzFcfDKZ4Fvf+haePn2Kbrcby8W5N40E2NwI+//l+MkHiADggA+ORnhaDFEULdrWoBhaHC2GyLTD1Jd472IXdZthsuygrjPYygAdBzMxcJmOCeV1raISRhx4EVV9VCH59XtWIICW0FdvAXdRAP1ASSxC7iiqKiALiQAFlQumBYmBBsOXmUPQG8DYRK3mRckIJwo+VyDm13M9CccyFZC1okwxK1H4RRMTtVb2RcFI+JlpEBdQ8edunYONZTBLBTcOO5fBlZUhuJrhuQzjZZhkWLQKFArlET8E2KCsdOHeppYQVdtZAw7WjQlAzxXrMkubKGgr4WYuhCprK7m7qLKqRwJdVKuiaYqpEBeMhKWqVnCvzKDeHKBYiAJNW4GibPPiPFWphuryAh5dcMON5ipeiYGplT5AGBzrBqGufFCEMZzcr5VaXgP51F8Kf3ZGSblMgIlOSVlrCXnmdeEFhKF1MTxPKK8H6GScZ2gGGeAlZ9vOcIGn0wI+8yjfL+Ezj7Yruf0I/NqQl9OFsiqvYDuhM3kJD+498biourjWm8C3eh2CDcT8gC5TULmG8g5eK7hCx74vnUzB5Rq61RHi8lwqGn1mAOvg7z+Evn4Vq9u7McR78UweQvoBd6hRTARKNcE4RdcK5dSh7WgJlZ1LaKPLxeikmEq+RIY2KydAnEpZcT2Wz/qQ10/MX6Q+gug5AmRtZezaLmJuwXzqUc6d5EiEhC9nq/AiTFS3thQlY9sVYMsxSHMObzxsL6iwDKGYuC27QlTTekXVqqgfCR3LCxknf+ln3sDvvvMi+ot1jkbdAMtdjYuXJJ/n9g+kDdqQl9F2JAz25e4jWK/R+Ax9PUdHNdDw+KDZhbUBPDqg3vawPQfft8h7DUzmMChrXJx0JQ/hTjCLOjNxbNG04+WiF/vQuV3gv1sa/J3v/RrGkx7+m5sv4D998xeAez0xdckF9OKrO/i/f/lXkFVy/2FQercDj/EzLYY/yMXZ2jhkc43B41bCknMFvXISIu/W6lnXK+ALDTRO8nlmYWFRGlQ7OrpaSxv49TsjMUqhqZOuJeTWK8DfWsIYh2aZo64zvP30AAfbM/z84Xv483d+gC+UR9g1Jf69h7+A+w/3ASNmWibAQQeP57IOTpYD6LmB6wOtk4lflVbKEVyLVebgA8D0wXDpLwx/gJ4uIhA8swNJRxEAua8MNHz8PSDQ0CiNsVvifqPw7eom3ljexL+4/3Iy1j3MRYZ2aSREO/fwhY/5HaVeIWVRkI0ohlwDAg59gOfcgOOhpU11I/DVMR+ilj769Oc9/GAFRbdpnusV0Gh4J+0e20h7+I5Fdc0LwNaAUh65tvjaB8+gXWTIwjMMshqNE2iJsBkIBXSyVkLptSgnvQaUDuY+wVDmp8fH82iaBufn5xHipXm30tBBKvp4DvORpSG7KfygEoLX3AxFBdaqHyo2gLWpB0FfqtRIQ3ypuuJiOl0kp2FdKUjhMwH4SEDI0Cyq7rjI4oKMwG4zJ2IKrtK8W6kqjqCFOagIsagKSpWEqYorfQ7eL83Xxrqm+pN/E+4wDHQzFxqhFhdnVLxxgZaCF8KbsixjzjI+V6o0JJR79tlncffuXaxWK7z77rs4OzuLgDDN8+e9jznceD3WDY805DjN9ca64M/5XGn+M96TYfU0smF9pvCLi14+L8MQWX/dbhfn5+eXcjyyb7VtG+uPSrKzszM4J47AvV4P4/EYt27duuS2672PeUD5jOxXBCPL5RKHh4d48OBBDDFOoRvBIPtyqnBke6cQPwWGKZjh2CNAWCwWODs7i/0/VasSyBJ6M1SRbTCfz2No7maIPNuEACwF26k6MVWo0ZGa7UNYSxdqAm/OOYTfDGWmipphph8FIgjHUyUi1XR0X2aZGIpJVRrHeVVV+MVf/EVMp1NYa/Gtb30rKmeXyyW2trZw7do17O7uRnhFNejBwQG2trbw+uuvX9oAYf3meY7xeIyyLPHMM8/EPJks92QywePHj3H79u0Ymk6Yw7FSVRUmkwl+53d+B4PBAIPBADdv3sTLL7+M/f39Sy70r732WszNenp6it/7vd/D888/H8N9j46O0Ol0cPv2bQwGgxjWfHJygjt37mC1WsVcqXwHbIIs/pv9gSkl6rqO57J92M7sw1TLpblTCYMnk0kEujs7OxiNRrE+GBJ99epVPHnyBAAiTG+aBmdnZzg+PsanP/3p+J7gM3CMsPz8HfswN4beeust1HUd+1tVVbGPM+Scz9bpdDAcDjEajTAajXDlyhVcuXIFnU4H77//Po6Pj+PY4RyZGrykMHDzHZfOl+l7Y/Mdxc+wvxDc9vv96Dr/3e9+N+YnTNXD/X4/quj5jM652D+Y6sR7j6dPn6Kua3zta18DAFy7di3mQEzNjNKNmfQ9y/dv+t7dVG7+ScdPPED0CiG80sAaD+8VqkmJRVlDKY/zVQ/DvMJyJSG4zikMeitYq+FqA9fV8Uu/aggKGQq1FpHEcFy/Vk1J7rqwGFAB8DkFVDqEH4awXisOvq70ovprFLQD2q6oemjEASDm/APvzZx6BtE4xCu5lnJrOEalh65DiG34TiA53gCG/1IpR4UbF026Tp4tiGEIq3wGoBFFFh1bzVIF9aWP4c9mlSihsIaPUn8hF5ZSl+ElEEFHXNix7i1QbwkMFAMV+V1UOyLUk5a8hDH8vFHRdMZnYhCRVZIvsdmS+9jSI1vKOT4DED5vlhJi/u996l/hP/v6X8HwgcVqqCOIcCGHnQ6qRh2clWla4QrEsHEefDaa2Lg8hC2aNZiNnw15BFnvUZ1WAKuRQjmWPmRq6ad6dXklyrRgegUYQjglyjHlPVTj1vAwPTIjYbEWqHYVdo0V0ADWoZTBG1EotiHHYTH16FyIE7MNZWf+S5qVnM260LtU9YY+oGigIsDRG1Hsea2CWkvBGQUbxqfPNRwy2DyAlgjCw/X4ZW9nBDfsQq8sigsJP9WtOEovdzW8Vqj2FHpHTkBchwo6BW092lycjfmMzsj5ykqb2zLk5muBcuLRlgK6s4WPcDGal/TW8DlbSp41VyIokRHyg8q/my2FtlVo+2FjoaeAMeG5mJaYWnIvukyAMdMqMBS4t1iPoaySfIVNX55DO6B7BCwPg0oUQHkWFgt9adfhPY+zVxQOiimyNwYxjUA0EioVzDIofgnmwrznSocv3vwhhjq82JVD5XMMdYVCuRi6qV+ZQimPQWaRZxY+SQR3crSFfKqhbAvVKOhxHlWK8MBXq2dxf7WPp/UWvnt2DUfjAVZnXRSnBttvAYNdhb83+AtYvj/E9n05yWcCOW0JVAce9uoK26MFnhud49XRQ3yifIqx7ePvf/1XML8pZcknCvmshcsNoBSyZbsG3yHU3uWy++G1GHu4QsKbbUfH3I1U6irL8FofNy8IDHUtqkfJVelhAJRlg5evPsXP7tzDFwffx4v5Etu6E3ISDtB4i2lbyjwaoJjRDjp84Rm7Cg8e7sFcqWAALO5vAYPQiD6cowAfoBw84HsWOrd4tTgFMAhTq0ftTXxfwCog8+joBkZprHyDR+0Kv7+4iy+Pn8d7kz0cTQZYXnTEmKUjjk6q1vA6hagQYOkBFZyUZR4OQKSQaICYx1UhlhMufNHRXnIUWsRdMTtw6B/OMT/qgwrvtqfh+yGUeJlJZABDuEuLYnuFXqfGcpUHlWloZ72+n1cQ9axX6JUNVt0CXocNtI+KQQ6wu6MamWZtyJMLYOEK2UhJ3nU/PT4+B4EOXTS5SGHeMoYrMzSJKrdUGUTglarDUlVFCje46OKiOlUgsTxcsKehbakCjoumj1KmpUrDNNwNWKsr0hC0dFHKa6YANVUBpWongkIu8nnNFNhQeZUCGz639z4qSIwx6PV68XNpnrBU9ZU+U7oI56IyDRVO3YZTqMl6IBQDECFXCubYNrwfwQkVolQbEiyzbthen/70p3FxcYGXXnoJb7/9dkyyTyUSn8s5h15PNtgWi8UlWJlCh1SJCHwYqvI5+HeqrCRQJYgisE0BdKpSTdWHqTJRKRWVd2mfY78jsB0MBjHvIT+3WCwiwOGCPe17bEPmi+O4mU6nODs7w5UrV2IILNWPvD+AS/WfKh/Tdifs3cx5xjpjXTEk8ezsDNbaWGdUHgOSVoBtlwLrpmki9CdM5pFCU46PtF+nik+OJ+Yd5Gd6vV7sI2l/Yxk2DV9SB9kUorPvU0nLemV/4OaFMSaqqghR04NzJPNG7u/v4+WXX8aXvvQlXLt2LRqIEH7OZrOohqNybLVaxXyJVVXh+Pj4Q4Baa8kz+ujRI1RVhU6ngydPnsT0E6vVCt1uF9evX48GFNyA4NxzdHSE27dv42//7b+N7e1t9Pv9OJbv3buH3/u938P169fRti0WiwVOT0+xv7+PnZ0dHBwcRODIfnj16tVY3hdffDEqEa210QyD5lTcWOIcQPVyGnJPiDgYDKKhzCZwZjtsQt4UOPX7fbz66qu4desWnHM4OzvDBx98gLfeegtnZ2dYLpd4/vnncePGjQ/BQY6j7e1trFar6P4+m83ifJhu2LAPcc7s9/uYTCZRWZluMLC8NBO7du0a7t69ixs3bmBvby+CxrOzM7z11ls4OjrCyclJhI98T1NlznQA6QZWqhBPlcjpXMU5gu/DLBMXZeZqLIoCx8fHUTXP/z98+DDOE91uN6YyIBznv/msbD/O/SxPuoGWvoN5cP7dHG+p4nozjPnHPX7iASIgCzQ1z+AWBvOhLHhm8w5sq7FcFbg2UtjqVZguS4wGK5ycD2HHeeL2CPi+g889dKVjDrvo+OsRFWUEbAhqM1t62I6PC2lfimGKz2WhryELpWwZGi68n7mYUq2KENIrwFsfcyO6bK088hmgQqhrdOMNoMYGsxfdSMi0x9oMwwUjEVOt86lR5QiEkFm/VkfF9VMTwnYDLImqs65Hu+WgV8GRODy/wNKQO9FTibU2LZH8ffLvti8GFGYZyhgAm66BfOIlXLQU8wNfhzx5IdTaGoTwQID5FmlGgaSNCC7NUlR6Lg8wbro2HNGNKFSa0sdUky4H6pHD/+vh50TVNdDIlx5YimrIGRXbSltp23wm0KntIhq8KBdAkQJc30NfqGioQUMX+AARwuf9+vtm/L+4SweoqAG7FBWc5Dv0yGeiCGs7KgLHpi9wUTkxiFEtkE9qgRytk3yDWgMMv2wtXK8b++dqx+Nk2hf1bO5DbjyBxTHHYcg/uNpRqLd8dCCm2o5/t12F1VkXq5sZ1ErDZU7uC6xVhFaUXt7oAMuwVhYqBQWPtmNglFqHNnNMegB5JtdUCn40RL3XQzM0MBUNXxTMymHrfQtdO8xuFLC5QvekXcNuK89g+jrmeNQtsDxQsMHd2rc+5qdsB0Ft6dfKWa8UirGPpjhmJfn8vBYgWY6lPDaAN4SiU2FaTjyaSqHtqghq81kIGc8U6jIofgNUkdyKYW5oEXOk6saj3hJQX0wlhJqu7P1HAh5tDmQrKZupJHw5qzye//P38U/e/TTKU8CViIYxBPbD++sUCM0QMX8qtht8YfAermYXuHA9GDictgOM9ALHtgvnNZTyyDKLXtGAIZzWaVRNJurEswxb7wDVrkE+VXAruntLH/8P/+GvwyzXissSQG5EYXnxskdzWOPvvvwv8Ic37+Kf7Xwae3szXBlM8dnRQ+xmc+TK4lZxCus1hnqJkVngQK/wN7//70o+zB2Bcd0jD1U72G6Gsxdz5IsMO99fwlQt9KKC3R3AZwqq9YBRcIURkNh4VDsGzRbggukHwv6SqRV8q9bzk4ao1lSYT4Kr8P5ohv/6k/8lbmbdJKdgN4YGA4CDw8WqK/l7w/urMGGH1FsYKPylT30ff/jwWaxWuWwwOSUKajoxc54JmwTd7SV+8/k/xqHpRXWhhpI8hgqSazD08+/Pr+F/vtrFV54+K+/SSR4e0od8hLKBhnkWgXZ0La4ld6de6ADb5f3nyvCC8UA0VCFYND4+J4Lxiw6qSb8r6tWd7TmWdY6d3hKLbgnM8pjPUxkHd1YCpUO2VWPQrzDsrOC8wqrNcD7pQWEdJU3QSndsn3ko5WGUh3XrcvjModSNtBK/r2kpfydrMXXcvZPf62CgE52pVXLeT4+PxUHARPXQcDiE9z7CHC40jFm7v5ZlGcMDqXxJFYipYozALg1bTkEQF+xcTPIazK1GlRo/k6r8uMggfOFiisojlp8LPS68uIijao2ggiqydJGYLsB4TS5cCB5SNSKfOQ0PIzygmo3hoCl4JTRlvrU0FyKPFJgQbm2G7VLdQjDBRPipEQrPYZ1xgZ+CJUJi51ysE+dcdHZlfbAuCcgIW6l0evjwIWazWTT+4PMSUlFZRTDJRTshGT/H+6R5vZqmicoyPg/VOAAiFE4dZK216Pf7EWalKqLUpTldCAPizJ3mUNyEYQz5u7i4iPnTTk5OokqPi+HJZHJJJZWqdwkl2AeoxHn06BHu3LmDnZ2dqGTl87FfsCwEr4TUqRIxbX9Cy/Tn3vsIDFgO9qXFYhFdw6mqatsW4/E45vSkYqzT6US1XlmWmEwmEdim6QGouGVfJbihSox1z/tyzLJcnLO63W5UInJccB4DEMdt6trLeYiwkH2D4yWFrbPZ7BJYtNZiNBoBQARFnA8/9alP4fT0FHfv3sWXv/xlLJfLS/2XIZvMX8h21FpHQ6U0rJcH+yKVf+l8OxgMcOXKFQyHQwwGA3z/+99HVVV44403cHR0FHNevvbaaxH0np+f49GjRxFcHhwc4DOf+QwGgwHeeecddDod/OZv/maE36enpzg7O8O9e/fw+PFjXFxcxJQAzz33HH75l38ZX/rSl7BYLLCzs4Msy/DkyRMcHh5GteQbb7yBx48fo65rDIdDTCaTS0pE5ons9Xp4+vRp7Nvsg+lmCc/hpoC1FpPJBMYYvPDCCzDG4F/+y3+Jk5OTmJs3fe+kqjvOWTQkUUrh4cOHuHnzJq5fv46Li4tLmzEpuGd/Ho1G+MIXvoC9vT186UtfivM9xzAV7t57TKdTVFWFz3/+8zg7O8PR0RG++93v4uTkJL5j+Z5J3280YqHqn/Mb521+lnNUqsSmspDPx3LRTGY2m2GxWEQDHirx0ygDOk1zLuO8vru7G/Nu8kgB3ybATFW/LDPn7VQZyfPTVBEcO7x++vMf5/hYAERtAVWJ8swtJeyyXYlCZLkyeGg1miqDMh6L8y70JIOxCvlcoe16WaDOTcg9JyFhVCUSDIr6UIlyK3zxl5xWUgZCOadUzIckv8A6lyHkdMI7AVg+GEJgHRLblQWzatVawUcDlxC+xVAuAgVlRYXH8C4qJH34XBT6EHQqAXS2WMNMaETlHsNSqTqDAizVhrUSMNfItU29BoZtT8qhWoF3QChLuDfNIKicIvzQLdaA08jvbB7gYLEO5U1hpqF6T63rgLnGCHm9gSw4kxBsQOCObgFXCfjxWkI/ywsF/cRg/EwnKojqoYqqyZjrsABaE4wpxi447yrUWyrWdR5MLDBZGyoQzEKrmBOPiiVTS/sSvjVDv1aqZKwrhWIq8MIWCp25i/3GermmV0oAohX4k608snEFV2TrEGYeSvq064XJMVNotyzUD4fIALQ9B7fdInuUx/roHVtUOya2Sb70aHpiTEAzGWgpvzcK+ZnBos1h5lpyxgGXHJ+lj8jz2EIW17oRQGo7onTV2gtoNIEvMHei9xFIAoDPDXym4HIFWxhklcDJtmsi/CO4rLcMTC3hytWuEaOQyqGYWNhCo+lrFOO1GU5WSZ22HRVNbfKFODu3HQXXAWxXIZvLOfVQoekZyTmZqfUg9AK92O+LqYDoeiDzRHm+Vmt5LepGhk2bWpSdbakAx3sp1NvS7+utYPwTxkXbV5ccwdmHlRWVo27FQbhzCozvaPzyzn388F89l7QLAIcIdmWceiwPdAgR9pJHdJnh67PnMNqeo/EZhnqJw2wKABjqGpXP0NQZVlWOme2K+m21zqFqVkDXiYv08lD+T1BswxxmOx6rPQdsNRhsL3EwmONG/wI3OxfYyef4dOchhnqJ39g9xW/8+a/CQqOAxYXrBTXkEtZrFNrCgmq9HGdfuYriEICxMAuN3rGFspI3c3FD2qaYdLD9/QsB5VlQ7oXQWZ9pUR+WGqttjWbgYPsO+bmB5nwWQpmRIebrgwfUrQXaeR6hW6Ydhlpdcjn+qKNqc5mTko+Y8Ey50vi7138X/0v3l/GH792B32rk+oWDXwVZqccaJmrgV++8gf9g/01wgmi8xbdqh69PnpONNg1Rw+cOv/fGy4AD8kENk1nY0ohauVFiuKIEusmEIs8bHZW1qPYlJ2t4NxkPlE7CrNv1/MnUId5Inlq90HBbLVTh4BoN07HwDmhnOU6aIbZHC0yrEn5loFqFfBrmmUbq9t/8mW/jv394F/NFiYsnwwhPoQC9u5JHJ9TjOyW8R5TyGGYVrJMdRM7LFjqG1rNN4RRc+FmWOax8UKl6hcZJ/XNz8qcKxI/fkYbqnZ2dodPpXFJecbFFIwkmuOcin/CJqppNJdfmgo9HCmBYDv6b+fLSxVoKDjc/z0UKF5lp6B4/y2ukwIALEC7wuRhLFYVpmB3P5cI2VXylaq9ULZEu9gHJFUmYQnUVoQqhZwpLUuUUgEuQLFWMpYuvNJcf4c2mSpOf2wSTAC61VboY5XmEvFzkUqHIsjAkfjQaRfMXHlTH0Zm3aRpcXFxE9QvVNQSuhMgEAQTX/DwXtwSfAC4BSoaJp32DAIdtu1wuL0GdFBg0TRPzFrKPpG0MIIb5L5dL3L59O+YYJFwcDAYx7P/w8BDz+TyCr9T9OVXQpVCwqqro+EsFT9pGm6os/m5T7cexmoJFHilMZThuqgzmZsKmkoz1yDKkuTMJBlMVEsvDNvPeRwBBtWC66cCwfhpRcCwRJBCupf2dYyjtd6zbTSOSdHym8IZlz/McvV4v/kxrHfM5sg4nkwkODw9x8+ZN/OAHP8CNGzfw+PFjWGsj3GTfoxEPQbMxJrom7+7uxrDrNLyc/XlrawtZlkUVIDcJRqNRVIfu7OxEh+Rbt25FOE8lZa/Xw+HhIT75yU/GcUwDi+PjY7z55pvRBZkq6fF4fCn/Z6q6ffnll3F2doYnT57g/Pwct2/fjkDq8PAwAujDw0PMZrOobE2V1ZxHdnd3sbe3B+dchIxPnz69BKDS8bm7u4vBYBBTa2itsb29jXfffRdvvvkmftRB85N0M4BwGADeffddDIdDPPfcc/jGN74RlYVpCgiWwXsxB3nmmWfw5S9/GWdnZ7hx4wauXr2KW7duYblcxlzBxhg8ffoUf/AHfxBBOfsdAXwaosvxTCDPsqb9ledw7krHJg+Oh+VyGd2w6XTc7/fx7LPPxkiD09NTTKfT2G+HwyHOzs5w+/btS3P5YDCIILGqKgwGg0u5RTne0/mD56bvbfal9Ofp5geAOBbYZgTu6WbIj3P85APEAI7E2VghW0hIorKikMOFRg2gGNawrSx0TK3ke34evpyERQ4XhTSAiG64DEsMX/i5rpPQUwV0ZEEmOQoFAuhKoBPDAG0w1/C5hFlDhQUlHZ+xXkv4TBQqhkq2nPdaK+to7AG/BnUET+scTyHU1iMaiRAguRxosYZxhKSEd5LEfq1M9BmgV5JjMGskDM+FMDRTKcnZ50SR6EPZYUPotUdwIQ3AkwsutVYCZgsvCsruenHN/IbRWZZqvZAHUTlR5zG8G1T8JOo/3QjIyuYq5pJM89ebKqg4y6AozeUZmmWBPECjZqAuGU6Y1gMh3N3lEFfffA1ji0lQna489LFD2xGwaEsVAa6uALMUkLTa9ciCMpJgzHZFweQdkE+VqMaCoKUZCKTVtRcloJdQ3FhvNXNuhT4QlJYCiB08w31bB7QWUArNIBNY2ldQPYum45A/zcXtu9vCFdLxmr5CthLwZGoJa60HUr7ekYvw0hYKqx1RQHqjcL7qIasUdC1KQXZScdhV0I2E86NBDBMHQh0H0FX48BwMJ/WQf9ugpFQK3hio1iOfhiS2CwtlM8yuZVAOKOYeUB7LfY3OqcC3bCX9whmg7ms0PS2hyWUomxXjmqYr7ZctPYq51L3LpL2LqTwzoa1pgnuzE8i43BVVreQlldFORe2iq5Atkrki9KMUsJqVwMvVtuT6zKcexcwFV2yP7lHgQnq9EeH1epNDt4j9uOmJKQ8U0HREObv7fQv3V8b40vFd9J6IyzMg+SCj4roV2Np2gjIwhDKrvRWu7Y/hvIL1Gj0lL95dM4OBR0dZvNL5AL3eCqvvjKTNc6DZttDPzVGUDa4Op9jrzHGtM8YPplfwxg9u4uDmBfZ7c9wdHuNqMcFuNkNHNxjqCgfZBB3VwHkNrRwWrsRILzH3OQx8BIQd1ULDYaQX6OkVnrTbMMphTy9gofA7s09hcN/j4iWBXvlEoTxr4MoMXikcfFNcq4upFfdyraWPt8G93Dt4o6FrB9vPcPGZFvmoQplbLN0AZqrXkCw9tIcbNShzi6YNbsUO6Oc1DNQlUxLr3YdMSlqn5V0V0iEYHXIuKQMHhcY7HC2HUACGOwvMLnrwdj3vIrzv6G78THmKhavxjbrANxZ38PsnL+KtJwcoyzZunsACWMl91UqjnWfwhRNluRe46AsX53e10vG5tZWNMZ/JTpo3Ms/SEAhWgLgPTsvZPNTbYSWcttXITg3qjkY+rLF7MMdn9h7hM4OHuJWf4pnsHJXP8H98/1dx8XhL3sHWo5gLmPT9Fl3TYPpkCDPXyFqFdtTGEHAeMQWHVdEgyxvAWo2Vy0KouJeo6VTNGb438Hm9V2hcJkrb8MJxVlyY836DweEUE+yg/2BzR+enx0/6QUUaFyR0C6a6g6qc8Xh8KYcUF/QA4qLyo44ULgCXFw38N++RqqTSRQEXLim021w0pLmguDilwom/S8N2eZ22bS+5DwO4tFClcimFhum1eHxUGBYXmpvqSQARRnBhmJaJ10tDkVlnXDSxLlkWAisaVhDGpSGAKdDl81FtkrZPeo8U+jCcLj0/BZSAAMKTk5NLAChVVaXhdXyuuq4v5WtMF4wEV71eL36eSp001D6Ffml9EzYBawMH5trabDfWZdrWVN2en5/jpZdewsXFBe7du/ch5SeP4XAYoc58PsdgMIh9lXVKiMpnA9Ywg1COY4bGKlQNpQ6vKcTkwfpLx1hVVbGM7LdpGGEKJQBEWMr+z/xxBJDMAchcf4TlhEwAYn9JlYZpyCjNRwgaOQcwhyqVwZwjOp1OfG6WdTqdxrbk/agWJIAh2CaU5jUY4kzQmOZfYx33er24mZEawFC1RjhNgFfXNe7evYuvf/3rmE6ncTwSgBJo7+7uxj4/Go1w9+5d5HmO6XR6af5INy1msxk+/elPx9Bq5p7jH44j9l0+N6EuVaT37t2LSsfZbIbJZBLDzjle2raNjsxp3rk0VJ7g95VXXsFXvvIVDAYDnJyc4ODgAO+99x6stXj48CG2t7ejupgqb45XPivH6M2bN1GWZXRJJjhLN304jvv9Pq5evYrz8/PY5whaU9Orjzo2lbfpGOL8Mp1O8cwzz6Db7WK5XMZyp5+joo9hzoeHh3jppZdiOz948ACPHz/G6enpJUU7ndm5+cS5h0A83dTZVB2m6uc0/2P6bmE9FEURXZq73S6ee+457O3toSgKbG9vXxoTk8kEAGIeTm7yMIXBcDjE0dERDg8PcXR0dClUXmsd57lUtc/5mOX9qNQT6btg8//pJiE3WEajEZ577jm8+OKLmM1mGI/HOD8//xPbm8dPPkBEUCtthNqKUYCADfVBgXqUAdojW2oBh07BUiURoB8cAU6inHMh5NR4uOCMjLDIIcQylQA1W4YQR4YlmqD2CY6nERDGHIqyQNENQaW4M6qVijmjogNkKbCL7qcuXJugjIo+s1AxPx+AmJ9QHIKlXmIYs5W1oU1DIyHKRGfk88yFSBWi5IILxjBZmnsQgJb8YVQPsi2kMhEVgDEpf4MY/lVvK/QfOTQDUS/mibrQlsFcxKzhUlo3VIr45HkZ5uiVlEnTTTkASFFB+pAoUOrerBBdndvTDlRfIJHemDvbjsBlthvVafkCqIMpRbbyyJZOwFirYBotqqagsHMmKEjPPUylA6xa11l5LjkFbSll1s26L/lM8i4aI7BOh/Bm5gljCDkBVD6XkOX3/toWBg+Awz84hy8NfG6g6gYocrQ9E4w4FDqDFfaHc3xwcQWq1hL+13NQjZiZuEytTUMyFVSzHG8e9ZY8D2F3eeZxPB1IyLoCVC0gxmWhDoJhSdOVcWhqvwaLIWxYeYT8mwKxvAK096JoTCfPXJRO2cLC5Rqr3Ry68di6X0Nbj6afQXmPzpkNuUSlv3XmFm3fROCdLT2qHS1GG6UATiCo/IYKTV/Fscwx47VAOjhxUy5m63O6p2499j3gL3yAf1In5YUoGW2pQv5IRNgMH5yx3fpnq5FC04RclKHMxYUPeUPlmbKlR14T0gLF2AeYjFCHAgKzhcL8qsFfuPEO/sXv/Sn0lYoqXh2ALnOcwotjM0GL63h8+tYj/I1rX0FHNRiZBQpY1CEpQOUz1L7Fs9k5/v6r/yXeePk6Gm/QUQ1u5afoqwZaeVTeYOo6GOklqp0MpzcG2DOzmEcRAFziXrWt5YvHFDmmroO+qjHxJR41O7ien8MEaZ5WHiOzQF+1OLZ9HJop5r7A3Ofoqwb/1Vs/i64B7MBBWYX+Bx66sWj7OXTrMbi/xNA6yR2qFVyvDApEH3Llcv4QFatqHbzTsNYHdeIamHE+E9W5wu0bp9jvzvDNJ5+QhwqPp5W6FLK8eVjv1+liQ27ZnXIBDY3GWzg4WHjc6I1x9/kTPFyM8PqsA18ZAXXAGpxZ+fc/+uBz+Af3fxbH50O4ViMvWxSFxezpALkHXOGgGi3qeuOBzIvJiUI0IJPwYnVJFcnn4lyk2nU/dhkAB5iFht2WfInS0Aq2I6pDTAox59l/ip/92ffwZ3pv49msRqk0Bkp2zASy5vjAhoSZHnAdh7aTxfc0FDBvS6C0cI2Squa0sQl3wzXW5ZdNv1w5USACkLytHj1dy888EHNMKqCTNVj5DN4rqK6FHzb45effxL+z+0f4H3zhu1j4Ev+Lh38Dyv3odv7p8ZN5cGGWKskIFOq6xsnJCa5du4Znn30WVVXFvEwEXwDiQjU9UtVhqh7Z/F3qxpsuOtLwtnRxkYY4cXEFXM5nyPJwgbe5qEmBImEEf8d7pjmmNpVY6YKHIV38wzKkZUpzIaamIwDiojAFoylsSZWMKVRIVYWEXFwQUpHDME+ek4aGsdybbZOq2FIwyLpg27PeuBBOFTPn5+fY2dmJqh0qpuq6jlCJIXJ8RgAx/JzgdLlcxmtQUcg+y/Dlra0t7O3tYTKZXArVplvsfD7HZDKJQIfPTWDJfJsErWm7AIhh1rdv38ZnP/tZVFWF3/md38GTJ0+iio1GAFyoe++jqpJOxs65qK4iKEjDp7Msw2AwQFEUMTcd4dvTp09xcHCAxWIRF+ypepTjgP2egJX9OYUTKTwhwEj7Psc1wSifgTCPhgdUwzE8mLk8ea8012Z67dTghEpH9iWGZxMmshxUGTFcdbFYoCgK9Pv9S+MiHWcpUCYcI8wjYCcwTMvAsE2OBYJ+theVWQR5BNF/9s/+2dgvX3/99ag+TE1v2F8IX5VSuH79OkajEV5//XVMp9NL4ducc+ienGUZnn32WXjvMR6P8d577+H09BSz2Sw6TFsrhizb29tRCcxckOk8yHklBYc0p+Ecl+apS+cRwqvr16/j4OAA7777Li4uLmJ4+3g8jn3l29/+dhxjNNfhO4bPx/l2f38f1lo899xzmM1mOD09/dAcznbe3t7GwcEBOp0OHj16dElR+69TpKVmIinM4gYJ4eBqtcKzzz6L8XiMJ0+exNyf6bvAWouLiws8ffoUvV4P9+7dw8nJSYTB3W43mhFRaZjOQ2l4c7oxxH+nG1F8RoK9dOOJ7zuWj2Pnzp07ePHFFzEajaKq89GjR3jzzTdxdHQU80I65/CFL3wBN2/ejBBzMBjEPsD7M6Sf3xvSnItsU/ar9J2Qbg6m7410/tl8r7Gd9/f3sb+/HyH+D3/4Q+zv7+Pw8DCW78c5PhYAUTUEYyF0dqEiiFEOyKeArrWoXvrBIVf7GEqm67XywBvJ5wcIEIkgLixCVMinBwU4+AjYBC6uF/wSDo2Yuy6W1ap1WBTDCrG+NjShooogEl6ekU7BdE2N16CjageXw5NLUboRaiIAMq/l98Aa8hDAxRxdZVBoOsAE51apU4E+yilk8/Xn+ewxt2MCOyQHFyJwFcUhQhhlAIoQMJctfDBnkXpzwWgESICsE5WWDqHJtrOGipu5pXQIyVb12vFaoLAsfl0e1H7hmbzxKE8UiguDtifhv2YlIJBGH01PcvF5LbDRakT1GdvS5gpe6/W9IIqYVqsQAhug48zB1AKpovLTSFiw1/LztidGHV4DvhCIbAJQhRfVpjMCGm1nDbtMcPHNZ63kAVwKMPKlEVOIoORzZR7D3G0HyLLwIIS9UOKIuzBB3euha4XMSkeuBxIy3PYk1DX2WyXGFdkcmI87GAbg6o0BzHoceCNKxrYnz5HP1y8g5nc0tYxLE/o/oTm8h88FCqK1IRRao+0aaYdC2gxdLbkjOwJBlQPymRN4PdTQrUI+d8hbj7YruRi7ZxZurEJORFErth32fwGdTS+o+voqqAQFOq92FOptBVP5NXgP8K7tBaXiyscQ/XooPyumEqbNDYgUPOWzEObdAbyROjErMWDxSsBeceGjMrftqUsKZtnsCP2zI/lKfQZs3bN4/G+t8MPpAXqPVBxn2ULyL7ZdFUxbJAcoTVds4eF3a/zszj0AwMgssHAljF6igEXlc3RUgyZMgLly+ER+hFxZzH2BXFkc2QH2zBwXtocb2QRTV6CvGnSyi/gZ57UoCTXz/GlMfY5jO8RQV7hqZmi8RuVyDM0SfVUjVw45HDrKiTISYmqhlUMeynbherDf3cZqV/qUWWj0j2Ss2EIjXwRlgdHQqxZoWvhcXKK9gowfpWKI+vy6wvatMf7Bq/8F/rf3/kf4/ge3g4EKpH+G94EdtVBLg8fnW+jlNaK6TwHdrPmQAjE9rHdY+AazKiRCz/wl2CVGKwYWHv/Tw3+FO3mF/8k7vy5uy8Aa0oHvFQ1fONx7/wCKJiaVQT3L0DQayng0u8GAJZyrV1QvKgGLTgEGERwqJ2k8lJP3CILZFud/lwNqd4VOt0FdZ3DHHUB7qMqIgo9ltAq9W1P8g1f/C9zNM5QqB5DD+stfcFhX1gOTVQfMoxjNxJSHyhwcFFRUCao19IOPLFHeW8nvw3cC9rvW6viM8B4rl6G1GqrWMCvAdVRsjl8avoFPfuohOqrBg2YPAPA7s0/i7/3xF4GLAr2nolzHn7y5/9PjJ+ygiiBVJqWhgQBivqw0zDAFaDzSxSWvtQlmUgjHv7ngSwFhCqjSELPNUOhUecH7bcJHLghTQJeWOw2dogps83MpuNzMP8VnSZVmaXn5B8ClhS7BxObia/NePC9VnjAELW0r1kUaBsm6SeuE9+Dv+SxpqCDhSwodqUZJlZBUwaTPlOc5Op0OqqrCaDSK+c4YSpeG3KbwNjVPoMMqgWKqBE2hzOnp6SX1JUPw2a+pUmQbss+kOf4IdXhQWUkIxLHBxTPdg9kPiqKICl3mN2Q/Zz5RKsV4X4ZFEwikkA9Yw9/VahXdn5k/M4VjfCb2WSq3qPiiWo65KVP4TYiegqU0fyKAS3nZAMQwbua9ZJszJyfHLaEclY+po3KqjGX+0U0lI9WxwNrJmWH9DHlOQ67Zhpswg+Hl6XilWopAot/vx/7P52Hdpjk0+W/OEazXbreLq1ev4oc//CHef//9CBd5HrA2cWI+Te/FFIZmKycnJxHk8P4pvJ/P5/iDP/gDfOUrX7kEfFMIxvmPhli8VgokU3iYzgM8NjcTUkUyIa/Wkt/vlVdewdOnT7G9vY0HDx7gmWeewcXFRczvmWVZBGm9Xi/ONx8Vyrq7u4udnZ0Y7n/t2rUIWQHEdu12u7h27Vp8plQBzdQVqVJw86CamrCX7c3n5XFxcYH79++jLEscHh7i8ePHl/oV72eMwenpKb7xjW9cAmf83MnJSQTnqVIyreu0njlvpJtY7LfM70nzotTwZz6fx8+zzzjn8Morr8A5hz/8wz+M4cmc29K25vU55xAmp2pm9v20T6WbU1RUp30rzT2avutS2Mv5+M6dO7h69So++OCDmJ6i0+lgMBigrms8ffoUOzs7uHHjBp4+fRqdwn/c4yceIHoVYFrIp0SVGcGfC0YUAs7W4XhIVIYEcTaEIotCTUCe0uuFf3SFhJzjckIsJaJEA7QdH+Gej/kLRVkYcykCURWoQmL9uE5k2LMTOKatmGJE9VkAZDH/H4JiLri9WubbInjQawVSClVTpR4QzFRCjjMXXKHpJsxyE3jSlVUuFp41gE5XYB36HRSAykldEAS6kNvQrMTgwawkp5rLgXIc2iDcp+2qdT44Qty0/RPQInWdwEwFwCQhoSGEmlCHsEvXag15aoVi6rEaKawOLeZXs/iztqOQL8VZWHkxoEjrNs3hZzuARTBJaUUBKfnwJDSb5Wu7KhpVqKBkyuYuhlm3PY22UkF9uQaFUj9BudcXBSf7pfICfLOlXDObroBM49b/ZwrVOKiN3SSf6/gszZbHwFg0Tsc+CuWRdxs0rULbMcgXTlyFt8VwpJgJXLLBndoWCk1PIBUmYh5izvK1AZFGNF3RFtFkxGs5Rzkf+5vLQyixUcgXHnVfIQ/mOwIbNBTl9VpBNRbaSohhtrBQzqDtaDFfaR2yCmi01Gcz0DHcm5/RrUfT09EkxwRToKYnnZDK0qan4TXQuXBwmSgJCW5tuQaK2dKj7a1hM+EeHOAKFccH55Im9H2GqysnykKOBWWB7qmHMxJmrFuP4pHkMbRlMEqaSj0x7N6Wokr0Bmj7QD4N4de5gOh6oPErL7yBf/bVz2HUSJ/SrZxbb6k4rgCB8FCAK8Q86frhBW4WpxjpBQzEeflATWGhUNkOjHYYqgYmqAw74UKVy3E1F3DYURZ7Zo5cOfR0g8obOK8jCLxwHQyVTB6nroup66CjGuSw2FIrWCho5dHTK/SwwtwX6KCBg8LKG1gozF2OPEyYHdVgS63w/zj7eQweeEzuyrOVpwrZ3MKVosbVKwuvg8KwsUCeweWS0xIhlN4bcchuBhnansev3X4DPWWxsmHHvRDw7ZSMI+U1tvbnmJz20evUWNkMqqFDvSjX9IcSlX740GkeBu3Ryxo4ODReIGIOg5eLGifWY9nma1gW4BqcKGV1C7iVCe9EI4YhrYTvuo6DqpVARuODsZiOG2/e+6iARPr+0l7C7Cca9cjBl15SeGiPwcEcWnlML3pYHvWgtxogRgKEsoV7+cyjWhawUMjwJ+eEBICFN1g2OZRX8F7eK7ZQsWyZstC5gwXWYcvaxxeKosmMRlRoxo1A5bGVVdABbtIEa+VyeK+QXVng2U+d4TdvfA3/6Mnn4bzCcbuF/+TdX8LT02240wLZQYVfeeEN4KKIeZB/enz8jk2VDReJXNQQPKTqAC4O0sVS+jsuQnh+umjjeen1U/CWKhE2r5/CsFQtmCodUpDFBVGq+gLW4c68Xrrg2wSGLFMKOVOFXgoR07Jxoc1rpwswlovnEhikKhIuxNLw6bTevPcRynCxSfVhGhqb1gcXzJvANc2ryLKli1nejws65sFjTrd00cj6Gw6H0b2bYalUfaVhe2m4NU1gUkUaoUoanpjWT6qYS9ufz0VgyXBUAi1Cc8KeFCikEHU4HGI6neLRo0c4Pj5GlmUxxJJ9hvVw5coVOLc2m+EYoJswYRYNM9hGDG/NsgyLxSKWl9DFWovz83NsbW3FsrKfpX+nsIH3Zp2xftP6Yv2xn6dQgQt7tsnm+EhVdUopzOfzqJyksmgTEvCYz+cxHJjtz/MY5sq6Sc2O0o0FgoMU1FGlmm4kpEZCbDMCJvY/1j/L6ZyEL/M5qA4lbOP1V6sVtre38Yu/+IsRenz/+9+PEJ/K0hQGGmNinxuNRiiKAo8ePYr3TVM5cNym/2e9czxtKiY3FdIpmOXv0jB9KoMZ9pxC1HSe3QR5APD5z38er732WsxPure3h/v378fNGo5Vzk1s781rey9mMN1uF2VZ4sGDB/jsZz97ydiJZe92uyiKIho18blZxjQs+KOOTeVlWq+cMzqdTlT53b59G2dnZ5fKwnLzPKpw099Zuzao4u85T/Oz7EtpfWgtofvb29sRrE+n0whtaTKVAvnUFCrdgKKid7FY4O233/4TwSphOeddXrfX60XjG9Zfqtrns7LPpJtW6XuRc0EKGbmh0u/38Uu/9EsYjUY4OTnBeDyG9x7Xrl3DnTt34L3H6ekp+v0+jo+P8c477+DNN9+M4/DHPX7iASIgC3yzTEJbQzizwIwAt0I+MK/DAonCDYZHQkKRlVVIk6T7oFQ0lXTutisLDknw7mN4llc+KgR1Q8olbsOX8pIF1VgEVgE2xvBp5ukLuf8A+Qz/zfKmyj+9AhCMRmiQwHxoNl/fgwCHrssEckCApNn6mmmILuEm1YoeiKYOkscqqE5CeLDLgjLGqZin0WfrUEie23bWYcP53Is6ClIms1qXN5/46ExrwvUo8zArH8NoN1WdVPNFcNrSiELgTtOXzzJkO6oVGeJdaQwfWlHrKQBKo+kq2FwF92uBB/l8rTLMl0GxGOCeQL9gApELAJTckio4YKt1vsZaYKPNVQSK0v4+OHCHkOhaTFu8UbClyD6zpb8EdWnQklVeYFWpoWsr8NBhDRHzDM1WAXhR3rV9CderagF+eqXghxb93goXizyo7UxQRspztBC3YG09FgeSuyxbenTOPGwBLPc1TBWMgayo9Ah8vQLMygEDLY7RzkufUWvwzTyOYs6QjB8HwHvA+egm7XMT66AZZDC1Q2fewJYaLtfQjUd50QIqKAlbL8rMYQizaTzKsUU9kBDrdih1yPD5tqtQGIGNbVD5mZXkFXSFQNd85lGOJVy97QrUM6u1QjmqZo3kV1StmJ3QvChfefhWwKnLgNWu1C+ha9uTEGmB6yqqISUn57pPZisxWzEroP/IJbBfIZ97LA8VescOT/+Mx5NqC8O3TVQoF2Mfx7uupe1sLqHrEe52HX75+vfQ0Y2oDdGIUUmYVLZ0hVxZTH0O4yUXYq4cxq7E1Wy9y3XhSgx1DevFfMJ5jYnroK9XAFr0VY1j24dWDpXLsafnmPsChbLY1g2mPsNFgIoGPoZIz71AQwOPuStxaGawUJi6DvayCf7pDz6DkRMlpWoVek89dG1hOxnMysIrBShRmqFp4bZ70rWsg8/M+h3igwlPo/DnBm/BKIiJRujDLuQZREh/MX97BGy12O5WeHyxFedhbzyKDbKUQjMHDw2FxnssV2tDAyhglC8xdS0e2QJv1of49vwZvDG+hnunu1DKQxnpU+swYekLthsW/E5F45OowK/02gwFQDQZ4fkqbKp5iAIxOCVnC4ViHCCjIWwDdK/F7LSH3SsT/MWXfoBRvsB/+71XoWoFXwI+d+vNvVAnrlVYuORZsVZlbgJFDXFL5hHnQw8o7VHqNioxY07K0C7OrqMWYgi2X//be4HUubHoDFtMn3H47I0P8Nd2v4Ff3/06juwQ760O8V9/8LP44aMDvHjzKf7x8efx6L19ZFODzAF50WI/n8EPWvjp2qH6p8fH60gXDFxMpuqsdIHARU6q8klVFKlCh9fmwj8FEmnYcqpaShfQBBzpvbgYT8FcGiLJcnBhw/sz9DFVP6T5vqhc4cI6hZFUjqTqFMK4FKSynGm9bdYZz+V9qHxKoV2qRklhWgpRgHW4HVUnzHUF4EM5HVPFEds4hX3GmAhQ0sUoz00X2VSncAFLpRnrnLngCO92d3dj6K21Nobm0o2YOQx5PpWDbBOq1VIAVZblJcVg6p7LOkvLW1UVFotF/Bzbn/fh4plGE6xPhspOp9OYN475C3mkqszt7e2YW47tTIUbQ66ZX5BKKRobsJ63t7cjlLi4uIgg8eTkJLr98g9BKvtMqrLl86fKUtbvpoKN7ZyG9qfjLQ23ZJ3xdwREW1tbl9TGfH72tXRzoN/vR9hJQEl1Jcs/m82iiQ7HYBq+TPfnTdiWKmPn83kcp6vVKrYBVaWEO91uN/afNGSafXw6nUYAzP7U7/eR5zmuXbuGw8NDfPnLX0ae59ENmOHfHB+sJ4bGbm1t4c6dO9HAJA213twM4ThPVWwsY5qHNp0jNhWVrKsU7LINOcY4Z2/OXQQ+6Zx+9+5dHBwcYDqdxr5pjMHjx4/juQRYNLwhIGaZ2G77+/vRJGa1WsW8nxz36Wc5zx0cHOD09BSj0ShuaGxu9nzUkarq0k2Sqqpw9epVvPDCCzENwXK5xA9/+EPcu3cPw+Ewtj/rJd144zjZrLv0HckypopRKq3TPJ2pMjTdwGnbNuZZpOnJ/fv38fDhw9g/+Dys56qqPjRf/qh6SeeF9OdpH0jDjnnwGdNcyJtKTGMkb+fW1hauXLmCa9euQSmF999/H+fn5zg+PsbVq1exu7uL7e1tLJdLtG2Lr371q3jy5Am01vjiF78Y85ym1/5xj48FQIyhRgEUeg24roevGU7po5mAatUaRhRrYQbDVxmirBsBkUgWglHpRsVCAFSyKg/lCIpFFwxNYgizV/DlWl1nViqalESQqBDD3ORkhFBLxOvTtVh5ARF0vTUVoPn8KRRMwooRHsdna1AGrKEhQ2R1jejGHNWFQb2nQygwlWSqFTMVU8lnYz2HxPpsGx2AYBbMQ9heyguU0bWH6wtQi0YnHsjmQd0WwF8296K28wKpXAgdLsZS3qaPmL9N1bL+143AZJfAQVuuITLhrrJiUMKFnc896oGOOfeySowraBRS93V4DgltZWgpoAU6IgDKpcCc+DMvfTICMkh5bCEqNVcoNKHfERgzbD2rHHRHri/wyIc/NO8QILXa1mj6EparnBg9wNP+G/DaRJDoSp2EoAtAnC+LWFZVOuwP5hg/2IbLJcQ2Xzi4XMMrAbhNXyGrEMKY12G6tpQQcYHzod2VONl6HdpAiZGHKIAQ4SLdg70W8sUw8bWy1QvcAaShMwNXGDgjIaiiaBSXX8n3F17ylUO2dLClRjs0MCuPYhIk4V0BoJ1zUZ+5XNohX4gC1ZZKVIgLi8VhJoq8MFYFCAtYNtXaZZs5NrOlqASVk1yEWS2QTj7vkQWA6rV81uUC7WjiYgsf4bOppfM2AxWc0lWco5RTQcGoABfmliDddWHsc3PAZQqf/cw7+PoPnsNOFcpnpb+iWm9KSG5LgcaAXGN0fYKf77+ND5odjDLJvVT5HCMsYeAxQY4cFh1lYxgxIMYmORysV+joBjZIxFbe4MgO0NcrFMpG+CfnyAK6UQYT14FRDj29QuU1zmwPWjkUcMiViyCxo2s4r1CH61tIGXp6hdprdL7dw2oEQAtE7p4GRUymoFcO0ARpApxdERy1jQ4K0nDdjsHkWY36pSVezE8BAKVpY67D+L5gX/BANmzwaze+jf/L+M9Lfw7duNQ/+ouaDheqvHT3OL8a4FunN/A3Lv5tnM57ArnyFs4r7AwWOJ30gZNSnI7zRO1HEB+U9XqlZQ4vOaYQN9D4foxAFICuVMhPqiQP8QrxWW0JNAMv6kIFUSo6hT//ybfwH974LfS0wf/m0S/C1UaY5vq7k+RpDLBS5w5zX8CoD9fLJkg0yosy0yE+Z9w80qLa1NoHNSjkHi2/xDOCIUwuNHSB/MxZjZvFOf53L/02RmaOd+or+O78Bl5b3sb/7c0/g9WDAbJ5+L4xcOjcbjAqltCDBm6pYSqFzDhYrwW08nta+tw/PT5WR7r4TPMUpYoXKjTScwgeuIjaBG2EHKnaBMClL/+bYWFpXiT+nAtdKsgIR7i4Sa+Thnim+Zo2VYKpWo33SxeWaWgmnydVR26qaDZVk+l90vrlAj9VjDFfXAo3NyFPp9O5ZEjRNE3MSUggxXsz/JNlIIxJwz5ZVpp2cLGahp+xXlNoy1DYVAnIsjL0bXPhz35TlmUsN5+R12FePz4boQPrPA0nJZTkPVNlC/sG65iuv2neQC74aWaxWCwuKePYr1PYkKp0Wed8Ht6naRosl8sIUglLCctOTk7Q6/ViWHIaEs5FOduRRhnMY3dxcXFp3LFuqexk2HLaXzc/T0Ud6zUFHZvjkxsLKWBKx2MKajl20w2HNDTWORchIX/PUEz2r9VqFUEv+y9D8dNQZ0LJNJcd75fOC4QzqVlKqhzlzwi3tdZR4UXQSEiU9nGayHjvcePGDbz++ut45ZVX8Fu/9VtYLpfRoTYtI8OXF4sFtNbY2dnB1atX8Ud/9EdxvktVwGldp3XMsZsqblM1Nz+bnv9RkDjdpEmPdJ5K5990HCul8Kf/9J/G6ekpnjx5gslkgueffx6TySSOa85hHJPpmGZ/47WuXLmCq1evotfr4Y033ogwPN2gYb0wncGNGzcwHo+xt7eHe/fuxXJvPv/mkc5vHCPL5RLvv/8+Dg4OokkSHZKLosDe3l7sl2mYeaoIT2Et65lzAsdPmnqBSsdUaU71bWrWc3x8HMfH7u4ufuZnfgYXFxcxN3F6b7ZpCu3o4J4Cvz+pbtI+SPV2WrcpYExD0VOFf9pmLFee57h69SrG4zH29/fjvKa1xjPPPIOTkxM8ePAAP/jBD9C2bcz1yU1Hzo/pu/7/LwGibgRctH0JF1NhwU4Q6LprWKVDWC/Pk7/lHKsBn4nphfJAW3hky7AYJ0hD+O6vApQMQFCvFGzPxwU6VSUul2tGFaMTSJQuKl0eQuGo+gtlJUiJZh06+TcQQ3JdMODQDaI60G0IHOK6KOQWpIKR0BN+rYzyaa/wa2Dpk4WXQwCYhShOonrQr5WKAkcQf5cvJK8aF9GSn471JKpA3cg1V8GkgrA3Xwj4sSH8mYCzHgLFFOvcjst1u7IsnpA2QLiohLSI4cDA+udAUDr2W9i8iIYZtQ9t1PKPD/A3KGcm8hxei7pMQGXIR9dVsB0VQWVW+QhuTSWAqhzTMMQHlWOYxHIBbG1Poe1qUL0qCjqBPRKSquCC0o2Oz+VFI26xrZMwTPaHlitrgW3KidLNdxwy4+CcjqHuvjKY1QXMTAcIpVCObQBZALwPhhseutWSi9MgOgsvD1Q0//FaBdWujtSEORARwuhN4yPcILRvhioCRXiBbAwZBgRCKiftoBtRaLpC1JxNTyNfOOgAXduuXtetAXxXwRYmGPSoGI6eL8RBu+kpZJVHOXHATEJ+3dAIcPKISsBsiZgTMVsKVG8GAdwVABbskwIXkYxxWyo0w1BnBFethMs3PYXFFRlvnTMv/S0A7XwpfQVKPh+NWIy4Q9PYJW40rIC6CzRDYHDf4+wV4LOdGba+W8i414Be+VjXbV+hPPcBzgfn6NKjHTj82evvofI5+rpGA4ORWmAKib0ulUXjDCpkyEMoMgDMfSYqQWgY+BimfGZ70SwlT2BRDY2OsuigxcSXuGpmmIcJynmNM99B7Q2umgUWLoeFxSgYrMx9hqFqYL3CyCxQ+QwXrocDM8U/HH8Bw/sOJz8jyrvyTEuuUC0KThUUxaqx0LMlfL8Tgb2ENXt4Jare5dUOljdb/K1PfRXXsxIP2xUWTQGGyXJjC5D3UrvX4Pb+BX5l8Ab+r/rPypzjFHzukWsLo9QlOJb+28GjgYToxgneeDw+GsFkDp1ujcm0h7N5Dj2VkGSfe6CXmpRgfThEYxXZKPKiBLSixs/GBuW5QnXoYPs+bn6Zlcz5KhhitV2P+tDDF16cmJWHyrwYtwDwxsMYj08OHuNaNoD1Dh8sRoghw1ZJ2HCbvmQF+h23WwDOL9XF5mGURuUNVk0m18t8nHP4rl25DEo7RPUy7xnArleAK5K3Jr8fGo8stzhr+/iHDz6Pp2dbUA+68JnHz/7cD1AtCpnTMqAdiBP1dlGhb1bQxsOGTcdLX838xt8/PT42B7+U53mO2Wx2CaKlC1OqSVJzDyprUpVCmt8sBY+psi4FilwocqEArBf2qYomXfhx4ZsuGNPfp6CO9+fPNgEnFyZ8Pt4vDdVLrwd8tKlAqhhKFzbp4j0NI+SzA4hOyB+lAOG5rF+q1giw+Ewp+OG1i6LAcDiMqqe0LpnDLTUOoAKRQIomFTRo4DOmYc8EfswFl9YFFVhUIzHcmTAzrV8+F0Eh+xKACA2orGTZUnhZVVUMlWafZhgqYehisYhhpezzBFWETbzearWKzs8ME037PgECIVOv10O/34+wr9/vx/yBadjmbDbD7u7uJbMiwmOGL29tbV0CqawTGlWkfZmL81TtlIJZtlca0pxC9xRWp3CJfYVjNw3Vp2qW105BL/t7aqhCIMf6YPsTytDko9frYTgcxrHP66TzFNuSKleG3rL9qHpk3aTzCmEe88URHDLcWCkVlYPpPMAxm85NAHD9+nU899xzEaR98MEHl+aA5557DgBwdHQUnWJZjzTFefToUawf9lXObT9qzuHfKcDh/1PF9mbaA4Jjzok0EiIoJ8hMIVR6f547Go3w6quv4utf/3rsYzs7O3j77bcvwUPWJ3NGEs6mCrytrS1cvXoVe3t7ePLkCV577TVcv379Qxs5LMtgMMBgMMBoNELbtjg4OLh0L5Y1PQhyabpkjIk5PJVSWCwW+OM//uN4jX6/H1WQs9kMJycnca5MN41StS/nY0J0qu2oPp5MJjg7O4tzJN85TdNEF2/OO5yn2I+pnOWc98Ybb+Dw8BC9Xu+SKVXaZnzXTCYT7O7uXtr4+1HH5mZZquberE8q+znuuJGyGd6cboLNZjO89NJLyLIMJycn0SjHGIM33ngD0+k0bpqkmxFpGT4Kkv+4x8cCIFIply1UyHUXXqJNMONwgApf4lW7Dj1WdFfOJdQ4Cw7GNpd8iNlcRWVWthR1m4QWBxVjx4v6RwGeasIAFBEWIm3Pw6zWuQRdz8ccZ2YZQAGYAytxN1UCDMwSgJbcZXRB1ZZhwqKsIPwjRIugk+HIcaGZwDVQrZT8n26r1VqpRIiDkA+PdYDAf3S9LpcPMBCQv3WDSwniPZWcIcySZjQEfcpy8Y7o1NwORBHk1Rru5PO1uq9z5oNyUEUwWW9BQjiXPoZ0207IN9d6aCeqLkl0hwBN1u0cQa6T/tA5dmv1nJbQa5uLqs3lARoqGm3IszLsvJj5eK66EDBDJV3bDXkTS0QqzPx/cr6HaQBtfTB/8WuzH40IjkRxJ8rFtotYD/CAmdRApoOJgxaYWLWAdYBzaA+Gsd8uDzxUaWNjUQSJ0kKHsH4XHKCblRGw6gX4NV0Bc7oRYw9dUYUUFHwzH9RsTtrYrfu7LUPbsW0Vlb1rEKtCiL0KOQmVg4QuAxK+DECtGijr4Eod+rZHMXaSa1ArZAsnUKyrJdfk0kG3IT+i81ALMVRh+wI6mva4TGF+aGDq9RiotwzyWXBTLqW9eydODHGM9KvuqdzT5gqm8ciqENqer8ds21VxHhHAnQDhbK3qtV1gebhOpyB1xnZRgBIIn898VBoDMkY4L5hG5rXVjkLnwuHOFz7Av3z3eWzNfATlupEyzW4B9bMV1Dc7KMYyHqnmzg6W+Avbb0r1J5NIRzWwIfdgriz6qkEOMbC4cCVyZdF4g1w5aPgIFnle5XNUPoeBw14AgbXXWMHghpnFVHuVF5/lXV3hwCxFzaha1NCY+wwFXFQvGnic2n4sYwGH/+qPfx6HpULbE+jcfyxOy7ZjJKQ+DMnI3DrZen4267HqM435dY18Z4G/NPwuGm9Re40muOt64y9BonZo0d2u8MUrb+N2VsBanWw4eRwU00s5EI3SWLga77UW99odfGvxLL49vgljgprQKqBR8F6jnWdYHHXk/VZ4uJ5bOwx7BKC5LoxaaYGFmY9qu2xsYFYG+QyhH4VQ/0bGaOdYlL2u9FgeeriOA0oHlTn4WlTNMD4assT8jsZDa4eeZi7LJU4WfaiFCSp9H0KXZfwTvlqrcWoHAM6RHilM5N+N16hbI+7Q7To/LRyglIQwO6ehahlX+ajCjb0xrvUm+Najm1C+EzbIpF6Z6gROwVoFB4VHj3eAlUFugXbbYb+cIcstmtzBr0w0tcmUW4NwLRuEnbzFdlDqMkT7p8fH82CoLhechHwpMOCCIlUypYvYVK3I66Q5CHlsKiR4nRT+EUikKrQ0THETGFIBmappCHfSEMI0xHFTLZgCwx+1INtcOHIhzufUWsdw1KqqIpTkNfnZFMxy4b6ZQ4vlTUElz6MTbLrAttZGlVaqFCQwowKPYb3MB0gFYhr+m7Yxn5+QC0BUAqb9Jf35crmMIJKAjWDMWhuT+PPaZVlGWJf2lfl8HuuMC9UULKSqnqIoMBqNACCGGbMNU4jFcMpUqcc+RqUajSj29vbiPdgfN0PvaZpyeHgI5xzOzs4uhcGmkIH9gPdM65nPD6zD01OFHJ+Fufh4UGlpjInXoKqSSlTWB+s6hTgsW6q4ZZ5LPm8KThkOmapiWY60PxDIsR6oyqRKcz6fYzAYXIKx1opZCAEiy8++xzFO6EtIkaq40nkpfSaGufb7fQwGg9gn2R86nU4EOIR1dA/mNQguy7KMIMs5CTv/8pe/HPsYwRwh7c7ODubzeQSW/X4fV65cwYMHDy4B/FQ5vQmDU9VcChHZDt77SwrbNOw4HVPpPMO+nObEZL2mGxmbas8XXngB/X4f3/zmNzGfz7G7uwvvPc7Ozi5tCHGeJZhM8wiyzKPRCFtbW+j1enj33XdxcnKCmzdvxnJv5tTb2trCtWvXYg5Jfs57H1NVMFdrv9/HcDjE/v5+dKYuyxLn5+d47733Lo2/tO7Ozs5QVVXsGxxL6fhNATzTDNDwg2PeGIPxeBzHUVreLBOHYZaXJi1UmZdlifl8HjeL2KZsg+FwGOf4TUUl25DqSrqN/0kH+3sKfzkOWF7mAi2KAnfu3MHu7i601hiPx2jbFm+++ealuYT9k3Vx7949lGWJH/7wh2jbFp/5zGfiM1BRzs0FAt/NMqaQ88dRVabHxwIg2mK9WFYecFrgoVklqrl2rfbyWsF2BRxKfkBxiTT2smEHFTu6FeCnW0CtFNpuyEcWjEh8BniHdWhyEXLQ+eCYWyG6GutaXXIVjmYhPUTTD5fLIj/CO4WQH24NCKOS0AXYEiADDU7imj4oDF3ijOuDY6YOzs7MrxjrxwgsUK2A0dRsxWlRIOUzFfMRsjzMu5iqFamWVEF52QQFGpxCtqB6JeQhDKGY0VSiQQSvbTeUUyGGC1f7HsN7KoLRbOFDrkUJCa23BKj1ngqw0u06JFVXgA55Bm0p5bSFioY58IDKHObXFdyRRlaJ0ktMUAQEmWYNB21QHkpZRW3oMgEMzHGYz31Uc2Yrj2wFOBNUhwGGpUYs9ZB9IOzchVx8zEGXL3yAcQDmAtuUk7BrW4pqU88rtHsD6LoFnIOqQscwGqpu0PYyUdt2FNotCTJVysP7dTvq3EFB+nbbEdA0+MCi7pugNlVrdVpXoUr6AvuchFN72FzDKObu8xHOFVMP3fqgzAxAtFUoV07qAbgU2u4VFaBSxwzpNrMVOh6Y3+pJezEHaoBtxdRJHQ01VlsG5cSiGLdoBgb1UExViokTWO099EoexBYSxlnMHKC0hMorBYSQd1uqS27gfD5bqGgsU5sQHrz0ESAD0i/0hY/O3qJCFuhHFVXTl2tI+HEAjKW0uVnJHCTuzQrorDcTGEauWgk1t1aAd/fE4+ITBq9uHePeH92KMDqapXQUyjNArzoh9DqM5TC5fO7WQ4y0wJC+rsVtOcgpDTwqn0V14SKoDnNlMXUdDHWFjrLipuwKdFSLnm7wpJWkwgtX4qqZwELBwMMFkxQBjHJOriwmroOebmBg4aAw1A2e2B4MPLS2mLgyAspCifNyX9U4sgNsvV5gdkM2m7KpQee0hc90DFuGl5yeqm4BreGyRDEbPqNaj2YrQ7XvcefwFK/kFQANozwM8+haFVzoQ67YRuFz1x/ib+98FdYX0CGVAef2UrVwcHi/XeIPl7fwldkn8MbFNTyZDFEtizX+8yoqB6XBwmZCz8ZNGTCNBMK/NSfm8Ptg3GKmGuW5Qj6RMdX2gHobWB4CdtAiGxuZF4KpyOITtczhTdjpahWyfovBzhx7/QX2OnMs2gJvPLgmtw8KQ+cVhkYWHQ/aHON5V+BalpTJKqBwklMRgFtkGIe4+VR9aJT+kFO1gQ/KaQUfTID4HtLG4WZxjheuHeHZ58/w2cH7+ETxFKd2gH909KdgWw3N/KrRYCXcLGxI5srClBbWKUkB4UUh6XwI8Wa9ayAPoejaOFgl47mTtXBe/xQefswPLjxS1WAKr7gYSlUxqRIwzbMH4JIpBs0leKQ5/QiyNtVNLBMXTfxDaJAqENLwMS5+NtWN/CyfMVVGpPfhAmvTmTOFiJtgYhMopqpA5orjtdLFOP9QGZaCA16Pn02VjVSbsLysl7QNucBKQ2KttdF0gmB1a2sL29vbODo6ii7Jw+EwlqssywiHzs7O4iKa0DFVmrEtWG7mqmPeKwCYTCbRyIQuzQQ0zBtI0Ek1GxfMVJ0RhBFypbn5qEBk/VEVQ6DG+ktzjQGI93TOReDKfjYcDjGbzS4ppliHVFxSFUpItBniSNUXnzc9l3COz3F+fh7DGjudTlTLeS+OvYRgBKlcnDM8mmqq9L5U1BGM8TlS9Wbav7Msw87ODs7Pz2PIND/Hc/gnVcASHnJspONMax2VhczJloI2jp8U0LIfbW4MpMCfP2cf2Jy3WDaGYqemFhwvDDP1XnLecU4jMLR27cTN+cNaMbX5jd/4DRwfH6NpGnzwwQfxWamoPDo6wuHhIcbjcZwbnXO4cuUK8jzHe++9d2nspmHy6fjnM6fzB8d3Cnp4bjq/pOCLz8Z2SnNe8jNpqPdHbbL0+3289NJLeP/997G9vY3j42PcvHkT5+fnsX9uQk/WJduHZcrzHFeuXIlj4eHDh5eU4OmcyXN3d3fxzDPP4N13342uzfw8lcx3797F9vY2tre344bAZDLB8fExTk5OcHFxETcGuBHBeSLdIFgsFvH+6SYW5/nRaBSBWtu20aU9zWtojMF8Po8mSLPZLJqFUKm4u7uLg4MDWCu5P1erVTRSSd+fBGoEqZPJJKo6U6Umj7ZtIwjdhHEfdaTvPL7DON/1ej3s7u7i1q1b0Frj7OwMzjkcHh7Ce4+nT5/GPpbWUwq52SfT+TT9PvFRR/qOBxD70kc977/u+MkHiErgU9sNIEiHEGQn/9ctYMMim58HAB0UCjF8NekLugXUIuRnCzVklojhlDFXEgDmkrJlMEkIeaIkX6CEx2kLoAb0SsrhinBPqkiT/HMR/IXFiO2Ga63WZfEG0XxEuSDeCKDE6lB+H9a74TpmJU7NgFzLduT3hIOOoLAF6p6cJ2FeAjRVcBKW/IgqwiUqrRg2rW14nlxy3+laRRAIhIUy5HO2s67Gph8gqV7DyraHCFTiMwVllS0Bu93CrDIBbpAQXJcD2VyeN1/IzZqBCnngPOqhQJx6SyFbeJQTuShVXtlyDb3gFDonHoPHLVymUA806i0FF4xO1vknfcxDma0EPOgLt4a7SsGG3HW6FahU7axDlAGBQsohgkbdys9sIXCb/cLmAuuUE4jkTbiOYgjx2tm3nFpAKYEiidJUeQ80Ia9bqQEPrLY0kFuooFhyjZY1vPKidgpjwJkAc2uHwWPJmeeNgmmU5F0UBoT+B1In9ZDqPDFHWR7mKI/mEYrCS369NBwZCCHeoXMUk7W612vEelPWCxR1gC8yqBXgHzyG6fegr9xG21FROUuF62rbIKskj6UtgOWeQT3QEVq2pUJu/CXn5XLikJ9Z2I5GPRCYXEzXalNTe9RzHRV85dij7q+BuAuqKqYhaPohzD1AuWZL5oYIFgIcpuLXK8l5WI5ddOLOKg8/l3GQBcVw21FRRVxMfMhHKX1be+kDVD+WY4v237rA146eQedEwQaYma18TLGQzzz6T0RxuLgSlGeFh+t47BdzLHyJAzOJYFCAoYWFqAGHusEHdiCOycqh8A7XzRQNdHBaVtjVFRY+g/MKe2Yew4wJ/ipv0HgtANErzH0GrURduKWrmFtRzFHClwHl0FHi0JzDYQWDnl5hiAo91eI/evJvoPfU4fjz0h7dI4ViLAlTY2g/INBvWcN38gCuJf+haj1crqG8w2I/Q7Nj8e/c+AoGOiS+hUXVJjl3wiYTvIIvHH794Bu4YXo4d0u0jQmKZwUH4J8/+iT+u6MX8eh8G3WVxfHonYKb5JJbFqGfRCMQCHhzMmcpT2dkUf5FWNho+VlQ+w3fMcGRG6hHwPyWR9t38B0H3W2hjUeZWbTzfjCLQQzHVtMM1188wrNbZ7jRvcC07eDtyQEej7fw/skOtPZQKrxjWgWfO2jtoMNE9HZ9Bc5JygTUOoYuK6/g6ewc/hw1QzTeIlfrsItNeAhIuDvnKsXcxUwHoD1+sf8mbt08xe9efAr/yff+IhZHfWRjA1d6+L0apYeAyxjSHepOeXjHxbGVtk/nU+BD/7FeYeEKeQeHOnBeYWy7l8HvT4+P7VGWZUxczgU9F5z8gs8FFrBWA/DfKThgnjUubNL8cIRGm8rDNDQMWIeUUiFCeLGpXiPIoOIsDadM1ZHpOanqKYWXLE8KdriY5LPxWdKFelpXqSIxBZNckKWQMIV9wFrVQgDExTfrcBPysq5SiEl3YartUpiYKllS11Oez3ZhWChhMBVaaQgp64PPy/xkXCQy99ZgMIiqNSrB0rBXqncIHRhaSeUoYTSBGuuHi3X2FcI8giiGBKf9m3CS4I0hiYR7VMTNZrMIUB88eBD7cqp8BRDBEs+9f/8+AHxIzUXVKFWObdtiMBjg7Owsti0VU71eL34WQAw/pImL1jqqENlnl8tlVCluqoUZrpuOTSq0eKSghzn8+v0+Tk9PIywhXGOYKHOY9Xq9S21OIMtrspxVVUV1VZrXkMB1a2srAiD2qc1cmwRzBF0cB3TNZd9JwSbbge0NIJap2+1GBS6fk9dlXyfw4PPw/6+++io6nQ4uLi7w9ttvX4KovPfR0RGOjo7idQlPCZXSDY50LtzcoGD5uQGTKlxTlSc/w3M24Ss/v6lwZD9Nz0nVZ2ku1J2dHbz66qv4J//kn8T77ezs4Hvf+96HNmx4HT476519sSgKXLlyBb1eD7PZ7FI4N7BWErNNiqLAjRs3kOc5jo+PYxg424vX2Nvbw8XFBe7fvx83Pwi2ON7Yz/jcxpi4kcAyEgDmeR5D8s/Pz7FYLLC1tYVbt25hPB7j4uICh4eH2N/fR9u2uLi4wHg8xnK5RJ7n0RiJ11oul9jf38e1a9eiWdLR0REGg0GcZyeTSXznpf2PY7vb7eKDDz641I/YX9LNNc6B3S4T4H/0kb6H2X7pJhTHWlVVePbZZ3H9+nVMJhO88847eOuttz6kmE1D7lMIyPHJ+SZV76fPyj6abkyl5Ug3AX/c4ycfIHqsjUjqoGBLlErAWpUHQBYIDjF/HxAgnBXlIHPjeSOLH90iqsZccGI1q5BQPij7mBOPh27WTqVoApxCCD2mk3GAbt5ImDM0kE9UVP4Bl1V9bU8UFXRIFnXL+jPMxQiIIpNKOhdMUbyW+3sVIF8li1oq8Gg00nblMwSmqgWyRsUyE5q4DLD9EPJrk58bRPWIWa5zBvIc1jdzrEl9hvuadbltCQF6C7k+60WHc9q+hJnxmlRWcrG+2lHQtYSNei3XcTnQPZFFYRvAbD1QqLfXykNTC7SDB3yjUe0pdM615B70AmYkrNUBSkm4augHtqASTeAkVUV8Zqpas8pDLRFVTsyjRxVY05M8dgyJZl62fCHhzMUUiVovqAc7Krav7YdQ1omV8F7noataHIo1gKqFalr4fhdNT0N5YLUDIJgM9HJJtKkbBdvxMJkYXrBvu0LCd9sygNuRKADNSvI4xjyeDtg6s8gWFtV+jrZUKCZhgd+6S/2G/VzqSupEh98r79cmQwhQwMs10LRAngncGXTht59B0wnhQKvQ9qGevJHzdOPRubDRFEVbwCwdWhtcs5NUBC4HljsaaluLwU2u0DZAtgxO3j2pj3zu0T0LzssdUedmK+a5lAEqYdGIZWq7AoM5hnUdnJULYJWrCAZptGLL9flUCyrvUe2u1YPi1hzmpTqEhEfwClilYFpgeiPDF6+/i9/6nS+g30heRI5lcRdfz61tVxynXXB6x7DBGxdX8as738bcldHwpKcb6AB2tPKovEFf1RjqBpU3GGoJUx7bHBZKzlEtOhDoWHuNvmpg9BydMJk1EPMT5sTsKIvKG1Q+Q0e1ogALHcMmKGfhsgRCZtjWK0x9jgYav//9F7HXV3AdB7XS6D924r7cz0WpKTJcmVczA18kMNBRMejhc43lgcLw6hRf7N6HQxnvvVgVoCGQksLJO2rQ4Emzjb97ehPfHt+EbXWExvDA4yc7Ev7rAddqYG4ktUOtBCYawGcOl5SEjYKudUiDEfqGFWWgmWkUY1F7Kw+MP7lOoutyYHIXaHZbqFI2DzLjxAm71WhXGkr5tYFKeOeh1hjeHuPnD9/D109u42vvPwNnDUxm0awy7O3OcNCf4e3Hh1JEbkpYg5GRL19P2m3Y1kioMePEddg0SJ/NKjyptiJA3HReTkFi5XO41GU5nVOUx987/gX88299BtmZqK7VlkM7ki8LivfzoS2oJqTqFgguz/JzyRfqsZMtkBctrC+RHqVuMW9LKBU23MLPNcLcVq9NpX56fLwOLjBTkEaglebTSsPguIhJQzMJ3FKFIc9NYRgXEFzw89hctFDxxoUCgSOPVGlI9Q7DNQk0UnCWLs4JmTYXZun90rxyqeKSIJWLzVSZxf9TBcbPp3nT0gV7Cu5SgMpnooqFC0Sel0JEQp1U9ZGGL9LFloYjm3nWuLDnIp/XIYBkXkG2DdV0rIc0VJMOuYRJ0+kUe3t7cQHINmUoK4CY6yuFC4QsrL/5fB7biu1OaEhYtFwuI3TkcxM6EqwRPiulYrgh+x6hllISfk5IR9fkFNiy7P1+P342yzJMp9PYbwgs0xyLVHHxmqw3urtybACIoYn9fh9lWUYFKSExgVfqML45nlne1L33o/5OgfnOzg6UUhiPx5cW7qlzMfsb8zESAvJZmfORfYp9hvCEZSc4ZRlTMxFCxHTMEsA456JykZ+luo91wXPZ9lTrEohRsUnAxfGXAsB0TuNc1el00Ov18NnPfhZf//rXcXh4iJOTkwgyqcJL+x3z4AESzkyQ3O12cXFxcQk2p+rVVGnJ+SMFM6kSmPWQKnc3y87P89x0IyBV17IfpZtJhKOHh4dxbjg+Po6htDTEYBukY5VlS5Vk3nvs7+9jNBqhLEu89dZbOD8/j/2LGybpfMfnmk6n+MIXvoCqqmLeRUBUzt/61rdiO/MafKY0FDk90vcF56e9vT3s7u7GPk41ntY6Qrm2bXHlypUIku/fvx9BLutgU6FPRewLL7yA9957LwJDKpgvLi5wcHCAra2tS/Wati37YRrun6r40g0vglIqwX/UwbbZ3CThUVUVer0eyrLE66+/jvv37+Pi4iLOmSwv2za9bgo+NyE5gI9MdZK63ae/44bNlStXUFUVTk9PL533Jx0/+QARuGRIQGUAc/jR+RIIgE2vQZw34mwKCBSMrpRp+C9CKK0PYK7lNYMjaS5hpbpVcGGhpL0o8HxYiFBBB6yhmctFzeN1AIZ2raA0Qe0X3ZMDTAEQFUaxO4XFJ2EMoaArAmgMZiMMZ1RefmaCKpLPZ4ugTCwTQEnoxesHoMh60m2o+1DPZsVnWJc3hl5nyfPoNYyU3H5r5R8hWLZcK/tsIeoahoXrAHB0Y2A7AZ4E4GFWa7VabEMPVLshf+JMFKHlOHzpdB5mpdEM1oBJyuWBRqHeEaVf04OE2nnCGvYbgY5ZzE+5dkcGJMS07agY2roqxdxD1z6E2CmBeg4wjagmY77MbB0G6zIVfxZNOyqBdW0n1H/I+UgQmy0aoLUw0xWQTl7eA9bB9YpwD6DZFrWPB7CyJoQnSn/sljVaK/n/vAHqnkdbqnXuQhuUiBmCi6/8X3JLauRzMSgxDdA5WsKXOZT16JzUsKWOOfy8UTGMVxR7Kqoy2ZTaetgcEk5pBeKitVBaw5UGi+tdeLOGsW1HoekLsMuWEpo8v5JBWyCfO3SPJXy57SgJbw5O2ezjbVej7cpzllOPKoDEeqhgCsQcmC5XqAc+mujQnMesEEPS+X+vxSXb1B7dk7DICyC6qNfgmeY4TU8FwyTpM/VW6AvdAHQJ5rWAxxjm7oGWOUBDbkxbAlv3HJ7+co3vX1xF/6GKcwbzjvL5TYNgOiNt6TMPO7T4H37qO9jPZ+jpFRpvsGsWaMLg7iTOyTaEFefwaOBx6srglGzRSxSGziuUyqKCibBPIKHBqe3j0MxQ+QyVz9AL8mT+3XiNBoiwEAAubA97Zo4cwMJnyJWEOE9dB282O+i/WWK5L5/NFgrleQt4DzNvxGmZGzKrBvAetsMdIC31oCS8ve1kqA48/vThYwy1wcI16GlZrOTGYpEHF2ZIf2VI7H/8nV+C1h5Z5mAyh1Z72J6HWmlgpSXsmQq8oExHATEnaRTUSsJ0XekQ8/rmAvh0pZFPVFBV+jB/iAHTajdUUHh/La45qMMVcu3gvYJzCs2kgGq03LPjkOUWq8xHYzI+w43tMV6/uI7H51toa4GG14YTPBxv4zP7j5Bpizfra6HeBAralUERwPC9an9dL8Ba7ceDSkkFXKy6WPkWPRQfcl528FGZ6LwWgJhCuQD8VAKaXSmK+ghijb/8+Uw2pxiyrYyDB7CdLYKiMKg9NdB4g7pO8qeEa7TeYOUMjHESXW6B+aqAg8Lg2gyfPHyCr33zeWy9/a9PxP3T4yfrSNWFqUoNwCUFTLpo5SKGSjzgcmgxFxBUdaRwI1U0UNnFMFDgcogS77sJRQgSuOBPwQPBWKqGSGFEukjazHuYqrQIGFKowDx4BJxpmCTLTfVeGhbIhevmM6Xl5L2ookvDgXl9/jtd8LO8XJylQJQL2fT6XHDOZjPUdR3DK9PyEhSlEJjtPRwO0e/3cXJyEsEuE9+zfVIlG0NAqQKimYnWOoYhp/nHqqrCZDKJ5aZ6jqG4hC/L5TLm96KSkv2K6kOGAjJ0mhCA7cjwYCph0px33W4Xy+USOzs76HQ6ODo6igpL1kWn08Hp6Sl2d3dj+OFoNLrUBoQeHCvMQUi1HYAI5KgmJNRN86wZY3B4eIiLiwtMJpOoaqTJSNqPU3CWtmOq0E1hEvtRURQxhJL9gZ9J3alT5RBzCXJcc+yynQhIUjizWq3iGAOA2Wx2qVwcW3QcT+HyZm4/ApkUlhF2UbmcKlxZ3xwTBJlUJ/J3fK5NJXOWZXj22WeRZRm2trbwzjvv4Pz8/NI8QLDOeuP4yrIMn/jEJ6C1hKtPp9NLADdVfKVAL93oYDn4u/TnnM/YTwlG+fNNmJiCGbZt+n/Wbwp7XnnlFbz55ptYLBYYj8d46aWXcHFxgaqqsL29jcFggPF4jOl0eknVls5ZfL5r165FAH7//v0I3jl/pOezHe7fv4/ZbBY3PzienHPRRZ1hveyDTCeRvttYP/x3p9PB4eEhRqNRNH+iepE/I6g2RkxYqE7mBgfnt+FwiL29vUuwMQWLdEzn7zlHpuCT9cC+xz7FdjLGxHrgkb4D+W/Ot9ws+VFHqvzbPNg367rGW2+9hbOzswjsx+NxNJ5J1YJsO47pVKGeqj43jW/Y/xjyzmM0GsWx//zzz6Msy2jCcnp6+ic+G4+PBUCMCkMVFH5KFm7MNcgchnQ8dVmAiC1g/Hpxr1sBaY6hwU3IYbjycEZUHLKAlLBDV/gIwFQjC/ioggvlITx0BhEK+pDn0BZSVq0EUDgdDFbK4Nq8WoM2lyc5nQJg0fVaXUjASeBHp+Im5GgDEByT15CA5icEmgSENISJsBShDCZRWnq5FhLA44LykWWjM68tg2lEgFtUgAKXoW+2kHNNLQtHVwTwxgUhlTqQMGUoFVViPkvqVwe357B41EGppVpR3LQ9j623xVgkWyp0Ty2ySsF2zFot2gCqkcV4OWmRrXRMyq98CCUOjrveSN5CR8BshT6t+5CAH20RQ4+pwPRKFvcCotZhs2tVqUIx9zC1i6HKnYuweDYhnNdLmHA9WKsfTeOhFzWmn9zHxV2Dq3+0QDap1iGaWqHty2QvRi6yyM8yi9YaqDbAktzDe4XZskAR2thnkq+ve2bRdDXyZRg3Qa2mXAjjHUqevmVfoXvksXW/giuMuAgD0I2FbqyE6CpEpSXz0HmtgoFO+DvkHdV1yBdoNHy3WOdBXDZQthsUc0pMdEK+wLaj4IwWsK0EADY9jaIj13UZ0JZGoKwVFSi8hC93Ti3anpit9E5t+LyKsLEe6JgCoJw4rLYFKNpCVKQy54iy0JZrmO5KhdVI5hXmTxQ4LhBxOZDy5iE/JNu/PPexH2kbTFwIKVsxsalHVGIJPIVWaAYy5puewhdffBtf/tKnsNV6NB0lZk0OQfkaVLLWoxkIgHW5hLoe3DrHr2y/FkFQE8KPc+WiWYqFQuPFPfnMdtAJkmrCxTJMSGNXYu4LdFQj4afwMMrCKI8z2xGXZr2CVh45LI7tEMACWolKLlUhLpyYr3RUg0Mzw8SXiDskAE5dF0Nd4ffOX0Hvqcf5y/K8vccKZuWgVhbLZ4Z4/PMZDl5zGNxfQLUOrlPA5zpuCEh6BgXVeCwOM7jrFf76wdcwdhZPbIl360P8zvmnsFzlQBJuzPx4SgF5btG2BovHA5i5BrpO5vS4SRRMTXQ4pw3gLgl99QRsxiM/zVGeKRQTCa13xmO1ozD5hEK97eE6Fsg8VGGhZvklWOesgp2UgPZQpUP/YIErW1PsdeYAgLdPD1BbJerAkJ4DmYNWHoW22Bku0PQ0nhud4m7/GPOmwDCvMAg5Q5RV8CH/Yt5tsGtmGLsWj5bbcG1C+rSHavW6bJz0tcesLlEFYJgqEI2SNAv82cT1BCC2cj/lPNpe2MjQHo0zUB0LXyWbKbmTdiLIVAIPo/oQgAoGMM5reK+gsoAilRdTmKiYFOdqrz26usZKZeh3athrCzy7f4Z/+8ZX8NnOQ/zsp9/BaTvA19Tz+Onx8TvSkLI0PxYVOwyN3FTqbSosUlUcF1mEDumilUCFSeKpZuNinYt6Lh4IGQkUCAoIg3guQQKPNH8ewRqfN82floYBcgGV5owinAPWi+q0HlarVUxST/iR3j9VFbFeuZAkWOJCL1U/pueWZRnDQ7lYTsvL5+Bim+Vm6CQXYyx3Xdd49OhRXBSnYWtcWPO8FE50Oh3cunUL/X4/hhB3Op0YEss8e6nilO0/Ho9RlmXMs5jCSQIrtjHDYgnZmGCfeegIE9OQZLY/VXxlWUbVDdWHvB/vwzZhWxM6eu8jCPniF7+Ifr+Pt956C3/4h38Yr8cyMlSb9UEwxrDYFNrNZrMImPf399HtduOzdbvdaMBA8KeUhEwaY7C/v4+zszNMp9PYZlyQby7UOdbYt9Ijhcmz2SwqSNMwSKoBUyhHtRpBzGKxuOS0zHHAUMn0vlRcUtGZgkOGY6bjg4pMglOex37P8U842O/3IwSk0pEKXj4PzYQ4/jqdThyvWq/DwjkPpoph1uVqtcLh4SGeeeYZvPHGG7hz5w5+//d/P6os2XcIeuq6xv7+foTn165dw9Wr/1/2/jxWs+Q87wSfiDjnfPty782beXOtytrIKrK4iqRMyyOxJUsW7DHc9rQ88B/jMTwYAZ5pQGNp3GN4MJAAQx5LgG1AsNFQQw0Z8lhqWG4Z7U3rtGTK6qFFmluRVawls7KyMvPm3e+3nyUi5o83nvjifixJ5MDdGBJ1gMTN+y3nxInt3PjF877PHj772c/i/v37UY3JeSm9HsdiCjQJhVi2FDxzPuHclm6s8FzpPJyCY76fgsJU6ch5ptfr4YMf/CA++9nPRuVkt9vF3bt30ev18PGPfzzmI3355ZfjmCcgTgHiaDSKbsunp6e4fPky3ve+92F7exvT6TSanPBgWTheGA4/n89j30hDuDdz8aUQmHO11hqXL1/GE088EefOyWSCs7MzXLlyBc8880x0hz89PcXx8TEmk0mcRxaLBdrtdpz/B4NBbPvFYoGTk5OozNvcuOL4pVqd8wqhd6/Xi+pctg0V9sPhMKqmUzi7uSHFfrVYLDAYDPCHHekzMlXgpv2GczDrc3OsbALtFBbyOylEZBoPvs82STcJbt68ievXr6Pf7+P4+BiPHz+Gcw5PPvnkO85vf9jxbQEQgbVqTta1AnNMFRRCDHGmKUeWKO1omoH1+1SLZStZkOs6/GSIaEtB1UBm1dqxWEPe9LIAMtVaceeM5AN0jeSzS92OodaLTBXCNHVYKNJchLkGCSnoUqtVAv5CaDOVcAgqRJ3kOCQYZU44KgAJOqMRjVvXWxqaHctJqGiC0jBASK8A7dZAN6oVhU0hX4haLuZurBBVmghglLnwCOtcyLFIEAhIXjjJEShwTlkBVcD6nrxO4E0efvce1Y6FXmiBwrnko2vaRuBVA1EZKlFfmaXC8kaD2aMMLpfyKyfqO1MJyDOVj3VhCy0w1qjYLi7kPnS59Em4AIj8GlrTLVo3Sa7DYq04rDtreNZ0JNzaJDkDTYmQyxGoPJVuAQD1FFa7HuVOgfxwFr/jixy2LfnX6j7gWw5KCyw8Pe/JIhwAMglrtI1eGwxpacf2SQi9LqQM7VMHnEnZnQG6hz5C0mIm4M22DfJZExy5wwmDAlN+UgEYDGJC/4IOY0slULHQaIpOUOBK2KdZWYHFzoi6tAKgfALWHXStxX1Zh3auLMqhWbu2NkIPvAGWOxrKBhWiEqWyKaUf2J6OpjqtiQ+qZ4X2iQeUhCwzjLlpIaZIMJVHORQlI0KINs2XXAFUhbrghN5clnB8BLWynEPqpelIqG229HFuanqiRs0X640MFXJvdg49Dv64RWkz9O4neTxLH0PDde3jvNAE9aPXQDO0+P7rr6Ctaxg41D7DWC8iwBvoCqswmTLU+Mx1MdBLDHSF2mucuDa6dFeGidBPlIku5E/0aKsGc19grFeovQ5qxmUIdc5hVIVB2IXQwaAFAFY+F7MLOExdG5U3MMqhrWrsmRKf/upz2Anqb10ptE49dGWhrEU1MGj60jY9o6BWFdyoE9XG3igo7aGtAL/ZdYWi3eC/ffgncO9sC2WdQWuqFpIQVYWwI6DgD1pY6pbMZW0fwL2AK0VwHpx8qcCLGygOkiOwCO8XDvo0x5Xft5hey7C4onD+rDg9q06QzQdg6byAPM4azJOojgpkV5d47uoBAOBs1cHpooP7R2Mw50JMOVGFgliFfl6i0A1OVl1Mly20jSyWMuUwypZoqSZCRxgfAV0Bh3NncX86FtfmcI+SgiKUzvj1/OMV5mWBdcDhH340VRbvO6ZFcIAOLszaeNmDUljnh9Q+fDicJOSI9JAxxbyKR01fYCgPrzCzLTirYq5fWzggc+hnJf6Lrd/Hwc4AZ7aL/zh/Eq+uruI3T1/A7/7+88hmGp2pjL+Ec797fBscqTKHi/M0/xgPLma5sEgXGHmeX8j/RfDAcE0ucPj5NKwzhWlc/HHByp+ECDwPF07MXUf1UKoQZJgjAQ4BSLq4IhhMDQwI7whxNvM2ss42Q71ouEBwmoY/pnWVLrCo4EpVQqnyh9dIF0mEfgwLTOtmU2HDRe7meVLYkUKZtFyb+S8J7pi3sCxLTKfTqNBhHZZliW63C2NMVCCOx+O40FdKwofZhxgOSSUR4TNhD1VrqfqQsI4KNf4/BXAMJWU9UWGXXiddzGdZFsMxuYhnvsbpdBoVQjwv4abWEmZMNZlzDtvb2zg/P78AzNM6J/wiPE/zcxJ2ERz3+30MBgMsl0ssFosI5dN8cyk4SB2bUzUSy5yOe8Je9kuGDaZwmiA4Va9prTEajeLn2dYEvKwHgkPmdKRijP0eQOwzRVFEQEGFIw/WBdufCtG0bTl3cN5iOxIkdrvdeK00t2oacksgwbpk/+L46XQ6cM7h6tWr6PV6uHPnDg4PDyNgTHOUEq7zmsYYPPHEE1gul3j77bdjSDqvkaoMU2ifQrB0/uA9prla04NgJ4XYbPdU5ZjOqwTR6XyShne/+OKL6HQ6ePPNNy8ob9n3AeDo6Aij0Qi9Xg9nZ2cXNjVSSHT5sqSMef3116Op0/HxMd544w3s7+/H/pX236qqMJ1OYYzBZDLBfD6/cM+b0DAFoqkKNwXh73nPezCfz+NzaW9vL4bGnpycYLFYxA2etP7Zv7TWUYWptcZsNoubPcPh8IIiPd1cSp3E02duGuqdthPnaGttdIfn53kejs20L3ATiNf7w4504yc1vkn7SNqGaX9Py5yCyPQZm867fM6xza5evYrd3d0Y1t/v93FwcIBLly5BKYXHjx9jPp/jve99L1qtFh48eIC33noLk8nkj7yveH/f8Cf///RYuy6vQ1fNCqiztYkKoVdU8KWKUiX59HSl4GgAcjFUHU1XXH0BxJx++QJRNcZwQW/XMNJTDUkFnF2HYxISmVJFuOc1lZIeLvPxvHWu1iAOWJu4eLk/lwtU8DooCl0CFcP9NJ11GVmmbLEGhroEfMj/my3XoMPlQN33ktOxEZiJAGrrPqKKLsLbUCbbDs7TCtDBHZTtk60SkElwaaQMVHyYEqgH61x0PiwE07p2mXyXije2D8Fh05XXu4cOyx0thi0WUG2L4lEWFCoC7aqhQj0Euo8EshBaCThW6D9sUPd1VIDZlgp1r2Not1ybilNRApnai1ougAHbVtHwRfLvIRiiqDUoUGsA2Dp3yOcuGqUQrrqgfmTOxbqPoNKTdjXeI1s4uHaGwf0Sgzc9zKoRM4iqEViXZ3C5KNjqvgYyB1NYeKfQeqmDpiegW7cbbHcWOJt0g/pPwj4JlmwhSlHbAZqOEQVUgM+AgM3u4wpeKbhcoxqF/GPWRydll6l1yLkRZaiMNS8h3hfGuwesh0JQHXpxcVVOAGM9zAKcXOfyY77IuqfRdAzyhUPnqIkqQXigt1/HMGpduahCtIVCHhSd5VCj6Uq75fP1WGQuQyqPxX1dwOFqrOKclM9ELVgOdcwXGR19vYNZqagOzoNBim3JOKAqca1WFVdrKMBpAbbMHao84DvSL3SzBpGcd77vw1/Bb37leWwFdbCuZVzWQZGvK6mzuifnVQ3gu8Dw6hQf6L4F5zUKZUPosKgKGVLcDiYqYn5ikKsGtc9Q+yYoBmv0VJOAxuDQqTzmPsPK5wBWMPBoqxptZcUIxRsY5TF17Thvz30Wr1d7A60cxmqJXDk8aMYwyuHM9tDTJdqmxptNH+PPFVheAnzuYCYa7TML3ThUVwboHlS4/tsZsrmFXjXweSZzEvujB5zRMKVFPchR7jjYow5eqq7huWuP4bzC4+kAZ4d9mLNM8hVqCEjLPFyThJtQid3Iw8L5tcpPKiQALRuUpVTFIYA8MQFGtlAoBwbnHy9lTtNeBOraw1sFVxvUKwMYD92yorALakjOt9/55F28cX4Jj0+GcKcttC4vsLc1xV5vgteOdzF3vXUKixD+PMqXaJyBDiZLV9sTaOWhwkBu6/rCNeIDAsCJzXF81o/3duF+jRdDFQ0JJW4UyjoD90vSEGbrXfzdwWPqOqB6kTmIvQlzttOYNC1RElLtmRrSsJiNCr4pSvJWugCDAcyaltRrqUPaEYXSSfj2lVtT/GeXvoanWgf46Te+H7my+KXTT+DX7jyPcr+LfKIx/OAxnt06BLyKz4x3j2+/I1UepGCsKIoIxVLVD2ELYUea6D1V4KULjlRFs7lw5/+5CE4VPywPFxmb4U9UHW06D3MBzrCvdHGVAjourgHE6/D/XExTnZYaD6RgM82FyNcJ4NIQtM2wStZjGqrIsrE9UgUVz52GFfJ7af212+3YLoRdbNu0bjZVSrwmwQ0XwAwPTR1WqdThtVerVcwLmea1JJBhf6Gihvn/ptNpXLxTNZi6E9d1HfuMUiqCyV6vd8FwhqonQpuiKNDtdiOgZh3O5/MLpgwMXQQkbx/BZKpe/exnP4srV67g8ePHF/JIMgcZrzebzeC9v2CCUBRFhEipC+pisbhgDMTfWd8pqKOC6/T0NNYbFX+bsITtmIJfHikkShVPBBY8d5pOgH2UfSs1J2H+NY7JNP8kf47H4wv9M82BmipcNxXEKcxKy5/OPQTWxoghCduEKq3lchlVqJsKLY5NqmfZh1jG+XweQ9rTzY9Wq4UXX3wxluMLX/hC7M/peOZ32u12BJp7e3vY3d3FV7/61djn2T5sE7ZLuvGQgsN03PM+UkCWXp91xoPnSsd+et401UGaPoH1kuc5Pvaxj+HRo0eYz+eYTqd44oknMJ/Po+HH5z//eVy7dg1HR0fxHtMchCwrc9gBwP7+PgBxemeY/pNPPomjo6MI71h+pi7gvaXz9+YmUwpP0/c5N/C+u91udEh++eWXv26OSPsL+116DAaD2Geo2B8Oh/F9KpnTOqWCmtCcz8d0IyHNq0vgyde2t7fjnMs+sAme02cn++4fdqTlA9bK//T5m26SpXML54d0juHziufhOTiuR6MRxuMx6rrGzs4OnnvuOcznc7z66qtRrU21+2q1iiHyq9UKr7/+Oh4+fBjv7Rs9vuUBolcCnAiiRIWDmPsPXlR5VHGYVQBRVF5lEHUPBGxl9Rq+OQMoLa62rqciKNO1SOoISVIjD+VEbQivQi5CWUVFF2KbuBIH2JDPksV7rdB0fISOrvDRlMQHmKhsgHmQ+/OZ3K8N7qnKJuYm0TUa0ZyFYZxeSTio8oAPxi+uWOfU0xZQc7U2fNHr9bSuBYjYtrrQDrYFUXVqOZf1AbZohriuQ41j0nqISYzyAEoJwUUAvk1bPkM1KXNGqqDKNEu5/7pHZZi8b5YCaLKlg2401EIulN8v0D4W6GNWUn4X8j/CE0BLyGi20KiuW8z3coFIwWClc+ygG/kdHhLequScrgDqvo6uyqaS0GJNJasTuFRMHdT5us+w/zHfocuBxa40fLYUIKobfyGPXrYEOichLECvQ6oBIJ9JJenSwpwvxUAFALSsnn0hANEroBp6CVf2Cp1uiekTLbT3MzS5hz8tsD8YwB61kM2lDW1b2tEZhda5jwYbNJfgAtm2JH9e02qheyTgshwomEojn1pASz5Ihncyd6QHHdIJU9a/K6/gwz2Ko7eKENEsLIrzBk3XCAAzOoSLi/LXGRmzNjex3gmz80LG92qs4QqD9olH+1RUk3VPQzdA77EN+Rkl7LVpa1R9FcdWMfexDQgmbVshmvZUAv68CTC5I0YsYo4i/+8cyznqYJrUOXIypwVFauvMx3BaE3Ikeu0lZLsU1WM1VJJOQYW+01ZocqC773H8QY9p08LgpULCqQ2gV2KAU/fVBafrpqNgA69r+g7ffeN1DPUKFgoT14aDRtuLMYqBxyr8tCHfoADAGod2iK4u0QuhytNASXuqElMXiOGKgYfz4tC8cC1o5bBwEi4LSL65ri5RQF6b2i4WAHLVxLyL8jmNoV6h8gY9XcJ6jVxZ/MzD70P30GL2hITKto+VqGGNRtMzyM9rdB8uAeegZkv4bktcl61LQK9cpxwZ2HGDveunOJt18fLXbiA/kX5l8mA8on2sT1WraMS1dqRX0VUexsdQZU/JNtZzpF4pwAXYO1eoRx52IDCw6QJZq0FTGbhaCxjzgMod2oMSO4M5cmPx1uNtGT/JHyrQQEtb5Nqh1aqxRAu3Lx3jameC290jvDXZwsKKkVbV8nHe3i1mmDTSOVqZRVdXcFAoTAhrofQYAKyCXmo0rRwVNM5sF01l1ven5DPMr0gFIJowdmqDY9fC0/jDD+vFaMYrH+BsKK9TyI29kCczHoSyoa1gAGQe3iLmr+T7w2yF3dEMvUsV8ict/vMrn8cnO3fwycHreGl5A7/2+AW89vb3wFcGiysFvnhyHatJC9lSwxng6mCC7WIBXzg4p8ND7I+4qXePb7kjXTCkCyQuCghhCE/SRU23271gdsIFDscsDTg2E/8TWlExyIUsoVEaFk3wki4ONxVRfC91eOTCcbFYXAjr4/dTIJgevH6q9EsXZKkyhKGYrK9U5cNzpQCGMC5VffE+qCTaBAtpSOtmeBp/cnGZhg+nwHVzcUf1GlWlbDu+TuDFvIEECFm2djrtdDoxDJTlXCwWyLIswiGtNSaTScyPyPNPJhO0Wq2oCEzVQQxRpPKQoXxUcrHv0HmY5xiNRhf6dFVVmEwmsS8R9BG+sc6ZR5HhsOfn5xgMBnHRe3h4iP39/QvKQ8K0yWSCXq8XlUcEbOPxGMfHxzGkdjQaxfrb2tqKMJ3gk0CUDskEboTjHAOEsEqpGMrMfpeGnqZK0rTfpn2VP9MxUNd1rPMU9BMcpBAvTU/AMGQAMSdj0zSx/qnCYt7Iuq4jcE7vjefg+1T1EbqwnFRiUlXIzYSyLGM+OqocN8O8nXMx5JlqKAKfFHSmyjD2zzzP8eyzz+Lll1+Gcw5nZ2cABBKxv7Of0ECH4bZPPvkkyrLE/fv3LwB7gkP2K7YV7zVVYqZwJp33WL50U2Fz3KfnSPtLunGyuYGRgsyrV6/ive99L375l38ZgMx9o9EIX/3qV6N69eDgAI8ePYqqyk6ng9lsHUnGczJ9wGKxiI7GTz31FGazGR4/foyTk5M453H+21RUpuo8zvnsR9yQ4Nybhrpyzmbb8L4PDg7ieGXbj0ajeC/cYEgV9VRQU1nM+svzHEdHR1BK4caNG3EO5fhkiox2ux1zRaZjLG13ti3HCuexN99884K51uYzg3VB2L+3txeB5TsdbHNCw/Szm2p7Hmm/eyf1Ydr/sizD3t4eLl++jN3d3Thvnp2dYbFY4Pr169jb28Nzzz2H+/fvYzqdwjmHl156CcvlEqPRCB/4wAcutD/H6Td6fMsDRAAxr6AKYZDKC8SCXUMzKpyouos/A5CCRlwsMzwWYS2Wz9fmCNFYJYQ5gouOsFAEkoWHB7IZF55pgdfhuC6T6EzdBHWgE4joWh4eYQHK3IP8/9pIcx15FfIiMkcijVOANUg1iZLRVD46HDe99eJYN2v3V34/W64hl1eSH81VQYUWygMEQ5VwRNjnWCcA8uQ8WmCurj1csc732HQBQExGWC6fAaoM9awvAtvY9g0XjGERnilUQ6CYiXKPyr/uvkLTCaYSI4YJexQTFUM5qVrNFpB8V6Hem1xy6THfXBFy00lIs0e29MAS0a20mLnQ1jqG9bo8hIUqnajvpC10I7n9TO2RL/zahdgBOAtlaIfwXQNUbYVqGGCYlT6aLQJkXFRw7Ry6JDXWgHPyTynYbh5C44OJRuaglEduLMZXJ5ifbgEAhq8bLM/HKDywuOqRvzBB9aiP1mMjwNSImYuZrnMNilkNw6jFGMZrBRX6czk0yEJIc+QkWuCarqWPivFO6JcuLO71OgQz5gIF4JlCrp0hn1SAL1APRGmoA+R1LQkDb1oSTu4h5Wpaojps2hpZGVR+HYVyDDSdDHS5RoBxxczDGaAeG2SlOC8r79G0NOA9OsdOQtczFRzPNWwe+tNKVJfVSNSjLqfLuA9lkP7olag6BUZLfRLqew20jxEVkaaWfpatPKq+EP72ibh7M2y7mHosdjW09bj94Qf43L1bGCxk3Kkw1xUzybFJoyhvZFOj6cl5WnsLfPfwa+jqEgZOQFkIHc6Vw9QVsNDoqQpFSFRq4IMhSh5fK+AwQQ7nNbq6Rh4+V3uNeSDDtTcY6BXmvkAFjR4atLUFHFBBwpl1UCgSXs59gbFaRjXiyufYNjOMsQi5E4HPfPEZbG9p2L6DqsV9WdUOTccI8A/tIM8DDdtrCaxVCjAqAm7bNqgGCu23ChwfXBZFemhPnwu8UmEDyINqWgGAyvmYIiLOi0GhCCuqN12Lws2slCi4AXjtA5T3WG5ZqI6FNkE5b4CmzKAPC3SeOcel/jzmMFzZHJOyjbNFB57PJe3hvajtACDXFiooCb3x0dl6ZJZQShTothMApwKQSe6/mWrBaIcikzlm5XJoeLRUI/kCnYJSiCYl3io4r4NSMFH/GQ9kDqiD8jAxUIFVaMoMd6rL+HjrFH/U4R2Bvoq5VqE88oz5l9Q6yoDXdgreajCUfB3KLJ9R2sFZjfd33sYHn34LlTf43clz+NeHL+LReIyf++x3IX+cywZR38MPLBauQMs0MJ0GXmdxrmrpGlEB+i48/LY8uACg8ioFEakCgwtcLnLSEFP+EU+ARRVVqhjiQjUNt0tVJKkRyuaiKU3szvKkOcpSFSWwzg3GXGppPjped3PRnao40vOl3yPk2SwnF44sT7q45eKJAJZ1TPUEoRAXzGnIZBpayQVmWmYuglOgsFwuI9jlopvfT++Hi0wqRHik0DSFY1VV4fT0NBqcsI4YWlwUBU5OTqC1xnA4RK/XQ9M0mM/n6Pf78ZoED977qNojOM2yDJ1OJ9YNzV5ScMgcd/xHFSphF+EeF+nsK1S78XNM/M/FewoVu91uPGdqFJQukLXWMXyb6iitNc7PzzGbzbCzsxOdnanefeqpp2L4PdVDKRhmHRHsdTodeC9OrARpLDOhKoAIulIAnfaXzUX2Zlhs2l+n0+mF3Gup0QSVWYTUqQKOAA1ADC3luXlvHAcpqE7hfKqAYztauzaUIEgajUYXxgv7LVV/BMycAzheCbUIwllGbhqkIfQ8J5WIq9UKH/7whzGdTtHv9/GZz3zmwmf7/T7a7TYODw9jqDbP0e/3ceXKFbz66quYzWYX+hHH2+Zcls4r6dy2qaRM1X1pO6cqzk2gzO9yfuA8wA2kFGryu9euXQMAPH78GIvFAjs7O9GEie3G8vCe2A/TudV7H0E989mdnp7izTffjIrDTQiV9iP2902IyrmB4z69F5o18Z7yPI/gjuOmKArs7u6i3+9Hd2HmWDw9Pb2Qb5IbPxyv3LDodrtxg4J1zLB39nf2x3RDLFXm8bNs07QN2R+YSoH3k34vVajz89PpFE8++WR8LvxBB8/DcZ0CVtYV65ztk8LZNKVG2lZUho/H4wtpR5xz2Nrawu7uLu7cuYOXX34ZZ2dn6Ha7Me1F+vdJCrrT5/I3enzrA0SFCB10FYBcAC8ITqjRkMKtwZa4IBM8BcUdRVoNoqoPRpRWFIVIrj1ZhPCcCO6lzL0HAD7zMEu1BpFmfU7mv2O56u76/5KzTRbutudgZkZyK7UlNFRXgFEqqighPCgCSB/CFalgAhDDKl0u5iMCFwOsaCOqYFwmAEp7FU0xtBVwQpdY2wagVAxd1pW8X/fXpjU0TOHBHJTRzVmFdgshfWKKsQZIEv4dFJ4BisYFH7BW8rg1TDQ1Yrhr05Gf0ycBZU0MhXaZqLxMBdS15E20BdCMPdonKqooJ08DTQ/ovymL3mLqYsh169zF0FpvgLon0KYciZEGHaZFEapizkmvEcxQ5J5tCB+moQqNOVwOlOMQ+rLw6/sr5fvKAgoexfF6oRuVhAMpY77wwQnZQC9qeGNEVdU4wDqoRkIwvRGg1vQFdDL88Py0h96JQvtI1G7nH6ixd/0UWnk8PhnCTHUwelCo+wplsQZ/VAFnK49iEsw/coVaK+QLh9bEYXbNoHsQTGmytety09ZAhPg+hh9vuqJTteSpXoKC82tIb1sadVegbWsi7VUOTYS6rTMLaHFYFndmK+fUEhKq2WfcGihXfVHkSTixjPcqU1BdgccCGRXqSu69GsrndSW5CAkrodaqQoJ93YhazetguFN6rLa0jDkTypCpCOirsULvkQfaAu1XRsc5TepRxXmEeRmzlcfZsxr/2fY93Pv9GzHfIfNwnj6XYXbLYfSqRn7iUI7XYZY+9/jeJ1/FXnaGM9fFnpnAQEBT7TXqoPAz3iNXLv7e1TUWPkNPVagCQSngovKw9jRO8eLC7DO09QptZcN7YlxhlULtspAf0cbvdXUdwKXCyuc4c8BAr6CVw7aZBTVjjl0zxRdWT2D0cobFNYFxxUyjfdIIEOwYFGc1mCtSWQvkWVS+Eh6KK7iHy2XMu8LHjScB2lS+Scirbbu4meQzH3Pcuo6PfVivNKwB4DyKMx3Np1wBNB2PemzFfTl3oq6rNZQRwOYaFZX3mGXA9SW+89o9/IdHt3Dv4Q4AoN2rsN1fSIg0D7WeSwFgnC3QMg0yLRJ7MUiRkHPvCTDDHB7SYixcgXnTgvMKi7KQXIBeYWUzGOVwLRfYJ27HsqmTd2sMdIXXyytyH8FwBFYAbQxjZhlr2Q30jcLn5k/ifzs4vRC2DEgos4OHg8OJ7cPXGprpQhDaxasIRZWW3QZVy9wOB8AIUPXMtxvyJtJMRSkAyuPziyfwS1/5DthZhvwkQ73d4NmPH0qakG6AuV0HGI+tTHKDKgRoqT10nLCTTcZvfLP33eNb5EhVAql66Z0UDemik3/Uc7G/CRjT/GopGEjzJaahmpsLVi4SqehJVWoEcFSfEC5R6ZaqmtJ8hMw5R5i0qc5Ky8XFYQrz0pyNVBh57y+4+fJeGQKeuoTyPngNLiYZcszFdgpTCLH4+iYATBd8rD/CKd4T25CLaraV1us8fsBFJ2alVAzlpKsp64nwjko0AjuGhe7s7ESwt1qtcOnSpRhaTaUeFYzp4po5B7XW0VSE4Ighe4R9NMXo9/vR2RdAVDTOZrOoAOW5CLEIFM/OzqIiiuVnO1O9RvVcCr6plLPWYnt7+4Iastfr4fr169GllaF6b7zxBo6Pj7GzsxNzdhKS8/8pfEkNShiaSzBB1RyNRlJlFuuPUJHn/YPGNscJQQr7Fvss4V0KDNP7TftrXdeYzWZRGZiOZUK9FKimyjKOR6pBOa7ZLlSlMo1Cp9OJarC0nBwjLAMhNyBgk2Yvg8EgmlhwXACIisHUPdl7j0uXLuHFF1/Epz/9abz//e+PKrk8zzGfz/Hcc8+hKAr0+3187WtfQ7vdjnPX3t4eAERjkHT+eieYy7mS108VV2lbpxCK50hBHdv4nYBXGvK7uXmSzjPeewyHQ3zgAx/ASy+9hO3tbRwdHeGJJ56IdcBNCp6XKjxConQzJFVJHh0d4eTkJJaddcE5hXMl7yNVg1LFyDpqt9vodrvRCGkwGERlKZ3NeS4qfZnzFADOzs7w9NNP4/79+3HemU6nKMsS29vbcTOEdcSD/Y7gn/09nS9T0EVQy/ycrVYrzoNaa2xtbcEYg8FgcOFZx2fZ0dERnnzyybj5wn6R9pM0FNo5F0PMB4NBVM2mx+bGFus73YBgn9R6ncM4yzJcunQJgBgl0ZGZ52Nd8G+F1WqFq1ev4vz8HMfHx9jd3Y2GXl/+8pfjuE7DrVMlY1qnwEW19TdyfOsDRL9WoAECvFInXS4WoluwF1jGcFIoCV9NcxJJaG9YLAXnXJqb2JD3LSoag6rMhJBi5jJECFumalFbQK+CSs8nr9chpBhSxujmeqyBYx1DgX0GuFwUgoRnVFwy36LNQ9hxqIOocHIhvBmI+RHF9IOQBFGJpG2YWJZrxWXdE3VeGuaraw+EkMy6t3YVVhbg+s5na1jIwzSAdgJ/lMXaPdb5JMxb7tms1uDTtkPdhhBeZdYh0xGmJteKZjMJhBSnWxUXqaKABKA0srmPdd19BLhMI595oNGYXzEw1TovZRbS4OQLB117FA1QzIKCSSMqwXQTQrBDv6v6a3deU4qyJ1/6CFW182hNPdShQDdnVMz3aAsxU2HezRKinAMkXJsQXMJcG8AL/NNVI6qqskF0N84MbFsUkOWWhu9ZKAUo7VFbg95XWujue0yfUFAfPsc4s9i/t4PuvQx5C6jGDqZSwWRH8vrpEuIInCnJH9iTe+WYAIDiXKGYORTncn/K+XWuTgD53K2ha4DGLqObtdQvlJBDKk3pyh0/X2hkSwvXUlBOhxyZCjYP+SJ7BrrWyBeiHJUUBhrtM4fltoY3othrTZzkIO1pKOvROVmDzGzpJDw7ODibCmJYE8KXa73Oo9l0FeoBkC1CH8oAW6sY2r7OeehhVgL/mpZCthRHZ8L3fBbClYMauZhaKBuUrbmSvgpxlyb8Z77RagD0HgHjjx3gq5Or6D5Q63HivNRNmNOajvy0QQ0JAG63wveNv4Iz18XKFZhrKbRzOuYwbKsGlQJW3qCtLMZqhYFqsArOL23lkCugDGOPORBz5eL/AcRQZB2UYHR0BoC5L7CjltFQI4dDT9WY+BbGegELjf1mFMOs26oWJaJa4H94/EEUE4/pbTFdah8q5NMaPoS5K+ti31K1FfflTEHVLii8pb9CS59o2sGEJAA1paSvwogxiuTfE3UbIP93RTIX9htcvnyOx4/G0NMMcGKcU15ysEMrIc3BkEWF3Iaj4QJb3SWs07h3/xKYx5VqxtFggfvzMebzNnyjYToN3nv5Md47eIz/eHoTk5NeaGQFXeo4v3dNhbZpIiwc5ivkyq3Dwj3Cva77BACUzqCXV/AdhY/17+C5/ADYljb7t9MPrF2jA1gF5L0vT68DAR5GwGqUfD5z8ZrIXTzHG7NL8bopRLTeQUPBAShdDhXbRJSkzPlrtEOmHIrCwm1V2BrN8f5Lj/CJ4R1MXRv/9ef/V0hVgcrK3KsckGXimn1Y9WEXmTjUA1Adi1xZqMIBi0CEQ/h0rsS4RhsHG87noPBgNY7Kz3if7x7fdgcXDpuhxun/Cem4MKDzLrB2Z0zVLIQOVPdwoUrFT6rOI5DjgjwNNU0XL2k47ybYYxn5Xr/fh9Y6hoES1KWqJd4PF9bpfQNrRWWqsAHWIaBUs23WI8EIISK/ny6GvfcXDEM21YWp8oTnSBeuXFARCLA+WD8sz6bSkorBVMWYLupTqLBcLqMRCsEcy0YlIgEeXY4ZRtrv92OY73w+jznpxuNx7GupEi/NV8f7KssSq9Xq6wAToXBd11gsFrFMrGOaqaQmDDRxYR0wtNh7UW0SYLKvLRaLCIzZF/het9uN42E0GuGtt96K7/f7fRRFgfPzc3zsYx/DdDrFwcEBrl69isVigbt370YVG9VSqdKG989wSl673W7j8uXLmEwmWK1W6Ha7EWywDlPgze+lqqsUmqewkmOY7VCWJfr9PqqqinXG3IKEZFQ4AogGKhwjrE/2F4YjMwyZwI/qrDQvKNszDXuniU6aO5LO1WnKBc41HPcMP51MJjHkmfME+0bqAk/YnJpDsK9du3YNy+USH/7wh/Hqq69Gk5wUQDF/J+cH1uPt27fx1ltv4eTkJN4ny5yOPWCd95RAKIX96Xc4d6XzGeeF9HsphOP7PDjv8XPphg/HKHOhPv/88/hn/+yf4ezsDEop9Ho9vPnmmxeuyQ0dzut02U6v2e/3ozs6oR4/k+bS4/yabm4RDG9tbUU4WBQFXn311Tg/0fV6sVjE63e7Xezu7sZnV9M0ODw8jNcAEIEc1dbOOQyHQ4zHY5yenmJra+uCaRCBaKr8JfhkfRB+c25kfabz1e7uLsbjcTSiabfb0Xl9MpnEtucz9fj4GLdu3boAENP+k77G8c5w4NFohPv372PzSPsS+0t6jqZp4r1cu3YNV65cwXg8xtnZGVqtFlarFQ4ODvD222/HPpqq9pVSUdFN0DibzbC7u3tBMclrpX2AY4LtxvnxnT73Rx3f+gARAeARGOWJyg1UTUhoGkJeP5+o4QBE1RiSNQBz1TUdBe2C87IDionkGeN3siXguKJVAiu5cPeZqABtULrRmZghsapCBCjKr5WCEs4awKEGsAL8TMCTbfs1OElCrVQtUaq8LxOgIZ2cCd6kwtYwLqr7IFDPecRwX96TT+sHgC4FqDL8FQBNRiMQ0TWAWqAdlTfKye+6EUBri/X3ARXBJq/n8nWZo2txJbCl7tFtNtSTWYcAE97EFFxtFUNA49hgHao15OQ18hLIVkI59cygNXHI58FgJcDTcqhRDnUIVxUFobZe4KKSMurGo5gRKvsAdpXkvGN9ZaJGFDCoUY6k33Hxqxs5VzF1MZyZuRergY7htbYVFJa1l7xuRQZdhT/qQyifz42wzNxEkFoPQttXGjbTyLTD2YsrzJ41uHTtHMdHA6iXWxg0wOyWA7Yq+NLAzzK4TNR02ULHfGNmFe63QoR0TU9y6c17Cu5A4OrkFnMISDhuupCmipF9WTehTvxatSoAXb5Hlafyog7LVzW6D5aw3Qx1V4wwspVC1dNiSBNUfTZXUvcZsIKWnJsdYLWjUS8F8tn2WkGbzwVcliODYiI5EgGg6kvOxe6Ri9BU8pQqlEPJ85gtRVnYtFV0gQcQDYvKkYJpr8di46UOqTCj67JuZG6Z3sigGqnjYuokdNwHJ2zORVbUcC5XKLcU/vTVV/GL//Hj2FnJvOZ12NRoK7SPPbr7CvnSoQrmKRz7t68doatKLPx6F2sVqFU3hCjnsFHxdxLChwn+LBQcGsxdBuc1zlwXN7MzcV0Og1TD44pZwihg7jQ0PLb1CgufRbVjO6BDqhxFeZjhsBniZn6M2mWYug5qn2E3m4TcijmcV/jKV29i3AvQr1boHkg9Nb0MZkVo5QUgljXQb6/7YxaAkfVwhcbsukE5Du+FuYppC3xwVYZTYsSRSU4+GiW19hbYGcyxqHLUVqM1KFHNTdwIs1sNsNJQLYv+cIkbo3Nc6UxR6AanVReTqo2zZVvaV5N2yY8is2ibBnnRoNMt0WtV2OtMMcqWyLSDMk6cz9NNHYWojFPhIVQ5g15WYmQWsE7HeUqHDStp9wo/fOW30VM1DmwfZ66L//b4u/Dl02vYai2waIpQPz5uOnHX7e3ZOIxheSb7LHyAZYr5EH38/e3pGNaHRUKiQASABha1tzhvOvClhssQndLFrEahtgb/+91P47/Y+Q/o6RKHdojfnrwX/92D78Bb+9twjY6KYJq7xLIrwFmNpc2hOw0cMkBpKOXF3dpKW8c0JVRtegWtPWyoh2nVQjerwDQr7x7fnkcakrkJD9MFTxqaRMUDVV9cVKWhZTwXv7+ZPylVumyqVbjg4udo7JAuNFLlTOpICyAq1NLFD001qLhLVYfAeuHF3/mZNJ9fChIJMtJwvRTQpBCC1yG8SNVhKSxIVY9pKGuqGkyVjqlqKFWP8BwpPEi/myqQvPdxcUggyvOxjpgPMl1cpm3A9qKS8q233kK328WNGzdiCGi3240OqgQ2VIUCAhOm0+kF0JmanbB9CbXYr6hoTSHx2dlZBMudTieCojSfJ0MAWWfsgwyhns/nMb9Z2mdZX4vFAv1+H957rFYr7OzsRNXS4eEh8jzH/v5+BOZnZ2cXFHDee4xGIxwfHyPP81ieLMvQ6/Viv077RJo3MR2jVCnyvACisolHag5DJSfblH2MCmHmfCNQ5AYB4S2vyfG0XC4vqJSpvKXiMN2Y2ExjwGtuAlTmSGRdsJ2oJkwhUwooCTDTPHtpaG06rgl9WD7WJ5WndHrO8xzvfe97cXJygqtXr+KNN96Iqju2/6NHjzAcDjGdTmO9eO8jSGdfSGE+6ySFhCkASueldD5MNzPS8c+5ZzPkNT1P2o+5yZGGvKbtQiB47do1OCeGI6+++ipGo1EEswBiLr90Q2ZTVc76Zf67x48fY7VaXRjf6TyZqiB5D71eD9vb26iqCoeHh3DOYXd3N56bc8PJyQlu3rwZ4R3nfGNMNFviWEvLltYtxxlBaHodHunvKeTlM4v9KzWzInjc3t7GG2+8EefWTqeDTqcTx+bbb78dQSbPbYyJOQM3zWTS8ZMCQM6t3OzZPFKFH8E850v2C2stRqMRXn755VgWgnZjDI6Pj3F0dBSfQWl/Sp//3BBMwTnrJK1DziXpGOB5UyX/Jpz+o45vC4Cow+I6LporgTyicgqLbqwBTrZYu59CAU7LAp0uszyoXouwMZwfIVehWYXvuAA9wuepEGyCmUu2QFwgxvxHWMMDKDHuaILhwoXwKxBAhc/VYdEVwq0I2hh+S2UiQ7V9JrnoAcB31mHJugbMIoBNs75HwkxdSx25Qj7f5CIkEUgrrrs09BClFSK44wJVNwDC+zoodZyRf1qtQZHPAAQFo4SDhXZdbwJBlyF82gtwgwrh0yuPehDyHU7WYc+65uJ+HZrsdTCf8etrQyf1TZCqRM1ZzGX1ePasRudA6qQ1kXYqZm6d39FLDrwUvNpCoRypWI4UUiu3BoxZjQiXoNZmGV4LMK57CEYZAoIkp6CP7aO8nAs+5MADYGYlbC+oxIoMig/zsoGqatiQHNtlCvXAwxznkgtzpFF3KvQGK8zRxvwzl9BfALMnLPROBVglaqhOA28yuHC/Yo7DcFu5H2eDQs8IdFdn6/oVIwkBgpJf0wdX6hAivPKoRmrtJu7EOKLpYG3uk6QeQACLLuRMLCaZ5KQsHczSYbWThbb3yBZh97ut0T2ycOcBKAbH7LqnUY40fAbkZw7KSZi2KG3l/pxBCFHO1qkJQtikKQVi25DSoHMiAInhxRJSHdrUeAmlD+H7+UzyXlbDYPbSViH/ajIXWakTZxSQycZGtvJiiKJUMPcJ46yQduk/cDj5C3NMmg76rxSwoV7Naq361Q2QlT66VxPk27bHWwfbOLnZx9gsoJVD7Q1qmAjoTJCX5cpiElySa29ggiy89garMOm1VYOBXqL2YqaSK4u5L7BwLWybGQo4zH2Oga7QVhJazpyHzKO4cDm0ctjVJRwqjHUpsFKXeG/xCBYKA1Vj6nN0VYkvldcx/mqG1S4A5VGcG3SOqtAPFFqndaBEProvuzzkg9FqPWd7j3IrQzlGzP0q82f449UDKJWo6rSH0wEcgnOOR7XK8Lge4tmrB1jUBU4eD4E8KGQyQM0NfL/Bn33fl/D6dBePpgO8ebyNqsyhtUOWW+gADn1polmYahRGrRX6eYk8b2CURztrMM4W6PKhloBDhlx7L47JmbaiQAxQbN4ILK6a8IeH8WJeFO4n1w1+ffoifuvRezBZtTBol7BOoFrRtShM+AMvADwqDZ1XOFl01nVS8GGo1j8VgvIwvJY5LMoCDSxaKji4JjCx8cmDwqmY1xBAjCzo5jV+d/4e/L/e+A5M9wfIzk1sN9d1UF0bNg6V5EFM6kspyWTZ0lagrZbfXaNRe7MGufxOqNPGazi3fiZstRZioqJ9+PeN/6H27vGtc6TQazOPGt9PF4KbwIwAIF0opjBsE0puOrxykZSG5HLhysUlFxWbi6YUcvI7XPylC8S6rnF+fh4hExc1qasuQU8K3KhqSsOICQ3TawAXw7uBrzelSes0DctkfXDBndYZFZNpfaQKQx6pmmwTUnBRx7KnSlPWOVUzm8YxqbEHF8fvpFTh4j2FOvzO1atXIwxKlTuLxQLT6RQAYghxWkd1XWM6ncbzpC7NrG/W2XK5xGKxiNCMkIl1RjhJBR1fZ/0QWjPcmqCJgDFdGFNltFqtsLe3h9lsFtVnDPfe39/HE088gbIssb+/j0uXLmEymeCtt95C0zS4fPlyBFMEhsDaHISKS6qqaOCwWCxi21Jhx98JF9lXqqrCwcFBhIssO4Eq2y3NN0igyzDipmkwnU5j3dHdmHB2Op1iOBzGsVmWJbrdLrrdbgzP5NgqyzK2McER4RXHCIEd4QOVoHR4TvtyCjGZ95Bjl+ehIpRQhvAXQLwP5h9lWDVh96Zq7vLly3j06BEePHiABw8eQGuNXq8Xxy/7GL9PcxytNY6PjzEejy+MS45bzrHpkSpuOdZSwMXPpCov9mWeN52P03GVKiTZvzefAdzoYD1/4hOfwFe/+lU8evQIs9kMTz/9NI6Pjy+kP+AcyYNtms6nVA8eHh7i4ODgwkYFv8s+slwuL5iEpHUzGAzwzDPPYLlc4uWXX44wn2363HPPYTKZoK7rqJBeLpd49OhR7Oc8UifwVP3JeZxzQbpp807AcRMgttvt2IfT99m367rG+973vjge7t69i7Ozs3j/nJOp9E8VselmGzcE0t9Z3+l8XJZl7I+pIpVHujHIfsB75nPw2WefxfXr16G1xt27d/G1r30tqiTZfunGFZ+1afg6r532SZY3HQvp3xIpjORGyDcDDnl8WwBESUSPmAMRCJDBraECAZK2aydJrk90tQYQdDgltCGgUz4AteziOSQ8WK0BI8OiG498pmIeN1Otw0+5qKGjsssQcwgSQlLN5fKwjqF6LoAzFaBdVBDqBEgGYMdQ2xjma9bvA+vQt2wR1C+tNYAUOCYLMSqgxGE6VHoI94r1HULI6airq6BGdHJP3gG2mwDOLHy/kJ90WaZzcdMWQVDTCffh127OnuHZMwEpvHbTCeUkZCm8kMpQRy4DVMgjaLXkKavbYvjB+yE4MbUAFlEpehTTcI4Aj0h3CYO09TClAKR8KXAwXxBgi5LFZ5Lnj2GndX8d+s1wduZINDXQPrPonEgZbFsHgORjGLQNSra6J/kX85lHMfVQVQNN1ZQLHadpABMUYZ1MoG9HwXYdsplGvdOgu72Acwr1q0P0ThTKbY/VMyV05sWoJHPIcyvtEhy1baGwvKzQPhEYUQ1UCBsW52ia8dB5m8pXFZSVdReAknrKlj72rXwaxojzUT3btBFzZZpVMDzpCnS3LQkLr3sKy10FKAWXa7SPBKh5DazGChjrGPoOL5A4nzvUPQ07EAVe/2EDVyg0LY18KXkrvREQnS8c6lqvXZGnHlVfgKFAXmnHpiuvlY2KUFFZDwwU8uBmLYYzUr5iJipCmyHmjpSxrSJslnlMnMPrjoSgCkB2qLsadc9HRazkTVVR3finnn4Zv/rG8+id+5gjNJ9LN9YNXd+lPW1LxqVreQyeOIfR8oAaaJlQ5q6F2mcYGEmanSuL2hvMfQGjHApYWGisfIY8EMpcuWiY0oOYn2jlRJ3oNcZ6AQMvpimqRu01nFfx/TpMmpPwcxUcr5zXmPsCPSWQrFCNmLr4HAYe17MJfvren0I+k/BleIXiHNC1gw8O3V7R4McDVQ3X78AVBqpx8JmW3KoagNJYXNJwrZCL1Mlk5zPAtR1Ut0GWW5jMoXzYW4eqZqJAU07BLTP89e/6VfwfRnfwf3n4J3Dv3q6M67AxBA1sX5ri5bM9vP5oF77R6I8X2O4vsN1ZoG1qnJZdvH7SkRDmwMp8ArG8l5DecWuJy8UEuWrQOA26M8cNlkzB5x5aOaxsjlUlO1FbxRLXWmd4ujhAkVnUyeaZmBkB500Xv/b285ivCizP2uheq3FreIrzsoNneoc4qXvrEGaFGOa98hlWq1zKwb+5WHavgnQ++UMmvFWVGR42JW7neYSHwBok5spI2Hswo2FOYYJS5xX+x8PnIjx0uYfrW/k8IOYrWLcTVaSSa3UNPxXkdQVAGY/aG3irRZ3pEAHmW+U2amsEPpqwWaIdljaHqjVMuU7V8O7x7XWkf8inebk2w974B3vqCAwgLjDSxWiqNuKih8qGdJFDqMEFW6o+SRU2m+VJAQPfTw1FWA6eM1Xbpaq91LGZ98F7TEOk0pxwwDpHFwFhCqRYvlR9mIJXqik3QeCmaic91zvBg02FJ8+RwgbWYQoeNxWM/A4VN7y/tN5YD1wkMtyVi0PmN0zvg3nstre3MZ1OkWVZzJ3HvIOpoUBqvMPvMzQ9VW/y/tjWeZ6j3+/H1xaLRayrxWIRDRSGw2GsE+ZH5D2leffoHMvPsg/zXwqrxuMxlssliqLAcDjEcrnErVu3UBQFjo6OYh195StfwXK5vAAiUlUZ+0Wag5H9l2q+FLakY4/1lLq4sq15T1x4s1+xnpnnkeXJ8xzn5+dRzcd7pbKL9cPxSXXufD6ParbUeZ19gi7ZBB1sd4b7s09TWZkCTqoeqRwjlGT/Tft1qiZOw88ZmpyCDEJrAPHeWB6CfK3FSOIHf/AHo1L0S1/6UmyvFIik8xDvZ2dnJ7rwpnPPphkFy5jOe+z36VjnvMQxzSNVZ22qz9INnM25I82tmoLpdC7e29vDe97zHvzar/0aZrNZhPgPHz68cK/cgGAOScLuFJoNh0MopaI793A4jK7drA+GnbM/pKr21WqF0WiEj3zkI9jf38f169fx6quvXtg8oWv7wcEB2u02Hj58iF6vh06nE/OPUonHuiSQStuH/6hgT+dyjkV+BhDIvLOzg52dnThHFkVxYQ5L24yhxI8ePYJzLm4izOdzbG1tYTAYxHkoVfenoPSdNqw2YTTH2XQ6Ra/XiwYm6cHvsB+kzxCW+fT0FGVZ4t//+3+Po6Oj2Fc5b+/s7FwIR07vdbPPps8yXj8tezp3pHMvx3i328XNmzexWq0wmUzi+f+o41sfIKoQihv+FpLchmL6oYMhgSyk/VrBoxJVCdafVw0iJIzKwwCITCkrkqg6c2HxHa7XdFUEe6LcUTGXX9NdL6Ri7sMmhD8TIIUjVUd6s1buAUAWQZSCamTl6Npr5WEaNq0s4PO1UobQTlmBgC44Mq+BJyFeuHaoH4YReyPf8zqcL6i/bGtdV7w35oWkOmYd5hc+64MKsQmKLhrauPX581B3a4UN4oIwQhIb8gcuVAw/s60ABLngbTxcAHWEoAx3VlaAF11ngTXYlHBzB7MKYR0LF1SaHq1ziEuuQXAV1jFcvOkIuFJOrq2DUs4rgYP50qOY2KASVdGFWa6tYg68ug+ooYnftW2pF10H4GMFOukzwFQOy50MtiWh16qsoYyJ4YbgxFPVgHNoumIoUvcVfMuhzjxU7lCuCuSvdaAyj9lzNXSnQRTqBndWADDGoSokpLuYOVz6koVyHvXAoO7qYDziMbtmotswVa/VWEL64EMoN+e8MBZNiTg2hT0pWCAo+sSYhWPGa4Ftplo7DmdLxPDbpqPQmkjuxOUlHWAZkC8doMQdueqJIzb7bJlpUbgiKG0DmMyWDk1HoxpoZEuP1rk8ZMqhEWg8R8yJZyrZMBCXaqB95lENBHLS7CZb+mhI1HQBU6oI0+sukC+k3ZueAK58Lg7Pyx0T0xK0zyy8kj5jao/iwEfVshj0GOQNcPjHG7R0A/+1foSOqgrn7643UAD5nflGnQG+/9Yr+Fj/DgplMXctDPUKPV1irCSvjvM6hi4DCM7IGme2i0JZPJGdYgoEp2QJR6bpCQAMVQmEMGjrFdqqiQrElTeiOgyg0UJhiAYVNKZoo6cazJFhblvIjcXK5bC6RBsNCjgcuy529AJ3P38dg+46Z2HnwEGXFradBadwUfCq2gJKwbfMOt8m+7/zaNombvQ0PY9mp8LW7hTeK0wmHbhlhuY8RzOs12AqTxbpDkDm0dMlWipHR1cCzxgym3n4zGG7u8St/inOt9sw2uHm4AxPdY/ied42Y7zu9oBsnQPRZx6X2nOM8yU6RY3cWAyLJQZ6hbauQ+isvwCsvBG49WxrH4MrK7Svyu70h1pvY9/28XvzZ1E2Rr5jZUNGno0eV4szXB1M8AhD6G2PZ8ZHuN45wxv+Ep5rP8Ib6kp4jvigXvTYGixw5rpoqkxeU3IuGC8GMV7mGTE3ERUnGg2VOzinca8Z4nbuvi6EmXNHV1dQhYVvEnf3HID26GQ1rnYneH24C1u15J5CGynjIGkgQllDm8Q5JmtQaQMHyWkI42ELj7zVYJQtoQsraSG8XEsVFk+3D/F6axezdoGJ7UM5ccQe5Su0rs3xXbfu4Dc//z6MvvKt/yfYu8fFgwuhVDUIrEOEuJgD1n/op4stvp6eLwWKm+qDNASS32V+NS4wqHhwzkVlF5U+aZhTuthOFxEpiEzVeWmuxKIoYpglF61UvKRKPS6mCM94H7wGVWKbQCFVBaULKiooWGdUl6T5Hnk+3l9aV+l9b4ZfpwvdNLw6XWxyMZoqwzYBLiEN+wUBVermzLKzfCkkSR2QCVqZKwuQEHMCBioGGfLH8zFnFkFjmkOP52OocZZlEbIRDHkvuQ0JL6muYx5GqtLYHxiu2O12cXR0FE0wUiWn9z4qILMsi/nK6NbKPsMF/vHxMU5PTy+EjPNaDM2lEtVaGw0E2CaEeLxnKvIWi0W8n1QxxLBC9vm076f5QQnO0vFL6M574/1uhgengI/wgPBiPp/HsZiqolgn6cHrEc4TxqQGF6kBUWrYwv5L8M2+CiBCYwI3QAAmVZEAYq7LdJ7j5wmO2SdHoxFu3bqFT3/603jqqafw4MGDOI8RVvN+2CfYt69fv47hcIivfvWr2N/fj/Mmr7lpCrGpGEzVxOlGTaouTOEP58I0hUH6uXTjJd3AIIxnO7BttNZ4/vnnYa3FG2+8gfPzc2xvb8N7Hx3XmcqCkIcge1OpzroEBLbRqIR5YGlSwmcBQ8DTekkB4+npKW7cuBHnsXTjgb/T8dp7j6OjowhHr1y5EmE1sFZgp3Ms5+EUYKXPPs6/rVYL29vb6HQ66Ha7OD4+RtM02N7ehjEG0+k0bqClzwaq2L33MRcr+xLNmAi20+cdwSLnprSfsPyb/adpGpyenuLmzZtot9sXAGIKl9PnDOuS98w5hM8ejulUCb2pFmRf5LOBY2szhJn3wevwmUpQb4zB3t5eHMMf/vCHo2HOP//n/xz7+/v4Ro5vi79eVeOhkzBjeAm5ta0Q0rfy0YwiKsKQQiBpbG0DoNNrcwMq/lwRoJ0SyNe0Vaw9ZeU15voD5NquYG46eW0NgBCVg4SfhGoMxwQEgNDcY32vgDVAuRWUlZUoCF0WhBw+GKYkCgwVymj8+hwm5EXMFmERFYAonV8ZLputwrkDwGu6ogQrpgloyIBiEmCiTtSEQfGoG3ld14g5KlVQTJolovLSBYgDBWTzAHXDfQtY9DBWyl07UfEZK6GnyoX2DOrEGPJdrp2nJeRQxTZ0uYCA1qmoTZXzUXUKJfn0TKVQjZy44ubiuJuGqVN1qOvQ1xj6TUW3IqhRaAIghNLR2IY5Lwko88U6t57L1yrYphLYSHMS9mNvFEwpcExZoJg0cMMujj48wuB+hdbBHKoSUxUA8P0ubEtLLr8xAO2R9ytQZVM9vYTS0pey3EIpD2e1hC4rD+cUvNey+A795fx5yYe49TWLzlGDpqOx2NXIFh6dYy9mMMGIyKw0XBgTrTPJQ1iNpP29ln5FN2mqcOmMKi7VEsosIbgI8FoUQ7r26/D80iOfyzg1JdB/YEOOxNCv5w1sW/Kl5QsHrxSqgQ7KYwk1rvo6qPUUvBbI2LQV6q6CqTSooPVKwKVuRAFZ9RWKuUdv36LpaNRdJcrQMxfDzuE9dKND6LGY8NhCoRpI/7S5Qr4Q4w1nBPYK/YOESLc85pdNBOLOCETMlnKepi1qyHzh8dHn7+LfvPkCuo/WJhgmhD1zM4MbDFSNusIje2KG7xm+LMrAELa88jnaqhb3ZHhUSuAgwZ+FRgGLYXBDXnjJe7iCwVhXSe7CXNyYoVDAwUFCmQFITsUQlkpwWHsj51YlCjgM9ArzcG6T7MBY6HietqolfPllhflNUZfl0+C+3AhIN6W4dKsQvozMSBtZF9IfSJ3r0qG+lGN51QF7JVytgUrj9OEI8EDvyhw/8MIXMWk6+M0vvBBz3KkqtBkNQ7oOh80AwAFmtiWgzEGAlVVQjSgvlzaHdRrtrEHPVBI2HtWXefje+pmjvEJLN5jbArmxyLU4WPd0iTPbxcrmkqsvmJe4Yv3Hxs3sDGO9wGcWz+CX7n0UyyrHsLPCqLVCwxBmDVERegDGw3nJU9nKGtwcnuJq+xwDs0KhLRw0rA+TiFfBGEXUeJ9d3IZfZPKaB1Sj4ZW4ksMhKgFjKLMHfKNhG+B358/hezqv4A86Sp/Bz7P4PJe0EQIDG69R2kyeJ3F3TOrdI8xv7EbphrOHzI3hnFlmUQHitK08Zk1Lysy8jcHwJlcN2qbGteEE/jmFj1x5G//lld/Ctqmxf7mFM9vFb5oX5G+Wi+lp3j2+xQ/+kZ4qh2i0wYNKEy5K04Vt+lqa3y41N0jhZKpCIBxIzS5SZRiBDJ1S+b10AZ0qFtPXNw0G0v8zRLEsywsqxPT/qeKEUISKN9YFF0hpPWwqq9L7T6Fgqj4DcOE9LsRZJ+mCkKoswhxeg+2VGg8QpKRhilSlsa5p2MF74/0RyrFeUxUU67bdbmM+nwPAhc8R6BAkzedzzGazqAqiKzK/x/x/BGwEhgy3BRDPRfhDNQ0hZgrMCLJYd6lhx2q1wnw+j23c7Xajwo2Q6sknn8Tzzz+P4+Nj/PZv/zYWi0Xsv1QoUp03nU6jG22/349mLXyNbZ22y2KxwO7ubqyzj3zkIzDG4M6dO7hz5w663S6cc9HVNA2Vpdss6y4NA6bakHn/0vD6d1IdUYWWhvoTbLB/pUpbglL2edYpVaWDwSD2VZ5zsVhEQEoISfACICoLGWadhs6nyka2D4EEv0MolSpF2ZcIMQhkWTbmd+Q45r2mOSOZ//I7v/M78eabb+I973kPvvjFL0bwxPbkeKiq6kLuyp2dHVy7dg1f/vKXce/evZjDlfPV5iYM7z0FgWkYMOdLvpdu0nC+S2FoqgBL25znY51xvk7fS0HOhz70Ibz88ssxl98LL7yAs7OzCLgJmVmeNF9q+jwoigK7u7tRNea9x5UrV5BlGba3t/HCCy+g1Wrh7OwMv//7v487d+7EMqfzCjd6qCx+p5Bdfo6hvt6LkUj6fOA8myoKWb/sKxxb6UYN5+lU9QjIM2Vvbw/Xrl3Dw4cPcefOHRwfH1947qRl5FxNJ3ia06SgcROsUQF6cnJyIS0Fy54+K1K4CADHx8d4/vnn0ev1LjgxpwpVzmkc8wR+afoMhvuz/xO0p/eVPgvZj1NAm47TzT7HduZcfOvWLVy/fh1KKezv7+Phw4e4e/cunn76ady8efPr8iX+Ycc3DRD/3b/7d/jpn/5pfO5zn8OjR4/wK7/yK/hzf+7Pxfe99/iJn/gJ/OzP/ixOT0/xiU98Av/wH/5DvO9974ufKcsSP/ZjP4Zf/MVfxHK5xPd+7/fiH/2jf4QbN258s8WJBx2JGQambAg1ThQXDGvCej0QFX50DFZAhBcEXgzBggLq7lqhR/djGqH4oEhjLjyqeAjnVFisw4kbsSlF0eHzsIjMIOrCKiiZVuvyu0wKZxrJfwcEl8iwSSzKJ6zz+CXmLAxPpFELVXxNLjeSLX006GAIJkI9REVhqDdeQzVAXnt4JQYZthXq0q7XZ1QEXsi7hTV4AxDhX1QoesB2aDwhnzGrdfmY3y1b+HWYml6D4nxGIwwVkvML9LOtkE/OrAGjy4BsQbUZUJyHsPBQd4srGcpti/xco33q1qqWYMTRdAQSu1zBtgXwUZGamn+YCtAhLJWLVJeLMzCddeuuqBhtS0F5HUIk1/WVLwROZqugpHWiPHSZQtXXEY5nsxrNqIWTD3o03Rb2DubwmYaqpFHtoBUAKdD0AwS0GsY4aOOgjSyMaQAAANo4OKegNcCIaJ87uNxICPOuh745x4nqY3BfoxwL4GxyAd1SJwGcudB2YbyY2qNzKO2ofLjPVQh9zgSgi6szYo6/VgmUY+l3AIGhqH6dAxCUfTRT0G2g7plgNqIl1L0MORFLh3IkE2YxcchnDZqOqDdbE4us1KFdQ4ir16KcBJAvPWoIqGs6AhWouF1ua1R9+VzdV6gslYwCjm1HA04cn+lkDQhUpSIVXvok55J84WBqj3JgYm7I1sSi7mjUfRkXTUsUtbYtIe1n7/V47+Axvvw7z6JVCVzMFqKMbYoEHpq1+hhKch9+/+3XYL3Gg2aMHTMD0MAqhbkvULsM22YWcyD2VB1Vg1QkUqVooWB9htyL43JPNRjrFWqvowszoaINE1hbWZzYblA0KtQ+k1Db8NPAYeVzFLC4bs4l1FlpTF07hjMPdYl//PYnxfF65OAzj+5jhWIS3JdzhXzuRH1oPdBYuG5LVIgKawVimDdX29Jm7rRAcXmB994+wB/feR2f7L6G9xclRrqDH3n0HQKTfDrvBXgYft8v5Q/006oLVWpRKdYIzyVR+jqvYLTDqslE+aY8KiuP6/My5JFwMh4IIXtZiZOqh0UpAHXcasFCo6tLFNrGzymv1upk5fEr5x/BP3vjw1hM2tBHOUbvOcGV7hTXOhPcPdqJIE5XOkK3lc9QOYPKGqxsLnkuXQ6tPKzXEsIcch96q6Bqhcmijd87fgqqlo0RpPkGPZIHx/oHqEZsWTykc01y1N4GF2aPhS2gai07ZSGPrKjiQ/05A6VD7lqlBAxqAI0KeUwvliHN2wgA1ofvBOV+tcpxVnfF8bqW75puA9dotHWN//uNf43bucPbDfDpxTP4+ZM/ji+eXMej0yHqKkPrYQ7XAvCNRYu8e3yLHKn6gAtVwgcudqlWSZV3/E6aMzFVQqQLBC5wAcQFSaoYTBcQwFq9lyo3uFiiWo4AIgWNXFSlC+LNEK80VBhADJXiQi9VnwBrkwfeE79DgJEqM1KlSKoW5EKX0C1VYRAWpsqwNAwxBUQpWGV5WO407GwT2KZAl2VNVW2pYo3tkIbUEvJw4ZfCKaVUVMrwnhhWfPv27ajA6ff72N7eji7NaXsoJfnR0ryQBC4pGKGCsGmaGA6d1gFzILKO03slQCM05P8Z3rdarTAej7FYLHDr1q2ogmPIYKqsOT4+xpUrV6ICksdkMolmEpsL5FRxk4IKwrXDw0OMRiP0ej3UdR1VkIS4bCeGNbLuqPjk+4Q5o9EIDMFNVVWp0o/jelO1SJhMA4k8z9Hr9WKfZz8mnKP6k/edutUyXDl1c+a9E1IRQrE/EsoRIvA99sc0pJ33sDn3sN45R3DOYV9LIUsK2tnnFosFrl69imeffRZf+MIX8Mwzz+DOnTsRpKYGN2l6Aio2b9++jeVyibt378a6SPt7Os+l106hYjoXpCq4zTGf9uV0gycN9U7nd/7/neBYCi1v3bqF27dv42d/9mdRliU6nQ6GwyHu3r0bQS7Lk8ImqpTT+ZYmVsvlEh/72Mewvb2NnZ0deC/Ox2+88QaWyyXe//73X8i9t6mkJEhnWDLVuJvPJcIugkC+nr6X9pFUdc3yjsfj2G8Hg8GFuk/Vy/fu3cNwOMRrr70W8yyen5/HOYvwm/XM/p+q7FIFPj9DhSKve3p6Cuccjo+P4+dY/lRRms4/VPOlSm0em1CP8DdNXcHfN1XwaX2m6ub02Zk+g9P2SZWN3Fh43/veh+3t7Zhe4saNGzg6OkK/38dkMsHDhw+xvb2NJ554AoeHhzg8PMTx8XHMo/uNHN80QJzP5/jgBz+Iv/JX/gr+wl/4C1/3/k/91E/h7/29v4ef//mfx3PPPYe//bf/Nv7kn/yT+NrXvobBYAAA+JEf+RH8y3/5L/FLv/RL2NnZwY/+6I/iz/yZP4PPfe5z3xT95MFFsA9hsRLKSTUhIhD0ChLWG1RGyolqSTVAkyl53QdoGOAU3XC9AsxKrVVsYT6S/IhB9acBsxRlm20hujkDiA6/BCW6EajVdAReqkYAnDOi3HPxGh4+W1NQUeqp6GQsi31Al2tAF91j86A0rLHO2xi+y/yHEhqrIi0BKQYAAQAASURBVJAwKwFgNrjF6iqEVSv5PVvIe01P6sOUsthVjQALr4HlFRXBJuvfrGT9LA7TkqvNFiooEWVhK+HSXnLIceMqLATzaVD+BaUa29PlAnciMA3gTTlAl6Gdw+tNMH7xWhRcrhB1GtWm1VBF4Gpqj8c/UEMXFjVamN4wgJfQU4LBYibggWYvXiM4KquozFxt6bW6VEt9quDWbGoJvzX12syCarIYCl9IH1nuiFqNIbKSN9Gs83/W4T6rBuhmeOJf1TB1WGw0Qv2UFejnlYAm25YxohRQtBqsVjnynH9IO2jtsVwUKFoNjPGwVqCiMQ7IBcg0bYXxK8DE9mBqgVzwGquddc5P5QGbIfYxl63zVqbmRMz5yT6sFx51D2hCHstsIXVOWBpTDYTweyoT6bier0JfDeNHufBHUC7wsRwrmEpFwF8NDNonAjeajkCk9rlDMbGoBhq2pUUhemYjbMuWAn2blrRZtvKou9JmgCgLpR7COc16XlLsczbAuwCG4+8I8Hjm13kOlYSuZ0sHW2g0bQ1tgd6jtWoVCii3DEwJPPHBh/jsyS307yPk9lwrM1U0oxFlZjVQ6w2TvRLfM3oZCycPx7GZY6BXKCAQ78C1sPJ5NFTJlYXxokoUteECbdVgFWzWB8HIoxdyFNLFmcBx7lpoK/kjqa1qrLyBDpNnT9WYQEdoSDfm2hsMdQmbxOXmsKJgVA2OXRdvfukaersKPhdDnO6Bg6otbDcX1WFpQYMUn2fwLSNAEUqUugowjYPtZqgGCv7qEv/1d/4TPJ2f4orJ0FI5NBSM6qD2FndmlyT3oQFARbwGPLy87pQoDwEsmkL6g/Gi1gvPCqNcTBdQNQa1M3BeRbfkThZ2eJyK8wY80DE1xvkS3VaFJwan+N9d+T28v5A/jH6n9V4wB6LPPPRSw+cyp25nc4y6S6xWOVw7w+3xCQZ5iZ1iBmuVKNeD4tiHnwMtfwQa7XClM0UrDGitHAZmiTrkd0g3tOoqw4PzUTQvQRgL0dyF//cBGkIcs5VVcIXDpF6rGaT9LRwcNAw0FOa2JXNtmg8yCijlOaW1R2P8+mEJhNDu4Ngc6l01YTx6IDOy++a8hrUaqGWDJysaPNd7jOV7crwweIT3d95G5Q3+q//3X8Tff+V78cTWKe6fjbEsczjLBZiGNhbj0RzTVhvRPObd49vmeKcQLYIIvs9jM6SMRwoe+ZMwIH2f5+XiiYtWQq5NwJcCDS6iuMAH1iG8BGv8f3q+NBwKWLvEvlP4F4ETVRUEdVycpcoc3pf3Ht1uN6qa0pyIqRKLB4EWr5PmqyNwsdZeCOHj+Vi/XPhzEcsFWxrKx8VZCi03Q75TmMP64OtcZK9WqwgKWG9VVUWVnHMummfcuHEjKs76/T6eeuopvPTSS9jf30fTNNHoJFXQsfyz2Swu4NO8hCwrwSNhEKEV4dlisYhwLQUQaa47noOwdjAYxJDkqqrQ6XRiqOO1a9cuqBmpUGW/GA6HWCwWFxb+6cKdEIyvp7nn0n61vb2N+XyOvb09HB8fx7HY6XTiQp6uvswdl6oCU0jEPpiCBY5X9mm2afq5TbCVjh+lVAw/ThVnDGkn0NdaYzQaxWuw7zI/ZqruS/s2P0M45L04F3NsMJcc1ae896qqIqxLlU6EjlRMEYa+kyoxBZhUFqZl3dvbw+HhId7znvfglVdewfHxcQzPJ1wlME1TLezs7ODmzZt45ZVXYgg9+80mJNwEgOwjaZhxqqpMIRm/z/6VblSkEIftufl7+tl0HgEE8ty8eTOqYB88eICdnZ0I3I0xsQ434fgmhPLeY2dnB61WC9/xHd8BpRTeeOMNfP7zn8fJyUk0ynnf+96HTqdzAQpxEyZVqFOhlip705QRaYqHd9pYSVXwaVqH4XCIy5cvYzweo9Pp4PDwEEdHR9jb24s5Cnkt1qExBpPJ5IJre6vVwvn5eTRw4YbL5ryQztlse6bZYNnYPmVZ4vj4GM656H6dKlJT+JjmWeX10zQePFL1YZoXlxtP6d8EfDbwSFW0mxsk6QbeJiOj0vHjH/84bt26hXv37mE8HmM0GsVQZNbblStX0DQNZrMZHj16hFdffTVuKBRFgSeffBLfzPFNA8Qf/MEfxA/+4A++43vee/yDf/AP8Lf+1t/Cn//zfx4A8I//8T/GlStX8E//6T/FD//wD+P8/Bw/93M/h1/4hV/A933f9wEA/sk/+Se4efMmfvM3fxM/8AM/8M0WKS4+NJ2UE3hlKi7SfQwro+rNFiEHXQhfiq68DYCQJ0/CFCWUlqCPIITQiwBKWUAVokqj+pChvzTK4Fq37q1hlVMBDAJwHQGchJaASsALUEzFQMKsVAQvAKJJha4DFLJBoaVFVZctw+cbDwMVgWU1lH+mCkquOtRFUEpmKx9VlhLyGEwhSNpNgJcxL6GAIBPU/YSQuvZoeuLenB+zvnyEh8p56EaUhwyd5v2UY4Vssa5Dl69Vk3UfiePyOgRbIKJahwo7HyGzqWWx3oyl/mxL6m/6XA0zyTB6XSDjh556C59/+Ukg92idifrPZZI3kSo0W+gYTpstA0z0iGqzzrEDFa9iyKNgA2Bs2krMNlohjyVzJtbrPmZKj/aSIfgBTBoxbWnaAUppILdeHH4bCb1sHS6gasnNJUoqAWP1ILgljhRcr4HKKb3XKIoGeWajm2rTiMMo57Esc/BewTkNUzi4XOri0ksl+vsaqvGwHQ3lPdrHAZIXopLivfqgCmydeHEy7oW+0Eh/iPlCwTBzhtTKOK6nF/u9N4DrBzAbxoipvPTB2ks/IghYujAedQR8nROP1VgUpADQdLSENAf10nJHI2+pqBitewr5bA3hnVHISukbTVvUoNnSoX1s4QqFuqfRORYn5qatomFJEwC/y4HWRMxk2Ja40G+D2rLQoa8qVAMDU+k4hwGyCVDMPOqOQj1QKCYek6eB796+j//+tz+BnZVsFsAJ5M1WCssdHUPybQE0PSlP3Xf4+JNvovYZDBxu5cfoqQoFXFAMAmOzQE9VmPo2Kr8ObQaAhWuhZ6aY+xy9oCA8c23s6gUK5TAN8NBCxVyJhbIY6BVWPkPtM9gADBnCnIZNd3WNhcuRKxsBJSDwqgCwCtTqd2fvweg1hcnTYdf8XKNzUIactxpmFf5g9GI8BKPgjQ7hy9IflQJ0aTHfa2F1yeMH3vMyvruzgA1h0rW3yJWBAXDuVjiY9wV4ZQ6KYbwIqr/QtR8vh7DeYZivAkRbb0gBQD+XHeFMO1it0XiNo6qPnqngoLCyIV9FGAchnSSebh/gz/dfw71LOfbtEK+Ve/g3Zx/ASdXDlw+uApkTtbsCdCWbKNBiomLCw9Frj8YZaHhoeBjCPsLKUNbaZ8i0QztrUNoMj1ZD3GifYZwvUSiLhoQcCDkeBdLN52HRExWHEHl7ChDrUFdWRZAHBxyteli4Cl29/oNNYw1jauaNCOdSzsd6VQEAZplDheSgLN8GaBgmC6+pYgTKOoezBsN8he996lVkyuIj/Xv4E507OHYtvL9zH3fKK/gn+9+JVw6vAC2LyXEPrywLZLmFbUT52G7XMMqjlTcojMXkXXb4bXuk6j8uvgDEBQQBBhcM6QKYC3TCtzRPWrqA2wyFTfN5pQo7LlyozCJwSRVEXMgwBJkLf4aYpqq+dDG0qWJJgSQXplzwtNvtCw6dhDN8fzAYIMuymPsOWC9YU6UWF9QM0SREIKRjubkYZshtujgm6OB5ptNpLEuaaywFEKx/Khg7nU4MVQZEcdPv92M4bqouLcvyQp64NN8c2zxVOaXKMSp3rl27FuFbVVW4fPlyBMJUGDIXJZVNaY4tujAztx/DY1lnBFdURhEUcrHMdq/rGicnJzEsmqHT7JOpuzHr9uHDh9jf379gBMF2YX2PRiM8ePDg60LJWT8pbE8X0ezzBPRFUeArX/lKNHigcolAA0CEWwzXTUPtU0Wucy5+NlWq0gWbBhMMcWbZ0rxxqRv2YrGI7QOsgQXzE6Yqz8Vigfl8jk6nE1WP8/kc/X4/9mXCuV6vF8PCOX5TkEM4uAmoCZA4PgnG0/omNCRg53hj3a9WK3Q6nZg/kuHz7BepOvWjH/0o9vf3YYzBSy+9FOuLYeVa62j00e/3Y53euHEDdV3j1VdfjX2AcDTdiCDwTOdNgib20bTPpGCIdcf30zk7hZQcm+8U6ryp/E7n6G63iw996EP43Oc+B+fE6fzGjRs4Pj6+kHdyuVxeCGFN56xUtXv9+nWMx2O02238yq/8Ct58881YN5z3+/1+BEYsXzqm0rHA9k3ri+MpHXfctOKcz/Mx5QDrUWuNnZ0dvPbaazg5OUFRFLh06VIM279//35UG7PPpvedQl/OU+kGVQpq0823pmmie3cK8VmX7BtVVcXQ48lkEvtaevB6KRjk+RjOTVi/+V1en3XE76WbipvAcvMcaZ9N7905MZza3t7GaDSKcPDtt9/G/v4+JpNJBMx1XePRo0d4+eWXcXR0FPOvAriQBuDy5ctxE+kbPf6T5kC8e/cu9vf38f3f//3xtVarhe/+7u/G7/3e7+GHf/iH8bnPfQ51XV/4zLVr1/D+978fv/d7v/eOAJEPRx6kxfHwAQoBcGAOO6qR1iCQbrCi2lOgSQihVVSvhVDL6M5p15AwOvUGcBZD3EKeNtsSJRSVUbGIiUKOcIhuvzyf5PEL6youDjVhGNbQIoS96nqdQ9B2Qj2E1ZGqRVpWB0ADFeCifCwYXQCmEFhpg2EJFZfZQsIq676EPJpg3OFDLjoCPK+DCjHUD/M4Sl16oFmXXdfyHoGSC9fO5j7CMYAqR6l/5mhregrFuY9hzsBavVb3VYR0DL9WIZSaYNVlodwh96HtybUN7ykHOm/lEbJ6DTzdP8LnzRNQKyMwOOSYy5YSFu0yFRyV12Vijj6X6wBzw4K9odmNj07L2ooTscuwdukFYoh03VNQfQGs7Me8P2clR14+D7kcOwr5TIChqiUs07VyMYZoLJBJbK3LVcwHipDXEACcUygKC+eVmAY0WVAcOlEcQiAjFYrWKvgQ+j+7VuD4Qx69tzTymYftqACfpW0IuZRb59wUd20fnbXpwFxhPabMSmB/PVBhXAmYpLnIhTDRDHChHZueAL5soYJJhnx0dt0gW3q0Tz06J2KK4jUkPP1c2slUoiptn4krsldAMXUCjHsaTWgrND6aMVUDGbA0EbItHc9NCKhrmVfqvoq5HVsTL4Y5HQGGrTP2Cw86S1Nlmc8FUtYdgdcC2YM5S6GCazSiIVC29Nj62CHemF3C4K6OOe+ypUfT0ZhdE/Cdz2VMNd0wD7Q83OUKHx/fhfUKu9kMLuQVtFBoo0FbNdBwqKkS9AZFUP4BQK4aGHjkyoaQVhXn5qnLMfc5tvUKj20fK5/DQWPbzGChBDiG7098Cz1VYddU2LctGHhMfQ7rVAid9sh1hWPbQ0/L8+HMdXEtO0UBh3/74AWZU4PStvNYIZuWYnhRaBRnISykcVCrGq4ncIubI8oSIiksdzWanRp/eusLWLgaudJoqfDHJhysVzh3HssqDykeCM1C/9SiavROAODErQSy0fE32VQstEWupe7KxqBxGp2ihlYOrUjO132f6TVmto3/6sEP4AuH13B60kfWanB79wS9vIyKRmVFjRnzA2rAeY1OVkNrB9coLJsc24N5dNCGQlQCeu2hMo+BEQVFZQ0ar7GoCtj2BI03GOglCu6MAZIzMQfahcVynpg7QeagCE+pRNRevqu8uFuH8O9Z1cLC12j5DEbpGLpslIb1DoervpQzQFLb0lHRnGknoeA67BQaAZoSbq6AIvwuHUA2n0IouveiyP4/7f6PqL3GF8qb+A/Tp/DfP/4I7p1uoawy2WwB0O5UGG/P0TgNH8LQ+50ShbFxHJR1hsmiLZuATLvy7vFtdWwqBdNFHYALi59U3cYjVeil4GQTNnJBmMJHAiYuCjeVMClUTFU5qXotVYBxoUPIxIVuqthKw4eZH43KLsIPAlOG5gJivkAIRuCRLtjTcGqCjs3QRZZ/tVpdWOSnqh1CDd4Ly0o4ykU5y5c6IKch5VxwEY6lJhusS5p2UPHCf1SyMJR0UxFKqMoF83w+j1Cn2+3i6aefjvfOz8xms9hOqbEFzQ7YJ7igJzAhbEzDgunOS2VjuphNgV6n04kmMKzXVJVI5ScVTSnMZnlScJSGQjO0m32M/QJAVNCxXdI+wGuwXK1WC1tbWxF2EipTpUqH8RTGc2FPeJ+2/WaONuaATMEO/8/fU6OD1HQGWKtmqb4jTGa+xBQ8MqwcAMbjcfwMgAs5AvlaOnYIrtm2HOdpaG3avukcxfbmd9jn2UdYd6zPNHSVbtScg7TWeOqppyIoeuutt3B8fByh1aVLl/D0009jOp3izp07Ecwvl0t0u11cuXIF9+/fj312M7w0vZd0Ht3cBODBPri5AZOCwRQIp/M02ystR6oOSyElx1tRFBiNRrh27RpefvllnJycoN/vYzAY4PXXX499n32eGxAM4d5Ure/s7MQUBUdHR9FQJlVJ0sBouVxeSD3AsvGg+pV1x/tK5ybWGccC1YOtVgtZlmE4HOLBgwfx2pwHmJoAENfho6MjPHz4EJPJBNvb2xfqNVUSpurQuq5juHYaos7xy+cJ64vn4gZBp9NBr9dDv9+/sFGTXmOzHTfBdNqmHNdMRUHwvQkgNx21Wce8Tz6P0s1BqmpT8y728XSe8d5jb28PTz31FI6Pj3F8fBw3l9rtNs7Pz3FycoLz83McHBzEvs0NID6nGLbO3Kfn5+fx2feNHP9JASLlkuwwPK5cuYJ79+7FzxRFga2tra/7zB/k/PJ3/s7fwU/8xE+880W9/LNJyN8F05GgCIthsnTHDGpDgj6GjFIxKDnYAKdUDCv0Qc0XXY0D7HOFOP/aFmIOwaiCs+FcAXyxbDRTabpJOQFkM1xYYDmEvINKlCbRkIUhzJByEs4oF8BTOF/7xEcwStWbrgj/QngylW25nCtbhVyPXvIP6gbBTVcFSOdjSLbVQD1U0CXggsJJeYEgppTzmBBirCsBXkAIMQeistC2EN10bYuhoaJAExMbua9s5SVfpZF2yxaIoMZQvafFaEbqlSo0Udz4tMcn8CmCzlqg7vyKxihbAo2GLxxWuxl0CEOjcEbCxEURmi3FAdlUHvkiLLRtULq2xHjDFgIxgeCuG0CSbqSukAmYzFYCKttu3S9tcGwGJB+gHQjc9YbO3ZL/0HbyCCKVc1BOYCIaC98pJMTVBzWaArzVUCb8gZY1WJYFMu2glId3GiazsFYWw94p+MyFBbVHXQgIzpcOxYm4tZo61EsLqIq14Qnruwn9iuoeFdzIq5EClbqSP1IMSSTs26M7C2A75MVk/yEkrnsI0ECt80xma9Vm68yHz4maMJsLlPRGcmN6Jedc7kjYamvi0Tq18JlCORZn6c5xA1voCDh1E8B3KX237omy0YW+aeq1gtIWohrlGHJ9mhCpmCc0W0g/sC0VNwNac8nbWA4VyqFC50TKAQDVQNSNaX92mYKbALObGn/x+lfwc/+fP4FLM9kMMLWUtxwpzJ6yULVC+0hBh/B+HzZAPvDEA1zLTyOU03BB7UdoV8N4j0I1Uk7t0NU14IAKJuYt7KkGtdfoqgYWKpqe5MrCQWGsl8jDLovzGlqJc/JAr5Arh7Faoa0sao8YOt3VZTwHj7FZCIj0Gl1V4mGzhZvZCY6/tIt2MKXRS43B21ZyH2Y6jM0Ar7wHMgPXzcVBvXHwuRgNmZVF08+x2gba4xXemx+hhkIrmaSpgrvfDLFcFjLXZuzjPsI+hq8/ng5w4pzk1lN8E9GlflwsManbqKxBO2/wdP8If3r0BVw3slj8W6s/G/MsagvYjrgpH9V9vDG5hOm8DZ07vPfqAZ4f7uOk6uGgNcAZejLGSr0OI/aiBG28hjEypgHgcjFB6XLUtUGu1uVXXtJ8WC9mLwoCPC+3Zvj+4Zexa+a4Zix+NVvFHII+KGpbeY0FE5dG9SEpaICGNdaGJOEZ7QNhPJl18XaT4VJL6tsoDbG2Bhwkx6HruJhX8UKUcngt006eXaVsbOheg15/helJb/1hh7XyUQHdVo3550f4M/P/EnmfD12FdqeCUR6dVg20athQH9YrtPMGmbEo6wyrOsPxaR9uliM7N7IJpICsvFjGd49vn6PT6WBvbw/OuRhORgUgFw2EY4Q/BE0EIelCNQWQ7wTIUggEIC4W0kXHZphmqpDbBAfpZ7hgAhBBAReXVBqmiivCiU2lIhenqUIyzQVF4JDmlGJIpfdrZ2keXOxzUZkqqVIoS1VllmVRbZMuAKuqijm1eB+8VgpPGL4LrJVXaVgalV6ERSxjCllYN6nZTKosIqjl546OjjAajWCMwZNPPolHjx5dgEXee5yfn8c+R6jFOmDdL5fLGK7Kf7xX1lNVVZjNZjGEmfm9UtXnZDKJC+BerxdVjGlfmc1mKMsy5mdcLBYRhLB/EBZ3Oh1MJpOoqiNkZt2lyi5gHSKZ5gBj+WkEQVUpFauECqvVKjqEEzgTNhLCdbvd2E5sd+ZyS0M+y7KMeR5pfMI2SPMVEgTwfNPpFN5LiH6/34dSCmdnZ9Ghmgt8Ql+OWYZfU7HFay6XSxRFgfl8HstHMwx+jrA6VdFyM4CKO8Ir/k5FXKfTifMLIRRVj1SgAog5LZVSWCwWEcxTQXt+fo4/9sf+GO7fv48rV67gX/yLf3EB6jVNg/Pzc2RZFucwgtSrV6/COYc333wzzo8cgwAuqAjZF9K+z38pLOdPnisFqjzH5tybglfO4RwnVK2mUDMNVzXG4MUXX8RiscD5+TnOz89x8+ZNzGazCFJTxVp6pJtDLOvly5exWq3Qbrdx586dr9sM4OeGwyHm83kEaoSMab1wHKSbSZvXzPM8mpqMx2M453BychJDrwnR2Jeomn348GGEWQSf7D/s8+nczWcAn4upGrgoCgyHwwtzZgqIeTzxxBPo9XqxrxLWcoOD45H9/Z3uOe076cYWgAvKV7po81mUAkSG3xP4p3CS1+HmS7vdxo0bN3DlyhX0ej0cHBxEJWH6bODcOx6PsbW1hSzLcHh4iAcPHsTwfz7PuNmVjv1Op4Pd3V0Mh8MIMReLRXS3TzdpvpHjfxYX5rQxAcQG/8OOP+wzf/Nv/k389b/+1+Pvk8kEN2/elGt5YH5dYXWrgloYZHMdw4yzhYS+8nNwEupnOwH0VWt1HI02qAbIFj6Cu2qkIgiMykYd8gcGaNW0EcMlvV5DPYIAKhAlxFK+G8Oag8ouOqpCvm+WgHGJurCUcptKAAXPoWsBfVQ8UnVFIw+XY61k9D7koBN1jLZipuAyUQI2XUls7wIMzeZCS1wh4LEaKXij4OsA/IL5TIQqtcA75dZKTFskCrvwuahKbMS1mApCAGvTmQYwzsO3JQeiC+6yXgmkoRGNzRUyKh49AK+QzQPgLFTi8ExlEaLrJ9WDzFOoHJDPHQ4+6XE5n4gzaqmQT+V1b6TuJHQVqEMorG0DdV/Ha+hKFtsE2NlSlIf53IlKsa0lB2QI37O5QtNTEbAxHJzAVtfS7lkZTFmO3DqktS/XMfMKzaAFM68BpYJ5ijjLom5g+63gCK2xuix0Upl1vrVVlUMHVaJWHq12DWs1xHlZBpHWDq3MYlnlqIyAXWWBK5+ro5FMMQ0AWgGtc4e6JwCs6QlsbZ15rLaD8UchfURXEo7OfqACWJCirdutHMtr+XRtxFJnKkLHfOajMpWh6aYUFSFOBbrZXOqxfe4xvywqJUtlpgHqgZS1CgDTGUB1Rfmn62CeY6Q98oULJigS+tw691FBqvx6M8HmCGHOCKZDUuZ8JmPKZXJeU4YHkJbxYwsteRw70jenIYSawEk2DqTvVwOFckthcN9h8QNT3FvuYPC1XGC2l3qxbYH7218IofeVfI+qbTuw+GPbd3BmBaj0dIldM8WZ7aKt65iXsK1szFOoIbkMKxgYuBD67NEOkG8RqH0Bhwly9CCv58rBeA8LhQoGu6pEzzRoK4t5OEftNbhk7OoaPdXg2LXQDhP1se1hqFcYqhIrZMhVg9oZfG71JIZ3gNktL2HbU4VsbuEzjWqrwOKSga4c8lkNVYpy12cKynv4TMOZAL8yheVOhnrgUSiPXAFdZWC4IwmHDAZGaew3I9TTQnCiT+ChFgWfvKzROB1gqoqKO+9iZ0ehG/wf934bO9eWaCuLQ9vBxLXx380+it85fBaPpgNR0FkVnzfKKeTaol+UIdWAx25rhkv5FF1d4a18S8AcZH70Rq6rMoeSIeWNBhxEgWjmeOTG8FThaR9ziMIrWGj851c+j2db+xiqEg/sCPfrHfzq+Qdwd76Du2fbEZgCgC4VJtPu2mU5HMrKpoTAOi/35VRUJuqVhivk/arM8cCO8CFcdL+0ASJq5UOORh9SdIhqXtLEerxn+Bi3uqd4a3SO79y6i//N6D8ih8d/c/JJ/OLhJ8JuYDoHyc9OXuNsIGrNUX+FxmpkQZVdZPLQXZSF5Pg1Fu28wWTZxuK1bWRzUTJm2sPnQNNzaNoOptsAd9rRxOvd49vnaJoGV65cwXg8RlVVMSk9oc58PsejR4+iOQSBAxcgfG0zrIkgA0AM8+MihWAgPQcXWQQ7PBgqRnUXF9wEMmk+uRRAAmuFY7oITaFDeh7v/dfl0GMOPP6eLrIBxM+k0ChVlhCsUFVDZUUaZsnvEgAQBqTh2ukiMlXfpCGlqVqH7ZC6o6awh5CAIZysI34uzZNFd1nWU6rUIqhM1WQ0Ddja2sIrr7wSF7I8N1Vhm7CDi/NOpxPBWHofhKkEpwSQqRouVY0yjyL7BtuKYJBQiYBqPB5H2Maw0rSMvOeyLPH0009jNpvF/kKgl4Zcsl9uQqF36rd5nuPll1/GarXCbDbDYDDAeDyObTAYDOJiOc09STUWF/dUR6Z9gf0nzTtJaJyOkzRUMoXErVbrQuj21tbWhQ2CPM/jpgOViAyLpzqScwoNYghg0z7IMU6YxzBrunFznqGykOVmGVgXhL8cRwT28/k8GuOwHIvFIrp5O+fQ6/VQVRU+9KEPYTweRyfg1FEbEEfbXq8Xx7YxJgLvvb09HB0dxXDTdJyw/tP5g6+lKnDWLf+fKszS/pQq+Aj4U7ViqmZM54D0OmlZ+BnnHD74wQ/iwYMHOD09RVmW2NrawsHBAaqqwosvvojxeIyTk5NofpJCd16bEGgwGODw8BB7e3sX5uP02lprDIfDdxRmpaklONZTcJoCUO89Ll++jMVigb29PZRlGd3fn3rqKaxWK9y7dy+O1zTMnSkNCMqYRoHpHs7Ozi6koOAYZt5D3gs3M2jWw7mD5dRawqXPz8+xtbWF+XyO5XKJ8XiM4XCIqqpw7949zOfzC+OEausUvnIsp3WaqirTPnBycoLr169f2HzjkW42sD3TlBI0lNnb24tz9Hg8jhsKHJNpX+V5R6MRZrMZHj9+jFdeeSVuCHBTq91uYzAYxDQHWms888wz2NrawnQ6xdnZGc7OzuIGCefR4XAY++Q3cvwnBYh7e3sARGV49erV+PrBwUFUJe7t7aGqKpyenl5QIR4cHOCTn/zkO56XE+4fdCgPFIMKGADVIgecQt6tUTmN5TxDcZhBNSqAtqCyC0Zf4k4sudokt5/ALh0AWd1Va3MHrp90AsAIB0NZCC3odKwDuHQGUDpASxsgYJLHTf4ToFkDNEbAYT7BhRAns5Jy0PBE1z6GZftsrY4EsA6p7oqKj3DCEays5HvNNqLhSzYXoEUXauYOZA7JYuJDyCTP4aFygjAVQIUsxNLv1S0giyo/gZguw9rFuS2qD9sO4dNdFUN9mdswlj1RgKbhsVToRUfNsAhsWiqGkhKi+kyt1ap+DUGZK+/59zxA7Q1gJX/k4qoHlIlKNTiBUPnCx7x2PKLKLAshzW2gbOnQ77S0Syh3PgtqzdojPxRXYILBpi15C20uv9uWQDYXgBkVMqb0KKZezFKMgq4aAbaNA7QGGgvVWDQdExSqgOtaKOPjgl4pyfeWGwvv+RBCVBsa49A0okS0Tsn/CwdXqBBqq1H3dFQEaiuQ1RYCjr0RR+XWxCdAFevUAV5AmOTblFDzeqDWYwNiUNT0pN/Ug7UREscO8xamaQhUAO91VyNbAu1zGwx1NGyu0HvsonJP1x6tCVCuxBE7C+rbaih5K5sWkHm/zrWZeXijY65HzhtU1LIuiqnkXqxC+HK2CKrRoDhrnTkwt2e+EOVq3RVnbVN7tKYe5TA4fTN1QelRbgn8bNpAPkfIDQqUY43/9dMv4Ze/8mFsn3rp+6GeXYCU2crDlB5VX8N2aPrjAS2hx3vZWQSBK59j4VsovEU7OAbRuMQEQjR17eCO7CPwY/5CQAxPVsiDQUoWQpxdDHvWymEa/k+louQ0dKiggwLRCFD0BhYaPVVhqFeoYFB5gzPbRU+X2DMT/PSdP4V84WXzBUDrFDCl1PP0hsHpx2q4zxTY+XIDVTdwISeRV8EhmHOK0VjtaHjjsJoXeKXawqc6Kzg41D4YDikPeIdj24da6WjUpBoI0DIeaMITQns4p3Boe/I9qwAq+8JHGmfwq+cfwJfOr2NStmG9wqi1wnnZRm0NcrNWX5paNmC88dHB2hjJVTrIV+jqCs5LHkU0QT1rCCwBnbsIhQEBetNVC1PXhvVBeRxcjTkXK+PQ0yW+sriO3zp5HoumwIPpCEXWoJ9XuNKdoJPTJSl8zwB2mgOZj6HQAgnVGtzxb7CQ3xEaAg8BQHnkRYPa/8F/sjRu/UecqUL9Q54rW60F/q+X/j0A4Jenz+F/ePxB/OIbH8XsqCc5F1tO7jML0DMxWsmNxaUPHMA6jdxY9ApRj66aDJNlG/NpG36WAcZj79YJunmNh4+20H+gMXumAQoHZZI69pL+geLTd49vr4OQYTKZYH9/P6bfSRdBKeyi+iddjKaAhAdVVHRhBXAByqQqLZYjDcVLlV08XxqWlqq1uIBIF5L8bAr4CD5T2MnPEYICa4fMFDjyugx1ZVl7vV4EVwxJZR1uOmumKkzeG/9PqMZypCqTdDHGe2HIJ9siVZNsKijT+uV303xnaQh6ChDTEDLmgNwMJ0xzUxI8P/fcczEkkO69BNAsS9qOhH2EYDxfqmijAo6LbdYZ1TapqQIXvgRKDMMmjGafoaLTe49Op4P5fH5hMc46YHglF9NcuFI1k6pL+d20/lP1YQpyOH7YlsaYmHctBfZUgALAdDqN7Z4aH/CgIjOF+qnKlGNvE+AR/hGe8p7YXml4NOELz8U65f30+30YI8Y2DD/kefh5KqOoDGS/Svtj2p/TTQf2aY6TNDUBQR+Vjcx/xzBuABFOpiHPgICgXq+H5557Dp/5zGfwoQ99CL/6q78aASgVU8YY3Lt3L/ZNzhfdbjfWQbrpkarx0pDldMPjnYBeOm5TAPlOY5ugOH0vnUs355O0n7MvsD2vXLmCW7du4Zd/+ZexWq2iIo7j+erVqzHcdzab4a233opzbVpuGhURSt2/fx/Xr19Hq9WKY5j3yzGW5mlNf7LtqWR1zsU5IRVz8blDgE1w/ejRIxweHuL8/Dzm02SZ0zk67YcAYv+lGUnaL3lQTcw6b5om5i9l+dkf0jlwOBxGBXFVVdjf348A/9KlSxdSShC4ccOBZUn7AO+B77FdOea4kfdO4re0XOkzApDn1LVr17Czs3MB9AEyd+3v7+Pw8PBCm6V91hjJIbpYLDCdTtHr9bC1tYV2ux3nHM7ZBwcH0Frj5s2beO2116JZEd+fTqeo6xrj8fjCJss3cvwnBYi3b9/G3t4efuM3fgMf/vCHAcgD8Xd+53fwd//u3wUAfPSjH0We5/iN3/gN/NAP/RAA4NGjR3jppZfwUz/1U/8/Xbc4A2b3u3C7TAAoagqTOfhuAyCDzzysBvK5Qt2R0GECDIInXcuawTL8TEm4o0sW4LpKVGuNQCxn1upAwjxdB6AXFIqpEYltBdVQYvZCRaOyAjdNJWDAMp9iUCdGAwlGvmUKqH0sE5V7Pgvil0bynDUdBRcgTTT0KBCdnE0IBVaeYbmITskSaeejKYwKYElgmoDW5SW67qp4/ZirsFk7BxN+1X2qxgAX7hk65AQMzspU4jFPFPNZRaUjnabLADxDrstIMDwVch6mVLHOGJKdOndTfWiCe/f/7Yl/g/+4fBKqUchmohTqHDnYXEJRy6FCowXoQCV56YJyVXkBNPnSXwwzhajgVCOKMwkdXavBmpW54Fyqa6CYe5hTJ3XnBFh6JSBtuSMgrH1aA95DVaKShFKA9UDdQFU1oDVsAIjlOCh8AOjMIwt5DTtFDaU8yjpDkVl4AJlxMUVZq3Aw2qFxGk1jZLENURt2ThxmY4WmK3U+eFNyAzYdMfuohgwZlvZq2gqrXQlJTpWn2RxB0SeAV1cBjnpp4zy4eEeFmJHru1y+X0zFdKfpBGAfUqfatsDppmuQz0URaDsqnl9bYLktZS1mDt0DB9sOdXviYl5CALCVkv6kBBS1zh2qgRb35bbck64JkIFyKA3a9GQs1T1Rl7owRrRVMEsPUwPLbQXlFYqpmOLUXY3ltriFdx9bQAXHZwf0H1iBn6HvzK4aFKcej7+3RukydL/QkYEa+rwLZkKi/A1wO5j4+Byodhtk/RrntoOxWaCARZUk5xvoFUww15j6PIYoWyho5UQFGACPhcLKG/RC+PKxbaFQNkLErm6imcpArzD3BWpvIpCc+wI9VKgUsPI5ClXCQNSKAjUNrBLlIr831Ktg2NLB/v90Da0dwGUOulTo7VuZw9oG+QwYvFQA3kMvQzhMriXnbYDd3iiYVYNyp0A1EOV6YzV+8eg78fTVf4uRVuiqHLkKCaRh8aDcCueCqArz5A/2zAdpOdA0BofNUABixk4F6AAZO6bC/3R0G/tnQ5TLHKPRAle6UwwHK8zqlhi1NBrRnEtJ3+zqCplyYTNAzrtwBUZmgbLJIshUlYoqO608FrYF51WEhQRx7++8DaVkUw2Z7IJRqXq33MW/uvc+WCtzQZZZ7HTnuN49x8eGd3G06uNBmINpwKIX/L6XnIPptEhoyLyRHhHkqUbD1xrWiukOIKHcVB7yWDXhARGApG688Dmr8NlffhEfv/0C9FJBV6I2brpeNlFaTn7ymUVTG6onlUeuHRprcDzpoZy0oCcZTBlUxEqeJ3bcYKu9RNvUUAootz2yYQVbmQvKSxXm3nfDl789D601Hj9+jOvXr8N7H01BCA5ns1lURlGJli7oqQpKVU1cSHIBkS4s+d0U0qSLNX4mVfwBiIssLjqY047fN2btTktwRzVDGnrN8m0uqnmki7J0AUSIQwCUwi0uaAny0sUu1RYs56ZSkiHAvV4v5kVkOVKoyu+yLtJF5Gq1igq99NypEilVrDCMMV0kpu2V1hXLmALHNDcewSnVYkopfOQjH4nKrF6vFyERc4Oxreu6jovibrcb8yemip3lconpdBrVaSwb4QDPQxBI8M17Zf3ReABANBPJsgynp6dxEc8QRyr5UiVYv9+PIcR5nsfc9mm+rlSxmCrKNpW2VK4NBoMINRlOTOgJIPaHFHRkWYbFYhEVhezjrVYLo9EoXo/jBUDM8Qcgmpik+fN6vV4sF6Hk5ljl/Q4GgwuAnRCH0IbQjmOCbcQ+wnbi/EL4nuaTI0xfLpfRuZvzzmYYNxWJVGgSRrNMzL3HcHXeJ9uZ5j0EJ+PxGJcvX4bWGgcHB3jzzTej2U+6aUFjFxqJ9Pt93L59O461zXZnuHoKpt9pg4BHCnHZtzY3XdLPcXOE76dQbFPttwnJU+iktcYHPvCBaCZ0dnaGvb09LBYLnJycoN1u4/HjxzG8l/WZpk7gsyHPc+zs7GAymWA2m+Hw8BA3btzApz71Kbzxxht47bXXYtkY9s5+lsK8TfC2WCwuAGteixsFx8fHmM1muHfvHmazGcbjcRz/TdPg0qVL0awo3Yza3NhK7ymt681UBVTXZ1mG5557Dv1+/4K5E99nW1ZVhbt370YjJvaLwWAQ55PhcIjHjx9feBam4d2pQjVV2qftzM+yvJspK9Ij3fzgPEO3e4L33d1d3L9/H2+++Sbu3bsX52ymAbh+/Xo0HUrnQSo9x+Mxbt26FTdjzs7OcHBwEOel8Xh8QdlJt/jDw8M456Rj551g7h92fNMAcTab4fXXX4+/3717F1/4whewvb2NW7du4Ud+5Efwkz/5k3j22Wfx7LPP4id/8ifR7Xbxl/7SXwIAjEYj/NW/+lfxoz/6o9jZ2cH29jZ+7Md+DC+++GJ0Zf5mDwmF9NAHLdielR3+hQEmWpa/HjArUTN5JaHBLl+rDz1UNBhRXqAF3DpPnK4Q1HICxlxGJR0kd16Ye5RN4J3BOkchcysGkGjbgK8AWMSciXVvrVKkMy3UxVBrF3IgZgtR4gGI+ba8UtBu7TS9zgkpoYu8P2Cde62Y+HhfyguMzKaiWIrws/ExBJvgEQhh0gHwZSuPbKVQG6nbmIveICzGfawbXUvePJsraO0DyBSIwXyBXgFGq6DSErjichVDt5XzMX8g4Sa8ispP2xJY5AqJQWP4tNchH11LoR545DNCP0SzGVMCs2sZxnqFN1eXkC0lDH55NahRQ57G9qmHtkm4bC5qO8LguqNgd7X0AS5ErRclaO2RlR7FLOxwaBXqWZRsVCc2HQFydT9JoGuC6tPxnIB3HvlZCXgPs6gkVNyH3IdOfrp+O+bWq4eiKFIhJBkAeu0KmbGomvBHWljHO6/ECMNpNI1Gp1XDOXFsrgDgMEMTQNzoboO6p+EyhXzukC1VMMuRsdH0PMpaoX0i99o6UegeCpQtx/I52wF8tYaNrpBcj8VU+kA1EOCWLRBzTKZuy7YQCN058hEMaxtySIZQ/mIqYeSLlgA0KW9IYaCBum9gSh3Dj72W900t7epyuUYxc6j6GvMrBq2JR+fYyXwSlLhNS0vYcyOKv6rWMdcgVc1NR6Bj05Z7ZB9dthTqnvSFpoMIP3VNuCrnzefSvxa7wQSlAv70+1/Cv339BYwf+xj6HN3GqwDaIeC16bIcHk/cPsSfvfYlXMnPUXuDnq6gISrB72o/xkNroqKwgMPcZxioGo9tHwO9DivdNXMcuw7ggRU8uqoJQNIFl2WDqcuRKxfgYg9GOQx1iTPXQeUNTmwf7fwYK9dCHiAkXZhX0MGxOYeBKEhPbB8aDoWy+Mz8aQzueZw/B/jcoTjK0DqvAOvR9DN0jhoM32oA60Wt287D/B12RhDgk/VYjQ1sR+Y6fZ7h3732DH7gzv8Z/4+P/Cv8xcEj+SwUSm/xyvQKdK2iYY0qNXwrDFSzlptp7VF5g0VTyNyQc3dIPjLKlhgVK+xjiCy32O4tsFUs4aBQWRMVwumhwmtZUIgqAFvZAu2wSxUBogJ8HlTiAVheLc7wwe0H+OD2A1xvneKF9gPksPjs4il4G1SLVvL7xXkHHuPOCufLNsa9JRZlgX5e4nb3CF1dfr0jskfMu+g7DjRKibkYtagxVe7hy/DdzK0pm1OwjcG98hKs35f8h8mx8BVKa6AaUQbTXNkHxWUx8VjNNZqBhR15mG6DbrdEO29QZA1GrRUebQ1x/tYIcPIsgZP0F43TuH93F2auoSuFwkl+3nrLAi0L07JwjY7PPR3/Azg6UHsFpSB5ZCsNr3xM0/Du8e11ECA8fvw4GgR4fzH8lYo6Kp82F6WpkoXKpzQUMlVfpXAl/Uyq0kqVTWloLRdKBAeEQwQOhCkEjOkClIu4NEyP76WKLH42dcUk5EjzPabQgYvoFCIBiAvaNBwsXQCmoXhchKfggfeY1jnDbwnoeA+sG4IYAhS2RwoHqf4CEOFDGmIHIIYBsyysF4JAKubokllVVYQ4u7u7+OIXvwhrLXZ3d+PCkGoSQFQ7eZ5HiEbYmi6KuYil0oQLbeYqJKxQSmEwGEQF42IhuaDYhlmWYTAYAMCFtqc6jd+lgpLXSdu32+3i9PQUw+HwAsRIxwHD84E1uOFiOM0hmY6DdDHP3Gzsj6lijdByPB5H2NjtdmPev1StxL7GXJZ5nsd8kdeuXUOv18PR0VGEpxzvzLnIPG5pXjSGDrKPsdwE62kOs16vh263i8ViEYEfFaCDwSDmIEzdoWezWayf1PF8MBhE8FIURXQ75vt0bGZ7sr8S3hC4Nk0TATSvPRwO4zkYpv3Rj34UR0dHGI/H+PVf//UIldN2IKBlO3jvsbu7ixs3buDNN9+MuT9TxWWqRkzHa6rKfidlIcdlqiBN36cCmZA0BUqpKjBVpKVKuhSKAqKufeGFF/D7v//7Mb/d7u4uHj9+HOevV199FcPhEEopnJycXFCl8l4I1DqdDo6OjnB4eAjnHL785S/j9u3b+OhHP4q33norQvLxeAzvPY6Pj2Ndp/fAa3O+Yn0xNURqvMTvpLn8qKRO89/yWZKqhzlHcI7XWsd2ThV5hO/9fh83btyIbuWXLl2KbZ3neYR+m6pmpsfgvMlNABp1cb5ge3POTtXnfMakdcWy85q8LjdruKnDeZjfTZX1ab5Zqlu/+MUvYrlc4uzsDP1+H1evXkVZljEf7/Xr12GtxdHR0YX8o5yvn3nmmRja/+jRo6io5rN6MplcUBCnG0MExencQ0D6zUDEbxogfvazn8WnPvWp+DtzE/7lv/yX8fM///P4G3/jb2C5XOKv/bW/htPTU3ziE5/Ar//6r8eHDQD8/b//95FlGX7oh34Iy+US3/u934uf//mff0eK+0cdygHVlocvPLJThdZpBl0Dq5216kzZAMSCejCfA1hR+RfUGEZMNnQVIKIVwYgL4bAMf0VQ3alGFuRAUCbW8h24kMKpDgpDRYin0PQEUOUz5kEDaKzijZzXhnyEzInIMGNdCTjymagrJHRNrq8rQGVBKRJUdxFyNmvFosASvw6DDv3E5YAKMMLl4R4XPpZBN+IW6xOBh/IB4IXzZAsfFZwuGH54HVR1bRVdn33I7aa8GI80bcQQSypqTFAsysUIIRGTz3utoqJH3JARFKOIOQgJJJmvEiA8DGUOqnfdSB66piOqlHzucfhhhToYNWRzWUzWI4umLbCM8Fm+7y+EpBdzJ6HwRtSWXkm/aFpKvlsESBpyKbIPKudjODPDadunTmCpCQYqXt5jGeqeCq7QgF7VWDw5Rjk2GH/1XNSIDGHWGrbXknDoAmj6QVIe1EB5ZmECSMyNQ9VkqJoMubFo6gxZ5lAUNeauQG0NmkbcmIuiQa2lzyx3MsyvKfQeemRLh3KkA6zzcA1QjqQeXUuUid0DH/PvARIar5sAwL0ANVOLkUrTFRVgNdA4f85BbVdovdxBvlBYXvZwuYeuFFpn61B13YixD0Jbm9IjWwDtUwvbVsiWDsP7ovDLVpKnsVYCq8XFWMZY3ZXQ4VoBq0KhGgOtYxlfTVsUVatL8pn2iUwQTUuMSXQtOS9toVD1NbKVR+tclKS2JaHghoZGSpSmTQsx12q29OgcOSy3JQ+i12Lukq1ERVr3BLS2ziRUt3PgcfDHLDqmgvlyP6q5TMkNEBUViC7jfBD+P7D45O4dvNB+EGHd1LWjycmJA6auQFs1MMrigR1h10xRQyNXDXLlsHA5tHJoKxtzIVpojHWFsapQe40zl6PyBrXKsG0WGOgVHjZbGKglWspiEFSEADDWJabOQysXwp0NFq4lBi2oMHVtDPQKzmtU3qCnG9zMzvC33no/jAOagTj4tg89dOVEKVxo5NMGqnbQZQN4yXnotYwvn4dwAethOxmqoeSE5ZzhrEZ/sMDCtZAl6sypa/B4MQjflTnaZ8mDOORAhFYoFznernawbPK14i3MdYDAuUxbZJmF90AvrzDOFwJe6xYsnZuT+dsrj64p0TiDTDt0ihoLV+CKOpf50IUyGS+OzwFeaePwJ7uvYsfM8HvTZ/Av3v4g/pvT70KvW2K7txDlnJJxxGeVNh5GiRq5yCxy7TDuLlFoi5FZovYZamsiOJMUAViHKEfAJnkVvfEhvNrDN9yh8rFefOEk36NVeFiO0MDKcw4eGgpGaZTOoawzqGr9bADkmdF0LU6/q0a3X+J6f45xa4luVmHRFDgrO3h0OsTi03uiVH8ScLmP4NFlCmeLDjoPMiyvN7BjB5WJYlFZBaUlDYSvNZTxyLRD5bKo0GeaCaUg+STrcH/5N/5H2rvHt9axqaBi6BnVX8vl8kKOLi7W+cc+F12EYcAaaHFxQ8UbFyRcKPMcKQDkdzaVczxSs5D0OvxuquZJARLLzkVSGroMrF1zU4UlwV+6oEuBGgEA1WxpSDEVV3QQ3lwvpKGsBDxpaBgPXiPNS5gqYDqdTgz7SyEm64P3txmunOaKSxUjbMtUvZgqwLgwpOIxVYTmeY5Lly5hZ2cHv/ALv4ArV65gZ2cHu7u7ABCdmrmwXS6XMbyUbcK65KKSbUOImmUZ+v0+AERoxRBp3n9qJMLyzmazmE8vzRtG84amafDJT34So9EIn/3sZ/Haa68BQFTjUoV6/fp1zOfzeP88UrVZCpw4VlLFa2o8k+c5VqsVtre3o7Po48ePY/9ifbHPnZ6e4tKlSxEAEigCiKq/ra2tC+HZHHM0BF2tVsjzPNbjfD6PeTT5PbYBAQdBMSFMqppaLBbRBIZzB8c4AeV0OsXu7m4Mh6eqkzkzUwVmqmhO4RfLkyqwAFyAvqyzFEQSEBNCpXNPakZUFAWeeuopvPHGG2i323j99dcvzCcpACa4JUB94oknMJvN8LWvfS26gqdhpRzXnFc4JxEEpfMSQV86b6QbAew3BCqcD1KYkkKy9Nw8WMfp/JxlGa5evYqbN2/it37rtzCdTuN4OTo6utCvHz58GMtC6Jwqn9kPmVe3qirM53McHx+jrmt89KMfvQDtmIOUuTRZ5+m98CfdhNO64+d5L5zLU0WtMWtjKfYp9pH0SDd7smxtlMNNDsI2zlfD4TCG5FKRqpTC66+/jtFodAHWphtVqVKf/YPgMX3GsK3YzoSJ3GBh3fB5nCps0/7Huh0MBlH5B0jKgfQZvfk84MYK6+nw8BBXr17FtWvXMBqNsLe3h+3tbdy5cwevvPJKNEnicXh4GM2TmA+YOUkJnrlRlD5XWedpmfja/yIKxO/5nu/5Qy+ilMKP//iP48d//Mf/wM+02238zM/8DH7mZ37mm7381x1ehwT5iwy6RDRN6T1YP4gY3kigZFtiwkAAJ8nSVXDqFYhjW4hhywwtpvrCawA6CZ1didIJAOoBIlCK30tDohSiIosmCJITDoAT0QXVenHh5YMaK1dgzjsf8in6BDYwlBMIJhN+Df+0FUijvIcrADMHqrGYWnT3xR1ZBVinK4FSygUXWaOSxd86JNq2xTW2HEruN9tSUR1Hl2TmgfNaoRoKPCU8s7mEtupazsn7NCXrMoSiBRORahTMUXwwMQmh28xlaNsqKjJdIQoPFZSkhJ7KAtXYx/Czug/Mbzm0Dg2KiUCjSx88FNdY20I+E0hpFhrdQxvXxFAq5rlkvTsaqQR4mi2lLiSvHVDMPEzl1qHrYfTFXIFOwBnPWY4uKlR0rZAvvRiCKMl357Jw/lWN86dznL3QoH3aR+femcgIuVs0DInOewq+sHERr7V4nC6qHK3MopU1YkYAYLEqkOcWSnkxDsgkPyL/xvMBDoiBDLB8cQlvOug+0ut72FIRgpuFQrUlyc784bru6sC68pkA0cWeQt3z0I1C74HkPpw8IYlGR68YuLyDug90Dh1apwrVQPJL9h84rMYC82xL+pRy0m+qoUK5JepCgurWRABf05E26+1bZKuweOtIPsfOiYObCMCdPKHQOpH6dpmE4TPXoVcCSfO53Gs1lO9kCx/HjW0pqMbAFaI8pHlSMRVzleUlDTigNZE8mC6XPtA+d8j2xY2ZCt1OMNERt3OPpiP1830f/Qr+9Z33offQR4gex1XIl+rVup/5DPDa48lnH+M7+6/DQqGtamybBU5sF73gqLxwGXpKFk7z0HFpcpIrizPXxq6WyXfqcsxdK+YnXHmDuc9QwCFXDVY+R1utYrjytpnBwKH0onAs4HDZzGChkCuLqWtjGsqllUM7hFb3VMgZohoUyqKtahzaHqZf2UHrUqjflUb30IV5Twf1p4SCqyb8MZobMRwKzszOSJqCpmNQDalED6o95TE57eKN1WW40VsRYE29wvGkJ6q1lgt1HkCWl7B0X4TXa42H5Ri9vArKxPXzANpjYFaonAD8cpVj2eSw0GjpBo0zKOtMTE2aMDcbJ8+jAB4BUQ63dY3dbIq5K6BUgHQeonzTXsKDFfD/fPwn8Vuvvwd2UohpSdeiKhrMq0KgHlMqBJbIx3/LNDiqe2hlDcbtpRgvaekjjdOJehBwXQffsUAVXuewqBT8wAbnZdkBy4YVlHaoZ4V81iogd/C1xp3pJSxcjZbyqGFReodX6h5+d/aRMEmu+/v5UznyF85xe3wGrTwO5328fbiFt4720HmsUZzJWLsycyjOVpg82YLtOFFoOgVVy6bSuLvEg46Haq//KHY178Ov8xtqj7LJ0MqamG+ScBFNeIbm68++mwTx2/Ogmoqw6/z8PKqbUjULF87pQi6FjlwQpeoALrSpLEsVFVwE09AgBYkpjORPAgkqFNJwqzTcj4sRAgO+RzUXlSMEaJuKHi7MuHinqrFpmghcUkfj8/PzmOdrPp+j15PwkFQBw8V+qtChajFVRG2Gz/G+AVnUpuGWvCcucnnNFC4QQvBamwqkVPG5CTh5HQKXtJ6NMTFf2Wg0wqVLl3BwcICmafDkk0/i0qVLsd6YcJ8LZLpn8nVegz9pWtPpdGJoLVWwVMMxXJFKUebc42eogC3LMgLO1GmXDsdUKtJdeTgcYrVa4dq1axfUUQQkhF4HBwcX1GJsZ4IFHgTaVFClIIGAgmBsPB7j+PgYW1tbePToUawP9v8UEjNMl2AmzcVIo5Bbt25hMBjgwYMHqKoKn/zkJ+Mi/+7du7h16xZ2d3dxcnKC7e1tNE2Dk5MT5HmO0WgU6411ulgsYv+kkUNRFBHy0r2adUaTGoY2E0QNBoNovMJ0Cb1eL6oEUyhLFRhBNxW9ac40gvoUXrJNCD55bmvtBcUm4TsgIO9Tn/oUFosFtra28JnPfCbCSo4x9sVU1aa15Gu7dOkSPv3pT2M2m0VgnIaOpvMMf7Lcm1BzM4w8PR9f47GplqVaLVUypsrkdBOF76Uq6tu3b+Ps7AyDwQBvv/029vb2MJ1OI4AntONGU9qfU5DnnIvq0l6vF3PXtVotbG9vo9vtotvtxv6zvb0dFXlpyDDnqFQlSJOedPOI9ZQ+I1g/6XzfbrcvPI/STZQ0TQD7L+f9fr8fn4V8Rngvxlunp6dwzuH8/Bxvv/12dBOfzWbxOfpOanj2506ng+l0Guf6NJyf/YzPGKVUBNTp85fPVrZjWnesP6obx+Mxbty4EY1QLl26hHa7jf39/TjvHxwcRG+QpmlwenqK27dv4yMf+Qja7XZUHy4WC3zpS19CWZbY3d2NbcRUEmmO0Lquo2J1MwQ7Vd6nYJn1wftJ+wXb4xs9/mdxYf5f8vBKoGETkuVTXUMlE/MHIoQm27aKKkO4tQox5v8LOe2oDhMlnCjHvJHzZAtRAlJhZVuA6yh4G8wMsjUc1HatMDQl1jAwuOzalsBDhvcyl562PkIlAjK68tJ4xCgfYZuETYbzWFH2MedWVoZFceODMko+0zoV6NiaubUIJigmyrG4R0p4sISSmlJgJMEoosOzLPxUyKuWGl94o9YgMoRrm+Ak3HSkTnTjY3itGKBIuKhXgPahTnzI35gBqgbEpTZAAhsoeqh3hlvDSQ5H1QDNMJjO0HwkKDW9lhxYtq2hjwTOfGrvNbSVhYaHWYkq1Y4azK4XwTjHR1UXIPfw/2Xvz2Ns2+78Puyz1h7OPNVcdec3P773+Eg2+Ug2JZHdluRuSx7QQguOLSuJY8cwEiTIH4H/yh8G8kdgBw5gBIYNA46iJEYSd2JJbbVgiXK3eiCb3Wyy+eZ333Dnmk9VnTrzHtbKH2v/1llVpCO1/yPBDVzce+vU2WfvNe3z+67vEGUuCEUZJ10t6lWDVn/lTVVJ2SMvo1bGVkm+K/ZdbezOIUniZU1XzEp3L3lLXfHmTEeO8UcSs/b+ku7DiNpwAaWBSKOWBTaOMIl2Sb1d7cHgKC5JkpJIOd8zY538LtKOzVMWEUpBq5YxyxKKwsknrYWy1ESRIUuc7LZ+Zlm+36B+amkdF+RNTbTQThquQE0dUAgCqjtGbbS01C6UCyypZOZYWA4U01sl5TBCL2HyfMELLx1grOLB/gZpPefmrxyQmZh37t+i9WnCbMsBJMnESZgl8TodV2QorYhnhsVaRFmD2ZYmvVQ+lXvR1+jS+SAuew7w7jyppOYWWvuun5KZA+9mmxHp2EmiZR0RZrDOXRALFcisc7x/XDpeBbG4pHLHOJXfnza1ZyOrEpa9iHjh7q1oVECEcRYENnJjIplZzl6Dm/Vzyg8+7wGaZGq9JF7nbq6ZuJLJV2B4tl7ya3t/SqQMCSVJlbAsTMC6KsmtpuZTn+BGNHL+eSahRNHXCyJluTApKYa6zllY99pJ2ap+Z+7XhcsqdKVTgZK5jdDKkpuISNkqfbjlf39sGkyp0dJLFjahqxdMiUgqpmNHz6mrnO/MXqT1VDG96cC72pkiHZdYpcg7kfNitYCxqNwBiSbWDkxUqmIDu88smtqtr3sL7FENq51NBsCD6ToGQ2WSwczEZLPUhXHoKoE9lOgaHAsxccDd8bLtGIgVm1Dnyqc2a2WIVUlpXIhJYTTTokY3npOZiLJ0c9hLYBWgHVOzGWe8sHbKneYZf6X7p+xES/aLBvXEmdeqTOOJk6Uijkt2apc0GhmTWexS0WNLPc253T3n6LB/ZaNNnqvtaOGlulkptgeKVBVkfmeEiumusIlx/1cCnLkxbHqFA9dU9VqpKAuNjpQHIF3oigKjGC3q/NbsFo+WGzxarPHe2S5HZ13nM1gxAuNxxHLdsNy02OMWj37Uoz6E2rlhZ2Gx2lLWjPNhXdOMb2uKZkTRsn69tkHwTazNCkS1jk0oSdHWKKy8WB1GQE9wjEoLSBhMBZ4qFfTfz4+fqSPLMk5OTlBKeaAkz3OfRhlKX2FV/ErhK4BOeAjrSEA0Kd6k4JFCTwBD4Md8ucKCIWTOye+G7KSw8LjOVhAZY1hgh+BPaCwfMoBC/ysp0MN7CIFJAVcEdJGiVBiQ0hbX20/YgSF7Su5djpBBNJ/Pr7xfmIZhQIi0q4Cl0t4hA+46UymU/QmAEbIMrxenUpBKUXp2duaZXAIK9vt9z55rNBosl0va7baXsIq3nYzBKIo8aFiW5RVJq7RHmqY0m80rjNWiuJpMKkCSeP2FjDopYmu1mr82ASbOz8+5f/8+29vbHgyVMSWgg7DwRG4X9kMIWoQgrUh5ZQyFwG0ImE0mEwaDgZemC7svTVMvE7TWevmvgIWTycQDFnLuoii8v5gAfUdHR2it2dvbo9ls0u122dvbYzabMZvN+Pjjjz2bTkC509PTK8zFdrvtQT9rrQcehc0koJLWLmVWwKzLy0s/xsK5KkCSsEQFNAWu+AuGwJ+APKGNQMj2FamysCXFn0186Yqi8ECSjCutNYPBgJdeeol33nmHu3fv8uDBA39u6QMBA8NE63a7zQsvvMD+/j5Pnz71YyBkjYZMMpnvAqrIWBDZabg+hYBluAERrm8hGCPAk4zL0ENR1hJZJ8K1M9xIeOONN/jggw8Yj8c+nfzp06d+c0g8X0NGcJi+LNclmy0yfjY2Njg6OmJ/f9+PsTAEpd/v+3VU/oRHuIk1n889cC7tETL3wjVP5sP1uRoywKXthAUn8uzhcEin0/HXGa69ISg7mUw8yC6AtLBrw2ejzPVwg0fW33AsyrXLWiLXL2tV+KwSpuFPemaEm3uyXp6fn/Pqq6/yhS98wW9GnJ+f8/HHH/PkyRN/DY8fPwbgxo0bvPXWW36dkHTo4+Njz2aUufbOO+9wdHR0hf0pm3nhMzBk514fN3AVMAxVCOG4kPb5s7AQf+oBRJQDlJJpxXjTK8BNGGLexzBdgVveoqkiaXl2YQx53X25j6qAEHm/DzOpEoSLxirhOJ6vzgn48AZlLFGJD4WQWqOsg5UU50DqFC0dOKUrf8Ro6STHRcOBo0VTVam9q/sVVmVUSbQFRIMVgBmZCpBcOjZcWQGikkBd1N29LdYdWzFauKAZSQcVebL4qUULu2JVlZUBvQ7aQOEkz8q1jQTHRJn7jGhe+ThWnxllVZupKmxisWJEmoQfC7KJFg4MthHk9RUA7II3KplwYh3rceq8GVGuzaKFAxVNzTERG0+SijXqAJmt9JKzsukK+QVM9xxo0zw2jtFYgaTCJNMFOA9G6yXx0dyBtdHS/Z3mxoG3mcWkTuZqYneerKUcUBikR0cL66XsOodkbomWpkr6dRN80XfgYnqZU3ZqxJOM2tMZJNW0VsoBia2GT4YumjjpYFW7ZlnM0kK9nlOUEblxwAU4eWOzviTShlgbSiW7fJqyiIjiEtMw2Mj9vHbugOLZZuwYfqOSxqkl70SUqcXUNKrUladnJdUuLGkl852uR8Tz6t7WLbWziDKFyW2DMooHP7gJylIba+qndd6+1abYXVLvL4AEq2G2Z8kHBe1PEmpnri9MAvUzS/vJnLyTkI4VeakY/kJJ94OYtY9yF4LS0pgI6mNDWXPpxHkT8pYDAaY33dyvH8c0TixZVzG5a+h/qJ18WUHWdkCt87i0lcehon5h0PlK2l4/BxNVHp/LCkAutBv7kUtmLuqOOekCHxTNI+Pmet15s5VV+vJiwzF7X3zrEf/fB2/Sebya/1HmpOKLnvYWDEUVIiXrxPbdM+6kJ0xNDWM1iSlZjyZeTpwAiTLkaIxVPl1ZmIARTr5cj0qfmrwwCS29JAFyG9PXcyIsdVWQRBMffDKrEpcBTsoWSQVSahwgNjZO6tDRc/p6ztjUaekFHZ15meoCRUcvSDH8nadvokrr5MuV36YqZINBURsXqNI4wLA0mE4dIuUCh8ROoHQbROObjtVZLiLoFcSnCSa1mJqTqea2RCsHIR6WXZjEDuxKLch3D2094EtlGUChOV82ncy3SmEOn2cAmYld6m9aUIsKEl0yN2532hgVJN07QAyj+EbjE365+REzG/Mw3+DvX36BYdbmLGs6dqTG+QN6wI4qRCWlnhRMtDyHLNN5jVkndb6EFWgo7G4dWZYmcUAZYCufVK1cErSpQE9lFVYbz1ikrD5XJNhAqz9netJcEfE0LmREEqrlvca10+nDNf53n/z6ylKE6pk6yCHXxFNd+RwrOo9UFZhkKRuK6Q1N1rH+GekCw1z6sioq/9rYekDWPa8teRlVZEGLjiwmj/AJ0squZMylYlnGTPIUlTm7EFX1vy0r1qfcj4TT/Pz4mTukgJG03FAOJ8BUCKaEzDhh+0lBI4cUwgIkSqEXsnKkYBZAIWSwhPLnsBCCq75gYXEu1xQWXoAHF0KGY1gAS4EjYBLg2W0CxIWyaflsYVdYa316byi/DVmKYcF5HaQUEEkKSin+QgBQKeVBMbl2ATMFCLkOdIaMQ7nXUDIt7SVMGLn/UGIphboU5wKiiLeitZazszOm0ym1Wo3t7W3q9TqPHj3y8tCLiwsvm5XzCYAQ+j5KUqswdqQPpE1E+ibXIMW5yPUEzBS2mfj/NRqNK8CLgEqTyYSiKDyTEuD+/ft89NFHnqkjQFW9Xmc6nXoPPQnvCRmaMjfCPg+ZZiHQIYewuQDPxAK8rFcsBAQslKCJ6XR6pbi21tLtdrlz5w7L5dL79okHYbvdZrFY8Nlnn3Hz5k0ODg54/Pgxk8nEAwCbm5veJ1HA3eFw6Iv+brfLaDTywM3e3h4AW1tbLBYLptOpn0sCkElbK6WuAA3iIynScRlvkuQqYLTMZxkvAioLE06AYJFDihQzZA2GctNWq0Ucx0ynUwA/D6bTKbdv3+bo6IgXXniB73//+4xGI8+iEkm8APi1Ws0DT9vb2172LizbEKQPWYMh0zlcf0PAJ3xvCPzJEQKR0j4yd6X9QwDq+np7/echqNXv99nd3eXDDz/k9PSUbreL1pqzszPP8BVWnswlSSe+fh/r6+v0er0rHqmh3+ZisfBrvDAVT09PfwzsDO9f1kjZOAjZlCEYGrIyQ+a5gNhhf0j7JUnCYDDwDGnpY1krLi4uPJs77DfAb7TIOivjIAT6QmBYXpP+uA5Mhn0kgKcA6SETU57ZIfNSNi1kTb2+SfGjH/2I7e1tbx8hFhuy9krgUq1W4/z83J9Pxhvg+288HnNwcODPEW40heM3ZP2HSoGQfRiy8cPNupCZGM6ZsP/+WY+ffgCRFatNkk2F3Ze3HPgUV8EepaQnlxXrsAKBBBSzVQhIVAFhRdMBUboCrQT0KJp4MDHK5JwWpatOLpxcMJQtq8KBdUhgi3KgXVmrZIyXDnxxBUaVeJzZK/eoS6gPDVmnAjdzB/7ZYsV4FL9HEyviwhVBRRXkYFJYVF5/ZU2Rt/Aeh84r6urAEfAxnq88CYU1Z1UlMbZgq9RkqyDvUnk0VkER0QpkWw5c+IgLwIC860CteO7aN55ZipY7Z1lTHhQVQguqqr+rfo2WFfPUukTNZKKoXTi2pXgqJhPXx7piSy7XnAF+ulSoQlXJ0O688cIyek7zi82P+a/O36KwLrRi+lxBd23KZG9A69A4puHCEs+rAjLY2Mkb1ZiyDqQt6o4tKIE9IiHVuWOGxhPrJdkmUR74VpbK465KRm26RGMXouLaL5m5MRZNMkw9Qee5Aw/zaue2NKAVppn6QJuyYR1bKTFobTFGUasVxNrQTHOyMiLLXYO3Gu7LWFZUD6WkcPJJo92wVjjGVeTuU+cw33QvJBMXQBEvnc9hUXdyXlW6IJQycXLsoq6YbWrm2xbuTYliQ/64hc4UZepYeapQJDemGKPo/HaLxRpkvzxCFREcNVAf16mfOMC58xmYJCGeVwBe3bHt5hsKXTowatF3YFr3o5j5tuW4lbD+fkFtP8fEisme83NsP7YkM8uyp1hsKNKRS4leDmB8pwIna4bL5yMmtzXtR24eLtY0Zd3SPNIO6K4pLnsR6cj6BHIbOZuBZGZ9PydTaDytHiiVZ2LjzKzm3dzQOIW8FVGminhpWHYiaueW8V349Y2P+Vu//ZeoZS48JcrwgTYmWTGRy7ryYU7Zesk3dz/hrGzT1Es240v6es6iAvbSKmlZPA4F4MttRIpLXhZ/wghLogwa6xmIdZVTV3mVnBxzaerUdU5fLxib1J9Pwj4WNuHQdGmpDK0MLZVRD1iKC5uwsAkZEV21JEPTImNdL0kUHL2zTW3gwLV44kJ6IPAjXZaYWBMVjqFb1mMH+ldrt/MstSzXE5ZrMHjrCL1MuTzsYFKLjS26m/Ny5wiNRlfI1/emz6OXyjOfhX0XgmVuUQUyxfG4jRGWYs2sADQLxmoHyGlDtkyxVtHQGevJlHmZ8KnaWJ2rWheJLH+0uMd/+eQtnp72wSqe3znhZsvJd42pWHGV/YYq1JWU6DQuPCCrtKUoImLlmLdUwLO0j/RFPXJ91kozmnHGZV4ntxFlBTRjcQCkcexBSrCpWwNslUDtv8PE1jMR01rBzbULPhnvgFZYSWEpFaqXYQtNVM/JZylq7DyPOUvQBsp6FWxlFaOXVvJrYlOdvwIIrWM2ug2bSs4vgTfKrkBdq4i02zhSYVK0gIdR5duYa4gstahgrb5kP96ogFrrgmgqwNH9vrtPq+2qYX9+/Mwc8mV8PB4DK/mUFOLCIBFJb8gECQ3pw+JHitWwCAhlbVJkSfFznX0YMtyAK+CYFAzCLguTl68z7KQQkX8LsCSgohQyYfErh4BvoS9j2D5wNQRGGFChDFnAO2FiieRRPNrkuqWQkmRaAVbDhN2Q8ShgrlyDsOkA3/YCgoUSxRCgkKJT2iWOY3q9HovFwns5pmnK+fm57xMpAuM4ZjgcehaYjJF+v88v/MIv8Lu/+7ucnp7y4osv8uDBA27evMn+/r5nhon0Vto+TVNarZa/NgE3QnahgIFy/8ImFCBH2qTT6fgC+7pUUQBHKbwvLi6o1+semJJ2C8diyDIVD7+wsJe2DJmiUjALuAwruWsI1orcVwBiwPd9CGpL/wqQIzJEuZe/+Bf/Iq1Wi2fPnnmJ8KNHjyjLkr29PUajEdPplL29PZ49e0Ycx2xvb6O15t133/Wy7na77ROeh8OhlyVPp1MPJAhzbDKZEMcxDx8+5N69e5ycnHByckIURXzuc5+7wsYSpu/m5qZnQx4eHrK2tgbggUBhrd27d480TTk5OeHhw4e+v8UnT8axgDwydgQUkT4RYCJMfZVQGWGyybz74he/yPHxMUop3n///SuMts3NTaIo4uDgwPvbCeN4b2+P/f19nz4rgGUIJsq8DFmEck8h0yoERkLGagiWCGAdMrJDYCxkKIZtJWvJ9TU6BLm++tWvMplMePjwIcPhkFdffZXLy0sPDidJcmVDQa4/tESw1gXY9Pt91tbW2NjYIEkS3n//fd8fAmjLvOv1eqRp6tPWrwNq4bqulLoytoS9HK7d4eZWo9G48mdtbc0nSsv8lvVxPB7z3HPPURQFw+GQ/f19Tk9PGQwGNJvNK3594eeF1ywbSDJ3wv4J7Tfk2XZ9s0QAVxlH8pyV65TNOBn/4Yaf9OX6+voVRmQ4ThaLBYeHh56ZHcrdxbtQrkU2K46OjvzGxPVxLZ8r57g+zsLNsesbKyGLUtpBNpZks+86QCjnkrH+Zzl++gFESxX8oShtFS4SKwzWS67iabXgjCtwp1gxAqP5CqAS8M2zd5YVwJbbKinXvZZMHTDm0kupwkYqRL8KwTBVemtUMf5EpiteikUDFhuGeFpJj0tF3oH0wvmnQRUQUsmDdR6wtnK8tLhMHFhHxVxTFifxtXbl0VhWnoWpAxKLpqJxYmicVEBi00kvdW5JR8q3kdy7SMCFEZd1Kkm3xgejKCMBIS7QooirkJUYSgF1c/dekXJHS/czAWtNwISM5+41qK5F2KQVMCegI/UqJCJRJBP3uni7gZNwTgbOWD+9UKi0kjHL8BEmTsVunH1uweuJ5f84W6OXLIgXlqS7pJHmjFpVmm8FippU+fYR5qBjOLq+Exl8mUrB6GTLRUNRVAEYi772AGe0tM7zsbq+aAHJxBAvKpp+vQpTURWDVLs2UssS20wrybp1CHgpXluashlX7VaN0cgSxc7bUCkoquTlwpQu2CMy5Ln7WT0pXHCKEdmVYxxFcUlZaJdAGjkZf+9hTvtAoZcGk2gffhNlq4TqReSuf7qn6Dx2oO3JWyW/9tXv82i2Rj0qeCfZ5fK4DYVi8845aVQyXtS4POhw8apFbS4Y1HJ2N4a8e3IHLMz2FMs1g+kUbP6+Y5RmbQfouARnmK9plmvKgyDxwvkuZl3L2SuxkyjnlsvnIe8V9D/WpOMSiNC580uLF4biSHP+UkQygfQipmi5/sj6UD+15D1LsZEDCeml+7B45taL2sySt6uk5W1FMV4xbIuG817EOpBRF84LMr10nmxZq9p9qtqyrEXMdhStfcPal0/4g7PnaR1YH+6kC7d5MLoXM3sup/d2Qm20Slkv65b1u+fcrg1JVEmqyiuAnngUNnXBftFBK8OaXjAzMRGWpi6YmRhdgYNj436Wm5ioEvgCTK37Ii/S46bOmVVmrXVV8qTsUFrNTjxiWLZp6SV1lZMReR/EYdmirnP24pGXTWdoOipnZN0Nv5Nt0L8Pl8/Leq+Ip6VjWdcikqmp2HQWtSgwtcR5Hxo372zsgEQbKeZrEUXT8mu3/pRR0eT/cfpVUNrJbrOIb3Q+pqZWj8+H83W3nkSgF05ibA2oymPSA3ELDYllMqmT1gonla1esxGQWBJVEKuSzfaUpHvJr+3+kF+oP+ROnPNRu8EfPrnLlcO4903KOtMspVbLKUvNoD6jFhUMlyspuCrdWqgXDmStJc7zNC8r3wfl1gdbKj47X1sxJ+U6BRe0mjQqMEYxmtf5/NoztjpjvtJ4QFMV/P3mG5yy5n4/sjSexiy2xHt1dc7NzpRHp03fLlgoC80sTyo017ErXeOCvaiKwnHsnrmRpay5TREUUFSgbOJ8FVUlQxZGqDLuGWziim2IwqTmqpy4dGCrXroHal5JtK1R6IpVaAsFhcbmVO3iQMdGnLMok9XnimRZ2Kew8r0MgeWfHz8zR8heky/s4Zf90NctlG0JKypkJco5hLV3XaIVgonC/gm9xcLCSgoTubZQCiWMKWFpKaXY3NxkfX2d8/NzhsOhl8cKsCZpjuG1heyPUF4VemLJdYcy4ZClJYw0KfDkM6SoDsFMrTW9Xo+trS0mkwmHh4f+NSkyw7RPYRcK6CShBpPJxIMmIhkVMFYKwjD1N2SFhEEY0oehp6CAL9LmSikvKw5BRwEepUAUqe7u7i5pmnJ6ekqn0+HDDz/k5s2bXvYr40FCN6SIlVRnAVTFN0/ko1LcCuNQim05pwSbSF/J+BXAs9VqeeaMgL0iQT0/P/eATNhmMh5knogMNmR9yWtSCIeSwZAp+pPOXRSF92nsdrscHR3R7/d9MIkAuTJPOp2OZ4RJ8EqapnS7XebzuWd+hsEOWZaxtrbm52Cz2aTX63FycsJ4PKbT6bBYLNjZ2fFjbjKZoJRiMBj46xQgt9freSBDmI+PHj1iY2OD7e1tiqLwycn9fp+PPvrIA07T6ZTRaIRSio2NjSvBQKPRiPl8TqfTodlsejapsE1lDstckQ0IGY8iE5f2kr4RtqIwIcVWIVx7tre3sdZJst9//30uLi78WNRa0+12ybKMra0tnj175ufsxsYG7Xabd955x/e9AGwhy1Dm1vVNinDNlPVV/g7nbcheC9dqOWSeXmdtyXmk/WXdFbA8/Kw4jvmFX/gFDy41m01arZYfUwKkC3Bcli5cSVi0IRDZbDZpt9sMBgOMMWxtbXF8fOwTegeDAdZaRqMRgJcIX1xc+KAPWetDeas8O8qyZDQa+fERPitkvMhY7Xa7nJycsFgsqNVqrK+ve0aktKf0w5MnT/w9CmNW1uXrjEiRssv6EoJncm+DwcCPu/Ac4SbJ7u6unwvdbpfj42NvMSC/Kyn2ZVn6EBqZQ5PJhPF4fCUVXp4LIdNPrlvW2jRNr2xISfvKOjgej7m4uLjC8pR1LRx7IbtSPkOuQeZCuP5d37yS88q5pG3C+XF93oTfEf4sx08/gChHBRwVkZP3mbryAFrZqPz7Go6FFRcuNMR75VXSWRu5wAVwTCMT4ySwY+fRVgpDULniHA3xyP1etLRe4mvrYNQKXBKQTT4zWlbnf64gq2uiaUQ2KFm/d87Z/TUax5r5tkHnChM7kFHninjugjOihStgyorpljdXYJutgDontV2BoC48wFIfGcqxAw1t5MDKeOo8+tAObC0bauUlqJ1PWzxz58j6ML1bkJ5ENA9UFcIgIKIDZLzfGw4Eq80cU7JsVGzE2IXNlHXpNweQ2GxVXxaNSvpcPQ+VqUDbxlV5t85VxeB0IO1ivZKtpZZ4qlisu/emZ+66ipYL8/BS9yrsReeQzAxfuveYREXMipR6lKNzSxwbJyccQ/O0CikQtmDln1kmzhvSJK4Psp6uAmQsVrm0XedHCbWRBWs8IF2mVRiCEtaiA3arO3cgayKMV8fg1Lkbd+nUBUS4tOESm8bO2600YJy8uGjEoJxUPF/LUUZRli5JWTwNjVHO99Aq0tgBA3kZ0STHWiWkHQB0JGlOGq2Nk/23nQx7shcx31T0PzE+rMMln1sapwXYmKLpQNNlXxEvoPdhzG8df430C+fkRcRynhC1c/76qz9AK8tF3uRw0WHUmfD4dMDtjXM0lnFWI9mckw0i7u2ccjFvcHrQ4/wVKPolyYWTTqcjTf2k8rysGK/zLTfm+p8YJruayV1DfahonJTUzhOKGwUXL9WpHzsm4HRPMduJiDLH+NM5jL60JDlI6X3i+me24+bNzh8a8kbs/q+h88R4P0aroPOsoKhripryQPNiTXuWXDJfbU6YwGs19GoVC4Lk0jLd0fyNm2/zn/2TX2bN4qXw4p1ZP7NA4jYxxJdRQdk0fGvvYzbjS3biESklWhmmNiWlpFSGlnIgYUcvaFZsRK0sGuPAQ2WdFyvWSZfLOk2dc1E2aamMhXWBKvXIBaDoislYVwUZmplJiLBEqqSuSnbikWcY9vWCpApX2YnHJBj3WdqNxAtTI9IZPb1EA//hZ7/iWMktA0bROHahRQ4816SjyjMrN6iyxLTTCnRyjHCrFKqwlDXNdFfB3pxfbn1AbiP+y/gtv8GkIuPTqQFKa3hwub7yyauAMGXUinxYOtmxBKwA9NszDoeNq8+xQvFG/QnfvP0xedU+by9v8d9NX+XZcsA753ss54lbM7PKrzGx3gOxlWa00oxfWH9MiaapM3TT8qHeciClsoTBLlpZL0UWBiLWbRJMJ3WwOCCtWEmZtXYel692DvnXtr/HrfiMvs7QwImp8R8d/SUenQ0cGFk6b8dkCgsFiC+khcHmmJ3WJY/KrStsyvy8xuFRAxrXd3oqMFbjAMPq97Gu3YgCduE8Wj036sazASmrDYTYrkBF6ZvErEDUslrzNJSVjYfSFpNrDx6ufA2rdTEtKawm1aUDkKtQHIUDH+VQsUH5NK6fHz9rR1g8hP8P2StSMIhstCgKRqORB+NCxoAUB2EqqzGGy8tLD7CFrMWQYRPKRkM5cihzFh+ujY0N9vf3vZz09u3b1Ot1bt265VlUYdEn9ySsF7gaQBCyJaT4EUaRMODk53Ec0+/30Vr7BFphN0qhKmzC7e1tGo0GT548YTQaeaBNJMnCWhSWogCUsErVFUnd2dmZD9yQ84icWIrukOkmhbL0a9ju0h6hVDr017LWekmrgLQCFlzvw1qtRrvd9qCuAJmDwcCfP89zL/GWAluASwk4ATyrZrlc+s8XKamMLblvkcPJ62HQgZxDAEMp9kN5rPSrXG8YACBjQiSbSZKwvr7Os2fPPIgoY0sAGSmkhVF4HXgIAWUBFcSTUV7vdDoeMHjy5IkfoyIhbzab1Ot1zs7O2NraYjab8cMf/pDPfe5zpGnKcDhkMpnQ7XbZ39/n/Pz8CrAiPx+NRnS7XV588UW0drL+k5MTtNbcuHGDL37xi35MLRYLTk5OOD09ZbFYsLW15cNXyrKk1+t5xtV4PPZtPJlM2Nvb8yCpgHviS5hlmZe3TyYTarUag8GAjz/+mFar5YHMTqfDdDr1TEhhrQkbzRjjwVVhpQoAKONE7kPWFwHFjDH8+q//OrPZjFarxQ9/+EPfNwJun56esra2doWpGkURN27cYDKZcHBwcAXMCIE/+X+4noWMbVgBJwL0yVgK1yRZW38SKCTrh/SXHOG4E9ApBNvCdeH27dvs7Ozwm7/5m2RZRqfT8aCesMJkLZa+EzBPPkvG6fr6OrVajW6360NFbt26xSeffOLXyXADQtJ3JcArBA3DuRI+L+R502w2fb8IgLy5ucnbb79NlmW0223W19d9u56fn3N0dOSZvRJ6JdcfJhyfnJx4X1BhjYftKuCZtGV4yIaHvE/WBtk8uXHjBkmScPPmTSaTCfP5nBdeeIE333yToij49NNP/Vjtdrt+80j6rNVqeZ/RkGmtlOLhw4c/tqaFm3TCkAyfjT/pGShjVsaWrCPhuAmf9SLlDtf8UGYv8/InMeJD5r08m8NrC4/rLNV/1uOnH0CsQDuRgVrtpLCm7QqBeAoYYbk5kKdoKmpnlR9Z6jyvdO5AmfTCet+5vOXAR+eVpDzbURiFWOW9lKzGg4vR3BUy8RwvjXbghWP6RUtL+8AyvZlQtkvSkWK+XtKtLzjtlJihxvRzzCJCLzT5wKDnmrzjwMxoqagPnfwRlGe/6cJiCuUZhwJclokLWcibTi66WFP++pw6zAFxRdvd5/SGwTQNyXlEvlGQdJZEf9j27Mn2p042FuWWrO6AzHjmCtEkxycxl6nzaytTB04WdceWLOuQt5zkNJo7CqAAkc5PkIrN6Hz1lgMHQqWXjuFptaqk6lUwS3Wfy4G75zIFk1qYKy8RF8ak1Q7EK6v3SVJttLBkHc2/u/fbGAz9dM7j8RrJtHDS3SJiesuQTCJM4picbhzgJczCPIwyxzzMm7ryqLQezFk0dcVo1d6LK1q69osy61N8EQZt6cZj3lSeNVvW3XjWOdSHGTaJUIWtgiDsSsJsLdRSyrobu4t161JAlStqy1JjLaRpQVlWxuvKYqvCNo0LlkVMaRRxVDKv5JSrSGtLFBuymmPXCtiVdy3zNU1jaDyjN28rlInJmw4EX9acFDg6dPde1hWD5pzCaPZ2RmRlzP3JFufLJlpZRos6Wln++ec/4CJvcJE1ybKIX3/5h0zKGk9nfSJleP2NA47mHXYbl5ws29xsXvDRaIvxsoZWlvHvbzH4KCfrJsz2DMlY03tY0H2sWHYV092YtQ8Kug9TLm/D+J5LJHf9oShajkEYLaHxaY35zYLTr0D7QUxZc+zFZT/yIPpi3aILTePYgc1lzY3v2thSGxvypiKra2oXlmRaVsnUmmQCtQtny4BdjV2oEp4TN87bTy3ZXx7xe8MX6H4UubGmnYTcVHMqnkH/rKSou7lvtZsftm7YSi8ribEhqUA8cGzBdZXR0SULa8it5sQ0SauQFWM1CzTNSl5cVwULGzu2oYG+nhFhXRBLdd2besbMxkSq5MI0qKucRJX09cxJl4sOkXKhKGvRhIV1HoQLG1OnYEZMZJ1Muq5KWirnSdF1ASumxvHv7hFtu/kWzzTN0xJVuPRlZ2thwFjUPPeAoYA/tnp2REvDfLNG2bS8cuOQL6QxQzPHFi6Vl8SgY0vCCtya24zDi477jzDh5Lzyf3CglTy2tKUWlSsgLDie5Ov8F+ev8d2nd5mfNEnXFmz2JrTTpQf7VOk2lcD6NGVjNZE2XMzrPNc4YVLWGRUN1tNJdW14ZpwuwRiItMFUMl00TmZcamyp0I0CYYqrXLtnRMtg8ojnasckquCz5Tb/4PwNvvPsHpOTFnoWuXvuFqhltSm1cO0vkmkBFZd5zA+e3HReiThwj9RAqTC1CsyLDBJYgnHtr5YVNzayqx2nyK7k0sr1g42sOIL4+zexQWUatVx5Ekdz7b4j1B1j0Vbnqs7sGYgmq0DJ1Pi+tPJLRmGrTZclsWclKm0xVbiLdTi1u484uPafHz9TR8hACX8WMujkjzD+xItNQCcpjgTsEJabSE5DBst1s3cBpcIQCCkwpFgX0EnOsVwuubi44O7du/51rTVHR0fcunULwP9O6FslAIWAE5eXlz+RsROyJKRAkoJGWHHClgG8tDOOY7a2trh37x7GGI6OjrwkrdFo+PeIlEzSrqXdQolbyH6UdpF2lWsPwQXgx5iarVbLyw5FeiesvTzPPRMlBBAF3AS8jFrYeuInGPpJioQa4Jvf/CaPHj3yUtqNjQ22tra8BF4CXyRsJmQ1ChtPwI1Wq3Xl2mDF2IvjmHa77T0JhVUjY1aAiWazSafT8fci/TyZTDxgJIVuKNMPC1YBKHq9Hlprn8AcMm3kPVJcS/uL3FleCwEXKXyXyyWdTudKGNH5+bmfTyFjzlrrgYk0TRmNRjQaDR48eECv12N3d5fj42MP8Jyfn3t2poBP5+fnHqRZLBb84Ac/8MBfo9Hg3r17nJ6e8tFHH/kxtL29za1bt3jxxReJoog//MM/5N69e1hr2draYn9/38+/Wq3GgwcP2N3dpSgKHj58SJ7n7O7u0mq1ODs74/z8nH6/z507dzg8PPTjrV6vMx6P6fV6fizUajUvXZZE5/X1dZ/oKn0rr4WeaY1GwzMQZTOgXq97wEMpxa1bt+j3+7z99tsAV5iyMv4eP37MwcHBFUa0MJKFMSeAC3DFi/C6NPknAYByvfL71xlXwhwM05xDFqzMSbk2+Xe4lgK+DWq1ml+T1tbWGAwGvPnmmzx+/JjpdMp8PufOnTtcXl56trNco9xD6F8abjJFUcTGxoaXpT969Ih79+6xubnp2wXwlhPCdBUQWZjfsq6FYKKs/fJzWbtlzsn1LBYL3nzzTeLYhSMdHh5ydnbmQXPpXxlzIVgl/Qf4eSNsvfA+w36QQ641ilxKvTwrQha8bNK8/PLLnJ2dMRgMuHv3Lrdv3/bz7tGjRx7MD+9RPA1l3RmPx35+S9sLCHpdFSD//kmMQnlNxqKsYwI+hmxVuT/ZnJLPC78PyCGgoTA2ZS2TdpLzhutmuPZeVwn8JLAwbP9/2vHTDyBaKBtQGlUlu7piULwJ47n1noiOIceV0BITA4nzNXQgoiWviU+hAxTjhQPnioYDSURSaqpzSkJuMrXkHUU6cpJJk1SSzwpc1KYqOkLW28Yc9jtQKB4fr6HrBbpMSJs5mVHYTEMnp2xq9ChmsVcSTTSNk0oenTiZIrb6W1W+jLjPN5FyXoW415KJJZk5+a8zjaeSZ1uyDg4UXcsh0w4wnGtqH7epn1mX7KxcW6lKhiw/l4TorAcpDpQVD8isHwCsRtiDlbRLfBsrU3wBHkUKLL6MOneMrGTs/CKVtSz7lYFuDO1nhtm2Y7aZGJr7msapZbalfD8rBWXDeRcmU3yYjM7duJjc0HyrnvOwWPJK+5BPR+vEFvrNuSsMZ4rWUcl8rdoxqEBRk1QAqDBXLFXgiwP/0omtkqUreXvsCn8TV3JUL4fXjpWp8KE9AsbGCwcYi5y+qFcekZMMG2t0VlbV6epQeUHZa1aAq6JoO6DByfAMxii0+J3FJUnsJMxFGdGqu0FUGheIIAAjVMxDVRJXMmjTdJJlXVj6n+Z0HykPokS5Y9q5NGRDMnOhJGVdUzQg6yuah5b2U8OzH+zyv/oXf4uDrMfSxHw62aSTLDmcdthqTbjdOmeQzPjB6S369TnPdYZMyhrjvM7jywHLPGaWp+y0LmnFSxpRzmVR4y9sfsJXW58yNTX+b62vc/zZPSc//tKI5Y2Ii5Mm3fsRtTMHDp7ciOnfN/QelBTtiOVaydYfKRonOUUr4uzlyKVNH5VcTGNm2y7oqP+RA6rHd908d+C4MJmdP2fW0YzvOVZk48SNB1W6YJ28Gbm1pObYuqpUfhyUdYgnjkFrYsg7lmSsWPYVf+35P+Vvf+cbbExl48AlecMqUbhMXQq2qbn1Mu84AOM07/CXWu+jlaWlCjROVjw2ppImK5rKMsN4kDFRhggXmpFb7b0Pa2pJYktOyg6JKujoBRro6IwIS4720ue6KilRLGxETy+p24KhafoxtrAriXNdFVyYBsOyzX4+4LRoc543eTxd47PzNcbDFmoWsfnIMvw8KKuonyqSSeF8P+vaMYCrtHZlrZMvp9qtU1GVvmzBxor5hibvlvz1nT/GYKkpTdrJKC7dJFfKenl2pDQnRcFyUkOrik0dsuUUiLzVimbVQrmM2D/rrsAt8My6k6LLD45uspg5hqTWrq036lPOlk0HSmkXnmJ/fKOW6bzGPzl7iV8cfEovhkeLdcpC0sOACO8RGylLbl26ulpqB+I1CjZ3LmkmOY9PtymaFhsbrHYJ8s33GvxvD/4NivUctYgqRiOQWky3WIFlGmxsiae68g22ft1EwXy/7diwAqaJTBq81JrSgYYUFUsQvWpHAyp3oLkDDan8Eldt6T6wAijlvDWDrZWwiBw4n2u3cWVA5Wp1PZVP8jxLHPgamyuBKWEYlQCVkTYkyq6ASAENlUUJWCjn+Dl2+DN5XGfHSAEnRxigImCdAFJKqStyYGFJhRJgkfVJ0SEFt7AxQqaWMCwAD7RJoSFMQbmO2WxGp9Nha2uL5XLJ4eGhZyTV63U2Njbo9XocHR35AvXs7IzxeMzOzo5n8EnRcp35J0b0IZtNriVkjUkYh4AX4e+Lp5eY00uxF3pHSoEVhpsIGxEcYDmbzXxfhImcoSwyLBgFJBDmYFj4SxFurfXpwp1Oh1arRb1e94mrUrglScJ4PPbnkPYPgV1hxD333HP8wR/8AcfHx94Drd/v0+v1vDRV2kzOJYWngMNy7ut+kuK3KL6U4tUofSbjSynl/TdFbilAkABSAsYIeCss1nCcCTtSQK2dnR0f5CIArBS0Mj8EYArDgcIC/rpfWVmWV5K12+02Dx48oNPpeFaV1prNzU0Az/rt9XoMBgOePHmC1ppGo8HBwQHf/OY3fZq0sM4ESBPQTFiOkuYsoIZ4vH344Yd+Lsv4bDQa3iP1W9/6Fi+//DKXl5ecnJywv7/P3bt3GY/HHB4eUq/X2dra8oDoSy+9BMDm5ib7+/u0221qtRqbm5veX+7w8NAzdmWMzOdzL2MP5eKSVC3gzHQ69XYCMv+kL0Jpfq1W82NGxtba2hqvv/467777Li+99BJ/9+/+3SvAn7SXgCitVsvP036/f+WzwrEgf8J19LrVw3XgI2TQhSBRyHCT8RUys2UdCUFFWV+0dmEma2trXlLc7/dpt9v0+32/EZRlGfP5nN/6rd/y86nf7/PBBx9cAXQEKApl5CHDTGvtgf3BYMB0OmV/f5/t7W02NjZ+DHADt3HU6/V8krHM9+ubVyEIq7VmMplc2RwJQbGHDx/Sbrc5OTlhNBpdWSNC2a70mazfAoyen5+zu7vr71Ek9SFDL9zkkvOsra3x4osvesbi5eUlFxcXfoNEruHp06e8+uqrZFnG97//fYwxXFxc+DAqmQeymZNlGa1Wi9Fo5AG85XLJo0ePPJAYjqsQnJV+CVmScg75WWjfEbJUQ5lx+Lq0p2yOAf65H/ZxuHkStlcoUb8OlocS6evXHM6bPwvzUI6fegBRUpSFCaaLKkSjqJhaecVqE1AGSBYCBlZy36qAd2w05dNKs65jGkWZOyd2JR3VmZNlhsnKNmJVTFEBldVrunRMQI1jo2WStvq7HYoW6EkE5zF2d0nesWz2JpRdxek7W2xsjTg561I2lZOr7eYsjhokU8Wy5+SvwnKUkBgTubCReBYEKiSugMp67tokPRpc29WHimhhab1fqzwcIZ5GxBOqtE/3enrpzm9jKCtmp6QY67IKrsmq9tAC6uK9FAVsNIlrLqWrsJTqM3Xu/OlcmMqKRWWSKqFZV76POD/KsuYAtsH9nPOXEvI1S33oJMnJRDtPzMpnUVVp0kXTnT/Kqnu/KDn5iwWR0vzj2Ut8vvGY/3L6ZTpZSTtdsF6bst/bZLblis5kZonnFVCjnERSCsIydbJvq91YWfQVJtEkU8c+zbrKs1Hdvdpq/BqX8Kx9re0A4Jbz81MWTKQpG5COKmB7UWBj7RhVsUYtcrAWVZTY8xHl7Q1M5N5vmgWqCmeIIktZKuLYyZhjbUgilyqbFTHzLCHSxuMbqmJFGuP8D02p0XFBlsWgLaZmXQJxUxPllqLmQM5kbmkMC6JZwXIjpahpmoc56WXE6RsxZQqLgaJ1aOk+gP/4R7/Ev/Ty25zlLWJdMkjnvDV4SC+e8Xi5zgfjHfbaIz442SbvRtxun3OeNZhnCbf6F3xz42N+b/gCf3h0l8m8RhKX/P7lC/y9wetEFRCz2NIkE8v54y7x5oL2zoTLtMn6d11wxmK7oGhGJGPNYqsgWVtw9OdrtD9NiWeQ9S3552fk32uRXFrUFky/NkP9QZPeg5zmiWJ0N6Z+Zqldllil/BpRtN08Sy4dmK+sC5nReRWoFNgPkIJOKvC86eZc/cz9PkDjxDL8Rsb+ok/vgxhTJci6c7n1TTY/ylT5n5nYojeW/Nuf/31erh8wNE36es4U5204tTFpJR2uUXJmIxY28gxF928HIo4rs9GmKljaiIWN6eg5JZrcRmxGLn25oxXDUlHXhkjBWeVN0NEZpkp13okmnJQtIpXTUhkP8w3+D8/+BR6crrO4rBFdxCSXmnjq1oN4YWlmlnrk5lvWVZStApVr2k8NOnNf1MpaQjoqUMaiSlM9MPCAokv4rdjNkWOpx5tz/oXWI6COsRalqlT4eMXQjapU8vfzjZVktnThJJ4JJ4dxzxaqTRsyTW5St55XclksqMRQ0znNWsY0qbkE7zRnqzmmmyyYFUn1cHHnkeeSik3FDFUoBefLJpOyzkv1A/7J6YuUhTyI8Js4aEiikna05LnekLe+9oi/sf4d38f/1flbPNbbFK0KyKuet4tN4zbeFNB2C7FODM3mkm59SS0u2D/vYZ6lLu1bO29cEruSSVfMbHR1bkmGrkJnrLALZeMJVvJrcGCncqE2RNYDjJ79aXEAYOxCW1RWhcaUCtUuYJRgWwW3bg2Z5wkX74uHZfX71fcAG0GvseDYVA9Nr1euihvj2IVYUJFj4cfaeJBURRZTUAHP+L5Tkf25B+LP6BHKLbMs80VhCO4IA0oKaSkqhekioIoUyCKplWLiOuglIFbI2JHCWCSzIfNEio+QFQjw0Ucfsbe3R5Zl9Ho9L1Mdj8cMBgPPGut0OlxeXnJ5eelZdXt7e5yenvp7FXacSKRFhiWAx3Q69T8zxjCZTK544C0WC89iOzk5oSgKD2p8+umnHoSTAkiYdHI/4lEmQEMoe5P2/UkMmLDIDgvjEOSTAlI8uuQ6y7L04IsEUvT7fc7OzrDW+uJX5NthiMKtW7d8WIfIFtvttg/fuH37NhsbGx6wCMFGAUqzLLvCqBPATkAk6f/5fO7ZosJsFf/JMERlPB57EEBk5AKkST9dXl76Nmo0Gh4QEuBPwFM5h7Aru90uZ2dnVxhuYbEsgKD0pXye9PlPKpTlPQLiSX9KMS5jU/wdBVALx/fx8TE3b95kNBrx2Wef8dZbb3F2dsbl5aWXX0q7yT0JKGKMS0KWtpN/T6dTD85Op1MODg7Y2try7MOXX36ZZ8+eeSDnBz/4AVtbW2xsuMC0s7Mz5vM5o9GIl19+GWMMx8fH9Ho9AO/5JnLl9fV1Njc36ff7/PEf/7G/XqUUk8mELMu8LYEAqtZaDw6FDDHxxlwulz45W8aQgBcydm7fvs36+jplWfL06VNOTk6uMJZDMFHaS9aE1157jaIo+Oijj34sqCIE3ULfvpB9Lb8v8zmc76EcOBy/Mo8lUEhCY+Q1Aa6Epbm3t8fu7i537tzxifHCzD49PeX09JSDgwNOT0+ZTCZsbW0xnU7p9XpY6xLWZV2QuR/6VobMNLknASY3Nzd59913PZs2ZAqGzLdGo0Gn0+Hg4OBKKFXYRtI2AqxqrRmPx34TJ3yPPMcWi8UVJmYo6ZU+CgFEmf+SdD+fzz3ztdVqcXJycoVpKudpt9vs7OywtrbG3bt3abVafPjhhzx9+tQzgoUhKL9vreXZs2c8fvz4SrBWu932Y04Cj05OTjyQKMCgrDPCwA2Z6D+J5Xq9PUNGd9ge0reyLoRhN/Lsl7W50Wjw2muvce/ePSaTCX/wB3/A/v7+FVBQnh2ytss1hCByqIAI5dYh+1CA0HA8/FmYh3L81AOIVrtCXGSsJnE+VsJCFHadiR3zRmcrX7147hpP/OZ0xVoUEK5+5tKQTeWpiKZKLq68CJcOiIwWTmqYzKwLKUjc50aVwXqZVow/5YBKcEEHraeKzrOSMlF0HzgASdmERQ+mBzuOWZfD9HCb7rxK4q27omY5sNTOHfBw9mZB82nsGYVO3uY+u2goZncKoqkmmisngV5UbL8KdC2ajrVZHzrprQO0nETXJMqBGaxAUHCy4mjhajwliaNUkvGgUDIViChMKlVUhW/qWH/Kuva1EatU6AyytJLOVc8Hx8hyjL+8A/Vjx9zKW+5eZ1sak1Q04Zny/nECMAtTVBeqkjtbyl5B41FCsnTM07/2xg8BeHtyi7+0dd8xgGxJO1lysmxXxaW71qyjyBvqiuekk0BakrkhXrrAFMcgtJR153EXZYYo12RtBxiZyDHyBDyVcejAyWp8zypwNnfjNW/qKszHgSGnX+oTLy3990YVOOIMHtVan7ztpvhiQ3l5parSl+PYPYBUVclOFuIbozBGkcaGvIzQQaVrSk2RRyhlKfIIrQ1lYio2pvLp0cnMzYmippjuJNQuNXlDs1jXzDdrtA5K6meWxZoDtNGa2pmluEgZJDN+7/B5XuifspZOMSj+0cnneDbusdma8Fx7yC++9CnfOX+ed892mC5Tvrz7hOGyhVaGwmie6w85TLr84uZnTMoaj6drPDhf40ZvxNFfOiX7vQ1sbCkPGsxtg3TugO/2Y8vae449VCYwuK/ANhjdi5jtGdKRJpko5rOE8stz4gd1ooUiqudMvjFjudakfmqZ71gmdy399yPSiduAyHoOaO88MiQzQ9Fw7MzG0BAtHcgnqeDppXbrRjVHnQep2xipXbjEdhtZ/sU33uY3f/QmG2NLWXcgmCpX6e6qAslcSFHFlq7BF24/4c+1PqKlcoam6RiCVjOrgMKFjamrgrRK4o0qtmFeDVLxI9yM5o6FqCwJhr5aUqLIraauSprKkls4LCOMVZyZOodFn//69Ev86vo7rEcThmWbs6LN02zA6bLNZ+N1Hj3bgFECvZzOj2psPijQWUk8z5jupsy2NJe7Ffu8XdL9ICbrg8o18VTRGOao0mBqDvDXS8fQ1VmJTSLKRgKRqsC0Fbs268Us1yzfuPuAjl59sTPCGLNgSkUWUP9+NLuzAr0AlamVp58FouqPrdbGChxL2xn5PMGGm06FZlLWvZWAtW5+prokrqTjqtqYiqoxqyoJc24jf60A7WhBX8/IqwAkt1gr/4yzsWVQm/HvbX6PHy5b/GB+l//78Bf5wektnh0MYBk5AmXpnnk6A50pkufG1NOcWlIwqLsdqFmecrmosX84oPZZjZ0/yrl4XjHbcazvomF9OInIjR3IxyodGscA9OAi1WuxrXa8Vu2kKgBVWQXzCkysG1TN7U61+zOiyt9x8qBHNFeU1bPBbufkRvE/+/Lv8++tv8d/evEc/6dPf9U9N5caUzfeCsAqmOfu2eq8C90Su0phNlWycoTW1idTu+ea/XGBcpXKrMIkr58fP1OHfEEPJaDycymqflLRFTJPQgArZDCIdEnOFxaRUpAKQCmFUxiUIMwFOb+AA+Jl2Gq1uH//PoCXSwv4J8WtAIj9fp+7d+96NtrDhw+9WX2v1/MyN/l8eb9cp8hcBUQQUE0KQMDLgsXPTdgjEkAhMm0BmPI898VlCLaGrEhpT2FNCbhVluUVo3zxuIuiyHtRSVtJmwtAK4CxnGM+nzMcDj1YFUWRBydEbi3MmDCsxVrLYDAgiiLefPNNyrLk8PCQ2WzG+vo6zWaTZrPpfQYlNCFMLw4ZZuJfJ20gYEitVvOgs7DKBOwLPRAlbVf6QQCb+Xx+xW9R5OMicf3Sl76EtZbvf//7PHr0yLdXs9n04F6SJD4AJPR9u17Yhsyf/z52TQiMy9/C0hUPQQGlhDEY+gpK2EKn06HRaDAcDul2u3zwwQd86Utf4s033+S73/0ug8HAg3ACtIdeeAKyJUnC1tYWWZZ5NmAINsdxzJMnT/y8CIHC09NTHwokANNgMPBgzGKxYDAYoLXm/fffpygKXnrpJfI89wBgrVZjOBxSFAVvvPGGZ5C1Wi2GwyGPHz/2Gw+yubC/v38F7A2Bc5lj4l8J+Pkq80yYb4eHh9y6dYvf+I3f8Kw2AftCubKsAwA3btyg1Wrxu7/7uxwdHV3ZQAnX0JAhLH0dboSEYErIbpPrlDErSdKtVotOp8P6+rqfq5IuLmt0WZbcuXOHV155hdPTU7797W8zHA45OjrybOkoivx57t27x8bGBg8ePEBrzeXlJbdv3+bi4oL5fE6r1boCUgJXWGSh/DRNU9bW1rx1wieffOLHWwjKhjJZ8fQ8PT29wjKT9grBr3CuyXoU+kKGcy0M8ZJnjwCXMjaFvStSfxlP8uyR9TTcRJN2kLbe3d0lz3PPyK3X67zwwgt0Oh3u37/vN6lknRoMBpyfn3N6eopSyrNDwbFrd3Z26HQ63qd0f3//ChNR7kcYhLJWhmMnHHMyzqTdZezL74ldhDGGVqt1ZcNP1pher8fFxYVXCSjlfElffPFFPvzwQ15++WUfpiSS8NBPV9r/vw88lDaXZ5J8v5DxIj+X8ReC7n+W46ceQHSFm8JWhXNkHAimM5dCXDRc0ezYdFVRELnwgjKpPPRyHHPHWO8nGFUSXyRMJBJ/O+Xlqc6HzrrQjKbyDCIbVR51pXs9mQvjThFPHbASLR3gONvQVcCKY9WZin1WHxrHbEsU9XPHXIuWjrGkl4bLO6ljMI4MzafOkzC9tB5AdQCnY+zV92OKpktHloTl2shWAKD1ASC6tCxbisSpO8i6yrEwF87LsGi5+7C6QuGFcQkV49Mx/dJL60FEnbsiX9rZpO49UVaBHJWUzoisjqo4pGJManwYDrj3iywznrtrKOorRiMGz+ozkWPouJRuMInrPxTUTxVmlBDlFcM0Vvy1/veBiP15l0iBXbiLWkunvHeygyoV8dxJkSUwx8TOc9KBNoqs7ZheAmYL41JYmFkrIsot8cKSjq+yGPNW5EA444JXiqYDgLEO+JSgoGRiq2AN12CT2wqsoveRRhkDUeT6KIopG9plBzRwMrwKDDSlxhpVyZYd6zCJypUfGvJgUxRGub8LNziVtiRpgVJQir+XctfaPHWgpi4qNnCsPCBvheWlqjm4WE1jE7t+3/k9zf/18pf4+jff43je4SJr8Gr3kMJquvUFXxo84WZ6xszUSHXJr+x9wP6yx3oypRVlfPfsOQCejPt8ZfMxAEsTM8rqfGnnKU+nfS7HTdIY+u/FzL85RilYnNcpmhE2gtZTF5TivAc16dTQ/7QEFRFPoXlS0nmYsNhIiRaW5knJ4lmX6ddzyhcWWF3HKktSJTBb5fxCy1QxuW1QRlM/dXOurCmyXuR8U3GeqzqzRFkF2NdcsJPOXJCKLtya1ji1nL9qaUdL2h+5NCVlqkTyYFV3QIbyjGuTWsqNjF8cfMbYNKhHhQcM16IFC2u8JDnCsrAuCVkSmWuqpK4cixAZJapkaaGuS3ILFyblwrhd6veyNvcXu9yfbvFkMuBi1qAwmtmkxtvHuxgjO3caU2qi2LAYNrj5Dx1aN/qbM6Z7KY0T7di7nYiLFzWLnYK4l9Ftz1nmMdGfdh2rGDe3dWawSmEiTTwr0YX7v8oKbBI51i6AeK+Wbg7ONjR53/BL/Q/cuMSQKE0cG4rYMd4cEc69v7SGB/ONa8+kCmlUbt0R9p6NKwZd6Zjk7eaCs/P6KvyjOkoUtbhAaQNFgrWKWZGS1EvqcV4x8yzRQlWsU5eqXtM5WlnKUnM8bjPZrPPRco9OsiCKDYVE0yu8hPnDv/8SX/nCv0M+S51tRSZfvsEm7n5lvTcpFP2C59fOGWc1Ti7aHD5ap74f0zqw1C8tt2YGqwuW/Yis58BDXShsvUQsLCSkhEJhU4OXeRfKeQkmEk5SbXqEaczaohYRqmL1mV5B0sjRkaFRy5nOahijWS4TdgeXPHqyQbxUmJrFppayBFtEtAczajpnZjMOsl4FrlbM7yrdGQM2teigbzzYK3LsKpmZyKIjg6k6Up5lFlYejmGitFo9035+/Gwd11l+IQMuZM2ECZ/Xfd2k8AoLZ/GiCouYEHwR0FCKGJFMXmdjSQES/twYl7oq0q1QxitMjc8++wzA+7B1u13PgjBmFbogQQ7iLViWJdPp1CdMi3xYinSRk7XbbebzuS+2RBopTJ8QELzOLlsulz8WRCJtHwKxIYAhwFjYV2EfCLgjBa8AfCFgFQIX0s9SyAu4J30vbSV9E8rJlssll5eXXhartebWrVtesifXLiCggCGj0cjLpUXmLaCc/I4UxnKdYUCCFK4hECuScimyhYUVBqZ0u13f7tPp9AoQG3o49no9z6iVfpMgF6WUf28IssuYDgHDcOxKX4bzRt57nZUl8upQ+iphQCH7SABRYX4K8ydJEr7//e/zyiuvsLe3x/HxsWfGijwyBELFl3K5XPok8Xq97hONhX0kqeOdTofNzU3PgDTG0Ov1ODg4YLlcsru76+0CRBb74MEDHj9+TKvV8mNif3/fh2xIMvOrr77KbDbjk08+YTqdsrW1RavV8oC0yHFD4DgEFCQgJsuyK+zL0JNN2HsCvL3yyitMp1NOTk54+PDhFUavrI0CjMmmRL1eZ29vj8PDQ4bD4Y8BhvL+sL/DdUDaIGThiTejMAvb7TbtdpuNjQ3W19c9GyxNU87Oznjw4AEHBweekTkajXwabxRFtNttvvvd7xJFETs7OzQaDU5PTzk5OeHmzZtsbGxwcnLC4eEh1lpee+011tfXOT4+RimXgP748WMPNgnbUdYn8WINQUGlXBhKq9Xyfq9iK3F940nWPgGkkiTh4uLiylou/w4ZnGG7drtdvy4LQy68FpkPwnCWTSv5t/RVo9Hw66ash+JfGDLk19bWPBM4XM/Fi/ejjz7ik08+4cmTJyilfIr6xsYG1lpvi1EUBZubm15O3u/3ATg8POTg4ICTkxPvOSljTjb4BCyU9UjWHbk3GVsC2objMXyWyj3KmiObM7PZzG9klKVLUR8MBt7LN/QWFT/kcBMsnGfSv7IehnMhBDmvg8vXpeHyJwSv/4ewD+FnAUBU+BAUqArmaPW3SAPFX09XjJwyrbzCLK6wLG3FUnPgoqT+6twxHeKFA21MBNqsPrtoKM88lHMs+yvgIJRKA15mnU4dMzGqzNyVdSBMmYgnnvKMFathsaZd2MbCEsVOGmoihS5h84c5JnU+cyZWFUuwqsmakI6hcYIHJuKlY7OZyv/QS/dS5UJPGlSSx0o6XIGOpgr8sBErkFWtiiVdWKit5JhZ30k1dbECNcp0JZ/DOE828ULUldxcF5W8OYZovroPCqid41koRd2drzZybSfhNaVSLixCSR9BfWhRA3cf0g8OCHZsv+l2xF48B9qM8zoXJiYaxWAz5mXK5WUD2yyZ3EyJ56zk78YBs7oK1kknq/AToApRWXnRLXuKeVf7/tHVWFPGXUcyM9XvujESLYw/V9nQzAeRA5Y0xOMc00i49Y+mjl01zyFNoDSo0lD22z4pOuu68BRrndm/UpY4KSuw0Hl3RdpSVmnMWlvyMqKsGIc6qtg3ZiXFsxWhSUXW+zjGM8Oyr8naLtCmeVKgM0PWi7HKgeE6N6TjkuZBSbyscfGCpmjB5IZicN+gUKylM77Zv89p0WFhElJd8LWtB9R0zu9fvMjTSZ92uuQju82L7WN6kZPNAtxpnzHKG3w22SCrGJS9dMEoq/PNzY/51298j79z44t89veeR73XcWvIWkl8Y0YUGZbzDkVDkVYy40Vfk44t9aFltqNYbEQ0ji21M8fEvXjB/b/+OGVxK8NGls0fWNJLl8Y62XPhMcnE0v1E+zTkZOrCZ6KFSz6f3rTEY6gPXegM1bjPO7ZKQHeWCuBYjF/96kf8vQdvUD9zjGaxWyi6ygdBKeNk095+L4K7N0/5XP0ZiSoYmzp9PSdDU1rlQ1So2IZJlcQMoCsfw5lxYSljU2dcOm/Cz+abnOUtnk777F90mZ030Jdxxdh08zjKLKalWO5aVATjQpM23cIYxyVJ4jw1l/WSaK7RpWV83kTVDXlTg3XtsriV0V6fMZ+n5GXE/GmHuKYwqUFniuZxNdm0Yr6ZMHpes/6epvnMFa5lK3XM9LICFa2bx3kzZnoT2jcu+Ur9MVD5Z6FJ4pJFtWZZo1gY99rELjlZtNFzjWkEYBirdQptIXOpxyKRtQlsNGec2b5b/yTQKTEerJUjL6NK5l35PuWuLcr6SgYbRQZjNbfb51wu6ry+ccCvdt7m1QRerB3yvzn665SV7JfIPXdy7TZZ8ssaql6i6wW1Ws56e0Y7XTJaOibk0w+3UblisVuQnMYcvX2HdGTZmruk9TI1LNY0y76iTDVlo1oPYifltgpUoa+yCi0roBBcEEwuXiAVExGwDesZk04GrrH1Er29pFjGKCBJC5bLhCIp+dzeIW8NHvK33v+qa/paiU1iyqYLIjMJNBoZs4dd/tPpX+DvbLzJemOGrZnVNUb2SviNqZiSSlvnfajk39r7PVJWafZ29exHgS2VWx+tY5d61qK2/pnw8+Nn6wi/6Idf4AWwCJmIIdsOrjIIQtmmgDAhk0SKilD2JuCKMAxExivFUei/KIwTwAMpIWMklJdJoSWF6MXFhU/9lfsLmYoHBwe+iAI8ANLr9bysTgAT8Tp89uyZZyR1Oh0PvIWehpPJxEuFYSVvDK85ZMjI9SZJQqvV8hJiOacwYuQ+Q1aiFFqhbDaUf8mf6XTqfyZglYCPwqwBPKMvZDjJOfI8ZzweMx6PPSA4GAz47LPPPMAjYQwigVwsFj7gYzabkSQJjUbDs15kbMzn8ysMUmFyhm02Ho99yIYws5RSzOdzDyQqpXz7NZtN79dVFIWX6xZFwbNnz7wken9/34MLAjadnp6ys7Pj5d6hB9110DwEtaQ/rodNhEC6/Fs8NWu1Gp1OxwPVAmSKT6WAqSFA3+/32djYYDQacfPmTZ49e8aLL77IjRs3+PTTT/0ckkRa8eAUAF/a21rrGZwi3T4/P/dzod1us1wuefbsGYvFgj//5/889Xqd4XBIrVbj+PiYTz/91HsTnp+fk6apByen06mXt4o0dH9/3we3SMDQxcUFg8GA2WzG8fExl5eX3Lx5E3CbATdu3ODRo0d+nZCQHGstp6enHhiRuSEsYQHDBBD9yle+4sfz9773vStAfej9Jsw+CUza2dmh1Wrx9ttvezak9IesfyEoIuueyM97vR7dbtcH/PT7fbrdrgdAJbVX2LgC1AhoOBqNmM1mXoovmzQhmG6t9X6Z29vbnJ6e+vE6GAzodrscHR2htWYwGJCmqQf/Nzc3KYqC09NT6vU6b7zxhgcQ33//fX8uWfvkXtM0ZXt727OGP/744yupvOH4lzEsYCesLB2uMxZlDRNATM4nTDfxJ5Tnh4wTeb+kvwN+QyFkmAt4GEUu/GV3d5fPf/7zWGsZDoeMx2NarRZra2teti/ri7SjSI3l89I09WD21tYW7Xbbs/NErj+dTnnvvfc4Ozvz40GY06enp0ynU87Pz3/MdzhkvYfP7XAzSPxghZF7PXxHnnVpml4BNUWKvVgsPGNVbDhkI0Kup16v8+zZM27evMnjx4/92nidPR8e4WZlaP8QsiHlWRk+l8KNl/B7S3iOf5bjpx9AxBXJQq7Qiyq4sQIRsY79hXISy3jhAk7kfU5iZr3nnrDBdLFiKLqUXjygpuyK7aMqxqPzO3RstGTqAKbSzTHPPisaikhbzEKRN1UlN66Al0UVqKErkMs6sNMoB+zp3MljdVHJF3Xw2ZEDD8H9Tt50vx/PXboyFponZhWeW7HlsK5NXCiD8hJuSSzWldxYL901JlPxUVwBfDZaMTvz1gq41IVrH7T4Ma6YnKqomn3p+iVa4qXZRcUwjCoGpS6vshOF4Yly9w3VdSjl04+jpWWx7uRq8dySjpUDZLAezHRgsruWeGE5/rplTcfktqSXzhmblGSqUNbyC52HfDt/FYyTj3qZevD5ecsFMYSsEgEI4yqdWZWORZZeOtahS+92XoHOL1GRJS7VWmT10VITLyrwNnPJvaYKfYgvF5SdGtEsR2WFQ/OMQSgzppl4v03TLMEoVGIcU8YoBw4ajTHa9YcuyYrYA4Til2htTByXFEXkCt+A1m5tBV5oJ7G3EYxvac+InW06+V9e+fZFC6hdwmItxkQxunSg8HzbYmouwKL7MXznjXsMbs74aLLNZV7nhc4Jf3R+l7+69TbPt07oJ3Muixr3mkM24glHeZdPJxvcbF6QqJJXWwf8aHyLVBc8mqyx3bhkPZny4WSH3569xG7zkq/8+tt85x98njK10MvJjxrkyqlNnSeopXlSsOxFjG+70BcbW/Kum4PxzN3TYtswvW2onUZQaHhlwuFak9pxQuPEyezLGtRGUL8wZG3H7I0Xls6TAqsVy66m8xk0zhxTS+ZzMVUkY8ckViUkY7f2nL+i+bXeY37wj1+p1h+3IRIvLHmVwC7eh6H1QdE2/KXtD6lX6cl9PaemSpLK87CpChIsZRV2IhLkvzt5nXfGN3k8HfDsvMdilmInMfFlRDJ1bNJ46tasRgENKsZaw4Gg8y1L2TJQumtjd0G36VKFsyxmuUgpxwmqWThgWoZYJW81qaIs3Ly4dWvI7c453//2qyzjBv0nMN9ya1cyUdSHRcV0hfm6ZvZcRjxNaT516ec20ujcMWVttJqweScib1u+sfOEnQhyW5KoCINxMn4jBY6TMJfWMDIlp/OWD8ywugLNElsxb5UHo3w4R+Xzd75oeJDK+cXaFUPYKqLIkscGa2FZxDR1RmEEZFOrZxjQqGX8cutD/mbvQ8wNyz+a7/L3Lr/I/3m+xp8c3mQ5cw89VSoo3Tqa9JYM/vyQ5yoZcj3KmeQ1xlmNB6fr2Pc73PgnS9bvKEYvQetp7GwGmjC5qSia2m8Aga38hJVPDJdrM6ld+RdqtzC69lKrji4cKCo2C7ZdOm/BXLv2Cg0DFeQXdZLeks/tHfJ6b58vtx7wufSIvSjiv51tUR40eTSugbakMwUmqpj0kH/UxXYM7faC/Ycb5LfOqz6oQo0KBUp5aw7xvLSBx6I1qpI0u0AU3FvQyrIsYnSuXNCNXQWuOPDQYnMNle3DNaz458fPwBF+aReGSfiFXb7si2wUuCKLktdClocUA2FBKAVFCKJJISnAlxSH4sUkoQUhK07eI0CXSHKFKXi9gA/ZLIAHBMPia7lceqBEwDK5L5HYXl5eeiaUFLICPAnoJF6ISimf5hp6P8l9SrEXgqLAFSmmgDoCaAjgKUxQufeQxVEUhZcnCxgaMqrkXq/L2qTvBKyUQBVhYMq9SvKz9IP8+86dO9y9e5e//bf/th9HUoBKOIEU8e1224d4SCEsRaIAp3IIQ07aSwpKKXzlfkSCKH0vbdvv9z2oIsDnbDajLEvW19c9wPvuu+/635GxIoyesizpdDqMRqMrnmoh2C7FrMyNkJEYAorSBzLvpN/CMS5y3+Fw6Mdqq9XyKaYCMImU8PT01EvEz87OWF9f5+HDhzz33HO0222fbCuMWrlWAcRkvACerSVtL30p9yXXO51O+e3f/m2+9rWvsbOz46WqwvASKfCtW7eYzWbMZjPPGBPgeTqdekBUvCsfP35Ms9mk1+uRpimtVgut9RUg/Z133vFAiYCtEoYkjFj5NzggQjw4ZX7XajXeeust3n//fTY3N3nw4AGADzsCvIeeXLcwr5577jkuLi44Ojq6Mu9k3ISAWrPZ9ADZK6+8wgsvvOABJbme0WjE+fk5+/v7PHnyxDODJ5MJo9EIYwybm5u0220P7lxcXHg2aafToV6vMxgMsNby5MmTK8xwwDOloyhiMBhQr9fZ3d1luVwyHA75+OOP/Rq4vr7OcDj04LRIfNfX1+n3+569Kfcr60CSJB4ANcbwySef+Hksa0s4d2TtEGBaxmMIvEq7h+16fXNJgOIw1CkE02SNEyl4KCsPmcTNZpNXX32Ver1Ov9/n8PCQe/fu0e/3efToEfv7+1c2DEQCfnBw4Mf+YDDwATvb29veT1Yp5QOGDg4OePbsmfeC7ff7TCYTDg4OriR6C8tWwE15TdokDCaR9VueGcJOlkPaKmy/7e1t3njjDR92Ixszjx8/5vXXX2c0GvH06dMrPqqy4TAcDv3zQGt9xbtR2jx8voYbfNcVCeH/5QilzCHwHL7vfwgT8acfQJRi2ziZoGPyVd53yoFENnaAV5Q5ZoRVEM+cZ1hZd7JiqxyzUOTM4CTGeUt59pwEhQgLSIJFoPKzS6tiv+HOsewraudOshxPLbWRcSm8CRVDA4quAxcax+49JnLgWxmDKRyzLV64YlQX1XUWDhhUxhU8Rc8VMPHC3X86cUCjreS/pvJktNpJrfO2S4ou606mHM9XycfK4Ao9AdlyB+4pC1mtCi8xq7avVewnJ+2tAkTqK4afSRwAmY5cu9iA1akzV9BJSIoY7pvY2V6p0qUQk1ZS80rSCaAi9/5osWKZClvThb0IgFqBqrur1EsPHJuVx9zzr+zTUCn38wVb9Qlj0yC5hMVGnf/g+/887fdTpndLpnuO4RllV1ORXaCO9QE8rh1WY6NoOCDHFdl4mbIAi8nMEi8dI8pd5KqNbQTLjnbnqCvipZM+U5SVT5eCuEqULVe7CmUtAuWAal+YV3JkXRW3ZaldkWs1i0qmHEXGMRDzGK2NL961tpSFRilLWUTESYlSuHNo15c6twzulywG2s+lKLPk1o2/ounawXv+lY6Jly3cPJhvKdY+KBl/e5P/zzffZK97yRv9fTaSCXc3TqmrjIVJqOmc3bqbiH86vsV51iDWhou8wRudZwDs1kZ8Mt3kTvuMbrwgtxFH8w7bjTGzIuG0aPHyL3/Kj967Q1ovqPfmlEYze9JBWTj+eknrcULtzDLbNZh+QfOTlJ3vFZy9krBYtww+gPX3DctexMWLUDuKaf2gTSNVLPuu3+M51M5d+IlJHKsxbytGL2qSsaZ27hiJysKyq4nn7vWirqq57t5vYidnbh4bdn9xn986eJ3mQQCoK8dYjfJVoE/RkLXJBVl0bl7ypeZDShR1lVMTZB5oavflaGQSHhbrPMw2+M7583yt/4DfGb7E+9+/S/9DxeDCeTiWNcWiD3kbsk4VLtMz2GaJTku0dtLaQWdGM8lJdMlnRxvklynqvMbscYN0pGheujmKgotv5Q5wQzvql9UVsOPWkrwFN9sX1HThGNyZW+PyrgHt2JvxogRrKZsx3ScF6SQhmZboRYFpVIm6RbV2C4CnVBU0VPLN/odopSix1NDoCuVxFhDu36OyhcEyNZrRvNop0hVAV81bKmm/n8/Vbdnq4TSaNKpk4eotGpSGhUnoJEuMUbCMyLOY0bKShCULB6yVbu5gHAN49OE6//PkX2e8qDG5bGBnsQPCcMBl3M0oI4utkrkXW4bb6yOsVXx6ssHyqEntNKJx5DZINmeGaJGT9WPGdxV5r6BsaKY3QTwmTKMqDkuFylW18WRXUt3K69AmxgWoFCsgUC0i1z6yNgE2dSxAYuNBunp/weK87trPOJBP5Zp4fc5//OX/F3+uPkKjqamYSLX4NJ/wnzz+Fja26HEVbGMUNjbOf1hZ56dbKKYPetAt+Gu3/5T/7NNfdqCvBLLYFQCal5FnnvrAFMUV8BAlLFAnPwdWcmVJg65+pqLg/z8/fuaOEEgS+W/4pf76F/ZQgilFRQgySLEmX/wFQJKiKHy/FEkCMEnAg7WWWq3mwwtms5lnY8l7YcW0EKBOzh8CoqF/UuiZFxaRYbEmII2cRwouwBdmwmQRwDBsJ5GFCSNGPl/8/qT4ltcEQBLJsRTHUhBL4S/FMazYP6G0K45jLxMNgVEpQKVPQ0BO2kWKe+BKAQur5NVms+ml3QLmyHUNBgPa7TYfffSRl18OBgMeP37MBx98wMbGhpcSTyaTK30jrK/ZbMZ0Ov2xUBVhDwqLTgIShMEofRIWo3mec35+7iWeMmaEQTOfz0nT1LOwrve/XJ+0hwTzyGeEjLPribkhmCwgjpxLCl55TQ5hCYVhLyKrD4EDmRvCPJPxPplM6PV6nJ+f02q1ePz4MS+88AK/9mu/xm//9m979uf5+blvo8Vi4cHxkEUpbST3I7LbEDzJ85z5fM53vvMdXnjhBbTW7O7u8uTJkyt+n61Wizt37nB4eMh0OuXs7Iznn3+ek5MT2u02Wmsf3NFsNr3HqIAr4/HYM0rlOiSEQtpJ2lo8DkMmpYx1kXzLmvbaa69hrfOWfOedd3y7GmPY2tryAPjl5eUVZnKn02FtbY3vfe97LBaLK4yvRqNBt9ul1WrR7/c9E09CJo6Ojjg+PubZs2debixzVsapAC9pmrKxsUG9Xufi4uLHACaA5557zvsTnp+fc3Bw4JnA0l+yRoVBQ/V6nTzPPXNPpP0SXtRsNnn06JEH5T799FPu3r3LbDa74gEqzHBhLjcaDZrNJoPBgPF4zPn5uQesZA7JWiTjXIJDRO4eAnrhJpaAYuEcEsabgO/C/JPfl/N3Oh22t7c9mN7pdK5YXwiI2e12ef75533fiFRcGJkC4otcV9jPzz33HGtra36D5/z8nOPjY7773e/6BHrZCBGJ+WQyYTabXWEWyiHPixAkDFl9wq6UcTaZTDg7O7vybA3tGyQwSJ4vEqYkPo0//OEPubi48CFfX//61ynLkmfPnnkZttbas+xlYwzc5sPrr7/OxcUFcRxzeHh4RdIfskhlU+W6rDoEDuU5K8+kcHNNDhlP4fv+WY+ffgCRCmSqCjSRf4o/nU8lriu0tT7h1gF4Cr10IKLVlhL3WjJxoI6yq6AVF4DhQA9JoCxrjh0YLd3nlSnElfdh1lnJCAXMWww0tZElnRiW3dh5A8ZQP7U+iTdvuOJFV55pEqSgK29Gq8CgiBcGnVmKpqZMtANHlfP804Vl2dHebxDjvL3EqD5vOZDNgWvu/2UDH9aAhqJhneF8QHgpG45dUxtbdObAIHASYpM4CZfV7rpd0EPVJ9YSTx3Qq7KKAVi4PhDPNqUgvaw+S9gr1rWxKi3puLqGmvJy37xVtdXM9TlUPotYl7wcK88odSCl8sxKZd01CJD1lbVHRErzg+UtJkXK745fRhm4vBOz+5uKvGlZbjgwQ1iMeUtR1HUFTlf3WsnBde6Yh8nU+HFjo4o5WjrwImu7NihTV3wvbeRAxsL1fzKxPrE6nRgqfIdFX1O7cNI8AFVeM+S3FpvEFM0IZSzLfvWFLylRkUVXISpay7/BGE0kAQBS92onW67Vcs/Ccb+wYkrJ/0WeuOxH6ALa+4VvC5Mq9LGlaGgXlFNC89Sw6OnKN9RSP4X5tqJoWM5fimg/tSx+Z0D7r53w5zsf8SRf5yDre1bhvEzoxkte7eyzEU/4dLFJO1ry5dYDpqbGH1y+yOG8w2Z9wihvsFO7ZJi1ebV3yNLEvNU/4MlijQ8ud6BuaP93LUYvWpKJhrWSfGDQ7ZzpXZhvaWpDzbypKd6ccGLbtA4ss13L8TcLuu+m1M+cv+Zis6R5oOk9yCkamvHNiMapk2wr4/rSRi6BO55r5z+awuQONA4Vpgd66diJzjdSkVdZHkXT+X6Ob2n+pe0P+b/8g1+mW7GJo8y1//EvJCx2SvrvOuaqsMFMbCnrlm/e/KSSLjcolaZEs58P+OHsDs/mfU4XLc7mTaaLFK0t0ycd3jl7Gf3GiNbzI/KnA2ojJ1U9fzFi+kJOZ3PCrc4YpSyxNvTTOd1kwV7tgh9c3OJ265x5mVCYiGf1HvmoxsYfaw9uZl0wFeO2XEZEtZLZZsxiQxFdGkzdVpsrDoDtJAva0ZKyZWg9i1j23f1FS0XzqETlTvZfNDW185zG4Rw1zyEvsJ0qZS4vMa3E9cWydPLlPcVgb8QvNx8SEVNTCRrFxOZobTA1Bx5RKB4u1oHP2C87zKY117cVQGYrkNF791XgFxFuZ6RiIGazBPEzZLWEkeiCveaIs36T5uaQf3Xvj/mXWw+ZWsNvTV/it8uXq5AP/KZA95Hi2G5TbOTotKSzM2atNaOXLpgXCc9GPWYVJd60SpLTmPPfvEHtwrI+M1VAlZMiT3cdu7BoWoqmxSYFpAZTpRi79lMO6DMVABrYeog8WdnVRh4WqBkfpNK4OebO2jkHl10uDjs+vdpGFrWM+NoX7tOKM/7k6CaLqObOWwGilJBPUkoUbV2ntJXUyRr+9we/ymefbkPdoBexe/anlu5zF1wcdojPY8pWSdTNMcOUWzeHbq3LlGMiV89LG7u1y0ZurfRy5swBnyJhFtaoxTEVtbIURnvCpNLWSZer/rWVp6MNvBB/fvxsHVL8wVUGTcicE1AhBEbkZ2FhIH9L4SDFTlhwgivsxAdvsVj4wjhkSkihI58hRZOct9VqeVZY6NsW+i/Jea5fgwBpcRzT6XQ86BFKq3q9HrPZjIuLC8qy9AmyAvaIPLMoCra3tz1wJd6A4sentfaBJJJ4HLIVxYPLWstsNvMFtfSNXJPcXyjPDoFLkQ2LvC4EXOVcwsC6Lv0KPSyl6JSCX9pUCnwZB7ACTmq1Gufn50wmE1566SWMMdy+fZsf/vCHfPvb3/ZAX7vdvgKMCoAh463f73swcLlcen8+AStFVi2sMGk3kXtaaz0gIww0uVdhwsnYEWAoHMNhmyRJ4vtHAivkCAHAkFkYSsflEOZjCJyHRbQAmMIalTEvY0b6WySWAiSLhFsAcQHgz87OaLfb/MZv/Aa/9Eu/xFe+8hW+853vXAkCEkmyMHe11h6QDRmVAnIJeCteiiGz9unTp3zlK1/h+PiY119/ndlsxmeffebTY2u1GvP53KcsD4dD9vb2+OSTT7y3n8hDBQAHePfdd/08abVaHswTIFdA6sVi4a9XpMYyFgR4ns/nfr5vbm7yyiuv8N3vfpc33njDy+4lcGh3d9fPg8lkcoURt7Ozw3K59Gy0er3O66+/zp07d1DKhdVMJhPm87kPIanX62xvb9NqtTg7O0NrFybTbrd55ZVX/DogY3OxWHhAdX19nfl8zuPHj70HoXgiPnjwgPPzc89ANMYwGo3o9/uUZemT4Iui8Onzxhgf0CPMWjnH4eEhGxsb3pdU0nb39/fZ39/3ATbhRka4zu7u7noG4ocffujbrlar0W63OT8/92NG5pJ47E2n0yug9XUGmmzaAJ6JK8ByURT++SXzRNr71q1bfq4VRcGTJ0/odrveWkHWnU6nw8OHDzk6OuLi4sKDgRsbG/66XnjhBd577z1/vo2NDQaDAVmW8fbbb3uGYb/fp1arMRgMyPOcBw8e8OzZMy4vL39sjRCQO3y2hoxOaeswKEWYlAJMttttZrOZ9zCNIheQ8+Uvf5nxeMxnn33m2zuUMQvI/8EHH/jnzZ07d7h37x7f/va3r4DnW1tb3L17F6UUP/zhD6nVan5jSZ5Xod+x+ObKMyW0LrkOGoYbEzL3w2uU+RduYsr6Wl7HEv4px08/gCi7/0uRsjqAQoAXSVwWwE8X1jPDRKpMlVocLx2w49J7beVLparzOYad1Y4lVdZcijBazuuYVLqolFo5RFMHLBY151kYLSFvKqLMAQSLDUXroGKTVf2vK2DNgYaVjDquZHAo/zoK4rnBJIq8DXamHNNCu9fFQy2eO3+1ZacCuQzkPZcG7dJ+IW9bipZj+7jQGSfTtJK+vHRtkF46dmHRVKiaA26zviI6qQZr5to2yip2Z62qj+cVQ9AKgCIsvEoKXbESwYFrKAcamkT5vhWpdm1kfZ9eAXStYzUqUwW01K0H7Mq6A6bqQyexjRYrKXa8sMw3NN/o3GdiFvy3Z6/xez96BSLL1tCBvYtBJa+zEC0UtUtDMnVsUhtB3tBe1m4iRd5yzNC85UAzVbo2k7Rwl66sPFPUMT2rL02p629wbZU3q/TeyicxqTwW04sMW4tQhVkV77LrYi22lnpPzOWaY9bYUhOnuQN6rQMIJbFVaxOwEV3qbgn+9bJ0zEQlbCFTsQ2MK5rLusHGLlV4dkcBmv6nhjJZgenN05J0oihqimXHsRbjueuDvKV80E/edYyz+pnlnd95kb/1TcWvbL7HvEzoJXNquuBO/YyX6wfs5wOO8i676YhElQyLNgubcKc+pB/PKNG80DzmNO9wmrXoJXPPRqzpgtd6B7z05jH/YP8r9D5WjF6w6PXMyWkPayRZZWNgofdezPj5JuW6oXmoaD/SLKYp4xdK5mPtkpIbhtFLirybEM1hetOQdTX1M+3ChYDpnqJ+aqmfGXRpGd2LaRxDe790LNvUsWWjzFQydzf2JjciF6DzrQveHe/ReYC3MxAWbrSAdKjdGItYsew02LWcr3Y+o6UychUTKcO/9Ud/k+Ko6azxdmYkSUkcGWpJQaQtk8Qy+NAwjLuoz11C6iTxyiimdwueu3fEdmPMoozZaYxJdUFhIrrxnP9J//v8hfaH/AePf5XTWYs31g8cEK0s093V3E4moMZu/c5eKDG5pmhC1oPeR4rRi6uwKxtBN17QjDJs4kD2yxfcsE/Gitq5k/KbupuzqnDrgNJApDFVeIqN9ArwV4rlICLrG76+9YyW0lyYglSVDHSd/3j4NS4uWuj1JWZYAwsfXO4w2854lm9jFhGRkOskMKVe+SFW8tV4psm7pQeoUKDGsWO9xauHv7XwzdaH/C/6H7DcKfhH812+N36e/+bk89wfbjK5aGKXumKkO5b7xvqY9K9e8HzrkkE6Z1zUeDLuc3jR5eFwm7UfRmw+zHn2S4qibVDziGTsxs10T1HWIvKuceMlLp0M2yoocWCohLxIulXpgDU0kMluA565Z1KDapboxGCG6RU2tbcMiQy5ibgcN6q1y/0VdzOKScLrnX060YLvPL1LGKDiHiAKNY84zPvAIZHSLG3O2GScLNpgFVErw6QlzffrLDYs/9rzf8zfKr6GfdwDFWHmGtsuebK/xn8+/IbfKFOlQheKcpVvgKkAQaUdUKhj48axxQGoFUCqtUHjkp8laMVW8mtfR2uLlTb7ORHxZ/oIWWciAxUwSpg7wuiR8An54i/FunyhF2aQfOmXAt0Y4wEnAUmEQRJ67IXvC72fBCwQsETYZxIiEIKUUniETJawIBEgUYBBARjk5yKNk7ReSfWVtgF8UVuWJePx2EvhwvuXzxeQ6Lo0S5glIctRgL6Q5aSU8pLWUM4rfSNsHPE4lHOFLBdhpcj/RX4t7SH9L0EkRVH40BLxe5NQFClmW60Wr732Gu+88w6vvfYaL7zwAg8fPmQ0GvHkyRPvn9btdj24K20jEnUpQOU1AS8bjYYHUQU0uLy89CmvwpoV9pQU5CGALX0qTEYJixFJr7RFKK0UMG0+n9Pr9TDGeJbNdUl4eIRF7nWA8TqrKhyj0vci9xdpqkgG5T3iQwZ4tq60qUhzh8MhJycnrK+v88477/CNb3zDs4TkfDKOQ3A6DHCRuSisvlBqHY5F8a0bDod88Ytf5A//8A8xxrC+vu5ZuOPx+Iq348HBAUVRUKu5zdEoijg+PmY6nTIej7l586ZnZ4GzMajVauzs7FxhAy6XS46Pj3+iFYDIqKXtJZxkPp97ht7nPvc5PvzwQ87Ozvxn5XnuQyxkzZLzJUlyJZgmjmNu3rzJl7/8ZZ4+fcrh4SEnJyf+XgTsTpKEDz74gBdffJFnz555lnej0WBjY4OPPvqIyWRCHMdcXFz4JHhph16vx82bN5lOp5yennr2tABgJycnXj7c6XQ8IP7kyRM6nY4HUGVsheBLv9/n/v37VwCz4XDIfD73YL+0dWhjEa5lsn61220/Jh8/fuz7dnd3lzt37vA7v/M7HtidzWYsFgtu3LhBs9nkyZMnHjwTRrmwCmVtkvuWz5Z7EMBR2Nu1Wo27d+9SFAVra2ucnZ0BK+/Kd999lydPnvj15M6dO5ydnfkQrI2NDR+EtbOz42Xtw+HQrwUy/j/99FP/vBB29ePHjz1DVzZdrnsQXme2y5xqt9t+40PAQPGSDYE0YTbKvYW2CLIORFHk18Xws2Qui3+tzHHZhJCNrsFg4Fnkzz//PEVRcPfuXT799FMviZ9MJrzzzju0Wi0ODg78NYQelNelyfI74XMw3LiUQ8bD9fXyJ533n/X46QcQWQGFwk7ThSWrO6mkEfZOnYrFVnn9hf5DVYEtnodWQ7x0/oASZFKmimRmoKZQBaRzx14zlReeiYXxaL0nozGOARctqQABS9FQZC3HWktHTkIMzi/ORk6GXVRy47K2knqi3bl0sUqKLhqaxcAFn0QLS9ZTK6DOy66dRDtaOlZc3nb3bxLHJiyaq98taxUQuFTUj10xGC1X0mKMkwhinachVpFcOrDMpxKLF2PFiKICMkzlFyg+iCKvVqVjvNkIrICKERAr0kvrWXdFA8/0FADVJM7jEFV5OC6BCGwdrHLyuOlGxSIR0HFZSUg7Lq1aF44B9TDb5N//6F9k+NE6jXNNMnZskbNXIpIJXL5cUtuekT1oM9/Q5E3n+SgMVV2xY3RhqZ9DMisrebMbH1lHu3uxlrypybouVAGDl907DzsHDEeZrf6vqI9UlX5dsRVjRTTPQWt0Vvr328gxDlVeYAZt53lXU5QN10Y6LbEWTBmhKsBQa0sSlyyzmCgyRNFqcXFgovJSvcJqlKqkzH7uWKzRqIWumDvQPHRhI2WVfi1AcdbWWK1YDJRPzNX5irWYr+XUjmJUDqMXoXcfGicOoHwpPeQg6zPMW7zWeEpLZ7T0kk8XmxirOMtb7NUuWIsnLMuE0mo+33xCJ5pTWs2D+Sa9ZE5mYnIbcZx1mZY1OvGCpsr4t/6Vf8h/8vv/HKpR0O/MaKznHJ91UZ81KDsl0YtjRidtyBUazfnnoP0Edr+TMdlLuHjFja2t33WTabbrpL3tRw7Mm9xxGxLxXFG0HFCK0g7EnzngPes4+bIkJ+dNJ1dXpVsnTOw2Bv7avR/xt77751gvIa+S32Wdqp9aWgdO9pw3XdKzSd36uLtzzo34nESV9PWMRJVEkUumjfZm9NoLsiIiy2MWlzV0WiIhVVTjoExWNgKDGyP+ua2P+M+/+xeoHSbsf+WYlwbH3GsOuVc7oaMj1vWcwmiWecxmOqaW5EwVNI4tyzU3Dky3Wo8smFwTpYZkaoknarXRUCXb29glTyeqJJrqarPEgrLUTy3R0qDzkmW7Rjwt0ZmTM6vCYJpuEVCFxabaAYsVG1ieBf/y+g+YekabxWD43SOHUJoscsCZUXx8sMV3b/U5KnqQuevAgs4dAF8mClLjAcW8XzoArFSVP4NbP5XBA4I2gtonDf41/W+TpgXz8wbk1ToB2FpJ2skoogh7nlLWIXptxKA+Z/+yy7ODAemzlMaRonFq2JqYaoPDcvZKSt6rzGe1ZXa3dHLh0kl8TWp8yAuJwRYKZVYbJxjHHBRZNArsQrv3aIuNDdEgp9lcYoxmPk1duEmUuvuOrQMYc43qZvzavR9xlHX55NG2Y+Qlrl2LkQMccxvxZuMR9fSrLOIUk0er9SY1qELxh5fP8ze6TxibjKNSM7Upr/UOeC+54dqrAqLLTsk/PPocs5MWScu6RGYFah5hY0s5i1EC4qoVm1UV1feCQrtnEjjfQw0KSykBKaWCylN2nNecX6Z8rxCQUTsQ0ebaAbI2eP3nx8/UEXpGCYgQ+gsJQ1BYh8JEkmJS5LeAD6aQPwKYhGBivV73bIVQLi1SVikapFgUcDEMCIiiyDPThIEjwRwh+whc0SHMqlCybIzxoIaABfL+kPUmYKJcV7PZ9ADecrn0QIkUTXKd0rYhs09YiQKGhtcLK5BJin0pNEPg5jorA7gCBEr7CMAngKMUbyIPDYGkEDwLpXOTyYTNzU3fvwLOhbLnUKr7+c9/nqdPn/Luu+96UEZCIr71rW9xcXHBkydPODs7Yzweez8/AUSkmA1BRfE8lBAKYXcK8Hx2dubZjTJ+w0JdJIkCYEsfypgPQV0B7wRAvby8ZG9vz88BYZmGwHko/ZZzhH+uF+7yHmmfZrPJ3bt3qdfrPHz40I9bYQUBfh4I61VSUMPgCPGuFEBOmJOPHj3irbfeQmvNn/zJn/i+krktia8hAyycm8JaFDBDfDflvrMs40//9E9ZX19nb2+Pzz77zAO+zWaT8XjM2tqaByQbjQbD4ZBbt26xtrbmr3U6nfrNBQnpmc/n7O3teTagBIoIe1LaQAB+Ge/SRwKah+3++c9/3gOJb7/9tg/ZEID86dOnfj0RZp9YKojsXYDcZrOJtZaDgwMODw/J85wbN27Q6/VIksR7hsrYCRlnwqS9uLjg9ddf936hwkJutVqeldnv9337CTtYKeUZnBK4IeuL+JJeXl5eAcxDq4J6vU6v16PX6zGZTHygkYBi0l7iLStjYz6f+/Es67UEcWxvb3sgVeaIpN2LxFpkvqenp9y8eRNjjJfFXreoCNnjwmQVr8TQRiDc6BG2oEi/Hz9+zOHhIbu7u55BK2vB6ekph4eH3Lx50yeIR1HEaDTi/v37/P7v/z5RFLG5uUmj0fCMd2Gly2aPsL3lWXbdokDaQjwY5T7FJkPS1MXKQVQBAgiKTYZsHty6dYtut8v29jaPHj3ym1uyZoRjUwB4YbCLZYiAj1tbW5ydnbG2tka73eb4+Jhms+nHQL1eZzQa0W63efLkCePx2MveG40Gz549u7IZJn/LJli4hsk8DH13f9JzOVQjhEDhdbb3T5I4//87fvoBRMGpqsI27yjnyVTifQYdKFNJAGcOQHLgnwM5yobzKstboYzWyYrLdMWWKxNF0Vix5ZCgEePSjUWWi3WggLAipVgoGpVsOKu8C1urYBYTV36EM8c8lGANW/neCRCz7GoPSqLce3QBi42KpRNcQ1QBpWXNAYtlWu0UV+EPcr2qgPqlY1AVLcfeE9AhmSjPFNNVuEs8X7H7bOzII/UL4+6vcGCWiZS/RlWAto7Bmbfdz1WBD2upXVjfvsqs7sGzjrRjd+ZNRdZQLHvQOrCoopJJV16NwhTLulD0S4pRzPJGRtwsiD5uVu0Fk9vQfrSSlzeOLf/RP/wrxBNFe1i1Y+aCArLXZugfNPlzv/ABf3HwPv/hH/91WodlNSa0/12rXf8K3Wa2tZKV61LAUgvasU+jzMmTReZb1KtU6RqUqTuvAKQuQMWB0MncEuWW6PQS22qAUpXnZ9UnSYxaZhTtBKsrAKnh9Pi2CqRw4Sh2xS40jl1ojHj/lJjKH1FVyc2iRSwKTb2R0aznjMZNeNag/yE0zg0nX3Cg1eCTnMaZGwsmVuhce7/DdFxioqhiWlbepMvKA88osrtL0gd1F8rSclL4R//Fi/xPv/ocm7fO+TfvfYf35jdp6oy6zmnqjJou+GLzIbmNOSx6PF6u0Y6WvDu/yUYy5uFig7VkSqQMu8kFnWjBSdHhvckNDhZd1tIZb9U+4y9/6R1+/+98kWmzxmXDEs0ciE5iWCwS+ltjRqMm8ZMa2XrJ7BcXZL026QWUjZLJqwVQI7m0LAeWomvQDyJ6D0uipWa6p4jm0H1gSaZmtUGwdD6JbizgmF+VPLWouzWjaCgaJ5aLvzplf9mj937sfT9lIwOo0sBduxaVf6tVYBPL6ahNiSLCEqmCRBle3jrmR5e34LDJ5bhFPFOkc6jnMPnGDBNVoSCxY6GKrNqkcKMzJtEFqnAMrS9tPmUzHXM7HXIrGVJXMXXlCqUkLunFc5fvY2G2o7zHanK5sgDIb7rgnuZxQbyIGN+MvA2BMNma0ZK6KmgcahbrgDZEc03r0LjfNY5hnV6WmEQTzXKwFlOLsYlGLcUPwrV51ouZbWvYnfOF2jERUFfOVy8m4m53yJOHG6hMOyajhfKsxr//8V8lUpZo7oI+bGQpmwbbqHZBM+3Zc83NKYtZillGkGtUrlYy36zaXFAWvQT9qMFiNyNpZzQaGYPmnH7Nfcl89+kejfcaLPuWsm2IvtNjdNhhMDFsLC0mLlmsRUx3Nec9TVmz2Ni6NSAAq1ShsbEBXckdNFgdsCYF1JNnrLAA5UfNknpnyfKwiV5q1l8ecj5qMT5vkjZzdjZH7D9aJ1ooyhgHhBYaCkWzveRXuz/iTxd3+Ae8/mPPciJLL57xRjIjCTY0POBW/f29g9uM9jJqSvNfDL/B907uulMoYL9ObBTZWonKFJ98soOeuYToaKEcQ11BfJx4lm+RltjYOpDUus03GzlwWpnqoyOLNfgEZn9oSxQZYmXcRovBM0t9CrOErUTWj6OfHz97R+gpNJvNPAghsjJYAWHiVSiMOQHGBPASGa2AkeJ3F0qLJTgAuMJaFNZRCDqGoIwASAIOdDodL3WV5FcBj6RID2WMwtba2Nig1Wr5MICLiwtfXEsYR3gNIeAjDK3z83P/MynKRbImBbew6KTAlHsTMFLAN/kcCbOQQAgpwKR/AF8gCyMlDKYJQcuQ7SIFtxTNUkCK/E+AUnCF+/r6Ovfu3fNFo7R5u91mOByyvb3N+vo6p6ennJ2dMRqN+P73vw/A0dER+/v7nJ2dsbOzwxe+8AXPiNna2vLyvmfPntFutz1QCPi2EcBQvBSl76T/BPzMsswzbAA/9kJwVkC0EGRbLBYeGLoe1CB/C/unLEva7Tanp6d+vIdg7/X3hWxdGTvSD9KHAoaura1x8+ZN7ty5A8Dx8bH3kjw9PfX3IwEN0g/CFBKwKQTH7ty548M9Tk9PuXHjBu+///6V9HHxFLXWevCo3W57KS1wJahHPns0GnnwKmRQCUj3J3/yJ3zrW9/i1q1bjMdjBoOBZzNJmwmLq16ve9+7ra0tTk5OuHXrFmma+uTr3d1djDEMBgM/XmazGf1+n06nw3A4vALKSmK4SJgF7JX1RdpMWISffPKJ98AMmWxZlvl1MJT912o1ptOpt1EwxnB2duZlr3fv3vW/L4DzxsaG33gR1pasX7IR0e/3/bjZ2Njg/fff9/NTwDpYsSiF5TeZTDzoGkr2ZYPg7t27vPfee8Rx7BOZZQ1O05S1tTXPNj0/P/c+hwJEyn3Lmh+CdeGYrtVq3Lhxg36/z9raGt///vf9nJb7LYrCM+bER+/DDz/0oJGE7si4E7BQ+jd8xgjLW9jpIWDc7XaZTqf80R/9EU+fPvUbK9vb234t/vrXv85kMuFP/uRPWC6XbG5ukqYpT58+5Y//+I85OTmh2+3SaDRot9ssFgseP37M2dkZp6enfq5PJhPfFqFvYQiKyXNV2l2eFfIcEhZ+mBC/vb3NxsYGaZpycHDgfUtD+4979+7x7Nkzbty4wcHBgW8X8fuUYDFJm7++6SSy9vl8zs7ODlprvvjFLzKbzTg8PATg/Pyco6Mj5vM55+fn3rN0NBr5Z5+Mv9AvVsaJjDdhj8qzR45Qzn2dZRiCrNdlynIff1bwEH4WAETlZJ/im+eDQKqCU4AX8WIq6k7mJ0Eotiqwo4UlHTkgT9hqUBXnsSLrKC8bK+s4xkjpgDWrK8ZdxRwq6w50dD+vAkqWML2hiKfQ2recfd5SO1f0PnFyTasdM9AnR+cBK21ufYrzbIuqOK6Alwq8AgdkLbYsyUh51k5RX7WBCw+owNbUkkwcOJGOHGhmKgmkLhzgGE9ZFYyqAmAVjiEZu2AY8QMEKo835VmSyuKDDspaxWZsOM/FeFYBb5EDSZKpRZc2SJdWK1/J5YqJmF5al2pd9a/OXX8JS3I5sCTPj0kKTTFssbk74uRZn9p8JXEvGwaTOmahzl3oTP3IecZFc8t820mO61884z967b/mf/3Zv0k/mbMeT1isWXQWVSnTzgtSlxaVVyE2xl0/OKAlbzr2oIkdkJu3qnGUu/dJSrYykFbAtpeuV2xOYW+axIGs6dRg6zXy9RbR3CUwmyRGLwowBhtHFM1qN7O7KlpRlRxPBrdVOP9DxzQsC5fInCQFumIdWhTGKiRMpVbPyZYJy8+69D6GzpOCZFKQ9RNUqfzcUCXM1+IKwLc0zgrKVJO1NenUUrs0XqottgLJKOLGC0c8utghPY+Y7rk5INLfvfYlT7M1tpNLFiZhVDSYlDW+0HrEVjTm7eUtHi42vJy5F82IlOFG7ZwnizXu1ofcSoc8zDZ5thywXbvkaNllEM94kq3zcvOIf3g3p/dOwvQWZLs5RJbGZzV0nlIU0Ju7tSJ5TzPdbTPftMRTRf8DzXy75thwxoHkeWyYvGzQZYIqYLlVstCW2kXkAFMg6zgfyPqZ8cBI3tSOeThbeWAWDU1Zh3/lpbf5jXe/SK/ySJW1ziSOhSzpwyZWlR2B84Cr3xqz3p5RVzklihJFgiHWLp2795GTypcpLFoVYGId62yyF2GVZX7SpFFtypgENuoT1qKpC6iINHfqQ9rRgkQV9PUcUCQKuukCrSxfbn7Gb8ZvQGSpjSov1giW625xjudgC41JnDWDKle/I9YHNoJElYzLOunIMr4HKlek54p05BaJop268JmsdOtEYRzQHmlUXrFKlPLM82UvYnLX8Bdf/JCmUiRKE6GIqgfrZjpBNwtMFKGWri1QcHreQWnjglOsOxe5QvVLTBatAlIsLBepfyapislnawaW4nmqsA3Dzj93wGbDgQQXywYHl10ePdng2TChcai4+WGOKjOefTMhHjtZ/LKvmdzSlCmYmvO6NIm5EqIiz0oUjgGXOZDQezQGwJzKKzZxslr8VVJCUo3RUeJk0GsFWaGwsaUoNX/uuU/51bW3eSk55rDs8u8++B8730gFEihia4ZXN494PbF8e9xxu08C1gVSbmM1TZ2w1ZpwetGG3J1DQmp0rphN62jAWMs/ePA5losEVTELXciY2xy0dcOduyc8OR6gn9Yds9CALhWmbtHLldev+3A3Z+Q7gvsCgfc8BFBRIGPGXZu1iiQqoWTF3Lx2OO9EBZFj3P7cA/Fn7xBJWFh0S+En7Agp9EKgSYpbYTuIjFAYSiGTTiSkwkwMpZIhWCNMH2FcCHNpsViwvb3tPacuLy8ZDAY0m01OT0+9Kf71ok0ARGGUSZF2fn5+BdwQ9kaz2fSy2tFo5FlY4sMlUkOR/CVJ4qXOAgCGxXXI7rwOLMkhvxeGwQgrUoANuTfxgxPGGKwKKgG3QkacFOQCIEm7p2nqQVxh5NXrdaIootvtelCx3+97ME5AO+kPKRyLouCTTz5hsVhweXlJr9fjK1/5CvV6nddeew3gipefXG8IHobMTJHJX1xceID6urRZ7kdAZ3mvSKyFUSbnkDEmbSgF9ssvv8xwOOTtt9/2/ShjRjy+pFAOi1pp71AmHxboYaDPdWZnv9/n5Zdf5tVXX2W5XHL//n2m0yn9fv+K55lI0aX/5Txa6yueZgIIzGYz1tfXefLkCdvb25ycnDAcDmm329y/f5+//Jf/MvV6nQ8++ADAbxAIm3ixWHgQTs4dzlVpM/GPC1lREnJ0//59Xn/9dX7v934PcID33t6eX08E/Ox2u8xmM8bjMffv3ydNU+7du0ev1+Pw8JDhcMjGxgbNZtODF8JkFCny0dGR30iQ8SR9GzKfBMCbzWb8yq/8igceBfSW8RkCiIAHSouiYG9vj1u3bvl1Qn5XQO/NzU2GwyGHh4dcXl7S7XbZ2NjwUtYsy3j//fc9YCRjRD4H8OujyIFlrRR/T9lA6Xa7nJ+f+4AVAWsFkJQxLmzaOI45OTm54ldoreXu3bvcv3+f2WxGlmV0u90ricICIsq6FXp1huBPq9Via2uLXq/HxcUFDx48uMK6lftvtVoeoJ7P5x6MC0F24MqmTciqDtm84b2IHYZcx2QyYTgcsrW15dnpvV6PRqPBeDxmOBx6Vu1isfCp1cJO1FpzenrKaDTy61bIjgsl08AV/1S5dmG9yvMlZG23Wi12dnZIkoSnT5/6z+52u34OnJycsLGxgTEuzTrcvOj1eh7wl9Ch6yxyaaPZbOZZ2yFb3Fp7RbIt4+8P/uAPPANYrl3YkeDY5KPRyK+7AsrKuJVDQGD52U8C+0I2Yeh5GErlZZyFGzUhIHvdEuKfdvz0A4gWzwqMFhZdhYiYxLG6dEYVYqI8m83ElSR5bqtAjsqDToGyLvm4qCnS6n1W4xlhzWPjC9oow6fp5i0HFOUdRTxz5z1/3RUn7ScOcFvcyGk8TMjbClsrwWjyNjRPnIl9ma48DHXuiuesoysWIb5A1LnyCcfCPDKVrFdkV+KzFS0dSJr1LfHcMRyVgeymY52kFw40yLuOqRItlGNeVSC1eArGM+vScxeubYUdqCowLGs70COZWu9NSMW+y1t4sE9nlrQCmox2QJNjUlX+jgrSsaVoOMmnY/VBUlYFXQW2WfGoUi7MZLHurj+eKvJPO5RNQ/tMMfxo3Vl4Fe53kynUPopQhZts8dTJNNORJuvBYh3+R7/6u/ztP/46X9w44s10SDxT3K0PeS4+c6yfZeUxV/VLXjEGXcKyrZgr+JCaKHOsQYDahQN3ZIyWifIJ3kUBJtHeE1HAZFU6QDmZObA1vSiw9YRP/mZM990Ge79zjlqWjomoFGjHFIvnhrytIbZEsTw4FFFSUBYRUVwSx8axYyq5so5KskrObK0DQETWXGQRPGix9j60n2Xo3JB3Yk7fbDC55cArVbgABg/4aOeBWDRiN1dbqkrA1c5/VK3A88aRIisj2rcvyYcDVAnLgeuzvf9O8+70HtmXIv7K9jvkNmJS1mhEOfv5gAfLLY6zDiWahUn4cvszFjbhSbbO0iTcqp+xFk/4eLnDrKyRqJKliflC5zGb8SWpKvl/Hr/FVz73GX86fAmTWpqDOZ3GkmGtTZlr7DwmHSzQ2rI8aBJPLIrKq++hof9xwWIjYbapSMeW3scR56+4sZmOFN2PIuLFKjDJWkteMYwXfe3B+LLhWKXxwlZghgP2T7/mvmzXP2j4MKh4vgor8v6akWOCCuBWNiz/+gs/5M3mY+qqoEQxMzXQkJkYlGW+KZsJjrFd1hSLVwpMlpK3oT5U5MsYk1bBOBG044x+NIPYUnQsX21+SonizfSSpnILUwLs1C8xNc2t+NIBLIVb96Kl24RQVXpV0VAuvVlVCeVLQ9HSGAm0qBS1a9GEv/PsC9Xa4daxxqlF58YxDRNFMqmKQWNRZYmpp84CInOsRKsVujCYWDPbVqjNBX9l8KMrjxZdpU59qf2I/yZ9nbiZMTtp+deLaUK9vyCPrJNgi/9hBbw61rhjscVJgSk1poyxNYONHYCnSvfcWGyWJO2MJ6d9Hhzt0TjUNE4s3aGhZywmNmQdzdmrCVnPrtKgn3dMRqshmquKSejWOpu4NVFSiH0qsMX9vwLGPLPQAImhseMAzNlRyz1LtMXOYqJexus3DvhXd/6IzfiSf+d7/4b7nJqhXcv4X27/Yz6fRuRW8f++eNm1h4CTkhzdKPjm2n2aOmUpu3fCbpSjVNR0jkbzYvuYj5NNyovU3Vsl/zU1sJcJ357d5Cv1J8RxyWLYRi8UsXFjXi8VeqlYe+6CJCqp1XMWjRplZIkmTnaul5WMPLbehzFaKP9cBZynobYONCw1tgKLJZVZaevX0LyMyETvLKxDKhZiiQvaMYoy1ySFWqV0//z4mTkEOBSWhwApUgDIl3QBEIqi8Kmd8oVeiojQo0qKiKJYpRpL0RYmU0pBJMXI5aWT1WxtbfHKK694g38BPKRgEJ+xfr/P5eWlBw8Bn+gqRQ7ggSkxh5cCW94j1yzXFbJIRF7dbDb99cshhZZIzwTsEtmvhBVcB2EllEBAVvFVC+VdUlhJwSuFn4BZ8jsCMAhYKcBbCGCFQFYIdjUaDQ/ChOwt+Z1ms8nx8bFP6Tw+PvZprGHgSZIk3Lhxg6997Wu+GMyyjDt37vDw4UOMMYzHY18sh1J3kRoL00ZAaQFBpYgXXzvxwJQxKPcsXmWhT5zImgHvU6m1ZnNzk263S6/X8+m4wuKU+XD79m0vG5a2k76RQ4r76wWttIEAnOvr67z22mvcuXOHxWLBu+++65l2W1tbXFxccHp6+mNAooBbwjATxpIwcAEPTF1eXrKxscF8PufevXv8/9j78xjLsjy/D/ucc7e3x4s1IzNyrazKqqzK7uplunq6p2chhzNcPJJpEZIAWbJlUCRkELINC7AAWxD8hwCDgGWDFGQZkCxRHnIIkLLEMSmSQ1GcnunhdM9SXdWVtWRVVu4ZkbHH29+72zn+49zfeTdyRubwz270TSRie8u9Z7vv9z3f5fHjx94P7bvf/S5vv/02X/rSl7wccTqd+nkv6bzGGA/2CGgkYLn0Q92vU3wklVLcv3+foii4fPky+/v7NJtNDg4OfIhOv9+n0+kADowYDAYMBgOfJvv48WO/2SAgRavV4sGDBxhjePPNN32ASLvd9hsOMmcFpKzPbQEqLl26xK1bt/jN3/xNz3qUuSAANDhAQwAXCQ25du0aYRjy0UcfcXZ2dq7vpf9lA0L8FwXQlDEpzOI6GC7AT57nrK+vs7+/fw5AFp9JYXpJ+I2wBNfW1nw40HQ65eLFi/4ahGEr0n5Zn2Xzp9/vs7e35xO8wzDk+PjYr5XyfuKtKsEZMj/EWmJ9fd0zgR8+fMjh4eG5TY3xeEyaprTbbQ4PD/2aLGufbGbIGBLwTeaQXIcApBI0U98skfnfbDb58pe/TLfb9Unsjx494vd///dptVpsbm56Fp2kYUtadJ2dXfd5lOuoH/Vx1W63PUNe1hQB7x48eODb986dO96HURKdm80mH3zwAXEcc/HiRcbjMb/5m7/J5uYmly9f5ujo6NwaHkURly9f9hYbdTsC6RdpQ2stg8GAra0tHxol66uw2J88ecK1a9cYDAbnbA0mk4lneHe7XS5dusRwOKTdbvPKK68QxzEPHjzw/qZ1q404jllZWfES6rokWdqyfg+qr5UyH+ptLn1Q9ycWsFbGwR/1+OEHEMEVFBE+MMBmVQruXPkd/iBzbDETO7AtnhjCmSG/EFI0AFxKbjR1gQ5UrMVkaCgarriXMIN4ZMl6inRVVT5jFl2oKtV3afYfXpzx8698xj/87ttc+J4i2Yu8vxmBZXE9w0QR8ci9Xxk7MMFU3nFlg6UnIEumYdG21flCMHfXpAonPY7GyhVX1XUr7f6enLnzsyGkK5aybUBp0jWq9GXnRxZOqoXGVIy9eBligpHHOo81KmYdlew46zqmpqpCaeJJ5fuWwWJdEY8N4UJRNNz5qxLPVDSxA3ujKn3WMSErgDZ04K2qwOJkZFmsKhqnrq/KxD03HDgZdtmAzGqiiaXzVJOuLpmd4NozFAZgYSkSzfiG5dZPPAHgf7f2e/xy+JMkQcGTwpn8v5rs09UGG7pxFE8t4aIC5bQDVMRTzQWoVH6aFXPUxM5HK5zbSjqt/PvHVdiOPFck6+CA7KKlyGIXwqIKiEYZVmsu/EZAkFYeb3kB1oIxqHlK6wfPWLy5Q9bX6Kh0Nbw23k9NpMlFoQlD41mIqmIlKqUdiaZUZKMGrachm5+VtPZm2EiTrkaMLyeMrxvKnmPqBWchNnUejdHUoEtDGbs5ogsHaBatgLzjAORovGyjcA7R2DKaN9hZGXL/tQbxZ00PEEdTy/Z3Lcd3r/FXX7tGtlXwx7/wCYUN2J1fZyOZEKmS1XDCq8kBuQ05Kno8WGzRqSSvm+EIjcFYzVnR4o3mCwJlOMj7PM9W2W6MmJcRP/Ez9/jwv7mN3V3h6KILMknGmnSjpJHkBNrQupEyGLaxZzE6Upy+HtA4deDv+KZBZ4rVjxWrn1qGr2rSdcvaXUvrqMAEisV6ABaapyWNgfIsXpSirFjPzgcTJpfcwvOLX/6Q//be23ROqw2RzFbhKVUSfAWul0kF0AcOcIl3pny59ZhAGaY2pqdSjmyMtobCaJeKe+R8QbMVWGyAspY8DbGhS0Avmu61BCQDCFVJbgO3DvQKroUjxjYkUprUFh5EBNBqCWADxENbAf/Ke7BiwKQBcaNwIODUUDZrqcY4ZtjzbJ2Tf3wJVt05BXNFe9/NA8ekU6jMVPOiMhZuhA6cL41jIlY3ybQfkq5Zbmwf81Z8SICmUUmXDRaD5ZuNJ2TTmCzTkJSQuiReckXxqOPYZEUF3im4sDlk/8Uq1ljPAuy1F6R5SD6q0duV+3vZLolPAlZ/t0k0s1hlKZqGrKs4fSMg7zpmdtm0oB0QKKxFcAxMaRsBD5HwjkpGrQrt+8+3pgUVG5rdBVu9CTvtIZkJOJp3KI1mdtICbdFJyf/rm/8VP9cUb0jDu1lJOY7QBnSrYKs15lKQMTGavVLx3eMbLmwlMf69MNBZmfOn2p8AHRYmQsUlNnOsTpUrbGy89De1Ob1wQRiWjjUpCdYC+OWK3xrd4iuN564dyqXNhioguDEhPWuw1pzxf3/lb/MX7v3rRG+O2WxOuPuPXydbMYQjvQwqqu5x4dQxiMsGEFhMoQnMst1U5YNZ+7zmjygomReRYwZX3oiUyvk/4vrGNkp0UmLi0K/HPz5+dI46WCTBBgJICNNCPqRLgS5SQwEz6n5EdZaLJF7K6whQUQeBpKAVfytjDL1ej7W1NQ8kfPOb3+RXf/VXSdOUyWRCWZasra2R57kHIkTOWS/qxTPtZdAH8CwzAeRE/gVLCaewDQVEkecJOFZn5kihLVJTeT8pcgWclaJK2kR8rYTtIeCnnH+dpSesnDrjRQ4p3qQ96ywkYRkK0Af4FGwpeMHJ4w4PD72nljHG+9NNp1Mvc5XiUoDht99+mzt37nB4eOjB1I2NDc7Ozrhx44ZnxU2nUw9WDAYD38ZhGPqAAik0hXklPwtLUtKJpRCWvm632zSbTe8PKICxjAEZG1K8P3nyxDMsBXySfpD+u3HjBuPx2DPs6uBZnVVTZ6TVQfRut8vm5iZvvfUWm5ubDIdD7t69y8nJCTs7O7z99tuMRiPu3r3L2dmZ98QU0FLkmtLvAphJG0sbCaNNwLhHjx7xyiuvMBqNPEi5v79PGIZ885vf5Bvf+AZZlvH48WMfDnN8fMzDhw/9fBDppAA90i7SRjIeZX5JW+/t7XH9+nU2Nze9HFQCeQaDAQ8fPmQ4HHqJqIDH/X6f69ev+7AXAZzrY//s7IyPPvqIsixZXV31Y71uAVAHtmWcTiYTvvWtb/HRRx/xpS99iX/0j/6RBwcBD4a93MbGGDY3N9nc3OT999/n2bNn59ZMWReFAToejz1LUNbUKIp8inSdcVdn2k2nU9bX1730uZ5KLONJ1hOZv6PRyHsXCpgi3o7SR0q5wA3xopT14vr16+zt7fH06VPG4zGvv/468/mcyWTi2boCgNaBnfo1yyaT9EOj0eDu3bv+OTIvR6MRaZpy/fp1dnd3PTBtrfXjQNYWYwxra2t+fMu8FQa5MF9ls6FuRXD9+nUAL00XuXuz2fSbTPfv32c4HDIej/1cFcsJadv6HJe+ln6rS5SDIGBlZcVLoOUecv/+fYwxHridTCbcuXOHO3fu8PDhQx4+fMjx8THvvPOOX/OEmbu7u+vl82ma8uLFi3P3rVarxc7ODgcHB/4+KHNF2rwOSg+HQy5duuQZpXWWdZZlPHv2jNdee83fh/M8ZzAYALCzs8P29jbT6ZRr1655H9npdMrVq1cZjUbnvHrFqzEMQ9bW1gB8iMzLnw3kqH+2qI+v+gacKAfqAVadTodWq0Wn0znHfPxnHT/8AKKqpLYiDysq5hZAaT3zLcgcQKMLJ6kMZwadG3RuCSoT/aLtCuUywUuQlbVEc0tZOPlo1lGUiSLr41g/JehJJSW0wkp0QFE2c2mwb3zhGS/uXXdMm6pYSV6Elf+f8140oStA83YFQhZgygrECx3TJh4pJ8lSYCJLOFMe1ASIRsqnC5cVU9FEwri06NIVX6pQdB6FThrZdqxDYTTWWUy6cNJg1yaqYkgpbOwAubyND8hg4qTYOnfhESgoOu78WvuSNAzhwpCuBE6uXbEk9YRl6rKl8jyrdgJblZSxA40Tx3AM54YoDlz7DR2LSPwYrYJoDOlaJetMq8AKi/d7FKBF51A2Al78iZJ//1t/l682nvA3B1+npSNUaHitdch/O/wqiy3DVjAmAFSzYLoTUh5Xqdeha1NhlQljszEw7j0yi7IujbloKB84U8ZQBAoTK9Iao1IXtmIyOtA7mluiWSWJ1hV7MS0wzYi1d09AKWyonfdhXkAUwjyFZoP5ZoSNDTYPCIICa0S64cCFMCwpS01ZsWpcKrMmjAqHRZ4k9O4HrDwqiM9mzl9yJWZ4M1oCh9oxwJR2QK8NnPyaVuX3MjHE48oGoKNJBsaN49i1WZm47yUF/XTYhJUhr1065OGzq0QjB2rFY0X/szktoH9fUTYCPvzOF3wIUboC8ysF8eqClc6cL6y7BKt2mJLbgMvxKQsTY9AMyyY3G4dciU7YK1Z5mq4xKhpsJyPeaL5AK8vTX1jl6PcugILmvvZJq6MXXR+OkeyHdB+7fhu+aplfNjSfB7R2nXz47A1L64V2IH8E6YoGFTp594YDtlsvXLJ40VQsNhTNQ2dZILLutOeY1aObcCkZEn7Udqw66xi5ZVytQ5Wfpw3c+mU12AiKluXnr30OQFcvaKuMqY1YmJiuXjDLY+c92atA/zFL1nVoMApaByXDmwEmhKBaJ2zo0pAjVTpmVuYG8MxE5HZBUCHgCwuHaZdQGSIsSllU6gJMwjkwYxmsVLGo/QaYsZikAr2r9d0GsJf26TwzHH9JQWiJR5poUjj5dqwr1qGTLVNabBRgQ135hGpsqLyEOV3RFC3L19ae0NWKlo4881CjMFguBDHfeP0B9062mM4TsnlFQQ8tZdMsQVUDtmn44voe+7vVh85K9ns66FDOqgWKCujEgYfBzIFYswuavAtFwzEMbWggAJVWmzIF+BTi6j5iYuu9bG0oAGp1a/QSZpzs+iVlQrI+59r6GWeLJoNZk+NJm63uhLwMOJ20fNiHNYrNYEpuY4Zmwa9ObrKwEXoeONC01LTCjIbSRJW53zSrgNLqOlEWG1kudsdcCNzHjtSE2EJ7tiYaVKaxGAIMTRXzJ7of8v8N79SCR9yFq9Rd2+fjDRabAXkeuj8F7l5nmoY/99pd/tb33sFaxcfZNi+OHcvp8WKTOHTAc9E2S6akAaud/2/3mWWy4zb6bKG9ZUadhahD4xmGGEUUFRRGU1rlwF2jIDFO+h04FnizldJOMlaSBZ8fXDnPvPzx8SNxCCOkDnjVP9hLYQKcS32dzWY+DEBkvyKtFFBIQDFhs4j8MYoiH5IASy9EKR7ER2xlZYXnz58zHo9RSnF2dubBGWHPCTtRzlHAPpHeAr6IEeCyzoAQEEuYP8C5oAJh39QLnJOTEx/EIUw3KbjrzEABJeoefvI6dW9C8S0Tfz4BkiQ8QIBJ8fQSZlLdI1HaT4JJhAEq7bKxscH6+jqz2YwXL174Ql8AGPFoEzBKWEsiFZZrrV9bURT89E//NH/uz/057t27x5MnTzg8POSrX/0qg8GAvb09NjY22N3d9W0xHA49YDibzXyhKOOq7hcmclQBaOq+giLNrktJBViUPhSml1yDjKksyzg4OPDsqLqcW4AAAX6ePHlyDhSRMVNncwrII+/dbDa5desWN27coNVqcXp6ym//9m8TRRGrq6vcuXOH4XDIu+++y/HxsfeAW1tbI01T7zcm7S1SYgF2JD1VPDsBD4bs7OyQpilHR0dcvXqVwWDAcDjk5s2bPHv2jF/5lV/xYN1bb73lH3/p0iW+9rWvefZqlmWcnp7y9OlTptMpi8WCw8NDJpMJo9HIs4nrmwkC1r3//vv8/M//PPfv32d/f5/r16+zsrLCYrHwibftdtsnegv49/y521yT6x+Px97rTljLwrIVH0Vh7tWZtwLoC3i+urrqGZnD4ZBHjx7596zLPoui8HNd2lwYX8+fPz8HANa/CtgrmyBy/kEQeGB7a2uL3d1dP0frLD4Zx/IV8PNNa+29QyXYRY6X12BhhNY3hOS/AGVaay5evMj3v/9976m6urrKkydPyLLMbyLJNQlYLKC6jHlhNwq7NM9zDg4O/LnL/SNNU3Z3d7l586ZPaBdpc1mWTKfTc9YBAnZ3Oh3PyBRmdF2eXWdeg2OkHx0d+bG7WCx49OiR3yCoh7/IevPy+llPZK+D9i8zi+X/ysoKZVn6ZPGbN2/yjW98g9lsxqeffupZlcLivnfvHp1Oh1u3bhGGIaenp+ek/bLBJDYSAhLK+4nX78nJib8P1oFNaXO5rvF4TFmW9Ho9P77l/iwAvbXWs71l/RfgXPxlhYUvX6fTKScnJ+fAy7rMXNYBeW79a51tWJcsC4hZD1uR865vFolHaaPRYHV19ZxdwT/r+OEHEMF7zonnoc4r4E08CBPIOq5gjafOZy7InAdbY+ACMRZrAemaIh5CY24rSTGk3YD5lgOxorFlsakqttiSEagq+SAIeOeAnsajhP/n/I8RjgJaCTROqaSGluahY62hIVtxBYx4AgqjUueQrdiK0edAsHigfBiMMpD3qu8rsE/Oy2oHHuhC0Txy0lfxY8z6To4czpzsLZoKaOrAHDREE8BU3oXZEigs2u494pH1MlUTKtK+a5fOrkG/cBLmInFhCzp37CZ7CPFZSdzVLNbddThGowMo+5+7DxNWkhZYysnyLsy+OsccNbj86xBNTQXIuPc3ET4ROZpZJsqBh1nXSdJRyz5aMklL9r8e8f6f/o9Y0U3eT2FaJGg07c6Ca8kxf2Pv66itBWvBwllbFZrGKaw8yTGRIq+AwYpgRdpzknMp5FUVVxzOnAw5SK2X9QV55RVpHHiat7QDayv5d7ZSk4obF/4Tjw2UFhNqgqKEOEIVBqs1BBqKEqKQcq3DYlU5sEtZtLLe9N9acP6HGmsVplTowKC1IcsUxdM2/U8UvScZwSJ1jMONmLNXQ+YXDVYZTKu64MKlilqA0ElPy4YDB7OupoxdCIYJFXnXga3NU0M8tcw2XdvpAlRWtcVpRHlFkxYh0a0R5YcrRFPoPM8Iphk21JVE29C/V6Xq6QoQ0goThxStPvfa65QRpKuarAf/3crXKDYc4PnKlSOaYc719in9aIZWlmaQ88XmM9o65V56kdf6R+ytbBJONOU7I5SyXGgtCJRlMGsyn8Vka5qzpqb3APqfwuANTbpu2Pw+xGPD+HLA7KIlHihWPnPJ3JIS394H0My3HYAYzpcerOlKxSiLFFnPBQbdeech/82jt2ntW8qGS1aPpo4dHSyqdHZTMVabqko+t3Ax5ad69wmqAXpStskJ6Oo5M5OQG9d/qnTrgrCfg5nzxKynaJvYVp6rjjHZ0M5P0c6r5GkbMDINGionUgEzk3M1bPGfX/v7AJQENMMcmzjPxLzjWNPiCxiP8ICKMATdfHWs32hsuXDriG8/e5VGW1G2DBhovbCo3KBKQ5FEBPNqt9OKfDmpwCnjx4qylqIdMLugsJ2cdzoPAVjYgoiASAVoFBpFokL+0sX/gXtrl/jLP/hFVKdAncaYxFAPFlEWotUFhQncvBDwDEs5irycWSUGu6hYjLi5nnctRdO9iEncmq+MS3TWhUvxlcPE1gFyVZqzqjYx/LpTTU3TqXxzFhqlKzCxYj3a0JJOYx6V6/zsK5/zP1n7AV9J9tnQMX9rcpm/8ukfZxEZbBpgs4C/NfwJPhpd5F/f/h5/+Qe/SCiWCJV8963OC3q6QUHJ46LL6bDtQD/xWCwVqlXwWu+IpnLg4rRIqsAWUEY578BqHekGCwKlWdMz1lpzBkkbM42g9r5oOJ232C+6jnxaMQh1prCR4u89fAtVKp6d9fkPBv8CttBEz2KCTJGuVa8TVBtohe8qstWS0ze1D1TDVrilBVNoyB3oaVSwBI+VpREVLIqIvAxYbJZsXBnQSVKyMiAvA0rj1tzRrMF43iBYLJnmPz5+dA4pWAXgqvsO1QEvKXwBX9RLwSXeTlKU170O60CcFNniLwX8Ae8mea/j42M+++wzrl69yt7eHsfHx57RAHDv3j3/+LrHGjjGkgAUR0dHzGYzLy8UyW1dYirnKEWQPFbYWALc1IFHKVJF8gl4MCHP83PMMQGeBACRxFM5f/Fj7HQ6HiCU5OiiKOh2uz5UZG9vj263e47NKMV8u9328jMB/yQ9Vq73xo0baK158uSJDxURYFL6OwxD2u02Jycnnt0kzxfgRF73z/7ZP8sHH3zAX/trf42vfvWrnvn36NEjJpMJDx8+JEkSjo6OWF9f93I/Ya9IW0uROJvNPMgo4FQ9nbluyi9MImEeCutMxpSw5wSUFNmzBAGcnZ15ILfuNyk+cBL4U/+b9L+0lxydToder8fNmze5efMmWmuePXvGe++9RxRFXLlyhW63y7Nnz9jb22OxWHhQSuaUfK1L2QWIkgJfwBxJIxYmrFIuOOXOnTu0Wi0eP37MV7/6Vd58803AMU6FZXZ2dsbjx4/5zne+Q6vV8inWa2tr/r+wt77yla94KbGw5fb29vw8e/78OR999BGTycSPv9FoxLvvvsvq6irPnz/3slYZRxJk1O/3uXDhAnmes7e3RxAE3rfu+PjYA1svBzvJtXe7Xf+9tJvIYQWUSZKEmzdvMhwOuXr1Kr/xG7/BYrHwMleRcAtAJtLLPM/Z3Nxke3ubp0+fMhwOz4HyApTIuiaMU0mprVsjKKVYXV3l4ODgnJRY1tp2u+1DhYTBLN/v7+97BqG0uWxadLtdH6YiydT1NVSAsCRJzoFEo9GIDz74wIOrQRBwcnLiz7c+1gSUFfapAOwAm5ubnjV9cnLiWZZyyFrx7Nkzb2cgctkwDP01ydwSwEoCa+SYz+fs7u6eY7DVfXPLsuTx48fn2N51Zr2cbx3wr4Pz0g+y1q+srLCxsUEYhoxGI/b398/NM1kjZ7MZly5d4itf+Yofn6enp94aYTKZoLVmd3eXO3fu8JWvfIXZbEYcxzx8+NADygL+imz/woULPulZ+lI2GIwxnr0tIHSdDS2PFZnyeDz2Scx1/125V8n6JnJgubbhcOg9hkejkX+/Fy9eeFZ8PURJxlqe5z5xW0DBOhAt5/oy+C5qA1mXxY/16OjoHPApa7l8Bnk5ZOX/3/HDDyBW4J14gqnSMZkKXJFYRoqo8iS0GrK2oow1RUuTDAqCWUneDl2IxdCx6IrmEtxqnDjmgyqdN5m8jq0AKQGlJIk371IVaA6Ea3wYumI+xktti0a1O9N2ryPefs4PDrK+k6vlPSdt05Xpezhzr28SSx44ForKIetJwelSpO1cfPYce7BoLBluZZPqWqufKwYTdhku4+TgkK1Wkrip8j5rQeokzOASgfO283szkWuz2QWXhhqPDLavUdZJwzEwuq6xKiQZG3ThwiPKxIEdrq0LV9i3AoqGxoSK7l7h0q/PNPZxk+lFxWxTeRm1BIwUbVBW0Tg1FaDo2sRUKdrCICHAp2oHmWGxk7OinUx5ah3ybjBc7I759uANjqYd1lamrGnHpsIo0lWYTUIPAoYLSzRzN7dwoSkS5QEdqPzotAN3ykbldRksgUFwoJJI4OPMVL5wS/m6gIrRxDgwxFiIIweGFCVo7RhXTn9M2Y4oWsKscYVroMsqCdRVxHkeoLUhaeRMBk0ajxI2Hhq6T1J0XpJ3IsbXGkwvaeYXDGU3JzoN2fp9w+7PA1EFEFhhGDlZfNFQNAvHoLTaAbjKWswcsr5isaoI5y60o6g2/4JM0TiB5ESRFqEDPBVkfcP290qaD46d9FSuUytsEEDo2GzKgI00QV4SjjP8nmLVJmUrxoZu7s/WLjFOFIdcJ+0r5tuWfCPnV/WXiFoZW/0J/cacq28csPf9i5iPu5gYjq9GbKyOaSUZgTYkqxMWecjkckw+iwgaJdbAwTdi+h8FlLGzN5hedr6ZyciNhTLR6Bw6uyV2340NXUDr0Em+hUGdtxx4P76m+HL/GZ/85isE4XKDIMisZy+K5UDRctJLEzqfuDuX9+gHMwIsxmpOyw4LG7EdDl2QSk0Sm5wKGFKtZRXrS1nHeDbdEjuTxCRLS2d8Ndnlf//Tv8ZryT5vxU3einOoWv/zsqCnoaPdzxOzINYuFTfILHqoYFABW8JADKqbcdXH4iMo6dKHn27SeaSZ7rjzDKaa9r5jH8pGkipcGrNaFI7FGAdunmjlbS5Uacm6AXnPcmnnlJ9I9tEoApQHDw2W1ObslSVjs8Zx0aXIQpJmzqKjUYugYgu79cYqyEcJ/+T9N5f3J1VJb5XFZhqVKQf2g5MWzwPP3K4u3LHEy+XTbcXulfudHyCypllc4Il24GTSzlhpz5llEWkakc9a2MrH1Ae+WAiTkv/5m7/H/3HjbnW9TSIVsBk65EyHhrICy/76B+9gjeKzk02KoyZ5YB3+GUAQG95uPiVQmsKW/JPhm5Sj2PWlctdJodCR4Rf7dwmUZmYyjhad5TWVeEAUbQkqFHQ7gAvNMQ/sZvW45RgFsFbxa8MvYEpNPHR+maoEmyny+z0CBfNhg976lPBFTNGxmLz2XkDZqgJnAseOxTpw2k6WxawyOADTVteTGMJGQRiVKGXpNFNKoziZtrBW0bo8YbqIGU4aKIX3lI2igjgsWWvPeBr3PPj84+NH5xCAol6Y1UGNOputXqjUw1XqMmaRZEkxIUf9eyka6u8lhYGAjWdnZ7z77rt88skn3vdK2InyeCkAhclTlzEKmFgvaOvsuTrjUQApkcvVgxLm87kHA4QVUWfjSTEs7SCvU5alZ6iIr6PWmk6ncw5QlfOT1xAJrjxHWHMi62q1WhwdHZ1LIBYWyWKxYDab0e12PXgrX1dXV3n11Ve9JDdNUy9BFwBVmCPCoBLQFDgHaAmId/HiRTY3N/nlX/5lL18GxwZ6/vy59/i6efMmBwcHbG5u+vcWcFRkdXIeAlbVx4qAqtKv0k/ClpJCVkBdYbQIGCSgk4Apo9HIs6bqoICMwUaj4cE58V182Z+r/pyrV6/y+uuvc+nSJRaLBZ9++imDwYBGo8Ht27cB2N/f5/DwkG63y87ODuPx+Jwktj7GhB0lbCoZjxKgI2O7ziKSoBFhKE6nU1qtFlevXmU4HPLBBx+cS46V+SO/Oz095fHjx34syhxuNBoe7Ot2u6ysrLC9ve1ZdG+88QZ37twhz3MPhBwfH3vQaH19/Vx/CtM1DEP29/fZ39/31zWdTjk7O+POnTu89tprvHjxwksVt7e3efz4sQfZR6ORZxm/7FsnAT8CsLzzzjseWP/000/PMZhbrRaDwYAsy84FygBsb28TBAFPnz49t47IOKyzCIE/wPaTPgK8V2B9U8Jay8HBgfe7e/DggZ8fsjaUZekDX+7cuXNu3YzjmPX1dW/tUGdc19lzwmwUifRHH33E2toaDx8+5NatWwyHQyaTife4fZllKZsgsrbVmZESivPJJ5+cC9ZptVo+mXl1dfVcaIzYH8gGgLSRAMlaax+SU9+4qINpsnlVl9YLcFyX/cqclQ2KuoS3Hm4lzFMJBDs7O/NrYZ2NLeegtWZtbY033niDe/fu8fDhQ0ajEe12m5/5mZ/xmx4ADx488OuSbMwcHR15hvna2poH9zc2Nuh0Oty7d+/cdURRxObmJkVRcHx8zOXLlz3zWtZAObf6dZ6cnPDmm296kE/WXTk3AUcl+Vnud7JpFwQB6+vr/r3qidkyT2Qdltc8Pj5mOp3yyiuv+LlSBxpl/ZZ7jqwLYitweHjoWeDCkpX7gKy5Mo7qn0n+WccPP4BYFbuqrKRagSsOw7QC5zIHgiUj45NPvZS1tJhAoYylSByg4WSnzpPNRFU9V7FyRIKs8+VXKXyLJqAd4BOU8vrunJyEGC+blWARAbVslWpaspQah2NFNAw9mCfpzyaqvOPCigk4BDtyPoBlEx/wUjYcliRhLzZwqcHx0HnqBXMnH9W5YwiaSBEucDKuBO9NVybWM+x8AE3i2Jx5uwL/KrltPHTXXUZgO9oBZQoaY0PvsSLtu4FZJMqFgswrgKmopHKhggLiQU4jK1F5iY0CyiSgbATEw4yVRyGL9didU5XarAvXJo3TSjZsLcmpJsgMViufphnMLXlP+URuLASd5Yfw3WKVV1sHDE3GemPKd3evE2jD6xuHrOomT4sZFEvAs2hCWYHBWS/wbDqUk1HLmBEvO11Y1MAxy8rY9YfVlSy+qTCV56UqnY8lMl6qsBGdQzQrHAOvCu9ReQGhkxJSWFSaY5OI+UbM7KJx3luJK3KVciBHWQQkjRylSmajBuHdDpc/KWkczrwMdPhqi+GrmsUFp8vWM037YUTvsQtzIK7aLbDosHTMnLDyamtAPCwIFg7cyzvOQ7QxNOhS+/bIp4oyogJE3diJR7AoQhphQfJrPTaeFsTDDNtMHBiY5e5rXqCKEjUpsHGEKkpsoB1NKK5u+lEAygHY4Th1X4HkVEJeKkPfUFMm2jEYg4Si3eSwrUn7inbpZOjj69B8t8UoaTn23Qkcv1FiGwY9CVCrufOEa+e0diZMtwLyAwdM207J4G1F2Yg8QB/MHYAVzit2roJk6BiqVjnGdNlQNI8Mm9/c57snN2jtL4F8XcJsI2B8w4XPhHO3lhUtVc1RS7Ge81Prn9PWKZEqaaictcDR4Bs6J8CglIXIkLehbLn+C2duztTJUVaBbhTYIESkxAd5j+thi39n9ckfujS/FTfP/azRGBS2VZKuBpQNd63Bwq09spHivDjd35RRqEwRTtza2n6mSYaW+UX32OTMBabowmCiAJ05JiLW+YLaJKzW6pp+1wJlFWDTsvwbV3+Hrg7IrXvM02LO3WybfzK8zUeDi+wPu6RpRJLkxI2clfacMCyZPel5xjiCCU4CTMOB/B5ct9YzFYVV6CS5DnjXWdXS2m1goRxjl9ABbyL/xYJeaHSuKDslKtWoQmGaxjGNc42dBxRxwDSN6TUXjM9aBAWYCjj1EucKR2wFqQdL5bgenrmpHRgqAjWmkl+P510P8rpxYVnpTXk7PqG0LWYm58PBJQ9SqlRjG07GrbVhJxwwMcaBtKqS91a7JMqqSmqtKKsG7eiE19qH/DavLBmcufJhKsNJg/uTLYo0RIXWMemtY9AWHfcYMs1Wd8KD1grhVO5ZjoVpA+uupwyWcuKaFFxpi801ZWKJ+gsajRxjNOIZa4zClAHDUqOURWtnDREFJVpBGJQOpAe0WrZbpMuKVfpjCuKP2lFn/wn4J0w/kU3WjdrrPnx1AE5ASEmuFBBEpL7150ihK4+rS6ilKJf/delvvdgEfAEjRa5IgOvAS11OJ6w3Ya8I807Ory4Xk4JJQAUBU+uG8fXiSI7634W90+/3z0l1BbSpp2vK36SolnbXWntGofhYFUXBeDz2qdCdToeTkxNfQEu71QvGw8NDz+YS2WOSJLTb7XMSN2k3Kc6MWfok1q9LABhhr/R6PS9HPD09JcsyVldXWSwWpGnKYDAgjmPviSbtJB5uwpiRMBqRdsvYqEszRY4ofxcPvCRJPJNLvOSkWAXOMU2l7evMV0me7fV6bG5uMhgMfFErwLWcWxAEXL58mdu3b/uQge9+97s+Fff111/3zCgB11ZXVxkMBmxsbLBYLDwwNxqNvPefAES9Xs+H1QgrrSxL+v2+9z9sNptsbm76fhcG58bGBtvb27z22mt89NFHPH/+3DPM6tcu465emNcZxAI6nZyc8OzZs3OMSHlst9v1rEUBrAU4En/D3d1dDg4O/PUmSeLbczabcXZ25v0urbXcvXuXCxcueIaopPhub2/z+eefk6bpueAmWR8EKBMQqygKbt26Ra/X4/T0lIcPH54LPbl27RpbW1uMRiPu3bvnx6HMj52dHZ/IW2ezyVyoA9oyJwUck/6os+vqsk2ZR6enp/ydv/N3vAVEXSb/Mmgn41NCY6bTKb1ez7M264xYOVdhIYvH66/92q/R6XT841dXV7l//7733hQmZt0uYLFYnJOWyvW0Wi0vTT86OuLmzZusra2xvb3N+vo6WmsGgwFPnz7l4ODAA31iDyDsZln78jzn+PjYr+Vy1MF6uYfUgVuZ/9JWMrbkEEBNNlL6/T5BEDAajTg7OzsXwiKJ0r1ez499YTfWQTpZT4qi4NmzZ5ydnflxlWWZD84SpuLTp0/PsfFlzAgIe3Z2RlmWbGxsePuAsiz9OBbbh8lk4pmN0r/yvczpel+dnJyQJIlP2a6DoGVZ+hRu8b8Uj1jZnGg0GmxsbHibC2GlA1y4cMGzSoWVXP88IWNQfC/r3rRyLxW2t2xaSFhbva3rYVv18C0BIf+oxw8/gMgSYKOsZKzKMbR0xeIwIYQzQ5loomnpE3iVsZStgKLlPKiCuQPhygY+sTldd41ZZ4uJn16Z4H33ggz03AFGLthAmDvV46tzFCageAwKm1F8rHzxXoGPAtzJ73TmmII03HvCknGkKuBSVwyWogVBlU6d9Vw4hw++zB2b0QEairwHOl22qWMHWsxWxqLp0E2dasKZInLBUoQLBzgEC3xbW1VJiismp9UwizXJwNI+KB1gayGalMw2HfOzOS4d0NQOWKxqWkclyXGKbYRMdxLiUUlymlaFOMSDgjB2IFA0KbCBomgFRKOcshFUP0foHJpH1gO+NoDgyHlkRjNL3g0oRwH/cJbQ1zP+ztFX+FPrd/neYhOA2YsON15/QagMgdJOJbioWC7GkgxdMIiwBYumXnpSKkh7zvPQgVgsJe+lA23jaixFM0vzzFRAR8VUaDkmn4C24m0ZjDJMI0RPMgeyRKEHLygNFAV2rctsU1P2c1Ro0ZEhCEtMqQkjR8dcvGjTfRBw/ZOMaDzFakXejZhvhIxeUSwuFqjCEg0DmgeKxokLdxjc0iwuuNdQC836u5qzOxa1mXkwvGxA3gvI2tqPOXAJ1M3jkrytSVcc6NU+MJ6RK/NhPGvQ6E2IRxZdVr51ceiYhqF2ku0orEAmBVHo2qEol+1QAUiqNNiwAijAjaEodCywULvnBy6tFa3AWJIzRacwlZQe8l6MiRKKNswvOJB6cSOn2U2ZnzTd2JoH6FRj85hZqWEYkZxpyoalqACb2UVD+5kmmDspsIncWtU4MVVStZszQZWCbBVMLmv+4uXv81f+4Z9mpRDQvyreeopsvSBIQ5rH1SZE7Ma5ieD1Gy94s7FLgKWvFyxsQE5AQ+X09YIASzdOIdM0Ti12qJZzv7KwUyLRlXAOu1y/LkQjJjZlRZ0HCgFyWzKzGTNTemXq86LJi3EPNQ9oHlj/eiZysuy8jWOqVXJdVTqARylnR1BWbZP2FSZy3nXtXed3qPISFSh0VrqfK+TLtGIfbmQDVaWcW0wSOOuIfspp0eE/PPwmn44v8GzQZzRqOrYZkKzNubg6ItSG4aLBYNTi4MkaOtVLT8EKmEcAKagkxVXbFZXEYKErFrRd/i5TmKZ14SGlY8GRV5sclbScCqRUrZJoY87OmqOPP9rbgOPY+UB2Mr517SE/3f+MrzSeciko+S+HX+T/8fjnXchK7NpbgEiZD6mJMFgPIpbW8GoUorUhCIw7z7D6qi26WWDmoQtnAWg439S/Pnqb/3X/IwbGcDjpVM2i0JlLmle54sbmKZeCjEQ13fvZSg5crZe2kjurGkCpUbzeeIEOLKZigNvItZUqFPmgQf/ajDApyFdDHxRTNvDAbPfimK+vP+bJ7DKqhOT2kNmjHjY2/Oc//1/wf/j4zzH6eJ0yUBXQ60BdnQPDCHo5pmdRRpNlIUpBGJY045w4KGlGOaZKYO4lC4rKGiIzAVpZ/zdTBaYIkCgelT8+frSOOmghxW49bVV+LwyAOtgHeFBLvPrgPNtDADEBtgQEFIBRCgwB8+rFT/19pGiry//kNQWIFBBK5FCAZyRJoSHywHqitDzuZeaksNEkoEUAUgGk6sCmeCsKYNbtdtFa+2JPZNbNZtMDCAIKyHUI8KWU8rJGeYxIM6X4rifOip/MtosrAAEAAElEQVSksGWSJGEwGLC2tkaz2WQymXB4eOiBp3oRJ15k0vfCCBkOh76IF0m1sNyEETKdTvnss8+4ffs2Fy5cOAeE7uzsoLX2icuSPnp4eOiBXvEeFGZgPYBCACI5Z5G5iQ9Y3VtL/A0lLEDCb+pJ4eJNJ+9dDwSR85YxsLGxQaPR4MWLF54tKZLVTqfDK6+8whtvvEG73WZ3d5fvfve7jEYjLl++zI0bN8jznCdPnjAYDDzzbD6fe2/EJ0+e8OzZM1599VUuXryItZbvfOc73m9TmKhSKAuIGYYhL1688Mw5SVEWX7LFYuHfs9VqMZ/P+eijj4AlmCRtJvO+zvCSOV/38awzfF5mK6dpymg08kzKOrAicvEoitje3uatt97i9PSUTqfj++nJkyceSJEwFgkREjDbWsvVq1dJkoTDw0P/uiITrjM16ynbkkr+zjvv8OGHH3Lx4kU+/vhjD8aIr97JyYn3FxWw3hjDhQsXSJKETz/91K9JMtfrDD1hSYlE9PT01AM29XFV92oTNlmapiwWC3/eMvfkdcWGQfqs0+l4+Wqn02E2m3F0dOQfK2uorK+ynsnvJUl5bW2N4+Njer0eQRBwfHzs2WDiVyuAzcu+o9IG6+vrxHHsve5+5md+BnCbOsPhkHv37nlvShlDwgzNsswz3+pjSwDwl+dmnX1YXzuF+V6/38g51zdohBkrAFlRFAyHQ1ZWVhiPx6ysrPDmm29y/fp1yrJkMBhwcnLC5uYm1lru37/vz0HWyvq9qQ7cCrDb7/f9OcoasrW1RafT4cWLF0ynU9+eJycnXLlyxdsICBuvPsZarRbr6+u8++675+aZzO36mKzfoyXNe3Nzk3v37vnrEDa/bEyJ3LkOjsv3dTmyAJ5aa27evEkYhrz++ut8+9vf5uzszHvwCrgKjpEoY68+LmVsGWP8BoLcJ+vBQfVNHrnO+ueJP+rxIwEgigdh3q79zgDKBZ/owjHsdG49E0UZiypcoWA7brAWrYodlDnWkSqdLFcXS/Zh3aQe3M/aLNmEIj0T7zCRpgUZHki02gW/CDtRHiuVdrCg8uxyEmFlKtVaxbT0qZEsf2cr0FPYQRJIoErHkhPw0zHinBxbwAadSRHvvtepIt0qoZtj06ACVzXxoApSiarzsZVU11bsutJJbcPUEk1Lji9GFcinSFcc40vCToK5Ipq568+7AfGoxIaO1ViMFVEjoGgFTC+49OJ4oDCNkMmVBBMous9TrFIsNmLma5rOi4JgmhNMc9KtJqMb0HugmG8sEzVnNzPan8UkZ5ZwAeOdEFTOfz98iyuNU7774at8+aee8l88/RaHow7RmebJ4RqPzTr/cf8aDxabNA412Yol67gCs4wC8k4F7ObCNnSLUfN0WayDMA8dUKJLfOK2rSH+IsMM5zJ+HFPJSa4tepFhwgYECltJg62qmHeBxhqDSUIW68rTLBWOtFeWGvusxcpn0HucEc4WmDhgernJbEszvWQpOo491dgLaZwsz/3sTdCvTIiiEnvSQqUBKlf078+ZX2gx33JMHhu4cQSuPfKOgOTOzw8cMzXrOtYsOJCxaFVJzFPLYpRAz91Ew2lBMElRi9xNu3B5Y7FKoUIXkIG1EEbuK6CKSr7ysh9slqPmKcpUIGQQuHbTlXelUv41SCLKJCAaZWz/xpRnv7RG59qQNI3YWJkQKMtZYJjPEmdbIOvPMAJtWVwoCCYB69/XRDPL8BWNLqF9aCr2rFoygyewWHVy1vZhwSgJ0TnM/vSY3xtdo/1M+00CCU+Jxpb17weEc8fwKpOlvLNsWr618YBx2aQR5uRW01AluQ1pKPdzoEqXwqxwGwytivGauzUvCMslAy1Yrh+qhOT1IX++/xEdlXBYTtkrQgamiVaGnkpJVMle2SW3IV09J8BSogiDEhtZFhvOXgDj1qNg4fofBWXhgF0bqGrNtCjrxks8ssy33O/DiaJ5XLg5Yy1FK+L47YTVz3Jan585RmmosZFGlaUbJ8oBk+lqxGzbYqch/9nv/AyEBqWdT6gtNapZYBcB6VmDx6dN9KxiqwagQhfuohfaSWE9S9DJwVVZMeSUXQKGRi0DYaxyzLtcOz9DC2ruxp9VFt3NndXA1LE9VS9Da2i2UnZWhry9usvvn1yFUVT5/Rn+rbf+KX+p/wkzmxMpjbGK47xbsZkF9QWVO29F0zCUhWZcTdZAabCGgpLclrTjnHkau/OshlVjdcGfe+19fuW3vumk2JFrr/H9Pq3XMjq6wd+frTAaNx2YXgGBeqaxieVPXviYjaCJwTA0GZlxrD9lzrMjVWBo13az3oj3CaOCjHgJzAoYqiy7075bK9bnmDKgHEXQMKi5m/yzaYNfffQFx75uW7LDNslYkwWwm68yHLeI5grQ6Ixq3FT38sCyuTVyjOiwoB268y6NYxxaq0jLEKUsgTbMiwitLBr3N5Gba2UrL0QHLp5OW4QLhSQ///j40TmkmOh2u77wqH9glw/oAvjJB3wBASRFExwrRfzl6vI3eV6dMVFnAgqDQooWwBesdRl1PQ1Z3uNlaa3W+hx4KEzGOpNNHv+yT5kUe8JuqQOZ9QJNGE5RFHkWhHgfxnHMysoKzWaTs7Mz71UoHoZyDnVGkjAu6oxLSRg9OzvzzCQpvkWeK8wgkXN1Oh2Gw6FneBljmE6nHhiw1tLv971EfD6fc+HCBc+KkSTg69evc3h46KXjUvyKNPHw8JA4jtnZ2WF/f9+z54T9JnJI6YsoirwMW0Bi8a2TPhBvzJfl9DLOpEiW8SfSRWEfiUxWxogwNetBCXEcM5vNzgFldZBHxvzm5iZlWXrfO6UUly9f5rXXXuPatWsAPHz40Id8XLx4kStXrnB0dOTlriL9FSmxJN1+8sknHphcX1/n7OyMN9980/sHSqHdaDQYDAaemSZtIaAc8AcAo36/z4sXL9jZ2WE2m7G6uurbWa5XmIx1MBHOA6n1OVH/vcyFOmAv41nmlTxOvPbqQRlvv/02Dx8+JMsyXrx4wf7+vgfg6nLGuq9fr9fzwIO06XA49KCyANuSHlsHX2/evOmTWiWBF/BzbX9/n+3t7XM+l3JtW1tbDAaDPxBUI9/X213GW53pV18z6s+RNePKlSvcvXvXzw1pb+AcgCZM4263S1EU58IpxKpgNBr5eS/XJu8vh2wqrK6u+o2DmzdverlpFEXcvn2bXq/HbDbjo48+OscUFiBQNkEuX77M6ekpp6enrK+vc//+fQaDAbPZzI+p+ngVQG84HDIajc7dIwQElLEpR93iQo76RpL0d50tL+uBrCnGGPr9vmfFrqys+Pnw7NkzlFJcvHiR7e1tvvOd77C/v+9ZxL/0S7/kZf3SDgJYy3vX5exyiDdi/f4ShqFfQ2WtlWtWyvmC9no91tbWGA6H59ZGa633rt3d3QU4Z8MhfS3goNxvZY0cjUasrq7+gSRmYYhKUvc777zD3t4eu7u7rK+v+/VC1p/d3V0/34W9+vHHH/MTP/ETfozUwVsBH6UvBJiUTUNZS+R86psZct2yGSFJ12LdsbKy4ln3f9Tjhx9ArEC96aUq2XXuPBB1XrF1KuZJESnnHwdY7RhrVrvgCqshmhl0Ebii1lKFjFTPbYM+rZh7VQppULGBTeIeb0L3NZw5No0k7SqLCyNpuK/RrCrGKxmsC1VwryVgo0vUhCh115P1hEUJ0cKdkxR1TvKoKGPrQ1CCdMmCFNZjmSxfV6f4AAMTOYDKxBbbLF0KZmghKSF1qIGNDGUb5k0cmFWK5HRZACmrCOYO6IqminDqinxdLsFcL51TDkxrnBXMNgInY2UJpJWxckzFyvesaClMEngwLm9DGWvCRcmir8l7CnOiINTkvZjTN2OKtmGxof216xwaz+JKbq6IJ5bh7ZL/6U+8x3908fsA/Oorh/yl/id8PLnEv3bld/m/5r/Av/nW7/D3nt/hT7Q/YVg2XRhKqug9yYkmBWUzIJs5vzurHWBdxlUBWqnzdAW6BLklnhh07kBWAZQBL6t2YFolVQXKxAWrNI8tQWahcNJulZeO2SiAl8HJe+cL8l5MumEgNOi4pFiEhJ+2uHDP0DzO0JmhbAYMbrWYXFYsLpZYXRAfB7SfO3ZYmBrGlzXTKwbbKdjcHhIFJXuPNxxAEhn0IkBnpWPXBtbJ+iqmZdHQJGcFJgzJOgobubYJFyW6UCzKAJMsx6WJHMkpHkNwFhJdK3n2cyXmt5qsvbeAwxNUp+3AA5Y3O7RGLfAsQ5TCCsPwDzlUEp+3HcsL1CJD1XddrAVjsb02ZatFutEgGlVmwt9fpXkG+zdbBBfmNJoZjWbGYh7T7czR2jDQbZJ7TQeITlyq62hLM98pmVvABCQjx1ydXtJMrpf07ge0941jYO6ElE3H3PyXXv0Bf+P3v87a3AUTebl/5aEZLixBbkl7mrJBxVh1V7gwEd1gTltlDEyTrl4QqYKGymmokhPTJDcBqnAM0zx1zC3pDyOSUbvcM9GlC2Oafr7CX77+db7UfsJWEHIlHLEWVCwToKEUm8GI3Fr++uht7o53OJj1GM8bLvhk3wW2FE23vubd5RoqEmbna+iYZn5ThCocKlc0D5182SpF2YrJVkIm1wzxIKR132IaCTbS6LSsNlYqWWGkXfryRk4wCDENi5oEnkmMduCUY6xWQyKyHuBzmzZuvrvXtdjYLv0bLUuZr8hWtUVZjZ4rx3Sz7nfe27BpUJFxlqWFRsl5KEsUl/yf3v77fLXxjDVdshYk/IVFj8fZpWpttUSq5NhkdJUmIiBXJadZuwI3WXagqliPSdWWVTL7zGSktqClIyIVcKkz5NnzdSfDzh37MJ1F/MoHX3M2FxV7Vh/FmM2Mf637KaVt8GunX8AOYnefjN29Si8URbfk663Pq2Aa53BYGsc+tMouk7YNmDygpVJKa6oU7Jwr6wM+P256RqywFTGK40mb/+3b/4SunvNX7v9xhvtrrFweMBivEY0UxX5CmjeIh84WQJ9o3+5b4RitLGXDUnYMpSRXRwZ7FmKbJd0kJaoo9vNCfHIVRVkxm6s+Diupcl4GJIG7qZ/NmownTYpF6JilhUYvnGdmQ7x5f3z8yB0SoiCMAWF01WVSwhwQcKPRaPiCRIpW8TiqJ0Q2m81zklzAFwN1Q35hTgjTqy6RrgN8cj51Hy0B1+qefVJ8CksF8AWwhAPI9a2srHiPPHnuysqKL7ZEJtjpdDzzTuRq4lcGS0aI+JiJr9ZsNvPSXLluKTyFMSeAm5yz1prDw0MfXiHSRZGFrq6uepngywVsp9Px1yHt2+12PXgmEjtp08lkQq/XO1doHh4ekqYpx8fHvrgTuaIAoNvb2xweHvLgwQO2t7c5Pj72XnXdbperV696oFKKPrlea60HQwRoEJBTCtDJZOLBFCkupYgUFmR9XEjbij9inSkn0rjT01MPaEgBW5fmJknCzs4OL168YLFYsLOzw+uvv87Fixe9f9x8PmdjY4Nr1655v7bPPvuMbrfL6uoqo9EIY4xnI2ZZxkcffeS91cR7UkIKRPIqIFY9GKQOvtbHgfSFAPLyvczTzz//nNdee40//+f/PJ9//jm//uu/zuHh4TkvUpkTdeamMPPqtgZ1oEKeA0vmkwCGcgjQJyD99evX6Xa77O3t+ZCcjY0NXn311XOSTJlXa2tr9Ho9wjBkb2/PS+GFobS2tkaSJCwWCy5fvkyj0aDf7/PJJ5+QpikrKyuEYcgXv/hFfud3focvfelLfPe732WxWPgwEfFwe/r0qZ8zdQZ1HVyW7wWslzWuLm2X16szDeugYJ3RVZalX7fq4JmEaKytrXn28vr6Ot/85jdpNpvMZjMePnzombSj0cgz0+qBEnVJ68tWCUopRqORB2IePXrkr0VY5BI+I8zQuuRV+rfRaPD8+XMODg48ECzzeD6fewuF0Wjkx2zdMqAOGokNhJxHnXEo/8VuQTZZRHor95N6n9QZeUopbt26xeXLl72cPk1Tfvqnf/oce1NYw3KNYvEga6bcGwQ8qwN3sklR3zQT0GuxWHiAWYKBjo6Ozo2HL3zhC977sd1uc//+/T/gRXnjxg0Gg8G5QJN6n9RZg/X1LcsyTk5OuHnzpn9vmcfCxLx69aqf/zdu3PCWFNZaDg8POTk5YTKZeEsHAZ+vX7/OwcGBPwcBLmXdknVYktflnGTNqo9JpZRvM9kYkHVE5mOapt4OYzQaeQuNP+rxQw8gKgvzC4psxRDMHYCWNxWhXsp/xW8uWDgJbd4LXcBBrCr2jmJ6wRniSxKykxBXkqPKt0znuAInckChqf4uxa0N8MEVZaMChYxjKLrAD/d8pSA5c4DQ7IKufMDwgR8o955l5IprVbF0RKpaVEEotmIdZf2K1ZdCOHUA5uSqIcgU0VSRrcDiUk7QyTEnCcFsKRtTRgBBRUmwTIY0gQ+lsRLcEJxn1NnK805Vvy4TV9RlK5UETxsf7qIMICEHldfU/ELgCjmrUEVlYpxY5tuK8dUEZZykHKso4wbBwhXc2YpivhESzQLfx3lLY0IXvFI0cMV5JRM3IVC6Nla+LxTxhak3689tyWoyo6VjjhYd3tja48/c+ohf7N7ls9Utbsct/pWVd/kvr/8cplWiTEQ8DB2bdOH8HHVuvSeaKvFeiaqwlA0HVOvM0pyUlLEm72jPDBX7rWjuQMYgdTLpMtHkbe3At7mBOGLwZo/2Xka8N4RmDKXFRoGTFcYR04sxplmiRhGdzxNWHhdE4wUm1izWI0bXNLNLBtMuHMMIINMEqaLz3DC8oVm8nrK6OmE9ySiMZm9/leRhgtoqsaElGAXEAycHLZouhdkaBZFFTRV5S6HKwLXz2BLkhjJSLPquz7q7JeVR5TlaGBarjmmqjKVxrCmN5su3H7Py9oJ/+k/ucOV/aJM8H6IWKWiN6TarsInSgarzFDMYojfWUDOH7ttFiooj558IbuIBaOd3SBU+Y6PQJVhXYKw6G2HOBqhuCxNqDr4Wka5rkhMHus9uz9DPWuj7LRZxi6JlYCVncNQBDc3egqyfsPoZTHa0C1FZQHM3oOhYplcts1LROFRkK87fLu1DOHey7qyvSE4tZz+3IDUhvY9i74UapG5MlwLOWwe4F00HWtsI8r6BTsEgbxFgGZgWm8GYqY3ZCYaMbEJuNXmFyNnEMN8M/PoTzqrXrVhhIuW2hV6Ge5Rws3EIwP/l8Z9hf9zl2uoZoSo5XbT54touf/XS7/F+mvLLn7/DfJawsTp24FpimFwO/fwIMmcfEaSWxVVLEJZLJjdUGyOKaGxJVxQ2NKhM0TpwfofKOkly82DBlf8+JlgYCANstengglMc8CRzKl1VJLsxNqhY6KEDvNyFVv9lzYMKhLMe8VGZA1dtuAQIqZh0Kq98DisQEA3k1eZIv3A/Z9qDi6pVYguFDizNVspWd8LptMXgRc8FyGjLVxvPuBAYvp+uYtC8t3+ZaOwYjCqwvJnscjFwUnKDYWYMn482/P3EVkEueha4dbo630fTdVKbk6iIlna7zH9n2uG955fRcYkdxY5ZCdhZJV1Wjmnc3p4y3W/zr7z9LlopHhczPh1suQ212HrPQlB0NqbciVMgcWxHYJrHS+ZhqTzbsdlb0Naps42wJVtBi5V47kKbUkVdJq4zzWi/S/pqxFnR5vR5H61h8sE6oaHayHIsdJNAvmKwkSVcW/C3f/I/49WoJIxKsgAvGVelcqzJarkwVjHLY5phXr2tojBujTJWUVYPPBu3yLMQa5Rrq0IRTCsgRDmWvw0sZdP4jbQfHz96hzHGJx6Kx5aYvQvQIawLKYwkDECYhysrKxhjGA6H5workSYJwCMgjRTZdUaPFJYCwtUZh8L4k+Jd2EoC+IjcS0AVKabrISPyes1m0zMNha0iRaycm7Amms0maZqytbXl2T/ClBM2kZjl172bpIgUNpO8ngCYdSl33VtNGHUih6yziOT85HoEKBIGR6vVoigKLzW7du0axhiOjo4Iw5BLly55UEIYNhKkIqCcgIciZ6tLAs/Ozrxf2M7ODleuXAHg6dOnDAYDms0mu7u73L59m4ODA54/f/4H/LVu3ryJtS6IRph24hkmQLQUznEc0+12z7GJRFItRbkUoDLGAC/zbrVavs2kYM2yjK2tLa5fv850OuWTTz45x4xpNBqsrKx40Peb3/wm/X6f09NTvve97zGfz7l16xbXrl1jNBrx4MED9vb2vBzy8uXLJEnCV77yFRaLBU+ePPFszFu3bnF0dOTDZMRrTOZMvQivpwIL+CJzUwAwYejJc+shRwIC/c2/+Tf5yZ/8Sd566y1u3rxJmqYcHh568EiAJhmbMk8EwBYZsDBgZcwJo7MOLNXBfmHUgQPmrl+/7gt/YeIOBgMGg4H3XBWJt6TRvvLKKzx+/PgcY6vX63lGlciYhaUp65Sc79bWFhcvXqTX6/H48WOeP39+jiEloHuaprTbbc/CTZKEq1evopTywU31eVYHPOp2B1EUeVBQgJD6IUC1zP3xeEwcx9y+fZuf//mfZ21tjQcPHvD48WPvQfpbv/Vbfqx9+umntNtt750pa1iWZT6BOUkSv1bXAV1hB4dhSLfb5eDggPX1dfI892BWlmV8/vnnXLly5VyI08ugpHi6np2deeZxnaUma5/YBtSZZdIO9VR42XyS9hEmYR34lH5bW1sjjmOOjo48u1DeT3xH+/0+i8XCM+lEbn14eMju7i5XrlzxIKeEhKytrXnAU85ZrBTOzs78/anO2hWJvTHGM2hlrb99+zYPHjzw9hPChn7y5AkPHz70mwvNZpP19XXCMOTu3btsbGyQpum51GfZwNre3uZ73/ue39gToFXGfl3uKxsKMjdl/RV7gPpmjDDgL1y4gNaa58+fMxgMPNAt1ghyjxDZ+507d7yvrWxKSZ8IO1j6sD536uuLeKYKg7zZbPqxJvNxNBoxGo3OWVmIRcXLc+yfdfzQA4hYyFYMZiMn2I1RFXvQVOnLQWYqlqEiyCqpcsUQtArvxyYMDWVxPoKFSy8OFs5DUAqdoqUwgQPulHGPCWcQV2xFEzoQMpy555YNBwzqHKKRA+OKxP0uXdPOp3DhgLLpjqH7SLtQk9BJ9kzF6Arn7v3TviLrW8J5VRRVEuSwuoC849hr4VRVbQGLi7nzrjpOSI6DJSMQvDTbBhAP1TmA0CddCvBZMcxgCc76x1VsOg8WUkkqlT0nqXbPkaKbKszCQuUZiXWJynnHFbmSGpqtVu9VMW4WGwpdqqoIsxRNTd5sEc0rFlDkGE4iBQa8zFyX7vn/wqsfshI68+jP85TV2H1fGM3YOOr/b89e42bLmT4OTIxpGpLVBdiIILPkHSfHLVrVmKpwAWFPOtDCSbwlDMWEDrDQRRWaoan6W5HFGl1aN/byKsCmhGjuZK/pdofJJU0ZJXSD/rK/FOh2hFrvEM0N6+8GNI8N8SjFhIpsJWR8OWS6Y8lXK+BQg5Kgh0aJ+eKCk7c0r1xw13s0bfPk6Qbdj2PWB5bJFYiGmtYLBQaKDqAUZdM62V5ckIcBtkpijicO0C+aruGLeor5QBPNDEVDM193TNF4ZEkGhsWaJi0DxnmDzISY2JKuhiTPwcYRhAH5Wqs27hQ6N+h8DVtaz9BUgXZgIQ44wpTYwRCURnXa2OEIKzfz1T52MsFevsjsS1dQ9grJyaKaG5Zwc4G5YInCEq0t8e0TsiJkPo8JtaHZyJlMGphxxPy4hQ5hekHT2je0DmC+4dqi/dxdZ9p3jMHmoaK9G2CVJVg4CXc4c9f1Z2//gL/7+R3as2WQEdZtIEi4jjJUYSCVV2Zo6Vwe8S9e/5BXGwcMSlfwNVROS6dkaGJKtLL09RyNS55tHjuf1DKuZOehC4PIqnljdQUQU7EcE8tmOOIfDb7A/t+7SvuF4e6faNPdmPLK6imvt/YB6Oqci90xX732EX9q5QP+/fv/M2ZHbVr7lqJdydiVpEcryDSmdBscyuJtHdz4gMmVivmWKxpnOTbQmLhiLBaGxv4MPUkrWXr1e+sYzyJfzrvOdqBsLj0LTVhtNkQ1UFBBPWUcXZ2PAIyqYiKGlXS5cF6asl56oKt0TO0L147JioDhuEWZxt7b0KaaztaU/+gLf5sr4ZCDssP/5u6/6jZBgGwR8p8e/RyDvMnudIXd4z75ICFRljKxbK2PeC06A9yHhdJaFtYyWiT+OjyLM3ZsZVU4ibGxmki5Rf24nLIRtHmWrZMvQlbXJpxOI8emTLVjCQYVshwabm0c8oPpZVITYqxlYGLGi8SzMlWpfFjJL169R0clLqzFGsbGOsaesg7M18s2D8OSSC3l80U1XtUsIEjdOuuBbEDlAb999gor0YJwFGAuLcgWYdUHLogsX7EVsGmcHUdS8KU4ZGQKikIviaK5e/267HueRwTaMM1jCqOZZxHGKuazhDLTjjqdK+eLydLSRJlqPFVsTUrlx4z3rf3x8SN1iMRIPOVarRZnZ2e+MOv1eueKZink0jRlNpsRx7FnC4o8UBhhdbaByGCloJKiS5h3AuLUpZYCGNYLSa31OXlunXm2tbVFu91mPB57NoeAHHXQUopwKcCERSQFrgBbdWmaMO663S7D4dADGXJIEVxnYwoAJFIu+S9HXe4JS7lu/TEvy73kedY6r0fxQ3uZkXJ6esr+/r4HzaQQljYWabLIIoVF88Ybb5wLMqmnctbZo51Oh2984xvcu3fPAyEi0xaAdDwec3p6Sr/f9yEWUtgLCCWyU5FgS/EpxbOAB/WUb+k3AT/lmuspzdZaPz5lvGqtGY/HPrVWWHb1fhGA8smTJ6yvrzMcDvmd3/kdL3/udrscHR1xdHTkE21brZb3BDs+PmZra4tPPvnEz6GLFy+eAx4EpD09PfXyWGHXyDyrj5s6ezYIAh+cE4YhJycnfp4IaCjsr5WVFd5//30++OAD7/smALTMwTRNz4UX1KWPwLmfpXgX4ErmSn08iuRYUryfPn3K2toaSikePHjg59PR0RGAlxpL3wpwNJ/Pef78OWtra379EEbmyz591lpWV1e9N6ewNF9//XXvEfnee+95hrRsNtTtGsQTsyxLVldXuXnzJoeHhzx+/Nj7n0rybB1AlHkrY1j8RuV6pM3qQK30rbDTvvSlL3F6euoBEmFZCYAi4MvNmzc9AD4cDn1YjTDDtHb+dBsbG379kLW7Hmol57W9ve2TrAWw2d3d5fT01I/B+rgV0EkARNlMkXVBQMg647vOyKtLkf/HvpdQoPF4zGAwOAc+ijfgwcEB3W7XM1Bl7XrzzTdZX1/n4OCAZrNJv9/n+fPnWGt5/vw5Ozs7vP3220wmE46Ojtjd3WU+n/sQqYcPH/r1BPAp7LJZIUfdH1fOr9VqcfHixXMg9Onpqd/IOT09ZW9vz7dVnfE/mUw4OTnxFhTz+dxL/uU9dnZ2KIqC58+fU5alB1nlHlM/FxnXEgYla1P9Hij9KOvL8fGxD2OqJ1qLn67cM2TTIo5j2u027777Lt1u1/e/bPzU718CDItvq7Ct5X/9c8Lx8bH/LCD30Ppry1isb1788xw/9ACiMlD2SsL9mNauqoprSxkpsp6ieexCKopEUTR0LUXY+nRlXS7BLwM+dCPInASxTBxwKMnK4YzKR88Qj7TzXrSVn19bPBQrOXOiqgReB1yp3AEqRmRsFZhXNMF0SpR1ha9LQwVlXUqr92RSFXhQ+aAVLSfnslEVmpIqgjk0TpT3W+x+GtE8DklGJTrPHYClBUF0X4qGC/wwFdhnwoq5o/Fp1FYDxbKtzgGHMu6ssO4cMGrFr0ov38uBhsvHiyzVe0QGnEuoVEa5a5RQGSWPqYp0o0g3DIsLtjKmN0tgOXZG/suC353X6KZlKx45I3/gN2av8YXucwBaYcZJ0eHhZINx3uCPr34CwON8g6i/oNtKyVSXaGadhDRdAoPSjso4QFm8/oLMBTmY0AG7QeoK+TC1HhjSBQSpC+mwgWOW5U3XBvEUksM5phFy8bcL9/isqOTjVSKzdeBZ73RKe6WJsjB6pc34qvaS1+RUEQ9D/55UY8QqWFyIMSs5nz7ZJnoR0zhWdIHpZUP2k1O6rZTp9zb8mHXgs8UkDkAsi8CFG5QVaF5YDzqLlN4Gbqzk7cqfLoBshQoYc4zZYGGZpTG9OGWax8RnGl24tGk1zZxEOXAeeSaoxmisgbAq3Cuw1nZQVlivFj0vUOtdgtMJFCUmTbF5QbDSgzgif/sVJpcSzypt7JforKRxrJgHLTfHc8XsUk5rY+YIjVZR5gFBK2WlN6PsaMbDJnQsw27AMNO0nobkXUu+WdB8GpEMLc0TQ95SbvxklmBRMYcvBcRDy9FPGkJtCN7vejm7hBaZUBFl1s8XCVGyARRty89desxPtB8RqYK80mqWKBY28oxb54dYFVyhZb5RrTkLB9SZSDm8oxRGcjW3q01Yq8FUKMhi3WJ/dsh//fZ/RV9nrGlNS0ekFiIFzTDnanLCTjBhnkeoXDHfUp5RGc7wlhOqVWIrOasNqqTcahyVMRRdN7dbe5ZgXrpAlFijUuPWERmTzQgTB+jcOECoCm9Cw2JFU1ZAGn5zw90PlFFYLATWgWwKB5qJjrtqDxtWgKJlyUpcyXh155Cnp6ukzzpVO1XFykzTi1OiZsl0nnh2J9q9l7WKK+GQK6HGMHVtEBkHqE4j/rv3vrhcr5VLIjYhmJZhpzPkQhD7IJRAKcZGOw/DGlAloR2qVFgDxIq9SY+DMuU/OfkWt5t7/Ju9Q/4XK/f4B1ff4vHJGn/hm7/Bf7f3Fi8+23SAZ+m8Xw0B7318A93O+cdPXuc/vPBbfJReYjpLXLvKJwsLdjvlT67c9UnPgdJMbUhutPeMVLnyRpuLRcRvTN9gUD7FoGmonNvdfX6vcwPSiLJdQw9x/fNi2uNfv/k97r29xeFpD5uUle+kPQf4rl0Z8FMXH/F81ufEzDkqNWFoyLT17aoUlJGgl5rJIiFLQ/JFCPPAnW/h7im6AgoxbqwK+1Kk+F7WLpthxt2PwnmVOv5jFuKP1CFsIWEACLNDCvF+v4+11oNRUhxIQVJn6dQlZ8LCE0aMgGt1wFKANim6hH0kxZoAiQLoid+UAAhSPNXTkOV9pVCqS8AEcBSpoDAYJCVT5LQij5XCfX19ndXVVVZWVs4FsNSZg3LUr70OvEpBVw+nqbMP5XVf9i8Thkhd0vc/5ktXf560HSw97gREk/cdDAYe7CjLks8++8xff10uLbJiYdw0m00v7f3d3/1dDwJJOIMAEMKyG4/HbG9vc3JyQp7nPH36lHfeeccDXsPh0EtXpe9EQikg63Q69YCh9L88RoppcADgbDbzQLSwrqQfRZInfSvFdx0QMsbw8ccfk6YprVaLN998kzAMPfBwfHzs5Xj1c9nY2PByuwsXLlCWJXt7e3z66afs7Oz489jc3OTDDz/04G6/3z8nM4+i6Fz6ch24FyBKwFJhhEnidF122Wq1PDspz/NzvnwCPkiQw8vtUGfmCsgulgWtVovJZOJZuAKuvvHGG9y+fZvJZMLdu3d93xljfLCLSOq73e45AF5CfsbjsWfrytwXBujJyYlP+m2325ydnfm0WvGUlLHTaDT4whe+wL1797zUti7xlzYQm4EwDD2D7vr162RZxt27dz079GUpcv172YCQ+SXMbVmX6gBJnbEp656wAL/61a/6OXJycsLh4SHD4dD3WxiGDIdDbt++7QORAC/vlr6srwMCTtfbUvwU2+02n3766TlmeN1fVNYB6X9Ze4T5PRqNzvn01X1m6+uOtJN8fVn+K0ej0eDSpUvnAGJZF4QFeuPGDc7Ozs6BnNKmMm/effddvvGNb3hmeJqmfPrppzx69MhL/Mfjsb9fbG5uEkWRXxek3SRgRfrqZUlxnQG6tbXFrVu3mEwmfOc73+HJkye89dZb/O7v/i5aa1555RW/gSLXvL+/T5IkXkJ8+/ZtvxFXZ/A1Gg2uX7/O7u6uT0AW1p5Iu+vtLgCuyM9FOr1YLLhy5Qqnp6f+Hi8bA0dHR7z22ms8e/aM27dv+zklY0NCzqIo4utf/zrb29s8ffqU7e1t74kr40M2wZRSXqWwsbFxzjdSVALT6dQDjDJm6j/XwfckSXxImHwOkef/UY8fegBRfJBa+87XqWhAuHAgRt4BE2oaJ644TSsQToJSgqxinmTWg1zauKIei/MhnDkGiniPJUNDmVRhGIF2aculAxitdsERZeKYb3WQzETuMY1jSzR24EqpqlCSionSehR5dlHeoZLBuoJVKzAtd05FxxW7OgNbfQ0mArYtfRNVCY1TSzSx6AKSkxydl5hwybiQw4YKoXx4pZ5SLsU2qgajhKBUXmLyOBNrTCTgo/JMP2EoltEy/dmFqLjHSnErAJYEwJgAx3aRv1WeY+Qsi3YEuFQVoKVcOrIAmuPAgypl07VXkDoWVzw16Gtzni3WPFPqg+kV/uLGbwAJnSjlQXqB+4ebzNcivnbxKdDmebbOSmfBZnvC59vrRFMXCBLOBHxwfRfkAipaoqlrE11Yn1Cdt5f+aXm78jyUFG3xjMQliitTY9uUJTaICAeVAafGeQDmpQPKihKV5ZTrXdINx6A8+AaYXoYKjGOQSaquVVAoVKbROXQfaTrPFdNLCSsPS9I+5C1I1y2mYcgOWpzQojODZORYgsECbKixkUVrS3Ea03sUkHWXgUbJyMn1ikS5UBa1BIHDitkrPpcCREczy3gREawYYhyopVMLxgWf2Cjw4KHYBLhxskzbLUM3LnRuXAqvhaLhZPkxEBwOCbY2keTl6eub7P6xEBNZklPN1rs5FIa8G1E2oPXCbU4sNi2dBxHcX3Gv1XJs2cEgxjYMaq4dwLCVknTdrrJ54UDA8DRksWFYbFmaewEmcn6h0cR6xqr4D/7C1z7g7z64QzJ04z0oLEEKWVf5oB5l3dzKOxW7LLQ0r435mZXPWJgIrQ2RKgiqCRNT0tWZS2O2mpIqACJTjoHYdcEmeWWbkC0ivNSyNleFHdjX7gOXgJavhAVaBTRUSEhAgWORhdqFtxyVTc6GbXSqaB5Zl9IeOMaxl0kvAlQjd6FXlZevjR3AOrvoHqMzRfugrMBv5VOWMRaVlthGhIlrt7agWkAL68ZAxaauoELPFLSBrX7j7ilWFknjLtyn5grzsAoNcjsClnZ3wYtRjyyN8L6Dso4F8LOb9wH4bH+Tc0ehmM9ifm9xlVe6L1jTKc04Z1K2UIsAGxmCXo7JNXYREIyrcJXq9L619jmJCj1AFxIws5Z0Hrn7mDDgqsOds+vH8azBX/z8X+XTxxf5Kz/9KwCOKWgVxij+P0/e5uys49bfwjH0ypbBJo51aWYh00XIbyz6PEgvUM5dSrHziLQQwEp3zpeSATOrya1hYS3vL65zeNojHAfVPK0YoRqCwPJ24yl/rLlAowiU5vXod/h7W29xNl11zOlCnbuOg5MVrr9+wn95+5f5M//0LxE3cppJzmDRIzkM0LkiXTekeUQzyDmYdfnZ7/3bZIuQTndBXir0bBnQpFNF0bYEU81st+OATvCN7iThVMCnC6XxbVyBlTp3fatTRbBQy4Ci3G1aGglR+/HxI3PUvZ2E/SLFpEifhMEAeM+pun9hXWJblznWZaPCWpPCQjzPBNyQxwkYIYVevRgWuVM9IKUunxS/QGE2iKdSEAS+oJeCRDwC5/O5bwcJpJDrFKnobDbjgw8+8DJv8cSCZWEsbSaS2bo8TMDVOkAjbVovlGDJrpCj/pi6XE/evw4U1kExuSYpJOusMjkELKiDRNIPwiQR0LAepNButz0A8OzZM1577TUveb58+TIHBwdorbly5YqXS1++fPkcsCpMOWH11ZlSMg7r7SCMF2FMCtMT+APpyyI9lEJVUqYltfjs7IzxeHxuHNX7URg/eZ5z5coVgiDg7t273l9NQLS6VNRay2Aw4PDwkDAMefLkCUVReH+6J0+esLOzQ6/XYzKZ+EJbZMJZlrG2tka/3/f+iQKOCHt3sVj4eSR9I+yoXq/H6uqqZxXOZjP6/b4POZK03LrfI+Dl+lLs1yXTdRZonSUbxzGrq6teoirj/sqVKxwcHHB4eMjOzo6XZ1++fJnBYMCLFy9YXV0lSRJOTk68ZPLk5ITZbOavQYC109NTJpMJW1tbzGYzzs7OmM1mPHv2jMuXL3u5o4wrkTRPp1N+8Rd/kdPTU1ZXV/mt3/qtc0CqzEUZXzLPjTFsb29z8eJF3nvvPR8AI3NPxn8dwK8zqKy1Xq4u4JWM6fpaIeOsLkGdTqc8f/6cf/AP/gH7+/t+TMlrSzq7jDcBTgaDgbdRkHT3+iZBfW0SCevBwQE3btw4x1yU9ULW8DpTWUAzWYvrgVjy+rK5U7+v1Dc8BPTqdDrn2Nr10BRhFArLVF5Pxp7MJVmfDw8P/bWmaerHmjDUhX1XT51fLBZ+3IhdxKVLl/x4k/OVdUT8QOvrfR1Ank6nPHz4kK2tLQ+UzWYzbwuhlPIAr7SvtJmA8LJBI+97fHx8zotTGJKPHz/GGOMtAIRJL/0i40wsMOS5wk4eDAa0Wi0/JoXxLNd1enrK5uYmZ2dnXLlyxd8PJpOJHxONRoPJZOJl3eIhKkFZ9XUEYDQaMR6PfR8IY1XGvzxOFAdy7tIWsjlT98iUDRQBR/95WIg/9B9fTQitJyGNE8tirUo1nVt0CdHYgRF5yxXdTnLswKyyJvUWsK9oOpagMMqq+tgxGBuuUE9XNGXDAT7BwmJiRTxyhaQJFWXFHBQmnhQVOnWvXzQdI61EEljd76NZxeQJITlRXtZctCzRWBEPHTCyWId44NIixTdRvJSUrSTNuSWcuV8WDSeNjYeWdD0CGzmgpfIjFCZZ3btPWVt9b9GFIAfV741CFwZVGPcaSvn3l5RTx1QylSTZVlLC5fcEqmJZKs/IE1akYxYu2ZPu9/i/SRqsY0ku2Y32DwEvlyEHro2EPaULyy+9+iEvFiv8iRUXNXyctrkUFqRWc7V5xnuDK5SFZpZH3AgdGLcwEaVRbDQmPBkpWocFJnLvmXW0C56IwcSKRSVRDRau3XRRAWRFBV7nVSiKlUJSrq+SxrcqdlnFlo2mtd0naz2Ia5XCxiF6uvB/L1sxViuyXhWIk2msjHeFC2vQFpM6xqBBsdiEtU8yZtsJ46sBk+sl8YUZWlvUfpvOowrwmlifDh2PKu+53ZDypENj7sDJIMPPNUlBLxPlmasOGLVuLsWK5omheVpSRsoDpsUiItYlc6OdpUDpxpq7aDfXwllJ8vgYG0fYRoSNQ8pGSN6L0KnBhrpiJgKlA93Et9RN7Gr5My7JN5y5c+x/Zmg9OMMmISdfiJi8Ujhfu3ZJZ3VGejUkHzRQqSa8MCOOSmajBmoUoXPHuCrTAF4kLjCnhLJTEsy0AwmVW4PChbvWxZoDF1qHbu0Z3ILtZIT5qIf49kVTtzGhjEVn1ZzVbu2QZPOiCX/q2qd09dyxDdVyzARYMhxw2NcZWYXup2WAjR14qGxlvTB3a08YluSFBFHVQCjlQCitDLl1Pp4v5isMjKGrFQElZcWEA+chd1x0KFEkjYx5lDDfdBYMItuWuU5kCEO3tjg7CeV9WrO+Yx8mJ5p4lFV0cVDFkmXqJrjGhtqFsOQlJorcWpaVZCsJWd+xxcrY+g0oF3allhLdc5Jlhe2UNDfn1ctbJgedyt/V+R2qxHC1P2CzMeHbH7/urAEqhqOTrFoaOmcjHJEkBbl4JlbyZ5MFPEq3WHSeMTCaOCjduYQV+3JYTeDKJzBYuPUi6i/4yeaDqikUBSVDk/Egv4RZhNQdI+rSYglXydKQz55uo6OS7XBIaQO+vYjYHa5Q5CGjRx1U5Oa8ia3z+6xeyw0Yy5/+4oe8Fp3wV4//hJM6lwo9V0sZr1V8nHVp6ZRLQYoBchuiA0PWLZ3HZRXMRGBpNVK2ggmRSshtCdbQUC69WBmFWrj+E3m03IB+dfRlXm+8AGC7P3YS7qBqv4bF9ArSRcR//es/6ZULWBiPeoRicxGJ5YbFNK0HAz1aW7FYlxJtPHMxyBQqdxtVWPe5QleqBScjd59LoLq3RT+mH/6oHcIO6nQ6TKdT4jhmOp36D/ACkoh8Tw5h4Wmt/wDYUw8zEWac+JPVfaNeZlKJr2I9lKT+VYoI+SoFarfb9QVpt9v14KEAicJMEkAP8DLHOkgj71P3SQPH8JHnn5yceHAA8CyYOpujnqAqx8um/i+zmOqA48sSyfrz66BkHSitg6Xyezn+MKBSvq8zFuWQn+tBGlJ0Clj35S9/2YNSAmZJUMlkMvHJt4PBwMtQpUiU4lfaXxhWUnALw6nuZ3d2dkaz2fRgV72f6kwXYTfJ34SlI8C2AB0vg0h1EEUKVmF0PXv2jP39fQ98SHt2u136/b4H3L785S/z4sULrLWcnZ3xyiuvcHR05AHrg4ODc0FFAtbFccze3h7j8dizfbTWnuElQEcURR4wkLkjc0EYujIPBJyVfhCQR+altFM9GVtAmzRNmUwmHiiWOStArADx1tpzDDVJ371w4QK9Xo8PP/yQnZ0dms0mT58+9XNoOBz6NULmn1yjjGP5e70djHGpw81mk4ODA7/ZIGwkAYv7/T6vvvoqH374Idvb2zx79szPDWEd1uW2WrvQGgFB5/O5T9KWsSLnKQBaXfotAJ+si9ZaDzDLeJP3kc0aGXPyeAHT6muQrCkC3gmjNs9zD67MZjPvT9lut89tTNTXSwGpBHBZX1/3adjC1q5vUIjvpKyV8ndh2srGhfSdrPv1TSUZZ3L9AjpLynyr1WI0GnmAcXV1lRs3bmCt9aEbdZa1JHSLZFjOS+4fp6envPrqq/R6Pe/xKuutjLXr168TRRFPnz4lz3MPxgvDOQgC1tfXuX79ul+b6oBx/T3ldff29jg7O+ONN97wDLkbN26wt7eHtZb79+9zdnZGURRsbm7SaDTY39/3LMCVlRX6/T7379/n1q1b3uNS3lcY+5JQL2NEKeV9auvyXgH7hJ0rEvzxeMyFCxd8inn9nm2M88r90pe+hFKK/f19v7EBS1ajtZZPPvmE1dVVn5Qu81Uk7fUxI2nSdeZmfTNCGNYS4CObQNLnsnbLho+Aj2K3UAd3/yjHDz2AiILOrq1YOe7mPN9S1Qd2x26JJorOs0qK3IZwWqV/Nh0gAkuwKsis9+JzBboD/PKOqtKSlWMs2kqanNuKQae8xBaWPmXCuCobwlSsJMEVeDjdEe8zRTxQS4ClBDtxgSFBumTtqVLOH3+N1qcYu68C1qRrlnzNJS3ERyF8HjgmXAUg1hOgvZ9hRaiRv4O7RmFkApWU23p2l2P/KUy8BImcJHdZ2KvCenYYSoGtAALrwEFdWFRplsVu1beqXP7Cf2/tEiz4Qw4baB+AIx55ZcOBm+lKQNoN+A8u/FP+nWd/kvVgAmhCZYhQ7BUpo6LB7qiHtQpjFbpCTh7NN+gkGZkJKRuWvK0rwM8ST905BQvrwUwTLgFNE0DWUxQdRVmNI2lTxz6znnGlrPPJC1NL3nQAcDQuQGt0YSDLUbFjWamydIrtOHLfK0XRDjGRokgUNqg6Uda2yvfQDmOaewFl01K0nbQ+nGToPHGgVMOQHbZo7gZ0F7DYsGSbBTyIPAvVRFAmAfPrmeu3wLKYhax8HDgGbOWHGWTVYhyqSubvxnQ8tixWYb65ZHfp3AH24X7M6cUWWSlBOQqrNSoMKNsJJlYUKsDc2iI+maPSEj1eUDY6BKkheT50QRqBAq2ZXe6AshWorFxoSlm6AJUkIlyUXPztjORgipouMKsdZjtNZw/5InTAoAqYbgeo1QzVKrCE5LMY1c7orc4oepp+e44CxouEvBew2G+7YCXtpN7tJwFZzwXPJCeK7p6ToxdNzWLVAYm3v/GIf/D8TVov3AaFyiAZlyyCAPHwVNaxOou28uuV2pnztc4jDJqtYMzAOP/DfjAiwDK1LrRiUVGjG6okCUovSU/71gdIuWmmlh6pL4MoygHq0yLBhC6ddmgiFrYEDJtByYpyH1AKG3CUdYlUyaX+iM9PWjSPFOmaYrFxfl0h15SVB6J7L8feKloKE7sE887zaj0xljIKCNLqhEuLMoaimTjWb27cWiB2EYFifCVgsWHdRoRarlWqdAwzORfxMRS2W3t1xmIeuzTdyttPLCiUUkTNnH/xwg+IVcG3yzccyBQu1ygbWDbCEW8leyhlvTwZ48YGheK7Jzf4k927bAZzvrS+y/7JCmVaraG54urrB+RGc3R4AYC8V8Kgwa+c/iRfvvjbTEzOwyLkf/neX+TWxmHl37jsLw8eVnMVZTHjyP2un/E43+Dt+Jjfmtxhst8hWZ+TNZas7nh7xpvb+7x/9xX3koVCjxWHiw7/77Of5P5HO8QjZ0ugiiXz+3/12nf5iWSGRhOpJpEK6AezZTuYAHRld6ChKANniWAN7h9ESvMvXf8Bf238DdhrOKBPO+DOhpZyGvLXvvstdDvHWsXTzy5Ap4BSkXfd4hechVBGhHm1qTQN/IZX2XSAr2PJL30wVVkV/LFB5RqVuXt0sNAEc0WQV/dDs7Q+kXuTzqv7W1BtprTcZw4Tu76IJtVzfnz8SB3CvpCk3Gaz6ZlAgC9gxLdM2BR1Nk8dlBEWizHO51CAGwECpHCuB4LUZcuwLC5eNuaXYlUKZQFbRJIprASRR0nRKVLcKIp8YAMs01Xr0kLxUxRWkxTWGxsbnjkn7TAYDHxBLK/xMgNQzv1lFmAdfBRm28tsxfpj5TqkSKzL3OS96sVkncFUB7/qIKOAOnWvRwFa5HcC0Eiy5mAw4Ctf+QphGLKysuLHkPSbyGj39/eZTqfcvHmTxWLh21KAYmEJyViSxOW6FK/VatFut32wSZ0hKT6N8n4iPQfOgWoiRQb8mJDx93K7yfnLa0g/11lmAtCIl6IAf1JICyB4dnbG5uYm8/mcJ0+eeMCk1+t5KbiE2wgDsdlssrGxwZMnT5jP5/41BSQRILCeviyAjQCSWjuPwVu3bvHlL3+ZbrfLeDw+B9zU516/3/eyTgHbBXgXMKnT6XgAU8bjy+zEJ0+e0Gw22d7e9iBDo9HwibIyl8UzTc5HxqDI6eV8y7Kk3+/TaDS8PFt82kajEd1ul263y/HxsR+D1lreeustTk9PuX37Nt/+9rcZj8dsbGz4deTk5MT3rQC6WZb5YKAPPvjgXJpyHciQNq/7RQqALp6Ekrhen6Pyv75pIICejNE6MCjjTUBsma+j0Yg0Tb3XX6fTodVq+WCnOiO2ft6yPp+cnHiPxIODAw8e130cZY0W5qsAPwAbGxv0+30ePHjgmWlyPS9Lp2V+CYAomwv1jR0BhpMkYWtrC2udn6XM8boHp8wXYTLW7zlaay/9bTQanJyccOvWLRqNhk+B7nQ6bGxs+DVIwptevHjBO++84+fl1772Nba2ttjb2+Pk5OTc+9TXbwFLBdyaTCZcv36dV155hSiKODk58cFPIhvf3t4myzI2Njb8666vr9NsNun1euzv77O3twcsQcIrV654KwgZqzIm5Oc6mC/Pq4PHcRyzv7/PxYsXuXTpEoeHLlBSvEzLsvRMQmEL1uX3stkjr3f//v1z9+v6vay+GVMPxpExIN/L/KqzUIWNK5tKskEQhqH/TCKbJ3K/+Oc5fvgBxOoDu8iBlHFFoM4gHjg5s6kYh6p0oJ0ylqAKOikb7sN9WAE/ZaJYBI715YpWYfXhi2kTqaVPYKhc6EnFBAwWkK46UC+ovPeynpM2m3Dpt5h3nV+fBJeYsPJOzC1lrBAJnLAM/XmU7tpUvLzWIHXX4cJGDPNrJSrV2E5BEBvKaUi2UTJZhJQVYykaaZpHDqh0HmoOLBD5tjKKIHeyW12xyARQVFjvO6dK6/zSYhdcI31gAsdc8mBi9b3VSz8yE7IELCuGmYCOsAREVeXzB8s+cL+3/nlY65/rGJK2+l21I5ppsn5MOLecvKnpqIRZEROpktQud4uflR0+G23Ra6QssujcUNudrfDixP0vNkrKOKBogIl0FYaiCDLxy7SECzzzVRloHRl0bj3jUzwTPbsycmPRXWflHZi71wnHqZNq5u5cHcOqdKCsUu77RYaNI4qmA0vLxBX5tmFQoUXWaqUtZCJ7dYwZFzZTzaMY4sOQ+MyN//HNkmig/XjUOWQtSHJLGWuiVooxmmYrJWuFZCsdirZj/iYjQ1EFDgW56yth5mYdBwwBJDNnD2Cqsdb7XLEXXkDnio0j44CF9SZ6XmCSwIFNFfCWXe8QDwuCRUnZCrFaUWx0HBgQaYJp7iT0FeDjGqFq59wBs2k/ovlijto7pri1w4ufapH2Lcmpm2/zSyV6oWnuBajdJibAeT+mIXk3YhpZdKrYW2ujOzmNpkOD9WqGUpZOe0Erydjvr2AmEWjLYAMWmxHdx9X6oRXTy/DV/lP+xm//LJ1qDujSSbibJyWzjcDbHuRVCInVTkb9x266m1BmA1o69eO2rXIWNiTAOi9EE7OwEV294HTWRC+c1180Uo4hZcAkUAbGEdWs81illAmJ9/Yb5Q3CueJg3OGzfMul52JZD07RKBoK3ll9zFY0Yl2njhVpFGUVDpQM8D6LVsPiRok1arnWGGgcOuY1VhFOFc3TatfDVICwcZsPepFhg8AxoWX9qBioyljydkjad3NPUdvoKJ39gZNE2yX45yXMmtkk4edeu89bnT3+k/d+FibJUhqsIAgM16Mj7mfbyzEWOpBQ5QqbWAJleT0yJGHJRIGt2lMZxxQepg3WggUXgpCf6D7iHwWvU0aBYxBHlidPNtHTAF2xq8NJgIkshQ1Y2AKtFL89e43pYZuP8otV8IkDgq023sfSg4mxm1e2UNgs4N/7rX+ZrZ/9L/h317/Pt199jScfX3S2DwWgIZ3GvP/oCuFIV/c+B0h+cniBm51jbKckq4A3vdDYxGJbBV9pPia3hpKSFo4pOTYNoqgkLbRjeVbtqEpFoA2nZYt7TBlXY/VxvsHCRKz0poyeN/w4ROGSoVcz4ob78JNOErflM4wcwTSpwEar0Asooqp/jQPsRf4vgUnicagzRTB3YLHOAm8PIizpILNL9n21aZT18Btp4vHhQnwsOlfo1HmM6kz5jcQfHz9ahxRzwoYSz8G6f6H499XBsXohJocwH6SIkefUZXwiBa0DVMLwEd+1utywXkhKkSKeicIwk99JKIX8F5aHMCTEEw84V8BIIS3m+1I0DQYDz/wQ2ZaAfFJUijeTFOvCSBNwR5g6UlDLdUtBKqCEPF9AHUmynU6nvpAVWZ2wRKV9pG0BDxjI7+ps0vrv/jDG5MvAgzxegNVut4tSip2dHZ49e+bBH2HvWGvZ3t7m8PCQyWRCFEVcunSJjz/+2LNzNjY2ODg44PT0lCzLfLKzyG0F6BGgV8z8AQ82CNuqntJdB2lFCifMq2az6dl54g0o4IYArALUiRS2zi6Ta5NDCvizszMvm5fHJknimUnHx8f+fIRB1uv1/DgSn9H79+/z4MEDOp0OV69e9a/R6/U8O6rOJK1bB0hASh1IH4/HvHjxwgOpp6enPlVXgClhJY3HYy/rFGadAGICKAu4Jefwsu+Y+ACWZcmHH37I1772NYIgYHd3168d0gdnZ2eegSpAaq/X820oAJHIdCWcKQxDxuOxD+oZjUZsbW3xta99jUePHjGfz+l2u3zxi1/k8PCQOI75/PPP/ViXtpZzkLEmYOHVq1dJ05THjx97BqTMIwFU6oCtbDDUZbJKKTqdju+vOkAvfS5rXZ15KeND5qmskdPplMFgwPb2tg+SqTNKhQFWly/LnJV5LIm+0+mUyWTCq6++6udenZEs5yBr6h/GvFxbW2MymXB8fHxug6POIH7ZRkHAyNXVVb74xS/SaDR49uwZ77//vv+7AETj8di/Rn1sCXgl7SkhKmJlYK31DFnxYZVxJP588/ncewgKWC5rnFxzGIZcvHiR733vez6gqJ6eXb8mWVelL548ecKNGzd4/fXXOTo64utf/zq//uu/znQ69fcqCXaSjS6tXfDS8fExT58+9f0oa4lSirW1NQ4ODtjd3T23JtfvXbKhUGesFkXBm2++6VOTwXmNyqbMdDr1925h/soYTpLEe0LWNy/EDqQOGMsYqYOVwigUVrP8XQDPxWLhN17qmzj1JPF638v4FWl4u932a8g/z/HDDyDigBhdWJKhJRlUDD8NyciBN1l7WaAK607ksGXDfajPui68AajScF2BW8aAgrzpmH8a99y8DUFeFfDKFQnRpAJkMiBzLEFVOo8wZopw5hiQqnTFugcApc+se28T49kv4bRiMCZ4v0NwBX6WVEErlW9e1rfOwL1UrrgFZ5ZvFCpTmNj6lGjxf7SxIuuzTCquAFEHajpATOTeVkPeWjKewkUlbQuX0nCA+aZifi0nGAWEU0XvIWAVw9eg98AFh5ShIpoZB/ZUCbNBJgwQvLRZAEZpY+wSMATl5dFKwEpVMUAELCuXP08vBCQjQ/7WjEBpFmVEV+UclYaLjSGR0vxgfo2Hh+v8sVfusz/o0YsdEJPbkvsvtkg+aDkp+sXcASBDd4LRzLhCW0Ha017aXMaKvKUoGy5UJ8jwkmrHYK2AtQxYOOahLhyjtmhq0q4bh3pRUPQaBOMU4ghBAyU8hbK6wSTOH80qWGxWQDSAsg4zs8qn6eocmo8rdl/o2laKY6tgtmOIBy6ttWhblzTqC+4KFI8cS9MUijwPMKXzydSZokxcmFFRBcGIp2MZuecFKXR2DWXswOa8pQlTSzg1lNsBm28dMpw2yT/rkpy5EBSdlRWj1VA2Asc6NpZwXoKx6MyQd8Kq713iLhbiYbFkwuYV8xDAGNCa9qMRejzH7Gyy/5MtZhcNNrQUK0DpGEhlp2C+ZbFz99x4dUFeauyLBuFUka0ZiA3B8wZm3nRAvYF0s2RYaMqek6qGo4CiV9K/OCJbDxiGPcK5ovfY0PilI37v7BrtZ24dUNb103wz5Oy2A3v7n9mK8evmStGy2IsLrjVPGJcNWjplv1hhJxxQVgOgoQoiVdJSBac2pKVTFjZkMmwSFG7+ZSsOQAsy5eX+ys8jha2l1RJYYlVytXXGJ1e3+YUrn/LNxq77E7CiEwpKtoIWf3H1XU5KxeNihd3higMoK8sI23ZzRudVqEQaoNuZXwOSU008sswvAKElOdGE0xKdFpg4IMgNqnT/bRBgm9HSo7UKMnIepIbFaoCJrWNtV5sXMld0rqrwi4odXQH4KMdGU8C/uvE75ASYaYTSbo1WhQMHe60FX4jPeG9+vWowIF8ya1WpKK2iqWLCoMRm2qfxClg5mLTYK7qs6TH/dPga+VnDvX5sHBMy1eh0+SHTaouNLf/25rdpqRiD4bRoQ2TIBgmqcBs66Aro1NW9QeS/i4DwLMRcWsBxQjBX/J3BV4hWf5fdkxWi8dJKAg0qqO4LsdtsM60S01DoIuDRdB2RfavctZ3KFVsXhmwHU0ARKffhuqBkYSKyLHTPCZQHN21o6SQZ/3j8Fu8PLvPoZI2y1GSzGFsqgkFINFeUbWqbARZ1EpPb2L0c7hwJLVZZLzXHOCDPLX5V35aVbcJcE6Ru3QpnS5DQsfHde3nLiQjKZsWKrWwrrLb+ccq4e6zOa30VqEq2jJ+34WypAPjx8aNzCDgkMsV2u+2DRqRwEHZfXWYrRaYw54Ig8Cwj8YnL89wzQ+RxIleuF2FSBMnv5L2kcBUGoTDQ6syTOjAmoEpd8isFTN3/rc6alOuVQlZeW8CodruNMcYHKuR57llcArqKVEuKIGF2SKEl5ya+Z/V2rEvF5ZwuXrzo22ttbY00Tel0Ot5v8Pd+7/fOFfoCiAkgUPcUlIJSQLB6oVkHGmDpAShFYP33Ii2+dOkS7XabBw8eAJyTnc1mMy5dusTx8TFvv/02R0dH7Ozs8N5773H79m263a734lssFqyvr3NycuLZblJUS/qnyOQFVJlMJt7HrA7KSCEvIKGAKzJ2pY+FDVtnitbZRQJeT6dTz5QTmaWMKSnem80m6+vrnv0kXl/SF1Ks11lqEjgjbSrnLyBuURSeWSmHgHN1EFjOQ1huAj41m03PqhN5tTzn5YCi+jyubxjUmavig1cHusABEXXmmkgpy7L0nnIPHz70SbSSODydTmm32+d83+qAQb2NZ7MZWZZ5sENAehkD/X6fo6Mjn74cRRFbW1u+bz766COfEC7vu76+TqvV8gCLtEe73WZ1dZXPP//c+zoKMP6yRFLAPWtd6riMW2utT32uyzBlntWBbhl7knD7xS9+0YPPAnYJ81dAVpkHYejCVOogoki7hbEqYM9isfDBLM1m04O1z58/9xsXsqbWwUdZY+ubDa1Wi1arxePHj/14q4/R7e1t4jhmMBgwGo38+cn6t7m56ftNLDHkPNvtNmtra/4+IUxkWWMFVJN7gqy/8vuyLH16/Orqqvf/lLkmm1TPnz/3Y/rmzZsopXjjjTcYDocMBgO/3sickqAeuY/IWtNoNM7ZYogs/ujoiCtXrvjQMRln4ut3eHjo21P8UAeDAc+ePUMpxWg08hsvErAkCdEynurrVH0DRe4rYRh6/0/xXxQGumweyIZAnSEv96yXN1bq71u/t4Rh6CXgMoelTWQDKk1T7yMp7Si+jGIVIZ6tsk4C3vNTzlPuPe12289N2RgSduQf5fiRABDBASm6sIRzx+DzDMGK8STMKhfuUO0sJg4cMz3HPGgcKcceDJ3cGQvhTPnkZaiAx9iBeaWqiveg8i2cWxYbyhfDIgsO547hFc4qllqVvqgKB8LpwrGxXKCEK2JtCErAwsixkEqWnmfuD06mHMwU0RTCqSI5cfSaogUmCHz4gYkc0zAaKnyQRaB8CIx4NZUN1z5hlTAMDijSuStCdelAxDKGRVOjrGtvkyxZnlZDeBJiGtYz66wGdWvCfNKlvQvpiqL3yF3g6HqDtKfpP8gIZyWz7Zi0r2gfGOJhQd4JmG0FRFNL49RgA5ivOaAsGZYVOOyAqnhsyLraJyHriqlWJpXnZGb5+vXHAOgKDXleNLnSOCVREZeiM8yzNv/iT7zHP/n8dbrxgl8eb/N/u/fzxHdbRCP3ekUnZ3YxIpy49jRVUErecrLSxqkhGZoKyFbee89qByqWyRJ4RVeFfgWkWOXJKwSpJZobSDN0EVcsqaA25nMHHoYB5AWmEVXyTOXGpjCpFA4A0i7wRC0cYFXGirTv3nex3fIhGSgwiQsMInT+YWoWOO++MaRrDqApY4VW1mWzWOUswgqnzCyTKo268vsqE8e2LRv4MWrDZZBMOIV0TbHyuSJILUlQkkRFJau1BJMUSkOx3WWxFqOsk4wXnYCiHVR2Aq6f09WYIDPV+wQUTU3jNEcZg9UamrELDarmhx7Psa0Geb9B73FJsAgcMzeCaASLjYCiYylbxgGxJWSTmKidoS7NMUArKYiCknDLcDZqYbKATn9GPm1gTmMm4widK4pOSXwSMBmtUjYMqm1pvVBML2r+jSvv85/+D79A31qMdpsaYn0QpFXCdXgejDAxvHXlBavhlJZOuRqdUqKcZBlA5eQ2oK9TZjYktyFtlWPQdFbmTE/cLklyBlhVBT65+SFpsz5oRDAR61Kd/+Lmb/BO9wG9YMHzosmJabObr/FTzQe8ESW8nxX8Wx/8eTY6U7618YAsCwmmgWO5DivGc7VZkrcVqlm48WmgaGkaR5Z0TVG2SlSm6D0r0bm7uZWNgHBWOAAxLyFQlI0qYCQ3oKq0eWMxoWaxpr2XoyS5KwM2sqy+ekq/Oefp8SrFXsuHZEhCvA6ch+GvnX2BYKqrDQD3ekZZXlk54WLYIRd6Y8XiJgCrHAMutyEFJSvJgv3ILNuy2uRJpzGfZdvcicd8cnYBGiVMXPqUWrgwDpS7t8RfHNCKc45Ouz4kZ2Zy7o4uoUKDGsWINNtJit2bSZiHCd3mGUAYlxQK8gs5/97mt3lSNInjkul66VKJC4XplujjGNMu3bo0U5gWEFiyswbvm8uQVnS62vr1ztYTLgUBkQoYmowUQ2ksx0WXPJVkrWrNMQraBc8PVvkbj76BahaEsaB4FrUIMYnFLJaenMKEVYsK7AyWf3MJ9VX/pY5RCEtAMJxVPpxZ1V9lZY1R9Z8EfqnSJaRnfUXetRWz2aByRbhwfsVB6gBKG7ogt6Lt1r2y6V4vnDgmo5N1V6C0DQiG/Pj4ETukYBDAaTKZ+EKt0+mgtfaBE1I0CogiLA4BYSTsYjqdeoZB3TesDlqIYb4UZyL3lEJZJJxxHHt5qhRJcq5ySKFRl3lKIV+XfEVR5JmGUkj1ej0vmxTgQs5TWErz+dy3C+BBBmFdSZsYY87JH+vBA3UAQIorYYcIQCvXKFJQSQsVcOHk5IQ333yTu3fvsrOz48GB9fV1RqMRURQxHo+91FMp5WWDwuKRdqsXinXvLPFTk7/X/49GI771rW8RhqEHEKVttdYcHBxw584drl27xle+8hU+/vhjAH7mZ36G9fV1fv/3f59PPvmEGzducHp6ytramu8rkbNeunSJjY0NptMp9+7d89K8uky5LjkXxifgi9hOp+OZhEo5z7QXL174grXO2hRQW8BwGTPr6+se0BOWjRzCBj0+Pubk5IRms8loNPIBKNK+wgyKoojJZHJOrryysuIlz3UpvQA5dWBc5k+9kBdwTZhhAvRJKIjMVwEwkiRhMBh4lpawcQVclr6vh1oIy1NAMJlbde9MmSvj8Zj19XXCMPSAyMbGhl8Der2eZ44Cfk0R/zrpq/l8zurqKleuXPE/l2XJycnJOaBUmJkSVtPpdPiFX/gFBoMBjUaDH/zgBx5QkTAfAZWlbYTlJaDn06dPSZLEA7PS1nWmWR0cnEwmvh+l7cQKQvqoPnbq3wsI8/DhQ/r9Pt1ul8uXL9PtdhmNRp5xde3aNW8BcXp6ShzHnnklUl7xQRTvRVmnJHxGZKtXr14lyzJOTpyXft2TUTZz6mNADmH9yTXX1zfxjmy1Wt5PV1h2o9HoHLvs7OyMJEk4PT09dx8RqboAwQLgvQzeyrWurKzQbDY5PT31Y90Yw9nZGTdu3KDf7/vgrK2trXNBHrIpdOnSJYbDoV8/wjDk9u3bnjEr4142EuSoA27iyynr040bN5jNZjQaDQ4ODhiPx97yIwgCLx8X1up0OmU6nXqfTmkXeZ8rV66QJAkHBwckSUK326XX67G5uek3Q14OwkqShB/84Ac0m03SNPWSeFmPer2eH8sy52F5L5f7Qd2WQRLvBbyTMVaWpb+PC4ArcmrZKJT7t9wnBMQUNqiMO3AWE8Iyl1AzGY8i8xdmvthXiEz8j3L8SACIqlzKiYuGAyPy9lIe5JkEpSu2s6r4DiqVnwu6UD5JV2WuKDARNQP0CggLHdhiAks8rdiCecUmqJiALhxgCV5FE/c+uoCycIVJ49Q6H0PrgMwgX7IfxR/RpzSqZVGiC8gqf0VlFEXLgTzhQlM0LemqSGYdAy4eOiaYKiowVGScFp8A6ZgQjtWkc5asE6r2MJYgNVUYhqZoBBRtRVGBjeKRlXcsjVNF87CSN77mfB115oqpdBpj25bGWUljAMEswzQi4okhWwm8N6MunF9g89jJkXXmPNHKSBEsyopV5ADLznOXQJythOQtTTxy4GQZO2Cufj3R2PkW/tL6D9x1q5JAWfbLFa5Ep0Qq4GG2SXKqeC06wVrF99+7yd3ha4QVgzTvVW2Xa5IzS/PY+NAZq13Qzvi6YX5B0X0ckLddmmyQO69LcM+Px45l6IJDbOWZWP1ZO4Ax6ymiCcSDApRCzXNUXmCbsQNMQu3AQ1P9txaThA58aUKZLGWYtlToqPQgn5KxVn2GE59CJ8HGMzitBkJDkJRwGpGcWaaXFLPLJRsfGNJ+QBQXFFmAUhatjQOrY4tOFNHUYEJdMXUhnlovXTYVfiBzwCWkW+abiuTMcjpzMpAoB7Rysm1rKSPns1aG2oEzCrJeQOOspGg66X3W1ejCjQPvS9oKoBXQODSoUYqNQlRRAU2BpuwmnNxpkK66+ReNnDx/seHmyMoD57NaNBVZT6Fzjc4iTOzAmLwDw80K0G65r7NZgsk1dAtU5UcZx6VjSY4jx24MIJ5Y5r844PPZFu1d7cGtIHX9oQys3DfeAiHta992Ra/kp9Zc8dHQOSWKtsrICIgpWdiImJKoCj3JbMCJabEdTLi8MuRe2PNgpCrce4ZzSPPArwH1FF9l3Jjo6jn/7oN/md1vX2F+PeMbbzzgYmPIdjJkrUqLiJRhrT3ji/1dvt5+wD/s3WZ/PYbDmKwPaeBAUXnfxv0GWT9GlSV5KyDIYLTpQJjGkaZxOKvYhhrEy7UwqHmGbTc8IKyMxUbahxJlKyFZv8KrvL+dY6rp9YzJPGE8SzClpn1j6PrtLMEqNz6CsKStMr5/esX7I9qgklCHljvdymcFJ431QTMLFwJUtiz9YEaiIn5x6xM+39+kzDSEFmqej48XG9C5z1trLzgZt3n9led8+PwS+lnDyeYjxyJPDzuMjUK1Cv7q4R/n/7z93/Mfn3yT77930/kDtkuSF5EDOCNVA9zc2mAaLknZaIs6aEJo+drrj9gKWvzd6WXm09glDAduI0stNL2bA6azhO3rI/aO+6jTKnk81eTzyAfQ+LTq1Yxvdj/n1BTslwH3s6u8P73Kg8kGjwdr2ElVOGiLKl3AUJkGdLbHTGwTW2hyaRuRY4eWvGuq+5uTJfu+iNw6rDL3WgLwqVw2wVgm3ZfWhQMpv0S6jbBqk7FsuPeygQO4nUevSJorcFi5zwR515L1lkzNsmmcFLpQ2MQQDkP33NRZjKjVDLsI3UaGrU2sHx8/EoewS/r9vgc0BAgTsEDkQuJTJoWmSJLq4IMUIC9Lk+seYrBkfiVJco71UWcxCBNSCpJ6YSPMKXlf+bsUNvKacm4CXAibq9FoeIDUWmfS32q1PCtK2BMi9xR2Fixl3/KedTaHFJgCUEiBK/5P0h6AZ0+VZemLtW63y2w24/j4+ByLRwCmVqvlGYrSP51OxzOmhK04n8+9F5ewROTcpM+FmTkYDHzCMbgC9/Dw0Bd4gC+WRSIoASF1j7bT01NfJMdxzNWrV31x/Pf//t/n0aNHntkmCcDCIhGmjLRvv9/3DJrZbOYBtHpbi0+iXIeAAVLYCnsLXEEu7MjBYHCOYSmHAJl1tmDdu7LOGoSl756Mvzq7sM7ikcd0Oh2yLOPSpUteLizgcp0BK30u7SrjVeZW3StP2IPCfixLl8o8HA49sCjsovq5jcfjc36XMlblegUwlzYRICgIAhqNxrk5J2Nkc3OT7e1tDwgKI08e3263z52HvP7Ozo5nHYuNgQAGMjelfcQXTSnn6ae1Zm1tjbW1NTqdjk/AlnkuAOr/j70/C7YsS+/7sN9aezzzufPNm0NlZlVmzd3VE8DuBhoDAYIhYiAlgCYp0RTDtAbLERYZdCj04rDCsl4Uoi2QNhUOSxwhilDIpEiAACmw0Wig0Y2urq7umoesnG/eeTrzntbyw9rfuvsW6SD8iGbviIzMvPecffZe0z7ff/0HSXBuSvKFWbmxscHp6akHewVEkuPjlgDy7+PjY7a2thgMBt6TUDxSmzYFMm6aa58wCX/sx37MA7tHR0fs7u56IE3GZFPmL96he3t7HgwWD9GjoyOWl5fJ85yDgwPyPL8Aum9tbfHw4UMvfZafy1wQ8FzaSFirURSxurrqATFpmzAMPUNaNpAknGRpacmz2MEB4uJlKh588gwYDoeUZek3TOTncl2yfpdlycHBAZubmwyHQx49enRhTZCE88PDQ5555hnPHl9dXeXDDz/0YTllWXovzIcPH/Lyyy/z4osv8ulPf5o7d+4QBAHXrl3j5OSEk5MTP4elPWScCDNdxkWSJLz//vtcuXKFR48eeS+/tbU1BoMBt2/fJooijo+P6fV6fOUrXyGOY28lIJtPMubX1tbY29tjOBx6hqesPzs7O4zHY782NxnsAmCLhFie5zKnmqBrc4zLBkrTJ1deJ2urrIUCFErfylomz30ZEzKXhaUs52632x7QlYAswLPTZR61220vg0/TlMFg4FPme70eBwcH//oAiMrA6Kai6DuDfRNaz4LTBa6grGXL4VQRjQEFeYQvigXoE6ApnLnfhdO6qK7w0s0qgnDi/A2VOfcNExCtte8KvGhag5p1QVu2HZsormXVylrKjiYauYRacNfpgkKUA3FqZg7Uno5nTgqbntUMRwVlR1MulWTLNSupUxF0S4KoJDtNyVWASRzTShcByak7lwNca4/FOsgjqJyUVqTeVp9LS7OeSxmG85AWkV4XHVfYVh1DNQ1ciA1QXlqQHKeEma39DhXFwNDadTtlKqsIz2b0DgM69xOUtZg0JB6VpMcu3dUxSIWR5FhHujCEc3ddVUsTzqo62AWKrq6ByPo+KutBhHBhmWxpbsV7QMyicje0MBG9wE2ue/M1lIX/0+OfIXm7RevAhZtkS5APHRgcjS3TSrNYVsQj8fRz19nZqVh6r8DEmirVLIZBzYZ0oEbRdaCWC9hRNUsPdO4SOsWQHwPxqftZOK8oNgfsfr7F+msZ8cHUAYaVA0m9lM9aynbgw39sbBxAAajAYq1yASqVk6ha7dokr4HWaFJig+gcLGrUtqbUkBoOfgAHGiw0QWYwQYgxzh9Ra0tZhC7dtp5LVaqI5pZo5sa5CVxISjy2vnAv5+feYrp0ieHR3HI8SdHa0rKATw2vdxsVhAuDzmzNNAxrn07l+1tSTvOequXxLuiH0qDyhllsGIDWmCSgbEE+cCzXYFlhsVQ9w0JbZldqMKhTEQxyqtOY1nZIPMKHxvQ/dEzZKnEs2KIrLEC5ZreOJLU9Qjx2zGRlLEUR8ptffoWlXZkvrq1y7TxaxU+z6Gi/tpjYtUtb57ySPmCoMx+SgoWcgMvBhIUNqKyirUs21ZiDqsPUhmRVSDhzfqhlqqhatS9sy3qmJTiARhV1uwagGgEhRdeyuXXCH1/7NrGqyG3AatDyv2+FBf1wQU/PHes3d7JkO66Z1TVwU6ZufoVzDVT1poTCtNxF9O8ZF5hiLeUgQeeOeaiKqgaBFSbQ3i/V1AEqQVaxWNJUac04rVEjE0G45h6U/+5z3+D10VW+9Y3bzC5BNY3O5dr12B6blJ0T57dkE4utE5pVWvGFjvOfLGyAyrQLBSl1zVYDIsta6DyTPtl6UIeEqDrluQakgFHZwgBX0xMWpykfqDXsQeITjVXNotMnoWOmlor/5b3n+d+sfZVfffAiwVxjlhf8+K0P+PL0ZT+HbWixrQo1DxyT0eIlzcq46xhEri32C3ePqlKoTLH8wiFaWf7jp/85ALvlgF8qfoCTncQx9Yc5OrCYSeRAR+sAyuFgxj8/fYG//eTznCxajOcp7SRnPEsJw+oC+EdoqWp58fhJzz/3hEUowSaqVDRTkDEOKAwWLgVZrD9dgve5rzAiRZYpvxAPVii7brPFgX61J+ys7ngl65j1bVnWLETxsi17FdFSRhA4dnI1i2DhxkF7bUZ22j/fPFLw4rUd/tyl3+Uvf+0XaO0l5/f6/eN74hAASpgBwnwRDzLAf+EXwCFJEu+FBHigRQIywLH0xLuuyRALgsCDRnAuL5PPlDTfoig8y0NYKcJ+aLIumtLlZkqjgI3y2VLgCaAh/+92u5yenvriSvzvPi43bHrAiWTt42zIJjNOCs52u83y8jIrKysEQcD+/r7/vCYrQ0CrZlG3WCw8Wy2OY/r9Pv1+n6WlJc8AzLKM0Wjk2176aWtry/vbNUEfKehEXttMlZbPFploM30zjmNWV1e5ceMG77zzjg8maAJgaZrSarWYTCY8evSIoij4xje+wWQyIc9zz87RWns/N5FnpmnK9evXuXXrFg8ePOCDDz5gOp364lQAYDhn902nUz+GpDCVIlY+TwroV155hWvXrrG9vc2Xv/xlX0g3gZ5mmES73fZppU3WmIxj6Wd5vaS/Sn823yPj4cmTJ95f7Omnn/bAeNM3VFiwk8nEM7OaPoAC8khghLCGxb9T+lXGqBT/cB56IGNVQDyZ02IjINcjY0CYWAIItlotL+mXNpG5XlUVh4eHLBYL3y+np6de8isAv7B/myw68ToT70gB6SS8QyS/khQt55S58/Wvf52bN296YEY2LgS4kzkuwVByzXEcs76+ztWrVz2AIX8A79fYHOdNGwUBfcVnUeaRtHvT/7S5ESKbI3fu3GF9fd3Lf8WPrnkt0tdwDgKfnp6yWCy8r10Yhh4IlTVmbW2N09NTVlZWiKKIJ0+eXFh3Ph7GJOdvettFUeTTg2XTYmlpyW92rKys8PLLL7NYLPjWt751AaSVMSdzXsKnZG6ITYN4NH7cg1HuVdrk4ODAM1qbDEpZl2Vt63Q6bGxsEIYhBwcHVFXl2YJ5nnP//n263S4HBwe89NJLrK+vM5lMuH//Puvr6x58bfaXgPrSDwJSyzOvuXkkG0/SNuLzKSzxq1ev+jaS+xBJtMh5hcXf7/c9m1TYo+I7K+u2bE4dHx/7Z1MTDG/Ofdng6/f7Xkosm0VyD7L5IdYmwm6WtU/ClWQMieRY+k5AbdmsGgwGfi2S9hwOh35dk3ktGz9RFHlZfqfT8Uzvzc1NzzKNoogHDx749fFfdfyBBxABip4r+I11X/IltEJZnAcgbuc/OQkI55ZoajGBJlyAqlOJReKry3Mw0EmKa6lz6iR3QQbR1NaFqSWc1wnLta9ilSjKjgPnwqn1XlvRVKScNauo5ZhpZVt5hiN1URKP3H2YsH5dA1SSzxcvxPgdyAcRVeyu00QBVWipssCZy1tFe9uBcap01wQO2JEQgXBhPUMTHLAhKdUoxWLVUnQtOlckp+66JeVZrj2aKNLDAJPU8unQecUFczh4xb2PQmE7FWUvpuiGKGNJ90JUXlJ1Y1RpCA/HhLsl6T1NNeigMyfRDRc9rFIEs4KqExFNDWVL2CDONzDMLFn/3G/Q2nMGYpC730+eLXgmqn1LjKaqpZhVpflHU80/+/A5Ugtv/NpzpCf2nHkaQHLi2mi+5orkou9ACBNqzwrr/ZFddr6zwdI7Ndi8cGBYkDm/x2hWP1AKl7BbxYqiVadX21pm33VflKKxJZhYgrMF2VaX+ZplfDVmdXeCabv2osIBYGWFbadUqWPq5D1BDhzYo7TFlu5DlHb+XLpwkmsTOl9KKluHwth/oai1hUbVhTuVqiWBxjEHlUUpSxRWLGYxUaGo2haTQNHSHqwW4NklljrP0qLlJN8A8ZllcLfynogcJlTDok5Yd0wgG4c1E1FAV9eerYMcq+pwjhKiqaHo6prhKXLEmvCj3bhGK8fgLEqwbh0Y3KsIsoCi40DNeGSZXKnly6lBG43OtGOgrs6xa7CwLkQjDivyMuBslKIUhGlBmYXEDxLvebpYO2e/gQPpk1PL3pcqNjpzyg+7HpyIpwYstA8NOndAaRW7sdZ75ADS+YomWIT8lW/+BO3+gjAwJFFJL8lYSV0xcLV1Qj9cEKmKQTijp+eMTYuVYMKjwyEWmF5yIJ7O6zXGKEwdouIGrPVp7yjQkaFXL0JVatloT7gcnhCpiiflEpEKqOrFVytLO8hIVUmgLCQV2VLoPGJroEcXDuwZ36xQlSKchgQ5zLYc+BONNJ3dDKwbpybSRLPasHiRY1sJJg7RlUHV0nVlhQVu6zlVt72FsmPYuH7M/mGftJ3zpwev8/fvfRq9NXds2lmATUxN1XX9+3Z2mfyg7YA3cGZ7laXbn/NCPObMaD6ardYBURYC4xlxwjCsrOFqOCWMKgrDOQNNgc00X3n0DM+2d/nbb/8g8V6I/qhPEsmGj63999x12Y7zCviJZ9/nM3FAEpWML2VwFvPPv/GyZ+xhcZsJOADShBBtzCkOW47BW8t+f/OjW/zj5e+yn/cwpSao2Xo/vPkRf3b569yOFJEKgEN+o/s8Z+UKlVEEkaGcRueJxZGFwHK60+efnz5LlJZUZYC1kGchxSgGUzPXY7cRYoNztjRB3S4189XLlEuFKhS6cAqBcObWd52fv9XZjlg/16tE+bChqi1rr8JELoBLZ4pg7iTG2oekuWd6ldrzYLbAjSMHvDrWb9DPsUYRBYZOO6OoAqKgotdesNkds5GO+fIHt90wkCC2UvFTa2+zEkyg+D5y+L14RFHEaDTi8PDQe/oBvlAUMEUSd09OTnwBIQBDU94oEkopuKWAFzaIFPXCmBCgsOmrJdIrYcMIcCIsNAEFi6Lw4KW8TinlPfIEmFRK+QJHpK1iaJ+mKdeuXePs7MyzwgQ8KYrCg4BSoApoJYeAhnKvTfBIPMfiOObo6MizQAAP9DXBVfkMkUoKmNrtdun1eh6IaLfbvo2EySEys+l0yt7enjeq73Q6dLtdOp2ON+9vt9u+cBSgR5gr1lovpZPrEWlwp9Ph6aef5p/+03/q798Yw8rKCkVR8AM/8AOMRiPeeOMNz/IRH7wmk0wKb2HMCcD41FNPYYzh5s2bHqyQtE3x2ZJ7FQapjAMBfKWPmmxVASaOj489k0sANgGARbLbTP6WAB055HzCBpRxJwEwTc+xj4dKlGXJkydPPIAujFE4TyQGLjBLBcBqFuVy3U0AWACAPM/Z29vzAOrm5qYHjAUQEBZVE0CSeSLnkT/iO9i8FxmDzbEv1y3edwJyy9yXNUEAL0kzf/z4MYPBwIPfZVnS7XbZ2Njw64kkKEtbrKys+ORbAYu73S4vvPAC+/v7fPe732V7e9vfUxPAFbaTSCIHg4EPbFlaWuLll1++AOjLZzbT2KUfmknBAtpIP4r1QtMPToBH+bn8X8aLMcYD9wIASd/L64V1K+vYYDDwCdsi4xYmp6xvYRgyn8957rnnODg44OTkxIdfydrdBOj+ZaFP0gcyXoPAJdULULyyskKapnQ6HbQ+T7qWdVHaTGvtmdWy7iVJwmAwYHd317eFPFdk7kjfNcGqwWDgfXZlfRFwShKVHz58yPHxsWcmrq+v+3aW1y8vL3vwv6oqzs7OWCwWdDod9vb2LvhDSh9sbGwQBC5YS851cnLCRx99xNNPP+0Tk611oVLXr1/njTfe8Oe7fv26tx+Q+S+yZvmM+XzO/fv3/caJMKOln4wxnsEu7dJMYpZ1VvpSnhVN5m/zOS/zT8aa3LdIp2UsymcL2Cfgslzf6empnwPCjBZGoWwCysaT/F++c6ysrHDlyhWfTH18fMwHH3zAwcGBf1YKCCwblL9f/0P4HgEQda6oQlc0BHPlizVVKkeyiC30C2abms62IhIQLYGwciwf8WnTJSSnDliJppaipyhqCWGVQHvP1JZMLnW4CM6lqVXqZL2qTkl1DDgnTyxaiqot7KkasDO1mFTVIGTlGIFhVu/+tpzsSRdOhqasAyxViU81DjJLMnIsnmyg0JUmn0eUA0Mw0YQL1x7htJYpRw4YdOy+WqbdUB24EJPaay2BeS0fDGcuATUfWu8f1QwjMJG75/TIEp/B7JKT1ykTEp+516pckyzPwEbOE1A5zzLbjRlfTYjmxqV05iVVK2L8VEr/3pxo+5jwJCRf7WADTfLgmDiJaO0moBR6lhOGmvg4ZHalzTgJXJBCDZCq6hxwvXz1iIGupbFBRWYDfnnns+yOe5w8GdD7IPRJ3VY5hoowWKoUsiXrZGyTkHCqiMdVDUArWscV5d9YZzm0pKcVRSdwXmMRlGlANpCwmLoQtU5aF2bnQEcVO0CpqpM8dWUh1ARZxc1/UKCzEhsFqNJ5+SlVg2DWYiN331aJV2b9pSQ0NWCh0Dsp5bAkoi5qa3A6yC1FL/IeiEZIbEFN+qs98IJxQGtf1R6h1gHc2oG1RsAQ7RhsOnP3EGYWMgdM5X33szJVvvAvWy5caLGsSE4CTATJiSU+UWRh6JKrSycVt8ZtFtjamy4b1gmQ48oBN4EizA2qNA6AjZ2U2Wrl36NKgw0D147CbAw0JtLMl12idnLqQNj5miI9gmAHqth592EVZjulilOq1BJNFdmSZb6RE8QVOjLYUjPoLqjairyXYSxUlWbYWXA2aZGfJVApwrOAMlH83Gde51fef4l+dA4gW60pWw50DhdO5pkNtGOxJg5srl6a8KM3PiRShlGZkJuQWRmTlSEH8y6TLOHDozUA8jKgqIMrqjKg35tjH3Zo79YBUsoxp5WxZEPN5E6P1oFbP9Jtx8iLR26sTA8Tfvn0c+yOeq7dUZyaNit6SlvVX4qUJsCisWxFp6SqIglLyDXpUe2dWls0mLAG6mNLvK+9lUOVOhCutecAawDTcpsPwbxw7ENrMTWwTOUSmW0UYCJNOC0xsWa+binbjsGmDKhCc3DUo93NWMxjvjy7yWwRU4xiVKuic23E9HHvHDQFvnbyDHpeW10IgdVCO3b/CVBsJGNMWj+AFY4lbAGjOK3a7Ff7HFRODhFMAx+A5eSwimmckujCSYI3Sqo08AEglz+9w5OjAXYUQ2SdVLpUfK5/D4Plz17/Jn/l8R8hPtVuUy3BSeRraS+lA9/KTsmzm/u8eXLNXWehILBU85C/9Lt/kuHSFArHOA8q+PHBO7ySJBS24t2i4MN8nSejvnuuBpaq0Ki89pcM6jU3cwEyNo8ozpz3p40spbCgQ4uNHHiorGP6NkFDVSnvv+hSyJWXIOsKgvl5wr14D5rAMWjzQR1uErl1VWf1Gjevmch1yJN/BoZgOu7zBaQ1ddAMQNU1hONaKlVB0TaE/ZyN5RHXeie81HvClfiIp+N9XogWLAWuf399lvAbxfOQuPYP6gCcv/Ltn8AsAtLHzv6A7wepfE8dwk5oyosFpAB80S1/93o9L2OTglnYDs2AASk+W62WZ9qIv5j4dQkL7fj42BceAsBJISSFtPxMwKKPSwOl2BEmZVOOLe8TBp7cY1VV3L9/n52dHQ/2CcAwm808Q+nk5MS/X8BQASGCIPBFpLRXk9EmLDYpsKRwE283AQMEVAuCgKWlJba2trwZ/3g8Jk1TVldXPZNGAMv5fO4LP8CDNUq5RNhWq8Xp6akv/CQNdDabecBQ5NWtVssz3pogEZyHlQRBwL179zyAl+c5L7/8Mru7uwB87Wtf4+7dux7EE8BKgAoBETY2NgA88PDcc89x+/ZtDg8P2dnZ8RJMKUQFlBFGjIDdAtA0A0REEi0+f0dHR7z++utsbm56fznpyyaTTAJUmnJ3Yb31+30uXbrE0tKSl5zKPQmDJk1TDyZJu8kYFRmosG4kKTYMQ8+UFDAgTVPvvSjgVROUbILo8jmSQry0tESWZZ7NJv6Jcr0Cssu1ibeZAHZS1Dc9SGXMSBKwrBdNBnCT+SvyeZkD8tlNpmtVucAVAXQE1BawQQC55rxQSrG7u+sZf2VZsrS05P1C19fX+e3f/m1ms5ln8wrAK3L2Xq9HVbm04evXr/P88897VlwTcGxK0mXeNO0RhE3XDP6Rex6Px15GKpJ5mQviA1kUBZ/61Ke851u73eaFF17g7OzMs8VkvAlzTUAguTZhhs7nc/+ZTdZcp9Px7ORut8ubb74JnPtbdrtdTk5OmM1m/8I6K+sUwOXLl/04kbEj7xGw6/T0lHa7zdraGpPJxIP74nUpISV7e3sAPrCk1+vR7XY9GNZM/JZNjmYidpIkPoFb2lGeOeKHJ2N/NBr559P6+jqXL1/m5OSE0WjkPW/X19eZz+fcuXOHT37ykzzzzDMXwD65b9lI0VqzsrLC3t6eBx5lw+mb3/wmt27d8kAa4AHyqqq4evUqw+GQbrfr07EF4JPNsaZEWkA9AcnkudeUkcs8lk02GYfNNa0JFsrYEba/gPDyvJTnlDyzZGwJUCigs4DnzTVUNojk2ShzY2lpyc9zYR5ubGywtbXFysoK1lpOT085PDxkOp3S7/e9OiBNU78xYYzh0aNHXqr/3e9+17fz7+f4ngAQfRERnX/pt6FjSxnt5MbVcUw4r5Nxa1aSrlNks7T2YKtZgJI6XHQdIKhLx0ZyDD8nwyw6MN80mMTQvRc61qBxABrqHCRysroaIDTWSVfBFXShA7ckBMSxjKBMHDujSlwhJEbzqnLXIX6FaPE7dBJRZZwvm1XKMXTGqgYEOA+zqMNkyrYDwpJTvM+jyOSC3MnAF11XxMenrk1N7MDYIFMU3XPWoq6cV19y4q5Dl5bxdY2ti+do4ph480yx0psCQ5farCHshKjKBYWYQFG1Q2ysyXsReU+RL8WgVii6IdNLEelpgC57mFAzvZJiA0X/fYOeZdhOTN7VhAtb+0K6PnB+kS584ovr98hswVu55aODVf782Z9j8u4S4VzRnUM0ccEvNnAMvWhqma9qF1YR1UEWtRxusWYYXw4JCsvJC5Yv/qH3+M7/+BLx2HJ4NaL3wNDaL9wYiDTxWPl2lrRmCfWxgWKxQh2244rc5NQSzhw4CBBMcxf8ESgHHhrnz0agwTjQxIVDKMquK+6DVkWVa8zjlN6eon+/4smXAs8CdLI8RbAwVC19nmRtccCFtthSoUJLchDQeVwHDOH8KYuuG8uqBittpTCB9YCjiaB1bFwydegYfapmDQa5Y/3FZ3VBb6GzZ1gM3biPTx27M8gcOEhRoggdGKjOpbxlqoDAz4sqcfcRLCqCzKCqgLKtMUHtU1pUqLKCKvQhGdTTbLFStx1urNvAMrts0ZkmyC35aolKK4K9hPjEySan10rik4D0rcRJcuuAnJPDCJNYx2SzQGiZaEs7zYnjkvk8ZvCtFkefrRiEc5I32z7JPJq5vtGV9SCTZ1Olbj1ZbJT8ydtv8MXuB6SqINUFqSq852FP50xtSIwhqvWb98slKqsZBu5LyP+l+zN8lFwmPdBuraicr1yVQDmssEd1MmNeb27MHZjZfaj5pVf/EOnjiO4ZvFNe5//w+E8TxSW99oL/ae0xZ0WLu6crHDxY4t7JMt00Y+f9dVr7DqXOB+6+wpmbd0EG02uO2du/61iRVjvQr/ukpmwqhUkCTm7HmOdj1r8zJxiFVP2YohM6pm/kwGATKnReMbqRkq+6QBDpZ12CGcVMs4D20pzKav7Pn/wV/ofdH+DDw1Wmo9QDWVY7hu2b+5fcJlUNZulMkS8ZfvbKm3RVREHFcd5Bz7XbDMoU4aQOpelaHuRrXI+OGeqMy0tn3H3cdeMjrgE+BWFS8XS8xyeffsR3379Wy9QNqlSczVOKM5eubBH0C/7uwx/kDz/3Ac8mT6D2SNSFghyqjiFeXji2YVwzKmu5NJFxLDhhVOYam2tOqp4DPnHr0D86/hS/+GCFRydDFtMYHRkn8U4cG5mzCKxy2SPaen9AG9RMZltL7S1+XVCVwtYbcZSKcKp9m8ozUkuX1wxVZ1FAva44v9myVTMGE7cjoyvHJowmbr03oQsyKTtuXQ8Wjn2oC9c3VWIpe+7azKBw92Nw4P4kcEz6pKKIDHoaoHMXxvLvvPhN/sPlV4lQFFgW1nK36PNfHf0g3zq+xt29VcdkDQ3ilRjOHIhZAWoeeADz+8f33iEFflPGBOesuPl87iVYwoISQElAMQklkeK8mbIo4ETTe0nYIyKFFbmtMBOEjSEsPGHENIE6AVyaslHxKxTGlBRlUgAJu0LOIQbwUlBJkd5qtVBK+cTZJtNFQKqm7FSOpiy23W5fSCSF82AFATtFdirF2cnJiU+UzfOc/f19dnZ2iKKIl156ySfsCkAhAS/C6mmGaRwfH7O5ucnJyQm9Xg+llGd7SdHZlGCurKz44rRpmg+ucH/++ed58uQJa2trvPDCC15K++GHH/LOO+9greXo6MjLUaWYDYLAS/qstaytrZGmqQ+3MMZw69YtVldXPZvls5/9LPP5nNdee429vT0v6xWZtFy/jCH5A+esmibba29vz4NETRBa/si4n8/nbG1t+T5eXV3lypUrLC8ve5+1wWBwQZ4q4zyKIi8Dbp5XWILr6+scHBxQlqUPzRDJssxDOAdxBJRsptNKu8qYDAKXTiqgtoASSZJwdnbmGZhNYEKAGrlnAWObbFgB+4W9KG3W9KWT8S7jXzYEDg4OLsgSu90uKysrdLtdz3hcW1vz4LL4b4rsXQAzYZPJHBfATgDkF154gfl8zic+8Qn29/fZ3d3lwYMHfjw3WZXCYp1MJsznc89ivXv3Lr/3e7/n21uAE2nDdrvtN0JkfZL2Eel6t9vl/v37GGMYDAYXpKGyCdEEfIwxHBwccHh4SFmWnm0r4LWMnybzWNhjslbJOIjjmMFg4NnSEjAiAM9iseDq1aucnZ150FpYnr1ej5WVFba3ty+Elsj6b60LB1lZWfF+gwISHx0dsbm5SavVYmNjw1te3Lhxw9+XtL0kYcscffrpp/3c3Nra8rL+jY0NDyzKppT0eZORORqNuH79up+HsvYJa1wYkVEUcXh46MeMPFdkjZbNH5kb4lPaTIEWxqtsLMmcBuj3++zs7Pj1/fT0lDfeeOOCX6kEuHzqU59iMplwcnLCgwcP2Nvb8xt1Aro1WZdN0DDPcw8wyutFgi/gn7AZZU1q2mnIWiYbJ7JeDAYDD7xKAJQA77JplaYpw+HwQpCJXF+n0yFJkgv+hrJ+dLtdbt26xfLyMg8fPuTk5MR/x9jY2OCll17i7bff5tGjRzz77LOsra2xvLzs7/n4+Jh3333X+2gKcD+fzzk+Pub+/fv+efr7Pb4nAEQbANoVJSI5qhKLVnUxYqDzSBNNrJcotw6t9xGrEpegbAPlUxirlgMBdOk8D7GQ1DLkeGpJxpb2nqJsBfV5rUuFzS15R3vw0IF2ynv52QCf2Os8F6XYcgVT3negXTi1HtQUmWZYsxOr2AGCytasyeL8/87rDzhW3ldOldTyTnfv0dgxCG0NbAozK1zU1xRAWAhTw51vvuZ+lxxpbOgk4fGJpvvQEhSGeGwIFsYF2bQ0xUpJcFRPwBo0CxaKTpQ71e3C+b7Z0DHDdOG8Cl3RD7owWB1StjTRSDmfO+sksVU7dMEcsXtvsZyiBgmjpxJmm8r5FhaWxYoiPXSAR1C4cJXLyQn/24d/mN9+41nSnYg8h7hWz1nlJOWShJwtKRbLyheyQQazS4YXP32f00WL3dc26ey7X66+rvm9yYskyn3O4hMzsk9A93faLiwlcZJ3ATN1aYnHBnXm+tdqReugDhYJFLU9I9G4dGb7ANaNMWtBVVXNsLSegVh1Yzf2Y0XZMRBZ2q+16D4x6MJgIsXkcoBplahKu3AFJePcksf1WKx9Q1XNmIl3IorhOatKFw4ksNqlkoY1qygOnaRSGUUw03V6sKJoa8+qK1vKt4HIrJMzS2evouhoirZjCSUjBzoq65i3GAthgIlDn2htQkU0M5Rp4BPAUXWqcytAGQcqVpEkcIMStqZWTroc1A/22AHZ8ZkDXwSoWKzVmEJqMIlj0drYYK/MKZ8yFIuQdjejfa3gbJJSnKZEwwXL/RmHJz04itGTwIHSsWGx0yErFKZbkT6JiOaGn/nc6/xPH71CfEYNULk0eQng0XU6bJlqlwwdOnDzqaf3+VLvPaYmwaDpkGGsJqhNMQ+qDpEqOTZdtsIzZiaio3IiXdLTOcYqIl0RzN08yZZcOEyVuPUsWZ6THXexIUyfyVFZQLobECwU1Q+O+Aef+W/5D9/7M+x9uMpTz+/ypfU7nJUt5lXEHxm+zfuLS+RVwPGgy3pvQhKUPGlVoDTpiSHINFVUM7fbDrVuPwqJzyA9LZmvhqR7AemRJRqXVEng2IaBIjkxzDY1k8sJrUhjEl2HD7m/TVzv6m61WCxp0l2NztwaW9YBGcGhJpwHTJ4KefjMCp9p32NcJFRvDWBonL9tnUKdHwwJForuDPoPK7dpM6o4uR1z9qMt3sgD9qsh75xsOIlsbCkDS9E37rnUqfiRzvvcDGFhDV9YvcvdlVXsJILYQObGYTmKeWdxBa0cYCXPNV0oRveGkBrnBZhrBwQGlrN5ykHV4r988EfRpy5ESUjlqlVRFoFbK8w5s+4zw4e8FW3BJMQmBpUrB2QCveUpk4d9UBZdKb78lVeo2qYOBrKYk6S+R+MDU5QBcoWjNrvnsQ3teahN7lKIVe58TjF4z0LZzPPWIxJyUnBBirxYVtgIt0Gh3H2oygFz8VltuYHb6MqH1vuDOj9UBxpiHYinS8X8agGBRS0C53+oLWl/wcZgzDP9Qz7Xv8e/0fmAv/Tw5/jWa7ccIGoVNoRXT55iL+/z1vEldk96VGVAEFaOOWoVKjRsrJ+xuztEl07ZYEOIxorw/ZYLNqs3a75/fG8dzWJR/hZJURNgk+AJkUkJ601YOcIeBDxAJeCbsBokkVIAR0m6leJFmHzCLJNCSTy1BPD6eOiDfG4TgGlK85qAmRRtzRANAdHkEInkx6+5CQr1ej1fEAvY2ASV5JqlOBSPOSn4FosFly5d8mCHGOkbYzyjSoAJAW211kynUy/RDsPQy3Ob/flxo3+5TrlHKcqFkSIAS1EUHB0d+XZbW1vDGOMBjtXVVR4/fswP/dAPsVgs+PDDD3ny5AlHR0fe/08YLb1e70LQgACHX/ziF1lbW+Pg4MCDgqenpx6gfeuttzzY0O/3GQwGTCYTzzgTwKqqKh8i0GQpSvCOhIs0C1sBPKUdmkw8GW8C6pZlyWAw4KWXXvLy28lkwsbGBru7u7747nQ69Pt9D9h8PKRA5MbS981wEgFzBQAG/NgPw/BCsrGAgwK8SB8BjEYjz0QVi4Cqqtjd3fXs1qZ/nsgr5f7l/wIgijRRgDKRoAoAIONLzidA/Wg08imx4u/Y6XTIsoydnR2/sZDnOYeHh76dJfl8dXXVA48iH24Gosj8EvbqcDhkfX3dg96vvfbahTkvmwgyJ8V2wRjD1tYWS0tLfP3rX/eJsNJucjR9T+WP9G8Yhty4cYPnn3/e+zjKeD88PPTneOaZZ/zrDw4OvE3ClStXPHhqjOHy5ct+A0Z8aGX9awZMyKaJbE40ZcUCjA4GAx9UBM5KYXt72/eJAKsC9IlHpYxZaQORvt67d4+yLFlbW2NtbQ1rLdPplBs3bnD16lVeeOEFFosFx8fH9Pt9H3Yi19/r9djY2KAoCh49euRZkWma8rnPfY6zszPv+SleiDIne72eZ0gLu3xnZ4fnn3+efr/PvXv3/HoAcHZ2xsbGhu93CQuSMBRZG6SvT09POTs748qVK8RxzMOHDz3oJoCxsOhlo63b7XL9+nWWlpa8p21Tsi2bY6PRiLt373rpvjznZK4JkNkEl5sbY+LHK/NQxvDHN/pk00jW+uZmoDyDBFyW1wsrXc4tc0Y29MQiQTYORHp8dnbm2YpLS0ve53Ftbc3PRwFbm1YBv/M7v8N8PqfT6TCdTpnNZnz2s5/l7OyM3d1d3n77be99KgzmpaUl//yXTcLd3V2/3spm3O/3+IMPINZS0CbzQXwQneRJUaUOHMwHzhxdF44JZaJzmTCcFy0AyXHtN5XUoKKojSsn79TWsXGcNNL9zgTnQF3R0YQz670GXaKyA0eqxLHyypYinJ9L+bKBcuECdf9ZBSZ1BZcLNnA+fCbEg4VBVn/B7GuKjpNc2rD2harBQ3CAqNUQ1kwfwKdOl6k7vxRxVikXolCbxFtdM3YiByY6X0HtQzjisQOoAIq2dr5s3YzocVQDXnXoSqYYJHMOQ0X3ozMnIc1cmnDVjqjSkGBeYkMX0BHNrGfUmUh76aupQZ+ggLzrfq+q2gdx5kAxq2B22aW1Du7WHnoofvErP0V4pmnXLBVVuYAUE9ZgbQllBIsVSzh3oTvKQNGD+aWS/+RHf5Wf733A3xu9wF99/d9gseSA6fZegS5DTm8rWruWlb8b1cBoTtXSlIm7/iquE8JDfT6mypptJimh1oFdjknk2kfVslwaX8pVVTnwEAcgl60QkYO6H7qk76yvmFxTFFdqzfAicOEdc8v4mqboW3RuKOtUZkkTFe/AcK4oe06GrnMZh7Zmg1mMUYRhRRzWO6iRY+OC88MMM0NeBlQtSZp2c8pErqBfrDjgyrGKXDsUHRdGEiyEsWRcm2l/a5SJk+wLk1HCM3zytnKvKdtunvmjKN354si1Z2PB7O5WVJFivurAuu5DsEr7dq1SQAXkQ4vKoTNSTG4HlIOMNC2wfXeBszxi0J9iezOUssShS/zeP+lRHqdEvZzWbsjxi4qbrQN+/f3PElGDh7OaJV3LNVXl/l8JwBtC2TP8kc13qawmVhWpKojUeeIyQKpcInNbZxRWs6j1uJGqmJmISFUY6/qw7GifvoxyYxRtoXDXQS0ppQZ4jFGMbEJWhNjU8MW1u/yllW8RoFjYitWgQ9E5IVv+Lg+uWZ6JQsYm598xf5L3w0scxu5agoXb6JCQm/nzC4LvpkwuBUxvVFAqhncgPsvBuJTxIDMM3xkTTzqEc0NyMKPqxD7F3UaashUSTkvmGzFlG5IjtwaGEzfP8yGsvFvSuTemWGrxDx7/CP8o+xFMCKmB9q4iPXHepfFZSTjJqToRZRpwdtMFDcVnAWe3LT87+DafSgwzc8g/Xt7lN/orEBooZbACBnbLHk+FI3o65vOdO/wP8WcoqhhrcKzCmp33e2c3+Oh49cIzrmoZgpmG1QJ7EjtwTlkIDUUZ8J8//Gnu3N8gsPikZl1AbzhjPHLImiolmAo+3/mQf7b+HDtna44VqGrmnYbxbg9la9BLu2crBtQ0qJ+3NSuzcF6kbrOsTkGu79UxdnVtweHWFc8sLITp7nxMm/6FDkx266SAhcoo/zwLR/jwlarlAnyMT3B3z6uyW6G6pbuuUqPmgZMmDwvUTuI3iNLlBX/4+gc81TrkqfiQ29E+z0SWrnZyu8wWvJtHvHuw4YDGwrGkTa/k7buXeSe4RBAZorgkjiuyRYSahQQT185Hj9dJCtev4fR8I0rlNeO6+j4D8XvxEJBDgIBm8ShMgmYiqLBEPg6GCXDTZPFYa33BBuchJ+JT9/EghmaaYzOYRUDLJlAohVfT00k88oShJgWzFFpSiDQZl1KoCkNR7kOuV9pCziF+bMKKabL05Bql0BfPQWGkNBOmxS9ssViwt7fnAUEpWh89esTW1haTyQTAM8GOj4+5ffu2BxiCIGAwGNDpdFgsFl4OqrVmc3PTg5fCJpJrlWts+k0KC1MkwfJHZH/vv/8+rVaLx48f8+jRI184i3xNQCYpoKUtV1dX2djY4JVXXmFtbY1vfOMbvPzyy77/5vM5b731FsvLy3z2s5/1IR2vv/46Dx8+9IU0cEHWLlJjCZpoyu6l3QWAavoeNlmjUvSLR2ezqJfCudPpsLW1xfHxMR9++CFnZ2cMh0MuXbrkA25kPMiYEZDt42NDxqX8XKR+AgYKACzMROkLKcib8n0JHBD2oXgLCmPt4ODAS2ab41LuqwkwyLUKYCDsQWl3WQNko6B5nTK3xBdNZM8yBpq+gAJyih/deDz2adHCqtNae4BDEuIXiwVnZ2f0+/0LTODnn3+er371q1y9epVHjx5d2HyQ8VWWpW8jWVOuXr3K/v4+29vbfv0T/z85mvNfjo/bJzTHjPSXJLoL61bAD5HVyzr7ne98h0996lMeOHvjjTc800xAvqtXr15gD4t0WuwH5vM5Jycn5HnOxsYGKysrJEnC1tYWBwcHDIdDbxMhoGJRFD7dV4Ba2cgQJpz42V6+fNn7kIocXMDeK1eukKapZx9L4vn169cJgoD33nuP2WzG+vq6Z6AdHR2RZRlXrlxhdXXVM9RkfDbB/jzPPUNQmOhNFqZIY+V6hbkmGyviZypjWDah5Pkl73n48CErKyu8++67F5K6xSOz0+lw9+5dfw4BjIUxLbYAMmdkHsgcffTo0QVmetNfuLnhkySJ90eVZ47M0yZ43GQFy9wUtrdcu4wxmU/SZvIZEjQjz2YZs3KI7UhRFLRaLbIso9/v0+l0fJ9++tOf9t8HxuMx29vbvP3222xsbPjE7ea6IH17fHzM1772Ne9/G8cxy8vLrK6ueuaqgP+j0eiCRL3pESzj9fd7/IEHEK1IlMvavyjBG62XHUu4NUUrS/Gkg6ogX3aFTjAOqLoVqu0WNJsFqIUmGml/3nBWA37UTKCJSIUdYFHFtZdbZZ2cMsEDmhKAUvTOE2ZFNmqV8j6DNlDufQZfsE2v1InRNfPRBjUgWINLydh6QFIYYUGhsHMnoTax8sxF8XYUH0TVGBvhDBc0kAM1UFbF7n0mUCTHDnhdrFlUgU+GLTqQL1fEx0HNJNNUSeABWROB1sYxqoLzgJBoDEvxjO2lEF21yYYR6XFO9OSMcOcQbl5Cl4Zg7wysZXncBaVQixKbBHQVoBTBvKRqOcaYkxi6P0FxLkEOMlh+Q9WvcX+SM0v3buBk6YW7V2WdWX7ZtqSHmmzJhcWkx+cBLKNnS565vUOkK/6D4TbQ4f5ihbJjKFsB4cwyXw3Z+1LFH/v062wmZ/ytt/4Qq7+SoiqNrlzYRzAVuborkKHu8wDyZrKusOUM6HlB1Yk9QGsDhU0iVF46Vl4UQlFiA+1DSoqO8swW+8qYLDD044LRNCU/cTTU+RpMryjKTkU40ejCBaL499WM0bJbS/GVk7CWLccqjKYWVbikXFNpwsgFiAAexLehxdQy9mRkqDLVSMQ25F1dy+LdXEhHLiQE5T4nmuCZgMrUnxcFvg0F7NQFFO2a6VuD3RjXDrYB7Ftla5aiwkahY28qBfLFJdKc3Aoo23UwU+0zlxy7+T+7UTj58pOE7iPHahzdqmg9itAfRRigN6rnzCpMa9Zmuqc52apINmdYqxhcOePs4YBoCk9/4QG/dO8HaD9xwH8wg2RUMV8J6mARN2aKrvN3AweqDK6dcSU+wqAZalecGauJcSBiqgqmNibAEqmSqY3JbUBcg4wA7drIT5hZ+dAB9rqWcValdkxkYXjXFg9Wu2brq4xf+sTf4OClNmt6RmYtlbXMLKwGYDD87qLHf/PkR/n88l02wjOOZh0oFd0HbhxVac0I7DjWuDqMae8Zjl9yIFU80bQOc7deAmU7ID7LUVVF7409TKeFbUWYOEDJeleHGplYM74aMNsynp0qAFd4ecb+c5Z2auCfJBRddx0//HOv81LnCf/1d36c+J8kaO0+M/3whP2fu0beh+4jy8kLcPqJkhdvP+ZWNAccY2VeRY7NZ50voARV0a94Ojqhq9wXis1gRJoUFKYFpT6X+2rLa9tXKYoACahSlXIJxdfmKODzn3mfr715q/ZBhPlhm7dHKWoSUnWMA7oqhUkt/+aN7/K33/xBx0gsz6XF35w9TRw4WbfO6+S5BDdHcidrNgn1AwjHdlTWeRzW/EYbGKwBnWsnQc4lvfjcW1VsMXTpNujESsIGrr2rJeUBT+9vuHAbVWoC1Bt0ZZ0Oni+5c+is3hgMLSay2JYhaJeomjVp5qG7j3bFjed26EQ5b73xlAcPCSx/9Oa7/Gcbv82ZqRibgO2yzz8Z3+Q7oyt8d/sy+Ul6bomiLejay3cSYtMKfRKjckUpz+iidkQw8nyt/TbrDTTHtq0tMkJnDeLTmb9/fM8cUgBLYSmHMAA2NzcJw5DHjx+zt7fnmWVSLIovWpMpKMWSFGnCHjo+PvbsvmaKYxNcE9mwFKxNaZ0cUpADvvgXFqAAg+KDJ0wFAXfknCLlFJmdMFzkPkQOLJ8h7A+RwknhB3hwRK6tWWxLQIAUngLiCRMNYGlp6QLbUsBT8Z2TeyiKgu3tbZ5//nnPhhLGz3Q6pdPpeD8pKQyFGShSNPGoExaleKgJg0lYo5PJhF6vd4Gp+NZbb3kJmRSnzeANkSqKnFLkkz/90z9NURQ8fPiQDz/8kMePH/Pyyy9zeHjoJZtS4B4cHLBYLHjxxRc9WCXFtIBoAlRLKIUUvjLuiqK40B4C7jWB7Sbzp8kakj4QduQbb7xxATAQMEPAiUePHvH8889zcnLCYDC44DUogIgwX6UPpX2E9QZc8D8TcExAKvGxbMphJYykmWwsnnLgQC/xXmt6CApICM4vUwr1j4PtH5crC3gj86cJtIlccjAY+A0FkV0OBgPW19cBPMNOQLAkSVheXmZnZ4d2u83Kygr7+/veL042KCQ4YmNjg8lkQpZl3Lp1i263y/7+PlevXuWNN95gPp97TzUBH6TPJTke4MqVKwwGA1599VV/TU0gsGmHIOdqAqVNUFDGlSQzSxiHADzN/vz4JkgYhuzu7jIejz1gKEC8+Phdv36dLMs4OztDa+3B1v39fY6Ojmi3255pK2NTwMHDw0M+9alPec8/WeeCIGBvb4/RaOTHZBOglvuWNfrJkycXPF9brRZbW1vcvHmT27dv82u/9muembyzs8P29jbD4ZDBYMDDhw89iHn16lWiKOKDDz5gPB6TZRmrq6u8/vrrPnikmfwuc1mYrMJMBvzGhPhbSrtKGJiM3Xa7zfr6Ot1ulxs3bvhn1qNHj3jvvfc863BnZ4fDw8ML7O0rV66QZRkrKyt88MEHfm6MRiM2Nja8j21zc6AsS+9j2dyMa647AqSLRF5sG2QuNsePrL9y3bJWibRe1gNhrTZDTYT9Ld62TQ9ieUa1Wi2/ASIqAwlOuX79OpcvX/Zemu+99x4ffPABVVUxHA7Z2tritdde49KlS6ysrPD000/7Z5c8709PT7l3754PpBKmroC+4vEpn98MEpJDvEv7/b6fUwJMzufz3zcL8Q88gOgYdMLgqoHDtnGgQVrRaWWsdye8f9RyssukglJTDUuCjlv0O+2MvAjR2jDvp6igZtMtQnRakrZzJict2ndjULUfYqTIlmq6euh8Dl0KsyVYKNBOSoXCBx6oAlCKbNkSThw4SVmn7Wb1+yNFenAOgnhJKeJNeO4fBzBbdSwPG7r0SV04JpaqmUsuFMMxK4MF3qMtyM7BCfGWqmI8gKMLWzNE3HXrShEei28kBHPnmTZfcWb7aPdZ8zXrWGnziN7csUts6topmlh25gOKjqa972S9k62EcGWN+GyJ0bWYeGLoWYvKS7K1NoulgMGdKXqyIN2z5Cst9KIkPBiR3q2oltzDXc8y8uFKzUbEp/QGwnoprTP9xzEXy3bNlBPAt+XodumB67sydfduFfz1n/xbfDY55t+798f9uPv28VVUqYjGLt1ZGVh+LeQb3/w0ZUfR1hDkFSZwEt6yxXmis1be2w5cP4ULS7hwfWIiRd6tQbq8xA5TyMqagaehyUTMclRRYnodBxQD8/WaDZQFtFZyTvZ7qA8GxBbskqVYLetC1gWdhBOFXhSYqO1ZkY4WVDPOrPu/sq7/w7mT6ttQuzAEex6kQiHyfVuzd+uAn/45W6noKBbLQc1edenCRVuT9xXtPUO4cIxRXVhG10LyXkC6a929a10DrJbOdobOSspuTNR2cuXFckAV1dYDC+vBVLEBcBYFCgKNFZNjYS5WluTEkpzAYlUz3zBOdrxpvGRUaYu+MWV02QE8SVJSrmqq3RbxmebkRkXQKwjupnTvOxl3tgzpbkDyfg+rIO+2WT6wnD2j+JmVu/yt3/sxenXAEbhAGBsob4UgPo9u0wDylYo/e+M7BMqBgxWKSFVEqmJqY3SNukb1AAuwVEBHZ0xNQoAiVYULOFEWq92GSDR2rDMTulCbOK4wyoGvKOMDdpR1qcQjm/CLj3+C1x9c5cWrO3xq+IhBMEcrw380/IiQgN1yQGkDfq73BoXVdJOMQ+uCLmyA90BUBmd/MFOUiaXsOR1ue1sRjXIHBCc1Y2JRoua5A4KTwEmbFR4QtrVFhKnXZV1C1TbohYxNiKKK//TFX+d6dMB/9Bv/e8qO5a/+m/8d//G3/yR3lteosoCio0hGhpPbIeOrV5ldsuTrJeE0YvU7ht0f0vzwyh3Wgw6FdW09KyMn662BMxs4kCtOStoKDBaN4kZkeHr5kO886tcLVL2wGMXiqAVJRViz1NY+tcdGe8z/9dr/zIFpU9iAr73/NOQS7KFgGjiPQ5m/gF7OGIQzTB6gcKw3XYHJNP/t737JBZ2o2p+wfo/KxS4Bf102dBNXGKh6od2zIXPs/SA7HxfiLynevs6T1AGApg6GMpE7eTh3AFo0Un5tcD7Dbn222vkXVol167MBZRTxscbENRi5nhGGhjIPMKWiO1hwe+WAH1n+kB/vvMftKEaj+JujLd6urrtbC9xnfG3nJj9/eol7u6tUU8feplKOPZprSIwD9U+iuo2coiA+U3AW1iFp7h5scL45J+cvei7YzUTWA8SyuemB5e8zEL/nDmH9iTl80/8J4ODgwDPYgiDwqbrD4ZCzszPPWovj2EuLpGgW83qRsQnIJsCAeMq1Wi1feAjwJj8TOZsU6M0CX4BJAd2kcAR8kdRkFTWBAvHSk+KqKIoLhbJcn7SNnEeKFTG5b/rJCdtDXtMMYgE800+KfpHuCSjT7/cJgoDT01MGg4EHicR7Sph20oZSdC8tLQF41pCAFYPBwMt6pTgWKbf0sRRyrVaL4XDoE5gXiwUfffSRL3wBzwQV0E5+Jymi0vZNqfDnP/95BoMB3/jGN/joo4+8LFnYSGmaYq31LLmXXnqJ0WjERx99xMnJiQ/XEWaOMIg+LhUW9pdIbpVS9Pt9RqORb/8mECR9K/8WpuVwOEQpxcHBge/jS5cueYmusE8fPHhAWbpwFXBA7ebm5gU2rYxBAQ6EAdcEiaVNm+CBzCEBhgQUEPmpBM8Ik6kJsAvzEvCgqITdiB+n+OsJGCWf30ywFoZU03sN8OD6v4z5I+xD8UfTWvsxKnNRPBE3NzfJsozd3V0PomRZxvr6ugfcTk5OMMawvLzMbDZjaWnJp9ceHBxw5coV3nrrLZ555hnPEJM+FS87YeTK/BgOhzz99NOcnJzw5MmTC/YFH5e0y88+zthstreAt7IpItYGHx9vH9/wkHl08+ZNsizj8PCQO3fu+AAoYUoLsC+bLSKnlfEk7NVOp4NSLmRmeXnZg/Pdbpd33nnHy56b40VChppro3yuMLpF8i0MRmst6+vr9Ho9fvM3f5OjoyO2trb4whe+wJ07d/ja177mfRsFgBPPRfGpvHLlCrPZjBdffNFL7QXklTYWAF1Ab0mTlms4ODjwPoj7+/u+n4TtJmO43W7zEz/xE54BKs+ztbU13n//faqq4uTk5MJmEeBDPJqAufzuo48+YjKZMBqNvPxb5m+TxStzuAkQfjyJW+aGrC3CFJd5JOudUopOp+M3JmQTRTbzmmnmsnkgbN0mo7qqKg8oi4dhURQMh0NeeOEFLl++TLvd9s+lpaUlH9AkIVWySfXkyRNefvllZrMZDx8+ZG9vz38nmM1m3s+0KcfX2qV0y7hrMk+bm4Hy+uYGUvO7hTyz//85/sADiK4ygmikXXJqH8qtgrCWFc0WMR88uAKxwaaVKwxCQ9TNWe7PmOURldEEgaHXWjAfp4RxRVVpCCxJq2CtN+VJEVAlMdlAuQCQEJ9AXLZc6izUkt/Q+S9ZBdFYO2P7GAJ77tNUtSxlG9p7iip1TEVVOpZFOHXnFQN6F6IgIAcULUXe15Sd8/RmVUJ3uwZ7PGvLtU8ozLE60VWV+FAVXYlMzZ0nyGtJdazIY4imDggCl8AaTesi/UQRzF2hBw5Y6u44DVw+gCqPL7AdwQFPG+mYR+KHlzrvwypy3ofOq0pRdSLoROT9gLynWKymRO2QvBcxWwvoJJq09ukb3XYpsP13KoKFAaUdG6SWaVt1jrWZyBWz2bLBpBbbqqDQBBNNfBh4DzoqSI9dwvD0qYqfbM35oFC0w/PJdTDuYkPL9IomH7s01ef+1HsczLsc/NoVsiVL2QoYfmRoHZQeqC1bupasuz9Ves7EcpJhJ1WNZpZ4UkFRorMKPS9cuxuDNgaTRKgwQBWOiVj1653U5DwEhLh+wM5d4TK5VTgmkXFAkZ5p0kMBuuvAnnpOSZiHAw9cQwbzGoBWijCrMEmAjSwWKIqAQDu0UaSEJjboUjvWZABV6EBRkziw2kQuFCScu7YuO64tJIynaDvQYbKlGLxVnUuNlSLIKqKjKSaJiE4WJLsF5bBFlSYkRwUm0pSdAKscMOmT2WVjpTIo7QKHAEwrAmNJTy3TTU00huRYYcOQoAYi84GmagcUXeNAjBAWyxVBPyfcmpGthMRJSRRVBC8vKCsH0HTrlN7D/T7RvmvkwV1L/7OHfOvkKTrb52CaLi3TSxEnzyta+4rOrmubquXAlbJt2bxxxJX4mEiVBFh6esHCRkytKyZnJsHU8uUAi9YFwxowdGCiolP/vjCB7++iZpvqQr6gKb+WyCFAj9aGsUl5Y3sLW2p+fuNbfCLZZjUoSJUiUh0yW7ASTrjeOWKoobIVnSiHwBLOLPnAzUfxXTWxC0+Zb7jPD6YB/Yel/9yyHTgvVGNQeYFNYjcnxA4AfHhKOKswsSYeWya3Kggttk71ta2KK8NTfrj1gL/88OfIluG//BN/hz/cmrHan3KpPWKxFbJ3a4PJ1QCUZbxaL2alYnzTrXNrr1qiH6uorOFxOWevajErYwe61aCR8+lzgSEFoFEYLF2VcKt3wOvRDScVNwpV+yC6ddC4zai54nNrD/kvNn6XRKU8Q8XbeYYKLFbCStolzIOLjEegGsX81dd/DDVxSc4mtpjQQgV6rutrdOw6Zc8l6g40dOtgMNVu46mWsp9bg7gNlvPNpzr5OHF2D37TzLgNNZ1DfAbBwjF0Tf38rBIouq7vxQrEaotpWegVtLsZSrnNidkkodNbsJgN/HgMoop/+/lv8Wy6w9XoiGejOT0d87jM+MeTl/hPDp7n7uEKeRZhUoNduGAUE8Hh4yGHDAnGATqw56CveM4utN9MiUaKIHPAqYDQzjql3vhL3TPehNazPFE1yLpQhBORc7uNjWhq3XeJDt8/vscO+dIuEljxSRqPx14mJswwYa+InFEK3KY3k4SASKLt3t4e8/mcZ5991rP3JNVVikRJnW36P8k5muERUqjN53NfrIvXorxXwI3FYuEBs6Y/Y5qmrKys0Gq1vPxPUkOFpdT01GreY5Od0yzImjJmCZ6RYvb4+NgXwGJi32R8SeHa6XQ8czLLMi8ve+GFF7h7964Pgdjc3PThCvP5nLOzMy9pE8CgaZZ/fHxMr9fzIEiWZSwvL/vUUwF+m0mXAng1PR+lsJO+F0ZTq9WiKAo6nY5nj1hr2dra4otf/CIvvvgi3/3udzk6OvLtKUmyzTTm4XDoPRaFjfWlL32JW7du8eqrr/pCvenr1mxrAT4ElBZ2ZNMLrymXFwBaCnaRFl+6dMmzIcV/UBi4QRBwdHREkiTen0vGkPSBsFubnnnSF02QRgpnAcDluoRx1PSkEym2eChaaz0wJ6C8JBgLgCxAo9gQlGXJ3t6eB2WEcSzsPgGTkiTxawA4UPDk5MS3mfRhc2zI7yTZW0AJAY8ENA2CgGeffdaDzzLHRK7carU4Pj72/nkixX/y5IlnXYlEVHwdb9++zTvvvOP7ZTKZsLm5ydraGkmScPfu3QvAysbGBmma8p3vfMfLq5v3In83gaPm3BcAR+aO3IMAVHL/8rMm8NRkSMs4OD4+9vJOAZOm06lnDgsgJjL+drvNyckJ7Xabfr/vXzscDi+k6R4fH3Pr1i0mkwn7+/sehJK+kbnUBESb9yr3FUUR165d84C/APPCFJQxP51OefDgAbdv3+bRo0fkee4T6D/66CPu3bvngbqqqlhdXaUoCm7fvu09Xa21fv2QNmuyxmUdEnYb4FmoMp6bwLyEUfX7fb785S9TlqUfGxL0A/j1rgnii2/n8vIyd+7cubB2HB0dcXJy4jdJZG1p2mP0ej0Pdoskt2lxIeNYAH8J7hLgU56T8oybzWae3d3r9VheXvZ2GgJMCmAo81Yk8zJXlFI+5Ee8DF9++WU2Nzcxxni/xuXl5Qt2A/P5nPfff98DvdZadnZ2+M3f/E1vGyKsank2Nu0SpH/l2S3tJWt4k3HZnFvtdttvnggoLc9MCUNqbtj9q44/8ACiVecpwWXqgLRgN6G6ZMmMxuymBKWijCw6MhggOI4oNIyjimwRU+Wa61cOOZun2FKxOnAeKbsHA5SyRIEzR29njllQpRBN3eeXHceANIn1hUU4rwvj2FK1HJAYjxyIUXRdirFJrJdhhjMnIy66rgizYV3H1UwOXUqBZple0ixWHWtI544BkpxYHyyhS6hqNqRpgJDBwhVq5wwRPLvRhC7QIsgcM6iKXaETZA7kUdbJSavUAToit86H7rzxyN173lXM11zhFE0Uk6cs/c8cUn51zSVQ1qzJvAfUcsMgtwSFAyV1ZSnaynuZqcpSJdoxVxbKs1qKbkA4T6hi5/uoC8g3Okw3Ql88i2xafCsBZhua+WaFDS3hKICpdsnctX9mPjj35zr7RMFf/9G/w3axxImZc2A6JPrcmHWjP+b+NCa606K3XWFCxfv//XOcPVehLhtsbOl+8oT9l9ss/3rLSYDroBgUJGeVC46ppcwCEptQMVsNnOH+qMR2W+TDhHReYLVGZ4XzjqwqKM8R2qrlpnLRVZhO6SWHYWCwgSVbtg6gUGAjQ/chxCNDNlC4YAAnJ5bx4oDrGvwQlkwtJSz6kJ+5cBu0JYoqwtD56bmbcexKk9b93VEM7+SOUZQGBIXrtypybM9wXhfiNSAvrNmypX2BbtMQNTOYsDa5bWkmt4ZkA8c8bO8VlK3AJWcfTaGsSIDo8oBRmFyQQrt/KOeFGNYsxNJgYs3p05psxVB1DcE4IDmGak2RbRXEeyH9jyBYOPDQRKDuBcxX2+gChvuWyZWU6ZUS3Sswk4hwFJBfm9HtLFheG1EuB1TfWGL8lOJntt7n7//WFxjmljJ1ITtAHShSz7fA+cIJw7lcLvljl99GYzitOnR05iXLM5OQW3c/WhnaqqCwATGmljIbIlWhUUTKMLUhReV87cJZHayk3X3lfUscGIwV1uY5eIi7PAIsw96cwdoJf6j1gKfCGE3rX1ijhQmZKk2sSyg0+dABUvHIgUrK4tsgW6nZh7uK+DTHxC5d3MSa5FgMaxW2FWOiABNrdO48Mk2kfDDV6c2IKlFsPXXE0ahDedpxGwqF5smoz5//4M/w6JuX+c//zC/xM+0RgQpYbU35+u89R3pt7INzwlmD6VeP79HzJdleyF//lZ/imX9rl59qV/T0gn68cPNFNtTreWiNZmwCqKXdmoCNaOTmWaFxyVJ1gdMuwaoLIRu/PLnCb50+yze3rzHf7qKWcierBQceWuXXuybbzZ7E2Lj+Mm/OgXQbnrOvlTr/uS6U96WUNHjxZxW/QmG2V7HzFc6Hliox6FIRjt1zJB65zSHHaLUUPUXeh2oTqtitjygIz9yGT7mek3Zzuq2MbpIxWiScjTrMZzFxUrLWn3B7dZ+3dy4RLASgtFwaTvip3pvsVz3+u/0v8fr+ZU72+gRngX8GVIOSoFURjoK6b+prmwZ+E80E6txftarvt6gly8aBoeesQvdMp97EcsEv7rXxtE7gziEaWaKZAwyDvPKewuA+b7H0L0yV7x/fA4cw2sAxb87Oztja2mJ5edkXuVKwNsEPAZTk5yJzbLVankknCZWSVirBGVIkSQHcZHg0ZWwCUgmzTAqMJhNNZJvCVhiPxx6UacrhwBX0KysrGGO4d++eZ0oBPqFUimgp+KSQFqCqKUVrypSFDSlAoRRJAhQCHiSR+2h6By4WiwsFIOD7IQxDL7dbW1uj1+t51kmSJL6wbLfbHkQQuaZIpufzuQ9ymc/nvp3EY1GKOCngm0BKEAQsLS15gE+8tKRYLIrCM5SWlpb4whe+wE/91E+xs7PDr/7qr3rJtzDilFKMRiMfZCG+aN1ul/X1db797W8D8ODBA+I49mE7zUOAFik85T6F7SPjod/v8+KLL7K7u8t7773n2WFN0E7Yj8La3N7e9uCPjENpY5GRdrtdHj58eIFVJGBhMwVc2laAOrm2fr/v/foEIBEQuskAns1mdLtdsizzfqLN4AN5vTCUmqCOAGQCBDSl4AJ4CtDSbre9lH08HntQXZKV5d6bhwDnzTTodrvN/v4+YRhy+fJl3y8y74+Ojvjoo4+81DhNU5555hm/OdHpdFhdXcVa64Gq4+Nj9vb2ePPNN9nY2KDdbvPcc8/5+fbd737XB0VICIcw9mQNkPu8cuUKT548YWdnx7f5x0E+mecCCDXZo9IG8l4B41qtFuPxmOPjYw+QNg8ZC7IeAl6KDC5NXVitcn5hmTbHvlgtSJ+GYchsNvMMSAGZtdasra3x7rvv+rVBxo7IWGWONDcN5BqXlpZYXl6m1+v5MKXpdMp0OvWety+++KL3rfxrf+2vEccxv/ALv8CTJ084PT3l3Xff9e0ksnullGeLrq2t8eDBA372Z3+Wv/f3/p4H3WU+CHMuSRLf9k1gPs9zBoOBH+/S5gJWCUt5Pp/zkz/5k/5Zcv/+fZ/iK8CfALTy+dZaPvzwQ/+eZphIE1iW/pS1V+a+AM2SGC6vb3r7djodVlZWALzEV9YSgOl0ytHRkWf/CXO7CaCJN2EYhpyenjKdTv2mSJqmXLt2jZWVFZRSPrzo7OzsAqBsrWVjY8N7VR4dHXF0dORBX5Esy3NM1jZhdzdl/nLv8myWzTxhO4r1ifRZ02dxOBx60FUUB2Krcf/+/QsbVLI2NaXO/6rjDzyA6BKXoeyYWk6kiE8UnLpGjWrT/PGNgMoCkaHqGCgUsyMn2VSZZvtogDXOcD1QlmkeYSYR/dURK+mUj2YhwcIBZgJ0VB3HCMKCXakfoscx2WrljN4rV+SoyoEBAsyJ7x4G5uuu2Oo9PPe4g9oLKm+yGkEVjmFZxa5ACWe1nLoGGCaXFclp/cUucrJSVQG1v1Q0rVOojUsmNqELPREmENZ6T7kgdzLmoueYZ94Dr5aWRVMnSdY5NINr2juK0fMlwSIkGisO76zQqahDWBTPtPf5ascSni5YetddazDOsFFAsEiwWhGeZphWSDg3hDNNkFkP/CjjWHYmUF5SW6UOVCw7yoER4ndWv14ZJ4edrzmJt55qgkxRdCxFzzj2T2WJM8XkxYw//cqr/Gfrr3NiFvzioz/Mv3v7CbvlgCSoH0TW8PnVe4yzhGm7RVlLtAf3CvoPFKOnApJTQ/JPh8RdTTyuqNIa2LCOUZb1nWekVdRBO9anUevS9XUwy5lf6vDgZxVbvzmg/94pVmsna45rD7/6qBLHwsr79Y3XgQyBNk5OHzq5cjUsXbhDDdhAHW5gzlPJla2ll/r8/K5IdtfmfPLcZ6q0QinZbXNFuc6d/6gSL71EkQ9DlLEUbcfADDIHSAiAHSxsHVphPVDVOqrQpfO2o7JufNb+dlgHYIRzJ+deLIdeGj69OXSJ6AuDiXTtQ1azUevzYIwDbOMIlRdQh/D07xtOI41pWarVnFnfSZd1WmJuFhxdDlDTENuuiFoFPGjTvW8J5y4MRpew9nsB4IDNvA/lXodFu4OJ3LwZ3Dcc/8KU98YbdB5pqsS1Zzy2vm1W3nCSyaJ1niwPMNwYcy12iXQdnRFgiakobECFYmxa9PScvsqIlGFsNRWKqUno1J6HqapY2MCBikHlPDj7bq3SufLgiTEOUJwvB44+15BchoELbrFW0QlzUmXRNQJTb9P4/xc2oLKWCusYeoElnLrNl3wAdujGS/cBLJbqdN9C0Xtk0KVxwVSdEJ0b9CyHLMdGITYOfYCSyNBt4OZSPgiZbzjW3V+4+hp/9c0fcWs9YEvFeLeHfmeJ1o+f8NPtA0oUAZpn+3u8N77J7Kx1zlwOa/muZ4QrrLHc/NJ97h8t8xd/9X/Nr/zxv8JKAHlVJ1zRABw1hFHFoE7RmJmC3apgNRxBZCALfAKyf0/NBDWJ5R997TP8k9nn3LzLIYygiCL3WoUDH2upsZ+zjY0Af8010xB1fn5VumemsON0SR0mdW6x4EB8RdFzAScmxIGiyqJLJ+kNFopo4vwIi64LQxJmoWd71/PIWgXjiKAG24olN5eKPGRsoRPnvLi6y41rR/yJ/rfZCku+Mt/il/c+hzU1+79mzW8/XOFPb//7hEcREjajjTq/30KRbMfEZw4ELFtOUqxLVTMkcZ6Yed0uxv3eRPK39WoA8XME5w8bzFVt6UAd+ATRrPLPHHkOVbGiaGkffKYqt87pwlK1vq9h/l47pNgQCWOe52xvb3t5rxRIq6urF5JRi6LwLBkBJwScEc81YcpJ8SAAioBwwpaTAkwAwCiKPLAlxYkwEJtFpPiora6usr297QtIKQqbgI68VxhjUuAIoCW/F/lWs4CF8+AWkaUJ+CRtJ8WcAEnC/pIiSbwgpXCXz2/KWEejEevr654JJvJdKZoEMJDwAJH0aa19QSVAigSrdLtdz0YTsFWKLgEUBACSNm+CxFL4rq6ueslp8z0bGxt84hOf8Iy9H/7hHwbgH/7Df8jZ2RnHx8dcv36d4+NjLxMWydvW1pYfSw8fPuSXfumXeOWVV7xP3mAw4NGjRxwcHHhvRmELyriVsSZ9JO0tcr9bt24RRRH9fp8HDx74/pYiWIBm8b8UEKoJoooU1hjjx7QAmP1+H6XUBSBdzi8eYwI0z2YzP8+kbYVxKK8RYFDGsFxLnuceBBb2YfP+BUQVdpMcAjadnp4C58W/eDwKmNVcC3q9ngcdRIrYDLhosjllrDRB66tXr3opo4xvYTEuLS15W4MgCOj1ej6MZ3193a8H1lp/7wJgiDT77OyMpaUlPvroI/b39zk7O7sQACHSZ2lXYTfLmHr//fc9I/HjLMom0CNzvAkcyfFxgBbwLFzZNJD17PDw8IL9AFwMZgK8b2QTuG8CmwIuCYAkoTLNDZter0cYhjx58sSzyvb39z1IKn6pwhaTa2xukoAD51dXV5nP57RaLRaLBZubm3zwwQfeV+/o6Ij33nuPH/3RH+VXf/VXOT4+9qEiQRDw+PFjL2mXczdBt7IsvbRea83P//zP8+qrr3qGsVyLSHSF7SmbJsJWFZaa3IecO4oi5vM5BwcH/PZv/7ZnWYv0uBnsJazIJgtVrqHpkyprq8hrRWrclAhL+IvId2UtFyaiyG7l+SXPoqWlJc8iFnB4bW3NP3MESJP1Yjab+WepsNE3NzcZDAbep/js7IwbN27Q7/e5e/cuh4eHfsNFNoveeOMNNjY2uH//Pk+ePOHo6MjPY1lnZdPr9PTUA/1Nmb/MM7nH5gabrIHyubLOdbtd/0ySMS3rrMwF2eySdb95CFD5rxWAiHLhHEHmGFR532AD9+XeRq5ArWJbB6IEBPPQMdaGlmJYOeCrVJSZawpVKnaP+4RRhe4WTLOYt/YuoWeuACj6IL5k2aohWCiKYUUQGjrtjNEsRHcL7FGCbVdYC+FJ6AICqnPmoV40WEaz88AHXYcWBJn1cjJTg4o2cFIwcMwkZesiLXRFjCt6HBNCWDAiL1Oh9cVP0dEoU3vNVS7UJVhcLCDzoWtcqyCduuspO46ZIkwUYTNK6m88NhSdAJIKXYSEcxh8qB1jsXJgyPPpNkXPSW7zYS0VyEv0vCA6rqi6CXqRE5xNCT+a0bq06gCfyhJMAoIsRRcGXVQU3YigcNdiQuWL5XBhvW8k1hVqZVt7mVyV1DKz0NK9ccZ/8dI/4C+//gv8By98lT/Re5tlHfN6rgiI2Jt0ATguu6zFYwD2qxmf737Iw+Ulvj5cpTyo5XtByNEnLSvPHnAyaVE9arP0tkIZB5qFC0MyMkQzSeZueLVFiqzvwERwfoh6khEMU1Zei4mm9Y6ZBhvU4GEUOhZiFFLVKc95v0ZJAovSlrJmmJnYoko3Dqy1dSK49UnLjr3VmFeBm1t2qWCWBgQzjSdgKheCggYduBRmawPiqHTART2OyTXKiDzbLYySOI5yfp9lzYpFwVwpuo/dmMwGro2iqaWzU6AXLoVXFxXRxCD+aiiwAtjUi2+VKqxS5N3ABdVmtvbAFBarcgw2DTYJUMagpwtYSens5iSjgPlySLaUuJT0SDG5FlBdylC6bsezkDIw6KsLTtZcLHvSyQlDw8F+h9ajkGzVEl+ZkE1j4ocx0chJuBdDxU/dfJf/+dVPM8zcXA4KSzQ/twtQFqo6iMlqYUA5RmmqHdsQa6hQXr7cUTmpzunU+v0j08LU6csVip7OmZqIqdWkqmBhI7IyJJwpkhMoO24joug677aqCAg0zDYV/fdC5hu2BnvroVgvKNMyprJQ2IqCigBFoiI0inHVQitLoBS5tTw6GRIdhS7RfXYOyGSVon1oOPyEAqOIRpr0UNiGUPRD2k/mUFaorMAMuphQY5JGmp9y/odBZphcCjGxJV+ueCo+oCoCLy1GwfDNkOlVyw+s7/C4Kngzu8QwmPK1vZuOhXcUUQ1L9DQgGmnKrq3nED5U5geX7/M3n/kf+TH17/HH/tFf5Ct//L9CK0M41X5DS5WKeKRZ9CPeylf4ctnjuOqyn/c5KjoQWPRCUbXOGYJqFoLIhTMHdLkxYKnajl2oM+3AQlXPVXBAovxbWHECpJWKINf15pDyifMyp1Xl1nhVufW8ShRFH+9DaLXz0Q0ninjmzm1ix/ovOpAPLItVKDvWJzKrQU63uyAOK0bTlLIIMIV2jEk4Z1gq+Lnbb/KT/bdYCaasBTmnJuTvHH+eP/udP8/ktAVW0V2asTKccBi23fkrx6bG1EzBCsJ5QJm6jY4gOwcJjXvUEE3xIS6y+WWiWilQt7Gtw2mEVei8lR1omBy7zYJ4aurNHluHeOEtOGz9bDax688grz1jayDWKgkb+z54+L14SAElaa3ieXZ4eOgLJQEJhOXU7Xax1nqz+CZLRgplAQLFh0qYDFIICmNHPnc0GvkCpClRBnxR02QVCrtDUjKVUhcAEQEepfBrSoel6JNCSOTOAojAeQErkm0pFsXDULwKm0WhtGdTAt0MlxFZtsibPy6BLoqCtbU1D9i89dZbvmAVtsrR0ZFncUibNFkdgAc09/f3ffhAEATea04kx81QBilQpU2a0lprrQfNBGS9desWVVXxyiuvMBwOuXPnDi+//DK/9Vu/xYcffsjBwYF/zfLyMsfHx57xtFgsePz4Mbdu3eL4+Nj71TU9Kj/88ENWV1fZ29sjiiJu3rxJVVUcHBz4dhawSQpmwPdFr9djNBqRpimXLl3yEuhmOwnjqJneDY75I20g7CQBXQQ0kjEjoIgEFAjYJKCuyFB7vd4FMELGVRNMbAKbwq45ODj4F1hwzeJZGE/NMBlh24m/5Ww285JPCVqQ6xHgRdpENhGaQIrMJyniBQgQcEM+Z7FY8O6773Lz5k0GgwGz2YyDgwOUcgE04ukon3V2dsb29rYHWgSguHnzpm8nGTdisfD48WN+9Ed/1EuT33rrLb9OiQy7qir29/c9oC7gqjBNP57YLeuEjPWmPLfJDpW+brZXUzbfbrcZDAZ+w0AA9w8++IClpSUPEDaPph/ix9e3JhMa8FJOASLFe1EAtaOjIx/uIZ6Ds9nMg+li0dAc83KPAgI3AZ7Hjx8zGAy4c+eO7xMJvtnb22NlZYXHjx8TxzEvvfQSzz33nPdhFTBYPkfWTrkXkckeHR3x7LPPsrOzw+c//3mUUly+fBljDIeHh/T7fdbX11laWmJ/f5/nn3/es/uEEZmmqV/jmhsrAkK/8847F5iDzb6VMdAEc8U6Q8BPGesCnsnPBIhvBk21Wi06nY5/73w+94CctdaDZLI+rK6u+vUsz3OWl5f9s3V7e9tvyokv5fLyst+IEYn4+vo6Tz/9NOPxmEePHnHjxg1WV1c5Ojpif3+f1157zV+LbPQJo3JnZ4e9vT3/+c3NCNmQybLMrz3idSv315TCN6XX8lyXMS72Gk0G5e7uLvP53HtWyufJplhTEi5WDisrK6yurrK0tMSjR4/+pV6s/7+OP/gAIvik42jk5IXBHA/OJSf1Ay5wX+6FSRDMFaoGV5SFPIqwkXUeg/OIMg+wi4DxtAeRIazc+/L+eUiKqpxPWnRrSlVpKqOJBhll4SqI7qr7eX7Wq6VgTuocFMp7/kdT5cE4CeMondu+B1lsiDdmV9YV8MoolxgsbKEaRNS5xYaKeFwzI0ORXrliJjlzoKqymvTYULQda1Hkw8HctVd86uSEDlxSdcF/zqoIcnueqJs5kKVsabIViFoFunAPRlvbSOkCpjcV68EYm1bYQGO1ouhoZut9WoeVCw/paXoPA4JZgRp0OHu+hzIwePcMPZoTGSiWUqKjKcHxlPSBxbYTTl4eIBLy8TVNd9s4vyoc48MqTdWtPXtW5vynL/8a//2TH+QXn/5lNgKN1oaf6LzLW7mjP38yPuLL86eI6kCdvWLAeuToxW/mSzwXHXIpHRHOFZ3d0jOz1l/V2G+v0O5ryjYEuaFMFbNNV+l3nmiK2vfKFbmQjCt0AckIqoWtPRANaIXOKtZ/233BM23XpqqoU+aUG0em38ZEbgxUbevAw8AQRIaTsw7BVPuxY4yC0vkOOvaf81t0/UEjCMCSXyqgqNNkm1Jm69ipZUujg4ogMFirSKKSiXF9EJ84wLFs14E1NcAbLpw03QYKEzg5/+ySK7pNaFmsKLpPbM14cp8VZBXVoFVflyY+zR1jM6qlD/XfLvG7BhW18v6NVtXBQkah88oBiGHgwKiiwnQS9GhOfLSgHCZEZwXT9ZC8D+MbFtMtad+LSO/V8pY6OT0bJ+QDS5A75mHeDcm6JUE/J3u+cCzPIqA7nBEuTxhNWsS/1eL4cyW7iz7dO6EHT3UOi4GuNxEsJqhBhlo6aUIoNgt+cOMBqSooVEhhAyIqIlWSoshtQGU1qSrRytIno1KKsUkJsIwFRQGM1TwplqisougbZkY7YCpXxGPHNNNRBbWEMxpb5ut4wCfQlsIGGAs7Z32KegKcmYpVHVNZg8FS1SxEjcO22knBmXJrdr5kMO2KqJcTvtUh67trQUH3sSWcucJF5Pl6UTofTK0w3Rgb65oZXXtrdYJ6A0WxWHXj66mn97kWnhCnJVnkQrAG7wbM1yF8ZszX3nuGn9//C1wbnnLnq9fpPoLlExd+dPDp0AUNlVClBoQJXiqIDT/Re4slnfLqD/4N/tC3/hw/8Xf/jwCsvGUZPRWQrTj7gCBT2FnI3977AndO3ZebyijOxm2itKRSkdvIKpSX7zvg3cllMVAFdUBWcZ7YrMz5GHeAo0JPFbpU6Ox8s0kYuNhz/0JVS2+FyWc1ZEsO7K8SW7O2FTqDcOoCv6q03jhbtg5kTtzaGExE+wudp8/ophlnsxbZImIyTp2P8GDCs8N9fm7l2xg0f/GrfwpGzgoAbbkUn/HPxy/yG49vc7w9dAzqwDJcmfCZZx5grOL9g3UO3lkDYdYbRbTj+tR5ObpzBXn9fDdg6jaoagDe1sxCkSCr4jzARVVu3Qqymt0/dd8HdOEY++HcYLXya22Zasc0rDfIXJtJe7q+zLu6TpB21iBFz5KvlXTXpyw+GNB+ovj9f137/vEH4RBwTIAXATmiKPKglaTLCtDUTD6U4kFCHkQqJSDHdDrl5OSES5cueSmtFCrWWq5cueIZh+Px2INlcRxfSOgVnzAp+uR6hd30L/MobIIC4ICjpnSzCdoIQ7DpJyhFvHyuFK4CCAkAJKCjsMEErLDW+iRlYW0J+6NZsMrni6/Zzs6OL8ayLKPVarG6usrW1hZvv/22v35pz+FwSBRFHB0d+UJXwIZ+v+/ZkKPRyLeRFNgSyNBqtXzi6ObmJuPxmIcPH/r7kvPduHGDL3zhC/R6PT744AMWiwXf/OY3qaqKu3fvcu/ePebzuWfjAJ6l1e26DW6Rrg6HQ1/s7+/vs76+TlVVbGxssLKy4gFaa51pv/hyScCDtPdisfBsNwGfW62WZ9188MEHzGYz7/vY9HqU8X90dOR92YQVtVgsPPAjr5f50fT4EpaOeC8KQCyMVBk70u9Nf8/RaOQBx6bvWxiGjEYjz6iUROwmAC7SYwHwJXxHfj+ZTFhfX/dSQwnukfMLuN5kDTUZcM2ivinfF/BB5rvMA5kzu7u7XLp0iTAMPXtpPB57qeLR0ZH/PAEPZW4VRcFrr712ARibTCb0+30uX77MzZs3WVpa4sGDByilODk58WsOcOFaheEpEnxJyxbmdHNtaLIQm4nUTW88AaGa7xEgWRLIW62W32wQhmhVVT4xudnOcp1NwFiAPLmf5vwRANMYw5UrV7hx44bvLwlSOTw89GxkkRAL6CNju7mpIeukSOubSdYStCMbRtZaVldXabVaXLp0iUePHnF4eMhzzz3H0tISt27d8mnFEsbx4Ycf+k2QJptyfX2d2WzmvQmfe+45jo+P/bNmOBxyenrqmaf3798nz3MODg7Y29vzoK6Ao7KeCkAs/S5tKnNP+lD6rAmayjNH1hUBBeV5I2CajP3mPGqyEZtjXcBtOU+/3/eyZZGf7+3tMZvNvL2GJK4PBgNWV1dZW1vzmyyy1r/xxhu89tprF56Hy8vLaK15+PAhX/nKVzg9PfVM4jRNWV9fv7BRJDJ9mTfC2pXnhbSBrFdND0KRrkubd7td2u22ZxbKNcmaNRqN2NnZ4fj42Le3POMEdJTnlmxw9Pt9z/iW55h8joQH7ezscHZ29vv6rvMHH0C0YBJLHrviRlUQxufSzHAO7T0XqoCCYF6x84WE7OkFTCL0QhGdadK9wLO0sA5MDDIHRpRr51/xg8x5d5nAFSk6gywPKYuAHNChwWYBQa6YTRPipHSyT+uYIFjHgrG1N5YJQAUuSCXMrDdnhxp8MxbMediAK7gc6yqcKM8gkYRWEzn5mCqpix/Oje8NBHNLmNTS0sgBjU1plWN0nRdkVQw6s/58ytTAYQWac5mb1u79RQdMFtLK8R6PwjJaXC5YDhbodumN+vOeYrHiwLX2XokdaEysUVWI6WnKOsk3X2mjllrM1yKKtsbqHtEop+pEjK8kzDYdO8TEMH26IMgieo+r+t4dKJcsz/j8tfv8hfXf4oup5pvjQ1JluVMEzA46PCoH/C9nL/Izw++QKu1CKnS9i2hCVgInR/kw3+TTySmpLsjWKqaXnJfW7JJi7fM7PHqwyurXFempZbamCReWlbdLx5AzkJ4oz1QxgaJMtAvmCer2Lusk3iRyTLkohEC5P7aWaxrjTC6riqoV1f6J2pn4A1iFUhZ1v0U0USzWjQexvfSxfqkk4MrvLKDnmuA0oBhUfux4abhy/y7aGq0tReF2kMrKAY26cOxcG1iikabSDhyTwB5lFPHYeGZt97Fj8phAEWaGaFLRyw1VrAmyelcyFYmyK+LdvbsbCMvKXX/m5ob83Ia152PNNtSRRZUGAo3Iv1VRYYOA7MqAZGdMvF9ilWJ6uc38Zg6FQrdK5s9VzCYhOtOYpEJ1SphEhGdODp+tV6hcEe7GKANF3xBMnb/beCkEowhPQ5SBH3/5XX7z/dsMp44JqgvHQCxbTkqqanl22VJ+46DsGn74+Q/46aXvsLARATULE83YtDit2qyFIzaDM4panrywoU9m7ukFFZqFiYhVRaRzhsGMQFnCiSY9quWpsWOTVTG0A4OauzWj6AoI6+Z0GroAl8ksJU0KespiMCzrkDOTsx64R8uL8RM2h6e0VcShzcnKgLJjiG5NCMsApSym0vQeulRqAsfO7j0s3HpkDFWaEJ+WUNbhKamzOqiiOtk7UJhQU6UuobvqB5Qdxyr7ty6/TuLjziHdqcdq16DykP7ylPGoxWFcsPqGobWbUXZCsmFINHFraZXgwDKZNwEsrY65Gs7YqWAjSPi7r/wN/lev/yU2v5FTdgJMAqZlCVfmmKcq1OMerz54imF/RllpsiLEFBozidD2HDx0IVj1F+/KzWMbu3XD1H8rUSnnCrVQzu6gZhUGuQOjg9ye+90at644Jp4iW8J78Z17+Mm6A9HYgV0mru0h+o59WfUrdKskCAwUAVpb0lbOvOy6cJrAkuUhTy8f8iMbd3g63ecz6QNuR4pAKb6+SPhno5f46t4zjecbBJ2S/+atHyKKKpSCeGmBUpZsGjO6N+St7y4RzN0mlm5Zyo4hnGjCqfJ2ItImJqw3P4b15ldUP7ty5YD+mZPO69I9w4JFbcVRh5sEmXHrVIMcKD6QZdtZP+jSgZQir7eBY/UXHTdniw6UXcd+VZ2MtJ2z1J0xSBYsJzOutY65FJ/xf3vyR1G+I75/fK8cwtwSrzUBCaTILMuSjz76yIMgg8GAl19+mQcPHviibDweXzBrl2JWDvm5gCFSBLfbbc+MkUJIgBXAAxJN5kdTSinXKl6GzSAW4AJg02T7CZtL7nE6dSbhTZmdAKJS7M9mM38eAa0E/GrKq6UgF9aGnO/jrCZ5nRS4AqoJ803CMqTdRVoqgEm73fbswul0ynA49EVuq9Xi7Ozsggy0WUALK0nuqdPp8OTJEw/4iGR5d3fX+/ctLy9z+/Zt75H3ne98xzNplpeXybKM0Wjk2azCBm2yCpvsHSmiy9IlQK+srPDJT37SM1MfPXqEMYannnqKZ555hpOTEx48eOBZbMIElHsWwEbaW84t3m0fB8GarCIBbERSL9eqtWYwGHiAWkBAGb9yDgHRJPwF8OnHIkkU8LHJGhRGmAAdTdmmAGhlWXpgRQpvYQoLu7DJEhawWgAFCdqZzWYX5oS0W5NJ2JTwNudC0xtO/C8FKG3OY8AHZAyHQ+7fv8/x8TEnJye0Wi3W19e9jF6AAwHKZNzDOatJ+kV84YTh9/bbb/Pcc8/xK7/yKz5wSM6VJIkH3WUtiOOY69evk6Yp3/rWt/4FIFkAoyYw1OyP5jr5cdBR2FgS+CKsYJlrKysr3stQZLPNDQcJT3r8+PGFtUEsHySJfXd31wPsSrm05ePjY7TWvPjii5yentJutzk9PeX555/3/nkiewYuSFNlPZZrbc6dbrfL2dkZly5d8t6sAIPBgOXlZQ8ILRYLzyButVo8fvyYu3fvet+72WzGfD6n3W57oLWqXNp8r9djd3eXT3ziExwdHTGZTLh06RK//uu/zmKx8Aw9Oderr77qE6iXlpb8hod8hozFpuxcAGD5v8y9JEk8ICUenfJsaG6USX/JM0DWYQHRZUyIvYAwtcV/U8a6MGElCEVCmmRjRtiz1lqGwyEvvvgig8GA09NTFosFW1tbdDodHj16xN7ent8wEeB9f3+fe/fu+XYTP8bNzU0Pwi8WCx48eOAZ7MKsbm4gNT1qZXwuLS1dSOOWsSLAngD/Iuk/OTlhOp36DRRZ+2Q+NIFraZter8fq6qq33UiSxK8pEvAkG0Db29scHx/T6XR4/vnnSdP0XyMAkfMv+zZ0Hk9V2xVP4bxmZbU0VaRqAMM9CJN2QWtpSjfJeXI0wO6lqNJJSsM52IVyAKIFZWKo5cs6r1OStWOihHPL9HEb23e64YoafMkU1V7CohvSmqk64dShNiaEYA6KWmadWsKFQh/h2EhVDSQqB9oViWMuKOOCVPK1kug4JFs1Tn54pC76/llcSIGpmZcx3gxeUm69zFeB0aAanlnagDXuGsSTS1fWsw7Fm1HlYirmiqyggKpXoU9idHbOmNSle93alVN6ytLvz7G6TTSuSENF6xgXnhK4IrroBujcBSkIu9GGyrOhTOjCOHQVUbYDsmENpNZMrpXfC91nQs0KMczWNT93600+07lHqkogphXkaGBsUrCwHkxYj8cM9RytFMZqerFb8A7yHr2ee1hMqpRIabrBAkpF+6BybJQKdl/bJEgss02XnFs+M+Pq2gmP3rjE8puKMnXXGY8tyakhtAYMJKdOCpcPAspUEY0kLMXUYOtFs2Uq40AwYzFp4NicbVwQQ+nasQo19nJGcDf10sQwqajGNaOrHjOqsrU/o2PjKFwRbiKcZ2jPEu5G6MrWXpmgc+PAbwVaOxCoMrr2T3RzQwI5wqkDYjp7rh+swgH64EJcAF25uZYpRdZXJCNLclJiIk0+iAjnlfeJNDWYqgJQ1mJUHbqja9k/Mg8slFamBcGiwiQhNgqcN2JlCccZhJqiF2DCPtGkxMR1CvO3YsouTG4qVKsORolCVO6ASb2UUbRCikoR9zPCsGI+SbDT0M2rKwtspVCjiGCm6T5SnD5rWI6ntN9JEQRXZNIunMhtMlQRVC1hTkNwZcaPL73HwkbkNkArQ+o9DQvWQrcjPjMJqSoY2QRjNVMbk6qCvspo65IDWkxNwlHVIbcB8yKiSp38lJoRnRy7MKQoqDC524SZbZ3LRHQFWlnaOuOnnn6XW619UlV/UVcBy0HAw3JGpOCVpIvT1sYs45iqOlfYetFO4pLxg4FjQPdc/7V2NcnRwo35IKBsadK9ufOqVMoxcWu5MhaKxIHLZcv5jU7XQ6oUWMr5dOsegbJobdALRWfHcnYb58u3CBmdxixdOeNPXvs2/6+X/yjm0y2K1ZJkR5OvVARTTdWy6DrJ3IZOont75YD/x9EP8cvf+AE2njrm02uPyW/Nqb4bkXcdkG8DS/WkTbFUuKCO/ZSTHbeT6NhyNcBXKLRRnlkon2Na522uKkUw00Tjesc5b24OOfZbOOfcEiF0HqfZsF7jayZ6Vct7w6kDq6EGChMoYwdSlm23CJjYoHsFg8GMMDCMpinZKMGUEcQGnVTMDjoeVFaFYtid83+//g+IgN/LNvmbx1/kG3vX2d8fYDNNsrSgneZ+rVZGUSwCSGGx03bWI/WzL/TMwPP1nYUiGgcN72PXzs5rWHmAVRXKj+Vwaj0Qj4X0GKKJIZ4aHwijauZ0FTkZvySiqwY90AX0uI2Toq0o2w6MzVYroo0Zg+6cjXTOUjpjEM3phwsSXdZMXcUgdM+Pts5p68xZL1xUX33/+B44BHiYTqeelSeFgoCA4/HYF7fCqjk9PfUSYikkROIqLL6m4fnx8TGTycSzUURqtr+/z5UrV3wQiXggidQOzkEmKeikcBdwSgpx8YgCfHEMLrFXXityN5FYtdtttre3PWtFimvx7xJZqwAcwkwTdkUTUGiyt6R4lcJJACQBsQS8lMNa69lj3W7XswmbYC7gPdY2NjaYTqc899xzJEnCycnJhRRpcHLypj+YMK9OT09ZWlri9PTUt6sASsfHxxe8uIQJc+3aNRaLBd/4xjd45plnfCp3WZaeFSPtJ+3VBETEY1D+Le+VIBFhtd66dcu3v0iJ9/b2aLVa5HnuJY3GGLa3ty+kLDfDcwR4EZBXAEQpZmU8SP8APr22CTxL8S2Fs0jRBWSSQ8b9x9l7AvY2fTgFeBAAtHkd0j7Nwn48HrOysnJh7ggIKfcsgJkAQIvFwgO6Im0WD8amtL/JvBMQBrgAxsj4FPBU2FoyrmVOdbtd4jgmy7ILvpWDwcD36XA4RGvN4eEhx8fHnv0s/TQYDPxck3kvx9nZGc899xyj0YijoyO2t7f9eBNAWYAg6Xths125coV3332XnZ2dC/cj/dWcv9IuTT/LJmgs7GdhnY1GI7+mSN8K0CvA6fHxMYeHhx5oFPawgJ9yL02PWK017733nr+Wq1everBue3ub2Wzmmcm7u7scHR0RRRGbm5u89dZbLBYLNjY2/CaMXHdzDAioJ2NuaWmJ4XDIzs4OW1tbPH782I/Zra0tLw0Xn9zr16+zvr7uJdyvvPIK9+/f58033+T09NSzT5sS6cFgwOHhIZ/85Cd58cUXmU6nfOMb38AY46XNOzs7F/pf+mg4HHppvDBvm76mHw8wko0W2ZQSNrGsRTLfZHwK8C9SZBmLo9HIr63tdhulzi0zZMNM2J5NX8STkxMfKiR9evnyZa5cucJkMuHOnTv+vEopnxK+tLTkNy9OT0958uSJ94NdLBasra35DS6xFcmyzIOT4/HYj39h1staK2ujtI3MdwmSEYBQNhRFKt2c/+KV3PQplHko47+5xkhytGwu9Ho9hsOh/4wsy3x40/HxMaenp5ycnHg/xI97sF67ds3///d7fG8AiHUqomkZgkXgAUWduQKtaGsHfBWWMtWOsThOKIqAvAxptzPGfZfgqzsF2ShGd0qqvYSwlmcFmZNwCgOwSsCmTprU2YbyuEbOh+f+e8FCYYOgTlp2Pld64mRbunAMvCqxNfNMUbTrRbeWV4mHoQ3cz8pO7S21cGCV6VTkiSKcht7TSQzmRb7sWIuWeF4zLvJaZlWzUxxg6HzhrBYW4jmzQxvrvfKUsbWk1HrgScAnEwWEMwOhIZgFRFPrfB3rAIgqUnxh8x5PqphOklO2erQ/OCT9qMLG50y7aNommDtapUkCoqlBtbRnRwq4WqWKcOZCR2xA7VnlikdJswUn6c4HEaPbFZfiUz5YXOJ6dEhhKyJVEQD7Vc8xT3HFXaIqR3BDs5q6nfR5FXE1PAVSBsGMAEWqSmxaMboaEY8dKNva08Sjul9HivabCZm+xIYFZQzKaMZXFfM1Re/huR/gfM35S7YOjGPlTDOqboIqjWPNKVUDihcrTttKKNN6F9o64E88ELGwtXHKk9EaeuGq5HIakR4FYK33rpRAFKsd+GABG1uMrs9TjxUZV65dK6x2El2lHAWoMs5jzUQOSFaFG/PRKKDoQXTX0NmpQbOFJRu41+c9N05E8pwt1zJkFRIUlqIdEE+0S6WuQzNc2JDFBA60NKoGP/3vAGNRnqFkazDCSfytVo7pamJUZShTTd7RZMOQeFSx8m7OwSdjpk9VJPsuFCVYKKKxa4fFmnYsw1wRn2oWm5qiVxC3CrJCo6YhZhrSXZ9iexnT4xbJmyHrn3rMr3z0Eu0TN2516cZNmTqZqCS1lm3lmcBlx/Bj1z9iGMzQdUBJqgoK64DKCkWqCqYmoUCzHMxYVBFtnTE2KWPToqNyIms4NW1W9JSFjVjYiMNHQ9JTJ9ssupbFmnGJ8r2CsgoIKks0MxR9l7Ar/V8YzVaQ8Ytbr9Yj8dxwG+BGdG6omdmChS15K09YzGMXzDJOUKOIqoClDxXTTYUNDDpX9O/XMnNjyVZS5zU3XqCyHJvE2EhTJQE2VKjMECwsVeoWXVXBfF1hQsML13a4Gs44NSFZFpEcaarYed+qQpE8ikkPYTxa5v8Tv0K+WnHt1h6P9peIJhE6D8iHDkTSuQP4VKWoliqOFh2+9fXbrL6tmH24zm98zn1pOHo+pOjXfTvXtPY19sClgM83TR2E4piWNnbAF3UisZvQoBduAyqYS9jQ+VoNbj0Na6sJGS9V7NaQMq0DjOr12gYQzOrzlI6paLWbZ4s1x2KVeR3OnGzadCt0WqIqjZmFnKk2ne6CS0sjvvTsHf7U4FU2A/h/n73M//MbPwZZiI0NVltOJy1+8uv/O4p5hK0UOq6IkpIwKalCTZGFnG13iGc1m1NZ0gex8yAMHICpCwcyy5pjIks+rJ+DQLVS+0vWEmxVKsKJIhq7+SneqcnIrVFFneaenBm0sJOVs1VQilqGbAGF1vLMVF5+bKIaKFwx6NWMtJWz2p2y2RlxOT1FK0s3yAiUYVbFJLUsQP6/HE75tZ0XeXyw5KmS7XbmUqED+L6G+XvnECBOgAr5vwB/Ii0WqSG4L+97e3ue5TObzRiNRr4gkwJckmaloBGGkIAZUujP53Pu3r3rC+eVlRX/+ScnJ77wbbLxBKyUAkLYfsICE7aIXLP4cAlIIaCahItIUQ/nHobCGBKZWrvd9gWbyGWbHmVN8AW4AFbJ/TYBmWZwgQCETcal/F+koU8//bQHt05OTlhZWeF3fud3PKiUpilra2sAnhUkxa7IWZvBFc0wA2GsCKPt3Xff9f24trbGzZs3CYLzVG2RpQrgJQwrAR2lzaWwb4bICAtKCkNh8lhruX//vi9iBUhK05RPfOIT3L17l+Fw6IMH5Fxl6ZKfr1y5wmKx4L333iNJEj9um/JP6asmWNYEvAWck3ZJkoQHDx74oBTxc5RiV84l46LJphMZswCPMmaF5SnzT8autJl87vLysgdlhL0lEj5h2Qn7Va5b2Foy/qXoF5BP+kjGXxM8lP5uyrKbY1naUoAGAfgl4KLdbns25NnZGYvFgitXrlCWpZfvCngt1yChJgKuCEgjQGiapozHY3q9HlevXmU0Gnn/zt/4jd8gyzIv4ZZ2lzVGWGJhGHLjxg2m0ykfffTRBXmztHuTHSps4SZbuQkyS7uJH10URYzHY9bW1rzHqDDKZJ6tr69faHtZ64wxfOUrX/HMLWEzCiNXbBCEefncc88Bjpl95coVOp0OS0tLniks0vGiKLw3oZxTNjs6nc6FdaqZGix/RBo8GAx47bXXuHr1qvewTNPU39uVK1cwxgVc3Llzhw8++MBLZoWNK/NAxlmzX/f29vjqV7/q07b39vZ8Hx8dHXnp92Aw8JsqwqiWNVrmhPShMNiFXSiAb3O9kTVBwHuRncuzSTz6RIosQGITsJJrkmsZjUaMx2P6/T7j8dhbMVy+fNn3kwRAXb9+ncViwaNHj+h2u+zt7fln7Pvvv8/W1pa3XxAmuoDKg8HAM8wF+L137x6Av15p4yYQvby87MFiedYLuNjpdPw9ypohlggnJycXfDPlkPaTdV0YjCJDHg6HrKys+Lb9OBNXwszu3LnjZcgCRIpFRZqm3sP2k5/8JCsrK35+zudz3njjDf88+f0c3xMAIkZhW7UfU80aLDvWA2ImUJQtat81Z0xPoTCzhGkrpLs8Q80DVKEoCg2BxSwC6FWODdQ1mKl2TJHCseUE2CtqlogYrUcTl1psIs6LMw04Qh1h5n6vjFOh2qBebDuWvKccMzBwci2rne9glYAVDzvtCiws6FkApk6CxjE1JNnX1nZnwRywimjsgL5gUfs1VeLbVAOWNRvC6jrduA5iCRfGGcWrWsJYcgHEcomShqqlncdkqUkPHfPFATrO5y/vKT7duc9u1WeYzjltaYpLQ7LliHBuSB+covaOiKuKctgmPJ4SANFh4ECjrMSmEcE8JljEBIsKnVVU7ZBwZt1nK8e0tLpm11kIMsPZjZju1VNeTh/x1clzxBgy6x50gVIclH3ndaac7DNQlgp4kK2ilSFQmty4hW5izlO/1sIRqlVhosh5gw00xQ+P0FHJi2u7XG6d8jt7N5n9k03ikZNotvYVm6/mtZF/nb6sFDoPnIS0BrpV5Tz+vGefdfJbGzjJoj8CjanDQbJhjVyHbi5Yq8irwIGjyjp56CSgvWM9Y9SBg9b7g7rBDE5XqdChwRSBb1cTKZ/kLaEr1iqs0VhbOXAESzAOHCC4lcFBgImcZ+JkSzPbciB2vlTRvRsyuFf5AIJKxm3mgLVoUqJM5NqrZmFKOIENVM2gVR4kVOpczoh2abkK9xpVj29hzQbZeeWuDBR1cAvKAbXR2KLnivLpBaZS2EUAoSVMS8pphJ4E6JppmewHmFONVQlBUgetzEOmRQ+rLb07IdNL8MXhE+7/7lUHMBXnQBDgwY2qZjrJ+qG2FrzSe0huA/q6IFCGoZ6xsBGFyJRN4qTJKmNWd8zCRoxNi7bK0MqgsazoKTkBQz2nXYetLK4UxP0MrWs7AwVBYJjOEpbnbi6ZQYE6S9y6kUEcVKQNtgDAYTXlu3mfR8UK3xzf5MlswKPRgLNxm2oaER2EdB8rWseGw0/EJKeKcAbxyDB+yp0jPtGkR5nfBCo7mvb2wqUupxFVN3HsUWsJZxWqMBAo8iR0DOlIkQ8sJjX81No7RICxinY7Q83aPvU2Pg7o33PzoOxXdKIclSkevrtJa9f5dxY91x66AitglVFEuxF3yk1YKpldigmnELzfQQWWeOyY72XLjdN8YOlsK+91aUNLFdZMQ4VLIi7de1S9Zkm4ic7wqeR+UyhwASfzdfcsA7wcWcZ9NFY+nd7E9bNliA83saFFtSrsPHD+qMMCfeZ8GKOxphqUtDo5r2xu80J3hy90PuSleMz7RYu/f/yD/Pvv/9scjLpUpSZsl1RZgF5oTMvQaWUcH7qgBozCjmLyKnYS4nqTxtZem0GGZ0ajHbveatdWVctSiIdpQO17qNALB9gHc0U8En9CtzGGdUCgeBrKvA4X9aZMblGlrZ9/lirW2EC59lwOyJYVi1WXvFwtl7T6C7qtDGsVn1jZZS2eoJUl0SWpLoiU24QaVymTKuFycsLCRPze8XWMVdw7WKE4bEEFg/cCNvcrZ8NQWHQZo284a5LvA4jfW4ewCqy1vsARWVKSJIxGowssMvFQMsYwmUwYDoeeeSUBKsIEkeKyGSAinoECgIhUT84hYJZIVaWAF8BOClJhpwgTcDQa+QJIinDxI5RESnmvFO/irSdsL8AXvXBR0iwAoLSDXBOcM5ekMG16iokMS4q3ZpEmQE9TSru9ve1BPWNc4qnWmhs3bnD//n2yLOPo6MgXUvIZAk4IYCj+WuPx2HtwtdtthsOhlzaLIX+appyenl64NgESlpaW+MQnPsHe3p5PAW5+rgDNIruVdhSQTD67KYUVFkwT7EqShM3NTZ577jnPAJIC+v79+16a2e/3mUwmvPnmm76wFraNANUia2v6lTUBsiarTACRVqvl70HeI/0N5z504iUpEsLmOWUMf5wlKONUQCphzwrgJXNQxqWAUxJ0sry87Nl7UogLGy5NU3Z2dnj06JG/Xxmbcq40TT1TUeYQ4MdcE+QW8EDeLyCj/PvjwKJ4kwpI9vFk4rW1NQ9Mn56eXvAwk3YVlmdT5t5kNHc6HcbjMV/4whd8QvrDhw8vePsJm665CSLAyaVLl3j//fc9OCn3JH3XXAul75qMp6bkV/qoaXPQarUoy5LDw0PP5g2CgM9//vN+/RSgVIJ9wjD0IOFgMGA4HPpwCJH4ih+ctc5PdH9/n1dffZXZbMZgMCDLMu7fv+/9RcuyZGNjg93dXRaLBcvLy0ynU3q9nrdpqKrKM9PkXsXLLk1TVldXfaL6eDxmd3eXzc1NbyMxHA59uMmTJ08AuH79OhsbG/65IHNB2q7pNymehZPJhO3tbR92FMcxk8nEA1LCNBTpvTyb5N/Shk0vz+aGkrBaZU2S1zYl3YAH0kRaPJvNPHO03+9fkC3PZjO/vo/HYz/GJPVd3iebLisrK+zu7gLwzDPPMJ/PuX//Pnfv3mU2m3Hv3j0mk4lfM2TNfv/99/3zQ0BokQQfHR1dYFrLRkETHG7aLIhPqqzXwqpu9kme5xwfH3sA0hjjn7GyOSDfAcRDc3193bMUhVkoY1zatSgKb22yvb3tgUKxiRCgVwDHKIo8MCvsXtkYOTo64q233vIbKUmScP/+fT+ufz/HH3wAUTk5VzU06Em9o5E4YCzIHMhQxSC+bUTu58EodIVUGTIpuk4vpajliQ4AsKs5wX5I1cXL+6xyQRWqNq3Ph86PSUDBKrbo3LHJlAGs8knFugBVpyzL9Vht60CXmq1Xgklhseo8GLMVKFsGXboCC+3kd1XHoOc1oBLVxWENVorvYDRyn60LyPti0K9QYwjL81RmYW5JurQUYEHukibBSSmpPRmVxYFZoWN/qbpwmy8FoEqCHM5uVySHAa0D52+XLSlux3u8kV2lHeYcJU6WXKaaKtEE8x6sdZlvJhQtTe9xQDjKsFHAbKtFOK1oPTwj3N6HW1eoWiHh3R3CQY/2YJX5ivagS9PfT5eGs9twszfmajgiUhVaWZ5UFaOyhQYOix7KKgKs/wOKSZXQDXMqa5jUoTAHVclaOKbC8sbsKrZStA4trcPStd+DNrOVgPfNEm8OnHQ3SFySbb5e8PQXt3nvwSWWv1Yn3LVdHw3uFuSDwAeoWNnJzJ0nn5rn2E7q2IiAKkoXfJJEdbKxK37R1nmoaUsQGiqjfD9jwS7nLFZTuo/qEJU68btsaT8W/Gu1xZYacu0Caep29b6bkey2WoKwYjGLCVqW1p4iWDgZt46MS722sBgquk8M42csaqwhsoQ/fMzuxhLD92oQr+ckhy6MwGIiTWsv84zBKq59DeswgyqpmXq1HFTX41iXFp1bCJQDDMpzlpckQmOclFoDvXtToknqAPTKgQzRzBBNQ04XKWbJkBwG5EODaue0luYsogQTV8RpQaAN47MW6ijGrua0OpnzhsxC1FlEe9+Q/cIpX9u7SXvHXbOqLO2Dktla6OWoJoKi68BcE7iNkB9/5n0uRydEqk7lNDG7JmYzPAWchBntglFOTZtUFfT0gplJGOoZbZ2RqgqDoq0LIluRqIqjqmYNakscly4IqnK+lgDJm23ahwUmUvS/m7iwpcKBPPfvr/PHqz/L/mmX4jQlOg2IzhyoE+TWB3hoDcPan9VqJ0+dXnJrrPnBM/jNAUVXeWlt97ElmJcoC4v1lKLlwEJCjZo7ZoDtx1RxQJA5WXuZBhRtRfdJzvhq4tapbsmPd95jZuHUtBgfdFmZOC9HlHXPiMqNk8G7IU/uXWM4smRDRXvXOjZfCy/3rVIL2g2gzrZiEoakh4qNby4oeiGHL4XYlgubEWbc9LIiX7KMn3Lrs6zzOld1+FUNztZgcpBbzzQUaW3ZVuSDc6a6LoRJ7qTlIk9GUYd11M+xesOqXCrBuM9VNbPv/8venwdZlt33feDnnLu9/b3cszIra+uuqq7em9hIACREggRJSSYlhSRSssOOCcWE5KBocyTFzDjksIMeBWXKY41HHtljhUWZWumRLAUlk4IEkAQEsEGg0UB39Vr7lpmVe+bbl3vvOfPHub+Tt5rwmND8RQRuREZub7n3bPf9vue7MFPksSFopSxd6LG730YX3r56Co2FEf/1C/8YgF/vvsTPXv9pBidVlLaEcU6jPmGpNSA1mp0HC6cWAf2A3rsLBEGxoWWcpNj5MirPlrShY8VnNedfbIt7L9bNSdkgC2aKqOvupcHE+Q5GIzcGy2u8TzfW7gOBTt2mlsocUOg8RTV5VZM1NNO2YjqnmM5b8k5GZ7nHSrPPmVqPdjSmpmdoZWkGExdUpHK0MhjrbCsezzr8xu5VTkZVTo7qBEeR25RULqVa1sowKOxWQ5gsQvVIEQ2Ns+dQcr7fPb6TDikeFhcXPbBWZjuJaXwZkJhOp97kvdvtetmcsMzKaaLiTSeFn0ifAJ9QWwbjBLARZqIAmSInlscLc0iKSpEVy2NEtifSOQGTylJVrbVPhhbWUrvdBqDX63nmjkibpbAts8ngSWBFAC1pWwFYhEEphV4ZwJDnCovjvffeY3193ReywppZX1/n1Vdf9e0hoEU5cVcKWmHNSFEpTJzBYMDJyQlnzpwhjmN2dnaekEaXJZ0CaiZJwvnz5/nCF77AxYsXPaAhUtQ8z304StnDDk4ZrgK8CSAi19xqtXj06JEHiK5fv+4ZbisrK3zsYx9DkmZFgre5uenlqWEYcvnyZZ/IeXJyAuBBQfEuLJ9T+dzkscLoLAfylPtV+k/A0IWFBba3t58IE5G5U+7TD7ITywweGT/CfrTWBbEIAHBwcOBTqFutFru7u6yurnpwWebJ1taW91ybTCY+xEDAsm/FQBXgFnjC20xAXkkrlvOSn+X3D7I4hYUsDCNp+zzPuXLlChcvXiTPcy/z3Nzc9AE5w+HQMy2Xl5eZzWbeQ03828QmodFosLe3x507d7zXoqwxx8fHpGn6hC9bvV7n6aefZjKZcOfOHQ80CrBSlrXLUQ5PkXVLQN5yG8hzxf/00aNHftwkSeIZgq+//rpnGcv8k7CSn/iJn/DAnwB7R0dH3Lt3j+3tbXZ3d+n3+8zPz9PpdKjValSrVb7ne77nCWDl4cOH7O/vMz8/75OpkyRhaWnJ95Wwu8rhUfIaAuplWcbS0hKbm5u88MIL3L9/3wd3tNttBoOBD8goe9fu7e1hjGF/f9/LtGu1GgcHBz71XTzyhIUKcP78eR/q0+v1nlijZU3udrscHBxgjKHZbPoxJPNXvDSlTwQAk0CqTqfj23Y4HPqkbtnMEhBOAF2RlEufyGOE6T03N8fq6iqdTofl5WWUUty+fZv333/fj6GNjQ1Go5GX6O7s7PCv/tW/emI9EmBXAE2tNb1ej2az6ZnEApaX2fEyVqUtBAQXAFQCSKQP5J4ozx8MBuzt7T1h9yHjQDaHZINI7scSpra8vOyvSQBlee1+v8/du3c5PDz0QKww0MWiIo5j5ufnuXz5smedymaitPN4PObWrVt0u12fSl2v16nVahwfH3Pr1i3CMPRtX2bT/u8dv/8BROsYeLqawTDAhNYDHEnXAVt57NISs8L6IZi69GRXwICeuhCMqF+AdEoxW85gGjgp2VB7fyWbOOaVUpBXjfNwmsuxOsCG1puzYx2QFxRhIuDYicIM0TMHFpgIKvvKe8ehYXTGAYTqWBcyR4WJrQthUaBaM2wawEQXIKV7j3TJXXh47CTNkthsAxyQNXVhDcGsKKzM6SJvCxaXsKBUkT6phJ1WtDUUACWn0i9bFNazjkJNHHurdTvwgTA6s8zmDWvhmFeSB+ylLR6EV4rXdnKx3sUKja0ZwdQybeFkuTYhT3ThKaYIVpqohTrH16qYABZma45519L+3Fy7O5BAWSC3BBcGjLOItSBAFwDhTl4vfOQCNidz6JEmRzEqkmorKuCjjbu8OToHOM+3AMvIBrw9Pstnj1/gC194EVUznFwGE7pE3aNPTflvP/53+ev3P8Phr55l4d2cgxcDoh5s/JpiwFmW2gFJzyW9zloBwzOWpOtAEJVD3M1QeY6aOX9EQo2p1BzDRlKS4wiVZuRx4WEQKUzNnIJk2oF6s8wFeAgy6AA9fGCDpIxmc8LqKxipsYFp4MiPBTvVeaZZ7z2XV/HyZZNr4ltVD4x0r1iCKZhMQ+LA9+m8m5OVHQcsJ28ETObmaGpFPHSeinbkGFZZ5sb9cDViMh8T9U/HajixBKl4vxUSfPDADRQbB9WC4ZkriPESfAEUbeRYtW7sW6K+C/dJ6yGTOTeHZi1FdQ9qO47tG3c1s5MG07YhHCuqO8r5A26Miasps3mXuDsLDUGY02iPmT5MGJxV/JHz7/D/+ZefpGFdGwbpKbApczGPiyTc2IErwcUBn2jdopdXCoZhzkIw4DBvcJg3CJShYk+9ECsqdaxZrGcYpgVFra4yn8Sc2oCJjSAxkCqGvQp2FBIMNXqksGNFY8syWA3JK4po6IJV8titVYtfCZn99gpz4kVXdevpeLlI3a4YbCOj0pgRRxmNypRmPCXUhpVKn7PVY47TGl/f/xAnT2sHeA9OE83zWsisEdA/p8hqdVZ+cxeV5ZiFOqZIcLdaQaDJq0HB5ksZrlUwkeGZDbdL+fp0nf2sRXQQkjYUcc8tzHnDkFc0tX1D76ImHMG040B4qxVZrUjzHbk047R5usQMN5zv3njZMliPmbY1ecUxwHVuOLmsvb9gZVcXc+w0FdlRYt1aLFJaG0Jad30vDHqTWFTq2InBRNixDjCctVxIjKmWUlUiixoHBANNMCoCeIYBpmoIF8ecXzrmxc4W39N4wAvJFs9FMT0z4dPf/D8w2EwwsQPt026V//Ar/x4m07TmRqy2+uhWj94s4eC4ycnDDr2JQqfKMTxj69opd8nXKPc/iv+BCxXJY+s3uNwaZtATjS7k2uLLGI4cq1CnlnBqTlnyss5Fyku0HcPdonKDTt2mltUu1GTa1kzn3CZO2jbY+RmV2oxWbUIrzKiEKWdqToYcqZxaMHXf9Ywbo1Wud9e51trh0WiONx6vM5uFmMOE+FgTDRwA2CgCakzsLCFmHcd+taFF1TJ0ZNDakM1CZtsJymiCQiJtfv9/+vru8S2OMlAUBIEvWqTwk8KsXDCfnJz4wA8pCjudjmclSoEuQIR4vZWTRYUlAngPJ2EdSIGrlHqCOVMuGIRlKO8h8kdJbnz8+PETCchS3Mdx7IGEsgdc2UtLvANFQizAZ5npIoWqHHJuZam1gJNlxos8Vg5hjNVqNe89JUwcYRVmWeaZd3KNIq+M45h+v+8B3yAIfCEq5vKHh4c+kEA8pwQ4FpDzg2xKYbfJdT169IiXX37Zg68CJEynU+bm5jxDRcDVwWBAs9n00nFwher58+eZn5/n4sWLdLtdbty4wZUrV1haWqJer/MH/+AfpN1us7u7y7vvvsv29jbXrl1jYWGBLMtYX1/n0aNHHjgSQFu8woSRJACg9FGZoVb2uhOpdafT8dL0sgRYQFwBh2VslJmlZQBI2DpS9MtYLbMLy6w5AfOWlpZotVoYY1hcXPThJ9LfwtgUSWO73aZWq3Hp0iWOj49ZX19nMpl4ibDIjAEWFxdZXFz08nBhCEqIgjCEZawJcCLghVxP2Z+vDLxprT2DSRKjq9XqEyzSSqXivei0duESItUWWwQBH2Q96na7NJtN5ubmuHr1KtevX2d5eZlbt2759xbfQbkOkeILyL22tsYbb7xBt9v181LG6gfZmjK/y3YE5Y2BsmVBnud0u10fBvTKK6/4NpR5eHR0RKfTod/vc//+fc9aFC/RlZUVP36ttSwvLzM3N0ez2eTy5ctcu3bNjy+Zn9I3vV7P+0HWajXu3bvHc889x97eHr1ez290dDodLly4wJ07d7h9+7Zfl2TtlbZW6jSgR8Cho6MjNjY2ANje3vbAdqVSYTAY+LR28bcVQEnYfxJqI3NhMpnQ7/f9XOh2u37Nl58lzEs8UJeWlvx8FqZ62fsW+F1sbOmv6XTK7u4uw+HQA4CSFCxAs0ib5fXlexAE1Go11tfXuXjxIuvr656RL0xaWavX19e5ceOG76N79+4RxzH379/3EmyR+8sYSZKEyWTC/v6+P5fRaOTZ5TLWytLyMngv7SzS4cXFRT82RSYs90QBTgUkLLPngyDwjFeRcFerVebn51lYWKDdbnsGvtxPRUouXosSgqO1ptVqUa1WWVlZ8T6NCwsL3ut0MpnQ7XZ5+PCh33gob4IJ814AUJGzR1FEp9N5Iuzn22EfwncCgIjzGkz7EbU9VzzkVSks3Af1cGK9hEwVqZPBtEhjLAq9uOsK47SpHAg5DBzbo0hwtMrJiLGKcICXlZmKaKUgrxn0/IQJzg8rr1oq+469pSyEjjlMnhQMkgnoVFN4q5M2XCFuYkN8rNGp892yyhXkaqpdaEYaoHshppkRnISnLI5U+TRMCbFQuSWvOMTPpf5SsDad3514w0narVXKAy1yOGP+gkEUuv+7pGSL+MoBjFcsBBarHKAnwS06B702pqYUubXMhUPyBFRufEKuCWHWDgkmxrVBomB8yoz0wFDk/B9tosgrLgwjrTmWmUuwLc7JnIIyz595zI2DZSIVMMgTUqsJMCzHfTSa/UnDpQpbTT93H+KqKia3mqsVZxD8dHMfgL+5/4N89msvEXY1tT3F8KwL26keZeSRovl6hT8//PeprwwZPpcxXQj50X/na/yB1vv8F+/9IZJfmSMcG6bNABPCwtszx3ozFj0XMmsognGGqcSYSlAAAcolCwcFcy3LIc/BGLJmDKpI6Y6M8yzEAYhhaFDKAeoONHTgiU5dcEI4dr/bwEnMpd+VBRXYItTBPSaYuSI+q7o5IXYAxiq0sujAMF3Oqa/3yQBzVCPcjsAoTNWgegF54kCS2bUxrY/32TtuYlJNuBNz8uEMFRjar1Vo303JKy5sJe47n0FTeGlOOprJojpNji2ARBuqU+ZhespUUgg70fp2ttqFFzn/Tu1AUCKwlrQWYWJnBZDHirTpNijyGMKnBoRhzvSgTrwXkjYNvWeLdN5uDL0A23L+ena7Qm4Ug8Qy/wD0H93n60fnnJxVJKcGjq4lDM5Z2rddsI5jEzvWUtqwfN/ZRwTKEqmMWOVUVEqOohMMSW2IxoGFqdXMFNTVjJzTYiq1AXWVMrQRKYaJjejoMX1ToaNHfN/VO3z9S8/QeBA6UCOGtO788UYXcnQ9I3hUobrrwC1hueVVg23kBJWMdnPEWmPAmWqPxWTAStQjtQHtYIRWllhlVFRKJxgxswFxgYL/R1//KZYszOaLIKFj5UCgSGOVIu7nZA3F8bKlvrNI43YXGzirBJUZ15ehYtbUxAND2opdgnTdsFE/5pcOP8Ebx2fZ6zfI1qb06xErX1HUHoSMzmX0L0BWDZks5QRj7dm1bs1SqKmT+dvQ3QPyxEmAs3oxz7RlvBSTHFuygaJy4MJwgolb34OJA7e8f6E6nUNZDaZJ4TmYFInJBRsR8Kw8lZ7eZ1wqssU2MqJaSgCYSQjTACJThCe5ORxO3Pj5Qz/wOv/Bwm+zEabUVMDdDF4bX+C/3Ppx3jtYodevYjONqlmCSSHvrs2Y9hLINL2dJv2tFmp2uomkLd4zNpgqop77Oa8U7MIiiMUWjHkAMjdn9VQ5m48JhGNNMHXrtvPZPfUPtoUXYR4XjMuCCR/38lPf3dgF54wXNbNmwSisWndPbqRU6jOWWwMqYYouQNZGNGWt2mU56rMSdTnK6xirGZmY47TON443OBzVObwzT3VH837rIsHUSe2jqvNjNDEM1w22kkNsiCqZW28DQxJlTNOQLNNEUU4c5kRhznG37taTKpjA3bPSJsTdb/2Z5rvH799DWGwiJS7L8wTkKwcLlFMXy95/g8HAg2oCbgk4Jc+Rwl2KPQGchOUlwJSwTOQ8pNgVUEQKCGF8jUYjD/aIdFqYHOLfJKCJgGdSUAqAJLJFKR7lC/AgEpxK8j7oGSfFrDzGGPOEd2NZPloGsKRdRcK1uLjI5uYm+/v7KKWYm5vjzJkzhGHI/v4+x8fHPkxEa02n0/H+dsPh0EtmpR1ETqmU8qwQYUDJdQgjSECx8nU/88wz/nmtVos4jp9ItTbG+CRZAVnLDC2ttU+DnU6n/MAP/ADb29u8/fbb7O3tedadgF7vvvsuV65c8eEtSinOnz/vvbi+9KUv8fDhQ65cucKFCxc4ODjg9u3bPlFagNRz585x9epVbty4wfb29hMyZvkuIKjI4SX0QSnlZYsyPuS6hJlTZhZKSEiZWQqnYKyABWV5oYC2Mh/kfES2KeCevJeEqQhT6vHjxywvL/twAmEJZVnG/fv3uXPnjmf3iF+azHNhEpdBCgGwBWiVuVC+Hjln6fcygC7zq8wKFn+5ZrPpQVTx+pOxJ7YCMi9kjPX7farVKmtrax40W1pa4vbt2/R6PQ8aLiws0O12vcebjGGtNSsrKz59VtYpYYWW52WZOSybADIfZHzIpog8zxjDgwcPuHDhAh/5yEe8H6N4zymlPIAuIJYwqUS2Kf0u8lgBjPf29vzvEoQjc3k2mzGbzbh69Sqf+tSn+OxnP+vBu0ajwe3bt/26OhgMuHbtGr1ej06n4/tOxrj0j6zLTz/9NCcnJxhj2NnZ4ezZs4RhSLfb5ebNm9RqNQ84CatM/P9ms9kTvrMSqFNmbousVSS/skEi5y/zrNfr+Xbf39/3wF9Zeiv3B7GwkE0X6Vv5n3jw1et1byUg7wl4VpyAgcfHx36j7MqVK7zyyiu88847RFHEhQsX/DyUdWQymfDGG2/4eS3X2O/3ffCMtaf+nuLbCqfyabkPCWNQGJCyJkj7yVeZVSig6sHBgR8rAiDKhpk8X9ao+fl5lpaWWFpa8qE5ogiQxHrxQLx165Zfk0aj0RMMSoCzZ8+yvr7u5cedTgellGck5nnO48ePn5hLohYQab+cc57nnJyc+PRxAXfjOObs2bP+PiRg42w28z7Mv5fj9z+AqCAYK/Qs9D5ROnU+g1Y54Cscu4LDe4wlLoTDhkUq5cQVzroASYKxC+jIqo7FJuwCVYBi4cgVNVkV9ERhI8eesIFG7zdo3rcMzinyumHWMQ64a2WEhxHhqDi3wDEthImgc6juO3ZgMHGy1qzqAiVUCMoGZDXHbAiSnLzlQCITO/BQj7VLDG0Y8rpxBu0KbOgKFSdtc+wN7KnM04GMRVPmoHiSVShSMVUwKkXSrIx1IRXWsT/ySJHOZ8T7LtjBhsXrFVK8P/rMmz4gdz06ZtZSBL0p828Vhn7GkLUqmCSgtm+ITzIHEujAMXVEnowDjbIaTlKoXZHu368AiFXuJKxpK+LfW/0d/i/bf4zcWnI0Wln6eZV2MCZRId1ZBTTM6xl/tP0658Iqh2bMq/0P85Nz3+DYjPnq3nl+/eZzBDfqNLt45ks4cmyl8Xzgz6uyEzL/r+o06prxMnzxb3+UL/JR8gpUCwly/7xi4wcf8uC3N2jdKY3lKQS9CTs/sMDxyzmX/nGN6MjdFNQswwZOF6emM9CarBY4hmdL++AUtEVSbrMs8ICqFPPBFKKBY5aaGHQBHmMFzLWOtSgS91yhskJyGEEyKPmyASYPaNaHJGcz+lstkr2AJLBMl3PIFCQGExXgfgJqs8L+fgKLU86tHbIVd1DHCWeeOqLxR2fc2lym+Y2IcOIYhsmJIZrBrOEABxf64uZgcmIwofPctIFIoGHWPmUCCygXDl2fCXtRFxJWZSx6qgknOcHMEE4s4UiTVTVJtwCyqprDbpPBpZSglpFfyCDVKG3pdIYoZTk6aBJVMjqtESf9Kulxhc5bIb1Llv/j+W/wP3z2R2gVjN5gZsljRTB1Hnk6dWtMVnXArA3ALsx4tvHYL3W5dfLt1IY0tdt1aOqJZxPO6wGRMuQ2IEdxlDfIUQR6SKRyDk2NiY04zDUVnZKj+T+vfZbXf/I9bk5WqeiURGWsxcf08yqJTolVxteuXqKqZyxEQ+bDAZ1gVHjAZQRYz3rcydq09ISJjWjqMakNGZqEip5RVzMqKmUvbzKxETcmZ6j+ToPeObDKWTS07+aEffcBe7pUIRgbzv7GjNFqRHKUYqpup1EV49kqhYk0WaKoHBp652OXxjtT/Ot3noWx86xUs4KhbBUnlzXtu4a4H2ICiHuWyn6AnkHl8PR+EY0MaV2jM8dE1JmleyEgTxQc6AL0gsqhG3/JsXUAmlJUDq1nC+axY97O2k42jXKbViqnALGL9Vc5+wsbWk+306kwvWG6nFNdGqFzB3gvtwecbZxwqX7AJxs3eTY+ZDur8qc+9+c8EJnVLZHK+X/u/DDfeLzBaKvhNmNqOWoauLkZWmxsCKbKyZgtmBsNKrkirRe7SIXvrvxfZQVQGLlrmrWelBTrmUJbCI+cl6SeuVCTaOAYhTpzDzSB8oFHVrvkcZ1T+Ori2cEmdOEwaVWR1QOm8zBdyqmuDjg/f0wrnlAPZtTDKXcHi1SClIVkSGYCQp3TCKY0wik1PeMgbZDojOv9de6evMjxO4tE3aK9C/A4TyCsF4FCVeO8GLVFxzlxJUMpS6c6JQrER9f556bGJdFrbZgFIdYqer0qZhJSeRRR23d+tjqzTNvKMUzNk0yq7x6//w9jDIeHh56BJ9I+rbUvdsrgoQBo8rcyG0Ip5Yv3o6MjL6GVokoKOpEzSVEuYN3i4iLnz5/3BeaNGzc8GNhqtbz5vvhGHR4eejBEij4pELXWNBoNz+qq1Wr+nLQ+DbaYzWYedJKir+y7KGmcZdN5Ceko+92VWUvy/YNMNCl65X/lonN1dZXj42NfEAl4pZTi5Zdfptfr8eDBA7TWdLtdVlZWEA9H8VETBomAKgLcCHgj1yS/S6Fa9p+U85Rz/8QnPuE9xwQcEgaetdYnkUZRRL1e5/j4mF6v56WZc3NzVCoVrl27Kr9jTgABAABJREFUxmAw4ObNm3zlK1/h5OSEWq3G0tIS4/GYdrvtGZKPHz/mzTff9GyhL33pS3zqU5/i6OjIS1mbzSYPHjygWq16UFVSZOv1OlevXmU4HHLt2jUODw/9GCkzl9rt9hOenRLQUJY9C5NJgCbpTxm7MrYEIJY5UQ7oEE+4MsjS7/efaGthCTWbTY6Pj9ne3vZegcPhkDiOOTk5YWlpif39fY6OjphMJmxtbXmmmZz/5cuX6ff7niklQLqw34RRKf0I+NAI+V0khQLiC2tQ5pacu7SJsClHo5F/L5GEdzodlpaWWFxcpF6vc+/ePSaTCXNzc6Rpys7Ojg9zEPmmeAFOJhO+93u/l8ePH7O4uMg777zj27Hf77O/v8/c3BxHR0deYi6M3eXlZTY3Nz3AIMCT9J8wq2RuAH6TocxAlPlaBv9Fiv3bv/3bfOITn+DKlSsMh0MODw85OTlhf3/fA52SVCxec71ez7eVsEr7/b7fsPlWskwBbGW9ff7557l9+zbVapU7d+6wurqKtdbLZCW1XPw9Hz165NmuZVauAHkSRnLr1i1/3nmee2m4PO/MmTNsb2/7tpbXEWZrWRYta7wEai0sLHh7CAGcHz586OW08lhZe+r1Os1m07++gFrdbvcJX1oB7mVuCdAm968wDP39Q8aVeOSWWYvNZpP333+f11577Qkm35UrV+h2u7z55pveKsEY431jhV2XZS51uRwaJmOmDH4KkFdeU2WOy1ouoS7lRGdhdh4eHnrfV2HOSl9IPwpg3G63OXPmDIuLi96vsNVqAaeM0JOTE+7fv+9Tj2VsCrAnG2RKKc6dO8e1a9dot9s+LOjk5MQD39/85jf9nBdmpXw+EKBX5qG0j4whUT7U63UWFxdZWFhgcXGR+fl5lFJcv36d9fV1jDHMzc3x+PFjz1r9vRy//wFEXCEDjhWV1RyjozbST7AYgpklmFrSumOw6BnEJ8oFWIjkOAAbWPICKKGQB5vEEh8V6ckFsy9tOrZQOAYGjkXYuq+IBznRIGfWiclqmnCkmM0ZovoMU82Y9GJU7mTRVaV8erLKXBJrVnFMn6Awhq/u4dmTs1bAZNmQq4j68pDJOCav5k4WZpQz5wfU1Embs2bB0CnkW2wlZHV3/jZ0vlKu3dy1KlMAmpE+BZ2KoA9/WAdEYgREUgSpIatq4vaY+HbkXl+BNo7BM2lrPtN6i9xaUpXT1GOyCuStBBMH5JWAyuMB8f19iELi/So2CtDjFDWZEQ6aoBV6nGKjgOYjSPoh8fGU3lN1Jym30H1a0boL0dgVtOE45+SphB+vHfOfJymBUpykVVKrOcwbTGyIwbLfb2BCiwGuTze4FN0iUZpGOOVfnLzC3xgucPjNZcKh8mxRB0g4n6+slTtDfgXjZyZc2djl9pVlXtzY5MeW3uEr3ad485efp/XAcHw1ID6BpTczRu+us2QNwcSQNpxUOx67G1FzK0fZAGzuCulAFV5mRagKYEMHaCjjwGYfHKAcuOy7TIKuDJiCCeTCUBwwEKTGB6Koginl9M04dlIu/SzAsxsHedUVzijL8VGD6GFCpGG6kGNj61hR2jWUiRxbKatCtjDj2lPb3N5Z4sH9pQJUUfR+Y5W9JUs8gcZ2jolcMnlW0URjJ3GetVXhE+jsAkyofegEBYAc9xyYY4qxG8zc39OaAq0c83gmjC/lwfFoWJjVWnsaupDDtB2gLLQeGIJpxKwTEo4U0cDN25NrMbaZUbsVY4OEg8sB1caULFUkXcOZP/GQrx5fpPFAu3Rz5UAYEzoQKum6cIVZQxVMXzCB5eL6AWfjQ+p6Sm41dT1FY9zvaJp6QoAlVilB0d8jEzG0MRWVUtNTYpzsuaJy0BPy/LT4mpiIQBteqDzi5cpDhjbmKHfeSzU9JcAxH3+sc526njKxEXU1Y2gdO1ceB85/saNHBIXhZkWlTkYdTkkLQDNHMbERscr5F1svUD0wHF9zYznqKSr7U9BuIyaraCp7E3R3RPV2io0j8s6TSc8m1qT1wIftTBZj8nruUnn34sKOwjHUC/tIbGjpXtLEfYq+dczVtOkGfuXAeXfWtzXhxBL3cycx1orkxAVayREWG0F5BZ/cayKY1ooUewqgUCTLE0Vy7OZA2nRgYd4+ZQ0LS1HFBh3n5L2YcDf0yerff+4On+68yzPxLs9FMYE67cuuCfgXkwuombvvmcSd2z/96ochcsxwKgYV524dSQtPxJFyacuxaxtT3AuscZYbUID+5nQj7XShcHM6PtFEA4oU8WLuTCAaG4KZeeLayt91bkG8Sa1bk7JEMZ0PyBpFUvRqRnVxSKs2oRmlrNe7rCQ9Ep0R6RxdLHoGhcZSbaeeFV3TMwJlaAQTanrG1ET8+r1n4ett/55h7O6TWdViEwOhQYUOLAy0YaE5JgpycqMJtPHc3mqUMs1C+pOEWRYwHiaog5i4p4n6UDmyxEPLUi8nHGbodFoEqBiwlsGZlmOLfvf4jjtEhizAmxQ2ZalvmWEnxZqAPQKiCOgoEiQBYaQQFTmSgE21Ws0zHiuVCq1WiyRJeP/999Fas7y87J+rlGJnZ8ez2AQcFJahyJgFQBFGS5k1lKYp8/PzHigQnyZh7ckhrAYBJEXaLecrTI4PeutJW5YZMmWw6X9LUhvHzqZjeXmZmzdv+oKozKI5f/4829vbXsol11f2sxPvOfFtlJ/LxbdcSxRFNBoNHj58SKvVIk1T1tbWWFhYYDAYeKnjZDLh6aefZnNz0/vuSZEr7SGSwnJAhIyBubk5lpaWePrpp31hPh6PPbAlbElhtpUZrVeuXPFejaPRiK997Ws8ePCAK1eusLq66n3zBoMBly9fZmdnxxfhR0dH7Ozs+ETUcv+WffwEZJDUXmGrCihcDlIps2XlHKVtywCtzJHy86XPpe0lbbl8PtIOOzs7ft4JI3I8HtPpdNjb2wPgzJkzXpooDJ1Wq8X8/DwnJyc8evTIMxsHg8HvStoVz0rxHBOPTznPMqhR9vsUELbMMC77JgKegQf4YBHxRJybm/MSWgldmM1mXLx40QMuwhYeDAY+gEVrTbvd5v333+fg4MCP5zRNOTo68hJgAVyNMczPzxPHsQ/6kH4pA4HS/t9qzsr/yv1Z/lnYxPfu3ePBgwc+iKTsSyqA8r179/z5CntTzkPWUgGxy+OlzPiWfgHodDo899xzfO5zn+P4+Jgsyzhz5gy7u7t0u13m5uZ8WNXv/M7veOBYXrO8kSCbRuIV2Ov1GI1G3Lx50/eFWFgA3L59m1ar5T0qy+u4jFkBfoXNluc5+/v7T7A7gSckuOLVWV4rJ5MJBwcHHuwWjz7xuC3PTwGOZU2W8CFJjj979iyrq6sexNrf3/dgpfT7M8888wSwdf/+fe7du+fvj2IZIZ6SEiAkVgHyWhKcIx6Fsr5JWwvLVl5PvmQsyFwYDAbeg7fM/pUjDEO/odJqtXxI1srKimeAlqXDs9mM7e1t3nrrLQ4PDz2jXdpNGI/Cxi/bPYxGIxYXF7ly5QrWWm7duuXly7LpJuvA0tKS7zuZDwKwSn+3223Pnu50Ok/Ik4VxLGBxv99nNBqxv79Pnrt065WVFTY3N/1GzO/l+H0PIDqfQQcI5hXHrtKZKkIYcAWZxTEY6iK7Oi1yhJnogAQXXJI2T70DlYXKbkAwcYWUC1NxhZOEoVR3oHJsqO2ljBcjbNMt9smRAyfyiiIdxISHETpxHkkYd76ztgscCKaK5NiBennFgRyzSJVkmg4Y0akGpZketjBtg1IO9PRsEm2hmpOFzrMtjwv58jRwbEV1at7uk0GlDULlwzTCifMI0zMHQgZTd86S0mxDDUUqsIm0k2UHhrCQ0AurMZhZ+uc0l6MukVIEKCKVk9UdEJlXAqbtAGydOAmxoWZ0poINFPXNMcFAg1aMzlSpPYZwt0twpAi7hUw8LgCxUDFdyZgehcTDInVzmHLyTEzfzIjD00UiRzExEakJmdqU4UkVraFvIt4ervNHGreYWsMgS/inX/0w8XHg/CsLgAqc3BQL08UcPdbU91zgTP0fh/SrZ6leDLjx9mXeaTzNbDmjOq+wOmDpD2xTCVPufeUci29apk3F4Sdy4i1N+w6EY4ONQ+LjGSt33SLq0oYDB94CGFeEmmbF+/e59sSDf0GYk2Wa2SgmkM96gUPKJDzidA65sSxMKFdcy8RxukudW6KhC5lQxiVE2xCsVeSpQ4xni7lnibrzVKBdMnKeWKKeA1YqD2PMJcX64gmTjmPqvLDwmEjn/Ksb10jernJ8NSA5caEU0ciQHGfoNCDpK2Z1zbSjiEZFH8wX6epFF4+X3XqgDORV56tmldsUyGrF/J25uaz0aYBFMLVkFee3iYUsUSR9Q1bRpHUYryjSlmMUT5dyVDUnfhiTHGvUXkzadl5w1fcrZLWExTuw93HDH128xf/wWz9EJ7WYWD0RAiFyTOd9WPpQnsBKtU83r3vGH0CgDJHKya0uZMhjTkzVMRCDATWdMsmjAvxLGZmEisqYiA9iAT5WVEqsXTgE4EHHph6zlc6xHh0TYNHKOADQalIbMsE6mXRRQKQ2cK+lcs9I7Fnt36OmU/om5sTUuDNbwVjFhfiA/a+v0Kw6/zhloPbY+kTytOmS2cnMKVhec5sNacPdssT30oRQfzxjuJYw61j0yPnVKuu8A93GiLDciuCgqmVUwyUSF7YWOnWbA7IplFdg1tTe6iFLFPHAQsmL00SK7iV9yiwskpN1Cmrq7h9ZzUleTeRShK128nDmZujAog2YaQCpY5eqSsbaygmtZMJ7vbPF+wBJzv/tzOdpawfebuUjfnN0ia/3L/Jed4WTcYXBqIKNHCtdQGoVKnS3KMIyhQ1Cx1AumOc6dQxjneKZ5lnVSZBNxfhNEpW5NooGxeZbVxGOhcVrPOsbVdgN5KdsdWG7U9g1OqDVbZZNOy7QZLJoUCtTavUJK80BzWhCHOSsVnoEGBKdUQtmJDotxnbOyLi28HMCxXw4ZFKgt+1gzNRENIMxkcqJVcafe+bL/D+Gn8aOQnQjJQhzwtBQCZ3wP9CFH1yYEwc51dAFJPWmFUaziJOjBuooIjnWVHctjQNDODKE4wyVjkGpIixIkVUD0rpmtFwhrbl1tfkoZ7AWcPI9M9QoQIlm/bvHd9QhIJwUr1JwSjEuAR9SLImvnwQ1AJ49Mj8/78EtkSJJ4SvFq4QjAJ7ZNBwO2d/fZzQaUa+7z0sCGIrkWRhK4/HYA1AikW6329TrdS8TE4ZMOQRGTOMXFxc5d+4ce3t7/v9SgEoRVfZGExBGQirKQJF8F+C1XOSV5ZEiE5TnCvAkUt1Go+FZSiKfFVbgysoKr732mmcCCetPCu5er+clnMLOkgJVrk+Av16v5z0RJfRCCvk4jr1HosipkyThxo0bvyspV0Cw5eVlD3ju7OywvLzsPSOfeuopBoMBX/rSl7hz544PkBCZ2+XLlz2gvLu7y8OHD3nttdd49tlnfaH7iU98gpWVFVqtFhcvXqTdbrO5uUkYhrz22mssLy/z1FNP0el0vA/Xzs4Ox8fHxHFMr9fzAJgcwhYUlm273fbeZ8KoK3uNlQECAZAE5BAATUDUsqy9LP+XMSr+gGXPSmGS9vt9DyDIXPwgkC3hAqurq97jMkkSD4ysrq4ymUxoNps8++yzfp5ev37dS5lFzikspiAI/BwVeXt5Q0EAMQFKBSRM09SDmAIiyfXK2BbmlbS1JFhLmEWz2eTo6MgDFFmWsbm5ydraGoPBgE9/+tO89tprnDt3jjfeeMP3oWxGCPDbarX8+K9UKqyurnpfRpGhlw8BOWUcCIhUBkjLYR2yTgJPMBGln+R/0kZlNrYETJVZ22U7AwHfyr+XWZDS9/J+L774om978dBLksSzUQVwE5BLQPIyGCmAnLDUlpaWePz4sfcoLLPDykCxAMvyGtPp1DPGZD0Xb9Ky76yAS9JHsglUqVS87588T9jdrVaLM2fOoLX2c0PuVQIsSur6+vo6a2trvi0GgwGHh4fs7u4SRZH34zs5OaFer7O2tuaZwwKS37171wfHCPAm7HmZl8L6kw2IMtgmTO2yZ2Oapn59L0u8pV2n06kHyIQNLffTDwLdwk4U+fHCwoIP1xEwWBiFDx48YDAYPCFtttYyPz/vfRg7nQ5hGHJ4eOjD08oAZflcxLNyb2/PB8OU26VsbSC+t0mSsLCwwNmzZz1IKF63AkwPh0OOj4+5efOmD8hqNBp+E08sOILAJb3XajU+/OEPe6/X119/nd/r8fseQJRDp0XxU1PEJ5AcObaInjl/NxOfFkyuSLQ+FTQvWEgqLaRWQ7z0No+d5Fgkh7aQvYGTicVDS9zNyeqa4ZnIFcA1lyRb23XPwSpMFFLbUaQNRdpU1B47+WKeFN6LLScRtSFU9h0QGg4dkKCyAvwQAHQMrdswXgkYnTFEPe0AU6XIaxZTzbGRIWikmOOEZD8gm52a8OvMpVKawMkpo6FjEKb1U2+5actJY9OO83AUoA7wQRtWBwVjKmZwVpHPQurj02AUnbqwlvGlGRUPHgYs6DF51YItQm4SJ00LR2HxN/FaC7GRJq2FTOY0Oq0Qh5q8FjJaDl3YgQTjzCzLrwao3BW9embR04wPf99N3p41qUYpGs3MhBiriVRGpDNOTEZwGJF3MrSyVIMUDYyspZdVqT8ISRvWM/S0heTEcviSpf30MVcaA27cXOfkUkjlyDJrKT76p97k+9s3+JXHH+Xm6+dY/6zGBoZpW5H+j6sM64paTRHMcpKeovFuzHTRsXvi45lPt7aqCPmIAu8zidagDDYKyWuxZ41mjVKASuD8DyejmOqNhFm7kMpFFmsdgJ5HhYm/dYy7PBFpuPM+tJkDnsmdx2M4ht55zXjV0LpvUTOXrqxw7Fd/WFC1DJuXUGplMTWD1Zosgfqm5dabG5jEUNkNUQY+f6FDVEup1acMLoWo2FBf7HO2dczBuMHxP1slmMLx85b6pqK6b5jMaaZzBQA4deM6HLk5ZxI3n4Ox80N0gUVuDM9aDtQvg47BRJ2C5RMHlkZjJ81P+jlJH6pHmv5ZzXTeUr0bMjwbkF6aYI3CTgPn/1nJyI8T2u8FTNvwJz7+Vf7uzY/SvhE4pmwhsbeB26TQmVtn3HjHycw1tK4d8gNzN6nolJ2sw3p0xIIeen/DiXFMvokNPXhYUTl9E9MzFWYqoBOM0MowtBE5zvdTWIRHecOzGZt6Rl1l7Of1QnKcFknkcJLXGJmE+WBAbp30WdiJZa/FAEtbZXRNQl1PqauUE1NlazbHxEZMTERdT7kc7/Dq6DKL1y3HV53nRDBRNHayIhTFbWJUdyeuLazFdBqknQIs1xCOclRmmXUiBmuBB/KssgSS9qvxoVSmYMtJ1ojz1FMEx6cp9a2HGclhSlYLHNt2ZshqxbxLNMnMUNmbYpKArBoQTA3jxQiUCwiKem6Nns5DVi8YvaHFVnKi+oy8m1B5HLmwFQtmHJAsjlhojLjQOuSH5t7nD9fv0dQxGs2vjdr8n278aX/PCmLDL+x9ireO19g87JBOQuw0QMWFLGgcoHLHBjSxI/rpmTptB9xYC8agirlptQM2XTDYqXzaeQQrogO3cRYN3H3R+TmKr8EpM70cchLMjPetRePA95rzKJy1HfMyXcyoLQxZbg14qt6lGqS0wjGhNmRGUw1SIpWT2oBeVmFmY3pZlbv9Be4+XEZ3HStTp4q8ZvjxD1/n5cZDwAHaNTVjZOLCh3NIoCz7WZNOMKIZjDmzfMJgkhAGOcZoKnEh0VSWSRoyHCcc9xKCk5DkUFM5tFSPDHPHGUvDFJVOMHFIXg3JE+1AwqWKt0bJajiGfc0WNiKFNH6k6F8M+Mgn3uczC+/w81/8SQcgflfF/B11CEtPPqhLYSwpqoAHUaTAKEuxysEh4/GYzc1NDxoIyCABBiJ/tdbS7XaJ45hms+klfeLZJoWQyIfFG67X63mmkRRswo44OTmh0+lw5swZRqMROzs7TzAqxGcPYG9vj8lk4lk3AhpVKhUvuxL2VbPZ9CmjZe83Af7knAXcFImeFD9SuAsgKQDk8fExSikPjPb7fZIk8b6AUoCvr6/Tbre5desWgAdQRaZY9tITIE8eB3jZq1yv9JuE3fT7fbIs49GjR56pIn3+yU9+0gc+zM3N+TaR6wG4cOGC75eyR1sYhj7R1VrLwsIC1WrVs4F+6qd+is3NTX7rt37LMw3b7TYXL17kQx/6EFeuXPH+fb/+67/u/Q2HwyHLy8s8fPiQTqdDvV6n3W57ViOcpkHLtX2QJVpm1I5GI86ePeule/KYPM85c+bME20pwI8wksrhQ8L4lP9LOwsQkSSJD2JYXFx8Qmov4LYAD3LI+wrIK16K9+/fp91u8+DBA8/UFQBlb2+PN998k263S7Va5fnnn2d9fZ0rV64wmUzY2Njg+PiYr3/963Q6HZ599lnCMOT4+JiDgwMvZy37MwpwJYniZQBJ+lR8FLXWXsacZRlnz57111dmSV66dInZbMa7777r5ZIiRVxYWGAymfDCCy9w69YtLly4wDe+8Q0vHZW+EOBH7AgE6L106RIbGxu8+eabXhIsoLYAhR8EeuW1BFwRwL/MPpS/y3tLuwg4KONG2IxliwJZ/4Tx/cHXLrO9ZI6Vk3CzLPPt+yM/8iM8ePCAXq9Hr9fj2Wef9WwtsYgQabKwfIWdXQYn19fXsdb5eh4eHvqkarkXCOAp41DAY2GcCcAk66es42XQp9/vI1YWwkgU8G1xcZGDgwO/lst6u7a25ttL5mWn0+Hk5MQzKz/ykY+wsrLiZfgC9iqlngi3kfc6PDyk2Wz6JPMsy9je3ubk5MSD2iKflzFxfHzs56EAqB/07I3j2N+PynJlYQbKOUynU8/EnU6nHtyVc5a1RcKHAA+gLi8vMz8/7zd5tHap6oPBwIcPHR4e0u12n2D/A8zPz3Pp0iUfpDQ3N+e9LGWMv/nmm3z+85/36+UH+1bGpDGGu3fvMjc3x/b2tu936QMBNiWARXx9syyj2+0yHA559OgRBwcH7Oy44EhhS1YqFZ566ikPtssGpnzJWnr+/Hl2dna4e/cuzz///BObCv97x+97ANExGlwASdwDHYjcyv3fhqcBCuKvpItqxwVuOCAhmDgmYJ44UEHYi8HUkleVl0OL7FdnLikyHDv56bTl0jbDsXicufePRhZzAqZIh3QnhQfyqnsOPLAaZgvGSaj7oZc3xj0no8xqjknlpKBOHjXJFQSWrOk8EOOeS8JUJwk2gMmyAyexOPZHYeafVV3oRlbTpA2o7inGy65NkuNTpmU4tszayoOuUR+mHeen2NgsPKxCxzSZzeXYfsRgw8nIa9sFW08prpx3AzvHklvLxAZO4qqKcBrtGKKuUxzbcdYMyasa3c0Jxzk6C0kbmmjogmSCGYyWHQgWDUtMlwIQ0qlhulTj/7r2L3ljukESul2icR4VUsqYrx5f5N3BGpVDxezszP8foG8CtkZtZm3Lpz59nc+/dY3m4pDpm3OkLcWX//h/xT/pP8du2uIG6y5d+NgQDxRf+uxL/Mbi85y7vMvq83tsx0tEKyNeWNvmG3fOs/SbMc3NjMPnQnQK8+9nzLY1aVWhxxmmGqLSQpqu9Cl4CKhZBlmOynLyqvO5zCoKG0hhb1HakueaKMmcNFNzKpfLHTvGBaTg0skrgZ9LFAWvlxtayCuGwxc00dA9KBoYguEUG1ecHFLbU8AwtFij0FHugLXUSbtVLSNrBIRDx7YzzZSXLj/izXiD+ZUe44MmwXt14l1LcMXNuf7ri1xvzAPQiN0mQF7PGZ4FjEsot8/3eWpln8Nxjcc7czAI0fNT8l5M/X6IyhxzMOoXQIw5BUQEfHY2BpZZ09kH6EyRVRXpWHH44RxVy6jcrNC+Y4pNCstwrbAeeFghr1psxVC7F5G2IqgZ4p5l/MdO2Js2CX67XQQRFexNJYCna2cTOfahhBxNFwz/4aWvMh8OaOkJQ5MQcbqLBRCr3MuHa3pK31RAT6gUgSVBwRzs6AkjE5EW/onlkJWjvEFLT5y8uXidHEVgDTmKupoxUwH1cEpqwwJsdJ6LFEnPExsSFc83KGYEnOQ1tmzEUdbgQnzAghr412+qlP/2mz/IiqJgzVqqe5qol4FSpLWQYGac910lQmUuWVenzkfzlAVtGC0FpE3oVzV5pfCrjSw2dmNZT5X3qxXwUNjcSu4FRbPmsUZPM0JAZYZgNEO3KkznI/JYUT3IPEPSRIqjZxLSlvMLnS5Yhk9nBDW3CWFzjVagCiZwEBiyak4ehwQThakY/scf/iU+WZmQKLfeHOcjXpt2+NroKd7tn+HG0RJ64sa4cfk+/OobL0OmCAYB4axglsMpaxh3L9SpW+9REA4VknxsA8d491RD5UDGcOiAR5+APHahJTotpIEFm9BJuQvf0FxAWXd/sgFkVc1oKXCBJgvOAzWZH7Ha6XOx3qMapNSDGa1wjFbWg4SRygmU4TitsT9r8HjUZrffYNCrovdjgokiLBjgcz1Lfa/YJEodG/jr5zb4aPOuZyOmNmAp7HuZfWoDmnrCfDBgP2sxSUPGk4hsWsUOQ4KxJj7WVA4sSc/S6BuiQUZQFHU20GT1kLQRMFqOSGvKqwScL2wRMhZa5x+riwCZ0G3qqNCgtCVNIubWurzU2mQ+GPDd4zvzEIahAIAigRWWCOAZh1KYSBFcNo0Xdkmaph4IkCJQCqRyoS4FtjBKBBgTWe8HXwvw5uu9Xs8zIsoSMGFDCeghX4A3nxe2Tzl0RYJD0jTl4OCAJEm8T5YEtJQDW7TW3lNKACDxU5QCVGSlAhQIyFIOnxDgrd/vc+bMGV+0NhoNDzAtLi5ijPFAnLR9nuc0m036/b739xKATUDdspRUWGoC7jQaDc+ekXO/d+/eE+zKa9eucXR0RBAEHpAQptDy8jL9fp+lpSUODg48QCjMkkql4plhlUqF+fl5nzp75coVDg4OeO2113j06BEvvfQSjx8/ZjKZsLm5yfXr1z0DJY5jzp0758Ha/f19XnvtNXZ3d/nwhz9Mo9Hg0aNHvPfee57BJO0skls4LYTl/AQEkWTSD/qJCZNG+rjsn1f20RP/wjLgVmaaAjx+/NhL+Dc2Nmi1Wp6xV2btyVwspz7LuQwGAzqdDjs7O3S7XZ599lniOOb4+JharcbBwYEHZ0ROfPHiRU5OTrh7965nnwpAJmOw3W57+wABbDqdzhOhQwsLC2it2dvb8+B1mqbs7e1xeHjor0W8DyW9e2FhgRdffNHP3Zs3b5KmKY1Gg6WlJfr9PufOnfPJvuJrJjLHZrPJ/v4+BwcHXL9+3YM2gO9juV7Z3Gg0Gly9etWH6wgwK9f8QSsBGRdl/1L5Xb5L/5fBmTI7UNhXZf/L8ronAIyMnW/lryiPkfkta7L0qTAtX375ZVZXV/mVX/kVv+kwPz/Pe++9558jKcnleVAO6xG/v06nw+PHjxkOh14uK6zR8rmX2bDC1C6HXgnbcjQa+YARub6ypYUwnEVue3R0RKVS8Sy6yWRCr9fz5y4bUYAft2ma8olPfII4jrl79+4TjEOttWe5CQtP5vPDhw89mC92F8Kgl+uRcy5bckgglfQDuJRiSRYG/P2nHBQyGo3Y3t5mNBp5/9NyeArg11WxelhaWmJlZcX72co9WWwfHjx4QLfbZXd317MKxXu20Whw6dIl2u2291AEZ9UhTMTDw0PCMOTo6IjhcEiSJLzyyiu88MILvPrqq575Kecm408Y53L/kbZYXV3lypUrnDt3zreRnN/BwYH/m8jZZZOhWq1y/vx5v7Gwv7//hP+n9El5jZT1cHd3l0ePHnmrjDJD+H/v+LYAxL/6V/8q//Sf/lPef/99qtUqH//4x/nFX/xFrl696h9jreXnf/7n+Vt/629xfHzMxz72Mf7m3/ybPPfcc/4x0+mUv/SX/hL/6B/9I8bjMZ/+9Kf57/67/46zZ89+O6fj3k9DcuIKb2eAjwPJKkXhWASCBNPCD2sKFJ5s0dBJK21VeQajDRVGWW9ur4scCF0wh3QuiZGFPBhdfHfvawP3XaRvVjnzeJH0uiRmd24mdKyjcATKKqzS3v+QAhiNhraQ6DrZrA0cu0/lxWvmxWvFlum8JT4pAJsUGvdDRmsGE1oPJOYVmCwopp0iTVQBRpM23Pt41koIkyUKhJYi1MXJ18KxctLDvGD65WBjS9AP3PUp5SV0aVXxkYUH5NailcJg6OiMYM6BH5XdEdEgBmMJu2OyuRo6s8QD5w1oI+3A38Ij0oEILmE3bUDctT48RVJOrXZMmL1XKpwNM/7X/jxRgRTM8oAczUHa5M1XL6NTSFKoVmeedWWAnbzBRv2YyidTfuPrz0Ng+Vsv/n3+vds/g8oUj/KE3zq8wk+vfo1/EHwvGDfmTOhkrmFPM/jHZ6geGubnNSqvc7txhUrLjTkTKdKmpf3CIduLC8y/7cBmlefYKCYYupAUkSv7I3Ppy2gX8uHDTHTRV1Y54E4p6rUpgwsau1V1kkVtsZmTpQczx0LUOIl03IW0WbyHyGsjg81do9rIMp1zTMZgasjriQMhjUJHBmOVC/OpZFijcPZsRfiNdVJSE1tUIWNuvB/zpj1HvBsSr+csLvW5fOUuX3n/KZhqdDPlhz/yJl949DTjYcxkIyN7VGfly44R2zvv+rz6b5o81E3Gy5aowEWm1RAiw/TFETowXFg84lzjmFvdJZSynIyq9GcR070a1S03Zq1RzNqW5ERRf+w+jOSRoro4wlrF+FxAMItoPjD0L1p+4lNfY5An3DhZoTuu0KmNia/k3N1dZO6LVbpPKf7UxTf55S99P/M9N2+VKRJ5wfs2OvDczW8TuOT29qVjNuJDDjO321ZRKQZd+Ag6f9KKcjfn1AZeRlxROSemQicYMbFRAdBoIpXT1DMOC6lzU0+YqIxOMCrk0IqpDYgxXqqc2pBmOGEhcKxHAQtFHqpxVgACAo1syJapMDIJS2GPFhNWQycHisk9sPjIVOn8mwr9cwoTGVSuaGwVRUQ1IKtron6xO9ufoMYzqMZoa9EzTdifurHfiBitKtLW6fqkcohH2svXHcjkxrOX6Bp3H8gThak7tlieWMKBJo/rRENbgGQJ1d0JlX2DDTR5otn7cIPJgnu9tOnOPVua0ZobMZ2FZGmICgyrc12e6ezx/Z2bPJts8dr4Ev/3L/+YA+xziNpTXop7fHVa5+/tf4I3D9Y46tY9k9dmGpu7uZnVHb0v2KygC7sKnSrEo1ZnjuHrUpxVIdV3IKrVhUw7spAXEuS+8l6uAhZG4yLYxPK7wrPAtZnKLUFmC8sKt3GT1jSzlmKyCJPzM1bPHHCts08jnFEPpj75uBk4FkqiHbPw/mQRYxUH0wa3jpc4Oq7DYULUKwDfzA2wmqYA5mDWseQ1QzDS1PdkDlnSRHN43HDp5EqTF5uDNT0lVrlLKVeGikpdAnkw4vL8AW+nZ0iPK8y/GbD0jQF5LcQU/Zw2NJO5hDxJnC9xvdjkC1xYVx47kNDExk2EwEJgUYEhiAxKiyebxeQaXWzuzHJNEmU0ggl1PYXTevi7x3fQUfbzksJKisaynAl4wu9QmIjClJFDALWyr5cUlMKeKL+usLWEjSEFtJzbbDZja2vLB5k0m00PnMn/BfSQlE1hLwhDTYpjOT9hyAlIIqwbARiFidFqtej1ek8U/QKyCngjAIP4/0lSZDnxUwA/CX6Qolu8DBcWFp7w2RIgyRjjGZpHR0e+TXq9ngcx79+/z8LCggdeJYxCZOjC7hLAUK5ZJG+SeFtmWWmtOXPmDBcuXOD69eseYJKxobXmwoULvPPOO7z66qsEQcCzzz77BJiQJAnvvvsujx494gd/8AdZWVnhzp07XL16lSRJ+OIXv+ivR6TSzWaTIAg4c+YMnU6He/fuef+4W7ducfbsWdrtNnNzc+R5zsLCgpfZn5yceNmqgMLAEwBiGfwRoE+YUgK8ltmVe3t73vNMjrIMFBwgJCC39HsZEBT5pQQDCbu0Vqt52aIU64AHIstsYAGw5+fnPeAskmcJeDHG8M4773Du3DmWlpa8t2WtVvMgw8WLFwG4f/8+q6urVCoV7t+/z/r6up8jWms2NjYwxnDr1i0fcCFeYyJNXltb45lnnvEgf7/f90yi3d1d9vb2aLVaHB8fe0BL1hQJ1Hn48KG/3vF4zPLyMleuXOHw8JCXX36Z/f19lpaW+Cf/5J94GwFZV2Qey2aBzJ3V1VWq1Spf+cpXnvAHlT4ty9PLY0P+L8BReaOhLGuW95bHlsFkeR0BA0V+LozXsr9heU0URqB8yTUJE0vOJY5jPvaxj3Hjxg0ADg4OvGRd1h8ZowLYjUYjb4Ug42w6nXLhwgWm0ynHx8eeGSkelTKe5PqEaS4edtVq1a8nsl6KdPfs2bMsLy+jtebRo0fs7u6SJAkf+9jHuHz5Mnmec+PGDV5//XXG4zErKyvs7+8TRRHz8/PepkDGrYBVDx8+ZGtryzP+wHmBZlnmJfH7+/uepSjMRWutDweRPhJQtQwIy/pcZjTKvUykuHKPkXEh4Fiv1+Pg4OAJr78ya1nas1qt+nCQdrvtQ0LKTL3j42Pu37/P9va29w+Ve2OlUmFhYYGVlRWuXr1Ks9n091e5dx8dHXHz5k1//xMQVgBxuS88fPjQy4KvXLlCq9Xi6OjoCV/J8iFjYTQasbGxwcbGBktLSzSbTb72ta/5RPQkSXxbynOEuTqbzbxfqUjZ5X5ZZu5KG5aZ4zIupV9lPH87x7cFIH7xi1/kZ37mZ/jIRz5ClmX85b/8l/nMZz7Du+++6z1W/tpf+2v89b/+1/mf/qf/iStXrvBX/spf4Ud+5Ee4ceOGT5z6uZ/7Of7Fv/gX/Mqv/AoLCwv8xb/4F/nDf/gP8/rrr3/bFwCuEAKKhOHTvytbeEEVIKKyp2m9KFewWwXJsUsTzqpFIjIOoENDMLYkJ+61osGpZEsYc2JhJP54eeRkcXkRUmFC9URibdxzRZuTNhWSRuuAmbinmJUuIJg5IBJcYWe1MGjkb0XhnCmsceBR2jQEY0UycCzDYKS8hE2ZotgMisI7tFhlGW8Y5zWoVCH1dCxDExU+WIqCDWOIjwOympMdEjnpZ1qBoJkS7lYJxwUzq2ADzhqKK5XHpEBTBUQqYF4bOq0ReaWJykP0LCdtxgTjkGj7mAioVGIHoKWOmRQfRJhqhJ5k5PUYG2rirvbgqnjaOU8uUKlh8tEBbV3h0WSOS41DNM5c/9Z0ld/av0J1ryjEc2hWpmgsic7QuKTbTjTm1a2L1O8F6E8esxSMqW1rxquWusoItWE56BM1ZoyXI/Kua+vWpWP+02u/zm90n+WbB+v86Y1vcnu8zGdffZnO+07GPlkImH/HUPlym7hmmMxransWNS12VAqpoA21kzRnBZBojBvPgfaso8mi8xrEKHSSEyeZe6hVKGVdwRs4+TKFNNkEpx/gomGGDaLTeVPNsLMAmznqUjAsfN4CS/VRQNSbMl2oeIaizYv31xaTuWRikzkg04Oa1oURKQtp3fXT6tkjdrIFdjbnqWxFfLUzT9xX1HYU1QPNr5+8jEoVpp0xyzXf89HbvDW+7LwWjWHubJfJLGJ2v0HeztGDAFPPmf9ahFWQNmKyBuweNthOzpHVXZpt2NOknZx4cUzz4ohpGnLyuMXTlx9zrb3DG4dn2T7okA9DOK6SbEe0Dx3L+eSKZu4dy+fvfC+D84ZwfcSsmzA8mqP6zAn5YUI4hu//sTf5tc3n6LyjfYhTNJRNhYLhlVvyQGEKawQHlBh++Mx9TvIazWBchKTkHjAEiFROXWUMbUiOLn5PfSJyjCHltHiRABMArQwd/WQQijxG5NCRyhiahKO8UciVna9BRWX0TQWtjH/uralLb14IBiwEAy5ER4wKvf+MAGM1M+UAz1Yw5T+98UdIupbuVTde42NnoZAnDhAPppa4O0OlOWqSglLowQTSDFurYCsRJtF0LyZgobrr2lNn+Pngk41xIFNWVaQNx0rPKxR+nxY9dRsieuaeOGsoRiua8Yol6ivqm3Xm3xkwa8ds/nBA+8oBC1HGfreBulfHJJYgNrSrE/7QU2/zo423eS4OidTpPSy1mrcmKWrqgF8bQtpN+Mjn/yN0ZAjCnCjKCcOc2SRyoUX9kGDmWMIqd6CfBJgEU4WeyTptnUw2crYD7oakCMYOJBRvR2HcxgPj/AqLjTW3EVVuN0swzYvzFDBSk1cUw5XAeRUuGfKFlLiWstgecLl1xHr1hIpOWY56RCrnIG1ymNYZ5zHDPObWbImDcYO9boPpQZXkIPBAobJQLTbS8sQlH+c1A5FBV3KUtoRhTrMyo5HM2D7oMH2v4oOgpk1Fu+Wk+k09PvXqtM4H1KWNK4YmcUCinvHnVr9AvuoCff7nlz/KW9HzZBXIGsXmTlzc97KCMRwWIGxgUXFOmGREUe4AQqNIZ5KY4/xeQWwdLHGSEQU5xipm44hKmNHUzpOxNEW/e3wHHeJNVg70ELaEFD7lIuuDXl1lsENkasL0ksK4zGSR0A0BAMqv+0HAUor18vOEySCPlf8fHR15kLFcHJYDIoTlCDzhcSWFixQ+IhUcDAa+HcqFoyTjHh4e+oK2DA4IkFaW/Ja94URSWKvVyPPcp0wGQcDW1paXUi4sLLC+vu6LU8AzL5VSnqnY7XY9m1IAH7nuTqfjvbNEXgj48IOjo6Mn2HLggOKLFy9y5swZfumXfsknm25tbbG0tOTb9t133+XmzZs899xzXmJcbuvhcMjly5dZXFzklVdeoVqt0mq10Fqzu7vL4uKil/+maeq9GYfDofdTFG9LY1xa+IMHD3jqqad46aWX2Nzc9PJvYXyKdF1AnHIhKv0n4MpkMvGg3ubmppfjyVgQzzORwUsfSHEtkkNhF4okfTQa+bHf6XS8X+fy8jIbGxvcvn2by5cv+wCD8vgrAw7leSDtKrLW8XjsfQnff/991tfXvRQbYG5ujqeeesoHJRwdHZGmKRcuXGB+fp4HDx5w4cIFD4xJwvXc3Bw3btzwTFvxl+t0OozHYw/mgwNYt7e3PfBx9epVzzrc39/n4cOHvPHGG3Q6HUajEbVajfn5eR4+fMiHPvQh1tbWePvtt2k0Gpw5c8bbAIjk3lrL66+/zvb2tp9/AqYICCh+oTLvNjY22NvbY3d39wmwrsz2k9/LkvGyFUMZWJTnyM9lsFfWv/LjyuvmB58j71X2k5R1sMxElfVL3kPGxNmzZ7l27Rp/5+/8HQ+ISVCIgKnC8BbQuSyRFjBTrBsODg5oNpsewBEgtyzxlg0J+V3sD4bDoQ+KkTlRq9U8iFVeS1dWVqhUKty4cYPd3V3/fPFlFYmtyON7vR57e3v0ej2CIOBDH/qQD3Wq1Wq8+uqr5HnOYDDg5OTEM/Gkrctzqd1uP3H/EHa7SKZlPpV9LyXkS5h9sqk1GAzY29vz7MUyY07Wb2G6a629zcWZM2fY2NhgZWXF2xAIeLa3t8f169fZ3d31MnPxUux0Oh5QbTabXoYv9iJ7e3scHR2xt7fn36tarXq/R2MMo9HIb7IBPgldgMf5+Xm/wfFBz09hIJbH9O7uLlmWsbi4SL/f5zOf+Qwf//jHeeutt3womQDf8jlC5okcsmkg/STAa5mZWwb15edy0nd5Tv9ej28LQPzsZz/7xO9/5+/8HZaXl3n99df5gR/4Aay1/Df/zX/DX/7Lf5k/9sf+GAC//Mu/zMrKCv/wH/5D/uyf/bN0u13+9t/+2/y9v/f3+OEf/mEA/v7f//tsbGzw+c9/nh/90R/9ti4AHBgSpBY9c7LivGBfOGaeA7TAsYDySKG1Y62JV58NLNaqJ3zhwtQV+SBSZ5DAEYNjcDmWoGM3WpFOB0LaK5gaRS0pHnq5KYo27c4lKvwU84p7fZHcCWtGgMhw7N4rq0DSMy5FN4f4SJNXrQOL3NliI5gsumsMpqeBEsHMtZXOID4pKOiF4X4wUkWKpmNmZlUHbOTTU3alsk5GHRR/y5PTsJmkMkONC02owkmmc8t4FZ5JHtNUGo0mt5ZEhSzVB4yjNlk9IqsGTDuavKqphBoTBYzPVMgjRetmn+C4j63GjNaq1DZHLkilm9Bfn3MgrIx5BVhbsIwUn7hwj0g5EANleGOW8eajs3zj3jni+wmJcaEHJoEkzEgLSkikNGthl6kJGfYqrG4a4tqYmXV+WINLTuI5SAsjc22Je9DYyR2Y8UstfrHz7zp5W6j47zd+lOTpHvVzPboLCUvzfb535T6bow7Xv3SZ+XcsKoNomGOrMeROKqnyHKtDH06gZhkEAWQzbKtGHjk58mTBei80rS157sa4Vq6AzQLrZb0qK4XoaHw67XjZgSaSHkuuHANxBq07DqRuPM5JDkak7YSjZ2NU3S2g1ip0YMlzV0CrwMmYlaJgI1pUaDDVnDwOQENl27L33hKNiz3W211uVZYxg4hsIWX++w659+4ZgqFm4S3LcC3GRPDO3Ss0H1uOz6eEDysMD+eZLeZsvLDLo80FWJry3NkddteaxGHG8dY8US1l+KhGfOK8USdnLOFIUd0Nqe/UGM83GK8AKxmbX9pg02wwXcqxkSU6CUg7OerZPv1piJm4sIvJUzn1dxPm3lMc1hPny9dXmN+eY2nHsveDKStJj+43X8Rl21jCiaVylDNaDguQ2wU3pTVVJPg68KJxrsfV2g41PfXhJBrjw05icmIMQ+u8PONC2jwrmInGaiZoBzxiODFVOnrMpGAqAv65nWBEjqJbBK1UVOal0RXl2GLGakY2ISqk0QCP0gVyq2gFE9ajYzSG5WDAxIaMTETPVAofReNTmfum4gJWvr5EdQmf1lt/bAnHbhBOqhHV/dSNfePmsq0lMJ65qV2NyWsRJgmIRpbabuG9p916Y0PHLMwT/EaI2CQIQzEcFOBbXAQPWTxgNF5V1LYCVr5q0AXDezbvAp2WvwbZ9QVmgLnmmH0o0EHOf/70P+fT1RxIyK3h9emMLw6f4b3hGfYnDQ7G9YIhXMy30KBDgxmHmDxy/rSZIiiYhY6dqsCogp0OGQqrnVfpbM6BfP41Z4qoq50UuU/h4WkIZtbJjZ/wri3Afus8anVmvX+hiTRpMyBLnGfheEkxm7PY9QmrC10+trBNJxwxNQ64TnTGfDj0MuRb4xXuDha5ubvE9KRC0AuIBhqVug2D0IIWRmHLkjUMNraoakalPiMJDHGYYa3yISYAUZAT6ZxQG3a08YFIKneBSX/y/FtMTERLT0hxMuamGtPSbn2a2IigmEcnpkauNH1TZWgSPtW5yRs/vM5ws+Xu70lOWHObQ3kmqbc5SjlfWaUsUZBji0bL0ISRC6zS2j0GIMuEnWE8K9Kmmmro5vTERB64/e7xnXeUCwUJSCh/OJfiQb4EHBTgTx4jsjP5gC+BK+XXKRcoZYlmWfL3rYr1cgjCZDLxvo3yXCm8hW0h30UKJcWvgBTGGI6OjlhYWGA0GnmpmcjPyhLJMrAkLBwpRuV1y+CBALFxHD/BaJHCSAAYOQ8BTOQapYhtNBqcPXuWt9566wm/ycFg4B8j8muAer3u5eblsBdhllUqFQ/ylttHmDbyOkmS0Ol0mE6nbG1tcfXqVcIw5MKFCyRJws7ODo8fP2YwGPDCCy/44ByR34pUsVarsbq66iXTFy9e9OcrwJz0u6Rqi1x1fX2dN954g4ODA5555hkuXrxIHMesrq56b7H5+XkGgwEPHjzw7NgoijzrCk79DMvjVEDco6Mj5ubm/FiQeVCWK8q5VSoVnwIKePBEgAYBcUXeK4DqysqKZ1PFccyDBw8801Jk6YAfx3LOMg4EmBBmbaPRoNfrsbOzw7Vr17x3qKTNXrx40cvrhT0rjB4JxBFA9O7duwA+4ELGeaPRYGFhgUePHjEYDNjZ2WEwGDAajTxj8amnnnpi7Aoj7fj4mMPDQ39tzz33HPV63fuO5rkLspBQIAnskPAUpRTf//3fz5tvvkmn0+Gtt94iCAI6nY5/vLxOpVLxgJnWmpWVFebm5rh+/foTEvAPtnGZHSbgjzCl4BSwEACtLGcuy3kBP+cF2JA5Xt5QKI+78ropa8cHZbNylJ8fRREvvviiDwfa3t6m3W4TRREnJyee4ZUkCb1ezzPBytdZtlk4OTnxP+d5Tq1W82NAADBJqhdWn3jWlse5MOuEhSuAvIQ1CfO00+mwvr7u5dYyR5aXl+l0Oty9e9fPqVar5RmyIpXf3d31GzGSzFutVul0Oh6wEmBK+kvWXQELywzkVqtFvV73yc4ylkRWOx6P6Xa7PH782DMay7YdsnbLBpIkUovX4Pz8PHNzc36jqtfreQD16OjIs89lbTlz5oz3KJQxLtLj/f19D0yKJFmCU6IoYnV1lTRNfQCNAHKz2cxL+CV8ZW5ujq2tLc94XFtb49GjRxweHj5xny97d8qaJHNhf3+fbrfLjRs3OD4+5uWXX6bb7bK5uenbWOb0BxmfZeuSD27sfPDeX2YAa62fYHCX58bv9fj/ywNREqvm551P2b1799jZ2eEzn/mMf0ySJHzqU5/i1Vdf5c/+2T/L66+/TpqmTzxmbW2N559/nldfffVbAoiy+ydHr9c7/WcB7IlPn4AjUjQF01OmntEOFMwqEhbiwKa0poj7zijdHhdhC7l9gjkor2kCJ2cWX0XHBjt9jJHk5MxSWJQ5aWuMT4YuAlVR1vpwl3Do0pe1dt6DWbUofHNhKbprzWoKlHasuUc5o0XHEEnrYCPnyeUBotAFCJjIPdcUTJtMwWwh920XHWuXLjtS6KlyoKByIS4qLfwc5TOqgfqOQacwbSsqJ5buRY1SDkw0oStUderOe7oxo6NnTKwlKRVMi5UBDyIHvqrcBV/MmgHhMHbtUnh/TZerBJ2E8VLMcEVjVY1qNSStheTVogguGI+SaKszw2Aj4ZPtW7wxnfLq5gVGRzV+jReJd0LC1EnW+0/l/PQnvsL/+vc/CUBHZ3xv4w4NlXA3bfHa/jnUYYzOLEngAiKSnsFWc0Ym4s7eIpONiDwLmCyBTrVPMz5+yVBf6/PU/CFvv3aRpb9VI08UlYUANU74UnOJ6bwiyQHlZODx8RSrFME0R8kulzAIDJAb1MyxsvJ67JikNRckYDMFgSXPNGFkUcrSH1bgcYXqgSZ+v8bgrCVvGpKuk4AP1gOSY+eBmNUscc+BWWGSkWtLozlhFCfkcY35O1PyasD2H2gyWbBkSzOCwJJPC8Q8cOChjmSw4tiIgXGAYq5QgXWsrbHzGDz7mzkHL3S4Pd/GtDL0RGMqmtxoFp864ic23uKXznycemvCdByT9yNquyHt9oiTs4qklhLfaPAoXiDad4jRgzcvUv/BPfaPmwSVnPQ4gaUZc8/1ODhsovoRs2fGPH32MW9fP4+NHHj1wrWHvNM4Q+WdKlHXgfJRVzH/lsYGDY6vQaWniHuQNkOmcw4ArDyO4Lk+l35kk7ufu8i0o/jJl97gH3zzo8w/KsKEMhdAEQ0zwkngpaJpzSXDm8jNmXTO8NMXr9MOhrT0xKcaHxZehcIinKGJMUwK9qEEmkxsSM9UPGMxUoaadlLSwFpSG7IU9JnYAK0McUGBminYyxvEKqepx87vUM3omYoHYfbyJsZqeqbqmIbxAQGWoY1cSIoNPeMwLmTNKAd45iiaesL/sPuDNB9YBhtFUMdE0dh2xXDacEB5MMlBK/TUjXNbFEOmVsFUQi+xrRxlmNCFK6VNF0KVVyw2MljtGN16otCpW0ezIsiCgqmnZork2Hkq5lXIC09NnUHczTCRJpgabBEGVT3IPSOvfyEiDR3ob43mzmyF3+gt8ls7l+mNKigF00lEPgscWAcEQ+0eH0J4GGGikMA69mMwEbuNYt0O3H3CJJZpxcnabWhRqUJnivhYuxTkIvArnBjCifEejf5+VHgjquzUz9C9viKvaNJ6QJ4oZg1FXnXM4OliTrQy4tziMS/Vj1lMBvSzCvVg6pKQ9Yy5aMhB2mRv2uSf336B6V6NsKeJem5TK7KgEwfMpnWLSQwEQCMliNx51GpTGpUpxioCZQkL2W8SZATaWSJM85CDQZ1ht0qwG1M5UMw/NjS2ppjA2Vl0ny5kSyqnZyqYIugHYGaDJ2TMC3rkrQA6gfM3TG3An7z0TX41fJHjXo1KJSUOM3KjyfKcPNeEocFaRwDX2m02GqvIjS6ARfe4IDBobTBGe9AxKNbC0SRG90IakftQECjjx8Z3j++sQwrLcgCGFMAiQRaJoAA0AtgJc0WKTmOMZ8WNRqMn5IZlJiPwu/4H/C7ZpoAAkoAsIFeZbSIyQQm0kIKn7IMoR5ltUva+EraVFDBSeMl7lROTRRo6nU49kKi19iCRFE3CqhNQRgopAboEwBLGyPvvv++ZSID3pGs2m9y4ceOJ1FYpaBuNhmdyyN+l+BUgTIBVkSCLzBFcTVQ+NzlarRZXrlzhG9/4Bh//+MepVCo+3OWtt97i3r17PPXUU/zUT/2Ul8nWajUPRuZ5zpUrVwA8QDGbzVhYWOD4+JgwDJmbm/NghBTLZf+tZ555huXlZbIs4+TkhK9//ete/mat5Xu+53u4cOECN2/e9G1eZk8Jo0YeX5ZmyntMJhMvU5fn5nlOtVr1Mt2zZ88yGAzo9/teGj6bzWi32x4oFTaUgBSSbi2AfLPZ5ObNm1y7do2lpSUODw959dVX2dzc9IUznPqRfqu5ItJlkW2HYcj29jZXr15lbW3NBw8tLCz48XdycuKDC4QJJaDDZDJhbm6O2WzGnTt3PBh1dHTE8vIyu7u7jEYj2u02Wmtu377N7u4u6+vrXp66trbmvS6FsTsYDHwSt4RoiG+jzHUJS7h//z5ra2tehtvpdHzi+FNPPcXnP/95ut2uB9zlu8xXAROstTQaDe+t+fDhQw9SyPGtgEM5yoym8vgoA4Zl+fIHU7Kl/8q2D9J/AqyXwRMBcIVlWT6vMgu1/BrNZpOPfexjfPWrX2V/f5/xeMylS5fo9/sMBgM/NgUwEqsCuU4BxySYqtFosLq66n37rLWeZScsZlmvFhcXn1gbR6MRjUaDjY0NL0uVDYF2u83R0ZH/W567YKl3332Xe/fuefajJACLFcOzzz7LaDRia2uLu3fvsr29/YRPoTB8JVjJWsvh4aGf7wL8WWu9b2F5U8Nay9LSkmc0l/0lp9OpD6ERxl45TEbueSIHFhuFVqvlf67X656prZTi8PCQW7dusbOzw/b2tmfkSf+IZHx1ddWHNXW7Xb72ta/5QDBJbhbJtmxwAT6wRsagbBTEcczCwgILCws+qVkk4cIgnM1mdDod4jim0+l4b1JZd8oS7fKaaa31G3dyHjdv3vR/F69HCfMSMFJeo/y9vNaX1QoyZgVIlw0aSa5vNBqeTSpz5vd6/FsDiNZa/sJf+At88pOf5Pnnnwd4IgWmfKysrPDgwQP/mDiOmZub+12Pked/8Pirf/Wv8vM///Pf+kSUCxVxJ4Vnm0j6ZJa4AjLuix/bacBK1MfLuTxbwwKTU69BAdlM4ArJcOoYGyZ0skSdu9fME8Ws7sA2YX1kiZNNB1MHUrqQBIUJLTp1fo2zpkLjQLBwKJJrCJQ7l2BmyCqaaOSCBWwPdG5JTgyD9YDeZUv1sSIaugJQqVNPLGVO/fHEGytfm0IvckEZsYFUkRZdMYudh+J0/tQHMa9aMop2TUHlisnMFcOTJXfdo42ccBpSTx2A6JJm3bWsrHSZ2ICOzknJiXAL+Hw84nZdE/Xd+ZkI0prCxBqV2QI8OA2BiYY5OlOFkb8iqxeBCsoxIaOhLcBWSzAxDJ+J+Fr/In/lt36C5p2QVubGSdpwk2P08phf+r5f5mev/zTNTUMSZFSUYjU8YWoz3h6fZfv+IlrBYC1gPsx4bXyJuJtReZDwm4NniaLTIAurLdHIMZpmTYWt5Cw3BzzbesxTP7jPr59/jlplyo9vvMdvPL5C8D8vs/z1KcMzsQeZ1DTF1GJsoJ3kEUBrF/KRpRA4mTlAVnUp2VlNu0CaAhgJ44z0qEJmFOSKzi3n89a9lkMOwUAza8Ks6aSatT0naRcZp1UQaEvSmNI7qhPuR4QTS9oK2f2wJl2eOXaitkhgC9pifTirRQeGvEAxbK5ROi8GkHXy95Eibbmxo3Oo7isa3wyYdDTKxEwGq8zmNP+o8UPUDORhBbtoaGxp8hhmswg7CllaPWbnacXGXJ/tapsPn3/IV999isuNLnv3Fmif7dLbTwiPA7qbi9iVjPhIk9ytcvv9S/DUlHprgv5ym91XL9JsO/ZafnGCUpbRfMBHf/ImX3jjGpWFMeO9GurMgOCLbWIcCJaFlvpvNHhUadLZMZh//4C9SZP2N5MCyHJzO0gtW5+sEqTQfGjIYidlN5EDsLKa5ZlnH/FcdROAvaxJXc9YCnssBANmBJ7pVSmAP2ElikQ5RznARDnG4olJiFTOxAZMbERFpYVfYoYhZygyT5VR0e5/QkGd2IiRTRjlCUOTsBAMqOkpFzgAXOryDCcXraiUoY29hHpGwMw4YDG3ju3VCYZ84dXnmVN45l9lD8J+Sl4NmTUCansz52+aGchyxz4MNaZZwcQBWS2kdzFmuKZIG5a8mUOSujEoh3XMPbRLd8/B/Z4rVKpQM+WsKSaK1l2Dzl26uNUQDRxb++haQjh2a7QuNiXyqEjujt26HQ61A91Mlf/q4U8SThR5YkkXMoJGCltVoom7d+QJ5FUDRmO1rP36dI0O3P3GB6YEDiyUNOTkWBEOoXJsCGYOLNSpA7DFn9CGuphiFlLZ/CpkyLFi2olIG4rpnPP6TFsG1ZlSa0xZbfU5Vz8mCTLWkhPmwiHdrMbIxATKMNUhic5oBhPvedkMJpxtHjNdC/nq5lVQMFk2mFqOSgw6zonjjFqYU09mDsRWlkY8JTUBpgDhJlnIcBozHMek/YToMCQ5UFSOLEnXsHycEYwzR91UiqweMmuFVA5mzBJN2jQsRn0ildMJht7rEPAMWjl6NvH/1zjJs0HTD6p0qmPGs4igADKzXGOtY1Ebo0iiU7lIGDgmong8GquoxSnTNGSWBeS5xhhFNomwU42aBG4sZYpmOKVSeEFSeFZ+9/jOOsrMjDKLTg75MC9ecOITVfb1kmJVQCApSqUwKDNyymBi2WtMwDeRvknBLr8Lc0fO01rrGSpw6qUo/xMAToobeZ4UjwBnz571PoaSoisFt7yXAFEC2CVJwsnJCWEYPgE8SpCMFJ7CTpE2LMtphc0hxZFIpLvdrm9/YYWlacrOzs4TTBDx/RLQslKpPJGGK4WlAHpSiMvfRqORl/5qrVlaWmI8Hnvje2Ftjsdj2u02X/7yl3n06JHv40qlwksvvcRbb73FJz7xCV+0CxNI+lzkjNeuXWNra4unn37aG+C/8MILDIdDDygII0kAh9dee80DHbVajYsXL3qQOM9z3nnnHX7zN3/TS6IFTLp48SJZlnH37l36/f4TklXpBzlXAbw3Nzd924qsT4IsBGRtt9vs7u7S7/cJgoC5uTkmkwntdpuDgwOfOgx4NtLGxgb1ep3Dw0POnTuHtZYvfvGLHB4eAqdSaBkj8iXnUgbyjXEhJTJuZCxtbW35Al82AJaWlkiShLNnz3L37l0uXbrEysqKl4yORiMfoBHHMWfPnkX8Aw8PD73HooAnwmhbXl6mVqvRarUA56XY6/U4d+6cT1c+ODggTVOOjo7Y2Njg/PnznJyccPv2bcIw5JlnnmEwGLC/v+8Br8FgQKvV8sDCysoK9+/f5/bt234TQ2vN+fPn6Xa7fs7LBoCks9brdV5//XX6/b4H/YWRK2tOGSwst3V5LZL5L88T8LEM9pX7Rua0rFVl8E/+niSJ7ycBWv632KfCnCtLhK9du4bWmq2tLY6Pj6nX68zPz/Puu+96ZpowYWV9FvabnKcEZmit2d7e5ujoyLMMh8Oh36SRx8s6PplMGI/H/rqiKKLT6fixOz8/z+bmJv1+n3fffdez+WScCEtPbAMeP37s18s7d+54f02xb5DgJbGQGA6HaK19KJUE+ZT7Qfqr0Wh4H1IBnMRCQBiXh4eHnJyc+E2u8saNWE6I3YKwqFdWVjzDtczeHo1GHB0dedBsMplwdHTE5uYmu7u7T9xDBUCWe6YAeL1ej4cPH/q5JRtDg8GA8XjsA0aE3S1AZqvV8j6Ki4uLXsIM+A2EwWDgLQz29vY8EFupVLh8+bK3BZBrL4PoZeBcfpZNw7INydHREZ1Ox0uh5bnSJ/KZQf4uR3kjUH6WuV72HC2rCuTeIvPp22Ei/lsDiH/+z/95rl+/zpe//OXf9b8PIpjlHYX/reP/12P+k//kP+Ev/IW/4H/v9XpsbGz433VWkuiGrtaQuiEaiHeUS+7No8ILUUM4cX+3+hTwMoFy0jntAENlnJwX8AEdKBegcsq0sz5gRVnHptO5RYzo88TJmYOpJUit936KRpY8LkJR7KlMWhkHeArrL606Kkmla6jtO+bgaCVksqAIRoXvXyETs8Z9qeJ1JGkTHICothLnbbicEsYZ2TR0RUyuiHqBf6ywZUTqHUwgGLv/TTt4xkwws6jOjPQ4ccEcScHuNO66rs7tEWCJUBhrCbQit5bFaEBWUYT9KTqNqMWO8RP1ZmT1qJDWlcZDURBnFSfDm7SdB6ANFCfP5TRvBzS2cw9ehiPLl371FVpDvDeiDR0AMF7NObd8zM/f/XcIfrNDOMlYqfb5nckSvz24wktLv8O1yjbPPfOIg1Gd0aNlOvGIN/obTOdCOrcM/8v/64cYfk/Ogh6TzzRx6lir0cgQThX9g4jNx+uM3llD51BvKrJ6k3+89EmyixMqa4pwEnFyFRqPHCihjwc8+vFz5FU4/2s5apo6/8M0d+EpgcaGASiFSRwLNa1L+6jCL8xghgH54oxGZ8xgJaRenRFNYrhdx4QwOuOAEhu6UI88OfVTRFumB1XYD1m8b0l6huFKwOPv0+TtzD2ukCjbXLSPChVZrHEMSIuTdua2YNsZ54GojMI0cjjQ5LGbJ3kE0UeOOZoWvnnHFYJ+QOuOpXPHMp5XxANLct2QHE2YLMYM/3WTJFLs7a+iAtiO69R2FF87uAy1nO1BGxsZLs4d8t4s4uMb9/jNd5/h/NkDNutzrKwecPfNdewkYLLfIv54j/4bLZa/kTogZCdhtKzJE/hicJmgmaK1Y7ZdWdzj7e8L2Fg84dFr66x96DEPFpZZfjXg8DnFT6+/x9/74ieZH7vrwzqGWFoE3kjYk3jxyVe2PuVTi7eYmIhAWQyaph57GXCOJreapp54Sb4wD43V3ruwqScMbUzfVHywSmq1f52hjRnauPBNnPlQFAEn786WnbS5SLRt6jE1NWU97DEsHjsyCbpIuxW/RMeYzImUYS9teoloRc14Kt7jUbrA/NuK0YrCaoNOFbWD4qYaKaKRQaUucdkAtlPDxIFj404ytLXsfqqO/VDPJR0/alLdDFF5+ART3H8XS4nC4sCEbn0NZq5PgknB3BsbKoeFD2VpuclqRZgHDuRNG44RaLUD9YLp6RpoAheqYmKLmmnUZpXKvvKM+KxusLWcrOZAI4zy/neq+FlPXTiQk9orkq5IkE89b1H49GN3f3LhJzZQkBlMNSh8fTWzhpMhz9owXssJF8esL57wSuuASOeeUQiwNenw1e3zzGYBQeB8/dbmu/zY6rtoZUgKgFmORKfUmDEfDvgjS9/g8MU6W922kxqXpMfVKPXS43EWYaxif9hwoSkHCdVdTe2xSz2e62UEaYZKZ45hXQnIE81kMWLWiEmLwBuxJEmOA44+mnLtqYcsBAMqeuaCVIp50tITZrhwIRmrIuF3YHshT1YZNT0lCZxXIcA0DZ0tg3a7V4E2TwCI4Bir4zRklobkmSadhDDTqNRZCcg9S2mw1Ry7kGGOYsZ5xEle45uj8+hR4D8jfPf4zjnK6aBl/7WysX/5sUmS+IRHYX1IcSOF0ge9x8pFOZyycsQvTt6nDN5JYf5BOZUxxjMtpAiV15BiWYoLKXolXKQMVlprPUOrDPaVGZjyOV/OXwAyaTPAs3Kk+JJzEAaksAJbrZb3BRTGllKKxcVFer0ei4uLPkxAkpE3NjYYDoce6JO2lOJSis0oimi1Wl4qJoEk4oko4FbZQwrwxaikiko4S7fb5bXXXmM6nXLnzh329/c9+La+vs6LL77I7du3WV1d5c6dO4xGI65cucKZM2fY2tryCaRRFPlid2tri8XFRe7evcvJyQlpmnLt2jUfSjOdTj3D7MqVK7z44ove7P/GjRvs7+/T6/W8d16z2fQeicLqqtfr3n8vyzLef/99D+pIv0jhPh6P/bmVfTuF9SPgYZZlvi2FbSXtJ6y98nzIsozz58+zsrLC5uamB3aMMbz55pvs7e15+XuZFVcGo+BU7v9Baa3048HBAe12m+XlZQDu3r1Lt9tlOp36tnj33Xe5desWlUrF++SJAk8kv+KdJvLV6XT6RFARQKPR4OHDhz4BXWSWYRj6fpibm+PBgwd+/Inv4de//nUAz7YbDoecnJwQxzGLi4ueoHPr1i3+xJ/4E9y5c4eFhQW+9rWveY9KWV8Gg4EHJkT6DNDpdNjY2ODu3bsetJHNC2nDMoOw7AsoAKWAubLelPtB1iF5rfLaJnNdwL4yQ1HWMpmz30oeL2NS1hhhsMn7yXV8/OMf59GjR5ycnHhMIcsyer2ef5yA2wKWCRAD8PTTT7OxscGjR49otVp+3RYgSkDpcghI2Z+zbAUgPoKyls3NzXnmuYCNZQZtkiTeVqBshQH4gCi5bulrSYsXL9jyJki5L8qSV2lTGc8yX7e2trxnYZmVLH0tydOyYdFutz0gNzc3519rNBpx8+ZNtre3yfPcWwUkScLi4qJPoZf5+Vu/9VuejCZ9X7637u/v++AjYVqLlYbMl2az6eXQwlgUX1ullE9o3tra4vr1636TQMBMuf9prVleXvbg+4ULF7h06RJ37tzh0aNH/tzKjGg5yoqAss2IzIUPjvEy8Fdm9kuYiqyv8rPMt8lkQr/f93NKvIjlfcW645VXXmFtbY2dnR0Pfv5ejn8rAPFnf/Zn+ef//J/zb/7Nv3kiOXl1dRVwLEOhkIIzhhVW4urqqjetLLMQ9/b2+PjHP/4t36/8AetbHZIIHKSnUmZlTpmIwQwvTwsnFjVyQFc8MA68m7ki1ujCF846Bp6aOflxMLVobckqhQFlIEw75WWzQeroj+VwCgG9lHHejMKAlKLBBBAPLLOGuwaduteORtZLl4XpOGsqBmcc2pDVIOlakiNLWHWsqVnbseCMhMQUgI34ETqZtwOO0ibkM006SUBb1DhwoFCRAhufFI9rgC3SJ/XMBZakDRegooy7lLShqNZm2HerhVTuVKY9ayo+1r7nxkQOG6HzQEzJORcfkCdgowA9zag/TMkbMUFvQrjXo2ItNgxQkxlmvkk4LHbgJwadGif9LoDbxv3AFdoFA1GCUZITmCw6MDQaFJLuGP7LH/sV/uXRi3zpt59jvgBqw4LNtTXucGQyhiamEU3Zsw2UhZWkz1f2LqIsHP7EmNX5HkePFmjqHGYuxXq0qkmOFfs/kPKff/yfEamc/+zCT2BGIR+6do/+rMLd1zZY+19iwnGKVbD8Dc2krYl7ObaaUNst2KpF12GtAxAB8iJIJQp9EupsrmAfCsagnOG/CixZFpCeVJi81yCcwXjdARjKAEbRuq0JRxlpQ7t+U1Db0tQfQzjNGawFHL2kMM2Zk9tl2smRpxpyMMpRW3WUOwaidexXBWTTYmmxyoObNtdFeqlj40zmFfPv55xkcwRxMfYTS7qYMfnhMePrLar7znJAZxqVRYTDnKSryaoQ3i/mc+6YuvXHilkzImWJVkXx3tZlTGz50p0XiAw8mCwT9gP26g1MO+PKhR1u3lvl+87e5+vhBvv5HNV9y/jHe2RZwAtr27z+1iVMphgHMUSG9z93Gdu0bL/fIBnA5vVVmtuOWfbjf/A1fv3Rc3TeK5LZlWPGgltfOrdcP2YVTZ6oU5Z0zfLpqzdYDPvEKidHcyE68OzWkXHMqVQFjExCTU8949BYzV7epK6nVLAEWA+UpDbEWBciUVEZExvS1BNiDIemRkpAakP2TMKjdMEzt5p6woXogKGNHWNRpxyaqgtEKWShExvRN066JSDNTt6in1eJVM5qeOIZiW095T9878doDC2zthsW8YmicjDDJAFZPSAa5Jg4wESK5ChzISrGoodT8maFg5cbbv3+QovKkWFuarE6dyy7wG3omFC8D4u2LYGKtliXdeZsLYJJkYQeuLWjsu+AOAHolC1ZTmSg82JtL9ZwwdOCifMNlZArsYsIJm5TwN1/NNNpRB5b79cqgShWO1m/qRps5EBHB64WHry2uHfNnAw5SB0TPS/aLa25sZY2FNN5tz5n7ZzaUo+FxohzTSdDHmYJmdV00wo7wxb7vQbTnRpRVxP1FbVdy1zPFNdrGTdqPPiPd3muvsXAahKdegarVpa6nhKpjImJSMKMdm1MJcyYZiG5VYymMdu7HdRRTGVPU9u1VE4MncMZ8+MMZVJMHJBXQ7JqwGQxIq0qspoDCiUd20S4NSO02MQ4q4SpZvrSlH/32ddZjPoMTUInGJHakKGN6WgXqlLBMDIJExv5MKC88AftFWM3IqcTjKgEKblVhNoQh8WHuMJX2ALdYZU0DTCpxs4Ct45OtQ/rUZJ23UrRgbOQqFRnKGWZzUIftPKVty7zFS6jJ5rkQDtZ/Sk2+93jO+AQ9tcHmXdlqa8AF1KYSqFaTvmUD/plkFAKWwGf5PXLxX+5oCoDc2UpqrCMygzEsuRKmINyzgLclZmOAmwI0+H4+JiDgwOMMV4CXX6NshxZik5ha4hMbzwee3lZOWxFCnEpjsoyMHmfMqD36NEjVldXfRvIe166dImDgwPffgJiCZiysLBAGIa+sBI2o7ApJexlYWGB+fl5z/iS/hU2ysLCggdB5RwfPnxIr9fj6tWrLC4usrW15QMNykDm1772NZ/oKgWjnFOWZZw7d47hcMj29rZniUmC6Pb2NnNzc1hr2dvb81Jcay1f+cpXMMZw9epVzp8/T7Va9Wya3/md3+G9994jDEMODw8ZjUacO3fOs6nq9foTIM0Hx7WAzu1227OIyqCEjAlhdo5GI3Z3d2k2mz7ZWqSQAqw1Gg06nQ5zc3Okacr169d5+umnyfOcO3fusLm56dmZAjKVwfoPgtZlJpBci4AMjUbD+6gJMCiARhiG7O/v8+jRI5/MO5lM2NvbI4oiDg4OvHejjPHpdMrc3Bxau/TtJEl8KEatVvPyQUk+jePYA1lzc3O+r5MkYWlpiXPnzvmwBZGvTqdTz5ibm5tjfX3dj/8sy/ihH/ohL8V94403/PkGQUCv1/OAl/Rn2RNV2J23bt16gsEs8tfyV1maLOuM9H0Z0BUGoIzzMmBY9tOsVqvEcexZuwKOiNxUWN1ylDdXZCzIZoqAZ2VG1mw244UXXuC5557jb/yNv+EBE1FBytjTWrO3twfggzpkDJ05c4Y8zzk6OuL8+fMe6BFZbVnqXZaeyjXKNZWZmJPJhOFw6EOcpK3K4NwHN4Lku7y/WCoIeCibO+LjJxtAcJrC+0EQVtZaWcNlnKZp6ueAtHXZ41Xkx51Oh1arxfz8/BPJvr1ej9lsxt27d3n06BHGGDY2NlheXvap97JOHB0dsb297cdkkiR89KMf5ZOf/KT3UJRrkHuVSNh7vR67u7s8fvyYZrPpmY4iQW42m1jrLAlk4+jdd99la2uLXq/H8vIyzWaTMAzpdDq+3UXmO5vNODw89IxrY5z/42Qy4dd+7de4ffu2T3IvA4RlwLDMSJQx8UGGtMzVMmtR7rcSClO+t0nfCTAs46CcBC7/F+BxOBxy7949Wq0Wt2/fptfrMRgM+L0e3xaAaK3lZ3/2Z/ln/+yf8YUvfMFH2Mtx8eJFVldX+dznPscrr7wCuMn6xS9+kV/8xV8E4EMf+hBRFPG5z32OP/kn/yQAjx8/5u233+av/bW/9u2cTnFSePafB1HyogDMigUtw4ejCEDo5LhgC2lanijCccHuKNh7UuSndU00NCX5ckkeFiqisWOCoE4LyLwiEjT3WAHxJLhCglKCmSUeFCzDouh1Ml3HNtGZIiyK3Ty2ZLUiBCBwr53V3GvpqQtgyWqnbMysgmc9gfs57p0Gtuhahj2JXTsUEmdVsMqymmsbPVWEA+WvLRw5ufR46RRIHO7Wqedw8H0ZtXsRcc8V2JMNeKHyiJrK6dsQXYSUdE1OXc/IKoqsHmFCRdoIGC9qmrWQysEEMsNkzX2QrN04QB9MqY2a5PXY97ENXFtX90/DAMKJS9QeLynyquUPfOYN/vU3n6fyzZDjF1IILP28ytcfbxB1NVY5YLgeTtlO58isJrUOtJlkEVo5P8bluMfxsEonVERxxn/x9K/yn/GTRAChAzIrB5bKSc7ilyL+63f/uJNKzluILa/fukBcn3HmlR32J2fo3FQcPefSUqt7LnnWVmPm3+6j+xPHNtQF+zAvGAvGoNKMvFkrAn8UeaXwHyykxLNpRONClyzXjE6qhN2A6XIODceuC3aSQk4HrQcZaV0zntfMvW+oHGaYRNE9H9J/OofWBBVYmBQDKLCowGC19p6HNncyZcAHpuSpdimkuRs37u+n8zVtGao7mrwCx1cDnvrRu9w7midNA7JxRLiboG+3mM5bjtdyag9Cwoll1g6I+xCNDMO1kP6lnOQgYOGdnMcf1+TNnOpWSHLsgLtZRzmvuEHBSr0ZuHl9q8MCsHP9HIt9yzfeeBEbw8r1Kd1LMaNuleA45E5lEQJL/cyAwV6da5e3eL+yitKW4O0qozVDOFQ0tgy7f3hKOxwzeG2RWm4LEN3ZF+RJ4fVpiyCkGt4iwMSQ1wwXqwfU9RStDDU19VLLAOOAmkIO3AxGxBhmaIzVpzJmqxkS+9/BecCdmKoLQNEzcpxfohx7eZOjvEFuNUthD4MuWItOkjwxEX1TZT08JkeREniJ9MREpDbkMGs4ZqNJeCbZZj3ukqPomwpRkdBzN5tHfWGO4Rm3CCnjxrzODXmkGawGTBYDlr+RUd0do48HmGbNy/VVbmjfnRFMDWFv4hPJbRSAteT1xMmejXHp5FqRV8PTNTrSDhwsNhd0btEz2YjQRBUX7JPH2ttMyOaEgI95JOu+WzuFsRsUeQbCTjfxaZK9sMlVDlFXEerTddSG1lkwBC5ARe4NJrSM1i15rOncgepeWowVTdoMmSSKaUsx6zgp8mwuJ5qbstAZcLV1jMZiUBxO6hwM6rze22A6jNGHEcFIEUk6cwAV7TaMxquGrK5YeAtQCpU5UP5Ob5Hn6lvMhUNa2iWt1vTUJxyD8x6Mdcb+SYO0m1B7ENG6Z5jr5Sx3U4LJwAGFlZC8ohmtJKS1CnmCYxVWIa+4TQUTFx6WYVGoVjNU4DbvQmWJ4ow4zOn1qizO9WmHI2p65sdfrpXz5SwSl5t6Qk1PiazzEp3YiJkNqOspdT31LNtAGR4PW0ynEaPcbYrk4wAyfboRl7nwMUIgMu5e3cwIKhk6MIShC1axVrl1bBQy3K8QDl2CtgIIQPeLTZTwdAx99/jOOqRgKLOdyr5SUih8kAVVLqSFvSdFvjxWiiwBzeCUfShFnxQLUoSUJXfyeCk65XwFcBFgUIqTsuxQHiegpwR3iH+hhKmEYegZM8KikdeTgksYIeV0zeXlZQ8Wlg3jRXYsIKmch8iKRV4nhZ54AmrtQk4uXLjAgwcPGI/HrK2t8eabbz6RkDk/P89LL73E0dERS0tLHgASuaCkLVtrvY/WYDDwJvny936/z3g8foIxJMy4K1eu8NJLL7Gzs8PLL7/s5XACGL/55ps899xzPH78mG63izHGs/WkQBSJtzBHBPTUWnPu3Dk+97nP8fjxY5aWlvwYkoJzOp3y/PPPc3x8zJe//GWm0ynPPfecBwbOnDnDzs4OZ8+eZX9/30sft7a2vAxbWIjlowwgiLeehCPIeBfQSYrcx48fe/BhOBz6NNTV1VWfNiusJwHD5ubmqFar3Lx507OfPsjqEeCqLKWV+QU8AXKVwa/RaMT8/DzGuFRqSbl+/PixlyYLyLK+vs4zzzzDaDTi9ddf586dO4zHY5+2LGDn/fv3OX/+vGeqdrtd9vb2PLtTQoYePnzo2Y3WWp+cOxqN/Fyw1nnTCUMqz3PW1tY4OTlhbW3NJ2zLNQpLdW1tjddff53FxUWuX7+OMYZarfYEW/fk5MR7zU0mEw8Wzc/PeyBZ1i+RpX4Q8Cv3wwfXPukXeY6sQ9Kv5TWp/LMAm2UvPmOM3/QoAzLyHmVGpIAlslEga460wSuvvMKDBw98UrnM9f39fbTWXLlyhWazydbWFg8fPqTf7z/B9BOAaDwec+/ePR9uVWZXx3Hs56CsBR9kxZbHpdwb5BBpvRCt5L2FsVge9wJElkF9aUsB18vzUhiC5TXZWvsEMCfrKeDB7E6n48FyCQaSsCYBfo0xPvVZbDqGwyHHx8d0Oh2azSZXrlzxG2fC1uv3+4xGI6Io8vPpnXfe8QzYtbU1lpeXn5CRl6+5PNZEgr+8vMzFixfJ85xut8v9+/dJ05STkxN/n2g2m9RqNdbW1lhaWvLJ1QIYfqtAGfm7jMXpdEqn0/E+k2XQt2yVUQYIy/30wTkkcu+lpaUnxkyZzS/nIyzQMnguGx/yvs1mk0aj4e83sq6PRiO/oVatVv369Hs9vi0A8Wd+5mf4h//wH/Krv/qrNJtNv2iJTlwpxc/93M/xC7/wC1y+fJnLly/zC7/wC9RqNf70n/7T/rF/5s/8Gf7iX/yLfhfvL/2lv8QLL7zgU5m/3cN7VanTBE6XZOmYKcHMEI4diGAiJ2GzkSKtaVdM5kWtEJxKdm0BTFrtHp8nRThKXnjGZRSJy9al4QpVQUBMcyqTQzn2ig1PA1qMKmRokg46LaTPIrszoK0il3XCumI1bYgsrggQSSEPHYA6XrVUd935RkPrPB6NY1HOOqdAanVPEQ1ipvNOTj2bz12RVHiDSZEsITDSxiY6DaWJBgW4WoP4KHC+YEdhIXt2Uu3pomE1GDKxmqbKgJiumfGp3/iPuXbhMdMFVyi7PnCgS17V5JUQE2smcw7wCTfmXNhNPfR/y+qOJVlOEXUFu2V4JuE/+OnP8f/++g/wc8u/wb+2LzD9kR6//Mo/4Gev/ykX+nCjjZk32G1XUEcqRyvL881t5rVmN20zMwFprkHDWnSCMRoTQCVO2cubHI+qtHWMigwmhOGaIo8Dek/B9//QdX7znWfcaYWGyr0Kc++HBCcJizXHrmrfCkgF8B2l2LiYjmFBHc9zrA7c72nmARVTi5zML1aYuJBFBqaQzLkAAmsVUX1GFjm/LkkNxUB9E5pbKWlNc/K0prpvqe2knDwd07tsyZspKjYFa9CNCx0Zx77JNd77EBwjSx4nsszAYlNXKAtjy98vC2qlyp03XG3H8v6rFx0AszHh08/c4OIrB3xu9xnW611+YvGbvDde5+++9TGWfz0hmBimcyGtBzmNLcWkYxmuBnRuWmbNkMFTOZNlaN0KmKxkfP8r79ObVXnz1gaLr0YMzyhMAtVdS3MrZ9pynpBLb6bEJ1MajwMq/9Ih0/q329QCmMy1mQvg9tF5krFiNm/cOtPMiXYjhquKP/78N/n7b32UuW0HOOvMEg1P1xtJXc4qzs7AAVUwaxvOX91hLT4mL2TLkoDs5MxOqhzpKTV4Qr7ctzEROYEyGJxMGU693zrBkEgZRiYqPOecPPhONu/YgXrGhejA+xamhVdiGYSs6Jlnc9VVyompsp+1SG3AfDCgFp0uEs5vMaSmUyKV+YCX/37zB6nvGA5fKCTdY0XlJMeEmrwakDZhspHS3w+pPUwhCiHUqHEKuUGPU8I4JG3HQAU9zXzSugk1Ni4+mGa48CENOnCegChFMHHsRmXc5oJKjQ8V0dOMYKzdOi839WId8evuaIaNQ0yk3XsXnoMCYubV0NstlNmO4DZtCmKu89SVaRi6BHWZlqfSa0lJN6jc0j8XM15UTBct6XyGquQ0WmMWamNayYRYZ+wMWxz26+xszaH7IeHIhceoHAggjK1n40+WDHnVoGo5YSUljnPmKlN2N+fIbp+yhrOqIgld8nYzGFPXUyYmJrUhAdbJ65Uht5ofX3ybF1rbpDbgny2+SHy9TlbRjBarjk1YFfmxYxSa0P1MYLCRgcii45wgzAkCi7XOd1ASjYNAWCMKXXgURtowHwxpBg7YbOoxKQEa4/08jXUhQxSMWXCA54mpUVcz6nriUpmt5mRYJd2vupT6yAF+NnTnZrUlSHK0NtQq4hUVMJuFZOMQNQnIx4pw5DaEKkPHGJX79GitlHRdsFD/v+z9eZAtWX7fh33OyfWudWuvV/X2pfv1NnvPysEMZwAMCBoCFxh0yBaCWkJS0GHSiqAUdgTloMMO2WH9QdmwTFniJsoiJREMkMAMQGIwA8zSs3fPm+5+vbzXb6/3Xu116+73ZuY5/uPk71RWcWSBjnCEMZqMqKgtb97Mk+ecvL/v+S7quH746fYTtgmjqCo/EiacfNAXI/qqzPm0VLnq3ScFhhxHCqgqEFItWE6/RlhZ1cCWamEir5W/V+WpVWmbFOTCxGg2m1y+fJm5uTn29/e9vExYVlLoCsgi5yIJmNZaL6NcWlqi0+l4YE1SdqVgl2usmsRXvc+kHaXI6/V6PnxDJNhzc3M8ffrUS/mstXz84x/nl37pl/jiF7/I1atXabVanmEigJn4cVWTmwU0lfOTdFQBQIQRWRQFv/zLv8z6+jp/62/9LcABbh/72Mf44he/6AtsYdBsbGx4rzkJTBH/SQFue70e6+vrnu0qRb0E0QjAWvVge/z4MZ1Oh5dfftlLsg8ODnj48KFPdL57964HbeX9BQSsstXguOithgI1Gg16vd4JIE9rTa/XO8GKFVBIJI7gJLmXLl3CGEO/36fdbjM/P+/TVvf39z1w8+NYkAIqVRlwMh7lXKoMLrkGAeXlHLa2ttjY2ODq1av+WPfu3WNvb4+zZ8/y1ltv0Ww2+cIXvsDDhw+5efMm4ECWBw8ekKYpzzzzDIeHh2xtbXHu3Dl/b69fv+6BCQEBh8OhB0KePHnifdystWxvb3Pv3j2fBry0tESr1fIhDpJ622g0/LwiIM0rr7zCiy++yO/93u9xdHTk5ySZC6TfyFyktabZbPLcc8+RpinvvPOOD1mqynerAO1pO4bTTLvqHCXgmPQNGePSdnIMGcNVVrS87rSPovQ9mZ9kQUJAFXmvqgx0bW2ND3/4w/yjf/SPKIqC8XjMhQsXfEgT4Nm5KysrPlxDbB5Exivzj/iMVudd8R+tsgirc2qV+V2d86UNhU1e3aTNBRiqspPl2qSdBOyVRSRhpkr7yPtW/ftkrpZ5V77Lvk+fPuXMmTP8wi/8gpfKS6rxo0ePeOONN5ifn2dhYcEz5KptM5lMuHv3rreIqDL0pA/IfD8ajTh37hz1et2fu7BQ5Z5W+0YVgBb/yTNnzrC8vMzzzz/P7du3fSq1SH1FfizMTFkIk8Ur8d6V54DMtdX+LXO5tKvsWwWIq8/mKkD84+5vdU6t1+veA1lSupMk8ccS5ryMeQEIhTUqoHKv1/PM6WeeeYYwDDk8PPR+tnKvJIioeu7/Q9u/FID4N//m3wTgs5/97Im//92/+3f5i3/xLwLwH/wH/wHj8Zi/9Jf+EoeHh3zsYx/j937v92i1Wn7/v/E3/gZhGPKrv/qrjMdjPv/5z/P3/t7fO6Hx/yNvyoF6cd+FjRSxA5aSniFraA/sKWspEicZCifWS9yUVZjAJR97VlBQsv5CJy2Oxo6R4fy0XIFXxBz7IxonyRQ/M0p/Qxeo4qTF8cAya+kT8moowcBE+deFMwc0iLzRsybLeXp43vDnfua7/M4/+gRody3CghmtW6IBZC3nNeaLV+Pkq/M/cj5b4xV3zXG3TChtaJID7ZmI8n5WwWTVkOxqkq5l2lE+NVpnDsycLroisPkwoLlZMoasY1+Fy2MnZ7MhsZphMPStovVGwsO5eaZnnHw27hv33loxa2qSAweyBlNL1tCYQBHk1idjT+a1D78RCZkwUcNhzu7nI/5X8zf5f+pP07cReqQ52zniSTZP76DBC8ljLr/8iM3uHOpHbbDOEH8va5HojEhp9rMGhdHM8hATwEZ4yGwSgoJWMmNiIhrJjEgFRElOoWLHFp1a2ncVX7t4lfnlPufnuqzWerRfmvDdT1zk/oMlPv3S20yKiHd/41kaWw58xBhMFKHHFRPoIHCgXH68woLWzNoROrNkK9pFnuJYgCo0pW+XK8ABwqggCA2zSejYhOfHdJOU3uWQfDlDBTnTpYju8xGmkXvEw1qIk5wiD1wQhXXAoAMjHYhojULHxQm2oQ4NJi9TewAEiDEOPMKASY1jhZVjs33Hec6lbyS81ngff3AN9Lkhj26s88riVdbOHLKy2OPglxt032piAxfGUn/i+nPxs4ecmz/kjdtniXYjgomid8XQeBDynaMXyJZy4p3Q2QTkYBI3BobrmmigmKwYHv9MSH27TW3XuPFXjr0gsyRHDvjovOMWI3QBeWpJdyNq+4bw39jm3nCR9rdr2DJRWzzybDlG/VivOSA+TyFvWq6+b5N/89w3iFXBzDrPNsAnLmc2JNUTD+519BitLL1S1gwu0CRSOYt6RNfU0MqxFjMb0Depl2t2izqpzjgfHhApl47skpRjRsaFpYgsNVYFWekht5XPYdB0Cwe2dIIRLyVPyvPT7Js6AdZJnBVEtiiTmSOGJuH+716iGRifKl/bsjTuD8g7CVhYej0jPQhJDw3qyS4EAVopmGWOcdueKy0YXCqy0SEm0N67UxmgFvou58E/HPtQGeulzsHUokNDXgdKhqL4CKqSRe7mkpJRHihsrKEoAcs0QhXGAYYKilrozkOsKaS+kt8LPHouix2qsEQDdwxlLWpm0LlBj2YOmFSK0dkm27825iNnH7GUDAgw3BsucudgiX63zvjdDk9nbsEnmLkHeT2SwBbLbD6HxBDWcrd4oN2CQC3OCbVLC24lTmJbCzO2w45nWAYzxfCM4uPz9yhKVmphNZHKiVXh08F1CWSvRV0P5J1/fp//86/8KfRu7EDC0AFwxC6NHW2J0wxJM9baYK3y85VSliAQVkH54VBZTLlvVIKJS7UBDT0tAU1D19RLsNPJkVKV0TOpB+DlbzMCUk6OsVTPCAPncapXptRKkLAoNNZCNgspxiG2HzIZKaK+Ipw6IqJIzE0EWcsyvJQTtDLOrRzw59Zv8Mutm3z2D/4y4ZMElbv7FY6dN3ORuvv1UwnzT9YmDA+RvUrhXZXjSRFQ9XoDfKEuxQucZPlI4rAwBavgY7VAr7JhBDiUAkyKV5FCSVFUBVKkQJNNzrnKugAHFH7qU59ib2+PhYUF7t275wE7Yc40Gg0Gg4EvuqVoaTabrKys+ORcSRmt1+s+oCLPc548eeIBQGG+SGCFAAACUqytrVEUBa1Wi+FwyOPHjz0gdv78eV88CeCWZRnXr1+nXq+zuLiIMcazaaQ9pXgVb8HRaEQYhl7KB5xIPa6yTMIw5OzZs3zwgx/kBz/4AdPplN3dXe9DJ8WfUi5lVHzDJBxB3lu+5L61Wi2Wl5c9mCwAq9x/YaMI+0kpxeXLl7l586Zn3gyHQ1ZXV1lfX+fChQsEQcDa2hq/9Vu/Ra1W8wC3FMbSL6pghfQB6Vf1ep2trS2//2nJfhXMFhlht9vl2WefpV6vs7u7y+7uLktLS8xmM1599VUeP37sATRpj6r32ekCvSppr8r+hV1VZf9IwSzhNtPplM3NTS+d3tra4sMf/jCXLl3iwYMH7O7ucuHCBZrNpg8XaTab3Lp1y4Mwktr67LPPMp1OuXv3LpPJhBdffJHBYMDc3Bxzc3N0u12f5Cpj9tKlS3S7XR48eMBkMmF1dZU7d+7Q6/V8CvNwOGRnZ4fBYODltpJ8HQQBV65c4UMf+hAXLlzg9u3bvP322x7MqtoQCOAmfbZWq3HlyhVWV1f59re/zdbW1gl2tPSF6vbjGIRyvKp8ubpfNYBJmFziASjSd+m/p2W/Mr9Wz1v6hsiAq2zXHzd/fvKTnyQIAg8SNhoNlpeXefDgge/r29vbLC0t+TlaksGFWSwgofRzAXAE/JS+J/O1/F61bpA+LD+LtYTMyzLXVpmP0m+rDOzqvC9gm8zpYhEhr5P2qzK2q2Bw9R5X8Zh2u82v/MqveF/Z119/3SdAi2fh+fPnAcea39nZYXd390SAi8zdYnEg+wp7UwAy8ZAUzEiY5EtLS9y+fftEoJWM/ypQLBYHt2/f9t6Jzz77LFEU8eTJE9+fq0x3AVPl9QLOVfu7tIewZ2WBqDovn144kXFWXSiUviP34fSXyLIFjJbFAZEet1otn6IsNhztdpsoihiNRuzs7KCU4sKFC1y+fJmNjQ2vRJBU7E996lOMRiPa7bY/znQ65enTp/+/AxD/KAdWSvHX//pf56//9b/+37tPmqb8+q//Or/+67/+L/P2/z1vCFY5Vg8Kkr4U62WybeFYHbJOkifKm9M7w/ljlogNlGcQ+sOXzBGlHVDnpMV4YI+STSCBIjnOx6nI8f6Iwi4JpsdedbYEPovSyy4o/bbkvOUalLW+MDUlQ3JYJNR2HKgRDWHaUaQHhtYd7a7t0BJ3XXEiwGM4CqjtFwRTiwkDHzLgsCBNcugYU8M17eTguGuLu+73aGiZzrtgAZ278wmm7trDngvLcIzMsm0Cxc9euUWqDA/yFl/sXeHPzr3G7dkZ4r5lf7sBaUH9aUYwyTFxQEuXYTejjKIRnQgQsOFxu+QNV4BJiAGU361jFF3+yCPezhxrbmQS4p5mZgJmNnBsQJWRW81sGtEozzVSBftZg8VoCMBHmve41VthNgsJtDPbN7OgTCk23JuuEJYFbppkDMrwnnhgiIaK8Rt1ei/Bj3ZatN6OiXvuXjcb8P3NF5ms5+iLhrinSLvG9xUtwSmFY++pwjrpsvgfWouJT2nftGMEKu3kfkWhUQqiqGA2jcizwBXvQJFrWJxhQ4M2ZcrofIad6bIjuxsfxOaY+SMd1jrQkFyX1bNjBxXo44TzXDvmosXtUwEXPe0mMuQNSzhy7M/DFyx/8tOv89rOBt33FjA1w1956WscPV/jS49fYPfNFYIpzJYKgrahfVthtcaUtqjqa/Pc/bRm4+wBxYZie3eOMytdrnx0j/u9RR5tLqJniunPHTGbRtjHNYr5jIsXdtntNwlutaltOVuDYAZalQEvKfTPaFoPHdgzm1McXS8IewH1p4rmU8PWJ+DX1t7lv/q9z9CZukWHcATR0JDXVGlD4ACqIi5Zo5EDMfMLEz638i4HeZNOMPI+bqnKvDzZ+bapMqCkTE62lpFJWAgGnlkFMLRRJUSlRs+kGKup6ykGzUZ0SFYJTSlKKmhHglkqnw27pk63qDMrAZrlsMe5aN+zIwsU3TKoJcBS11MfxAL4gJev9p5n/lZBujtltFpnsmSp7xoINUUSEI4L9LRg4a2MYDjD9AfY6ZSg1YAodH1PKWwJOBe1gMFG6JLOAzybFY4XXFRRBmvlbuFIgDphcYdjSzhx9zhPj8FiXTiQL5jZkt3txoMNjxtGUo8FICySkr0I3gtRFeVzJyxJjcqxxbGlB6JWFJE+DooKFGQWk4bYQGOSkM2f1fzpy+/wpXdehL2EsK8IJo6xXtOu/xSJJZuzTBLnDxi0Z0RRQawNMdCuT0iCgv40ppXMmBbuA9AsD8iLgMd7HYqDhGQ3YOWepb6Tu+cokDcVc6H7kDexDlBOVUZLjx0wbGM6akRGQFFJOh6ZmF984U3+8NFVJuMYrQ1B6CS+RSEfTIVR4OYfpaxf+IiCwoOKgXYLFoVRmCJw4KI2mGFEqI3zNVSGSZkWHZTSeoBUOwuAhpp6Rm2qcvdMK2X4qSoYmoS3JufI8lLGPgkZb6eoXJVp24pkeqxMyOswXbDYtQkbK13ONrtcbuxxKdnl5dp9XohiAqUprOHQjHk3q2OnAe27kB4Y/5kEBf2zAUVyai7/6fbHfqvKJMV0Xf4mhXLVN082kbBJcVSVAkoxUfXMEqCkKkGrMnmqxupV2aIU01I8CQhVZekIM6XK6qtelxSrck6j0YjFxUVf3KdpytHRkfdzi+OYhYUFD/YJS0JSO6u+TuAKVim419bWGAwGRFHkE0EbjQaj0YhWq+W9rIRhKOzCyWTi2zlJEq5evUq/3yfPcz7ykY/44u7y5cscHR35UJbV1VUPJkhKqRR1EpAi/oR5nrO0tMTe3t4Jz8oqYPGhD32IOI759re/zcLCAnfu3OGTn/ykv2bxYRT/u6pcWc59f3//BMtE2GcCoIhnY5IkHvhrt9v+HDc3N7l8+TL1ep00TVlbW2M0GrG7u8v9+/e9jHc4HPp7s7u7e0IyCvwL4CHgpbbSV4T5dJoVeJqFJt8lvVZYSuDSYwVckv4i7XRaYneaFVkFSk7LmQUIr34JO3B+ft7f6+eee47BYMDGxob3RHzf+97H4eHhCf9AScC21tLv9+n3+z6F+Y033mAymfjzAFhZWeHhw4d0u122t7d54YUXaDab3muyKAofziGMXpEk7u3t0ev1yPPce/SJRF/mjdlsxmc+8xnefPNN1tbW+OY3v4m1LjhiOp16BnGe535RQe7d4uIiZ86c4datWzx8+PDE/6osv2p7V9nMwq6uAlLy/yqTWRYm5HXC4K2CWXIvBbQRpp2cjwCEAjZWX19lVlfByDAMWVxc5LnnnuPevXscHR1x+fJlL5vf2dnxc++NGzdotVqcOXOGw8NDRqORn0dlE3BvaWnJZz5I/5R5t3r+4iFZZTPL9QgwXmVty7kMBgMGgwHD4dD728l4qPrkiS2EvPbH3QdpV5G5Cyhdlf/Luchroiji/e9/Py+99BK/8Ru/4dOfV1dXmU6n9Pt9hsOhX1CR544sIMk1yDwtc7L4ga6srNBoNHz/EwajJLnPzc3Rbrdpt9vs7e35ub46F1X7qFyzMD5v3LjB4eEh1lpvw1ANKZNzrNozCPhbZXVWLT/kveM49n6J4t1bHR9VhmR1E+CzKAoPrkqA19zcHM8//zyj0Yh6ve7T340x3iJjZWWFtbU1PybW19dZXV2lXq97Ofje3h537tzhnXfeIcsyDg8P2dzc5JlnnuGFF15gYWHB3xeZO04D//9D2//XKcz//7Qp44q9ogRWdGadJLhMP8ZaLx2Ohw4Q1LlF22OJsngFKuuAwHBqyLQ+Tl7GJQCrUsJcNdMPp67odAb8+jhZNXXhKlYpz4pxqbclM1G5tGZjHIgYjq0HxIq4ksxcbnlNEQ5hkMfkdUXWdNeetZw3YZE4MHG8bGk8dgEnSdcyOKdoPnLHLlIHYsQDewxixm6/aORksJNFTW3POP+2Hdc+6WFB0ivI6roENuW9NeFQYUuAyqrS/7Cj+GjrLv/R1hf4zpMLtGsT3h2s8oOvXWdtt2A/MrTmR0DiZIa9KY1RRt5KCIYzgsMh8ZZLHCYvsLWYoh4T9wOKWBOWKdkiDXX3zZA3Y/69C7/Pq5OLhKFjQkV9uNg6IFAWOw2YEVAYjbXKg8GRKniazfGh5gNqKuZ8dEBmAqx1Hl0B1oNwSZAzKBK0smgU8/Ux/fkao2lC3AuYLCr+0r/22/xc4x0mNuC//OAn+fKjZwm14Vyrz3vfvsDZf+6ApFkT4m7FV8Zaxzo0BlTg+o5MSLkLWhE21awNaItOCkzmUn6LQhMERkhPKG0o8oAgdPTSolDoijRQinoiRzlVgXHMQm1QCqaDBBUYgugUsxDHSCzykgVWvk5pi8nKv+kSgIEyebYcN8qx75JDRe2gYOUHmt9vvkB9eUjtQp/JnTZ/4yu/wAc+cJdf2niTo7UaPzrc4NYb50h3NFkbop5FT93xBhcMahKxda9F0TAQG/a/t8rThWWaZ3uOhRVEjPspH7rygPhCwb3eAqE2fOHC2+iLlm/tXGL7jVUGFxRFYmlsKmq7zptt+zMF4WFI5x3L/OsB3ecNNgyYNRWf++TrfPHRi7TuKUzkxmvcd+m+eeoCOmwpXRZ/UhPArGP43LVb1PUMrayT0JdJyQJ4GKvJyrTYQOWkquBx3qGtJ2gMMQaDM0A1VrOVd0j1zAeZNPSUhppRoGioWSlTDssAljKZGcuoBF0mNqJXpEzK8JS18Mj7GK6FR94vcTEYYqzyoRoFilHJXuyblKFJGJmEr3Sf5/dvPM+ZWBEOZqx+X9G7mFLbmVGkIcHEgYc21GAMujciLz9csHeAPXvGEQIVHqTLmgGjVXePdK68F2peK0MsjJOgWuXmxGCqSs9ZRZl35LxDE0tRk4nfHSecOM/M+Mi6FORJCaCHirym/WJQMLVE/bz0uC3vVagwylkcFDX3XRakihSmc25+tgHY0BL1lQ/jQuFT700Is0sT/p0PfoVmMOEPGtfIb9fImpasU2Bjg0oLgsgQxTl5rklLgK6RzIhKC4PCKupRRlYE9Pp1Dp7MER2GpDuKdN/SOCpYHBQE0xloMIF2fpD9nLwWkDdM2T/cgotLAlcnErczGzKxkffrjFXBcthnkKQ0U2F7lM89ZdGhs4kAyIqAIDCesBmHBcaCVhCHOeOZ65NZHjDNQvIswBjNkYGgH/jjFFb7BOaJjcgIXLq4iTA4CXNbuXPZN3Xempzl7dEZ7vYX2e636A9qJG/WaGxZiqsOEG7fcT6/kwXF6Iyl9myXnz33Lp9svcdy2GNRj1kODHM6RqPRKHaKEbfzJl8a1R37tnz/9yarYJwkPKs731fHesWDtT/dfvI2KcLgJNNJWCtS5EoxdVoeKGCQFEZSJAr7oRqsUgUGpeCD45RKKWSF0VZlnshrToM7UjwKGCTFTrUwFaBxf3+fx48fe0ac+IkJmwbwLInRaITWmrm5Oe+b12w2vdxNEnlPsx2bzSbNZpPd3V0vxxTgVBJ/Re4mIKT4lgljbWNjg3v37rGwsOC9sN73vvcxGo149913PQggLBlJzBU5dlVqOh6PWV5eptlssra25sMPxEuu2q6XLl1iPB5737EHDx6wvLzsPa6kzatAmzBTBTzL89yzIsMw9ACSsHCEWSZgcK/X8wBvq9VibW3NezfevXvX+1zOz8972faNGzf4wz/8Q8bjsU9pFf/JKjAn916+C/NFGEOnPTer7CkBrqqST/Fnkz4p13P6PQVYlTaQ95H+UgXXqyw4OQf5Lu8hvyvlQnKWlpb8/RiPxzzzzDPe02xlZcXLkm/evOlBxHa7zf379wmCwIPTUtTPz89zeHjIZDLh7NmzbG9veyahANCSwvzgwQPm5uZYXl7m6OiIbrfL5cuX6XQ61Ot1zp07x3g85ujoCHBhpFEU8Z3vfMczV5vNJi+99BLb29t0Oh1u3LhBt9slSRLq9foJsF9r7VOCrbU0Gg2ee+45+v0+77zzju+HVUCwulWBJqXUCTCmCoiJ7F7+XmXeyRwmwGN1zFQB+KrMt1ar+dcIE03mzyqDTsCdKtvxpZde4vr16x6YvXXrFufOnWN+fp6dnZ0T/rTizycLFCJnljCVKjCYpqn3RK1Kp6tzc/V5IHLoPM9J05S5uTmfVC4AdRVMlD5bZYnLPCR9SN6nCppWAbBms8n8/Ly3jVtYWPCsNZHwygLMwsICjUbDM2plrP3hH/4hw+GQa9eu8fDhQ8+4huMwE0lWlvsym81OzDXLy8ssLy/TaDR8yIqAn71ejydPnvDKK6/QbDb9OWRZ5kFyec+q1L26UHCajayU84s9ODggjuMTvrJVdqvMY7LJPnJM+b/cb7GUELBU0s3lHkvfOG27YYzz7O10OiwsLPjngLS1PPsXFhbIsoznnnuOhw8fejbwpz/9aS5cuMDc3Jxnxe/v7/PkyRO++c1vsr+/T5Ikfn4RxqzMD5cuXSJJEu7eveufcYPBwNtPyALLH3X74w8gClBiXLiCnjmpcpG4kAkQMO4YLAyn1u2XKkcksS7ExEQlmyVS6LgMhgjL/BMv63X7AaXpPdgMsroimLkiVc9K9h94qZsUwKZscWHGmNCxsJR1BYXOLeHYeh9ED2wax5oMJorleEDmbEOYdUovwpKRk9fdOWdN975ZS2FDy2xOgdLM2orJkiU/KIsXW/7e0OiZA2eylsGEmmDqQh5mcxD13e9F4uSX6b4l6cFssSDdC/11KuPa92hdsx4e8ng0xwsrW/RmKd+5c4nmflnQD0POXjliGp/BRhqaEbN2yGA9oP0wJH0yQhlD3kkpIk36xiPC8QR95RzBs3Nesuy8Ikt2TwHdawmXwwO+3r/OM6u7zGxAOLJspF2GJoFCueLOKkwhmkfHWPlrZ/4ZE6vJifhPNv8Uw1lMPguJFAxtTL09QRcRC8mIROcs1VxaURLkLC316d9Nqe/mNLYsf+c/+Z/w6xu/RF63FK3CdaLQ0k6nfPSzb/Od6Dla9xRx3zoGZj0mmLrAFKWUC4gwoIwLTrFhgLIBxULDSyvHq8b7D6rAEkQFumTtgKIoNFpbjHaywCzTDmApNCpwpv9KAaFjCtoyzCGIHQgxnbiObo1rKxXYcp+SYVgoCHDXVh5LaXuS1gvHnojl+5C7cRj3rPOCfKfH3NuWyZkmk/mA1Z0ZUW/G/pcv8V+/eI1gBq1HhtoVXY5jSh9SCwWk25o/8cl3ebzeYVqE/Om1N7j//iVe2zvHk/05rFGkLx1h3p7jVXuBxtyE4VFK9DTmXnQWszJ1AGndgIFnXthk5aN97hwtsaoN640jnm895cYnznLjO9eoPQ1oPDEc/lkHpA2+t0Qjk+AUt4ChjHWp8LklTxRZw/momtgBSpdfeszL7Xsc5g1awdgxXNF09MjLigEa6tgLpUDRCUbl33N6NqFvUvbzJpkNaQVjAizPJ08Zmcj7GQ5NSktPMMaFSEharbAHeyZlZJ2EGeB8tE9Hu/dJy/fZLVoejOyb1LMjJ9aFqTzK55iYiEgV3Jsu8xu3PkD+qEHzqcaEhqIRowrD/Js9VCHjwc2xRSN2LMNaQnhmjXxr+3h6j0Mok+xtrF3a9hPFtOMYomJFoTPQY42euQEtTGhwc1jeKIMrtOuv0VCRHCqCKYQjx0gMJ8YBgiWrvcpIjwbON1HnLgXZRO4ZI7YZeepAoiKFrA3TpYJwaUSzPqWdTtFZRH+U+iTfqVFsnDnkY8v32Ui6JNqx+xratbHIyH/t2nf5O/knSQPDUnPo2HrKkhUBhVVkRUBeOKuFg16DWTfxQGFxYKntF1zo5wSTKaqYUKQhWSskawZMzgYuyKQBWcN59jYfh+x8suDDL9yhFUxo6Knvc5LCPbQxAZaZDYicwQGpyohVARqawYTC6FJyXBCHBZOZfJC3xGFBGmfkRUCgDdMs9IChUpb+KCHPA0yusdMAcuXDvWjm2NjSCGZshF26pkZmQzIb0i9q9EyNqYm4P1mkQLMzaXGvt8B+GfKiRxo9E8aopagb4h503ptgdcrRM6VHqbb8L/+df8K/2d7EYBnZGZk1ZOWHqyd5xDeyRd6dnOHXOq/y+6PL3Jsuc3uwwsG0zu6wweFhEzsNHOA5cz6/0YH1LMReGkLKT7efsE2Ky2rghxQrVdmabFWgpcr0EtmzsIakGKkyd057KErRJN9nsxnD4dBL/qrvX5XESeHz48AiKTIF+KkWXlL8i9xLGDECPoFLZxa2kzHmBKiYpikHBwcAPpFXwEop1sRTb319nV6vBzgml7RTv99nOp1y5swZb8IvbS3Xtri4yIULF/j2t7/tWZDNZpPZbMZXv/pVRqMRV69e9a+XQIkoijygJqyWVqtFp9Px3lLnzp3zRabIj4U9uLCwwIULF7h165YHED/ykY8wNzdHFEUsLi560LTKBhVp6tHREb1ej6WlJay1HB4eelnlYDDwAQZyvSJdPDo6QmvN0tIS+/v7bG9ve1nb+fPn6Xa7bG1t0e/3KYqCW7dusbu765meIgU97ekl4IT0c5Ecjsdjzp8/z3Q65eDg4EQxX5VtwnG6thxDfq/2ySqYWmVYVb1Fq0ChgClVAEH+XwXhq8B7lY0rsnsBKX77t3+b69evc+bMGbTW3Lt3j9dee42PfvSjfOpTnyJJEp48ecLt27cBFwwjwKFI7ZVy3mXNZtNLiyeTCdvb216uPB6POTg48H2g1+tx9uxZFhYWPFMR4N69e358iQR+ZWWF5557joODA27cuOHDX27cuMHGxgavv/66X4jIsoxOp8NoNPKAjrR3vV7nmWeeYW5ujq9+9ase7Bb2ZBWYq7KQpX9UPfiEFSeLJVUARpjXVdDp9FwoIE2VSVa1WqjKcavAdNWqQeTMsi0vL/P5z3+eWq3Gt771LabTKa1Wi9lsxo9+9CO/v8yfEnxizLEXp8znP65PV+dXWcyQc5G/y7gWAEv6hYwdpZQHkCXIScC4o6MjD/LIfCYMwqpnYrPZ9AxjAYyErS3yWmnfvb09bty44Rcl5DkzPz9/gqlqrfN6ffLkCY8fP/btM5vN2N7e9vvK9QaBS/etph/Pz8/7dhNf1tFoxHvvvcfu7i57e3s+kCVJEi5evOhlxK1Wi2vXrvHiiy/y3nvvsb297Z83MhdVZdfVZ5OM6ypLVvarPkNlDqmCfbLoJnNKFYyVey/9UdpQKUWn0/HHF2uBNE1ZXV31THoZk3mec3BwwNOnT+n1en4eqNVqfOxjH+P+/ftsbGywv79Ps9nk53/+5/n4xz/Od77zHb7yla/wzjvv+OemBDFJEvu7777rn5nr6+v+mSzMxGpbtVot37f+Zbc//gAijjFoA1f0FakiT47lrg5gwnsPBpl1bJHQMRN1KR11qc3lARUU5f6qlOuKl6AuLNHYgXoepKuVyZalT18wA2VPAoYiCxb/w2AGJrCehRBOLHlaFjalNE/npeQNBxI61iIsRkNsCLUdW8okSwB14OS9wVSVickO/FO5YjpviY9coQgw61iSA8W0YwknilnbUn+qyBrW7b9gaWy6pEyMk23Fh4rZvGN+TRdK4DNXRMMynTpWXko8PptzJTrk19a/zX9y5/NsP5qncS8i7jrgNjnQTipac36GwbR8YISOqRG1Y6xWjJdjigiCyTp6VtC72ijbz3L0DERHmrm7zpcumBR0nwlZDpxn1ueW3sGgCUcQKMP9yRJ6pOkbt4plp4HrJyUD8W/s/kku13b5t+duYVAujCRzvpn3Z8sE2knQFuIhq1GPh9E8Bsu93QWKLCCoWUbLAUFmGf98n6tL+9x8+xwqLYjrM2ZPGxz9k3WGkzO0aopo5PpoMJh6wJCwZF1WVRrWorLcMWmTwPW9hsImpb+Itg7gLmvsotCIp1gQuEAAU7IKC308SQRhgSm0u87CsQ916JiIUVSQzcISQHEpytZQypAd09DmGgLrAlbKxFJbVGwAdMmgtar8WUHoWF/KBA6Aj5T3fUu2RyT7JXCvoLY15twTg57k2EDRfldhahHdaw1GqyVgn1lqe5bv/MMPOnl9ZvlPL55l8X27fGHjbd5urPGwN89zC9tcu/Iqh3mdYZHQujTBvF/xrZ1LPN2aJx+HbFzZ5cl2h3ff3WB7vcfRYQM7CdjeWud7rWdpXDlCbYxp/Sild0nz5679iH/46kdZfOzmH3As4iJV7L0Ug4K5u6ZMUFfe82x2acKfP/MaR0XNMahKhhc4abE4TzmmoAD9AQGWmMJ7Gh4UTSY2Yjl0RVVHj8oAFfc6rQymPP79bIm2ntAuJc4TE9O3NbZwYSstPXbBGFjvrQgwQ9M3KYXVzHAJto6pWGc3b3NU1JkLRgTKcD15yv1sid/8bz9NOnCerNMFS/MxBMMZRT3CpCHBsEBNZzCxqMKghhNs4gDTYqNMD8sy1GBEMd9yYF7ZuaftgGDmvF6tVsd2CzgWtwndAkoel5LYoXLjf+Ik6tHAzc/BzDGsQWwc8L6HKrcEeTkALZhYl/61gbeuKGJnVTFrw2TFYOYyovqMRm2GKQKiLGA2jOnu1+gPNOmeYv6hOQ6oymH7I6vUfv42IxOzEA4csGuc5x9QymxzOs0RkTYE2jDJQ3rjlMFug/AgJD1QpHuW+a4hGBuiQe5WtJSiSDR5M2C4HjNrJR7gLGIo6hYTWmxoMalj7YZ7ETsXCn7tE69wJuo6n9dSmi6S9LaeeH9LcNYOqcqYFA6clv7cTif0p46lHSi3qBEGxqXaW8VkFrkQkjxw1hC58qnH7jlZstpjQ9DISBLnm9hIZmyPF3g4nOcfHHycB6MF9icNuqMag2FKUYKO86+F9C+W7PGpm5OC0IH8JnHXLZJ9ZdzP3nM4gOEZxb/aust/M9hgLewyNK5If2+6xrujVW4drbDTazIZx/zrP/Mqv9J8yP9s68O8+92LLiCqbrFxWQwnBpR7zokPsgmcDP20ZcpPtz/+22l2UxU4kQKlCoRIcSegh4B5Ve8rAVmqDCcp2KvMQzGBh2O5ZlWOXJWjViWmVfZF1ZdLvp8u+k+zhLTWrKyscO7cOQ/01Ot1tNasr68zPz9PGIae5SdyYAGqqgEbQRB4/8gwDNnZ2aHdbnu5XBzHHBwcMJvNvE9dnuecOXOG8XhMs9n0r5drWFxcZG5ujqOjI1/QCSNlZ2fHp9M+fPjQA0kidTs8PCRNU4bDoW8fYZ40m00mkwn9ft+DOmmacv/+ffr9Puvr65w9e5a//bf/Nlo738qPfvSjvuhsNBrek1D6jrTf4eEhS0tLHoiVMBPZjo6OWFhYoFar+Xt/eHjoC/BHjx75dhqNRvT7fZ48eeJZMAJsiUQwCAIWFhb8e1W91OCkPPh0vymKwgeoCHAuAFMV6Ptxx6iyBU/Lj4U9enocnZYty5iTMVFlxFVlzPJeVQZSFQARD87Dw0O+9a1v+bEqgTJvvPEGKysrBEHgPTEFnL57965PUP7ud7/L5z73OZ599lnfT+7du8fBwQHnz5/391JYiSJJFJnqYDAgz3PW1ta8RPLWrVsesJawoVdeeYVnnnmGxcVFrl+/zsOHD3nxxRf58pe/TL/f9wwurV0Sq0jSBQwPgoDFxUUuXbrEW2+9xf7+/gl/wdMS8CogW203YckCJ7xbqzL0qoRW2t0Y45mC1TlQ+oCAYwKoVUEwGS/CBBMmXnXeSpKEl156iaWlJf75P//nnom2sLDg36fKzJa/iYy43+9jrfUMZAEFpe1kHj86OmJ5edmDtNUFnOl0Sp7nNBoN1tbW/DxaDYmaTCbs7++zubnp53C5xipYLgsUwrAUuwj5EhaZMYbDw0Pfj4QlJ8cRBrW11i/2rK6u8vzzz/Nbv/VbvPvuux4clIUjub8iA57NZqyurnLmzBlWV1eZm5vzLLrxeEy322Vzc5NHjx75cSwyZpmz5ufnPZgrSc3yvMvznHq9zpUrV/jmN7/JjRs3ODg4OLHQIH1A5uXTdglyrCp7X+aVKugchqG3kxBmvdw3eW3Vg1PmoSRJToDbH/zgB30fyvPcg6XiaXtwcMDt27c9kD8YDDxjXYDL0+ctc8qFCxd48803+drXvsbCwgIvv/yyB+flWSuAdxzH3Lt3j5deeok33niDg4MD7zc7nU5ZXV31ie8yjzUaDd9H/qjbTwSAiHJm+E4Opo6BN1t6Wk1dwMqsqSkiJxsOptaHfRRJyXCRh5IUFrYE9upSxDv5mg2OmYamTED2bJcyadMb5uvj16IcO88Z1RuInQ7aexGW3lp5xRcpHBvHqIxLCVwIT6YdVA7zt6aMlyKikdsn3Z8xnY9KIMUNsGmn7PS5JRwZwklAnjrQMR4UBJMAnblrrO8aspb2ac3xwDBZUkQDmEa4IJUlx/bJG47VGPaFieP+TsmabK4NqCv48uELbD9cIN4PUDne8ynuQTuacNBy7x9MC8fcsni5ILjizoQaG2mKUDNa0e59gHDggFLvRVlYFp7f42vjM/zGd1/m//i5f0yvSAmnTiJ6Juli5jPuzpYpbMmEU65gTHTGwaxBLciY2pz+LCHQBjKNjSx9k1KLMw9ML4c9no7m+J/f+3nUnQYqsV4Cb5Vi3E3J5gPWL+1xvbPDz83fZO3DR/zuz7yPe8NFLtQP+IO/+XHSvkGNZ6BTJ1suyg9roZMvW60hjpwPYl6Q10IX3iN90rjEZawt5cgudVlrSxgassxFCIhUWQXilWgw5Wttye6JUheaYm0ZHDA7lieD8zcMYkNhHBioIkMYFaXnoiWISkm0tsehKYrSX9H1DcdYVJjE3ddwUKC7Q+d3FwSQFShjsKF2oCp4bzhVJvK2Hk1RNsYE7v6ZEFqPCpoPRpAbln4UcvjuMv94ZYXmpqHxNOOHL6zwg891ma+Pefz2Ksm+ZnJlygcuP6R1fsrTXpt2MuGz7/8+c+GIW8M1HrfnuNraZZgn9POE2/vLhO/UCccF5372ATe6Z+n8MHYs0MAB9sHMLQSk+5as6UCtPMXLVaeLBX/+xR86FpkJmQtHnjmYqoxU5QxLCbGkGBcoJjZip2ixm7cp0KyFXdajQ0Ym8dLnAk1KzsSGXqLc1lO6psZiMCDA8s70DBMbUdczGnrKWtglwLIQjOialMwG7JsGEQWdYES/9Dks0HRNnd2iTb9ImZR+i+vRIevRYXm+OeeifUbPTFn4TkzzobNUCGbOv1NnZXhO7GjdLmXcgCrtGoxBD6fkZ+YJt7vY/gDdqGHSkPBoimrHpIduHk8PDcpqJguKooafY1UOUQ9qEwcWHi8kVR6K6jj9WBZ99MwQZIYicRYJeV2R1YWNVoJuNcWsBXndkncKwvbM2U9MA3QvRD2NmEybztYC0JEDsIrUMrxoqO1qkp5BGUuyn1HbqjEXjkhVzqz0ptwI+xQodnO36l3XUyJt2NyeJ3qQ0LkNy09mnBlnWJ1jIic9zmuayWJA72LoFtFqIusGUyYJ29h4UE5HBSqwJHFOFDoUtlc0aSyOWAoHaGVp6KkPTBHAMC2ZqVoZx0S1mozQB5oAdIIRoywiLzRZ4dKKTbmoYTINM43KNWpWzr+xgdCi0gIdF6RpRhwe2zqMpzGjoxrMNAPrFq3uvLnBHTaOH9Ty3FZORZB0LYNyXitSxyqVPqIzhR5r4q4ia5bP/aLsJ5SS+QCaOqWlx/yXO5/ile8+TzBWZHMFwTBwIGHNeU8WQGYNuXEgoQMjLSQGCkUwcHN5kbhwNfe8M/TPhsw6P5Ux/6Rt1SJUZHbVoliKZZEy/jiQpcrKkUK76l1WBfTEz+m0nEsKKimcqrIuOR8pvgRkrPqVyesFgKgWkyIVFl8wYcQIQ0pAzL29PQ8ySVEnDBwBMhYXF+l0OqRpyvr6ur8uSVB+3/ve5436z5w5c8LbUSn1L4SJzGYzOp0OL774Im+//bYvtsEVmS+++CKbm5vcvHmTvb09X8QPh0Pu3r3LCy+84Jk2Ip+WAk+ktnDM+Nvf3/csHgFABdDodDokScKLL77I8vIyh4eHzM3N+fsnRZ0cS9okiiLv2yXHs9Z6ny2Re58GEoQ99fDhQ3Z3d718LQxD1tbWeOaZZ9jZ2QGg1+vx5ptvennd3Nwc165d48mTJ+zv73Pu3DmGwyFPnjzxoIz0iSp4DA4UElae9N0q+FQFkIQ1dNrzTsaH7HuaCVuVKFfZQlV2XFV6WJVZnx6DP45JK5Lkql2AMLBE8ptlGZubmyfG2NOnT7ly5QpXr171gNF4POa73/0uX/7yl8nznOeee44XX3yR559/3nvCXb9+nfF4zHQ69cBzt9vl9ddf96Bip9PhnXfe4e233wbwDMZms8ny8rIfvxcvXvRAxXvvvcetW7cAfJiTXK8ASVXA/sqVK/T7fW7duuUBWJG0Vr3zTt/3qmy1Kj8WALHKLJXxIscTFpmMl2qQSpWNK8ernkeVRS2Ai/h+iqefzAMi50/TlMPDQ281UD2/6rxZBcXlHo9GI8bjMfPz856ZLO9Vq9W876hYKwiDVxhpAuiIfPzw8NADZVWAtcqIlTkYXAhVu93240/GevUeFEXhpfBRFHkfQZHW7+3tkWUZS0tLPvlcfGrffPNNlFKsr6/z4Q9/mLW1Nd544w0/D8t4lLl/Mpn4hY1Wq8XCwgKTyYS3336bvb09L4MWRqIwNqts0263e4LdLIC6/C52EP1+n93dXR8kJP3w9HxRHdvV8S7gsjyfOp2O73NyfTIfTKdTH/QlYKw8NwQITJKExcVF5ufnieOYvb099vf36fV6fO9736NWq9Hv9xmNRn6caa15/vnn/f3e3Nz0z9tarcb8/Lz3gwTHZq4yveUZLM+jNE3Z2Njg2rVrZFnGe++9x3vvvcfW1hYHBwd84Qtf8KzZRqPBkydP2N7e9nOB9Bl5DsucIAsOVQbw/9D2xx9AtE4+7AI3XEFoQsdMcowUhZ4pD1xFI1OCfK6ILFz/cGAEHAMdOEBKlbJnx5ZyAFgwdazFILOOKaHL9y19ryQt2Nc1pU+fsN2UgaxWPkxLaZN7f+WZjk5+bclTJ9tMDnOUCZl2FLd7y0xWDf2zCYfPQW07YLJiWXgzoXdRs/BOweGzMXHX0r8IKz80DNYCmk9heEaz8E7OtF0GruSW1uMcEyrioxyrYmr7OVkjIBy7RNr6bu6krRNDNIyIBwWqsOx+wAWdHD1bUN8YYL85RzByks3p23P8tTO/wFdvXmf+R4ELlonxhVw0sBgURaRo3XUru3pakHSdb5zKDLYMGnBp2GWwSHlfTAitBxZl3TXo3JK1Qj6y8oiJjVh9RfPXwj/Hy8/dJU8Uic5YDvsEseHRZAFjFSpXpX+iA2+0MqQ6o8AyziLGs4jOmyFH1wumJqIWZdip5asPn+GfTZ/HPKlxz0KYK6LSjy+YGqKx4dJ/B6PmBsONgO8la/zo6CVmbedbmbUMr1nF4rQM1tGazZ/vkO5alr+77xh5WqNmuQcUAWwUesbqrMWxJNg4yXBYAgG2TFEGTvghQkVKXzigMAgLFA7gs9ZJCE2hHR5eKFRYBrQoi1JQTAIHEIoHWa4JogJbekr6kBWDm12MgsAeg4eUkuZGznBDM3dXY1MnnXWenxbyApXlqDEuPKY8b1UYbBwRb+csPLUQaKzW2Frk5KYAgdtv4eYA9YYt2ZyG1e9nTO82sLrJta0h+mhEvtTkzseuoXJYujllWG/xW89epP9Mhh4HNB5q7nYukF8dc3Vtl+EoYfkdw+NfMPza4n3+4e/8DPM9l6bqPPQco1gX0NguYMcx5kx8nNBe2xjwQv0xExOxGh2R6swlyVYSlScm8kyvrk3omdSDM9eSLe89GKkCo7T3oeublEAbn4gcKMNO0aRvaj5x+XK8Q6wKFoIRB0Xd+y1mtvREVIbCamJVcFA00Rh2izaFVfRNjYaecjHeYyEYYKw+GeJS+uNdPLvHkVr3zOhwYhxAY0zp9acdo1UHmFbq5sqxe6CS5ehR5kDEnQA7ywhGjqGqxzlhqOmfDTyrLOlaosfWBy4JExyLDzURRb0ytvRDLAsHWfSJFOPliCJRzNqK8aqTmOqZwsQu8btoHjMS9UQT7wYEj+puro4cmJTXLbZjsGmBTgrS2oy0lOvWo4zdO2fdeSsFNsJEMCoS5uJxee8N3aLOcthjPTykb2qci/b5K5e+Qvd8nYMPNfh/fOPzrP9BRHcxdgs3NbeYY0MHVtrAOC/IwELokpd1ZAiUJYwKP0d471MgDMoPC6GTHNf1lFYwJi5DRtIg80Divqn71znA24HLs9LXw6Ap0E5OfejCSJwHSPkVlgBmc0YY5YShFHGWLAso8oDR0ybTvnZpxxNIMkgLGJ6z5A3n0+sXKbRFFSUgLCz/yrM3GDumcjB2vobh2IWFuUVFxe5HcMaLMn+UDMRyuLEYDLhztMTcO+4zRvc59zw2scVGpR8lUOCk5aIekDRvrANwg9lxsNK0rTGhW3g7wTT/6fYTsQmQJcwp+TAuIJSAaFW5sfwuhf1pD7dqIS5FpABOVbaXFHrCHJPCtJpIexqgrPrRVdmHVe+xamEmDCCR3YmsdzAYsLy8zGAwIAxD7yfX6XTY29uj0Wic8Lw6ODggz3M6nQ737t0D8GyUqt9XmqZsbW2dKEDr9foJ0FTO8fz589y+fZu1tTXG4zHr6+s8fPiQzc1N3nrrLT796U+zubnJu+++y5MnT5hMJqyvr/sAAmFnrK6uer+51dVVrLUesKzX6ycK/KOjI8/kuHv3rgcQJIRhd3fXs33Eby0IApaXl9nd3WVnZ4fRaOT93QRMEVDEWuu98i5evMhgMKDT6XB0dIRSiq2tLebn57ly5Qq3bt3iW9/6Fm+99ZaXorbbbe7cucOXv/xlLl++TLfbJYoinn/+eT796U8zGAw4PDwkz3N2d3cJQ5ccLf6QX//61z3LswoSSWE+mUxIkoQ4jr3vpPTzar+s9ulq3zzNHKoGDlQB8Co4WGX3VsMzfhzrUPq1jKP/Pkbt3t4e586dI01T7t6968eN/L96XYCX6s5mMzY3N70kVUAL8c6bTCb84Ac/4N133yWOYx+KcfXqVV544QXSNOXrX/86R0dHXLlyhfPnz/v+VxQFP//zP8/P/dzPeY/FdrvNeDz2fp737t3j5Zdf5kc/+hHnzp3jG9/4hgcJJOzFWst7773ngSxpy3a7zcLCAt/5znc842w8Hp+Qhcq9EzBI/OcE2JCFDVk0qMrSq8m0AsoLECXS4SrjWhZD5He5R7LJe8txhakn4Iok+sq8Zozh1q1bfO5zn/PAWJW1KECfzNFaay9flv45m82o1Wqe/Ssy7Mlk4llonU7HS5yFbS1A6P7+vrdZqIamVNtXviSMY35+nrW1NZaXl31oU6/XY3t7m8Fg4Bmx4sUpDLLV1VWMMR4IE9apMB9FRi2WD0tLSyeeJeJfWB1nMsZkzhMZ8fr6Os8//zzPP/883/rWt5ifn6fdbvv5Xfq+tJO0R/X5KPdJ5vLq+G00GifuY5Zlvr9JH5Z5oLrYIN+rjHYB6nZ2dk7I8ofDoX9mCkM+DEM/btI0pd1u+3CSOI5ZXl4mjmMfhLSzs+MXDaremFEU+WeFXLv4ocrCkly7PPOk/532T5Y2ELn7hQsXeOWVV3j06BGz2cwv1EmbSF+V11XvaZXtLax6YaMPh8P/kTEQlSRnul+LWHkQ0QQQFCXDsAAVlACKciw/E5VJwqaUQZfhFMHMhaJAWQCPhaUo76n8/wLjADNfMEDpW6jKxM6SmWdtRW7n9iliRTQWgMQdzwauoDWBgIjuGqOeJeoXKKN50mtj2jmNbTBhRDgxpIeKpFcwf9sSjg3xkSaYQW2nBDSNuwY9c+87aynAeSJmdc1wLaCxpehf0EDIYEMT9zT9ixDMAoZnXFJz7yrMv60ZL2nGz04IHyc0HgTMem0S5a5j2lEsvGX44aOXUM+bY/AQfJvU9g0aS9YEPcmwQYCeZMy/McPGIXo4hcIQ7YfUHwXoccZkveUTrXWZZOnbvLCMl0I+P/cWn6495df/1T14uMDdv/sMUeYYiEMTo5Slm7kPanqiHXgbKhp6Sm9Wo9ma0NIxc8mE7qDG/L6hPz9jZ9Zip9ckPR+y9LfqoGD/Be2l7kXqQITeRU3jqWLvT2r+tc9+gw/X3Qfj/8udX+Doxir2/JgPnX/E6199BlVYokGOTdwwzOsOxVO58QVsdbO1mCJ2gHVeF/SjvH5J+g6Mkx4rF86TzVySrQSm2EKhQ+v9H4s8QCnQpcRYpM/uYKXXoVXYsqFVYFFl0e9kz2BNJb7Xlp1bmLeS7FxodCPH5u4c0JZ8fcZgIyaYNRxDzAoL16JnhZO3TguUtajx1I27okBlForCMdcARlrQT3cKUQjBsabblpLw9MnIeY+GmqJTR+WG1e8M3fELS9Sz1J5a8lcTrDYEkxw9zinaMXvXztMKYLiq+JUPf5d/cu99LP+wDG8qmVSSlB5OSnA11mR1x0A0JcBztuNW0QR4a6kxGuPDSoRtOCoSCqtdEIqeEjChpd1DwVhNgPWsRQcOGlpluMVWPufDUiYmYjEc0FAzWnpS+itGTMrwk8IGDE1CEBhaeoaxil3b5qCUpDZKpqLGgesB1oNJfRuzm7dLP0RLJxgyI+BTy3f5UrhBuufk27UHfXR/SJG00dMcG4euj8ch4ZMDbBrDfhfVbjrgPB+jrCVfaaMHM3RvhI0jdB4SWWg/1H4hpkiV8yQMnL2En18Lezynl/dGQGYTu1CUaVsxnXcesGZlylxn5OTfo4SiGxMdBY5NOFJEvfKBXoagFCnMVjJ0PSeIHGsujXKcr6glDXNqYeZShUs0aydwQVjKgCo0w2szUp1hrCoThS1pSa8e2cSx+krUfTnssRZ1+dT7b/Gt4llUhgMLEyc/1qFxPqjaYI0mjAq/eGCMxmHzCmO0ZwyHoRPIW6sIA8eW09rQCUZeOt/QU9+mAlSDY0aOTOKB7VgVFOVk1LOpm08M0MoI44I4yR2jG5hMI7JpyHSUkg0CojJhOp44aw8sZE2YLBu4OOB9Z55wtbHLK7uXefT6GWzgmKYqL20MZg7cC4fu+TNat+jcMVvbd6H1OHds7hDnWRkp8jn3THIp9ziAu1y8w02FbnzqGWmY02+oUnFQesyKFL1MozfgFqXKNZAqYKqnzlplMq8JJ5ak5wDMwZkA89MU5p/IrcqsqbKqBHypyjKrqaRSDIlETsDAatGbZZkHbKo+f6dlmdWiSgrUKrPqtAxUvgsgUC2qpWCXc5biP89zH6AhBWC32/WAjMj7pNgV1qIUwAIQVlllVZCxKAoWFxc5OjryQFqapieSPTudjgcv4zhmbW2Nw8NDD+pJIMI3vvENrl+/zq1bt9jb2/PMsqo33qVLl1hYWPBFs8gWhc0j1yggxdraGg8ePPCF7WDgfLHF3/EDH/gADx8+5I033uD8+fO0Wi22t7fJ89x7MIqsTzYBiAXckDYLw5C5uTneeustv4/48D18+JBarca9e/fY2dnxPo+ShN1ut1lcXGQ6nVKr1djd3eXx48de8iZebLu7u8zNzZGmqfebExZOtV8JK1I8KhuNhi9cpU2rQJ8U0VUWYrXfVceDgFNV8Py0zFnOQ4By6dun2YbVNq3+XPXqk9f2+312dna4evWqB6UkzKYKgMqX9BsZB4eHhyfATNmEFScycsCnzAowLv3pzp07LCwsUBQF+/v7RFHEysoKq6urZFnmPdKuX79OGIZsbm7yvve9jxs3bnDp0qUTrFrAAw4CeIlMczgcUqvVuHTpEqPRyPtW/rj2lrEu85a0h/xN+prc1+o8JCFOIt+vgn7SnyScSF5b7RfgglOEqSdsWWl7macEnJNzrc5ve3t7HBwceOaxMOIWFhY8O1PA/Vqt5gGnKIro9XocHh56cLAKhs7NzVGv1z17+ujoiMPDQ548eeIBHDlX2aTdhLEbx/EJ78OVlRXvQ/jWW29x7949zybr9/tsbW2xv7/vQ3oWFhZoNpu+LUUqvLS05NPZRbY6HA49qNhqtfyihjxT5D49ffr0X2D2Vp8r4/GY27dvc+/ePS/HlXCoXq+HhEwJ6C+gsABZp0N0qrL4qtWGtF2tVvP3rPr/6gJElQFbnSekzcUOYzwee+A4TVMWFxdPhHiJXcRoNPJy/vX1dS5cuOBlzgLqHh0d0el0mEwmfjyJdUd1fhHfQxkPy8vLJ5jawm7MsowzZ86cuM7q4gVwIlhMPjfI/FNdHJH/VftdVeEgY63VajEej31/raob/ijbTwSAiIVoWFAkZWFpIU9cIa8KB8g5ObLBBo51ZgMB7Eqgr5TeWlyRl9edZ1YwdWEILvTEvUa8CgEvhbZaUUTuPcXY3wOTgQO8vKy5lCxTMvCKWBFkx76LJih9thJX/AYzS94MfOLocKdBe3WAiZoMzilqOzBZVMzakWPILGgmSy5FNhpaBusBJlLMmpAeWKZz2l9766Fh2tZEA1dsz79bYDU0nxh0Do3vOcZH84k7/6UfqZLpYxiGzl8qHIMuve9U4WRa6X5BEYWo7Bg89LfMWiyKM+kRr7cVph6T1yNsqMgaAYMzAXP3U9LdCWqcMV2qEU5iTKI9iCvpyaoo8arccnRF8bP1bfYKy8W5A373T/99PrP2b5L8g7kyZXmeItccTBsEygG6wcw6Wbsy1MMZkSoorOXWrXVqT0LGS46l99u3XyR8o0l9pyAcFfTPJc6LsG2Ju4pZxxANnHS1tl+w/GrAP376Gf5B52cwZyfUG1PCKwPmGmPW0h4/TF2/DIcZJo3Y+IM+ejABrSFzk4HVGpUXDjwzBjPf9OC0S5DFFaiRIYyOP4A6SbMiz5xE2WQaIgds6FBAQbC5RqcuwVnHBUo5X0MVOMmfEvDPgpkFqKD0OizTll2RbE+Ajjq0FBlluIszl1PaeY4phQMuZxpCiw0ck3TWlmShY4DZBWOU49JYME10YVG5q9CPAUfjMMpphpo47zeV5ZCVQGJRoPPChdAYW4bRlGBjVIJCMmGWoR7h0RQCB0zZSDs2bADpoWX2a/sMi4TwdzoUkWMIp4eFSySva4LCzQlFopi23DwiDLVnPnGfLyy/hcawFh4xsRGLgfO+65savSJ1QRTAQjAg1gWpyohUwcSGpErkyYpIFc6bsGQWTmxE39TKQJWAi/GeBxDdMXL2iwapzmirKVuF+1DS0SMCbclsyFaecFA06Zkascp5PnlMWvFUfJx3PPApvnjOp895OHaLBoEyfKJ5m9/5+ec5fHURq+HRn17A6gXO/24XqzV6NGP/w4v0zyt00SkZgmeIBhAfWeKhId2dEQxmjM+3CEd1kgf7qFkG/SH1XoKtxcwWaiijwFT6CW5ey+sBWapL9rIiazp24XTRkp+Zsr7a5X2dXS7W9jnM6/zTVz/I9PsLTv6Km2ezpmW2YLA1J/cN04wkyWkkMwJtiLSzC4h1QRQUTIuQQBn6s4TDUY1H3QWC/Yj4SFPbtiw+yny20HQ+YGG1x2p0hMbQDhxALIEggAfxUpcMw8TEfGzuHkufGPDP7jznunCUl56nDhS0VkFQuHR03P+1dvYGWlviMKeQIk4bD3iOpjFqFBCHBYEyZGWimLAMAQ9YS8BPpHInmy8TkCdlEvfIJORZALUCm2uKw5jpRJWp2JBOoVbO41nDJbKPLmZ0Vvt8aHWTf2Pl65wLR6RKEaHQSmGs5Zs7V6hta+KuJRwfL/Q56ws3H+y/ELk0dBxLcNbWjKYhec31i2DmWKjxwC382dD5IkP5OaGwnr1Y2BIctCWDWBavMkWRymr38XPNzbtuHrPiNVs4u4aka0kPC/KasyOZNQOK2k8ZiD+JWxUwkeIMjtlUVflmlUVVBULkA36V+QTHBXpVhihgUrXgkP9JQSAFRpVtI/vIJsc+DapJ4SGpytXzMsawv7/P2tqaT4M9f/689wFrNBr0+32fKiqFZZqmvvDe29vzkjhhQYErZhqNBltbW16uKGy5Kjum6kE4GAxI05Q33niD0Wh0AnDY2tryQSsCOJxOeZ2fn/ftJKCOtG2n0/EFmoSgCFBcZepUGTLPPPMMf//v/316vR5f+tKX+MQnPsHnPvc53nvvPc/2yLLMe1hVpcrVQlKAgldeeYXpdMrm5ib1ep3Hjx8zGAx4+vQpnU7HM5FqtRoXL170AJvcy42NDQ8QipQyDENWVlY8CDkajdjb22Nra8szHKvnIuCh+H+Nx2M2NjY8e6kamlFlyFZl+FWgusoOrALZVSZtFTT4ceOt+vNpcLwKNAhgUh2LAsQLg03CcU4fR6TzUozLmKkmHFevtXquAqRUJboyLmW8yjnu7e2dANSfPn3K48eP/bUAnsH7kY98BMADGe+9954H0KvhHhJIIV6ZQRDw3HPPsbGxwWuvvXZCbinjW8aG7C9SXZHbV+9Jdb4S8EdA9ypoWG2Tqldite3EUkCAH5nvqqFGInGu9pnT91ruD8DXvvY1/sJf+Av85b/8l1FKce3aNUajEf/Zf/af8c1vfpPV1VX+yl/5K7z00kseDJf7JgDP1tYWDx488FYB29vbvj0fP37s31fmOmE1G+M8RyXdWBJ4FxYWvDer1i6M5eDggLfeessDxUmSsLOzw/7+vmcYnj171re3yLTlvaqAurA8Jf1YfO+qfnk3btygXq8TxzEXLlygKAovoa0yiKvjrAq67+/v841vfIO1tTV6vR5Pnz49YalxOrSo2j+qUmI49t4VkFt8aUXaK3OjUsoDvCKFFxm9yM9FXl2r1VhaWuL69evcvXuXMAy5cOEC8/PzaK09YLyzs8PBwYH3gRQmsvQD8cJ97733+K3f+i263S4bGxt89rOf9fYBwpIUD9N+v0+9XmdjY8MHawnoPRqNPHAu87XcB1mEksXF6vwofxdwtsrClHmt2v+r82J1QVHGXK/X8/Jukdiffu3/p+2PP4AIjvkTKYKJwdY1JlTeIN+E+E/5ksasrCWY4D/sB1PrmSFiqF9iMA4MtMfswCIuU5gtBNkx0OG82Fw4SzQy6FLybCIHUGjssWdb5PYtEgBdejdaD34qC5TSOAAyPKiSNyzRYUjrwpRZc44itUznXSE0WrXEPcV0HsCS16G+pRitWkxs0TNIusolNZeG8XFXMVq3JQirSQ5h2oGsZQkmivpTF5hilbve5NAyXtKEE0hrM9Sw5qTigQsGUIWTf2WtoAyZkWLOeUZKUI0ylje76xQ1SnaI8verSCGva/JGhJmLGa1EBFnownEyd8/kvssWjAumGzPmdI3tYsgkj2jrlE5tQhbMEWDYy5qog5j95TqzPCCYKIKJxXQc02ZmAgeMKCe7nV0fw9s17F2XfByOXbvtv5A6puWSoWgVmCjgz3/6u/zjNz4IDxMvD6/9iX2GT+Zof7/GZCmluDJm571FvvE7Kyx2jesXoxl5O0WPMzDljVdOditSS8IAs3NIcXUVq2DWUC74oJTwUcqLlSqBAmEelTJllEUHBUUeoLV1MmOh2JQdzswCKIFeW2gfjCL+h1bwQi0TUAkGljOINaoEMCgl1bhzKzQ2sKjAOiDTKCdjBCicRFECSKwuWb7l7yZSnmHq+gweKPB/K9zfVJ54xqHOnM+cyozzt8sNKiuwMhFnjr3ILHNgZBA4cKrcbBg4ILcoIInpvjRPfdfw9FOKv3TxB/zf/+DnOP8oZ+fDEfERzJoh0cjSeJo50DFUTOc0RZnsbmJQzw34lbVXGZqEhp6R6hm9rMZW3vGMvpWwT11PiSm8/+HQRmADGirzIOJ+0XD91QZ0Td3tZ2JSnXEu2icj8KBOO3AsgomNCMrkZK0sdT3FWOdruJXPsRAMCJRhOewRFUXJerRMSqBwYkNiVdDWE+/VqJWhXQZqpGT0yxsZKfiPX/gNsudd51gO+nyx9wFe+cOXnQy9UBz+4oh//sn/lL4Nyaz2yc4T44DQ96arfPvgMlfrXTKr+fLbz1FvTVlt97n3uI2ODMVY0boZs/BuRpG4eb+I3QLQeEkxm7PkiznNpSHr7R7X2rssx31awcRLt43VtIIJLz9/l+8XVyA2RPWMKM5pRTlxWNCIZ4TKYFAkQY5Wlmke0pslHA1rTEYx9COiA01yqEi6ls6hYbWbofIpNlTkaUDW1EQDl/Q8Xoy42D7yEmBdIkkaQ0bArGzLwrqQm4mNSPWM5bDHJAmJ45wsC7FWObYfxo1t61iEzVrBNA8IS9afLtOb3ZMBJpOIfBa6lONSYqxzxXzqwnTqakqqMyYmAgVbeYe3J+tcSnZZDAbcna3w3nSVw6zOncEyu+MGR6Ma00lEeqNOfQr9y4bGI03SdWO2f9EyWS9Yu7DPmUaPa61dPt+6ySfTPk1drvyaGa/PAiZWMTSKB/k892dL9E3Ko515atoxCvOaYxJO57QPzwFKKbvCKovOFMmRJekbkt7xM6OI3GJf1nBMRgzkzah8lqsy6Ma1Vd/EzIpyQtIyP1g3t2knCw980Wu9fyJBuU+ZOD+Z1+SliiHIrAMwU4Vp8NPtJ3QT4EbCKOCkL1jVH0p+lw/6wlyUgkA8vUSCWGUdSMFdfd+qNLBaWJwusKshLALoCDgg5yfHl4JVCi8Bug4PD2k2mz6BVGvN4eGhB6MGg4EvFMVH7MmTJ8zPz3PmzBmGwyGTycSzfDqdDvfv3z/BkJJCGPBMLKWUlwhL4qiEhGRZ5qXAxhifrtztdhmNRiilvERO2qzf77O6uurBFGHbTCYTz+QTFuVkMvEAovgiVhk0UgBLoMra2hrWWh4+fOhZhZPJhEaj4f22hEkkgQvCQBRwpWqqLwCpgHTiuXXmzBkWFhZotVqcP3+eer3OV7/6Vc+Ostby7rvvcv/+fTqdDrVajf39ffb29vje977n27fX650ocmUT0EgKeOmPzWbT+5pVQVkBBQSEqIYYVKXE0veroHjVp1D2qZ6DbFVQquqTV2UinWb0WOu8DcMwPBG4IucqAIi8X/X/1ft82hag+nMVcBTpbjWx9ccxsqqS6SqAWWU9Skpvs9nkE5/4BD/84Q/5+Mc/zj/9p/+UwWDgWYfSX2VfAeiNMZw7d44rV67w/e9/n9u3b3sw7nT7CagooGGViVY9pyqwXGVNyT2Q/ivMSGlfOb58l+MIqHvaXmE8Hp9YkKnOZ1VJdTVIpSgKXn/9dSaTiQeG3njjDX7pl36Jq1ev8r3vfY+LFy/y/ve/n9/93d/l6dOnvp/W63Uv897Y2OBDH/qQn4+uXbvGzs4OjUaDz3zmMzQaDay1bG9v86UvfYnNzU3CMGR+fp6lpSUWFhY8k3g6nXJ0dMTdu3fZ3t7m4ODAh55Mp1OWlpb47Gc/68d4vV73rGu5x1UWcL1eZ3FxkYWFBZaWlryvnswNBwcHHB0dnWCDd7tdfz7iqbe3t+f7jzybpO9XmXDST7IsY2try7PCe73eif4rzx/pW1VGcvVL5ozT47D6bFxaWvKszU6nw/LyMu12m16v55nTc3NzfmFDnldpmnLmzBnyPKfb7TKZTLh79y7r6+tcvnyZy5cv0263vbz76dOnvP322wyHQ7a2tnj8+DFFUfg2FeC31Wpx584dH741GAz8tQm7Ufqs9F/p/9Y6+wBrncemLHY1m00PWov0W6wIqm1fBQ9Pj8WqJUr1NdX5ZDKZ0G63SZLEezYKmPo/LgaiLSWD1knThOGHUh4EVMZ67zerJAXZMRfyVHkAw0mhXfCBMA7y1Mmjguykr2E4dawXkUw7hqGTRNnS7zCra7+vCRWUQQrKOFDB6hIwiwGczDpruL+HYyhCJ7EWf8bRUuDCTbqKZjxlv64Ix4q8bqlvKfqXLPEmDC8Y6o8Chhdz2ncDlFEEE5iuZdR2IrIVS21bMVo3NB8phqEl6mumi4b6lvPpC4eK6XJB/akma1mivmKyaIm7jnVnQ0WgLMHUAYYi/9K5w4iGq84rKuorTAJ5agnLazShorY15fHRHJMV56+Y1zTBxJTyPicxd/6G7r4UkWMQqbKddY6XMercMdI+9fx7AExswKQICZRm67DFfKRIdcaZ+AiTOObQ9n6HzpaTrhaJIlUZgyzBWE1hLRvn9zn45tpx2msGeR36ZwMPYIXnhvzVF7/K3777KX5h7nV+v/MseZqiM5gl8PLKI5pn3uPxCx0+PPeAX2y+CcC/fvPXKH5jiaTvwiVsqF2QirWgNIQaKx9SJjNsoNEL8xRJ4BiwDeWK2LL/6zLlFBwLxpbhKC4oxSUlK+VkzEpbjAmcjLhQx2CidmnNOnaMRGudRNkaxzzUkXHHNar82Q0mCWBRugQI4difDFBxgS20ex/5n3zeCyzjZUVyVLJSNd6vTrzI3EGOWbuWU5NbCMoqbHo8Nq3Sx3LWso9IQJFjOJZeeYVxjEYcqOiDNmy5T1YwW2mQp4pZU/H5T9/gD/efYelVTfeq8+oUzzM3ph0DerwQkDVcuIeJYLpc8K9cfpvCaiJV0C3qFEWTnazNpWSXtfCImMIzDGcELhgFS6OUKA9t5ANVDJrH+TyZDVkM3GpVJxjSUI4xMbQxLT0htdlxijMuaTxVBQdFnZ3CpcqmKuNavEWqciJl6JvyA54NPKiXVdJ1hf2olcUY7dmNW3nHBRMBDSy7eduBoapgt2jx9771J7hcKwjGOShFPgt4VDR9SvTQxgTW+iCWjfCQP9V6w7EcsfzZhdf4B7sfY1JEfOx99/nznR8A8M6fOMNf+9qfI97V5HWLXp2wutDj/Z0dkiCnE45oBk6GO7WOISiSa60MGY5Z93LnPo8vzWGsIgmKkmFYYK0it5q9UZ1ev07ei4n3ApIDx/Be6hUEEwMU2MBQpJqspph0NMPVhCItk49T54XXuq85eL9h5fIOH5l/wMRGrIWOhSiy84iivG5DvQwn6Rc1WsHYSZ3LURBFOUlYMMsDVFCydgLHJJ7lAUWhyfOAPNfkk8iNVaN8eBShY5erxLiU+GmdaRFye7rGm8Oz3B0scjCus99tko9Dot2IbDEnbk+Z9RLUqPR78XOx84xsPDa0HkwoanWKxD3jelctf+vP/udcjnqcD5sU1tAzEx4VmtdmLrynoWZ8MFG8MzvD7+y9xOPBHPu9BkUekE9C9FGICaB/NkTlTu4cjo9ZiM5WInQLFhaK2JI1NMFEl8/V8lmdu9eFY3fOuoDhWkh6YABNER+vryzoCVkReFWBzCkYhco0QWDQQN+U7E/ZCnXsq6ghObLU9nOXjJ0osnJx4afbT95WNT+H42JbCiQpqqveUlUAr8oqqLIIpRiGk75uwqYSxoK8lxTfVfah+CSdloHJcaqgD+DZIMI+rO4j7C1h2UgisZjVt9tthsMhnU7Hh0XMzc15do3IX+fm5uj3+xweHrK6uspwOPTyYgG9+v0+vV6Per1OrVbj8ePHXj54cHBAs9lkf3/fg5hVoEpYjYPBgHv37nlvwDAMvXxXa83R0RHvf//7veeZpPlK2IMUssJ2kTaZzWYnJN7Szp/4xCfo9Xrs7Oxw/vx5wjCk1WpRFAX9ft8zMkXydu7cOQ4ODhiPxz6oQQroBw8e8JGPfIQ0Tdnc3GR7exs49uErioIPfOADfOITn+Dg4ID79+/z+PFjPvzhD9Nut32bi0S73W7TbrdptVrs7e0xGAy8H5iwIquM1CogIIC2yN+stSRJwmAwONEGVfCoKoWUviX7ypipMvuq/fG0/BmOC2LZqu8hY64KvlXHSXUf6SdSdAuD9cyZM17mLrJEAWmrr5f3rr6XgAjVdqsCZdWfq/2lKh88fe4C4CVJwtLSEg8ePODP/Jk/w87ODufOneONN97g0aNHHpiS95EFgHq97gNEGo0Gly9fZnNzk83NTX/tIvOU38VzrgrUVc9NgD0BiKTPyILJaXAD8GBtdQ4Umbh4C1YZrNX+UB0PwuKUvlN9H1kAqc7HRVFw69Ytbt26hbWWD37wg3zmM5/hvffe82M9z3O+/e1vc+PGDX9+cp21Wu1EqEYYhiwuLjIYDMiyjGvXrnmfws985jN88pOf9D6aQeBCXe7du8fW1pZnuI7HY7+YcRowHwwG3Lx5E6UUOzs7zGYzkiRhbm7OeyMuLS3RbDb94oksrGxtbfGDH/yA2WxGo9HwzwUB6kTG3Gg0mM1mnD17lg996EN0u11u3Ljh26/6HDo9pqr9QFiXYnlQBRfF/kH8UquS7urzURZtxF9X+oLs98EPftCDhwIMDgYDdnZ2mJ+f5+rVq0ynU/b39zk8PKTb7XoG+cLCAru7u54J/+DBA8Iw5Bd+4RfY2triS1/6EnAsJZa2XllZ8e+/trbG+vq6l70fHh6ytbVFt9v1FgXNZtMvTIhPZavV8sEt1ee17FOr1U68TsbJcDj0XrlJkvjFpKqXscxd1cVB8T6VeyiLcjIOxC5Exrm1Trovc23Vh/KPsv3xBxCVCySJMCeYSUVcyolDhQ0cq0A8sFRmCYdFyXhSJQjoJMTB1KUyKmEiSiJzCRYqY9G5A5QEDLSBwqjSZzBW2AKfzuxrilJmK6CDMY6N4FiI7v+OsejeI2vIe8n/lAMiI4iGino4o5tZlm84WVTSzWg/1ITDgqQbEfczmpsBaRm+goXROKL1uKBIAmq7FhNpagc50ychwdSBpcmRJRpq4i7M5hVx3xJMNMEEzJL1nlPjcxmql9LJLHlcMsU8gOPafryk0JkDLyfnM4phQGNTk6eK8VpCEo2YzGUOBB4bMC5QxrVtOWkF7rpHbUXWgvioDMspC0ZlXPE4WYr5P2x8EWjy1vQMaSCrh+6+pirjQ7X7vPDcI9IgY/NolXAM4bjAhBENNePfv/DPWAsGfHva5GicEg0daOjCURxQmtcgm3My23/2sb/J2bDGP659iPuzZS50Dtne7RCNHLvw3f/9i0w6Af3zmje4zn89+AJ5yXqJUkv8uHAy5cI4zz5rXeqyLllyEvYwzbD11AXJWNcHjlMCLEWuy2RlxwAsSsAuCJ3EEgVhWDCdRqXPmXVkR+3kxaYEIQV0tLpkJIIDG4Lje4tIokukzxbHoKHSFjsLXPFcnptLKnKMTopjRiqFQsWGwfUZJoyJhqBnrvAfnVG07lmisfVWAJJwfWLoFyXo6E9NeWnziVAk6zqCyN/lmAIo+uAjkceXfQrg6FJE0rWM/9Uuic659YeXmZ8a0sxij5z3HpT9F8eczRrH4ICJ4NL1p6xEfe5Nl1kIh1xOXAriWtilE4w8W1BYaA01Y2JDMhzzUOTCO0WLQBmGJmEjPCz9CAuf2gywVcyVxwsd0Ac+7GS/DFRJVcZaeERDzYiUYVKCheBks8thj5FxE9PERKUXXymrtdp7JzowMWdShqtMTES99Myr6+lxcq/KUJliuhCSHEDYm2CngQcmCxTdwrFBNsJDDoqmS3m2Y4pyAtYY/q3Vr/PNwbP8V+9+lI0XDnkpfcSHkkf86svf5+bRGRaSIYGytELnBZnonETnBOWN1dayEvWOA2isA9ICjAdaJ7OI/VGT7Cgh3guobynqO4b2Uc78zAA52Jy8HjiwsKEZrjq2dV6Dom4pEotJ3ThCA7EhSApModhfCPmLn/wm9WDKctgv5ekRdTUt7792DFI989cOeEZqUErc86IMLSp9S7NZ6KTM03KBo1wgKBuvBAvd+NOtnDjJ/QLDdBQxPUwJZor7b67zH7+5jp66trHKMe5U4NKkg15A0W2gA1tKdZ1UWOwkTGxAaR9YQ+npW7tyRKRyfnvwHFMTUaCYmog7o2Wejto8Ouww3xjz2y/8v6jrKd9/6zLxdoiNIJ8rnCVC6OxEWpt56ROsyFNF1tDeJiOYHjMQg5kiHFnCiSEeHD8zisQ98904de0SjZxfp57Z8vOAIqdgZEOmWUgVG1SFcozqyJbsz8r/ZH0ksKTtKUpZJo+bzNqKPA0dy7GAYMKxp/JPt5+orcreEUBQPsSfBhOlcK1+oBfZs3yoF4CqygAR9okAC1IkngYpBVySgqOamlsFQIB/4b2qCZWySQEi7ym+fNZazp49i1KKN954A2stu7u7pGnq02Hb7bb3mWo2mz5UZTqd+uN2Oh329/e5cuWKL57OnTvngbyFhQWOjo64evWqB2LOnj1LHMcMh0OWl5fZ29vz1yVgg4AVcp2tVsuf+9HREcPh0INseZ57+aCAKsLUkfslycfdbhfAM0cEUMuyjI997GPcuXPHsyWrPorj8Zg4jrlz5473d9Ras7u76+XaCwsLPH36lDNnzvDCCy94T7ZqnyqKggsXLrC4uMhzzz1HURTcuXPHMx9ffvllOp0Ob731Fn/wB39Av99HKeUL35/5mZ9hYWGBO3fuMJlMmJubY39//4S/n/STKvgm4IAU5VUAqerLWQWvq/2uyk6SfWQ7zZatSiZlDImcsdq/q8Cl9PPTbL4qGFIFJGXc7u/v8/DhQ1566SVfUAto1ev12NraIo5jxuOxD575cQytKhgkzLjT4Mtpj8fTTDz5XfaR/vr06VM+8IEPsLa2xiuvvMIHPvABvvKVr1AUBVEUeam9+KqJnFZsBc6dO0cURXznO99hOBx69piAWMJ+qrZ/9dwF5KmGW2itPZheDbsRcFR+lnstoJK8XuZLAVIEQKyCyHKvq2O52jfknH5cW8p9qEqM+/0+29vbPpCpulXnRwFTpa8LoP3w4UNWVlZYX1/n0aNH3t/0N3/zN7l48SKbm5uexStM3uocXR1T1XAVabvhcEi9XqfVanH58mWuXr3KwsKCnz/6/T6PHj1iZ2eHnZ0dz5ZsNBrMzc35cSr+g3meMxwOPcNW2jmOY27cuMH3vvc936erYPxpMPv0mJQFBIDFxUXfZ+X+CUtZ5NbCqpS5NQxDH0KT57lnbot37auvvup9bmWRSLxp5VkpzzaZF6pjTJj44gE5mUxYW1sjiiK+//3vs7e3x6VLl7h48SJKqROLBHNzc2xvb7OxsUFRFHz5y1+m3+/7a6vVamxsbDAcDv3Cjzw7xJtTrqsquxfQULwWkyTxzxhZVATnxSjSfbkeWdCr+ubK/+RLxpTW2vtrnjlzxgcw9Xo9Njc3fXp41f/xX2b74w8glkwlEzqQUFXCNYq49OuzOKlqrMjqbvXf6pC4X6ALSxbpSshHySQsgzEEDHMMpnIglWwESXZ23oTlzyUoAW53J60qGR+FOyc5Z0kpFZ/DcGor/3eVSDR27EUHTjpUU+TZ4yVN0jV0r2rqWxHB1DLpOI+3cFwQpZq8rv1xOrcNRayo7zjWxvxt94BrPygQn8Ngaph7z4Gd9R1F3C8I3nbFV/OJIu7nJD3N5ooi2kwIZpCXShBdsrnyhqK+bcgaiv7VAhtYdDckPtKeVRgOnf9WGOWYJCLqTtHTnPgoJD6KULklOhg5ALEfYYI6WTMgmFqG686frvOe87/TmaV/NuRS5CQgd6crLCYuXWg2iJ1/nXYpy+8+WYVHNaLMgYHuxCFVOevhmM/9o7/K8g+As5o8dQCvKiBru/Tk+pUjfvnCTf6bVz7BpcgxaVrRhL5J+cj8A/7hxSuEI43OoHdN8dwHHvAfnv0DAiz/69d+Ff2jFmHm+mfYz1xqsLGOfWgsShkHIhqLynJsoFFaYZMQE5fhOhGOSRQ5RqG1iiAwBIGhyCOSNCPPNUpRsgY1WRY4/0Jwxb+Ae1Z59pI1kOeB667aYmfaFe55mcqsyg5een3p2Mkm80HkZMmFckBFcCw9tpLCLINDftfuGnRcMH5uwqiUS9txiB5rBmddSsF00ZDuaRZv5uiZWxCoJuwizEQJbdHHH3Sq6bsiLfQsRcFAyxR1pdQxsKgUKnbjKxpY9j6g+IuXfsh/8d2fYfmeS1oOx8b3dwcKOLaTMJptKeufzRd8YH6TVGesRkdsRIcAPM7m2YgOffJuowyl0MoQlRLmkUl4WDSJVYHGsBgM6JsaK0GfrqlTlEnLHT0iUIYZAXXlgLvdou2Bya1sjk4wYiEYsBgN/LG7tsZyMPTMRgEcxTfRMfUUBZpI5WQ2pBOMXICKSX2QhjAT0yCjpcceDBUvv4aeYlOD1WURXXpNSqpvrAov4xbQUeM8FSc2cgExwZh+XuP52mM+f/EWr/Uu8O3uFT7UfsiDkUtVz01AUvqY1oKMVDv/SHAAXKF0CcYaYuWYzxMTsRAOOMibvH/xCe8erTAcJax8K6B9b4KJNVkrYLIQkjUd6JTXwCSOUWgiKOoGGxsIXcBQGOekpacouMUirV3K8LRQLIRD5x9Ysi9nNkBz7FWpyxTsCRGm1M2KjLmwmkRnjI5qqH7oxqeMpzLQg8AS1B1AKCnHRaH9woKZBsx2E6K+IuorqMN0sXDegeWgMQkuzVlA9UxB+dyzkXHjRlEGmCjnK9hTjFcr4H1layQzz7b8q9/8VdIHsUu3rhmCkcYG0L9mKCSsKSlc6nyAYy2HFmZuYbC/ETrJdWmJEY1cMInOLIfPhn4cF4n10vZp+zgJXRWOvRhMj/1Wo6FhtBy657gEpKApMMyysAxgwzPgsY5Z36mPCVBEyvKzq2/z6s+MuFzf42da7/J8vM+Xh1f5jzb/DEnXEA8MRazdZ4dIYfW/0Ew/3X4Ctqrc7zTDT8ACAaeUUj65Vj7AV4ESYd1IUVc1l59Opyfe9zSzscoAqxbe1SCEqjeVADNScAmrTlg3ciwJJhEgQYrDRqNBEASMx2MPKu7t7WGMYXd31wNoAiQIC0+AyzRNvdeb+MWJrFek0yJBE+8xOWdhIsprqxJZ8ZbK89wXe1WDf2F3CGDZ6/VotVpMp1OfMtxsNj1AI8dcWlqi3+97P7k0TXn77bd9kvHq6ipf//rXPWNP2JzChhTmULvd5uHDh9y7d8+DPeACWi5fvsznP/957ty5w2/+5m+ys+MWH+v1Ouvr6zz33HNcuXKFNE25ceOGBzXPnTsH4L3jhD0lQKMwNnd2dvi93/s9tra2fCCNyL0FVDkNzkk/ltTlNE3/BearFO5VUO00CFgF0OReCchT3aTPViWQ0odPy/cE9BZgSsAnGYNVr9Efx1CcTCY+ZKfVarG4uOjZXXmes7CwQL1ep9FocOfOHZ9iWmWQyflU2bryu4xp+fuPkwv+uL9p7UI7xBrgE5/4BG+++SYf+9jH+P3f/3329/dPsJPlugTIELBBPNl2dnY8eCj7yj4yHuRchDkm13iaDS3XJPdUWL/SX2Q/ASmq81A1XKIKilcBxCrgVmWynm6r0+0moNzpPlJlX1fbStquCkpXQTJpg+r9lXTyubk5ZrOZn6/ee+89njx5cgKMqwLY1fM9fS3SBtPplE6nw9LSkrcjeO211zg4OGA6nfr5Y25ujvn5eQ8Cy2KLMBwFwBL/SvlZKeVZ4AKOV89NzuM0q7jKUJPnkVg27O/vn7hP8iystqn4x8omi0tJkjAcDj1Db3t7m8lkwrvvvusBdLECmJub88nHURR5f88kSbynYJIkTKdTnj59emJBDRyzPssyDg4OfNjVbDZjd3eXmzdven/aX/zFXzwBmMszBvDPLGstzWbT+xz2+30Gg4H38JVzrraLgNEC9k4mE+/tKfdFmPnVzxIyjuRey2LTxYsXOXPmjL/+Xq/H0dERBwcHfPzjH2dra8vL6UXmvbW1RbPZpNPp+H4n3q9/1O2PP4DIMZvIKrCRY6gdAwWKYGoIRwVZWOoNgWlbYYKAIMODMjrHsypUURYLxjGbTKWlrHJsPQnwUMZ6AFPnbv8iLo9ZevaZACgBIE8ey4DsmIVoAseEcMzIspAqF3+zmnbJycoVPdujFlnLSWqzlmWEorbrwlSSQxgvx4xWNLO2Y2VMC8eynCy6wksZZ2iftSByKkh05sz93f+FIeHA1WjorvvoknbSr9qUqB/SvW6JD8s068Jdi3G2bU4KvTihyDXNWzUH0tqyeAoVZ+eO6KUpRbpMkWh0FqNnBdP5iGlLMwdEByPC3R7FMw1fvGVNS7aY075fpqQOcw5fODZjvT1a4XztgMIa1CCkSBV1NSWzAXk/Ijw3ZjYJaT2IUIUL8vi33/pfMP7DZa58rY8NNVmjzuCcYjZfEIw1xeqM/83HfpdfaNyipTT/MPqYfz9hOz6TPkUVkO47U/+lVxVP3rnIX137N5hemmAnAWqtoPEwIJhCMJy6xOlJDsZioxA1y45Fp0p5H0STRp5JN12w3kfQZhqdFJ6JpMu0Za1duImxymGT4uNlHVCotHUSZet+VpLCPNXHgJ8FJe8jrCb5YFHKol1og3Wdugw+ttb9X2TL4qOIVljxwIycT6PCvT/jgPRJTOuRpbabU9QsOx/WmLqhualId6aecWqVckChTHRKYSLtmMY+JKlkUJXJ64rSo1QYxXKJwanv2i04zJq6XABQfODTt3hnuMr8a6Gfa4KsTGBONEWimMXl/hGeuVWkMHfhiOfrTxiZhAJNt6gzNAmtYOwZh85XMKdrQw6KJrfzJovhgOWgx0rQJ1U5W0WbAOddODEOSJrYiBXdp0B7BmJG4Bh8JmItch5769FhCfyZknl3LEcemcgHoWQ2dAzCU8DGxEQO0LIR2ho6ekJHj0tPxox90/CsSAENARcKIoaWgfVpyNKHUpW59F6raWsnfyhQDqjUo2NpdCnrjfSYvqnRDsf8W0tf56vD53h7eIZJHhHr47TjzAashUcMCvfheCl0baSV8YAiOFBxpkIPIn60dZcPNB/CBvyfJn+KaadG1nSJ5459bLFJgUoLVGBc+nJSTc9UHszXyiUiK2XJi4AwKNy4jI49CQs0MxtgbATaJRkHGNp6wn5pjhdROE9Ek7j/l/cPVYZ3tTPSekYc5SRRzjQLmWUheRaQDWLyYUgwVgQzSCbumRVMAO3m59GGwbRy1DjwYKGeOWa+HiqCkSIawqwDWdMQH2n0DMJRKQOeWCcjnlpGywHDC87OQ5lyPrDuORJow6J24HP6IGbujmGwoRmvuXFqEkM9mRGgfF8QT2CMWyjAumdU61FOkerSAsSxfbO69s8XUQUEE+VZh+mRcQtv5eJdESvGixqTWJRRxIczBmdCsmb5HItdX2qpjKJQRIUbz+HGiDPzPf6nZ1/lSrzDubBLUydENuOXW6/zyfpt/tHhR/nfvfuvsLfbhqEDeWdNzXQuZLrgjl3UXPhWOOKn20/YdrporTJkhB1VZRrI33u9HrVazRe4VYnSaV8jYbIIQCIFcZXxVWXsCKOhWqTDsXF9lZ1XFIX3hiuK4kSS62lGmrAqer0eWZaxt7fHaDSi2+2ytLTkZa3i2dVsNr3XlISobG1t0W63ieOY2WxGu91mMBj4gvHg4IAwDKnX63S7XRqNBlEUeRaLgDviT1ir1VhbW+PJkyfMZjMPZoq34NHRkZeF3bt3z5v0x3FMs9mk3+97hqGwpIQxpZRif3+fNE19qIRIAqMoot1uc3BwwLVr15ifn+fdd9/1BaIAiMLWE0al+Gw1m00uX75Mt9vl1Vdf5f3vfz/NZpO/83f+Dru7uzx9+tT7ms3NzfHJT36Ser3Oq6++yvXr13nw4AGXL1/2wF+j0SAMQxYWFoBjv7I4jrl69Sqrq6scHBwQRRHLy8seTJWQm2rKZxVQEvBQQOyVlRWyLPP3WvaTPlIF0U4DkXJ82apgXpW9dppZVAUrTzML5V5XwR7Z7zSrqgqOyv1N05T5+XnOnTvnE5EPDg5otVq+oJ+fn/feclXvvmqwjpyP/L96zlUQsfpz9byrDEkBuowxfO5zn2Nra8sH39y8edMzk6RNZSFA2IQiX7xw4QJzc3PcvHnT90E5T7lX0jZyLbKdBhJPz0lVfzvp53IewoKrsgSrEukqwHua/VYF8uT30+Dr6f2rMlmZR6uvFWZp1a+zerwqwFkFRuUY1X4n7ERrLYuLiwB+EUCOLfPU6WPLPlUPQLmWg4MDD/IGQcDnPvc5zp8/TxAEDAYDvwgj7Vhlukmy7um+Jtcizx35WZjTp8dI9VlRfY5Jm8s8We2jcjwBVuv1uu8HwuYWJmC322U8HvvwktXVVcIw5K233vJWFmJ3IfOz+PaJtPvMmTN0Oh1/XwTE293dZW1tzZ97tQ9Vf5c2/cpXvkKaprz33ns+2Ko6p0hbnLZKCAKXsC5AYtVLWCwRGo2Gf1aK3YcEIWmtPZAoz39ZpFhcXOTOnTt+nE6nU8/4/cxnPsPc3JxnGYtM/+bNm2xubtJoNCgKF9Jy9uxZbyNy//59nj59ytzcnF8wEnZodQHmj7L9hACIxw+gYEaJYLiEY+eRprEdNzno/NhwXUz3JThFlUnJIp+FYxahLVlVpgx60Md1qGMNcuyBWMTKeyLq0nstmLlj6YwTcmirFUnXgYYSxGIVELhjFJE7tyBzhZnVruDpjVOypgUUUQ9mc5b6U8eqq+24oI2ob+lfstS2NKM1S+ddVwRFA+d/2Lqjmc4bwpFmvGJp33VFZXKgGK8amg81w3VL0lUM1y3Nh4rZvJNwJ40ZKk8oaoY800QDl8bcv+DaKRoZ8qbCPK4RDTWzFiSHeNmWDeB8/ZCDsM4TvUzWcJ5y4Ugxa2imHcVsLkKZGvmZJv1zzow/T2HhLVcFhhNbtq/lMx99y9+P291lfnbe/R71Hfjb1hN6JgWjyIcRaMvhC5asWSM5sDT/r3MkzYLNn22hc0j2LdmcYeXqPtvbc/y7H/46//bcE6DJq9MZxygf1IKMqYn4ZPr4GIRWillL0f/UCHOYsPTVhO6zUJyfkPwoJRpb1DTH1GP0yPkfVoM8HOpnoDDYfp/8qvNjyGqKvF24rJXIAYnWKvIswNoQpWwZouJYT0WusLkmSDOsDVAKB2rkrtO7fcriQTwMZ9pdX+BARhU5QMTYwIGNgcFkAaZQnmUlwSm2UCVrsWTyWFzac1JgMrePKgFHNYyIdwMam5b2w4xw6B66RerkocFYUX8YAhYblw8t49pGks9Vef91ZsqwIVOG0ZSbeJ9qB34eh/VoBzqW7EUbHi8UWO18QFuPCvb+9SEfnnvIf/6Vz7Ny5MZ2MHMgfnSUMTubOhll07GwUE72njUss42Mf/fqdwBYjbqkKsPgfBADZUh15qXJExt5Nt9GdMhy0CdShsxqJjZ0wRY6o5enZATEygWdRCpnYiMeZ/MepOwEI84l+85b0CS0dVYChAExbmWgZ8U7oaRwgg8WCTCkqlyNxrG9JtYx5JwcOvCyaJe+G/v3Ck7pMjt6xNAk5dzm+oOa5ahM0zMphdUshz0OCrdy6xiUMT2TOpl1GRYTlODfxEYUaN6ZrXEh3uOT9dv81tEHeW+4TDuaoLE0g6mXdyf6eEx1ghEaQycYOeASRaOUXM9KWXOqMnqmxi+/70fcv7zI6/c3XAJzVFALC7S23mcwKwKioCAvAoxVhIGME0UYOFAfIIlyCqMcK1hb5oIhI5N4ufdCMHDBNOW5Tkq/S41hZBPHVNQz6npKt6gzMjFhUpDPAZOAySxgNgyY9py9hC4gMZCUi2F5wzJbNATzU+bnhry0+JQ/t/QD/mTaI1IB/91ghf/ov/wLRAOI+o4J7/yAjbOH6AT0L7vnTuu+pbaXYxL3fC0SxbStMbF7dumpY4eb8Lj4sRqCcp4ocAnyRSxhJXg/V2td4nJLj/38aiWMRFv0TGFi6J8LKZLSzzA79kJMu5Zu20m4nbct6EIRTizTlqZYwLP5ZYFMFaU6QLuwsKxpSQ8cKDmy7kPb/+0j/y31j05JVcZB0WQrn2NztsD/efNPsVLv819c/CI/nDb49978VYY359111i02NqWfq6L7vHFS8Mx5A9ugMkf9dPuJ2qTYFOmWFEtVwECKMmGCSCEKJyWPwn6w1p4IMZCCosrukmJGCjwBAqtFqYQTyHa6aKwCF1UZZtWPTIpicEwOpZQvQg4PDzk6OqLVavkUS0k2nkwmXv4JLjlWZM5KKf+6brdLt9vl7NmzdLtddnZ2WFtbo9vt+kJ9NBr5wvT+/fsn/LV6vZ5nZuR5zrlz5zzrRth/4q0o7EnxJxTGiHhTSYEuzBgJhgE8s+Xo6Ig333yTZrPJzs4OSileeOEFgiDg9ddf59y5c/T7fVqtFlprL0sUQHM8HjMYDDxYu729zRe/+EV+9KMfAbCyssJwOCQMQ37u537Op60mScI/+Sf/hJ2dHS5duuTZZgJqNhoNHz4j/aPdbnPlyhWGwyHf+MY3WF5e5vz587z11lueQSNFrhTLp/sZOMm2FOzz8/N0u13PmKoynqS/nGa9VvtUFdw5DRRVmXynWWjACWBRjlllvVX7qoBf8rO8vzCFlpeXuXTpkvcs293d5e2332Z/f59ms8nKyooPl6my2ASwrPqXSptVAfwqaCNfAsSeZs9VWW4C0gVBwLVr13j22Wd57bXXuHr1Kl/60pe8HF6YtiITlcUECW9ZXl7m2rVr3Lt3jwcPHpxg4Ek7yd+q6c0CGAuz7TSgLDLS6n2UeUEAHRmf0+n0BJu0CnxWwWO5z1XA77Rs+fScVWUdy7VXgZAq46/aP8WzUWSwpxdVftx9qbIb5fXWWm7fvu1lxNLPZZNrkNdVj19t/+qC0Gg0Yjwe8+abbzKbzVhZWWF3d5e9vT2yLPOgUHUBqTq/y/v9OID99DxX9XE9DZbJuQo4LozDJElYW1tjY2ODx48fk+c5q6urPplcFswkPOjg4MD344sXL/ogEVlU2t3dpdvtcuHCBfI859GjRyfGdLfb5ejoiLm5Ob8YFYYhN2/e9M+XOI79go8AuVX2b3Xcypc8u+bn5/1zr8pCrbJXpS0FbBOZsaRoy/gRq45q8Jm0n3jfig+lsBZlgajKQlRK+dCy8XjM5uYmV69e5cqVK+zt7XHjxg2Wlpa4cuUK169fZzQa8fTpUzqdDouLi9y6dYt79+5x586dE+D1wsKCB6OLojgR8PRH3X4iAESdQ9LNS9aBIW8ETOa190EsIsfu80EM+hjcM6pkBUpBUbIeTFiyCbWTFxdJKdssx6ZnG4GXPcu5KOtCP8JJSbGyeA/GcGK8mXs4tYjW06c+q2MGhQnsMaXLlj5wJZNw2EshtSy/ZgkyS1ZT1PYLdBFS386cp5uFrJGQdg3BVNN8mpHXIpIjiwk19T3DZEUTH1nyuiI5MkQDTdyzzOZdmuhwHcIhjFetZxlmLctsFJHMoPEgqNwHy6zjCj0sFKklHJQPGuvayyddF/CR1j2+17/MZux8rMKxJe5JuyhM7K7dho71qDM84CMedTqzTBcT/uziq/48epOEF+InQEQwVSSHlv/w3p9hlEWERwH56oxgL6b+VFHbMUznNJufjcgWCnRzTPJ2jfGK4t//+d/il5vv8u80foUL8Z4//lbRPk4SBpIgp1+kNJRmsugkrsEUsjqsLx7x8rMP+E37MunakHOdHkdqg2hoUEd9VD3xXocAhIH/bo1LYlYL8+Q1J033ydZW0eyMKArt5Ixxji0CdCAPX0shASeBcd0o16jIuPRk46TLtpQjeyNBDZ7OW8qcba6dNNlZtHlgxIGFlKCg0+Jboxy7Mdd4iiG48JXQYEYh4XZEY1PRepITdyfoWUGRhoxXEiYdzXjVJeiiLGFfkTXABKXcRbvi3ygHVOhZ4RltTmIZHDMTRUpZWA80qplFFQZ9Quqs/DhFwfBsjdq+Ze+lkF975nv8xoMPsvAjRZ4c2xtM5gOOLtVJDo99TYuk/J6CuTLmT117h04wcmxBE9EJR84r0Lqk4W5RJ7MhjTJsRBKOnX9gSGZd0Ec1CKVRhmpICMrIJC4wRWd0AlfYpCpzgS268OzGnk0cM1GP6egxxmofyDG0sQe0xB9Qtq6ps6iPA1q0MuX5pzgozxCpnLh8jciqA2XomxqTInLnVc6teSOAMy1qjwPuZ8s0SlBsZgMa2iX/ZjjPx25RL2+jKdOrpz5JWsA0YerFukBj6UQjjFVkNkBjWQiGZDY40TZB2aaZDT0L1NiIwio64didQy1gLhzzoDNPbrQDCkv6amE0YVAQlSBhoA2JTyt3vxdGU1hcAIc1zPKQrJui6jmL4QByaOiZvz4BXhtqRqEULSZ0TY3bk1UeT+fZmzbZHHTYPmrBmy1CoFgwdN5W5HXHMJy1YDZvsWfHXFrd53Jrj3PpIR+s3+djyT5LgWurqc3omxl7xpIqxR92rwPQ2ixcMnCgyFMwYeCYuRaCkcKklum8okjLoqNcJIvGluDIMl7QpS+wG2/yPEM5xmVSArQWYQbLACyHqz6WzPuFiXJMop3faTiE+u7xfOntSxRM5rR/lrtnvAM589SB/vHQeoaiCZ1FgQ3LILByiKmSNW1Di7GWQFn+ztaf4Pu3LqFGAVZbwv5xkNbopQhjHTM4Cgt07hbxTGogdInjOgvcc3ugCUfuXgVjOXd+uv0EblLACBAoUl1JU6wCgNVCswqewEn2lxTdpwtTKdSqzKcqiFFlW0ihXGWiVFkZIrUS1pIU41UGZfXcqqxKCdYQNtve3p5nCqZpyng89t5VS0tLJ/yv8jyn1WqdCDeZn5/n8PCQq1eveqBjdXXVg2eLi4uMx2MWFxd98T8cDjk8PPRFWKfT8fJjSbE+DQLIV6fT8cWxJAwLCAnHgKwAwoeHh77tdnZ2ePz4sWdZrqys8OTJE54+fcr169c9gw2g1+tx7949vvCFL/DWW2/R6/VYXFyk0Wiwvb1Nq9Vifn7ey4zfeecdlpeX+fznP0+SJHz/+98H4MGDB55RGASBT2oVlqncE2FqShEt9+n69es+ffT27dve/7AqD6/ed+lrIrcXVmmapgwGA+I49qENVS+8H9dXqwxFKc6roMzpPly9T1UW1GlgBPD3q1oIV8eUXE8cxywvL7OxscHGxga1Wo3d3V1u3brl9202m6yurmKt9WEz4hMoY+M0M6t6PtXxePq6qudVBRer4z4IAmq1mmfD/uIv/iKvv/466+vr/PCHP+Tp06f+OIuLiywtLfHkyRMP/kgY0fLyMs8++yyHh4e88847J8DaanCFMcYzqKqg72Aw8Od1mkko41/ut9zT6rwiFgTV5OUqKFVdSJHXVIEeue/itXcaqK0CI1XQu8oAPQ3cCkNTFgg2NzdZX18/IRGVBRcBuarMUjl32aT9hsMhSZL48xBQvsqMrIKjwAkAtgrkSvuORiNu3rzpU5R3d3dPJPNW2/s0GC/3VJiCVYBNQKrt7e0TfptyjjLW2+02nU6HTqdDq9WiXq9Tr9fR2iWwLy8v+3YbDAY+8Xp1dZVOp8Pq6irtdhtjXNr906dP+e53v+vnif39fXZ3d5nNZszPz3P27FkA9vb2ODo68mz4JEmo1+snFuLE6qLZbHrmpfh9ih/oaSD19M+n+5r8Xeal6j2vzithGHrbiyRJqNVqHmiXv0mflQVCOWfxxq3X654lLwsBVSYyHAeziVXIysqKBx9XV1fZ3d31wVovv/yy91M0xvCd73yH0WiEMcZLq0X+3ul0OHfuHFo738RHjx79kdmH8JMAIFrH5itSXUoWA+9vZiIweSk1NtaHnoQTS54cAycO8HH/90BhXoJeperZMxetAx6VAVt62dngGKBUFlRmffqzicrBmFnPXDSUQSy5Rc8seU0TTozzjlKc8FP0hY11vlvOv8lCN8Y2c6KhJq8p+uc1wcwx9KadkHBqqAa31HecSXx9xxBODbU91zEX3oJoWNDYUoSjgnASoGeW9gPQM0M8CAmmhtZjTXyUkR6FbH1cubAMS+lld3w7TM3S2NSufSKLzsr7onG+cKG7lqyuuR4/5Va4RhFroqEh7hv0tCAcByirmbY04Sgma2rEpF/uuWzB2NC9FrMWHgFinKx5Lta8k019kZv/x2tsfz4kHipqbyTY0AE923/CQJoRxIYASNIZelpjcDnn3+08ZmBCjFVci7cBNwE/mi2iSm+xQGniEj2OlGO0te8ZolE5sb+5zLfqq6wqgCZD3URFEA4LqNeYLddItg1qNKWkB0JWfgDKCyf7DbRn3GQtILIwUwyOak5KrGVl1mAKXTIQy6YqFDp04SoOSLSoAIy2ZVoz+DRlz8YBVAk6Yr3E2ZaYICVjEaPKdjDuHotPWqb92FKRcezEo4j6o4D2fUNtd4qeFpgkYLoQ0d9ImS6IVNS6IIayX6nc+UnaUBFMCygcqK6sQVtKtqEDKt24c+ndTpoufU+5pOvSa9LUKtOepDUHCpU7j0UTOMbvuZ97wONph9ErSzQL65nO0zmXCD5ZdgsLelaCBuVhJys5f/GF77EU9UlVRiccObmwTXg0XaRfsus6wYiVoM8MF2IivoMALT2jb2IiLIHKvdx4aBwQqJXzRNwID5382dRoqBn7xvkGLgYDMhswNAk9wKBZCAYuZRxFR4+ZoRnamCfZPBejPbQyPkyFUio7MTETNfNpzEEJaqbKsRollEUAxFSVD0kUu6btWX3imzmZD7ALAZ3bBf/4yYf43178HbqmjrHahYRg/HEBx2osWYNFeT5TEzpw0sZOdh3MOJMeEanCexwmpf9hpPKSqekYh4EyFFZjcKCuG7eF80i1zrdxYhzoeViauzo5siUM5IOEY97mhSYs2b7ggk1yo5nNEox4DuYaOyk/mM0Uqun6UDuY0NJjD7gKaPuN0TP84Ogi73WX2DtsUfQj9ESXQJxjAa6+a0j3MnY/lGBiSLqW4Fd2+ZvX/1vm9JTFwNJUEYkKMVhGdkbfWO5lA74xvkgnGLGVd8hKJul3nlzAahiuanTu7CiikfPJDaaG8UJI0TSoTFHbM6T7ZZJ2zXl+FpFiOqdKWw8H0ums8IsHqnCLLMJCVBw/12xo0TNHVY6Dgki5c5IdbYBPkRd273ROk9fwdgKOhWhJ+obxSvlcUooicUnLwcx6CxATqxLMdnNFMCxp0oE6Tn3HPa8KLH0T8f13L9F5LaZIYHjWPVeLmsVGhrl0glbKMWNNJVlZWTdPakswUSzctEQjQzA1pWcy9M6FmPYffbX3p9sfj02KNmHxSGFSBRikKJF9hf0hskEpqKXwFuDwNJByGhg5DfLBcYEkYKHsU2VqyX5i0F6V6Z0+nhxDgAABU4wxzM3N+f9JYTQcDpmbmzshEc3znIODA2q12glPQgEvADY3N32xIwxA8TKUwv7o6IiNjQ0v071586b3cWq326yurnq/MDn3KrtNCjL5LkWeHC9JEl9ENxoN/zdhkVT92oQFtbKywqVLl3jnnXc82DkajWg0GiwsLPDGG2/wzjvvEMcxb731Fjs7O2itGQ6HnD17lna7zZMnT/jhD3/IZDLh4sWL/2/2/jzIliy/78M+5+R617q39qq3v96mu9Ez0yB6MANwAGIjCIikORQIg/IEAZmmZP5hRjhsyyFbDlsOWXLYYcohRVCyZYYt0pJpkwCJbbAQADEDzT7TM9M9r/u9Xt7+6tVed98y8xz/cfJ3blb1UIb/nOFkxIt6VXVv3syzZf2+57tgjOHFF1/kH//jf8xwOPQ+kHme+3uTMIgq+2o8HrO2tobWmsPDQ77yla9weHjIaDRia2uLKIp48uQJDx8+5JlnnmFtbY2XXnqJw8NDvvjFL/q+uCinFcZNs9kkiiKm0ynGOA9BAS2lLy9Klasg4cXxd1HqLJ9dlf9Wz1GVTMsYv/gZ1d+LFP7q1atcvXrVp6o+efKEvb09tre3WV9fJ45jLzF8+PAh/X7fh/0I4C2ySgF/5B6r13TxnquMpup7BOCWQ14r3nDT6ZSf/dmf9fdQFAVf/vKXMcZ4RutkMuHu3bvnwIbJZMLq6irPPvssp6enfkwJqCTS2Sr7TsAyuS4BBOXapU9kvZD3C7hefX+VSSnzo7qOCKBfZd/JOiVrUTXBVoAOcKCKAONVCfV3ApUF5KyuZQKonZyckGUZn/vc5/iRH/kR/uRP/oT9/f0PgObVa6iOyWoyMOAlxDIXq5s4AqbK/V9s3+qYrQKkWZYxn88921HCOaqMc+l3AUeF9Vll/VY3T6q2CtWNEwEKZX2I45j19XWUUl46fO/ePYbDIf1+n6tXr3Ljxg2ePn3qGYLXrl3jU5/6FGdnZzx8+JA//MM/5P79+56NZ4xhMBgwn89ZXV3l5s2bvPDCC77/JGleUtCrDFhhBAsYLeC+sCKFtSdyXzkEDBSvVHmeVhmoVaC5+pyt9sfFZ6LMXQEt0zT17Hg5l4xh+VzZTJJwI5FqN5tNv9F48fks17C5uUkURfzqr/4q0+mUp0+ferCy1Wrx0Y9+1P+9ULVBEeAyiiJ2dnZoNpuMRiMeP37MfD73zzcB4/80x3c/gIiT+c5XAqxy0lnxXdILB9xJCrMqwT6rlPdPKlK8TxLgfQ3BAYC6UE72VOCBKxMowtz6ABSAoGT8OfDBAYXhzJJTAhilBFpSGA0uFMWWKbl5qp1HkwKrykRYhQM8QsdcLCJQuWK6qlCrU6K4YL5SZ7ammW0ZxuOArAFZU5GeOsP/2QYs2qoE77RjERpNNLQs2u7+o7EmmFqUDZitOilyNLQUqWLegXgYEI4t83bMvKNoPHvG6N6KD4rQUPrBKTZvHJN/YwMJghHGYjQo6eMRRGNLODfUdcZO3CdPFd23xxRpSLzXIzoKqT+MHKgWR0xf7S59I+eu+HOsT4teFPResFwPF0DEcTFm3KvxHx5/lP/nGx+jNYLm3oLRbkw0dn0gSZnTD80IQ4PShiBwjD2tS6ZlvWRUYRktEl6Ilijpcd4iqS0Xpm44YZCnaDR5p2C+EjBbDYiHlsENzaU/94jLjR4vNff4L//hz9J6ZAhHC4pug3uf0qx9rcPWv3iKDUv2XOUhYvsD2N1C5O2LtnWAbbBkt6IsQeACWIxRFLljIgaBpdDuhq3RqAp70A1w69/vGIPKpS5HxrETC+0ng60mKJdjU3wOrVEO+wytkxtasPMAPQqJ+prWA0vjqWMbmjRgthYxXU2Y7CiylsEkRTlWymsz7rNs5O4xayqGlyI67xUVVqGbG1ZCaOAceCggvM6MT1mWUBVrrJc5+0yYKGCynTBf0XTenfH0R2v81Np9/sEXf5SdewYTuvafr7g5YzWkx24dKdIS6NAuuOHFFx+zEk4w1nnc7ecrDIsarWBKJ5hwPT4qPQgdgNdQC9a0C8nIbECqMnom9UCKeCeCA+g6wYSWnjI2CYXSDE1KowT5XCCKe8/EJGgcu62hHNtQaxesMgN/fanOGNuYFPc7lwLtJNVVmfRR3nZBLHrGmNj7Hbq06DlDU3MgXRmG0gkccDo0KZ/8gTvc/vzLbq3VjhH29Lev8v7f2mQtHLlrMIm7HuXCWDIbMsYFy4xNQoHiwWKdo0WTwira2knhHFDowENJXZZ2AseaTPXCncNqv9ZLe8nrhY2olQOJJmWyVhgUFEZhrWPG5UaT5wF5FlDkLpjEFtoB8MKYs26OqsCimxlpbcF0mKK15a3ZJe5NNzicNTmctDgZ1Zkc14lOQvLdBcFh7JLQI8e49WEmyo172aiiZHVPthR/69rXiFRBrAwnheYL2SqF1dyZ7bA379AOp/zP1r/EWjjif/y1X6TYrzv1euTmQaKhceBCtkwIWV2xaLrNC6shHGryumW6pslqkbs2Wz5jCwdiqiY+qGi2WSMcu7mhSpA+wNIzpa9MOV8wLENgyqNn6kv/1ALINNa4Tbe4D0nfkJ5ZbzdigyXTsKjhfYnDqSZrlpsHhXvu6IH11zO8GlA0yoaVsBj5u0lZMmuJlEGFhrzhnnX+uiUgxyoCFA21wBi3cYa2qFxjQzeOTOysSvIEpquh22hQUKTq3GbY94/vjUNYGJL6WJVNVsFEKVYuMhCl0IAPgi1VttNFJpP4LkkhLmCbnFM+S4CCKsuhyuKpnluKcjgfglGVOY5GI+/DZa31DJTj42OuXr3K06dPuXz5sk/43N3dPZcgOZlMvERZgjyEtSGsDvGcm8/nXiL34MED2u0229vbzGYzarUap6ennr0DsLOzw9HREe122xfNElBQZeiJtFiSSq9evcpgMDgHNG5sbGCtZWdnhzRNefr0qS/QhGUpBfjNmzf5rd/6LS/vi+OYmzdvopTizp07TKdT7t69y5MnT7xP1qVLlxiNRrz11lteeif3IvK8Xq9Hu932BviShpznOUmSMBqNzsn2BoMBW1tbHgBVSrG7u8tLL73kv//mN7/Jw4cPmU6n3Lx5k5WVFVZWVrh9+zZHR0d+fAl41Wg0aLVaHB0dsbq66j3gxIdSAnaq41rGepUZJWNa5sa/rGiVIr8qia6CktU5UWURyT8JaVhZWWF7e5vt7W201j4wYTQacenSJT784Q8zm804ODjg4OCA4XDoC3m5RgEOz87OWF9f9wEU8tkXAeqLTMQqAFq9jyq4Judot9usrq4yn8+5fPkyH/7wh/n85z/Pa6+9xq/92q8xGo283HoymZxjDcu5kiTx6ejf+ta3vH1AkiTnWHUSFnRRtlqd+3JPArwppTzAJ21T9QasyrerIKGMI1mzZD2R11yUmVfly1Ubh2q/V6XVVRCuKnGXz8qyjIcPH3J2dsZrr73GgwcPvO/oa6+9xl/9q3+Vv//3/74HhqpgYXUjpbp2C5gqAI14El5kkAuoWH0uVDeXquOlOoYEaBQgSjZiBOATsKp6DplrAmgKyKu1JkkSD1YdHh5y6dIlnn/+ef9sKAqX7i7y4C9+8YscHR1xdnbmx4/ct7ADe72ev7YbN27w4MEDfvu3f5s4jlldXeUHfuAHAOfNKps1X/jCF3y69OHhIfP5nNFoxPHxMVtbWxhj6Pf7nuncarU8oFpdk9bX15nNZvT7fb9eiB2FUorxeMzBwYFfJ2VMCRtV+krmX3WOytiXsS2bY7KRIpsSaZpSFAXj8ZjJZOIDTQSYlPEnadJKKe99KP0tNhdVdUAVBJc5Nh6PPcgt87b62irYL++RvpGxMR6PvZ9vmqZ+8+dfLQaiMBmUYwCZyMmZimQZilIkoDP3x7oJXcEjUmUsLFacmXlSBpjAeWmULr0RrVbOc49lMaFMyQhEmE7lZZV9EE7dhxSJ8p8nrChdBngAJeixvC1duM+jZH1ZpSjqDqTUhcX2YjaeO2IRNli0IBwrZmvOZ3DRgdoxLNrO7H627vwL+y84X8PZuiU9ccVdsCh//wCmW4pg6t4f99370TDZsbTuwXTNFVrtdM58oM/Jr3TugMrrjSF7+QZ5TUFglpLlkkEYzEsgrOEYRyvBmKwOWTtmuh4RTBroaYYeTim6DfQsxwSKYCYMTuvP51idlsbNPk0V8Y+GXf6LR3+Z2r2Y//rwx4gninhgmW5ETHZKRkrh/O2muzlxkqO1RZVegKCx1jH9kka5S1ROyKZeRpzvz1dI4yWAuBX1mZiYQCmilTmougumsdC+azgcXeFR5wqfXX+JlaFrq2C8IF9J2fljTXqWOfAQIMtReYGNlylVphY538xIYZKS2VImMAv7sMjdtQeh82czhSu6JQA8jAry0rfQlF5sHvjQ9lzoCWb5kPTsxJJN6JiICps54EQFFgqFDSxBbDCZJjiMad2FxqEhOZ2hM0NeCxg8U2N4RbNYsZjEMXgkHEFZYd9a56NWKJcmO1GEExdiYUPlA4YUJVPXgI1KVpFxTEvxOwSWXmxlmIq1JQBtcQCkzPdQMVt1YAkaFh3L7+99iPWvOA1nnigWndIztRz3RVgylWqWvG48AHqlccaTeZduOKGu52xHPcf+K9OM5RiWHoCBdinIa9oBbvtFG4Be0aClp2wGQy6FPQBSVTC2IUdFi5mJvVS4oycUVnFSNOkEYy8xBgewjU3CxCY+/TcumYEzGy3BySArwcu6v86FDZbnD5yMpWdqnt23sAELG3mAryj9+ySkZWESZkT88ubn+R/88IfY/pxjfGV1zdqtBf/Bl/41/i+f/AecFk1WgxET782Il0dnNmRiAw/wJbqgE0zIbMjCBi5UpuItCXivREl3zmzo5NomdoClyugVDf96uYQQOY8AAQAASURBVN7MhsSqYIELY5nMYjemjHZzLNMOZK+GBwUWHRcEgSFOcvJcI6nos1lENomZLGqw0OTTgL/33/yUYyMWyzEf9zWbXzfs/3BM3incnDAKlSnU3D2/oqEia1mwxkuDrQITw3bY5/eGr7A37/C777yEOYuhnWEnLtVcb834Wz/6BYZFjWyYEM6VA7Yi60GsyYZG5RDOrQPbyk2yWUeTNw164RiISa/AJKoMDlNubrQURarKAC7Loh3Q3CsYXglckJk2RAoyGzqvULvcfHPrjSUJcsIydMcuAlQOpmFpbI65uXbCm29dJa+H5DVneWHCcsPPLP/puQPmnA+jpX7oLD6mXc28tDUpEucLWdQMFGq5L1KCg86DuPxZqcOuJibbkkIpzEo33hxTXdQPHhQ1oBeKInLP2vTMOAVDDsMrAab1fQbi9+IhAF21UJECQgpqOaoeT1LQiMxMQLPvVGBWi+qL0qsqE+oi2+siMws4V2xIQVs9R7WIlqK5Gvgym818QRVFkWc+wdIvT4rsPM+ZTCZsbm7y5MkTn7h8cHBAFEUMh0N6vR5pmnJ6ekpRFOzu7nJycsJoNMJa6wudfr9Pr9djZWXlA+1Tq9W8vEvaXFg7VUaU9BM4NpIEwIikWlgiJycnvlAUkPciK0v6TfwAf+mXfomNjQ0eP37MH//xHzOZTMiyzEt9lXLS00984hOkacpnP/tZH2gg/aO19sDPbDZjbW3NAxRyHcJ0lNAMAT/6/T5xHLOysuLTW588eUKapj6w4ODgwMunBQCZzWYfAGXkWqqS10aj4QHkqjyzysiSMV9lilXbTF57USIoRfVFwEjmV/U11ddV221ra4tr165x+fJlwjDk5OSEb3/72z4FfWtri52dHQ4PD3n//fc5OzvzbMqLjDPAgxjiMSgMvCrr7eLcqgKKF+emHBeluta6QAYBYyUw4/r167z55ps8ePCATqeDyFuzLPMhE61WywOLEix0cnLiQQ0ZNzJ3q+vHRWCvyp66eM1yjqpHY5WRK2tUFWT/TszSKrBRlQ1X27MqR5b5La8XMEnO+Z1YY3Iuef/JyQl/+Id/yF/5K3+Fb37zm9y6dYujoyN+93d/l09/+tN87Wtf48tf/rJ/vbA+pT+r9ynAXBVgle+rbXUxhKU6Ti+yUr8Tg1ZShsU3EJaAajWR/iLgXRSFlxxXE+lHoxHD4ZDbt2+zs7PDbDbz47ooCp/kC87vVZLlRUYs1gXSNlXmfKPR4MGDB+R5zgsvvMCrr77KZDLh0aNHHB4e8ujRI15++WWuXbvG6uoqw+GQL3zhCwBeqry+vu69/+TaZKNmdXXVb9ABPg2+0+l4JvRoNKLVanmwTBLshQEvbV/18JSxKOuDgLaLxYJOp8Of+3N/zvsaZlnG2dkZBwcHnnEK+H6S65bnhXxWq9XimWeeAZYSeFnbjo+Pvd3JxbFRHTfyzKyuHTK+xWu0Om+qGypVWfZisWA0Gp1jRVbH7f+v47sfQCwP8SGUP/TFZw/cV6sgyK0DFoR1pN37wgkEc2H9OdacKZGXIF+y3QQMQ8hY5dpkSvmTlxdbfJiKCZUHA4Op8XJnnZfgjlmCk/7cZimJluJOmCE6c7+LzzTrtTFnpwVpv0w2DhTxoCCvOUmw1aELGplrmk8L5t2AuCcefY4+mZxYB0DOnGdhcqaYrZe+biFEQ5ivOfZHnkJ6DNNFRDworyXEFZ1TS/+5ktUzXwbR+H4pi12516ypGJqY18fXKVJF1goYb2viYd2zBCeb7vpdsrQLTBH5uSpcgZvXQ8ZDzQ/8wd8m3IvRmSLtgSqc4f5sVZHXFNMrOWhLtgr/2U/9AwD+3VufYpEHDjTUrjOjoEBn0Gk69lKBC0WoHseLBi+sH/rvI+X80TSaOMlpPcwdaNZ0RfDpDyiSU0X7riYZlNK+RYYyCZ1vn0GWY+MINVvKmFWWg7WoZpO8naCMA11MWoJuuWMHBWlWvscNaqUgCA1ZFkAZpAJgjHJDSW5FdITaoAOLLV/nQblMn2MFoS12oT1wQWDRoXEAZGBRw5Da44CVuwXp8bwMMdBMNyLGOwHTLUvWNtgwdz5jJfHHRhXmoXKgiWMElkyH2XIOS7qyKiqLXOAAQS+lN2ppAVA4oBFbJjdbnCRcQEc5h7GozKByCArQ05xsPaP3xS3WzwpGuwF5A5esHlsHQtQNtpWTNBZsr4zopFOeDNoMxymJznm5/sQz4CRV+KRosh2MS1ZejcyGHqiT7wcm9eDepfDMeyBGOJBC/BALq72fogPxgpL1NyaiwKC9XHdmonPAXFamJws4pzHU1bxsTkuBe1+kcg84amUw5YAQufHEOsag/FwASYCTokmgDBpDr2gwMxG/+PGv8Jl7P0LzkVsHi5rm8m8q/vNn/hx/c/dPPIMRErQ1nhkogS1aGVp6RqJz/zmbwZC9cMJ8ETLIa6zFI9bDEVtRz8uqF6XhXqRyOoG7nxmRAxeVIaJgbJeMSmF3RqpgMYpR08AFfZRjUiWOlRbFuWf0am3JFiHj4zrRaYiaKZhDkkEcwmSnpA0GgPjohk6yTzmn4kFOOEnQeUA0cuFYXkq8MMw6bvNHGbChA7sVYCLnwbcbn/H/vvuDtD5fI1hY+s8l7lkXWer1OZFMs8iUlhLWJ66rAhr7hXvORIqs7uS+JnTzJphqitQw3QjImsoFhZXPRJVDPLJkRQmclZtcWUP7DbM0yJhZxUneXD4/Yek5mBZs1oYYLDejY/6DH/s1ADaCAanOOMrb/E9vXyGcAhaikfXsYWH+53XFomMJx+UaEkDveU1ec6nH8jBVxfI5LrJw/N8NFp0prLb4LSJtKx6J5bUriw0saZD5OWXxH1HucFkotNu4mrv1aL6inbVKqX5Q35l08/3ju/i4COQJCFBlXwkgUWVdVf3SJFjkYoJoFYyQ4+L/q8wJKSiroIEUfFVvKLmWqoxQCpbq66r+aMJCqUpVkyTh4x//OF/72tc8oCUFW9XjUNI0t7a20Fr75GTxKxSgStpkZWWF6XTKysoKaZqitWZzc5PDw0OKwiVN9no9n0YZRRGbm5tsb28znU59urD49gkAVvX7A7hz544PPJH7C4LAAy+9Xo9ms+k9paSt5BzSRl/5yle4fv06d+/e5atf/Sp37tzxIFqj0eDq1ausra35ROTt7W3u3r1Lu932Bav0pzDMBDiV/hJwRJiHzWaTfr/vwQ1wfourq6v+PsWb7cUXX6TZbPKFL3yBr33ta7RaLfr9PsPhkAcPHng2SjVMoToeJUik1Wpxenrqx18ViL7I2Koye+T7i+CajNuLwMxF6XJ13AtgJKzYIAjY3t7mxo0bbG1tMZvN2Nvb8x53N2/eJI5jhsMhjx8/5vHjxz7V9uI8EtnrRXBHrlHmTPV+5P9V4LF6XpmXshbIOapgtFLKj3cZE/v7+2it+epXv8rKyorvi2azSbvd9oC+Mc7rTMZEFEUsFgtarZaXalbXKpEFC1AobFwB6QSMqPbtxU2Q79Qn8trq/VXHQvU91Y2J6oZHFXAWgOciY/siMHlxrFTHlVy7UorXX3+dj3zkI/zsz/4sT548YTwe85WvfIXnn3+eX/iFX+Dp06c8ePDA95cwLYXVJuu0JO92Oh3PNn78+LG/ZgEUZbxU13+53+omUvW6BYiVn4nkuN/vn1uTZa2SdaPT6dDtdn1ivYBaAAcHBz5ISsI7+v2+/3zxfpX2CsOQF198kVqtdi5d+Pj4mHa7TbPZ9Ou1jK0kSXyC9ObmJn/0R3/E3t6ef9/Kygovv/yyZ2RKv8l6I3N5NBqxWCy8LHg+n/t0ZnmPvO/09JQkSTzIL20fRZFn2Q2HQz/3ZJzIvF1bW2NjY4M4jrl+/Trr6+uMx2OGwyF7e3veB/L999/3wKj4QFYBwzzPabfbtFotms0mvV7P3890OvVjUUB9Weuq/VRdb6obM9WgpmrYTnUuV8d5de5VGa3SR7K5J5tk0p5/2uN7AkAUFqANHJAmvn8uRMWBg7BkwXlvpdx9Hw9tyU5chpwEZUiHD0AR4KQ85HPEZL52nJdsEOdX5wFBOazIrfAAma1ck0gti1h5hmJWV4Rz6yVZJoJw6rzcwomim0w41aDnDvTs3Qx84EtQBpkEC0vzqUvTbD6xJH1D7cx58AVZQDgx1E414aQgmIeEs4LGU0VylhMsIsKZobnn/A+DRchsVTMep8Qh5CWDUdo0vDliksfOq6+hfeGVHi0BIiog4sxGTIuIRdtJuIsaTDZC5quK1bcWruBql+E3ASwS1ybpWZluaSwm0dTfTFxbWzcWihpMLudEgwATQv2VM/7a9Td5OF3l8bjDX6jP+fzMOMZI6WcWKEtuFEYrohyeWTkB4L0spZ3Mzo23g0mLH9t879zPBrnzbNtZGXB8Y4VoYpluuCTslVdO+N+/+Kv8D3/13+L6b8zJmxFEITZw9FKVF9jU/Yy8AGNc+nISQQRF4ny98rpapnfGBmsUpgjQQVGChxatDdkidE1dJsJao9ABpQy5xM0sBEmByR0rz0uUFaioZCNatfS31MsiWwXO89DMA6KjiOZDaN/PiQdzsmbI6HLMZFMz71rylsU0Msfa0oLcn5+/tpRjB8PAgeeJ9f2pF6VH6cK6a1AlWG+sn18CLiqlliuafIaGvBZ5gF9nxoMa/vOVAzwa+znBwlDUI2oPYlbfLpiuacaXLXnLYGsFUWPBVmdELcpc2m7JYMuNZqMx5oe3H/Lj7Tu09JS4TA2WYBQB+aqhKL2iwf3ChYlshAM6ekJbzxjb2KUPoxwYWMqCxXNQjsyGzrfPRiUAljvApnxNqrNzUuaIghkOsJzYhJmNPNB20c9Qrr0o/y+HxjC2cQlURtTV3CcICwgK0CmBOHBBIX++/Saf/8mbZP/1FsHcWThEo4I7v/McJ7/8DefNl615effTRYfHsy5Pp216sxrjecxsETHrJ/xu7SXnDzjRvPxn7vPJtXcp0OiS1QwwM1EZFKM9WAh49mGqF6UHoaWjJxwVbR9AA7ASTlGhQXcLgrDAFNoxeLMAOw5ZnMaEE000cKFJsXHPnLwOi7ZBbc3ZXe8RaMPDb+845mquCGYuCEsvFMHMjUUXkOWeJ617UD/OKRJNEbnk9UXTJR7rbDn2BTyTe9oIByRRzqQFdqq8hNZqqMUZqVKkOnPzvQT6CCzMnPR2shH4Z2k4t6gJBJlh2tUUNUMwdYFbycD4JGN55uapJm+4z1RWkZzmZLXYj5k4KBgaB9qaG1NOd0J2d0/58a33+LHWHW5Gp6TKAjUaKud3Tl7hjcMdRoMadhISjDWBcYFKtSMY70jys5Nxm8RiAigaBdEg9MzO2UaBKhTBXKEy19Y6c0zJcCaehhYJiMKWz+hyQyVRBUFolhuTgcUWOCBYQVhuPA1MijFL78bqGhTMJQjKBaupMUSTgsGVkLzJ94/vwaMKEADngiyEpQHL4kWKRK21L0KqrJuqBFmK0Kqf2EW5oaSAXpThCWPnop9TlZ1zkfVVPeQapZitFu2SCvrMM8/wu7/7u4RhSLfbBRyQJcw28aYSny6ttQdHquwnWDLMer2el7EJ40nkpOvr6/R6PZ88KYzNTqdDmqY899xzvP766wRBwHjsng1SiNXrjn0v7Jw7d+5430EBPwUsFR9GYdyJHLvdbvP+++979t/Z2Rmf+cxn2N/f5/jYhe9dvnyZ1dVVBoMBu7u7vPrqq14+nWUZv/Ebv8HVq1dJ0/QcoFIFxqpsKvFtBHx4TKfTod/vexZXGIaMx2PvdSbF9WKxYDabobXm4x//OJ/5zGeYTqeeOSpSzO/ERJGfixm/eFhWmYQyXi8y7qog2UWwUN5f/Qz5f5VxK55mF1mH9XqdjY0NLl++7CXKx8fHvP322+R5zvb2Nh/5yEcYjUYcHBx4v7bZbPYdC2ytNaurq1y+fNl7JEpad9XXUQAymScX51WVBVxlcEo/V+e/3K8ckva9WCzY3d3l8ePH9Ho9XnjhBcD5p8laMRqNOD099fJJWSMkcELknVWAqtrWVbC+Ks2t3uu/jPFUBQvl/dVQiuo4qLKkZB5Xz1MFrKvMyIusaOl7eV/1a3Xdqr4G8OCfAC2f+9zn+JVf+RX+7J/9s3zmM59hsVjwW7/1W/w7/86/w8/93M/xz/7ZP+Py5cvs7Ox4sExAuSqIM5vNfCpxlmXcuXOHP/qjP/IBKkVR+P6sbgT9yxi5Mt4F1BUwrLoJ0Wg0vEdgNe1XALdLly55/9hms8mlS5fY3t4mCAIePXrEP//n/5zj42O/Lgg4CXB2duY3nQaDAc1mk3fffdcDjQJ2NZvNDwC7ArJWx5Kkl0uqtKy7sg5VZepKKb9OAX4DSOaNbCINh0P/mVEUeWb2eDz2z5tut+sZ391u9wNr3GQyYX9/nx/5kR/hl3/5lxkOh9y7d4+NjQ2CIODdd99lNBpx5coV2u02QeASpE9PT3n8+DH37t1jdXWV1dXVc16dArQeHh560FCsKE5OHLYgz8darebnX57nPlylqjqQcVENN6qC+NUxLZ9f/ftDfl/9O2M8HtPr9dBa+6Ac2ST7V8oD0TEJHUCnSnBAZyUzCXyhYyt/2Lvfg9GgMgc4ivm6SI1VKZe12rEynBeiY/Ap44IW1BzCiUEvDHk9YNHU5yS7Oi/lkhoPDgYLx6LTuXXSXGuduTvu2sOpIWsEpGdF+Vkl+Bhaf+3BHGpBxnQ18AnHs3WLzgJsmTCZNWA+caCiXgkYX1IsToMywVgz7yjivvs969ox/U7dvS2aMbN1RTRynoiLZkReV4yuGYppBB1LtplRfy92RTDwkd0n3O2tkeROJqbKwqtIHctTvMGUcaDQwKS8099k0TXE/YL2PUhPMsJ5BMqBRuNL2r3X4jzz2oatLyv0zBnSj3dixz5puiIwHijmXcvuzWMOTtvEcc6vv/p/41JQ5+/1bvBw1KWwLklWAEStHdDmv+aW5xqOYfgHo5dZTcbnxtuiCHg+3fffpzojt5pIBdxsHXNiL5P2CqKJJpgbin+wyt959t9m9w0XPhBOc6zWqMJAXjhZblY48BBAa/cPIIn9eMuk2IxdwAnKukRlApSyWKAoZBE+z5xUyvqEZBUYzCLAqBIIzKQ6roCICqxhOWlsyZQywDCm+VDTfGxIzzKCeUFWDzl7ocboimK+arBxfr6Qlv/rEnSvfK8WmnCk2fl8Qf9GSP/FioRTmkRYukK21IAuvQ21Ki0JLLYCztlAYXHSSgCNJUsDx0aE0i9xyVZ2rGGNDRWtB5bJhubsozlXrh/TTmZM8whdtuski6hHGVFQEOucNMgZ5QnTwgGEMxuR6oxVPfJ+gql2bLonWdezDw2a5+J9tsMhQxN7AC4joKVnZCW7UJdBGyIVFtCwVzRo6DlrwYgC54cojMeZjQhKBHhiEgo0LT31foBY9z048LClpz7EJbMBBW6OCpNyYQNi5diNLT0jwBDbMqiklADHunDhIGhSlbGmJxwWWSnfLvg7N/+Q/9Vz/warb7mxVKSa9Tdz/sM3fo7//M/8V/zd13+a5O0aQQbBDPTCossxEBbQBBoKJDApyGD6kYiVYEpmQlKd+XRjStZkgQIbkqopGYGTMyvjmZgaQ0sviNSZ93p8Y3qFb/UuE8YF2TAmelKDyJKMHFgo/oN5HaY7BY3LQ37s8vt8uPmIq9EJu2Gfa6FlRdf4jXGd//mf/Aq1Q+evGs4dq1AY7acvBH68oqyTyichQWYd0DV3zOvxZuCsAhbGW2RIOFVbz3iSd5djX+p+YTcGBQGKQZESJAVFGqBsmZgeumdUeuaCtopIUe6HOEuQQDkZbs2Q17XzRQyW80buyYxxzO8Aov6M2UcS8ppjsRurKFD8aO0+/+gT/1eOihb3F+v80emH+O0HLzMep3zo0j6//txv86XZNT7/zedJjkKCmiVvFxTtgmAQMF+F4xWFjUzpJWg9a1lZ0CUYapVjTcY9l3ocjSzRyAGjOnfP0eHlEBOXKJ8u21KXi1W5UTOxIdbKpgVI6rwtkduVaEakAtraPQS1BEkF2nu4msiSnrr+zmuKrKaYrQYUKR/YTPn+8d1/VP/ghyVDSlgB1eJZ/sivAkZV0FD+VQEXKRKrhYTIrqosRikkL/qASeEhIIN8L6BT1TdRrrN6fXJP9XrdF3vy2qOjI65cuUKtVvO+eAIWiux2c3OTR48eMZ1OWV9f93IsaZd2u83p6Sl5ntPpdADo9Xpe9pumqQdD1tbWvPxZAArxOxRvq8uXL/P66697pp6wQassKJEe371714OsrVbLG8tL0b2+vs5isfD31mg0qNfrrK+ve09EgP39fc9krNVq/PAP/zBaa4bDITs7Ozx+/Jivfe1r/PzP/zzHx8c+uVUALSkEpVBfXV31IHSe56yurnL//n0AX5hqrb0EUcbfcDhEKeXTQbXWPHr0iDfffJPXXnuNd9555xzjUQAlYXbJvcuY0trJtCeTiR93AkhfBAGqoKH8rDo/5NxV5l4VtKwCCxeZbcIGq9frrK2t8fzzz3vG0J07d7wv2cbGBkVRsL+/z/7+PoPBwI/D6tyR88v7VlZWuHbtGtY6z8ter8d0OvXtL2OtKr+VowpGfic/MQHJZSNB/B2rAJ38vN/vez9VSVm+dOkSb731Fvv7+4xGo3OBHQJoVeXD8vuLjCtZM6rAnLDU5HVVD8IqoFEFuaobFNKfVaCi2j7Vta4KqgqwKuuSAJDys6qkU34mLFe5JhmfaZpSq9X82ifWCsJ+FoCrXq/TaDTOrVmTyYSzszN+53d+h09/+tN8+9vf5pOf/KSfA9PplLOzMw8+CyNO2nc8HvPcc8/xwgsveAZdlSVZPS4C7LKZIvfR7XZpt9u0223W1ta4evUqzWaTz372s2xtbXn/UQkQuXbtGru7u9RqNWq1mmc4C2h+9+5dPvvZz5IkCR/96Ed59dVXeeONN7DW0u/3OTk58Wzw6maWUsqnVTebTebzOdPplPF4zObmpr+Hqhw3TdNzPoUXffikHyXQZzweewBOfB2FPbiysuLnbK/XI45jn64s6494+YmnoDz/5HqiKKLVatFqtXjvvff8Zy0WCx49ekSWZdRqNZ9QLyD4zZs3uXPnDlevXmU4HPKVr3zFs3uVUl4C3G63PUAqnynM0KpEular8cwzz1Cr1TyTczQacXR05C0VBMStrhfVuSefW53H8jsZ79XndpIkbG9vs7a25oNTtNb0ej3vsSqbEXKeP+3x3Q8gWudRaAPlwknUMolV5yUokLgiAutYfJKy7A7lX4dSPmnZgX2lB2LuGEoe+MtcIRIPirLAcsxDGziQ0UmUHDvPeba5T0r65W5UycBetDSmLN5UmeyMdWw8bOn7pkvAMdXY2BCOIZpaTAz70xazdSfvna05H8f5KiSnMNtwhd2iC40nMF13rIt5+f1417FZZhvQ2IN5193rdFNRO7JM1hyDMttSNB9b5t1y8dueo85i0mNFOI4JFu76ikjxQvOAd0/XgZItl2uCDOarFr1Yek1ZDVnDsZpOx3VMzVB7OibuR+jMkH7rIXTbpGnE6NKqL7La96wHVFVuPSszb1iypkVvzBgfpfy1H/sS/+uNr/DvH32MJ9MOV8MlzSPSBYFykkhjlPMtC50UMZMwkAJerd8H4AsnN/nLW986N+R64/MssADDvHBTaTfpUzs2pfTPyY6LRLHzhRnx0wE2Cn0CqJj5k8QueTkMyjFXoBYZqrBkG20vB7ca731oLS6gITQeMgvKRFhjXNiDsBAJnCeiCiwm0+4egzI4pRr1bXEMHCiDTHByx0KhByHpkQPHt7+yIJgW5I2Q6XrI6FLMbM06Wa/IIjWOdSjnLYFJcCADFoKxJpwo0lOI+7b0TsO934KaaoLMMu8o0jM/Xb1/qZ4WJbBZnlhkieDbWOakVVDE5R+3hV16jOI+y4Ga5R+xxlJEMLwBr754n9wGLIqAJMjJTEAjWtCIFsQ6Z2FCtHL+bQsTcDBtsZd1uRkfMjQpRrmglJO8SawKB6zpjG3tUpgdqzDlqCgLlTLUIy51rg21YExMr6gTq6IEvxZk5SKyHfZIVcZ+3vGSZZEmO5myk0On2jHr2nqGVoZLQZ8CxdjGzEzk2Xly3sJqIlw4yVHepkDRKxrMy5jZp4sVCjSPJl1O53X685S8CJjMI2aTGLMIIHPjS08DooFygHAOtb4D33RuKZRCLyyrv9rgmy9fxcwDGnturZV1VySxru+EgedCM8TD8jhz/h3r4cABmaogVXl5DwEzEzsQE8cKXQ1GTEzCYd7i3nyTb/Uv8XjY4bTXpBhG1J6EbL6eEb4aoVJL86Fba05/MOfS9WN+bOs9/krn6zwXZmjlgjQiFZDZgqHJmVn42nyFXtHgV4/+DEXNkvbcOpk1XBiPVcqPZyprY+3IEg8dQFYk7j7nK45xp7OSFWyFme6mb6pyUr1gloVLeW4596y21MrrNDh/xiIxWCBKMzIbubV/XZcsXRcAFk0temEZ77iwkXAQEI0gPStccJEqAUbtQLGspUrrAYWNAvIyXEgVECrDmp7z5mKb/8U/+Bs0HlvGu4qsXdplNCz5jiZQmo1gAKnBxNYBfJFxjEATuvuNLCpThHMIZi6lPZxCOLHMV5W3J0FB992CcGwwiSJPNIumpkhKsDBwLFAZT5TLFAp0IiFazvJguT7K+uJkzrVg6fO1szLg6SvwgzuP+O+sfZMfrz3lt8Y3+N/9+r/OZEM778MScE17hsmmJvu+B+L33CHFtLXWg2dSRAHnCmv5elFqJIBM9TVVTzgBIQRgEgaDAEjVAv47AQZyHYvFwhd6IleUIjtNHZN8NHLetxdBEjG0l4JzMplwcHDgUyBF8pbnLin44ODAe0tVWX3gGHV7e3te3nxycuJBTQFEzs7OGAwG7OzscHp6ynQ69Sm69Xqd4+Nj385SZJ6cnPDcc8/5axbfvirIIQyVKIo4OTnxoE+r1WJ/f592u83h4SFpmnLlyhXvuygeg0VRMJ1OPaNSTPGfeeYZLl++TL/f58UXX+Qb3/gGeZ5zeHjIrVu3uHfvHrCUc4qvVrXIFvBje3vbt1mv16PT6fh+kRTYNE29lFrGg7CPhJ0kBebXvvY1vvnNbzIcDv31yxgR0Pei5E2AlyiKODs7o91unwOaq8VstX1l7Mj5qkBC1auuylaT/qoeIhltNBpsbW1x5coVD2wURcFXvvIV2u02V65cAZwv2rvvvsvBwQHj8fgc07EKYArDUySKkmrd7/c5Ozvjwx/+sJdQVu9FWGgXwd4qcFgF3KtyyyoQVvWKlNdWNw6k3yVc6Pbt27z55pv+PrTWtFotgiDwQIuAUQLKV0GhlZUVD97IeiFecwL0ZFn2AZmntNdFNqDcj7CS4zj2HppV7ze5Hmmn6vlkHEv7ynmbzSbNZtOPpTAMvVxbpKPCnBOw5jsFi0i7Vue8sFmHwyFvvfWWH8OLxYKvf/3rfPzjH+enfuqnyLKMf/pP/yn7+/uebSigaXVOCWvy2WefReulRUJVFlp9NlTHuDCmr1y5wsbGBmtra96TbjKZeJbvbDbjxo0b3Lt3j0ePHnHz5k3+zX/z3/TAXr/fZzAYsLe3x+/93u/x8OFD7w0o7V+r1RiNRgwGA05OTs6lQgswuLKy4u9B1vHT01PPAmw0Gn6tFSCuumEgTPGLjN0q+7IKhA2Hw3M+qjK3JPxFWJYyNuv1ug+WEha6gL8CgqZpes57VTaU5Lkpc//ll1/mypUr7O3tcf/+fb7+9a9zenpKu93m05/+tH8ODodDnjx54u9JNjBkHGxubp4DrWVjZ29vz4Od4hl769Yt70cZxzG1Wo3pdMp0OvXP7OpGnjDQRd5d/VuiXq/TbDap1+veA1LA1hs3bvArv/IrPhxsPB5z8+ZN+v0+77//vpeZy31UGeR/muO7H0BUIk0svf0WUES2lHtZD+A5lqIABEsWk85Lf6NgWXj4sA6l0CVLyUQO8En7hStic0vWDEowC6CUUCeuOAbIa3h5rypg1tWuMM7cZ8RD40GLInFm9GnfSaiLmCWgSRlYol3RuGg66dvZvM6ibdl8fUE4jTzQGE0MwUITTl07JAMXYhENHRAYDy3TTBEPHODofBUhHsJ417VH1rKkJ4rxmqtQ8xo0nlhsfc50z00onS3ba7GiuJEccXbapJmXQK12sjJwIK+Xw5bnD0p/QVXLmV5qMLgSoiys1i8TPzhxRbIRcLeU1s1cnwULJz2bbCsaHz7hZ67c4b/X/RK//K1f4W+ufp66bvDucIPL9Z4fKpHKiYNSrlnUyPOAMCwc6GYVYVgwm8Yo67zVIOB0WufH6+8CdX+ebBGyEQ7892OTkJeUn8ezLoumk/OZ0BWp4cSi5wWqP0QlMTZNMN26Yx1GoQMTAw2FcXJm7cBHO5tj0sCPj6zlHjouvMTJE+VPLKUsUVSQZQEmV+jIOqCg0P6r0g5wRJXnKMFDSxnGYp2cWYcGkwUw1yQHIc2Hlvphgc4L9j8WsWgFFGshJ68o8obFRrkD/QLrQCMrVNMSLDROeq2n2sk2p4rkFGonhnDqrieraSbrIc1HlmARkacw283pPR+grKXzvi3Hg/LgcTicOxBGl2zh8v8ARS3ClqB+NCz/CIqcaZzILgFUyWqEJZaqrAtD0nN4a3+bJM6pJwu0soxmCUmUO6DGKuIwL5XVlka84GRcJ9twfXaSN/n8dJd6sOBqfMyV6IRULUMXJiWbUEJHAqyTBqsCY12KsilZfA21oK4zZjbAWE1HT2ioDK0smdVMbEJdzTFKozE8zFY5LZpMioS5DZmZiGGeMi0i5kXIJI+ZFSG9WY1FHjCdxyzmIWYWuDRhQC00wVT7BPpg5jYdgHJDxn3VC9kEgbqFZo5PSncDVhq3HL8SUBG6NTqva9KTjP/09/8Cam1BXnebMM4ewdk4GOXmvXsjJWPMbXLUwoxAGep6QUO7BOmGnpOqzEu8PUBKQCeY8F8e/ln++L3nMOMQPQ685YPSoEtWWny2oPUwpPeCY1H3PmT57M//Xa6GTeY2Y2Zz+sbQK0KGJiZVOR2d8/eOf4z3RxuczuqcTWqM+zWSqfOQVWbp3+fGs2W2FjhWY9lW8xVFkVTAvKklHlrGO+Xg1i7NNxpA7nBnElX40ByfLF6CdwBJ6Mbdad50mwuhs0AQj1SdQ/1QGIjueTdplMVgpFBz54FYxJp5R5Onbn2TfnfAmAP2gCXLtwR8a0HGwmrvtamskx0XNQNT7TYegMIaMgLvkerHT/mm+lNNcuY20cKp8Qns4DbeBs841qEqgdPxZkCwcPNe5U41ECxcmw6vBJjILgHc8vNM4MZBYeG0aDqfV5xHMCsZUS3jhc1Ddut9fmH1qxwUcz4SZ/zuh36dnILMOtA6UQ0Kq1EWGocFwcxSpCUg3HYbS/zp/k77/vFddEhRKcW7MHOqAA3wARaNFAvV9wEf+L2cp1arnQsTqIZeVF8nHn1VFowAGFLACPCUJAlFUXgZsNxLFSCqMiSEvSAFkMjhRA4lRaQxhmaz6cNOVldXfeEvvxfwJkkSL1MTJosAIGJCX6vVWF1dpdFo0G63GQwGHB0d+fCVnZ0drl69yuPHjz0Lq9FoeG9JSUqVdq3VatTrdba2trz5vyQOh2HI5uYmvV6PKIqYTqeeafLo0aNz7M+NjQ1+5Ed+xH/mcDj0Etjbt29z/fp174VV9T4TqXIVSBYgMEkSVldXfdEq6ceDgfsbVN4n8kkBCrTWHiwdDofnrluSmY+Ojsjz3IMdQRB41tF3Yv2JN16WZaytrTGbzTz4KG1ZBeeq8t6LYDbgr1fArSrbTMZuEASsrKzQ7XbZ2tri0qVLWGt9Cingk7OttTx48ICjoyPvlVb1ERVQTBhJEtRQBVyFXSbAymw244d/+IeZTCZ861vfYjQaeRBOgFb5V2XsVeW6sPR4i6LIWwwIiFcFUGW9qAJNch3iFSqAX5VBKG0lIEM1IEh83GRdgqWcd7FY+Haqrl/SLxeZpBKgUZVjCxhTq9XOMdHkXHEc+7kqYIUwaqs+deKjd1F6LOtllb0pQLfYCQgTUBKpBVyW18t7ZGNHwnLk91Uv1NFoxK//+q/zi7/4i9y5c4fRaOR9SGWuCqAjoKIwsQW0WiwWJEnCYDDw75E1pzrWtdZsbGzwyU9+0m8Q3L1718t+B4MBeZ7zsY99jPF4TLvd9kFPP/MzP8OjR4+4c+cOBwcHHuSTJPXNzU3yPPeMRZETv/32217aLuuahKMIC1UA0Uaj4Znd4gkojE5hz+7v73+gfaos1eqmiMxrAShlbMuaI+ucjJl+v+8BVAGfBXgUtvhoNCJJEu+BK8+llZWVc2tZFfyXsd1qtbh79y6/+Zu/SafT4cmTJ34tqNp9VP2AZW42Gg06nY73mK2y+GQ+CZhZBeQ7nQ6LxcKnWsdx7J9r0iZynoubLHINr776qgdVV1ZWPKvx9u3bKKV49OiRDzcT1qzWml/6pV+i0+n4DRG5T1kP/tViIFKyjYKlsbuXMJul3NhJIkuQRLsiQWeUcl588EJQeg4WsUJh0ZktWSCgc0VmNPHQYMNlgRQsTJn+LCmMasmACvGprcGsBCVLlqOJlS++wrkltLa8B3cdwuywCrKGY6uYEEZXoPkYnp61ybqGcJyhbIQuYNFySZl2XVM/yMibAXpuSc40zb0Fxb52EjgbEfdzipommBriUUA4NtQPA+JBQdIPiKYFtUNN/WBBNHVJwM3mmKeTjgcBlHHtOL5s2A771N9OMWFBXselBctRAQ91BnnbebItFiGN9gxVNGgcGIpEMVuLMeEGk82IvAbJbBnEohC2psXEAeqH+vyzj/59LgV1nhY5WhuisuJ8POzwZ1ff95dQ13M6sYSjOJaeFNCAZ+9hYUXPgTqLPOBaeH6aFIWmoRbA0uMrN5rCGu6PVktpn0WXLL6kZwmGc8xgiN7edOBgoNGjRTmGCihKdmqZMoyxqDShiDXKWhZNTbZa8SUoQStbavfiOGcxdxV9GJeAWQkeli93BJpSqi0Agpk7ZqKwGpkFhE9iGo+hfmxIj2eo3FDUQ0Y7EUXdMlt1AEK2mi+Le6sc41Dh0IFCmFJgQ0t0GtB85KTs8ahAF5ZpN2Cy6UIFrIbaoWXjq2egNfPNOvc+pTF14xJrjZvjDvxfzllVWMgMNg6xocZEATZUpU9ohpoXKOPA5qKZOP9J3MLnE5hLtpLIN23ZZ/EQFo+a2ABm2knqIyAP3PtVCXKY8v8DBaqhuHdpg5aeEamCjzffoxNM2NATGtpQWJhZ7X0QUyyRnvp05QgHHg5Myn7e4TBrc5w1aQZzMhvwZm+XaR6xKAKGs4QsC8mygGIQo3JV/iv99eaOdSxp9CpnuRZW2GvKQq2wpOW65V9TlOxuhW93t4Yuh6EfrzImDcuwKSpfS7Yd5fy12gFVWPeZWStk93OWsxdSTCzrsdtsKUpmrA5Lf9sA8kST19x6q7FkNiCzQenzmLuEam19QIxIlhtqQUMtuHW6TfAodX6fGoq4ROOFlOrou/76TaiItsdo4O+e3mQ3OuPObIfVcMwbo8t8/eAyr24+4e9d+Rc8mnZ58xs3XIpwbLGxpUidDDmrOS9Dt7mgPPNc53iP3PTMBVHlNV0G9zgmrQ0gmC6Zi7VTw6ClsYGlod16WhTa3UKA8+srQfU0yIhwUnitrQPFFpqoUzDHbXpNNkvQMi83aeYOcBtvBdjYEPWDknEuYWFLkLiIFaN1jQ0NulDn+h5cWnGsjGPCptaxAIWgXIbJxIFjhxdlOox/f7kh4e4JaqcF8xXNvBX4nwlgGY5LcDJwgHDtxBCPnFIgq7l1q0hcsrRLcZaV0QGObjxCELo1tKWn/J3X/pCNTwxp6SnXo1NWdc5WUCNSAROz4HOzNW5Gp3QD9+zQSqPLwX4lOkEVLuEaXNuq3IXj5DXlNxu/f3xvHVXA4iJLRwrsqqG+FAXy/4uyJOAcI+wiu0v+6K8a0VcL1CogWQ0EED9CKeIBLwGUAlx8AIVlJwWNtdYXuFI8drtdjDFsbGxw7949rLV0Oh3G4zGNRsN7scm1i0m+ADACdABe2lZNF240GpycnHiwZmNjw7Ooqp6AWmsuX76M1tozBsXUXsDBanHY6XRYWVnhxo0bDAYDkiSh1Wr5okvSsMfjMU+ePPF9IKwZKRgvXbrEyy+/zK//+q/z1a9+lZs3b2KtZTAYeECn3W57AFEYoFWGV/UQUKjZbHqmnQR+SLEqoIv0e1UWKkDu6ekpZ2dnnh0kfX1x3FUlqFWAU85dTTDd2Nhgf3+f8Xjs+686PuWahIVbnRcyFqsMMRnjAmDU63UuXbrEzs6O9yQbDAa89dZbHtDodDocHR15sFWYWVXGk4z5tbU1JGxBQOFGo0GWZRwdHbGzs8Ozzz7LyckJd+/eJc9zbty4weHhIWdnZ3zoQx9id3eX119/nVdfffXc/K0CgOLNKW0sIJ705WKx8HOpXq97+WtVTl39KkEyo9HIF/07Ozt+Dkiwznw+9ynjAohFUcTly5eZzWZ88Ytf9KBLURQ0m002NjY+4Ncqcn7ZDKhaJAioIyCgbELIvcn4FZBOxrDMZfHklDEvYJ8wJ2WeyVwTcLXaxtWAIekDaefq2lQdu9XxKeOiysTWWrO+vk6SJAyHQ8bjMe+88w6//du/TVEUnJ2dnfOVq67v1TEm/S/zW9bgi+OxOt6DIKDb7bK6usrv/d7v0ev1/IaPMOySJPHAnNyrrFGf+9zn6Ha7fOITn2BtbY2zszPG47HfnLl16xaXLl3i7bff5uHDhx58u3nzpmcTzudzBoMBSikPjgsAXpXEdjodsixjNpv5gCaA09NT+v2+lxYLyFe15aiySBuNhrcDkPX4+eefZ2dnx/evjJXqRogAwvIckrkhcn9hqG5tbflwm4s+p3JPVUZ0FYiWsKnqZoY8j6rPWvl9HMf0ej3/fnlWCatZxoR8blEUPoyr2+16ALrf73tWd3VtrI7VLMsYDAZorXnllVeYTqfcu3fPWxr0ej3/WUVR8Pbbb59bT+r1uv+bQlj0snn2L5Pa/7cd3/0AonVsImWsK8YKAZuWoKIviMuCyATLYtZqtSwaA87JyQTM0zmkPcd6yxONDV2xJ/6FVgEGgplBWcf8y1rKsyKdV5MrbmzgfBfzWsmQLNOV6buCKa+5C4sHrqCNJtb7v6nIeV/VnzrZ3/y0RrA6J2vHjHY18cAyvmwJ5iGTLUUwi8ma7nNG1wAVs2gpwoll8Ay034+Yd50EerqlaD5S5HVFnoSMrigae67AmW5GZDXFfFURzxKiMUuZGK7YDK+OeSk+IWu6685rFjInK3fpt3j2EoCtF9xfrLM4Sbn8/D56mpA+nbmidJ6jspzJ5rZn5jhWKJ4p4mSmmtXGxEuU72QrWKsoLe/oj2rsRmf+Mw/yFU7njrIzNgmqBOFYdjnFwgFaG4FlbjNqcUakln/UnRUTzDwgrcRnRiontwEGy2gRkzdc/+gSWEnPCnTfeWEIu1DPcwccAuSFYx5GIWiNxaCyDNtqYGJXlM7WlGP4GQVh+TCSQBXwLEplFboEDTGOaWjLn+VZ4G/ULCp/qI5D4oOA5iMXJBIPpphIkzcChlcTpuuK2aYLElGZYt7RpQdhCRSGJSKVay8/btwNiUY4P8PAkq8YeOT8yU5eDphdzqitDpk/alJ/qh2gX4CazFGFIWjGKJNgA4tCQBNbpnkrDyKaOMBGMSbSDrSd56iJQeXG+Uxa67+inJepdLgNlGfQSmhLNDIuaEUrMJZaGbZtw9KDonydLcEeAXNM4NaWyXrAajTmZnxIRkCqXILsv3v3r3I6bDCfRmXCddl2mS5ZmhBMtPf7C6aykVCZN4Zl8m0BqYVELYFMmScyR8SnzXu6ehC2bLtAebaas11w64yTyi7BIbkmq9x9u8CpJdMNtbRpOAccLrEZL8MH1+4mgum2ZfXN8vND1/YrdxWjS9r7pIJFlcE7y3M7FldWd30wKxzD8mp8zNgkpWR74YJTsN4zEmA1mGCsYrqISnsKd+5gqnxbFwkOxCzXOAG6ksQFkdydbvCf/+bP0r4Lk61ShpvBk0+MCAlohfOSeVdKegNLWGUgDqUPXXDW6CqEE1WOYZiuOVmuLpwHYjxwz57BtYCibglmBSYMluBmBHXlmKxFoYlKEFfSzU1o2U4HzqM1PuRfu3mLl+tPeCV5TIHib3z1v09yFtM4KJzlQuTY8EWqyBuwWHFzOm+6gJRFQ5U+jY6ZaSIHkJrQoIqKNQM4drCBWOfUlZPIW03F03c5H8NyTY1VmVRvZHxZlCrl23YZ9hLOyoTqzD33i1gxuqoJZ+XcDC3TNc2sq84xJZO+a8/xTrCczz70zE0erS0HRY2Tosn/qHMXgLnNCVRASEygXOPXdcxfqM+B0oJAaWRlLazhUbYGQOPAeOBcWJ72u/+vr+8f3+GQgkGKZAEOhdVXlWiKrKoK6FS9BmGZclwFiIQtIkbwUoRIgSLXIUWWMDOkuBDmVJX1JsWUyJirfm/CTKl6WUmRKTLjwWDAtWvXODw89FKqyWTiJXPtdtsznYT9sbGxwfHxMfP5nK2tLS8vE8mtsLSKomBra4v5fM7Z2RnNZtMzx+7du+fZdwKQFUXhgcuq72SapvR6Pd+2VRB2Op2ys7PD/fv3PYvns5/9LFEU0el02NnZ8TJxOC9HlDYV1uSdO3d4+PAhr7zyii9uq2zK6XTqvcrSNPXyzYtBFdIGUuRL/1flofV63bNd5PfSviIHL4rCB56I1FPM+quAjHh1Cegh1yH9Ua/X6ff71Ov1c5LvqtddlYVVHf8XgTEZ2zLma7Ua7Xab7e1tdnZ2WFlZoSgKhsMh9+/f5/j4mM3NTW7evInWmtFoxN27d33asDCqqgw+Kdq3t7d56aWXmE6nvPPOOxweuj/sBAB54YUXvIfn6ekpx8fHXhZ4dHTEvXv3ePbZZ8+lfFfl5jKWhGEoPxPmbBUsFsCiCqJelAjLHJOvWZb51Og33njjHJAlY0TWnSpY02w2+Wt/7a95rzphKf/QD/0QP/mTP8nGxoYfu+JHVx0/1UCn2WzmE3v39/f9a/r9vn+dsPwWi4VnBMo4kPuQ9ebiOJd2ErCkGuRQZa3K+y4CQtWgoYvnq87zi2xZ6TcBTKtz4/XXX2dra+scOCjXd7H9pU9EPipsbnmNfK3ev1yzPAuEjVj1rhWrhKqlhdyjgITXr18nTVMeP37MfD7n3XffxRjDpz71Kd59910vnZX7qoZ2CJNOpMrSnvIZ8owRdrdIbuW5Iuu1SH3l/FXZtgCkItG9ceMG8/kcYwwnJydcunSJ9XVnfyb2CE+fPvXJyxIaIwzyXq/H2dmZly+LTUWVpSpJ0wLSVdl8F8FfGZcy1uU88v/qJmB13Mnz6ezszLMmJWin6nFafY4D/jkk7dZut/3zuAqEV69V5tDKygq/8zu/432Cq0C1nLvRaJyTQMsmkWwILBYL/9yQ9GphNf8ryEDEs2qcbNIBd7oQpkRZsGgHBoaZ8aEoReQkcvI7J2lWPgAFa0l6JSARalS+LASKxBWdNnbnDuaWcFqyDEsPL6ucHMqUTMQ8dTJAU8rEhME3X3HXa3Vpzp+XBbRxidB5rZSXLtw96tx5yLV3pxTxCkUN8kyBdjIpnTmDf1VYFisODJl1HSgh5u2LjpN8z9YVReo8sByL0QGA03VFOHPsxyKF8Y2McFinMS3PbUumZwE/sPOUujAwS/BAzx3QJACIgD9FZNn5vZD/tP8XoJ3TiBacrcWwFjPraFqPM9K7J8zWHBirrCsWfcZHQVmIaYpKUMiTrEu3PiUti7tsHnoAsbCGr/evMS9CCms4zlpYC8ZoglLWrJSFeUCeOE+ztxeGNMw9mwTgyDjgJ1LLCR6rgkVR+p9NU+aXLa27DqDRuSU5mUGWoTsr2CAAazFR4AFGlMLGJRVFgLHhCNa7LrjHWCdfVtYV5AqUtpi8lCVrS547tmGeBQShJc8DtDa+QNfaEoQFBQHFQhMexqRHisZTQ3paEI0WqMIwX004fanGdEOx6DiJoY1NWdC7Aj6vl30sSa5queAE/YDkVNN9p2CyWUoTDdDK6P2wws4CVD2HUYh9o01jXDLdyjRvAbdsqD0ApufKgXqldNWUXoaoBL0wRI9PIAiwwzEqTVwbKoVdLFBaY9tNCDS2GX8AyHIAVwnolABbEZ5nAaiS1bz8QbneFHiWMoDODEkJUBo0a3rMwKS8Mb1C/4+3aR5YOmUifFU27QE/vfRiMwIOK5YBEfnyfbpwGyFaWJ4S7iBNY/ASVpGzQgmSqGWyuaTLOxbekhHl1rclo7Caam0C9/WczLTSPtWEYFP6uOqilIba5WdGA0WRWIJyfShqmnBiiPuK2Wq5tgYu2ViX7EnjbEIpElisQF631EPnCTmzMYE1pMFyp0LAQwETq8FCzUeKpC+BJC6oygQwvqy8dYIEYqGglc6pqwiDCxWR1GQTua/1sDRtVrYEqJfjxoRONpunjlVuItcPVWm4LS0P0lO3aSRAU9ZULMo+c4xSS+2kYLwZuPNrS1pucmx3hjx8OWJ3q8fPbt7jUnLGK+kjXokHGGIaes5KOOUrw5v8v558jKfDFvOTGmwr5p3QWRKEJZgcWhcEUlACg4qTV1SZkl7u9IcO4LXKEo4CiqS8Zy037r5EqmBil+PDg8uqHPPaunYDNKXHiICHsUEp54eorKJ+WJA1XJBLXtMsWm6MKOuAWJ25MaYzRe3UODC8/BugiNw4zxqq9OFkmeiucAxeA8YonuRd3ptv8XP120QqoK6WjPPqUVjjpctZaZg4tIYIuDffwAQw7WoPGIoPYuUR8v3je+i4yDz5Tp5C8rNq8Qn4IlHOUQ1AEZaVFDYChEkBKkVflckkwGO1KJTCsVpYSLEnX6vvs9Z6JogUh3Jvw+EQrbWX6tZqNQ/wiYm8eFgZY5jP56RpysHBgS8sj46OmE6nNJtNDzj0ej2stVy7ds3LB+XzhRGklOLBgwe0Wi2fdiptdPPmTW9w3+l0UEp5xp5IFbMs88yex48f8wd/8Ado7TzihFGys7Pj0zwlvKUKOkhxWAUypDDWWns23dOnTz0rR+Ru3W6XlZUVn0paBYAuAm3SfuKFVwUxpRCsFrvSr+vr6z69d3Nzk729PS8jHw6HHtje3d1lfX2d4+NjHj58eA4EtNZ6cKDdbnNycsLKyopnWlULXLmmqjS/+rXKqgF8UMT29rb3EJtOp+zv73N4eOjnx9raGhsbGwwGA9555x36/b4HrsSvDTjHepNwG0k6PTg4YHV11V+reHIqpbxMVFi14hkn7VoF1aoyyiqoIG0uMu8q2C7/lyJdAD/ZOLgoU6wW8VUZuIBw1WtSSp0DQGRcyvWLZFpAy6IouH79OsfHx/ze7/3eByS+cghDWTYohCVYbRNZly5KjqWtZDzIv6p89OJ6WGWjyjmra5+8pirvroI5AkRXQcnqmlr9rOpnCFAr7dput2m1Wl42K8D15uYms9mMR48enesfufcwDP286Pf7vu/knqWPpG3kq6y5zWaT7e1tDg8P/WcLG1MARVmHZS0XtvXa2hpPnjzh93//99nZ2eHRo0d0u91z1yDXXO07AaZl3AogKXJdARqF7ShBIeIZK6/pdrve+1LAUKWUZ5D+zM/8DHEc89Zbb7G3t0ccx9y/f9/3gTBOkyRhPp/zzjvvAHDlyhXvAdnv9zk8PDwHxouNgbUuMAqWAUsyhqXdq0zQqq1A1U9Q2uYiy1DmYHWtk2ejAHYSaCRjr91uA/g5c/EZLKnRIn0WprmM2ernil0JwMrKCk+ePPHMY7lOOWRDSPxzq88rYRrK+JSNSGnP6hz+0xzf/QBiWWDrwhURwlqQ4lUAQi1Sscy6xOQ0xAQQzowv5HTh/AxtafAfTaphHXrJWixBHROVEuWKj1o8NBSJq0ayJpjEMU5MWbg4IMBizLJYF0DGlp6CJoRQuZ/7tOgQKNQ5hlHc02w0xuTTJu27DvwLZ4r0zBAP3H0XERQ1l6aMdjLqedf5H2Z1qB+6ABY9dwV5/cB9H40V8zVLchemG4r0BOLOnPxJ3YWmpMtkyqyh+Om1t5lZSzgpAytqhnCkS6ClsniXPmbTdU3zPgxe1EzziNFO4Aq6DIaXIhbtTeYrLuBFvCqh7NsyWGFwPeQHVo79uU+LJtebpzS1+6NNKcv1cITLbnVJoNMswmDpFzXHapFC2DpPQJUrippCK8Ub80usxNNzw21mHYsv+A7xmadmwfRBC1sz2BKEisYWtcixhUGFARhDttXm7i+kXPutkNrdE2wYuHCVooDcQH+Emc6wraQElLX3PxQQTZXSa6Vd+rJWTrLsPc2UJUlyikJjrWI2igkPYlpPFI39gvR0gZ4XmCQgawWc7aZMt1yCskkMNjGOJZeV474EKmwIi457jYSu6EFANNTUDpxk0GrD6FLAZMsuESMLYZpR9CKab6c+VdpqaD0yjLddYIqAfx7oU5Zw5lh+ed35iemFJRrlhMM5erKARQaBQTVqoDUYN6eVTlyytVaYekzejEpA1q0D4Th3mwJmyQySseVANQdc2rAMYqmwlSXJFZbsOqs1JlKkOnNpxypnpgpmJsKEkLUUec1tCAjwrnIHFoVTt55kDQc8e/mmAHVAnvomwbAMfPLrkiz8ugIaytSrgjeU9xecf78J3OaFsm6TQQAO8XH1bK3z+Oq58+ocH1Dhmd0aCvCJt1aBSR1ANV9TdG8p4hEYLCp0EtXRz02ZPGmwclv58xcxBNmS1pitWPKGYS2ZsBP3SNXCyZT1nJaeMjQ1FjbwgUd1vfQW0coFURWxY10LWOr7RcDRCqiUhjmRCmgEcw8a2sB6Fms9dJ+T6LxMRy7Hf66IRpRhXiVjTvo0UQxvUvrJuo2rrOnAL51BMrQlG9Ry8lJYym7dZ043y02nxBCpgI1gzH/y/D9i+GzMzEZ8pvcR/smjH+Q/2f8pfv7lb/N/2PkTfrP3Kp959yWySQwWktYcQsuiW2Bjt/6JB2YwU+iRIhwrZhtuE6GoOxZyMNNeIi8BJigYPCvjQZ0bu81gLvakfpPJjy/jBkVeGubGqnDrmoDiucZaCBclwLsTlNYhlmBuiUfWpd0nmsFN7Z51QJG4TTNnW+Ke2TpzEuzaqSGviw8HIAnv5TX+Tz78B/xc/ZigcUKkljrjwhoMlswWTGxGqgImtmC/CEoPU/da8Td9OlshnCrSXkE4NctQpwAWK99hIn3/+J44xBMMOFfoA+eKD2EIVoE8AcKq7CM5pACqBjJcZIBV/wkzSD5fir7q5wmIJkBEEASe6VH1xpPvBcQT03dJYxYQr9freU8mYT0IACXnloJVvAzBBakIoJPnuTeZFzagSHW73S57e3s+UXJnZ4f9/X0PLhhjePbZZzk6OvKyTmFYCShSZchUwVdJrj45OeHJkyeewQiO2XHv3r1zUmStNePxGGOch6MkTFcBV2MM+/v7XLlyxQOk4nMlYI+wE6sAjvQrLJOWwzD0QJ9cu/RPlbEln33t2jUePHjgGaD1ep1Wq8VgMPDjoNFo8OEPf5g4jrl+/Tqnp6e+GK8CpQJ2KaXodrveV7FacMo4q7Ja5R5E6tpoNLxH5erqqme3PXjwgF6vx8bGhmfxKKU4Pj7m1q1b9Pt9nyhcnTvSlnItly9fZn193ae3Pnz40I9fYwzr6+uEYchkMuHevXvs7u56sFmYoDJGZOxcBOSkP+TeBWQRSabI6quhPjK3xfevOo8FVLzYh9J+1Y2DattWJcPSV1UQUu6hVqt5GbwAHnt7e9y6dQvgHGNaPls+twp8CYAiY64K+sn1VsdC9Tqrr62yvKrsKWmT6piqypKrLNnqeJN2kvAUmc8yd6ryWTmn9IOsv9JPq6urdDodvvWtb3lGq7Dcer0ew+GQk5MTf245RPY6HA79+iKs82ofVZmn1XuXrzKvFouFvz4ZJzK+5L1VeXYVcAX8evKdwEo5p4BMEsAiPq5Vj0u5flmHF4uF34yR+xcW3YMHD/xrtdYcHR1x+/Zt4jhme3ubS5cu0Wq1UMqFcL377rtsb29z584d7ty548H2NE0ZjUZevi0g+NraGlEU0ev1PNtTKRdO0u12P+CrOx6P/bOhOk5lPkqbVkF7mUcXmXjyLKuOL5kf8rwCPBg/HA7pdDrnnu8yfgeDgU8C73Q6AF6aLXOgun4+99xzPiX86OiIk5MTv54KuLu7u8vVq1fZ29vj/fffP9d/8pxYX1/3vqvCps/z3PuTSr/+aY/vfgARKmwR7QEIVTiJq5M3WR+gYkLnE6YLx3xRhQMUTeJkkiZSvmArEo3SwKJk/ISldLLAASe6ZOqUEuoiKf0FC2fUroZQzAX0grBMlK0dObAgr0E0XQIpWMesAVdsKmEkhu615JpoXBbhGqIBbNRG7AXbRBPLZEPTepwzXwloP5yRNULSuSFYhEQjB4TozJIMNNHYyWkdeBGQ9A1FrByLchIQjwxZQ1E7LlB5SDSxZGFBeKQZXbUEE8eiCKYu+fK12j1OTUg4dUW5jQtQTg6sF0Apu/SgxZ8/pRblDO6v8kL7kGOu0HhaPijKkItoAijnD3nwMU3jsaL1OHcAbKg4+0hBJ1oCfKMiZTUeE6mAwrqCV0q/QGmu1M44nLQwGI7nTbQuQ0bCAqUsoTbouWvrAMW3xle5XO95uRpAz5QBMpXxV6B55+E2Pz/4WzQeaUbXTBl+A+lp4VircQRRCNZiQ037Pe2Su5MIjIVAUWYgoNKEoN1E7Z3SnLU5ebXt2tM6pg7gpMllarQHQYE4zknjjKwIGB03SB9HNPYs249yosEEEwdkzZDhlZjZmnLpyanFNHInRc6VB/xQuARUBRQKPXZ/rJjUEPUCwklI3If01GCVJWvC8JpLOU1OYfvLBUWimWxozj5anjpxCOhk15I3DbWnAfNS7h/ODLaegjGYRLtGjuy5cKNgbginrk1dR2hIYuxpD2opKoqwoxGqs4Ktp9gowKRRCTwagpIh6MBIFzqkc0tFkU44K7xE3oYaDIRTJ202kfYgpMxZB8Y59CFYWP6b42corGaQpxzM27y+d5lwgvdrQznGljBp3Xx381xkuab0YhQgzs+byiEy4qq8V8KhPEhjy7lnz5+j6mG69Pmz2IilqjQoASXwYFqR2BJItqha4ZhjgQPKtDYY5dLAtXIhPgrISy/OJCwoynU4KwKKEvAe91cI37Voq8owpwLebKFemKJvpS7EqrxXx0B08tpg5taadjj1IGFDz9EYhqaGxtDWGQWKwmoyGxCXqNUiD0hEArtYgsZFrNyGysL1kwmWbSQMuUgGi7AptQWj0eW5Txd1bGw92I9ynn/B3JLVFYvWEohWppSrl6AgGupHbm3OE8W8Vcruy02xYKGwoQtZsW45AW0JCZhZy3/3i/8WzS/VKVKYr5b31TCE2gV7dMMJQWDJQ5fmHoaGeaFID5y/YTSyhFMB55wXY/9myOSqm/OdO4pobAgyU4aHLcfT4GqIiUXLbj0gZ0JFK1j+YWIjSUiXgazA4IOoeqaOCixFoyBZn3JptU83mfD1WzeJ7gY09/LSH9IB8ouWwoSacAbpcRn0oyCcKpKeJZoYH4QmrM7ZinbP1OqcKp/TVkO/qBEoxcRkPDZz7ucrvDW7zBujy7zb3+DRQZfknRov/fl3+C+u/wZfmt7kjdEVBnnCrIgYLFL685Sjsxa0DIumZtZxwSnKun4x38cPv2cPSQOt+hEC54pwKeiqhvwCnlUZR1L4Vn9XLYiqRWn1PcIiEwAyiiLa7fY5Ga6AfOPx2EvYBBCEJXunKv0SpoIw4qIoolar0e/3Mcbw9OlTut2uB3eOj4+J49gXqCKNFjmyeOuNRiPfNlUpoRS51lpqtZoHgXZ2dnj33Xe9V9jm5ibHx8fMZjMfoCJm+PP53Bfbk8mElZWVc6DLjRs3+IEf+AGGwyFf/OIXqdVqvPjiix7cOjk58T6PAh5eunQJYwzvv/8+0+mUa9eusbGxwbe+9S2fZimATr/f58aNGx7QFQBWimABdy4a6EvfCqNNwj/effdd3x8COEu/dDodz6LK85zHjx+Tpinj8dgHYOzt7aH1Usp+dnbGxsYGw+HQ90EVxBEgbTAYcPPmTba3t7l9+7YHPL/T+Jav9Xqd1dVVut0uzz33HJ1Ox8v+Hj16xN7eHp1Oh62tLXZ2drw88d69e5ycnDAajTyIVwWeVlZWuHr1qgd01tfXfTGutebk5MQD+ePxmJ2dHVZXV1ldXSWOY548ecI3vvENwjD0LKxer+ellsIUlWJb5oNINmVuCBgj8nwBfgXglzkiAHwV1JefC8BWtQUwxnwgoKbKMhSwXa7rohRYmIUPHz7kwx/+MH/7b/9t39/T6ZQHDx54MLt6DdK+wqCV9anKiq0CslWQ+DuBLhfXJTmv/K7KUJSNE1lDBVCRNUDAGAnwEKDnIku0Kt+8CNAJ69Za500qnnXj8RhrLZcuXeLatWveexXg8ePHvPbaa0ynUzY2NhiPxx8A8oQZ12q1vKdede5WGW5V5pisEzKPlVJeGiyMtosp2VXGpIyHKphd7UsBu6vXI+NE/Emr4SPVaxVZdVEU3utUUp2rY11kxlmW+eeetZZnn32Wn/iJn2A0GnH//n2+9a1v8fjxYxqNBr/4i7/oXzscDjk+PvbXtbOzw87Ojl/vpb/EA7bVanmG8c7Ojm8nCVwZDAZYa3nuuec+wBCW8VT1z5R2q/5eZMUiU261WvzET/wE6+vrRFHk/X/v3bvnmZYyZmXDS9bc6rqepilra2vkee7XHdlYqfo7XmSqGuNS6Le2tvixH/sxrl69yrPPPuuDuWazmfey/Pf+vX/Ph5aJ3Hw8HjOdTnnrrbf8fJJ+FJZ/NSDtT3N8zwCIwgiygcIYl9Ybzh0QWJWoKWtLPz7r32tixwrQuS2lx9aBOqos5ENFHqtznyeeYip3hYAHAiLHMgznBpMr4pErBk1YGuYXjimY9A15vZToFopo4ph/Im0yIa5ATkvgIaJEAZagQdJz9zDZDFm0nJeWzkJGVxWqSJhsaWrHht7zmu5t5y2WnFpGVxRrt5xpfjyw9J/RrL7l/KKSvmW66cA7EyjyuiZrOoZOI10wzZxssIih8UQRTS1nu5Zno4JvLWolSKggsqgMXyQDjjlXBjPstAe04xkHcYdfWP0qX8x/kM6bpxTNhNH1BouGIpguPe+ivvIgpF4Y8kbAJz9ym2awnJhn2TIp+bCYnPf5A44WTeZFQGEtB7MWxmjCsEBrw2IRESiXIiog7pNZhxeb++fOkdkAlRjESu9Ls4L/7N6Pk9xPUL2EYGZJTjRFugSDTS2CuIuyFn3cJ68F7PwLlzJtowA1z/nAoTTFk6foRo1FWy2BPABthTQrimfSJCPLA+aziOxui+ZDxdX7OXFvAkoxX40YvFIvpckGkxoPENnIlmwh5WTJBa7/tMUutP/5+uuK4TXF9HoONqDx2DFre89p5lfnMAvo3ArRCxcEUX84QGUFzXrM6Fob215QGIVJIBwpUHoJwKkykbftANpF0+lvlS7DHObusz4g+zMGOxxBHKHqNUy7jtlddWyjyQI1ywhKT7ag9HV0zFnLfCUgnFoWrfIPmTK8KK9pDyopYx1gWbIUg3nh5KvWelaTCTXzTujCJeaW3t+/wj/rXmPRwoE8yoE5eqGIxiLrVV5aKYBhVnPAWBHj5Y7eh0+GsjABvRa4ZMGJHFnYhyJBjQ2ELjQHQEcGrQ1h6MI0sizAGkVaW5BGpT9HmLOaToiDnEke04zmxDon0QVnixqzIiI3jtk6L0LmRUBeBMzzgMUiJM8Citz93pYp3FgYCgBe9rkKLHYa0BGfyQCsceDuxhs54/2U9Mw4Rmbg+sfLoIegjGJ2OacZztHK0tALAmXKwJQADcuUaz33fogBbv6Y0LV1ni7ZciKhd0xKx1zTC8fMjHSBwRDpYsnajECktrVSOm1Q2HoBpaUEmSacuuuOx9YzCMEBlkUNx3zXbnNrslFJYJ5Z1NRdz2RbebcA70FrXZ8HSjM0KcUwQmeWrKnIa+X4iq0HPScmJluEZeq6C4QCqB3bZQpz7J5ji6aML1CFokhLW45AMW2WAKYwCcWfM3fIqCqfn64xXT+kCjIbogrn7buYOY/FolHQ3Bnxk+t3AHgl3uc/+tiv0dBzPhIfczlsclZM+MF3/g5FGjK8HCIWAsEC4mnpExwopkoRjdx8KFLXDot2UNqBuGuUdg3mJTBbLr/CeAzm8A/f+xhbH+rzv/36X0I9SqnvK1qPC5rvj4jaMZe6ESYsOPvxOnUdkdmALx9coz9KyechteYcCbAKxxqdG5Lh0obDahhvBZjkg0v/94/v7qNaEFeLZCkOJTQBOFfAwJJ5IIWbeKgJ60oAwe8kyRMAUhg31SIa8AWWFJhSmAkD0RjjAUUpjhuNhgcLhWFYZSkJu3B9fZ3T01Pv1yUeS8PhkLW1Nc8sE0bP9va2lwm3220PFEgBK2wIkUULQDYej/39CYhSbfNms4nWms3NTW7dusXOzs4HQlYuMpjC0CU7t1otptOpZwT+4A/+IF/4whcoioJWq+WlaCJRFpBQmJEvvvgiAG+++SaABwGESSJFurSfsCslmEJAh2qfCptPwGNrnV9ZlSEoY2s2m/GhD32I1157jTAMOTo64p133mFvb496vc7a2hrdbtcXs3LvWmuePHnCwcGBB1SqDFXplzRNmUwmXLt2jTRNvQS6CqBU/SZXV1fZ2dnhypUr3LhxA6UU9+/f54tf/KJP297c3KTb7XJycsKdO3c4PT1lNBp5MKbK1pJ7lcTS3d1dOp2Ov6eqL2a9Xufw8JAwDNnb22M6nXrZ5FtvvcUrr7zCs88+y507d7DWehapfK7MQwkjqfp/SpiIADnSN1WmsVLKS0Krc1PatMo8C8Pw3JognyvMqiogKSxUWTuq1yBgBzg23HQ6pSgKvvzlL9NoNNjc3PSg6mw249KlS9TrdW7fvs3Jyck5qWP1+i4CnAKSiFRVWHJy7UopD5oKQCRAiLy+Kv+vSoiFNSmfKWzOKmBZXc9kTYCldYOwKWWuypoj/SdtLUzMer3u/Vl7vR61Wo3T09NzoNXBwQF/8id/QqfTYTQa+Y0QWU9kPd7c3OTSpUvcvn3bj9sqq1ju96JMtAoEC1B30VdQ5PVhGHr290X2XFXaLVYTWms/juU1VXm3MP1kI6g6bqvXX6/XfWCWBIAIQ1DA1yroBc6i4PDwkN/8zd8kSRKePHniGYZVqa08Y6ognKxhkmYvr0+ShGaz6debs7Mz9vb2/LySewD8GlKdl1Upd1XK/NJLL7G9vc0zzzzjmdoiJ3/33Xf56Ec/Sr1e5+7duxwcHPDCCy9w/fp17t+/75nUVfZhlmU0m02m06n3aQX8/QijUqwgZN2Va6qOFWm3Z555hp/8yZ/0thWz2Yzf//3f9wEq29vb/KW/9Jf8M/bRo0d+HZDXd7tdtre3mU6nnJ6eet9GYef//3N8jwCIrliJpuWCVzimks5diIp4biljS3mgM+EP5tanNgOEo8Kx22JNUQtciEqxBBrBMQOxjqGhClDWEo2sYzDqyqJgS9/FQJWsRPfjZFA+NKaQnjj2i/h72cD9bNFyQGI8tEzqqgQpLSrTjC850/7uLYgmhoUJmLcd09JEpZ+UdWnMReLkxXndMl9xkul5V5G1DdN1TdZwrMusaZlsBBQ1J3eedy2TicaWAOJsw5I1LcGgTi2H2r729xgsLHY1I1Uhe1mXcFpWuMoSLJbMGRyRi2hq0AtLJ57ydNJGRYaNYMx0U7HYbKIKy9kLmiKxtO+Wxau1dN53Uj4nQS+YXk34ye5tXh9dA2BuM94fbfBq5xEApyZABYagXMgKa3g6WXHSRQzjLEZSjOVfYRXhxF2zRjPKEn6g9vjcWBubBJTlQV7nPz76QX7tjVepv5MgSmfHMIHg42eMz+oUb4TM11KChSEYZ6hWnWiYOdmttSgiVJY72W0YoPICAo3qrqC315nttlisLMcUgXUhHEFBGOdobZmPY7hXp/UANh/nxKcTbOjG8OBmjdElxXSnwNYzVGzc+zOhrJXnlgTl0Lp/BuwiIBwEJCdOgomy5A3Xv8XVGcfrIUlnRpEH0IupPQk9G85qULMMAo2eZi48IsmY6NixahNXTIcTBwLkNchTTZoVqFmOLlJ3HdpSIS9JIGvphenYxub6DiYpTX/nOcFwhpqUb9LaScRL/1LH/HFrggmWPmh56phJJhQAv5QaL5yEPK9rdGZdUJKxWMowksIu5ZYlGJgMnK/kw78Q8PEfukNuNa1wztG8yb3fvunXAhsq8hL4C5TbLFiswHzVYGOLrefoyJCkGYG2NNM5cVBQCzMCbbyfXyeZEuuc1XhyzuOvFmS0ghkzExGUC9jRokVmAs4WNbSyvNjap7CaVjBDK8P+fIV26BKkMxsQqYJAGSYmphuO+UcPfojDd9cdk1QYduCAssC6oCdtCaKCMDQkUU4UFu66o8yBcFYxyyMe7q+iZw6I0rmAca4fwqmlcVhQxCWYWx6yDqc9SzFV9HLt2YUFighDYTUdPWFsEjIbEpTsRAHRZjZksQhIp5Zo7CSwJnTjoUjdnNALmG3EBJklGi/tJjJbMMpd0epB3XLMJ9r9oRUq42TAVkHu2iavQRHh5NKxANZuDuiFC+ApYrDa0nxaMG8H5DVYlEy5ouZkxulBUILWeFaoKoEqrQwE1qU8l1J0uT6591RnmFxhc41OCorcgfiLtsKEgWfPBwtL1INoXHD6ocglqfc0eeLGajRxjGEXTOLAu96zoQOsretHz2I2sBJMmVm3fl5+ZZ/1j4349PaXeCXeRwMtrairiP/H4DK/0JzxrzfOSuZ3s7w3hU4Kkp6l8TSjSB0T2LEKYdF07D4JYyo0hGNFNHbp1wIMFyUL0YRlXxvl7TG836eB690zPj94jvrrNeoHLsm5tj9DzxZMLzeYbLgAlt10TEhAQ89pJnPGs5h8pmjX3fozm0Vl+AzM2+6ZK7YrNsRLvL9/fO8dAlpVGYewBAmrRaQU6FKwVcEGWHomXizS5OdyrqqXYRXsk6MKzEhhWGUJCQtCmCRxHDOdTv17rXXpt+IfKEVtEAR0Oh2stWxsbHjwcjKZEMexZzgKG7Db7XoA7uzsjCzLePbZZ30h9Oyzz3pmlIBaN27c4OHDh4zHYzY2Ntjb2/NFnkgKkyRhd3fXg2zPPfccw+GQMAy9jLfqjyVMjDiOGY1GHkQVH0cBJJrNJoPBAMBf03vvvefBwyRJePHFF/nGN77hi7K1tTX/OQLWpWnqWU3ilSeptxI6cZGhJECpACZBEJy7Fq2176NPfepTKKX4yle+4pmQ4LzEhJ345MkT/xkCjE4mE1/8ShF7ka3UaDR8ETwejz1jVA4J2rly5QrXrl1jd3fXs/q+9KUvMRgMuHz5Ms888wwAJycnvP766xwfH3swBJZFc5VRJ96P6+vrNJtNz7g5ODig0+kwnU45ODgAlhLCtbU1n/Ir587znP39fa5evcrKyopnCstcE4moAL1xHHtWo/SBgPvSttX5E0WRDySYTqceRJjP5x4kkLaVzxEA8SKbTQB0YUsJ4CzXXJ3vAm5XGavy/6OjI37zN3+T3d1d/uJf/Is8ffrUJ7Beu3aNPM85Pj4+tzEg703TlGeeeYbt7e1zybXV9USuozp2BdiqMiYlXGQ4HHpWqQSXyEaAMOVkHRQZqjDtBFCXdXB7e5vnn3+evb09nz5cBS+FZSV9JmNA/mVZ5pnTAlwKkFllABdFwe3bt/3vq16lVfBre3ubbrd7biOgytiWo8qWlHEunyPzWViwAopnWeZBfunvqr9h1V5Cxml1wyJNU7rdLt1u148jWYMk9EY8DRuNhh/zwnTc2NjgypUrfr4sFgsGgwE3btzg6dOn58aEtIfci/iFVoPFqs+zqlxfxrNYP8iciuPYn7vq62utPQcYylrWarX8nKn2ZxVoL4qCw8NDnnnmGV555RUWiwWPHj1iOBxy9+5d7t+/z+HhIfV6nX/yT/4JsAxcuXTpErVajSRJvJ+qeArKs7dqCyBtIyE0w+GQ+Xzuw7TEN1LuqcqslfvvdDr80R/9EXfu3GGxWPjnRrPZZG9vj9dee82vd8K+r/o8CsNRNslWVlaYTCY+SKUKRv9pju9+ALFkHUqSohRXIlmWQ+cOPJQCNJwtGYvB3IFaJtEsOpqg/J2W1MQAz3A499GBAxeUsV7OJkWbY9pZzz70ScpmeR21E8dkERllNHTFookcqCIMtnhomW67e01OKT2eHPjYCBfkTWjdKTBB4Ezjc000LdshL1ObM+vByXCsKWJcQEoLkjNF3oRwBPM15301W4VoBHnLgUf6pSGzUbIEBK34Tio+9ux9AE6KJsqUXm25dsBnjJeJBQvHAp12A3bSPt/YuwzWBZJML2cUiWbeDUlOnF8c1no/MifHLKXlaUj/WXgpecLvn7wMuML+YNJkdd3tRp2YmmNbVfrLWEVU8Tx0X/EAYqgNZu7AjIyC3Gga+jwiP7MRjCL+xhf+JuG9lM5+KZdPXWEYjR2YPNhv0Xo3JGsY8lpY+qtFBIsa0TAn32j5opZmgg0EVEhR8wyVFeTdOrO1iEXHsQ91XCzBzn5EtF+j8dSyvZeTHI0AWKym9J6vM9lWzFctebfc/SoTnO1Cu7TkyIEcNgQfWGAUehASThVxT5GcOh9QE8FkU9H/kEEvFOFxRL6mCBo52eMGtQNNLXP9FE4cY1DnoBYZNo6wceTmkC4Zj6UkV5UAVJG4+ZKc5ai376F2t8pJhGcaeVxMuRAVBwDUnCx5mhGeTVBnA+x8gWo2ynvWkOWQxtggwAa6ZLLZ0uPUkNe0CxaKnBzcBIowW4Iw1BxTUhm5hoB4bAhmrs+D6TKkwSWyu/keTQuaDyNe+omn7M9XCHXBejLiTusG6WmZEmtdOzmms1uL8pfG/M2Xv0S9ZNYaq4lUQV3PmdkIY51U9umi45h2JqCXu4fn8bxJb1EDYF6E9Gcpo2lCnmtMEWBKNqAt18ZLu6f8+Oo7nOUNhkXK3ITUgox6MGduXFjIajhiaFJmJmJSJJwO6tjEcP2ZAyf5x5Jb7STJVlEYzSIPyI1mPo8YzGqYaej8WxfOO08YfWwtHBNWOW9LZRzIFs4dK1wvLMT4zR+3FpfAbWViB8oQYAiwNPScwmrGJiFWBQXmnA/izIYUaKzRLphKLz0slXGbOcKa1uJVWa7DYbl2zH3SjFsDTabQuaIWZOQUfKi5z+D5lI1kxIeaT/mH730MdatbMlhd2rSASHnNbeDo3PkhmgSOPhIyX3VgoA2t/ywXZFJ+W24UyZzIbAEEju0prNVSkou2HtycmYgwycmKMhBEZNkjS3OvcIzI8l+ewqwTurW3cBtR8YjSrkMxrQfnfDZ1DmpeTlqWP5fjyCS8kjzlD1/+tYothCtIC2sIlOZX2odAysXDWIuZB+VGWOT9P3VeshDHlmIBWVt5aXBec9dxLsDECHOxZCBWDpH4K2tphnMiZciakPcV4dRtttkkYroWoHJIh4a90QoGx/CslR6Yzc6Ea+0z9kYrBIElGGhM6IDMeGjcpqTF+f62LjTS94/v+qP6B3hVjgdLcKTKvpGCSoCCqkxY2BkX03GrTIWq+bmwTgBfYALnCv8qg6cKVI1GIw9kCHgiHmpStEnR22w2fWEnII2why5fvszu7i7vvfcem5ubjMdjdnd3Mcb4AI84jj3zcDAYsLa25n20BCAShuP+/r4He4Q11O12PUPy3Xff9YEyq6urXLp0ifl8ztHREbVajSdPngCO+VFNN636xzWbTR48eMD169fZ3d31gGSz2fSJvReldAIQgWPaXLlyhX/+z/+57+/Nzc1zxaMwlgRAlIIY8EDkxYITHDAnBbKwtSQpe2Njg26368MNJpMJ3/zmN7l7964v2mu1GovFgq2tLc7Oznj8+LE/jwA4k8nEg4RwHkQCPAAmQNtwOPRM0ZWVFTqdDlevXmVry/3tdnR0xK1btxgMBtTrda5everH2Hvvvcfh4aFPERW/uCpgI2DC9vY2Gxsbnhkr80NkjNPplKOjI5577jnvo3jt2jXiOObk5MQDStU03KpctF6ve/BCiu4qkNbpdHj8+LFng1UB/Oq8E0muzHNhxWmtvadd1X5AgBIBVeS9wnS01pIkiQe1ZS2pAlgC8gtoIXNQgAC5NhmrYRh6dtznP/95z3K7cuUKb775prc1qIa2dDodXnjhBR49esTR0ZFn8wogId6Ycj0y9mXzocpOlHHe7XY9yCJ+osLS6na7nJ2dYYw5JzU/OzvzIIp8jeOYy5cvs7q6ysHBATdu3PBhE5PJxAO2F9dYGeNyD81mk2azycHBAbdu3fLzQsZldc5VGeVVv0o5n3xOFayqgofVzYEqWFSV1MoaJcCbyLXlsyVQSgA4YXOKZ2Kr1SKKIu+52uv16PV6vPrqq56Jm6YpDx488GEtwsAuioJut+uZ52J9sLGxQZZlnJyc+PVPxi0smX7VZ4uMI2HJys9l3AkgXAUVZczKa5rNJv1+39sBiE9u9dlWFC5US8BWecZU2eIX+0raP45jzs7O+K3f+i0PpPf7fb/uCdtxNBp5gFb6TYD/+XzOxsaGB2GrfSLWFHJULUXEo1VCYqTtq5sA8twVKwGlFP1+3we2NBoNz9RdLBbnpNcyHldWVojjmMlkwnw+92uePC+EgVu1M/jTHt/9AKIqC73cutRLlnWLDRyDUC8spiJBFkagDRx4Z3UZCCLFo7UUsQtECObGSZwj5dlL4dyU6aCOlSTMQ2ErAv7zgrnxxZ4AjSi+I8hZP3RMhTwpQySUC2DBOjmWnjvppM4cAFnEik+svM+Xaq+Q9DKXIJsoVt6fkq1ERGPIGgGtJ5mXUOf1oGRnOiAzb2iCufuMYFYwX3XFsRXGZuSSmwefMDAOl350uWv3RVPxF9a+zczm3J1uoAqYrylvxC8pzCaE2tD5NJpI8WTWgW+3sNs5qbJ0dwbYoF1K4izBTJ1LkVUlQKwzQ5EEZDdnPBdmtCL3x1hmDbNFxJXY/eF3WjSdD1ulip1kEWmY+/+HYUGeB561FZRsN2VgYgpCbXzwQmEN/9Vwk//j23+e2uOA8N2aT8SWNioSyOqKuG9Z+1qAiSxZw/lwKQvzFbfYJomidmAJ33kIhXj5VcZClkMtRacxWV1hGjkqMtheTONhQH3fUjvJicZT9LzABprx1TrDS4HzFlwpPCMMcOxCI4CD8vPmXIJuYEkfRbTfd8C0DSyTLU3vQ2C3Z5hpSHwUkh4r2vcLes9FzH9wjjFOaj98piA5DYgHUKSK8LhwCBmgJzNMbBkdN6gdBP5zhU0oHqJ6UbikatzYRlmsccETJnHMLQfyWOJ+jp4VRPs9115h4DwQ08T5IoL7/EC770NNthK7pOES0E/OcgLlxlQ0yJmvRo6JWM7dPHEp5aoEsm3k5v+iqQlDdx1ZsywES2YzFhSWcJKzdktze7TNc41DJibmRnLEYq0gPQk909GaEkyzjoW51hmxHg3JbICxmk44YlEGg/yLkw9x62gbpSyTWUyehY5NlukyEbucd4FjRaItQWgIo4JaOqMWZ9SijP40ZTxNyI3m945e5t3DDbIsQD2u8YlP3mIrCbnV32EwT5nnIYNxSr4IMPOA5u2YoAH34w2SRzHRCM/OW4KsJUAcQVw4tmnWdqzKojOn1p4SKMt4kpDNl1JYkSi7UCqLCpz3ZJ4qorljeQo45P+fFBRWU+BYiIBnW6bK+R+iHPA/MCmdYEJEQZFpwoklGZSy3cgx0qrMvmiYM10NPKM2LkG4XlZD5S7FXj035IcvPeaF5gG/uPI1ZlbxqfY3+Fj9fX6r91F+7dFHGZ42aGsHXE82dTmWXahMkYANS2lwXZE3C/KOdYBrpggHAeFEEU6WG0rSvnIobc8BiI5xX9J0tRsPqc4wGIZ5iipZi0rh/BBz5yU43nIndUEjziM4mBvm3RAbWKKJJms4ia/OIBqbikUI9J4Jv2PAjrKWs7zBPz77GD/VfotnwukHXlP1mZUjswUjM6fAclBoWGjioaVxkPvno2zQFbELHStSd+1Fooh7Gqwh7TvA3z13lffYzWsybkurjMB6kLYWZIS6wMRuI84GClUYilrEvOPUCwyX1zozEWmQYa3i+bUjWuGcy80e0yyiX2sSTp3HsDAlrT6/ufb943vnkOJBgMKLBcxFuZ8UTeLTV2UeCHAovl9V0/cqkFgFX2BZHFeBIGEfVos04FyRK3I8a61nSEjBKYW4ACZZlhHHsWcNCkNNKRdkIcW7pHeKPE1rzcHBwbn2mU6n3tcviiJGo3JDtGR5Ccgl0lcJ2hAfKWlXrV3y8MHBAaenp0RR5MHAatoluAJOiraTkxMPSj733HM8evSIoii8Cb4UV1LQVYNoBOzodDpsbGz4thZ2h7SnMHeELShMkIvJp1VZsBS5URT5+5xOp3S7XZ5//nk+9rGP0e12efvtt3n99dfp9/sMBgMv9ZXzikRZ+lP6Q0CHer3uAxIEnKx6YYIr1FdXVwmCgNFoxCuvvMLly5fpdrvkec7JyQlf/OIXAQd6bm9vs7W1xWAw4Pbt2xwfH/sxVQUMq0yk6hgOgsAn347HY27cuMHKyso5SZ8wacIw5CMf+Yhv1/v37xMEAd1ul+Pj43OfJcy00WjExsYGR0dHHtStAkQCEIxGI3/fAiLNZjM/BuScwgqUgBbxL6x68Ul/y7yVOS7goQBvwnLqdDq+T2R+VtcSYZ7JGBEWq7CZ5DUCSB0eHvLRj36UP/iDPyBJEp4+fcr169ep1+uMRiN//zL+ms0mxhjeeOMN9vb2zvkSrqys8Oyzz2KM8T6Ccj0CVMnaIe+R9hI5pcwrAVEkqEM2Q55//nl+9Ed/lNPTU87OzhiNRn4+DgYDD/K98MILAGxsbFAUBXfv3uXo6MincK+srNBoNHw4iNynAHZyrffv3z+3TlfZcRfZcmma+vVLAnUEEBU2ZvWeLzKLZf2WPpf1rso8rAZxiX9prVbzfoECOuZ5zksvvcSHP/xh6vW6Z3q+8cYb9Pt9JpOJZx7K5koYhjzzzDPnnhHGGC8XlvV6Npt5mbMEMcnGkjDKq2OyOt7lWXRxfks7yVdpvyr4Ju9rt9v+NYPB4Fy7yLOx1Woxm83o9Xp+Y0A2uaoMYTlkfHU6HXZ2dvjc5z53zjKhuhbItcl6KJ8tidEAp6en1Go1Wq2Wfz7Is1sAYEkxl/aUc1ZT5GXcyZogzytZy+UZXmVOb2xseAahrCHS7vKMlWuWsJQqG17ASwnyuriB9d92fPcDiCwZcbD8KoCfKgE7Pbdo7DJkQDkAcBlCUBbehpJF5GSNQSkvE482AQkdWGDPhxsotUxnDV1Bb7WTMBexPsekcdftiuJgYcmT5XvDmUVZ42SUi7LYSAzRIPD3qHPLvKN5Pt5nvlYwuJqyaCvGly1WpfRvauIBTHYs62+EjHec/+HwumLjW5b+9Yj0zDC6rFm9nTPeCqgfw/CypnM3Z7IWUD82zFc0jYOC0UndyedKdo6yrpAc7yheSp5grGVqXLKnCYHQECxckTS+4mRt7QeWyfNOQv3e//0FuiND75UJKzpguzVkmHRoPpzBlZRFq2S0zZds0GBuULnFNBU7633q2vkWAmRYlLI8Fx0BNR4t1kjSjKhSmM4WkWPvYckLTZaFSACJUpbCuMLQBoojE5IVAQ214HcnLf5PD/48d9+4ROORpja0HmQKp24MTNcVJobaYXk9LeWkbwPDvOPYThgIMidlC8chUXcFNZ1DUZDvH6DCCLRC11JoNSjaMXlN0b4V0dzT1PcXBJOFCwaJAuarEZONlMmWYnophyhbSuJ0ySos1BIoFDaTN1Ir/5V+aLPdDFVEjhH4/Ih2Y8Z8nJI9btB914HWJnCyxmiomVtFMHNMveQ48NJHObdt1jFpSFF3D8f4sAzZSUoQGsrQHjdmdG4c2KeUk/qVAHuROoAHawnHLkQlmOWorHBBKUqhSkk4WqOyvExM1hCn5J2akyyHyrMOs5qm2I4co80GpKcF4cS4a4kdiymYL9OQTeiYTsRuLcjqyocz5KlLw714pHtDvvroKi+9+JRUZxznLZL1KeZey28kuCAM5QHE5zpHNPScsUnYjM7ceVRGqjLunG7SO22wuj6k25qwyAPisCAJCtIwI9SGUBm0Mp6paKxmYQLOZjUm85jeyKWPK2UZTFJGs4SiUNRqCziu897/+SVu1xS9D0HWKUpPTAVJgZoHNPYN8chwOo9p33MbHk9/zHL1+QOeaR9ztXbKjeSQS9EZN8M+j4omDbXgSpgR4dLNAxT/ZHSVf/9f/BXiU8dis9qNhWBhyeq6tJ9wNg3hDIKpIZjm6HZEUW765DWFDp2UO6hoQccmoa1nLAiIKRyIiDAVLT1Tc6BbU7kgo8DNTbfG4BmIwSTDhAl53W1kOMal5n+5+xn2/o0/YWYjIgp6ps4f91/koGjS0QP+7dt/gycP1lCZdqCUUYyuGIbPWqzKy+eAwpaMXJUpN/aB5CikdgRhKb0N58Zvaj39REDcU1AIe96dO4wK5jbntFgt53fFm7H83knSCwpBeJUDr4tyfUh6lvpRQZGUYGrsEsGtds8cl05viYcWXbiNkkVT+76TBGnPkJQNCguLFcVfbn2La2FIosIPgIWFNUztgp7J6ZuAsQ2JMSSq4NSkBFjuZ+uoTLFYUVgdUsTqnF+lzpwUPJy4Z4awPVFuvS028OFH8owN5iwl2MJCLRnPW8mAXl4vgb6yrzLD5HKdrLnc/KtFGRrlwnuUJY0zrtVPeX+0wTPNI95T6wQTxbzjGPnR2JL2HKA5vByQtb/PQPxeO6QYuMi6qAJ5AiZU03erPk3yPinEBCCQ9wm7TD7vos9WVapVlURXwcxqKIW8pwpeSXEvhaIUFkEQsL6+fo45U2X39Pt9z/6Q1wiQUqs51F5AxSAIvB8iuALnYvLr5uYmp6enzGYzD0YMh0PvySVgVBiGdLtdrl69yr1793zxJYWasDWazSbtdptut0sQBDz33HMcHBxw+/Ztbt26xac//WlGoxGj0ch7VBVFwWQy8VLSKsBnjPEBKVXpaKPR4Pj42Bd+whbb29vz/VZlVVWBY/kcYd6FYcj+/r6XWv/1v/7X2d7e5vT0lN///d/n29/+tge3xHdOCknpxzzPfbtL8qoAIVo7/8jT01MPfon/5EXA5fj4mJ2dHdbW1jg8PPQM0K2tLa5cueLTk58+fcq9e/d8UE6VoSXjTcafAJbr6+s+aVSAmGvXrnkQQRJdO50Oh4eHPHjwwEvjX3nlFZ8OPZvNqNfrvmgHPAtQfDIfP37MeDzm0aNH58I6BDiJ49gD7sKYEqCxai0gjCgB+kQ6WQXo5ZA5K8CD9JmsA8LylD6R8S0ggoCLMj+kbwRIEkCmGjoja8F0OuX999/nZ3/2Z1lbW+P4+JiDgwM/VqV/ZY2J45j19XUfUNFutz3bUObl2toa3/jGN+h0OufAdgGnBRwUwFU2GVZWVmg2m35j4O233+bo6MgHHwVB4NOxAba3t3ny5Anf/OY3PcAmbSTM6fl8zq1bt7h27Ro//dM/fY4pKiCX2CocHx+TZRm1Ws1LxS9fvswP/dAPcfv2bc/Mq66l0pZVJqn0jYzjNE098CVruIwTmddVkPaivFWO0Wjk1waRVsvc3t7e9tcnoSJvvfUWm5ubHB4ekuc5N2/e9ICj9NODBw948OABe3t7RFHE2tqaZ9cJ21HGfa1W8wzDZrPpg2EWiwWnp6ecnJxQq9W4evXquQ0sYctXfT4FlKqCsDJO5fkkzDlZs2S+ic+qMM+rVgPVZ51s2CilPAh7dnbmmXXVeZjnOd1u1wPmDx8+9Out2A80m03W1tYYjUb0+/1zwTDVzZmnT596JvZoNKLX63nwV0KOZHNQnqWA90ycTqfel1jsQ6rXKs9AkTiLdYOsEfP53M/lqq9olb14enrqQeVWq0Wj0fBj3lpLq9U691kX5fb/bcf3BoBoAAXhxFCkS4aRKvDgoBRoJtbOO60MUjHxEmBx57GV8y49BZW1DvCzykumdbVYAl8wiVzKsSLx4S1GK1AOUJQQhSJRXjrnWGwKjcUoVUqey+uKHCtP584rS1lXxNX1HFsvmGwFTj4bu6RPUwYxFHX3fZE4dkZet8xWApfSuaKYbVimRwHzrsIGAdMtSzwMmK4rilrIeNc6Ce68IB465qGNQZeG9JMrBRt6ToELVAgWlqQH09CStVyya9EuiE5Cjl5V3Pz4Q9bTMd/cf4nZuuInr71LZg3Pto74YucaqKRkS5YA4syW7CTrA1rmK5qf3na71aeLOoU1FGXf3AjdBDrOmzSSBbqidXQgYemDUQaoGONkl2J4Xzu1DK4rhibmcW+F/+jxz/P6N5+h8Sige2bB2hJMcv54WVOVrBcnCZcUz8aek6qNt0vGXelxefhnF6x9PiYa5ajRBDuegFaEl3Yxa21UVoBSmFqEygyNg4JwZkj3XRjKdKfBeDsoJcoGUytA/OjySmFuAeM86WyhHEgoLESjUFPNxlccw3Cx62ieKjTMn59iC40dJfTfaZKeKOqzUvpUsn5U4QBUHRhU5ga/iSzRSC0ZPrGiaCVgLME0QxUptX1FXi/niBLfO8VihTJMyEIUkq81XbDFQkPoPEajiQPhqyxf1PIzVBq6dos0NtIOZCnnsMoKdGbQDTcx0+OMKHHAkfgdmmgpY7Wq7N/ceHYyUG4GnE+QdXNZV5KPy7meBIR9i36rCS86AKewmmtrZzwOW+jCATgKWaNgvl7w0dZjDrIVDrI291lnVCQYq9mbtjl+2CHdmLJenzBYJEznMeOpIluEmGmIWmhU5kBdvXCgj8hZxQbAJJbi8ow4yclzTRBYGrUFizxAW+h8+4zZpRZnL0aotEBHpSTBgNEuMMZqN+ZnXUXeUPxvfvr/w4eTJzwbWWa2oG8sJybhQd6mo6cUKPaLgP/y5Ed4OlvhaNZkr9emeS8s03Epx5eT0urIlhtAiqzu/F0XKyFhVAJ+5QaMVaACi1aWiUm8zNuBOe66ByYlrkRsz2xIpAps4dhs8chiAlv64rkNDxM5dNdGAVmrBJNLVlrfLPhH/R/mH3ztEySPY+dvV86BG3/xiFeTbxMFDni1OBkys8BJokdOvh3MVBkM5J5B48v4pGm9gOZeQVZ3AGnWUCW47lKFhe1nK0zMOM7RUqQoGfc4Nl2mIHNg8swW5CagUZtTWxlxpdVjOx3wGyd/hsWKwkSBe65lbtMsHEA4MwwD8RdVzLou5VgXTjasM1tuKFn6N8KlNUHpMxrkkLUse0WL3XBEQsjIzHiUG95c7PDl4TN88+wy9/bW+fC1J/zH13+Vr8+ucGt6mWGeMi9CDmYtnvRX0AsXrJWeGX+P0hZWu9CXvA5ZTROPDPNJQJ4owpklGoMuSg/UsNwgrKtzG4iqUB7YXwmnnGSNcuOhnNuRZrylyeuWcOL6IglKppcS2ZthO+nz/miDUBvyQlPULLXjZUDNvKaxXec3/H0PxO+9oyqNlcIxz/NzLBNYMlPkj/2LYJ58rfo1VV93sQCVc1XZiAIqSPEqx0VJ88Vrrp5XCgt5rUj8qlJmwIN0p6en3ih+Op2ys7PD8fExtVrNS8Fu3LjB3t4eeZ57AGg6nXqg69KlSxweHnogpCqdFLmaAJvyf0ko3tjY4HOf+xywlFOKBEzYfqPRiEajwSc/+Unm8znPPvssDx8+ZHd3l8ePH7O3t0e/36fVarG9vY0xxidCC4hS7a8PfehD7O/v++JOwNT79+97cEqYIFX/wioLsSqdDYKA1dVVnn/+ed+Pp6enbGxskCQJ3W6Xr3/967zxxhscHBx4qWPVx038tYRlJcCzgC4Ar732Gt1ul6IofCEt97W5uemlbtL2x8fHvlj+9re/7RmbL7zwAlmWcXR0xL179+j3+5yenn5gXFfBSMCzZlZWVrhx44aXrn/pS19CKcVsNuP+/fte+rqzs8PJyQkPHz7k0aNHXn4ujB1pa2FQCeAmnnFyCNAl/SjzQYp9kXbnuUslj+OYwWBAlmWkaernk4AZAmoJ6CGfXWXeyTogTCdJWhawTcamvFY2Carg03g8PueXKnNTPAQFjKn6qMm8NcZw9+5dtNZcu3aN/f19z8pdX1/3rFu5XgGODg8PWV9f5+rVqx58jaKI3d1dRqMRzz//PMYYtra2/NgSL8PZbEatVvv/svffQbJl+X0f+DnnmvSZ5V/V86Zf+5nuMT2mOZjBAIQlHEUCoFYkRGqhDcXGSqSk0K4UoVitQmLEEpKoXdGIopbUYilBhETuUuBgDAEMgJnBmMa0mfbdr593Va9cZqW7mffec/aPc38nT70ZkdCfbM6NeFH1qrJu3nvczd/3fI2XxXY6HR8MIvctAJPcswBnxri09S996UucO3fuGNgk41zYn5PJxPfzY489xt7eHt/85jdZWlryTCsBL69fv87LL7/M448/zs/+7M96dulbb73Fa6+9Rr/fP8ZElbYVBrCMqYfl9zJvZVyFG0chAPlwv8j7yLlbrRZpmjKbzRiPx3S7XT8GpO0FUJLxePHiRc6cOcOpU6e4ceMGN27coNvtcvr0aX+tb7zxBgcHBx6Qkg2sELgTgDVs6yzLuHv3rge6BByTedZsNr/reRGCfKFdhGzmCGtZXvvYY4+xvLzsk+PzPOett97ya40wuYXRKm0tz1KR48rYEJakzGF5boUgsIR9nThxgg996EOcPn2ac+fOcfr0aQ/ov/fee/zar/0a0+mUzc1NWq2WnyP7+/sMBgM/72R8CTAoXr9x7FKZt7e3fRCSrDGNRsMHyojsO1yLwo0BuWeRQIcbTfL5Qp7/4XorzxeZAyHzdTAY+E2LZrN5zDPzj3K8LwBEl5oMOg/85mYWnRvnl5Yql75aBR1IcK+NnESrTLVnKNkIiCuG3ZxFsql45skHuwoQVAEIAI4tZaKKTRgtfBB1YX1ru3RPgiRO7eWeLnV4EeJgK4msqpXEk8SlJK+4ey6a0FIFaWcOOiU9sjR2NMnEkB45EC4eKi8VzdsO3Mg7TnJbNJ2sOVtx9zTvOgbevKN8sIXOK4BnWHkd9JxklYrJ0d4a0dKOVbSTdapUWYWdaWYXM9LrdWr3Y3h8xL/x1Ff5Zv8if/i7T7B63bD3IcUvrX2NibV8qvsuv999jtlyxPjDUzrfapAOLcNTEZ27pQNxC9fY03XNv7z0AuA82wyWHAcKNrX7kD4qatTjwoeoGCzzIqaWuMlXFBHWKpr1GeNpjfxOi9YdTeNBRv+ROv/Pez9C+UaXN1/q0esvUoCjWeX9WBOGjiv6BaRpHJiKTeZYJ9HMMe7mPVfcPnZum72vn3UeblGEajUxh32KJ89TNGPiceH6qJuS7k9p3Z5w+GQbPasz78Vsf1JhGgYrqcylchLlSrK60O/jfeJUZLGF8zeMMkV9X1HftzQOCgZlDLkmOoro3NCMzjggsL5XsRLXYbYMzXuVv2PkmII2dp6GVBJcAc3L2gL4jW/sQL1Gudyh7BYMnlI0b8Y+MIiSIHFYkXdSbNRFZ4FvY6kqCXwAGkmRVZiKBawp6zG28iNVc+POIZ6l1kIFfLlE2EWIBUAyKivGIgtfymoc+++PvX81T2NVsZmMn9OSKJ23Y/SsTu+qCyBZS4YYq7nc3eVm4wz6SC3OE1UgRd3wG/c+yN6oxXwWk2fVh8/YoHZqrL+uGJ/scFt1WLpq6CkYntXYrgu3MY0SvVRQa85IopLNzpCL7X3O1A9Yjsdsxn1yG/Mfv/knGA/dh7i8nzDLm9jE0rZgY03ejihbLmynnETomSaaKmzbeGsDcPdfNOBUcsg/GHyU/+X6Bxjf7aAzhZ4r0iPF9Okp3/jMX+fXjp7iK3/944BjiOlTmmzNjTXP+rMQT6u04I0YXbqNFGE920gRT0ryTrQAEY1iVNboxRPneWg1kbJEGBJVkpGglWFo6t4HsV82saVjs4kVIBWg7dKQF+uwrNFYByDmwJXRBu23UmqHlsGjCpO6eXcy6WOsAzTFFsBJy2HpbUW979YGZxHgngPTZU1ZF1a7e6vpiq5AL8d4F7uN/uPOv5Yqldr1BaRxwcwa6ipHFQ6ktMqx1tVKwUcu3OKHWm+TKM3/ZetL5JuaFV2yEtXIbMFv8BHqe5ba0IXWWO3k+0UDZl3HENVTjalZGvuOxZ83XBCZaQfPMoMHE12bOjZ3+za8lp3h25OEv/vmJzE3WjTvKbq3XDBJsVpjq6l560+d4OwjzuvrH7zxIcw8Qqel72el3fN13nFewZ7JXwW/YN0GWVmDcq6YL0Fz17H7y7bzK174qjrms8iY/Vopz/GqM2yV+q1zi00iphvKv8iqhazdWE1hIi4sHbAeu0IjwmCtIpoqJhu66nu3WZJMDWMdMf8+A/F9d4Qf4uUDvhSAwLECARZSo5C9I9+HkuYwHTYEAOW9pIiFBdAXelHJewvwEAKcD58vfG9hOMprpPiYTCbeb0rYhY1Gwxczm5ubDAYDms2mlxwPh0Of3CwegVLMZlnGwcGB95qT+xQfuFarxfb2tpcBy/ucPn2a+/fve6lYvV7n9u3bHlATqaRIvk6dOuUBxZs3b/LEE094oOyRRx7hK1/5Cru7uxweHtLpdPjZn/1Z+v0+3/jGNzg4OGBlZYX79+/74IBWq8Wjjz7KG2+84SV0gC9e5ZA+DlNjBVQMAYKNjQ0eeeQR1tfXvQfcu+++y/r6uk8E3d3dZW9vz8uVhZkphbuEIQgwGQIeZ8+e5eTJk9TrdY6Ojjg4OOD8+fPcvHnz2LXX63V2d3e9L5aAXgLAdjodTp8+TRzHvPrqq95zUIrhEDiQIwSmVlZW6PV6Hrw4PDwkjmOf8Lq1tcXJkyeZTqfcvn3b97EATsLiEUl2KGuUsRAC6hJmIv0qY3htbc2DdRIaJOMllJ4LaC6gUeiJF85rKcAFOBUwTMCyMKBFrjeckzI2pPgPgZZwU0CA6nCOh954ItOUvxePyMPDQ5588kleffVVjDEMBgM2NzePrSXgrAT6/T67u7sA9Pt9z3ATGe/R0RFLS0sYYzyj6+LFi3S7Xay1vq8kYEdAD2utB4parRbz+ZyVlRW/CaGU8qnuwuwtioLHHnvMe7iJJL7X6/k2ECDnjTfeoN1u8/GPf9z7sEq4xgc/+EHm8zmf+MQnmEwm/P7v/76X1t++fZssy46l/MIiMV2A6CiKvBVBuDEjoJvMdQG4Hl6f5fuwv8UP7+joyDMBQ7BYxgjgfy99sba2xksvvcTrr7/OfD5nb2+Pj3/8435zQEA0AY9WVlY8c02S7YVl12q1/Dou7S7ApsiExeNV7lXuSUC+MKSm0Wjw/PPPc+rUKe9JGscxL7/8smdjr62tcefOHfb29vjoRz/K5cuXee211zyTWJ5BWZb5zSkJKEqSxI83GS/9fp92u+0DkELANo5jZrMZJ0+e5Id/+If51Kc+5TchptMpr732Gn/4h3+I1pp/7V/71+j1epw7d47d3V3efPNNfw31et0D4qH0XDwFZT2p1WoeCFVKMZlMPMg+HA5JksSzccMNFwE6BfSUQ9aU2WxGt9s9dj2yTsizROaVAPOyfsnY7fV6x9jCsq78C8VAFJDPJJp0UGA1ZKsJUb4IOBHvrCi3iG+dFGBQpSobWzFBHJhQNFzxajTeg88xlFTleRbIIuuug3UB2rrCzyqFnllMDS95lcKPSiYtxYz8TuTBYTKkiRVKObBmeFqTjNx1Fk1LUt3AxoszbKRIphHR1FDrl5hUsfK2+317u/JVfA+SYUnRiigamsZexYhULrE5PQKUK66mG4rGDgwvWGoHzlR//uSE2utNJ2Ur4EObdxgby5JWGOtAy6IByWFM/d2Ezm3DvR8yfPzUXf7G536CpbehZ2H7Byz//g//BrmNyKyirnKGl0v+3R/6PC8dneONLzzNvKs4eryk1tc0DooKcFEMzxueShvMrAMEJnbOjaJNWS4e5v28yVpjdIyBOMsSDyBqbZkc1TDvtulcd6miujCMt1L0HN75h4/RnDqGTVl33pLRzLE/JfkzyvA+jfVDQzRz8ssydf027yjO/JkbbA87fPbkNf7xd57hynfOsGyhaMSkcQRKoeo1inZC0dDH+hyliG/v0u6mjE6nFHUo25W/IVQoN4uqV1iG8r2F2o0a8QjqBw6QQFnyJoxOKfY+rEkG0LyeoHNYfT2jdz1i94MJZU0xvFSCUSRDRTq01A9LpmsxGAf8zecxtRJMzY3paAa1gUv8VqWFOMYmMaYeQ2SpdTPKB230XFF2ShhGoCt56NRS2x7Czh6sLGGjnpNcFwv2qZufwgDUmHqMnuborEBPFGo2R40mUK9h66nvdxtFWBeejdGKoq0XKm4NVkW+OcXHT4AbcPPTSR+tT39WFtS8YucphS4NeTumDIKWbKLpXpvyrf3z/PzJFxmVdbrxlLxjSQcLKSlUEsxJxJ3dZcq5JkoNUc2Jb5WylFGVjDt0wEhRUwzPKf7Kn/t/8+HaA5a0W8pza9BKYaoH5r1S0VQlBhjbmHtFj1G/ydK3Uj/WVAH7n8zd/Io1yVHB6kspVml0xQQ+uqjIU3ssJMoq5+PXUnNGZQ2+scTmLUNRcwBXMjGML8QkKEZl3Vk1NJULc1FQO1iAsnKUNTcHhHksvnVxZiia2oXeVBLeer9E3a8zfyymrgq0ciDa3EYkqnDhLzYhtSUJpUuUxjqPxFxT33OS6TJ1jMq86eZ1WTdUuJBnR1J5ICbAUjqlaEKcKUzivB0x0NIzMmvIy2jB9I0tNnH3nc81eXPBUlWm8k/NQRXCRofGviFvqYo5rvwGVDwhCFFZANmd2pys6u+f/NgrPPaD2/xS723qqpJlEfGgLPjS5BS/0B4QHjERFIrZsiJvRV7Cq8qKDVpYji5EbhzMFdnKQqoczdyaooz7/uDx2G/ShR1rNfxI6y3+/Ov/Ksv/uEn9sHChSXtTKC15u0HeUKRpQaQqADguMbMIWyrSRk4+j9FzVaVmG2pHi+emHJO1iLJhUEaTLStM4jbD9BxqR259FwkzCvafWvixPjwO15Ihd2bL2MR5BdtIYWPFfNkQjxYvlGCduY2Ym4gnu/fpl0266ZSN9IhOfUamoXPbSdfzVoSJYLoSUTS/Dx6+X49QCihARGg0Lyyx0E9LGDghW0UKvofZgt9L/hYClMJcDCWxIZsxZD3CgiEVgoRyzoeZkVIY7e7u+nCPbrdLvV73YQ6rq6torbl3754vOIWRJpJPYYhIGImAPCLlEomxFKkinV5fX/dARrfb5eDggLW1NS+rnM1mPHjwwIMigAcvRTr54Q9/mN/7vd/jqaeeYjAYsL29zZ/+03+at99+mxdffJGiKLh27RpPPPEEWZZx/vx5rly54hk6y8vL7OzsUJYl586dY319nbfeeovJZOIBJ2EhCitR2l0YiFIYC2Ps/Pnz3gNuf3+f73znO74Q3tnZodPp+FRQ6euQOSqee9IfIvUWMO3kyZN85jOfYXNzk2vXrtHtdqnValy/fp1r1655oFHaS2t9TPqplPJF7vnz57l69Spra2vcv3/f/72MkfCQEJeyLNna2uL8+fMe3MuyjP39fU6ePMmjjz5Kv9/n5ZdfRmvNYDDw4Pv9+/c942p/f9+n3d64ccODC+LHFrL0BGATMFkKcpEYDwYDer0eBwcHfh7IvYrcXvpb2DshWyt8vdaaRqNxDLwT9qIATyIVHo/Hfr6F7EiRc8sYkvOEPnwhUCj/ZJ6GYGPIlhVPtjRNuXLlCs888wxf/vKX2d7eZnd3l0ceeeQYcCDn2tnZ8SnZo9HIX4uAJtvb28fYX3/hL/wFrLVcu3YNcBLt4XDoZfFXrlxhOp3yyCOPsLW15QGYCxcuMJvN2N/f586dO17OLmDj3t6eD/m4efMmo9HIs+tC8FBkuGVZcvbsWd555x3+1t/6Wx7Y7/V6/Mqv/AonT57kxIkTvPbaa7zwwgtYa30YkYwDOWQOCNApsmVpW5HjGmPY398nTVNvuwDH/WjDDZ5wvRZwVZJxhaknmyvSd+HGT2h/MZ/PGQwGXlosQJlsnsjzIlyHjDFeSi8Aq1KK8+fPA3h2Y1EUHB0d+fVZZN/y3nI+WSfEOqAoCm7cuMFzzz3H5cuXvYflnTt3uHnzpg9RunbtGi+++KJvi2effdavPeLRJ/JdSYHPssz3RZ7nHB4e+k0PkWDLsyMMq5J+aLfbnDp1ik6nw6/+6q/y9ttvezalzFfxIhQW8mQyYXt723vvCrAq8utms+nZyEdHR9RqNU6dOuU3A+W5JkCiAO8yR4qiYHNz0/tUhs/98Pkc9uOJEyf8ZwbpA3nWC5grYUxnz571/sHWWm7duuV9ggXEFLuCP+rxvgAQVSVLIoFsJXHFz9ixpFSVFCzAnEhgVWmJZwsvO5NU8rDqb0wiPmgWjaqCQ6yXLyuq11FJL5UEqgDKsQrjaRXAUklyxe+wrCnyuiKZ2Ir1FMhCS3uskFG28owzFROr8o8Tn6u6sqx0x4w318lbimxVEc0jageW2Yqic9sw3tTEY8t8SbH0XsHgUkr90JAtKzq3C7LliGTqrqu5W5A3NcnYgIpp35nTf0aTFy5AI3m7SZRRedkpLjX3MIBWitG8xuETms5zu8xHDaJbbfafVvznP/T3uTZf54Wt80z6dTZ/+A6/cuFzrEQTIiyZjViKJvy1H/9Vnqvt859/9cc5NbOoAay+qIkzU/WJY2p+5uNv+PYZFykHZclr2RnajUVicm41JxsDkgocMhiKo5TW6oDcGubvdNl43RLNDSjFrKOIqrAXXULat44Zp6E2cH6UedMx2HThvMeUscQZpENDWVNMNiIP/E5/4ogfPvcuL+6dYf4Hq3zmf/8Fvth8ks4LiSu4sxKbJqi8QC0vucIyBmUid95JiZ7mlAeH1LaXGJ1KXTK1cuCJQ+wq2pYCFRlsFpE+iJmvlR5MbOw4+V62ohhchnxzTnd5AkWEPaqjDh1oYBJ8irAyTu4ej1z6cDpw71vWNSaCvOOWjXKQujFZJeXayAFbwni1szkqTZxUODEY45KOUdC+6sCGeFR53+U4gK7XxSonucfgQJiKsegkxxq0sxPQkxI1zlClgSRGFSXWGFQ2QxnjAmqUwq503NeKPZeMgzmmKsZsBQR7CwJbbTJosHrBEBa7BO+vSvW9scc2GMABi1FuePetk5itlyhRnKodYlfncLvuzwOQt939moMUmiVaG4zRGAtmHhPn7sU2hrLm2MfZqZzVaMT/MnqC+/MldudtdqZdBvM6g2md//YDf4++afBvv/YLjG51XfhSobDdknm3uje578L52ZXNtEqMh9nKItHWxBY1rxK0K1m31W7cNHXOcjyhaFQgXPhUSQ01FZPbyIc8lYlb08qaS8KNpotNEze3nLdituyAOOdH68KeTFyx0axbixu7ilGRkqiCCIvkrueVVLmucrQypOAZiHlFlx2dVm5zoO6sH2xsHBPNOOY1BECdhqaeM7SKg3mzAr4rT90quKajp5RAbrS3C7DWAW/xxBJPTZX8az1jOVuRNcd6sNI9n9ymRjIBZRzDdfAY1PcUlA7UEiCvFhW8Nl/jTNznr5/6VtXwDcJjK25/F3gIcGimKOPCumpHBhu5Pjex66u8HYC8EdT6xgOfZeJe4zfn5hUQXghD1UJWrScouvWMB5u6kmMbKC1lK6FMFfHMMp8vBk4UWfJCsbQ+Jo1LDo6azg4jrfxL64tNQWerUDFYK6a8SRRF01A/dAxEH44UPFujDM9ANXHF4q6e9XU1p7TKM45dX6WYVgnD2FsCpJWPyWY84Cc23uAHmu9SoviB5rs8nSpeG55mly1GJ2Pvl+zsLlzAFgnfP96Hh4AIAjwcHR35kIGHGUcCdoSJnyFjMQQJQinYw+wned+QsRLKA0PgAfCFcMiuEjYCLJiUUpBI4SLXIT5K4/HYF2xyTXLudrvtwYAbN2547ykpWIBjfl71et2HXoQeZyELrNPpeE8v8U6czWa0Wi3OnTvnUzRXV1d57733OH/+PK1Wi62tLW7dusXTTz/NZDLh5MmT3mz/8uXL/M7v/I5nI1lreeGFFzzT5V/5V/4V7ty548NWROZYliVPP/008/mcd955h7W1Nc8GlIJ0OBweY5MIgCgMuiRJaLVanD17lps3b/KHf/iHnn0lHnGHh4feV1KYSSK3FXA6DOKQwlpYRX/iT/wJHn30UW7cuOFDNlZWVtja2uJb3/qWZyxKwSwgTMhMlT6Q+5CCUySPoW+XgGmnTp1ifX2dvb09rl696uXk0+mUlZUVzpw5Q7/fpyxdsvP9+/fRWnN4eHgMLBA202Qy8YV4mLALeAbkcDj04zRk08oRsigFXJtOp5w/f96fdz6f+38i8QsB1lDWL3NFGIPCrpNgEdlAkA0Dkd+GYL0w3EJ2sVy7AECyPoRBLOGGBHBs3ZBNipChDPDqq6/y/PPPs7Kywq1bt46xtYThK+9x//59Dg4O6HQ6nDhxwocvhMEuMtYF6PnSl76EUoqPfvSjLC0tHZOb/uZv/iZ7e3v89E//tPdwu3PnDi+//LIHI+W+pA3jOGZnZ8dLRmUMid1ByOqU6wnXLxk/0vbhZsh4PPbrsoRlSDuGYKoAOpI6L+CRtK+AN1mWMRgMOHHiBO++++53yUlDGXO4ASRgdhzHnD592gO2+/v7x8BsYQQCno0r7R8yWIWlJuNVxlD4fMjznH6/fwwEk7kvgUEClklbTadTL7sVT0IBqqX9pY2ElfqFL3zBexLKs0XAUmHShRJdWYfiOPagsQSPiPWEbGTIfG61WsxmM7/2SzK5zO9wU0yuWZ5po9HIA2+z2cxvagmwFq55SZJ4NqLM5zzP/fu2Wi0fxiVgojABpZ2FASmAbb1e95tloX9kyDiV65B7kOdvq9Vid3fXrx9hqM5oNGJtbY1+v++ZmeLtuLu7i9bahzTJHHl48+efdbwvAETHenDSptmSon23RBlLkWjkM5atPA+hKl7AgwAmVhjtih/PMCodqCFhLOL1JOdxASiq8gPTCzahxYMCJlkYvXt5o16wHk1cAYYaLx3TBR7oEEDCyYVdIRaPKqmdsd4H60RzxH29wfSEY1vMliz1fRxjLYG8BapUZKuW2QPnb4jVjE8q0mHE6IwmOYLxaYeeTDYVjQea4QXApkTtjOhu4gCfTEzrLaPTmmeat+hoxV/d/yh37q/wb//8b3Iu3eU/eeenmNfazLYKHk93+HDtHh94/g7jT9RYjUZoZUgwrEaWsbEkesIfqxnezRVr34opE8f+q80EVLCowpKtJfwHW18E3E5eK54zsRFfPbzMZnsRi1mYiHa0ABQB9ESzVJ+SWwe6pSNDthx56aAyTgqnZ85DMsqc3HLedoV0NLPoqQPMormlfmA8IF2mjnU33VD80M+8yIfaN/m//8afpHsNogZsxn0ubOyzXzYrMFlV6EIlY2pUQSHKgZrKWNQkQ6Uppp3S3Cs4upi4cVCohYRZWZhFMItp7mg2Xpxz+0diylXnw9b/xAysYm39iKY27B52mL26RPM+sObAE6sV8bRiQdXVsTGa9hVRVgWFiPfZIKfxIEaZeCH3rJhHecsV6To3qFaDcrVDtlEDcozR1CYujEE9e8TkQYvVlyLKhmPxYpwcmTjyabiqcGxfAfhVYYmmJdFk7oCmooSy+qodmxMVtG0SOz/E2PW5VYp4VhAP5xUQVoVoRIoyqQaCVpQ17ee6ngd0bo0HCCFgDRtLUVcuyV3GUyWx7lyN2Pt0h6Sij53b2mf/lVM+aEKAONMwJIMIfaiJMgfOxhMYn7IBqCnrmULVDCWaX7v1HNN/dII4w4/lCNh+ssvlZJ/s7SXW33TrgI0Ug8cU9QPrE4/RMK2XDjnV7rqjuQPRqaS8ededt6xDkSm02EBoqKsSw2KTpKxVTOQa1FruA8P+vE2ZuOvXuUuVj0fHx5pv00qamkwMeasChKqNHaud7Diaw2RNM90w3Dha5YeW3yZRhQ9MyWxCU81YiiYMqyAO+flR6eTb062F9ETnimikiWYKU4UNebZlxVqr6YIES2E03kuvbip/U0tHZ0ysoigjSIzz8LQKoorNbhw7WUAsCfGQZcqB+A44nLcqcC4Bq7Tz5Ayk3j4kRUGvNuUbo8s8ufwN/lnHzOYclDNKIFWKl2YrqEItAmWUW99FaqtHlslG5H0bp2sOAFRlFUQytdU9GA4vJ+6ZJP0ZsHxbqsBY5QDwRKELN+dmqynztiIdLa6xrnLHuLeKD23c5f60i7GKw6KFLixRbknH7hmt59UY1W4Dp+g4BiJANHWejaqo2LuTCqSuJOSTzfgYoOg6wfX3qeTQsfsjd3+6sEzXHF06miviaVVU65JIaZ6t9QF4e75Jv3SsrK9OEr69fQY9h9ZO6RULNnJj+Bhb8/vH++YIC1QpHsIjBOOk4BdwImQOhnJlAaCkoAjBxfAcgJeehZLHh5kLUgyH55drD6XNUniHhYUUmMLuEjaVsMrE+0xYYfP5nFar5WVWBwcHHgSaz+dsbGywvb1NnuecO3fOs6cAX/RI0b62tnbMR1B8pSQg5eLFiz5Jtdfr8fLLL9NoNLy/1WAwYHl5md/5nd/hzTff9HLW73znOzx48OAYKHL37l2GwyF/6S/9Jc/SE3ajtEGapjz22GPcvXuXW7du+SCJer3u/cuuX79+DBSSIA5pE5GTaa158OCBD4wBfGBGCFAJq0bSM8EBC2FoiPTdU089xY/92I9x584dZrOZTxz95Cc/6c+zsrLCzZs3vZl+CLYI6CG+azK2BVRK05TBYOAlmwLcSqhGp9NhMBhw7tw57t+/z2g0Yjwe02q1PINRmKJ37971xe7GxoZnXAkLUa4LHFgoUncBLw8ODtjb2/OyeQE1hNUnzMNer+dZVTIHt7a2/JiR1FrxMpO/E9aRSOhDAF7mmQB9oXRVfCfDe5C5GNoXhBsE8prQLxWOy2VlvocgVchEDsFVGSNlWfqAoTNnznDlyhUf1LK1teXZrTI2BoMBly5d8lJQmbNRFPm5LT+XoJR6vc7m5ib37t3jO9/5DkdHR4xGIz7zmc/wzDPP8M4779DpdPiVX/kVlFKcOnXKg0vj8RhrrZcQh5ssSinPhGs0GnS7XQ88Hx4efhdYLHNAgHcBhARsE/BIXhsCgeHaKkzDPM9pNpvH1jwBbeR78ZIUJrWARCEjPVy/w745ODjwMn2Zb+IJKP/k2QB4EC0cG8KOFVBa+icER8OxKjJ02cSQOSegnaw3tVrNg2UiVQ/BxTAEanNz059jbW2NF1988dimVPhskXaT+5LNEVkHJExGvAUFyC3Lkmaz6YE0YfvKxkko6w3vWb6XNpNNiXB+tNttvzY8DACL5YIAyBJCI+FUkmAtoGboLxmuBTJ2RcYsjEQJrQo3b8K1WNYJOaewCzc3N3n00Uf50Ic+5AOorLWsrKwcS7rO89w/X+bzOUdHRx70FLuQ/y3H+wNAVBAVlvpBSTqsdsHqldeAFKiBdCnOXLHvgCk5Cd7LySQLjymrVRXgUPkYKldnmypJUopmXTh5dJmIMbv1PmlFrUpprcBFlwZpvRRPrjOeuutyTEWq8zvwwJauKKdiDVmtMCdmNJXifHuf+1wkb1naB86zUJUO7LGR8zNUxp2naFZFft1JvPKWA0hN6uSIRcO9f95xTJn+o1BvzGFWMVqqAiueWUZPzHkm3Saz8Au9bzP5YMr/4+Ufov3tBq37hnwDTp7Zp6lKMqu5mBxwUNZ5UHZo6RmpMswttLQisYaZLfj3bvwp2ndzstW4AteqyZsbTKQ4OqtZWWy8k5uIEsW7++v89LnXXd9bw9xE1PTCf2Zicky75PrBCj87+gvUdxWTtUpCW0ASBIXUD11/mxQmvUpSOXX9WdYVjX3nZTbvaF8Expll/JND/v2nv8h/9PWf45vf+DCdCuDVBbRUzg9tvMP/bM84j8688u+rwJK8VT3050CqyZsxcaeBSjaYLdco6pq86wpaCoWaa8ihfj+ivm+Jp67/Dp5IHSO20JArGifGZJOU/qtrNO8plqYOsJ5uKLI1QzpQdG8Y7zFYJk7O6RifAmI7FmYyMUzqMTrLydaazJcstf0KOKoSYXVhKVqKoh1RbPS8f1iclpSFAx8KBdPtNrW9CFVY4op5RhwBEWUrdb5wuYbSeYF6SX8oWywrilccgTEgcp5Iu1TmirmFBptUnaFAVW2vs2IBNmqNalbSlFrErFcFX+SQjArHfrQWE+nvSlI3ifbBTeKbqIpK7qsU3ZslN6erXGjsMSrrPLW0ze8lp9xmQcXkQwOxpb7rGHll3a0zZd2BbXq+SMFWZbVLmhhaak49Lpgq9zsb4UG/JT2Rpc0FhMi6lhqma5ED86oEW5s5OaeJNMTOD3XeWTAUHZBayWlLi86Pr8HiGWceYlQ5WapCK0NRBYJQycTzniUeOz9WWwGzjs0Neu4sAYp66th2xjG0o7n1fpUmVpSrc+7t9xiebrAUTeiqjLFN0Ri2ix6pKmmqGShDZhMiDHWdozNN67auwkxcIJIDDC0HT8pzQDsWe+Vx2oxm1APmqj+qZSrFUFpFpGWDSoFxzE0X2BIgpdWzaLQVOcCy2hDSc5i3qjE/deNdV0nfRduii4WXpypce//1c7/BRtQC2sxsTmbFl8+S4zZoTsY13qqA8KZW5FZTonh7toWyzoIhmVgHcGmqFGZFUdfEU0h33OZC+15ZBXlpx/KrEtJtrBd9XwH1PmlcKaqMIs96FRuPyXqEqYHJII7dB/ehqWOtgsRwunHIjdEK9bjAJtanH2dL7v0kuMhtNljiofbPZJtA80FJWXPA7bylnCIgcQFm8dSlUCuzAMqVXfTtrKLSmsQ9PycbikcvbHPiqSOe7dzhyfpdPl47ZGQ0//XBc/yDa8/yxPoOR/M6H1i6RzuakRcRZcMyWY+8/YVbVxZ2AN8/3l9HyAgSBoL4+D3sZSXFpbBgQukwLNhcIfMgBCBCtoJIrkLpsTDJpAgShknISnoYdBDgUwq6h43dQ2mlFOICSKZpysHBgZe/LS0tHZMkC2AlgSoifRZWkUjgxODdWkuz2WRvbw9jXJCIyKElHEEK3LIsOXHiBJ/73OeYTqdeQvjEE0+wtrbGvXv3fKF0/fp17t+/75ONpW2l0JQ2O3HiBE8++SR/+2//7WNsTMD/fnNzk9dee82nWkZRxNLSEoPBgPX1dSaTiS+wJYCm1+v5ROZ+v89TTz3F17/+dba3tz2DDvDgofjoCQAh5v1huwsTUECcxx57jB/90R+l3++zsbHBO++8w+/+7u8yHA75zGc+48GYS5cucfPmTQ9oAZ4dFEouBRyW4l9AJa01nU7Hy8N7vR6z2YzpdMr9+/dpNBrEccxTTz3l2YWTyYTHH3+cLMt47733vLekyNSttezs7Hg/sXDMhWxa+ZkE7wwGA0ajkR9TMsdkPB8dHWGM8fLPEGQJPdSEiSlSe0m/Dv1EBdQKx0wI/j/MCpbgibCdBdyMosjLGEOgWM7V6/U8qCly1zBMIbwWOb+wU0MArixdmvj9+/f5wAc+wLe+9S329/e9jPy1117zYJeAUEVR8Oqrr/p2SNPUs6xC1rT0TRRFdDodXnjhBV544QV/vo9//OM0m00PaOzt7QF4FpSMN7E0kHEn7S1AkoRqhLYOsu6FTG3ZvAnX2tBnNs9zjo6OvLRZmFvhuYQ5GSYJi02DXKP0o6S7v/fee5w7d47l5eVj0vjvxRINJetra2ucPn3ag2LCIjPG0Ol0/GvlfmRtDudoyJwM/V9l3ZcNGLGeaLVa1Go15vM5w+GQKIrY2NgA8NYLcj3CapTzhsFPIVj7gQ98AKVcANLOzo4H02QMynNDpMECGgrgppRif3/fhwodHR159p4AbKPRyD8XJOBIxoiEaYmEOzy+V0hIuDEhbEjp/5CVl+c5S0tL9Pt9H1Ala4wwF8UTWBiGo9HIny9k4koQWJqmrK+vAxzzSJVxEnpeyrgW78der+clzydOnOCJJ56g0+nw5S9/2a+j29vb7Ozs+HkxGo1YXV1lPB7TbDY9s1S8GENVwx/leH8AiJFybKTAs8zUlAtVKKyXOoWsIjHlV8Z6ZqLImRdpymoB+iXKv76szNh1bp03V8NJO0M7OsAzFqO5Y01YrSqGh/VeawjLkAV7KB2bCrhxXyWRNsqtZzoCtDoZidKcqh0SzywbL1rizFLra+r9kvqhIp6W1I6cFLB+qKkdFiSjuPKtUjR3S/Q8quTdmtqR8dfdeKDoP1WQj2p0Mgc+6hz03LE/PnDpDktas2ssF+KIRJWYSUwyttz/4ZJ/+bmv83NLL/r2KK3iyNRZiiYs6cy7E94uEjo6Z0vFvPnaWc6kItN1IKsuLCo35Ms1Rk/OaapqlwXtgg2s5nC/zROPOUPWgpKsTGhGM67nI/7B8Bm+uP0UehLB15cwfUuzYqHmTeWUa9Yxp6Aq1lvKsd1m1rNQHYPFgYtFHdKRZXLCsXZG5wy9pOCv/OovsLZjyduuSHXekqCV5ZOtK/x6+sepHRl0XjrJbZUa7YrwyldRW6JUYdO4SmWGvKUxNSdNjo807dtufCUjw7yjmGwtmFnLb0I0j4jmlsNHe7BuWH4TxqdgeKny+awZB/oVsStsY8jWUvK2Ip44QFkZBw4KaONl/gbmy5byVIbabVC0LaZuHOBSSfxr+znxrQfQbKDW6ihtsaV2rLqpwg50da+LOZwvN4hHc+dvmAKxgVz5eU0lJVSlwVGLq18UpWMayrwrDRQldjaDZh37kJG3Km3FGou8ZFVCVhxQ5b6PclPN89LJVK0LRnDvr5z34bxgttbApBX7sJKYKwt6VqDykua9Kd+8d45HL28zMwkna33yrgNfZb0R1pv4ZyoD0WQBNCjrPDV1Aaba7NCR8/QrK0YcesGALuuKzWhCiXJZOxXjKW+CyjXpoAL7qk0Tao6BqHODKgy1Q/dLx4CD+RKYmiUd2sqawcnbbWxJxFeQamMjdeCWjaBVcx/Ox0WNslaNs1VL2TE07kR+raWSMAubzCqwKNKjkmw1WgTv4F5X1KvAK6swBzUGZYMzLFKX6zpniQkG7a8twlBXBZlJsNrSumd8mn1ZX7DNXQI3HlSy2oG5dVUwsTCc1zGRA4utsuhcQwKJMgxtwryISOoFq5t9LnQP+MarlykakQOao0X/ytzS4msYA5ljIBYNt8Z4QNo4Rp17ofXzRhfw/x0+yr/UeZfMWnILfZMyNHXGNmVsakxMjT/X2WYO/Pf7z3NrvMLutEV/3GA+S7AKshXNrFs9m6zrv3hqSSaW/mWNMg5YHG9GXjqtCye9rg0NurDMu7G341B2wZpFQV0pUl26jaqqDUyima2oBYO5OjKTYC2k7TnL8Zi8jGinMzBOLl0mjgEYDRYJ0NjKV7BVXX+1dk3Wq+s1FWPyyPpgs8nmYl2QDQH3PazqCY1ojq25gVfUNPMVQ6825e54iT+8c45sUOPf+eRv8W8sXaMZzdjsDtnPWjTinLVkxMSkzKYJtUxRG5T+s4GJ3Fib1hfv+f3j/XOERV3IzhKAKgxZCQG8UK4oAIkUvPJ3wDEpVsgylII3ZFwBnmH4sLwxZKGIHCuU2Ml5hQEhxbgAFMKACVkRwgSbz+c88cQTbG9vc3R05IGDw8PDY+mZ9Xqdfr/vC0wBWeSc3a6jvm9ubnrWi1LO61BkxMIsevrppzHGcP/+fYwx3Llzh09/+tP0ej2Ojo548skn2dvb4/Of/zx37tzxYMzDBZvcS5qmXLx4EaUUr7zyyrEiUIC6kydP0m63eeutt3zfK6VotVrs7+9z9uxZxuOxv/75fM6P//iPc/bsWZrNJm+++SbXr1/nzTff5OrVq56NIkCBtLH022w280WugJJaO7+wKIo4ffo0zzzzDLu7u2RZxq1bt1heXuYLX/iC93BcWVnxvltpmnLmzBnf93L9wqJM09QDCSItlGCDWq3G4eGh7w+lXECAeJEJS288HrO6uuol41prrl27xpUrVzwLJ8sy7t+/T5ZlrK+vc3h4yHQ69UwkGRPhPAjluSLBFVZQvV6n0Wh4iXutVvOSy+l06sdVkiQcHR35EI1ut+sDegS8DWW7cq4QlAeOAXUyR+Rvwzkaypzl72Suh5LZh+doyCyT1wqoFa4JMjY7nY4HQEQWKsnSAG+++SY/8iM/4hlbu7u7fkyGjGLxUJPgBWk7AVuFwRaCHXIPAqLI+BXASzY6wvsVAFHShUVqHI5LaZN2u+3bWKSqKysrTKdTf+1hm8j6KuuLgEjC1hWG7WAw8Oyzh/tW2lDmhniqCkNWJNdy7slkwvr6OleuXDkG8AFeDivrsYyJZrPJaDRiMBj4cyuljoXEyLlkjRdmpQBlITBZq9Xo9/vkec7Kygof+9jHWF1d9Qntr7/+upcky1oiIKA8EwTclHkic1+CbgQID583kl4voOja2pp/TavVYmlpiaIouHr1KvV6ndOnT7O8vEySJAyHQ65fv+6BOwl0kYCTwWBAt9ul1+v59bRWq7G2tuYlwzLmGo0Gq6urvu1DcCxkbUr/Z1nmr02epeEzdTabsbS0xI0bN7yMW8apsFkHgwFaa7/x0W63vd+q9KEkX8t4FQsQAfRCdmq4Fki/hgzhMPxExtLFixf9psq9e/c4PDz0gL8wH2XdFUsIka+LJ+Yf9Xh/AIiVNIgY71FmIsfiiGaGsuZYQ6b6vS/KcgcORVNTyYocw0XnTi5btLSXJ8bTBUsL3Hl8UEpRASyRK4ai3FbMP5fwLNcj1+oAukUKq5zVhaAYF7ySLrwbTQ1U4Xy0JKzFKjjVc35+6/GQeVsRZ3C0HjE5ZanvJtT3LeVWVEljoXc9Z7oeOxZYTdG+U2BSRWunoGhoOrdzimZE44GhaEWkg4L+kzHR/Rrx1LWVLpzkLm9q/qUTL5Fj6SiLwfBvrn6dT3z2Pb74oQ/yc8svVkChkxjXVcnYxrT0jBJNgiFSDrvJbExkLK/NczpXI6wunKS1AmlV4QDLWUfzx5963SctAxRWk9kYNYr5oeYdRibizTzirTubvHVnk7+ZfZZ0J6H+QNGr+pYKNBCfMWUcaFjW8MU8Mo60IhlWfpmJYt6tpLw5bH/a8O/8wBf4q1/9MZo3Y8z1FdLC+bvFE8tsWTHdsNQOIbMRTybjavBU0tkkRpWmShF2AIKNXDr2vBvRvEuFFsHwrIKagUJRtCxFQzPvwexESX11SnG9Te8dBwolE0PvjUOs1iTjDnd/MK5CEqowklxRP9QUbVuNTRyLcmqo9RVFPfKEUwduVXMsqgZrpCjTavcvgfRQ0b4ReyaPmx8WaikYg6m5gIT5OEVkuFbjAnu0O3+8Z0ivP8A269h2iqlV5zLKM3V98m6kIRcfgOpBP80wh3308hI2m6G6bZSuDK3LEqOcXNEFsRjvoQfufqg+MKjCHEtfdoxh7VnFphahCkU0Kym6CUUrpmxo5m3tgS/x0ZxsdDARtO8XzN5p0Xks4zBvcap2SHEuo3bQ8N5yZd2iGwW6SMBAUSV8i6GkqlLTGw8s45OuH+LYtcGsdKnEpgIvo7kbS00FB0YTZcoHRLmgFEu2jpfmRnMc21M5UGe+ljLZ1BT1CgidQzxRlA03VrLliKJpHRCsoa6cP6APpJpVjMUCalGJRhPr0q+/pgodmfdcCrMAPDaSdVv61kljy7prX11Yz/TUuQOoVGyIxgnvjje4UNslwhBFU3IbOeDQOnZkTsSSnvp1Q880s14134275mTsNgfyduBjGQG5+9rUM+ZWU1jt5wU1g+rlPHX6PudixdDM+I1n/w4ALaV5LW/yzZcfpTawxNOF9YWpPD1zFNFE+Q0lG7u1KZ5Z0irExCrFrKsp24Zophf2FQr0DA6KNr929BRvjE5xe7zEcO4S6A/GTQaDJkmt4I9/8m9yPlb87u3LjB60ULnGNktUZIgMNB8Yd/81xygs6k7W7CTpltmKpbavWXmnAAMmdTL/MoFZVy9YhRXr1moHqDswz/q12urFtc+XUmbLLhhIGdASDlUd59cO2Ms71TRXoC06V17anjcdiOyCZlxfxtNq4lbX0r5bVGnhmqLmWJUmcQBelMnaprwnqtxDogyFjSB3z9UyVZj1GS9//VGa9xSrd0qw8NLTZ2HpGh2d0UkyBvMG3SSjtJr9eRuTxRRt65Kj63j/UGFJfl/C/P47pBCTlMbQX0lYQKFcMfS1CgEF8bCSYkBe+zDwGBaiAlYIw+NheXJYyMj55PtQriXnk0OKydDnsN1ue58+eZ+yLL0M98yZM6yurlIUBZ1O51iyZ6PRoN1uey8mATYODw+Pve/t27c9eCXsRGG+bW9vU6/XPSvjqaee4v79+z7cZHd3l6997WtkWcbGxgaf//zneeGFFxiPxx74DFlL0l7SH+12m3PnznmW4MOgT7PZ5OzZswC8/vrr/ncCPh4cHKC15ty5czz//PMehDlz5gw3b97k6tWr3sg+LNzC4A/xeQtBZJH8CRgj4Rw/+IM/yB/7Y3+Mt99+m8PDQ5/SfP/+fY6OjrzXVpqmPuwB8LJrAQ4EnBAARopVMf2/f/++T3F+8OCBZ2KdOnXKgzu3bt3ykmeRLb/00ktMp1M++tGPMh6PfQjPYDA4Jp8MWXjfy48zBFKEkSTybWFK9ft9JpOJZ3zKGB6Px54dCxxjKQpAKgV5o9HwEuZOp+NBLwk5COeCXKOcS9oPFiCAtJPcWwj6SUCHyIPl76UthJ0nUlJpD5E+ggOmzpw5Q7vdpt/vMxqNjrWbeJLW63WGw6FPX55Op9y9e5enn36a5eVlz4CVNgjlrQIayjojSdahB6Hcs0jp5V4EVJa2D20VBNATQE5AF5mLoT+nSFcF4DHG+BRd8ZiT9UtYYd1u17O9BAwajUZsbGx4EHJnZ8cHXISArsiWZX7L+AhDNkLQMooiBoMBFy9e9AB82A/hui3zTcahBAEJaJZlmWffybgIx5jMj+l0yokTJ3j88ce9tD9NU1588UXu3LnjAap33nmHCxcucPHiRV5//XWUUj79XDYMZA7I80TGYLvd9l6EwnCT/guvSxjPEgz19NNPkyQJd+7c8SDkyZMnPXBWq9X4xje+4Vl5AOfOncMYQ7/f9+vp0tKSX48ajQYPHjxgY2OD6XTqw2PkPmUMyvMm3GSTQ56F4WuWl5e9n6w8V6VvZZNLxq/0kcinZXwIqCcbXALCS5+F4zj8GwFeH342C3NT2llCyOI45t133/UbAF/72td49NFH+dN/+k/7tVPuLYoiJpMJjUbDr4GSOC9BKuI5G4Ln/6zj/QEgKgdoFY2KraTwQKDWjlWiCqAKRbGxPQbolXXtpcMCgJgqdVeKHcCDiQIOOumvK7akGNcFQYqz9gWCSKOjyrNJGSv5F05CJwxFrSiai4TnqApmUYWkcbqirEwVj3YfYICT8SHZiktQnm66Imt8ypIOcKEqM8f6qfUjJhtORjrZtKBislUHkE1PKJauKIZnNcmRZbqp6F3RxEsZ9RtNxqctyXDB5Bhc1PxA4xqlhUQpdsuiAgpz/szqNwHnjZbZiNxqhialp2eOMQUYFLnFS9t6Oudv7v8ArW3D+ETEvKto3zVEsSUZlk5WuqR4tnPrWN83opyr8w1Q8FuTs/wP9z7OW2+dpvNevGDPqUryZxagCTjgpKgpz5Rxfbc4dyyMw5rz0tK5G2sHH7B8/GPv8CPNff7q7/8YzTtxBYhZ7wE3PuXOmR65QvVkNONOkVA0FFhDNMxcoR1H2DShynWgqJhQNgaTRmhrKRuabMMVrKQGmxqy55ynGMME+1ab1l5VYFfMIPICamnlHbnwV9NzReuu6/PhOTdfakeORatzA0QOyNQC2jmgQ5eWbFm7YIVuDZta7DxCFzA+Y8ifmdD8gza1vgv4UHkJ8xzShKKyE6BQXjKrGni/NGFj2aUONtYUrdiDd9FMOcDPAMo6hlxpULMcNat0/vLBbH3NgYfLPShLbC3BNmuYZoqpO5BtuhKx/0SHpaslUWbIVhz7tjYofXGfN9x9FnXnj5ktNxxTucCPg/rAcHQ2omg68Ck9WrDiogyYWaLS3ePhowkWy535Col2acBnTxywp065NcdUoGquvZw5nuKTbmdrjhHa3DHMetXmR+kkn5mNsbZi41XBHVQhHZFyUlVdSXBlwwIDrXtOEmyjyl+wVoJJiCc50bTAxHVmPe0BlumWgQqkMVEFdmuwkSUSdsSKIW8rirZxARoKfmH9BjvllB9deoOdH+9ytnXAR9vX+d3+E3z7H37AMSMjtx6aSDFbjhcJ2SIpxW3WeGZ4VK2jBmypiKeKtw9O8JMrr9GJpm6NsY55aNBoDB2dUaJIMWQ2BWWpDfDBPy4gya27bt7j0r61dfNUuZTl1cjyvzv1And+eoUPNW/wY80BtYoR/W7u+uODad2vIVHumL55Q5M33LwOnxV5u7KYsO65pOfKW2zMO8r7ZCoD0VD7eSHeo2h4pL7NZjzgV9/7BPa3V0iPLDtPV/OqZonPzKgrxcRaYm1QNYO1CqXdgLCxZbKuUavaJ46r0m2COFDYtUvRsoy2Yj+WhIUYzdwGy7wTBZ6oyrNbrVZoHAtbnoNlqiiTiLK2ABAl0Xgn75HPYp5eusfdbIm1xog0KlG5omwoZrYKYslcirbYJExXIorGwqvYxHB0zi2sOnfrfTJe2CEcPBFVVh/GS6tDBUFplWMyFpaiAY3WjFmUVtJu9zxuRXM0irrOSaMSrSyNKCfRBfemXTAuhCqaGZ9YLazJ4cnYMa2/f7zvDmFyyAd/CesQVo4AglIIhNJmAahgAfrJeeTvwmIofK0U27AAXeT1IcgnQEgIKsr3AiwBXrIaFtTyXqH/VBzHXmY5GAx8aqowEsEVx3Ecc+vWLTqdDufPn/fJzMvLyx50HQ6HxHHM5uamL4BarRZlWbK6usrOzg4AZ86codFosL29TRRFnD9/npdeeol+v+/bfm9vjy984QueURMyNUMwN2QESXtJsMkrr7zCs88+i1KK7e1t7t6966/p5MmTPqlW+rPb7XqmztraGj/3cz/HcDjklVde4datWz55WN4zTGkVYEm8uaToFNBQik3pg16vxyc/+UkeffRRZrMZ7777Ls1m07ertNXa2hpaaw9IiNROwMQwWVV+LgCGyGUFjIvjmOXlZQaDgWfhlGXJ3t4eOzs7Pjjn6OiIyWTizynBECJvFqafFMYPJ+AKQ1TGsxxKKV/EC9gkEm4pmh955BHPEvv2t79Nr9fzwKMwER8GhB9m0IkEMgzIkHYrisJLs0PACvBMtZDhJvOn1Wp5MEzm4JkzZ1hZWaEoCg4PD1lZWaHZbNLv9zk4OPDBQ9ZalpaW/N8/DHjK/Oz3+z45WcaSXKe0sYR1bG1t8d5777G9vY21lrW1Ne7cuePBUAHAJMhI2IECZEiQgzCtpT0fDjWRf8Ax5rIAGQJgjUYjf04BCoUB1+l0yLKMo6MjGo0GKysr3m9S1iyZw8YYDg8POX/+PBcvXqTX69Hr9UjTlNdff513332XOI557rnniKKI/f19nn32Wb797W/79UeYhHLtD6+zMhZDz9bwXoUZKdL3hzdoZN0VcFDAuul06u+72Wwes1QQkF3aUVLJd3d3uXz5Mlprbt++zd7ent9MieOYr371q36tvnz5sp+zzWbTBzVJUnboeyiHSHHFJ1XWCplH4XWJ3FpYeG+99Rbj8Zi9vT3u3r3LaDTil37pl/z7SNqynFNYu5JIPxwO/fiTcRLHMePx2LeD+E1KCI/IpqVfZANGxqTMFekTYeIKI16eD+EG3ebmJpPJhBMnTgBw9uxZsixjd3eXw8ND3/8S9BUyEGVsyvNUvCSFFStrCSye2yEDUo7QPqFer3sLDvGLPHfunE8Nl3uWeSnPRBnTYlEiGz2hp+vDMu//teP9ASBWPoXCBMxbiumqS+1Mh9anaDr2DZXnnXFJy6gFM7FiBJhYUTRcIWcrrymrF6BhmR5nwqgKBfPhJhVTqhTA0FJJHiuGzdxiE4XFkoyMD3KxkWPcFHWNLqkYjO769FyRrTlZW+e2u/blZIIGujpj3rPEE0XeNjS3FdmpAqtjV4SXULQryV0MeuISP03kZMnJyBWH8l5ONm0Zn9KUczd4Vz+2w/abG/TeVcSZYXK5YEVrhtZQWkukoE5JqkpSyioBtaS0ioyInp5hcF5oxiQMTUonnpJbKCsx81FRRxnnv5itWdIjRb3v+jhvx0w3Lc/WbyIpFQOTcWO0wldu/xhJX/Mffe7nqT/QrB64Ktuxwdw9x1M3Fkzs/AZN7BhVLjCnKjhTRTJysr0ycTLCsgoPSUbQfxz+zE99ha2kz1/5g5/k9befYGVoGTxmqO1VnjUdxXTdFcVpBVCUDfj/9D/C3/nO83SrjWbx8lOFS2M2VfFaVoBmVAUm2HpCUVM072rSgXaA7wiGl2MadyOaO5bJCVfgN/YNs65GWceyM0lEWdNIkq2NHJMsmjqGK7oCu1Ql5Z+V2Citrk9RNkqydUXR1FjtWEbJ0JIMMrDtxfyLLcVuwxffLhzCQC3F9FoOCLHKAVzKXYelsgCYWQpded9Z989qRfuWZrbkgkSmK9XOrnFpvFZDMq4FvoiOOWgSTTwtvdXAvBs7kKxi+xY1xyC1EYxPaJKxZt5TZCsQZ5rmtqV/2bVRPIXZiiXKFPU9x0aOx24MJVNLPC5Z/06BnhsmW+5Ba6LFuhDNK3DDWLLlCJ0r+nmTR5o7DMoGFzv7bDdPLViB9ZKoVlb+c47F6oAmx450LD3LZEtjYsfci7Whp2ekUUkGiFwUXH/XlGZsE3Th5PdlzVbhFRWLsQKx9Bxs4cZv0Uwo65qjcxFlw50zykDPlJcQC2vVOstK5tbyJ7uv8NE/eY26ytmMJvS0oqkSIqXYKeFnWof8qctf8mMmUm/yBx+5ADda1A+oWGvfvfPlU3DLSnJt8R6K6dBC4QDS/YM2mU1YYYRBU9c5eXXBImEemjrr0RhTAUOzHj4YSI/dc6JoUDGR7QJUqp6lK9GIL47P8We7t6mp7eoKF8XNo0nru64/swk609SGjkVtI7UA2SK35qiSyv/RXY+JFcnUoofW3/+soylbBl1q0IvkY1U6r8u6ytHaMKsCs0zDAW42saRxSVMlHBj3GhUZLJH7WoUyNfaNe1ZRrf81Vc0d19/SB62dymBcvBIrxmJWV5VXYLU5ZioPYLUYk7pKETex+/lsRYM2/hmlqgXkU+13iJ4x/Ez3FXKr6eicg7LOn00eJRlCbWgcGKsV87by3osmUdW6ab0vcfd24Z7zsQP9irry6erCfKVSBKCsl1OXKAoTYSN3TXlH0azlZGoxHqzCMWuxZCZBi1+vsozKOnvTNijIOw44dCxE2Y10mwLfZyC+/w4BBaVQCBMUQ3+uUOooH/ilaJHiTF4rhZr8/cNMrIdlkSFjTf4v1waLQgTw8mQpHATUkusJi3IBGEXiubu76+VcvV7PF4FizD+ZTDhz5gxKKc+oE6aIFJ4i/xJvOkla7XQ6nrH04MEDD2ZJEMvjjz9OrVbj6OjIsx0l+RTwRWEogQyZX9LWD8tA5ZDiSsCVkKkiYNqZM2d455136Ha7PP3001y+fNmHo2xtbXHjxg2GwyGvvvoqd+/e9SxUkdROp1PfBqG0UvpfPMLk/xLyMpvNuHjxIr/4i7/oZb55nnPv3j3u3r3r5d+h1LUsXdrwfD6n3+/z2GOPMZvN+PrXv+7lk6FkXdiPMqbA+XQlSeJB2ueee87Lknd2djzoJW0qSbEyzoSFGoa9CCtGrlH6TViBrVaLu3fveoBaAgwmkwlZlnHq1CkPKsj9ShEtHnDSf6EcOWRcyrySfzI/ZIwMBgMGg4FPNT04OPCSQ2FRCcjUbDY9ICuhCpPJxLdnKJ8U8FSuScacjA0BjwVokvaYTqfHgG9rrQ8xEQAmlGEKSCPMVpErP/300x5cOjw89KnJsn6Jz6iwI+XepD/LsqTb7XqGq7RZkiTfldQrrEL5u6WlpWMy0Fqt5jcSQq/CENgVeby0pYArD4O44/GY69ev86EPfYgkSXjnnXd8yMW9e/fY3d3l61//OleuXPHz+Qd/8AeJ45itrS0vgxXAXsZqeIQhQ9IPk8nEA61RFLG+vs7NmzePbcLIehoy0UOgV9pc5MmyESOAc7ipIrL/d955hxdeeMGD9GG4iZxbgHaZk0VRMBgMjo250CJAwGMBewX0T9PUWwSEAVCysdFoNHybNBoN3n33Xb9eCNAcRZH3DQyTmeVcAnaLrQDgNzSkXaTfhbW7vLzsma+z2YzxeMzy8rIH5mT9lyOUncOCsdjv91leXvavz7KMfr9Pt9vl7t27TCYTut0uKysr3rJBbCFkrouUWcZraF8ga0QYeiXsy3a77eezfEaQ+RGyXcWq4c6dO/7+xNIhVBQ8vHkhvprSprLBI0DydDr9F4+BmLcBpYmnDkRsHBhqg8Vkz1suxbJMFGXqpHDJxCzM1y3EE0NRV5W/l2OFWO2krfIhf9aNKgahA/8cG6Zix0AF3ICtuaI/lDIbQAtQU7ik3bKmydva+20BiBeYD3dRirLmzpscWeKJe40y1qe6no6nFC3L2hsZ3dsJ0TRn+d2I2n5G7SitGBqa+kFJ3oycKX6/AjqmrhhUuaVMHZiAqiSYJw1614E4h1/dpF4VhyZWnDu9R141TKoUmbVkFQNRrkuM+nMbkeoZ+6ZGhKWpZxirkTyMCMOS1hirXYBABquvO8aXYyhViaRnM87FE35zssZ/euVPsH17hXQ3prav6B04g30TW8pq40TPIc4dcykZu8CYMnVFu567UABtHXiVTCAZl1Wfa+YdF5qh5zA+6YrUn/mJb/D3Xv4ES99M6daVD6Zga4oZNhmec+Be647zgZMwkiiDv/cPf5jGLGC5yCS1TtYIlTdYrerfwv28WG5gEsfG7F6bkmwPKJdb1A+6ZGswXVdkG4bmfc2s62RyJlYUax1MI/YFq3jjiVQvnjpwzFQFvUiE55VssWhaokxT33WgqqrYdDp3YB0aKKuAlV3HPNJz6xK/LZTNFD2toyYzypqiluSMcSBQWbeOTBYp5m2wsWPocH8XtbVO0dLkTUgHUN8Xv9KquSrwpag7T1BVWsqmIhlDPClRhSEel6h5QbrtDANtI6Vspcy7Ca37hnQ/cwzJWENhUMZgagnFUo3eeyXZRo0yUfSuO6AwHhfY2ElHCRZXk0bkndj3nYQjJVODzi3JICealTRu5qhsxhc/9ST/4Ueuc2++zOnGIfMlQ23feWgmvRn5JCWeur7xHnOFZeeUQReRB22kHdK4JEdTVj4IJqnA2UgxX7YYa4mwjC7njC4qaitT1rpjJu9s0LlpmfWUD1ZqLU/ReUo8yUkGJd1W2zFOK2AoXy9QM4cmOeabdazoyDKz0FGWT9fn5LbEEJOo6oMdEaeiJiM746CoHtTAe9mTxLGhrEAzcGtOlFX+r3YhI0Y5eWtplPepLVNFOrI0rycOtHpQY7fo0NEZkTLUbe7XoggXoJJUjGjZsKgdWQeU1dz5bJX0LKxlZYWJ6GTXXZ3xwvwSNbV37PlTWoMJkCCD4aCckSjF7fwCANmSUAmrWyplw6naoIoV8URV/ryWecelgsvaoAzozDEEQ1DORrAajclsjDG68teTZwdYZVHKUlMxEXOM0Sjt+o+KjWq180D0XpRVSI7O8ZtyAk6PNyOfOK/LRXslE8u8F/nnqby/rDsASVR6/0MUZCvVupPAbElxunsEwGPJgHHjFr8/foyvHj5CVia8t7uGnrkNgLJWgSZzZxdSGzqv2sl6RNk0qGqRMzEMzlXeUdU9RXNLUoXTTLYi97xj0Z5eho+lFlUsAwvZmqUTFy6BOVu81liFRrlnGopaVNCKZxzmTUazFIwiHinypmNM1o6MDyU6Oht/V+jQ949//g8pYELGWFmWvvAO2U7iTSbeXALySDET+qCFDJ+wOJG0Y8ADBqHM+WEAUg4pSKX4FsBBCp1QjhjKqaQgEqaVFILCiJEi7NSpUz5JVArYBw8esLq6Sr1e94ABuEJmeXmZsiy9X6BIDrXWDAYDVldX2d/fp9frMR6PPagohXpRFNy+fdu3sdyrFOoC2ITFfAgehQzQMJRiPB57/0ABFgC63a73dfxzf+7Psba2xnw+58qVK1y/fp0HDx5weHjoQT/x1JO2FjBOAAqRMoq/mMiKp9OpLyIBLl26xMWLF30Iy+OPP84777zDF7/4RQ4PDzl37hzNZtPL4mQcjMdjP95+7/d+j9dff51+v8/Ozg4XLlw4lrgtQIwAeWHRH8cxN27cOAayCbh17do17ye2tbXl2zcEggSQepjxVxTFsaCRkHkoxfDGxoYvgmXMh+NYWE337t075gcqoLiAL+Jbp5Q6Jh2WaxK/QAFBRGotc1PGpwBioT+evJeMN2EgidRf2tVay3Q65ebNmz45Nc9z9vb2fLCI+AvKNY5GI++dJucRIFUYmwJAyVoUykxlHB0cHHB4eMhP/MRP8LnPfY7bt29z7949VldXPYNKZJbCqpWAGQG1pf9DHzwZr1mW8cEPfpBnn32WTqdDp9Ph5MmTfPOb3/S/+8t/+S+TZRlXr17lypUrvPPOO561trS05CXuwsYTgKPdbrO1teXnk2x+CJAi17W9vc3nPvc5P49lbQrDjoQJ3G63GQwGHggVKXO32/WgU7h+hp56R0dH/r6vX7/uPTzH4zEbGxvHpMohC/zhn4cMtWaz6f1OZb2RtUPm0MrKimcYFkXh2cayRofAmYDdAuQKy07W3lA6Luv/bDbzLGIJagrHm6y5IVtOgGBZqwTolX/hppk8l2QuyBGCsgKICkuv0+n4eSlrxmw28+ucPEvFI1E2BEIpcriJJNcjzOSlpSWefPJJLl++zOXLl9nZ2fG2GmtrazzxxBP0+32yLPOBV5PJxAe9PDzHQ8Bd1prJZML+/r6/p9Dj8nuFV8kYke+lX+V6jTF+4y5UFISbhjJvZe2V30naubCMwzHzRzneFwCisAdnXe3Tjn0RVFjqh2UF3LgCv6gryjQibyuauwZs5S2oHDvBRIoos5jUmbUr45gWRUOYhrYC9qpkZmH+WBZgUOwYjKq0FDVdFVkVg7HuQDKdu0AWCYCxsTuPAyLcuUyVxBrNcaCNdfc77ypOpwcYnNeW6RUkexOsblVm/Yq8Gzs2VmFZujLHJNrdL7DxigEDtaEbqJ27Fj2zNPYdUNK+p7j7Q3iQySU5u695U/PH1m+QoCixVKpLwAUN9HTO0MYkypCZhPVoytAkRFjqqiS3mrly1nNYWNIzEqUZ5PWKGeLAXhO7NjKpcmDGNOInX/rXyd5eIj1UrPRtFdYg0rjFeBD/LZPg2SfKLuRtZa2Soh4aJxmsKbIlvQDJjHtfsHzwJ97m5S8/xm//3U+yklUBKXOIMjfGTK7RMyd9dMES7hzzLkzO57TfSyoPwYp1N3VjzkuYY+0BMh8EJCwXMfe3uCTqe9uozgUXqBM5kCMeKV8Iy9/FO31MrwUbKfF0UVDrahyZ2AFDUVUMz3qaRttJrB3T0P3N+FxJPNR0brjXOuBdY2ulS3q2YGLrpMZV2+o5JAcT2NlDdTuUNahFZnF/FYCAqSSxyqJLg+p1sMqBRNmJEmUU6VDRvZk7oNZA7XAGxqKzHD3KIC+w0ymq1QStscMRqlqw0QrTa1VSVBeCZNDYRLsBq6pBWLqfWVWtAVW7ukAQjZ1pTKK9l5vMcRu5kKKyrp1HaCzp7QpVGKJZ1ZmRQo0m1N7Ygo/ASjxir+hglnM4rGEiaDXmDLKEookH7bCOTaZyN5516dieAgI3kpyhSfn3Lv0T3vnXt7hQe8BmPKCuci7HOTmW9WjK9T/x3/q18n4x4tMH/yd2n09IezOSpKQWlRRGe3A422iy+2FNvpGjayVRXNJMSrLbneq+F2NVNQtqCm6XNZp6zl5ZUqJoqoJUKZZ1HY2ipxvUVc5uOWNm4bBoMrvdJp05EBLr/GqTYVGB025+YG3VRxaTRoxPHkdcujcMo1Oa5EhzfbrOxXTXpS4DuY3RPn3ZUlc5KYaZSdBzyBuqCpNyc9mt846pKaxwE0M6Ax494hP1iE/U36a0xgU12YKZNcytdRtEwKCiBo6t2yx5Y3KKaKaoHVWbQtW8N5K+jfO2xVQWEQK2jSypWST1znoK0zSOQUr1rKiYmB2dUxpFHBlmIbgooJiyRKr6QGKVY/pFtpIwu36sHZpjCdfC3s7b1SZbJbtu7Bs/D0xUMbkrb8GyhmMZhynlgWWEGzvVw9JCdirnL37qt1iJRtR1zvP1u+S2wV/b/xT/04sfpdZ14/OJ9R1qScE8tdT6UDsqK/m9Yz/O2wqzVCXZV5sa4OwPOveKal4vQH5hIqKqVGe7CKUhaL9ZWX08spCvFe4RrxeAtzIWraxjINoUYxWpLkhUydjUmOUxaEtZdzYR3rdRZNy1ai38/vG+OoTZIBJCYX2E7IDQ11BAQ5ExPQz6CUND2EWhDFdYRSGzKSweQv8kYc2FwI2Ah8KYgkUqpRSmsGAVCUgh3ofz+ZzpdEqn02Ftbc1LuYSxoZTi9u3bXm4rgRxHR0fs7u7681prOTw89GyXkJUo0rkTJ06wvLzsGWA7Ozs0Gg1OnDjB+vo6+/v7HBwcHANOBVwKwdCHQdSHGZfyWrk2Yb2E7S7F6Ysvvsi5c+c4PDzkt3/7t7l79y79ft97mYVMIwGtQomngB8CQknxKKwxKRhFkjcajfjkJz/JwcEB4/GYfr/PV7/6Ve7evXsM4JX3E8larVbzgM7q6ipf+cpXePPNNwG8b2FYgMr4C339ZAyCAzDEf/LmzZt0Oh02NjY4OjryEk4JNBBwot1uexAhLN6l4A1B7jRNPbNRQAIZk61Wi9XVVbTWHqx6mN0m7DBhPIXFvRTpAqjInJG+EiAilDouLS1x9+5db0Vw5coVLwkV8E6ALrlOYSTJHBa2lgAx7Xbby64FEBUw5s6dO16auLe3R7PZZH9/34NLAsCGslhrrd+4ADwoKqBUq9ViPB57Ztebb77JL/7iL3L+/Hlu3LjBwcEBp06dotfrsby87BlxInUUzzZhfYUSbvm5gPhlWfLhD3/Yy+b39/f5zne+w7e//W0/foT1eubMGVqtFltbW0ynU4bDIdvb2z5AYzqdcvHiRZaWlrh16xaDwYDd3V1GoxGPPvqol2ULWCQMaZHKy9wWeamAggLoyObGiRMnGI/HvP3228cAwpBlJ4fMkXq9TrPZ9Anae3t7fm0fDoesrq76dpHx/b3sEmScS1iIzHnZaHlY1jocDun1eoxGIz9+QhasAPZaa3q9nt+0kQCcfr/vx4SsQ+KNJ8C9+PbK7wSck00PYSjKPBKQTuZW2E5ynw9vYoVtEjIQpQ8FuBYwUynlN5VkbkpYijxzx+OxZ67L2iPr1sP+wyG43uv1uHz5MtPplAsXLtBqtXj55ZcpioKzZ89y//59bt265de0EydO+HnRaDR88I3MhUaj4edpuKaK16AEWY3HY/9zWfNlPEubyXM5DCESxqLYG4Ts0ZBpKfcta5qMRWFpyrXGcezP9Uc93hcAookdowxckVDWKlP2yo8wmkEydSwFCc4Q/68yxaWuKohnFquqwITKr0kKKZ1b6v1F8e6kypXfobUecBDZl64khyDm7i7dFODofI3O7cJJoEthG1nKKGA79lySsqpAg2igwVhUxerIVhRnkn12S816ZGguTZmvt9j5SJ04g+F5Q+dG4gujWkMzW3bnNyk0txWzZV2xa6C5bZidcuzMoqHo3SigUxI/qHkZm/hOHZ3TfLbzFmPr7icBZtZJ1Ho6J0dhrCJSDjAMj2Fl+lRXBQdlQq2SOt8sIv7w3QucmFn0CO/9KP6QeUfReSslniR0zAIsLOtVgVuBjiI9LNrKByTosgJ3lOvH9MiNBRTea0yZSj6ooP8YlA1LY1uhS8XTnXu8MXjcefuVzu/OAbsOkE4bObqoe+BxdBaWntnjP7j8T3iudpef+6v/Z6JZBeAVC5DQg4iJ8x3UM5EwUvnVVYy2icFECj2eYaoEYu/dCFV6s/XMqSizLoU4ch6F8ZSFFLECHcoaVQABDM9okhHU9qbo8zFlTTE9VdDcGGP7DaIHEc3dihatwDQTD2YD3idUwBhdSTJ1p12B8hBpgyqUZx8Ju07nVDYBFkqDrQA8mxisNmSrCe27CyYaxiVyoxS2Uavq/QZEzuNQPbyDYoBUWJiS0BoskFpXUtWq+ArCjdwPWLAOPTBTMYnnDgguE7VInrXKA4wiyaa02HaT5XdLdvIeJ5IBNZ2zvDoiu1rD1CwnOkP6e23qe5ZoXrFCq/ebPFLAJPH+q5LyHGvD0DT44409fq41Cm46QaS1q5WvXGkNkdLsmpi0llOr50Ta8eams4RIAN6sIO9E5CsFOi1derbR5HPlwQ7xW1QGPvvou6zolCVd0jcuKGliEq6YFqXV1FXOdtFjNR5xKpqhVUxHFWxnXbpXNfMO3l4CoGhG390HwRFn1of5lAnU+yUmUkw3FNeGq/zM8oyJrTE0mrqeU8ehPZlNSGyBQZOZBJNClLv1X/xvJeAqyvDyeDcmYHq3zTczNwdWooy6spQWDkxKhOXI1vhkreRaEfGXr/8Ug1md/qjBfJISA7NuJZut0oG1Zzo6H0RwGw61fiULrpjXsrbpAtS8AvcSF0aic/f/jrLkqkCpQCKsK8BSQxItUKpIG5SyqMQ4QKwCzGZL2m+EqdKNQWcB4sa7hB9N1rTb4LIWL4Ev3DM1mrt+QyuPGVoVPAcr01+du+evqhn+xnd+EHu/TvOe5uLPXOUfXf4SW2mfpJUzG6csnRhgrGI6S9C5IltRFM3IbxJFc0uUUzE3F2FHKAcEjzciH34W5QsWYpzBdFP7hHMPIMpzHMW0TDyguHziiHkp7ysbCVDTlSedVWhlKWzE3rzN/qzFpN9AzbWzQ+hp9+wYVgqEmWV4OiLvfo9B/v3jn+tDgAcpUIui8ECRtdaz9aS4F3aaGK1LERLK0GDBJpICQApjAX6EXRUy7EJQLAQYpaiaz+esr68DeAmeHGEhIQwrYWSFXowCqK2trfHyyy/7Ing8HnsG071793zhFDIoBABdX19nZ2cHrbUHOUOJqaR0SrEuBab4+T366KNcuXKF4XB47NpDmWPI4BRQJEx3DX2fxB9sb2+PXq/nWS8CwkjC6R/8wR9Qq9W4f/++LyClf0KmjfSTnDsEIAQgkCAVGS9KKR555BE++clPsrq6yq1bt3j33Xd56qmn+K3f+i2SJOH3f//3vSRRQGIBS4T59pGPfIRPf/rTnDt3jgcPHlCr1fjoRz/qgTRJbA6L+xB8FaaX9LdITKUfJTk3BAHC18sYERaSMMGkaJXrbjQaDAYDzxK7fPkygPfIW11d9aCGvOdoNGIymXgmmrxP6Ckm/StgkhTmIeAo/39Y+ivjSECTo6Mjtra2UEoxHo/9PB6NRp7FJKCBSImlT8XjM2QHy/mlnWRsPux1GoKFD79WxtPDUmwZRzIe5JD2vnr1Knt7ezzxxBO88MILPHjwAKUUa2trrK6ucnR0RKfTYWdnh+FwSKvV8mnXsg6Evm1yvcLUfOmll7zsVPpdgLA7d+54wOPnfu7nODo64sqVK/5cAkbJPOr1ety7d4/r1697gDf0khRAutPpeBCm0+l4gFDOKXNQ1lRhbLZaLWq1GpPJxEtYw2AQOb98L+utAKnh5kqe5z7tdnV1lV6vd2xjQ+4tXOul7WRtFWmpBGaIrYE8M2q1mmfDSpJ6aJUhoTJ37tzhxIkTrK2teQDp9ddf96w1STEW1nNZlrTbbZ92L7YDssk1Go08sCvjSNY7aR/ZWHrYPiNc98IjXOtlHlq7CF+S95NNlZAdLvPq8PCQdrvtwWFh88t6FG6wySF9Gsrg79y5w+bmprfYePrpp3nllVcoSxcMdfv2bQ90K6U8u88Y5+MbMpqFHS/tKtc1GAyYzWYeeHx48yY85D5h4Tsp1iDC2BbwWvwUw2d9uB5LiJZYDcgcEHBWAPYQnP1nHe8LAFGZSrZaAROOnYU3LC9akrCrPCtR55WheunkV0UFOhYNF55S1hapzKqwxJlLYTYejMDL6VCVr6GW66GSQwMW4go4NIkmmRS077nABiXpshGVF6MwD/GSa0msjScOBJVryruWlprT0SUaxWp7wnyphUlgVnfMwdkSdG5ZJpvuOmdLLvk2W7U0dp0HWDKqAlYOFNka1A4V001LlEWgCmoHDmQyScU4Ki3jiznn4kMiYGIdPtPTigNjmdiIpiqZo5lbjcbJKMc2oaPnZDamo+dEAS2lpxW7ecp//Pw/4j+p/xTp6016Vw2tuxkHTzWc+XwBsXh0KTyLI55UvoYVU8ZWDJQ4q/zoStdmurA09m0FnokUrurrVDFbtbQ+tM9/9dTfJ8Lyn976Ka7uX2ByzjFKopnrE1VWgIMkE6cK+24bPYfZCmx8+j6/+fh/z0pUQ6P5H4YXKevwiZ95lY/3rvG3/4ufdfeQRCitUZMMk0QeVJB7w+B8DHXEdLWSk6y3iBqXKdtpxc5cAAAQFMEKSGJUWVZsRycxNjVLWbhgnaJlsdpSm7g5EWXWJRBX4E16EJH3u9QqKWOZLDzwynqEig3qKPYgoqrkmGXq5iCRwjbrkBeUKUzniQNAOH69RUN8+EqoWEsmqV4T2QVIqZUHImwtglKjBwPsaAxFAUqjeh0IDbejCGUMtpRxo1DWVKBmVFkOVKCcVthYwdx6fzlPq1WVdNgon6Lrgl6tY49WGwkCXlPg2XOqtKh5jipKei/t8AcHl/ipjVcxVnNxeZ/X0xXKuqWbZqjEcHQRyrbB1kt0vUQpaNbnlHeD+6pYyI3YfRi6XRhOuvhZ2so9FIRxJl8BZta9fnLQJOrHPtAmmiqmj80czhq7sI+4H8Nh7ICkqWK2aqBK6xaPSavhd194ii+svMp+2eYj9RsAzHEN2C+b/EDjBpeSQ97O1/gbD36IG6MVhrMaB4MWasvJ6PVc1haIMwGqK5AqYO25/ogZb7prb+wV1PYzdF7DRgnvXN9i/5QrUlejEYkqvIRZEphlQyOaKsqkeg5UzHCgCgqqpLtR9Z7KedA+kc75O/2n+NrBJd5+cIKi0KRpyWRUo96c8+bz/z3Gaq7trFFMY5dsnRpULsw9/FiRr3mn8qCMFHnPJbbr3MnYdeH+RpeW8YkIm1of7gILdt9G1GTXzClKvZiPgr5qSz12xUhmIwqjnYxZWZQ2KByw1dgz/hkmPo1FXVE08UzYeKpo7pUOrBM/x8htwJUJC0ZdxZz0ycYyr6069kxGW+J3m9R3oblrmBbBGFdusyzWhqxMsNbNuda2qVjQ7jpN7PwaZSMInLTaakhGisZ+6TaLcGu1C29RlC0wiRt/El6misX8Mv7i3fPi0ZU9Xr130oGWsyrpXEGiSgyGus7ZqA2pNQo+0LzNajyidXrO/+3qz/Bg+yTNByV5Sx9jaxbNBSj//eP9dUhhKD5HIZNvNBoxGo184SlFgjDdhJEWAowhAxAWbJiQLSeAVViMCCAigEsoJxMgSHycQo82AUCkCAwLaClSQ8bcbDaj1WpxeHjoi6i9vT201h7kuXfvnvexG41GrK2tMRqNPJAngKOEriwvL3uPJ5FHA8d8JQV8O3HiBF/60sJjV1g0YTEkRXvIAAr/CSArjMjxeMydO3f4yEc+QqfTYTQacf/+fc6ePct7773Hzs6OB7xCeVkokQuTMGFRyEnbyWtCxufa2hof//jH+cAHPuAZQi+99BI3btzwLKLV1VVv+i+FpLTP9va2l4H/+T//53n88cd57bXX+G/+m/+Gfr/PL//yL7O3t8fP//zPY63l3Xff5fOf/7xnLIkcWICwkKUjMjt5nYwjSVQNPQTDsS1FsviXiQ+Y9N/S0pJvp9DLT5hVAtgJWypkagGelRMCafI+8v7CChUwUAr8cL6EwFXIXJPXhT6Y4fgSBpYU96FXpowNASXD8Shfpe9Cxmp4LeHYle/lGkLQV/4fshJDUELOIezCq1ev8swzz7C+vu7TwNfX1z0QNxwOuXjxIs8884yX1I5GI/b29tjc3PQAsFKKjY0Nz0AbjUZsb2/7vglZaoAf+zK319bWjrU74MNHhM2qlOLChQv+HJLoK8wsYeLJmqm15qMf/Si9Xs/LX5VSfOc732Fvb4/Lly/ziU98gm63y3Q65c033+TGjRt+bZK+DmXgYZvLfQsgE24wJEniU3VPnTrF/v7+Mfb4w2CvrIECCEpC7mQy8Yw72UyaTCY+FVuYvx/84Ad59NFHGY/H3Lx5k/F4zObmpmf+3r9/nytXrnjw8eLFi37OCJM29EKM45iVlRXyPPegprxeWImnTp3y4Uoh6C0bQCHjWp6JwtIN50GapnS7XXq9nn9WCbCrtfZtJ/cirF8Z88ICHY/HHB4e+vZrtVrfNS/C+REy/eQ8f/iHf8jy8jLf/OY3WV1d5Zd/+ZePsfYFmJf7lvE/nU4ZDAb+mS4bC+G9y3ognwlEGi3tHq7hsjEQPuMffj7LhpVcx+bmJltbW35tK4qCJ554woOzKysrnD171nuC3rlzx2/GyOaerLF/1ON9ASAKQ7BoOkDJ+bW5wBFduCLUxooigRgHDorkM5nijeqtdlIjSQQFAUUUpljIIF1tsWAleekXleS2ApfKqmBRBpKxCz5BxQvQsAIdTKywqQPEHMNIzl15fmlDNHcytnjqvOaKtqGuCjTw68PHibRhnmriDCYnLLW+YrZkScaG1j1FrW+YbMYkQ8tsxQFsunQSbRuMAltJ60bnwU5j5l08m9OFjShOndunow2RUtSqdjBQJSxbMqtpqYKWNgxNxNxqIizGKuqqYGIS6qqgqQtyq8mt4Yl0zgfTe/z2hWvcWlvh1qkTnP6tFP3T+2T/ZA09Xxjj20iupQpESYT9tgB3TOyA1nhqXTJwXTFvKYqWhNK4c8z++BG/8MjLfLx1la+NHuW/vPujvPjWBXSzIIngI09d473JBvUDQ7ZSgQsV0CeAavcqzHvwy3/mi/zF5fcoqHGzmFNaxd+69mmmm4a/c/ZrvDWf8LcS0HODmhUQaQdOxRUTNAJVhVMoC6YWk7djTOLGdzSao+/sYh45iYSR+DAFK4xbd002jjDNlFkvIm/hZMutksJGRJkimjlvTRMFqbMCmFgHmJjUUt+tvOBKS6mdLF/PyuqEHAuZAJG6W1SWo45G0KhjEst0llay0AXoKaxPFFA4YE8ZswBCWcgFfXhGpN3fzkpsLXUgU7izFEeuTSPtglyqcwJVYE4gQ66CHlRhMDVpSEk7r+SjSgAz0KXBxNqzlCQwRdrHMRPxAKmfU2ni5vTRiNdvnubH1t8gUSWX2nu80noEU7P0kgylLPXHBsTaUEsKZnmMsYp5HqOM8uByNFPkLfiLp3+LH26UQJOJmTOxObfKCUMb0zd1lnTGQdnkZDxkxXUX28US8WFM7x0oGwsPzCitmBh5Se/qlCivM++IpQNMT1vUzKXCYxcyUdsq+aHGNv+vwQf4+c/9m8RjjZ65AKF0YLn1F3+X/3Dtbf7C2z/B4PNbfnPGnrTUBoqymrs2qmSdhQbtfO6UcWu5AIjKWIqmpqwDWTVH0ggTKdKhoX4rJbeRl3FL8rKxmqxa5JLqwk2tCvCpKfKWk7SrKulZV+EatmLSiT1CQsRj9Xv8tdf/OOvfcknxR48Y9FzBBfdAqSvH7iznEdZU9LbKY1BCunThvsYzl7qsKw/Esm5R1vVJ0VBuLFbXYzWomXJJ6WqxTttoARLnpYxhULlCzxSlVeRmMT+2OkNMZ8Tl7i7n6vt84+Aib12/SLaisNV5dGH9ei8WB+C8S2dd7QO4BAzUwgTMgK48R1TF9g2mpjKebZq3QceGsuaASBM5j0SASVlzYywyJFHJ0ayOrQKYpmuaWcW8lPVB0srjKRXgXG1OtCzZksakAWu4rJiLGehceVm5MLQBbOxCVObV4jrvQCueMxvWSHEbMONTiumZnB/tvoZG8wvtO/xY8xr3ypS7xRKvT8/w2vAUtx8s0yhgshEt0q2nTs0w2owoFllU3z/eJ0cI8oWswNDPSIogkYDBQvYk55CiVYqWkE0gRUMo/Qylu/J+UrRKMVMUhQdWBPBQSvmwBymmBPgK7+lhltNwOPSedFKIjMdj1tfXUUpx48YND1xubGwwn889UCQm7sKMqdfrbG1teRak+Auur6/7+xyNRgyHQ1/whIUvwI0bN/z1hexJufaHAYCwWJN2FXAG4NatW3zlK1/hsccewxjDxz72Md577z1arRbD4ZDd3V3PFAzPLwWrFHvCDBGwVSS2whqU78uy5NOf/jQf+9jHGA6HaK15/fXX6Xa7tFotDxYmScLHP/5xvvzlL/t+FNBpZWWFVqvF/v4+zz//PKdPn+bv/t2/y5tvvukTf/M858yZM7z22mtsbm6ysbHhQdPw+kOZbAg8yfVKsay1ptFofFdRDXjgTOaBgJMhKC5gc6vV8gCrBKCI1NVa6yW80nehrFkYSiErLwQp5LzyVYrskLUZ9r2AuSH4G44RGTvAMQBS5lAomZe/k/PIe0r7AP4awveR9UPaUsDKkIUYgonSdyFTTq4hlMzKoZTijTfe4LnnnvMA/WAwYHl52bfT1atXPcMqZPAKs1jOE0UR586doyxLlpeXOXHiBBcvXvRztd1uM51OeeWVVzg6OjrGNjPG+GCkfr/PcDhka2vrmNfd3t4eZVn632utvTxY2k2ktvV6nfX1dc6fP8+TTz7p2ad5nrO6usqlS5f4/Oc/z/PPP8+bb77JrVu3KMuSw8NDL4eXPhHfSMDPEZkT0ifCBpQxKN6jsiYIw1vu5WEQMewbuQfx3QxBVwFc5e8F2Dp58iRpmvKlL30Jay1Xr171GwUC+j8MLsv1Ly0teRBRpNryfjImxHJC7lVYkfIc+17sPmmvcKwnScLGxoZPhBcv0+XlZba2tkiShP39fe91WZYlvV7Pjx0B6eR5ELIbBfAMZeBKKR+sIkfIKg43R4SFaa319yxzW14ja4EwmcPno7ByhT0qc1BS28M+EM9gkWePx2MvHQ5Zr/LskGuWjZWyLFlbW6Pb7dJoNHyK++nTpzl79iy/93u/5xmKS0tLPp063KiTJOYsy7x1gvhmPrxO/NOO9wWAqAuLjRegYNFwoFI8dTJTq1xRoIwAGApSBxI6tgmVdMuxDSRBNZpZz/Zx/mbVRI5wjAgqNkVtUSxJkZKMJUClkso2XDGet6KK+WOZd7RLjpw7ma6kYIqk2UZQNLRPgjy6ZGnd0bS2La3TI05EObsm5p3JJo/2HvD29ATL75TU92JqRwVWKxoPZugsx9QTetcc4671oAqcKGP//7Rf0NyLUcay9J7i1k9CNNZMt5wXXfuGJsrcNX10/RYGl74a4dJec2vQQF0ZJhVV7eElRSvXnjlwZGusqRxTMYIGpiShpBvP+NHNt/gfx03m7S6zIiKdWOLMkq1oD85Kf0aZK7rLdAEk146sl8mVNQckO1n7on2VsUw+M+avfPB/4d/91s/zP735gyjjkndbB4rsA25x+OzqO/zG/We8Z6CNQFdF9eC5DFtqNn87ZhYpVuIRvzVt8Kn6gCUN/3h0kZ2dHrTdPb40O+MKXmvBmAVQXAEBFE5OLnJKATXq+1XhnZeoahGKM8s0UpQN66WXVjkGYN7S5KeWkIAEZYES2ldc2nO2brBrc9ReSnNb0b7rPMVMrUrtNo6do3NVpQRbz4zVeQXmiEQSFvLospLegru3WuqAzJqTURb5IgRE51VgREUyVMZUcmLtmDlRBeZIwJByIB0V4KfKEuIImyao0ngJMtaiitIxBQ/7cGoTAJNWwOOirgCtvK2AT2INBq0whIEK/HdAb7i8eo9K8Od3ks/qfjCLNqvXSG7W4KNQ0zkr8ZiyU0JkWUqcdGyWJUytop/FqGmEzhRl01CThGLrQJPaoeU/u/njfL53n9+9e5nRuE4+TlDjmGSoKNqWL/zJ/4KT8ZBXZifZLbrs5R3eGm1SNg3jM5GTwc4dA9WU7j7KTo3JVo3hWU2ZurEQT0FPF/cpoTvO/NQFdLw73qT3dkQyspS1SgI7t/SiKaU11OLCebnO8Kw+Sbn2ARaF80GECmQXBmI1RygtceL8a+OpdcBP4vo1mlnKhqWjM5d8jKFvGkRYz0RsVRSzUVkjHrn1oDa02JEbVyZ2YR5uPLq1ToG3tGjqlLrKIXY+qGUdiC22hHq6KCi0riaLcj6DqlA09soK9HMAYZlWicA11yYmcYCfziEdh0C3m1/DUxE2diFhnqUMnhUcYcnzyDHj6xBtTui0Mj65eZNfWvsaudWcixV/9dL/TKIM22WL3xk+xb1RF50rmruOqWcSt2llomrjLKna3jhWe+2oJMoqSUoVZmKr1xYNFiCcCtjRFdHOsPBJnS9ZkqTEVMxTZR3ACM4jFEBHltJosiKmyCMS45ic6bD0AL6N3PPXxMrZWQTM9nisqA0N0dxUXrnumq2GvKko6taPA7+pEciZdeWROFsz/Kvrf8Cf/cGv0y9blChaesbF+IBHkzojO+evHXyI39l5jKyI6aYz7vR7THZbqLn7/NG+V1A0nfw7b6jF+AnXo+8f74vjYZBE5JnCaAI88PSwoXsICIpkrd1ue8BOihQBZYTVIcVUCJwJQBPKRuX9pfgTNlcoNZViJwRP5O+EPae1pt/v02g0vK9WkiQ8ePDAA1hHR0esr6/z8ssvM5/P6Xa79Pt9ptMpBwcHXpp6cHDAnTt3fGJr6LMnxbwkOUtqZavV8myyS5cu0e/3GY1Gvl3k/sOvD3tLyRECjCHr01rLcDj0KcvCHp3NZj5lVUAOadsQ7BVgK2StyOsFvBL2XRzHnlmYZZlPlH7ppZfIssyDqd1ul8PDQ06dOuVZV0opVldX+exnP+uTlb/4xS9y8uRJrl+/zr179zzDRhg9ly5d4tvf/jZpmnpwIASfv9eYkt9JEIAACXLvAoaGYLMUxOIBGLLMpCA+deoUJ0+e9CCB1i7R+ejoyEuWZSxGUcRgMPBSPTnEL/BhoPhhbzDxAgv7PgQDZQ5Knwm4JdcsIJH8jbSRjFuxBwjTa8MNBWmT7+VfGI7XfxrzUMaXnC9shxDceniMy1cBMiS9Fxzb97333mN/f5/19XXPNJVU5k6n4wGtLMu4deuWH8NyPQcHB5w5c4bnn3+e5557DlhIt8VDdDKZcPXqVT71qU/x+OOPU6/XuX//Pq+++ipHR0cA9Hq9Y2NL7BAkvCJM3Jb3CO9dAO1HHnmEV155hZdfftmvKZ/97Gf58Ic/7MH8a9eu8frrr/v7FDBY2lvGnEh4w36StTdkwYrnpbSNgF2ypoZzSea/rKsHBwf+/xLKEbJo5XkQrtUynvI858aNG3Q6HQ9gyYaRAM8hEC0enfI+4jcagkfS18KSE+BJwj/Et1U2RMI1VuaI1ppPf/rTdLtdH0wjUuH79+976bQwRJMk8f2dZZlPOxc/SAFOw403YwyHh4dYaz2TT9ZUud+H50F4rXKvEuAjbSztLuNL2lusJoTNaYzh4ODAs2l7vd4xRrucI9wU3N/f90B82JYPbxSGNgjCKsyyjEceecQzv6fTqWfE37p1i29961vU63XPUpf08bt37/L6669jrfWgtsxpeY9/IVOYoxlY5VgTeVuRtyHO8Oy5ZIRjH2aO/eIKGvfBPppVu6pVgZGtKVShqB9YijokE1ecKkUlSYTIiIRPeQZYPK0WhtxS1LVnyZWVJNqbylchL1JoibxKmEsmclIrf37xQjOw/KYDXUyiOLd8SFb18+XGAw6KFi+vuIp3/xnL1tdc8EjerKPLOiaG8ZbzY4qmjqGWt51sq7lryJZqDiBT0NwztE6MKF/pEU/w8vBo7ry8Pty6Qd/EnIxKIhQza0hQ1JVlYp0XYqoMM+tkc+vRnIFxsrDMxjR1DgYmVrFUze3SWg5MxIfaN3m8do//WX+IoqGYz2PssiK9bT3Yq3McCqkXoGGaQb1vsArmbU3Zc0yyaOaArmwN5j1L/dwRsxsdeu+6Dvi//te/RL3uQDVTN9TvxzQeWObaMF8uOZ/ucnfQo1kBZEUPio8M+UtPf5l/vXebH37jX6KYnXCBLCrnd4dPcDH5A05EmkQVUGh0wy1A12frlVzXONCrlmAaKXl7YdYvX+OpJTqaUyaavB0vfBOjiGg0w+pGxZ5R2MgZ9JdJtXuaWdLbh9hagjpZ85Lg4pkRaVqgpynFKEGXyhfTyoCpRY7NaWUsum/8OLcOWDGJJopzSr0AOyUx2zO4ZnNsNoMkpmxUu7CzCpCs8BUbO5wxni7k08TaAREW1CgmHsv8CAojC2iNOhphdvdR3Y7zcytKVL3mpNBao3rdRX0ubEIPcFTfxA/5YijlwVwPKMjGQQCOyCGAhMx9SWL2QKUB8sIBxsD6dwx7P9PhRDKgF02oL2fMs5i1ZIQZJ/S+E1Vemovr3vuUwaqKWakroDZVPN7b4f/32oc48w8iWlWyeJTlRLlh94MNVjR8aXKWv/K3f5HWPUMyNYy2IvQlS+u2Yx6XVVp4UitcKNBoTvNBRJnWyNuVH2XsZNXxIHKgXQXeqwKUthgMw8KNs7yjquAid/mJKoiU9pJQk4gcHBo7bq32QHSkKBvRwme2Ynp6Hz2lyJvaS3etVui5Y0KXDU08VmwXPS6lD8hsQldlPkTFsbUtu6bJr7/xEZpzt3bMugIOV+C4MATDcRMcmU3AqIpBDjayaKNI44WXTaQsKrLYHGzh/Aqnq1VQksXPAWePUQG4kaJsO4brvOWsMkws3poV02+ufDq0sP+8UhnLf/Xhv8/qc2NyG7Fd9tgv2gxNndeyM3wkvcc7uebPfOP/QDFIIbYOpAeSupNIew9EI16BDge3jgRPUbdkPY1Zc6CtSVxAly4dozvOWDDqlHLYoTzvqmuUzY58tXB+kAYvnTbVQ7FEEyclxij607qb26XbJJr1FPOO82m1VZ/JM0HPq00PYxxY2LDOPmE1OmaB4GxMIJ64DQObaB9KI0nUJYrCaCihfnbIjXyNX739SW5cPUHjdkzzgWXwCPy1P/V3+XS9ILcRsTbMi8jbnJAamLsPCMMzi49a0Qziyoe5aPL94312hL5HsJB0hQwkMaEXNpdICkOml/ytSOIEbBKZp4BZcs6HWUnys5ClJMVeyGSSAlkKnfD1URR5Blie556lId5fBwcH1Ot1nn32WS/NFq8mCRFYWVlBa+cPt7297cM25vO5Bws6nY4vrlZWVrh//773tBJZ7927d32YRWg2f+nSJe7evctkMvEFLXCMTRe2jRSCIVj5MJNL/kZCCkTGKgwZaatarebN/aU9xVtQ2J3ijydSzI2NDS5cuMDTTz/tQcqLFy/yxS9+keFwyCuvvMJbb73lQVJhWw2HQ9rtNi+//DKbm5uUZckTTzzBD/zAD3Dp0iVGoxFXr17lYx/7GN1ulyRJ2NnZoV6v02g0PPA5nU79GK3X69y7d8/7SYZ+jCIJFjaNtK0AONJ+wjyUdpFziLeZeFeKvFRA1c3NTdbX1+n3+9y5c4dms+nloCIbFeA7ZI6GIE4Izsjck3aX8S8gvbyu2Wz6fg9BupClG0pXBWifzWasrq4ym82OAXVyhIzKkCUVsgulDQVAClmPIYswHIeyJoT3L0wk+bsQTAlB7IfBy/Ccs9nMBw8999xzvPbaaz6dV1hS586d4/79+4xGI+7evcvR0RHdbtcziMMwKFnT1tbW+O/+u/+OBw8e+PuJ45h/69/6t3jmmWc4d+4cFy5c4K233vLrxGuvvcZ8PvcgmmxKiGxXwJmDgwMvMZb1QsBJYQxK35dlyYMHD7h586Zf94SJGG6kSLvJBoWcV84hQHfYz/I13IyQ9VL6VzYKOp2Ol6eGa64cTz31FJ1Oh29+85uAS3cX4EdS38PxGgKccq/yDJG+CJ8l4doXymLl92IjIVJa+b2AtAIgyrNqMpl4gE/uO3yeyM92d3c5efIk586dY3d3l5deeolms8nJkyf95sulS5c4efIk+/v7vPnmm9y9exdjDOvr6569OhwOGY1GXhos4Bw48LjZbHom/GQy4fDw0G+8NZvNY3MnPMIxI2BdyNh7eD4JiCzrpswReWbJM1meC9J+YX9JH544ccIzMEV2LRLuUO788Hw1xtDv9/3z+NOf/jQnTpzw3oqyiXdwcMB8PufKlSsMBgPSNPXzfXl52T+jZdNN1v4whOePcvzzDyBWa3g0d35q0cySjBTpkSVbVZjUyc+iGdT6llnP+S8V9YU0LJopVLRgf0WVFHS2rJgtu0Ijmln3wb9iJVKxC+PCpT/P24o4cwm1Jq78+aZVcVNWC5UkA1dMlGhuvcRZAj3Gm5pkbIkninnPefPpqa4kko6ZOOtq/sLmiyQV4eL18UkaUc5sRRFNLaZuGG/GFA0HqCZjx5jBOh/E9ghGW4pk5KS36ZEiW608IJtg4oiydMEaygIzVwjGM8vRRc1mPHBtXqUwAyRKM7aGzEbeZ6ymoKkKKjINczRNnZNb97WuLEPjXpdWk+tybZtElSjl7ne216BWefBJoSiMN5GtJVNDUVOuSFcLWdu8C0eXLWee3ObfO/+7fG7/Gc42Dvn72UdJv12jvNNkfMaFeyRHmnhbe/AgrRWUy3PWoyHjvSZJS7H203f4P579PX6mdcjEznk3t9y8vs5mqrwf1rio+bZJVQmlcvI7YFA0HAjZiGBryY2Bae6K4Hkg140qEKPy+aKSvulRBkpRtlJmvcqnrGkdi83qKhzIMblsHGGjqGLsOcnmfFAjzxqoQhHnrj1VFYKgjJMmO3Ytx5h2LlTBVvPIUNY1SoGqWGvK4hhKVfiHMmCTGKU1NomxiUWpBaMKW4FPFbgQzStGZlIhigooXJAKRPiIWyogyVoneQZUuwX1mvv7onrIG4ew2CReAIXy5aF1UX6/YBcrz8IEvOzUe5ZW57BxcO/VId52C/Cv+mUcoaYFRBHNexk3pqusJUPqKufC2j53Bz0OihYqVy7oiAV7K56CyqIqLdY48M66a6npgigtGW+lzDuKxq5l+c0xepgRPVanriIyk1A03HXmTc28pyhbJZOTsev7aq7k89gFcDQTxps1Rme0B4ajDNR84c8q96gs6NhWSewVW7u0aBTJxP2/qV2BO8mTRYCQdZs1s6UATJtZdO6Sq73Hn7XHvRABG8UoE1HW8D57zi5Cu3FuNR09rewUFLmNWdJTShRDm2CsRt+tE02p2Mpg9cKyYrbkrk02lARQqmwdWdIT0EFqb9VZURVWM7bp4ucC7uWKxkHpA7asdutZmTgbAVU6drueujkUTy16KNRcBy6OTsvYcgC+BOkIgDgwNf6zmz/OezdOoI9iTMOgp+4iTz25w59/+h8ytqljmqaGqFFQr+dk05QoS2k+KFHWsfkkiVnk7Tau5OuZIh2bCrSzPpxEQGEnu67mh62YktFi7hbWsemtUjRXJsyylKR0Y9lEDrDLbcmDvItSllpaUpRuraHQRHMXQiKbdS7xfMECnvc0NjIeXI0yd+7GnlkwmXHP36KhKVruOUu5CEezi8/1zE2MzhVPbOzw9+58gp2vnGLrXUMyLtyGTcuxW001OFvxnAdlm2meMJtVO/KxA2Pb94rKDqBiS6bq2Ht9/3j/HFL0hkWdmP4DHlSR4Alhf4TsubAwFWaVsLGEddLpdDzAJmyE0GMqlLtJESgFiiQ7S0EaAihSACul6HQ6x6STjUbD+xzGcczu7i6NRoMLFy7w4MEDDg8P6fV6HvDT2iXhjsdjzp8/74Ma9vb2fHtJuMT29raX+koxJKyLkydPcu/ePVqtFvfv3wecbGw0GrG1tcWXv/xlX6BLH8g9CRAWAoTh/+X7h6Wp4m01mUzodDoURcHu7i4rKyte4i0FplLK+3MJeBMmcJ4+fZoPfehDrK6ukqapl2Hmec6VK1d4/vnn2d7eZjAYMBwOieOY8Xjs+/z8+fOetXL16lXG4zGXLl3ip3/6p7lx4wb/6B/9I27evMmTTz7pgROllC+EBSwQYESkmUmSeGZR+LoQCAjbKAwmkPMI8CfjsNfr0e12/VgTFqmwmcJ27/f7fswLo3V5edmDMZLo+vB1CLgjY1lr7QENue6QeSXSQ5k70ncC7oWsYXkfYT6GPw/nRggCSnsImCq/k7nbaDSOsS8fBqvDI2QLSnuHLLsw8TZsE+m/h/tNfh6CiHJ9s9mMO3fu8Mgjj1Cv1/1YODw89HYDAjBLX4cAQwjYNRoNzxK7ffs2WZbRbrc5ODig1WqxtLTkpe737t3j13/910nTlGeeecbLVcuyZDKZ+LaWMSXr1/Ly8jGftpDpJSCh3GMocw2B2xBUDYFBYdCGwKyMl1DaGa4XD/9M1m1ZB0JJfzhOwg0aCSw5PDz0Y1Q2VUI5bdif4XgI+11eF15vyI4N54OwnkPZr4RoyGvkOSGsX9kcATzoFG40hCzmtbU1DwS2Wi2Wl5c5PDzk+vXrfOxjH2Ntbc0nmr/00kveM7HX6/lnZZqmnkknHsGhtYB4+B4eHnrAUUBD6Q+Z7w/PMRlf0mfz+dzLpyVgRq4hfK1saMjcFBBcvBzld9J3D/f7bDbzm11yzTJOpH/CNVj6OExTFxa4hB/t7e1Rq9X4yZ/8Sd8/woaV65RnlqxHwtaUNVQAyn+xGIjKMa7iSsoKLBh+CtK+K0QlHVaOol3J9+budU6+7EBExwCrvlfOC79sVOmZFfDSvVmgCktZd754Tg6tvZ+XmLonI1uxEitAMbcUNc28q4gnlYdaJaGK5pakYlwVTchbCybPAuhxoODF9AG5hQTDq/sn+fjGTWbLlnqpiCaabBUau+5c6dAyXVMOQF1zIKRJQRlF0bQ+jbfWh2zdUjYgf9CkXoGyAvgk4xKeHnMpOSSrqp8IxdAY37h1VVaFu6apSlJliBT0BEiwMWObsqnHaKAizdE3MLYxEYaWKphkKd2ZC/OQsA3xCWw+WBj+503NdGUBdqBh/yMl/+WP/BqZSfja0aP87q1H+A8+/y9T39E892d/k3Lo0jXjseuDaLooHK2GeU+x1TuinzZIMcQHMYPHSr702P8IwE5puVfW+P3x4zRvJJSpA9eSSiI2QZBcAAEAAElEQVRZV5bcGuY2csnDFcp0dbTmC+xokKFmc1RpiBou1ViYXcJ8U7Ny4W2YwPzMMnpWYtLIeZ/NIB0o0tsJ8WQBbikLqnASX3Bps8ookr2Y9Egx7zkPTT3QRLklnrmUZ2FfhaxCnygcsO9MqtDawnwxz6DySaypiqlrHIipnYZ6Ok2pZa6dda6IMyfzz1tVcmtp/PUWDaBuoFiAfyZWRJlLaVbgmINao9LU+R0WJSpNwBjM/oH7szRFnd7Eah0EOnyPJcTaKlX2f33h1LkBLXYITvosfqXibeaApQpEkaTWENi0lrg/5XDWpFOFejy7dAeATpShSkXrnl3IwKvxTidH7QRoQwVqdaIMiwNu8q6bvyjl2qBiQeWVDleV1oVdpBBNNI0d5xlrEihaECdldR+G9p0Mq+suUKRRJXanFsbuHhyo6lipOiox1pKVcdW+EhDirn8pmpDbktI4Bp5J3dpmY0sydgwsZaqNlFJRJs4DUUChUAaLcfPdhmByNe6UscxWLOfSXYamwUo0Ym7dh5AS5Tc1luIhl567xa2jc0Rz5UE9ZSsQs8TbDFjlGGoOQLaU9jj67MOOLKSVf19mE1QVAKIii527Z4KkF+vSVunBEGcGZRebQ2XLYJKIeawo6/q7GXa5qvxNF1YAkkY/J+K9K1t034oxNRifrUC21HKy7TZ8dosuSa1gVmhnJwGYQlGmlvGmGydWOzahs/CgWvtlc8k9x4olB4KJxYJ4mcZTx0A8FuZUgZxlMLfKmmKpNeXeYYO0ApR14TxyDYbcRBijmZXuRK3GjGmjwEQxs55yidbR4gOzhKJFmUVXa4bzx3Xv6X0bExVsdjjwUJcuFMqkbkNNQGNjNXkZYRLLZ1au8Pdvf4SiaSuZu2a8qbF1Fwam0bSjzHsmzooYU2hUbLC4OTQ66ViTYXt9X778/jwEHAwlrsYYxuMxnU7HM6LCokukUMIsEL82YfU0m03PfOr3+54xIgWNMM7EI1CKEJG3CetMCtvQn2o+n7O2tuZBK5FKC4vKGMNwOPTMrdFo5AMJwBUqKysr7O/vMxwOuXDhAr1ej1u3brGxseFTSUXG3Gw2PStR/KfKsmRlZYVarYa1lqWlJQAvf7x58ybtdtuzLwRQ2NjYoNPpcO3aNd+eIbATMm2kTUKPuFAKGYIOgC8OhZkjLI6joyPPwFFK0e120Vp7YEOAGWHaNZtNfuqnforJZMIrr7zC5uYmJ06c8F58ly5d8sCFFLOHh4fflbot9/7EE08wmUw4e/Ysv/Vbv8ULL7zAYDCg2+2ytLTEtWvXePDgAcAxBiUsZHgyRiXwQkASKTpDv8aQARhKJ4XxI+EBwtQSL70wKEbaU0C2oijo9/sYY2i326ysrHDnzp1j0sGH2aLyT84VSocFrJN7lCN8jYB5gC+WxfctDIP4XnNTgCoBXmUOhyBlCNKFLMQQEJC5GTJWv1fBLoBUCCaGYFU4VkPQ8GHWYbgREbZZ+P9+v8+lS5f83L537x7D4dAnhIdS1zRNOXHixHeB8jIfhR0tII9IV6Mo8jYOIYgZgqMi1Zd1QHzhJBxF5lyz2aTZbB5rC7n/h4NKwvcThmjY12Iv8TD7UMa9AHEPs+seZhJ+L+Zoq9Wi0+mwt7fn2yscl9Y6Oey7777LM88845l44/HYM3VDUCn0GgzBpdBjV34WyvTDsSxtUJaLEA5hCIvPqlynzD2RF4/HY59A32q1joHbsjkVgrlbW1vMZjN+/dd/HWstt27dYjgccuLECZ599ln/t5I4HQK6wo6TIJpQwhyuU7AAMiVERYA5STgON+XCI7QAEZZ4uLbJ74SR22q1uHDhAhsbG7Tbbay1HBwceIafpCELyCrPsnD+Cmgr5zw8PPRjJQylEkBSLBvkc4G0q0jJd3d3uXbtGrPZjK2tLe/L+72UB2IPIWCibBZI28rcfZip+U87/vkHEMEVILklHVVsuLHzvoMFGCKFjC4hHluauWMXRrmT8NnIFU5ivm777m+kiMq7luTIAY71viEZFZSpdjLbjqJsuEJLl1A7cICSSBGnq87rEOtAh2xFk605cDM9ctcgEivx2lNldR2xY3pFVcEhrJOumlFXMDARR5M60zKhrEH7Xkky0ujc0nyQE4+LKtW5RjwxtHY0tcOcaJa4gJX72gUQHGriqSEdxuw+Z4gHzvOwrKkqedIBEJ89f4US5UBCDJl1ycuJLYlwgGBmHHiY44r3BEukLAmWmbWs6im51QytZTWylNZSV9Z5lKmCnnbps8LuyjvQfODAVQEWJJlYla4wHJ+xlKczkqsNSCz/ztd/gfRmzQUcKIhTV2SeSfdRcxcGUt9zcllhniZj18YmhatvniQ50vzJ2/8mK9fg8CnFjSLltewMv9S9y8RmfOfoDMrAvOvYJKkqya1GskTv58voTBFvuAVsmNeJp4bkaIbKCwf2WMco8lK/gOVVrDaYLcUVsxbi/Sl6OEZtLKFMvfISM8wem9F8tUH3Rsm844AHW0swzdSlJysHTtoY6nuuT4suiKS3THUVBlKlsAr70C4AMmHhuTmlHNZmFgw2XTj2knjf2XrqmICxO6HCzbG87cD7OFPkTdduLujIYHWCTWPv+Smy+RAscD9XjoGYF5DnC09CrcFa9NoqpEkFZjmgEUlODmSf/jAC/KkFYC+Saflsp5W7nwo0tJF7vbQXuDXIMRUXIIr9/7P350GWZfd9H/g559zlrZkv16qstbur90YDaAANAuACQSS4SSQljq2RZMn0mNZQHo3DMsmxRnKER4ygaEl2aAmLpkKUxlJYBjmWaFqmzUWgSIAkNiKAbjR67+rqqq4tq3J7+fb37r3nzB/n/s47mWxZpCP8Txs3oqJyee8uZ3v5+57vojW6rAI46RLNrEq4lB6yW66S6opHV+5TOINtWIaXkhCiIyxNN0qCT5yAXU4rUuXTfMomPhCnHniqsn79QVPV9K+QHO28v+V8XS9DHSZ46WUCtpEyvNQIDEQJnAjyzrpfnPb+cEliSZXGOu0tBVL/urLprQW2zRCLZV748KiiA0XHS3fLFiH51gmAXbmaEVZLwSVB29RMRJWgarRf2r3KtJfZ55aGKjDKMrQNUlXRUIUHg3CkyjKxCRfbfW7ayz7ZfmSXIS7Gt6W0o01U1P8OozSpKsDWP6+Z0DjIjX+Qg7JDZTW2qAEk5+dIa8/6QKya2VelqvbAJVw/GRovBZ47GHpQVNis/U3/zMqBrceYKsF2HYWrmNgWaEfZrtul9hp0iaVt6sKqPpkyDms1VQ3QJRNFe7c6KSVP/fOXLb/joawmHXsgvVEz6wVkFSbg+LzxzVUDwGGeKajwnoJoPwY66QI1Nf6cdQJ9w5QUruKp1m3WnhhzOdvng/ltNo3hb+x9E//TK99CMnGkdUL1kqHp22V43lDlNgDs/u+AOjBs5MF8+UypUkXZhvxQUawkZAN/skWX0A7TMsV1S86kfe4frpDMfGcXLcV800GpWdRj0TpNwxQYbZkulmnSyiqSMbT2qjrcjWAJUba+QUF8Nx5S5IlBuTC1xD8wTlKMwZnNzc3AQJOCU6TCcaKjsLIEYBFvtvF4HFhCwnKQAkjASPEWE98j8ekTNp+k/sYFrLAeYyBKwAABKISl4ZwLqZh7e3tcvHiRGzduBO8mYR9KISttIQWaFC8iZU2ShIcffpjBYMDq6iq3b98OkjNrLe95z3vo9/vcuXPnBNPjdNF9mgUCBE8xWDKZTnv/xcBBq9UKQTCrq6snzPQnk0koQB988EEeeeQRHnzwQY6Pj/nd3/1d3nrrrSDdfPPNN/nMZz5zwrdQPPzu3bsX/LTKsuTixYusra0xGAx45JFHQojBiy++yDPPPMP169cD26/X6+Gc48aNG6F/Y4l7DBhKG0kfOOcCG1Bk6vJzaSNhEIq8Wca0jBN5/p2dnROsKgEVYuagXFvOe/bsWe7evRvYs7EUM2Y9xkd8DlgydWP5aew9JwycmFEb+7cJ4CdgqowLAeEFTJR7Ow02yz3K8wmbLGZTCbgl4+w0+Hf6fHKN0wyq00d8LjlOA41y7tjXVJhXIpeXPpOk3SRJgs2AhBgJ01bGxulxImND2lZYYALSxe0UJ8sL2zcGv4TBKgC1AOKyPshzxsxaGe8xOzFmpMXyb4BOp8Px8fGJc8Wp2O/kiyjXE+aqrPnxtWUcCVj9TmBxVVUcHh4ymUxYXV0lSRI6nU5glAMhCCRmHcbPdRoYk2vHY2tlZYXLly+H3wkjfj6fhzTyPM/DJpGMzzjxfnV1NXhSytopQSsC0Md+fdZaBoMB9+7dC/Jxud9YRh4Do3KkaUqn0wnMYmHXa63DOhyD6wI8i+Q8DlGJzx3PBQEJlVIBlJYNofPnz/PQQw8xm82ChHp7ezus/cIElP4zxrCxsRE2vORzf2tr68R8E7ai2Emsra2xWCxYLBaBye2cl3/LZqBsjIiPbKvV4vXXX+eRRx4J64wEewmLO2YwSiiVjB8Zt7KZJ8nq8nfC/7kYiBAYSiEwwhFCUHzoRr1zY73ktcoU2cjWaaaKbArzdV/oFZ3a2B9HOsYHcDSWxvVlC4q5oso0NtWUDc/EcApMzWIsm5KsSc3GUcFofr7qCxfPdPMei6VWtXeYLzbkmczMs0O09SnOPg3W+zxVKCbOh5EATKsUZaGxX6CqlKNHDenUMDmTkk4sw4uG7k2YrWqqNGN4SbNywxv5i7xRV45sZGG1ILvdoGhTp89COnVMNzV/bO0rzJyhcJqxLbH4ZNOGUhT1wOvqqmZHQuE0lXPe30pZqprmsm4KxlaToljgSFkmpBo83iJMpsVWwfrLmrLhmZvevwqmW4rud+7ynTuvspP1+XuvfZzRekbzRoouUxY9h80cZqpoHHjA4gP5LmaqscYHkwS/OQfDBywf+MgbPP/bj7LxnGbeU6TDxAcXNCsaquKLgyv8eyt3mNiEpim85Dv1QBd4kHDmoMJSOY2ZK9pN/0fcvEw8QJwn6LFCtN26skhyqPyzBlThAwDSsfFBO8MxKEWVm8DMsk2HG6foBcEXDICyQk8WvmA1eEDBeBDYJv66yahO4a4ZajbRS8CdGqhKFFrMNgUE07CYZLRGHlAXhpKy+FTdCg/uGYNTCt0uSDNBipbTVlifunBevlxfw4oKtFIn3hOARFngEuOTrKu64pevnYOqAh35jk1K7Gpagyq6ZuoJOKVPgKMCMgiQoKzDUSewm2Vya7g3ua0oZMm/UaFsVSdCV/77WcHusItWlp6ZcCW/xyTNvT/mRNO55QJbylshgGpVKHdyqXYGtLLYYsnIiw8BQq3Tof1EkqpKz7YSsMwmoLX1LO1ZQeeOoujUKcwZlAmebSaPrDwwohxo7RlYwyIPoJayPkRFVf4eAWbzlNwSgohsw2L2ayly3Z4e3NQe0DV+gHvGnQ79UOXaMx/VctNFPDsxUGBoKz/fDA6tLBk2sKIbqmJcZp7R262lw4VnHgargIUCS5A1n07VDsB56dmcuiAw+j7SvMGPPf5pL502E/7ejU+w99p5Jtu1gXW93iTzOmEaD0ZWqQ/LcUYzX1UBbFSleCVSA76erQkEO4yJW1DRDhsCMbCJUzRNgcXRr/yuvVvUa0DNsK8atUdjLUnGgilqy47pcsKWLQ9oLjoe+FyC5D6dO5n4z0dVeaA9BJI4v6ZnugTrA1QudY7gSZg+mrLdGrKWTflz259Bo3kk2+WV6Tl+a/o4/8PsWe5Puty8tUHa8Rsci7YKvpW2BpZlE8PMl16HVe7lzmVTgrR0aBez8J8LPsHZYnvLeSLPVTnFmTPHDKsm5Swhn/s2L5uKKvNrdiZ2HbqoPRATFgtzAmyvGjBbNZ55KywS55ZhRN843lVHDDyJZEiYWcICEIBDmCOLxSIEkEghLEVgzAoRxp6En8AysTVmR00mE+bzeSgI1tbWgCWoIEWxgGzCkpHiXVgJIuWSomI2m4WEZAk5ia8Te8eJl5VI5ETuJqCASE9Ho1FgoknBJj57YtgvxVOv10MpFeSqV65c4dq1a+zt7QFLUCRmWZ2Ws8YFUiwBlX6LAzfk+5j9JWzEqqq4e/duAE2ffvppPvrRjwa2jdaa4XDIbDbjjTfe4Atf+AKz2SyY5gM89NBDwfBf2mh1dZWNjQ2eeeYZPvShD7GyssJLL73EL/7iL3Lt2jWcczz++ON85CMfCWOr3W6H5z84OAggiLSxgCnCgIvHX1ycxyCMPK+AKjEjVYpRAZ0ESJL/xVctBmmlLYXdKH0hgJtIAIWRJGNb7jNmUcX9JX0rbMK4/+J5Id/LfJLnOs30k76RRGUBy2Uux6w2mZPyfDHYJPcas8ZiOamMg3g8xuM1HqMxiBr/Pr7OO73v9CHniMFD8ZcDD+YIo7Tb7QbJqIBQAnLEzLcYqI3nX9xX0t+xN2MMloo8VtK1BQyWMaeUIssy2u12WJOEoRWzLOWeZDxba0M4RrfbpdfrMZ1Owxr1zd/8zRRFwd7eHnt7e9y6dSusR3Jvp+XLsefmO7VvPK5kHMbgbgz2vhNQLGM03ryRtpM+igM6pK2zLKPX69HpdDh37hxA2JASIG0ymfDoo49y+/bt4LsnoJcwR+UawpBM0zRsOuR5TqfTCfNDGMbS77J2jMfj0IenN2ROt5287zQbXO6n0+nQ7XaDzDZuX/kcksAlGZ+yqTKfz8Nn2ul2jxmbly5dot1u0+l0uHDhQvhM/I3f+A329/fD7wXw1lpz/fp19vb2cM4FYFtA2E6nEwDDGMwDv1ExHA4Zj8dkWRZk13KP8hku55P2EoZwHEwlY6bT6XD//v0T80o2CCUARwB7ARMHg0H47Ja/O2J7gN/P8a4AEEPARF1QeKahC3+cSxKyFIpVqmpw0Zv4m7kjq9mFuvCsBTPzEuJFz5EOFWrhC5SqCdVUUXS85Gu+VrNBaiN5753o7yU/8v8r5wGIKgebQzLyRYqZweSMN9GX0BXlvAxrvlafs/BA4vFDhmQMnbuVDzUB3ig26OkJWjsO522KtYrpdsZ0XTF+sCQ7NszXFMnUsOjipb7GJ40WHRjtaJKZY97zv5+NkyCFTcYw+OCc/HpO644jHVv2Pqg5nwwYuyRIAr0M2QOEDaWpnAcTZziGVqNr9qEPUPH+iAIwNpSlb2HmNN3aQ6ynPa7maomkcqDbJbO1hgdVMi/h7v3bt/gLFz7PP7/3If7pr36c5j3fvvkHhnC/i5lB63btM6V8Wy5WHZeSDjgP0GTHjsWKYvShKT/90U/xgfyQXGme/e0frUEc/y+ZOVTuAcRcl1gcuaqwTgUWV1VLmNezCevG0FIZ/+Hac/zsxsfp5h5dvH/cYSVToVC33QYoxXQr85JyGUelB2zNpECVuR+7YiSpdR0i4l+b73kwMZ0s5bNmYVFF6aW7wpBToCpfbLsUHzpUQTayIZgDo4LHpCr8+DSzJQsKwBTeb7K1MsOmPjijyj2CoysXAoBUWdUMxCYmrSgWCY05oWjWhW/XRdcDKC71XodVM/XgRqVq373oj6uqlhpLWygFi8L/X7M5cQ67f+Cb6swWrpVDaTGzkmI1rZl9UWozwtjTkd8hiJxTIV/LJoQADELTXALQIrXVizpN2DkPGkYSZoxmOk8xWMY257hqcznb41/uP4kzMLq0BE098xfc3IMbARyu+2PVTAOOadNacqsULjE+iVf50CIJv5HQCrQAPTW7UMHCqdonTjG41PCeqDU70cwc4wcIDEbfPn68ZUnJyBV00zk3LjpsbqFX0OrMOb96zGNpiSbjv/7gzzF8psmGGTF2Gf/lte/m/r1z1CTK0H+6qBlkNSMWgLlQ42qvQr3ccRWmm2fdVnT1jKFt0tJzDJX3PNQVY5v6pGKxXphDZ7fymzvKg++VyKuN87JiGSIOSP29FM6Q9Bb0n4Yzlw/5dy9+jWea13kyOwI6GBz75Qq/e/wAbx5tsn93lU7pmeFlo/a/yz1rHRlGNbBqxp6JnI8dprBh7tpUMT6vwjiLAWObebuEvXJlyV7G9zHaf9NO5mgUw6qJ1g6VWkxqaxaxIh0pOrv+D0dbg+QebPYbTE57gC4ZK2ziyMbeisOnqPtxZ1PF5KwHzjw7dzk3XOIZiIm2qEphz8/4c9uf4c76Gs9NLvOv7j7GV159kA9+83Xek77Fz97/Q/zm1UepJgmmXXuSFRozUzjjmehm4DcIg0+mdRw+kVI1os/9maJo+3UmLcEcVIEdvOhoiq6jcaBra4Sl7N4mHvhOteWZjdvcK1ahVKQDz7Keb3gmJcbVjFfPBk6UpSw11mqSrKJcGCgVydQDxqaQNcz36+TMsp2+cbx7DiluYk9DKbJPM4mEGSYg2Wl2YMzYqqqKXq/H4eFhKCokZMNaGySAIleS4rqqKo6OjkLhL0WcFDcieZNCQwzih8MhQAhPEQN9MZoHWF9fZ39/P7BKut3uicJPWHrChBPpliRDSkjDeDwOzButvW+iMYbt7e0gfX755ZfZ3t4mz3Pu37/P+fPneeCBB/jUpz51Qroce1PFUtDT8j/5WgAIATfkiAvVLMsCYwWWvo3CPHzooYd48skn+dznPseHP/xhLly4wBtvvMHLL7/MfD4PfnJJknB8fByuu7m5GSTczjk2Nzf5xCc+wVNPPUW/3+eFF16g2Wzyvve9j7IsA1Ol3W4HieTR0VGQwIuUVwrHWFZ4dHQUfi7gmDGGvb29wDASIE88x2SsCNNqsVgEZpAADFKYC/AjXpani2ABCmL23en/47Ev3qHSbjFQEYNjIvGXwlqeL2ZFyv0K41bOGQOccm4BCeTZms1mCLOQ8Xvaey4GJwTEkvEXA2sCuAuwG4etnAYST4Pd8bnkejHQFQMzst4IaCtBSKdZfyIVFRbWYrEIPqa9Xo9XX32Vfr/PeDwOstD4WrFkWJhq8QaI9GPMQBQpqrwuBoPEa1SAIRmjsgnRbDYDQCPPHVtCCMA3GAw4ODjgu7/7u/nu7/7u4CmYpim//Mu/zO7uLq+++moI6RgMBpw9e5ajo6Owtsl547Fxuq8ExIz7RsZB/PynwdwY+IsBWCAwydM0DfPo9OaGnLMsSw4PD3n88cf5k3/yT5LnOcfHxwwGA5577jmOjo54+OGHuXTpUtisEYapSJgFmJX2iX0/hYEoDFVhagrbOV5LZAzE3olx2JU8q4x5+VnM6JVxIf0qFgkiCW61Wr8nBExYssKCPDw8DPcoa5WsgfEhPxMG32g0otfrsbm5yfb2NuPxmC9/+cthQ+fVV19lf3//hP/i2tpaYPOJ53G/3w/S+M3NzRPXlOeTtVbkykVRcP78+bA+i5/oaDQK75V1Zzwec/HixTB/RRIt4yX+XJdxJBsbsXpBmOOSet1ut38Pm/XfdLwrAERd1d6CdSKvMIDMokZiM8+GKBu+CGv0rS/w8YBh2VAkY+8JFjwRHQFIFO/BZCrAX+1jVDrSoZeEJhDAJFMtvRSLtg8xEe80uV8z9cVD2XI0DpQPWLH+WkUunle+sPVJ0tECpGBoG/zW8DHW0jHj4ybFyoC0N2PRblPlCjPSzNc906joeIbcdFvRuuuYbyjSMUzPOHqvw+SsL6hnW45kpHDjBOUgv5YvGXoK2leOPfhXS9+6WnFoIa0rtgpHVxuK2itsUgONksBaOE1DF8yconKKRo1MNerE5qFNMSzIFVSlCWyRvFFg0ybZwANOg/fOuXZ9m7/2hX+bxqEiWYXZpgd657st2jNY9GpW6Mz3Wd4Hm9c7hvXf8P3H4a//8X/K/6Uz4G45Yq8y/PbkYfIDLyOvmjA7X7DyliHJ6wIbReEqXl6cZVxlYVwUHejqGZ9YfaV+9gWfnW3T2E3YfMovAsUiqb25Etju4LQiGcyDJ5aMH8+MdGAturCoSnvz/V4Hl2jKpk9L9pJ7RzKumYC29gBzLNmCyjNIPfOMEJySjDwjpsp9+I9nd5maoQcu9eymqqFCUniV1cysVJGairnz43zJTKvnjyMCshKUqigLvQTlnH9Om9Sg/cKiZgWkiQfyEo9UKatO+JZ6EE2h5O+kosQtClRiIEk8CJUmqE4b1Wjg0sTLl63F5sulThhtS2ajRpUWkpNsPvGTtOJ16AhSYAGlbbosOATwSUclyWDmQdFyCSSiNWoyoyrb9PScmRkzthkbZsTCJmR9701YNZbXqDJQua3noFo2toK2nns5LRHrsT6qzL9uVDVC3xVdRdG1Hpisx2yVLyW74MHV7q25T2FeUVQNv4YFZKoGr4Wpmhg/h//9c7/Nb/zhA0pr6BdNDuctJkXGsa2was7fevv7ee2Nc5hu4T1InaIz8+ys8Ei1dNlJmwp4LEnMlQvegAJm+jfWycSplzA3TMHMpaSqYuaSmnnok3JbdVJRlcNkS4fPBg+I+XWyCIzNJdNPJZbCVTySzvirH/wl3ppvcVw2uTbd5P/31gf4vz74VX50/VX+9v1v51deexI7TnzKcenXktFOLZWrpehmRgCSVD0/bep9fBfdOoFZ/FBDeJRCV3bps1kt+71ftcDVkvpT4S+rxjMzcl2gdb2ZZizGWFzlZduTDeNtP5SsI/XmVeHq9GefamwWjrKpsW0V1gprvJevmXpWqzAQhbWP9Zj3ojIoC93ulP/ga/8u1e+u0XujIj+uuNDW3PvwKhZLrkuStKRKvFejdQqMo2x6RqEzMOvV64khhProBeiZ8uOzZl7nxz6hvmyAM16m75Tf7EhqywozLVE2o1jxbY3yzF3rFO/t3ORLxw+h5j6kat5TFCv1A2lHQ5VUzgOITVPgnKKaJVQAC43SjrJZA4+9+nMlr20cxh6g/8bx7joEtIiZN1IASsEsclDxHYrDF2KwRPyZYumksKDi80vRICBXnI4q30soibCeBGyK3y8hB8IAEUP2+HliqaIAGQJUrq6uBvbF+vp6kGodHh4GX8OVlRUODw/Z2toKEliRzUoS5NHREZ1OJwS2SIEoRvoiWUuShKtXr4aiMy6y5YjvUdo17itYFnbSXtKuMcghrwNfzHW7XZxzPP3005RlybVr1zh37hyf/vSnuXHjRmD7tdttJpPJCYBLKRVAXgEQzp07x5/9s3+Woij4hV/4BV5++WUajQZ/7I/9MV599VVWV1d59NFH2d7eZm1tLTA0gSBHlPEhCdZra2vcvHkzgMGrq6vs7+8Hthws5YYyDqSNYo+z020mbSlFuxTpMs6VUoEVdBpkEw9E+Z2AviLv63Q6vP322wGAE1ZOPJcEWJJCWIIF5D4EaI1BX5l/UjRrrQPAEYOIAmYJ2JdlGYPBINxHPFdj8DNmIp6WEwuAJuCD/P40aB0fMTh4mjV7mtUUt7uMYekrAc+Ojo5OnEeAYWHQxunZ0q6ydkhYhQQKyTXlWqfl5DJGYlm4gK9JkrC5uckP/MAP0Gw26ff7DIdD7t+/H4I8pG1kbsh9if/rO6XrSj8KU+v27dscHx8zmUxoNBqsr6+zuroa5sEjjzwSQGrnHLdu3Qr+r/GzxZ5y8fPFa6H0v8wBAbZi5mDclzFDWs4j668EG4kP3uk+lXEo6/1gMGB/fz8ATN1ul0ajwYULFzg6OmJlZYXnnnuOw8ND+v0+6+vr9Hq9kCAvQK18RsTjS9iHAvzKnJG+kWcU8DT21gVOhHXJuBL29mkQPt6wkTEjGy8ib5d5LedKkiT4Y8q4kCCTeN2K71HmSrzm7e/vB/uOL3/5y+zt7fHEE09w/vz5sPEmnowyxsSbUfpKmKMCbstnlcwjuY/ZbBbaXD7/ZDNG+nltbY2NjQ1ef/310CayGbO3t8f58+cZj8ccHx+zsbERNvEajQbD4TCsNbKOCYtTwq6c86qEeP0qioKDg4MTG5//puNdASDKoSsP6rmavRPYEbV5vdNLvyazAJRPQpaERgEIy5qx49OXPetN2D/JtGaM1NJoXfivBaiyGaQD/306FpYEgdGhF55B55T39tNFvWs184WXLiAbOFzNUiy6S8murhxmYX2KL/D6aJtJmeEmhsEip9Oa0zxs0OgrWvcVylmy44qylicD6LlDlwnp1DFfVbT2ShbdlHTsC8XpWUcyNKjK0Tisd1JmjvmK4SPnrlM46NZF+MxBTytmDhbO0a6RnZlzHNeG8hrHxCWkylI4w8xZerqkwDGsAcaucsxcLTmktlurC2BVgTGWKvdgymTH8ewj13nx1x4DW4OjU8j6njXjWhWjhxzZvkEXiuzY37/T4NYWvFWMcIn/mT0351ubd3lpoelq+PLsMv/zvfdhU5g34cofeosfv/hr/Kef+xFM7cvXSya0dMZWMsDWBbte+EJaWJmFsxTOcW2+jSrgcuuQwlXYmfGsv6MFyeEYjEZN53Cps2QJ1sCRTRW2lVG2DUVbkR871Nh7CJlmGphIcWCFsrX02TqfQJwnHghYyJj3QJirwYoqW0pdAbBePux/XoNeCIAhYSHWh5ywBJaVJUhXqdlAbjCEjTWqpqGYahglgTknAJBNCcniaur/kLOp8npHTWA+xSwdL1d0XhIMqEZNx9UeoANQee7lzaoOWzCGsp0sQRVXv/7UriAQSZSX96qXpCYP+MRsQAhgn01qBuikRBWV918En0hdLf/YszXoVzmfaN5QJZkuqXLHbN1f39Sp3OkIXBXJQSMMsaEKn/ItLFtLACxF8fwnV7/M2p8ec1S22UxGHJZtPvU/f9x7dg58v5ZNFST9LjU+1bnnvU/ToV8fJw97cDr4MFb1fWrLwjn+6ivfB7+yHtoumXn7h/Ff0awmitdvnWHl1ZTxuYSk8mBU8GwMfn1evkzt/ec9ORVqUf+hWApT1JxkbtX9qFOLdZqxy9jQExbokMacYUl1QYplViUkE2geWIqW9uuGWTKOdaX8fdQgnarA1ADipyeX+Kv/05+gc8OvlYueJRlpbu6so1GsJFN6q2OObIckKymGOckMmoeWRXtpEWAbnABCbeLtK3TpSGYE0FkSm8um8/PXnpyzLrO1jUTlAcuajRkkzCy9D/eLLouFwU0Ni1KT9qao1JKOoHVQefahJnxO+pRohUtsHR7mv9eFiz7blp+r0+0kzKGia0JIiR/rMCkzcPDIxh7P37xAYvxzL1YMw4uGC9nhyTlSep9GrR0Yh5l7KXIyX7IgzdwF0PPw8dRvPtRjUFmYres64MUnR4t3Y9HUeEW3whmfTl42HXlfYZ337m0mBZfSAz41+DBmojELH/rkvZEVtlzuNoyqBkeLJsZYehsjtjsjrqzs82uvPkExbnC4HiPefjPLzL/hgfhuPGLZFiyLeykmhbUiv5PiTAAyYUWJ350Um1J8JEkSALf5fB7YJHI9YZbINcVDMS6ehHEkLCZh/p2WHUrxLmBmLK0Wc3cBWCTUZHd3l+3t7cBkkwKn3+8HloY8z2AwCIWp3NNoNGJjYyMwK5xzjMfjUChnWcb6+npIdd7b2zvxvHGRLkBALNEUIC8uIuVnsaw8Lqbjc0lxaoyh2+3y+OOP88ILL7C3t8dv/uZvhtCN+XzO2tpa8LIShleSJDzzzDM0m03eeOMNdnZ2AgPq/Pnz/L2/9/d48803AzNxPB7z6quvcvHiRT7wgQ+wsbFBt9s9AXTIfUpRLT6UMpYEdIiZpSIxjCV+0s+n2aoCdEv7CsNGvNoEOIjbU0CJGBAXYFoAa7lf6Rthscp18jwPAKcAXbBkh1lrg7eosOSAE+eX/rfWhjY9ODgI3pVyPhnr4g8nIIaMYzlPnPYcz3lYSoqlHWJpZ1mW4bzC3JKQnndiqMX3H4Nkp8fu6ffFc/y0JFLGePwaYaEJmAKEQBxhfgmzSYCPGJwR4E76UcaQgEQy9oUZePXqVXq9Hv1+P2xonD17NtgoTKdTLly4cGLcSnDHeDwOrGYZY7HvoYy1Xq/H448/zs2bN5lMJhwcHHD16lX+yB/5I7z//e+n3+/T6XT41Kc+FViX0p7xvIeld2UMEkt/yDiVa0sby9iBpcz/dB/LeZRS4ZrinSdBHALoy/gRYF+88owxPProo+zs7IRnfemll9je3ubZZ58NQU2y/orsWO5NwkBEOh4Dp/HaL8nwjUYjhK0AJzZX5L4EZHwngFCeS55b5sQTTzzByspKAKwXiwXD4ZDJZHKCcRqvL9IeaZoGuw1h7Ml7hIEnoN1isQjMaunTM2fOBMuP69evc/v2babTKY8//nhoM2nreMMu7vc4bEisImQdAU70XafTCWC6vF7CieQQ1qc8mzByy7Lk/v37vO9972M6nTIajTh37lxgIrbb7fB5mCQJZ8+eZW1tLfRbkiQcHBzw+c9/HqVUWItlne/1ev/nAxC9vNCDRFWmAvMQC4gMtnCIdDX8zi1DOZT1qZg2VaixL45b92uTz2bNgLFQtuoiupZ5STiFT6eF/BAkDGC2qUiHHsBZ9GKg0Aen2NQz/8wcJDDCy8JquXQN5qTD+ppKsegYyjb8P//Bn2ex6vhz3/8vudnvcTxucmZ1iCtWKDq+UO0/rNn6mmW6oencrZhsGhp9S9FRNA8qnPLSyGTqaO1VtO7D6H0ljTcaHgQyHtQ0c8f9ZxX/t83fYWhTVnXBzGlWtcOgqPAA4AxLSxnmzgOHFYoN49igYGy97MvgaCgVUjkbylHU3bFpCiZOYXA465kk4P3Fqsy3ib0y5e3BGs17jvlaDe4amJ713pXmKKGxp8kGjvE5x/GHF3zyyZf56Mqb/Ff/7b/FJ/d/nHRaB3hYxatFm6/PLvJ/X71OS8/ZG3eYb1iqjuWx7j0uJwN06WjmPnFTDoPlvSu3ecE86vvSgHWKzw4e4w83d9mzimHVwEkgAxY1MaQTSzKYeY++GiSoMhXYbuDHgwfJHHrhAmtJFeUSJBO/NpHIzfz4NiFV3EJpfUBQ4pleVV4nbmduyaKzNRBeWlyia8mkC2zBZfDGUm5nU0VldQDGfeiHC759qnKwtgrOUXQMek/TvO/ZNwJsKUfwedOF9YCf1p5hZhxUCj3XPsVYntcu56tLDXRqJLMoT0idyVLcaILqtDyQlxqqTNd+ebWnnnMoARHr+ezXkggcrDcSBDTEOrT1oTfhd7XXmwAW4M/ltPaBKwW1nNt6UKosMYmXw1u0Z8ypiiudfV5Ir9C+4zcNvJdqzfBMPOPJacKcgBoYsqpegxxFV3P8RJds2KboVWg0vz5+gr/7he8gv51hZjA7Y1Eth+srZqvgARSHrepnWpS07y4oWjnzdc9a9CwyFfXBksW7ks0pHFRWY/BAcgghSf38tw50YilaYJvW92/iUE6HjQJJo5e+donCyo5vKmPe+/QtGXYRIGO8j2OFZ4Mt0Axtg7ZaYJ2mUo6ZTejqBbMqxWYw3jbBOkJXy9Rh6W9bA7M4SLOSuSsZVg3vtVfUEueaAXc2P8bWEmlrNUo5bGVQC+8XOFvTgW2oK9Azv84Ptfd5RCmqpqt9cgljzCxqZuRcPqeWidXKAnnN9raZnzNFLWfXns1IZtlMhkzdgscad/nz7/kdzqVHPJDus2Wm/JmXf4h+s8F42yznr10yMlVZMxA1lF1H46hi0TEULc1sbbmRBjVrvvShNmWu6NxyTDeVLFkk2mIbjs18TDlPSJxsxikmZxwNXZAqQ9MsMMZRVAq7MJBVUCmq3AOHTnk1ge3Wn8E18OnvO0KWLbR3q9qzUVF0otcrv5km87XK67nl6s0VNGuNCQ1VcDhpUrUs/YcN1RNj1rtT1psTvufsizydpZRU/Pneq/yFtdeoHnIYpdBoJm7Bl3YvMbjTWALe9bpLpZbWK9843lVHzA6KwRMpmgQ0kZ/FUmMp6AVMGY1GwRTdORf8AaUgl3PHBVssCZPgEpF/CQNnNBoFkCD2oBJQQaTRAjT0+33SNOXcuXMBXJL3Pvjgg/zKr/wKzz77LN/yLd/CV77ylWCYf3x8TKPRYDAYsLm5yblz59jb2wvMEQkNkPu9e/cuAN1ul8FgwGOPPYZzLrDZJEAhz3Pe//7385nPfIbJZHKibYUdJoV3DKrEbJ8YmI1ZZNLWscw19hsTwDVNU86fP8/a2hrvfe97eeONNwJYK6Cc+I+1221arRYf/vCHuXLlCuBBrv/xf/wfA3tOZOCHh4e02+0QejIej9nd3SVJEgaDAQ888ABbW1vBM1HYKWVZ0mq12N7eZjAYkOc5+/v7zGYzOp1OAG9jby4BiWezGcfHxwHAk/s7zWwTUEfeJ2NAQNfYL04KZjmPzAdh4AmoF4+7uE8EzInnVSztFVBGWLLT6fRE+In8L+NZJMQAb731FlVVBb8/AVclzEIYOTIvZK5IH8WHtJEAbgKQyPPH7OMYFItlqNJuMQgra4O0Rwxkx88fs2Njlpzcg9yHADOnwfBYFitzQ9iGGxsbXL16FVj6ojabzcBilWvE8vOYOSrJtEop+v0+AE899RSXL18GPFD54osv8sYbbwS5qYC6sh4uFosA8EjYRZZlAdCO207WJnnGF198kZdeein87Nu//dtPeMjdunUrsGU3NjZO9GssSY7HuzxbPPZiubO893Qg0GkWc8ykhaVEdbFYBOBdNpCEASr9FgfyNJtNdnd3+cIXvsDa2hq3bt0KGwzClhXfREk7FvBcQH6RzAowGT93o9FgbW0thIMIKCfPHbengP+xjcKFCxdoNpusr6+zvr7O2bNnGQ6HwVLh4YcfZmNjI8zHTqfDm2++GRjEwv4Vn0x5bq112HBSSgWGe1mWwUtXfFVHoxH7+/sB4JT2L4qCXq/HjRs3WF9fD+BZq9UKDEJZ+09vJpzuR3l2Oe9sNgvsV5nLaZpyfHwcwFYJYimKItiHyDPIOpFlGWmaBmm0yKWffvppfuAHfoDj42Pu3LkD+M91kVrH/odHR0ccHR3R6/X4yEc+whe/+MWQAC19FVtp/H6PdwWAaGsAQ1kVUoyd9mnLybTyDJvUMweEvRGnqIqJvwcFvReWl7N5Jkw6tTjlQSdd+eKqylTwMXSqLibr8803aiDCeMCjceC872FdnCVTR5Uqkon/mS4cZdsDn6ZgyQxz3uNKF6oOuVAcPaYpO46Lv15w+FjGxfSQRlpyMOjgVrw342Rbo0pHsWKZbBrvZ+YMi66ibBtGFz37oWj7UJfxeUXZSkiHDlda8r4HAjAEX7SN9+yxbmYUTmPxsuOGShjakonHBmkoTeEsqYKFgwyLBgrnMHUhvq49paxAsV6jEIXz6c2ZUuxXCan2Sae+bxRZUjGrAZRve+gqX7j1AHmmKDqOsu0He36kSYeQH2gm5xwPf+81fvTir/FtdVjXf3102TN/cuelbiU0O3O29IT9ov6DySXMS0PVsZBYmqYgre87T0s08EBjH4AP545/OTTkRx6EqpoOrRx5/Uw9bWmZOaqE7WzI0C7QhcIp58EkrXGNFKcyz/KpfQgFlEvGFXo0Q203yEaeeeo6rZpdpiN5rfcdLJp+/IhEL8yNGhQQdFKA8CCBrIFCVTpIanCjUpQqTl9mmSwszEGnAoMKliC+B1csrtNEjWd+Pq0XVIMspJx7ZqlbJiJHUlUfdOAbQpdLGwJ/j3YJFCqFy1P/rI06dcWopT9ir+M99JyrQ0zqNlSKqqX9+2vmkqpqUFCxZCDWLC4JVbFGoetr68oFr9XgL+gEkYVFLyfVCj0v/T2VlQ94SQ0uTzHGMbRpLbMtmUmSa+6YbeoA0CVTsAuYwbIvlACesHAGVS37Opk42ncXvi9disXypeMHWXkxo3PHYhMoVr2Pnaq8R6sqoWwrnLF1aI5msZp4G4Sp90J1GmYPRnKZWpqL8wzEAoWjZlgHBl0NdjlHSxu0tt570zjUQqPmnGBx2kT5QAzrIFE1WFv3gxNgcZly7Zm6fg0PwLtxzFxK5TSZrmgrvxvc1QvGLmFVe4nAtEwxc2jtez9Ppz3AXjaIJPYuhA/5c1sKHIVLAuvXs9n9xTtmhsUyrbIg60Y5nPHS3vzY+cAt/HvL1jLBW5f+XMnEp8N7lrtdguV1enk2WILcQXadVSycw+B46OFdZg8knO8c8/6VWzyU36dnJnyyOWXq4Eq6xwvTi7w2OYN1mtf62+zu9mjPodF3UFtPSOgXwKytQdebWgsYb9es5sphRssNL6fgcN2ETScbbRTIUVQGtbqgqRe4ok51rhyTTU3V9p6gGu3lwPmCWVYzY+yS9Tfd8AxIWb/SOl3ZFDC8oJcJ5tb36fC8CYnmelFvzljf71XD+fkj7O+KpR8llovNIx5KB/y37/snnPvAgm3TwijNxC6YO8+itzSY2II106ofePm8xinKyl8/2/d/tJYd/3lO3Z7f8EB89x2nwSlhgQkDSgpCMTAXkEOOuECN/b2s9amWElAh78nzPAB24EEaKb6keO90Osznc46Ojuh2u4HFEzPQYhaiMI7yPA/sE2F7CZMkZmkVRcGXvvQlvuu7voszZ85wdHTEaDTi7t27oUju9/uhuBRQQArhqqp44IEHgsxSCjJjDIeHhyfM89M0ZWtriyRJeP7550+wDOMwh1ieJawo+VqYX9IvsZw2ZmHKP3mNHAIGra2tMZvNOH/+PF/96lfDswqgJMyXT37ykzzxxBNcu3aNX/qlX+Lo6Ijv/d7vJUkSXn75ZTqdDjs7O8FPa3d3N3hSCQAhoG8MOsR91Gw22d7e5uDgILBDBZiOAVVJSI7ZJwKIylgTlk6cICwFpowrSRmW+zstKZWvpV2l72KmmvTdO/mgCdgr9yYAVwygCQApoMLpgBMBtqQfxOtN+i1+XSwnlXsSwEGYZTJv47Eh14sBmvifPJMwoeSaMXgqIJiMxxi0kNcJEypmyUmbxc9wej0R1nIceBQzbWMAWg4BYWQNkyTaNE0DwAxLQFkYVvGYknVnf38frXUAQra3t/n85z/P7/zO75DnORcvXuTg4CCsYQIQyjgTkFoYZmINEN+D3Gc8n8VrUmToMSs5ZtrFXoXyfcwkjRmE8doghwDN8Xg+PS7jfpL1Sb4W0EjaS1hyws6Uz4vYpkKANAGfBOySfpDnjUH+GDgWlrEAszLXJRVYwOAsy8JnRbvdptlshjEka64c0ndpmgaW86OPPsqjjz7K0dERN27cYHt7m0ajwdtvvx02V2SjTNrqzp07YZ2JQ7SEbS9tK2CrBAEJ+1DAUPH1lbHvnPs9c1faeDQaBUsMpRTr6+vcu3cvhIvE1iCy7snP5fNEgEwZN3Lv8ZolagNZo8ViQBiTsgbFFg+y3ovlRaPR4PXXX2c8HvPaa6+xt7fHYDBgMBhwdHTEYDCgKAquX78ewH85PvjBD9JqtTDG8NhjjwU7hzRNUUqxu7v7+2YfwrsEQBTmj67Nq8zUehZeDQiK0b4uXZ3aKMWff58zRCmcng0iIIJNlwWrLkHPrWdO1cw9XTnSiTdlFwBlbhUu9abzwmrRlYNaDpdOHLR9MVPVvozY2quxBkCqVApjRzpyIXCicQDJTSja3q/uxmKTyipcoWkkBdPcG8frwsvd5j0VkiPNzCc/JxPFdAuyY1isepZk0fFgQXovJTt2tZTPs1AWbcUPXHiRwmlaqvLETgV9WwbwryG7jkBXaWbKsVkzSsAytJBiSZVPa25EhVOBIsWDDXl9fufqQjZxLEpDMvLMrPd3b/Ib+0/i1j3Q1dr1UkuX+NCb1bcs3/Xvf5Gf2v4qAK8sZhzaBv/fqx8lmUJ6pHGJZ7BkScXcGdaTMaky9EwdgZ1XUGg20yEvL9ZIJ44kKTmX5Hxv+3Uq1+IXx+v81v2HQxFoU0LISkunDKu5lxUCTzRuYwFVeCl91W14Ca116FlRgy4qeJ2FVN/U4HQdaFAp1GjiGYiqQ1X7ZAr7VZg8ztSMvukM120GWaYEgnjgfFk4m4LAigmeb9XpeVV3lPPMRlXB+KhJW1hxNZjhAUwP4qn5cpd2ZWPMoFS0r6UhAMbV7L/AfjQaipIyV1CnROvo3kKQi60BWMBpHWwKAFSxZHUqYVQmOgSmqMphZiW2TmHWhU9IdmJxUG8AJDUTU1KXy5op6ZLa78TW1gi5ojrlgQh+zQi+is7h8hSM8QzSLMU5mLmEVJUB5CqcD5FJRz7Yp2hLoAe4ufk9Um6nPeCtihrIrH00k8EcPZyhF1toNL10SpXXr2/6YCKbO4qO918V4KSYCeITgVbtJZuQae2PqZbAl3KeHZzicHWgkDU1UFM45okObCxnNWYG2aF/lsW6rceqP71ZOJJZFfpbfP5C8jbg8GMmP3YRS3SJUCWJD1ER/0M5JLxp7gypslTWswInW0smqZn7e5iviXdfzY6tAdpGvYEgISz+M8VvWqG8/ynA3CYYbVHaMyLtwrPbputq6Ss4dyRzL6P1QLkH3Gzm16WyiR+zYV4RUq1DynI9H1vtORb4lvZrfNNDV/n14Xt4bXSGX7r9Ho7HTabHDf7Gt/4zfqC9z39/9GF+4YUP4BaapFOQ5QUsNFUO8xUB7wmfQZ4d7xOg/biG5mFF2fCy76LtA6EkrVoX3jdWV4506nxoV71B4ffrHFsbQ+7NV3woycjP//m6f//EZuh6kBvtUMbiKo1JLCUGZxzdW5VnQEcpzFXmpc3Fim8TL3X3th8rN0vG2wnOEDxfy6aibDlcWrNe3XLuymFw/M69h1hLJvzxlecwSmFqi45cJZh6nqTKsKIbvNORq5Sntnb58ms9Wvcc7bslZUuTzCw4GF5MWKx+A0F8tx0xsBCzgmJQIQ45iCV4sXeYFGkCMoh3nLDspNg4nXQbs62ENSXAR8x4iFOXYyAhBtJiX0a5tjDa5DnER0tYS7HvXL/fZ21tjWazyWw24/DwkJWVFQaDQSier1+/zvr6OuPxmPl8TqPRCEEcSin29vZOJHFaa3nf+97H3bt3OTg4CEwfAURiBkVchAmIKM8h74lZdNJ/csh7BNCQPhXW2uXLl7l69Srvf//7uXnzZmirnZ0dnnjiCd5+++3AUPzZn/1Zrl+/jnMusBBv3LgRGFfSf9KX0nfOufDckpgsoIgAACJVXywW3Lt3LzDhYqarjAmR+QrgKgmscSCPtGMMtMR+fcKylHMKsBy3owAcsfRdgG8BnWIg9J36QJiA8ZySdoqvNxwOcc6dANJlTsV2AEp5bz+RlseSTJESit+ohAeJ76FIgYXVJfNW+kjuK2YexuBpDADKmJPxKPNMziHnFZBb+lPedxocitcdWQ8ECBAwsNVqnWDYytoiklO57/icxphgdSDjRMDUGMiR18f+ljK/hb0p7Ge59sHBAZubm2GOAhweHobwiVhC3Gq1guebbIKcZsfG40F+Fq+xskESzytZp2Kg+vQ4ljaK+yUeX+KnKWun3IuAUaeBxbivYsaXyIiNMcGLVq4h9y3te5opGLNZY+mxAFwy7mIwUkA2AeHk/mUexPc5mUwYDAZ0Op2weRQzlOUQcFekv+9///sDaz1mcJ85c4b79+/z0EMPcf36dfb398M5xbNvsVjQ7/eDBF6Y+HHby5oioSSz2YzpdEq/32d1dTVsPLRaLTY3N8NnlZxnNBoxnU5ZWVkJ/SF9fu/ePR5++OHQ9vF8FmBXxncc5CQbVQLYx0FQsjF37ty5E2uDbHKc3nyU18ha0263UUrx1a9+lRdeeOEE+Ctgrqwrpzcm4gR1Ce+Sz1uRUscs6N/P8a4AEL2R+pI1UTW9OX46spQts2SUaIUpHGVeMxVrX8Jk5jAz8eRS2IUHGyW5GWqWUe3Z5NkyNdOhZmKlE4uyvrDKjzww2L5raw9G/LlqsEl882zqAQMvZXaBbWXNkkWJ8uCfTTTJ1JvCgwcsbQ4tvaC0fsBd7hyy17pE+66jSr3nXTp03ghfmCLGBC+nRt8yXPHBDeNzXuLV3IP+o55Rt/mcL6r7Dxu+pf06DVXR1ordymCcY11XzIGe0oydRStoKcPMVRgcBphYPyFTwNQGfwW+Bm7X72sox9x536mi9i1jIYwwGI8brI8dh087/nD7Vf7+23+EZOyL1fF7ZnzLI1f5yzu/yh/5X/8Tem8qDhdtPj1tcjk54qyB35hcpn+vS3rGUTUcras+GKTbnrBuCtK6Qq6cxihH2igppjnrZhT8w3JT8i/Gm7wwucT/Z+t5L7N2itkGrL3u+62hHEZZNJrKwfXZJmYGf//WH+KLvVtkxwozt6jCerALUIuaDp0CNasp9rmrMl/wmgKfONzI/fhIagCxfq3THgQ387pxmw0vexUwUhOAAGeir7Uf855xtQSHfs8cSwgMNV2C6SdL77qawbPoLsF2d2sXtbNNmSu0clApH+Rz1tYhMR5oy4YOtSjBGDCasuXPRalQpQrzBcDmBjOvPEqt8T6INVgIQFKDdmXpJdHIhgCkkxIzLrCpqSXHDpsaD0BVHsSJwSj/ZhV+5lJdpxxLg/g1J6kZzFUurFAXQCmnawm2UqA9qOiyBGMsCwwN5cM+wuVKRdny7ZRM/dpjU8Xssj3B5FLWg00N5ceOfx4B/rQPo2nUf9A5LzWv6gRzgHSgyY+gse83ReY9hX6sQBcZqqhqr1ZHXktSrYHisQJ1NwkSZmmHTJfMnGaxSNCJD5CSMSFy9WO7oJwmqFWHTR3pQKPnkvpMkMiXjXqtjnwQdbH8IFOFpWrVqdKwZCaWfuxmSYl1msIZCgxa2XodcjRURd9lpFi0ciQTL28tG97Xr8r9mg1+bHpvP39dXUIn90Bd4cyJVHJXh5KcTfvMXBkCo2zhZcxov8Z3disWbS+lLZuKxYrf2PIbVZ7taCYKXViaYxdYdCLXLZvez1VVnr0u46+ZFWjgVwbv47//Xz9O94YPy1r0PEBLw3Ip8d6Cg7JB1lqwUClZXtDICibKA23Z0KH7NlxTNiMmqz7USFnPAJxsGS9VrhzZSHwF/e/33+N9TvXC0tpd0H+k4Td3jCcGF9bwcG+fe9MuqlAkU8eio5htekBNQNgKTWKqwDz0Deql+oNLiQ//aXg2q4SliGw7mfi5pizM1x33PphSNd1yrdIOm7sAxIrn7JJNTABoP7b9Fv/h2nNLdmF9GKUxaCpnObZT9quKvapJ37YY2Ab9qs3txRrP9S/y9Zcvsf2ST7vPjhcU3SZHj6aMzznSse/TbyiZ312HFOEx6CSFdixviov003/0C0gkrAc57+niQorgGAgUsEf8rqTAF1nbYDAI146ZZu/E2IsLRPlaDOObzSYHBwdcv349AATi0wQEZsfTTz/NvXv3GAwGgPfdEvAqz3MeeOCBIGPe2dkBlsX0zZs3A9PvoYce4pVXXmFvb4+HH36Y5557jtFodEIKKKCGtIU8SwzaxP10Ws4pr42LfgHA5L0xA+jZZ5/l3LlzAJw/f54nnniCD3/4w2itOTg44KGHHqLf7zOdTrl58yYrKyvh/W+88QYf/OAHg/eWsFZieaJcUwpRGUPCUFpdXWU4HAY/PZH7XblyhZWVlcAISpIkJOtOp1MWiwUXLlzg2rVroT1iiWvMfIpBbmHECoAp7LZ4/Mj4jIHZmFEj80NAOPl9PBfkujH7Ni6o476az+fcvn0bpRRHR0cB8IoBQQEYhAEsknEprgX4EaC63+8HsDFmocUsR+kbAW5jMEvGUAxWxYBBDBSc9iQ8DcrIIeMwZnPGTEQBrWS9EMlpPHdlXggYKvcynU7D2JZzSV/JmJCxJLLI+HzxehE/fwwMCxAeg/bC0hVm1cbGRgCyBQCXZxRWsvSrMOVkDAloI/0iYywe36fBdxm3QBjj0l4xG1X621ob+lqeM5Z+S3vI/ccM3RhcjAFxWYdkXMr5Go3GifPGY6TVaoVnlH6L54usFcJKlLYR8FcpxeHhYRhHEv4h9yChVTI/VlZWAjNSgm5Oz08ZA3I88cQT5HnO5z73Oe7cucOrr77KaDTiE5/4BGfPng2fTffu3ePg4CCw5C9evBjAudXV1eAVKH0t41DA1NFodMLa4508MZ1zdLvdsNkg87jf73NwcMDq6mrwCd3a2gr2GjF4K/MvZlH3+33OnDmDc47RaHSizQeDAePxOEirtdaMx2NWVlbo9/scHx+fCJkBWF9fP/F5FF9PPhtEyi9/Y4hHqGwOra6usr29zebmJqurq6ysrNDpdAIoXFUVly9f5pVXXmE+n/Pggw/ywQ9+kF6vx/PPP8/h4eH/uTwQhYVQZYpk7gsspz07wrMUQNVFhS5BGw/KVZmieehZLx4gqf3mar8vkKIDwBeaqlpKjJ1W4femZt3oWgbF0DNGtPMViq3BMGFVVTmB9aKcL5TMgppBJSEEKkh5i64vuPxN1ezAhWI9GVFVGirFVjaiyhXrL0+ZnM2oZj7Rc/X6nKKdoEtHyygaBwXFSkI6KNFFQja0TM+k5MeOoqV8+m3pQdR0bJltKdbNhJkzdLE0VEVVF8pVjST0dEKFCwEiuYKhU7SUBxIL/NeFc6T4AJZCOSwwthqLQuNIseHcqnKoAqphijOQXh6TKm/6X32iz3/w6Of5j3rXMErz/NzQuGNwytI0C94uNtBYevoIg0WPDX/pj/0iP/n5P8rK25rpuiE3JUNrWE889dkoS1EZyoVBLzQ9M+Gg6uAMtBLvpfZw4x6pMmyYEUZbZucLFrcTXF5RObgz6/FKUXBYrXB1uEUy8f5wXzm8RHPfkUwq9HACSuHyDJSXH3vvQD9GzHw5toO0OZLu2lSHglhXBOmdsi6wgZzR3idQLX8fmI2Jg4UKDCcP4gh4jS+glYCTbgmKVX7sJRNH+23P4hL5n8hPdWAKel/Dou2ZWDIP830PQJZtf56VGzPUrMC2clxuPCOnVMFzT4AS5Rxly+BUhslN7RFp/T+RhcsfXVXlgdlWYynnrpnFKJbgrVaEVGVYshlrBpqyS18+ipP+Mj5AyS7l1yoJ/nx+HbGoyvsv2o2OBzKdw+YJa+2BBw9tSoUHThY2gdoSoejUrNGkvrc6rEEk/eLTl6pyCRJr/0PPqNSQ1sWO9czGkNKsHGULz7RLlAdjFSxmSQhRcdp7H5aiyrRQTZYhOOIL6BS0koKJS6gqHcAYZf36WrT9UNJK+bAXDelIkw1hsUYAwXEeMDNze5JRWjNUAysxM+F7kfAr69CVB6BSYz1o6Byp8j6TQ5thVFknvHtf1nllqHIfsOG0n2/pxJ9rtlFfrwY1Xd2u3Wzuw6HKZvBGFLDfKUipsM4xr7W7bmYoy2XwyGTThHmYzBx6BIu2B8GEXVo1HGXLr9nO+PsQb1Nd1Ay/VuJtL+rU9UbiWeCDskHVcCy6mrLpgo8pUCdPG3JdkqYVi2mKtZqiMmA9eDpf9QzEOBna1P6r0u9V7mgc2ZrV79l8i85y404Om3irjMWKo3Hgn8Pi2apX2nu8crCNmXlbgaKjcJlFz5T3j8QxLBtM5hnMDeQVWV5QNQw21QwesctdEwC3lLDLZ7aX8Pv2tLlDz+vP/kLWV0UyVkwu1KmGSgV7EV0rBs4lU/7S9m+zZtoUruJqMedGucb1xSZvzra5O1tlb9rh/qjDcb+FOsowE0Uy9Ux/vfC3uDF2dG/OUdYx38i592FILg35c098kb//+T9EfpDyjePddUjBErNhTjNjBECL/wkAkOf5iXCHmEESAxQx+0eKn1iuFrNEJD0yZpqcLgCF7RSn2cYFtEiy7t+/z2g0ComqIgMT5gwsk4a11ly4cIEvf/nLHB0dBXlZzGYSfycgFIlyXQGTRqNRkF71ej16vR73798HlixDARlOs3tiWXIMtkgfxT+T/2NgUallyIEAAovFgsPDQ5RSPPzww/zar/0aP/zDP8zx8TGf/vSnuXXrFh//+MdZX19nMpkE9tDq6mrwhbx27Ro/9EM/RJ7n/PRP/zTj8fgEI0kAYEnp7Ha7AZAR30dhdwkQ4Zzj6OiIxx57jKryhv3yfhkzMTtpd3cX8ICtgBcxgC3jR2Sv73TEjMPTEtyYbShARmzaL2Ne2GECfkn7S3/GYzEGLOT8wqCRcSLXmc/nIYynLMsQGiFebjFYPpvNQmLq/v4+29vb7Ozs0O/3AzgQz++Y2SdzMT6E/SUARMxGi9lisZw1BlNPHzH7MN5AkPPKOWTdkPsSsFTGVcwkjdl50g7x74AgHRYJa8ymjGXG0sbvxEKVr4VNKj+Ln0kAXgF5pY9l7ghgGAd1xEAsnFxXY4A3ZswKAClMUmGPyXgXQFjaIJadx0Bs7Ht4mo0p4zoGgmNwTc4Tewhaa2m324ENPB6PA1AYg/pAAAilXaT9ZX7Edg2yCbW2tsb6+noIVhFvwMlkElKM19bWTgRNASH9WJiA0u4CKIr8OA44kvt5/fXX+drXvhaCtuLPKtlIk/YQIFU2wObzeQgTkTEXP6u0nYRFCWtY5oWMk3a7DfjNgX6/H5jIRVEwmUzY399nZ2cnsOk3NjbY29sLLFgZY5LILPc7n8957bXXQiCTjKd4kyEOQUuShMPDQ27duhXaT0Dqc+fO0ev1fg8DPN68krWo0+nwzd/8zXS7XbrdLr1ej263G4BVAS8ldfv69euhT6Xt3nzzTe7fv8/TTz/N1tYWR0dH7O7ucuXKFZ5//vmw8fBvOt4VAKIH7hxmtARBRL5sjQfikpnzWEftoyWJy5LWLIq3qquioIDaF7GWKTqlMKWvuG3qWWziq1jl/pw28dJmYek4lr6LYr5vDWSjZTgGuMCIFFARCAyVZOZQ4+W9zzYU7V2fTNzWc5QCnGI9GbPoQtlOGF3wi8nkrCM/zpmc0eRHjsEVWH85ZXxWkx1rRpcVnRsaM/dg0nzdS5rzW579VuUatud0VUlXL+XHqfLeZoWtKPAMQ+scFY6G0hxaS1d5Q/mxdSGARKRfjVqyrPHMxAxL32a0VEmBroFT71NF5hGQ+W6L7/r1/5hmFz7zoZ9lTTeYuoJrBfz25DFa9zwA+lBzj0yVNHRBQ2kaugAFP9i5xk/OPTtVOW/qf2CbtPWcylk+lh9yae2IV47OgXb0zIQ35mdJR561JLLIylneXJxl97hLepR4diDwG5OHyXXJ5cTxcDLjP7zwm/zY2Qf4Tx/4Fb40fpj/oXGJsmnIlPLMuUUBxkuqg+Iy3nhUy9RbaxSu3YQ0WQJL9VgXll6ZLBlyyro6CdiPu1Ivr+H0SSDa1kCStIvDs+HiNGKZA8o5RhcU882K9tsmeHWawgWgIUiJnQcIylmOKvy9TS/6P466ryfkxw5rNBgf/jHZ1ky3LVihVYIkrApgU3YMNtce+LTOs40qhy6ttyCwDj1L0IMptpmiigozKTwolWh0YevAI4PNDGZahjaziakljW6JTwhQWDeYMDX1XB5WQemBryrT2ETCMhzOeKATW3lPuyzxrLG0IMPSM5M6mTyhsAY9UzUTuV6XLMw2FVVngS6SIPv29wWZqryvpvaS9MCmSrxvHYCtfRkFiHXGke9rOreWKciTM4qsUZLMM9SsJJla8kNFY59ayqyYN0qUTaPx5a+1ks48G7fy9+6DNBxUngFtgaF1kFrKXkm5BvMHK9wkwSyWf/iVuSJp6DDedOWwWtcbKr4PdOFDrlwCiJwXAuhbVDU7p2YeytctvQQQDS4wEBuHlkVHB4m3Mx7c9sFBNUvNKdDQy2pj71pkK6CtWugQKKOV4lLzkKYpeHTtPh9YeZu/+8XvIJ2kNPo+hdlLf/WyHQtAecZ4dqw8w9z5jSj5HJuveEZ6MnZMN1Oa+9aH9CjYbI4oUExthqtDksIGQJ0c31AVmhStnAfbAWsVReE/mNKx94MMz11bfthUeQsMW4NrC+X9dSuCP6kpFHrqP8umZ5Q0RHg+6c8KRTtZ0NILjgdtsqH/vJuecbh2SXZ2zgea19EY/vzWZ/gLW7/pA3GwtLXl7+59G//Lr3xTDYx66fiSfV1/H7OnQ/8sLULkd3qu6L1ZoZwhHdsQ3GQzmG17if8PvfbvcDxtMBg1qUYpaq4xY00y8wChKpe2J+3UM4JtAmXbMV/zzFNnHKpQZKOMbFQxXTe0H+5zfvWYB/P79XrKsq2+cbwrjhj0iAtFYdYIQCVAl7Ac5HWxBFAK/xiAFNmyFIYCOsYFvRQSAg5IcXya0SXnksJFAAC5nrxeincpDq21jEYjtNY8+OCDdDodqsobw8fJv8KCk+RJ8LKxTqcTJM9nz54N9wcEMHM0GoXQlJs3b3L//n2MMVy+fJmq8km6ccDJaQYQcAIgkr6JGT2wBKviYv808CiAg4BwaZqyt7fHF7/4RTY2Nmg2m7z++uv80i/9EoeHh3Q6HVZWVrhx40YANKSQjc/daDQ4d+5cKBJjiaiwalZXV3HOsbGxwWQyCeCJgAjC5JM+kKJe/Oi2trbo9/tBXr62tsbKygrb29u8/fbbJ8I9gDB24vCTGHCODxmH0v7C6IrbToAWeX/MypUCWQp/Gf9SoAuYIGNbziH9LhLknZ0d9vb2AvAubJsYdBdQzVrL0dERq6uroS2NMTzxxBMMh0P29/c5f/48Dz/8MIvFgjfffJPRaHSCsSfjRdo0Botk3sr8kusKQCv/x/JEeZ0AGzFDL2Y2xn1wGvwWYE7OK/NR1hV5v3MuzLUYoIiZldJuArzN5/MANm1ubgbwFZYgoPTjaWA0Pm8s4Y3BSmFnCWgmc0LuUZiF4qcYewBKWwABJBFp5yOPPMLjjz8eUt3PnDnDtWvXwobKn/kzfyZsejz//PO89tproZ3iTQnpy/h3sZxV1sv4mWLCgbwvBlfj9pX2lmAY8Rp8J0BSmNsyN6SNJHhJgmbEN1ApxXvf+17Onj0b2uyVV145wUqX+S7jUtpR+mA8Hgf/SZH8Hx4eMplMeOutt0IQi7TLdDo9sabLGJC+lnFzeuzK/ckG1HA4DBtVEvISs05lPskaHc95YYHKRpqME/n9dDoN4SViR/GJT3wCYww3btwA4M033+Tu3bskScKFCxfo9XosFguOj49573vfS1VVvPHGG8FzVuax3J98VsTPKsCiPI94IO7u7rKyssLe3h4PPvhg2BxKkoSvfvWrPPPMM3zgAx8I42AwGHB8fMzNmze5d+8ed+/eZTabhY0+mdenmbi9Xi/8zfDAAw8wHA5DSNeTTz55Ajz+Nx1/IADxZ37mZ/iZn/kZrl+/Dvg0pf/8P//P+Z7v+Z4wQX7iJ36Cf/AP/gFHR0d80zd9Ez/90z/NU089Fc4xn8/58R//cX7u536O6XTKt3/7t/Pf/Df/DRcuXPiD3MqJQ1WuTv+sF6O6rWziC53WXrWUhlagG7XsqvLAny5B4wundGJBGQ8c1kwkkIJJaIlScIKrmQ1WCSux9k20/vy2DiIRZo2EtnjZni9+PdOvBkEXrma+eJmWsL7k/gcPKmYXClZu+kk4s6lnIBpHx8woVjx4OV9zZMeKqm2ZbhrKlmc32QSmG57BVrYV1jifEG1huuVl0kUbD15ZmPcMzz54g5nTzCroakerXgDntem+BmauEmUpw/rnqaoBBJewpUsqIEXRrxf7hvIBKpXzBWdDVWTKMrQpKkiYPTBVNcBMNMmBweaO5+c9rhdb/NDKDSZW88pkh6LrfbUez+9yUHVoqIIKx9jmuNQxdJZkaCgbfiHLdMl5M2KhJ0yd5rUi5+q9TSg0LnU8lIz4B7uP+2dUjs8PH+Zsfsz18iZvzbcoS0OV11LLTsGfWbnJD6/uMncJuUo5KDvgFOfMkDvzXpDD4xwuz7wnnqqZrTXDTxhAuvTSV2s8gKFL7yvotA6MH5FYSrKrKmp5Y+WDQ1xulsWpqv3cXN1JzrPZhLFYNX2yKnVBjnYhQESX1KxewDps5lDrC+zdJjbz31e5Blunxs7L4FNY5TCfpl62WkLnakIygfVX5+S73mh3enGFg6cVZcvicguJZyyGVFwBJCKAwydX1+1phJXpQUWdatLSUnZrHw/rQriKD4OocKnGZpr0qA46cQ4iwAphv0nz1f0RwMFakqycA6XBOrJBQVklgZWoxzNclqLrua/mBSrz0tpxLV2OgS6b+yTlRZcQBoGDYprWbGcXZJdO1wxEAQ1rEEXk3CavsFiGRY6Z+X6xMw8Mz7b9TLX50gtzPk19HzdTFh3NfKNOpsWDJXac1tdVJ/ziMl2SKssPv+/zvPXoJr10wloy4Vx2xNnk2FsaqJK/8c3/nDvFGkdlm6OixS+//hROJQGoTac+8ErAq8Aqq5wPqTKKqmmWfRLGtQrs2W7uWcIyX8cuIcOvJ+BBrBzHaJZDnYysKqLALMdsQwdfxmXAFuTaM4ytUxRrJSOT0L5yzOW1I840hny8OQES/uTa73Kz7LFXrrBXdlFjQ5UrZmu6Bri9XNgUjvmKCqEjzkCVwqznx7KMW7m+3JNyUCXLDa2zzSEGR9vMg5xaNhWUU7ikYl2DRf6YN7hC4/KaGWwcZRNmq5qqtvJzdZtK2JQulxsWvTcWFCuJD5+pE6i9DByKjgtgqlMerBbWb+UU51rHPNG4zU986H/m/EeOeE82ZE03KakonP/QHrmS92ZNTh8VmtWrMDlr/CaDnzw1kKcxM4VeQOZVkv7zeqLJjn1YWTJ1wRagyuDgKYNNveelTX2st5Kk575m/1+eB6Ah9gkJoKFsOu8NmltcWm9yGNnkEkQzWjeOk9qjUTO6pPiOC1c5n/d5NL0P8v5vAIjvqkOKzdO+eVJUSGEfe6IJcBAXprHsL2bExQCGFEYxGwk4UVjHUtEYJIuBQgEOYzaMFCpyT/K7OHCg3W5z4cKFAIII40leK9Ip8bWSpN8sy4JvnTBYqqqi3+/TbreDZK7T6QSARvz/8jzn1q1bAWAThlAcVhMDitI+sVTxNDPsdGLqCbVBDfTFITNFUbC/v89nP/tZjDH84A/+ILdu3WKxWIQ0T2GAbG5uAgQQOQZw79+/H4p9ATJiX6uzZ8+G/l9fXw+S3OvXr7O2tobIFcXTUpicAjwWRcHdu3e5f/8+eZ6HAIyqqtja2uKFF144wcQTP8OYbRcDx6fBj/h1AtTGP5PXxozE0x6UseRQ3ifMr/F4fAKMlHEp1+p2u7TbbdbX15lOp0GGGQPoAjYJg24wGAQ2V6fTYXt7m2azyXg8Zn9/n/39fT70oQ8xGAx47bXXGI/HLBYLer3eCbaf3Ku0fbwRcHpOxuBODHS/UxsJ4BFbFpxu57g93ul8VVUxmUyCJFt+Hq9J0k4xm036OAZ10jRlY2MDa22QZ8r1YnBEAhlilqTMMflaUuG73S7ve9/7Tmx8CFAu8ny5hrRBq9VifX09JDuffnYZQ9Za7t27x9raGh/5yEfCJoZzPuX9hRdeYDqdcu3atQBICigWr3fxpoMAd3G/S1tJ+EU85mXeCOsSTjI/T7PSpW0kSVwkxQKWyfoqzyzrlYR3XLhwgSeffJKiKNjd3SXLMq5evcpkMuHw8JDBYMB0OuXMmTNo7X0bB4MBxpgTEtg8z0NYjoCneZ6zvr7OYrFgOp1yfHwcLDPkvhaLRWDAKbVMPT/N6o6ZqjFYGVtQxIw9YVAvFgvG43HwGxSgTjamjo+Pw+9kDWs0GoFJfPpeYib0pUuXwrp8dHTEzZs32d3d5d69e1y/fp3BYMDu7i5f+tKXwhg4f/483/It38KTTz7JZDIJQGx8fVkLJYBGKRXsLmS8CnNwMBiwvr7O/fv3OT4+5vj4ONhRyFgXq4rj4+MQfBIHxchYnU6nYfNNQGT5WyNNU55++umwXq2vr7O7u8vdu3c5c+ZMYPn/fo8/EIB44cIF/vpf/+s8/PDDAPyTf/JP+IEf+AGee+45nnrqKf7m3/yb/K2/9bf4x//4H/Poo4/ykz/5k3zyk5/ktddeCx9cf/Ev/kV+6Zd+iZ//+Z9nY2ODH/uxH+OP/tE/yle+8pU/0I3HR9HWFC2CjDOdOs+GKcHMvH+YrVmDqq6EvB+VLxxjxkUsXRY/KAlgqWpgUFdLkDJIpGvJlzAJVeVBCLmnkNpcJ9EKU9H7MPrzecahv6guPZConH8Om3gQJ5lB80aKsrVvHzqwSnpmTNWuAYSZD3DJ73sQqWo4XOLZhXLtxYrCzBWLLjTvO6bbimwANnOYI18UugR2xyv8xuQxvq/zCm2lKXCkKJ+4DEyco1snMMck/oYyDK1PmR26iobyIN6xTenqglR5VGjmfNDBzBlaqqJwJki9lQMW3jsMVQPFOZxPBvzu5AoJhi0zYV4lTM44OguFxjKrGUcAo6oB3YK9KsPMfPK0WTgSbXl+fo6ungLH/NODj1EMc1Sl0BNNQylGRY4z0DAFZ/NjLmf7XEhyHm3cpdN6gqONBGUzTFKhEZP9lIld8M/vfcgzSpXj/rxT+4h52a0w9JzWYYxRjzkZWy6t54Ot2bCNzDMWJZ11SWRaJtTGDEaW4/fEETEL43CSbGzRlaEUjMaJPYAHAtOxQxW2Bs5riUIG2UBjZvXu9aT2/nv4AcYPrpBMof31BvmhIxtZD9A7GJ9LGZ9bp3tjTtE1PtAjqTvcKs8eimTXv+dQYLMa3C8cznq2Fsa/t+w1KFsGmyzntBWGphTtDuyF9kmZuKtZjOJ96lwtiVyyEqmsZxZWNrQTzqHmJdm89CEy1vrk5dA/NoCYrWTBzNWSWmVZUXMWNiEdaNKBtyZA+X6ar4FKbC1PX/ab95XTy3tyUTqxhTT14/EDqzf56rOXGGrHem/MqoP55zbJjxxV7ttv0YUkrTCFB6mbewVVllHlvs2KLqhGhaqiaN16MyTXJRWK3zm4wqs3z+JKjRompENFsWL5yh//28yc4v/9K3+Kxr5GlZ7xZrZrlnbdN2VDYZpLyb1zy3TfEDhUOlzDnADUvF+fn0NGe29Jg8U6RTt4mxoMjrFLaamSxFTY0jO7Fx0VJNvBA08O6T4H7WROgeLPrn+B7/6OF+hXbWYu5fXZWb7Wv8C9ak4K/OStH+Dl+2eZjjNsYTCFZ2amY1fbbPg5Uyj/eaQljdz4UKvG0ZLZ6pnHPrG57FiU02THJUWrZoJq6Cb+D+1plS1ZjSL9N74BTf2LYdEgT0um1ONDOygU2RCysaMsvUew08vPscXqcv2omo7BA5lnxTbAJi6EOLnE96dIkyUp2oeaQENZ7s86fGXyIH9h/XfpqJSW9oycsgYPfQLzO0/4P73+Bf7FRz7A5u8aGgeKZOLqjbl6bgFFS7PoKPaeyRlfLmltj8k/vYJZQNFRTDehWIXq7Jy8WZB8tRvWOJf457EZzNcss7OikcezeUsPAJN4Nq3SDqY+xAincMtUIVRtH+CsQi+Wioay6VhJZrT0wrNBhQn+jeNddQiTo9vtBvmXgGYxw0iKA/EVi9ld78SIi9lBUkDK98IgicHFGMCIgTE5rxR/wjCMgTSRf8aMJilUOp1OKMikgG2320GqK0eSJAG0KYqC7e3tcB6tdWAZifxZZFNnzpxBKcXOzk6491arxeHhIaPRiPF4zJ07d7hy5Qqj0Yg7d+6ceO5YYhgX9FKkxmxQaT9pp5hZJb+TIloAXgFgRAYrRv5JkgSGlgA4UhjCEtyQ4ALnHK+99loAUzudDoPB4ISME+DOnTvh91Iwi5RNABlhasrzyZi6e/cu9+7d49y5c5w7d448zzHGBDm1BEDEAEzMApQjBv5kLAl4FoM47wSwnQa95P8YFIuZdjHbUQCs08xRKarzPD/BoBQWU7vdDkBhlmUcHx/zxBNPMJ/PefHFF7l48SJHR0dYa9nb2+P8+fOcPXuWtbU1rPUhObdu3WJ/f//E/cRAXzyfTv9c7lPGlrCx2u32CTAolvAKECmMN5kn8fg8DYJIu0sby/nkZ/H3Mi5i0DxmaMlrBQQSn8Cjo6PwXhnvsTRWAEJ5hnjNkOuKbHQwGLC3t8ezzz7LRz/6Ufr9Ps8991yQXQqQF3sZzudzJpNJGOtra2sBdJExG7PbhJkqfqOSmltVFRsbGzzyyCNMp1Pe//7387u/+7sBFBPwptfrnRizMlflWeI+iJlwMq+k72VMnJa2xxtEMWgugLhIa2Wtjdmq8lrZ3Dk+PubGjRucPXs2AOXiLZgkCevr61y4cIF79+6xt7fH/v5+GFexH+p0OkUpFTY/RGZsjAksW2HNSfvOZjO63S4bGxvB+zZmIMfjTg7ZZJJnECBRrgmEvheGe57n9Ho9zpw5EzYGgPBe8fgTW43FYsHR0VFgYl+4cCGsazEDUTZKOp0On/vc5/jMZz4TniP+HIhVAXKMRiNee+01Ll++zBe/+EVWVlYAwibG1tYWKysr4bNBfBAvXboU1oNWq8XKykpoy6997WscHByEebC1tRXmpAD/kkQt4KGwqYVtDic9VeXzVcJvOp0OrVaL4XAYmI9HR0cMh0POnDkT1qbf7/EHAhC/7/u+78T3f+2v/TV+5md+hi9+8Ys8+eST/J2/83f4z/6z/4wf/MEfBDzAeObMGT71qU/xIz/yIxwfH/OP/tE/4r/77/47vuM7vgOAf/pP/ykXL17k13/91/mu7/quP8jthKPKwaciLwtAU3hwzmiwyhcIZm6xmWccFk0BHAlhBT5RtmYJCstIe3aMsDqEmSesQFcJS8OfwzMcPAAISxm005DMHenEUuUKaz3YYwrvgagLz0Ipa8qDQ9VJu4ps5A34UdC5ZbFGMV3zrMKGKigLA5XibHIMKwWNvYLtr3jAsLnnQ1M2XipDoqmkbBYdE+47GVW07yeYuaW8qcmGFeMzKUUH7nxlh7+99x1837e+QoVj4XxhJXLkmXXMsCEcpXCwqJlAM+eZhUWNFhgcCzQzZ8BWpAqGNRurofz3RV2V2tSDnaruk7JTkUwSULBlLDtpH4ChTRmWOdnAt2OjRhcMjmPruDHbQCnH9WKT5n0vHzdzh8bxyuwcz7SuczlJ+OjKVX45fwpXKWzLsqob7E3bpA46yYKunnEl3SNXGT/YucV/25hzWKxQNhT/0dOfZb+aMnPwi8P38vzwAvfHnXAfoyKv5Z0WlxjP8lHKe85FuLmMPSxBgiz+XH5A6ZD8G1S+liVz65RNTWArxsCZKAyVZ13ZVJFMKxarSfDZczUyFeT8C0JIiE0drvTS6+zIz6fFiiLvO3ov9hk+usp0XWMWsPJ2RTqyVA3FrGfoP5owO1uRb49RL3RpHCQMLxpcWnoGXVGHNjSqE16Qy7RUFbEjXZ1Mq8AsX2eNQhlNfjBHTwsP3GlweRpIQjatgVilsFkN4kr6slZUDRXmrfe5i5hvNROynqgBfJTNAs+a8zJcMy5QhcU2E5zRFJ0E6zRjm9NQBRkwwO/mFV3L5KwOoQ/CDnSzZQqz9+TzE63Ag2kSVlK2YHq2STKp6LYGaBR/fOU5Hvqm+1ydn2FYNfidew8x7PkUZpRDFz79u5x7ppTLU0YXMsY79bPXbFU31wFc9QxN//NUVRxWLV559QLrz5l6I8Wve/1H6jTJOqE5mdTer7N64ySFdFSv13PvDxoAXliCv1IE1mNcxrNyeMC47hcJMJE1ZuE0GZYts+Bm2cLgKNCMpzl5WoOWC/Fe9Ov49IwiO/YsXht8I2GzvtF/sPdxPv1rH6Cxp5jsOFzi23D2gCJVjtJqsqSkyAy6UVIcJpQNFYKDxKJCl47JlkFSzJ12lE3PVKxyFfxN9cKvtbqW4uaHc44fTCmbkM1hPfFF97jKwoIg/aYqhWtUtLRfX4dl7gO3jKOqNFpXYPxYmKCZrQNaUdWsYpsCxpLvySaU5egpmQD4NaqWzicjTdG13l8VwtwRr1YLPN69x/evPMeGboZEY/AbLrlKKVzFxC04rOb1ppJm7gxjl/LZ0XsAv/GVTi2js4bhA1DtzOmsTtnpDulmMzbyMb10yoXsiK1kwG9degyN47HWLqkqqdAYLM+NLvEvB0+BdpisBgsBVym0cWRZyWySoQ4ykolCF35szdcsrqw3A0caM1dLG4eF7yefXu3vNRs6kpn1IHWvomtmrJoxW4J+fgNDfNcdUjxKyIEAclIonWZxwbLwldfDSbltDPLAEjSI2RRxeEksK41lnLFkMJYnimQtTv6M2Xwiy5IAEGF7TKdTrl69ChCSlgUsTdM0yJcHgwHD4ZC3336bzc3NAIzEXksCuAjYIgWbgA/CFBLj+49+9KMcHBxw7969fy2rS45YIirfxx51MbtTgAHpjzh4I2ZlnTlzhmeeeQbnXAiGEcBBGDrSxuPxOPhnyXmSJOH69eucO3eO4XDIzs4O0+k0ePWJXFkkaHmeB0bO8fExly9fDkyTvb09sixja2uLVqvF1atXuX79OhsbG3zgAx9gMpmwu7sbwDJhiQpTKC4+pS/iIwaD4vEkrMxYdh+DgHEfxHLlWNIMnJgf4kVojAmSPgEIZUyKDBhOypVFwi0AkowngNu3b4eghTt37nD58mW+6Zu+KcyXGzdusL29HcImxNssZmmdBj7lOM1Ak6/ltQJKiAw0BpBkfMmYicEiOWQMxP9ikFDuUf4/PefjryUERX4mAFIMzMh8knPLPG21WqHPY19H8a4UxpOsPcPhMMiHRaK8u7tLs9kMAIvYHsRAZbwhYIz3Vt3b22M8HocNjFi+G28aVFXF2toaeZ7zcz/3cxweHpJlGe12mx/5kR/h/Pnz7O3tMZvN+Ff/6l8FFqSAQPLc0m4ShnK6LePNinjMn16nTzMy5ecC1sj4lbEmDDaZm7JuyZoUM6LzPOfpp5/m0qVL9Pt9rl27xptvvslTTz3FlStXuHv3bghpmk6ntFotLl++HOarWADIBpRIW6V/pW9HoxGj0SjcW8yqX11dZXNzk2vXrgXWpCR+CwgpzyHviTfUZFzFsux2u8173vOeE8C2AMn9fp9z586RZRl37txhPB4H4FD6YGVlhY2NjRMWAdLuMetTPosEMJXnl/9XVlZYW1tjY2ODXq8XwMpOp8MjjzwSPFdv377NJz7xCR544IEATM5mM/r9Pv1+P6y91lqefPLJsIa/9dZb3Lt3j/39fR5//HG2t7eDB/Lh4eEJSwulFL1eL4DKsj6JD6LMfyB4Dcv4Eh/JoihCSMqVK1cAAjAfew//fo//3R6IVVXxz/7ZP2M8HvPRj36Ut956i93dXb7zO78zvCbPcz7+8Y/z+c9/nh/5kR/hK1/5CkVRnHjNuXPneM973sPnP//5/90AYvdWSTKuKLqSEuploVLoKryflhRrqvKgXTrxhVBVpzJ7xotPaZZQFGNrhqEF8OCgqrzXF4og2RLwUFdewRgkqTW7JB/UTMi6KPaSaU4UEMoSmB9l7sEDPYfBZYOZ+nOI5FTAz8KZwH6wTtNsz1msNpitJ8zWvSyt6MLKm5rFqqJ7q2R01tC+bxleMKy9XjA5Z2gkivEZw8rbzsvtlE/zbO45eq/C/vkaLJSdnvqeZ87R1oqZczSUom+pwUIvawbo6ZKZUxROU4kPGS6co6EqJjbl64uzvD47y3ubN4MXmcsteqYxc8ff/uSn+H99+d8ieb3FwjkqNHNX8na5xqJKQp/cKda4V6wyzlMeNJZvW3mN38gfYWwz0pGrZebQy6Y8mO/xZLoPZNwperjS+8d5jEZx73CFi5WjqRd8sv0am/UES5WhshoKf2+fPXyEz/Wv8OHV63xL+zXOpUcczj/CwHh25f6kHQVZKC9f1p4xF6TI8d9s8R8nQkqqrB8u9biTEBOnlsCO+G3iHFWzBszs8jzOQDI0mJkK6clFS6PnJWWjEXzxXOooVrysT9eMomxc+yqamhVXwWLNM5bWX1Q09yuqTu59/Ob+eoNLhsmOplorUEkF44T8nqH9fIf2bolLFLONWkorA8LWhXyxLLBFVhnapAYPBcjXpQD+fpxWucYlGSbR9Xz3MmJdVKjS+nAj57zkN/5j2eJBqcoFBqhyfhPAM5C0B0ciJqgHIwU0UVSZlzaXiSIximRcYnMvv7WZItEVPTOpQ1QUXbUgUZZkrGjuuxDWBDBfB9MuUTVKqCphpjkqGTj1WoODsunZuo2kZOoW/Lsv/nu4f7FBNvAbFPc/BPlQ0TjwGwwA81VImwVmkaDHcxr9JjYxATyd9xSqUaGr9CRQrXwKfFUj0lXD95mp56GASXNnTsjtm4eW8XlzQpbrjPdblTYNc0ESmaMuCo9dg1jydTMpsE4zcCkNXZBRMUOzV6kQoNJQFcZYkolPES5aPn1e1mJTB25g/DMJC3bVTDF4JnEy9qxCW9sX2MzRqm+wYUqUcjgHZWE8yD7w1hRepqwo2v7zRiwqJEk4G9TeuAMXxjXAouM3U7zlhvGWE/gxfyE7ZOE085odKp8LlPXn2UKj0YysL6I22hMSbXlqa5emKfj1wyeYnK88aF8tGboiiZc+I1pDpL19YrF/1u4NGF3S2NRvSpiFRZUm+CWua81Pbn+dyiXcrSbcqzJ2qxWuL7Y4Ktvcmfe4Plrn9vEqo2EDO0wxU42Z+s/wlbfgsa+PqBoJk52cwbdN+d5HX2I7G5KqilwXzG1Kqio6ZsaKnqKV5dtXX2ZsM9ra/xHWr1qY+iFUYkny0v+Z4MBZ7T2LS02pNW5qaN73/sBl27dB+6ZGF7pmk7sAlvtxqahSz2Is2t6ruGpA1VawUnBuux98dH97dh5XRBTybxzvikPkiqurq0FOKkWMsFGkuJciVRhVUvxKIRSnZ0rBEwNrAkBKcSVFkhQBAloIwCMSpphBJsCBFIQx8CMFhRQhSin29vYCQLa9vc2dO3eYTCbh3LEUUp51Pp8Hf77t7W263W4oXieTSQAwi6JgbW2Nvb09gBAgAHB8fEyapqysrHB0dMSNGzf41m/91hMMqxgIjAt5YZBIG55m18USy1jWeebMGS5cuECe5xwcHNDr9QILZ21tjQ9/+MO8733vOwFmSXEnzytAh0gyhWnV7XaDxEykeQJA9vv90I6NRiPIQsEXhnmec3h4GM4hrJWdnZ1Q/ApYJcW1tLFIUCeTSWAOAQFAlHElwJGwLmPAKAZh41CWoijC+IpfJ+cUYCseq9baIFmV18xmM1ZWVpjP57Tb7cCulHkjUlcBIeQ80latVotGo8Hm5ia7u7uhvYWNtba2FkBgSXDe2tpia2srMNCOjo44Ojo6wf4Txk/MpJRDfhYD/TFoJ3NWQGQZF3FgioD+MegRWxmc9qmUNhQARK4l14vnxml2ZAw+npabxuCmUipIU2XsCKtJxkQMoslcEDDKWhvmuIznD33oQ9y5c4cXXniBqqp48803KYqClZUVhsMho9HoxPojwK0kz4rcNAazpX1lDZI1UcIkkiQJ7DJhFEoIyNraWpi7R0dHoT9jgFDaOva1jTcdZJ2Jx7v0VyzZF4apbNhIX8szHB8fB9aaAELyPALkxvNKEpQ/85nPcO3aNZRS3Lp1i0ceeeQEQHx6zmqtAzteQME4WVneJ3M79ledTqcn2KfCHqyqipWVlRB8cnrzSzaTOp0Oh4eH4fPwwQcf5MyZMyHsqd/v88ILL1CWZbC8EGDROR8mFUunReobz8VOp0O73Q6Ma7kPAeplQwb82vexj32MlZUVVlZW6PV6JyTt8/mc4+PjAKJKQvNiseCrX/0qN27c4Nlnn6XdbvObv/mb3L59m+FwGD5b489uay0vvPBCaN+Y2XzlyhVu377N/v7+ic2z1dVVWq1WYPMfHR2FIC6ZJ8IyzPP8xKaXrDPC4hTJtFKK8XjM17/+9fAZIM/5rwtxeqfjDwwgfv3rX+ejH/0os9mMTqfDL/7iL/Lkk0/y+c9/HvDyg/g4c+ZMMKQUbb5M2Pg1kgb2Tod8AMoh1GUg/AFetg3zVb30jCp9EeO0omxp0mFF1fAm9sp6qaU1eICwBg9t4oEKm0I6JYAXOpJ8CnCj65AD71kIkhqrCxdIMcnceuCjqoHKRIWAFM8wcsEvscqXgS+SYlplnrVSZfJ6fMFm6uLQeBBNpk6FoteeUnbaFC3FbAMahzDZsYwueg+6RVuz6CmSqWa2AdNNQ9FWOK0Zn3ekY5+E67RmseIZFbp0XDm/R+WgAFrKA4eZUmTKg4czp0jxictVfUcSuLJXo1oGh0X50AhnmKuSgcu5ttjmt/uP8uXdi5xbGfDRS1eXfWscyVjhlOJjjXuUg4y0gK/VaZjzzlXuFGv0503M3PflufSI72nfoaNyjqzllw+fZnNlzHHVxiycT7cGElXRr1pMnICCFSw8gKgaFUZpjLGBuXkl7YRht1fNuXOwCpml0Xe8tLvDv/PYl3k43+U9qeN3xmscTFsUbQ9cDMYNOg7PhkuMBxHHU2jnJ6SSAlp4pqIOMkAcuPEE8nTptZksgWoggIfKOshSyk5KOq69NOvzS9qznvtxXLR0AGRs6tloVdvhmhUMNenIj8GiVYMmrcyDLG97b73WXUU6gN7rE/S8RM0r9ErGwfscbM+xhcYcpKy+kNHetaTjiiqH6brm4KmEqgFl24bnd6loWvXvYVN6XzcXWE3x/Hf1ZoFPqPXzzRqF7XrvPl146XTVTJap0s7Vcm7nASMrTEcXzff6Dy95TWnRtReq09oHxhjZrHDL/50PUbHNBJdpqkyD8qzGRFsKZ3wIirKkynJcNCjbjsmOxmkvYzaLel7PawNsAeFqUGvm/LPZmunV6Du6bwxBw/15RoUjT0pGDRXSnV1mWfSW4JAu/HuryoN31UqD4bmEeb1E6zqsxM1MYLHGjNmWmQcgM2yYRCxB8OuSrF/pHJKxpewolPXjywN3FjO3S0Clngs+IVdB6QKA7GWrhGRrAW61cnWAiqKhPJi4wLBwhoyKCs3QpsxnGZlR2MTLYHXh2aIAo0uQDhTjnZxs4CisX78lqT1RFpv7ueIy6xnohV8HF/LHpgJjHJ3OhP6tZr2++g2pZO6C3cXwYtSQGqomNbhIGH8CnMo9nlgPtLetACidRpX+88/mDr05Z2t9wLNbb6NR5Crhr5z/ZXJV0VCWC0mTYzvjN68+Sn41Y7Zde/rVba9qX1oJYgm2CoZl6nflJbpmVrPt+n5HYrGaMNk02IZltulBsj/1xr9Nf9akP2oy7zdQM42Z6prd559PZM9Zwz+DD01y2IZlVCZ07vjiq2woLmz2yXXJpMpomQUtFmhl6ZgZZ5O+n1tY+rbFipl59qHTtPWcdTPCOo0bJ5T9LDRyMtE0jr1NiC6oNwr95kk69p8tZcvLuqumo2w5XKtCpZastWBjZUwnKdlpDWgnczbSMamuOC6bdMyc65MNDss212cbnM0GIZn8G8e765DCRrzTpPiJ5YFwkpElACIsGVUxCCcFacxyEcBFAAA5TgNi8lopDONiNmYqyfkFNBGpW1VVQeoq6b+LxYJ2ux0Ao5gdJvctxZMAjAJY7O/vB8BRnmc4HFJVFd1uN4Agx8fHKKXY3t4OcmFjDGtra8HMfn9/PxTXcRF5WvYp35+W18YgmPxsZ2eHCxcu0Ol02N3dDV6CIgmTgvvJJ58Mktejo6MgXxOGVrPZDPdmjGE4HDKbzVhdXWU0GpHnOZcvX+b4+Jj5fB5kajGbst1uh6JO7jEGe/b39xmNRuzs7PDMM8+wt7dHWZZsb2/T6XSC3E36QsC6O3fucO7cuVDExmCGFNgxCCjMwJi5KUClFOKnWaAxkCZ9MR6PA/AXj4kYoJnNZoHtCgRwaXV1NfxOgKXFYhGCHKQvwMvn+/0+s9kMa23w2L93714IgpAk7el0GuaqAJp37tw5EY4ifRoD+KeP06xNeXY55L0C5kpbxWxDAQPlmvFYjuXip+fr6evFsvAY3FRKBYZgvBbEsuN4Psj6JSBFfJ24v+XcwkAWefnq6ipvvfUWQLAxs9byhS98gS9/+cucP38++FYKuBp7wcpYkvErEl9h0MV+jNLvMTsxPkcsB5dD5rKwpWNAWkDGeJ08vcbIP5lHcb/EQHN8H6cZv7JGO+fCmB4OhyRJwsbGRgDyZR04zWSUPpJrnQY4pS2F3SiApQBJVVUxHA4DMzaei+DlurI5EANTMVgbj9X4/4ceeoj3v//9VFUVAnKstSG9WGS8u7u77O/v88gjj/DBD36Qt956K6yL8sziaygbOeLZuLq6Gn4XexAeHh6ilAopy3K/ZVny/PPP873f+718//d/P9Zajo+POTw85OjoiBdeeIGDg4MTG27CBJT/syzjiSee4Nq1aywWC3Z2dhgOh7z22msB0BTmYJ7nJyTQ8ViJP5+yLAsbJzs7O0FKLzLzxWLB7u4urVaLK1euBJBUGIatVotmsxk2+2TNkJRtSWSeTCYhZEv8LGXNie/t93P8gQHExx57jOeff55+v88v/MIv8EM/9EN89rOfDb+PF0xY7sz8bx3/ptf8F//Ff8FP/MRP/Gve7Fk34jMlRab3btMYV/uHZYqi5a+RjS1F5pknVbFknyRzR6NfMUkT5qsKrDeKlyK7bEF+7EIysgCPIrPzPob+X1J7wgU5s1oyFapUGCXLhOdquTb7czpH1fDFafO+8xLMusByThhYmr2yG/zoZi5loznhsGbPuMSB889QNRxqvGRolU3PMinanu1UNj1gMVsHVcJswxeGs03o3IRuOqOhYOL8v1zBomYdaqBbF2ELp5m4hG6dgHxYL+yF0wxdQuES+rbFlhmQKss/P3iWX736BMUk4/y5Q75t8yqvzndIxt4Un0qRDj07raNSVKkoO47z5piPdq7S0Tl/auUqv5w/zfSoNrh3mr9/9F5+uPc8E+eYlBkXun1mzkt0bQa6UqwkMxqqYFVXgME67Vk4lUK1/IK4OGpw+ETCn13/ArfKgsLBvxi9h985fBh1swnbC1QJZ3sDLmSH9KsWqRqyaiaMZjlV01E4zbzfoKMjgKqyuDzzoFIMhkXgiEv0MrwEUM0GVnYa69fFEttk7uW7HmBwJOMSXXi5o63HAkCx5p83Ganl65UKabTpWNG5lTHZcYzPOza+7siPLYuuZnSuw8pVD8AL4DNb09z+eJv11yqyfsn+e1Jss0Tfz1l7XdG9XTFdV4x2NIuepmw7bOKCPDqwDBPngwXqPkjmfj4H0AROgIeu9gTUpQsMRSUb6dFy4jRUSofQFeVUzWqrAytcDRSkIKnO8lrrTN0X0W567Ynoj+iPqsp59Ajf/q5mmJYNH8okbNFEWRr1jY5tDnrmf1anIy+6Ksz1xYpDGc/2dAlho8IlfpyrUqGM3wyxBkh84yTGRvflNyFsonCJo3lP0Th0lC2/abFYdWhtMQuLOZqw+ZJheCGnaHsm96JXjzENi46ugaJ6IyZCecUaIbAF634q3NLbsLlf79ZOfGq9HFXu5d3O1ACwvF+Yb1qhF96C4jToIqxs6xQZXgZd1L6HHjhUpKqiQcnMJfWz+v4pWp79iPNMWzNTATTNj/3GjlOKs0kfgEmZYY1bDjDlGYgAbaVpmoKy0pSF4bhq4LQjHdceupm30hALApv4tVaO/NCRD3zIh63Hi03qJO6W/3Cxqa6DVzzjLaWirS3/j53f5OLFf8Gq9p6HWilSDKkyfGVhGdomf6hRYFQertdQBpNUNA4dutDMtvxnmwSSiBS3cWiZrWvyfUM2hGTiUKVvJx/g5Bhe0szXvKR7+KBPSU8HGjPx4/XO/3IZ5SBNwSTeD1A5n1psU4fNre9v7bwHqsiKa2ayM46i42X0xw/Dt29dI9UVa8mYVFXMbUpXz7BO0a88aNjTE84nR94uQBfMbIrVmplL+eTai+TPllwdbnLr1y7T2HeUbUXRgdmGo2o6qrWS5sqM7ZURrXRBL5vyUHufzXRIQxWkqsIoy6JmB1ungrepVjaMwbVkTFfPeHu6xqvDM3xi4zU+2X6Vn3Uf/0YK87vwEC8zSXcVpszpkAIp5qT4yvP8RAhBDOIJiy5mVEiBICCknDsuYE//LE5TjmVocRERAwPCTpACR9go+/v7DAaDE1JKee/pAnk+n9Ptdjk8PKTdbjMej1lbWwuAjXg7CagkhvRHR0dsbW2FZMk8zwM7TQC1mBEUs2xiAPE0e04832K2IhCYM0899RTOOV5//XVmsxnr6+usrKyEgANp/9XVVdI0ZTqd0mg0GI38JtNoNKLb7fLAAw9w/fp1ms1mAK4Wi0VoC2E4vvHGG1RVFZJXZdyIXFz6WNiHi8WCJ598kldffZVLly4Bvtg/OjpiPB7T7XZJ05Rer0ev1wtAbCzlu3r1Kk8//XRg/kl/S5tJ0R4DiOJxFpM54vESs5diIEvGgcwLAR6lTxqNBv1+P/itiT9YDK4cHR0xGo0CU3A6nbK/vx98JwVYEHbsmTNn6Ha73L17l+PjY1ZXV/nSl74UwniuXbvG5uZmKMAPDw+5fft2GNMxyzeWkMdAQFyzxs8Zt2csbZWfCRhwGvSOGbLv1J7vJJc9zXwUID8+Tq8JAooIEHKaUSnnl/Ei4Q1pmgb2V7xWyDVlfZH7lnaSNNmY/Sv3IQB3mqYcHx9TVVVg3wmYJ+eU1PYsy8JGw3A45ODggE6nc8LPMwbZBQCNmXJyfyJbFuAmXvtkTZD7iEFNOWStiYF/mTcxMzFef+R3Mqdidro8u8jKYzZ3HGASe+DG1hPyvLIGyzyWc8q4EhC53W6HjR9Zi+WZZrMZvV7vxIZHs9kMDF9Zv2MGuFxfPPZkbBwfH3P79m329vbY3d0N6++LL754AtCWNUsYc2fPng1gu4CGWmuOjo5YWVkhyzIuXrwYZMPC/FtZWeGJJ57g6tWrQdYtm095nvP222/zD//hP6QoiuAFKJ9vMfAmYLiwCWUN6/V6TCaTEGaysbHBq6+++nvAQ1lL47F3mtkqfXXz5k0effRRnn322bBGbW1t8dBDD9Hr9QIjX5jLMkZGoxEHBwfcvHmT8Xgc1tDpdBpSrE+HDjWbzQAYC/tdANn/QwHELMtCiMqHPvQhvvzlL/N3/+7f5S/9pb8EeJbhzs5OeP39+/cDK1ESxY6Ojk6wEO/fv8/HPvaxf+01//Jf/sv86I/+aPh+MBhw8eLF8H3R0jhlPciXadKprVk6yyRVm/piOJm5+vXe182nLqtQYC86xkuRy2Whp0vv3ZUNfYFV5jUAaKFoqhPeZzZVS4bbDLJBBRrmKyYkLIcAh2QpI7WpL9YEGLKpT7EUiRss2UAosNr/Pq0leS63pFQ80D6gX15i5fqCbJSSjiu6t31R7rQiHZUk84xkYmntK9JhhU01VUNhdz14UrS9f1068qCpWeBDHpSiW39mLmoGYkMZCuc/8CYOLIqWKjE4Dq3jdtXxgSbKUriEhTP0qxYHZYfz6RFfPzpHllV84qGX+PLuJf7xSx/hP37fbyAp1LpTYE1C0YGWznDaF3ebpuCM2SVVHQprmZYeKJNgiJlNscC1YoX7ky7ffvY1JlUeil5lvVfivWKVTCkKKjaTAU47zMhQteqdlImmaMO/GDzD147P891bL7GVDPnExmt8ZfMBsB4g1tpPzEz5RFGjLNYqXNOHwiRHSZCZUlagPXBlMxPkgKoGoFUFlBbbqD+4JOBD/jCRFNaaTSuMNC+vrydFZTHTgrLZrMeUCiC09z4kSE2VBVVUPkzAQNlyTM9XJAND644f65Oul/y37ltsqphsayZnHIvtErKS9F6GLpxnH53zbD/brRifTxhdMhRrFXqmcZln/anSgykuqUECWbMsATSKE1192vGyfQREEW9ENDiWrxO/RGEbQv197ZEYaMIANetYQmic9ixCH0jiUKa2OKgBMmoG8Gn/Mt9HLJOCwTMPI4apstQsOccC44ENZ5hVCcWKY3JGhWf0HngKV2qqDFziQX0BImcuRZe+LbS0Uc2CbKV1AefqEAcjbadYrHrUQlcewEqmilnhA2fK9Tb9Kw1m6368CDuMWt6qrKNxoDxQn0BXT7H1AwpTUJLnZd3q25ZfbwtHduwHaNXyoR3JxAN4ZmFJJtUSHNR1MEXEOvw9bK2onZ2BTjpnUW8EoCBVBQbHzPqd7YYq0cpSzpPAxE2moCu73FhpOdSeB/zmqzqA1D09ZeE0pdNBcuyUQ+FB2RTF2FlyU5ImFbZSKGcwc89ai+XA6cR7II47SwlrlTvm65rZhu8fCfVwBkistxKoZfo2g/zAMxoBXl5s0NLzwJCunMXi0CiM0nw497zw5UD0R0c3+E+e/lf8l7e/j7NfdHTu+DWkaFOnKsNkx9F/jyVdm9BsFIzfXPUM6GaFaZe02zNa+YJLjSmvvnCJ1l3PaK5y/0zlpsW2Kr8xUM8BFCSNkqrUaONwpfKBJNXSexULKsFvjpUKVSofIjZ3LDYsa+mYSZWTqgrrNC09p6ELD8gDGluDeAlGWTIqMl3RZs7Y5qzoGX9q4wukmxV/5/u/k8N5i0dX7rORjrmc75Oqirb27NpUlVg0g6pBWy8onJ+3DVUwq/17NZZ+1aKBBxYrp8hUxdjm9MwEgJ3GgJZe8PzwEl/oX/F/n3wDPHxXHVKcxcWISFNPAwmxpLkoipAmLK+X4lFCP+SIWTlSCMdFPSy9y6y1Jzy8xHNMin95T5x6KuBmLL2WAkPOJ8ysWDbdbDapquqEfE0K34sXL/LSSy8FtoTcuwS1SLiEFMvOOdbW1oLXl/ieCdtN2BoCRsnzxKCXFM4iPxWgRQBdKfJarRabm5uhoBeJryRIr6+vc/fu3cB0AU60vfTNYrEInlc7OzuhkNdas7+/f8KzTWufAHzx4kWuXr3K6uoq7XY7FKzC+BOwQCRskiZrreXg4CAUgIPBgAsXLjAej1lfX2cymYT2F2AgZrYWRcF4PA7gZzy2BBCJATJpr06nE8AKKXwltCEujgVok3Ev99FqtRiNRqGoFlanSAsF7IqTZ7XWrK2t0W63g59et9sNcvAHH3wwBBesrq7SbDZJ05TxeMzx8XFgkt67d49er8dwOGRra4t+v88bb7zBdDoNwH0MQMnYjgGyd5IDnwZLT/88Bpzi38Xtc1oiG0uRT7/mNOAr/SM/j5PZ42eQ9Uf6N95cOM08i8G+drsdzisMUtlcEG9PAYQkMVwACzmXrB0iP45BPJHDitw5BjtkfIl3pbCxZB4Jo1I2X+SQe4mfL8uyEwCic46zZ8+G4CJZ52LgN15r440SaWulfAqz9EHcv/F6FrN543uXDQ1pT7kXkS8L46/ZbAbLApnL0vfxZkjMjpW2F1B9MBiEjS0JGBG/UQnXiOedhDJdunQpMO+E8Xvv3j12dnZOAJxiMyCfYUdHR3zqU58KAS0xQL2yshLat91u0+12T9gzfOxjH+NXf/VXabfbnDlzJrxOPADFx1M2D4TZGI+7w8NDLl68eCIwS6TaAuzJGJagHWFzyueWzL9YVi4gauxXK+nuIpWPx8jpteSdAGjnHK+88gq3b9/m0qVLfPM3fzPf+q3fymQy4fj4mL29PUajEYeHh4GZKYzI01LpGECXz0a5toCxwqqNPxcHgwEHBwf/RsJffPzv9kCUQ/5gevDBBzl79iyf/vSneeaZZwD/gfrZz36Wv/E3/gYAH/zgB0nTlE9/+tP8iT/xJwC4e/cuL774In/zb/7Nf+01ZPfxnW+AEHzgDDXLUAe/twDO1LJgb3buCzFduhBaArXXYU0o0iVBemzmDmd0YDslDkrqEJT5EtiQ16a1pLBsKJwxgXno5bMCHNYFufPXyY/rxzH19ZSOjNhtfe56h2Nua8+wlL/9/Lej326SOPja7BLWaYq2JplaJluapL1kpmUjS9HJKJqK8ZmEtdcXzDYS0rFluqHpXV0w20hp7leMzhkaRxVzp2kcVsyrBIMKEuVMKd4omqSq4oGk4lrRoG9bHFQdn6KMpa0X9CuPmm8kI1pqzmG1ys/feZZv2XyTb229yb91/qvcXfT4nftXOLq9Sro2o63nOOOZgr7dPGMSfCFuMxtwprkreK1ocjxrYDPvOQWeGdVShovJgMJqHszv89L0AuIX55SipRe8p3GTXGkaKuFKdh89097HUsk4UKy9ZrkzX2WrMeKPdd5AA3919w97Nthc41RJO1kwrJr0sgmp8s+/mKfoVskCQzr0xfEyfMOBtcsCUi0BlxBWkvmkcBdJGm0n8+BSQR1iULNelWeZ1aGs4BxVI6FqepDIac9otSmYboE9TrAGP7YA20i8NL+Axr7CHiesXrOsXB1QNVMWvYz+wwlHjzkW5wt0WqHu5fS+ltK+V6HLivmKZnRRY1ulv1/jKNZtkEE6jccxlGcUucQzmdCeYespSdo/lzvJNPOBJ0uQLvxceaBLGIMx2BiSqaPQBx/CIhMe/94KUKcSrJXy0uA6AThcrwYPbT13Q5gHYOWPCgEvBVCM7qnKFVvZyINZWCY2p8BQOkMy9t6ERUd51qj27DY1M9hMkY4cjX0VWM6LmlEbB4/4NoBWusA6R1mZmslWg6ONimRigq+ba0DRcah6DCX9CSvXfbK43EfRcZBazAya+yXDS1m4VlsvWDjjQenaG9YsnGdg14N7bHPM1DNRs/4CZxTpcUoy8WufNT4Dp+ia0G7CNPUhHT5lW9fjYBmgEzEVgUx7MGnsMrpqRkNVzJyhp6eMnfebnNgc53w/2syzAlH158LcYea+fdt35sxXm9gUkgX09MKHsugyAIiBIVmn6c4dFNZ70prEUhYGlXqQsmjVoVA5AQxerFiyvg7szPHFKswVtN+8MHNFmVoftFX596maES99fjk5oqEsUIc2KQ01iFi6gso5LJZDW3KtWOHz40d4a7rJvVmXr795gY2XFLqwHD1m2Pn4LR7oHGJR7OTHtMyCymm6ZkaqKu5eXuV8fsT9YoVJlZHrkoYu6JgZty6vMnar2G6JSi1K+XGVJRXOKopZ4ieOg2phgn+iK2rmdz149VSjCwMWkokHmRuHXv5tphaXwZnkmMIkVPUEtk4xtjldPSVTlQf3rPfCbFAxcyktPUcDbT2ncAljm2PR/HtnfqdeDjxYKAzCoW2g8bLnoW2SqYqhbdDVnrkuAKLGYtH0zCSA6QtnaKiCrpkysxmpKlk1U46rJucafVp6wefUI99gIL4LD5FJxT5LUiTGTIQYQJHXn5bYnmZuxeCGnEeYYVIow1J6KOcAAqMtlo/CSf87SYzUWgfpmjAcJRlU2EjWLpMye71e8NQ7e/YsZVnS6/W4fPlyAEiHw2FIcpVDnkWKRynknHOsrq4GKZkUm1pr+v1+KOY+9KEP8cgjjwCeoCBJu7/1W7/FG2+8EUAIaScpJmPW5c7OTih+wYdtbG5ucuHCBe7fv8/a2hq3bt0KfSZeYALyirzPOUez2WQ6nZLnOUdHRwFcnEwmJwCf9fV1Wq1WSNqUIr7RaNBsNk/InQVEFVB3ZWWFfr+PUorDw0N6vR7GGHq9Hm+99RY7Ozvh2iI7FjarFMcPP/zwCbZdzEYVIBWWrCuRUcYMORnP8nwxEzZmdcZAoITDCOAsnocCMMlzTCYTer0eh4eHARyToJqvfe1rAOzs7LC1tUWn0+HGjRs459jc3AySZvD2WFprXn755TAG33jjjTAfpH9i8E/mi/RvHCYUP1sM9J3++n9LVReDT/HrY8m3tG88T2T8x2CE/B8z7mLmnfw8/r0AYtKvzWYzgE1yLzGLMU1TNjc3f49seD6fB+sBYZEJm0mYzgJYypoTg49xmzcajZBcK2sXLIHr2WyGMYaNjY3wLLLuCNAuMmt5LmMMnU6H9fX1AIQK2CmbERcuXAjA1HPPPRfwDFmzBHSUdpP7ihmd0m4x6zkGIOP3xBs/8vwyL6WthXnYarXCeiPS3HPnzrG7uxtCMQQwivs2BrsFpJU5JPN3c3OTM2fOhDVNwM69vT0efPDBwHZ/6aWXqKoqMGaFdR1vOMUMNzmk72XdaTabrK+vs7a2FiwoBIgTQDDLMjqdDtvb2/T7fVZXV3nve99Lq9Vib28vsBiFjSzsTdlImkwmgS07n8/Z2tri6aefDpL40WgUNtvk3gVQFXm2jJlms3libApTWUDVoijY399nsViwvr5OlmUURfF7JMBxv8SbXPERb8iIv+Hbb79NkiQBxIzn8mn2qYwtAU7jzYJ4I0/+j8eGMNpbrVbwOv0/DED8K3/lr/A93/M9XLx4keFwyM///M/zmc98hl/91V9FKcVf/It/kZ/6qZ/ikUce4ZFHHuGnfuqnaLVa/Ok//acBT/n/4R/+YX7sx36MjY0N1tfX+fEf/3GefvrpkMr8Bz7qAr6qpW1V0xdaZuElxgKueF85L5ksm/UOZbGUoXmvIxVCAjwwsJSESqKkN9j3ycwAZmE9ywh8YVQnaAbpqfL34WWJGrNwQYJYpT6opGp4kAoH003tQYSu9zBceasGQypH0TYsOorurYL8cE62kVCNUrq3PLjwM93vwIw1O2PPJmsc+sFTNj24ulA6tEfZ8gX7fNUDrmVLUbaMB2Dd0n9NWGB3P32RD73wo7jM4YzDDA1rL3up88Z33uHPXvwi4IszoyzrZsSGGZNm1jOsXMqvHr+Xz957mEZS8uu7j/OP/+UfYvPJfe69vY5qluipppq1mL03DSEqeaMgmTSZbzgqZ33a55kFX19sMqyafEfrFjO3wuFxm97cS7JTVbKejGiqjKH1TKa2XvDa4Iw3+C88qNMxM76zVVC52kzVJeiFIhkqmo/7Hdx0oHDa8c0rb/DP732IXGms87JkMzDe5B9oJZ6Z0tP+fRaNtYr1nvcoE3apnhaBSeh/4Zl4CLuU+n/n/LhyhLAJ8EzBZGaxial9MOufu6WPmC4tpAkYRdFxpEOFzV2deqvIXm5Rth1l24dIJDPH3vv9LmPjwCeHilfn8aNdWvcKlHMM3z+HYcLqcxntXU+nnPcch08aio6jalpcWg8Y7aBUS0DE1TepPOtQLbSXb8ZrlVOoQgWwT2Say98TQBORLcMSPPTtEF1PflcDRkpST2vGpgBPQRJqomvUa4bfhFCBWSigofSTq8mTAcDUy/XCv4nwc1X59OG1dEKFwrolYDEuMqqGZyDKtc0ETK5YGEcyduQDy+Ss8T6sufWpw85vfPhnqAOkWim5tlQ4yqr20ayl4mpmKFtL1CKZAEoxLT2TutjscPCenKJNGLfpSLGoPSnLlmG6U9G8K76hJQ1VoFsl022/+2Iz32bNJ/qMneWBdJ9v+rZXePnJMzQ7I7abQ+7dO///Z+/PgyzL7vs+8HPOXd7+8uWeWVl7d3VV7wsa3dhJgARIScCQlARKBgUTlj0amSE57LElz3jkGDlGEzOO8QRtM0aWaEsjk5Qo2gIlG+ICYiGWBhtAd6MX9Fpd1bVkVeWe+TLf/u5y5o9zf+edfChAZMw/JgI3IiMz33KXc889753P/f6+X9LnZikd2mO16leBo/a4nBo7KPp3ocA75pNY/M4DGOeBKw3PjXZhTlnxhsRYP0RGVg0eDgzxkZmcaw1HZXtijVYMFu21Gfas76smJ9T5xLYCrDKzuOlUUlAJxsRhytJsh/444mgnZvdxMHFmrwkXA65s6rh3EwAsMDWBgdx+PjWvwtH5wPoaJrn9TAvE71TxfP88843vcier8J1RjV4es5XO8OLhad45nGf/sEa6X0aNFXpkx3WbEG7beOXtnOrmmN6JiIUPbPDk/E3mwh7DPGIm7NPNyuTYsbKsxpTKCWU1ZrFyxJ1klqoes5s02E0a9PslVKJQYY4Oc0yuyIYBGQEkGjXShaLVeidKmXTUteBZkrB1oWJFYUNJIhjNKnprAWlVc/7cbeJCah2gaWdVGnpIRGbDc1RGQkBERkBOpCxATExAWaXkGDKV01QJR3mZWGUuobmpRmhlx/daMCZSOf/j3vt4on6DVtCjRd/5KUq/auqizFPlhfJd09ADp04s6zEBhoWow0zYJzEBH629wT/gx/nR8sO3TKsA5G9R5/im9r5CSyadMsGUyaOsc7pkVyZ+MjEXVZdftuuX0ymlnFpByhhlQikTSEnQLJfLrtxTJkdSGhuGIXt7e26/5Aa/+Mi1222nVup2u9RqNXZ2dtzEXpQf0iYy0RGA5XsfDgYD1w6+R9r+/j43b97kZ37mZxgOh66cTFQ2opoSQCTt4v8tgRq3b9/mxo0bvOc97+E973kPGxsbvPTSS65EVkpjRUE3GAyOQQYBhltbW66UTmCfUsr57UmIgygjBbp1u13K5bKDiu12m8Fg4FI/BdAIhKlUKiwtLXHlyhUX7iLAVia+AiF8RZjvlXXu3DkXLuEneAvsgYnCTY5DFC0+cPaBiPQhH67I+2Vy64NaKTmU7UrYiYSk5HnO7du3XTLp0dERSinuv/9+bt68yZkzZ2i326yvr3PmzBkGgwEvv/wyrVaLhYUFut0uN27ccOm90t8EcvkTeQE6vgrRB0Dyvw+3pkuV5W//OX/xVVj+te8v02XE/n7J877KbFrZNA0R/RsF8rd/ToIgcKnqPhCT/itJslJCL0BN0neNMU75J39PQ0NfZe2r+KTPAM6PTSCSPC/qQoCDgwPyPKfVarkACYDFxUVnGyCQrtfr0Wq1+MxnPsPCwgLNZpNSqcTu7i6vv/46SinuvfdefvEXf9EBKtnf9fV1B8b84/HbVKC4D2b8c+q/Vs6RfwPBH7/9/0UJOBqNXOK0nE8Zl3d3d52Cz1eAy2fB8vIyzWbThaTU63XOnTvHu9/9buI45rXXXqPb7ZLnuRuzJeQG4MKFC+44er3eMQXe7Owsw+GQc+fOufPSarVc6e7e3h67u7s8/PDD1Ot1PvGJTzgQKu0k6jn5jJAU4t3dXQcpr1y5wgc/+EHiOOazn/0sOzs7rr2lraQPb2xsOOAJONDdbDbd2Ob3SSnfnVY8C3SWzzMBd+PxmMPDQw4ODqhWq5w6dYqFhQXuu+8+Pv7xj1Mul904I+Bc4K70e7m2xEfUvxng3xyQfiNhPrIueU0URTzyyCNcunSJN954g1deecWpUn3lrO9H7I8J/rbkeVExlstl5/f7x13+RABxa2uLT3/6007O/8gjj/D7v//7fPSjHwXgb//tv81gMOCXfumXODg44Omnn+YP/uAPnHkqwC//8i8ThiE///M/z2Aw4Cd+4if4J//kn3yPb8MfeynUN+IxJYnJAv7EO0zltmRZp4ViSyZhqoBkolTMrF9iVoJgPCkJFZAG9r3B0JYfJRVNWrGv027So5xPopQ36wKMmEAV/owWeoYDu+60XIRUhBSluJA0crJYMWpakFTq5ER9ODoVUZoJaF/QfObpr/FP9Psg1fw3P/Eb3Bgv8o9v/jmrrqza7QRDCwx1UUJb2S7kzpGismfIIquKGc3owgdPUzrKMQFE3ZxxM2D2rYyZq3Dwl3osNa3Xy+F9Zf7te57jD3cushYd8EC8x04W0yiIV4QhQfHPD57ii3cu0hvGnJvfB+CNl84w95qis7dE8NCA952/yjPP34+Jc6p65IBOluniWHKO8iFhR2FOZwQYzsfbzOgyNTXGZMqGqERWfZh55XqjJGRoIm60Z6kE9lz1TiqeKl8DSgzMmH/dW+W/fPOnnO9XGGQcZH2iri0nbwZDykFChFV0jfOQsKcI+/biLAW2g2QocnIeLt3CZIq5igWKkkpsZJAwE+AkZekCJiSwIKto1z9dmMIgsUm7Ia7Ud6LC5VjwSB5ot87ylgVHWdkwnrPlxPGBJq3abde2bYjFqBkwWNAMVgzjhZRoP6T0jKa3FGKGGaUDC5L2HgpIazl5nFv5GKCHVrVnAlH8FV+OwiIcIw0mHn4SlqJBjZUtU8S+Vzm4YgpwX6QgF9zrbnBO2gE4VsbsrOr8x+BY2Ad6AgqdWlEgoNfGsklbvmwsCJZNF3BRyovtY+DCWjzY1c9ihoVvWkOPaRcltmFPUWrb6xZjr9m0YtNxw6Fh2NKMW4aoqyAoQlSkGb3zroyhGQ0ZGsMoCakUgC0rA/WE0tWyDcapw2jeAppgs4TKc8KDAbNvBRydiUhrdvwYLOcQ51YZrXGl8AK9Hiu1+dcf/P+Qf1CxGOTM6jKRsp3iVgr3Rhm/cfYrcNa+vp+P+YvDn+VaNGtVZiNbQmyg8J9Uk/bDju2SepuVvHPitT1AOZiUlJZVQmKC4no0RYBK2QLG0AJ4CTeR9+vEoMfF+XJ9guI8hbTTnINR1boQlCFojqlUxjy+couqjiBP+Kvzz/A3FlNOBhHX0oyf/+Z/zGg+t/vtk2eFLdct+nMwVpix7If9P+xB6SgnPtRkFdh70JZejxYy0roN2/kfvvbj/PfBj6EHukiHLj73iptdYQihks87Q1I35LXM+rwaxWA/IByEjFqaB2d2OFfasX6ROiEgZxj07f8qoaEH9IubLba0N3PlvIkJ0Don14Zgs4QeT9SDwcgUav1inAogjybl2P0T9vMubaV2zAgMYT2h1ezz8MIGGgv07qttMxMMqGpbhiz+g2WVuJtWQxMRqZSGHrIS9OgXyVLLQZfcA8kSstMKLNCLVMacsiAwUIZOHrMYDPj7uz/G13/13Rz91Qq/sPAsR3nZKQ5zo63S3ARuPYADjC3ddyXV7bzKK91TaJXTS0tU9ZjwwKrAXX/+0fJDsfgKFPHb82GATCZ84/tpcDhd/uR/P/ZLj2VS4YMJv3xatulDB3nc9+byQxOkTFGUhhJgIgojKTkTzzAp/6vX66ytrbG+vu7KTBcWFpifn6der/PNb37zWLm1hBfINvzJsLTb4uLisTANwClFnnnmGfb391lbW3MTUvHGW19fZ3l5mVu3btFut93kezAYEMcx9913H91ul2vXrtFsNvnwhz9MlmV8+ctfdiooUcfIewVwCkCUH/HmE38ypZQDZeVymXq9TqfTOXbu/VCV/f190jTl3nvv5ZlnnmFmZoZLly7xYz/2Y6781leeiOpFzolMgFutFsAxn0hJMO71egRBwNLSEuVy2QUX3E1R55evTu+z9D/pr+IhJmXp0/vkK818QC6wW2De4uKiU6CORiP29vZYWFjg3Llzrj+I/50oyfI8d22+sbHhSutv3brF0dGRU0bJPsMkGAImUF6UlT788e0A/HJA2Xc5Hh/Q+de9f635/0/fOPDbfvp5mCQz3w1IToNFH4jebX3TyigfKMg4ICWsUhIqfXk4HHJwcOAUsgKLFxcXuXbtGvV6HaWUS1qW9pXxSUpxpZ0//OEPu3Hm6tWrTgVojA0S2d7eBqDX67ntNJtNVzINOKWzD2nkHN+8eZOXX37ZqewkPGhra4t33nkHYwy//uu/Tr1eZzwec+rUKc6fP0+1WuX06dPs7++zubnpbvZMK7qkLQUATqtQ/Xb3AfT0efBVnzKWCFiVmwEyzk4rRH1IdeHCBe6//36n5q1Wq7zwwgscHBw46Hvt2jV+6qd+ijiO+epXv8qNGzfcmCo3NQTMyj4vLS0xPz/vypvlZo0xhs3NTddf5Fy++OKLjEYjLly4QJIkvPXWW84bVm7y9Pt9B636/b7z1RQrC+l7c3NzrmzXh2AyDknptvR7ORa/LyilWFxcPKZ+9j/DJKlZbsJJf5fxReBnr9dzfUuUi7u7u9y6dYtOp8P169c5ODhwlQB+6JX0faWUO6Z2u33shoX/fUD6lDwuoUtaaxYWFvjkJz/J5uYmKysrXLlyxSVzT1/ffp/zlYrSL2dmZtxnt3y+SBjYH3f5EwHEf/SP/tEPfF4pxd/9u3+Xv/t3/+73fU25XOZXfuVX+JVf+ZU/yaZ/4HIshKKYtNuQEpDS0EwXINGHLfkkvMSpmSbngGBswxWGs7pQxClGJeWSfEVpCBZG5rFN0NQp6JHdblpWRAOroCgdiDJy4qsYdS1cyGIYNy0wSisWYOrUrq+/rKht2Nd1Tlt4VNs2ZCXDmdKuPeYw52y0T02PSGvQupIzrtvgiGBkS7XDYc7hPbakWmWKIDH0i5RMAZ95YD2w0opV6RgFWVlx+FACgeG5p/+ha5/fPHqAraTJu+du8PLgNBejPcoq46XRCf7VzhP8309+DoBuVuK+2R3GecDzL93L4rc19VlFNMgJxopyZUw9HFvwpGErnUFl9hyavAhTaabs5IZwAHlogxFqKiUnpJ3XbbuVbWpnufCvSsl4MA65Z26XxAQcHtSICx+tweN9qjrl45d/lnsbO/ze772bmSswOG/PTSVK2c9zoq6FFmWVUAvHBEqh0Vw7mifsK6KOhcKVIEGrnLPhIVDiRDDGjANKYcp+VkePTQFCcguf5AtCrCfwUICXA2O2DQB0ktsyeixgFk+4tGIYLuBAtLsWjC2BNsV6RnM5UUdTu6OIuhYMdU8bhnOKyjYcPqQZrhooJzAMqNwOmX8upH4nxWhF95SCKGe0BONZhYnMxLNMlLaFYpJp1WGqj/+f4VRbqghJoIDb4imnR8qBfXmvK19W3nHKde+gSQHrPHjoQJMqVIgFCDRKTRSKRVmsKzf2VHsOQubeeFOEboAojf229347GKmcSrGbldjL6kW54xHtrMrt/RlUCElDuTTj+MjYMuPABr4k9aIMPgVyq7ILB7iy7GBs0MMEZaA9rtDQAZ998le58dgskUqJVMZ/feujfDdaIy4lxGFGFGbs7jUoXbVfykw5ZOfxmMFDA4IiiCUOc4b7ZWuFENkDUkXGhQC7++Mq00tmchYDC1BGXinteprz5munUPcMGJ4I6d2KaNwo/P5QDjI5D8zA3hgKRpNwKl+NKkvuqcKGJnJBFzbMwgbXBBjUKHA3k+z6TeHbp0iaOWr9+Jf1PASNZj7I+G/v+S0Oz5XIUKwEfRpaMavLvDbOKSl4rFQC7DGXVZdwAGpH0w+B0FiFXmrH9mCoKO8a9NiCfZUolp8rJi2hIYsU7Xs0g1Mp8V5gA3UyiA8CjLbnIuwqSrv2Whm37HVuQkMwKNTmgQWGKtXoocLEBl1LMZnCjANQASaAwZLhnuqO8/araqvciQo/V4BA5VSxCvOhiVgMj2hnNfrELEQd/t0Hn+Wd8ws88y8fJxjBuGUYLhnyZkpUTahVR8xUhkSBLTmOdMapWpt3OvNsHTU43ezwYGuDtw6XWax0GecBbx4s8b6la5yv7LAWHRT9StPLY1pBv1AW2q8xNT0iyUIXnvMfXP15Prn6Ah+tXaFdgMRE+ojRlFVKSWUcGk1NpQTFh39HBl3ga7fvobqb8+W37+OT898mLoJTMFZlHquMuPBhlFCVobGBLnJtJATspE16WYzGMMgifnf74e9Nmf/R8kOx+JN3ARQ+BBAQ5PsR+cocXzXgr8v36IJJ+Zg8J6BFwIkPsmT9AnZ8H0RRgcm2RXEUhuGx9Qvs2d+3N4FlIr+2tubUHXNzc2xvbzMcDhmNRvT7fY6Ojrh48SKzs7NOUSOeVgIv2u22U82I+buAB5lECdySSeHFixdZWVnhu9/9Li+//LKbuEt5ncDJubk5HnnkEZrNJm+//TY7Oztuor2yssKZM2eo1Wp897vfJcsyFhYWqFar7O7uMjMz4xSFotIajUYOmO3u7nJ4eHgMhskk8OjoyO2Dr9Lc3d1laWmJO3fu0O122d7eZnt724WjnDhxgjt37lCtVvnsZz97LLVYElqlHeWcNRoNB0PG47HznZf9liAbSbwVL0O/v/nwbNoDcRpQiZLGL60WFaRMniWR2u/3AplkWw8++CDLy8uEYcju7i77+/ucOHHCJYru7e2xtbXF2toaJ06cYDgc8s477zgvyI2NDcBOtnu9noMtMjGfTjuehvW+b6Ov4vOBEOCUSVKyOw3+vt/177fb9AT/bq+f3u7324a/fv+68I/zj7N/cLzUWc7/7OwscRw7jzlJCA+CwNkHyHgiVg0yttTrddc/FxcX+TN/5s9w//33u9LTV199lcceewxjjAtjun79OmDzDUQ1J+NPq9Xi9OnTXLt2jaOjIzY3Nzk6OuLJJ5906l0JHpKbE8PhkGeeeeaY6k/UZ7Ozs67cWbxUL126xMbGBhsbG06xKL6lAtek/87OzvLwww+zsLAA2AwHKX/2AaOcDx++y2t8sCg3jHx7CwmKaTabTiksKl4/yGd/f995/QVBwP7+PpcvX2Z9fd2F0ly+fNld6x/60IeYn59Ha83999/vVHGiOAfc+PLoo49y7733sr29TZZlzMzMMDMzQxiGvPrqq9y8eZPXX3/d9QvxwH3nnXd44403GA6Hru/I54e0j69Ej+OYg4MDB4+lnWq1GtevX3fl8dM33wQ6Sl+Wa0DasNvtcnh4yAc+8AFnAyFwUM6lwOc7d+64a3tzc5OdnR3yPKfRaHB4eIgxxn0mSRL4YDCg3W4DuJtFEvpy69YtVyr+3ve+lyiKuHr1KvPz80RRxJe+9CU6nc4xGwj/JqM/HsHkBsjs7CwLCwt85Stf4ad+6qd44IEH+OY3v/k9N0p8RayvIPdL5eVmmJRJHxwcsL6+fuxm479p+f/bA/F/M4vCGeMLPJTEXVF4mUAR9XLSknIla76y0Plu5fbxPLATMwlPMdZaqghYsRN3SV5Oa0VpWQRx2yZpmkLtJgqxrARJVoQkCOwxVj0U9Q3jIjxFZYasrNxz1W3jylgHi4bqpqJ9T8iP/cTLzAVdGGsIcxKjaekBo5ahtJ9Q2oejs8Vktp2hUkPrij2upRdHZGVNfdPQORlS3TCUOjl5T5FUFPWNBKMVwSijeyLm/KXrfGj+bX67e4Gbo3lOxvvcHM2xNWpwszNHKUg5E+/yu/sP8+31M4wOyuycsNu+UNkmKQf85vUnCY80ld0Uo0KMsu3V75TcOVSljCQP0SkkTYMpvL+Wlg55c7xIqW0IShYEVFVGSImhicgTXSSmGlaCjM80tznIEuq6RFgEnASbsVOI1b5d5eM3/xNmX4f3/CfXMJ7yM49godLj7WSeqGcYLNiStCS3F/rIJNy8M08tsx6VJlDMRT0+WnuDhlYMTcqvHjxFeBAS65Tt1CZ9pSWFSjIINGo4xkQhKOUUsaIa0pJmm01Usyo3qCQlL5dswI6GLDZW5TOw0Kt0lLuAlbwcM2oFhAOrzGxe1ZT3cxo3Bugkp3eyyv4Thuz8mH6ioRPReDuktqGJ+jlZnDGc1Ww/HjGezcnqxYUS2ngGC8XMBNK5kJbiC4vnaeagaAAUnodOZaYsiERUiwUYUqmQN75Haab8L2IF1MtDUSwq511q34Rbh5r6MqVMoWx0L/UgopQ7+2ODrzYstmU84Ou2xwTwSr+W8ccEipX4yAUrDE1EPy8x3qxSHRVKwZoNoDAB5ItjGOmiD+DKkU1oOBnv0X14RLk+YqnZJX2PJo/GzJb7fGrJWgrcH1e5P54Y8P+DIEXpHKVswAppYJs5sP6cJtQkDUMYZeSZRilDlmrU2HqhjpsBemBtFlQOr/RP2+PII4Ym5jCrkOQh20mD3VGdrUGDQRIxSEK6/TJpEpCPAurXA5JGhdK+TWYOkiKcKqS4Fm1/Ssuw+OE7vHvhBvvjGlvDBlf/8Bylw+ILcqhdGX9Jp07xFajcpi9jPwwFHrbzKqackVUC+rM2nT4PIC8bTFiUHxlFXqRZC9RPyPij4TIfKG9xfxyQmZxA1V27Plb6XgV9QyuOLqYsP6Np3IQssjd/7OeEIq1BfxUO7wv4yHu/y2ONm/zGQ0+hlaEcZNSiMU/WDvjOzkm203lMJUMFhiDOiv6pSHdLVJKArGIDYPKZFDJlc4mKGzKqZL1WTRJY+wBjb8y4a7RYytqCw7JKXIlwTY9oZzUaekCAIUHbsmCyok1z5oIeZZ1wT7zNk9V3uPcXtsmNZiHs0Ar6hVIwIFYZVTXiXx28C41hMe7wO7ceZOdOC11OuXa0yDAN+atn/ohb4zne7i2hleH2sMXDtVssBkfsZXWuJwtU9Zj/6JufQGn4G4/9IU9UrvM/bn+A5//5I+QhVD68Q7tT4Z+bd/OJ+65aX8NCXRyQOAVw34ToYhBp5zE1lZKhKBfWApfmt1nPW8x/scx/vfZR/l/nPstmVqdvSlTViISAo7zs1K9JoYh0QJHJOFILxpR0glYV9oa14sbL93SbHy0/JIuACl9p6P9IeIDvFwWTskRfUeOXHMrkyU8EFhAnky2ZIMl24jh2ZbhS1lWpVOj3+27SIsmaUmYtE3KZYAo0EL+pIAhoNBo0Gg1WV1e55557GI1GnD592pVzxXHsymwlaXJ5eZk4jjk8PGR/f99NnK9fv47Wmrm5OdI05fDw0MEm8WkT0HHq1Cm2trb44he/6Iz1u90uvV6PnZ0dHnroIVZXVzl37hwHBweudHV1ddUp1FqtFufOnaPX63H58mVu375NvV535a7z8/Nuci+ly6LUWF1dJc9zt64nn3yS0WjkAF0cx3Q6HQdcWq0Wr776Kr1ez4VMbm9v02632d/fp1wu8+KLL1KtVknTlKWlJQ4PD52pvUxk8zxnZ2fHpafKORYosrOz47wBpURRVGGi4JLy7m6365R2PmiSPiXnxYfZfpm89FVRxyilXDl3nucONsjEeGVlhf39fZIkodlsOvBz9epVDg8PeeihhwB4++23SdOUlZUVTp06RbVa5fDw0CUnLy0tcXR0xKuvvsrBwYG7zqRv+NeEnyY9rfibVpf5160P4uQYgyA4lq4qv33g5wO96ZLiabWivF6WadjnQz3/NdNqRH//fWg1vU9326b0Z/ktY0StVmMwGDAcDjl79qy7OQGwu7vL7OysU8v5ary5uTk+8pGP8JGPfIQkSVhbW2N2dpbDw0O+9KUvcXh4yK//+q/z67/+6w5iXbp0icuXLztbgGq1SqPROBagNB6PeeONN9je3j5WQi22AICzCjhz5gyf+tSnmJmZcephKVmX/uED1/F4zObmJrdu3eLRRx8lTVOuXbvmbsaIr6eMeefOnXN9sFKpcO7cOeI45tvf/rY7Z77XnA+cp/uCXE9Sgn3p0iUXWJLn+bGk++kfgbU3btzg93//94/1kTAMmZ2dpVKpYIy1PRB1oajCJXlY0q2r1SpLS0usra2xtLTkbgQ99dRT7qZOr9djc3OTarXK6uqqC8GS/u6ncMvniYBMUSj7gSSSLizeirOzs27ML5VKx2wfpL1kvPGvZVEQyngjXpLf/OY3WV9f59FHH3VjsvgMSjvL9XF4eEiz2WRtbc0p2aVkeX9/3wHOnZ0dZmZm3Pk9ceIEq6urrK6u0mw23WfLrVu3+KVf+iV3Dh577DE+97nPce+99zolog8Jp69VfywRqCefcTMzM3znO9/hL/7Fv8jOzg7vvPPOMWsUuR79myL+mCZju6gbRVUsasY/7vKnHyAaC1x8LykBLmlYqD1yUXPZDhcUME5KPgUsBGMBiIZMFamXagJ30nJhhD+yCZh6ZAqFmAUKMunVhYJIlIYqN9Rv56QVu864a9UjWeEzKOW6OrFAKurn5HHAeGYCRGWiEZ/rsPzEIffPbPKX5r7F2+MV+4SGdl5hJeiSrI5p31Mi7ho6pxVZFfQopLxrt2NBWUx/yXpidc7n1K9phvMBlR3DeEaRdAL6i5ryQUBvTXN1f4Gr+wt8cO0dHqrd4uHyOkMT8XB1nT9U9/PsHzzEf37vz3By8YB7l3Z5vX+CRT2iphV/kFX4R9/+ANFORGVHkdQDGxIRF+m3qeZkyapLwigjx4LXPIC0b0NempU+68k84dDQrBXKkyJltKEHIB5tcyklZb+AzwZV/tdelRdunOYTCy9T3VTkoW1/DCSLCcPZmJJOyAMYLCowht7JnJPVNm+PVgiHhrQKLT1inAdoNBUVoMLclhEbCwOqesx/vfWT/O+XvsIDUUY1GDl/xOeOzqFTCEfGljCPEwsPjfXHzKOibHfKEzAY5gSJVYUarTCluPDltOcxGCvSyDBYzcjDgDzUBEM4Ol9j3FRksaJ+04YPSJn80fkK9dtjB6+zQYjqBcy8FdC4ndJdCTi8NyRpWE9DB/V0MREuQhBQFvKZUu5UhkYZCwKNVRaaQnFlokL1pw2qKL12ysViXUagYg5ExlP7ifyPCZwr0peNUi5gRhnjlIBqMh5P4J6vQvSUhn74igs8SSdg0QWhFKWGk/JlDxAW63dedj48lH0u9jcPoREMaaoRGYqdvEFZJ6Bg8NCAemOINopqkJHmGmMU3a06GHtTwo5hFsL+2eoWH//J/46qjrn7UmFkEhKT0clTdvKQa0dzKAV5rhgOymSDgOAwpLJtE9mziv1IyNKAPFOYsYZUE7c1w1mrgqzdsjdidAaf+x8/yL8qFSX6aVEGXCSdA85Ls5RYj0ALvhXhICc+yhi1AtKyDWtxiqxCkZsFFiYuVLrMhAMWoi6r5UPeDs86sKey4lxp0MpYD1aMUyL6S6Qy64GYKg7vywrlq92YHiuiI824lX+PslFWNRd0iYqxJVDHjZB3sx4vj5u8PDjDtw/PsjOoc2NrnsVnA+q3RxyeK7H73pTltQPmKn1a8YC1SpvZsM9C1OG+eJOeifkrZ77NQVqjn8UkJmA1PmSz3mC70SSKM9IkIB2GkFgAqHNFf624Vg2ofnE3M7F+ojpRBNuBVfRmoFJNMAoJh/amVNQvSrtqhpK2kDVQOZFK6eSVQrlqv6BLYIg9/vxYCfBc0CVWGe28ysXSBjmazGinXnSehSpnMe6gMbzdW2Jnu0l9oUeWadIkYGtvhhfnz/BU4yo74wZr9UPe37patH3GZtriv/3ST7nx54l3v81v33qc9933Nl//8sMsrWdW5X91Dv245mMPfYOdTNsyaDRDEzi4l6HomZiWHjIsKHQ7L9nkbgIik/PzS8/x9xYuUd3J2PgXZ/ndf/8hPlJ7w6kLbXtlHOVlcmxJs+t/RtswFZ2wFh3w9fEFzld3ua+2ycag6caKHy0/XItMAKS8zPfL8kGHTIhE4SK+Rf4kVxQn/oRC1ChSaggcM7WXUk5flSgloDIZFuWHTMhkIiWqDlHy+SoNCUnp9Xr0ej1KpRJLS0v8+I//OFprtre3+dznPsd9993nJrni5dXpdByM1Fqzv79Ps9lkYWGBo6Mj5ubmnIJPki6VUm5SJD6MEoIgHlWnTp1iZ2fHqYWWlpZceMv29jZ5nnPnzh2nhFlaWqJWq/Hggw86pcje3p5rv06nw/7+PjMzMwBuf6vVqlOo1Ot1p9AS5ZtMygQeSKmtqHn29vYolUqsrq66yezy8jKHh4f0+32Wl5fp9Xpu2/V63Sk9+/0+zWaTW7duMTc3x+7uLvfdd5+b9PmhPNKXarWaKwNdXV1lY2ODarWKUsoBUgGSflkvTErnfB8wmKQ3+76Ad1NY9Xo90jSlWq0e8/UaDofcunXLhRbU63UHk69ever62f7+PgsLCywuLrK/v++2XyqVWF9fp91uOzWT77kIHINDfjiMXFsCTH31oey3vEYWfzLvX4/f73r3gd/d1jP9Hn8ckMd8ldXdIKX/eoE2skwDy7upGe92bFKuKWOT9Kn9/X1qtRq3bt1yNwPE/1Qgi4TeCKCK45jf+73fcwrCl156yY2D4/GYn/zJn0Rr7UJPZmdnOTo64vz5886DrVarYYzh8PDQKXH7/T4nT55keXnZnaetrS1nASAlpt/4xjcol8tuXLlz544rWxV1sygPBZxK6rrW2qlh8zzn5MmTtNttjo6OXEJyv9+nVCpx584dvvKVrxAEAU8//TTnzp07ZgfhK8lkDJVryIe5oirf2Njg9u3bbl/Bqs2azabzYfTPtbxXQHitVmN2dpb5+Xmn9J6bm6PZbB5LWF5cXHRlr+fPn+fs2bNOKS6ehG+88Qbf/va3uX37tkuFFp89uTn1wAMPMBqN2N/fdzedpkO+fFW69ClRYgsclnJosHBMvF/f/e53E0XRMXW8fy34Pn4+GPe3L2rnGzdusLGxgVLKqfP9caJSqdBqtdyY3G63mZ2dpVwus7Gxwe7uLidOnGBjY8PdQADcvt1zzz2cPHmSPM9pt9tcvXqVSqXC448/ThRFvPLKK+zu7tJqtXjkkUfY3Nx0qsZpyC99xLdQ8K/33d1drl69ysWLF/nN3/xNTp48yac+9Sl+9Vd/lVu3bn1P3/DbatpCxa9EEMg8rTT/Ny1/+gEihRIwtf6AWdl6ZokJvygMJ6mpdvIqhv2A8z402pbKidpn3Jx4IAaFyjCLBTgqwtwwmrFJucHIkFWsSkmnhqhnAz0sqLCld0nNKpDyqICLiS1djnsWmOjEuxtVBAcIGLU+cPDk2k3+ixO/y1vJvJvECNxJTEhDZ+jITu4H85qwb0vJ0mqOygLK+zYJFGMn52HbvlceS6swXDBEHevtmIzs///5pS9wNt7lzdEJNsYtvnV0nq+8epGffPgNZqIB0cOHjParrFaP0MoQbMZ8+o1/m3//3FdZH85SuxJPVJ+JTTvNY6uww8BydIgeaqI4ZZhbb7e8ZBNoUbBc6fBWf4VSO2Ox0uXHytsOnKwEXesnFcEHH3qLa4nmheEK3+6c42BcJR2EVkFTeLipzJCXIKomjGdicqPJY8PwTILqBTCT0Ir6HGYVglGOKb4c1UI7eKRk3Lu6w52XT7vzE+mU3Cjm9QhNTDcrEwxtuMrzm6esTaDBljCLAjGOHJwIB7Y/5KUCpGSGYGTPWZBZ0Ijy0rtTW66tR4q5K9qVm49a9vlS2/bxwZzm6DyMFzPi2SHpnSpRL2Iwb+m5CgymnnF0QdE9FZDV8omHocIlIruSXkl/FWVgVkg6DRAWADHRztfLRAai3D5msKXKunhtppwSEayqTnnr06nt805VWFyrUhIsi68c/p4yYiiSk43dJ5d0LWQPF9ohj1uloHc9iioyN6DUsXVL0IrvlejUh1NefRZ2QUknjD1Pvr2sjkoVSkNubOhPb1Aiu1XFLI8gtzcsahs5UU/RuJWSlSL+zpMfYJBF9NISt7othmnIYBzR65fIOhF6EKCH6lgIT3VLUYqhdGBoJsalyEPOaNaqghvXId2skFaL6zWziuv2RYgPbWCTDYWy9gI6EQsHSKvKqexMYMcTExobvhTmFg5rA9pQqiR84PQ7vLSzxmxlwFanzqBfIh2GqEGA7lsPz6Vyl5JOSPKQo7TsbCaMApUVEv/Qth1YD7pqEZwRkzmFGUA/L02kpJ4CT48V9ZvQrk6CRuzj9py+lYSsBV1upCHraZmvHN3Pc7tnuLPfJOnGBIc2gEn6Zx5BfKSobY5RqWH/IcNnnvoGM2GfAMNModpr6qGDdDWsUrJeJB5rZf1ge0kJ0wtJUg1jjR5p9Fg5oB32FJXNgHBgCEaFh2Iiyjd7frMIkhokdcVoFrpNQ9ZKiepjllpdfnr+tlPFBhgHC4cmIlA5ZZ3Qz0sOBPZMTFWN6FOirMfEKqNnYhuoQ25DTBQM89glI1Mc3/trl3lxcJZXtlf5yP1v8Vhjnf20xh9sXGL3sM5X1u/ljeYyWa6JgoybozkulDZpZ1XeX7mKiQxLfxSQ/6U9dgZ1bu+0+IPVh0kWUlb+w2u88Np5VAY//dSLPFy+xbDwwgwwVhGocqdALKuEfh6hlU1Ez1A2dCWPaAMPxFuM/1ybyq81qW9k/H9/66dY/oVDVsLD4pgnwT0BOYkJOcqjotS5CHQxAVU1Yq3cJjEBi2GH3Ey+W/xo+eFa/LIkmVgJBPQnQDJ5rdVqTjkIk/IjUVKAnUQILPTL8gTSwcT7cHZ2lvF4zGAwcGqPcrnsJokCS8TrSLytxEhdIKUAFlGZSLiGTLaDwCYYnzhxgl/+5V9me3ubwWDAPffc48CTD1MEWImfWrvddpN0UUOK59/q6irj8Zhut+vUh6VSiTRN6fV6TgG1s7PD9vY2J06c4OzZs8RxzO3bt9ne3qbVanH9+nUWFxcdQL148SK9Xo+lpSWuXr3KSy+9xOzsrGtjCWI5ffo0h4eHbjIl5bidTodSqeQUUmfOnHFqGUkNFu/IVqvFd7/7XZfqPB6PXclzq9Xi0qVLrjTbGEOj0XBqoIODA27dusXh4aErPz46OuLEiROulG44HLKwsODgCeDKCEVNcnBwwOrqKkopWq2WUzAuLy+zs7PjytimFykJlX7ow1xRtvjhKcPh0MHV8+fPs7S0xN7eHuvr6+zs7Lh+vby8zMmTJ11p+ubmJq1Wy02e2+02/X6f3d1d5912eHhIu912/VDOla8snC71818j16H87yvmfDWXDxZlvT6E89vDn9xPA0N/+X4QzwcfvsXA3Rb/OHwo5f/vAytZ57SyUhb/taIuHg6Hrg2kbQSY3b59G601q6urLuAEcGWclUqFJElot9v89E//9LGy3Ontyw0Rea94rp0+fdqlAEuC9srKCvfeey9JknDlyhXG4zEzMzPO6kDGjaWlJQcjRVWX5znlcplSqeSuf/98yY0VaXu5ORGGIRsbG64tTp8+7Y611+u5IJ92u+0CMwRkS9sKOBOAJaXYPryRviEq9MPDQzqdjhufy+Wyg5miHpdr1RjDrVu3uHTpEh/5yEccjJXxWeD+cDhke3ubV199lRs3bjAajXjiiSecX+ijjz7KF7/4RV566SV3Y0l8BGUdpVKJfr/vPmP8sC2lFDMzM65sV/p1qVRy+yJKSvF3lLGvWq2yvLxMvV53XoRyjrS24T0vv/wy77zzjjtv8nu67NdvU7kOBRbKewTK+jd65P9+v08URQ4eLi0tsbm5SaVS4fz589y4cYPt7W03Pgt4azabbG1tkaYp9913H1/96ldZWVnh/vvvp9frMRgM+PKXv8yjjz7KPffc48J6vvjFL7p+5APPaRWxD5mlr4xGIz7/+c/zt/7W3+LixYv81m/9Fn/9r/91PvGJT/Abv/EbDmpKm/jjhoBsufbls0xuCk1XQvxxlh8KgJiWIerbCb7K1KScUU2UP6oQnMBEHYixqiywKjKUDRwxGsZ1C/rCoQ1nUZkpkjGLkJTMkJYU4yZEXetXlsXF3Z/MqmzSiiIY2pLhtGb3QRKhVWahYzDGwcO8CHsJxnbypxMLO7Oi5DoPFYtxl/3cXpynwiOujpcdzNnL6lxPR4SRJL/Y/a2va+enFwwNwVAV/o4WLgQjC1PLe8app9KaPf5Ry0q/AmV4Z7zEtw7PszFocvNglsq1mJvnZrnQ3OETZ1/lfzUP8/xXL5GujQhzGP/mMv/Vn/8oP3byCuU967eHmoAZo2yZsgqtcsgEhnKc0E6qtj3KOWpslW2PNtb52t4Fwr49ttlg4rm2nrbISoaoD898535uX2zxidVX+NvLX+A32k/x/OBemnpoYUCx5CEEYU5WNlTliXFxsaWaqh5zYzjn9rOqMv6TlT9AU2IrG/H2a2vUi3JjE0BVj8lRzGhFpAI2R02CEczFfY6OKjQqiqhvrHoty60CEdtPdFKUwuoCQGc+3LJBCsoAgWa8XLOhCyMotaG6k1G/2gUN3XN1Du/VoBRH5zTJmRE6MOQHMeU7IfVv16ju2FL2wWIApQwV5BijyeMCqhdlj4hCUBSHYIGYeBbmuLRY6WsSgGATE4ovTAVQVJmy5cvKYELDccBnJorG0MIlndhrOA91EaxBUZ5tnNpPyojzUBX+kkI1jy+upNlTrCkm++6eL24wqNwc80KdrEhkjPL/BDRJP5n8M2kXV3pdhMIE5LSzKkMTMR90yYyypaeDgO6oats4Vcxc14yOilTFOasY02MYNTW1jZxn/5t3u7ZRBdusYn9QFMnGOPuErGzVlSpQjOYU/RMGvTbA5JpSOcEYyHNNMg4Jwoyfve8VZoIBr3ZO8NLGGqUwY7Y6IDeKuXIfraxStxIkNIIhmdE0giEzgU2qzSmUXkaxFHaK0tDC6LgAOEMTcV9tE4DD+aoL5OhnMe2kysG4QiMcsjmaYa3UJpJBlIlK1ba9ohTY8IqesaBLF2XMw7xk065VamHtSBP2reJXSvzjI0O5nTP7ug1tOjoTk9Yn49Rf/t2/YRV9I3tDSGXFNRDbD1ETGLKKVdzmsSlCggKSRkBW0TTvaXN/5Tb9vGQTg1VKoHJ0kRKcYb35yjqhk5cpaevfWNUj3jV3k9lH+7x8Z43a5+uEA3uN5eHEyiArK0YtRVaGpJmTN1Pi+piZ+oClWpdmNGQu7jMfd5kJBswUysGynvhEyrmJVEoZQ4+YAONKlaOivHfymAFG7pzV1NglEUfYJGSBh7JuG2qj2U9rnJ094M/OvcIfde5lNurzyVPf4Z+mT3HUK7PRbmKM4uRcm2u9eb7EA/zl2W8RqRxK9qbO/fObPPPc/aBhN6lTmRvwc0sv8oG5K87HsaX7DE1ITSVoR541sUomKlWV0yqSkhMTWJhIiWEe8U4+x//w6K/xV578D1h+Lmf2zYz/69d/jt/4iX9ILy9RU2PGJrCQtThG8Y8c5pFVFxtNBpwq75MbzbXRIlmu7zZU/Wj5IVjkC3kcxw6kCYzzVRPTEweZePqTNJnUSamzqN1kciETVJnAiq9gvV53k2QJ7JhWi8k++bBPnve94UQBCDioIxO32dlZN1kSZZkct5RKy++FhQUHNS9cuECj0aDb7Tq1x9LSkgtUmZubYzweu+TnJEmYmZlxajwpWR4Ohy5JeXZ2lpmZGR577DG+853vcPPmTZaXl13pYRRFvPXWWzz88MOUSiVu374N4NQwAlT8ibhA3kqlwnA4pNvtOn+yUqnE2tqaO1eSJHvixAlmZ2cdkO33+9y6dcslWV+4cIFOp8PCwoLzjxNAIO0u5dICN0QRWK/XuXPnjpv4io/YPffc44CrwJPDw0OnmJHywP39fafYFBUqTCapou4aDoduu6LUynMbKtBqtZzX4MrKCrVajc3NTafabLfb7O3tcXh4yMmTJymXy6695ufnXTDC8vIya2trbtKb5zkHBwekacpgMODatWtuH/wJta92lPfd7W+YTMalzFLe50/OBbb55c7Tz4mCyp/ky+/vpyTy93F6Uu6/Xv6fhkt3gwjTIMW/IeGvd3qbPrzyy9YFAPvl7GmaOoXwxsYG9913n2sDrTWtVosbN24438qDgwMqlQr/4l/8C+6//34XqCKKa2NseamkGoutg/hntlotF4xz+vRpWq2Wg0mA6+NZljk18/z8PDMzM+R5zvLyMrVazY1jAqt8xazA5yRJ3HUDHFOUSYCUwNRut3vMw7RWq1Gv18myjOvXr7trw79Z5I+Zft+TfusrwvzXNBoNVlZWqNfrDoCK6g8mKuT9/X3iOOYP//AP2d3dZX5+nt3dXcbjMUdHR2xsbLC9vU2SJMdCWOQ49/f3GQ6HnDp1ijAMuXz5svPxE5gl5072WcYEeW44HLKzs8ODDz7IwsKCu6njj/cyTggcrFQqxxKIx+Ox88e9efMmg8GAw8NDDg8P6Xa7zgtTxkH/BoDs2/Q14S++GljOif86/zPYh93lcpnt7W329vYIw5Dl5WVWV1dZX1934SutVovDw0MX+DIYDJzqs9PpcPr0adbX10nTlDfeeMMpZOW6kkCtac9Gfywpl8ucPXuW+fl5er0eN2/edJYely9f5pvf/CZPP/00zz33HJ/97Gf59Kc/zauvvsrXv/71u44b0z6sWZa5G0dyc/BPqj6EHxKAaAIKyFco94rgkFzKEI01pDeBLRHOI1vemdSsCi4cGAvpUixwiSfhFTI516mFiZIoCQXQUFA6NIRDQ9Q3hH1ThFtYdU5WwiUy67SY9BUwIxoYp1gMhjlpLUAnYuhfqF+EmWTW5H826rOX1bidzDI2Aa90T6ESe3FdHq6wErZZnOkSbpdQGxawDBZD6rfGjGZDwmFObUsRHyVE/Qg9zgmSkMGCVRaFI0NSCyi1c0oHRdltEHAq2uP3Dx/hnc48oc7p367T6MHNr57m7XuXoBNRuRVQGYDZLaMyqN8e0Y9S6oFVUfmek3kkqjtFVEloBAP0WBFoQy+LLYCJc4KODX1pBX2u7i9QXY3528tfJzM5KRn/xfa7+PytS1TvaErtDFPO+NTat9lKZjgZlniruwyZBRc2iVQRDG0qahyljMt20qhyKG2FDJdTVJjTz2Nu91sEIwsZ+ybg/3b7z/FPz36RxADGwledQFq2pXyjLCQoZoUbAxsEU9Vj8mFIUofa1lTH1bYfqtyeZ0kFdyEqCo7Oa0oHmrBv6K7Ok0dQ3c4tRADGdc3+o00a62OySJHM5YyXc3Q3oPlcmcatjGBoVYWjGU37npDhvCGZtSAmT22JKqEk7BrIFSrVmDCfhJyEpgCAsu9MVIiyhEX5Z6as2swozwuRCZSE4wpHVYBDRVH6nGPn3cWXLO01iuJ4GbIDiTjlogN2Hji8aymz9kJTin30U5i/Z5yZWpeolsknykV3rHcFkHYdgbIproHK6eQVWkGfX3jiW/zTF58i3IqtYi+E4aIh6iiGC4bDC1DZVoQ9w6ilizHP3viwnoG2bbOSIa/kUMoISxmlckKjMiRWhkY8YqHcI9IZq6VDXtg/beFU1KOqx9SKdNt+HpPkIedL27SzKk+1rnGhvs0wjzhX2mFoogl0xwJRSe0t68SBk6wIrMhMxFFepqX7ZEZbZWAhv2roAXNBj9204d4TqYx6MKIajDlRbtNNS2hlGOYRlSCZKLNFEVq0bUWPi/LbjE5eJss1S0GHBAsqMzRn4x0++t6X+YMXHubElyefA8M5ReeMLpSTk/NotFXRRm2NShVpLSedTwjinGxg1dEkenLdBMr24cKn0mjFuKZ4eOmO9W3VI2p65JJ8waYCS7lvUJTAJiagEXRJTMjp0h5P1d/hPa0F/iEfxBioVUY0yyNmS30Wy13qwYh6OKKqx1S19dqLVYomdzA1MSGZUcW5Mi59uJfHzAVdV26cGU1Zj2gUMFCSjSOV0c9LBCpxANH6/tn15IX8Ny7UnwKUh7mfjB3y+nCN24MWFxtb9PKYQR6jU8O1/gIfP/Uq/+r6I/T6JZZmO3xg4SoLUYe16IB2XmE5GNsAoQDGeciHn3qNXhZT1WOG3RIBOSvhIVrZ45awm728xIoesZPFBfjk2H7/e2/+FW5dWcKEOf/Fj/1LHizdoZ1XWAuO+J8On8SE0F/S1LYy5r8Z8sWnHuInG6/SzquuLQWKi4IxISDPtVWX6lGhoqxxIjrgWnWea3cZIn60/OlfsixzHoIyIRCliq+IkrJh+ZHSLklAFhWYqFSk9FTUHgL/ZIInvlPiMyXlxkEQ0O/3napEIKVM7mUdUtble9/JpFzKnWXSvLy87Ca+4tMnkMWfSErZZxiGnD17lu985ztOsbezs+MUd1IaKKBHAMB9993H/v4+7XbbqZckcXh+fp719XWnUtzZ2WF+fp47d+6QJAn1ep1bt25xcHDgPPgODw954oknnJpRjlWOTWvN/Py8gx6SCCrl11JyKGnLYRhy+vRprl+/7kosxbtLVCwrK9ZmSCasKysr7O7usrq6ypUrV1wgi6hBZHI3MzNDuVx23nthGFKr1djf33dBKNIWlUrFAYfhcOjKm6W/ic+VTM53d3ePHZP0U/G5TNPUQRdfmbK0tMTCwgKHh4dsbGyQZRmdTsepLhcWFlhdXaVcLrt0XUm7PnHiBPV63cGBbrfrlFlzc3O89dZbdDodV8os0GC69FiuHx8K+GWh00EEAu3944RJIvM0aJO+7wN3Uav5wGn6vbL4YMMHGNP7LIuvYPRf//3WP72v02rIH6Qi8rflv06gnsCdK1eu8PTTT/PUU08dU2PWajUXOhHHMaurqy4so16vOyWZ9E3AKaD945cxTkCijHfXr193pbOyTb/8UrxVa7Uaq6urGGN49dVXHfCW8yw//o0Qv439myiyyBhQKpWcItrvA3IOZYyTmzrSP6bPuR80JNudVo4qpbh+/Trvete7+MQnPuGSjWV8OTo64rXXXmN7e9sFo8j7nn/+eXeNy80HuZEjMB4mCle5dmTc7nQ67O7uHmsneb3fV2T98llhjOHKlStsbW3x7ne/m0ceeYQoihygHY1GLvVa/PmOjo44OjpyvppSSiz9wL/efNjln7/pa2u6X/vXhbzWV975z/vbKZfLVKtVDg4OaDabzkpjNBo5ld4999zDnTt3aLVazrtQPqOkn4tv69bWlgO+rVaLLMt4++23j42xc3Nz9Pt9HnnkEU6dOsWbb77Ja6+95q6xj33sY5w9e5bFxUXq9Trf+MY3+J3f+R263S7nzp1jdnaWy5cv88EPfpDnnnvO/f3Nb37TXQd+28i2faAoN6eSJHF92Fch/3GWHw6AqG14hjYWSrkwitS4dFWdFGXM4mOGLbnLY8hyq7bTxcR03LRleGEfVAqBMQ5+BWNDUrUlo3E3JyoAV1K1ZZvjhiLuWpiYhxCOBBAa8lgRjnILCZUqHs/s/ocKnViQmRfl1cEQDs9p8hLMvmlIqnZyt57MuwnLiVLbql0iw3trV3hvacCF1g6vrixjCkh58ID1PEzqiqhjJ8lqKSBpWshZ3c4wAcz+tZtc+8pZyntFwyrorQSoFGpqzD3lbWZXe/z9b36Y6okuc5e63NyaI7peRieKwWrG7Gua8YxVXo7mIgbjiH/6R+9lnqLdjYCXQulYNoTa0NBD9FjRKI04HJetErOcEQwjTADn423GSUhUVbw4OMv/4Ws/xp95+FUeqa1jjCJpWEVSXB/TCvrspg0SU5Q71FOuJwtFSrEhGOcYbWwaaCVHqxyVQdgDVU1R2tAIhrSHFaqDFBPGdPKYRjhy3mc6KVSkiWFc0ywGR64/ynYBqsEYksLrUQFSOmsMpJlTwErWiE1otQq+PNIM1lKyUsDM2xB3i7LkecXBRcV4NUGXxph2TPkgIKkqTJhBlBMfRsRHhsPzVgU1nim+NITZJOwkxwIYXcBBKOBhsTPFThllVYSALUcea/s7LX5nqlAqTmCOfW8B4/JiPWHh9SjpzaZQH8p1LB6KTBSGqAIKBpPEZGXMBFz6Y533mICgyXr4HuAppdEyRkyv0w9OsW1XqBNze/6MtsdlS6cnSk0HGgvYOV1OPTZhkSJuv4xqcj7W/C4f+OBlp0wEGJuQxAR8/eACa+U2+0mNvVGNejhiodRlPuqRo1gIu0QqpZ+XqAdD1qJ9C4FUQo62/mxGsxgecX28yHzYZWwCHqxaBUYvt+ClWoAtAVxZAbhWwkPmgi5LoVUKtLMqe1md+cCuByjCNLrsZ3VXCjvGmhjK+8o6YSdtFgEnOZmx+1bWNsF8mNm/68GQUR7RycrkRqGVYSYc2PLfTGrjZdyftHc9HBUJvSXnRzcuatYlkTnWGX9t8au868dv8Gvn3sNCecBiuctMNOC7Bye49a01C2creeHdacHsuGTL7U0lIyqnZJl2EF2VM8wwKMrv7TUctQPKO4pgnJJFmmZoweHQeECOzCb5klEungNYDI/I0MUYb/d/mEeciA74Pz/8ex6oSt1xCriNC5/Hsrbls+JfGGDo5SWGRNT0qICEY5suXwSntHOr6g6UDZupqbEDw9IXxAMxKiChLsp2peTa9tvAlWX3C/XnuFD2RSqlk5XpJiXykuKr7UvUwhELUZfv7J+iFfX5xXu/ybfb5wh1RlknHKQ1HivfIJOtGxtudjQu8+fXXiDA8MrgFKV3Suw9VWclPKSdVVkMj3h7vMLfWX8vf++ef8lHn/0lSt+u84G//B0+s/B1IpWxl9X4pX/575Evj4gONAuvKL766CUuLW8QYPh/3PkzvPXf38/8J3c4+/59rv/9+ygd5Ty7d46PN19yIT2gHWRNTGDVt2QOaO9ldX6//QgP1uw114oG33sD5kfLD80iX9RFQSWTIb/USxaZIAtsE7Aoajj5gu+rqORvKfcSqCUAQErsBMQIFBFQINDRD2TxAYKoDgXeiNH/tFKv0Wi4bckxSRmcePGtr68DOIggi+yHJG/WajUHum7dusWFCxc4efIkZ8+e5ctf/jLtdptqtepCVqIoYnd3l1OnTrkQACmNDoKAy5cvMzMz44Bku93m/PnzvP766w5YyT7AZOLpT8ZnZmYcVJBJZaPROAZG+/0++/v7/NzP/RwvvvgiV69epd/vs7W1xcWLF2m1Wk5lKJO04XDokl5LpRKj0eiY8sgY4xQufqp0s9nk4ODgmF+fhErIawaDAfV6/ZhyUGCpvKfb7br3ymRavOlgMumU8y8QRMJcgiCgWq3S7XZZXFzkzJkzrhRWvBploirKLvGLlMnz5uYm+/v7HB0dufJ26RfT25+Ggv6140MZH3740M6HZnf77SuSptWBvqJsuizff52/TL/3bn/Lft5tPT6Imi5/9h+/m8rqbtucVk9OwxVRqclx3blzhy9/+ctcunSJubk5ByVEKSjrOX36tAvLEIXueDx2EFDWJwo18dsTeGiMcf0dcOngg8HA9Vv/eGR/j46OaLfbzr/Rf63AEYG+d4PR0+OvnHcB6ALs/QAPeZ/YUUjYh4B42b4svnJPlHM+nJa++vLLL7O+vs5P/uRPsra25sqzRQXX7XbZ29tz17lsR6wkRKHtl8JL2rDclBIoKuOY+NnK58HdoPU0yPPBnUDCL33pS7zwwgtOmSihM3JN+fDWH/un1bb+3347ym9/nPav0R+kAPZ9Tr/f9Sjjh9y4E6/eSqVCs9l0x9rv91ldXaXX6zn1oSi1ZVuiMP/N3/xNgiDgU5/6FEmSsLi46FTfWZbxsz/7s6yurnL9+nU+9KEPkaYp58+f5/Lly9RqNT7zmc+gteb111+nUqnw4osv8tRTT/H5z3+eBx98kL/5N/8mN2/eZHFxkbNnz1KpVLhx4wbve9/7qNfrDhz714Lf5+TxhYUFV9Ug1+ja2hp/kuVPP0A0FrhIuEE4mKgHYfKcC0wpyh7T6qSM2Sir5tEJk7I4beFQMDYM560CrHRovQWzMqAU4dAqDkdNVaSCKgJ/HW7divJeQkKITmw5alZSRUq0Jq1okpoNAclDCyGtMrIoKy6WrKR4vHqds9E+O1mNskq4opbtRpLCz8kknK7sc7lr6C1rTAh6pBjOWQCa1BVh3zBcsKV4SRWMDgj7hluHM2RlQ2M94+hMiMqhv2yhz+91HmEh7PDb648Rb0YMyxlbpsH8F8rE3ZzbH8tRw8AebwRBCAf3aQKjaFwNSZpF2fDQtq0q5uZ52aC1NeRXGdSiMYdjK70Owgw9tiqhgJw8VzRup/zqSx+g+XLM4rs6fLB6hf9y488SaQiHOeVSQlmPWQg7aDRHSZmomrCZzpBWbViDURboxWEGCvpZyUKzYkKnAsNDlXX+2fBJamlOVjJoZUFjZnKGxqqRVG6Vo6apaeohoc7QSpGQ8fGlV/iV+DwzwQA9sl5uQWKTlE0UWlPNfHJuXcmspyDOYwvhxicS9ishKtGkcykqzlEHEY3XY+q3c8p7CVk5YDSvIDAoBaOljNECx/wMXaCJKZR+QQEPAwOZdtt3pcRFH1ZGQQqmnE1AolEuMEXUV6TFY9oUpc4FRDRABipQFlxKubOANn+RMmj/Ei/8DY+pCwuY6P7G8zQtQjxEjSzHYXd7ojg0CACcQESjFNp5Tk62Ydfr/yOQdaI+LIJxHWi825IXI25ZJYxNwNBENPWQ/axOQw9sPzM5NT2moQcMTcTPLnTJ0MwHXQugigHuKK/Qz2MWwyPaWZV6MGQlbLOTNsmMZiVqu+1qlVtQWKjKtMqZD7oc5WWaOi+AVeiAFFiw2Ar61ocvbZKh6GSVIl3WXg+1AiQ1ggFlnTCHnZgk2BscrcB+OAlYkWPX5EVIR0ZOzkwwcAq9sk4YqoiZoM9hViUzmhw7BrjzW4Bkaec8YKL68o7BlZFiIVoOdPIy98Rb/MzJlzlMq1S1BWWH9QrXKyfIy/mkjD8vrhEB0GNNMi6hEkXU0+iRIhzYm03ByFDwOqsKLcPOY1bVfLG66UqAyypxikOryrOgtabG9PTIevEZVUC+hIYe0stjWnpErjQ1PXalxJ28TK1QjwIuRAaYeBEWIAtK7kaHlC4L4NvOGq4E15ZV2307ysvefoZO1dfJywSYQklq11MuyoIFigpQlFCSMnb9J+N93o6W+GDzMv/kzvspBSkb4xl645hmOOQwrbJaPmRnXOehyjrPdi+wmbZYCw/o5RpSe61pZVxJ+B/tnqf1ds4v/87HmXtgl79z3+9wIdrlZ7/zF2l8ocZ/9HN/iXSjSmvbsD2sE2DYzJr8h//i36G2pfi//Mxn+c92/i2CUc4ruycor2SsZ3Ve/u0HqA9zHlm4zf9p+Qt87JG/xcq3DHGQ2dJoPaKTV4rQlNB6RhYl3oCDwu2syiizCc3PHF5gtXRIWjXH71D8aPmhWMQfSiYY/mRHJjQ+rBBFXhzHzhPPnwSLykRr7TzufI8qY4zzwBLIJ8BIlCF+6EWWZS7tV+CQKKtkMixlyDLh9CGSeEsBzvdPlESSOixwDWxJa5ZlzM7Okue5K5mTskTZ/9u3bxMEAfPz8y6ludls0mw2mZ+fZ2dnhzRNabfbPPfcc6yurvLEE09w8uRJFxISBAHNZpPNzU1XMi1AdG9vj1OnTrG9vU2n02F7e9tNLAWQyCTVT3YVBaakVsokbH9/n6WlJe7cucO3vvUtzp8/z8MPP8z29rYLPxBPNlH0iTeaTJJ3dnZcGZyfminqxziOHWyUEmBJdxa4kWXZsQRtKbkGC2T6/b5T9illvRBlez5ok2UaTvlQTvzctre3WV5eZnl52XkYHh4eUq/XWV1ddcABcEExN27ccN6bApz8UKFpb75pAO9DRQE6fjCArzL0y3GnJ9Jw9wASHyb6bSHXnq8olvbw20pe/28ChbL44G8aJPpQxbc58F/jg9Hp9U5bI/jv838DTlHnl9bmeU6/3+eFF14gDMNj14eMCdLOopCVccc/Jj+8RmvNYDBwimr/uGXb7Xb7WNmv7+Uof8t7jTEuTEf6pt8/YHIzw7/5Isfrt9O0x6S8XsZeAZ4ypsm1JgDvbqXzMFE1Tj8vxyx9Z3d3ly984QsuOCTPcyqVCk8//bQLYZLyb/Hgk/FBSsD9/igq8GazSaPRcON5o9HgPe95D41Gg+eff97BUL+vy9/St3z4KmO1tJOox+W9orSfbgc5d/I5JOsRdbcPaqUv+J850wD5bv1ZxkL/vPqAXd4vP7IdgYCVSoXBYMDi4iLdbpdms+mCuHZ2dmg0Gk5lK2PU3WDneDxmcXGREydOsLm5ydraGp/85Cf53Oc+R6PR4GMf+xjPPfcc5XKZN998k/F4zAMPPMDCwgIf+tCHeOSRR3j55Ze5//77OXfuHK+88gqNRoOFhQUeeOABqtUq6+vrPPjgg3znO9/hySef5LXXXjumzpTrbvpGhN8Ofqq8KPcvXbrkbC3+OMuffoCIVXwU3ujWwF/Z2Z4u7LKMmjyvCgVcHtnwk6hrJ6Q6sa8JxgadWgWiSiHu5RhtvaWkZNl6EnqprwKHTGHmHyhU4VEVDu3kM60EFmgM7et0Yn2rknpAWrIXR1aBpFEkEBeBEjqxIQYS0rISHhJ5qo/fuvaEBT5xMWAaw7nSDlkBUQeL1pMrRbljjHp2W/EBjGYNaR2iI0XwtVniiKKsz7aNKUJl/ud//BEGS9Y/ceGNjOF6mbRcobpTTDr7ASYyJE19TO315Oo6X320zNzXS5My80I5ZBWihlKU8tLwNFFX0Yr77A+qoCCKMlThIdnLSyTbFfLIcHZ1j9tzJ1mK7GRYlXLCgQ0YqZbGDnyUVEiaaxr1AS93TjlgjIKsYoh0bsukVe7KiE2m0JFVRA4GMSpLMSVTJHfmBErTMRHlXeVgUB5aQPN/XPkCAYobqeF/vvMuUHCYVTChIYuN9X40xU8Bo1wSp/TfqIDeae4gltKGrJmhxprmaxFzbyVgUrKKZtgK2HmsxHjWMJ5NwShMjgWJzuMLUAYTUagOrRqQwFjol6tJQApWnWh0sX+hsf0rKCa7OV6YirKhJKJeDMwkrcit7zgoVHkBEeV/SXLOvNcFxgFDCSURhaBTAMIEBCp1PESleJ2UTxtlj8++dwKDlFcWLYsJIGfyPoWZKIWkfYpDlMctfJwo4VTqQdt8olS07WkhT6Qy2lmNSKW2JFiNmNd9ByDEI6+mJmAIrIfckMiBtr20TlkntOgTqcwCJJXTLm4wZEZboFGUHJ+K9thJm7R03ykF9wp4KaWnYxPQ0n3upLPMBV0SAlaiNr285Pzdyjop/Bs1gbHj0TCPyLHJs528gr+0s6pL923oAZ284tbdS+sAzIVdqnpEYkLGKiRWKUOTuNTcRjDk6nDpWGCNCbSD7xZsGgIvyEPSgss6YVgAnoYespnOUFYpI50wF3ZpBX1uRnM2CKUIVcIogoEmHCjCLtbiYeB9zoT2xlPSgP4JQ9LKKM0NWGl1ON3YpxmOWCkdshQdsRQW4K4ASwLiAJfaKyEkudYcpRXKauxUhGVtwZwtRQ4KsDigpkdW3UfuALNAygBDTObKjKVEOUcTkBOTgR45GOjvX4Bx8DAzmo6pFG1s+6uDjUXZdc/EDmpmBVyW8wZQY+zOUYampFPeGS0xzgOuHi2wWOmSG8XbvSUWSl2e3znNhdYO74yWeahyy6pdCeib0N3syY39XBjmEVfeWeFEalh91nAnmueeB/f4fO8Bmp+vEXcNd27OcvGRdXpfPcmL109RPZXytc4lFl8y7D4Ga+EBWS0nGBn22zVyo7gn2qF3NqN+256njglJWylyl0dUmFU9Ii9K8wXWijpTF0rbZjDkXHWXm+N5XttbYdCKMJUMlf/JPGd+tPxvf/HL4/yS47tBC5kMCaCSiaMP5AQODodDB5tkkiuQUNRDokaUya2UBsqEQSZmsm+yyARcgMBoNGI0GlGr1ZxXkigEpXxxZmaGubk5vvvd7zrliRj/HxwccOLECafqEM8/SQMNw9D5HEqbyfGJolHAH1hoKSEgSZLw/PPP8/GPf5wPfvCDXL161ZULA6yvr7OysuKUVcYY91sm3mfPnnXBJLLOIAgol8tuYthoNFx7SzmivM4Yw/Xr16nVagDHzolAXzkvvsekTJYHgwG9Xo/NzU0HXWX95XL5exKEpey8Wq06YCGqI621C5WRcxlFkYPJ29vbzM7OEgSB8yrc3d115dF3K+30f8uPr7Ta3Nx0JeOiYvH7szGGOI55/fXXXdmpAGAf1PolpHJN+I/LxFeAjg8mRSnqP343JZL8L4sPF+6m+vMhmr+t6ba6GxT01zWthpTH7wYd77ZMgw4/XGkaWP4g77LptvD3yQdvfsn79PFJPxOPQQmTkPYXeCvgFyaqMdnv6df6j8lNCikNluf9dcp7/RsiPliUtpK+IOueBuI+UJbn/KR73yPSf07GKnmNUjaoQwCrP777ai+/DafPua9ulDHXh8NBELiy8OXlZebn513a9Xg8dgFDSZKwtLTkoL6ovSuVigNxEqr01ltvsbm5yZUrV46lik8Dbf9mhijhZX+nlbg+MJXPO99jV9rcX7+00bQq1Fca+ufJB2TTINAHjPLbB5LyPn+ckPdK/5e2l9JqKWGWNHk5X3LjQ64JOVZ/P8VT8ObNm/T7fT760Y9y+vRpnnjiCd5++22effZZ3vve9wKwuLhItVrloYce4tKlS7z11ltcunSJN998k0qlwszMjBv7j46O3GeFrP8973mP8x31rz2/LaYBdpqmHB0dYYy1jwB7Q0/S1v+4yw8FQLSlqZP/87DwRCyAoAA/33TfwkIL8qIeJE2FGtjXR50iMbmAkUmteEtg567ByLiwlWBkQUZSLe5ujIQsTGBQHqkioEKR1oo7nBU9UVZhgzPyXBH1rIplsHI8bEQZw6hlOBWM2MltqvCcHjJbHdCmZaEYindSW4aW1BWtdxLySDGua+JO5sJaVJoTDGPiXo5OA0ZzUN0yVHcyhrMBpYOUpe9kmFABEZ3zOQvfNSy9MObgYpmsZEufSyPDYD60fo0JpJUcPZ4AljyEC9Vtri4v0CmtkpVtWIESxShgYsOJ5hFzQReVQz0cczQsEWpQyhAO7Xo6eYXKRkAeZdSiMUkjZzE8IlI5YTkhGNnSi2ZsAUQz6JJjSPKAOMx4u71YHL8FyKackeQajPUpVBkW+CjQgZ0UZ6nGBBoT2Ml0LSzM8U1kS5JDe36zWFFWGb/XeZiTrZeIFNzYniPWsD6cxZRylNGFJ5/3QR4GZBXlPN2ASRq3KVRw2hE0jDaM5mD34YikDuNWUatfJAsf8+4LDEa6oigEDZ4HocC+yfUAFuj5Kki3vtBYj0O0BXOZ9zrZrrxWUpll3QLeXLnzZD9MlBdQU8CeglQTFP6fzutQlF0Bx8FdboqEZYV4hVo/yUnKqaxDXu+SrAuPRJfIniv3fnsuPPWiHJvfxt7hTtSIkxsWso9uHcr25UilDsCUVVIAmZx2XiEzmpoeuXLgdlalFfQdVOzkFRbDI3p5ibEJHEAq64SqGlm1WlpyZakoaOohYwI6uS09aQQDByTBwsmkgGtNPXRlrXOBnZD0CnWZ/PRNycHJhMBuXyV08ooXDJLS1EPru4dy5bs2lTYkUqkNodAjlsKOS/wFqxoMyN12I21PZCcrU9Jpcb1MnTvNxMMPRVYo72pq7AAaWDC2n9VJTEhJJ5wMBjT0kKGJuFDZYu2JDW7tzDL71TLhoBjzI8OopeiehmQhpTrfZ2Wmw+n6AReq2yxEHQ/2ZW4/hnk0Aa7F+Zb9EbUeUKhCK0QqJcH6IQIO8CYmdP3EgeSiVLusEpranpeeiZ2Kc5hPimtbuk/PxMzrHkMT0dJdjkzJlVKXVeKuR+dfWHg0xmQMPRVjQw9JsgKIFMeZmND5Icr25XnxRRQ1I9reVEmMDVJZKHfZH9W41W1hjOJKe4G11Tb9cURmFEMTsp7MsRgeMcxL1IKxuw61sjd2GsGADz70Fle+9AA7D2n+5kd/l7eTRb64cz+jWUW5nRM0E07V2ryhT1J6q8LoAwHvql3jD+bfhx7bdOa1s7uYYJ4gzNjLq5wND6GRALHdjkrd2KA9H0WrkC1CcIpSboHCco009IAnq9f4rd2nALjTnSE4Co6ntf9o+aFYfL8opSalwDIZk5IpmYTKBA1wyjNjjPOxk/JkWbf8AMcAocAZf4Ljq7lkIulPyCXIQNQgAgZ8f8RyuUylUnGqRZkISzmvKBBl/8TzTCbFxhju3LnDiRMnnHpG9r9cLmOM9URrtVqEYcji4iK9Xo84jjk4OHDtJ+q9kydPsr29zbPPPkuv13PlfQLMWq0WnU7nmMehQAcJF5FgDz+8JAgCZ/af5zbNulKpcHBwQKPRcCnM4tslvn6i9hkOh7TbbTdZFb84H4aJaurg4IB+v89gMKBWqzmlo4QfiI+ZpHTL+2UfJWBAlEVKKQaDAY1Gw8E7SYIGmJubOxbqc3Bw4MqjRaUlwEHArR+kIJNw6UNS+jgYDFyZ3ng85s6dO+zu7roEX38i6wMfXwkzrfybVvRMt5+vSpsGNtMQxJ84T4MRAa0+dPK3M60c88tk/WUaEvmlm/5xTUOOaaXjNOSa3ue7AdHp18tzfltOH/P0duX68EtkffAm/cMHbz6g8xWG8lofCMk4Iq+bVvnKuOiDRh/myf7Ie6YBsqzXv+ki462vopPtSsm0jL/Sjv4xy3joK+Dupnb14ZXAp+nX+T6r0yowv5/4KlC5xm7cuMGDDz7IpUuX2NraYnd313ntra6uujEqyzKnNO50Oty5c4ft7W3a7TadTsf5D/oA1u9zPvyX53zbDP/68q+BuwF3/+aHD9nkWKWf1Wo19xkhVhCiOPbBsw/GplXE/rm+25gyfQ3KIuuQPiCfQRLAc3Bw4CxAZD8kbEturPlgcnp7R0dHjEYjHnroIRYXF3n99de5fPkyFy5coFwuMz8/z7333uvCrEajESdPnqTX69FsNjHGsL29DeDG1n6/78KOpNRa2sa/gTANav3f/g2ZbrfLzMyM+7wpl8vs7e19j8/rD1r+9APEQsmmi7I2VFHCbFWpk7JGf3KvlA03EWhSpDcHQ/tAkFjQo8dFSIFWxB2bmGyUhWACKSS8Qaf2vWnFJhxLgEtaUYzrCp0WysJIkRWJzwIIkccoAJuxB6YTiPqTgJe0lRFJx8cmI39g8SrX3lhFRTlXx8ushDapdDivaF3J6S3F1gfxvpDGek5aVsSdnOGcIu5ZD8iwB3HXKq3iTk5e0oyaAdEgp7mekschWZwzWI7pryrq61Ddtf59oxnNYFHoiSGtMFFbaUM1GNEdxeixcR5h1s+ugCtxTm4Ut8bzANSCEcNBTDVSFqh2DEldkWFLBY22kzcTGZp6SDuPSQYRQQWCcU4lTMiMoqHtXfN7mzvc6M5xbWeeagpBoe4j0AzHEeR2kqoLsKWjnGp5zKmwT96JUMkYwpy+KZEX5Wbnwy7JjKGyqQj6KVk5pqVTvnVwlr85+xpDNbYeacA7nQVUYlNfRTFFkcBsVKFiLJRzKi/+LvroJABkAvpGiynjBQ9CZji1IAoIDTrKyJPQ/i9qP23sT5GILIpVeZ/KrS+hW1dgnCrQRALSiu3moIzCqHwC16LcQjyp25ddDD2PRNmPYj22PyhMnNvnssn6bTt4x6lwNgQmmPQxf1u2ZBkHEk1o12n9S0XxWYBG44WewCSUwweFMAGhDlopkPcqjk/+i32cgFSrcnQQMbf7WFZWVefKPQvoJEDJgsIykUpdCXFM5pRlDT2kpsa08yrtrOo87QByYwN9bGlqRkzOXm7LgCOsFxtYkJUVasGGHrCX1R38SQoFYT8v0Yr6LKkOLT1w69GFwixWGZ2s4hR+TT0suqQiNs6xjqq2x9vQNlCjrBLGhTqwoYdue+LZKB5/Fo6GNpk4K7t9E1DrzlPRpTKj2U4bBMrQCnoEynogCqzzffuyoiTaB3zzQZf//J7P8fraSf5B+QNUSwmnmgecrLZZiY+YC7ssFmnSudEOFAVFebhWuVN6ApSD48nDQQFJq0WZcRlDr4Cm1UIJ6Iep5OgCBOoiTdq2XSvoW8ha+OsF5LTzKg09cKXGAmzl2P+w8wBboyZ/Yf55fmXrffzi0jOeV+EE6h7lZacibeqhg8M1NaaqE9pFvwyw8E4SiKsekJa+DcfVlfb/nKerV22ZctymnVSph2OeaZ9judHlYnPbttPYtstBUmOUh+zFdZaCzrFzrtUkvOR05YC3FSQN26c0Of/RqT/gP3jfX0ZdrTHT7PHJ+W/zH9/zKCqDMRbU56EN8uqZmGZpSLsRcGnVfnHr5BEm0/amjrLWFTLkgIWHUp4dYIiKa3SYlyYep8UY2c5qvDNe5ONzL/NE4ya/t/2gHf9+tPzQLdPqKl9RIb+ngYdM3GUi7isXRcUmIM333up0OscmeT5IkMnRtKJDJhFRFLnyLT852l9kP+bm5o6Vx/rqFlEoGmNot9sYYxycy/Pcqc/uu+8+l3Ip4R9yPKPR6FhZdRzHHB1Z1bYo6yqVCr1ezwU2dDodoihiZmbGqQS1tj57fpmdDxJEySngSCZeSZJQKpWoVquuLWSSKW00HA6PKVG2traOHaOU5YpPYBzHTlkq/UHUWwcHBxwdHbnX+/6V0/BC2kXSYQUmizqr0+m4xGdRckpibLfb5cyZMw7ciiq11+u5bQrYkBJEOSa/xFKggQ9tlFIcHBzw8ssvO49HgYayf34ZvfQRfzLrQ5xpdd7doJrAKL9ET86Hr8ST9/rw01+ftLE/+ffBhMAwf30+TJDFv958wOdvb1qJdjdIOP2+79cO0/Byert+v/XXN318/mNSlu7DRf8GyDSQ9KGND3a01seANuD6koxr/vH659Zfj5wXP7VXtiXnxofJftm0HJ8/vvqKRH890yoteS9MQmVknPS99Pz1CwzzobO/H8Ph8FgfnIbk/v4KgISJMvPKlSscHR3x+OOP8/DDDxPHsSsbvnXrFq+88grtdpt2u02323UhKtPekXeDndPLNFD028NXc96t//iqYj/Ey7/uYaJoPnnyJBcvXiSOY/b29jh37hwvvPACb7/99rFwo7uNA7Jfd+uf8j7ZH/8mgg/a/PMhnz/ynNyYUUq5Gy9+FYBSVo0qCnYf4skYeXR0RKfTYW1tjXK5zDe+8Q3u3LnDCy+8wKc//Wk3Rr/88sucO3fO7XO322V1dfVYyEyn03HneGlp6XvGB1/56MNbuVb9a80f65vNpvP1DcOQpaUlt44/7vKnHyCCB6zs72BkH8s1BPlEiaRT62mY1CzAinqmUBBiE4+LYI8sVsSHNjVZ5YZyO3frz8NC5VSABZ3Z9emkSFcugKIkPuvUUMrsOsOhTV02gQUZWSQgzf4EY0PpMGM0Y6WOQVLATCy8iGbG7GSK9bRVTLw11/rzmFpGuTZmKTzidHjAdpaQ1gxJPWCwrMjakFahc1K7hhq3YNDTGAWjOYMymmCoCmWlZtxSBCPN3OsjmjcsXA37GXOvQ1pWqNQwbgSEQ0N5HwYnLflKq4ZwUAzKhbovyzUhtt0CD24bBSrIGWUhc2EXlA0dyVJNWoE81wSJodtSNkl0ZN+jVY4p50UpZBkyW6ZtFJTDhI9Wr1NWGijx9Zv38Njqbd46WMOcMjSvFOe7nBEGtqZ9lEeTxGxlSIuyMj3UqDxHxTk/Vt7m0XgTqPPqeJ7yjnIQI48gAkItJYKQjwLyCO60m5hqih4HBajOydXxiaMyd/k7zSz4ijOMURPvQQ0msFDOJhZ/7wdBngQ4GWgBbeVvlSur+pO3iccbQGQmCkHfm0tKkTMmJdAYBxlt6qw6BgZNWEBBKY+GqURmMIEtjyZTVuHoVIj2WsgL0C6hKLZ97DZNgLuuiy5tfwc4taJK8cYG5dSLOi0UibJOWY+ZKAZ9daJ4H/p/u9cX4S4qK6Cifzo8+OiaMrSw6nqyYIMnkCTm1HmnSYqw/Tu1QKaARXNFmaqUpmoPwmVGO2DWDEb0TOSAXNUrVZUSyzKJUxKuhIcAzl9RbAACrB/juFiPhGaAVeKW9ZgsL7Gf1W1ZdaGUs8fUIzOaflYqSrSH5Eq7MmK0XUc/Lx2DXVI2CzgPxVbQd4Eeth2P9yWwZdU5mrKawCwp3fVTh8Eq7Wp65NKgRTWZGc2peI//9MHPF8cY0yy8KEVdGJGTEDr1nqgupdw4wDA2QaE8GzrQNC4Id17cLZDHBWbaIJOaBavBgL20TqRS4iIpOFaZg8+iRNxMW/zr/Uf5xcVvFMcbFl6LuoCRhn+4/eN85bWLNOZ7HIwrvPqHF/jULzz7PcpUUUHGZM6jUxKcxwT0igTjAONKqUVt2c/tpFZAoivDxzhlZFR4Ub45OsHGcIaqHnNvdYsr/WWyTBPpjFPlffbTGnmuSPOA2ajHYaGqF4WfQPtQ1H7FtSE3YcTzciXoMl/vY3SdWpywkzVJ391htFElLkqLdeGXOjQRb752itJFzb+79FJx7eWowG5snIfW2qAYVnPUMb9D23+zAjRObAh6eZWW7rOTNbncW+HFw9MslTu8e+4Gl/UZfrT88C0+yFBKuTRbmEze/Umt+B8KqBIFgIAsKXktl8t0Oh1nej49mfLLZP1SZSkLFEAkE2mZxAjYksmIX9YmIKVcLtPr9RiNRi4EY35+3gExKe2ViaNATzkGUf2IujJJEm7fvu1KYtM0dT5+EhwwHo+Zm5tjY2ODJEmYn59naWnJKTHExw9w7RgEAQcHBywvL7O1teUgo0w075YI6wcJlMtlRqORgxdaT3zbOp2OS0aemZlxik9pIwGItVrNpRzLOkQZdOrUKYyxacbb29vU63VmZ2fd/vg+hj58yPPc9SNRFw6HQ3Z3dwGrMNzc3HQTT1GnyLn0vTC3trZccq6okqaVLN8PVElohSxy7qRP+mWHPvS6G+jxrxd5r7SX37999dw08JsGWD50kHX5wMcHIXf78V8jf4vvn1wn/uun4aZ/THdTm00DDDU1F5h+fPr5aUWRbMs/9un3+QrIu61PILTf7tPgx9+eD5OnIY8P5P3tijJPwI4PFX3l4fS5lXMfx7F7rUCO6dJlOd9361+iAPRf53vY3Q3c3u16kLFbjh9wY8V0G/i/p6G6wK1pEDa938PhkPX1dQ4ODvj2t7/tHhM4N21F4beJf11N9xV53Aeh0zBOblD4x++3uzzfbDZZXFxkb2+POI5517vehdaaV199levXrx+zopAx5H3vex97e3suaOvEiRN0u10uX7581xtf/rnxH/M9Un0VnrzX70/+fvt9JYqiY0E0g8GApaUljo6OaDQa9Pt91tbW6Ha7x9T/sk3/RqC0pT/+yLEopdjY2KBSqVCr1dja2mJnZwetNefOnWN/f5+1tTU3Vp49e5bt7W1arRYvvviis9vwx1f5Pd1WPmz3r0NfAVyr1eh2uwyHQzqdDo899pgrZ/7jLj8UANEoRRZNFEoqsJArGJmizNiCwLSsCUbWiy7sm0JBaMtkw2EBE3OIejnByE4WRg3tvBSBIlnZoFNI4yJJeYTz19Njm3ws8NEoRTjMGTc047otmQ1Hhjywpa86M2R6UsbaWw7JyqAzRRYbdh8JqK0b4iNoNfvkKOZ1jxk9IsFOEINSxoWlHapqxBirBvLLKo22CsasYv9PaopgZP0Wq9s5nfusNCocKCrbNmBFj6H9QIpOYkazitodQ1RSDOc0SVWRR4q0bKGoTiCYHZEexQ7KGGWBXE2PyHKNLib8RhWqTmXVj0obapENMTDaAsd8EGICSJKAYUszfmBgJ+gdC5XKQYqKcqecIdWEXduGlSDhV/bex19oPc9Dcc5gv8LCWXsHO10bod8qQW6ISxn9UURYtQocCnYVxfYL12ZWorRffAAEhudGM7w9XuGvz9zgt3afIuqYokzWOC/ERjhiZFISAySarGQwScDy8iGDl5csuIpCVJZBbv92pcOi5FMFvDLGAubAYNJi1qo8KCgegYG8Gevzp4vXw3F4V5T4GvEplN8FTDQSfEIBACXsBCaAUcCiDx+zQkEox5F6z8tjyWRgd36KyqBSkQeDGmv3OMreBNCJKYJkin6sJr6T8j5nXaA8xWJonApRPAiNxgWkTCBhoVKUwyk8OqXU2ygvDEUVqjelJiXK4EppUdK+XkiLKA8pwKYR+KDJvJTW+aBrwUMuwM9eRAL+YjK2swZNPXRASsJUpEzU+hyOJmElChp6TGJsOuxO2jzmnSclpxLUIgrHWGUsBj3a+YheXmIns36JAveGpkRZjxnmsYVkJix+Ahb1kQvWkLJWV0Yb9EmKJF5JF5bjqKmxC5TZS+vOK7GsE/bTOo1gSCvoEZFRD0ZWLZp5atFiiVTGqWiPXl4iM7pI7dUORg5N5OBkVY3oYaGXwD/bxW17NYPhRLlIVgS+pM67MFBjV24updni7Uix77Gx4E/Ku8UPb4hVQeZGF8nXFGXWIZ28zLXRIu+tXWGYRzSDofUgzMu0gj5vj1f4f//hn+Uf//R/z+8cPspnn30KU035cOsNLpU2joXVWOCYcjguE29ENNdGvG/2HS4/smiPW+UkuYV77azqUpIFpo5N4PwxJUhF4JiE0wh8hKIcv0iCtnA1pV0E5UgJdzursjFucbJywOa4yXzU42Z/FmMUrXjAblLnlYM1jFGE2rbfzcEsJ+N967lJcdNEQT+NORXt09AD7oxnLQzMlSvfz7AJ3hnWL/HvfO3PE9XHLN27N7nmC4/f//g7n0Qlir/857/i/D7LynrKBokhM4q5osxBbiLINaTJGeYxgTZEpIW6V9HOq86rOFIpnbTE4811d82a0NiAqh8tP5SLwJskSdxkWybP05MdKQcFnArMT2BOkoRut+smKKJOkPX6k0JZp0zafRWclGDJdkRR4ZeD+ZNQKUk8PDx0SaoCYj38ugABAABJREFU0Obn510plD9pkfLhJEmcP5cfCiIKOjm+er3uUkOr1SqNRoNbt265ABU/hbrX6xFFkXutqAK73S6dTscljoqKQ8q5RWUlE0DZJ39yJW1ijHFqREk1NsbQ7Xadt+B4POaee+5xpca+AlEUK6KGFAP+KIo4ceIEg8GAU6dOsbW1RaVSYWlpyYG8brfrQKicxyiKXFLx0dERWmsHfFdWVgjDkHPnzvH1r3/dTT4FXItRvgBPgNu3b5OmKTMzM+zt7TkQ4fch/3z6yiIJhIHj4M9/3TTg8vupP+n1wYpMsAWQwqSU1Ycs0jf9vj4NfXxFrl8GKe/xQcm0J5q8TpSw8pgokqaPQZZp+DMN6u62D/6+T6/Hf/5uMBeOw7LpfjwNMKbXNQ1n/XX4sNW/SXE3hZpvdyDwX9Y/rcjz91u2fTfvSjl38iP9Qf73Vdo+wJlu27tBuWkw6IMYge6+UtFfj9yIEKgs15nAzWmAKMDGB33++6fb2u9bskj/7XQ6dLtdt87pa0g+W3z465/zafDmt4XckJCbAbL/ohI/efIkURSxvr7ubljIeZybm+PHf/zHWVtb4+DggOFw6BSSp0+fZmNj43vGC4CzZ88yGAwolUru9VtbW98DrPM8d58X9XrdjZF+e30/OOqDVL/f+lBNa+1KeKUvzc3NuXFTkuW73S5JkjAzM4NSypVfyzmS8+8ro+VzQH6mx7E333yT9fV1XnvtNd7//vdz48YNVlZWyPOcq1ev0uv1uHz5MrOzs3z1q191sFRUk6VSCa21uyEGE3A63YbSPv6YJmFE0lZSXXC3sev7LT8UAFHKE/NQESZ5AQEnj4OFiCYo/NJUkeJYgMCom6Mym/Y7rmlMYM3xnT/hyKAyG54CuFJoUTyo3JYaq6wIU8kNwSC3nocVRVqxCcNKTeCFKAulDFoZC9aUwYawZHZiU79pCAeGcJBRi8eUVUaiUvbzsitHPLl4wEanSVMPicmp6QF5BNXbAw4uNOjck7sQDhoJQwMm0RAYuocRupaQ1hOSXDFcisnLGUEnQGWK8YwiqRuyGDCatKyIuvb/tArmyALJMMpI45xgFDp4g7IKoPEoRIKxpVRUYG8Q5iyWunYCm1iASG7bJB8HKGOoVO0kvbKX0l8q7urGdpDsmRiKElijoBaM6ReTYk0AmWIlPoI4Z36uS1IvY0JNFI0ZDmIeP71ulSiphcC18pjBOGIznaG0b/c5LiW8PDzNKI8IlOaFzZM093IGCxqd5pjIivNKQUpJhWjG6IHtR3kWUIvHDL1SVxMFqKT4YiPg0Ez+ln6XVpy4Cpd67EG+YwBFIFuuLNgTn0FfeRjYMnIj6cKyT7mn5vLVkFkB+8BuV0ChKG21seDPPzZRFYoCUjy+FEUJrzmmQDwGGz3gqFOKlF1PFZibYx6F02o/AXeiMHSljqlx/ofHvBmZqA9F2ThpbuV+K2R9pijfxoW1yLb9/+W94r0o+yTLUVEGmqFoZzXmg66FgMoGW+xl9eJ50HlErmzKtwV4oVPRtYK+V45rHMyDSaBI28TOm7CTlxlmNiRlPug6tVhiQjIPDvUKP0Sr5kvcvtbUmDEZZfHcy0u0gp5TFIrv3tBEDE3EvLLAJ2aS9iupw7LPvbwEGgfiMjSHaZXzpW3XXpmx6c+twN5AEauKSYjORKHqSrkL+i1wcFJCa0GrKB51USo8Lk5PrArfxKJzCdwUNWWgcijALkCGpqUHxbmJCviWFd6BtpRXe6ai68k8t8ZzXCxvWF8/E7lAk2cHp/lnV59kMIh58F23qOmxg8TiP/n80Vlq1wKO8rIFwpsBCx/YdscGMCxKn0UdOM5D0prhr539GvfFW9z74Kbbn5YecDttUdZWibqZtDgb7/DbB0+SofnozKsOaosXo3huymORSokK1eLQjfQcU3wOTeRUlKM85Kn6O2wmM/zja+/DGGtXUQpSZsM+3STGGDhdOaCfx8zFfRZDW75cVhmquBli4aAtDxfVIbntA0GhBk9yTZAZAp0zt3LI/m6Dn3rgDfbyqu0jCqobhoNc8dd+8kusRgeIe+TQiNUEDLPIZkgX1/ggteclIiUx1pPY+lfaIBXxshQ7gk5WIdYp7wwWqAQJZ8u73wPAf7T88CwyyfUnxuJVJVBOKXUsCddXysl7ZNKQZZkDi/K3hHX4ijNRSAmAlEmCAD1fmSgTYJlEyjZ9pQ7g1G+iIuz3+1QqFep1+/3T963K89wp/6RESgDozMwMFy9e5MKFC7z99ttsbGy4tgJbqiw+TOJFJfudJInzRWw0GnS7XarVqlMYClADnFosTVP3GlEM+W0mpeCinATc/maZTSz1QYGUPotv4OnTp92kU/yypLRtPB47wHl4eOj2V0JiyuUyN27ccJN0meQeHBxQLpdZXV1le3vbTQ47nQ4HBwccHByglKLb7bK1tUW9XqdarToYKBNkAaZSqi3+XfJe8aH0J76+Imwazk0DFjhu1C/9BHCAdnryOq0Gkr99gOSD9buBM9lfH1rKuuUYfEjgwwN/2z5YmAY/09uX/iF96get527Aylc8Tj/nt9sPOm55/Q+Cjz4w8IGJv77p9/qQVVRtMs5Mg1kfRvnnVhY/WXm6pFS2LTDFVySKevhuQFj6nVyr0l5+Sa6vyALbL6fLjuF7gatcu/Ia37/Qf800/J4GVv7xTkNuGUv9Y/p+gFLW48PF6f4pY7vftj4488GaLD6w9dVxSikajQbvf//7WVxc5M033+T27dvHxuVHH32UWq3mxrzd3d1j56lUKrG0tESWZaytrdHpdPj617/OuXPn2NjYcP1Erh9pE6UUCwsL7Ozs8Oqrr3L58mX29/dRSnH69GlWVla4du0a4/GYRx99lEqlwn333cf+/j6/93u/x87Ojhu7p8cV/3xPjz/Snv55lzEvyzLOnDnDYDBwbRCGIfV63VUH+H3VV6/6ilIfjMuY6Z9P2bdWq+W8dTc3N7l9+7ZToO/u7rKwsMDNmzd59tln3dgufUfa0L/WZT/8sc/vq9L3RO0v+9zv96nVau4G3/Q4+IOWP/0AsZg86nTihZiBCw3xFXG2PNmqAKPeZFAdzgYkNYq0Y1EsKrLYlh4HI8iqFpRpz4NN5iwCJlRu1Y3BIMOEmqysLSAM7DrzUJFWIa0WZZ+FZ5sqUpZV4eM4blgFoFVQ2ceSRshypcvtAhq2syqxyoh1yma7wUOrVn1S1bZszKwMefsXavy7H/kS/+T1p8l3KxbutCMwClWUvuZxDp3IwZZgpAg7AVnVwp/emYygp0nqinCQU9mz/o7jGUVag85p257DnQrBQNsQFQ1pDdJ6xkp4SJZp8pJ9PEgMWalQdWoIo4xKkLA9bmICG/CgxsqGlCQaoxS10tgCiE5KfiKkFKRonVNTKd8Zz6PbIVEH8lhTCcZoZZjRCYlRqFTZUrtSRiVK6BbqvnKUMhpF/PnF7/DG8AThAHonDM3ykN4wZidtUjq0g08U2oFvNWoD8NjybV6fbVn4lOfkhUdgrFNycvomsApWA1knYvXcEfsp5JFGJSkYuw+mHNuSeB+eZR4YCRXGKEymCu9AcGXBAuBkyb3XiN9gDmhVlOZbQukELwqr1BPgCMd8D48BSuX9YMQbwG4vExWjcupCBwY1k3RlKHwT7bVl0gJQFm1n4tyty4ha0geHUpZsDAZvneJ5aIzzN3QhKbkApgk8lHUabRWO4mXooF9uXHmsOxcCJwtwamSd8sXMf38BDV1wS+ABTSykDjCuBFXUXhPF3kRB1c6r7vSKQm+oLKDYz+pEKmMlbDslVFklroSyn5dIlC1PXQyOnArsdjLrYJSUl4JNYhYPQ1HDSUlvLy/ZkBY9QpvoWCm0eCvmRrtwFfHgAxxozFDUinXbEu0KGYqloGNvhGj72tPRHm/mqxxlVtlW1gllbUNd2lmVflZMdIs2d+czsMBqJ2taVWUe0Qr7jE1Az8QWCikvzKNYxOdPjhns2NrOqjSCITU9cirNqCgbj7C+g4kJ0Srnq71LnIl3WQyPXFkwUACm3IWVJCbk7z3zCZqvR1Q+us0vnHmOB8q3XDhKP48ZvNlCnevRySaKx1hlHOUWyn649QadnymxFrYZ5yFZyfBXz3yDs/Gu82Ysq4R30iVORXtEkrxczjkd7fPK6BTXhwv8hdbzVFXKP95/P7/z2+/lF//SF3ioss7/87d/gXQuZea7Ef0Thvf9hbedurKXl2gUsFRK5jW5hYc6IStAaYYCE5KAUyIK6MtQ3FfZpKwSNpIW7U6FE3NH9EcRe6Mae6Nz7HdqRFHGxnCGQRZxvrZLWY9pZzUWgx5GG4LEkBvlrhkpYbZOC8pdT3GQkSkYpiF/4czLvD6/yj2lLXf+jYbhnOLnL73IvaUtex6KftzQY3ScofKQUFkkraKcPAxYrR66NnCl1cVwIMnfUeHpKSE+W/0m8+UeJZ3yROU6ebnwL/nR8kO1+BNLSdSVL/gyoZS7/TKx6nQ6DlJJybEEdviTHfEHjKLIAcnhcMhgMHDPSaiHgD+ZAEdR5BRkMrHyA1WMmSRR3g0u5XnuypNFOSheTwKMBGaKQvHKlSvkeU6n02FnZ4fFxUWef/55qtUqCwsLLiQkz3N2d3ddOIiAvH6/f8yPTNKLRQEiQLNWq7nnwzDkjTfewBjj2qZSqTj/MAEk4/HYlehNh7qIqbyUEovKsdVqOZgq+x9FkfP+S5KE2dlZXn75ZU6cOEEcx4xGI7dfWlvvxIWFBba2tmg0Gs7X0IcPp0+f5tq1a98TdNJutwE7CRcoKcrEarVKv9+nWq06mCxJ0AJxO50O9XrdpQoDx0COnHPxXZTtijJQjtGHUtPqtGlVn0AEH/hMK838ybwf1DNdpup7eslvX03kb9Nf/LJk+F613TR08I9BVKB3O4a7ATwfWPoqPH/C70/8fQB2N1XVD9qGr2a+G4j8QTByer+mgZs/lt0NlE2DXxlLBoOBa7dpmCl+eQIbBc7KenwgLNuSMVDgiUBIP0DFPx8yDvnn0+9jdwOAsj2/n0wfryj0ZPsCinwI6++3lMP65+hufdPvu7IOOaYoipx62Q+z6Xa7TkmulHI2GGmacvHiRZ588kk2Nzd5+eWXnVer7JuvKD5//jyPPfYYR0dHPPLIIxhjHDwrlUpcvHiRK1euuHFMjkUUukdHR7z00kucOHGCd955hwsXLrC4uEgYhmxvb7vPuYcffpiFhQWuXLnixkKBWO9973splUo888wzLC0t8elPf5o4jrlz5w47Ozs8/vjjzu7i/PnzvOtd7+ILX/jCXVW30ra+Mna63aevM1GNw6SsV8LFZAwV5aXcmBH7Bx8cyg2q6b40fV3KdtfW1nj11VdZWVnh6tWrDAYD93kiIPO1117j4ODAjYkybkvYllwXcRwzGAwc6JT2kL7l93n5CcOQRqNxTD0+MzNzTMX4b1r+9ANET7WlC2VbHol6qACGClQmJaGFSrBQFIqnYdydTPLTsvW30yk27XbGJjHrBMKOIe4awkHukp6zWIJTCkVhLSCtaJKKsknOGlTNbisrKwcG9digx9bvzaUSK8jKE2WUlEvmkeL+xibNwt8rCKxx/43uHPct7/DvrD5jJ8qFHOzU8gG3tlf49d/+CeauWOVYVtJEPVs6PZzX5BGuJNRobDmnotgnCz9ESJbUQRclp+PmZN6TVg3BSFHaDchKMJ61QRwqV+ih5m+/+ucxeyWSKuSl4hzkxgHdIMhZjDvcGs66derEejGWb0cMluFC84AvH1wCbUNpkrzYMWA/rZPXM5SxsLakU47SMmUFCZkDVOXKmJ2jOvM7ufWrM4pyKeHh0h2+cPAgeQhpzRCqnEppzGFWJeobTBxSLdkU3MXQmnrnRhcel6DSnKxkGBm4VNlgaDKWA0haGbVrIaiAW90WwdgQjDILPPIctEaPEuZfT0gr2obtNKwKVpasNOnmKswxmYbUgkK/jNbWg2PLmQUGFhBwErBiLOQTVaGUMQvs80GiwEl5r4BLUehpA6m2ZdilQo6rDSg1AY96sg4TgkoE6k224SCnr0RMNCpRTskrpFCgvfFCU3TiwUR3mMV17H1GK+8DWxkLwEXZKDYDAvMFSOnUA7aeYlHaw6kSKQBnblzJtytrxl7HDjaayXUt4RMND7SNCSiToYsyZU3uQFxVj9jP6pRV4v4W1d687lPVCUMTEJMTqxEoGxRxqgCMSQGWLpQ2qakxPROzU5SnVvWIyAROWSb+i5Iu286qLKojhnlEQsAwj10idE2P0OQOJmbKAtAM5cCcTQoecjOdc6XCLd0/1g6drEIjGtLLS6yEh0Wpc8h8cOQCY8o6YZSHbvz0z4tRhXqZIrVYJ4Wi0b6orBP6WYkek+TrxIOL68k8ozxiYzzD7rjOXNRDK0NJp3y48bpTpTX0kLdHK3xh/wH+1onf5x/tfohv/+rj7H9gxF94+EU+0XoRwCkvAVfS+vrwJCdO7bF1uER3r8GpC7aU1pYHp+5S++CZd2gEA9cvLISKKZuEe+JtPr74CkMTMsqs2vt/2niS1coR/+nq59nMavzNl/8K+TOz/Nlf+CN+vvVt3v7GWWZvwe88/Si//dpjxFcrfPoz3+S/2/0x/vU33sVMG/7BMx/mv/qJf055X5F1Iw4fH9N8JebaaInHK9ddafrQRE6JKXAsUDllkmOKw6QIX4kK9aeAWylLf75zjlo4olEbUgpSgiDn+sEsSRJyYvaQjXaT1/ZX+MtnnmeURwzz2PlhigJxAu8SZoKe+x4g4S6J0WS5xmjFOA14X+1tnqy+Q1D0rUilmNDeTCzrhAvxNjuZTTzV2BtUUZQR9TJe+Mol/uXPvoMZBnROBjzcuO0UmbK9HFuSnhtNglWzjothoapHbHfrnKy1eV/jCrnRk7HwR8sP1TKtCJEyYpnE+ubuoigUHyZ5nfjfCZARVZ0ALgFIEijiv08mlGAn5KIskHJev+RNIKNMtgSWSGBEnudOiVcqlZwPX5ZlzM3NuXTPOI4xxrhy6fn5eadm0Vqzt7fH7du3uXPnDvfffz/b29v0+32ngAiCwHkB+qoRKd0WKCGTRQkLqFQqrt1lUi2ejNVq1Sk6pEzupZde4uLFizz33HOu/EvUlHIc4/HYTSAHg4ELqjk6OnKlv9evX2dhYYHhcEi9XnfHIqERAlglZEYez7KMjY0NVxr9wAMPuERNAZrtdpvV1dVjnlzGGBYWFmi3207x2Ov1HOyUfRePR4EMElqzuLhIFEUcHBxw8uRJ1+6+mitNU7a2tjh79iwPPPAASZK4skSllIOtvvJmGoxPw5Hp68CfyAqMkPdPG/7La3y46AMauQ5knXKufTDml0n7sM4HlLJMq8i+H/zzH/cBn/+c/P/9Fh96TL/ubpDOb9fp138/UPiDtjG9jz5c9cGcD1h9qCYgT4Cff+4ETtxNQSfb9hOH5bXTINq/0eHDVh/GCXyUa8U/Zv/3dAKyD8/9Mnppf79/+GOhjMm+T6xAJR+Uy/qmgbAPF33wK20o5c2ilJbj8cdoGc+lDev1Ok888QRLS0vcuHGDj33sY2RZ5pTNX/va147BRn8blUqFq1evMjs769TW0m5hGFKpVFhbW+Po6Mip/mZnZ2k2mzQaDafWOzw8pF6vc/HiRfI8p9Vq8dM//dP8L//L/8IjjzzCBz7wATqdDk8//TRXrlxhZmaGbrfLwsICZ8+edeXJq6urXL58mU6nw+OPP+7Ulq+99hoPPPAA169fZ2lp6RhQnlb2SXvKa+6mavaBta+QlM8rUa9vbGy4MVXKnGdmZpzCfnt7+9i456sAfbW/f43I52etVuPy5cv0+32Ojo5c2XQQ2CCsxcVFVlZWUMoq02W8E0V5kiS8973vZWNjg/F4zLVr19yYPn0TYPqmBUxuHp44ccKBUv/5P87ypx8gKjvpDweZ9TmsaMIiTTkrC+jJ0YXyLQ9hVNGkZctU8tAGhIi/ms4grSmCgVUz5rH9iXpQ3suJ+rlLb9WFD1cwsjBsNKNdIErcLTpUobQjF5hAEbSC89BDKefrJQpHPYJwmJNHFk5msWItPiBD8fZ42Rn6f3L1BZfCmhSTpqpKCXTO8rcgqRhK7dwpsoxWlA4NlX0mJazSXwzOcy4PFHlo2yQP7WMSAgOarFSogIKiBFspy4EKkGQ0hGPF+OVZosCQNAwmgKPzoHJN6cCGzAAsR4e80D49gUCJIpkRyGsoBwnjPLTlaR5UA+hmJdRYE3UttKnqMW1TJVCKTp5BmDPKQ6qlMe1OxZ6DWBMGGUpZsLIzqlvlZCkn0Dm12Hp5BcMco2C2PHD+dAA7g7pTvhmtyUuGe6I6p2ducjM1rAYx//Z7v8HvfvtDxEew3T1BVRn0MCuUcAYTBDBOqFyxdzZUmjE6t8Deg2ULpDp2smwK30Hrg4gDgXK+3GNaTUqFlSgYcYDdXunGKXLte8ykD8DxkmUZfyQoRbwLcyYKRwk/8WCOKf5XqYJATbZhcCXUYCGAC4ARoOmgoqgEbR9UxrjrSEqYJYhIglEkwMQqdqfUhoUKUJSC4o0o27D7M7k+pITclT3L+6ZKoP0S5WOvKdpP3aVGUeUWrpyK9tjP6nTyCg09IFIZR1mZcpAQF2W1raBPhnYlwC3dJ0PRz0tU9YiV4IgjU6JnImrY94EFh/28xFzQJzeKtinTKtSF1o+vzHbWYCU8dN50AbkrP5XU3SERZWUVekHRuJL6LGBLgjKiQqXWzqv2/V4ydKwyxtiy6cSE9PKSAy6yvShIXSiHX7KbmJBxAeIyoxnl0fH29JUQygYrCXTNjGZc+BMGntKzrBOWdIebheqwEQw5SGv0s5jcKBrhkKO0wsnyAQtRxx0DWBj4zaN7+NZ37yU4YXijvcz+IznRzRL1x0YuGCVHExSgMlIpt9NZhibkXQvrrP3Ud9EqZyU4ZIx4QYZ00xImMqyWD3ltcJL31y7zW7tP88ruCQ7emuPnP/JHPGfO89tfe5pf+9/9fWuZ0Fbc/PxZ3nxgyH+2+vv8jVf+LXrXZqhq+J+feZqf+3MvkFYM3TOK9zfe5vfr95PmFfom5HOvPIqeH8HZAeGbs6yEbbqXxjDWnD+9zebbJ5kJJqA3KiBnjqam7PmVdunk5UKZaQovxcylXosno5RUzwR9PjTzJrfG8yxU+6RGUwozhknIh85coaRT7hzMsFDtMcojrvSXWAg7Ram/cmOWxiZBAxwW4M8EtrR/bAJKKkMrQ54ZRknIYtDjajLvQmLAeiDWtjL+0TMf4sa75umkJT619E2aBZA/PXfAWK2w+J2cf3znz9ECoj+7w3J46FLGpX/oojxfoPm4OO6+sR9aYZDz+sEyO6M6H1l487iC/EfLD80iysNareYmbqJ0gEnir6geRHUgYR0yMZaJq/j6+RNrAVdKKQeSfCWEMba8TCYa08b7ftoxTMoPZV99CCqT6Xq97lQ44qsnE5ZKpeJgn3jsHRwcuDbp9Xrcd999XLlyhX/2z/4Z9Xrdqdn8ciyZpMu+yXHL3zIJazabzhNRAG2j0XDbmy55FDXd7u4unU7nGHxcX18/puiUCVy1WqXX69Hv9915Eki6vb3t1u0HnYjSR85Do9Fw4FYA78HBAdVqlSAIWFpa4vDwkNnZWXq9Hkoptre3uXjxolMgyfpmZ2edOlOSQ6WvlEolhsOhgx+iDtrZ2WEwGLC9vc1wOGRhYcFNkkXx4itltra2ODw8pFqtsrS0xNraGm+88QbNZhOttQPZ/oTULwWdhl0CVaYhij9R9aGKrwy8m7JItj0NZ0QN5pdVTyv3YOK1OP28bH8a1Pmg0y+79d8zrRj8fqXQd9vm9OPTwG1aNeXDQB+qymumIab/+/spGQF3A8C3VLgb2JQ28CGJr8CT5/x29ttkGrDKNSXeawJb/PXdTUnlh5IAxwDmtO+ltIvv0emrsGR9fmmxf459r1RZnw+xp9trGsLIc9Oly/6++GpvOR9BEHB4eIjWmkajQaVSceq4aSh+9uxZms0mt2/fdjBKbvrAxNtVtik3A8TbdnV1lfF4TKfT4SMf+QjtdpsbN264Y2k0Gjz22GNsbGzwvve9j3q97sYtCbs6ODggjmNu375Nr9fj4x//OKurqzz00EO88sorlMtl7rnnnmPnUxTyolSUz6tTp06xvr7OjRs3eOCBB2i1WrRaLad2nB475Pz6n5v+Mg3Y/X4oHrNzc3Nord3ntFzb8/PzDtrFcczW1pZTG06PBf77BOT5Y7gPueM45syZM9y8eZNut+s+P4Ig4Pnnn+czn/kMn/nMZ3j22Wd59tln2djYoN1u0+v1aLVa/Nqv/RqPPPKIC+d67rnnjoH46bFjum+maUqpVHK2G/J5+idZ/vQDRKB8kBF3EoZz9stQOLSKO50askAxrgcYDUlVOfig8iLMwwGVQuWUWu85CR7JA5scbDQkdUVaDiavL4CGTq1nYtJQLvl1XJ+AxTxUaOnAyq5fJ1bBJrBTmQlszCIo7Rvio4ykHhCMc/qLIWejncIY3vqZnQgP6Gk7GW+ovFAipTSU4Wx9n7eyVaK+obsWEA6s2jDuGrImlI5y8tIEROnUmsMLGAvHnt+dsbBU1IiVvcKn0RTKxQLkWOBj2zUr2jYPjacstGpOUT6OZ6zJfSvoM0pDjC6820Y2zbncVoznMlrRgG9unWWuPyYrV0iNJi6llFXGXlKzQTgDu3/VYGTTMrGCvLgx5uZgjgfnN/nqlQedCjDLNbV4TDuv0EtiC36inIVyl1DnHGYVgnEOWhHrzClqEpMxSCMb6BKCHo7dRPAgH/Kl/n281V/ht7/1JIvFGBb1i1CQNMcERWMEGjUaY0Kbzmw6XYJBy7LBzGDGY9ummZ74GWbKBY+YOC/8BdVEAZgrW1osk2yBh9Neg1CkHjNRHqoCLkqZszyn7PtNbkEi2kzSn7WxikhlV2yUByTV5LeUKqvUKiKVrTNEUfgxyj4UZdYqt+pCWVQ2gdhgJkEn4EqbJ9ekhdc6Na6sWScFGMwMSvwfi37tAJSsTxSJHhi016c5/n9xM0BJcrS8plivO36BkUW4SlYyk0ARcvqmRIs+PRM7hVen8JZr6T7bWYOySuibkkvFjVRKqwA7nbxMQw/Zy6tkxnrBtTNb+tzQQ9p5hbJK2Mlqdj15iavjJco6YSdt0gp6FhYWvoUBxgaBaGhnNWp6RI52Sr1YZXSyCgRWRTg01lNRF+EZUhK8kzYp64S5oEsnLzOvejS1TYaWcliBOBmK7WSWZjCkqiZJznZfypTDSSJwK+q78UjlhjywCjMUHKZV+mHJJUBLQm5CQDuvFhC0KMsmdr6S/bzEblKnqq39wRvtFTqjEumcZiHqWOhpApp6yNCE/PTsd2m+e4BWhvtb/z/2/jzKsuS+7wM/EXd5922ZL/fKWnqp3tEbGjsaxCY0REIiRcuiR5JlSkNRlo5sjS2P/5A8Y89YHln2aDzH9sx40RzTtkRroWyaEkCIgAgYJHYSDXSj96W6u/bKyvXl299dIuaPuL/IyIcizTP/AcY9p05V5XvvLnEj4mV87vf3/d7m3/0jv85O2WMtGtXek+6mi/LucrHBRjTgO/kSrx9v8Sfv/R3GpsG3p/fxd/7HP0L54IRf/tAvMTUpyUDzT//BRxndU/Kxn3yNr/0vj9PoK5oGvnr7fm7urNB9RzM0TfbnbWYblurcDCqnxptMGmw8tA8PwejqKl2d07p3wPi4yUPJLk+duc43l5fo6oLe2ojBoMlWd8hQrzgVpLKQGpbTKTs4GCeKvRSXMlzVoT29aIKpTlR3ee1tqaFW4Z2UQhiraaucgcm4VfT4H65/kJ3+Eo9s7TAuGlRGEWvDuGzQqIOstpsD9osO9zb36UZTH4piY+ddPK+cH+jYNHxwkFVOPbgk/iK47/rom8v8ieov8r7z1/gLW19FK0Nb5UzPVejnLRe+qHj5tx9H/cIus42EzBaMbcy/cfeX+Pfu/QWWrpa0d8D+q3v8+bu/yWY8pCDy6dPgvDolpRqgqBPRu8qpSRtxyXDWYNcq/rl5F6pQ/uHij7cfjS2Eh+J1J2EeYfmdlG9JObAoWASYyWKy1aoTyIMFo6RaSumSlDQJTAoBxGAw8OqcMEFXFu7GGG9kLyEZIQgzxvhSLjkXUaOtrq7y1ltv+RJiAZMHBwenwBE4f7U4jmm328Rx7D2dZBEv4EDOdbEs805KNlksybWF/k0CaMP3hFBDSodDRZEATzmPNE39oljaWxSI169fp9ls0u/3vVooz3Nv8C/7XllZod/vs7S0xGw2YzweM5vNePvtt4njmPPnz/P222+ztrbGeDwmiiL29va8AlIA72g08jBy0Y9PILO0l5Qwt1ot3v/+9/PQQw/xwQ9+0Ht37e7uMp1OOX/+/CnvOVGCjUYjJpMJaZpyzz33nHotVNSEiiwBeoulorJgDUErnIT8yCb7CZVYi2qhUCl2JyAQ7n/xNRkPMkZEESqbqJUW4Y4cTzz15NzkvEJ1z+Lxwn2E/16Ec4swavHn4WcXAW3YPvK+RXj4e23yudBbNWwnOZ8wWGNRsSkQO3zoAZxSGMo5hX6i4bVJ28pnBUiG5aBynmF7h2qzxXsj1yc/F/gZtnM4b4Z9V+53qCgM+2YIiMRLVEBk2EbhA5Hwfsj5hvf9TsBW/i9WF1przp4965XQ8p75fM7ly5fZ2Njg2rVraK29/+ybb77J3XffjbWW/f19lpaWaLVaNJtNDg4OePjhh+l2u7zyyivcvn2bn/mZn+HRRx8lz3M+97nP0Ww2OTw8JMsyPvzhD3Pjxg3uuusu/t7f+3tMp1P+/J//8z68amtri36/z913381kMvEPz6SMe319nZs3b3L16lUefPBBjo6OuHHjBg8++OAp5ebKygqTycR/NwowDe03wjZdhGLhA42wnX+vBwZaa7a2ttje3ubo6IibN2+eKleXEC+xtJA+L3BX+keSJLRaLe69916uXr3q+4/MnSGk3tvbo6oqnnnmGb71rW95G4qDgwMajQbj8Zhf+qVf4n3vex+9Xs+f661bt3juuef40Ic+xH/9X//XGGN46qmn+If/8B/yzjvv/MC1/n7zlXghvvPOOyilWF5eZmtr645z6O+1/fADxHqxXmYRZdOBq3hmIeMEYiVuwV81HUSwSoF2ajbxGDQxTDcVjUNLPIW8C/GEWrnnXi86Tq0YT6HMOPE1S915mKgWFyoHPKrspERYNRTpsSWa1QEtAnfAJzCjToJUVAX7T6S0b7r04XxZuTK8euHdt22GpsnMOL+zzdpk/rDKuGZT1hsjXosUzb0ck6Q0jiv03BDNjVfBlZ3Ewc06ZCIelxSd2L9HF4bR+YyyqUgmlvmyu15R+zl4GgCUyqkKbW5JawikSoON9UnZ8s0TMHP4iHsK0tJzjucZNnZKKF3WfpRTaG5MONfoc3DYYa2aY2PnNahqA/1hkaFzTTw3VKmmq2f825tfoqVins1blHlEoive1bnJNyaPeT+6dpqzlo1JVMmsjD2ESJRBY7k1W0LnBqsUa40xjzRv8EBywMRGtJKcycww70aUq21Q8GevfIxvvXOR9MUWq69XPPLiHtP71hjcHdcA7EQ4iNZQGWyagFJQ1d4Wkfb9UTWbDlBHjh7aQvv+ckflSg0BPfSTg2kCr0McTJRbEBmsJPhIeXKlfCkuqgaEoSWC4kQ5KIvfSCD4iYLROjqHpCqHUM1G9nT5nvgklsopKWU81MeTtPKT8BXZjwOIojgMFYZhaIqNROkbnMfCg+DQw9D/XasYF1OVpS/7drrT72lynFptrKrapkDjA0SW9IwlZt5TrrAxLTV3ij49J8cp8jJdEFkHbkTN1NMzZjXUjnAK47FpsMSMM/HxKbAhUEPKNgEfCpLUqcLAKbXf2DQYm5SsLguemZRET+vPTnxKr8C+iW2Q6REZDvadTY6cGqtWDFao+o/2gSpjm7prMg2iWjUracfO87Bd5yi7Uu62yhmU2cm9MEBtEQFO4VahvT+hePCBg6lSmg0nPo+SSH1Pts/l2TpvDDb5zNbLfH7nMeaVe10Uol1VUKBpJ3ucWelTWM3+vE1PTznTGHGt7PHnv/ULZM2c//ap/56f//pf4IFzu1x68Tx//4/9F3zv8AIH//Q8/89/6Sd5uHubf/ylj6BTS/Z8i7feu+nHxvi8IRpHrvT5wozRVgRzzejWCu+9/wrfv/0A70oPOJ43KZYq7jlzyKO9HQqr0cr5/hVGo7KKRBnmswS0JVOG0oQpbRalXRgJ9XxKoUFbjnOnzhmYJqt25P01ozqUJifCmBPFakF0yntQ1/ciraGzUyPGLOkZ+0WHG/s90kbJ4axNFrt70s3mrDdGjMsGug5BGZQZ0yplKzlmMxo6r8HYEs1h/8tn+ffe/8f4y498jYYunP1Hu/Jl1UObcO/SAZeSLVZfK7h2rs1PP/l958tpU1Dw8fe9whtffRRdWEZ/+pi/du9v+z5yWHVY02Pu/1fe4NL/8CDHH5nxH977Fe/1KOPJA/i6zNr7MtaemW2Vczlf567uES/PzrjvLqPR+Z3VKD/efvg3gUiyYE6ShEaj4dV7opwLF6Oy0BX1XbPZ9OmY4j0o5VTGGF+WK+8RsJKmKa1W65SnmfgqitJRYJ6UxwqoknMP1VxaO/8/WURJSMjS0hJ7e3vkee4DVeAkQAbcgu7s2bO0Wi329vbY3t72ZcPyOuDLs8RfT0pypX2klE2CSIxxPmrnz58HnIpQFJaTycQDxX6/z+HhoQeVm5ubdLtdr0QEvJokTVNWVla8dxc45aSU10kpcrvd9p6V0v6iRplMJh4EF0XhVahZljEej33pn8DKtbU1vwAXr8XBwFnlSBuGi1gJjUnTlK2tLQ4PD327x3HsU6y/+tWv8rGPfYwsy7h+/Trf/e53efXVV32KtQTLhKBLKeXvT1mWLC0t+f1KebDcnxAayqI9LG2G0wvWUB23qIYK4XAIlxZBk+wjhMiLYG3xmAJ9QqAdKucWVWOLasKwnFSAyOK2uJ8QoIbbndoiPP9FBeTitggPF+/dncDi7/V+OK2YCh8shO0m9yU8T4Eisi/5v9zLRfWgjGGBJ4uAclF5ugjUBIKEvobhseEEcIcwKdy39Fe5vhDyhepFudch8FlUGIb/FtAp4yMsyQ5DQ+RY4RaqdhdLuAV8rq6uejsEmQPlHDudDmtrawwGA65du8bh4SHdbpe1tTXKsuTFF1/kXe96F4888ogHXuAerghsvHjxIpPJhHvuuYfXXnuN0WjEF7/4RR588EEefvhhf69Go5F/MNRut7nvvvtYWlri6OiInZ0dzp49e6pfFUXBrVu3fPiVBEwNh0P/XSMPQu40dmULQeFi31m8J+FcEo73EKItPtiQvnDmzBlu3rx56jsBoNfrMZvN/JwY9p84jr0CX2vN8fExN2/e5EMf+pC3IJF7Lm0ggVplWfLcc8/xzDPPYO1JMM4777zD7u4uP/mTP8mtW7d4/PHH+Uf/6B/577DZbMZnP/tZ/upf/av8pb/0lwD4lV/5FV566SWvXgznTTm3ECJKm0ynU/8zqWgQ2PsH3X74ASKQdzWq0pQN5zlYKnUCB+t0XZvUP6sU0dxS1UEJVsN8RRFPbK1sAUoLS650uHHsINlsxZUnVw2Fzi1lU/mFa9E9CVTRI8dM5ivUCc/OP7BsWZRV6Ln7uZRR2kihxG+NGjQYRTy3NPoS7FKRL1l6eka/DgC4ka+w1nTqnpdH57jYuM3ANvhbl/8ovcaUT6++wpeXFekgYnhBM13XJEN3kOzYMF/SNAaG6aqme71kvBXTPFD074tZezVnspaQHZZMtrQDogkcvKdClYrl1yLfdt0bFfuPRszOFTSvJjT3Lbo4adfJWcPGdyEdGQcrC4suLUVLU6WQKlfudzxuYhO38BRFRtWEpdYMrQy2n2JVTtG2JMpwZmnIqoZeOiWaOOVn2VUkquRv3Pwj/M1z/4zLxTpmnLCajLmY7pHtKahLUlcaEx7u3qZvWhhbA6pKsdUYOBXS0RZNa125s674E+0joMXU5vRnTab3xuTLMLyrxdIb8Po3H2V7WBFN3URQbi5hEpfCrYsadlkcMLQWVVYM3r3FeEuTjFz7FC3l4a5tNpw/p6j/ajj4AyXGi9BQ2VNpzV6FWMOrk0RkhZ1FDi5CQDc5KWW+kzqmqo8hKkeFUw4GgNMBwHp82PD/IWyuvyyiE59GZRQ2rdBlhDKmVgrbus04+azBg2+BhnfyQDxVUuwvLvh3qBSs4aINvrwW3+sVhuHnw11XJynr3tM0rGaxzie1q2ckqqJvml6t5EqLm769N7T7IhuSnbxum7SUUwReK3u0Ve4SlGuw2K0Bn5TNDuu5om9atPTcQ8W1eORTjQHadWDL2DSY4dJljU3oRRPW9JgD0yZRTlGV24jVOjV6r+rQjRxgO6g6pzzwRHG2a7oOtETDurzXnCQ7W821Yo2NeIjLXzYk2iU192vFZBblpOqk5LS0kW9b5Jfj+p7MTOJUYCGbrr0f3XWepNdJm4pisKtntHTOudYxFYq7OkdsN47ZL5d4ML3NtWKNv/nqH+GvPPhbvDI5ywv9c/zHF3+VZ5+7nz955S/xn3zgf+Tfef6PYw5SJqR8/cGHWP6djLfuPU/nhuaF+QVWGhMOgPs6+zzfP080h0/95HN86UtPATCtEvI1w90P7nD5rS1aukBpyJbmnF055uru6qk+FWlD552YnZ1zfOxPXEJjqUpNMy44HnZRkXHl1EajlCVROHW2lc9brIVWLN6RESo17mFFvfWiMb1oQr9qsVT7dM5sgrbGly/PbOL/7uoZxmpM7U/oSnudYr5C0dNTLg03eN9dVzmct7g5WGKjY7BWcd/yPg80b3N1voZSrj1W0zGHuStPdknXEY3eDGUzlt8xpM8M2Ur6zgs3VnS2R0TKXffENPjM6gv83zceo0oj/k+f+Z/pRRMfHFTYiL+w+dv81T+7zeEbq/y1h36bM3Hfj6dElRyYNn9p+7f41r92g0mVopWhpx1Ad6X4DbKocNfmk7or7x9aWU1SB5t9ZOUS10c9ClMvmNsGZX4covKjtFlrvQ+fgDspqRJvPcCrVkJVl6gwiqLwiyuBRbKvpaUlX64qpcmtVot2u+3VEaIiCxfoixAlDDGQxGA5/iKQkHMC/OKj1+uxtLTE8fExURTRbDbJ85w4jr2Jvuz33LlzpGnK1atX2djYoNls+lJY2aIo8uo8UUK22220dmmVg8HAwzspRQ6VgcYYBoOBDx144oknvE8j4INXPv3pT3P+/Hl+93d/l+9973uUZel9HKUEuixLH94in5NFlXgkdjodX+4mpYICEQXgHh0dEUUROzs7VFXF+vq6V0Z2Oh2vxpxMJl55KnBZQF2j0fAljFmWce3aNba3tz2MnkwmvPe972VnZ4fDw0NGoxF5nvOlL32J9fV1dnZ2ePPNNynLktXVVX9cAXmy2FTKlSk+9NBDAB6KigJT7mWYziv9PQQ5i+W7dwJ08rk7QTS4c5ly6Ht3JyCzqDKU995JxSfnKX0nBGZ3Kk2VMRiW5If7vFOpcXj8EIj8foDxThBw8bVFtWHYpuE13gniLsJdaQdp5zCoQ16XOSGEvouvhV6Ji+0cHncRAMq4FfXz4v2RxPMQDMl9W1SMhu0Qqs/CeU+UbDLXhYBP4J/0dfl5eE8ERoYl7fLgJky0D+9xOJ+G/UHeE0Lt8D0CbZVyyk5Ru0lbJUnCpz71KR544AEuX77M1tYWnU6HZ599lg984AN+nsmyjBdeeIH77ruP+++/n6985SuAm28vXLjg793q6iqrq6ukaerV0fKwS+bymzdv+rlYgNPh4eGpkup2u83u7i5nz55lZWXFK71FzddoNPxcKfPl4twQ+mqGSnmBdov9PBzHoVr0TuMtbHv59/Lysg/vajabvk+KD2K/3/d+knLPAR+Qdnh46B9oiX+t9IvRaEQcx/47YDqdcunSJd773vfy5JNPcvnyZZ599ln/cGJnZ4df/dVf5c/+2T/L2bNn+drXvsZ3v/tdD/uqquLKlSv8nb/zd3j/+9/Pzs4Ob7zxhldUL86/iw83BDAqpXy58pkzZ7wSN1S9/0G2H36AaE9Kk7FOdUgdphIGUtjYQbAoDEooqBVKDvRhnUKw6EA0c681Dyrybj15Ve41kypfvmxiV55adCy6OCnPjWYOFOrcKRnTviIZWeK5pZy5RVwyceep6nMQ5VU0d6XN6bBWC1aWfK1yfn3lEkOTMalSXp2d5Wt793NzsMRHl1+nr9q8eXOTh87ddoEDKwpwCrjppmW2DslIMT4bkQ5BGe0CYvqa+apC2Yj5mmW0nbiftxIfMmMVkBjU3JUat3YNky2NLizFkmFpa8TkuIcuFckQyhbojxzxt9/1ef6d6Z8hGcYoC6uvlkzXYkxUt3vswEE+i4mlOtzgvSQ/sHmFls5pHEQQKUzD0IhKntl8jWWd8dZgnXjsVGImgbbOGeRNxClNtUqmtZIqygGtMFrRigvWk6FbBJYR897Jwrq0mpu7Pe6vKqxSXMiOeLnI+ezg3fzSNz9G62pMOoflNw2NY+NKnXFQa74SU7QUVSOhbCnG5w02tqw/q0+AB64Pznqa2ZpivgKqcmAk7dcw2brwEWtUrT5UQZmv6yskFvJ6wjCclCVLMnKhT/kTuv3WENUqiM1pEAkQ4cqKBUJaAo/C4Oc+ZOXkfV6ZGII5OElbFuWf5SR9OSivls97n0MpO7Y2GLMnPo4mqZWFnCgWLScJyLIPd3knx/HtJYrF+jUbcSoF20ND30buc2HgkQgtbXis2kvxlNKxBrwmtaRUHrZFGJ+G3FJzojpFeWgycutUaD09Y2IS7wso5cYSUiIqPQmsEKBzLjrmRrUM4JRgesKBcWCuF0/Qyjj1n66TdGXfxsFDCWmRsAjZr4S5rEYj9qolNqKBf10CNsQzz6DJdH7KC0+8DZ2HYuXf3633cWA6GOtKY2cmpR3lzpNUGSq74GEZ0MJuNHOpzaogrUtbW3Wqbpu5LykFSOpzMTXouZ6v8spgmyd71zFWc1fz0M079ZOhf/f7P0v6zS7fPXsPX/j2kzQOIt66sEH7SsTYNHhpep7qrQ6bT+xx/O1N7m/sMNm2fOpj3+dLX3+StWjEZjbihbOWj3ZfZz/v8EYDttIBJnWp3PMqxmrrFIHa0lYlplLMDjPe7mf8zFPP8/rxFspCSylGeYP5qqXsGH796qP82Sd+B2sUzbigKCOSRklXK0yl0JElwvkGylZWGjNxvwKYpqWrc5eArq1Lui/gO6OLbC4PWdIz+qaFxtCqPSZNncI8q8vXu3X4j/PELMlqJZ78ShNh2au6lEZTWs3F7gGP927y6uCM8zas97md9qkqTTueMzcx97X3eLhxk6Fp0jcZ/8rD3+Hzy59g/92Kv37+dzgTH3NYdijaiqfPvQPAwGS09Zy74kMe+XOvMigyutEUjfF9tG9aVCj+H4/8T1y+f93Db4BWHVZm0BxUHd7XesdBVD3z/oYSfiN+j+IPKopeKfHvmxbdaEq/avHBjcs82rrBO/MN3njzbP1QiR9vP0LbfD7n+PjYQ8RwYS0LVYGKWmuazaZXIYQeYmVZeqWYADWllC8pE8AkMFEWvotlVUopDwbk/4A/rij/wgWybAIulXJqu/39fZIk4amnnvJ+UKurqzSbTWazGd1u16vYJKmyLEvOnz/vFTKz2YzJZOLLukMfNCml7Ha7jEYjBoOBh2wSHBOWpU0mE7TWviwvhJtf/epXWVlZ8Qu8tbU1sizj1q1bLC8v+wVjr9ejKAqvPBQYWZYl0+mULMt82fja2hrz+dwrDOW8ZAG3urrq21vKoMfjMQcHB5w5c8YnZQoYrarKl8RJ6ZoxxqdHt9vu4cnysvseFyXoaDTik5/8JO9973tptVp873vfY3193Z/fq6++ynQ6ZWNjw5ck7u3tobXm3e9+N0mSsLOz44MRjDEein7ve9/zSseqqrh48aIvqRYgEm6LAG5RSRcu7mWfi4v4RTVauOhfhIIhbFlUI8m/wzLoO4HLED4JbJIFdli2GqrDBM7fCZABpxbwi4BjEeKFACk8hxBihmBwETzJ/heh3qIyM3wtPFdpV1E7ixJ5EQ7LcRdVhuE5huXDYVuE6jEBUYuqazmnUMkXAmPZb1jCKvtc7IchMJR9LMJaOSfZFhWKdwLacs6LKrYQ1tzpnogSO9zv4v1dBFwhNJSgEa21n9vn8zlRFPHEE0/Q6/W4fPkyKysrXuEuZb9JkvjPZVnGlStXeOyxxyjLkvX1dY6Pj1lbW/Pee3t7e74ce39/3899cn/lu0qS7A8ODrh16xYf//jHfcK9fMdcuHCB8XhMs9n0ZbLywKLRaPj3C6iSdmy1Wt4DcWdnxwPQJEnIsgylnC3D9vY2y8vLp7wpF++1tOvigy/pJyEEb7Va3Lx5k36/T7fbpdfreQ9ZeSgn8FLaP1SdXr16lcuXL/P000+Tpilf/vKXuXXrFkop7wcsPsUAn//85/lTf+pPMZvN+NznPsfh4eGpPv/cc8+xu7tLr9fj5s2bft4J59m33nqLK1eueHAdeoKGKs1QaRuCdfmOOjw8JEkS2u026+vrrK+v/8AY+f22H36ACIj3kY3dn4o6/VgBGuKJpYxV7UfoAkqkrNlGDtiZGOIZ5Es16JtaV6YcOb8/8WRLhk7JiIQ31IAtyhUmspjEKc5Mgj+eKqHRrz27YuXPNSpcOIoAM125MuhkYInmDu4IIGluTnir2OCFyV1cGm8wyDNSXfHW7XWS11r8k7Wn+MPrr2CmMXu/fDef+8UZ8xXnOwfufOKJwtT+j0Ub4rH7edF0nSzvOIWkU1Raiq5LQ5ZrUbFFFYpkbE8pu2wE8zxGF5ykoypI44or+Tq6UC4cRol67AT4RNopWcw4wUQwrJp1oIx7/Vyjz37RRUv1gIb9eZtPLL+KwfD27XXiGKj329Zz0sh9yVyabZG1cvbnbacI8T6NiqXELQQ3oyGDYYtmAZSK7fSY2/MllLJMzmVM1jX/7XNP83dvf5LlN+Cu2xXJcEbVcPC0yjSzlZh8STHZUnQ+vMfBQZfl38mYnLE89b5LPHflAqgMlZcQufJllPJefjo/gVCufN1AElNmNoBxtRKv7u9UCkL/P1H7iRcinJQrCwiUrQ4/UYnBlroGk3iPQ78fWx+sDOBhWSsNI1OXNwfHq++9Kuv91bDRxvYkgMX3GQGWAZCsAacv7QvOWaCgG9cnoM7DVHm/OoGIJqrLmWN3b32oieWk/DjcP7UnaiXwMSjRD87Fqwrt6fuGgE5/fvV7a1CpjYPJAvoAKjRD0/Alj5V1qiWBCs6jz6UJi9cc4EtKZ5WDelEdPGICxR1Qq6XcYJqYBmPTIFUVLT13dgE4YLlWJ9weFB3v4QcOxGiML1fuVy3WEpce7EqgS/qmRU9PvPKrreZOzVjDTUkwlvMem4b3dJRyaHAKx0gZ2sqVL6eqcinPtSISC+Oy4X1lbaIxNUC0EXSimfeRdKXfuQOYqqDCKSLzuh4/lXtgYUnPmJuY3Dio+43D+7g1XOKnL7zEB1tv8e9f/mPM9pqUZyyVVZx/cJdrV9a5Kz5kdF/JH37vizyY3aJsGe5ZPuTZpXXuio8oWy7FGXABMiZClfXxqhhlcB6uEU7VWd/0WRlDZMmtxlaKqFNyYfOQn+49z8v9z2BiS2EthdGU2znLK2OGY6fws4Vmf9omz2MUMLMWWynsYcqL+Qqxruhc1fx/9j7Bfav7vP0bK7y49wCbT+0RYYn3EnQJq49M6F83fPHqw/zRx79PjvPs3IxGHIifJA6whWEiiapqf09NxIkaNVElmSqpjKIV52w2Rpxt9Ml0wYvmLEUZsZqMmdmYSxNXzn0wb3O+1ecg73g/y4lp8FTrMv/gXxxx3+oRa/GIwkZUaGYb8FDrthtH9QDvmxb/yua3uFGs0NMTDLoOBTK+Dw9s5vuhjA8Ze+LVmdfAX/p0QURXT135fv2eNACQkTIMTObH5ZX5OhOTshJPuDTb4mOd1/h7+ml+vP3obcY4bzApKZJFjkBBUV8sliyHm6jbjDEeHIqfYvgLvixQRRkWLpJEpSZAUpSNUg47GAwYjUZ+ISsKpNBfy1rrS7hkwVYUBc1m0ycUdzodsixjdXWVLMv8QvD+++9nMBh4hWQURRwcHHj15erq6qmAEYEUs9mMdrvtS7alHdI05eDgwJ+LQJ2lpaVTCyRRb4riUxZ6kiLd7/e9inFra8uDEblf0oZSKhjHsVeAbG9vM5/PvS9knuc+ZCXPc9bW1rwZvUBUgQ+yH2l/UbtIe4vXmRx3PB5z8eJFxuMxeZ6zs7NDHMe89NJLNBoNHnvsMV5++WWee+459vf3vSJV+kyn0+EjH/kITz75JNeuXeNXf/VXfTDME088Qb/f/wEFnSzAw3YTtZX0s8UFbQhCZB+L6jvZ76L3WwgNpf9JH5X9ybFDBU2oBJMxIEqyRZAQ+ivKOYQw507nJv0/VKrJdcu5L253uubFNrrTtYfttlg+eCeF3Z28/sL2/4P8PDx+FEWnri0EfOE5CIwJAa5sIWxcbA9pfxkHIZQUKCgQJARtMs5CQBL2pcVzCdsKTkBR6IkHp9O6w/aW44dQcREAL3qAyj7D/irvFeW4/AxOSrRDRe2iWk7+DlPvNzc3T6nVH374YZ599lkAPvOZz/hxLw9g5vM5Fy9e9AEdMnavX79+Kqld2ldSeEP1o4xJGVdy/Vo7z8AkSZhMJpw5c8a34WQyYW1tjWvXrvn5VLwBAV+KLYDz4sWLHnTee++9HB0dcXBw4FXcMn+Kh+zFixfp9XqngkkW23AxJX5xC+992LflO1gArABZScCW76Nms+k9jqXNf/M3f5NPfvKT7Ozs8Pzzz/uHXTdu3PAeyNLHrl27xi/90i8RRZFPVw7ntKIoePvtt/31hGBe+o34JoeK3MU5+U7XLGNR7C6Gw6GH1FprNjc3f6C9fr/thx8gKgfdnDea8kqisukUgGULMIqq/v3MxGAa7ufxFJKhJR04kKdLi6n91qyqS0+Vomy6ZGYBglFdCVdlNaSMwES2Pq5Fx8p7HJrUHSOeGebLLgVa/AOrWslo6/8LmEgmlnhceUUTwGyc8p3RRV4dnOGdg1Vm0xRbatRhQjKCV/+XB3iteoDuBLpX51wf9kDB5nMzinaMesV6GJkMCkZ3NckOCrJ+TKNfosvEJT4XDnjGM8tsTdRbddsq6/0ifQBNfZ1RZKjKWplpnBqz03CLf1Fz6uoEiAJUTUsWlySqRE81JrNEyvggl6oB9zZ2+fWDJ2ndrv0UmxXGKu6KDylsRTlO6OxBPKswqQto0LVEbFQ1WOlMXBIndVIzDiidbfT5dPt1Ggru2jzk6laTaKr5YOsS/9WrH8WMEo7viUjGlq0vpjSOS1QFRVtjVhNmyw7ymBhGFyA7UBRLlr/76N/lj3z530CXluae4nyrz/eO7nPtZPD+hyh1UoqpcWnL6gSc2iQ6Ea/FQVmheBkK1BIQJyXLCqdElOPJz4yoBg3MdR2Mchr8UZcSeyNPqyCtQ1kqdRLMUqvp/H7DZOi4Dtupgs8oThSRssm5yznK+Se1xM+CwgWqeHWuALqFTR4E+LaLwdoAMlbK/0wXJ1DWqw9lP4aTzyjXcFJ274+rOJXGLCpE97BC+UvzTRIojESVWKF8IMXNcoWunpLVqq2DquNKH3GepwA5Ef2qxWY09Gqng6pNYVPWahVgT0982eheucSZ+JixdYXBLvTCXZz3ADSJ81ZU5hTQTGuYl9vIh6gAteLR+R9KiaekM7f13MPDMK1XFIsFEUvMfBiHnIcoLiMslSq8d1xhY9p67vdzWDl/rbPxEbqeH1QFqrRO/WsjTOzgVVWXz45Nw5+7wFIp5RbPyMTW6kcizjWO+J3qHlo654HOLuMi5VsH93J/dptJkULDYJcL1pIxS40Zuln6/tCJ5u7BR6V4cWebu9990w2jUnGYt9G5qiGoqgPTje+3AhjBebuirfOD1JZUGagUSVryN+77p065WkXEY8XbZYt5EaOOEpKNiujVDr/15AMknZzBb54hzdz31NfeezfdlQnJt3p845MPcl9rn5dyl178c5vf5W+rB8g3Sv4P932FvarN0rsOGI4zPtx7i2+/72F+/t7v+/PLdMHYJiSqIg2gcIVyITf1vxNVYaxmVsO6TLtUb/Hi1MrSSyZsJcd8+fARhnmDLC3oxHMmVYOnOlf5RnSRV26f4VZnifdvXHXJzpTOR9Mm/NuPfonDOnl5bBrsF11m5wrub+wAeHVrYWPyOvU8TP3OdFGfo4PzxmqGVbMec7VZP5bNaOhUvSb16sKhaTpFO9YHwySq9InkYhdwYFr+eMvxhK6dsZUccy4+4v5kcDJ3/3j7kdpkoSelW4Av3ZKFsCzMZMEgv8AvLS15eCSqPCljFTC5ubl5qswWuOPCVxbroepAFuvLy8un1GehD1iouhKQKQtOOe+yLNnf36fdbvvFXKfTIYoibt++7ZWBZ8+e5erVqzSbTd73vvdhrfWBH6IwXFpa8j6DeZ5703hZmApkkIWRlDpLe4SeawLn4HQyq5REAl6tKYthASONRsPDieXlZb/wlfJqay1bW1vcvn3bJyKLL2Kr1fJwV8DUcDj0UFAWb5LqvLa2xpkzZ5jP537x2m63PXQ0xvDAAw/49NGHH36Y27dv+zTl3/iN3+Dw8JDhcOjTnquqYnt7m49//OMMBgO+8Y1v0O12uX37NhsbG2itfXK2LOTDtOoQqITwKFRfhfAwVNGFaixRK4XKrnBshIt76WOhynARsMjfoiiSxfBiX11U9N0J0IXXtvjzcAvHTAjUFhfnvx+4u5NqUNpCQE2obltc9N8JBCwqrBbB7eLnFl9bhGzy+TA0Ivy5nHM4v0h7h+XBcOLVGd7P8F6G0C1UXEl7hOpBea/8XOZKOQ7c2VswfGAim7TvosJ6sWw4LEOW16UfhH0g3J/YLSy2swCyMBhGrjf0SJTxEV63tdanE4sqOyynXltb82pgsSyYzWbetkKgl8zNt2/f5oEHHuDmzZv+NUm0lzYRsBQGFcn1yYMPeZC1tLTExYsX+e53v8u1a9f4uZ/7Of/wZH9/3wO+paUl3vWud/mS8e985zvcf//9fp+tVgutNUdHRywtLXmgdv/997O0tMTnP/953nzzTd7znvfQ6XS4desWa2trXL58maOjo1NtKvcj7NtyD8PvN2nzcNyIx6Q8vIqiiMlkQq/XYzgcepgdRRGz2Yy1tTVf2iz7/u53v+sV7PKQbG9vj6OjI/99Ffap/f39H/BmDMd06PO6eD3h3LnY/2WTz8uDq3Ach5/pdrteJdpqtVhaWrojdP29th9+gIjALQfgormlzBzMaRy4VGBdgamcpx4W9ADaNx04dAEiDohBHYRSAFjyrmb/iciHqUiJsaqZSpTXILAByjgloS7xybBxHZiSDp3XYZS7kJGoqKEntcqpqKFc7LwP45lB55VTLCYupZdBwm9cfhfjwyZqFtG8EaFLyJec52Bzx9LZqRieixmfTXli5TY3ltcp2jE2gsHdCfmSU0J2r2l0aZmvxHTfGVM1E9q3cqqGJj3OqZox0big0c8Y3B1jbQ0GtT3J5qiv00ZgG9YBROWu0ySu3ZuxW6SZ1LVfPHUQxSrHsWoPfrewnCuKXp3wWisV81WnQDqct9Glu88qMXSTOYmqmNgKrIPBqnQlzJkqiZUhUopx2XDZHMYBhSpT6NxQtmLOp4dsRym3q5z/9P5/zB/f+cukbzT5+d/9RTpfbrN6YIinBco438uiHVGlTqVpUuWTin/hX/1nHJct/vE/+gTpkeJPPv+LZNdSbJ1mvJkMiceadGxQVeUAYr2ZYPQpU6tZDSflmeDhE6U+7W1ogNSeBJCok19ilLYOOCpq1aBAPk7AncA/cKXQADN9AiAFFmrwrMArFNXJvnS9H+ug4alzdrQEiz0ptZbXvGpQnYJsFJpoqjzg88pE4XfW/d/WYStWK6emrPuiB6vqZBxi69JnmRjDfRlOgk5EVSvHtgshQaGfoj35mdXqRAEqL5/yPqzhpwKbGl/Om1EwM4n3FCxszFo0YmwaJKpyIRoor6jLiRhUGZku6EUTJqbhg0CWlCQczyii2AFILBEu8ELKkHMVnUrH7eoZKYYcTRa5tOexaTCsmrT13Csdx2WDXjRhYDLvD9fTrgw6pfJqrYltgHGgUQDixDR8mvTQOPP8RJUU9X4Mmo1o4AM4Ej2nMm5/siWqdMCnSk6NEelmysLcJFSRA68Cc3raqUMqlFdozmzi1I61qm1mEvaLLuM85ZtHF3l86SYfXLvMb9++n41oQC+bcn0WsXXukD+2/By/fft+UA4EqkLR0CWRMnSuaqbbEf/Rfb/KwDaw2tJLptxpa0TugcS0SkkGDvreGPfovpFwvdjkqSfepqFc31QvdHn2kYv8ofarnGkPyF/f4u/c/gRFFZEMNGc6Q8bFBgDvPn+D70zu5b0PXOa5KxfoRWP+zH3P8tt/+gH+2NJzHFRtXv/5Lf702rc4G0341L/6be7J9jkT99mtuvwHj3yWV2bnuJAc8G9+5jfYiAc1tIu8Gi9VFeO6/SSdO1Inir6unvnk7rSGjcMaskl5741Zz4WClSnDWYMH1/YojFMS3i6WWW5N2TvqMpw1mFYJ35vew/ub71Dh5vK1eEQ3cm2bKQfCOxtjDNqrBMXzU4Ch9MWJbZDak/Ag6V+5cvB7Zp1dQFdPXShP/UuXlEVL3xqYE39SCVRp1WX/mSro6Ql902JmEx7Prvmfr0YTejpGxbXf7x+8YuTH2w/BFsIHSbldWlpiNBrRarUYjUYsLS0xnU5JksSXjora7ujoyCsVxbxdFH+9Xg9jDIeHh97AXUoDgVML21DRI8q82WzmS2dlESI+VbPZzHsxhQtbWciEsGk6nXL16lXvGSb7sdb6FN/nnnuOb3zjG8RxzMc//nGqqqLX6wHQ7/c5c+YMZ86c8WXF4HylxC9KlHiTycQvACVYRSDBohJJFnSyeJIFdZjQLCV5ssiXUBkJdgFXMiz3BPAqw3a7zXg8Zmtri+Fw6MuPxadse3ub2Wzm/dtkoS4waTabkaYpvV6PM2fOMJvNPNCVa+n1euzv73P33Xdz6dIl7r//fvr9Pm+//TbD4ZD9/X2m06kPZBmPx+zv73vouLGxQZIknDt3jtdee41z587xwgsvYK31ak1Rs0r6N5yAmRDIhm0pfVuuNyyZD0FjqHaR/iPlpdK3AA+F5L0hEJLjyv8XS33Dexne93AxHYKZEFyFEE62RUVYCAUFZoWp0eG2CBQXx0947iHQDCHXnRbtiwq5ULkXbuE+w2sJrzMEimGbCMyScS8PPUSxFoJZKT8PPx9CMPm3QAs5noDJRYUicOozi+cu8CcEeQJDwv4VQk15mBH6Lcr1hP0mnBcXFYZyP2S/0rYhXAvBZthnFvuVfD70eAzhcQhZw9L+EJJmWcZoNPLhTZubm76Ednt7m1deeYVHHnmEqqr8AwtjjA9Z+fjHP853vvOdH3jAtHhN8uDqzJkzNJtN1tbW6Ha7XinebDa5efMmjz32GFEU8eijj/K1r32NRqPB1atXWVtb8w8nVldXvQr++PiYRx99lOPjY1qtFpcuXeJ73/seq6urfPe738Va69WSN27c4CMf+Qg7Ozt84xvfYDgc8su//MtkWcZgMGB1dZW9vT0/by6C8rAvhmNrEcaF40KsN8TSYjqdMp/PWVtbY2fHPZAejUZcuHCBS5cusb6+zmAwoNfreb/a2WzGW2+95e9jmqb+YY+UQodqQoGS4QOKO/UfAbthenk4jsL3LrZBCOVlk/FtrfUeiDK2RI36v70SZotPPNZ5DaZq9Z9JLGXLlTLGE8j2LI2hqRVxrqQYoHFsqBru3/HcuLLlyqIq5VKXW3i/wmhmoeGUiUo7cFE1HAQzMUSVOwdTQjLEK+riqSGeWXRuMKl2Ho3WBT3o0qmhJPilymKqTJMe5dhI0diPmNgOUaFQhaK1Y2kMDLq0VI2T8lyUg1z3NveJl3PybkbRVuRLUCxZ5muWaB5RZoq8B8q0mS+7oJTZhmLtZc3obERrL6F/fwTGgVDvNechqkU85mxkHcMqT8CSLiCLCg7LNunAgVTnH2ddUI0FE1uasfMsi6aKYs0pkKoUGkeK4kLBQdnh7f011kYGtCaKDfc0D/ziT800ydiCtVQNfAJogqK0msoqchOzV3Zp7huieUWVJmzEA/67wX38929/mNF31lm9ZhmfheY3OqRDg9Uw2Yx9eWqVwmy97itHlsk2dN+B3+3fy++8cw+P/9RbvPw7F0meXUE/dUzVX3ZtUKeDmlg570GNUx8WpS/pxtZ9VgUqukbiy2xtVXc8CTCBOq1YaG4NuSqFEm9Cbf3/LdTqvxr+xScgUtWKP1vJwahfU84jUcqLS3WiPLQE5cucKpVWYdALnLwegkIJaFkYw+6E8OpdhfU+iD4QJYB/J16ZqlY+uveIatHG9TlJArNyDwp0GUBICWoJT0WSoO0JdPTHFoFkDXe8glKAasBqleUkSEbmeY0HG6LGg5NAj8I6wGeMJtInqcICvwD6VcuXRs5swpoeo5UlwyUb9/TElZwq44NLZjahpee09ZzCxoxtSobb57gOxpDziTCsxaO6mxmM1ZyLj3xJ5pKanVad1SrGVFUMjWZCg7adn/JNdOClqn0MtYeiqaqoxL/RNFzpZ+0lh3Lt0jetuixVuZLteg6CupRZnwyFCOuTlXMbuev0YSlubtA2IaPw96Gt56zGY852jnnjYINWnLOdDUijikwXpLqkeT3iqQ/cYCOaMititLa+jFcrS25jZquW/+tTn6ddQyhRHuoK/unReziqk40zVbCajOlcg3/2O+9m5Tr83VtP88jybca3zzK6F/7i2a8yNJoH797hxut3cVg6td2Heu/w/B89z7+29RW+u3wPL5w7z6d6r/Dlf+GY+9Lb3HXmgJ9e/z5r8Yhn1taIsDzVvMwj52+QKMNaNOYXz3yVRFUcm4Q/0XuWnWqZmU0cXDUp78puEGF5qHHTQb4aHC7pme8PomSNbOJDbuSeglOfjk2DJT1z3pgo8vr/y8mMo7zJK9U2pdW00oIHO7u0opzKaraSY/6lC9/jl6ZP866N2zSjgqHJfBCLCSaPqlYRzkzCfasHvqzYDTXX73vR3KtzRSkoQUPhftaikR+D47rcX/aTqBJszMykdVjM1PfplMqNDaz3EO3bli9vrqzmZrHCftHl3sYeN9QKzwN2Ens/1x9vP1qbwCcpHVZKed9D+SU9SRJf8npwcODLh8MAjXa77cMbZAE9mUx8GVjomygLjXChLYtJWbS0Wi0ajYb3EhS1hUCJRR+zEMwJiDPGMB6PuXz5Mvv7+wwGA1ZWVtBae+8rwAdwTKdTv9iO49iXTe3v73N8fMzKysop4NJqtRgMBnQ6HTqdDsPh0F9XlmX0+32vYgkVJaHqS9pE7gVwahEvpbKh2knul8BQuUdy37rdrlfwtVotDg8PvTrTGOebGIIWAZOiaBRo0O126XQ6bG5uMhqNvAokTVP+3J/7c9x11100m02+8pWv8OKLL/Lmm28ynU69p6aUKN97773cc889vP32277/xHHM7u4ut2/f5u677wbgxRdfZGNjgw984APelwz4AeAmWwiqQqgYgplQoXinBWeo4goX+vJaCAfDxbP0uUVwtKi6kf0sKusExsh7Q+gdfiZUk4WLbIGjot7VWvswn7DsXPpEeA3y+UXVYQj4wnb7vaCf/H0nBV8IGBbVh4vH/l/bwvkhfNgQ3js59qJKLiwXl+sKr1d+JgAvLN29E8gR6BMGa4T3X85Byl3D+xzuS44TgrLwHocK5TvBJLk/cizpi4v3UcZ1WO4b9rEQPi5CykU4Gp5n+ABIAjhkHIbq3+XlZdbX13nooYf4xje+wUMPPeQBouwvilyK82Qy4a233uL+++9neXmZlZUVb7Fw+fJl/x3Ubrf53ve+R6PRYH9/n7Nnz3J0dMT6+joXL17kS1/6Em+++SYf+chH6HQ6zGYzZrMZL730Etvb2zz//PMcHh56f9lXXnmFl19+2YdOiffr5cuXefHFF2k2m+zv76O15sUXX/TqSElvlgCdS5cu+Xno9u3bvj/J/Qv7S6g4DL/LpO0X+59Sztt3ZWXFp1QnScLKygqDweAHyurvu+8+5vM5169f9w+ZwnsnoA/g8PDQp1Av+mvK+ArPZbEPyRaCZbmucG5efK9YpEg/Fh/JNE39Z6REXM4/yzI2Nze5efOmt+v4g2w/GgBR2EftN6YLp4aqUojmzscvPbakYwcGq8SpBV2SsiIZuyCMWS8+8d+rvQ29n2GtcBTAo8sAEOCOo0UFVW/KuFJg97dBVJJVFnwJGlcWjCiZrK3hiD2lfopmkO1FmMQ6WJpCPDE0jubkvQbzXuTKamsw2ormNFtzrM7Il5WHduBAWOPIUnRg1nMKwWgGRbdOR24oyqwu+7aQjN25RbFxFbQx5B3twy5Q1iUZl3XbW+exuJzOuDHrOc9Iud66/aLc1ioe5wkXT4DYcFS2afTdMZZ7E68c0bnFJJokLXmydZU38zOk3ETnDvASKaoMlnXBf3L+N0hUxLhMmRcxcb3AHZ3XTDdazFfg3/4Hv0DztqJxbDh3fUbZjJhsJejCMltzQDbKbR3YAEdPGv4vn/o1/qP/8U+Q9hX5smFyRvPcbz6Calv+7if+Cf9i8Se5+fXzfPTC23ybpyhbcDY5qj0yLRgDtZemjSOvnJLAGC0BHkpRZfFJSW1ksFXkPA81NRzkRE0ocC52O1SqhoaSbhybk3LaSp2UEscGWwUgMrYnqsRG5eBbobGp8UBSQJyHiVF9zDJyILn+mcyTyir3b1Wfj3b9xZdUxxZmmmiqMQ1LcqRZfa0k28/rMa0oOrFTsYKHR5LQnIxdujfWAcNT/oQCY4OgDWVqtXGoUqxftkp5FWPAFnzptLJOYYtAShvsW+4lJ+fgg60DWKqzko06kVi2ftViLRoxEN+/Wnkom1M3VUxMo/ahG3JgnK+nvHdYN5CUKke1AksgyZKeOeAlHoFYhjX0ENCSqopBlbEWj3wQBrhyZoGPEjxR2Jh+1eJMfExhYw8fnbLwNBid0CDChccYXEqtK9M2tbpN18ExTRJVesipMQxt0yu8xqbBvIyDfuDGiusnJ+c7swltlbsS7bo9Ugx92yChpK1y+qbpvfvGpsH1fIWNbMRdF45o6JJbsyWsVcxMQm5iJvcU/MWN30YDk1nDDaX6HjkQWtI4Uvy/3/okf/2BL5DbiOaeez09Vnzuzcd4z4XrzhUAw+Pt63xh7UM88fhlvt+6wM+vv8bTrTd59l++wMdXb5OokptVl1+88DW+8ycv8lTrCgemxdOtN9l6j0uK/mjrTR7PrrGk5pxZPyaqQexm7Lwve9mEoWmSqorNaMSklny3Ve5LjAEP3RJVkWkH8XbKJbrRlB5OedM3LVajke+33VpNKInaBO3Rr1zp7lo0okJT1GE/ldUsMeOu5iG3pveglWUwz/hD22+wnR7z5nSTZuSA5cXGLn/6vu+S6JKjos2F5JBMFdwoV+r+a8h0UQcNVdzVOIBldy+Hpuk8J4lo67nv5+K5aaxGXEJd3294paqMNwlZceEwU9I6ZKhflyUPTZOZTdgrlzguW0zq8Xdj1uNo3mJYNJgUCXkZUVYR8zzGVJqq0ijtFPuNvaj2XOXH24/QJr/ECzQUsCdG8FtbW165t7Oz4w3rBZDJL/lxHHvop5TywFB8A8WnCfD/F2VYqHiQBZW8XxR44uskC+oQrNxJnRWq/fI858aNG2jtyq7l2PP5nJ2dnVN+jaLIk33K4lvKsJVSbG9vMxwOybLMl+M1m01v9r+7u0tRFCwvL2OM8Ume0r7iUxWWasviWzbxhBTvrlC9JjBR7sHy8rL3I5SgGvF6lGCCfr8POFXi8fHxKY+46XTKYDDw55ZlGcPhkJWVFX8v5vM5u7u7dDodvvCFL/DhD3+YOI75tV/7NW7cuEGWZRwfH7O3t+chytLSEufPn8dayzPPPMPR0RHdbpfvfe973Lx5k8FgwFe/+lU++clPAi5h89vf/jaPPvqoL2kMF59haaj0XQEc0rYh3JOfSR+X8wp9M0OlV6gEldfhZNEvEDMsK5W+JkoZOIGdsuiWPyEgX4RGIfiRY4b7koW29Pssy7x6StRUb731Fu9617v8uUoa63g89sAhhBG9Xo9ut+vHfthucp0y9hahm/TFEGiF8GARDN5JdbUIKcJt8R4IXAjVl6FSdBFc3kmlGbZ3eI6h0mqxtDQEdyGckf4n9yoM7gn3C3jIuwhhwvMP/y99R2DiorowhDNyLIEw4bwStnXYf8Ny9LDvh2MrLGkOH/qE9zcERXmeMxqNfBiVzHmi/N3a2vLzjnxHSHiTtGEUubTlxx9/3Pv3nTt3jqtXrxJFEcfHx2xubrK/v8/BwQEPPfSQv4YbN27wwgsv8PTTT3Pt2jVeeOEFDg4O+NznPsfa2hqvvfYae3t7fOELX/APemSuF8A8m81+QNEsZe5HR0f+XKVUWP59J4C+qJAO+9FiPw/7ejg3CNAO9y0qbFGNixJPbDjk++Tq1aungmrk+1P6SAgx8zxnd3cXY4y/nhAQh+cTnksIOkPILABSHmLId5P8kQeWrVbLe0eKSl/maXnoIR6ZAgpXV1cpy5LLly9z69Ytr0z8g2w//ACxVm/FY+djWLQdUEoHLjwlHbiSYatcWEjeVTQGxsMZcL52ZaYoWy4gBGCyoX1pNEhZs4NpZaaIpxZVwxZdgIpPzkfXysN4ap2fYQ0PZfNe6zGgXLCJiWqVmqicKks8rv1tYk00h+zAYmJoHLs058ZRXW5SGlSliWr1pagqt7tDjuKeS4tO3EI2yh0YVBXE0xO1WDyzRDNNmUmbOPDqfB5VrZCz6LIu3a5ViLZWlM3nsU8+xrg2WU6mHOYt5yUZ4YNUrIJ5TxGPoagiutqVCuvE0FClA5TLcLY9obAx0+PMQ5xGUtJSc3bNEm8Um6RH2kHXwmBiS6ZgRWc8lxvePNigqCKef/Eiz1cXaSsHIntvWJKJS7guM83wQoMqdfetypz3pS7g6PGKJx+9wtV/eBHbKvnd4UW6l12bNI40T/70qzz/hUf40EdeZVk3HQwdwpe+/iS9iaXouOCLZFh778kv5UpBVZ0op5Q7NtTKKmNOgSlrajWgCXwQPThUJ+q9SqFS64JR6v2qyGCtcmXN8j7LCdQSYFgrG11icq08LPVJSrLiRF0oABCcElFUdrIYlv1HFptUJ8c0CjXVJMeaxpEba41jS3O3oGzDtU8rWrcV2e4cVRr0rMCmMXkv8epMga1YiCcVjcsu0MNmYnJqsY0Ym9YlFFphI+1KqRUueCPRJ3BPqRM1oWyiPhRPQ+VUbhgHGC0noUp+c+z/5F4INFXuHEQZmWYlXV1ws8y8aksAWTsof0xwnn4JAtuUVxbKz9eice2n6EqQBdR09YxlVTCsQWFhT9SIORFdPWOv6vp9S9BIqio24yEDk9Gv2pyJ+14xKKqsEBZ2o6lTIarKe91lqqi94jRtCmY2ZjUaeR/DsU1P+cRRd5l+1WJsUpYidzyN8eWoFcp70CmhtIB06MX7NzYNsqggqROusxp2uURg7fwjiTxIA6cUPi5WWYpn3sPvgeU9hqZJJ56z9FrC3378p/g/n/1nJElJ+vUuLz51AZRTr11Md6kaMPnSJrP7Ei4kB0w/OOKZ5Zf5xkfv5T98+Nf5R7sfpOxYxjbhYrrL//7PfJGHGreYbSe06/b46w98wQM4cCDwp5Zf8OXWFYpzsVMkDU3Kmp4yru+H7zuqJCfy4SXu/84TMIR8mS5c+XntGVnYmMSW9CunnttQA8BBNtdHa28xLAdV2weonIBEp96T/uQUea6dpU21MuzlXVJdkUUlnzzzBq+Ptrgn2+dCdshGPOTZ4b0clm0ea14nURUvVC5BWRSwbTV3QUP1uQiA77XGp/wzx1WbQsW01Nwnlw/NybgbmMwnSF+u1sltTKrc9R+XLUZVg2mVMigb7E67WKs4mjUpyoiiiihLTVlGmDzCzt1krqp6jvShUG4eVKlxc/BRilqbs9SecZy0+fH2o7eFJYvWWu+RtbKy4gHfYDDw5V6yWJhMJh7QtNttGo2GDyGRhYSUQxljfIlsuLgOFXOyIEuShGazSavVwloXirLofyewEk4rxATSKKW8QiRUg4hnYa/Xo9/v+7JeWbjI/gSmiDfXfD6n1Wrx2GOP+QTPMBDFGEOz2fSJnc1m04NKuT5pS2nfUI0kajwBW1KiJZ5lcu2yUJfrF+WNJHGGAFHKvOM4JssyJpMJWZZ570QpGzw4ODhV7irqjve///1+AbqyssJsNuPq1avkec7+/j6/8iu/4tOPZSEYtmOv1+Pnfu7n6HQ6XLp0iaOjI+I45tatW9y+fZvHHnuMhx9+mNdee41HHnmEr371q5w9e5bJZMKVK1cA+NCHPsTGxoYHu+FiWo4jgCuEa9KuoRdlCA9DOPR7laFKKWkIb0TxF0JA+az0S9lvOKbgdOluCBd/P2We1ppOp+Nh6tLSEqurqywvL3sFalVVzGYzVlZW2N/fp9vtenWV1i49W8JowlJJ2bcApdAmQJSLoTI2hKlhm4YqTxk3iyq3UG0Xtsti4MKdFIlhiblAKRnni6Xmck6LQFcgyyLECR9AyPksqoHloUaowJPXQggs4CacnxbVWIs+ctIfw3sf9s/F/hoeP2z38GHKIoAOQXQ4fsLzXlS5CcwLYeviMcJ9yXwlDyMEqMr8kqYpnU6Hixcv8uKLL/r0XLG/UEpx+fJlVldXGY/HrK6ucu3aNR544AGOj485d+4cu7u7rK6uUhQF77zzDq+99hoXLlxgNBoxHo/p9/scHx/7QA9R5IkthYDVg4MDDg8P/bUKmArBXdhH5T4tgsA7/R32G3kotNjnw3YM+9UiVJf7FfZ/pRSdTsf3+9XVVYbDoYdoYcm8MYbd3V3/UC98QLAI18uy9EnM4XdN6M8pfSFN01MgUM5L/HDle0s+J0nWi/YD4Dxqx+Mxh4eH/tzn87m3FREVq/gAywOE9fV1sixjY2Pj1Fj5X9t++AEirrw0mjmuEc1x3oaHztvQRooyctChaLsS4/mSK3vVpVP65R2Frhwc0FMos5PwBClLFjCoC9B1EIrO67CWWrEIzucvmkJjYIjm1nsmmlh5KGEjKBsSOBKoz4wr79WVU2qZSKPzCpNq5++YQ/t2RXNnSrGUYmKNLl0ZczIy2KWIsq3IDizHZYvz7T4HyQVW3ihAwXwporVbUKUaZSxLVyzRtKRsxUSFodGPiWcVZcuVLmeHmuN7I6/0SZKKAnfO6chQZu68XCCHUyQKCLMRLMdTro5XPOeSck9dQtFRlC1DI5YPQNoomZjUJzkvp1NuF8uoUUwycIMkSwve3ehzIT7ms8MnPQgullKqzPJfHH6YL918iIOXNjjzLcNoO6KTQfe6IT3Oa3Wfomhr8raibLvy9vmqK0MvWy4ApnVL8Yn3vMrtaZfjn5jBJOa3f+09FJ8ekT7fQc/hoc5tvl89QjMqeKcYceV3z6NWLc1dF5ZTZW7Rnw4dEFZlhY0jl8SsHbAWMCZqToHLAoGtVXVQCScQuoZxApt9WXDlQKERr0JARRYz1660udAO+Fl1kqJMsF9tsRKkIoBQyqEFJgI+QEVeD87NxgbVrJyycRYRTTTZnqZxZGn0LemoIh4XRLPSlXTjxm3ZaoJVlBnkvRSdG1Q3ZXghxUSu35ukbi9TJzIrhU1iVFk5KCu/xBSVK1u2FvLC+U7a+v9R3f5lBXHkPldWEGlMI4FIeaioLJg08uBRErwFCIplgEDCcBO1svdl9OaM0FIOoGWqYoMBPT2nsJpEGWa2PPE9tA5+JbokUxW51T6pdzMaUaFo13AstS4Zdk1PSOpB0a3LTAtlmNUpshpDiqGrZ8yMK2tOcSEqeQ2InF9i7hVXS3pG37R8qEpuHZzqxu4XBSkJ7VctDJqemhBhGNelpNQga2zTWt3lfAfBqdYkXEXUxmF5dqJKhqZJLxq7kBWrnQ1CaU/gbz0mCuu8H9O6XLlCk0gb2IientI3zfp49S8mytDTc1o6R2PZTo89cP3I8pskquQTq69z6ehhXt47w3A74bGNHb71WIczSR+A1XhMW+V84GdfpB3lPN64yaqu+P++/3+gp6f87Xf9KhfiAbGuSM+Pa6ireU/zsve9FKCVqNIF4NRlwwKVK5SHghWKnnKBJkOT+rLitsqdJ2FdVrtrWhQ25lx8RFrfDymzXdLO1HlgMu+JWVkXEBIpy0Y8wKC9V6SoVBOcJDsKvC8LG3twLcEl0oflHhc1SLxZrrAz6/LQ0m0ebV7ntelZ/tDqa7T1nHPJIW/Pt7i/dZt70n1aak6qKj7QfsuftyQlFzYmssb3WYGnKEME3ChWiJRlWGYcVy0KGzGpUuYmpl+0GJYNBnnGvIqpjGZaJMzLiKrSFHlMMYvdlFIq1CzyD0VsYk7PjYkhblSoRklZRChtadT/NpVC1WBba/cgp2xVrCxN6DWn9Bd8U3+8/WhsAtyiKPKgIvRCAjywkSRf8cqTQA2BGAJypBQKThYeYUmlLCRarZZXNGqtPYwS9Z14NhljaLVaHh6EAEfOPwQGi0odrTXD4dCnY7755pteHSkKh/DYUsa8v7/Pfffd569FwOFkMvHXK20wGAy8t6As4gSYCniSBWGo+JQF+CJIkusShYws1ARodDodX3Ys1yXXK55b4/GYXq/nYaIkT8v7e70et27dwhjDXXfdxYsvvsjjjz/ORz/6UQBef/11VldXeeONN3jrrbd8GICctxxLVDEPPvggZ86cYTAYcHx8TJIkXL161auSjo6OUEpx1113ce7cOTqdji+/XllZ4eWXX2Zzc5MbN24wnU752Mc+xtLSEnASiCILavHbvFPbSX8OoXMI++4EZBZB1qLHm9w7ec8ioFkEMIt9Ud77+8FCKQddX19nbW3Nl34uLy+jlFOHHh0dcePGjVNKzzRNefDBB1FK+X5prfOQPHv2rE8wF2/MUEU4Go089JGfh4o5WdiH6qbwfAUkiC9nWEa8CGNC9aaoohbHc/gnVPjJFt4zOO1LGAKvxXsj9yyEqOF9CftECPdCyLsI2uQaQ/gYAulFRWHYLuE1LwIkCXgJ+5XsM9xXqHhb9NFbhLBRFHnIFyrdFgGZvD8EpqHiVsZU+EcsE2RfEtgxHo/9XH/33Xd7AHx0dMTq6irNZtOX/z777LN86lOf8mXGh4eHXL16lfF47ENMnn76afI85/Lly1y+fJnr16+fUsbKseScw/L+sK0WAf7iGA7HZ9iP7/R3eE/COcFa6y07wvstn130aQ3B4eKYkPfJ93AURWxsbDCdTr2SbzabURQFzWaTLMv8+JI+JUFegH/4IFBevHPleK1Wi26369ui0+n444p6P7z3oVKwqiryPPffr6ESPFR0ipoynK+kZD9UfIvfZbvdPlXlIKDyf3MAERzM61xzHoMmdtDPRoqi7aCf1S4QQVUC6xwgUCU0BgJNnBKvzDS6sCQTp0Qqm67kOR04RaGoi9LceqVYNHcQMxlb4pnUb9awMIMy05jElbJGcwcKo6nxC4gy0+58KosuDFSWOpICkzjI1dwriebGp00DmEbkPd7iuUEXmii3TExKO54zvAeKdoKyMN2yTHYbJ+XMFhrHCUXH7a+1Z5hsxURzS95VLF0t0ZUrtTUJVJV2vl6lQefiNaccmGpUPqVal/hU1GGREeUuaCaqVVsmhuzQMLpLsdEYManTRM70BgzK7KSiVhu+eXSRaKZQlcFqxQc3r7Cmm+xVc54bXMBqGN2lGN2V0NyFL/7nP0E6MpyblDT2ZpikzWRdM19SlFlCVTONsuXuaZU50FO2LebCjH/vvZ/ns7tPcumfPsDXvvYYuoBv/fx/wp9+/U9z/fY5fvHRb/F3X/w0Zcfy9195P80xfPl3H+P2E12UgX/rj3+W//RX/xjZAZQd68oo5y4B20a1WnKWYzoZVebugQq+fwXWmkQ7JaqqlYOhsi2qFYBVsJC17m8F6LTCVhprFCaPHPwzNQgUlaCAR/DqQw8CBQrq4DiiqBH1o8GBxPo9NrKoUtHYj2kcJLT2DNlhSTycE43nbiyksVP/aUXVjCmziLKtydua+Ypy/V6DSRWy8h6fdYpiddPW1c8nqjNd1V8urQxiqRu2LsG6dDXGtpmceCgKRLTWQUVAFaUDiWWFnsz9PlRegLVEWsJfVL1vByxtpN3nAHStVtbaAcfIKUAFOrqHBu76yiLi0MRO+aYq+qbF2LiAkjzwOCyIPESrUN5vr6XnzifRxi7cxLqS1GEd6NCvwxzE76+tSoYmJVOlVyj2a+iyVy2xwYAZToXV1jNulCsALEUOMIlKUIBfT0/qROaIg6rj1XAzm9BS8zr9tvTAp6tnvoR0bBpOhaYNad33pDRZFGstNa/Lq5tehZipwqs0p0XiwKzCAejgu07UdbmNas+9BKM06DktXdA3mSvTVQWROSl5TjGsxGNe3d/i7eM1PrL1Ng+3b/PG9AwPNnf4UPNt/vkvvskvbH+dtir5q2f/Of/CxvdY02NUL+dsckSmKv6tM7/J0KQ1CIa74wEzqzkTOT/Jhq740IXLgIOuVR32UdXqwLae+1Jb6vLb0EtQlInu57G/Zs1Jurb0GVcqbgAHd3NEPViRqNKrMlNVMTPOt6+t8zqVu2RWl+QOq6b3rhQlX1udJHO7fZ0ke6e1l2Wmlb+n7rjO33Y1GvEXt3/bQ+K2zn2icRg2AvgxUJiYfu1JODcJ+2WHnfkS0yphkDc5zjO0slRGk8UFw7zB/nEHU2mUNlRFhJnEJ2pt5ZTWGLBJraiOrQs1qScXnRg35GMHDXVsieLTCyWlLMa4BzLFNIF57SU8SlCl8yqW8mRTP5FICsXR4RoHTUM2qi0TftCG7MfbD/kWliJPJpNTnoOLqohw0SQQLCw7nk6nHniFSgr5ZV/eK6oggWnLy8te8SbwUsAE4Bdkck6Lhu2yYJf9h0BIFh1xHLOzs0Oe56c+K/sMzdmVUj6VUvy6xE9QkpmbzSbWWr8AG41GzGazUyAWThacIeSEk4AUOefQqF7AYZZlHmDKz2UhuLa25q9hd3fXG/9LCWG/3/cqG601m5ubxHFMo9Hggx/8oE8LPTw8ZHd3ly9/+cvcc8897OzscPnyZa+YvHz5sleSCCyM45iLFy+ysbHBO++8w2Aw4Pz58/T7fQ+KX3vtNbrdLvfddx/NZpO9vT1u3LhBu93m1q1b3Lhxg9lsxnPPPecX2k8//TTj8ZgvfvGLPnhFDP6lDE9UlSGEChfnYX8JF+8h4JH2Dz8TqtRCCBH2KVGlyTHCv8PSU/l/qHaTa5SSPvn3ysoKy8vLbGxssLa2RpZlVFXFcDjk6OiIN954g93dXSToodFokGWZV+qmacp4PObo6Mgrr6RcUSDy7u6uL8GXc5b+t9h+0qbSNxcha9heAgdCOLc4RwB3VC0JIFgENbKFYKeqKn+tIeCSdpRzlvskQG/x/so4CoFFWG4agpEQOsln5PxDJWoIVeVBiwCXEA7dqc+EiuQQ1sr5hqqycAv7VdhWcBJ4EyqLw8TnxYctoS1E2N6LY2YRwst5SVmyHG82m/lS2OXlZb72ta9x8eJFrl27xuXLl/m1X/s1rwQ/ODhgMBjw2muvcf36dbTWHB4eYozh6OjIexH2ej2uX79OFEXcvHnzB5Shck4hyL0TbA37xZ36bPi5UJ16J0gtf4d9YfFh1mK/C48f7juEtNLH8jw/BUgB79srD7EA7zsMeLVemqZeqSe+guItKNcg75fvZ7EYkf4r32Hi4ykPCieTife5lfEfwjwZB6HCWJLT5YGbwECxJhBwGCpZ5frC/izzujzICxWN/2vbDz9AVNDcsyRj6zzrrCtlFkWQLqFKnWLIpG4BkUysU/nVn9elKyFOx67cOO9oGsduXVElwZOZFJIJJFPrU5TjsQsZScY1zEgURauGioX1qsVk4hSJKKdKEkgmKibxQlQWp76qTA2cnBotmlvS49yVYDYibKxQczBKUfRiTKxIxhXZoaFsKLaSATo1tG668mOTOIg623QKu2wf8iUoW648uuhA41gx7ymSEUw3FOnwxEvCRJAkFWXlSjpN6lKlAUhqSXdODcQcWK2s5tKb2/RkHpay51p5WS2VdJMZY5swOWe4J53TL06S8B7s7PLK4AymYZmvNqgaiu8fnuOPj/4IL33/bjpXIhozl3KdHVUOrmpF3tXk3YjJRoeiXd/7xJUnmwinJFEw/+CIv/LYb/Nf/uM/iqkXiDOTMKsSig50ryi6P3OLNd2kEZXkaxXf6d/tSsUL4I02T/6pl/j6776Lq//TRZJPHvPp9uv87VWDeltjGoabxQqqxCum3E23qLlLYVW1b6fvzh4MO0/JotQnvoGl9u3oVYe2fk3hQJ5VWOP+AC6NuQaQOnHw2c6jk4PVn6tPzJVKS3m0eB6GQBHca6UmPo5o9GMXTDQwNI4rkv7EQVDrIKNpxuQbbUyiKVsOFhYd5z9atqBsuXuDNnUSObXSDx8wEwasyGJb4DpJDLHGyh/5ZTXWzqvQCoCvvzDUiVIRwDYTdz/KyENGGynUPHH/Tuq2MgZVGqwOwGF9LylKVO4Uplq5kmlVGac2lS2OwFrK6TITk7hwD1v7BSq8skw81kRtl+jCVYNjXDpzDQ9daarzRRzajNxGXsmnpcwTxbguORaQI7Bnt3JPw0KF29imjGtwlNuI1aioVWUlRmkyXXil4Wo0cgERde1wZV3oS09PfKlqRsHlYt2DukwVDK1TAIpyTMpjZRvWCkHAwylJb27pOaVxD1rQJw9P4rnFxg6ySoiKGyaamdUk1iU4i2JOzq9C116AllGVsdkZcTh1pavnGn0OijYzk1BYzS9sf52enjA0KV2d83B6m77JOLfR50JygMaSqYozScW1MqGrCxIscxvRUBUzG7EUTznX6JOoitymzovRtnwZdSUTY31fxqaBQbMZubJyganglIOpqmjpeV2qXHmw7DweXT9K67LlrE7j9opBrN+nQXtQDK6c21jt2rGeciTpOML48mGBm+H9Kmp4ey1fYyMeOpisDEkdNAJOYXqzWGFsGoyqrD7viKOiVaeRa46LJv15k0mRMs4TJrMGVaWoish9h9ZzYZwVKIXzF8wjVGRIs5Kq0lTzCC2q5WaF0halLXENAk3lQrmqSqG1deXIpcaWClvKHAmUClNqqsg6KGjUiWK8riCIDOhCoeeqtgyx2KR+OCTff1qhSlfZUMQnDwF/vP1obaIcGgwGHrgApwz8BfgJ2BBlQ6h+CUFO6BGmtSbLslMKI3mPKEuMcaEn/X6f+XzuS6VCUCPpzeKbFYYTiNIEOLXgD8v/2u020+mU8Xj8Awt0+f9sNmM8HtPtdr2f18bGhi8JlTCY5eVlbt68SZZlPlREjOzFay7Pc1qtFpPJxCtvZHEkIEwWiAJSp9Opv1ZJeD537hwHBwd+8Sr7yLKMdrvNZDJhPB4znU7p9/usra35pE5ZlB4cHHjfr0uXLvHX/tpfYzab8f3vf98vNIuiYGdnh1deeeUUpNvb2zsF6u6//35GoxHz+ZxnnnnG34Pnn3+eOI65du2a9+e7desWf/gP/2G+/OUv8/jjj7O/v09VVRwdHXHz5k0eeughHn30UXZ3dxkMBnziE5/g61//Oh/60Id8n4uiiMFg4BewcBqWSJ9bBC+iiJG+cCdlYLiADxfx4T7luhdBzmJJY3he4fmEAG1pacmXJK+urrK0tESr1fLjbTgc8vrrr3sQXZYlrVaLpaUl7rrrLh/GI+c9n8/p9/t+7E6nUw8Ow4V7uLgP1UCheigEiGGbiNr09wKtYYJwCGfD8wzVYWG7L3omhhBR9hfCDmuth9Oh52DY9osqQ7nnIRwUFVT4UEPmqnDek3NfhJGLZcMhLAqhVgiow2sM/RDlfTInyvwmarNwWwSDodIwbCPZQjWlnIeoG0PV2Obmpld93wl4S39fbGt5XxzH9Pt90jTl8PCQZrPJaDTyc9tzzz3Hc889R1EUTCYTXnnlFbTW/MRP/ATHx8cMBgOfsr6o4JQ2Vkr5lGFR0i5ec3h+Av3D/rg4f4SqwVBReSegLPcvbBOZZ8L7FD4kC48XPgiTY4cQbREyAl6dHaqTBQZ2Oh0/H0jfEIsKOaaMu9ls5h/syVwuP5M/ch7GGK9slP4Q+hfK96VSyn93hyFN0ucWwarAfwGs4f0LqwHCcSXf8QLj5brk+0CO/wfdfiQAYpTXE3umXHmudaCqbNT+gglUTUXRtWR7LoXV6hokiiouOSlbjmfWgztdudRdSWC2yqUp69J9Lp65NGX5g4Jo7pSKUV5/+QmYUO49UW6dL1IJNnHlzOIP6NRSYLLY+cBZ670RlanDMSrr0oSzCD134CwZGqqsLkGsvfc24iFRbpmvKNBQLFdEU02VWZo7CpRivmZZegumW9Ql3zXcy5yHn6qcSlNXTnHhUmZtDYnwINXaGvpUNQBKLW+ON+m8HVPWll66wreJiRVxt6ATzfmdyf00DjRZVLiUVUDP4YvXH2EwzmB9zq2nM5KhIv2n55gdGC4MDcl4hkk0JlbMlyPm5yNspIgndX9oKXRuXShM213H5k9d5+p3z7H8BsyM4s3pJu0blvE5TZblvDXb5MbxMvkDU6DJnz//Pd4oZrz6+nlUpXjjnzzoysxzxfRCwd8898/4Gx80/O6NJ/jLD3+Vt4oVkkOnNiW2XMnXnfp1/oMyfw/IRPkHJ8rSSHmPTQfR6n+L/6CoA8Vvq6hVb7UyUMXGwUNwfa2GvM4fsT6IKAtFwSjnIOXRifPtwioYxqSHEY1DaO06WBjNc6Jp6dR+NbA3aUTZdgnieUczX9bky/VuG25hbSKwkXWqReMCiFSpKTsyXmrOp91n1Ah04dRo3m8RahWkreEeoBQKW5ccRyBhMuCAu/yiW6sbla2VobH2DxxMpGuIqH2ZsqqVlwITrU7cMYsKK34WxpxwztL92wWu4ACnUqiq8m3c01NmNqaympY63Tck0CHCMrapC5/QMw/X2ir38EbKTyVMJbcRiY3o26YvLZbS1rFp+ICStp6TMPEKwFARNjapV6X1ojH9qk1SK9WSqKQXTShqL0YJdtmIBy4kJfCmm+Gg5tA0aeu5K4M1CZV2IKwgol+16eqp/z+cJFJHtTpPrrWnJ4znKc2Cel4yJHlVp2qnNGrAmZHX4M9dZ6ZKZnUQTVX7QPqwGWXIiViPB3x8402eH5zHWM3tYokzjWMyXTC2KV09I6tBqMbSUhV7NuLDm+/QVgVzX0pr6OoT5V1DVRgUmap4qLVDW885qNquXWzG2DRoxXOv8HT7cCEhhY3p6ik54m059cpSOFFcAl5dOjAZY+VKxavAA1FUfaJO9MdRYRlwSlb7Qua1lyHU6lMNB1XHw2BjNQdVx3keWs3MJBgUwyoj0wWFifidwUUKEzGrYvJaZd6fNcmriHkRY6xiOk0x9UMPMw+AO6Dq+SdOKqLIEEUQxwVxXFFVmslBi6jtQqOiyKC0wVQRZRH5ByhmHrn5rH6mgVVU9QMRlWsKVQc/1Q8qlA+Kcg/aZK7Vc+cfHAJB+c727NW6ec2keH9bm4BJLaZhTjxkE0OjO6ecdkhGpxUiP95++DcBdbJwDhfP8ou8+BJK6awo20QlF6qJRJE4n89J05TZbOYXQeHiPgxPCcviBBrB6TLHRUN3WXxLOqOAOVFNyP5ksZMkiS+pE0VhWJJXliXNZpPZbEaz2WQ4HKK1Znt7m9XVVfb392m1WvR6Pb9IE9DZ7/fJssyDOlkoi/pPFBeLi2NZlMu1icJSFCjXrl1Da+2TnaV9AO8RORgMfElyt9v1CzgJeVldXfUlwcYYX174+uuvMxwOvRouz3MPgs+cOUOr1eKtt97CWsu9994LuEX45uamT2Tu9/tsbGz4tmu32xRF4UHmhQsX/ML97bff5pVXXuGuu+7ygS0f/ehHuXTpEu9973vZ398niiK2t7c9MBUFyng8PgUSFpWGi8pBOAEeIVSU98u2qFQKwWGoHgvHRAgd5PNh6amAAwGGGxsbrK6uepWhKHNGoxE7Ozu89tprTCYTD5jTNGVpaYkzZ874hHApEx+PxwwGA6/uWltbY2VlhW63S1EU3Lhxw1+rgBDx3wzbahGaLpZgwp3VZuEWwsHFcSltE0LZ8B6FbR2O0VC5uKgMC48XHkNgt7xX4E34oOJO8DMEyYtKOxmri96Q4XUJhBSldHjuovITtbC0T6gmkzlTylxDYCbHkfOTfYZKO/n8olpuEY5Jv5Q/cm6yiQesAPsQ1EtbCPgNgefiODw4OKDRaKC1PvUwKs9zJpPJqWOCg6D7+/veLiME0mH/kLYVZXr4vRH+WQSfoeo4VMJKm4Z9OxwvYf9a7K93GhuLJdQhtOx0Or5EOASFci/C/iO2IOF3m/gNCogLw7bG47FvVwGZ4ucYtkdVVYzHY//ASinno9jpdLDW0u12fZCS3PfV1VV/nTKGwocDYTtIPwzHpnxvy/mL+lAeish8KcBTKeVD2+S8Qw9IsS+RuUzuU5Zl3tv4D7L98APEWiVo6gRWq6HoKhpHJzAsmVjvdYd1oDGEh6qyRKV1IbalcWG0iYN9srZXxqnITAKF1k71UpdF66kre7aV8wZ0fokWkzifNmUsquAU7DSRwqaKvK2oGu54ydjSPDDOh41alVgrJaOZA3OqMNhEUzbrpL5hXif5KgdycMmOvWjCRjyg6CrWXy6xWjG9GdHerShamuwgp+jGTFc1nVsl8SyidbugSlPSsaF9U6NzUPXiyMSu+lXCZEwdMCNefdUoSA2uVYbvW77MN+96iO6lyKkgI8iXY+YritZtSzlM+MevPUV50OT8yyUvPnWWBzb3aqgEs6+u06hg6cjSOBZPSQd15sua0bkGqnT3m/rcoqnl6JkpD5+9zbV/ci/zFUXnQ3sM9pZovt7g7TfPsHxd8fRf/g6f//p7+a3vvJ/inPOu7LWmbKfHxFFF9nITXcHfv/J+JhdStu465OLyAZe//hDHFzW6gJ9933Oci1oYFPmS8w377NF7aO46WNVanXBj3vNeeoDz6zMOuEl/9V55dVk91vqfq8ieqAhFCRgkMKvEYEvlAaApIqKsBKuwusKW2gWpGOV8CQ0nALLeVGRdCEBioVLocUQ8UjQOFc09S/OwJBnlRCPnIWkSpwIyjYh8OcWkysHCniZfok4Jt5jUYutO4dLNFdEc4jqxPNuH5bdd/zWxYueDzsfSgXt3rVUaKDfhVLlfNHVlxihVw7r6WFpDEfiXGHlIEDloWMN8wFsQSNmxUEAbaZ+o7kSewZelUoD75ch/nsiPVZvcQVqkXLJ2ozMnUc5zcK9coq3dE33xjGurk3JOavBTWU1qqzrVVnklWVqrzpIgxrWwMQdV4pV4qapIVEVXzXxYhqjNziZOph8pw7BqMlMpiSp9kMmb+RnOxMcubTnp01MFe3U6b2FjNuIBQ5OR28TdFw1Dk3mYKSo2SWaemIb3OTRoElzZ7gmoSsm0g6POh7HwKc1tPScnQqk6uMladGkwjQiTaFq3FL9zfJGfWXueymp60YSdcrku6Y3rFGjj5mB74qfnSmvhar7OcdnkiaUbDCtX6nxctrg73Sel8gq/1WjGzEZkGHp6yiPNm/Tr9GitLGMDqTLkVlOgawgcMzMJD6buaW+rTgaurHZpwXVHc/eqJIuKOpF67sFtWnsLFkBG4fpEnagtny9s7NrS4mGxQXtA7INHlPEgcWaSOq3b7cvYBkM0oyrjG4MHGBYZ/bxJZZz/ZlFFTIqEooyorCwsXbqwMYqqdD6AJg9UzvUDjSiuKGcJOq1IkookKWk2c+Z5TJHHtHrul7WyjLAWrNFYIB+l7mGHBVVo9EwRzRWphbLfcfNkrcqOSzenhr+a6VJ5b1ex2VAIhHYPu8A9YIrmTrGvquC7LnVzddWw2OgECJIaVGyI08oBR20p8vrpdVyhrCKKDc1GTiMpqYymlRS0kpxzrWO+fOvxk4dEP95+ZDaBffJLfrPZ9GoDWXDIL+3yi3pVVbTbbe+7BHgVRlhaKAsv2XfoaSR+SUmSeD87AQfj8fgUwIQTWCNwLVxciVJBSotDjz7Zh9bag0VZVMrCZzgcAk7xISWRVVUxmUw4c+YMw+GQ8+fP0+l0vHpwbW3NX7OUf62srPiFztbWlgetcqxGo3EKjgiIFLWOwFYpB9/a2mJ9fZ1Go+GDatbX1/1C+vDwkNXVVfI8Zzgc8uCDD/pjXLt2jfl8zoULF2i325Rlyd7eHl/5yldO7W80GvmF6Xw+Z3t7m5//+Z9nOBzy5S9/me3tbc6fP8+tW7f4yle+wn333cfx8THPPPMMr7/+Oi+99BL7+/tkWebDXM6dO4e1lt3dXW7fvu39LH/2Z3+WN954g8FgwMWLF2m3217hOBwOUUrxyCOP8LnPfY48z72ycjAYnFKkhUquUBUWGv6DW+AL1AhVL3dSIMn7Q3VcmP4s7w+hl/h6CiAWz8KVlRWWlpa8Qk5SkHd2djg4OGA4HNLtdn2C9vb2th9jstDu9/vcvHnTlwtKf+l0Omxvb3tAqbVmNBqRpikXLlygKAq/GA8T0heBXVhiGwb4yLZYfnsn8Bj+fBHSLHryLQKc0IJgUeEYArXwfKQ8WKCYjHOBKHcCxuF5LypQF48bzoeL9zo8J5nDQnWqgMDQUkHmPHlgIYrjRcC5qGYN1aHSzxaVVqE6MwTZ4bXJ+d/JHzIcRxJksbW1dUp5Fs6/4bYIOeXeHB8f+2sWGC7znQBVaWOl1ClrgnALx55c03Q69YBc2kXuRXi+IewNoezieA5BlVxnWLYd9tHwO1DaTGCsnIfALoFz4ttnrT0FuORz0j9EwSchY2E/k/AyKReWc5ewkrCUXh5YyQM/gXLtdptOp3NK+SrjSKwtBGYKpAwhqrR3WE4dKkPl/1KaHMJ9UV6KL6O0f2hDIP0rfPgi/5c5SuaXUImYpinb29u89dZbjEajH+ijd9p++AEiYGPwVXD13GXqMuGocApErCsxSkaWeOpKlXXhFqMC6qLClcDqaUWhY+K5xVRQNGuvIqWIp6YOazHoubtRZccFLVQJPnxB4IOAxKoOT7Fa+QAVG7lgF13WadF5HQ7gOKAvt7SRA57KWKpmjM4N0bxyMDHWmDoUxUbKgT2gqsvdJluWaBqRjizdGyWHD7sJx+qE7Kginil0bsgOnadc1q9IRhXNPcN4u0HRVV4pV1WnibmJang3i5z/njkRypnU0tI5qpdjdZPJWUOjryiaGhtD2YLm1YTpBVfWlXcUvc6EcZE6cJS5RWF6bGntVkSzitH5lKLlyrnH512Kc+u2pcqh+sNHHF9dYvUFzZ959Dvs5V1uTe6hbCn+o4f/Z/718b+MSRr0XooZ3G/4W2e+xj8/+zD5oMP2h2+y/5vnWE5dCMB0nhKVML5QMbqxwn83+hB/9L6X+ejSG/zNzsMo487/X1r5XaY25xv/y2PoSvH/evtT3Li8TrvlgOSFlT77c5c+qwvjAZdNYmwjOQmAqBe/yuKhtvhcKm1P3lcHpbg3ONplAxWifG+bQtflekCdwgxgjUJnlVOO5g4CqnlEvKvJ9hWNvitFToYl8bh0QSfK+THaSFP0MqpMM1+OyLuKsqkoOlA1HfA0qXH9voaZqlDEM4gnriw+mlsXMlQY9/5IMV9NaPTdcXQJRYtapXuiGFQlPvWbGsibWKEnOWpeUC03HbwLywAqodnKQ0ZdVGCtUwimMZT1eyLlygstbh9KeUWl358Hu07liFaY2ndRVcark+WY8n431k6SoOPYeNXWRjzAWO1KO42D/sZqDkzbJzFnqmCvWvJly5LWG5YFuxALVwKd6dyDIpfebDycNDjodGgTrx6UfRzYDrmNWIqMT1Zu136Lcqxx/f5x4IsooK8gIrWVV95VdXK0qAkBZirxKsqDqkOmCl8yPcAp10RdKWXIeV3Gm6iStAalDrw7m4qi5dSuJoZmVLg0aGWY1ABtZpNasXcS6lLV+xOfxUwXrMdDNJbleMJKPHYPYKIBa9GYg6rNWjR2ZdnKUljLca0YXI1GdZl1WQfVGBIF47oUN1IVbeVKqCtlan/HzMO+yjqFZaoqBnV7Clxs6zk9TjwnJf16Ixr4+6mt8epC2Q6rDsOqyR6uTH1SNZiYlEGZuf5WNujnLTSWWRWTRhVlnfB+PM8YTDPKMvJBIKasQ5iMwuZ1EJPv66CyijgrKPMYLE4NmFbEyclipSq1g4uRiwEqi4j5JHGK6LkmmmpmqoGq6od2Wh7a1cOzqIONjJtbTXQCDcH920S1QDvmBPTFYGPr329j92DDWYnYk9T5uFZhG4VqVH7+jJOSJKlI4xJrFZXRdLI5raSgNBqtLOfafZqRU9NrZUlUxXI8paVzElXS0nPaOndK27qPX4xL3rf6IOr1JpwWXv54+yHfQr9C+aU9XAiHShlR7YXlieEv+KEyRVQ64isYlmxJ6rAsluT/soAIAytkkSbnJccQpeN0Oj21kA5Lt8KyJ1FjSameqFrkGIvlhuCSPMEtoG7cuEGj0fChJEmSsLe35xfG4tslkFEWh7JoCo8jCzQJQhHPR1kEy7UCPlQDnFellLdubm6SpilXrlxhNpsxm8245557aDQaHqzcvHnTX09RFHS7XZaXl1lbW2NtbY0rV65wfHzMe97zHh588EEPb2VRt7y8zFNPPcXh4aEHjq+//jp/5a/8FXZ2dnjiiSe8Fx/gYfDGxgZvvvkm9913H1/96ldRStHr9Th79qyHYufOnWNnZ4eHH36Yq1ev8uabb/r01b29PVZXV5lMJl7BFPa5ENCEkCNs61BZGKpnQpgVLpTlvSEcEdARqunSNPXJxt1u1ysM5diz2Yz9/X1u3LjhS/Pm87nvO+fPn/dATxbeUoYsQTHSlnKsM2fO+LAhUVaWZclwOKTRaHDz5k0ajQYXLlzwpaQhAAzBVNhWrVbLByNJn5G2kC0ECeG+Fts0tDS4k1I0VOnJ+AxfC6HXoppR9hWmmi8CMrneEJKFDwkWQediP5BxHgLp8DpDZVioJpT3CsgJQWYISGS+kn4bnuei6k2uJSzpXPQvDH1eF9V60h7h+clnFqFNs9n0qrCw5HYRCofjQI4ZlqCCs4lYLBUfjUb+2DL3y+eOj49PwdQQrob3Q9pe5vpFIN5ut31fCsGjtPWiklTaXsZYWAYcqjXlAZUAu0UwJ/OunG8Y6jSfz/3DE7n/MoeJGlDaRcqJBfxLe4p6UWwzwu8u8UGV75Tl5WX/HSewT44bhnXJz6Wt5bXw+1P6j5xP+B2/qKIM59wwkAXwCsNQab8IpkO7E7kua60PWJlOp17RKG1jjOHee+/lvvvu8xYQf5DtRwMg1vOuhIGUGcQTp+qL5g42xDNIBtb7HOrConOnSFHGUqWaohHXZUiqVg86AJi4tSyqsiQjl1JstaLKIgfDGtqXOEO9eNHaK8xsXaJqlVNWxTOXYFw1IJ5a7yWoQ7+kAEbknVoBkcWUzYhsmGO1C0apsrj2YKonSxOSDKjahqWrhuN7E8qGIl+GfMWQL2m6VxXjbUWZOWgXzWPngTiOyPqG4XnnjyhANJ+lZEFiMAqKtvtbldp5RI5xgCqGiUkxg4QqO1GHmsRdy/ATEz5w9xVuT7tcemObMotYSXOG8waSeG0iGJ0HZSLybkzZdD6L8zXF0595gWf/4RPMVhWmAT97z0v8gysf4fApwy9//4PovZS1mTvXrp6RJBWju3OSUcovfuorNFVKK8vpr9SDP4J7Ogfslx1mNzpkGfzffvJ/4t/9yp9gftjkL3zg6xgU81VVt4flv9n9ONuNY6qW5ad+4lk+/9X3kuSK8UM52TcTHlza5fXjLcrMqUNVUWLTxCnm4kAmo10IkFMd1oBLuQWwUjhAKCrDMAFU0pbrkmPVqNDaGfsrBTqqULEDvwqo8gh7lJLtRrR2nNo17ZdE09LBNeX6vI2dz+ZsuUnV0My7mnmvhoWZK9ETRaSJ63LACqKZ8wCL5g78xlPQlYPiVaoom85bs2ooyo5LvEbB9tci4qkhnjpfThfE48atS/y2LjBFaayyJ3CvKKEoKVvJKfWgHwcKVFl7GlrrgokE7AXCRhvVN8JaJLxFVIvhpgpXNGqTCEQdHCkf2OKDXJRy5c1aezWxG/+gtUErw5YecWwajOsy3yU9I8JyWHvXVVYzw4EhByAcDJvVSboutXiKRtclxiVpHRAysJn3yhuaJpV1aboSiCGBHbmN6EUTDxIlUAMg0XO6eubSc3Xu4VUvmjCoQ1EKIrR4M6rSl9pWKBLrfnEa2xPVYVvPfUkzuLJbUUgWNmJiGsz0zAGzKvLvL2xEbhpE2rq+bE/uz7znFNdlE840jjFWY5Qmt+56NDWwxfke9quWTzeWkBaAi+kuDzdu+fLglBN1X6QMfdN0ikgLhdW+JHozGvrS5hRDgWZoFKu65ErZIlGGrioZK8PMNJih6Fdtr9DMVcT1fO1kvkbR0jlzk3BctTwcbOk5EQ68OhVpynHZ4qBoszvvEivDpEwYFw0GeYN54e5XUUXMZwlVqbGVUyObUhPfTimXq9q+oFYm1xBNzery/1ZVK/8UNEvsXKMK7YBbYmv1XYnWbp5K2nOqSpPPHBgsBylqrp0P8FzRmNR+vA1375KCOjhMObW7MP+q/s6R+VG5+bnKnGdq2aK2QJB5CKy22Eb9BKtOR04yB/105JKZyzwiTitajYIsKUmjinaSk0QVWVTQinN6yZRm5PrrajxmKzl2021dUi79WCD0mfiYtPaXBFhScwbWhQXJWGirHK0MG3rCsWk46wKKk+/4H28/UlvoUxgqrRZTZ6XEWaCHLDZlUSULEFGUiJpC1IyAV1fJJouJsAxRSpYWQWRYlgx49VIIFcKFpLxHFkKyeAsVhmI2H6ouwgXOeDz2fn6Hh4dordnb28MYl1osasNOp8PR0RHNZpMocoEtAo8EegockusIgUNYWh0qL27fvk2n0/HqlKIo6PV6PPXUUyRJwuuvv84DDzxAnufs7+9zdHTkw0WMMayvr/sF8Pr6OpcuXWI0GvHhD3+YdrvN8fExxhiWl5fRWnvPwG9+85tcuHCBy5cv85nPfIbBYMDNmzdptVo8+eSTtNttvvWtb/HMM8/4a+h2uzQaDR566CFmsxnvf//7eeCBB7h9+zbGuECE4+Njvzi9ceMGZ8+eZXl5mZdeeonHH3+cK1eusL+/zwMPPODh2Hw+pygKf99CKCWQNoQx0g8lkGcx0EDeEy6IF8tlBSzI1m63WV9f9+XsrVbLg+7xeMyVK1e4deuW749pmtJsNun1eqdKEOfzOaPRiNFoxPHxsYcPshDf2Nig1+t5MDGdTtnb26PX69HtdllbW/PgQOBIq9WiKAquX7+Ota6k/cyZM6cW+taepLBKe4lqVsaatMsiPAwB1mLpZthm4Wt3UiSG8FBgRgjA5D3he+8EQGX+CX8eKs9CQCfzW6jC+70AaAhHwmOHpaZhuWqoUAsBYqggCxWzouYO1XHAKZAibRaWxIewbNG/Uj4fwnJRNYftGELQ8L6JEln6/Gg08mArBLjh/Qlfk/tsreXo6OiUklUeoizew/A8BG4tloDL90UIpEPFpfRj2aeMRTl+2H5iv6GUotls0mw2vdI8TVP/PRWen5yXXIeM3X6/z2QyIYoiP1cK1JR+F8JgUaXK3B2WY8v+W60WzWbTK5Zl3gkffoRwN/RbPD4+9oEiMiakDWWeCMNTBNaFvorSltJW0p7hg5dQ+S9/h2NPttCj0xjjVeRyvQISJfxE3j8ej3+gkkH6u3wuBNfyUE08Ff+g248EQCybijJz0MlGrvSoSp2yT5cO0lWi8jIO5JkYql7slQnxxKBK51uoZxU21pStiCrTRMYSzWovvZZTbuVdTVSXMUNd8tyA+bL2ir0or/3YIkUyNlSpK5+OtFsMJWNbK2ncPkQNibWowtZhEg40WuUUN8mgcJ+JlQtusBZLXSpdK79QbnG+V3WxiaVqaMomTM5AsVKhSsV8oyIdRsxXLbpSJIM6bbqC6aaiMXDKTXeO7npMIYv3unS7cArA6SMletcN0qINaaUgMkyqBjQM8zXD6ouKldem7Dzd4qmffoXnd87x3S++i7WXKloPRozutlxsTNgddpxCJYPqkTHFMKXaSZi8b4K+0iSvfRa/8ruPsja0HP6hGemlJp/95Y9iHyj59Z/6z/nj/+D/SDxWTNch71muFWvM57FXzlxs7PKFaYvJ86s0jOL2b50jmcGHOm/x6wdPsvKKIv+pPn+me8B/dr7PwX6Xu+OYv77zEVo7LqF6ugG/9cpDZFdSnvmjz/Efn/kGvx69F+4Z04wN2IQn29d4/uA8ylp0XgbEyvnumcj1Wa+kEQhWOdVq2ba1pyFe1UdsTmSuRqEii4rqX1As6KiuJQXKPEbtpzRva1q3Le1bBclghi5c/wZc/2rG5CspeTci7yiKrgPNUrJnY3uqdBhl0aUiGjufxmjqwG7jyNK+NadYihncFbl9NJVb9Hcqd1qN6qQse65JjiOUsURzQ3psGZ93gRhewVsokkmFiZ3XpZQJK4MrB0/iWk1UByd51Z/4HNY8XdieiKKCX4DlPU4B6lTIytRlzgqfcq6K2l5A1JCyNRb2aUHVkNNGGl0aKA02jTwzGNbJuOJVlxP5lF2nzGtwJnLlwwZNYWN6euKDP/qmRWZdia8oGR18mnvl30HVYVBlDMg4EzsQUljn/5fbiLYumZgGLT2nF0Xec29omj4xuRc5mb944vWrFhvxwEEw61TOYbiHnH+iKobGefylNUyUFN+ZcR5/mS7oVy32yiU2YqdM6VcttDL0aq/GTDnF1sBmjG3qylvre6VKQ9VQ/v62dF0OKCEuqkAr57uIcqgwUsaHzYADdv2qVYeNGE+tcuVeG5jM+UWqqoZCmq4uyI37e2IsB6ZFV89o1yrEPdNkaHIyVdI3GTNVeI/Cg6rj28gAh2WHRh1acnW+ytzEXsVW2Iir81V2Z10MituTLuM8ZTDOKIsYUypsVSsCjTp5glUnCttZBKnxgFA3KqKkIooN5oKBWeSSh23wWatcCb63OnD92k5jr5SORhHKP9BoYGt+bo0D/g0ZGvX3hliHSKCYndWKhNof0GpL2XZQ0I0ZMJlBNd0vzlFiiJOKNCmJlKWoItJYTM/d/xtxxWrT9dVmXLDWGLOdHdONZsxMwnoy9H1uIx7UMNiBdbEAiGqF6Ni67zJJjk5qmOxKvLX3jOxFc39f03pi6dtmnY7u9iFjNsJ4n0xR5JriZMH14+1HZ5NFT5ZlHv4tlq1JiXNYfitgRBYs4QIlVJAUReFTHWWR02w2fdmfKCDFK0k+H5rThwt5OFE8yUJO3itAT/YhCyk4KbMLFXaLCqQQMJRlyWQyoaoqVldX/efFd0r8A2XhLwvEoijY2Njg8PDwlLplPB6fUqtI6eDS0pJvJyl9A1eilee5VxGeP3/eqwWfe+457r//fj75yU8yGAx46aWX6PV63Lhxg9XVVdI05ZOf/CRnzpzh9u3bXL16lZs3b7K2tsb58+e5ceMG99xzD1EU0W63eeeddzg+PuYnfuIn6Ha7vPPOOzz33HNsbGwAzt9sNpuxvLzsgeYTTzzBtWvXeO211xiPxzzyyCMsLS3x6KOP8sUvfpGf/Mmf5PXXX+eee+7h2rVrRFFEt9vl4OCApaUlbt++zXe/+12azSZPP/00g8GAJ554wietnj9/nuXl5VMQQhaSUg4upc1hOa1As16vx7Vr1/w9DO+vwAcBNKKSlUV0p9PxwHBtbc2DiX6/z97eHkdHRxwcHFAUBevr6zSbTba3t336tvg3SmmoBN1IyraoNpeXl/0iWOCxKExbrRbr6+v+fKWPTqdTdnd3SZLEK1MFTgmACPtdmqanSiDD/i7gIY5jb0ewWKa4WMq7CO1CyCibPDhYVBHK+F0sWZUtBHRhmacAIZkPwnG7uP+wXHQRdNzJV1HA4eLcBSfBGyEgXVRHLl5/CAgXjxmqCMNzCktIw3sksCmc90KPPZlLwzYVdWr4vhCmhiowgUwhXA4/E44r2cI2C+9b+GAoPKaAsLDtwr4Uqr9ljhVIJdch4V0CAUU5lySJD6oSIC7ANCzfD2GYfE5gu4BOac/JZMLx8bEHhFJ+LmnG8hAqyzL/XRICRLn2JEm8t6DMNeIz2+v1/DmG41/u42w2O9X3w/sq+xMY2ul0TlkSyHuknax1Ng5yLLnPocI2hP/SZqKMFPgn7SRgT34uqmiBfRLUEo7N8AFOCEXl3ofntDjHhO9d7E/hMf4g2w8/QLS151rsfPOieV32NLe+1NTWCillXJKySzWGeOaUCdHUkAzdJGliV2poY4VJnceZQAvAwbmmIl9SNPru/yZWVKlTkUkAi5SjKgvzJeXfJ6Wb8aQuOY6pVV91UImoa2plBcqFgURTiCeFC2JpJycebjU4VLYur6znq5lJuJavQWpoHBmieUR2oKiaiqVLiumZupwlr72e6nNIRpb5mluUR/O6zRpQdK3jI9apykyt6jJxXdYGTC4WNK8kZEeG0Vjz9998H+lOQr5eUnQSiqWE2ZrluVvn6P7PXZQ13PqI4sn3vcm1wQr3t/d44eZZmhZmd+f8yof+G/7kF/51yjY0n2+RfWyfNC4ZfOkM8SRifA6StKRsWtrX4Wff/z0eTZsUvYrHfuIdXvrm/ZRdw065jKki1r4TU3Tgs/vvpp83mZ8viLKS5d9qYlLF2eSIXi03/Zl7XmJuCw6P2ui9lK/P2nzu2afYVNThGxAdxlSZ5a9v/SaJaqJzRbXTolTQjOHx7BqF+Qg2UujZCUBUZXXiuadOlDaivPH9ulNBETkVUFy/EFmoQMUW3SzQ2mCsoprGRP0YPdA0jyA7NGRHFclo7gJcjIOWVTMmX00p2hFlpphsKp/QXXSlvM8FnEgZsi8lnCmiuiQ5ntYeosqNvSpVjM8qik7K8jslk7Mazk+pCo0tNGoS0TiMSI9j4kmdmj63gEs910VEduRgRtlUHpi6kkXjvQZdO1l3Pc0Gajqnca1Ptdr2AUTU5a3il+mSzmvoXisSZSz7gCOt6nF8InuSMnKoIWMWBUDxBFaqyqki/TzhP+PGsS4qn7C+3HSgaVgHlbT0HEyDwkaMTaMuQXbhFqIMdGnMtWy/LmXNVOGhXlfPmNiGgxxV4gHkxDQ8ANyrlgBcEEcN0AobE9Xgz/niKYpaRTg2DTbjARXKKxf7VYtu5MaHCydxAHNVj2ir3KkVcb8ktFWBUZp2lHsl1sQ2aKk5UeRSeTNd0NMTZnHi05YrFEPT5Ga5QlfPQDvY1q9aLEUzV4ZfAsa1eZWeAKoKdaIgVJUryVaFV3ICrOmxu746nEauXWCjA6yWiWm4z+uZK8e2J5D0RtWpU50NOdorKiM95aBq+7bOVMHNYoXjqulAk0l4fbDFcZ6xnM4Ylyn7ozatRk5exgwnDZqNAqUsszyhLLXvZlpbqjIiTirKMqLKNTox9QCp+1udMoxVKGWd24FR2MKp8szcpQwzj1CVIh7rk2wmyfepdxnNXN+X7xNR+6kaBEL9vVFDRVtbhtioLiGOnSpQAkVMYpGyYRtZaBh0WpFmNSRUliSuKGs/xTO9IevNEcYqzrf6bKZD7kr3SVXlS+jl3o1Ng66e0osm/l529cwH+kgi9cBkLOmZvzd926p9JWuYbLT3JJWxEb4e1Q3Sq70lxatUQo9kE5WwJF6PbeogpHL9HKtds0V/8F/Ufrz98GwhSJEFuCzW5Bd0WUyFCgRZVBdFQavV8os6SRIWU3gJBgnLk0RBJYswMVIXyCiLsnDBHUJJWSTJOQnMFMVaeG3NZhNrrV+oCcyUhXu4yFkssRLoJ6WezWaTjY0Nr9YcDoesrKygtQsOkDbs9XpekWOt9aV9i6WPcq3iwQiwtbXFaDRid3fX+6bde++9LC8vY61lY2ODra0t4jhmf3+fe++9lyzLuH79Om+++SYPP/wwvV6P97znPbz88sucOXOGl156ifX1dd797nfTbDZ57bXXPJg8Pj5mbW2Nqqo4d+4c169fB/BKQqWcV9nm5iaXL19mMplw6dIlrLUcHBzwoQ99iM9//vO+XK3dbnPhwgXKsvSlzzs7O5w9e5Z+v89LL73kSwUvXbrEpz/9aY6Pj30gi7TF9vY2zWbTK+QEKks/CYFzeL/lPRJmI2Bj0R9Ra83KyooPIVlfX6fX69HpdPy++/0+r776qgcF0neWl5d9u1trvXKr3+8zHA49ZBaY0W63WVlZ8eoigdNhAEcURaysrNBut3177+zs+JCgNE29wnZ5eZmDgwOm0ymz2Ywsyzh//jzgghsAbzcQKkvl+qWdJJBFjiFbqMa7k2IvbMcQMoawKQQrizBtUckXwl9pixBmiAJQ1HJ3Uj2GqrRwv2Epurw37CsSdBKq8+R6wxJ5+Tuc/+T8BO6E8DMsxw0BYOgNJ/0yTVM6nY5Xo3U6HfI85+rVqwwGA9/3JWBHxoVAInkQIw9rpD/KvKmUYnt7mwcffPDUQyJRpoVzUgiGF/8fquIW4VBYihz+PFRHht6I8lAoTVM/d4gHrHxXCAiXz4XXKnP+fD5naWmJsiwZjUan1PHS1wRoCeCSoK1Fj035Dul0OvR6Pf95UXG32+1T/Ui+z0IlZDjHCwQM5yGZR8Lv3XCMhe3caDRIksS3hXxPLp7znSwawp/Jd1V47wUOhv19cXwKKBRAGAa2yDiVOSwsuw/HZPgAZFEtGJZPh3PN4hyyqHz9/3f74QeICpYuO0+10TlNlUE8rmFd7NSHugr+nhh0YWoFi6VsRRRLEVXDmUUnI+f9ZuvyRxJVqwtdia7VTmlYNdzfLtyjoui4L1NdOKWiid05mFjUke4GJlMHOaqGWwi7cBHl1X6qtNhEO7WTsdi0LnWu1YqoGoKk9cqyrjCzqi6TrSJXnq0sN/IVuqtjhhd6rL80Y7yd0tyD7vUZ0XdL8uXU+bKNS8pmRNWMSIYlnZuaZFhQtFsULUXj2NDoa8r1EwAaFQ4emkSh5hHZnma8WlK2LP37NTq3zKYpH/vUS9wcL/N2/wKNfszSuw74393zHH9/69O0ntnlP7jvK/SiMc8uX2Q1HlMWMaq0JC2n2lGVYnx3RfudiH//4c9yOd/gv0x/hnM//Q4vv3meprbMuxVlK+ZdrZv8rf2HyHZjvn/lPEvXFcOn5+wXXaqpC3L50L/8HP/8xUfJrqU885nvc29zn3/yz/8QYNnQE8ZVikkU39q/l/8snmJKTXJhzL/53J+i83ZM/yFL5wrYCzOqSUwVG7ailH882qRxqKiakB479eO7korxPHXFbdZCWaHkqVvqFu/i2yVlwFjr3qNAZS7m08Y4pVCtMrK5Ro1i4mNNc9fBwuZeQTyeOrWbdYCrymKKpZhqLWW+pCk6rgy56Finao0cEdZlXbpcL2h1oVBzRTRTxBNoHLpkclvDzqKjmK27kmTTsFQNi2mV6FbJeBrT2tM0DhS236K5a8mOXGq5jSrKTFG0FNMN7fwOm5ZiyRCPIlo7LgW9bOItBDBuHCopP8aBOV06IEorwyYRJokwbX0K3nmVoHHtqnNRL1pUZepk5EAZEvge2nBi1soDK2qvUYHBPkRFu2OaRHslpA9eqUudrVasN0cUtVpQynNdUm5FpAxdPeOwTrvVypACWV1CLD6IAg5bau4VdAI0chsxtM36tB1M6ZsWXe2CTw6rDvck+/RtzKDKnGLVNIiwTn1lGi7xOBrT1dNT6b2RMrT13KkT0eyUy9wulpmZhLcm61wdrnI4aTI8bBM1Kv7G+z7LufiobgLNQdnhv7r5CY7nGf1Rk2IeY0oHwpqtnFYjp53mbLcGPLl0nSwtvB/il47exTe+/S7SI+37KcYpdXV90411Sk3UySJIzl/gX4Xy7YHCexBK2ffEOjjV1VNyG7FbdX0p9GHt27hTLjuQa2IOijYv9M9RVBH3Lh1we9plWiaM5g2meYK1zkLAVJpiFhM1KrQ2XC1WHcwrNMeNCmsUUaNikMdEcUUUWaLIUlWKKLKkSUkZGS9iNqXGlhpbKtTUzZOqUCeKZgNxeQIBVeUeFvkQkfpnWgKZrPsu0vXvf7pwPytb7v0C9KuGpaz9ewUM2thCBLZRoWL3oEnVoSVRYtCRodUoWGlN6aRzYmXYag442zhmO+2TqZxeNCFTLjxG7nkvmjifTSJmJiWqaaYoV5MaJmaqIK3HT1IH3oThMuJ3GYI+CeqRviGBOjJuZsb1d43x6eCJchBRfqbr5KDCxrRqi4EI4ywCVE7ftDyobuu8LkO3zieTk4cTP95+dDalFCsrK9xzzz2kacru7i43btzwCwNRNYSLZFkQhAErsmiTIInJZOIXFXIcOFHJiGqw3W6fgiqyWJHFR57nHkQCp/wOZVEpgFNM3OXYIbyTRc5iGWK4EJbFjyxaxBvs0qVLbGxscOnSJba2tgC4desW/X7/1H6qyoWupGnKzs6OD7OQtpSkZFl4KqW8+ftgMDilBlpZWQFgc3PT+9vt7+/7klOBXPfeey9f/OIXeeGFF/y5Scler9ej2WwynU65//77WVtbY3t7G2utD2f5/ve/7699bW2Nfr+P1torGW/duuXDWi5evMhdd93Fo48+yssvv4wxhvPnz/Pkk0/y2muveTBQVS4NdDQaMRwOefHFF5nP51y6dIlvf/vbbG1tkSQJa2trvPrqq6ytrTEajdjY2OC5557j7bff5vbt2zz00EM+1EcAWLjQFogjUFvAorxP1EJSvgjQ6/VYX19nbW2N5eVlVlZWvPpuOByyu7vLyy+/7MNper2eVwuGfVh8GW/fvu1TQAWISIlkt9v16jDAqxdXVlbodDqsra15KCJ9+OjoiHa7zWQyIcsyHnroIQ8l33nnHe8vmWUZW1tbKKV8n46iiIODA46Pjz20lHEgY3URrEh4T7joDxVAIYQQwC7tu1iyHMJ32U/oSxiCjXDcyLksAr7wNRmXon4VxXOoipIthHWhQknmrvB1eU8YyAL4BxfhPqREVhRyIdRbhG7yeXldykwFsq2urvLkk09y8eJFX+4u6mNpt/l8zte//nW+9rWv+c8++eSTfPzjH2dzc9O/N4SuMtdcuXKF/x97fx5kWXbfd2Kfc+729twqK2vLWnrfgEYD3U0QIEBQAEFTokSJI2rkUYwnrKHsGDsUo5iJCcsO2RMO/2VHyJrwjC0NJXlCMdqszSGKFEhiIwlhazQaQO+1dddeued7+fa7nOM/zv2ddzLR8uBftXAjKrIy87137z3bzd/3fJff+73f49q1ayilWFpa4rOf/Szr6+u8+eabPtxJAKww/CJkfcGChRky6cLXhqCpeAVKG7RaLc/KDRlvcgirT84lHn8C7AqjezweMx6PkSCo4XB4jFUp1yDAv7DB5Tki66YEF8k1hsBUeG8SoiOHMDclcErGoIw9kfzKNctaJOMlDNuS65WNr9Bq4CQLVuTVJ9mK8k82uYSpKQCy2B9IAIvcj6wBsgkXSpVDoC/cRAvPdzJsScZFCN6fZOjK2Azl0icB+fC1ITtcrjdcC8I1R97zkx7/zgOIVkP74Zy4PyPKe/QfjYgnDvCQI57VTKkIQt8hZS0mVTWL0XmzzVcisr6TRc57kZNCVzXjbO5SmKM53h+xbDj9nPgcSrFltUionYTYzByIaRLH1rLa+cHZCJKhdcxJn3Cs0QYHCJbWyzmB0N7QARXUxaC12MiFqcQTRapKBmWTdpYz2lCYJGOyocg/MmH6vRa6cHLjqgGth441ONyMWLqlmHc1WatOTk7ceYqOpbUyZb6aEF23zJYcWKsLC+2SfFnTuJXSe99y+CdGfObye7y+d45vffU51t6wxE8oDp+y/PLZ9/mZ1k3+7ss/y8+sPfSAxJVsh/PJIaZwAMza8oi7xRrRWPP0J9/n+u4j/InWjN8sKsqmZaMx5K1KMT1q0LsWU7ThXzx8gWtvX8CeLmm93aS5a8iWR67wLDSHH6347y98m8euP0XZtPxvT3+NV2ZXXJ9kirY27M/bYODO1ir//f3P8p+//FUA/oe/88cZvzhlqTeG22tc2tjn/XfPkuzEPKxy/tubv0Dvs9toZZn81hmKDvzle7/I9NoyqwPrZICdJhSlA7CMJRtANKuN/rUDCnXpXluljqFiBwnRRJMO6kTkPUM6KElGc9S8cv2eRA4sXErdGO5F5EuKvOfAQitjMjZejq4s6FyRjBaBPslIuZCTqZs/qgZoqsyB4YPHoFgy2MygsoooqcHQWYyaRMS3G7QeKHRRcu4bY6pmzPaLGUVb5MyWsm0c0NCs0LFBaxc6kacN5vMYVdVjTrk2cYziOuyI+j6UQhsDWmMShUnj2lpA+TRrNJ4dFbIZHbB3YhHx/qH4hOYfAyAV/nsHZhof3CLzDqUW8tp54RiRSeQsE9IIGyk6ifNIcyBUzGay73z7rGO5haEl/coxpCpU7bXnWHLDOvVXWIpjm3E6GqKVIa9azIwLDjF1Uq8ErQDkNqZvWuyXHZajCVfnZ3l3dJZuMmNaJZQm4mxjwB3WiJRhJR7zSLrj2JAKEirGNuOvfvs/oP2WC3+J5hBPXNtkFhoG5isp/RdabCb7aGvo6YIf5Je595WLdO8YVit8KIZr1xRdQmUsN1bO8bn/3VWWowkVirHJuDNcpXdTu/UwfL6pRR8Lo1AAoZl1CcMaQ4OCick4Mi5IZL/sMKmZn8OqQaQMe0WHO+MVGlHJqMw4mLaYFjF5GdNIXJ9URjGdp2jtxv58nrh0cwW3751y0uFSB2FH+LR0cB6YaVphYoNSFmsV81mCzWu5MVCMUwptXTgVYMaaqnCAvrCUW1WNk+oFA9M/F2qbB88eVNRrnHs+mTol3KUT48NGTKP+YyK2kJjFPaQGnbgUZWM01iiStEQpUHX68Hp3hFaWtcaYRlTQjAoeae5xKh7Sjab09MyD2IkqaasCrSwHtWS9V4PhBRFFFdWy/YihcezNVh0eJKB4UidWn4mnXnofKUNbuR1gsQeQcy7rSZ3IndTzwAGAkTJ+volcvap9LyWhW84nvoYFkQcuhbE4s4kP+alwQHWiShIg0bW3aP0ed5/VYvz+9PjQHEoper2eZ7OdPn3aF0dhMnPIoAiLCAlHEUaFJFAKWCbvkeRUAUukQJGfy2vCIl6uLww/mc/nxxgX8rNQghh6b0nxFsoPQ/lWCFaEEjKttfcFvHXrFqdPnwZgfX2dTqfDxsYGOzs7npkWRRGDwcAzVY6OjlheXvYAqNaa1dXVY75+jUaDwWDgzxlFEevr6zSbTc6cOcPW1habm5v8/M//PJPJhOvXr1NVFQ8ePODq1assLy/T7/f5zne+w2g0QimXYCwFodaaJ554gh/+8IfeO0/a6OHDhzSbTc9CsdYFFGxvb3N4eOj/L21njGF3d5dPfvKTWGt5+eWXuXnzJv1+3/e9FIkC+L355pvcuXPH+y1evXqVz372s0wmEx80srKywng8ZmNjw6fAPvbYY3zpS1/izp07XoYsaaQy7qqq8snYFy9epCxLDg8PGY1G/h4FYPrkJz/J2tqaN9ufz+ccHBywu7vrGV6j0YhWq8XKygqnT59mc3PTA2wi3xafriiKPOAjSaAiMxU2l4zHo6MjqqryYOFTTz2FyCSlgM/znFarxdraGufPn+fo6AhrLXfv3qXRaHiG1dNPP+2DVHZ3dxmNRp6BCI6xubOzw2Aw8CwqYQ2Nx+Mfm/eh5FLGgDCSThblJ8G/8DUyF0+yecMjnG8hmy1kJZ0EKkIwRSSMwlCWzwpDL4Bj92mtPcYYDpmMu7u7x4KLZLOk1Wp5MFH6OU1TiqLg6OjoGJPNGOcd+vnPf54rV674dUfk6wKo7e3t8e1vf5s7d+74dnrkkUd48cUXPfN2f3/fy90ldORzn/sczzzzDK+++qpf45588klGoxHf/e53/TwS4EyA7kajwac+9Slefvllrl27hjGGRqPB2toar7zyCq+99hqrq6u+P6VPT4aMhKChBCgJiCWMyRAsDNdb+b88N0KAWvpf1gBrLcPh0F+ntKNI6mV8hExzYSzKs0FAptACImR/imWGPGMEZAvHp4zHcJNJxoVs7ggjUDZwZPyJJDp8LgpwLAnqslkQ3kc4jsPnocwfY1xImDyDJZVeAMDJZOKtCUKw/iRAF0rmw0PeEz6Xw/kbznc4HgQk7/kgZu8HAczhPYXzXKmFTF+uJWSyhtcW2imc3CD4SY5/5wFEwLGUZgXJyEl147ljG5rIMZn0yHllOdaFYxDF45IqczJOVVtfFW3lzNoTl8AcFe6fL85qtp9S+OATkyhKapDDJ7k670UXJuJYIfHMARdVqpgvKy8DqxKII8fmam8bFxZxwmNNCu3wcICl8hJrq2rAsoJkYrg9P8WwaJBFFdEtl7w7W4tYWx5xeKpJNHNgS5VayrZiXmnKFkxXNCaBohMRT51hPQcOYJpNUrSF0ZmIqHDBGCYDjmKK9YLsakq+pHjm7DZf++EznPuqYiW17D+rWH1xm8k85ZHmLgCNRkE7nlNZx5w6Ew+4HA+wlaJqwGa3z9hktLYUb796mWwOE5Pzf3vtl4iAb/+rjxJ3LeWq5ej5OWfO9Lnz1Ut0ZvAX/+K/5m/9sz+OMpbLSwccFi2ILH/ihdcBWF0e0deGZ9OYv/DaL9A0i/adlokDkQ5TbNPwmdY1rucbYOAvfuRb/PPbz1NliruvnCdWYK5M+cvv/Tn23j7Fr/zCq/zuzafpFJbGruL1/+E52pmgG7VHUOUYgsnDI85uDVFFCcZQrXXZ/pkl1+9135s84sJXoflwgs5L389VO6HoJFSnGhQdTdF04S5ly4WclM3aGzMYN6pyadfpkWL1auW8Ltcc408YSY5hBPmyYtJyHoxFryJaKkiuNilbhnRjwny/SfwgIztQZAeWbOgYgia2lA0YbsZMTkUoA9MNg2kYdLdwqabapULraDGgo/r/JpGAFlt7mLowJF0aZy1g8cnMgAPlYpcQbRInCQ+DF7z/Wv1/9zvlpcWL1wVgTy1RtnoBCiKLd7CuHrM1OHFE05Iod5JpG+uFhFq74IwIw8X4gJvFaYamwXvzDR7mS3yu+04NIsY1c7DJf/fe59g77JJmhfP/s/DFR6/yC0vv0I7qlFc1Z6tc4vXJJnemqzQjBzTeGq5RWk0WlZxvDfjTa9/not5nZpMauE/5m3/wBVbe0G4jpB4vJlkw0IoO/Mqf+xY/27lBv2p5FpgdxyRHAkS59dCIDUNl0bliVDVIqehbV2gkqqJqWmarzj9WVbUcVtUy+cL5sSoDh2Wbi8k+Y5M5hleSs9+EaK5q6fuJQ9cybLMIPqmsZqtYYq/sspN36RdN9mYdRnnGcJZhrEIry7yIsVZhLeST1HV3oRcBI8oysjVjLDFOAlwnDwMoXYfjNCxxUhHHFXle/zFjlAPdLNhSUwwyCuvmop4pdKGIS3dfqkrcs0gSg2swOhlSe3LiA6nQ4iFYy4IV2EQYhtZ/jk1ryXBqPLCu04qsUVCWmkbDBYq0koIkciy+TjKnFefO789EnMrGLMcTEl2xFE3pRDPvJwh4tuDQNDxYLaCcMP/6psWsStzPbcq49hV0HpclfQHFsZ6NKKzXrp66BPFo4oNv1qIRVY2+NdSU09GIraoHNYBY2IiunpKoioYqnR+nTR2DsGb2Cpg4Nhk9PaNXB/g4gDNnbFPve6gDE1jxO5zUgGBSe1ge1dcrzEhhObZrX1JTe3P2KEmo/St/enzoDvlDfjQa+SRHSQYWKVnI3jHGeFmpBHYI80KKviRJfLEWSpNg4YsoQEtZljQajWNyqrDYkEJdGB8iG9Xaha2kaXqMYXESrBDAM7yGk8WHfJVCV2SheZ770Axw4JOk1r733nvs7e15Q/fZbOYLydFoRKPR8KwpKci11uzv7/PUU08xHo9ZX1/n4sWLdLtdhsMhnU6HCxcuoJTi4x//OLu7u0wmEzY2NtBa88Ybb/DOO++wt7fni0q5Nyl2hUEzn8/9+w4PD3n66adZXV3lwYMHbG1teVCg1WrxsY99jF6vx7vvvsvy8jIXL17k0qVLPHjwwBfgOzs7HBwcMB6PeeKJJzwrryxL7ty5w2OPPeb7tNFo8MMf/pDHH3/cB788/fTTLC8vMx6PeeWVV9jf32dzc5OdnR0P8PR6PVZWViiKgt///d/n9ddfZ3t7mwsXLnh2qgATN27c4PTp0x602NjYODZORf545swZ3n//ffr9vj+PsFS73S6bm5u+2B8Oh/T7ffr9PqPRyIMoAlItLS2xsbFBu932jMXBYOBlmAJSdbtder0e1lrPEBIAQubP0tISSikGgwG7u7vs7+/TbDYpisL7PwrLcW9vjyiKGI1GHsgM/dEEeBGmXlm6EKPRaESSJJ65GzKuBIQImXYyF04W5SdB/VBiePJnH/Q6OO4NGDJHw7kaAhKykRFuOMgRsqDa7TZPPfUUeZ5z48YN+v0+4Ji4jzzyCJ/97Gc5d+4czWbTy2R/8IMf8Nu//dteLh/HMS+99BLPP/+8T40VOX6z2aSqKm7dusXXvvY17t6964GZS5cu8dRTT3lwXynnQxmGdHz0ox+l0+nwz//5P/dyZElq/9KXvsTe3h6A71PZvCjL0jORhZUo6/Rbb73lgbaQ0SVt+dhjj7GysuLXQ2FACtgW9nd4CHCapinr6+v+PlqtlrepkD6RkChZzwUw39/f968L04YFVJT5JAEbsjYKW7PRaNDtdr1PoIwRGaeSSiwMz9BnVz4rBCFlzWg0Gh5Mk/Ejmyoh81Oea6GHoMwpOa9cj3wvzyMB0YWl3Gw2WV1dBfB9IGNbniPy2bJOyDon/RpuiMm9he0hzzcZOzI2Q09BeY20ZTh/Qlau3Ef4OSEAeJL5F87ZD2ITh+0XbhB80HoSAoTyOSef6XIeaXu55p/0+HAAiBpsEjtWYAFVoohyQyQMJHlZZSnamqKlSZu6lhcr0pGlSlwSZGQhGRv/PlW5kBZlam8npSgT56UY5Q6krFJFlFvvr6hqQCpvu8/u3Z47NlekyfqK5v4igMUkdeFeOammAxucNFLnlUt6DhiIyoIN6CaqWjCllKllmqliO++xP28TacMsU2RHBizsv7GOqllaZduChskZSyd3bK/5iqK5Zxmdh6KrmG0YercdG0xtZ5jUMv7smKqIeHpzi0+s3OHedIVv3rlClaZUKfz5M6/QSea88e4zXPkzN/lLp52s4w/7TxBh6eoZf+rym2wkC481KbIoFz6O3x9dRpWQ7Wkmj+b8yXf/LNUgYf3FHaK/e4rdj2lUavgPPvoaf2HlO/yn/9+/wujzY/545y3+Hyu/hC7h6c4W18en0eOIftHkO7OKo++fojhX8M9HpzCvLzkPzdombVIkzFcU3YtHLDVnPBKX/NbROVDwuw+f4WCnRyeD7m1Y/vX7/MXNb/LX/8afY3UKX7/7Mt0jx/gToMvEkB1VqHnuxmgSO0AqjqCsXJJwHJj41oCV1UC18NWzScTuJ9rMVxzwaxLH6DOxCyHw/om2Blmmiih3AFw0cwzXqHBhJfHMgQmdh4a95xLy5ZqB1DLYVkmUGpK0dAykaYLZy2htWZZvAN9rY7UDqcumYt5THF3WFG0oegbTrNDtAjOLSXZjd02JJWsWHqQB5+mmlPyBpVBVfV+xRc8VRVuRHTkGLwb0vMImi4RkkR5b7UB0GweyY5nzlkVSq/zcLtrJB6poB7w4pqPyzF6f3oyqNwICRmKsFuxGu/hsd30Ryix8StGSbq1oRgXtGhysrOLq7By/+fanWeuN+Wz3qgcq8toz7+C10yzfBF00a1Yk/MGffYw/tfKaB2AaquC/vv6nmP2rDc+WdoEVlqiAubHcvXyZT//Gdc7EA/fZVvwX3RqUTOp2Ll07SFBSlSr25h1a3TkzldCv2g6EybXzZs3B4Bjeuqil3PUGyMSk9I0DHXerHoOqiTKSKK9qpbE7F7peO63r84lJyW1EQxVEWEZ5hqpqUCxYD6UfTQw7eZdB2WQ373J3vMz2sMt0nlCVEVUdWGGNWjDrajYulULlThqt2iXKKEgNUVpRlRqMQsUVUeT8RlVWoZR1IUXKYvIIM01RuaKwUOWKeKxqWbtjaMq1IrL8GqgVf9EwpEhXLkTK1gzwyTlL2baYRuVClBSo2BAljsWIcnJnXYPzG0tDVrIJnWROMyqIlCVWFZcb+5xLDgEnA26puQda23pOWxXkQUjPzCaOvVlLhMXLT5KrJUxEmLASRiJAYB6waSuraejcv2ZmE3p65sYgeGCvpV0wiVaGlpqzzMS/XmToR6bhJ+6ynjjArr7WmU3IawA5URURlqFp+GAhOSY2q30O8zpAR5Goiq6a+sCUoSRvBwnM/aoOzNELDyxJLpcwIWH+FkRMjGsjYUY2aubl0Jpjff7T48N3COtQChsp9kIpHuD9mKqq8kXpSS8ySV4UWVcI4p2URgkrI2RkhNJlYUhOJhOyLKPb7bKysuLBk/BaHjx4cEzSJv5VAmwBxySaIZshLNAmk4kHx/r9PleuXCGOY4bDIZcuXeL+/ftY6+TR4o8ozBiRKwsLUfy5BGjSWnPjxg2eeeYZ1tbW2N7exlrLysqKl7CCYzuORiMuXLjAN7/5Ta5du8Z4PObMmTMMh0Pa7Ta3b9/2jKxQLh7HMePxmOXlZYwxNJtNhsMhP/rRj9ja2uITn/iE7/d+v0+aply+fJlz586xvLzM7/7u77K+vu6BnpWVFYbDIUVRkGUZ77//vg99efPNN73sXfwu19fXef3114njmHfffZfnnnuO7e1t3nvvPZIk4e233yZJEr74xS9ireXBgwf84Ac/4NSpUxwdHXHz5k3m8zl7e3se+JHPFgDk8PDQM3bSNOWZZ575Mcl8o9Hg4OCAq1evcvr0aS5duoQxznNyOp0eY/NJcnYcx96PTsamFPJ5nrO7u4vWmrt373qwTwpakYIeHh5y9+5dzpw544GY3V1HRmi327zzzjtorTl16hQrKytsbm56Fqj4lQ0GAw/adrtd+v3+MSAwLLBDUAOOSy6F8RpKC0OW0kmwQo4QGAjnycliPgT5wveGoGAIBMn75VpPAvlRFPnU2vCzZe7ItcqmhXg/CstuOBz69Urm7d/9u3/XX8OlS5f4/Oc/z9e//nUGg4FnYf/8z/88e3t7PHz40IMTAtZ2u10f9CPjJY5jut0uk8mE7373uxweHnogTcDRLMv4whe+wDPPPOPXKVkTp9Mp4/H4GNMqBEknkwmnT5/2fSLrmQBRAv4KQ1PGgVKK6XTqN4NEei3zM2R8CvgGCwDz4x//uGfsAd6OQMJ9hL0n4URyXdIfgAfWZIzKOQQ8TdOUVqvl08KlvUN2uJxL2lLAyPAzJ5PJMb9AYZBqrb08WuZzmDAsa6Rcr4BsJ/0EhZkZytflOTkcDv2cleegXGNZln4DLQThBMyERVKxHKFfpfRNo9Gg0+kcA25DYF5eF7IYQ5BRKXWM1X9yYy4cq3KcBKRPznNpr3AtCK9F+iNkRMpGYbguyM9lbITsVzlHCM5KHwsbtN1ue0n5T3p8OABEA6qqaoN3S5Q7hlHR0jWoV/+hUy5YWbp0IQ7KalTlCm2rF2QkSXw1maoBSRcc4oCGBQMxyl0qb5U74CHKLbpy0maTQDqqiIc5NtYoZVCVIdupU3CVgrIeMJEiX204cKROgkUp56kW16BA4ZhYKvBqM7E7pyqNA1hqqeWd8QrjIiXVFYPHNa2HirV3CngbB6K2XTJz707J8HxMOjKoPSflbRxW5N2YfNmZ6Q83FVXDYBsVy28kdP+wwex/dcj+tMU//M5n6d6CLFMcPVmic8255JA/t/4K3/rYo/zi+tt0oykRlguNPkvRmP2qzc92rjMzqWdDJXWCqSo16ZHFWMXNo1M0Dg3qC33+xrP/nL/613+DjT+5y//6yjf4W41fo8ogaRZ8rvcOXV2AgufP32dmI1r3I6rM8HzrDv9m71H0XPHagwv8l0e/znwz58lLW/wffvc/xJ4v0HniGaF56YJRpv0Wk0nG7TLin15/gXzTMOt3adxJa5YV3N5Z5f/49p9ltXLMvSh33oKO/bMIokmPStDa/UtiBxrKApQlNZO0nuSVAQlYqVOjTSOiSjWDJyxVp0LlimiiiWeKZK5oHFhau4bpmq6l9QvGkkkcAD5dh7INZcf5Z65/J6JzL3eS+ytjTB47MGaYkNyOaG5b0qH1svqiBXlX0Tgw7HxCo4xyid7NyssbE2VRtaxxZhW2fmCqtPIsQ3CAYVnqWg6pMUYRD/UijVpDvuTk1MlEULkgAKWW95ukRpKUA8s8kTAEmFQALNrj35v4OKuwEksA6+ab1UANeLmQlRro187/058iYDmC8wjVwjoMmMLg/NVyNLeKU2wXS3zz4FHm45SqM2W/9thr6JwIy6BqYZUD9XWBtzkwRnkfOHDgzGSeojVYA9oIQ9ltdOjCAX2V1SSq9MnPbT1HGbdOYRxAaup7FVY2CuYmpiCiG0299FcVDrStFKhIYQs8kA0ubOeto7N8sfsG98sVlqMxp5IhVjtWp4kcwc+kbrxag/PR085fdWYSlqMJD4oVElXRSeccxqCKYH5Fms7DiipxffQv332eahw7r1AW/eGSiIEa/ENbH2ChVH3uZt3XU0kDgXIWoXLHEsQChZtvqnTtkkiYSC0nlgCRYzYZZvFcOcaOVVA2rbcPsLF1EuLEOMlz5NiMaVbSa8yJtaGd5ETa0EtnLCVT1pIxrShnJR6zHE1o6/kxnz8B+ATYauk5KYZxHfKR40DqmU1YVhN2qo5n5QHczddcwrd2rn8JFRObkajyGBgnoLdBk1KhlaFftWjrOU8lhwxN5IFB8QucmIzVaELfOJZqQ5WeUQiOQdhWOUem4Vmv+6btz2twcuKtaolPNba5VSrnmWgT2qpkbDKGpuETjwGqWhbdVjkViuVo7FOXu3XwUCj5X4+O6OqcoUkZmqYLDIpG3m9UfEz7VZsKxcwkNHSDYdVkaBoclm1GpfPUHFUZ0yohNzEaSxaV6FHsxstPgcQP5SEFQxhiIoyK0E9KiiUpaqSoDQth4FjBE7IiQjmSnEMKbwEuBaQEBwxsbW0xmUyw1iVnbm9v+4IkLFTkukJ2ihRIoV/YyUIsBEkAzz6z1nL//n2efPJJzzLc2trCGON9yESatru768FKgNXVVV+EX7hwAWOMlxw+++yzpGnKcDjkueeeYzqdcv/+fQAPnA2HQ/b397l9+zbf/OY30VrT6/U4ODhgf3/fezAKs0TaVYBXKazOnj3LJz7xCW7dugXA008/TVVVHB4esru7S5Ik7O3toZRieXmZKIp44YUXPLA5nU5pt9vs7+97n75er8dkMuH+/fsMh0OeeeYZ2u02h4eHLC0teY+y6XRKo9FgOBzy4MEDbty44RmuIs/VWvONb3yD27dve5BZxtJsNqPb7fpwHmEQyVgUuedJcCQ0/T86OgJcuMjh4aEfSwKsCkuxLEuf6CpsWHmNeHUKCCBBMXfv3uXKlStcv37dB+qsr6+ztrbm5bRFUXDr1i0kBKTf7/vk5jiO2d7e9szO+XzOdDolz3M2Nzc5PDw8BhSFyeewCBgR6bSMP7EPEEBSmFhhkS5gVgggCoMVPliSGK4TcoRjT94XzqUQ1Aw/S9o4BBJPSh4/SOYs7Sjs5larxdbWFmfPnj0GYsjaIYxSOQRUEuDKWuulyt/97ne5efPmsXMaY+j1erzwwgv0er1jsl2RxR4eHvqAClnvrLWeWSzjM1zzJPAjZGjJ+JrNZuzs7PDcc89hrfXhHmGfy/wO12YBTufzuQda5Bxy/rAvR6OR98p88OAB29vbaK3Z2dnxoRvz+ZzxeOzbQxi/Ek4lbSFjVOZKyMiT9V76XcAymb8h8CVM2nCDKAxakefDSQmxtK38PwxcCgFIGWvGGL9GyDNM2jC0LAh9cUWSHYJpIsMVAPckOCebW/J6sXw4OX/keuX65Lkm1xAyOaU9w7Eu8znsX2OMB67lXPJzuT4JUAqf4eFzUtpKjjD0Ra41ZMiG34dp8KEkXtYaAQjlvTInw+e/nFOAVwHgtdZcv37dS8R/kuPDASBqfLGuDBQtRZU6OXGUO+aVLhyw6GS7+liCLLHyrB2RPfuPLqxvJKvdZ5UNV1CqWJGMDfFU+eJQWDtFSxHPIB5XlJ3UgRKlxeqIKtY1wKNQtQTZpJEr3AuDKg0kEaqosDpBjPDDhFn8NdX/j47//P5gicoq2lnO7EzJ7DTk3YSyDckIsO5+i5amd7tguh7T3CmomhHxpCLrRww+VtBZnfDCmXtkumJQNPj++HF0EfH48j7fe+8Sp39k2fp5w2eef5eNbMi/2X6EtsppxAVpKydRFamqaKk5H2vf9gWrwYEZFcoVY1ZTWO3ZOa04587RCvmq5n/+yPc5HY2YnVL86vm32Uz2mZ7S2MiQj1N+MLnMv5i8SDSH/Vmb39z9eZo7lqLpCr7hPEMB1TtdBuMe//l//K8ZVQ0e3LnEn/9Pvs7fv/N58o5F6vv2PZhvaB7Z3OZv7n6O6mqXP/PL3+Gf/fATLB3Uvo9A95stB9IkLNhpNVCg89pf09SAVVl5fz3ASZqNcRLm+lDWusANavlioSiaiqKVUjSV8xcsHXtV59ShHQ4gLDNF927J4eMJgyeMS1PulWTt3LGlSk0xTlGTiHQ/8inlrS3L4XqLzh3N0q0Kq6BsGubLmtEFRdG1zkexWUJsSe8nmMzC+pxOe46pJZrgpMjGuATYOKmoYifbV9pSlhFlEVHlkWOB1awzpa1Lk/XsPTcHJbyhtWc82ArC1K1/pgOJsAGFY9SqOlXcswP1Sckynukr7F2X1ryYU7YGgiQswr1/wSpzL6yBLL1gnLrzOc9Dd73WSZlrZmMnmnO3WOPbR4+SKEN/1kQpyEsH5nTjKTOT0tA5E5P6azDBxVvr5JCShAwQaUOZ1mBefT02cjJwqyDvwXI0qSXBjhm2X3YcEFgDze71Fl0DraYGdMo6gXi/6jCztVmydf2k6z6xsXu9ttb9vIKH4x4RlsLGbBVNv4b6do7AKHdPEuohYNv96TIsOdBnq1iiFedu/avXQlVZdFHRffeQwXOr5BsljB2iqZoVOq2wlUJF1gWOVAqKOjE+1+hx5Njc2hLPFTpXjv1aOiY6uHuw2oGDuvZslPEgILWNoIrBNtx49qBg7MJFbOIYgiozqMgQxQaUJUkqlhs5SVTRSXLayZxGVHK2MSDTJZkuWYonrMdDunpKWgeDCKNvZlLaek5uI584LcncB1WHhi48gCi/F5adY61WHJUNLz02Vte+f8azBDeSvtvcofJMRfGXrKymq6celJPkYQkgSVVFZTV3SwdyS0hPAT7h+qBq+QChHM3EZHXQyOKPq4YuHBBYX9PQNF24j1mE49wqF1LjCkWqKi9HbqucMSlJHW4iXotresyRadCvE7Svlz23LNc+pBWaUdWgsBGHRYvCRhzkbTJdEuuK/Xmb0kS04pxRmaGxjEs3NyZFwqyIKauIqpavV5XbKLFGU87q8Ja+Xni1/vT4UB1SEIWyM2GvSAESFrph4RsyC6SoCYFDAUDguC8VcKzYEbZGKPHrdDpMp1MP8JyUYYXFU/j7k8ClvCcMkQiBUSmWpPgbDoe+gMyyjJs3b7K7u8ulS5dQStHtdj04KV5qWZb5AAulFB/5yEfY3Nzk1q1bPjzj3LlzHBwcUJYlq6urXLp0icPDQ5aXl/3137x504NRh4eHvsjK85zBYOCLXAFQQnYLLBiZk8nEF/+TyYSlpSW63a4HL+R34gWmtea73/0uGxsbrK+v84d/+Ic+yETa+utf/zpPPvkkP/MzP+Mld9///ve5cuWKD4/Z2Nggz3M+85nP8Oabb/LYY4/x4MEDjDF86lOf4ktf+hKTyYTRaMQ/+kf/iKIoPFAW9kMoLQzl7ZKgLWNRCtU0TY8V93Ldk8mEKIp8MIwE7UjIjbBZ5vM5u7u7bGxs+GCH4XDo+0BADBnfk8mEfr9PURS0220ajQbT6ZT9/X3POpUxLyxVYaIeHBwAeCZkCJgAP8b0k3ko/SSAwnQ6PTY+hM0zn899nwsgcnK+yWfJmA/nuLz2JNspnDfhtcp9yu8/CIgI338ycCW8thAECcFJOed0OmVlZQVrnXee+GMWReHBCgFKlVJ+LsvcCIGkcIMhZE6HLGlYgJ0haC9zXmSpH9Ru4eZKVVV+w0HCPj4IKJX+FWZwt9tlOp161qKkawsAFjKr5TqOjo48y09AaXmvvFb8NLe3t/nc5z7HvXv3eP/9949JaAWoW19f9+0kLL9QnirXLMzEcANG3ieM9rBvpb3EVzAEkQTUlZAZ6a/wM04GvUhby/OpqirvKxmyAuUrLOS8J+egXL8cci8C1AlYH96/nDucMyEgHs6BcK6Em2AnAcDpdHpsfZfPlfHzQfLfk20UriXSDzJnw2e+PCvD56gAfSFLM0yEPqkgOPk6abPweR0CguGcCRO6pV3k9/Je+XzAs/J/0uNDASAeS0wFx0iqHMNQlzVIWB/CIizaNeOrcIwYYWy517vXxGVdQFaWsqEp24ro0HqJcjIxHoSIclc4VqlitqqJp5bOg4LDJzPnvxgY3YvM0F9u5TwSGwelAzYi7UETk9YpvB60CMAQaqaZdtfvimuXBHt01MRWmuWzMzDQ2IoZPVqSDCKqpyakP2wzulxhVUSjoRleUugyYbyhaRxE9J+E/+KTX2av7PA//uHP0b0Z0Tiw8AnL8LKTBp96aszvjJ7nr/38b3kGzPvjNZ8M+7MXb3E+OfDpry09p1Cxl38NTRNTax4NiwdoPLVkuuJw0KZ6rOLn2+/y+vw8mfsbgVcnj5CMLWapRI0i/sl7L5C/vsz80yW7d9d5+PULRKcgGVnn41VGFEsVqlVS7aZ8rnWV3xs9R13bkoxhdtaQKkVZaZI5/LXP/ituzk7zr/7hz7G0Y/ktPsnStvIBPVWmfJ/HUyc5NImT4cYT630vrYZoVkJZgk5Qk/oPj6bb/bJpAocDTLqGl9cCVVPVPmeub3UF6dBStBSzVUXRqwNJGoaoUzC+0+TCHzggU1+YUA0y4p2UeDcjPbIuHKVm5lYZ5D1F3ktp7hmiqWZy3lB0IvIVg+lU6MYcpSFJS1qRIdKGymhmB7WhblwRa5cinCYlldGoOk5aKSjGCXGpqDKLLTWzYeaSWZUD7qOmwVQKL2NWTrJpFdjMYgqX9hzdrWqgR8a483ekBuTEl9SkykuQPZFJ1UCaVl7KvAjtwM8ZFC7pebGiHJtvPqzI1t9HAQOYBQPR1gw6CYARKwLnpegYxL919aO8vnGe7WGHc70jIm1odOaksVsQenpG37SorGYn73pAVdXAmzIKCfDoV23Ox4doZZgXMbEH8hzr0CRu/OnCXZsEi2ic5HRQtTzD1MZA6a7XyB8kNatuKZnS1TOGpsnp+MiFldRdKam+ugQiByKqyKKn8OD+Kq2nCg8wLUVT569Yf66A7Y5NCMT1GC1gd9phTU850HOKKKKbzBc+sHLuWDuvyso6mXGlULnCFgpbJkSzOpW4VAt5f+3zrIv6niWdpwaDTe0jaBOodJ1WrsEmDgy0qQMAo1bpAco4rkjTklgblpozUl2x0pjQjef04imbjQMSVdVhIDkRxrOyGzqnq2cUNqJftenqKRPrgl8EBBMpsDD0EiqqmmXX0Dkzm3hwzzEEXWhHUktzNcZJam3sJL22Qb9GcY3VnpEXYZmZlERPKYhY1pNj8t4Iy6x+X1vPvaxXzu1AQuN/51iPBX3ToKj9/2QMhuEls/pzdB1cMquZjCJV7mrXaamtaEXOY8md26Vj71Zd3y5D02SomtzN1/xnz03CsGq4kCAbcVQ02Ju1KUwd2GIV87K+l5q9OssTytIxpJU2WOOyvq1RLv167qTtaIsqNZSKeFqH3JSOgRvlEBduiMXgA29kzoCb2/zkipGfHv+OHCFDD/AAooBQYTEtxRosiiyRlIUSrBC0E6aQMCKkKBEDeFgAEcKI6HQ6zOdzzp496yXCUrTIdYTXPhwO2d3dZT6fe6BAqYVPIxxnR50sosJCT6StxhjOnz/vQ1QePHhAWZZcuXKFq1evcu7cOTqdDg8ePPBpvq1Wi9lsxrlz5zzA8+lPfxqtXSDLwcGBT2eWc7z++ut85Stf8cy1LMuYTCa0Wi3PBJQiTO73JJAjPxfWx2g0ot/vM5vN+N73vsenPvUpzyRaXV3l+vXrPojh8PCQ3/3d32VpaYnvf//7lGXJrVu3eOGFF3jw4AH9fp9Wq0Wv1+ORRx5hd3eXc+fO0e/3qaqKZrNJkiQ+TfqFF17wgTplWfLGG2+wubnJW2+9xfb2tmfVbG9ve9aKgHRhcS0AYsiag0XStKSqhgWsMPGk74XRJKyoTqfjQyTkX57nnD9/ntlsxtramvdIfOSRR/x4H41G3L17l93dXVqtFmfPnuVnf/ZnefjwofdyEzmzAI/D4dAnoVZVxalTpzz7MUkSD1KH8kiR6YW+ZQKChewbmQ8C6giYJUy1+XzOmTNnPKgdyrs/aOwIKCH/D8GIcO6cZCTJEcomTwKH4doioMNJRuTJc4T+qXLOmzdv8uSTT/Lcc8/xrW99izzP2dnZ8YBuGAohILQkTYebDbIeCCgbym9l7Qsl4tJGwiAOGZzCgArvP1xj5F5ljAozUObpbDY7do/y/8FgQFmWPP3009y6dcuDOSHgGK6dcl1aa/r9PlmWsby87P0cQ/BQ5olslmjtUrs3NjY8gBdKagV8k/eNRiPP1JW5JeMuiiLa7fYxYEmAoXDdjePYs3BDFl+4MSW2AQL4hfJtYdYKoCvA4HQ6PdZ3IVsu9IwM2ztslxD0k5+H40dAzvBzZF0KzyfrzgeBeiEwHrI4w+fTyd+HTNOTwLvcq3yGUsqvDeF8DTdcpJ/D0BlZh4TFHj7LpT/l8wWYlHuX9ToEHOXn4Toi5wn/dpBnm7X2GCtV/qaQjSBhZ4P7O0FYvD/p8aEAEFVloawwmfM0jKe29sWqJceVkxXL91IzKuN+J6wY8S8UllHozVVljlUGOFmtsRQtlwJbNsFOlJNLp5CMLc39iv6jiQtMqcAnwiq835scJoLGPjR3LTbRmEyh565cLBvaFRmeQaWOAyBKYZU9BqIq40JAsoOI4YpLT836YJ+bUo46ZGnpiveGIV+KUMYBPWVWyxIzd76NpM/PtG7wT/Z+nqoJ9s/v8Uun7/Jv7l/h6eZ9rmQJf3T2Edp6zpm4z37V4WdW3ifCkqqK/+XpbzAxGX2T0dZzZjZxrJWa7ZKokhlOwnwmOuLIZuiZxsSWC41DyqOUeKbZN23+62/+aVo9+O7BZd65dZblTEGhsQ3D7O1ltFV85Yt/gy985a9gY5g+O8G80XIprtMM1SpZWxuxly+RKMNv3f8IuoC//d3PsjK16Jkity5QoVpTfPXgab7zgyc4937FvKdZuuZSq9EL1qeAL67fFTq3FF3lAWGsA5SxYIsC1WzUVFLL7OIyk42kBsZOU2U1IGbcWLbKFaKtnRKTKO7+ooJeSZyVGKuwlcKOE6KjiPhhQvu+A8BXruU0DlsML2qa2w41KTqK4UUXiFG1nRQ9apWYSjG50aDKHBA+uzKnvTRDa0MSVRRVFCpz3WGVY23FhiSuMFZhrCKfx5R55BJoFahxjImtl4vqyPm1pVnh2DiVppIE20rR2V+APaOL7oytXUM6yMEYUMqD6hImE81KB9DF2rH+IoVN8QCiVTKf6z+0sLWHXv0HHMrfnJ/3NcBog/uF+j2lRcl76obRha1Tmd15TEL9fT0n7QJUVAbO/48JBxcvMH7KcvBEyVJjxuneiNMtl1ZboTgTDZzfnOwyBKc0MRSF84hbjsZOVimUObnvmrGojEJjoXTeki54QnNQdWjrHK3s8XAmLcTQmiFao6imDjqSYIurs7NEM+eZaurADwFQ3UU68LLzbsr/5vx/RDvJWWlMeOXGZdr7aiE11wGbz9by7NrL8vb+Cu9ubvDVwTNsTbv88NYmvdniHusLwyYRra05y2+2yLuQDutfxYt1nNpvEF0DrBHkS9YFkkQWm9VBI9pCbIkyZygaxxXdRk6vMaeVuPbqJTO0sqymY86kR7SiOV09q8M+Zi6AI/D1E4agrn0CYRHesV92aFjnK9nWzo/woOpQ1ezsSZn5MbBfJQvZLJZIGRo6Z1yvrQ1lPdgoMvjCxrRVwZFp1H5/hrHJfDBJCBxWaHIb0dZzJjbzASRpHXbSVjlD2/BgZoSF+qv4GE5MRlVvBFUococo01Y5W6ZHYh0zsFHLhVEOCJzZxKdtG6s5Mk3uzNcorAP3lmLHOBxVGeMyY1jLgo/yBnENppt6UI2LlKKKmJcxxoIxmqKKyPMIYzRaG0wVUeUaCo3KNaqoQ23Kmm1aujW8Ds727FjZgPBhTXbBSDWJY51WGaioBp7rtGuTWWxi0G0HOkeJodOeMX5nheZDRbCX+NPjQ3DIH/FhEESz2fR/0MshxVLou3WyeAg/M2Qbdjodz8gJ3x/KJoVZExZhZ86c4e7duwyHwx+TfZ0slk6fPk2apoxGo2NytrBACouzf9tXcGzIra0t0jTl9OnTdDodNjc36ff79Ho9tNZsbGyQJIkvaEKPx+FwyNLSEnfv3vWejTdv3uS5554DnHTw9u3b3gNtMBgwGAyIosinOE8mEy9HlEJKgAjpj5ABopTyLJ719XVfpGntQlDu37/Pw4cPsdb5Lb711lu++L516xZf+MIXOHfuHJPJhPfff5/5fM7q6irf+973fLvneU6/3+f27dt0Oh1+9KMfMZvNeOeddzh16pRnSR0dHZEkCefPn+fv//2/z+3bt32arABlArgURXFMFip9JyxJYcwotUjk7vV6PP74475/tdbs7e2R5zmNRsOPFUmEvnTp0jFGp9baywqXl5dpt9tsbW0xGo14//33efzxx9ne3ubevXuUZellzGfPnuXs2bPef02CV7a3tz0bTrztBCQVICGUWcpXAdXlmgQ4k9RfCfMR4BDwUs5ms8nKyor3BW23255p+PDhQ/I8p9PpsL+/788dFuwCSAnQIOE0AryfBKpCwOQkqCjjMJz/4brxbztOAvfh/D8J5OR5zre+9S2UUmxubvLpT3+a999/n9dffx1r7bGgkHBNEmZzeG3CchZGoaw9J5lhJxnM0o7CjpOkdenr8Nplvo5GI9I05eWXX2Y2m5FlGevr69y/f9/LgeWaQxbu1tYW3//+9/nFX/xFvxFQFAWvvvoqvV7Py6JlnZbrjKKIra0tyrLk537u544l1+/s7Ph2EGCwLEuuX7/O+vo6xhj6/f4xECxkEcoclDChECQMAXxZm2Suh8w2AR3F61NkxLLOiexe5o0AhgJayVoYbiyEMuAQZAtDTeS5Ec4BaeuTz4ZwjMv75HdyDnl/CFaeZATKWJL/y3nFYiIELU/OlRCYlP6TNpWNNNlIkHEUMjhl7AuzT8DrsE3CeSrn+LfN13BuhO0p9yDy7sFgAHBsPQjZ5HKf4byRQB65H/l7JGwH8RwN5990OqXf7//EPogfCgDRRgriiDLT2JoNo6taWlr71QGeEeRAxZqtFy8SQIumRtcSUl24YBVwIFDec6wCZR37TMIGAGxbOXAmVTQOLenQcPh4TNl053HXuChCvCQyKIYlBEMVxkkI66CIKq1ZDjasnOvP1AtQRdhWXp4JWG0Z9F0CcfdeRf6jLr0Di7reAyzJUcLsVC0XTeqiO3IBB1EOjya7bMYFyz+7TVlF/BePfYWWnjOtEs8yOdc7YjPZp29aNFTBp1rX/fUNTcOzUzSGqC4uxdNKEjedjM0wrBp1EIPiSrZDfBSBhf/ytV+nczXlr/6n/x/+2rf+NK1rGf1PzGEW8fyTd3jr4BF+9U99i40ohrlmvlaRRIay5aTRZR6R3M2YvtOAyyXfmDzGw2vrfOyPX+f1Vx7FakUyULyZrzHda7E6tlz9e0+xWkHeXgAcUWGJZgRSV3xRGU9c2ycTW6f4LvrYZJFjIFaVC1EBxudSxmcXVDmXOLxg1ymDD7hQpYVIwTgmuZrROLAko9rrE4uJDfMlxcEzCZ37hqKpGF+qGF8EOiU6caEoraREq0XYwixPKFsZNrGoQrnXRQ4UtFYRBa9VyoFqNrIQWYo8Zm+SOkmoUS59SFt0WqE0mEnkveiUsmSNnNk0ZbLXQk8iGrua3q718r2Va1Pi4RzTiGnttBhd0DT2ctS88HJnAFVUDkyMNWoyhzhCx5rkcIppxBRLDeJR7iwB6tAVzwqMdSBHBhvXAF8c2BYo6xKfTyCntraw9OuIUl62rCrl1xKX8B6AhuAZcyiIpxXL1w29W4r+jXUGf3yXZ9e2aEe59yac4Rhl/aLlg0MAKtnoqCJ6ao7WlpmN2KraTPZbdGrZtRafQC2sOks0U7w2vUJLz3lYLDMom7y6f5HsQNXBTPUhIKKtmYUFfON3n+erp55zY2TuPk+1HEiiK8d2xEB6xMJHNlGkA0vxDzfYbcNWpmgnMN6ssLFFzzTRTBFPHBCJqu8vdpsr6Xe6/O9v/AWSkZsLTQVVE/Q8mHcW38cr13Im6zHzZY1JnWS7yqy7nwiqJbdpQmSJGyXN1pzl5oxWktNJ5qxlY5aTKRGGVpSjsZxKhr7/e9rtiu9XHSIM6/HwmITYuQRGrv8CH8GkBuBmJvEAHeCBv5aec1B16Fct8pqJtxaN/HsqNA2V09Y5y9GY/arD2KQsRxPHBGUhZ56YzCUMK8feM/VYym1EQxdU9bWuRiNnJUFS+xmWbn2uwThtTC1zLpnZmKM6hCSpWYyVdX6zkm6sMazrCTPlrtmwSC2e2YSCiFv5OnPjfv9gvsy4SqmsYlYljIqMSZEuNiOqiMooGknJrHBtMssT5nNHUTaFXoThlAvmqc7dWNaFG5Mh6JeCZ6EuJjWecVo1LCZzY8Zmxq1zykKdmG2T2kNEgGYLlNp9X6p6PLrNFWXcpp6qlAMnc4Wexphco3N33SPbJD2qNxJ/KmH+0B1SBAm4JymZobzvZCF5kkUhvxNgUQqR8DOk4ApBCXmPSOyETdVsNj3IdtIfKvwqRUxYwApwJqyzkwUhHGeanCzcRB4oUkBhGomUbzAYcOHCBc+CkACVMBBgfX2d7e1t3nrrLc6fP898Pue3fuu3ODw8pNVq+bCUZrPpPQgPDg48kAP40Ay5rpAVErJA5P4EPDt37hxra2sMBgPu3bvHmTNnODw85KWXXmJ7e5vr169jjKHb7XqW5cWLF3nllVd44YUX2NzcZHt7m0ajwYMHD3jyySc9G/Xg4IB2u80rr7zCpUuXaDQafPvb3+bKlSs8+uijbG1t8Xu/93torTk4OPCgZcgM+yBmjciuBXQIJYbSrjJ+JEH69ddf9z6KxhguXrx4jL0UsjY7nQ4HBwf0ej2SJGF3d5eHDx8ym81YWVmh1+tx+vRp//6joyO0dr6T4/GY7e1tRqORl4MCXL582SdlC0ByeHh4zE8sLIbDYltARWFSAceYaQIGyL/l5WUfyCL3O5lMPDtxeXnZJ02fPXuW+/fvezA39OcLgY4QiBGgoSxL3wchwCBz5oPmYAichJsD4WtkbgqgEq4BIUspHOPhdYpv51e/+lU6nQ7PPPMML774IhcvXuT3fu/3PPAV/hOGn4wvAaSOjo745Cc/6RmbGxsbHB4eMhqNiKLIA4phP96/f58XXniB8+fPe5A1SRK+973v+fcIeCZr3Xw+5/r169y7d4/Pf/7zDIdDhsMhP/zhD8myjM9+9rO+DfI859atW94vczQa8aUvfYl3333XpzKPx2M2Nzd58skn/Zo5GAzo9/tsbW3R7/fJ85y7d+/yla98hbNnz3JwcMDW1hbNZpMnnniCJEn8XE7TlDNnzjAajRgOh54tKDJlGZPi/Rh66YWM1tBiQEA+AaMlfEXGvIx1ecacZIaGa3E4BkLJsIwJeV84dsJxF4JkIWs0HK8hW04+L1xfw99Jn58E0cLxHYJqEr4SPn9OSrlDVmC4SSCgW6vVOgamhczC0LM0ZHZKWwqbWQBFAY0FXD/5TJH3iRRdAnNCOw+Zv8IelecjLPwtlVJ+zQlZiiHTUJKuxUZiZWXl2HXK+BOAOQSq5Tp2dnb+/WMgOmZf7WFWF85VAlHhQD4vY1R4UNBENShTM/oEWLSG2ivRFQWOEQOdB4a8LUBEHZQgHll1bdHaMSgLB09FXqrnZcu1F1kor3QXAgiLh/o+sD4opWy4z3D+Wy4kQ+SVqjJYHTsGZsCK8pK7GnhorU0YXlhyIQwlHDxvuPBlSzQzmLuaaG6oMu38GnciVGWZrsXMbExlc/6ji6/y7f4jnIkHaGU4mw1oq5zlZMJfufgVhqZJgkudHJuMRuTkdiJjK2xMT88Y25S2yms5nqaymqFpEqmEc9GQmUnRJZRNOB274v3iJ+5z59XzdD+7x59pP+T/vJ0yfWrGs5ce8vYbF9mbtrGJ5Uq2y7+ebNB8EFM1LfFWh8lmiUFjj1I6d6H/lGHzyi7/zT/9Vbg058+feYV3+48TzS2tbfhr/9e/yJqASxonOTNO9qiMC3bRlcUasNaFakS5A46OBXKohVTNB2xEkQeFbRIvAhXqw4eflAbbWIS6mFiRzA2nv6XY+7gDabAwPq+ZL1vKjsFmFVGnJM0Ktu920LkDj0kNK6sjDwAq5fzzrFVE2mKM832zsQNa4sigtUFZRRwZqtrfsLLu6+SoQTxXlJnFVG5+6KxCRU7+qyNDHFdobZnsZuhcubZSYF9bojdwYI6Tji6AfQeuG/ThENVusnS1JO8sea87rHXekEp5FM+nU0fK/a6sUFXt7xDXrynde/2Us9XCJ7QGarH1OlAFf9iJn2g9p0QWHUqXT36OSeqU5sLWKdHB5+kF4Gi1+xrlllM/mjAcrPH+/6LgpVO3ASjqYAtw4SWqOs6SBMjeavLr9/6KS43PFdEUlqdurSpbMHqkhMTQfD8lnrlrzQ7h//03/wRVzdBUBmanLPmTTmOp5rrewFA0d+tgJuP6aemGpbrjUpcBis8d8fMXb1JZRS928luAL996kuQPlmognNpPswZnrfv/ix+/waXWAa3Iecfdna4wqxLeePsia69Fbo1uuPNkhw5gMRHoTwz4tUd/yD/48mdo/qBuj6Ji92eWqDLF6rs58xVN/xNz4qwizQqSyNBtzDnbPmItG9OLZyzFjlm3kQxo6blbl4wLBklVRb9qkdRfu9GUftUmrX36ACIMWlkvy9YYjkzD+1EmuvI+r6lahI5oDMYmHiR2n2NcOIeXEyt0zSRsqIJKKQqTkWonf96vOgD0oplnFAorUbwEK5T3Pmxot3HTqAE9x0wsSKkwykmKAbSypNaxkMVbE/ChJ5FyKcTD2i9wq1ziASv+PgF28h77RZtIWR5MlshNxLyKGecp8yImLyPiyJAXMfm8BiaKevOhrMefcQC1LkDnilmxYPxpA+1c5uNiffYBNXU6t6lZgGXbuGd3aiBgQtvcye9JLMzd/3Ve72bPFNFMQX0NqqolyRUoG9U/WzASo5mzlvDzU6a8rW1KavsB+VvCal0nnLsNptmaUzbwk/+99tPj34HjJIAWFl2hVAsWBZ18H75PWByhcb58zng8ptFo+M+azWZEUXQs8KPT6Xh5XxRF3Lp161iCpFyrXIcUTAJmyvWEzKFWq+WLopP3KdccshnlfXIPUtDu7+/7UIozZ87wyCOPYK3l29/+NrPZzAOIw+HQS/VarRYrKyvcvn2b7373u+zs7PiwBWHYjUYjlFIMBoNjjBi5hlAyKfcKHCvk5T5Cj77l5WVWV1f5xje+4dlOv/Irv8JoNGJtbY2NjQ1+//d/H621lwHPZjPOnz/PtWvXuHv3ri/8l5aWaLfb7O3t8cu//Mu8++67zGYzzpw5w8bGBkop+v0+v//7v8/TTz/NnTt3ePfdd32BfZLVFcpD5efSP1EU+faR+5ICMiwsBeiS38l7hZUljLTxeEySJNy6dcv75gnItr6+zsbGBkVRMBqNvJy80Wiwvb3N1tYWnU6H999/38uOQ9mdSIwFqAxZRqFcU9hB8nPpO2HLCftP5kIURZw9e5Zut8vGxgZpmjIYDDh9+jTD4dC3/9LSkg9ekaTyu3fveon93t4ep06d8ufo9XoeHD0JkAhgJZJzmTOh16TcWzjvZO7JHBIwJPQ3kyMEfUIASsAzCdaRNOxwvJ9kKh4dHfGd73yHhw8f8uu//uv82q/9Gv/yX/5Ln54essIEQJb72N/f55vf/CYvv/yyb5/9/X1+8IMfeMnwRz/6UTqdDteuXePVV19lOBzyO7/zO3ziE5+g2Wx6UHZnZ4dHH32URx55xAOwRVFw7do13n77bcbjMQ8ePOA3f/M3vUfm5uYmL7zwAjdu3PDy3zRNWVpa4hd+4Rd46623+P73v+/Tj9977z0/53/2Z3/W+46GLNQLFy7w0ksv8Ud/9EfcuXOHoij41re+5d+3ubnJM888Q1mWvPTSS3z7299mMplw5swZnn32WcbjMUdHR3zkIx/x41LWPmFZDwYDP1eFIS0sMmENynwM16Zw0ygEjqVfZT6EXo7hs+PkGAjfK+8L5bTh5tZJpqGAa+GYlnOEjNsP2qQKPy98v5xPpLsheB0yE6W/BDiT9SyUecs6GEqS5dkkEnJhJIfPAFkDZe7K80Br7T1mxa5C+kuYucJKDDfhRImQZRntdpuVlRW/1oXMatlISZLEJ0YLAB1uNgiDMHxOh3Yc4Xoj40c2kYSBKn0t7y2Kgr29vX//AERVS9lMUieKUrP5ao8zXVjKpvKvhYVcWRiHAgKhFOnYQO58D1GOeSZBA5UR7ztFkdUppxU0DwxVphhc1gvPwpr9IACgO7/7auuwT1UX844pab0Xo3uNpmwq77moKoONIlAOyBIWjrLuvTZS6MJglSvGsGDHMa21EbMURo+WjBR8+qPX+KZ+gub9lNnZiqW3Uye9HkbEc5cqrXOcl5aFpWjM450dx6BRJV/svcmynrJV9ahQNevGMV660ZTCRmyVy75wbmgnlWuoovbkckb1kVr88dlQzk9Pla4Pl/UEDPzauR/wf0/P8def+Sf83cHjtO8o+huKu/1lUDD42hmyCP7pg0+glSVfcoBY+x60P31IQxWo3LVh6/IRw1lG546lOMz4v7z1F2gOLFEOeVMRzWvPwvmCmWISkAANXQftVMlCulk2HHtQUryj+YIRGk8tRRPmKwlZt4PZP0S3WyjVRJeWrO+A6KpRg2m13FZRe3PWHoi2Ds6p2hX9jxhUsyROK6LY0ExKykoTRYZYG+bKsQSVcWyYNK6ojCaOKm/oL4ep6nGCO3cUG4zRTOcJVRlhKoWtHNtHRQbGsSvWlSVJHVCotaEsI4pZDHea2LFCjeHi20V97bDz8Sbx1LXj9KzFxJZ4rGnuuflYJcoBp6MxSmvIC3S1hM4rJ1+uDPbhDirSoALkVSuUjlCdFraR+k0Ak0b1PPAkonqTQDtAUUBi8Tosa2CvqsejCSQlqv7DzgReiJXx4KIylrKdYOKIZFQS708dICkAZeX60UYRyhiXxl6HJiljWf3BIXfOn2P+6/cZm4zz8SGJLrlbrJGbaLF+xPVGReTGYOvBwtdPFy4tOwLmq4o/9sLbvNR7n7/R/TzJdzp1W+FBW1tvilRXpvzZp37EuMpIdclSPCVTJX/ntZ9j5dtuh84kULYUZXsB3Cy1ZsSqQqPJdEliKzrRnD/32A/4e1ufYemtyLeXlw7XLC+tLP2i5cGyp7tbLEVTHn95hy89+CTZYb3OZkgngYY0rv1wTLCDAgyvuP5qbceMPjXhV598k/PZIavRmEgZVqORDxEBx37eLXuO7WccYLZfdViLRhhdoJWpE3on9bqUY9CMTUqknFw3Uk7yu5APF545WtjYp/MKaFehHKPQJrSYk+Mk6IWNfWCHHKkqHTgpYVNWe4ATG/nr0rVvbIViLRrRr9qER1vPmdUhPC01p6ELIowPOmlQMLbu9xOT0DctBwzmKwyqJsOyQWUVpYmYVgkH8xaFiZiVMdPcPWSFxWwtlEWMNWDyCDWLoIJoplElRHMHwOfG+QJ2J3j/WVXW46N+vrowH0vVtFStOpQmc76TxNaxDmHB/AMXnGMc009Av2SgiXJQRnsQUBduw8eDgEbW9vojC2EU1xuK4s8qUnjLgg2cLixL/AaDDu9hsQEhfwf4ca0d0zE5qgPNfnp8qI6TbCA4njAa+ssJ6CBFgcgvQ9meACZS9MhrQk8tKZAEPBBGQbfbJc9z7ty5473ITjIdQ8aHHGHRKIewHKQICQvDEMgIwUg5BBwSGZhIWZeWlnyK76c+9SnOnTvnAQVpt8FgQKfT8cb0k8mE7e1tZrOZD80Qho4xxrdxCKhJmyZJcuzaT/pfyT1IMSoAlYCC169f56WXXuK5557z771+/Tqbm5uUZcnDhw85deoU/X6f9957j6qq2NraoqoqNjY26HQ6PPnkk4BjQ2ZZxubmJt/73ve87OyNN95gOp1y79493nvvvR8r8sOAgPCaZTyErMSQRSTvK4qCfr/vE20FXNJa8+yzz3qwcHd3149JKaSHwyFnzpzxQQR5nntgTUBSYdoI8Lq8vMz+/v4xLzwpgIX9E86RcDxb6+SwITgmLB6ttQddwoK91+vRarVYXl6m2+1irfXJuL1ezycJX7169Zg0tNPpeIA5lCmGgFno/yjgicxla633QhN2m/Rd6N0mAIbcT8jeCudeyCT9oCOcZycBRwFcOp2OBxHCc4QsLmnbsizZ3t7my1/+Mn/pL/0ltra2+IM/+IMfA25OrjnT6ZTXX3+dGzdu+N/LXBSW3nQ65f333+f5559nb2+Pq1ev8vDhQ37nd37Ht+vGxgaf+9znuHHjxjGm8srKCi+++CJHR0c+dTz09kyShNlsxrVr17xsV/rvxRdf5GMf+xj37t3j/v37nt0n/Sbepvfv3/cgioyFz33uc7z88sv0+33PGAs3g8qy5M6dO3zsYx/zbXH69GnOnTvH7u4u3W6XU6dO8Z3vfIejoyM/bgE/buXeBdQJAS45x0nmacj0Prk+n1wXQpA5PE4yBk+yx09+9sl1MmSxnhyTcg/h+JZ/8j5h5Yt9gHg+hmnTArrJmiDjVn4fblLJ54Xj+WSAi0h7RfYtSeoCQIbAnMz7fr/vQT1ZC+V3aZp6X9qNjQ2/loXPdAHxZF0ImZPSfrPZzK9hjUbD/07GiSR3n+y3kJUp4yRkusoRburJxgrg2ZTSbzL2wyCw/6njQwEgAnViMXXysiWeOgaekxxr549W//Efz61PXbVaueTluiVszThKJhVV5oqPeO4SfaNcioRFcqkuoHlomK5qpqdrf6+63hHqkwcshDlB/ZoaPLS6LmxK48ALrVCFC4+o3HhyARGRaGeF1ugAQ6ctdSc0iXbhHbGlbCnio4hmUjDOIBlEJI8fcW+0zMblA8bvn6Z1bsSk36O8NKP1oybVTJF3nUfYuWjOXpXwRLrNo+kOhY2dTxWavnE+V109Y6tcYmYSetGMpDa/r6xiPZq45FAqHxCQBukx4oXYUAVF7YGlizrkg4iqbfhn9z9Oc9v5tv3ju58gmViig4Tpw2XoGNKBZfz5Ee/dXSd9kPILX3idr998nPhHTfpHLf7q1V+jd1ODsTT+1ZJrfwXpURCak0A6qH2rIhYgdF1gJlPr+65o1YBuzfSUsaSMraXzUoS6cRIVDmSsTvWIZnNoNcFaVr+35/sQrRk9vsTwXIxNItRg6sfRvKvI+u7aBA3LWgVxXKGUJY7q3SbtPAZrnMFJ6SJLUWnKKmI0zVwKcuFAcR0b7E6DeA50XZvMJwmzUeoYaXXgiYoMKnZhKvNCYyfu/abSFLsZ2W5Eow+tsvYDtK5Az7Yn6OEEkpjZyimmpxWVAlVAVDhGnC4sszWXiK6MxY4nqGbTsX0k1VprqAy63XKLp9ZYswD6ECagUgsvwkhR1Q8R8R50r3HggJFAFoDSYNIIk2qiWeXmIBwHDCW8RViFyqECViuqZkyV1juDuUFPZi4gpyghdmA/sxKV1CzJev5qk/hznf/DEb/7sWf41ZdfY6fqkqqKRFUc5Q1/3YBnO1d12IzfpLD1w78GJgqr2S6W+LlL7/FH73+Exr7ymw1eMhm5j01UxXLsQKnCRExsyouP3eLNG0+SDMGnQMeL8/fSOZkuGVfOo68ROQCtFc35+Edv8vbDJ4gnagHA4z6jbFtW0wkXskMatYGosOAuN/bJXjxg/spqDbjW16nApJYsKWnpHKwD3W0tFy9WS/Q4omwoup0plxt7rEYjImUZVg0O6HAmHgB4ia9YKqSq8v59WhnyGriTa5tZt6ZV1gGI8ruoTvSN6sRh8SzEuhAcYWMLOy8MiDqoOsxMglaWhsrrYaycxJjjhbykZkfK0K43YSYm8+d2rMOSM9HIeRLW1yzyYWofxK1yiYnJuDNfYzfvMiga7Ey6HM0yJMQoL2OUsuR5jKQFK2Upiwg7j1BT7WTBhSKqGXogLH8nERa2qPhamsSCgqrpNg1M0zpmdM2OpnJ0Qt0oSbKSfLcFpZMiRznOZ3PmvE2jXKFKiGc45u3ceqm+sP1UZT1DkdrvVHyPTSTrokutdyAf/lntxnf9h10GVWq9/B1lF/6e7qOxkXEeh9oGkxEH1Ee2Br5tvY5C1ixoNdxYm+UJS42cg3fXSAc/Dt789Ph3/zjJmBAPRPmjPzTvl9dLMSdgD/y4T6IAYJJYKkdYBLbbbV+Qh0Eo8nkhgHgSPAvZYMK2kM8PZYUhUywEIkO2g9xnyGKRYk3OlaYpb7/9Ns8995wHIK5fv04cx95LbzAYeFmpSGRv3bqFhGuELDBYAIAiGZdkVZGoyfWFnpJhkR0WxiJ/7nQ6WGs5f/48AO12G6UU6+vr3Lt3j29+85vs7+/7wvKJJ57gzp07WOuSbT/+8Y/7wJfnnnuOL3/5y0wmEw8c3rhxg+vXnf2PsI9OFvbSD9KmUjSGXl4yloQJF0XRsdAeKSZHoxHnzp3j8PAQY9rVei0AAQAASURBVAw7OzsY43zsGo0Gy8vL3jtSpKfClDp//jyNRoMkSTzT8OjoyIdryHUKgxPw3pNSKC8tLR1j1IR9I+8XvzsBfMNAAgEihGEr7NJHHnmEpaUlDg8P2dra4r333vPhJ0mScOfOHU6fPs3m5iaDwcCzlJaWljxAlCSJDx4SVpD8PwSQhOUjsmlhOwlzSvpC2k/6KJxzIRh3cv7JGBQw7iSgE4IRspYIiBL2twTPhAy20EbhJCPtjTfe4Hvf+x6/8iu/wtWrV+n3+8znczqdzjEAPuy7oij8XAz9J2UMbm1t8cYbb7C0tMTjjz/urRTC8S33vLOzw7179/znN5tN1tfXefTRR7l3757fSBDAJwRs5N4ElHv77bfZ3NzkypUrPrlc1tNQmiprGSwSt7/97W/zhS98gfX1dY6Ojo7JaQVEOplg3ul0fAK5gI7Xrl3j8PDQM9ROrqvhmJdxdbJ9Q7A4XKvC151cK0LmXQjkhR6vsl5KP50ELENWrHwN16XwGqTPBSyTtUrYdCL9Vcp57ArYJc8aAfmEQRpaEYRgt5xf2lwAQWF1iiVCURTe26/Vah3biJOEZHn/3t6e30wRb0CxOJD3yTgTO4KTmwcn5crj8dhbSDSbTZrNpmdFh5sm4mccHrJ+hxsqoUeybKSE60VRFOzu7vp18iQgHF6vgPDyOwnKefTRR7l///6/XwCiVapmDDhGg2MaGBdyEjl5oYlqQKdOolWVJZ5Yz5LKOzVjwbo/+ItORN5WZENDlSjKpgtniQoHIlWZY1O0dism6xHj8zWwVLIoNFgU3VhPRnDApgl+b2s/xUi5uiOvUIXBJtq/VrwUHTCpAgllDXBEygMcab8g226gC8X0cs7H1u7x9eIsdqKY7LYZfG2J0UVLGsNkp01zpjD3G5RtKNuu8CtPFd6aKVEVGkXftByDpf5NW+UUNmJiMrp6VrMNC2Ym8eb4bT13Emfl5MRHJjlWGLfUHIOmUcsCo7mTOkYYmg8jtnbPM3si5//01p+k/N4K5o8P+eiZLW79o8couhHDK5a//Owf8bf+/p8gGcPXvvUR2vcc3Wr1t5tAk6SWtSH9UIO2YT+VzYAJKqb5uOK0zJRPzk4m1kvmFrL0OkjD1MxW65ipEsxjIlCzEpIEm9V/JFUGNa13FWZz1JUl4pl1rDvcZzf2NLN16N2tw1jAFaXKyY+t1Uwnmft/6Rak5oFmvmZAW5KspD9oY41CR/XOY+YuOs1KJlHmfABVDTgqaHQcqGEtaO3eU+Qx83FKspvQeqDIezHTRkVjJ6L9wDK66Arwzt26jeuBoyoDVU48dUW2G8uKzj1LNLMUbYUubM1ODIqY2ifS4aX1gzNL4WiEXV+FwyPQCjseo2pAlrKCJDrGOrS1FNmHDgU/h3q+pAEAmGqo+2+xtgQAJKArgyodg7DKtAMIBQPQyvVvUaLyAorS3Ysx7h+4hPUa7LRag4ZoMGXlS6cYv5jxaLJL3zQZmib9WdMHi/jrqZmENgKqBbMJ49rXxoZe7MbVRnZE/OgIM+g6uwVqQKQGU3rdCSvJmEkNBM5tjLaWS60DXn1sTvR25hNjq4b1tgztZM6pZEThvfU0BsWkyvi51Rt8//Jl2jdSb/fgWKGOSRbX4NfEpC7gBZHKRnxs4z5/tLZMMlLH7tlklo3WyMuIhXENoJslahhhI1hpTTmXHNLWcxIqunrqk4Lbeu43L3IboWuZsIB7EkwS1WPeMasVM5N4BmBDF17em6iSmUlp1d6DLqE4qZOSdd0mLpjEh6fUYGNUTxC5NmnDRJUs15suEdb7LY5NVl9DxcSk7BVdppUDIQHGVUY/b3JUNJhXMcN5xjRPmOd1snDpNg5sGUiGjQJD7UUJulR1YjwkNRtaFyFI7cZZ0XKsu6ppKZaMsz+o2YEqsotpXNTtOoyIxw7Vi2YKXTh2ezxzn+tkwDHRfJESL7RAlyLumH5l06XQy9otcv2y6cDkKnNjtEpx9h2xgJWLcUQN5mFwbEYFSlts6dZAFTu/BZ0YTKncuhkboqSiGGagLDqrMKUmyiqqUYJulXQ6M/LCAa9aWxqpKw4aaUFlNJE2ZJHb8NnsHHJnuMpqY8xeuspPjw/fEYI9UtQLe1B2+mHB4hCwS1gVUkSFQQAhYCjyrrBolqKm0+kQRREbGxvs7u6yv79/LAQhBJxCNkl43eG1h4EJAthIwS1gihSl4WcIgFpVlU9AFqDnwoULXr48nU756Ec/yoULF9jZ2fFBJ8Lo29nZYT6fe9BUPMlE5ixASCjhFbBQvoaF8MliWIrSULYVMsCazaYPzyiKgk9+8pO8++677O7ucu/ePZIkYXNzk7W1NV599VVu377NpUuXOHXqFEVR8Nprr/H888+zu7vLa6+95t9z/fp1xuMxr7zyir+/UH4W+n7JfUgBKWBLKG8VAESADelrAa2lqJfPDsEQYd2JrFgpxfLyMs8888wxwOjg4IBOp8NwOPR9KeNDgLRQbi/XP5vNfPiKgKMCzMpYBjyrL+xTAZBCZo4AnMvLy4DzxlxbW+Pu3bvkec5rr73GysqK718BIbMsYzweMxgMuHz5sr9uAf7u37/vr0nAFJlvAloIUC0sVxlbAliF4MJJcC4EeGQsynyR8Sd9JSw5eX84v07OX/l5CGRKG1tr/RgQKwOZ2yHwFZ7LGMPXv/51fvEXf5Ff+ZVf8aEhZ86cod1uex/RkAEm7OKQ9SWfJ2B3nue89dZbfOYzn+HixYtcu3bt2JwUXzrpD7nX2WzG66+/zssvv8za2hq7u7s/1h6Av/8QXDo8POStt97i+eef59SpU8dCT0J2bejNKPf28OFDbt68yenTp7l165ZnosnadnLMn2T7hmt8ON9OgoMnJcbhETJKQ/lwCKbJe0MGeyhxlrESjrmTmz6h362MFekTAc9kU0D+H/rnydiSfhA/WhnT4ZoagrbCGIUF23IymRxbj6X95J+As3me+7R6OeI4ZmVlhfX1dX8uASInk8mxaxVgT+a3tKXMHXluzedzBoMB4/HY+/PKJobMTVmjJKlbUulDoF7uWdacMBFZQHphYp58rkm7yN8J1lpvWxICzyKhljUplM2H65isYQIWi0RarBt+0uNDASACDlSLWYSj+FAEV2xGpmaJ1enMUW6JcoONNFWqPWMwmoMPOlE1IFRZV2BFCiN/HM6hvVUyvBgzXa/lwrUk+dhl1QUY/DgbSuR5qqoBRIMDESPt7iGNnLeeBOJIOIpasKIqkVnPDNGkdNLNmnUxvZLzl1/+mkuwvFKSDOpFrbJUZ+Zk72fM1jXL151flIkh7ymSEQyixKkvlWVmYrQynlnTCJg7E5OxHh95Ly5TUyy70ZSZTVhWEy+h61ctDBpjtQ8eEE+uhnIyvyh33mxnogkmhflaxde+8N/wS//gv6Ixg421Pq/fvcDa2KVdR1PF3/k7f4LWwBWXp35QG9nX/exScev2VixCKer+Fp9CbWsJm3Xvg7pQVXXozow6Kbl+yIjXZupAMJMtJGtl5kClaF4zZWomLCeSjWyrZph1mv58qlg8gNsPLONzirwb1b6BChJDVWnHDKocU9BahYqNY7vgwBo0xHFFmpY0ktKT9SSZVCnLxNYsIQuq0OjYUBYR5SwmOoyJJo7505xAPHZzp7lX0TjIeWCblC2X8Gxi1/bJKGDMzguYzqDZqOcRnrUkc8uDXxZUaVDdrntBDSBiLMoYVFHCPIc0gZ095ydZVKi0fnCUlftdtUgVFoaaDxXSYA0on4jsGkSYyW7+1dLFYO5arTDyesBqBwCV7Wjh4RhO+so4QHOeu6/7U9cGeYHKUsxwhF5ecuAnoJaXoKxY+94e/+2dz7PRHPLt965g9zOa25pkspDT+zUpHL+RO42Najmlhkw7r7uGLvj85Wt86d4LpIfagymmBpPTuCLCshRPmFQZhYk8KPXRK/d4594j6NKFpoi0tEyhk8xp6Zzz2aHrrtp7rzAxc5PwwhO3eXP3MS9PFfYXkWUtGdPSORNTm6ArQ1FFJKrifKNP55EB03eW/ZoJNTidzP28w0KVanQzwQwTonpTYFbGDCs3lyT1fWZc8vvYZC7huF6fImUYWvda2egQVmKFZlg1WI4maGXoRlN6wQaJHAmVYyQqw7Bqes9Aaj/FmXVrXUvPMVaznEzIbYSxDlg0aA7KDntFh6OyydzETKuEvVkbaxV7k9biXJEhLyNKo5nPkxBvp5i4tmSu0TVT0LH3PLbn/XijHLy3bw3ulu0aII4dS9RGDoSjUhBbHwhiUwewqdwFi6hCEQ9dGI7z44Ro6tZRl2bMwvOv3ggzka0B4BqYTt1zO+8pirazczCpra/PYjLjAkwUqJnGpsL6A+I6tKn2YI3iihhoZoUbUzVoVxm9CIbShqPDFqpmB7a7M8oyQmtLpF2QTCstGM9dmrN4xxbThCiraDZz8jzGWoiXZzSzgvNLAwf2K0teRSxlMwyKbjKjEZVkUYnGkkUlozJlXkU8GC0RjfWxTayfHh+eIywGhX0Vsv+kaAuLR2FdhKyUkL2UZZkHmgTckGJA5IJJkrC8vMzOzo4HD09KlKXwCVkPJxlJAgacZPaIXPAkG0uAhCRJPKtLipOQKXLx4kUePnxIURR0u13u3bvH+fPnvadjo9Fgd3eX1dVVD7adO3eO2WzG+vr6sWsOGVchWyOUdEnRGhZiYSF+sk3kXqRNG40Go9HIt8XS0hLPP/887Xab+/fv86Mf/YjNzU16vR77+/s8fPiQ9957j8lkwg9+8AMePHjAgwcPKIqCwWDA3t6ev3ZwXpYC8EgxGIIMJ1NPQ+ZQWPCGwIEcJxmMIYtF2kXOG3plCbAmgLd8loRiZFnmpbHS5+KzGDLl5Gue5x54kENkogJsitxWa+3lxyJTzbLMMwTDMIKHDx8yGo04deoUzWaT1dVV3n33XWAhDxdwPWQDhxJIGd8CMoTsMAGVms0mSqljwQtyCOAYgv7z+dwzjsU/LQRvQrD7g8ZfOD4ERAjH5UkAUfpQQIoPYoeFQF8IhgIenBXgoKoqBoMBr776Kp/85CfZ3d1laWmJNE25evUqh4eHHlyHBfBzEqQKGV4yrg8PD9ne3uaJJ57wTCcBv+TcIu0N5+m9e/d49NFHuXz5Mnt7e8fCJARUEpasrDdyTe+99x6XLl1ic3OT/f19P67DeR76/oUAm8iuH330UW8DkSQJa2trzGYzzzYVOayEAg0Ggx+bC6HHXvgv7EcZC+G1yToXtqccJ69ZxrV8Xvj6ENAOAzcEKBRGmgCD8k8YeOG4FCZe+DyT90ZR5DcKTsqIQwmxAIXCUJT3SiK7bGCFKdGNRoN2u82pU6e8B6JSyieGC7gXziu5VlmnxuOx3yAIfQNDJqTIitM09SzGTqfD6urqsbRjSe+WtUmYqjIfR6PRMYsIOY/0x8lNhuFw6EFKaduQ/d9oNI6t9zL/ZHzJJoy0X7gmyPnlGX0yQTpJEobD4bHx9T91fGgARJ+wSu1ZqBXJ2FC0a2p4DVjowoGLZUOhC02V1iCTcWCP+A9WqVowzETm3HBMjWgOycwwuJIwX8GnPQog5b0PwcvwQvBQmFDuwhbXhbUgTMPSeHaTzhXRvHLoD2AbkffFk8+MpiWzjYxoajh8NKVYMqjYSe2Wkgnp6gxz1Gbzyi73WiucPd1n6+eWWOpN2DrVJn6QUawXqFHs2CRLOZGCwmgaqqy9tsb0TZMKxR+OnyJRFR9r3KmlyA6MGNv0GOvnoOrQUo6l46SZLnkU41g8Q9Mk0lNm1jKqGmAhnip+6d/8Zbp7zpfxi//4v6Jz34VUHP7jCyzXDD9wIKKkacYzPBCqiwXgYvXiexs7b0ITqTpgh/r9dXvWzKaoEEYh/isEwGGmPJghLFZTsxKTiSWZGspMU7TdWMNabFmhJjUlO4kdMBZF2NgBhM6fTqMiTd5z0mWA6SnHRJWU40ZWkNf+g0pBVbkxns9jsKDnCuYRRSemnMeMhzF6pr30Wgr+7ti1jy41re2Kw6daRFNIRpb5isKkUHQcq6dWuaKsJdkaovMmOql9REto7CmqzPkZZgOD1RrVdH+YlI3aPiBYl6xaBNB4qX+r4UC8WB/zJLSRrnOSFFY8G5pNbL0wuh8oVFURTwqKXurmjsKHllitPPimS4ulBhd9YEp93gAsNBKaosEGlX6lI0ys0Gbxh6QyC/YoAOJx1Y2cDDuOoarQ3Y671mbT/UxroELNcmb/z3O88uemqO2M9gNNMrI1wK38OcR6wa8p9SXo0vqQk6lJaemcmY15rLXNxece8uCVc8fDJxSc77ggkQpNokta9T1Oq4QXlu9y7al1qne6DlxKHatLGeUZjuCSeCVEpaELChvxVHebdx47Q3Gn7ZiRscWmlubKlKXYpa6vxAV7RZdEVbQi93l7RZcvbF7lu43L7Bx2KQuXwv7Ry/f5uWUn79K5Ip4b5ssRVUNz9g8Vkw3tgf2iBujctRUuLAjNxGRegjysmmhlPJsQi/9dhfX3M7MJjTrhQsJVAJ8y3NAFbZUzpMGZeMDYpkRYchsxNBn9qsXDYpmteY/dWYe9aQdjFZM8qb0DFVXpwC0AWzmQyzEFgVKjirqAm2gPAEa58uB1lENTNkhqYFCXi7XOxM5H0KRus2DBwq03ToK04mRUMwTrtTKeChvb1q/VLuhJxn09V0yNdwjIbRKo2o45WDUcO9DGATswqoObFJBVXubrPFgd8y+KXSATQJqWSPiTHElUMcsTGmlBtzGnleTMyoR5FZFoQyed04gKtLLE2pBXEWlUURrNw0YPBRyMWrTSApOUtNMcaxWtJKeXzhg1MiczjwqMVbyn11DKstqaUlQRw7l7AC03ZxirKI0mLyNGk4x900GCq6o6MTpKjQN9LTRauWcpHkuF/unxoTikaAk9k0SuJz5PJz0EpagIfepOMs1gUQRKsSVApAAEAh4eHh4eK0JDCdxJpksocxIwIjTHFxBECj1JF5UiKiyMpNCT6xMZ69raGu+//z6nT59mf3+f4XDoJbFnz55lf3+fdrtNnuesrKzQarVotVo0Gg329vZYWVnh4OCA4XDoQwjCaztz5gwf+chHGAwGvP322z6gIGSIwXF/yrB4FHBR7ke+JknC/v4+r7/+Oq1Wi1deeYV2u81TTz3FG2+8wbVr16iqiqOjI27evOkDGUQ6qpRLzwxBoLDfT8ryBMgLQSIZSyHwG7JT5PNC+bgwVKU/G42GZ6lJX0ofylgTsC0EpaXIFvCs1Wp5pozIYoWlKWOm3W7Tbrfp9XosLS1xcHDAysoKzz77rA+LWF5e9lJR8UsUlqmwiFZXV2m1Wty+fZs7d+54NqsACuLVderUKXZ3dzlz5owHAQXoOTw89AXzdDr14zIECz4IUFdK+WvrdrvHmD5lWfr2FDZV+BkClgl7rtvteoApnHMhyCZjNASRpC/lOuWQsRQy/D7oXmRDQj5D1pzQYw8ciC3nlWsuioLf+Z3fYWVlhaOjIw9svPnmm8deL8CGjMMwuEHuT+SiwnJ94403+MxnPsOTTz7JgwcP/HiU/hOAT/pJ1rl33nmHT3ziE1y4cMFL65Vy4UoCRp3c1NBaMxwOeeutt3j22Wf52Mc+5uWZMi6E1Sz9IO0oyeI3btzg8uXLnhUtc/X69eucPXuWXq8HwGAw4OrVq35crKysHNuAEYBfwByxF5D+kvVeNohC30q5Hhlb8rmhv52sVyIVBo4lQAtQHN6DgG0ypsQ7VwBAAdQnk8mxTYYQ7BMmobAJq6rykthWq+WBZgEk5Qj7VrwGRTp84cIFz6D9IA9QASOn06k/33g89uxYOa8ApZ1Oh16v55+/0ubyzJI2CBm8IomWMSnzTOTSVVXx8OFDv9YIY1GuT/onXJPl+7D95bXdbvdYgI7cd5ZlPsBKgMNw/ZHPE5/eNE3pdrv+PkOP2JNsSAHw19bWWFtbO8Yc/UmODwWAqCoL2gGIqrSOEWRdUZ2MDEXXpSJHeV1oN5T3A/MhFfWzI54ZqlRRtBzgUGWuwKrqEJb00CUtz5dcWEpUSwyVxUs3j11bXTT4R1PAepPvRVpL8BqAshVhYseyi2bG/1znhryXEM2c72MyLJmeaTA5pbE6YnTJsYUYxSSq5PFsi/ywAacKHuwv8dTmFte/d4mlpw443O7xxz7yDt948BEuX9zl9rUzqF7O4+d2GJqolinmVDZiZmPWozHv5hv8v779x/j0s9f5TOua8zlUpZfxNfSU3bJXywKLOi3UNc56NCbCYpTmyDSCRGbYKzpEM0vvfct41kRVlmQIab++H/BJ1uJbpetkzmiOT+QUxqELyKgLaVUz4IRVavBss6omsumyZiLm1ofciIyvSh3IK76Z/jrqLovmlqSsg2xKS9HUzJdc8Eo6dOCUbtQ7I1GEGjtmmo0jMAar3GtRCpvG2Mid08QwW3VgXtyPKSPLNEopDjOiiWP/pEcKPYfWzKJKy5lXCqJZxcHTLcYXFNkBTM65azcaTNeAUaT9mpVbQOfuFF02OXgqomooZhvGtblRLgzB1KyivAa667aR0AAX5rHwPVNVhW2kqHnh/fPA9aMAX1Wq6rkgwHjmAFZVpwALTlFWmOHxtAEbfNUry45eWVbOziDwN3WvWYBuC3ohko1zbM65sJFF/wIefFTGuv6pJ7Qy1kvAo7lBT3Lne1iUTpbeyGqAEP/V5gUqUTV4qJy0OdLYJKZ7tc9+nPAzn36LH26f5+jdFTp3FemRsF6tY0pa135FS3nQpkqVT6IF6nmlGFUNfnHjXb7+SUN/2iSOKlpJwXpzxGdWHCi3HE0cUy7OSVRFJ5oxKFv88pW3+VryBEpZeo05nXROK855pLmLVoZ5lTiGlS6Ym8SxCU3KSjLmTz32BnubLsAl1hXNqOBUnRihlWVemyPqGpFfqsNBjNX84tl3aZwvvMx5IxlQ2JiWntP9xB7lzTVUZSlaiuzIsHKtYnI6ZjTLKGxMXjMBG8qBNLXz5IJRqF1ydKIqGjpHlx3/+iPTJK0p3zOT0LctJlVWS7RTChtxVDaIlGVUphirOSoaGKvoz5pMixgJIiqL2Elh88gFfdTMPbfW1GnDFVCzpKN5wIKR8VePWZk3Ts7rWHgmte7zgnGtSucfqOqNLp1DYyJpwsptjNShIlFu0aV7njkmrvWguQ/BCZmKLbcOSeCYyQScdACxTQwqNajIEMWGRjOnVYOAcVSxlM2YFCmddE5hIoxVJLoijSrSOnZeK+u9P1txjrGaaZkwLRMqq7wkeLPb595wmSwuMVYxr2I3Pq0i1gaNJY0q+vOmD39pJQWTIqGqGdhKWYbTDGM0h4M2Fui2Z+xGHQbDJnFSS97zmKqfQmQ5MMuO0lm6wJaDRkXUKqmmMTqtyJoFWldY6xi+rSxnrTkh1SXdZM5T7S0qq/mZ9g1+Y+svou8lP2UhfkgPKTbC7wXwksJBwMQQwJKiRsAAKQzlvSHzTBgHUgg+fPjwGJB0kvkUhrfAApAMAYUQ0Ag/Q0AnYXoIeBLKUIUN1Ol0ePTRR30x/vDhQ27dukWv1+PixYuMRiMvMwU4f/48Ozs7HoDa2toC4P79+4DzJXvnnXfY2to6xtAUE/wrV65QFAWbm5vcu3fP+7HJPYq88STQIoV3yBqRwkyYOUVR8Morr3B0dMS9e/eYzWZeZmqt9T6C8rkCFoVAZAhmhuyzEOw4eU0C4ALHAN0QeDrJ/AqB6BDEGo1G3rdRCvWwUBWGzsrKii9CpZgWJo58poyLo6MjHnnkEbIsY29vz8u8q6ryXpmHh4copTw79vLly7zyyis+BEcKc2EZTiYTDg8PKcuSwWDA5uYmu7u7vg+HwyHz+ZylpSUP7Blj/PUJs1X8GIVZo7X2IJO0f9iOIYAWgs4yRwXcCQEgAX/CeRKyOj+IMRiOyVBqKhsFJyWmIdAs80s+LxxDJ+dwkiT0ej3PFhZGpDC1BGA8GU4UrhHXr1/n+9//Pi+++CL/+l//axqNBk8++aSfnzJ+pI0FDBKgUoCjUCodRRF7e3u8/vrrPPHEE5w7d+6YZFkSsUNJrbTVvXv3WF1d5ZlnnsEY49lsRVFw48YND7iFGyrSTsJmPn36NMvLy55ldvXqVb/GnJSNCph469YtdnZ2aLfbHvQ6PDzk6OiIfr/PZz7zGV588UV++7d/m+FwyA9/+EOyLOPFF1/0wJKEg4RrbBzHHrAR4D4cKyEzUUBBWYNlTQgZgiGLTnxf5efSNtIeAh5JG85ms2O+dyHrT/pU5quMdwnhkI2edrt9DCQTwFH+yRFuYMgzstvtHvNDlDVLritk84pUWNYpsTQQCwFhO0vbCJNRxv/BwYGfMyHoGLLqpZ8FFLXWHgN7Qz9CWd+iKKLVavln80kZu6wLYXAU4BmN0uet1kJ5dBJwlXVQJMehbYU830TuLOuijNtw40nWA7lHmaNXrlyh2WxyeHjIT3J8KABEV/QvwiwkDVe8zQTQ0cr6gjuaCwjiUodjZ29QF+LKp56auPZFn1uyviHvaCZnXQHVOHCeUaaDZyAqs2AXKmEiCguxZicuvPNqJlYVMBZrUMvGGpM6OZqNoGpokqEDM+KjGVHNQoxmhvlqwt5HI+cZpZwETFWO+TMxGWt6CllF+2pGPE5579RlVt6zDCdrnH7P8v3XP0ojhfvfO0ekofejBsv/yZSWqmqEznmENZQr1lJVkfbmnG/2vSfi0DSZ1f6GTs68YAE1VEFO5L3Cxjamb1roOhUUoKtd4qf43pmakWdShfg8mtiBcw5ss4swk2jBFJSfmQRUWYOIFWhhEdavtxm17M4SWeXHgiRxFkmdlpssAEcTGOlHha3ZQLWfWVMz72ni2tzfSeSdd2Z6ZJhc6tGe1954xmAGR8yfPc/RpZQodx5fyURAUulzRfuhY/W19gyNvYLx2YTBoyntA5itOulhcuSuRUJhsnsDVFGynGqGlzKKjqqZktazJnXBglmnLHpWepDPJYtC675jgOVLlmxgiafGte/RiCrdcJYBBR7Q8gxbg/MBFJA08PwM2blVCjZ2DEFVVA6YrMe+JxuV9SLdbKDaLczeAcjCnabH5M42SzHZIgFYxoyMC5Oo4+NIO39U6jFHVG8+6MUfYt5rVL5Xbo6aSBFRz20NyWCOmsywjZoaW7MakUJtNMY8doHo3i5m/wC9ugIobH8ASsO501BWxN9eo/qzivNLA65fThm0mnRvRmSHFi19V9ZecMqxp03kUpKrDCfrrOdfq567hY345Y230MowqhpoLOfSQ3IbE9VJwZkuSFVJXicDGxQryYTfePybXobrNgqcD9/EpHSimX9foioPvA3KFt1oxqnOkMLEJLrEWL1g8FlFph1A6DYZKiYm89eb6QU7MQrSimc24c9c/BF/56XPcebfOGZv0dYkI0MyNuwctjgo27wzOctGesSoymhFuQMCiybGap/+PCgazKqEVJcM8wZHeebSla1iPE8xRlNVC6BJGHBKWcpSY43GGIXJI8g1KlckQ000cc+OKIfYLMa6AHEmcUw8NJStGniL3Hpd1GNJFcrbVkQzRTR1wSKO/VdvapRuDPsgkdJ6+w5sPc/rzRJhJlbZAhA0TUUe1UBgQg0M1vYHyl2DjeUaDWSGpJWjFFijKGYxze6ctdaMSBvWm2M6yZxZFdOKcw7mbc63+jycLtGKc1bTCf286Vh+ce6l8rEynMpGrCVjDssWe/MOq9mEo6JBXtO+cxMxnGVOhgzM5wlH0wbWKibDDFto1CzCNut1IXEbJFFaYYwiitxDodOa02vMmcxTRuMGxkjBbtCRIUmc5BlgqTulrDTnlwaURhOfM8yrmCwq6SUzYl2R6YpePPXPupZe3FekDIWJOCqbZD4wyDIzCdt5j250ATXX/m+Cnx4frkPYM+H3AkxZaz0ocRIIkARlYRiIxCtJEs/qEBCk2+2itWZpacnLpaR4CgHCEIT4oAI1ZCrK60IARF4joJYwz5aWlo7J8KTQX1pa4sqVKyiluH37ti+QpFAViduZM2coy5LhcMjDhw/pdDqsrKwwmUxotVrs7u6ytrZGr9fj2WefZWNjg6985SvHwDkBCqqq4uDggNXV1WOsDCmAAV9gC9AXMg8F/JDPlmJW+kmks9L2oWdaCKJK8R62dehbJm0Z9kfYD/L/UPp6UootQFZ4H6HHYeh5aa1ldXWVoig8oCThJ51O59h9NhoNXnjhBd9e9+/fPyaLDlmLwmwSL8LhcMi9e/e8zN4Y4/0rr1y5wt27dzk4OOCll17y3pfCJBVmmsgBZfxLASyH3IMAg71ej9FoRFEUPvQllN6H41lr7QEOkRoXReEL6xCskXEsAIT0XdhXAo6ITFqYPCeB3tDPUK4lHANyhMBi+P/wawjmn2QVh5sNgO/P/f39Y2NdvCgFABIJq8xvaRMBkF999VW++MUvYq0LN+r1erTbbS5cuMCDBw8YDAbHpKMy58LPCPtD5td7773H3t7ejzGrRQIatlNoP/DWW29x9+5d/70A1qFFhLRVyMzL85ybN2/y/vvv+zVSwFQ5z8l1QUAd6eujo6NjfSFj/Bvf+AZf/OIXeeONN7h58yZxHDOdTtnf36fb7fLRj370mCw1lGuHYy3sR/ldOObknDL+5boELJQkYZmnZVl6gGw0GnnWZuhXKPNgOp16oE7Ghsh3W60WKysrHviT55T0s8yRUEoroLQAjieZkWE/hQzN0KIhjl2QVsjCD9cH2TDY399nMpn4tTYMc5pMJt6CQq5b1lZ5Lp0M2QqDmdrtth+/S0tLfr6Hc1M+W1iYAnLKOWQDRiTDAoDKfcomojAIgWPPLGlr6XdhVYrkXDbtBBAcj8d+bIhc++SmlfgpNptN/wxYWlo65ov4kxwfDgDR4JhB2gEo8cw4sKBOwq1S5YssVM0wU1Bm2suxHAvNFWnO164GsCJXsDX3KsZnI0YXasmXhtlazXwTJppdgIfU34vPIXBcuowDuGzqXqNK45hYwnKKNWVTL8AewKQRujDMT7eJpxX5UsLgspNRl81gB7lylBRVKF47usivdl/n6csPuXZwkTTRmCfG7J5NOXdxn8PyDFEOydASfXJEcavDxr+4xhu/dBZzAQqrmdm4Ti+tGJqUhipQyrIUTdk3LSbWhQx046mX8DVUQVe74qpCEWFY1hMqlE8KndiMgpLKaJJ0yrhKvfxOiuXwEOBElw4wczeLk8ZaXOBJwxWZqsIHzwhoLMzPeGa9P2KVBmw3VcttA/BJwlJMVIOGc0s8s1SpcgV5pH2hHtXpoIMrEbOPT4ivtmjsu8+MZ3imGdai0oSiFzO+oHwfOzm29WMDIJ5YdAKNvYLs4RHpfsxkY4Wy6fzKfBvUX5XBgVdFWd+H+/xo7satzhX5qiEaKrJD42TEiULlpfd+pG6/eFyzC7000QHW1dlT2ARMbP2ccemmDnDTpUWVFTbPUY3MBwk5kLcONbGQjN24tRrULIfJFJIEtHZ9J39klZXzPayM8w/MFz50aOVYf9Zis8iHKbl+P86m0oX10k2Zl6EM2E/bWvYMDmzxRxWkNys8OOj6yxz73vQHAESrKxDHqHYL9e4t6LQdY7KqQCnngagURilUHLH2VsH9X15mrTGm3ZzTX4oZn9eYWNE4cGO3qhmRRacGeRqODVY1HBjVi6cYq5lb7Vl4g7JFK5o7f0RV0q9adfp5woNihY2kT6oqF26EoaVdQNLcJGS1z996dOQ9Bo3VJDqnoXKiugFbes7MJETxmEw76eegZvAtxZNjzL+habASjxlUTTbiAYOqTWEjWjonwnAl2/UpxhGGCuXsGOIJ/9kvfIV/cO2XaO4579aio2nsFZz9vYS/N/qMA5JsDQTXaeTeYqJSUGrX6aV2KJ9R7p+wxWt2GTiJr4wnXbp5GRVuwymaswj1SPBBHmXLYjIHvKlCu3FTMwx1rkiP5LNqlmC58AwUhiDIuHRhIKE1hg9nEuw8wVl11DYKJquTumtAUNK5bWxQxvm42tT6jS1VKTCgVnKipKLRcLLgNKpYa4y50Oq7kB0T88bhOfIqYqUxpRXnnGkc0YtnNHTBXtFhZ9YlUYYHRz2O5g2mRYKxTuq73hyRm4hJmRJrw/sHq4gsOc8jqjLCTOs/pCeRYzRn9Xwz9USNgE5BpzVjozPioNHy3pDLzRnLjSkbjeExYC+pU80TXdHSOT842uS5Kw8A2MkdUK2V9VLyTjxnP++wNevyRGeHRFdUVtMvWo55a2LGZYZWBUdlk0HRoDSa0kYM84zKuPNWVjGaZVxaOaQTz3k46dGfNKms4jv6EtGoBhB/XKHz0+Pf8SMsMETaFDLfYOFRFbJCWq3WjwFaIStGCgEp8NbW1tje3mZ3dxdwEtMQrAilknKEBbr8/ySLLfS1CyWPIh1tNBo+5VgKYAlIaTQaDAYD7t+/z9HRkWcvChAnBY1SyntKbW5u+gLwueee47XXXqMsSy5dusSTTz7J0tISL7/8MlmWHWMLCihRlqWXsQmjU+47ZJedbAcBCgAPnggYKCCIFFVhG0m7nTSclyI67GcB9kIQKgRcw/YP+0MKTwkckc+Q4lKKUjmvAJciNV9fX2dlZYWXXnqJy5cv0+/3+da3vsXVq1f9eJHiWIrnTqfDd7/7Xfb392m1WmxubtLv9337RVHE0dERg8GAOI558803eeyxx5hOp2xvb7O2tnasf0XGLuCxtIHIu8VPUZKuQ+9MYZfKV0k1FdBL7lvCLeTzpa1CIE+uRdpNwHkZvydBO+nbk4CzSF1hkdAt7NaQvSRjT8Da8HNDYOjkz+UaTm4shGuGvP7k+06ylk963wloJiCwgGchuBcyJa217O3t8d577/Hiiy9y7949+v0+SikvUxffNpmTYaCIgDWhf2coxxeWk7DzwjF+ElCTe5Kk2XAuhyCcADrSFuGcDgF9Ad3l9dIOIYgVgkPSl+H8lvO9+uqrPP744/zJP/kn+dt/+28zGo1oNBpsb29z+/ZtVldXOTo6Yjqd+msV0BMWUt4QPA7vOWTQybULmCTXGMqHQ3Cv3W77Z4qA2YeHh0RRRK/X86nrwooL17PweiS4aj6fMxqNGA6HnnEnQGPIoBYLCrm/kIko1xXHsQ/TOrk+SPsLq07Y0QKQ7ezsMBwOGQwGHgRst9u+/4RZF66jsukVJrmHYUXhJk3I3paNAK21X4OMcYFlAgoK21ae97IGhoCcsGZFei2+ndJmnU7He2hKX4bMRNlUAY55RwrYKf0nSeACtoYbIPK3hEiiQzZru93m6tWrXob9kxwfDgBRg4kdkKNzvJTYAReWKtGOSVhZdLEI2Mg7TjYqZvNR7oAhq5xPXtkUeaJlcCVmdipgD9oaAPwAAEIOAaVM5MAs8SDz8mXxbJxZdGU9KKGKqr7umtllHXij84qyFTNfTdA7lsHlhNm6A3KcDA2QYrNmsvTzJpmCZ5cecmfnMkXXUu42ePajd3jrvfP87C+/ww9+72lGTxWonRZkBlaXObd8RKZgpgxHNiK1MMMVY0emQVVGLNXGeBonC0xxYQohWwnLseRRKdIiZUhwSabr8RHGWvbnbS/RjOYO9I1m9nibUQN6yXGvQ1WDyMI2k/4XxmeV1gnK2jFMo6IO1InUQtKsF+CliZWXQ+vCkg7d2ClbmnnPvSeeWfKeYnJGUTw94dEzuzzW3cOgeHVnk90LCc3tuE6vtS5gAxbAUTBOdA14qtI4H0QjzKLFOKb2TBTJYjytd2qmLmXaAZXGgXGxA9P8GEws6cABGfmKG7vJ1N1THisn/43w40jksAKgybxBKap24sIQKkHqapAydzdjNe5a62MhVa5/YBegi40WADlKQVliQgai7EROpqhebT47m6FqT0FrrWMiBq8VOwG7qBW8tFf+b2upptX1ulA6cNFLlamZoEr5n/kgiJrVFSZ5l92M2BjUrECVFbrZgCzDzuYQFahmE93rYudzVKPhJM6d2LX7PEc1M6zWZLsTrt44w8deepVBu0lexoxHMWU7psjdmIwK10dV04FWJrE1k8xCYlhJxq7dUSzpOUUV+7na0jlr0YihaVBZzWo0Ylg5dl6FdYxEXTAoWrR07SOHoa1zN2dVybnkkGHV9ACfgI4zs0hDa+s5hY1Yj7cBaOicmUm9v+CgatFQOb1kSm4jv2ZEGHrRInQJ3PqyrNz171cdHkl3ib64h/knayjrPPjKTkT7/pxT328wW4scy6+eJ8L09mPDLtZsq+qNhqA/Zfz7DR+1mEMmgaplKDv1mlH/2rEEITly1gBRDjrXjo1cs5VVZdCVXYTuKKiSxTisMkXRrDc2svprw4F/VQPnH6jAZo5Rp2aRY80mxj1LtCVqlo4VqSx2GqM7Be3OzHn3GU1lNKNZhq7DQtKoIotLEl0Ra8PZ5oBMlzzTeuDCX+qHVL9qcWOygbWKRBtGeca8ijmVjRlVGQ9nS2zPuuyMO6RRxeFel0MLahwRzTSHLcN77RIqhc4q4rSiKiPOrA1oJznddMZKOmV/3mJn0iWJKppx4eXInWROMyoorWYpmVKayIGEy5q5iRkUTqrcS2aegTsomhzOW4zLFGMVsTI044JZFTNsNRiXGTtzJ7MvjVMoDPMGSlkG8wZ7h10ervaI6nCUg6MW5SxxIPRco0rH8leFrj2Ua+aodG/tMflOexmTWec3WSsSorkiqz17fwogfviOMFgA8FKjEMwLpbhhgSAsMXmtFItSmInkc3V1ld3dXXZ3d30RLJ5QIQhx8utJeZz8TA4pRMJkWGG+SVEk97K8vOz9llqtFlmWcfXqVS+PC+9PAEQpggFWVla4du0azWaT27dve4aTyFivXr3KxYsXvZ9YmM4phwBRW1tbvjiSc4pcMwQXBEQIgTwpGkMfRyl25V7EdzF8nzDopOCUYlk+U4q3EDyR14WSshCkEEbjSaAglCzKe6VNhFm3vLzMCy+8wAsvvIC1Tl599uxZ7t+/z1tvvcVoNOLMmTP0+33PxAvloGGB22q1PGNKim0ZDwImhv0h7FTxSRTGUavV8kw4WAC1EjYiAIqAhOJzKa8VcEDaQtpL2k4K7vF4/G+djyHQJsCRFOMn2bYCQIQ+ftL24bwRZlUIvIcAiJw33AAID+nf8BpDICPccPggVlA4j0+eS9hVzWaT4XD4Y2xG8VULAVsZ7/IZ4NaTH/7wh/zqr/7qsX4Ox70Abu122885GedyP6G8OATI5Z5lfMvrw/sL55L0j1yHfC9A00mwWOa8vFaANBnP8tky3uT9cr0n56C8RtZC+f7LX/4yv/Ebv8GnP/1pvvrVr3pg+2tf+xovvfQSGxsb3Lhxg6OjI+C4NDU8wrYXwE1ANlnLZJ0QIKzdbrO0tHTsMwUgFplxOKaEJSfA+XQ69axywPuItlot2u22X1dkjTnp0yhtJYDYSZ9Bab+yLD0bUEJHpC3D9pWNKbn3qqpotVpeaiy2CnEcs7a2dsw2IQSL5X5ljTo6OiKOY++FKAnzEmQl65+wALXWdDqdY+NG5NryvYyP0WjkJdcyzkMwN2y3RqNBp9PxUnrpK+m/ZrNJu92m0Wj4fpZ5FV6ffKYwQ0U+Lu0TPodCoBJccv3Zs2cZjUaesbi/v8/9+/f//66jJ48PB4BowKYOJEThk4lNXfDHM8cOUhXkncjJS+eGtH5N0VQkU+uDMIRRZpUD7ianFfPV+lSh36EUl8ERFqbyVcBDpFgleE0g6TSp69x4XoK1HqhQpWNFRZOC/uMtpqcVk9MN8jq01kb1vVYLEAzrCs79aYsDE/Ns6z6/K1Jpq9gZd9D9mO/cuELUtnRWJozKNme/EWG6DS62tyksNFRFT82Z1UhSQ1XMTIIF1uMjlrVjHRY28nLDH+seq4mUIScioaKtciqlSGxFV8+IMGilmJaJ8+Cq284kQfuJDFYtwCApzk2k/D1LMIf0vTLi9eU+S/wOhV2IxScziwyaCpKxcZ6Iha3BA0XRcYxQcMDN0aOKzU/f4z879z1+Z/cj/ODmRW6/f4nGgRsfz/+HN7l5/VGygxp8k4d9pEFr3/+uExf/r5qJ8+ycQd51QEI0KUApbBK56zZQpZZ4HDAYaz9DAJsmFJ2gQAnYdjaxx8ahMlCc7VE0xRtSfsEi8Rfpg5ohWwN/VUqdeq38+PPM0cKNY5knVi9AdRRBgJGB6QxbVqAVJpNUBrv4VxSY/YPF/VSVkwFPp649qRmHAcvXeSsq/ztlavm7JMMG8mTXPscns2MhythayOcXnoh41mbRi4lGEYoCG2lIUpTW2DRZ7KJq7cJfBkdO2j2ZoFotx9KsjJNyzyzLr8c0P1kQaUO3OWOctCg6lmTo2gtguqGYbhjSvnZAU9NiWhXNpRmJquhGUwe8RRNSVbFfdWjrubMUEFsCZaisphu5Hfzd0i0oq/GI1XiErkH/SFkiZWipOUlc0q/aLkCEObmNPFiYqooomrFdLNGvWqxFIwobkddrg3gRVmhW4xGFjYmUs1nQyvpQk7HJ2C17VCgmVcaoyshqj7x7sxUea+3wHz/yCv/d8/8zTv3AgcFFU6O7Maf+zRbzi6s8+LnMy3Hd3F4wR4+xwE3gPWicZBhcaFA8BZGMy7xyLEH8Bo8P6jHGA+QmVh7ArBrOisCNMe2AwAjmyzVLUYNpGHSnIE4rtLY00oI0MjSTglaSs5xNyauI3MT0Z0162Yx5FaOVZSmd0k3mtGtKtlYGrRwQfH10Go2lEReczobMjWvvg7yFxtKMCuYmJtEV0ypha9yjNJqDaYtrjdM+lXuUp+wP2hTjFD2I3aZWqajahpvd9TrRGJqtOUXhEo2TZsG5tQG9bEZpNI91d2lGBfeny2S6ZDUdsz3v0olz2tGcVuT8N5tRzko6pbCag3nb+xuOy5RmXDAtE+7YFYoq4lzHsXxnZcLutO03pwbjJiJFnk1TzCRGTSNsq3LyZm25+t5ZomZFNXYGudFI1wCgwmSWKnNjZ/uBs2CI5go9VzQKFuzTchHO5Jic1q+Buqw3TFRgN5Au/g6QzRPz4fgL7KdHcIQAoRifhwwzAYjkj30pEKTYFcaWFPMnzfelwHrw4AGHh4c/BjScBAvkOAk4fBD7KfwMKbBDUE1+L8V+s9nk3LlzbG9vMxqN/Nfwc6TIEunUZDLxBfZHPvIR9vf3ybKMF154wTMmTp06RVmWPqm51WrR7/e9PC2UZQmoJucKWU/hfZ6UC8u9h9JKASJCRlm/3/fFrLw2PMK2D4GfkwxQ+ZmcU65d+l1AxpP9FHqoSaEuRbMw+DqdDpcvXybLMpaXlzk8POTChQtUVcX29ja3bt1ic3OT06dPM5vNuH79+rEAAViwEUMQBxbAkoDTZVmytrbm70OKffm8OI4ZDAbHvDgFIJciOGwfeX8I2gnLZ29vzzO1QpZgCALLOcP/h1JJmWuhLFA+T4DL9fX/H3t/Hi1Zct/3gZ+Iu+SeL99ey6ulu7p6RTeAboIgAJIQCIAgCUqWNTRGFj2HhmRbY+l4SJukdGR75CMfmhpxPBZ8RhJl0h5JpmmSli2aJGiQILiTTTShRgO9V3dXde1Vb8+Xe94lYv6I+4uM9wBRpOYvYXDPqVPv5cu8N25sN+MT39/vu461loODAw8MRckqZRXwGLaplEf630kAJ4eM4bAPSllChVII2EIAHY7RULEq/4eqLcBD4Fqt5seMvC9MpyD1HqoOpX9LX75y5QplWfLoo4/y4osvesAW1nO9XufSpUsYY+j3+5Rl6dVt9Xqde/fu+TqTEHipMwFQoWpQ6kzqLeyXAkrkX1mWHoLIeUKlpnwuTVOWlpa8qUjY58OxJ5AlhKMh9JdxF+awPDo64vd///f5xm/8Rq5du8abb77pVYNXrlzx406UtwKORFUooE/UZKGSTH4PNxtOqtmttV6BLQoyCdkPARZwTC2XpikrKyssLy/Tbrf9Jo/MNxKiHqrnjHE59iRP4XQ69eCw03FriIODA6+Iln4t4cBSNtlICF3ApT9JX5ZUHlKeUGmntWYwGLC/v+/PI/cY5oycTqfcvXsXrV3aj5WVFdrtNuPxmIODA/b29vw8J31Tns3hXCh5DmWsippe2mFlZcXPN2IslOe5zxMpz9STm2ACAcN2kboQBbwYjoWbRXme0+/3PYiV7xXyDJX7kflTNv8Evso9isLbWstgMPiK59sfdXzNfH2VfGBl6mCPhJzKl3mU9W65KCgamrylqR1VTjf9kqKpnWFKbaFkEqUUcDy0rMpXBcdVhaEi0Wp8aLP8L4eoW3QB8bz6gpMbVF46BVpeOmVaAenAokrL8FKH6aYLXZvXREnlTq6qkExVuDxdJnEJ9ofTOtfzFR5Md5ivWlZetkSZZfbKGisFTDbrNO9bZvs91nYt3asj9CRjrTbCAGPrFqrrekpmNRMbMzSuo7e0A4sCESIspTLMbEJuY4zVzGziQxe7ak5WVZqxmn7Z5GzcJ7cRuXUmD0rD+IwiPXJqUpNWcFTW/8Xx+i1TtQhXVsdD+wRa5S3lnUcFYtnqXLrEqx7jWZW7cFaF9VrrDASUUxzmTcXBOyymVaKbBXaQcvdz5/iH97cA6DUdWCsarux//tQf8l+mlyrYZsAYCL6YuTBq69WWyrhcgCap3lP11Xji1EtoTdle5IQRxZwLYXRqw3RowRhsPVnA0QrqKevq02pbQekqXL8yMUkmzn3ZJlUHNfhcoEg/Lq1T/NasU0AGMCZvUYUwVx+o1ZxJDA42mghmZ3PmFw2dL9eo71kC0RoqjiCOKVpRECqssK0G9C4SHirLHfDrthx49OPKkoxcHVqtsNVGQOwdna0fu8pYF1puF58VNarrX5KfEQ9WdRXuHroyOxB9HD7a4RDaLee2HB5FgaqcmNHa9QljHCTVESaNWX1txh/sPcCTy3d53W6y35mTA8WRI1bRDObLlg++9xWuHq3RiHMudffYqh3SiWasxwM60ZTcxuwWXeoV/RqWDbbNknM+1nO0skxMSqLKhTKwaHI7W2FaJvTzJrnVZCZmUqQuTDSd8q3Lb3Au3Se3MbMyoa7mPlR6VoU8HxVN/tc7T3MwbmKBWFdhScblIbRWobUh1oa8jDDGvSaO4tYojNFobSjyCKUt1iiUtpiHFE93b/Dvfvtv8XM730brrjO+yjoR8dYy0byke926kHqvMFzME7qojESMpEQwTmFrcKHoWlUbDKoao9VmUoxPXeD+QdFyqQRMvADvpl2g0hIVWaLYKe1soUFbzmz0WWuMeaS7TVNnbGddtmqHLMUTrk3XGZU12tHC5fpU7YhbsxW0slwfrZJEJfMyJi8j50RcJry2s4lSsNkdcjhpkJcRZamZTxOi2JCPUmpLM+bjFObVZBpuXlTqRSzUl+Y4d3dLUUZE2pCXmjQtqdfHnL44pJPOWK2NOVM7YjM5ohdNKNF09JTP9p/kt249RLvlwOF6bUQnmTEtE66PV7lxuEy7PqeTzmknc+6Me8yKmPXGmGFeYzCrM5kn5FlMPothJrtF7oGpMjfnRFPNPU5Vymrt0y0oA8kM/5xvFy6CwKn4E3QWfA/ApQTALAzWUGJWoyrl+GL+U6U9tkEiJjQ6AIllorwBl5xDrmc1zJch6xlsraS2HVPrf12A+LV4hGF4oWoN8F/2w1BbwIdmCmjpdDoMh0Of9ygEiJPJhPF4fAxOnQSVYejsSeAQluUkaJTPCXAJVUqhkjDPc29+cuPGjWMKKFm4y2cEvgg4tNZy69YtvuEbvsEDOnF4Vkr53FdbW1u8/PLLXL582efqEqAg4EMWk+JiGao9Q+VRqAoLoUlYL6G6Td577949AL8YPwl35H2hUjSETCEIlp/ls6EqUe5FFFKhimYymRwLDV5fX+fChQu8733v4/Tp09y/f5833njDGwTkec4v/MIv8Prrr/uyf/KTn6TVajEYDHyIdngPUr4Qmkn7xXHsYcFoNGJ9fd3fa1mW3uFYoIv0H1l8i5mBOPiG0AgW4cfSXhK6JxBb+mgIXk/mGgwB3UkofhJ2Sj+Vz83nc5aXl31YrZRDlD+y6A/Dq0WhKdcS5c9JZaZS6ivqO1Qfy/lCpenJcXQSxofA9ORxEpaG4FFr7cdfCBWlTgWghWrL/f19vvSlL/He976XL37xi96AAThmWHHq1Cl2dnY4f/48gM/BdvXqVXZ3dymKgna7zebmpledCSgJQZ3cnyhkRWEs4y8EiUop74Auxktybbl/+beyssLDDz/sQ3nD9gn7S7ihECoZpU1CQ47RaITWmrW1NV566SUefPBBvuu7vot/+A//IZPJhEaj4ZVpko5BVMYCk0IYGM4LsokxHo99yKmApiRJaLVaPheeqO/CPhCONwFOIYyX8ZDnuTfFCQ295Dkj5ipSLxJyLPBdUggAHhhfuHABYwxHR0fHQsLlXmS+l/EH+HsMw34ll2eSJH5TQV4X5WWv1/MQXwxf5Bl1eHhIHMfs7+8fm2d3dnao1+veiEnG4GQy8aHBAnWlrkS9KeNQNnbCfKkyHoBj+TVlU02uLzkIBXoWRcHu7q4Hf7JhJe0hZQKOQWh5xknbhYBVxoHMh9JPlpaW/HXldfnusb29zSuvvHKsTf+o42sHIOpFuGaZ4NVFUe7CNG3swlXjeeUCayCeulyJ7gQLlVI6dAqlyZrLw1TWFtDqmOPliW/+YSizhMWF8MUzBwmXY/EeVTillY20A1eRdgoXC9mSYr4c+YWJ5KQDXE65amFzzFFWgSoVk5FT85yN+0QPjZjf7oCC8ZZbMOpLQ/ZGNdY2BmS/vsZso07cSrhU32FmI+qqJK8uNrcRJYqb81WsUXT0zIcxtlROhqZFSU7ETDtX1hYZEZZU5QxszYc5l2hOxUeMbUqEwQDzIqKVWfIuLty2Ul/qMOxQVIYVJEYtQpm92ijCL+h0UX1RriDvQpXoFpZu8QcoRVFT2BrexTeeWeadiMEDCv3OI777wVdIVMmX+lucbfb5g599N409S9500DmZuLD3eOzO/WTtrgsBtqAz4w1+KB0wKuoL9Yr0SVspMK2u1I8swIRNYvJOTDSrDDOC/hVPQzrtwuCjzGB15PuFhNC7frOoGxMr9LTAxHXXtxL3Zulvqgz6toUy0R7I6sz1R5NWOQ1TB2bQGtWsY2VyrrlQzGTfTTlevVjNQNYYMBYVxxW8ra5XqQslJyHhF6ZI41WdquovFXiMZkVlRqMq6KkW77WArnJderDocsiFGwMm1ov3H1Ou6cCN2oU5a2uckhIHe22SuHBzY1zYsuwuGeOaI479z7Yo3b0UJdRj4lHOmy+d4c997AXuTpeo1QryYY2y5ox5TA1sbFlLR5zecCGnD9R2SFXJwDQo0eQ25s35Jv/s1ruYZQlKWcoqhFWqIc8j4tgBuiKPELMQLNhcL25YWZeDLjH0VsZ8YuMLRFhaesLY1CqFsYOQM5MwKuuMyhrXb69BpkmW5sSxQWuLMcpfR2u8I24tMLDQCg+uIm19frr9gzZYuDleZm5iWvGc0aMZrbvVQj2BvBMTT0o6N+dEk9ylfViqkXUSnw+3qCtI3Fgr6k4Z5trbKQZN6sLVi6bFNAxgUY0CnVTqvtiQJIUPAW4mGfWo4O3DFWpJwZn2gDQqHFyNcrIy9r+fbhzRjWcsRVPqOqfTmFGiuDNfZjdr08+a9GcNanHB0azOUn1GXroxtD9uksYF660x24MO2TwmrRVYq7i0tudUhd2cVBfE2hArQyeZMS5q3B71iFYMnWROXIUrN6KcRpRTWkUvmTo1oio5nR4xLOvMq52YEs20dCkpShxQHhUp21mX3EbcZoVRWWOY17k6WGM6TplFCYfTDndur0BkXb7JKtfi0HS5B9iaQU01OlNsA/HE5Wy1CiIDcZUXUudh/kmLzliYxFSu9zKPS8oHlCgAF93YhY87NaqYJ7kwcVW5xysP/WSzKTy8ujsSBaFzrlbGha+XNbc5I30RDWXDYOsGlZboxJCmBQ/0BjzY2ec3/uBJagcnLvL141/r4+RCUCAI4MO6QmgUKgZDRUOodBDwFjp1ClgKwVh4nAQpYbkEyHw1ZZN8Vq4d5gAT8CPqi/39fQ4PD9nf3z8WWhrCgDAfI+AXeN1ul36/TxRFnDlzhp2dHax1+aNOnTrlzR7Onz/PnTt3WFpaYnt72y/gpHwCL1qtlleRCVwMc6+FqiuBASdVW6ESTBaI0m7ilioheCHACZVSErIWAiIpz1e7hiwOw7DAcLEqipxms8mlS5e4cOECnU6H9fV1ptOpz7V25coVrLWcOXOGz3zmM34hLH0qiiLW19dZW1vj4ODAAz1pG2OMh9QnwVKoDDsJnqMo8orSUEEY1o20e5ZlXhUUgtuTKrQ4jr1LuZQphL4C2cT0QdpZQEySJB6sSL43KWeoIEzT1Ldn2L/lZ7lPgUbh2A1DMOUI60baVdpT7l/qI8wrehJSyZgJQ3rDMRjCXSnTybDfk3NA2Pek/mX+kPfK5oTAmHDMPv/887z//e/nwQcf9Gou+ZuottI0ZW9vj5s3b3qAIX1DzvXggw+yurrqYY0AtPCQ+goBathH5D7lHI1Gw5snhaHI0o9ChZuo0QSGh/NAeP1wXpYxEm46CDgaDAb++pcvX+batWtsbW1x9uxZrl696vNDSp8Ro5Mw3DaEOJ1Oh06n4+tf5g5JKRFuRslrkmJB+vJ8PvdGRcPh0CvOBIRL6LNA2iRJjrWBKB7D+UieVUoput2u77dxHNPpdPz8IXN+OK5DmCuAEfBzZWguIkrGUL0szwy5T6l/UYRLyLW4zMs9SKhwWZasra15yNdoNPy8Iv1d6j7MNytzvmzWhflPga8Ifw4V6jKuBDLKXBjOeWH7hfOHAHNRuUpfk7EuIctS19LnAX9vYcoMqbdwQ0aem1EUeWVsvV7n8uXLvP322/9/BBCVKEkqxVSkyLpV3jurUCNQsiBQ+DBFGzkH3TJxJhDT9dgrkjx0ilwOvqyrPMCy4PPzwwL8yM+AzyOng3xIkpeR4D1i5KDzCkYIZBTAUTlkLkwqLLpQHkDKAgkWef7Q1sNDlAMBE1NjRWdEkaF7s2BwPiaaOEXO7H6Tx568xY2DZc589y12fukcWS/hUrrN2Mb0VMasIlg1VVJahbEKM0jIK7OUGQn7punDmSNVUFe5c27WeWW8UpAGWgtjNTkREYaxqfFmDpNJjbatwrqEX1SMKAzPLutqAV7LauFfAwknlDBz+Xs8C9yHy0U7WQ15swo3yqo8cqkiU9D/pozvfseL3J0usT9rcXN7hf/9l99HY0cxeu+U8UbqHLgjSCbOAKWsV+1SAbWHkpj5eknrlnaLzVrilGdFiU1cbkRbQSsPzLQzzDCVuU5ZU0RT60x2rAu9l3yNEkYbVYtX1w8cvKMoHWRNLdHMgdFF31PePVlC9aNxRtZRlUGDXRQoAI4eoIk5SeEgS/nAlKlukLxFBdaq92hdqbncOaXMLrzP+vPqElSrsq63zj3dqcSEdFWh0JEzSyGJ/T0SR/5nGykH4iWHYaQc4IuASDmpT6R8qDMKbBz50GY3PKty5YaomgdUaRwkzApspD3kL5oJZcPBTVUCpVxXo1d60tFRtdqizuo17OGRM5hptzCTqcvnqKqBrzUmjVh7QbH3bR3qUc5SY8ZINx08jF1/sRqW4imTMqUZzb2RSVdPSVRJTzs39Ps3V1B1l3MuimXBYIkiQ6uRobWBGtSTAgUkkYNLkTIeQmllMFZzlNXZG7X4H++9z+eLG+VuUZwVMdMsYTZNKUcxm+cOUZFl6dSQVi3jcNygKDRaW7S25LmmKCKnOiw0RS7yPeXEmLHBWuWUe2U14VZz2ls3NnnLbLr+H1nGpzWNHVvltdXYCKbLEfV+Qn0vY//xOoMHoeyUkJbopCStF6x0xpxtjMlMRD3Kfe69pXTGuEiJqnDgepSzWnNhIYXV5MaVtRHldOMpuY0obMRRVicrI/rzhep0p2hzOGwSxyXzWcpLsy10WmKNQscWUzqgGsWGUysDanFBM8loJ3PW6yMut3fYSFzenL28w1oypKOnbG8ssZd3WIqmrCVDjsoGR0WTfu7GkUGRm4hEGVbSMbNGzAOtfQCmZUo3njIxKdMyoTARO3MXenI4b/JicRaAWlxgrKI/azCY1Yi1oTSa8aSGOaxBO/cGNSaPoHo2JYcx0VSRqGqDoRrn0bQa/5VLPVRpRyz+uesNr2RuyxfPRVH3S8oKp+6DPHFjuqxXEQg1G+RQXQBhm9hqDgAbGQc2qzFB7hSiulaitBsjArTjpCSKDOOjBs3uzAHwImKpNfUw3FjFPI+xgDGaqFLcxlFJp5bRTl17AmzUhjzRuM3nak+gzNcB4tfiITBFvsDLYjCOY5rN5rEFRwg1JOxJktbLQl1giuSDCwFQuNANgcBJlRwswKYsSkIVkwCh8G/h6wIFJFxOFrTh4jO8XqgSlIWoLLrW1tYYj8f0ej1WV1cZDAbcv3/f59uazWbUajWuXbvG2toaWZbx6quv+oVRWD5Z4K6urrK3t3fMUCRUuUn55H6kjCdDAUOlk0AvwAO9sI5PQqAwNDNUEhljfC4yKUOYS+4kUJOcf2VZ8p73vIetrS2v6nn22WeZzWacP3+eLMu4f/8+KysrzGYzxuMx4/HYt02YiyxJErrdrgecshgPFYhhXYX9SlQyodomrNMwP6a8Fp5LTITa7bYHaSG8kboUSCQhi/V6ndls5q8ZtlcIlAQ0FEXh84SKkYD0A1H9hH3IGONzLkouTym7hGbKeJUFu4xhgcgCwkK4FeYfC4G9jJUQMoRHCG7D/ir38NUgfwiCw1B+mQPkn8wf8nkBwqJslc9JP5accwBvvfUWBwcHXL58mdu3b/u2EUB2ckNC2kLeJ0Dn9OnTbG9v+/EdjuEQrss5Q2WmtIso6UJQ/MADD/Doo4/6MRXWs4yn8XjM3bt36XQ6XLp0iXPnzqG1ZjQaeXVbuDEQgsVQlSfl3N3d5aWXXqLf7/Paa6/53HgyZ0k6BznPZDLxUPjixYt0Op1jYaViSDKdTr2SWBSAUh/D4dC7QYdAUcZLOC6lD8sGBSyc1OVvw+HwmII93BARABfm/5TnmfQdKYPMcbJZtru764GeKEylj8JCcSz3EtZ5OC6MMb7MUpYwnDyEoPJeUSyKUnQ0Gh1LSSFpP6Ssoq7e29vzoFX6jUBm2UCR/hyq/AT2ydwhz1DZLAlD7iW8OATDYS5JqZvhcAjgQ4vFZVmOUKktc4bcTzinyyZHmDdXFKDhWGs2m2xsbLC1teU3zv44x7/+ANGKYkwtIIdWYFx4ZNZWqCrHoctRFIQ1W0C5v/nceJHCNJV3+o1nlnmphEkALABOwFnk9XCho7OFMkze59Vx4IGOy8lYSeZFMWWMz9NnTZArqQJjJnWrKhtZD9x0SZVjyzkw29hCrtnOu+QoHt+4z73kIRp7Bl0o5j3F0hsRuy9coJNZ7m0toVKYbRSs6glaWXKrK+dkp0bsmwbfu/x5vukjV+noGbtlh7rKSVTJ2KYuvyGK0mqiIGZ7bJ17s4RKJqpgaBr0yyadaMqF2F3P3YsLsRWIZiOwqVrkrVP4dixbqlKTVrtuVdh6PLc+r50yQWhZVL3XVqHuCUzXFdPThtUXFAfvzTl75oD+1XV++fV3UH+lQWPHslQZG9gI6i81uPH4KsuFC40TAxNRMlGpZmIi4tUpyrYWMEwAWGVaoiUku+pLKi99XxITmXjmQpvVdI4umuQtvHIRW7nwpg6eKQNkOUSarKWdi2wFz0VNa5X9in5rk8jBQqnv2FZqTgetJYS7fmApY3deCZ+O32oQj1X1mcqlfJ5Bs+HgnoJk5BSCRcVWdObua76kXd3khYOBRYmp2kjlpYOE23vYosDM5kRLXa9K9OXvdrBJ7OBxWjmrZ0L1q5Dj0imMVHFi0OLmAKWtB4DOCMGFP4ODjFYrbFrt8ioX2qxLQzRVlA1djflqI0BrbK3qDHFUwcHF5oVqNXz4sipK/3c3QRisjmnfzvhnb7+TP33xZW4OV9BpSbZkSI9czkObGG7NlhkXNXazDq/b04zL1OfKmxYJt/Z6xIcx7ceGFEZjjHIAT1mKUjMvY8pSUxYRh6XCzhcQSFVKRFU452BMtUlRt7x4tQsKytaJL8DKErULmtcT9kZrNPY1g7UU+7Zm7XqBKixHDybM1tx8VcbWK1kj3Lyl80oZllpsw7WVsgqbGHSj8GHM0vetUYwvlCRDTTKuUhpYTTKxzHoaXSTM1hSXnr7Jan2MVpZZ6WBas8obeH/udlSl3sZFSieZkZmYcZ4ymNe5NVimKDVxZBhOakSRYT5LabZmxNqQFTGRNkwmNeKkpJ46F+pGmnN6eUA7ndOMM2ZlwmptzEoyph3P2UgG7ORdchPxaOMuQ9MgtxGjss6kTLk/73J7ukysS7anXWJdspTMaMVzro9XMVaRlxG74xaF0YxHbndXjFQoFUk7Ix+nvL68SVHoCtpGlFlF6qrnFtqixrH/PZpor9rWhSI3DvS15jI/Rd5BWjYz3AaEe6aJctkqmPcWmyImgaLhjGbkETFr4dOQ2CpFglNZK59KRPqfqRun9FNAatCJCxdvNDIiq2jEBZG29BpuF3WaJ9TiglpUMC3cg35WxNSi0gHSqaszcXGeFzG1uCBWDqbrar7Y77YYzmuc7x4yyOqkUelMhpRlViQcZXVqkVN/tpO5G3NWUY8KVtIJjShjWqY80bjNu+q3oV6ibLzYZPz68TVzCCCQhdrJsDRZFMmiTBYeoZIvXDAL1JtMJj68V46vpigUGHASSm1sbHhnTgEKEpIWLtxOhlOeTNIOC2gjZQ9VjnLvsrCThU5ZlgyHQ7a2thiNRijl8h1evXqVS5cu0Ww22draYm1tzQOgF198kdlsxr17944BI1k4z2YzXn75ZZaWlrh169axMp5ckEp9hWUFjgFSWfTKYi+KomNgTEIvAa8SCUMeZWEb5hiUOpH8ZKK8kZxhrVaLtbU1er0e586d44EHHmA6nfLiiy9y8eJFBoMBt27d4uLFi1y6dIm33nqLz3/+8+zu7tJoNHjyyScZDof+mqJokuuG4dMA3W732EIzhL6yeJZDFvZyhIooay3Ly8vHlFZh/5XwyVChJO0uqrUwbDtUrIaL60aj4QHkbDbzTt/tdtvDCXE7Pjw8pNFo+FBdWIATUTvN53MmkwmdTse70QogFZAj5ZBQWgmblvYW9ZsYLci4kcW8KKykP4VQUc4dqoTDdhCgJ/Upv4d9N1Q0STsBx0Ch/F0UaScVjdI/RBEnECLMhynw69lnn+V973sfr7zyik87IOPEGMPBwYHvw2FosgDKWq3G8vIyWjsXblHbzWYzdnd3PcCTa4pCKsx3Kv1JVGo7OzscHBxw9epVjDHs7+8fu4csy1hbW2NlZYWtrS0eeOABf66bN296qC959GSek/EjfRAcaOp2ux7ShZs6Anhkvm+1Wpw7d8672so1syzzobd3797l3r17x8Cp5KsMlWUS6iv5DQVi9Xo9D7akncNUAZJ/V/qenKvdbvswdNnkCtWQ0o/zPGc4HPrcp6Gqr91u+zlecvdJmoV2u+2dnwUwSvvLPCmQV3I8KqX8fBU+i6TeRqORh6xaO3dk2WATsCrzn8z5YsAkodih67psoMicJGNe5ikBliEIDKG9nFvmK6nLUG0qYzDMOyj3Ls+BUBEvdSX/pA+GYFfaW+aZRqPhw9QF7obtGKodT84T0h+11qyvr7OysnIsP+gf5/jXHyBWh40XeY2izJlE2MpUxcaQNxXxtMo3WKnclFFEmaGoV2GSRpSJlrylyZvKKdMkGsEEa327AIaAd0sWeJgMoXujoL4zJd7uQ5Zj203MUnMBELULfzQ155iLqdRT1mKTyKszJG+fd2RORZXhFlG6XCgjsaByUVm69714dJbJUswTnXu8eeoR0pFl5fUZeSsmGRbYWBFNC5ZfzslWm+TtlJoqK4ODmLSST85sRKJKaqqkF42dcUq1ApyYGiWKbjRnaOrkuHDnsXGhbytqhkExw3I9X2NsUk7FR1xOt2mqAgMUeeQgi3Eqw2he5auqDDt0DmruwBNQuSgvgLC4/drItTe4nICiUBxvQTxWzFeh+9g+m+0Rm40ha+mIT199B/G0hRpH7Hxxk5WrAHUHB5vKOyVbA8kc7CSuDDQsJlXEU0vUd/1LGcvRgxGR0tTr+SI0uSixtdSBJtm1F1BjF6o8gXC6WCgrxcE5b8WUtcrxmIXKUlSMyizOLSY8PkdmtXgHHGwswdRdH7eR8jnEyrrl1AP7zM4mzMZ1oteaZC2niNWzEp1oX88mcZ0+njjwFhkH6cv9QyKlsJ2Gh51FzZkPtG8bpxiqKa/es7M5Kk2w47F3nbaxczpFK1Sj7lJC1lIH94xxoBQctEPGinwhxqkODS7sOdaub0mYcgUWi0ZUQUeIpy7sWeXGjSWffqDafLA4RWHVRjbWx41YZIc40q4ApVmoJEuDkvca60KnpdwnQrGxrg/NXu6xdnmIUpakVjBvJZiJIioVST/is1980rVvFABRU7W5tjRuJSxfKRnurtDcsdQGJbNexPCi23hw13YsM6qUvKLQNYlT7NnEYJYMUa3E3GxQ29fMV41zlW3l+JDnUqSqbjyuvKhIRyXz+5qiDpP1CKvhkb/wOn927QXn8KxyUlX6DYhEGYYmZWxqnIqHJBjSSt79K+PH+K+f/ZhTJM/V8fuNLdmSSyGgSzfek4klPjSo0tK6bbm+t8Juo0WkLfMi4urBmlONZbG7h+rIRikqNiyvjFhpTkl1SapLevUp6/URjWiR22hUpGzV+5yv7fOFwQMMixrL6YRuPHMGJ9GUzeSI3EYclU2GZZ23xuucrbu8r6XVHBYtbk5XGOYOBN+bdrlxuExZOuBrSk1ZaKdqLhV2FENiiVo55TBBjyN05hTFulTUqnydyXih4jNpShqDzlNq1Xyhc+tV2q4e3RwRzV1qBRu7serz+7FQBDv1YDUGZCPNiBLclcUki80dk8J4y1A2jBt/iSGql5iiGqvaktYLkqSgHpU0koIkKl3IvVWMZjVKoykKTbee+ZBurSyNNKc0mjgqaacZiXZQcK0+phHl7M7aXknaTR3sLYymEbvPDbIa9ep6a40Ry+kUU1NoLLWo4CBrEitDTRfM4oRp7ibQbjpjkNW9u3OsS1pJxuGswXJ9yqRImRfu69VSOqOmHWis6ZyXJuecu/U8Opk69evHv+bHybA3gU0hIAjzh4UhU/J5WdyfVN5I6FKY/+2rHaGKSa67trbGmTNnvLJNFEgCUgQmCRgKk+DLOULX3vA6YXn/RYqp8L3iyDsej9nb2/NJ/AXg7OzsMJlMUErR7/d9frUQ6pxUn925c4dr1659hXJI7umkylB+F4AV1rssdGVhHKqjtNZeCSPvlzqTfFUh2JHwOwECsjC11nLu3DnSNKXb7fLwww8znU69ckdynWVZxsHBAXEc8/zzz/Pcc89x+/btY7kg8zzn7t27DIdDVlZWPHiZz+dsbGzw5JNPsrq6ymw24+7du8fCFgXOnCz3V1O1yvXkvRIeLVAvhDxhm4eq1/AcUk5ZKMs1JN9dvV7346derzMajTxMF7Aj6p9ms8n9+/f92BCIEyqVxBE6BBhFUbC8vEy9Xmd3d9dDOyl3mFcsLLscEhYqYEvAgADRk1D0pBpYYGuoGK7X619VGSv9Nmwn+byUMXzvyb4eQkjp03Kek8YO4bWkrC+88AIf+chHaDabzGYz7ygsOQVv377tYeru7i5HR0fH1LVJkvDwww978Cz5NB966CEuXrx4TIktfS7MWydzoADE4XDIvXv36Pf7HB0d+bB2uV4URR6yLS0tcf36dT/XDgYDms0mH/7wh3n00Ue5evWq7zthiLoo5US9GpoXbWxscO7cOSaTybFQe+nHoRmHzLfSrkVRcOrUKQ4PD/11wk0P6U+NRsObd4iyTuq83W77soXqNFEiLi0tMRgMjimDpR4FVApsbTab3tQkfM4sLy9z+vRpDyblOgKHQ5WpzGui7BWoKs8xAV0C8PI89/O8zJ9hWgDpm+LELD+HzxmZvwQEyhgInY3jOPZQu9FoeFh4dHREv9/384H00TAEWPIyCoiUFBoC60KH5DCliPQjGfsCIMM+JNBP+rr0PwH58llRsYZjQ+Y0GUdS3vl87tWoYT3J57rdLo1Gg9lsxtHR0TFgLP3iZEqBP+r42gGI2sEBccz04a6pU0o5ZZoib1WLrcL6BOnR3GJaTmUliyNVWup966GUd4+V52oFRXwIcwX6rILWXUvnVkZte4Q6OMJ2Wpj7O+hWozJ2qB60ucHGksPQOezqeQGlxSZqASclfLdSI0por8oFENmFQUgV1lw2jC/nYF5nbBNOp30GDxunFNMurK95L2Z4EeJJnZXXDPOuYr7h4GGCpaZKcpyasFa5LR+YOjObLIwTbEJLZcxswszGzGziwpdVSUfnPgT6ar7KxNRo6jlnkzEtlbGiMwwwswo7jTzsiqeWeAK6ND5MWZyDrXbO2eDqRMAtyqk+VekWrbNVxf4zJa0bMdmShYsTJvt1Hn78NleunmG0vcqdPdcf6jgYVj89Ipt1KFoO4kXzKrQ5UT5fX9GCeBCRTJzir8Ay7ylGDxW878k3+fMbz/FD//zf4t1f+PNkL/ZII9Dz0kGjeeZVZy4noV2ovaomM1WfE0MgVUFl+Zv0QxNX+bcKpzyFSj3XqEMVIi1A0CQOdBYNC7EDz2VNMVtWNHcdHCjqri71XLH34ob7OYdkVNVzCXpWQCdxkGIAeq4pWpZ4DOnIMlupchFG2jkNx9qPGZM445VkYilqkHU1ZcoivyFAFFW5CXGKQGNQrZZTCVZfMm1RwMoS6nBAedgn6rYdjI+Uv5bVChO5/HpY97PODSbR6LKq/wjKhgNbOqvGYAXwZKfApLoCvBZVqRpNPaJoRG7cVipFZRf3oaZzBw6thTRxP4PLs2jtIu+ihF+XxkFOpdw9K9fGy69ZXvrIFpvNIQfjJvPIejVpfU8RT2J0AeOLhQvFNMqnMJDQzHhmSQfOUCSeKfbfafnf/s1PAZCo4wvQElWpjFNSDCsVLNPA3bLGv13+B8zSGjZ2Cmd2a+iqv+gCoqli8oAre/2odEB57vKClinMlxVPde4wNjXeWbvDkan5694vVhibGu+q3eZOscyr47O8MjrLSjomwvDy4AyUipU/jI+lblClU7OFc7NViqIubrklS9fnZM+26T8TE6cFrUbGpZU96lHhw7MTXXKxsc/N6QoAT7TvehdogM3kiESVvD1fZzdrEylLO84o0VydbQBwd7TErJEwTmoYq9iJOnwxP0dhXFhzYTQHoybPHV6itjQjmybOFGacoOba5QSca+JJdT/G5QWsBUo9uWdIiOZunvQO0aUJUjWEUL1KVeBze6pjakGvnNdus8REVAYyuLGTVs8evTCJMkmVVzhxqtFoqijahrJdQGpQ2hLXCur1nEhZ0jymKDStRvWFtZaRFTFpXJBGDr710kXelUQ7sxiDYlCrM5jX6dZcrpjcOFfv3EQ0YgfmsjKiV5tirCLVJafrRwBMy4RuOvV9vTCaWCkPEg9nDWa5MwqLtWFcpjSinFpUMCocsBjkLjR9lNXo1acczRsYFJ1kxlHWIFYGSGjELifmYF6nV5/y9u4K+aCGesjS6OTkReRySeqSVziLypRPYfH142vrkIWStdY7ocoCR+CELL5Ct0VZTJzMmSfATRYHsuA5qVwKfxYIcPr0ac6fP+8VcPfv3ydJEkajEd1u99h1wS0mRLERAiCBnaECSt7/1ZRUspCUhPnys4Q/12o1rl+/zsHBAePxmFar5WFmv9+nXq8zHo9pt9vs7u4eU0QJAJNzhlAnhDKymAvBi7SBQKvpdHpMabK2tuZVNFIHoZFAqD5pNpt+QXzS/EIWZVtbWzzyyCMe5B0cHPCbv/mb3kWz0WgwmUy8O2ie57zwwgvcunWLPM+5cOECZVny9ttvHwvPlkW8hPnKIvPxxx/nW7/1W7l8+bJXTI3HYz796U/zzne+kzt37hwL8ZR2CxWKYX3KQl0W/dIGYf+dz+d+4R32cekzUo/SVzqdDs1m0+dTk/N0u12vNBTY3Wg0jsEpcfIej8dsbGx42BKWOwyRDpVkoYpIoF2n02F/f9/3mxCwhWpT+bss/mu1mgcCEjYdhsCeVAnLEY5taTPpmwKIRC0VKjnD0H3p3/J3gd4CEUIoKerjVqvlyyFtIeY3ofutAB1RikqZ9/f3uXnzJo8++ih/8Ad/4O9D2vvtt9/2quAkSTg8PDx236JUvH//vleUra+v84lPfII7d+4wmUx8PYYhmsPh0LtjC0SaTCasrq6ysrLCK6+84vN7hoYsAnlu3LhBs9nk3r177O7uHjPk6Ha7vPjii7z55pu8+93v5tSpUzSbTZ878fDwkF/6pV9id3eXT3ziEzz++OO+7l588UXu3bvH2tqaN7GQ/iHllnlH+oyMh+eee45nnnnGgy8J7c+yzIP2UMEuZiqHh4e+v4/H42NATMauGKwIsBMgJSBVoKq0s9baK0OlD4a5BiWkWjZIZM4P71PuQUChgEAZCyfD2aXfyAaAUoqjoyPyPPd5EuVZJxs50t8EDIYwVeogVPgLiBc4K3UgKui9vT3/XJbzhipO6YdSryGMF+W59ONwXMrnZMyGaTBkXIa5CEOVvNTjaDTy86qMLwGF4YbXeDw+tqEY5t6V8Gc5b7fbZWtrywN02fyYz+f0+31Go5E3bfvjHl9DAFEgmwNyJnELybxdqbVslestsxRN0FllqBIpl1MpWoS/FrGDIMnEoHOFLiJMulAoiuILAuWhdtdp3TUsvTUm2j3C7B9Cs4GazFBpiipKylZCWYsWqibjQCIW9NzlWFO5qRK0L85tkwWgVEZhWeR5MrH7myRmXCSRV1DCYFZjbGr0ojE6UyR7mqwDjR2YrcDqi4bBA4rRGU00t5AYSqsoUS63mjUYFAZFhst7KPCwrnJvpOJURQUaQ6pKNJbdssFu2SVRBQbNg+kOHZWTKsPPHH0D/+Mr7yUfpPwXH/wFsG7hr3PnKmyiSok4wzumCvMoGpXrdg3yriWaKur7MP3ogG85f5VPrP4hV7NN/t6VD8Kby8QTRfR8i2YG269eYCmuAJ2YnBQuvHylPeHgqEs8ttX5FZKQP55a8rYi61msdnnziu854P/x+D/j25uuPnJbMrEZ5b0G6QtNzIoinhni/sSF6erEKUzTpFKPqsDcxVb9wrVfPIO8ckfFGGykK9Wj9AlLmeJzL4LL68k8w7brFA3lIUG5mjNeBUqFqhRbVrvJJp4ZqJyTi6bF1Cp1YgTUwBypytQF9DwHGr7O5suW2qEiHVrmSy6VgM6rBMTTGSy1XB1X/XbhlK2IJ5ZoLuOpBBIQ0xdrUWUF4ZLYhQQfHDmCHkVwbwebpOiL55wSsSj9WFEFTn2Iu5ZWTjFp0sr8JdYV/Fceuuvc5TlEgU00qrA+P6Sieq9wnUR7WLlQYFkHCitIaA77XzFH6fVV7HiKqXap5VBpCqs9Bw4jVeX/VDS3c567e4FvO/8G19MVhqmhrNpm2nV1atIK5hWRzxUYTTRFp8SH+ld1XqaK3uUDxjZht+zSL5vMTMLMJsxNwq3ZCp9YfY6ZSfjDySU+c/dx8jJinsfMsxgzj1A1y+bvLcJKHdiylcOsYpouIJYHw9XmS9GynEkPuZ2t8G/8/A/S2HHKXZ3hVbD/wd/4eb6r9RYf+tm/wNZvzHmzUblDP5AQffPEhXzi5gUxBRKjoqyjqB1Z0C4VhXMFj31O2wfO7PHI0g69eMLEpBxkTYzVZCbCWMWb4w1e3dukkeYcZu5LS2Ej5kVMZiKmecLBUYuyCBarkvsvtqhxxJ2aQaXG8f5ZRDyIwOKd1rHQ2wN0QrO0VYoFNw50edyZUZXVw4UqtUYKedvNi7rEq6N9egvcM8OkbrPAxNXOirYuJDw1EFt0WqK2a+hcUbQMNnVzvoqNCxFXENcqB9ZpQlQrSdKCstDoyNBqzKknhVcCRtqw2pg4N20UhXFh0vU4p5vOyEpXf6OsRhKVLNcmPiy4E8+ZVvlDWvGcRBkf6pvqgsJEtBoZZ5tHTMuESZHQTWceFBZWk5URaVLQijIOswatOGNQ1Klpdw+TIsVY5VShyYxxmTIrnMnXR85c4fd2LtGrT+nEc3bnbWZFQtoYMClSDycneeoAY/V/Xkbcmi5jLN71WlzElbLsHLWJXm2TvPOIp5dvsZEO2Mm6TIwzDVuOJw7EB2D468fXzhFCFFGiFEVxbHEYhjSLkitMni8LeAE8ssAROBFCuxAmAB4abG5usrW1xXQ6ZXt727tcyoJGFilyLVn0yMJDAJMs2EQ9JPcnCpbQPCPMqXYyhFfeK/BNa83W1hY7OzvHQrMkR6TAxuFw6NVAJ9VVoZJLriX1ICAihLeS4ykMrVtdXeXcuXM8/fTTrKys0Gg0ePPNNzk6OvIhZ7JQBbeYFeWaLM46nY43MFheXvZ18/73v5+yLFldXeXll1/2gLbf73uou7e3x87ODuPx2LtwJ0nCu971Lh+uKfUifWRra4vV1VX29/fp9/usrq7y8MMP8/GPf5zt7W1effVVbt26xeuvv8473/lO3v/+9/OTP/mT7O3tsbW15VVVoSIwzPEmdRwqFAWoyaJVDumTS0tLXkEpwEtArYAAOd9wOPSgutVqeVOToig4e/asB8hJknB0dMT29jZaO7MECf2WsjSbTV9nklvz8PDQA77wngTYSn8VlWj4vpMKVlnkC0CWfi7q01CFJkpE+V3qUe497L8nVYMhoJQ5QqBGCD+lbALLJL1BOA9If5G2Ozw89HUU5sQTg5dwHglNVOT/yWTCiy++yIc//GF+5Vd+xecwlLYWqCJ5CZeXl/09G2PY29vz5YrjmIODAx544AHKsuTTn/40DzzwAE888QTLy8s+vN1ay3PPPccXvvAF/syf+TO8733v8+P3zTff5Gd+5mfo9XqcOXOG0WjEeDz20CjLMj+WpS7KsvRzsBgN7e3tce7cOS5evOjvfzKZcHR0xMWLF/nmb/5mrl+/zuOPP85zzz3ngZm1zs13eXmZq1evHjMLkRDZdrvNfD73YEaeBaLCPnv2LK+88gqTyYThcOjBpTw7BMo3Gg2Wl5e9g3Wapt70Q/LlSruJs7BAYNnEkjYNDYGUUkynU9544w0P8mTelf4hfU4gfQjXpM/I3CDKTFGJNhoNnx9Q5n5ph3CTB9xGjjhNhxsYJ42ZpP1k7hoOh+zt7XkFI+BBpag75fkgzxVrrQeMomqWcSRzeBjGfxIQSm5JmQ+k3aW8oqQO6y2MMpB5IoTlgHfWlk0hebbJvBeCSElJEOY3lDKFz0aZ15aXlynLktOnT3vlpYzFUKUcbkb+y46vHYAYVYBwbqtcZC6PoY0VNnBmNFUoZ9FUFK0K0hWQjGwFNlz4W5Q5M4e8sVAJyIJZwpS9EQqQjqBzu6B5e4J+6xa2VkOv9LCDEcQxenWZ4lSPolmF6crkXFiiWYEuHDTUhaiT9MIFV5SOJYs8YYpjykPAKZCqFYnKtAeKg1GDmU341YMn6bwNqy9Pydsx0dwwX4lJByXxPOLoYkRj1xLVS1JlmNmIuYm8Mim3mtxGdJWTQ5doZjYhsxE9PWFmE3R1zbFNuVasEGE5FR2xpOf0tGFi4Z8N38l/9+VvJb5ax7Qsvbc12+9fWqhsLGRLsHLHMD6tyZYgfm+fpcaMbm3GZn3IA809PnvvMW7dXuXi+V2u317D1FL+2hOf4++/+UE+93vvpL6nSfuQ9Ry8iKaKouFAr85cmwscjOYu7G5/2CIdu5+juSvMbFUxfqDk0qN3ufnsloNrLUtZU/zgI5/jbDzglcwpuu6XLa5lF6jvOkjLYh70hh+2lTqIKIq54L5BQKIDeFFWGQkkMeSFhyYmBVs3lA1RyAk8s9jhCLuxXDmMOriTbKeYSHJnuvvXua36v4VIoUuI+gpVKoqm9S7jUebUhbpw6jpRwEq+RF0ppMTZ1CrQ7RZ2Nq9MS6qco5WMV/IFiuOqKowPu1aRrt5vnTIvirA7+yBGJEY7IxJj3e9l6ZR9SbzIWaiVH682UoG5SXW93FDGuqqzKrdmpLyDs1MDV2NMK4xWrozKjS+BuOGhykolWlqfhsD9QaFXlrHDIWZnz4ViL/ew4wmqXsMcDVDdNlYemKIcA6LckF3p8sij93mjvkG/2SCPkgrEOojdflsv8rlWfcckiqMn3Jzl3WoT1z4rzSm38lX+83/+Z2k/16iUvu7+a4OSm//xMj916ef5gS99A2s/1cTWNWlpqTc1O+8rsbFhcipxfUnC4U2w0WGqhVt6AowrB7Kbes6wrNPYVnRvlhU8c4Wf9yJW4xFNHVWGRpqyXhnWtKGcxeRN5RV34tJeJA6ilzUo65rGbpVLFeUV5r1rBXeePcv9Jzp0GnNKozkaNigLjckjlLauyUrF0SDmbn0NGiVqkBBPXB5IVUKSg6Sp1jkudL+6PwCTRJUp1qKM2EXIsOR8lHKB25SwMeT1ClRHTuVsIkXZsBQtB/9sYhzo09aVt6rrOHWAr57mpHHJcn3KtEiYFU5ZN8tjksjQSt2CtpVk3F5aYjSpsdqekVZmKUu1GUfzOrXIOWJbq5jkCdYquvUZsTJMi4RTrQGxMuzPWpXqrmApmTEsajSivFLkOcMZgHqUU9Mlu7rNtEjoJHOWkio/YZmgK0ftwrg8rP3ZErPSfTVJdUktKmjonMJqltKS3ET00imDooa2yucpXEnHxLqkG89IKlfwmi5IdEmqCzITk9sqL2Gcc6o+YC0Z0Z/W2R20uZEsY6wztbl6sEqeR5SFcydX2vqfLWBL5ev/WBxy1TYAqYIzvQHb8y7b8y4r6RhjFeOywdla/5hq9uvH194hgE3CnGSBF0IaWdzJQkYWC+JuKSFbskCTxdfJxYgcokJYXl7mwoUL3uhge3ubw8PDY4uek4s6KbOEiIYqGFGoTYLNr5MhWyEckfKF0FB+liTuAhk6nQ6nT5/+ChWPgLRWq8XW1haf//znjymrwvDKMFQrzJsm5RPYESavL0vnzvnYY4/xyCOPcHBwwMHBAaPRyIe9SSijLIbBKV62trb46Ec/Spqm9Pt9tre3vUry6OiI8XjMu9/9bq8Q2dvb4+DggN3dXQaDAUmSsLGxwc2bNynLkp2dHQaDwTEVnNaaTqfDcDhkf3+fRqPBgw8+yBNPPMG5c+c4e/YsRVHwy7/8y8xmMzqdDr1ej3v37vHzP//zXlGyubnJ8vIy9+/f58aNG8fC1qRdpc3CHIYh3JK2DBfJAhND9aX8LO8T1ZEAJukDElop6qQkSRgMBh68hbkEZYzIAlmAowDner3uf5e/C0AR2CHh1qJ8lXY5GRoYQo0wVFAgiYBSgVsSxigQUsB+CD2kf56EiXI96asnVcShei8E5GFKBKnjcByEYyO8NzlCFVMIj8O8idIfwj5QliU3btyg1WrR6/W8GrZWq/lQ1FqtRq/XYzqdekWZKN9EmSjXAwdzBRo/+OCDXpUrqt/19XU+9rGP8eabb/Ke97yHX/zFX/TK3M3NTQ+MdnZ2uHv3ru/PAkdk3pF+FqqlZV7NsozNzU2uXbvGb/3Wb/nyKKX44R/+YdbW1jg6OiKKIn7jN36Dt956i+XlZT7wgQ/4uglVYAKdZANBVMUCuBqNBsYY3nrrLT7wgQ/4OUTm+rAPSh+r1WpegSvASMCvhPaLSm13d9erGcN5PdyYkfn/pPmHbCLU63WWlpb8nCfhvwLs5fqSl1EU2cYYD2+n0yn9ft/PF3L/4diUsaKUotfrMRqNjuWclX4o75NDfpY0BdKOEpYrIFT6e6iEDJXCYbixAEkZ9yfVgWEOR+lTJ12WZZ6T+g6feSfD8WUjQoC+zCsAKysrDAYDr9yXskq7yhwn9yTjczweH5sTwmdJmKZA3heOf3n2h/D2X3b8/wQQ//bf/tv8p//pf8r3f//386lPfcoX+G/9rb/FT/zET3B4eMh73/te/v7f//s88cQT/nPz+Zwf+qEf4md+5meYTqd8+MMf5h/8g3/A1tbWv3JZTOIWlqrEKT4ypzBLj6Bogrj5OkWbpWxUiqrMwaqyoVwY5mChdHNhs5V8RtQ2wcJQQGLzvmXp6pS4P0MPx9BuQ5pg7u+gmg3Mchc1m5O3E4q6Pma2ElewSGWFg0s1twAta3qR+7AEVbmvuosH6h9b5QLUVaFkURJZVOGASTmJuZsv82+tfYFn158ifese8WCIareot5rYwZD6So/uSwqM4ejyKfhm6OiSoYGBaQAZEZaOnlGi6OkpJYpcRSSqYCWaMTQl1/MVbuWrnE0OeTDZA2BiEl7NTvGTt7+FN146R3qkSB4b8W/+6d/nV28/SnZ/jdPJIXquvbumVpbGbk7/4RrTrZy/8tAfspN1eXbnAV6/u8nv3niS7lugnjbc3F4hOkho3lV86v/z5yjroHqGomFp7C7AVmPPKfaieaVwTJVb2AtkiSFNClQBg4csm+/Y4b946Jd4PD2kpTR9Y/jO3/9hkiNFtlkSzTVfGD3Ab/UfpZdM+PdXf49fOXqK39l+iFq/AoRU4Cp1xikk8QIg24Wi0uUyrL5YxMqbq3iTzgpu5U2NjS1FCulOTHrkFLdRldtTV9BNcguqAogdSFSFC4n0IflUec5yi9XOETWZutDo2oGi1rcML7q+Vj8oKZrOgbhoRpVTs1rkAVXOidpUcMROZw6cVWGPKju+znb3ojC1INwSqhDmqg8LZG82nInK/hHRqU2YZ5gq/4ouClhequpoMRc4NZaokbVXRppUV2Wq/Jar8RLmn3TtYZ3qq+o7RM5UxUaRz/2m1HEA6V5wYFO1Wi6MO4ogz7F5gWo2XBh2HLlcjnGMXu75+0QpbKKhrACRVqy8bJn/6YTTzQE39TKmVVLMK/Vd7CB41quUZ1WYqbStKKb95kkMy7UJnWhKOY1IB5Z0ZIinxjlfzy2pLqirGGOcShTlDIjK1J1DZZragT02/wnwnq1oVGxcOHNmfb8Qgw2UUyzPTQzaGQ85R253L+5lQ0JENHfKcBNDVDmGR/WCxn5c5c9cqIeVUeQdS/u2orljnOI2dX0zbyqiuSIZlZz5PcPwepdp132+VgFIVbrng84r+J1bdB45RaCVe6zC16NKRVsBVBu7XJ4mcvfgXIBdOHcydOctay7st6xbyrbB1kpUbN0Gz1GCrZV0N0bUkoJ5FU67UndfoJKoJFZOAZ6XEVEFyyZ5QiedczBtstYcE2tDPcqpRy5kuLSKzMQYq8jKGK0MS+mM3DiolrUdDDvdGZBqZ/zRiHJ/nlgbsjKiFtUwKGpRwVNLd9DKspkMyG3EH3KRRpST6oJhXkdjaUQ50zLx1wFIo4JGlKOrtBrtaM5q4r7AjHSN1WRMiSbCUKLpxjM20gGHeYsSzbRMOJUOqOucmUmYmJRElaynQwyKpFK75zZCK8vcxPTzJoV1RkECI2NtGGR1+rMGrTTjcNaktlIwmdQoj1JmqfFqUtl8E1grzszVtwGi1EHWODaIW7NA16QCsBMajLKU3bhNqgvneG01hdHo9tfjlr9Wj3ABLgsdWWQKGAmBVxiaGC4EG42GX9iJo6UobE6GKctRq9XY3Nzk9OnTPi+Z5CsTULG0tORzNsHCqVEWRZIA/+QiSBaXJ80bQnAIC9AkizMppxyinlhdXWV3d5cPfvCDvPnmm/z+7//+MXVVu92m2+2yurrK448/7kMwBcLKojIMV5XyykJMQuFms5k3D1hdXeXSpUtcuHCBWq3GzZs3+bVf+zWfh7Ber7O1teUX0oD/3OHhIQ888IDPHdlsNjl9+jRnzpzh8PCQF198kcPDQ5Ry+Ruff/55rl+/zltvveUXhb1ej2azSavVOqaOkbILYBbThvF4TJZl/PAP/zDLy8tMp1Pu3bvHb/7mb3L27FmefPJJZrOZD6e+du2aDwMUNeTt27dptVp+HVav1+n3+77/hW0TLlil3ULXTunX0hbSj6WPibK00+kcM1uR/iBhpgIRQ/OCXq/nrychiQLPxVhnNpvR7XbZ3d317WqMOeaSHapowxDAEHgLiJH7lJ9DsCj9PoRRAkBkTEsdCDgJlbxy7hC+huMhVMaG0DGcR8J2CMG+zBlyvpPjNYSWUiYZQ9LG4biRe5b6DpXOcs7bt2+zu7vL+973Pn7nd37HgySBOAJ/BUwIaBXYKKGk0kYCOsW84Vd+5Ve4f/++nyc/+tGP8uSTT3qzmpdeeolr165RliXPPPPMMdWduLWHym6BMmmaHtugCRVb8vt4PGZ/f5/RaOTrWu5PUiYIvAnnWzmXgDhYKMm63a7fMAiddBuNBq+99hqDwYDLly97ZfNoNDrmcC/PDMmDKrlABXKGGyQyp8g8FzrGi2mKpIaQf7KxIueRuVKcyQU4zmYz+v2+V+cJ7DyZO1MMiuR80jclFDcE3OFcJ3BMPhvOM/IZaTtpT1HXSRtJfxfHdwnhDXMoyngVFaGoZcMxc9I1OooiH2oN+NydMufIPQocDUG+zAFJkrC8vHxsw0TmIZkjRcEp/XZvb+8YzAzHeziHyL1LftBw00/eJ+OkXq9745QkSXweTcnNG+Zj/uMc/8oA8Qtf+AI/8RM/wVNPPXXs9R/7sR/jv/lv/hv+8T/+xzz88MP8yI/8CB/96Ee5cuWKl53/wA/8AL/0S7/Ez/7sz7K6usoP/uAP8t3f/d08//zzx2K4/7iHT+ZehfhGORhrmfc0OrdEc/f3eOJUY2XqTB/EbdKpsdxCeLa2CGlOhlAbmAWUCxfmysGX7s2S5p0Z8cEYleVQGuxohGq30asr2E6TslMj0lDWI5fTrlwswOUomynRJIMc1Dx30ENVAAhXVhdmrZDwZKvtwhEawFa5rRLr3l+pFCkVe0WHb2m+xV/5P/8yPzn+OFuf2cM0nRKO1Q5YiyoMajyjqFuSqmyJMs4sRVmauqBZXapvUiIsEZa+aXKrgP2yTVfPeFf9Bn3T5L+9/xF+49VHiQ5j6juayRnD6qUDNttDru6u8c9+8Ztp3bXkZ6AXTdDzCthgSY8U0/WEaArd1xN+7p9/uwOuU1hVUCZVGGOzAKNIh5rRBUvad9Chsa1p3rNOyWYVtYMq/x9uMd/cq0BDorwzsEkUWR6jevClP/8pAIamYGgVBsOzswvEE0XWtZA5hei3dN7g7934EAB/ccWFybWSjLFS1L5rh0gbip/edFCoNA4kKYXKi0VewaqNnYrLAS5VUCmZKiVXllchzFXOudyFG1tVOYX3dKXas6iljuuukXM/juYOokdzp2iyGmoHluZuQdaNHACKte+PzqnaAUFlKhfrmoNWptcia2unAqsUiAJCi3o1niSnX6R9uDCV8krMGqxSRLnkIT0hw7Eswl+Nwc4zVLuFmkyxgyFohW5USR/j2IF32YW1HFMcQqWw1A4kSniwv5RArpPziapClZXyAExgocshJ2rFxXxAWbqchsZix2PUxppzlR5P0L0l7HCEmWf+OqpeB2tQceTqS2uXyzHWDlQBnZtzPrvzGB9af4OX66eZJjUXtlyZYWRLlfQ888xjAUUNzJYj4snCjGc5nbj0A0b5lAjSjiZVbNRHlNZSFA7kmsh9rkwVRA6gODWjU3HLBQVo2kJXc5UDti40WXvXdINmXNS8ktbB0wqcR4qWnjO3hetfAegua1COEqbrevHZqr3zlpu/uzeKKmdoFbJdpSCYL7mxYSNFc7ekfcflwjSpIm8orzwvawqbQhkripaDgbpyGM46DgyWTacE1M0CHVkf0ttJc7IywlpFrzElLyPu7PXYWBmwVHOmKiu1MZmJvUKvl0x5a7jG3UGXp0/dJlaG3VmbWJc04wxTqeW0smhlKIwLEW5FGdMy8Y6/WlnONw9pRJlX3k3LlGlp2J+3KKzGmIjZNKEUAxGrOd0ZEquSwkTsz1p00jn1KGdWJlDCvIxpxRmX2rv08yb9osm8jLkz6xErw6n6gMJEaGXYLlLayZxYlbQiB9/7WYNaVBApS2EiCuNyABqribVhJR6znIyJsDT1nESV1Ct5aS8aE9UtO0WHL44vMrcx+/MWNV1QohkUdbSyTMuERpSTmdiHPBsU14araGUx1sFPYxXNOMNWSkuDUxpGGKLIYJoFveUxpdFE2hBVg8lYiCrn8tJoilJ7d21jFaVRWOv+lZV6cZ7HGOOU4ffvLnPYaVZTmfJ7Ba/eP0V9O3bh93/8lDNfP/41OEIAIMolUWJJqKEszAUInFQlAX7R1Ww2/b/l5WUfenpS0dRutzl//jy9Xo/JZML29jb9ft+XQ84fhhjLIjRcmIiaIwRzUtYwt9VJpdbJ8sBCCRGCDQGIy8vLfPnLX+YDH/gAH/7wh/nyl7/M3t6eh6jD4ZCjoyMfhijXPnmtMN+U5JOUsglgunTpkjc9UErRaDQ4ODjg2rVrZFnGww8/DODzg62vr/OlL32JdrvtlXCysLtz5w71ep1nn32WOI4ZjUYcHh5y9+5dHnvsMUajEWmasru7i9aa27dve3MTAQCyWBPjAuCYA7WEnNfrdW7cuMEHPvAB5vM5P/3TP82VK1cwxtDr9Xj00Ue5fv060+mUbrfr83JJKG+r1fJw6uDggE9+8pPs7+9z7do1bt68eazNw9DusD+GClMpr6hsBHAIbA1zJEofCXNsymJea02v16NWq/k2ErUNLNxBBSDK4lwUitIHrbVeeTSbzVhaWvKLael3MgZFoQj4PibATI4QrEkdyN9DGB7CLwEFJ5VLJ2FgeD9y3hAwnnz/VwOJolyS64TnO6kckjEgACNUcIbARaBeqI6SsoXhjdY6t9vXXnuNZ555ht/+7d+m1Wp5AxOpC3HElblDQnCLomA0GnmF4Um1qjEu3584/4o5i6jZREkq9Sz1Ln19MBj484Xu0AKnQpWV1IOUQ/qEvF/aIo5jH1YrCjqpJ3m/KPAkpDQEMvP5nPv372Ot9b/nee7DjG/dukW/36fb7R7LRymKNAnP73Q6Pix3aWnJjx8JdxYFu6gLJRem1N/BwYHP7Sj3JHkNR6ORB4KivB4Oh17RJgo9UVmG/SfM7yh9RJ4boZI9zJ8q5TXGeNApfT1JEu+eLn1PQnnDHLYCpqWem82mHw8h8BZYJ+eSa8nzMxw7oqSVsSBATuat8XiMUsqrHqVccv/ymXBukueePEMEuIbqy3BDMVRZSv8N7+ek6j4cqyfbQMokh4w/cKZq8v0iyzJqtRpnz57l6tWrDIdD/rjHvxJAHI1GfO/3fi8/+ZM/yY/8yI/41621fOpTn+I/+8/+M/7cn/tzAPyTf/JP2Nzc5H/+n/9n/vJf/sscHR3xP/wP/wM/9VM/xUc+8hEA/qf/6X/i3LlzfO5zn+NjH/vYn7g8ylZhhca54OrC+lCwvOaUhUWTylACp9QaUzm9LtReecedo6y7kNe8DY44cCzcyEZQO4Te1Zza9oToaIzdO4CVnvt7aaBZB2sxzZSyEaOspawvFGke8FlQpcGkkVOpyZcsUXJVzzYJExQlpEkqhY8ARFP9SYsKs7pWApSKu/Mevz+9xN/9ze9gpW8plhpOoWWdisaqyhG3kVCs52hgaDR1ZViPnFokt5q6KhnbmJYquF4sc22+SU3n9FIX2vJze9/I7167hN2pYxNLtDwnOUpRJVx6xx2u3lnH/sIqjQgGDxn47gP+3NbrdPXM5SFMq/aZO2iRjK0PL7YK8rYDfdHcUrSATFO7mzhYmEM8d9CiTBSzFU08hflGCbsRjV28Ic14UzO8ZHjn028B8PbPXXbmLHmEall+YXyWG/M1VuIR39J8i/9u7wP87v1LYCHvGagZn0OwlWRusYpiVNaYFgllA371qZ/iv9z+AH+Qb7huFFWQKy8csK2Ursq49sO48HtV/S3KYbqmqB9ZB85WlxGTGT1zUCWaB3kaC1wIvHUOxpX/gzM5GSkau5bBgw4qxnNzzEG4bFZfAFLXx0ziFGKmAoSibDNp5NRoMhaUgE8HoFRhF0pZrbGJ9iHO/l6tXRilnDQRkAnPuvep0kC7Bdaiux2nUPTuywaVJG545ouJN1RFgpRNL8pcgauvcECtFIVWga4AoarK6p2hBSiWwf2oSsEYgEkVx9hWAzUYQ55jDvtObTifY0dj9NoqtsozglJOqQgngKWbG668cZa/cOY5znaOODxqYRLrDBgU1A4t82Xlw4lFbYi2VW5HB6LnyrVlN55RVvTNRrL54sptYkUvnmAwmMJJLF2OWAfP0BY9inzYeppZPy+p0uVAVLHBxm58dW8aD1clZ2eiHMw54d/iy7MejdGVOteHilf9i8jSumtcv0wWn1PGbQrZyG0IxHNDOjJMl6twq6aimGjSo5KDxxKiDNp3S8pEcfi4ItsoiJoFSVoQxyVaWaciKyOKUpPnEefW+qzWx9SjnEmRehMOrawP0ZWQXY0l1iX1OOds84i5iTldP/JwL1Elg6LBqdoRAAfTJo0opxPPHIybdjnfOHRzSZkwLtxAbsUZ+/MmrQgyE1FYTT3KGWZ1ro9XGOepA5i1qc9FOC9jJnlCN53zyNI23XhGJ5p5F+hBUefOrIdWhlONoc9HmOqSmY4xVvNi/yyTPGGaJcwyF9I832ny9FNX+fa1V9nLOwyLOkdZnYP5Ot10xiivMS9jalFBL51SWM3hzIG0LI04yuvQgkv1HeYmQSvLejygpecMTcPn2E1Vybio0Y7mxNowMSmFiRiXKaV1OXprukBjyUzMvIyJKyfmRpzTTuak1Q7NgcBUq8jKiNJobs96Ln9hqckKZ/QiQ7LIIzcNmSpc2YLNNZSKgXwfKF3KB1UqVK5ciojYKXJVz6CHMdk4gkj6uIXU0Hg7pb7nwte/DhC/tg5ZcAs0DCGXLEgkIXq4EPhqyqKiKDg6OmI6ndJsNun1eh7AhYBjY2OD8+fPU6vVODg44PDw0BsFyHVrtdqx0DEBQifD3AQGnHRxlAWagJSw7GEIZqislL+HZZW8VQDvfOc7WVtb480336Tf73N4eOhBj8AeWQwfHh4CfNVQS8ADOlkcrqys8Pjjj3Pu3DlarRYvvvgiOzs7xHHsQxIlT9t0OmVzc9MDw7ffftvni5SchaJGvHXrFkopvvzlLx8L/5L27PV6fsEsnxWlzWAwYGlpiVqt5h0xL1++zLve9S4eeughnz9sZ2eHz3/+8zSbTXZ3d6nX6zz33HNMp1M2Nja4f/8+Kysr7Ozs8Nprr9Hv91leXvaLUAEcAhQkR6NSyoc6i4ouhEYCH8LXQ7VcURT+XPfu3fPhyQLFpU+J0kb6UhRFXtEkgHp1dRVw4HRpaYnRaOTz0oXGB6EiSYCCjAO53/l8fgwsSz+Ucst7pf9JqPxJYBcqek/CaulnMpYFFIlSOCyjqMfCcoTnErAQgshQpRnCv6+mRAzLJp/5an8PoX0IEiW8VoCK/C00ygiVmeHc9Oqrr/Id3/EdbG1tcXBwQK1W8+NExiDgwXGYL08Uf2GaAQFBMueEIESuLRsdAqEEUoWgG/AgTOpjZWUFpZxBR6PR8KHU0h/DDR65x/C+G40Ge3t7vmwheAWnNCzL0pvUhG0p0F7eG+blCzcRHnzwQTqdDrdu3aIoCh599FFarZavQxmnEq4sAFAcnMNcpjJmpZ5EkTafz48ZfwhMFpAXKgbDEGMxR5JQ3TBMW+r9JNCVjZ4w1D7Mbyv1Hc7xxphj6TqkzQV6yngKoaqcdzAY+DnhySef5Pnnn/dq5NCMSM4rfSMcN/J/u91mMBgcU+KdjCQQhbXAt3DOkHlI+mb4LA/TQ8gmjPRFqUMxApJnd6fT8SpRedaI2vfkxqBs5sjcKfOnlP/evXvH8jWGsLLX63H//n1ef/31Y6//y45/JYD4V//qX+XjH/84H/nIR44BxLfffpv79+/z7d/+7f61Wq3GBz/4QZ599ln+8l/+yzz//PPkeX7sPWfOnOEd73gHzz777FcFiPP5/FhMvNjHy2F1lUi+CkMzVdhePF7kl0JB1lXepVeVlrLhIEbRxAMOU4NoBmWlSivaeFAni97OTUP3+ox4dwg7e9iNNWxZOlVZs45OVsjX2uhpQd6tYWNF0UzI2s6kxGpQgfoHFuDH/aIp6trDQ698LBU2qlRhFijd4tnWqodGAEzQDihZ45RDe/MWh/UW537F0rx9hEkjUC4kNRplqHn1pdBa4oYiAmegAkRYNiODAfpGYaziWrHGrWyVvaLNUdHgv7/2AfaurWAbJel2wvK7d/nA5jV+595DzEdNsiX4q+d/k50zXf7b1od45swt3tW5jVaGusopUR6GKlwIZN52ocbxxELlwiwQNp445VHzRsJ82XqX7FlPk4wttaEhmrtclrZeEs1cXrXy3zzkR5743/nm+iHGWhKl+X/uP8ON4iFmqwpbKpSGi8kuP3njW+jVpnzbxTd4c7TBNHPuw/FQo46cgkmASC+d+kXsNE8oa7CkG9yYrLi+lZeoosQ261CU2CT2Dt4hTHHqNweR5h2Xl1BnFtVsYpULl1QlVSi2My8RAyEAVRiXd3Oj5373hhZ4dafkNlw42bqQeURNJ5A6Bhtbl4cxMz6c16T4HKBaDIWsG2/KQjQrIHHuwz6Eee7Us6qsPmucElTyFhJFC3hY9UMP5Iyh3NkjWl3GrC2hD4bYowFmPncpJpsuLDjMY2gC92cElFaA0FW0O7/kqPP56dRC9ebVioFAcqF8W5gYyef9Ide4dQ9bq6Eq5XW5s+eUiKVTZ6o0xUoIgNxvOIaBsh7TfS1m9sGU1dqYtJYzTRJs4UJz5ysqME1yijlxz1UG6vs5w/Op3wTJZVIpqi/Jogyt6mctGTGzJZhKlZe49jYJkGtMw6CzqMo5uDDwkHPYrHqgJW48Sxi0jRU2NnT0zNeZa6dF2a2CBEOErjZPlDeAMYkFoxhe0Mf6O7jypX3XVrJ5lAwK8maCLp3xSNZRpANIjyzj7xiRfbFD876laFre+fBNmrHbBGhEOYkuvQNvpCw3R8us1sd04jlaWWrplF4y8Qq3aZmyng69em5W0c1uPGNaprTiOe1ozmZyxEHR5rBo0o3dfCHHQdZkZ9bmxtEKo2mNu6MuRakpK0iplCXS7v+7aolIGxTQTud00pk/T6oL1usjxkUNrQyZidENS2Yiro9X6SYzZmXMMKtjUB4MzvOYV4rTRJFxarrKMMUYhTXaqee0xZauPaOhZpjVmZgUU3XaWelAoMaSlxGdZObrqBHltFp9DMoD1k40o64y6pFbzOQ2prQ5GkNuF19NEl0yKOo0opwIw9QmPkR7KZlirKamcx/uXJiITjpnMHfuzf1pnSyLKfKYclaZNClLvJPyXLFOuZlDqZi/2SUZKsq6c4l3z9pqGqrmRKXd6za2UDPolgPPSVJ6c5XJrY6TA2uw2kJUgUPgmNFZNea+fnztHmIEIeBDlBMCDkQBJCBGcrrJIlAWxuAWdgcHB97UQykXunbhwgVOnz7t1S79fp/hcOgXfrK4kMWLLOhl8RsqQkLDCFnwhbmZTiofTwIKAQMnVYpyTnmfqIUEhj3//PNcuHDBK5AEKIjio1arcXR05K8VRRG9Xs+HstXrddrttjcXOHfuHEtLS1y5coXf/d3fZWVlhVOnTnF4eMjp06ep1+ucO3eOfr/P2toajUaD/f19nnvuOa5du0aj0eBd73qXhyBS561WyytJBCZIWbvdLsYYDwfb7Tb7+/to7Zw1e70ep06d8qHP3W6Xb/mWb+G7vuu7mM1m7O3t8eyzz/L222/zfd/3fYCDa0mSsLKywvb2NoPBgNXVVdrtNkVRcPPmTb/gFdWoKI8E7AlYWFtboyydw7GopaStwgVxGMIW9j9RsEjIsCyERbFjjPFKqtls9hV5zEQpJufVWntFF+BBiSyCBdhIGcNwv5P9Sc4v5Q3HjoRCi8OtOJ7L5wSmSHlDYCbQJYTmYV8X4xcBI2HdhWAgVOWGdSufk7KGC3u5TggoTkL6UAV88nX5XeojBKnSd2X8SFuGOQLDz4cKyqtXrzIej1lfX2dnZ8eHB4tb8HA4PKY2C2GHuGtHUcTe3t4xsBvCb+kz0s5SNgltFxf52WzmwZyE8MtnZb4SwCxtIfOd1EGo1ArbWfpOaDoS1o+UV3KUhvUmOQTFEVnUeDJOZf4wxnDu3DmstaysrPDCCy+QpikvvfQSd+7c8X0ibIcwhH48HmPtwvlaAL4AsTAvp6QYEGgnYyyE7kopr/YsisLnPQyfHQKn5LUwjFneIxtn4SaU/C95geM45vTp0z58XfqpmBLJRlIIvKUP9Pv9Y3kXJVfs5cuXieP4mMmJtLs8zwR0h4DwZD8INw3CTYGTmxThs1XuP0xfIK/NZjNWV1f9c0HmitDkRWvNxsYGGxsbXL161atWBcjLJpaExwuAlJD1NE19e8k9KKW4desWn/vc59jZ2Tk214ebf3Kf0+n0K5TMf9TxJwaIP/uzP8sXv/hFvvCFL3zF3+7fvw/A5ubmsdc3Nze5ceOGf0/o0BS+Rz5/8vjbf/tv87f+1t/6FxeqUkhhnGrNxC48Mpm4EElx0y2aLiwZVYHGZKE2bGwvDDaUgbTvwtiK+iKMLZ7A0vWC1o0RevsAO55ArYbKclS3A2mCbdZgMqesVzsqrcipk2KXR0znC4BiI5f/TRnrl5KqcOGfJuGYYspWoYSqEKgCuqxUVMatS7wbp8DJSrWjSsWtYY9/79Tv8Bv/8V2mZczNu6uc/6ea5Ch3octGVkrQ606oKycNa+qIyJT8r8NHOCjavDo6zVv9NY7GDbJZjJnG6HFE65aGp+a856HrXFnbYPvOMr/6O9/kFFLnLMVFp3x6tHaX/+ix3wKcCctR0WSn7HIu3ffhstHM5aksGi78NpkYorwKQ586k4hkYth9V47NNCubA8obay53ZB0mZ6G8mPHUudu8/XOXSVo5qkzJW4r/28O/xeVkn9eyOk2dcy1f4xevP0kyg6xnsKUbPBGWepyz2RhQWkU9ysmymFQ5cKcz1y6pcmqXRLvJaFomlEZRtFzjDbMq1LY0lepNOafeABaZCKLCur5QmEX+Qot36ibS2HpShbFXUCd2OQfRgSKuMM6UQ8J3BW5V/2yyCEsGgdgWU+XcDFV5JnHgRpUCH6vPRE5pGMI0935Rv1pUWqkHUu0BvkkseUcxXdWVszZQOJWhajadUUwpbsrB9cZjdKvhgN+Nu85ABdC1mlP6VfVqA1Ao/4cmKj6MGrzqVgxdBFa5zQLrQon1CWMVpdzr4aJfBQBYIKAYvBgLwZdo3ajDfO7upTRQr2GPBqhWc3G+4HumQPHOnZI3p5ts1gbEcenfZ2JLMnLuw6LSK9PqwxWwsJFy+T4TB3CbOsPgYLG7r+DSFWBMlIsdtsrNm1lv8T491U7FjTMBqVLZoSpQp2Lr/+acgKuTGwejXfhq6WGuy1FZlT+GljbMKgXuMSOSCvp0rxuKmjo2P2ZLbu4+eDRyYdQR6HmESaB9y+UHNTFMV2PqfYOtzxk+pSkGLeq7it1pi3OdnJVkQk3nTE3KZm2IVpZ+3mApnVKPcjqJA3XGKppRxqR0Lr3deMrEpNR1znLsKuRe1qOmC06nRwzLOokquTlfZW5i5iZhu6jRjefkVrPRGjHKawzmdUqj2FwaslIf007mzMqEwriQX1MZhrQ8cNPcmyzRSebkJqKTzNFYl49QWSJlaUUZBsXBvMmVq2dQicEWFSkXh3erfH9BWXRiwIKODFFkISqxFrS2aG2pJTlH2zUOZw0OixZHRYOjrO4Vh4XVtJI5mYkpjKadzGnFGcYq5mXMSqVWL63GoNGBBNmg2YiGZESklGTVgG1EOUvRlIlKmZuEKZCVEUlq0MowNwlzE6OxtOI5bx6tc+vtdfe8lPkPnFu2UdjYEo8Vay+VTNcSDt7h6qJ2AMVHj/jk5c+zEo9o6jktPaejZ0QYenpOR5fUlWJZ10nU8ZQrvzXV/MW7/74DhxV0tFLPCjAKFZXezOrrx9fuIYsXUUGImlAUgWEYFOAXgrVazYO6MPxJlAVy3l6vx/nz51lfX2c8HrO3t8fu7u4x9USj0fDqIFlcCgyUMoVhVrLYD41GJIwajqv/QoWXLEJkgXISHoZ/E5iqteZLX/oSjz76KN/3fd/HeDzmp3/6p/nt3/5tX+YkSXxYqqjWzp49y7lz5zh//jzLy8vM53OuX7/OnTt3fN6x8XjMnTt30NrlfJTQ5DNnzvh8Ye12my996Us+l5uor1ZWVlhfX6fb7XoAIaG2a2trLC8vs7+/fwy6bmxscPHiRf8ZCUX84Ac/yObmJufPn+fBBx/EWssv/dIvAYuQ5evXr/PzP//zgFNCbm1tURQFd+/e5eLFi/R6PbrdrjcFkIWnQOiyLNnd3eXs2bM+TDKsa1FkCQwRCCqHtKX0K3GtDcPPBQZIfxYIorVmMpn40EgBcaHKNYTOoWJH1EAhQArDaKVNwjyLcFzlE4b/CRwI4WIYJhzmIwOOgfGlpSUP+uXzAvyl7KGCSs4n5ZOxGYLCk+BQyiM/S92eVCWGYF5ek/PJ/+FnQgXVv+jaUvcSmiqfFbfdRqPBeDz2c0Sr1fKQOVQ1Cgy5c+cOGxsbvP3228fyokq9hqGgMq9JuQUuhfcXKqm+mupSziObJgBHR0ce6EkorpRTztnr9Y4BJAHJoqYMczyG0FXGVmguI+MjbAsJnd3Y2Dg2HkUtKKpwcVc+Ojri2rVr9Pt9D2EHgwFra2tYa+l0OhweHnLq1Clu3rzpx1iYC1DKLJs1ArLDnI2Afz0cTyfvUzaRgGPnkH7YaDR8aPVJ4H2y7aQ9RV0u/SncpArVpjLPSS7TUFF6Uul+cryIilHqfGlpyW+kCEjWepGHVULoDw4OvkIxGMJzmavC+Ub6tJQnBOLh81L6rdRLWO4oirxT+L17946pc2U+63Q6WGu5desWa2trfv7RWvPxj3/ch/aHOT6LomAwGHBwcOChvUBeuf5TTz3F1tYWe3t7x9S/4X3ImPqT5D+EPyFAvHXrFt///d/PZz/7WU9Fv9oRTmTAsQb7Fx1/1Hv+xt/4G/wn/8l/4n8fDAacO3cuuKADLVFWmQkkLvxT5/iQ3qjGwqAjd6DQRpaiqYimDu4p6yChSanUMxaTQDJU1PctS28XNK4dYG7cpsgyorU15wibxBBHmF4LG2u0tZhUo0tN3tQkE1OFCy6USt4gpbCoWYE2xoUwAzaKKKqwVMmx5tV5J0L/rA4eErYKbaa69wo6YuBg0KKrZ3zq0v9CpCz/x9kn+Kk/+E5WDzJQCtOqYSPFfLnGpeWr7BrLzw/ezc/feifbN1dI9yNq+26hrp4+Yr074qmLd+klE3793sOM7m6CslzZ24DfWmZ5Zhk8ZDj70dt8YuMK7WjGuWSfoalzc77K9ckqbx6uc9BvYeYR/7TxNJ37LlzZakU6tuy9t6R5M8buLJRIeUsxvGgpe4bNtQHbN1boH7VYmVje8x++wJ9ZfoH1aMiFOCdRmg9GP8iZ1SMObMupGm3E/3L0DKXV/IcrX+A/v/HN9Hc6bJbWKTnHThmYsVi4GpRTH2lLPKly/VkHn5vahdZ14hmpMmgssyzxAHGcp67NI70ItVUK4ojOrZKicvmed3VlRuEUVMo4cF3UFw7A4swdKlJ1IerTCrqIi3cwlkK1y8J8x7q+V0FKqxdKO1sZeJgEL82TEHf863iXcNfvXKioU9gq5zLcaQd91PXnrGdQpaax4wCklvkqUN8ulH2uPqjVUEniwGtxYoJTyuUeTFyaABMpb0jjH2s+HLk6bZVCQOHUla6u7LFwXh92G0BJk1TmK1EwT1m8iYgqja93M12owk4eurfkxty9bQcPK6hso8irF/04V5AOSn5v+0H+rXMvODYa2UphrFwORFFRlhBLPs25rsL1le8zVsNS7MyPvsL91br7W4uHGGuxuXZ5D6vNC5PYCvJZ0qHbqCnTKjdiVc6iASqqco9UuQWlLrW1EFmvQPSh1rj6s9rlnk2ACOVTDYiLs9VAphlv6iqNAV65aFLnTJ+3LKbu1GF6ZU45jUlGqTNPqinytiWZwNGVFR549x3uN1o0dix3r6+x+bgL331zsM5mc0BuIhpRRjeecajd4nlc1NidtRlX6sR5EbPV7hPrkkHWwKBYr41oxXPGRY3car40d8ZgzThjf9Yi0SWzImGSJ+RFRGkVReHmmtmohi0V737ijoeVw7zu8x2Oi5RYuZyHiS5pYLlZRpUaz+UiLFFs1Ic+nFdMTepRgR5GmBZEHecWo7VBR8aDwbTqPLWkINGGSBtSXWJQzIuYWeGeT/W44Ag46Lf57eQyg1mNWZaQZVUahIMaNrKsnz/kTHvA4dzV33I6JdYl4zJlJRkzMSn90v2tqTNSWzI2NfbLNh09JVUl/bJJrEoinJGMsQqDop81GecphY1ox3Mfzt2IcgobkRsdpFKoFIAyl8Vux81GkA4K4qlmcKnaAe/Cn7/0Rf6j5TfJbUkkO9HWYjAcGAcP2yrh92cJ69GYRBluFV06esZnBu8hmmjicTUPFdUcJ3O2gfFWoI7+Okf8mjxEmSHKHMBDMFH+SAL+k4vvUFEkQEYWgbIQWl5eZmNjwyvnxEE4DAmUBW+oxAiVjwIwwzKHYWJhKKs4UAo0O6nUCCHoyYVTGGIMeEVJlmWMx2P+5t/8m9RqNf7iX/yLPP3007z44ovcv3/fq+/W1tZ49dVX+fjHP87S0hLWWt544w1efvllr7yRnF7D4ZBOp8MjjzziVXfz+ZxGo8HKygoXL14kyzKuXLnCF77wBfb29jh16pQPHSvLkv39fabTKXt7e94oII5jNjc3eeCBB5jNZly/fp1v+qZv4uzZs5w5c4Zer8f169f59Kc/zWAw4NKlS8zncz75yU9y48YN3nrrLT7zmc/wPd/zPeR5zvr6uleM3Lp1y7vaLi0t0Ww2uXPnjk+oL8oiCRMWRZWoSNfX130eq2azeQwgC7gR5ZaY1wgYDfO+7e7u8sADD/Dkk08yHA59f/pqajTgGKwLF9Dh4jtUT0nbyzlC1ddJFZ2E6YkKSfp/qKA9CYFk8Sz9LDzCvINh3xVQKqAlfE3uQ1SW8ne5jtz7dDr1QDaEBydVgHI+OUd4z2F55PxyvrAsMl/8UecN2+mkWu6kujQMtZa2kL+FbXmyTV955RU+9KEP8eqrrzKZTHwZpT2kz0m7CXQSyCr3La+HgEauIbC3LBcO3vJ++SdwUUBY+LfwGqKkDnNVhuGgorxut9vH6l7mL+ljoi7udru+Tg8PD9nf3z8WBizvPTo64uDggKWlJa90PnfuHEdHRx4yXr16lcuXL3Pjxg0PEB977DGfokCUsKLelbLL/YlyTPpGmqa0Wi3q9bpXpAlUgsUzRsawAEd5vsgGj2yEnD59msFgwK1bt46Ns3AukN8lBYTUgYAsuW64kdTpdBgMBn4uk39hv5dyy3iTjQqpA8kBKc/Tvb09D3PFvTpNU5588klarRZf+tKXeO2117xKcjKZHFNhzudzD1KtXRiPnFQgymZKOL/Icy1JEprNpp+n5e9RFDEcDn1IfDiWw+iDvb09yrKk1+vR7/fpdDo0Gg0+85nP+M0LGUvyPJ9Op+zs7FCr1VhfX2d1ddWPazGLkdD4ENQCXgX51fIh/8uOPxFAfP7559nZ2eGZZ57xr5Vlye/8zu/w9/7e3+PKlSuAUxmePn3av2dnZ8erEk+dOkWWZRweHh5TIe7s7PD+97//q143dMD5aodVLgeWhCHHU5cLC+vUfdo4N+Zo7hL169KSV6IfnUHecbkQo6nLsedMIyDvGuKxpnnf0jgwJEcZHA4csLAW0z8iOrOJ6bXQoxmmFlPWIiKtKOsKnWnKmiKeV0osXdEW3OJYF1W4JzjzjErZRVyptizHlWKAuKnqsoIAEs5X/exMYRwgQVmnNlGWbJTyRrbJU7U7lFbxYLrD4EFYfQmKpRp5MyaaG4qmZn/W4uM/90OUDYOtGS48uMPsfMzotzeYXsh5cm2fl97c4s6rmyQDBypowaPn7/P/euB/5cdOfYyz9T4P1bcZlg3uZUu8Pj7Fc0cP8MreKQ7uLaHqJc32nIdO75JEJRdbB/z6lWfQGczWDY1dxTNPXOOtVx6m/7Dm2/7M83zr0ut8tHGPtq7xn+88w3O7F9FTjW046PSDG5+jqSAHXs66/O/9p7EKCqNJRg5CXEz3+MXtd/L+1Wss6zrdZAalMyWxsSE5jCnahghDbiKMVYxtzLCoMRvWWOob8pZ2IdRt5yo7LRKGRZ3ManKrieOSab1KBl1GpKKAS6rhVuXr675wzwHDdpOd96/4PIjxpESZiLKGz4eItZVaq4ImsVOWmljUiIp4FkC4Cjbaqv/ocgESBaSZ2kKBJznkHGxSXiGrjOQIdS7OKi8XTsvKqcbmK4rGjiUZOQDsTVFs4ERdwZ7ea4rWdsFkvXIzNhadldjpFBVpyo1epeqr1HxKwXQGaYo9GmBnC0UfAJ1OZT7ixrbk9DTxQkGojPLhyd5xWVWGKJXqUBWWqLTeudm58S7KDi6XoC2tUzFXY1Ego84d5FWlwWYZuoKngHPFThKsLNbKEoxxeRAHQ69aVKZwOR+tRuFgKFqhM8POlXWS8yWnOkOGgwZ2qsEoGttO1SlKVO8OnBrQ0WJ+qO5hOR7TL1temRXlVYh8FTIcKUOJhUz7PuOVqbFFzTTT9TBPJT7sPcrA5M65OBk6OF3UXQoJWyqIDXVVkiiDSSFru/vL2s4RfXLKMrMws4bxliHrad8n8/UcNY1o3S+9EZaoFOcrThFsYkXZMdDJ+b888Yd85s7j7O+tO0VzpULM2pqNf265WjtDrQWt+5a4H7GUzoh1yeXuLq14ToTLt3eQtTjTOGJaJhzlDbrplPX6CF1NzNMy4Y3DDY7GDYxRvBWt+WYvS734P4iNt2VlvDGLXL48VbluzyJIDJ1k5vMlxrokrtyEU13STWaUVpGbiLmJaCYZNV1Sq0j83EQMcxeePC+d8UgzzhgXKapUkFjiuMRaRZFHFFm1GCk1E109L0zVWaq8f8QWyhObe00Dw4Sbh5uLTaxK8d59K2L5jZw7/06LR1e2KaxmViaMK/ifRE7lqY3lunH11dQZo7JGP2+wnjolQYnLVzisDFNKtFNwls4wRSeWwmp2Z21accZGbYRBcX/aoR7Lc7VSXBdqMf/NI5/3GIvboJE5MYZElfzieJnP9p/g1niZg2mTpdqMRpyzPWlzrtPnH1z4Rfpmhb/51r/BrdurkGsPKCNt6b4NtaPSbTqoxUZE1lUMOwVMgiSeXz++5o4wb5J82ZdFqizarLX0ej1Go5FXxYT5h+TLvIRgNhoNyrL0CfWPjo6w1rK9vc14PKYoCh8uF6qlwjCuUHkgi+MQOkieK/m8LFYEopxM+h7Ci5OKsPAIQ8jALWpkcbS3t0eWZbzyyit80zd9k184iWquKAo+//nPs7Ozw6/+6q+iteahhx6i2Wzyxhtv8KEPfYi33nqLRx55hI2NDYwxvPXWWywvL7O2tobWmq2tLUajEb/+67/OwcEB6+vrrK2tsb6+ThzH7O7uArC8vEyz2aRer7OyssLR0ZFXH549e5bZbMYDDzzAI488wunTpxkOh4xGI27cuMHdu3fJ85zd3V2eeuopnnzySa5cucI/+kf/yJuMTKdTXn31Vb7t276N+XxOt9v1C1YBe+Jk22w2vSOrgOjl5WUPQ6Io8gtoUXa1221qtZoPRRN4srS0BCygX5gzTdp5e3ub+XzulZGNRuNYnwwBm/wemiTIuUJAI4q2EAiE0EpUsCf7TAihwn4FC3MB6R+hkkagU7fb9SGuIWyRsst5J5MJZ8+e5fDw8JixhhwCQAQghq9LO4Vlkjo4OYalLr4a5AvfF4JAqeOTZQ/HlABVqeewHQT0hGotOUSlJUpSqWM5T9je4SHz1+uvv853f/d3MxqNvKMvuA2PwWBwzEQlNLEI71X6iJisGGNYW1uj1+v5spw9e9YrGbXWvOtd72IwGHg15c7ODgD9ft+HEYeAWKCpgKgwh2Goip1MJjz00EOcOnXKh1lLaO3169ex1vLMM8/wqU99yjuBf/rTnybLMpaXlz3Yl3NKKoZ2u83e3h5nz56l3+97g5K1tTXu3r3rFelf/vKXj829Srn8jfv7+95MRvqngKqwTSXUXvqLzB1HR0cergnwlLQAIbSVNhF35xAmiVmLfD68TjjHhNAW8KZhMk8JLEvTlF6vR6fT4eDgwJdPQG041sNzNRoNv6kkz9TJZEK/32dnZ4fBYMDe3p43cxKVdrfb5aGHHqJWq/Ge97yHO3fu+ByDMjdIPYTqTYGPsNjkAPyzVsZwuDkXhluHymnZrGk0Gn6zKtysk808ma9OKjEPDg6YzWa0Wi2efvppTp8+7dWLMpf+zM/8DCsrK7zvfe9jc3PTl/2FF15gd3cXpRQ3b970Gx5yDxcuXDjmRP0nOf5EAPHDH/4wL7300rHXPvnJT/Loo4/y1//6X+fBBx/k1KlT/Nqv/Rrvfve7ATc4f/u3f5u/83f+DgDPPPMMSZLwa7/2a3ziE58A4N69e7z88sv82I/92J/4BgDEMMG7oVZrtagKzdeFC2Mrc0XewJt1iDrRRlBEFpWrYzngwJ1zvqLIuhHJICaaTIjOb1HeuYfutLG1lKKdEBtD3kmq68du8a8WYcUmDhbi4EGGMhbTTl0uwrHreFY5N2JZ4Ah88YqvKrxQVQs9CbFe5EC0ATiobiTX3MzWeFf9NhpLV88o6w6klLXIGWAYRfvmhDdeP0N8boIqNHaUcOeF0840wUJ7fcy/sfElrh8u0zyTs9KYsFKbMMprtJM5//3+N3M4b3L1aI1fnL6DyaRGOY3pro75jguvsXJmwuVL25xL9rmRrfOru4/TTWa8o3WbX+cZl/Mvd0Dk+Zce5Pz1gpt/1vBfnf4thqbkfqm5Na/zpYMtmkmGzhUlLjfiraLLF6YP8M76TVajMddGa+gCpnmMztzibSUa0Ywz2pGDOYkuURUoInFmI3nP1VktKjhb65PbyBkm9OMqD+FCqdpSBe9be5sPdV7lTKz4L878H/yo+g5+Y+cxSmuYZglxolzIavgFoCixaQLGOKdiOawlnpXovAJ2GnRW+n4hfcfqRTi8OIiLe7H1+QCrt1d9w4fBl1Swyy5gYryAhiiLLhR5xezlPap0CkSBjNKfwz6uCxZqQqiA3ALElXVV9bXg3Eqh2i1XH/7+lDebATAHh66vr61CHDl346MBKo6cAhiXDsBUKkKrq9ynZQVGj+Uc05Uyz/2Lpq4wJtGuHkoXTu4VdFWoLbj79+G/KpgvNNi4Cmc01rW3HFnuphNrvRrSjsfYvACtXfoAayGJndFO+Oys3t9+2110vTHiRn2ZeRyDxuXtjBYbDcpUiqcKloqZlMyREYaLyS7NsyMOjMvNqPOIsmmgXvBoeo/dUvGd73mRly+d5kJrwFptzEHW5LnXH8RGlu71klnPbXKIMtjlHwWduBD06Yai3ndlMLHbvNGxYd80+PfWfoenvu+WCwXWcyJl6UVj1qMh22WDM/GU3/ue/5qmjqirmJpK+IF738AvfOFp+pdjyrr1c6GqVNftm4qiZbCx8dWXlxqTWkbnYemqe62sQTKF7psRg2+YMe3XSYZwd7zEVqvPnVmPbjKjEeXOvRfF3ekSsTJkpZsHkmRGYZ2bcqIMw2mN2SglbeYYceU1Cl3lE5Swc3ndReVaTMMSpyVJUqKUZaJrmElMokqW4inDsk5mnIqwqJSHGucInAPGVgmorWJ33mZ30mI4rVOWmrLQlKXGGkV0v+byY1pgP0G/7b5YqMT6VAg2BpMap26NLEk749TykDPtI07VB9R0QSea8Zs7D/P266exNePGVgVAiawDjrFzt48nJaZU9JIpiTLE2oFjwBu1tOI5hXH9elA06OcNusmMft6kFhWUVtGO5qwkYwZFg0I7laXBhXJLzsXNxpCaLujGU3IbsZzGHMxaRBNN65bLOyxjQ5Xue0H/cvWcVDhVdYx3ltfKsB4P6MYzrj57gfYN2I1cOH8ysrzw8AbDc5aOnnL7ygarL2ka+wZVwuh0xOCyZbqmyDrxwuwnmIP1KF4oD09w2a8f//ofoTLo5P+yoFZKMRwOj4EcMTkRQCShgaHSQBYUEva2srLC/fv3fZ5AWQgITAgX7eFC/qRSMAy/ChUfcogaLlQgCjQ8qUiU+wnPGyqaZGF6dHTEuXPn/LlF8RKGSvZ6PW7evMn58+e5deuWT2B///59Hn30UZ566ik6nQ7Ly8so5fI9TadT5vM5e3t77O3tMZvNeOGFF/yiu9ls+gWjOMKKcqNer3Pnzp1jrpuSj+v+/fvUajWSJOHpp5/mx3/8x71r5aVLl7yiVBbVy8vLDAYDD0Yk1DJJEg9AxF1bwmknk4lfkAtwDEGIAGEJzQN8br96ve7/bzRcEneBBKdOnfqKvGUnlXrWWg4PDzk4OODUqVO+vU4CgxBuCYwUCHkSJAh8k0PAhEAAGQ9wPP+cnE8Ua5K7UCCEgM2TisQQVoZtESpiQwAj55FwS1hAsvDcAhYE2khoq6jDwhDRMMT5ZP+X8p1U6cr7wrIKrAnrQ+onhBfSFidVg/L6SZVYeA6Za+SQtgoh31dTMgokXF9fZ3d310MQUfGJ+iosV9h+cq8CcA4PD+l2u3zv934vvV7vmCHJ5z73OW7fvs2tW7d8qoN+v8/169f51V/9Ver1OmfOnPFK3HBOEtdzUSEPh0MPtWSe2t3d5fDwkA996ENsbm76Mh8cHPDFL36RF154wdefKBQlx+F0OmVpaYlOp3NMzS05XgXop2nK/v4+p06dYnd3l3a7zerqKtvb24xGI1577TW+8Ru/kf39fWazGYeHh6ysrHjhVDhvS32GKtzQnXplZYV6ve7TLIQu22HbSx+XfzIWpc+JalleF8gbzuFfTaWqtabRaHDq1CkPLk/2qcceewxw7Ofw8JDNzU06nQ5bW1t+bghzTqZpSrPZ9MpK6duz2Yzd3V1+93d/19+nKP4FEMqmwnw+58KFC35OknEh1xOYHY4nCceWuSIcV2EYt5RT5nG573DDJE1TTp065ecSAdpyn61Wy495aauwr85mM86dO8d4PObXf/3XvRqz0+nw/ve/n8cff9zfwy//8i/7zYFWq8V8Pvf5ccW8RuYVGTNSryfh7R91/IkAYqfT4R3veMex11qtFqurq/71H/iBH+BHf/RHuXz5MpcvX+ZHf/RHaTab/IW/8BcAl2viL/2lv8QP/uAPsrq6ysrKCj/0Qz/Ek08+6V2Z/yRHGBIsC2VdOBdeq5wZB+5lkomBylU5ylyYnU6gtl/lJ/ThlPhcaTpXlAk0jizxpCB/zyOkd4/QnTblg2fQs5wy1ahmynwpIh2WmERX4dLagwsTq0XuQzE9EOHGNHcQyRin8FpqLFxVA+XhIuRPVWGmAhLx5iNOUSGhh9Z/VpWK+/MlOqogR/F744dp3tVQhQ/q3CncsiVnuPDvPvF5fvIPvhWVK5qP9Pn2869jqsJ0oinvO3OdW+NlDmcN7g26jCc1zqweOUdM5cLftpaOWNmYcL5xQDPKOMybrCUjrkxO8ffe/lPMdhuoQrH64CGPtLe9oiuauzK3345RRQ5G8Xau+fTw3fzbS/+cF6YXub63wnc+9CpvmQuowwQUPJ4O+dywyal4yMxG7E+bTr1URqRV+HpPZ+xMOoyW6hyaGbEy3jCitzoif2PFhfjhnEiX4zEzmzCrFr26cCrArKswsaWpSv7v61+kphKgwZKGw6xB1CoYmBmjgyZLuXWAyFaqOkDNqy9VSmFOhu2WVd7BtFLVCXSqRT7s1+ekKxaqM11akC8U1aI4KquuY1yd2ti5VUdzU0GxCljHeCML07DM1qzPdSfni2cFplapd4y7ZjyF7nWn7srbLqRazUvnlpzElDVVOQFT9X+Xt7GsVdDTgMoKlwtwZRmTRg4iz8sFiGzU0dWXTMpyUYf1OsSxC2uOjk9lylpsFaqrrMUkVdhg1cdMsjAfAQlPxjsTUzjVsLK2Wuy795tYUdQ18dSgWLRneNjZDBt8aY5WlrG9DvbGHVSthmo7hzWyRR9whahAouVYDkvJg3g7W+Z0/Yg4Psu8AsHNbcN8SfsNA6shb0DUyqECTPJ6BPSiCSt6xm+85yfIvsGKOSyJUiRKU1cxvzfr8P8+8yycgUi5jvZKNuW7X/p+bGq5/TEgyascem4iU4rF7xa6N8zCWEaUn5FlYmpcTkf8pe7tr6g3iBjZOXVVI65KJtfPTIyql2TvcHVWFhqlHAAy8wiupw4Ya8AoBkUdY5wZS7GWY24kzhyrDvMlRftuSfZmHZNC/cByb9ihlcwZ5y5MuJ81WKuPmJUJxioKnIquXqU0yE3E3DrHX1EYLncmXvmWRC43qpiI5KULqy2Npihd2bMixhhFUWjyeYzJI9Qs4kuHW0TKwcdRnkr1Mc0SznQHrNdHNKpw3VSXXBuucuONU07BrKnU5+5/m1ia9xSrr2QMtxIOH7c0duDgG3N+7tt+nBQHQRNliHBh/5GCulI0VURDpURKU1ZS5mFZ53pxBttwY9dPXIVGZQpmVV+MHEDt5w0yExMbQ00X9JIJ3XjKcjL24ewzmxBheXO6wWoypq5z7xQOcFQ2qOmCfu7+n5YJqS7ope6cnWRGU2ckuiQvI843Drg37WIallrfoguXT7dMFSqylHWFTayTqhOIQ5X0QpercyoJRZV7JkVzS21g6CtNpJxS0TYM07UIVTrzLpO61qr1LfW+WTiJV0fWVowuGRhFC4j49eNr7giVDOI+K4urMFeR5I0Lw6OyLPOunpOJyxcqC3BZbEgOpdlsxmOPPcb29rZ35JTFTWiCIouEENacXAgKeAnDswQuSC6sMEzvpBpN/p3MoxS+X94rELTVatHr9dja2uLRRx9lb2+Pfr9/7D2Hh4e8//3v5+WXX+bBBx/0cGE0GnlICC7J/OHhIYPBwIfqbmxs+IWRhPlKGPT+/j7vfe97fW6sdrvNvXv32N/f54knnmA8HnsopZTizp07GGNYXV3lG7/xG7l16xbtdtuboogaq9lsMpvNfG5KY8yx0L5Op8N0OvW5A0UNZq09pghrNBpMJhNOnTrlF9DSfhJat76+7p1lO52OBwCvvvqqz8snJhNxHDMYDCjLktFo9FXVcKHKTuCEADCBygKyZcEsJiLyfjmXAM+Tzp+y8A4X7OH7ZMyEIZCSQ242m/kwfunHotwRYLW0tES/3/ch36IOC/u3tJmM1RASAh52CXA8OaaUUj5MUSBiCDLDegzVkdL/pewhYJTj5DVDZWL4uRAyCHgIrxmOyfA1AWsSqiptJX0nVDyeVEBKeSTH6Lvf/W4++9nP+roUUwdx/JY6l/4TvibtPB6PeeWVV9Bas76+7vvebDaj3+9z+/ZtRqMRP/7jP+7LXJYl3/M938OZM2f40pe+5IGNKEVFRXf79m3a7TZra2vcv3//KzY85H4PDw/5J//knxyD33LvspHzG7/xG74PnDt3jo997GNcvXr1GJSXc1+4cMErKWVOl76nlGI8HrO5ucl4PGY8HhPHMVeuXGF9fZ1+v89gMGB9fR2ttVe8hW0g7X1yU0jcfcO8iFJfoeJYKeWNN6RfhRsXMp9OJhNGoxGrq6u8613vOnae5eVlWq0Wb7zxho8+lbqQDZNbt24xGo2OzQHNZpOzZ8+yv7/PmTNnfL7M7/zO7wRgNBp5lZz8m81m3Lx5k93dXY6OjvzY7vV6fPSjH/XKc4HYUi8S3j2bzXzEq8xTMl7yPPd1FhrCCOQOn7cyJkKFtMwvcm7pe7L5IXNdq9VCKUW/3/f5OqW/hapsOU6qkQEPxF999VUf0n7hwgX+1J/6U96IbTwec+vWLSaTCYPBgHe84x1e7Wut9WHbModvbW0dU3KHc9e/7PhXcmH+o46/9tf+GtPplL/yV/4Kh4eHvPe97+Wzn/0sncqJFODv/t2/SxzHfOITn2A6nfLhD3+Yf/yP//GxCfBPcphYeeCXjgyzpSr8LlqAAp1b77Qq7rHR3BLPLEXNvaesK6LMwZvhRbDNkmKqiTQkI8u9D7QoU7jwSxN0p03eTYljTdnQ6NwtUpKJ5Iir8jFWYMZUechEVWi8qgms1s6lt7SoWYZZbR0zNwgPDwWtg4jiqOshYqD28ocGSrg77XJkEv76tf8T175wjrWbxrs/S0hX0YjQM8VPXflG3vnoTepxTivKSFTJ//bmu8hnMf1HmuRWc7F9wBNLczbSAW+MT/HxlS9zLj7gdycPc2u2wqnaEW9P1/nlG08wnaakX2650PCHp+ibdbqP9RneWKI/aHJYNClrLrG9VYtclSio92YkyvD2ZI1mzy1i67Wct4brmNR9xiRVniqr6KiCiUnYPuiSrEBduVx3JoV1rTjTPuKB2i63CgcHVGW+M89jmnsWU8vp6TmXOzs8VbvFQ8mMn7j0v/Dxo3+f+He7lE1XruWndzkfuwfnyMz45/Mm+2WbL9/cYnV5RN8Y1ChGl8aFtsYBcArBU9jWWkOgHoxzZ6zi2rSqG3FBLl3/Eth8zM05XqjjxFzDRg4gmhgf4osFnRV+oSvu3rYCESqvxoSEJUtocwlilFrU3QI7HbrxpKxFrSxDWS2gBchbBVaANRRxhUZK62BgWfoyKWOx9RQ1z50RSasJpVMdEu6kRhriFjaKqpDlajGjdWVYY72Tsg9hNIuQXWUWIN/9sbpNmVMqBaPSoAvt5xNft8oZ4HijFmNQzQZ6qYutdsJsnsPuAarhgKedzVG1FIzFHB4SnT3tVJWqCrkuDCiNSSvoaaF2WPDc7kX+rxd+m19Rj6FKRb6Zs72Jzzso5beZhlHyFfeljDNJ2S7b9PSAjo4w1qKVoq6qHHC25FvrmU8fJzPyWBo7suhGgVILHi7/m9I59WLxGxJQwePSstYbsRKNuFE0SNSU3FrqSlPpMymtpQRyZZyRC1AnRuNCeK2FMovc/RrlALGulG+KhVmFtvTzJoXRXpGejN18byOXXmLe0fTeNMx6imQMh/stxp0hnXROGhVkJsJYF0JrrOL+uOsUiKUmjR0cLI1iMquRTVIoFEfjBntZTJlpyDSUTtVNqYhmVSqAUsYCxxTCqmFR1Ti++uoZd0/autDbyBJNNWtftuz9OyUPdXa9ik9XoFHcfo81mp8MIBlkxFMnGS1TxTMPX+dinHG/jGiqkpnVlChyNLnRPJIYtsuCzBqGNqFfNklVyXO7F0kGitr11N1PlUc1mru56PAJt0FgYsXa8pAn2vd47vAiOrJVLseUc/UDOnrGzCa09JzSaCIMjShnJR6znXdd2+scjXXmKMr9XfJP1nTJsIjBxDRMRK4iMC7Hp8aS6hI9U8xWnQLaj/PS9U2dUZlJVeSwUieGc2hudTW/ug22qAr1l4Fl7CLPYjR3oFLmmayrsJFLYWICBbCJQM/0QhX89eNr7pBFhIRYhul3wjxcwDFVmYQjyQJTFv/NZpNGo8Hq6iq3b99GKZe0XwBUo9Hwjqjb29tefSYLo7BcoetquDCRxZIorMQIIAwxPDw8PJYX8aQSKgxjDiGJwAM5vyxgp9Mp1lq+6Zu+yTsk37t3z9+71FWWZdy9e5ennnqKZ5991psnrK2tce7cObTWvPTSS+zv7/t0R6L6sdZy/fp1r6QZj8ecPXuWd77znd7Renl5mdu3b3PlyhUuX77sodzh4aGHERLuGeYiazabvk5rtRq3b9/2KkTAux2HRh2ykBVVlDhyCsgZjUYeQM5mM27dusXTTz/tQyp7vR4HBweMx2O2trZ45JFHuHPnDnfv3mV7e9uH7rXbbR/CfebMGXZ2drh8+TJXrlzhscce8zAYjqvUZCEZKu1ChU0IroCvWLOFkCzM7yWfC40IZLEv6jCBqKKQkc8K3JJ+J4t6uY70demb4fiSMkvIYhhSGOYxDAFX6JYLC9Wh/C7XlzrJ89wDgTAEUz4X1u/JcSfnCyFf+LlQjRiqiUMgEH4ubKMwn5yExgvUkPcJoJU8m1If4lQtsCXMqQr43H1/9s/+Wf7pP/2nPqRyZ2eHjY0N8jz3RkhFUfjcpGIYIX1MwnKVUsegiLRpeD9HR0fHAKC053w+58tf/rKHNSGglLbudDrcvXv3WHuL2k3UaYeHh8f6eVhP/1/2/jzo1i2/68M+az3Dfvb8zu+Zz53HbvXtblrdrRYaQAYMGApiDCkEjqFwUpWYxBUnKexUoMIficuxnbIMmIqJA2VSDJGxEAhZAtFSS2qpJd2+Pfedzr3nnnumd9zz3s+0Vv5Yz2/ttXdfsBSnKqWbfqpOnXPed+9nP8+anv37ru8g66a0hawt3W7XAz+hrFX6QvpNQKhQjj0cDj3rrCgKzs/P/QaQ+JEK6BhKr1utlt9gkvETBgM98cQTjMdjn5guPrDyPNheI8NxKe05Go24uLjg0aNHnJ2dcX5+vpFyDvhgp09+8pM8fPiQk5MTP27jON7YDJLxKvcgmxn9fp80TTk8PKTb7fI3/sbf2JA+C5sR4J133qGqKm7dusXR0ZFvYzmPrMehTFv6DPCbNTIPQllxGDAUboLIOJRnW/i8k82EkDktY2sbwA9Z2qPRiLOzM/9stdZ+oEVfOJbkEEC7rmsWi4WXt4frhayN4QaNnCe0GwgZrWF4zG/l+B8MIH7+85/f+L9Sir/0l/4Sf+kv/aV/4XuyLOPHfuzH+LEf+7H/oR8P4BNUlWm8D4Gy66Sm6cSSTo1LYc4cACB+WPHKboZVWAeA6AqSicbqhHJoiBcRyliqDi78oZVQRxEm1VTEDvRomE0he8g1yBrEAbycVBlXcDgZs/FBGQBVN16DFE3/e0+62hWU4julBBiR8zb1kGoIb46ZZFEo3h3t8Ufe/J+797UtrVFNOUgxqfbnqBPFwWvAl7t85XO3aR0s2ekt+fnRM6j32xx/9ISnO6e8Or5JS9dMbEZuYhJdU1vNed3lqfSEK/GYv/Poe/nKN25z82egfj4mWsHBV0ruPqGp9yv+2kf/Hv/TN/9t6lLT0pUDDxsrqWQG2aVbrHrtnEQZdhKXDh0pQxzVnMx7mJYlO9WezVGj6WqFNoZ6EWN3DNU842hlMElEiWVVucK1oyp0Ez6y2lcsRxnDufMIey5J+Y+ufBlXjbuFejpu08lcMnE60qRRzf/60fdSmJiXu/f54uhpPj18BzNN2Lu24MKkxAuFElmvblimVY2NI8ygTd1OMK1oA+xRlV33t6EBHyNsqj2wajUUu4bufeULX11ZJ3mNI0ykMKklWinqzFB3cAyh2FBnm+PEBQY1bMYY0pGmd9fJUPN901gDKMRns06EqScA+LrwFr9GO53CwR4mckCjWuLHqwD4bg5szuW63VDImxRjO19gixL76OQ75r2KY+i0XZt65iWNhDl4XQN2ylzx7L6QKCFf8JTyqdJyvvV/HFIWnl/9i9bbQM5sFnO0sA6XK9CKejJB376BahgmFCWqNugkwnYSt8EQMPh0Ybhzf5+9p2a005JZbB2QpkBHFmvBlIFHghVApAnKKWFxbHkhfcRe4+0QoTD/PTSo2hrPAsQCpcI04JXKdQPIKG8ZUdwoQLvgF5M2kqDCja0fufY6n0yj5nxdamv85xsMU1NwUWvumDaljYiCa7ss2sSPU3rvKQegB2tj0W/6KQKMIk4r9tK5A9Y8k9wS54aiH7nNnq5C19AaO6l6cppQ3dao2r0h1m48jYs27zw6cKBg3qRgCSUwtgy+mtIfW86/x1LMuvTe0yyPLOVhhcoq4rQmaxdkSUUWV0TacL07Zi+dM4hX9KMVvWjF/+VXfw9qFvvNHvkcG1tsYjGVmyuF0SSqZmqcL2DV3KDz+bSoUjdAZeMLmaznhGw6KAs3OiO+sLzKj59+ktOV8+usjGa0aDOZtvl7n/vr1LT43775x7h752jNbowsURt2T4zfeLOx26CoWwqbGm9LAM7b8Fb3kqRJq6/RZMqNP/nbWM3MZJ7hLv6PZXNvw3jJuGqjGxm3DIvKaB/8kjSTeVy5L5uFiTCppX3q7AjqVK1tHHBMVN0EPjlrBrzVgVaGTJW0ZHGybvzWicJ2XefXFhJVOX9IGi/Rym3oqRrihaU1ttTJerwo41QP8yctLNRG0NV3jw/PIV/KQ4lSyNQTaWroUxayUyaTyYZ0Sc5zeHjIrVu3ODk5YW9vjwcPHjCdTmm32x6QEkBDWI8hOCHgSwhihEWEXGPI5ABXeAnQA2sWzDZzKvycsOgJfw5rwEQYRkopvv71r7Ozs8O9e/c2fKmMMTx8+JCf//mf5+WXX/ZAk9aa559/nrOzM8/MEzBCJL0C3h0cHLBYLLh27RrD4ZCjoyO+/e1vk+c5k8nEM1MODw+pqopnn32WwWCwIXkUaXCv1/PtmGUZ0+nUy8an0+lGcqYAN1KkC6NU2Gqz2cy/dzab+euP45jlcslsNmOxWNBut33S71e+8hUWiwWr1YpnnnnGMwAlQVo8046Pj32hCq7m+uhHP8r5+Tnz+XwjzCJk1dy8edMn1y4WCw8arFYrL3sLQR8B00QGLsEws9nsO4BI+beMbQErQlYU4FmDwpgVP0hrrQ95CIv5kAEELgghBHKlL4TtG4IT4RgN2XECCkj/CNMwvKdwvsm1CEtY5vQ2eBjOuZB9KH0Rsg/ltdtzM7xeeU34Wvl9CHaEnyfrSugDF8o5BVyWcR62SbgxcHJy4tmzAjxfXl7y6NGjDZBU+jYMcKjrmk6nw2AwYDqdelZjaOEgfW2t9ecXsC4EjofDIU8++eQGMCKff3l5yWg02kgWlzVZfOJ2d3fZ3d3lhRde8CCyMNgmkwl3795lPp/7sA/Ag08SqiTXItd1fn6O1tpft6yh0r+yjglwJBtIcm65h8Fg4P1ZBXg7OjoijuPvuCalnHxZmIUAp6enzOdznn32WdrtNlmWefBwm4EY2gYsl0u+8Y1veF/Xs7OzDSaqgOz37t3j+eef958XslyHwyG9Xs8DgrIh9Pjx4+8IHer3+55N3Ov1+MQnPsH+/r5/ziRJwv7+Pm+99RYf/ehH/bo0Go348pe/7JmW4icsgNje3t53jEVh43/QMzBkcm6vH8Iw3QYY/0VzcZs5XJYlaZoyGAz8mhUGXoVM0u15K58jfRSC+rK5EW6sbG8ASDuHHpfyOTdu3Niwqdje4PiXHf9fZyD+/+IQzzNdWh9Oklwaym4TphDjGQB1EzghtYHzRLIkC0vRc4nJdarZedsZoE9uRU1xbJugCCj7KSZxPoVY55tmEt2kLDfhBzTBKQ3gIKmhcqiGpUPlwEO1Kpz/mV3LLQVMg4bBKO81DkRURq0LTfF5C1grktSKAr3Q5L+yz95nz/j0lff48n/yCro05DtxE8Bh0IWlyjSrXc34pYpoUGLu9DinR/89RTK3nN/q8lr/BrE2zKuUH9p7ncuqy888fpFfeXib0b0dvucj7/KVN28SjWPSpULnFe0T6+9fRRbdKp1krgTdqnwB6NhxMHoeisOa2/8Q9jtz6qb4BJhVLYxxFDm9l2MedlApLJq2SlCsbEJ8ljj54jxB184rbWossa7JlJMivty7z89FHyWdWH7ko9/iV974GD/69K+TKNfguS15XOfUFlrvZOQ7Fr2fE99tc/rFqxR/4CHdKOdz7bd4Oj3hvz75LNFCc7t3wb1qD12CSRU2TRw4JZQtYHmty8WLMfEC4rljntl2SjFM0JWlGGjiRuZLEmMiJw+UsSzATZ00jDkFXIzhYAcrjEIDyVTTfuwStFd1QueR9Z6JNlG+eBYQ0DRgQNVx6ba6NB5Us8q9PlrhfBJ7jn1TZ07C3FhLYsvKMS+ta3cbKT+YrYay44A8XRlUXWNXK1SSNHPTulASax1rD7wXoEoTbOGABxVHjo1YR9gk2mTdgmfduTVBfPO2wMUP2HFRFT7ESLwQfahLM3e9FUGz/ggVTxkLnTb1vfsQRWAs0fUr2IsRtqrQe65Y0UpTv/Uu0e4QVBO+0smou6kPf/FYoAKspX2nRfoDNf1WzqkBu4yFwOkAo1KhK8fuLPbqJnTHMaDiJVQ9y8S22KPkcZ3woFac130MmkRVXjb6oNqlo3OmdZtIGXaiBb8+fxJiS3IWc/xrhjp155VwCGUs82uaMrJN+wUga+0Yan/7i5/FfEbxd7/+SaxxbEVba2zp2Hqkhv/w+/8+1+NLv1ZkqqS0kQvR6BqyS0XZU9SJA+aVsei2WoPBBtpZyTBeEjUgIE1aednWPvAHLPHS0BrXVB1N9/2Yixc7XB+OKeqIQep8EI1V1PPYMR2F/R25Oaki56278/qCxZUuyyuGKLf87/7IP+AP994mwsnCdbNuGQwaTUnd/M6tMVNT8J/EP4K1DSivQAWD2aWtK5Rx1hDGKtoNCByrmtJo0nPN4E7zTFE0QUFw+nHVPGvMmglnYTdecCUecb094us//iK9B4ayo0gr6A8V55/p8nRyyd37+7Tvx0RLKHYtxX6NrmB+VbuNEZrNgMo6Zt9qPR7qxt+wpd36PgnCUDpqHYbU0iWPywGjssNROqEfrXhYDOlEBSuTeLblsk7oxjnGarQydOLmS2gDLOYmdnLxBiXUhWJxVYEJwFPTgMlzUR1oin6EqiDK1QYbXDfrVd2CqnLtFpXWew+vbNKwTPH2Bs6D1qkZqqxZ5xrfVBM3AGb9L9l4+O7x2/6QL/RSiAu7TIzopZAMWQmhB5MUcSGTaDKZ8Oabb3J4eEin0/GyL2E+ifRVCiuR9IksSwrn7c/9IAZZKEetqsoXuVL0hUyfUFon75P/y2vD30tx3O126fV6vPPOOzz//PM8++yzAPzyL//yBgMxfG3YblmW8fjxY++3JiCEMFqefvppnnzySdrtNv1+n/F4zIMHD7h37x6TyYSHDx96SawAZiK5k4K41+sxGo2I45jhcMjHP/5xXnzxRR49euSZMRICIJLQ+XzOcDj0xZ1I7ax1YQSTycT7qL333nseCBCgbLVakWUZTz31FMYYxuMxh4eHnJ2dsVgsWC6XHBwccOXKFT7ykY94b7goivjMZz7ji38B/4TlKecVzzrpp5BxlyQJV69e9amy4skpaaUyXrZlydu+c3JeGfNRFHmmlrDSYM1eTJKE8Xi8EbwhQIsAfiK9a7Va9Hq9DbaQsMH6/b73kBTwOQSTwrH4L/pdOHbBAa8hAzP0KBNQVdoJ2JB6C1MtlEXKEQKWMv/l/dvM4O35uX3dIWAfvl7ANvFBU0qR5/kG0CbjVOZUOB6k/T+IjWmM4fT0lLIseeGFFyiKwoPuu7u7G/JvAS4mkwmwZpWGElsJfwr966S9hV0na52cezwes1wuOTo6Yjab8fDhQ8bjsQ+iiOPYA4tyrpB9evfuXWazGc8//zyf+MQnNsKn5BpGoxE/+ZM/ybe+9S2eeeYZDg4O/HWPRiPSNOX09NR75QlYPh6PGQwGG7L8sE+kjYWlLOBhr9fzkuWiKHjyySc30o+NMXS7XU5OTri8vNwArGTcgwuy3d/f5969e3zqU5/ie7/3e/nFX/xFDyjK5sRyufTPKVljjTG88sorXLlyheFwuAGYCWtU5nA4FqVdRNa7t7fnAavVarUR6CIbCjKeJAyqqip2dnYoy5IvfelL5HnOcrnk+77v+/ie7/keZrMZ7Xabv/W3/hbGGIbDIdPplKOjIw+ohuvONhNQ+lXGmfTHB4H80t5hwMv2Zp+cN1z7tudrKG+u65rRaMRoNPKs3/A6i6JgPp/7OSIbjQK6yzWGFg6ysbMNZIa2DNJ3w+HQA5fyWvk+8P+JAvi3P4BoYbUXYVKI5o5JiHYgW9TUKHWqXaFZWtKZKyTEE0lXFmVVk7oqIAk+9CS7cJ53AgpWbSdpKnsRukltleKhTgQsdAXt2uvQ+lAUIVBYBUVfkz2ymDRGJRH6fAKRpuytPc0824Xm301NrOtGtmbXL/CyykgSehuG5VLRPnX3OBp1+aV/9gn2LktspIiXxrejss6jKf+BKc/sjaiM5vD2jF97/Um6vxY5iXZS82A2ZLzMGLZX/J++9gfRvZLDf9KivK65/lbN2+88Dc9UpJfaSQcLx2KrOrA4jsG64AC55iwrfZFoG3bb4Scek2iDzvfpJTkGxdV0TEvFPM4HjMcddGRRyhIvoVG9EWHQSjnJ3UQRPZWTT1tEK4NJNDfiFn/55j/kuSQjUm1uxN/irx7/IKvHXa5lI0wMJ0WfH7u8ze30lNNqwM+PnqMXF6QjN3aStCJawvS5kh8afov/w2t/iH/nM1+gZsqqjlGV4qn2GW/mxyQT5VJuixKEVRdHqKp2wLPI3K2TEruQloa5VdomJbdah3IIsGwgO1EeCIzyRl5najDWsYEaVqrx3n7N+0bGA4GOtUiQFrrNzHNggEkcEG0iRdW2tJaO4etB4cqxbuKVhdqg93axkXYejpUKmLfuHuIlVO11SrMaug6ss2YRNg0jV2tYrtYLe9ZapxlH2n18y4FuVS/ygSgyj2XuWAG7BFRs2j0KvttZ1dxjI1kU4p1PGo6EiWnXbMTmM3RZe2apqg16dxfKAnv7GnbuWId6bxdbliitHZjY7WCL0oEOaeLOaay/RqVVwCpW9O5ZtDJc7Uy4Ex3TuZvQe98iVm1yr6tDRbHfADs1PlRl+Ibim6vrPIgW3C0OGEYLxnWH0ka0tGuIf3P4Vbo657zucWoHnBV93rIR7y13wUDdtkxuRlSNjF/6VNfOK9ZW2oHajX+kalirJoHelRm/cXGL43/U7LrGkF3W7rqVZX4l4YkfPuNKlPO7/t6/RzLVTUAQzJ+oXDjLod7yh3XjMZk2myYWDxzWRm/YVigL+a5L0W6NLK1xjS4NOle0Tw2PLrvc3rnkuDNlXqfkTQq7O5lybDPbtHOlsQF7VgI6qrbij/bucK+OWJiETFV0dMmjustpNeCV1gO+URzx9eVNxnWbvXjO3dU+Zp7QuxcRL9ZzCuMA7NWeohg6QDnWTsoba0NuYgoTk2hD1XEbV3VLEsjXc9c9z/T6WWIdYy9qAlmqjmPX1WkjvdUQKeNk66YBZ7UD5MSOoPvAeDm/yMTrFi6Mx7qQoiSqqVFkuqSjC+6vdkgbMHFhWn5DqLQR/WhFnsTkJqGjC1q6Yly1mZYZ3Th3LPdmh0Mr5304LttNkE2T7qxqJnXbj2WTWDoPrd88kJR2E0HVdWOm6EfMjzW99yyrQ3cvwnxNVO2epY3NBaax1WiWmNI6YFmUB8o0f0rl/WTjFejKuE3OZqNvdktthKl99/jwHMYY73sn/oXgCm8Bc4SlJF/y0zT1RZmAfVI0i8wQYDqdslwu6Xa7nhEhxujCiGq1Wp6NJoCkFFVyWGs9MCNFjFwL4NkgIq0V83w5T8goDIso+Vl4hECifG4cxwwG7nnf6/W4desWvV6PH//xH+fy8nIjHCRJEj73uc+xu7vLa6+9xp07d9jb2+P555/ny1/+MoPBgCeeeMIzO5599llfkK1WK89sHAwGfOMb32AymTAcDj0YcXFxwXg83pCyhQWjMAJ3dnZotVo888wzGwb8eZ6zs7PjWUHWWnq9HpPJhNVqxXQ69W00GAx4/PgxV65c8cVklmU+YfPs7MzLKIWtJeDVnTt3eOmll7i4uPAsHgFDhVGyv7/PlStXWC6XXF5ebvRRu92m1+tx+/ZtRqOR/13ILBN/sjt37jAajbh9+7YHHheLxYYkNgSORZ4pDKIQCJTzCwgdjhOR4HW7Xebz+QbLUYr3bbZPWMjLNQvwk2WZT5EWEFrmmczFkAEnxbdcT8imFdavgIXiI7otSRbARe5zW0IbSmnlvraBBpl/HwQCyLE9zwSYCFlOcm4Bk0KvShlrWZaxv7/PbDbzqcUhq07GikiJtyXP4fWdnJwwGo146qmn+Pa3v81gMPCy2jCpVvpMWKHShpKae3h4SLvd5lOf+hQ3b97066GAhF/4whcYj8d8+tOf5vbt25699/jxY0ajkZcBC9tYAE5hogmAHlopyP1Op1PyPOcf/+N/zHvvvYfWLgRGxt+f/JN/kpdffpmrV6/yvd/7vZ5Zu1gs+NVf/dUNua+MI5kve3t7H+j7GALk4h3Z6/U8g048RU9OTrhx4waDwYDxeLwBFkmYlPS3sJ5lzQg9F69fv87JyQm/8Ru/wa1bt9jd3WV/f98Dyd/61reYzWY8++yzPP3002RZRlEUvPnmmz6hV9YYucftZ0zIXgvnroCrYdpwyBaWOSdrtsisjTF8+ctf9uPlE5/4hH8uhnM/SRLviSvPutVq5ds4DLaS8bsNfsrv5FkowFsoTReGn1gpyDMglCyHYKC0kwD0MuejKKLf72+MTwHQ5fryPOfk5ITDw8ONdTych9tgXwjWh5t42wzIi4uLjXPI9YTep9vP8H/Z8aEAEJeHyrNcwH1x17WlbCvSmSUqGnZdu0lkVI10LXFFb9lR60RmpTzrqE6c/5urQKFzaijbrjiqWwq1BNsU76aRTktB1dS2vhi0EahyXWxJcMX8dg9dWbLHS8zQScnqVFAZkOAHaJhRTWFoonWCrlXud0rhC14bW6JcES8U0dIxxjqPDb33W3ROC1RtqTpR49GkvNSx6ih++Ik3SXXFUTLl7975BO13U/JhQ9H/50Me3xygbs+Z/sIVOhkUQwemJhNLOq7QZYTulRQ7mnSivZ+ciR1jzeYRUT+nxvlTpXHNrHZfMKrMOllyXPHO+4fcamk6cYHGuqJXKR4v+zBJnKpupUjHrvhb2YhelBOh2I9mxAvQSU05idFFSbVX0VIJLzZpv7U1PKgs0etdyr7lvOix+0bNP3nnJX7nrbdJVMVT6QmfGLzHt+ZXHZswVlgL6cyispodvaDfcQPPpY0adAU30gveL/bQFcTLBpETEFCtx4qMYQ/aNYW+qmmkiA1YFWnHKgz73DiAICrWEn6MxSYuBTxeriWc8h7HwlXeDy4qzHp8WjdubDBuVbX+tz9kDKY4JlbjL2YrN+8AJ2FuH3gWrQCY4Xj247uqsbO5Az6Txq+s8ea00/lGIIk5PfP/VoM9kORm3aQpfxD9OgTh5Vp0UFA1QItjETagKg1w2qSpbwCrSjnwMJAZq9quU5gB1cmwc4NNmgeK7HRp7UDRuka1M6rnbqIfuodEOD4EcJBrRkPntOIry9sMkhVoKHuWxVEA5DZt61Pkg7a2Csq+4hPtd/lmfp3/20//CP27DtyOV+49+Y7iY/+L97gST/nRn/lRht+Iva/b4qqC6wXRUtE5Net+CgDP6U3tWIUalvsumda3uYY0dqnG+Y6i7DoWeP+9nOTxBJKYfLhLV1Xcqzp039f0Hhhv/7C46sDA7Nwxy6VfVd1IZxt5LbVCNWnFxjTzpVbr9kB8ca1ntdpIEecG5s5rb1knzMoWh1mJtYp4FDuwXuapdUDS5FnjQWl3YveaRGmmJuUvvPk/4v3Xj5zXXqlIx4o/8Ee/yJ/b/0X+/a/9EfjVIa1Rs7n0Yu3A/QvH7gwl8uXAtbGuoTSa0kbMqxaxromVoTKaaNkwDVcQW2eNYWLF7JZdsxnt+lL70colGjehNyZ2HqaFgvl16KqCBINKDLqE7kMD1lkMWNUwEJvNAO8vaJy/nwDLcvSiFYu6RTcqyE1MaSNyk7AwKb1oRaJqsqhkWmfUjRfjMF6QqJoHyyHdOGcQryhtxEXRYSdZopUlDfwPElUzM65NWrpy3ra1Yn7NsTdtxMZaFS/ctSdzQ7JQxAvLal9BDDUKHZghqsrJ8E3sAGKT2SZopkRJgnUMce7mik3cWqIrS5W556uNaWwlAG3cZtJ3jw/lIeCReDIJsBSCB8IUFDaEsH0ESAzlnrAGe0IPLfmiPx6PvcRTEn6lQAgZEPIz8TiDNTMQ1uCnsO+EpSDgSVj4yTWFgGL4mdvyLSmsRdImRdjDhw/5uZ/7OQ+gtVotX+wL403Ar06nw+7uLnt7e7z++uvUde0lzz/0Qz/Ee++954Nk6rrm/v37vP3229y8eZOXXnqJoigYDoco5fwXsyxjMpnw+PFjBoPBRhBJyFARibSEMJydnW0wUJIk8QCjsE9PT0+975jIjIUZIwXy7du3ieOYO3fu8Oabb9Ltdjk4OOCpp57y0sGrV696QPf09NSDdVLUyhiRYrbf73sQWYBqSYxN09R7O4bvl2I/lIeGYyIEl6WPhVUr4ysc5wJ4y+/lGkVSF8o2Q8ae3MP2Z8u5ZEyEBa5cdxhKIGNc3iP9KIBDOF5DNq28V4ACpdSGV5wAOwLeAx7El3sMg1EEdJGfyWdKG4fzKGRHhSBq+L4Pmm9hO8v7w/ba9nkUVmQInEp7yVgJ04TlXLLehJ89nU55+PAh+/v7nvU3n8+9l58AKsLglTEEeGuGOI7JsoyPf/zjPPvss0yn040Qp+eff55er8fXvvY1Pve5z3H37l2frC5eoMI8FTBQwCAJHhJQe3utE5a2tZa7d+/y+PFjn1h7cuIsk/b393167snJCX/7b/9t77/a6/Xo9XobidEynqTft5mlIfAcAr8Crgn4BW5z45lnnvH+s3L9rVaLwWDgAUUZZ8KYlM8UoE1Ay6OjI27dusVzzz3HcDj0myUvvfQSP/VTP8Vzzz3HaDQiiiImkwnf+ta3ODg4YDQaeUBdrr/T6XjQMryfEBAMx7bI5cM+CA+Zn7IGyTNAgGGt1x6YURSxs7PjE+rlGmUdlvsO21zaWsaggNrbDN4QlA9Z+NKH8uwKQUa519DOQdazcP4IQCqbLWL3IGtFWZbeEkA2BENvw3AdCSXWYf+HGwIhyCjrtrDfQzajzBnpg3Aj5b/v+O0PICoHjqUTS7JwvkyCIUSFA0uUcXK3qq0aj8M1kyRegZ4bip6mThyo5wpSRbIwDTMRH7ygjANeTBMAIdJRJx+mCbZQRKVtfNzwEuZo5UA6oxxgl84M8aJ2Us7xAqzFDDuOyRPUFy44Yy2dClkLulh/tokaHEFD61x7QDXKIV4630eAuqUp+pp46WR8Jpb7gc6J4fP/7SeICpg9U5I9SKj6lvk1xZUvFVw+l3L95ysmt7roytJ5bCm7ivZpSdVKqTNN1W2wmEhAK8ducQAlUCs6rYLSOl/EdlIyLTPH4BnUqFrRTQqSeyl1aoiUJbcRw2iBRvP84IT3T25SdazvZ4AXkhbP7X+TRGU8n8w9U6l1oTEtzV/74b/Fa3lOS9VcmIx/Ov0Iby8OHPgawWvn11E1/NDtt5hVKb86eYpPHb9DhGVetZwsr+VYmLq0RInhtB6glKWjFBdWuaLWQl8vWZmkYQs2DDNwYCDSD80CY3BSWoNj3tGArR1FvFyz3SQp2APTyo2tsufAnCg3EK3Zq5KUbGOHJDngzBW1rXFNLSEd0RpotBqiorkuAV0C8M209NrrUzfsXe1k1GVPkU4UqqyolysXCNtIgaMV2D0HRNWXDYCp1TpgpqxQu0OqtiKdNUBgHLmwEdYBTAx7qNrAKndttVy5EJWmYHdBMY0vZChVDjwlN+5J7lPmlWdtucJ/LWFet4//W8BD6dMGGKSusascM52hvv4WZI1BrnyBqmtUs1ObvPsY4tixF9st168BeOQChRQ2jkhmFV+4fIaj1gy10rRGivbZmoEoDKjFcQCOCjuqdgwxgLlpOWB7br19g4mg6LtBIOBLlFsfZKQMqEXk0rbbimLogDNJl7dqfX5VOzAqPKyGdlJR1pFLsu5CJL6YyxyWObBLjSJRtT9Xnbo1GW1QuWJ+NWAgNu1Uty3JTHkAceNQQMMiE1ZuyBazSlH2mo2l2qU9x9qQRSW1VVRGU7csnZPGKqCxorBaYdrGrclW1mDrgekdnfPoss/Bb2haE0OdQryqufiDXfrKUpYRqYX2uXv2qMI9I5aH2jMmxY4jWuLXOoC8Wcj9ZhFQdyy6crJZkzTruV6Pd2XX1wYwjOZN2rBa+/9FinRiWe1rz+qTdpb5SrPJ0LtvG0nu+vlXtZs2qbXbwKvXX2AmVRDpjgP8OrrAWE3UeCGeFj16kWMbdqKCfrTime4ps7rFvG6hsRQmZlmnjEvnmXiUzajRLEyKsYpelLOsExZlitXQe98Qr9y6LSx7E8HslvN6TUclZUdT9JVXB0gKs1YODDdJMLbLZi5ZBzTaBjiFZhPPgs7dmlj2AgZxszGhTMPk/O7xoT2stR48HI/HwDosRYoBkWytVmt2vRQMUhgI2CGAi3gohemOxpiNn8shBYMAkcIikQJDQAMBn4R5tLOz4wEM8fMT3z8pAKUoDdlYcs1yXmGJyefGcczVq1e9lE2uWam1rHJ3d5fLy0t/3UVRkOc5v/7rv87t27d57rnneOKJJ3j11Vd56623uHHjBkop3nzzTe+d9e1vf5vFYsH169cpy5KyLL3cVeTYAoiGxZwAN7u7u55BJGCLFH+PHj1iMpnw4MEDD8B1Oh1Go5EPK/n6179OFEUMh0P/WScnJ96nsNVqeSbqc889xxtvvAHAZz7zGfI898ywUFYmjCsJR7h3755neomsNww4EGBAjiRJfGHabrc3wK+wyBa/v9DrLgT0ttlvUhiHgE/I6gn9AGWcSh+ETCwBqmWsyBEGUAgAFhb3IegQjv1tJo34VcrfAqBLammr1fJBOqEnnLBJ5bO25a3g2Lo3b95ktVp54EzmVVicw9rbMQR9w3kTbgqE8zgEaaQNQtZiuO6E8zEEEgSwOzk54fT0FK21B5lDhqVIkcfj8Qb4Ip+7vTnw7rvv8qlPfWoDQO52uxtAhoyxMIhF2lLWi/39fRaLBX//7/99D2xnWcaf+3N/jsPDQ3Z2dqiqip/+6Z/23ovXr1/3wSTCypb3CrNLQKYQtJJrCINjRqORH5vdbpfJZIK11oe/ynwU2bT0V5qmjMdjz9iUDQUBVLelrdJuIVgTeuQK+CnXLm0fsmGlbe7cubPhpSupzwKCy+dJ33U6HQ4PD/mpn/opvwHzAz/wA/yZP/Nn+MVf/EW63S6/9Eu/xNnZmQeFBagVJqeA8LIBFY678BkReojKWiXvlfsSID5k+8m1y/uFpby3t+fTpAX8FQn33t4eWmvP3gwZdsLkDpnMISAeArzhJon0rdyLzDNhSQMba27IMJRNlPB5L5tL4GwRxuOxZ48bYzg4OCBJEr8mDQaD73hWf9D6EB4h41vWG1n3ZAwVARlHnvMyTn8rzEM5fvsDiBaSqZMnVy3lC0MBZ3RpG49CB6KhoOy4gjFqJMt10jCPaii72hdMRS+QkgoYGbB8Qr9D70EXMH5Chhl8ZyHRuTdDj5vUKaUwj09RjzX6Ix9xgRbhuZQrJl3Rvr4GcPJBgyVZOuAk37Ok04YVJam2tZN3J0vLaifi7BOWK7+M96wD127jJzV1x3L41Zp4mTD6REH77bSRcis6J4Z4WZNdOklz0VP0HtZEq5p0bphdiykHliiunR+aXYMM1M57ipYhidYF6k62pLKOYRQtNbpUZFGJjVzblybilVaLV1qPgIjvH7zBFyafRJnGp6+29F6+JFLaB5D+1Pw26djS7y54mA5RleXP/9qf4Htu3Of7996ir1d0opynO2e8tnSm+uD6ZideUJqIQbwkUzUtXbKqY+pUUXVAWQcQ68gwNy06SelTY1d14sAXZZhUmZdzUtcOYBKWWllteFy68WGxsfagY5Q3kjkZRk16MVK02gbYrteJ3mrQb9ita9aVAOhA46eGBxVCYMyzDS0N83E93gHn0ajiNdtRrqsB5nTpGIh24vxASGLHuAHvKehCBpxPnorXn08UgdYOaG6AHWWtMx/L135p5ZXrJKczWCxhZwBaoRargAUmwSwOUMFaKJvCvgxYfVh0tFnIyyaBY2q59reRzNc1007aTYAs1xcO6FW1wS5WkOdo+RKfNElnxmAXS1SaQNnIAUqF2tt17MUq0I4LyBm5vhWp92v3bvC5J+8AUPRdH9Ut6U82giJUI10Wdik4cKS0bqdBBf1BtGa96XBQ+rZxwFFy1mx+TIS9hwdJVvvuH8JmbJ9ZDyAr6+S3pdGeVejmd+3TqgG6quK07iKJtXWCA7gSi55rhu/WLPf0xrxZHDWAd2SwVpNEBoNjIjpPQds8B4K2UAI027WfZQTzMiVrJMKrOkErF0RU9NTaH6/ZSFKVAO3WMxNlk6a0Gmu08wUtVQNCK1qRSL/sBghtU0u8gPaZ2wQLWXx1pxneFpSyRBgqIhJVk0YVaVSjc+XnTrSyJJW71/kNNygWV9vUGcRzN6+6uiBRhkG88gCoiSEu3YaMGyOVZ5SKXNmtD4r51SadvFqPLVU7r125dmtVk2St2U/mPMh3GMTLpn0itDKMq44DjFG0dOV9J1cmQWNZmJSLokuia9pRSTdy8uZhsmJcZpQmohvlzKuWkzbbitwkzPIUVcPySHtPU9swVpWBZOLa00aK9lnJ9FZrHbijDJmq3DlL5/3bmrjxIxJlgwPiqbQfF3XmVAsmNUS5Y5NW2TqgzOrGg7nlvGW/e3w4DylMxY9NjpAZtc3oEP8xKfRhLaOUwkFr7QsMKehCsC5kK4RMrLCQ35bxhYwDMdUXTzthmUkCcFhghAxJKdhC9okUb8IIEVnr+fm5L9RXq5W/xo997GN0Oh0eP37Mq6++6j9LvNHu3bvH+++/z7Vr11gulx5QGI/HaK159OgRaZp6b8jRaESe5/T7fS/dCotwaQdhHUoR3u12N4BbYc+ERa4Uc++++65nPQ0GA46Pj3nrrbcAx5zqdru89dZblGXJ8fExV69e5dGjR+R5znA45N133+XJJ5/k/PwcpVwC87aMV9hwwmgVQBpcgTidTimKgn6/70EMkX7L+ApZMmmaMplMyLLMs1jkM2TchiDVNot0G1gUYBD4Dkac/F7A2nBsbDOwtuWPIjX8IIaTjL2QVSj3KvcpzDMZayJtFiBQ5qAAoOFclHuVz5XXyc8EWBR/PvEINMb5yAloGIJvMre3WXzhvYfzKpxr4XUJo0/aNmR0hXJZuU5pk2121DZLStadENDZZjrKv0PA6M6dO3zqU5/yPpDCuhUgNmyDEHyVTYhw7RLQT4BMAVeEuVrXNfP53INKAgIaY7i8vPSMxiRJaLfbG20YsrKlfcN5IZsrAraHwUcCWoVAn7yn3W57OwZpbwmwCNdt6Tv5bAHRQxZsCGCFm0qybsn9yWflee69F5VSfo0O+ygEbOUcAgxKf8vnyu/kXqUfZY0P202OcAyFa4W8Zj6f+zVLgECA5XLpmfoh8zgE38M1WgA92VAIPyuce1EUsVwu/YaR3MMHgeBh8IucU+aKvGabzRjOA2nXEMjfZgnLOWWcylojzGY5ZL0TX0xrXWDUBzEa2+02zz77LC+++CJxHLO7u+s3hoxxsvhPfvKTDIdDrLWMx2MuLy99O0h7SQiUAI3h/f1mj9/+AKJy0rxQXRHl0JoYyo52jMPSUrWV9yusUyehw0D3sfOCspGibtgG4glXtR3DT9XKF72+4NN4+ZqAlOuU13Uxr0Qm17AT5ecAq+MO0W5G1Ylo35+jzb4DEmP850lhGrKkdAW1FHM4EDCuFdm5k2Hp2hWDyQrKLiRzx7ose7oB9ODFj9/l4lduYxoJs5zLtByDIlrWWB0TZxV1J8FMXWpk2VHonYTlgSbfg503DVWmSLUimdbMrkYoLwF1EkldWc9wVMaStEuyuHJSsRI6ccGkyBpQxpJMNVeyCbqCxWHEp4fvAE5yXFEzqjtEK3fOsu8A4D//3D/nZxYJC9viZ0Yf4ddPbqIsDForzieK1kWOtRE3OiMA/rXe2/zdyUv89OnLpCPL9OmaWBushU5UUFlNOyrpa7e4nq+6mNT1XX6nT7ysSdPK+cc1oECB3gBfcpNgEgFqDFZrrNaoosRejrD62nd4YdlYrwE6GT/GQqydtN02AJFes6iiwjr5YGUdK69eF7vhEY5Lk7p+j4tN2a9PVFauLyRF2qZNsEfkwBxlFNFKsbhVoeqY/l03SN14bQqKNHbAl3XzKsoV3fsNAzgAcBy42gDxicjpLWiNrQ32yeuoB066nHz9HQe4FSWcnMPBbuMf6NrQJhqseJOqNYgmbLktJqF4lblrWgOtGAciChFL107anZT1B8qLwzZEK2i1sHMnv476PVAKW5bY5RK7yv39KqUgiUErrOwaKrXRPg74aFiR99u8s7+PLhTtE+U3RcJjcSXoT7UGkm3kZPY+sMhhts28dCBLph2AI3JPoGFlW0gsdcetpVWv8ZLr2DUoZxvQuUk3lvaxEZBDJymY5JmbR1GTgFsZ6vMLfw0aqK32GzBe+g7UqWX0TOTBLllvTWxpjRXWOBZiv5WzqFNME+JBpdfrcCToodyb8yGV/YyyjqiMC22RPya1JAvHQnM+ek2qb2rQtfb9hV2DsI6dpjxzTcZf3NgwaG0asNd9viocALja0+vwHC2AoLvHsJ8NioerIYsqZVXFmNQB5mVH+TVHrC107fx2O6c1VeZCs/rabY7I59gIqFhvPDSfIUzyqtmQc2CYpf++8W3nmdBdML0aGu/JRZ5Qo+jogkRVvJ/vclF2ud4aOYDSRixMSlLXZOJb2Ny8BKIs64SdZEmsa6omSSw3MTvJoplqLkBFfBInVZtR2SbSFjRk57ZhT67HOQpmT0A8cz6dVRqxOmgCvRp2YI3is4O3ePfTezx+uU+clFzpTjBW8cf33uJG3CJRJ/zvv/8nqdE+uOXVyW2+8PmPUrUViyNB8tfgJbjP3fA1/u7xoTqkGNnZ2dkAXC4uLry3lxTHrVbLe051u12f/LtYLDaYElKwCINAgBABEOUzpOgST7ht6ZMcIbNLCiQBJXd2dvx1iJRv2/8wPKT4i6LIy2TzPPcppu12m8PDQy8zFdAH4Pr169R1zYsvvshrr73G4eHhBqgi/mZxHLNYLLw/2WAw8OxNASCEVRayF7Ms8yEdeZ77oiz05Nrd3fXg22Aw8KCQAHkC0h0dHdHtdnnuued48OCBP9/t27c9o0vSVq9cucJsNqPf77O/v0+SJIxGI/b393njjTc4Pj7m7OzM+yU+88wzjMdjDg4OfEp3lmWeQSPAjPSBjCMx5N/f3+ftt9/mE5/4BHmeE0XrsBxpb+mnxWKxEegDeH+8kFkkoJe8NwQHBJCQ/g/BGAFcQoBxG1yU14VAR8hsExaPHCGoIOM9lHRK4S3+h/v7+0wmE3/dUijLPRnjPEqHwyHL5dKn5spn5Xnu/TrDdgrHe6vVYjKZ+PuTcSX3Fc67sD1C8GN7ToZAfDin/0W/E0AgZFvCGsgI31cUhZ/bwvSV1wIbye3b/bMNHMo579+/7+X+0gaSEh4yIK9fv+5BP7l2AWtCdlV4bvGwEwA3vFYBH2XNabfbAN7CQClnFyDzOJQry9gJGcahlD9MehevQWkPuXallA+8kIAh6dPQH1TGcThGwjaWdVDGmbRFCKqF4ygEf4VVHLLc5vP5RlvJfck4EJApvM5Qai2fLdctzyhZFwQElWuS/hP2toyhkPUIay9NAa+WyyUnJycbz4uwb8Oxs836FSZdaEcgmwTS3/IckI0VeSZo7cKvzs/PMcZsrIPbAGG4qSBtLBsOIVgfsvdk/odjFfBM2SzLqKqK09NTP4bjOKbX620E7Mh1CQNWNvQuLi74zGc+w4/8yI94L8XVasXp6SlvvfWWH9dXrlzh6OjI+2p2Oh0fQCPPqBAIDtel3wqQ+NsfQLQ4Xz15QFkH+iULCUhxwFlUWGoNi2NNPHdAW5RDVEYkC+NkeTuKaGkxmWMdRCtXfCUL497fUs4DUeoC44pgCU4JCz+spU5045MWFNMCQipY7Tco/VXNQd4mmy0pr++sPb2a1zsASgWfC9FKNWb2lrLnJIHFQNG6tOx/s2J+FLPaVz4xVlkoBg44UhU8mAzIgKoJ7FANm81Ejsm4OE4oBqAj533lQyS0K0ZbI+Ml1bp2hbCNnZdU1bUkyjbJljTAgmOAVh1FFBuG6ZLaanQF3ajgcdV3YE5iUSVcSSdkp4r5NThORvzt6T6ZKvny4jZ/55ufZNAUtulIkY0r/vJP/1F+6DNf5/fvfZU/sf8rfPHBE9BSFHVEdu4ApV53xbxqEgWtJWnQImWBds17D/e4nihauuQy7/Bs5wQNZKpwbJoYWpcQLzWqrsmSilmdkUQ1pTVMjZPV6dIZ7N+Z7aNziPIam8QOoKprByAqveFzJgCMjdyYqbLGZ7ORNdso9iw0kSIr45hNVeak+YiPn7DJFE6+3BTNNrJ+/OjcYttNgmjUSOzz9ZirWy6UIsobtlXtrq3suOJXNSB1525M96Gl7ELdVnROrJPk9vuooloDDMpds4znoudCjNo12E7mZL5lhUkbVlNRQVWj0gR1com1zRenOHYMx0ij2m2sfLHTa9aWS35uxma19hQVAFHApDptADjwmwsChtgIDz6aRMGqeXhluhnLIQWzGePy38USM50SX71CdfMQ89U3Uf0+djolunkdO5qghn2H3i2W2LpJXEoi13/KrWcCHvoxCnTf1xx9Zsp7yTH5PtRz5dizwljUgWy8sJ75BA4gBMf+ks0M1bCw5D4iDCsbo0QK3MwzG0PULeE0dtLmirVPp3UA7PSmptTWb1KE8me02yg4nTufV5M0EmalfNvZCCLl0m39nGjagsSQTmJ236jJ+5oQ6J5dd0Cdjg2m1EQNqGRMA+w1jFYnhcYD9B7ATBXJ0p3sRn/EKHdfRvdbcyqj0bmzCfDtaN0mjcr1em4JoN/0U1+X6Kheg/jNawS8jSJDFfiTmpYhmSna58ZbaMhcnD5lGy9bw+NRj3e6+yyqlNpqEl2zKhLiRdMGlWPLy6bTXDvwOZk7NlxUOPAzbUJUElU71LYB+ZVxQWGZqtBYrHUS8cUVRTp2g0TViumN9XNQ1hRVg545dqsuDKtlyjBaOu/CqsdeMueCLloZEmBctzFWrf1vG0CxF+UMo6XzvdU150WPyrigH4NiVqW0dOVBxETVTRBQxUXRIdE1vVbOuWpYsUat2cLNtSYNkSdMttelGy8RhrlNeSI55fcefpPpXkamSxJV89r0Jmdln9LWTE3Ef3f+Mm9dHFDWEbPLDmoZoWNLOoHOaU3ZWQf5AJRdRTmoiWe/9cS77x6/PY6iKDYKdIC9vT0f8iCSU5FPCmslBPHE58lay3Q69ewT+TuUx4Wyz7AAl0IwBHqEDRHKPqUwm0wmLJdL+v0+Ozs73Lx5k8ePH/uU0DBQIWRJSaHfbrc3inUpsnq9Hjs7Ox5QFPZYmqbs7OxsyCWFbSTFmki/BAwSplEolRZwVoqiMDF4Pp/7EBUphAVgk3vPssyb1e/s7HiQSbzihMn2xBNP0G63OTg44M033+Tpp5/23mQPHjzgwYMHTKdTdnd3uXfvHk888YT3aJSgCK21T6+VInEymdDtdv19yT2dn5/T6/V4/PixD+YRdkzIkplMJoxGI+8fJz5qwuSSolGkl/Jz6U9Ye/lts8XCfpY+krEjBbQAJWGbhqCE/E7GhLxGCurwtTJmtoMSZD6IbDEc8+F1L5dLDg8PPYAlAEwYNhMy3wAPCEibgkv8XS6XHtgJQfqwDYANv8nw/kIWUsiklJ+F3qQhkB/6o4agYQgihT+Ta9vuqxD0ENBdPNdCWwRhbglDaTKZbLA5w7UlvI66rjk5OcEYw87OjvfK293d3ZC1h/YB0jYhe1LYgSHTK2ROCigkY07YswL2SQCJeC0K8F9VFYPBwANB2+CrXE8Y7hECdCEbbRsAl00gAbGEuScAlTA5Q089AbDl/mXshuCyrGGhRFreJ5YIMnYXi4X3mgT8pop4WEqQkPRdyNoN7ycMSJHfhUxW8UyV18smi7BzZdzLnJP+CVlu0tZiDyFesLIWyTgJAcxwnIQbIjLPZQ4Jc08AXWE1hv0H7rksa7rM63DtkfsOmerSdmG/hYCmrMlyyLNFzi0MTtlQk/49Pj7280L6UgDBcKMnlK1XVcVkMvEsdnmGyhgXwPTpp5/m5s2bHB0dodQ6TCyOXXK49Jesg4eHhxv9/ls5fvsDiMERSqnkS/viMAIDaVPkdh45E36rIR27Ar3sOF+tog9x4gC21b4L57AadGNqr6xjIpnGM06kolHuqkiRR3npslqDjL6AEUaOMHysA0xMov1rXXqpavwTWUujG5loMnOsx6qrSBbWAXha0X1siFeG7NGCKutRDCOilSVermV60cqS7ygGSeWlpwI4gKLuWNKxYnnkCkRTa1df1U3oTMtdf7x0AEzVEp8y5znVv18xfjamlVaUwqRs/Lhs7IrUOHZsv68ub7n3JG7BEwmtsi4FNF5ZWiP4z+78bl7ee8ifP/o5PtF6wN+efsZ5gzUAb7SytG9N+df2XyNRFS8kc0ZnPbpdSKPa+ZsZSytx5vod7RYsrSzzInVspllMPNdUmSVTLuwh0RWpUozrLufzDlXXsXxmLxQcvWropgXHyZgfPHiDlbU8m4z540e/xleT56mt5vGsR7yyqDIAm2qDTRPUzsDLHe0HzFnnldWAQLYB8ALWqZMlO2BWQj+0BK5Y6/04rV6TXLwELwC7VJMQKoeNYbVfUuxE2NTCWSDBWpboqqFhhgxFEwAIpUV12ph+2527GQO6clJ7+Wx5D4BarLCTGfZwzwFPzf0QR42f4Kaps7/W6RRaCUQRqqyhFfk5JPMvTDL3997837EG12FEApbp0jr/AAGGauvBOR+Q1Bzip6qqhslZNFKdw0PHPrz7GHV44F5cVU7CPOyvPTFrgyorbCvlOw651EhBY0cQLyy76QLbq0nfiYjnFjMPds6MZXWo1sEZcqpoPc4WdWvDYkHVFpusATKtjF9zqrZyGw+xIUlqio4Drsu+Wofi4Nqy6lr/fwEW5T6sgn6SU9V6fT0aTBqRXDnGjMZUmaKz9QALx3zVsVw+G/kE7ZCBGC8d4w+jyKKKfrRy02AlN1o3rG7XqHWqmr7GBzypXPH62RHdVoFuWc8mtonbTJKNHasd+9I2rMDL5zvECydhlTZMsG54qfXcQDkZtwYiZalFPq6cvLnOFMt97fq7GaNWQzx356gzTbtd8HTvjLuLPW62L2lHJV+sn+S9dBdduP7aAI0Tx3xujSqWh4kD05Vj2AHcXe25Nmna2vleKmoUFybDLGLSkSKeuw0g96xyoSomCfvfbQqYIIWZRoJ8WXVZmYSzvMdeOsc0QSktVVFrx95bmJTaKox1qc211SzM+utJaTUtQGNpRyW5iclN7EKzlPEMwES5lGo3rqF12TAQm7EqY/LsFeXsNGDNKFbuGVTamAjDf3nyg/zXFac5AAEAAElEQVTqP/oo7dOGRRa7jcVfeAH+rf/xF3m7POI3fu1Zhm8odh/WHCwN8yua0Yvu+0PZjf0z1iq81Dmeue8kbi38LgXxw3iIBCwET7YlfFIkdLvdjaCVxWLhwThJzBVgTgocAdekmJLicLugkSImlGQB3vMsLBykIBM5YQhmCSAQSizF60xkg+IJmCSJl3Iul0tmsxlnZ2c89dRTXmYnDJpQBi0BBiEbRorRoig2iqdez21ECcCgtUu5FlBHwFmtnc/k0dGRZ2jIubcLQgH15BzSB+BAJjHuf//99ynLkocPH5JlGYeHh/T7fUajkfcktNZy9epVptMpx8fHtFotH2AiAEuYTCxtLGCBBFSEDJkQvAqBzb29PV566SVOT09pt9ueEXZ5eUld1+zs7NDpdLi4uODg4MBLRUMQLkxRDpkzISslZKIZY/y4kv7bZr+FMnkBXOQetdYMBgPiOPYgsfRDCCaH/RD6JoYs0jRNvfRe2F4CbAm4tlwuPVAhRXx4/9v32Ol06HQ6dLtdH54irxd2llxP6Ocn4zmUbIeMpG0m3wcdYV+HPwvZmiHIIwBbuLaE9xKG6gjAEQJCAr6FgIm8V+7vg4AFWS+kD4fDIRcXF0ynUw9iy3gNk163wVUB2cJrCj8vBP5gLSsOA1MWiwWz2cyvBbKpIfJiWDMI5XNl/ApQJ/cyn889uy8cHwJuh0CTtP1wONy4dmHsyfWkaeoBQHmfXHsIKMrYlH6ez+ce3JXfb7PSw/YTdtv7779Pp9PZYNKFlgByru1UY/l9GNLR6XT8sywE0oSdLJtHIQNcPPzk+RSOS2MM7XbbB7nIs0v6JIoiL80GNnxr5fqeeuopzzQU/8oQfJRxI5t20ucyHuXZJn0mQGjIIpX5Le0djseQrRy2qcyj8Lkoa6P8kU1BWdPkdQJ8h3Nf7kfm0Ec+8hG63S7vvvvuBmPxc5/7HM8++6wHWn/iJ34CgOl0SpZlzGYz4jjm4OBgoy/CdS1kJP5mjw8NgOgDH4RMo5zcLJ2uWTwmdqyuKHfSJmG4FD1FMXTeRLVRkDZgV6yICjCpCx6x2oW1xKsGBLHBuSNXvJtYeRDMhn8bV1R73zu1/rm/5nYLkzhJnEiebORAsvTCMSBN7Io8Eyt69w3Zeek8CXdT6syxVFRZIyEb6dSSXVRUncixu2rn91dUESk0xb0UnU6upxpQs+xZsqykLjreY0/892Y3NK1L1xb50H1YnUL71BCvFItFCxtbHzIgckmTgFKWz+y8w0E8ISpgN16wqmLHWGpA1YuqS7xwoOW/cvXb/MboFokyXItbJOeND0TscB6rYKfjGI1ZUyGqRUTdhvNlx4MZ/VbO1WzM9eSCTCm6OqeoInTj76aqpp+UQWPp6ILCWmoUeR47cHWkSDolqnIea7+rc4faQqoUPz2/zd98//scuKkM42mHgTBOI43KS8cmbNiIJpx9HtAKPC6j9c9NGq8lzBrqrqHsR6TThgnaMOtsEmOTyIPCNoJyv2LS046BGFlMFPvxpyth0Tb/z2HwzYTW2DJ63oHiJhEvROV9QD2w04zdZCHeg2BnC9T5Jebpm84GoGwAF8H1ajzzT9UuYEZ1O2uWmGfsWmxeoPZ2Mb0Mde8xNs/RuzuuraoKjMVqiy7qhlkYgKECEMp1RWBpwlbWa34DCirPYpbXbjBvg6CZkBkoAK2NFCqOIImxRYG6duwk1kWJzXPHQCwKdKeNvRhhG6aJUgrVbbsQFZ8OAoqmX6IGKFZuvakzxe3sAhUZz7yuMwJms6Ju2QYoC9J8DR4oym284eXqQ0HSNUNuDc41tg5WUVcavXKbCPHC+o0aXbmx50J5JFSnkacLMGMh1RWV0Xg2rDysnzgmmgypMvf/i7rnfV/dxYBu1STjlOGdmrK77jyrYH5tE9UdpEtauiSKLFVm0UsHXJtEeYbmmoW6ZtQqC3Fk1tJlqxxuXDpwT64nKprk5NrNDxu7hPti4DZdaiwlClNr34ZWg9EOHDPAMk+Imza2EdisRhcRrbF1mzRNu1kF5cDQOo9Q1nLUn3GQzLjLHjWavdhJ73Tp1nUPvjd96FOjC0PZUSyPLO1TxY5eUqOojHsulD3Xr4urwAtTjqOChS35Ax//KhcvdzAoPj64x9+580kWX91ldl2vn2UyZi1EM9e/ujYobenrFQvVYmFTzvMu7ah0TEKrMcqFtSzrhJyYwsREyjKtMzdOcJs8yzphXGbMoxaFiVjVCVHHclF0WMYpWhkqE7n07KpFrGpmuQskKgauIW3UPHvqZqOjkTUr40BvmzRrgnYJzImqSXXlpPu452FkbBPUpNBAqmpMr6YYJFSXDTiUuLWudWHpntQu0EbCmxSUbc3kuZpoFm2sN989PjxHKHuS/0sBGsfxBkgmfkeLxcIXfMPh0DNaBIwQhoXIm8SYvqoqOp0Ok8lkgwkWsn9C70W5rhAECQGOEKgQJkvoDyXSaElTDoGCXq/nze2F9TadTj2AIdcOrrDu9/tepiXeYyJzhk2WkBRbIkEOJYCTyYT9/X3vr3V+fs58Pmc+n3NwcOBZVXIvUrCFhbsUfvv7+77Ylv6QfpBCTLwW67qm1+txfHxMkiQ+6VUp5QETYQiJF+NsNmNvb8+z74TJJuCXFLchG1VAH2E1rVYr9vf3fd9lWebBSpExCmPx4ODAg78PHjzwEjpp3xBI3ZYnyiH3JD8PmXIC/AmztdfreXAzHEtSqMr7JOwgipxXpowLKZYl9bgsS5bLpe/bkI0TMmDjOPaBEtuAHeDZTv1+37e9MN/COSt/S/8K6CZArrSDhLEIQCEAesiqlPkeSrhDZlXYtuFnh9ce3kPYnrIZEIJc4VgWxp78XySfImncZl3JGAw/R9o2ZKqGAI1c13A49IBbq9XynqLS39Jf4XtkHMRx7AHM0E8uBIuL4LuygMLidSrXIhsKIrsNgVZgQ4ov5xIAUea+gHfT6ZTLy0t/bdKGEqwkcn9h90p4h8wF2TzZ7iPZjAmBsBCo01p7sGt3d5der/eBfSHtJ+yz8H5kDkuwi3jNht6JIftRxoYwaEMmYihzDcE2YR9GkQuLkr4SEDsEqwX8lw21kLmbZRk3btzgzp073poCHNj/yiuv8NnPfpY4jul0OmRZxhe+8AXG4zFPPPEEf/SP/lF6vR5FUfDFL36Rb3/729y9e9evt9IeMk/l2sP5IuMhTVO/dghQK/0na5SElMm1S7+EG1KhD7H0Qwg2yviWza08z/1GjjBpww0iuY8QQFfKBRd98Ytf9Gv8jRs3+B2/43d4sHI8HvPee+8xmUw4Pz/ne77ne/zm4/379zfCs6Io4saNG77v/v+agRgy/+pUke8q0okr+KpMsTxUxEsHXtRtxeUrFd13YzoPXWG+vGYwkSYdOz/BYqDIdyzLI9CFY1uFrBthlejGpN3L1RpWWJhsK2CibjzXhEED7n3FICLZ63h2I+BDHJKZ+yOMmb1vrahbmqg01K2IaJaTakVBQjyvMFlCvDSkI02ysCSTkrrtqr0odwVpUcUkmrU3YW1dmAwOHGmNLLPbliSq8biMpQnKgOKVGat7Xfp3nYyvfV6yOIiZ3kjd/Z1kxIsGmGwkkrpw9xAry1vLI16rblCnMIwXzFYtNxibgjrCEJXOd6wT5eykC/rKsrIVUe7SRqs2xAv3nkFrRWkjMlU6efJYkx/UzFYturg23205yVuE9Sb4ZR3RrkBVimTqOilTJS8NHvLR7B5To/nRwTf4L/TvxFZNiImy6MKw21rwFx/8qyzrhL926x/xuBpSG02dNeDE0oF+qjaOeZg0i3BZOZCsGRNe+igLnV2zBXXlgjmUMT4EgmCcm8SBenFtG0ai9ZJNGYvpSUz7sWJ207FL45Vx4RHN+Ku68QYg6BNqtQtU0IWbHybRjZdowzSz69eVHQey9B4AWqF2h9BIo5PCeXZa7bwZTeykfA6Eae4liTGdlh+LABiLGrgvBvp0hNUKPRxgJlNUpw15jmpnDZho8H6kaSBVVs3crNegkb/uBpRznotNH9TrdhMAXw5VAwlrW4KGsZQ9WqDnKwcepgm61aJ+8x2UVlhjiW9eg7xAZS3saoUaDqAo0N0Odr5wbESlINZuAyGUYwt4aFz/5ntuvthK+5AdAfzlHlcHai1BZj133byyLOrUs5mjwoEpqjLoomFi2Aidu7XS6nXgQxQbitTSmrAJIBlQVlH2LWjrP9Nv6jTjuR2VGKPWw9eAXpXodx9iJjP4IcfUnNZtD2pVnTXQXuwaZtciqk4ArgvTLnLXV0XWpbabhLJch8UA3nsxWuFZgVFes/tG2QCoKWnsWMpxI4Mu66hhATbAXpNY77xADbrSxLMmSbl2nxGhiLCoxudQPGatdgy6wlrEEsHP/6Xb8FhpvQFuKwPRQjdsYkWia2qrWdUJLV155p2N3TOizoQd6YCr0Dux7CpMA4hFWGqr+F9e+af8qT/2yy5USlV8rbhKpkoOoxYPqpyn2qeU9pjHywFfOH+GxSpFWejdN2tWa7PZVfQV9W6FepC69iw07xX71FYzqTK0MuQm5mGxQ25iHud9VnXClWziAbuTVY9lkpDqikhZupHb9NlL53SigkTVTJvEq7YumjAWSxTlQIfKaoxVpHGFLhTx3G6wYYUxvLjixrdr44AJokTGn5Aog03cd4mktFgjzNXg9XYNGsL6O0gxdMnpEubmJeI1xONo4/3fPT58R8hYCIvmnZ0djDHMZjMPbo3HYzqdDi+//LKX3Z6fn3N0dMSDBw+8BCw0dBcWy3K55ODgYMO3TMCD7SJcGFsi6wrDIUJmDbBh0C++VQJ4SbEoYNhwOPTstYuLiw0/K2GgxHHM2dkZnU7HS83E609SNLvd7oavoVynADRRFHkgScA6Ya7cunXLJ35KMQ8wGo3IssyzBsXQf5slItK/qqrI89wXonVdeyaaJGv3ej3Ozs78dQo4JUCRMGZCqfXu7i7L5ZLRaMTe3h6z2cwXjGFRL+0bAlDC1pxMJlxeXnJxccHTTz/N4eGhZ6KIZ+XZ2RlKKfb39/34e/ToEZ1Oh3feeYfhcLjBmJHPDIHnsOCWawsLZvm5FJ3SDlIwy+uk/0LWIKzBYxmP7Xbbt6f0TZIkPkBCJH4CAocgYchgCudayDaydu29JkCjJMl+UIEubFYBVZfLpWezCoi6Wq18Wq/MJen/sDgPAZnQUzCUIcu8lfkazkMB4kIgMdwQCF8n/dLv9z0zS9pbrlnGvMzNEBwUwEWSi8M2CRmD4bVkWeaBdgG45HOkzUJwKdykCCXK8u8bN2542e5gMGBnZ4eTkxOUcinxr7zyCkopL1UVCwFhWAkQHbarHKGcWMC+UDYs7djtdrly5YoHssFJ2g8PD/m9v/f3+jb9yle+4pnJoVei+EDKWJC+F1BJ1mJh3HU6nQ0rC7nOTqdDv9/31gQhcAcO5BaGomxSiGx7d3fXg8Ih4CtzVNolvDZhdIovZBRFrFYrbzsh420wGPj7FRa8vD5cQ7TWTCYTD9RLHwoIJ8xFYW2DAw/v3bvH7du3Pdj/1ltvMZlMePvtt9nZ2fGArbAd5V5Evi5s6jBASWTfsrEh1y3t02q1PGAbWhsIwCvru7xX/shcEF9GWf9CADFcI9vttn8Gd7td35eyVoVzTtaiEIyXdUaYx7I+yu/k5yGjW+ZBHMfs7+/7a5FjtVp9R6r2b/b40ACI4kFlpVjWLkCkaitaI1geOvaWrmHVVsyerlDtiuw0YvBezvSJDJvVqImm6liSmUvcNQk+SCJesjb3b8A3/39hEwLi2yWeUlYr7zPlC64GNBKmTdFXJMPEs3bSsSWdWWaN51xrWtM+d8VPcrEgTiLqTkI6K0ASeJuOXx23sRp2X180rMiGZWNc46RjzeyysyEXtHrtESZMsrpjqBspNTTyOA1VCnuDBWfKSffkvqPCupCVEjjIid5qo4wDb6XQFzCgG+X04xWvKchUSV1rYkDlGmXgieyMzzdt1tEFvbigo91ki3JIZg7gVTVgYJCumJsWmSrJrUsbLa7WzKYZ/cqCVlxrj8lNTEfnFNbJ6+aTjE5TpHceWUwEXZ3zye67HOoFmTL8lYvvpXzQxXYNphVRjjLyA8tQG4bJkmGypMY2iaOOiakxqHnkA1QAiBvWiVaOjch6PDmvQ/da8SkzSZP+a20jYV0zjZJLTevCemDaJaCWDpzUrIEV3Uihc+vZYrpesw5VZbDtaB0uItLS5m/xeHN/lGc2otafIYcy7nwMHKvCMeCatNoWG4nO7sMaAG9VQFliruw0gHyzgIWSg2Z31C6X6MN97MVlc73ayZz7WWMVsA6hEPBKl3btkVqv2yUc+05m3rzRbP1OQl0M0DAY/T3XFr3IHfhrLWq+xFhL/MRNxz5cLjEXI+wqR+/tgLGY0RiVZZAXbKQ/GYNuNgWkPf3YALCOUbcwqQcFlXHzkRS/7tSZgI7r8RX21xPZGfbZOefdjmNeNSn0h7dOeSIuqK3lo599i9efOWK/t+CZ7oR3x3ucvbVPslCsdlXjjdncfwNe1i2F3I0ya5ZmeBR5Qsb6ulbHHVrJDaLzGXUbEqX5w/1vUP8bilmd0YtWLOoW//D9jzL9RtulFGdrFqyJId91YFnaKqlWMd2o4CCeorWhjoKE5ARIHPAVlZZkWoK1mDTCpJpooeklBXntFqm9dE4S1USrNZCsCzcPRS6Ngs7DnMXHOi5xXDkG4tzG1GJyqNx8qjNFJ8qJgE5WMO9ZZlcj8n2wmSFeaJK5S+r1wJKGalCTzJov/Y3E11hFZTR1s+ZEufIbFy4p2PoE9zDIi6btAbra8FfOv4+f+MnPooyiGBhal5rlzZKf/33/Kd8sD/irP/17GL6h6JwZNw8+EVF1LLPrjmloGnBeS9jXLPbgrq0Up0WfWZXSjko6ccmyThiVHSZVi0hZ+nFOomr2kzlaWQbxCq0suYlJVM2z7ceUNiLCUOMYi63GjzA3CbXVHCdjvjK/yZ3pPq2oYrRqU9ZuI8ck7jlsfACV8lYLG4f8V5YAqzE4Bqqsr6aF25xJLWGAu26CagT4R1nSKc7PMgmfse51pqUpht9FDj/MR1jchzIkAZgkJVTAjCRJGAwG9Ho9ZrMZ4/GY4+Njzs/PWS6X3udoMBh4IElARcAXLZI6uS1zBDaKZFj7zYVyypBpOB6PfZEVSr6SJPEMSAEIRUIo3lTgpKchSCIhMvP5fOO65bOm06l/f3jNAg5IoS/gnJwjyzIvIZYiUgDWsiwZDAY+1bXX63l5q7CgQhm0gK3iP1WWJe12m/39ffr9vi+CjTE+yVTaRYpGAWLk3wI6SoE/HA45Ozvj+vXrviANC/Asy5jP5957cmdnx6czW+uk3sfHxzx69MgzTyUkRiSkoaRV/r537x4vvPDCBigFeHAifH0oZw0Zn+G4DsECKWBHo5HvP+kr8fwMgbOqqphOp1y5coWqqvyYkH4W8CIEL0NgMwSA5Gfhdcq9CbNN0rIFDJD3yPllnoQSVAGChYUmr5exIow0mWvCANuWNEu7htcqRwiabjOXQ5mhABOhT9wHgYcyH0JAUz5z22MTHIgTMoxlnQqBpvCztseAADHikSrS4jBYREB6CTrZln/mec5kMuGjH/0of+pP/SnvWSr99MYbb7BYLHj55Zf50R/9UfI85+TkhFdffZXRaMTl5aVn1grQI2DO/v7+d4Ap4dgI5bvGGG8fce3aNQAPXL/11lveP67T6bBYLBgMBhhjOD093QgOkWuR9tweW/I7WTuE1SeBMQKwSRsKu1Lmk0iK5V6knWQed7tddnZ26Ha7vPnmm35DabVakWUZv/t3/25++Id/mCiKuHXrll8z0jTlh3/4h9nZ2eHRo0d+3ZU2lHuRjaSQ1Sj9Kf8P5fyAB+qEnRqO99AaIEkSXnzxRQ4ODjzwfHx8zIsvvshsNuPo6IiDgwO++tWvesb1YrHYWHeE2b79jJE1ZbFYeNuPcG0R70QBsGXDIdwUEKBTfHtlwy5cS2Tt29/f9xsR8kyRsTGbzTbW4E6n4/s5PML5D2uf01AOLfcQ2pPAmo0bjp3z83PfJrJGSVBZeD2/2eNDAyB6cI4GAGsK1GJovTeVSSzlELr3FE/8twZnFFdRtyLygxoiB7JEhStQix0DkaXuQnYSsdq3dO+vAUMpyh1jTBGFRUlTzEdlwwBpPI9UQ95ae9nhPb2qtrB+GmCgpRjcNejKEi8MujBEqyZcoqqh3ezqJBF1uzEwTiOSaUU8zrGJJj/MyB4tiFaxl6xK6IIAocIgEQWlyEtpGeraAXq6ZC11jZRj2vUc2NF/v6boRf668wODmSXOe3Jq1vLYRu5nrOIonXBW9jEJ9KMVRRHTrpui18Jrs1skc0M+jOhrZ6jfUy0mZuWvNSpcwrSycLtzwWE8oa8LCquJV9DZWbK832sSdBW3WhdcVF26qqCrNP/m4Jv8xLWPMYuvkY4U6bym6Gm0MvwHr/1h/jcf/Vl+f/ct3lvukUwUZW/tF7k8iOnHOYWJOUynrKyTPJdGexZWMtboomnLSENVu8Tdho0o4R6uQLWNx54rdKusCf8I2DEiZzcN6yv09ALQTfAIOBCibq3BJ1dImzUzselzZaHq6vV4EMaWH5/WF9+qrJ2fJXb92gaQc2w4S1QalzidxA64lue22fxbVRA36dFoBXGMyQJmTrFO4DOn5+idYfMf437XaqG0S7VGKUwr3pC8hmxDYcBusBBr5f8tQSI+PKVat5tjKDbXHbDdbOX6ygGLxrEPtUYlMbrXheUKtEb1uphHJ1gp1qomibkoXEdq5YBFwEaRZzf6ZN/mGqxSbrmp4cvjW0SXsV/nxAuUZhzZSFE2/xc/SKvc+qexPJs+4t9/5ad58NIu4BiNby6Pebb9mJ5KMMrwZ699gfOjHqWNWdmEk8UnfX90H9Ws9nTDJnVp9SgoBxalLOHaJmukNYrd2DGArYSZaFgcxiwOY9J5m8U1Q0LEfzN9kb/6E/8qUa58X6xeXhL3LMtDTdVh3bfCLq1hftpFVYpvjY9dam8Ro8omRVx87qxyny/DIIvJd10wTDxXRNpwkM7YTZd0dEEa1W5DoHIMPhlfbrOlAfCNZXXoPBrjhaK0hisR/JPf+Z/z4LN95na9u/fp1jkdlfI3P/b/4PQjXeamxd3ikP/0n/8+yu7aY1bY7ZjGM695vu20liSqppc4qUsnymlFFVXWsMs7+AAhx1Z2naBq48+Hwvs7nuU90rGidWGp2pp4acn3ImrrvABNaqkz7YAw5eTyqlYM3qudTD1Yf4qewrZrlHE+jlHHBbFEylI0O0daGWJds6oTUl1z3JrQjkqG8YJMl55dmKiaUdkhNwmJqkhUTUflrGzKrM44iGcMowU1mp1oQS/KOZt1sVax01k6UHWpfKiatx2p3XxaXFUg40I2Uxq7Da0shY1IdYX4npqkecbnzYYIbpPIhxfJ949mTLrNLcfY9muGcr8re+t09+8eH85jW+onhRc4IE2kZcIuu379OmVZ8ujRI15//XWuXr1Kmqb0+32efPJJz7KRourw8BDAh4Scnp56GRqs5VLyR8AyKRIEHAjBQ1h7cIXsFWHZSaGxs7PjpbSTycSDDnKubSaWgHByXgEUoiii1+t5DzsB3UL5Zuh3J2EJUnAKU0oYM1IkidegFNeDwcAXlp1OxxeKoYwRHPAyHA49w0UKTinI8jwnz3PPlBRgSeSMAn4KoyMs9geDAf1+n+VyyY0bNxiNRr5gPDg4YD6fewDmypUrXso4m80YDAa8+uqraK25evUqZVmys7PDm2++6eVv4/HYp+sKu1EKVilwhd0kiaDh+JTiV65BpH/yJwTBpZ9FtitF6ranoPSHMLnkPHmeb8iiw2uQNpfzCotRxoK8R/wqQwaZjJNQvioAgEg1t4GNEGALZdwheCAADazBERlbUoiHUsaQcRnON/m9tF/I6JRDzh2uIduywg/ySNsGHbdfL3NYAIlut+sZxtJ2YehO6I8aspVCkDNkCBdFwXg89n0ggUEheBa2UcjSXC6XjMdjHjx44D08ZW1cLBY8fvzYM9WEwSay0pCFLGNNPk/YWeHaFl6LMPrCdirL0rPmqqri5OQErTUvvfQSN2/e9O39+PFjHj58CDjmW+gTKeNTJMRZlrG7u8vOzg7z+XxjfgvoCPj1W9ZBCf6RICVpc7l2kTBLf0gIjtZORr23t8cbb7xBlmVMp1Pu3r3LtWvXuHr1KlEUMZvNePXVV7l//z7vvvvuBhO9qioODw+5d+/ehtRfwH+Z6wIKh+BTKNsV5rZs6KRp6lmTwsQLWaFZlrGzs8OXv/xlfuM3foM8z7ly5Qp/+k//afr9Pq1Wi0ePHvHVr36V999/n93dXZ5++mkfziJBYNKeISgn40TA7nA8hgE6IZMvTIKX55cAvaH8XO5B7CaiKGJ3d9ez7WWsyobR7u7ud1gZbAOyAt5vb9RsM8jD+whZ3zLvw9/v7e1t+D3K+JNwqt/q8dseQFTWEUHk36EnYZ05UMYkrqg1LUu0dL6I8bxywEOqKfsRaqfg1vEl01+5yuBuycXzCbZfoWKDjix53cKkhu59l5TqJZJ2XRioVeN3Jf0Qsg5pCmprnd8a7nfC7rJaoSuIFzX73zREy9oHq+i8JlqWDggxOCZbEjX+aM7zUJct8mFCtDJEixIiRdVLG/ajpv3+lP1v7Lr02Ns5kVXoOqZu0j5dmIpjjDnJnWuz5bxFL3cFdJ24YqxugVIW3SspuzFYiHNDETfyu/0cZRXd+5Eznl8YRJJZp85brK9XvFUeAQ0DcRn7Ik4ZeH18hImdzHUnWmBQREpzt4pojRxDL57TFPOK42TCz09e4JXDX+C0TomWlrRVYh9H6LzEZBE30nP+YP9rHGrF1BruVR3unezRz9YhN2VHsaMXdNs5N5NzEqU4z7tEq7WUUueauuUkmfM65SCekSnFtM4oqgjTcjJokScC0LAQVV2DUtiW8yHU8nwLwC8JHVHGAXIoBZFqAEW8l51/j3z/r2rHxouaAJyGESVy1jAlViStVit06VhkYahL2VPYyPlZ6tL6wIsqWwOPngWWrJOOrVLYswvU3g427ns/R+/p2Fy3rvEJze7+NHWqG0muhQYYtFGEunK4UeSovHT3iZtPNksa4A1Eb++Ze9ZCA8a4ZONmPop8tHavcb6IjQ9mQ/pUxklNVW3RpcEkbiFWwQaFjaA8HpBcLFwYytIVJ2a+QHXa2N0Bavdp9PuPqR6foNKU6MqxA5JXOaTJhopRPtPNdbsB0Airz6AwqfUhFlV7vfbpKmCiNevLRr8Df+vkc3zlv3mJ7iMXVBTP3fz8mU8m/NCf/Y95u9znL/xX/xN691zgUrEDxcAS31hgVh1m1yPfr+Ltqku3tlZWecDGb7Tg+lT8FYV1Ha9g7+sT9KKAqubihStESvHu6oDsTNF95Hz7qo5iBcQzRTKzPi0X48Ze1XHXEo9cwMrdL1/nzuCY9nsJ8XL9+zCJ14XjGJRxIE+UW/LjmtpotLJU1qU5A0S5aoJa1uBo1VZgVMDmdWC4iSwRiqkx/Hvv/ut87Ru3HOCkgVrxl3/f3+cH23f5s1//M5zf2aXVhBTZWwWtsQPw6kaCD24joerXpJexZxC2dEk/ztlP5nR0QW01UeFY7D7sq7CNd6fduGeZf+AuqbSaWuTi3TXo64O5EzfOvGTcgEkdc9Js5f4oA3oWeSBNjn684tFqQD/O6ScrKhPRj3OmVYvcOCn2rM6orfN0XNkYYzWjssPKxs7PUvoCw630nEgZzzgf1R0WJiXPY5SCVuy8NuuuIcobtpVn6DYN0DybxVs0XBeBJqXarL1Eo6YN1Hr9LYl8u3p2Z8PajpaO9R0VDvyXfnGM2eC58N3jQ3eExb38LTIpYexJUWaMYTgc0uv1+KVf+iVmsxlKKT7+8Y97meSLL77Iq6++yo0bN3j06BFKKf86gH6/7wuFkFUYHuJdGDICQ3AjLB6k4AiTZYV9UZYl5+fnG8w2kVIJi00AQgHnBGALve3SNOXhw4dcuXLFSxVbrRZf//rXNxhuwtSRBOX5fO6BPCna5XcheNfr9fw1baeGijdXWBQL0CnyVZEgiqfdfD7n5OSE0WjEs88+68GjJEnY2dnZ8FiUa3v8+DE/8AM/wL1797xcWdKexSdS5HZnZ2deDjqbzTg/P/cFYa/X8zJekVHv7e1530EB8sSPbbFYsLu76/tTwLIrV654L7wQ5A1ZcAIObI+jEGQMATYBfLeLaQHj5G/x/hTwJ8uyjcI4vAbxBhMgBfDsQWEAhdcnYz/09AzBEGnHfr/vvcxClmGWZV56LwBUCBrKuA9ZvQKIynULIB6yDEMGUAgGhoyi8Drl2AYEw/+H9xQyvcLXCXAl4IywAo0xXnoZgl2AXxtkfoSsMOn3bQBDABEBlQQ4k77avje55pBBKu//2Mc+RpZlPH78mNVqxenpKXEc85nPfIYXX3zRy2Zfe+01VqsVjx494r333vPzXRKJZf0RcEqS7Lf7VOZueAjAIlYDnU6HnZ0der0e7XabL33pS3z+858nz3M++tGPslqt6Pf7/nPl3qRPRGYtc1AYrcJaFDmpAFvyXhmPAnYK0BsyeyWgatuvVPowBN5lzu7v7/PKK694RvNyufTgZlmWfPazn0VrzXA45N133+VXfuVXPMMvBNKOj483ApRCSw1ZsyXYZjQa+bVSZOkC6Ms42gbBi6JgNBpxcnICwNWrV/1zSDauwvEkP8+yjL29vY01ZfsZF65N4oEolhuy/sm6Fq6PMlZl80F8HWGdYB+CfKEMXNa/ENieTCYb80meV+G8l3sM14zwHkL2rFyrXI+8J1xrjDFMp1M/NmRciuxb2vO3cvy2BxA3ZHICyFka4NAVUMLIqDsukVIZx6YAiJYV86sJSavi7t1Drp8bqo6mGELSLhn2l1xcdjGZQUnhyrogD43kRa4s3l/bISkh60CAIyOef7klXhqSaeEYTQZUO0aXDiBUeYnttiBWkDuEQxhUpp1QtSPSSY0uDDqvWF3pku/G6NI6ls1ei967c2ykaWUli1nLFTYRHrSxkUIV2gOjOqkxZbTh+2i1Ayy6UY0dpegayq52sk4Bb4uI5554xP3DW85IPnZeVKphZ8TamdRfFh2sgkRVYFQjQTUoq3jn/UOuNwXfg3KXvI6prSFRrm0ENImXTjr5n3/+X6F9PyL+Nwx/aPjqOvhDC9DlgMH/68nv5s8e/AI7uuBXF89gRqljl7SsZ4OmqkYryHRJojTzKnU+lw0obbUrCgfxkrOiy2E8IVEOcNAKbKdmoFdUnaZNm6RVNwgUKliMJWlbinPUevwAKB9yohE/N89+tevCNSqtAyX7XYq9bBO0U40kWfqxaRcsqMq4lGoBmhqmnRUGXg1FL3Lnj11auS6U92MUCXm0sn5M+y8JTfAQFuq2pdivmaiIwR2XHk7aBJqMxqjhgDprpPaVdQnM1joAUUDXWDsWZBRBr+1AxrJasxplPgRsQquV9zIME9LlugSsNfEmm0rZZg6Uds1GDL3ScFJwkyiqbkw80h7PNaOx+4xpjVquHJAYRUT7e+s3Wwvx1vIbet9J0IuwCIN+e7H3iF9NnyZeOGZaWgXjpbZUHeXZVB7Es2uAGKBuOxDMMxS1ouxaOqomVTVVx1J1oOxBvmtcErF2YGzvQc1yb+2BaCLVAJnWf2bVUsT5ek2wWnGQTDGBN6PVoPIa+8499O4OVc+iG5mqSfGhV7PbhqxdYFptTOKuScZmnTX9ihtjaAvDkt2dOfOzPccmFxZuav08c6xT7RloALYZAMKOK21EUUfY2PVF1VEehHKetwLM2zW41HwnvTApX/vWLQ5+zQFtEj508SPu4keTDt33I3berJldi8iPIucj2lqHtXigb6mb54hLKY6aAd+JcjJVOF/E1AXbVN0mxKa13hSRcJ11YI77cQRUJtpYI+rMbRwArEyCqpXfIJA1XheK7mNDnayfb8pAPnApzLrWLrSm1MTaAXG76cJbPJTNBcTKNGn3NQuTMoxcsEuiakpgEC8ZRkt2ojnTus3KJtRot+Fk3TgpbUSmS6ompSmKaxJdo5QlWmrKrlo/z6qGkClt0vztnoNbcztA+MTH0jGYwcYWDSTUkJoN/1hZc+sMzFw1npSO9boeNxady/OA7x4fsiMsmkM2nRR+UpQMBgPa7TY7OzserJIif29vj3fffXcDNAulb3JIMRSyP6SwC4uysPAPwaBQGrXNDBLgbLFYeK8xYYlI0Rx+dlicSYBKyBgTnz4BW8AFnuzv73tGppwT1qED4oHY6/U+EOCQIuji4oLd3V1Go5EvniT8ot/vc35+zksvvcTrr7/uP0t8qES+WFUVo9GIo6MjX6SLb+P5+Tl37twhTVOuXbvm05TDoAFher755pvEccxLL73Eiy++6L0tBaiSkBbxA9zf3/eF36NHj7i8vNzwYZOCXK5VwkrCQySVIosPmSxFUdDtdj0b8t69e9/BZBMgI5TJbY9l6ZuQfSbsNhkL8hphkMnrBFiSzwsBN7kWkXJuA5JSrAv4KYxYAS8EOBHmm/xc5pswNYWJKPeQ57lPEBb/SJEii8dayIwFPFMvlA7LOBAQQGTuwpQL5+I26zKUIYbrR8j23AZZ5DXb4CPg2bPi/yb9slqtvKxegAxh2IZszQ8CET6IFSkghbBt9/f3PWAka5nMU2GCbbMaw02P119/nZ/92Z/1TK9r167xsY99DHCbGffv3+cf/IN/4FnCWZZ531Nhp4XXGbICpY3DexBGuLD6wvF1//59nnvuOQ/Yh3NlZ2dng30rYHF4fxIQI9ch80Q8WkPPOWEIhx6iIcgmAR8ha1bWivA92+Es8m+5/qOjI6y1/N2/+3e5uLjAWsunP/1pXnzxRe7cuUOe53zpS19itVoxHA4ZjUZeTh4e4XND/h8+O2Rcx3HsfQzFjkD6JHx/uJET3p+MdwHLBLwTsFL6VZ6twr4W0E7mnqxBMs4EIAwZf/IZ4f1JP4TAoICO8r7wXuTv0Ms0XA9DH1Bh+W77oobgejiewnkonxk+e2QehZJqmZ/htYVhOSHzU67zgxjM/7Ljtz2ACJtgnvu/87JzCmXlAYzu8Zzi9QHJwmBjV1jolUGX0Eor0sMZJh6iK0syA/N2h7ODFnvXR1wshw5sE6aWoZEwsgZy7CbTx0YCXgWMg63OUY0UNSohXlaossZkjfxTKVhW6/dZ24AJ1vkeKkW516bKIqKVQdeGqhMRLZyEzDbsPBspZtcT6icTDr66wHxpB3O7QkIywsCIKFfoGi9LpFTr1FKlfNFeG+3lgbp2rJmq5YCh+DSh+2xO2XfJ0U7WpUhnNY1CjUgZZqVLyvxfffFP0HszQRlLdqIxERwdjSk7B0S55T/8wu/nv/yR/zuR0mSqxkYu5GV56CTi8cpw+KUYqyxfubzO7+5/A1VDO6kwlzS+cpqBXlGaiAhXAHZ0jiod0JSdKuKFwUaRS/WsNRGG0hryKnasqoVy/pgTTfvCsJvMWVTOK6O0hlVTsWf9nJVNMC2LVRpVVE6mq7XrO+sAMl94BodJNWVbU3Vcwa5Kly5c7rmGWwf3uPFbt4T1qlDLAlXVLhAjB9qsZcZbIPZqN2pCLCrq45afR8o6MNAXyxGsdhXtc0s8zbFx5oGuumXJ9xtPynnD8BLAKNKYVGM6BoiIZ4rszMmM06lZMxa1whalu8eObuaTbWTKqQMPaycRVmXtZb/UxrVn1Hj1RIpoXqJXZcPObX4eN6EkkWqAAheCoRuWljDIJD1949BrkFQ15xPfPz9/62DOVzWq20FlrfWcrQ3m/GL9+k4bO5thfZCOconN4ZpgBehSDTi19mHUufOVcxG+bl5WbeWBIasVZd80lgEWo9x8dmzEppi0TSpt5EAkvzkQBcwzjQ//aF1oVldq5Hvl5Ha0sd4570tFvILaKuKVJVk0gTmBWVxX59jGEkGOetAi2d/zoGuiIiZV5tdFJylX1LX2NgpR3owzs8kqJAj3cTe6BlC3g0mEeQqNH54CYkukDfMypRfn1FZTVDHRQjU2Es2XU+uYyjayjt1rCfxunQdijYJWTTGIxSkDEzv2IECc1NTNMNGVxWY18dJZL4TXWqdgejXqgdvYiZUh0yXdOKe0EQvTojQRuoSyr7xVhC6cHYSq1HrToWHKSZgOQGHW1hPx0jHMbWLZKE39M64ZZ7FlfuxY2DLuJMgnHkcbm3qViVg2gSTteNV4GxoqIlpRRVsXHjh0/e/AxETV9KKcVFVM67Y/n7Gaqc3o6AJjHTtd2gMgS0sG6YpxnmFS50Uo/SPt6pLFbSNxXoO/Mt5StZbiCTgqUmb3rFRrm9TaKQd8Ar1SftNB7BCiykLznEVDMXRt7TyLv4sgftiOsDiCdYESx7H3ehOgZDqd8sILL3ivJCk8xaRe2AHL5dJ7Pj377LM8fPhwI802ZCQBG0WEXEPIVpLPCoERYKPoEfBSClMpEAWwgc1iSSnnD5imqQcrhAXY6XQ8MNDpdDwzUEC4xWLB3bt3fUKsMDaF2bRardjb2/PnhLWkTK57Pp/74jIsrh4+fMhTTz3l2ZuS3ikMzyiKPBswz3NOT08ZDod85Stf4eLigr29Pc8WKsuSd955h36/z3PPPeeBOGFYCsD61ltv8cYbb/Dxj3+c3/f7fh9JkvD48WPvTyfyYgmYEVBHwKgwbCXLMt8W0gcCcgmbMk1Tjo6OPKgihbYcwugTQDQsFLdZLtKeIai33aYyPmezGUVReIZlmDAuh1ynsKcEfBQAUcZUt9tlOBx6Kfn9+/f9+0OwRwCY0BtSgL6dnR0fyvPw4UPPDpL7FRk3OAm8jFVhNwqjUylFp+O8UmS8hPMsBOZDgCMEFJRSnmkp1yh/tgE5AfRlIyEc1x/EqJI5HgJLofxa3ivH/v6+P7/4fIbnEABK+njbSzIEOsNDxoWMTRmr4TnkHuT/8jMZcwKkfBCoIawwmXviHzcej9nd3fVy97OzMz+WZd0MmX3CbA29PgVkD68rlHdK34cgjDBd5bNknQoDNYAN2a+8P1x/w/4JQ22kzcN5ImCarHcyXoR5Ha6Xsskg75fPljacTCbcvXuXk5OTjYR2wG9sbHuEzmYzf11KuYCmLMv82hCO0aqqPtBKQxjf20nYIdAWys3D5xTgmcPS/iGgK1JjsdiQ/ws4LptuAtpKn8uGxvZ1yXMxXMfk3mQOhpt44ZgO/73tKSrsadmsEvBdxquAmzJmwmesXM82uBc+48N1YBu0lHsLvSvD0BYZr61Wy0vAfzPHhwJADCXCa4aRVATuizyRZTFroTL35b5ONfGiRhUVxUAxX7SoFzGDJiAhmTnwK5nGXOZ7DnBYNX5awgTzRXQTcFHb75BvqQZcc9dn19cWMMikWKa2qKLCdlNXyde2AT4aEMqArmsHkCTOL215kDjmYW1QhSEpDYubXZZ7EcLIXO0nKAPTp6B7knH9FxacvtLBxE66im5kkUnTlgZs6orcok7QFT4RWe47L2PE7ylaiReYouwqWiPFuGiTTBVlR9Ma16jKham0zjTTYof5sy1ef/+Y3gJ2d2csWxnpuJFKZ5BGNQWwPFb8O5/7Z/zFN/8wP/TR/xc/OfsIyyPVMGFc25XdiNPvNWSPI/6Pt/4pdwonjd7JlqxyGQOKvi7IjUvtXFnNyiboomGqxaAqg0mcpBpcMZsozaqKiVaWrFKMDwzpg4h4bjiMp+y35uxFMwzww/1v8q3dK3x5foNv5NfRhXISNq1duEndgEbGODC4YcV9EMstKpqfaeVYdg2wV/bXY0cYia6AtY3s1xAv64YpKBNEwCULDTupyoRRYyjbDgwQ/0ktY9y4QJioaK4nrz2LUVKii11D/13HQK1byrEHZeFVYJXMDbzktm4pWmP3sEgvVuhBH9tKKHuKZGobCX7k7huc32fzb5smUFaunSon11ZKEc/LBmAEldcoNk3GlAC3OFDRndfJw61yACPWtaELusGDkGjlPQklVKXsr9FSB9LWEGlsHMY2u3OqfscBng1gSHPNlJUDQGvjGJd+PWu+eNTGe9YBmFgTL+Ci6ECpG8BWpNt40FdXLoAnZCC6ZF7xo4sQ24I1a3sNZsxNay1/rgWAs2htoVQM7tasdhuAVstYwQNideKYdB5olTXYtwvOb9Y0feCB9eY+rd5gy3XfV8xvRkTWjUOTKM8iDKX30dwB0HbRYnyZ0hk7cEfSb4UhJj54ktIt14hy7OjSWCZl5mWzLozDMRA9KzSiWY+bOSqAp3YS5rIxvXPriltfPIAFVGVElFgfWqTyqGH/rQN/0C79ufXQrcFWKwyKlUlIVO02QpQl0TVot6lS9AV0bhKDk3W7hmFNAIlSaGUbFinNBlmz/ihFYSNnaZE6/8cob774VorsImDoNb6dZUcxe9Kg6iYsSlmWtfsCk0SGed1iJ15Q2giDcuM4kWtxbEIXmGKZ1ZljgNqYVFX+ngGmVQ9jnVftyjqv3YuiS6+7otcqqIxmusxIJop8qHx/Sz8rA9FK4b1SZT0NntuSbq2aBHJVWbf3F3x/i5RxHsktyPsKXTeMw2btTRbumSdWDY61rTzT1W59Gfzu8eE4ttlCIStEijopjMVLSZJLhWknhZsUAmVZerP7vb09Dg8PGY1GtFot5vP5RhHxQfI8OcLiOGSpbcuXtoEQ8SqTQissXMJiUTzK5P4k3bLf7zOZTHxytLTFvXv3uH79Op///OdRSnlzermXkGEpEnABPbbZH9ZazyiTglR8Eh89esTu7q4PahFQNM9z3nvvPa5evcqVK1dI05TlcslyuaTX67FcLr0P4dHREb1ej+PjY775zW+yWCx47rnnWCwWPHjwwDOxAF+cDwYDHjx4wMsvv8zXvvY1bt686QFhYU1uG/aH3m3iJydglvSb1i44ZbVacXh4yGAw8IEkktx6cXGBUorDw0MmkwmDwcAzQLc9EKV4FSA0bNtQviqH9K9co4ArUuiGfmLz+ZzDw8ONFOCwWBbQQlhBrVbLg8wCDEiQg4QRSP8JOCfXu1gsPDg9Go08CCPAVnidIufL85zhcMjR0RF7e3tcXl56MGyxWNDv9zcYmyEgFIKqIeAjgOIHsQZDwH1bairXFp4vBB5DRlLIBBSm4Xw+9/0jny/yevm5WAyEIK6Mv1DGHm4abK9voZx0uVz6tWw2mzEajTzYIsDucDjcuOfwkHEUSjTD+5f1bDabfYdMNs9zsixjMBj4axEGVxzHXLlyZePc2xsqYQq1rM0CpMlYDQHEKIoYjUY+BR5coEbI4grXPlkTxKZCvEpl/ZM/ITsy7OftuReu62FQjazDYb+HIJiMe9nYCUFvuS8BkIUxK++T8Sz9Kfcgc0PGi8xJYQoqpXwQlPSxSOW3gS+Zv9KGIZgqoL70iTA/hW0cMhPFkkDAR1nD5fxhm4SsvxDoC8dguAaGc1iOkJW4PZfFF1favtfrMRqNfB8UReHl071ej+FwuPEMDoFWue6qqrh+/fpGuvTx8bH3DpUx/+lPf9oDppeXl3z961/HWvsdcvhwM2qbGfqbOT4UAGIIHIqvmY2b9MpR5Aq4SmGWEcm8YdRJPykHptSTFFU7udtyX1N1lWfXZCcaGzuAJSq3dpd1AAgS+MiJL5JaF88ipXVFrG3AC/c7n+wqg9ME12gtNokwWUy0KLCtxDFNyhpdQTItKYYpdqAoeprVrvapuwD5QHuGTJ24z9x9s2B6I8GkrsDdYFsZx1xMkppSJIfWAaFR4YAId62Adb6NJo3RNYyfcAyhk//uBq2JYyEVfe18J5eW1ZWKZBTxn/34H+Tom5aLly0sMobvuULMNmydv/b8/5N/u/h3SSbwu7rf4q+Pvx+DZS+aUXUacM2qRoJq0Y1H4X/wrT/Mxf0djieGb37lNvu1A2KqLKa0mspEdFVF3RT5yjjAUpcNkzKDrqooa1fIGmuZzDP817d+RTqNQDsG41+4+tONp6LlQbnLvekOrVbJ/XwXSU12cmHr2xHj5MBhMaorxzAzsUjIm8K3tpDERKuKutXygTshOBgV1oX11DU2ibGx2mDcSMiEMg4AN5Fa+7lZ20hZXTuUN3PO9xxrCmXRVbxmjEWKumX9nEumLvE4XhrPbEIDT98kP+iSDyPaD919RisHlDpZKnTvzlB33kd1O5RPXsHGmmLgwhxsrLGDdgMIGqhjJ19elai8cPNmNIGDXeclqBQY496XRg5A0Vtfenx4ivX3YiO7BhaCuaeshdx8Bwi57ix8cq8DXBtgsLkOZJe2cqAiZbWe17p5XSUeBg3gWhhoJx6MsaznHbifCSBXGEdtjheuD8tesKYZNhhgPuW7YT+tf4Eff8JqtpFtfPGitSzcZUv5w6SWya3IA84yNnTpfN9K6+ZRVLi+didx17ATzbEi+63WICBxhIpjv05Wdg1OOsYbTvps+Y5QCgk4MSmY6yvsRcqzL9/nsD3ji7yAKhXpJGCaaYv4Ydpg/fX31/xAEo6rWhMvFMncuDnXAJD5rvLzWth9qgCbOQZiaeM1IKXXTMkIQ23d5kzVyK+VAZsa4qVumODrdHXHLnVMRxNDv2EeAixMSi9akdcxqlIUPdevqnagv6otISveh3w07Zwo7WTEjZ9l3XIS5g0afWSpM0sxVMTLtbx5eaC9LNeNoybdOFeUfbCxwtbQiiqXwoxjp06qNvup29Eu44hJ1WY3WVBbTT9yssJpndGJchJbk6mCVNVkUcl53SNRNZkuqVF0lEtj7uicvXROr1UwaLlz5G8OSFdroNMk6762MZR9S+t8LV8XZqXrLveeP7b7a7z0hx/wuBwyrtscJFMiLM+0HnGgU0jG/PXf+bc4/b6BD395c3nMP/xnn8YksNoJx79azzkF+a6l8/C7AOKH8diWy0nRLHK74XDo/y3BIGVZ+uIwyzJGoxGPHz/2X/CzLOP8/Jyqqnjw4AHT6ZSiKOh0Ol4qHMqQBNQJjdhhndQpgEEIFMgRsoxC+SSsAchQHiyAh4Al4kcl4KHWmslk4tlqErrQ7Xa5vLzkxo0bvPHGGx5kkHMBG9eZpqkPwhAAKry3kGkiIQXdbpeDgwNOTk64du0ab775pg9Y6XQ6Pljl7OzMs0oODg7o9/seFNzZ2eHWrVu8+OKLnklzcXHBL/zCL/DEE0/4QJXpdOrljfv7+0ynU95//30uLi48g+bKlSt8+9vf9my30WjkC3YBdlarFfv7+wwGA8++EwBR2l7aJMsy9vf3fWjC/fv3fdJoXdfeW9Ja69l2YaEcAh/bHn4hmBFKBauq8sWqsLzkddIPwuIR30MBJ6W/5F7DfpaAHBmTgC+Mz87OfCJ1KO8TBmkcxx5YrOvaS1rb7TaTyYRbt25RVRXvvvuul6Tv7+/7ollr538nSeHPP/88g8GAi4sLz0AM2X6hbDNkS4ncWa5D+krGtQAy28W69MG2jDgEY+Vzw9/LawA/poRVJn0WfpYAEWGKeXheeV0YgCMg2zb7cBtIMcZ4FpiAPCFYFK6JAriElgzb64i0nQDLwjyTexLgVt7X6/Xo9XobY3mbBRieP9wMCdtTfEIFzN8Gk2SNE9n+0dGRvzYBpFerFYPBwAcaPf/887TbbR48eMDl5SUHBwcbczkE0EJQMFx/t2W3ApqFa3zIKpPfCfAkbSVtu81qDQOjwmfW7u7ud7AmJ5MJ8/ncg7jbzEppu36/7+W6MofCa5T7De9dDnmWCRAm1ylMcRljwnqWEBX5f8j6lc0LYRvKXDbGbPg5ynWHbSDtL+tluIEl9yCsW2HLy3omY+/27dtkWcbJyclG+I0c4TVIO4QsYJmz77zzDt///d/P008/vbEmC+NdNvp2dna8/2dd1x68lr7cHkviWXx6evodGwb/suNDASDKEXoRZZeG7juxr4WqpaLsK+K5q6JUbdF55YAJC+l5hIkck0hVDQipA++4pvhStQPXPHvQ4sMNHDMraPygYJOUVM9QlMEXAI0iVwSa1EzlJZimnzqwoozQ4wUkMaurPeKF84eruprVUBOVsHOnJN+JyAcNCAVesmhiqLOIeF4R5bEHjxxo2MjfmntXTUKNsM2cn5xj4xjr2qBuQdWJiBeGOlFEKwfCDt+pg2ANWB5p2t8qSS9j1LMz6irigjbXfrHi/KJHvgvZhUWdu2L1p2YfwSpYHVp+Y3Wb56+ckKiIx9WQeO6YPfHCJUYXXc0Pfv/X+LmvvsgPHt/jn0077P/yiP7dXS5eylCFocrcA6QVVS7PAMd0Asj3DK0z3UiCLZmq+ez1d7kWFXR0i8/eepcvpx91pvhp7aXhA73iT37t3+L//OKP84l0ys9evszpqMeV3SlvzQ+xaSM/LUPzS+3ky63Es8Z8cdm0Vd1SmNSxF7EWm6Xo0lB1mzHWspQNU0lSu60CWilWKepWw56xUPdqFj1QhYadgvIio/vIgTsCWpsmydZG0Honc7L0yoF+6bQmHziGoSoqx2zqGNonkQ/QsBqGb87Rq5Lps0OKJzN3bguDO81DN1pL4YuB4sEP7nA11qwO25x8MmmKakPnEdRNujitaEMuTCdFVw5YVJHGDDuoVQXSxga0MDzBeSWG33kappuqDCaNG1miUJbdmiDsRCK1BgK31xkF0aJ0Eu2okUW3E/Rk6diSAiJax5LEWmy7oed9wA6Pqs36PRrHRA7oUMpY9zPrAHmtLKpwmxxyPbpaA8VR3vgBggdIlAESQ4RlVScb4LVVoFDYliVTihrt17WqqzxYba0imSj2vlVSDKO1NLj5/MU15daLqGFdydGwKCMs1MozGh1rMcJc3SFRax+6ullbQh/ZOK2JcsgujE9DdsxP1aRBQxQZytTyyb33uN665Jd6z0CpYRS7eZGu2WbrNbdZw62FSpHqirhJCp5UGbVV1G3HrhOpODQWD0FXrsEpiwEnyd0C7uoM+lEgD2jk8CZSqEXk7R5ch7n3CPCoPN7s+sd5CUaM6zbGOsZpa2JdErJ2oHLdYj3+PYC9OfYWlftypotmE6W0oCFBUdoYVWl07jaFhJkc5Yrscg1yqtptVpVdRTnTJA4fJDpP+NbomOPOlNzUaPE8VDW78YJJlREplwwuK+SibmFQZKriou4yNW32oxl1440ZYXiqdeKCY5ThUTkkwtDRBU8MzmlHJZGyvHHpnkOLY71m6ApgbSAdNxt60i7KbbBIP5Y25o1yly9Nn+TxcuB8cLHkdcz3Hfb4kfavc6/q8Bff/EOcnA8wpQaj0NMYEktUQO9hTdnRzl/VmMbjF8qhcdL3R0F/f/f40BxhEQT4omU2m7Gzs+PN5cXPb2dnhzzPPftNa83Z2ZlnhMg5xANRQj6keAh9zuTzQwAxBAMEkAM2/OBCoCEsqEM2ZcgGkiJE/i/ypxCoSJKE8/Nzdnd3vQeasFHkj0imsixjMpnQ6XQYjUbAmoEYx7GXmwowI8VV6CcXFnDCvNrd3fVg1q//+q8zm82oqor9/X1//qtXr/L+++9z69Ytrl27xs/93M8BeLaIFMthOz9+/NhLjLvd7kZAQxRFXLlyhdVqxXw+xxjjmY/f+MY3ePvttz2wJ/cayiJns5mXsUs7jkYjsizj0aNHPkxF2FGhDLvf73swUfp+PB77AnQ2m3F4eLjR16FvVujrFgI/ITgs8m8JbQH850ZRxNtvv41SyrMwpRheLBaelSVjIY5jut2uB1FCBpiAXHt7exuMrdFo5FlcIgmP45idnR0ADwocHx/7fnzw4AF1XXvfUfGe7Pf7niF38+ZNDxqPx2NGoxGTyYR+v++l/NvFtYCl0hehNFZeG4JYIYMpZCPKHAxBvXD+hazUbaAlBG77/f4GMCnXIj6a8u+6dqE8AlCGa0D4bwGpQjBO7mEbVBTW9Hg83lhzBKwJ2yFkQgrQErK/QrmlrImyFiRJ4hPPAb/uXFxceBBMAJH9/f2Ntgv7T9bDcN2UexCGpYCn4ZoYyqQBH4wk7S6y3W63y9nZGVprzs/PAccavH//Pi+88IK/7xBQ2waIBZwSAC60cNhOrZf+CseQrJPCdJPrlPeIrUEIqoabRLKGheNRpLhhe4aBImFIkzynhIEXtrm0d+jjGI4nYW+GGxJaazqdDlevXvVjV+5LnjEhWCrrlaxZ8/mcW7dusVqtOD8/9+tguKkWztOw74XtuD1flFIbLE0553Q69XNHGNXj8XhDai790+l0NqwSts8v7X50dOT9dwXoFiZmq9Xi+vXrfN/3fR8HBwcMBgO01nz5y1/mjTfeoNVqcf/+/e/Y3BwOh379lQ2I3+zxoQEQvc+TEIwKS/+eYbXjAlFACiRI5jXRonIm+klTaFlI5mptCt8U4iaBOoIqs8TWgS6uIFh/tniF6doxYuR34kclIQMeJGwYPw7cVOjShQ6YNCIC73tX9VPiWY1eFMQjg2klEOuG9aJAK6LCULecnLn/fkkyKak7MVUWowOZvq6gdeGK7emtlL1vzolzF7gAIZPIXZtJIIkahuOsYflVTRCKts7OqVDECwGFInRl2XnbODP+VHl2St2E2Kx2I3Zeh/m0z+/4Q9/k1fQG55cDWr/rDPOT+6z2FGUXBu8a/osv/wC3xjXtxwn/8Td+hD/+7KsAfG16nXTqklXFfy6dGX7lJ76HnSn8TPQy3d0lZ5+7yuymonvfAS9lF+Y2YV6lrsC3ioVJ3TkqB8SJp1pLwSf775IoxcKU/MKbz9ALgmR06WSUfb2kmxYcRnNGxtBPVmRZSaQN74z2aR0tUHXPAUPG4lmIftASANMNWBg7r7fWpQwixz6tW5EfTy7Mp5FAW0stASBVjYrXHmRWQeskbsAahZ21iFfre5DQE5e43IAITSiKriCZu9fUbcXsaszi4IDOQ4UuI5KZXbdHDaYVE59NaV12We2l5EM376qedeFFWUXUqdDK0m4XTB712ft2xuSJmGLHOJZOYolKtU47tk213/gXqspgdASJRjdtYgYtx84LmMECOnp2oaQZG9MApY6RRg1Ys+4XA7oIFk9rnbw40g7Ml13gBqTUq9KBjh6c0dhU5OrOr9GWJSqOHUgo5wSfyu3ep7CdzG0eSOBOAyS61zYBNrjk4nHexqaGZKYdKNuwqzyDr+3AEGWtB7ysBp3WaBy7bgO4EFAtNl5+q+qtMA8N1ijKoWX0bOIANPBsb9UkMVO5NTIq19YNMhYzVToAUeOBLGUs0STHXo4xqfvCXzQ0bgH5TNxIftswv6LXst9mAyNeOE871aTj5iYmNwkE6eMyvmRc6IadZ2O3JtaphtgwL1t0k9yBchL2MW8YiCXr86FQVq1T1HXzANJQW8vctMC6NozKBktWa3sEax1L0FkPgG3XTlZr136KMlZN2jCUaxiXGRGGlq5oqYrSxIwWbZJZw0CkmcO12/xRtdoAOsPgJWMbOTuNr20G9UoRdUu0UuzHM3rXJ0zTHuUkoh5WqNhiTlOKnrsud67Noiq7sMSzkuws5f3TXQ5vzdDKsKwTWrr2bL3KRCRR6cKqqLy0GWDc+B4mqqawEZGyno24MoljIVrNYTyltBGzukVhYmZli2G6ovrEFPWNPrvfls2ENchrYsUsC1jc0jTSh83xD09f4fWffI7s1Po+SRaWv/PpK/zP/sgvc6885vQbhwzfUnRODbqyLPcVoxfcmjq9Ea9ZtEp5+49orv1Y/O7x4Ty2ATitNaenp56FIbIhSVCGNfAhYJpIA0W6J0WrMI0kTEKKEGHAyN8hEyn0wgpZVCE4GBauAl5KYqu1zvhdPP4E8JSAFDlf6BO1WCwYDocbDJUQoJRiX+TF4/HYe0KFQKdI/AR4EtBIrjFM+ZT3jMdjtNbM53MePXrEfD73BZ3cc9iuTz75JFEUcXx8zJUrV7h58yYXFxf86q/+KtevX+e1117zhXy73ebi4oKXX37ZA0+vvvqqlxDnec67777rzyfA5mKx4Pj4mJOTEy/Lk+AOYTCJJF3GkDAuhfW3XC65evUqRVFwcXHhQQvpjyRJfMKr+IDVdc3l5aVvMwEmt5lXMoZk3EqfCstHxo+8TsAdpRSr1coDRSHTRySlwlQUibn0g7BthdUTMiMlGGE8HgPr9FcZz48ePSKOY+8DKmNd2L2dToc7d+5w7do1rl27xs7OjmfNClsJ8CxUYY8VRcE3v/lNZrMZTzzxhJfqCniwPbdlTgljS/wa5TUhUCisvFAaGTLMQqBuWz65DXTJ3/IeAX8FDJFrlveFnoQCsoWWBiFgIq8P/fDCtU3+L+MyTCUXEFjGTJqm3jNzG5CJoojVarXBfg2ZnsJQlP/L+iBjReZbmqbs7e358SnvExuID7J0kNeEbRnKhwWkl36XMS3gNeBToEPWrJwnZEnKGrk9v2DTSiIcE/I+OZ/I5eVzZNzDGoSUuROyPMPNHplX0sYCpkt7CHg+GAz860Ipftgu0p/SZvJZw+GQdrvtWd2hZ6HMjW0QXK5Nnlkf+chHeP7557HWcv36deI4ZjKZcHFxwVNPPcXHPvYxOp0OeZ7zy7/8y1xcXHgAOXxeyNwToFr8UwUEDVmu4XMnbDO5x3DehfNA2jr0eZX+En/V119/nePjY55++mmWyyXdbnejT8I+/yDbAFkrj4+PeeONN3jvvff82nt0dMTv+T2/h9u3b3NwcMDZ2Rk/+7M/6xng0k7CjA+fgfJ5sob9VtiH8GEBEIVEEDBWlIXsvCJeRsytpmor0rHyZufFnmNr2aYF0okinrviQpcQKVewmVRRtSGZqsYYXcBCSyhL8km6UrBH61RXL2E2639b7fziJAm57DcyxcoBEwB6FUHlfNDUqoBWApXBttIGVHHn0rUhKgxR7oCYYhh7iaGkzNIAolZDMVQNoIVnBynrQDFdNaBUS5HgvKJUY8ovRZjJnJeaato8WhnmxzFRAdNbmuzCkl2YJlG0AQwMxLlleqhJx5br7RG/dPIc3RR+1/U3+KnB91F13fmrTBGnNSZOSOaW8jcG/J2v/RD/dfcH6d5T9O87gLVOFPG8Zn41oepaOo/h+OqIeZ4yu6FYXq0Zvq1QeU3dUnRVySuD9+kouBrF/IHBa/wdfqC5B8cEMy1LaeGvvP6D/Ou/4w1qLJy28NLAuCadWaqW81QEx6wqcQmprbimFVU8XvWx1t2zY7k2bDSlHHAtjDwBIBQNSLU5rlVZN4y5hs4s/YnyvmixeJOVVcNy0R7UEemyLsEW7vz5UNE5cbLj2dMOXY9yPHPIaug8Ngy/PWZ5rQdEjpFYWNonDkAve06qWPYsxZ6hc6/NtV+GxZWEi1cMeqdAKYsxCjuP0ZOY5F5KOoZ42eH6uRuzZa/x65Nbri1WWLw1EGvPyrVR5BNVfUhL7FBYm236qnhw1uADgMS3zgcCNWC+MtbLPd06Yj2oq0Kgz7oxgnZgpn9vWTufS2tR86UDA+NN0HH7UMa4c8Qi19VrL8YG/LGxdu3RrDmyvhUmQuVO8i3yWe9piBsftVgpBGCykC1XlfPUE/m1sztw7MAay8ok3sOtKNdrJkDrTDG4W5EPG6m4Uh6AvDh0rzGx8lYJvi+AiWmCgAxQi0WEfHaNbRlqa/z1JUtL2YxZrS2tkXjT4lnTZU95gFOOpWkS7mRNxr3epgYVuX7zthONtN0BeZZllbCsEg7aMyZVi9UyJWpBvqvXwVK2Ye02my02aRKpjXIeiEph0A5UpJmHppF5BztPOlc+xVwvorVXbrMWK+38Q6OVsxzQtfM8vKh63Fkc8PHBPS6qLvNv7ZJYSBZQdhqWeatJSlfNRoGw85ujRlFi+I+e/HHevblLaWOSJrDk2fQxPdXiB7MT/quP/U1WNva//3e/9ceZ3jmkNTGIPFrCuPKBYvpcTTqOGb6tmD9R8UNPvY3GPS/aUclOsqSjC95b7hHr2ns6AiS6oqUMLUo6umBcOdlepkuM1STa0NW5Y9HahBrHAh/VHeb/b/b+NNbSJL3vA38R73b2c/eb+1L70lXVm7qb6laTbLNJSqQoyobIGVkjCKKB4QgDARoDHgxGX2wDhgfjMYzBALZHQ5u2RqbhsZqkhxQ3cesm2RuLvVR3VVZlVeWeeTPvfvbzLhHzId4nTpxTWRQ531jsF0jcm+ecd4s3Is59fvF//k+VkeqSVJdUVtFrzzhudhidXxTtsdrNh7qCeCR/JwRzT6BAzG1EqivyriWe1M+ldGpRKeXc0AVVx1B0Y8qhQpd1caLUFWHr3nUKxOWiOIrh0waV19Whv1dE5QO3hSqeUGkh6jJRVIkH22Qy8T6G1loPuiTldFWlJmnBkioplUJDDyyBIWHQE6aHhqmpq9ceBu8S8IWBlRxHCnxIgB8qSSQoEeWWeDbK+QSgau2qjIoxvwS/EnxLECwqr1Xz+tBzT4JCUXKK4uXixYsURcGbb77poaEE8o1Gg4cPH/LEE08wm82Yz+eMx2Om0yk7OztEUcTGxgbr6+v0ej2+8pWvsL+/z87ODlevXuXSpUu89dZbXLt2jcFggLWWzc1Ntra2fFAogeLXv/51Pv3pT3uljMBfpRQHBweMx+Ml5aCoeOI45oknnuCNN95ge3ubs2fPMhgMfLXoqqoYjUZeiSX9Qp5TURQMBgP/7MN0yzCVNIRzq2qxsM1C9ZKAoePj4yV402w2/f7hfUrBC/F2nM/nnJ6e0mq1aLVavgLs3t4e/X5/Cfysra0xHo/Z2dlhPB6zv79Po9Gg0+n4dhEwJurEBw8esLW1xfHxMa+99hrPPvssFy9e5Ny5c76/C6h99913uXfvHru7uwwGAx/8rxb4CJ9pCO8F9rbb7aW+Kc8kfG0V7K0qPkOfwFVvNTlfmI4awocQ7IgiV56bgA1YeLKuKiTlOFK0ZFWRHM4V0hZyX+L3935eaiF8kfNJMQutNd1u1/fHra0trLVeBZimKRcvXiTLsiVfxyiKOD09XQKoAo2kj4fqzfBaQiAe3pMogAGfrv/iiy9y6dIlqqri8PCQO3fueOgkbRT6GcqYggUIlLk5TEmXxaCwfcPnECoD2+02SikPRuUZSTuFiziiBpS5WfpDo9Gg2Wx6yC73K2MpLDYTxzEbGxtL32OyoBFWgpbvtNlsxoc+9CHKsmRvb29JESvtI6AxVDrL9R4eHvKd73yHS5cuYYzh4OCAhw8f8s4773D37l0PtdfW1nz17Xa7zWAw8N9PYbry+fPnKcvSK//PnDnDV7/6Vcbj8RL4DL0RpU/Isw3niVAhG96XgEN5XeZS+T47OTlhf3+fLMs4ODjg4ODALyKtfk+EYE826QdVVXHv3j2+8pWv+Dn/ypUr/MAP/IA/53Q6ZW9vj+PjY05OTnjllVf84trx8fHSOJd+vqpK/LNuHwyACD6dSjapPhxPKrITV2W19ciQjA0mcWl2UW7RuaVxYClbdWVE46r6Fm3tqn1iaT5S5D2XFiipvvEsgIECLSUF2bh0YYEQVlMfy20CMAQ8lk3QCV5pVHUbWK3Q89J5d2URTHCpmqXBNupKPXUKqsrNErzMjgryTuahZjo0THY0k/OGzj1onBifuu0UkvV9xNZ5lFnnKVYaF/zY2IFUgRQ2tigBrJFT8dgI2g8KHv61iMlVS/daQvuBAW3RNSwomorJOZcm/LH2DX7p8PuYbRv+5+9+hPUT13bJAI4+bPjMpZs8GD5J1VA0ThzA1KVFF4bx2YR4Vis+m5rxWcXWx/e4v7XF39q9yS9/7aM0MoiH2hWXqVz1574u+Fzndfo65dTk/P74FdJjRdmpA0mlMJ0SAzSSkgjFzBqiuQvGoxzmxw3n5bim6arSeaWhaNSkYlbUK055zNb6kKJVy4JkUMYRajJbTs0NN7UIyKPcevVQlWkmF0tad2Kae7WaJXLpkq1HhmhWUfXbEGvynnZA1Lg+ja0/UzhT/8lu7ZOZu77b3hNlnlPLVpmrvKye6dG+N6P1KOLg5drbrFOis8p5uBURptCocUx66lQ24zPa9ccHGf03FJ0HJdHcYJUDb1Wmmfc0492Ik6ci8r5deJgKwKusg6BaoUKpr/iV1X09HuZEp3VKaP38RC2IMUspzFb8JBLtFH81/LcBoPRziQBLrTCtyKdpWuU+78FjKcSuXrGdlCRl5Y6ZJXUldfyxvA/j6qNfna+VA1PuGbuFBVOnRJvUFfqwqaW1ZxagKMLPPVXD+RkupcMCWhsiBS+s7/F7H+1wPEmxlULHBqXhh556C4CXGnfY/sH7zMqYl/pHlEbz5sEO42EDtW4ZXozJe9JZ3Y+wOnU8s8Qz61ONpU/39MwvWKAtJoLZVgO2GrRiTdQpiJTm/3blX/Laz57hXrHOmfiUV8dX+J9e/xjzdch7KmhTd6/piUtPbiYVeWRZiycMqwYqq7CldrYMpaOvKlqoMlVp0JWtKzKDmkRMi4TN1pjNbMKbJzuot1ukp5AMLfFEOgjMtlyBEmWtV5irCkgMubU8Knuo2FD0LPmaa5+8b2mrHAOsdaY8OtNi/8MJ8zXrKh+P3bU5Aaa7rioDE3h1Hs9b/O7+MxyOW2gst4brJKducSvvKq+IjWYWXfdjU4Nz8edFQYKhpSL+++NP8YvXXqEYpehxRHqi2f2r9/mtF/8l/5/RU/xffv0naDzSJGOXPn7ygqLaMOT7rlCOL8wii2im7t/WogpFP5mSm5imzjkq2nVl+4itbEQ/mjKqMlrRnEd5jy29KF41qSGwKEErNC01Z2ZSNqMRiSo5qdrkNkIrQ2UVkbIUJiLRFWlUYRNLe++984vVMLziqov7Cux+rDjYGSlDMyr8AqPMC1Y7uA+1V6IsCgZ/f+jc2aUMLsfei1bVRaSshmikMel7fVq/t30wtlCVASwF7dZajo6OaLfbPngMCw/IH/CixAi9piQYns1mZFnmA0BRf8m5JNUyTPUNVUVhKlYIMmSTYFL8zAQqiqJIVDeiTpJARxSFosCQgF48HsXPThRf586dYzAY+ODu+PiYTqfjr2c11TFUuoVwVu5NKpJKW7daLTY2Nrhy5QpHR0f8yI/8CH/wB3/AnTt3fDAmBUiazaZ/rdFoeB9BpRQPHjzgIx/5CO1220POBw8e8K/+1b9CKcWdO3fY29vz7bC9ve19HcNqyYAvKiBw7OTkhJOTE05PT72X4f7+vg9EpY2zLGM2m9Fqtfz1VZUrEnN6esrp6ekSKJD+IEGtFB/Z2toiiqKlqrGraaarAFyOEz7rLMu84ms0Gnnfr7Is2draApxCamtrywMf6cdSKGh3d9crcN9++206nQ7D4RBjDP1+36c7C6jc3d3FWsurr77KM888w9NPP43W2qfwjcdjn4J84cIF7005m824ceMGzWbTKx7v3bvn217GUrPZZHt7mzt37iylpIfjZ1X1Fyo5RckkSiTxpJN2XC0WET6nsB+HfVteD0Fk6C8YKuZChZ+8HvrHhRB0VUUZjrnwWkLFVXhtco5QpSbz3NHR0dJCgBS2CBcmwr4WxzGPHj3i+eef5xOf+ISHV2macv36dW7fvk2/3+eJJ57g85//vF+Y+PrXv+5tIDY3N72/qsA68dQMixuF9yswKoSy4id77tw57426t7fHyckJn/70p+n3+8xmM771rW+xt7fH6emp96OVcSLASuba0P8zz/OlthCIK4sw4bUIHBQwWxQFzz33HJPJxHt1hgVPpE3DFGZRJctcFscxn//8570qdGtri29+85scHR1x4cIFfuAHfoDz58/z9a9/nf39fY6OjrxKWtptfX3dX3fowSgKavGDHY/HHi5LPwiBddg+8lqr1eLll1/mmWee8Up38Rd89dVXefnll1lbW/NqzOl0ynA4ZDgc+nlA5nE5x3Q69TC31WoxGo289YKoFMO+H4JNeZ7iHRx+v4XwM4TpIYALx2me594XWACszPGNRsO36SpIDO06wsXIcPEs/FyoWpVjyHPqdDo+a0DOIZYY4f38WbcPBkAM4JkuAQNFS2F1RN5RZKdOmVelinTocsm0daojkyrimXXpaOXC+D8dusCjbGuiqSUzoA7r0+mFAsYHJQFM1HXAYuvCBvEsVDctVEGSgpuduGqbHjBQK3Mq46otRwqbrDwqpYimpYOMlQUyt49xKkZJVUsmlsZRwXi3gaognlc09l0hiqKlvIG+BDPe7yy2ZHHFvGYkJla1csdCZCmNRpWqruDp9s37MRjLlScfUVyKmP9Pu8QzBwpU6c6jnxhRzhJXQbRSmLNT/jcvfo1f+e3vd15mGaS7E169f5HLBxP2X2ly+mJBehBTZZadP45o7peYRBPlhnhYkFyKMVZx5vIhEYbGXkzVtIsUNeOUct/Kz/A/PPwk/+zKr/Bu2eK3Hz0H2t2zPHfdqBjXUaNWLrCMJ867ysQQn8bEs9IV0MCleTdUxcy66s7zWcKsnVDlEbutEQ+KnbqCtnUKQcA20mXgHcaRdV/WlfNdq3oZujDk/Yh4qEjrhTeTOCjdvz5B5yWnz3Y5eiHzfTIZWu+Lh4LGUUnjoSMgg8trzNegcejUtWULyrbFtCtUo0InhjiumH2rSzzNGJ3XFJdm2FlE892U7NT113Rkaq9EAxjmG4m3CzCpZXxBM91JXOAcuTRTdA00tPFBvQT4qlLo0iyp5ojqar4C/aBWzyqqVoKNNLpWaWKcj6BVylVFrp+9K0Ai0mMcbBRFqIyrWh2KtQ5CGqeK80Va5D35I0++JDTYNDAFzmIHD9NFKrmMV5uo2stQPlwrI7WcV6346q2ATe2AjbEKVSjG55bVTaJmjSeqVrO5AhnhZxpK8bHOTU7ONhkUDU5mTSZ5Qmk0zcgNmERVzmOv0jycdJmVMUVR/8EzcoUp4olaqkavDMy26pWzeDGe3HW5xQGtTK3Sw0EYDXlP7qFLv3sCwH+69yP869eeh1y7FFwN8eaU1kO7BGJU5dTUUiAmzyOIDZ1oztzG2LrwiqqVukQWWym/qCPgVypO67kii0tGecY4SymMK6DihHCqrnqNh2Vqrrz/n637tqtoDy9md/lnn/nvGP7VJokqibCs6QkfyQyQ8Ysf+nn4kKt2/P86/ij/7a99zlexdkpv5eCmgarloKAuDA+HHc52h5zrDRiVGec7pxx8uEP8rQ7JqIa2yi3qVKm7Hl0XyQoVma5EkOVrB5dpf6lN49gtrMUzw/3negCkqsTGTsGYjNx8YmOniEzGOOVqPd8oA0UbV3FHxm/qnkWhS6aVs4/otOYYFJF1ILEfT4mwtKKclp5T2JgKRVfP+MbkEo1WwUY8YlA1KWxEoirules0VOGrNuc2ItMl4zIj0yVzSYFXMNnRC7hpBXTjChABOpfFk3qBQLlCNwBa2YXPZ72vrp+3pk5Hj63vi6IotJElGTgPxLKhF1XAceNieta679HvbX8pNgkcRbEh0E8CqNV0LgmQRVEisCisNitG6UVRLFWbBTzMkJ8hvAyLWwiMkPckKJLqweLZFKoj5LOrYCO8B4GdYUAkgbqojMAVOBkOh2RZ5qsRr55HwFuojJTgSVK4JSVWAnUBp3mee6XSnTt3+NCHPsTTTz/N/v6+P1a73aYoCt555x3Onj1Lo9HgySefZHd3l7t373rgJDDi8PCQz3/+80tKn69+9ascHBx49VOz2fQVn6uq4sUXX2QwGHiPqzA1/Nq1a7z44ovMZjO2trY4OTnxKdcCP+T+BZBKACpp0ScnJx7siS+WBMGiyszznP39fba3t30arwSe0udEBRUCJDl3u932IEMM+gV6SxpxkiRetTkej1lfX2c2m3FwcMBwOPTwV+CaPP9Wq8WLL77I4eEhzzzzjPd0lL49GAz89fR6PV/Y4e2336bRaHD27FkfWAsokkI5Tz/9NOPxmOPjY4bDofdqlP4kbRleY+gXF/apEC5IO0l/FLgX/lsF7I+DheHrq6nKsm8IAkQBFoI++f8q1BA4EC4ohGnSYfp02A/CLbzH8F7COSOEFNI/xBMuXFyQ8RKeWyCNtdZbMwC+/eQYkrocFmEKve4EHMu8s9qmq/ckixJSlVyuraoqDzyVUgyHQxqNBhcvXvTzshRJEQuKnZ0dv7+A7NBiQRZipHJxCGdDRZ/MW9LPwtRWUfyKp+lkMvHfD6GCTeZ3aQN5bzKZcHBwwMbGBpcuXaLVatHr9Tg+Pub69eveq3Btbc0Xo1lbW2Nvb8/PJ3LNolTs9Xr+HsRmYDabcfv2bZ5//nnOnz/Pw4cP/QJSVbniRqIsD8Gd9J92u8329ja/8zu/w61btxiNRly9epWf/dmfpdFo0Gg0eOutt/jGN77hx/Ply5cBGI/HXokqqnUpVhICb/m/FPqS72f5rgmLi4RqYFELyuLfdDr1UDRcHBDoK9+LsiAn40PgXZZl75mD5fyhSlCuJYT50idWIWxVVR6Yh8eQ+z05OVlaWJAFmPB75s+zfSAAogTIDji4gDAZGwfZ5vgKjJMzCpPENA8rF9BODcm4pNrNXFGVuiCEsi71VFLK4pnFFsqrkDz0EJ4RB4EoThkXFRDX3qOibPTFLjyMcMF2925J49EcPS9d9CkgUTqXwAxbKxxrAKILnAJCHnqdVi0Vfk3iimDo3NTp0VA2NMfPNklHi/vThbunaKq8z1eVQllpdB580ZV1wBhZiiJegB9bpzt2Nb03FfkTEab2/4qnLujPcQA3igx2HnFStZyypm60oquY7VREdyPMOx2yR4r57pzJWcu//fFX+e7pWTayCdfeeY504AoaVFlEejjFJNCISx4c9/i98imajyzHL1W07sREc+chWLbr1BldUVhDWxXcH/Qc3DSi9rMoDYXVpHGJkS/P3CkCwVX2VqWlaigS5YLMlrIMrXUVVktX4MAWmtxENE4qqIwrrlEDK5cqHS8Fp7q0zlevdOCwaEVOBdps+YI+G9+FaG4Wqfq1gk4PSvKuUw6mA1dV2bQd+Kgy12cPX4nZ+VqPzv0ck1nynZLicoVSFlNEMNMkxzHxOCEZOWDQflCSnuTkvRg7ikkPI/o3XBGLoqOY7kYO5qiIquGqtdqscuOjUOTrNciOrKu6q9zvaPe+TSy+arACPdXo3MFCUeChlEtprvu8VT4ztIbaMaYutqJqn0lXyTh5jypZVQYbafdTqeUKzDWwdwCSReGSYDXPRvWXT+UKtqiqgqJOfy4rbDPFNBKnQFydh3UNsWTsg4NoAjPr8bu4X9deqqwL8YiPo8CZyNK5a9yzTvA+rACTC9alx0p6buHaQWnLzFr+2Y3PMPv1HZKhJZlaWoWrmvyrP/0h/q9n/4g/GD/L8H85SzyxPLzgiqjMnyqI2wVFzzLZWU7nlZ/RHKqu+xnNnDerqucsq111Z+kHxJZoCmvfPkHtH0FZ8uDvXQDg9eNd+t9Kae0bphuaogeTdsL4nFqoyepzVpmlceBuvCqdx8Koylyl4ko5H0BbL/BIOemQ3Vjn3aiMpeoY5mXMeJaSVxFnOkP2nsxZ+2ZK62G15Ok4uqixmfUFtJTASm1JgP2qx7//lb9D8m7Tpe1PLLMtxX/xD/4Zm9GYv/P//ffpXY/oPKg4vRJRPlkSzSOkALYSBWIPqrZBWaeUPdsdstsasJ2OiLWhNJq3m1tMk46DqXXldufNqGq/1Bo+K7cwZGKFxlJY69J0+xDPah/UEhqNAo1ygKz+CtLF4vuubDols3inhtYcqtROlV4DW4HRnWhOaTQGxaRKqdCURjNSGa0oZ2YSJlVKpktGVYZWlrVkymuj8zxI++ykA46LtoeGpYkorKYXz+jHToVcWs1o3uZo1mIwy1DWqavDPqqs+84dXXYgtGpGSAEVcO9XuArcAtRtVKefz+1S32nr+VJ/kuJBqlLka4ph7KxExDpBQKbOCfwjv7d90LbVP9wF5EwmEx94NBoNRqMRk8nE+9VJUQkJ6iWgAXzQur6+7oPMg4MDDwp6vd5SihU4JYKkGEsaKrhAazKZ+JSz1bRCAYxvvvmmP3+odlhN65J9RU0hlZBXwaIAm0aj4VO2BRSJsf3bb7+95DUnBRTkvGGanhTPaDabPj03BCDie7i3t8fm5ibGGO9fFqr75NgSlAs0kmtvNBq89tprbG5ucufOHX7yJ3+S3/7t3+bNN9/k8uXLPP/883z5y1/28EEC0ieffJI4jun1evzRH/0ROzs7vh9Iv5hMJh5Yyb2MRqOltHOBKQJaWq2Wb+9+v++ftSgdRYkTplDmee4rwkofDJVDEjSHilN57qJYFBWOQMHj42OvbkqSxKtK2+02d+/eZWtri2vXrtHr9XwV0rAwglThlqIbN2/e5MaNGzz99NM8+eSTvmCAPNOiKLh//z7dbpfhcMj9+/e5cuUKg8HAH3dra8urDquq4uTkhAcPHjCfz5lMJrz22mvvUbDKFkISUQd3Oh0Gg8F71D6hWikEggJKQkgUzgPh76swQP7JPquKstVUR3ktBHWyf2gnIJ8PKwqH4zoEjo8DCOE1hfe+qlSUxQ5J5Re1VnjfAk/CffI85+rVq3z3u99lb2/P99fz58/zD//hP+TXf/3X+St/5a/w+uuv87WvfY2iKBiPxx4Eg1OujcdjD6kF2sh1r16HXEOo3k6SxFdsPz099X1wY2ODnZ0dfvM3f5O3334bgPPnz1MUBZPJhEePHvnjyjwrackh5JR7lraT1PgQIoUgXK5fFoFarRZvvPEGTz31FDs7O9y7d29pQUL8ccN+EvrIXr58mU9+8pNsbGx4NV6z2eThw4eMx2OuXLniYeDJyQlHR0f0+/2lOdUY44seiTpYbAiMMV4B+tRTT/mFgG6369s+VDKHKcDSP+UYjx494vr1617FHHr6VlVFq9ViMBgsjdm1tTXfVqsQXxSnoUpbIJu0cTgHy9wp/VOel3yniEVDOCZkP/n+BLxvsMz14rUq35MyX+zs7PjvpVXVb+iVuJo2HY5ruZfQ9kEKfIkfI+DPK/uUpauWLd8Vf/kUiOFW33uUW8Ay2YpdQK6gcVincG5H6MIBrezYfV6XThmlq0WALynOyrpCKkorosL6YGmpmEoNFX1Qba0vNCEpz4tAxi6BjaKp0f2UdKCIiqoGLa5ogq7TJEN1IganVpI/3jKJ3uqUMrn+GURTg0k0JlFEOcSzir1PK1r3Ilp7dlEF2jqVXjKqA87EMp0nLv2vEIjhgiEVGZSyqNyBJpMoirpC6fSMZbi3zubWsIaYirzr3ovHlvncVR/+r975LLpQJM2c47LlgtvMYKKIZOBS8JSxFNsF+3mHWZnQbufo0qlGosSprEwWk55aBv/zOboFHH2ozXqOUy1N3WcBTMuQqJK1xAWbFYrZNGXtgWXed0VUVGVoNAzfnZ8jjSoSpRlaQzx16hrddPQqyp16LgLWswkNpTDa8NHOLX7RfpjKKlRW0UnmTAvr4CG4ysuRxhrj21PXyhXnZafRuUurnJxxAK7xULt+21aMuw4KVy1XmCTu50TXW5z/oibvKkZPlpBVRFmFNcpVBi00ahoRzZzCtmxFVA0cXDlN6LwTsf5WgS4NeU95/7qyCadXY6pm7ECggmLd8OgTCqtNrXKy6JlToppGHahXru+o0qlVVV3IwsZ2MTbMSvBsFDat1Yxzp4SSviybVKvWlQCpuv9rd04UWC/trYdG8FMZi6nVhipWDrar4AT1r86XtAaP1vKeKdICavGa+ACqGlwuAcJ6TLoOZxdC09X09RoaLqU4y4c1Tl1Zj3cbQWU0yipGF1xBkbAwhi5cCuXjqrumqatAbqx7vrpw853MAUlaotFoZbzqrmhb4khBbLBA41jRemgCxXW9CFFa9j/qwKdPp5b3w+ddP3+nDAWbRKhuG/vwgCiyVNY4AB9JYSk3L6vY0L1VK/KkMrGBfG1R8Vlri40Muq7sS6kdNKwXWXRSOViu3PVaVaev1qBWlZqjkzZZo2CaJzwad9BZxWwTrI6oUjwwqzLqUu7u3CY1gCbOSirgTr5JdK/B2nVDOqzHSxRjcJYHqnK+htlRiT4foWfaF6axsaora9cq4rn2z3haJjSj2pDbKmLt5mIUNA8MRV0Uy2p3jTY1qKr2N7F4RSdAVHcy97zw/cCl8Lq0YVHmRcViAUzn9Xw+sbWPpetLRUvGsXiVKo7LFuOyNqxWlhuTLQqrMVZTWk1eRTSiklGZobH00imTMmWnMWIjGXO+cQLA3rzvFmuinFGZcX/eZ2/coxkXrDcm7E+dVcR2c8SZ9oBpkTABJtva36NPt68gmtXBWz3e/Vb/p5DcZbXor+4ZyiRWVxWv9y06ygNmqw3JyFmmVCkLr+T6OmbbToGoHjNGv7d9MLYwuFZKLaXlCfASpYsU/JBKjKPRiOl06tUpEnyGCgNYpIBJyqQcM1QLhcBPih00Go2l9MlwC1UUoSIm9EiC5bTsEH7IeUPVY7PZ9IGRpJ1JgY00Ten3+6RpSqfT4Y033lhSf0lKsRwz9IoU2CfqDoFmEiiVZelVMOKHKMohAViSxixKpDfeeINOp+OrQQt4uH37tk97k6B5bW3NK2oEHEoA+9Zbb3k4+vDhQx49esTu7q4PSCXIl2sSs/v19XWKovApZcYYxuOxByaTycSn/Qq0kyBUAKKkAs9mM9rtNp1OxysX19bWfBAubRz2z9DPTVSeol6RvtRqtXjuuee8X10ItXq9Ht1ul6OjI68YElUmOGVgs9n0xv6Hh4f+WqTozsbGBnmec/PmTfb3973XpjyXnZ0dbt68yWAw4Pbt2/45SjtKcCzAWfqrqGsfp6oLUxGl38r43N/ff6w6UIBROOZDILSqBgzHT/j7KvgP9w0Bofw/9F2UY8v9hUrk8LmszkdhKuQqyJR5Z/V+w1RN2cJ+E/qTShEVAbECY1ZhZwi579+/z7Vr1wD8GJYU3SiKGA6H3g92PB77MSJ9Q+wPwhReub7Q1y5UboXzoWySIgz4uUu8UU9OThiNRl6plyQJu7u7S+pK8VCFBaANq+vKva/2T2ljAfbijyufl2NMp1NarRZra2vvAeACocIiWzJXb2xssLe3x8///M9zcHDA5uYm/+gf/SM2NzdJ05Q7d+7wxS9+kePjY55//nlfufj09HRpoUhSmGWxQdpXoJgscKRpulS9XLxRZc6ReUi+a6Q/hun2oWeotJOoIFd9LSU1WZT2UkxJ7B1EFSr2C1KdWWCqzKOrwE5ekz4j310yN8p3pFzLap+TcSvFdoD3eEPK3CTPMZwz5J5XxzosfJFX+5bcq1LOY1T8jGVhLywiI6nZf171IXxAAGJok+ZSidzv0czQvV8y2YyoGormYcV0I3IqgMqBo+xk0WjxzHofuHjuVErZaR0kJQ7eCJHQJZQNfIqo+GjF0xpeBgCkbDpliKQyW62wWHQNjuKZ8/YLwYOqzAIeynsGlHHKSivG+Br0KCdpJ8y2UhcYR3V6mYWiW/tOzC2tB5B3NB9+5R3eefvphQJO1QFkLNetMNliEhZ/M13WCovIFccQm1+BCcnYkjwzpHjU5vi0zRquIEqV1gViKmh/vUXehemNLdI5tLKcYdHwzyyewPgTU/huk2hSkHQUn1u/xrntY37+4WeY9xXDiynZ0FA2nN/d4EkHaXUFpl+SDWNIjFdHEmtUo6KwMVktsRyaBsWJK47iFCZO+ZbGJadVmywqyVTCfqWJZlL1GtRMo2c5JoatqMl/cumXSZQGW/G32vf4p/OI0SxDaVtXu7XYOHIQMU2waYwqylo9xyIVHrzfnVVOWWXaFdMLlvmHcqLYQcFikkChnVrvqEnntgNqsy0L2qKGMetfSmkeVs43srLoPAcFVRYx3YyoGsYp4rCMz1vKVoKNcNWQa8WgLzBRg0JRWIlaUFSEACYz7jOmVjzVqkJV+2nK56226Kl2vyscZIytP4+qHEjzqcoryiFVmLqgyGLMyvX4okXBPgAKuyia4fmcqpXKC0WRnFPpWlVmcGnT8keXQDyBf/XvJo0WKbFyTVovzl/VkNOo94LD4HjKuPGNxisOXYElV0xjoZ6yRNpAqejeMRTNunCDFE3RysHl4eKCRC2dRBWGhU8ndXu5hQ+F1haN85xzUKgGWKU0tFtkmK1ryg6Boto9O11ZTF10xRelkUeZQGW1myeDtqo6riCUPjgijuUPArVQGQabKBCrDA92TK1A1LlCaUOUQEvn3Jpt1Om0tW9totCRxRqzsJ5QYBLnyRnNrSvsoqDTnPPE2iEb6YR/ffosydAp2crGol8OL9f9wlL7yaq6yJIbzIWNKJuWec+l1FcNyPvQqouA2NgtvFSZxmRgmoYod2nvlQZtaliaAlFt9xApmnFJVle1KWzETjwk0m7OmPcW16SMU4Kqoi7uYwIwXm9Da3wV5lBFB1DVKcZqrheK5/q71aRurDsv4fo7RNffj4GtR3Qa8drxOYbzjK2Wk+NrZRnkDRqRu4crnUOeaz7g2vQsmS6JMAzKJmvJhI14TKIqV/AldunLhY3oRDNOihbv5AmFcUpvaxVZVDIrE1pxTlXPRY1js5g/6muzGsYXFUxqWKvBqgXUq6yiEeUuhbm+N5krdGF9OnJepzvYyC1GJmPr09CLDsyntU+kfFHW37Ph3yjf2z54WxhMyE9Rg8zncxqNBuvr696MXwCYBPxhVeFVeBdWVhUoI6o8CVYFCki6VOjnJqldWmsajYb3/ArhgJxPKg7Dwo9Qgjz5XAhfVlMtu90uk8mEXq9HVVU+nViA1cOHD9nZ2eHChQvkee5BkOwv0FAUdRKsyTmlcmSapj6QEoDTbDY5OTnh6tWrnJyc8OjRIx9MhlVH792754PnOI65c+cOn/3sZ7HW+vTqR48e8WM/9mP0ej1effVVer0eBwcH7O/vc+nSJV8FVgLCfr/P7u4u8/mck5MTzpw5w927d5fS16StJH1S7hPw7STp3vP5nOFwSJqmHqaNRqP3pDhr7YqZDAYD9vf3mU6nNJtNn2rY7/d9yrbcv/S5sJCApCxKvxVorbXm+vXrrK+v02q16Ha7bG5uepggcPb27dse9j548IDt7W02Nja8V1ye574SqaiYHj58SFEUDIdD7t69y9ramn+e4k0nVcyl8m9RFP45hH0Q8G0t4CBUzUl7SRtLXwpBuCiUwsI9oQovBIOPG+/h++EYkX1X35fxJf8PrzH8fAjgwvlGntfj4GUIKla38L3Qo1DeWz1meL0CwGV/8ayUhRBp39VrXH0OjUbD9wsBR/P5fKkYSNgf5bjyWp7nnJycLFV9Dr1Hxe8zbFO5RzmPtKukzcszDBVhMnZDaDadTn1BDHl+Mh+Eiy+i/g1VrMBSurGMQylkIgrG0KYhSRK/gHF8fOyvJcsy77soz1QWGcLFnvF4vLQwNZvNmE6nSzBQ2kIWRrrd7nuK6cizCBe45Liy73w+92rFsI1lC78/pD0FfoV9R56B9De57tDfV77v5PtT4JlYFADe0kLmOGudmlt8iOX6pa1FpR5eY3jscH4EWF9f5+TkhMlksuT9KPA37JNieSFq7H6/7+8nHNfheAtfC9OmJR0/TAUPzx1+x8xms6X0f6UUzWaTtbW196RN/1m2DwRA9OqbIDitUoUuFDq3dB6UiBqu/aj0RVTSoaFqKFfkQfzFooUSSFlXDADq9Lx0UVkZW/tS1emv2clCYaUrS9GsvY+C9C7xWFoChYbF+UpT+4Vpn46JUq4SbSNdnDfWS15pAHpeoUzEbG2hsDEKqlRjIkV2Ymkclxw/lXKpfczNvIY21gU0Job0RKNzloJ3lxLn7t0KaKwUptIuw9pClagafECWlMz7OdlrLUxsSSYuUrdKUbScamVyBtKPnpD+8hplFXFaNHwDxVPL1TMHvLt/3vk7qoTcxhxVHc42Tvl2bVhfJXUKqrGUPVf1d2N9yMNbG64aNZCe1n/MphFJs6CymkyXNJQz3icxlM3IFRuZO+/INK44lxzzwztvMLcFu5HrL7qAoluDksIVZSlsxTNJm7ldpPoRWSqjiSLDrdN1+hYHZePIPc/SuEI9k5LOvZR45kC2qtz5VW78c3F921INE/rfaNC9W6LnDtYVbResFi3F/sspVbsC7cDE6JJifDauwZU7jsmsU8Mk7jOymdQy3a3/LwUCYotVddqxH2SL/oBVWBMEwaLuEdVh4X5aHJBRhcKmFpUvvjh16ZmUS2Wui/LowqlQpTCHxT1vE7ljhQo/JTDep0/WwFMgZ92nvDAxAFrulmr1WQ1YXEEMlyoqqfXyeZO6dFxF0Kb1a04xtkiJVqVdGuNSdCEEhlJ9GfEKVA52miiAj+Hqb21NYFKYl84+YHipThcVn0MBJaECsV4kUBbiyLgsafCqMQHWUWlJaoBX+AaFeFz/ERs5CpmMHChJxm6eC+HMox33LE2tQLR6AUwwUBA5yFw6eOwgo4OwUbNJGpdEylkAiAWDFKiwpab1wC6AZD1G5usOWJvEEln33EZVRqIMFAqyRRGNNCuYFtmiPxjngVu2FsVArp45oJvMSHXJUd6iKjVlC4YXXGVzga5SdRlrl4B3mpREdRu6glM4xe8qMKr7rsz90Ui7hSZja8CuvHeentegPnaqw3GZ0YwKmlHO3Mbouq9mA+eBaCLcfTXqRaEVEmuV80AcG02qq/emIdfHG1ZNN/cnrkBL7EQB6MKplHVpocSD+7KpXMq4kfZVFFXEaJrxzPo+P7h+jbVowm+fvgBAu64sdnu+SaZLkprMNaOcRFVoZZiYlERVzE2CUS61OFKGWFc0khIFjPOUbjYn0oZpmZBGJXkZY9Ia4IrC0q944awScGPVD3W/NmAXCkT/eTdnxXOLLnTtNOIIutWQnRrimWVae+NGdZEvNQvgf93O053F3Pe97YO5raqLBPqJIk0qMA8GA69ykQBRUlpDYCbHlMAmhHVh2rPsJ55W4j8lKW4Crd5PZbQaJAJLgfBqWmSo9llNbZbgeFUZJoGnVAKVIieSlhymVs3ncw/CRC0BLAWjkjopbRiqQqqqWkprffjw4ZI3I8D+/j5PPPEEV65c4fT01Pv9jUYjXwn7ypUrtNttrl275pUqkup35coV76cmSo9er8ebb76JUoorV654eCap6AInwsrJcs+TycQHh2GAa631gae0s4A8UcOcnJxw8+ZN1tfXabfbvuBAlmXs7u7SbDbZ29t7DyyQSs27u7usra1x584dH4BKuqL0pcPDQ1qtli9+cXx8zNHREcYYr0DsdrvcunXL37Mxxiu1ZBNV171793zhmKIouH79uu/T4VgI/61WrQ3BtbRjCO4ELK/C8hAwhlBR+oaMv3BfOV84BsNjhtAwfC281sep0UJQ8bjPyudW55gw6F8dg+EYDoFMuL3fdUlbrLZx+FlJxxSoc3R05P0n5Z6lQI30NwG0ctwwzTZUWYXtL/c+nU59vxfAK76LAnvCOUnUhavzRQiEwpRTAVAyn4QVy0NVdvh8ZQFI3gufU7gQIlA6rBIu6c7yHESJLvciyun9/X0ANjY2PJCSdGlp0/CnnDMsdgK8Z04N06jDsSnHkOsIn43sI+NG2vBx40FU4qHnnrR9OK7knkLVs7wXfueIvcRkMvFALlwQaTabSyr88LoFoMm/s2fPMhqNvNWHzG9ShVpAoMBJuebHLRjI+JI+Ks9aILj0mdlsRrPZXKpsDfjzhosJ4TnkdXnGoV+ojAH5XpdFSvHLlHuLoohWq+VVveH4k/n2LyVA9KnDZgHEkokhnlZOkdOoH4akPdYxlZUCDbCooKycf6KNxBOqNu6P1aIASh28xvOFGiGe1j5aKSTTBdQU1YG/1tVFoBr2Lbzt5Bqtgw2xgLv6jz2h1CvZJ/l6SuPRnHgcU/TiJYji/MgUR8+lVCkczDvowCtXwGDZsmTH9U6xoSwjIlzgpOq2NbEiSlyaoTLO69HU11iliuNHXV56+i7f2b/C+lvOZ9HGLmCb7irGV0pIDP/B87/Jf/ZbP83pOxuc7jRpaiAx6CJiOM+IJgrTTCgOmvy3N7+Pq70jHkx6JGNo7VeUTe25jG04L78n+oc8KjZde+e6DvZcUY5+Z+r9sya24k6xSXyYODDccM+raiak0Zh/pzMABjwoS+bWqTdNDEWvDsitpWpaEhVxoxjxK6MXeVj0+PbpeVrXU/JuQnaiKPM2JipQRQlVhW03a084TXw4ZmMwh1hz8JF+DcM0yf0j1Mstdw+1f5mqFONziulWgkldNXCTCgi0C8VgqVFGMdstnWKwUitp9dRqGqeAVUY56Cj7C5+pP4dVdQqkKwyg6tRSnxYaORDk9lPYxKAKveh7VmC0jDE8zHEp23jYiFV1VeiFkvBxHmHKuKq3yoJlARKlH7uxERT4gEXFa7UAT0uATS0Uf6peGBC4I68JzLNKvXfWtKCNWSqsQsWyYtED5sVrkvbs1YYCIWV+UsqDzUVlaHfXykBrz1K0qFO+F3PT9KwhHkceri7db/CrjYEZvpq7rlNXXbGTRXvKXKGjiip1YC/vL9SZHvZqgypVrcB2ympfPCKxVHaRTurhs3Idxa512WhNnfKtjPxCxkIl7TwETcwyyEsgGdQQGtBR7XmjK6KJpopde5q4TnE2qiaoeOWrV2ECGsusWniIKA3JCBpHlipZzINegVg5gEz9TOLI3XAnmtWp/O74JnXKybbK6/3cuJF9TWadX6tmsaBUudRYk9VjNYJE16u9KBJVOVVepdGFUyA6/0/naekqhbvzmEgvxkPQF6ZlsrCx8I/EfaCSFOfYVb82CZjENVQyrsEoUMVujq9SPLDGgiqdmq/dyOklMxJVMjYpTZ1zUrToxjPGZcYUyHRBElW+AMrcLA8yLWnDytCPJvTimVuo0YZWUjCYNeg1ZjTjglGRURQRunCWI9Eo6Mu1T/BsR25Wnv/yH06V1UyrBBs55b0UCyobClMvwOTWFe0xmQO3ZVNRtp2q2mqnVpSiNou5ED+Hfg8ifjC3VVgQKprCQEZeDwO+EExI0CVKhVWFoewjEEoCRVF/SXoesBTAyTHDgG/1usPqoPJe+FMUFiH0kIBIAuSwWrSAMQm4kySh3+/T7XY5ODjwn1+FN3JNEoyF1Z8BD12l0qZAVjnO0dGR94188cUXuXXr1tJxRel26dIlnnrqKfb397lz5w5ZlnF8fMyZM2e86gQccDg4OCDLMi5evMiZM2e8cb8xrtiDKPbG4zHnzp2j2+3Sbrc5OjryQbIEpOJJKPcb+orJ/WZZxu3bt7l58yaj0cgH1uJbJaqvo6Mjzp8/z9/9u3/Xn/fy5cu89tprPPPMM0wmE7797W/7vhIC4fl8zp07d+j1evT7fa+Kkgqo/X6f0WjkgdHh4SF7e3v0ej06nQ5ZljEcDplOpxwdHfl0z6qqPGRcVd1JcB6COemboQdlGICHKfmwDM/CY4V9crU4T7hf+Hq4XwgvwvTT1XRC2VYD/tXxv/r66mdXFwBWP7+qSlz97Or55LOrirHHAYJwn7B9VtWCq5uMyxDct1qtJfAEC9WzAOIQ8Ml8Js9CPi/3HN5fCLBg2U9P/DhFNScQRuZA6W9KKa9Ylvdl3g2fqwA6mUfluct9hsrEVqvlryO8d4HY0j9kEUT+JUnivU1XlYnyu1yrLCpIX5R5VKwTZO4LIZOommXulHEvz1oUo5LWG6o+pY2lonI4Vnd2dvwxQpgqbSTqOIFZAibFD1GetbR5uMncHKbzrvZH8cCU5zSbzUiSxKuv5dnGcezPFy5qSZ+XfjOfz73XowBM+S5aXWiRfVfHjkBSSVWW5xk+4/DnaDRaKnQjwO9xsD7sD0VRcPHiRXZ2dnyxmHPnznlfYYCdnR0+9alP0ev1UErx6NEjvvvd72Kt9enoISwWS4nwPH/W7YMBEIN0XioXmBftqK7GailaErgqqiTyAZpJ6yBYucDAKsgGhqLj/K58Gl+gwIsKSzQH7CJNuKr9+OxELXktyWaiRZAeqqEkAE9GpSusEWsiCSCVgnjxMJUxzl9OAKJhASOihfdhMsiJ5oaqGTk/vbxCGcujj3WY7lqSgXLwbzGH18fHBzomxinpprGvWikKHZNAnFTks9h5qBV1VcrCBck6q9iftLG9gipNyE4rJjvae/6pZkm372Be1YD+lRNO7/ZpHhiG44iosKRRRZQrVGk49+Q+P3DmOm+Pt9n7g/NsPKwYn3GAoXHqbiK7m1L0Yr6SP0H7TkTRMv66XYESw3pjykvZfbrRlMNK8bnmLf7peknZTlikphk2Gs5zorKGro45qdPIh0+Arp/73l/to0rLM7/ysyRHMempKzTRemS4+MYpxy/0yLvueejKgjHYlqgs62IdeYEajKDZwOp+3Y8rbBJjIoVVFhIX8VsL8+0SMfu3kV0AwcgudSob13DC4JSEFYvKt9ouQJbFKxFVrhavK5a8Cz1sAQcRq/p9o2oAWb9X+7ARWQcuS+WgTqUWx1R1JVvctflj18eI5rwv5HM/BXzW48MPMLtIszR4TzwfoEeLAkgOAC3DQsCryOQz/tpC+KpYAoyuYrRUiZZHYBdqM/lZv2cijc4rN24ViCuiiXVdZKn+w4/F9QlI9D6OmXUegdoVhaqSAKBqB/vSo4goV8x7eildV5Rqxuj3QEVwCkXfpDForTBZPVdVmjKPyXLXPtmx6wtRsZjXxhfcc0Y5VbeqFFbXSjzxLazHpKkhYOjt2o7rKtBxxbxWLurK9VNbaOfZGtVKrhrIzNfrtoxcu1Bp+vGUwazp+qdRHhoBLkW9kmdVPx7x90tcUaei7kBaWapZRNmCWd13nTehg2OSwu8Wfdy1pnFFpFStTqsXJyJnhWEjS0NVDE3qnovGLYTEoGfa+xCKOtdBKDc+3QKXJo0q1pIJWllflVgr6oJZltz9vUSVurRpUSA6da31Y9ENC+v7hABEZZDi1EyqrC664opGycJZNHMFmwS8xoUbW1KAS9ffwbpQTOYpeRkxrRIOyh79aMJmMmZUZZwUTSJlaeqcCgfsKpwCtZXkzE3iLSciZbwydmgaDMoGkTZ0kpxpmaCUpZ9OGRYNjHXp+FXknnvZdJDd1osiYsmxlMKP3L+isBFr0Zif2f4in/2xtzgqOwtVLvBE9ogtnfJCesx/+df+35xULU4q9wfYO7MdvvB7n0SXULSVf4ahqjyqU5u/54H4wdzCYFQCKwnMJNVIFBdSNTQMpKTa8tbWFmmaLlX4FaWHBNmikpDgR9Ql/X7fB9QhgAkDqccpmgQqStAfesat/lxVNIVBT2h6/+jRI6+CEDiWZRnnzp3zVYTPnTvn7y28xzAgFbWOFKMRtU2j0eD4+Nh7oImCRSmXnnV4eMhoNKKqKtbX131wL6rH0WjEw4cPuXz5Mo8ePfLVOwXM9Xo9n84svmdVVXH58mW2trb4tV/7tSW1z6NHjxgMBly5coVut8vNmzd9yqEElaIgEVWKpEJ+8pOfZH19ndFohFKKXq/ngfP6+jovvPACZ8+e5dVXX+WVV17h+vXrbG9vY63lC1/4At/3fd/HE088we3bt7lz5w7Xr1/n9ddfZzab8dZbb9Fut7l69eqS4lE2qTYtFbjDNFUB29Kuw+GQ1157zT+j0DtMQIKcQ5RcoRonhEAhSJD9V8FeqFYKwVSo+FtN85PfQ2XN42BlqNJbVRUJpHgcZFwd8+H1v59a8HEQc/X11eOFn1sFhquvy+8haF1VXa6OX/lMCPbeDxqGiw5KqaXUXVm8EIVUOE9Za71/oPjlySafWVWwwWJukeckc6XMefLZZrPpVc5yH51Oh36/769T5tpQUSbHEM/A8NkJeBMVYjjnCIC3dlHpWu5BgJz09zRN3wP5BDSJp+tqXwgBpVxf+LmwErR8r4TtIQBR7jucW0M1eJZl5HlOq9VaUoCLB2Wj0fBVtcM+It8hMjcnScJ4PF5S6kZR5L0Gw/6zmiq7OibDLUxTDsenPK9w4U0UlXLt4fwlzyr06RTYO5/PlxT10s4hNJU+IdcRPhtpj9WFhvB7X94Lrym0K7HW0m633zM/SD8Qb9+bN2/S6XS4cOGCV95ba/niF7/Ia6+9RpqmHB8fe09HedZSDErsRMIU6/9/Updl+0AARMk2kgqtVrv0LWWcWX2VOlWMLp16ycTuD3hRfOjKpV9NzliSNxZAQYK4ykLZcum15NTm9pZ5LyKZOq89k+DVFz4lqlrAEJ/WZcNrrb+4JiVVI3Ypu3UlVlUZR45gAUu0rgGGdn53ReXeU3WBE6VcGmSiMbEiHruTlq2EskVd2AKyqFz47tUBaynAtFZcRllFNUjx6dCilEmg1ZgzO3VVLpWt4ap2bbS5MWI4bRA/TGv/PUN2YimbDtLq2HK+f8pvHr5IMrSc7Z8ymaU0D1K2/iQhGRvuvbHL5g1HzvYO+7zeOsN/cOHX+b//sOXWG89i4rrS69RgMgcTN7+lmK9lKAPzviYa2tp3sZ5woornkoznkgm/N+uyGQ2IBhHzdWo1nWJytsHhoM8/ffQSf3x0ibfu7JK906CtLGdf2WPwK2dp39PoyrJ+zRJ9O0LXKckCRop+w913rVoFsFnqikXMS9+3VIWDh410AXmMhcSlHkvap3vu1P6XotYLQGJkndrN1jBPGazRKF1DRIF3RmESiWDtkrpR/At92rpx+9rE5RBbbSE1UGiXrqyAyEEbRIEo+0tqsxVoiK/C7FWBAg0FZtQwRVXuOUS5oapTAUNlsPNnrFWV8oeGAqWVZ2EC60Wxh7ELIlIrkCV12Zc1EeBvg7FbqwIR0C/zq4AWqXiulfcs9MeyK4CzstikHrvJ4o82D7XkXLKLJfCtq1Vldbov1OAnV/RuGu97J/OOg2quArzVtQ9erSbsZnMSBZE2GB2AE6UwkfXqOWP10nWZxIE32ZSB+Zp7DlKExAHp+o8LpdC5oUrj5WJRNnaKPAXOm1ChigpVGNQsJ41KDJaiXMAagX06q5ivp64wSDDHlk1IT0FKcydpSUvnzE1M2a1QrRKIMDG0s5wxAvIX1+XVh1PNpEiZlTHdbE47yVGRJTuyNI6dClkUfqdPan9t4fdPJ83rday672inTs+7CpNaKpSzT8DNpfNeHYRnhniiqbIa3taq76pRg01Jw9Ylm8mYvbwHwGnZJC8jVAWzTV2PoXqM2XoxoYbQ4fcQwMTEzKvIQcEcD/croyipMDXNdwWY8IrQKrPEUwfmxP8PakVi8MBt7QOrtSXVpb/v47LFsGiQRSXNqOCkaLGRjpmbmEyX7rw4FeCoapBpZz/h05qrjNJEtJKCUeGUVWlU8WjSJYkqUl2htSGa1+ny1ingnWLefedPd7S3NDAxfuxZbdEYIizX5mf5b25/hnnlfG2n0xRTal649IAffOoL3C2b/IfXf5z94y7VOEFNNXquXTGwGDpHhqLlbAYWizCu/bwC8XsQ8QO3hYG6BBySNhVCMTF6F6WGmKyLakZg1Ww282mnEsiGXlMSNOd57vcRk3RR/oRgLkyHCqEiLAK2qqr8MUK12GqgtxoIwjK0EUWHHGs6nWKtZWdnh9ls5tOnJMgOlS0S8Itib/VaBYCERWEkMCqKgmaz6dtPvJ9OT099ilq/3/fqpOPjYyaTCXt7ezzzzDOsr68zmUx4/fXXieOYGzdu0O/3fWEIKXDw/PPP86UvfYlGo7GUvvjmm2/6YHU4HHL+/HlfEEGUP6IOkaqY0nZSHCGOYwaDAe+88w7/5J/8k6V09pOTE9bW1nj55Zd9leFvfOMb/PEf/7Ev4mCMYWNjg2azyWw2o9/vL1WJDn3PQugUAhrpMxKUh0A8DMblPQE0q0q6UM0WpnqunjvsX6seaLKtqrUeF3SH5wwVnrJ/uO8qtFxVuIUAKYR3q4BOfl+FlY+Dg6vjZhUwPu7/j4Oi8loIPlaVVuH1rG6rbRFe0/tt4dwmCjkB6JI2GUKcbrfrC0fJfnJ9oYpRFjyk7UVZLecLgSSwBF/AVZyVfUPFoMy1AjvFw04p5dNGZZMqvMY4D9csy5baUuCeXIfMI5IC/bj2lXsS2wNRYDcaDQaDgZ8bw2cp0EnUj71ebwm0yry46ssYwvnQX1Hmk9ls5lXLoq4MIe14PF5S4EnKczg+5RyhklraLryHoii8t6BAqlXIGbZr6KV49uxZ4jhmNBpx9epV79M4nU7Z3t726stms8ndu3eZzWZegScFaEJQJ5tcV7gJZJX7CtPNVxcnVuco2UJ/QYGt0k+kjUKQLWrFsP2kDaTPV1W1VFEZ4MKFC7zyyivs7u4ugeHT01O+/vWvs7Gxwcc+9jG2t7d9v/zOd77DrVu3aDQa7O3tLaVIS59ZhaF/1u0DARAluAqN0l3BExcE61KRTAzJqMTEmqIbYZLIp9pJIC/7SPVLrANnUV2FWIJHD4ZEDJgEqwdyLEkdVAuIuLr5FED5Inrcc7NyEAcQrNYOTtgVL8QaOthIu7YIFFe6MnUw6T56PG/V14oPck2iwLj7Xkq5Ng6wmsQVjQEYTRpQuXRCgRMmccbxJk/oNmckd/uAZbqTEBWWZAplW0FaspFN2M0GvFXBuwebRN/uMN02lA3nr7Xz7D6zt3cBaLdnPNd9yM/tf5avvHOVC7khnkTkXUXjWJMMC+yHhwzSLubZEcm3O0QzfCGZaFpgY8351gmR0twuR5yLNDfLFGwdYBoYXI7AQvSrm/zm9DPo0nKmhOb+nKqpuXVvk8YmbH+zcqCwrZnXVYtF3VI1IZ7ERDPL0UcqkuOIze/Wz1ZSWa1FTeeQxEjVXqfgNBBHC0VcjFMg5tqnEFsWvoS2LhBBKR3fOrWfxqURJ2YBD2Lr030Xsit88OphpGLhe1gDPg+1yhoq1kpDp0ZUDkbK9QCq1P79EBBa7YJmNdd1cQscbKRWW1JXRq2BoQeNvm+zFHBbrdCFec+YUTVkFHDoi6uAe63u716JSADu/EF47+urYzN4b6lCOjhIWI9Hn8pcL2woSZsNj2NA4a7NRLIgsFBHihJR2vnBfh+tYHBJe+jjLdsMrkiOUrT3SiY7sQfuia6YWyjKiGQOycSSTIzzg1MwMe5LZVg1iOaucrzOnU2B8y9w/bxoqbpitvVzgDJ136mV0CZWTrVWt12V2iVvRVXVRUeGM9Roijk6pjR1Koh1izveOsEozDhxCz4CWutpUaVy38pBo8hQoVyqr3LgU9dwrJfmToEYgDTxeHVKWMusjDk46DLpplzZOMIaxXy99kn0qkmB5tSK1xrGW7c4Y4DTsuWqlBeLVG6du8q9Q9Pw507G7p7iifZWCgKCofbSy92CkomcKvKdyTZZtCik0kwLcg2NQ0PRUVDDvqqhFmphWw8gaxcqYODvXvpjfr/ztKtonBREyvC/Ovd1jqo5f737bbJ/q2BuEhJVcTY55p/f+z7u/MFF8v7ie01V9QLKXLnx7x4iyijyPMZaOJy3mTQyHuU9TooWkzIli0qmdbr4SdGitJoxllhXrCcTGrX6UCsDGgoTU9iISBnmJqaoIiKZO7RhWiRUVlFpTZHHmLS22Ag8EE3isgRMalFjtZhPlPXzJcDMJvz+yXMc/PY5uncMHQtdA/HccO3TVzh9ouLQtHn4zhbdtyN6tyvyjqZow+BJ11emm9p/N/g+Zms/5aZanl+/t33gtlV4EPr3CaCTAFDSfEXJIwF1CGVW95cgKYRtonALgxMBNPJ7CI5WrzFUPTwOLoRKjMcpmUJgs/pZCcZCYBRCt8lk8p4ALYQIoY+dpAmHaWiSri3nkqIjAuzEs0+ARbPZZHt7m3v37nmYK4qU4+Nj9vf3abfbXLp0yadiizqv13MLOKE6tCxLWq0Wu7u7PPfcc5yentLr9djf3/f+U6LCFEXIRz/6URqNBq1Wi1deecWrF621jEYj3nnnHV/x+fj42KsYX3zxRSaTCc1mkxs3bixViBbVj/htgUtr++hHP4pSiu985zvvSclrNBo+3U8giygHpS+EqZby/B4HkN8PPoXgKYRhj0tTXIXwIYCE98K7x50zDJRDwLzaD8MU2vAeJKVQgFM4DsLrlM+/32dW22T1Wh/3mT9tv9V2WlXtrbb56rnCMfp+CwKrqrDVZyD/T9OUyWTilYUCDDudzpJKTynF9vY2+/v7HgyF6jKAK1eusLGx4dW5Fy9eZDabeYj13HPP+erdw+GQa9euec/Yoig8NJdn0Ww2fTGXUNEqsDME47CYn8P5VuZorTWf+cxneOmll3xa72AwYDQacXp66s8tY0TmG1hWaEtasRTrgIWSVtpUAH6apsznc38vjUaD09NTf7zNzc2lZxqCMZkj5dihgrDb7fq0X1m0kvdEIS7XLYsvIUgLIaEsjklV5Ol06n1LBZKJbQO46sOrSmE53mw24/DwkHv37vHiiy96X8vZbMaXvvQljo6O+Na3vkWn06HX67Gzs+OVp1JgaTwe+36otfZQWZ5/WDxEnjWwBGLDcRz6c66OgfDaQ8VvOC+G86UsyFRVxXg89gBWqUXq/2o2QjjXNhoNtre3eeONN/iN3/gNTk9PKcuS3d1dfuqnfsp/Tx0cHPCHf/iHTKdTTk5OfD+SVOUwvT6Euo+bD/5N2wcCILoA3Jm9O/Ny66ovsvAHK1oaVcXE05J4BKp0qsMqdcFw49gQT9wf+1FhfWVVG6k6PcsuwQqBlgIrfbqspEZpXCAnQZYo/gKY6FRGgNYLQFiDSoxysYUBpXEBWWmccgGc+jBIcRZPL4VTR1ZxDRqRwhB4U//TvP5DSy/uQ1RhNqp9nvIIVag61dBSZS6ILnouzQ+7qLYq/pCqBKUsnTTnZAN6t1zl0uGliNZD41Qy+00erPfoJTMGT8J2Z8LeVof2VwoOfjRl89uK6TwlKi02ifj4mVv8xzvf5K1ixr/+5guYqFaTngqwMlzaOOat7SaXNwYc5h1QDnrO1jVlo0PRVlx//QWeuv4c5iglHmrW3oTdiWHe02QD44vlOCUVXgE33UmYbmg++ew13tzcYXJ3nbynKFuu3cq2ReeW3g04umJJTzU6V/yHP/AF/qNX/yY6X4BBpRWUhmpnneFTHUSlVqV4Zdv8TNv1qaDQiUsLFQCAT2GWYN2mdZ+ro2GbGQ+Wfbpz5CNlQiIn4G/1/6p8PFgDnNqrjr2XCq3gzmMtzjux1C7tPrEeGtnaP9GZa1KnWuMDa2WhEiBj8IDVeyYG6kIpRCI+l+4/At3AmlpjWKuJwz7vb8kulIaSJiyqw8cWX7F4GCifD9WD3jMxrLhce5mGKcmqHquiMAy9+EJgKOcUtaNql0RJRTRS9G47BWLoV2hSmF40qDpNuegoD6NSXZEp+Mcv/C6/d/ZZ8irCWE0vnZLpin+w/SWOqxk/u/Flrv7v9jmtWjyRPWJQNfiP/+THqQ4z4jE0TgzzvvKp4l6d3CqhUuhSYWNNVVfpjWr7htxGJGOX7i8pnLOLfaJphyRL0GrIyMz5Hz/8c7z+whkqFKmq+NLgGb7w9Y8TT9xYqBpqodKrao/GxNBoOB+9ls6prEI1KqLIONuJAu4ermHGiZuDK+sLGulK2hf2H/ZRkWG3N6QRFajI0KgViFW6eCRHHxIoXPePenFmLZ0CzsNP1Q9Fjl22XQrz9fkZTOJAnvPNs64IzLwufKIXj79sLfoFwKRMOZ032WyMWUuntKKcvIzAwHx94T+qjFMVqnJR1ARlfV8QT8FzyTHfv3GdNxtnKKzmWwfnmZmEro65N1/jl+6+4hR2D5vEI0W+URG1LOkpFG0Wqf/a9T2vkK6v2RiFjgxaWSYmRSvL3ES++nNhImZVzHo6ZVy696kg0yXriVPsSHEV2WYmIdEVnXTOKM9cdXHrrDms0bSSAh0ZVO6Kv0g1cYHdunBQViC/rkAV2s8BkbKMTYZWlnzNUhy57IN4VquOayV3QxXQKSm6kbdAMYnCZAZVQud+RdHSC1uTup1Gl5SzcPje9oHcQpWKBOtZlnF6eupVCKupSWI2L55Vxhju3bvnveckQJTgNlQchYEvvDcQlmsK1T+r6YshFAhTUgXohcAmVGvIa6twRl4Pjx8GaKtQVGvt71OuV+5NUiOlEIkEPGma+rRISfEFfPXd8Xi8lPIYxzHtdtuDA0mrNsYV/zg6OmJ/f59Wq0W73abb7fLss89y584drly5grWWfr9Pv9/n9PSU8+fPs7W1xWg08tWsRS2ktebk5MSrn0ajEaPRyFeovXv3Lt/85jf56Z/+aZ/61+12+fa3v+2DPgmI79+/75UrWmvW1tY4e/YsN2/eBKDVanFycsJwOPS+X51Oh3a77avTPvnkk6Rpysc+9jHefffdJb/NOI65fPkyFy9e9O0s4BYWqX5hxd3VYFv6iRxzFUCFICmEEWGwHI6dUEkY7h/2Dfn5OHVU2P9knK0qiWQLvffC30PPUhk34b2v3mcIJR8HGOXaV+93dZy832fDfcJttW3D474fWP3T3g/v5U87LywKOEm7zmYzrzqTcb69ve2LCxljlhS4oUfmpz71Kba2tmg2mx56/M7v/A7Hx8d86Utf4kd+5Ee4cuUK4/GYwWBAHMdcu3YNgGeeeWbpmmWOOjo68iq0sKqz+BFaa+n1ejz77LNorXnuuef4/Oc/T7fb9WPs7bffptls8pnPfIZms8lkMuH69eu8/fbbHupMp+7vvqqqODg48OnDSikP60SRd+bMGcqy9BXaJ5OJV6evFtIYjUZMJhPvSyrfD1prX1jFWku32/UFl+Q1AVIy302nU5599lk+9rGPea/V9fV1Hj58SJqmXLp0iZdeegmlFF/+8pe5d++eT2EOjyn/ZB4ejUa+cIjMUSFYlOe+OtZhGXCLpUev1+PSpUusra0tpVynacrFixd59tln2djYoN1uM5/PfdX3Vqvlv4fkuOG8IVtYVEbSfBuNhu+Pcq8hcFxNxw8/I5tAyFVfSPm+FoiolKLf7y/NgdJfpY1DL0qxzJA2Oj095Y033uDhw4f+e0aAs7WW6XTK8fExjx498gpOUfSGbSALjaLOFfD654GIHwiAKJU6owIfbJUt5y3lzdOtIu9FLsWvH1FlCz82UaEkE0s8M1SpwjQXgXdcOqgW1ynPktrs1SKqTp0W5Z5yqoe4WATXWOsLvIQpeAAqLyFbeBuq0rjUWPFYy0tUUWKaKZQGPZ9jm4to1qTRgglFirIdOdASO4WdLgx53xKPFVXDEmvjOVJo5K7nCowlXwOdVpAnPu2r6CjisSXvu0BNT7SDtWXtu1fW0G6WMG/GFF3LdDOi/aBAFxHx1KmVACqj+c3rzxGXUFQRNrGcXk2puhUQMdjrcvbENdDvfPc5frposNMYEXULrM5IJoai7QKzqpXwzo0zxMOI4187x/qNkvGOq/aad50PVTyznPm1BF3DTqstnTtz9Lzk4d/p0L4X0d4zzNYdUDZ1MQBwz3G+YTnfOOFrd56mue1gT+OwhqptiOaKeGbIDuqVzAzWognVNEIXJZQVZDGWCD2akl/d4PjZyMOPdOCgmo01ZdtVILZpsPpX1Sm6kXVecgafeizpoM6gM/ipBB7WNxKoBV1/rDtt7WVoUwsFHuqFYFGVdRBcF6QQ1Zz3BjRKWLVLVV7yZZTzBftEOEWjXH/9vklWUmLlELXnoVPxKAL+6W8FAWxl8CUlqcsmgP/19TuI9N7X/D4sj1H5nCvExHu3VciIa0urajWGfbzaMUyVXvJkVEFV5yootjKLKIFy03D4IQefgEXaamQhdrC+yuq+XL/fS6fcrzJ+du0eP7t27zE3ofnaPOETWcLP9PeC10f8R0DnRsTpywWnIXQGlEBno+B0IW+TZ2ljhU0Mz6QP+fBPvM4LnQecS4/JbUxD5TR0gbGaTzbuUKF4Pm3zfDrwxzmp7vKF6GMc/Mhsue0VmGFCfhoRNSuKwk0w//L+RzmeNUmaBWURef/axh91SBMHuRwI0lSZ9s9MzxVZb8bZtQFXOkcUVmNLTd5XHsR6WCuFTozzwLWxm1N7yQwN/IONL/Pv/thXqX7MzZfg1INPJh363bf46N/8f9RFnWL+i7uf59rvPUmVyrhVqNJ65WhZysIBNKKC860TchMTB2BNWefBmtfXKZYSNrZBP68V9DUA26t6/J9e/dt0fr9F89AV2opnln/+732Sn335Hl8ZPcXgd86wdcsQzwy6tNz/dIzJLPM1XS8K4Pu+zt285CC8O0+n7YqdtGP3nIdVg0QZD1qNVXSTOeMyZT2dcDhvE2s3hqXoTmEiEl2hscxNTIXmNG8wyjOUskwK+aNZMc0Tl9KtoGhZ0mHtcRmBFvW2dgXDkqFivpYw3tEkgxp4xgEcrD9ra+ifd1zlaVUv2FZWY2slfpXWFifK+VlWGZw8GS+8JavF3wfRDGrLxMfPJd/b/kJvEpiHCrsQPkgxEQmYpJqkGM5LACTBggQfYdox4EGknEuCEQkkJAAKU1NlC32cVtVeYfAVBmSh6iyEgHJvYXGS8PMSqEhwmWUZrVbLKy8EjoUpjtJmkqIdxzEnJyfebF6CcgFmkh4s1yNpghLISvGT3d1dDyrb7bYPrCaTiS+OIuqMdruNUorRaMTx8THj8RhjDLdu3eLv//2/T7fb9VWdRYHU7XY5OTmh2WyyubnJwcEBBwcHHhIMh0OstTx8+JB79+75AHA8HrO9vc3u7i5PP/00xhj29/c9jKyqyhcIKIrCp2IfHh5y//59X3Vb0kg7nQ4nJydorX3RgcFgQLfb9f1SFEQCS958802Oj4+9VyTgTf9Dj8fHwaXHKfFW/RHD91Z/Xx074XuritnQsyuEnKHHnHz2ccdehZEhWF8Fl2FaZXic1WOHMGFVPfinwbjw9fBews+9H+QLgeEqsA2vaRV0rgLPx+33uN9lk/aTn6IeTtOU559/3qfAymfH47FXXIlVg8CY2WzGgwcP+OVf/mV+/dd/3c8Z0s9l8eBLX/oSv/Vbv+Wf0/d///fzgz/4g1y/ft0r5KTYR2jVcHp6yjPPPOPbIoRzWms6nQ6tVouPf/zjvpiGwMXbt2/zm7/5m9y9e5fXX3+dX/iFX8BaVzTlZ37mZzg5OfFFKWazmZ+vBPbt7+8zm804Pj5md3fXK4jPnz/P3bt3uXHjhvdZhEWKvTxPmTN3dnaoqoq9vb2lKsySFi79RZSSYYqzgNqyLHnw4AFPPfUUa2trdDod8jzn9ddf5/79+z5VWs4rxZskNVjaL4Rv4XXLOaQasPSbk5MTn2qepqmfU2VeCL9PrLV+Dvza177mF102Njb48R//cX71V3+VZ599ljfffJNr166xv79Ps9nk0qVLDAYDD0nDfi5KcVn4EpW69KuwX6wqCB83x4ULZNK/5XWxhwi/LwXQieVFs9nEWsvx8bH/vpTrlGcmfU/sAGRBDBZVwuW8Yv8RLtqJalM+I9Bbvr8kjVv2kYrgq33wz7J9IACiF2cpfEoV1Ab09XtSPdNGimRqXDARfgHohdefSVzAlkytf71xYjyQXFIk1fuuwrglT7VAQeKhC9RQoP491jVkdKnJhtojUSnIawl0pJyqxWgHGqIamNQeaZK6GU0N8/WYqtQ0clOnFdZejzGcTJtL16kMvlqrLiE9cSzJ+jLHLn05O3ZBYmUUunAeWFFdidpBWzBG8+CwT9WtiHKXLp73IPtOib4UoXsFL6zvcevdHbKZop3mHM0cHNJTV0xi7dtOKTq42qL/Tc07rz7D/SPDxeOS06ua7NSpQca7EWVDc/lfVky3IZ4ZWvemjHc6qBL6N8VzkDq9THvV5OCqGzDO/xImOy79rGxZTArZkSKaQ75lqboVv/rOi6Q7E8rjDtmJ8oUOujchGbl+koxwFUsz6/zexjHYmXuuswLiCLSiSl1xi3ga9gkXdPtgv+4/aFtXc601QwoHpgjgW2RRscWWAdxS4MtUC/CRwijiZ2fq48s1yPFEnOBPWG+lQEp3bKkk65RNdgEPpdtoi8714jX5Z1h4NwLELNJCXTPUcKLu/0ot/k8NpORSK+v7/2KMsKh0XC5UVz61mcV49EDQLt2pnzO8YlGF14RXEfrNj3cZ3O4mlF35DIvPLEHC4KcyDv4jada14lKVBpXX/SAzlE/lWKMW49QoVGyws8ifK56464hmFmM1bVUCy74a4faJ7L3vnZopWhuqBqisIk5LqqIG3UZhK4UaxURTTdU0vmpzMjVU4wWguxBP+T+f+1c8lWToFZI6tyWRyqis5biakChNoiIqa7k13yI6iakiS9IssMFEq/s50WaFtYqq1JRFxtvDxqJNThNM7JRh4vWnLIuUb1tDXA1Vx3ChP2S3OWRuYk7yJiqyxGMH+asUn1483RW1u/FgSJewkTgflt+bPM1/+vUfJXrgfFmTgWK2a/iDf/s/40uz8/wff+enSQ8jdK7I1wy64VRyZeKAl3QXqXQsSu9UVzyY9hkXKS+t3+cw75CXMRiYbtT9pE6t1rUCEVw/l5RqEysiZdmIRmSNgqKniKfazd+xohvXxuK6YrZhSU8V1SRyC2h1oZfGsXHgtP5uMbFbOLGpATRWK79glCWLFAlJS85NRDNyv1dWYVBMq8Td36RHaTRZu/RFXjDueqTISm5i7h2s1XOXwlQKm2vn02oh6hdEM+UUiGW4GOGuNxkrVxwtc2+sXYfRBXcsB3YjX6zIQd16/xw/tgoW46xoKdKhmyhMwxBPI/o3S4qm9t+x4BakZltqYSHx+Njwe9tf8G1VCbSaNhS+Dvj3VxVVcgxRzsi+EmyEqbyiVFg9fqickKBiNX0wVH6tKiTDY8lroXJiVYkRAh4Beu1221dKFmVOGOgbY5hOp/7awzToMP03DGL7/T7D4dBDRQGMkrbYbrfJsoyHDx/SbDZ9RWZR00mK43A4ZHd3l+3tbfI899cnYEBrzZtvvukD5O9+97tsb2+zs7PjIYKk/wIcHh7yW7/1Wx4axnHMxYsXUUr5IM4Yw9HREeAK4UynU99Gx8fHlGXJYDDwqe7yfObzOUdHR+zt7fHmm296RdP9+/cZDAZ+H0k9lII6cm5RqAg8lC3LMq92epzSL1SzhlWuw371fkrAsA+HcFp+yj/p46upkqsgLoReqzBrFULLvqLkkeuW63ucYi+shrq6PQ6ArqZzh+AOWBp/q2PucRDwccrMcL/3O9bjPrs6Nlc/s3r+8Fjh6+H9hnOYjBEB+K+++upSOqjAE4FyMp43NjbY2tqi3W77tOAwlVLaLoQbcj1RFHH27FmOj4895HrzzTd96qxSyivy5JwCJa21rK+vew89KTjU6XS86juci6Mo4vu///u9kk+uKY5jxuOxV3rJtWVZxvr6Op1OB6VcxWL5bFVVnD9/3tsayHMej8d+jgvnbgFueZ7z6NEjb2UQ+k2KKljglaROS2q3qDnb7Tbnz5/nzJkzPrV4Y2OD3d1dvvrVr3L58mV/rVEUebW3zDvhAoCo5aQ4iqRlW2v94ozsI8BQ2lO+3+RZi0pc2lcU46+99pqvOv/iiy/yt//2315qr+FwyNHRkS8gJTDYWutVnKuLDaFVgQBE6d/yuRD8iYJT1NwChcUuYnWxodVqLQFs6bPy+2Qy8QC42+16gBoWNZF2evDgAefOnePk5GRJBRkqx+X30PNTziV/K5w5c4Zer8dsNqPVajEajTzUXO3Pj5tz/k3bBwIg2uAPdJfCpXwKsomkgqWDDs7U3lBm2gMAG6kaDrr02Ch3/oFOrSaQYaECcuep1TXKva/DlEW7AAzivyQQYimdqYYONokc+KghoKrMEoiwscbGkSs4UBrng6gX7+lJTnW2SdWM0bMKnVfoKiKaGVRlKJuJB5xWwzRPaBmn+HJej+5UvsABrhhBYV1QGE8N6anbv2paF6w3LPMt10bzDUX/3Yqhiojjivlhk2ji0uniqSEZRkzOJEx3LfY45de/8grZUUT/HcPmXx9zK3NqPtOuMElM68BQZZoqU6QDpx5zKh9F68BgtaL5YEY0S4nmBpNodOEC/KKbMroI+fmcZJww26y/FOt2T8YQT+oCBmuKeLyAiMriUrQriCdObdJ8pCguVMxPGly8fMDRqEt6akmHlumWqp+zIapN+oumS0mOlCEeO/WoA4MGawxo7VIwUzC5O7cy1JV5BQThfAGNwK26eIr8/SAEOzbOA1EgnRWgt3im/mdd5ETNtUsvTisHmgjeBwcXxQvMg267OJZASAtSWdm/Z1kUesH9rkQtmRms72f1M0nq4i31PUeLYl31fahFSmZlnT9irBewMFALOvWjK9qyBPKkuSxLSkQBddSLCn4M1IpAXf9UYXuq4Jw1RJR9QiWRstY9T1gUQVld5Q3OKZ/z+wK6VlLKPONAlXt2bmAZTKGxhUalhjipKE4ykoOU/FxBNINkWNHNrZ9bvvErL/B3PnqBOK5Ioop5kRBps+gqVlGWEWUROSAjKbjTCIwiSsDmmpIY6/uBIt5PiOYwv1AQP0rAGkyiiCeG9tylcyejiL+W/h9cMZ5cL+5dW+f1ORPQolzVYSkMpCymadFAfJBQdHwOLqpURBONqdzcJd1U14s8VkH7vkslRbn+MduImPe1t6QwsZtbornFZoZ+OmNQNOgkc1JdYueaouNAU+gz6ca7ArFwqFxfb+mcCMX16S7x/YzNb1mXQj2z7EcxBXA33yQ5iWjdV3TuVxy8ErnU7BREaSj9LZ4uIKKN4CRvEmvDejYhwi0OZUnBFGgcW/KO8rBblNQmVgwupcQjdw+2Y6isYmgaxNowa9XpvbUSP1aGyhpmtUQ/TP/XhXthuilQfzG+dA5q6r7LbG2bkSUlldEcz1vMGglR/UVdmojjvEkaVRzMOrTinFHR5v6gx3DU5KjVohGVrCXT+pkaOsyJMMytS3XutGckcUU7zTmZNDFWkcYV8zKiqjSzRkbzYPGcZaGvyhRF1xCPNY2DgrybBvdYjz8Msa6wibMAiaytAbRFVa4PJlSo2BFJZWH/o5qia8Aoig4ML8S1qnoBdU0sbYj31Pze9sHcwjQm+T8s/JhCeCJQKUy/DNOhQighAWKoIgghnwRsAhbDIEoASVhZ93EAQYIOue7wM3KdEsiG9yveXquKrDDgFRAoBWNEaSS+aKEqJKySKgCt3+/7Y0hF56qqODw85KmnnqLT6bC3t8fZs2fZ3Nz0QeadO3c8yJPgV5RLb731lg+qrl696pWhkt5269YtX3xgPp9z584d4jjm7NmzvliLqAsHg4FXHzUaDS5evMjFixcZDoeAC7gnk4lXWUm7P3r0iOFw6CswK6U8VBDVlihsut0uR0dHHgzcuHEDcEGmnEcUN/IcxD9NoOhqYZwQXgnglWctbSzPP+xTYR8VuCxb6B0WKgQFLCmlvL/kyckJx8fHvl+HkHA1LT6EbyHYkuB9tU8LdJZUPfGGDMfWKkCT1MsQhIZjM/zs6vWEgC3cZ/Vz4WdWrzu8psepoVbbI/x8+LnHqQxln7BdV8+9epzVZwALYPbCCy9w9+5dD6hkbIafk/8fHBx4r7y3337bA7oQ3K4C2dUFiTzPeeeddwC3UPD000+jtfbFjARmpWlKs9lkb2/Pe+aJ194f/dEf8dJLL5Gm6VJ/DFXe0hbhnD2dTnnnnXfQWnP27FkuX77si2gMBgMmkwn7+/t+jgt9RPv9Pq1Wy1dmt9Z6eBn21bC41HA49CnC8k/U6SF8D+dm8XIVFWOr1WJra4ubN2/yi7/4i5ycnHDx4kX+8T/+x3Q6HdbX13nnnXd49913OTw85MyZM1RV5ee2UK0nz0fGzmw287AyhHECNcU2IgR5JycnPHr0yC+8yPiUuVGAWlEU3kNRPhPO/VJoJrw+WQBaVSqHRZSkjcICNjIvhAt+7XbbP4N2u83a2hq9Xs9nEaRp6q9VFoGkmrIs7jUaDf9dKYBX5lJ51jIfNZtNDg4OSNPUW5vIODXGeD/WcHFCUpBh4Uspx86yjLfffpuiKNjc3PSLR6tzu8x10hZ/1u0DARBDpZD7Z+vCFDXA04sgSBRgUWEpmqr2vwKvFhSoV/uwWQ1FU5GORX2iWLJkUoCuVSMCPMAXE5AgSxcu1dcBBfz15X3F3vf16N6rSB+6gNCnYlZ2WdFkgMpArFFFRbHRQucVUV7WqVUWXRrKLEEXlmhuKFsJg6tSTRlMYsnziGZ9Dd4DMV4EqiaFsogoexVRDZnm69B5YDhNNDoyGOvuy6SKou1+rxqWNDIk6zPSmx1mmw6QFT3Ye6Fi9+IxL20+4HLzkF+4/jEmh31GRUbzfoQylubtBKss8cSgqtoTrOmqWJYtxemVhJNXCs78bsT4YtMpRhPFdCNCl86TK9pKyLcr0nsJeU85lV+tGhpdct5dedcF+/EEcldLhSiHeGwBpw5Mxi6gj+aWztqE0aBJElU+OE8mhnzm+kg8s0y2NXnftV88U+Q2Qhe1Mk47xagqK2ya1J5oTmVmjWs7vNoOX9Hb5cVLh1Tu+WcuQHV9oSYl4IutqErS1h2gUlXdF6sazpTQuJYw21JMr+R1ZWW7AINBNWafsvy4dORgC30NrcV7hCFpe/L3ZA193qO+qQFElNd/wPjbtiiUiBxrf1A8dHSwzymE5fo8iH9PrrBcqywuLN4PC5b41wOg6BYClv3lVuHfewCheMOFx63B5hKErFWK4TFMDUmVdb6nNnGKLm0ta2eG/Jcv/Qv+8/s/zBu//Cy1oMuB/sT18+aNlLIJB69kvm9jIB2A/f2e62PGcTvXJvj5TyfQqAtPiGeqKqF7r2KybVFlStG3tO8oslPXNqLK1N+JHTyMFaNzEWVb+p37ufYdvZSO7efZup3f036qXgCykB27Ssa6ihhdAJM5BWDZBNO0dTEktSjuUwMtqxXz/kIFVjYXBWpM7RMrhaKonBKuk8zRWM40hxBbGodOubdQ1sJsB9IRjM83yLuK5NQVr2pFc7RSJKrCxJbZpiaaWuKGpWxZxkaT6cL195nzq7XaValPR+47yY+VCsbn6ratnPqvFef0kxlb2YhEVWwlQ77EE9gIJrt6Semu5zgYW8/x/XcNp0+4toiUpbKaODJLY9FqUPWcUhrtlXdWs6hWrKH9cKFAFEBWthS2UeFKfIPJDHkZUVQRR7MW3+EcnWTOpEzQynB3uEZlHKjebE84GLWZ5QnN1pw0rvjWw3NUlebSxjGzMnEVnZUliSqyqOTy2jGl1UzLhCSuiLShn82YFCmPTjvouWJ8duERKot4qsLB1LqPZ6eWyZZemlsmJltUI7eu7UUxbWJLAqSqqhd5nMI3O1S072gGT1mSMTT3jQOIdeV4+bvk5Gm9WJT43vaB3R4X/Msf/6HyUAJMgS9ihi/Bf5hqJIBk1aNQgh8JTIqi8PAohHISeEhxAAlu5PxJktBut2k2m5w/f5779+8vKbVWA3xJpxJImCSJvw7ZRNUiShgplHDv3j02NjaWoECYSibVlaXIyOnpKQAXL17k9u3bnD17luFwyPb2NtevX6fT6XDlyhVfYOHChQteASjg9dGjR/T7fV5++WU2Nja4f/8+Tz31FOfOnSPLMu7evctgMPDBmvgrynMS0CmBYxzHvpjBrVu3lsCrKAuNMdy5c8d7JQ4Gzp5D1JCwKIwTpuuura35vjKdTn2QlyQJ6+vrPr1bgnVRmRRFQbvd5rnnnuPg4IC33nqLNE198Biqq0RlE0IjaS+5NoHEYT+R9pSgOAzeJVAPA/ZQxRamvoqa6cqVK6Rpyle/+lXv7fY4hVw4JuT1UHUbfg4WUFTeX4WjIdQIx2WohloFiHKO94OH8loIE1f3C+cGuZ8/LdU53DdUN61CyveDhavtEy5uPE5xGO4XtmOYnimA+/nnn+fWrVs+lVT8QKfTKcPhcKmvnDt3jtPTU9bX19Fa87u/+7teESgAX56PPC+BKLKYIBYFMpdI+vD29rZPYxbPvLNnzzIajYjjmCtXrgB4iHN4eMhv//Zvk2WZV9uFCrvQN1GOKde1vr7OuXPnfNViAU2ycBD62YVwMo5jhsOhLxglr8t9yT4y5qTtJK1Ya+0XFaRwiTwnqVJ87949Dg8PybLMq46lTY+OjvxCRRzHHB8f+3sX+wwBvWLNMBwOff9M09TP3+Px2KdIi09h2L6j0YjBYODnG/l+kLRteU9el/l/1YsvVMHLHBTOSQLNptOpn7vDFPXV7z6ZM4uioNfrMZ/P/b2EdhsyL4oVxGg0otfr0W63SZLEq0pl3Mn9SDuFUFdgswBPaecQ2rVaztdmNpuxtrb2ngUB6ZOPS6GW7AO5x0aj4aHz9va29+Ntt9vvWRCUOVye+186gAgsqhzCwqdKu+DT1ApBq5WHDbqwqEwtakoE+8kjM5ELLHWFD5Kl4rKkRXojfdlXFCQChoDmgSU7mlN2BB65a40nFe0HiryrfdoaWvuiuko6V6yxvSZ6VkKksWmMnhYLFZVASWMp2wnT7YR5T1NlCSaByVlLuVbRfFibINcBHsbdo1OpuPTdxknF5EwMyhINI/I1Q9nU6AqimaG6kNNKS+KLQ/7XT7/Kz2c/QOsBDC5FNB/BqNUh3pox37Rsfqli71MRL332LT7cv8s7k22+9uASv//6y8RjxeSM5d2HW7QHDkSaxPktzvuasql80K5L60zqNcTHMVZb5m3F+Lym+dD5MzYPrA/SomGEjWC6a+lfd4UkRPmRTBwodYG7Ipo6lU88cSmEcwQoOuVWlTpVlp24qp+qcr5kJna+aPHMBYWnn55hT1Ja9yKqelRFMzCNGDWvIRGuT5bZwl9T+qtT2LmiM95LUwoSzCPnOZm7TmkT59emKgeHXIpm3Wczdx4X7FtMakFDNNaU/ZL0MKLzoEIXEdNLATQUWBmaCwbqQH+tUmilhn42srX0S0HJAmganP9aXawAC6oQKC/kKrhPHQw8KzB1GRRC/dyEm5qgoIoA8Tpl2asO1fLigaQwez9D2YLPY13QL22wgFwsgRZdWQ8HPAC0YCK95Ge4aLvgvsNTq/deh09zjvVS4ZgsKdmIZvyt7W8S/6RhUDQojebW4QZrnQkf2brHvIp5d7jJrQebqKMU0zAOEtcqVhUZVGRdhWLlfloLVRkRxRVY5V+LY/fwH+QxcVyhFPzEE9/ll37/E8SvK8qWS32tGq7Ssr/Op09JosqDHYBGXJJXEXkZU1aa0mjSuMRaRVVXgDZWeQ+8oop8Vygs7PRGPDju8eT2Id+/dZ3/6g9+kO71GJM6MO48JwM4q911qVoxGM8MVRIvVZmXuVjnFlJDJ57TjAq0sozKFDWLyPtBQR2/WGX9/unAOnimXZVlcNWRnQIeTE9RloqqaTgXK+b1YDWJq+buVPHOr9VEwRCMXKpt0bFgnLVCP5kR64qDeYd2PGdUZW5estA4Mn6eRLmq9za1JBNLlUZOnSj3HGwLv9N6Mam+2VgbTGYoW9GigEj9vTHZCVSkLK5ZiapZAjMFeR5zZFrMyph2mtOMC0Z5RlFGVFYxmyWMxg1M3QeKPGYWVVSlO9b9QY92ltOInZpxXsaM85Si0iSRoag0ReVUh3kZYYymKiNsbOnedd6UoeLXRjC8CtFM2t4w3dVLnqdaWUqrfSZCPLV+/JuWIVEKrQwqMtjIFWLp3jXMu24eK1p1FWbxINV1A8uP78HDvxRbGNSH/nGhigAWwaMUr1hVFMIyqAAX4IVpe4AP1sIgSoJQgU7GOK++wWDA5uamD8wEQp49exZw/lWPU0FJABPColC9Jq9JwH/mzBnW1tY4c+aMD3rFa0/UIqHfmbSPwMjNzU02NjbodDocHx/7lLLpdMrm5qYPJi9evOiD/c3NTTqdDkmScPXqVQ4PD9nZ2eHRo0d89rOf5Y033uD09JSnnnqKVqvFeDzm1Vdf9YGvKF3m8/mSkkQp5yklYPDo6IjZbMbOzg4AR0dHpGnKeDz2QTvAnTt3uHv3LltbWyRJ4n0Ze72eV6+Id1UIdqIoYjAYLIFU8WkEfNCntebixYtkWcbx8THNZpPnn3+e4XDIG2+8QbPZ9HBDAnV5tqGaRZ5jmN4WpsuJKml7e9urZIwxXjUkfU3SqAV4rKqnRCHV7XY9RO50Or6Nw39hUQTpa+Hvq2Mp7IcCPsJxJ31Y9gsVwCHUCdP8w3M+DvStXk84XsJNjrU6lt8P/K0e+3E/Zf8/7Zre7xpX4cr7XU/YzqFyUdRt169fRynFSy+9xO7uLnmes7+/z9bWFq1Wy6ciCzzKsswr1AAPbcLnCMvQOkzRlLTog4MDHjx4wGQy4erVq7z77ru+onhYAETsAgRgheAkTG2Ve38cUJb5Kc9z7504GAw4OTlhb2+P8XjMxz/+cb7+9a97qBkq6WTBRdpsOp16eBam3gsslPaChcWFLFwItJRnlCSJt4AYDoccHx+zvr7uLQnkHgWWjUYjzp8/7+f+MK1Ya+0XXkQdF7aDeNLK56WdBQJLcSpwiyQCXgHvOdvpdHzac6ial3kpfNYhJBf4Fs5R8lwErIbWGOHiQdjXrLV+gUdUoo1Gwxe0ChceZF7QWnt1YJqm3p9Q+ozAx3CBL1TVCixttVqUZekXyGTOlPmm1Wp579vHLT4IMA0tKMRDWRaZZA6+ceMGTz75pH9/MBh4WBz281Bh/ufZPhAAMfQpc+lKLtDR1QIqOZi3UMuUDUUyMT5NWdSFNlJYu9jXakU8NV7tBiwF/4Av4iJKxsZBQev+zIMAPclR85L4EKgqbCvDRhE6L8luzrBJTH6uD7H2XohWgc1iouGMqt3CJho9K7FpjEkiVGkoWjGt/THFRk2ut1OGF2JmGw4GSpRiGi44VqW7xnLoUppFGSYKTWVguhGRr1nKPCaZQdW1DC/ERDM4fCnBjBWzew1MavmlX/tBtqaWRz884xNP3SSvIp7rPuR/ufESc2C8G/HsZ26w0xjxL77wOdbeNGSZojyr4OOnREbz7z77dX7+7g+y9iY0DhQnzyi6NyHvK5KRZXgVetcd+J3uKMp+BUovKYyiubtVeV4oSzzRpKc1mKu/B7u3anBcQ6HQzzKeW1dAJYJ06ArqHL2gaN+FF3b3+Mb0IllcOj+0kWFwKWL8yQlrv9tEGcunn3qHL33nWZJRRNG2pKpy6eE+tV65blNWLl26Ul6dBQEwipTvTxgFlaL7doTOIe85IFKmhmKtQpWKxl7EfMO49LqpojyTY0vlPBELDZVCla7yrUvzVaSnJcNzDhK7auHKzQTauus1LFKRa8WRqmrVpLILxaKu96/3oVIQ20WBFwI4IX6JdXVxed2nXlfKw0GBD1i7xDNRqvZ+dD8Vy7zTqwYFFAhUtCz5H74nvVvhgZ/3UV2xgxA4KNBRV3Wqc/3TK+jU4lmGqkRLrXzTC9C4lAKtpI/YxbFE1Rkr/1ljFTMbcSY+4d/ZfpX9ssvEZHwxeppWnPPZ3ptoZUg3K/QVw8RkJKqiq6dEyjA2GW3tgo7KagyaRJU0VMGJaRFhSVRJYWM0hlRVVCjGJmNmEx4WawD82Gdf5dUXLvJC/4CNdMJ6PGErGdLWc742fIK1eMInOu+ypqWSbklDlVT15DkxGbmNfEprQxXunMqQUjGzCbmNOKo6nIlP/TXdLjZo6IL/+vb303gQU2WLMexUrIvFHBu5hYF06OZvrF2oRxUeLi8UrJYHkx5nW04hsj/rYBNDduQU6/LsTQSTc5bUKBpHJcMLiZ//tTJEKLRyqjWTuoWEdGgxrYqGip06MXX9OJ4aTBw51fPIUjYXoCvKLbNt5zerS0t6avnSjSdJs5KicH88xHHF5LBFQzlgJYBTFzhVZu1L2r0z5+ClhrvvemwatFMAanwRHmUh0gaDdSnS9Vzk5ykDJrW09yyVKKXrsTRf09h2iTKJ+w5LLO00R9WqwTSqKKqIVFfkVeRe14YZCToyRLEhigzzWUKZx+jYoLWpFYcVldFUAWwGBzmVsu64cUWvMWdWxkxmKRUwPB9hkuV5QpfOr9Z5/lryVJGvVyRD7e0VjFWkunTzWT0fRIXzF5abTqmIYoOo0FXtxwpuoSobuIJswAJaWyh69Vz65/Or/t72F2hbDdLD4iii1oOFn1xoRB8a1YdqgxDSiToGFqAiBJWwCJSkcqccN45jBoMBR0dH/tyiPpTUwCzLODg4AFhSPcpnJT1aUgblc81m0we+ovBoNBqMRiMODw+XKmpubm56L63JZOLTwgCvlmi1WmxsbDAajej3+/5YEpRevXqVb33rWz79+VOf+hTvvvsuP/mTP4kxhpOTE1577TXOnj27lBorfmQ3b95kNpvxyiuv8KEPfYjXX3/dA0EJAkVdNJlMfMpbnuf+PkW5IhU9AQ+CBQQ8fPjQB9f7+/tEUcTh4eES8JUiCLJ/URScO3fOw4Isy3jllVe4desWnU7HV/YWIPn3/t7f4969ezx48MC3VbvdZmtri7Nnz/Luu+96UCubwIwQcIcBe5jWKZBjfX2dXq/Ho0ePfAq6gE1R7sh9C7CQoDZNU99XGo2GB9aj0WgpoJb+G4K+MJAPoYB8flVlKGNmdeyEfVruN1TXhenY8nweB/dWFUKyb3jeVWD5OGXiKqiT/VaD+dVx/ji14uNA3yqoXN3ChYrHqQ9Xr03eCwHLZDLh3Xff5fXXX6csS7rdrl8AEOVtq9Xi3Llz9Ho9D5BhsdAgMDqEkyF8WW33UCko6e8XL14EYG9vz3uASr9++PChPwewpEIViAN4RV5YYTrsE9KeMh5E/fs3/sbfYDKZUJalX9gRKCpzjqTvh+riVXgr4EnmY621h21hFXpR6oJTKydJQrfbZX193af3hkp1GcdZlnkAKN6UAu/ColZhqm0IGOWeyrJkf39/yRpAFkMAr3qWTdJsi6Lw19rtdnnw4IF/Hd5rmSHtLfAyXKgIPycVwMNFEBnXIagLx4ekQ8u1yXMwxtllSB8T+CttEvZDsdcQSCznChcBwz4nXrOyCCXf0VEU0el00Fqzvr7O4eGhV1CGx5HvagGZYV+RObbdbvt0cHmv1WqxubnpgWkIlt9vgebftH0gAGJY1ERVDkJERZ16GCv3ewlVXamVGlKIiqgSZYU/4LI6yadProAKX5xBuWBPF5bs1FI1ak/DwC/R9hre10xJheVYY3st52mogNJ5H0alAWup2ilqXrjjlBbTTJhvNmr/Lst0Kybvb2AVTM5oqtQBJpPahchB0kUFZpZgykUKlY1AVTDfMpx5/hFHT7RppwXGKkZXLed/KeXhJyz68pjnzzwkjUqO5y1GeYr55g7DS5p/7yN/yJ+cXuSb33qCd959msbQUlyF8XnF2eYpP7H+DX7t7EvsnYe//uHXuNrcp6FKbs42GVYN2vcVk12YXihRpWKgNL13nRdl6572160qiHs5VdoknluyExcoN49coBYNbK36MCQjBwujovaAMwu1kahVTeICQ1VastOKW39TkRxHtB+4YPyFT7/L3f/+CXYaI0wdsEe5JT0tGfyo4ZOXb3N7+Axlpvlbm9/k1Y2LJKMe9pwDIlIkRRkDlXEpzI3UF6wR6OCBVxR5RSjgCmKg0blLIa8yS9WAdKDJ6xTHKrOYXknyKCEdKKrDxPuxRTOFSVzqpC4V0SjCZBY9q1y/EBAosHJFGbecRl3/X2Ch/L9UDjbWYDFMIVWlO7aHiLAwqvPjVQqvBAVkanCICsecXQaKStXwUmBd/XxrlaHfLbgfr0oMzi/jeZFiLJ9djF2fYozlPanKSqErZzdgE+2P54FgDQtX95NziMfhkuowLIrk4WndfMqSYFjTU2Y2IVEVWhk2szFaWc7Ep+Q2Yr/q0dVTtuMBQ9NkaJq09ZzCxgyN9iBRY9gr+2xEIw/zZjYhqfM1xzaloQp6ekZqK3rZHtfmZ1lPJvydi39CUj+0CENLz2nrnH+r/zqHVYfK6hqoGYamyRhDW8+p6pULOd9h1WEzGlFZTWFTEioMmoYq2I4HjG1KZRpEyrAWTRiaJtvNEQ/n0Ny3rnhH5Io6yZh27ejazdQQR8b9oqiOXqQvK1DD2MGnMuVg2uZMe4Caa4quorALZbCzpHD7JIOCKE8om25BI2wPmxjSgVsokHEdE1FRL1rV/V3XlgNFe5EOLN678djN6VYrqkyRpCUb7QmF0UR1Z83nCbpMaD1yCkTpK04VuVDCzjfBiHoYqHCegaMgXR3roJzGFROxyil6XcEsGSswuCTpvfhxYxWocbys7K37bFG5xiuqCEuKUtaDwCiylEVEnFTMZgkmj7CVosod4BwC1ip6jXmtYHWw0BjNeJ5irKKqNEbhz2PrIk/tPZdGHFoImFgxOecWXHwVbi335hpnLZrQiebY1AFeEwGJouhoot6cpK6unjUKxh3LdEOhS+VUpHFtf5FD3sFbL/jn0qznyO9tH8hN0ssEzsHCmyhU/KxCDQlaV8FICEEkGFoN6uS8EgRL4QFRgknQAYt0TUm9leOGYCMECnLtoWk/4BUxopgRZYmoGcQT7N69e4xGI+bzuT9mGGRJwB2miq6trfHiiy9ycHDAeDxmNpt5r7O7d+/yQz/0Q/71CxcucPnyZRqNBq+//jrPPPMM7777rg+YXn75ZZRS3Lx5k7Nnz/Lo0SMPP59++mmm0ylf//rXl3wNjTE8//zzfOc736GqKjY2NpYUL+JFuL297dvm9PTUKyxFUSgqSFEeTSaT90CFRqNBv9/3cFUCyatXr/Lyyy97X7If/dEfZX19nZOTk6VAuN1uc+nSJV555RWfmvjSSy/5cz711FOcOXOGs2fPeggRpjUKrBSVkyjDVpV20g87nQ6TycQrjULwPR6Pvaryqaee4saNG0vgQIBjnuecOXPGq5nEr0v6cQg9BLKsgrfHjY8wKF5NPQ4/t6rqlePL/o1Gw0Py1TZ4HNwL9w2hfzg2w9//NHVieK3h58JrWU0Ll9dWz/enqSEfd33vBxofB2TDeUuUeZIOLOnyAlekqI8UCApBcXgfAlREKS2QMYSzq4Wb5Fl97Wtf45vf/OZSFWhpl1CdJnBPFGOiyA37WAhgw74Rtk8I3NvttveZe/HFF7l58yYPHz70ADq0gZDFDgGQ4f3JvCkgaDwe+6r1skgR3pP08RC6b2xskKYpN27c8H04VK5tbGz4tORwYSkE8wIOBcKG8H5zc9MDs9CjMPwOEaglxxDIKH6P4OaRzc1NDg8PgYUKXwCjzKuhElj2DT1sjTF+rg09EqUfhAtrMj5lnhWQurrIF/6T+WtVeSv2H+GzXYVv4SII4C1CrLX+OxrwHrfhHBAqAkM19cWLF7l69Sof/ehHfZp+VVXe+7bX6/HhD3+YbrdLWZYcHBzwjW98w6s/5brluFrrJW/Ev3QAMTt2HlVW1ybntvbw0goTOWgYl2aRcmid6qfKXAqqSZQzwg8UShp8deIyrn2yyoWi0RnnO4+8/o2KZFhiUk3ejSgbQVGVUBUVAApRK+rSXW8yMqTWYqtgJwvVehsTa3ReUfQyqqZ2KbwbTSZnnIRGGeczqCrnzyRp2QJvbOwUTrqsU1x1rcSp1WO6BNMt+evnXud3Hz3DejZhViVcG59hvBOx8/IeH9u6w2/deI78bpvGviaegN2C6RnDE9kjfu6Pf5D2A83okqH/5DGf3H7AV25d4SOd22gMn3vlDbKo5NO96xyVHSoUr7Rvc1R1FsVbalhVNaBxatC5VMV2zaHLBf/RhbtfG7kKmLqEvKtdWvFEk69B+24NEWeWqqlIxjDZ1gyeL1l7zXX9KoNk4CDiz/zVL/Hf/O4PkJ1a9j+i+b6Nd/kfsydpR3OYa6ZFQpRDNCl59qkDUl2iKsvoQsTnmnv8P9dOGeRdTOoUVzpf9rC0sTMJdB6XdcC98r2ufFq5dZVkc+1SuNsQzRVVXZxF2sUkELVKotwVAohmdfVT62BB2XRKoLJtXZpk16KLyn2mUrWSUE6++CnqQ5U72CyKQmWC9OsaPkrQbWPr4aIyToUo1ZWJrUsHlgGg6//XqkdVKQfYA2i4nJ7vfEFdKmKwEirNq3CWBaWo+VikHFscaFTLYPA9aYQrUNHDJqO8d6EujUv9F1gox4oXgEJe80rEyi6n1wbHF/WhP7/FFUyRP9hkH+uAF8CpyTgxLcYmo0JT1GSrHc/JbYRBsx0NaOu5V/KBg9qNqCC3kf85swka44AdBTOb0FCF83cDN1a1pq3nNFTBwDS4mBzR1TMiZdA4lWNDFzR0QWEjtHKgsKunDE2DTe0qEw9Nk56eUQED0yBVFa36uEPTpKunpFRL13tiWjRUwaHpgHWvRRg+v/E6Z35qwB/tXeXkxgadGxHJ0GLSBZzTpXXK3Y4mnhqf7i+wWRmX1uz8W107Hxx3OR62KOYx0yLGpoZk5Kq3V8liX5MtaHvRgflmReeWq96r0b56sM4hmTovXoyqlX0OlpdN5VKYIzDKOkBXQ3pla//YzqJfmsSlcZ9tDRgUDYoqIokqHukONoLxWb34nqmPocQnVbnvBliM15Oq7dPLVeXmImWc+s5gKWzkCtUsvND9eFt7pyLv6sWCh3WqcdupFYhKQWbIopLBLGMyS0nTkrhWFGrlrBasVVSVq6JcaUU1j3wlZflOMEYzmaVYq1DKUhpNbA1J7Kpv52Xkfs5j5mnh7qm2b8h7iqKlagi7GOQmcXON8y5etLGuFJXVbMQj/rdbX+JzP/g69z6zwcSkGOvG2ve1r9NSKc+nOT/34f+Ok5da7P1IH4Db+RY/9+W/xnxT8XCn/v7VBlFy2/pZ+Hn0z/632ve2vyBbo9FgY2PDB7qiBgkD0BA4SPAmm6hUJDhchYShilGOJ/Cn0+l4uHVycuKrgUraWqi2CVVEEkiEip9ut+vTTMOULAnqkiSh1WphjKHZbNJqtTg6OmI6nZIkCaPRyKfpSeAbpjwLJAiLusj9NJtNrly5wrvvvkun0+GFF16g3+9zdHTE/fv32d7e5vXXX+e5556j2+3yzjvv8Nprr7G9ve3Vk5/73OcYjUZcunTJB3L37t3j4OCA27dvA64K6GuvveaDr/Pnz3v/qWaz6T0W+/0+k8nEe4ZJwJWmKTs7Oz6FrtFoMJlMvO+ata5Ii7Rj6PXWaDR8AYokSXjttde4ffu2B7Kf+MQnfLXaL3zhCzz77LMcHh56E/9Go+GP9+KLL7K5ucnm5iaDwYAnn3ySN998k2azye7uLv1+n+3tbQ/mQgAlgXH4etgfQxUW4GGpQBPxSkuSxBfQyfOcbrdLURQMBoOloj+i2lpbW+Po6IhOp/Oe1O3VIFb+L310FeqF6afyOXkvVPOG6r3w5yqQE3AghShWx5+cY1XhF16DnD88ZqhCXt1P9v3TFImrQCIEpuF1Pk6huHqO8DOhyks2ed6r8HP1ngXmhOro8NoF0IhK7f2uIUyrl4USAT9y/PC5SptIv5ICTXINok6sqsr3QdlH5pzVuVeuKYSxMmbC+TGEWeDUk1/84hf5oR/6IT7zmc/w4osvcv36dV5//XXyPCdNU/8dEALOEC7KMWUsbG1tMR6P/XwubSegVtKZJQVVVGViMXDjxg36/f7S3Cop3KL8Fmgl1yHFbeR+19bWaLfb3qNW5m3Zr9Vq+YUDAYphUQ5pc9lkvpDjh8pP6TdSkEXmE4Ggov4TW4X19XVfoKXT6Xi1pixqxXHM2tqan5ukrWSxQu4zhLDSFwToSbvLz1CxnOe5B50C0GUuMsb4ZxL29dFoRLfbpdls+oIoMl+ORiN2dnaWFhjD51OWJXfu3OH8+fM888wzXnU6GAz4pV/6JQ4ODtjb2yPLMjY3N9nZ2SHLMrrdLnfv3uXtt9+m1+v5ZyXzSVVVnJ6e+r8V/jzbX3yAqFyamvNoYlG8oFYiSWEGUVfosk5VTRyoKFqaaO489kS55UAjKL3wQqtSZ3IvirFkYmgcG69oHF1IKWugHM2dAiIEEib43XstWTy4TIaWqpsxvNyic3uKqgyznQydp0x2YpKpUzX4ggDVAjxUDbwyRCqFut9rmKhB1Wb5tlW5KqjgqzJbDXGr5EPNu/z8N3+Ayasw29DoC5bZDuymcz7Xf4N//e2/QqOE2YtT+r0Jszyhn5Q8nT7k+Y/c4uxfHfD59e/4+7zQOGE7HhIpw19be4th1STCsB0PvFLntLLM11kEWA1DfBwxXdc+qE9GFpNA95Zh/a2Mxv4MVRmScZ1O0tVUiVMglU2nLCk6ti6sohk+Ac0953U1/CT88Edf49U//rDzOMxcgD+8mPDZzjV+LvkszYOCc588ItMFNoaz6Sloy7yMaR1VVM2Y//3F3+V/ePQp5j3N+JxlPWpx/d4OOxqqjqGhirqSrIWyghr+UBlfcEfV1WMFcOm8XKS11R51VtfKvBoMyPvxRDHfdM80igyqcIBBF051GM2dEkZZsMYVVNCFwmQVKi8pW9S+eHWfrBQ2MT7VWFKYbWRRhXaMoC6qIqpBZep9HtPJBUB69aF1wbPOteuXEhNpQNva09Gphm0N+aEGbDhoKHDEqwrFG1G8DX0qcP1a+IdYXH+mViKFKuJwPLqTsgwTCeaBZJEmaiPlvTQFBlrUAloK+wvgoon0EmgUuOjPL80Zs6RgdGDJMi9iDk2bymruF+u0dM52POQ1e56jvMXEZmgMO9GEsU290rCtcmY2oaunVChfIAJcMYiunlLYmDPxKSdVawnwScrz2KZMamgp6sEz8Sl7ZZ+GdirFmXW0f2YSZjZhLZrQ1TldnbNftenqnLGN2YmGzGzCUdVhTU/QymCs9vvnNmJgGpyLT9mv2sxMQqoqjHKqxu14wN/of4u/ufYNXrtykTvft8G/evdF7Bsd1t60xPP6uVhbPye32KNlnNQpzSZWqAiKpkZtzDi7eco4T2j2xuRVhJ5G3kNVNmVdWrIrUOR8UmUhJwoNQ+u+lIwMZUujWiWJipiYFBu764nmrkPrXDFfcynP2Frh2Kj9VhPXX8omrGcTusmMdpyzP+sATsEXzaF3q2K2rv08MV9T2MSNEVNbI6gSiB3EbOs5l7vHPLzc46jZwGqLLhSfX7vL3Bb04ylnn9nn4WYflCVJKsoiQj1ocPRcRNG1C2Wdgipz3poQjFGgk0ka88KvMIkqyjIiz2OnOCw01rp21K0SM4v8uNDKutTmIl4oFevTVkajtaHTnDPRljhy3yvin3j0YeOLn8jzEPWfWHqYwN7AKqeM/a3Bh3ixdY+/1b5J1nAp7QbDn+TuS/4/P3qOv937Jp/IEqAAXLrnt/O7/Fz0GUxi/Xzp8uTtYi4UFff34OEHchMl1d7e3ntSJQXWwSLADpUQEijJH/fyh34IdkKFiMCobrfr02bFe67T6XhwKIBxVR0VgoAweOh0Omxvb2OM8UrF09NTryxJ09QrB+fz+VKBEAlwJRCWID5JkiVwKmolgZCh1xlAv9+n0+nw8ssv8/bbb/Mnf/InrK+vAy7YvHDhgr/Hj370o16deHh4yMHBAbdu3SLPc37/93+f+/fv+4C7LEuOj499aq2ocZRyhR/EUF/u5eTkhO3tba/OEwUROAA5n899WrIE+81m0/uvhX5/aZry8Y9/nDiOuXv3Lu12m8997nOMx2PyPPdp4+12m4sXL3L//n1eeeUVPvvZz9Jut7l27RqtVovhcEij0WB7e5vZbOYLRJw5c4Yvf/nLTCYTn/YsykipRi19LkyXX/X9k/aQFL5QMSu/hwURwBWNkRS50NBfVI7imSibpDCLt6UAgsep64Alda4E12FBmFBFFfanVai4CuEfpzIUAHB8fPwe2PenqfQe95nH7R/e2+MUP4+Di/Jc3u/8cqxVFeT7jXF5juE1rF7rKrAL4Ya8Lmmj4cICLPqYjHkZP2Ea/aqqT/qA9MNQBbqqDpRzCjgTuCRqt1ANJ/cSKv0e14ZRFHkrAWnHUPEov4cpu+Ag6b179/iFX/gFtra2OHfuHC+99BIf/vCHsdby2muvcePGDQ4ODpbsJ1YhESy8ZXu9HuDmArl+uSYpUCKqbrlOWagJC0dJv5G2E7V4qO6TOUAqDQN+7pPFEhlnYt8goFBUnWJhIOeRIl7hopAseoRKylAFKSnnP/ZjP8b29rbvS9euXeP09JTDw0M+/vGP8xM/8RO0223m8zl/+Id/yOuvv+59aKWQiszt4Rb2x7AvZVm2pDAP/XhXv3vlOYkXpTwXAdari2TyjKVwmPTxsIiKwNYnnnjC96kQoo/HY//eL/3SL/HP//k/J01TX3hF5mitNb/xG7/Br/zKr/g+81M/9VOcOXOGa9eucePGjfeoeuUcki7959n+4gNE6iInFp8OqOv0ZQnAdeECxbKhiefGpazVXmrgVIvJxPqiKYBXgUR19Utl3eei3JIMK9KjKflGk+xoglWKvNt1HolmASR8xVFrvTeS3wJYYSNFlBtspJltKKKiQTQzTLacf1PZVhQzBxpN6gKfqumCWlUtQKBrhPrwGg8PZZOUufh0Yb4k15pmBRfjI/rPHrHXWeNvfvxVdtIh/+LNv8IP77zBxfiI5GPHPL25z49vf5vcxnx75PwmhqbBj2y/zlHZxlhNN5oS4Qo0VDhFR0MVrKUTElUyMykVis1oxK18i6pR+0xNNWo9J54kKAvzDUhP3PWVLUV2akhPSlRp0HlJ2WqSHRWkwP5HYuIxPl0XZYlyGJ/RfOzT13jtV55DWbj67ANe7tzla+lHnColhnhq2f8YbOsJaMi7Cf/Jk7/IO/kOVQadaAYaykpTtDWT7YinkwMGeQOTQDJSPChHxPczBzB7OQ1VugrfsUaX1SJWtNYr20RlJNU5XWVYtUhvrtxD9KmT5QL4CjDGQpJUUDnILYo+YFFdt1TEE+XgRGzRw2kNKtQS6FIClkUVWPunyXhwx5TObf3xPXBTrp/bUGob9r9C0pVZKBMt2MwQzZWHgwvl4bJK0PXzGiaKwlfgoSwc1G1JWJk5uAYZG4/dBPjVoi1XLRi8B5zAyfr9JQ9FqeQsbYldun6FxWi9AP81ZBR/Q9cHltOWdWXqYjR120SK4bjBm7NznEuOeXe6zRPNfXrxlI10wrBoMKgaDgLWCsWZTdiOHABJVF3FzmrW9IQKxb1ynW40ZU27MVuhHBy00NJzWriCIKJMLLT7yhhUDjAKJCzqlYuWquVqEWxGI//62MZsRO6chY3cAoJ1nx/blK6a0VBOAdnScyrTYGYT/n/s/WmwZVl6noc9a+3h7DOfO9+bc2Zl1tRdXT13o9EDAAEEBwyUTNKCZTJsixZl0I6QbdEOK4I/ZDv0w1KIDktyhEwRIimIAwBShAAhSAgCIADdje6uHqqqq7OmzMo5b97xzMMe1vKPtb919j2d1eDfbvWOyMh7z7DHtda+69nv976jMoWiF0ydZ5/VZDbEWI3Rmrdme9R0xk90v8Pac1PWPjDh7733aaa/uUnvVrZUlxd42OUV2rgHSDp31gBb6yN2m0N0yxIqF1BzFK0TLNz9w4Z48JN1C2pHYTm+A4F7wCPKw0RnqFbOYi3EBi58SZXKv24wxa6nzDcSbBCQtwp0rlFGUcQQzvEJwWnPEo2U7++NMCNQll5UqjrzGkWhyNctWTOgSBy4sgFYbUpPP7sMKaqU0rb1jP/yym9Tu7r0f5malLt5ziuLFv/e5rf497feONNFfvHhp/ntxx9met2pDvxDA+nvpYegMhYWmvunPQBa9YVTDxYBo0lEFBWki8il0IfupmmLMrxHHr6lGiJDXMsJtCHLA2pJRp4HZFlIEJRhO5lrY3GY04nddg7ooFPlhi253iVYraqTdWHP9GVVQGpDtLKcC09ZC9wfaDdT5+X5fDShpSKuhN8kKs9XQ8f+/EQYCCy1I03etORtgw0NqkJUVaGcqtl8j7Hoh8v37bJanled+D0NAMgf8jKxEUWClDjJpEcmGwLwZCIiPoHT6ZSDgwPa7TbdbheAg4ODM5Om91uq78tkSNQjw+GQTqfjJ4+z2cx7UYkyRYBLdfIjx1YtxaqCjapiSNZbVUEFQcDR0RFhGHLp0iU6nY5PF7179y6PHj3yKrjf+q3fot/v+6CU/f19vvjFLzKfzzk6OmI+n9PpdM5M/iShVI5DXrfWMhwOOT4+9r6GonTJ85yNjQ2azSatVotWq8Xjx4/p9/v0ej06nQ77+/skScLVq1dJ05TJZHKmBPFHf/RHefLkCe12m6OjI/+elFk2m02eeeYZrl27xh/+4R/y8Y9/nE9+8pMeFkRRxPHxsVcf9vt9lFIcHx9Tr9c5PT1lf3+fXq9Hv9/n4ODgjFKyCoKqyjYBDqvXSNqFgBq5jrIvUnJZvXbSdmUbVdWRTJ4FxEo7q6atPg2OrYKkVSXc09R5VehThddV77mqAkuOWfZZwjBWFYar6rnq8r1UPKt9/nt9f1X5J+dg9bNVpeDqvq3u5yrYrP68+lr1ddl2tc2Imq7f73u/Q1FPy35NJhPq9bqHOtKGRDm4WppehdZVyFZNxK6+vhoGIfsv12y1xLeqshXlZHWR9VTbYLX9yr5WVeGyyLoHgwEnJye89957rK2t8dxzz/FTP/VT9Pt9/tk/+2fcvn3bw8dVuKmU8mO7WAQcHBx4mHTx4kXCMKTb7XqFr7XWP/AQiCXnSs6XPOzZ29vjueee8+d0NpsxHo/p9/tcvHiRD3/4w8znc772ta8xGo3Y3d31ZczSL0RxPhwOzzz4EnuGZrPpfW0vXLjAYrHw+3BycsK5c+e+y14A8A++3nzzTR4+fEir1fJQ9J133uH09JTZbObtKQRSvvDCCxhjeOONN3xAk7SPoijY2dnx50HKm8Mw9KXX9XqdOI79uZQwmtX+VlWOyzWTtiQl0TK+VR/+SZsW/9d+v+9Vs9VxrzrWrrYr8RqW45AS91XfRUlAr6paxYJjfX2dZrPJo0eP/DFKn66W2r/fw4unLT8QANHDMuugkQQUOD8pRREplyKcOsBmQnUGFhaxKhM63WSwKMNSTOh8jGonhVcKhtOC2psPIYpQvQSVFagsJ5y1yOpqGb6glv+8I1upPKr6ngGV0mn33ryrCerO88r5Ny0VT1YDZeruGUAoQKfcdoUlOiBUuCRlpZ1axYjvVfn9JM6Y2Jh//erX+UrvKs819gkw/BvPfY31cExmA/61q6+S2cDBwGDKx9vvkdqwDEBwG28HMxKVUVjNRxp36QUTmiolI/C+apkyTIqEILA8XPQI0jK8wig6nRmM67TvpRx9MkBnIc19Q/9Pz1ncbGKDgM3XQzfx/4UD0v9ik+Q4w2oX9NJ9F9IupD1nbD+5ZNiMJ8QjWHQ0f+X8KzxOe84rMhQzfcv1jzwgUBY900x2FS9Gc35ntA0KBkUdcgdvwqab5E9tyJ3jdWpliMsvD18mGji41WwsSFThyijn+RJ65QU2Cp3ipQRbRQ0HOSJNODFl+wRJRlb5cqKpc/evqDmFki0DS2pRxgIoGtY3Pb1wn1PGqa8E9ulJAMqpFJeTZge+bWDdRLcKGkKLtWbp2VWWvatcexgo5chOTWhgoR2wCN1kmlzjU5kLhY2d0lFlypdTev9D5c4V1qJwZctuu9KHSvAvPna6tB6QPqSV25W8AtNL9aAWUBAoDyCXabpLmOpDV8r+uuxIbv9EJYmxS5CrliXLLoxluT4bKJdQXVmqsNF13xUAaSwm1MvgFWMhcIPG3EQUaEZ5wlY4YmJqHMzb1IKc9xbbnI9OGRV1BwNL8isgv1CaWBWkLB8iuFLhOonKSrXgnLZ2SgGBhwGWrOzricpoqAVTW/Nl0nEJDgU0NtSCiamVkDFwisfyIhclCNTK0NML9vMu/aLBRjBmPRiXY0TAnIiJqZER0NFzhiYh0jmFUfT0lL5psFkqnAVUPlys8X+98S/45l+7zD/5p59j96sZuoT2Z+wkPPdy75nAqdnGWY1WtGCYJyzyEBsZTl62rgRf2jk4hXBpA2GiZbuZmphIBfyV3iv83Oe/xf5n2mQEFFZzJTrhqCj419pv87nPv8vxZ+vMTcR/cPvPcffWNovzru9Eg4C8ZQhmmiIxxH15ggDTPCLSBWHpfdkOSyPybuHYnQQjWdfg1ThE1KvyUABlOSzq/OePf4yPX/lNamoJEGsq5FxYsBGMCKmzuvTCqUtTz7Tr25bSykDGh6V/qV5o1lpTjodNCqPoJinWKoanDbAKHRQUJsDmGmtBR8ZBRGUhNK77BBalLPOFg44AxrjPFIUmCAxRlBOUPouzPHKBLbWcRWK9V6XA43CmHEuMVv5AKtuHKlyb//e2vkxDxUDA1KQEynI5jP25uqRjpiYlUk9JQrGK5MRiTxXD6+U4EOCtGlSu0Iv3n2T+cPn+XqrqhurvMhGVn1dVQtUJaxAEfrJTnbgKtBHoJimZUr4lE6PxeEyv1zsDRp62n08DBtX3ZX2np6fey0r2Sz4vE9bqJLs60a6uu6rGrHoCVo3dlVK+BO7w8JCvfOUrPqRBvBAlOVlSkAX2jUYj7zk4nU7PgKLZbOYncUopDy/lOGUSliQJx8fH/vNaa7rdLlevXvXltlEUMRgMvJH9yckJn/rUpzh//jxvvfUWb7zxBpcuXSJNUx4+fMgHPvABTk5OaDQa7OzscOfOHdrtNtevX+fWrVusra2dAZ3NZpNms+mBZKfTYTabsba2xng85pVXXvHlg/fu3ePSpUvs7+/TarUYDofcvn2bS5cueYBcq9VYW1s7k7Qqk0UpO5ZzUVXuSTsWeCxtswpaRCUmSZ+SLi3AU0CJTKyrZaVyzcQbbxXMyKS52v6fBsTkmKqAHpaBFLLf1XLUaqmrtImq4kwgQLWPPu3np0HPpwHOp4HRVYhX7Ver2/he266+X1VwPq2fV9dTfW91PKoex+pS3Z4Am2ppa71ep9/vM5/PvTp6MpmcUXVV91XWKee/CglhmS6/ug/VktLVY5HPS/+X9iufrbZzKUmVcWwVDsm4K6ozWW9ViRnHsR9XpJ0FQcCXv/xlbt26xZ/5M3+Gf/vf/rf51V/9Vf74j//Yl8CuWgdUoZPAu2pS8qNHjzyAkhJ7SWqvHnMVSFlreeONN3x4VbvdBuA3fuM3ePvtt3n48CEf/vCH/dh69epVfvd3f9f3PXlIIA+jrly54rcl51UeKnU6HfI8586dO34Mrt63zp07d+b6S7s5Ojqi1+txenrKK6+84tufqLkbjQaHh4fcvn3bH9f58+f51Kc+5e8H0+n0u9pI9QGIKDvH4zGNRsM/qBNFaXV8k+tXbU9VFWuz2UQp5ZObq/f8VT9D8d1chd5hGHrf4GofrK6nKAouXrzIl7/8Za8IFeiplOL09NRbZsi5rCqrx+Mxzz33HOfPn+fg4IAXXnjBj83V+0AYhty+fft/YgBRLRVDRehmAVaX6ahBtfTMEk1cybGkNjvFlgM0tb7F1MvOV4AqrPMxtK5ENk80JoLGgSK2FpXnBNMME4eoQBFOC4KWdnBnIfu1VI7oUo0oJY5+Pm1LD8JAoSJXnmzK/QK8/58oUiRd1O1oKawIywmSwZU+ymTF++mVp6qwhLUcq+ISkpQQMoDNxpTMBqwHEy40+uyGA8Clpa4HY+Y2Yj2cOLVSMKVRBjCACw64EB+XAELAQ0igDJEqiFRBgMWg0RgildPUKTvBmFa4IJzA5HIBCibTGr2xxUSaP/XR1/nSrY9QxIpfePEV/sHdz1O03TU8/pDlN174r/hf7Py7FDXF/+0XfoX/x6//Ra7/gz6Hn+xhNUTDnBc+/IhHsw61vuH4JcW/2XnAR7/2BYIUsjbYBUy3Aj67dp/Dok5yoDGRJcPym/c/yKJnuT9fB6AYRRSxA35vpntkt9vUysn633v700QTd0132yM0ZZpuBRxiLYRlKmhZuqwKCeAxkGZEs4rBfgkQXaK4A4tSoqyMQs81JjH0B00aJWRUhjI4xakxJUhlsW4JZ4rmfY2tRWQt6yGDY4YKIvHgxJcooyzKlLBQAlB84IoARLceCgcWlFVYIenis+i6ILZWLGGOUg5C4ICoLhwItALOWJYzq8Ke9R0s+8cZwFjCQEnVtaHy7R7KcQKcTyL40JWqIskntRZ2CQIr8AncA4jq9pzfnK0AyPK7pcpUtlcNUll6Y5bbrSiUbQlIRcHst11Yuu0p6+HYeRwGKVklyXiQJlB33nbOf7DORjikXzTZDfsUSvv0414wJcBwPjxlYtzNK9E58xIYdkogOLUOAqbWlXbObeR9E9t6RqQKJqZGLxgzMTUCpchwDxnAqRhljABIVEFHLeibOpHKiTGlgjHwoTCZDWjrOYnN2M979IIJBYqmXvh9NWjmJiLRzhMxURkvJI/456cvcTM4x0cad9n71/v8rc6f4/zvLz3oCmFldgmPJVwlDApCbciNJtY5I1Nz5z6u/AFdCQFyqkb3QEGlCp05GPt7s4T72Xn+ze4+H4iWBoKBSngtnXM5tHwgdnBubEYOmMk6rXIJzRqKxECpfpf2pZVFYzFWU9O5A7PSaAXoWQiGQQkOXeM1sXb3ibIvbAUzfnHv9+jqs5AwUJqu+m5wKEs/b5AcK8YdzoBKr2a25diGG0PWkhnd2pzbhxt0kgXt2oKjWkEYOf/CIgc7DyB2Ccw6NmRpWJZCu+tWFJoiD0gHNWq9OY1EPJfK+0+pRExnNWphjsYi3UsZRTQs78OBKwmPB+581PoWnZpluJaFcAY3J+eYtm6yUHOOC8Wjos2Gtnxl4RqPKeF3jPP7nJqIc+GIwioyNH/rc/+Iv3Hwl7n4OwvqJ1EZigQoRdpWhFNLkBqmWwF5631P9Q+X79OlOgmoqiFgGQAAeMVedSIqk5NqUqYodaRsTBQgst52u814PD5TijqbzajVanS7Xa92eZqXovxcfV3WK2b2AKPRyENKWBq8VyfqVX+96iJAoOpfJZP++Xzu/asEUmmtaTQa3qD/7bff9ucyz3OGw6EPKZFyLFG5SJLzYrHwikaZxAqgql4DgasykY3jmF6vx3e+8x0/kRTwdvnyZbIs45lnniGOY37nd37HT9x2dnb4+Mc/zv379/nIRz7iQ1euXbvG3/t7f48bN27wjW98g09/+tOsra2xsbHBt7/9bX78x3+cN9980ysHZSIrUPRDH/oQd+/eZXt7mwsXLhBFERcuXOD3fu/3iKKI9fV1bt68yenpKe+88w6bm5sMh0PeeOMNrz6U4xXVkLRN+b9er3voUQ1RgaX/ZrWMUiaeArGrpZNStt5sNnn8+LEHlFI6WS1rFc+009NThsMhw+HQf1/+rcLvVTBXnfyuKvqqUEmAtaxDYIwEAUk/lOMS+FKFnlXA9ycBuuq+VKHV05RN1fGhCoBWYX4V6FaPqwouVvvw91qqcKkKZ58GMlc/K79LQnk11VU+s1rmW32gAvjzXg2akrYhr8t4sqoWrdodyPEKIKp+vgrRBIQLOK5eN1nkfSmNrvYDY4xXB1bbnhy3tHH5rIzbRVFwfHzML/3SL/HzP//z/IW/8BeI45ivfOUr3zWGVo9d+oq8L68PBgMODw/PbN9ay/nz532bFt8/aWvXrl1jsVjw67/+616lBnhrgfF4zK/8yq8A8MlPfpLPfvazhGHIo0ePzowPEh4iKtPquRVQL/cmuZ5Snru9vX0GylaPVWvNhQsXyLKMR48enWmj0+n0u8CgLFmWsbOzw/nz5/19QbZdffhTDemqKmDlgVJVlSzXN45jf3xy/FWwJ+1bPifrkPYiD8FkbJrP5yRJQq/XY2dnhyRJUEr542u1Wh7sVf07R6MRtVqNS5cusbOzw+bmpl93u90+o64V2Cy+lHLMWZbxIz/yI/yjf/SP6PV6xHHsS8xlzB2Pxx5K/ssu3/8AsbK4iRY+iVlgm4MDQg/cZ4PM+WSlLY0JFHldeUgjoMGEikVbY5YVSg5ObK5h04yiGZE3AuJ+Ru1oRpFoZhtu4lYtafTl1eBTjwU6OEixBIyymKUoxAMM7zcloELK8qAMkFEUEmShwZMR5eCOMhCExiVUlv5xqiz/roXlHwp6wSda7/kQhF4w8ZP6y7GLjRc4sBGMKdD0y3AEcJOrSOVEKvdea3NCF9ygMgyaiIJRSUGnReyOo+b2M4wK4okha2k+1r7LK8MPMzmneSF5BAqiUzdrTa6N2C1B6vEHFR+qPSTvFsx3m2WoimW+GfGv77zKf/TqT1Lf0OTrbr8n0xq9WVkaqKCoubLK+9kGtb67Jn0DRw96sJHztcNLTnVTBvKYGP723c/ReOyUrMEC8q91/XXr1mZMbbhMEo4jiMobmVLLpFg5/+KVWIs9DCcsyxJyyNoOApq2JR4o8sYSuoUzje0n6BTCsSoBoiKYO59EnTkFnLS5+qGlaCdOBSiqwQxsaJagT1mnMCzBgASoePWVxr+HlCtbVbY7F46g8vIPIL0Ea67tKl/maMtgFQrX9wQeun6rlqXMAtis9aXFyw5ZAXkWV8JsrPcstNrBPq80dI20DENa9h/XN5frkrAleU3Jl2XTfh3LfltNXld2mSp9JjzF92Xl1W/VhNhqeIqiAjqt+8PpYmfAbjRwnoOhU/tGqqAeZNSCnG44pa1nTG3NK/MilRNgCSg4sZEvZRZgZ9Ae9AH09Jy5DUobAtcOk9KjEJw/YawgpvCKZElW7ukph4XzbtkNhgRYJuKLaCErxwWApspcQq9xCj13WrV72FD6Ibb1rASUU++D2Aum9IsGtxY7PJvsEykXvLIRjKnpnLcn21ytHZLZgP/Vn/p9fin5AjtfdL63RaKc72Bl0bnFBppIGxZ5SByXKg3l/D9tUKCkD2iLzbXzlNWQdssSlGwZ/nM+HHI+HAJ1AnW2RvVDcXLm93czxd2buwSpwtRdWXfRdKXHtm7KthD4MX2c1ZjmMUmQkQQ5x/Mm2SRyat7IQK5RuSIaa+K+S28PFg6U6dQFMalJSEPBj9W/W1nwJy2fbN/my5OPsvGN4Mx9SWf4PtU4yEg7ETawHE2bbDYmrLWnPDzq0W7NvMpwMYuwVkHkSvWLQlMUGpNpdGjQkcHkGqUs7dYM01QOoGpDoC1ZHqCUUyjmRUCWhizykEA7GNm4OGLxToftb2SldYCD+mk7IDlOWayFHH+w7rwig5ys7SxCfuuND/KHD68RBgWDYRN7UMNGlmDiHi7JA0BbM6hGjp2GhB0HiopMow9jeu9C1gmxGuZr2lcSTPecz2Qw14Qzlp6nP1x+oJanqY2qk71qmXO13EkATbW8D/AlUOI5JeqtTqdzRgHUbrd9aVaWZV6xJmBNllVgUQUDqyXYYvovpVNVFcYqdBBQU6/XaTabvoyx6kVV9TlcLBZnwgxkkl+v1/2E7uTkxMNFgZCiFpIJl4AHSQy11jIYDL4LEqxO2OWcV1V1cRwznU6Zz+fUajV2d3eJ45hz587xla98hQ9+8IP+XMtxSWl1p9Oh2+1y7do1/sW/+BdcvXrVT9Alefj4+Jj19XVOTk44PT31XmLD4RClXMn4eDz2pvpf+9rXfCCAqBivXLlCv9+nXq+zWCy4c+cOu7u73Lt3j9FoxKuvvsqDBw8YjUZcvnzZqylX/QnF07JaXl8FF1XIW1XQyLmWc9VqtTzkXiwW/vyJklLaea/X8z6Isu7T01O2t7c9QKzCvmrbrF67VVC9quJabZtVpY2oz2SpTrJl8l7dxqoasLrN1b5d/f1pyyqce5rSsLqu1Z9Xt7u6zqf156epDp+mKpT3q/u2+l513UopWq0W9XrdBzMIkJG2U4X1AuXkdWmHVVAs+1Y9Vrl+q9uuXm95fdWDTl6vBolU2/8qiJbX5NpLiXG1XcES8Fb3QTxBtdYeZEnfELX4b/zGb3B4eMjnPvc50jTljTfe8OOMrE8AkADAqjdoNQRFIJ5sW7wFZbyuwnfxahVwt6qMq/afvb097yPZ6/X8+qVfitehKI/zPPehJ2EY0mg0GI/H1Go1D8qqoTWNRoPj42Ov6JQHJhcvXuTmzZusr697r1ullAeBq//EyzEIgjOeveKxWQ2tuX//PpcvX+bx48fevkLSkJVaJkUL1JbrWL23Vu9Rcr1E7V5N1Ja2Ifc5GU+Oj49ptVo8//zzZx5sVWF2rVbjpZdeYjabMRqNmEwmzOdzvvzlL/PMM8/4/jSdTn1/ms/nfuwSmFkUhQ/NknPz+uuvo7X2D93kODqdju9jR0dHf+KDh+ryAwEQdQHB3M2wlLGoHJ/GGWQOYASpJZgZTKTQtSVcCGcOgBSRckow68DhvKcpamoJApSDOcpYFjstdGZYrJVmsZMUPZiQxCFpu76EABLQIOChel0EKqolxDCV0koPKhTf7WlnliwnnDoPv2gMyhj6zzlFjAdRMqstgVWeBcQLtw5d7peJoBUuKKwDCaIwCrAkKqOpMo6t84ISeDg1NRLlklyb2n3XoBmaxKe49vSUoDzoQBkPKmKKEnBYZkXskoSHgVPe9eaYULH/I3AlOnSeVB8f0w5m2NASjDWLDvzI+Tt8abFObWjIXxjS0Dntt0MOPwx505n7zzY0iUqxd5sOlIWGmU3Rd+vOz6y8phh4smjzy8NPu1MVwX/d/yTNOyGzD8w4HjQdSFPGhRk0FMe/fY7ayJJ23fkNZ+WkMoBm6AYd50+pHDubLaAoYJ6iiw0/b1wGeJRPxGsuDEYJvNOgU0gOHWQyMdQPHAQJUhjcgGCqiMaWaFQB1BXgbIIl4wtSi6mF5aS9olyKDcwDD/pszUCmyvJEe/az4CBgVXlo1BIohgbr01nKf6LcEkgpS2R8qI8En+jc+pJlU6p3XX9SUDhVki37ilVAqJcJzcZSxM5rcKngK38OzoI873WoHHRVFg8kq4pEpZX3VvRqRVh6FvpreBZuftdn1RIaVn3ZPNxUAk3xv1fXZQPFRm3CufAUgOfrj9gNBxwXLTZrY3KjWZiIQBm29JDDokOiMraCERMbE5SqwcI69V6gTFkO7UB/UW7vpGj4fj01NQ8gASIcwAMIsNzL1+noOf2i4WFmT09JrUskjnRGROHHC1cKHZR+hyGFdaXVGQEbekKiclI0/aLhlJB67tOdAZcYbTX7eQ+AXjBhXj7hOS5avNB8xCvDK/zu6Qt8uH2f/UWXn/uRr/Obi4/Te1Ng7NlrJSrRQBsCbZjmMa1ogRF4nmvCo8CPw+HMwcJwYqkdpegiLO8xTnkt6kKAwpZjDpqGjilsWbqBxWC4GhkuPHfA/TubdF6t+YdeOgMTB5gQGvsWlRvyumWUlgENaZtalDOcJiTdBYtZg80vRaVPL2ALwpnrS1lLM7oQk3UMWdslJf+NBz9DpB14nhURGsvChJws3Di/KByM08oyz0PmWUieB8z6CRcf5ujccvJ8hM5KJX9Q7nfqAsXSNUvQmzGa1WjGKZ14wWnQ4OSgA8oFPwVRgTEaHeeEoSFNHbzNdEg+D52Paa6JuwXNWupvo9M0ojBLBSI47h+EBmMV2iqCwGCMpthd8OjzNYK5QuXlNWpZICZvWopWXj5cc/6seR1UP2I0dBA8mJYBUnIP9gDR2V3YRey68506RWLRhWLrm5bO7QkPfqLFYq28BmsFKlW0Lw0ZHjfJckX9QUQ44YfLD9AiiqtWq+UBX3WyWlXwydN/UYHB2XJAFT9BmQAA6H1JREFUWCrj5I/6fr/v4ZnAjsFgwGQyOQPBRNHWarV8eZfsw/vBhlUgIr/LpLT62erxyORD0h9brZafSIZhyNHRkfcblOOR71bXWy1BbTabZ0BVFayK0qjqv1WFA3LeqvsrcEF+lkm4bLcKJaqT4uvXr9PpdLzK8MKFC171cePGDf7oj/6IXq9HkiS88847rK2tMRgMWF9f5xOf+ASvv/46b7/9Nru7u96H7Gtf+5r/nCRlF0XBwcGBh6RpmvLo0SP29va4f/++h3r9fp/NzU3CMPTl2gDvvvsu0+mUk5MTrHUJ3P1+H601ly9fZjqd+gm/TLCrSraqAqe6VL22qkEKcj6ttT40R37e2tpCa31GSVS9JlUYE4Yha2trXLly5Qzkls9WFYsCnWTb0tarQKQKeaoApTqhl21U1WjV/lCFlKsqwPdbqtut9pGnAcFqH1v9/NOuw6qyb3W78pnqeVhVDa6+trqsfv5pn6u+JtsViJOmqYfpi8XCp7AvFguSJPF9rOoTVz1f1T5chXTVEBz5X9pdFRJXvQRX20R1fKueC3kgUm2X1SCW6oOSailrdZ+rD4Gkf0nbzPOcwWBAo9FgOp368e73f//36ff7PP/889y9e5f9/X3q9bofv+SfjFNxHHvPuizL/Hhz9epVms2mh1gCnKR/yTgbxzH37t3zgGxjY4PNzU3yPOf4+NhfU4GC/X6fGzduEIYhn/70p5nP5368lfM9n89pNBpe6S0KOzn37XabZ555xl8zeVCwWCx8Ge329jbvvfceQRDwrW99y6u4q+1MQJ4sVX9Hgaj9fp/xeExRFDzzzDNY64J95N4iQLYoCt59912vGhe4Judbrqf4K0p7lWsu96Eoimi1WpycnPjrKm2s2h+rbUnU2sYYHjx4wMWLF2k2m/6a1Wo1oihiMpnw7rvvejWlPEC7d+8ezWbT9x/pe9X74qryVY7n3Llz3Lt3j8ePH3P16lUePXrEYDDwHsLyUEkqG+QB3b/M8n0PEK2C1t0Z4XCOiUP0ovyDJNSorMAGATZyiY4qNxSNmHiQoqcpJonIW3GpBHOTNFVYbKwZXEkwkfU+iRa8uovSizCYGcJ5AbnBRiE6K5yqKQSLWqbGCkuxS+VZFTrowk1gbS3wx+Rhh/xncGmS2gHDYKGoHxmiqcGETnEXLAyDZ2KIS+hZKkOKmgLtUol1YNCS/CvqRQW9eEasCjICB6rQy0ADFE2VkgQu5ECXIQsFyvmjlbMrjSGQQR6FLg3zmmVps1MXGfqmUQIGzTCvUSTQfk+T12FxMSBtKsLtKXeyLbDwgb3HfHN6xakATQnZrOY/vP2nsVoRhzmvL/aoH1mGVxWLnZz4KCRvwj998jHqB071R6b5LwfP0brrIHEwc5NeE8O3npxnfL9DV7vP/ld/8FnWTi3zwBIElrybUbsXO6hjIDmpKsyWINAGir3agPv5ugNhoYa5wU6mqEYd22pUwNGS76Ic6DIBFDUXJKAUqEKhMwctrXb7WiSU3oZOzSLXGYAcnyTuqhsVWsCzhWhimG/FkFtUqrCJU1KRVxtp+b9PLFbL9iK/V6GgdJDAOrAYrLyurPOQy9Tyc+CgY66WkNxYVKkYtNInq6nq4PZJVbxGAawk7Z5NcBZfVLkuHjiyPO82UK7vBSzfL20OkEAWszwen7pcHptVoCuBTR5MLTmgX6p+it73VJ0FhVVFoiiZpVQWoBY4mHdYtEl0xkHR5jDvsB5OOMmb1HTGyNQJtPUWAwLfClyYUUZY+hoa19/LZWQSMhvS01Pm1vkPOijotjm3ERmBe11nUJZAi1JxWpYXgwsRiVTB1EQ+0TlSBSmu/NMpE2P/QKKnp6QEFOWF6eg5CZlTIprIA0Ypob4UHbMoZdqBMsuHFsGUdjinE8751ugiH27f5xujS2y9eMjwaIdoUp5XSzneu+tuYkVWBHRjUVIr9hpD9l4a8tbJFtEfrhNPDDp1bSxPFGlbs//pBvNNS1GzzLYUX+5fY2pi5iaiESy4P1/nqweX6dbmfLD3iHFRoxUsKNCcpA2M1Tzpt2m9G7H9tSnjS4nzRi19e3VuSVuKw482KXYWbNUn7NSHpCZ00K/roN+gPeFJuk3c1278CBStu+5YTz+RUWtNYVaer9DyxW/fWN6HysR0Mr182EX5urxX2hY0b0fEgznDyzUmH56xvjZhOk7IFyFKW8zEbUO3MpJahlKwP2hjjGYxSIifhBR1S227IGwsO7G1ijAsCJQlTUMXrGIUVlkKqwi1ITOa3JQm/VEu3QULNOOMOCyoRxknUwdj5nO3L9lOSpZr1EyTHAbohWK+W6DnmmAcOFVh4AYFCXMqGuVxl2OeXpQPTEpbB51pP/7LA0adKoqGoYi082COoNjIMNPAncdA0U4WjOMEYx0cVpWh8ofL9/+ilKLdbrOzs+NVDVW4IMb4cBYMxHHsw1Hm87kvDZWJsai4Tk/dw6NqCZ+ULolCpdlsMpvNfFlv1ZtwdVmFDHIMss9VBZFMprXWvnRRJviiZEuShOl06vdzZ2eHbrd7psy1KAo/YZLjF1UE4Cdxq354MmmsQsJVkFAtQ63ufxVIVZVKMpGUY5bJpCj+BIg+ePCAw8NDdnd3OT099aDs9PSUdrtNFEV+MvzOO+/w8Y9/nGvXrvG1r33Nq0+azSZ37txhMBiwv7/PcDhkf3+fvb09Hj16dGZfDw8P+c53vsPBwQF3795lMBiwt7dHvV6n2+2SpilPnjzx567RaPDo0SOv6BRfslarxfr6OoPBgG636xO5pURcwKCc6yo4luve7XZ9OZyUVgpUiOOY9fV1DznE/xDwYEkgQtUjr/qa+CbO53N2d3e5f//+mWtdVZfJdV9Vi70f6BNwWFVZVdt5NUV6tT9I23g/JV4VAD6tb63Cvaft39PWU33gIP9XIe/qd1f77tPWX1XjrX5+9bifpp582muyT1UYb4zzhhMIDni/viogrKquqoCuur/V0ueq4rAKi6UtVdXa1WOVdlMNrRCVlqjqVq+LXPsqXK5el2rZrWyrCmGm0+mZ72VZRq1W836vQRDw7rvv0uv1eO655zg4OPDHI9uXfiMWFnL+Hjx4wNbWFi+99BJJkrC/v0+WZV5l3ul0uHjxoh8bHj16xM2bN/mrf/Wv+nOcJInvzwI9p9OpV+wJtPzIRz7CN7/5TebzOb1ez/u/ZlnG7u4u58+f58GDB141LPsvnrlra2tcv36dPM89qDs8PPRj/euvv87du3dJ05T79+/TbDbP+E9WVerSXkRBKeejVqvxhS98gXv37rG+vs7R0RHHx8dn7kFyzebzuVesr62teTV/nud0u10ajYZ/QCel4HL/FVgn16f6MKIKEKswujouyWfSNOXNN9/k1q1bRFFEu932DwFv3bp1pn1W72WikpTzUC2dluOrqqxlvwQIy3271WpxdHTEYDDwydnS/8bjMWtra2f64p+0fN8DRGUh7cXk7YhgVmDqoYcxeSMkPp1jowATBS7xVCt0ajCdMkknM+SNAELlMgqUJWuF3juxCofAQcNgUVAkQQkEBQQoiiT0ChcPE0rYUYULAnfcBuT/cvDywARf3hqNLMmpIcicusqlSkM0NejMlr54iiLWSPiFDcrAisAdk5prgtSQnibEZemmT/MNnDm+gEFJT01tQGI1J6bhVYm9YEqiMqbUyKwDEYnNSHRGTMHQJBgcKNzPuy5kwSuTZkxKAOD8EgvmhVNhZE2Y7Rof4pGNYv72rc9iQ4iDgv/6rU84D6sSlC2KkLt3ttiowWRW4/9778coapD2DEE7I7gfYRV858vXaE0sizUHtv6T179Abw5FbMnayk8CF6/3aJ2WScUK2u8G6Myy1pkyGCfouCCcKL8PJoS8s0xNzd3f94RTOF87JbVBCZM1ujBL8Wn15i0T0BJmgIN/+WZGELuSblM3ZO2A5NgBYFU4OKgKp66NJkulrSnBtg1cMJAP4FFOkWtCmG25Nrr1x5bBdUhrpRIxMqXRfymb8+mqlO3IOvBXlil7b0MBDtb1ORTu9epnpby5Cs6ltNm6zyrrzpX4OAKl4lZo4Nk/iqzCA0OU8sot318rSkBVOBjqy5EtZ8qVbQngTemZWAWRPtW5PBcSaGLL9Z0FmRV4aC32uxDiyjmQz8OZABZ/3kswacNyXwNFL5zS1hmZnfKd+XluTvYA6EVTajrnSnzE3ERMlINzUoJsrPYJy67kWPngEQk3AudZqJWhoRc+HKlAE+ASlzMbkujMw8K2nlGgiLBEKvevm1JVmBKUamRX0jG3Ic1y+wAxhvPhkIl76kJRehtuBBMmJvbeqYXVHBvXyZoqZW6jcrsFI1MH7SDkVjBkOx7xbPKYx/OX0MrwTOOQdjjnv3uxQ/zlOiZ0DxBsFBCkJaANXMlybrVPUtbKgdFn1o55/c/XWExi7Cwg6gfUThWzHeMUbEZBaFk0Cr786g2+On0OZZyXYe1QYwM46hhu2YuoAvJOsVTv5orWeyF7fzRh+Eyd0UXN9EZK1EixRpNPIuKO+4PrQm/ELI8YZHXaoVNI1oOMRpgS64LihSNORw0ubZ6SG829C+sO6KWaxXHdeZoCRdk3lS37tfRhH4ZUAn+D+92U/Tda3sOC1JJ8p074+SFxXGCNJowK5rku/QthPo3RgSXvlx4gGvKWJZwo0jygW58zXsQE2jJLQ5Sy5NYptsNSnWiq4FxrjFUEpfIwKwICbdDKEgWFf32RReS5C1hRQJEGWAu2UTC7aEgeRERDjc4UqlAUNUvesGXAidvPcKwxsUWn+L8F8no5drC8R+vUjblFYglmqgypkXHH9fFgqslDC62MZpRijULXivLcP2WM+OHyfb3IxHlnZ8erzsR3SjzqpKQtDENf3ip/uDcaDT/ZkDRO+X0V7siEtArGFouFB5JShlUtTXy/RSbk1d+rpX/VsteNjQ0/icnz3BvYD4dDH5Yg5YxVCCOTIlF3CFiUQBOBOhIMA5yZiMk5qipDZL9WDe9Xj7fqv1g1uZd9kAmZpGxKiVkYhpycnPDOO+94mKiUotPp+Im3/Ds5OeHb3/427XabGzdu8PrrrwMOooRhyCuvvEKr1WIwGBBFkQ+Gef31130S82w248mTJ7z99tu8+eabHB0d0e/3OTk54cUXX/QAQxQ9ogiSoJSjoyOSJKFer7O9vU0URYxGI4wxHB0dnVF1AWcUfnK+qgrYtbU1Tk9Pvbp2PB77skxRWo1GI5IkIUkSv42NjQ3fhuCsgmwV5r311ltsbm5y8eJFr7iUpTohrk7Qq+rE6vWuTuSrYKkKdVahmky+pW9JG6qWJlZVqk+DcdXJfnU78t2ngcSntdP3A+JPUxL+SWXU1e88bVn9fBXufi+YKK9Lyaeol3Z2drwKqtvtejVv1SOx6usHnFFVVV+vKkhXS4xXr1t1fBD4KA9mVmGklOdW/WcFsFSP+Wlq5uq/1etabWtV6ClgTvZnsViwWCx49OgRn/rUp7h79y79ft9vUyCXUsrDLAFLUtIahiEvvfQSV65cYTgcsr6+jlKK/f19vvrVr/ptjEYj9vf3/QMZUYOKZ670TUmbN8bQ6XR4+eWXefXVVwnD0KunRfEcRRFPnjzh4cOH9Pt9RqMR8/n8TMJxHMe88cYbnJ6eYoxLEW40GqytrTGZTHj77bfPqP7m87kfb6vne3W8lmtSHQ/kPJ2cnHDjxg1u3brF6ekpp6enfiyXByVJkhDHMWma+jG42WyyWCzo9XoMBgMPVuVhnqi8a7WaDzuR9a1CvmoYixyDlHDLAz3xL5TztbGxwcbGBkEQ+HMg10l8gGV90k+qD3xWVfhVsC/wXNqVqDblIZ0AxkajcWa7/7LL9z1AxMLgalQGj0A8tCR9QzgxHHw8onsrIqs78FMkimhkqQ2daq+QUmYFjaOceTtiuqlJTu1SLVhuQ4EDONY6rzYFOjWozKDSDIyhaJQ3SmvLABNV8Tl0nmbKOtCjK0EOqrBLEEkJJQv3mVwr4rElGju1o9WKrBU4s/1QUdQ00TDH1oKyZFb2ewkisAobOfi4c+mIye1tn9JstVNVrkUTRibxMGFuI9aDsUtvFc8yvfBgQFJQO3ruFE/KqZkMzgNxYmrMTURPTzkuWu67Snt4mNqACOfppQsIh5bZHhTTkLyuiA9Cxg82iRP45oMLBK+1UG1LMHMTuq/evUzjTkTWgPRJg3vvtWhFCr2AaztH3L95iaIGtSOn4FMG1CwgfqdVKtYga0E8dOeg1nfl7HnDpRbrzJ3LS51Tjo8uEdby0k8Q9ML5Ek7PGeqPnRLFhGBDNzn+W9/8SdTDhO0y6dNGISqpYcMAG4cuMKQElc6b0KLS3JU5A+FJBERgnBVi1nJ+hnmjTG0OSzgo4awWTDlpN9ESeCuDm9DKzwYwLnm6flzQfz5Ywr1ML70zSziorEtJtqFZqgTBqQYlxbmiUHJ+cdYrW7HKqWZtRb0j6xd4WK6vOo/2gK7iXehUh67vqDNQr0xqLr0TXZ9Sfj2qWIayAA4kwrKkudLv5FxSvu8DTqrWAuX6z4DOqkqyBIRnfBorm/BKRc5CRh+6Ys5+1l3UZfl0I0iJsBQo3hif47Wjc4RBQbc256XeI9p6Rr9osM7Y+QaWimFnMODKjts6RVvjlYnSP3UJ/ufeszAixqUoi1JRl++LZ2HfNGiqlEgVPMzX6Og5kcr9wwWNYTccYaziYdFlQ0/8eJKojGPTICjhY6QKMuMApagT5zby/qmSxCz7dpS1mccuSKWnp7T1vFQ+W+6mm2hlGRcJ3WBGLcn46x/5H/nb7/5pOrfdg6Oz7c2pDrPC+erN8ohu85RBljDOamy0J/S2jvjOvT1qu2PWGjNmx13Ucc15guLG7GCq/QMcvVAUdYsNoWgYB/AE3FG2r9D9n3VjFj3N7FxB1EjR2oIuCLoFjSQlDgt6yYyN2oRuNGOQ1QmVIbeaRpgyzWN2m+48n0zrXO2doPcsw3nC8XELBhE6UxTtMsSIch+0PZumDK4f53bZx0WNrNyDq6Kmma9rZs/PORen9McNLBBFOVkcorUhm0WESc6FrVMesEaRBgRxge1AOg1hEfHM+hFRUKcW5CySkEkaEwcF8zgjCXNa8YLTeZ3JIqYW5iwWCSfTOu1kQZqHTOYxncacJMoYzhPS3BXaT/r1ylilCcaaomUgV6hWjn55SpGG6Nea2ADCiXIeloFrByqXIUu5EKqOC6OiBIWS4q3L4Byse4BkAwjmy3t7sAA1d/0m7AfkoeF0XsfOA4J2Vgl4++Hyg7IYYzg5OeH111/3CrA0TcmyjM3NTWCphpCyNMBPVGQyXS2vlclGFZZUPydLdTIPnJkcVhU8q0oeWAIGmdBVJ+mrijGBfPv7+34CJab9MpGRSRcsS8iqk3PZ9traGu122xu5y75I2bVM0qr7LBPJ1ZLY1Qm/KIyq5d4CImSfqpNVeV8mwDJZDoKA9957z3sOjsdjLl68iLXWq/qstXQ6HQ8aa7WaT4YOw5D5fE69XueNN95gY2ODxWLB5uYmb731Fqenp7z11ltn0oKn0ynvvvvuGRWXlLpJCZ4oZsCpdNrttleVyPVYLBbcunWLGzdu8Oabb/Ltb3/bf17a13Q6pdvtsra2dqZdSNKnnOsrV64AePiwvr7uJ+jAGQhUPb+iLhQFkJxrObdBEPhU2GazeQYKVa+R7EdVmVgFdtK2pU1WgVB1Ui+vVwFGFVzJa1WV3Gq7rfabqnLtaa9V+9yqmm0VKq4Cu1VY+H7bW1XgPQ0GrsLEp8HD1X14P4go7yVJ4gFhq9Wi3W7z4MEDX8a8ubnJgwcPfDiDWBlUFXxVQFxd92qISrUPS5uoAiQZFwQ2CQypHnv12q6C6eo6q+dytdR/tRRerklViVktbc2yzCt+5XPT6ZSbN29y4cIFPve5z/Hbv/3bZ4JFZKmO3RJuZIzh4cOHHB0d0Wg4yxlR1B0fH3sIJqrIqi+s2GtYaxmNRmfgk7T39fV1Ll26xGg04sqVK0wmE+7evcvp6Sla6zPntWqFIOdCALIxxpdIW2uZTCbEcUy32/V9UsbHqvKuep+S6yVq8WoSvCigwcG9vb09P9bI+qoPrIqi8PYPAnJFSd3pdPz4JMrEIAjodrtorb3XIrh7oPi/VtucbEf+F5gYBIFXUcq2BSovFgvv8bu9vc3x8TFPnjwhCALW1tb8MdRqNbIs8+rWWq3mAaC0k+r9T87lbDaj1WoxHo99wIqE7LTbbb/vrVbLt9nqvftPWr7//4S1TpGlZ051FU1cQ067gfsDvpxI1IaWtCw1KmKnLpI/4GsD540IziMpbSuv0NNl+IQrN7LoeUEwTdFZ4bzkLBCFkOVE/QVqM3QBDhXZovc1Kyce1d8l+EVny32QRYIgikj5UioHG/FljZLwLIvOSyipS+87Uedb93thNDpdKhB17iY+bT33ap+Ygq1gyMjUSW1wZp/mNqJvGvT01IMAAYbOUy0m0QIcU6bWEfuIAq0MPTXl2DTJbEgBTLMYqyE5NZhOAWUJeDBXJEeWtKcwd5u0Di2zqxnqQYwFktcahBN3DI0HAdEEZjtgYsuiCKmdwqIL0WQJm8KxJpjBoudgYTgpz0vkgGHWdOdZpzDfBFDcaB/yanCBbBwTNtx5rx+5iWbzviZYOEBc6zvgowto/XHdla/FFpUVEAZYXEdXWcH210bkrZiipkk7ZTLcZAbGkrUUyQE++wbr0qKnu1UPPqec1Bne7zBeuEAXneHhdzC3mMgSzhx4lMluUSvhd1Fe2xJiKFMGpygcFLF2qUpSOJppSnVhoZYlzHmpvLE46ABLlaJIAuV7ArcrxFAZRZDiLRTP9Bnkdbt8HZzqMKyEoyjQZRm0NFnxLwTXV3TmFLY2XJZIVxdRHgqE98q/yjZU2ZcEIkg5Ooozqc9+P0uYKOBQfBNtefqqymQfrLSiXJTXi3rAXtQnQ9ErQ08CbSiMZpzW6Gfuj4mJqXE73eal5D7gSpibKi3VhIqHeccHrIiqOFIFCS7gpF80nC+hDWlWwlXcZxecFC0fbiIlzodFm1FRZzccYKxmUkK/pl4wKj0Kt4IRiSoYlcpCrQxtPfeqxZFJvEdigOWddBeAK/Gh8220kVdQrgdjMhswNTXORafEFIxKuHguPuUb48t8oPWQcZGwHo55lPVYD8fMd3O6tzQohV7k6CLGRG4cHc1diu9aPOXxpMPxoolBsZlMGCwS+vM6OjRcXT/BWMUjus4jUdqABZOU11NAegkMxVdULbQLPKkXsNC+r5lQkbWAdk63NUcpFxLSrKWs153n5CyPoAazIiJUhknuzmszTImiOceLJhfafe701zmYtmnFC3r1GUemDbF1IsLj0IUwBWX/KsGZ84N1fTicavRCkXWMh2QmwiUXn7o+mDdge2tIoAxhWJDEGb3GjHOdIeO0xknUYL015WLrlFgXREHBLI9Y5CGb5yfcG/Q4mTe50OozL9yNeKs7LhOwl96MV1onvDPcohGmLtleWdrxglAVmLYmt5pbh5sUhaLdnDOaJDS6M2bjGowiaGeYIoLYoCYBNtXMTIIKDfn5gmCsSU4Urfcs0cSgi9IrOXaBaEHqVP7R1PhxpKgpTKAwoVOFB5klTxR5DeKxIRo7P1arQaWuL+e9gqS7oD+qu4czd+uudPppKuUfLt/Xi/yBLk/9pZRzPp9zdHTE4eEhaZp6ZWHVY2mxWPgSYQGNos6rTnplgl2r1c4oC2QSKuWpVRXOKjR82lIFlDI5lKUKcmT7VTWRbFdKm2VbMpGtwhRZb71eP1MGLYv4Q1XDPGSytqoek2VV9VVVLVXVa1UIJe8LzJL9Ey+otbU16vU6/X7fT77a7TYHBwdMJhMuXbrE8fExjx494uDggPl8zmAw4I033uC9995jb2+PBw8eEIahD0uRayOqGQmgkMmstAGZzIs65/z58z6gRODM/fv3PViQczWZTEjTlH6/z3A4xBjDP/yH/5A0Tbl69ao/B3LdJMTk+vXrjMdjHj9+7CeSzWaTk5MT7ty54yfQcs4EKMg1l5K56oQa8Kod8QWVdO/JZOKv+aqCbBXsVRWF1etWBYZVEFKdBK8qy6rl7NW2Xe0jVVC1qhpcBXXVZRWKyedWQdbTPlNtw9W+8jQwubrdKghfVRB+LyXhKmyU8/c0td/TzoNYMgiwlvL24XDo/flqtZpXuwlYarVavny2GuRTFAXNZpONjQ0f0PPw4UMODw99m5Vty7WvesRWHzjI+FRVJa4CWXlP/q9eq1UwJio6pZQv5a0CLYFGoiqXbVfDp8Q7ULZ99+5dXnzxRdrttvcvFbWa3EPkHIJTp4uaWKml953WmoODAw/ipe1WU7ClTY9GozO+f1VlttxrqsclgSRyjsRzUbZdq9X8NqTfyXmTPi7nT66/eKbKAyS5d1XbdvV6yD8ZR0VZL9uYzWY0m00ePnzoIbX4SlbPvXhFCoCUe7SUB7fbber1uvcmlDG/WqIu0FbamYBEGTOlbYqaUM6BBJ6JOl+u4dbWFhsbG/5vArnnVLcjvouyzU6nc6asWu71qx7KMg6L1+7p6al/ACUQdTKZsLa2RqPRYDgcftc48b2W73+AWAJBnVvCuSVrKvJCEWTWK/KcKb2lqOszycPhzJZqLvfHfp5ooqmliBVZo4SPFh+GogrQ8xyV5lD6s9lAe7hhg6UK6gwoVHgFov8fLw50k0+5QZY+bKLCkn11/5ffLyxB7ny7UE6pFU4KinrpoRiW4DAoN1Km6qJc2nJaTnYFruYJtIO5P6VDk7hgFJQHCOI9FlEQqdyXMzrQEKCVM60Xf7ORqaGVoaikL89NRBK4UIWs3HhmnO9WOLPUO3Nmp3XyxPkABqnbx3DsIFTSXaDyyJWbz9w1zVruHIVTS9a1mE7Oo5MO7dT5ROrcgcEicUqTtOcUeAJPlSlTlBOY7RXUjgPCadlmcvjVL36K9q3Al5OLMs1UwjOUWUKfInaAzr0Oap6h5iWEMRYWKeHJgFBriELU8zuk7WWISOPQMN107VDK3OOhm8CHM1fqXT82zDYU9WPLbF0RjywmUuQJNI4MowsBrYcFo0sB0djtb9ZU3iOyfdcSTnKsjiqgjxIeViYY8qPiLEgMrPMzlN/lnzTqMvXb+ylaXBk05f8CHr1az1bCRvCpy2f8waxdsshASF65brEFUMvNnylxLq/RmRJlBSbSS5ViuT5RJooHolwXWwGCZ5Yq9MytZ6Oq8r6q/uElf5hV1M0+kRmB0AYTaK9WtoFAHNcfM6tJVMFObcjtaIO0CPxuCbRzcM+Bvf9h+CJHaYtY56QmJDMBkS4IleHm6Q6P3tly224U/LkPvc6f6r1OUy+IlEs+BkrlsAtdGRaJ91d028yZm5iNcMyoDFGSPh9g6ZsGbT0jxpCVYHJelh9nNvBqxUjltPWciXVgLNEZ3xhf5ihvsxmOuBgd09Fz2jplbgPagQtvORedkhKU6dDuD5mDRZvteMRB2ubZ5DF3Z5ustyb+eunMlFB56WlZGM0iDzGxcgq3PGYjmTAvQp5fO+DVg3NobUjERwEHv62A9QoPUsa1c1UogommqFvnO5opglRhw3IylrrEdAn90nFBLcyZZS5gJgoKQlWQlk+7JnkMIYRlA+qnzu+vHmTMi5B5HnGld8Jbh9sUTcX51oCdnQFPHvdY/1rkxpUATKD9uTAy1/IPs0BZ5w+pc4MykLY1JlAk/YK86ewZDo/bLDohs1lMs7FgMEto1VLSIiAKC0bzGidJ05/DOM4xkWaWR1xbO+abdy9y/9u7RBcmfOHKuw485zH1wKVzpyYgVAVZETBMExph6uFhEuTUg4yH0y5ZFhAEhsmsBlaRLiJsoVEKwlqOOYwxxoV0dV6LCebOvqKIYbFhmJ6zJEfu/GexJmto9zDJQp4oTOS0u+HcBbSJTUTWLv/QDfCWI/ONAGUCrI5Iu5ZwXNpeTDSLtEG4Nad3fsh4tEbjsXYPMZeCgx8uPwBLdeImpWFRFHnwIwoNCeOYTqcerMhERH4WAClJp9UJsEyoqgnJMtGEZQne0+DB06CE/C4TYZmMyHoEAq760lXVkLLvMokSL7EqyIOlH151wilwQEqpDg4O/IRzFfasqpWkpGtV3SW+kgKeRAkiE2bxwFtVholKRMBmo9Hwij+ZhF++fNmDxul0yng89gCt3+8znU65cuUKBwcHPPPMM5yennq/QFiqTrvdLkXhkrWrcETUq6JeKoqCo6Mjtrbc/Vr8rOQ9Ca0QUCKhJBJk02g0/OS7CqRPT0957bXXfOm4JKBKOxYFk9baK3QELkiAikBPAUmStjqbzajVagyHQzqdjr+eUkLY6XRIkoS9vT1fOlhVwFbbxtNK657WvqoQcRVMrqoNVxfpf9U2tNpnqp+t9plVeCf/vx+EWy2zfpoC8Gn797Tf3+/BwCoIrP78tGOr9oHV8ujVY5XxpgrTpK8K7BPlmByflNb3er0zbVD2J4oiPv/5z/PJT36SXq9Hnue8+eab/PIv/zK3b98+o+SU5WkKQbElqHrQynhUVSlW28TTHkhUlXCiapPxQ4COqPnSNPWq5SqsFlgXRRFra2selkmJLCyTlqvqSknaXQWUokaz1imgpY8CHvqfO3fOAyhZREUn10ksNGR7AgIF9MoiimgZJ+WaCYCrAkp5XwCW3EeCIKDRaPhjbzQavPDCC2RZxr1793xpeZIkbG5unoGV1TY6GAwYj8dsbW35ccYYw97enrfRkP3t9Xo+eERKuuUa9fv9M2nExhi2trbY3t5md3eXOI65ffs29+/fP6P8l3Mj13k4HPpQL3Awc29vz0Pa6lhTvdfJeb98+TKbm5seQmZZ5u97UlotKm9Jq47jmHq9zsWLF9HaJSfLtgaDgb8XyvgdhqEPS3nzzTdptVq+b2mt2dzcJE1TNjc3uXv3LuPx+KljydOW73+AyFnmEaQOBunCTRCKWBEswAau7DOYWTdhKlVGRUJZeqi8Ei4MoIiCymRwuX5bC7AmJG/XSHsxqrCEwxK+rZQtKkOlzFSSXUu1RwXOBKlFLQrnyyXAUUupZvl/6ZEngSku+MHF6+Z1p5RQRhReFYUUgBZwA1FQnDkeCXXJbIAuA06c95mDgyjoF00yG7AejF3Qig18WeGciKZekNqAE9MiIeNWus1R1mZhQp5JDojDAihItEthDUqvRYDpIi496aDTmLO433KnL8CnSccjpxoMAsN8Q1EbuGs834Zo5CZueV1h6jlhkqMUzHYUiw3D4mMzisd1TN2gZ5rk8ojilS7ggFuQOcWhzqG+H3i1Xu3Egdi1153npNuGU5llCX6yLaXqolozkfNEtBrikUFlpQRUazDlTLEWO+hcCDR275tmnem2dmqfBaQ9Re3E+Tcmx5bZpqLWt4z3NEFqGZ/T5WRWkXag1rcMLwfUTi3DKwFBqcrNE4jGlumuYvsbOY33hphWvFROUQKPqGwY4oOGWnohFiUVCw1k2rUnZb2Xm42cusqrDmODClwYDNkSGloJZgCwyvsrqtx6CIgqt1VCOw8NAZW5UtynpSuLj2LVDsAlKttliEoliVn61Jm+my+Bou+jeQV4Vv+v9DEJUxHPNp+wXrYLH6BSAa2+5FnWJT+qapmze08XhiIO2QqHNFTByIbsxQNi7QBLFBRsxSMSlbMVDpmnG4xMwnHR4tZ4k4/37hEow9TELjHdKgo0Dx+sc+U3C4KFC5D64+c/Sv1/l/Knu69hrKYXTF2ps3FAb24iD+kyGzIyDhImupxsWk1W3laiUv4sQSnHZpnknpoakSrKMJWA7WBEpArmNvSffyY64OLaMY+yNR5la7w93yXA8GPtm/SCKVdrh5zkLTpqwUHRIlCGSOVoDD+x/ib/2VtfAODGjQNeaD7iKG+jZ7oMJzHYUJMnpX/WAgb7TbrXZpwuGmw0nOrwhe4+uQ04WjRRypLPIx6Ou+5Y5yHKwtq3QuKRPXPNXVvWS7hkVPnP+t/lAYYqivIhGGhtKKwiL9XBR+Mm/TIUpNeYoctQkcxEaGVpRe6PoIUJ2a6P6KcNnkzbfGBnn/ujHo/GXQqjee7qY96eX0BPnbefjQqCmSa6NCGOcsKgYLMx9SXDWhsmt7tEo4DF+RSdpJj58p5I5u5Ns0VEo7FgeNBCZZp+Xo4jkYXQ8G4acm3rGGMVjTDFoOjWZrxzvIUZRdAqiF5p898/+RDh5pzPXb1FqAuOF00GaZ3LjROSMGOaxS4V2ypOsgbbjRFaWfd6rjFpQFjLKYryep6ErnT8UQOdK1To0uqTE0NyWmAiRTC3nLwQMd1zD6KyVsBsy/nKKgO1U2dpsdiwFFfmmEKhjmNMvSydzFzJulQohDNQuaJo2FJB7x7uqAKmO5qsbSmKOqNtDRfm2KP6WQ/VHy4/MEt1si6TKlhOKGXCKJOTqupQknil5Kxq+i8+VeC8FgXctFotP7GUSVvViL86Oa6q8eT3KkyQSf9qwqRMpquTY4E7Ai6f5gElQKIKhgQ2yiS3Wo4sE3zxiJIJuXwPOFMG9jT1VRRFbGxs8NJLL3HhwgV2d3dpNpvcv3+f3//93+fWrVtnvADlWEQ9CfhgAikZE1gmQSQvvvgiJycnKOVKMzc3N2m1WgyHQ/r9Pu12208EBUptb29Tq9V48uQJrVaLc+fO0W63MWaZYirgqyiKM+Vnt2/fJgxD7t2759tBVfEi3mlVACZ+VzL5rbavavlnNVxgY2PDQ9zz58/z8OFD3y6k7chkXOCFAIA4jhkMBjSbTZ48eYJSyp8jmezLtur1OvV6nStXrrC/v+/LBVfhmwCb1fYk78vxyb+qSlH+F6VQ1TtM1iET+ioIWbUHWP3OqnpwdX1PA/RPA4yrULG6jtV+ubq8H2h8P5C5CjNX4WL1s6vbqX5fXhN/VikZFYgogFz6rfTP+XzuVXyz2YwrV658F/gF5x37K7/yK9y+fZutrS1+5md+hj/7Z/8sv/zLv+wDIVbDNqrXGpbwRH5+2jWT45D3q5BZxujq2DmdTrHWJQxLArEoJlutlu+TWmvq9bqHegLWt7e3vZrtwoUL7O/v+3Ji2ZckSVhfXydNUx4/fkwQBFy7do1Hjx75fZHgC+lzEp4ShiGDwYDNzU22trb8OZB+KvYBotirKoZlabXc/FtCpKqK6GpJtsCwVqvllZJyTarQVQCYqBsFJC4WCwaDAR/72Mf46Ec/6h+2nZyc+HFWHi6IEn+xWHDu3DmePHlCo9HwYFJUl3fv3uXevXvMZjPCMGQymTCZTEiShCAIPEyUMU78G2ezGb1ej3PnzjEej4miiPl8zkc/+lFeeukl3nzzTW7fvu3PUxzH/sFddXyptvXqPUm+Y631bUigoFKK2WzGt771LebzOZubm5w/fx6lFP1+3wNBCaKRc3z37l1u377t1ZK1Ws231+l0ymg08uXm8sAmjuMz6le5l4lXKbjArKOjo6eOK09bfiAAoi6N8E0E1ATqOKWZshDOHRCIRpZoapltlk9YwiX0kWRiZS15LfCqKlExus+7oJSsFWEiTTAviIYppBkqL7xXGaXy0ISqAkVkInlWxCRlpYSSgitwpNyfsm+bmnbg0OLUUMqlEQep9d8x0RJIWFEfylJCGGvdd7zfmnW+TtU01O3AEegUTYxhZOoudAFXMlhYTaAMBc5bLVEZby72+M9/9c8S993EqzY0THY0f+EXf9cpi6IZcxPRDBxoaKhy4puF1FII5obBpI5pFsx2oXasSXt4I/qiXorlEos6cYqddK0gHIfYqLyGswDbzrBGMd8uqB0GmHGDZKJQRpM3LYvbHRqlsrH1wFIklJ6UoMYOMBW10kvQuHOsSs9LE5dqknnZ3mJ3nYLU+pJjB6vx1880EvRo4hSmpeoIY1wbCTQmcmBr8uI2J89H/njzhlNZ5nWnUMqayqksm4pgYSkS5Qz+Sxhe6ztAFU4saUcRTspk4jLB2cSK3a/MCCYZth5hpL1ZV358xtD/DJG33huN0CzDHwrlSp8jg8hobWSXZcpGOXgosFBKPXPlfRBVJYRF567fKBykVUXZ1ktQKP2oCgBdB3LwX2fGv2e180Wsfl6sAiicElHWX54CDw2rielnllV4WOlX8jBi6b3IUukmfo4ol9IeqNLvUD57Vp2orF32YYGaFU/Itp4TKJibgJrOSE3pTagsUxMTKcNuMGQU1NnPu5zkLT6/8Q7/ztodAI6KCW0dU1MR9/Ixvxp+zAVCFRYVKGoDy2+8+0F+6qPfphc4v8K2Tgmwy0CU0luxqRc0SoVyZAsHFhXet7ChF2e8DAurPXyc2hqxLUhtQKAMx6bpxyBJfJb1XIyO2Q0HJI2Me/k6fzB+3pVc64xBXqfdypjbOX3T4DDvYNBciQ7505dv8urpeX71wUd5rnfAj3bfWULzwoJSzNe1Vyg37oecPNkleHnAenPKybDJfruDVpZbpxtM5zXO7Z7y8L1NVL1AhYZz10941O5hJ6GD4ZF18FxbgrggH0dgFLqZYa1ChwZrFGYWgrYubdgq9GGM2Vqw2Z4xXcSkWYi1YApNHhqUsszzkGbgINzjaZvRosaFdp9amNMMUoZ5jVa4gDqcLhqsJTPevOfKwE9Om2xeOSEKCuKgYKcx4sGox8Fpm0YrpRFldGszzjUHPJp0GaY1FuenLE4SN74kGSbOWWtP0cry+EkPgDwPMIVTkdukcP06lj+eYHFU5+bwHBSKxuaUXnNGoCz9Rx1654b8xIW3+Y32S3CY0PofG/zu4EXOXTlipzFirTalQLMoQkw5Ng0WCXHg2lBqXIiKUhYVWYxVLgn6MCGcK/KWxbQKTGRgEWC6BcdhzMarAdHUkrYVwcyy8xVDrZ+zWAtp7iuKyBIuLOHEjXePfjTEPqkRLpwnYjF3wSs6Kz0QgXgq9wJ3rwrmTlWfNdyDy+SkfBh4qshGCXmzTH7/IUD8gVtERSWASyYWoo4AN8GYzWYkSeJLUEVdUJ3IiypEJrVwdkIvKhuZzPZ6PeI45vDw0E90VhVST1NDyfbkdYGb1bLB6uRbXquW+4nyp/p+dT1Pg4kCdapBL6JEEzXbtWvXaDQaHB8fe/gkAEvWVVUgKuXCPn7qp36KL3zhC76cdz6f87GPfYx+v8/BwYGfbFWhT6PROFPyWBQFb775pgcIgC8hlH2QCa+UhFX9suI45tq1a2dKNUUVKaqkoig4PT31CsAwDBmNRkynU39dq4E0AgIEysnxVif7sshkX1Rgg8HgDCxZbQ+iUMrznJ2dHR4+fMjNmzdJ09Sfq9VSULkGwJmk0NX2UQ3KWV9f5/Lly1hrefXVV5nP53zkIx8hy7IzMKkKuqvwehV+PQ36VNOVV9tIFRxWy+SrZbHfq49Uf6+u82kKtiogqr5X/f7q8r3ef9p+rX5vdX+rn38atHu/z1XBYvU4q/1XPEDl4Yj4h1aBrMA0YwzD4ZBLly55r7e9vT0fKnLjxg22t7dJ05R79+7x4MEDNjY2+NSnPsWlS5fO+PtVxypRPYqyq1r6Lf+vqlBlqUJ7+VnWWa/XzwAia5cJyZIaPB6P/fgnFgebm5s8fvzYnw9rLScnJx4cLhYLtre3vfpY1GAC0eM45uHDh3S7XW7cuMHOzg6PHj3y4Gk4HNJoNDg9PfVQUCCulOL2ej1/fqRN53nO4eEhw+HQ++jJ9RFVfLvdRimnTJ5MJhwdHXnALgpzaR/1et1baIi6UoCV2HbkeX7mc3JtTk5O+OIXv+ivQafTYTweMxgM/EONer3ur8dwOOTixYvkec7x8bFvA6JqPT099QpDuWZViwQZL09OTvzDC3l4o7ULltnY2ODg4IBr1655Rfm/8q/8K9RqNd544w2vNpR7vIzXgA9JqVqNdLtdXxZ9enpKt9v1/UCupYTLPHjwgNlsxttvv82FCxd8/xG1vpSNnz9/nlarxb179848LJT7i4yD1SCsdruNtU4pW6/XybKM4XDofW43Nze9+vF7PbRYXX4gAKIovkqrLVcWVAKgILUuKXdmCecQDw1Zw/kYSagKwKKrCOeWtB240BW9hAgODlAq/wxB7hQm8et33fv1OjZNCaYNVFEHSYOtKKikVFkVVN7Hg0qsK0N1E4qKmkVDNF36MolCqyjLMaNxjgkURRKQtrRPVgaWULUspQPIjV4qI407pqKm2A0HvD6/yH/+1mfJc81PXn0bcH5b9SDjRzrvMjfOn6yhF0xNjUawAOUURad5k/XvGFr3phRJSDDNyBpN2sGc//bww/zCzlfZCl19vYAEA2RpSHNmmexGqFdj6jioGk45oxRVGdhvdmmcun0Pp9B5JwTjAHE4hdqpYrHeKFWLFoybCKYtB6XCmQA1B9q8V6A5W8IqHpHeF3PhSoeDhYNg0dSS1xWqBLriI4l1bVHn1pVGBizLV7WGvFKrJk8m+hmnzycs1hTh1Kkc46GDftHEKWOisfvdlRS66xWVydK1E8uip0iOIe04OBzMHYQK5u573fdymnfHZcpx+YSuFpA3Zd+kHZYNVcCflCCDA4C5XgYuVMcYW64jsO57Epgi78m6UU/9vspKgGtL4Cc3/hIeSptf5ZvLfuWgYDXsZFWJ6MuUV0ujK+vx65OSZa081DyzyPfLPu3DWLRrE8v1rvzxWTn/yi7BYjVUxX+2kuaMcus1IaWHYEBT5TT1glAb0iJgkYc8mPaYrrl04rT0B3yQrvFvrP0xkHArG/MXvvVX+TOXvsN/sPMabaXprY/JkzXXto1F55bFiYOPoj4UT8KpqfnwJI1xqc4YEpUzpUZq8SrA9WDMYdGmsJpCuQcOfdMgVoUvi35o1nh3vkOiM1rBnMwGPvTkJG/SCBYsTEQrmHOQOpD3+dab/Fz3GxwWbd5Z7PKlww/QDWc8lzxmNxhwkLd5e7ZL0si4Wjvkd6fPEmjDNw7O81Z/u0xSt8y3amQNRf3Y0DiwTLcC0o5i4zs5j+sdHrRaJI8DXjm8TuvCkPHdLjY2PMkCCC3N7ox6nDGa19jYGBNsGZLQTVACbQiVA1tv3dmD1MH0RnuB1obpJAFlUYGltzahHmc8ibo06g7kZXlAHLl1FUaz3pzSihcMFgmzIsKguNQ85avDS7w2PEetlrPZmtCN53TiGTdaB9wab3Fv0OPcTp8nJx1MoTj5zibJM0PyXHPnzjaN9SnmYZ1hnBO1DN85cLBxNonp9Sa0GnMWUUGWBcwOGjR3JxwPmmST2Lf9YhISrM9Bw/r20NlYaEMS5aR5yCIP6NXn3L+zyexxi1krQR9FfOLT73C6aHB3us7PP/cady+u8+BLN7j6Tw13/3KXq8+cEOmCk7TBeFFjnoXM45D+qE49ydhtDl0KdZgR1XIWpwlhPyScunEyWEDzIcy2Ioq6KyWOhyE6s8QT48ZI68bSrKmpH4nC342jxcI9qIlPU4LUtdfGviLtuntGOFXo1CVtpxuFUyLmivihovlAYWLIWoq069SIZqpoPnI2K1hLnmjm68u/WX64/OAsAu5EASJQRMq+RAEGeFWGlLNJQijgPcRk4iMKlyrsiaLITwJE/Xd8fOwnVjJJeT9V0ftBEvm/GjQhk2pRHsk+C/wUhYco9kTNIuBJJo4CtASkCZyCpfpRPKB++qd/mg9+8IPs7e2dKVV87bXX+M3f/E3u3r3rJ2yisAA3KXv22Wc5OTnhl37plzg6OkJrzd/8m3/Te26tr6/78jk5J41GgzAM6ff7zGYzvvGNb/jjFiWMXKP33nuPbrfLkydPGI1GXp0kqpvpdMqtW7fY2NigKAoODg680kZKAu/eveshRLvd9qnOUl5Xvc6imBRQKedUJoTyeZk8yiS66j0mk/IqzJQ2KIqVVqvFyckJN2/e9Inhcm2qbaPaJqqlpQLrqpCvmsLbbre5cOECg8GAe/fukaapn9DLvlTh2yrAWm2/1XZRLQuuqg2r66l+t6q+qrb5VY/M1c+sKgufto+rn13d/qrH3veCfavrXX39aaDyaZ9/P/i5+vnVpaosq74m/VfadbUsdrFYeAWuJOJW/TKfPHnCZz7zGfb29siyzCeOK6XY3t72oRlPnjzxIVSratOqSvRp+1cdc1ZVl5K0K+NrmqZ0Oh1fki+eeavnTz5vraXZbNJutxkOh4RhyNbWFsfHx+zs7KC15vj42LfFRqPB1tYWSinu3bvHYDCgXq971Zz43UrAhXj4vfbaazzzzDP+YdP29javv/66H4vkAYYxhvF4zHA45PDw8Aw0FD/Gzc1Nbw9RLUWW87i+vu5tNUQRLX1ZIGkcx14ZKEAK3MONJElot9usr68DDnSenJwwnU5pNBq+LFuUdKenp9TrdabTKScnJ2xubtJut0mShLt3755RdgdBwP37931avUAzUVSu3qtqtZovma4Gtsj3kyTh8PDwDAS8efMmcRxzdHTEv/qv/qs8fvyYx48fc/HiRd59911vQ1KF0nme0+l0vAJba+23LT6ekp4tSeVSQixAVwCwPHwZDofkec5gMPD3k1ar5W0tNjY2uHbtmrfKGAwGHmoL2JY2J/ei0WgE4NuL/G3Q7/fPhH19rwcUq8v3P0BUDuw4lZjGxKUKIHbJiBw7wGMiBylMrKgNLUW0hCPBwlLUAnTqAksc+HHv+XCGkn/Et/axxmAubmMXKXpzneL+Q1QYUlw/5+ClqI8Kp4S0pUcblMq2ZRCmW20JJn14g4jDtPIl0OE0Y7FeI5rkmFCTtULyRLHoRSw6TkWTtfEwSNbhFVLWQa5pGjk2VAaxqMIlhL6d7vL//u0/Q+eWZufmgrfnL6AKg55lHHyqh/63DBvRhJcb94hVQd8GvDq/xLvTbVIT8nZ/i3BqnA+kVpg4oKgppibmeN4k0SkxhVcyxhQUFvJF4FWhQerKiZ260k2ugrkrMw9SPOwt6i5tOUgdpAtnlL5erlQ3ayrmG8qFpFSbSu5UXsHcTRJRy1JVlbtz53/P8GmagPeijMYO9HoPSevaGtZBy+m2ZnoeiqZl+0saNZ2X3odzzHCE3ljHTqeQ5aQfvc7RSwkmhnDijjkal9Bw5MBgPHKKQtdGyyeHuZuc1k7ce7WBg4nRpEwpZukpGI0ti25AvJYQjjPnBRgFZO3AwTxw6sOyPeLVc+psWIphGYCirAuPAEh1BYKzBIcGBx3LRGdfAl0qElWpRLShg9uitK3CwmrSMgLSKuOah36iUsytB3kCHnVul9YAUJY0l4A4rADHQDlVWvkZ/38JE6uqwu8iivKgoNJegDKFXY5BTp1dhqr4cJbKk2lrS8+1pU+fQEoTKublgLClF2wEYwJlCJRTXw3SOreybV6qPWQjHPPeZJtZEXGuVGz9/f6nGN7qMTznbvgdnfAL117h77z402y8QZlWbtFTzdzGjExSliQHXh04sa4Euh3MHBxEe/sDgyZRGTEFh0Wb/azHyCQ0dMrUxD71ua3nRKrgKG9za7rJpfppuQ7Ls8ljMhtyrfaEiILtYMyxaTCJa7y12OO3Bi8DcKP+hE/Xb3G03ebtyQ4HaZvMBny28zbP1x/xawcf45t3LmLmIRcvHRFqw/G4gSltEYKFIUhhvhagc0URKyaXDPXDABNbwqEbj+qPAoo9zeUXH3Pn7ha9zpTaek6oDcN5jUAb4jAnCXNqQc4sj4i0O9/jtEZwGlK0CnRoSdMAU8QUowiVK6K+pq+b9C3Qj5lEEelaSJ4FJGUS83pzSi+Z0QoXPBm3uHm0Qy3K6dbmLOYR+UlCkSnuFx0e5Irg6pgrmyfEQcGF7oD+vE6rOadRSxm35wz322xe6NNIUobjOkW3YHGnDR+cEoc5w3EdjmucDmKirRm1WsZWd8a0vmBwe43o3ITz5084HjVZzCIIXYhKqi3DUYN6Y0GkLYXRjKY12o0Fm/Uxtes5jwcdJid1iobhW/cvsNEbczRuMkoTbr9ykfODnKMPxlzafcjhrMVaMmWauwTmo8dd9NYYazS1KOdg2maaRfSHDbJpBApMZJmfywlGAfFAUySKxr4lmriHiLpUdy/aAbRdWJpYi0x3amAd4AsWlslewNEHQ4JFSOe2JZyVJZZ5wGLdPeyZXCgnqzNNONY09i3N/YLxXkBRc/eUxr57ABTOLUFqy3uUKS1V1NkHMT9cvu8XmWg1Gg2stX5iaIzxpUISmCGL+M7t7e0BeI8qUanNZjPvZyfbkJ87nQ6dTof9/X2CIODJkyd+glYta6pChaoqDJYTbfmMAKuqZ1MVqMi+CbSUCagoKS9cuODBgpSJrZYDVqHPeDz2Pmqw9OoSVcSXvvQlAM6fP+9L9T7wgQ9wcHBArVbj4OCAKIp4/vnn+cxnPsP58+c97Pra177G6empDxMRwDCbzdjd3T2jxgP8MYmf4J07d/x+yTHIcT548MCrjMQbS8IeZEI+nU7Z39/3CsU8z73XISwBskCLVYWhXJuqH6NcI7l24s8oMOHcuXO88MILjEYj7t+/z4//+I/z4osvMpvN+MpXvsLXv/71M0pRud6XL1/m0qVLPH78mAcPHvh2KPtShXOyiOJIFI5Vz7mqF57WmrW1NVqtFjs7O1y6dIk33njjjOebeJNVr5Nsr1pqWi2rryrfBHasnrtVsLbqmVftB7JIyMP7wTh57/1+rm7vaSW0ss5V+Ff97PtN4r+X0vFp31nd96eV/Fe/L+ur2gpU/8kibVDGMnmvmgw7GAzodrvEcez7zdramgcxzz77LF/60pf4/Oc/T6PR4MGDBzx8+JBnn32Wl19+mXv37nF0dIS11gevyL5KvxSIU71W1esv+9lsNn0fEvWxtM0qjBRFZTX4RdqHqN3SNPXtbTabeS/CwWBAr9fzaumNjQ2vSpMQjfl8zsbGBkmSeLWx+N0J/FosFly9epUoirhz5w6PHz/23njj8Zjt7W0PEGWfZb+rwL6qBLfWsr297Y9boJuUuYpKfnNz0/vgycMwAVFy3KImlGvSbrf9ZyUhenNzk52dHW7cuMHx8TG3bt3yIG80GrGxscH58+c5PDz0+zgcDul2u4xGI9bW1nj8+DFpmjKZTGg0GrTbbV923O12z1hEjMdjarWaL82VB3D9ft9fd1F93717l42NDdbW1tBaMxgMaLVaXpF5/vx5arUa+/v7PPPMM34M39ra8uXdMtYcHR1Rq9XY2dnxCtXhcEir1fL3fbF2MMawv7/vA8ImkwmLxYJut8vly5fR2oXhCNSTYJXNzU3f/+SeIm1TypKlvU6nUz/Wirpd2q/AX/GJHAwG3gJlfX3dt5N/2eX7HyCCT0t25aUOKqRtRRFBVleEi2WAQv0wIxosk0VtqLGBJjkO0GmBKizpesx4N/RlqWpZpYk57QOg08x52c0X6LU1VKvBoulOp8AbKV0+48mG208PEAS+ZDm6sA6siHJNgTKK6ZZGZwnO1D0gbwbM1zRFTTFfK30cS4DmS67LBFD5X6fuM/M0ommW+6gM2NiwMBFsLcgf1SliTThKCQYzMIaipvjde8/SbcyIzhWc5g3+yR99kq2vaTr35szXY/KWpnk6I+1FqNyp4AR+TtKYqHSKT1ThPNBsQIaCzCUZO/ii/LnW1oEyB4cdMPNgOAEd4Et0TbAMGSlip2A0cVm6Hjj1YVFzqpRwYrEhpYm+KwsuaoowtYSlstAq912XjKqcYnFqiUeW0UXNYsOSbeb0vhlh/tQpP3vl23x7cI73/ukz/F9+8R/z6eQuh0Wd/9Mf/HVsHLlrGSboeuJCd+prDD68xfic9u3BhXmosvzWAe8gLUuYF9aXMptyYqozp5YJ5g56R2MXqqNyBzib+wXd7/R5899aA22JRxHhuKy3U24SbYPibIkyOOgnr5WqQZXrZaqsLJLWDEtoCGW6q4WIs5NjUeDOApdAC85DUTmAKT6F7g2LLtuBKuwy3diUELFMRnZJ6iU8zJaf85sULirl0KIuLHdMwlTQJUjUFZ/EigIRSiBf+V28CT2cL/txFXwKlHTl1+UfN/IHmOWpPolWqaUVApwJZDEhGKtJVE6GYj0Ys9cYcmuwSagNizzkSdblfHjKnXSLdybb/KWtr7KmHTDcX3SwAdybrlFYQ6A0P9G8yW9/9kVurZ2j+2ZA0je07mt+6+Al/uLugkjlBFgi7eB/YSN6wYR+0aSpF8QUNHTmHgzYkADDxMZ8e3bRgcOiRlvPuRIf0dNOZeKClgyTqMbl+IhIuWCmk6JFgOVJ3uJbk0ul2jnlT3W+zW44IFCGj9bvcJh3mJga35pf4ivHV9ipj/iLa1/jjcV5/unhx3gybXO9c8RPP3+T26MNXl576P0S/+5bP+HGocM55Aarm+jUMN2KCUfuQU/jkXsQlbUtyiiyO23u1JuoRsFoWoMG5LoMgSoCVOaA2dAkTBcRveaMSBv60zomsai6e/qQL0Jsrp1naLMgsyGtzozRcRO1ltJsLdhsTTieNJjPYopcu4CQdXdtR+M6Wb8GseEwLrCD2PWllkE/qRHOFLzR5lajRXhtzAs7+0RBQZoHXO5NeLZ3yK36Jo+OelzfO6Bbn7N2YcprX3uGJ/02QWDYWhtxYBTN5oI4zDHG+WRu7g658sFH3D1YZ/+4SxjlrK2NMUYTBoZp0SAfxEytYjQP0HGBtYo0zhmkdbSyvLi9z7fS81ijqddTxvMai0XIO3fPs/tNNwakPcudey4kIGqmZIMaFIrWnZDxqIutWfracDjpokKDDi1RI8MYjUkKtLI03orY+E5G1tIs2prJOU1Rg8YTpy6fbblOpVOoH5WqQCAeFajcEk4zwmmN0+ciZ42RWZ+8jHb3GYDaiSZYuIc04dQSZJa07fxpwwP8Q5Egs/5hlIkUk93QBW3JA4kfLj9QS9WUvjq5r9VqPpFRwIUoCKsATSaZ4l8HePVGdVJorfWT9X6/7xUi9Xqdbrfr04NFGbcKC1a95WTfRY1WBTTyGYE1UkInikFJDpVJrXhZifqyup3qtlbDPwBf7nZ4eOgVIzdv3uQP/uAPALhw4QI//dM/zWw24zOf+Qzdbtcb4D969Ij33nuP2WzGw4cP+eY3v+mBp0x+wzDk/PnzHpBW4VRV8VkN9JAJmkAEKeWrJoJWSzXl3FYhkng6CtwQSCshO1JuLOuV8sdWq8UzzzxDo9HgyZMnbGxscP36dSaTCW+++Saf/OQnfcnd48ePefHFFwEH5N566y1efPFF/vAP/5DPfvazvPzyy/zxH/+xL68XNeizzz7L1tYWN2/e5PHjx2cUhbIIbBXvRQHKcm2rgQ9StijtWEqWa7Uan/rUp9jf3/cwuOqbJ7C9ep5WYZrA7GpfW93f6u+rYK/aDqteevK6tDmBSLKsQjvZpyp4XN3WKoSr9rUqKHy/fXzatqvflaUKXN/ve1WAunrequBwVSG5CidXVZOi0rPWldIK0BG/TaVciX2z2QScz9o777xDr9cD4PDwkDzPaTab7O/v881vfpPr16/z0ksvAfDee+9xfHzMZz7zGZ48ecIrr7xyBiavql+r11LaioxV0odl3BKoXx2TBMhI8ITAP4Hkly5dYm1tjaOjI+7fv89kMvHnNYoiPz4IgO90Oszncw4ODnwfaTQajMdjXnjhBRqNBvfu3Tuj0BRFmKjn5vM5k8kEY4xX7cn+SGKzUoper+e3L2OrHDPgU9yrPrtSwl9NKxbVm6QFy/glZbtra2so5fz7JNVXwqNk34wxPHz40JdH7+zsnOkHck7W19cZDocMh0Nmsxmbm5veqkK232w2vYq60+l4pXiSJB7iiipR7oWirgb8uAX4MVvuxaJ4lzEliiI6nQ5vvvmm97M9ODhgb2/Pg2oZF+I4Zm9vj6OjI28hIWrU8Xjsw07OnTvH0dERo9HItwMZe09OTjzUFq9GCeuSh4hVsCt/H4glijyQlAdBAk4HgwHHx8e+LQE+aGZ9fd17R8o9TwJh/qelQCwhwnxdk7UhObLUhrb0P3SqQ6shnLmn/lkrIK83lonGxhLMDcpasm7kQIRaGtwDbrJRuN/13g7m8ROII+i0yNeaFI2IvExATk6KZaBJ5RqYWKNTpygS0OGUSE5phFJO/VjCUBNSKrpcEMZkNyhfC0qFn4NmVrP0M9Tl9yhBYgkPxZMKIF1ENEv4IZ6KBJajvMVfeekr/IP3vuD330Yh0ysdFy7zWx2yrMNvpNvo3HLtKKOINcEwJWiGtIalYW2oiGamVOYp/sGtjzPcb9N5wamOMqvLIJWCwipUql2AQKkOseW5BlfOG01EMQTRZAmZisS9BqX/YSag1p2bcIpX7AUzp+YrauXrkSIal96ZFmpDQxErTl5UxAPFZ/7SN7k/WePmrXP0tsb8Zy/9Aw6LDn/z//dX+Mxf/Cb/553foW9i/uf5X+ffffaP+Ou9+/x2601+8eJV/kJrn5pqcSuLHKBKItQiLduqxbbqHH6sS95cgsG4XyoJTy3zrfL3rvJp0GlDEQ8s801F47FluqeonTgIqssQmPmmov7EMtt2ZZiNexPXvtcXmDRAWQ3GlKnhFYWqLMp6RaKHh6ZUGwbgU5jlM5mbTDsPREr1onKviwekgEn5r3CKPFsorzykwPnG+c9UQlFKRWAVGFpVBqKIMlDEgpHyQFC+5w+tour9LjhXdlMfsiJAT5/9g82XM8u6nqZEfMriFZLgk7r9ORHwKMeoKn1ZUSYyi6q0fABgI1ICGrj+th5NeNtukRmNLg8stQFvTM7zl7a+yp9pjAhUwIN8zGvH57CR4WDS4u1szgtxgw/FAf/bi3/A39U/ysGrl6kfZCTHisOHV/mP/o01/v0X/1sAYlUwKupEKqdAE6m8/Fmxn7fpBVOOixZz6yZle9EpU1PjueQxcxtxe7HNk6zDj7TeZTcYUKDo6Dm9wEHFRBXcTreZm4ifaL7JqB5zbJr87/+Hv8x/9+ZnGV82XPrAY/7qpT/imeiAtk55J9vi/3Dpf+Bv3fkp/s7h5/nZ9W/ys5uv8p/c+nG+dXie4ztr2Jqhf74Oe/Dr77zEuS/mDC5HzDcTisSl4CZTS+PQkJy6cx/OII1carsJoXaksVqz2NCYJCPQhlkaMXzcRs8147qBwFJfm7HemlILcyZpTLoIoZljU00xDwhbGaqWk89LZU9gybKQICmwBrbaY+phxjSOaCUL0jzg9LTF3e/sOX/BSn+zpzG2nbO+OSIOC9L1gMGogUkD9nZPOXhth3ejTT62+4B2tOBuf41BI2G3OWQwS3j7wQ7d7pTjSYNopFk8qZMbxZOoSXysyV/ISdOQbmtGeBLRbzToJHM+d+0WX753BftOi8GVkGZzzjwLUbUCaxTdzoR5EjGfxujAMjxuMpkk6KDgLmtkwxrPXX/E9fYRJ2mDeRHyzdEV4gEEqSFdM4RHEUXTkJkaGIVqFIyfsTTfi4j7lv5nQvQgxHRyVJSTnTqoSqrpvhHSezeldjRjut1huqu8UjyYQ21UEI+d1YPOLFQUv1ZB3g6Y7kTuodLUersPE7j1xAPry5zjgbuH1gbW/52hc0sRu3ajy3WOL7oU7uYDF+4VDQU6llUSP1x+YJZVRZTW2qtLpMxvsVh4XywpV5LSIZnEVZVq9XrdB6zIumUZjUa0222ee+45P3mZz+eMx2MODg58WdOqKqoKE6reb1Xj/acBGYFGT5488enQxhivCpGyLJm0yiS3CjhE0SLqtdlsdqasW8Jgbt68yfr6Op/73Od46623vApJa83Xv/51dnZ2/H7dvXuX/+a/+W+4ffu2B4PiISiTpzAMva/Xj/3Yj/HGG2/445ZrISobASMyiZXJa/X6yrWqqjyrakQ5doFkMlGXSaKodyQkRYBGGIZ84hOf4MMf/rAvafzABz5As9nk9ddfZ3d3F601Fy9eZGtri2effZZXXnmFJEnY3t5mOBzyd/7O3+Hnf/7nOTg44Dvf+Q6/9mu/xo/8yI98FxRrNBp86EMfQmvNK6+84iGnQAg5vwINpW0KkBVIDks1ogTqiNIlCAK63S4PHz7khRdeYH193YfIiMeclPzJeRSF5KoqDjjjwVhtx6vAswrHVqFZte1LW616Ia4CytV+8CdBv6cBt9VJeXWi/jRF4epnZR1Pg4Kr218Flu+noHzad6vrfT8FZtXvtNqeZrMZrVbLg3A5x1IeeeXKFR49ekS/3+cDH/gAJycnaK25c+cOe3t7vPnmm7zxxhssFgtefvllrl+/zu3bt/m93/s9fvZnf5af+7mf4/DwkDt37py55qvjk1KK8+fP88ILL9DpdDDGcHBwQLPZ5Pr16x5yBUHAaDRiMpmcgYgCNC9evOj9GPf39+n1ejz77LPs7u6SJAlPnjzh4cOHHBwceMB3cnKCMYaXXnqJKIqYTCakacrW1habm5se/GituX79Ov/8n/9zRqORD2CSkKbj4+Mz1+/8+fP0+32fICzAUPrb5uYm4MqGtdY+KEXgWaPR8JBIvPzkQYjcW+QhiyhET09Pmc1mZ8bHxWLBwcGB/1zVe1KgZBAE9Ho91tbWvMJyPB5z48YNHj586McOUWhKMM18PvfHlCQJH/nIRzxsFfVilmV8/etf9+PgeDxmbW2N2WzG+fPnWSwW3qdWxmJpp7Va7UxVQJIkXtF5cHAAuDHxwoUL3L17lzzPOT099SpDCTOpqm7PnTvHuXPnvB3G3t6eVyomSUIURWfaTxzHrK2tnUl0lmCZk5MTD7wFnEpiuaj5q/efVqtFp9Px130wGPhrIedY+rp49sp9R9KZRTE5GAy8+r/ap7/X8v0PEHF/4EdjS5AC1qkOF2tl6vIcTKSJR5ZoYgjmhnCSQ+DUXkVclmAa50eXN0Pyhj4DDwUEmhBOPr2HsnuEM4MulQXhOKM2LY1F7VkVkttB6/9XhcVGgQvSAAcOk9Afhw2X2/PqRCBP3PHINkyA993TlaAXq+2Z3wGa7TlZkWACi8mVB3QS8hK1UrrBjM1wSO3UgZSiHlH0aihr2biZ+mAKmWxJCrGep8SnIbp8r3aSnfnM/I0ekYaGzpiaCK0MqQ1KD0SFSlWpHrTkiSLtOQhslVPVWU1Zau1+LmJFNHH+flZDkTgQpxZl4IkV4FiWi01K5VbkgKOJIO1CY98pEfNE8eG/9hp/fuPrFFbz7/zzv8z/evOP+PSFgB/L/jzryYQfTTQw5v/4Qspf2fwiz0ZN3svG1DZmbJSBMxkBNgRdyvQOi86yDZVPf0ynwZNPd7GhSwOdbmsaB4bJrqb10HlzNh5LGR0093PmGwH1w5y8GdC5Y9CZoXsbwnGKDTWBtLtFBoVxYT7WQpphJxNQ16BQ6IUleHQM9QSynOCKU6UhQSYoJMEbw5K4iRdiCbM9HBRFYlV9qErVngds5cWoKGGtdqouWy2DxsEyq4BAgVK+z9hQu1JetVQQ+qAilv+ryi7I5wT4yetuO8v+5RWKuUVb6yGBe7hwVmVIYZeepkVFSVjpC+JX6JN48+VDhDLQ3O+Dzl0ps9+vEjLqwi7DU8rXbaBc+nQOBYqeWhBhaauMepC5ynBlCZSloRckOqMXTvlzjTGBKtN8i4jLnVP2PjikG8+5n3d5Ic6IVMD/rHnKP4tnHGd4FWZylHH6tXW+ffUiLzfuAtALJk5lqAw9PWVknIdGUy/4jx/9NF/9+g10qoiGmsVWgV4oNl4tYU1uCeeG39v8EfI6pfJ0qfg0IXTuFRSR4j+76JLI4xFcvJ+THE3Z/hpMv7jL3+r8JSYXFXndkhwp5luWaKQ4WpznS8HL5A1L910ICstuBspoiniT34232Dl0acfxyDK+ENJ6lBMPDKZ8cBOk0HwwIxrXyBuaRVuxWFdkbXeRaieaWbvGk3GMHoVEC0X87JC97pBpFvH4oMd+GhKGBVkaEgQGJiFEhlpvjtaWNA0J4sLx9WnA/LhO0M4IYzcoH02bTBcxUVAwHNXBwjMffMhoUePJky5KW2yuWbt6ynReYzKrETTnPLd+yMtXHvBrdz/CLI248rEH3P3KBX7/UZvGzoTpQZPFg3XuXMoIT0MCDaPDmPoTTfeRoYg19ZMCnblwkcWDFos1RT5sYq9BEBjufPsctxs7rmFu5gSPErJ+naxtCXAPcU5sBz0MMY0CVStIOgvCsCDPAxZlqbFWlq8eXGI4SdjsTGhuTJnsddB5yMXfLjj4mHvIZqzGNgrsQqPmAVnT0rpv2fuNiNElTXEYUz+KaBwUvgw9mGcoC/PdBtHUsPltd+3yxPXtInLjy6KryJrK33OisVOvZ20XjlI/gLQD8dC1C2UgXFgaTxaYQDtvxZrzTQ5nhtl6sISVCpJD6wPO6oeuIiJrgV7gH076sK0fLj9Qi0ywG40GcRx7RYR4DgloUcql94ryoAqnqmoIwBulV33diqJgf38fgHa77cvYRBEjnzk9PfVKGgEkVXVc1QNQXpeSW3mtGowhahSZeFQhT1UdKeusKnzAgSVRkVShnCj+xKOwas4vMBAcpHj8+DHf+ta3/KRStiNgTtRQcp7FD+r+/ft8+ctf5q/9tb/GrVu3zoCWIAi8Ckh+f/HFF+n3+97cX5SbVU9CUTMFQcDGxoZXFIqCSSDk5uYmcRxzcnJCr9ejXq9776utrS12dnbIsoxnn32WL3zhC9y/f99fv9dee42bN29y/fp1Op0O//Af/kM+8pGPMJ1OybKMX/mVX+Enf/InfVuSEjgpgxNfv2o7UMqZ/AdBwGQy4caNG17N02w26Xa7PthH2pgAWWknURQxHA69ygrwqilJ0gZ48uQJ8/mc9fV1D2+63a5XqzUaDTY2NnjnnXfOtCGBu9V2WwX00sZWYaO0a/l+tTSv+j05F9VtVMOKqupCea2q0quu83uVD6+CuFU18Koq8WnbqH7vaT+vqgWr56F6fp62vqft/9P2vdpfqspTuS5VH0TpQ/J5gTV379717WQ6nfKRj3zEl65ba3nw4AG1Wo2f+Zmf8WPa8fExv/7rv84v/MIv8KEPfYhHjx4xn8/PhKZIm9Zac+XKFf78n//zvu82Gg2vzL548aJ/kFMUBXt7ezQaDQ/SRCkpar8wDPnkJz9Jo9HwwR//+B//Y7a2trh27RrXrl3jwx/+MEmSMBqNGI/HHB4e+qCUWq3G22+/zZ07d3jttdc8/F9bW+PXf/3X+dKXvkSr1fKJ7/KQ4fHjxx5CKaU4OjryISfiLympzcfHx97qYmdn50w56/r6ulcvy3WQhx7VByMC7eQhjQRZNRoN39e11oxGI3Z3d33CcZqmHhyLyr4aTiJ2Htvb24zHY370R3/Ug14ZNw8PD31pd5Zl3Lp1i9ls5r0Dp9Mp7XbbXx+xWlhbW2N3d5fRaMRiseDu3busr6/7e4C0UVFxtlotPzaJn6P4Ur7wwgte6TgejxmNRv4avPTSS96/cTQa+aCXXq/H7du36XQ6NJtNbty4QbPZ5OjoyN8f9/f3sdbS6/XQWtPr9Tg6OvKlxmKlsb6+zsWLF/0DJwGJovI0xoWhyMPE0Wjk721Vdb2Ma2tra16hKQr5an9N09SXMMv9d/Vh35+0fP8DRAXJaUH9ydxNtiPnwdd8EjjFV6lm0qkhmBcUjRBTC9CpQWEJ58unuSbWBLMC3QygdlYt5Cb9rhwpGhtUblxYxqJAp7mHRHAWIp7xaSvrHU2oUVYtyyQBtCuRspqz5c3l9sWz0EQ4vz5Ywo1SUSbbMqrCf3LFYh4RFJD0LY2bCbVBjiQ3x4OcKCoYFHVGReLLrlRhCCeGwoTYUJXnL8cGGhNpD1nIcvQ8x0Yalbvzgg5dmnUTdKbIG+5GNDQJcxuVSayuJNIlWbr3Oz/zGIDT/36v3AmnvJttuXJeqxXzLcpEbcVi05LXDc37msFzxhnlDxTj51LWd4a8uHHA67/6ItNPTHn54gO+/uZV4mHEuZ+8z1YyZl6EvPGH1/nbF10S1G9PI2zN0NYpUCcJM7aSsb+uUSPlWjgFWsytJs/C0gvOcJh3nGKppGw34v1ygiiKvRw9mrH32zNYpKAU60lMenGNjS8eY4cjVKMBUYg9HWDTFBUE1K2FosAWLuU72FijOD5Fr/cgzyEMUXEM5R+9JDUHsWoxdJqEUUFKiAT12CRGFcUSTktQypkG615XqcbW7NIX0av4KoS8UMvwFFOCP/k8QGhQi8CVDVfWoWypPixTm3VuvV+hB+3B0ifQg0FREK4oAKuA0e1jxbfQf0gOzS69BSUsyXLWekD6l3yv8h3381LZqCqf06b0LSxB4lLpXPmZiiqxLJc++8Dh7H7r3EIJ0CNVUPNJxiHr4cSVLxcBxroyYsD78MnyUhzxn176Db6TNdnSUy6GGkj8+9M89v1QSq6DObwz3eZTzXeZ24j1EpYbq5nbyNsS3Mm2eP03n6c7dHA+ObagAqf4GuYUNUdP03ZA/aTAapitBxSJpf2gYN4LygcFlqyrad831AYFJnJQaLpbc+NboIgnlubXC+JhxmItYvEkQOeGaGLImprkOHfwKVRE44Ii1kRAkWhncZEaGgeFh5cmUv6BRxFB2o3JGxoTQG1kSQbGe8zqzBIsIhYb7lrkbUN22OB+rmkkKXGSsRgkBE/qBNcnaG2hlUGmSWel6rDQBElOMYiJTwM33h4ELHZz7ufroCzFKALrHq7Qzbh7uAZAqzdj/LhFMNX0B+uYhqG1M+bkO5t8ub3GH6fPkhwGtO9YHl3cIC5g5xVF2uqwNjVgC/I7gffhjWaWvGZYdDXzTciaIfHIga945NTM48uujxaFho0F6jQmnDoVc1F3ycK6gMZjp/DO2yF2PYV5QPiwRhbHLOoWlSnCqSJIFTc5T3QYEo0UJ0G7BGpuu9E4Z/ePy5LhSJEnIccvKYq9BeGThKwFmVU0HxvikaF2knoP1NhY0l7M4Ydj8gbkdTcGJIeKeGgZX4LFTk50HJKtFW58a+RQKBYLDWspPj1eRWRty2zPDRrJfoAyislOne6dnGhinKob91ArXFiSI9dvde6CtrBu+8pU1PKBe+g123ZjSZDyw+UHaBH41u12vYJEJkoyqZPyVAFRAroEdAiUAjx8k0mcrEsSNCVQQwz4ZUJYVZWsTgiqk23ZP8BPuKQUSsrHqkCz+tkq+KxO1lYBjSglJARgZ2eH9fV18jxnNBqxs7PDYDDwJW67u7te7SF+W7IPMhES5YasX5R+VRWRgMBqiZ5MDgU8VJVnsr3xeMz169e5fv0629vbPHnyhLfffpssy3wJ2Msvv+w9y65eveqN8G/cuMHh4SH7+/vcuHGDdrvNW2+9xd7eHjdu3KBer/PGG29w7tw5FosF4/GY/f19nnvuOTY2NphOp9y8eZOvfvWr/P2///f5xV/8RR+McHBwwNWrV/31ns1m3L9/34M+AbKinIyiyJfryflLkuQMXJIJuqgsxfS/KAree+89P7mUNgd81/8CDKuQTo5Nzm2n0+Hy5cvs7OxwdHREnuesra15BapAjadBLmlLAodW1X9VALjaRuX1KgyrJlBXFZbVRUIpqssq5KvuW/Xnp5X6rn5udT2r4FDacHU9q997P7BY3Y+nfa76e7XfrgLCpy2rMFcAoexr1b8VOBOyI0D54sWLnJyc8Pbbb3N0dARwRs0mbVXUUQLhp9Mpjx498qEaoiKT9in7X6/XvRfqq6++yqNHjzg5OfEq106nw8nJiQ/mqNpKhGHI1atXWVtbO+M/C07tfXh46FN8p9Mpr776Kl//+te9SqwavFMtpX306JGH74vFgrffftuPNXJNarWaLxMWa4ssy7yH3vHxsYeInU6HwWDAgwcP2N7e9qFPQRD4wCgpaxZF+N7e3pkgJ1Hmyf5Kf5Zwl16vR7fbRSnloZn0Qdne888/z8bGBuPxmG63S6fTYTKZ0Gq1uHDhArVajcePH/sQncViwde//nVarZZ/gNDv970SVcagjY0NRqMR8/mcw8NDBoOBL7t9/PixP0eHh4c8efKEer2O1prd3V1OT0/9/UTuFeKDKMo7udaiohdvyP39fZIk4d133+X+/fvkec67777rw2fEB1HaUNXDVQJ43n33XZ8IXRQFzzzzDHt7e/5zojJtt9tcu3bNB6lImJg89JEwFBnbZd+VUn5bk8mEvb09Tk9Peffdd/1xhWHoFaiibK8+WJxMJr5EWkqe4zj2/oz/ssv3P0DETYaKenkotlQH2TJlN3fKIhNpwlmBTg15I8CEiiA1TvkkJcUKAlvehKz1HnzB3BCNc/QiR2VmqZLCTWptVEmCEvBRlBMIlEuEzaV8WaNzV04twi5nNedUOF6ZAL70Sn4XAFMtP/WgsCjLrHNFcmyJZq5EK5oUDC81WKxD590xvVfLdLc4xGqNTnPiULG/6LBbGzoVZODUhjZwSbZ6YQimOUUjhMISLBzMso0Q022QbtSJhmmlBNSpO+sHlv5zkLcK5jbwYQyBcj5pv3b4CTY/dED/mTrWKl5uDXgybWMiSD5zxMmTDvX3Yn70517l9eM9+uM6//cP/7f80fBZ/scH1/mVl/8uX59f5v/5Bz/Lf/GTv8Rz0YCff/V/w795+TX+xsbrhAQ8/4kr/Ief+DX+XGPAf7l5kf/X4Of4T6/+Oj+aaL44N/wvN6/4cxmrgqCVkahlCEI9yPz7+TwiUe6EZyUJk3CJ3XBAulFwWCyY2pR/fPI5N3mcVWaIRQGLFDufo+p1ODpBb3cw3SZmq4ONXHm62uu57TVDr5CVNuACQa74UKA8cW2tfuyUh0FqOHw5YXzJYOqG65uP2bs04I2vf4DmzcQpE1sN0pY6638YCEgs21mpFvQdA+s+Y8oGKwEpqpTfyXp09WegcKm37nNqGdhiQVXUix4elvDc+yEa97qoDm1Y/gEpfwMJfKuAQ9cfXL+HsyAfD/Nx/b6yLRfEtFSxVhdb8UeU0mK3PQliWKYqu5W59SlTejPa5TqkPBkox4mz25Hz4x9clNsqaoqenhMoS4YiwCkOjVUURhMHBSd5k5FJ2I6G/P484p+cfIK/sf073M07fDCGF6MJAYr7ueFalDG3OREBgzRx44eEypRhTBpLU6XMsYxM4gNQIlUwsTEBhi8Ob9B9z9DYTzl6yT2EWP9OQf96wOBqSOthwemzrgG3Hrhz4TxI4d7PWnYvHpAXAROL8/kbxTTei2jsW+KJg3Wnz2tUAfEAlNE09xWLtma+5R68NJ44gGWCiHjsHu4oY9GZIe2GpVoMspqm+XCOnmaorCDvJShjiUea0YUahx+OSHuW+oGi8cSQnLjE3kVXs+hprwI3kcXGhqCZobVlXCYrExpUoSjuN0lu9DGFJksDrFEEtQIbWAeokoKsq1C5Ipgp4oMQVEjt2CX9plfm6I7BnNTIFgG6nqO/0uLSO06+VsSKYAFpp0Nv7kh5ETllZhE7RR0W+tcC5luWvGuJenPsnSY2ssR9jQkD0vWCYGNKPg0p6hHqocLMnAds3nTnvnNbM0nrmNBiE0u+tyB8XKN1R6NT14+jkVMuqkzBUUw8Viy2SkhXK0gexURjpzRvPgid/YY8ULCuJFhnlvlGRNbQFJGDa7VRwearmvytGlY7G4fJMxm6nhPFORf/PwHhN95Gr/WYvbjHo8+HRDcGpNOYpJGyuNMmmjiLB/vcmFacM512loA+Uyij0Ftzuu2pS45ehCy2NfXNKfNxDbQlXSjiU83oqmGxHrL1ao5VMN1243M8dP06HjnP0yJWLDrOpzhrO3VmkcBi3WAb///2/jzIsuy87sV+e5/hzkPOWVmVNXZVzw00AGIGSAAEQFISCYkMipIsS7SexWfJkhkiJVt+4TAVwQeaDIfEMJ/kkGQGSVEW8WTSTxQkQsLAGQRAoOdudHVXVdeY83TzzvdM23/s8+08mV2NgZoMxP0iMvIOZ9zTuXvt9a2VEDQmqBs1/J03nqxN41szZEIu6aAy6e52uw5UM8YcS/MrpvsZY9zkTpgSxTSvYpqRMMb6/T5pmjrA8STIcj9gQ16ffC8hQKccU65NACkBD4pggrBYRMy+0WgwPz/vJsHCYhEtrt3dXW7evHnM7VeE7YXNI0wjSbmTspP/WWYdX6vVqgMfJE1QwAdjDMPh0JkDrK+vO6BXJvqlUokbN24wOzvLBz/4QV577TWiKOL8+fPcuHGDyWTCzMyMY24IIPLss8/SbrfdZFdrzdmzZ0nT1GljJUnCL/7iL/LII4+QJAmdTocvf/nLfOxjH+P06dNsb2/z8z//8/zAD/wA165dY2Zmhv39feI4PmaiI/cjQIMAxVL+MqEVg4Fut8vp06f563/9r1Ov17lx44abiAogu7m56eq5aP7SbDaP6VLKhFvqAXBMKGFASZuU9jsej1laWuKHfuiHePDBB4njmC9+8YucO3eOU6dO8dnPftaBwaPRyKVmCsOxCFwXgbWTIFsxRbnYjosprRJSjkXwG47YctKOpWxP9p/icU5eW3GbIvPvfmzJk8cpvj4JcJ48r/wV5QyK/b1YDm/Eoiz26ftdf/EYRTBUzquUOubwLe1R0tiNOdJHFGfx5eVllpaW6HQ6NBoNtre32djYoFarcebMGdrt9jHNtiLIJWCQpL0X712Y2SKPsLq6ys7OjmOQzc/PO7BQKeXAQ5F9EDBFWNC1Ws05yQswt7e351yVhSHd7/fZ3d11phoy5i0sLDAej7lx4wbNZpM4jt3n4to+HA6dc7xS6phGotxno9FwZhyPPfYYxhhee+019vb2KJfLdLtder2eA94k+v2+A24lbVUckuU7WbCSti96pdKnhb0ouooyrgpDcWZmhoWFBXZ3dzk8PGRtbc2ZdERRxEsvveQ0eefm5lw7OnPmDK1Wy5mjCIAr4GcURU7rTxiX0s6kTOR5KIYswhTd3Nx0piSyYAfwlre8hTiOuXPnjtPvbbfbbG1tsbW1xa1bt5hMJk6qQdrI4uIip0+fZnZ21rWXV199lcFg4GQZxMSqWq268vqP//E/0mw2ec973oPv+/z+7/8+W1tbtNttVldXSZKExcVFZzgzmUzcs1xSl33fd6ZcAiICbG9vkySJY4HeuHGDCxcucOnSJaddWXQnF5fqxcVFp9EpfUvARnlWiIu4gI1fL771AUQDcU2jE88ZpSiTgw7Z8Qm6UZCWvTwdVlndQiPAggU40rKHjg2V3Qx/lOB3LAXfaE1WDY6MFCRFMsmse6unyAIPEyiraQguRdGBiRmQWr1DldpzGqXt/9C34F0+cc/CI2aeUWB8+52ObRqUuEUGQ5tC5Q8SyxrcG6DGEwuMjEaoUonMP01cD45cZsGaO8QxKMV8fcCt3hwvHyzb8+XgZlryiBs+pb0JGENc9fEiCyYaz6ZWppXAloOAK5648irCQYY39gju+vz8+kd4pLHBB+pfxcNwdbLCw7V1vn/madrekB99+q9y7x9eBgX1qkH/2iyLniILDM//4ycwGmohfPwP/5KtTw/+7O7f5J9+16+gawltPeKMX+dtS3c5TCqUVEBqMmZaAxa8LoHyWPS7pHNxzjAss5M20dWjXP+xCTCZopo/f0t+wiPVdfd9UInZzzLW0xF/NHwAk8Ev3Ptu/kNjmy/tnEcPNd/1B38LfbdM4yYEFVvvpCkEPuMLc6QljTfJrEnNIGIyWyIMNH5nzHi2TFpSlPYistC2Z2uwgk1BjjL0JHXAlGt7viaaKVFe66H2D5n5+2V+9+F/xX7+g+qsX+GttcfIWlavMws94np+k352xCKUdGZtbKpy3r9sp8r/izlPpuyfLqBtniDihX1FNzEpAGP5PsYzVtNtciQZoAptyRml5H1Nm/wHjsqNUITNVzApsm7jyhE/VeG/MykphDJAkoN6OcPw+H7mOFuwUCbFY4mzcvHe3fsTjMJieTpg8yTrsXh9kIOv0NYJncynrRPGylDWMRU/JjWK1Cj6qV1FbHtDnh6d5/fuXALg9+5c4sGFbcpeQjsc8fTOGR6d3WRt2OJjy8+ilTmmOWlKlhWYoehmZco6BqMZZiXKKkarLHdczlgftuitalQa0L2cUl33mLk6YesdZdSuproZsfOWMvoIi8cbG5KqQvc9DnpV/vtH/4Avdi6yHrZYOXvI080zDKlTezZhsOzjD3M5Ch+SKlR2FOXDjL0nYeXKDlvPLlHeU+jIULvTZ+P9LbqPZcx9ISDs24WNSVNbcG0QEAJeLz0GCmeBYryYYWYjeguawZMp1Wcr1DatY7M/tBVlPGXZxj2rqTtq5WNIpiBRxK2M0p4mem6GrJbhpwJYG3SiSJqpBa0mloHtjRVJzVC7a53ULSBXJqlZ9p/xNMbzGS0ZRoueXeiKIKlaGYasnuJVUtKhDwaC5oQwTBkNQrLYY3ahy2BUIk080lqGqSeMah4qUZhaQrZZxo8UST2j+4BCjxX1O5qFp1OihiYLDPXbkPmWxTmZ8UhLhrhhn0m1NWsgAgp/BDNXrcv1aN5jtKgw+yHVTePAQn9kiD0LvOsY+mcV49Mx3Vjjz45p1Ef28QqsvzLL3HP5ZNG3wKi/72NWYyadMoPTHu2tJUYXZrn9l1MW5w/ojcp4fkb6UpPmhu1jg7MJXuIxyRRZJUOVU7tA4hn8akypHDOJA7TOSBOPcGbM+bl91vwW3d1argMLpmSYzKXsP+STeTBZyFx2gtEQdLV7nZYNWZCRlc2RdEMlRXkGrQ3ZSR3aaXzLh4B/whATVptM2IWlIpPsIltHJoECHAirC+wkQHTnhBkjzDGZfJ1MuSyCiKJbJ+cpMmWKQAfgzi8gUxHIlFRsEdgXJmS5XKbdbtNoNI4Bd+I0+dJLLzkTGKUUTzzxhEuDvnnzpgOwtNbMzs6SZZlLLdvd3XWMOrkHASaEeSQi8lL2xTRkKRP5rtVqsbOz4ybvoj8ZBAF7e3v88R//Mbdu3eK9730vt27d4tatW475dHh4yHA4dGBJtVrl8uXLHBwcsLi4yHd+53fyiU98gkajgdaa3/7t3+bKlStEUcTGxgatVsulrl2/ft2lgB8cHDgdSbkfSSOUshFQMIoiLl++zKlTpxiPx5w7d44f+ZEfYWFhgZ2dHU6fPs3f/Jt/k36/z9raGmD10X7jN37DGdMoZdOmxZxFgL8oitjd3XUpnvInbULKUgxgTqbLCiNU0qhbrRbvete7ePzxx/k3/+bfMBgM2N7e5k1vehPnz59ncXHRpVh2u91jbaT4X4C04uf3A8aK7fckcCj7nQS/73cPAkwXncclTqb7ngSz7tf/hIF3EhA8CSgW77FY3vcDEu8HFt4vvl5K4v0Yh/djT568TmE3SZ1LnxdTHFkokGuQNizAUZqmzolX2t6ZM2eAo5R3GX+KxjYCUhaBTLlmqdvRaOS095Ik4cqVK9y7d49Go8Hi4qJjWW1sbDgdRCn77e1t2u029+7dc+m3whRsNBpOW076qIwx4qz85JNPsr6+7vrA8vIySZLw4Q9/mOFwyLPPPsuzzz7L7OysY3nL2C5lKE7s586dc3qI/X6f06dP8/73v59Op8PVq1dpt9tObsD3fZe2urOz48ykjDGOsSluvq1W69gY3uv1WF9fd2nJcRzTbDaZm5tzdTE3N8fc3Jy7lvF4TKfT4dSpU5w+fdo9M6T/i4mWAK2SZru7u8uLL75Iv98/Nr5prR3rUsaXTqfDhQsXmJ+fZ2Njwz331tfXnTGOgHfC+ms2m44hKkYlURQRBAFPPvmkux95ZiwvLzvGnqS5z87OAjhzlc3NTZIkodls8uCDD/L5z3+e9fV1B2zXajXu3LmDpCIvLi7yvve9jyRJ+NSnPsXGxgYXLlxwRl/St2/dunVsLBN2JOD0JyWVvF6vu7Tpvb09p9WbJAm3b99maWmJWq3mjFMk9b3VajE3N8fh4SH7+/vHxlP5PSGLAe12+xj4+PXiWx9A5IihIyygtKSdK6qOc7OQxDhQBmXdai3glR9jcgQkCgMprvoYXXGgTRZoklaQMwPthLu0PUD3x0SnZ4ibPuF+BFnG6JTVUQj6iXV3ztTrAYUc7NBRrluncOxFlVk2A8bgT3I2YS/F70foKEV3h5iDQzAZqtXEjCco38/BqtxdrmJ17sobQ8KlJkm7hN9TVs8tTS3LTCkWyn1e2V9gEgcobcEQm2ZtCDuxvU5fEwwTvH5EVhYxRvvPG6cW5PQ0UTPAH6foKKW/4jGZT8GDD82+zKVwm5qKuREv8HT3HEulLhtRG4DshRZJ2eqRYaxovaSDQV6HqSIYZJbdlULrluYL33GZau3IVTszmkAdPcxrYURZJUBIJ61a1+ccpQlUQpZo0px1eiNaJBv6/N5olfV4hrX9Fv98/F7+qbHu1frlOh9Z/0lUYtPxKkO4+fQlbviX0AnUAXWz4lLNvdhgqiWIfUzo03kgtG7SE9BRaIHBBPxRSvbcy5jT32HreadPVi/h9zVpNbBp8vHrf6AAZBWftOwT1zzMahNvocab557nNwfn+cT6d9AMx/zs6m8yXMnYfE8bFKQlGC7nbEKhuUV5ZYYFQNG52uRAnzAGFUevBSA0HDdNkTYeiJ2yBVAcezHLt88UKrZtzTH/BKjLjVJ0ahxT0LJ2T/xQEnagnF5ea2XBQeyE3gT6mBGK205OlwOJWUGb0O6cH1cYHMeARnPELsxfu8WLvE8rYzAcpWO7w6qjdGGXGn0S4Mwsk9KLM4yGXuYRG01qIDaahh5TCyYcjCt4OmM3qjPISuwndS6GO/hexu6kxkq7y/P3ThMEKZ6X4emM371+mYXZHl8drhCnnnOKtRdn2WcVL6ap7SJKpCAkJcIjNFaPMUNzvr7HS/MXCQ91bkIFWckDY1mBOk5p3ITeRXs//tgwWvCYtA2lPU351Tr/z1e+F2+sqG4aXpo5RSWBuAGdBwJGy4bGa3axZHBKE7WssU8cKC49ukYjGLM3Xma8YCjvgYpTulcSHji/xd5XzhBXFYNT1lypvIcF8fsTO4aWAkggBeKawl8ZcH5+n7sHbUYHFcZvGTLRGdXP11l8eshgpUR93bqjlw5T4prHcD6kd8GQLU5QZUNW1nBmQjQMoRvQfFXjjQ1pSVPbTJm0PDoPAtrqtCbzKdU1j9lXLIt78x0lMh8at2yfGJxSDC5H+NUEAwRBSqUUkWaaVmVMyU9IMk2cegReSpinrwezKevdJrPVEeUgYX1tFn/OjpVJotGNmFI5YtTzUStDzGGJM+d3LRC9sUx1O0InIaNZbWVAy5Dk5lVpyaYI19YUrZsRk7Zvx7PImhlVN8bU72T0zlctWzKy0h9hJyK4u8u9HzpH9J4enpfRKEW0dYZWhsNhBa1gHFsmoHWmt+PkeE5R2jcsPGPYrJYo73qUDyLipSb3viug1hgwjgJCPyF6tUntnn3+Gw/8rkdaTW0aeyPGZLbP+pWEeOyjtCEME+LYOmXrUsJsaUja0HTXG/gDhUpATWzfHi8cAYMypgJMSpmVZ8ifETpRMFJkpcwCyD2frJSRht434sE0jW/REJCwaAohk3BJvZWJcTFtuQhYyHs5lqSuyqRWtisK4gvQd9LMQ5x9ZbIizp33S2EUkECYhu12m2azSb1ed9p14koqqcC9Xo9Op8Pu7q5zVZb7l3MJU050ACeTiWMEwREDTI4t2mQCzgh4ADhQK0kSN0mU48jkO01Td89hGDo2S6VScd8L80PS3URbanV1lfF4zO/8zu+wtbXldLnAgiei4xYEgXOHrtVqzM7OcvHiRZ555hlWVlbcBE/qVbQsBViRya2wf4Qxs7i4yJ//83+eubk5dnZ2eOSRRyiXy9y+fZtKpcKP/uiPutTccrnMxYsXuXPnDteuXePGjRtOr21zc5Onn34aY6wm5IMPPujKbHZ2ljRNeeGFF1wKpYAX586dc4CtpDtW8vmEaE4KiwmO0uIFVDLGMD8/78pkY2ODp59+2rH7BoOBY+8IeHDnzh3HppL2V9QwPJmiLG2iuN3JOKnfKfucZOaJHloRXJd03GIqcRFMlOMVdUmLYNbJ67lfivX97ud+YOL9wMPiNZ0sjzdiS97v3k+mLgub842OIdsIU0tS5qXeB4OBSymVMhRN1cFg4NiAw+GQ4XDomHTj8diBbQIoStkWgbr7lV/xtbgBy+KFaKkKMCK6ssKSXFhYcCCQpOrevn2b5eVl9vf3GQwGzmRCQDIZY0RzUCmbOi0OxZI2vbu768xULl68yKc//Wln3rGwsODuUQwt9vf33SKJ9NHd3V329/cB2N3dZXl5mYcffpj3vOc9vPDCC8f0BKWtLy0t0Wq12N3dZW9vz2ndyaKJlK+MradPn3ZOz8ZYvb7HH3+c7e1tp8v37LPPMjc3x/nz511K9bVr17h69SqVSsWNn8JmFFanAIFF13rAubRLv5B+L6nOlUqFUqnk2tdjjz3GM888w+nTp5mfn2dra4vRaORAxeFwSKlUcgsrIoORJAmHh4d0u13CMGR1ddW5b1erVWq1GrVajTe/+c3cu3ePfr/P9evXj415tVrN7SOLHEUd3CiKmJmZIUkSbt265YzT/sN/+A8Mh0MefPBBzpw5w6uvvsra2hoLCwsOyCvqFwLuWSD3L+cVZ+d6vU6j0TimOXt4eOhYg6KrLP1EwFhJXZffHUWwX/rENxvfFgCiBQ1t2qzVqcpyEEc58XQ8SLXCSKazp/DGBiJDWtFkpXyCUMBpVGZISx5pyUMlGV6cEQwSJu0AlGW7pI0yKk7JAk3QT/D3+iRzdbwoI6lohkshXmTwh6lLqztmkqDAm6Sk1ZCwZwi7KaW9Cd5gYoHE/pB0c9sCg0pjkhhKJZifQ1UrmCSBKLY6eGC1GOPYgYhohbffZe4FRdzMZzm+tmnVaYSpBJwqH/KDDz7FnNfnb//hf+/ATAu6aozn4fcmZNWA6FQNf5Ti9yILwubp2Spnh6UVmypOlhHXwVQyVJDxePkuHoYIzV5a596gTWw0SeaRodATWydZzrizdXREblNGQBbljFV0otiLayw2+vSyMveSLr2kRDsY8mo8YJj5DKKQZ8dnic0azw7Ogmf4/OgSL0xG/OK996IPA972lb9I71qb6oamFcPPPv8X0JGhqiHxGpaRmllX6LBTcMMAq98XHV0vOYM0LUNlJ0Jt7aO0RpVCjG4fbZunouvE3rd/4RyDimbc1hxeWGQ8j9PXUwkFwBmnh5lUzJHpjgKUpvGaT0knXB8v8d75G9S9MWf9KmktY3DaMp6Mb0gb6XGzE/LX8t7pB9q2QKxBG1RQSBOJc+HN3FBFjT1M0ZUZ8jRnjpiKkhpbTshGljElGmiiZeZYeQXAzbH/pG+m9mKLDsvHjFIK4XQKJcVeH99OgEvRXHRGJgqXkqzTzILqkpaslAMU3fc5+9ZqnhbGkrw8hM3oAEPsfZPrnlp2ZuH+FMfSpaO2dSvuZSE7WcmyAXWErzMCLyVKfEZpQGx8ro0WeV/1VT7+2P/CRX+f3+i+hXfM3eLJ6m1eHq9wmFR4vHqXV8eneKC8xW+98hir47xuU8vuREFFWwB+bHxqKqKqEnayqgXpjUdZJfzV2c/zb089gb5awpkr5WOCyiCpBsR1Rf2Wof3KgOHpCmme8TtZyJh51eCPNJ0HbYpqWrZswzQ0hF0YL9j07XBgHCuvf0ZT2TYc/KszvPa4oRyBqVl2GxkQZly/tcTiwIJecTMHuU0ONmsNWtuxPTPErQBvbFDKsDOoUQoSxmFKpTqhVorYfKLE7Cs+1c0IlWaYQBNs9xmfbuJNPMoHmqRSon9GMZlPGSUavxaTtSMOH/XteSea0qFm9uURnQcr1uH5QBGfnjAoBWQvKGovrXN6tMDWO+wCVOPOhMqex+ixlLAUY4wi8FNMnrZeDSLqgZ2MDpOQZjhmvd8i8FIOh2VmqiNKXmIB4lJKtTpBK8PIT0kT+4PaBIa4U8ZrxPQnIQfrLc5fjfAPRjReukOzUefg7ac4vAxpI8Pr29TlLLQLEe3ritraiGBQIqlYZ+u4GVLaG9N6cR8T+mQl+6fSjKzdYHAmY742YhQFjGOf/mEFOqFdMNq2LL7a2DJVMw+yvE14MYS9lMb1AG9iKG0N6DzaIl0d09uqoyaayobHzJpxJkZpyY47JlOYzMsbnv2fRh5kiqhbIkrLqMQyQyM/41Z3lu64hEoU8ZmIpBNg2jFZrI8Y28qgfAMD37IMc/BQpRC3U4JDW8Y6VqShIWvFEGnKlYiJqryOcTyNb/0osnVOmkAI4CYTXZk8CYhSTFmWfYqggkz6RDNOJo8yMZAUOgHmhNkikzUBs0QsX8CrYqqmTPBkcn/27Fk3OZRjiHPp4eGhY5BEUeSE+4MgcJNDARpFbzBNU65eveqYQMV9xchkMplw8+ZN3vnOd3LmzBlXHjJBlW2lPIWdJGUrZVOcjAozZXNzkze96U1sb2+zs7PjtAIPDg4cqLmyssL6+roTy4fjAKsAgOJ4rZRiNBpx9epVLl++zOHhIaurq7z97W935i7ve9/7WFhYYHt7m8XFRd70pjdRqVQ4ODjgoYce4p3vfKeb5D300ENorXn++ee5ceOGS1G/ffu2M5zo9Xr0er1jQF6xrRRZrAL8zszMcPv27WNAghgQCGjdbDYde7BSqbC9ve1cRYX1UmSlnkzbLbJA0zTlzp07VCoVer0e586dc+345s2bfOUrXwFwzC4BhqUfyX3Ivcl/aUvSLuV64ChtWYCwoqtosXwE0JZtBMAosnOK53yjfl681pNxPxDufmzfrxUnwcIiaHi/azhZH8Xj3G+bIrv05PFPLjAUQ9LZBbgQMCSOY2eOI/22Wq3i+75L0xXDkdnZWVqtlpNnEADy4ODAAWrFFP0iO6q4aCDjgACTd+7c4QMf+AA3b950sgJyb8Jg3N/fZ35+3oFyu7u7NJtNt83t27d55ZVXHOCdZRmLi4uOgS33Wq/XOTw8ZDQasb+/z2uvvcZgMHC6iwI27e7u4vs+W1tbBEFAv99311Vs60VgWtp4t9ul0WgwOzvLwcEBn/vc5/jYxz7GqVOn+OIXv+gYZKI3K+nHq6urLC8vOyMZYULX63UAZyJyeHjo2I7CtGs2m/R6Pe7du8cDDzzA6dOnXRrw2972NmZnZ4+xx+UcWmsGg4G7NwGvZMwCC9jPz88fYzILKClMfNFxFJBWzGk6nQ7z8/M8/vjjdLtdx4iUZ43WmtOnT/PKK6+wsrLCcDh0eobCnJS0cElXf9Ob3kStVuOP/uiPXHr00pI1DJT7G4/H7O3tMRwOSZKE8+fPu4WxmzdvsrKyQr1eJ45jHn/8cZ555hnCMGR5eZlKpcLVq1fZ29vD8zznhNxutx2QXnz+ClBZr9ePLbDVajW3mCjmYvV63emFSrkXmfqySCaLaHLsIjtTxvFin/pG4psCEH/qp36Kf/AP/sGxz5aWlpwTnDGGf/AP/gH/7J/9Mw4ODnjHO97BP/7H/5hHH33UbT+ZTPjJn/xJfu3Xfo3RaMSHPvQh/sk/+SeOuvwnCZ1YnUJJGRZDER1lhFEOPuST88y3IKNK7cTCGxtUggMWszAfmDMLHpickacTRVbSdv/cXTGpaIwXotKMcHtA2iqTtqtM5ktWFH5sXZ/TsmbS9tGpZRLK/irDpjsbg44SahsWlEsrPmnVJy1pdNKgXKuQXbuFCnyUp9EzbUyOPhPFmFLJsg/zMHFs3yuFCQPQGn04JEwyjK+tA7TWmMAjCz1mgiG3o3lezlYsmCM6aL4iKXsEPftgTcua8DB2TCotqaZoGMWo7Ejjjcw6WnqHHmkTaiphJ60RqISmHtlLz+w19+OSYxqqDOcircS9l7w+s6O6xYOkrPl3X3gLKPjR6/8dAF7P4yvK8Bvh2/GHmrCj+L9/6c9Z0CqC5gj+p69+DIxNSWxqhf/cDK3Agn4CWpqycinjUdPqDWLspFkldjunWZk7uGaBTbFLylYsP675lGpWt8AEPqVDQxpwDAzUiWXXpe06aWiB8NGSIWodOQjr2Kb7FoExHSlMYOw1pLacVAJRU/Nk9TaPl9ZZ8Qw7maEvFsDaarehscweMT8p2oYfA0dz9iFY1iBgEo3yDMrL7Nw3yftWpjCSDi1MQ6MgMBAVnUZs2zCpttvm5is6NjlwnWsg5rgj+msPZubEoR2rT1vn4qLTsZMrwIJ5WQGUPDJQKX52BBQKwOe0C+VHnVIojGMXS1EeGSeRg5I5wCjHzlmJJjdP0g4w5Lj5S96XUIrMN9Z4SGXUVEKoMwZpiK8yMqPwdMbWsEEvLbM9afBwqHmzGrObGn7t2lv5qw9+kbvxLA+X1/lgZZMZrwrNXQ7SIf/j3TI6jnLt18xqt9UNTX9Misp1D20hie5nWSWkKJa8iLPL+0zGy5bhNTqqDx1D/3RI/3zKyu/ZNNjD8z5BD+p3YfdJKB2meLFBZR6VbWveoWNbhq3XxoznK5Q7GbV7YwZLVaIWtG6kqAzufdjQuO47bUKdYjVpY02wb9N905Ii6EL7esZo7nj9kT8booasTqcs1AZ4OuPCzB6HUYWDYQW/FrP7eJX29YT61X0whp33LtK9BLW7UN9MqW6n+KOA8a6HN9GMFgMmcxlqPsakCjXSrp1XtmxfDwYwHvnMfckHEiZXlpm0A6qbGf7YoKOU6lqH8MYy8YMZyX6ZUTllYfmQwE+ZLQ3JUHQmFfaGNfpRiXHiM4oDKkFMyUsYJQEHwwomse2vXR2RGUW3W2KmNWBUKYM2hKWYSWzH5CzUkIFqNsAYausTtt5RIpgfEekyM897TGYsq3PrbSHLXzJUru+SzjcYrlSsc7qnSGZr7D1aob9qxzGUTb0OzvWJEo/xJCDaK1O741u2oCI3zLLjcdywLtjx6QhjoHyrRNQMqG9k1NbG9C822XmLHU/KGwGlfQs6xlWIWvZ+o5ah9ECXM40+Whn2B1WG45As1Xh+ymTfOmGjDOnYJ8s8OAzZVC3SkYdqx5SrEeO+j19K8OspaapJYg+dj49Jzsg2kUZP7CKN14yJtUElGmQxIbWdfDQKUdMU5m/LKAqZFxloxfRPOEqVFHBDwDGZTMlEt8gOkOOc1CWUzyW9WQCtNE3dZEkAPZlky2RL9K2KTBawYvoHBwcopZidnXXaSb7vO2Bpd3fXaXjJtRQNSgQcEVBGQL/xeOy09+TaBQCUSU2n03HbSjnIMWU7mezIBEnStoS5KI6V5XKZc+fOufOfP3+el156yZWZ3LNM2uHIEKLIyCoCQMWJloAnL7zwAouLi7ztbW9jdXWVer3OtWvXnOaViOTLJPmFF15gY2PDmUns7e2xvb3NU0895cwFfN/nueeec0DZZDJx+pTCgpLJp0zYi2Uuk3H5TJhjkootrq3FNiKgaKlUcil/oi920sjkJLAlQJwwmVZXV90k9vDwkMPDQ+r1uktVV0o5EFuu8WScTPuVfibnlH4jacLF+y2CNEVg8STYWgTPpV++UVrwGwGDcpyToN7JMhPQ7qRG49djLX6t+HpgpWxzPzZjEcAqXuv9zivfC5gnixdyHun/ktYcRZFzk11YWOC5557j/e9/P6VSiW63y87ODmDHm40Na6T57ne/m1qtxs7OjruGRqOBUopOp3NsPDpZVr7vO41SMQ4xxjgm23g8ptfrEUURy8vLjvU8Go04e/Ysd+/edSnsvu878EqMQiaTybGxRsB1YTBubW2xu7uLMcYBSgJiid6iPB+azaYDlYohfVgcjQU4kv2zLOPu3bssLCzw6KOPsrW1xfz8PG9961sdG/zmzZu8/PLLBEFAvV4/tvgizMrTp0+jlGJ/f9+xg0ulEg8//LBbHDl37pwzjxGg8uDggEceeYSzZ886zb2rV69SLpfds6XYh4oAlaRsS/nJolOz2WR2dpZer+fkPYwx7O3t0el0qNfrNJtNtra2ODw85IEHHqBSqfDVr36V06dPE4YhvV6PtbU10jR16cLlctmds1arsbKywoMPPugAatGBvXXrFo1Gw2nj7u/vs729fUyPeGlpyY3r4/GYSqXi7nF7e5tut8vDDz/sdASbzSabm5vs7+8zOzvLY4895lKvt7e3AY6ZowjrUZiBRb3CTqeDMeZYar+0j2636/q8/BdGrDxvxJFZdJSFSS71UmT7fqPxTTMQH330UT772c+698WG/3M/93P8w3/4D/nlX/5lrly5wk//9E/z4Q9/mFdeeYVGowHAj//4j/PJT36ST3ziE8zNzfETP/ET/Ok//ad56qmnvqnc62JYB8eMtOQdTcCxQJjONRB1nNk0yLmSFdRPMsJOYtN5DSQV70ivMI+0pN37LND5MY4Kt7wXk5Y9spKH8cqQGaKZEmlonSlVDobo2BDG1vkzDa2GlgM5U20NSJIMHWXEdZ/hUoBR4E+szmFypU1juExy6w5oD7O7b5mISqPLJVQYWNfeHHVWudgmvgdhYI0ncoMOhAGlFcqzmo+LQZdhFvLpjYepr9vr1HFGZsDzBMXKmYUa0sAjbQYMl2zamr2PEAwcPqCpbnj0V0uoiwNKz9YZziRojHVuVQlaZSRGo3P6RZijhyozaNQR0FMAVBQ5c6zwXebD3DPWEdSLTQ7GWBBY2GDGg/KucfqS9t4tmzBu5OxUZYXu7b6W6eIPc8fr3EUzxzpz4BnHrkpLOFadhD+EtGKBi3ipZctcK2rrkdPpBDtJH894VnezPyIpN0lqR+cV8MimzOfAXGYnp46dqYwDv9GKyZzhcrjFZwYP81xvlVYw4m/M/x6EFshWORjpUog9ew6HxBksqCeuyopjTBtjwCQKY/QRuzAHRC1KWwAPBSDMv3LHF1OWfF8xDHYLAHkRmdysyDqSy4cFrb7ittJOimDh61KGc5DPUyDSl1o5bVBJQZZrgQKzUL/+eEWDFAc2Fs4trElhHTrQqgAuCiB5TD8xr3e7nwXyPQHmgZAsB/Uy2nrIYrnHxrDpgKOxCRgnAbeTiCtBgIci9FN+486TeDojSnxeO/8Uf2fmGp7S3E48Sge2zztDKU8RLyQ81TnLc4enASh7R3qhvraA4igNKHkJ9XDC5p8dkO1XqG3l168g6ELUsh3Zm2RkJY+oCdVNg5+bf+goQwd2kaa2FVPbgtG8z3gBdt9UIQ2h1LHyDf644o6lE8PcUz7lw5T+KTt+Zx6k9ZDWSz7+2JCWYXDG4A8UtbUxUbNSGIOP6j5q5MxmZbjc3KET29SFejBhpjRkr1LjzpMK41UID5scXCnR+8AQz8/oX1IMX60x+5J9NtQ3bYOOGx7mQKP2SkRtQxZYd2Hr9GxTgcNDw+q/1agsZfstAZM5j9pde5zqZobXDjFei8qmYTKpUe9YHcSdrE1joU9iNNvDBnGmGUUBSaoZDEtoLyMpaauNmWm6ezXUwKMb1RnUyqQjD+/QZzLvU22O8XSG79nrrq0ecOd7Zzj9uRbNqwaiGG8Uk5UDi/HXEvqrHv4IyjsW6Du8GGK8OeKGx7ilSUoBSSUkrikGpw3m7IioH6BGHpUzPYbbNbL9Ot5Y0eySs3DtItFkFjLfEC3HlFsTfJ1RD2MGoxJQor6WUt6xDKDdxz2Mn1F7qUTpwC66pCGMlhTR5RFBmFiM2IAxigwohzFaZ2R52nfWmlCrTqiWIkZRQMerQT+wbtjK4PkZWT4WZplG6wTfjwnDBGMUcexRbkwIgoTedp2smkHJlmXYnhCPfVBQrsQ0qmNGUYBWhqGpMI1vvyimHhZTIAEHmEmKkoj1iwOjpFI2Gg0mkwm9Xs8ZWMjxADcpEHBEnHBlsiRaeQIWCdtFwMci2CSTB5mYCttnNBpRq9Vc6rKkJu/v79Nut537p4CSAm6JM3SRHSbnFrZJ0exFvpNUqnK5zHg8pt1us7y8zCuvvOJS0sIwdAyh2dlZlypWKpVYXFx05byysoKk40rK9pkzZ9jZ2WFxcdGlDxavqwj8FA1bBKAtgj/3SwlN05RXX32VnZ0d5+4pAJ8xxk3gxDxBKeVSdgX4KgJZ4ooq5gwisC/sVTjSzBKWXdHQQkCqubk52u02lUrFMWhGoxGHh4eOySn1IcCJpBoKg0rcmoWtImUEx9OXi8YkAtaIQUqz2URr7dhPxVTzk6DsyVTVYpkX9QZPsuOK7MJifzx5PLl26VPF88tfETS+Xx8vXlvxdfE4J/c/yfw7+b2Am0VA7+T5Tx7vfpqMJ6+jeD8nr0+iyPQs3kvxWuVc0m+KwL/sK0CV6MeJftvh4SEXL148Vu/b29uOMba0tMStW7ccA3g8HtPtdl3fkEWVvb09RzgSkK8I3goL9t//+39PkiS02233eafTodlsopRyhkjSXkQfsCg3IenGYqYkJkz9ft+BXRcuXHCLDaLZJ8YjnU6HyWTitEg7nY5jRc/OzrK1teUWcIpjjvS/brfL8vIyxhjH1paFlBs3bjAajbh06RLvec97aDQafP7zn+fFF1/kzJkzPPzww7z66qvOBTkIAmf21G63HcOu2+06Aw9ZcBAjp7Nnzzpmm6TNiq7kSy+95NyggyBwDMa7d++6Mb4IWAkb7iT4LCYqkpIr5SbjN8DVq1f5wAc+wPd///fzwgsv8MwzzxDHMaurq5TLZUQ7V4Del19+mbNnzzI/P++eb6KxOBgMuHbtGrVazS2KHB4e0mq1mJ+fJ0kSNjc32dzcJI5j5ubmnCZiGIZsbGw4cHV2dpbTp0+7vittpt/v02w2WVtbc67dor0r+sjyzO71eo4FLyYocixxrw7DkH6/77ICitqjgGNLF8cCabvNZtNlDgyHQ1emwoSUaz/J8P1G4psGEH3fZ3l5+XWfG2P4+Z//ef6H/+F/4M/9uT8HwK/8yq+wtLTEv/pX/4of+7Ef4/DwkF/8xV/kV3/1V/nu7/5uAP7lv/yXrK6u8tnPfpaPfvSj3+zl2HMr67Js3VoNRphrRkFiWRx6lKDHEcHGAaQZplElKwXocQQb2wSNBsnpWfztLqY3sCmnlRIYg6mVrWPxOLLMPaWshiBgQp+0GqLjFD2YkFabhIc5kivgo3N5hdJeYo1GsJP0LNTEzZCgM8bvjPC3YyqvQdasMJmvHIn8Bz4qCDFxBIGPCkN0s2kdfXOWG1pb1qFn0/NMybdsnAyMr8l8+V4R131UaohaHqeDfV4Zr7D1x8us9GKSmkcWVNh/xCctQeO2oX9aMbwQ0/xqQP9cZpmCpwekG1UWHtwlzTSdl+b48Aef5nd/8y2svHMNX2fcC+pUWmNiNGUVU1YJw6yErzKbWpd5JMbL3TxBEBxhfAkzUXQPM+9IsF7+0lCRVK3OllFWn8w52MbkgIGwwo5AJ8wROJgFlp2nUst4AWWBwJAjc5u8DnWe6u5NrPumjm3q5HBZk3kw/9UJ/jC2gHXgoaPUpr+moFKdg7AWkDXzOeOnXcOLrbFEFhpMaFCxBdSynGkouoQqyUFEK0RntQW1/W9mU875MbeCfTZKbZaCLhf8Mn45IR147p7dzwZXKFgwUfQMc7YSGTlL0XU2VJChPGMn1QIEFkBOHaZ2sh1rdJCRaYP2cyZCflxTMFXJfAu+6SxnDBb1Ao04e4teoO03OjnSOXSYJNLftHV1ln4moG0xJVocmIUlmFpGIDlgJ++zIGcHCqBrOEpF1kfXaRzQSa5bqI/KzIGcBZBQjqOO6y06d+ccXC2ay2QelJUhVhllldLQCpjwSHWde8M2a/0WrdKYsoqZKw34zd4T/N1Zq9tUChJqYURmFAf9KmuTGTIMHvD/Pngn9bXMpXBbDVZD9bWAnTM1Sl6KpzOSICJKPVKjyYxinPgoYL4y4E6nTbxeIxgqmtd7RLNllIHadkrnkgeecWWVlvP/oUKlYjqVp3omhvLdQw4vLoAyxDWI2obBsk9p1yPzFMbLrKlUBsa3jLOwaxgm9phZoGndTDCeZYnrKAd/QjsW6lTatSap+XgT2yGyQDEaltgcN+hFZUZJQGoUUeKTGdueB4+N2XrXmMAboMch0cTHD1Lii2MGBxX0BLIS1DYy2tcTRnMeaQlKB3YMLh8kVO90KV+ao9TJaL06YLBaZe2jGQQTlIKBCQg7mriu6K8G+EObrlveN0zairRkqNwO6PlV1sstmqWxBaQmId1uBXMYomcnVJsDWqUxcebRnxkxDEqoTJEeBsycOeRANegeVCnXIybjgEp1wmQS0KyNWTq/z/YP1di9NsvFT+yhxgkqUazOdzgYVugAlafs6qhKIKkpOpdCkrodT8ep9Cuobiqy/SppAJO5jOFOjfKmT+kgb/ORBeKTKvSuRHjVxC5c9UOMAd+zoPdkEKLmrbt38niV9vWMmVcyOzaqjKSkiBp2EWWymMDQhzDBGJhMAlTTWMOhTKMDQ3dUJpr4FgRMPYaTkMEoRPuGrGFTjdFQrkRkOcM6i63cSZZ5ZJmyeqJeRpJoggCqc0Mm4xA/SIgjn2gQ4pVSslSRpook1fha6M5M49sw5Ad8kflUBKaKrDlxPlxcXCSOY7a3t51uV7VaPcZAEhfMoui8/ElanYBI4lwsLJAim0r+i9tmkeEoumWS4ioaXfPz87TbbTY3N11KoLgtC+NEJjpF/caTzCa5D5kEC9AjE2YBk9I05cknn2RxcZFf//VfZzKZ8D3f8z185CMfodfr8fTTT3Pq1CmUsnqE7Xabfr/PvXv3KJfL7O7uOlByc3OT8+fP8/LLL7O5ucnZs2eZTCYO/JR6KTKvhOFXZGYWwZQio1LKVCmryXhwcOCOJzqC4/GYjY0NB9oK+wxwk8EieFQE6aQ85+bm6Ha77r6EvVOtVh3IC1avq9lsAtaMQoCjw8NDOp0O/X7fXe/J1HVh/khbE4BGQFABCAVwlXuQY0jK5dLSEqdOnXIp9VtbW05vrMi4nZmZcY7cRfZMEbCS8i1OjovMSrlGCUnfk3ToYlkVjyXHkWsRxm6x/xbrtsiyK7aVYt8pXqOc534AoNTzyf5RXGy4Hwvw5Pvi9Z4EM0+Cgyf3/XrsypPg4UlQUgB9AeJln8Fg4Iw9hOUkphxKKWfo0G63OXv2rFvQkP6ytLTEpUuXePXVV11fEgdcGfckFVbqVZiOMnYBLo1azIXEWENciQVolP4XBIGTe5D6FJMOGadlXBGQT9K3BTjyPM+Np1IWSZKwt7fnWI/CnixKDEgdlUoldz2nTp3i4OCAOI45d+4ck8nEMfgETLp+/Tq3bt3iwoUL7OzscPfuXTzP47nnnnP91Pd9zp49i+/7zizm5s2bDtQShmOtVnNj5+XLl/F9ny996UtugUYpRa1Wc3IEwpqenZ11jvGnTp1yY3+z2XRuwmJQIosXRYa9LHyNRiMHJA6HQ+BoXF5fX+eTn/wkly9f5m1ve5vTtxTAb2tri3PnztFsNrl9+7YzsJGxptPpuEU6pZRLB0+ShHq9zunTpxmNRrz88ssO2DPGcOHCBWZmZhgMBgyHQ7a2tpzsR7VapdPp0G63uXjxIq1Wi5s3b3Lz5k2yLGN3d5darcbq6ipaa+dUL+ChPC+lH0dRRKvVAnDGXiIzcXBw4JiComksGr/yeVErWJ6lktot44SM71Lu8nwXoPTkYsTXi28aQLx27RorKyuUSiXe8Y538PGPf5yLFy9y8+ZNNjc3+chHPuK2LZVKfOd3fid/9Ed/xI/92I/x1FNPEcfxsW1WVlZ47LHH+KM/+qM3BBDFCUxC6JoSFgDK9Q0ThVIF1mCuO6ijBDWJYTwh2dwC7eGfWrIagqcWyQIPMkN8qk2QZWStGtFc1ab0RRlefwJphprEqCjG9PuYNEPXa5jT85AasnrZAZkAiUfu8GmBAZXaNOCi4YJNjzYMz9TwRxnhwQR9Zwu9e0B1t87kwgJxxYPAR7capLt7eLMzkD8MVbWMKeX6hzlwCDmoWg3t/5JHWrLgoY4tEy0tWTOPSUsxpwesBAfoRJFWNDoybL/F53/6K/+Up0fn+eef/AgLT27ypsYBX+48xOU33eX6xiIL7T7r3RKzlSGhTjk8V6bhjxmdi2mFY5rhiI03N/ng2VfZT6sEKsHD0EmrRJlHnHlkRjFKAgf+FUEZAWTsDdk/T5hZBQBRdAEnbUVSA7+fpzL6lnliD2YPkZbs6ywH2rMg31ZDaV8R9A1RU+GNIRhYd1Grt2jZoP4oRUeZBVriFBVneIcDzMEh6cceYu8DE7xJicaa5yaIUtdivqNj4wwrVGaPGTdDwr4FcYTtKO7baIPxLHB4ZEaCO/7CuQM+tvo8wyxklFrG2ffXDvjB+rP5zXu89dwdvlpdordTJ9z0yUqFQUIZi99I7q0YpBhQQf6DKUwpl2PG4wBf9NPClGzsowceWSVf8Y21TWU2CmJt9cISRYaHMgp/oPGHivFyckQXVDh2nnNEz+/fsk1FmiBvG5lNs5U05czTaGNyTU6Vax0qB/qKAYuYs6jUWODROzoeOY6qMuxxCqzSLDdvsVhpfmx1vA8XmZjGU+g0O2IZcjwNWsKZsRRYk6KXKIDk0TaGpJ0iUz0N9DJDbDSPlu5xo27ZF4vlPsvBIe9ovcZvbT/O32y/TFOX+bmHfp3zfp/NtMR+WmfV7xCoCtvpgH934zGWdxKMVnjDhCzQDBY9hmcTfu7yvyc1mlClNPTIpS3vp3U6aZVZr0+oUv63a/9rSjuayo7h8HKdqKkIDxRhJyHzreOvKmhLqhywUwbH4jTa5AChR1yzwNTK7w+4++EaxjPoSc5UThV6YkiqmtGHe/Dphk15Tu0YUF+D/orHaEnRuG0Ie8oezxh0gpWwMAY8TVrW+MOU/llb9vpmhaejc6AMeickrWeoakK9OaIxM2Q4KJMkHnHsUS1HDIEwTCiVYqJGhUBB7+GY7hMZ+iCgeUNR20oZzmvLXM5TrEsHGbWNmGimxOEFD12JyBKNSRX+yOrxHl4xZGGGShXeUNlFjkqKKadkBwHEmiTTdCdl9no1slfqhIki8wxJUmZHGdKWZq9TJ+kFYBT+oXViBqjNjBh0Kq7p9XZreJWUg8Ma1dqYJPbg3ITJcoNwd0B1zYNHoBLGHIw8gmHubj2jGC+lObsZvImitG91AHVsx1eVWgmJYKgx2i6mpKE1ygn6+WJNCXQ5pVa1z/rooEwc+XheRjQJrHTCTETfCyjNj9it1ynvWqOepGpIKwajM/AgaFnmnwB8vm9NWkZJQJTawb+/U3O6rsOkhNLG6iSmCqUNfjUhGQSUg4T+qGT7ZT422uNyBAbiE8c+QZAQhAlBkFAtR4wmIZVSRH+Ys4kyjacMi/U+fT3DNL79osjgkdSkIoAn4AvgHCoFGBTAQfSfZFIgGm1FIFIMSkqlEuVymfn5efr9Pp1Ox00givsJwCFAQFFovQhoCWhWrVZpNBpOv0tAgGq16q5Za+20y8R1upi6LWUgk3E5v4A5AkIJe1HS2mQy2Wg0+I7v+A7u3LnD+9//fu7cuUMURezt7blJq+ilbWxsUCqVWFhY4ObNmxhjOHPmjGMNDYdDZxLw7LPPupQwYX0WU6+FRXmSeSjlU6zrk8BKUYtPNKyKZS/HEXBQyisMQweKiSbk+fPnj7E2BRgumhFI+rTU4cHBAePxmNnZWT70oQ/xzDPPUK/X+cpXvuImz8V9TzIftdb0+33iOGZxcZF2u+20LgXkkjYi7VHuAawBzKOPPuraw9WrV5lMJuzt7TE3N+fOKyD0zMwM9+7dc59LmqVEEYwWjUxh8Ep5yR/YVNc0TZ2ZgbCQpC8W60HKMooirl+/7kDe4vGK9Vx8L+1XrjEMQ2ceUgTwimBtMaVfojjxL35+kjF4v2u4H9PxfgBAEZx+I8DxJNBZvMciuFlk5hVBKQGPe72ec3RdWlpyANLi4iJbW1ssLS2xtrbmyqlcLlOtVllfX6ff7/PmN7+Z2dlZ/u2//beORb2wsEClUnGahcLou98iTaVS4cyZMywtLfGFL3yB/f19Wq2WA5M2NzdZWVk5ppUpQKfcYxHUFBBaWIqS+izMYGmXAjgNBgOWlpZ49tlnAZx5yMrKCpubmw4ckrFHQFFhWWptJTBk+7W1NXq9nitzWWABi7H0+32+8pWvHAMwJY17eXmZ1dVVGo2GAwvX1tbcolKn02FhYYHxeOzMotrtNgDPP/88W1tbTgtVXIuFNSlmOIPBgCAIHNDqeR6NRoMLFy7QaDSOMZ3l2Sep4zMzM4xGI7ev6BPOzs66+hT919FoxLPPPkuz2WRpaYmXX34Zpay8hmhE7u7uUqlUWF1ddcYuSinq9bpjcAqwdu/ePbcAJkBeUe5DHKk7nQ537949lvYrY3yRrXjq1ClWVlZcPztz5ozTwuz1em5MFaBc2r8sujUaDWeYIlqx29vb7vkl5y0u+BXbvYypYu4ierrj8dgx92XMlXFF2oHv+27R8JuJbwpAfMc73sG/+Bf/gitXrrC1tcVP//RP8+53v5uXXnrJ6SAKLVZiaWnJOZZtbm4ShiEzMzOv20b2v1/8zM/8zOu0F10oCIYZwWEMeVpaFuQuzLnmodEKsgzje9Bq4HseycYWWecQVa1ibq+hW02y1QWMVowvLeAPYsLdAaYUoA/6FnwEsp1dVKPB7f/do5z/xBpmOLJGIiYlqQdkvnI6itaNNddzm9gJbBbaidWR8YUFLjJfEdc8kkqFUvU0wWaP7O46YbePd/mMNUopl/GXl2xachig4gQTBmS1MipNUUlG2iihcnAlrudaKKGdKGNA+7nztIK0rIhaVt9swe/y03/pX/KrH3wXz710jtVLGzT0mLJKyM6Oeai9TWw05cuHXGluU/UjHmps8WJlyMX6LoFKOV/f40p5k7/69s/T8MaMs4DZc0PeWbcsqJqKKamUw6SKMYqSThikedq1pLEKFiPg4QmcS9hoEra8yZlsEPQsKzAtKZv+a46O68UGv3MEFPlD6+zaP+WRedC+nhAexqQVDx3nIGGuUahS+2DRUX6hSWZZqGmG6Q1IDw4odTPmZvuMm2XGA8+CxgU5F6dpmJgjkwytnDN4Gh7l8RltUJJenLdlPDDKOF1IAEopj89tsBdb04VJFvDbo2UaesQrkxV6aZmyjrnXa9tynWiXAi5pyapkTRkAtGdIewF6pMkqGd52aM2FDCRJlTC29xHkZjClQ8vA3HkPlO/61O8ZMt/LjVEENMXVpT/OiGua8ZJ03iwHl3IWoAcoRaaP6htw6evGs+m8KsoIdrrH0oqNp8HkJieBbwF1+S7wLLBY8ux5Au+42UrOKER+UBW+s4ZDFpwU4FBHqRtvVH7OzNPHNBkF0HQAMkeMRwFHBeQsGreoOANfOzaj0Tmr2Dv+w3CQ566XVcIHGi/zcMWuXi14XZa9Qz6tH+FTw3l+sN7l/WWAOmd9gAlQITYp/3P3EfwvNfAHQ3ufccpkuUpcV1QWhtRURNeUCVTC2AS5EZJ9eC74XWoq4hMH7yDbLpPUDOVrhp23KHQErWvWZAkNJjAMFwMq+wk6htH80cpAXPed+Ub/jA+qQTDIU1nLHv7YphhHc1Wb3m/sQowyMDqoUAosuOYP7T7juYByxxqyjGdzcNIzjGcD0jIuhdn9YNZ27IgXYtRI05oZ0O1WKO1p4tMjHljeoRmOyYziFRbJjCLwUtJMM9ccUA8iBnHI+kKK8TzOndshMwp92rB5pkH8dCPXu7XAZlJp4I9t29h4d0ipA0EpITIBeIZSxwKeZnGC5xnSWGMWM8rlmCTReF5G+8w+u4d1DgcVfD9lsl1l5o5lQqvMsP+oIj4s0S8lJEOf+vUAf2AlGoz2OAiaqFJG9XqI8UICBaGByUxGVs3oTTxUz8eEGcMlTbivqa9l7PRrjMcBamI1EnUGUTvDX7DCl0GQEvop3W7FLtQMfLyeR2VbEzUNOlEYbZgspqhIoSeaUscumugIlDZUQqvFWJ4bEU1sG5+f6WGMXRzcpkm1HDG+2CO5oCiHMSrxCJV1l04TjyzTVJtj4sinPdMjzTSNYEJiNKFOubk9B4miejeg9ZrV/Nx7HLwzQ/vcBpTOwBcGmEGFGcrL8HMTG60MWmcEucxHb1CmHMaUq2OMsWzDSiminv/1xiVKQULZT+wi55SB+G0XwjCr1+uOwScsP/lhLyF6c5KWJK7Iwk4Q0wvAmXkEQUCr1XIpxcLEEWDmwoULPP/88w4Q8DyPfr/vJs8SAl4JC6HIRpOJRrfbdalr4rwsbKNqtcpgMKDf7zvGjtx/kVFXBCxlkl1kGcpkSyYxAjbu7u7ypS99iXq9zrvf/W4ee+wxdnd3+eQnP8nCwoIT1xdWRa/XcwBRv99nMBg4w5koihxgOBwOefbZZ50OZLvd5vLlyw4EFaD3fmmRJwEeOJq0SWitnV7ZYDBgZmaGubk5x0SSSWiRYVMqlVz7ELacpCs3Gg1efvlll4otQGER3BGGF+D0HqvVKpPJhD/7Z/+saw8C0kp9w+vZZnKNYIGPfr9/jLlXBH8FFJD9hPHabDY5ODhwZSIsL2GggWUECSNrcXHxmOacsFnlr1qtOmOXnZ0dVldXmZmZoVKpEASBYzIJqCkp/HNzc67/1Go1187EhEAmz+Px2DlGS1sVMLkYRVCwWH7SzougrrTF4qKAtI9iWxHgWhYLTjIdi+yhYh8rtj9hJBe/F0BKFhlO7nfy/df6rsi2LLaXYvpk8f4E1BcXXmnvounWbrfZ2NjgC1/4wjHGrTGG1dVV3vve97Kzs8OLL77ormFmZsbd561bt9jY2HBjivRBqfuZmRkeeeQRbt26Rbvddu1CxioB47rdrgPLBHgXQE9AGnEUPjw8ZHZ21o11YsYh6aGSmntwcEC9Xmdzc9MxeDc3N51OnRgWCUN4ZmaGvb29Yzqlct56vc7MzIzT1hNJBDFFEVMrwPUBKQsBLc+cOcPMzAy7u7s89dRTVKtV3vWud/HBD36Qu3fvcuvWLba3tx24trKywtbWltOPbbfb1Ot1xzbtdDqMx2NnECPge5ZlzshjPB47VuPW1pYbZyWFfDweH9PdlXYqEgftdptSqUSn03HpzQKiVSoVbty4wUMPPcR4PKbT6bC8vOyejzJmzs7Ocv36dWfsIqYoch+AWwCDI6di0fWVOhZ35d3dXYIgcGZPJxnjcRxz/fp17ty5c4xFKEDvZDJxQJ7IQAggK2zNdrtNu93m3LlzbiFtMpnw1FNPcefOHXetnue5lOWTjGU5n9S/AI7SFmW8FImB0Wjk2n9x0eYbjW9qj+/93u91rx9//HHe9a53cenSJX7lV36Fd77zncDrB6L7rWycjK+3zd//+3+fv/N3/o573+12WV1dzXfOGSWZIfN17sSc4fcix1BScQpJajUB0wzCAF0pkw0GqCjGW5zHDAao5161P2DaLbKFNibw8PZ6mN6AbDhEBb41MIljzvxO34IUc22Mp/F6E9KFKll4xDj0Rzkg8robzsHFVACEnBllLKgUNQKixizBSpPSq5v46/uYfh9mZ0ApTL1qAZFJjCkFmCCfpIc+WcmDwLN6i2XPAXHeyOqbJRWFFxvKezHj2YCoYSir1KY9en3+7uqn+KO5y5RVwmbSoqon/MijX+FsaY/duMGli7uUdcxjtTVi41GfmdBPS8wEAyZZQFnHzPoDApUQqJQsN2AYm4A/GK7yD//4uyHRXLm4QWw0vsqYJL4DtcRhGIRhmKdVFoHEwnO9dTMmanl0LmmatzLq61HO6swsEAOkldztN84sEGhsWavUoCYxo9kZ1Hd02ao0mX8uX1GMrOGBjq02JVpBZsjKKnedtuiP6vRI9vZRvk8aKnwvJWoYdKQIu0f3Yp1pZdKoBKfKdQwV/ighano2DVkYhmBBQ88yA02Q758pVGYBUt1MeWb7NMuNHnHmUfMjNmstYs8jNh4r4QGrwR4bi3ZQ+/Wtt2E8TXBgmUA6AW9sGZcqA29iqO5kDJY1h2+LKV/3qa/ZlHUxcrGAZ5727VkWK6llFfpj3zlNC9NTDC6MjwUX5ftjqdGCuqnjwJ4xloEozsAmT1Ov+XjDMsSp3SYzR8xFzy4YqDg50jIcRzAc2ZT/VtNun9ep+58DkHwNHQjjWzYwSQqeRo0m1qgIMJWQaNb+wMg8bUdXR7LMjV3E4AUByQs/4iQdOtCvAxpVZtDlhEApGjolNTDMCzZFUdUTHgo36GRVBlmJmp7w3tnr/MrGu1k9+0neXjpatQKYmJh/N5jjH/3293D+2QnG1+hJSlby6a/4ZCG0aiO20wbDrMTYBIQqxdOGgQmJjY8mY8/UuFTeIWvFzLwQ4g8zjPYo7SkquzFxLV/MySCpQtZTeCML+pf2IWrBcN4nGFl24GheUd2x5ZQFhv0HS+gJxE0Yz/nETUgrGf1THs27CYt/4BPVIanb9NjwUHPwkMYbW1DQaKjsGIanDPsPe5T2bTs3gYeJrRGLpLqrMKV6PeCwWaPRHtJbCSn5GWUvpuLFHEyqjMb5okyg0NpwOKww8gOWGz2G5zp05yos17rsjOpMEp8HFndZe1dMt1fBdEIa1z2SsaZ5e8xkJqD5mmEyY8eDRnNEv1cmqcBkIYHYo9YYMOiVyfKU4EoptgYoozILrT6+zljbbVO55xH2M8JuhjdO6VwpQZAxGQeEWwHlXUPYz/Biw84TPpW5EaP9Cv4QSgeGuGGPPzqVEc6MSROPdGwXnbJAkTRK1O9F7I8DwjAlMVbnN64p0mpKtWwnsL7OaJYnLNb7KGVIjdUZHCc+80HsUohbpRE3O3Ps7TaYjEJK+4qkZqhVJ8xXbVpQkmnGSYBSNvV4lFjtwNn2gMN+Ga0NpTAh8FNqJTuoRKnHOPbxlKFRnnA4KrPXrdGuj4hyxvvdgzZxN6T9VZ+Fpwe2r3mK2rpm4z01kktjtGfIYuvQnKSawE+Jgox06BOXLbMxNVY+Ics0rcqYKPHwvYw0U7m0qsLkmsmXWrs8NTpDd1gmq0zoTcIjeY5pfFuFTGYlzQhwkwE4SlkVwEAmFLVazbE1AA4ODoiiyKWByiRvNBq5ibOkvMlxhY03GAzcRKGoQyggQFGbUT6XbQWUkG0ODg7o9XrMzMywsrLi3EaLZi0i9i7HkHsqAjHFY6dp6ibEknZojHHMk06nw/r6Or/0S7/E8vIyc3NzxHHMvXv3nEGBlK8AQhICsInm02g0coYtAmiI1uL73vc+nnjiCV588UVXjsLgGQwGx9ILvxYTTICVer3O8vIy7Xab3d1dFhcX2dzcpNvtOm1HOZ5SyjFd5DO5th/4gR9gbW2Nubk5bt265VhexbRqqcdiOm4QBNRqNecUOh6P0Vo7wFKAupPgWJE1l2WZA7+L2pClUsnpyRVZczJJFZbXeDx2k2dJcy+mnwpAqJTikUcecdd/5swZ55QtzFkBJpVSTmusXC6zs7Pj6l3ageiINRoNt79oZQLOvKCYdi6A0d27d4+ZDBWjCJzJ++LnwlxTyhpMCEtJolhXJ92gBTCWsjl53DdKSZaQPlZ0IxfZgjiOHdAv9f9GadLy+n4hZVLcr8iikjRIAQGFEbuzs+NkFdI05e7du1y+fJn9/X0efvhhXn75ZTqdjgNjVlZW+P7v/35OnTrFr/3ar7G1tYXv+ywsLBwbO/r9vmPlynULiCtl3Ov1ePDBBx3Ys7u7y8LCAi+//DJhGHJ4eOhceYX5J+zefr/vxjEBHKU9C1Al7XJvb4/5+Xnnaru5ucnMzAw7Ozu0Wi201o69K8xZASJlLBe2dREslgWMer1OFEWcOnXKAaCiAyk6irLoIuC41M1gMHBswfX1dUajEYPBgM9+9rOcPXuWwWDgAFTp69LnBPAcjUaOgSx1KdcrKcEC8omTu+/7jp3Y7/cdO1DAVkmX3tjYYH9/3y2ybG5uOgfpa9eu0Wg0aLfbNJtNx0A0xrgU5729Pe7du8fCwgJf/epXXftbWFhwsgiSstzpdNjc3HRjpBi2FFn3slhSqVSYmZlBUoglZbyYEp5l2bEMgZmZGbfN4eGhYxoCx54rskAjzzx5dgp4eOHCBV5++WVu3LhBEAQ8+eSTvPvd7ybLMu7du3cMNJTnv+gbF8eiMAwd+7IoUyFtTFjaorUsWQqSCfGNxjcPORaiVqvx+OOPc+3aNT72sY8BOCqnxPb2tmMlLi8vE0URBwcHx1iI29vbvPvd737D8wjS/UYRNTyX1poFucty7NsJubZ6haYSHrGLygGaedgGkySWieh5LjXY9HqowYDsoXNEqzMYPesAKLCGK3HTh1MVkrKicWtEFvqWDefblGRxWjaecq69BkUaKMssU6A0RynWeZqXZTBZkGE8G8CVZYI/fgXdqGNKgUu9TCsB2qVkZqQVa5YCYHKGTdiJiBsBWaAo7UUkNZ+kHeTGEFbfLakbNIZeVkGT0c3KVHVETU8YGztZruaIT5ajWlplTLKAFEWGYj6wqwOBSq1oP4qqSpn1+zS8ETU94XKwx8dvfB/zvx+y95aMQRySZB4VL2acWIDPpW3K89scpbY6cxwBGnPsKTyM8MceGx9VTOYCvDhEpQYvMtZoIc4sUCjMNAFxlML4Cp0nhZaCmMQoZ8SQiTlGkqfCp5lN7T1ppKE13mwbVakQDDN2vrREY81QX4sd6HWUwm6vQWUGUkNa9kjKgQUkowxvYsEXucfGNc8CHCGOUaty/cXqTsZwUWNWJ6SZ5v907reIjYeH4ZGwx6JXA45Wgf/2s4/w3tXX8A99VAr12xAOxNnZkAXWgAYF47Zm0gaTKvoPxkzmfYuDaQsiZkFmX2sDgcE/8FGRgmZC/wGDKqf4gR2EtCeDnQVcPC8jfqXpgFCrjccxgNilAivbP4ogvLAEjQdxDta51GZpL/I7xxSAZ2NQScO+zzLXBlSagrbMu6OTGLvQ4Gn7P7PAs5xbjfLJSgyMJ7a/hgFqFOFNQsuALpqw+NqmMWcm12nNTyM/RI055v7s2ru7dvs+KCVkwNgoPAxaZfY/Cs8YGjpiYEJqyv5/pLTGV4Lz/Ny97+X7F57lidIaLR3TMz5fHF3kZ77wfZz9tHGMWJVmDM9UiZqKuGb40KlXuRTskKIIyQiU/UuNwlOGAMPQeDwS7BJ/h8cv8AH2Is1bH7nGS5+5QtTymLSso3Ljhkd9LeHgio95a5fapxp4sR2vw0GKSqGyqansW3Orw+/ShB1NbStl0tLoRBH2MqobGqM8alspcU2z/e6Umec8vLGicblDZ71JZd0nrYA/gJlrCWmoCHoelR3jAH0AlRTEQLVlvwGoTkDWsi7nAMMkpOwlJEbjedZISGtDkngEQUKaWU3IM61DonqfzqRi2Wl5yqwvRlT1hOGKptSxLNMkZ1ECTAYh1GzbT2qGyuKQ8SAkjn3H5EvLeQq3MoxHIc2KnYy2m0MOZitwG/xRyuHFkLiZwUSTjkpU9hSlXkrYTRksBWQlQ7RXsf0214Bt3E3QkaF7yaNZG9MdlKEF6cB35ZP5ijT2CCoRQU8xaSm6DyVQylz67lxtSKBTMqMIvZRe5OPpjNnK0JVTyU/oxyVqYcRBkBG1M6I5MH6Glx6twqdGE2eaQGdM0hwkURmVIGZStoNC6KdoZYhSj9BLKfsJSaqplyJKfkIpSIgTD5XXxzAOGXXLhDs+M69GYCAte6AVfi9m6cuauzMh2UyE8uwzPMjPkWaawViMA2TSZZ2cd/s10lQzSTzS1LJEFZBmiij1OJjY1JDATwm8lNCDXp6BMI1vnxBATAwnBDAQRotMaItsAQF1jDEO/CmyHEajEVprms2m0zwCHBNQjru5uenYbO12m26368AiYb4JUCHgSTGVVT4rGmFIJEnC7u4u/X6fy5cvu7Qr2beoy1gEioosQ0mpFmdSgGazSbfbddpX4ugsOlmAcyOGI8dXASgFKKxUKu48QRC4Mi6mo4qgvEweT506xfve9z4+9alPuXKT88tk+iRD72T6Z3FiJhPMLMuYnZ3l4sWLeJ7H3t6eYyzJPRRBpZPlL4y7q1evMjc350CGIpOtCGgKaCBAijGGZrNJlmUutbHb7TqGnaRjFlNq5T7kOzhyCZfXrVaLlZUVp1l2UnRfjCYEjBUX3fF47Nq76Hk98MADzMzMEAQBGxsbjo0nDNcigCiMsNFo5Jimy8vLTrOuqCUnIOzGxgZRFHH79m36/T6NRoOdnR3XJ6XdCytuZ2fnWEprETArsgKLWn9FlqBSyk2+iwCg1HGREVwMKUOp2yLjVeJk+rOc/37sRHkvf9LP5DqLbffk/RWPXYyT7RRw/VtA8OJ2xWuXOpeFkn6/78wrZBwBOw489thjXLx4kd/5nd/hi1/8Ilprx8Lu9XqcP3/eMaaLLFm5Him3NE25du0aDz74IO95z3vQWvPaa6+59r+2tuY0+ZIk4YMf/CCj0Yjt7W2effZZtNbMzc2xsbHhdDRXV1e5cOECGxsbvPLKK06eYGlpiU6n48CymZkZp1eqlGJ+fp4HH3yQ27dvs7Gx4XQBpZzkWSHXJlFcFBJAsNVqHdN5LC7GCINM6lJYv7du3XJpxsIezrLMsQ7FzfjKlSusr687tqgsDsl4bozVcxTGWrG9yiKD1IsAd41Gg3PnzrG3t8fCwgK3bt1yCxoypnmex8LCggNE5Vqln5RKJc6ePeukKyQkdbzT6XDq1ClqtZpj/odhyN27d4+ZjAlIJmB4t9t1epHF8pudnUUpK/Nw6dIlut2u050VsFnYptKu0zR1msUie3FywUm2L46pUt+NRoNyuczS0hJ3797l+vXrrt+88MILtNttHnzwQQ4PDx17VsbDIvu8yDyWsUbGz+JzQ56PxXRqGbdkQeMbjf8kAHEymfDyyy/zvve9jwsXLrC8vMxnPvMZnnzyScA+UH7v936Pn/3ZnwXgrW99K0EQ8JnPfIYf/uEfBmBjY4MXX3yRn/u5n/sTX4dLA0wKmmGQp6BmGM+zQIExFoTLJ/x6dsaCh+UyxBHJ1g66VkXPzcB4gr+2bwG7WgWi2Dkap6UaRsOkofOUOgOeIi0LrSz/pyzLMAssWIcxzoFZgJHUy9lf6VHKc1rKUyFTq7+m52fJ9jtWa5HQMs+ihLQa4o0TVJy6SVDQmZBWA+K6lzN9FGlJk5Y96wqag2PWVAWyekpbZ6yrhLV4hjm/T0OPaHpjBpkdpJeCQ1I0VR2xn9RY8g8BiI3PrNfnxmSJYRqSGcXt8Rzb4zqHUYXdfo3Dgxp0fTCK9lVFpZ+hI0VvXGLLb9AKR8SJR5CzMYynXu+ymwNKJydbOrVsLQwE5cRqRaIdkAza6tdlOUiizDGnXTIx5YBaGDMe2/RGAJJ8W63QcWrToUX30GDdu+MUUy2TnF0grfiE3YTamkfvHNQ2lHWtBuem65x6MwES7an8UYpKs1x/D5uirA3jBUPtHvhje/8CoBoNSUmRlmG+OmKc+FydrLAdN2l5I875L3Ej6/MvO2/nb89+hRmvyvCwwu+88mbCviJqGvafMPgjRVLPMIE4dQC+QfkZ3mYJIo3XijC1GJW7lmKU9SfKwPMM2svI9nM9Iy+DsjVMice2zknsn8oUKlGkE0UwUiQthcGey5mk5FUjxkGW9XgitSLXM8z83DjJU4TdGG+QP1yyzLJypSvmbtwqyX8AhR4qtmn9Ks3IqkFuqJKfK29rKsmO2IGa46BkwehEZU0yX5OFHn4/cjqeR2OTcvVu6/7191LURjQ6N1LJXV9durmnmGkMSY1ldQ3z9GXLBvQoq5heFpIajdYZZWJQ8N2zX6Whx/y7/TfxTzvvpzcuMeiXqbxY4eKXJ2BSd4+D1Sr90x5pCMmFMX+6+SyByphV1sl2bBQBhqCA9lZzRO4HGs/zQx96nrFRxEbz0v9qmf/j7/ww5Y0A/00dJi+02HvU59RH7vK3zn6O/8PeX6L1YkDcAP67HfZ/b5nahiEpKw4vlmheOKA/KDPcsCBx94GM7mWP+aehtmEYLHs58KmYzCj8EXTvtvjZj3yCX99+G6/sLtLvlRktlmncVpT38sWAMH9e5OxP49vyzXzDwlyP7ZUSOobxKy3q+4p+KWTYtOWaZJowSOj3rQ4iRhFNfFrNIcM4xNdjqn5ElPn4ZCyU+5S8hJejJauZV0rwHhizFzRovwphN6W36hN2Dd5mSHQmI+sGOdFWEZQTssyyE3XVDpCjYUipbPtjnHrEqUctjEiuHLCj2lS2S4wWRW5Bk5UyxvOGsKsZzmu6D9i6C3dt+9GxobadEvQT0lAT9CzjzjZQA57h8JKifBDgD1OyQcColNC4DYMVCNoTtJeRpgpjfPpRSKs0ZhiHxFlKLYjwVcY49elGJWvy48VE+AzJCcazEeeX9xhEIQe9KoM4pOJbtuJuv8ZivU/gpSSZJs48qoE1AxpMQjIDoc5IE59KEOPlAPc48an4MWU/IQ7sj1FfZQzjAGJF+xXwJilZ/tz0JinKGMJORPPVGr1LISxPQBlrfOJlVMKYARW0zlDKakGOY59xFLh2UAoSJrFP6KfEqaZRnhB4Kb24xEJ9QMlLmKQ+c+UBm8HS0bNoGt82IYDGSafk4o/4ImAEHGN2VKtVp5MlfwLgiWuzsE8kLTRNU5dSLOnTAhzJZL+YJicTcZlQFcE+mYScdNQVhoIwNWQiJNpO8l4mdwLwCHgnE99KpXJsMlgEM6WsxL1XrlEYFcJOkwm33I/oe8nE6OLFi6yurjpjAAG0ZH8hJXz1q191LBsBe4sOoSeBQ6mP4vuTE0W5z1ar5RgwwsArgkQSRVBKyiBNU+bm5lx6pEzSpW6KdSRtSuq13++7tOnf/u3f5ty5cyilnC6bTIDl+ov3VQQGpT4lVTmKIlee0n4FFBZGpxgDDAaDY8wdqb+ittyFCxf45Cc/SafTYXZ2ljAMnZB/kT0px9nc3CRJEq5du8Zrr73mQCQ5twB/xfYn2ov1ep2FhQW3PeAYqWLCsbe3d6zPSbmcrK/i58U6FFaS53m0Wq3XgXJFd98iwF0E+cQ9/eQ5i8B+kXV4EkCUtiEmQJImfvLai3E/UPyN7rX4nbCGBdQoso2L7bHIchQzi4WFBSe/IACW53n82q/9Gk899RTGGBYXF2m1Ws65+MKFC3zlK19hZ2fnGGhfXFCR9+PxmF/91V/ld3/3d50By4c+9CGeeOIJPM/j9OnTbjxrNBrcvn2bhx9+mNu3b3P58mUHqH3mM5+hXq9Tq9WoVqtOm25tbY3HHnuMxcVFfvd3f5fd3V2n67e/v+9AsXPnznFwcMCHP/xhl57r+z6f+tSneO6551ybLaanynhdrVadnuTOzg71ep0gCJibm+POnTuuH0vbFxZoMS2+CDQX26qkORtj2Nzc5Lu+67uI45j9/X3CMGR7e5tz5845aQxxcC+m2ouZB+DGPAH9hfk4MzNDq9Xi3r173L17F8ABjGfOnKFer7O7u8vm5qaTy5idnSWOY3Z3d6nX667tC6N+d3eXJElotVrs7e1x9uxZ2u02w+GQlZUVrl27xuHhoRs3BYSVviNlIWOnsFmLDP1ut+tco6MoolwuuzT84vgvbONiWnZxIUfGlkql4sY9OSbYZ2WtVmNubo4sy7h27ZqrL9EXffHFF3nooYe4cuUKr776qmOLCiApbFUBkOVPtisuXsh3vV7PgZRFxr6YC32j8U0BiD/5kz/Jn/kzf4azZ8+yvb3NT//0T9Ptdvkrf+WvoJTix3/8x/n4xz/O5cuXuXz5Mh//+MepVqv8xb/4FwH7kP9rf+2v8RM/8RPOGvsnf/Inefzxx50r858kijp3lnWI1SfL8sLMrDaVZVFoy/gp+ajYRy/Mka1voudm0XFiTVEOe2S9HqpUQgU+Smuy7V2ywQCUInz0QYZLbaq7KZmv8LpjkpkqmWdThNOSQiWatKLQkcGLyI0zFKNZjU4NOlb4Y8uUM17uSGoEcMjBIgFBxhN0q0lctuCn159gyI1R8FG+Jivl6WYVn6Ti5SChcoCYBQyt1pSkoSZlBX7G0EBNRbS9IQEpsfHZS+oMsxL3ohm2Jw16cYlXdhcZdMuYiYff8TEKlh/fYv8PlmndzNCxNR3RkQXs5icp81mGSsaWweVpdJyy+6YGXj4BzoxmHAWE6euZWKJVCBZINQLuGOMYXSoxpDWPeKAJdz0yz+oaehMxz8nQ+eQQcmAoPXrg6nFE426Zvc+eIhxAMLDsNJ0zAlWuf2jZSgY1ScHXqChx6bVp1WcyYxmo3gTSimHvUZ/FZzLXLlGK1Lcs1iy0qcNZeMRU83pjstNV25Y9A4kivTiid9Gy+IyRA9mUuXTk4e8FVIOIONM831/lsdo9zoe7rKdVLgcj3lS9w4xnGQoPnt/g9u1z6BiykkHPT/DChFKu55UkmiyzDKNs5JP5FjwIgpRxPyTYqOCl1iBBJ+5S8IfWlTquewx8q+FWu1om6Fs00JvgGISZD5W9jP5pj9EZa7RiEu0YiA5kzRcBlAN4j0DlYgi47PUn6E4/HwyUNRKKC/mBvofxtNUx9fOLMQbje5h6xTIMk8LqrraSAChlX3vK1b/si1JkJZ+kER7pi8a+ZbLlsgEG5ZyXHYioj/QOxWCJo65+BDA6ANRKMhitaJXGpICnLGs4yzRxXrg1ZR/kY5XmgGJCL7NpZd9RXueR5U1+u/kgn9t9iGfWL1DeMQyWA7wY51we1xRpaFOBf/SJL1DVMeUcINRAUEA7MvLUaWUY5sBibMDDMEbxaLjJD7ztGT535wr/t8f/v/zT2e/i5bVl/tbZzzHMSnzkyRf5nf03M/umHf7+A7/F3xv8IL0XWiRXhvivVAmN4u8++Wl+pvenCLd93vH2VzhVPuS3Ou/EaEPpiQPiL86g2xGl1SHjP57Dnxuz7B/yM6u/SXTGgqy34nl+8rM/Qv2WT1pW+IP8eSEp4iKRkCnSTGPKKWkVyu0x7DXQQcZyrUuU+mQo+pMSfpC6poBRLDd6VP2IXlQm1BYgqvkRwyQkMZpRFKC0XcDQOsPMxEzmSlRvdjG6SfecT/M1w0GlhD9R6AnWwARQOnMuwFobgjBx3wnQtx9X6O3V0D4MzqZ2QSBfCCDWJHXF3tsMqprY/uZlpCMfb6DzMdmQljy7MFEzRImHyRRa23TcpJblizKa2i0ftZQyWrBt2Q9SymHMOArIMkXJS6kHE+rBhHEaEOqExFh24EJ1gJ+3p3owIUNRrkREk4Bx4lMLI0algJKX2FTowOpOApYBmEte+Dpz+oNJ6jHM04Tj1EN7htBL2T2sk6QaTxsC3+pVzpSG3O228A98qtsxma9JQ/s8JmeIZ76mdTsmagdMThlrrGIUxijSzK5yKGUZ61Hi4RfYkUmm8XKDlijJ/6eeNbhXhjjTdMdlSn7COA2Os66n8W0XRfbRSeH04qRaJtmiy1cqldykIwiCYyLowqQolUocHh6ys7NDo9HgypUrLs0uDEP29/eddpVoJkraaKfTodPpOFZLq9Vy4KMwXIppjSdNIYqsRmFzCYNP3sv+xYlbEXSS+5bPAAdwSjrg448/jlLW7bVSqVCv16lUKlQqFadrJUCK3OPGxgY3btxgdXWVg4MDBoMB3W6XyWTCYDBwYvaSZivuqu1224F/Uk8yOSwyz06ytYp1Ld8LiCeplOL6KgBt0WRFyqiYggZw7949arUaL730Eu1222lmyaTxZH0UWWVFh1EBmbrdLufOnTtmMHOSLSYAlVKKSqXiUhONMU4jUrLHhA0rE1XReNzY2HBaacIirVarToMLrF6n1Gmv12N/f5/5+XleffVVB0AWGZpi3CN1PZlMmJub49KlSy5NtNfr0ev1CMPQpSbK57u7uzQaDQdqCotIQHpJIRTzoZPtvAgunKxrieJ+kkotrMZiHZ9krRbrXs4jqdYnFx6K7aPYFqVPybWKXpykmRbBymLbPQmGn2yDJ+/15L4nmUxFILxYPlKO0u6jKOLOnTssLS2xvLwMWGDx3r173Llzx7mKi3xCuVzmHe94hwNTivpzxfPJ51IenU6Hw8PDY6DQRz/6UZrNJltbW5w6dYowDPnc5z7Hyy+/TLPZ5IknnqDdbvP000/z2GOP8dGPfpQwDLlz5w5gZSUeeeQRlpeXHWv6fe97nwPrhLEoJibNZpPnn3+eL3zhCy51+cqVKzz88MPcvXuXra0tJ+Ug7aHIfh0MBo6lKcBa0URDQEMJAXJlMUWY2gJeFZ9DslCxvr7uwM719XUuXrzI9vY2a2trLC8vs76+7oA+AeAEBCsaZUl9VyoVZmdn6ff7x1h+RQOY/f19tNYMBgPnbC3M63q97kxyyuWyG79lLJJ0Zt/3nbv23NycY3MWmbcCnBU1SYv9ScZ8KTu5H1lMEkkR0U6UMUgW4KTNF8ta/gPH3heZ1EmSUK1WKZVKzM/Ps7CwwGuvvebcluW4IgUgCyDi/C11KItrxphjLHW5xiIoWuy7AlILc1+YpSelLb5efFMA4r179/gLf+EvOD2Bd77znXzxi190qPzf+3t/j9FoxN/4G3+Dg4MD3vGOd/DpT3+aRqPhjvGP/tE/wvd9fviHf5jRaMSHPvQhfvmXf/l11O5vKsxx4EFcTHWui2ZyBoJNXy0UkDFWywysLuLsDGhFupEbuiiNbjZI765Zt2ZAeR6jcw0OL2nq9xQzVy2omIWapKzonxHmlHWGrG7YlOY0VIwWFIPThtqaIsgMSUnhRfngnUIaKMdM1Il1JC1GFmrLYkoyCwgGFnzR5I6mqQVIEXdilad75r8RLDtKjnUELv5f7v0ZvvD8Zby+hzcGf2iBMG8C3tgQ9g3+OGN+YliapOgoRg+H7L25ydbpJnO3rfaWTizj0wJCeYPV5AYV+Upp4JFWDPVShFaGxGhimSxntjIFTMo8y76xGohHqakCuqkMyAzh/pjz/58yhxfBiwxhN6+rzORGKuYo9Vip/Bw5WKk11dcO8KIW6+8tUe5oauuRbUO+nThqhE2aQcXq5BkVoMcRWaPM7mMlRqcMhtx1NFIMz6QM7/lUt2JMWDDzEGBI2TRco3Njlsw6/3ojhb/v4w8U+o7P6HSKv+PlwKltV1kjIxwpGrfhVf8s5Qs99HzGb2y8hf1hhb975TPciDxeHZ/iN/fa7IzrvPbHZ6FknZyNZ/D9lHGvhLdnJ7J6YjGr6qGivG+YtBVxU5M2Ikg09TvQvh7hTTLGCyHjlr2n5u2I0maPrBJw+GCD4aKm1DFO7zEYGqKGJvMKYLCfF4QQH8VARZq7MSiUNa+ROrO0LFsGJ9moxljQMEmd0/KxSFKUDIzJ0UqcSjObvhzFNlVZPj+5v2gkyrmUsp+1amQzpSNHZqyenkgQSH07oEAVAEK5B5dSn29iKDCYOUq1zo/f0B5eltJQCT2VMk6slECEZmwCIuOhVcZO2iDKwUW7nmJ4Z+UGbz1zi99uP8L/q/4eas+X8UdH15YFMJk3fOB9L/CRxgsAjI2Xu6fn2wCxgUBBlCkyZaiK633+XVsndDKfH5n5Et/VvEpDj/nfn/4ctxYXqOkJnsr4QOsqfAj+1MxzlFXM33r4d/nquRUeq63xPy++jf/N6udZ9jv81Xd+nlf6S/zI4pcIVcrGh1skRvN98y/wz8vv5QdPvcKF0g7/PHwvf/nsl6ipiM3UguYpmpqe8MPv+mN+PXon5S2NFx1JGQgb3MpfGIYT61Ss+5r6qQm7D5dYmOlzOKkQeHlarp/geZkdF3RGHPlMUp/Z0hBdGlH2YspeQsWLqfkTSjphp1ZnEISkmaISJGRNzf6DLaq3IRgkHD6oaL3iUbuniOsQzVgjEmMU5VLszDiixMf3rbHHeBwQpx7GKEa3GviRIgvzhpMqCAxm5KPKKaaagrYp8KpsmByWLeAdKWqbKUnV9uXRjCatpoyHIVnkWUpwoijvanSUEhxGtK9pNp8IUS1D6UAxGgeFdF5FNYhIMo8MlY/vHr2ohK8zan5kQUVjy3KUBJSChOFelWEtpBFOCP2Uqh9R9WOizMP3UjydEerEsvGVoReVqPgxcaaJEp9GecIo9gm8XEA69QiClF6/gvYyZpsDosRjZ1xnf6NFc0M5J24dZ0cLkJ5l5o9mfSYzFqDM0Ja5rgzjKISxR1T2XXtIUo3JrNv8cFgiDlK3vVIQJRB49rNev0K1OqFemuCr9LjW6zS+bUJ+oN+P2SdMiiLgIwCb/MgXFslJJp6k3cmkRzQR6/U6p0+fdkydTqdDt9ulXq87bUKZtMhkTkDLcrns9AOLBiICrhRDJizCPCnqkBU1yE6CEMKoKLKqigw6+Uwmznfv3uWBBx7gXe96lyuj0WjkGJ2iu7Wzs8POzo5jm0RRxNvf/nbG4zHPPPMMv//7v+9SuOTeitcrk8tTp0651GWZWMnE7yTrT+6tOAEv1rvU76uvvkqpVOLevXtuX2FcybZFtkrx2KPRiD/8wz/k8uXLJEnC/Pw8k8nEGVOcLFvA1ZtoxrVaLcf4Eebla6+9xrlz59jd3aXb7b6u3Ur9yP1L2U8mE06dOsXs7CwrKyvOPEImtJ1Oh+vXr1MqlajX61y5coV79+5x+/ZtHn30UZ588sljaexhGHLq1ClefPFFB2LJhLnZbDpTm8FgwGAwoFar4Xkeq6urTs9NNCGvXr3qJv6ioXb+/Hm01s6BOY5jbty4wdbWljuPgGvlctm5+55kaRXbcbG/FtmJ0neLBkGyr2jCCXggQFqx/RUn7MKeLKZvF4GQYj8p1rm8rlarVKtV57xaHFNOsiqLbad4fycZqbJdEdQQsEvAslOnTjnJBmHDAW5cOQm+CwtLzFBmZmZ497vfzaVLlyiXy9y+fdtJFpw5c4bHH3+carXKZz/7WXq9nrt/Oe7JdO/7sYY9z2N/f58/+IM/cODu8vIyYRhy8+ZNhsMhzz//PA888ADXrl3j1VdfZTwe02q1nOnJa6+9xtWrV10b/vKXv0ySJHznd34nQRBw69Ytzp07R6fTYXt7m7Nnz7K2tsb169cZDofO9X19fZ3v+77v43u+53u4ceMGd+7codvtHltYkjFHFgEA1tfXKZfLHBwcOBMVSdkXgKw4ngrIdtJRWr4XWQtjDNvb2zz00EOsrKw49+z19XUODg6OadyKiYzsJ8ZBRRmOarVKpVJxgLwsSEjatbTpbrdLGIbUajXHmJyfnz+m/TszM0On03H3MJlMnPydMIzlWTSZTOj1eu65VEzrlvRl6T9SvrJIJKDscDh0mouix1jUK5ZnlJTjSfC2Wq06VrZcszxLZIEtDENnqCYLeKPRyJnXSJuW/Wu1mlsQkhRkWZgp1mHRpE2uUxac5LdIkbFeZL2evJdvNL4pAPETn/jE1/xeKcVP/dRP8VM/9VNvuE25XOYXfuEX+IVf+IVv5tRfMzLfTvC9OCPJtZFc2mJqLGMsTiHwyHyNjmzarymHZNUQXa+QtCr4d3dJt/dQgUX8daWMGU9AafeZCnzSkiauG+KqIqkFeL0jAefWaxlpoBwKkfkGf2xT8/yhoXnDgkb+qMC2U1Z3r+jaK+68Os1XjsohaSkH2owFKrIwTw3NlAXbcp0qcfU1GgfcGJWnUvvKXtPEYHxQnuELz13mwr/JIEstO0rlrJxcsw9zlPark1wTLk7ZezJDr1Xwx5kFw7IcE4lSa2IirLEkswBODgKltRLNPJXN1/bitKQtZ7hJvS0HO7FLS5bt5oBJimCMobQ9ohFqDi776NQQdFPbBjA569QCRiYzEOhjWoam5FPa7DP3os/eox4qDSkfJLku3JE7Lkrbe0GhTJYDxz5B3+DfhKBvCEZ2n4PLPvuPG0pdz5qw5MCvAEbKWN1Bdw/aMlCrm4awqwi7htnnO+y+pU19PaK8OUB3+iRLbfaeqOOPDc3XRrRu+Ow+0eK333+F4Xqdc59M+fn5P084yMh8xcFlj/kXE84djLj9pyq5gYTVIlR9j6U/zqjsxAxWQpKyIhhktF7qWJOJQLN/u87hFVsHWaBJ6h79Ux6lTpa3zwxz6x7e3Cwz/QneY3O2vkJcH/BHmXO+BZtGqlL5YYY1hBGAVLIn8/ReV9dJlpujGLzE5LIE5qhcjXGsMrLc9KY4AZLvi6urnUOU72F6fVS1esy1+VjIvlFsXwcB2c4eqlyy55f+YYzVygQHEBbBw6K0gdyj61f5a+PlDsE5eCoMRjJY+61z/BXvY1xubHO6dMCC36OmJ5RVzH5ap6ximnpMVceEpAxMiKcyPKCqDIGO6Bmfxyp3+ctv+hKvXFrimXtniA7K4GcsnDrkr1/4Iu+s3CBQGTWVkOaVmAIeECpFNQcSq55hbEAeO6FSjqXY1gkBGVW9y9h4NL0JbW/I2AS09Yhlr8tDC3ahZmACHi/f5fHyXWLj8z9e/F/ceT/aeIHvbrxInKds//jKp8mMJsLj//rAJ2nqMQMT8nNXft2xMBu5ZmtsNLHy+MH2l3nXn7rOZzuP8of/4q3OdAtyZnhkGXbN6pi47ZENqoyigMbsgFqYHyv16MchM+URmVFEiYdWMATHkpsvDdiPqgyT0IJfOiUxnjMO0cqQGcVsbcjuOxV7OzO0r49Aw+FDKfNf0XhjRXZlTClMiGIfrWxfjRKfShhjjMLTGUpZt2JPZwzaFfROgI4UfseOdTq2fSmpePgj61LtTULLAjZgAvD7tk3KwtVkTjFz+jAHywImkwDjZwxPaYI/jPG7Y+IHqjaFN7Yak0pBtRxZFnOmmS9b9kM3qjBTGhJnnmUhZh6J0Q44VMonyTTN8oTDasJhp4rKdQZ3R3VKfmJNVKKAfmRT1Q4nuVGDMgxy8NQYxSSxacQ974idOFsbElc0OwcNtveahKWE66MSwZ5P2LMyIWJipCd24SFDEzd89h+FbCEi9DOSsWKmOkIrQ6M04W6iadTGTnsxzTQTP2XohQRBDnb6KaFv3ZYBAp0ySX08bWhXbPtJjIeOuc9qxTS+1UPYC7Va7ZiRiqQVywRcoghCCLNOJmRRFNHtdh3TSiY9nue5idTMzAy+7zv2ohiOAI4dVEx3FJaG6E2J6Yocu1qtOhCkqJlYBEVFt0lABAGeTk5Si6YAlUrFsdZk2yJjAuxk5rXXXmNlZYVf+qVf4tq1aw5MKQICsl+R1ffAAw84ULGomSWMsOIEvQjGCMFBrrFWq7kJXDFlrQj4FNPBZGJXvK/RaMRzzz3H/Pw8WmvOnDlzDEiSezjJ8CpOOO/evculS5dYW1tjcXHRGaMUwa3ivchEXsCxlZUVsixzGnUrKyv0ej0uXLjgdLZkvyLoLdfTbDbp9/tsb2+7Y9+4cQPf91lbW+PLX/6yY3+ura05AOzw8JBz5845PbKrV686cGh7e5tnnnmGhYUFdnd3mZubo9vtopSi1WqxtLREt9tlc3PTMWUl9Vgm0svLy449KmmFkiot7X5/f5+1tTWncyfpkMLolboSwE4MVCTVsGhsdD/23kmwp7gYIKncYsRwkjUoxyouHhTbg/RVad8nz1c8l7DTlLL6ZfV63fVvYfQWgfPidchxT8b9mI732/7OnTu8/PLL/MRP/ATD4dBp6onDtri4DwYDl4ov4Im8l3jllVfY2Njg9OnTPPbYY3S7XWc4EkURX/rSl4jjmPPnzx8rs2L5yGspkzdiU00mEwcOTSYT2u02jUaDvb09Dg8PMcZq0QpTV4AoOeZXvvIVNwYAfO5zn3Nj882bNx1QfPv2bQDm5+df13a+9KUv8cQTT/DmN7+ZdrvNF77whWPjoPwX1puU/9bWljNKEtdiYfdJ+rwATEtLSw7Qlv5bBBijKHLnuXfvHjdv3uShhx7i6tWrtNttlpeXeeaZZ1haWnL7ihRG0TRHACoZ18IwpFQqOUOhMAxpNptuGxkj5bolxMhG2IkCkt27d8+NR9KGlFLMzc05cFHAtjiOncatPMtqtRrAsQUCeX9ShkBAURlDBUwUB2ZJhxezJmHgyjNIwEkZR5RSTn9SJDgEBK9Wq4RhyO7urjtOu90+JhdQXDh55ZVX0FozMzPjDG1ESuR+1yEh72XcKKZuS5uWNOrhcHhMUuHrhTL3G0H+/zzkB9WDf/vjeGWbLukdjUW5aYVl8dn3KgfGcCwgldq04Sw4SoX0R4ZSNztiMXocE90H+1lvxWe8iHOwDPoWDIqrRwwi4+X//SPTDrAgYVI+Yh5KurIFEOUkOFDRH0PYs2BN1LCAk0wOsxCrh5hAWrb76dywJfMsg1A+9yJIqjaF2ZtYM4GopRgtZQQ9TW39yGBAtPYs6GaOJjmmcH8auhet22l4aPe12+fGMAUmlXWRzcEhA7tvUsRty4oBCDoe9Vs45uhRPudRWZjcQKQYyhjCrnHb6gQGyzadVD7X6dF1yD7uHjn6ToCrwYpNLw475uja06N9HbhaKAfHZFJYTTVj2VyDM1BdV/gjc+xccl1pyU7eyx3b5sazuaB5ADqC6q41SvHGBh3ba0kDSCsWwQn7FqxMS4r+WQsMN1+zn+vEEFcUh5c19Tt2//6qNbSImoa0YvCHivpd8MYQ1+x96MRQPhA2nQWkh6fsfkHfkPmQlpV77Q+h1M1cmvK4nVOvm7ZMwp64JluQMOwZJi1lte8A4xv8vqK2IX01r2tpP4W25/QJTQEY15bl6E+O9pc+bgp4oHP2LiyyWAOJnPV7Ejs8cW5hFRe/N54iqou+Za7JWWy7f8I4yUwSYFUn9n9cs4B6UoW0ZMhC60xsQgNhhg5TlDZksb2pRmtkmVJGkWWKLNXEowAdpmhtSEY+ys8IyglaG0eyBHK2nZBA8z5V+B8lHlrMR9RRuqcxuPNJZJkCo/B8e8ws1fhBSpLoE1RNG3LcLLNutkpJSmleTplG6QJzVNZjCseR83ueIU005a/UCA8N/sgu1sR1y7bunVNMZiwbLTjUpFVj239VZAhy9q5vUEZBahl8AFlobNk76rTcQF55qUJPdD6O2XRpow21ux6VbUP/jCJuGBq3LDN8cKbwY1PZY6hMORd0AJ0U3itQicKLoLRvF6oAt/BhU3MtQ921YZWP1fHRuaKmYng6c2Ojiu3+3gQaNy27ezKj6Z0zhB2rOzk8ZcjKGWR2jEgbhU6gC+Oe1G+xcWd28UBPlHs2KGPLU/qPiixTG98codhgX8ORG32iSEsGE2b2s3wbqSOVKNDg960epjc+um95Rihjx8H+KkfnTyBuGncvusj0zM+jY1z9yLNf5DeMZxF2lV+HCQzGM3gDbZmdMSTRmFf+H/9nl/I0jW+9kN+kjz32mEvDLOqAAccAtvuln8rkTyaS8mO/yIC4X0ql53m0221Go5EDQgR0Ez21k2mFRYaLHOMkswdeDyYAx5yfiyBAcXsBLYpMh6KuYjF9s2icISw3pRS3bt06lvpaZBadvCalrPD9/Py8cx8tiscX7+skE6vZbPLoo4/SarVc+uz+/j5PP/00/X7/vuCR3M9JvaiTWoXCcBG3y/vVxckothFJFRdX6JNg1sn7E5aOsFOFYQewsLDgGEkHBwev07UrlquwVYrpbZLuJoBGcfIqqaKSYlwul3nkkUfIsowbN26wsbFBlmWcPn36dWnTk8mElZUVGo0Go9GI9fV1d68CQEgIgFpk4kobku8ajYYDKARcEEddOYbco+wr7s8CtidJws7OzjG2cLHuT7Y94BhYU9QDvF/5yvvihF7Kr9jO38gRVbYREEAYSKIdWdR5e6N2dj+m3snrvF9bK6YMG2M4e/as0ysUjUzRGC3qYkpZi1mFgPRxHDMYDDg8PKTf7zsQMggClpeXXds4qZN6crGimE5avN5iWRfNgwSALAKSJ8u9uGBQLJdiWngRSCue9yQgL+O3ADTGGPb39/nDP/xDXnzxRbeP9LGHHnqIRqPBeDzm4OCARqPh9BiLrs0COEdR5NprvV53gLk8B4TZDpadKM7cci3Ccr59+zarq6sEQcC1a9colUpcvHjR9fUioCnPDSlTeS6I+choNHImWAJ4Sd+U8iiygotjn9baeWsI611S8oXRORgMWF5eptFoOAD7woULDjRUSjE7O+vKQa6vmPJf1NKURTtxBxdNWBmD0zR1eoditFWsN9lfjiVgqpRXHMdOKkTa//7+Pvv7+84URUJez8zM0G63HdiqtWZhYcFpTop7uIwdwg6WvlbUTC0ucslvAwGHx+MxV69e5datWzz33HPf0O/Rb0kA8fDwkHa7zXed/ev43hu7M590Nj1y9FXH3r/R9m8UlnEkwJU6lmL4nzuK11S8/v8s55P7UCfYWnxjZXG/a/hazrJuv/s9FPVJBOcbi/vWcREB+Rrb3vfavsb+X/Oc94ss+4bu62u1x+Jn93t/7JrBtceT92KBT2HHHW37jZSTY4++wbXdr40eO0fxvk6+L37+J4xvtN/+l4j/Uv3+TxQny17acVG2QX+DZZU72J90Hf+vFnJuxyo1R58X437bnDxOcTutX99H4GgshKO2+M3U7ck2XvzsjdqnshqbKhMGrT5+HX+SeIM+/Q3FG507P6br88Xx4H7bS7u73xhw8vV/7fg6Y97r7g/+0+rj610LkGQRv3vnn9HpdGi1Wv9lzjWN/6Ihv0l/+Id/2E207seAeSNWzEl2XXH7opnHSZ0x+awIWsokCzjGRHgjIKH4nUyk7se6ku1kEl6cIBdTM4vbfi2wrJi+K/sLe0YmaifL637lJ+cQVoeUQXHbInB3cj/f9x1brsjOGY1GrwOBZL8ik6wILMikvhgCKgpw9fXagIS0BQGJ7ueoe7/9T5Z1cYIuk165lmIUwSy5h6IwfzGlsgiYFFOwi+1OWDjCapEJtUzmhe0jk1phaEq5F897ErCRMimmZ54EieTaTmrAFUGj4n0X2XxJkhwzm/lm46QTq9TPyXZ0v/r6RuJr1fXJPnny+/+cUZRmEJBCAIqi9lxxEQGOj0tSb0UwqZgyer/FgvstIny9OAmYFl/fr46K250Edoqszj9pnUo7E6DtZHq7ALEnJTDeaIHn5DXdT/9Ovnuj8VCYoWI8JaCWAGlvxGS932cyXhQXsb7W9nJdxTG3CMAVzx3HsTN0EaBayknSqiWKi21fK4rt42stFHyjbN4iQ7l43CJYCThNyTdaLCjqFxc/+3r3crIt3q8NF98L8DgajfjX//pff0O/R78lAcR79+6xurr63/oypjGNaUxjGtOYxjT+k+Pu3bucOXPmv/VlTONPENPfpNOYxjSmMY1pTOPbIb6R36PfkgBilmWsr6/TaDT+RKs305jGNKYxjWlMYxr/rcMYQ6/XY2Vl5T87Q2Qa/3Vi+pt0GtOYxjSmMY1pfCvHN/N79FsSQJzGNKYxjWlMYxrTmMY0pjGNaUxjGtOYxjSm8V8npsvd05jGNKYxjWlMYxrTmMY0pjGNaUxjGtOYxjTeMKYA4jSmMY1pTGMa05jGNKYxjWlMYxrTmMY0pjGNN4wpgDiNaUxjGtOYxjSmMY1pTGMa05jGNKYxjWlM4w1jCiBOYxrTmMY0pjGNaUxjGtOYxjSmMY1pTGMa03jDmAKI05jGNKYxjWlMYxrTmMY0pjGNaUxjGtOYxjTeMKYA4jSmMY1pTGMa05jGNKYxjWlMYxrTmMY0pjGNN4wpgDiNaUxjGtOYxjSmMY1pTGMa05jGNKYxjWlM4w3j/wf6ZiRBSmOwkwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1,2,figsize=(16,9))\n", + "\n", + "axes[0].imshow(image)\n", + "axes[0].set_title(\"Row of houses\")\n", + "axes[0].set_xticks([]), axes[1].set_yticks([]);\n", + "\n", + "axes[1].imshow(edges, cmap=\"gray\")\n", + "axes[1].set_title(\"Edges on a row of houses\")\n", + "axes[1].set_xticks([]), axes[1].set_yticks([]);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b961d99-f6d3-4aaa-a9b4-465bb2a02c1f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f150d75d-824e-458c-a410-63a6c66d1f27", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3 (legate) *", + "language": "python", + "name": "conda-env-legate-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/cupynumeric/source/examples/image.png b/docs/cupynumeric/source/examples/image.png new file mode 100644 index 0000000000000000000000000000000000000000..68fab2d4d9365d778ee98f74344a0061970060cb GIT binary patch literal 305442 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 mmhlYJG*LFQ$R-n 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 fW?vT39hi;ti5FF!R{_wvR#grNng(^{n2LbJPH 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�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

nFz&eE;6?`O=fQ(X&HP;5-`noYIYy4A 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 z z=G;}kjE8Re-JiSafsobFI^9JzfS2b+J z3zEbOl8wnmC6Oc~po1itz|cwa7|#=+B$Gs%-Sy8F(ryHj;sZ~05s>2IU=mzCDTOSH zv~H45St{}{i$D|60-gi5iyp;S-aRU8YvSrhM#Lagg2Ds$jWsvhY5bXy5*1KTnt$EZ zU;4~=YaCw5XIF2#14hS+7UobE=v1OWibbreSuFCbBL&xz3<0hbDx)O?%j8rNV98PB zNF;R*-a?YoA``for#7>4rB_iwiXsL<&@EPR1(MDY-&=E?gD} zt)4{Rw!wM$8LP-9!X_=HwIy|QDU}uDPB|921!mErO-Mn&r-aaYFmeQ9WGEF)Y#FrN zVTi*(apX+rz}`#nK9cKrQidZV0&~jOFysq0o#$4yN-68~IlM$OQeq^?#AH)S<;7Kp z=eb)B=`UZjNR@LtQmtaIS-j`!zkm5XpE~)y>#INi>nqnzFM06VEvL8tZtwKUBljsC zL)XnZGXpRB*cLVj>$MOZt*?v4?w9}c>wj|3TmS6+PyR~(_}hQ#C;yk1Ot;(*-urJZ z?8{&K4-fqI_zQ1&=YRf(gST9I)OY9BEj3E!nv@c=w##h53~B*W8#6uW04(350p-*A zGqe8vH+|`JnO=YOm07=2R51(aN93UGUF{`tCb;hSj4s<9%Lzzr8=P!bu1VL0Dd|pk z+fRlk22*k0*{9Xb_0ss14#l~X=uMXIII-d3VDSP2!1sIJNfq%t1w;iz6_M~AG64}P zs-6y8091qmAOR?>z4dLO-_^094?o?7Zj=u*07*7x?)z`BAAeM*=D=-|7*BS@`$C6^%@@np z9_aLy>{dvj7sn!JC8ixA1iVFHg7GP97LovxnS)iq8=vxg<7MWYR3auB5(-sIBQgV| z#7wf5~^}+3Nj^0 zE0RG{xos1YOU}W<@LZ#Gqmq;+RIw}?uetAizxLG+ysH0+LzkO#_dmP!r;E;C=~@{J;aUsl3#H>$QMiJO zR;Z|gXl=Fl|Nhz2f4l$tKJwn9|FQe8pSkUa|MJGqm^}~me*0fPcH-oxI={Yo;?*zv zALrHjj=A-@!l@Y-0zhEzl_`UQXTvopI^FeWPGB&+We=k&-Q49fv;O_pJ#oCmYkIs> zI}|OX$&XZ3*s(J~CL}>|^}?=P@9B=(n3E=Dnln39&&=g^>*Bcv@rZ%G>Co||0ol>X zn&z&Y9PVDZ<@V#--kipF46G@8$pJ|sNhExmjY$G3DoCe*2q4)GW@|9scmT#5kIu|^ z{FnV@N9VDA-|$+h0e)l zyze|%Ly!~??jLIv%BdONuYF+mzZ#I0A?hsIo0yU!;f%1g ziJHAktdkTJNSGv$DHB+V7^9+4HpG!QYvP~-SlOs`l+uS@@Wv|uoQzcgMa+$mxfwUJ z=mKYlmI$Ckv8Cv8GmUK%XKW%x>MwFK)|OUDDUww&%hYfVO$t6XE{EDjFeow*2!IX% zq=6!(n2KP^!e${vva%8$NGy5~m3{8Kg*XfpM~*Mva?OQyCX0zBMbt8p@`|WT(^)fL z`KDu(ob_fMxQjdYrzA2&hyYY(>2)Cc>{^0M5%=tAzq%=rEL?>Y(>0g-ZyY?4fBdg% z{K((EHT|c(um9^eoq25g#*NOq@`!AUl!Oe*j1v_Qx$cU(Gc6$XtcZz?S^V^$9sOka zEARY`^t-+9Deh?e^&frpokxxy`uRWqi2r%_CkW&EDx27jI&oHHasU7z07*naRIcr6 z3Sb$gP-N^xnb;ae^b}Jkq8J^Ag~JPa-*fr7)y0Q)t$q#t2fEW`VcCeNAilgb7p(Cx@Gu^Lfj5&oe{#S@Y=mIQK4+nedd##v?4a@-UCtL z1$6GCmI+B>@xh%R|H@=@(!A&N2Y3I_waRb`oic?ivS29XpdCme6gej+rO1jx0zqoz zByuKhi0-J3;jC$lu2HAx}IDkQ~> zyfz$32vAN^fyy~>Xa-L`-O1uGP#ihax#g;rdRi%ONSU~$$w}ftu8V1-7pIfX98Y29 zf}Wl@aHHp-!6L;Buojt#r8qH;2{GvVcGX|omPl1{7EY#X*4=}P-@Nu$f9vwnYmObc zb94C6*E@T@nfE_;{X4cq7QqvQK*m_>Y!Ih&K#U~|TZ6#4KmT|C?d-#Azp($p;7#ZL z>%ZOqTk}g#d>B8$`|tYXkf$ifI?^+;jtz8>DH3x;;Sr;Os+DRHEI?3d?%bDm<9QAk z-gy7%)wSgZW$&w(uYOJDYa~!-ACvmpb#p~`CZM9upw3fnR}KqB$YcsbL7#sXI}e_D zY`S+oomXabY}2}fx6g`&jdQ9GZdT6@cdxu;-|1~&g5kc&a4^IG7M+Czz@!A}r36wc z2_YfLB#{8=kYEiYXtriNU;rHe9q@w5zx&B;WL^L9648~m~$WFtd!}*`oZg(W``GOyZQaAQYa`*_g*7pG@P>2pIWViOafOuSbX~7 zW_=5PaP)6)IDAc3kh2o82trMXC}b%jC6RmqFW3U4g5p$!&=G->2tj)mXe|iDToS7& zAy`-@7ZqouSqQF9uA%4yLnUL{T({tH9bKDhjzG7Ux{HN0Ru@?+Z`_(Urp}@D+E2>S zG^$Tb+on0W=ld6AdO%KyDGC(Nn~N#M(NfRTj?qthz)G8)F_L*I9Wu6YNCtxh z$as3wrkQHG_s_YnZ3D9Mu7@PII&%EVYy4aG|Iw$H`o~5$@y3l8Pu)1UaL1YcySJ(+ zVoL;_MXHM|%2o}F9=C=47szOpszLKJr z(n-Z4Lq6k$E zh^`$qZC$fM7p9}AC~fYRI&)OUxsaR>Z6jt5x@P#;z#~axjWM07>J(iYI=GCAT{&%TKC8V zfO1&0qcq#m;DM$6cjKGum+pV+o4@q?7k=~)F*rZC@dK~>ow0O8j)f(f=}1nAL@9JA zNgtJBK)I^y{MCEo7q0xm_J{HIc=fBl@hD(6u_ML;o=69hfr-5F6bY>_$N+4{JRd!I zD$pu0cZ)Qsd4A)|0}H?azVpDDlau9#j$F0t%ggsS{TX|CSH0 z&Y~#^P}zzEiX8oX=b_zg5#rxYEwe&;>FvJoE*7)7mCq$hQiJZ<6=foD6fC}Qx}OwL zc+K8gdP8l_s(JFLkqq4Qa{YajeG$mS9DUSHJ&hr_nMkz~oy5=qGKneVY~sSqPZ<$l?sm&gu~b3|uhAy$ zS|LLnlNvQCc&-^p03-|%sI06iZAVL)n5-k{f*mv|Vi`=w=MEUzb=(!(mJ5zeMkQU2 zo;G67IC5e^3?|Yv+6LWoI%~0hCAw2r48&oec;ejREej`|oyxw4Vvj;?S*1*@4Yqdx zExKAoN5j3lrjoMCjG%pxKqA&)sLAOXNEq$BAMLklph{-a)3e#0IK1P?p(d#A)3sWg|79wvy{9>>7ZM377JxyI=!u0aX#c2#wr$gK46ppPu zc;gwjcWYhiFg!c4?$*05$sL=ks;0$oVik7ffyI-X#yh;gIsn%AekCD^L13MNOqgrf z7XeAiB$1?-sgnXUV|xTR{7)<#?DEfUp4oC*0|^p>0HzRjsaM{RIooHxsD%Wj2lh8A zCSY0=-*~)N2ovM0mpx=o0?OsJ&m$_Oi&rl>?VL>_&#XJY!7cV3s_1Y2t`FX>4jtd~ zV>_)>YFtrp(FU)SZbTLaa6;%LlM*ILs3N$ktpdv#*Y&KOI9ty&XBLzN1+Wi#I+L#U zF|m)tTp}55ZDl$VLQsm%N#Eo;>)K0?HbyYOP;8=6Mk~`%N@@hJ# zD5@*fN0?YHbQVH7*}ZD z>+j*!$7F)-@aisF>nt{dn-}W-_bng!>lc6Z*T3@FU;3-g;r~Pb@Sp7;On&10yyWb) zKJyocesst-6*Z_XL{^-IWz&dQHs!(}eh7QB57WXXh%AyupcYE!$YtP?1BoU=p*T&X@@~VtQGN$k;e zbN}vsIXm=$g*zU7?SE6DLoG!m??#Q&YaiU=2)05u(>(5?uV4G(~pIfzR>xsozrtz*F zBT1Le`{LB*8}=Qm_N;E`%B?)-th@8Ym*h38tEj6EADJ1y@>+Fn1c!fOjmKgI36en* zConNO4JU~tDL~7_38~*CNpf}50xJW&V9%NZth26%$vyw!yxcl}c7>TZI_en>-K{$J zTveKFl0u|p(RWQpOm&vNk)$FU>pDJ&NvGge`=~Q_Smqpa=<-aNX_o>s=Om><%5x9@ zabI~q>CFG|%OrQ0giNi3urnyCy)=!FHDbfbM_=n4gjN||N!K=Y?R=O3S&;16C~cLk z)mZ=$dKN@Tu?zWH;!=BJsQt86rz1V%GSsk-*jjo-X? z*aR+q4p`ra!F5z<+QqS`^w;n8Jt~t$(afZ|z}NychMJ3jhEFN?!K@x;mN zZk4C%%2_ByQ;>-OUzI1yb3fDzL0p==Y|yes8!fZQ0K+ATERa=7vUBmgg9O}-`x|^i zy41tk-koy`CaR#Sq`v#5qmOQ;NB*x*UjLGxxw-T3?_KymSN{A*E-ckh)T+sc?!05D zh%MS;($lrgS!x%kH972-zx65kvWu=#4kB}kwZZeesv>cO0b4X)8S8umxFAo4z0skL zlcEPM+G==le)k)na@#X?YzAQECHDcTKE6I%{+=tVD6X6{{xw=$Lzh=6s`~f@H{3sl z?N0N6pBY8NkM4Q!vbgEoT5;3)69(tPg9nb~<*jY9&C0oz-R|x~SE_|e>!_kAbwG&ThPw$T)`Ov$wU(J!(A*Q*D%1Zc&wv zGy6KVj)My+Meo5Bp(1fxB=jPYvEa%SWJR*1gH2UnPK~`P%*ReH<~yb?E==bP6{M)R zl$OrV7PB+OT(MSFvr8N?aiJ+^jV}a8NY$OYAATKebKl!eeEhYWswI;%Ha>FS{t0OUOHF;KLIBb#!jqE{$HL*ywcASx#sie(31ZB^G^(BM?Cj6e^N7wxFgICsxhUb8X0JUrNgqbtrZ&-K3J>$wwrp4ZH;#urapS<%`tL2T)ZQBFS z9_e(>s8{YePJ`|3jcV@FQ^Vcvfg3JXi|02?*MzNSH(^)aG+o<-8)CN`;spbXMGGlO zrl4h#iEWUoNhlHn(8NhQ;@-9nRlNoeIDFf1fZ+fb?l$m;esfxHZ*Q&7wu5Kdp|yeO zh*umMN}`~0DvDVqjd3{_b86UfDx|4n;UlzctZF&SqS1(j9Cy=p*G{z^XKAzx zhIhRGeLX_EIeTCqCm<0rr-p+U*}K#zM9YLiQKc)Zf-X2j_9?balcv#g05}0iQi3#* zF-2FULRk+$CNg>=V_MRE>tceD=(9rC3c5^RPY&2MQt~3pp7h;ax%#uzuvG66DJ+{sOs#SOtqol zcl9P5scyDr65=pWJi4;`7Pk^K!SyP?MS5b&qOH^zbF!(comc2On<5Av86q>WirHrq zAdw)G<}>T5S388e2CY24zWI{!N_QW$k_0Uj;!EyTU-{$CuJ|wi=(hWRWzSt7Kk(mY z?|o?_>a19%%O88wo-s+0Q*TprbIwt~I*8~ySepLEKcw4-#5ypMZm3F*n4*u0O~=*7 zOo>d*T~;vy1k$1QArHDtD8{hisBX2wZr6qU718{rp*LqI{7P}_iB0>Wqt9wul@IKAPA?6| zL^ywPxZ6E&;6kxuWk_{Zv`0@5Fnrr^bz~;^N7exrSe7zLNitXji3F51Ns<@R>i)Wp)no4A2N#Axa`ZD(D2L3Ol{O-csD$Y7}%;xJG=y0ZV~ z?X`%)6H^R5wUx9qOB;p@-S40pGO{rWa2x zzREk%%~lqTRwM;od*8zGch^7mjh9ZoaK|jX;p?9|so#Had(Jrt(_a4g4_s3tXc3Aw zRq)v+D3Nn!8Fx;9?J+vAPP!G8nAjWdD{msAG0~Hc3VkFJ#DR@wZl;(?sf}r4#czJ( zkHjNieD>04f#3C&4|Qfo0GP?1dnmW=#G2mqinE&s*PmSL;i1MXh%+N7s-Lvx(2F*@ zoynHHdE@BiG-%iNUOO{4pO}>UPCV1c1^eLq$%X!(X}Y%R3U1Qj0Q6@;c>A-l;MPM8!rL*Mek#b|%tPsBU!9 zAjt#~DafQVu|pKKka9ZPXHLDF6#M4P%3EKQg`|S<0**Y(sB7V-EjUI-@Q?)*5tyiw z&;lf6H!Sr9(?V%NsUWrJP@Vnua+52qrH-gm$z;SYF>%w=|u$ zsnTV)_SQLwvCn}ev2G!gX9W*qGg~PQLmUQ*M_2aWI$n*1XNm;w`bIBpxVWN?_V-od3D~i_2$fj=Qlv`f^l{43du~(MCfe;l_ zWFRU<9x#AZ0T6-URrE7&h_yl-FWypgU-qH7XJ-9{EV~!};`8rW9)H6EX0qos(fRiH zYUkP)ug=(?TkGjp*YcWpiWlYN;#1hOc-yox)A7Q-3#Z5EuGPKU>f+X?ws-74de-tq z`(~V8=wBR{a(3a=aNp#?z31m|n8z1~RQRK(cRRfP+;9XKyuibF0K1Z_ASHBSO+g}P zBu)i6fMk+Mhfs9aA$7vq3|PSc|1%4Na{k?Dw%6mv+3ekoZ$1j!SyqJ>Ry2khHZ4gb zJX?qqGPQvMLNwf2Ozf2qK%M30dS)Sz5i?1_t$=4@FXcq1RBXL@w)+$Rtxu>&8_PEu zogGIG4xSN1NGSpe5+*@wRz`IyD@x+7E}_*(Hao87Yp4AUzP6prj7_Q2+sd+@>L5B= zuV3jbPMXM@WG*i4x;QnSd`_fic1Kgpb2DFiHp`u`qbRh6UpW1=lAZaTb;6}t(GNu$ z&Ua!R9M??^G6N884Oe9#J!!eEs>!D1JOV3@D%WISQ7rD-XW3%41k3Tyf);e#>XL`LW4+^xuB}(XcihulUs7@J4BqoNL-SA;Vf!NDV;nCWUbkVhU{z>|>4x z?jj-TB1qz#u=-^OGDukeo1kS(OywsE17bD5y3HhRb1c3O>q)u}P-<4J#L_R*w38M{KH_0|;1 zs=kO_q>7rV`wzxX z{H@=o_MW`+<`4gyUt2qN?caFPtro5HpE>)JwiPx?C8peIt3YTAw4yU1an;%XbmCRb zF~tDGaH|S%i3u6D5=>wUG~?CV z<;K-#EC6@Qy`$W=&ur@hx1IFFr`C2=*A~T6I4@1)%THOe_q!S^rmlDGxo0O>SX(*x z;*F*2kNCNrPYqSG*?UNxu{*Z6L^V6Iik`o3Z#J6+oo|euJh3u>eckcRk?}Ad&odr? z=|UmGCPWpH0K}m2Z3>7MP$mI|c3AarLkut+;GbCIP1oJ^A6Ld(m)q4Ee*f!7-8@%D zcq8LiFA_)wwC*ew+mrm->8PhP!Gl8lm~Dq~P2;f+dO$6|OU z^A4)}=RU6(ZKE-sVPUQy-Lb9JJ7d=9S&E(cfWkULQkEsklcmu+#w1iMrv6@PD z5{8F!xotIXYVI)V5F;}@2n5BXJYYPrGBFY@i31dxi2Wnriw(Q+r(WRkMR zP!;-f{f|A~%i=Ilee2Zi2Wqbx8OBk8KvBi%qJpV?Z1rx3J(&U#M*_v9GLnuQfFzU5 z5UJQY=MrR!_T`mNs^@anUI;~8YFEl2sy(LE+5i9`07*naRJ&h9pZXp7qW;mB@{j%D zTg;!Xzv3qxSFKP8^w}@{V!??;C#c{pQEkbos9G&^+d2LJoOo^hcvn)2I#@zrZ!Dmo zlZb5{naNoqqu@tNMDhNgm`q2f9z8ei|BV0esijFYokBJnpXS#u3{M((;LtrIADW}v z`lj2TEPHFmHka*)I#%{H{mN z^>}sejp^CFH`~u#QPuGauz2-VuB%;pUSv$423+sKC%0zC1I8P`8^D`+ho~d~6`^F3 zfbd|P zUBPYKF^jD)I_(U;u^fr)rs{J@-Q*a7gbPktrpbg&fLW`fV-zCQ zOay=8A}~WBC+HyHpa~3O;_}kznF26c zep#=9(J7=dCs{GMUb0@Xy6et(5=6mivcgoESA%qY@ARz{j8HI}4TCKLVeXn`d zwrcIG)jIT5+p4dQb#Cie>MTXHPB?*p1W3q0=JDnxcRuIdGw!|5-fOSn5A<(8{lEAB zes#yt^^@zy}N1mC7#GR#jk>}jmg(1;B znaZ?!)4^qh82~jrPeKeDq+ocSVTJ()3@{Af0D}RTVMgS#tllds00PhPA^|7}1rf;h zAHDXdnqO)pG`{4OUFOroAtkZ)R%;r$3`85G2|`urXVe2c7+!f~V(0g^ zHy>beF<;W)nv;xcSndl0ed)@OdKS>>fd~{`fBa&&eb)d;=-0TPgn8BRxyN+GQkSN@ zo#vY*n`Q&eh!tQM4Mfp!76_52Jv!{P=#wll3~iL*cl*A#oRN+a$~j$Y_BX_a8J&8H z!i$t*Y2VXv5UEUSWG-PDkjrAtDST?QBSu@U1SqvW%|ml)Y2K&sn9Qj>kkeklrp)6B zvr1T`VIT^|u`D2t+a*1!Qt3IWRhCN&%xTKGaB6ro_mv-19oY@|`)4+->AN_oC`)aq zGYM@ZxGr=#*L>nE-QZc{X=>q^L1u*^e0o{7waE#}j7N1h$`GJ4wu5JcicE9gIcA7V zD;ZCuFqhm|oO|Q(;RvPpSgkR!Vd>R>5j*7>X1Z0^EyUK(JY9^@s~Uh=5Td$ZRaQFk zp@kR5PZvDTGbrkig#hjL15G*nz)LKP^2@Pbtz5C~=3{?cd*|1VC%Gc?z%@EAd!!9- zKnZ20`H$%vFT_WR0D9MKU-Y8%ShKu#^T~>wKG~CFH!bCBt=X^$e(~{jy<1m=!>$po z7(a2b?Um>1JJ(HLvie9M`D1$JWPSMd<+9()p9&>T9Ih2{%Rs{~#&OphwZ~4+Ow3%d zYkylI1seDd6A*)jrG{~B7>DM7<_Jv8p$!942^eO;jI5-XSNIj7AfNyUsJ!HQ;^Xm| zs2$aNq5_g zO!%AyCMT8yRUmn%DhE!K4X9WG>}-;$?hUS0J?4>jm6IDall(@&>WtI;L zeF7M(W6Gf{r<8JQDObJxiUG+4ON^y7uX9{V;+fR(wlADzPAd3$B=7*YKuEtmnQG&C zu}riN*BXciL}q+H167QjU**S{D`iym;ZcW-vp_(3g~E3ptvIq9@b}MbS=&Jf&MX|_ zgj#{f7P+Q8%xZ(jR}T?uL1CQbP7?>F&)0DnzyzL?4l^SouCw9oEaO&?e%>n1EC(s} zgbSD+ykn^S3D?}Wem-4Cry`#F^H+9c$Q-r06sIzw&^#^Kgary*Sdw+3LWxsdIrNeG z3(WB<3{S&QXOtO_`&K}5m!-3QIe*8V%UA9kiKEWGpU$j*(S4Fx3S>Z_cI|OC=r%KD zhSvTQ-aMuDOQA$>`?jV=?^v^Z`MSeY#K%*)Vf)-bb$NjblgyoyxOuc!HHYRJ24I{CnPqha0VoJ42!-be1&Dxg{l2RX z_Ltk8P6yYmMi# z3(JKZ(LC@o)2FRyEK37IFM7qeHxJ86nsFOcDXd9Z>oLn@x))|1T7n5UH!^TMIrf#;;?gqFm0Wq{_?3bJ#|HAW17BAD>_j&TmAyt(#^#nSlB*Qh?T#@8uRSs}5`*Va!hDKO}u)z7@5A{F2ACq9(i zFn_{VDdVslWeP9xw5Pf{^HyDZ8nv7MarfXarWRW|S6I6A9dBp6n{krkT9Orq!vu8v zs;jTAetg~OL)qL(iBzWBuW2hCo@iFDm^fcj_2Wpd+A%dWx_BX}8gpT)sBSMOqk0x= z%g0ZfmF`&c9vZFaIfblecTR`FkU70PK@T6R6}=l|T2m)G`N5&|^x2t-neAKl zg$gO2)mVpK0~5%X3`-%ALZ*&-EiGqJx8lMbu*P{Nck$5jMw2bv^mnkt5la>02n%@54;~s}=u^^9(B{`3I@QXH z0Wa(Q+1l>a2_eLVYIHI#H<5=qdG@JLSXj+B9L@o-DvV9>_A$`WVK{CWMwfddT-#Xq zGVR42HfP|bY0=Y+$0E*D{(N(7mW`U_3BcvsfsyAoI&{F6`z5gJMVp^?YV6gCRD|N=hWsWRlq|uQ|zyv=8Z= ziY3?dA|<&2Bf*u^CL?jaJY5JxJx_Egiv9f3aes@HiDjhl)1!Gub_4$YnQarTZf{d?A*JAKd8f$h1YxMb}grAlTNNSW52=7tc?JJJN)wQ0LjQl^lb zoC$9^^bxwLb3x=0=ba&1_mERQ>(fmuue*vye+n6zwl4Wn6PZlj^ILD?S};*e)Am(* z@KO7+t9B4P>o4|>=31wkkSXNW%^hX>M?2-KSM2kP%g;z`zUKUJX`z+lsy8`b#2xu? z!;t35$f4tT7w=cUuE(v)gEvo7@67bZJI^N*>8vf5>W5C%Wd5ddv#QQ_D~j6Q`I(8C zotHinsuaZ@fJP0FDT;$Y%E1g63^M@oEHN-FHH-_zkQ(Ij38Ty;^ANoTo;Q&;@VuHt zo=X<5{Xly@oKGX056fPTTEcXEU+R*HL!iT`x{PcJF{=;Vn0P&17UCJpSwkCM_PR+a+ z6d7^;IlER{=f#XfJ!L4UF@;uK6r4m$LZ?ck=W@BoAj^#9mKjZz1r-vhwLHi)K$#a% z3ZA33fH~_GA$<#G3dAgqatm7UMCPnl%&5(1=1ZQL#04DM=Dn2r4g$zvto6Er8l3EVIPxAV*n8W;BGLF_altO$?ZJh8Yl{F3hFPMh$4xWVy%y7?t8# zI@-LRJClQT)-gbUTR-u+rAFLzdE={(mw7)~H;K#m!R;@+qs29))bz@k7TTl%$qI?_ z3yrQU(bUzv=xg5*-B0vBkRS6Fkc*R^wU&-tfpk-0>T}#H3cC<$xxa+U+tc0}Qy_5?YhkK9!uDvW&rhB53yJq6dAkLpgdd*b} z1DnHNXrIud$JXJcUUy{%my%W+FbCA#rZ}J!b4#I@oIZT(ohPyla~FBS`I%FwRc~ie z)tnA<`2lnKWMg9H=G6zA5WQlrfr+$%iR!$}mtZt!LK$Jel$IvXG6lnsaxefUWw}B} zc*xW|kmCO!0a1`J`a`SEq>XwvEBcuWxmH}auIcP)U1|I6w_H8dpM*|KH1*db>H7~B zLa3W6>^@twlu~B5jXk~?rui}l`O{0!f9yhsYw-4GUX*`QwdcKx(O4!gU6)58wLk-D{g9FLDeV6`LC+3#-g31q` zDm$_p@b}N`9G_2hE*Y+b==cVy@2PS>>g8C!3!fH;4WX7R7~)8LMztrgPR-P0S{(BX zm|@)rTm#Y8ODs_VM3#AQ%(I>Y;SR!@?YBMg^?lRBvc5KIZ&-bF_3zyh!)R$u8X)Gz z+Jc7PPi-bkT$hMD|AWWa-R(y|Ir(8Ry#DyU4I}>R3dxvH@V!ZMWx0Pa$4K|ssmt(UKQJ*XNIpH~$?$3M;wUQ)InL_md%@CyJ8yf& zTe&!!t9tXN>Wzu=?Y6xlnQE65X!BfSV&>M7!yRA-HT=IM@&g72O!UFP5c8m4L249( z0lC#aY9>pmo>)>+zf^cI0?F z%vs_0orF-vI?m%P$qgtNZSy+w`kFeA!!wqtqU{RfNY(WNmRh9?CZ&C%Wi|Jhc2qM5 zLo&{+CQMsrI2?6MXt{6g-4_B`j8ri6{XJzzb_4$2TK83O2R>jX`0K}!Y3GYaMrX5pJmq+9Nw zU;XO_P(!08QvXEvBbB5CFDZ)-tAjA}VhgKF7VrS<$6-)_)jMDmg9Qa z+Yf7@VR6HykDdhci=Gm1S%320*_igNT>R`V{O$ciT*oikeDB-}$GOIqWNyKhc#==s ziPG!JK6jZlLZu9`Rn)>la{z*J1}3B+4egNkMNs10bJ5}pOkfnv1C$VLt#7&mu3crC zQSPWF)*4@0$9pp)(osRXR8qs1Da(!aMX6O60;V6Z)LG9_>e*B<9%z&C%oe*SXML_| zQFdi&OFhpDt7rmEwNr4+hH=sGHTy40rr=xaQO78Q@dzaX0MU|oKbd&hRNhJ(4vpXc zc*T+3fWN=Ky}GEoqMUD6y+jo|G%y*;De6T6lQj$LodMM_L3n;CjgU4bQeap|<&_%H zTz4Zxc|>CSh{?EMh^6D&bI1{EeP$h!Yp)No_x&J(0HA=Gf4@tSuZcwH8fCb;5juw0(r?93_dNgQpYbCoYJ58qs9}<7G@l-~@ctGgE^(YlX;& z@u(r0;}jH`mr+8r_tX469ipg9Dz%4><{jA$`1|X-O3P{6FOJW8LvEEpsHzdao>yb*9>aIxJNQj0p(GV3GIV89&LDG zUX=ga&#SU4IdqE8?P^Qg%^MD$CEVF%8Wabj?&%{>#aOuG)%`O^_qQvXU-0sO`@+h~ z@Q?kmZd%AE8qCPw_{JMQwe57X)NKn&fBRdWQ2xB4d`!$IANt@We`}UWS9pyNUP?wj z@MmxOQCz*{_N? zdwfVOlZ~x2bF_$k!!N4yiDq40-P^+k&iAjo>R@5j#YLra`uwSy9KNOJM#7WIFBbHX z>Bhv&OX-=8P$|&B^J+zSCBxDpj4_NcC;t`ALVvgxWPyDhy+CGXmF;Qq-(#W^0`Khl2PLfLY$ z3%z9{xy^`cSGGPe)rVb8!`$14PkwVg<~{2*zrHDb&rgPQ=DnCb2J481*mihwm@id* zUP$Vl0l$?nRP#OyiiGQ2-ZaWJUkZ;gYl)Dd1uOTKR&y8UX~)y7&{@o`kPQYAL^KnY zb@C!+NL@ZisHR58eNCAa@RamJ)vXHZ;sPanFp0vh*UqiWr8d+>lW^VP(xX;wKN)0{+cocAc=MX;GAb{koX|ZPlxUnA3hQF0B``Q{M?qV=PXj; zC6Lh^f`RN&bz`kM@#Sx?D&!x@RoVeyq(Ec+b^mhBK5Jwr zUm0J(+dk4qM-HsL_WJRE`F#HG-@o-$GX-z!+5PUzcXl?v^9^tM9`d*DTk3r7=lI4y z&qn}K+&rA5Z23%7-X@M+We@F}z%3(7<$>Ax0I5Bc%HbEcuwr^PzisY(%jWsvu^XFW ztkG~+^bUb9V9S+H7gt}X2Lo*S_+(9v?dY>=|D-J`ika!g#LRE1a~-63mQ3+1iL_b* z4D)SYco~C&FohI}5nu`e6t)jm){*dlZXHM)D`c+$)X+c;jT)p-`8)v8y6tbzFJDlz z-q@b@TH**d*_B5wc7l~@^4`@4zd4)4sT9$_SC;>%S<=3@gUlb()>`tt7Dm;2)m1bpquhU9Iph8&9)qwOPy!t|<5xjf|* z9%8CgEyq~wsHT)#Kgww<>eZgCu&_Wv5#S_N0qN3e#6)FWM7kI=F1u8CIh*>huF)9LNQknO zNPzS`vgotG1ZkMTqA$l!UmWpMX1qkhSxr*|OERa8Res)qBje0)Cd8vzO@r(P{Qa%# z{nN05kGp8FO_6KTVl(ga%mt=8!l($Zcp?|b?xM_vm%idtdfjhs z9K*9de&5*Nz4`XH{-9mD=g@Kg>s!L1d;j!x-<73ZPle~c`vZLD{@JD!DwQ`@RFtcq z$#U1odbM|M8pYdky&*lBlnbhHbY^1sjwLZ%Zw8mvryDGnoF2Pr(OWy$U|SZR6!}@~ zykuWx>|#$VKRG^A!}{x&X(gFdetsZ5GToS%d1e26NA%FZM0Fkwpe9ky89*>&7&D9} zU?`-B7z7l2z@pDk){*dFqB@YGQNyzuH8g6)$r>b3mSBLXews`+mcz`oonb~BBetA4 z+}s@KGw_E$BSoV&r%I0TLL&;;~B}sm&g(jr&RPzluk`wvdR9 zrJe7t!T;P>)LPt4r?g;zqd44S$l5D>o_Ece>gyX1Y5!G2poUTONvYc|RAR0JYDFIf zFLMS#lw?J(Xk$e=SLdVkS+ty1QfsZRDbE;JevShgL9y!@M+{>UDy6AbeVa*=S!Uoe zZVkwhTekLSvcz>xcZ)%{WUMgYwwR-^u977|#!Wp82sO$;3l9QGD*6Ppmlzc~?#$_t zOOCFbOoNy}8%wQq%rY*d-Ym{hYpLKK87iJFbo5xW zGSH2SHc1C)#*G7?KNm%5qXsl4M6Wp9%2%6M!#do8w|Xhp$kgyPLW2p%6=gJY0m~#Y zJ~cXUrDOsJym3ZT%3f-nZiXVlUNqM#!ot%;^3 z(gyxh4RWR?fxMK=YLZeDlrnQ$u)-8(lv@r_23_C2n@A9P;d zD)WK(3X?5Uz$nZ|jbhCuU2BjzukEI^BwnFlR}1&V7AXy1x( zNa-@tX5$aH{q&@{_F8b7W5z&dNj@!lnJ-9|a<~$4Y1^x$TKie%Okho~JD`^{DY0&kL=i*y0DRFLK%_R0%t~f+zb>F z-#HHwiu0L9g_4BYOqWtZtTnpwDzE?mAOJ~3K~yq55xNY9P*|G#Dc#iDb#%6SXQe2G z=X*p}v!EdHy#Jjr>KLay38?o}x28dM1ODFbO{$(FSy%uHB?D&~rDSbzxDfP9T{a+l zPNrw(`@IHgkST_BbGX_p4lUwR6;e%ktKAPYvflbDJwP~kktdDe)PYmxIihn$->^gF zsQMpA(Ff35|1)2E)ID~Q&n;T$@?iO4^@`!}$S>>u)*FLM0`x4iY2tQ$?2!shCRY~@ z573;Z36?+hgt_b!wMu2(=3Th!_RBw={P~To)$Jl$e#-IW@qt#e=}c z>z0Dyg~q@YXJ=Yop?mDgYcEvCXPTuej~^`ZS=?MZS{<57LYqsD&(v_m_462)%kk7e zB{?_`IrEz3*1YI7Fp)O!oSH;6X8;HCi30SG0F`3zZ9Z2zC z$(fo2$`W9}@}K^#7rn)HWLG_e>nPZ^rfdlG|?==ie9)$kFZ! z+1SH?%!6TfuJ~E4ai%oPlEHUp)Bie6m4b~Ptl*7D1DodWDt%SgJOlWJftvMflDoLS z=(A|@CBJ#}=W6J7S@cyqpJzR9NeqyXaIeiq+AdEd)qTeMJm{vvwuA&)G;0o(q>lut zpII(49xqpUTp-2}>+@b|@_1QmPMCGhjkb+*8MQYtr#RcrZa_HRYwEe2$i%n9LAmL#m)>+10*nz&`Y z<6L!op;_8+?2Jd}<&D)N!-MC-LYbdBJ~NSCv1`%Q%I(%Nb82cHa_0A^mzqccnc_c4 zR3!`;U>H+^86%8mX(A{rJUolW@QQIHWQzW@MB2cw)Z`3mkOF}OGhp4D{(kCIr>Wh7 zO&w7*u3diORCKLY+51NJf4!clNOJK#%>M~;Y(%Zl{O5jwF{XLCVf90PV=)+G;qc#( z6JM_<2*Eo)s=7Cx5IQZsv~wm1C}D}8M3o?}4!GG9;eb$!>D!9;E%k4_ylm)T_%$<&k~O&<+D#0 z$6=`iS0&>FVc@Wc;7IVj4IA3Vq{{0~ZWN}{lEZTkU1uxGQk;3R{ol#%Ky%{P6kCNz5DtvA**cP`paL8+!Zc|GmWm( z@7wUmg%?yE)gftB8Ob(Qtwz`sm9aM#0+?cvVMh{?CV z@5KGh58v{|@tghc^MAn2*T3$se)r#k(v82!roNMGYd!g)yS`JDH+*x&cM9*pJD$9# z1QbLMkT=)QD!D^MLY=6~@?9-V#8cTI!p0HEORitYm*-j|>n5jT>bFi`v+ZJOWT7#< z_SnfBX89e((?iwiFa!_J&eU+#&Ka*VbJiaSg6R0M8t~F{jgIIwYG~kjHHl$iz!+#G z0A>_piutw=FvIX1W{hA^BHu@|E>rv$GDV|SRLFA)#!OoMM|Y+*msK7>6^yd%(upG{ zl54V5eKdIT!Nn+vB=^6I`9JUre7L`gwIb~-tmUntZI4g)f?Sn_BkyK=Kev#v9(eT7 zSnq#K7Bf}5yZ=+lxdw{E$F`16-?+K)Sbd6nM$cVx(}4@gt~;EOLiS1U9{@;-BlQb5x*@f-nWuiWV3Tw*gq;it@t!?L9m)}xuzzJKP4iOWJq`T`m>gJC9t zr&I}?YE{9=Z2J14Np?iRF?DX?V8%cJ8{Apl)2$ki-GIM0y}lE29dsZR z?a2y~Vh~kFqogZ~bAv0B<#Ty+;#jQ#)a2MQRwbsz@>?fB4aM}BA`uw{-eg4DMUTO633DBgAzs)FIPfKi#b(#eb4Q4KpL)F=Q0h zGZ_PdzEMoj%s?0hqZlA4ybR3_FoWmR(7>~503=YB%s?2EzW17$>5EZ*S{I->SJ9P6 zP9)n*Vm>N?=1RhhI~JD%w(`R zFiY$+o`AL*+-g_@Vt8hJ1e71XcB{$EvtUJ_6C~Lnb0#J(0YVsatk5s=Fw39_*9MM5 z2#=M}ry|po3I|dDaqsopbY>u&2TGh~N)sa-SHd}wi%F`j4Lv3bA~x*6yi~B(nkc*G z@#Ff6J5IG%{mmTqKNbc#p^m-w*VUfSL>-{GQ~Xj@Ks?9A*u;Tm)qv~<{QdFu?H<=O zhR!@*0FQm>p{masX}MXf@|mKWd3@%wXr?B`*peJ?$D?{_oi|m!Y&sDZsvcI*_A<3& zG^37iO05UOxaO7=Gyu*l^>0~kARhVb(P5Oj^%XC@@|jcpG`})h^=F-PC29Nd*;3dJ zDXeCzt{HUUuI|?eg#Ee{ujfoICir9p~0N0(`puH_2Nr9)f_vbHw$F zXH|NmjPjk6Jz2eT$y?c+(K&~ilMb{ z?;9l;wTgL`!uADxlX+go8XX|Ta~d@~rvcO?L?8hMVEOfbcx-;TV~+qCGgfW9?CDe4 zb^XwNxO(vra+Vf&dDDf}%S8d;AA9)zVAVo07Oaqu4@`E76Rff3J<}WUE05iX=iR$N zC^y^^|HSEA2cCGv+J#3j@N_qCY}5aPM_cap3Dq1Bra;ofH{|8c zDYNO0(@U%0Gp8Q?ZXD%d7vA+MwfD>8w0g$HrKsyY)yK&wz8fdZdOL7(}}pdK3k})oz@gIsfX|~C~@sb zLYWc7OFU0`ETha8Ov*T8aNdnqrMd7kkDnc8#-yWv{DKFLkFQz3ZYf;ZTIhD-EVY|* zyayuvsv0=Gg6v&xFuv`&4|cI)&%nTu=p#G#j$L|Y@BC}tvino^f#``F3I~q-(`P?- ze(CFLKDGAOItLc-<*%=Q^pm&Tx2AO2mj@m@z5I%W2fn-=&wFqFe>UFKIt~Ga6f!N| zI6JA*8$oNcM|*POhUv4&rHi^ zTHF!Lj!wRXOXvA;Mtf&Y)g29F_O(t@Va zQqePD@L;U8Fi7CbH1oWSH9Eiyp0jmn1~oiS0%ax$Qw-gmADO*yv6H`_4H4%Nw|UK@ zr;?qf?LONY$FCgTg6Dl|afnk6?P(g#AmPw3u$qy?S=}w| zU%MrtHl>be-}4#KQTef+;4lm}nEwUFcP$>d`z?;(|Lg1*MtNo#v}iK@WA!I%ECqZ4 zAsEqC&ifvxiEvqFL*FXS45t!IxqR}d`WXtsx0KZ;_CABlZ=0;I`|H{4@kgRqnz;GS zd-UFKk~!0^ZfoyNJit;bZynv!9x@=i0lAM|6HXR=g^>HPO$wDITA-k+LUhnuyS8<< zdU(*TdF087)$^5VeW-T9jD*pp@$AUT^OjQ4Zs-h9Y4Q4;W0o_*9f6kCQbGkuITenu z?CQJ7zQ>QAtdG&mbYHML7v>~Sd&kl(b6&BK8)>$7sLru$a0S$b<3GEzHg|k<;_Mxl zePe0qvOQz@{n_WQ`1+3ZhfgF|e&P?_UHjJDiCw`jf4=uK@1N@apz`h2+t1C+eUZNH z@q53#>+>r%uRL<)7r)tm-N`3@B=Ed9-1zFLx5WD;ghC3L7H^!MQn;2UB0SrZ6ECcL ztLrl!bLreUiLD!(Lu9tOwQ!`DP=BiV!tuGuC4#8{%zn{v!n7mcPt4s~I(X}FyvI#Eezr#Qy3^a# zUcT{$t{=p@#SgG*GRwX;g;NS1P%3gbvdS@*G*-5C+0qw*d_ zDbs|iVD@L}`d#&lZ@3-*$q_iyDL?i@KxwBF8edxcVf3!cI*SEo27D<)ADO?H7yV2U z8Ya@{j0vka7oeW=)T8qWwGPH)X48Q~{PJ7Q)Gzs)x%jEa!=CIV@jLHPPkzIjiB^D3ZTFR^z}ua5_|uk8GMy11e~ zBo|i98fiGQlv$rLq&qhs{fZy$M_zMg zE?vz1wu!wJSN}un%|+TVVZT?nQm1YxIsCId+lPNyf640o|M2aPykXa;qxh12mF|50 z3u}IO^@`(1`U4N&@R4<2ojGOCL zc0c@*-$aJSzD$4pk6(P~UCEImpzy4;x^s3q!d9LuUpSn~)i<;;zBr?cESo+j@q%^D z!AjB()*gGjFGeTBU88g3c1o?U?QOBH&fV7ba%MIx42K7&0D1k`VyW1e7FB0kXQ9&K z9S7pRO7WkHP-6gN9%$?X9HDO{jSLv1HH>Qt8wt;k5)t%GuY_}(fYnei$~$v1yN)dO?`^loxl>XQq|!5l5t95yyXZ)FrJ43)z@8V<8+Upy0Z@lT3 zO8Z>v3W0+-!7HMjX4bIG13!v;@i@}}R&_J4(*Nl?A2*99Cga4NUu zSYuFw>;~ikZ*!-~MUl6Yw8*Kl7{OX^=J3Ych57mF(Bha~8n2#V>AMyYkH8H#WX>b`1r$x{`Eu7PjP1 z9W1|i^TDNYDvFk`Of+9UF+2UE%eMVu{tt&5e}3P+Z{GgbG+F;(DU5Sp8U60A;lpR+ z+<{9!z2;jN&)h?cSdu5 zzVjl{Tb)f}L-~=OY(@jO^@3h*u@(Egey%Zr7p|NRh8CtdN`CX?WDUbR562zM;8$uA z7&DA956CD3rKy*CJOmi^Y$hoz)sgRsDD%9`wmU#7Q=m};(5N+P)l)T@q8KobI|p>% z^@;<>nt5U+aVdCakDVmf>gnHoCH`-J?=lA+Tcurh&82&f^@jbyDE{-C7yxGp$!rrH za6_qj>crDISSpfn-g+TDeb4aBQ)2kh11kpNTrOSksd!Cb8Mr5oEeV}@#JS?>hiW@k z?`y8!!SaDDOD;`C3I^b>q-PDGa9l3`@UGW%<9yPo?dj%Xq(*m!?IYc_JL_}rzx->% zXMLKPGx_)6nm?RS6}zn_4JWF@nQjF`Sq{Td+*n$RXTW0`dB19z?$5x zqffO4HOTJ&hb#C1kh3iC$Nznw=WWwxXLq*FW?MG9*(94z2uTPfbfqJxoOs4Nv2r2` zmJ`K}AfSh*CrFVdNRt)_=^?#tvc1mk?9A-UPH%5}p7$U0e*gCQ0Llq3Cv!o|j%=0tK-Go5D+lK{xQmm@(7^38 z`(@Yoq1r{KvsV&VA$gLqD&VR?N8ewcPc`#jn0pJSzhQ1%XYx$igvH zd%V@+=!k6eu8O!qdY@4%Luv-zbTNb8KqPr+>{t3n3PeXGeCt& zB$yJ>HCd6fpo)of4!RNn5Ym$f0!ShVMfbQNf)Loq-}>#rlXI#^hi%xbWQ0ym(6#y4 z7cZ8~f>5?O5ll&A(4b0%eRTM+%TxhkI<8g^hD#o|SI_x=Jv1&`vZ=$!e_pTlGgSb4 zI`f9dNd%ZpTIv!beU(4{5v|#Crl)>fc4cxEKd$Z(EHgmpV8X!#TM%{6`1|^K7xaZ6 z?#BO@{&UGiEZY_Ul6ddh&!hywE{Z4nG6pA>?z?pM!{XBOW|BYaK1zN$uLX2r(c|cU z#FtbXeEBpC+cp?Cd&q~S?=LNDlop&$#TIO86iaAiSt47OC23r>m|7MjNiqR09GU_r z27{$ZD-Mi1TGosgJHH<{_Z&1cl8M;eHyQ_DWfRc6=ix-zqX05RpygCLz#wb|#9?>S z9G7_+Dlf}r;)NE_Xo|BcD;+Sn)2KD)6>RQ?0MTNKz#N$*{k-%8gP5Ed0J=k+9i;W>Z@K43+FPIK-kzy` z!PR=weL-o$ojcypJ5apU!~XqGZ+y4omlo}KW&e)5n%-u+*ZWSqzcAy(Z`VFP8sCI{ z4_&(IzlR#%D$llwK;q`bkKb~~^{A-vBVzfvs^rgp8Ga}B)B|O{ukBmUH9dGZe_D}I zQ1ph4hK|vQynr|%=Fcel>QzbAH0D=l+2L)sZK>oZx#&^wSMn!M~ z;7c1$jLXZlwEmDlTs5XGQA@(975JF(kfXT2!39aOpf}dI)YVn9dz>}9&UE#T6}7AZ2U~R&_2{A{59I`rGEw02 z@Q3>PbtenYNC0fxu#44G+#?Vn@^V=MkX39M<`H@={A*sEw-KDNI z@#uq{N9mtq`F3M!^+P!Hti&8H9}bLoKmc>$C6hai`<51nTuQlXs9*~ww+zc?mtor+ zyzC&B#EPn-2#i^RePa4Jz$gPYH@u%;JiEA@@A_`cII_pgD4G_%@0Q}pS1@XD=ekr8 zFd2ny`g;4)0R~|!KCa*skum2SIM{kcH09rkD6-dD<3}fo0fM<2R1qm zy|HrKo&Ip$!~N+^c=m~})cyBB+gtdD7vJys!dCBXNqJ3NZmxV&HSYac`R|z*zEcnn zIPQDL_x*v?kODw1w651Rb0Q)yKuxVS(eE+Vbj{X!rxG5&R+>1`jxFB#W;152ojiep z&o_!KcA?!Imlv}AWp75WzkJwiDov3pSLQ?l3NEglcl*N;cNL7(kgn+69Vdzgbo{Sm zP=Q?QEJLEA&J}RkG9@lsIs+vV$|@jQj1z_Ap^7?>6+8uxG>p%T9z_tzAU>u5E(Ib5 z7}aZ^6844pJ5?$rfVfOLGVWYf&e#4|G&4Xn&2%JNZ?aNvy0ayLh*pAG+_JcCvhL9S zct!Ghd4Byp4;*(pDigX>XRj@ae>q>KhVPCuN8A=f7>kRQP3G(kj_A&+;0FVp%je?d zyg_9ay2Tqz5Jf$cATl^u%3XS&ty(phdr8~VU{zZM-C7?_pQK(ZGe0;W6A7l-xGDPn zb+;D{Y9?zwnyY|t2>AuZ2YTmuvDt?g9u}TXC;bg2?_)Up43aLi2S*}Rl0{`pTR^t4 zA1qGDO2%|n>kOhL3T4wtQPFcAM@g|Dh;|-sNstP}VI9mKC0;>dDt9&QF}gl#ZBKW6 zZ_eIzs5B>XWWITWe(Z02%qmr_*LIg=${=xaMg7rKfI-*_KtwjjothEgX)+%mAfy7H z<%CkmDO`4ziL7a6Fq`j8X4|v5p7M;lDvqV&rG?&Mk#N!AVmStwJ&nw%Q$j646o{k= z3Yaa~aPU%DH!<1RUQlhA zJqwiEAKCe1_n$J&4R5{mz?-eF8oM_+cD%NHn$C`&_p2lB9-O)5C+sibEsx?KzB9Pt zUq3hhxNrV~oVBa*M{TL^-s5Xe#r6YD@eu`@c9+BEjS(ZHFobBKTh+U4hFB3Mz>@mlzaoDwb?8lc^Mp6G{kllRCsK zRATa&0q6h}==fL?!DVL@z(++;fYGw@ME%|=Jp;w&wpftbj*WVjlq*$_m~Z?^N5zs# z8=kN~e9(f(TbGcrgI<79(Q;p79Ifi-UYAhZc-Iey{hq|x1QdBkYxKv7yw`GE7vEM* z0ox3v$c94h^6IHQ9^amU&gIc;Daqh1HGHa8&PysC?3;tdVUf-A@3A#22eU6B{Qilu zP3OqVu44}s;?{zzrjPp}*wn09IlTRb&!oA`vd&{?XTf2bjhiR;mO9r)=YAJDiYH5P zS@fOvHJo}%n}TtkXBrwT4qaKLeZcNr#05>97i*-FElD;lTcRKvG$wi=CM8v{bV}TU zZmC>97KT?C07(D_vY*mPUPZl(9}0RsUQ2hJ!}+w zYo#+4F9a$x{72eiEHS{+p1UO^ht8&U%Ag-Wz|=|A75m`~q?33!fWm zc_p>)O11x?wz7Bj#6`crImY7Ogx{P$K>>^;3VSD)T=^aH*2pXOXY@hmkDA$6zghG8 z;QQY&v_b3sz4*cKL_#(I=!@1AD{LYrxAK~5b70(KZ0LyAcqWoQzco8P+%8=NH0jg! z%J_JcdE^oIMY`3JO3;PI0nMMV7F;xFFUW-9_oU9ol0nafHM5T9WZopA9T|%Rv99Vw z1%{4uObI|T1ece|5~ye~MVCZ_NnEyc#;6LDsiH1HXObcoKqQmL47mU#0R)pl{2z+! zpdu(h!ScVn{KeVypxYuAEOCwN^tfYjNe}%px98QOn78E8rk`Q&pUbAm7u1&Ief2I& z1*o>ryMMl*I6Y$Fk}rLKqRtuZj{$+V7bboj$y8h3%O>-pV2d`~L(--~>WaYBURT|= zvBf>HL@}c~YZJ9MHYi0!!fn5)_*9DD}jR?Ffj zw&Aj(0O0X-v3c0r7|}6%^QqE?`YHIQq|h|JmoeAZ06}5oC`1rTLF?S5JI@&hDLZ9 zq9wv&+(EdT7_yWr`3z#|4!NkcB+G5d;Od8+K}p7q{bPKW2F zI$Wj3>e|pX_YOUz{bjb7^*?{xM9VAq*ymktE7^bPgE(jRo&R;{?C4L`k%cv9F7*A+ z{xw&A!~gcs2VYMbllFs6;vIJf-aLKaPE$Kkd~6rKni-1$kPUsw@*Fcp6LKqLnZ(CE zh0B@}LC5qQcUyck?UA;ac5`{!UNbYL86ux>T$$&gY=U=Nhe6`Sr5n!5jrmzkQ!(9# zkbIeY4!u&LZ{F{i8;OMUOQe%l2@#xg3Eduqq9ahn7-bZojDi9L3fB!4l30PnA)^AS z@;p?r!V(5_MaMrSgGrndQ~;l1iJ+8GmjBY1|2m@Xu5%KTQmeyxXsW8Km|yT1^+g^ymKb0Wy?x+w($n9+Xlqp$$}^{Ak(3}X{}!|p7{%( z`upj)EYDTHZXbQln5?v)cjTmx6L8kKap1%2Z%=@T8OOo62{2}vow>c}S~#2iW6eqY zp_23!3fJ6=lh3koj^*`{RKOBI#aN5-4!eJGN-{{!=PN0;F01Ce<+TJ!)=L$K08^B5 zR8g5JD3m4o_2ck~gcCTL4-V7XHaWNATXDAUXmK8vn7sRXegC_{XsY7B#N1huC72SI zF0DV23@`{=0g^0=umI6nG%=Wn6lZw`;bB?xg zy$7Q*NMQ(of(SlQ$DFz%DHU$3l-LF0($lH7hDlwpb)9Hhsq?OfqL3i!Emxe}ZEbar z&%|<_r=885@M4Vmx{ZxOb|N0FZI~(CO53jZ!PrA{FU_tf&Hdr7-Hq?zW4CzelEcY2 zTsY_9zy5L0sU4sEg0(g;ju?*>F1h^n(3__Yf2mj;&pgwL%fH?8@{!a3%Cp0%$G78O z>ClJ*8HV1yDrfTHgsa7>_Lj${JjSKIT)h~cukjhNfr7`erAR`Bu(En`EN=N6WA4l5 zl#X~z>dGI2BY{Qh`_y25&J4QZr~46dZIQBGWjOEmP$L|Ug!D`JKm~@5b38`Y;c?{j zS|^hMNG8BQ0WeU>G$^HvQBW{|5~{(8G7<*#Bt9031d~C0Oab7dB0vd;oW1m0@1`pT z;S5a5x>^p61-o*kwqK_A{5`|VrlYjweqsL~%b<2+Yi6=908y1C?p%jG3r_NYjQrJG z?@4(BQ^$vc$*$e?Q@@Vq>+^vtCU!JB88J~Cc3hk+T`fiTd#YcXS+h7C)3S!6DpPl( z(?O`=88|yg0B!}YKD(2xS~gjEicP&XHbnr=Krz4MRPZ$%c?!d3_tMCCHQ_Sn!s4k9 zZ@fJv2y{Mhz8E4H~PM_$?G?3Zn-NX4Oty!m)`sBSh`m#PMlBK@02D3Ql<=}?Q z`}o>YO2lH$>9s9d5@kWNT)HLO6$6mK6kg$?gX)Ar%MV7FX)%%AG$WgvOV9< z=HEL$mvGAB{Fgsh*||@j;-afXIjj2_r4+Vf(Lf@=AZ!JQ$rvarrhH<`@QH-OL)6)v z>fmlk%FBul!yq69G{?=xD0-67dMWB^o~iU!rbLnRiYd894BfNZBgq<3Bn^~_3OCc$ ziF8%78cXt;Qpu>R`{4ZVeXR&0T6#}&|Df}8(r_s~+L6J6-nSu#J#2}wIv5|ApI+k5 z;+jJ5gt+vwi^77{o41C_qkIemXyZr^%ktqc-3du`WWK453;cBtO zDsyVaV_fAZHQ3<Wa~bW{K8F!-lN9AU5Y-m^~;t!c^t7ekHWY9B#paPedISd(uGfJT7kT@|p zW0VrkCvT%Vn?Lu>g3ld3It;Y$ zZ3;g;Q?9NwuAb60A~7ns&bl^b?JZX9qiWA<@#Raxvqsi%RLzI3Q=Cqdr~|QME}3vR zR}62*+GXR~lYHX!nMuo;4ZegEPuk;F+rlx6+nmBSSI+MI)Xf=zUl{ixA1?lqn`1F}h2lm5)}n=bY(^*4q8#=1)DS ziQuHPwCPOLbDi3skH;FgqtrYOE2(&c&F!w(>0ETNI_thEd%>zNvR|KhcjBsD>%MX6 z+!-Nlrlu3G2GyE_rAvC6mK4t=rDrW~`FKjQ~(XcNK$OR-X?=Dr?VA>aAwVv$RM4)tytk>D&nL1j^jTAiY z%QJE)9dj%jpPYk_kAyZy9gS1-{;tSrc;n)VCCB9YLX^~~k^W(XTvsAnBTnShfTRt@ zd^!E**hCu86&?Si$cE1e*^nh@P#G01Mwz1m6OaHGLqrkOCHCxQ0rqX;HL zdNK$=0c3{)K@b2&bopThLt#==n3jnCUBpB9FU zjx`5P1VF*e^Hv|&e(m*np+rPJG6{n-+qBk9AL>du7sOwZCh|{|<~>&QbN`MLe^GO6 zeu*nuQ3Vj&&a?^d7rwHEn7UZhYXqHf%Sp?oL{vsC-J+t&b+02&sK5v-NWzKSh^UH! z09)@qJV=+UA1*HYR=Cb6M)i4S(F0_>37Y?i-6Qs(bjx$KIO#oS0(A9@{nX_39Hj z$OfRV>@MhRG~o*|)oTnUJjQjT+-#1`scs=RpewZ(&Q}Ffvw@!exgv-@*>+w+?nZ%l zCQd0NhV#2mx*8_tiQzOx3JtNqCxv*mIF*OtCGkikq~A6=lamWc{I4Js_?%1zl~9r; z5SXB*rvL*oBomSeiD^_)co|M^8Yu%hk_frdKr*B&@Kq*)3S@^Q*|s212I#-Dcj`J5 zEC7Z2nu9Y99XWmR50ZQSUgmkzk-ywe{pn7RMXARqW^!`u-?5ml2WNaxLEEU-?Qbj1xs<7 zZzN*jKi<Pj=lR&GgOX+nh1a zr=l0qN0i?jdL?|*n>Ss*nV$c%v*%IvqeD$xotb?6{P%CLPv3RXjXz0Fe*extUFX;P zlTTMIDZIJ%58G3>7}4Yh{LH$V`~KzO33p(=9(;bbiKPos9=OhNj}EsSGV&G3QuBN~UAtlSF=RibkSI(-87LrDJSEORiKIhf z6eb89R+&mB(PEG%5JCh&0LdT}9e@Ja4TXUS0E7Np_rX(rjhq0Md!un+dO>TUy!by8 zJKwEXdDE4>=^pHOT0;Xoi;Q^KBZHe%D6~}XiI`5W(=L2z^EX+&XXr#uo>;rfHTLLi zxiQ@=}#2?BASZdxV<;sw4{nvrWm;R8=Yn zHVjBoMG$GxB?y*OR8T$>+X0mlfL!D}FsO82Fr4lFUQB;`uUQnK=Kt-s^1)a5q-AL zbPwpEN-`K6$BNmM3~-`(8;-?XwIRP@D^f9w=7gXY;w5BTTc5c>v4*Fftlb=W?$XCUC|*~Y zp4;o#ev|aw&pc|`wmC9)aN;j}e^q<1Bp0AS-_V*fwTXnU%~AvN!ZOIr=G9XKghw6A78n0*5@A1dN&P?6U^eOV85?sCR?^tBi4^l?G5o_9SdiP zW8S>>K2;v+CvcpM1B~q3+B+)V@<|@GiCDepN=MqebIVvWVz{~^; zJ=u499uZBMk)VM&ptgn z2|2y^_c-)4#(CGeef zbBpOR@6Sg>8H!1CbR9XREZ^Lh>-kQ?dgoZ7;^2DfKW;Sky{AnYx@V>QVM&H$N~W}> zsV^E}5Visk2@@?bld97SqQ+ZUhO?EWqMhg!c}6RGMUTzPLT1`iHR%sb1Qwn(oAdLH z{t0pUbPikww196siD0vvXreeu0&Rn1wP|?sGmDgYF%ej!&CjB3iT?Ko8XuH7;oP_^ zKdgCg)c2CJN`1Kbnu(dTudhKVw00ET{Zm?xnQXjv=2P?cH~r?VcVf5x^)CPIRagJ# zD_gv{Z(vyliHWB=j$Mj3@BLZrqshr%edd`P)Y53=Prgr%Ja@}4U-Vp^tx7)7{PK6q zfBDh<&xqG6M~%IaKfdv*aHNEDbZl-cXy!=T*Fsz*Gnw{CpRmdc8qSPY5iNN}!J=iU z8ecI{+te3Np(-C)wJ7O!PbX_vof>c`bIyzXXMNS-jH5;#AD9R#4cE^W8u(03u93>8 z)6fGq97?C<0%WBCBoRyo71;x0`1xlM1$Av%;!VhrW1QGQnG|+?i7+eIk)j+k57ywQA8+;RW0j=N_E691s;fWGA zIMKM(-Fo1RxN2j6Zs|P{>5aZ}Rx+j3owt~K-l5~=if5%dP^uDarV#jBhob=oVJiR% z${?jvAtk1XJyzTn7Os!H9uD5hqDCfFXD%`kuV@oB-iaDlxTdY&Y|hWOdB@4h@hpf4 zZuqN`se-S?l}|N#W}%73S=kMSM2DKB9!43{Sxax>y+@=+og5ahB_2*TU!OVd+Uu^K zTXgfW`JuqcCOFHN=H|q4y+w$%UlZPdUo8B|J8u-Pdipmt*Wokw{(ZAY|K?16l`$ND zrhar2o_yq?>?<>=hp&I~R$1>Kdoj59?C#rt0oc)|==YcY^{(NkzVw5)rA^Xt^1Aqw z{~4V*YCwhoeM56mHwSXQCLXMs9w>MmpU4ZrL@XbW&1AoV?yh`;JDhB5I#jHf{#0yJ zC|2zs%GInm6;}AHc%AP=pgNK-LM=`e6utYZ5!S@RB@HT>iYa>S%^zlSfUZPTA%y(Lv#fA{RW?`Fw7bH?tu3ok#VvG!7XS0xh*NZ>lRQ=K(C zW2VbTi2m;@zeSt8eS0Io+MVwH?Q;&dP^FI>28hInGP-2hWZeeg$?J4bCQq^|&?nd~}qM)Y3m-_iM&kW6iQ%Gfob`T-UYl zS z67{3uLf3WSRQ3FPNH(MHq}!SdI>Xhj>FUrKwB!?=(uBHvG%o_27}ZsoTt@A{l($Lh z%R^TZ2(XG&3aLq_kL1S#iWpc`+I@ZN>#_yW;-5I1D0X~u`mpn8JDpv6{fVO~chV*E zjVJH=U@(I=E!lqc(AP?zJ^w#nf0eI)o?qOjg&I$Fna>v+MEO-#;<_ z_p#zr8=ks@rzVcS5n6We*jMj6>{vvO9Xq-C&vzeu^Yiz;?p^CVB)n1ey@y6K#|_AU zp>Jv~>gC~_tKOkXxv_%cyDFmuGt<0^my-KJ=v|nu_eEnJ^+!?#*^`*LtR`LS2+!9q zKQ!u+=KPn?k)U@nuLSAv0HAkYGbsC6EJy0(Y=0~ns=a1=AqO3b4kSZ*5+T6lh0G*? zGYSce6RH-#m@F}qD!K#{jQVnnD&}lKk&Os?XQ~QGAOt-bL^7xVvV$sMkPSfZe11<~ zHmN!V2vVwE{V3A0Ae+1J>#_NvvPTY7tc@-h!=hht4?Hhq=eWT+OkQ<0|Hv`wa;moe z&DMKe4dO(UlxDkMb_)2|$nJf8wcuP9J7>FAjn-YtPM)ew?$j=C?VHJ_nL=63RjYD} zSY;hPHSUBVTA_`H4_N27#2k;6vd_fBqEuP^Lmb;~#;d)Kv*CK1K`=U-_Z|JW8!Hi` z-O_fQO>l^$9NKW~q_wVXs`xyj`~RDv4zcfC%U|54#~qb*zNBqIFc~wJm>*`paB-f? z$Vpzq^l)2|%L?a~C3ytX27xX(EYm55XFX=I{!Af8B+;URW-L2+MqIh+Y<|Um%m~{~ zl@d-$lHa;gKlGY8$t`t_vMuKUK!D(q{A_?h*b0DB0YF@9a{7w8+g8;Nk`$i8#*M?8 zE0*l>6{S>a!RTa2nhrW98=KCsmUOO7m~k!{&l3=!)EJnrWXMt@r^eNsi2`zZK+%B) zmiCm3lGrfeXj&iN{vGvYr$u0hR~(zuR&1F(DV%Qf%=BI!-Jg&8AsTDF+dLQ_f8c5y5bLaa<1%t zpofLe-gVC}mR(H4+Yhh)+x>5!yy>o28{6uRSZM9}arP z^|DAZlaLF`FC9XyJ)3kAi%i9UMO)r4BorOVApS`Kd`?gb5R*y_-~e$Ih(@WxH7c1B zgGu4hG^&`Bx(6sgiL@7zKoCiMECjd|5roT(ftH(}dnuC82uv`J74&eVt09-Wa_e-H zZrYNXN-bv8Ho*$Q*qJya`OGh#0^~$7tZ_78m_0nWsv?Rr?ot!$}AaQEBmLLBJTRKm; zAIo_N{)thX6MoPJ7cDO7Y}L^tcU_%Jm0L;f*vOfHLIvBkc<> zeE%}`;<0Q@wAL8Z5IEn->dY27(`t6-(aIKoaae{`6_@y6kMV{ z8(*uu&3VBvlo{ca^8y-nj)uTGXpeneM7mrnYFi8({%ByuQ-bxX>!GoI}f zc-pT303ZNKL_t(=4J322Cn~nq={3qorMY!a)P;25la=8b&!mAM861g(^p%$nuqIWk+ zzg~8rE#0)NJfEhD0822lJ)WI$?s2Oqez)ld&U$jJ501p@HGKv!(K(@r`9Ur64*eDMNP1a0oJ(>4`R*6Q?0(&z%upwg5qbg(Jk zk~0QC2)F8MV#!h-fEOt#9RN&8RI(^6aZl@}KTt7r9Z*`u#aiI#YAYbgb4LDZ${G*hElSb>UgNPMXYfF3Q6RMPI$*U6zAv$oO9d zWVZrPPzELgM5C0rveW=%1_}^v0J5h5BALjg0UdxM`)85>LayPEt^h8P%9IHVl$ARF zysuv~Z3-KFzC}6|?WxI>uG~8Q?nfrFM2-FAD!lj_B})V=Wbc@hIV{e!)~oXeq_KcY z6tw&O-}N+#r;Zn(Y zdckKMDidq11ARyR0I_;kZ|}op&3(aNma@M(qt&>g>wkr}UnG;24P65TFSTruxfUGg zyYosjA=b)?eJ96M0&-NMYX_H-ao3U~8}4lRQu=#O z4ToLZj90&kaQV$6OG{@)9=i3nSJyj6ep=JC;|I@#@3{S$4GoS%>M!ov_#^wkgdrQU zp(_`AN%>zB zBM4seCvp2*GnTh??fmGRU$!mcDUB_Yh}G#h8&(`gaILFQ~uIgliHdBCg`wz zlBEiqt9j~B8MAB(!ilNa@qSsXqo*h0fMviUE9>`+(v_DS$*lXvtn}`gRLP~W{5_W$ z2jA1j49mM*nkacWr4kb@-(0TRg0K|;rHnEf{7CU$*9)=*i#PNhF^x?N2IedMJ*DDR9pfjoIo6shHecNN{_tR^K2o~5WAYpO ze}7)pzPV=Su2=s4T6Ot`FUYVz!98tbQK(M-MAKD0w>^>?Yj~%y{}y4Y{MUiD@zmt+ zu7CK3YS-}3J+)`P{@B$0H$HiZ-@n`O@K4)*<$7ZhGGOSQ3mp|ZGoGuhbqDp~5hbu- z;aspLJg55X?9_>Ne_*BVwd3W*#?ZXzE{98(%9RCrB-2y3!(BZT?zv%jzRndX>4q~s z5((kr3l4a-yeFNi;_`5R5SO%W*ApHXkYPYJAj6RTGzCcns0a#52_$Y3NQ496ATT+I zBM+*WypRTTd@PA`f=Qec0$d72P|hUidfO9+XO2Z0L;?}Y()f}1QeP>1)9oYM4;%J8 zoY99b!t-y6a%i@rxiHQ}6X2YcS4$tHh@)Dx^xs(D_t#6O2NFo$@wEN+wrNyXuPUBq zx&oW~`1DebWpuhv%&3lI>=WKIkxY*Ht0V27c4)HCRoOFK25iBsUUT@McvH>M_8+B< zKh9+83$e?-gIynH21}cJ-i`SzfQwhC?+@O6g%y`QB)xl*S}+VJ}iInJWYhcOu+&X+_D8p&?Ojd&O~Mj5V@c- zHGEp}RvQE9U6Lvii^$78dq?S-O-J+Re>Eb#H4@XjhMxJ4Ys|gdDx)0U<@tG&ffEi^ z?;mH?7KE(;MA#7RnAmLtrs}efOv#2`Y5iIXuT*HA&vLBLCxiQQ&BaWp67LcEy$!RK zC7I}ghCad}0!FZUI$A0PM4$weWQSFfr7AblI;gHRsvvVlPtU%A>MO=`MC4XO?P z2@YpcXO|X@YZ@j8S-8}hH{{DUKAV`XY)FiLvR%II?Pq8R{T`kC$L<5xPv&Yi;jL$V z7Rm#?OW*qP=j;ovy8I{FRKu?5p3ji8@)PrQvC+sMu6gvvD);fnvM%+8-w!--#Y0=# z-5&+N|8w8(9PdQ%FPXU3Eo-_j~eO8;gW+@%mi>N=YhQEn=`gs9dh>L|VZ= z$OZt}klngz;Qt}x90?*U1rTLMK?x@&lNDV`ry=7U$pj2QHe>_m$dX7U8^Pu()~-r2m6;m+vqF2J96F}Wq)T4(Np%3hveSbJB=2~dzqNCM)Xl8e@XSaEjB5VST#M+)E>Ol3LhV6*t><(sleTJn zkwu^Omuw}J7(RQvmO#`xmhC)Euc|%S`NMqf&(URhp7 z^sArH=VhmzJ9(h5mJ?zuS-tlJE~+2L6L$K8Lm68%W*%K-JUf<88obv%Ue-C`C|37o z_LslbTa;}gOSW!Q+*Pn`*rEZ;O9WjM4Baj0k|>Q%c+?tef1zj-n=)FqyN{f4t=Mq5 zu<di-;Vkv^XQld?^4h3u4ZN zl1sFd1)2G(akUDZaL$`MPNn6`>-J^JQ%my3Wufp;<+QV{Q0*+< z`DDU1CwkE|xa86M-~Q1j?>l-vJ$RpTm;SfzuRS95|HXDBd~0Cae^2xX%X z&IRhLyxPPF8hp#j)y{a#<+qaK36HU%Ia5y~#$s(mhiII|jYhtyGM!zm9_M6!eD!U| z%WB3wm9JN#L&*>>z2I!tQIzO(43&Zc+?3l37=R8y(G?v~;4&rrA0|F#;QvoH8f3*p zF6J@t|02VHY(SP25r72|u$+&NPwds4oP(4bH^vUnt#MVdx7;%D!HJ5H6y?ZoH{-FB zh7c@tRXGp$FQgDDh_+NeP$>LgRKW+F++~43{?AjsW!lc}Y|rjychh?#y#XPVNL4K7 zspn}Bf%9cYv7LIJiWNmf5CjoX5EMipfsjsmvLu_jz0B5`oj%{0Z+V{Q`wyJI`~Cj! zcR0pf_LI#0p;pn+SwQk*f51!oF9oY7*EXHCD{Mw(C^NpmlbH%PQ>d*zNN#CvP6s-$~ z4!R#-CN0&31*qE`8kn-I7#DB=FYI^$2;MY` zdZ;#8z-3&Ue>(2S_K+0RZJBZ0(QgQ!EKS6hrQBO8hx*g!B6Vu+tf$&lA(}Jep-<#a z6nYlD_uT$Kqkg!P&UBw&``xc=C;x26QrXU#OHY05B6;CP*_Rrf_v(q!+i(_-plbMh z_PLcm_-HJ&sqK!QpX*&6acjyWVnu4uwoTO;y|qwU5zSLGaex|wgIC>fs#GtHu8|pf&v+mb zY^ZWwS#BWyP7pGk` zCg!(hF8Rc_m9Jd2f$qtBk*IFdhsM8G*}zX-OdBJ2KI*@?zu&ByT-ChCrGye1#^j3j zi|yAs`z|$3>Jg$y&s> zd;vQ;?8Ajk3-;tIJ?1Mt7CF~{-+C*r@nC9CUqLG2x~=Pvbl|$^Fn<{N*G?r#%s%t* z68EWbt4MM)!y_IiKJ}4q3-2HL^xCp8sLMmd^6hZIF*I2YDg<hAAkt1(7{{#*!=Fp4$BB!K#4rJsAZdz7@v1e%@7p0I2d2UC=)nlOWpxP!0kN zmllzdR5e`mr|km7ss^U*)!IZPP!6C>``m_nt}T^bTI#Qj#xZv?-CB8ubEUWvEy00A z7Ga6Y44qBELa0tL3iNjB!V#3~k&$mAP`0mUXMpi93 zKeG1u6I0u8n*FA>;>?-i%L^X(G!3{PXc$|+;!EY{mwo$FvGDV;AMbqgsJ&+d?{MnY zD+f~pR%2s0oEz$k6s~HYst%2hQpMe&L=2lX*;0|0_3}`fnx%8Go3dKif>(4WD`|J< zhi@A=A9khF<%pVQNO74`fF9Jk#uqOvMXTQK1yvI75U`xOva7?@T>v^R(*RH70Fw|f zBmgFHnc%{x@mN0VI)-Baa18t}2X%(PJGg+;^u*r5jIS7k^b1SGp@~&yK6mG~&NnXQ z{VdhTe!mzGoUvjJ0}JZBR|YBps;IT_J-rvak(vne*n{yOS2p7K0K@ogzx7`{&=ss1 zTeae`(Ru~$R>h`P*7wieTG}&EJMvcKcK!HhG40e;4a|RELrFs{*)`Y`V*qMr?7hHl z3!Y!{<)r<1GFc}l-}5Pd=Lz?KcU54NDaC~*RsHGVudXlVX^T=SJ#hAx+YB#GK;odFJ#D5BB14s zAoYbC)Lh*!7%Wvlg#K8zl>r5zm9*JMyV#nwr?OYx*&iZs+>+s2OIdNUlPRd=n7(H! zLN$*8YZjaxj7gB~xQzRZyQ4+gBx_Z5FmWMW5T3RwR^jc(l3ayZ2vn4FLvlv4(3UDJ zDhxD+hUlXHQd{Ue1y_QqX@Q|k-V1Xl=%ix~7f2hSI?D+*7E6Wltf&X@z|x~xF&=Az zqWPu4^QGu}{Cje*2+cUbt_1IGb7GT)bz=4UK>J z|7T~nD|JUy|;rraJ#N3<={XyT5? zst!--dhtTz_2Y&G5JPk}X=B-?3&t~4mu^Qoxnx>a>JdFq5v(wchV-8bD`yrW!F|Jq zM+GjE0LmH{TmbO!KU}~u@c+fFiO|IL5(?d&mZn*KsIq$up@GP7goOBBT+AaBZxgJ&OEaE{&I>?)g70bRQg;m^83@{;sYj4rr>*=pJ!aHZ5-P zT}| zs?1kz@m~6qO2IYPhX=|*#uNki%+aB!1lbONG07m3c@?gRcp;Y!4uNsDjXj++t zxfvqXDcUqyo|z~tnHp*i#P!)j#ks~g3JRq#UCQ_*xxr{Sr1>U1FF5#| zxvWA3jL)mi4@jkiV+fb6 zEOq3|kxJ?qzdiM6Rf9OyWntvrALF?rUD4>oRna$y4^65nzjQ^-g~coAu7Q^RL-ijJ zN5?Z+)K?5IcpoFCTWy^=+EoL<+1S4GV(7i-UHXL)=MUMz8kP9izu?u5i4I)ba9ReK zr@$)c@xn%2mK-X% z^hKe?I$~Y7sHU0`D`GluMGc35pq}XkDbbX#JG$h|wNs_Cp<|ysU<4@#qb)UW^|3W; zPfu;RqhEjFw1to*vR}V8f9Mr24mZ5a9`Oa0Oi3DBFxpomLAC?F1|Lkt?SibR*L5c_ zlnF@7`sZDLnO-Noh#mKAU6TVJ_jg_YzTyA;)w2VM_{V;B zxVD|r#8($y%%3%OR)71uZuob9Tz&k`sb7R&tA6CpLUeE45B~b2g}e6@;R1T0ag*zM ziMZ8TThh($@d&OJw7Q~rnhWp_Clc6JRw1ff5z4x8#YL>GV1d!nsY2W0LdIcK4~L`S z%DTUI1vf0>;C884OyvuutQFc8^mMYTz?3h+6on+dFm%Z%-oYRhpg}4?GDyi>T|z?O zG708zKzN#0Km?kn;h#Kc@HB1+fJ1P=iT(8KU;>x9kp9Z4qv=(;oxE?&$sOIcn9x}A zxrOq-&gC0wMpjjezl}C3P(UnLJ5#(&Ev4tK<2lh@7v zeW}*bm8d19wH4jV78-93H}t;M{2}k~L_X~_#(L*|OjXM2BsEWiXD{41uy5+FO$APTYMLq6bSpHeDXL6W3YbKKX;2Q;NPe8^F7Zwz zU0;F;Sr%@7lmH(^7r0gAN+?HqPD)gYG)$KC;+u#cIRM?1lbON zapCJ?)GkUV)*@0UGfK;{g$EKDSySnFfq-hYPU`h>+CHQf4yC5^c(`fEUTpVp094TF z74k#^VGgGZEggfW0d=+`n=+0JwkS&N((E~}R_5cO=>RRRAr~|AuNv(VXTl8=O}EHP zBgHdI(*t+5{JszX<HnIOo#?9CVgB+IT+bDrGZGmCx8Sj%P{~QmuXyZ1{jht$ryzK z1)wyKdSH-DN%h`mZ zbLugj$QcuTF$uCA040Wya$HYFgqmp$Wg}q}WrPNKNkFemRE86kO%w58ZJaibs6}Jr z(@DI#<)T*0IFux~qf< z|97A8%(vL1&wTgekDus;UNH1R-DX+BMBHkvQWJV1Vc=e|n+G({(tBmSu1AB43aU;N z-Pl?-l+-nDRs_P~aB$ZcZd~k`rIMFPS;^FtwPbB6GJ4hs>LFb-b)9Gh!`H~(EYl$+ zD2NNg^_?!cOy2cs)NdinV{0}ZT7)p&lK33}U zssz;qtT|JMa%*`h^^Fy|DE=u~i+8tseZ=3N%qGiC(Xr-R>#eF__V@M0p_tO( zEvb7Y9=zSTwDptSMyGV6N-b%T=`vH%S5B3c=@UhZQFH&)zb&4kRV9`>H#MYbQ1;rz zZ+785#z4gbzVp-l!V5a7H>SD2y)@;h(%Rq%04X56Ma4adPi|E{wcRG62y;y>72w2I z#5OA(&oCTQ!WUY3)F+CNj=~W$07eyBY7TaVmTx$fx%S_O%|G`h@cNGc03ZNKL_t)i z?XV*zzHz<#=1$V9?BMFmm`6bcrJ`=>Xm?D4YzII!>HA(HGS=pI%_Ua8s3~aBIdz57 z1+Az`X$5ZUb;FoGj1}Ffb~zqvzMvYs6ey^2jHXefvj`g{0ghY>Yoe|IJ&@&3dpWIrrqtRsdxy{f#R+KKqCL>HqkZJ(|1sv%k1yhIqSsSI{jV zZQC{V`!AQmU+Y+L^ybc^jenQVek!e>vYvn7`}f{|C;>QzUTEB)O8!LLYOM}UPVdPY zv1NsV#{I-G7@KQ6r)?oKe;9xcjWfK1L*O#W zxBwSifCGZVY5CF7kvNr*%A?xC?1|h4Qq0`9YH)u_hzW|ZhwAXaAPEJN?Yi;&Sc5V_ zxjT1RS69l5R#2z%kvE>Mu9wGh8lC*iUH*S{loTz$x8+G1^EBl)=8aW;eAArdu~hA; zQ*9rz_os_f#p%Ih;|-04YLmZ`3n(zDImLO01|shlZ*Ti}&usjY@)>}??eBFtrAnA< zy&ai5S2B zy|h2`sq1X!6}=fHsff;{A0W0L@_HMprRP&BxaM%!X;&vMIz}A;Qos()IMQXTTYo%# z&Gz2V-+D7{Fz-!#`v!6F6?F-&xukm6L`;T+f?B!oQeRAhYzM%(pb&8voLcj&>THim zoh)lGZ9$eBwqkScT)EKM0MotoiVH929iu`S z|Jqsm>AOnz5B*Ag=!v=7@oebvOs4JFV~8k>CLWo-C0$aq5YFrHV|J4n%R+09qoGtHY`* zSEZO_Kzfn8f1)B7(Pj9NzyCGXQhMv+NNw_`U-Ex*y3edDZkA(&E2Dr_BeN_DF zKrwFI(Eblmp#V{}Zsg6(m)4F|>H@iglgjozsVbHp@4|a3FUEdc_MbeTbOToIO&yO8 zrpDo}nAtH}Pr1;EGb_Jm>W*zWQycT!3dC1_z=vJodH~BHX4qve;fnz0C8wkcWITRY zV-irtm4UZLuyR#r>dHHNs{h=2<6jEY%Py-^9W9qID(6G443gT0<2*aU~S>ju+_Mta#v)_&;X}OVD0FryJp$N3%qlImt3|XdpeVy zZ1WCg{7vJ?XLr&J5Eq z7gRD~n@Z`LWreu(HDZN4pD9oP!jO`x$k1McD+U4y=7JJHKuMo4;&Gu#p8&p(f07Im z{|~_hIFn#_7l**RI0U%hfB?ADdjA0@PPw9#l?zMzJMtSHk^Am~j=xMgUeI% z=v*_ta+bFzRd?pt{10Z1Bs00&xW7h|VqCax+Sofr7 zSjE>lew`ie4XP``qrxLnx;|f?*q#5(HECTH13_U^Reu|4d8`9xAxy=Z(O zDnYgbpu`8f3CqS@GOcHb2HUZ#X+^Ec~HY?})#sXduuZ*jBzUH%@& zfJ>hc1;GI@bZFjXZp} z_;vTCdc)dcK0~V+@x2-=G~XQEQkOlNYCL>q(e3G@$yC8<(T5h@JjLs1G;wKC3I@0p zGk3&m-k=?dr%Z>09AB&A zwlq-+M@u?)9Lh~wj3>fDPjXtRtaJTT3V4uiY}Vm^zG~&^#0{V9iv8_U#)FHI?``$p z`m0KqUTHz~uCb_w%hbw+eV1YqWIF(^s1J9{wRAT!GhT>BQfM5_)=}HjbXbf7G>ml2 z9IA~BMrU<;Rjw5cB{6qAODU*ws)J{OGK(;z0M9uw51JP!Yo6O2E9nYcP{Hf%&bvPN zIhQkd!KHnf!q$05$K5mY%l7IkrjDCuy}4agpE+e3`%1imWJoo8J&s_%^W^Iz_dK<4J9#rvDn5Me!*>ef%@^Zsg~5TVo<4g1<1F_H zT9y33lie#{1Q2h*FNZ(y#^*nN(E@bME6iJhVktT9g=d9kMH4f^o+{+se6g4vF}78? zg}p;ME(=5|8W%8!P^Ziyp3tN(iB1%#r*X#zhb}GCmqhtG7|T-zMY&|biziGUAW)LI zI^HEI1PB$NibE*^>ifQAAb>A8z@*?1gh_CZ1Bx<9d;u=-4h{h>0L~s^a%&?AwvBy*!8gx-P#qAli(~B3<-bA+Ct0yV>QFr;}!+l8O2U zo_W2xF?VtT#r{X{R1fy_RtE~#lt(}eUudkKEw3EAx*~mkyx~~Kx*Ll7;$H34DAsLyRpD}mc1maY*{!wVL>!E=Cl`jqY`900M3QtrAZV3DRs@0L3qmWpebF^ zGBih%2)HqGs6IGkG>jqWTFoqO%pT8jT;@bW7lUp>uY@DOg%$Fkd4aO#AsnlO5}=?w zpHBSvimwSBN(Jgtw!LZTo=LA~;gndmwAjJV(rF3(PV6XS%B|yR<*xJA|M&VIJpcFV ztv|ctBWuP#_Op)K3-jN*pjZs|h`${u-SMILy*-C)*ZR%oM?OW#;eYg1fCW$YI4}c3U0FHMlMu@~aSZT$5 zJBw+;7#9?|h25RjT3IUpxasge#)~v>7U`RiUmbVqxwD)Xj!0JsC}l5SGjJwJtEyl% zKKRt`@HFR80<81#?fx$Yx*GM&w&c#R0EL=vg_miGjZt@hq2bWmSKgF3kjNB@E23}D zytY{fr^AEe!c>IVk;WbI##@tv|9)-%_zTulz#jYNjq0}-$NDR7$Ry!ulCkp4@?LrO z0!K5XId%3-R}JU7+upwSLhuH&r}=wRfv0=oK=tXRKS^nY?Cu zZ`I$mOl%shX z!m7vxg#!(`sOH1fp%EjTl`Uzj#UJMj28*BooND7D7im2#g{N_0g***j*x|WQ>rhn< z1i*PdJ8kUVdEcg-cnCL7hcelo#gl5|!sHp_gw~v;H&#yn)f7dmHL@YF?+3;= z=x+|b(6si4pIdoHYTkE;uRCYlp`(%yu;2c!@veVOf3Bm`Vx_0&KlO1UcKyj|PL7RV z`v{;i)gO_e$fh3+-uNWm^-H(vWX;8{oP!8Zh-hA5$?%j{SX0*=P=xf4Ms>5&)FtPN zdrr_79`%`&J~g>=L{Kqb`h@$!6rK*t@P%nBoeSo9nuanVrt6mt&1Osmq<-d1fGP&X zASDA8N>lKm7!;E}^C+ld0>lRsP?Qkg2k}AR6G^FJkcvX!U0iSosyGBEk}`+mGGUaK zeBsF0+d-j2LCl@qdBNIbImMqhz4gk3?d7SUyCLo!VIhK5oIIClRg5dmYhT!T)`l6= zi_H%_aWGITjwONdUwugYv^UthmsGMk;O^a(y>_pYElh9hWr9;%28ee)XiP**a}ZlNzhqkuDO(}jcb z^Rr!QXhHBH?VQkO~IK@}F`?GvGD~pR~?zmV-Ddp~*`Mn0(ueksq%C+7MeX&q=4W~H7*(`q7sgP;dq*-0q`^{+B8}Y zL>OgW!Pz+Hxi3$@pXvYt^Sh_?tt~rB`S>i0Ua=r?LhEP>rz;mUtbb}xy*sTOj;=cM z!{NJIYn{INi|_y3j4!5c`_$9*D)9RX5i1VxjvWnaHrxMkurKehXWM`KN#gB$HrJGy z9Qxo-DEw^YmU?T!HQ&x$_nY0>K*VhGf zsQ}5D;DXp(2~eL9U2#FF0({~!)AB+Jgm{!GC4F7$M7d1*oJhlwgh~Q#he1zY3P>;j z5|EVpK9mnYa-R}OC45k&m}F1@2o!{&d_egeAW8sSG6)XA1&07MUl>Yq-v_{yR5~fG z|LO~LUv+>HM%}9R6TQyLl578D+M$=neJ9IZ{dM3A<6aZZtq$bk<&dHdRnlG6_qHWj zt8UHt@5hfs>Ix^vfy86C$_FkDwX4$Ai8RrCkAsdhwuL4(hRJie#e2`)v>|nHDwED_ zGCHT`v?Hl0NN?keH-`0VLcI;&&*Ar@BUZ?AU)J28+IiuX+pBtne z>xQc=t*V)@V%DHihXjL!VG>Vse4hmM)5TyY?}RxK<@nSAFv_GC+@c>C=Bt)pnA&vL z5Z&36uuRD_pW7<-AEx8bXb?$H(*=|cYo2{RDnYgb@CgG-V?kjd#BEbSD9%Ae`*>D2 zsZcgd2s_MEnwhJv7}BOCtadpPT5!SUxXfv=I%7PG0Fwe-SUNO#0Rb!4Fl0DJuq1_7 zw<+?%530UrQs&!L?foTfQ}qEgo|u);ubnn|B6uuTJ2L0$zW4vB`$FyfWX#<-{1NvX z*Szn)&n#W>;12`e>bviY$D)7xQ&W9GB?|20mm*Eio1cDZGH)5Lw47Z$(V%W_&?ZL* zKl7h>$JLvoz4I?I!^!8kQrYt@3ly_Y6^xLjyAF_?yS9JSb*>E02@pqtKr#?vz!!$c zJeO(Gr4)qtTO2skB>zVLP8Fhe2U}S2)u&}&Kz*+>7FiqLM50~nq$rWf}KxXJ_M+Etxrlh9|8;=J$u zZdXl9=Hvv}_>(t^Uw8F15pmtX-y<48(N<`0;>oR5+1Cp*_MN+RU1oQ(kS?!6&+HrP z$}qIibB=`O`}vA#uO(WyjSPQb-N@JrSx-zA9$X=RIx&(qugqNF%H=?4`RGCR`PraK zMu*S!_0?18Dm-Uzw{er!*Z94Z@%Y$?V-(bjdj1h7@1f8fI_W?`&{FHVzSo@_R!OI= zH6Qbm@RjK+rXWoY4NCa{@y5J>p9$2I%6hpsKgyL*}pLh&x; z1n{m$-%cqgmx7Ae^30`}1lbONF~Lc8in~HaXfX^3E}*;aYWos6Rc2nVu#0YxPSwyv zT~%+OVWQmXO+;pP2~Ys#=G2)MB=oQp02h|7d72l8)S-&0}ocTi1L0 z)9?MkuJ_`OOS7}tywkX(6KjrzZ+uRl$X5QX73f?tcJnp*sd)L$ujUGe<@tSg&i!%7 zw5^hIrRTD|F_-G)vz87QdI5$LTQxA1##TNvL?m^o)ZlxXp_I_663U~1P$4MOR=V(l zWtcV-L{~lwlzmV4G*1!=LR?>&OqkLJKs4XylF%cgM4FIbfU7GgLg6s@Oe$STN(6&a z6aevwWD-#8sB7~NmV6;(>iZobHd8!Bx1 zi>d>=25n@yTlpfquaA1o)Lx|cOIC|dG!WUexbys&h_$fdobUhoV10AySQ1G5@jCH9 z?{I^pS6MqN0}6sihZi=$+7g;NBx`q{y?xW@o>V4VUWJ~S8{4FeXk$HZIh0XFuk+qW zH*Fi5ym$H7$aCeKU6KCUTK}gbX)kk~|FRLJF4T;bCy%l(&NM7(vdJS;%!USs&ppz| zw&^`B-^-|{CwhdDQ!j=K59CK2Ew^CNo}PMe3N$S1-ecXgRBF>}-HL3Q60=Mq4&j1v zB6Q`$bfub9wq7=T%9(p^xIjEAZK@huj}P*-OE080e!9PUG-s$OL9$m*b9*i@Ssz0NDWZ z*2sWm-Wb^H54vr`6>0`~27H|LrH!Z+TRoDAv8u1e+KGcPQ_^LqOi z{e4ZociqU8MqSOPynA6oFnx6>vxi4s@A=m?BfC?DTzZ4-pSii!nwlOSjrWL%0Cn2^ zou%p54UK2S}!T(1!s{r+0@;9x4NHs(qMK9q0G8{ZfF;%sJlemHSq|40a6xU%8( z{@}HIsP#V+-m~!mRpsL`J^#FB%j$|$f8Nt1;3KmzwLAOX71C>(#*~$lCMS{-P#8J| zh4LtoUYtrWC4EDdCdYRLgC3wl8`$9yW8LD@nf3qCSM{5tu1gi2`u_}Ug5xr$C4Q+zr1byqLkp~y`nhJ{+Yz0IM%-IwX77IH@hvl+RZ%teje*DFKirPPJ5Lls z?c=GTWzT=*x)~o_dHruN>NXz%x})_^y%EYY=2|Y`O4a=zvDmYj>TrZ|Dnwm6Q~K`M z&-z{HE-=yzC${Q>WnmLv9CU2T6Gv_b$vQ;bT@)u9x%bd5MZ0VPZ+qI||6 zl_Jok3&NoZ$q2Q5gL+QD=l0&L4lqb20Z1J#QDBmSKvGJipi~myyuzR_(<3!-5s zDSm0pZFA*%eQFn{uHs60>ldFsm(ruvEW7^RpPs31NFEz`(>?WyJ5 z1#g;~IW17UQZMWdRK7U)(ai&|Wu^+{Wf+{hsb(xYrzUYGt9&L|aav?&q5Yb{iSNuC z?%R{>s&di~FSi~{3@0O7CJ!5cqF&3ovxg#It(}ZkXQG)igNbUUDA0DK+ql8#TJ)8% z2A|CRMN`tgqhe9)86=Z^q;tdCBI0OCf4jrn{mN zWINu$0;Np0C__}|cCiG51-V(GLN~J>WTjHjU8la7Z7EO9mP3JtF>iJ{Rn>A46pEtU zJT_1#(xH%$!V3sXcSEp3o@T}B;#`CRWg*dcg}?LHBTrT+rIlw?bk>$O=?AMjf(^2= zZb@gq>{#d-6<2D(XL?z6`^4lGRr2e9Ke}+~lh3|){m;MK_lNWkKMI`PUaJ%@T)Os| zJFlzx%-Snn3t47tc|M&lcV-u$XiI=G1Dq#+ST*)!uu6wp>mM<<_JmT8oc`=602gSU zChTCiD{nQovz5fAf)nXzRKRCJmq_jlsWeCf5*#kL)FqTpT&gQ73|BD@YD?i1?pn>e0WsO6CDxxV@_u*3^s80!X3H5#ED+;BU1oc6PM+q1dRaE+lGDt8$ zGAXF<0~`Pq6kKrS13o}}C@ueyTc2_FG(&+Zu_bi0*IZZ3vxiwOn>V~bKvq_l1D+$o z%zApPYKJooP=@m6&OLW3XI8~zVb#~ax38`~-BWVV_1d~hyhA=YvZr1Pa7R@PEt#iH zT^$)cZs>m*ylMUTiPU7?S}jHvtS@*otIgij%U%%hs@^lUH{E*8(9{oF`_J!5U5e83 zLrVsqEOuv_uW*h93`M1AYCC@{_~pu6D55HodnbgHOnTMx_w_`s4fM|W>Ud;GPZy0I zwO!b6jF;oN!mK%O4K^|Ws%rM|zU<8lq1CshD~jV41}Tw9o{Y`( zVFj`s00VrINl~3r2!$-Gr@9YM2|FKDLGGH_8kQ2Zp`O^<001BWNkleyjsJZ!#(U~iDc6>-a^~JuwPIELz;#HPP~g-rG~H+ocw(kQr&+`$d+nS3i2<|fyPFGv3n|MTFC$&wLT?7UPz zH`x+@boX}$paTVLX)V)Sxj*lNZ#q=>m;jORm?0(iGzmMvZAOJp zG&qbg<#WMHP)buu1UM-g6mWda_Y5kiX6-NNiU1H4OcE#x>JUa9Dmkb}0Z9n;B|yPl z=KEm2;=*T8AVe|=245+k0fGaBC;-%l&jmny2#w}%-}Y>CM@>X)cfS#|^7_0s@Xf-9=Vh8MI5 z*=A(o(d4VLG7xfeqowlN((vpR6``7_)_pOdF(S2clYBEf`|6>@{Y}G7E^~Dq@D-r| zetA)O8h3qWEgm=;`Eo6YXNE>E9_+27;A(a4j*;NCT72FYhy7m-q-CHO-bt)4W+pty z&C>>@<|^MuacOc_@wOFI)wWFrooon>p+sfD){G!wjz*y*EoL=efJ&Qb&Peu6XkdVT z71>>kwYLstulZzG^)vC2obKw0Z`>qz?ZZXo(kn>D)d0zua$DHBAV9VQV1#)plh&aW zg`;^aZ0m|rrEG2rKyaIzwy4Qlt=aTEXSk{=PG=^wwKZLU;(|LZJ%cSsgbW9Ok%q2W z&@~Te#1xDuJTqBdvU%{W-=BRtC^)s}s7sOd#mSDyv3X5rg6r$MrqcbZvV|+Aib_f-sHH`%b?MVS`gE~XtJqo>tXLOZiW>?diU=eiJAnj3LiWsLCbQ4n z_uSjHopb)@jeXwFyY%^f;bAHB5aq;51hOTR@PL=bD;F|9)C@eFbW(^!ONMZ8LODUk zO8|_(fSeZ)9weNEBQOC@2rwYMa15}r3KojVD(QrD9)NVhGhy+yH{Q9j*BMqe^k(tJ z@$iC$cJUh{tMT7`>+#O5CIjF~wXt(wd~u*yP(y)QEykB|SZ z6Q}Vx8=l_V^O2=#s}E{=cdMyL!T8mW9xq;}Pi+3^l)UZaL{zF(ehl;Z`ID)Nx37HY zL?j&t>E?^~KDPM!Z5a;_r7mxk4NGL9h1Xib=`3_MNG4p6NavGA*gw9o$WnqS(aRqy zFF$@JYTIu<)cvE;*?P&)Ja_Q&-hL(1mpJtKE z1fbwl9u#0KK;C*0iqugAHp&n&ly!k62AM!R$4)Rw;|ocs8(dJv$y^B-(z5YbQ;*Y5;UpXrga-&LoiI?afeRQO(t)f5ybT?D3AymP zi|*efqF8#Qk)As;LMv^%`#<6e;s50UQNQ!)?z3zn;l!dle$C!z+9mER)vx{7M6UwD z3ul$c9R&~*cfYu`;6PS+NrrM^n3L(_S_!3^bBoEg-3 z;8f=y)n~W-W!2oaFvcB?!jDn?N^&CSc-zVs$GQp79IhXGFni+$Stty(BQx3;E{RL6bGm(C7ewzx2O-^>+z?6rf9k-;Zl+B9%%#ny#ZF#F6WuKMSr?>(`z zTpO~?Q0Yit_eip$-B_k|WA3_F{cPflsMe8X?L-v^OtWPClEIm=g@N|qhd=tMpB*U8 zEhV(3`+DV>Q(yY-zg2-O%qrJ(RFNkq8)jL5_xZ62G7{1#Hf58P;NeK+(nQ{dG54q> zRz44NjXcN+AhIA9PB{n|CKQcJbV;PjItpO5s6Cu3z=H%^NH|H(3+n{{PaeWc24Ddx zt&q7CRzbiC;f3%54u9YTStXtD!b8G=n7oH25CEIKZS8L_DCf2qh!bPyihEAl4wqG4 zai)_&ti2P0bL%ovEw}7Yu+bXj;a@0jHZQ!G2K5kG=gXHgawe9=ycIf^ThCNjby9a5 z2~mw*4)#(wCHF7t+;(o5nTnH_H%HIEVxZmub@XuJr2y5=;Cr%d+g@7yzMg#ckz)&; z@zn2DjD4>@Io5l{p$0=b0?ygLXW0iT*7q@0k3Kd&Kty6g*L~%1iE3>6$fUcYdcuaa zzW+r17JEEJ`m){;;n{+HcHY86?Kf^^TN;e4nGX;pc_EsJ6!;Q%jb(nCeEEJFI(Jx(@VW~PL9`<&8I)}YV+96d@{aFHL_|uAmu!QY)Nz= z1jG&i5*}i@p|X$(1j@65hE=R`ORUJ2xmAqOQ#Hf2_WHTmCB^Yz-N|%iVZVd|*bFyq zI@K<9WEo5$X;yM*1RZXCW%o3yJ$>`eE|0cP?ElUWcb9ysXtsQ`i|736PgX7EYI)jSCgF}7GfA!N_3!lih9Czamy38s{*KYh(p4L>)(uHOD z$XnEd)Fl_go_f2)?Y^k7;zZVmyfwG+xGRxyqeqPC3%2^Whos1%9#M^ydWkgSP7?$2NQwVdk`#%fIWLb4yVbC2!B8x z!jp&hj@%lh4ZN1lAxJeqvNW55br7m0MWw6^!Q@&mfuxbTIvckxX^w5ae3=hi?@Nat z2>@{U+GEf3EgK$>zeiJhcFxWh-k%jVeq& zx(LtG$N9zwm$v54t8cq`taSU#2+L;w_fh?lJ(Lz3FI#*6iB1lHbn}J#9&5k-%sA{& zJtkp26*2l+HAW<4TpV1;0|~8z%yK>GbUxc(YqiZc5t;mlqe{hgFCmngCQVx0B{!4lgj;DqyP1PPkwa0CC zYYvZm?dQ8SgY3D*Cnm33dARe%=UzYm;0>?b(^#{!cHZ97*)yw-9nG3AyYP(RjxZj- zXZ(uiCve5VSsT?#QQBQ>tUT|VwA$v=>rO^#{?-Hap?z9Juf2DASj<%hCuJ>P)vC-^ zH=NqOSsBRGdF zLGE%7rKBrWmp*giLn2eu7K(|Og!2Hrmri(%T=+cnl*oXjF@==E)amm0F6kwl7v#JY zL@8v*qBVvkJM&HGvUk0UUH*?de~+_0fkzW>uU;(A()kNa=*noODdi1OSsX z+H;k+&P8u(>}w#%n%;7T42w-5>sdWN8)w@W#?HTTK<82Cb0dd}nnASj;Qn)tu2Bm= z6q9>*wHj<1w=b)HDLqmf+}4D4nG zDMyw;A|_jMN!~a%1%{=Zw)8(bm#ywER4=({Pv>vPCmLa5C%<@&-}Qh$fCgPK^vtnN zC=l}DZ3~AAfY<>*T1c9x6<|?}i8u?hu$&UH1wa~U)FP;kd0H}!b>r2g;Z(Hb6n8C- zmP02cnz9)1e7iHutU-bhc5-#m2B z^&>yJV)F2gYkqk7wWmI1N;G$A*OC|SJmcnp^6Zh({Rghr7f;<)KI`O|>O-trJOD9p zFkQ9uc)H=Km8bHTFKj$|qN6jZj5pTKhazZpjbT$O8f|W@opr(-EjHdprhW^3+v&_UX19|i)N zy>XFe=?YGm!c2bg{$I{_cj~a*)2dw-kd#h_KKITd@-Qk1g?GjW6b6%~vgO5F%7If_ zLWv-%P1@9U^i5ae)>RA7U-7D5L7@H5XOcY!$(i$yZaKcLwD6ypeER9>cv-UN!H$z( z_XpE~3l5Acu(GOW!@lP>eyC*Qq7~DVFUY^kn(Fwc~lz?Eaou z_*OnTtMbjAhn)c&BiefE(d^A<*NrTp5SYa3wy|Du8bUGm27yzVH;}N_6umO1TE8w8 z0Ajr@d~8f@-a0zJ{pJ^Y@0uN(S9zX%eG7IzYWL?>pChVF(n<)?8Y&$K0kH#sgp+z`eG zX3rnJXV3lnX2O9LuQ_zJ{FhloXWf6<346uvhF;w|`)<2VHQLJ$uH8L&@sU-JM>Lov zH8tGSd8HMdz*)Ba(&iO=-+uL9e)Nm}SnpqdJ2`m6=WcFb!zcg#H&YtOv*oXBwyBkz zXkmR{--b4_X95~Fm7zqA<*Us2>h8q$E`*aE4J{vyPm9qkoWnyAe zu&Q?kODA#mk!F8u)+Gqq&^dq%2(wCWqg{dt!l_CZ+U>RJ@eQ(pzCPIzDuT4C*uAUy zcel?dmXzEe=XEtJKmGi*z3yzauqOESsdGQFYT}uxV~5usyY?OFUB9?-?UH`;>=jS{ z6mPy~>tR`IY(A0T40X}%c}Ia>>``?56&L(^_*B{zrR6d1424FEM=$9bZN2#U`LBOr z@ZUas%i`FjUwi-YvCqHZlSf*w`O{DSc2*SDwKYyV|wP!BORvittKC)YW!1|m)tW&R`;IfeNN(Pw<#d&1CHb%iJBfJ*S2|ADN zDmnsr4=;!z;BXo_DRKqxB{@gz3Gfh1WI+rUct;=>r5}A?? zBo;pWXVtKs8nmYSRtTR-EfUC}8HP34(G-QGATmN=5cxddTA~VMSOFzD7nO=EQVLUO zOL?w}88JC8;Q|BI=zQ|4C@SItBb3@Kj#-uUubHam?MugxUHuBt1=Dfh_+p?4oSu2| z!S3PSD7g)_N1mS=9iHc3(#apiPgd8QJ8c1QB9nD{53jqP`w}RT%sn|3NjO~DasNd4 zN;-b+yGD!OZya&5zT`V%{Kw`*-P$!PrWzqjN20Z6;oG%})%7gJg{cx0&G_C)Mxg{NP`8OW9 z?wa|{$(Wh=Y5Jxse(}_aM&F9-4{krF^7tbk8VWgB^x#fq^ z%o#YNrWT9ipY=_8w>)=<%c2*~!{^h^iYQOjHf2$q)A0POOCl`Ks z_HDnL`SZ7Z@X=O|(}u5XW!3A~uqwa}riMQef)@DOfluG(@b}mon#qzGru9PYgjG>_~dR0W2f~ z-Yf6Ca$Z0(;00NbcTzfl;8?LBXQ7<53X&ZFA|uHI-V&sTRS<#~KXg+y;F`)CjqcSZ z&I{z7mx5AqB~DF2TZlpiKnM%8au* zdJyJ4fk?Al$0oK&JGy6TN&B3c>eh7|myWn~FFiY|2y9E29($y9*^0URTbOxb|Ix!6 z(^EV3#JBQ&wH2>;`uUPHVN_qYX7}MU-_WN_Xwf=0ve*GqHe9-Mx_C`+Ed+oAA4QYrS_rC1J&TPlob?3Co5VCQOtwPd9ARGp{ER6{=>8(|9 zr{;LRdT(bZgII43o|we8a}L+G-*mX}^w_C+m8YdoujJ=mlt&Cf>r>Sp4Iw0lo!O`Y z#14QKS_+6GW+>!wq%1*EyOYQQY#9+?bLyy;J5B^`Ta3C?N?<69)3hZEu21C}c}dw) zW=kE8u|d16Bgn!4@PVRY;kWA3w@wt2$fp0$`$A`))gN7W=f(F-jZfv-1&819>U(a# ztZ(J2c-Q62()Q?TWvntAe)Hv_xBp_3d}Pk`RWg}fI`k7Yx6Uo7*0Q5lI@LPJYKd=7 zzHZ=tEi2Qt>1Tg_&#`59{rGFQjs5(6Um2PG))gOqytGsO_y^68-}vWG&*~h=He5dI zgJylS6|5Q@SZTQS^!aXa#dGIO^cPQF|KR8aGfVefd~nUN^Cn(+&7M1l-;o%SEVF=J z=wi)f8%NT47kV2|tQ@rUE)uDfPWwD)FL~zK5(JzO#14Wi!3j^G9NIbQ$O{h_crQGJ zC*fcfdGDpO2n4*xX;$Gh_7Z?_1SWu(y#&aE8XtUT-TM?Kt`7CPKm@65ilCCSGA;|# zQ0PQPS-4R2flirZt%N*}gy0~Tl~#%3ycFbl;8iY6qR5Hh^A8mxAYXd!zQ^2kEBD7gK<)Q096dIW+WYjxx092# z%H?B6io|ec<+-E*rHf%0`*_U*!-t*s|HcPrgpHI|}mrF!s zWJNta5^mpmVCJF^jjqK1!wdYEyB-cpg)lHUN5NReNZq6y-=0l zHz{hv`0A5KuqACQj@_z0UkHnHlS_a4n}2`lLm&Cs zm+yRd&#V7&O77~p>15vz@af(4f4uyrC#%r8Zmzttk*me_NRpr1xpZ}^C!RQ8&#vCL z>eQ;z(bwENd*P8YfB%YOs}62IcI1W=-`xDxri108wP2|;;{$0;B$y-0!rXfn&<-4! zL=(BtyUOGHLsC{aC>Rg{>>=3$LI@+BATT+2C?^0G#DW!j=>ZqGz_5e^6Kv=}1{#i~ zw@_9&!Ade5VezA{H)u(y+414!X)&^KA=M%)psg$!Uul*Du9t0EBo61blZGm3T-2Em z#ukH6h(=Kvr?RvJ?`5K0+O6Yu9+EN&sxV!6Zsf(oMGdgZv$T8p#A2MCHP^m!bER7H za&n}tg_Wvy=H%n<)dSDV?_&P(7mtj)O65o5_)pX4Yb(zlW2@MDk!^V9@U}Nrw6BYx zHokLi5SG+M^1*q1gE{h=Hy(HQ&mAne#nu1j_C1qKC|MT4$>}8pC!vZ12X^888`IQ4 zSybZ0{`ATNe{|&YJt&E4DdaeE79J&GEiSL5}uT(9XJ%>pU2``>yg0KfnCI;jQsWnNh121IcB3 zda$(Xshb}?=fx58>Aia<%_GPDiuyaoc7O0M_k3*Z?#;itL zEy2lKH@|Q1EKbXoU)ydXYma8;T)k>VS{Qp~OJVH%$JU-WJ2?5rKWUwHYR%)X7+E&5 zWq0e^>A$`4hN_n^K^(}$2SLU`DB4pS z4$`p@PO^ZMnHQ3UU~-UFX4*pl5C9`^V1S%(0URJaKx`FGvrvGOP6F*6*OjAjRnm>R zJj0W)jZ(xSFXbV8;W{nc;nRzzSt8^ZKO^kDUAVK3mKZTRZVmb3m#9z4po3!Yj?_^=}*> z_Jlw1p738rzdl%ivC0H_`N&SZZ?H93m&K~krpYd}MyMbGi((ngq63#;+fmU}gk3nI zi-x5m!Gd?6tem^)#N7EG{+a$`0Ip{GliEOp|8mdHjsk;h0X~)PxB|ouK#f-;Vi*v> zgi6XF09l{_R)8f$ag1giYsZ>cKg0}Xs{mqv7=Yr@>ZavW)xHkWoVx-!Q4m&Y1u38u z2`bCG*1i2JmX%B@pR0`i?3GKbd}@22d!n$`kRD%JvL`zWT8N46qHv1Y=SmeHe8gVz zz?&X8y6|#)f;UW@(2Gb`S34T-@?ZIfU0a_VFWq*e&eg#DY5mwXgqu;PL1_ea+uQ6JN>En}?>-o@)Z+w1r2Tn81ndZxsgok+I=Yz^A3nd^%c4hBb zJ{=A@=bG`-c`2P^ohqcc?C!}pFxImOy!Yr6h54gR9T|WK*6$f9o_o&Wne#t-q-y~1 zPoA#<#LYqc!QGFIMQ8fZLi<+7;z$UH9e|oy)XWTMSVe7V!U(ytND5$0tkGK3Kt4V< zJk)68j3Em!gBZkM0x>a9&u&^?i;G2svW1d?ELo2rAR!>7g6ML;IPdkhXtsrB?`M_V zr-s@C^STG)t{;Jz8j6zrtIGo3L&;b{SZg|Q&trv6W3T@1+>y^5dPM#8k6t!$VCIUy zc+Y%xmD#y%@eTK$I``JU-#hx*>-TNgj3!IAS;E1@ZE>)}qLL_A+>=B=Gb62Y21mh__1_njkl_J}UvS15k4_C1pW5X-gERC~Ig7nvIIg zV6x4qp!MRBQ^TD}j-mdDQV@eECt@gAAZ^WV7;3enNGt1@LW`1cq`g2Wa~UY#m;Ut9 zH+@7lyg0J4bKm(BU1KZ6 z^638VCA0m{Zg0!llGLWP@BI0>^CE0ffitO_I?R$X0htQ|kbrgG6at2I$|&t!EQ_k~ z@Vu0;3ScKf2AC{P6JF*Da0E{dfON`vFCi=|26=Kq2xUAuI4Qi4V1U6Syzor;1L*+i zfc8f&EO#F)EJ-Fa(bZX|mX2Csm{TSqmO0xFOPP#(>V+dRqR0Sn9^?r7x(p`F{oE(@^(P%{gm%0a9Y zS)>L9)eOoCt=4dhZPu)GS1h@ikyHHxAuNi+A_0g&U}ZtDL~(U?%TNoTGTGeA(6b7i zFN7IIhM*MnG=6o(>;4{EpuO&&7HGa4N!cnX*V5!8X9+ehvSOQuLTA#!!S%`S|L)S< zuKU?&*F&GW^Xjj?=bX{~vriRndiebxd*pu|y7Dz&umAMSHwNK%@wsPcWxdh$P3&m9 zpCqrj;q7nut9?(boVxWpJD0b%&Yg_=Z})E=9St>_*tT_+skS&($6EbI%HzLQd|HJh4 z+lI*qwjMOPyW_R%J8Z4C!p$X1CyVWNDJ*C0z*AQ2YCEr`mN^i(#qJI}liK6^8IFVE zw2W={d1oJ;?Ra0ea-)-LtEphCLM^7&hjG?pZhad4j$Lsu$+@TOAa($1W+7A-fP!L* zV|0*h6^n!him=cXSSUN1xumz#PPI$RJ1jF1SpYi6ALR3k=adpj(52MK049`PiGboz zlXS9x?%JKNzW(pDVp=@slSw9hAjx~*E*KRhnkJd6KTfA%@*vAMe=c~jrw z&-cIY<_q5O`6r$mPG5ajyFd0EH9EhQyzBUQsB>f&ZJvY4=BKNrv+PyyYERTxT9~br zrur6Eju=^ph0h$M&vkV2(9Hw&D03y7MQl6;$O|ruythh`=PVG`sDKSAFPv3Ez^4x{ z7MyhO1UTUd@Ln+Bq?3Xr1Pc$qLV>*qEC4SAlO;enFbN3nL6!i5B>`e5g$E!63kb-( zeai(|Wt^9oF(E3wvc*M96#6zvF!@{uDhm-8QXxoK2WcadM#8F~ZOal+GQ*iD(9|4O=dGcazDBx$nMPBddmL}wR4W4NQC(6@Ds$1(r&?E) zj?lO9-`sldg+obgpb-=}xo_&Cw={ylgp0G&6)l>Uo*Rz#Rx8(J`~Ua@_&@lMj~xsF z?cy+l9KNc&RMJ>;*^KI$;mBkKE4-4*aKD#MI+?MIM7q$^VVebc(*ua}+X6s3`?nIrq#84KesdRSnoX8NN4C7>A5Efk8&Z}l~7YA?!*-`)1 zwHJL^DXZcOKT(yLQi10};&QDVE9NK@>?BFS+UBfeklG&q?I$mN=&kk#_{7!koc*_r z@A;!Ut2h3|#Cz?1`cM2&>!Q1h-@5aMXN0%tH@QmGmbITf_s>h}U;XhR9*Hp`?i_ws&DMT3mL~@4&1rli~p2 zXmsSJ{%YtbYI4Y7+e@9ohwz3&g-kl>b)FZ!A@UByE>dy&SXDbf2$e&^LBn|=ETogd z_|OSMP0qq=3qZohyVEu}n*3!M13c z^}^By2^$_4@!Di;-HW$Jk(gVE!_6nmYcE?cWw_^9wc$n9C{I17UN4WjJMiEA^_t&@ zd9kg7uyJ7FhF7$8tAM&zyI%y_GN`*^?~=1jx_o`2W$p@b;$bqKSsP2fe`H|2O}j`1 zZR#oaim1=g{?J8(b4*H`)<9DCG7UGhI5ZguUQw2W-6qauyVihjMFqwnQsfK>$yC=R zXG_ZJBrakhDEd>6C0A{j#?pj0jlzxB6~qo8dVZ<^YY@N$N;<2GSr&$oX%;gmyOAus zpmq&Gqm;T?U|<+vVj@@q69d$m&&~?1QmzzOD-|+jM*(E>+t#K`0mN|ij`zRplby<{ z#w$KPPc0cK$r?Fjp<(4Mc?J}`ltS8iF~Fki#y3xvZ@G8;zQX_9cw_%RcfaGE-^2F* z@sr#2ce^e-H(yiv^{P+Zx^F1^%Gmo?$y|o5fBM}6{pk%4jlcDbzx~1;N7nk2tcG$~ zE6!c`xAi|cUe`^8*|xJ6VbZzTs@_uBZiL!XD=OJjPh8x2{jXypz=Ajwx#Pq2zbQ1qd%}Cd_`wxtwyd-^A^JuB z<$=;{2q%@s)fYwOdXyu#^E(#{i0X0i(j~}H$Y->)3O*Lnkd#u$SdTtCb_YVZQP>Qv0tptpH*yxEFyNsijvG4 zL9WwInF*&U+KJ$C}Xy+$Zeeu~Z zzu^vS{-@7A&3BBy?t?F$xA4r$i+|N#5&Xrkf3!3%l6KcWeX0~tYvIrPKmW0>nE|tq z76;Ni^pjVAY~p>3v(34F;JnRMXL)w2t}h&39c%HFSrNo+l6HbKk2c`gIY^mn3c&+g zUf5Xi%BPA$Z(+SHJNC+QKtLX)+7fx07uaMG0LM}NAtT^CAi>HBA-p958J6A!9v;Gx zBM2{H;RzsN0yrm~fCrI<5G;rVSO{+=oCF{Mco4vVMEtK;P6hc)C8lU@$ackChzzS- z32T+hi>!>)NEG08ZaI|AVgN5-gLc4yOO%dM0vR7=#n^*P$>rp7rG%s6SS5@_$@}{3 z>c?KD+tan*AexwKs7>R=H?A>~;6fGenvg);+_El~oUq={dY0!2mn=S>FU3GD0C9X3S$$0^xj4QXbU;s&B0#Pn8ecNQkz&=&T)nStdSBjEwhQ@mQHe{(#yi`>dV|rueQph5LyZ&Yeg*; zY_2S1Ec!A0jTk`IPap=broMK_q0m}d1Ra`z&Xgi5?OKx znqyg5lub1Yy`sy4K>(~mu83KsQ^|$GE18tU5u~msNj6F`@Y-neZ2hI=G{yF;XhAo zy(`-Cw||1_EqiYH>!&Z;J38oQbob!ad&YHtv6*$4Mu$LRlD}E}kH7nj>~!@~f1EbY zA3JdK8x~(5pF)N(+j{)VWpXzIbv9Fz#tQ;(*hL+#99gkv%mQkpUDE} zQaYbU99JmQmK{m5B#aW$y2$4NThqaoOrd_i35_6c$w?+BB#Z=oZ%2NAkV25dQq5>Hu>6*CvG^WGJNo9rzlkXo3DH3ut& z%$9cjwtuSLRKv5y*Poet5oqUO5?^hOqX%$B@9R!-IB7ciIk!DoQJMU=6Px<_HfiY^ z-5K>J#ZZEE=D0E2vm~#D(={VBfl>~=a@tvvVWtv+toPW?;;?bS@F$jreO^F&rv&wMp0HoWLACO|W~PVzlI8o8!9x=v#14QGQFd`QUA-!Le3)dhp43WF9tEeUv}C@% z)JX@RHJiyw9?L28tWscBV8vh}Yl#rgB^L@48t%^b74o<%aJ>_~vxT5~NspC4!7rKo z*{8a{P=-eJy>A$gq|1Vi*vAzI?NsV1iF6KSUEFQ5KvJ&Ymd)OJwBxa#|M+-iurV?C zy6<=P?k;V3`PX_IZ+mLzd-uNo-Xkk&_4cY|_9@UxJwss@SKx9Jf9aWB@BU(;t4S9h zpGUfF>iCxJzdKSti3}k&oj(VY%uQ6obI$dG^RRg;P!UB+!bz1El#vF`O6MJt1L2LC zLy$}5b7GXzC{xB3R2rz*%W^`D|7koT7y-hd0+OsYl18KHcjnIR<(zx^*=_Ar-eBkD{a(H=)cH}}%QKh^ zezTEgWu&YPOpHNMZlEYt9Euu>Xal!oa7PkQjWk!ZM z?^Fy(f&5;{rPp3un7%8Y0NNP|G$R&}j>-ruv_vM|RB{!&3*PHNoz!Fntu3L-a1N^o zrA$k0Tp>NFEv1ASqG$%8h7W6|6$J{7kRx=9CG&P1W zO`(}3U>PwWmT7=mrZvwPR{+zvj-~uF`yzhjB7?(br}|OB2XR@-jd3!~w1wd|vm|q= zHgK$hKNMhgYs5`$mnhTX_%QdrZ${0dZmC{WgYwg)TRx&K|a*ok{Io4cR zA|jWz*AH}p-a62UCPcAd9b#CF0OHKK#+C|#^%v6+nLkEJAJ zoNMBzR7CCBVH8&A&=I{ptE%lF9+>=QovrLhC)nw2riM(B;jmidQ5JJ3A!tNZf~j~I zwY$e&x+&Z-5k*hkX;1keCjktdEHgvTOmv!)y`&z5DiP9a`a+qqu*PK6VVjFiD160x zz{CWMp^6)7Kn-9(2W$RKnQ=gabS2VJ!AZtCrPTrrZ?0wFjFFx5_kW=OnHo28_!}>4 zQswa^ql8#aBEbwKr9@l9EokKAG~+7bV{4z3$Mnl@ z`_7wwf1GY}I+_~}axIj!t86(@+q;fE@VQS8(@yTRFrF`Uj%7*3qn#&1q;Be_&AN+Q z`wI()?orA$VF?N_2q_62w0M5YsjXwI-#8Gj?k_(!@_M5T6}IM46ZC%#W1xy-ttGSRKgG&a$>CKaX3k2s1d_FluHMW z6T2KkL-UBa4l4pv#Kp+3{LC%jrRJt60siH7_p?;bd~u-_CU*MY-$Ob;1~R zQmt`xPnYMt@icek+SoN0nQga+2DUHPZ~(fG?exJiEJa7Sd| ziV3Y1qlO?A(o#)`XCo>yk3@N<6Ebu46Rj)3gYWGfy|P^0xv}`$k32Q)wpdB=<(;p6 z=Fp$rvN0^{+0DuAILz713umT#OYsQl<*x;aP%ahzP=hhb8KK=X4g4SXbQczOjgALP zxyovxIzQoG*ikw+IrG%y{->PVpUhsqar#bk`lhKzPw#v)iI}Dw1d+?6fQ5sU%3Q{V zHb;$SmLM}MV+0UMu>Q}QXv#D()B>6@Kmpn@)>10D5t34yF)(BVpo&sxI2uG7N)3S_ zFyK(c5-4t&Ay7sSAdv@cF65f^onHw>!)gcuL(=IWOTyxmOo5x+k zbjA|_9SO(6XKhak5TgAEKDC*#lw>RwRFH(T)~3Toe1|fVoH@j}WISnIn-HA?tDVDN z|5nt{YIl3|B?l5Zb*@%T;8gD)-$~5HRv@H-wVE*2(j|>7^#x7U_np~Y`^3NrYc5Z$ z%LK_tC(l{Z=VRAaU*6Vu?3=Ct1&qsM2N|Pj?CT&e!(4LXTs4hzA}uOF`ZKYSL8jE? zBUhrg%<3LvUf&edrCBEwfWzV?l~019@h3*Nne&B}Oh^;KBYeNwmw8xwmR# zGM`xG#X?f1j7MV-epksx=s(D4_;agJ#TvDMl2S zu~Z3YO(_h~)KX0WqM5ZkgR2aoP#_kLqSi8K3zjhpFecT=7s|>E=exv+-D6o-)x5=p zkz8nC4L6@S=W2EISN8NhPE(x=SwwQ7DW}GXJmp88lM+Ws7YNHT_FQC^Q42&JHB1kd zwLU*pl8Ngiv3LN5S4R!~{YR4x1cgEE*vy_mUbT0NG~$<=M{~W^aPf-a1~A&tT%uXR z01d5C^j=l14xEHqKMit@B|t{J*zxB}^<_1_KHL9}5^iqm;8dbmDI8L}KX5yU7`|Bz7RhVyWkGA~o%*N>f zyiDG4V0qQs+aBgN%m|-7c(qwHM)ou6)2^=c5e>V1Y_6NHmpV@ifSd$q-wRnb4Ei@+ zlO`(WqXN@OZzNK=a7*MWCZp6KSw-Nurn}-|XGKxKfC9#FW2mByM)hdM04SVxpIFH0dpQwTxe=)pyq0&r6R?j4;MCSFO;>=qa1#JL0V;_<=!?SnJ7XQyw> zPM-*#og6(qcKoT+4zh_KAx3dfKqO#Nxk5Wxihvrb63#Lonx&M>ET9osTDkHl17lhf zu0W`!ngNDomU0*kP)d~{1b}fQp@vxlOYpy0stF-lC_`bvpcFS?Oc`bgh@!+Y0O`n= zN-B=7HMn5A#!c4YwYK(D!2kdt07*naREzdb{D8stKD7v@ORxVi3UEl85ycW=g(lF{ z4M`xm%~l%M z$!$+Dq8sgri}h~rcyeDLU7vvDkQAxT1jit8DCw8>*5Wuf6iOteI^{97)aslz?I^$F z?9!;(%ZwZA_SyYKHqF8?A^qp%;ruo$yV-`z9hSC`I0?~%@+8fbX>H*v?>chv&&2R$ zFYm>LY{XuyJ4t3r%tUL__?{DhaKoQ?tV-|Rn$-@^c&?Hz>ra)u6cb`ko^PQ%;^i56cT;`_I?NWyctmX?cGmaIKQFN|l1Gq@t6e!$$ zwR+dW*5S40-u2iMWygo}P2iMGiGGn#l zm{f6>TC2dI00?E7jRlZtn!EbA&|sQ^Dhf~wrZr(12eH-?uAxA{thSaLPy>Jv7{j3{ zKqO~IGZ+dOO_^43EQJL$gLX71(TqjoUvX57D@y@WvwO7Z%CQ&rO~bUkJn-meUSuqq zyz%XGL=mR7r6zJ2CsH{~N6aBA7daVYW+)u#M3gDPtxavfv`^H$qMeZ&2Z*~OaJ$;LT3JvYZ=qZ}84^_E6_(YBocDbD!q^P`AzRd<1j&9O`4)wtxeHasEudaaNY zsG!4WxIJEU(W&q~k#9bK>Xiq_^GPmV&gFVK@CdUD2x1k@7t3K~%w1;u%zVbC#nuw$ z!h)o#t>3)wLTEv0+;i$3FX$Hd3Rtmr&D2)>fJ$LNP6B)lo=B*p#xKy~mhWwpMs+^! z^BOwCfI1oqaGGuH;<cu8v4roM#;zVOF>A=3ERP5*e8>p~?#{jtxl zi5<;C8VX@~Jd$m>#L>pdhSG>48bvy+CjkqcTsQP#325e|PCC~QdOc`OU1&9Thfd$f?Uqa$$=bFUu;*E%h#=ZI~ ziq*fY{4B+VPd^DKEFS&Q{Z=#W3_NE*iE$k|Bw$3K$0%_^LQ@Xa^0TsR7wyoa(lu>9 zTXW-K>#1XG~5_%l^jnW9Ou)3>p|MR3AIBzq7>;jh>Hokl~c?3=L<($($r_ zu5<-2doqqFMygTQ3rbBQq;+e$Q+Bgj>k?S5lrw=FT?s17PV(GKiwhewxkq0%x1N$N z+hIB`eZIbDb1Uc2wl}vtHrg{jR9GsVX?fL0-*)J%n;X#|`Mz`Vn6DA8xKm9~e-m{<2T z$Vq^&^=j172|aj5ipT>zmuVq8J9t-CJC;Bj@a=QWeeRik?I!An@-R@8!x#z!YGAe- z(|aOENGBLP>rH{TxQ&{ zRO;f#KAtP4z(@8x^m68^lsRJ$etu=5GN3@t>*-?J)hw4Kn%Qo^l^bvt_483a$wi)% z4e~lQ90XRmTDq=@hIKI0P#T3nav}~lAFZQ_I-02Oe(B)q;qcmxorM!CTgMygM}}9A zcjs@4FCLFC?AP-*xMz_>3UG(FzqH3Q!KC6wSPQOG#%<fXROo`M%d z(W}k$o5L z`6*{}daUoM&F$5~ZpvDSWt95;Xmm;#kduH|+AQXMYH;|4D5(!jXCnu2u!2m%M=B{b zii+U_*d&xdyy*BXC9p8No^?8^&li&I5()4WG za;voneVQe%8<{Lfq)S2FdbsR4%uSJ)nRcu=v4A%;oT`X(CF6pkIL>eX+Qna(JYs(O zY{LaoH$~y*y_eXIb7KE#Z~t@NvB&Z^K8xdLqZ==%Bj@Cid39vrkvG2eiyKWS9vr(+<+&b29y~_i7}c&WDG<@QcI)|LmPmec`ip3n4y_tETJ&g5Jn^zq0)imL{SP& zVF;)N3@8Dt0W$zGh8xQjp~i5M0n~uQ7)qgG!I+YQX_D^wv~(S|c%D*1c8xg0_TtOS zz^5+G{4xU`eg4UTi1NRDG}v)%ej?YC08Yp zm0r86gJ7e&Z{teu`0z?==fzuc)wA>r2ej~t9kG2HFPca@f= z6Pio-#By^M&#ji6Rq915ou>JT{^C$^CE|AV=6ve{7lX?5z#bY7$Y9Wu-tH|aut8Ch zz@DJVK~4f}yIU-@6h?N4DR*@OZ(5JbuyFJ(}aqWcJdx|_NpKk6w%K@FwGP)ebI z&6VAg%H(|XjX${hU;ge=Uh(dYzZ9K1`+Pg?rG>lRd}XR; zSN>O1UAiTgZS2T;n*FtW{H<9|OKQiz@uh{SxlN%MW3o}HZnfu1yl3N1iEfDi<-VaF zeA%Ra)kecS8ez@&J<2Cbjc9%U4{g8dlZ`I|-}tky!;rUMXaI1LPTZSaIP5&TJ9zN` zKYf^=K2UsWZ{^g)*t0YB=ceoD$7`pS(s#Z2FBTgBFWh`EF`97@rVPs>ms?{TYq-f= z1#oa=(8h~nKj;gC;xjJ_&8*_a2+cIru;9?zfua-!Foam9c}4+h2n;uxD+Uk)SOCOQ zSin+d6kw^L79i9ZfEfzSv=|)zB+-m-T#30+H%x@hl|u&re5NUW-UY5+O&&k5%-lQY zn5H7L)EmNO4wPlqG}=2vzuxtYB_iRO%5hg~xfONPq478sa8%ZMjJ+<8h3ETkB-v8E z)EfW&W^_@`p*wTe{MFJ-P0UwfZ*(-N`6>%t<@jw9c<61r`6Q(d2R8OZ?U{T)Lfdc4 z@k*~9c5`kz)vI+^JR+M%k7&M$xwN{`IuIq~o4eg${}ZJ#<*z@p>$Y(Ff;XLywMZ|| z*l259`Qr}b&eNkKG|ZYWTx}QL+F9SvR)>Y<@iDP$Q#!s4b1s&jz1rA6K2aA}mgc;u zGnC+FOYt~AKH{`g-r)|hI&#{QiUK-dX1}4oaz3eC(@V z+sN--6IQcAcTh$HmHhZdY*K%qgjX>2FOTdqw1(Le2d|IgfQ!ykF? zn}NT2>Z8DOAOD;sy?3-WQa1$--Mrd5eywxkQup}!=ItwMcdcJLkv@Mbo;%7<9jrYu zGx7Z^)%JVd^#=_;tYt(~D936Jnla5u#w@NghHEfmxz!8?lu1H0h$Ym35~ZogEQ1DP zgPT6AY*BaTyF|IWCVTS-b^y41pDexP1b}nNSH8_fXX0n?iaZvB^Fh(@OqF!X2u-s} z!n>WBoY0`u8ot&%2IWM*|KyHcM^5;@)ArnNR>YC~!)f}$;D+)6|L;G2c>2Mqa=V|a z^nz85z?5$L?>{_D=THK>4>-1Tl#5G@qX<=*kkVZSGX!+7>y-=YAp~;q*86m^` z)`d3>zBc-{4%_TZBX_1hnv+jg>)U}`c>9%o(IY!Xrpn=W)@~ltS+cq(FiFer#T96; zacFm_6uJMA#^IlRG+)%(8yT!VxbKHA$hlm@6xHd4#?XHa3oXCeb26vauUZWjVRd;{iB?mq4b4z~hF( z7%CN|p=LVLOe?PNuQE8uoGg!xLlY?+raclnt~Hbpp)4SLODPd{V*CrkU-^fHz#uvC z>kr=ji3d^y@cOw>FqvxyvN6$jwJRcB{;4k=|E-v_SotG=bzdZkL1obqF>BZ826mdR zgW5J9a3`&|oeVKQLN1B=O2_b5+&Q}UJ+FBO@SdMP0lfF8j{sjk^$j{f9goiJBNwNG1zy>pqRVsyt&qvp54`D57B0F#>9sR57KA#= z8VX{Gm4X_Q1=hr#F_bGXNi2&kbqwJ;|H89=#)#I8X4DceYd{QvAr_Qa4FY0`0kI&k zglcNIww7575Mro>g(ZX%4Zt!3rWLoKnp(>Zfo2SYgCA!Qq<^;W4hy>{J1ckNy1&`T zPhI^vTzBCUPct?MZhLnHzK&c**uVoHe5)&y3DLin_?^ylIiSiq9Jy2L_te~U@xpdm z*hBX3%wKD~{JdGO?|F2At8P$0?ZJ_{n_J!}$TBr+6C$piSefvac24x8{?_f~d~%NO z$Xx`}YX=;rBFn{kZnVYs3~ZW92JM;2_VZ?6xU+w@asv@_3m0D7{$%xD=3M9<#_7pw zdj1M&MKV{&m0P(Rj+|3F(Cs#dqjkDm*tKEmRi00^UG#P*g#n*_{tNY%dimI(?si)# zxiaq^9H}f{86cO0nu*-Da&ut>7dfBT#9F@U4I86mqelzPz8h2n6~)fDue_|G1TB>; z$@luP=Npid0N)sPbFSfe$ec-O%ft#lpuu#t1W*GG!%Y;kWDvv2e8mAMfGcn)z{SRm zqnc?jhbS;cq{FDSlsbent8;p9^^^FO&ow<^TCe-5&e(514h8N@FNAI#Zwh)^IT9R0mx_^P2%nWhFx*pmJM*6jRs60q_602}{1*{TV|a{LEhgy?3m)Q=mE*x64{q+g&Gf5T-6tpjk%~ z|3Clv^?!2dxjLHD@0^s}fby78seKs8z!(@egGaRm*WZ=e-EPc!yfjGrTtQ|<`U9c z@X4Lcwe}QvW^>W7JJ=(qAx$se9gIz9D?81(_SnR9^z6|cWb6Ee*NuH5J~Vu>Q<+{D zoi=cLr^{6p`x+a2_F+Et+=J}$`JxkzjdVP*tPgCH>b2rs-51hPcl$iL+dGQT`5i*1 z%grR{HW?StOnZ(n!ZUEj_^`-?Q|=FDR2DCl{LNh0%dc(}^1u+m?()!Sw`pQL&S9Wv z$=4t!0dZ*&a55i?1Bg-fWy%3CEh0WD3@B(Kvz~TS1#m!tD}XBw1r9*v(#9!@v9Pjeq@6s-cQZsuVe>Qg34vP{^tD(Az%v zqwnpI%$3F3r@z&`@^Gp5CrLe7${$&b#a{O49rVE8*6wrN1VyZ@dm^F zq5zo|9Ax?7FWvn43m4r|uk?oL#2PT35gzM&!U1A61Czo~XsW1XqQ_}U0c)tsPo43> zl%W>a5g1|!00K~Jh-QEVtffSAstl;%(10b{VG=@tKvSc@!HlKWf&d`Kfi_^8Fil`s z|M*88ijZAfrLntv?{e=pO8^Y|UJXBR=7Z;jj>`L9`i7WrS5d8u8hQG^=_CFAz9ee; zBaKQY^u`P6XGU+nR_Smf`o7oixAP6USM9mNXs_tNC5-wb1AkMIr7}>{lOKpmP^liBZHLft*~hU!L8J$qrW=LAj9j%87?U#k{} zyx*9y+j;t|GfLJ@@mrGDZzFLpK0~WRQ#j z$Pc456mT*)05}xD0Voc|6}aM1TowD_t$`+>nGlLIOAVmq5Gv`l20*j!zWe;s0E86s z_wKs1>n|RYAx6@-OP<7T6jde*(@i` z!*AUEKl{IK$)}IpVadntx(WDT;b(x0U;OU?e&OFAgZ;@rssSH(p9B1L@*YjT_2gSt z&OMLw=juQK5@~baXd+zd8N~%R%*se1(;AWMZ-=XY@)K2D_qt~az|^Y>HqH@cl}94V zY{nQ%8Ota)7L?GKQ6)VZQEG*)`u5Kww z3;AOsGJD|3wD8tfwK{^F--t@b{UBPX!JwGepmZ}o9dC#3Wiw&ri}FxjCvdkKg`mI! z$0Wr<=DPDsF33rMuXR8j18Oo27N=E~z|+RbG{CfG(a-^(=%w0KL%=~!4ITW8%*o&& zbBg`&R#!O=B2OqnW78W9(hzC0-mK#X+*gOEGh8RrKi~g{H?KXO^f8sSp(gq%!}umg z=xUdxx^QHv_T#gKyp3BmLxL}Sy{rI3xs>2CrG7`Bdf?ozS@NY_cNqGayKew~clS?N z^6kg}D}XnDYzFw{4^!Yj|GU!C_PMP1|4z)8hfa{K7FT}4d|;93aBjyd?@2t85y!wW_~QEW z(&L~c zGm;jmvCfva$8?vy%VyT?r;ecD8BK+^E-dq1-g1SgTwHQNP6B+X;cHz0gi_i_2+9)V z6*3Jir~zcE!AefVXm0}(Rk{}LEXi;H&A=6|<6U`cQDrRY(lkWa2G9iR`2WltJfA9L zkofG({f8fZB8d>Re1Ns9&=UY60NU{L6iH=77Y}I;V{%tD(S{Y z@yKsm@@F^SV(91Y+y}h(j(1sd@y|aDZ2#sv_B7U?n`-bzE6bP;vdqyA1EIWiArx}- zHkBztL)Cm7KY#8@RimI&Xoer)!to_li5I5=}KgEi@y> zaHCvnpaQM&1BL>W+Kj`15E!Px(0|1rgdWpFb8*{Og&SLX|5tWcv)rw+q_A@LV{XpOj~V5Dzs6f-+X%6y!6^X9DP~uYmM7aoLitXJA>X<;^mikeeI@AHn+B5 zyra$&gZ=Ltd~ti<#V500zxTgIYJB&0yR)*97Rn(H?zp2uSFf=Xc=GRidm}lz?s}!2 zyK&la-h7a%E)j_RB?&ARBY3RiJhp02E)k2YDACwUK`mKx~H>0~)Y z$~I#8^0g%w^nfF8MJ1AMuc)z`iqFq@_1}0>Mqr%vQOxlTHPPlOOb@`^GN-pF42``0}y+z_0!Ay;lG2g*Tm=U!yu!Y9x^` z6{`JAxzaMtX@+)IgC~A(;+Z{buVueM zXf*Q*6#xJr07*naR4D447%rLS6owK_wWfu{u_on&r;+DmgjmgZ3Ly(5(a_&(I+|Du zO9-yBL{np#p_)*DD8e+c6d=|x#h?H%Yl+dWwxHGm28?RPBw#3&oB&K|!l4*53fx-O zf9G!w70Wa(3{6r!)@j}Ty?gsDKEHo>xjMNX@4oz@?LdYzyMFkdR6Eui!b?c0^~L9L z@8!Qe_R8kB)E)ehc=UL_$X_^f+f)ldb0S(iV4kNlJJVi&IeW`uGJEE9_}`~KX6WIZ zTxv91gTiQMG&y_N#44-pj{k)?wSvB65vZM0Z7kiMj86dGXPC= z5!z4~KszjhX0a$M)$dA?Z%;#O_ysYHBpA~`3*|5rQuwi4MZJkSfF|m=u2~0Mq^JU! z!(-D$96{i4I zMi_(cf86=hE4GT?*jbaE=f@{n;ngBfHrFQQ=AJ`O z#N$Wf^R3c}^|Vqxb1wUB^5r6%>PxY@IUE!wws#HZUPpC~szH>yf2p{HP~6#HEX27< zzkK<4xhS^N@H-oOe(3V%+s3}KwbI*JQ@vzk|JxkWD^<5EC8mc%CKO1(6R$Ja4L`js z#!t0J%QAT4F=PX{uYRc$Z(il&W9J?T(}~&jG?u~-Dk!Sg&h->)ogXq5>Rg!0+MfE0 zu}V9Qjfr=UUt8;B7z)UvM{Q+0BaJb;E&M_3^va{A=T_+XIZBWiN~2EEn65w7jBQfk z24wp36(8gzz^`o!08B#yg1LbT;m{$1lrsDPg+b<*y;;Zq;R*l_I8<2r!uYw-k6(<_cyA+R!yQ{ptI zO>%Kgrj!o?CK}&$g{&4R%UsH7Gu?RmR(&F4RHckCC3GTOn{iF9{o&YlX%jnsC-YP4 zIh-3qEeCi)xTXYD3O7_)kZNC2$7qM9U>Rk^2`$qPgxpv#W0?kprkcSLMF1)pOC72x zA&SCqVvPX-fwfE%3ua&erVS$$)>>jMl&c9Wg>j{(+!(5uBU%5)e_0x4$@W?UDmzkn z<}D27gGcjgV-tHgkbLm(80+&Jj=pL)3}ZAi1eO-R^QSi)dh(Iu_g;JWE&hXR!ArdV zb;s@BTG)TfQ?1)CyzI&o_jVsmcf2Wlpz)d?{ZcYpJ#_)U+JB%d_MUs zjnbROG?q#c zkL>PD=CcUHoQmKGM-Li3H>hnirlxyECu8c$18V~p>5S3xsVtn!^AX)bSXuf%u0I$K(m>sr!UQ4M&?73NYcIBcEauVRzHo0IL(9lq6Rx2SMq^@R#0j>+Q zhJX}cPSf=Va9dNZf;QA>FyBxPZ@CK}-xhseJ#XXZ)MXTj2X!F3#pQd~z3pg^hd9Y$!Z zJO##h5IU`jprp_UJtwj`#UsyCaUhrt#bCSUZeQVGT5%C^l#8v4?mJuMl3_-t)FDbh zaYg~H;`gzCEYMvCewT}mPaOwD0o*bt<}BgXp_$@FK}bX38c5D191UXq7&Lq+6vj|Z z0Yd?3OAV-@#=?RqLIDs)jA4KVtN|lNaRv>5AafxJqtW5Pj|ol9?zXw z*`}cQA%8&f`FJtBwqb0sd(SfuPXM1+s0`IRcLJ;b`I`~4>fHNh`;n*I5z3eX70uJv zzx(R_Z$EMFgYH|M`a}8W-f{HvR}LRL_1ZW-xSCJAes#F*LHq5Z>RsceiIZ2cH>@Ar zRb2Y+{br?|j#aZe0^YaZl!U>$Ztv{(}#7^~A#tyH8Q$0BtKK;Cgy5pCwGrgi0 zB*XgJ)JV@aqk5w?a=LmrKb@i|>o8%mTHIdQtb1KNu{uEY?bGvJ<&o6hR1?OTQRX}; zqr%6WhioH@vwEF~OVf$qut8BFma{2EYZwSjab?A+%L&K%HIA9eA7*EnJ0GU)|!Pw;vZfJE{*b)H}!XNS8eR8C-qxOS4&qS)bGWvoCEIc=Qk7*{ohT|LgyOeQbH~Tfg=2^(tYnZcY`v{L5?7 zQ-FGadQLSsPzl5+UT2K~e0&OZC4)`V->c1zuEwklZ>aISEkG3+FhjU|}c&v5v(A z25~Ngb(A_=*3{Qi%HRU1C)_!PUOdU;N-bpj5tMYCf4!W&Hu4wQiTZsWP>iPc;(Ghihu;rLldz3kPJkQQvIV^CA88 zY3%w9(nb1%A_2Qt@&?GE&b84{S*KFePbu?`Q&{QBbVoY;lGLZX!H0lA2fu8ALucl z|G7Se2iKGWo&u0yA%Sz_N>eyEAYzWmoWbu@g`XS@O6g@viY^pft}?uBfBOdxTPz%` zL3&{Je6)Jc?l@*8A&@R`4aRhso^hu7E}mBc37=byHoGI7GFVP1ao#1~5fNL{2ns=_ zR&XAP65QG-Xs5L-1)fN`r-l*)794XRPQWosJzrXZ4l**z~i>ZrMVY5t)-&#&(KxmH@ePS&pN8qJn5(Ld5(Y_+!2kgnQplFZbq`C4m5 zk?E1eMtiKjmN+dY&fNa}&+6tc%|V?#Exb(lW`%oa0s}lH!a0XXIrHY(a&~dGCB`>8 z4Zk_J_?$(Yj5HFvw<||Wq9&t(c}m=EQ)p;(aDo@a5m%!|W_L*$=M>WcRtPpPzkr|_U!Qp!Dz})iM;vi%-)3Sg~7q&IB;TTmq1Vy~ZnbsX%C+3NaQux3;D5wLpVh#}$ zA`8v{FgU3Jsi}jo4$_gp0L>ggEoC7PfG7?SY8bXK2Lgm(IRFNi{lC z)%)6qNANc;{lQZemzi5;5B{t%f(b%&>66OxpIiU*n@oM~O^D68+VucR{%e!EiJapr~F^z~l4YxL4XcYnLJ zlk#)b9e%80`rL?S7WuIDjoo)tVPF65>dt-T`C`m2-qajBJfPuxew6pCV!()uQ#CyX zy>gN^k+kZ~-h4%@M)J0JV`Iv%VwBrvw>tc!%07C3>dwDHab&XIY1;DxVfqwYc)%sQ zHaAMKYjlL9y=qKKA@a`tXeq^%CCS1>jdf;tq(`6-pY84`bR=vjSgV)avVm1P;}{Ex z)=@5rW6`jRTqTUgBj381fE)+p`Q-@U5@1gEG^AD?w9vuaj6-8l%cKpUgMN_#boIx-j_b2k$Ab zfAg+LT|5-G4_v6!0%%iFM^ZXN5|*UjsE!XR8;=hqJS_K~drv6^N1RYsLpQHPjyeWY z5Q?-+r?!=+vxr;A9ALRM!f2^g2ao*ntKZ??Qcu07);nLXT$OD6Ks_SX1tz>_%m-E+ z+M9A4rpDI_(wTeF#V6c07e}G0kl105RM!nP+QMqbx(grJ@$$ur^`2T#4^!^H%b3RC zs~Ut*>OJ+0c&c62#SM`HB5aXb3WLOgTMmXFic|zZfiuT6XYk%a1b9kGpq>yoC!oLq zFy#OGL*0nEA~B&ryG z{r2R1^WI{4zkK$}q1)#-+avG1{M4PdKDRXSuJsqPy&w8wIl|`7F0dmR!v2eA4-VSx zbl2#@o8EWHAM5m=Kk!z|nOY51W7u?V%dd~Wm6S0#b8<9wt}}8~{`9lk9$XImd+seE zw!=y8^LRqd*CT)CQfd9s5w?49`nqaDAKrGVog~AgmegpP&3yG*_Ru36Vfl5ByPLIP zrJ3eZZx&Z-;~Rc{)7x3MV<3BNCEM}r3BC#De$J9!>YxTvQH<|9~iM#pB;n4c|xm){-i)npU?M+$Zs*}^5>v!69bG>fg5nsZJm=r~&)~?nDf>3QIqo=o%YSj#3(rb#4GLH|z`|f2ty-tYQR=Pro*G7FygX=3-BE*Fh^hc3&PL~I zZ?d&F54qbN;2EYUT;k7 zFJH)7hkIvcZhmz&zIo*P+3X?na(w&c*Z1BfpI?iPt_%*n>5H?~KnE|E4<{GWVl8hU zh}MqXdH(f>?;M^t#~^Heu_37oR8Iy6LA2*`Ig{kY-X(P~$Nk&6TwHkbDxEb;vOyEy ztF-9w{7XI8Z9OpQ!~7MkI|pyyzp}Il4E0-%Y??5zPoysTm8v>*ddKL@2)A|Vg?EZJ z46P2M(yrCMZIfL$6Hj2vQD=*)TcaeQR2pW7wUA_eJ}nG+t@8bscqEaspPC3!>bHB| zYfrgIGZrTMhI=3JfopE>TumhPr&lA8;{dV;>0Ua zJ1WK{Gx@Y@Dv_^=K$qb+Xw!+`AnBX z83zZ;tpjtGf9&AXFGld*XgI*aS(p^4CkIDQ4LHEy01JZybje}cu`0xKgNBFH-tx0Y ze#YVh%5vJkmhjirR3vqiae!*he4t!2PdszXBM|06 z9Mu1~Z^&RAh?60&DqGpSvOck+F74*ZcfNp%QqjIqzT=}Y1EG!qpblJJ7>L`y^32+( zj4!A7rCWZp+hT1`NBh^^Cr17Yi)M${aY&uVo$vh<#K-mH5$xsFiQCJIv)S69yQ|u{ z{{jBO#?8|!{IG8NtnXbVlQliUsrJEJ$_B%nt;}D}j`(8F%`ED!-q8h%SL6gp$eN8( z;DvMZ_4^vZvg;R%-nP3;#jj*;DdqRR&G#!;$DX=c!DFMvH{IK488yw63ocexI;s$} zf?l)zIli)ef*P%*57Na$zR~ntA%YktjAglq6W>|Qp0uvQwCW#y5S5HSKXhjM@q#}vh7;lWU&*}q| z+8oR=Aw7q;p7YE@zzJExT~VEb08A2PMohSTI5Lu_txB4W)oj_Fl<)tAbi>q?No*kk z9EPI}!;7RR0x`Ky%e7%w^nbt)9!(M%Eq(!@7VsSK9+ZM&bz|le;g2o1RCze(01G&{ zU%mOy&R0t7jnkG}2MePy#$`V7$o%tQjxpz$vv9lZ6;#KLP6WAjDF-Y7>0AFm;m$VY z`)Ui9?rh-Kug_iDHq}8x3QIVRFGTg49V>$6!Eqj))f08^M+VeeL$r1IV3&<6n_n#$ zca-@M5V!*oEMu%!2m`?Z1~@RT7#u_7ARV0Lc!R|jhYAJ}BFBRRSnvdJl)*8_y`>C+ zIQ!{Cn@P?zY-B{4P+hq?^xj35fOzJSz^MA^Pp5Z| zeTi@Tc=M?*#^hu3Ux4NO(Yt>;74DJi&r%r=$Bw+7Ri;1DeK}8u*Vn3D)CTR%?ty6S zfj3u9E54qMKVSqi#ZXnOj>d_s`k>-ik4)y9>bCZ{a-~dCj1J>w znRq*YW?RjGR@Lj*M-IuJs&6j#Gss)+8|lQWqnA&Z^s#-XpStNDFLbQNE6Rw(37dwd zbhf;1Dm%)oHzNbTUaz?jw502daxokUY^Yi~Y}Q@FFQm_3;*i=p+qkze=ZhKeFy9hB zp?%SribX+0nGi;7M9E<8YZnuc;{dQmca&&}nsAGpii{N2S}h zbgq8D+L#aHS_$imI``UBZK@vDC7P<$`PL*;Mvp~G@s`o~NK*8+)3h9Inn60lANkod zlvP^P0x4Xj+aJbhuRkrGyOuhu%TmKSi)eWCy>Tp)vtQw$mcoL9f?H2PR|mU)ZTZV- zX}og^jyVf%kA(QZoW%}YY0pgGKJCqfQL*-( z(0Fb0gB>;{##nZsbKz~kQ(sC}_s@h_23XGm=9_*x4a3-$MF2JVNG++ZT-I>Pb1T=_ zt$Qq@{!;~|;II_TFk-1-8537jq!1}=iJ%ZcMW{v!TR0fNVF5TG0;`rez(E2S9E9_f zS{@uIbzqiLr=m~o=~qe}DMTbFlQ43vF-tCAy>j_w7x`RN>!RxSd|VJOsAir8Be-@9 z2sX&izy9;Bzc~BxU0wX~>Mb8Be|PO*Se^W*7a!N7A4~r@A3OX?`u6CV`s_!h0Yff* zvgA%}tZ#O2N*3PoAU~@fIQz|=A0k21Zpu6>$C}sv2Az3nbm>%ldf z!3Hi2HLf-8CJ=?e+SaeyyuFo2=3&Kd!5ph$tv!7k9jmR1K_S`Syx zX0}`1jh#t*qOmd{uCr&yiX#1?2e+}NUM0oq{M=Zih8UNHibhPSn$@(o6KOd*o(=Qu z;ypi=+Z-FYF0+lyTwgB-MbSHc^Y3*d>ww4t4$So*dPp)d^4b&JbFX<^!gIXAUh5w? za=l$R4OoRO23kTZ+`s+#0cQXlEVdW`HYHGcKAil@U7 z*!mi7-K}kH^GVBnkj{ZI=LrZCCBzj4L{PXPQg}lIg&&I)Ziv*_C@Og_JwOTI1A}*r zIYt5r&M`s*oUMOqoA$7#Z?rz9oBbL)f+!g6A{ zb0J#3dYfE)?cIfHe)r_hSMdDFe|Yt;e)3lOt>+(`^Dlk&-EaTY^6`m3?L4+qGZUQr z&NrA>y|ui3AX&ZV&C&4GUD1j0!@`9JgmX}1xzbV7xeg={Nl|Jds>Fp{>xEM$$}9KmRxC2v+SB#*`bQhz8NP)vVlhV{ zOV2aPndj7(9al~?hsC%ATa`@}K)s6Axv**^HoTM9BI2-n^W4ePV9YtIB0h1;UY?I6 z+IFJ}*s_wQ^*XWM(;zrOkIaC6|7rqq9N-HZ49tm4S#S$(xiwm$(*o-A09xo^%iK?r z-l?`swvX;8^KrVqZYjS-TpnsZ)J902MqX93)%n(Tsg)gNTJZ^8>S{Gj=jbpSpBUuD z4)(S^pIx`5HcrDT0|6tl$xm#4iCP2a8~_VIz2#l8rInYTp;lT-DJ=B>D5zd>eY{zo zwj7pQhb0Vdxy_#U=63kO)hq%&{c?_BDJd(S*yVE{Cs`2PNdbaMY>&22?S-Ddy* z2Q5iNK~!{a#j6!sCfiJLe&*=Dy=yPV??Y$Mx83j0wl3}2?yt3Fp@eDocbJ)@#rm_$ z*6gcU`Fb1m@$s=9_2`dsZadY(vj(;HQ;t!9T7VIc5UR*xvjhc13V&M!g}-OAj!K?$ z=2!qBme3GLAcPu9JqeamPvG$QejO;;3@$F15!NOPJI0hY#4Nu0s`3Gh?|gJ`DU9UI zhY*-If`vd_kcH)>AktXsNh%vzlDSI1b4y9fjqOUkIeQS0?Hvwk+>QDk|M}87SeZCaAp>D;|}DPhG)B4*!Kba_!)r`CWcc z;Z{eXU!{0$P$3o2T8~c@N;Knck2Rm{)p&N`Ktn5L^Q8+}ul88uwUq~0;ys^*4IHlI zF%09OWJ{|DCwp#8&~jZ)c*T&3XmdCYRSU>`UWO^2Po7*qw*)Yj!?DW2o%?b##Z9r$ z91+0O(RiFKaw3JeS0<}Ds@U&dD2Fd++#lZp& z7F&$SQ0?x0&%$XqSir#oMq})-5$yu{*r6|W8FP*~*l17ZYC5(bRoXta_s}z1b@m+1 z?Y!S}_@429-`+ng#jR(%-N$VE<|&*zv5LLmYZu0P6MJt;UhL}G$?KF~JcBv4cXA*- z%_7fbcjE9^u+8gVQ=AbG8YnEGUPo~z-BtWOofbMR{4JdpIygA$s#(qhKoJ531waf3 zJUj;+0R zttiGIxT>VOBd8`67ye@P-hcd^6BF#+^_7~`%vRt;+R$Q}u23>*{(Ea33q?Dw>GFxtW~2MgZ+$eScr$%*GA zD8OO4kCf){TrYwntG=y$1<=^y01G%Uo*#JAt83Il1CBWhhlojvDDGE3aPcL;dFr9# zxsByw*Hn?n>t}A-^9q9=8FzNcM)kXn{ne#GNL#=7^+omGa=3Sb&A;51(-56S#FQT1 zesQR0<<&S|UPU$AUC;N#=`}eUxl4@$4a+Ot?*szIoTC6dbDj$&u$oCN0WAS&KmyQJ zqa|+863~#?qMI%7LJLa?2oMNJ2-H(o**2X4VovRaFe*5XQ|(u|Urg~fw2DJ?@7 zY>YMrgJ?7+jxw8sz(BnS3>k@x)XWDBv6o` zK`XuAUry8E9(w)myCg2!-D;XUnUTa9JG6CKWyHCn;W+IUoQi_;KVKUA$na}==Y6G) zob*<5$NLpZsME#byroq7yuykEPWVhyp5K0F#zS|qO~1Zwr{5p{p#8u^#rOTb0wMC$ z5Kzk;RMa*}(5x1O%~&K0F1&Tj4XYa?)kp=XEa@Tcw9jb(CDV{ck8DU)?h}snc|$qo zh6s`6l4!xBY*?2xI=49BAjbhvfC8R^1dsrLAm9dy8z?*lU`s-%K=1$ph$jRATL=Qd zk5NK`10}(O0v@0~FgOr|KnQ}30C54}2;Q2Qfgt1&BMQ`dFwWor z!bkwDtGeXwBMSr8sv00N1y#Z&y42Zd(@2n2seAowBb@k4(H|n1ArSlsbIb>T1du?eXM}@d3klGM2SN-0f`Bc| q0|)>?06_r22?YrLzyI(5VgCoyHv1}bKYqag0000 Tuple[np.ndarray, np.ndarray]:\n", + " \"\"\"\n", + " Randomly initalize data and centroids of the clusters.\n", + " \n", + " n_elements: int\n", + " Number of elements/observations that need to be clusters\n", + " n_dims: int\n", + " Dimension of the elements/observations\n", + " n_centroids: int\n", + " Number of clusters\n", + "\n", + " Returns:\n", + " A Tuple with observations and centroids\n", + " \"\"\"\n", + " data = rng.random((n_elements, n_dims))\n", + " centroids = rng.random((n_centroids, n_dims))\n", + " return data, centroids" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "58705e8e-dc70-4039-a820-b8e596b8b05b", + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_distances(data: np.ndarray, centroids: np.ndarray, data_magnitude_squared: np.ndarray) -> np.ndarray:\n", + " \"\"\"\n", + " Return pairwise distance between the data and centroids.\n", + "\n", + " data: np.ndarray\n", + " Observations that need to be clustered\n", + " centroids: np.ndarray\n", + " The center of the clusters\n", + " data_magnitude_squared: np.ndarray\n", + " Square of magnitude of observations (|y|^2)\n", + "\n", + " Returns: np.ndarray\n", + " \n", + " \"\"\"\n", + " centroid_dots = np.square(np.linalg.norm(centroids, ord=2, axis=1))\n", + " pairwise_distances = ( \n", + " data_magnitude_squared[:, np.newaxis] + centroid_dots[np.newaxis, :]\n", + " )\n", + " # ||x-y||^2 = ||x||^2 + ||y||^2 - 2 x . y\n", + " # pairwise_distances has ||x||^2 + ||y||^2, so beta = 1\n", + " # The gemm calculates x.y for all x and y, so alpha = -2.0\n", + " pairwise_distances -= 2.0 * np.dot(data, centroids.T)\n", + " return pairwise_distances" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "670efdea-fffa-4835-83bf-04c8dbc544ee", + "metadata": {}, + "outputs": [], + "source": [ + "def relabel(pairwise_distances: np.ndarray) -> np.ndarray:\n", + " return np.argmin(pairwise_distances, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cc81dad6-7661-449e-adaf-58c35698dfc3", + "metadata": {}, + "outputs": [], + "source": [ + "def find_centroids(\n", + " centroids: np.ndarray, \n", + " data: np.ndarray, \n", + " labels: np.ndarray, \n", + " pairwise_distances: np.ndarray,\n", + " zero_point: np.ndarray,\n", + " n_centroids: int\n", + ") -> np.ndarray:\n", + " \"\"\"Find centroids following the algorithm in the reference mentioned earlier\n", + " centroids: np.ndarray\n", + " The center of the clusters\n", + " data: np.ndarray\n", + " Observations that need to be clustered\n", + " labels: np.ndarray\n", + " The clusters the data belong to\n", + " pairwise_distances: np.ndarray\n", + " Pairwise distance between each data point and centroid\n", + " zero_point: np.ndarray\n", + " \n", + " n_centroids: np.ndarray\n", + " Number of clusters\n", + " \"\"\"\n", + " # Get the number of points associated with each centroid\n", + " counts = np.bincount(labels, minlength=n_centroids)\n", + " # Build label masks for each centroid and sum across all the\n", + " # points assocated with each new centroid\n", + " distance_sum = 0.0 \n", + " for idx in range(n_centroids):\n", + " # Boolean mask indicating where the points are for this center\n", + " centroid_mask = labels == idx \n", + " centroids[idx, :] = np.sum(\n", + " np.where(centroid_mask[..., np.newaxis], data, zero_point), axis=0\n", + " ) \n", + " distance_sum += np.sum(\n", + " np.where(centroid_mask, pairwise_distances[:, idx], 0.0)\n", + " ) \n", + " # To avoid introducing divide by zero errors\n", + " # If a centroid has no weight, we'll do no normalization\n", + " # This will keep its coordinates defined.\n", + " counts = np.maximum(counts, np.ones((1,), dtype=np.uint64))\n", + " centroids /= counts[:, np.newaxis]\n", + " return distance_sum" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4fde3328-4b8a-454d-8a43-82fdfb51d1e4", + "metadata": {}, + "outputs": [], + "source": [ + "def run_kmeans(\n", + " n_centroids: int,\n", + " n_dims: int, \n", + " n_iters: int, \n", + " n_elements: int, \n", + " n_iter_check: int\n", + ") -> Tuple[np.ndarray, np.ndarray, np.ndarray]:\n", + " \"\"\" \n", + " Generate observations and cluster them into requested number of clusters.\n", + " n_centroids: int\n", + " Number of clusters\n", + " n_dims: int\n", + " Dimension of the elements/observations\n", + " n_iters: int\n", + " Maximum number of iterations \n", + " n_elements: int\n", + " Number of elements/observations that need to be clusters\n", + " n_iter_check: int\n", + " Determines how often we check for convergence.\n", + " \"\"\"\n", + " print(\"Running kmeans...\")\n", + " print(\"Number of data points: \" + str(n_elements))\n", + " print(\"Number of dimensions: \" + str(n_dims))\n", + " print(\"Number of centroids: \" + str(n_centroids))\n", + " print(\"Max iterations: \" + str(n_iters))\n", + "\n", + " data, centroids = initialize(n_elements, n_dims, n_centroids)\n", + "\n", + " data_magnitude_squared = np.square(np.linalg.norm(data, ord=2, axis=1))\n", + " zero_point = np.zeros((1, data.shape[1]), dtype=data.dtype)\n", + "\n", + " labels = None\n", + " iteration = 0 \n", + " prior_distance_sum = None\n", + " # We run for max iterations or until we converge\n", + " # We only test convergence every n_iter_check iterations\n", + " while iteration < n_iters:\n", + " pairwise_distances = calculate_distances(data, centroids, data_magnitude_squared)\n", + "\n", + " new_labels = relabel(pairwise_distances)\n", + "\n", + " distance_sum = find_centroids(\n", + " centroids,\n", + " data,\n", + " new_labels,\n", + " pairwise_distances,\n", + " zero_point,\n", + " n_centroids,\n", + " ) \n", + "\n", + " if iteration > 0 and iteration % n_iter_check == 0:\n", + " changes = np.not_equal(labels, new_labels)\n", + " total_changes = np.sum(changes)\n", + " delta = distance_sum / prior_distance_sum\n", + " if delta > 1 - 0.000001:\n", + " break\n", + " \n", + " prior_distance_sum = distance_sum\n", + " labels = new_labels\n", + " iteration += 1\n", + "\n", + " return data, labels, centroids" + ] + }, + { + "cell_type": "markdown", + "id": "70927fda-821b-4858-862d-cae5e6e6eedc", + "metadata": {}, + "source": [ + "### Lets run the kmeans algorithm using a set of inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "18ac6aab-48f3-4cce-8587-b0ec04600cba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running kmeans...\n", + "Number of data points: 256\n", + "Number of dimensions: 2\n", + "Number of centroids: 5\n", + "Max iterations: 100\n" + ] + } + ], + "source": [ + "n_centroids: int = 5\n", + "n_dims: int = 2\n", + "n_elements: int = 256\n", + "n_iter_check: int = 10\n", + "n_iters: int = 100\n", + "\n", + "data, labels, centroids = run_kmeans(n_centroids, n_dims, n_iters, n_elements, n_iter_check)" + ] + }, + { + "cell_type": "markdown", + "id": "0f57c8bf-831e-427a-8bd4-143b71838e4a", + "metadata": {}, + "source": [ + "Generate a color map to differentiate the clusters" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "01adc703-9a64-4985-9c86-3c5082f0d891", + "metadata": {}, + "outputs": [], + "source": [ + "label_color_map = {0: 'blue', 1: 'black', 2: 'red', 3: 'magenta', 4:'yellow', 5: 'green', 6:'gray'}\n", + "\n", + "# make sure we have unique color for each cluster (total number of clusters specified by n_centroids)\n", + "assert len(label_color_map.items()) >= n_centroids" + ] + }, + { + "cell_type": "markdown", + "id": "66864ad0-c462-4223-a249-f2539bbaf63c", + "metadata": {}, + "source": [ + "Plot the clusters. Each color represents a cluster" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5be8c360-d945-486a-9b0f-d0774287f4b9", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "

" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# lets plot the data colored by the cluster they belong to\n", + "for label_value, label_color in label_color_map.items():\n", + " index = (labels == label_value)\n", + " plt.plot(data[index, 0], data[index, 1], 'o', color=label_color)\n", + "\n", + "# lets plot the centroid of the clusters\n", + "plt.scatter(centroids[:, 0], centroids[:, 1], s = 320, marker='*', c=list(label_color_map.values())[0:n_centroids], edgecolors='gray');\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f19e27d2-7f24-41da-962a-6a3ea61e53af", + "metadata": {}, + "source": [ + "#### Exercise: Change the number of clusters and number of observations and see how the clusters change" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b89556c-7e5d-4965-87ae-f3a653d9d3f7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3 (legate) *", + "language": "python", + "name": "conda-env-legate-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/cupynumeric/source/examples/newton_raphson_2d.ipynb b/docs/cupynumeric/source/examples/newton_raphson_2d.ipynb new file mode 100644 index 000000000..43a7edf74 --- /dev/null +++ b/docs/cupynumeric/source/examples/newton_raphson_2d.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ce118a4b-3c3f-42f6-ae1d-8b825fbccb93", + "metadata": {}, + "source": [ + "# Newton Raphson Method In Two Dimensions" + ] + }, + { + "cell_type": "markdown", + "id": "5e12d3fa-68b7-4c43-81c5-515b07a0b33d", + "metadata": {}, + "source": [ + "## Learning Outcomes\n", + "This example teaches how to compute the solution for systems of equations in two variables using NumPy. There are two equations, $f_{1}(x,y)$ and $f_{2}(x, y)$, with two variables each, $x$ and $y$. We seek to find a solution that satisfies these two equations using Newton's method. To understand Newton's method in multiple dimensions, please see [this](https://wiki.math.ntnu.no/_media/tma4125/2017v/newton.pdf) note by Markus Grasmair.\n", + "\n", + "The example also teaches how to interpret a warning from cuPyNumeric when the import statement is changed from importing numpy to importing cuPyNumeric.\n", + "\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "a814eecb-682b-4573-a2c8-572e5f0638f7", + "metadata": {}, + "source": [ + "## Background\n", + "We consider the following functions,\n", + "\n", + "$$\n", + "f_{1}(x,y) = x^{2} + y^{2} - 13 = 0\n", + "$$\n", + "\n", + "$$\n", + "f_{2}(x,y) = x^{2} - 2y^{2} + 14 = 0\n", + "$$\n", + "\n", + "and their Jacobian, $J$, \n", + "\n", + "$$\n", + "J = \\begin{bmatrix}\n", + " \\frac{\\partial f_{1}}{\\partial x} & \\frac{\\partial f_{1}}{\\partial y} \\\\\n", + " \\frac{\\partial f_{2}}{\\partial x} & \\frac{\\partial f_{2}}{\\partial y}\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "\n", + "Substituting the functions, $f_{1}(x, y)$ and $f_{2}(x, y)$, we get,\n", + "\n", + "$$\n", + "J = \\begin{matrix}\n", + " 2x & 2y \\\\\n", + " 2x & -4y\n", + "\\end{matrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "795b4478-cd32-438d-b806-a1215f3a07bb", + "metadata": {}, + "source": [ + "## Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7a0284e7-fe55-4137-95a8-0fff06d535bf", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4252e070-a4d4-4b07-87ad-c03e789f062a", + "metadata": {}, + "outputs": [], + "source": [ + "def function(x: np.ndarray) -> np.ndarray:\n", + " \"Return a numpy array that has the computed values of $f_{1}(x, y)$ and $f_{2}(x, y)$\"\n", + " return np.array([np.sum(x**2) - 13.0, x[0]**2 - 2.0*x[1]**2 + 14.0])\n", + " \n", + "def jacobian(x: np.ndarray) -> np.ndarray:\n", + " \"Return a 2x2 numpy array that has the computed values of the Jacobian, J\"\n", + " return np.array([[2*x[0], 2*x[1]], [2.0*x[0], -4.0*x[1]]])" + ] + }, + { + "cell_type": "markdown", + "id": "136e1298-9af2-4fc3-9b1a-a56afbd54d7a", + "metadata": {}, + "source": [ + "Setup an iterative loop that updates an initial guess $x_{k} = x_{k-1} - {[\\mathbf{J}(x_{k})]}^{-1} \\cdot \\mathbf{f}(x_{k})$\\\n", + "To compute the inverse of the matrix, $\\mathbf{J}$, we use the `inv` API from NumPy's `linalg` package, and to determine when to terminate the loop, \\\n", + "we compute the L2 norm of the difference in solution between two iterations and check if it is less than a specified tolerance." + ] + }, + { + "cell_type": "markdown", + "id": "a91752f1-5ca8-44dd-9a26-525cdf87ab51", + "metadata": {}, + "source": [ + "When you switch the import statement from importing to importing cupynumeric, you might see a warning like this:\n", + "\n", + "---\n", + "\n", + "*RuntimeWarning: cuPyNumeric has not implemented inv and is falling back to canonical NumPy. You may notice significantly decreased performance for this function call.*\n", + "\n", + "---\n", + "\n", + "This means that cuPyNumeric has not implemented the `linalg.inv` API and is falling back to NumPy's implementation. This means that the API would be *eagerly* executed using NumPy's single-threaded implementation. If the API was intended to be invoked from a GPU, the data will get transferred from the GPU to the CPU before the API is executed. This can have performance implications, as indicated by the warning." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c243f28e-ad5e-4c64-8340-96922785c253", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Newton's method converged in 7 iterations to xk: [-2. 3.]\n" + ] + } + ], + "source": [ + "# number of iterations to try\n", + "niters = 20\n", + "\n", + "# tolerance that sets the accuracy of solution\n", + "tol = 1e-6\n", + "\n", + "# print additional information \n", + "verbose = False\n", + "\n", + "# initial guess\n", + "xk = np.array([-20.0, 20.0])\n", + "\n", + "# Newton's method \n", + "for iter in range(niters):\n", + " xk_old = xk\n", + "\n", + " if verbose:\n", + " print(f\"iter: {iter}, xk: {xk}\")\n", + " xk = xk - np.linalg.inv(jacobian(xk)).dot(function(xk))\n", + " \n", + " l2_norm = np.linalg.norm((xk - xk_old))\n", + " if l2_norm < tol:\n", + " break\n", + " \n", + "# let the user know if the solution converged or not\n", + "if iter == niters - 1:\n", + " print(f\"\\nNewton's method did not converge for this function, tolerance ({tol}) and number of iterations ({niters})\")\n", + "else:\n", + " print(f\"\\nNewton's method converged in {iter} iterations to xk: {xk}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5a2e401-e058-4bcc-ac0c-4caa80102079", + "metadata": {}, + "source": [ + "---\n", + "\n", + "We see that the solution has converged to $(x, y) = (-2, 3)$ which satisfies both the equation in 7 iterations\n", + "\n", + "The problem can be cast such that the computation of inverse is substituted by a linear solve, as shown below:\\\n", + "$x_{k} = x_{k-1} - x_{k}^{*}$\\\n", + "$x_{k}^{*} = {[\\mathbf{J}(x_{k})]}^{-1} \\cdot \\mathbf{f}(x_{k})$\n", + "\n", + "And $x_{k}^{*} $ is solution to the system of equation defined as ${\\mathbf{J}(x_{k})}~ x_{k}^{*} = \\mathbf{f}(x_{k})$\n", + "\n", + "---\n", + "\n", + "We can then use NumPy's `linalg.solve` API to perform the linear solve as shown below. And we can see that the algorithm converges to the same solution in exactly the same number of iteration" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "11527885-0be6-4ebf-80fa-9dec85bb0c3c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Newton's method converged in 7 iterations to xk: [-2. 3.]\n" + ] + } + ], + "source": [ + "# number of iterations to try\n", + "niters = 20\n", + "\n", + "# tolerance that sets the accuracy of solution\n", + "tol = 1e-6\n", + "\n", + "# print additional information \n", + "verbose = False\n", + "\n", + "# initial guess\n", + "xk = np.array([-20.0, 20.0])\n", + "\n", + "# Newton's method \n", + "for iter in range(niters):\n", + " xk_old = xk\n", + "\n", + " if verbose:\n", + " print(f\"iter: {iter}, xk: {xk}\")\n", + " xk = xk - np.linalg.solve(jacobian(xk), function(xk)) ## This uses linalg.solve\n", + " \n", + " l2_norm = np.linalg.norm((xk - xk_old))\n", + " if l2_norm < tol:\n", + " break\n", + " \n", + "# let the user know if the solution converged or not\n", + "if iter == niters - 1:\n", + " print(f\"\\nNewton's method did not converge for this function, tolerance ({tol}) and number of iterations ({niters})\")\n", + "else:\n", + " print(f\"\\nNewton's method converged in {iter} iterations to xk: {xk}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c9f494c-518a-4f78-9e88-1aeb2221fa1b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/cunumeric/source/user/notebooks/stencil.ipynb b/docs/cupynumeric/source/examples/stencil.ipynb similarity index 99% rename from docs/cunumeric/source/user/notebooks/stencil.ipynb rename to docs/cupynumeric/source/examples/stencil.ipynb index b1580658f..72a635efa 100644 --- a/docs/cunumeric/source/user/notebooks/stencil.ipynb +++ b/docs/cupynumeric/source/examples/stencil.ipynb @@ -11,7 +11,7 @@ "License\n", "
\n",
     "\n",
-    "Copyright 2023 NVIDIA Corporation\n",
+    "Copyright 2024 NVIDIA Corporation\n",
     "\n",
     "Licensed under the Apache License, Version 2.0 (the \"License\");\n",
     "you may not use this file except in compliance with the License.\n",
@@ -33,7 +33,7 @@
    "id": "35c48e6f-1bde-4aac-af55-b7218cc22491",
    "metadata": {},
    "source": [
-    "To get started, `import cunumeric as np` (just the same way we would import `numpy`)\n"
+    "To get started, `import cupynumeric as np` (just the same way we would import `numpy`)\n"
    ]
   },
   {
@@ -45,7 +45,7 @@
    },
    "outputs": [],
    "source": [
-    "import cunumeric as np  # instead of numpy"
+    "import cupynumeric as np  # instead of numpy"
    ]
   },
   {
diff --git a/docs/cupynumeric/source/examples/torchswe.ipynb b/docs/cupynumeric/source/examples/torchswe.ipynb
new file mode 100644
index 000000000..c4b6173b9
--- /dev/null
+++ b/docs/cupynumeric/source/examples/torchswe.ipynb
@@ -0,0 +1,219 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "5be6c57b-7cae-4fc1-b78f-899becabc6ee",
+   "metadata": {},
+   "source": [
+    "# TorchSWE case study\n",
+    "\n",
+    "\n",
+    "[TorchSWE](https://github.com/piyueh/TorchSWE) is a shallow-water solver created by Dr. Pi-Yueh Chuang and Prof. Lorena Barba that solves the vertically averaged Navier-Stokes equations using MPI and CuPy. It can simulate free-surface water flow in rivers, channels, and coastal areas, as well as model flood inundation. Given a topography, TorchSWE can predict flood-prone areas and the height of water inundation, making it a valuable tool for risk mapping.\n",
+    "\n",
+    "High-resolution numerical simulations—such as those on real topographies requiring hundreds of millions of data points—demand distributed computation across multiple GPUs. Although scalability is achievable with MPI4Py and CuPy, this approach requires manually partitioning the problem and managing inter-GPU data communication, which are complex and error-prone tasks.\n",
+    "\n",
+    "cuPyNumeric enables a distributed implementation of TorchSWE using only NumPy operations, without the complexities of MPI+CuPy. After porting TorchSWE to cuPyNumeric by removing all domain decomposition logic, it scaled effortlessly across multiple GPUs and nodes without further code modifications. This scalability enabled high-fidelity simulations exceeding 1.2 billion data points using 32 GPUs, allowing researchers to tackle critical scientific problems in flood inundation modeling without needing specialized distributed computing expertise. Overall, the cuPyNumeric implementation reduced the lines of code by over 20%, and simplified development and maintenance by eliminating complex logic for managing distribution and communication.\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0402fb01-748b-48d9-9caa-80e7510ade80",
+   "metadata": {},
+   "source": [
+    "\n",
+    "

Deep dive into the TorchSWE code implementation

\n", + "\n", + "

Original code details

\n", + "\n", + "TorchSWE uses stencil operations to model shallow-water equations on a 2D grid, where each point is updated based on neighboring values, simulating water flow dynamics. The stencil computations are structured to update each grid cell iteratively, based on data from surrounding cells, mimicking fluid behavior over time. Below is an example that mimics the basic structure of the stencil logic from the TorchSWE repository:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "640f0b62-f70f-4d8a-86c5-7b4739e60a33", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + " \n", + "# Example dimensions for the grid\n", + "nx, ny = 128, 128\n", + "grid = np.ones((nx, ny)) # Initialize the grid with \"1\"\n", + "\n", + "# Stencil operation \n", + "for i in range(1, nx - 1):\n", + " for j in range(1, ny - 1):\n", + " grid[i, j] = (grid[i + 1, j] + grid[i - 1, j] + grid[i, j + 1] + grid[i, j - 1]) / 4\n" + ] + }, + { + "cell_type": "markdown", + "id": "0281b3f4-5a48-40cc-9ec8-0fc9d7fd760c", + "metadata": {}, + "source": [ + "This code iteratively updates cell `h[i, j]` using adjacent cells, representing a basic averaging stencil operation that can be extended to various boundary conditions and flow dynamics in the shallow-water model. For full context, refer to [TorchSWE on GitHub](https://github.com/piyueh/TorchSWE).\n", + "\n", + "Parallelizing stencil operations for multi-GPU systems is challenging. When arrays are partitioned across multiple GPUs, any update to a cell requires the updated values to be shared between GPUs to maintain consistency across boundaries. This communication overhead and synchronization make parallelizing stencil code complex and difficult to implement efficiently on multi-GPU architectures.\n", + "\n", + "Below, we outline TorchSWE’s MPI4Py logic in more detail to highlight the complexity involved in this implementation.\n", + "Here’s an example code snippet that mirrors the TorchSWE MPI logic, implementing a simple MPI stencil operation from above:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0d7db631-3ae9-41ca-a0f1-07390349fbd0", + "metadata": {}, + "outputs": [], + "source": [ + "from mpi4py import MPI\n", + "import cupy as cp\n", + "\n", + "num_timesteps=10\n", + "\n", + "def set_device(comm: MPI.Comm):\n", + " # Device selection for each rank on multi-GPU nodes (TorchSWE-specific)\n", + " n_gpus = cp.cuda.runtime.getDeviceCount()\n", + " local_rank = comm.Get_rank() % n_gpus\n", + " cp.cuda.runtime.setDevice(local_rank)\n", + "\n", + "comm = MPI.COMM_WORLD\n", + "rank = comm.Get_rank()\n", + "size = comm.Get_size()\n", + "\n", + "# Determine grid size and decompose domain\n", + "gnx, gny = 126,126 # global grid dimensions\n", + "local_nx, local_ny = gnx // size, gny # local grid dimensions per rank\n", + "local_grid = cp.ones((local_nx + 2, local_ny + 2)) # with halo boundaries\n", + "\n", + "# Set up MPI data types and boundaries\n", + "send_type, recv_type = MPI.DOUBLE.Create_subarray((local_nx + 2, local_ny + 2), (local_nx, local_ny), (1, 1)), MPI.DOUBLE.Create_subarray((local_nx + 2, local_ny + 2), (local_nx, local_ny), (1, 1))\n", + "send_type.Commit()\n", + "recv_type.Commit()\n", + "\n", + "# Stencil computation loop\n", + "for timestep in range(num_timesteps):\n", + " # Boundary exchange with non-blocking sends/receives\n", + " reqs = []\n", + " if rank > 0:\n", + " reqs.append(comm.Isend(local_grid[1, :], dest=rank - 1))\n", + " reqs.append(comm.Irecv(local_grid[0, :], source=rank - 1))\n", + " if rank < size - 1:\n", + " reqs.append(comm.Isend(local_grid[local_nx, :], dest=rank + 1))\n", + " reqs.append(comm.Irecv(local_grid[local_nx + 1, :], source=rank + 1))\n", + "\n", + " # Ensure all sends/receives are complete\n", + " MPI.Request.Waitall(reqs)\n", + "\n", + " # Perform stencil operation\n", + " for i in range(1, local_nx + 1):\n", + " for j in range(1, local_ny + 1):\n", + " local_grid[i, j] = 0.25 * (local_grid[i - 1, j] + local_grid[i + 1, j] +\n", + " local_grid[i, j - 1] + local_grid[i, j + 1])\n", + "\n", + "# Clean up MPI data types\n", + "send_type.Free()\n", + "recv_type.Free()\n", + "MPI.Finalize()\n" + ] + }, + { + "cell_type": "markdown", + "id": "660621f9-2bc9-49a3-be59-cde1ce87df65", + "metadata": {}, + "source": [ + "This example follows TorchSWE's approach to domain decomposition and parallelization as in the original implementation. It starts with MPI initialization and sets up logic to manage GPU assignment per rank, dividing the global grid into subdomains. Each rank is responsible for a local subgrid with added halo rows to hold neighboring data. Once the domain is decomposed, the user must ensure proper communication of data at processor boundaries, accounting for datatype differences between CuPy and MPI4Py. For optimal performance, the appropriate type of point-to-point communication, such as non-blocking send/recv, must be selected, as incorrect implementation can cause deadlock. Users must also handle varying numbers of neighboring ranks on domain boundaries and ensure data exchange across mesh, topography, and solution variables. Non-blocking `Isend` and `Irecv` functions handle boundary data exchanges, allowing each rank to receive necessary data for stencil computations. After a `Waitall` synchronization step, each rank performs computations on its subdomain. Finally, custom MPI data types are freed, and `MPI_Finalize()` concludes the environment.\n", + "\n", + "The actual TorchSWE code has additional complexities specific to its use of multiple arrays, GPU memory management, one-sided communications etc.\n", + "For the complete implementation, you can refer to the [TorchSWE repository](https://github.com/piyueh/TorchSWE).\n", + "\n", + "Explicit distributed logic, like that in TorchSWE, is difficult to debug and maintain throughout the lifespan of simulation codes. Most applications, including TorchSWE, require specialized validation tests to ensure correct outputs. This results in significant programming effort and further complicates development. \n" + ] + }, + { + "cell_type": "markdown", + "id": "e93aa24e-fc18-4f69-819d-59b5997aa087", + "metadata": {}, + "source": [ + "

cuPyNumeric Implementation

\n", + "\n", + "In the [cuPyNumeric version of TorchSWE](https://github.com/shriram-jagan/TorchSWE), stencil operations are implemented using distributed array handling from cuPyNumeric, simplifying the code and removing the need for manual partitioning or boundary synchronization. The code operates similarly to NumPy slicing but scales across multiple GPUs. For example, the stencil computation in this version would typically involve using simple array slices like below (instead of the nested loops with integrated MPI logic as in the original implementation).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6e15757-a681-4a09-9f82-6304adf82fb4", + "metadata": {}, + "outputs": [], + "source": [ + "import cupynumeric as np\n", + " \n", + "# Example dimensions\n", + "nx, ny = 128, 128\n", + "\n", + "# Initialize the array h\n", + "grid = np.ones((nx, ny))\n", + "\n", + "# Stencil operation using slicing\n", + "grid[1:-1, 1:-1] = (\n", + " grid[2:, 1:-1] + # Below\n", + " grid[:-2, 1:-1] + # Above\n", + " grid[1:-1, 2:] + # Right\n", + " grid[1:-1, :-2] # Left\n", + ") / 4\n" + ] + }, + { + "cell_type": "markdown", + "id": "f29f5387-3408-4bff-948d-55519412de31", + "metadata": {}, + "source": [ + "This operation is automatically managed across nodes and GPUs without needing MPI-specific code. More details can be found in the [cuPyNumeric port of TorchSWE](https://github.com/shriram-jagan/TorchSWE).\n", + "\n", + "The cuPyNumeric version of TorchSWE eliminates 600 lines of code related to domain decomposition, communication, synchronization, and validation that would otherwise be needed when using MPI4Py with CuPy. These 600 lines require substantial knowledge of distributed computing from domain scientists. By using cuPyNumeric, the simplified NumPy code scales efficiently to 1024 GPUs, making high-fidelity flood modeling accessible without requiring specialized expertise in distributed systems." + ] + }, + { + "cell_type": "markdown", + "id": "7e5d6565-ceda-4b61-8826-b6ae5aff3c83", + "metadata": {}, + "source": [ + "

Conclusion

\n", + "\n", + "cuPyNumeric significantly simplifies the development and maintenance of distributed simulations, such as TorchSWE, by abstracting complex parallelization, synchronization, and communication logic. This eliminates the need for specialized HPC knowledge and reduces the risk of errors, allowing domain scientists to focus on their research. With cuPyNumeric, large-scale simulations can scale efficiently across large HPC systems, enhancing productivity, reducing programming effort, and lowering development costs. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb3a186a-3ea7-4150-8ec0-7760ad2adf1f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/cupynumeric/source/faqs.rst b/docs/cupynumeric/source/faqs.rst new file mode 100644 index 000000000..b581ad791 --- /dev/null +++ b/docs/cupynumeric/source/faqs.rst @@ -0,0 +1,206 @@ +.. _faqs: + +Frequently Asked Questions +========================== + + +What are the different task variants available in Legate? +--------------------------------------------------------- + +Legate offers three different task variants: CPU, OMP, and GPU. A task variant +determines the type of processor Legate chooses to perform the computations. + +What is the difference between Legate and cuPyNumeric? +------------------------------------------------------ + +Legate is a task-based runtime software stack that enables development of +scalable and composable libraries for distributed and accelerated computing. + +cuPyNumeric is one of the foundational libraries built using Legate and aspires +to be a distributed and accelerated drop-in replacement library for NumPy, an +array programming library widely used in scientific computing. cuPyNumeric scales +idiomatic NumPy programs to multiple GPUs and CPUs and seamlessly interoperates +with other Legate libraries. + +Check out this `blog post `_ +to learn more about cuPyNumeric. + +When to use python vs legate? +----------------------------- + +The ``legate`` launcher affords comman line options for configurtion, while +using ``python`` requires configuring via ``LEGATE_CONFIG``. When running +local applications, it is mostly a matter of preference. When running in +multi-node situations, ``legate`` has some additional command line options +that may make usage simpler. + +What if I don’t have a GPU? +--------------------------- + +If you don’t have a GPU, you can either use the CPU or the OMP variant. See +`Resource allocation` for informations on how to use the respective variants. + +What does this warning mean? +---------------------------- + +.. code-block:: text + + RuntimeWarning: cuPyNumeric has not implemented and is falling back to canonical NumPy. You may notice significantly decreased performance for this function call. + +This means that the NumPy has not been implemented in cuPyNumeric and that +the Legate runtime is falling back to using NumPy’s implementation which will +be single-threaded execution and can lead to decreased performance for that +function call. + +.. code-block:: text + + [0 - 7f0524da9740] 0.000028 {4}{threads}: reservation ('dedicated worker (generic) #1') cannot be satisfied + +or + +.. code-block:: text + + [0 - 7fe90fa7d740] 0.000029 {4}{threads}: reservation ('utility proc 1d00000000000001') cannot be satisfied + +This indicates that the runtime was unable to pin threads onto available cores, +which usually means that the available CPU cores were oversubscribed because +the user has requested more cores than is available. + +If the user does not specify which type of processor to run on, legate will use +4 CPUs to execute the program. Legate will also need one core to perform the +dependency analysis and schedule the tasks. If there are fewer than five cores +on the machine, try reducing the number of cores (``--cpus``) passed to legate. + +This warning is currently expected on MacOS. + +How to determine available memory? +---------------------------------- + +On Linux, running the following command will display the amount of +available system memory: + +.. code-block:: sh + + cat /proc/meminfo | grep MemAvailable + +Available GPU memory (for each GPU) can be displayed by running: + +.. code-block:: sh + + nvidia-smi --query-gpu memory.free --format=csv + +Both of these represent the available amount of memory, which may be shared +with other processes or libraries. You may need to reduce these amounts to +account for these, or to reflect the actual size of your problem more closely. + +If you do not have access to run the commands above, then refer to published +machine specs or cluster documentation. + +How to handle Out-Of-Memory errors? +----------------------------------- + +.. code-block:: text + + [0 - 7fda18f26000] 0.805182 {5}{cunumeric.mapper}: Failed to allocate 8388608 bytes on memory 1e00000000000000 (of kind SYSTEM_MEM) for region requirement(s) 1 of Task cupynumeric::BinaryOpTask[oom.py:24] (UID 18) + +The above error indicates that the application ran out of memory during +execution. More granular details on the type of memory, the task that triggered +the error, and what was using up the available memory are provided in the error +message. If possible, try increasing the amount of system memory or framebuffer +memory allocated to the program, or decrease the problem size. + +Reducing the ``--eager-alloc-percentage`` to, say, 10 or less can also help +since this reduces the amount of available memory available to the eager memory +pool and will consequently increase the memory reserved for the deferred memory +pool. + +Why are the results different from NumPy? +----------------------------------------- + +While a majority of the APIs will give the same result as NumPy, some APIs +might be implemented differently from that of NumPy which might lead to +differences in results. One such example is, :ref:`reshape`, which returns a +copy of the array in cuPyNumeric but returns a view in NumPy. Another example +is :ref:`astype` which does *not* return a copy by default, where NumPy does. + +Such differences in implementation are noted in the documentation of the +cuPyNumeric APIs, please review them before opening an issue on the +`cuPyNumeric issue tracker `_. + +Why doesn’t Legate use my GPU? +------------------------------ + +If you explicitly asked legate to use the GPU but find that the GPU is not +being used, it is possible that your problem size is too small to be run on +GPU and be performant. Either increase your problem size significantly or set +the environment variable ``LEGATE_TEST`` to 1 and run. Setting this environment +variable tells Legate to always use the prescribed resources regardless of the +problem size. + +What are the anti-patterns in a NumPy code? +------------------------------------------- + +Check out our :ref:`practices` to avoid some of the anti-patterns commonly +encountered in applications. + +How do I time the execution of my application? +---------------------------------------------- + +Check out the :ref:`benchmarking` section for information on how to accurately +measure cuPyNumeric execution. + +Why is cuPyNumeric slower than NumPy on my laptop? +-------------------------------------------------- + +For small problem sizes, cuPyNumeric might be slower than NumPy. We suggest you +increase the problem size and correspondingly increase the resources needed +for the problem size as described in the Usage section. Take a look at our +:ref:`practices` on how to do that. + +Why is cuPyNumeric slower than CuPy on my laptop? +------------------------------------------------- + +For small problem sizes, cuPyNumeric might be slower than CuPy. We suggest you +increase the problem size and correspondingly increase the resources needed for +the problem size as described in the :ref:`Usage` section. Take a look at +performance :ref:`practices`. + +How do I use Jupyter Notebooks? +------------------------------- + +See https://docs.nvidia.com/legate/latest/jupyter.html. + +How to pass Legion and Realm arguments? +--------------------------------------- + +See :ref:`advanced`. + +What is the version of legate? +------------------------------ + +Use ``legate-issue`` to know more about the version of Legate, Legion and +several other key packages. + +You can also run ``legate –verbose ./script.py `` to get +verbose output. + +What are the defaults? +---------------------- + +The default values for several input arguments to Legate are mentioned in +Legate's documentation. + +Where I can read more about cuPyNumeric? +---------------------------------------- + +Check out this `blog post `_ +or this `tutorial `_ +to learn more about cuPyNumeric. + +Questions? +---------- + +For technical questions about cuPyNumeric and Legate-based tools, please visit +the `community discussion forum `_. + +If you have other questions, please contact us at *legate@nvidia.com*. diff --git a/docs/cupynumeric/source/index.rst b/docs/cupynumeric/source/index.rst new file mode 100644 index 000000000..b0e163d8e --- /dev/null +++ b/docs/cupynumeric/source/index.rst @@ -0,0 +1,34 @@ +:html_theme.sidebar_secondary.remove: + +NVIDIA cuPyNumeric +================== + +With cuPyNumeric you can write code productively in Python, using the familiar +`NumPy API`_, and have your program scale with no code changes from single-CPU +computers to multi-node-multi-GPU clusters. + +For example, you can run the final example of the `Python CFD course`_ +completely unmodified on 2048 A100 GPUs in a `DGX SuperPOD`_ and achieve +good weak scaling. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + installation + user/index + examples/index + api/index + faqs + developer/index + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`search` + +.. _DGX SuperPOD: https://www.nvidia.com/en-us/data-center/dgx-superpod/ +.. _Numpy API: https://numpy.org/doc/stable/reference/ +.. _Python CFD course: https://github.com/barbagroup/CFDPython/blob/master/lessons/15_Step_12.ipynb \ No newline at end of file diff --git a/docs/cupynumeric/source/installation.rst b/docs/cupynumeric/source/installation.rst new file mode 100644 index 000000000..d5e97c844 --- /dev/null +++ b/docs/cupynumeric/source/installation.rst @@ -0,0 +1,63 @@ +Installation +============ + +Default conda install +--------------------- + +cuPyNumeric supports the +`same platforms as Legate `_. + +cuPyNumeric is available from +`conda `_ +on the `legate channel `_. +Please make sure you have at least conda version 24.1 installed, then create +a new environment containing cuPyNumeric: + +.. code-block:: sh + + conda create -n myenv -c conda-forge -c legate cupynumeric + +or install it into an existing environment: + +.. code-block:: sh + + conda install -c conda-forge -c legate cupynumeric + +Packages with GPU support are available, and will be chosen automatically by +``conda install`` on systems with GPUs. + +In an environment without GPUs available, ``conda install`` will by default +choose a CPU-only package. To install a version with GPU support in such an +environment, use environment variable ``CONDA_OVERRIDE_CUDA``: + +.. code-block:: sh + + CONDA_OVERRIDE_CUDA="12.2" \ + conda install -c conda-forge -c legate cupynumeric + +Once installed, you can verify the installation by running one of the examples +from the +`cuPyNumeric repository `_, +for instance: + +.. code-block:: sh + + $ legate examples/black_scholes.py + Running black scholes on 10K options... + Elapsed Time: 129.017 ms + +Building from source +--------------------- + +See :ref:`building cupynumeric from source` for instructions on building +cuPyNumeric manually. + +Licenses +-------- + +This project will download and install additional third-party open source +software projects at install time. Review the license terms of these open +source projects before use. + +For license information regarding projects bundled directly, see +:ref:`thirdparty`. \ No newline at end of file diff --git a/docs/cupynumeric/source/oss-licenses.rst b/docs/cupynumeric/source/oss-licenses.rst new file mode 100644 index 000000000..a6a9b0226 --- /dev/null +++ b/docs/cupynumeric/source/oss-licenses.rst @@ -0,0 +1,123 @@ +:orphan: + +.. _thirdparty: + +Third-party notices +=================== + +TBLIS +----- + +.. code-block:: none + + Copyright (c) 2015-2017, Devin Matthews, except where otherwise indicated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Devin Matthews nor the names of any + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Cephes +------ + +.. code-block:: none + + Distributed under 3-clause BSD license with permission from the author, + see https://lists.debian.org/debian-legal/2004/12/msg00295.html + + Cephes Math Library Release 2.8: June, 2000 + Copyright 1984, 1995, 2000 by Stephen L. Moshier + + This software is derived from the Cephes Math Library and is + incorporated herein by permission of the author. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Random Kit +---------- + +.. code-block:: none + + Copyright 2005 Robert Kern (robert.kern@gmail.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + The implementations of rk_hypergeometric_hyp(), rk_hypergeometric_hrua(), + and rk_triangular() were adapted from Ivan Frohne's rv.py which has this + license: + + Copyright 1998 by Ivan Frohne; Wasilla, Alaska, U.S.A. All Rights + Reserved + + Permission to use, copy, modify and distribute this software and its + documentation for any purpose, free of charge, is granted subject to the + following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the software. + + THE SOFTWARE AND DOCUMENTATION IS PROVIDED WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR + OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM OR DAMAGES IN A CONTRACT ACTION, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR ITS DOCUMENTATION. diff --git a/docs/cupynumeric/source/user/advanced.rst b/docs/cupynumeric/source/user/advanced.rst new file mode 100644 index 000000000..b6bbc31fc --- /dev/null +++ b/docs/cupynumeric/source/user/advanced.rst @@ -0,0 +1,42 @@ +.. _advanced: + +Advanced topics +=============== + +Multi-node execution +-------------------- + +Using ``legate`` +~~~~~~~~~~~~~~~~ + +cuPyNumeric programs can be run in parallel by using the ``--nodes`` option to +the ``legate`` driver, followed by the number of nodes to be used. +When running on 2+ nodes, a task launcher must be specified. + +Legate currently supports using ``mpirun``, ``srun``, and ``jsrun`` as task +launchers for multi-node execution via the ``--launcher`` command like +arguments: + +.. code-block:: sh + + legate --launcher srun --nodes 2 script.py