From ad0a7f00c15b145cc9fd114cd2c7f7096842366d Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Sun, 17 Jan 2021 10:27:53 +1100 Subject: [PATCH] add tests and stuff --- package/MDAnalysis/analysis/align.py | 3 +- package/MDAnalysis/analysis/base.py | 10 +- .../analysis/clustering/__init__.py | 23 + .../analysis/clustering/cluster_methods.pyx | 7 - .../analysis/clustering/clusteranalysis.py | 0 .../analysis/clustering/include/ap.h | 23 - .../MDAnalysis/analysis/clustering/methods.py | 23 + .../MDAnalysis/analysis/clustering/src/ap.cpp | 191 --- .../encore/clustering/ClusteringMethod.py | 4 +- .../encore/clustering/affinityprop.pyx | 2 - .../MDAnalysis/analysis/encore/covariance.py | 14 +- .../MDAnalysis/analysis/encore/similarity.py | 19 +- package/MDAnalysis/analysis/pca.py | 11 +- .../MDAnalysis/analysis/reencore/__init__.py | 23 + .../analysis/reencore/similarity.py | 300 ++-- package/MDAnalysis/analysis/reencore/utils.py | 111 +- package/MDAnalysis/core/ensemble.py | 52 +- package/MDAnalysis/core/universe.py | 88 +- package/setup.py | 4 +- .../MDAnalysisTests/analysis/test_encore.py | 1477 ++++++++--------- .../MDAnalysisTests/analysis/test_reencore.py | 120 +- testsuite/MDAnalysisTests/rmsfit_adk_dims.dcd | Bin 108924 -> 0 bytes 22 files changed, 1244 insertions(+), 1261 deletions(-) delete mode 100644 package/MDAnalysis/analysis/clustering/clusteranalysis.py delete mode 100644 package/MDAnalysis/analysis/clustering/include/ap.h delete mode 100644 package/MDAnalysis/analysis/clustering/src/ap.cpp delete mode 100644 testsuite/MDAnalysisTests/rmsfit_adk_dims.dcd diff --git a/package/MDAnalysis/analysis/align.py b/package/MDAnalysis/analysis/align.py index 1593cd4f919..da0a641e3be 100644 --- a/package/MDAnalysis/analysis/align.py +++ b/package/MDAnalysis/analysis/align.py @@ -338,6 +338,7 @@ def _fit_to(mobile_coordinates, ref_coordinates, mobile_atoms, """ R, min_rmsd = rotation_matrix(mobile_coordinates, ref_coordinates, weights=weights) + mobile_atoms.translate(-mobile_com) mobile_atoms.rotate(R) mobile_atoms.translate(ref_com) @@ -630,7 +631,7 @@ def __init__(self, mobile, reference, select='all', filename=None, select = rms.process_selection(select) self.ref_atoms = reference.select_atoms(*select['reference']).atoms self.mobile_atoms = mobile.select_atoms(*select['mobile']).atoms - if in_memory or not isinstance(mobile.trajectory, MemoryReader): + if in_memory or isinstance(mobile.trajectory, MemoryReader): mobile.transfer_to_memory() filename = None logger.info("Moved mobile trajectory to in-memory representation") diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 72e270cd539..9b7408f9650 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -113,7 +113,7 @@ def __init__(self, trajectory, verbose=False, **kwargs): self._trajectory = trajectory self._verbose = verbose - def _setup_frames(self, trajectory, start=None, stop=None, step=None): + def _setup_frames(self, trajectory, start=None, stop=None, step=None, frames=None): """ Pass a Reader object and define the desired iteration pattern through the trajectory @@ -139,7 +139,9 @@ def _setup_frames(self, trajectory, start=None, stop=None, step=None): self.start = start self.stop = stop self.step = step - self.frame_indices = np.arange(start, stop, step) + if frames is None: + frames = np.arange(start, stop, step) + self.frame_indices = frames self.n_frames = len(self.frame_indices) self.frames = np.zeros(self.n_frames, dtype=int) self.times = np.zeros(self.n_frames) @@ -162,7 +164,7 @@ def _conclude(self): """ pass # pylint: disable=unnecessary-pass - def run(self, start=None, stop=None, step=None, verbose=None): + def run(self, start=None, stop=None, step=None, frames=None, verbose=None): """Perform the calculation Parameters @@ -181,7 +183,7 @@ def run(self, start=None, stop=None, step=None, verbose=None): verbose = getattr(self, '_verbose', False) if verbose is None else verbose - self._setup_frames(self._trajectory, start, stop, step) + self._setup_frames(self._trajectory, start, stop, step, frames) logger.info("Starting preparation") self._prepare() for i, frame in enumerate(ProgressBar(self.frame_indices, verbose=verbose)): diff --git a/package/MDAnalysis/analysis/clustering/__init__.py b/package/MDAnalysis/analysis/clustering/__init__.py index 304ab33362d..dc9a6c31f93 100644 --- a/package/MDAnalysis/analysis/clustering/__init__.py +++ b/package/MDAnalysis/analysis/clustering/__init__.py @@ -1,2 +1,25 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + from .clusters import Clusters from . import methods \ No newline at end of file diff --git a/package/MDAnalysis/analysis/clustering/cluster_methods.pyx b/package/MDAnalysis/analysis/clustering/cluster_methods.pyx index a5422815cd1..fbe16d32465 100644 --- a/package/MDAnalysis/analysis/clustering/cluster_methods.pyx +++ b/package/MDAnalysis/analysis/clustering/cluster_methods.pyx @@ -22,9 +22,6 @@ # """ Cython wrapper for the C implementation of the Affinity Perturbation clustering algorithm. - -:Author: Matteo Tiberti, Wouter Boomsma, Tone Bengtsen - """ import warnings @@ -83,10 +80,6 @@ def affinity_propagation(similarity, preference, float lam, int max_iter, int co cdef np.ndarray[np.float32_t, ndim=1] sim = np.ravel(similarity[indices]).astype(np.float32) - # sim = np.ascontiguousarray(sim) - - print(sim) - cdef np.ndarray[long, ndim=1] clusters = np.zeros((cn), dtype=long) # run C module Affinity Propagation diff --git a/package/MDAnalysis/analysis/clustering/clusteranalysis.py b/package/MDAnalysis/analysis/clustering/clusteranalysis.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/package/MDAnalysis/analysis/clustering/include/ap.h b/package/MDAnalysis/analysis/clustering/include/ap.h deleted file mode 100644 index 0151e10c883..00000000000 --- a/package/MDAnalysis/analysis/clustering/include/ap.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -*- tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- - vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 - - MDAnalysis --- https://www.mdanalysis.org - Copyright (c) 2006-2016 The MDAnalysis Development Team and contributors - (see the file AUTHORS for the full list of names) - - Released under the GNU Public Licence, v2 or any higher version - - Please cite your use of MDAnalysis in published work: - - R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, - D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. - MDAnalysis: A Python package for the rapid analysis of molecular dynamics - simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th - Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. - - N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. - MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. - J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 -*/ - -int CAffinityPropagation(float*, int, float, int, int, int, long*); diff --git a/package/MDAnalysis/analysis/clustering/methods.py b/package/MDAnalysis/analysis/clustering/methods.py index ba626d07a45..e0e3651cd5e 100644 --- a/package/MDAnalysis/analysis/clustering/methods.py +++ b/package/MDAnalysis/analysis/clustering/methods.py @@ -1,3 +1,26 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + from . import cluster_methods class AffinityPropagation: diff --git a/package/MDAnalysis/analysis/clustering/src/ap.cpp b/package/MDAnalysis/analysis/clustering/src/ap.cpp deleted file mode 100644 index 4594547070d..00000000000 --- a/package/MDAnalysis/analysis/clustering/src/ap.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/* -*- tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- - vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 - - MDAnalysis --- https://www.mdanalysis.org - Copyright (c) 2006-2016 The MDAnalysis Development Team and contributors - (see the file AUTHORS for the full list of names) - - Released under the GNU Public Licence, v2 or any higher version - - Please cite your use of MDAnalysis in published work: - - R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, - D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. - MDAnalysis: A Python package for the rapid analysis of molecular dynamics - simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th - Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. - - N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. - MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. - J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 -*/ -#include -#include -#include -#include -#include -#include - -// inline int tri_i(int row_i, int col_i) { -// return (row_i)>(col_i) ? ((row_i)+1)*(row_i)/2+(col_i) : ((col_i)+1)*(col_i)/2+(row_i); -// } - -inline int get_idx(int row_i, int col_i, int n_col) { - return row_i * n_col + col_i; -} - -int CAffinityPropagation(float* sim, int length, float lambda, - int max_iter, int conv_threshold, int add_noise, - long* clusters) { - - float* res = new float[length*length]; - float* av = new float[length*length]; - float* simav = new float[length*length]; - - - if (add_noise > 0) { - for (int i=0; i < length; i++) { - for (int j = 0; j < i; j++) { - - int idx = get_idx(i, j, length); - float val = sim[idx]; - float noise = 1e-16 * val * (rand() / (float)RAND_MAX+1); - sim[idx] += noise; - sim[get_idx(j, i, length)] += noise; - } - } - } - - // for (int i = 0; i < length; i++) { - // for (int j = 0; j < i; j++) { - // int trix = tri_i(i, j); - // sim[idx] = similarity[trix]; - // sim[j][i] = similarity[trix]; - // } - // } - - // initialize exemplars - int exemplars[length]; - int old_exemplars[length]; - std::fill_n(exemplars, length, -1); - - // main loop - int iter = 0; - float lambda_comp = 1.0 - lambda; - bool converged = false; - while (iter < max_iter && !converged) { - // update responsibilities - for (int i = 0; i < length; i++) { - for (int j = 0; j < length; j++) { - int idx = get_idx(i, j, length); - simav[idx] = sim[idx] + av[idx]; - } - std::sort(&simav[0], &simav[length]); - float *unique = std::unique(&simav[0], &simav[length]); - std::nth_element(&simav[0], &simav[2], unique, std::greater()); - float max1 = simav[0]; - float max2 = simav[1]; - - for (int j = 0; j < length; j++) { - int idx = get_idx(i, j, length); - if (av[idx] + sim[idx] == max1) { - res[idx] = lambda * av[idx] + lambda_comp * (sim[idx] - max2); - } else { - res[idx] = lambda * av[idx] + lambda_comp * (sim[idx] - max1); - } - } - } - // update availabilities - for (int j = 0; j < length; j++) { - float rsum = 0.0; - for (int i = 0; i < length; i++) { - int idx = get_idx(i, j, length); - float r = res[idx]; - if (i == j || r > 0.0) { - rsum += r; - } - } - for (int i = 0; i < length; i++) { - int idx = get_idx(i, j, length); - av[idx] *= lambda; - if (i != j) { - float r = res[idx]; - if (r > 0.0) { - rsum -= r; - } - if (rsum >= 0.0) { - av[idx] += lambda_comp * rsum; - } - } else { - av[idx] = lambda_comp * (rsum - res[get_idx(j, j, length)]); - } - } - } - - // convergence check - int *tmp_exemplars = old_exemplars; - int *old_exemplars = exemplars; - int *exemplars = tmp_exemplars; - - bool has_cluster = false; - for (int i = 0; i < length; i++) { - int idx = get_idx(i, i, length); - if (res[idx] + av[idx] > 0.0) { - exemplars[i] = 1; - has_cluster = true; - } else { - exemplars[i] = 0; - } - } - - int conv_count = 0; - if (has_cluster) { - conv_count++; - for (int j = 0; j < length; j++) { - if (exemplars[j] != old_exemplars[j]) { - conv_count = 0; - break; - } - } - } - - if (conv_count == conv_threshold) { - converged = true; - } - iter++; - } - - // find number of clusters - int n_clusters = 0; - for (int i = 0; i 0) { - exemplars[n_clusters] = i; - n_clusters++; - } - } - - for (int i=0; i < length; i++) { - float maxsim = res[i*length] + av[i*length]; - for (int j = 0; j < length; j++) { - int idx = get_idx(i, j, length); - float tmpsum = res[idx] + av[idx]; - if (tmpsum >= maxsim) { - clusters[i] = j; - maxsim = tmpsum; - } - } - } - - for (int i = 0; i EPSILON] pB = tmpB[tmpA + tmpB > EPSILON] - print(pA, pB) - raise ValueError() - return discrete_jensen_shannon_divergence(pA, pB) @@ -557,13 +547,8 @@ def dimred_ensemble_similarity(kde1, resamples1, kde2, resamples2, ln_P1P2_exp_P1 = np.average(np.log( 0.5 * (kde1.evaluate(resamples1) + kde2.evaluate(resamples1)))) ln_P1P2_exp_P2 = np.average(np.log( - 0.5 * (kde1.evaluate(resamples2) + - kde2.evaluate(resamples2)))) + 0.5 * (kde1.evaluate(resamples2) + kde2.evaluate(resamples2)))) - print(ln_P1_exp_P1) - print(ln_P2_exp_P2) - print(ln_P1P2_exp_P1+ln_P1P2_exp_P2) - return 0.5 * ( ln_P1_exp_P1 - ln_P1P2_exp_P1 + ln_P2_exp_P2 - ln_P1P2_exp_P2) @@ -953,7 +938,6 @@ def hes(ensembles, values[i, j] = value values[j, i] = value - # Save details as required details = {} for i in range(out_matrix_eln): @@ -1496,7 +1480,6 @@ def dres(ensembles, if allow_collapsed_result and not hasattr(dimensionality_reduction_method, '__iter__'): values = values[0] - raise ValueError() return values, details diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index c4e8b7b9afe..872a2143738 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -289,7 +289,7 @@ def n_components(self, n): self._n_components = n def transform(self, atomgroup, n_components=None, start=None, stop=None, - step=None): + step=None, frames=None): """Apply the dimensionality reduction on a trajectory Parameters @@ -335,15 +335,18 @@ def transform(self, atomgroup, n_components=None, start=None, stop=None, warnings.warn('Atom types do not match with types used to fit PCA') traj = atomgroup.universe.trajectory - start, stop, step = traj.check_slice_indices(start, stop, step) - n_frames = len(range(start, stop, step)) + if frames is None: + start, stop, step = traj.check_slice_indices(start, stop, step) + frames = np.arange(start, stop, step) + n_frames = len(frames) dim = (n_components if n_components is not None else self.p_components.shape[1]) dot = np.zeros((n_frames, dim)) - for i, ts in enumerate(traj[start:stop:step]): + for i, fr in enumerate(frames): + traj[fr] xyz = atomgroup.positions.ravel() - self.mean dot[i] = np.dot(xyz, self._p_components[:, :dim]) diff --git a/package/MDAnalysis/analysis/reencore/__init__.py b/package/MDAnalysis/analysis/reencore/__init__.py index 8f9fa134849..b98f9a6453d 100644 --- a/package/MDAnalysis/analysis/reencore/__init__.py +++ b/package/MDAnalysis/analysis/reencore/__init__.py @@ -1 +1,24 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + from .similarity import ces, dres, hes, ces_convergence, dres_convergence diff --git a/package/MDAnalysis/analysis/reencore/similarity.py b/package/MDAnalysis/analysis/reencore/similarity.py index a50681c55e9..6812b9b3f97 100644 --- a/package/MDAnalysis/analysis/reencore/similarity.py +++ b/package/MDAnalysis/analysis/reencore/similarity.py @@ -1,10 +1,36 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + import itertools import numpy as np from scipy.stats import gaussian_kde from ...core.ensemble import Ensemble +from ..clustering import Clusters from ..align import AlignTraj +from ..rms import rmsd +from ..diffusionmap import DistanceMatrix from ..pca import PCA from . import utils @@ -25,13 +51,77 @@ def clusters_to_indices(clusters, outlier_label=-1, outlier_index=-1): i_labels = i_ids[cl_inv] return i_labels, n_cl, n_outliers -def ces(ensemble, clusters, outlier_label=-1, outlier_index=-1): - try: - clusters = clusters.data_labels - except AttributeError: - pass - i_labels, n_cl, n_outliers = clusters_to_indices(clusters, outlier_label) +def prepare_frames(ensemble, frames=None, start=None, stop=None, step=None, + estimate_error=False, n_bootstrap_samples=10,): + if frames is None: + start, stop, step = ensemble.trajectory.check_slice_indices(start, stop, step) + frames = np.arange(start, stop, step) + + frames = np.array([frames]) + + if estimate_error: + edges = ensemble._frame_edges + indices = [np.arange(edges[i], edges[i+1]) for i in range(len(edges)-1)] + + bs = [utils.get_bootstrap_frames(x, n_samples=n_bootstrap_samples) for x in indices] + bs = [np.concatenate(x) for x in zip(*bs)] + frames = np.concatenate([frames, bs]) + + return frames + + +def prepare_ces(ensemble, clusters, start=None, stop=None, step=None, + similarity_matrix=None, metric=rmsd, frames=None, + estimate_error=False, n_bootstrap_samples=10, + **kwargs): + if callable(clusters): + clusters = Clusters(clusters, **kwargs) + + frames = prepare_frames(ensemble, frames=frames, start=start, stop=stop, + step=step, estimate_error=estimate_error, + n_bootstrap_samples=n_bootstrap_samples) + + data = np.zeros((len(frames), frames[0].shape[0])) + + if isinstance(clusters, Clusters): + if similarity_matrix is None: + dist_mat = DistanceMatrix(ensemble, metric=metric, **kwargs).run() + similarity_matrix = -dist_mat.dist_matrix + + + for i, fr in enumerate(frames): + clusters.run(similarity_matrix[frames[0]][:, frames[0]]) + data[i] = clusters.data_labels + + else: + for i, fr in enumerate(frames): + data[i] = clusters[fr] + + return data + + +def ces(ensemble, clusters, select=None, + outlier_label=-1, outlier_index=-1, + estimate_error=False, **kwargs): + if select is not None: + ensemble = ensemble.select_atoms(select) + clusters = prepare_ces(ensemble, clusters, estimate_error=estimate_error, + **kwargs) + + results = np.zeros((len(clusters), ensemble.n_universes, ensemble.n_universes)) + for i, group in enumerate(clusters): + results[i] = _ces(ensemble, group, outlier_label=outlier_label, + outlier_index=outlier_index) + + if estimate_error: + return results.mean(axis=0), results.std(axis=0) + + return results[0] + + +def _ces(ensemble, data, outlier_label=-1, outlier_index=-1): + i_labels, n_cl, n_outliers = clusters_to_indices(data, outlier_label) # count membership in each cluster frames_per_cl = np.zeros((ensemble.n_universes, n_cl + n_outliers), @@ -52,43 +142,91 @@ def ces(ensemble, clusters, outlier_label=-1, outlier_index=-1): return utils.discrete_js_matrix(frames_per_cl) -def dres(ensemble, subspace=PCA, n_components=3, n_resample=1000, - start=None, stop=None, step=None, **kwargs): +def prepare_dres(ensemble, subspace, start=None, stop=None, step=None, + frames=None, + n_components=3, estimate_error=False, + n_bootstrap_samples=10, + similarity_matrix=None, metric=rmsd, **kwargs): if isinstance(subspace, type): - subspace = subspace(ensemble, **kwargs) + try: + # scikit-learn class + subspace = subspace(n_components=n_components, **kwargs) + except TypeError: + # MDAnalysis analysis class + subspace = subspace(ensemble, n_components=n_components, **kwargs) + + frames = prepare_frames(ensemble, frames=frames, start=start, stop=stop, + step=step, estimate_error=estimate_error, + n_bootstrap_samples=n_bootstrap_samples) + + if n_components is None: + _, b, c = frames[0].shape[0] + n_components = b*c + + data = np.zeros((len(frames), frames[0].shape[0], n_components)) + if isinstance(subspace, PCA): - if not subspace._calculated: - subspace.run(start=start, stop=stop, step=step) - subspace = subspace.transform(subspace._atoms, - n_components=n_components, - start=start, stop=stop, - step=step) + for i, fr in enumerate(frames): + subspace.run(frames=fr) + data[i] = subspace.transform(subspace._atoms, + n_components=n_components, + frames=fr) + # in case it's scikit learn + elif hasattr(subspace, "fit_transform"): + coordinates = ensemble.timeseries(frames=frames[0], order="fac") + coordinates = coordinates.reshape((coordinates.shape[0], -1)) + for i, fr in enumerate(frames): + data[i] = subspace.fit_transform(coordinates) + else: + for i, fr in enumerate(frames): + data[i] = subspace[fr] + if n_components is not None: - subspace = subspace[:, :n_components] + data = data[:, :, :n_components] - n_u = ensemble.n_universes - kdes = [gaussian_kde(x.T) for x in ensemble.split_array(subspace)] - resamples = [k.resample(size=n_resample) for k in kdes] - # resamples = np.concatenate([k.resample(size=n_resample) for k in kdes], - # axis=1) # (n_dim, n_u*n_samples) - # pdfs = np.array([k.evaluate(resamples) for k in kdes]) - # pdfs = np.array(np.split(pdfs, n_u, axis=1)) #pdfs.reshape((n_u, n_u, n_resample)) + return data + + +def dres(ensemble, subspace=PCA, select=None, + n_resample=1000, seed=None, estimate_error=False, **kwargs): + if select is not None: + ensemble = ensemble.select_atoms(select) + + subspaces = prepare_dres(ensemble, subspace, + estimate_error=estimate_error, **kwargs) + + results = np.zeros((len(subspaces), ensemble.n_universes, ensemble.n_universes)) + for i, space in enumerate(subspaces): + data = [x.T for x in ensemble.split_array(space)] + results[i] = _dres(data, n_resample=n_resample, seed=seed) + + if estimate_error: + return results.mean(axis=0), results.std(axis=0) + + return results[0] + +def _dres(data, n_resample=1000, seed=None): + n_u = len(data) + kdes = [gaussian_kde(x) for x in data] + resamples = [k.resample(n_resample, seed=seed) for k in kdes] pdfs = np.zeros((n_u, n_u, n_resample)) for i, k in enumerate(kdes): - pdfs[i] = [k.evaluate(x) for x in resamples] - logpdfs = np.log(pdfs).mean(axis=-1) - pdfsT = pdfs.transpose((1, 0, 2)) + for j, x in enumerate(resamples): + pdfs[i, j] = k.evaluate(x) - sum_pdfs = pdfs + pdfsT - ln_pq_exp_pq = np.log(0.5 * (pdfs + pdfsT)).mean(axis=-1) - print(pdfs.shape) - print(logpdfs) + logpdfs = np.log(pdfs).mean(axis=-1) + logmean = np.broadcast_to(np.diag(logpdfs), (n_u, n_u)) + dix = np.diag_indices(n_u) - return 0.5 * (logpdfs + logpdfs.T - ln_pq_exp_pq - ln_pq_exp_pq.T) + sum_pdfs = pdfs + pdfs[dix] + ln_pq_exp_pq = np.log(0.5*sum_pdfs).mean(axis=-1) + return 0.5 * (logmean + logmean.T - ln_pq_exp_pq - ln_pq_exp_pq.T) -def hes(ensemble, weights="mass", estimator="shrinkage", align=False, - estimate_error=False, n_samples=100): +def hes(ensemble, select=None, weights="mass", estimator="shrinkage", + align=False, estimate_error=False, n_bootstrap_samples=10): + if select is not None: + ensemble = ensemble.select_atoms(select) # check given arguments if estimator == "shrinkage": estimator = utils.shrinkage_covariance @@ -118,8 +256,6 @@ def hes(ensemble, weights="mass", estimator="shrinkage", align=False, f"{n_u} universes.") # make weight matrix - # print(np.array(weights).shape) - weights_ = np.repeat(np.array(weights), 3, axis=1) n_f, n_w = weights_.shape weights3 = np.zeros((n_f, n_w, n_w)) @@ -135,9 +271,10 @@ def hes(ensemble, weights="mass", estimator="shrinkage", align=False, for ag, u in zip(ensemble._ags, ensemble.universes)] if estimate_error: - bs = [utils.get_bootstrap_frames(f, n_samples=n_samples) + bs = [utils.get_bootstrap_frames(f, n_samples=n_bootstrap_samples) for f in frames] - frames = zip(bs) + frames = [np.array(list(x)) for x in zip(*bs)] + else: frames = [frames] @@ -149,40 +286,22 @@ def hes(ensemble, weights="mass", estimator="shrinkage", align=False, for i, (coords, w) in enumerate(zip(frames[s], weights3)): avgs[s, i] = coords.mean(axis=0).flatten() cov = estimator(coords.reshape(len(coords), -1)) - # print(cov[:5, :5]) try: cov = np.dot(w, np.dot(cov, w)) except ValueError: raise ValueError("weights dimensions don't match selected atoms") - # print(cov[:5, :5]) covs[s, i] = cov inv_covs = np.zeros((n_s, n_u, n_u, n_a3, n_a3)) for i, sub in enumerate(covs): for j, arr in enumerate(sub): inv_covs[i, j] = np.linalg.pinv(arr[0]) - # if j == 0: - # print(arr[0]) - # print(inv_covs[i, j]) - # print(avgs.shape) diff = avgs - avgs.transpose((0, 2, 1, 3)) - - # print(avgs[0, 0, 0][:10]) - # print(avgs[0, 1, 0][:10]) - # print((avgs[0, 0, 0] - avgs[0, 1, 0])[:10]) - - # print(diff[0][0, 1][:20]) - cov_prod = covs @ inv_covs.transpose((0, 2, 1, 3, 4)) cov_prod += cov_prod.transpose((0, 2, 1, 3, 4)) - # print(cov_prod[0, 0, 1, : 10]) - # trace = np.trace(cov_prod - 2 * np.identity(n_a3), axis1=-1, axis2=-2) trace = np.trace(cov_prod, axis1=-1, axis2=-2) - (2 * n_a3) - # print(trace) - inv_cov_ = inv_covs + inv_covs.transpose((0, 2, 1, 3, 4)) - # print(inv_cov_.dtype) prod = np.einsum('ijklm,ijkl->ijkl', inv_cov_, diff) similarity = np.einsum('ijkl,ijkl->ijk', diff, prod) similarity = 0.25 * (similarity + trace) @@ -192,79 +311,30 @@ def hes(ensemble, weights="mass", estimator="shrinkage", align=False, def gen_window_indices(universe, window_size=10): n_frames = len(universe.trajectory) - frame_ends = np.arange(window_size, n_frames, window_size) + frame_ends = np.arange(window_size, n_frames+1, window_size) frame_ends[-1] = n_frames return [np.arange(i) for i in frame_ends] def convergence(universe, data, func, window_size=10, **kwargs): windows = gen_window_indices(universe, window_size) - data = data[np.ravel(windows)] + data = data[np.concatenate(windows)] n_frames = windows[-1][-1] + 1 traj_frames = [i*n_frames + x for i, x in enumerate(windows)] - ensemble = Ensemble([universe] * len(windows), frames=traj_frames) + ensemble = Ensemble([universe] * len(windows), + frames=np.concatenate(traj_frames)) return func(ensemble, data, **kwargs) def ces_convergence(universe, clusters, window_size=10, **kwargs): - try: - clusters = clusters.cluster_indices - except AttributeError: - pass - + clusters = prepare_ces(universe, clusters, estimate_error=False)[0] return convergence(universe, clusters, ces, window_size=window_size, - **kwargs) - - # i_labels, n_cl, n_outliers = clusters_to_indices(clusters, outlier_label) - - - # ag = universe.select_atoms(select) - # n_frames = len(ag.universe.trajectory) - # frame_ends = np.arange(window_size, n_frames, window_size) - # frame_ends[-1] = n_frames - # n_windows = len(frame_ends) - # frames_per_cl = np.zeros((n_windows, n_cl + n_outliers), - # dtype=float) - - # for i, end in enumerate(frame_ends[::-1], 1): - # row = frames_per_cl[i] - # labels_, counts_ = np.unique(i_labels[:end], return_counts=True) - # if labels_[0] == outlier_index: - # n_outlier_ = counts_[0] - # row[n_cl:n_cl + n_outlier_] = 1 - # labels_ = labels_[1:] - # counts_ = counts_[1:] - - # # normalise over number of frames - # row[labels_] = counts_ - # row /= end - - # return utils.discrete_js_matrix(frames_per_cl)[0][::-1] + **kwargs)[-1] -def dres_convergence(universe, subspace, window_size=10, **kwargs): +def dres_convergence(universe, subspace, window_size=10, seed=None, + **kwargs): + subspace = prepare_dres(universe, subspace, estimate_error=False, + **kwargs)[0] return convergence(universe, subspace, dres, window_size=window_size, - **kwargs) - - - # if n_components is not None: - # subspace = subspace[:, :n_components] - - # n_frames = len(universe.trajectory) - # frame_ends = np.arange(window_size, n_frames, window_size) - # frame_ends[-1] = n_frames - # n_w = len(frame_ends) - - - # kdes = [gaussian_kde(subspace[:end].T) for end in frame_ends[::-1]] - # resamples = np.concatenate([k.resample(size=n_resample) for k in kdes], - # axis=1) # (n_dim, n_u*n_samples) - # pdfs = np.array([k.evaluate(resamples) for k in kdes]) - # pdfs = pdfs.reshape((n_w, n_resample)) - # logpdfs = np.broadcast_to(np.log(pdfs).mean(axis=1), (n_w, n_w)) - - # pdfs_ = np.broadcast_to(pdfs, (n_w, n_w, n_resample)) - # sum_pdfs = pdfs_ + np.transpose(pdfs_, axes=(1, 0, 2)) - # ln_pq_exp_pq = np.log(0.5 * sum_pdfs).mean(axis=-1) - - # return 0.5 * (logpdfs + logpdfs.T - ln_pq_exp_pq - ln_pq_exp_pq.T)[0][::-1] \ No newline at end of file + seed=seed, **kwargs)[-1] diff --git a/package/MDAnalysis/analysis/reencore/utils.py b/package/MDAnalysis/analysis/reencore/utils.py index ed64c4455e0..008b67f25f3 100644 --- a/package/MDAnalysis/analysis/reencore/utils.py +++ b/package/MDAnalysis/analysis/reencore/utils.py @@ -1,3 +1,26 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + import numpy as np import scipy @@ -8,8 +31,6 @@ def discrete_kl_div(a, b): return np.sum(xlogy(a, a / b)) - # return scipy.special.rel_entr(a, b).sum() - def discrete_js_div(a, b): ab = (a+b) * 0.5 @@ -20,75 +41,32 @@ def discrete_js_div(a, b): return 0.5 * (discrete_kl_div(a, ab) + discrete_kl_div(b, ab)) -# def discrete_kl_matrix(matrix): -# log = np.log(matrix) -# cross_entropy = -(matrix @ log.T) -# # print(cross_entropy) -# entropy = -np.einsum('ij,ij->i', matrix, log) -# # print(entropy) -# kl = cross_entropy - entropy[..., None] -# # kl[np.isnan(kl)] = 0 -# return kl - - def discrete_kl_matrix(matrix): - x, y = matrix.shape - matrix = np.array([matrix] * x) - div = matrix / matrix.transpose((1, 0, 2)) - # div[~np.isfinite(div)] = 0 - log = np.log(div) - log[~np.isfinite(div)] = 0 - log[log == -np.inf] = 0 - div[~np.isfinite(div)] = 0 - div[div == -np.inf] = 0 - - # logab = np.broadcast_to(log, (x, x, y)) - # logdiff = logab - logab.transpose((1, 0, 2)) - prod = matrix * log - - # prod[(div < EPS) & (matrix < EPS)] = 0 - # prod[prod < EPS] = 0 - return prod.sum(axis=-1) - - - - - + log = np.log(matrix) + cross_entropy = -(matrix @ log.T) + entropy = -np.einsum('ij,ij->i', matrix, log) + kl = cross_entropy - entropy[..., None] + return kl def discrete_js_matrix(matrix): - value = np.zeros((len(matrix), len(matrix))) - for i, row1 in enumerate(matrix): - for j, row2 in enumerate(matrix[i:], i): - value[i][j] = value[j][i] = discrete_js_div(row1, row2) - return value - # x = matrix.shape[0] - # matrix = np.array([matrix] * x) - # matrixT = matrix.transpose((1, 0, 2)) - # half = (matrix + matrixT) * 0.5 - - # pB = matrix / half - # loghalf = np.log(pB) - # kl = (matrix * loghalf) - # mask = (half < EPS) | (matrix < EPS) | (pB < EPS) - # kl[mask] = 0 - # # kl[half < EPS] = 0 - # klsum = kl.sum(axis=-1) - # print(klsum.shape) - # print(klsum) - - # # kl = discrete_kl_matrix(matrix) - # return 0.5 * (klsum + klsum.T) + x = matrix.shape[0] + matrix = np.array([matrix] * x) + matrixT = matrix.transpose((1, 0, 2)) + half = (matrix + matrixT) * 0.5 + pB = matrix / half + loghalf = np.log(pB) + kl = (matrix * loghalf) + mask = (half < EPS) | (matrix < EPS) | (pB < EPS) + kl[mask] = 0 + klsum = kl.sum(axis=-1) + return 0.5 * (klsum + klsum.T) def max_likelihood_covariance(coordinates, center=True): if center: coordinates -= coordinates.mean(axis=0) - _, y = coordinates.shape - cov = np.zeros((y, y)) - for frame in coordinates: - cov += np.outer(frame, frame) - # np.einsum('ij,ik->jk', coordinates, coordinates, out=cov) + cov = np.einsum('ij,ik->jk', coordinates, coordinates) cov /= coordinates.shape[0] return cov @@ -128,14 +106,13 @@ def shrinkage_covariance(coordinates, shrinkage_parameter=None): r = rdiag + roff k = (p - r) / c shrinkage_parameter = max(0, min(1, k * inv_t)) - - # print("shrinkage_parameter", shrinkage_parameter) - + cov = shrinkage_parameter * prior + (1 - shrinkage_parameter) * sample return cov def get_bootstrap_frames(frames, n_samples=100): n = len(frames) - indices = [np.random.randit(0, high=n, size=n) for i in range(n_samples)] - return [frames[x] for x in indices] \ No newline at end of file + indices = [np.random.randint(0, high=n, size=n) for i in range(n_samples)] + arr = np.array([frames[x] for x in indices]) + return arr \ No newline at end of file diff --git a/package/MDAnalysis/core/ensemble.py b/package/MDAnalysis/core/ensemble.py index 0c5e10de377..08cb6eb687a 100644 --- a/package/MDAnalysis/core/ensemble.py +++ b/package/MDAnalysis/core/ensemble.py @@ -30,6 +30,10 @@ class AtomsWrapper: + """ + Wrap AtomGroup to access properties such as positions + when they update across Universe trajectories. + """ def __init__(self, ensemble): self.ensemble = ensemble self.universe = ensemble @@ -57,13 +61,19 @@ def positions(self, value): class Ensemble(object): """ + Wrap multiple Universes so they can be treated as one for the purposes + of running an analysis. + Should be able to plug this into any AnalysisBase class. """ def __init__(self, universes, select=None, labels=None, frames=None, _ts_u=0, links=None): + # Point universe attributes to self self.ensemble = self self.universe = self + self.trajectory = self + # set universe info universes = util.asiterable(universes) self.atoms = AtomsWrapper(self) @@ -87,7 +97,6 @@ def __init__(self, universes, select=None, labels=None, frames=None, err = ('select must be None, a string, or an iterable of strings ' 'with the same length as universes') raise ValueError(err) - self._ag_indices = [ag.ix for ag in self._ags] self._ags = tuple(self._ags) self.n_atoms = len(self._ags[0]) @@ -120,10 +129,11 @@ def __init__(self, universes, select=None, labels=None, frames=None, self.labels.append(l) self._universe_labels[l] = i - # pretend to be Universe - self.trajectory = self + # new Ensembles created from select_atoms + # need to have their frames / current Universe + # updated when this one is. + # TODO: could definitely do this better self._ts_u = _ts_u - if links is None: links = set() self.links = links @@ -137,12 +147,15 @@ def __hash__(self): def __getattr__(self, attr): try: - return getattr(self.atoms, attr) + return getattr(self._atoms, attr) except AttributeError: try: - return getattr(self.universe, attr) + return getattr(self._universe, attr) except AttributeError: - pass + try: + return getattr(self.ts, attr) + except AttributeError: + pass raise AttributeError(f"{type(self).__name__} does not have " f"attribute {attr}") @@ -186,7 +199,7 @@ def frame(self): try: return u_ix[np.in1d(u_ix, fr_ix)][0] except IndexError: - return None + return self.ts.frame @property def positions(self): @@ -205,6 +218,7 @@ def __iter__(self): def timeseries(self, atomgroup=None, start=None, stop=None, step=None, frames=None, order="afc"): + # TODO: what to do about atomgroup? n_atoms = self.n_atoms frames = np.array(self._prepare_frames(start=start, stop=stop, step=step, frames=frames)) @@ -228,7 +242,7 @@ def timeseries(self, atomgroup=None, start=None, stop=None, step=None, @property def filename(self): - return self.universe.trajectory.filename + return self._universe.trajectory.filename @property def _atoms(self): @@ -280,10 +294,6 @@ def _prepare_frames_by_universe(self, start=None, stop=None, step=None, splix = np.where(np.ediff1d(u_frames[:, 0]))[0] + 1 return np.split(u_frames[:, 1], splix) - # bins = np.searchsorted(self._frame_edges[1:], frames) - # for i in range(self.n_universes): - # yield frames[bins == i] - def iterate_over_atomgroups(self, start=None, stop=None, step=None, frames=None): frames = self._prepare_frames(start=start, stop=stop, step=step, @@ -302,7 +312,6 @@ def iterate_over_universes(self, start=None, stop=None, step=None, yield type(self)([ag], labels=[label], frames=fr, _ts_u=self._ts_u, links=self.links) - def _get_relative_frame(self, i): if not isinstance(i, (int, np.integer)): raise IndexError('only integers are valid indices. ' @@ -323,23 +332,26 @@ def _slice_universe(self, sl): _ts_u=self._ts_u, links=self.links) def select_atoms(self, *sel): + """Return new Ensemble with Atoms selected""" sel = [s for s in sel if s is not None] ags = self._ags if sel: ags = [ag.select_atoms(*sel) for ag in ags] return type(self)(ags, select=None, labels=self.labels, - _ts_u=self._ts_u, links=self.links) + _ts_u=self._ts_u, links=self.links, frames=self.frames) def split_array(self, arr): + """Convenience function to split arrays of data by Universe + into an iterable of data arrays""" for i in range(self.n_universes): - yield arr[self._frame_edges[i]:self._frame_edges[i+1]] + ix = np.where(self._universe_frames[:, 0] == i)[0] + yield arr[ix[0]:ix[-1]+1] def transfer_to_memory(self, start=None, stop=None, step=None, frames=None): - - frames_ = self._prepare_frames_by_universe(start=start, stop=stop, - step=step, frames=frames) - frames = np.array(list(frames_)) + """Transfer specified frames to memory""" + frames = self._prepare_frames_by_universe(start=start, stop=stop, + step=step, frames=frames) for u, fr in zip(self.universes, frames): u.transfer_to_memory(frames=fr) self._set_frames() \ No newline at end of file diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index 1ad49910ba7..be2ab1f1db5 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -584,49 +584,51 @@ def transfer_to_memory(self, start=None, stop=None, step=None, """ from ..coordinates.memory import MemoryReader - if isinstance(self.trajectory, MemoryReader): - return - - if frames is None: - sss = self.trajectory.check_slice_indices(start, stop, step) - frames = range(*sss) - - n_frames = len(frames) - n_atoms = len(self.atoms) - coordinates = np.zeros((n_frames, n_atoms, 3), dtype=np.float32) - ts = self.trajectory.ts - has_vels = ts.has_velocities - has_fors = ts.has_forces - has_dims = ts.dimensions is not None - - velocities = np.zeros_like(coordinates) if has_vels else None - forces = np.zeros_like(coordinates) if has_fors else None - dimensions = (np.zeros((n_frames, 6), dtype=np.float32) - if has_dims else None) - - for i, frame in enumerate(ProgressBar(frames, verbose=verbose, - desc="Loading frames")): - ts = self.trajectory[frame] - np.copyto(coordinates[i], ts.positions) - if has_vels: - np.copyto(velocities[i], ts.velocities) - if has_fors: - np.copyto(forces[i], ts.forces) - if has_dims: - np.copyto(dimensions[i], ts.dimensions) - - # Overwrite trajectory in universe with an MemoryReader - # object, to provide fast access and allow coordinates - # to be manipulated - step = frames[1] - frames[0] - self.trajectory = MemoryReader( - coordinates, - dimensions=dimensions, - dt=self.trajectory.ts.dt * step, - filename=self.trajectory.filename, - velocities=velocities, - forces=forces, - ) + if not isinstance(self.trajectory, MemoryReader): + if frames is None: + sss = self.trajectory.check_slice_indices(start, stop, step) + frames = range(*sss) + + n_frames = len(frames) + n_atoms = len(self.atoms) + coordinates = np.zeros((n_frames, n_atoms, 3), dtype=np.float32) + ts = self.trajectory.ts + has_vels = ts.has_velocities + has_fors = ts.has_forces + has_dims = ts.dimensions is not None + + velocities = np.zeros_like(coordinates) if has_vels else None + forces = np.zeros_like(coordinates) if has_fors else None + dimensions = (np.zeros((n_frames, 6), dtype=np.float32) + if has_dims else None) + + for i, frame in enumerate(ProgressBar(frames, verbose=verbose, + desc="Loading frames")): + ts = self.trajectory[frame] + np.copyto(coordinates[i], ts.positions) + if has_vels: + np.copyto(velocities[i], ts.velocities) + if has_fors: + np.copyto(forces[i], ts.forces) + if has_dims: + np.copyto(dimensions[i], ts.dimensions) + + # Overwrite trajectory in universe with an MemoryReader + # object, to provide fast access and allow coordinates + # to be manipulated + try: + step = frames[1] - frames[0] + except IndexError: + step = 1 + + self.trajectory = MemoryReader( + coordinates, + dimensions=dimensions, + dt=self.trajectory.ts.dt * step, + filename=self.trajectory.filename, + velocities=velocities, + forces=forces, + ) # python 2 doesn't allow an efficient splitting of kwargs in function # argument signatures. diff --git a/package/setup.py b/package/setup.py index 4a1769dcbb9..0314aa0d70b 100755 --- a/package/setup.py +++ b/package/setup.py @@ -413,10 +413,8 @@ def extensions(config): sources=['MDAnalysis/analysis/clustering/cluster_methods' + source_suffix, 'MDAnalysis/analysis/encore/clustering/src/ap.c'], include_dirs=include_dirs+['MDAnalysis/analysis/encore/clustering/include'], - # language='c++', define_macros=define_macros, extra_compile_args=extra_compile_args,) - # extra_link_args= cpp_extra_link_args) spe_dimred = MDAExtension('MDAnalysis.analysis.encore.dimensionality_reduction.stochasticproxembed', sources=['MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed' + source_suffix, 'MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c'], @@ -454,7 +452,7 @@ def extensions(config): #Let's check early for missing .c files extensions = pre_exts for ext in extensions: - for source in ext.sources: + for source in ext.sources: if not (os.path.isfile(source) and os.access(source, os.R_OK)): raise IOError("Source file '{}' not found. This might be " diff --git a/testsuite/MDAnalysisTests/analysis/test_encore.py b/testsuite/MDAnalysisTests/analysis/test_encore.py index 4f22eec8098..d7d8f9f5654 100644 --- a/testsuite/MDAnalysisTests/analysis/test_encore.py +++ b/testsuite/MDAnalysisTests/analysis/test_encore.py @@ -1,25 +1,25 @@ -# # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 -# # -# # MDAnalysis --- https://www.mdanalysis.org -# # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors -# # (see the file AUTHORS for the full list of names) -# # -# # Released under the GNU Public Licence, v2 or any higher version -# # -# # Please cite your use of MDAnalysis in published work: -# # -# # R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, -# # D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. -# # MDAnalysis: A Python package for the rapid analysis of molecular dynamics -# # simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th -# # Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. -# # doi: 10.25080/majora-629e541a-00e -# # -# # N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. -# # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. -# # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 -# # +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# import MDAnalysis as mda import MDAnalysis.analysis.encore as encore @@ -41,8 +41,8 @@ import MDAnalysis.analysis.encore.confdistmatrix as confdistmatrix -# def function(x): -# return x**2 +def function(x): + return x**2 class TestEncore(object): @pytest.fixture(scope='class') @@ -71,209 +71,208 @@ def ens2(self, ens2_template): ens2_template.trajectory.timeseries(order='fac'), format=mda.coordinates.memory.MemoryReader) -# def test_triangular_matrix(self): -# scalar = 2 -# size = 3 -# expected_value = 1.984 -# filename = tempfile.mktemp()+".npz" - -# triangular_matrix = encore.utils.TriangularMatrix(size = size) - -# triangular_matrix[0,1] = expected_value - -# assert_equal(triangular_matrix[0,1], expected_value, -# err_msg="Data error in TriangularMatrix: read/write are not consistent") - -# assert_equal(triangular_matrix[0,1], triangular_matrix[1,0], -# err_msg="Data error in TriangularMatrix: matrix non symmetrical") -# triangular_matrix.savez(filename) - -# triangular_matrix_2 = encore.utils.TriangularMatrix(size = size, loadfile = filename) -# assert_equal(triangular_matrix_2[0,1], expected_value, -# err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") - -# triangular_matrix_3 = encore.utils.TriangularMatrix(size = size) -# triangular_matrix_3.loadz(filename) -# assert_equal(triangular_matrix_3[0,1], expected_value, -# err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") - -# incremented_triangular_matrix = triangular_matrix + scalar -# assert_equal(incremented_triangular_matrix[0,1], expected_value + scalar, -# err_msg="Error in TriangularMatrix: addition of scalar gave" -# "inconsistent results") - -# triangular_matrix += scalar -# assert_equal(triangular_matrix[0,1], expected_value + scalar, -# err_msg="Error in TriangularMatrix: addition of scalar gave" -# "inconsistent results") - -# multiplied_triangular_matrix_2 = triangular_matrix_2 * scalar -# assert_equal(multiplied_triangular_matrix_2[0,1], expected_value * scalar, -# err_msg="Error in TriangularMatrix: multiplication by scalar gave" -# "inconsistent results") - -# triangular_matrix_2 *= scalar -# assert_equal(triangular_matrix_2[0,1], expected_value * scalar, -# err_msg="Error in TriangularMatrix: multiplication by scalar gave\ -# inconsistent results") - -# @pytest.mark.xfail(os.name == 'nt', -# reason="Not yet supported on Windows.") -# def test_parallel_calculation(self): - -# arguments = [tuple([i]) for i in np.arange(0,100)] - -# parallel_calculation = encore.utils.ParallelCalculation(function=function, -# n_jobs=4, -# args=arguments) -# results = parallel_calculation.run() - -# for i,r in enumerate(results): -# assert_equal(r[1], arguments[i][0]**2, -# err_msg="Unexpeted results from ParallelCalculation") - -# def test_rmsd_matrix_with_superimposition(self, ens1): -# conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( -# ens1, -# encore.confdistmatrix.set_rmsd_matrix_elements, -# select="name CA", -# pairwise_align=True, -# weights='mass', -# n_jobs=1) - -# reference = rms.RMSD(ens1, select="name CA") -# reference.run() - -# for i,rmsd in enumerate(reference.rmsd): -# assert_almost_equal(conf_dist_matrix[0,i], rmsd[2], decimal=3, -# err_msg = "calculated RMSD values differ from the reference implementation") - -# def test_rmsd_matrix_with_superimposition_custom_weights(self, ens1): -# conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( -# ens1, -# encore.confdistmatrix.set_rmsd_matrix_elements, -# select="name CA", -# pairwise_align=True, -# weights='mass', -# n_jobs=1) - -# conf_dist_matrix_custom = encore.confdistmatrix.conformational_distance_matrix( -# ens1, -# encore.confdistmatrix.set_rmsd_matrix_elements, -# select="name CA", -# pairwise_align=True, -# weights=(ens1.select_atoms('name CA').masses, ens1.select_atoms('name CA').masses), -# n_jobs=1) - -# for i in range(conf_dist_matrix_custom.size): -# assert_almost_equal(conf_dist_matrix_custom[0, i], conf_dist_matrix[0, i]) - -# def test_rmsd_matrix_without_superimposition(self, ens1): -# selection_string = "name CA" -# selection = ens1.select_atoms(selection_string) -# reference_rmsd = [] -# coordinates = ens1.trajectory.timeseries(selection, order='fac') -# for coord in coordinates: -# reference_rmsd.append(rms.rmsd(coordinates[0], coord, superposition=False)) - -# confdist_matrix = encore.confdistmatrix.conformational_distance_matrix( -# ens1, -# encore.confdistmatrix.set_rmsd_matrix_elements, -# select=selection_string, -# pairwise_align=False, -# weights='mass', -# n_jobs=1) - -# print (repr(confdist_matrix.as_array()[0,:])) -# assert_almost_equal(confdist_matrix.as_array()[0,:], reference_rmsd, decimal=3, -# err_msg="calculated RMSD values differ from reference") - -# def test_ensemble_superimposition(self): -# aligned_ensemble1 = mda.Universe(PSF, DCD) -# align.AlignTraj(aligned_ensemble1, aligned_ensemble1, -# select="name CA", -# in_memory=True).run() -# aligned_ensemble2 = mda.Universe(PSF, DCD) -# align.AlignTraj(aligned_ensemble2, aligned_ensemble2, -# select="name *", -# in_memory=True).run() - -# rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) -# rmsfs1.run() - -# rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) -# rmsfs2.run() - -# assert sum(rmsfs1.rmsf) > sum(rmsfs2.rmsf),"Ensemble aligned on all " \ -# "atoms should have lower full-atom RMSF than ensemble aligned on only CAs." - -# def test_ensemble_superimposition_to_reference_non_weighted(self): -# aligned_ensemble1 = mda.Universe(PSF, DCD) -# align.AlignTraj(aligned_ensemble1, aligned_ensemble1, -# select="name CA", -# in_memory=True).run() -# aligned_ensemble2 = mda.Universe(PSF, DCD) -# align.AlignTraj(aligned_ensemble2, aligned_ensemble2, -# select="name *", -# in_memory=True).run() - -# rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) -# rmsfs1.run() - -# rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) -# rmsfs2.run() - -# assert sum(rmsfs1.rmsf) > sum(rmsfs2.rmsf), "Ensemble aligned on all " \ -# "atoms should have lower full-atom RMSF than ensemble aligned on only CAs." - - # def test_hes_to_self(self, ens1): - # results, details = encore.hes([ens1, ens1]) - # result_value = results[0, 1] - # expected_value = 0. - # raise ValueError() - # assert_almost_equal(result_value, expected_value, - # err_msg="Harmonic Ensemble Similarity to itself not zero: {0:f}".format(result_value)) - -# def test_hes(self, ens1, ens2): -# results, details = encore.hes([ens1, ens2], weights='mass') -# result_value = results[0, 1] -# min_bound = 1E5 -# assert result_value > min_bound, "Unexpected value for Harmonic " \ -# "Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, min_bound) - -# def test_hes_custom_weights(self, ens1, ens2): -# results, details = encore.hes([ens1, ens2], weights='mass') -# results_custom, details_custom = encore.hes([ens1, ens2], -# weights=(ens1.select_atoms('name CA').masses, -# ens2.select_atoms('name CA').masses)) -# result_value = results[0, 1] -# result_value_custom = results_custom[0, 1] -# assert_almost_equal(result_value, result_value_custom) - - # def test_hes_align(self, ens1, ens2): - # # This test is massively sensitive! - # # Get 5260 when masses were float32? - # results, details = encore.hes([ens1, ens2], align=True) - # result_value = results[0,1] - # expected_value = 2047.05 - # assert_almost_equal(result_value, expected_value, decimal=-3, - # err_msg="Unexpected value for Harmonic Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) - - # def test_ces_to_self(self, ens1): - # results, details = \ - # encore.ces([ens1, ens1], - # clustering_method=encore.AffinityPropagationNative(preference = -3.0)) - # result_value = results[0,1] - # expected_value = 0. - # assert_almost_equal(result_value, expected_value, - # err_msg="ClusteringEnsemble Similarity to itself not zero: {0:f}".format(result_value)) - - # def test_ces(self, ens1, ens2): - # results, details = encore.ces([ens1, ens2]) - # result_value = results[0,1] - # expected_value = 0.51 - # assert_almost_equal(result_value, expected_value, decimal=2, - # err_msg="Unexpected value for Cluster Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + def test_triangular_matrix(self): + scalar = 2 + size = 3 + expected_value = 1.984 + filename = tempfile.mktemp()+".npz" + + triangular_matrix = encore.utils.TriangularMatrix(size = size) + + triangular_matrix[0,1] = expected_value + + assert_equal(triangular_matrix[0,1], expected_value, + err_msg="Data error in TriangularMatrix: read/write are not consistent") + + assert_equal(triangular_matrix[0,1], triangular_matrix[1,0], + err_msg="Data error in TriangularMatrix: matrix non symmetrical") + triangular_matrix.savez(filename) + + triangular_matrix_2 = encore.utils.TriangularMatrix(size = size, loadfile = filename) + assert_equal(triangular_matrix_2[0,1], expected_value, + err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") + + triangular_matrix_3 = encore.utils.TriangularMatrix(size = size) + triangular_matrix_3.loadz(filename) + assert_equal(triangular_matrix_3[0,1], expected_value, + err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") + + incremented_triangular_matrix = triangular_matrix + scalar + assert_equal(incremented_triangular_matrix[0,1], expected_value + scalar, + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results") + + triangular_matrix += scalar + assert_equal(triangular_matrix[0,1], expected_value + scalar, + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results") + + multiplied_triangular_matrix_2 = triangular_matrix_2 * scalar + assert_equal(multiplied_triangular_matrix_2[0,1], expected_value * scalar, + err_msg="Error in TriangularMatrix: multiplication by scalar gave" + "inconsistent results") + + triangular_matrix_2 *= scalar + assert_equal(triangular_matrix_2[0,1], expected_value * scalar, + err_msg="Error in TriangularMatrix: multiplication by scalar gave\ +inconsistent results") + + @pytest.mark.xfail(os.name == 'nt', + reason="Not yet supported on Windows.") + def test_parallel_calculation(self): + + arguments = [tuple([i]) for i in np.arange(0,100)] + + parallel_calculation = encore.utils.ParallelCalculation(function=function, + n_jobs=4, + args=arguments) + results = parallel_calculation.run() + + for i,r in enumerate(results): + assert_equal(r[1], arguments[i][0]**2, + err_msg="Unexpeted results from ParallelCalculation") + + def test_rmsd_matrix_with_superimposition(self, ens1): + conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights='mass', + n_jobs=1) + + reference = rms.RMSD(ens1, select="name CA") + reference.run() + + for i,rmsd in enumerate(reference.rmsd): + assert_almost_equal(conf_dist_matrix[0,i], rmsd[2], decimal=3, + err_msg = "calculated RMSD values differ from the reference implementation") + + def test_rmsd_matrix_with_superimposition_custom_weights(self, ens1): + conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights='mass', + n_jobs=1) + + conf_dist_matrix_custom = encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights=(ens1.select_atoms('name CA').masses, ens1.select_atoms('name CA').masses), + n_jobs=1) + + for i in range(conf_dist_matrix_custom.size): + assert_almost_equal(conf_dist_matrix_custom[0, i], conf_dist_matrix[0, i]) + + def test_rmsd_matrix_without_superimposition(self, ens1): + selection_string = "name CA" + selection = ens1.select_atoms(selection_string) + reference_rmsd = [] + coordinates = ens1.trajectory.timeseries(selection, order='fac') + for coord in coordinates: + reference_rmsd.append(rms.rmsd(coordinates[0], coord, superposition=False)) + + confdist_matrix = encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select=selection_string, + pairwise_align=False, + weights='mass', + n_jobs=1) + + print (repr(confdist_matrix.as_array()[0,:])) + assert_almost_equal(confdist_matrix.as_array()[0,:], reference_rmsd, decimal=3, + err_msg="calculated RMSD values differ from reference") + + def test_ensemble_superimposition(self): + aligned_ensemble1 = mda.Universe(PSF, DCD) + align.AlignTraj(aligned_ensemble1, aligned_ensemble1, + select="name CA", + in_memory=True).run() + aligned_ensemble2 = mda.Universe(PSF, DCD) + align.AlignTraj(aligned_ensemble2, aligned_ensemble2, + select="name *", + in_memory=True).run() + + rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) + rmsfs1.run() + + rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) + rmsfs2.run() + + assert sum(rmsfs1.rmsf) > sum(rmsfs2.rmsf),"Ensemble aligned on all " \ + "atoms should have lower full-atom RMSF than ensemble aligned on only CAs." + + def test_ensemble_superimposition_to_reference_non_weighted(self): + aligned_ensemble1 = mda.Universe(PSF, DCD) + align.AlignTraj(aligned_ensemble1, aligned_ensemble1, + select="name CA", + in_memory=True).run() + aligned_ensemble2 = mda.Universe(PSF, DCD) + align.AlignTraj(aligned_ensemble2, aligned_ensemble2, + select="name *", + in_memory=True).run() + + rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) + rmsfs1.run() + + rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) + rmsfs2.run() + + assert sum(rmsfs1.rmsf) > sum(rmsfs2.rmsf), "Ensemble aligned on all " \ + "atoms should have lower full-atom RMSF than ensemble aligned on only CAs." + + def test_hes_to_self(self, ens1): + results, details = encore.hes([ens1, ens1]) + result_value = results[0, 1] + expected_value = 0. + assert_almost_equal(result_value, expected_value, + err_msg="Harmonic Ensemble Similarity to itself not zero: {0:f}".format(result_value)) + + def test_hes(self, ens1, ens2): + results, details = encore.hes([ens1, ens2], weights='mass') + result_value = results[0, 1] + min_bound = 1E5 + assert result_value > min_bound, "Unexpected value for Harmonic " \ + "Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, min_bound) + + def test_hes_custom_weights(self, ens1, ens2): + results, details = encore.hes([ens1, ens2], weights='mass') + results_custom, details_custom = encore.hes([ens1, ens2], + weights=(ens1.select_atoms('name CA').masses, + ens2.select_atoms('name CA').masses)) + result_value = results[0, 1] + result_value_custom = results_custom[0, 1] + assert_almost_equal(result_value, result_value_custom) + + def test_hes_align(self, ens1, ens2): + # This test is massively sensitive! + # Get 5260 when masses were float32? + results, details = encore.hes([ens1, ens2], align=True) + result_value = results[0,1] + expected_value = 2047.05 + assert_almost_equal(result_value, expected_value, decimal=-3, + err_msg="Unexpected value for Harmonic Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + + def test_ces_to_self(self, ens1): + results, details = \ + encore.ces([ens1, ens1], + clustering_method=encore.AffinityPropagationNative(preference = -3.0)) + result_value = results[0,1] + expected_value = 0. + assert_almost_equal(result_value, expected_value, + err_msg="ClusteringEnsemble Similarity to itself not zero: {0:f}".format(result_value)) + + def test_ces(self, ens1, ens2): + results, details = encore.ces([ens1, ens2]) + result_value = results[0,1] + expected_value = 0.51 + assert_almost_equal(result_value, expected_value, decimal=2, + err_msg="Unexpected value for Cluster Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) def test_dres_to_self(self, ens1): results, details = encore.dres([ens1, ens1]) @@ -282,516 +281,516 @@ def test_dres_to_self(self, ens1): assert_almost_equal(result_value, expected_value, decimal=2, err_msg="Dim. Reduction Ensemble Similarity to itself not zero: {0:f}".format(result_value)) -# def test_dres(self, ens1, ens2): -# results, details = encore.dres([ens1, ens2], select="name CA and resnum 1-10") -# result_value = results[0,1] -# upper_bound = 0.6 -# assert result_value < upper_bound, "Unexpected value for Dim. " \ -# "reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, upper_bound) - -# @pytest.mark.xfail # sporadically fails, see Issue #2158 -# def test_dres_without_superimposition(self, ens1, ens2): -# distance_matrix = encore.get_distance_matrix( -# encore.merge_universes([ens1, ens2]), -# superimpose=False) -# results, details = encore.dres([ens1, ens2], -# distance_matrix = distance_matrix) -# result_value = results[0,1] -# expected_value = 0.68 -# assert_almost_equal(result_value, expected_value, decimal=1, -# err_msg="Unexpected value for Dim. reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) - -# def test_ces_convergence(self, ens1): -# expected_values = [0.3443593, 0.1941854, 0.06857104, 0.] -# results = encore.ces_convergence(ens1, 5) -# for i,ev in enumerate(expected_values): -# assert_almost_equal(ev, results[i], decimal=2, -# err_msg="Unexpected value for Clustering Ensemble similarity in convergence estimation") - -# def test_dres_convergence(self, ens1): -# # Due to encore.dres_convergence() involving random numbers, the -# # following assertion is allowed to fail once. This significantly -# # reduces the probability of a random test failure. -# expected_values = [0.3, 0.] -# results = encore.dres_convergence(ens1, 10) -# try: -# assert_almost_equal(results[:,0], expected_values, decimal=1) -# except AssertionError: -# # Random test failure is very rare, but repeating the failed test -# # just once would only assert that the test passes with 50% -# # probability. To be a little safer, we raise a warning and repeat -# # the test 10 times: -# warnings.warn(message="Test 'test_dres_convergence' failed, " -# "repeating test 10 times.", -# category=RuntimeWarning) -# for i in range(10): -# results = encore.dres_convergence(ens1, 10) -# assert_almost_equal(results[:,0], expected_values, decimal=1, -# err_msg="Unexpected value for Dim. " -# "reduction Ensemble similarity in " -# "convergence estimation") - -# @pytest.mark.xfail # sporadically fails, see Issue #2158 -# def test_hes_error_estimation(self, ens1): -# expected_average = 10 -# expected_stdev = 12 -# averages, stdevs = encore.hes([ens1, ens1], estimate_error = True, bootstrapping_samples=10, select="name CA and resnum 1-10") -# average = averages[0,1] -# stdev = stdevs[0,1] - -# assert_almost_equal(average, expected_average, decimal=-2, -# err_msg="Unexpected average value for bootstrapped samples in Harmonic Ensemble imilarity") -# assert_almost_equal(stdev, expected_stdev, decimal=-2, -# err_msg="Unexpected standard daviation for bootstrapped samples in Harmonic Ensemble imilarity") - -# def test_ces_error_estimation(self, ens1): -# expected_average = 0.03 -# expected_stdev = 0.31 -# averages, stdevs = encore.ces([ens1, ens1], -# estimate_error = True, -# bootstrapping_samples=10, -# clustering_method=encore.AffinityPropagationNative(preference=-2.0), -# select="name CA and resnum 1-10") -# average = averages[0,1] -# stdev = stdevs[0,1] - -# assert_almost_equal(average, expected_average, decimal=1, -# err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity") -# assert_almost_equal(stdev, expected_stdev, decimal=0, -# err_msg="Unexpected standard daviation for bootstrapped samples in Clustering Ensemble similarity") - -# def test_ces_error_estimation_ensemble_bootstrap(self, ens1): -# # Error estimation using a method that does not take a distance -# # matrix as input, and therefore relies on bootstrapping the ensembles -# # instead - -# pytest.importorskip('sklearn') - -# expected_average = 0.03 -# expected_stdev = 0.02 -# averages, stdevs = encore.ces([ens1, ens1], -# estimate_error = True, -# bootstrapping_samples=10, -# clustering_method=encore.KMeans(n_clusters=2), -# select="name CA and resnum 1-10") -# average = averages[0,1] -# stdev = stdevs[0,1] - -# assert_almost_equal(average, expected_average, decimal=1, -# err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity") -# assert_almost_equal(stdev, expected_stdev, decimal=1, -# err_msg="Unexpected standard daviation for bootstrapped samples in Clustering Ensemble similarity") - -# def test_dres_error_estimation(self, ens1): -# average_upper_bound = 0.3 -# stdev_upper_bound = 0.2 -# averages, stdevs = encore.dres([ens1, ens1], estimate_error = True, -# bootstrapping_samples=10, -# select="name CA and resnum 1-10") -# average = averages[0,1] -# stdev = stdevs[0,1] - -# assert average < average_upper_bound, "Unexpected average value for " \ -# "bootstrapped samples in Dim. reduction Ensemble similarity" -# assert stdev < stdev_upper_bound, "Unexpected standard deviation for" \ -# " bootstrapped samples in Dim. reduction Ensemble imilarity" - -# class TestEncoreClustering(object): -# @pytest.fixture(scope='class') -# def ens1_template(self): -# template = mda.Universe(PSF, DCD) -# template.transfer_to_memory(step=5) -# return template - -# @pytest.fixture(scope='class') -# def ens2_template(self): -# template = mda.Universe(PSF, DCD2) -# template.transfer_to_memory(step=5) -# return template - -# @pytest.fixture(scope='class') -# def cc(self): -# return encore.ClusterCollection([1, 1, 1, 3, 3, 5, 5, 5]) - -# @pytest.fixture(scope='class') -# def cluster(self): -# return encore.Cluster(elem_list=np.array([0, 1, 2]), centroid=1) - -# @pytest.fixture() -# def ens1(self, ens1_template): -# return mda.Universe( -# ens1_template.filename, -# ens1_template.trajectory.timeseries(order='fac'), -# format=mda.coordinates.memory.MemoryReader) - -# @pytest.fixture() -# def ens2(self, ens2_template): -# return mda.Universe( -# ens2_template.filename, -# ens2_template.trajectory.timeseries(order='fac'), -# format=mda.coordinates.memory.MemoryReader) + def test_dres(self, ens1, ens2): + results, details = encore.dres([ens1, ens2], select="name CA and resnum 1-10") + result_value = results[0,1] + upper_bound = 0.6 + assert result_value < upper_bound, "Unexpected value for Dim. " \ + "reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, upper_bound) + + @pytest.mark.xfail # sporadically fails, see Issue #2158 + def test_dres_without_superimposition(self, ens1, ens2): + distance_matrix = encore.get_distance_matrix( + encore.merge_universes([ens1, ens2]), + superimpose=False) + results, details = encore.dres([ens1, ens2], + distance_matrix = distance_matrix) + result_value = results[0,1] + expected_value = 0.68 + assert_almost_equal(result_value, expected_value, decimal=1, + err_msg="Unexpected value for Dim. reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + + def test_ces_convergence(self, ens1): + expected_values = [0.3443593, 0.1941854, 0.06857104, 0.] + results = encore.ces_convergence(ens1, 5) + for i,ev in enumerate(expected_values): + assert_almost_equal(ev, results[i], decimal=2, + err_msg="Unexpected value for Clustering Ensemble similarity in convergence estimation") + + def test_dres_convergence(self, ens1): + # Due to encore.dres_convergence() involving random numbers, the + # following assertion is allowed to fail once. This significantly + # reduces the probability of a random test failure. + expected_values = [0.3, 0.] + results = encore.dres_convergence(ens1, 10) + try: + assert_almost_equal(results[:,0], expected_values, decimal=1) + except AssertionError: + # Random test failure is very rare, but repeating the failed test + # just once would only assert that the test passes with 50% + # probability. To be a little safer, we raise a warning and repeat + # the test 10 times: + warnings.warn(message="Test 'test_dres_convergence' failed, " + "repeating test 10 times.", + category=RuntimeWarning) + for i in range(10): + results = encore.dres_convergence(ens1, 10) + assert_almost_equal(results[:,0], expected_values, decimal=1, + err_msg="Unexpected value for Dim. " + "reduction Ensemble similarity in " + "convergence estimation") + + @pytest.mark.xfail # sporadically fails, see Issue #2158 + def test_hes_error_estimation(self, ens1): + expected_average = 10 + expected_stdev = 12 + averages, stdevs = encore.hes([ens1, ens1], estimate_error = True, bootstrapping_samples=10, select="name CA and resnum 1-10") + average = averages[0,1] + stdev = stdevs[0,1] + + assert_almost_equal(average, expected_average, decimal=-2, + err_msg="Unexpected average value for bootstrapped samples in Harmonic Ensemble imilarity") + assert_almost_equal(stdev, expected_stdev, decimal=-2, + err_msg="Unexpected standard daviation for bootstrapped samples in Harmonic Ensemble imilarity") + + def test_ces_error_estimation(self, ens1): + expected_average = 0.03 + expected_stdev = 0.31 + averages, stdevs = encore.ces([ens1, ens1], + estimate_error = True, + bootstrapping_samples=10, + clustering_method=encore.AffinityPropagationNative(preference=-2.0), + select="name CA and resnum 1-10") + average = averages[0,1] + stdev = stdevs[0,1] + + assert_almost_equal(average, expected_average, decimal=1, + err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity") + assert_almost_equal(stdev, expected_stdev, decimal=0, + err_msg="Unexpected standard daviation for bootstrapped samples in Clustering Ensemble similarity") + + def test_ces_error_estimation_ensemble_bootstrap(self, ens1): + # Error estimation using a method that does not take a distance + # matrix as input, and therefore relies on bootstrapping the ensembles + # instead + + pytest.importorskip('sklearn') + + expected_average = 0.03 + expected_stdev = 0.02 + averages, stdevs = encore.ces([ens1, ens1], + estimate_error = True, + bootstrapping_samples=10, + clustering_method=encore.KMeans(n_clusters=2), + select="name CA and resnum 1-10") + average = averages[0,1] + stdev = stdevs[0,1] + + assert_almost_equal(average, expected_average, decimal=1, + err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity") + assert_almost_equal(stdev, expected_stdev, decimal=1, + err_msg="Unexpected standard daviation for bootstrapped samples in Clustering Ensemble similarity") + + def test_dres_error_estimation(self, ens1): + average_upper_bound = 0.3 + stdev_upper_bound = 0.2 + averages, stdevs = encore.dres([ens1, ens1], estimate_error = True, + bootstrapping_samples=10, + select="name CA and resnum 1-10") + average = averages[0,1] + stdev = stdevs[0,1] + + assert average < average_upper_bound, "Unexpected average value for " \ + "bootstrapped samples in Dim. reduction Ensemble similarity" + assert stdev < stdev_upper_bound, "Unexpected standard deviation for" \ + " bootstrapped samples in Dim. reduction Ensemble imilarity" + +class TestEncoreClustering(object): + @pytest.fixture(scope='class') + def ens1_template(self): + template = mda.Universe(PSF, DCD) + template.transfer_to_memory(step=5) + return template + + @pytest.fixture(scope='class') + def ens2_template(self): + template = mda.Universe(PSF, DCD2) + template.transfer_to_memory(step=5) + return template + + @pytest.fixture(scope='class') + def cc(self): + return encore.ClusterCollection([1, 1, 1, 3, 3, 5, 5, 5]) + + @pytest.fixture(scope='class') + def cluster(self): + return encore.Cluster(elem_list=np.array([0, 1, 2]), centroid=1) + + @pytest.fixture() + def ens1(self, ens1_template): + return mda.Universe( + ens1_template.filename, + ens1_template.trajectory.timeseries(order='fac'), + format=mda.coordinates.memory.MemoryReader) + + @pytest.fixture() + def ens2(self, ens2_template): + return mda.Universe( + ens2_template.filename, + ens2_template.trajectory.timeseries(order='fac'), + format=mda.coordinates.memory.MemoryReader) -# def test_clustering_one_ensemble(self, ens1): -# cluster_collection = encore.cluster(ens1) -# expected_value = 7 -# assert len(cluster_collection) == expected_value, "Unexpected " \ -# "results: {0}".format(cluster_collection) - -# def test_clustering_two_ensembles(self, ens1, ens2): -# cluster_collection = encore.cluster([ens1, ens2]) -# expected_value = 14 -# assert len(cluster_collection) == expected_value, "Unexpected " \ -# "results: {0}".format(cluster_collection) - -# def test_clustering_three_ensembles_two_identical(self, ens1, ens2): -# cluster_collection = encore.cluster([ens1, ens2, ens1]) -# expected_value = 40 -# assert len(cluster_collection) == expected_value, "Unexpected result:" \ -# " {0}".format(cluster_collection) - -# def test_clustering_two_methods(self, ens1): -# cluster_collection = encore.cluster( -# [ens1], -# method=[encore.AffinityPropagationNative(), -# encore.AffinityPropagationNative()]) -# assert len(cluster_collection[0]) == len(cluster_collection[1]), \ -# "Unexpected result: {0}".format(cluster_collection) - -# def test_clustering_AffinityPropagationNative_direct(self, ens1): -# method = encore.AffinityPropagationNative() -# distance_matrix = encore.get_distance_matrix(ens1) -# cluster_assignment = method(distance_matrix) -# expected_value = 7 -# assert len(set(cluster_assignment)) == expected_value, \ -# "Unexpected result: {0}".format(cluster_assignment) - -# def test_clustering_AffinityPropagation_direct(self, ens1): -# pytest.importorskip('sklearn') -# method = encore.AffinityPropagation() -# distance_matrix = encore.get_distance_matrix(ens1) -# cluster_assignment = method(distance_matrix) -# expected_value = 7 -# assert len(set(cluster_assignment)) == expected_value, \ -# "Unexpected result: {0}".format(cluster_assignment) - -# def test_clustering_KMeans_direct(self, ens1): -# pytest.importorskip('sklearn') -# clusters = 10 -# method = encore.KMeans(clusters) -# coordinates = ens1.trajectory.timeseries(order='fac') -# coordinates = np.reshape(coordinates, -# (coordinates.shape[0], -1)) -# cluster_assignment = method(coordinates) -# assert len(set(cluster_assignment)) == clusters, \ -# "Unexpected result: {0}".format(cluster_assignment) - -# def test_clustering_DBSCAN_direct(self, ens1): -# pytest.importorskip('sklearn') -# method = encore.DBSCAN(eps=0.5, min_samples=2) -# distance_matrix = encore.get_distance_matrix(ens1) -# cluster_assignment = method(distance_matrix) -# expected_value = 2 -# assert len(set(cluster_assignment)) == expected_value, \ -# "Unexpected result: {0}".format(cluster_assignment) - -# def test_clustering_two_different_methods(self, ens1): -# pytest.importorskip('sklearn') -# cluster_collection = encore.cluster( -# [ens1], -# method=[encore.AffinityPropagation(preference=-7.5), -# encore.DBSCAN(min_samples=2)]) -# assert len(cluster_collection[0]) == len(cluster_collection[1]), \ -# "Unexpected result: {0}".format(cluster_collection) - -# def test_clustering_method_w_no_distance_matrix(self, ens1): -# pytest.importorskip('sklearn') -# cluster_collection = encore.cluster( -# [ens1], -# method=encore.KMeans(10)) -# assert len(cluster_collection) == 10, \ -# "Unexpected result: {0}".format(cluster_collection) - -# def test_clustering_two_methods_one_w_no_distance_matrix(self, ens1): -# pytest.importorskip('sklearn') -# cluster_collection = encore.cluster( -# [ens1], -# method=[encore.KMeans(17), -# encore.AffinityPropagationNative()]) -# assert len(cluster_collection[0]) == len(cluster_collection[0]), \ -# "Unexpected result: {0}".format(cluster_collection) - -# def test_sklearn_affinity_propagation(self, ens1): -# pytest.importorskip('sklearn') -# cc1 = encore.cluster([ens1]) -# cc2 = encore.cluster([ens1], -# method=encore.AffinityPropagation()) -# assert len(cc1) == len(cc2), \ -# "Native and sklearn implementations of affinity "\ -# "propagation don't agree: mismatch in number of "\ -# "clusters: {0} {1}".format(len(cc1), len(cc2)) - -# def test_ClusterCollection_init(self, cc): -# assert np.all(cc.clusters[0].elements == [0, 1, 2]) and \ -# np.all(cc.clusters[1].elements == [3, 4 ]) and \ -# np.all(cc.clusters[2].elements == [5, 6, 7]) and \ -# cc.clusters[0].centroid == 1 and \ -# cc.clusters[1].centroid == 3 and \ -# cc.clusters[2].centroid == 5, \ -# "ClusterCollection was not constructed correctly" - -# def test_Cluster_init(self, cluster): -# assert np.all(cluster.elements == [0, 1, 2]) and \ -# cluster.centroid == 1, \ -# "Cluster was not constructed correctly" - -# def test_ClusterCollection_get_ids(self, cc): -# assert cc.get_ids() == [0, 1, 2], \ -# "ClusterCollection ids aren't as expected" - -# def test_ClusterCollection_get_centroids(self, cc): -# assert cc.get_centroids() == [1, 3, 5], \ -# "ClusterCollection centroids aren't as expected" - -# def test_Cluster_add_metadata(self, cluster): -# metadata = cluster.elements*10 -# cluster.add_metadata('test', metadata) -# assert np.all(cluster.metadata['test'] == metadata), \ -# "Cluster metadata isn't as expected" - -# class TestEncoreClusteringSklearn(object): -# """The tests in this class were duplicated from the affinity propagation -# tests in scikit-learn""" - -# n_clusters = 3 - -# @pytest.fixture() -# def distance_matrix(self): -# X = np.array([[8.73101582, 8.85617874], -# [11.61311169, 11.58774351], -# [10.86083514, 11.06253959], -# [9.45576027, 8.50606967], -# [11.30441509, 11.04867001], -# [8.63708065, 9.02077816], -# [8.34792066, 9.1851129], -# [11.06197897, 11.15126501], -# [11.24563175, 9.36888267], -# [10.83455241, 8.70101808], -# [11.49211627, 11.48095194], -# [10.6448857, 10.20768141], -# [10.491806, 9.38775868], -# [11.08330999, 9.39065561], -# [10.83872922, 9.48897803], -# [11.37890079, 8.93799596], -# [11.70562094, 11.16006288], -# [10.95871246, 11.1642394], -# [11.59763163, 10.91793669], -# [11.05761743, 11.5817094], -# [8.35444086, 8.91490389], -# [8.79613913, 8.82477028], -# [11.00420001, 9.7143482], -# [11.90790185, 10.41825373], -# [11.39149519, 11.89635728], -# [8.31749192, 9.78031016], -# [11.59530088, 9.75835567], -# [11.17754529, 11.13346973], -# [11.01830341, 10.92512646], -# [11.75326028, 8.46089638], -# [11.74702358, 9.36241786], -# [10.53075064, 9.77744847], -# [8.67474149, 8.30948696], -# [11.05076484, 9.16079575], -# [8.79567794, 8.52774713], -# [11.18626498, 8.38550253], -# [10.57169895, 9.42178069], -# [8.65168114, 8.76846013], -# [11.12522708, 10.6583617], -# [8.87537899, 9.02246614], -# [9.29163622, 9.05159316], -# [11.38003537, 10.93945712], -# [8.74627116, 8.85490353], -# [10.65550973, 9.76402598], -# [8.49888186, 9.31099614], -# [8.64181338, 9.154761], -# [10.84506927, 10.8790789], -# [8.98872711, 9.17133275], -# [11.7470232, 10.60908885], -# [10.89279865, 9.32098256], -# [11.14254656, 9.28262927], -# [9.02660689, 9.12098876], -# [9.16093666, 8.72607596], -# [11.47151183, 8.92803007], -# [11.76917681, 9.59220592], -# [9.97880407, 11.26144744], -# [8.58057881, 8.43199283], -# [10.53394006, 9.36033059], -# [11.34577448, 10.70313399], -# [9.07097046, 8.83928763]]) - -# XX = np.einsum('ij,ij->i', X, X)[:, np.newaxis] -# YY = XX.T -# distances = np.dot(X, X.T) -# distances *= -2 -# distances += XX -# distances += YY -# np.maximum(distances, 0, out=distances) -# distances.flat[::distances.shape[0] + 1] = 0.0 -# dimension = len(distances) - -# distance_matrix = encore.utils.TriangularMatrix(len(distances)) -# for i in range(dimension): -# for j in range(i, dimension): -# distance_matrix[i, j] = distances[i, j] -# return distance_matrix - -# def test_one(self, distance_matrix): -# preference = -float(np.median(distance_matrix.as_array()) * 10.) -# clustering_method = encore.AffinityPropagationNative(preference=preference) -# ccs = encore.cluster(None, -# distance_matrix=distance_matrix, -# method=clustering_method) -# assert self.n_clusters == len(ccs), \ -# "Basic clustering test failed to give the right"\ -# "number of clusters: {0} vs {1}".format(self.n_clusters, len(ccs)) - - -# class TestEncoreDimensionalityReduction(object): -# @pytest.fixture(scope='class') -# def ens1_template(self): -# template = mda.Universe(PSF, DCD) -# template.transfer_to_memory(step=5) -# return template - -# @pytest.fixture(scope='class') -# def ens2_template(self): -# template = mda.Universe(PSF, DCD2) -# template.transfer_to_memory(step=5) -# return template - -# @pytest.fixture() -# def ens1(self, ens1_template): -# return mda.Universe( -# ens1_template.filename, -# ens1_template.trajectory.timeseries(order='fac'), -# format=mda.coordinates.memory.MemoryReader) - -# @pytest.fixture() -# def ens2(self, ens2_template): -# return mda.Universe( -# ens2_template.filename, -# ens2_template.trajectory.timeseries(order='fac'), -# format=mda.coordinates.memory.MemoryReader) - -# def test_dimensionality_reduction_one_ensemble(self, ens1): -# dimension = 2 -# coordinates, details = encore.reduce_dimensionality(ens1) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - -# def test_dimensionality_reduction_two_ensembles(self, ens1, ens2): -# dimension = 2 -# coordinates, details = \ -# encore.reduce_dimensionality([ens1, ens2]) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - -# def test_dimensionality_reduction_three_ensembles_two_identical(self, -# ens1, ens2): -# coordinates, details = \ -# encore.reduce_dimensionality([ens1, ens2, ens1]) -# coordinates_ens1 = coordinates[:,np.where(details["ensemble_membership"]==1)] -# coordinates_ens3 = coordinates[:,np.where(details["ensemble_membership"]==3)] -# assert_almost_equal(coordinates_ens1, coordinates_ens3, decimal=0, -# err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - -# def test_dimensionality_reduction_specified_dimension(self, ens1, ens2): -# dimension = 3 -# coordinates, details = encore.reduce_dimensionality( -# [ens1, ens2], -# method=encore.StochasticProximityEmbeddingNative(dimension=dimension)) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - -# def test_dimensionality_reduction_SPENative_direct(self, ens1): -# dimension = 2 -# method = encore.StochasticProximityEmbeddingNative(dimension=dimension) -# distance_matrix = encore.get_distance_matrix(ens1) -# coordinates, details = method(distance_matrix) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format( -# coordinates)) - -# def test_dimensionality_reduction_PCA_direct(self, ens1): -# pytest.importorskip('sklearn') -# dimension = 2 -# method = encore.PrincipalComponentAnalysis(dimension=dimension) -# coordinates = ens1.trajectory.timeseries(order='fac') -# coordinates = np.reshape(coordinates, -# (coordinates.shape[0], -1)) -# coordinates, details = method(coordinates) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format( -# coordinates)) - - -# def test_dimensionality_reduction_different_method(self, ens1, ens2): -# pytest.importorskip('sklearn') -# dimension = 3 -# coordinates, details = \ -# encore.reduce_dimensionality( -# [ens1, ens2], -# method=encore.PrincipalComponentAnalysis(dimension=dimension)) -# assert_equal(coordinates.shape[0], dimension, -# err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - -# def test_dimensionality_reduction_two_methods(self, ens1, ens2): -# dims = [2,3] -# coordinates, details = \ -# encore.reduce_dimensionality( -# [ens1, ens2], -# method=[encore.StochasticProximityEmbeddingNative(dims[0]), -# encore.StochasticProximityEmbeddingNative(dims[1])]) -# assert_equal(coordinates[1].shape[0], dims[1]) - -# def test_dimensionality_reduction_two_different_methods(self, ens1, ens2): -# pytest.importorskip('sklearn') -# dims = [2,3] -# coordinates, details = \ -# encore.reduce_dimensionality( -# [ens1, ens2], -# method=[encore.StochasticProximityEmbeddingNative(dims[0]), -# encore.PrincipalComponentAnalysis(dims[1])]) -# assert_equal(coordinates[1].shape[0], dims[1]) - - -# class TestEncoreConfDistMatrix(object): -# def test_get_distance_matrix(self): -# # Issue #1324 -# u = mda.Universe(TPR,XTC) -# dm = confdistmatrix.get_distance_matrix(u) - -# class TestEncoreImportWarnings(object): -# @block_import('sklearn') -# def _check_sklearn_import_warns(self, package, recwarn): -# for mod in list(sys.modules): # list as we're changing as we iterate -# if 'encore' in mod: -# sys.modules.pop(mod, None) -# warnings.simplefilter('always') -# # assert_warns(ImportWarning, importlib.import_module, package) -# importlib.import_module(package) -# assert recwarn.pop(ImportWarning) - -# def test_import_warnings(self, recwarn): -# for mod in list(sys.modules): # list as we're changing as we iterate -# if 'encore' in mod: -# sys.modules.pop(mod, None) -# for pkg in ( -# 'MDAnalysis.analysis.encore.dimensionality_reduction.DimensionalityReductionMethod', -# 'MDAnalysis.analysis.encore.clustering.ClusteringMethod', -# ): -# self._check_sklearn_import_warns(pkg, recwarn) -# # This is a quickfix! Convert this to a parametrize call in future. + def test_clustering_one_ensemble(self, ens1): + cluster_collection = encore.cluster(ens1) + expected_value = 7 + assert len(cluster_collection) == expected_value, "Unexpected " \ + "results: {0}".format(cluster_collection) + + def test_clustering_two_ensembles(self, ens1, ens2): + cluster_collection = encore.cluster([ens1, ens2]) + expected_value = 14 + assert len(cluster_collection) == expected_value, "Unexpected " \ + "results: {0}".format(cluster_collection) + + def test_clustering_three_ensembles_two_identical(self, ens1, ens2): + cluster_collection = encore.cluster([ens1, ens2, ens1]) + expected_value = 40 + assert len(cluster_collection) == expected_value, "Unexpected result:" \ + " {0}".format(cluster_collection) + + def test_clustering_two_methods(self, ens1): + cluster_collection = encore.cluster( + [ens1], + method=[encore.AffinityPropagationNative(), + encore.AffinityPropagationNative()]) + assert len(cluster_collection[0]) == len(cluster_collection[1]), \ + "Unexpected result: {0}".format(cluster_collection) + + def test_clustering_AffinityPropagationNative_direct(self, ens1): + method = encore.AffinityPropagationNative() + distance_matrix = encore.get_distance_matrix(ens1) + cluster_assignment = method(distance_matrix) + expected_value = 7 + assert len(set(cluster_assignment)) == expected_value, \ + "Unexpected result: {0}".format(cluster_assignment) + + def test_clustering_AffinityPropagation_direct(self, ens1): + pytest.importorskip('sklearn') + method = encore.AffinityPropagation() + distance_matrix = encore.get_distance_matrix(ens1) + cluster_assignment = method(distance_matrix) + expected_value = 7 + assert len(set(cluster_assignment)) == expected_value, \ + "Unexpected result: {0}".format(cluster_assignment) + + def test_clustering_KMeans_direct(self, ens1): + pytest.importorskip('sklearn') + clusters = 10 + method = encore.KMeans(clusters) + coordinates = ens1.trajectory.timeseries(order='fac') + coordinates = np.reshape(coordinates, + (coordinates.shape[0], -1)) + cluster_assignment = method(coordinates) + assert len(set(cluster_assignment)) == clusters, \ + "Unexpected result: {0}".format(cluster_assignment) + + def test_clustering_DBSCAN_direct(self, ens1): + pytest.importorskip('sklearn') + method = encore.DBSCAN(eps=0.5, min_samples=2) + distance_matrix = encore.get_distance_matrix(ens1) + cluster_assignment = method(distance_matrix) + expected_value = 2 + assert len(set(cluster_assignment)) == expected_value, \ + "Unexpected result: {0}".format(cluster_assignment) + + def test_clustering_two_different_methods(self, ens1): + pytest.importorskip('sklearn') + cluster_collection = encore.cluster( + [ens1], + method=[encore.AffinityPropagation(preference=-7.5), + encore.DBSCAN(min_samples=2)]) + assert len(cluster_collection[0]) == len(cluster_collection[1]), \ + "Unexpected result: {0}".format(cluster_collection) + + def test_clustering_method_w_no_distance_matrix(self, ens1): + pytest.importorskip('sklearn') + cluster_collection = encore.cluster( + [ens1], + method=encore.KMeans(10)) + assert len(cluster_collection) == 10, \ + "Unexpected result: {0}".format(cluster_collection) + + def test_clustering_two_methods_one_w_no_distance_matrix(self, ens1): + pytest.importorskip('sklearn') + cluster_collection = encore.cluster( + [ens1], + method=[encore.KMeans(17), + encore.AffinityPropagationNative()]) + assert len(cluster_collection[0]) == len(cluster_collection[0]), \ + "Unexpected result: {0}".format(cluster_collection) + + def test_sklearn_affinity_propagation(self, ens1): + pytest.importorskip('sklearn') + cc1 = encore.cluster([ens1]) + cc2 = encore.cluster([ens1], + method=encore.AffinityPropagation()) + assert len(cc1) == len(cc2), \ + "Native and sklearn implementations of affinity "\ + "propagation don't agree: mismatch in number of "\ + "clusters: {0} {1}".format(len(cc1), len(cc2)) + + def test_ClusterCollection_init(self, cc): + assert np.all(cc.clusters[0].elements == [0, 1, 2]) and \ + np.all(cc.clusters[1].elements == [3, 4 ]) and \ + np.all(cc.clusters[2].elements == [5, 6, 7]) and \ + cc.clusters[0].centroid == 1 and \ + cc.clusters[1].centroid == 3 and \ + cc.clusters[2].centroid == 5, \ + "ClusterCollection was not constructed correctly" + + def test_Cluster_init(self, cluster): + assert np.all(cluster.elements == [0, 1, 2]) and \ + cluster.centroid == 1, \ + "Cluster was not constructed correctly" + + def test_ClusterCollection_get_ids(self, cc): + assert cc.get_ids() == [0, 1, 2], \ + "ClusterCollection ids aren't as expected" + + def test_ClusterCollection_get_centroids(self, cc): + assert cc.get_centroids() == [1, 3, 5], \ + "ClusterCollection centroids aren't as expected" + + def test_Cluster_add_metadata(self, cluster): + metadata = cluster.elements*10 + cluster.add_metadata('test', metadata) + assert np.all(cluster.metadata['test'] == metadata), \ + "Cluster metadata isn't as expected" + +class TestEncoreClusteringSklearn(object): + """The tests in this class were duplicated from the affinity propagation + tests in scikit-learn""" + + n_clusters = 3 + + @pytest.fixture() + def distance_matrix(self): + X = np.array([[8.73101582, 8.85617874], + [11.61311169, 11.58774351], + [10.86083514, 11.06253959], + [9.45576027, 8.50606967], + [11.30441509, 11.04867001], + [8.63708065, 9.02077816], + [8.34792066, 9.1851129], + [11.06197897, 11.15126501], + [11.24563175, 9.36888267], + [10.83455241, 8.70101808], + [11.49211627, 11.48095194], + [10.6448857, 10.20768141], + [10.491806, 9.38775868], + [11.08330999, 9.39065561], + [10.83872922, 9.48897803], + [11.37890079, 8.93799596], + [11.70562094, 11.16006288], + [10.95871246, 11.1642394], + [11.59763163, 10.91793669], + [11.05761743, 11.5817094], + [8.35444086, 8.91490389], + [8.79613913, 8.82477028], + [11.00420001, 9.7143482], + [11.90790185, 10.41825373], + [11.39149519, 11.89635728], + [8.31749192, 9.78031016], + [11.59530088, 9.75835567], + [11.17754529, 11.13346973], + [11.01830341, 10.92512646], + [11.75326028, 8.46089638], + [11.74702358, 9.36241786], + [10.53075064, 9.77744847], + [8.67474149, 8.30948696], + [11.05076484, 9.16079575], + [8.79567794, 8.52774713], + [11.18626498, 8.38550253], + [10.57169895, 9.42178069], + [8.65168114, 8.76846013], + [11.12522708, 10.6583617], + [8.87537899, 9.02246614], + [9.29163622, 9.05159316], + [11.38003537, 10.93945712], + [8.74627116, 8.85490353], + [10.65550973, 9.76402598], + [8.49888186, 9.31099614], + [8.64181338, 9.154761], + [10.84506927, 10.8790789], + [8.98872711, 9.17133275], + [11.7470232, 10.60908885], + [10.89279865, 9.32098256], + [11.14254656, 9.28262927], + [9.02660689, 9.12098876], + [9.16093666, 8.72607596], + [11.47151183, 8.92803007], + [11.76917681, 9.59220592], + [9.97880407, 11.26144744], + [8.58057881, 8.43199283], + [10.53394006, 9.36033059], + [11.34577448, 10.70313399], + [9.07097046, 8.83928763]]) + + XX = np.einsum('ij,ij->i', X, X)[:, np.newaxis] + YY = XX.T + distances = np.dot(X, X.T) + distances *= -2 + distances += XX + distances += YY + np.maximum(distances, 0, out=distances) + distances.flat[::distances.shape[0] + 1] = 0.0 + dimension = len(distances) + + distance_matrix = encore.utils.TriangularMatrix(len(distances)) + for i in range(dimension): + for j in range(i, dimension): + distance_matrix[i, j] = distances[i, j] + return distance_matrix + + def test_one(self, distance_matrix): + preference = -float(np.median(distance_matrix.as_array()) * 10.) + clustering_method = encore.AffinityPropagationNative(preference=preference) + ccs = encore.cluster(None, + distance_matrix=distance_matrix, + method=clustering_method) + assert self.n_clusters == len(ccs), \ + "Basic clustering test failed to give the right"\ + "number of clusters: {0} vs {1}".format(self.n_clusters, len(ccs)) + + +class TestEncoreDimensionalityReduction(object): + @pytest.fixture(scope='class') + def ens1_template(self): + template = mda.Universe(PSF, DCD) + template.transfer_to_memory(step=5) + return template + + @pytest.fixture(scope='class') + def ens2_template(self): + template = mda.Universe(PSF, DCD2) + template.transfer_to_memory(step=5) + return template + + @pytest.fixture() + def ens1(self, ens1_template): + return mda.Universe( + ens1_template.filename, + ens1_template.trajectory.timeseries(order='fac'), + format=mda.coordinates.memory.MemoryReader) + + @pytest.fixture() + def ens2(self, ens2_template): + return mda.Universe( + ens2_template.filename, + ens2_template.trajectory.timeseries(order='fac'), + format=mda.coordinates.memory.MemoryReader) + + def test_dimensionality_reduction_one_ensemble(self, ens1): + dimension = 2 + coordinates, details = encore.reduce_dimensionality(ens1) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + + + def test_dimensionality_reduction_two_ensembles(self, ens1, ens2): + dimension = 2 + coordinates, details = \ + encore.reduce_dimensionality([ens1, ens2]) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + + + def test_dimensionality_reduction_three_ensembles_two_identical(self, + ens1, ens2): + coordinates, details = \ + encore.reduce_dimensionality([ens1, ens2, ens1]) + coordinates_ens1 = coordinates[:,np.where(details["ensemble_membership"]==1)] + coordinates_ens3 = coordinates[:,np.where(details["ensemble_membership"]==3)] + assert_almost_equal(coordinates_ens1, coordinates_ens3, decimal=0, + err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + + + def test_dimensionality_reduction_specified_dimension(self, ens1, ens2): + dimension = 3 + coordinates, details = encore.reduce_dimensionality( + [ens1, ens2], + method=encore.StochasticProximityEmbeddingNative(dimension=dimension)) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + + + def test_dimensionality_reduction_SPENative_direct(self, ens1): + dimension = 2 + method = encore.StochasticProximityEmbeddingNative(dimension=dimension) + distance_matrix = encore.get_distance_matrix(ens1) + coordinates, details = method(distance_matrix) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates)) + + def test_dimensionality_reduction_PCA_direct(self, ens1): + pytest.importorskip('sklearn') + dimension = 2 + method = encore.PrincipalComponentAnalysis(dimension=dimension) + coordinates = ens1.trajectory.timeseries(order='fac') + coordinates = np.reshape(coordinates, + (coordinates.shape[0], -1)) + coordinates, details = method(coordinates) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates)) + + + def test_dimensionality_reduction_different_method(self, ens1, ens2): + pytest.importorskip('sklearn') + dimension = 3 + coordinates, details = \ + encore.reduce_dimensionality( + [ens1, ens2], + method=encore.PrincipalComponentAnalysis(dimension=dimension)) + assert_equal(coordinates.shape[0], dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + + + def test_dimensionality_reduction_two_methods(self, ens1, ens2): + dims = [2,3] + coordinates, details = \ + encore.reduce_dimensionality( + [ens1, ens2], + method=[encore.StochasticProximityEmbeddingNative(dims[0]), + encore.StochasticProximityEmbeddingNative(dims[1])]) + assert_equal(coordinates[1].shape[0], dims[1]) + + def test_dimensionality_reduction_two_different_methods(self, ens1, ens2): + pytest.importorskip('sklearn') + dims = [2,3] + coordinates, details = \ + encore.reduce_dimensionality( + [ens1, ens2], + method=[encore.StochasticProximityEmbeddingNative(dims[0]), + encore.PrincipalComponentAnalysis(dims[1])]) + assert_equal(coordinates[1].shape[0], dims[1]) + + +class TestEncoreConfDistMatrix(object): + def test_get_distance_matrix(self): + # Issue #1324 + u = mda.Universe(TPR,XTC) + dm = confdistmatrix.get_distance_matrix(u) + +class TestEncoreImportWarnings(object): + @block_import('sklearn') + def _check_sklearn_import_warns(self, package, recwarn): + for mod in list(sys.modules): # list as we're changing as we iterate + if 'encore' in mod: + sys.modules.pop(mod, None) + warnings.simplefilter('always') + # assert_warns(ImportWarning, importlib.import_module, package) + importlib.import_module(package) + assert recwarn.pop(ImportWarning) + + def test_import_warnings(self, recwarn): + for mod in list(sys.modules): # list as we're changing as we iterate + if 'encore' in mod: + sys.modules.pop(mod, None) + for pkg in ( + 'MDAnalysis.analysis.encore.dimensionality_reduction.DimensionalityReductionMethod', + 'MDAnalysis.analysis.encore.clustering.ClusteringMethod', + ): + self._check_sklearn_import_warns(pkg, recwarn) + # This is a quickfix! Convert this to a parametrize call in future. diff --git a/testsuite/MDAnalysisTests/analysis/test_reencore.py b/testsuite/MDAnalysisTests/analysis/test_reencore.py index 66a385f039e..39d5c09bed1 100644 --- a/testsuite/MDAnalysisTests/analysis/test_reencore.py +++ b/testsuite/MDAnalysisTests/analysis/test_reencore.py @@ -27,6 +27,7 @@ from MDAnalysis.analysis.clustering import Clusters, methods from MDAnalysis.analysis.encore.covariance import (ml_covariance_estimator, shrinkage_covariance_estimator) +from MDAnalysis.analysis.encore.dimensionality_reduction import DimensionalityReductionMethod as drm import importlib import tempfile @@ -44,6 +45,8 @@ import MDAnalysis.analysis.rms as rms import MDAnalysis.analysis.align as align +np.random.seed(0) + @pytest.fixture() def data(): return np.arange(24, dtype=np.float64).reshape((4, 6)) @@ -102,7 +105,7 @@ def ensemble_aligned(self): u2 = mda.Universe(PSF, DCD2) u2.transfer_to_memory(step=5) ens = mda.Ensemble([u1, u2]).select_atoms("name CA") - a = align.AlignTraj(ens, ens).run() + a = align.AlignTraj(ens, ens, in_memory=True).run() return ens @pytest.fixture() @@ -115,6 +118,21 @@ def ensemble1_aligned(self): a = align.AlignTraj(ens, ens, in_memory=True).run() return ens + @pytest.fixture() + def ens1_aligned(self): + u = mda.Universe(PSF, DCD) + u.transfer_to_memory(step=5) + align.AlignTraj(u, u, select="name CA", in_memory=True).run() + return u + + @pytest.fixture() + def dist_mat(self, ensemble_aligned): + return encore.get_distance_matrix(ensemble_aligned) + + @pytest.fixture() + def dist_mat1(self, ensemble1_aligned): + return encore.get_distance_matrix(ensemble1_aligned) + def test_affinity_propagation(self, ens1): dist_mat = encore.get_distance_matrix(ens1).as_array() clusters = Clusters(methods.AffinityPropagation) @@ -136,19 +154,27 @@ def test_hes_align(self, ens1, ens2): result_value = reencore.hes(ensemble, align=True)[0, 1] old, _ = encore.hes(ensemble.universes) assert_almost_equal(result_value, old[0, 1], decimal=-2) + + def test_hes_estimate_error(self, ensemble1): + omean, ostd = encore.hes(ensemble1.universes, estimate_error=True, + bootstrapping_samples=10, + select="name CA and resnum 1-10") + mean, std = reencore.hes(ensemble1, estimate_error=True, + select="name CA and resnum 1-10", + n_bootstrap_samples=10) + assert_almost_equal(mean[0, 1], mean[0, 1], decimal=1) + assert_almost_equal(std[0, 1], std[0, 1], decimal=1) + - def test_ces_to_self(self, ensemble1_aligned): - dm = DistanceMatrix(ensemble1_aligned, select="name CA").run() - rmsd_mat1 = dm.dist_matrix - dm = DistanceMatrix(ensemble1_aligned).run() + def test_ces_to_self(self, ensemble1_aligned, dist_mat1): clusters = Clusters(methods.AffinityPropagation(preference=-3.0)) - clusters.run(-rmsd_mat1) + clusters.run(-dist_mat1.as_array()) result_value = reencore.ces(ensemble1_aligned, clusters)[0, 1] assert_almost_equal(result_value, 0, err_msg=f"ces() to itself not zero: {result_value}") - def test_ces_rmsd_enc(self, ensemble_aligned): - rmsd_mat_enc = encore.get_distance_matrix(ensemble_aligned).as_array() + def test_ces_rmsd_enc(self, ensemble_aligned, dist_mat): + rmsd_mat_enc = dist_mat.as_array() clusters = Clusters(methods.AffinityPropagation()) clusters.run(-rmsd_mat_enc) result_value = reencore.ces(ensemble_aligned, clusters)[0, 1] @@ -161,12 +187,84 @@ def test_ces(self, ensemble_aligned): clusters = Clusters(methods.AffinityPropagation()) clusters.run(-rmsd_mat) result_value = reencore.ces(ensemble_aligned, clusters)[0, 1] - assert_almost_equal(result_value, 0.69, decimal=2, + assert_almost_equal(result_value, 0.51, decimal=2, err_msg=f"unexpected value") + + def test_ces_estimate_error(self, ensemble1_aligned, dist_mat1): + omean, ostd = encore.ces(ensemble1_aligned.universes, + estimate_error=True, + bootstrapping_samples=10, + clustering_method=encore.AffinityPropagationNative(preference=-2.0), + select="name CA and resnum 1-10") + rmsd_mat_enc = dist_mat1.as_array() + clusters = Clusters(methods.AffinityPropagation(preference=-2.0)) + clusters.run(-rmsd_mat_enc) + mean, std = reencore.ces(ensemble1_aligned, clusters, estimate_error=True, + select="name CA and resnum 1-10", + n_bootstrap_samples=10) + assert_almost_equal(mean[0, 1], mean[0, 1]) + assert_almost_equal(std[0, 1], std[0, 1]) + def test_dres_to_self(self, ensemble1_aligned): - result = reencore.dres(ensemble1_aligned)[0, 1] + result = reencore.dres(ensemble1_aligned, pca.PCA, n_components=3)[0, 1] assert_almost_equal(result, 0) - + + def test_dres(self, ensemble_aligned, dist_mat): + spe = drm.StochasticProximityEmbeddingNative(dimension=3, + distance_cutoff=1.5, + min_lam=0.1, + max_lam=2.0, + ncycle=100, + nstep=10000) + dimred, _ = spe(dist_mat) + old, _ = encore.dres(ensemble_aligned.universes) + new = reencore.dres(ensemble_aligned, dimred.T, seed=0) + assert_almost_equal(old[0, 1], new[0, 1], decimal=1) + + def test_dres_estimate_error(self, ensemble1_aligned): + omean, ostd = encore.dres(ensemble1_aligned.universes, + estimate_error=True, + bootstrapping_samples=10, + select="name CA and resnum 1-10") + mean, std = reencore.dres(ensemble1_aligned, estimate_error=True, + select="name CA and resnum 1-10", + n_bootstrap_samples=10) + assert_almost_equal(mean[0, 1], mean[0, 1]) + assert_almost_equal(std[0, 1], std[0, 1]) + + def test_ces_convergence(self, ens1_aligned): + # clusters + dm = DistanceMatrix(ens1_aligned, select="name CA").run() + rmsd_mat = dm.dist_matrix + clusters = Clusters(methods.AffinityPropagation()) + clusters.run(-rmsd_mat) + + results = reencore.ces_convergence(ens1_aligned, clusters, + window_size=5) + exp = np.array([0.38, 0.19, 0.07, 0.]) + assert_almost_equal(results, exp, decimal=2) + + def test_dres_convergence(self, ens1_aligned): + results = reencore.dres_convergence(ens1_aligned, pca.PCA, + window_size=10, + select="name CA", + n_components=3, + seed=0) + exp = np.array([0.5, 0.]) + assert_almost_equal(results, exp, decimal=1) + + def test_dres_convergence_spe(self, ens1_aligned): + dist_mat = encore.get_distance_matrix(ens1_aligned) + spe = drm.StochasticProximityEmbeddingNative(dimension=3, + nstep=10000) + dimred, _ = spe(dist_mat) + results = reencore.dres_convergence(ens1_aligned, dimred.T, + window_size=10) + exp = np.array([0.3, 0.]) + assert_almost_equal(results, exp, decimal=1) + + + diff --git a/testsuite/MDAnalysisTests/rmsfit_adk_dims.dcd b/testsuite/MDAnalysisTests/rmsfit_adk_dims.dcd deleted file mode 100644 index 40c96fc8374a4cc529369dee551f0aabf4921390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108924 zcmd?Rc{Ek=_diUcGKC6-N})nzDkaX|O=L(aLy_qBJ(_iq=_^rg`}e5?5$ao z2F*09G;5$z{qA@Dp6}KKO)_Nu`@{bq&Hr`Y-6iAh|22>Lf1Z1k z2mIeY`tSGuJxezJ*FIj7i|4dw9GVo5#^K(*ap2zWsPA0}3D@^QyNVf%jC@o#yuqA4 z4lbn>`BmgHco{j5DG(c`+mY_TBXn@iF!Fz(C+3#TrW(^O@%it2!pLVi!VWievHa8r zp_gw7{LS0~e*&tZOKmkwf87&a>bQgX&MrGcFWdJ*n!*NzzO! zq-%Xnnm-qM5C%18^Z7!Q>VtO3kAR4GLY9<2BVaF;4~w3%*!L#=y)0;S8L%X z$G&*!aXl>F{1<9Qnd3P@6%Xxv2K(!VV&f7kY}wEck5}oTPybH1(W-^xUra|Mhe>Gi z%LsoDlE%cEN;p_s3d#o*;orjHaPH4}*d8y7Umr5~n)bwRvk}d{c;h0rK?_-Ld=Zw2 zgVQ$P+~;fYUrs(g^4W(Kueae8&0;iPorI;0=~!|k^h_Yhkv7)?rNOBSeG|Ax%0Bk%ec&73(wHc;;U*Q zoHag>+i#??`krLo6uOl!e=p~%J|}raas}_ZQ^+45#PGjWD|zS%Pu3Q~_`Gc{_teYg zsq43MduR$f+otf1=dt|0dM4)#n#1Scj^L-ufz>W7Wz84!dC^3B9+aieCacwW=e6I& zmK7wGxrzdgO{53g?u+p&JM89_HVccF=z`6K12Ff=Rj`~~4=zWqz*6I(*tugEuJIp? z+e+MVL-`6^I&=Z9lMcZ(%eG+c#aul9J_{#V@5gNxr*YHLN>u0lczJCS;+$YK-sXTy z9;u`C@$-;VXaQ&CC*^pC@6IA1WopOfvV!aQqNv+~8**(;KYj%FwPkR zCjuuzt8t-V-Bc;gd3#7$AC(Wq?w4U!c|P|$X< z47+`c;fCZ)p6Iffy=FMFPS6~Fqp*V4l>ompcVPDc6SzfdF~{DE<`&s`JT!hipVwH! zgO`uv6duKWET{7;Ei(=p*g;Q&<$1dNKz4g~n-T}6({$IX^le%k`7ZMm-xj9}8K2aJ z?vX9R-hd;Zrk@L+BFBSmfP>`RPT8&0I9K-@%xI;lKh+o|(Jg7BEtmgM?za~-DNu*| z%#h=Ngd&Qnc|;H^%dy>d(GaN^irxF3njSo-Di^vi6wrm3BiNE0BYZ;7m zx&c1}{(;@z=V0013H{#n#EN|{!Pln-w1*7={SS*_`m+PjsV)l|cgNbfDq6u~<@wP3 zsj&9lv`lf$tTv(e*gBzh>L@$ui{nHJkQO6)CeYyBn@BN!zgWM|L=1Xr4ZTGKhZ%ce zMO`JRt6zY1#oh5ix&gj6)JLDw8d$qU5tBxnV^g^j&dj_D_X>^Rk53S^olb{;=F<2s z;w20|84Jse;^39XWxId@`J&&JcXe`oSCW?HZj!CNLnk5!vg1-U_L3DT(DOEF`5Yqa z4RXA4Tp#YZdz3c1UL*et{dmXR&y*~`l@=dI z&9L+E(E9`2ov4qS$_8M+d1FyO%^a1po$!9i47`2C1RsZwL*vF}sBvK_s_4wZCmnHE z?45~TJ%h3FNf_Ei=VP5l22xQlp8lDPx90Ccsh$a#XB&YTuG#qC@iqN)Zw_D4NJc^c z&Hdset}D+J6yI-$7ZZL$9|JX9^8N!%czX+ycRhmao*rPmKSFr6syE%}FHP=#-RM$z z4xM&-NscBz>G<`xbSKAv2ali3m)Mqlv&OOEwgs&EbUepC1AeH`o7eXJMBcoYERMKR zy?kG)kgcY*8tv3La6k3gewr?>7{Drf)i~Fxg@zTV@`q9Oywc2+n=g)I+2O{#%2t^j z|1ufmoFl_nJ93HGCGHDZAZkxEfFE+P5I%1XJhcb}i}Kg7+e`+l9HsExoO*CecZO5@ z$ADLy0@!^|68$4pCfF~?yXHS1oapZh=3*uYz~>ai?e8EC~)72`Oq(uR}n2eI|K zB<`n|&+lI(aO1PtyyJK%&z>2;u`5P#-~|WqA6-?gy58M|WhkBLKkgVqguS{Kmj{c~%*KQRxLv2KP|4y;??@`(jSxaTj#pL;I zBCX%-NVs+$&A1sY+I6-GM@tb5_FRU}qmN*1wE>lRHS zQiX$*2J>PIrzGokdNvn(jGBSv3-wSfe;Ur+wi-)29B}UIaaf9Bc+?;q zmlTf2ld3wncb60XbDM!C9qKsQ!3ezM)n#01a7i)!kle@v7x4Yn))(LpKy&ayPX#(XPirD_{DJZIF;=;nWV76#8 zWbCsL`iHL;yWLWN;-Q1#!^qv@1?$=3o4vn<30xnT)5_krARK2sLdQENDw#752?5g3^~gF(+FS!_zK;bm>L-p)d$PXz8F*(;cYElEvhW zNH*f2KS%IgaXI(AS zZ5;xJ&V3-`Z!Neb%b?TaOR%rMGG1Egh>oxQ@Wo(T4A%F>t)0pEVR#JQ$&Nzvgq_&e zvJ4N!72+es{rKT#G8U+3VrxPOj@1svdqDz@)fs|+if+Tj>w6)+>5}kKGs;#r-kVxR zE+SpW0$S?Xoh>?5d5uPQ7RQd}0r$PQy4->x$c~-U*RiW~0>A#fil;hk;vZMNxUjz; z8^|o?f%X%4dYd+TN$!7FSeGMR%vddFHdk1L@XUl9zE+aPjl29g^Zs0ZW|hS4uhMza zqhd}7IK{J;*YMJ{l{{}?3HzT-;vv;W5Nx$y;DRF%QUPv&#d_)Kn|ki{Ku z;`vV99L~4!;LB4+@V`aK!(^AU_NEXH)ph2t)+4yj!G1iW`47F9Av#i#Kq_m!XwA*@ z;@GsicK0%hg)6EmU_NaNB;CFWexAqRlSdtxWNYK|>mx8YVJPa|oQWB$L$JirAM-mx zQ6?@I4Q^~hlkb^Wy=pI(B-CK+p>jM^u^(q|PsCNdm*LNv$yk4)A3FHlhsj6%K`{Ao z+SqU;ef6&-?=$!5`R<-v`R^(HkZUARPNF!q0rY6pJ+Y@!ZQa<=$3oC>A5iRyfn^~_ zK_OBaPwbXKh2|dr$$PFfJ<;S*A&gm51iKonp=z@iY+f@6N`nJn?qoICl_V`*9hfAj zmgK@o&00|Z5d%))EEg@+)-=@Aw3Jzl=aZZx+5yF-OZk zz0l%kB^0bw1>1(1g6*~ewD{X}iqgDDrKXQa>vIEnxPG9?dPZ#TI)okV2eS2u85|hC zjFmUI@b1zezA`z3NjZ_v#O&gyE}1;I`vzX5n9el0fHy%Tw|K>K`NF1 z#zNo~7h9<>2kJzJ;Z(ZYpZvUXDQ!nH#kzK9sSoWmbL}8LsUXV>v-i@xM|bE_f+B0p zKSrzMb7{cnH+1H419jUojNS#8iOZMo6%IX^4{yufLTmaj$cQ-yt^2-%X`g18k}i!J z%DwU0@$ZoEs~#NV^dM-kC#2_9!SL@x;HJzhVa)RRfF5Dc^5Cnjk*-ah(Sm{CTW=@` zPHTlzizCET=X7y#S^-r)IZAGeG-yghllVEHOL*)%21bRKz>CKx;pDETP`FhIe{9po zr~qSpwN4Y)uT(@Q&yi?&QU-ffo`RLH<-jo21J3s@fES@X@lLm%Xi#4TBbySyasPBd z)u~Xd_@yQe7rbepPaGYZc!f?`_UGE!y?C_!etLTH6zy7YoEAKjX7xlR-thAP*;t*W z+A*pey8RWIU=|&yXrrJSSuUE8LkA{DQpk>HB;WCdBG<)|W7vLie~cVWyA>*aeq{{a z$6pK6Y_Fc#cS;|OyGiP(50R4VmH-O+e<5bN5xni`EwFL)B%C=f7zbWhh>1hJvATCE_I1j}e5VkcS+xqsOXp(A z#uUsB3B^|zQgNnFA-tlgR)%aopq#;grU97&P`T zeAH1#g^h2Z*S8Ds@&@VPbc)^_jkV0p+N^J`I$GB9G9UX<8w&2{tI;!AEj}j z)pUNVCXY-Tz)c5#5Ih^i4?j=m3@>YTn>~etx{cy#Exoy{QF86S-XpiPg_Jn#pSVhO zw>Vy_8zeTyz@)%gpnt~`ZoX`W%;lfqifKD|dQ`*qhlX%1-VOq~DZ{K0A!69}vGn0d zcj|H(P4Mb6oml^tw#nAhnPLBEySgRc{W^v_E}3yprTM(NHina`bJ;mQhObo5;%Ms- z)~N93H6EsHy-AH1#9yYI&#UNcz9Mz@_Y_9$h!c#~nZrv7kMUEb9m=*z;h0HO*uLv)~zonqgE;yI5(N|@bBY1S|3)6r<^7E?q@IX(Hu5l#Nps>3s^ zJb35v2<~iiXLD0mewwetbB11~=Ba`7({NTDP1;twbp971{%kVn2XBY3P2XTu^=OP* z>4~Eh^zllNDHfXtVC-C1Ja%m;=4ehw<8v#phoLQQQ?SM>;j1zE&Vm>KEbV9aWt6=@A?o83m>K z*M;zk1H!Hy?}Vz>Y^ZB52GgxwFluZw6z%*A*T>y}&>SUn(YgUw$|k_1_nmcyH!q6w zyPp;&$4NtB*$eT3bet%JN`d;~h`JvwQ%G@lL!HXMe7mNRwzOXD#QzvqURs4{R3l*J z`F?2P_y>Z{q`|M-TR|nP9~gj~=rl>4ni{LAqb!${bfRgP(jPHy$3~Lxd4jH0>XK)+ zfjH>1HAVIo#Q+&u5Y#RTCck#sO*%LMa=kCXtEESwB9r0Z{k2fBO%dM2xr5r0iNb=A z&w~7pMp18TB5|K4F@9Ht$anWrYrn0ew{Hl=I-j7ma$Tf;`5e{F+e*q(nPRHBn~-hn z1Yfn+39d1}#SJfH>zRpEhC2DVMSg1YVr)qq7-nJygiP6K2cJ8_rvMmtugMC z9~Q5-MSWu{tgKVQ5w1hf0(>y|%v20j7=>y1vS?~v2d5^BkkZ`*&IMc&T0Hha3Va5g z{`D~Xml}HSbHTpj7GS)h6+WEdi4olrv9c=$|CL5#Z1Z+pwX+O|s2;$$gNM<&S31UP zq+`>#AUruL7-P!--)igPv%n_!7Fi6`#iE4Bh5o^D6tOy zq=goyymRJEE>AGwdm~5l)#zY8W0u6LuB~Q!i)h}nX&#S@3FPq!%h+zMJBM43ill?mksOzmmde?y{fa@t(?cgS(aqMqQtU z(>-HhVJJbvjH9q)bT#Nr)Iiq|1FXEGi@R0aap{s!tWREqNxR!(S$$(VN~lT<#{^*LQ{r9IH@gdDn_Qut5a zi(C5@JTy|F>tq3#d^G}t3%=ku*ce7!ivrIF{a{Yryt;|c@`aAh?ci;89iD7TfcZYN zU~A|dI4C*JM&Ej{xc&?@mKvf#M_=rg_!&Mln4ou|JLX52p=J*`e5rl_wED?Hi~eZg zZ_r4}mYYG@A8u04&F9q3pKqAT5Iu*7m?!ag zpJ=vzwUuS<6ItDU4e#%p&Mo72^OX2RUUn^&{qnZ4{?P>1Xz}LHJJ#@9r_KC+t}C|& z%;NkETi#jh!LdVE@b7p}{`Y$gJ7unAB|BT5{=t-=-*aZacoRPUrJbBd|0bP$EzbP@ zkt(|<(5J=4^t&#c#HSWwRJgCO%f-y@s9}NdW9l}Loxd4|y!|X3(;E)gI*RHP*Jz5> z`ZjbV*q8iMGAXh07L}dt!P|m#86V-=kjqyYBvQcNXL)U5lw*tD|_dOX$ zU!kj4O~@#}Sv;0%FKE|HfHv>@ASL_&Po-KoZvF$n@hMn(OQC#=61vv5LuKn(_%cBs zIzI$}hWZugYvT>iSC$BlPBBpTHU{=*U9lT;z(zch(tZ?_-9@ivC~%wc0m^DQL8eQUc>SQ~)bmR`y$!rg z3ncYuf14ai2w6^1S6ZlY;v?!F7DT6n5Yg}7GqHErdhy+zQDE9*6wLi5C49Iw7A~(8 zVRL0HcuZUm9r8cmcB%sAUbzeY+vM@X;S-Q?u?0qs)yHQN-egja6;^#P!{C5vXn$%J zepNQXJ8cv3->49j+B6Rj8+u|`Vj`-q&c^u;D=^+q61#hHF>zoDP944!{RSmt?#dn5 zWgU>NprHiy&dX7k9gdeY;nm=yX(T$6c9&;n1G+9Cxr z&7`1bc@?M{Jpj?|1FY0;gl&oL(74DMMn|>^USfe*S2CCG#9ESvnlDXMzfV4yopkB? zBl@K%&Hd}G*mw0LKD5q;FK_@i{7vTI@*TXOHIa>zy?9B&B7U7XpI=rP^ZI*g>{i%7 zYtL^agHJM)zuZlTEj=ULeLEfgYMg^lVecS+>sN?Bvl9+Iu!L53J4n-agGD=U*=7Iz zE^dld7EM#6$;|%*HO$N)$WI~qQ7s0wsgT3{sgyFzN}MtDuW)k15J_2H2j+#3U}3od zZb&mk_Z16Ju6POhXl#53#*FD3d&4Wdz-wu)NJjUfCq9T~KV3YvS0{=Z)e+f?JBdsLfe%T(ytg=RMo& z8Xv}yr&$^4=1!sc25EHt<6j95{X^aQT%et&jA=q+tgz?RFz7qE9vWu$!|x+ZQL$bM z-@bSOi@q7+4Hrw)D*g$s`u(tKv@1^S=_Jt?HBcwm8jBvyK@S{-&%X}G$iq^&cZwE{ zsCLH>^Bhq9u@(9$DWUYhbFjnqJSd5L>?K zBK3FxQ_pULp3Pf@sLPpkvb|Q4v!xf!x67h!=Q=4+UXjBW{G_0A6V|!q%IONm+~32R zkIr7j6r9Lb*=r?wWjxCY!JH(!k=1{%<@4*7^L^*Z{CSfx$DFj`{WI;jc+(=zEeht} zE!jMCd?LH)EaXuqmhx`rEM6H^z>OQK*r(t;hutK$F*(issw+9NwSc{x3b@#IE6dL- zV42QxPFYpR^>BcTmu%;Zqyo zrVZlB0rEU<|4k~a-9dKy;^=Fz0rAxy;_+y8;n1`u;i%qnD08WS^AnH2vepua>8*lo zeFtOwrv6xy%1xF;|lRmNuGunBu{?$#Pyx#%cnFF1&DCsJ_N z!7ywNcf`nZ{qT}=6D<1S13S_t)~y?AMg8QD)3b5+s7qUgm2IAqjKLX_of}RM)%(%J zTY2K)9w~MT`rm}N$HT$odKP?5ItO2)m2i%<3Yw*={s&hMxhReEdSyYmYA$3R)dSPH z)8XxefpGTQBG|ZR7_4gWt6Qw|PAD}v02iuTKto7_KfMCMWKt_6gs9-oaEYeq^cvpY zGr?o1fy?*42jx#A&~xB)TsO`f121&PwZ}`~OS=lZZA%osD(cc|oJl2dcWBeQFZ8*r zlXN{~dFn@7R+=!8$4?)|9&uir`h6{5&UEF?hZb;>Oe#;Uh~nCJxx8;m3Y*W^z}F|F zGn(z;IW6g2csPw)2ITYdQ}I0F%N#y?YBkqC3g?qUk#j#z;Tp8&1y^Qr*XkhN;ONb; zegj5;so;XOp zk1#xL&Kb?|w!)kCbnrKigRzaW@Gi{)77MFtTk=+ke0l+0t4pTV>Vwq(TN~vC_GbC* zo%CXdE*om}=Dzo;NK@q@=`{Cax#qhxLME5)cD7RQu{UTWTG9PUuS7e)d4k_M2T)9S z1UFZFgl#@2!D7-E7!lVBYGbAG%oY{AZ_)uu$qmp`Wh6|^oejsOPQeU+2dGu65H7lJ zhENCt@c3+7e`rfxkHFp_(`%TpUH+6%S?MAE`z}xJu|#T{&(iL8JNoDUO$?iHM;IkJ z*0b1Bc>1XxuExCt!*hM{PO2$p-m}DyXo&M&^zmzxEw=e6;!uaE;Hv)UC--BbNr~B!ypJD&Vhs&2T?J z8P~3^hU?!SL+Mam{Lwl93&&WaXQL$^{W2YUcAtfFRE=?RhCQBLuo5>9T7!xEd{IRv z8QpZVaK`SXIMaM3s)$>#*X$%Lx)6x#C<*P#cA(Pic>Gqo6qknoA6#kP(M6M|T&L3+ ze$@DHw777Asc=?44a~hhL6t-|*!JNboUJ+pK?;XJ?(a{befWU7hJlt8`^1$No?b+T z50298*Ydn%oJ8aD=*BaCTe3&iG=88ui7U?AaPMym*hU?=Kxr)Z&XwVY&X=_RMh1l+ zbE5i!?P53c5L&LPQJWp@FLEkX*_R> zE>{o#PF3&Q$?Wl78l0m*LzDDK-oslEZvx0W?GT<@CJ9*vJHbH0Z68|F0-ir_K* zI5*Z0rn#uV)6d81JhgzbHkeX?p)nQ7oG10at(3itsp(HE?PxRP%Ns5E#1=E&GtQUa zr^c{*bRKUR9LFy2X0h9X1?)e55nrts!T!&ccgia{s;+7cBD^wx^OHZ#M_h zMiC4xx5m3X9)w zfRo1xz*4OpK8hCTJk=BR>V{!22{yd)b}?e9JAT@)kMRjESlF-{*Dtk4<0qrh`BM<) zRYu~Q?~W+DXD}vaBPP$9iSg$(F#Yi`3`udu)w}ia6t%*}=Kg3rU^to_dJHuSZ-LZF zbu?~$4$Y?P!Jz$ikB^zDEqcTM(0_GFS-H4jRb79i??n z0d#x7@!I`bFNM_-O=x)DY*0DtIsWF?B4OIWslrN6TlyT5O|#pFk{`s;)IVP+QT#_X z&_Fj-MpN(IH|><2RG}v5BrJQShSkod_+x<_ddR5?*##nus$bI`~d$B`h0r7EGELwq)7C zfXa3uVrUg4Mo6R8ooirL-VbdY+%Q30isO{*aNtM}{44QS%KImvmtqVWHtxpKu@x9K z=m`4#D9695ThQ;&7W^z7fi8W*vHN~|lo@D*mlw4`t5*pWHQp3tPW7@gSUHy-J4I88 z=YEn=Q)Zi&8obU$ig)a?;_BC4oN(Qe$;6J!dWN&+$ao%jQo>K^CG)C%5qxA;0-JY> z;vkI(?om3MjmJ*ns@-m!``3kg9bLnkgTh(6A(vmcO1yTr7jggAAWm`2;USBQcrn(n z`+^2mtUAwyea`VTRgu5V-N#a;#k?$fH?O~Nh#zmP83pS(dG!}92@>PDLIQG>?i z7uH?XF%r7`9te1UGb~s_VA**DlG^vdNhxLgk|4p|H??uVp6U3uUHPKM32}XVLg>N;nb-u-u zY5C21(oVfi&!c*9O4B0>Q$0y;cbC!&qrS9aUxN53x6SUsZACEavkn|bYy&xo#v^xF z9{<|+MtO}s|G||fuD3(o*aRrqn+4}1b)fqs59rF#gN}lw;CbYsVD2DqtLI@0D{E@t z&AM-}bMzLtA-fW?3}3{78hS=vuAH4sv1?CSLicSs=sQG#*&UpD1 zZtuu~)7LHui+a4Z6K+hPOt(3t5#3Bp@t?`g^E-{0-;+ybPT+t~6L{=b1D@#M#?v3K z=H>+syfM{<&4)*^`};7~o1DwRoXViLf!&v-aAar^Hy+I3FryScFg=$SPmANpKfL&Z z$13)3jF!|SGZ|G~c;~87d~>H8=bT=~S2xVzTdC_=?r|hn?U}@TVof+x&7SXkG2>3< zuatWEBem*ivRULG8Wx*ICkE^x>HC}Mtjun)d;3ni2}O(RCM4zvAL=vV`OOHJdHu3* zl9b_+d4Ew)H(HE%u!e3o=8?|LeYE&`3mumD6wJ*#Y5P_^9;vUw!@4S{@$W-22v_Gt z-WTarTM3=DkY=;(Hz>_*3N>ASDLRY#!m`xy5=?&wD#m?*0j|d&=|mfpbTz|L{cf1^ zTOJ!aJ3vrxgoRbMAY&2)PH)eHvXUE^-a9PJI2{ek-$sLvdV<}1`?GZ)9u5a#+dkpi z$ZFx7tfjcIT#M=@x&EQw1u}OXLlG`@;`#lmF!PH))HGE?!?DY-;e7{Kzt+a9f6UN3 z+zcoIBfU!+8p$s>{zve>7llZIBZpov6a~a6o4TYHpg9Tsb zC~>3Cs=7HZT*;to9bH~lNz$asPdCf+$)Mdd=2I~(_Aj6(uiubVA1S_Wl|`PJC#ZO% zBCnBarlM}~wD0*7+MX-LcfFIT9zw|G)NN|s^px_yhf&Lz5YaI=Rh-jpidg=~0k#xa zgWXzdVMX`xaL(yGe2mxuf<`RlbpHthjMOl7>odr5QpJp-i!emzB?J}^#Bf*1{ZH2% z+rOEkdW|bC-Q|M5J|<{&67jOsO8mVl6!X&O;?H-<7$uj9BWwLpDJm3~4BLXkqZ4rc z5P#gTCjsT13o!IdELvpxO92p%f&V@VwZ@0Mw*_cdKsbHLilF=@Cur)5-fWn!%=v%2Xxb!G&VlKC zVBJKXuwWcV%w5boY>;m}9?#E`_*FU&UY6*<6cy*XV);p=KXw@P z2^>jBt1i)irEkb{*LC{(>@iKT9KxYjtl4y$1=~pZ@U^`$?DJtOFDr}T+v;vyDZ$E* z{O7X_4dYH{C4SLyg-ma+B!$0C;+aW$LeSK$LVkiRWcNM|KQDJe>Kz%pA8-O(zBt1Q zNnO9L$RAu@4G>gw8^uvc#-dYfPb#iBO53~^QD@o)3VfR&o(q8SMx_|w#x+(rUQY|9Z0#iY#aw+o5eJR{> zoCBLLUV**ZTj0>?*+SFu{dEz3LG;RREA@}vN~cy_7qcBTsDJcXdbp*Fo(}HCKiVd9 z*$p4AkG5iig(LWL*E{Nrj-mo7DcaH6W%tsxur_YR5uw+^&7f1Z9eO@}2Y#o_FgkCh zMC%=nx6RFQlIcjZG+IBt#H-yI-H--3a2kt!m;;V&|SF_td-?Jt~^P+v7#5<7`Iv2 zeLO?>@Vwz6}2Y)_vzp8X%=%742MUDr;*ac}zL z`=sue@?pLAqZCRz%G4qQ!FDbol zV1fp1yVp&u2)Be>eN8yMXs82P_XYqX=BnUa>8I5W4VyVTzo>ACx6pyhsRXQ6853`Sq#6ipCu+^m#qTY?h{Hb|Uf}uF{C`ip21Kx+^;8gr6&|Cc- zlt-R}QSIs|+kFNKol9^=ETF^{i(Y9dDE3LjMHq*QsfGCQ%rX2jpbSIK9>bRKJV{*T z;Q7$?sI+Vi&NY~fM~a4G+oxx+{L+3%tZEc&EC$=1446Y7S1%#e-n*!^tQqUopPS>V=p2 z;H(R5n0JILb|2z{(firu<_SI_eUbITkMp+j8ouj!m@DFndE2lQUKQ-e%Qe0D5t(v_ z*;GFDWfc$j9?BO7drN$qqd4xGHs9{kgO{W?(kzc1wD{TvIv>%Ge$_9mGw|ys{7PyO zg2G~;$C5Ir9K8qHwx5D1CrzA@H3)xv8Gvsr9r5hqrMT|eTzp^>fb(0@(ND-nU)^kM zcwUGP8f&oaT@`-0SBjN`GSGE+7)IzhmShd#3I@jVMC+pq-g&NPjU0l4Z>b~0~k0b5#(eJLD!N;@V>k+uJqT&`J0CP zC-3o)4>0=YCJ3}n0rx(t5IKGh7;6s(FA9RyMm+(C54Y{sKC=ViPq+Kc-$z^Vz=pQx!|26gAsxIkcXs7(e zi8M0qC|UH4BgJ|jF?QxQJ7vr6;ydq^!oj^ckY2bBHodGA)~@^`sE>YEcP=Mg)J}<| z#cy(H>6OD|dGISGX36sn+0Uf^N|)~rQsTJ#<+P&vV>-RF4?BIiKzGjWCM)Z&G@<+~ zm9~$f*N!dX-pPFg|J4YG1Mb1~3$Nj{eg$lM`wntq@4=t#U7+?(3I9a>f&qZKd7PifeXS9|K^|+>e6egR{cK`&2jTsVs$E zK1oxv?$fQKv&iwwP4UXz4q>*eGqg=V1OE3P!^8pICBNZFoZJGa=wOZKlXUT##LKUJ z6;WQ|wR8%94#hsMVE9tv$L&f7b9ZShF8Tq_74pGx={(3Nud}PzS|JWqj;KqKnNKaZ zR?)oSC#mN_bq+LE;0%KU6zp`0vR`D=_DdaPb*4M-H_D;Wxku=xK@Zm1agA)H*HPv9 z`_z8%E5%B5qpH($XzJb@lz)XOz+0QD8{EZvWm2@Oqf~sXsRg6%_6NZlH5!(1m(tGAmVTy@AM&eb8Ht^76 z8V0pmVA3%+ynQtq$E=IPfbT)5Xq=9>#aw*n5{fa?ea^xbMOOl#g=70Cay~z1PDz@h-ko?H;MQ>3#a-y)=Hw)A|A42qmUl4Mw5=MTh zgl?~Q!kV=$f=S`2+8uA*Xi`lyd7KC*745Th)k}q6)hTdaS6S9SIEE((v-qI41FI$> zhaX$cv+mjR{Hh7OZh|6L-}pw^srj@T+-b|uc=6?-`4n;V7TuJsp!=@1+|C zl6E~l+<<)HK`?h#t>G}7%aK3adB+elzLnfX&m8~I=;bxE#9g1}t@EYy2}8ugI~5>j zwx@9O>hZdPQ4=8}xfn+8XoOUagP^x_43zE*f+vSH;P9c;x`VS^=~Bx`+MRArkLFw^ zE6dN+`@<#rxBWgP=nUj1!DF~R$&!6l7xJX`I6m_!hbPBH^WlDuoaN)iw)tKxl{J(f zFHzv{3fE|>u$;6O>=vUQZxAd8mJ20g7lU8JWw;*E0pTb9z$lvv_$g&4(WE83sbU~B zPc{*{eZC>q#v6;HJA2Z@s4_CxznmUwOT2R3)5WNh-$bn@3lf+06Z^NzL3Db5XoS;X zUwavj>q>f4BJ^>zv@b3U55RYi*J7ilJr2^=#|>7a(NWTi`8Vn@JaoPTTA6>Kc4r-2 zxMT@SPV7GObvRI>OR#MJvGBpm-h7t?yNeESqG95ILYnj5m1 zKZx(E-lXS(C!y4JvBz2+q0IGuSX5^T**F34YeS9fD@fV=&x850fJ1;LIt0m=d9fpN^Vf z-f0iKt|IZo6#fJ&lYtmDWh6f6cn!r}cfiI$2gm=F!R;FlfUS`=$fdUl>)ZywtSvi0 z?^Pb`33?4-KN$uX--i)rj=+k9w{W;%4?KyGfe2bIQ+Jx2lHw^ z_z!(622*@YH( zuN0@Q@)TG5Y^D84nd$9tt&0)+`C0~r&Mhq?{OnEejk8? zXPcniG$kB$_bY5VZ-qt2$D_>DUg+6(D0aE|qOJ5Y9IR!Ht~p+4Dn_E)X;<`L=zt?F z24J|v8?@%%5;S@+3$c9GAnV>GwcP(Z*Rd zv+zToP%K>QfUjH^V%+F-ybza!wTlz6vD;p(w68+zUB^+ryb5R3Zo?75Td=V#9CwZn zN1c09vH0>ZEIQi)d-AsdpD7VGt*@@#r?iO9&zeJbpYEY_cS#>?q#8evlIB4XX1q?y zgFn8s=8huZ5j)oL!0-ev=pW81ztf3^wc;#zhkSq{(+T|ExI7*=i)|Z^lC;kxUcxJKzFXx{b-rP^Jwq5qqxh!lg+jy+y zSp(xTl`cMWq0WHHlg9W7rYEV2wBQ| z;bBTWSS}fW0~`nA!Hm8*aKuF1x_dFM?dy$J8IpK?o`P*bxj4Wo1=sH{z_nwmP(7~{ zUm6_3!q7By+!}^kR!&Bq(GO1>-vZ}R?l45{c-`@APnz#Y5^tPDBdqPl8a*!1w`VzI zHQAghY^ucW;AH#nMUmiZXai#uw}NNM2{^R>EoeRKk0*8v!4FNQ|H*r^YVJYcP&cT` zi~;qby}{Al8E(k`68c6=g`Z=kLGMz!-Sl`*(6qe>Zx46FpGmvnZd3vczSRMKZK}9> z=QS8ABZJ3}S>UcHbxe=@0Of)qE_~{Y6=6eh-5p5}xh?lVkWDk zr0};9>siJunFsvd!<#qe@}%N)4x3ZJZl1~Praq6$Zmngh$4P7y8pver!<9PYd35z` zcFI}IW>!8tOldO*`E1}|mq~0m!IZ`7iEL^$j3<@6qj`SssP7LA9=)^cf01_He?9*1 z`;UqiQJR#N6&a;SdS2%#L}ZUtNXklCMMkL?C7bZQ zKEJ%af57{zN=DhV^w<#!nEsKDRcf+@q>DygGvPl?o!G{v zm^Mjs$&^)ET$s+3s+dKqKkm9Cl^@5Oyb~LW^e%XeFGHrPv-n zw?EPvCVwgy>W^L&F85z1zd5}q;J! zdI#*dz7P61jzJ@7lzc7B1dU3Kapd{&Xu852Z)$vjC!NBe^!!RNnwAQabQJMl zCqb9ZqadMkn5-(fO!R*3BENoNI+@vSqL3~}soer?UYVxGty}k!?Y|S0xg?VcDq1LM zUOQHZNujD=#grPT!d-qmpuck@f8anJwPk&!;#+ITW#|gh{qmS5K9~3!Kf95^DI;-P zr6MISJS7Gx89>%B9oS!cTsHAxclfAw1a_oHfn}of89iH|ZL1op=DvhJd(_Y^zYJog z--1`R`uJYzEvgN-M#F1^@l~J6csY9psss!{Gw10Tx-1k&4+}-N>kDyTuXt3Omxc)= zR^i*yK-{n-9s3)_qupSCjCd4>5wHuhTDIYqrvVuFI_AIg^=`8Q-wLmy6-)f-_So6t zmk)ZvyWp)*Ip`Lwgm2(rR}4oYGhuaN0#xu#q3}qVy#A~QnLH1p@j2_LUk|2JQa0Ux zjRyN0{H2w(L%1qzD(^IN;tvTUxcivJ+;(^rJN=Wf@iBGI)cQzaZ!;)ppfBZUo)YC( zmeY^Y2UPj{3~gL`fu=px<)Q(-SX<4U?b|u<%Ucrj!hH?5U%8a+t322tau`4B-hl%~ zDDzj1Dq5f;qsL0iX>_8ksClv-w3zi2zNA;nADfH;kraSLJPS0b~|FfI1uWBFD}b=-23G`jmc-4zIpXAJt7*x!jJew%T&H5kB0I6~z-D z?&PTBQJnXFDjS`h&$l1>@Ugo7d`mh%zw#<6AH1l;-2-BdQoazq?U-Qs-W@8VF2TWX zKcGNQS@J>3;NJshxVm)?OeJ%(;=Hxid!ZU=X}Yr}`i!(ixi77R6X@Lz-p4#}N`FJtE82j5k=#zlr2b9-Wz z*Dy3HF-BkeTd>Gd(o?$>a7OKEFmCGuU1P#!L25(kl1?ePo1LJxuH9I+Ski!d75M3x zCVI7QHSOyAU98^vM9A=xJoV1!z-^O1bf|sd`2B0G{D59C)i22-ttX49@W@gzy!N=b zC3QDBSv{kpfl6%rRp5>frtrQDa~@IBmG?dHXq=~$A{2c z(**y{QpaH@ilErk0luvHEJT}Hz_J-x@I5XJ8us3YFN;bb`{6C9m-48ktxb|IkqN3+ zyM=8ARid`LH63_!O}6Mml3=USliYV!i!LS)1l1nR^33qb6mQv4>~)}@P~vGyD=r=R zpLM0vqRIHzeLP+&Hp9`P3Lfr}57R<(Ah|&Y?tFMR6j1J2}BoIv=$le_)K8tnD!UXBYf6TM>s@3TQdr37;f%!E*0j_|x7S zPt5Sc=hsJI(nfbouG)mNX*%ZI9D^UP8ROdbws`wb0IElLVrI%HoVVE!N6dH(@BMFr zQiBgnFgJ(f{*`cGqcT2Etc5wQJ+N`gEc`iYB_^+Q#wma1BHWKd(?{_bus9yo|Lw=( z{^fYIyaWq}mSF*9Jh30n?ED&L=IjKHP8D`F&6PW-ZKMll z*Hc-|ekz};&cP#e*)mk>eFhKVW398e)5b9@D{$pbVG%rPNi1L8u!SRj?Btx8$-LJ! zk5m6`=i6`7_?$&F>tqM8XHf)CI~2l7w_S-j?})Cq3f#4f&Z9CctnTLhKz zr_fvcyyFWVHS7)_vb)cHr-|dE|Ntr;;OJG!u)Hge9hVO3Z;ib|sIO5P^y!+V; z`+Zr68Zim@Y(P4Czm3PwGjh=L`6M9H5I#)F~Gcx;t6u9$ogJ+Vc7t;HGx~a*y1E zuMfV0lamQ1EA_)hW$XX$y@~&xz^7xr@Nr}e{7h>Pt-YMUHRrn!5)%XpvqwX70SK*m zqaauLB7_b43?`N_kYXi0C)A`qazZzppmGD=`M1YeI<{!9sE312wZcU|YwU1oGWsQ$ zV`fV$EIghLUV85YO}k67F4kjc+FpM$Jl{wUI<#lwr|mgN-jNkMjpoii<5=bE0B+hm zn`3)#=BP4vj;No?hl0ZRQL-=hu}b1?35h%=ID}(f#qplqdn86wD!=G0F{aWoC0{U( zU+wo|&*N)&>Vr5wwb_^3Hq7RaS|eCi=EX66m$I&W0U!7f$(8z>`GLb2wotRUJD%^ug5Qz{-mFdiL}A3m{=u_?7VWtdl&s>!GHRR$8KzsO;QU1x06y2 z`$&e+I$Z_4-rI{U7te@F329VxvWQv^9iiM&pJ{!y7N4^JM|Yb0@H%~c9(22crUlef zhb%o#Z*?^(A+NR-e^!{eTWM_52Ci{im9)u`9W5|0rJhc2!s;b%k_#Gh~f@34PnE;~hmy z+-g4o2k4K%ikSYmN8$}-x{XDAZ-loLT0oY#8dN0q%$}|3aLiN@d1*Za7KK29&RkHM zs_E!pdQtp&P@$qxX(9!_kD{$l&e7iwdYrgRMd~x}qXX~HQ~PeYWWVAE$vxWfZ;f~w zx2lM~6)Lg*29e~CyeN1<6-9Pzq6C*^^q@47Qp%qb-#kN#wO>U0D|zCW8=XjLVUc+9 zh#{Q1stx^H#>o8k8^E&qqhPWy5@?Y>oS*v^`e2w9qxI(6JNcq z$o0iik2+*3SI!v4q1rP3Yvs#FhRgWho8jy;w*!}rY@*@u+v&2;T=KUp5YJ?-rw-Z= zXhCcVRd21Jz;WH!Y>x%ID_U^VglJk)LWhc~nPlqnnTjBIET~M}) zmv24qM(+ojlBLu|m|9#xR>6&QZTVUH`tUa0G&N>@V;gCvFqp&Nd2|2qTlt1X8t2!9 zvC>%=hD>*U@^UU8t?tdf1JwAk-ZfGR4We^hO2js~qTqKWU#M1ahdQP6aO3B1$XTF- z5$z;h8706H&1vxDt2YdH{vh-J`&9h%r$|0{&q;CgqrG&dXeynw-9Y)zQbbh`P5OO! zIL-U^Lf)m^5P~Xtz}WDkaHq#PSSaY=tcS*U^3qH+)$u{gEo<=CJSWU>>WRAZt?*+% zbDVYeCJcL71!J8RFuB`xu=5@UXU0azt_mY)zx*WalAWWOdkuKSk^%f${}1W^{Z2o> zY^A{OooQwMQQ@WgNa!%}4D{RR4g;5^RJ10w$^RW*M{AE~(AZuh=;6+C`L<)D#m+aA z=*6x&s%&b{{WO59KaJz#S(z`Pi1EK;0;!KaOJORO2z_u8c?T%f|0(cFTUchg7ZN^X!G<9cn_U@!O zB#BPg%on{*RLgG8I3$jHW-6HU*$6YnPJ#I$1~6d#VbDGM)G3SJP?O9`kM@4To2({$>5uEN{Eir2HTmB1^edq)Tek2sWjAzE3!vW)3U2n_UjpqlAfKT zqD@HAs!^tOZ!#SAdIG((dSX(kBObjYF$-r(%(e4?agPx795wJxj5#hII}cwiT8^9c zIpEn!sl(k6g3}VDIrcJFyf(fsCLbG#zgMhB^%h@@kg};;pXlO?b+yoU>}@d3T?ShU z2Sdi~TVQsm13r^9Ma52OMjJF62OL|CMcrI*-bG)`JCua``zK<~t~k8vCH;_2WvICS zG+N}9qwd`te6%qG6&{9S>&_6Y?Cy%5@B8DYPw!#+xr3l{b6a|z{{X2~3%7Q+oIcJtY?RG!|vnay6u@s{}q`Fs2s{`>n5 zZ)|Ji7TY@RwB$Z_3w+G)&s}E!^o#sw^(Fqc=nj|Mf6j#(HJp0ChR5};;ORY1^5WT< zd?$S~51+VzhdBW^{qbabhcJ##4&nDp7qXtG1K+;fo4dW~%z4fqXyL$8nke=6?j0RL z9fb_BYO1SzkY%FqzCs4dxj9nbbQc^PbrFvE>f_CcW|*g9fWKTvYp#*Ivp2FgtMd*~Af;&|<aEH2>P8t^t-VO|Qs2=7{bKrUy;|zYX_CFdE^%~diR|6A-$MIdQntNkF@%h(20N)O z*!JB7mkb+(3rp<&yZ6-PkD=GcC9qXF4uTD}Ky&dJ*nUg+Rjz;uqq+=g6*~>zW_kPl5)6Nt5$E?ZxtHzGAJm>K! zqexCET)+-*+}UpXR(|8Yj`#0REo*|eBn(C1yz}F@9kYUsY59}p7exF zI_Yq!TBYRU?WgqL3Ou*^0=;Q+qV)V)v0Bpd!z%|u!^zvQvi}R{)N~MbPOXP=i)x|& z#Xq2XOcM(>DdVH_FTi*~IBaV;4qG}l!R5CbVCpXu_!@8+{wzNZm-^KSqxbF<%C2t( ztqO@p^rStk{NW;hQ=md)tV^hT>s=b_<>Y1Cjdch9qU*~OB$iG#39f;3zEGNV7rv$5B|+3T zc^`R9QsO&{FOYWE0Q$JOmdq2HXo;|tk_z3ad&2`7K3Ps({X0-vb%LlC4^js0zBtyo zO}J%sSs0z|UgVp5~QsD*ZDo^GJq=R0*T%x=Hy) zb!?w92xomAh;K(tMD3T;(c#rFJn&g!%?^#gb8!*)Jwjrt9Zbb}#u@n3D+n9UZbXk& zJF!(?V)>RV#j;DWc(-c~DxQhN1Lv0Gi9`Q?4t{=iJ6@GQ)M~zzs;s@l^f}*TYl5P| z+qw=?a@wJ;Ulmjr?~!^uad0j-O$h$-Q6AoLDFy#qPZ{xHG_;dQCJ9}5%G*wyX5WtY zCfc%7?F=@)Fp9a&mf!B3&z%=K@c3=RB#uT0PCWCLcI51!yX_Z~K}4Il<4Z23^k^b2 zy%K8mzDiFOjd<4!iJA7TFIO*i;(?t5`Oe4<++gO*bE~GX-tJx;Q2USe9#LWas`K

TCUWh0xJFb2+?nhM6-+9@ZKd0)`Up$q`#SvdIn&j-zpg4qYBw4+KExS z=F*+izEpFpKP{}hN^A4plIqe5y8Y%J1|=U^#?`rjUGs8 zJATTKr>H~yMiW^5{RGrKJp;T>8$E2gVpit~xZckTwZ<>UV~^}`m5njBO&^8>J$g%g zxLUZcQ3>Ou{Kvt(N;sN49PHY1Wy>B~(j4Ik+0HvgTe|A-?yO$yIj@=ek9|Y2UBb!s zU3*gfbY2)9>I=_W%HWpX7#MvsUtZ8qATQHjLZ*ocq&avHHCGKAxzGBQeDdQ++MIEp z#wxVu!yx0=Gske+$}Sw&(2jR)NurL2m8qX@#JLvZ=R#!SWtqB3yzo;c6dLs6;9`$A zU^a0u`uCZHx7QhCZ>>JK!(tvzZXAVwZ45BX(-{K_y>RvbOZ+~k4?4f~!qA@X()ahq zTq(O1ztIgNCC_PqiV~9F6-S<|5L`wXkg(?H|Ewzyq)kKn z!l^iEb1$sTRm7-8>99#X75pw}!ga+=@%a_$|95&Rt?jsu?#}5=@3V8nE4pLpdAleI zQP2~E`xpsEpTfnm=~;r(s~AuKf0!>D46AKKcr#cHv-*92AHiQizM&AN{F(xDjpAU! z^s_>twkIU)x*(gg`GGig?GB-Ibdb<}-#}{fxhVGRmmw^Bbw=EqxR{nYdx(z*n+rwp z8$tc`8E8uT2_X^r@I<3o5PRx@%YuACt*{Gu`G?cq7w5zsC-lg7#d*q-eDQxX4v|Kx z0d*`pBdbf83BUd7pvP`ATyHfT-KEawyhJ5jA)x~5QpRGVe>e2F-3N5RG8yJKC@GkAWb1}aLupeA7e zTnnp)7*%DQt#cC!taP!V*cJB{`(s1mWOS$qz*VnP(NrZ7AYBX`QT=U}IKY*sjlqo;WAt*UK2`u%opl(gY=*F91;Ih(7W7V)s`e9j%0 z#}iU=Iap^a$4J@%_2PN9&rbF{xt&qx06(6S&c&M}x$JNJcg}b_5@HlCPTz{tvp?How z_FSNZ>;^hlw~JCYE}{wf-^DwtzSU> zaw6+vA2bT|C5h;p=c^?0~~j%|i#{zBGfk@*vEQoP=lRT43~~FQ79r87%J~ z5kA)02|442(C7=H)L_y;brQqz_Gwj~f1?Zc9XXz72f6a$Q8qk%kSBZeh~$jmrR*c+ z)RmXT@(JY#wm+Z6!MCE>EiareX{PYNoBQ}(cq(^risR0QrS}n~B(5Lm!;il%<$vz+ z{K+bkKMeAb{$G$MsVv|HpB8eflRM`oZ{a6TLfKrpS0kqmXSbnJx98v};IZa^3)agHxOehH>psBlV~kto{nN$JW5Xq;N>gNP(FKQs=s*9v+mgh18)7Kx09SY}V2|(P!Eu z`J0X|RJ=T#E@2s2Z_wnQM_Xu|Ml{`Nzk}LUc+)ulhvaJ7Kz(g|sVP01;X&BNo4BNJaqf?O|`tMJ{F4hveVRMC*tJ*Gf9Ffhw1u>KRnX$%DKzv$ck%X-9)fd2Jd7`T31WjP`doMb z8}DR5(piZQYjR&0x2mQ>Ww?}y^jS`gQ@vFU6bP~=)Hl zw+rKXrO6V)pUSliUkY=(Bl zxJafAO|)|H6`CvgDJ_G0@#18G519_*^CrtBo_j2pDW&totS}xNGeOFuc(S~C4)1$r z!FBHH{9d>~SGvrlH{VZ+Hx>H{OXeRHc3fQu&lS&u;PnqqZcxL1>D93O@g&Gz;{%Bw z!eIETQ^JP3JH($S+sjuvsL(aP<20mjI`!H-g<7^cikDy4iNojgm3aLH@+k|nK>Kko zC^&H%9!bH+hlHavPgPgW6i*+KVt4 z976Em@&qUzwNUP0q2zGssuvBdkEebrCKR9F&#^<$czLD1lreU>Lh;c*N#%Lr!iV?aa&tJuPq-oLu)GJ|KjkK|O$sV>W+({9 zt~rslM@RD8u|xQCE?8W?Z4O<$I$HkJ+fkUY%7oh07yi$>a=+em9QS$(j%#m$!96=- z$i!?gewYD8Y(w*Mhn3?>d+z+r@i z&~urG_-}15I6m}_@U;4)sJPh!KJ078^w;)rf}A?#j-$4Efm5E>bSi ziQiTFaQw50Y^CnWQxl_k+QS_@wr?VD8@itjT6ggY^W*&f)_#5=9N-~;3V6=dcs?~K zjh8wk@WOUkTy=jt`^yjV({I^4aYhVpciq9;haKnFc%F^VJ?1$(Kk%B3^}MIoa~|pN zhOY>BIs4WPPJVZfPyKtz^TvJQyhks17C&W|!hdxQi=`qNLBch{Jb`=1kW` ze0^Lbugi$w23*XQ=>iYb9mM|0T6|~dXVUI*nieI*kyj5}T43fSj_uRM(X^BW?O~3v zXwpuIiP{MRJxW0E?uJIjrg%Y`gZCdZ1`VY-_#tO+^n2}xFYA)s~FgQhR`Z8wZ1J`1&f*w!>Hj;!A`Y3syr~md-{X0+qL2U-FtO9 z4G^(s8MK_=245wv`=f@@a3jAH6i-|SmeKam_gR(fi%)O(v{4TI_O-)D{qo`Q_YBCF z^0~{+d*G|3cfdqc#w|^DSbb9mdoKF~1GZV=jD!jJ(!>N8uloX5zNA82+g0JuO%*}7 z>qv@y6Ua&RH9cLZ!Kzo(c&(|VvGz^l;qI=S+BlpQZ!BhoUE4U{dNm8_{(Q19m4{<2 zKi1vN!3v4|>2fsBKa{~nuMe{Ek1Tc!Pv-M0@_4d!I&Vo@$&JdZ*tRW+`9%y*4Oqc0 zTV1*M(sE8)yoe)v&g0LEBl(9-7(2}$%byGf^WC6PJfyxKPmFs@<2$#~u@MIR%BYzx zb&aP*Ne5}g`VCZ~VkjEkBH75v=j20&`wGrlNid*tA`FN*Dm*DshLSmp#mda9qMO9W zi~hWWqS}trrPj}6{#To=sulQEac_RD+KuNdK1(@DZ|O)BV9<;Wu=Vf)co274W>&CT?3UR{c1y}cEy$io zL%SByKbMX?@6dBnxU!ylC8g2iUlONpyu=RrbdQXFkD|1O7*e>`L^8X*l(KR(U9-AD z4-6aWes2%@b;*l*#SV{BJF`qBNP>BbcIXp|s95Wp0ql*`xY2q7wD|G*8g>%$gaQ(&^ zxWgRKdcH3{(TYVogLvF-x)%35OT~!`a&UZ62x|Il!l%)h=F1Tc(B!CRKxZz%2UQxq!3Zrpk4|kk4Zz+yAVuMST7@{oG4zHK@#8E@5ptI2}xT&gwv3L_E>IpFK z$85n!w<`^pmQHXah59a6=9GEexi$Aa1q^vb!B5xI)2v$2?s~m&yfhMQKAwdYp#mh2 z?I4<2t(BXNb0=ZZ7W%lp9eJcj3N2f@3qNYdQs_b=$3achVZ|WcYi7r#4(&Ox_^q^S zA%L#-EEP}9^bzK+SA$~?fx;={I^lJv6j(hgAFh7=1Lj%|m~>_q+8*zX%Osw)SKC6I zTsIcmX7)sdX|C8y+Xq*@7=#aF zTIo)yNAy_Qt8)@&Nj=XoKXYKI>0L0b*$jvJy^*y)*N!UE+{n&0+i{jfZ^3Py9htj) z6#E9B6^hmMMVZ4)QkmE+KW8;um}#L+p>O_oU3vT6Otf7w3u|M0V^fO?9{-jPx3w~% z)56Ds=C^C&n2IfQN9h{*M~2W7#Z5G({-Nk7cu`Ah6h*A;C#tL%BJ{|O72C$92z!#E zp=sa1 zVbknRFnng3NQ1w~yw*m@z9=dJ-7l|RF!b&WCPSPz73ZT_(6ke zBs|zGgPsezi6v>y)YoCRcq3v6`Rd)HCV36bf4`S*w6~-a%~~>bLr+jEdII(zEU;#( z3@e(=aFwna2J4QJ`k1ch>ZOZDdc832yEoFW0IV4Tcpc_Ry_pEy`+r=aH8WA;ejf~- zJPMyp*nj~a=i(4!7o6qa6EAjYguchBV2{rvFt!>5^~KV=-3_S&Wq1P~{m@6Nz0+~m z+vT`d#RY%uUW6apk|a)Q0+!s1LnuFhp|{JhM(GsZoOcQvDtBT=hn;BuYBTyz-GmxS z6L58NA8ajpA@$?3V5eh|;5OJvKIqwcs)-4sX(NbUcxv;VKs}z^PlJCs$oS1iPj>t` zf!}@i(vCrSO)U+3b{(%7LAB z@z<0zUK~@z7b|l3QC%#TpGfBcvre*h^(8)ccI7L+Gq8bi*L9wzDY9klZjQEy;dLPaJTz+}FS+2y{gk5k)YWjF zuI=nQ&EzOOBOgr2CKp$Cy3}+*4A|U7zD;euu%xRw*v?OZy+d+f zp2Sp((lNjfYm9K1Z1;u`VX?LGpnwt#fStg%xvVqaTVleOZp`851+)2W0rKIr<$UaW9M|6o<~io8cu#m58wDkBuvG!yj7#9F zSK>HGc^5bCIL?QIa(L5~EH->rz)9mJ&oXEoSAST;eGPZ=NTk zFPUumi{hF-?JAt3hlq+~D7ZO=OMbLFoGacd_(6-H`8QTfZg*as?*mlu?{+-Q z{}K5sEv2-wJo8z!PnvnNWUM0|K)0q)6- z#YCe->~d%$Y8_9*>I>O;BV;4`rEJFP+6=6XiO0%^%kkCPczky_2j}dJ!ozE%UPRND z|IXJ5%U?85n%TU{olgqGCy0q*1+o;AEl}ux5j4O50awH8V4R!-^_nYSt4@h*d3>sv z9GgPFJW^=&@pu{(a*y0q^f~Lh0joK6=J>8-_|*CZyw%B>%LX{HXW&W>N*Kr6RY$Sv z(JtJ3$alK!e}V!;X{T|`JbL-Bfpk%c8!kO3yPs-2N_6B8rySW*%4F^pmT`2qXinJ~ z#-F83@^gv7wWYHRb*hOn z+)2dP(F_M7H@&NT}kjmxF{U9vJr#lZk1)b z<%uPw8I)1wLc{#W(5U!}@^e3ri54;Xbp3gpeA_%d80~HVgQJfE4?6|fL0z!)oGN~h z{5%`M0|m*)D=8R?ehNmY9%qlL$^G!piu+Kr^&$8qXkbCqBZ!!?5Yl^B3q?vdMDsTh zq&sOFZNr~5wpyQOmb{?_@1N0Wy+t&xwo051^}?LH8=&m7q!}iUgAdz6<)_ru<+|bH zC?j+|eV+3~%%ifa=0 z3jOA&LA>*9L9y|KaB^cD_(v7NBuiyn`awWllbINBwHIc4N*So!1vu-|SUlgqJB~T; zf`4W&!mO);s#xNsu4d0F%aD z0{sm&@cC;#T$9y7m;KwpQz=hyo?9bMxH*bOXl;|N+-@t_1=|t2eG(hJk^~dWO=9gH zcd9?2CKkz~gq(Bg)HuKBfA%X|Ud_f!_h#ab{{8UH-%ePNau6=JX2AckJuciL@tCSV z9aFkUnm>c+n#*9?cR5~sU^$M|QX^@`7X`6XeJ5dVyHVn%z4wI3_S?ZUYcmX0TLg&> zHL!D6H*|g09(!3UV3hSq@G0~H2c0#L^yjhA$srP~!)^*WK~avzD?H&x>~N^trXt#9 zZxE87tblI{8-%fA_K08i-xHF*j)Ve-8=&;LBN~JnNu2Au;G7c+@@@OUYTiOA!+zOu zeW?-Utceztx@*$UyHbujx12mSxYL>MU1?-aw(PCC2Y7zG4Z>J6T#^SkBDgQUUD8F` z3+953%@V&fMh_!;^~0+!KKMFh6@HL>gL;Dn`2B7KM!%kqX}hPPenCI%rsIS`CpV#8 zpM`jE!6a<`V2FQ@KZ7rB)v(FU3tDqWf{%-|8%Nq39(waUlx&pze%sMlq`DY;KXk?~ z(wzO>xFkHGunmuYiNynlq&+*HrIHU?isSZ`qC$Q)HhtfT?Z<{m(6lgIl0O0SHul5K zTi-(Kn7uHb9taPAU#?J@v6Sv+$549b%hb3^+8qXl{Afrg<}S!dPv^7F<#B9Z@5y`i z#xUt@XYc1JoIkmUw{JYadtaWBwBAu(c1kYRpT`$wNHfnV1>9@)cCNKP zz^4szIQV27PtD8ZmOo{j+(iVpVx(RIxmJn0D6WpA0qnyQS4VwdK$JzR9vMX z{&?jrcsN=@{-H?7@i-0i>j^B~q>44Fy`*`(4Squ5zk6@N*GFJm?gkTT*TAmWr-F;+ z0H`_G8EP+VhMNzk!pwCjwCQ+*nsi=jMs~zIfu~@y<9^Wl*BNyxdSaRPQ>d)hz^-8< z@#|eZ)a?CHnyU}MgY8|h>aw}S1N;e>oHOCuvnC;YPO>o3SC>?MDcANGB|rEn9CZIS`MoJ8N2N|Y;`c`iUlB!tC;QRyz}SjAiS6J; zbP4S9xetjphXC3=hrS!`!OOKBaJy~~?6j~Gy3P3s>h7uVOX^PDNLR$PHM!8s&L1$k z4szYDL*lvqu={{5WUQ2ftlN6HKB_;w8$DBK&WRHLy-TIj`f^hJ-jfa-P8T06=?=vL? zaHqb5lq>uK7aWpc|E^^cQ@yL8GozQNf8d?$le;FJeX2u$6{0C%=qECcxIwE!Ceeqj zNz%KS4_$3nO9fTeCB8;4`g>*r&C;tQ7E|b+e-}zEFQi1p8XCGDDJ*p`-JW}q9wr~4 zz&WMjQp;QN*^2FH?e*j0t*N>&eqIMynve3N_>NGNTL|tWePQb}JLtUmHW;7y4aIO0 zPELFcoA2%bjlLrIRcc9mH6@JK8i08UmN++d0y++xg*!DyN;{$!;Ni`&IPO9$I;{&s zZKEvIzq|{x?{7wf$}MPUnTtE!5^?m-H5hUv89TP*;+EfA@PfDqKfR3j?|f~Z^O^d4 z$jR~83@YkqD1P6rCLC~zfr`^tz@qLuOxG=e8ox|X3*82N`z8q1JEn*q-*2Xst>NTy zIf>dH+$Fkd$a8jUahbIuC(pLy(BIQ}uN83ccHp$`%bDWGa(13Gi&CDizx+4(d^tpQ zGva7b&6H{Z@IB+Yg*RAItp>vqR zhH~A=t{mRql;4hcPDjS((Y(59vIz?&NC_7Aq{fOz2aFTj-;$WiM)SZ}HyYk{a)7Vi za>44^Y)IGEhe3-v3eGb}(cGg0$cgQ#|C!r#KL0J%bQbCUvubL6Wyr^$+3+e;i48Jo z0grvXjXgf^GBx9C4QCb(Z2u>G1m8&5cb>zl(SM~8!`(vG=ju;yDmoNn%fb*&HKN|Z0m*Lfh=7F3C8ZDG`~Dp=aH`-z@! z(c*)hA5%a;HEA|4r2I97;*XTag7F)FsB|uaj`z(VkF7<8;mhUSt!L0C4KIp(d{`{@ zoG29iOc%yq>rI`!i%3!PIjz#@#|y?;^0k|t~)di`&?X#$*&|2 zw!S|mfAYn{_vfLH-vEpr(+~TsU4Uxez3|@zbByUV7!@mKK zDVRRb09bom`1k0msGDX_>D|*D-LHLc+`nfYU3{iTHwTUvtmKtq)zv`qNw*bu6!;2F ztJG=kw?qH4t~}^H7vsv^(7nMD)#6npKYAbRotF-i7rztcTGonhx5ZG}uo_x9V>R9D zrA{k-v&2r;uB3c=JtZf3%kN6lPR zF8_u2StX!xe+6`rr+{&nXF|jOBkfEdatgcepEPKoQb|gQNGe6hQ1{upghE9cQAx@W zGKWa>s7Xl^ApA^* zyCE*{I=D;d;V^+$DH$~iPerTZR24^*Z(f9k!-u1qwlUgNuf(-?w_#}WObk!rFNoj&>BIzo{xTxdZ_aCCHRc)fT>=qVY0drIPJeG?2jbz zsA(fCiynl#CtKt6P5#*GCghdL9hCU z&|up{>NpfhQ(pv7$U!^$`Dzl0$7qqQWq-=E?qHu!m$S`o-fT_rJAVA#?Yu+YOis!~ z9W31Apv@~C%oo(c@j_LUT&{&@h7ZPYZ7Z}3TZZRndE>6um6+z4j2c__<66TsOe;8o zC-^hiU3CiEewX3KJt^2>AC5N57U88rUGz(O0^bZG;jPzck?p+M?D@WOHtk$Gn=|tw z>mD1=8k2`Gke&JeuE4>t&u}hI6;C`D*tT&qQ1r+CzvrG8 ze-9Lvu7Dvm8=*qPD z@wJ=D_EtDa9SfpOYCdFgIFg=RjijCcM>?Y+BCT%%i`l`F>fHL%&y{~z`+Y5Xc}J37 z$!4;_1~n{1EuCF^aE~{$OcakiFo7Sm`)0lGq}5P2*aiMI9N?Bj{o?G)!}!PL#OJ%G zvfE~dS$bLt8#U}Td)ur`d7bZBYrHmXUptU8i^`dd_Ct2pMw6QIJJ~#+lkDa*d7+>A zy^y_K!(MFEW$A-7>)tO`g!fr>uwLpJ$Y@l;>dj9fYj!s%waMU+#hU1@KNN4P4nXzs zB_KKaK6soHI$ql=VcqjU*f_fbZ022tPKPnD{L*lE8&U=c1fY{lj3h4L{D z>CE{*R=X8O%q}mDU)M1R3{}ElTeN@BilMt(uoH4=Q8Xvv2z(J49(BaPl z6i*t5rO9%La}UATI|t$Pm16k5LKeM(-oeFxF_4_^1OC0boXpl%vGX5K?n0~-i&d0k z{CGd6CFHg0_uXgNz6%9bUpTvMXvfw)zrapsv@lWBWcK&=7IuBPQjQPt>9i2oQ1~`)8mT8nxK7*b_ z1<(r4hNd{%(EYJW)V<>mbNo@qZk5EaUau%t7Vw+>GE$~H*`L|U4N9b!XG1z*LziVN zX`<8``fC?QGBd+z^1QXQKgxq%j~P$X>$GXHjv>k2f6l@^&avAo?y^sJgud#3d-(}L zvWz%+cKNyze`Bp4cr=>9=A$>c+3QO!54<&k3oF%ONLHKGt~7VH`1&OFa)&vi>T67L zzQ;q=tqoB}OODX7Dpeg#I5f`2-VX$q&b3-hS)o+~}&-N>*)pJbOS z&|kSGX63nsg+|}yn|!3X%8FFZ&}%6qnuwuo*x%{e zzsQJxlPJ#}+sDK*etTFkvt%LBHta;|O7Y2!O?+v@aCW(G6rXTO5?tj6LCV%j*tNbK z>eb}&;3`#&-|c|n@k=quV?Ay(nVEYWqBSvFI(KfRs4*ARd>%r^sZ?F z-~F_>a$%}y+k`R9^qnOe*ObV&&Eq+P7mv7p=KYwD{ShV(yT>k!9ZkRI8_*t~`z!() zn8PM@wzcHT^oX!qT%GDv@EDQKO;7F*)k04HQqvhY{YM!OHQS?U_A)#vWNRfX%~8JJ zD(rkX2M6S8;m6L|XmHdY#eXK@w2NcW{gyW_HTJ+DeW3#(U=%KQTZ$o)zBtizG|K%p zM{&Ck-aR=5GhYwD53QzXNFtO_kiye_A7OyKF@`J|j2b0p!0*u-D1EF0aT$j2s{Ih` zJQV|PIy=C%vlN~@J_E^$Nnqr09ai^GfIvy)CRoa`@vVZ^ouXWCnOQEH5G}A1W`5)O zwJF@^EhqQ{zjcgPu;E)pvpJft%x0Xa{-5W{`(wTE{2@2Yu^We)CbF1bkOOyRlObI3 z8K=p&@o$DEF@@bX*vFx(*zu2p+4eDY{E8Mord|-k`jzzOdsC9QjkTM^le?dDvlVio zrzZsl)I`JNOFdAhFchcuO5^!hNsJY^RTly`z@nG?Vd{au+ytjEcom<(t(fR(<*T+D zwg-Aa_s~%yof9{>_balY&_f^Y9_i0*{Obh{>(gMTN`FkP9ECF3Q*iE>{j1BEFsT;qoo@I##k2k6jKR#WK?V>1jQSrxUqoo)xd~ZhV znvHcjVR$5O6V|CNLLZqiXqEOJw&Pv+HEcCh7>$SQp~6nWTnaY_Ho%~L1F`nG4f0Qx zp=Yx#vX{&7jdLQ7v5i8>^D!7WsQ@3RpTsThr-j^G4O%?O#y@}eVUJe`Uc9;o=L(#+ zzX!+Q`-|N$A)yc|u3h8mTckt-o0hP*YSApL>@q8UrAm31)adOYC0Y<~O98XI=~et( zx|p+sLY~G_`oIHZAD=}UVJFCC_DR|+(?m0L&y!D#kl%I}ll-A$6fYdwt8W~ly9tGq z7MV@2KUGlb)EtVun@Cv`b4ls#8H(I^oqnHxA@Cgg$iuRS%rrjJ#gOlmG3*8HEP6_t zJ>QaXKZ%Bsm-{!QKkuU!hh7R9b5GE|uaX)oCntwYdO9zi>fUXkg@!^l{^@Rc-Z zd~+IB?^4AT2Q|^sL>(3H0;UXDhB}@;Xgnqm{~b!fTUi-6IW+}sh9AW@&(GkNUp0bn zt_1rgrC|MmP&}3DfO$(s;H+2gVUS!B*zKRi&HJFms*fLJ7jC4pS9fo*KBY7!W2?b( z_TCgHTFQ%Nn>TWe9|OTg$fRqZy#Z6x|H7sT>Ui_o1S}mP%7 zONkyqm0F`{zM%o7w)xPlhyZ$e&Vkn4+(7-`r%+Br6wMO6SG#xT)5F!7ym3`(aY3np~P4ABqxEDeKTjj zJDSDIHKk#yY8@2c?FJeC1RRoj2V+`$psZRB4T=RlEwNf z0jzo-2~*r(!kM_ckbl)0LCTd@Q~ zb+%_)B|AB;n2n!O!>iqG6RQUgWDen}eD72P=%^V1i!E(McWQOv*NX~xA+ZgT-`Ya= zk}e3-mOxQ(4J_$-3||sMASC5H?28?Y7W-sz-kb3_r)UyhmvF>8cbA~aUTeIZzY-_h zOTwl%`*Ds<4F2@Z!}l+a;Qi@QIBS0djtR`c>n(e6?yw-tOGw0h=X3FUX$1ar^+8WT zEC2so^zj3}vcMDdOwq)JwWWCQzmI0sfB)nQ3*VlHhPaoIURn;r%iZvUyV8W(jsBk4&>`q}m@YQ_|H`GOgHEPI_@AK1%!3oo;a&ezx( zDQ)U)8&5k8jVXVj8(Gf^C&#hrr1&v{oX0Pq3!zKMcmGn-UptoM7b{bUz*D>ZEsQ;1 z{DHszFOYko#ke0{+u{ALOOUlt5-%yJ;vw1R!kM-M`c03ANo|=hJ6HlL+wX|0EnK*p zjxA!_Gh0|+{||i5L}#XX@r(G@^OOAAENNEwEw#SyUq84fQiT;NryzftpdU*p;%=Y8 z=-@UFFVrr@;J9^oz(s_Sf;U~kMGu>o+=P#_Zo$d`X_Ok#1#Kgj!jp4VoWz)B z{)fyGcEiDubxdn#%M*oM;*u^lqOgN)@Y~3)H$CNFXr1KZ^1{IKQx#zQ5SXX5ino(( z5>MR+>|yCjHfLP|zd5LjyIe2`RR5ghJwGI~CiPovVU!M8NDiYlg>}sASSg#Ud69n- zB`?|;ZUB?CS3s76n7a{+a9O_q{@f&p-Z=!{o6W=i+TLii*$6j8kH-_$%h9{PoiHB{ zL#-t?_~XYiEDoN8y5|gVW0)`QeC&(H5hl3F#}J1(FT?g~KP<8wiyQPO;lS!?9z=_Owr$t_)=P--KQta~KSZ-kLDx%3>RHY+X%5o_CR{C z2`8!gg&XOA5PFPEV8)WA+`W!4a2s$0cJ|8S^)_$I=PcdrX{-J^ljmIf1@v!pZ38yos%)|nmtad z4oCUUAl!P;83X)A;GE{yP_O+Ma@w|mz;A{Po9{p=4nS|6Cde&RLd&?B_|0$yN}QXG zYX5wYm)VPZB@(e|ZwyZQa0ELns&M$w8XTT-5~syxV*joC@SV+WOuw)P^_>==$w(87 zH2eThSP9_KZf@7;-{&6c1+ne{5$xN%OYGQUbsDumo%ZZjB2%!Y5NTiXe7caL-F)f! zx&(?ocaZKHWzp{J8k**JikgpJq0_?|{Y$${Z^npe|GXj^BJ3wkt@9~welc|f9-?mV zYU<2CNbv>9w06otn(JRj$tE|+@%(GL?f;96)LzqA=}+`;<9F(q@q+MHH}R(LC}fsI z1NT#^VbrlcTHf=8uJ64^OMhRdMfz2wFOx$zP4^33(ObwbKAaMk?WN41os>6zJ>?&F zr6YFJN%ETxZFoMA4$xz!?N!P`1-;6B!c#sd(TGp_E)qS~{lZt25KXMyKZx+#3()pSW^=dUTjhekSX>lqz&hUuP1Z64>RR`YdagCLii( zD5`w?h>MI3fY<@Ypk4kNcz^x^(m@*d$;K4tCRpK&s0IH$_g=)k0NY~%FJ@yXIQ7ZE zzOq>``jiIDPg)NLei*{PnsXwV=-Ci(>KyduBU$}o+E~lT|M+n_qqCoMT@O=2%^8wiRYo&LCed%HaN1=o_%;88(Q^fV z3ek+9oo^yZzSWtw*jSKfjuX97wI!3|a`aj1A8R?UOWyAUujR^oCV#A%Ro=*9<$Y~@ zx>CI8>R7=Mc0`#gI2;StrQAStri(imtO8Dow!DFT0H2-g&t|{d$L@_NVzU>%X9nYx zNY1l|y;`PAf7+C2yZZ^IF|UJtOV^;99bK&TW)0KXAg~FyeHZ$GBiP191D2tEP&{C> z8tfM}fbZGI@U;0bY`FRWa;J1a*|2_C<*F$#^OVs1&ObO9TmX+^p1}L*196^);2*S2 z02|#8(BbwJWUD+OE_*Bl+6sGutI_bw*9YcQPUPA~R`NNGN7xdxTWrH^2li}fKelK= zE0)*x`rS|;MJwgrMVVTSpd+W5}20g}rL zK;NhoYNpAe4+-6b%QwN`-f(DHkj|a{yIQpVK_Pe8X$@cLp3Q6Qv}Z-#ci8z>p7nVs zF|)67Oyob8e_<5E;**avKY3~P(S9N8Sbc}7ygk6SSr28Vmnzs96~R*(V#hSvhO&su zwJi8x6+0O($?myK;>Y;uu&q)Ryo|jDOw?8Z|F}n16GH@!TuTw0_umdj(FUyi@54#w z&!A^n1+w%M?928D_ncEOW8WZb9VLZH&O!!Zf)UEjnT!4MJ<-(E8nd^o!sfGyShXb` z=hsGH(Bd4_#3R^tC=w@xgkyzbE^5q6#+0=i(CKFqp4864m;(a8p=2q}TJe8#W!}ku z?9!X7OjEp$tyOgACHw8O8a{b57(5~{c>4;DTOEV6e{o>hnEPa^_`t2d zY5{{K&vG59X_o)iOozn%nlMGHU36*NWLCRPmsOooXS+|GVLoN=1*T>L3#fd^mXjW( z?U_oc5@z&xy9<4E2&Y`bbkcVc_Li0NX~H*8n*7h5u8tl?$;<8r3M;O7@fBMCD zescN!|{B{1AVm;v=(!tv32;L^u5xHPp0$|jD0W0WM$>>SD|@Tp=A z{oM>&%~+_458FRt6d&{C6~EL%nfdDu6rZ1}2(e5ZzVAN)lWfbOHdg2i>6ApiScDno z3o-85a!gogjF-bzabuz>R?O5z|BlPBY3ogJ>61n+&j(Ot;R^n1pK_b>=JTdcjhT1t zboTXzFi#8Kg=xkOtlF-TT}^dkDi;`kNADZg#wWq(k+rZgQ5z=z5PoNTuvjX40h|A6 z8;jIsJQyD4hHAdyM5mteAD8Aa=gUvoMJ0VYTc%BK|6O7=3P)MPu{wU;nzJJ5+*zy_xD$25?nv(x{t_LmGJR@MC`BrO4ejzVA=8}kMlU$P_LrU%Nc$xfY`2dIM%WRR429y`n5WS z)k@-qdU>2R{0jI^NQE!+^Pt{I4I;lsz~A#?ZqV#UqIZY4gU8DlXuBN8Y2^%rC96t6 zY$LFuZQ{A0o@nqEdi&;j4aA4f%<)U{6nwU%7Y6@51s&K1fh)3L;+X5)o2lnG!>hxf zV^wwi%vO$-95!Y_=Q{ac-_G!1-y66qV-7+h-h;%iBN1HYVA@_|!8fmjCIY*(-qju7 zD2>97Zw9FIWI6833&4Aer{UvRPwW=*;0s4AM<07nj7m4b!sa=+FJ=!;{=F6#WIJO} zrU5Qe`UaEqpF&yeMtF%c;NglpV68L&t4}eg-YAEkG62J4mY}PqEhPUaQ%)(8ne8&6i*FIB75P(oQ=;G%$|5C+ELwW5 znu6xk(ugT7WWRt>Khqm@_-zA87?je5JC#)PA)lH{OKI-OgS4aR1W8ZLqj?ij$oR=Y zy0oc|!UUtF>H3#+ciC^!eEycq3_eq-=Wkk+@rGh0o>MQpqheo)hVMbr4UvQYQk|}_ zvkZMiCqK5*z6EFKOUNOi@gkRo^z5O0%Sh6%OQc}QtrYxg6TP)^CztIOlyq5O(2P)` zmTj*YUtP&W`CHiY5+&9%Ycb!@b5*qV$Uknzu;1qyzo=IrRW#FQ*X}IdqQH-oUgZBoXLgfWz_&I-IQorXu@{qcfAEo@b)fTd4WFfd0Ki<|Dja7{`4m@oyEe`?@Cn_j`!DfHsq zUx3DqW6<%`HwbFZhL8z&xgHr3Es_y5e5FE`1rqeN|2PW2F@mfLv?${4BD#>bib~%) z(wCM^^!ZsTEozUU(6{0AP54|6pW06$k4tF%?+m(fd_T2cJwYdCouM_0^XcRLGRpI4 zAU)+16t+2wE_M}B<>7i7_qU2xuFjxK>9O>rb(*vAF%f`8d>+lp=6akfJR=-W{OkonbCw};*M|XaCRII^U5BBcWEiimG6e= z>FwYY+YhTVHL$#&BA(dz38Hi|K`*`yjt(D$Hh-!?V^kc-&3Xrl&!2;DfTX-)7_J!GT!QxfeTx*>U{aFx*#?jU%QX!Qp!% za9w-^?wyo}poIC%q-!_eG?_3{dE%N4f z7@icFX+*#iiyP4EC*1e-%HWhv3ap8XfFSiE&T8>KaqHnAmb)#Hl@&xV8RxSsB1eT@ z_YI)m!}?L(_9;}i%Zc94nMuhW^8~KeCK4su(`kv>r2kx<8vFlbY5nTi_uySD;7B67 zWcZhj($J)Fy~6i!fws^U;Y9iTd@882p-`^cps zXODv(?Q)UC3I`#lq|1CSsxg0Qp+hnHHH%x{#6AW;U=Nk`NI7^i%?UQ7MIRTE+_ODo zI3S${Y>uQ|dz>l5#h1)dz36YmSemD*Og#fzSit(ttaj@+KJJ$amw#q6r`A3P{@Pc; z!T$ZQV}_crbNdL|kE6h~H3JS;l*8&43y^N)M1kcyIsHy+@r$~3OmXCJwq}qEo1izH zAMxQ1ufB5(`+i}WcxdV%(9j$YI%{j;&Z#n(wNw^OR}8>YXAtMxyWsvbfx)sw=o34y zD)21Luqa0l>qa-jpDQhpd_oR?>D~c9on_!1`H0i?ImRceJF>O@*6gI^1?EOlwC-lJ z&@Fw9eJ=50uYa5wi2+ zKQVV?v!1MGmhz|S<|~(TL80!VPoETE+k_$*(JvKd#qEai^B#d+i542JlSln{d5oQO z74rXPLh#)paGq=oYBBL(?iI~Fdtfh`b6^v=?OXwKmkQ5|wi2kd<-u9&!C>}qG`H>a zb`Xy|3eEuo@Qv66AC($myzN6MOgIC5BOZVumkxy^?{RT5BcQx!DwwK=*MG@3VL|wo z7Y!c6_mm~^qiwHo7OICpR=yKnO&*RrcFsmyOB0k{s)_|O?9rmj9naj?$IrV>a3{YK z8~Xw<___r?9^j7o>-M0g*bnWO_~3;p6Y-6#9cFg#L3VgGCgr$b!?v+VGrogEcNZAM zZin+zW<#KLJ2Z`#$3@==J}^ZzY8AQ!jxE8IcpH3w*$X2B6L8poSe!pS8fQ;Aii@2p zaAoZ&95%HAm&>Q%6YETz@gNKp7e?ZfY)>>lVTu7;KSSSwBKZFHF;{u&gQdF8D&{jW zoVD_|*s)<+^sZQ)HXImCX~woxnXrNk%NJ0sXc^7fC7k!Yc{E+)AeCF3rsO%NDO$UQ zocAGKv!%V$*brGzGYts>{Y5U7Z^%&V6P0Y~qn|!6X}-;KG75N0s+kfE2L?+w9DekN_T}`^)}vj5U;GYL zJDwqHVGsJTG>1kX+fC+2!l~~0UK;pe7fEl}NIxq*DJjpA)~?c_ZSo3akaLTP<*Edx zUKn%mHe{`1cJaSG14Q}*`nbLG;$R<_CHUfaaGEp}`<`oImbNwkNdiN1?(GN29x%-f!xqv@W@;j zTZT@=J}WC6oA3PJb8p`Im#`#rC*1xL59l@sKxRHzln;P^`&{AN6FGPinrCG(dJg=Y ze+~{TkU{Iab@0%r23A}dg1h(V;>Lo9aM?o|pH)xC?cGChwMj4Z*_onufD?M_k3o_4 zZ$R1y&rUaR+w=x-gF86pZ*+`J_$Wd58g!`Umj<2B)TT2r?zHvTV*1`EbUCI4QoC9r zT^SojKNKQJ>PsHobxtQUD5fTzbZXSfpy^ku==`aZH2+r)S&uI#f4fVxjIX7q3P;Ih zW(_Uw*Fyb|U7*KdMbz>ojZE723cH+eG7+*tDGPSdyBA?}FLWMtUN@sWn|Wm8W<|RX zO3|j#LI=>IQS>z6A4@39VGV%|EOks4kE`en|S;8W>{19hfk%Ms?c=#*(0<4tFA%c4fl0j|oR3w4d z2CCy(Z$<34`xofXIt=rjo`F77!Njq(f*!LMhTr%E2jpMF^?v@4yu=#L>z##9ZzEvD zSr^#XIGsydxQn0Sw4bf*zQ~?xE@YWS60Bt29WI0UK-}gw=wBg)?|VjKSM+?;D{@0C z=b3ma%MwGQggvFu!Qj5a7zfPM#E#E5%#mwPY8^0}t7k5ql$UC0M=cU)^ z!6A>4kSysYx-n55w3ijZAmQ(yQ(_J?quL;q>xG;46|iE*J?M9IBWM}d!mGYPxV}fw z{CNZPIckjIo^x>MAY9UT~U~KFx)ad z0|t*D!|C`!nEx&wbl-)-Sl=S^PH*^IU=h(Zpt!%7cBHR2}f-Y`QBXv(n z+UGWk*5)suSh@LRkuaOSNN=b5?|bRrhe*2Ty@{r8cA=Cop%-f1SW+}JA}#L+%|pR%e}NY3H@s{TJr!Mf*NBf1w_WF^J|0EHq%QX+3u-ez5q0nkA^In*(zm!WB%g zVx!NGWHHfdj2Bn2XQN)T*T*lh&nE?cZQ~fq{9r-Nf5y_cfQ5AINf>RB*-s9aVyHyj zjiTqRAQ_eA)O5{=wnY!7h3DGY=QMjlm)Q1Q<-&dA5+D1vSXB6P9K0+{ghffV@F_P3xcx2gVaRQW{xcFC zKe%DtC?O*{MPU7#Ou)_s0r-2PGmaXsk2i1G;jHG(_+iy-T%tM-onN?PV!;NaD?;D! zHZwGPw-Q$;u0=z6Q@oHd8Ba`KChXrtLQYK{k1rB-6pO6UZ=Ec5`hEwUpW|@%yum1^ zT?<(WLI$*X0+^>RfUxQsFwCxooufX$;CH2P;&lT^h3A9a=i88B6$9aYTevNalC1E( z8{7G(QEZj$CdxkU&YGGOS$2RMckXKgUp8eYiwIc4|Ly%QQqLd3e#D&ppXW+@`{nqm zWHFXK7>~*Vt8~)xGT7fQ2{gDv0!ZyIuQ~Y;%T4KKW7OiAePRTA@}+<`)f&RQ-+3^_ zUDHH2y`FFpl5a#cx#}=+eGUAs%7K@s!hx&Zj^|d^zm< zISRh2Z->XflSJ&QlW2|WM(A*#0=;&_#nQpbkUs7(WPcg~VHtb4P0jJJVQmTIcPXNY zjWOPTriXn`ZwX!5=it_!cVHrX&WRQM;mO7k@Lp^TwjS-`-mkW7b4V%wsPwVeOTm=S zscGiccW1$?Yww}wq%PjmUx=Gj#-q${1(Ytd7T#&jm~v6*RSFxAbUhFs{#k>XkEUU+ znJ0eJ499y5ys_JAG3vcA6Y}5o7`iu1V9c$>Zz}FM`j*haQ}h+yG<*HP(M*B*+^cOuYpfd~2}3k-&%f8cvh3HV67<+>b`1X%QXHa|R`9e#Y9UHmtU zO6Kd*sHMu3^vs6p+LqJeG$-<1?L|?`;;7g(m)2S3l7!MJ8d!Zs==Qo!Z1M$qdHpK& zHZqzVQcUs@m2~TypnHERrp4kz^tS0F`47&gUx!lZnW?}CS}3NK`mMAxyNALa{3g4q z_w-WfGd0Bgpy#zc6kze3B$j-nEz9~fm`lnuj7$1MV-kPT&Gbj)S$dmX70;2>_ro-{ z^?=Y{v7MgJ3!|ce$&@3#n|^H<{Fc?Ow4W>}bofXLbC4DCA-CBPu9n@MzKuP(t;2ji zdGH@SABcK>edTc~g+lk9cMv40?Sfsp|6v$9!~MbfaG_!$*c9I8Y|kI43p7M{Yh#?B=ZF&J`k0^c3tqZr zz`pg@x%VU2aCm$KGx!k4b`Sc_ZVXhVJ92WQF;bh>#X3_}lOxSp4s_-5DoXm3KqzoW zR#k=4+NXJx7o0(jBTHzDz`~!RnnA8#tLd*wH8ob}l2mgUHSfPfV*N9exTcUUy9hji zakr@F@nxF2qMV{^)5&323R!-Qp@Kz*^Y({-ad}nez+wC&c(tNXs1AJu2A8f09nAgkhngzt zMJwUmEGgV3rIMq7~-%iWN^AQoKniY=uGI{u@Iu#v9 zY(?9{$tZm;7k6(D$4^>5cv|-VxajxI`_TwvVzw@h%xSR-zp5a*PC+^l?r#+`ear8| ziN{&ccI63El z0SkR-<01oU8l_Cevszf<^e{FtL4^(Z_dxW$$ci&AcYvSkYN6xcK+IDdh8g$zqqtu# z{PQ{hrIBZ0_mnx%_kAXJzw#Vs{@qnHUfYcwRi47G)Xry{Ta);%r$2e4NKG~{H%jcc zOamN3N5aetC7?e!AIM$`dzIu-OUD+?4tZd*%?cbl$^?yO48c>H6Y)fk&~q`a8N6h# z!P7>8w|}b(UPcGPL-mK8&Y}JM#1IR%V&rso&i5)ye4vxUF* z=`Hu_btK%FPzH<7^%I`CL_TWHK5?d!Jxd(5iAlZclO-Ze90J1K1~x^xfXu^OxHGm9EGuuq5qn*%eJS|d z*Q`X=J_UU@O+r(rwYV{P32yx->=!3G;e7XP=y$>%Lw1>B#CpM7*|Qz{ZJmvJ-%N3l z$4ZPn=#Qb}jPUDI3zYNl#aJCHH2bQ6SA?!{r3w+IO_WBjP2XULt0A5hdQo4vor9e9 zA<%lv0;Y|2fQJ{Wp~|EH)-*nY5l4z4z5OyMKq5#K34P&0U&cA*ja*l9fA;K;Hye6q zt$6Fj1kr>Z2bQ|fkaaH`$lbr$z?->*vjZiue9ij5qVF5^S^(^8Oitr&^}vf*@hEd18%0V$C#x(gckWPuHyD)f4rS0_U5nF45?FbSsJ z4uv3T9S#Cth~%DyK+(nNaBa~NaY(Zygt_K}%pxuD%2>p$PD=vaxu@VolL|JJj7P1m z5m=pe10ve%K;`apuo)r*119!ytGh=-q|H5zaQ=SHGuUYOAFn1>am6*zKuA(|g6!M!&U z@mW$jN?h2DCRq_E)#8HF{~95>e}-9_g^>2?Hs@#PE9yUUC2Qv+m|uGfdp2hnsWhn5 z&TY!HU1la-N%N=CbuPmDOW?o!iWlzRIi$WlkN(uulGmtO`k8QrMAI*mGk=A$x11#% z#bY#B)|2(*2l6b3KyDW6wc)rBg?R>=qrm^_*@!{Y9hIKhoj% z?`Yl{VJGSIita_cpps2*$W&FL;dqlwL)KV{hGEKoX}-Wu&%4=5R_Y~G8L?1y7GlTJ5c@vj6P zxlEy`_4P}x)Fv2C-Oqr}2@DQ>Qb+0O+PK_a11qxuzX;ywi&Z|jx_vc{)Js7}l}zk= zk%9-lAI4py&SKh`8uSsDVVqMcM(c&*;3o?)v3&$i9Nh&gGLj&qJdE?7smd(7_OYq{ z!md;>GuTvYVMq53W#>cpi@%RI;3i-H%w2!F6-vh*hj-Wt=SKg5+FnhxyJC)>nH;W+ zclqyJxqjy>*fDq~e7+VAjlpsNx^B?6Qxmw-g+hOYGK_6ITCblt6O zWv8dH`5GzAU0{REQ@F`8%%8GxRrgqf>TsGhUY#oX@yzq`JGSbJHmSsSGPUu?nXjch zxeib!*)>J1LVp{3?C-#@{`Q?4U3Ut6JUYRvsSq;LdZ2ddZNaeeTj)O?j63G4V8Lz~ zoPYf|`1yQ*q)lpAd#4`$=B0s0(^sfl^BD>zt%JS?np@?}IBYaKWmCcDC}_fTx7EV3xC4HE=#O^oBk}Iwxi~(1A?A!hRGlyhSGtYF zd&UlEkunjp)HLyE-#IWSsD?t%9N0bgFAPe%08{F{!LKw1ZuU>$+7h>L2dt|(NzKFj zv*H*Y6@A&)tefmn+&Q*Y`4_(~w?Ff=GT;mL?_zozE7_u_3QTr`IuoC(5xSTXSgy%F ze#b5smf%*;rZ2T;1snYMSDm}r{ZR!hZ`5V}(u^o^-bJC4@ga;F_p5x={>&BJb+Uv)@7IFS*AqfQ$v&a+!y4g^{|;aEHO_gv7bO%5*6D>^ z9<45P4=F0#_Ex_rX_|VGl3bUf7q8umPDMo(>Bgbx-`IXde4kYGecRw71_l=Wurnwc zC-&{Mj=EatQMsw`($3w5Ud!c*44>;3`F#9c_~yJ-QS64$BIWu1MF+q3FDm*nrzrl; z%A$Ur%ZnDq%_+KTG`MI~fPT^brLIK}XSo-RDsC>^=XI-4?Q)l*G=tK@f^Qy$#?K=P z>lSP+oHa$UFlOg0-@<9be4Af(6@Fh!^6@7uB@(>W^&h6S}1{CJ^K zc<`lNe#b5yA$q8oi=KE+Xv^F$6qQIJ@2D(BekR@-N8nFz9R3Y(f%RT3thR8$k2mH> zUtcb`94!@QXvu*AF9jVj8(zD)&G*oX%>_130t#JLPA;4_xT$cHgHqAiywbvwL*<40 z)xrvo{VOP_ws_?`>HG>|q1OfB%N7OULBrw#yGxt$-8OV9O#D2)kkPdTgAz-9uYIWX zt-1E4;E8Q!p?U1S!S$ z^rFyt;FfS~V1*FA#sDoF2jlw6$>{gW3%_E!i8CoD;*s1a*m!n@lC>ZH2BqNU^1*ms z*cCf`V_@!>f=1f`XdK-QCsv8N=)DOjly-t&cuzc6j7Pkk53;r@z-e+<2f{e`yX>ts)&|&^!HFo?^ zWNNfNTiY$T&)kmsC2mx7=*nesCOm3s%JO^W{Pj+Q{cM$KIaP^+;$(Thstwi3&2XL9 zjP2_6P>yWErv<+tSST|3nGWaN(PiUsYbJfMVAu_9n#LMZGTE9TmPWiSsmX)&COq`R zifb}->8GQ^ga!*vpJu=&J8km44EvbM(qxDt$L{zEFSQT&{O3IelstrN;B)-lc^A=z z_1N>X3FRwV(Q5h`Szms_G@=W;hp5r`v;xB>OR_jpg=tZ`Tr921@f~Ws6JSQW95d#n z=&?3LkqKA-pe~^S<0>m)lAD9-BiTrg*aQ7B=}=Bj!Dw+-xs9O!({a5pu{IPxXIbE^ zr4>$$5_221N5cGS4>(MV5NBE}g_rD9bj}Qe<(0)SFA?*N)*Qj;)NFi=tH!&6dc6Jk z024DxVg2Gbf~q&BYZC70{1r_5m_l!e4z9F$;f0JD-0B=)d%z6? z$0lO!?ZxPSbS*00&BT_i>0e-m?m_wEWo*sA zh}fJZLsID5kYc3&f~Tzw!qi@V~zavYQl#XWI&#((a;7=-ho zt3Q`l`Eo*3cWSA+F>ACX`(HEU%CUNMd1^{0O?yspwBv#av zkqyC&X&k}X<43b>>u?U;8qdrjNwgg~jUPTuWX<^`j{P^5>J6hP{WpT6w+?1zYJdJ! z>P>m)-V}3z`8vjr+6sdOHSr+VS*LH*$U-j(*#~H;dyg#c1xn9?F5w2JuqwzC1dt7kfW-W>l>`jry7Kt2oqP^DqT^ zQ;H=GvYb6thilBVX|JcwC&vuv{LY%4`^;D$V@d}FXJ)jya?W0NHhy(sd38^I*x=1! zu{{|e-IY^oo%wH`B^!#YX*t%Ao$dP6bX4MsW;yyiQe^Hd0~+ixpvzu;9{g!VmlsZ= zozIDFE&8&gN0fvK&d99uv7HFP?)chf-p~2%3b2aoGKEs(%~A zwVgo>^Xboy{{5+<5=!00Bj~p=fpK9e%pWkDH4T$k>6*a(`;)n1WHME5&Za{_8hv*z zp=a1)9_^Y+r-xIh`*bw@L!#*0K9Ezx`*Mk5cc!a264M+RH_w8*25VFEr#z2ZX|k7! zHZy-qbHQ&3em$tf!~QaS>GTyb8Go=NN1BFF%?O+G5?f5ai*uS~xmRC;u{Ph}QY*(J zBjsqD{tL%mO7i1C4Q^>r;=ak!yth|_+hX+i2Z~%=qr!?zW8Sr};I_SXtSfb(+D~(? zEwQ6QwiQQ~bmbrod#>?zA~rfOI-w^$()+XaLJ(~~MDj*x01x<+5ZuEI>|3Z#40!g|p&to>XK=l!BHer5;mylIBgixzBY zl;zfHHO@%X!utI>j6F6TX$EPK z%Sge=Mg3uT!40WZo~Rw}j8Kd5Xv<82uyqTjbX$$R4{prXaiLc3haS9fKzw#(e!^=x)A=Q`@pot5|^e(mI^JcwhXv zW%#$J9t*a#B6UIob~nAj3JYZZEf?@nmCm9iqw18wz~1PJ(_@8eu!_CA!}% zLck)ir(pF?{E%Ia2m8{o>)l#NzZ-_#2SV{y9C{jjFADP~nP5(YJ?1D6!0BGyQBe~C zBaK+Rc`*>u%i^*7`Fh9nF+o9^W9&OSK@Y`n;iYq*D&{V{AzZavzconSk;t@I~ z0ke}Pz*EecjH-~sTleq6pXKKGtPz7{ZA&0iu>rF%12ez&#lSEdNUBR9*4+?;ru`HB z{Ov;JaXsXveGoip1KIobsBx1-&3Q#A7AwQtsVnMVMq;<@Pz=|Og=ydl^tiqa@BeJZ zWYfLK^2vs(WG>45^D#WT9NoPh!au437Jq(0E?I&zKFTmPOPzCmY4NFu14eu>rJ}Aq z_x5(=%kHjh(DP*MeP2F}5*QQHlSwz7*jvkyX0hh{sAtIc-&FYLxHQ*(Xn|?#Lj>)* zftt^iIFVb3vrCFGI^!{JjrxqwTI^&w|b)m(ycC+}lP&Ko?mtb^mKX1ss)3LAH};(qE!F)OYGN=Ie+!ApVq znwa~HLrKVs*}R`}*OVdm%WBKQ3l&ni{euuO@ICQ8$DiWg|lm_SG`oAiKx0d7Upi|i1l8MhuM_0Wg7+7%`{n8I$O4b1+J}rR%uUyRD zc?5qCUPjyZyC_Y`MN#58Y`Rl{LDTD@Sb7&nD|0X+pa_}9x5cccvv_k?#GothLT~B~ z^eZ?EpPeO$bGrw{K{*JC$VT+@DpWo#MorKoT#2}e?v5w1b=^L!d%q0tyb^FP5g7a0 z6uBYYusrv-(EEF?aKrAF(AYy4d*uHKkvm=qT~FJ?DB1^?`sm=_Br^;T8-{0_2f?V^ z9=U$Qu+V)4Y>XEC2UngDaopy}fo$m~u=!MXZr|k2sXeT?q1~91&+0MdxG_^~9C#$# zk;az2sSw(Sw)X~7^>h$jmjv_ZjSyOTjiNcmaGB(Ac1Dlm(EKE(4VcEv=83$klf>r0 zv3#gEhOs$OT)J~Ghx{DKp9{U{nAMwdef_CC#g9#OKD?qckg;;%j0*_jzTg2|6%od; znBnZ36iWN55FXn$fZavhx1rgKdun=f$aO~!qdjM&Ik7#&jD33Qv7d!G_iLNbeuye# z8^t|gsZ6bEbtaz{|CVu@d~T}E+=B*OpJdCKCJr2M$c|wH+<2hDn+CCcn0mpRr&js1 zyH5x`)k1i`DVjRp#<6VHbS73LvUu`jm;_T~mf zN8a_ZXRU!LcSq`RiG>3DXG(BktTdgLY0<<%hf$5{d_G5?_s7}LDM{Q{Yb?0;j4PFT zyHewW_`Ce;%2{tcSTe<%{meXB*4CA8tz0Pi(u!Y8ZP;ar0kdNaxIImY9t!dtb5wzj zxarSt697Qb0qouggSF4D=_V-6s<&DIp?ix<={|tT55l<7B9Ly;q^@=#(`ASK2UphqHR0%a+SDpi;wv{v zUN!oHcRL!f|G_g%?NtO-jg$CyU(Bu@b`>poC8+9JfrTD%H z#=J*}n~1MRwjlD|H>{DCXJVK-Uo4Pg_FrkP`rC{jMRiE)`3UX5^3mFP9jV&;G4sGq z1VkP|Q{`G595EfL*5b~!4=wF$K`Gc3DwQ(0p->k-)s%ZH8 zkA$zoBvh^Pz_KC@L|+z=*vAKjZ4&t5s}4`yKKNLhfFGe7;U2pY4`v_4l#hjQ*?9wJ z7iMFwb^$(HKZDG!M|iipLd+6>gXg1vq9@-YS@9i)oRs8RU0Et!mt@&)Mb2@RWvi3| z-B;@I(G5ek>S+@#G90Vg4u1WF{n0fTfAbzp6JEi3Y9j`@R>Nf16UfGY!^S;rShKGM zJ&I+xYPTBYt7Q4iT8Zve29%s*!ql-Q9Fl3v7+)hUSz*qE=f+givZl4RA!U1N@a%Yb zzMa{H)ASp$x$qU5MwQ}k)g}C?-iyIOD)8+(yBk4% zvjvi8dP839xqfr3Klc9Uhl&7OJZmO8JW??tZyDB%U4{OCmtxO}RJ=Yu0hOk{n7Sea z71hfTzhkAS`HjSjTXFc%V<;+vjSzc928(r`3!h9hQL|wX&VQMO=Ci9{aBUn;Pj^Aa zd?WlG`&Mvjmw~~DH$unT7GcD2Egb0nT{t`04pXN)BePQq^QWmG%t#tLXFGwnqTu~6 z61VS+#m*IL&|hmCx`;V+EI<9ml6(nnJuSyF zCk>kY)#6D%ecCKG<8?&`hDJE@*Ekm*R`z6ZUSEdJ_GO+_Pg2Q|$EQ0m;F~GK)%2J; zT!k-t%doVw1)HbVBIQaM{+zFdaJvL$hf83qP=~@}pYiSOTYRf%h5lyoyc87Jw@HcG z<(f>nWz1pb=JeZV!sQbz{%c&ByZbTptzP2qiV8U0yM;=Ni+B{21#8>wVt&|WeBO2t zkzzfdn?4(lJLbVBVi#O*AHvNI>rj1jIX)da07K(LkQQemNY6iv%Sl(^6L1jOEj!@U zehz;Ar7*62h>lea&^3Ar!%2N3I|%g3!aZPuziU#hAgwfrcwWd zD-NniJfnx(7Xy%4G#X=DX2HWR0^WZYAiT?FT$#CAtdlZueDPkQpx)RdM&*Mn%LiEn6K!wy*AWS}g9$%tQws9q&ZjNv4!}qR$1|mi&9&lnVmYnRi)>L*}Wn zT;x6W<{R=?h`67wYcWsSkQ!QcOnmOZm&vyDoZ!mmuHL-5q7QE_?aQdo{>-}-LY1Hp zZjO%T^*!Sl<}#gGk%?@*J(Bl&jpE$bV>#3+n)$V%{G%4g<;p&6S>KxpMULEX)0R!Y zj5u<=783@_GP+5Uy6w`m9`rOsmhIS7{T|zJ?hj!!eJU1%8bK`aA zuDtZ8JKeRsI6kQ-J94_RXk9nX5pm_y=eG3LG-lLh18&-{#2Y6?{IpJZD?@SoQ zJzv9_5g$(FH-6mI63BJKz+k{B9BHx{Tu#7t?&nGB&MY^y;_hy` zJX5dCC8xEhZ>Gi87gD@>Q-Zg8Dlz4!G^^9U;?uAW%(j!I!mU;msJw<^+z*s1%5mTe z36|EkV9s<|R<4(2srh$ozx)qzUaB1WQl7SBrNm!ZgJlbK`S^np@7Jnx@*XqB8(7n< z)Q&q=IJ18*8!E;)@`1>mzFqIg>RI;O(e5l_ZF{=S?!ntB{g^T~h`w_p>G)z0E4BMk z_23X*eGp9JFoC+?`_oc6lw~8t`e?o{gRMnO{vh%{xbm6Eht8MK;mo5-v^J3B7yV|0 zd~d|)KlK=+Un2H=oWk)z2e2;hDw>{Ez|Fh>7i7;v^T}D5o~y*0JCESH@IF3Ee#Xi4 ze;7XFI|j`71LZwRG`XY6U7ZT7zbeb??^`h2>lyCvzmMqkxv*Eij?7^haD286%U&GD z9NTn!H=B-x_{E6qSO#UUFeH8Rfd`{tKG7eoi$uIReL40g?-TP(He*QoL=;b*g4K5t zQR?c3Yf5@J{>=~imwV$xO@}Z?TN#N*dcxp#B98fNfhaJ*l zR);<#k7@GJ8)=T8{0EXkBU;rTqW8xtZ0YhGACJF7_^V1BUilaa5kC+g(1F(pMn?Qo@^?*M^8oavp5Vs)(@e3$Dpie z9cGrSf^OOfn7@dC?JR%1oT`k-z-D3jz$`&xN2@p+uREqUjlvuCR9H7hp?ID-BFwbJ zEWjG!i}x4d*60huozxoPjh;BSvGJ|o`q>)G{@P&E!M}p^5CuqTiM-|^7dWa!qBb)O zzx%~Ob<-;Ra@&TJ8QU;J`!EJNT@o=!K4PX9<9%BW1_^D0Fxt^jX(m16h5r|7!=GvaMNVz|tA3|=O~asCQC`&*H+ zx3uWdWW@U6=3H1|%AZL#|23|B@!%o;mN(#oYbCsJN1T6s2^-7L{W5bLq4wQfGLC^SZoW0VLT?SjxZj&9?Y|!V#a9zIHs7uYms+_n` zlKUXdKdMsfHKQ4NlHXtv-HOi{a@?+{!jV!sJkVmrg?B`)J4c7D5{7Kb7N7q}BWC{8 zV3m;xJyzM!Gei8otF_rh*PP4$8FJG@O`2|yVP}>s1I$&pb@xB`j&H?@i(hbOz+>27 zd4a-hw{gm`8pr+KVq-xQGJKlQ^tm0~UU$IovI={yQ=*HsG#`9Y<>_(yJQ=FVuo0SU znqtATZkDW()TM*8BGV54!J7$hQML6hBqXjP)jt~_#_j_LZGv`f3eIUpq3GEFtoShy zm4@Rn*wzy##on|_y2Bw@M#AcsCAvt7Ih%2bNIFjp?S+i%g3!bHrg>@3H~!mygD8T`$N7OJmr`TfzyKH{$$Ad3aVzBOp;95@ljW z?W4EC;~)trmv%?))kp+>nS{?`U;V35$#9cik86{bLUG(K1diT{!tX2atY90Qq%zTc z*L92>a|g3`lwqgKaqK>}3!!2@dH1C!aj@k&`pX@KVaW*?>?uRn{9+h;oj`_e9%iq& zi*2ti!!Pj!dP)?-{NH1Y9B>C$#XNbfZpG+_+gSYbEMjynAn@oNgsm%uyU`hZ6uC(S zl_~_wUVw9M7J?_eiD;7X7WV%U0lp-z40=Ogs|LjN7+*AUIJP zXO@)+EvZ$);WfIL8}m!B==nhCqizX_g5F}SqKTqNW8nHwSbqzG+6p_|c^n4Km?f}3 zH1j{WGG62Xk53%PHR}al8rg&1t{(Id_pNcJ8M9vKbDj9S8J0NkT&6t-{_VlR(q0sU z?Kv$cn7hq`c=Xy38Xt*eLDLxi@*7V1oN>H#Hi`4YrqJ``1YV1r%FJ1DOwJoa|MX~v zFA3u9#r-J%tQTMG?8%W|`cqX$g7o3dZ3sDVdob7Q7HwXfK+#?*mL<88S{%8kYE`Y`jd7Y~>8;k=W6)Eqp7j;BKS z>C7e4X$d{=h5!AEK0Q?&Y3Y$)V}%1l`}uMa+}Bv z=eT%q@3=lxyWEqX|A^k2=dN68Zo>!Gw%okXfK$c)jpPkQ4wF`3>rVx4I;l%{8)L?* z8M0uDC98s5=ytR#KW21gVOwuHzw%}2QNoVtCLinwv7m@!OgNW*Lp-q1sW* zm?k!OmPN3-(Vz2&1@ND=_)Hl4G1EMfT`mu&w$3C*PMyV1Wixo{?>HWni06ZrsXSMa z#Cnmh^nH>>?Q=`%)v<^_s%Ft}{6wxw8^PWp?|Uzid|la>nhkC=|K>mo7iT8jx1h!V zQR9kK=J!Nx*6C_7cbpWTCrR^Zu;|6HmZ6XQH(Y!oN#Tktd-{F@6&f&gXd6n!`o29! zf|=5-=+!36J_hm(FZ&5m_-Df?Ro=*y=Y_LUw8_!nY!O$+j8|gJa5X0OG3DFA)*Qac zfi-KL`S+F$_op~fSM&}X5Z7z7g98U1bK$Qd2PVYyVBvc*d)QzRlSFdOg+YAOr5}3* zgwW73grZEz#b*84a59AB|N1jSd?uqL{MqUMf4K6?sDe}URg8AZg|tKlYNK}`J@PnwUTne( zli4s@v>a2W45Dk?=(EyG7aW6bMVI3 z8%v%V<7q#CoE+;5?;Vne>!u8MFL(GWPr}|O=}5l41#?~=L*4EoWM3A~z<^7L-&TZc zSWM|DC&?Hlr@$?`~v z8t0D|eG;pbm>6s*)-c9=ciV`EmfKLlL9EF(S+H8gn3X3*??Zn>7GF@~f`M`(Kl%@Y zeLvzt-5YE*D?{(-E3ow35Bc+}@q4GZCWV_Z$Z;a1uO;GBuXzZV6M%$wCV1dtg2#L8 z#5q*faOvTKcZ*H%_TD$)@FrK6H%6(1nbyI@ZT=ZvDw!TN1nx_T5lcZ zH>|+fh$zvoFc>k11nhhxg`)I8-%mhEpdFD6&4!HA}K)w*JLH|@|zvjtqw)1V<=RG z(Fh*38s=)7(L856yb2D3HJ5N}em*SU6ruX-Ep#)j#m(mp*gxYpD({JWn&`K=m7-41 zIBhyF(dV3D<_s}*cuG}a01n2bM;^dqvj7z_RQ?GNdvTqg+KH7yNs@w5L{V48M zY{GG;1qhow3%8$aN8gbLq4;VYRxe(K-DXEIGHO3Mo~%P+dnU$aT@v#e52B0ZP88Ul z6W6f}KGu&=-}Mb%OV%PI>Jx%I8t|*W4ZDW5!d6A(*G)vuAwz}BPRVfLPI-EZnv?og zU52@;GQ-+{xrc4}NYqf==k;Jrpc~IrSuy694L7fI=KM5sI{Y%^sL}d-LXnTmmSI$d zEK}}E@$v0ugxa;jL%I#dZ{^v#TZP%tIy6qOrnagTKjdpOP2?rbXInAS+K}IqHTc!n zgwY4Zb$zMN`A2nV9%jj;iN>^@twm2|(Z4A#M#;ei z5({VDLsa@}BwzcC{9ldOXVMPc_zv{8Q{h54MGjV%qItE*cj}3_QuJ0H{Gvv0v0F&p zMZ5!&v^e6u689%|qG9)23>NO=yjUAAHoT08HTz(=XcIcbvunLYIAZq(;*w*qs3lLv ziE?jfOt*pBgc11mJw(jSGQ&gzKa74j7I~iCA$ipr?jvHc;>;A-E!m3V&f|#HDne}I z9cWclz@{W0XU}KC>fBn4lZnRKq3)0{XcboXEfO|OZxZUPrSVbfo8V7%{1#_xIU6?$ zzkB==+84QF(CT1J(M!N@!vLhzCBy0dQasa|iMTaeu;lD!SjMbGN$F<9eA@-}?>R_a zQ-RTaZsO_OW3r^V>}*ViH}=ag}Ku% z2zk9qgydLt7_eEmk@-NlKiv{ng8Jaj9W7XOH5Au93KQFhz%Yw^lE~#0yFP z!Ifj~hH!Pu07g#{*nOY}VdKF!Yi&61y$NN%>9gjd8GkQw;9^w=&i&Yv)AIW8={tXp z-yKfvEx{aL5yrS9WBA-Cjtc8y7`GsvjsZzbnKhM>6%+WZ$28{68B0^$ICAbVhTIP3 zgF*fIMoDx5?e0O{vVJTs@S|A|AFem-&*%YRye<<=*VF($TN%ZM%u)0}G)G58&~1DG z&E|vI$9%YHy@>y^TzFE-j^%Xb?6nq@s1x<=aBKb(J;!J2HR*jtpI7R&czU}r|8$!0 zv#J&!HfuBFkpcU=*z?*^N9NwPn+Sr&!L|mD> zONM$6aQXvX4>@bylCviuLHaD z(Vjk(9^^^)N6uU_%bn@H?AYJOj_1mZSb4^PJ{C%xBO}K%ZSu@6)Zvb8Ml{dUr%bU0 z`wVraX{!U5i1?^&mnUy)_2n4bK(;}TxEoyy>?$xN0V$DbdD@`!IBCmra^rp5$NK@*E2Jeaf#;~QzJSi!1*lK2+d)|^hH|#m0 z(1m}*=V0eyC(#3JN%e6eUpdH*RkALu6@9l!$=$if09-5+$lUW0?729AGei2ZH7|tO zDIx4`3I6CLg2YVTv#*C%+s?miZ3e#W(hoyd9K zhJQPLqv*UMg=h_~@K@w^cNrEaHX$kODf*_|#pvT#ab;v4YP>S>{N+}>8*v0%rfflc zbPDt*E=6$kN@R}+N6ah%suoe0)H?+KRxLwx;41iQAAq&VHdLxkLz6`cemTv@2p2z0 zk}yYBco00#^~dNBVz10h4SfFL4w?8VP)^-|w(#vxR5*#w1tl2qy#ziMS8;l5G42Mx zL{ztWEdO~A+mt^cHLVi|N48?vxken^Db3YUa=acWL91w4`ddiy?`9dU*{H>o?fTr- zPt-rBN%Ont@vD&fhyz_~kbbQU_X?h4?}0Z+_74n8B z=-dzjccBlwTpciXVx4g0?;XKm`ewoH_GO{=wFaIh6Y|r8kg(1WJtD;%_ScoduDbO? zkIY;_rZZg#ipvo!Og;&(;+_bVGp!ICV27N{PQkmoDuP|)&@j>o#pY2c-!>GlqsBt9 zVHF+>+X~mftD2|8C+eE=)*=sF{|X8Zexv62KjaLN<%x%?j40Ni z*Hv9^@H3;LyFG1scV%OWGlR!@vhU5l?5!iP@k$SV`{c;@a(fo}m@r*Nhuya*vG9u| zgWi9I_x%Tusw&0D!aJf-E+0k#rFi=F5o~3f5Vf=krRG0zTSJZ!I~91{SBZ6BwYY1E z2_ICLQ~sJ6vny@?YrZn{$s?F9e}fIXt6;JHHjEvz@k%(4B-uUqeQz6lzl+-B1k54zJ(&z1S^R?Piw%fTI@7r&Q`Ea&S#qShHf`?fvvaOBea;whPMju#T}|k7 z&6XDi7*Q`@hnJRE(sPRmrN#R)Wsn?=ME`pBEH$oHlH_Mm!{2NDS@c=dqO#{pgzdir z$IAOC7$SOqlY#5_c%hwSm6$Xq-Ph1qsi$irK73usHp?$VpT} zZEZOMU*%$f`ayi(v<@>KjX;NnnE8KG0{B)X47l4V9QxA*I2id^caUiyx8;$rh;$FW=(6?WVGdWYZcc}_Si4`jxk1F0yYVvQGG_T# zA>~OqR41K;Zq+_4Te}gDbB{x(|8?9Le;5fa$B;cp#FdSCQ2cxh(dTk;LGm6fye`AF z`8d2aOYr{q6UDCJ9RG-Dt;}>9dLREFPYPasBZADq^&fLr@i7+3YjyDTvaQ?P}}`~Ub# zP4h4s-1FlCHL@VQCyzvVFwn$?>%}^Aq1X#@_q{1U2%;D5mOZ1xJ-KeS7bj=>(`0`T zZ!8UD^Z8I-xHp;)RN`n5H-cT-$BPxFpMe;UJ2GDG>*Du}m4 zx%6drPwpA%A#(Agp~paeJ=d2TcapsC-QN@DNMfhcO+8q5rh z{!A$7OCQl^*|xnKPrkOL`+FyTa5blOHv<}9v7)Yu1q-z`>6fBKo&B2Z($k1KzfJf- zM~nXgwfRI|pT+%b`BlxHV^VF|)6$J`cHV3>?!yboUeum2fMExNsg*s16^EnPIv}1e zW0RPlHJN+6j-=`R5qvB9Nr%Kmi~TU+Y`YxDDYZTv_@Fm+^qj@M5IbJ!ZA=G!4fgyh zM}tRF42qNIJ9Aw=d81Eb8!fK8Y{)}v?YKYPlHmu;c=41ACw_Hh`bH1lig0G5Uk^si z_2zD4Ps+$R^Gb{>6-B>|?QC0~jW=Y)Mtxd;QlRN0X=cmGux+g-_fen659)HchZ(mX zb7aSPJGOtbmzyGHJtkGqu6V5yofD_QF-4;`gw?cgQ9Ro z1p4u+j31Yu@Zmx6^{k8tjt?6u;;?vjir(3eI*EKXd?c;=#4uGD&#a1xOmm&ivPbi2 zdnt_-CUaPJZYswtAIo6CVtrorL|{2L(?y70<$RsM@q;G|1ZG`^`pAJNlOE$VP>-8HGMZ$bG2YdW5BVBt^^ zSAMmnwtQEX`imOVRY$&Rv*Wbyj%@L8V0dj0KB?@-HFANRtQW;e#(`XRlf3tD2vci^ z(DaJH`8EBR`)-KHsrd1P=+W(A=g*~|{|{H5zG=dU3~gRJtVF}U68sVR1!qz}AaLUo zWSz;y*q4Ve{^%a`-+dA5pvqput)NO1MeWgtF8|goPY~nOILRQCbM^X-g1dnT92U199}S zJybiwK|epSKUf(nMC@dF$_`y}>e(Cd={e>MVOK z&$+!-d05h?Ay%^Hbm+3RS+40Anm%rOl>z0G)l`^H*b8XIQQDl!H zQncIN44L`&QL?%m;~E~KendH<#Qp;XiI?I$=x=zE{{`WVKhSC=M>{1Yk-t+CeNNgO zdc%aJA?Ca#`m%Pu{qJ$*a*anY3Vww_MYmD4?GB!u&B4(L=TKI>9q+}uYH#roc)4$Y zPUJkCNuMtUt?hyTontUc6}hc0E79`r5Drb*gLwb-@O^n0>qlHh_Q?Z~T(k!jTQ6bn zxm(CDc!W!#uh6}s7JDx@;-%ATNaXy&k;oq~YHve=vK&)4t1&W6jt*jNZ6W&3cCRy{ z#~)4B3=#XwTpYOdmje^!dr;odm0Jp}nLFN&)nB{u(he(joDuoNX@-39QIo4gU(n5Z z1r`od;={Fn@guzxLG|s>xvR*X0cv!dqD$qE*5n~8u2#`uX`%s7r&-hTp&?aHi#m9n zA-5{qP-c@MyGd(t%@{Mr{*M=ZiVpkw%JbzJS$dC9=3?zmBrk75`^HZ=y5kWl-oJ#z z&^ws;%znF zH-%7>m%`Iu26&$4g{9#U_;Y)qA`MnX#&rdgdqWorSnWvWmc zcOKuypGL25WeB^H3tqpEO|NgD%Zn3;{Jjrfar1TvSb(fjm6;mNb} z!Wr`lp+n99YLznB*Yr%t_q2q4AL98SBlJCF4ciH$@grg=>L0ix@x>TCAHEV=&zAm& zue970%17P992l6$4zJo!^FuUs zXC^UK=E?>(9T}b0k%yng@uq1M&2_@KqhT0h`ZnQ%D{fpk!J9u@N%keH9^?J%(@nB$ zJNlZivG|og>KQY6zBONuwr5n5IcFAH@Yf3~ZY^|Y^9Ekrs^!5obNtxxM03vE7{X46 zgZX(;3)-o~@kF;c-jf{g;J&F`{>FW({KNgXv(8@BL ztE_{mR_;an5LbTaB)+12Q_e0Gf6E_r>St?nq@g9vqpkQc*Ob2}*s$49H#TywPnS4H zHVALT)?b@&M@CaNSnbXJ5B+%eX$Vcz0@-}9H`hp}r&AB{Cx{>PkE<1@Jg{WwByDPj zYw(+z7SHCGGrEmvA{iD8I3yW`>mDpwz@ zo*a72l_lw(lJk`BTQd6-N_6OxV9J}SrW{x!_u;b|^!;hbofqnGweoz+*p=%(Stnj?YC*NWl{ zt2oZc>p*(ST)8!Z1$*P^mK4wB1`2O!M=)%4JcksvWU^}*w~Q1v!1fOR*;mGkk7V~I zGp07xW%VT$*%iFUB##=L`EwUeu}3j{-8S6&z8)2i4x-VYGThu%f>EFLL8Yh=H#*!v z_3IbAv{f8zeKH|%bABY_%UikCId@6HP!Uc7fCf1@dzA^Pol5ZBd8=*WBi*dXr}fW#|Ee}XT1tP-~I&0uksl>%MQa+gPIp) z|9V25erGf}Sh8(z?B#v;*MyrI*5Pa4uQ)sCIdr~Xg}wSI1bE2LZoo4<|9KJ9t?py# z$DjC|s=^ViKVgcqCJVkAajl~cCrAdlgOALWgB|!$-=40vZVV5y<%T;BoN!sZW0Fra z8()v%>4tnbUXyJ${6cg07Z~{P5kAa3g^~G(@wkup+XpN_&CS*5j>YhtA)OZX1CexW zG%7DDsCZvj*nYN>ue3(R9yQ4$>0)#9TgtN)%aqe~=O`L&|0$`>+>r3L8SeCmK>ox? z>_|_*(>9^F(0mAPxy;AJz)`rT8Hmnbt#GGC4d-;1E9d&HRMrmGRem@^X;D(9*m`Kg zQ_ToFCmvJ=OpH|a8MahXk9-YRy)a98^dv1!**=PAlo^zaNC!;exxcIIY@zkFSzJ)SqbSTqD z`!OKbjhPv4wESYjb&>;dZmr9t6g4iD?43!&N+kK8K|uFwnA-U)&K$Y`i`$P;aQ-uF zE#E_}_B&>)>9ENbnMG&n^ItOydT+GnmG_REZ{Wzm*Z=pq@@2hSP;2)X+gDsd^*_no zDw03;+lO6Phe_QtVE!jtgo=fjEZild(up`>m5JPs+rX3s@YbINpMcG1d}0GC?H9pG zo-dCEhmfAT85e)8#m`O$(Yxh&6xQCxu~koTuiz#gWY-{C<0&q>{6uKMcl_A+4I%Bc zn2~JEq9fYezg~~;pIGz4aXWq*WWkUKTXwnQ$@8zgxxGA)OOt#Vo!^e{KiIV+=w)j!IKh>k2{y((#`-kMp@93~X{6V2cj965cmcep|9_2#) z&2`!CqctB)bftZy9hd8xao}4UZvNuJA-!$nePSlMnmzR*ZD~8ejHXjGx%;*TeKzT_ z+*o|fcV0u^?i=-!i7M9)HfFV@KA$$N zL)*TF)Ld-EJrhm2XQ~me8rieEQ+;j|_K@~oeI9dDVa%QvqDfRjyUh{&sx8FauN$y( z-ZD)3GaTa%bis0$Sfqp{;CgWia{Kw<+!!lpc_bp@P&72Bn#tJ^C|xog(K*B$9q!k~ zw%c*o8IX*WH}f!V{7P)J$c6cWBX|*5gss|=jkH;ZnO$aLd3YQAN%cUU`2*#+|53$c z_9F$?z9}0rPAbQJJ}N)0bFKB;_}ZX~<_FXR~!YX`gH_Jr2R-`o$&Bichl zdnTUf%|spdF^If7AI8U5;{3@19BF+4?_ZRn)6{IV)Let6*^3a}aR;it6=HPWW_+!; z4R?egV0B%x<1ssMME4lh*jJ)%av_#z?!e5|$FMK_Ha4EUh=(#87nz(v?am8$QoIKZ z{14)O}TP9g9-ncD?6`jOW)TmdF`LV{o(%9df$Y@=QQ9E`Th1e zXvNPH9T~LUouB1*+&v|j_hN$?rWeI8wK3e55-aCOBI7Qku(M7t&Wi6!|16m?UXS3@ zhr^h1eh^=O8_pA_WUjoQLc^kDCVz_MqeZ0MkRYyk?$0`xB6xOfG!@ZL>R7kn`h{)T zze7AD2gcA;-sKCo^kkHH^qw~E%oOi54y8|v=<|c-I+Pjmvb(J zGHgmPlU0K0+$oaxrnKSn=dnEft{wkrrSfXmq4Y5rNMnaC+%df~y_Tmi?`j9;Ol-@D z;?_K0B6%XUU@rC&9sHv!=S;NaMH>@77hO*Eu{sY;*Wob_OB$Z3$6E2MEWTweGqM}! zX4Yr^PDeh`Z6r(~KWd-%cIAzf5CaIq0{M(XgJ?7F&} z7;}SPT@KCE=eJEp43SKiiCTTGJMPX{He|=fu3UY>i&KOZce8^#3nXh05#=NIB`;PC z4WfRRDAu_i#}WOLIj4JT&fG$pb&6+;F7Yf>RcIpjqOhuXu1Sby{_t>`Rkdc-_~if0 zmB&xoF|VaLJMYk?$`Td6{qhc}4{MO}=r%%e6tAys#m7@?k=JM+eEdq#Z}t&VRdbBkTV^uPR_Yuy{53l2-vm2d z#9Zsch%G9{zxeftt6qlk!LnP?Uykc#Bk}zBEbPdik2ANrAj&BcjV||t@4Y^l9I*_| z?yN#pqda6JWg{?QCXOAPjnC5d$oSS97&ta-kCg z7u$09vSBDR!}kWxqOH-}VFMI$lM;hG+51>pBvQ%8@j- z0w20R!KKlk;Op}mKVM7!&On>(h2t2}N{_SFn(>Hyeiqjp*S{z}vNH}Uqx zW6axl2?=K|VP?c(Ol(zv^yh1(8)hZ4hi^u3>p6(qHv(nHC*fd->>&^3AoIdJOpl)n zU!5G3G~IxLCJUj{U<>?TiN{U;Jf&t0Y_9Hy^Vu>Na!anH7>9u(4pXa2;;@rNfcx`n*~752Kd;MSs!U>znGbg>d#P zS_>nxR&qIpuH5Tcmn){q^A#%Zhs$;hzG_CcvSa@rZme%+PvcJ(oOIKXy*kr+2wVTmNyYfrd0Y=ke1Y~kOt9hTgP!jf_`e98;J zCz&hfkMKn8AtQ7&iNtof7su3_gNflwp}A};mTujT3*tizeX$QQPOIVII2|5#QP?AV zuDt6vltnpP75`rs6{lCvl#f{##k>4L`53THG3dKnIT}@@=s(fG_E1-xD-MDEYioRK z*%Ui#+oQ*{2rN1<79+AJV61p@hd9i~jel!lt+yY2E>1 z?2QS>_K{YY5K5p~xzbHC z;U}Fr>WdwRjWyw#M#_`8Z(l{lEamju(?MuelwB+x7 zVVs*C!SeQPxj^=kUmLYzJ6p+vdyB@tx(o9vliB2VJnza3Y;ytJvqItR-c30y(39H+ zH5{%~-Tv7)P23yqO=*#eN;=p4po!qldAn=^$1biv~Wo3)Q1j zxK{SBy>BM6=xB`WJHi=uKA2x(ec1k{E29f-81HP%FQ+wld$Jl^n`-e;zBvs~ikBqB zlta4Nux7Ly|NU{~_VxBm`{2z7u1)D|5kQ~AjoAK#cu!TDQ|nP6+jaC|rEe29eCx^y zQSQw1u;IW+D~_D1OOr#I+%-#sk40P8ohezM<7Pbj$cEw89$X6-?p5mZntmhJWChTd z3O&st`6MTvfp6Nd>`@ommdj_Sk;IzFWM(;=M<-MczhBIT1QJJ zsu=@Jn$XVJpTEv{Q+uBW&t`ftqrMa0i!Z_Oi5`o>%;@T5##TSnc~=;`MFWjlI8Ai$ zI^Qr(*h6W$!qsW~9UqoGLe2K~=)PHQmWk<;cr+RSMnTGU~b9RoCu=bYJ=fidG>^I(x8tcVZ zvBHZV>jl!py9Ez-6IN?Rd%BB{pt1~HA09^mdNWvfeNS8>IJ03qE4s9#$+|G%3$)^; z;s4!NKE7tho`20aVu~(@tyQ7*fVT*Xe2(3TRd^M81lC(O!{FjtL<)lp>&sB>P>g1t z`S_c12*vNOW0K`Xiz`NZ?wjV%He2eH~`Nfy*(4V{qk_*eL2z=oyGUQrMRhj21#R{p{~U`drmrpwIkM%3sd95bla(RBdqJBA?ag&EwJd&tko4#6>xl~#LiC>t~uDvxaMha)g2+@jNZ#pT#D zCAOu0G9~p32**CVCvi5bn@;G8> zxKr@T@DW|7N;l$JWxVcwrBf3dC3|V(@KXzSg>^sLu5{m@lJK6LSB0O8=&ImJo}zQ2 zT$#4U9uw?s0oPB;fkJ&~9II7s_HKX=SK2^jM6&FBQt_^Aw&YQk;Z5-h7*9mw!C!IoDJjjxJm8^$zR^$(W)zWAbMQIgX?I! z>nsY_3CA+#DZXicg>g?|yQbH|-(8!l)b#j#tR7=Wo71zqJ(Es5vgn3A3-0~zb7lIK zDzx470Pj~-VB4FE_)%Pl+?f6FKDZu%Lzlxx-n;QObCFOs8d3IB5IJEz9Idy)JbeK^ zua&=_kb^e*>xCP%5IuWu!M3gk5k6)U)~00Qw9iNNxBR)v}ul5Jtsc3v=GoGUVu?;xqq0?sBygV=`=bGn(?)=}_)? z@5kOz2E5uxmB-|Hc=Sg2mA7Pv zdCr7G8(Q&LJ9iG9DH;8XRt)Pa=M7mLYR|oC_1QbD9=)$i*7CC&4cuz* z^43*&e>#e&pu@P?BMWKmmg0HZFzB!Cf~Uf+oU>O!jb0~=z2S+GsaCkqI|*TnTcOy} z48sil@po!67DRhsiRdOe+Q2ungr8s4tRdhn0DH?lbDF&6xl@`5rD|*v3@pY;_?yCEt z;bCKJz3Kq<6QKz3358|VAcSxTZtoq2A1d>(X_Rm>YVt8?ok@dpF>B-TtwYI0FC9B(em*LJYKpRs=^A>FRw&w#UWg(UjVOz7x6UU9Ojwa zMY-gav@YdgXk<3rqUItcc_`+0ZH`~B?2xtI9nI}uDF!-OiqD9higHXBSA0Jz7pGMz z=gS??;$tWps*Bg4$`MDxyJC!H2h@4#BU#YSh`Tup4F^v7&%W|PULv*jMN@EfR2${b zGq;;EPtS$dQtep$&zfx|v-Mc=BJ-Ac(d=0;8@mKEr*9NLl(nIMhZz3LOJIUd4|YD; zlV!`h@TGrWZd*Bmbyf^z)6E0eICBL54(`JbGgE00(uw{1#M8{A1-1JGbK$fgPV3W> z5wWe9wozfpj%cpx)}DTnQD2lE%chc<8J;P=%^#ik#Xgzc#LszCSVLbD6pjl4j|ngK zvhdSx{cB8>3TMeq*f4X6Gwbyf{a2U`^CB!c)Yy#MLaf<+nIkhF%f5YWUFJ%@;YR5(ko6U{ z3S+(;t1B5KHDN@FKit1A7uVD0G|66SsM_(~K_^ap=gtlXy;+ms!VZ@_+0U{8fB3kw zn{d(Hsv9!9#)}CIVB_Ex?DaW@BPHi_BB(W|Od?H=#j#Sf?*TJ}&6Py1zaP&Nty(c# zEu3q*M048q|K`Eh**o!;WVJSkpWf=53jcO^i(fj!tSa{{3sqI=sAD@mp8d2lt>N(1{UPh#@ zFxu8XK+&9M*#F`ZK776lowT1=)>)0u{?ww!dTkz~DQl|q=%Q!B2eA&UNUG1*eI@sy zA-Mz-Ywq@SqRJK<`I^p56(6U?AVX@2mv^$xZ#hId<3zP5zRyZcHL*CNF!+e8^~ zBuGj1i3(4@>KnfKXQ1M=DNos&Iag6K`i5tvA1)2M?ps=Vo(VSzJX{$m-G%na0D zoCDT7i2A1_LtuXtRsBw4RKsgBcRduQNiB>Ts_;&c%=a=2%+EDp?GSV3``fXtx%lx~ zxUpPz`>V@jC$TPs-Fr0Ww;sN9EOukl5I3$dvEluu7W}zQmq(wfF~7rW{GM4UK8%xa z*?k#37M_(1{RMo^ke|7*?Sl(68`+ zpDWkPv(@t6T}*y`0pnLyqW$b*9p#rdp~Q8k z#99|4Fn2C4$&9(D!v>USF2)?)tr(SdNPKHq7-PB?1F#=L7(!O!EgZb?7+c=nz@EBq zP&4fnMh{bA#cdV(nW}M4r9MZlG3RU1QQq7$rMcX*H-^-w`7%2irnvHEa8ouj^5f#> zVf3x^W8@pH)K&f9nJ z8==C^(^P5N(?~M9!Y&nt*eHJkj+4y$@-jo#9jPxYFdKTemi*B_OBSy(q>7x^hlD5g zq2L;Jcb49nRYhn!dlS5kmtowK5vaQ_88rb4?1m}G7T#7z-}+F0XMvfLJFNRV647r> z@I0z1lJk?G-^Nv#wt9FuRY4JBF(Eb`yVVw;kyAGQ7VO4U$CEa9lQjEiT}s6Tmh?k zvtU)Y0T);9#gw&cuzc%!jIVnHJx3qHSg}c%9%YJMce+gfO@3v>ZAxO#jbZsT0mhL z#%rG>vpB|s_KE&XA08%5j1UI7hw^*FXlBibXVASkmLG3V^=7^J{ozo4?LSa5biz_P z(2+~(^x(|0j$Cu3EpI=L5q`JAvHs2YYP%2pce-^Tu-P@N-2Lv+rStv7}iFOR(;WTf; z?^$l_KFNdD2GY58)S3l5_2jG;7LU9W!%BsF_E+wLlB@QaY$F~6H=2YvGo;dyb!0C6 zB!0~`e?zHR4Zb}rx%%1BoT!t`LlY$n9@v(H9Fu8$t&QYt+OT!&2o|;hzhpJz8_|KM z%Uqc~A%T&9y0E@U3eRLGvqNL~EEYsiwR<9OYII|nbAO(lJ(|6*4rA9|y?EzbXWly{ z^Vn)dbR2&!J|TS(CXIMyygLWvdBs+PRwoVEXrd7luB))ido_lh z64r)_CKslD$Kvgp-1t_TnOSUKM_;;8MdW$uGHTdC;CRK)h#r%IN zj2mu5t1&tp9&Raq$}c~czEQjgyex8F1WFYaN|EMcA-|32fqnVph129RIHl{yf2vwBa-EKlq9@t~#t;Z^XObG}&uU z9roV!R%XiEc-!qVLi-=V#Xd(-9iD|cTQf2CXg=CDUk|reGcdz)1%3}*jUc1Zs4Y*y zG1nPbmp=u0QS0$xf{^-#Y*k>Zc%ZbZ60$oG@v# zrgYHwqbjg3-nq}kn!-$M4BQQ+q8#%J&tb@wGJG0w4);dBfKRiR*f8W44s3ph3r*E! zmQ$gL?-$(Nro~<}3_0kI%#(EuIX+X1sm?k)HLWhMO5W+sF*DKkG?`oR1JQF|A%4*n zu<|%wY2Ae~DIhDNw|6cY*@a(0b@^K^r7*F&!A@0p0 z)ChB)ivfc|~Va3lJZGNR8mrRYapC2dE&V~h0` zl_oAUQv8QyD*j`yD%tIHQ0`@p2l0V2b%>VqAqhcm(=akI5pxflqNw{_WyH57%2J)8 zaHr``N}bzfVZZBS9NS!=sT`j*N~y2%IsEVNl+qVtSoY6Ww`@+eM_FyL-Z9OD-C?gf zwN_?r+pLUHIjamkRS(`q##l76QhHgxDmQYzDD}EJAmwyRSo^nw{V(ao9xx5N>n=md z(v_H|m4oqKg?M0I0v)FkOz3bHvqZbC-trjh-+aT^=YO#OraDIp4{VyM2`%Rf!&1kV z-%MQi@|!D9dwYpI;LpM%Aq-m|%4gG?(DJA|$5*=XOt1|*R}1SqRF@6B>#$e&J6she z^^YZ|uwY6BF8h>W@z?WE8Sn`6+J3={HsY_$|1Mn!+Vq~N$H%XAS<&B&ztiMA?dBwG zV0)U5_}}NsfOnO6lm7@|F_+PP*%jPQID*#x`(YWAiFS>a;;-!n#O2I{ZRt=f{yiNN zEH{CYcj$9s5qvD?;`7gJtZB9pRUwPuIVu~Sj~v3a{LQ!>um-*_^I>)3Gz{k4fa#?t z7=Gynx@W(^sJI%;8lpn&32L1DPo18AhWwOQj|~^fy>p2rLvA|r`7Aft?>X^tnkR2; z3FLFjU{+j+;5)l!-0`}Rbd@w^S9v%1noBM~X1_JVWfrX#{?Za*L!8v-fjXAlA0a*x zYdzY$RO9KP`s{tpoQ`_d;-QiZP>d%>J+-EbgB^#NyYuy8M_%nE`)BbrH0$KXsb+Tk zA-jnTD+ih3ZTNDADWC1u;8+I@R=DW$?8%=vUit#Nf+T}?tO|vzo}houHH28)#XSw- zJuLkKt*c*A=B&p3lT=wF`37~##RW&yVQ1mJ@9I~NyTx<4G}(Zi-w5OAu_LW*EjY)| zh^=Jy^wxQeLcg2%Hmek0o)if$aWg(RWWdii4Ml-nkaam6#jir3BiZn2?;NC;+62oR z#bQDgPQF;A{f)PdZ~^8+|0N3WdE#D`cOXiVF>9!)?<#+-?WS<#uCsq~rn zQ5o%~hw4qn@Y(rHF^Jd3<{4h-r_})a%9HTPFa`Q{q6wUtj!^gI=zC;0_HHhRd6y#L z2TQ-B?=q}Y(oxwg6IvO&F>%l;nvS6H_g-A)-jlPhCo?fRj@3&dSSoYn zJ;{VW$ZEmdhb`Gu-qBZ&M03gP_O#fLK=1fC`i696t9HF;nb?)bEIaVw#@0*-k7Qb? zBAIJpvxfz7_2#A=sOHb$I>HNDq@0J{FY0W#`%vmP;lRfa@y>#h3u&K|MlFKm{f3|a%5FVHBNx#^imD#}ztrNtg z%Ramr+k{`LU3sa>omWNEzq!GNX~p{7;;O|)*_ynZVnP41dVD|Llyw7z1HHkGUZD-x zU&V`Y^7=NWQBxj0dAq z`J%cLHN|UKb5EhhO+PL@;ls}J8gca)dAH9K7Owct&!-EM+Dtf=gVm|7X+Xo9hKyEJ zm{qLKhW(8h`dO1B?|nzxF&fNXt-}{VKk?f88BAqgSy4xmIWf{fJN`AsU9H2?W16gf zF7wt-6-K=<f( z$2(H>ggake@Zk2$#%x{RhiDYcs$0SjN)Vp><79qyjAiR%kzDUC{#3~~?W`hiPmN@i zM)nxAh;N6QtQFMTEV+h5ob_z8W6eugqYn~Lmu zJq~L!c#Ar3k9dQ@dv7E1;bl1H6hU7+Ha{nC#sQT~JlK|xnYXf_b$d2Wl&!+q?;FuL zV7hd#jYg-)rC9oV4jy|;-*MIs1g|-QtwlM|o-_|<`%l5p6SJT{zcYURi^6u5G#vTf z2Ww_};czD-e0vAX+dmw;w$H`zh)q~Mdk=zC#7n;JA{LD(M~}~E(QfTaEU&16Q|cXL zi3i~7S<%CCRXD5WGlnh^?OAp!7YFDt@rc|nQnZ;`sKZ?=>M}#qo}cEJ2~Sy*_ls+> zq5ey>@Vkn(8%|(W#$8-|^$1ztDv-b9Av#MBUUOl0@6h-Or}o->bXu|{Y5LsR&4iIE z4jkHB^q0oYoVvnOI$~uW5{)3Ly%lFqY`_&;thgi9kgq>!upX*{L&pe9A4|9e_Ia=!nqd3n2v0vk`&tW*s6I%%1SqnQy zmME&nrzk2`v%)(~R#QrR)*UPDHox@4_9NlTc6L>UN0*0J_Bl~n_r~C|=!KHA+7jkgklwvmq@yiH zrHsW~{l#dTw;YGUa`1b_ev}&(!Es+Ps$f6J08Ve^VQ?Q9C!5*tpWq!J1Kf&ACKJmqtbE z)Xjf~*MF`d?A>X!Qm;gge>u9|IuAYB>-Ck6m?L^`5%TC8{yJ%MpzJC$KIutb$ef?% z+p|wy*>M~ffAkgS|NngbFFlSewpJplLp5GG3p4udWsLb;gkS3m&}vgA)(S(RGHe4n z&Paz%=pgtu9tHl*K=U7&a7|3dn5fyf-DwM^o)hoiumw2RX)|^VJcvKpS@6EM3brcy z;NI^v442%%iQK19>3$d0`#vJ8@drHEpho|Nb=V<5lap2%bJZMcIzP4KCgFr=pL1nZ zk-QToyGZ`in+duh)chLEnVONbstso1xW){XOhUokV9u2ewz6K%Y_iCO+ajI0e7FS* z#Q)P+{ym*?Ipf+G^TtmtR>)^I^r!`&50R|7c)!Yid9bF^nu@UZ{7y;+$ij&hEi4$Z zNpfu}?&J_#KHY1^$U*YlJhkP@wdNevT#IezOLj+Bbo-vak#O=Q>^{E6si{@SU-ty; zbpy@1KZ1$JN1PYW%2fTYSpHd+lTNAe-y&o7pJm8Bi=`JN(U9c_gu6V$gt@B?nC2+n z+u`*Y@XUgJ5{+29MU56CYv4coI!qi&kttV zXJPuwjrf+c5hL5BBXW5^JiPCPP6Ldg>UCIAT2D|0I4x7gZ{46cot~v^R>@YBot{c% zd{1RVLZ-5^{b%Ls04-ceHIhu*Q>EKVJ+!yBz~z*>IN}tC&eJ=fLDvpg@?abW7B9t$ z#5|ZDEyws_#hAEdJ$AKRfw1&h(7Ul7L+k9uc+thHgd=cCzZjdZAHuRRIXFN02ri$i zz`ua~IAyX0V;UBtXw4O@DLajsJ@RqC^%*>yUV$-rd-3m4AsTsK#oNxO5E8H#PsDrq z_SzLR%_zi^q4~JA>pV;boD~oJL-ez|h&@C1p-t>|WSyChw;p3~?zw^v!YAGv=p}Py zjdJwk4rRoUy~^E6T}&VKN;#NvO=)@28bXRjS#Jxh@oIo;r9BXB*8zjSG>5P1aI~wH z{;qNV?QyKzt}WB%NAvYksg|P+b{jtPg6KMFO{bz` zEBYlfW_t{uCPs3WXtcEhqo{r9CtPWb)TM8i)x=S-)m()&AI_>pT& z8T`|h1DZOr{xb28_c!O)04tVWcI6cZPflzs9PE;&REY{>$>`=BJgGTL)LYTCb0TfU zV_ABny=V=+IVoru+g}>Yqx-t?xkV?Q>(iYPUY&TjQ#&@zj^TdIa8@`3v-*V>l_FQV zb+_Rk2NP~7(criTs(kZDll$$2O}n5j3&n@-`#|pT!S2-AB(vXBCmQT%#3q%2JYv+0 zmhXJICp3^xH-)fARuFxP8?sqm6HfW;&NE(~JRslCNa;D&Qa9kHtD4+otIgu`qVeCW zN5f90d~ajJV8xB2DjG29r#&C-_oUeuKe~?y<%AgUjah3>3Y57rL^93~yK&O;Bwm+H zRaW0Pz8VxuBZ)nJifhildjfdBVFaJM#B-%ZEcGIiIXkF3%_XxF)+dTpo(jLMj$?-S z6sFzk#Zh*{xvKFH_Bz|0X;#U!Ioy)w1H#$%tuH&t&t=SaFaFu)Ok?Tqn%%nra}L*K z7vapftWxFt9eVsDJzw^r|KQb2jU($Can%daN4>t|sk#O;X6p#Y?K@t2Jw@Lr*=sb@ zr1sOF=wAH}J?d(5WsRJnZnbdOtwx8HMtoha#n3w{Or2&Zo_BMmU)AO&8~GZg)-2m) zC;GfA^-p{8n~^h3%Uo&Ew*h_ZQbWg^ClZ3VwQWn*7v}1-eaY-m9Ye1N z;sMT*Ir~)t8;k}=Wk#}-_%@1Lw`9n5*?9}cDR=pQ{goO6ByV}#oM&H%ruS8qSKEjW zvdc?E7~RAM*J5aTZ^q)wnMgiYh`7Dwa2-{Qgs1sP-EbIYIXBSL=_YEVlk3r`S6JEY zC#)O1M@-%q{Pfjfi_3;odnZizUuqm)?+yOEsKSCK*U)}NF|x-VgRjFT;nrlL|Gfh7 zv}fab-+5>)yV1@b*_eH8CWg!$g()+Z;-j3~bHC@J(bjw<`<_6paV{R|EW;PgS%^!R zgZINbW88{x{KZgs+V(=qAa9(yVTx%%;m9{1f>w2focvGdcuRxd9Sym4iw={&iC)$~x|070ALy(l zUw^RWotEMkiLArlvun}X^c7NVufVh3Y1}Ql56fH6@O_BP&=>FF#kU{$oF-Z0A>Z*f zLyJvcn)1?jJ$Bk@%#*wAxje?1vC{2yYO6dylG|}Q>&VfGHna+qZ0u#pz)d#f>|9|U zRsVom@=Huyau2H(OFwvv!|+|V5$6Mh&-pI{dfJN-oZAZve|JN}JCX@EY>uw3COBMT zg~qD-;+M@=nw(p!^t^O8-0pn;(k1Qm%Z~Ti6t3E2mg3Q3kK+IIhjKku16@bBBBzG| z@&iKQ+^`4k*@vLb$v?`N!FkH7q0N*zLF!>CWsAZePFz{q!}d=p_n3qig*R1H5*LLR zHu5M7oIk#7`?wxuT2Wofe!pIJY+kK%*yhG=iq5)p#b8UW^7yJg+NA$hnuw~HIP#HV z=lM!W?`Vk~51PY!K_W(O5MI>!G+5VLf=%TaaJ;(}mvdDFC6|CTNfNdB(pbyI$9X~)(}?6@?+o=wHi@PB;R|7ETWvzMGhqld64y8uJE zi|M8v#_F;H92&I_%M+I4`=U%7*fRsajfP?3zLD7baV5&yufYL>bj*D+6T@z1BV(vE z+>{8{{!BK`TrLzARyGF4tV2bP^yj&r#Q@dYXi@eIdo=H1R`zGf?R-F%qdLdltHYYf z+FUNV=nIuLJa$;Rb*|X3-UR7lc;v&UTRfS5qY-sRHRqx}q0BlE#i{o~*~F+RQ@;hX ziFuKX!^?>WPb>0H-=mf`jsDttYE@!rgxZOlyRW{cfz!^Ae8 zY;EXG=b?5SU2I27IqOPxJ264ilB18=v!{BXyG?_L3dE0USEGN!Rw_uusA;S)+vj4Z2_?{*`5CclkxB4(n zX>3B1`pe;;m4>XjDY$1Bj`vp0aQk{9y3cgL=p~j|do&UIjH02@%M9I5`U>-=GlDV$ zF#EKzu>KV!n?<1R^sz9posQUDtC2Q08_jRZ-Y{Z43I|Pt>eF6mt>J-2%?yBl1&WI8 zVCC7LCCY&NyA=QGmCBE62b2%89nq)N0O+tm{RIE%`gscO*Bs+Z)PjU|7_T9BuuPZ!G$&CMW5+6n4Zd|4C zf^*m7<<4Sk4Lg7jRhuwFtq==R&g1p?{h0ec8$%0<;n?X4?ma$%pccX{2s@7*&K2nO zX%9MXKLm$=;t`r7Jrt96p<_|0oQs#Sbm}2Y3eH2G(>Wy0IE(bY_wl*j1r$EYgYSr~ z(g!sce?N`EA)A&M`&77Og_5s#`l?*~kf)q$SDvFUT%EvMQ=%m!4323LKn6&bZ<`A%#P`rT z!kUZA9N5O!gGGBh2>Ss3xEIW6k6UuH}?Nu@y$MM~lxLqr8sLJ<^E6tPh3 z4(!50Y*4^;)*F*cVcu*1}!n*SL zwg_hL126Ry_HJ?*&kBFHptTQQO!ecdEI0mnE}xNWj%?{{&j$8ZG&e9}r**q}q}OkAnC~@|8}vgBjGVgWTz2m~%0je&gc# z=x<*(H_PDMAA>oeG>OAkcV}9g6iyB9&SMSZ*m_+@9+Wf2_bNXYym4oN%u6p`Xv_sE z28{YF``P&VtPRv+Tk&W0lD)?+aTYf1E547nE_|=vl&j^lW$Z7V?K0`(i?e69e+%aI z_hq(kAp8CDqu-}yTt3}f^4!k+CTzuvZZ;hBkIX~Gf4J|o_XTnKhT(Xm_k7Cv0lX+)?EASjojEgtOO#I3{=|(PL^C{_{P7M?Q37 z*7!)iU8^wkVjvB=`Eu`f@}^fO240O6=6$^MERy(YX&gVLwB_NWA>!JJWOPsh?GmM@ zV>^`Viw1MaqMme2>&gwnmrocQ!chq=IZnQ3j8fgW&8i92H%UfHr6~=^nQ{0&9d6wq zciX|b;-u7L;L$(mp(0K)U40seKiAc(22ZxtXXG#~uKMvEO3SCvD5*ih9(7*$^9}pf zf5McOlK+{j$*lPAIGL@=?8^;VtFOgpkt$4*+)>DBVKAv{vr2jZO%g4+UBiy|A3HNY z*_{oVH0AI;jy#%a&(Wc-oFey=?njyn-_o52`}(r}h_+1Z)=7N5-MGI&B!8?4~Xjry>7ijdO|gd zJ#WA!D}Deg{UXX@Nd!*ERb29Z+=e{ zz73N7)Btf;_r_LL51D(}A@^1^?wF6o*DbTrJG2o0oY;ja(mSlMzlx&;r}6pac|>h` ziEY7e;p%t?8N!%3*uFkvKm8Fe<7W&SqAvRneWt2wam{AQ?llotU>|KJjy98SthKP1 zjX3OrIu~`R#qLWlF;!lNUt5kN)c6jLynhPM-It;2@BkgGej%lo8p}KVz`^?3JRuxZ z!$4j3Ze_q6;ovt$6E0pUev)$e{PeM;jgB3U%6EBD2M6Zwv}7BXhAf^Td_LnJ$o6}I z)yM8&S#~*Y3@E`!%k^ktw-6qlImnXpj%uu`!O$+Tjy0A<$q(ymH*1Qnyj%i9ir?tww)lZe0fEq>rY-3DlS0DaSe)v_~ z702?LVRh6K<#h8b#bTM6Vspg3Z04(%AsPKYmW{pZSAJ}I>$2WSrJ;4$@|UqT4{69;eZo7eUvd@0j83EDhbm0byoeK%D-b#DIj)OGYN_T2=(QOubFBu~4n(>%ZvKq7Rug1`d>A2Q?7FGxK-UN1@D@{npoQ z&H2ow1vB$p=_OsCum+|~*w=(N?3!@E5n)scQ=>%o*sG*>a6sHSy)Ot)M6&iXlWdrC zM)oM0uKabh2@|qSneotuOQgSVWNppOos605Vath|t!OB{q6iO74r!&%fgN?|+5Q(U zi3i;K*K6Dv^e&2x<+2C&YQ!;hdhBX0?h5OM zRK0D%%~^(=xl*5752@?uKk!2o?lu2FTbIM-&%O*H%5sL%a9pE_A6bbRIaDHJjykbJ|K-B;qN-%b< z4o2zmk+^9(6)&AvBF1YAva0fMLv<~3$|pgqT>kq`4rsho3$9zYDla$pRDxeDQK$-AV^cTHG01 z8pjCZs}nqKjKj&B^Wm4X6%MB*^SFHrx?{yKOW8_&q{vfhe zR^q+;aa6kP#DZog@t~*@!-dHjn!FdyqOM@{ugmz=@)2fA>AC6DojCb>gE;HwK>PIw zn8Yh+oNJ3`W*+cf{a&eiv_aW6xlnodt3Fo8zf|^{R4Us_8pAccHDbNYu&0M`(dTr> zt&CV)^=^S(aec9(;~bP;5VwZh_x|(e=A0OgGHTDgGOtAcRy@+fht?iWoZj4?#lrKi zkuGJ%4BBHnaWf&uV`d*AZ+=pBLB+>kFKWYhEsL#j@ z*3KEsoWmm+x33>J74_k+NAbcDkn`f9ww$OL$f#jK)O!=bYRZ25uKfMdyB)YmzLQT! z3)iM2twWM{-n$Pi;<`&#yen1PMY3-pnSB7%acINji!C`LyCs_i`tea$cLra#r%fj( z@|rzANrqDYW_PXxQwkpo_dD56h%MI9Qh$esCS7pW=O*Wfj!r^k3ib$2`NvS10COPx9T2tPg zBYAYii#x4baeS3OZ39|x#&Tc&^B|B*cldGVQ1MxG_NIaGo*sF+an=@Vws_Q-*AD4& zg02>aG}2;-bW^(ONIz++Av5|l=Di{p_TBHm_QPzc5_s29xvTx3IoG3#Vvv z`IH~ne(M=FT>6Z-95oIws>Pt+Z{g)F`Aq2*%>VQmH@c{Dhi5}NiN|HoWK|~gXhgLP z6Moav;j`Pi93Y)5lrvMebaFR9t2o9NBrLEsJxU_;ZH~D-L<_{d958-|*$X zU)wUdc_+r5j_1}@5&ZXc8z#+<=AP1MHdqg)vce)t;MbLm8D4$#94No-4yr zteHICm}Osd=(k6OmC|?h_Ir+xU9aJI?-Hm@&Be0rtKoTaF9MGpgT{suoVmFh4Q~ib zP+W7f!|tHF!8N=&`39@(ej~*2BX(QYpsSoKvqWYve2E$>)a!HW-Zwb%<_>NYUc&(C z?F1FePV-Pa|S&8c1%i$9^45rBuxIZx+cXo6~YWJovd}9r(1)UI5F%;i_%)prI zg-FiWgD-v+c;I;lmEsJ}&b)xm7O(Jg@EZ(Iz73OmZ%}eVmDL^oVzmEfxXo7QpgWR- zcqlW*a@mWCW5rPJ2jl%s=%jDO@1;gOrX!qltsfYc_zGKpNZ$0=DV&*lA9?qlBjbZO zCLEr?x7S~M@~`nLTvU$r!w2ZOgs>k-smYi#}rCsinyzlR8*@c!3hnyXgL|9G})6l-DX31OA-ZA@??IOP%q!YvRl4x<-;3#9(nvUQR$ht zNO`y9nsW4cwc-%c05N6NN@1lFZjNY=L0%4cJp8iqDPp*i@(r7CQpcZ$k@vO1vq3pXQ4;O3A;4pE&ymF#> z(`MYa%8z}_f>=DKHJcum&(C)!cH1xfifBv5bu{J$4K0q9J6~ejTj;Eip(b-kwGQiY(s4b`*&uv?dA9Vow3pnbEmym_{?F&i z5uYw28V@nP-epYCyaI=VNAPfC3Hmh3hxX(Z_#oWUK0T%*C}1f5ESi8^t+g;I*?^ZF z=D@h;9Nb>95m)loWB9^_xF$@83*HB@DWVWA`fG4Kb-y@{&f!bu4ZJ+{1a7_VWB#Zb z+%Ec#Rp!!B43^hIM~BxZ81kPbjal8aF;&uRxT&Ey8>f2lWWQ!K{ne67FDtBV0NQSi zV8c-%tQ_UbKlMWC6cEBmExcHJzJ*Az1K9Yc7aut^q4qI5zHo8p$4EQ2kiA^!J`?hb zbaH0c(098PYY)3}jm%Q4+FNnURJrfEi+kL(2^V}2&za+IQ7oL8l)^c?sK zvtM0>iLSg^Yx z@`77n^yF@MKg9=IEVZ%Fwj~y~^@FPV5MgSLkq+BJM4Jjja_<_P<0>qUpNL6+`ye69 z3A**PP}DJBc{E^vGB9zOvaSCn#olMNGLNN-`Ghu#xkZdJcluc6xOcfSZ{tVhqmwHB z-SSB}wcQ+QeZ@Po)CPeL9pE0`0sA)q$z2EF`Q@>Q2wsh%@x}1^_z&j($wGC=Oib-D z4%jBSlk0a`(n^7s5z=FQ?O(29Ql1^{{M63aq}2jPHaz`)nG|Q zE7nLZ*uu?`PuAISv%LSp8aeRyWmh^4cIVMw0knM>#QK*axoTiEC$5a-ee=#Vyxo&6 z&ZTg}yd*iR_2cZqVVtovgC`cGORsu3hpkU#(;t0Uv?QKG?K;tHNH~+h%xbJs5v!3>( zw{sGG=k=vYzVy@+;&|nL6#M2XOzaxS-Cmwd70%ek=hnQv*^tR=WDaspK3}2I*?wwF ztsauo77uoWh9xy!oY~^BaFKR2VZ>)|&VA!clV$;IlIqRA(hYB;7s#3~eq1|T{5vJh z`N-0Ry^>v-n_$B`(%bW%A?{TZEsl|MQp`C`jCVA@vW-e6vichw?4JT{3 zvU+_>Mz#v#?6(k`Y$PLm+j8a(aS`Tp=f@AR)EJ$>b6qZhqwaLBVSs?lq8kWx{W9POI|g zRDIr3(V(f>cQjB@W5>PP^r`)UxhmDrYWEdU!fC##`U8!&zD3$5@inGta9mOio}X7? zb?=4@KBC2ja{nF1hKv&~N{Y-X%`fRP>se#|4YH%HuQSJMy0h6_$sx3IhDd;Yrz?>)(ep5-RI_^<)B>w-fO!XBJp1O2 zkWT@~9GC)~nrXtx$;Xzo-I#jwGOX_0!|d?NYekxTGDVkjt2Mb$&i^Y^&Drm*6}R6u=7%YooSR#R36tOAZIi2T zOFDr8%kRQu?{oC;e+4hcJ;ajQUubJDJJ!HDoc9rK!=_%pp526X9%q7dU*C;ph z>RdiCB=6WZC0IGSa+Wgh<#pw9;&UZCr2+C5y;gL>+~H8x4$0f>FxlXoVsK@mGCks5 z=&{G~Wpieyhddl)RKC)6sjGlI)U)yuQBNx*(7%%!7H=&YRnJyYS>+C;k&>$(0X`SS2~&Rb?t% zcH=E3MOMKj{uF}Ts?dDWIXqi>8NuRenOaYnVq@Q9Rl_>WEYM=l*19Ya$4djq{j;4d z|9K_de&JYFzWCqw;B|7IoT2*=A(HKUaPJb-o*lrF$9v_yn}?0(q@yKg#{QdTBBu3t ze083T?mO4OG$0qI%cn|KWd=T8T8|4U>u|I2A_SEcAkN_+QXdxJXV2C6d4I1s#?NAB z;%$T9kn0G^+CwFSo{i6<}HH|qV*M^gZT2Uj*mTC{&X&U0q%70#3$$equj)E^Z<-$iX$B^b%odhsWpDVb8+1SoG=@8k*h0ilRqYaPTHv zoMZ>n{tH|UzTr%DEuI&su+{2%+|;2Vjl1jfzRYP8@9Hu8wK-kK8StREn|yBB@W`Ad zY~In7am9MPZ%~hEvtJ@Nxe8a$9LCsIr84s?K$|Da#c?zq1xu39Xn!00( zGn?k2vt)#VlQZ#TSt{mTcR^TzHrl4_QS55srJs|fBvx)#_Inp7!zotAx7u>2y!WQ}Z^}`}U3mc>99b`ri)(@zD{OP~ zr_o&eB!a&kgvJumi@QGd;h;82oOLLT6V4B3-QNuU@f}RdKO-2hIgQW8_T}^Kvg5Yu z#9zDH(la%XLB*{(+%S?+euU!w=!V1Fe#a6?iF2XdA}?-~+-AfL7hdwVqDqk!_XO#4WWFZXOP{uvm9SYP6Yb+*C{6|| z7XEc+uya!$sI!s%yfaVEYr&Hh0o0YbbiHR0+_ox=_KvY^X5O6|%VL;xp&KWscVxQ- z5$s)Du+R;Wj86l2#tP&==#sGCra`*+` z#WH`Z*5R@II$Y{fk1ss*IDM}g)8~AJ|6f&3=&sG~KmOp(_iACk)}q-OHSsEbM9Tcv zcvPxJjjqzWQ2z?Q7b;YVZ%Dl+;tOAPMlVel{6<-M`vy*9#}GIQmKT2F4=<-w^Q{+w49&UbS{OX%V+_PGCNW#_}{^|{m&VLNM zTw$IM*pGE(YjJJWay%JR4E@<% zMenJ#@VckNWp^8KcA6$vn~3}J#9NH|eiwsvu4B;SQaF4pMQYnT+-;SIbuvF0e|$4M zZZE>kk^-!2wi6G8iGBItY+UQS9`;*vp{ZU9zpS&UA9fiUHpK|~wFb9$<%xeV7vbM0 zBlA!O8cEjjU7O+9JlqFCF~0J95=3H$-gq)P6W4w6pb@?Y)(I8p)9V3xwz`bK2^G+5 z@eV!1Kf-IveXMx;8dbJxeArI*p|`$cQ5P*jnei;1GnN0S%B-sSA_RC(7f1 zaY|xAxH3;KCZue}&$5*feanL`9Y0nyLs5bg=PEsqUQmup=Q~wj4>5=DD<6N^$aB99 zW^0H;Ex%kb$Q`7#==miyq-t{6%=3Rjymo4o_m52}uUECREc5TIP%opLvifJ2mT%~O zu6*sB;_}AJ*Oc$>cfIUyN={jejK84*cmFCXV?HXL zUIqvc6mO?vM|j%BAm#6H)H*FdcDKd&c6lYT;&#BduY7k&C%k&`X|y!_7XgmX&_1#b z-r}_ik)6S*Ovyh-8E{rFW4bh#d#&`B2g7helibfBO)a|iQ00ZBcNi2>g?6`3!#L#%wpg6S94%oOG_S^+1>bS#_IsEl*CAq~ z7M*=`nJ3=psJF%()y9_Tavwai-G(|7|MxxklF66g)=YfX^Dm=AL?u@3D}^#+FJ7wT z;pesGNYpJrQQBPe`aKqZttQ~XhBf%OFAq-|Ohe4C87Q8fk7K>^aiZNqSRE?BvRMc5 zJ)jUydsiV-*^AnH=i#Su8~YkO$AfqGG2r+YJS_T(z~1W2bJgO%%eCn$eZD`Pta<6N zC4HCLh*#H*b8?z9*T;*cyL>n|Q=z{axV%RMRp*C@S0sQVY1lcG z`xmw1oFHK_ORn|QZ67u`?#wO?8?%|M4JUdurN(3@YDU>`rEo8H$Sh?`l@-5MTJfHI z_a)tJDj6E_z)6?q;!x=gt+VDGbz^Z~*>K%iasG@mrmb-7?#NDPV2EUm)IZ~B#cMQL z`x1sfui;C%u;ts`MzH4-OmF`cHlJ#7Me`@ZbX7QTlnRC0%6nCMOifo~?Q3Bn4Y1^T z@y+SP83>2kmf_aY-~4I9#sl46#138*_5iZPA#U|HT$ve5pRd}|0C z#!Zn2ws<->ZbYhd7p>KEk=|UG=-v8Z{25nF>>}N{*saQvr9+g*m5Y^>n!U>71ACOj zx95~49?8nqZQYgNoHC?^(MouIt>Rhv#?3E0JGLeZrdOm4rzPvLhj~g!q=hx z%wklCcm4CZGWof8VCnB;NIZERc0bDz?pBOJ^Dm(Jh6)UFD#EiR;%0kv1q(G!p^4sZ zyk2t(TC9Tomi@RVJEZeE7mzXb47R#Fz+xS_L#+_rNV|=2w9G>8v5{yL90i^A4zPIX zD&F&IrS9)W#qsPOWqh^{Vhrvm7WFPE=GUym0pX8N{~AeV)BzQdy%3)kj}XfM#5U}Q z70bjeRQKO=<&+CC95SRm-yTr-qfaXi)AbR~w6YsY%wEMda$8WANjsX z;L}@itO<-@y{ifjUu;W{>@c>M&sN9o!rETp&qp#}Dk^NkbJnh`5=URh2Np~^Xu#y_8HP4x@-z9f=&Xqd9y_sGe#5`fIwM-7C?&NknRvFC) zn_{G!oWQNW`tkUc5zHDsl&eeCvTgzVErDx z;)nF&pAoHib7lY!MEKIl!-HY5UK~HfQ92B6tk=(mi{A-{@rgb+T+x!VoF>a2n()DB z3mz^Kw$a?i9J^l_->)1v&fS*1M!Rs=ke2igYRzbgm+w)NbNuf%)RUQWKet4-sg0&( z<8Itn*pdIZN3zI5VX1sS6v=(K;cn7wS!bG7MsY`lIKs1f@ms5SerejCJ5xg0D^czk zcM|9m-j4@tGib78ka!N0Iq8Jt1NwwfFCmz#zc=So>B00G=E2$5?3k@>&EGq0`J>d3 z`HCh#YWzmjX)VrA*X6HUKj5bJ7oIx0yziyP3znatJLoS?zZ9Z_&I)5en(eL zb^2F)MoHWE7;2@#yeaBDB|E!4G8@hm{!Z}#Er$E6QpKzhmtFi%UrUGcob>svqj(k7 zWH$NIi6KMf{GTY?e_;+C=x)y?fcyZ7~cNPx!Wl&gKj)J-54Z`k@X~ z5g)jhM-2bHCmBnn4L=ly^IMNNj@=c(_cBv>aJ~aawfyh7@}DVhmEQNp*rk+Tki)YhX| zI)-=hHo(+sA@H>TZP)LTu6Q;g=FP*#N76OyCv(Z~rTBF7JZg_!l)Gniak9L`E%|R=h!zc~@~^!6`foxrZCsFVON-1xD|CfK`4!asF358h-tb z>yM=l2}WIzOdt<<(9O&BF~Eqb7uSLv2dpv zS9Jb{67f0d-oB0gmPcVXem}O&&4tO`d2l|m1ic;SLakj7gly^pqlSHPE++`DA84cA zTy=D9_fnasJ4Gq*Yo|EO92q=yghu(sJv!vTR)LosTCJv#n6qu7t`uVkNp5o*_LTUp(tLC4BY>y&RfKCt|wZ&lgM-!DUy zw{Oc1r>`z=8G569!M0uHU#{ntXVg}d{c7{#SU|5J#Ugo(az52xI3d@S<*z?0uCK4l z?oJ=W3{~K!YJ#m}h)XBC!nZ{nz8oKgHS&EnAZ9VLoO5u>@*j*zJ%kH)j^XXb^EkCh zcm;XY80Gx~kG%iDx~x9;576h)0t5cJXvFX0q0O;$;EqR5jFr2*frSspU2DzeRRQGl z=5&=Ec)9e&-!`z|#>IxL`YjyaI2C@o`35uP9C@bIDNO%Vg>TLmQ8fDsUJHBcLscyn z+kM9C1wRlV^LQ(FJ!;(-x3pryp}Dqv?QhG6(kGcd;eS6@IvlD*)b)q(FujEG?iKK9 zPy&^ZVvH?ZCwG-KNHyPrsXG>8{Frg5>plkc$E`w9(0cs6J5A=R({Od?dVDEak3$E< z_4pSi8diw_StzLui=fvqe|1|z?yoqK*c$RX!tNq^4gOU6|P#+MG&3h&PJr<7p0@q{l=$Ud=mhZeka$dS`@ZTLJ#&XuZ8Jh#i1vElCA)v6h9 zKend!RvVT^xzStIfhOs)hc>g~?Fc8Dxms~Rv=Oh13;4ol$s(s4^2P&oE{IW=43idP zSAD~U>96rv{}nO@-oTn^Pw;%Ga5{6IVQ%RcoDca9bgM(N^(ySPsvg@4OJ76013weg zII%{LE5&DCw@=)?dmAyJ&X#l49XNE3DSO`34ddk!w`AQ00FbS#G#Y~^{WcbOONNy z#Q+TXE%D`(DC|ez+tM~eka>mvg;eY(lr^p8P z0wU2Xu^j>$g`(G-RG6(CCESY@ut?s9%C$Rj+k7b;uV%vN`~Z}knvZpl3-Im!VvO9e z2m>GNfU3qWboZ2u@7K-PV0IENJ4&&;_f|O0J&fTs*HF6h1nT$Lhc_QD;AYoK%&Xl4 z^LG28I;s+vl24=2%^k=aUWUa5;*iKL!Gi01kh@QMakl5s?&N(mbQHJMv0dnVd=uU# z%txD{qo6UigM9xsMZp3ubV#XD>THXZyh+EDOeAYXqS&ysA0^wIoTka3_lLq(` z-$eXWJu!KFG$ws+iIf52Yg3(vG{5Qpe-B>RDvk@9wCCKD3SW$F#o^z4*zlv|E5l@0 zNGr+HH0ALoZXEK-jTVOk=(0)tuMv^#wxhFT6Qh{%BAOqAdo!YaD#u*x$$rUcT-9v^ z|IW@}e$_yRb{xUiacPY1+Lz~R z5;&n_43}@~NaOiQEVt?-Grw-EXBWqaDsh?I7XG+;I7`lv#y4B?u%{1~HV7o|d2v$L zrowS_WvivP6@(Z-rH`nd7KV@G;?l0D#K7aq55N&9_)T)z|i zm>t2L3uR{P5=$+s9t?aL%ih(J746lDm1jFJ<3R`)1^IJAg!~MuR3(#m=R4EYq56KFI2VDl9`7tLn~xQtPms;z^=dInj z{U2Z6oYR&qVmnc39#3`C2!0sSMm`TQEVPfN#IpG*H=O;~$Fj@1_KdR+W%;TOOcM^% z|II6PLmJcRq!ITY(4nQ=$18;mzH8?bT(+r1&k4o2DqVp6@?2@}C|>T-dolXbA0cLj* z?0gF*DrGR5P%4?9Je;a1fLUD$1|8f4w=PStall5bjNFBW)3UK@!hH1d+lW>V*8v|6 zqi5!MXsKPmtABQ3;es`IoxcvRo8;lo^GpoN9U*>}Onmy1jObcV-0bfKyWqAsekTn! z*|YHHSOGo{-H$bEub}V4$KrjcM97IsIL&#FeDyELyDl@#3Geaxw;Jz0QRS6VVL}DT z{ySW{N3FH^!%~+`zN_(QoHifWn{)d*>3f_oV2GTV8*Kd!%MEXEVd^!wOW)(O(>>H2 zk~z@q3S5qQghy+BV^5eW*C*7W=B_4lZ4Ehhpe`?{>vM&=H8)C6;YkBK&X9Tk#W{_c z5^hhA?Uwu$)`X3X%{k(x9v3x{dADT4!k0e5E!#Wz5mAOLy@Lq(kPB761-Nu!sXR{? zVAtGU$eqwlvQvGraaIUc+}Fb~9d$fk_(+-NKS0q=ZLJu3JP)=#cfV|G*AC_HgLfR= zmP%#$k!i~1_|wYdFSnExKeSyYPT<^_$2v6+g_3I&|;y8+~e3#jB{tRr?Ki^NbOP3VU$; z4hOD!@5mW1#A99M!|J_(k_8Il!1xyQKJUWi2Er6>Z%LzO#$0(oQy60^;_Q5bk%OwF zqa*(HmBQm2e-=9HD$&x~+H#o9oeCo(aj0rrc*DjK&wXe9%+e zf<`X?bG~x3XC=7m5h^cSM&_SNM2Ium&2yhHTGpdh*p+qrw_@Ph#W3qJ6?0pT!cp-& zr+&#r)9Oqt_L(XFT!_@@0(_db06%0W9;dn=Zw?CM@|t)7PZwkP=+kKY`UXPgJ%QJv zduZF}E9UVVie(SpbeAS=O0}3!E-ZBETwjs%+r|0T>{BFp$5~$7;qJ*zn_4ni#%o*L z+sNw}K?f)4NbdGyKiShgPiw={CoLI%%$K$5Z8&Oya6_%V_)|E5L3zR4cFv30H6ASe z$DZZ89e8Dg6I&elj~CL7OFnt9-UJ)UiIGwNx^a4)IGZn-GCALxjpCizTXLA`{fziR z{{7s7#`G*S;y-tU$05(yq7LGA^ZtO0J8val`5JojZenM>=lEoO3ugXL@nXkkak+eh zSJDqWxG%2OyY*OqPeWdjds{|jeHIUsJ$QQy@$MKfQ9Kx5n~U@PR#UF~VkZ4|J=X7_ z!nZwN;%t5eZq61*ME}ER5MPKUl`EmgS?F>m6>a;9$1%3G?8u{`x7Zes7n@`InK+!X z3qzu-G0vH`#BTTAC{byPt1c$UmGh-{iOepqjSv^XRBSh2i8bz95%^Ph>&J7^qs26M zj_Zs19i7EF(*V;x?Nr7EB`Y@`E>>L67AY2oH!9O|Pbnp~aZ2+&U6nkCEG6aQb>*7L zUuC|tKD1B$R098;kVqbABxe_kXg1gD#D>m2x#EHJuD^EY1L=eJjBihqb&@A}(N?+{ zGB>jG;Y)8HZW8z6^ab9unCw8$f8@^BsTupqzU0h1bEeivCV7wr&%%yD$ujT%ZAx#s zll69&Y~xXPT2FOj^>gXT3=QVz!XUP^3T10VaBPQIW@X24RYD>i^3$mQW&{_v%HW!J zy_q<+AAM8@a^2zX?7Aq9>Go0lGg6^Rl5~;IHsgVXuB{ur`F?!< z!-&sr7_*fy`&>&*+2pjG+2ibZ!&owS-8^_|PD_40#IpN*{c_4Y9Q$JsXJwPr8L)hg&egx-qkLjM+R+nCmh_IO!_cCplm5 zu#&80l(2q;FXz@-9)01F^p))6k{{8GeVHhKubh+eyK&8rDDF+^K%0_aejF1Zy?yC% zWr4Lt;`mk*#!f;!o8Rfl#NsY&H!qw%>7n%V>PR)~L=KPW$03zNS-f`;vldCda(WjI z$!W{G(?Zx`aSO)oa$~bC?$Y_OW#CLJ9^Pcb*kEyjU(ll3?w^SHpuu+Cbh+2!C&nqY zh|baCTd(?DweStzjQ9cLZW@f}B;J+r&tV!;2e&PfuRQw|#*$b3u}7VoXG#9&z*j7A zQQ_?U4J9j~&2+2!Y#}a-g=ycoU}ht(jtgJ0#O)^@9BshO@UXw^`5=tGa-(!TXj{>{hOJqHmne+vw+F2Zbae}C}ag}1@k zI9Isp~{T`XuUDBS0(DS2U7&r-5oqA%} zvgUATBGUNE~e0Oh4~utGS)jgFKcV9Yhlk9&^o^RK`&yb6Ykh1uu%88g2=#4_iP zXd=7rjgRXwY?FA-#qqvqrsN$|H8?9@n`g}AuCQDB;Ev{09cWGq>2;T1Rp+POwQ{C= zEB@_knE2of4qSK$wt69{yGpdmxsMo!9~hZekMZBWW2JBxx?VP9c)2c*Z`I=@@qk~J z*?7D>4^E79pzazA8nw6MGMN*V8_IX;6ftj9cYNLrk#fo8!&SQy^MFO#D%aX5ej z=6QI1Yp!Ggmg0%id<1>y1)Zp_$bXfBH6H^pf37Y(E~=nw%{|3_;}B)ulNQRom5xWt zT6`$$)+wy~p8b@giB(}r$E<1611MK&?%q(Yx6#DHj62F#CtH}#4F&sK7(~R(JSSZrG?`G93e9h-yVJKLoza2Qs3hf#d!Gy*5x64%ai46v(%|Ioj9F{wU}+3WM^ zb3<;HXIPh##+=yQiEDJ6nY&CJ0^3`0VfWUOEox1NIW6ds>B?93jx?EV!MEQH=_KB< z*V3=rVf+RazEy}AdJ>nrTtO4B^Kh70g@6OX%5cR^4nb-&PunTO1=O6T=}lgWtgcw#!rLGNSs!IvNMO^+GQU;oss>v##$_1zZv)T z%*Xm)<8kfSSPXcYg9~TH+qiYQ-1BC^&t?NsHS^&Ty#R+t<-@0XAJT0Ku;ton+$`FM z1Jc_y-gygSOsbJ&_YnSvzG23bZ)pBUoo8~j#O0>VHx}XL#c95}zij81Z| z&K%%Oj}?}DlWNFaO|AJEa<_9e5{7~Xqw3V;*(pE!^|vrw_6Cj{U*kgFO~n0tigWF5 zqNmqAXdU_lqkn60XiyDWB>WL?lH_TpHsV#`c|QnS6bzQn$KPF{vm}5?Ak4_88eK&9C|? zC6~r4tqLoYuKqPjNxmMotgThbtDR9dI0&a!S!0JrI9e|V#1jc1un-% zl`S~9XeZ1bFTvM-GqC&lU{p29!iS-S$iA@zeHJXnnfE(kC3h0<_*_`Y`|sSi6Ee#` zjPEMj@M+u;?7MRnr=3o~=k9)3#a@E0ZzYmqi_tXf093D4BJ$cPRCnEpxu?o8eOeW^ zWgS3=_Iof)a+*n_CF6A9K904&fI;7P;PRZ!sOY%>FRu@WWydbiob7FFb!6Vpnqqm}Vy9`Q~U?5AemmIsI^Hm~1j>&MURt#aZ^+28u8_uWx{WxlOAC3|Z_sAI? zSrQN?^S1!@_X}d6^ge!Vh~$!gCExirl26mS(a=JA5JNh0mroMcO-zt`BO%+(AD45aYukD*`y$#fz4!nsV=SH}1UZ$@lU(8836mMwv!j_rsVz z^K95D-<0*_Gi2~nvO%j{IP8aq^wB(MwbF-^tV5Y7ox6&03Ul^GF~2&7E7M{awqJJe zjnX)(Y$VgaX7F%M3J0G_;jj4(@-PC+asC_uA7g}!3~IorxZz&tLTAbpMg1;#ur zbGYoNvt<^XC_1-Z!%5 zO2v&;d!>((VZ)w_oVoa~Hy6o{O4MWJcERJvTy7NlH|10XupQ5hgFwU{= zaxHR+5Jn4;`rjiN>YI3H%XZoSP;`4l-=l#5%XsR@hr&H>1O6?D&h3wnz@0deZc`x4V z0=T0aPOSoK(ad<-*vozR>Nwh6w2&mv1F4I9`}H3s(9@zMn!9Q}mAJ2^3hi>rOO2t7 zM(*rK1=8b8Kg!Z5=xC3emVN3 z517+emqpCQ>xa?A2okvW6q7xM!KR0(@%U zOAi~)f6STTYf9HeHk9q?NW;u4`J~H=9tBOK$TQpr51C1KYUCs~bEl-U9;C`;rsi4& zoz3G-itXN1$9G72i9ZEA4W`NBNV*#pPcQC;kym>#z3Pgg(~&Xc9KL{L>JZ*7kEQqA z7g5FDKhm3{aBy0Ubi3vC*8pj%rk>HB^o*4NLFe&Zod4Bp1l?WZwg+jq!2n1{ZO zN-XoQhBQ-;N6od!D!YL4zdA6G-U(;Zdx#(F!wKa(EGl_};ofnqG`FJNYi#L=%#7@_ znGL5HKyaD?7lYdo5urnUh!(xVQ53L_%sOxmQ^LxTsLV%P3-eJ5PGWu3Ar$8B$C`8v zb8e4eYr`cBYPrK|+=RliD#X421MF0#ICB3-RK#WDi$B%a6~S3ldm>CKeIRf2!73kb z=&!HF<+2>S)g6O;ly}$W-@%sKPq1Px@8r3)VJK+`!<%2hTl5D!Q@F?fViJ8j@D6oX zUt+^vG0B^)>2A4%Hgm48y=h85znIfuJ13GA@OD&=J#CyJrm0g#VA${+$yIIKLAnMh z^L^_Ky|6gmf`+L*(0u+6)G6Z#+cAs>7bP@oZbvx^OZs)Ol#1(JsFJrs$}RYh`MVn} z-RVSIWp0!c!z?b(>2xlc{Yon-bG}8KFT6%T6Z@a>?XWA+B9`_2gSo}1s>wwtbI}#1 z`w*MH94mN#sJ?h5O17yGRA7l&{vyw44u^+8__1*oSi z5z16Mg=*t#f^*9~LFL2y!s0F=;+YILG6+u5AHcSuNoe)^T8OrA5bit7(@Dye0se+b z`kB)g=xwgu)cJeYsEo#*=oS=f^o`0EedgPGyJ&zi2{QLOX6KdXeTg0q=uv@r|{J@>W~(T{1J$PCL-HlP;v`_>g9pux9a9P+Qo? zbSi2dh4#%O&jep8o$N`^KJ%bCl}=>fdpZ|pf#p}cgLPRSb5&bm>v#p`!7a!rXy!NS x7Ivr~!NY9?4b4MHNgGGeVP-Ef)7#8mO7dNf^q3hE;{&dgHQSZaBL2(4{{^IzzH0yg