From 855934c3f3c5da7463790b5975a2b93938907c9c Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:26:29 +1100 Subject: [PATCH 01/73] rm old releases --- code/flexidot_v1.00.py | 3129 ------------------------------------ code/flexidot_v1.01.py | 3153 ------------------------------------- code/flexidot_v1.02.py | 3177 ------------------------------------- code/flexidot_v1.03.py | 3161 ------------------------------------- code/flexidot_v1.04.py | 3325 --------------------------------------- code/flexidot_v1.05.py | 3390 --------------------------------------- code/flexidot_v1.06.py | 3403 ---------------------------------------- 7 files changed, 22738 deletions(-) delete mode 100644 code/flexidot_v1.00.py delete mode 100644 code/flexidot_v1.01.py delete mode 100644 code/flexidot_v1.02.py delete mode 100644 code/flexidot_v1.03.py delete mode 100644 code/flexidot_v1.04.py delete mode 100644 code/flexidot_v1.05.py delete mode 100644 code/flexidot_v1.06.py diff --git a/code/flexidot_v1.00.py b/code/flexidot_v1.00.py deleted file mode 100644 index 299780e..0000000 --- a/code/flexidot_v1.00.py +++ /dev/null @@ -1,3129 +0,0 @@ -#!/usr/bin/python2.7 -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -(2018) - -version 1.0 -= version 0.121 -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, ccv, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - from matplotlib.colors import colorConverter as ccv - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - -load_modules() - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.00 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (in prep.) - "FlexiDot: Highly customizable, ambiguity-aware dotplots for visual sequence analyses" - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - Y or 1 = ON - N or 0 = OFF [default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 7] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -P, --plot_size Plotsize [default = 10] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -T, --title_length Limit title length for self dotplot comparison (only if --plotting_mode=0) - [default = infinite] - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a:", "auto_fas=", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 7 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = float("Inf") - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = check_bools(str(arg), default=auto_fas) - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - text += "\n" + "Title length (limit number of characters): " + str(title_length) - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=float("Inf"), delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = len(cmaplist)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_lcs_len=None, min_lcs_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_lcs_len provided - if max_lcs_len != None: - multi_factor = 100 # one digit - if max_lcs_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_lcs_len-min_lcs_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_lcs_len-min_lcs_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_lcs_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_lcs_len <= 100 and 0 <= min_lcs_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_lcs_len, max_lcs_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - else: - # generate legend labels acc. to standard interval notation - interval_size = 100 // lcs_shading_num - pos = range(interval_size, 101+interval_size, interval_size) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_lcs_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_lcs_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = "%d %s" % (max_lcs_len, unit) - else: - legend_labels_lengths[-1] = "%.2f %s" % (max_lcs_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "Selfdotplot_GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_lcs_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_lcs_len, unit, lcs_shading_num) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % lcs_shading_num + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf")): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=prefix, filetype=filetype, verbose=verbose) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >=50% Ns are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, name_seq, wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, length_scaling=True, scale_delim_col="red"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, %2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, %d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=size_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # determine label orientations - if len(sequences) > 5: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # if string is proviced - else: - color_idx0 = 0 - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # diagonal (self-dotplots) - if idx == jdx: - # skip positions below diagonal - counter = counter + (counter - 1) // (nrows) # + row_pos - counters = [counter] - # draw both graphs at once (due to symmetry) - else: - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter, counter2] - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - # if custom matrix value printed into upper matrix triangle, skip data plotting - - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == 1 or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=8) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_lcs_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_lcs_len=custom_max, min_lcs_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, spacing=0.04, verbose=False): - - global t1, line_col_rev - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - -# testing mode for debugging -trial_mode = False -# trial_mode = True - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - # input_user_matrix_file = "AngioSINE-v18-alignment-identities.csv" - input_fasta = ["test-sequences-9-Ns.fas"] - input_fasta = ["Beta_SINEs__select_consensus.fas"] - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-01.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-comma-str.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-100+.txt" - # user_matrix_print = True - output_file_prefix = "SINEmatrix" - output_file_prefix = "SINEmatrix-NoShading" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, spacing=spacing, verbose=verbose) - - diff --git a/code/flexidot_v1.01.py b/code/flexidot_v1.01.py deleted file mode 100644 index 8e5443f..0000000 --- a/code/flexidot_v1.01.py +++ /dev/null @@ -1,3153 +0,0 @@ -#!/usr/bin/python2.7 -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -(Bioinformatics, 2018) -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, ccv, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - from matplotlib.colors import colorConverter as ccv - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - -load_modules() - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.01 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (in prep.) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 7] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -P, --plot_size Plotsize [default = 10] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -T, --title_length Limit title length for self dotplot comparison (only if --plotting_mode=0) - [default = infinite] - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 7 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = float("Inf") - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - text += "\n" + "Title length (limit number of characters): " + str(title_length) - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=float("Inf"), delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_lcs_len=None, min_lcs_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_lcs_len provided - if max_lcs_len != None: - multi_factor = 100 # one digit - if max_lcs_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_lcs_len-min_lcs_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_lcs_len-min_lcs_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_lcs_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_lcs_len <= 100 and 0 <= min_lcs_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_lcs_len, max_lcs_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - else: - # generate legend labels acc. to standard interval notation - interval_size = 100 // lcs_shading_num - pos = range(interval_size, 101+interval_size, interval_size) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_lcs_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_lcs_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = "%d %s" % (max_lcs_len, unit) - else: - legend_labels_lengths[-1] = "%.2f %s" % (max_lcs_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "Selfdotplot_GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_lcs_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_lcs_len, unit, lcs_shading_num) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % lcs_shading_num + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf")): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=prefix, filetype=filetype, verbose=verbose) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >=50% Ns are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, name_seq, wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, length_scaling=True, scale_delim_col="red", title_length=float("Inf")): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf"), rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, %2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, %d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=prefix, filetype=filetype, verbose=verbose) - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=size_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # if string is proviced - else: - color_idx0 = 0 - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # diagonal (self-dotplots) - if idx == jdx: - # skip positions below diagonal - counter = counter + (counter - 1) // (nrows) # + row_pos - counters = [counter] - # draw both graphs at once (due to symmetry) - else: - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter, counter2] - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == 1 or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=8) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_lcs_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_lcs_len=custom_max, min_lcs_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, spacing=0.04, verbose=False): - - global t1, line_col_rev - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - -# testing mode for debugging -trial_mode = False -# trial_mode = True - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - # input_user_matrix_file = "AngioSINE-v18-alignment-identities.csv" - input_fasta = ["test-sequences-9-Ns.fas"] - input_fasta = ["Beta_SINEs__select_consensus.fas"] - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-01.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-comma-str.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-100+.txt" - # user_matrix_print = True - output_file_prefix = "SINEmatrix" - output_file_prefix = "SINEmatrix-NoShading" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, spacing=spacing, verbose=verbose) - - diff --git a/code/flexidot_v1.02.py b/code/flexidot_v1.02.py deleted file mode 100644 index 21cb5ae..0000000 --- a/code/flexidot_v1.02.py +++ /dev/null @@ -1,3177 +0,0 @@ -#!/usr/bin/python2.7 -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -(Bioinformatics, 2018, doi 10.1093/bioinformatics/bty395) -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, ccv, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - from matplotlib.colors import colorConverter as ccv - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - -load_modules() - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.02 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (2018) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - Bioinformatics, doi: 10.1093/bioinformatics/bty395 - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 7] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -P, --plot_size Plotsize [default = 10] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -T, --title_length Limit title length for self dotplot comparison - Use last characters of the title name (instead of the first): add an "E" (end), e.g. -T 20E - [default = 20, the first 20 characters] - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 7 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = 20 #float("Inf") - title_selectpos = "B" # B (begin), E (end) - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - try: - title_length = int(str(arg)[:-1]) - title_selectpos = arg[-1].upper() # B (beginning), E (end) - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - print "test", title_length, title_selectpos - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - text += "\n" + "Title length (limit number of characters): " + str(title_length) - if title_selectpos == "E": - text += "the last %i characters are printed." %int(title_length) - else: - text += "the first %i characters are printed." %int(title_length) - - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_selectpos, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=20, title_selectpos="B"): #, delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # take last characters - if title_selectpos == "E": - name = seq_name[len(seq_name)-max_len:] - - # takefirst characters - else: - name = seq_name[:max_len] - - - """# keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - """ - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_lcs_len=None, min_lcs_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_lcs_len provided - if max_lcs_len != None: - multi_factor = 100 # one digit - if max_lcs_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_lcs_len-min_lcs_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_lcs_len-min_lcs_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_lcs_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_lcs_len <= 100 and 0 <= min_lcs_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_lcs_len, max_lcs_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - else: - # generate legend labels acc. to standard interval notation - interval_size = 100 // lcs_shading_num - pos = range(interval_size, 101+interval_size, interval_size) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_lcs_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_lcs_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = "%d %s" % (max_lcs_len, unit) - else: - legend_labels_lengths[-1] = "%.2f %s" % (max_lcs_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "Selfdotplot_GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_lcs_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_lcs_len, unit, lcs_shading_num) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % lcs_shading_num + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf"), title_selectpos="B"): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=prefix, filetype=filetype, verbose=verbose) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >=50% Ns are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_selectpos=title_selectpos)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_selectpos=title_selectpos)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, name_seq, wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, length_scaling=True, scale_delim_col="red", title_length=float("Inf"), title_selectpos="B"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length, title_selectpos=title_selectpos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length, title_selectpos=title_selectpos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf"), title_selectpos="B", rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage=49 - text = "Provide valid max_N_percentage, kmers with >50% are ignored\n" - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given: %s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, %2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, %d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=prefix, filetype=filetype, verbose=verbose) - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=size_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # if string is proviced - else: - color_idx0 = 0 - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # diagonal (self-dotplots) - if idx == jdx: - # skip positions below diagonal - counter = counter + (counter - 1) // (nrows) # + row_pos - counters = [counter] - # draw both graphs at once (due to symmetry) - else: - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter, counter2] - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length, title_selectpos=title_selectpos)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == 1 or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length, title_selectpos=title_selectpos)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=8) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_lcs_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_lcs_len=custom_max, min_lcs_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, title_selectpos="B", spacing=0.04, verbose=False): - - global t1, line_col_rev - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_selectpos=title_selectpos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_selectpos=title_selectpos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_selectpos=title_selectpos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_selectpos=title_selectpos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - -# testing mode for debugging -trial_mode = False -# trial_mode = True - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, title_selectpos, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - # input_user_matrix_file = "AngioSINE-v18-alignment-identities.csv" - input_fasta = ["test-sequences-9-Ns.fas"] - input_fasta = ["Beta_SINEs__select_consensus.fas"] - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-01.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-comma-str.txt" - # input_user_matrix_file = "Beta_SINEs__select_consensus_matrix-100+.txt" - # user_matrix_print = True - output_file_prefix = "SINEmatrix" - output_file_prefix = "SINEmatrix-NoShading" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, title_selectpos=title_selectpos, spacing=spacing, verbose=verbose) - - diff --git a/code/flexidot_v1.03.py b/code/flexidot_v1.03.py deleted file mode 100644 index 867ce99..0000000 --- a/code/flexidot_v1.03.py +++ /dev/null @@ -1,3161 +0,0 @@ -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -(Bioinformatics, 2018, doi 10.1093/bioinformatics/bty395) -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, ccv, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - from matplotlib.colors import colorConverter as ccv - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - -load_modules() - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.03 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (2018) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - Bioinformatics, doi: 10.1093/bioinformatics/bty395 - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 7] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -P, --plot_size Plotsize [default = 10] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -T, --title_length Limit title length for dotplot comparisons - [default = 20] - Position of selection can be specified by appending a letter (e.g. -T 20E) - B = beginning [default] - E = end - - - GFF SHADING (for -p/--plotting_mode=0,2 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 7 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = 20 # float("Inf") - title_clip_pos = "B" # B (begin), E (end) - max_N_percentage = 49 # fixed value, no user input - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - try: - title_length = int(str(arg)[:-1]) - if arg[-1].upper() in ["B", "E"]: # B (beginning), E (end) - title_clip_pos = arg[-1].upper() - else: - print "title_length position information invalid - using default value" - log_txt += "\ntitle_length position information invalid - using default value" - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - if title_clip_pos == "E": - text += "\n" + "Title length (limit number of characters): " + "last" + str(title_length) + "characters" - else: - text += "\n" + "Title length (limit number of characters): " + "first" + str(title_length) + "characters" - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=20, title_clip_pos="B"): #, delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # take last characters - if title_clip_pos == "E": - name = seq_name[len(seq_name)-max_len:] - - # take first characters - else: - name = seq_name[:max_len] - - """# keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - """ - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_lcs_len=None, min_lcs_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_lcs_len provided - if max_lcs_len != None: - multi_factor = 100 # one digit - if max_lcs_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_lcs_len-min_lcs_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_lcs_len-min_lcs_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_lcs_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_lcs_len <= 100 and 0 <= min_lcs_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_lcs_len, max_lcs_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - else: - # generate legend labels acc. to standard interval notation - interval_size = 100 // lcs_shading_num - pos = range(interval_size, 101+interval_size, interval_size) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_lcs_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_lcs_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = "%d %s" % (max_lcs_len, unit) - else: - legend_labels_lengths[-1] = "%.2f %s" % (max_lcs_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_lcs_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_lcs_len, unit, lcs_shading_num) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % lcs_shading_num + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf"), title_clip_pos="B"): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Selfdotplot" - else: legend_prefix = "Selfdotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlim(0, length_seq+1) - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos), wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, length_scaling=True, scale_delim_col="red", title_length=float("Inf"), title_clip_pos="B"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, max_N_percentage=49, verbose=False, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, title_length=float("Inf"), title_clip_pos="B", rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Polydotplot" - else: legend_prefix = "Polydotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, %2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, %d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=size_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # if string is proviced - else: - color_idx0 = 0 - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # diagonal (self-dotplots) - if idx == jdx: - # skip positions below diagonal - counter = counter + (counter - 1) // (nrows) # + row_pos - counters = [counter] - # draw both graphs at once (due to symmetry) - else: - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter, counter2] - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - if len_two != len_one: - logprint("Polydot GFF shading for diagonal fields - nequal length error!") - return - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(len_one+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == 1 or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=8) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_lcs_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_lcs_len=custom_max, min_lcs_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, title_clip_pos="B", spacing=0.04, max_N_percentage=49, verbose=False): - - global t1, line_col_rev - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage = 49 - if type_nuc: - ambiq_res = "N" - else: - ambiq_res = "X" - text = "Provide valid max_N_percentage, kmers with >50%% %ss are ignored\n" % (ambiq_res) - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, gff_files=gff, gff_color_dict=gff_feat_colors, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - -# testing mode for debugging -trial_mode = False -# trial_mode = True - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - input_fasta = ["test-sequences-8.fas"] - input_gff_files = ["Seq2_annotations.gff3"] - # input_user_matrix_file = "matrix.txt" - # user_matrix_print = True - output_file_prefix = "#GFF_poly" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, title_clip_pos=title_clip_pos, spacing=spacing, max_N_percentage=max_N_percentage, verbose=verbose) - - diff --git a/code/flexidot_v1.04.py b/code/flexidot_v1.04.py deleted file mode 100644 index f5b3481..0000000 --- a/code/flexidot_v1.04.py +++ /dev/null @@ -1,3325 +0,0 @@ -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -Bioinformatics, 2018, doi 10.1093/bioinformatics/bty395 - -FlexiDot version1.04 (aka lab version 137) - -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, ccv, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - from matplotlib.colors import colorConverter as ccv - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - -load_modules() - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.04 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (2018) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - Bioinformatics, doi: 10.1093/bioinformatics/bty395 - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 7] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -M, --mirror_y_axis Flip y-axis bottom to top (cartesian coordinate system) - Y or 1 = y-axis bottom to top - N or 0 = y-axis top to bottom [default] - - -P, --plot_size Plotsize [default = 10] - - -R, --representation Region of plot to display (only if --plotting_mode=2) - 0 = full [default] - 1 = upper - 2 = lower - - -T, --title_length Limit title length for dotplot comparisons - [default = 20] - Position of selection can be specified by appending a letter (e.g. -T 20E) - B = beginning [default] - E = end - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-M", "--mirror_y_axis", "M:", "mirror_y_axis=", - "-R", "--representation", "R:", "representation=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 7 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = 20 # float("Inf") - title_clip_pos = "B" # B (begin), E (end) - max_N_percentage = 49 # fixed value, no user input - mirror_y_axis = False - representation = 0 - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - representation_dict = {0: "full", 1: "upper", 2: "lower"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-M", "--mirror_y_axis"): - mirror_y_axis = check_bools(str(arg), default=mirror_y_axis) - - elif opt in ("-R", "--representation"): - if 0 <= int(arg) <= 2: - representation = int(arg) - else: - print "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - log_txt += "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - try: - title_length = int(str(arg)[:-1]) - if arg[-1].upper() in ["B", "E"]: # B (beginning), E (end) - title_clip_pos = arg[-1].upper() - else: - print "title_length position information invalid - using default value" - log_txt += "\ntitle_length position information invalid - using default value" - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - text += "\n" + "Displayed plot region: " + representation_dict[representation] - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - if mirror_y_axis: - text += "\n" + "Y-axis mirrored (bottom to top) " + str(mirror_y_axis) - if title_clip_pos == "E": - text += "\n" + "Title length (limit number of characters): " + "last" + str(title_length) + "characters" - else: - text += "\n" + "Title length (limit number of characters): " + "first" + str(title_length) + "characters" - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=20, title_clip_pos="B"): #, delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # take last characters - if title_clip_pos == "E": - name = seq_name[len(seq_name)-max_len:] - - # take first characters - else: - name = seq_name[:max_len] - - """# keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - """ - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_lcs_len=None, min_lcs_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_lcs_len provided - if max_lcs_len != None: - multi_factor = 100 # one digit - if max_lcs_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_lcs_len-min_lcs_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_lcs_len-min_lcs_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_lcs_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_lcs_len <= 100 and 0 <= min_lcs_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_lcs_len, max_lcs_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - else: - # generate legend labels acc. to standard interval notation - interval_size = 100 // lcs_shading_num - pos = range(interval_size, 101+interval_size, interval_size) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_lcs_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_lcs_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = "%d %s" % (max_lcs_len, unit) - else: - legend_labels_lengths[-1] = "%.2f %s" % (max_lcs_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_lcs_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_lcs_len, unit, lcs_shading_num) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % lcs_shading_num + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Selfdotplot" - else: legend_prefix = "Selfdotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - # print P.xticks()[0], P.yticks()[0] - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos), wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, x_label_pos_top=True, length_scaling=True, scale_delim_col="red"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis: - x_label_pos_top = False - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # P.axis('scaled') # make images scaled by size ### optional update ### - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - # xlimit = [0, max_len+1] - if mirror_y_axis: - P.ylim(0, max_len+1) # rotate y axis (point upwards) - else: - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # # use same tick labels for x and y axis - # if P.xlim() == P.ylim(): - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(xlimit) - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, x_label_pos_top=True, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Polydotplot" - else: legend_prefix = "Polydotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, %2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, %d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - if mirror_y_axis: - height_ratios = size_ratios[::-1] - else: - height_ratios = size_ratios[:] - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=height_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis and representation == 1: - x_label_pos_top = True - elif mirror_y_axis or representation == 2: - x_label_pos_top = False - - # print y labels on the right, if upper right triangle is displayed - if (representation == 1 and not mirror_y_axis) or (representation == 2 and mirror_y_axis): - y_label_pos = 0 # last column - else: # left y label - y_label_pos = 1 # first column - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - # check combination of shading parameters for triangular output - if representation != 0 and lcs_shading and custom_shading: # both directions in triangle - logprint("\nAttention: For triangular output custom-shading and LCS shading cannot be combined!\n") - elif representation != 0 and lcs_shading and lcs_shading_ori == 2: # both directions in triangle - logprint("\nAttention: For triangular output LCS shading for both orientations is combined to max of both orientations!\n") - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # no color if string is proviced - else: - color_idx0 = 0 - - # use best LCS of both orientations for coloring triangle with two-ori-LCS - if representation != 0 and lcs_shading_ori == 2: # both directions in triangle - color_idx0, color_idx1 = max(color_idx0, color_idx1), max(color_idx0, color_idx1) - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # calculate figure position in polyplot - # diagonal (self-dotplots) - if idx == jdx: - if mirror_y_axis: - seq_num = sequences.index(name_one)+1 - counter1 = seq_num + len(sequences) * (len(sequences)-seq_num) - counter = counter + (counter - 1) // (nrows) - else: - # skip positions below diagonal - counter1 = counter + (counter - 1) // (nrows) # + row_pos - counter = counter1 - counters = [counter1] - - # draw both graphs at once (due to symmetry) - else: - if mirror_y_axis: - col_pos = sequences.index(name_two)+1 - row_pos = len(sequences) - (sequences.index(name_one)+1) - counter1 = row_pos * ncols + col_pos - counter2 = (ncols - col_pos) * ncols + ncols - row_pos - else: - counter1 = counter - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter1, counter2] # lower, upper - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - if representation == 0 or len(counters) == 1 or (representation == 1 and kdx == 0) or (representation == 2 and kdx == 1): - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - if len_two != len_one: - logprint("Polydot GFF shading for diagonal fields - nequal length error!") - return - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(len_one+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if mirror_y_axis: - x1, y1, x2, y2 = y1, x1, y2, x2 - n1, n2 = n2, n1 - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - # P.xlim(0, l1+1) - if mirror_y_axis: - P.xlim(0, l2+1) - P.ylim(0, l1+1) # rotate y axis (point upwards) - else: - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - ## axis labelling - ################## - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # settings for y labels on right side - if y_label_pos == 0: # right label - ax.yaxis.tick_right() - ax.yaxis.set_label_position("right") - label_dist = 30 - else: - label_dist = 8 - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - elif mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif not mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - P.setp(ax.get_xticklabels(), visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == y_label_pos or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=label_dist) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_right() - elif not mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_lcs_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_lcs_len=custom_max, min_lcs_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, title_clip_pos="B", spacing=0.04, max_N_percentage=49, mirror_y_axis=False, verbose=False): - - global t1, line_col_rev - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage = 49 - if type_nuc: - ambiq_res = "N" - else: - ambiq_res = "X" - text = "Provide valid max_N_percentage, kmers with >50%% %ss are ignored\n" % (ambiq_res) - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - -# testing mode for debugging -trial_mode = False -#trial_mode = True - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - input_fasta = ["test-sequences-8.fas"] - input_gff_files = ["Seq2_annotations.gff3"] - # input_user_matrix_file = "matrix.txt" - # user_matrix_print = True - output_file_prefix = "#Test" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - mirror_y_axis = False - mirror_y_axis = True - - output_file_prefix = "#R-upper" - representation = 0 # both - representation = 1 # upper - representation = 2 # lower - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, title_clip_pos=title_clip_pos, spacing=spacing, max_N_percentage=max_N_percentage, mirror_y_axis=mirror_y_axis, verbose=verbose) - - diff --git a/code/flexidot_v1.05.py b/code/flexidot_v1.05.py deleted file mode 100644 index f326726..0000000 --- a/code/flexidot_v1.05.py +++ /dev/null @@ -1,3390 +0,0 @@ -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot Version 1.05 - -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -Bioinformatics (2018) Vol. 34 (20), 3575–3577, doi 10.1093/bioinformatics/bty395 -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - P.switch_backend('agg') # bugfix for _tkinter.TclError on CentOs 7 servers, see Github Issue #5 - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma' , 'DejaVu Sans', 'Droid Sans Mono', 'Sans', 'Liberation', 'Ubuntu', 'Arial', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.05 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (2018) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - Bioinformatics 34 (20), 3575–3577, doi: 10.1093/bioinformatics/bty395 - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 10] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - -O, --only_vs_first_seq Limit pairwise comparisons to match all sequences to 1st sequence only - (only if --plotting_mode=1) - Y or 1 = ON - N or 0 = OFF [default] - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -M, --mirror_y_axis Flip y-axis bottom to top (cartesian coordinate system) - Y or 1 = y-axis bottom to top - N or 0 = y-axis top to bottom [default] - - -P, --plot_size Plotsize [default = 10] - - -R, --representation Region of plot to display (only if --plotting_mode=2) - 0 = full [default] - 1 = upper - 2 = lower - - -T, --title_length Limit title length for dotplot comparisons - [default = 20] - Position of selection can be specified by appending a letter (e.g. -T 20E) - B = beginning [default] - E = end - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-O", "--only_vs_first_seq", "O:", "only_vs_first_seq=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-M", "--mirror_y_axis", "M:", "mirror_y_axis=", - "-R", "--representation", "R:", "representation=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 10 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - only_vs_first_seq = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = 20 # float("Inf") - title_clip_pos = "B" # B (begin), E (end) - max_N_percentage = 49 # fixed value, no user input - mirror_y_axis = False - representation = 0 - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - representation_dict = {0: "full", 1: "upper", 2: "lower"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-O", "--only_vs_first_seq"): - only_vs_first_seq = check_bools(str(arg), default=only_vs_first_seq) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-M", "--mirror_y_axis"): - mirror_y_axis = check_bools(str(arg), default=mirror_y_axis) - - elif opt in ("-R", "--representation"): - if 0 <= int(arg) <= 2: - representation = int(arg) - else: - print "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - log_txt += "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - try: - title_length = int(str(arg)[:-1]) - if arg[-1].upper() in ["B", "E"]: # B (beginning), E (end) - title_clip_pos = arg[-1].upper() - else: - print "title_length position information invalid - using default value" - log_txt += "\ntitle_length position information invalid - using default value" - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Sustitution count: " + str(substitution_count) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 1 in plotting_modes: - text += "\n" + "Only matching sequences to first entry: " + str(only_vs_first_seq) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - text += "\n" + "Displayed plot region: " + representation_dict[representation] - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - if mirror_y_axis: - text += "\n" + "Y-axis mirrored (bottom to top) " + str(mirror_y_axis) - if title_clip_pos == "E": - text += "\n" + "Title length (limit number of characters): " + "last" + str(title_length) + "characters" - else: - text += "\n" + "Title length (limit number of characters): " + "first" + str(title_length) + "characters" - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=20, title_clip_pos="B"): #, delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # take last characters - if title_clip_pos == "E": - name = seq_name[len(seq_name)-max_len:] - - # take first characters - else: - name = seq_name[:max_len] - - """# keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - """ - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_len=None, min_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_len provided - if max_len != None: - multi_factor = 100 # one digit - if max_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_len-min_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_len-min_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_len <= 100 and 0 <= min_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_len, max_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - # generate legend labels acc. to standard interval notation - else: - # use default max_len = 100 and min_len = 0 - len_interval_size = 100. / lcs_shading_num - pos = [float("%.2f" % (0))] - # calculate interval positions - for idx in range(lcs_shading_num): - pos.append(float("%.2f" % (pos[-1] + len_interval_size))) - - # interval_size = 100 // lcs_shading_num - # pos = range(interval_size, 101+interval_size, interval_size) - - # remove unneccessary zeros in decimal places (i.e. if x.x00 in all entries) - while True: - last_digit_all_zero = True - no_delim = False - for idx in range(len(pos)): - # only process if fraction with decimal places - if not "." in str(pos[idx]): - no_delim = True - break - # only process when all entries end in zero - elif str(pos[idx])[-1] != "0": - last_digit_all_zero = False - break - if not last_digit_all_zero or no_delim: - break - # remove last decimal place (== 0) from all entries - else: - temp_pos = pos[:] - for idx in range(len(pos)): - if not str(pos[idx])[-2] == ".": - pos[idx] = float(str(pos[idx])[:-1]) - else: - pos[idx] = int(str(pos[idx])[:-2]) - logprint("Shortening legend entries: %s - %s" % (temp_pos, pos)) - - # eliminate fractions if unit == bp/aa - if unit in ["aa", "bp"]: - for idx in range(len(pos)): - temp_pos = pos[:] - rounded_unit = False - if "." in str(pos[idx]): - rounded_unit = True - # round values up to next integer (keep integer, if not a fraction) - pos[idx] = int(pos[idx] / 1) + int(pos[idx] % 1 > 0) - if idx == len(pos) - 1 and pos[idx] == 101: - pos[idx] = 100 - if rounded_unit: - logprint("Fractions not permitted for unit '%s': %s -> %s" % (unit, temp_pos, pos)) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - if max_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = u"[%d %s, \u221E]" % (max_len, unit) - else: - legend_labels_lengths[-1] = u"[%.2f %s, \u221E]" % (max_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_len, unit, lcs_shading_num+1) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num+1) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % (lcs_shading_num+1) + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression pattern in target sequence - result_matches = regex.finditer(kmer_string, seq_target, overlapped=True, concurrent=True) - - # investigate all hits - last_motif_start = 0 - for result in result_matches: - # skip hits only differing in length of TSD region 1 - if result.start() > last_motif_start: - counter[counter_pos] += 1 - last_motif_start += result.start() - - kmer2 = seq_target[result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Selfdotplot" - else: legend_prefix = "Selfdotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - # print P.xticks()[0], P.yticks()[0] - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos), wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, x_label_pos_top=True, only_vs_first_seq=False, length_scaling=True, scale_delim_col="red"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if not only_vs_first_seq: - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - else: - text = "\n%s\n\nCreating %d paired dotplot images against 1st sequence '%s':\n%s\n\n=>" % (50*"=", len(sequences)-1, sequences[0], 36*"-") - text += ", ".join(sequences[1:]) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis: - x_label_pos_top = False - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_wordsize%d_lcs_data_file%s.txt" % (prefix, wordsize, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # P.axis('scaled') # make images scaled by size ### optional update ### - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - # xlimit = [0, max_len+1] - if mirror_y_axis: - P.ylim(0, max_len+1) # rotate y axis (point upwards) - else: - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # # use same tick labels for x and y axis - # if P.xlim() == P.ylim(): - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(xlimit) - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - if only_vs_first_seq: - break - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, x_label_pos_top=True, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Polydotplot" - else: legend_prefix = "Polydotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, >%2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, >%d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - if mirror_y_axis: - height_ratios = size_ratios[::-1] - else: - height_ratios = size_ratios[:] - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=height_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis and representation == 1: - x_label_pos_top = True - elif mirror_y_axis or representation == 2: - x_label_pos_top = False - - # print y labels on the right, if upper right triangle is displayed - if (representation == 1 and not mirror_y_axis) or (representation == 2 and mirror_y_axis): - y_label_pos = 0 # last column - else: # left y label - y_label_pos = 1 # first column - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - # check combination of shading parameters for triangular output - if representation != 0 and lcs_shading and custom_shading: # both directions in triangle - logprint("\nAttention: For triangular output custom-shading and LCS shading cannot be combined!\n") - elif representation != 0 and lcs_shading and lcs_shading_ori == 2: # both directions in triangle - logprint("\nAttention: For triangular output LCS shading for both orientations is combined to max of both orientations!\n") - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # no color if string is proviced - else: - color_idx0 = 0 - - # use best LCS of both orientations for coloring triangle with two-ori-LCS - if representation != 0 and lcs_shading_ori == 2: # both directions in triangle - color_idx0, color_idx1 = max(color_idx0, color_idx1), max(color_idx0, color_idx1) - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # calculate figure position in polyplot - # diagonal (self-dotplots) - if idx == jdx: - if mirror_y_axis: - seq_num = sequences.index(name_one)+1 - counter1 = seq_num + len(sequences) * (len(sequences)-seq_num) - counter = counter + (counter - 1) // (nrows) - else: - # skip positions below diagonal - counter1 = counter + (counter - 1) // (nrows) # + row_pos - counter = counter1 - counters = [counter1] - - # draw both graphs at once (due to symmetry) - else: - if mirror_y_axis: - col_pos = sequences.index(name_two)+1 - row_pos = len(sequences) - (sequences.index(name_one)+1) - counter1 = row_pos * ncols + col_pos - counter2 = (ncols - col_pos) * ncols + ncols - row_pos - else: - counter1 = counter - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter1, counter2] # lower, upper - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - if representation == 0 or len(counters) == 1 or (representation == 1 and kdx == 0) or (representation == 2 and kdx == 1): - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - if len_two != len_one: - logprint("Polydot GFF shading for diagonal fields - nequal length error!") - return - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(len_one+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if mirror_y_axis: - x1, y1, x2, y2 = y1, x1, y2, x2 - n1, n2 = n2, n1 - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - # P.xlim(0, l1+1) - if mirror_y_axis: - P.xlim(0, l2+1) - P.ylim(0, l1+1) # rotate y axis (point upwards) - else: - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - ## axis labelling - ################## - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # settings for y labels on right side - if y_label_pos == 0: # right label - ax.yaxis.tick_right() - ax.yaxis.set_label_position("right") - label_dist = 30 - else: - label_dist = 8 - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - elif mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif not mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - P.setp(ax.get_xticklabels(), visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == y_label_pos or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=label_dist) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_right() - elif not mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_len=custom_max, min_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, only_vs_first_seq=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, title_clip_pos="B", spacing=0.04, max_N_percentage=49, mirror_y_axis=False, verbose=False): - - global t1, line_col_rev - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage = 49 - if type_nuc: - ambiq_res = "N" - else: - ambiq_res = "X" - text = "Provide valid max_N_percentage, kmers with >50%% %ss are ignored\n" % (ambiq_res) - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - - -load_modules() - -# testing mode for debugging -trial_mode = True -trial_mode = False - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - input_fasta = ["test-sequences-8.fas"] - input_gff_files = ["Seq2_annotations.gff3"] - # input_user_matrix_file = "matrix.txt" - # user_matrix_print = True - output_file_prefix = "#Test" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - mirror_y_axis = False - mirror_y_axis = True - - output_file_prefix = "#R-upper" - representation = 0 # both - representation = 1 # upper - representation = 2 # lower - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - - - - - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, title_clip_pos=title_clip_pos, spacing=spacing, max_N_percentage=max_N_percentage, mirror_y_axis=mirror_y_axis, verbose=verbose) - - diff --git a/code/flexidot_v1.06.py b/code/flexidot_v1.06.py deleted file mode 100644 index cb223c0..0000000 --- a/code/flexidot_v1.06.py +++ /dev/null @@ -1,3403 +0,0 @@ -#!/usr/bin/python2.7 -# -*- coding: utf-8 -*- - -""" -FlexiDot Version 1.06 - -FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation - -Kathrin M. Seibt, Thomas Schmidt and Tony Heitkam -Institute of Botany, TU Dresden, Dresden, 01277, Germany - -Bioinformatics (2018) Vol. 34 (20), 3575–3577, doi 10.1093/bioinformatics/bty395 -""" - - -############################### -# Requirements # -############################### - -# import system modules -import os, glob -import time, datetime -import sys -import shutil, getopt -import unicodedata - -def module_install_command(module_name, upgrade=False): - """ - create installation commands for Python modules and print information - """ - if upgrade: - load_command = "python -m pip install --upgrade %s" % module_name - else: - load_command = "python -m pip install %s" % module_name - - try: - logprint("Installing Python module: %s\n\t%s\n" % (module_name, load_command)) - except: - print "Installing Python module: %s\n\t%s\n" % (module_name, load_command) - - return load_command - -def load_modules(): - """ - load Python modules, if possible - otherwise try to install them - """ - # make module names global - global cllct, gridspec, patches, rcParams, mplrc, P, Color, SeqIO, np, mcolors, rgb2hex, regex - - # matplotlib - try: - import matplotlib.collections as cllct - except: - command = module_install_command("matplotlib", upgrade=True) - try: - os.system(command) - print "\n" - import matplotlib.collections as cllct - except: - print "Please install module matplotlib manually" - import matplotlib.colors as mcolors - import matplotlib.gridspec as gridspec - import matplotlib.patches as patches - import pylab as P - P.switch_backend('agg') # bugfix for _tkinter.TclError on CentOs 7 servers, see Github Issue #5 - - # specify matplotlib font settings - from matplotlib import rc as mplrc - mplrc('pdf', fonttype=42, compression=0) - from matplotlib import rcParams - rcParams['font.family'] = 'sans-serif' - rcParams['font.sans-serif'] = ['Helvetica', 'Verdana', 'Tahoma' , 'DejaVu Sans', 'Droid Sans Mono', 'Sans', 'Liberation', 'Ubuntu', 'Arial', ] - - # colour for color gradient palette - try: - from colour import Color - except: - command = module_install_command("colour") - try: - os.system(command) - print "\n" - from colour import Color - except: - print "Please install module colour manually" - - # color converter - try: - from colormap import rgb2hex - except: - command = module_install_command("colormap") - # additional module easydev.tools required by colormap - command2 = module_install_command("easydev") - try: - os.system(command) - os.system(command2) - print "\n" - from colormap import rgb2hex - except: - print "Please install module colormap manually" - - # biopython - try: - from Bio import SeqIO - except: - command = module_install_command("biopython") - try: - os.system(command) - print "\n" - from Bio import SeqIO - except: - print "Please install module biopython manually" - - # numpy - try: - import numpy as np - except: - command = module_install_command("numpy") - try: - os.system(command) - print "\n" - import numpy as np - except: - print "Please install module numpy manually" - - # regex for pattern matching - try: - import regex - except: - command = module_install_command("regex") - try: - os.system(command) - print "\n" - import regex - except: - print "Please install module regex manually" - - - -############################### -# Usage & Input # -############################### - -def usage(): - """ - usage and help - """ - - print """\n\n FLEXIDOT - ------------------------------------------------------------------- - - Version: - 1.06 - - Citation: - Kathrin M. Seibt, Thomas Schmidt, Tony Heitkam (2018) - "FlexiDot: Highly customizable ambiguity-aware dotplots for visual sequence investigation" - Bioinformatics 34 (20), 3575–3577, doi: 10.1093/bioinformatics/bty395 - - - General usage: - $ python flexidot.py -a [ARGUMENTS] - $ python flexidot.py -i [ARGUMENTS] - - - ARGUMENTS - ------------------------------------------------------------------- - - - INPUT/OUTPUT OPTIONS... required are [-a] OR [-i] - - -a, --auto_fas Imports all fasta files from current directory (*.fasta, *.fas, *.fa, *.fna) - -i is not needed, if -a is activated - [inactive by default] - - -i, --in_file Input fasta file (fasta file name or comma-separated file list) - > Provide multiple files: Recall -i or provide comma-separated file names - - -o, --output_file_prefix File prefix to be added to the generated filenames [default = NONE] - - -c, --collage_output Multiple dotplots are combined in a collage - Y or 1 = ON [default] - N or 0 = OFF - - -m, --m_col Number of columns per page [default = 4] (only if --collage_output is ON) - - -n, --n_row Number of rows per page [default = 5] (only if --collage_output is ON) - - -f, --filetype Output file format - 0 = PNG [default] - 1 = PDF - 2 = SVG - - -s, --alphabetic_sorting Sort sequences alphabetically according to titles - Y or 1 = ON - N or 0 = OFF [default] - - - CALCULATION PARAMETERS... - - -k, --wordsize Wordsize (kmer length) for dotplot comparison [default = 10] - - -p, --plotting_mode Mode of FlexiDot dotplotting - 0 = self [default] - 1 = paired - 2 = poly (matrix with all-against-all dotplots) - > Run multiple plotting modes: Recall -p or provide comma-separated numbers - - -t, --type_nuc Type of residue is nucleotide - Y or 1 = nucleotide [default] - N or 0 = amino acid - - -w, --wobble_conversion Ambiguity handling for relaxed matching - Y or 1 = ON - N or 0 = OFF [default] - - -S, --substitution_count Number of substitutions (mismatches) allowed per window for relaxed matching - [default = 0] - - -r, --rc_option Find reverse complementary matches (only if type_nuc=y) - Y or 1 = ON [default] - N or 0 = OFF - - -O, --only_vs_first_seq Limit pairwise comparisons to match all sequences to 1st sequence only - (only if --plotting_mode=1) - Y or 1 = ON - N or 0 = OFF [default] - - GRAPHIC FORMATTING... - - -A, --line_width Line width [default = 1] - - -B, --line_col_for Line color [default = black] - - -C, --line_col_rev Reverse line color [default = green] - - -D, --x_label_pos Position of the X-label - Y or 1 = top [default] - N or 0 = bottom - - -E, --label_size Font size [default = 10] - - -F, --spacing Spacing between all-against-all dotplots (only if --plotting_mode=2) - [default = 0.04] - - -L, --length_scaling Scale plot size for pairwise comparison (only if --plotting_mode=1) - Y or 1 = Scaling ON (axes scaled according to sequence length) - N or 0 = Scaling OFF (squared plots) [default] - - -M, --mirror_y_axis Flip y-axis bottom to top (cartesian coordinate system) - Y or 1 = y-axis bottom to top - N or 0 = y-axis top to bottom [default] - - -P, --plot_size Plotsize [default = 10] - - -R, --representation Region of plot to display (only if --plotting_mode=2) - 0 = full [default] - 1 = upper - 2 = lower - - -T, --title_length Limit title length for dotplot comparisons - [default = 20] - Position of selection can be specified by appending a letter (e.g. -T 20E) - B = beginning [default] - E = end - - - GFF SHADING (for -p/--plotting_mode=0 only)... - - -g, --input_gff_files GFF3 file used for markup in self-dotplots - (provide multiple files: Recall -g or provide comma-separated file names) - - -G, --gff_color_config_file Tab-delimited config file for custom gff shading - column 1: feature type - column 2: color - column 3: alpha - column 4: zoom factor (for small regions) - - - LCS SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -x, --lcs_shading Shade subdotplot based on the length of the longest common substring (LCS) - Y or 1 = ON - N or 0 = OFF [default] - - -X, --lcs_shading_num Number of shading intervals (hues) for LCS (-x) and user matrix shading (-u) - [default = 5] - - -y, --lcs_shading_ref Reference for LCS shading - 0 = maximal LCS length [default] - 1 = maximally possible length (length of shorter sequence in pairwise comparison) - 2 = given interval sizes - DNA [default 100 bp] or proteins [default 10 aa] - see -Y - - -Y, --lcs_shading_interval_len Length of intervals for LCS shading (only if --lcs_shading_ref=2) - [default for nucleotides = 50; default for amino acids = 10] - - -z, --lcs_shading_ori Shade subdotplots according to LCS on - 0 = forward [default], - 1 = reverse, or - 2 = both strands (forward shading above diagonal, reverse shading on diagonal and below; - if using --input_user_matrix_file, best LCS is used below diagonal) - - - CUSTOM USER MATRIX SHADING OPTIONS (for -p/--plotting_mode=2 only)... - - -u, --input_user_matrix_file Shading above diagonal according to values in matrix file specified by the user - (tab-delimited or comma-separated matrix with sequence name in column 1 and numbers in columns 2-n - e.g. identity matrix from multiple sequence alignment - strings are ignored) - - -U, --user_matrix_print Display provided matrix entries in the fields above diagonal of all-against-all dotplot - Y or 1 = ON - N or 0 = OFF [default] - - - OTHERS... - - -h, --help Help screen - - -v, --verbose Verbose - - - - - """ - -def check_input(argv, trial_mode=False): - """ - commandline argument parsing - """ - - global log_txt, aa_bp_unit - - # helpers for argument parsing - ###################################### - - arguments = ["-a", "--auto_fas", "a", "auto_fas", - "-i", "--input_fasta", "i:", "input_fasta=", - "-o", "--output_file_prefix", "o:", "output_file_prefix=", - "-c", "--collage_output", "c:", "collage_output=", - "-m", "--m_col", "m:", "m_col=", - "-n", "--n_row", "n:", "n_row=", - "-f", "--filetype", "f:", "filetype=", - "-t", "--type_nuc", "t:", "type_nuc=", - "-g", "--input_gff_files", "g:", "input_gff_files", - "-G", "--gff_color_config_file", "G:", "gff_color_config_file", - "-k", "--wordsize", "k:", "wordsize=", - "-p", "--plotting_mode", "p:", "plotting_mode=", - "-w", "--wobble_conversion", "w:", "wobble_conversion=", - "-S", "--substitution_count", "S:", "substitution_count=", - "-r", "--rc_option", "r:", "rc_option=", - "-O", "--only_vs_first_seq", "O:", "only_vs_first_seq=", - "-s", "--alphabetic_sorting", "s:", "alphabetic_sorting=", - "-x", "--lcs_shading", "x:", "lcs_shading=", - "-X", "--lcs_shading_num", "X:", "lcs_shading_num=", - "-y", "--lcs_shading_ref", "y:", "lcs_shading_ref=", - "-Y", "--lcs_shading_interval_len", "Y:", "lcs_shading_interval_len=", - "-z", "--lcs_shading_ori", "z:", "lcs_shading_ori=", - "-u", "--input_user_matrix_file", "u:", "input_user_matrix_file=", - "-U", "--user_matrix_print", "U:", "user_matrix_print=", - "-P", "--plot_size", "P:", "plot_size=", - "-A", "--line_width", "A:", "line_width=", - "-B", "--line_col_for", "B:", "line_col_for=", - "-C", "--line_col_rev", "C:", "line_col_rev=", - "-D", "--x_label_pos", "D:", "x_label_pos=", - "-E", "--label_size", "E:", "label_size=", - "-F", "--spacing", "F:", "spacing=", - "-L", "--length_scaling", "L:", "length_scaling=", - "-M", "--mirror_y_axis", "M:", "mirror_y_axis=", - "-R", "--representation", "R:", "representation=", - "-T", "--title_length", "T:", "title_length=", - "-h", "--help", "h", "help", - "-v", "--verbose", "v", "verbose"] - - arguments_sysargv = tuple(arguments[0::4] + arguments[1::4]) - arguments_opts = "".join(arguments[2::4]) - arguments_args = arguments[3::4] - - - # setting defaults - ###################################### - - auto_fas = False # 0 - input_fasta = [] - output_file_prefix = None - collage_output = True # 1 - m_col = 4 - n_row = 5 - filetype = 0 - type_nuc = True - input_gff_files = [] - gff_color_config_file = "" - - wordsize = 10 - plotting_modes = [0] - wobble_conversion = False # 0 - substitution_count = 0 - rc_option = True # 1 - alphabetic_sorting = False # 0 - only_vs_first_seq = False # 0 - - lcs_shading = False # 0 - lcs_shading_num = 4 - lcs_shading_ref = 0 - lcs_shading_interval_len = 50 # interval default changes to "10" for amino acids [type_nuc = n] - lcs_shading_ori = 0 - - input_user_matrix_file = "" - user_matrix_print = False - - plot_size = 10 - line_width = 1 - line_col_for = "black" - line_col_rev = "#009243" - x_label_pos = True # 0 - label_size = 10 - spacing = 0.04 - length_scaling = False # 0 - title_length = 20 # float("Inf") - title_clip_pos = "B" # B (begin), E (end) - max_N_percentage = 49 # fixed value, no user input - mirror_y_axis = False - representation = 0 - - aa_bp_unit = "bp" - - verbose = False # 0 - - filetype_dict = {0: "png", 1: "pdf", 2: "svg"} - lcs_shading_ref_dict = {0: "maximal LCS length", 1: "maximally possible length", 2: "given interval sizes"} - plotting_mode_dict = {0: "self", 1: "paired", 2: "all-against-all"} - lcs_shading_ori_dict = {0: "forward", 1: "reverse complement", 2: "both"} - representation_dict = {0: "full", 1: "upper", 2: "lower"} - - # return default parameters for testing purposes - if trial_mode: - print "ATTENTION: YOU ARE IN THE TRIAL MODE!!!\n\n" - - commandline = "trial_mode\n" - - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - return parameters - - - # read arguments - ###################################### - - commandline = "" - for arg in sys.argv: - commandline += arg + " " - - log_txt = "\n...reading input arguments..." - print log_txt - - if len(sys.argv) < 2: - print "\nERROR: More arguments are needed. Exit..." - log_txt += "\nERROR: More arguments are needed. Exit..." - usage() - sys.exit() - - elif sys.argv[1] not in arguments_sysargv: - print "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - log_txt += "\nINPUT ERROR: Input argument %s unknown. Please check the help screen." % sys.argv[1] - # usage() - sys.exit() - - try: - opts, args = getopt.getopt(sys.argv[1:], arguments_opts, arguments_args) - - except getopt.GetoptError: - print "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - log_txt += "\nINPUT ERROR (getopt): Input argument %s unknown. Please check the help screen." % sys.argv[1:] - # usage() - sys.exit() - - for opt, arg in opts: - - if opt in ("-h", "--help"): - print "...fetch help screen" - log_txt += "\n...fetch help screen" - usage(), sys.exit() - - if opt in ("-v", "--verbose"): - print "...verbose output" - log_txt += "\n...verbose output" - verbose = True - - elif opt in ("-i", "--input_fasta"): - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: fasta_file '%s' was not found!" % str(temp_file) - sys.exit(message) - else: - input_fasta.append(str(temp_file)) - print "fasta file #%i: %s" % (len(input_fasta), str(temp_file)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: fasta_file '%s' was not found!" % str(arg) - log_txt += message - sys.exit(message) - else: - input_fasta.append(str(arg)) - print "fasta file #%i: %s" % (len(input_fasta), str(arg)) - log_txt += "\nfasta file #%i: %s" % (len(input_fasta), str(arg)) - - - elif opt in ("-a", "--auto_fas"): - auto_fas = True - - - # multiple gff files: reads them into a list - elif opt in ("-g", "--input_gff_files"): - - # append gff file only if existing - if "," in arg: - arg_list = arg.split(",") - for temp_file in arg_list: - if not os.path.exists(str(temp_file)): - message = "\nERROR: gff_file '%s' was not found!" % str(temp_file) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - print "GFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(temp_file)) - input_gff_files.append(str(temp_file)) - else: - if not os.path.exists(str(arg)): - message = "\nERROR: gff_file '%s' was not found!" % str(arg) - print message - log_txt += message - print " -->Running FlexiDot without this gff file!" - log_txt += "\n -->Running FlexiDot without this gff file!" - else: - input_gff_files.append(str(arg)) - print "GFF file #%i: %s" %(len(input_gff_files), str(arg)) - log_txt += "\nGFF file #%i: %s" %(len(input_gff_files), str(arg)) - - - elif opt in ("-G", "--gff_color_config_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: gff_color_config_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot with default gff coloring specification!" - log_txt += message + "\n -->Running FlexiDot with default gff coloring specification!" - else: - gff_color_config_file = str(arg) - - - elif opt in ("-u", "--input_user_matrix_file"): - if not os.path.exists(str(arg)): - message = "\nERROR: input_user_matrix_file '%s' was not found!" % str(arg) - print message + "\n -->Running FlexiDot without input_user_matrix_file %s!" % arg - log_txt += message + "\n -->Running FlexiDot withdefault matrix shading file!" - else: - input_user_matrix_file = str(arg) - - elif opt in ("-U", "--user_matrix_print"): - user_matrix_print = check_bools(str(arg), default=user_matrix_print) - - elif opt in ("-o", "--output_file_prefix"): - output_file_prefix = arg - - elif opt in ("-c", "--collage_output"): - collage_output = check_bools(str(arg), default=collage_output) - - elif opt in ("-m", "--m_col"): - try: m_col = int(arg) - except: - print "m_col - invalid argument - using default value" - log_txt += "\nm_col - invalid argument - using default value" - - elif opt in ("-n", "--n_row"): - try: n_row = int(arg) - except: - print "n_row - invalid argument - using default value" - log_txt += "\nn_row - invalid argument - using default value" - - elif opt in ("-f", "--filetype"): - if 0 <= int(arg) <= 2: - filetype = int(arg) - else: - print "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - log_txt += "\nERROR: Please provide valid filetype argument. %s is out of range. It will be set to -f 0 [default]." %(filetype) - - elif opt in ("-t", "--type_nuc"): - type_nuc = check_bools(str(arg), default=type_nuc) - - if type_nuc == False: - # interval default changed for amino acids - lcs_shading_interval_len = 10 - aa_bp_unit = "aa" - - elif opt in ("-k", "--wordsize"): - try: wordsize = int(arg) - except: - print "wordsize - invalid argument - using default value" - log_txt += "\nwordsize - invalid argument - using default value" - - elif opt in ("-p", "--plotting_mode"): - if "," in arg: - temp_modes = arg.split(",") - for item in temp_modes: - if item in ["0","1","2"]: - plotting_modes.append(int(item)) - elif arg in ["0","1","2"]: - plotting_modes = [int(arg)] - else: - print "Please provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - log_txt += "\nPlease provide valid plotting_modes argument - e.g. 1 or 0,1,2 - using default [0]" - - elif opt in ("-w", "--wobble_conversion"): - wobble_conversion = check_bools(str(arg), default=wobble_conversion) - - elif opt in ("-S", "--substitution_count"): - try: substitution_count = int(arg) - except: - print "substitution_count - invalid argument - using default value" - log_txt += "\nsubstitution_count - invalid argument - using default value" - - elif opt in ("-r", "--rc_option"): - rc_option = check_bools(str(arg), default=rc_option) - - elif opt in ("-s", "--alphabetic_sorting"): - alphabetic_sorting = check_bools(str(arg), default=alphabetic_sorting) - - elif opt in ("-O", "--only_vs_first_seq"): - only_vs_first_seq = check_bools(str(arg), default=only_vs_first_seq) - - elif opt in ("-x", "--lcs_shading"): - lcs_shading = check_bools(str(arg), default=lcs_shading) - - elif opt in ("-X", "--lcs_shading_num"): - try: lcs_shading_num = int(arg) - 1 - except: - print "lcs_shading_num - invalid argument - using default value" - log_txt += "\nlcs_shading_num - invalid argument - using default value" - - elif opt in ("-y", "--lcs_shading_ref"): - try: - if 0 <= int(arg) <= 2: - lcs_shading_ref = int(arg) - else: - print "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - log_txt += "\nERROR: lcs_shading_ref %s out of range. It will be set to -y 0 [default]." %(lcs_shading_ref) - except: - print "lcs_shading_ref - invalid argument - using default value" - log_txt += "\nlcs_shading_ref - invalid argument - using default value" - - elif opt in ("-Y", "--lcs_shading_interval_len"): - try: lcs_shading_interval_len = int(arg) - except: - print "lcs_shading_interval_len - invalid argument - using default value" - log_txt += "\nlcs_shading_interval_len - invalid argument - using default value" - - elif opt in ("-z", "--lcs_shading_ori"): - if 0 <= int(arg) <= 2: - lcs_shading_ori = int(arg) - else: - print "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - log_txt += "\nERROR: Please provide valid lcs_shading_ori argument. %s is out of range. It will be set to -z 0 [default]." %(lcs_shading_ori) - - elif opt in ("-P", "--plot_size"): - try: plot_size = float(arg) - except: - print "plot_size - invalid argument - using default value" - log_txt += "\nplot_size - invalid argument - using default value" - - - elif opt in ("-A", "--line_width"): - try: line_width = float(arg) - except: - print "line_width - invalid argument - using default value" - log_txt += "\nline_width - invalid argument - using default value" - - elif opt in ("-B", "--line_col_for"): - if mcolors.is_color_like(arg): - line_col_for = arg - else: - print "line_col_for - invalid argument - using default value" - log_txt += "\nline_col_for - invalid argument - using default value" - - elif opt in ("-C", "--line_col_rev"): - if mcolors.is_color_like(arg): - line_col_rev = arg - else: - print "line_col_rev - invalid argument - using default value" - log_txt += "\nline_col_rev - invalid argument - using default value" - - elif opt in ("-D", "--x_label_pos"): - x_label_pos = check_bools(str(arg), default=x_label_pos) - - elif opt in ("-E", "--label_size"): - try: label_size = float(arg) - except: - print "label_size - invalid argument - using default value" - log_txt += "\nlabel_size - invalid argument - using default value" - - elif opt in ("-F", "--spacing"): - try: spacing = float(arg) - except: - print "spacing - invalid argument - using default value" - log_txt += "\nspacing - invalid argument - using default value" - - elif opt in ("-L", "--length_scaling"): - length_scaling = check_bools(str(arg), default=length_scaling) - - elif opt in ("-M", "--mirror_y_axis"): - mirror_y_axis = check_bools(str(arg), default=mirror_y_axis) - - elif opt in ("-R", "--representation"): - if 0 <= int(arg) <= 2: - representation = int(arg) - else: - print "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - log_txt += "\nERROR: Please provide valid representation argument. %s is out of range. It will be set to -R 0 [default]." %(representation) - - elif opt in ("-T", "--title_length"): - try: title_length = int(arg) - except: - try: - title_length = int(str(arg)[:-1]) - if arg[-1].upper() in ["B", "E"]: # B (beginning), E (end) - title_clip_pos = arg[-1].upper() - else: - print "title_length position information invalid - using default value" - log_txt += "\ntitle_length position information invalid - using default value" - except: - print "title_length - invalid argument - using default value" - log_txt += "\ntitle_length - invalid argument - using default value" - - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - logprint(log_txt, start=False, printing=False) - - - # print chosen arguments - ###################################### - - text = "\n%s\n" % (70 * "-") - text += "\n" + "INPUT/OUTPUT OPTIONS...\n" - text += "\n" + "Input fasta file: " + ", ".join(input_fasta) - text += "\n" + "Automatic fasta collection from current directory: " + str(auto_fas) - text += "\n" + "Collage output: " + str(collage_output) - text += "\n" + "Number of columns per page: " + str(m_col) - text += "\n" + "Number of rows per page: " + str(n_row) - text += "\n" + "File format: " + filetype_dict[filetype] - text += "\n" + "Residue type is nucleotide: " + str(type_nuc) - - text += "\n" + "\n\nCALCULATION PARAMETERS...\n" - text += "\n" + "Wordsize: " + str(wordsize) - text += "\n" + "Sustitution count: " + str(substitution_count) - text += "\n" + "Plotting mode: " + str(plotting_modes).replace("[", "").replace("]", "") + "\n" + 51 * " " - for item in plotting_modes: - text += plotting_mode_dict[item] + " " - text += "\n" + "Ambiguity handling: " + str(wobble_conversion) - text += "\n" + "Reverse complement scanning: " + str(rc_option) - text += "\n" + "Alphabetic sorting: " + str(alphabetic_sorting) - - if 1 in plotting_modes: - text += "\n" + "Only matching sequences to first entry: " + str(only_vs_first_seq) - - if 0 in plotting_modes and input_gff_files != []: - text += "\n" + "Input gff files: " + ", ".join(input_gff_files) - if gff_color_config_file != "": - text += "\n" + "GFF color config file: " + gff_color_config_file - text += "\n" + "Prefix for output files: " + str(output_file_prefix) - - if 2 in plotting_modes: - text += "\n" + "\n\nLCS SHADING OPTIONS (plotting_mode 'all-against-all' only)...\n" - text += "\n" + "LCS shading: " + str(lcs_shading) - text += "\n" + "LCS shading interval number: " + str(lcs_shading_num + 1) - text += "\n" + "LCS shading reference: " + lcs_shading_ref_dict[lcs_shading_ref] - if lcs_shading_ref == 2: - text += "\n" + "LCS shading interval size [%s]: " % (aa_bp_unit) + str(lcs_shading_interval_len) - text += "\n" + "LCS shading orientation: " + lcs_shading_ori_dict[lcs_shading_ori] - if input_user_matrix_file != "": - text += "\n" + "Custom user shading matrix file: " + input_user_matrix_file - text += "\n" + "Print user matrix values (instead of dotplot): " + str(user_matrix_print) - text += "\n" + "Displayed plot region: " + representation_dict[representation] - - text += "\n" + "\n\nGRAPHIC FORMATTING...\n" - text += "\n" + "Plot size: " + str(plot_size) - text += "\n" + "Line width: " + str(line_width) - text += "\n" + "Line color: " + line_col_for - text += "\n" + "Reverse line color: " + line_col_rev - text += "\n" + "X label position: " + str(x_label_pos) - text += "\n" + "Label size: " + str(label_size) - text += "\n" + "Spacing: " + str(spacing) - if mirror_y_axis: - text += "\n" + "Y-axis mirrored (bottom to top) " + str(mirror_y_axis) - if title_clip_pos == "E": - text += "\n" + "Title length (limit number of characters): " + "last" + str(title_length) + "characters" - else: - text += "\n" + "Title length (limit number of characters): " + "first" + str(title_length) + "characters" - text += "\n" + "Length scaling: " + str(length_scaling) - text += "\n%s\n" % (70 * "-") - logprint(text) - - - # collect settings - parameters = [commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype_dict[filetype], type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose] - - return parameters - - -############################### -# Helper Functions # -############################### - -def alphabets(type_nuc=True): - """ - provide ambiguity code for sequences - """ - - nucleotide_alphabet = ["A", "C", "G", "T"] - - nucleotide_alphabet_full = ["A", "C", "G", "T", "N", "B", "D", "H", - "V", "Y", "R", "W", "S", "K", "M"] - - nucleotide_ambiguity_code = {"N": ["A", "C", "G", "T"], # any - "B": ["C", "G", "T"], # not A - "D": ["A", "G", "T"], # not C - "H": ["A", "C", "T"], # not G - "V": ["A", "C", "G"], # not T - "Y": ["C", "T"], # pyrimidine - "R": ["A", "G"], # purine - "W": ["A", "T"], # weak - "S": ["C", "G"], # strong - "K": ["G", "T"], # keto - "M": ["A", "C"]} # amino - - nucleotide_match_dict = {"N": "[ACGTNBDHVYRWSKM]", # any - "B": "[CGTNBDHVYRWSKM]", # not A - "D": "[AGTNBDHVYRWSKM]", # not C - "H": "[ACTNBDHVYRWSKM]", # not G - "V": "[ACGNBDHVYRWSKM]", # not T - "K": "[GTNBDHVYRWSK]", # keto - not A,C,M - "M": "[ACNBDHVYRWSM]", # amino - not G,T,K - "W": "[ATNBDHVYRWKM]", # weak - not C,G,S - "S": "[CGNBDHVYRSKM]", # strong - not A,G,W - "Y": "[CTNBDHVYWSKM]", # pyrimidine - not A,G,R - "R": "[AGNBDHVRWSKM]", # purine - not C,T,Y - "A": "[ANDHVRWM]", - "C": "[CNBHVYSM]", - "G": "[GNBDVRSK]", - "T": "[TNBDHYWK]"} - - # nucleotide_match_dict = {"N": ".", # any - # "B": "[^A]", # not A - # "D": "[^C]", # not C - # "H": "[^G]", # not G - # "V": "[^T]", # not T - # "K": "[^ACM]", # keto - not A,C,M - # "M": "[^GTK]", # amino - not G,T,K - # "W": "[^CGS]", # weak - not C,G,S - # "S": "[^AGW]", # strong - not A,G,W - # "Y": "[^AGR]", # pyrimidine - not A,G,R - # "R": "[^CTY]", # purine - not C,T,Y - # "A": "[ANDHVRWM]", - # "C": "[CNBHVYSM]", - # "G": "[GNBDVRSK]", - # "T": "[TNBDHYWK]"} - - aminoacid_alphabet = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"] - - aminoacid_alphabet_full = ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*", "J", - "Z", "B", "X"] - - aminoacid_ambiguity_code = {"J": ["I", "L"], - "Z": ["Q", "E"], - "B": ["N", "D"], - "X": ["A", "R", "N", "D", "C", "E", "Q", "G", - "H", "I", "L", "K", "M", "F", "P", "S", - "T", "W", "Y", "V", "U", "O", "*"]} # any - - aminoacid_match_dict = {"J": "[ILJ]", - "Z": "[QEZ]", - "B": "[NDB]", - # "X": ".", - "X": "[ARNDCEQGHILKMFPSTWYVUO*XBZJ]", - "A": "[AX]", - "R": "[RX]", - "N": "[NXB]", - "D": "[DXB]", - "C": "[CX]", - "E": "[EXZ]", - "Q": "[QXZ]", - "G": "[GX]", - "H": "[HX]", - "I": "[IXJ]", - "L": "[LXJ]", - "K": "[KX]", - "M": "[MX]", - "F": "[FX]", - "P": "[PX]", - "S": "[SX]", - "T": "[TX]", - "W": "[WX]", - "Y": "[YX]", - "V": "[VX]", - "U": "[UX]", - "O": "[OX]", - "*": "[*X]"} - - aa_only = set(['E', 'F', 'I', 'J', 'L', 'O', 'Q', 'P', 'U', 'X', 'Z', '*']) - # return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aa_only - - if type_nuc: - return nucleotide_alphabet, nucleotide_alphabet_full, nucleotide_ambiguity_code, nucleotide_match_dict - else: - return aminoacid_alphabet, aminoacid_alphabet_full, aminoacid_ambiguity_code, aminoacid_match_dict - -def logprint(text, start=False, printing=True, prefix=""): - """ - log output to log_file and optionally print - """ - - # define log file name and open file - global log_file_name - if start and trial_mode: - log_file_name = "log_file.txt" - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - elif start: - date = datetime.date.today() - time = str(datetime.datetime.now()).split(" ")[1].split(".")[0].replace(":", "-") - log_file_name = "%s_%s_log_file.txt" % (date, time) - if prefix != "" and prefix != None: - if not prefix.endswith("-"): - prefix = prefix + "-" - log_file_name = prefix + log_file_name - log_file = open(log_file_name, 'w') - log_file.write("Date: %s\n\n" % str(datetime.datetime.now())) - else: - log_file = open(log_file_name, 'a') - - # write log (and print) - log_file.write(text + "\n") - if printing: - print text - log_file.close() - -def time_track(starting_time, show=True): - """ - calculate time passed since last time measurement - """ - now = time.time() - delta = now - starting_time - if show: - text = "\n\t %s seconds\n" % str(delta) - logprint(text, start=False, printing=True) - return now - -def calc_fig_ratio(ncols, nrows, plot_size, verbose=False): - """ - calculate size ratio for given number of columns (ncols) and rows (nrows) - with plot_size as maximum width and length - """ - ratio = ncols*1./nrows - if verbose: - text = " ".join([ncols, nrows, ratio]) - logprint(text, start=False, printing=True) - if ncols >= nrows: - figsize_x = plot_size - figsize_y = plot_size / ratio - else: - figsize_x = plot_size * ratio - figsize_y = plot_size - return figsize_x, figsize_y - -def shorten_name(seq_name, max_len=20, title_clip_pos="B"): #, delim="_"): - """ - shorten sequence names (for diagram titles) - """ - - if len(seq_name) <= max_len: - return seq_name - - # take last characters - if title_clip_pos == "E": - name = seq_name[len(seq_name)-max_len:] - - # take first characters - else: - name = seq_name[:max_len] - - """# keep first and last part if multiple parts separated by delimiter (e.g. species_prefix + sequence_id) - if delim in seq_name: - if seq_name.count(delim) >= 2: - name = "%s..." % delim.join(seq_name.split(delim)[:1]) + seq_name.split(delim)[-1] # .replace("_000", "-") - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - - if len(name) > max_len: - name = name[:((max_len-2)//2)] + "..." + name[((max_len-2)//2):] - else: - name = seq_name[:((max_len-2)//2)] + "..." + seq_name[((max_len-2)//2):] - """ - - return name - -def unicode_name(name): - """ - replace non-ascii characters in string (e.g. for use in matplotlib) - """ - unicode_string = eval('u"%s"' % name) - return unicodedata.normalize('NFKD', unicode_string).encode('ascii','ignore') - -def check_bools(arg, update_log_txt = True, default=None): - """ - converts commandline arguments into boolean - """ - - - # convert valid arguments - if str(arg).lower() == "y" or str(arg) == "1": - return True - elif str(arg).lower() == "n" or str(arg) == "0": - return False - - # use default in case of invalid argument - else: - if update_log_txt: - global log_txt - log_txt += "using default for " + str(arg) - else: - try: - logprint("using default for " + str(arg)) - except: - print "using default for " + str(arg) - return default - -def create_color_list(number, color_map=None, logging=False, max_grey="#595959"): - """ - create color list with given number of entries - grey by default, matplotlib color_map can be provided - """ - - try: - # create pylab colormap - cmap = eval("P.cm." + color_map) - # get descrete color list from pylab - cmaplist = [cmap(i) for i in range(cmap.N)] # extract colors from map - # determine positions for number of colors required - steps = (len(cmaplist)-1)/(number) - numbers = range(0, len(cmaplist), steps) - - # extract color and convert to hex code - colors = [] - for idx in numbers[:-1]: - rgb_color = cmaplist[idx] - col = rgb2hex(rgb_color[0]*255, rgb_color[1]*255, rgb_color[2]*255) - colors.append(col) - - # grey - except: - if not color_map == None: - logprint("Invalid color_map (%s) provided! - Examples: jet, Blues, OrRd, bwr,..." % color_map) - logprint("See https://matplotlib.org/users/colormaps.html\n") - old_max_grey = "#373737" - old_max_grey = "#444444" - colors = list(Color("#FFFFFF").range_to(Color(max_grey), number)) # grey - for idx in range(len(colors)): - colors[idx] = str(colors[idx]).replace("Color ", "") - if "#" in colors[idx] and len(colors[idx]) != 7: - # print colors[idx] - colors[idx] = colors[idx] + colors[idx][-(7-len(colors[idx])):] - - text = "%d Colors: %s" % (len(colors), ", ".join(colors)) - if logging: logprint(text, start=False, printing=True) - - if len(colors) < number: - logprint("\nError in color range definition! %d colors missing\n" % (number - len(colors))) - - return colors - - -############################### -# File Handling # -############################### - -def read_seq(input_fasta, verbose=False): - """ - read fasta sequences from (all) file(s) - """ - - # check if file provided - if input_fasta == [] or input_fasta == "": - text = "Attention: No valid file names provided: >%s<" % input_fasta - logprint(text, start=False, printing=True) - return {}, [] - - # combine sequence files, if required - if type(input_fasta) == list: - # concatenate fasta files - if len(input_fasta) > 1: - if verbose: - print "concatenating fastas...", - text = "concatenating fastas..." - input_fasta_combi = concatenate_files(input_fasta) - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - else: - input_fasta_combi = input_fasta[0] - else: - input_fasta_combi = input_fasta - - # read sequences - if verbose: - print "reading fasta...", - text = "reading fasta...", - try: - seq_dict = SeqIO.index(input_fasta_combi, "fasta") - except ValueError: - logprint("Error reading fasta sequences - please check input files, e.g. for duplicate names!") - return {}, [] - except: - logprint("Error reading fasta sequences - please check input files!") - return {}, [] - - if verbose: - print "done" - text += "done" - logprint(text, start=False, printing=False) - - for seq in seq_dict: - if "-" in seq_dict[seq].seq: - # ungapped = seq_dict[seq].seq.ungap("-") # cannot be assigned back to sequence record - text = "\nSequences degapped prior Analysis!!!" - logprint(text, start=False, printing=True) - return read_seq(degap_fasta(input_fasta), verbose=verbose) - - # get ordered sequence names - sequences = [] - for item in SeqIO.parse(input_fasta_combi, "fasta"): - sequences.append(item.id) - return seq_dict, sequences - -def read_gff_color_config(gff_color_config_file=""): - """ - define coloring options for gff-based color shading of self-dotplots - """ - - # default aestetics for annotation shading (e.g. if no user config file is provided) - # dictionary with feature_type as key and tuple(color, transparency, zoom) as value - gff_feat_colors = {"orf": ("#b41a31", 0.2, 0), - "orf_rev": ("#ff773b", 0.3, 0), - "gene": ("#b41a31", 0.2, 0), - "cds": ("darkorange", 0.2, 0), - "exon": ("orange", 0.2, 0), - "intron": ("lightgrey", 0.2, 0), - "utr": ("lightblue", 0.2, 0), - "repeat_region": ("green", 0.3, 0), - "repeat": ("green", 0.3, 0), - "tandem_repeat": ("red", 0.3, 0), - "transposable_element": ("blue", 0.3, 0), - "ltr_retrotransposon": ("#cccccc", 0.5, 0), - "ltr-retro": ("#cccccc", 0.5, 0), - "long_terminal_repeat": ("#2dd0f0", 0.75, 2), - "ltr": ("#2dd0f0", 0.75, 2), - "pbs": ("purple", 0.75, 2), - "ppt": ("#17805a", 0.5, 2), - "target_site_duplication": ("red", 0.75, 2), - "misc_feature": ("grey", 0.3, 0), - "misc_feat": ("grey", 0.3, 0), - "misc": ("grey", 0.3, 0), - "others": ("grey", 0.5, 0)} - if gff_color_config_file in ["", None] or not os.path.exists(str(gff_color_config_file)): - return gff_feat_colors - - text = "Updating GFF color configuration with custom specifications\n" - logprint(text, start=False, printing=True) - - # read custom gff_color_config_file - in_file = open(gff_color_config_file, 'rb') - overwritten = set([]) - for line in in_file: - if not line.startswith("#") and len(line.strip().split("\t")) >= 4: - data = line.strip().split("\t") - feat = data[0].lower() - color = data[1].lower() - - # check, if settings are valid - if not mcolors.is_color_like(color): - color = "grey" - text = "Invalid color specified for %s: %s - default grey" % (data[0], data[1]) - logprint(text) - try: - alpha = float(data[2]) - except: - alpha = 0.75 - text = "Invalid alpha specified for %s: %s - default 0.75" % (data[0], data[2]) - logprint(text) - try: - zoom = float(data[3]) - except: - zoom = 0 - text = "Invalid zoom specified for %s: %s - default 0" % (data[0], data[3]) - logprint(text) - - # track changes of predefined settings - if feat in gff_feat_colors.keys(): - overwritten.add(data[0].lower()) - - gff_feat_colors[feat] = (color, alpha, zoom) - in_file.close() - - # default coloring for unknown annotations - if not "others" in gff_feat_colors.keys(): - gff_feat_colors["others"] = ("grey", 0.5, 0) - - if verbose: - # print configuration - text = "\n\nGFF color specification:\n%s\n" % (60 * ".") - for item in sorted(gff_feat_colors.keys()): - text += "%-30s\t%-10s\t%-5s\t%s\n" % (item, str(gff_feat_colors[item][0]), str(gff_feat_colors[item][1]), str(gff_feat_colors[item][2])) - logprint (text, printing=True) - - # print overwritting feature type specifications - if len(overwritten) != 0: - text = "%d feature type specifications overwritten:" % len(overwritten) - text += "\n\t"+ ", ".join(overwritten) + "\n" - logprint(text, start=False, printing=True) - - text = "GFF color specification updated acc. to %s\n\t%s\n\n" % (gff_color_config_file, ", ".join(gff_feat_colors)) - logprint(text, start=False, printing=True) - - return gff_feat_colors - -def read_gffs(input_gff_files, color_dict={"others": ("grey", 1, 0)}, type_nuc=True, prefix="", filetype='png', verbose=False): - """ - create feature dictionary from input_gff - sequence name as key and (feature type, start, stop) as value - """ - if type(input_gff_files) != list: - input_gff_files = [input_gff_files] - - # create dictionary with seq_name as key and (type, start and stop) as value - unknown_feats = set([]) - used_feats = set([]) - feat_dict = {} - for input_gff in input_gff_files: - text = "...reading " + input_gff - logprint(text, start=False, printing=True) - - in_file = open(input_gff, 'rb') - for line in in_file: - if not line.startswith("#") and line.strip() != "": - data = line.strip().split("\t") - feat_type = data[2].lower() - if data[6] == "-": - feat_type += "_rev" - if not feat_type.lower() in color_dict.keys(): - if feat_type.lower().replace("_rev", "") in color_dict.keys(): - feat_type = feat_type.replace("_rev", "") - else: - unknown_feats.add(feat_type) - feat_type = "others" - used_feats.add(feat_type) - if not data[0] in feat_dict.keys(): - feat_dict[data[0]] = [(feat_type, int(data[3]), int(data[4]))] # feature type, start, stop - else: - feat_dict[data[0]].append((feat_type, int(data[3]), int(data[4]))) # feature type, start, stop - if verbose: - text = "\nAnnotations for: %s\n" % ", ".join(feat_dict.keys()[:10]) - if len(feat_dict.keys()) > 10: - text = text[:-1] + ", ...\n" - logprint(text, start=False, printing=True) - in_file.close() - - # print feature types without specific shading settings - if len(unknown_feats) != 0: - text = "Missing shading specification for %d feature type(s):\n\t%s\n" % (len(unknown_feats), ", ".join(sorted(unknown_feats))) - logprint(text, start=False, printing=True) - - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure(colors=colors, lcs_shading_num=len(used_feats), type_nuc=type_nuc, bins=sorted(used_feats), alphas=alphas, gff_legend=True, prefix=prefix, filetype=filetype) - - # print settings - text = "GFF Feature Types: %s\nGFF Colors: %s" % (", ".join(sorted(used_feats)), ", ".join(sorted(colors))) - logprint(text, start=False, printing=True) - - return feat_dict - -def read_matrix(matrix_file_name, delim="\t", symmetric=True, recursion=False, verbose=False): - input_file = open(matrix_file_name, 'rb') - - # read sequence names from first column - names = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - names.append(line.strip().split(delim)[0]) - logprint("Delimiter '%s': %d names - %s\n" % (delim, len(names), ", ".join(names))) - - # check if names were found - otherwise try another delimiter - if names == [] and not recursion: - if delim == "\t": - new_delim = "," - else: - new_delim = "\t" - logprint("\nMatrix file not containing data delimited by '%s' - trying to read matrix with delimiter '%s'" % (delim.replace("\t", "\\t"), new_delim)) - info_dict = read_matrix(matrix_file_name, delim=new_delim, symmetric=symmetric, recursion=True, verbose=verbose) - return info_dict - elif names == []: - logprint("Empty matrix file with alternative delimiter!") - return info_dict - input_file.close() - - input_file = open(matrix_file_name, 'rb') - # read matrix entries as values in dictionary with tuple(names) as key - info_dict = {} - contradictory_entries = [] - for line in input_file: - if not line.startswith("#") and not line.startswith(delim) and delim in line: - data = line.strip().split(delim) - for idx in range(len(data[1:])): - # print tuple(sorted([data[0], names[idx]])), data[idx+1] - if symmetric: - key = tuple(sorted([names[idx], data[0]])) - else: - key = tuple(names[idx], data[0]) - if key in info_dict.keys(): - if symmetric and info_dict[key] != data[idx+1] and data[idx+1] not in ["", "-"] and info_dict[key] not in ["", "-"]: - contradictory_entries.append(key) - info_dict[key] = data[idx+1] - input_file.close() - - if len(contradictory_entries) != 0: - try: - logprint("\nContradictory entries in matrix file %s:\n\t%s" % (matrix_file_name, ", ".join(contradictory_entries))) - except: - log_txt = "\nContradictory entries in matrix file %s:\n\t" % (matrix_file_name) - for item in contradictory_entries: - log_txt += str(item).replace("'", "") + ", " - log_txt = log_txt[:-2] - logprint(log_txt) - logprint("Using value from bottom left triangle!") - if verbose: - logprint("\nMatrix information for Sequences named: " % ", ".join(names)) - - return info_dict - -def concatenate_files(file_list, combi_filename="temp_combined.fasta", verbose=False): - """ - concatenate content of all files in file_list into a combined file named combi_filename - """ - out_file = open(combi_filename, 'w') - text = "" - for item in file_list: - if verbose: - text += item + " " - print item, - # read in_file linewise and write to out_file - in_file = open(item, 'rb') - for line in in_file: - out_file.write(line.strip()+"\n") - in_file.close() - out_file.close() - if verbose: - logprint(text, start=False, printing=False) - return combi_filename - -def degap_fasta(input_fasta): - """ - remove gaps from fasta - new degapped sequence file created - """ - - # degap all sequence files - output_fastas = [] - if type(input_fasta) != list: - input_fasta = list(input_fasta) - for input_fas in input_fasta: - output_fas = input_fas[:input_fas.rfind(".")] + "_degapped.fas" - in_file = open(input_fas, 'rb') - out_file = open(output_fas, 'w') - for line in in_file: - if line.startswith(">"): - out_file.write(line.strip()+"\n") - else: - out_file.write(line.strip().replace("-", "")+"\n") - out_file.close() - in_file.close() - output_fastas.append(output_fas) - return output_fastas - -def legend_figure(colors, lcs_shading_num, type_nuc=True, unit="%", filetype="png", max_len=None, min_len=0, bins=[], alphas=[], gff_legend=False, prefix="", verbose=False): - """ - create figure color legend - """ - max_legend_length_row = 8 - max_legend_length_col = 4 - - # define output file - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg" - logprint(text, start=False, printing=True) - filetype="png" - - # check if length of information fit - if not gff_legend and ((bins != [] and len(colors) != lcs_shading_num+1) or (bins != [] and len(colors) != len(bins)+1)): - if bins != [] and len(colors) != lcs_shading_num+1: - text = "**Attention**\nlcs_shading_num (%d) does not match number of colors (%d)!\n"% (lcs_shading_num, len(bins)) - elif bins != [] and len(colors) != len(bins)+1: - text = "**Attention**\nnumber of LCS length bins (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - elif gff_legend and len(bins) != len(colors): - text = "**Attention**\nnumber of GFF Feature Types (%d) does not match number of colors (%d)!\n" % (len(colors), len(bins)) - logprint(text, start=False, printing=True) - - # set alpha values to opaque if none are provided - if alphas == []: - for item in colors: - alphas.append(1) - - # legend data points - data_points = range(len(colors)) - if not gff_legend: - - # specify intervals, if max_len provided - if max_len != None: - multi_factor = 100 # one digit - if max_len <= 1: - multi_factor = 1000 # two digits - # len_interval_size = (max_len-min_len) * multi_factor *1. // lcs_shading_num * (1./ multi_factor) - len_interval_size = (max_len-min_len) * 1. / lcs_shading_num - len_pos = [float("%.2f" % (min_len))] - # calculate interval positions - for idx in range(lcs_shading_num): - len_pos.append(float("%.2f" % (len_pos[-1] + len_interval_size))) - - if prefix.startswith("custom-matrix") and (0 <= max_len <= 100 and 0 <= min_len <= 100): - unit = "%" - elif prefix.startswith("custom-matrix"): - unit = "" - - text = "\n%d Legend intervals from %.2f to %.2f: \n\t%s - number: %d, step: %.2f, unit: %s\n" % (lcs_shading_num+1, min_len, max_len, str(len_pos), len(len_pos), len_interval_size, unit) - logprint(text, start=False, printing=True) - pos = len_pos - interval_size = len_interval_size - # generate legend labels acc. to standard interval notation - else: - # use default max_len = 100 and min_len = 0 - len_interval_size = 100. / lcs_shading_num - pos = [float("%.2f" % (0))] - # calculate interval positions - for idx in range(lcs_shading_num): - pos.append(float("%.2f" % (pos[-1] + len_interval_size))) - - # interval_size = 100 // lcs_shading_num - # pos = range(interval_size, 101+interval_size, interval_size) - - # remove unneccessary zeros in decimal places (i.e. if x.x00 in all entries) - while True: - last_digit_all_zero = True - no_delim = False - for idx in range(len(pos)): - # only process if fraction with decimal places - if not "." in str(pos[idx]): - no_delim = True - break - # only process when all entries end in zero - elif str(pos[idx])[-1] != "0": - last_digit_all_zero = False - break - if not last_digit_all_zero or no_delim: - break - # remove last decimal place (== 0) from all entries - else: - temp_pos = pos[:] - for idx in range(len(pos)): - if not str(pos[idx])[-2] == ".": - pos[idx] = float(str(pos[idx])[:-1]) - else: - pos[idx] = int(str(pos[idx])[:-2]) - logprint("Shortening legend entries: %s - %s" % (temp_pos, pos)) - - # eliminate fractions if unit == bp/aa - if unit in ["aa", "bp"]: - for idx in range(len(pos)): - temp_pos = pos[:] - rounded_unit = False - if "." in str(pos[idx]): - rounded_unit = True - # round values up to next integer (keep integer, if not a fraction) - pos[idx] = int(pos[idx] / 1) + int(pos[idx] % 1 > 0) - if idx == len(pos) - 1 and pos[idx] == 101: - pos[idx] = 100 - if rounded_unit: - logprint("Fractions not permitted for unit '%s': %s -> %s" % (unit, temp_pos, pos)) - - if bins != []: # labels provided - legend_labels = bins[:] - legend_labels.append("max") - legend_labels_lengths = [] - for item in bins: - legend_labels_lengths.append("[%d %s, %d %s)" % (item - min(bins), unit, item, unit)) - if len(bins) == len(colors) - 1: - legend_labels_lengths.append("[%d %s, %s]" % (max(bins), unit, u"\u221E")) # infinite - - else: - legend_labels = [] - legend_labels_lengths = [] - for idx in range(len(pos)): - num = pos[idx] - try: - legend_labels.append("[%d%%, %d%%)" % (num - interval_size, num)) - except: - legend_labels.append("[%d%%, %d%%)" % (num - len_interval_size, num)) - if max_len != None: - num = len_pos[idx] - # as int or float - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths.append("[%d %s, %d %s)" % (num, unit, num + len_interval_size, unit)) - else: - legend_labels_lengths.append("[%.2f %s, %.2f %s)" % (num, unit, num + len_interval_size, unit)) - legend_labels[-1] = "100" + unit - if max_len != None: - if num == int(num) and int(len_interval_size) == len_interval_size: - legend_labels_lengths[-1] = u"[%d %s, \u221E]" % (max_len, unit) - else: - legend_labels_lengths[-1] = u"[%.2f %s, \u221E]" % (max_len, unit) - - # set labels and choose file name - if gff_legend: - label_text = bins[:] - edge_col = None - legend_file_name = "GFF_Shading_Legend_n%d." % lcs_shading_num + filetype - elif max_len != None: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_max%d%s_n%d." % (max_len, unit, lcs_shading_num+1) + filetype - elif bins != []: - label_text = legend_labels_lengths[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%d%s_n%d." % (bins[0], unit, lcs_shading_num+1) + filetype - else: - label_text = legend_labels[:] - edge_col = "black" - legend_file_name = "Polydotplot_LCS_Shading_Legend_%%len_n%d." % (lcs_shading_num+1) + filetype - - if prefix != None and prefix != "": - if not prefix.endswith("-"): - prefix = prefix + "-" - legend_type = "LCS" - if prefix.startswith("custom-matrix"): - prefix = prefix.replace("custom-matrix", "")[1:] - legend_type = "CustomMatrix" - legend_file_name = prefix + legend_file_name.replace("LCS", legend_type) - - # plot legend figure - fig, ax = P.subplots(3, 1, figsize=(len(colors)*2, len(colors)*2)) - for idx in range(len(colors)): - ax[0].bar(data_points[idx]+1, data_points[idx]+1, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[2].bar(data_points[idx]+1, 0, color=colors[idx], label=label_text[idx], - alpha=alphas[idx], edgecolor=edge_col) - ax[1].set_ylim(0,1) - ax[2].set_ylim(0,1) - ax[1].legend(ncol=((len(colors)-1)//max_legend_length_row)+1, framealpha=1) # vertical legend - col_num = len(colors) - if len(colors) > max_legend_length_col: - remainder = 0 - if len(colors) % max_legend_length_col != 0: - remainder = 1 - row_num = len(colors) // max_legend_length_col + remainder - remainder = 0 - if len(colors) % row_num != 0: - remainder = 1 - col_num = len(colors) // row_num + remainder - ax[2].legend(ncol=col_num, framealpha=1) # horizontal legend - - P.savefig(legend_file_name) - - return legend_file_name - - -############################### -# Analysis Functions # -############################### - -def wobble_replacement(sequence, general_ambiguity_code, verbose=False): - """ - get all degenerated sequences for sequence with ambiguous residues - (only residues considered that are keys in wobble_dictionary) - """ - - # get positions of ambiguous residues - wobble_pos = [] - for idx in range(len(sequence)): - letter = sequence[idx] - if letter in general_ambiguity_code.keys(): - wobble_pos.append(idx) - - if verbose: - text = "\t%d wobbles" % len(wobble_pos) - logprint(text, start=False, printing=True) - - # replace one wobble through each iteration by all possible residues - # repeat if still wobbles in new kmers - kmer_variants = [sequence] - while True: - if verbose: - text = "\t\t%d kmer variants" % len(kmer_variants) - logprint(text, start=False, printing=True) - temp_kmers = set([]) - for kmer in kmer_variants: - for idx in wobble_pos: - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - for base in general_ambiguity_code[kmer[idx]]: - newkmer = kmer[:idx] + base + kmer[idx+1:] - temp_kmers.add(newkmer) - wobble = False - for kmer in temp_kmers: - for idx in range(len(kmer)): - letter = kmer[idx] - if letter in general_ambiguity_code.keys(): - wobble = True - break - if wobble: - break - kmer_variants = set(list(temp_kmers)[:]) - if not wobble: - break - - return kmer_variants - -def split_diagonals(data, stepsize=1): - """ - split array if point difference exceeds stepsize - data = sorted list of numbers - """ - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) - -def longest_common_substring(s1, s2): - m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))] - longest, x_longest = 0, 0 - for x in xrange(1, 1 + len(s1)): - for y in xrange(1, 1 + len(s2)): - if s1[x - 1] == s2[y - 1]: - m[x][y] = m[x - 1][y - 1] + 1 - if m[x][y] > longest: - longest = m[x][y] - x_longest = x - else: - m[x][y] = 0 - return longest - -def lcs_from_x_values(x_values): - """ - calculate length of longest common substring based on nested list of numbers - """ - if len(x_values) == 0: - return 0 - # get lengths of each subarray data - lengths = np.array([len(i) for i in x_values]) - return max(lengths) - - -############################### -# Matching Functions # -############################### - -def find_match_pos_diag(seq1, seq2, wordsize, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - - # forward - ################################# - kmer_pos_dict_one = {}; kmer_pos_dict_two = {} # dictionaries for both sequences - - # reverse complement - ################################# - kmer_pos_dict_three = {}; kmer_pos_dict_four = {} # dictionaries for both sequences - - # create dictionaries with kmers (wordsize) and there position(s) in the sequence - if rc_option: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two), - (str(seq_one), kmer_pos_dict_three), - (str(seq_two.reverse_complement()), kmer_pos_dict_four)] - else: - data_list = [(str(seq_one), kmer_pos_dict_one), - (str(seq_two), kmer_pos_dict_two)] - for (seq, kmer_pos_dict) in data_list: - for i in range(len(seq)-wordsize+1): - kmer = seq[i:i+wordsize] - # discard kmer, if too many Ns included - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - if not convert_wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - wobbles = False - for item in general_ambiguity_code.keys(): - if item in kmer: - wobbles = True - break - if not wobbles: - try: - kmer_pos_dict[kmer].append(i) - except KeyError: - kmer_pos_dict[kmer] = [i] - else: - kmer_variants = wobble_replacement(kmer, general_ambiguity_code) - for new_kmer in kmer_variants: - # print "\t", new_kmer - try: - kmer_pos_dict[new_kmer].append(i) - except KeyError: - kmer_pos_dict[new_kmer] = [i] - - # find kmers shared between both sequences - matches_for = set(kmer_pos_dict_one).intersection(kmer_pos_dict_two) # forward - matches_rc = set(kmer_pos_dict_three).intersection(kmer_pos_dict_four) # reverse complement - - if verbose: - text = "[matches: %i for; %.i rc]" % (len(matches_for), len(matches_rc)) - logprint(text, start=False, printing=True) - - # create lists of x and y co-ordinates for scatter plot - # keep all coordinates of all shared kmers (may match multiple times) - diag_dict_for = {} - diag_dict_rc = {} - for (match_list, pos_dict1, pos_dict2, diag_dict) in [(matches_for, kmer_pos_dict_one, kmer_pos_dict_two, diag_dict_for), - (matches_rc, kmer_pos_dict_three, kmer_pos_dict_four, diag_dict_rc)]: - for kmer in match_list: - for i in pos_dict1[kmer]: - for j in pos_dict2[kmer]: - diag = i-j - points = set(range(i+1, i+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - -def find_match_pos_regex(seq1, seq2, wordsize, substitution_count=0, report_lcs=False, rc_option=True, convert_wobbles=False, max_N_percentage=49, type_nuc=True, verbose=False): - """ - find all matching positions with matches >= wordsize via regular expression search - fuzzy matching - allow up to substitution_count substitutions - convert matching points into lines of the length of the match - (+ optional handling of ambiguities) - """ - global t1 # timer - - # read sequences - seq_one = seq1.upper(); len_one = len(seq_one) - seq_two = seq2.upper(); len_two = len(seq_two) - - # set ambiguity code for wobble replacement - general_ambiguity_code = alphabets(type_nuc)[2] # nucleotide_ambiguity_code or aminoacid_ambiguity_code - ambiguity_match_dict = alphabets(type_nuc)[3] - - ambiq_residues = "[%s]" % "".join(general_ambiguity_code.keys()) - - # look for Ns in DNA or Xs in proeins (minimum word size) - if type_nuc == True: - any_residue = "N" - else: - any_residue = "X" - - # check for wobble presence - if not (regex.search(ambiq_residues, str(seq_one)) == None and regex.search(ambiq_residues, str(seq_two)) == None): - wobble_found = True - else: - wobble_found = False - - # dictionary for matches - diag_dict_for = {} - diag_dict_rc = {} - counter = [0, 0] - - # one-way matching - if rc_option: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0), - (str(seq_one), str(seq_two.reverse_complement()), diag_dict_rc, 1)] - else: - data_list = [(str(seq_one), str(seq_two), diag_dict_for, 0)] - - for seq_query, seq_target, diag_dict, counter_pos in data_list: - # split query sequence into kmers - if not rc_option and counter_pos == 1: - break - - for idx in range(len(str(seq_query))-wordsize+1): - kmer = str(seq_query)[idx:idx+wordsize] - - # skip excessive N/X stretches (big black areas) - if kmer.count(any_residue)*100./wordsize <= max_N_percentage: - # convert kmer to regular expression for wobble_matching - if convert_wobbles and wobble_found: - kmer_string = "" - # replace each residue with matching residues or wobbles - for jdx in range(len(kmer)): - kmer_string += ambiguity_match_dict[kmer[jdx]] - else: - kmer_string = kmer - - # convert to regular expression tolerating substitution errors - if type(substitution_count) == int and substitution_count != 0: - kmer_string = "(%s){s<=%d}" % (kmer_string, substitution_count) - - # search for regular expression in target sequence - kdx = 0 - start = True - if regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - while regex.search(kmer_string, seq_target[kdx:]) != None: - # search for regular expression pattern in target sequence - result = regex.search(kmer_string, seq_target[kdx:]) - - kmer2 = seq_target[kdx:][result.start():result.end()] - - # skip excessive N/X stretches (big black areas) - if kmer2.count(any_residue)*100./wordsize <= max_N_percentage: - diag = idx-(kdx+result.start()) - points = set(range(idx+1, idx+wordsize+1)) - if not diag in diag_dict.keys(): - diag_dict[diag] = points - else: - diag_dict[diag].update(points) - - kdx += result.start() + 1 - if kdx >= len(seq_target): - break - elif regex.search(kmer_string, seq_target[kdx:]) != None: - counter[counter_pos] += 1 - - if verbose: - text = "%5.i \tforward matches" % counter[0] - text += "\n%5.i \treverse complementary matches" % counter[1] - logprint(text, start=False, printing=True) - - # convert coordinate points to line start and stop positions - x1 = [] # x values reverse - y1 = [] # y values forward - for diag in diag_dict_for.keys(): - x_values = np.array(sorted(diag_dict_for[diag])) - x1.extend(split_diagonals(x_values)) - y_values = split_diagonals(x_values - diag) - y1.extend(y_values) - - x2 = [] # x values rc - y2 = [] # y values rc - if rc_option: - for diag in diag_dict_rc.keys(): - factor = len_two + diag + 1 - x_values = np.array(sorted(diag_dict_rc[diag])) - x2.extend(split_diagonals(x_values)) - y_values = split_diagonals(factor - x_values, -1) - y2.extend(y_values) - - if verbose: - t1 = time_track(t1) - - if not report_lcs: - return np.array(x1), np.array(y1), np.array(x2), np.array(y2) - else: - # get length of longest common substring based on match lengths - lcs_for = lcs_from_x_values(x1) - lcs_rev = lcs_from_x_values(x2) - return np.array(x1), np.array(y1), np.array(x2), np.array(y2), lcs_for, lcs_rev - - -############################### -# Dot Plot Functions # -############################### - -def selfdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}): - """ - self-against-self dotplot - partially from biopython cookbook - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least one input sequence - if len(sequences) == 0: - text = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - text += " No sequences provided for selfdotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1 and multi: - text = "\n\nCreating collage output for single selfdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nSelfdotplot Collage: Invalid collage - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences): - ncols = len(sequences) - nrows = 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nSelfdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Selfdotplot" - else: legend_prefix = "Selfdotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - global t1 - - print "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-"), - log_txt = "\n%s\n\nCreating %s selfdotplot images\n%s\n\n=>" % (50*"=", len(sequences), 28*"-") - - # preparations for file name - name_graph = "Selfdotplots" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - list_of_png_names = [] - - counter = 0 - for seq_name in sequences: - print seq_name, - log_txt += " " + seq_name - - counter += 1 - if not multi: - P.cla() # clear any prior graph - - # read sequence - seq_record = seq_dict[seq_name] - name_seq = seq_record.id - seq_one = seq_record.seq.upper() - length_seq = len(seq_one) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_regex(seq_one, seq_one, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG", - x_lists, y_lists, x_lists_rc, y_lists_rc = find_match_pos_diag(seq_one, seq_one, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, type_nuc=type_nuc, verbose=verbose) - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - # print P.xticks()[0], P.yticks()[0] - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, fontweight='bold') - # P.title(unicode_name(name_seq), fontsize=label_size*1.3, fontweight='bold') - - # save figure and reinitiate if page is full - if counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' % (prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - else: # not multi - - fig = P.figure(figsize=(plot_size, plot_size)) # figure size needs to be a square - ax = P.subplot(1, 1, 1) # rows, columns, plotnumber - - # shade annotated regions - if gff_files != None and gff_files != []: - if seq_name in feat_dict.keys(): - features = feat_dict[seq_name] - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(length_seq+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # collect lines - lines = [] - number = 0 - color_list = [] - for (x_lines, y_lines, col) in [(x_lists_rc, y_lists_rc, line_col_rev), (x_lists, y_lists, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.axis('scaled') # make images quadratic - P.xlim(0, length_seq+1) - if mirror_y_axis: - P.ylim(0, length_seq+1) # rotate y axis (point upwards) - else: - P.ylim(length_seq+1, 0) # rotate y axis (point downwards) - P.xlabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.ylabel("[%s]" % aa_bp_unit, fontsize=label_size) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # # use same tick labels for x and y axis - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(0, length_seq+1) - - P.title(unicode_name(shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size*1.3, fontweight='bold') - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s-%d_%s_wordsize%i%s.%s' %(prefix, name_graph, counter, shorten_name(name_seq, max_len=title_length, title_clip_pos=title_clip_pos), wordsize, suffix, filetype) - P.savefig(fig_name, bbox_inches='tight') - - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - if multi and counter >= 1: - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - P.subplots_adjust(hspace=0.5, wspace=0.5) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() # clear any prior graph - - list_of_png_names.append(fig_name) - - print "\n\nDrawing selfdotplots done" - log_txt += "\n\nDrawing selfdotplots done" - logprint(log_txt, start=False, printing=False) - - return list_of_png_names - -def pairdotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, multi=True, ncols=4, nrows=5, x_label_pos_top=True, only_vs_first_seq=False, length_scaling=True, scale_delim_col="red"): - """ - pairwise dotplot (all-against-all) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - # check if at least two input sequences - if len(sequences) < 2: - text = "\n%s\n\nCreating %d paired dotplot image \n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += " Please provide at least two sequences for pairdotplot!\n\nTerminating paired dotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 2 and multi: - text = "\n\nCreating collage output for single pairdotplot!" - text += "\nRecommendation: Change to individual mode by using '--collage_output n'!\n\n" - logprint(text, start=False, printing=True) - - if multi and (ncols == 0 or nrows == 0): - ncols = max(ncols, 1) - nrows = max(nrows, 1) - text = "\n\nPairdotplot Collage: Invalid collage settings - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if multi and ncols > len(sequences)*(len(sequences)-1): - ncols = len(sequences) - nrows = 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows and columns:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - elif multi and ncols*(nrows-1) > len(sequences)*(len(sequences)-1): - nrows = ((len(sequences)-1) // ncols) + 1 - text = "\n\nPairdotplot Collage: Few sequences - correcting number of rows:\n\tncols=%d, nrows=%d\n" % (ncols, nrows) - logprint(text, start=False, printing=True) - - if not only_vs_first_seq: - text = "\n%s\n\nCreating %d paired dotplot image for\n%s\n\n=>" % (50*"=", len(sequences)*(len(sequences)-1)/2, 36*"-") - text += ", ".join(sequences) + "\n" - else: - text = "\n%s\n\nCreating %d paired dotplot images against 1st sequence '%s':\n%s\n\n=>" % (50*"=", len(sequences)-1, sequences[0], 36*"-") - text += ", ".join(sequences[1:]) + "\n" - logprint(text, start=False, printing=True) - - if multi and not (nrows == 1 and ncols == 1) and plot_size <= label_size/2: - label_size = plot_size * 3 // 2 - text = "Reducing label size for better visualization to %d\n" % label_size - logprint(text, start=False, printing=True) - - y_label_rotation = "vertical" - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis: - x_label_pos_top = False - - # preparations for file name - name_graph = "Pairdotplot" - if prefix != None: - if not prefix[-1] == "-": - prefix = prefix + "-" - else: - prefix = "" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if length_scaling: - suffix += "_scaled" - if multi: - suffix += "_collage" - - # calculate fig ratios - if not multi: - ncols = 1 - nrows = 1 - figsize_x, figsize_y = calc_fig_ratio(ncols, nrows, plot_size) - - P.cla() # clear any prior graph - list_of_png_names = [] - if multi: - fig = P.figure(figsize=(figsize_x, figsize_y)) - page_counter = 1 - - # prepare LCS data file - lcs_data_file = open("%sPairdotplot_wordsize%d_lcs_data_file%s.txt" % (prefix, wordsize, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - counter, seq_counter = 0, 0 - print "Drawing pairwise dotplot...", - log_txt = "Drawing pairwise dotplot..." - if verbose: - seq_text = "" - for idx in range(len(sequences)-1): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx+1, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += " " + str(seq_counter) - - # get positions of matches - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - - # write LCS data file - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - - # plotting with matplotlib - ################################# - - # combined plotting - if multi: - # plotting subplot with matplotlib - ax = P.subplot(nrows, ncols, counter) # rows, columns, plotnumber - - else: - # calculate figure size for separate figures - if len_one >= len_two: - sizing = (plot_size, max(2, (plot_size)*len_two*1./len_one)) - # sizing = (plot_size, min(plot_size, max(2, (plot_size-2)*len_two*1./len_one+2))) - else: - sizing = (max(2, (plot_size)*len_one*1./len_two), plot_size) - # sizing = (min(plot_size, max(2, (plot_size-2)*len_one*1./len_two+2)), plot_size) - fig = P.figure(figsize=(plot_size, plot_size)) - - ax = P.subplot(1, 1, 1) - - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # format axes - P.xlabel(unicode_name(shorten_name(name_one, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.ylabel(unicode_name(shorten_name(name_two, max_len=title_length, title_clip_pos=title_clip_pos)) + " [%s]" % aa_bp_unit, fontsize=label_size, fontweight='bold', labelpad=4) - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - - # P.axis('scaled') # make images scaled by size ### optional update ### - if not multi: - if length_scaling: - ax.set_aspect(aspect='equal', adjustable='box', anchor='NW') - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - elif not length_scaling: - P.xlim(0, len_one+1) - # xlimit = [0, len_one+1] - if mirror_y_axis: - P.ylim(0, len_two+1) # rotate y axis (point upwards) - else: - P.ylim(len_two+1, 0) # rotate y axis (point downwards) - else: - max_len = max(len_one, len_two) - P.xlim(0, max_len+1) - # xlimit = [0, max_len+1] - if mirror_y_axis: - P.ylim(0, max_len+1) # rotate y axis (point upwards) - else: - P.ylim(max_len+1, 0) # rotate y axis (point downwards) - - # plot line deliminating shorter sequence - if max_len != len_one: - ax.plot((len_one+1, len_one+1), (0, len_two), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - if max_len != len_two: - ax.plot((0, len_one), (len_two+1, len_two+1), marker="", linestyle="--", color=scale_delim_col, markerfacecolor="r") - - # # use same tick labels for x and y axis - # if P.xlim() == P.ylim(): - # tick_locs, tick_labels = P.yticks() - # P.xticks(tick_locs) - # P.xlim(xlimit) - - # evtl. switch x axis position - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - P.setp(ax.get_xticklabels(), fontsize=label_size*.9) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) - - # save figure and reinitiate if page is full - if multi and counter == ncols * nrows: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=.5, wspace=.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=.5, wspace=.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - counter = 0 - page_counter += 1 - - fig = P.figure(figsize=(figsize_x, figsize_y)) - - # plotting separate figure files - elif not multi: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02, left=0.13, bottom=0.05) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.02, wspace=0.02) # space between rows - def 0.4 - - # name and create output files - fig_name = '%s%s-%d_wordsize%i%s.%s' % (prefix, name_graph, counter, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - list_of_png_names.append(fig_name) - fig = P.figure() - - if only_vs_first_seq: - break - - # save figure - if multi and counter >= 1: - - # finalize layout - margins & spacing between plots - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - if x_label_pos_top: - P.subplots_adjust(hspace=0.5, wspace=0.5, top=0.95) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=0.5, wspace=0.5, bottom=0.05) # space between rows - def 0.4 - - # name and create output files (names derived from SEQNAME) - fig_name = '%s%s_wordsize%i%s-%.3d.%s' %(prefix, name_graph, wordsize, suffix, page_counter, filetype) - P.savefig(fig_name, bbox_inches='tight') - P.close() - P.cla() - - list_of_png_names.append(fig_name) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - print - logprint(seq_text, start=False, printing=False) - - return list_of_png_names - -def polydotplot(input_fasta, wordsize, prefix=None, plot_size=10, label_size=10, filetype='png', type_nuc=True, convert_wobbles=False, substitution_count=0, alphabetic_sorting=False, mirror_y_axis=False, title_length=float("Inf"), title_clip_pos="B", max_N_percentage=49, verbose=False, gff_files=[], gff_color_dict={"others": ("grey", 1, 0)}, x_label_pos_top=True, lcs_shading=True, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, lcs_shading_num=5, spacing=0.04, input_user_matrix_file="", user_matrix_print=True, rotate_labels=False): - """ - all-against-all dotplot - derived from dotplot function - - lcs_shading_refs: - 0 color relative to maximum lcs observed in dataset [default] - 1 color by coverage of shorter sequence (e.g. lcs = 70% of seq1) - lcs_shading_ori - 0 forward only - 1 reverse only - 2 both orientations (in opposite plot) - """ - - # read sequences - seq_dict, sequences = read_seq(input_fasta) - if seq_dict == {}: - logprint("\nFailed to load sequences") - return [] - - if alphabetic_sorting: - sequences = sorted(sequences) - - if len(sequences) == 0: - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " No sequences provided for polydotplot!\n\nTerminating polydotplot!" - logprint(text, start=False, printing=True) - return - elif len(sequences) == 1: - text = "\n\nCreating polydotplot for single sequence!" - text += "\nRecommendation: Use selfdotplot via '--plotting_mode 0'!\n\n" - logprint(text, start=False, printing=True) - - text = "\n%s\n\nCreating %dx%d polydotplot image\n%s\n\n=>" % (50*"=", len(sequences), len(sequences), 30*"-") - text += " " + " ".join(sequences) + "\n" - logprint(text, start=False, printing=True) - - # read gff annotation data if provided for shading - if gff_files != None and gff_files != []: - text = "\n%s\n\nReading %s GFF annotation files\n%s\n\n=> %s\n" % (50*"=", len(gff_files), 28*"-", ", ".join(gff_files)) - logprint(text, start=False, printing=True) - if prefix != None and prefix != "": - legend_prefix = prefix + "-Polydotplot" - else: legend_prefix = "Polydotplot" - feat_dict = read_gffs(gff_files, color_dict=gff_color_dict, type_nuc=type_nuc, prefix=legend_prefix, filetype=filetype, verbose=verbose) - - if lcs_shading and not type_nuc: - if lcs_shading_ori != 0: - lcs_shading_ori = 0 - text = "Protein shading does not support reverse complementary matching!\n" - logprint(text, start=False, printing=True) - - # read custom shading matrix & match names of sequences to fasta - if input_user_matrix_file != "" and input_user_matrix_file != None: - logprint("Reading user matrix file: %s" % input_user_matrix_file) - # lcs_shading_ori = 2 - custom_dict = read_matrix(input_user_matrix_file) - if custom_dict != {}: - custom_shading = True - custom_similarity_dict = {} - invalid_entries = [] - custom_max = 0 - custom_min = float("Inf") - for key in custom_dict.keys(): - number_key = [] - - # convert number into float - try: - value = float(custom_dict[key]) - if not "." in custom_dict[key]: - value = int(custom_dict[key]) - custom_max = max(custom_max, value) - custom_min = min(custom_min, value) - except: - value = custom_dict[key] - if value == "": - value = None - invalid_entries.append(key) - # match matrix names with sequence names - for item in key: - if item in sequences: - number_key.append(sequences.index(item)) - else: - number_key.append(-1) - # dictionary with tuple of sorted sequence indices as key and number as value - custom_similarity_dict[tuple(sorted(number_key))] = value - if len(invalid_entries) != 0: - text = "No valid number in custom similarity matrix for %d entries: \n\t" % (len(invalid_entries)) - for key in invalid_entries: - text += str(key) + " - " + str(custom_dict[key]) + "; " - logprint(text[:-2]+"\n") - - text = "Custom user matrix given: min %.2f, max %.2f\n" % (custom_min, custom_max) - - # artificially rounding intervals if likely identity/divergence percentages - if 0 <= custom_min < 1 and 0 < custom_max <= 1: - rounding_factor = 5 - multi_factor = 100 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (multi_factor*custom_min // rounding_factor) * (1.*rounding_factor/multi_factor)) - custom_max = min((multi_factor*custom_max // rounding_factor) * (1.*rounding_factor/multi_factor), 1) - text += "new (%.2f, >%2f)\n" % (custom_min, custom_max) - - elif 0 <= custom_min < 100 and 0 < custom_max <= 100: - rounding_factor = 5 - text += " > artificially rounding custom shading intervals: old (%.2f, %.2f) - " % (custom_min, custom_max) - custom_min = max(0, (custom_min // rounding_factor) * rounding_factor) - custom_max = min((custom_max // rounding_factor) * rounding_factor, 100) - text += "new (%d, >%d)\n" % (custom_min, custom_max) - - logprint(text) - - else: - custom_shading = False - - name_graph = "Polydotplot" - suffix = "" - if convert_wobbles: - suffix += "_wobbles" - if substitution_count != 0: - suffix += "_S%d" % substitution_count - if custom_shading: - suffix += "_matrix" - if lcs_shading: - suffix += "_%dshades_ref%d_ori%s" % (lcs_shading_num+1, lcs_shading_ref, lcs_shading_ori) - if "ref2" in suffix and type_nuc: - suffix = suffix.replace("ref2", "%dbp" % lcs_shading_interval_len) - elif "ref2" in suffix: - suffix = suffix.replace("ref2", "%daa" % lcs_shading_interval_len) - - - # name and create output files (names derived from SEQNAME) - if prefix != None and str(prefix) != "": - prefix = str(prefix) + "-" - else: - prefix = "" - - # preparations for background shading - if lcs_shading or custom_shading: - # create color range white to grey - colors = create_color_list(lcs_shading_num+1, color_map=None, logging=True) - colors_2 = create_color_list(lcs_shading_num+1, color_map="OrRd", logging=True) - - if custom_shading: - text = "Custom Matrix Colors: " + ", ".join(colors_2) - - # write lcs lengths to file - lcs_data_file = open("%sPolydotplot_lcs_data_file%s.txt" % (prefix, suffix.replace("_scaled", "").replace("_collage", "")), 'w') - lcs_data_file.write("\t".join(["#title1", "title2", "len_seq1", "len_seq2", "len_lcs_for", "%_min_seq_len", "len_lcs_rev", "%_min_seq_len"])+"\n") - - # compare sequences pairwise - save lcs and line information in dictionary for plotting - data_dict = {} # keys = tuple(idx, jdx), value = x1, y1, x2, y2 (line positions) - lcs_dict = {} # keys = tuple(idx, jdx), value = length of lcs: lcs_len or (lcs_for, lcs_rev) - for_lcs_set = set([]) # keep lengths to calculate max (excluding self comparisons) - rev_lcs_set = set([]) # keep lengths to calculate max (all) - - text = "\nTotal plot count: %d" % (len(sequences)*(len(sequences))) - text += "\nTotal calculations: %d" % (len(sequences)*(len(sequences)+1)/2) - logprint(text, start=False, printing=True) - - print "\nCalculating shared regions and lengths of longest_common_substring...", - log_txt = "\nCalculating shared regions and lengths of longest_common_substring..." - # determine matches and length of lcs by comparing all sequence pairs - if verbose: - seq_text = "" - counter = 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - name_two = rec_two.id - seq_two = rec_two.seq - len_two = len(seq_two) - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - name_one = rec_one.id - seq_one = rec_one.seq - len_one = len(seq_one) - - counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif len(sequences) < 5: - print "\t%s (%d %s), %s (%d %s)" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - log_txt += "\t%s (%d %s), %s (%d %s)\n" % (name_one, len_one, aa_bp_unit, name_two, len_two, aa_bp_unit) - else: - if not counter % 25: - print counter, - log_txt += str(counter) - - # get positions of matches & length of longest common substring based on match lengths - if substitution_count != 0: - # print "RE" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_regex(seq_one, seq_two, wordsize, substitution_count=substitution_count, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - else: - # print "DIAG" - x1, y1, x2, y2, lcs_for, lcs_rev = find_match_pos_diag(seq_one, seq_two, wordsize, convert_wobbles=convert_wobbles, max_N_percentage=max_N_percentage, report_lcs=True, type_nuc=type_nuc, verbose=verbose) - data_dict[(idx, jdx)] = x1[:], y1[:], x2[:], y2[:] - lcs_dict[idx, jdx] = lcs_for, lcs_rev - - if idx != jdx: - for_lcs_set.add(lcs_for) - rev_lcs_set.add(lcs_rev) - - lcs_data_file.write("\t".join([name_one, name_two, str(len_one), str(len_two), - str(lcs_for), str(round((lcs_for*100./min(len_one, len_two)), 3)), - str(lcs_rev), str(round((lcs_rev*100./min(len_one, len_two)), 3))]) + "\n") - - if not verbose: - print len(sequences)*(len(sequences)+1)/2, " done\n" - log_txt += str(len(sequences)*(len(sequences)+1)/2) + " done\n" - else: - print "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - log_txt += "\n%d done" % (len(sequences)*(len(sequences)+1)/2) - logprint(log_txt, start=False, printing=False) - - if verbose: - logprint ("\n\nlcs_dict\n" + str(lcs_dict)) - if custom_shading: - logprint ("\ncustom_dict\n" + str(custom_dict)) - logprint ("\ncustom_similarity_dict\n\n" + str(custom_similarity_dict)) - - if verbose: - print - logprint(seq_text+"\n", start=False, printing=False) - - if lcs_shading_ref == 2: - color_bins = [] - text = "\nLCS lengh bins: " - for idx in range(lcs_shading_num): - color_bins.append(lcs_shading_interval_len*(idx+1)) - text += " " + str(lcs_shading_interval_len*(idx+1)) - logprint(text, start=False, printing=True) - - # calculate maximum lcs length - if lcs_shading_ori == 0: # forward only - if len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - elif lcs_shading_ori == 1: # reverse complement only - if len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - else: - max_lcs = None - else: # both orientations - if len(rev_lcs_set) != 0 and len(for_lcs_set) != 0: - max_lcs = max(max(rev_lcs_set), max(for_lcs_set)) - elif len(rev_lcs_set) != 0: - max_lcs = max(rev_lcs_set) - elif len(for_lcs_set) != 0: - max_lcs = max(for_lcs_set) - else: - max_lcs = None - - if not max_lcs == None: - text = "Maximum LCS: %d %s" % (max_lcs, aa_bp_unit) - logprint(text, start=False, printing=True) - if custom_shading: - text = "Maximum custom value: %d\n" % custom_max - logprint(text, start=False, printing=True) - - # count sequences - ncols = len(sequences); nrows = len(sequences) - - # get sequence lengths to scale plot widths and heights accordingly - size_ratios = [] - for item in sequences: - size_ratios.append(len(seq_dict[item].seq)) - - P.cla() # clear any prior graph - # use GridSpec to resize plots according to sequence length - if mirror_y_axis: - height_ratios = size_ratios[::-1] - else: - height_ratios = size_ratios[:] - gs = gridspec.GridSpec(nrows, ncols, - width_ratios=size_ratios, - height_ratios=height_ratios) - fig = P.figure(figsize=(plot_size, plot_size)) - - # for cartesian coordinate system with mirrored y-axis: plot x labels below plot - if mirror_y_axis and representation == 1: - x_label_pos_top = True - elif mirror_y_axis or representation == 2: - x_label_pos_top = False - - # print y labels on the right, if upper right triangle is displayed - if (representation == 1 and not mirror_y_axis) or (representation == 2 and mirror_y_axis): - y_label_pos = 0 # last column - else: # left y label - y_label_pos = 1 # first column - - # determine label orientations - if len(sequences) > 5 or rotate_labels: - x_label_rotation = 45 - y_label_rotation = "horizontal" - if x_label_pos_top: - xhalign = 'left' - xvalign = 'bottom' - else: - xhalign = 'right' - xvalign = 'top' - yhalign = "right" - else: - x_label_rotation = "horizontal" - y_label_rotation = "vertical" - xvalign = "center" - xhalign = "center" - yhalign = "center" - yvalign = 'center' - - # check combination of shading parameters for triangular output - if representation != 0 and lcs_shading and custom_shading: # both directions in triangle - logprint("\nAttention: For triangular output custom-shading and LCS shading cannot be combined!\n") - elif representation != 0 and lcs_shading and lcs_shading_ori == 2: # both directions in triangle - logprint("\nAttention: For triangular output LCS shading for both orientations is combined to max of both orientations!\n") - - print "\nDrawing polydotplot...", - log_txt = "\nDrawing polydotplot..." - - # draw subplots - if verbose: - if lcs_shading and custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "Custom matrix value", "Matrix color index", "LCS color index"]) + "\n" - elif lcs_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "LCS for [%s]" %aa_bp_unit, "LCS for [%s]" %aa_bp_unit, "LCS color index for", "LCS color index rev"]) + "\n" - elif custom_shading: - lcs_text = "\n" + "\t".join(["#Seq1", "Seq2", "Custom matrix value", "Color index for", "Color index rev"]) + "\n" - - if verbose: - seq_text = "" - counter, seq_counter = 0, 0 - for idx in range(len(sequences)): - if verbose: - print "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]), - seq_text += "\n%d\t%s vs." % ((seq_counter+1), sequences[idx]) - rec_two = seq_dict[sequences[idx]] - len_two = len(rec_two.seq) - name_two = rec_two.id - - for jdx in range(idx, len(sequences)): - rec_one = seq_dict[sequences[jdx]] - len_one = len(rec_one.seq) - name_one = rec_one.id - - counter += 1 - seq_counter += 1 - if verbose: - print sequences[jdx], - seq_text += " " + sequences[jdx] - elif not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - # optional shade background according to length of LCS and/or user matrix - ######################################################################### - - # get interval based on LCS - background_colors = [None, None] - if lcs_shading and (lcs_shading_ref==1 or lcs_shading_ref==2 or max_lcs!=None): # self plot max_lcs_for == None - lcs_len = lcs_dict[(idx, jdx)] - l1 = lcs_len[0] # forward - l2 = lcs_len[1] # reverse complement - - lcs_shading_bool = True - - # calculate shading acc. to chosen option - if lcs_shading_ref == 1: # percentage of shorter sequence - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // min(len_one, len_two)) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // min(len_one, len_two)) - elif lcs_shading_ref == 2: # by given interval size - color_idx0 = min(len(colors)-1, l1 // lcs_shading_interval_len) - color_idx1 = min(len(colors)-1, l2 // lcs_shading_interval_len) - if color_idx0 >= len(colors): - color_idx0 = len(colors) - if color_idx1 >= len(colors): - color_idx1 = len(colors) - else: # percentage of maximum lcs length - color_idx0 = min(len(colors)-1, l1*lcs_shading_num // max_lcs) - color_idx1 = min(len(colors)-1, l2*lcs_shading_num // max_lcs) - else: - lcs_shading_bool = False - - # get interval based on custom matrix - if custom_shading: - # matrix value - try: - custom_value = custom_similarity_dict[(idx, jdx)] - except: - custom_value = "" - - # bottom left triangle = LCS forward/reverse or best of both - if lcs_shading_bool: - if lcs_shading_ori == 0: # forward - color_idx1 = color_idx0 - elif lcs_shading_ori == 2: # both directions - color_idx1 = max(color_idx0, color_idx1) - - # top right triangle = custom value (not colored if text matrix provided) - if type(custom_value) == int or type(custom_value) == float: - color_idx0 = int((custom_value-custom_min)*lcs_shading_num // (custom_max-custom_min)) - # no color if string is proviced - else: - color_idx0 = 0 - - # use best LCS of both orientations for coloring triangle with two-ori-LCS - if representation != 0 and lcs_shading_ori == 2: # both directions in triangle - color_idx0, color_idx1 = max(color_idx0, color_idx1), max(color_idx0, color_idx1) - - # set colors dependent on lcs dependent on orientation - if lcs_shading_bool and not custom_shading: - if idx != jdx: - if lcs_shading_ori == 0: - color_idx1 = color_idx0 - elif lcs_shading_ori == 1: - color_idx0 = color_idx1 - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx1] - # for selfcomparison, only color reverse complement - elif lcs_shading_ori != 0 and not custom_shading: - background_colors[0] = colors[color_idx1] - # set different colors for shading by LCS + user matrix - elif lcs_shading_bool and custom_shading: - # print colors, background_colors, color_idx0, color_idx1 - background_colors[0] = colors_2[color_idx0] - background_colors[1] = colors[color_idx1] - # set grey color range for user matrix if no LCS shading - elif custom_shading: - background_colors[0] = colors[color_idx0] - background_colors[1] = colors[color_idx0] - - if verbose: - if custom_shading and lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - elif lcs_shading_bool: - lcs_text += "\t".join([name_one, name_two, str(lcs_len[0]), str(lcs_len[1]), str(color_idx0), str(color_idx1)]) + "\n" - elif custom_shading: - lcs_text += "\t".join([name_one, name_two, str(custom_value), str(color_idx0), str(color_idx1)]) + "\n" - - # calculate figure position in polyplot - # diagonal (self-dotplots) - if idx == jdx: - if mirror_y_axis: - seq_num = sequences.index(name_one)+1 - counter1 = seq_num + len(sequences) * (len(sequences)-seq_num) - counter = counter + (counter - 1) // (nrows) - else: - # skip positions below diagonal - counter1 = counter + (counter - 1) // (nrows) # + row_pos - counter = counter1 - counters = [counter1] - - # draw both graphs at once (due to symmetry) - else: - if mirror_y_axis: - col_pos = sequences.index(name_two)+1 - row_pos = len(sequences) - (sequences.index(name_one)+1) - counter1 = row_pos * ncols + col_pos - counter2 = (ncols - col_pos) * ncols + ncols - row_pos - else: - counter1 = counter - col_pos = (counter - 1) % ncols - row_pos = (counter - 1) // (nrows) - counter2 = col_pos * ncols + row_pos + 1 - counters = [counter1, counter2] # lower, upper - - if len(counters) == 2: - seq_counter += 1 - if not verbose and not seq_counter % 25: - print seq_counter, - log_txt += str(seq_counter) - - x_lists, y_lists, x_lists_rc, y_lists_rc = data_dict[(idx, jdx)] - - # plot diagram(s) - for kdx in range(len(counters)): - - if representation == 0 or len(counters) == 1 or (representation == 1 and kdx == 0) or (representation == 2 and kdx == 1): - - fig_pos = counters[kdx] - # plotting subplot with matplotlib - ax = P.subplot(gs[fig_pos-1]) # rows, columns, plotnumber - - # shade annotated regions if gff file(s) provided - if idx == jdx and gff_files != None and gff_files != []: - if name_one in feat_dict.keys(): - features = feat_dict[name_one] - if len_two != len_one: - logprint("Polydot GFF shading for diagonal fields - nequal length error!") - return - for item in features: - feat_type, start, stop = item - feat_color, strength, zoom = gff_color_dict[feat_type.lower()] - start = max(0, start - zoom - 0.5) - stop = min(len_one+1, stop + zoom + 0.5) - width = stop - start - ax.add_patch(patches.Rectangle((start, start), # (x,y) - width, width, # width, height - edgecolor=None, linewidth=line_width+zoom, - fill=True, facecolor=feat_color, - alpha=strength)) - - # if custom matrix value printed into upper matrix triangle, skip data plotting - # text print in top triangle - if user_matrix_print and custom_shading and kdx==0 and idx!=jdx: - data_plotting = False - # dotplot in bottom triangle - else: - data_plotting = True - - # mirror plot, if plotting below diagonal - if kdx == 0: - l1, l2 = len_one, len_two - n1, n2 = name_one, name_two - x1, y1 = x_lists, y_lists - x2, y2 = x_lists_rc, y_lists_rc - else: - l2, l1 = len_one, len_two - n2, n1 = name_one, name_two - x1, y1 = y_lists, x_lists - x2, y2 = y_lists_rc, x_lists_rc - - if mirror_y_axis: - x1, y1, x2, y2 = y1, x1, y2, x2 - n1, n2 = n2, n1 - - if data_plotting: - # collect lines - lines = [] - color_list = [] - for (x_lines, y_lines, col) in [(x2, y2, line_col_rev), (x1, y1, line_col_for)]: - if col != "white": - for ldx in range(len(x_lines)): - lines.append([(x_lines[ldx][0], y_lines[ldx][0]), (x_lines[ldx][-1], y_lines[ldx][-1])]) - color_list.append(col) - color_list = np.array(color_list) - - # draw lines - lc = cllct.LineCollection(lines, colors=color_list, linewidths=line_width) - ax.add_collection(lc) - - # plot value provided by customer instead of dotplot - else: - alignment = {'horizontalalignment': 'center', 'verticalalignment': 'center'} - # P.text(0.5, 0.5, custom_value, size='medium', transform=ax.transAxes, **alignment) - P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, **alignment) - # P.text(0.5, 0.5, custom_value, size=label_size*1.5, transform=ax.transAxes, - # horizontalalignment='center', verticalalignment='center', color="black") - - if custom_shading: - # omit diagonal - if idx == jdx: - ax.set_facecolor("white") - # use white background for text fields (top right triangle only [kdx 0]) - elif type(custom_value) != int and type(custom_value) != float and kdx == 0: - ax.set_facecolor("white") - else: - ax.set_facecolor(background_colors[kdx]) - # set background color if lcs shading - elif lcs_shading_bool and background_colors[kdx] != None: - ax.set_facecolor(background_colors[kdx]) - - # set axis limits - # P.xlim(0, l1+1) - if mirror_y_axis: - P.xlim(0, l2+1) - P.ylim(0, l1+1) # rotate y axis (point upwards) - else: - P.xlim(0, l1+1) - P.ylim(l2+1, 0) # rotate y axis (point downwards) - - ## axis labelling - ################## - - # determine axis positions - if x_label_pos_top: - ax.xaxis.tick_top() - ax.xaxis.set_label_position('top') - x_label_bool = fig_pos <= ncols - x_tick_bool = fig_pos > ncols*(ncols-1) - else: - x_label_bool = fig_pos > ncols*(ncols-1) - x_tick_bool = fig_pos <= ncols - - # settings for y labels on right side - if y_label_pos == 0: # right label - ax.yaxis.tick_right() - ax.yaxis.set_label_position("right") - label_dist = 30 - else: - label_dist = 8 - - # x axis labels dependent on plot position/number - if x_label_bool: # x title and labels on top or bottom - P.xlabel(unicode_name(shorten_name(n1, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=x_label_rotation, verticalalignment=xvalign, horizontalalignment=xhalign, fontweight='bold', labelpad=8) # axis naming - if not x_label_rotation in ["horizontal", "vertical"]: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation="vertical") - else: - P.setp(ax.get_xticklabels(), fontsize=label_size*.9, rotation=x_label_rotation) - elif x_tick_bool and x_label_pos_top: # x ticks on bottom row - ax.xaxis.tick_bottom() # ticks without labels on bottom - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) - elif x_tick_bool: # x ticks on top row - ax.xaxis.tick_top() # # ticks without labels on top - P.setp(ax.get_xticklabels(), fontsize=label_size, rotation=x_label_rotation, visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - elif mirror_y_axis and representation == 1: # upper - ax.xaxis.tick_bottom() - elif not mirror_y_axis and representation == 2: # lower - ax.xaxis.tick_top() - P.setp(ax.get_xticklabels(), visible=False) # inner diagrams without labelling - else: # no x ticks on internal rows - ax.axes.get_xaxis().set_visible(False) - - # y axis labels dependent on plot position/number - if fig_pos % ncols == y_label_pos or (ncols == 1 and nrows == 1): # y title and labels in 1st column - P.ylabel(unicode_name(shorten_name(n2, max_len=title_length, title_clip_pos=title_clip_pos)), fontsize=label_size, rotation=y_label_rotation, verticalalignment=yvalign, horizontalalignment=yhalign, fontweight='bold', labelpad=label_dist) - P.setp(ax.get_yticklabels(), fontsize=label_size*.9) # axis naming - elif fig_pos % ncols == 0: # y ticks in last column - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - elif idx == jdx and representation != 0: - if not mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_left() - elif mirror_y_axis and representation == 1: # upper - ax.yaxis.tick_right() - elif not mirror_y_axis and representation == 2: # lower - ax.yaxis.tick_right() - P.setp(ax.get_yticklabels(), visible=False) # inner diagrams without labelling - else: - ax.axes.get_yaxis().set_visible(False) - - if not verbose: - print seq_counter, "done" - log_txt += str(seq_counter) + " done" - else: - print "\n%d done" % seq_counter - log_txt += "\n%d done" % seq_counter - logprint(log_txt, start=False, printing=False) - - if verbose: - try: - logprint(lcs_text, start=False, printing=True) - except: - pass - - # finalize layout - margins & spacing between plots - P.tick_params(axis='both', which='major', labelsize=label_size*.9) - try: - P.tight_layout(h_pad=.02, w_pad=.02) - except: - logprint("Attention - pylab.tight_layout failed! Please check sequence names and layout settings!") - # gs.tight_layout(fig, h_pad=.02, w_pad=.02) # less overlapping tick labels, but also disturbingly large spacing - if y_label_rotation == "horizontal": - if x_label_pos_top: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, top=0.87) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing, left=0.13, bottom=0.13) # space between rows - def 0.4 - else: - P.subplots_adjust(hspace=spacing, wspace=spacing) # space between rows - def 0.4 - - # save figure and close instance - fig_name = '%s%s_wordsize%i%s.%s' % (prefix, name_graph, wordsize, suffix, filetype) - P.savefig(fig_name) - P.close() - P.cla() - - - # create figure color legend - if lcs_shading: - if lcs_shading_ref == 1: # percentage of shorter sequence - legend_file_name = legend_figure(colors, lcs_shading_num, unit="%", filetype=filetype, prefix=prefix) - elif lcs_shading_ref == 2: # interval sizes - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, bins=color_bins) - else: # relative of maximum lcs - legend_file_name = legend_figure(colors, lcs_shading_num, unit=aa_bp_unit, filetype=filetype, prefix=prefix, max_len=max_lcs) - - if custom_shading: - custom_prefix = "custom-matrix-" + prefix - legend_file_name_custom = legend_figure(colors_2, lcs_shading_num, unit="%", filetype=filetype, prefix=custom_prefix, max_len=custom_max, min_len=custom_min) - - if lcs_shading and custom_shading: - return [fig_name, legend_file_name, legend_file_name_custom] - elif lcs_shading: - return [fig_name, legend_file_name] - elif custom_shading: - return [fig_name, legend_file_name_custom] - else: - return [fig_name] - - -############################### -# Function Call # -############################### - -def main(seq_list, wordsize, modes=[0, 1, 2], prefix=None, plot_size=10, label_size=10, filetype="png", type_nuc=True, convert_wobbles=False, substitution_count=0, rc_option=True, alphabetic_sorting=False, only_vs_first_seq=False, gff=None, multi=True, ncols=1, nrows=1, lcs_shading=True, lcs_shading_num=5, lcs_shading_ref=0, lcs_shading_interval_len=100, lcs_shading_ori=0, gff_color_config_file="", input_user_matrix_file="", user_matrix_print=False, length_scaling=True, title_length=50, title_clip_pos="B", spacing=0.04, max_N_percentage=49, mirror_y_axis=False, verbose=False): - - global t1, line_col_rev - - # check input variables - if convert_wobbles and max_N_percentage > 49: - max_N_percentage = 49 - if type_nuc: - ambiq_res = "N" - else: - ambiq_res = "X" - text = "Provide valid max_N_percentage, kmers with >50%% %ss are ignored\n" % (ambiq_res) - logprint(text, start=False, printing=True) - - if filetype not in ["png", "pdf", "svg"]: - text = "Provide valid file type - png, pdf, or svg - given:%s\n" % filetype - logprint(text, start=False, printing=True) - filetype = "png" - - # read gff color config file if provided - if len(input_gff_files) != 0 and input_gff_files != None: - if gff_color_config_file not in ["", None]: - text = "\n%s\n\nReading GFF color configuration file\n%s\n\n=> %s\n" % (50*"=", 28*"-", gff_color_config_file) - logprint(text, start=False, printing=True) - gff_feat_colors = read_gff_color_config(gff_color_config_file) - else: - gff_feat_colors = {} - if gff_color_config_file not in ["", None]: - text = "Please provide GFF annotation files to use configuration file", gff_color_config_file - logprint(text, start=False, printing=True) - - # if color is set to white, reverse complementary matches are skipped - if not rc_option: - line_col_rev = "white" # reverse matches not calculated - elif not type_nuc: - logprint("Reverse complement deactivated for proteins!") - line_col_rev = "white" # reverse matches not calculated - - mode_text = [] - for item in modes: - mode_text.append(str(item)) - text = "%s\n\nRunning plotting modes %s" % (50*"=", ", ".join(mode_text)) - logprint(text, start=False, printing=True) - - - # create dotplots - ########################################## - - # self dotplots - t1 = time.time() - if 0 in modes: - list_of_png_names = selfdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, multi=multi, ncols=ncols, nrows=nrows, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # paired dotplots - if 1 in modes: - if multi: - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, multi=multi, ncols=ncols, nrows=nrows, length_scaling=length_scaling, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - else: - if not length_scaling: - text = "\nPairwise dotplot with individual output files scaled by sequence length automatically!" - logprint(text, start=False, printing=True) - list_of_png_names = pairdotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, multi=multi, ncols=ncols, nrows=nrows, length_scaling=True, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - # all-against-all dotplot - if 2 in modes: - list_of_png_names = polydotplot(seq_list, wordsize, prefix=prefix, label_size=label_size, title_length=title_length, title_clip_pos=title_clip_pos, plot_size=plot_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=convert_wobbles, substitution_count=substitution_count, alphabetic_sorting=alphabetic_sorting, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, spacing=spacing, gff_files=gff, gff_color_dict=gff_feat_colors, mirror_y_axis=mirror_y_axis, max_N_percentage=max_N_percentage, verbose=verbose) - t1 = time_track(t1) - if list_of_png_names != [] and list_of_png_names != None: - text = "-> Image file(s): %s\n" % ", ".join(list_of_png_names) - else: - text = "No image files were created!\n" - logprint(text, start=False, printing=True) - logprint(50*"=") - - text = "\n" + 50 * "#" + "\n" + 50 * "#" - text += "\n\nThank you for using FlexiDot!\n" - logprint(text, start=False, printing=True) - - -load_modules() - -# testing mode for debugging -trial_mode = True -trial_mode = False - -# parameters = check_input(sys.argv) -parameters = check_input(sys.argv, trial_mode=trial_mode) - -# read out parameters -commandline, auto_fas, input_fasta, output_file_prefix, collage_output, m_col, n_row, filetype, type_nuc, input_gff_files, gff_color_config_file, wordsize, plotting_modes, wobble_conversion, substitution_count, rc_option, alphabetic_sorting, only_vs_first_seq, lcs_shading, lcs_shading_num, lcs_shading_ref, lcs_shading_interval_len, lcs_shading_ori, input_user_matrix_file, user_matrix_print, plot_size, line_width, line_col_for, line_col_rev, x_label_pos_top, label_size, spacing, length_scaling, title_length, title_clip_pos, max_N_percentage, mirror_y_axis, representation, verbose = parameters - -# evtl. overwrite parameters for testing purposes in trial mode -if trial_mode: - input_fasta = ["Inversionen_IDs_v2_test2.fas"] - # input_fasta = ["Inversionen_IDs_v2_test3.fas"] - # input_fasta = ["test-sequences-8.fas"] - # input_gff_files = ["Seq2_annotations.gff3"] - # input_user_matrix_file = "matrix.txt" - # user_matrix_print = True - output_file_prefix = "#Test" - plot_size = 10 - plotting_modes = [0,1,2] - plotting_modes = [2] - plotting_modes = [0] - lcs_shading = False - lcs_shading = True - lcs_shading_ref = 2 - lcs_shading_num = 4 - lcs_shading_ori = 0 - lcs_shading_interval_len = 15 - wordsize = 10 - wordsize = 7 - x_label_pos_top = True - filetype = "pdf" - filetype = "png" - mirror_y_axis = False - mirror_y_axis = True - - output_file_prefix = "#R-upper" - representation = 0 # both - representation = 1 # upper - representation = 2 # lower - - wobble_conversion = False - wobble_conversion = True - - substitution_count = 0 - substitution_count = 1 - - rc_option = True - rc_option = False - label_size = 10 - - verbose = False - verbose = True - -if auto_fas: - path = os.path.dirname(os.path.abspath(__file__)) - files_long = glob.glob(path+"/*.fasta") - files_long.extend(glob.glob(path+"/*.fas")) - files_long.extend(glob.glob(path+"/*.fa")) - files_long.extend(glob.glob(path+"/*.fna")) - input_fasta = [] - for i in files_long: - if not "combined" in i: - filename = i[i.rfind('\\')+1:] - input_fasta.append(filename) - -if trial_mode: - # start logging file - logprint(commandline, start=True, printing=False, prefix=output_file_prefix) - - - -###################### -# FlexiDot Execution # -###################### - -main(input_fasta, wordsize, modes=plotting_modes, prefix=output_file_prefix, plot_size=plot_size, label_size=label_size, filetype=filetype, type_nuc=type_nuc, convert_wobbles=wobble_conversion, substitution_count=substitution_count, rc_option=rc_option, alphabetic_sorting=alphabetic_sorting, only_vs_first_seq=only_vs_first_seq, gff=input_gff_files, multi=collage_output, ncols=m_col, nrows=n_row, lcs_shading=lcs_shading, lcs_shading_num=lcs_shading_num, lcs_shading_ref=lcs_shading_ref, lcs_shading_interval_len=lcs_shading_interval_len, lcs_shading_ori=lcs_shading_ori, gff_color_config_file=gff_color_config_file, input_user_matrix_file=input_user_matrix_file, user_matrix_print=user_matrix_print, length_scaling=length_scaling, title_length=title_length, title_clip_pos=title_clip_pos, spacing=spacing, max_N_percentage=max_N_percentage, mirror_y_axis=mirror_y_axis, verbose=verbose) - - From c1a20186933d9d9e0cb9678370aac8d17626dbe0 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:26:40 +1100 Subject: [PATCH 02/73] create changelog --- CHANGELOG.md | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..94489a1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,150 @@ +# FlexiDot version changes + +![alt text](https://github.com/molbio-dresden/flexidot/blob/master/images/Selfdotplots_banner4.png "FlexiDot self dotplots") + +## Version 1.06 +*14.04.2019* + +* [new parameter cheat sheet v1.06](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.06.pdf) +* [new FlexiDot script v1.06](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.06.py) + + +**[Major bugfixes]:** + +We corrected a few bugs, including a bug introduced in version 1.05, affecting dotplots with substitutions allowed. We reverted (for now) to the pattern matching algorithm of version 1.04. + +
+ +## Version 1.05 +*14.12.2018* + +* [new parameter cheat sheet v1.05](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.05.pdf) +* [new FlexiDot script v1.05](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.05.py) + + +**[Faster run time]:** +We modified word match recognition, speeding up FlexiDot's runtime. + + +**[New feature] New option for pairwise dotplot collages:** +With the new `-O, --only_vs_first_seq` option, it is now possible to limit the output of the pairwise dotplots. Instead of printing all possible pairwise combinations from a multi-fasta-sequence, only the pairwise comparisons against the first sequence are generated, if switched on (`-O y`). We use this feature to compare a new/unknown sequence against a batch of references. + + +**[Changed default wordsize]:** +The default wordsize has been changed from 7 to 10 in order to prevent people from running FlexiDot with small word sizes on large datasets, as this presumably takes a very long time. + + +**[Bugfixes]:** + +We fixed a few bugs with the dotplot shading legends. + +
+ +## Version 1.04 +*29.06.2018* + +* [new parameter cheat sheet v1.04](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.04.pdf) +* [new FlexiDot script v1.04](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.04.py) + + +**[New feature] Graphic formatting options for all-against-all dotplots:** +On request we added two parameters: + +* With `-M/--mirror` it is now possible to mirror the middle diagonal. + + + +Basic commands: +``` +python flexidot.py -i test-seqs.fas -p 2 -M n +python flexidot.py -i test-seqs.fas -p 2 -M y +``` +Command plus aesthetics as shown here (as described in version update 1.03): +``` +python flexidot.py -i test-seqs.fas -p 2 -M n -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 +python flexidot.py -i test-seqs.fas -p 2 -M y -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 +``` +
+ +* The `-R/--representation` parameter allows partial dotplotting, either printing the complete `-R 0`, the top `-R 1`, or the bottom dotplot `-R 2`. + + + +Basic commands: +``` +python flexidot.py -i test-seqs.fas -p 2 -R 0 +python flexidot.py -i test-seqs.fas -p 2 -R 1 +python flexidot.py -i test-seqs.fas -p 2 -R 2 +``` +Command plus aesthetics as shown here (as described in version update 1.03): +``` +python flexidot.py -i test-seqs.fas -p 2 -R 0 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 +python flexidot.py -i test-seqs.fas -p 2 -R 1 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 +python flexidot.py -i test-seqs.fas -p 2 -R 2 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 +``` +
+ +**[Bugfix]:** + +We also fixed a distortion issue in `-p/--plotting_mode 0` (self dotplots). + +
+ +## Version 1.03 +*17.06.2018* + +* [new parameter cheat sheet v1.03](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.03.pdf) +* [new FlexiDot script v1.03](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.03.py) + + +**[New feature] Annotation-based shading also available for all-against-all dotplots:** +Previously only available for self dotplots, we added annotation-based shading to all-against-all dotplots, allowing for many new visualizations. As before, annotation information is provided as general feature file (GFF3). These features are added to the middle diagonal (see our example below). + + + +Basic command: +``` +python flexidot.py -i test-seqs.fas -g example2.gff3 -G gff_color.config -p 2 +``` + +Command plus aesthetics as shown here (+ LCS shading, wordsize 10, change of subplot spacing and line width): +``` +python flexidot.py -i test-seqs.fas -g example2.gff3 -G gff_color.config -p 2 -x y -k 10 -F 0.06 -A 1.5 +``` + +The test files used here are provided: +* [test-seqs.fas](https://github.com/molbio-dresden/flexidot/blob/master/test-data/test-seqs.fas) +* [example2.gff3](https://github.com/molbio-dresden/flexidot/blob/master/test-data/example2.gff3) +* [gff_color.config](https://github.com/molbio-dresden/flexidot/blob/master/test-data/gff_color.config) + +
+ +## Version 1.02 +*09.05.2018* + +* [new parameter cheat sheet v1.02](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.02.pdf) +* [new FlexiDot script v1.02](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.02.py) + +Changed handling of `-T` parameter: The character count of the sequence titles has been limited to `20` by default. This limit can be changed with `-T`. If an `E` (end) is added to the limit, the last characters are chosen instead of the first. + +``` +-T 20 (the first 20 characters) +-T 20E (the last 20 characters) +``` +
+ +## Version 1.01 +*21.04.2018* + + +* [parameter cheat sheet v1.01](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.01.pdf) +* [FlexiDot script v1.01](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.01.py) + +minor bugfixing + +
+ +## Version 1.00 +*21.03.2018* + +first FlexiDot release From 2bc0e633b3bd8ec26878f9986646b598bc0c0687 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:27:26 +1100 Subject: [PATCH 03/73] rm old changelog --- code/README.md | 150 ------------------------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 code/README.md diff --git a/code/README.md b/code/README.md deleted file mode 100644 index 94489a1..0000000 --- a/code/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# FlexiDot version changes - -![alt text](https://github.com/molbio-dresden/flexidot/blob/master/images/Selfdotplots_banner4.png "FlexiDot self dotplots") - -## Version 1.06 -*14.04.2019* - -* [new parameter cheat sheet v1.06](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.06.pdf) -* [new FlexiDot script v1.06](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.06.py) - - -**[Major bugfixes]:** - -We corrected a few bugs, including a bug introduced in version 1.05, affecting dotplots with substitutions allowed. We reverted (for now) to the pattern matching algorithm of version 1.04. - -
- -## Version 1.05 -*14.12.2018* - -* [new parameter cheat sheet v1.05](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.05.pdf) -* [new FlexiDot script v1.05](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.05.py) - - -**[Faster run time]:** -We modified word match recognition, speeding up FlexiDot's runtime. - - -**[New feature] New option for pairwise dotplot collages:** -With the new `-O, --only_vs_first_seq` option, it is now possible to limit the output of the pairwise dotplots. Instead of printing all possible pairwise combinations from a multi-fasta-sequence, only the pairwise comparisons against the first sequence are generated, if switched on (`-O y`). We use this feature to compare a new/unknown sequence against a batch of references. - - -**[Changed default wordsize]:** -The default wordsize has been changed from 7 to 10 in order to prevent people from running FlexiDot with small word sizes on large datasets, as this presumably takes a very long time. - - -**[Bugfixes]:** - -We fixed a few bugs with the dotplot shading legends. - -
- -## Version 1.04 -*29.06.2018* - -* [new parameter cheat sheet v1.04](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.04.pdf) -* [new FlexiDot script v1.04](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.04.py) - - -**[New feature] Graphic formatting options for all-against-all dotplots:** -On request we added two parameters: - -* With `-M/--mirror` it is now possible to mirror the middle diagonal. - - - -Basic commands: -``` -python flexidot.py -i test-seqs.fas -p 2 -M n -python flexidot.py -i test-seqs.fas -p 2 -M y -``` -Command plus aesthetics as shown here (as described in version update 1.03): -``` -python flexidot.py -i test-seqs.fas -p 2 -M n -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 -python flexidot.py -i test-seqs.fas -p 2 -M y -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 -``` -
- -* The `-R/--representation` parameter allows partial dotplotting, either printing the complete `-R 0`, the top `-R 1`, or the bottom dotplot `-R 2`. - - - -Basic commands: -``` -python flexidot.py -i test-seqs.fas -p 2 -R 0 -python flexidot.py -i test-seqs.fas -p 2 -R 1 -python flexidot.py -i test-seqs.fas -p 2 -R 2 -``` -Command plus aesthetics as shown here (as described in version update 1.03): -``` -python flexidot.py -i test-seqs.fas -p 2 -R 0 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 -python flexidot.py -i test-seqs.fas -p 2 -R 1 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 -python flexidot.py -i test-seqs.fas -p 2 -R 2 -g example2.gff3 -G gff_color.config -x y -k 10 -F 0.06 -A 1.5 -``` -
- -**[Bugfix]:** - -We also fixed a distortion issue in `-p/--plotting_mode 0` (self dotplots). - -
- -## Version 1.03 -*17.06.2018* - -* [new parameter cheat sheet v1.03](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.03.pdf) -* [new FlexiDot script v1.03](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.03.py) - - -**[New feature] Annotation-based shading also available for all-against-all dotplots:** -Previously only available for self dotplots, we added annotation-based shading to all-against-all dotplots, allowing for many new visualizations. As before, annotation information is provided as general feature file (GFF3). These features are added to the middle diagonal (see our example below). - - - -Basic command: -``` -python flexidot.py -i test-seqs.fas -g example2.gff3 -G gff_color.config -p 2 -``` - -Command plus aesthetics as shown here (+ LCS shading, wordsize 10, change of subplot spacing and line width): -``` -python flexidot.py -i test-seqs.fas -g example2.gff3 -G gff_color.config -p 2 -x y -k 10 -F 0.06 -A 1.5 -``` - -The test files used here are provided: -* [test-seqs.fas](https://github.com/molbio-dresden/flexidot/blob/master/test-data/test-seqs.fas) -* [example2.gff3](https://github.com/molbio-dresden/flexidot/blob/master/test-data/example2.gff3) -* [gff_color.config](https://github.com/molbio-dresden/flexidot/blob/master/test-data/gff_color.config) - -
- -## Version 1.02 -*09.05.2018* - -* [new parameter cheat sheet v1.02](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.02.pdf) -* [new FlexiDot script v1.02](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.02.py) - -Changed handling of `-T` parameter: The character count of the sequence titles has been limited to `20` by default. This limit can be changed with `-T`. If an `E` (end) is added to the limit, the last characters are chosen instead of the first. - -``` --T 20 (the first 20 characters) --T 20E (the last 20 characters) -``` -
- -## Version 1.01 -*21.04.2018* - - -* [parameter cheat sheet v1.01](https://github.com/molbio-dresden/flexidot/blob/master/documentation/usage_v1.01.pdf) -* [FlexiDot script v1.01](https://github.com/molbio-dresden/flexidot/blob/master/code/flexidot_v1.01.py) - -minor bugfixing - -
- -## Version 1.00 -*21.03.2018* - -first FlexiDot release From 443c19d64c3ce125a8fe0e991fb2d023b97707a7 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:27:43 +1100 Subject: [PATCH 04/73] init env yaml --- environment.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..e69de29 From d4950b409554740e7c067362965f02a29f0e572a Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:28:02 +1100 Subject: [PATCH 05/73] init pyproject toml --- pyproject.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e69de29 From 53d4595d9c31084366fc62b9386f0a804ebb9736 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:28:34 +1100 Subject: [PATCH 06/73] update image paths --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9c2b99f..106fabb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FlexiDot: Highly customizable, ambiguity-aware dotplots for visual sequence analyses -![alt text](https://github.com/molbio-dresden/flexidot/blob/master/images/Selfdotplots_banner4.png "FlexiDot self dotplots") +![alt text](https://github.com/molbio-dresden/flexidot/blob/master/docs/images/Selfdotplots_banner4.png "FlexiDot self dotplots") FlexiDot is a cross-platform dotplot suite generating high quality self, pairwise and all-against-all visualizations. To improve dotplot suitability for comparison of consensus and error-prone sequences, FlexiDot harbors routines for strict and relaxed handling of mismatches and ambiguous residues. The custom shading modules facilitate dotplot interpretation and motif identification by adding information on sequence annotations and sequence similarities to the images. Combined with collage-like outputs, FlexiDot supports simultaneous visual screening of a large sequence sets, allowing dotplot use for routine screening. @@ -14,7 +14,7 @@ If you use FlexiDot in your research, please cite us: ## FlexiDot versions and updates - + *We are currently working on a new version, including the long requested Python3 support. Please stay tuned.* @@ -105,7 +105,7 @@ with `-p/--plotting_mode 0` In **self** dotplot mode, each sequence is compared with itself. The resulting dotplots can be combined to form a **collage** [default] or written to separate files. -![alt text](https://github.com/molbio-dresden/flexidot/blob/master/images/Selfdotplots_banner.png "FlexiDot self dotplots") +![alt text](https://github.com/molbio-dresden/flexidot/blob/master/docs/images/Selfdotplots_banner.png "FlexiDot self dotplots") ``` python flexidot.py -i test-seqs.fas -p 0 -D y -f 1 -k 10 -w y -r y -x n -m 6 -P 15 -g example.gff3 -G gff_color.config @@ -118,7 +118,7 @@ with `-p/--plotting_mode 1` For **pairwise** dotplots, the collage output is recommended for larger numbers of sequences. The collage output of the 15 pairwise dotplots for the test sequences is shown below. By default, dotplot images are in square format (panel A). This maximizes the visibility of matches, if the compared sequences differ drastically in length. To enable scaling according to the respective sequence lengths, the FlexiDot scaling feature is callable via option `-L/--length_scaling` (panel B). If scaling is enabled, a red line indicates the end of the shorter sequence in the collage output. - + ``` Panel A$ python flexidot.py -i test-seqs.fas -p 1 -D y -f 0 -k 10 -w y -r y -m 5 -c y -L n @@ -132,7 +132,7 @@ with `-p/--plotting_mode 2` In **all-against-all** mode, FlexiDot compares each pair from a set of input sequences. To enable the identification of long shared subsequences at a glance, FlexiDot offers similarity shading (switched on/off via option `-x/--lcs_shading`) based on the LCS length in all-against-all comparisons (see below). - + ``` python flexidot.py -i test-seqs.fas -p 2 -D y -f 0 -t y -k 10 -w y -r y -x y -y 0 @@ -151,7 +151,7 @@ Secondly, a defined number of **mismatches** within the window can be allowed wi Lastly, both mismatch and ambiguity handling can be combined for the analysis. - + ``` Panel tl$ python flexidot.py -i Seq4.fas,Seq1.fas -p 1 -D n -f 0 -c n -k 10 -w n -r y -x n @@ -171,7 +171,7 @@ In FlexiDot self dotplots, annotated sequence regions can be highlighted by **sh If you wish to find out more on the gff3 file format used here, Ensembl provides a [good overview](https://www.ensembl.org/info/website/upload/gff3.html). - + ``` python flexidot.py -i Seq2.fas -p 0 -D y -f 0 -k 10 -w y -r y -x n -m 12 -P 5 -g example.gff3 -G gff_color.config @@ -181,7 +181,7 @@ python flexidot.py -i Seq2.fas -p 0 -D y -f 0 -k 10 -w y -r y -x n -m 12 -P 5 -g Previously only available for self dotplots, we added annotation-based shading to all-against-all dotplots, allowing for many new visualizations. As before, annotation information is provided as general feature file (GFF3). These features are added to the middle diagonal (see our example below). - + Basic command: ``` @@ -210,7 +210,7 @@ FlexiDot similarity shading is highly customizable with the following parameters Shading examples based on sequence orientation (forward, panel A; reverse, panel B; both, panel C) are shown: -![alt text](https://github.com/molbio-dresden/flexidot/blob/master/images/all_against_all_shaded_orientation2.png "FlexiDot shaded dotplots") +![alt text](https://github.com/molbio-dresden/flexidot/blob/master/docs/images/all_against_all_shaded_orientation2.png "FlexiDot shaded dotplots") ``` Panel A$ python flexidot.py -i test-seqs.fas -p 2 -D y -f 0 -t y -k 10 -w n -r y -x y -y 0 -z 0 @@ -224,7 +224,7 @@ When comparing related sequences, multiple sequence alignments are frequently ap In the example, LCS and matrix shading are combined to visualize the relationships between different members of a repeat family. - + ``` python flexidot.py -i Beetle.fas -p 2 -x y -k 10 -S 1 -r n -u custom_matrix.txt -U y From 6d903544e2e6f81cc7581922acb59982d36b06a8 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Thu, 12 Dec 2024 17:29:11 +1100 Subject: [PATCH 07/73] mv docs --- ...ble_ambiguity_aware_dotplots__preprint.pdf | Bin 0 -> 91531 bytes docs/SupplementaryData.pdf | Bin 0 -> 5716138 bytes docs/tutorial_add_annotation.md | 33 ++++++++++++++++++ docs/usage_v1.06.pdf | Bin 0 -> 30541 bytes 4 files changed, 33 insertions(+) create mode 100644 docs/FlexiDot__Highly_customizable_ambiguity_aware_dotplots__preprint.pdf create mode 100644 docs/SupplementaryData.pdf create mode 100644 docs/tutorial_add_annotation.md create mode 100644 docs/usage_v1.06.pdf diff --git a/docs/FlexiDot__Highly_customizable_ambiguity_aware_dotplots__preprint.pdf b/docs/FlexiDot__Highly_customizable_ambiguity_aware_dotplots__preprint.pdf new file mode 100644 index 0000000000000000000000000000000000000000..855327218e086207736e734a9fd459593c2e3898 GIT binary patch literal 91531 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 CJ>h=< literal 0 HcmV?d00001 diff --git a/docs/SupplementaryData.pdf b/docs/SupplementaryData.pdf new file mode 100644 index 0000000000000000000000000000000000000000..611b0817f2671d99582dc86f4a7157609ad8c457 GIT binary patch literal 5716138 zcmd3P2RzmP_kT!MD4}6pA=`C_YbGm^k-f>fM)oEY8Ih5d9T{0gHW?Ywup%R)6iQ`N zQHlQVd#@Y!lRo+O>Gyd2dpv~eJtG31F|kCtfS|yi)xe4-_LdyxNM4zfoFK5etFxP#tA;ZY zspjC|3W6c{(bp)MINQkETQ~sELH&p{2Lab1#esk1q)2;n^gB>b!+9C{C17a>H+$eg zaIk{4xr;sshI$?df_^m!vhkZR`a>xCT_7mtZ%_fuhl1#DFwBQAIOeyF4~5Xb!7=y2 zF_*wG&xB*Hgkzop$J_wN+#rDBiyBA(bEN?05&;DIL(G6d2+YF}m`e~C0tGSm31U7J z#JoWe^JqcLB|;clgwR(Sg0}Jpb=Ob)0g!M;0{lV04y=ZBad2}sL%INbmUXaqMSX*S z(DZ?2p&;~|z_Kt9`dwgII0%BK0W2#3f})28mPLRN=+DH(!79!UX6i^+eJ~&vU=5_F zD_GtRz+DRSO&aq}9z`Tr(%#;|6+j1q86gD23kYUt5DeF#n7=_WqlaQf55-&w!wegS zxdeu}1cqTOY~vCPgW#A;;Fu5Lm=6Uow+mpHE`WKo0Osui8<=6PM4)ds1c8;1=GG=s z4xah|SNTB*K%fMLp}^V;Dm;WsS9ukW*%U`aRKpf#S!&jERM3)wysELu&k|#D^dn&=3tHlDM>$UBH_^ea&+$iPqVFA6OTBtOiP?AU+)JK$%MZs^m)fVb zm0&3sb)F*|j4syOCC`3cnNDf&2%Ao~plt8Ft!poM*7ut4+N9g^r_T0+&m6oS2)Apx zyL?|FeB%7!#1r#q&Q?|f!v>#dOUi}C+4h4C%AY^3%uK!npY<_I3wm429%pRunQ~3! zah-md!)Se1>2$h8gU6kQi?MH>o#;SN4!!Q}x2V-sB#4lCaO#69^vdbhHZCgdH|%!ag06Q46lI%fg?lx#YbF`Ak5X0LJ~aP+VWPWU zz!IwUvc{N@a=!j>wf1YaTT?;?O@!R%SMD9D(dN)vlryk4()UX-OyZs_eA&_IN9UtR zqZoMgjbdDoFIW2>CT^KK&pMHw=2qDurRJsKwx^Me?T`2h$eA9vs~VO;m#&>oD-!|! zxBprkPB*Nkp?GSF`=&Ya%;ogUa{}`ay|cW^AJ;A{jK)|qgxc&0>T{Y`iocToXc5$& ztcj!#IBhn*RvFP17mXyvIl8&r)IR4 z&MtHDnGW-+5)XBGXZt()JN>tKVz^Sx?I08A#4VxQRXHcmmqOFqDh1PhO4Aj*-$bl=+00u% z$fgbHS*p9lNZjHJ*`sC(GJr8lMfEIe9}3~GKWOUnwqmKNaz5bMfs1GEWHXesQOx={ zmtPbUbG~FNikxlwHW9Hfazp=AU*Uc?leTNeeNQmmoS>iR81T#Vj&z}JT_B)TwW#bV zdS8@r;RItnch0RVjdEv#NENLNOY3sK_H$g3QPJQt(VD)+$9^%1vcJ;Q;lzta9^h|X zQ!gXDJo{_skXlz0I{O1wI_d>kmuEjg-1-~o*@Y(QtC@xfQ4nMR2iy?(Srs z{Ceun>Wgc@z%D$MS#kE0y7f3c?9`1qyF1?zmSb+Pp1WH1vLm@Ys)i8V%XS~V-?Q9) z6R~0!KTTM5Yq_-Y`^-V`*>Ce@w!M6xI|~$3ilAh|1X0TXeyT>CFMF3Dz)<%K-_}uf$Vz(-zqu_UygiNxdg!?DCoWVav@t!vX9MY4R@x z%bOXp7vc-LXKT)Tlyx33x;|=SagxnE>?LKJL4&58$|?DYV|$)_$~|Y0>{L1}al^3o z{plfr{*iu_JClkuT%KKp&9v#peksC~K6~#Q9U0t0?*L+NY?N=DiQKToLu;L$)%A zeWtxlt9aP(_{%f59-9u(=MM9x9h9~vHgeVqak`#1rx~TNI9K0t4tAhbunZr-o7}lvUC?FRk?D&_ui?_pqHSt*C!noF?E> zM*Tg?*YTP5^3tTg%3TR{LW@iom?M9dFq!w2eej|`Rj@c0QOa)WDp&_w>kV}oqX!`w zH%jDA2}ZEF30EdgAH-`%5XN53 zI-|_1cBT4uDeR)dgVue?%=xF}@M`W{_M1{>nDT#}MbJSCli{G}IWJMmlY1lNcEGNN zJD0zX#NoH@JtL76s^}d`V_}gQ$9{ZSF(+>!`>vtLSq%zAdw~?4Y)#%Fp3HnQikZGx zX=W42sQ9-}M_H4p9K{aq(##?hVv(QNpQiA%ylcX*2Tju&cZ)7kLZ0yL+y;yr&rCOItc8uab5+RFk1VM%5@H_aj#hKv#IO<2fbZW8Tcm zr`3!il2Y>s<8EfxpMlTla4Vb??}iCy%9ibRyWk{j6@P=vZ|P{O$~9Kuv>_MPYfo3i z9=Z@j-t@^ zZ-OmWGvuV|-X}drRhvwzRQz`LeKG!Wpn#e~qx@XoT(7(aWf|)u^(*5V$F*US-RH$_ zx}+UQHR`|3cR)W-E9gp%aScVZdlKkyqKoOVIH|_S@N&s8t&3758lyHdFzc8sx0KVb z8)Jc)zUREBWE-=;Q*LjGqh9)ia;}*PQ$8bDxTwo)3~m@od^JqA=@`pIo-3=OTJa-! zyAekXKgDwj{fQNedAs^V!ldgYNn5loJ$F4l6f5N%)VMd=rmJn=yJG#0+&AW44XKtY z%oiH`PSz=XW3k^HbtTXTsnMt1pSK-M>t+9DkO#_!Xk67hjVHD)&>AdjRGk zGUQu2Z$J@xam53>?Q|Z#x{PCu#ibP1eQDuAmX0cu4dNYb;+5AUiRa<4^PAMqKWS8KgBd*N=0_g;FF7iS`v$_1H*GG0`6pZq#= z=emmw)owk{J!69WHfM9+>fz}n4a{-Y-UjvEMiNfCs|x4oDj>=C(a0Q)OmcmCABNG+*9snkgz^%DHsLD8SOx zVcjlSAgZ%AXifY1i!Uu&wS3awsf%nre0st3P7|uvx#vrI+FUkMuvQr-p9#$oQ0FUw zp%vO%HCeL38#gpUWY`v|1nfL6e~K$-lg)XeJ>*k6+BCoV#-x^+9j{JAiTXBe=maVF zBn&1t&&;-ORzma0iKgs|`v>CUX$e>(9m5*z=If}DnWav`r}2^)(rF(PCPgZ%?Lm!D zw{2JXSrC6#;aSR{$hNfRY-EOj`18G&p}S9$MrV8yFYf773<;geNP3tmZShg20U_(I zQ)H6cv*eV}bKyO`ls2Q=Xa3q0sW(A4N#4qjHroYRtcA-QbyOdO$( z-AmUa9iR3|2b|;``}oO*#PYaJ;~o7ArQu65{2IP&;GUpd1?^)L1Y(BPEb4eqU9^4J zCOz8PWOMkqi9acDhbW$GV`dBI5$HY7RQU2eN7t$GEX}iZA`baUc{6fQ7vmAXeePFl z4!kP+SbWZXL}J09t^{o498h3QXZu+|UfOut_! zhka#;Q*6HD@xmy;u!>~N`fU^5jky$S!d%}$nD^AdS8*D4%&mB6>V%!PgnT zF0Umg!Zwa9Dos>dgGKZWA6FjZ5SNFpA^LdzxLH-tyq;2U%41qaUJ)?em_D z@FwK(s1}V|HSN79bh09g5PDDVU`hEtQ138R?NL2FVhNTjHr-q$dN8*0I@wv_1O81W z=9-k8tc2cQZH(ef`Hq@dx;`B%}ke=!Y7+LZ>>GSzT{B!Ns@nQ}s zoeBL`M>PdU3hzf+-7PCl(G{J`zEpgvm!^-w`_Yx%6|H(uA+Qhnj`jjN=JjwYi{f*mXryTc(gl7}YkA-upps2d5vgsV54)spG(L^#j zxnd5}O34LIq7JXTEk4JZJk89U0&m~YVXql=?x@1Yc6Wzd~wib_`yNgBkOv+ z*l&xP+L`4qYg0v1r)F+PXwWJtUR&nYd#V=^%=Tiygz4Sg$-wl=x0Mute|oIJ@b4aCUYoV{3}{OV>_ z4i2`a4h}ZpllDLa%i7)&tZi*CY42kF<9F1ZGDsIQXKP1S2WJpEb_bTVc6M=0*Y8qzVfmfPfz6XI_Y~01)$bltWruTDgJ*;lNeymT3Bb0395NWqV?N zfxsaU^vks|SHk)E!8#jvK?V4Ma3Io73%CV+jTAcU$O}OTfKV?0g4sYc6L>ZdeMB(| zhzTO0sL`X@v~~2TfaXu5{}pD?P0Zwww(dw*YcmrbDF<8g9mBK{5#AOi2o$;nCv>EB zE2XGgf1-3doc=-DZ=m59*n|cO;ok}kRA>t{g2J27KoP*7P<-8hM(`idC?ZWzG7Eyi zuqX*`MG3eY#0$I&1m)-F2MI!jaD(!wsPaz({S_pzO-MAX?T{|O>`_K~0RO8w*qPYx z7#DyTRNiI#`2z*Uu{nXz5N-tqeeX{MZ->lw6#hFrLc)-(@F2FpgAj&ofrlTm6&?tD zD?I<4Iaqk$P%J$BTj3Evpy5FP@BottcnvN*=zIirxLD<;Xl-u;q)N<~>LG*8M7#|b@OhzenAn>n^3f_ZdL2%U2fIBcLC$NsL9&j^A5S8@-F5MdL z2SIQcj><`3DL|`KR9fqQNZV|at-mZOI`VwHAzs;H;&^LhpPTB;Dz~SUyOMnT2gwPqLEvc~01eg#=2r~v*VURF7TLjoY zeh>th{peW-h!li>!!Z3xicAQ@Sz%0$OBj=@0;~Xv-NG1l3uAy0#sDLX&Rp$;`9hd1 z;NM|B42Hw}ty!zz`U~()MTa%SfTcBcw%)mpLo?(TIt~JbY|(LO_TbQpx)*24;rbtF z{ej~pY!LVF>P--^<3Ah&lu_T}7y!Tha0~!#vZ>x6f^gWssW*&cAOOYDY5Xw2&;P3G;W#|_$r<<` zRsHsAW`|np&ujpMf5)mF!jD!k-&G70@*}SPK3&o}0FM(?V~-_)p4KHcE#8!-5h9 zK=ER{Ajn3s3ZRHLK1Z9S9pMZ4tN04xaCfUGv<w*6E5a3=uj`u_s{CgwlH zU>ggvDGxXxqfH0k5kXL1(I1YzxFf7Fg>ruhYXnZQ z8oDeHhtnuu`d0xCfuH?;2YVL>TN9TZJO0@D_xq?5MpxtLMHDN4B6hpr26k0~rS3PL zKj6b}@%#n1dj7zlHa&k}8L$}=1T6dxJb!_$o4xi5>AadU#_h3JCKF2xF%rW@7GaDnd|;&woxuVH^rj;`)0E0P6!2`auDjo?lUb zQF>Un8-l99{U-$w92rI5fFv8Lc>Z2%C7CiIWB!A7|;WTV0u zvQgLy!Bn;ac zV8#@m|0S3Oae2HoaIy`|7;OR-7Uly2f>@Z*pzjc7R2ltGFykx)(N+-)^RGz&dIEY@gb| zi8G*Yz>@GMux^9fHfnlv8g0J<111$VP1a*gNnY9ZuSja9~m`e`&%A;iyscA_9xduZ087zr`zqqDAGG!hu#K z|1KOja~{2Z!IJPN!tp~Awx9e!dK{HJ$1K49VV{8<3j~vA`9mqdq-A!5q7cTD`8!D$ zVO$iq#(Dla3jx;PvJJ`)0n(`G<>#NV4a$$1G*C>Wcqi-wvg#-Z|I#E7##K)!Wd4E# zZo?>HfKg~g@V_zYw?-I>3CI5}cHl(J(5;OBg&jN5)<5$K2-HC_y_o+SudpqJ(2dwE z6x%WBa8mHyj=*y4W* zfza7YRLKt3mHES|u+hE@-RO*kZp09wm_Q^5iuSYBfbSS51-jAF4BhByhGL=&J7YZB zkNIni$JInA^00!s&0N8R6ClDud_WikJ6F&%XJ>N-v0btlfQKM`NIGz>@Hvrt3Cxh!ww|^)aBRQ1ym?9JmJDMAXeY z5+bz8`CG;v@(%=2DLO3sel0`-LVQA-2%_=)C4v}{-#`$KV$x3t;>14D8vtNQ_!9)N zC~aBFVl~9}VaGqwx*J;&Krwp}>AOS<>6M5#A2ipHYMX(Gb+vAEOAvgg1bPpf%9W&_p}$e+^9>*h-AQk zq&7krj@vJS&evc^{_D+|fa)%^L+P@)Ia8vN!-&wn;CZ|yCgP7r85BRHsHsp}#aNVt z(Q-t6r2cqg(D`U%d>s|J?e{h3cS1CFdgZCd{8+e?J!GYi!=#l?RORSV!ve%Fq+USe zB-Y2dd=#p;54YAjkxSVy@aCiUqCtO9e)88|c2{5TfFdS7aE=GH>XlFl%CL5_Dx%TAmhZ+dp%~C&L?DoU)t|W9=#KG zMCzOShZWwVy?0Kog~ybpeXBlGpD0vny5>jS6iAXC)H0)KWIXK*PJVx#__@+Citrx) zF@J^Ai2@&P+dm~yNbXm^rtGPfkS{lBMR9SUfA7+jRSZs{Y%gz>SqW~T{x=j*%+EFHqLsKODgk9bL@#@cu$C)UMQ97C(qLL@N9}GbXsQ5f!m7>&0PowCS?!RpD z_F6i`U-ky=kyNYNK&bbv6Ji72a&Qf#P0;Xx$jWl!%)`Qc-+J&4SXq48B`c!sMQ_aX znWi)=xH#)U&8~X|6c%LfX2~>a=kD>)sdp;a6dFv74h_7>k!CO;dAa_g+NJE7b1(DA zrxAvS?aj9AhlCbq_yV&mG#@0IH&NKzFfgT5f}V2`=zSHLH;W;!m{lF45SY8SuRVCs z#IkZuHA6Oeg_iM}{OybPSxhq*N7qQNveQQh;D6;7OPky!HctAYX~J+lLD^O91!51) ztpa-$ykZTPk7|7%kjPbS^GmG${T4|NQm@vYU`$TYjW;>!o3RpE$IK(FsQ&fIR}wWF z?a;KDW0NL+gHHKOpn(J9<{>&47)vIk`X{RHw+%e6dpKXcR&43}HFyw4H@;8VAjA6* z{#D9h_fl8#Jl$+DNn_n6<1eOBBL#c~Y>i|evX=I=RnWoc)U|r{$5=A+!JG6LpGc~3 zP?AE3bk4rJ?iZ@^W~j0iVsC0Bg|sFbN-Pgic`+n8aPUa5;=%_I9Zi7{0W^buR=V;# zsqLZCd!L9OeYKE|I)0L5g3)C90d;eC7ieaZ}Rr9lKjeN&{iFNPU5icQvI0^)5e}RWFPLpHtx| zcRJwyXdJsQzwkJol}#f|Hk)L@u4^}mHX94D}Wu8_d8eH&%6fx94r??;IXS_poLJ6f!< zxNpDo#91$)t3j{?{GZ~ElU(};%s?$a3{%hlXQ49~CX2SCX^ctL{xvvp5|ro?o&Unk zZB*XoQuD_Y!^Ro6mfc}h=$|qWy?h5czBlwEb{}C(B5p^F#Uwxf8e?(Q6Keg7W$ZTd z1Y>j&z&9Y6u@R&}pk~d^CJF+T=m0LoF?%qakUF|t>h~1D1cBE29~7YJ`6UTxy@s8u z5LAroClYX#1?mPY3IA!X?oc!1#Ea1S7|6rI{!3E`Y@-PTw$TIv+eqNTFbP~>{J#wb z!Z11Eza?h^I3ZSa5geAq+sGLvcY}Zk@Bs~xSUE#u{wtI*<#Sk+x7EnFF%@+E9hQW@ zj`Git0vTu+rWpSZRR<;q3Or;ZHnp*pIBcWRAGT2n4#Sj!?+9W{8vn0BjH61>b&Y?8 zznf|pt9-Zmum2l2e=qi!3i`jr69mqp0xhKfg(utCtsmBE(;?hs7}lu$btYj-ivCWm zA_ReB+|dQFSSI~CbS40Sp|$pA=&V!B=IV%S%uDjmCl**L790n`;+GmF=edjPXR0LPt(tOQmRhKI?Nf}tM+|(^g)b< ztVc=$SSPOeA`g`=EAr^Vv$ghv)*nN@e|y+|kjb>T&ph#BT()KUL_sV0)tAjHd)6ITzAZ9}^DYI>l4P}eJ@l+{KjURa_vwAd#M-Htc@&2&R9iav;B`^*)|4j* ziQ?rS-1e8;eL6{wZahaApP`2Q_Q9j;t`FAwSw1%1jKFA}fUQZD%8nUlC9c0cQ(wmB=RqNcdAds%jaVSukZdmM!M%#fd za*^r>W5roYuiR&OugA#CPsT48weZ}W9ai{eLr!Ps^ir6m<4mrbl6jy0^Y`p@)Mxe9 z_YP~ni~jsbZe{$~^zwD>#0+)P#o!C*}~_t#O+5Tw_-6-J@&z z;KWn}-&2D9LTNNNzMXUc!}i@fVLa{hX_+**jo{9!Mw9o=GM5mm-Z3f3hXe@)K9Ep= zW9l3;lr!U3{fHawiVd4{3`fs2arqQC_U7S9=_x6{>y?x$`)uZ8V3pw`)z7L*u<9h8 zsS_8>qer_ib}ylz=6HrwoPkZFJ!$RJhZ^y0Vjd;6Nv@*#l%!`4@GDZpvDE~eQoDSl zW1U)Vk{}=S(fEmEhox*Fm+S~Ea4NLcL2J7AVUSi&ri=6=qP6R<7hqTJu8-26uMLhn zV{-9xf`3W_B)qj%I95?Z@VW5Hy~_bw$YYB}h&BbBm@-amdoc~)^a+9G$&mcGoehznlfHh*ES zE{R-^_i{diw^HPIi@5hQi99HRnEwV7S;brIFRg%0A?mPdIQ3K01>1w#JAFtDOgs^`@G+yq@9m*;9nH60ikAm3^EDpjJK{^B z_@w;YV6XI;SODC=vxUo2UVPA7GfU_aUXa|Zbp+RaT}B7)xJ6!HZplxNayy^k!rYastcA;{Sat%Z+Iy<2Ml42VNp4)>_yPi_xmbj#AhM7c5 zf*Jg87-v<9h&taIk3NQ+NDbnd%(H$Ud6F^USbws7im@|^vOKwkxN*TVI*DPc1NflE z9CK5~WT{CKI@|ZoscWSi`2PXGE-I2rDuVvc7ALY$|pSJuR7cf-0+~ZqoI_jRi@E z$AQmt?Obd>$=E%WJ3a6st#+5(!*uKO%u+9A>Z8Y1&V#HER4VP_98594_wXV|q-wxf zL6da$*r!2t4qr)6_Sq-6IiADQ9X95(C>A>DO>>-?@A%zYt^d!wSW5AD0hD-=FX?o&6t&dW;1*|IiHT4Sab z_Lk-qGKnpdzW>!J&2l=v!@CteOD?F^M{&s-ygrs4kTI{sN7 zCAIhA5^L_D_6^v;xzp1LPQIf|eC{Q5yT_K857}nR>(;#L zc)dOsZ+vY2J`w!(zISx=I<{&z`zm~r$A&s~y?#h|t!pBEp{_8()mlKL!ndiQb;mkUcY%ldltbn zMtHjVxX-YRTWda?lUh29fH?%eNpf|svOcL8*Pf9J1`R$XU&1J*<^~05t}zlF)r@}~ zkCe8F_yW`EzJIV>zQ>|TNbl~sAf{x5uuqcgbA36YTD;KoieyM{>`j5WTOaw8GOkui zNIKuVbu?&QC3c9dNl>@Gv6Oexrserj@g&mHkQ0gFgzVJkt@^)fOfOnJFXkG!DB4{0 zka2x94&LeM%Nq9m8^UXD;!dl}M~kk}oHJL`ykdAxrhUwf`Yr}<%A;{UTgtZlxj@v8 zLL+_9vn@yDX%AVqvXj^)VKrY4jlkwtO@#ZP!#c%WL}5Vz9t9#oW{=%gh60Oh@U<%r zCLqs73Y5H!CA_ZjFvCG!f*n(S}9(@YWT6f z_l=sNoYQsJCEB7WAI#BZej#1L7t^_?8iZJ*Q(uc7$lw$4(LI#k`{W&bD(CIqJ!cNt za_&tZSZ)92q%<_n{`oy0I;Pm-e>! zi?u$z9-zx_@fx44<*-819^#u6*9t+GrRr%~CAgavt73~s@uwX=bnRhnxn?E))|fL) z>@n2icI1_snCIv2__`vi4hXP}m1L{>-Q*Ryc9cr{%-7xL)_DOI(4piqV@vM%$Dcrh9DZCZsLZm=xUv$o9H~~@ zZ(?iy`94f3fd&Den<1!aXz*FI3o(A8JvUBucj5a5W*TlX9yM3)3}4P;p*Q(jl3{`& zYD?H zoL5 zIZ&9+COjJ=Ls}uSElZ|j&JFNCXX&OMk+MMAz9*!D z=(^{X&$arlU#jFP_U1hZFD!~7_!Qm^9-bVNDJ7Ph+fCzlkHM{0b<$t*hRrvtYJy$$ zGk1-L=P#K}HL?==Mfp&35}OsDXKr%S_l-)G%?4de6#KkxCl#p!8Pq)qu6hhkX$~z=om>3$Yki6uTW zxuI{)C%o}&saANL45vCtKV+|;mJ=lJVEIJRe6h06>`Msed6Cx-b&5+G2_6%$BCOqy zUx}UIuoo{}DAp~R?pPBU#J}2;#v-K0?{e~ruc5Vuh(_|`eCeD1#heEbrj+EGDvu)Q zKixgx9>s`H3imECPkd?4|rRGYV&z`?{bLAHV4ZE4O|Q(rG~vbwGIss>W1 zO0STschg+wCiV9{!z6pV2i+_#GRz!(++Vg^KJ1*V?lAo|D(P#|Zat=3Yq2rd%C(|2y~BWce906X?Uyp<5-_j~oQQ zjaA|ZymH)v+vc;5L^I?2qVz5lo?vXulpz5h~4O=e0Z-*n^h&$`YNj8*ASH@Igv8d zl;SiRARFmWPaNEB_HO2jVOOe)m~7IsHy!CcZ*1`*?`5_Rr77z;8q-UC$rn!@A)bei z5tg~FN$`e_>DvpB>ZZtAC_Z}p;gyp*kyb|gqNw?(gvQfuJm1IT$@X~~w z=ZbgFo-N8rJ=3Q=Ys8x$HJ30lwx)Uf^Nh`Bz4u*jWWdY4m&oeyE+u>&DO{Vo-0aH| z;-b(x!#!BvxCDJ4xt=GT^}xF_`hxbU@A%OwOfH;xxm|mxVlt`d#-C@ru#s)%RTYxB z26T3Zi}Go0iwt*#r%rDKcwL`4H5_X}?w{`*vFYet?a_w^8w420y4_h4en!JX zWeaX=8f3XT!`Yim=P9}Czga>fnBM%Z=5PWs7+KD2p z%|LA>_B;%3J$PSqDGE&QX3Y1(!A55?;_s>q5uYR3>jpYIVk>SJJWn^Qv(#sT!*R_Ei)Mw4d+~*@pIO%PDIz1V~HVC_Vh)JovAJ5*^r-|aK}qE`+_Upd6X zc?}|y&iQKJhihM8ulblblZ^4{9xJ51j(TaHNOM=@m{;V=`C7av{5BiC#3$X&YSFwt z!L*aGitbB35l23hcjM(%I9?p>$?!nV*(nu`*H1Rm#J8sV-&zSK0L!ZIMUxxV)+Z9w zaewp^*7vAInwB)*u_K)1>N1UO)B1cZ!c3{fUuyJAcSB`+AX)4U-#d8Yhd-%w=30;S z=Zi5&LL8iveHv89YOPwy1cqnfr|)S$lPKlCbg8~H*3ak8?WX%48p~ha88jR=pLyt$ zzi`iDkGX=dXzJ`?((ejJ@7}%i_gJGBwTbQjxZyb_J-q$K+Q5GC zKlateq{p|GOrpEMeH+$R4c1m)ugH~~ip@By6$ z5Mc6C8s6JERDVwqf62|n{Yoi9SQ7y8C%&i4WXPnKE&cJT?9N6~& zNBXKEEvBS)loYRX$o$>4D=xB?yLmifYlLJP-<|kw}=|Gc(-gkLBCj9$k_}5zD zEnBd`|8Mt}M-KseKm)cUW0RNY{lb6Zz^``86aH5*{ohpAMu*e(staeoY|KHwTXh%u z-k+%6A#NBl{~SC)!7YW}P{GYYZ>WI4j?Qh^JQx6X6K$X=3SH=pKDPnbPY<`y8{JHX z9W!?RZ8;bK*aw3TD1L(hn`8qF1Cq6TKnflPg91BW{jA9(u?GZ8nz>p#*aOG^YO2Yj zzBsI0T^(ISz+gu&M{7PuFIOuEdp-weOZ4wxdpA2rFHY3)$++qcC-;c%cH04={OC=& zV5nVuQPn~yvi};Pb|$Wlwhpeq4({7fi<1OI_vHPaS}4%k4Qzveq82Cz0opbGCRL%Zd=;{tq9=c4S=Zv7^b2r|7)}&Oy9nTlbD+^LPs~98j^37yWg$B_ zQUbB{m>m9%o+s31_vkZnaQqldbto>+wiXor80fDY8Q|=zAE2X`H=DBeo0GsGn2x<) zw5e@fa$)w2!6gz^QMDs9Fhu=37{a(%;2iIPbGQ_I%kdsi1Ol^x?=Q~%!gix=9xjDB z>}cZ%FyL+!GQT=p3UlHDc7(sKsevKjXdR4QJA0;f8caKk$Wnc`Z~RVhLhYoI%UK`l zAUs{`KBGG)^`@OUQpaz|l`Bca8-zM1e_yv)W_Fh6lh;2zrxKX+$ZLM4!$JF&QDoK0 z3xa2Hvw|!mpr}*4F1uGu_bFApp1;qp zTx2Wi7Xcr9D&d&Y4?VqDBBHMt5H&EkTnpUecswGY{7gg!8#B3;c`q$xQfz9BpIVBK z`1m@bteb1oYv1UJ7x;5^J%l9c7lmu&*7m+M&k6%_Oz@P7oWbW2i5ar{Z_(Tvpq!h` z8hlCP@&&T!akzWELJ<1QGvH{M+L+_o-2BVOD3;CuL|NPd#X5hhx|r?WFZOt8_RMOb-!dFb0kN+ zW-cxWo)J2k;>A@5mt)~pr$o?akA6K^q(Q|a7t2+6u(|wl-&qM|!^s3MET$e_o3=J2k({C;~WWHoG;W)Ym z4NfqS31xJb!BEQiZ zKjN8RTwq`h2uue_NqC8w+eZAPP+E|h>Nk+%wDDQ0U zejRX1%;ketL>;HvgBpT}RCEMOV^;6lI^9|;Y8)tOWy~GUGWRB9^Srhek^VuDPV?KE zXn6YGp$lP3cxx|xx%ET7lgPV~DdI7h=e74gv)VoDKjc2|+%20oo{&XIFkyVgYhNy6 zmBAG;_V&ZIvIMmw5?Q5*1FJztgd8dCUgjC-aGjucHr+4d)VX|wmEW=P8eu9vPx;B} zJ|~`@=Q(Sy1Ywht*7w9&$!8UgbQ$lm%@8bQJAS;d^wfK4|BT(2Ergy5F)}UowuN@= z4#^gfw$`>N6K5g(7+TXpnbl~$OM^z%ZfPi>O`YfzHFMV;O5~~Waq|O5Us+7i9#&+! z?07^bRB5)He(xQUfx}4416^>&uauV$FYUMRU{z(*n5re$ySD~cbF;0Dd%|Mav`C^-)^7A0RoSt(l?7{eAAO_#8;Zc69}PCs6`dx zMsqu;hortp*-b2ca;YTz5OzP%;M~i-qR*apQOjKtnxZ(be*&MC*tnt%u@8?#N3~z* zMHan0wL{NTaPM`;aH44WGrGx@UtIQ>G`~_YursEzWn7G9lvp!e;eV)oEQ&fF|7uFb zl+S630Y&M$@6~7TA5wdHttT#^tfi9Ou}A9bGwbUwb}gMiB4%fpxs_sVXp?#eZ@%Vw z8^t{0qg}M({E9asvx;C zrEohVH_g21;Fm%lyS-*m~9Ihsr&?(VtARBnNT{={(+S9Ri^uMv6d z2`LO0kOs-1k?fhKw zDEp0QeU(IhTxGOQ#|(Jas{Y}i?(cLuaRq{n4d zSGUz=RyO~izUDNpf_jN?Wf}}yPia9u(GO^x_VV!dfC?4c!X$Qk?P3_c!-RpmFX#~ zJtWS(Zv1?MtXw7=CKJl-9Zc-}mNK0C zs$JGg^V(L#MKxk&<8aGX7T>!C4|T10Ma62h~q*>Q z_3$^LyndQs`?Y@FLr;0iWi{<;%PQT7tcp2SMLGJ9f^3rVhV^%;QIqF|Dli7wm$Bbv z<7nvONM$guW~M(HUpWZ5;j%A>S;lO4A4w_z1c<_>;VO z_Z=Me&52@xmAtx4;d<$Vi=0H`hgRN?&?idzrz<`j&J2axkxJi~b6wPiPaM!cm2ttT zrrV0b?Cs*GC!pl-|>+EWt z)YGGk=?f;fQ7n1?0x!jad-bYJ;&T~2>ec8`QuWmHx#3^G=*^~48I3@1&Ad!J&UMZ~ z_i*8p>W-I;mDj`$;(IymE`|=PSHZzg_-TWN873_BBgO^Y7Y0lYr9Fp;A{|QvWhbWY zU;g>?a~Vbr#8C<{{9q!2xldeiT$C^p^`Sa_j z*K$P0EiqXM*V!v0)cnu7_S@BJ&aQMf<+>9+10T4huJ3VRnk^=N_t&NW$J$#!*Rd&z{q8+?{4w6QN3T|^OX^Z} zcP%ZcG^c5hjFL5bNe@#EfSz5r6KQ~InMD*VKf8O_H~FSY1msrm!zK4zAcLE&oDZ#q zQw}H%s^B^whyitmp;2X{SCqxUmr@H0C3aIF7Di}}reaRf>KOysH>;jn91wj?C;2(ie-a7JB*zt0> z%~KW;h1l)UMGT>=FSD-kB7{~VVHGe+EQR^S6rridG2NRykc(ZMpVWFoxY^}a6~?lm zrP^mHhBRUIEwM;>3>*iW*370Ct2c77XQ_U;=9&wp_|jPX9&qp?rhQ)or?EL*dPN;@ z0O(7@YsB3LkPFW7K}2Y5%A5OZL>#=7g4e(K2+;I)LDb`%0OuaN+kEK8Y3Q0m3)Zx1 zf$gW!GHw@gL+@JLlMfbccCoIa(zh2rI4ej?{{SMTNTLU%9k(2eizC$PiM|PjB%%{sxt~~?h^~~ zVm8?$(;9yf?^|yiD;8pUCs(1%gemA6sm43=kbEZMt639IzjJL8CGTL@a-|vG%}a!` z@y8dFbr`n=z3qAe$&Ytv@vjQhc%LKTu7!`!s-zTgncW)PjMS(;o-Jpn-Wx7{XL(^Y zvCWu4&`jmqVCE7|WkIwicecM8gv)=tZr4CK* zXyiw0HEKCUc}JstfDdxC0{EburXJv-$XPMRQ^4yYXF!X7(dT3HiSsiGd83xX8A0fl zF8FEcp`sj#T5TqKz5r(EJ9t#{k3O5v#Vwdni_n$yk}A4|#~}UU`y!`Ena6Xag3+9( za8GzK=N46p{H>xw8(G0 z0y{!>+%aWcLE~Z=8yiLE2_yx|-%5DYe`+7fW{t?1@HRPp-j)JIW#VxwWRe&N5SJBELKVBXj8;1 z{dYSlmfu4=fB%~NdnWb|Rp8&hM*kkw`L{817ADSL17&P%Y;=Hik*omz55Sl?8wX&~ z;eT9+{TCqbU(4%Re~rom#@7BZ9M1BO6=;8G^Zr)iKmR0VVrKb0jQ)2cJaS4jHRE^N z(ERAje+c6Rf6MTY&f%sl))HsVTKuH=#G^Vb46XDL3KFg${rTZ3$2feTA4<1_N|7Y) z{h4M<6Nikwv($^yO!ofA`+liq)Jj8+xP0X$AYdrHi*e z`i5UePZrN@gmc>G|X83c2kPyiV*Dv9ayRt%3x8$ zCsUCp9)o;|8itZa`qZDaqCAm#4$0{`0g|9q@RgPdc4zn4#!@0ng^~E%sMc~WwJ2nK z<*m1?KKrai&hS}bX6&$LSB%*Z^oQ-KFX4E^w>tGYJp)Nv0~iCkiDzDF(20#H2$ar; zz?VDN85+W0 z!D@!NHn(AMvQegAoxnhg9PwI%+Y*j$uzGhv>dnTZ%xJ^{w()wuSuEjhlrZLh{XPzy z9$r^5=@-2_gAu77JB$?KIvwq{u@09&b!J-+x>M2piCpxJlaKvk#I~MOOZ{H+34 zuO@N#vv@koAX9FTKCOn6&V@i-8q?~R*AW%VN^^nb6Ou+87^S73Jn-4aLxLttE6|MxHYg6Zc)w<`q;t}s?kEFsMO-bAtbe)ydyW53&(ACeA zZ!*w!BifzuX<;aYynyrAgDWSuar1m#9tv*iA^_pg+*eYk+6BGN-5yrQ|5SHN#f_1T^=OiDcK}u$hG4J?e!`yRctYBJIVmsP>Y7=&Ehy^;(j+-^ zmpwJ%TYJk^o?H0gO7t8nhJM4dAa+l4YFpzdu9U3r8@E`wJi)DZY3Ur>z_`(wE+Kth zWP4+~Sd|0R;tr#l&LM9?nw(8EkJ(9#r|N^KG*@NlTB^`w+T!1zAD0mhK9aMR#y|p5 zqfbo1>QiF5d#gm1cCK$~$2gNY`7aq~h_v`1iR_ePEYN==ZPl)r$1QgV-m2TP7CyU+ zcsQlW-0uMkzOX+XI39mc9aB2qgo2C`v|YAk?;0Dyk?YML^2!WO-tfiIo>Qi?A^Vhsr{M(MT^bqOYD>_HA8(!9%uO69( zth39k$89S_)9S20+!4&dquR4@3C>R>LZ;bTP{q!9uRX+G=)}wCecC00N+U=tdib3#T`vpl3{I2uULL6iV zV(uNoU2hMN4aTbn8*q; zH#C`Y_ap~)@x^KF?!yNWZs{k5L@a1}I3K)EERaMg;L>6!d6 z56j^N4qM9jLz^xbMs;oKK39hs&I8e)e-y!;UpHX#aA#cfbc=k%(ULeZMDWg&9;Xyv zlk>qbhfLLo*h~5uzEp6HS%9&|8iyWx7>HT5%7 z8cEgsw5=~yuL5?Iu}Li^)(jt3ni!b=O#sVSdKk3vLKA@cC`!-Aky%Qg#l~Dd_muQUw%V`Sp{A}ESkDLv*moX-+biPKR=4vjPt#GI4`}sjn zaa5S1s0~{=X~KnI_Q7^s!N54l>x3~=dc7~lp;;j z_}Z=wnOrp$y*E4B-E;^+J&#zy(V|2yeGycvG3z$sVy7E%f&OmwW#^jPqFbS?IA$31 zI`W|~#L<$wcE>pkwpoIzjGYT_^I$8@gaW3VXONvY*SM3c2^BWhm4y(zTs{0Nqs9Xc zDI%C!^CxU|^bivpkCF~n7x>`T0M-*gJwk? zO96aC9NIg|QEW)Qxn{mnuu<8FD0u!L?HJV4P!a+!g1@6kK`)-dJhDe*OMxRnvGIjE z@Ym)(i2t9ve!o!~|L%4DPhCHDMmoUUKRW{}9RPoT6+rV1h?Zh!VxwdDkI+tkXdV7u z`Tv_s^S^cd{t*fBhh+JGQQ|N30S*>A00aUm{vDs|_zfeSa;rbAG<)JI{|~BnU+m8M8pV%`Mc8rih8HjQPwm>~&)#`- zE$iRv+}A8v!u|Nl;m>AdrEzW6N==q~E>7~!>iyp+v1hL~6uYZ4pp~h>LeAQ*bWhu% z_jyR^q(LvD&bux)zL9Brxn_1&&sNT_)4u8`dT1goAzozA&Cds&@!q;G`%NW*UDr^AQ7cdSNT8zr2v z(d zM3yK=1s{~q;LvB899OdNj;`dO#?lURrHKsQm{>(HMrWy;C7Iv{&R*+S@Ng zWanYeoyjtaiDek*`RzfIT|P@&>e*#!i@D)<6!l{!%BeOtz(ukIN{FkT0S>O3;5=op z6XinKg0k5z<`zchTVsLK?N+az2w8Qb$}25eDn)7?IxW!xUEo;6=Ptye3oLK3$ekfA zM}t*mOAPLvTjdia6j<#MsLl+m9RnHPjU_J&4RtQ)*7{Q)rww($lk%>pR((G%l4Ntc za?DY!9;;N1qN>uAS2$WeS=SnQRwpuStOmIED}y7!7%kjNZO@ZvceqOX+qqEhrgxl* z<`vW)w77j&vL1M}2SYNHNOdKJjdX!^cZqgAIlkv;jpIg?i#QcM=QSxk_-Tf(KhlGN znWaUPV!>5zx$0Q{kzX9qk+>dBZAGmx?jvPZM3z}vU`{I?n#;Pi7ihhmS|B|{B2fwu zE*M3jX1u(WRoOL(6@l6@)`AoRa^1&znfU=Gzxj-%i@w0q?oxJp3?&iN&D{7k=3OkquoA~C zT|%JDq3~P#DF-FuiKLgA(&dzt{D-5APAi_Yy9HpnmXH_3@^$CAuO6)?mmckbSGxkO z=je_qH+ZTWh+RSp>CI=KDmbEQ3|4}b6!(&kquHJbUho=ngtvY^KLPx%a?ofY>7?U( zQfEF6g|oPV>V>`k%gX`(+w&V^9Ohn`Kh2K!%hq+`xB2Rmehxmr7hvR<_}=d?2S@h3 z&p);R8RND3eox6PO~sL3X{Bk^COmelQr_#vF1E7PS)q3q-ka~a*{?Ug?~m?V{=B}g z=~rv5KYzS0t+n#|ZS}qn@*n+tVNhY>BR&lGU#H!j-ul71@xYI)7B&j`^XRp9$^Iw( z`$#Xw$9Bfp=|{#Rr`YGBZ{*HPt7f-7Y_?ds%=rFKd#`U7SF_9bJ@h4TcHe*Y-s=L3 zPZvCL@=5Kb@w~d_`+1*!%HEW3kt^$Y!C&Xky!$DO-b?a>sO{|G&BL4;@2O)QY&wakHJ2yAMu={rDi|u1KpR0C{_t|1~6Mu@s!NbXQRVQz3Mf=&2{m*AH zcg+q*o|7L#v36N%FO8S-?(w2@g=^iHxLaM%m*M$#(6o=FpR;!-j3XZg6W)y0jauL+ z%`)Q%!kc`=D2t!{^^JuY(-dgM+;QS@2E&izEEo7Xr>gCsv%(23zu{9(L_Z9s3?h@AthutK1#Pn|@xJPTm<2F2!K6N}ZQFvsUw06l&T7GFI<{2Tw zpHwc`G*KV?>BSxheBcz&glj9U`WaZ*hS#zZ8*4nT&GgxO*SY5k2fOR*`2(pPySu0k z4dMi1YiRByxy#VHhwtBwRW5#9u5%R{za2{Cc&-UjG#hUz~Wo8&PgFmCmc}GY% zPrYr=-1G)Hj&?hAVA_ZVtS<*(lyzcPwJ zOV54H%kGFZvKUy%CMxPfuGw_m0_EA!+E~jS3c;THYugz|wgXEiUx%cVZzI%^X(QaQ zrHAqKUak&@JD}K`B;^f-AGB7ECDpa}YA8O_y&&BhBxt^FR>S;Qu2n4 zSSvqR46WZ2XfXmLQ2~-_MjiLXBQ7VB2scB?cmscJgC*p=rP8xf27t!V9;jT(B8<1k zjNp~B^WHFYS6&#ZwH_D}>UwBMpyQ$KjN_s8|7l#P!_M#QUpAQxo$#I=OJQ>d*yO)o z^Opg5Xa>@GX@dR~huIkz6apyy`+;b4d3|$hVoDo}FPn|pd?3-9?OdIWTCf$RCe#(7 zMuf+yZh9S#6F%MX^gEKA){l_bWbbBM;Miqy=l%7;8sC;Ki6387+=owla#;F7x}&|ay_kDrI0>8rd#_o^1Ui-JcDEPPUCv zS8EP*(2kd;{qat^q*mG`(sPk9{N$0Hr2{6rE2`MiF z;9f1#x;Ihst>m>WFCIljhtmP<=*S4LqY=Q4MufU;06WqF>_`W&BRb$Ai3Zs5cQ(L| z(X!rjFy1<HV?F~XIO(2E3I{R#>MKIuo$oxTaVC?28DmvHqa{5p6K;ObT2&;L^* z`Ua5wtB%lLGN&KDJ%`!|f6|W;F29Rb%;)NzQ2+>vwF0u23#YEsmZlnfqx{m{-DTc$ zdl_ySau%|4LmIu*xpSI3svX#hqyKCJ>4Phfbm5JNI`HPgZII@|9gya0gLXVN9AS1I zgM(w3#|{(i?D)BXS)F(}fR8Nme7ROf2(0;%!Pbu-asnYdRAZgII^^%tqVuv$62j(k^gJMyf(t?UAZz z^|m5{s*9=+x)$2Ov=%zQK^Z|oZwMs zRU6S*ThT96lck(`HuoVHH%$kdn-$t$E9HAl^^QtUGqs%OgJr7QD>KVoJ7Q&QWzZ3v zCD7%aL+JUZ5Ow@=iQC=;!X0iz!d)npu`%5C48^x(?bSeL5LcO>=DhJd(l@`k&{eKp zS0_-lt4XBtP!FKSp$dlTiz@7>5vtr)Bv5gxdRN8Dmux?IFh5FE+sYGbbsq_y7qGJ$ zBUG+N@CsGzD-){TmP4w4R{=xi1C*5yRTwQ3szC8>8d?4RjmzaKSGU?(Zk;!V_rCPQ zZ20E482&0NVi2fuC0HcF0E*%b>c`aP>a*4-y{8QR!y=#NlOIj{wH5KL?eCiueg&+B zx@QRnt|w!!j_A-?YtLZoNz0K6AH ziKY!sl}F#V)bG~gdDmb0ACeJfq~1#Nd}aLZY8_JJZ}#Z+&|pPqlFQ?N{rd zV;~<=+}&KoScHpZ#r5J%!x-QWu2hcVuTEru0<)o#Y|7mYe}uXI=xW*(*mk&eV(=c= z!=wQBT4Z&&FenY>6VV8ktQv3?7`E_u&Y>?9H+od5!<8bG$$A=wryHr8W%~;n5e9-J_4Kzt@D+>+@&PxdRRe{;_|Bb;fN)|h)C;r3{&PXKLX z7)n9$2%Eo`cg`eFtE><54TdtsYHhBB8WwKq9lMXAteHV|fwlFd63*LZR>^%~y_uu5 zWW5<>w^pN=M;&V#q;`>tT~%hZ!Y?Vf$)8V)#l2+?BO1aW=CVk#)(coW{finzHnB?W z^<+b}(am8s)vS9;eiToOS@k}#>%ZhC+h)9Jo)#eyT85Dfty9KgSqC$oyshALwXNr- z>?<^rfAnx!OKew37M9q>UK?1`Ahw8=>u4wQM>b_mK%hvT=$b`-fACVq2lW<^Oj5{2#1159utb*@Ka*gd0%9Y~%u%3n6eU?$W09C_QcMTM zpz>Z5$D9jTqiU)ZDcv?siJMcFP+}BG9K>idMaO@Jz+#)O)F}=e`4!)$Si}}uTYq!e z$9JiURhiTH^h=6#b`FP%_il#9iK7_bAh6xMJ$&2J;-pV!eyRAyAsfBsX`w= zb$lwrK{ZV-n^dF@_UKXx*65mQO<_&0O1umeH*++o9L;;{>)3uIHk-7PK{Lt%ZFC&V zG?YdM+ljHH9zHEi(-t#9gUJE;f~83%Hiq%pK+Uk33yTAK3YtZljux{EQ^xA7RJ+qp znj{WpvsZCTpA=T=G}GX-OG4--h^8zNGWCtRmrIagUbr!2SIQ2qOat$s54g=%?J3PJ zPDl*29k$cKjk9vRp%PEbJ8q9a<6=SLd_1gV^j=sNG%z4>VEy_lL6Wx`*J)wF;34=? zR0Abj;}1CE4GA0=zH<_TCVT$~Nnsm*w@V~&khq<{X$`r}++@W3T{5y+jw0{OHd~Y^ zP`q%9+LqveogbGR$%EwOhD?+y*H26e`*6h(aB_ftU@7s|@w-8y#CGEGhYhU78+#>4 zBt1P>iF<2%*J8$t%-2~0!R>_W!3Wp;uFWd62r#?|Kak2z*=HYn=agTi$P29mJcFp? zhc4a(kQK627SEt8*2h3pGJ|f{*;UrQGd(z)fB01M4lB}|rGAy>6Xns?9s`CK>8D&9 zn6g#Vyb1bCNe62JTH#CXU=fgbkzU0`fx`G!3=)9GZ&B*Fmf~T83BN9Nl!h%K&e_K( zL2@l|&pdLugE1}ftUcL%02U}bVBfI7d9e3oV@m8JprM%E5OZB=SvbE^H!LVOo&m#!O=dPjAFsxCPoc{LM+!wt2M+avdqs&6S|S^WwmuK0(C}rGov54Z&2f62U_24NJF(oVtNF0<;s7nPDM3|3AX=+I6W@!O^lSrhFE5}kMRB%XO@R3UCarfV$ zRg+R7B5QCzlG1Anq*Sv=eB_|;dHigMcR>BSI;0FhbsE2`D`C)9NNHqI07ys~vE?T; zt5Be|dFIajlFX#XQ&+LAAIJ4Z0v3|eSn1{2Yc4gm%7LG)w-4ds9yP_v)o zr(vq?tbTcq5UFjifQ0HSDA#KR z2_6Rl3Y!nF!R5P$1k?uJ;emb1{VpVl5aT!=$rGY-q>-%$fdFDS9xcm2%inWH{S( z)6Ygj6xc`+sU=<`6iUmavrGh-u~5u%lnDx>s}@D}4K6aQe78`tQVL?@+4^0}hg(7jyakMe7 zK9nw&)_TfG#dg$N+E}{1sfDC}u|f228Ex&`t|MsML%6AnKnQ z+~()Byi;OP+%UP6mPteGHfZJj7HM=kVQp9Wza6!@C|^wk^_g41^uH)jVoUO1b0rm^ z3O&uQ>uqw*TE^Ar-p|WcTY6L1&aWJoLHgV)LoNbbFVCzd7t%Xq0Ze0rGuxZjmLA=; z`y{c2C{hVi_kbxCa~_;RA}*Na%Ow4Nn~K@0pNu3~dxx+0s3{dZ82pF!2`u;{I`9u5 z;lUUB!D_4Ys(3F4q zREp}AV=6ggDikOll~L=(#DwLl2{Z{EH8LC$x=LnvQYzO0$|b*^0*(el4st5gNHDx8 zrgT~g6DrgwKvD^iEdG;p0wnkUB$EJ166)Uq4ZmdnBDlKnQA5iun~iG9L~(2d}sDZi7cIA$*;!LN{ZMqFI4T!Zce;DB(^@v~XcbXyNKM%1I?* zj;4`-N=heL=M=+cB-&syBs9Uc4NFqlV~(nAf=E)^Q|B}-qe|2)rzXhMTf<47N@I>L zrh`bHNz+&Dt@rurQdob3Zj~rXk}&N_x;E%x+X3#$x?ZkvOAIOIgdk|g0VT}D5jNF% z(8x{$so2h0e(L*M}W8sO1UM$naHW1!JdM>q+1TgYYx zeDzyc%j}{QXpcz5`tpSycm~i154iZ_mpF(5#J7sNJn<;gDVA`m<&k~Q1i@iB4`s&} zfO;2FS9 zd!gP$mc3Ji9#jSTVmSgcLBIF9XwY8svmiNtHHOpvUR@^oND~!E(W58ka!8{+_h$ie zPD7s$+Y7a2N~uCOM1B%Sd^K#!*>@)#HTX8!ks=X)3HkWWL*Jt&x{_Nx@EFd$i}UnZ zU+d30OGk28C&C0)tYa6MP--3uJ-op-Z(P558-|^rSD1+1DW}6Pu`^y9xis19`Kag z+{IY#70S+ovp2>lKZiP@0?DrWo_uzH3%N*jhH6otBK@A>c9!U57MR1HuN)g7=-qX5 zpzlSXk_G`79a)lLPac+c(1yikX`) zkqW55%)!j3tj0|9zk~?2@Bl(Q8Uz=1Wd+g5o-C9a@y`WkpIE4&VtJ2tssfO)G?Z0= zCUoGK^gItjqdCK8Wvd-AP|zXa5G5*`jIe+OQb$&3`Ri*{E4o%s1PBS~RYdzH>Pi78 zLQkW$H|=cU5NXV$z)tK)2S6-at1=F;GRF4K)5-uiaP1%WvM$BpM)-E9?TNPEP>C#w z4Q4PS!_x(Se0fwWCxT`(E5Z`~92JmBxX1XSJ( zT}STaOHANF$3c4CA-KDc5>ulTz5oHT&-L{9pzKRIZ6YrM;2@dG9CCdLW%q&AQ<*Xc z@qDMN9guCT6AXZxzTRbj;C$0m9f#ShCrb`Gm;x>oQ%XQ=AY*unRsLl@OI+-uaagcX z=vxqTU{t`}AY=;Qo+EG4a>rj&`sT+fa~QxDimI=n0kel@3_@ZgWBX%n%YP{iJ8?TL zg`HC zsvKaXME(qG1L*m5xgIzK+W0d^hF`aJ&Z1Xv|Jys3P3C2psk=tm8(ko(Lp^8^Ex^>7 z%5_JAe4_BBo>4e5cTFJ~hnfV{iu(qDOHX#H@u8nls!n_zyc#O<=ECQp{V(dmr-v)?FK?3N+ zTlG7ZDQy_5m6wgQ$|#1Kra&xX2GFF#1xg+o!+nWnR7xRPWb>4D%4J0RzI?O*JUp2v z(ZL(TnPDG>g1iunNDqF!367;ZzzRE#kF|Fml7Yza>6ma>xt52MxG-%_kt5RJupKT- zKS--P3+Oqc4)!BenryYq16kZ`?w!9$eaK&bS?QHytf9Ot#8|pNYs)A-iOLp?u?B+; zcheV>ta2mUN4y&EB!eD`OY8OC=8=!;x+TwRG>#My2p`vb5rL4Dp7h9XR)n>QdiJ6h zK*aP;)?waXt-j1cv}dLq3&Q8RfZ;J25CXGE&9iAY`?^3Xp|8m!Rb+C-pz67g3o^P% zKq3mHbnfG%d)+q^pwH8#GcYpWa?dt7X9?Q8s+nVbSI6*`v?$ht`8!X>lkp&!hedIb zB|!*KO}l7Jv@ z0@7GV)=gpCa&&=1v3Wcf$lNvs>O@S2;ydXLgRJuf5%P4vq~~498y6MrIB4EhCl;%E zCAfoj@_X$wsB4z(B%dRl-=+rn5;KvUns8&1sMloMK~fU#;N7xgcUvs|$-8B|DJ-*U zXE7e*ouDY}UibC$mJeV$C*ib{^;XozNb5p!-RqPWBdv4kNBpMB*va;aOD2@gC_#d> z-RR=3i`>0)2`=>EYAx|WY!R9xl2xUTHA&`S2=gkMEEQ5c6b4a6 zlmZd5Nw;EX#+oh}1=M&qpG`tRP$woZI?J-)q=3F{Dhm9(*fcFphp>YC#qrqC0 z_8zH8u`Ws#O@axIKESEa({X~W%kOZAD)OVeII7TyXcC<`!&Ha?REc*4dG2txY*6-d zO+PKw(|R>pJ60uMNayuU$SuHpM3T2YcjFw~)oAOR^$&~NN@wHRcA~sUa_dNZ2I-Ro zxVF$R|DO-zy}$?CW- z|LdOo@2DK?tPFo)^SJW0-mhD!QTEbzWvjB|BfH@pMxOT7})XBaj(Hp)y^i zgu~3n@kuQ`X#UaZWKB7~*^}(;!;-pvmYLhnrVR34z3vT`NwbIP%C)W5=t_L^_y@1W zZ`ltIuD6dHvbT?q50#ZqG=?;jCQh%{FR0Q?-$#%V$(2e$;%oGJ$CAX~?k>*Md*yCV z_>NG#!Gd!8MVN!0lZeemsiywgq8x26~4NB8;D&nCSX zT}gJKl0lJBm5S90RlY_Q7b1{gUP-t;^kC$JvW&5js>|^Es}{F=Z@!l2(3GVcHGG%n z?cPszzxD;pdChoUVa+Ufq=LpECQW&yYAo3OGLy& z%Ts;3+2oL>mJ>mqu9;<85y1GwKuC$5m4pXJI_t7(!o=n8VB}?f#6;WfBL9IfBSmEY z9jVNG*^IhQjq(FW?_EwGQzF{MO|Kjq$Hu|FU5ma9eY7N#_Qt~Zof`%Ad1*B{zr2PD zcgi#Zb5Sj-^j(`+u%k_7AY)EvdCdjy{q`)@C;8lYky54Eoqc1%2Bzv1^%D8U2OrH^ zd|&h69WzlAKJPH{*mBc+gs`t_4Kn=+w^Z^wh0!Q9a0;VRndfM=`n*1@0>Ye!RDBv9 zEm`Ugrl!;78+^1tWekZtl@ifFdu3RMf%T(%DW;EQzKy|WdR145v%T`|KY$P2?-)=Y z?_MqWfQts9%C{0d9SDL+%&+}zB{pccqn}~niSo9eo`J(>$?ufA*^bz!v7L`SA)GKD zUw;h>AaG!U z=d0K*fKau-DQ;E?e~BVA+O7baf^W{>Ntr6HEI#c-H|ZY9xK;~H61_~QwT6zNrYhZ3txWNbi)Vv`y0Y9 zBCs^&u*eJEgeg#}q*aV%LdHxr6t8H6)Vf1v5=|?q!A;m)70&j~gaUxwL1{=rgf}xY z2)jp<$5}>NI?MB~K8E!d6fBwMSU!!;`0~+Om;eL6Leab{iJAF)g(8R|Ku|%e`*PL_ z+MsJa;#*!6f#X($-`e;ZYf3PE9M^qgCgnLs?S5WjPZQpd?=aHy z$^u@S{L)DERgJ4`oo~UyNr#cCos`n8-E;8+v$3)c8p=9*<%P`$n$!0%qn6za)PU5i z_7!{dCxg)kqbo!~sQ_|(_fVulk}q&eSgjV@EL#-v?+RPVIPGCGkC2!XD$XppG~iDf z9T0D@k(a1`7m^INuxc1(Ce!zLN%F9Ix#upI@Lx)9nX`=IyI(OA>-2(9 zZnulTli8n=A&IdJ#tUx^jOWL{6cEX`G^ujE`B0_2r2}0ttA%w(EH(f};eK2O^V_u{Awo3DSv)hP0n>kIgqM> z#a+G-xoZ>|nbL$3Q$^PEi&_6rU4w0--uHTyn@5icp&7=#JnlDPee5^oGx?GqH!c>! z$=kFnz{?Gj1tnSj8}{&LrRf!#fFFWz+}0Y6+_^NYYvu+GtRWIEc0w^6U{WZo7b z1iz?GO6D(kQ1GXXlh_QRXYdwmlFm7b=;GMt5P*Gx)6V4!2Yc4H#RI`9o`!@$=iVU{vcR2Ez^rNT(f#u`Z%BJXCE<3?am+af2|^9Ny*w{V z;136S3HJ(eaZtLCh1R%IpEyAWE4Y>_5~Z0-fc;>XJn}t*x(<{8%x%ai7f$mzNfGl6 z;4yl(hbfTT#b?cM+317uT=B^tG4jq1bJ4sH`&DNOl5mY{DfIIp8C4>U(?+--E0M(# z?lR10(C{3>3Z+|-u2ofB5(`0>4PprKu~vb$I_Q^mcz zY&~IsQx!}+B;#6&m8Vg(3x1QU^H3G4-vi1s=vk;lUSfh!o``_VG?e<#{kX&b$IWCc ztuR9)LF3|qWWs1dQR2snhILaUJrBI$j=pcB1x^!U5@K5A3#gD(Doa|bJ1=;KYSb>Y zb(>8ovZSj#G@0h4@_`OQ&uTfVcf(qyh=T{aWq{=aIJMz#p^ZX>o>j}{oSXj6;8+?3 z1aS-dXx3B$Nv#mmo>bK7S&&o`BI?Aq4dP6!y9p<=AlN85Yq68mg0gvJ$FBYX4P(j8 zH#`ReC8yGGq4}!J=I1>t1%dfq6j%o%P;&ydU#r|6HVQP4tFR7y3n8BkN~aIkzDH}W z8-ce`O?*9ied%S>W*2rbD(A0dYXUi|uABLSArE8r1^iY+eyz`#%H*D4)T-s@Q#qXl z+h!=)wOg0)qlSUc;#k|4Aa1}nU#IxKLn6-N16BdKVC9D!-Bia#>Mt|VI}NA2 z@d~3&C`FK!1UrG+%MAfeug87x3dA~za933i7uK)@eT!i* zodb_pQspj~`2J=f$*f$EUKy<1`V>@SRmjHRgP2crcq;lQiuy4T`Wlg(k|)sKpcQKg zmuh(6zQFvr7Q+-H?M~TS%DAO)IJfQR7TcOy2WS5U8@I;LESx;_(*48@FS2ZB>gJ$b+CETv~hub%zgWp zqR2z@4Lb?eV9_lNDZNo!j+2N@tu8O;omfos1~BSmVdtc`ofxWyXS^cpdtmfo#E8+> z7whugjUTg00YxanN?-Ph8O89}V*0&|U{UQY4M0iJu2i*6x{4HFO$_z>q@=o1x6E0G zMX5$agZ%9}C1hqar$tW%zn^V?G!@^bxhrHJ);o@b{jrK)Qg_~pl<0gM=g3QfBDKvc zZ&!9*B1)AL_~8VMmn0k$`2x!0>?Yx;o&m36g!Gb-wHNvHts;_iT~JC9VFj`KfY6>U z$(|mmcZ(yr*2tssCn;6qqGf4B_yVeQUAIc1AE*f3`Z>0{2H{4k+Y8;ggtQD zBnH~ivknq+9;#nfg>;dya(zJ82tsUY^)F@bB_w!w}rK@QZ; zItt1A@95s_vaD!rG=b=(ISTlyJJ$T#8KP2h0I0@)@z0h zZF2Ok=4Y7!@7MFKvt5#x-AaqPO>|A#m^D z}%T#+5+q<&qJL(e!OS40?gKZ5D$unR>6O*v}WHW@?yL+ z+8?c7EM^EJkCtsZAC^x5&-BnmVr08KI-b+vakw08E@%}eH^1f$K=NQK2zb08_Xc~x z4RZP7{ilKe`hF6*t06B0AWH>+ER|$mh6GqOXt)7uGX6f@(UQQa!~vKC0+<6KFb8qB zW5YiR^a=&vyL_;X&%UxX`}tHC9VSc#)`;Smol!ss9D&8JXeBk(YYt z3!fbLH?{fWK7{!+YlKM=+a@dktyjal3Q_5C+y3({8XF$X)&FH4hwziN1>iA#hgb?D zSeFZ`Ra`_YeFHjFsBSXpgY?fh;(FYaOc>nx&Vfn9EzNm$26!Aa?Eq{t0@xG>u<5h< zW^b{WLSRb)g^Tg-Flre`JYih@N9}Rb{u^&$^-Zbj45f5d@1JN$V>->)WQ2_un<@0obhd9};r^kVg=^YDx#+TA~(@)Qqq39SU z6jx?28FM09%ud?Z!b_Y7>$*i3idN115*}~s_3o&6o3G%cwa^_zp(nLghw&|qiB$dM zm%RB91$oJu2sMs+un2`7s<@^hcu?eT z&>vCC$0>4F*ojzWoiAuoF1RLl#7o<|++(?`y-K)zsWPOz%Jx<$I(LM&Og6$jpnGer z+-_db;kJWd*1(s}Jyo4DYrVQ<&x4}0ivHrYe4OlSh;sm)lho?{@!MTM5+HyiWCD@^ zG#>%YiZ~FGA$9TVP4df*pmIPOGXEtFA}zM7FM$Wq4u4$zT=yDc4CN5twhnqH)2$R7 z1X*&S6Q}X#jMkSry+9>4E@cJU0w`pTiLgh=2k*S;M8n51>wU_}m?Q2Y&$YWo-|jxW zDg~MSp&=eo#q0I1H%OO9j)%oQLvU0PGm!HTV8d#q*^sBj=#dhpUp1vLUSSmCaCsDpIS#v?bp=jUy$O zUDIzK<%X}Z*5t7#K@^pfh=A_l@*#E7vr%6|KX((Nty&7yneWd9E%NH|DoQeAKES4~ zJY2Sv+)YcRQa@OfCpmBj?{)K~Z*VKh+NwZ4T)J*5jXHIuaZGaLBP1Rvy4!HSXcBy1 zF+=)wY!zvyO&)cyRrdV;usm*wFS^-cZ~m_HF+c3XNz04lI#x8xlCUFe`**Dbo4e(U zo6^Y4v@DuE>ZZX}u7&4 z{f~G08m(XG6EBcB$Zl3roF}2CJku<-EwnNpc(J`(q7`qm`*=oQX!_ZgMmBlx((x@0 zklTWP$V$h%gyuG0{pVF|a!s2CNOEX75i%y>BKv~P;xJh!x14N5uXGs=B?=dXv3;Q{6!eOz z#69X1nUw86kc^qP*B_4Y^XQ3vYtRhMCf;l@x^aGDzW`oouPmAA2#&jNE*+>o2wstT zWTEDpLzcB=^$9wg+pPbZvGvari!DsoHwAEw2K#OFWWjC0_5^;^x6AGxu6Ci5>r{boO`j*DDdfqfa-i@gZzm z&CA0I@gCuQ3W+)PS*)Pj+{CKHpi?8F__o*};ucIVQpR)Q1pjdKe$)|trR}|HI8P%9 zy?KOcjdv4P3ARyJMQ(hRgkN`Z#53@%Kb^wL&!jO18+Z*+SLM&6gg1Fc}R52MAG}Ma z(O*MT=52P#@U5n4cDGMkPR3U^AO)g{G4%$o=HD{eG>ImHt1j#u9uw{T)JAb35&wGq zjjP9lD{K7MBo+bBPv_EIm&I!%2^|rbdG@OMhlndjALtur7J0>cePhN`#n?AQZpx1? z>DQAG^L7#h)s>024dWJ9Zr$8P2h<6|1`vngCa3{20Ugqs?jxeU=F<{cMu-nqsZkLh z?gf2g>*$B`3(}aaSJ|O0Vv9SW@9e@p&5^Mc8`}FtJd2`*23myZfQbKG`3-=-Sx1w? zRLYGuO>Nnb$v7|!YvqZRJ2izR`ZsEgbFy2?@dvf`5r%|83p3+Qp^4_ZKgH4fjpO<_ z&!cwYlDzn88GKE-HJBMRmdITV-|Me*BL`X8JdKXo>RQ})z5TKJiS~7sFmDonahiqp zp>hs$JSxmE31>F~!t>VlA}UBh7*bj|H*43Kz0})EUv3yuz+P~tsd@e0M4v-DcQUED zg5`X7WRz2x=ebD^K@kMMJVz?=_kj4Ey6wB&TcJwYF24VK=JhvE#lO#1{(WZjUlOkY zDb{#^^9gPs-2_mQh@BP4IsqhV=H_MQ`oGFL@!#q?05xR(%fZ`!Ac74jJHZO1ePicf z=H`9N&cphUg@X%7!@=<%3X%LP{{}lSq~gC*fN(z4p#e@^{#*7puK!lCfsOs4$N$0! z4o;wy2sco9ikBHUljG!OWd=&}u(RGn~49If{yFIS7TuRuLTAE z11C6GS($k4jXT zweF9rwHH{du9s)5iteY*duKYwG_7rKZwvPw-7~a54)#_?JFMr^2-K65C(`6PsxjWlj3#xHyA`_~2v;Ml(PS#%QS?_j8 z;f5!)FZ9m2-RUANvGfHglgG`PUwYLY?&bBGU#d1fVkNB*-2St{M0w2rGx9qv)_3&p zP^lTlaRyh5aaJ$r4jrFE7=-WsF^)Ich-i_1q*cQ zsMt^3YJaa~Tz){Ws-JO}mQ;AHZ^H!hMeBGHSit|L#ol?c2%2tbJ7WjcE^-%3_{ zbo^stGdi*@?(j$-`Ild{77QciPWn5m_{>H&-B^6uKPQNnS2n@plg96cV ze~C@bQ%NWoWgBRAtG!b2&1ll+ej=P0622g&TEQ(0Qb<5CZErqxQUTVKtZA8N14^iA z8Mg75r-SZS;*6S9ee1-9h{~ShSy2ZGe|&7M@qyxX=PA;>%VNP-ih(ro6GLjv>aw3rBg8b{z67N+}NQ0sxU z#AUNV@8J9sGp$<$^QokVNH6bv2L+0B)OKff3;pn!f=-@S*THs>_jPumPe9Y2&ec4W zSEswyWf~t(nz^iW`P8VNKzp!%4UDCm67n!$f8@EoUdg_AQKVeW^s)JE7r|Yh+ebR< z+^}56U%FvWJytIXoSqpq8AeNV?`ixx1wW0nYf)rKgXW~N4P zQJ>+_-1k~t9BY2>-Clhxw1?hpO-t6I{z%FzpixpWP2<^fF~Q6T(BonAnY|wn{7=+mgF?~8JXa~5?q>E2wsHjB3Sv-v?R zBzL7}VU;r0s7i&N9$>+OHxupKnukI-)CD8qN|OGWA7z!36Ix<*@?L7UaOvR6v*?o= zlDs?FS3vY*K;u<{g~gEy0$4B!ISm_U-*V&TyLrK?Jw9skrjP&PE(i>ld5+AIzp`|( zk)fGoc(cP}B(2skRUr6|l)K^d6#bpG(0fV|945W@MtZ$m2R#W#D?hyMNP}&LDR~zK z^YpOvTdk?-=i3KKKMtvQ5|oFMgDtE-lzslhM=-zFM-~$NohQ3@jic10YI=zws@iq3 zu2H-E?$*Za(>G@eyp5Lz$MbIUFE)?_gI1;;>n|^zz1`Ec12I4wrYC9&%^+46Jmd6a z-#7IoUdt@w>Z9S$c`zZVpBpTj%YKO=?Jq|?ryerMe3p7&_3br|>=x$A>j9$F{p3PM zx%aMqp6}gbQ17S2R?W->hTh{ic@N0^?y`6ONL~FyrPv5rxAu@tgnU@fD2?|UuYpxl z|1%%*MPZ5>hMK_RpGdQfFUmA?hXudAipHuCwvZn9eKkey!9dCZ|Df@ub-6~{OS*dL z61M>wHrlqmOFw?1#(Ov37BKq4KnJ%b!V7a)j~p4c+A%9k&oANgssPJr z^UuNOW!;-9|GYOmNQFqCuD92Qp4a{rHmoh!8y^%~<-e(YyudPezYy>`7_-5L# z8u#6VLZ+DczL@7e&VuqIGIcM834&xnp-z_$r%NG#O6tCeO))$6$W2`V{)u z*dhLVX*VHG-F1%m{@jhXaY!Z6C8j5$C8mU$GE|yhUROpHX0l#k<3y1HsHS~vYas98p2 z3<-*WP8m}qgcNw9huLs@=Z~;so1?w?OhDCh=FC zX8JHKBHQBe-(nZ1#;v~0I`>}J;Gm_K|JMH8_!s3z15GX){vpO0esj$S=Xi`YcgmQtXL-m5!A3?XE|)z`BYV&A6P(42iiA-So%u z(2LI1T)DYjIU(*}n$Oq-2-?{$Kc{}yn10Tfg!N)Y6MTq7%3)VfF-7yGiP>mfq)&`p zkv=AwXdNEl^se9Pae`4+-!g%KVRFZ=Cz@+U;kGOxX2#?1VXss^WOxUTNYd)e%%K!{ zzqg)FqkaA?DM(g%7_6!ZiDS51yL{(%J|h>-eOEJE$PKZixvX^1YR z;k|)8GibPGmdFn13*)6|+VLP6;!jlznUP?9{4}N}+w|g_g8uVV_h+|hxA3yoK&_f`zHbkh~O! z*mUs@bVZ8eyhfZXm21^H$>`10A9k@6N)v&&^$;Ms=;OZ!@QA7xUe^(d?|Q|psN!veq+lcH7;ckw-{koW*N11lI`Lcq->6yY0!S7o*`QvjqN+Iat zWsUgriUn-X&<33?Ry>t1$eF3BWvVPaT4R_Ob|!(>Q9I`19qf#x(atZs^G z_4!Xk_HaZV&vUEK^xA0i5mp`g#207tJ2Cf+s)@@Gb9rzy25me5FcMUfuc2BpPmRj~MCI0Mt<<@H2lfd5; z-%O;Iwe}i4?zR{iDu&kr1{9*P zUgp7!VkdDH*&AIhBG0E=BnF;ZFw%dN&ZzkP(u2)Z-=II;%VH@wyP3PAObmszQlWug z-9=VU`uVI8EkS&yT@w=}$MY)do_v02;y~iBoi1da;YX!SGR56Z7Acl8>;$E4-N-yP zpRIycmngBxbM*5sv5KMjYJ|&t?}|0tQBtt!8EixxFOm!PtiCuJL`RA_t|M9XoOe@v z`7+MuY==MG{p&;cTsQESxc;9X3fb#Jnq6BepBxPW!o?ilIYK^=OL_6vpmw_n*oonh zI@|H+XRqp}2s;|H+x{j|(^?(U6u0>y97%a$w-IW<60}G8Qjnx3v6uAW7b?CkdQBwd zjopT2M^8imzTckskl|1=c_b_PTt7}Sd2^1a;}WHD#)o#Aj9P#nd^Hqi>clmu#&(|_ zyHVt`SC#CV=uw~HyOQxp_Z68w8FHw~b2 zkqF!{*T;~)RsjRufKGvTFzu)3KdI#78%va#b&EfnF^@FF+n(ApX5~ zqOt7)1}}p;1#CsdvXvS4UefRpoo~3gS9S`dVc*yQ-G4!><#;m!s&e{Mm+w+dt;8I*x71lyO(z{hrG5#2vUX#`=S zvb2uG5_rFIzhI3gVfLv$CAc8k8&+XVZb;vj`6=SHP&bIg(_|)r-o1?d@>yFe=Hscg zAu=`Z<)MXm>R*i~@ziQg%R}1YG+uW#=ApwGjUO|Qf0L;N0oNX+!_&YyNX+v=I<`$- zLxB7KY^Mp89<*mP5^zv`$`tm@R5k#HTYngWegA-0ZD)CitXm8H*1B{QxT$()NL&#k z`#tLIVS;xQ`mKzXnruv=(}`Fhv)>)BS}j0@#!$HUaC##&!gh!(ED$`r!D9DAI~ZtO zT>h=in*rGfo0wBN0i;92F(Q0V8}C*|g}t-|7#mO}C!L#G+UU3aiy_a#@ovMP{S@|0 zS{v*YxJt}XkCgT;5IWolOOpR19T~YslOOKQW&9-Lw$dF#Md(lCZtsUh>x!O=3~h{h zk5<%D(*@Hli{Iu+_?1st(edg9_G7~JmyKW0 z#ObbhS-*e74A9w6INhFeY9y3j$bR=_!OFos?Dlibp~t(BLXnH1LqRU-31xhtEAJ2z z2S-?Iml!Vx$vk!V^ZHh-_6@Zil;1Iq?R%5^GAelule_(mX69KAjjp5?E$QIm45Z3V znZ^dOz+U0TwlkA8-P4I7kL^1LJ1ne4>;6ufPqc&6X_{lK9DXS8HrDiCY`oN8My?wYM9jZ!PU?DbWlez z7)B0)VljGu)oLcZS99-*RtWy-;ww>l_1i5l>7kcqT`uTEVdNGlc^+$lB<@Y8!hIz;*{Qa zGvnE03s3U|nFR5rS|bN4SHqg;*?wf3#V8Km_2`;6sy`Z?z?Nap%~dYy=Fb~h8oGaH z_5HN&3vMgtFkqfeG-YQ}bm@VtRY(gDaWOn-T z4T}t)zG3i0bRdnDw4QBhnp8YNBp0`HZKlJIXBDW%x~wgGkVZExot#h5qsBz;Y}1-?>T<4-nl@@MdP2r-4fwZRO=6T*1#-y63)$u1_ZX=LemO}RK!n3z?r z59-m83l%&x$f6tNF&7abpo?>EH=4>#GRZLRsh-Nuqb7`yR;N`vS!Fztrjq@T{Zf|W z_s=uVYo*8?j-=|%);f>7D#6uH+*1k9_X%MS%e}sa1h7dpIIHIz8S5{_YU+XIh$L0(xXwnUpD+khdtTNWiD~ zJ2LHzN0`qh5o2p4E|t+iPf?kP#voQVbozpA>YdiFHXn#aUmmX=x-6YU@}3J7{X4xa z&qi&tngE@5%g7{&;<6GG&YjsT8`J_s-+mHgd(elVSk8qjX9)Bp#<`~VrXASkP;V7d z&9iidY_gkj=U#{GBVD`ATCzzo65L!%seo}ARXXY5v?l-nM9(=>amsHrjjIy5=;BiP;y3xQYur$|^E6&v^ss&*P1NmO zieq_iXoIWag`F*3=sg%;UhzxEaBwU#$!_!ayF>0L1ai#vKMjn@1K{xqalzFg?L^|x zmr@bDQ)S;BorrNTEAh45R2MGQ-$y8QxAM#_rgbL8hm}_vXz2LPMHE}!b;k%$<|%aK z#8)RXrpJM}N?|`ixZdY+!JlSaUHvAog7H~!)vb05&znU^PN@9!0!Y3QT@U=pNCC|> z^!WQSGJm&+sr8Ba_B}0L`=v^l^wb2xfH{dG2)%p!3+e4{8+IG7fseRXzu-rxuG0m< zi2eB<3VU6NjwTWr zKIqGlHR7hEC z9HXT)auQFEgAz)YhH%VeU~@m6jON*QjZ037_1Q!{gA^ky1?sT6}HtDj=2CGC_YcFIzo5+V|xSachAaes4n5ILdiE1QNWFeM(w&yGGvBF?hc* zIzO5BOi zyOK_C0;MnkjTf5eT$|m%b2!AsfLJMi>yI?^0&X{*iU`N1@KZI)j$&5X4hHu^5ot?- zCu(mynnW&iUJk2cd;<9|wc92sHXoD35nX2>+G{X!cH2{PXhQE89jzAD9{Iji$3bFl zFH#&(6D++W&?v!Ti#G_yDv;=;t@AIzE_;))Z_CNU_Qk;RwNL_#VIR|(jORdIVnxgne^`PK`BAvLo2p8nwjqK{iIzWtm_Zec!+Yq%8siOXBc6>dF1KeTWS za_S-tuhzC2_QCp5(4=5*yF|l5JhX7e?SpAcjyh%X;#1fgR7Qe^(&#R9**^c5cCGK8 z-WA>s>OEF-$#~o4uUk-){uX=jO_uKFr(vcp^S_kR&g!2bhSIceOS5LPsF(TS4*55GESc;d*^6PR)Dc+bAE0m^ zKl-M86UiiZ8q0J%p!s(2KyW?hyrJpw2yX4$96cI)&W77=4Ys~&5-I_Ts~=B%Na+)X zk{JSL-8E=!890k8&+`a6!<@-}SZ*3U@_6|s*Tq z0XP1N#zzs3-IcBmk%3N8Jm({8IS83p1+9%B^PlY>CHKz6=!|~dG{R-+o=2Mrj z&^0z>QGpoPnLK1o=VtlWCrDfzY%IbLh0(u4b@dH_50xAs`i6>PpIJUb>}=%?ZG^2X zpjMWKmUeG>StKlh@)l4lb6q>bw??|=wuUTUWpr)LfbWg|`9$k~IlACr<@-OrAljL8 zbi)~I={Wp`Ci*FAzV@wV!Z(-j(BZ^y-zJOkpR_-A*uQQ#?-^rwOS1V3Crb*WUJ76Q z#b!NCV(E9veF<51X>JP+DxeDA|>0CJyq4MG1&UX`5S2QG!s$nC`>iYluB$rR5ZLyqG_mBU%~0_a-HP15IJ;}bo0C`FhDQ1@fdy2Xv)I8+$>R|)9C#;)u=Mm8L-6wV z12u0g(ng2DOoypm8pjh%03%)i155hQhzKDBGsf~uOjh$2%{=dKD>EEBTY%v=%NO=F z*7`tZO@9W{3t|R^?q~lsj9m>T$u9&rtkA8SX?P!Ku={5)!l`nwr%zQ7OGL%D2!q&j zfQ;w8AhV5e7QM*jc6SNqfH_;VV5O(d9(a6#A)H1x zb$BlAhw;*}YTOd)j%4)*6%+fVK_KX@zc6|3xa*V@BP%@p>2n zY;6DcuE1KMdO4sQ%jrLzBFw@YWMbE6O(p(Co!4(~6AR!=6yQ1CcnC#+3v87<@G(Mq z$Ky}xMk2-*2+Co+q)(EPff+n$l;UOxkWo39Nu{3g&g!gDc=`tiuyAe1mEQLK<_JQ7 z=n2~!vrML67sHj{R$J4D&cw`uf*w6lbk8wJ?M{4gtW@mbpv1ArI{hDiZ~e=oir?O} z@UeWvMRlXi-%j$iOHbqBWvw-0y*#2ei@`l|0SB&YQzw-qtDUFs53Nm;~kL4?8EU_p)zsnE>qmbHkgwZ0c%L^146G?jj~(oqMW9YQ&6Nj z{~@?+%?H7!zau}Fo+`%L&o3M^P1Js%^{=cse_qZg&sBEq-l$`FJNmQYo1Q_X+!vVg zpPUro3Xc@4hZ`kn$jDMl^gEwDd6J7^zd4*0fKBs7M1(d}e*{YM!9m)6Mv}Wyyitrp z9X<}zKJ+i$&XM?Mir$A|XxmrnUq($${ytJN`r_hZs?1nSObmvBOH9n~xTk@hDGI5b zc)6m27b;5*N64uT2vqAT#Bu`3Mj^ohKVmaI={lg!svvZU0i&< zy>o+KVsXMg!W4?TOCOc|NIiF4{;NqVqcyk~O8_Q=`>UI$55h7>nh)Q#C zP4zIjdp%`}Hg_f(?EdiVo)F$gf=68xYrV1Br3L|di|^eopldzRWNrt|0OG2 zpnVrg@*$99sVABtP*}6hsYI*ZWv<2^6}{qo*2qEc54DlkQ9l8nO{QckLccQTiFPN${5I4)d#GWi^}EI%WFRsqI>Fz_t6pU;sv&c16nFRa~De5p}s zIhVi*iI@$RjHb7kspy~N{cK}Xdhs?!c&tF}c(&Ts*RRH57aWAgv?>p@D?_S876D;T zAnrNw?JYTnwyUnR2`W1`{}yP-=Scks=*2$cB2EqN0iN-T%Mu1mL75o2pTY?B;{-?Z z=FpqJDhd=j?|!a%dFS6mW#s7FDIqCI$nR84EZN8XQp1OaQcy7nY#$TMY%(fIWnsvn z-|@=10|A1A1C2F0Lx^Z9Go6qB&_`Rlxz%xBW8Jqx7qK|HHESJ|bLBq;Q)sOmEVZ_G z`Bs|V-CUl}yA~0f0wx(CNMzKs*eLq1ne#%`7-Mu>eY-RK^jdLWzdl%vlfai-?ypFNOQ6cG_g6?9+yK>c`PC_@bQIp^CwqZZo_4Rjo^*vUph7BVu! z5H5Zt7Z?}c%F0FTP>ygR0qGVv)9M(P#WbjXfmZzAx`Pkp)vM9agVU|Culy}M7Sl@T zV>R|>k5zOJg2oWwUX;H&pC~*$91zhM3R-RrEiJ7ufwMSb$&#h_o_B75xU9)Pum)1) zus55>r9tr}*gue6wbox^v(&J6uX>#{_QZF4w{qdnul!4fw)(S(_0@*UwyX2c%OQ^H zSFVUJyR^^;YL0#;CEYYXCo@5dm0S!A2_fMy3Z9L7keRufu}R(^UF^WKB8A6t6@;^v zY;0{ucr-{!zX;M%y0?y%83UhG7a}1)>q2Tj90#%jS~ooohsVsyk!i8o<($*C3P0_b zvqWsMkeJ=WN2anW!7L{57`eX-^VIkwu$NzGgbCeMxy9lT6-vc2L%zcRd7q!3r?#Z| zaS9)2(H|fR1m|VKlf1pXO<+cVC6HPRhP}{$t5?Pd2Q+03LzCazOQLGb)jDFJps;g? z%CvHsPqIe)nl=A;dOqivN^A-kvw(mAUq5=b0pA)P)ho9zqu_I`Z|zP`-)4Kp!$C#5 zGY!EK^#AaJ+QUk8ahR}BGzc$#47xjCKXV{|cX#JA3cb9%1eWr5nlSlociPqYzP1|? zvy@^>E{S%mj1_`3Nyx-+qB}~-ODIoA6Q+C~J7oNM_LbEb;zc5e;rAr(Kf8mpjZt<1 zn#^b0aUjpB;UltO$_3`0hd%6vwU+E9K$8_OrHL$>E;Q5=OFm&f^d%zA|4CT5)anQ0 z)aZ?6nIJ5zoOglol7a>FA0NAebCd5-tc#+jOa-d%Zht>n;pwzE<}XBm2X`+z^l~4p z!KI|EYzn_~={R(XtP1^7yL?`9*t31scRPS4<)<)T<`Nd)^5?kSIV~Ewlu>57ovE^% zt8@ZxwAdxrs=E66_TKV!s}9wbFXs0>Q6Pr5%ylz0bzmmU$X!OHB5zl-b5nteKlaxu zZ~H>a|1PMyj*d<&v*xNR)#C{tRQy*M_P~}CBXt%*A4?PVQ=Eae&q)V2LIy&ZAnJYm0 z*}YY{Zg1%*01Z-qHJ05=Qzc3$w8RpSt1MJDhNZwDpEhBECkfYtAZq?3Iy`rg(C!#oUE4;`IW`#ZdceiC#Ofs|YNs9zTBE z7A)G(^bCzKH|f64uWJ>F)g-a{dk@!M`J3<)+ zO~a8%OZNI~mx#+WYM+1!5RtB30GC|IS^0n%n2+)RLlUlX`ZiYIL@A-X?r}C0>C6 zK8C_E51=JDzyHgR51Pp&5H0os!4m8X19lTVT5Q}Cz?=tM4kubQsA-@VlPVxtfEwu? zX-gu(xTN8>UnWcivVNNe1T5@=%wkL*)WME{EyhH;Xi%Gq&2{g+Kx}KJU5o+*XGpimquHjcETtUT3er!&nKK+15{v*w>r!+CN8)vGX5K z-YxHWE=V^--=D9N(+ZZB6^`|IeD_1jgxC4_`kH>1Dp|t%E+uWXDzE1#52=z(6=Wjg zroUMsyO%ewq6Zah=sC1fWfu&1V#LZxL${R)6t=^0S*pYc_A3Uws{ncZN;Ho@!rCgF zg#Z%_j&1!#jiwKnq%`%SKAGK6avn{^M)GJDHPLXi#uk(s7RKCj*koCv`X%^{E~u62 z%`37-WKsp@BBJ z9=BIMK0XBV`N5t%>^6V;;>mLfFSLY&8qrDKcZEPkbHn584embx5}{&fSqaX0RSEuN zjVV{?sl9guxO2xU;sc$wDk>B7%K2SzLI4tm8kZM-nYBbr!xjGJFX(2%gYa1fQUtEY znO1BeGzKG3I?^SCuN3i8a`lpAmEmpbh}n(4vJkH#hdR?PX97&SkY@I14Q7TQ%t3&u zMc_)qF(j*0%r?@ezEX)8{tt>-vkKbW_VC{;B5vudC2)zJs5PXjQ0%gDz#IWCbeWQ& zGJRS*HVTHLMJj{g@EVZWNh)n;uiA#eUcK)PO>})b_$aGATo%-6)+Xc{ES5!XqBGkLn2-s}g4w?!eU{!@< z(}0*iZS!$_aC}xrO*f|slB`CcwvPvZ3U@(Xnr9ZgZj%XXfSvv@PX~ z1FzGWWVTvS(SO!1YIk?K+c#pO&ys-i;I5sOK&o(%V;h!Ak5l%D0XvWltga2MwKVE0 zPNy$QnVA59H9?#B`A&Fw`BkAijelO=I8-Kue~OQYc|UFu1k>s^M^M)=Fos*dpOo2& zu*vl_k0X9#eB+W&qD50C*fQz;;0@tM0pj@HK7Sm_q+0xOT(O(3+m_jU7z~%Qfgn!W z<~eOv#oL1Yi>^!($**Q$sw0H`=RPM@!MU8hgfOj#C*Ks#*Klnad(wJTx!|#Fj{Qosxw-kuSIUqC%vNtTUJy^5?|ywok8#Hu zdok(A$<#928$P%_k*OB z)Glt74!GcBTMbLd?I4(945)XtGN{~8T2F7;kIaJ>IrhSx^!AeIyl=n+At+E&^^s>O zVG%o|zP_na^ zYBze=+yIh8{NAqXK%Sag1MmPw@%mXy{$h&cuz+q^Vhoi+jl8+m)2r1B-b5@- z43X+SEjPErY?ZamR~hpvAo@{wvA6>GIEHmzT>FmkLja3Wm=7NB_ z@n0$P9TVuh?433xxa1w@(O?b}+&OBLvuCaS2Mzd?Q>V$ap7!%BMAi99tx{Z)j^}+9 z17;xLQXVM+F9(?|V1QwWFKJ_e(twzEnIn9SwecoBK3Oj%0~_^9s#A;o?@&*1@$jf0 z$MTCYW#iF*@6BGSae(X4M4p}iSiv_S-nYbBL~^>;u_z9uCNn4Vlh-pfM!Z&agwx^j z(OPe9ruHBJ2a3BWkSwG*|@&PwJ9;dect2CIQo7+~@KMpGMa zU#yfpHAkGpSl#xoeXm^uDVwOACpivm3(!AA>Tu>!4zlu>^3^K~dobrEC}&F>Hm|hd zV_x5c!9(+oK| zdAX<;8jr?75@P%rKEAa7vUkqT_nEpzXjt|Hzas4nb9v(v%#yWru>5WsT>na2wRTkDu5pXCg`nbD)RF3fY7Y=7O51?1KvOC$?j4B zn;bkf$+~F#9<2&^yC?{cKCFF|=Wh(?%9nwgP7A1R&abk)aI5av#oH9vs{bjKn9$_q zzC-Pp%N-DOxoM=q_Y`yD(Y*WIp_r?Uo(IHAX7hJ?TVNCq!gV$h(jIOL;4^UR=~}FI zOtG)j6Xsp!x#bxs)ogh~HQ=2j@aT*o>V5qJOF9f8h?|f~upryJ>C!h4IOyFhEK6*f zospg%@cS;Ul=Botsi2n^7bY;ghvTD8kW5JqWe0NP#U)O_U3{31D_HM1b=|)=4rI}{r;sL5Mx0GmNylh>(W3V&jH)} zg~pjsexHyT%>Gl_SPjnJl+%xc2Ba2H3wA&h-G6Na?P_-0TP;dhksi zB1D~`Brwg|3SvOdYl)6P+bP>I0EkHnW;LiB)IQRJfBW{0S+k}qBu~4sK|l^2t=QXf zgefR(*fb#QD7ywQa2#a1=4Fg^0ciLT)PY|whC!xR8f!xLmvWnsDijwqFXRW|DY+MO z-f-2KZyG~^12=pxABbQp9eZyAhMoS{460xEFVCW-r{9V^i{gNrE%^j9CtOiT8U zLj#)DUiH3Bqy^+c-LJx7ce*`&Nm8=ma%&#&m96$V)R{sq%1x`caHBy|#4`vGY-!k9 z^!LLVfxWJ-E;+Q@c2qAVWB4IL19`Aw1S#z=^^DCYi_?gK1++Bc(f}YSEgo8|vE5<@ zgLcG+s0%Wl^bi)$geW)IeRkNIe7(m#E3e@2B=0h9nev2b$mQ`&6?woz-04SEbi|D| z;GVic&f4eUpzTgF&jKzDq5GRM$yP3x6B7V4Hn`VH{tbm)f>wv1K2##xV7LMwa}fY! zNZn5qJTwe=R8&-2i+W|*B&>n+0cLf$AO5SU$SKoJz_npQO%-dU5)1{R zUVOkDC5&Lv6_c(;fALTsnA7WFLm2=YO1<^89bY@DSPI3<%L`#|lH9ly*a&fV*EWK2 zIW8ucUH1|34IP|Q&r5C{cXC!M%fvRyi~ki7LlkTxW?&Z$0I+p`UUNq+P(914>b=8G z?opG^@))#Q^a{b5Hx|)GoY$WqA;2vgBCb@W(z_x)R0%j`y&j4Dp{DS09vYQ->F&!w z9LyrYtBs5nld8Lm6^vG*RI661hztk0ul3p;DFe5d9TP>`RwLOV4*^T8fZ>DlX}VjN zyFr9;(avH2X4OobDp*26LmT7KpqB$PDoJ$nXit48D=@^PDe$-91D2tr* zPoB@AA0QfqZdNt6)(0J>Qd`7R@$=tI8K;gBGC!1X6v z`hMoOH-%H~LW6l@!$5%aAvzt&sR4j`aI12TZ1PHH*vyTO@Ety>7Hj!<#QK2m>eneJ z7ncVP+R(Q9!sLNqJ8;aJY5?Q1E83r`Tlg>q4IooHo7uau4`ans*mle9DSe}Lh5F*y z-Md7CGsCDq0Z=_e3!6p9H|&wA-7#Ty78Y6{~W)EInJ)2XKse@HiAe=Up#} zn1NkO4Ig7~VDwdMYum>F4ni#_?r{tsu(b^iK0Zx@C?$1b*ve$)#n{Bz+PLW^;G})n zuls7K<`mSCzI4->HixCTFd{P4KH zbxdTr0%8gKCJIt(;}GDvfJ^Om90ZQ=@0gMU)m|lIQ{Zr(KZJh*1ue3&c{IqOd={-# zWY`t<+o0B*(a*xdz-}-b%U@GMQ&BwXk3Dx|QHQ&FdKe{Nf1-gPh(pV>XwH79l^Mac z8|M#-#GITiz9At?L#h-<$kZuDWHP>l6lzrKIVIVy5Bxvuz4<%TZ}>i3O4-T2gzQ^m z$*v+~-*?6&ONp@!McKn3gzS_BW2ANlEQP3_vib3zt0cPAMhN< z;|Ir)n0dYK*L7d_d7bBZT@!OmubMww_lj zCB5u_IBv~%v9&HKC<^$WbfGHIo3h(|I#Ztq;Fz|<#kj?B>j%ZFBc&WC3e4Yvc6)^Y z-gtE`YjnjjbzPa;RshB3Rl-l3=z5o{G}ve38_Gbt4bk#S z=oOC+j4@(!n%A2eb|Kw6{yqP}xu3n`*Hw@^{An|;R_LS%4oo6mpTG>A&>BZ`o|rNH zT||QA`qM zFD!+OIgc!C@-NF@pgaDUMNAHMbf~*wq4n;o{M1%smTnVvkrowqN7sS^NpaqyiH%>z zz!UgAhu?ywMRt5QzR;kbSjTMd&r7`JLU7At;Fi+6qTd2e0}%d;WD-U_DtlkmMX9Z< ztuq1Ta;g&-sr$vk`TpUz{6Fer8Cc78SXs0}y_KE9hLIN^cS#XAXu6(#i7jSy%K(;q@V)OeeYY!QF}5D}yi&n`=^BGIA#dIHhd)MQy<-Yd{bSOeI%g9FVm zbRK|LM|_{1l;IHEV_+D5Y*viVdhC$WW8+wM)43RxEYxQD_KLQx;)j~v-dg8=Q7d$| zUckc9M(v=*rDtFeGRP{Dg}IHRi2^Cr5IZ{f?@NgPSG4lA9quw2Z)01{1>(a*GWM|(3nmmed1pGi2?C3$|zxp5``m+G1 z8k6Ys^Go7qQlX=$N&u9!61imM4B22cm!%anKR0gq6!xZ+<$q)b8YA2CCQzN31B#mG$Fqc4E)j8YPmrE=?^PJ* zhb!PXxI_Uk`#^&N=b1oM8$RG)Lqo(Ol^kUeYNPYk^8Wb!@gItnR@Go3FHx~xYbXU& zoM@ag0Oc{G1UGD*8jKWCfAYpBPQp6jLkpf&Wu*ga0Rbo;gs;2jb`Q!1|MF0Y&y zP_QaPNs*VJMA240DdtE2n@5%%`#as-^Zv^@;;W^mwbfFdDnR`K8VwL=**Szjbulci zK&-<)7XbRK5~y|>GYAa<=q0Zl9+g8-t9l+reK((kA0MUrohK9{05=9=QBS(Sa(RC4 zhTjjfsjWaz)L!yH>NlVSiSxfXGW`bIH(#8<@;NK-K?RkdpmDG&>rSl%_pahrmEaZxbwGE-&-B&2PIqF@W4O!$@g8`VE{=N z8RaSO#=rkSXrd?7{`q9`Ce(BuA?r4_apc>~oFd;9wI2Pse^tU3V)fyd9EYH3QI33X z?46?9IBbniWpVJ;r$=}>cntUP(f<0+#(smA^kYEJe@$QIg|g3y5_3sP5o&aguO-Dj z#x5u*2qbdR^{>osymX}2xL>D%eptGIX8hXj^v!n7|3r8-IF>8q(1C#i{cAb?(M6yg zw*?2*g5yayu8lySV%IS*R$vJPHPL=<$K{j&pOiT`Cm^g@{EqWl7cd* z;9u7DJmbyflS=eYgWdqMcQ0sZT!~uXquhH|k9W#~+m_u=Rz}OsfGQKP-G2G$#zfNZ zYbWVn4I6AxJFzIsj)zT_eU8)cn0etV1W;Jycr8q2jKQhRyv``fuwO-xQYHolY0nu2 zf3-H#VqKtznf{kvM$7VjGCZZevv5p#_PhNYYuRLRYe6OtL}BG&6#(><6H}EKEjYHO z3fPFU_2&XM$6V=W+xCs>G4{NRF$C0EXz8^Q;KM~RS9OsJ+}zww-+H3f^P{~ z___Wn5KE;%k0}2Eq<$LNfbIs%!g5Fs1V~P9UUr^MI-%yEf~b?ak+ zQoVwq{>B88?aNP$g+u)CfW2ae^X_lM*(uXm`3}-2!pmITg~K_`V>vZ6RAXbG-!pc~ z7p9;X4z#fr+1jAF31hL&cs)5=IdJ+daGnz9u-O_o_$4$%`jNO@i#6UTsn)rrM{Z@Jvon&t6g#fsyJp@ZsHO3`x>QkQo(pI0cHN-n_o->2 z_tSs<1Xm`MCHNZ5y*&gmb-*q_2fEU1AZ&OgOllOI$Gf3R;F5Pkal-DeUt00(k(<^) zi)?Lad7-Wv)hz^3iwZ3yv(on)wu(NvOFlXC=+7_5T_54oV&wp@!5*KAv^>@MTbuRf zdb~+weYmaZhQufY^;HErOnfr&R0S@ar^1Z42_$_{YBd5dS66p?^p7Wsgb=+_hn2`1 zd)Y?y9IbV{_}%*{-7n#aCZ-Vn&HB*{dP0^;Xov<*pb*MZU!X?4U;lceF|=g3=v+Rt zIIJg@NZ)YXa|S3if$O#|b7>up3$5E=uJy2|%yM$o_pgwZMeo%Ob-+iOlmQGO!^<0v zwb%={ecIzk@6F;Fc(CnjyjsBa!g+M|{WJo-DXP8HrJR}m zsV&Tr?r{U@J^rA22lModjBd%QfwYtqY3DDj`q%_+6+^SUpB6l@^5m-dAgfvZkUQL> z;zHl!E`%<)*7Z=~W~JV~Te`fpbxa+(_Fd{1n=D>!(R006R9N_<9>qGoQmeM;6%RpI zTl)~etD!W%m9K=&+2+oCfD9wZ$K~gP)b+7_o5r9du`hpbWKQZKZ^mtEgUuEZM zEPu-N1bRIC`fsn3Uuy4-y9B!4cm)=Ch-@hw+OCs& zVmC{>LkbjH(aY^B+?7?o*f=;S1xX@-<%l7Z^1Yzc75;1@m0?7~bRzal6Sl$G!cUDS zsoZb-2rsvLQ1AeYjJ&G%xyo|V4%Kc)RrY_c?DzF`%4)fmZwm%?(jCQ$IE}}{1wT?P zGRM0gpD$`V2-Li1Q~-_RO369=-hQKkG-v#{j&;$v!L0Cp5%kMJ7XrjBKYcIAQ#)MZ zPoF-uM8^GCaEd~T-MV#^0S$a~?|5N?f`W_uFtrNOt((+%CDFIg^b|#_Sy14P9_47S+rNs;k>I~D8alsz6#9mHm}}3L&XQ$So`~sFyPYBCr1$y* zxqv0lI6W%I&3XV|Ne%BBDHr}dViOm@eP3PGk#0ydg1X}?(k95PD){>jmv~c;pf{)$?|y*41{mvjid~2iR^rAQOb0 zKYzXxMT{y?=6u~BW;HHp#F6E#Xl!|`qW7H?VHb&n4h;49Ajb)c{Imec(kopP%T%LvvzwFg|%f@y{h2!L=UHzoOS`MGz zzys4MR?V~9eElz<`vIK1Yc~Ceo?e`F`70fIV`Bs0lYBq0L|Y$Dxgw7iB4|BlP3KV6 zZAfG#kn*a?G641QjBNZn^6#Z)M)P$APJ95}D4hwazs}t4mxqx)hWVb4vo_`xcA7VZ zp0K8f+lB;A3YDJ7F=T9Km_dq-fu#_Kbn8op@bbX0H=|e%g5QKuRP1Z{(#F$62;sJ) z4~Ohw8^>JzybR~@M_|D2nq@gXJ31jRXfz0Og@V*lEROSk-TK|Mm*aDY?@LQBId3vw z2nfKD=6`HgwXXlkZ>AekG87Y;3BUJ(?|$mOjL@H7#~-Bgq4~WgoQxwbr@u1OUUi4` ze4e$$m|I)Fptb^1p$K_KdAW!n3<%mFT=exjk(EGbeY!crLjzjf?itXbm4lC7@Ni%1 z1~z2n2HLOL(_A*xMqmBl(xDo zc^cLD%1~jKM$ z=QtQ)W?VMl`K_b8Y^=lYySba@4L4$}TlxwlX;)QMZ7X#5_s5eHNSxFjZ`UZ0v%pNi zQ{*wMwrjm1P2Okxj=p6)6TW}@goPTv`;2weI0U(4`-ZyvZ*?CZ;noh?ol&_YiLpoZSv-o6%hq3Pgob2?8(D}N4d z{C8qfw6FAoc_O#U=1}2f3Zhc1E!dgA!j#51z1^G;;Pb(v(5}D#@6#*MO#($wtqKDS z(uZ0(e_WG$w&2oFTjcC%qCzUg*$MB8Cb_kj3%*_*?JY%7CWm^8_hGO~R)&THSnqF= z1d^vZw?-Lbmdw|;Zd81C=VCO-+EjJEKLJkRK)rKR`OFfK#k$p}J3;wrfn;QEZqs@sl41*3v~8fa^iDd@Uz zcwZC@|IKa&`IZtlEL=7=6y1I_txt!4zjxov*!+*bz629%WxRZ%V+Ce!m+QYbd0}C_ z_iymmfvgXMDLYLmiG;iQyE3yMks0UXBrn)^EeQ<#lS6%l2$tcMc4) zZ+U7|ttb721HdKDt5@~MicJ5IZvg!{QTa&Q3c0Rh2tZBA8tPPMtIx7bO45;M`r>&@ zN{JhP>@+cC%}&qc&Ks#c48)xL;o?A}?={00%WiDeV>_?K=Qc!AjjtPE7Ye@S6!F89 ztSlXJ^OuA4G2$b}s465^CcM$;Z9cTYl&gOFwwjSQvL@>6$a|2i%|B9N(1Gm3}>(`pdo>$V!vKJuPejP?ZsJ?3BDU1kx+O`f!uFkH-aj%Y!R}gAJI?~pe@g& z1YrpY2~}j{;FPY|Ur%B)QbCBQ%iUKnI>f(g(d981vd>{aV`ow&XbmBicd%}w3k2t9 zvDW3+|LKv=?)-gOX8F9}`lX=#$GKsHQJNakFFGtm&lbZV&y(u*>dv?B`KvfzP$gO= zA&QqC9atHo$%Nl7dJC1eD}u)Oe~E3i0)P8LAWaulRV7KZ(eiQK zc=+G}6{iv-gSd){%A%!!fPe#Te=czA_&4{T(xbW7z)VE&pYItmNWJJLy3l6KFT|eO z>z*=Z!WH`$aJ)n*)NQpaUF#?0_zM1L`LkXB+385eUkF{6!@sh+R@IDu7P>Ua`=`Ad zu!|!p3~0~*FhYk0yrTo|;iE@d>e#n8KOyywA^4}J-0EJDcJW>`HeEmXi%Uw_IXDX6 z^u9s_|J&(KN;2;*H`l9~^rSf{}y_jaH;`wvZ=Sy)5Lr} zfE!R*=MBCfre&={bIKS1X(+1p>h9=c^kE&xCZuZHcMS~pJZC}6Y4 zjw&kdmAr(DFw=3pR@K1dB6%uF@0a*gMz(Qta>kRhObbNLczDOCLJ_v$vwWFu0YI`{ z@*xD-H#|(wVza-!{mglEl0=BbRod>!Qa~tM_g1m za|&J0pht0$sR*mMAW}?Pnt=fgn2^6mNAwM%A|mA6LHb5mQtZWrTJXXEB!6Han#Le* z&fEsPrH9)0`$gn#l1p6MSdud=pSVf&al1F)o>(BDc}M$~qOmtGRcfeX{^icN)Yra* zQ{QgYFP~_Bg=oL$sDZ^!FVaWUeXG|MqZ5~eRm&4hbK33}=(FLoqTjp3>#;@3jMv*BU68H_fERrYEaIoBLw(kO2GsB25=2~MkN{TbeWV!4&15+${@ z(t@zsM}vak$(37!z7kMt-aRsd*;RLkiKJQHu;PUQZjaZEURY4@)65Kva+&kP9SrP~ zdBB(AR|r}7XqhOk>lOTu3b}?QW!smF=6b^0`4b4=pHpeQt^a|ztZC7-MZ3BE-@Q_P<4@mVlkJ{1#!dT)8%|@VD9TCqQqY z9wQ}1%3amu1HN;uGrEyO&iyS9?3I!^ow%#_(DRhIp~{_~1Xo>iy|TJG83qQ1Cr-8) zxGw=m`d%>Yv&5^zr+9OBKi+z1&dl|#QVW}6U2QC}$45I^SLIqMl#fX|Pnuqv<@IwN zs{-hxMP4N-8xyyMvx9?bX!m~+vRbdpq(^bz3rD(2Icpmn91O(Ndktki51D?PR7?Px zorR;KOZWG%qteDPDx>=gqsLhiv?l)q1@-DjWM5ZTVZ?)Qu5vBU>@ zxB2N7GYpff#5CTF%-y#x;-9gh0UJTLUMwEiE+W~uYJOqlD&G_N+4YF@xc6ux2 z%&i}-QM5O!%I-`I{-~%-O^wit%S)M**{m0}aCGEtVgK>thi=rmzqg6??gif_$$F$4 zP?ItdKu^n!I-@5T(6Jg3PPHlYqc>$lArOy`&(E(Mn9_?=i)h}#D0|mdJvr~75k{zZ z#>;bS?>D1)<#^KTPTSIU?mwUGyqX@r0CWf|&98bBP+oyX6pJj|h|9}9^9}K}e^xWa z@bC*nZsOI!tfea{jxS`pz?1lZ?wVak?`e z`W$T7EGq2m?0^n9wT5C)V*y7&XMRCJ z0TL-DE}o#k_&Cxf3O+=Zd-CZGsOj>=0%OXVpVc+CPot`_j8-ZnPi>h!V~q zU*Xeyfkfn&OS+i$XUXz(Nb|siJK~Utdq%PD{3e*;{|;UbgEt zPSelzCexb_aL$o%aBwU#@}%juBOJhb#y#svO-qvn&2+G0Iwj-x3U5f=B9z=* zTyp@3Ggg4=FpUyML~<{EoR z>cWA=gien`K!c^@e z+Q*>Sb!l=E!1)48@q2~hU24xv1RE#Tae&4v##UUb&Qc-mLUehFl2#EU|_ zsef0NzJrdCa8@+Crk=%1v?nF`qi2#ZW@cu`Yp}1f!YGGpo7aswq59Zs9dTiZw4UFM z>rG78EqH68<8-xweeH;HEI~)L8NK+Sh9vA+0=uT846<)# z=4z`k2)thh2ZC{CqCzmNizY}p_i1syo2v#FNb&4X;N{k3mMi&CiVDnP79gXtL<*tr z!T;kmVxB3UULm5W`*vYbOH`SaLKEytC{e#T3)C^VT%m1!C={8^t#C%yT8{;~U&+>QB z!~75N_G)03nHPq;4eNA$oezi(z(*Y%9CW(c)FIb>lflW_TiJI4C)BF-2V-@?v8W?i z5mrN;YwfI!{fLMOLjb2l6>!Z6kGkE#qWO`OWn;Ju=9A&@1(E6AGRymS!HJ3&I6F`= zpsxf#LIY~C9wxA1x{#E1p^G|tWW_uSkNdTNRNBy;1Fh4R$}mGm<~NUYHo=HojU~} zK?#qt+z-0K$!TykR)5H5Q27Zbr37xl?DWBl2sn!T_KWf{UEK=b{A~Eow&lX6YmK0) zE{1-U2R17LsSsey&E^pzw;9QZ?OFOneNsam>+a!Ua&YdlmJU|XF#82LvGjVOUR+vA z3+VCLZE!icDxta8X2Ucz@b&4_r5pL^X;X2Rml%xfTnEN=L=e=&Kzn&I zs7P=u8`d1sRZjw`riK?Lxd;I0moHzw;=ylE%+KdtHw5Pi6Fa-^ z)wTAB%b5u9YX*Oj>z>drdCNat%f<+o!^8=ng|_I(ayO#hXk9e#ORMrTC~qPAQ}q@? zQSp#pB0^o+7I17zyw_lo=vXgjm~%Y zDsPS*z2noJ2bZxi|3QTUU!W29>J{C4!7IWGIxZLf$T$_7Q+{Fv)yrCE(iYy-n3((` z<68MDiXUL?%Nl|44oI$TGBj8@77*v+VsWalTSfJ3TmkalqT|(Lakn(aT*o$w%`N+Ckp7JWcaW@IXMa1Nk1Yf zKSe+A*8Ps8Wa)PqDWUFf?*={fc_Bw46vYfUjg7n4I5`98te;j&^()=xXTvw~6hZ&G zl#PK*3<}g~=l9DeY>vcwCS6GlNx5}qw}*G63HEX=VU|$+AN*9dwf@BwGa-?bY9@f5 zL_6Uyz-$5@qw6p~55aVn^YW*8&nBmyX_{cM^pP(;J*+Y=bWJ}&r3U4LZS!7DY&*o~ zmtSE^vxWd{sQu2i-reN3qkkBjGbc@76$8rKK=2Lif?>XHeNMgwJlKlCsPtg_2j+cb zHYgiLVq_5MG;>eA?C?6O4!s|sGR*^J>wAxiLsI2RR4r)`P&l;N*7m-MJrrY>J5^ew zVmjO9k*Q?ku}oqvWaFS^Qi>3s#JWS7_T~-cw>wYb_mGsyJQSec@vYC4;1-$`B+Q zFE``wI!ziL9=5)M?&;Ci$10!x`xQ;=y38yi%7%}n=uGMTkcZ*R)xYGlzZHK0v(z=` zrgTu-+Lp(O-z_d8R)ggdhxQjQUR<~hb9Q!SqPEK74}6P*V|>^{l+n96iSPMR4nY2O z16YGzM?g-MQ}w{QH9%Gnc6#zxf|?M%$;wS;y;_qQ=H2jSref|xPny7oJJtTEaXn1_I*R|-}3t!K@T$(kJfpOAaeI351`A#m`4ha@5* zLe;Sb(?wE7<#>@0Vg~{aBD%k%B$|7N%KDlaVzyaAE%1IEyBpA(M4^imQ&7kRjP*snbYQu#(7>vt0Gi7mFsb?*`+^h|vMp*hR02dEiJiT` zcC^HRgNKl8g8G{YXB)6r#e9bL%ra+?bJ*7h*;rCqMpJS0RS z*TJ9>1|C9q-33cZGgL3oIaErZm1Sj?geRo4WL$&2q|^$iS?2)Zc`s1dlp{x7J8wzq6n%++ zsIHc<$sVRau79H5Ex&}Y<_1Unc(oNyUD5V|=RlIwvU}8WJB1@5e3x z#K7fu7T`BP=K>kIl^XPY%yT$t*J-Kq)x$DuQO-(C{sxuDnzyBz zki1^1Ml;svMZY00jW3YpZPU%&o`=IHbz?LC%fBowq-r=)KI_F=j#RRYloW=YS0X|} zk3e^cKABZOfRY%FiHQl1akFAv17Q=m{ajVmFrZy~|IVnQ%xfyXt7j|=e*ah&Jjr>y zJk3`VNOS|Ed7nV`89u)=`<27pp#D`n3eG;6mdLtIQvVMksi5cpX zJUHV^cwm4r`t(MJ!|((;XeUQ0)#IkK`D;zq$Q|j{n=G)xjIXk&-6`V=coULe3jNCI zR3U4siK%!R2SBcB?I+A~NVFjQ2cV4|4$4413`k1$3Y}-)K2hsqLjb$Gw6v6QI#9`{ z-3Mr%ZloK)bpWXRJKTBoI9>dnXmahR0_iSoOv^=cw^26R#c!)b~H4wc%{|wGZ+$89+`3 zl$fR(u6Vrlp;>g;ni(X2!6`zX^UlG$_wNBb10p5ZvZ*3wpV8=nn*raNXp}Lj8*dRc zS3qb8x39IDse4(Mp89_&DS%3iZ`+~ZAbtR8S#9!@{qYB9Ly%(nh$*J1kz!lkYYwP< zpgp6Sn-ESA5X1TIF6k`ZeQ zwcFZzc_osM1FryJQb3Cci^25{Kzkqnc4V~^|1XiW6ukp8b!0T8=?E1-q{)SFr@UE|7kmtcfirm@YzGD%=h*$+gZI4GiqG=e@_x!g z1Naba;>?1cMaiM>hk?|W{UFWgr*rl9K18|qgq2wqaz-%D)_C}~-}f&|rjHiWnp+#b zI?H?P96xZGG&#}~a>hCVna3BL>A+f;ud)ZFM>C08Uc-TVgRtWS=rtZG|8+mz0_i;_ z8IG|uMiJM)2>OiOBXvtOifJ{CsF_dDpKV@HxBW*fL{ENEf@6D# zn_W3bA)ct6bWC;t+P6eEKR%vXfS~kO=8OHfaVxdT`aV1kO_MXMHdX{umzc2d3kAk_ zPks#`6?pv9sGk2@CIvKeKLp|$*>{G=)J%cV)v;Qvl@bV#=B=EhpLpmon7{Se#dUoQ zC2_w^pV>I_KiCu8v;zi?C_PzuelkX@@^AZy7>3x|a~=C@TujFbVg#pg?=R(++=HYd z^g!o6G*!g!-m`cc_VAMRj7Jjr9LWm_ewa)k7Vy*2%Vq6|Av$qZ1n<_RsP)bm1|q_4 zmP8`soOSX&g&r(aZzbNceWVu(*2dTbRggA*AOe7z?c;CuS5{Lh?qL4O-aT6$GvJwE zwSi39L4Hw3KP?c5+>QmN{TIL_8iokN!|Q_SvLacZIQDLZ>VLAJID1n-R_qxA-!M8D z=L-=bsa`(a>Dlm)J5l3H2ff=KQ*}DQfs|gw8+zKe<>xH`U%Wg!f?wT%G1_-+iAtG6E1q14Kp_=jwFh8ILPv*>H~&%%tJS!r{$rgZuaIw?xM!BO05V zzimhXYgpE8aDud-Rv2MPa*?0Q(*Kb}XCDa_UK3JLIUQ6vy=xV1=*r_}T0Vn#otgR8 z*dAM_g9U-o*6^sPVB&8!vgHYD&Gmati~7F*hkCVZ4*b;u5ab zI~YOb;bL61Gylk)mBsUlAY-U7`?%cD-PzVh!jdWz16VlPyWXal{P*jg@oOKzBx3-e?SY zMp_yZkW!odAOl>$Q6SOJ1uR9Dl+~z6D!GTB839d5#X?QM3B8LT6ilA4XhREVF1!ZU z+Ha7OdYcm~4hj?*$FS2`tastsmAICh%*K!p7XFOpZW-XCX(%Vdc9i(7fjdVZ%Nk&M zmVSO>QN!aoi1yo^NXp?|vAfvRvat&k6{NZnJ!~tOyKPo(%mY6!1|)$tB$=}{GIb{( zZ}I;Hj-dw6dy=qD33EF;E-_qMX69imJvun)QJGrg;^q>2|Af%y+ z_!~adzemnqq!RjB{L6aiqL)5KZE;KZ`1vcMpz<;b0iXn}l}NUduV_CP8cZAeb{qC8 z)tsM~2Y=(X2g*E*PM;gR6`)M*Kc0K>t54zszd#aqML`J~kV8gV%ltozDNij#({ORm z$uofYF4lNX$_fB@LTv%)`>XkgQJnu({+kz1e{u&3g#wiKbrzHhOk%jMGdLsLK-vVX zDSx*zJcj>mHgDNU1O7h*%sDHoJvWWmaMs#MR(K;2ACbf|rqk;@68g^m{6$q;NtY4{kWbUdSiH+# zzNB);+L7Z(nVs)HxC3d?SZuj16)Tz+f%4~>CUjM;PI%p$a1b=tI}=3Cp$rU(QkvVk9e*{gb^Io0K?YaP3uE8SzTFO39PO;QH7U($32IWlQy_azJFLj4y03!`|iS} z(D$1YUcOwbyH477Z*FA+(`U$bg06f#u3t(@svSwG41Ex4D0~)59!)4ypnPw&{^mLw z0Sy#mII2WB(x>$Ak1F3sujf~h5Q>14ldXE4x!2s7zw7v~%YV{QCYkatKR-X9F9Mgb zhlj`NV-Qlgjg5*;93U$80MkiBjILjUDuu?c`tKTIKlZGMl`&gG$VKot(d$J1`~A_U z^P&|@Wf#tFNHL{#wi4*Q-v%%H@W61MUNU2hb#(+UMBb#5OAll?C& z(9{HS$jCEI6XT-VlaP`7IeIM?R8z&2Gx2N4p2h}jTr=irCZ^9 z@%Uy9cG^)GSZ3TOZ%9!Y&I#l8N$?a8?qF7cg9y~+jgf!dQqJHWr2U&os5sDU5_t%? zNhdHltaI{0&Tv6EP?nY|8pG9ilb-LZTa|+1NDb$ETcqHgB0j4!98L~=9XxMDf`0*; zC=(w4KofKHCCLPWxs=Vu#-^xfG5iVsAJHV_SqV-#&~o`=Y8#AZYjf6+ep_n|K%F40 z0BTyMkFnms2iyiY%C^1i)6mcr2I3&Hn6O_!c>`mYn2E3h%<1B%5#WbGpmlVKgk&>F z=&jf8OxV{<_=2Rm!$w5)SR0uK_bnt~-88|k0V}AD&63db=LArp;?!`fB-(esk~#s` zDap+Tk}l+eFfi`UG4rlkQx@youq*4@WT-_1y!>l8yTt1)klL2?U5gDIOSu zpuVv1wfjGx8@je5-GD;W8Ka$zT!%W|ZJq_r0C!K*kV^)2g|rcCUb0DMV8)-ho9DKl zY&rAeGY{D6SC6pGxmGtgoSj9>!WUK-KHvxx3kwVN;kE+lykG#r1-s&0tJ7J*dI?_S zK7rWQU&E1p%LcQ{VgZgi(|QfO)qpqdx;qwH=aO&RLCSpTrM`idw+ZArtEXzpfmSZw zfde}|93CDK0Xm8jppNeD`a0MW{nu)FGS{wnOuSR=;u7DtFgJhj@Sy+~&R2jC0svxm zhMjLC?Y{4Z!}e0GA2^lXJcl3qdS}ouTshM@J!dV;v(K+?SGz&Gd{ZitwmqO;)^ zbWd&BO?7m7K@++Z;eCBQbBA90HrZ}bQPKU7Kcus30KBh$1qe{I6dLB34^?BzU}UhG zu;<;Zuj7kC##uDk>w$!uUpX>9ez~KoDAF2~M*V$#X3Ijr4|q`-rywE`p>siZsr&Y; z6VG08@Bc+M*iZ3Y z8?AZwuBX4>;<&?#ev@NEw2(;jSTNR%m)t5;NAMommi{jetr;b+D!KW-?fq|$Zslxv zIdNscP9hEh1uf1`X0IL$Lx8az@(#*d1my)+FlBz{&N(?)NlA%5MSvN_tt1o*B1mlQ zVqzl1#>KgTAvw7%ay_7cGdKTWRRVnyh+!p~+bh-0+!Xd+vlKWHQ#v3UI%mf?3JQ`J zLFV(WC{~;CaD{>c*4H;aHg?!oT}D73OU&QQ%q9IQ!IEo19X~J3`>jYHhex0(j9ciz zqV+XB+u^+d1N*uP4z7eg32{*pHkt@KLhcT3FXKdUPN8 zZ|dwQ-b%a{@EWMK*Hv}4?4eF`7|HFHiP8wzmvLu&b^vga#j{HB%E*}ag*Ft`E!mGY zPPW@B;)z9GT{SSd0K5@<-rXUe-4fh;u}ZcE$ZG_(hTyMnRk!o@JZ^d~-UU3s{u0s+ zxE6Gg>u|@zW>DAk)%Y*wLl+s(S_Hs17@%OirIN;I#>K=%c0%d1_=JbPydKj7Qivgc zE`yLPQAh`b(eJJKJi?p|XgvopTUe?H1~lqp#f4_PY5>_r`Z{v40LuISe9=~@4|T>* z{QGSi)gZwE4&sq6lOG;VQoqjMo|k*epPoVd*;^n)Wy7z$Af9@}Z{nbi#@aD~z}slu z`kUZ%-~6y|72SZg-TMsw8*QbnhQmL4YeL!G{?L5=F#L-eMz`}cul4V_#Db~mb8U5G zs*-d{Wlwyn88xn@2A&rgW>V9-Q}O&QZ>y+AYe;Ok+MNozfR+5;%y%xj>Iml-2^G2f zjZo?wp7i$SQt6O2KX$H7+4vh6wD~xG!@*C^Y%ciGz`0@OGBBX~9O&LB?J5cE)~jA? zcfR&-@!u|zai;R&^&*Jyog(Utk{6mIRk6;!mpKm%Db*_<$`nD2FBCz}U2>%yl_heST)W`-?X^H2@@)mN_~Qyg1sG&Wb``ATkdSkL^`HIFd%YaZt%wQb*q zaBMl(L-z(VTrm34b)+|nii|fR2={Fr2K4}T8n((QEs=ccJXbxCvpcxitep^~fT_36bvTXXoDiUM3 zG0oJi5%K3X3}97@-j2ReT5!$hTq&QcPLbdXtTjyf5Y?7KvCtS7%meTvv)lf=MW92F zUA;QhD{Ni2XO^Ag()%*J>-^r=XR| zX)^lR9h3g_XqN;E;nB9eHV6h%S}%N#jv`*h+WKX7E{9vUuiFitnn9uL5OOap82y+! z$Rf|I@PAy00l1I=ol03YRJvplJyF{ZS^wP!M=|g93Pjs$t|Z(PN;@vh zsQj&H_QCuAxML4`)8tP%HcnyOqUcU`(X(C~+K1U#dv$(=EHM@t~8xf44rk zjh0D)iEz0wBo{6K32hQ^PH*2{ONmkf^DO}W1}!3Ql9PRo_FSc9VVmu_Ok>-YZkA|N z`Y5ol_oay~FDz^y?l={vt498F!p%)9=c3X-*>-CRb-&s3`QMRjE=;f)KDhkoD_n#W z^tyRork$FY_}aI6H3WGpugZ+qH6FtmFAW7Uck25N~@QQB+$ z`E3GTzO}m=(Q}S1;(u?-bCF!bmS_@3+#7cez2!PcL(rVl9WQ1g*-7zap&<2c*g9t1 zZbX|MP3%i9H;qIIeB%I${U}geW0*pL*UA=fXKq;Q1;@u1KGVQ5!f#iyK~dtLFwn6( zQ*ivG%$Jcp%Q&#h{$~^47>^i}KVkd98jT}&6)V*`daG&M{eEOB%C`F!*>4WOrg>ox zHYT0g68X5bIm9Vopznhb`>aDFtnV<&4a{wAA-bs(c&Rhwc(PP**!8fVRvwRs z40H^ippc`|+`dv;->^M(UFus1rtqjZ5+}f8%Sy<>0MYnxJ(k4k?p>smI#7>J{{0&K zEC;K5=60Jl13KB-7iL2sH+etj=)l0pFcj3iY=cT4vwCorE%`<)ty|hU=3FVOn~JU> zRCyxnd_@;7eRyjBom2jCHRAcd=sJP4A$U9iHO6<(vM&V;Jzg!%W6p|-OILZ+VbSd8 zalk+U3kwVT^zzr|s7m1VxWSJ87BW5rmkJpU#K=)%pu|KJCgrO4hHln%p6aMyqNbov zLjEcF$&U+VDQTssUttYmsP(=C@7uFK*61luzujo3;PLQ2k#dNqMp*`{R|;q(Auo&H zyxB~`i4_(AUC|lxu)Q{6Loj+N&OokAs)hi4|A5>L8_Ocmh9IaZ#VGkKk%2r!bJc}9 zkwY%}VG#XVJZ^@z!ZU=taa`}?Hi7rFVsd5CJUd(z_;Xa1j@QbZ6~VlvyLt5(i^RwH(`=O z#Mf>avAnBhUK%_yDG`d_zi>j)SlJ_?@|?oPV@iqTN%JC9z0#Ks#mwleZ96z(i!xS* zXa-d7Czo{@^Ao}<>11GF0&d?JViwHC3D{qAZzWDTJ&anflGA~+_h5hs4e#omC|Q+)Ou7#P?{R{QJ;u#Pje6^P_XV@B-CVCLVS zj3h@z6V5jUh3DW>#I!DsY=o#hq9#$isJq7bR#kl z^O3^Ze`+_5=DSIpp<8?mluCo184D(z&ptmVU=;O>Ti`928e`@@`k<%?%xX5%b);rF zUI)z@!uQw}&EfDhFz-^-1M5--tOceE1v}$aHcn&mrY`)Y%5Q$OHLdI#`pQ%P9zGJe zsQ^Mu6%7`{aNKin+y(Qo!rFF9NyGy~L$HIqqZrGs=UThKku?F=F}Y;Z^2}Q`5jq3N zb)sH!Z+~ToPgyxw`3?mPH0?HTl9yuERy0op+B2;Nt^;9`e$$@7bWK zSx6(H!o9MKn4y;RZ3%2Ct&i%V9ADvKn0FPxz&`~iz9+looBE)_yxp?z_PUi8)-_R; zz~Bl+St80nW&*=N4KpRfG$6)I#HevUdx{53DZsJMS9jRT{`JJy_TzB?x?XzV$9H2y5 zzI!;Xaz|Mmq(XaFvFmof+vm!`VrDTN)%gl8ICo(TiT|>(nA(k@@PDM|JbJ_eg}l=x z21IH-NnyuHbKv_TJgyaLA5ZagN@+E;^Is@gun&CahixxCf5^C4C+l*c08WQhzB;IV7q~6V@u9kFWn>v(QC%s` z^F0;&-_xw3#_}RgRl&_x(Un3BJ{-AV&gI$~ObyFahFelDD1wNH@W|2aT0ZUGY#x}N zp}l?aonz#)Jon$;eEt6{69x*={&RJE@JsKCz-$(qfX82Gi{G@_?7fl0hiv%%JM!17 z_Bp3F_0A!pSYGS@#d4eafOA2 z#p6=XBk^l{TpmB_y{2u60RdF6`n{t|@811rof;9mUvEOK=aq0S3v{b02OexFKF2gw z?pJQ@mjC#jkfjKLfEVfmo-xl=Je9pvxvMKjOG4U{&Gfd{Pf-t7`=wNf(~K6^JwT0k zKWIn8(Xrsrn0e2dduGIAvsF7(PvS*@|2m0^lEPM)_{U+&f+D?kyR@-`VuQ9G=q-lk-@c`@yQf4FeaK6)=zF%aPTy zb&~bwm-dxqz4e0t=Y2UE|DLGVo(o1RZRIp=DLMU%R@x#>OmDu3!ZXvdrJH zqZcM#Xxd5_TKYzCX z2DFLe+&BHc{LfYJNua9h@Dwk|OS!=Z9Wk%dMS=DiTm%D?){@j?-He((JFpN+WF3$e%rx?p(Yay44l_8Q(g7pzpY&SUW+GDGj5Ez9h zL&I%<)sZMG5PO4!T*Y$?vo4+*UmJ zH1cC??1`nCr^LhYj|ag5yb@`tRpywXp`nf09_GW@O)p=NYZ$fzY4j$Lqb3)=aWh{W z@u<)w)gGHPj)}#iZp~uo0XrbJk0weZDDjCMC1So-JkZk7IZbIWCS`RjCLy+bVB^X| zY?cMGYZ)JOB-(GFCGqx0zfv>u-sBd-aKC!8BeDJS4V0X$zB2H+Ds6P9%+GNt9ARQ+Y z@TQzSxi9P10!&^eW>&w30bQQvsQuOVqRpRG8)`AIH*Qz+`)J4rAkpvP%D;Xo1!6jVCok&M)F zWq7ZLdy8r zM(rxgi3e`Fw5X^EHav=YUL0p&5+0VQ?L}cavBW;eDK`
)Y?A?Oci(cJ1$P+1 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


HRFli1n)Sze_Hwnw*UbeI*%8zTg8e z@hAi2g+R(vbpH-UWoJ)0irFM|hnE(C(hxF`EpakL4H}6~5{k!Lqszb_GXb%8&^!vPS(x$rI36DF1ee`>X{|!}=KPN7zTi?#{ zxoC2fS(sVHwuR-7O)|KQ8w+5z<>aKMYH#M?z`7p)bjNau>QhnTW{Mlw}i6 zvZ0MocD}#y^6!uT5aMY^HdZ{*zj|V>pthP>QqW$Vz`6jC_R}Kj7fo2xqT&lreokr4 zp;HX?w$7-V~cst{cNwRDQv*_<}DJH zD0)l9cwr!&_xGfvg{c6tdM)EO%#)VF+~I1(TA+_#`0F(xtpD@s6|%8y>d5Zme-%6+ zBZ=PK07Yzu&#nTDWoDW={712%oz$&jxIReQX3k`p>pm{}Vf1T{OE!az=1VSTqaQnmK$>%!c;+_PiaFVu6I-&0@WFTzGY67f+b3J4QzXd1>=CM{l!;wHf@ zUi#JJK%qi1Ob^!M6aMTJBH){h5674_epUTdzLmNx%Ghi8bALLO)OEd2u8>Q7LcIC( zBH1Z7Uzx?vWP`NU*s7QK=9QWEfSdl%qub? zf`NE}=CLBsSO5AS1+;H(is7xkXz1>&Y3T;AxsbmL>ChhEA9XA(r z#3T@liXr6oq18~S7LoT;-J_q~w8&HTO1U6hevuC4XLu5C7t7;Vc=)bJ6L462=W0ge z^%SUf!s`jo6FC*l@l#fui#85%io)<*JfLL~Rb}|nV zBEzaRxM2<#lMPgBGwdA9gdq718zRre|<4>=zC z6Y{d%T|T}c>|@_c|DWe zy{ih@r}P6N49=&)k-uE?FYNJKF61c4xBdH2ITmEKJqHf(xR~Dvm-PUF2&)Us5+MgD z$jcvXR1Ey6b~v5y!a1gB4M-5qzR7K{62kxA^rMi1y*&zI%l8@;Rdh7pGFBRj3Dz-kvhVW@qUVKr(u zvMtKKRtQXgo$>FQ=&m?>4yb~o-8cx(yQ7N-nem(x!P;b>Sz#2`8fHqbG!#ew4SSjv^>#qx9;P@ zW6pU1aZkJdKXDImbJeb?_rJy6H+pUoF{!{JhVMHI@qT@h(E6Ej=wKM0%-f0U5m6KL zZoF#7;&EK=x*kOO83wP6Y?i(2KbwJb7)(VIhXkT^6qJHAgt22U2 zi%zjFXtb0SV^S8ikl%pe;SQ_lYw98SZWKMk{=43~yG18vzp%(KkaP5k0jE(HYQaBo zO7}Eqxj_#A)k=ogLvc1?$Vl&P6?pLOUNn44pdIEqQw?6e^c|VTmB<1 zMHU(Z$6$LS{Ep{QSouJJ-ZF3vLf&j9n)v+{o7h0*4)Y4jJOen$h6uLB0hEwIs0wui zX}4t`07UfeLjSFacIy_nNsz+&M40xfW{7{tvnt1r)z`$m%-6GZB-=eWegYL~keLLA zx4^_Q0T|rOKl=<+;@+scLA+|iChcf*Zf3p2R2OvXlS-*QUBu?lM{KWs03NLRGDwGj zS^w)t)vsS;nk2IeNrQxX)7MCcT4fBZZVAg^K{i4s`fr8nW~@8o71JpwiR(6dp$zJ#m$Z?BVS0o{04dRFbmxe=A9dgmDBV zTQ(9KM=_I9*qVXzLeNLuz1sv-;ce(UnA?*Qh*6~s23}M!r5hPBvwiB3iA$US>;f@X z6TSCQD)5hYUQ^s#-e9@6uL`uCU0iI2a$TkFg8sg~ikg}bc@kn`K(KL?^fmKhMkYO>@6-OV zcSlskD$)hZfOktIdXJ9vW@g(WPK+4gtxv4TsM2e&qyPQD0+4t3ho2ICm74)p;?9!B z%Eog;*W<}aL$Lbr^*x`hu-4Ec)<{f}4S&`J`zsSD>Owx{SQsJB+0E>$Ql&`b2In^a^3@E|+nALd)bG=Ovko53PX1 zKxmJ?c>_oTd@Iunhlda4-YagKV7z8#V(R=`fh#&3DCmw>PfPzB%4#AAsEDtFLa>1S zbj;;zPpBWkyIkNdDK3U_cppp8HrCc2nwgCP5)RmC!qHaru~kKKK6zX%%a6h$Fs#ZKwfy6U@y4i+pmpp>^`IMEA|$;NXW{U^^Y0 zArwcB$6)-=!bsya4|ZiaL;~KxBQ14caAcgb5_s zVu0DmL;}?XP(JnaJc4>Q5G55ZI)Uog@%)YL%osI>i5*l+yOWlT_A6_X4A ztaowGsi_kxK-*z@V{;P+W~lub28yB}FK-IJRX>n9vfe`P0^bqf8OeCAV&Gy>RCEjO zXUBlR0nD65rh*34vYquz*MVA*cFnIO91U#j~J_z$oZe!Anmr zpoxV#3Ery_}vxN!i7ZzK#tIZNOGR5&+CeEC3n+OQ>F0W zSluwHkfGn#l8CWFNVI6u!|5Wp1$YWmA1LrFTcNP?SMu1f)lTHMBsXVm=&luA?76T;1J&e=jNpzBK;$B@}e2#k|VE z{x`nL$NgF-vsQm3NVvd8!!)fct9xfck7rPk zi;QY+tfQjeD|-Z30QF)J9RLJ(V~lC>ClDoNLm~c9Rn-Jzy+Zmqaj=4IA=5H7<+T}O z8ll(Y$bkeDfEL&sCRMhHwg0Sk86qeIE2O22tXgmj?=OF8GD$w3dO*8K?1(tgJ>s>#69gJ96S=r-U`r7=|r$2!mF2fPv zLQFXkhaJO$OO~EG+ai_GR7N_DiH!|ekY>{fvhzxBEDS12oZlIJ``nv~k)-?_LoO70@vyI4liW@U-%5@g#d)>(Bmi071t3lMK-(-IBeYYM~Li|W(lD5BqS;4}@$_3f!7VnLv zu zZ%6B1Q^^1jHfsnwE(Z>P!e3dmuY{@DLU#_QVv+yoQ}<$2LgsP;AcI`r(O zJhK5oys9s`bLi^_pWnT0S~DFGB-|FnGkR}R@b!iAO?RC0L|=HMg<3>T8sR>PB=GtxJ;S+&G_%& z)vqP3vVM`%1CtE@WvOipXo_( zlL~cg24fOXD?Z1O+tVE`JXhm&(yY_BWXw$WQrE5=)fCYfA(oZU2wXX z>%C#spI7j^Z7}u2&Q7CCey=9uo$U6uzi*-0i4)O8QvHlHg{+GX4XfNe(}=IeUQ=YMfBwnd4V6ll z_X-NlpHE53v$^0XHhDAiG#dGHAijDH(9-~t`k(I(@HDjjbCaUnf`}?M;!=|GrYYlu zkj^|eQ?-EN!QU$e3y^>S5wK}E7Ox!MYyZDJEmP_q0DVE04}4K~XQ#HIvH z0#;te9Uqno-*mQjD-y6T0EGq50TU7eKi`MJx1WC_jza(27g)eJhbc*;VEz*wO0T*u z%BH;O*%L{=(5^+tn~D<)Q_-(@h-VKFGTm+@FRgUM;=F%B2D%aNOj<60zl9kxH9Lo4 z(i(T~d3JV`ZZFgs-$8zCbu{vN2pkW@QUencAanCvQAn5bGId+)bRB5F?0#YBI@S$gD8$#(i$Yrqu_B^1~vds!;13rq=QK8MdyG<4Mj4PaxS07|Oj1-)Hp)&W`L*YUo^s@=G+1CuPJcT^?b zF<%3n-pNeL1hH^N#;B9d<*Wh4&ynTQUcL2`m;&WQlmgHo;uK#&S zP_DXoYS~rJO}KoqTj*Iw(UVoM-r=8KkQS4kJ_`a5h{N06JyxhSj`@`AYpc)UmCIfJ zgLik92g{OLiJl_l{3c;+Uw>0Z9f3d8t zeF!f=Dy8*N&gJ2_+Uk=T+0EL&t>!_3VZUTG*V+ONLyk24D-YKZ1FEX40SqiA;*QmZ z>Ld^Q4!TXJkhP-O2_7ItjSy^jQ2w`Gn^#kaY`h)du##9zoG{TRl&T`%{oIm?NwFlLdNBMSN zY>uS6&G1E!^KvJuaPdMZC*@7YM+oCV^RGp!k0X~7dUUR9%79?7Hw}xkPDm1NTL~^e z|8v8(aKm?a>`Pb}Rt=!l0lm%KHS7Zeo6Mx+PGmU6glD9jq)B!bdq6u{dtnWQ2*QjeWPC^ZXiC>?6W* z!yX#d4CzEh#P6m2b@lb2(?EisFdht<0Rf3`BfBbvfV@)ro=YJnoJC4z)-Lscf6$u8 zkPw-G=a@AJX1vYayzvX`>#t#GSKOF2fRB(7!@&-M||1P~L)`%hYF^z&H2EtAej9Q2fW= zaNMN7efuFi5QM5*pub90$+ZV%<}m7d-%x`iX(%+-`Yk_*0;!Qh|I>h(n| zEG&Rm(Rrq>nvg8QX>DQX(EYqc^`qkll>^O>h3y_o z(!UOdERB$e$jsJu1^^?K0%oO19DP4LOAU8Vj~@9n>psk#;AqQ9a+q&6hBO!4qrwMsFy){6i>K3KGafHq8bcQ}t2Fyk1XorTI#@a0B&IR*0Nny+dVYR>(N>NQ z!@s>t4pH_ZLu@Q8K7RfW)zwK(Eq6U`P30=};q3m-!~Rp5E{9m!Hkb3)U*MYlyw60* z%7IdLW`Q-F_z80T7B})t&@zQGL-|*hUI?C zv#%o~kdFndtf*pgTR(j#sHJ)y(X`!M&3{1Ye$(LF)mrU4$X#LNGC4F|67HLqm)ozg z?k9XY?Li|&)vU`E5*%DDUGw;1do>d2RDO1(DA%P;@t!PBZ|;rDj(+@+B#P?>xDjIQbeghd6vH4+NH!qRhvp3Iwh<@{& zJG!$gVpM}&wR{J_)|ohWfDC=7B5jNy>j9(~Koo${*tp`g<>)oLx5falQ^wdD55a^EV9a!!g(uJB8?D=PjR{IGu^h$_(Vi&q0D#p-9QX6+Aq7O-^1OSnBUA7M4rG?=XatRy}2% zhbN}Klw#Fun!mm#9Bz$uBgWlFuobig7WLMsKT~|iqX%CM@(f^Nym*&yh|6W6yz>ZG z5n=ySn6a~}qo)UfhVHNf8^_p4MIFR78xHfp&OcW)mya5P(u3M8@~s;C&-^8pFYXV8 zm+dXAIT$EpL}|^isMKovb{IafW*UBy_1VqE1z_mfQR9o5!nd6=nUPEg9-%9`#0{)+ zBr1A}+9zDE$SSYP%%t1TgWi}WBvPF>xF#?+#cSEQ;w2jR&iiuq>tv_6{XFncaggy3 zM&)UvuH9r278Vu}F*sf1y4y)8nGHEZ!aejr7(9*846eSHKxg!>gp;*Wq)7S`V}c^{ z{YVfwcLgbINMlQce#75EWb}^Hg5)dZq_i|BD{d|?lU%);r(4AQ%e-wZc!tjy!l)!a zCoS!Zo_Qe^yLjQ9Z|%NHZ}i9m(TE5MO^;Xsn+VJj-piD3x=>K-20Ua1czwkj5X>)l z^$X94>2^%X#AISTg{+Q_PSZw_c7N)nx^_<1ccKwbVhq9X?}nAuL{SyI=B`)t>nbXK zbYKV_vfsFLyeysbGXJSWkgr~L9Tg`V_}x%@17Q(Bt*3)B4BGX#VPW(@bqDI*)%R1N z4xv7bKg}>2V4qw4K7C;x0=Hj4Tk&2x`h6ndW*(r^i6=K^AD)ylUN^b(tEWNFCS!mC^J-@bp9NF|I(Grz z1(g%TBWGeEMK}U%5d=NFF?hvL|30m5@;C?_d;wwy)QY4@T4>J&2iVPyC4QYZXzGMy zGvMpIykZB_EaQ)9CO(1mE9E1zet6dyLL*{QL;P#ELi()p+6PX4V>eX@UPiNUUEh0Vy>>PLX_a@;c)@{u%EmBICsY3O+S=TMd*~hKK^apX7K29 z@1iJ-p}lOp49aG}(|K7ojRj})LQ;6((Tc9xLk|WG%3o=E1!g*at{7(X)|8z@Y~^ zD|kCY7AnloibDzM5K0|E514?0cR_LoYl;W?((erP2SVhE1q}?0s} z2~EC?F;T0*8S1jfUv6PwJl8XMsb&&Ysrx3WJreE$)s&i=3}I)6sERUsSA=*=W<*G6XzGUB$STe>-<0fCudOu^ z%_odFZGm0~eXO7+-nk3(5+`P>3%;nk48cHqCO7dV>huMy>T{#4Z?{Y(hcLdmvS7nY zNKIG$+UNf2+w#KFabW)QH{tUdf6k|x*nTH~YC>j?I)PiKvW`10jI(?T10gAW{~2(r z#&+wJ)ZZ6VH@m*6uwM~EdlddKib)20$%T7_Gb}o~*<}^m_Y%S-!J93ka<@^fW5Mfy z@A*(sL2KWB*V&Cu=iTRo@g2K)g1L*2Lafm(d+j(N7&-VM8nzGf%YtHNF)?4Ll7rpY z`wfb3**Q0UJytDv{fgv<{UU*R;J8i!2a4yg;~UW)(y;D}ya>U%@ zgcWN!U*%a+cAI9<2-MDfsH?gZY;l@TBdrk|8YjQ5L!OKF#=d>cKOuqixT00(aZuhw zk+#lni{X~n#G!C#BOlC#N#@XHF4N=@XueA5P9X#m320i4K&YSks=iz{8|9tiP;>gB z|DPM)`|*P+hv35_qH?k{6zF?tRV0~fC&a4iJuLZwH#@8RaMNetza1xv$lpTe3r2tOvJ1W=heHs@CD0FMG~s^a-DBmb;#fwX z!^%fyewhNY>T|iuF0QVNdX~dhMz=6}E%|%*M0Xn*7u^)AzGq;F+fqunJ$f_LSS0)X z^DIPu?lmxrL2oHmQC~j=xCFJq<1knhVhZ1_XDKZA#-=tLzC7)u$?j88LF(Bsu*}0o0EdH_OW47K9~`e>B^IFP_!vix_9p$ z^e8;Hrn4F%>kWUOE?!QT;vRo@*|xR%>)~N>JtmQEWeXo5B3&L=r@UvM^h zKjE4jBp628i4k{itH37p^k|pI0W`UyqN0XAe#9prAg?(-I|H@+?>o~g8UpmiCN0+! z#DIAgKaYA>IY>MAmWnEiodPcxYp$jUzz!Y`rEbqPH8^E`)3#y>V`P0@5Yiw1H^;5hxDXm~HHX=;TQy3o# z%|nO{fbg|@T3&s{S_DOXDhU!z;5ZBe)VJ@mkts9a#)@-+*~~DrG#!TGLWic4o32nO z`Ub{Jqhy~LZ|x|GNiiu#Uh4-?uC--*z&=mZyr-zk`OS$ng1v&Y*?d|Qw#n_}ImZ#Lx^kK^u07cAfk~Vm? z@J;VW+mlye?QkzB8rHg7uPeXb*{l8Z?Z|dF6i#i$EmmZjH&8E-VtkwUBCSXy7UTV$ zyv|&cfR3@o=%F4dNJ;~_Qr1BW}l}r@T$f-Q*{4@gt3a17pPvyaw2#VO) ztN!pp&FR5OQdnV1;|J9Q|>G;^o?$How7-{fv zHpw>GH@tvbJ(!mBX%*K*!ZQo*N$)m^z-&6;&_lfeT;M0r#s@WxEdv?*iM7^=Inh|u z>orUEhF_x)oO7q<^8+Q$wIP8pQW@xp9!7#z^{b2wKgfRxHCGTP~ zT0^%8BEHmy`J^_|G*{<$C#I1yq#qJz>Rpfl5@oJ@dZuI5{N1D#P5jIS_ zJ{u6!>Bi00)lLLQ8NEp!@T%sc5sOb0M&zeW02^u$r#u!^$$0n}`DIX1owrutI4HHE zr8_{pcC|Of3MTCa3I;+`^KwPf+Lp7`Sg2lIPrt#%H8?l-z*|>M4qiYgOOg=c`(l!g5#ML5 zx+Fjzims8&$@zRY{Tmn6iAtNnjDc;sddXChMskUO%D}J#%FDFN->K*-Q3aas9{6kv zLhw`{-%*Cu76=0?Wi1v0XDC zN;{k})y!JA_x$G&i5zv^{uW# z2#bL7Eac$3w?+Kk(&yKf4a7xA@`n7#5CDaxXI@;Fc>f4Zp*HnwQS~*(?f?2t)qOahAm8l7o=$`;_cQDGB2Twx0Q8~?tjx9?i3 zeQtVpYrdsLHuCDyy{~X$0-p^^(G1cGStd`f8{PUY-bYMGw_H&jZW`eP@egpmw^pTrSdIZoBXUXW@ zW4XV^1@Ufh5U1d3Cv?GSeYW_Vv0sB1uSTR17L+J6)Qpg!-C|@V`ja9ATTO((afj!G zJBG^L)x-*`fvbw+og0A&E5KPb)T@9hD@$2;eU#4qYn^LP=~YL^U1%2r5(29kG)e#z zq2uk(0ACAR55j96P>4^cfR_eCw#0u7-dv*{r%+cyJgT~C6m=mQd<_UM!(^oPWN6(! zhh7&3Muvzh`m+~*eZ1~cL_~zdMNVesm8wo;)T&C$KH{M~f)zIBBMT_BoNddf)ZCIdnu)2-A{!X6E{fO90_#E5Z*^^r zC0YvfZ+nM^VEo9>{e3JP92AU7U(9>b^qPc#{fOmH?xijsUk_X1e#(*h? zFy-Ndzg9C`UeI0~G^cPR9xUqG$F55jYTo2zWk)>x3Q>|aIbwIPhk^5Y1}dEZI6zr# z^_&K!qDMdLJjP2iJQOL!AF8?K7JiR40o#fJkEbH?36a1jh_%Okmx7bVfL0E01bBtc zZZ{4SU_dhf;dhpo8JqO}300X){svB+4f9Jbl~`C<4`A~9n>Wy<7N8FXVRoX1IjSn3 zfa)GBC>ua}aOoGjrguIhiz)`Bc;q&wgA`&j1VCn6%pdO8h-{_n)AtpjToD2TG^L1{ix|Go$K9 zXAMRYb_Tb`=E`p~{$0Fos$495#OY%=8g3vv7~hn6nk)Dm(cA>xE1@#HWk(2_a$G z_}XxUP_O3y3{a;#Vq%al0JNy6&=x%kkOWVBd^);elF-I-7`^3vk*`aUqu?v&OF1g; z{UUlSJQC-xLUPkdosc@X6@!{Im_lk`l@Z%J926l(gt=P~GxOV(o zNid%%BF==!wD1{o%;9|HOUK-H^;C{Tm$~rH?1LFz=gKQIh_B@#5J{Wl{PtnHj(sc}B5JJ^dAJ~{`p*rxVg5~xkY9dD^jvCVf!&OF z>JANnbu{jGQ|U!IG($Rb=cpUf=EZ+MCWFaQWov$#owqMtWAc0F&nfu(IUpl{g(B%E z5ZtJu7gE}_`4fr$duO;ISFU5wI6hBAGV&Ao)yq=`X(p6IaOE}By^s1ZGgv%4mRZoj zFl>KCEZe^qoFkn0^u4U(vfSj_-p|#yGopxtgNRqdmUK6o95z|+yr%vj#y2-Nhe+OM zaH6sb?sWvDBbH5=4f88u>vG=paI}|SycFiO-4T^E6mG23>{{2B^;bX5cI<*m`yJa} zLVE#Fql=4+FNJAf>uvRJfQ4UDUVv|fd~obF)hyvw@JR1sCS9de!Ta}bp1IW-&&Igd zVf@@xH|CygGWs_ff6ewxkMv0SY18l2OcO;1+cB6SQz9Zk+VtdbHpuJ?NV*Dt_{=axH>kTA zGdROkAb^hmx)pL@GkI0pDu5<>L&eM7PtMN4DV*jrR#t4s-MiLOVG=bY;!tyYi}q*Z zy-rK|nTLRe4f%noDr3wCzHwb$UAToRXwA5NLl;67r5A94C1sR8-gq#VZ;)2l<=zIn z=nCv2rw=^;wYB-^&+n(qvpCN}X}8EA0w3%YqB8wB`kHVI3BsmeplLW_Le=>)Re0xf z)MAL$L23q~?D2O$8XG^|eK16AawJ1$696F(hKo00`}P+C-J~hs{>|dE;-8%RTDp_| zsMsNIP_boEh+9Vw-*g?LuCB7MjNr}}7Vz=~to{7duPFWKo4gXb6qL)~J{}7er^*-e z{TK-4Ojrj~;RxuSSKCcFj5dGbZlNwYdxjJdAxz!7aThb2_xm%sXy26HF4?o1z5O3P zDVgqecd*cLy;3qQPFJ~L{RdX8;RscK}nf>>!&b&cY-sD$Add3$19pod+c_v4iFhV0}2I>88$tnCJ| zuQ`cYPTzgqs|g_X_zx2P*XKy(X{P zz8>+C+m+6qp2GtJ<8yf6weeSig}67>{Druqq#ZG#Fd|xvx6>?s`0KaSHqF8?GR>9a z0{7FEuxqeBZIYxW#^Sau{B&D;e%KCI-8Lt;LDTi))FHj>|H&daU`|~gmslKhUK$;x zzHx6Ow1rRKH6b;6@4H#XzfcvFQ8*!Z)0C8y)C~eA2;o{T3jI7tgE6Ul!-EKq{euEW zuot`i{C3k1uzeRI-EU4#!yyY)CADdx#(0^Q(tPhwn25I)EI`obfK>zbwDW)MNmxC7 z>nhd#PItv8XgAhzchn6Gev}c5@1fCSz}~qYW&Y^F!|~&+HVkpy!m25aCeW!ugi{{d z-kZ8Z-^JKCmYnEV@ue0}@Uf*x+Fzf^F3lvL_|3-7Aj#v5AS{!6_O$D^WyP@>$uFLX zpTdaRBFNXc?auwK7Vf?xD@%&M(s>utw-s@K)U^{^y!W)6x0TrcF{T$U%)h8zi{9@r znW56k)l_6^hNhsF+MQ28#+yGlP}jEn$F5FtV3wdg;Q_AwJjjV4N?xj<{2j9Z_3(`i zP|vwj!6zgP{^GWNxh6?+yv0@wpHDWYjwZ>Ei2HRexo|Xxf3CyXz`?lQ2Df8c5;$TQ zBP1+*41C}{3vKNQASkW4hzKY`6DUg^6!z3k)gccs?NU^^uiDZ4#^Oa!xPOty#ioe{ z+|yqdUHpJ>DX{Us;@khKJ=tCL8Z(`o39)u{E2~dJb#bXTnl01Qc>hN9Iot$}3qJ|s zrEXpget7qR3N6)4F7#EP`2Oi}G6dO`9s+jE-iU{3(lnC)lRZFoN117Y`T;?*&Cf@O zUAZ}iQw6>zq!ju4Y9N;GY)}T$J@EIz*k-cbd#la{qv(qoL>~J)23jy5`7+Qui#lxox<6ft`)h#7Ik)r302>m9RUif1oe@xab5D8L+4DtNu}7|KhAHJ!{J zmaM^Y_!wxT3xph;At@=T1Rn=vNWqt;P^10vdhDX2Uj_!0s69Z(KG#R-U#AC48LY0Z zzK3D1(`h82PCcm`Y>=5w5r6_1d-#((oSyE5>1O<|x9KVEd8@&O=GbYIafT(K=RsOV z4Fe<70Sx|uG0jmO;%uyP5P=jh!=Fn3dz9k;aJ^YAA4)G%yD+@Q>?e{Mx|1ekCMvBmeb!N zC8A=}ClU>eV2i-Rp?k_+H*B`cbGGGa@Nw`1`R69lT?sGlhUaxRXNOfqJwGlAqX)H_ z@>1abfBHXbLeOIY;#Q&dJ5A`Ofv|HARIg84X!y*7!^0H-Qw~KofIwvJy#rkW5q9Wj zhnPUJwBT-U&s9DR!25ka3TU-TDN#^LhOa^vUjzw#n@K=AWB{&v3wO~_9!Ma--V0L* zYV9WEAC1!q2?;Kj0MNOa_@#qQYhvTI7tBIkm<0VR!^NkQq8s?5qoZUT+GRj3!o+m) z^aPr9o#N4ZC?)`t;9zf$S3saxuXL{E@`Ce{ohJvNy8spo(vrOg6)j+TngK&#B3299 z_%NOU(mODDb`u3icZ?u&YD8Mkha05J2ezcsL;L1XBbFE;oR3iyBHDb((^d*XU z{8w`W`9m1l*_sB*jt3z>q~d<}hsGoCiV9(hD$PiG7K!z~4x1R;(LyFIj zZgp{zxXG-%^eYtAz*z-`2h_HU)nU-^1jAV(?HW*@Gyx6+o?%Zf*t0bc5XE$?>z^}` z8~4F$edf(~FJRAyI3C6c^^cAcU0btk`7o!@*{UoDgLX}Vf?Os`O$Z!MPfprO0V@Qg zfkMsP2pFsb=)*-hP%EPqN2rjsbBC(6;jYAHWbl{(9f8~6Lnkn?^YnQYmUE;6pPWI9 z8U#3yGTsAOQRt!6nA}9N7U+X4=MnJ zNXNlYk;gJBB5m>`eL(dbCvYwHB<2Kc#rNs*x5}z?WI!HsacM=xO8T|81RABNX8@Aw zonHzJ3OEKHeh)76AKn5Zdnx(S$TPw z^fopzQ3<|~mnxaZV1u{E(NKy9BVsA1<38{i7tsd07X=4bo><%(v|(pXB00|jnJ%bM z1B7sqMn5(;FRwp&(3|xuJv;*|>+|>VVG=m><-;BSseM`!y%#h2>=DglZ*k;BqXnC=_i; zDl`c0R|l|;5VEAt@_KHJF?;=ni9$cR?Ni3(CSu3o)b#)6IgeN3*Wb+%ZEbP@J_-N= z?{bu}1sRYG`W=gB`CUSCeF5!%5X#mEAsf&OglHVpsOJHmdU5sJ+FCRPAMyk6Oc^l< z5M)SuB(@DSj?T}ZTCsfb<#*^iatVbWpfwit3J4(XE2s}mQ6wPQMc4y~69xyBPudgl zT>)5|CgRG{^0iapc^!!8`lkcp0CU7ILO>cOah2Q$*w->adxjTdr4%qG+9 zlq&_=!8G>=#>V3O9-sxIYVAWoIUpTOh2`8G7T}1`h_nqh0!bQ>GQ8b)AGvSO2*UH> zyN>Jzq_;i;Jj1nX*V5fmiU2nT?32trh`{?_z-fspU}QC*_Z5=eNV8}n36gJn6_~l# z`(D%X3-UpkyoWo6w+K5mn|g%(xF))&dZt#{TsS>uA@;jjKMdZ)Z0&nnSMqi;$9VH6 z`WVzk&7x@trr7m&JO8&PI4q4>DG9>5uOOlXCeq5792a(smex3M>%4vg(FTm8p}xNI zDLJorGAhU2mv_}k6s(ZnayQ<| zmwmuT@E4iyLzfXI*%IZ+&FDW-Oi33fP*zF}{bd_IjwAw@YZ%{-kl_b-oPpkRe`O_* zraWJoX=|egQD2bO2SNxaewNib0RHyc)CWxUatH#sA2g=S?@Ye$FG7vivgxSId1d`J zN!U1UJP-}X`*0m;C_F5TZL{8VFiofwRGXk&74%ck7Y$~iWzGNu$ccPG3kKAICM(!g z7}QV;FaMsYsVRhKPxMLwChei~Kqn<#kt~lHPe!S#b0a1F|Bc^O1fUL3NkTKMjENG> z1g9h`Yix?wQbToXlzcc#n*`;Lf!qfpX%`+D=*d{=4*}caF=bI*Ue>4WvAKRb$04H1 z8ja#L%CKLzqCVXpANlTKMLCRC;B(F@I&vfuXjm zSd^@)>c{*`uZwdLf$z|T%czX%?35`GIp^ApD0nfJnxB`~F6#S&MkT0EA)Y8cy)!FU zZ7D9k+ZIdBViANbqqAcD)5RO~dwE4{*bP}(^O&P#QL2LT%VRS5+bpqCfjE`JpCZJ& zvcRgr>wLOpPXD$rz0#SOljU^l(tT$iZ-xy%qr$kO(w=-nT0l$b(1uG~!PA^myC5?l zs7iHTA*iNPCo%qEwUm6AmwRdYrdopon%!1ZW&+8091E||?6}w4#O^uFeu1lvNifqI z^j2RC>k7$f2%IaG#H9;VlhsS^GPYEG6%}L_l#O>pjY>~R>QC3(*yZ#-H1T>?R6URR zKnaG8`J;VB2!7Syu8$OMo>;OD`9+-SMU~{zv&0j=AQzU2HS@6;=e=NKXp`5G;ulBM z(p>P0Q}47MK`+b7nC7c2wTZ2BrIgYMU}t=&UA~w3p*Iq?MG@X5sZ1reQ3&19En+;1 zSdi;YErS#g#zDc5-~{(5H&P<&a1tfX%7mN4Z%x_B4=eYWQZXHZF;g=?4Pt)PAMo)? zWzs?wl9~*Lxw&8sK?m)_``Bm(WNv43n~+<`oV7PF9=ZSW^e0be`}46Tv9rUxK%bT> z4Gdw8S+*Nwz zwdP1dyII_LInc!zNR>5$8ZKiFs3i*vt^fMq4eSeU>l#PH(vFt?7WP0Y`al*^W*(l> z;v&ffvjd!`LP}Gj@|L?{3v#57+o5-`@EQdB63t572>*GxksZ#9WeE+(2k+0*7;1?a ztOJn|oiT_RLmzR`P<@zj>q*%X3rE~mShfoE5gw1_^nuA8DAT=k06MY=;f`aH&VTMC z4CSSi8N26dRrXC|_$5>owti5|1NW8z`m>W{8qw~15uu_ziU+Q%^Q3K2>N7A3p%Hp% z_}9g1i*0j*{#icm74nsuy|f7lCymjUGER&#Bf;5X&(5b?TTP){2R4Lx_9-oa7E8vu zX~>bR6Jvy3JGhU9-*BO1h5y_QTFZ3B2#;)Y6!&TD-%XbQATkJYcy8XTDCW%W-J|=) z(!A{YgN!zgs4a@^Jr>MD1_i*%4<9(<4L9f*K-?-x*Bb2!+R6M@s_;suW^Tsc-J}@) z@+C-sH%3M(8R)I})xJ8U$Ww0S%a44<6i5{y&^A9$D_-w=a`3G*5&xgPtXp=I1@$!7 zX!knjZ|%SDCK?zClQ3W8&Yh-W&V$QqZoUc?Up;s|IS{vtIu;C}AB1|))xqEaXsZP@ zEg&tZHMn=L7yOLk0}UNamB!zm6VGyMl;xh?ZC*yJdEUpfm;FmztY*25eU?QKBDley z?%f!(_RHJSzWTy!)_nynR z(6^ish_#QQkO^6Q?JISz+7+5a8O%ISj>XecBP6uTt8};OPM$=;78+c}%@w>N6F!C_hNIpdJoWycLglc@~M&)Vj*2HFg2r+fN#2FpdP~Nr+02M8#-#O3u z5dF)$SjX0wp4Gs?fU-`PV>!(BAtk$ZEqof_&7Z(SgO+z#lzOm2d>hSE7p(Gt(d{oy zTF=Dz33&R?p2fVe%Q&NbbM+A&vIy1~YDx$2Ysh!!Tl8AQArME*MRjnvVNOCxiR~^9 zg5~9vKwQs-ur2p#khz5TlxM|Ztx|zniv^D1sY*Z3Lt{E`?z=)`KqhkR)O(*)*rx~odG{q^IH$~3Gu*(BN9*ln_!609rib2}sK@l3LO-K$K>e*7$slmSx z^u>8V%5`MxO>oB4$M#VY)o|%v2~4tz1@e4R5hp0&2cQ5$C$uM0`01&@{q+fU`z$=A zb`S&tSxefM^o9n}32fci+E=N)tU=O)_~Ol1zBe6y7dMZO=i&QeBYHZK5f=4lfld8` z%n(wn->1GoSfUPeciP!v&)qr5oH^NAuP#f%@cG!-(F{LxA&sZq>4s>UNNset=x)WN zUoMv|;R3Y+QrQjmuGv}YwGIMW@!%<5v9_{6zmhN&;b-T<6zLLv*V$3Mb4pt-ydh8Y zVwR9r6t(6$WE#m>DESp75_K%-#qUx9hFs)?+U%`+U*dM+9gOB7WjXsd`AvduHK&{C z)l#F>+|Ie|tf1BbE=6COK14=V_AWg_Vaq;~5<;Fcm|MDkN-gYkC88eCjqsT97BEQ_ z)T5>gh7Ia0ow|_5H*|eiIwmN2uvxzG%Dy}ad;G#WI$cpsDFKjFwJxjukl)HE`R1a7 zE;I;8s57+5MeXf56(9tk!;;hP_;^6b1Ovjtl9*Aho|UaU50P;AH6Ht9e&=uO)3PRdm%J{y9Su^!W01DUyHcPwNW%rWn7@Jka>|vMvT1jmvL5+%12$5#0Z(T zeE2ZQL$SePKRNRzmh@4!>yIC)$I-OcS6+};?d17AIciqZGGV;6TcO!9&A)!6H0Px; z{?W0Z;<05t>gE?gd8PX{{a7p_yq^NF2p$B$t+sb!?RuDgN1F*nJBT7Cc>Y-X3pqEk1@TfYLZtb_!>rzhC+?&$~#Sei&wqfX~X3Q-Gc<+ z&T>M+B+v}NhHKZlcIJ@*>KNbx@QG->QYa`Oax-y{y$h=p3&vXs7puf?V{ld3!S)Y% ze8IcjjLKvC<25uelU`~OJKw}h(6WJ%vTW6$zFKNxp{3hz-08gB-8Mse!~?V@3bVz7EEBGBjS*1@DQc<}tsYSUChr7&aLPlyI)L z+{V4ld$>R)15|_H0T@P#S{{H#gB!w2x0D=2p9pJzO*X!b-749Q@%KaG>R5aZ{{8Z5 zi8F%z>{OO~%`swTAQyrw1y02+2f2#K-m_##?e?+(R(oaq*;3#Qpq96{XA2N#;N2O;E4RFYDP0e9Gz5zPj_Tbab_4>NIL+7tKIXSiJnI))z9w1lBYhh>P z@&F%frJvBTUXo4JR1}>n9X*=ft^J(j)ARHGnON&@%D5Ml4?3vZG1U~$DVuY@Pqxuk zl=~Uq?^jCkajaMI!hbc+1a^QYA0&e(0DBMBt8$R(L4WP_U=K>~n zs~5qn_D&Wdq2%tC`^d+ZPTx#s+uy&*IQsb>t0NHeOz(yeL6YY#RmN$Amj)aF8@q5~ z{XEcQXdelmR^MTm`TY6m$!c<~-wEj2$XvQ?ID^BDNjKRt=tOsT{-kwlm*8AV>>^Xd zFp!6TP}&QTN?E~#Qe|Su$pE?b^5{x}3fuiTL;Y4|+{+A9R8&bDBBE(kNA}#xFGuTQ zv!Cr8%?Z=C#QOBU$iEbALg%sV`uSDvx-QRn`STc)+?LYyQNNQaV=oO7)k__}hA3a% z$fXg4735)Ef1EjN0D^1|5AE!}WZp#)bMAvWz(PcszN9_&lopgeHz%XY-po`*uAAtR z<-&nqBB8wI$&Vx#vafd05-x}&s@KNiON8gzL?6%UTT_X1M^lU5uv@_6ni0A3B@$o|=UW=D7 zCB@>d$6B8caAXNwOGl17pM?JB;5AEm~Rz_Kr+ck!mctsWehKG+IM|~Ng z{;Hy=2t~sFY+1!@vw@p1)(m2QS!?0u(mUJFq;2a`oWU=@DiS&|bv92M+zEB3Obr zBUVS}jdoA2z07u%@~%Ce#VORbtItDJA_Df?f~KW|Mxxw+F9~bw<@dq$n<`txe6Aje ze*fsP;N(0uuTw?mZ%Wz^no%{lZ^+BZcx>g?yA9_$#+meAIA4rJOg$Y;V}A(=$-C(Y zfbxQSA)BXz`Rq4&d8Yvo{$K*CoKXLca3lmbiT?d6X)q0AAhQ|N`+$@E+$1_~DXy(A z{xvJ#S3dNIGF}>fa;Mjx=(n9m{PU2RZY5*XUlGM7;85`SiJQ4dEt`O$7LW3KMGPCC z6w~hs0h1>iex#8O#i7NeTcotBO_EFcjt!Ro^(&`HzCr6Su33HUb43b@c58yi7h2L& z!JWpAoU~Y@^{Et>XhJgI#ns0;)wsUNiGkDq$yE2U<i1NyyY0E+hs{(NKwZHVPvtTj4oC&1TqE1_iTUEfK*Vd+E!83 z8_q!m5AiL>Ric1&?Z?Pbc8L=8c3?)xtDpm{n8?xes(8$=bA4tYoxgYr03&{=~BQg81M&chPjXU0&`kwkUTtis}T zb(k!^Hd<8=nuMUT*7LGFhzjaP&?|6tacPfqL5MECMc*-Dk^7x(sXs!i7~j@M!hx4c zN%3+u0lxS`=JtKAm-x2fTFfh^PQ15{2$^zUlZTivs>B%#_y~`d^6{7#?I*R-j#SL4 zX_{QCHF)1b=1(qo*!iKB%lgpCf$4uv&=1R|J0G!Bq@Rf5}HrjqgPZG)q{C_5TV z4N~N6qb)C_uL-yZxiP+`5FoQWp|W8#Mk4BchpLk4T%vjXt8uw`2*kiedMPi1##)NS zHGmO&K9kFJEge^1%Z(sqe7{xsC%lKuG(&h)mh-;%#?uvta1jVh7B5`|wPsdVlql7Y z1$AY2_}o~J1?Mb%i&sRh?tb0D zmzrs*Gf;;M4Gp!MBvcKA=rrZktHtY=VPTjBW|CKKX|TU|F_VP?&*0A*w48;TTCYzj zAc%#RYDoI`5!^}+VtK`J@w81r+Esf>)X(#-EPKGB4^3kmr|iYR5mPLvaGTKPCttAAe+ffmymomc*sZalRQ` zs=7MiXv=--PD>}T%ULn?xu)F;wG@?;akkDgo`3 zj1yCJ8UjEftr^%QT!V(k9R!{5WsC7`L_v9z2iH|`Q*U3!O_3X+FH&tN!m(AHL{*$B ztf>*&Uw1GxGz32ghbNj`cG*N1L*KV*=H|7GTOADe^{;Q{Y*`50)r!onFhqu#W9+V) z3+$fkS~Fx;~&q}{wkujX5AZ{{UU?ub2J9d@c zHWZ99t%slsy8Ocf@^&sx>8 zXLMLOvIq6Eatc#QRpp43G$-}vI18~v?p^XP6=KKaz-=$rav2$Bk7hm=an2glmN+34 zq+WbgH6jpplts|IFSe#6)rtJH^`^h_54Y70;tjt`O|k8>LfLQ`IwX;svjPF1MJJAm zL`XEw3k^G1sdFc~aYrxB)}1;CXv=EY?$JZ0s`mO4-e&VkpDkh^i%tyK(WOyhUJopo z9C=|+$gy|s1TPc>PZ^4ZF%3f5_rnAS?9J;MDB_E>%kad-VDMu8v-i;+~HNAj8fn=}B$yu21>kSv*8c!q?7xUTFJDucIB)EF*+T=Y| zoX-L|JvL`9y!?wr77T6HDlCfN zd8Nz$oL*oyto9Sr(x$%CJI;3rC^#J&9p++@ZBt3PlhPPIZ+v?j1%^Ml{I(myM|l2JJh8z-vgfTIj7fcR2MlugY@=4qpn# zn|WBG#mcV(0ZL$bM^XO%+S*TIxFG#cdwiB{ zA*Gr%dRdJfOFK4ERIC4IS-t2-85S(92#^lftdfvOjLJa;J?Fh)qru4y- zos$1HO#Gh}lL19}N_g|xZp9qq>g>%TM=xewKG>3#;+^Qsw!NXVL9}ee`Q@}Ao^mh`z1)cl<{#amGFuh^sPuunZyxqVgO^HD1Lq(qo}A`Dq{tuuAQz*caC2vn#>86 z_S9T#e)|(T^gD<+5`b91z{0x6ryj0-7$uu*O+sOI3&eB7P6LQYw0hELIPv&F|FXg- zo9L}!={d8YVCK6R5CI@nBp9j{X2dR!gRFL2AaD%m*t!xm((A)Rcla~_>ZV!%a3U_ z+&XyX@4S9!_)XBkY`p9J$%A$MsLx1lWfdu%O*@_JbtvHkVTWoL2~*32h*=v`71#zO zrH|N4*5%9)#OW+HIZk0{W5!14Jj}W=Bku_jM5GE{At$@RqJl+8C~ITW1Bk8(s{WW- zE~X9e+4TqMkOjd81A$5Cu=h9Scs@OMml7 zBwB?gt=8_&&WE2c{}^%6BQpxJAu~6-dQAy#H|NuTlMnQvyk#RmK}zCfSlh{boUe(1EidTYkQq;| zJ!NF)8FRd=5Oj9z3vB>i9v-lOPb*#6Fmf4!x&~&@5nrlMHzI6(K|klf|;Wr#*E zM9oji^%(TRZ`L>D7u}#fv-wdJAxlc$pfP0x2}m^?wB6i4#7V0TPSY;oU%3*U3_56J z?Zi|4OVtG`CQR+>2^kf7VPiR7 z-*4~qb#+_e#)A%c)DIq~3GUze^#}keG0-874u4j*xBv9)KZ~Oj0j^Dvh%4@!#t#^+ zEPpdC@{NEc*`-fYmI^fu1whgHvIEFIzI=Gwezt!LS&ZO^?%24k`p(0)z8cN&%F=jY!{f=OEX-yFqYL0{-Tlk;|$s361R zKA0rus?PVYrM>RDN=kZJf{|_stav=3^Rs&yXCG3;1c9sY*f8rDctxS-NMDIp#HC2Y zt6rs5A_25!{I`#HgTRgxe#W#`D+oF!rP-fW6(`tpfjbBqYj^`%XZRJw>|=ieivW69 zum5@8MAr>(`k|-~%)7SV#v%k^bSHY&Yffi5SkO_wP5y^v+hn2d- z6y=axkP+P}nB^0XjgQY~j-}yQRQFi^2p4>RC|JftRDhZnB#Ln~pfSvt;&bPZ4g-n( zlo8Q2HG-J+_22<4bgVIzW$1y&x4>YHi;m-_Cp)*- z?0%C-GYnvB`#xopn=--_-mSMXhUZ}+Q3rrtb91xzb+`s$&qb)RDeTkuQj?!Eal{aN=Sb{^G{FH=9A($EJ$NQ+W43<+^h$09Em)LzNPW!DuBC`8!Rxw<%pl{QAHoB@J_83@lO-R5mh(77#SHh!e)~S(1-bW zA}K5>TXjiM)moM52Z@*hwW3KGHZ^!;jOiI53g*gOp(Q0%#4Bi$VRem4PbWNm1Ian@ zz5V@t3Eyov1JG)Pp)Ko2h7O5`dPMzLrFb4w-U<=Rul&;nf-byqAClfHF2O4D!DV;u z;86Ijxxi72YNI<(qaPfWO8DYx5FilpaB~4h5fpYV_0vG*ytd|uYL^fTIB%DCCqr=5 zn10`q2(1DzA20JCBHRH|F*a9QsN}|WR&^&Si++tkp4F21*xiA9cy2F~g-)pGg~^xI z_>=Umt!ViB0|7z!hB7|{0x+b577%zHo7ST*hL5da{M`*6Y<8K#LU$i%hctaxk2lG) zUP3E+hHA#hEf>EXL%g+t>i_lb+15 zio+*$#Xg+#L4@L{hQ&kIPgM}it4P;2cR-2`q`hM3<;v7uOzmYHt3Qpc1VCEeuqBNr z%OFQB+{K~b0?xiw-oz`~+HbQW7_lMGv8Xp-m0GjaKc*?+hXw}#N`69v1aVhQP0dpzwA$g1gACY;GAC@w z-^tJKU+1UkAe1VLX1RwlUi z!`Rwql6PdS$>8>{#;ITpCiB&(DSxINK?j2Mw*Uh=^a^V7JIA!cjz!Wanj zr$3!LVICV|XiMjM(XvoxMPLd>lav*sRh890FMnx8s`|x3TT3hEUhf@p#-^U;X5OZ= zpN>-y?TljCsA<^oU4*24n8V~&UiY#K$0z4jJP5PC57J{p%hvsAv0^X>2KV4|fD!$y zfZno)qhm}wO^$vyeV7>)Y9qfc%E2SJr6FJwWln0J%_7~2{(%2U-6x{|G)X}E2)4$* zDqg2ZXkKM|uZtK@&FNs@D#D(4@#e}`c?fU+X_C;=(k4O&)WN}SYkOx0I&#n@G2T#i z6Zt4arHiFH)_$mIMeus4G?9ydcAmsZ2bYkLa0+ohJ4&dUcJhewQOus}6iYM!G3vIo zWjgon8D$@q=K>-x`7UcRz;70kfLwwK^2m*6TVp7l;S(mi=3%7aa$xqw4& zorVY*>cx&m$ylzoXSH+%c^r2QQ0V|n69V)@G$f1gD@xNQDz&iUPeeL?)nyHyQJV)i zvkKA%Q{>2Y?u{7lmFGemTyFtC31lIEHb=NaH!_v5p4gucqd8NR+rY2{$bFA%=Oy~s+&eyOm$^g?V>yhMGQ~D z33l$;M}@hB)~M8)3M#TIW_tEN>`kZM4G?e{ka235di68^ic7yF>F;+i)aU=_l#agC ziF)Jp`(MkPfUTbm8;S7<#*C`*AdqIF63!*SM$SxQbaag)eI7xJV!yIiK#|+32SvWv zGH$Qxbhv$o$WTa(LRf;;lxU7bs{u+eS35g-ek?*emm(X9H3o-K-WDg>ctZYp^^BGfzwHEUOeR*ZA5jqiJ(Hsm6m+puK=l!vj9aCqd zLQa7)3tdkJab76?3rDXs(-7N2tYi@lYO~EqHoIxn);s&t)j25vKu=ZfV&dXTVqIJD zUk+tA8I}K&JL4S5^fQ0<(*N@F4YUMjO-mY0gvXa%3g2p z&Hw^9+c53)_M=b}8|vtvKtJ?1-z9*xi4wrlru7cU?D5f8u40vy?Y9rG&X*I7jGCICeO{`(W41 z@`jH5e$P)-GW_{%SYBB@Y^NG*?Kkl)iI^`cdwHC5X40PaTe`JDhwlBz>$-ko#<$d3 zdOBm_m9cq%^uCg90PP@9JcGdv0IY^_U15#(cNC`nHaQ|oBwcP3QY#XG=L2pW%^Wdt z_TcksR%6Tg@P~R)LqBUx-Y$yqh-d#Hd&?V_62L@9IB%u1;G+KA_Brqrxs4lGsE|ar zR-zcOfqlaS!y4)8l9lt0G&hGUMHZ7EEZ{2tVfv6NXutZOM<&jz?8O+otjz=se=I6! zD2u&gj>yBjNu~;N;N&tCqN0x;T(o(uDrk3ML`5dLdN5>yz7qyUNXXnE2+FRIE(pv9 z{2~ODQxqF;B3q$i<6*>J4cDN4krCa)U@ruA)i;bRL?gnS*VPTk@bdZQU3AaT6ba7R zpwMaJ@7h7msA+QHuPZ4KyuQzyfv^VyX1H^-bZWk9FZTFeY3Xvw9*|f5XNy|}l$N{+ zY5Xo%er?P3{Nqa!;jZ!H$o^u|P37NJuJ(+)ys7J(I{=^sE0!(hk_680NmpN#)%V^d zpe#+Vkfz~C$^%)*m3ug(_ZR(baT{D7CWF%`%=(E{hzf)N8j4RP5OMhR^yn8RVWqf) zMBbsO52@Oy{9rLzTT!$Kt>)+;Oi%@H)}S%5MJH=(bNqPkIk^66zu}usQ{4lXn4adF z3ZpNvqo{uxeZs%n^HQCew_Z4Z7V{7nojEoC977ZZea@Ae znp?ORmrja{njeX3P8msW6;T1@__lb@&1Q?P((v;_YjOxC>Mrcv{gze#Y^OiE90g(>6yR_6)J08k%TAii|;Hw9}*erJ$3 zY$lMHNp8o`PIVwCYS0rWe||^j_vEw{uz!Nup?t3dChcVDtPYi6O{c?q8$JoxQ9A)% zNvzHd2^P2pmp_*-KfW8!k`wcz11e(}JdT#2q@04-2b_nN!!Pik-|(zy_E5qTuft^8 zaUJk1$IGcQ1>_ocgCv7o_f_U%;&cX&{;K5S&bL`f?{PN|;CrsDtmJem%LfF3S%;FX zyN&|Ej3-fIcYhP+wI6io_=^;{MR(Rjj9}w`yJ9Uc@mx?Te5SM|F%y)M2U2jZDsn2O z?V$XEt#eV^1qTi^%Lx^H3BM>Ith?r#!N{63x6bF}9&p(`Pyc+0;@Uhv)h~0RAedghT+nW2-sYr`W7#? z=W1KjbAOd(azL7Hgla;h=|3@$%Jm7YMMBk*Gv+I zcBp(`rcP9igf%fvQBm?!Uu{QMmB|cIBj0xg~aps(5e@C=vk&rnpm8-=NKY?%uU{2!icWfP+sm~d(r#` zK&iNNOWK;6pm6dUAkSOv7aa-QkZQ6-N1oNVH4?w(DQ597+X;V>{BMzP_{6hKO^e#%||3=m_!dTd%GLwS#vuP8>~4wI!lFBRVRf0B^_Jz{o}P zrLm|VoCF`+!P__8wy-gDWcv9a~A zY&X>H{TVohuSk$nBk>P&2OuRNIC+YtDb(Hb@$LSi*1Wwe9no%|f0RKSjRo;ULO!AQ z32XHz!pNvrhZbfILG36d|2jQJ(JYm6IYP@8OL169czzz}hgLl~31D8b=5XF#Ouom} z;(%LpyiBy;ZmO<7&<0Smemwo5AkZ z!TWfxzi-F{8HZl}61FOt7A6-#V-b0=WxTEk$3h-y(d)O-KYdgGyaW^n!GVFqyo16WzzgY_AB*y1 z(<%@c1b@q%jA)RdCW3nnbqOy4VuVjQJ}TpsZtAss$Rh5+iTdu8hWsVDLwJ8F$J(smRk63m+;Zx_2VtQPQ1;SBLs2Q^rBC7*UMX8X%6Lfw=r2tXaP~3Mb+H5H5>E0R>b=& z6Lz+2iu`OQqk{_5&Nhl8j$RlqLmw=-T}1v}Rq>-!iN1y?)B)AhX?w_J-cbduJhOW8 zt47!PzrpDAAm7|zfgD#I3;P$h26e$b$PZ|PLmt~s%_&TOXiO0%{tLL9mexD*hl z(yYlfHF~2ade?a0K8>>#5?fhJ{*V%HGI4-IW5G!V!Q5#qF@^^G=~(&-0PYX-7BCcp zz54Cs<$jJICga#jm<%^juG4~edc#-j>F636HCx;w3y$N(f8zxRuSGVAOIUP#=K--5*Bp+ORsrx_9Y*C%O(Rn-LH7QO$zDjTF*+@3Y(?h_3kP0^f~Ej|kz4Q5 zM+LR)ZK6gSgSui{4j@J*!Wx5CZ~{3pgSC1Im}2IC^6w_@w-z+1k!{w^Vv4f~U2DK+fPo9x@CNAuPMny7Sb2GC} zK={_+!^O(<2-DWS?74NW-=;kTWVH@AJt|2*H~2+9gyIFs31cfQ{igwMgRh8C7bnD< za3qLKaGL6TW5fk4PsA{wYTcCt z`kfet8d>_Zw;t@uYXimi?I@76d*;T{(%3SJ$@Vj%fj<%F8E?BA0~)7S0MBuXd~N7u zqX=2a$bHOYU+35;v*BXoIz|gN8s1gm1Zy|olR(;(EL&Z&UFzudfDflwx-=lDD|22C z`V9<^Q~kTrizBQ^U%ut~JiP?$0eNFe{uz%A0Z6vvS)zMMm>0V1MJk*TH6>_hXfX8I zb&vYtX<#q|WhM|rIZT+hHl5KQ%d-U7lhrIkhQ!iz9!z*znNQ43y*MIy?L1rCPaJ3Q z{V2@z$!BgV%QxBSOe3S#rJ(EqXmm#m4HiF9@t?#y zSh|NMW7A6matp73S^EvDFd)WfhoP>E6hIZ8U2xT8ANnXw&j6uh3A%rF!*PSE7_G+Q z#BbK9>AUncCJHYjE7H+-wZYCwrtid{J(k9i@%mRdYoc*6A(ldmKyiXeLoK@w3GsBP!#pvpt|e0i zv!2d#EhA)cx$5$AtFzxwi`rm|`eWxDycVjAbmp1aS-XhMx%ye~SVjEeGpqIg`t}t- z)jxi`FY_Ze9f1M0`Y9v5s&g}IZgCTb4ysue-;+#wBo=kAr9NOvnA%d1fnK#(!uJjk zSsIGtPqnqRw^OCcSdIB z&kcIOygS(|@TD+fiz}8}7>rr#D8H(jZFCJVcR{_JUS09NrRJWJj0I2u?n?zC-Q1cQ z8^40;BS^Z)eZ=N%wtOsPW2dS*LUN%(z_u0(>Gf?6BTX*W(fZAV08+!ZeFu9)k(be* zrz@;iO33S-PLUR}$K-ox6L2(OQi)Dd9GEHhQIuaAoR@M0# zaU`bT%3HtYs4Q6KE7WZ-CzH0)>PiAa-8o=nIeJtsNkGGRlaxx402%E@TTUEeHi)B+ zmH)>&E#5}V3j>=?U?}U0se+hSOfl+~9K}caVMxM<(ABZNG#JA#jJT}oWsA%Wjq16Fm2#wEEA_NP=!)N9bnSW!2He z7$C3$NXnB{g(^dvBQUi|n>!s*>J`WJ@iNZ7Alk{(aXSP`TfkOe$oc2b3q()|WD+{*3 z$^bD2k_4<8@;olj`T`Gpkxm*Q~XImHCz4XJ*e zQe3AIQy^x05_Sd@!SU<2U+45NDkm_}Fyhdg=urjO%KvJ9D6v7ogY|d4od;p+1=%%Q zWBd-Rx`CD%__H4EtvlG;S9z%Emy@YZ!AtvEras2jeC&RZ%(+^`2QN#YHHE%6RnkQF zcx`}KLlD$^Q28h{N3-FN#TwAa#}jhTP2avK&U^aVXX2}s)IUFy27I{otc7HB>@>X$ z1Z0Ck0s;w1Nm=kiQBgGVk%Zj1>-PIrX5&Y)Qp~=J1%FooR5-;H(uA{3k@z_cRo ze|-EoldZ~!?|$0#2}yCI1uhw$1-S`qx*^om9Uqv(6 zYGkBz?8=;@lp67#ykpsr16OkcoKPW3iSspaY{hHbLi!pCmJ>xk2wHQ^Q2xr7XAt?bEx1!Ed46tm=Cm~`LdfW=1OW*!}_>}xWsljB6kp^I| z2+dY^{x2WD9DT^o>eL`yHPcihmVf=62he!`{>&QvW`V3&hG97yixW$Dhh4(3xoN#c zb^LAvYn57|po7Yw?Hv5_GV&xGp9y-J(Rw{n3ah0%o71M|q8YhqF&1Ei(FhYN5PM$z z4L3ut?^ig{9-f}Mf0`jTfGH$Qzd(>iw>3VxHTn~WFqH_lg#r1k=TnNf|Nh1@1Y*mx zv4wvBrT8S1yKEMr%LooA;#RO;yLJ!MqX({l%r>+l*;!d5Gr-^v!XgAPyE-)to9n`=sGEjc|LFg^Qj=hIjX;PPGw7u@bYa$q# zcBEL2tnkr(z#jmu>I-8Fd%^b+r_oxSj_&T-J9t|6rH&vuPE1S$xgqda0c}!1!&jO{ z1>ZG5^JuW>+McU797~7M#&1tiOv5;uw@a?ZCha=z?(Zbykf##CD+5 z{=&vaj=K)+|N6?^dX1_yZ7B!COF+fTHVPS;4*0Zf7Ag_?w9&4LFHZ|`asO@y&iRX|0H&LX+Bi{W-iIHRoiHUf2np75T!(TP4(?pTV!y?r zxx^$L%g(F5L?q2BF8+RW)EUrJsLxLae$^fS=a(lI5K;Vo23dZVE7hQZQ1^6Jg|?mq zj6`8V^Vs(*1$G((lB@m62btK&C8VSPbXC>T(o#tYLkxY9v5UzkYIF+- zCP}SWmk~lyS6~c z+%;#26^dPyn=S*T$a}VyL4U@!~a!P@Bh>PVqyeYwNvl{AJ6CabadQ@ zewj?re!`ij=IK3@&Tu~%#`6S*!;ao{0DYnr-=H9gzSkg2y@<=q#3cL>JCn?6EvK+h z9BA?Ymdxe7`F3tjc-d25Cn(`960{w+XR5E9=7L-o5aA$w$Zfqet{&bUe8A_!31kV# z|NKBLZo1+*c*wuIpb5bbN3PIA$k;olh%st3;SG=l$K>ZcRR=$ohf6`h5kIy{%qNEw zrv`XqAQ1EMk*5vl9u6*76z}6&t${7qTc~k7Jw4%M>&hHqU}@+nd_?D9KO21c^2#X) zPLk8nwTu_)^oO2MScA>V6QIBV*bOX50Sgx6k22$GGQUoQmw$!8yN@f)duHU#Kl#884BR8<@RXWTwK z#6K1d=z}?*0aa|3l27JryTix|X46L50~GLjURO(N;->h&4};PJlT~Pki*=)sX1&T3 zxYo4*?|+O$uTP;8WcrbI?J+NcAABXy*8xvo!d>-CJmMfVC8aD54CgnRAL}t@R@0Am z01UK&hS7I>4q+V=6EpA?HQ*srVdNxhUdJdwJn_1FH(C_@v2j|@PEYsVrXXh&eYQ%B z#2-mRR}aIm29^k-`dTyfdX+5LWP=6Wz?G?i7zF8n{}oC;7Q=kd5X{sE1}}ucSbsRU zMwl^5RKO&75(s#-C!P(YGa!nGkB^Vi%P^*6wCeUkvz{yxNk%;bvDKQA$=xdyJQ1Qx zOG~T1Pe-uybdKZrkRb2;vN`?~foB}Ztt!4n*rovcSDfdKJlAC`ya@AIU~?vr!hq!K zj~_q2eUow=%|I~)LEj6M$xuS>A|k8`62X7mAm2-?5rh&N%`8=#Yu+0CI(d?Z=3MXfag@rZs9hCS! znmOGR9&Y9e>$ zoTtjrjGlrU;^Gho3u{Gk@yaWJ3x@rAB?|H<&a*YIK>X{MF+75(PbMkPKKQDh&Q#q$J^U*Fa^KYs9)uXW@mIgak=F46`!^*nrfBIVng^2*nUf|0L8M z5a3wj*~aJ*>nyneEc0V0G%L@cqJX~WK&qb88xcf>P8gu?1hgEToIsNhe6{7bZ*9c4 zSQm=kFOdeB0IQyyCjeIL>r5TXP5L*j$$vJJNu;Z*3zI{05%q^^9PDGynVWUWqTyt` zfy3EN+|UG1KR3qzU&)V|&+yF64mJZ$qdpCnCk!iBkDNJ8xgKA~KI~Kv@T%WEIC#|6 z6?>EtQp9qbB`3S{Q)aXd7+r(J(%63<6P5p{CV}D3SWz9F@3yhhXLIm_X7{C9^T~uF0<{j-%)nl7zjX_11C&#&hd`cEuoC$E zyV9}i*Eyhwr?GH?{D;1Si>i^zh|Leb$XJ=-vZf>gS6pUj$7E$;3 zo_T#H%;Y%4S6o=mlFU5~d98IZ&k*Grok+sE5?+68?_#uB9qF20ocZU$%$LTOvEzs?7Vale2l-|n zmu`JQe}5-YF!lMh^~J>XYlV{N>s35ZHB*R>En@7vKU(qO6xHk{9gG7ddE&*JiT>}gTpt_QFoX(i+q``c28iV{2CFN#zx zmB*%Vyw&&efcxKC^1mJ>rVN3iz60;MFAJ_9pz4Mm?nWB0adW#uct??vbUn9~4O+>pz>KTO(4$n&GrxEB%l$h6rF1DqAgEW?H( znNSI%_3z)cIWMQygG?s~D#DC_^SKW@&$&x;>hQw%f{bH6D=TWM&rf9^Hg(=xkMy$W z{ym!+o`3Ld9?J9mgIHK~=_wKexBsjtqq73sWpL?%ATKC|Xw-vND(1Ggk55%jAed&I z9slk>{B(tCZ!R}75(&CdppUu#^Jlq{FSHHdK7cM9FDq-TffnTsyHx7~7bPgqy)$hlX&96g)U5M{mCu=0KxxyB4TfF{-fmlVY1 zO0t_zL&S}`0)E5NN-)u+Ti{r-t8otPvC`ne|VO`o1?l9g(s@(}DHg0>KPZxX>X7&})zYK4mN}Irx1Q=H;n)_5)uj>3A-9jq>SddqdK> zTr@!~slJ{n*GXQx5e{RkB!cEn54nTBegltSl6_T8HT~gu<~<8yIy!<@Xw2Srad{l{ z`_87~=7;r!ngNzmuV=>b4_dTCpK`C>5rkv1XyeVm?ni3x-~db!gsY%grhXENZN(Ln zOIwZCglp%H!>we--oTxmo9pE2dWE^9zhAbx0o5vk6$L8V))x9_9OhN^Y5W@>LZ15a ztY?xOCY|}*;!W|sr{$1{R5{sA!12#P6Gua%5SqonGy%;c(15f4yS^#ZhG1R}w<)Y|;9eC}7LbGq z2|4>?ng<-SQP3=fG`bsgEabAKcpFA7%98+YroxDvCb)n5Sl-G{H}Kw=kFN1DY&+Tz zF@+LxC<{TN=@2AcfaijPIqJW>v_xwXlGSPG4LH900Tg@C&KH15!8J1 z{;|(xd3{nRB8!DZP(J2h;mptmdIi2tD-Vi9J?V_#0=3XEhWiN2z_$U102*UGLckPx z)ZWf%STg}gbxvbi*)W=o03aYtOc7h;(5V3^R2Ned3T5fd^6Ja86b0x8W!PJL)>j=Q`0;T2SI) z9DbWb-(^YC z(-VtOvGT1!`TqA0lnqgwJ@xfl0D`mYvi4zDJ&XptZD$wy(^`Q*KO_<*d<7A?EDi=n zukTAsa&V~iBHBn2bprdu_eXb!lN*}oajZ-rMg!zy=VDh&q2~sp2tZojvJDutfQVpe z=pSmN9Lo5NQ# zp5rpGu|9on(u;M^{XUB_oct%J>^QgHq%P$M`P-<@Es8arr6GQ9T@Al~+*BPE64G#V z9$LVVN+XVnwT`2gV<$>wFP zEVB_uPSdxh|G$i89H7Lmx0?Pi&ZnKhvQW@#4{+rf!D>6?MJl*T3oiy&N#HR)eWGsyh#G(FZf9* zbE+h0YdddA?il{aOeb;CABA=%?zp-4Qo2E0jMfqIEl0lpumJUN1GV}RG< zFwPuc1B7|)#2aXFf(r#)TY;O)9L6Y@RcJ9p{k*<>tLEXyKZ;hKU$GIHIrcC~!aDZG z{iwsKs8G}Sp3kfJ$&_;*#*s6^@;xuMnY7hoB&Lb+?!rHFtuWbsDlGW2uUgM3;5{cup?v=d_&FOOU_n*=iIRpE<00!HbmW3 z6^RVe>INvqjhf7+hlZkNY^rN(58ygfh{c7rK(*0xWD3DkCxMuyDU2nMc#M6ebDP-7F+TwX4hFJInNu(S8un4oTdtAU-Iu1L~V? zHPN}WmN-G|W?u)B2;YV>G&A+o@v&6({|@$A_;VQU>P~}IV({j7v3I&$c~Qv#O3X|Kb*COMaOIMBdY8ifV78V~KH6h#;*nV6WKXpf zjC4+?d&k1VgV#CTVkH@5oBgBz;wd!Bdi`hV)<*H&6t+sB6<)=UfznN2c!smU#H412Vx&rN2#8(TE42k9QOop>CM2+ z8O>l^dyxaTFNnue@LPxMEQ*Ix?XA_Y<|5a?Ft-<)bEx}-?Yo!nW)mE6I3-v~T(5WS zq0jqAM&e*tJ^#YthsHqt!p8%AZKYpL^1r0&JgW7Ql!S5@HzL`THL4RmqCXT1={k*P zFoLLAf|9ax1&ft(98Tuk=$>kSl?219pfEkwe(H9Y#`);hCilmt;zQ z3Swde9*g$N+8AvkV`GGTeNz)29S0v@*pH9W3WmR<-&AVu@;Pc_EZ49YV)gL8WYegj zQhr~?IWkAINBwvtUk=qWYFsi=S5=J<8h5m{mAk(&F0ELH`64SP#|IqHQSFV*DYPX; z6q>mSp#CqOJ@aB>8+f(CrMTN73H{0UA3iiTHYO(~ma{`8`*wC#apg`~315G|KVL}b zY59+d0e7ai8aP*nrA*vYANC$?Fs_z_F_(?u4~jbzps_)F!-?B1s<;+9C~mRZF!9Bx z=?ys&Dz67?4mi`y62$iUE-qV;utFUT@qP5HmDJ6UZ+Jv&=N+k7T9(#ec+Bbc$@rkN zq_9)Ig2g+X$lwr!9~!gXEEX9#xqj`-u`$LO!Ri- zE;LBKwXUg6p!X40pQs*HQ1zt|lam`87<9C?q59ITt;`#YibMk?l>tY4)qui)kPqW9>(i zBw}ebj^jJMe9pM{wSLXLdX?Xe?bQq|PI}oeOJ!wcOUvS{q$0@KLFW#RnX1k0h~RnsF51H4fJrucLUWG4SUC5F@`z5%6q&drm-;R=C)q&mKc-$ zKHR_fq?O2&+I}DcZ7v(l$!UFY(bDoR)VM?#=0X)MiV@;BJU>pDykmhmFMPc)=Vcxx z0nLZkrLy!pKfgZtMDKmH=LFasD=RC70ZuNiA^|%E$Wx#g-AeiN$Sd^f+fU!V;XmR* z@TscTsNJ6pRS&7MrTBs?Dx;pGb%d~7*Zh5%9uG~1f}TEne^bBeD>VOxy>|I!K3@x& zQG7!w7SZ_$i1NW7T zjmg(9Ufrw0?-r!6vqL4vq%2n;nSdCv46zc5N{Pxt1l{D&ptmct4sne4$9c7yedM${ zKNBx|EsJ5A3`6t1L-K#$XjK(JlbA9oMFiB$A_%nel%yXJ&+E>+3-=C(iJpkzI{pS1 zAGil|b8=2w^yX`fd%Z)?&BTljBfh0XG>_07OYx1aZ47;XgnEmsA{r(p6mMvfDxuWq z3q9#u?7sVjV0^nYZ7sxm9Q?3tH3UjP6M3}_FoG<@j&`IhQXH~CAq$3@T)_}9N@a%240#vsciu&dQ4@g$OP<0e88yu`&vogW01>rYlm4bo-xZn2{9ohMWpTXz{rSEX9EpG!h+3Boc3Xcxj zEL<%rfH)b#G*DB;eusN+MHgmB33Q+%9#aF|2`^Lwo|w&f{mU1)cY2`m0X}u@b=(2b zGYT<}=c$z#X)pQt>F&z+_x?ifaKz;j^P*`GFRDE!w%pY8#iYlJBp|BE=%>{k z+T?lNG=>RGrvvvK!5K(Yk1m`l%2Y-F_jgwxza*C?QUwf;aExr1u()^pa zv3wa~Lm8LXv=hf@>+NuIa6}M6U5KL{Y2Ym(EiH{c+ZTR8muLty^TCyv8ji#M1d@ja zmRJ7}%3o<|`}A3?wacoclb+I%q)4K06PsN$nOVJtXh8a@ktcjH+qimkQhz{gQf;lS z-XCl~3S`62K}{$-E2~K3dXg;OAZaDec*537S=zP!yLhdLVo*Mobe!)MC? zVlu~@%mZ-W$Zu{#7?gB(izyfaoB$dRX3)PHl=c$&v$OQ_Mk`D+t$U#(x`acB<8 zE0h+3r3p00CK-gC3dt^+E^Fs#jhz_?{azquA7fT43~8IG)s*UA8}!65HQ z)trV~0A@Wfy@9V@gl+w2JR`vWTMmi?wZDZZYeQ# z+kss*RW*En92rM~`F0B-T#8M~mp8Esj=sMFlVy?*Q>+bdh8H>s&o4 z@-+lz@DmjlMm^b~16jx=83BwB|A)7?jHEr9nbzq`Q$WC8QPUmJkqO z(I6om0s<1!poEl23P?8sf=J7m>)!jh&okaJ-Y@6F*<*ijj9X!?>-xu>znTopY;4Q; z5E0C1G$jYMK}A&`uo}qQ>)ku`=hzRmwL@f(gbu;2d-Q^^qQac1DJg|a`mU^olQO#Z%d_5 z)1GtkD@l9X+sj*}ucc1L2Ja*jBV2NS3KBKi?D3rP6D(x3vg0-Zva_rx)V>k2ru|> zB(Bj>g#0ChvkU4USCxctH2`hloH1$gjB2e?QdY*v7CbOb>)VM+yP2^P{_a zyC2!Yx@H}ibtV+vu&NZ^I5hmE&mS&Rb5O9Aet8?NLlDSKw`VQk1swp>;wDXDA4}j@ zl5YN>19JgYcFZ0NW)gTpRjI2BL)Jo7lSUA2ZEaOm`_KpXcCtcMvHiVnJCTj~v#9q0 z^9u_*y9bZ3Z2Zo&a|s_a+7(e9CNmc-onEktsPC;|!`sIi9(Q8Vap;KTu_fwI`r@fs z#E?S_EIK+%0Fsh3FqnkK`>rQJ26k$+%DokJ6hRsaJ~>h%)u%}}x5Uttr))r;YGauQ z>M|&Z_TzzGQ>zp>OnRBb^Ioob;#n7NjgUI~rO_X+*gi8iIM zF7Cp4GB`XRn5dg+GNp|UDxKR~()N=S`t<>^cSP*8YSWRDd>Q=uB8TOf5%eK1}-@Gpg&-aM<**e=-V-Q zlc6&orR;ola#5#!)W0h%?BUYN@WhyWpOK2D{^ZQ^Ljs2>Oi`oUlC#F4j&o1VJ5dcy;4 z;?`R09{g@Svw79{nEHa#p;xkX@6un+lw7(!(dTWELU@Bk+xN zD6>3YNL|0#V!Z!jLb%`3=omW!-L^Z@kc)wb@irUJnTc4(BiTm^gEo0KFxWYg2LujR zf3SI5nQvoQ;^6(_N3Uzn&dv@FzRu|reSkr5tGu#uZ*}!(mo}rXY>-TqGDLE=L|^+o z>~6#w2p>MYe2F`MLcpj!Ff%siWg2gxlb6nab#uJDO!$jS2PTh_C?(1iNRK99UP9QT z1-P8ybck>S%>iM$`?F^c?#8z#{t*B<2*W$DFNAG+pPT~mHi9T9V98?0shz~)bF{6X zNmm**l@=Qt^%)8oY3obi2t1?m^c+#_X99(Bdk&ADo?h+)VSavoAt4Y*lq=wZTxIX; zTg<-&ZEluNyXNtGhk+>q@6rPV^hgYcM)l5Fkfi!P2Sbd5C&Wm0KUN%$|DQ0I3)DCO zS8)nm+5@+NMGk$`EPaEiLG#t`j>~DhImZY>4|c7=0?VuC1@fc_fy`ju$5?_P+m-L$ zy(=s8g*E!%+u@2U77#*j!_=@|p(raWFBlZ7_H`oJY^Zoa`v-9MR9j$RXZgJgKd99g z%s&vUbhv#tI1X*=mA_9xc(0h!DsI_`R);H1MzexBRN6cQS8MNC$zoVy8X5j;y{Nmd{==~7OYm!B3P~fAmtb! zW$gJ=ysBPa$1q0cg$S*V)KjwT!-0!8+sxUqcZP;AJ}#oNlqx+>>|~(uWRsWwM0SPl zhaK19o#EMjN(h6iK~Fq|Bge(g9-l&S4aSSD(5?pEb@>6-y;Tq?gju(HuB4@n3nl%9 zZ7E-#6vD4qi64wR>=LrvTay%Tq;@Qz+-vKhjY9F8Wo!sOliU)-GYLSP6-YZquI`jK z!Vou;c5s2v|L5CT?a2)N+MN?8^<>}HoYnG|YK+JuD4@e>f3SPwZje#>vWu5YZKK!T z8#w-9<-3W6%}ZWgctA-2!(-IyI=U;_%TG44$Qp0&WWQ7Yo+F(9Ez;Ckqsv{g8bM`8 zIPijXHZ?pvoZWCiH+Ek?4uwfJN5^-xT!Ydn`kIZz%St*p$8Pv}N2vIG%Hd%k^Y-@E z%0@*&0g_DNs-7PAlnN%)nHn2YsSyctmpeB(#N8Zu7-MQ~9tcg6ZYH>g@&O`-c`+$$ zTQ@?C(j6ss5g8fVTU(`X-+qINL(;)>3NlJ|mzQM;-?@m;zhPc0pi|yQ{1PC2uZ{LG z_Q?1n*Tm`23~6Us;)`pcyZP_l#c`?nzu*C6F6K~pGl;;9XX569$cp?z!^_S3j3%Bd z0t`H4HZ0T?Y84$2@aI~;6dDtwnnjg6>T*U+uJ4h}!(a_LKV#p%b;;+jyNnfUu`{{` z5?CsVPWCe0U7k2kVFXOx(EkHNv=8D%|Py>K%%;{wO?5_b0-rXxN;G@xs^+GQ5u?u}N6zO}bKC34s1U z4hcjqw@H8OYGj^4!#RLyvRZFyYkvc%6&e~!Z7C!eYB52G;>DPgm$wA_0|LdN?vryt zR@wRaHum;uZ~jW+zU(iPF!!ZxJN+;&Rc{+9Q}G0`j`s83A$xq`JKt|1(y}*TV6&ki zj3W99_|OX-4lXW_AFQbHoZ3|B6XXe4ya-wJ96w3NDP1?;IvFxWJS|?LD-;kfB)3ccK?tfmWf(?fa;GzkMKNgVN&n@ z<|qu3mD?Q~YbhguD$*EFA10szA-jTl;s=0yFb^L}ckxd?`~{Kim7Ky|Tx$3%^SjT! zVx604F1VPNzH=)A1Ojwb+;9Z~Wk7NJ-A@n+g2ZkJ01!8VRX$tfQ|ZY_IL0sOpklE5 zxwFQw@cWrjzSp5#`^$|Jg=6rZNkHH+6!ppt7JzHOAMVvEX?BGdKA6e~LUB3O&I-z@ z<(i=6ew0Q!GUjhd@Zbgm50B)smqlr!&3-k{W%Qm0_UI@~BK>=|T?q`NCgQGbKPim! z1bcBxatm_tYIM)~N0L}r( zWU7RJQ*EuM>~Xxuei%dVf*)4##PqGhO%-_wyg4T^HNb9?460|(ipTHY%&l)@Vfu52Gp&VF*SWz6k&uX)8#$bceov1@*3?R237^&)D#Qgas? zr?XRk9=&*_erQAc`XEDAi=`?y7B8U>7;iN6YHzkri-dVKI&x3+`pPOglxujNWbZgi zaSykQG8!8XNBJv^+F>>;c${V2^GA4 z!%)tEQgwgRrDF_rA0z_iZ3NiZi{J0~oKm=+z$C{7JCiC4X)r9gxaWFiczK|B5$D*;CW1y%-UDRzpyNNOf?Ed8I#P1-&}G8J*JAIp`aAmwt$q|nPnQhc zn0;XtJI;4gFUce1*@rs4#v>QvTn@wqv_D^5Xc(N>f_+>E&@Xp~aNpcacs3<^&`wR` zMgJr8Ggc{)1Ayj$+PciMZa(z!_3a{4yn~gu3fU~s0PI4A_#R)Z=r6YFDX|#+5$=8g zJ-5&uDQ{TrAKt$ootP*T4z!0fPH>E;rBS}WZO8FSI-R5;@fw~OdiYN{gmHSAfBW|0 zqT5n8HX<0b!((?L!2uBVlg)^j-k_e<|ejB zju|WO9Kg{EF%(~cSU{t}Yd9*us4!7FVEmXSMv8Plr@Q(Cu zetF`MK_KM~u?+y7e;paIa3E%)JJA+uiwPkZseMsgAPQM)Te$_n!5x6#yo`>9w3Cn- z%&)#MLZNwXf3^rnL?XO(SHT-hsD zSCe1sqe>Fg;-U`s`k9C#fLU5-$l^Q7%z0>R%n5NU{~?_^Eulc~TJ3zwnz;(!12h)^ z>S<)@haPmOnHDi1!}lD@ii<(DbR#M)=zs$4-%lI%0-~@fMcr3`=>Y;iTf(0c24l0E zo*!(0&Ko)vkagl?S=88+zmruQP}$qa;hUaZux9yUKe{0Dv+TUXoS||FOU-n!8S~cI z4ZfaVw?7&f39l}(R&frgJnk1>HhLEzZ(2y=bB)GJlqq>+>}V2uLF8z`KMXHP^G^Q><%jz0 zS7)zB5V8)uB17!`FAk5X8t~b}z;U!c(^y$oRa4U?F94eTOd{I@=>SXl z`w|K&z@&kg0wGM9TE^azVy__JLF#YrrKP5_=QR5SAyJ%VEimIgnSRf5`*wL=$SRs> ztHN3j4dU9n%%6Ron7eX;SS9F{O(G6TzLjj{As1&qx6LOBq71pvTbb3?v4(v{zCZB( zk#rv|j;rE!_U|?dXGUc{C2^o>1k?bBYv(_9bH{P%g!)>g_qSCXzc9mqg0@3=I)#ZkN`Se@#p&s z0EZ`Ne3#1Y>*Tc06K17n6OipE^G0-LZk|gT|8Ra&`KF{~|R$DNlpSzU1iVj>3~CdZau* z>30}THdVr9a-veR90q1|Wpw^wf6j>5M70N#%#VIqkGdbhep=c}OG_h) z>+9`}M<8g#JRhs$+v`|7@Ux%f(KIkv=QJ;+p|B^xRA=39SwXT0Z{@GhS;)}-#- z6O`=g!X{5RFyl=<6|ll0#aGma({vX2{|4|J6B&Fp`5qDYnj+@6Nuq&S_Z9^q9zUkZ z3a*?eVq=FiezIJOB*d7UC5_Rcj4CNh^>IJf9h9PnHv`HUMB-%r0|o**N^j7O&TKFb z7{2pHdYYP2~zW&7L&oGkC3us3eH~~y8y`!GcP|bUw zC2f`!?~JfSAAoW%ucG1!q*#w-ork+pU%$akus)l$wXQObx2CQbO-w=pwh(3}rqJpI zQpLxRdLQc^YEXE-7I3z3CaB8xTM85NY1Kmmwx6qaxU-~VG{FJ z`g$@1By=UgaVM5CGW#-b$BbL7>%RVC=X;0iay7zBq&Qs1kM3fzMf)F9jQ<9v&8ClZ zm0srov(DosIMTz6>3MjNEh!)+Fk##k&uy77PDw>Ywl2=*g&vJvcgpPC98o0UzRMLk zlIkzxVffpQO7yz;N=Qfu@5YVp2ncD^)SwPPW70-F!f2u*A`0fq>q184PiqA+g&F?? zhv!dg-_k+rXcQmsODUzE%L0}W%5_M&dZ1Jd6D2Lb=E(zr66RT35@hJbvtDA9xeW(l zj&S#kq(9!Dc@LFd{|VO;3G@fE*k1|ayHs%y!`j)e&VzY)6aT4sn$Dj? zltbg3E^3#o4k5{_-(rimdE*E)C$0p z4KWIFGz>I{JJSLoBREX0y88OUFvpvYt~PJKWIJye3MEK_k(QT7r)+@iUTvPO4t%jT zsGzd(*GwFOIOy|UL`3YtNe@`}*h>n{ACPETa`WzB^g(K#=+In=daLf7=k9NK-rzsT z^oL$VgoBY+Vj=LC?KcTx9(JBT$}bJ6{%DXZ+^Kq~fQdxCG&P$anR@Z!KbVELf%`_& zm(NXdiQ425t61-?Fb0HffXt`g9|W_pXx%DQFFk{Obqnzr^x(NNutD~&i= z-niz00)N4V$0k}fUKXv;=8o6G)>gDii)9yv48=fSUoMjq%({NT6(yMTQz;O4H;s0E zdt0UDi5#M<9qIui>hynj$_}-MgViWLao0Wq1PO)%ZNy}YuNU(c=*rj!PTw3nA4@51 z?8?&g&C3a`&uMs@z=H5KGc#-JR}v!B%)0NF9l+aRz4d7jsz`o!sZxK>DaQYW|Z_#z+@W@wR8_b3|@ug*D29wR7;**nq0D*Fpvk4k2z^}vT+%-He zi2w!#0v#|GNRl8TBYQyL`+Fl7q6QWg;2(TWLG-}>cPsKa*A^mgAX##S$&U#IaN6av z0R{)f1LTM*10;@BFTHc%OH{{sVF)mCWZBX)2Y?Yj-os`grHVz}#a26daQWgISH^60 z#2u#uCF)D&)fTBFydNBPEt$>g2^_`-_DY384>x1-$PiUga9RQRv(O$4sQ*96nGN z2<-w%(hW>S4UKi`e(MQBnkJn~xX8cEV%N9T)YL#6H}va1FnnkjdiC@3b8_;U`3H&D z8cdV}KEKBNad7=}r%?Ee;;_(+HI4IShMb|!Z0|zph_2+AH5P9y@WQv<*R<#ttDr9& zic2S%QYyUN^h~qcII6-%*^#;_f#cX?yM_Rt#p(T%8DW`n-@`S$IwEZ8DKtq1rpNsg z6Cc!REk;rxZLhGj)WH~aCHvsuVC)eRASzoTewdS;o10tA1Cmy`!G>)(aDgQPt+`pN zU*9+6n?c+C%>*?f6B85on;yha{8AWv)7U@{jA+HT+w-IS2RSjRbwovQ3^2w=e=i*( z_~DpMm8q97lap0Rmu~qugyZ;gR;ax?%NY^=TV|clzLiWoZ03HKyzUghJAe)+fkg*- zv!A)Vt}^(TqBCr&b{%2SK&p6zy&J6^M=UJ;tY?ejf+n0H{p_a+zdn2w&{BpCq@C?7 zFM~$Zb!a9ZbvH)EGe{yLER6mc#L2wQyQieo2O45X5wo$e*{Asr6jF_ncYV|v&Q7@c zJ?lV$jd5$EyN{@Mg-!n_&(VNFPi7qn|B_Uqs=BPX=8t=d2^^kM*cm%8 zn<%cT5A+9yw4vE9-)phe7}Q7a35B*o{4l+oJ$TFEQU+$F6jc71^-7qtbSWD-#l`F* zA`~o*(1b&N0WV{!FW4TS3Jcu?c(Aq|`Bu8gZr;2J+e*=yi0p6iG_Qe{)-Raf0QNE4 zq*VifJ<3@zuGJ>A*Cbz6MSFkmylp3!zkO)qEbD=*hDmbIq9kzFmnhCspC5g-S({La zzX15Qa;iRrxQkM!$!}@wjMR(_k~r_v1D2R@6qpo&QaA%+OqcS@xGL_OQT{G~iLk&j zI`CD36#ZT_5-y8aB327JcxTdR<>Ulngy+!*0F2dJeZrYSWS|qLA>q8OFu-DgwC){z z!!f#bE;B@CDt8s1xbxxZ=caA8cCRqKE|ZsbH`Cjl7eshz7gK-d?Imz9-7_Fbej2p= z`>|qS(1$7I!l1uWAwWoO;VA!efrMEv41gDaiq8tol4TR>yx&JhmYM_vqw;UtchpQ_ zG9K`+6cmacfoKof-?~My!VL&<>JY2X|7^$5K?!0*VE%0xbZVeMNN@7GLj8;fD1;cW zW&HQHgWYO>Y91%e3ngj{-un1Z$8Isx%%7Pr{y!uYFwT?q)XL6MhuCU8bZ4EeYsZS= zeP5R9TTIFDtl84ub02uVtf`rc^pzcXts1QJ^|fjV9r%7@zJx5LyAD8#I~;|p}<}5%D<6v)(oN|SH&g)CsEx`|uKvM~*Bixj^ z>@J|F;uSog6#J+pDEJR<+S|+7SEqsZhOU|1UNj_sMRX3fD0fRti7D$v_SMBYGtq&XwACRWF{r%x=qqY6nP&?vu*sUFYzPn9?KMZY2+kSeedqti{d)cCw$K^8X zjEpt1O=1;UYx0MaT!6N;fxmbMtLL8YpK`dPBA-V3mdAm##;{t((C`E#vtfn6f588g z>4b!Z|G>piEGFubi2t00n-4ctf}Y+2iYUM*04Idg*vH2Ql&xXj%6K?-$hUxUFMD22 ziUhE+!@Cj(?cyxigJA^;3B=5b06-woMYD7FE3~3D!t@~toG@B7>nz0@eu=f;_pc`D z>sP1c`bfLj9nTUt3X*P%zB||^hfWTbjtC};lRLPKI{5(n*ab2i$TBQOY`}Zu@lgwz zoYsbNc%oju6c-cIZbfqj2>cEbZq?{6z(C*f2@DKGMGXZ(oHpX_^L>l%T3~hr3yCFa z0M;bA-n+$9IL}(1W`*9}1(x<4`q3t9?(6drtQjlxgo%B$zN2foEREXlR4^L2FY6Jj zHR2buBHxo+ZE&Q?cA$eFBs_!;J~xfIk2Jkh1S)kF)oj^%E&b&)$aH)$q6icloC9pC zshYaF-R~v!^Z--B!NI}foKuxnR(1fsdcOcBK0%*;gH{ZITrX@(+HaAWObxGLWR>Eb zDwG#DC_#m948HYF8q){=942D}9rL*ybqhBm?Nw^=f|sS9pw1;9d6~)>PDN(BymFJ$ znV?ZA3~D9|`#U%4qt2_vzxsXqi!PJ&C^#BZy7c@aFF@x7}{ zRzcwr41o}YFkBRAlf2N(Oq!}0fGFep;Wfi8!%MF$S@UZvRKgdS9&A@bo2YF@t zfAaOGy8HT~M#kXH065MAO^Dd*wP{DB%EHD*PF`M1=xtF^QDdXG>J1JS3$l;~U4!;j zATlSQ*#d{Sq(k>UWm-IRaA>>CQcnf&)f%%UZX6qqf02)9PQKcE?gh82#wOGXZEj(3 zMO6eQe*a#|j+r*0cW)ie8AuV5E@r^ZprD}0nU(O`rO7RU`WSdb=&e5V*GdKxnS))2 zOgR*4M7W@|UZI0u9@<1v`$P#ewuM~?(p#)tm<>pk$)yjSI!?oa;q{Hk;=Wz6Ww z-a$h1$n6ScF?F5p$8T4&UVVOBy;z&*yITA#V&d{+`g*@c@jKn!Q5?e6ut1sZ3D7v^ zH&_Zp>Cl6ckdtS_xV0jU+ias3I1l`=SFf>9g5eKc!5GMFXklw>1=*gAM$mJ+VgCcA zqZK2K{eFBmZ>oi6sGy132;KeuDnG0x2T2?Ee4?+i}6qEf=gol(j7YL+CR zgX5*$a@G#CvI5KzkjD)HTBcTJPEINciqy2Us9TIioG_LW3rk8v`A+B2Z zO}ZZMil1Bgm-+F`8=yB${p7}(cF^Ke`dZbEMY*v1A2>9Fs4A%{GZO~twFm^4iJo5t zD&kJp6i^D>7@Tb7vX?e`|I#KVte-fMFZp#r2XFHGT4JTV{Mz_h90o)x`?L&${djBB z)$#As!OOdD!!7j>=Z5beU-~_N?sqopF?@e-`20w8&qLzNId7C2?7`HePlxDIk)AN_ zHu!p9qeuSBYLMn&4sUI5rzGQ$dQ;1(Fe$`^8G(kw!fi$6Y>cnkuCDOFXbzjXW3m9sQv7UHOwIOi`jCJ=UB;M#GzTANsTtUK-hDAXZ zZQaCFBSh?Ma?PeBcL%(mKZkU80-22PaCFZi%~!w#=-5G+jroxB!b|?EaHr0qgS@X2 z$+U5U$|ol=nggPISSa6I@Lu!V&f$?4_p|(BTwKrL8)1G-A^1wOJOExbl;ItBtFh(8 z4iKWZZ>s{{4S__wK@6nU!%9HV<2;Lnqjw}nny_0(Tiesqv$n2oaA2VK)yJA1G>(Y- zamDq?B2MpR`iUx;8Z6_2`zGB0Q*LNz_-?uT#pI>T;F?7q+BcR|!H%)0kyPtf+|m{y z9H|=|I>J9Z!%Ho!6i-A`bvwu0R~q%g5FCu1%)*clNGe+02Q)1(Al{qA98pG!idd>s zaDSOtoHmH>}w<3ZW=lKsH1X5fPIdCa<_q6|I?ajhAvV4Y5@^ zX}nrLfQl$}x&uwl)U*ZexP}G*@j6zNds#`7Q(}(l9DF^`=3>~wlxGuU=y@wz?Rb3p zNw;U5VIeZ(#PiEb(W?_lfG#NjsdV|x+`NnT8h43YuA0k0@e8*UP1s{BzG`fD<7lj2 zJAed$3O_rVG>5XVf#4Eqkni8WPjKAcNxGho9j|%EO;f#GF2>!sEx;dm1!y3$va-N0 zEDqBtX=$@Spek^P4yHmlu-esSl>IP5u;p*Ed=F^*;FfkPzfujJ3uuwX_7pVbh^o7lRqu3WDSAj{|d6nvh9AyEHOIq}jWeg@cz(40{FglT%WL zQ~6}g%p`8d{FY$OdGRmLE7Ib4{p(Si(Jzy#cW#FesWg0x}Hz+}-)Zo}Yk(m=vFU@#PMmbid^* zrmYGV{EYH=cXn5~7PWnEGQ6)gZYL5lF(kaUI0FmxpF5o5G%@T#I8pLdgS}Y>xGm#2 zBC)TIE7H4J!Gv^x*cTSiB2@n0-S#@8pxyTo#P_?=x@Qq79BIR^XSA(i7OPJ+0!!g)&U}c;Z zs-AKZZdyF&u&_#muxn!C3@Jinm^Yf7<)Mp8MRGhw1ir$EB|5t1LZnYB;r>!#`Lf(H zS0FLBJx(hB>V7CL;c|HthmM(l|H*{EboMvay~xVM%GS@4wm89Xq{DOx?9?fnegsF@ zWD5w#A>kX4IKYSRJV(vfQBky(`kxOTR>f+f+A=rlHrgCd8%Duq(NEgbhRVSEgt<`F zTW5dqqYkdG?`>jc+x}{W{FKQPMqR5!z~hL_j=4EGM@L5;WX zmRNbtH5?iS1a^w~+-r}_d+YDj5x>Z!uRjpfp8$)3<*)JOS{ll?db)e6K22A@&qz3I`&qAR7h<8y zHGwz)*$s-MBGeO=ocJFN(YRZYJbikEPb$^djChPY3~H$VJ}ny(s~;>Mo~4O(1p34Lw!t zgz zI_!k0?wOg7;E4gCh9#dD*gT(KA?ws+o{re|%KyGf`czFL(<6gS11s&*21dGNxMUIX*(%e%Gixa=E`Drb(SEmD2~1oN#Sb!<@;ub#U^A$9ig)vE+CBzd!jF zt5l3Y^95#i<$~_$80*ToX=G?k=xM&HmSkJgWp&!Iz-f`HkylqZp*VdlT%0oWkkY2E z?hLAOILaWE8=2h?>BX>V4GauK^1(7DBQLfIXAi}{E1#hpo0mrbXBwN2dt>#EOm)>= zH~8>SN<5y(SB*N4mnq&lS^h$}9MHI{7)iBHTT|18`}^=L62w z_7&N}jb*5?#UOq3CaF{~Qy714X_JHWM)vzSl1uY$!f5%zx%CYGaPO>4c-M(a2Ix*Oyz1M6Hzj{O6&Gcz>-!ClB&5qN@g`X zjZl=S&e*#JFBfIe#cJUJRyZLctVfdDw*BojcJQXg2*@zQ(FQ7nk!kNhcAwh#6U_cw0BK!Z_-&-n&w`N*m_CFF_J2P?TtkIa z1AGLSY?)IXDTYN-_Dw@kDaNHuV+p=Q}E^h;-!(DKz8dpgoe?qKu!o@e^tIC6#yAEJ>R~4o0t%mkoW;poTmjW zJJBL{p0*n5>;Ks_hg4~nOV1os>~Uhu)o$ft0*`D6sJiKoW|isd&Yqj_ky7LPtt@2k zl*w=V{nH(I0^rel{WSALXTupj7{YWtUZ*e;Kl2S}32LgU=;fNg^$g%bNpbPLz`vQA z<8id&3nEgF={; z=h!`T=B#6c#|#B?Un$dD+ei_I1HmR{dVGhY!rn!FmM%+JdVCx@Yv$AP>S_FWthW90 z-3Si6aQY}U;A%k60A^lfo>3-UEA8__LzSnxW^(Kvr{lDdZVZ)*96Fq( zoa5>lLo9`=i(~m0TN`5A9SWmkC6;a;9$*Ib-Cww#I{=6~5ICrZ5;Bw``C&%66m_-m zd>{?Q(ZK=61H!~PP(1+G4J((wqOQK44ir3qO~KtJ^)4dhTQENR2=d)fb#Gv$N^gu^ zdkpJ!0r!Xdcv(-)=q!3NVkX98Zt5BsS7g*eR`*ngh)*b6x#nD zch%yZpmH#(H7<@w$Esrd|;gk37~94H--=nr~U6goE+_y(}$MXXOu%(?IvcACwW`&t)8dzMFT;Ru555J1qosDwg36goXp1R z#Vq;^UyV!Oy?gOz`MJ_g5N+j{Qn(y`u`>%FY;(}A=FZ2hU6BhRN9863Ckk2M|{=~`2#vxVIgY}{ja;T;P4py+|abSXHj~^d+ znWjF|FUgaN_FbfaX5@P||JijA^et$B9%YLA&K;J?w3L*dihXFjWoAMnZ)&aRnwdF2 zc`^s=B>2Z29zPyCC0%8{4+IW{U=Ap+x3{-9Hi`=iS)FXa&WOydWEe1Q^5o#=j$=|y z#l^#ee@XB{G)h)ZG`MGxE9Gwi+yj4#aR&Xp)U`v(%D7G(!G#3?RA%}w4Qc7?PCGO8dmr~&0mB%boP^NA zi2IOb4O?axVj?1Ase{1j1oTNYOc|0%UH;n_CQ3p=qGN|saL@31nU5p;Fvu$2qGor_^!^iKhuBi#l0_tLa9aLto zKu{0ZF<25In=$yUp04f!6Qqhbx1rH>7;b|SFeC!$i9{9wXoRi3CDvA@2)OB{?}!Hy%vL@1SjJ5K9WIGZ%AT!D14ikl+*Y+@AOeF4d8?jt>zLm^u{a zEJ|{uqTuc5z!+Gg|I_iJw3O2eYcR+?ffMX7&=3qIkto_i=trH)Yu?G%7S>QkYX*SB zJ|=$QCCF!h8HkLH4ZqLJ^ksOs()7@vi^k)tJCatSSRM;&d}sQx^TD%c!hT}@XP(H< zhF$Q(J>+L9RgQp`zWxg;hzW9>yciFNN;{M0!HixS`>g_u&Wl(;l&8#U?C_8u@SlG& zg+|Sr_aX}_tB9YV$R2-332P0YL2!DxIdDrF8yW`f!D00IhA9dPN~$yFXI)_6gYJXz zD$`g7=(&Vbo`FFV?l9itE&@p@gU=;5*M7A6<7QJ)Qu2lK4Ja|@^_D!l4}?ru7C_l` zUI$w#u!{h)Kq6)(<2-ZC-XtI3%l49f4p1T{l2;#aat0Q->f6_^L?1vhZT=Bra(`cY zT@36nuO4OzG6VR58n3u@rU(eQfs{Z-Xr&8ngPMf*at|gIfW{gurA#36V_ws`e)sO( zska3MpJoRtOixoRcLB{A2-xTsx%!b2%114rDDJj|rbE${$;=p?3C|RQm0Y*RI5rRv z<5ZNFr@hWPXK>EK!&3x3PI{Eo%NSQw)a&5J7E zp#ZoDB_(AYB+Duhfb1ChVu*tS?>VVIa_$_r1%%8jm zpaiw!=dXZqdtmmFl%(r~514pai5`+fiHM8SiMUG1Q1f7wCy?;eMb>GVPX;~D=vMD8W)Wsrh!G3k zn5Z;wg8*FZx7z!xzGp1(6MM3b+JDv8)BsFWM#}-`L-&N-kCT%Vkgv9vs}WQ4(EsO$ zRVy_M(tl>HCgXEZ+UY0ys1AWYFt)i^%N`Io4_o9EXFxn|dU>3*CuM`;Mj+$E|0)5& zMhMQ8Tlbz_Ct6atOLM38OHhp`g<|WJF${KwY`1ejwU=Zq~xI4I4?_;hq z=PDw@T+745jTw2YwS0TgLRl!2@w^IRXG#dfY0?UJ9q{9Vf`I}Fmt<#)3Z;w%{g9^0 z?XWsyVn5248VRm=$yT=>wQc#$x)vWFOs2UQZDS!4yF2U@u-MszGwAiIrioCvtvl>U zirGZhpQbFo2yMCjUgf}O7yg<}hr0q6BU^F?{e9@ya8%X!0y?>2txU151Pgw)}q zUyE48L_lLq0j>qH5Ascu*%H4!uQo9W66?c_o=W8X3%q1A?=Rf`a9xq}1%C3r6E#)2d9Jd){DS`vNopXc z^MfYx2_rdLz?!_zYIP+cnVgz42acirJE(1+>XC~2sXCC}6-IfoBwgruw{e{8UzUWo zs9$E9o&5Tu!7)NyBwc}pg#}?2!+m|0GluevIa2CqBv6Wh>t%a$6U@*0OiXVXWM9-e zj7MYpK{Ch2x1wRV9Re?p49Lk-z}gKO9p#F!($X@(s#EXP zZjpS2=2zliwlv(*_u1_IpO?S4E(ac=uYNMum#I%i%P%ceR!}ICc8-8xma>u(NU=(M z`4beN5F81f)fFoclwSV^^2ymLM#-}cV$~6MWu+cMnTIZ~ns=ei_LwfbO0xw-E-jLEU8He7 zt)M2P-UY~(CAZ$#`Y5_+5A+?10yUN9YxdGCIoz#16-`ZITxGjQV%+;jP+!O*{q$kV zK$8K{!s{@`-paEE12zdAW?KEl8&lbX?4;x!--!}B1sbG|uh;g~@8Vk6d@8$vZoDLz zdDE5G@)l-|sJZz(SiM1uV+AA^{3$6hars3Xm?7b|yZHT0W5f%X6KQf}!X6wN7<_W0 zxl-zR8JhpmlEJzHA`FVM4e;<57rP!oQvD+}wL!?@-I@D1IX`~|`vU4>ys5tr3X1bq zI(pV6hV#7U4oyh^Og^Uj@~6kno#&p6!CUgG4*u_ddu6YR=ABUuhFEP?6)%Md{7gwHL9C>4z2UkKODn1Q^n7Ij# zZ>3adei=bEj#f&e1QS!@YGiD=OAF^zD0WwD8H^?S{P_*0ow*y?FwXi zhoguJrD2aO`km}w|ExZ;=rr5l2H@(|`TP~oxqa*kW@fh#Alu7?In$R=<$wns@&c@x zV$*YsDQ=qkp6^e+RcZS~lNDgk6%%hu%f?Tn_I27~#OSb*LA>1Mqc?NJCp%`_6hZkn zO7`DIK{^HWv>dwyWUtr3r5~%g1}zcq1w0R(fCzx%4SP=@7#*ZIKN~2?ApL{Ef z{qW%#?AEpqxHvK4!t-KUx*h98DK#9gk&L4JcVesAo`mMVkCLBy3HYRck!-vRyP#?~ z8hRgyI5;^!59Nn70FwC}OdUY|v+ z3(*1J@Psddta;M&qchfD5jle(GRXXkAEWv)TR&cD!-48%$*4Aj;(=Qo{0Rw}nX2r_ zz~NSMY=8jY<>jH!s@vJIR`QRJkHfns+=QeJY&|ITRg{$M;l1D;sLI&eo_4p#R%8~1 zTwr|~y5E8$CKx-VL1?D0^cT=lQ;h-tgu@5%*@dtqu&bA{`3}#Ib28!mN!_ip=_58Z zodd@u9FLis-4>{4f>YRZKW7_BNU^M+pG2dXo+7Xm;w^KrNBWpqS^061ZfHYU*|(qp znw`RYdkj0i(y)37dSECSUzo-D7B!TY-h*p<;Y-H(Ubeu?+VO*jxc5wKje$=oYgmr zfJ--xqh?)gCflEct4g7>B1nlwb=5*cZHA(4gq z6E-^J^HjM86UT*#he&Iy_6`lj#m3%$x`n8#JDP>?UGAKm;*-GT0^dX2KVF?SRGyU2jPb3G__kn+QD03-d zf^z{UJ|^zJ2T{^+%t15Be9ssAHU0xN!cE8l++XAuerF&V0U*}RDznbj>i%X zRzNFUAteKlLvYLoyX8Emn|a$#lwc)eHStBK&vK%EZtcYmh35p#=RW?3o-Y@<(|$+$ z+^k#k!xTp?K37BYp6g~@n%deZbrjUpGgDKU_kGD&{r~6d9q}?SG08#}BWS^Sf3uJx zFQ%~@p=L5^&<#`EA)w@k&OX9`63Y^qQUO>nY)3Sgd`*Ob-avD$+K`(&@U&r8(mXEG zF+j~2^@)F8O}ZH;Qvd$#c6nQk8O}HcPHlJYCmiXMb)kY+bbGUn%Rhg%JQ}{`;c*Ct zeV8V%0`X7Oe-~&d#h>lKwp=g?`uZ2{PKfMDU+cRMlrsnG1_ntpr5h{i)t@b<*!t7}Y)Ao=@_-UT{9vt1XQkbAoPN)q~Q_@b}hyfHJq z^9HjhHdD4fKTJ& zNt!nkr)*$g2*}k|S6AWmM5Lq;;Nex6hdOrM6TrTDX{ezkAN66rQh!njyFUn)6d|`W zkXv!_`X&vo9pAtEKu^!xY`Lq$+pbs$X+L1N3~`3;^b$JG)Gi}K_V~mN)}Z$MXNzw@ zs3uO?Hyi#GxblgQoMXiC>FNEiS{)G^ zi_1_Zqo(EXF5o44jNsc=Nr#Kc!Zc85^YEb^y zDJjQQp18Nag}sG=Z5i+|p{4tUF7T=e`S9_k(ihHt+#Ef}Ck0Xj3493w&1j zqWRg4Viek)Tvk-uC(c3*$Hxpgv58leUt=uWseDC^0*8F@&+t=#eOF#?jQs%kWS3|Yp(z3WN7 zfzkn-Kp_qhwTMS5^u#Z1Z#qsRcjd_8WA8%ZFup_<1FDIG^5~-aa2<##xqV!&(-N5bWt8S z=RyAnP+jCMURhOoNd<6hW!|mtPbi7F)=Z4u0YEG^JxbHrrCXf*mNF^b#}^;=SE%E{0xd47N<@ZoasBz z5*;owqVJ_Jyar_n@hS20V#YV2)yaM<=#h!k=^~l}r3(b8rx4{r8wA?0C@9TD=`6?4)0Woz#BqUr?sI>6~i1w zT_XB1m6Y-c_VgO9@&k<9AGp(JQuH z-V}-{RVG$e7r#}e?dN^YlmGp5Hc|6ef*D|96o`cBbf3Em7J`s2=L&davkb5U-8^mNf3c)}4h8~S>CL+{Qa zdvByW@Z)@19s~A~3A^k7F+M(m$7?v@3z|uP0ku=w3e(D_{U%70$oLI3+bE?hz{qMr zN=2ojICYyXJBO%&PXS@3LGG*0wB=F1TivtbUtQ1Tp;je?MFypDOnuPD9R8(Oy z2jFkEzcBVb6wW)pLOFbaO+9{ZQ}Xif#-DtS6ep%$+r0Ij_qea`-PAB7-~FOm1g>~k z8^E>6Gtyq-hoUna3UDFNWP$y5m;Lw_J;J%r8S4D!{$w}0VHIAoKdl0FNm&^<7~0(T z86C_Qp>hEnoN&AvQ#l=-WGM0|h)wx3rG0^X1PWG=20WOJ4BLmjPYXj%2#xf zZTX^WM>T(V=<;|AJ|~&r*@KAY=6s6>bV1qFWY`_`E}zUNh^4-H9n#PLRvM94c2oPh zioRiM|J}34IcvL%rLdz36W^A8LuMlYA=rYHk}@snpE5D_{t!XFA(&5!IINc!7ZHrm zm);#I^_^os)_k?uspv>(n-=~DS-U@}z0>`LQX@wXGavEC6zbf=9qT_<^EQ#9Ig$ zWVnWg*HDn=6Bb6FM5#`>S_rwk!@HOp8kyaRlDBG}mJ?btT=Bh5^lmnrTE;Q8Epzjf zrcc~%jMdv`t`t1jTbh*77JH2f2sKP#wF7b#6o8ynzW(}bJsn6Yqz5tIX&pE<6{Mk+ zS{0(LqqA$|*qdY?E$}{w%S4`Pko@60)qOv@sEtVM28B&E4h$TKyDYvI(yM@~>~8)B zh-1hl+<65rBO)U!VCFCw_u=rXt*HTC+HIIhRoY!(_*x_^m_aNm*|9 za_FPz*tf$h_x$DB@xz?~Ru}%7IYbR`c}$#~)FdIEs8pBakN*J#@rq=VJVAB8KTh$a z7ZrApFcKMw@K^Yw^mbwY=~6P`t_6>;wv2AlM4bx3Hl&~!9tR0A)YI|#M7h1pF$`pi zTKN#40CG$l4=)a?fz-h3KKk=Bi)80*Qqne@<9M+u_0Dw#+P@pugBh{5sBy+TQWpPY zlBGV?>i*|}kgzDo4XyW_5H*l51U;BSIk;>8EZ!*Kp`0_Smpu6;B;|ZfoEv{U44LO} zDH2o>g~qN7xfcmTGRrM&ZHK;o-P_+Eoty-S_9bLv;3b!UYHukz6`yH0unb^TO`yvv=DldO(eGoQhF68pryNa~4_ZZ*8Y!yxuz#LCq~r{WXL8QI(2lz zH1~*;>HosSfdb%X0}vFqCZu(Fohg*iI!Gi(^B(r~{qX*P$}1!kx{v(3N%*ve@CT2_X%%Ab*UE0Q3V!>Ec$srM&Kq zqr>a|1xFg3?euX-(%@SNyRsRaN4wBKc~SKabG%b>(SCz;2R@*4NAZ!`s-XgVEoYO= zssuyM@~*p8s@+!1_BiiI$@vj4kv&X^!e50~(BsMp1gC_DJL79Gl*UrrXXWD~7$wQy z0@N=YqRgDxLGby@h$_U`4J;(EN74M;d{l_Bp`6E}<%u+zyGAcojR)3|_6T@80dYXM z0%a(GxWV>^b<}|)0kr+XhS;XtnvnIask>7UI<#ef2u_ z)Z0Nc;z5}3padIJdA)6WSN)=)ajno#OQYdVF%OplA3b=~#;F*(kAhm!P9-_H4de?@ z71ij2)xaF^Kyj*jVS#S+T~QI+3lk!xNQnLqVDfRRPt=TIKpf+JcY%tB(+HTmeR%`O z1v+Ffefn%AF%N^w!MHr(#wOf>d-_h2cTat>Xq38jEF9k3+t%59t9y{nJ$d|Cx5@d9 zhMt_#3b;a{q&5L{y3LIZ37>P~@B!S1WbPw^M`SbN;+~`vZ|y}ScTQrElh$#jBz)S( zB9MOcVqjeF8X_79kM6RnD63>jcszk4%53R_FFH5(9Q5&5u|a>0O&(%ZT-P3qS;Yc^ zURHK%a8rkG40LrNPhdZv%5Q4&++Xg2{Z36;nL%1f(SfNK*L3!;u6Fh0_q5K4H7tw% zq>3Bol9m=>C5@Sg4zi~pAQ{RfVne#4`sh($Hd+o267^%6z)U#v1utdq2K&Qc3|ov? ze8!F(z?1(gn8LPeD)bEqjB|yjjb%hU!Jw_-MY@Cd>G5tNK)iF>xU!KsiTFJKZ2+d2 z#}7{h`MUejuvvQsCdeTm7=e%n0$=05z%*uRa&&OW0Vey?r}g$rov?jF+x)(OdIE^p zI75`eERZp&Ui1@(J6Hzb(f5j)z@qS2_hXT4lTNFMH)yf4mgl6`+&$l1=y-7gymnG{dY5XvVCXi4g`~;i{)% zpri;PKHGqRAG-Jc@jn8j2|8If#LplLhbso{7r1()9hMb1hZjAUeOvB#i+sE*Cbl$u zFfUUY`d+mW|Fy(p9V|jo*3kc!WfX}!P)y$@CqutcA^Zz;XWRf7P}glZQXuv<2%9w! zTXhlFI0Iv26~>f-MF8YRuoirdEz(Jd0J1?JEK4unwxfoT4iXKfCa4m?X^2GSH?=WpOt5IeVXm;SF-L;K176OC5JYYebKi-ML%TB%o`z=p*WYpZ zVgtQ2owzek+mTwWmXXPL)q_DPQyWhNUfPG#B7di&Fir_Rd` zdvIS3{tk>VE=mx*!EbG4TA_f9A{`zXSu=m1QsNuS%}Mn((aPNhwm=OwTJ-i+fjjZKP3dT-V zAa*+{Q3ookJvy(tf}C6@2!rT2Ko<37WQ?IC1Yan<@i2%-h23e{UhM+bik;B zQ#}l3xq)xye@nSDDN_owx_yE{owxr8`F%w8)4xc!KcmJ?K}iWqiZ(}c;-p=W zz*}rzb_uS15AmCVOiliQ;36gS?Q?bYYDAd4y)*QX-L%tobnX-kZr%X#b{e7%nrHTi z4aSTO?I=44?;}oO{@cpRO53H6fdTbp#Jdmu{tt*AQQo6tY09B<>@do~sZ(%!_vGC} zo%y!BvcuQWKJw6JdyNF`--bt7RTu_QX?gHlxc}!>Z=nHuXgIkIj%d+kp@zuF0ce9k z|9xp4Mi9WFhbSCufsD)GrUqQ8DXcw^0)8=<(|dVDt^cn7edN=+vNy=SadouW%^jR1 zBx+-&*uANLiQOb7=BH)L)Xa6(eQ)%UJjVh;jPyLeo50}!_`UtZLzuk`zXq7gPFX#t z*0GvmOUs0C=j0Dn9_lL|U67q=p4&+dhfy@I2b*rb|8KpGmJNGsO8tdWC}Tr@adAp~ zJc4wFgaGV=>4-MdYd58FQ#V%|g4|TsIVnM*o&`Wct0hw=zMLc*khjC*b>V4SF7{w!q|&+v7}&ig-P(aEoK}XykIEQ=VM4XKDnT#~#O}BWxdqUfgVoQM>!a zfB2VJP9NH-e&ss>-IWMa*1s!CbYPsoI&N=!BcU3rrtB{P6+hKS&Dqr#d|6#e>rIQTplXM^ znIdmh;5|@-Y#(40P{4sv6n2)DShir#`d3SYz1TNDncU`0XTCQR;85 z0}lnf@OP2EQA-9jy09d2k*rUZCJFlXcn~6#QhOa?m{K!;39r!g8*OfE)C2GZ>`#C( z3S)vW4u2CY2n27Tmud=%S4Kug=d!_L6f*mWv4v;<2q75qFAE6SFJvF~i{aJz_oeas z844$&r3lsQsx$Ov`Nf7Z1Fa21_A#5I50-!~&1+Km1@ zFX}Dd<>Z)ndNu_IcLQMoD%Kb$FoX>zX$;&Fi<+DBE=;}#r@i<5s@^7eCECbOHo9G@ z;FdDTVaJI-%69W|+U=Q_`82IUMtg8v)}7Ii3eK_C1L};(jhbi{b;_Q9>W*WmoL?Xm zj3MWPn@1Y`J~816Xcg$vJy2+Z!=KLgR2G1V(3@NR?1Y$nH)AEO6>E{o%mC^O+xL;} zx~FJxTiS+JF15>iU`tXE37Vx+;@GSxIXIZ78+CS!0 zDAx6e;QCP(AMOC6Te2uMUBMR<|Cvt2gZt>^_%!@pv+<*H<6c!S`{MM!ysl46yKRI* zYkTRu9qrR?0CyV_eN7)38$GrwT3JZ3)hKljvnafoAfoQeAUd|CyeW-(#v#Me+)DjP{^Eq| zrb{ESdLxR<%k$5_LyQlU2a z)gJP4^H3OtyiRDpVR)rV@mJcHH8jPjhEY4G%DFi?h^LAtfdF&ra)ErDxN~fIc^OhR z-;+5v-~v0hk?rQb_7gk8c`p8|#xn8}%hW$L-t+IlXlRUdzNPjgv)NtoVMCX#+Oi^} zE$K(jhu?Ah^i#iG7a?r~78z*SA*gcOm11(K3uzpkvCJyWUr;j3$^UzWF>^;SK6N*{ zX(fczO!oboR)T_(cY`i>B*g-H<1vBLm6Dd$d94E`KEO9Z2*YZcm7To^*{<8)85gJu zSy^p?$i(}o+I<1xn*s!-5QoaBhTR1A6MI$`ETSrMrxr?}Bej#>`F%!p&wPeD!?M0| z@ulcTp}l>TrNWVSwh8FVTl9KJeyHqjFQg?=7?`u+6Nsw%49e*z^^o|4Tm=Duo!L3*=#teKRx5HAtktuc5qK1A=Z0; zoKq6|akZH*5RPIbW}*0Y@z-C*Vn5BTpR7M&i+S7Yh~fm5iww07$?72+n=Fz?estKa z*t^xb_r%1+Ku>E3auX;NLDB>XC>S{U$EiMmyYO!$2?x+$Z!QltpaBdU?AmKJkwycNC z*}dU3k!|@lN>8KgHu%AgC-(lY#+jqBx{#ppt@;I<2+= zgoRAsTlS2^`+b3 zWBI95uk{B=d}%Y&#eWo};eFqoQzqEnXBg&tehTx6>iq^@=P$SL-#*c-Cg@=bI zhZ|RwJ=i`JG&JJGd?9I(^Y(uy)gR3gha?-Z>8wsb5uJpU)+&95>8jVRxoDbA~Z%cJy} zzp6Fnllb+PmhbP?z$NfN?&$Y3&wL1Fcy_Pjoa*FsUWVGZ%*ICdPrj@U|8Iu`bWT?# zQ@tcr6I7UXV}At_v`wsZ)ii<4Iq~sd`@zTCaA=d$AkRM-)N@`&@)jc*F(A|Wu#`b_ zXB5knRDnC(NRlL-p$XFEy%R3Ia36d`ug&+I!%c;)VR3PB1qH9%-Q5A?+Q06TWte7F zj>Y{*KzSdR$D}Y@*$8Px!5_y5h|{%n zI~e|;Q`HOpXXxr5N%i|fw`>r1H2LhTk#xCar|(nmZ*De|YA97kYqgl<3d-YXR_X|A z$G=sT^&?OQ{!~YSh|jse3zJ0f)i4IDNqw33-6V7_C@WLB{Ze@GS#ylJA_-F|;a>Hd{9Y|I0yflO)X>0pwb zo1MLG@Oat0JR};mjvYftyQi^7C1!k_f>_3ixqIy}UwMEvk!<@=+ zH7ehoMCh2np3L-}o?VV^g&7m-MGchDM1xuvv^hKeY8=O(N%Wj)4wOA z)nQdpvb(QmLi5!%yi*Lt+_>#HZ<=BA;h$fA(u|d@c3l%{hh*QOO2)iwINT`-2nk5| zjYxM)=%6|Vtt1%68ZhS{)xN>SQz$zh{61C)_8I8B)VYpH5KK4rg7_WIYoAg}(9d1sFV7oX>o#AE3Uf-xFOdTiUlRsGQv_whSfjb}ik zp2LBWY1aCGqAqNyPlBvZTfEM>4T|)uDZWZfUROaw#3ktXb9ZyI)Ws$qQ zVe$+8V0wvPw?P+Oc4;FnA;Xrl-_+#p!KfkRZr_dXAK{PUxrP;oxX48&w!8WKtOdXe z2`MRHaeV9ThryQs=`$QDpx||zl7dd}#Lwz;OJ35&%y7u1*N3yb+}+y?+Q%3uc+HTu zYY2*3(`WQ2nzDif(0rcXNiYd__)|&CkYMt)B0w~e^SDL6w_!_DNT&8;f5;3?gBId$ z=U;G-P+gM4NcXZwG z6vG~W4PqqOuR-nTOL%z*?rlMn21JVfbY8#ms)n$VwIu6X+4S$(?loi+Tu$aGe1?*T zXnhaopRe`x^+3vV_w<}^cGG~7&~kF7%iR&<7nww?s=BqWV*qw5O}f(|La@jM1Jb13 zc4ooB1X^f=M#m8NY!Q+%ApqdL=!!AxdXzvgNX?IIVpRD^j{iZosj&M!1s!2iDL9*A zeL63*_2g}Bci_{9G~&hc$b^mYkr97^DNu^dQNdog>CA{}tmz=n@)*~V?W;#SyK=)D z^)tVowKo$NlO7r919Q>U=;yMy&xXE44r)U|i2~9GImW+<8bwvOyz8}{Vqb8onm{%J zXXd|`OSI8M)zE~MqZ4FgT!!W`3?jL$yy>d?bImIbN6CfOR7Nf2>O-f~NkA#A!fXKA z*c;|H_;dbY_?s!ze5?>R!k!KLPI*hl+7Ef-MG#!mk4?|aFgA6u zZFGt~+w@f>p4hbUe%4?P?f#8U$tuLYqo3q#0Q@BH)v)jH=|K(suh83s3H6TjFTNxK zvj?Sj2}ZwjtqZ!-I_p3?RRA{eD|d5<0hmYUhyExRm6vl85WEEm;#2Jr?*&B0IRIIh zIA&(PnK$Zzt4>xAmiQWVdH1UAw=AiosMvrz6;vyDXRsph8%k<((Bcv$=p7z?9N4WD zNY`q216O*@ojpP`tgWiUH;gErq&v`3mLc8&Fr4|hKCj~&U;{vJAC(&nBQR_Ml~HiZ zCjl`ENF<36(xr5~cE&h#Clk{3TY(Vvzo1h02MzmEfrUrN8}ad~446tWU@QzE6=HyS zW6>Z+-Ck&;8%1M;rpbCX%SSZ#F6xIPB@H?J4R=TR2b1H`^)@}5%$14hzu%!^DU}G< znkY*-xa0b*dYTmCtj(dYzFH%b%y~4;26Vy%D1tx1K51y;_fx`SN(L){O2#WjVl)^g z#B-YwK2wF5J1n2__#&QwACy2=!`j@>)tFu0~Df5DCoVrv;$=W(|He< zUP)0KHKwQ1Y(fn8w_=Nmo(FaxT-3ac(cec*!C0=86y1jp!yx2vfLgmuI}mT6v~3LB z4~NKBVUtYHwOkRpJt^DbfM{yTeq!BgrkLNR1DchI=|$%w?;z-cHYJEvmh0(H|3IS9 zRnHHTLY?2m$MeFc2DKr+0W7FWuWWRWB0ST+`Z(pyHUv?7Ud)Fq4xuRBZNp7Z&$b(` z<@eV1nx-9nhHWh0(kn`g?BVWUj5Q>W8vs#*W(Y);TwDSwQ^KEw*`BQ;=-WcQXRqxl z@i3xKnbt@2IjjFGM(7efbXT{pm0^Zc1SYft>RI(=KC+^k|8Gp}XQ1AJ2zHdSnAZeW z3V#AS=p-~>6>G`F z+E^L(`PO?*yr#nIKtL0~-gtAh>jzw|1d(MC`$ZXhR#14N#}*e`WoikZ+AJhmS-lWs z)7#Fec~!oNtK_R~I4{3|{}LTKwEz-;Wrjb7iJ$A1pG^-PlM{))yJ^ylH%PN`%JRR92&>Q-Y|uGrDgZX)56x71 z1Z~aVA@3N*)l@5=pxf3qn%*ao_?>o6?(z|b?>9vq*4Fnb&6(|^t?9?eU1pW-ou`*G zV;OFuAdd#@g%nujt@jJnGqENU!+jK%K7d&Q^bS83j(u=BiS|L71{Op7br%LV7(ks4 z;2o%?j%0xp2{BBy1;^yot0Sndskn9lV|VPMbc<jvE zlk;WvklYZ{1)tks^b=3*gb?TDGiBBW1>CPHVPi^7&{V6{MacWw-ycAdnHq7gQiepR zx>mlwN~RO7WEThz5W;Tw^1xF-$+UOC%p-BvT$uRxiGT9`Nuud}>&wql%~&Ox8a-=v zs-Jl_pINt(+gc`Fey^ktl_yCrS!ACa_d>wU*CCVeA@da;!X#w)>fyPw#g@jQ)bOSh zRjL6(_jgpXtyQ=X!GRrM(ea}};{jO43R)*5-;CwMUPveIstSpIAw9@-T}ZH>`CBo< z8UPj$l=<$??NUN@=@z}TQv^ofVuL2Xn=Iivzm1>+*6hzy z@#NT4{U2SV!{YPlnYBVGta0zD$~gaq6e8gd^7Ur|jX!1jQrwITshucaXO*+D(jiS| zObZDO1tkzffr!!^orNU7Q%xs&z2x($xftW0UfL7Z$SsnIrZQ(lGZE~FHEd_FfGSrhu#57x(a z9q?%*qoeDps~bI!N&z(nRm16gdyShlDsAqLcd6oT+66(z zQX-2tAOIVEMFG1m{jWC`@@I!mIg0W)X6*zFGcr2@x_(T&SOhDeOzNSD8-1s``BWxf z3);x|_%dx8=ou(Yw!rRpxQ^_s29*(R<_JQZ_SjyC~ao z3uRDwL$g5h5P8mzTJsOSGsq~<5#p0~7TU6Q*sbXQRGD-^t1IwsZbuGWxFZ^_6U_|+ zmFvmLN@g6o^gOIT=um!$99(H4Nhk5K3v5FwB8I?EU#GJcZU-*eGe|`edyD!`L+$Ku zvr1A3{P07(K;StN;sv2eRGB?qx_v|%F%Tu>&$adSZzn2zMS+Fkd%6G6nXNoM{_$JM zUNi9q%5HAjS=@Zp#ln#nh=ZXU-(*yR(*y1FCb&|O7QX#U!E9&oJR>|RetQRMr1UwI zAU8YL3j$9yh}*&VDpQ7dTeuG-2{x_OxV~AHb!{1WzFj=8MQhgZmNS1g{$-Yj&7MxK zc)onq+lLEci?2YBrYqbb(nEE8nnf}Zpfq_B;^IkV|IWB58q_@|Z{H1FM5zD|AO%Xz zAVxQq_5uwJoU<@q5TukucpPx{fo}@4&DqWE;Yn96rVNQvjIrI*$w!B)5@rvp#DgR$ z8(0-|d{O!ES3XJK(7=s&DQ0t!Zc6J$Aa7u%$0I6;0WTOP*kq^o^!3TBJoo#J#H~qQ z*U3A*<$v?{X+_bS{-D`Nmv(NvPNx6lft}w@9I8N5c$*XOekw1Ie%`>wfapSsgySlL z-OZ@Whd8$RKSf;zL&`v~B|*nMc}-0DR@ zjsUUVbC(X0+Dtf#KicM;O%GM5D@~&Q%b#;JWNKn$)-sNP?~Qp5s(1?T3;4veQHncR z;}5g-LqkGrV7+izLkEJ-dE>FPv^1+?=dFS)iew70d#IHCfGa!{XB+A`&E9nkqSP-B zhKZnvIt>k5{j3-D!})%(Z}0qXJD*Gy#fjBSyDiJO)heg!ZRzVYbFZfB9|hF*WTqKT zuDrQr@ypC`PQzJR%?;Hv`mszA2U7%GlSGg^!p3dcu(JZs z=hf@ioipbDf)@9mlpbJ7y+7$K<_*+hMM7j%^>5LJwmiKyElorw&w; zx8tD^A8dNa`722N-;F2|md7F_#TS>?E$6+R5K%nUG`;Stk!oi7_TTjZHmXwuxJrOY zZG82O6w+eA#^_$dz`^nDf?9pqp-Os1;rrzkJf^Kg>QSEC9 z7is+z*Rj#@5Xy%?mlrqr#DCSAUl+bAOnW~nR=`-Oq$Frl$Mfhx^H}ca!N+o6ddf_c~vLZoun9}!m{`1MY&(4L*u;}iv z<2*OVy!Uc*8D)3F&(EC3g#9d9#&6KD1#}DFsuWtBZwLRL|Cd={j! zzNs{Ix~RRsNaIc?h02D;l_M!TriP5oDhD=CSRdHfwe|I2uPuv>9Rg9S{wHhYx!+Tz zG1h&)l48+YY}1(#oZPGJMHl7a&I<=9ZCZaSE%8L+0TG7i+>te;3m7PsW3&L+BGmRaimw zy2FQu?i@m0o;U~gy3HL}xDyUlX&lCXuDlY&2@IG!QFeO8&`qyzdc_t#1d0~0zA9v{ zSFEm`S3<>k5JDjBvON)UVWAHKUJv0NkIPfOMthXMO5F4H1MPgA=mPBY2L@>k@U_f~K>=}TReE_uHzET6O$WT2*W$h;} zJ!@KcmA27Uf#q*D3ZB{wUmF3vcl~utQ;Vy5d+T4f;c4cxz^M z)v|>q8Z!^^4W>US>1S?vp^5RVu*x|KJ&&}tYX93_y1>pNa*rp~{KA z4ExG1c2w_dF*L7*)(TDhKyNM|IhcFp{Oh1Qh{JGr*B=)X6-O5Vdv)B5b$m+KeD#&Q zEw_SAd0tO#HE#J9l?xlIQtf?yeq-6bj2N%SD#Z9Js=G4*_c&2)PJIRDeWEX#mP7BR zj?FxGDqJvRaBH<-MnXnrxCo4ns?tL9{=Cu`hwdW( zc6#AW)#@*`yjDJd`OT5%m>ou;my`(3_evJCsJlc5^@xYBngz1uOeY4aTCQt#D(95? zsT8`mg)=+5+~=S@aDo?=LN7N0dG^O!)04=s9+;*@bs?T$xLBSH{U0VrJO)dPkK#M@ zoO?oDj?wz^A4;b#)58cjXN}L$qD10RI5$$V(L>w8dTA$jU6hM|E;RAR8VugBNHw*! zSLYhcLB-KRbwW(C|Ao(xzL*8U8L1}VGHfLDUTW@^xuOL!pRKY>Ke2{se^OOO7<@CU z3G8wSZ{OxrR%&otGv0yBDw_jvta9ng%cfsDDvZ|k2O{9w!fjDkB{ZUHN)G5*-+ppJ zI{tYVZ|u1In0NB&{mbv|TA98ZsG_c8&CcahocN?)mA#2@dz;4Rb=j7GGJUe`?{0P& z1_5<_U0nm!iHNxPWV8PGSU`%MoEYJ{qJ{iBRf-+Uy;xoTS7(DS9?0l!|pMAW&35nWlcok019=q_Ib=(?Q>yRYy?0Q}+eS!-N zTQT4%3K4wbf!}-Zal-Zu!;*vcTjPL?Zs8@DH;Co?=}+H{&pf6&8E*`EA1~m<6(Ej; zj>=AyGgxby^Qf>KOA?W9=$-Be5C4gNKk;60Z~}X-BiN$>=m2{dA?-;a0#DL=R%88O zz|(;Zyxi9fsTNi=!N-OJ&RK@Wl5au00VgB{cneY|z65XHF213@{%Cs0!_Bd3UAH({ z$2IoJ1ef8=(KG;a+aHxk(~c-s(mu?nz(b%!Y~pZP2MZS)<-Mw&(^h`nZ|!yMCpI@1 zm5IMHe`+4h%q$+UFfuWrk_sWh%H`nUp(BtL%lWgjGY`&oNL~}kY+XA4JL?|E%RaTn z^`!6;+B}ET$jNS-?r&?Xn}!C4jdsr(?AYq8?uK3t!g;GMLDXpw7a$Q$A!4%v!dZqC zOZrLe-G`l`EDS1zilR$YGwkqPN)@ff{PpWbg(*@oG{u-#yP3uOgk-@ z%*R~8PP}SU*EpFcEAgxwJto!vHU`jC^H&bQ24^;v!i$W>*r9-%c5l+!Jq7U}go`PC2l-Ru~;wf+f7f9cT|61@4 z{za+u8P55eKTV#n5tf?P8Yzw^27Xr*`}G)4mzN;nl^i_K&b;m)L!VSWRnp{@&AilI zct-QPA?}m1OZ+>U@sG*pd;6hH9O{2XsQz3tOJ`Ozk>)b$5oxl)`Nx3iyNLe*W1dQm z3Zk6ddLReC0GlSDA6^!D@+G=au9 zh{qM?F~}*3k%9p~;F-41N6)kCL5SL7`vbi4xH++;Bs!d4@`Rnu+M|t~_rEDQueT#f zzlm(y-rsiJPS@^zX1&JfK%_~UMEcG;uUA?#CAiOi(;!z93-s>GSBr7(B5mi56Fc8cK9}GlhWhUEVNXwrFtowSS zOD(}UFrK*Rg5=kF_(3upxYGSe`EHvF(}T2J#byJ6?Lk$?Cu(G+5;eRiC@2^bTw#o|r88F%Q`saYdC(e!{bVj-(k)HR>t;J!_-U zL3!`3bRM^%66Wo;=0K9($m_}&9LB)$J{@A!z&x| zdl&QmX;YnX_F>H~&C!^O<5_)xv~ZLoaiQ5G;R{hF9x6i1+`t<>a%ZKrXt74KN0Q*i z^1oqYWyRvO2KyD+9|uuiaEEtHHp7w(_-|eFc14cyyo$r-uP~ntld#kZuCK_Nw$}-A zHFs%PD*w@#s@?NCQAvO6p_Fy$)b|ifA_msF6oh4E_lrA@IXBkjhr1IbRc1jhlAH_1 z1n4TFx+8*0r2k;U0<#sQeGp;8kpt>zunz8=sm}=SykULrsE!lBe(b3}BldT&D7S-~ zgP58${>0~kbmwy`= zSy_C0ZfL-KxDYv*_~n!Tz2iQS=KT4V^b3)bmFCyF)v4GJTKTiu3r|%0Y=1r)(mUuC z8}(p6`lVY`y)@U*H$I-!;(#Y$RUG4`Tgej6v?$;1g)f2D%@qDfa%fnM>pP(nFKqbX zDQDe%{W!r8Y0*Pd6qRufMt{Q71tlJ^0$|*r*P};RIb}d{?L4stv|%M8tO$Z%k#PJ) z*clmkIV9i1%k}XQ4iC78*Ev|-*HoG1w}SArT9ldN+GC%ie9D08zhL3=el95r?dqH^ z`jfn@jQ2vnd&OdK+*W9HCkTXg7#6H$Lg$_zc+(=5>JoYf+h0?R(JBQuO%E$`d$X@8 z!8Zsf_?^3K5g{Pmo8 zs|LYsAWrbnUy$r6Eehc#-whWYqg|fbJM?LC6Vz{hS^t{j<_m~u@5x&J7jbolB+=`x z+_!(3*;`m%erxOmv&d|WS76ZAF7M{p6GOxKRZHVbaM^&1C1esDI*^l&-}?!CGypRJ z4ogZ3>4W5|dEiNulI}Rq9WjO0-t+3*?Kp%(NKH(icbBxPIE7|SZ;c_zQT@Rs?OT=E z$77QK6IbANxWN?4gmZAw-{1hnws924QX-rzn%M*TDmiK5tw-kszI3mqrbD05h&6+O7(7( zjQOgvJTGBuG`~oH8Es{1aiS8#z!uEStPNmqG`ZyWt-e>lA(;nXlh%3@s1-CaNqPDC z0bvF`C0dx0Q+M`nGZDNDjU0wVay85P`c^}G_pMmTEGbZ9d`sT029IE1uLeIi#c);& zuB@~xQqL^jYJt)!7!}}E0{d<@bBL3R3+5{Fg7%+>hsROJfXMb0TV3^sir>v!P9$Xm zvPxJ2*Tq!w>T3M>u16Y{GU*Xv^_Aw_x z*;IPJColih|5Y;>L|Y2^|NOlsyffCj(;xdezWT+Dpu2R?6b9Ko2uEdMs|wkvtZnQ< z63ux}j)YyOkm}Eil7ibr!a#bLO_5>r_Nih;OpE~@#pDvdWK*3YO`I1sdQL~v{jvI; z0My&RMJq^w-hTXeT*tt*jDLk1cC4s1MNuL6@h zpky3>o-x`C%G1)L%f>TzthoP?CDf!aEcMM>&*G)LpI*m%7m1 z9pH>>^||UM6==XADf!}*_cMrT z;5D`*;a|sV(X4@Jg_?#29tn2VZR^4^+C-g@Kudl0hwLo@n@lI~Q0!_9&>w33cgEOC z!+>CC*yN0PN!8-&=$K2HXOy7O{Z`A~p0~ySCv~@!`)H8{oh_DHfTQIzgD;*g@FWdH z1GFJa?emHV7K=mS!KIbJ_zUGxc7_f*SpM^1cD;8GYd(NP58PaK!!DtsfAW9BnLta`g_5YA>0qEKn zw*$;OqJSXmemnBLl<)k10q8vRd7;996<(@uZWgy2hL27D(W3#_(nNn2(M^`?Av=9` z+cmuB(&+k^^0zB}5y%+>-PAKgI+h5=z~z&FjqSphjLq=<3htGl9cu_pjkw7TRENFv`5}*;xVH9g2>Ag219e+ zgx#TVJUe)GJKR?ZMx>8_M1o?}UTjSbKV0MILl`FrNTl%#xXc^F^q`EGM`Mr-Y6$k; zQpK5y_V(<#xIO}8G*Ii$RP{^9!mSMQ%YN{{3WqpJz4Y8nq-cicXm^2bHNUVBtrEh> zlHQHv+gsFzA%_vppy-#?+|vYU3Opot@!QYQ76ueJEO*230chHS0R|~a{)-<>`o-4; z%)2aGJbi~aP!&h4VK^eJZb2k`)z3*IlWQ8D$CRt;&}$AhHiwxdUm9_5 zY3;EY^e%K#-Xe8~aR8r91D!|S;fyclCGFP|AW>yY+FNB)&vv0=+!H7msh=A%qQ0$>71IizXZ`y2vw0Db%c9c~N_W}afg7zFs9njq~ z?T7gtI>g)4N$#@c%i;@uSH8M%ch4NBSlip*TMJV1Eg4d2Zy7&UIhC-h1xETUd;0(W zgR~7gL*TuF4h{^dxn7*i?|F>oVL<@N$o&f52ryh=J}`I{FM+(c7Yo{#goH78uTXp0 zpnanT50g~~*~k&fnPxbK+T{O3#txvAv6PQ`zws3Jix||$z(gMjc#%^^h(IQa0gUn# zoa0(oM^AcaQ=;1<8>PFuS+ul*(hVv| z2%^%B(nxoAgQT>ego=cK(k&$*B_K+7NU3yubGhI5objFW&olPen+Mjt?qAG#%_|^4 z&rktdkl?>xE>YTWOKvym1#Y~)d5DYkIIhwsAQ8MSDXG3wbUk}uUlnbkKe2H$cz5o< z(nRzDP#s1^lrDj`%xBa<@z^0*%!|b=HRt$EcsI-b7)rdNIp0m&XpfcQ@)V$f0aN$G zogyeH#xsNAPQIUYGkRwMCy2Kd$g80i znb{Jp{Cc6(xOe)Z^YyK13G~ow2%;B;OF8J$6e~0kJ;k<xgC;#FAk7L2|gbSw|QgM}QTKNRMV6pPqVbjz>3q zC_-m9Az0qaq#dlv~B~1?(a`+(s6qI@p4p{m_13s;0(f@aW{r{I-i1JOisYOIZ zOY-ni`J9#s!fc9fI7l(vA&Sl%zPvpm8MNy62Wt)d;Nc$M3v9q<6%up7>bN=fLWGHk zEwW3^`l@i9QO;GyCzP#zb-tGtXH3mv+Dj~sflRpIBGz^&IO0&yL^?j3s0q{>6dxYGT){{8{!|Roc_?~;P+L zZ(Nt*wAv#Qw(C(T&!M@NTuhq`PU>3vg*s9L;u03MO0)0#KzxE{#*QfAq>eS!)F6xL z>cg$=(xoZoIB-;N;wEQTH4Op5Blv=XwU~7uL(Ui)%1$pZ~Fk%E8! z9%W2V4aCIA+UTPUKm{d@pc}$qkK+SVn1SXiQ(* z`9Q7@kwKX$D*Okk7i~m<4&74wf1t50<;gB+GyxlHM}z;ucRiR9Dp%X=`So?2T`Ne`LP&_v?Q%HGI={rOGx852$5J$1niF^8#Pxc$$rCj+WiM`ZfZ1 zGC#c{O`+vx)ep8Jpd&`MD4dn(RB9Nqd4^xW-*@2;x#|fZYd9GoQeKZBbV01(*fB(T1?z zBFJ9cvqNXgl=g+uy1l9C%NLR4$-~>&4rL{FCUY0{Z$nJ70SoEZ5q%a{$?Cb1Pm0NZ z;@|%dgYNk8Tf-D$|graMYAvll`pB3 z+S8}2#Iolo0oF60SqYmuQw|cwiiVg42OET_X)+mw_d)%%Jp-LzqO2uQ1g)fsP! zxvT@^%A^<|fH6sW*h3T%_=Tbuwa4UbHMS*!`;@VXNg$LHP=;YY3K{_V2fZypGPH~U zvw%Ck9Nw?Z%~L3>H#auG^5HGKqEs)~&xqTTqodRR;7E0A>ubVWq7MKD?Z4*D_(y0$ zXynKGM-HzN05&n^6mHjOtUXQmeH}6pmQBlId#Zl`hC-sjjC)^rWE5O@3gW>%qhJso z5z=Y`PF6C2ltPjf%m&!DhldAeO?}{%Oq8a&dVEr8Mn(o?m_dx&OpAZ^@T2Bnj=;MH zM!g}XRo$KhX_N@zf9ZKfFleb2dqt#2b+X8a9Fr;Vwvh%)Hn^@Oq@+Mx;u++lV0rt3 zft#F>;n6q90DWuXWK<$$B=Ih=XG_|67+9PF#X z8(bwCyuppNw46Z_JzpVbn&b;cC{W+^vJG54#5k!|JRlrv^r$|+VsNo3Dof*}9MLzh zBXTwRdpGE0$oCMCVoz7*R$g8kb_Y>rewb$hS^2qWin^-m*0*<)K>Ps&{!0a+R|!ah zK3b2U3GpiEz{nGdF3761)uWxW$B+Tb8f^KH3N~WnhoxVfSoHS8Xs{h_`g=f69=;;Q z?zVCwr2t^)vQAROb7JIPrZL4-{sSeFyu3X0ATw1K*j{hp4M043c;g@R_8=v2dK9sr zLPqmI;t$BCNd>S~uhrZSI|e6BpjIc9pPilMOL@D%GD>2TU+j9M0euTV2bCb?g?7-_ z+Zzl=LARu|VsTrq5E&U}=s-$Y@>)&krPfWZt&z^d9}?R;4s3_Q>- zRM1DuYB2Ci;p+wC`LJQmTeoWA&%>__JlbJw6(QL`97QOv?d)i*^8jT)DjA}O7&1)g zb$5a(SP+2M>C7L8vvUg8Ccc^tJ+}%IAz7-a*mbm!zIggrsvz?ywm1DT9eyYg?kg3* z>^n_)O_iggqi1Jlp(F;2I@nvS=LfNnzFt924o1UXmfmGjN*jAFDokqOX-; zawqk6NQv1LH%#qe3Ragf3K(A|pyyB4_gfbQL(#n-B&pQhcxhwWs}vQg%&Gaq5~{bH zVNP-JdeDAWE(`MUEl*;lRzl_(41*SpsQAmR^-P<8J(3k#%Ocz~M;8tYT ziQZ?4M$7aso%e02Dcp>O+r`dKUHKi1^Iy+}(|khgnHD*DQ^Uppv(HOtcHp1}4R&Kg zLkPJ+y-QJ6mOiXK7VKB&G)>3gN(*bdb9b(F4mw=2w@@H-8#gZe8>GbDLbd#T_{%^u zqZ?aW;O~~~*S8CeEgXx`tFbJ910|dkSS%dpVLx`t9U5c^{Cw<>IoD`{AkY5w zOu()PCQRz%nHspzr9;iE`#D3FY z_|AfA0BllAf7tQhck=o3XE);WE2t2Z({bmAnWX*AHv$6QWR6UEYx^dgs(du?<=zOS zWSjipepQb5wlnqdMw!IN3&Z)9nkF@sdNn@e6sj-_MH^iY?;MwJ6n7As?C~Pi3|Rdn z0cWt!eyknDZf*Cf9i)>%iGC`Tc0$T>I3_D)|1n!X9_wa zb;@*aK(`4Eit{%JfqGe7yt=rk@Cg$wDK#3QNsRP8{qd3T5-*@P>}k@R$IegMQShQC zsWH_diDd|=V%9L!R(Gz8BtMD-2#Ul|K_g= zvU|D|eqMobzgKE1u}{9@OiEsZzC(_mWCnP`;4`(mygYt$sZ2Ckh!kVf3hlfGt{#Fw zubrRb3e_N)f{~xbi|fHoTn5lpl#OFhi3C&76|$6F(w%#ka$#o9Y_G*N+3V_RoRXC> zo%H5twVq0a4BvT?%zch54ehH6<%6!JgVQs}SMT2Kfldd=t#w1@f?&?j^x2qhbASsB zs{X4RA(=f$*~;FYAaHAbUd|e;^(SO-%kl6An56)!Bp*>$QZgCzT%c(0iRYPkYeTuf zHF8Z=^2e@li=_Nuh;ir0P-8*z!IWwi!G`>1-o&d|C_BN~@9oqVFloGmy$RWhaw5u~ zWqO(Dm#+bei?3#i4MnrF+J9!Rkjwyx1X&g^<{;n7W5kIWPO{Q2!KctC0?zqt69jgPsi>syzTh( z-yOgUhNu!SK7)WmK=OFFx&Ll|PdTcYg~Wvc>Gz&1{b^vZQ?|fIz?YPA%n7#+hR^Zk z*)B2~0rv#@S@2K?b2oe{>X3J(r^)&+Rv)9%t&nfss{4@XiOLtN&RffC{ zPMDR4J%#HTE)|AF0Sll?!nHuDJB2}n5g8g9b43z-O!WJ5YZ)esoW2e$sflNXk8aF( z>dH&fnS_*B{E;g@fM15J~+>oy?bY8Zf^eU88c|gGBUWF>&GSoTb>5?5)THL zrYtv}B-EzwmZNH@>gQ;c>>Og0+?@8$4H&)}cqP~%LpXTS`$;HG4v0xWGN>G9{Zvz3 zJvKhhPA#GZCP(l*REd>gVd3n}#GBpAMIM#8H~L)1dhnq1KHvo=5D+|&PZDo~`KXij zv1?_qBv{iy(_@x~ZP!e9E5}S4=bZvPNEx|VZSUfEE|MrU&lyzF+usi%6~z!a@te1j!U+7Kc|$O`nwt@V4I zi@(PtrZ+W69tu4Hwt*h+;LD7v7PXkl4 z6qrPLtPit@q*E{=EF#+>;z#u5mymKe{;8+TpxVm)I!L!>rX-Mga5rc^nVSMhuTXg$ zbOvP$J^=yf2EIe_3xzB)mk{!d%CYT!eEbEarq#d;r0()-_Uva$Zm!X;>%E`x?w3?GRYPf33xu^*38goA`O`8oKqLn~YE@|X*^qp- z3Hv#$yzK?n;cWoFGp_<_f!IXK0J zm7iZ+u+R-<3P;Y%&0llk6Juq_7^Yl)2vZoq>nOW1&=7BDju_=!N|=2}v$Qz0?-&`Y zmR9osgU}fHS-bR6zl(`g0fJRuR|hs)rk`Ud0qc`J-&6cWH?&83^QEm=Xa4mr(r7hEtoAbVf^u9!-5;8Uuryj9~EhoKA_+FbGF+pAvo7+ z(;x4y6s|=+(i(Wx_s!bYwgsG%!FdtzTjUWr7tHw5239c`C>QVoAgLVL3+l5K zG0@Ec60GTIbFc&;c?Ut>K)x2Wd>9q(YjyHud=roAeBllU!U9XYYaUvAX2BfVwr$t` z8I2QQrxK{vgATLf0}+*HW!D+_b(+@qxId{RfbYDsgCsrjA)%G-^-*c3r|KAuwGS;U z7@tfM&K%k(^D}c6&6wEQe@(w==&iZ9%BEIURXw^iHtQefeo3_;x>LY-N4*QyTYbd> zm&&8d$b6=<4|X-@P;H+D!of%SEexEd%R#=?=HGjOU+t5VFZuJ^JmXa|H_1|Z#5v}Tp{4H*&$0eJ4VaGOEPDa) zl2js3*9tfwUy1RR*enORUZ4S^?9O zj()2x89B%~7uf;Z+=3rysMqhQ?tSCtDkCqbWbMmw!b{?-4(O!^5Lag5Q0Q;`Zlj<{YMqZKj{e zHdt@VVH8D;VsPIheZ$j?!#aw2JBi2gitXs^aE^H9dt)QIy)SfLBu}l@etNIv2)pk3 z6DyjLgEMYK2Peq*V4#$moZLS!U}>Af?kqBbu8VgC-XiSU#j|%*ifgWZl#@HMm=|?* ze0jm}nd9EkXte0w-&NHoD{wahUJVF9Bh(pC2zTDWT31(JePb@@G*W6VXte);;Oz0hCySi;!hOFQ^DFU-`kd5$U{Q3J=@}CvCix7CP zLs`qJ!36%SxrP*hepLocCA6gv~bLz{ELrwV|ERfmDj*WCDo1p=jO^f z1MLg=rh#xCj4IjgCUiZxmeaP9>PxZ|w`A0N^Ql+%@$E;KzjB-=?sSM77P?XAMy;Jk zTQozD+Fs+aR%M(R`gnTSRZy2G=|DvRDc0cO8>Ni>R3lHy8|0(9 z6pp*C3E?!Ji<#uvU%#~X5KGmzJRd?33!XjzHT5tM3-in}ahfl^miQ#p5UJuc8y1Kd$hrs+`UDem%vM2ea=+}zoy}okqFD|k< zogC@yEx2Ko(ZJ8wpTh7eFDeS(>HqvW9BwB*Nu3@S}g-_yXqfaKo;fR>P!P|(7| z2>{4G!)C>FMAYaWpVoBEoj3({HzaB#ekIuBgdLb+;d6~k9C7`sPMz~*SEIV zXaMHJ(|86m_j&s7v?6E6`pVMXOi`BEhbSX)ah7<*PSGJh@D_Ci5s0?Y(PXg+z& zJA@kCO=3lLjY6D*gZ`3s_ZzQ?>Z!Rf?}0Sd(&fxsA{C zQd=||rCu9?f+2Ce!6`8`6iIeyD0CurFk_sGeJ@8f5j?}NXpJLy9HTma2jR-eX8!@| zqMFJ+D4JOOeg5JV(R*KQ+Lf%NLG{s=JBQc}pt$jmJ)&>itcE5%(`mG9kq&^ZTkjS{emFDO7_N`aRr z*kS|dbVCecqyvRL>ekPmMRsvZNii7J^>lTGg?C8jf_QjuYG&?ne3Uh;cFxA#^@a`! z_*t$H|0L0ZqN3YIM!9j7DgaI+#gAfaZ{NIOVY7FEWGxzGgaxU#p{C|_D9d60Dg=BU z#)v4jKSuZ(SiK&LXi6+)T+GDqG+jDje@Lqb^7fi}Q~|BJemvz*kZ1UoJf77r@|2o#?;nl{+(IIYXGl#xR^hVNOvK?rUul%#L@|wrhib zbThz4!`DR4K-Vg&Q-8Wc;!ESFN}}pEA<}2h+ipGdG401tj)RwFCdf=1?c=xxi<)HP zqvK3}OC_!oZiJPsZHzVg&|Piq6n_@^sJkR-2$2=PYj&aLc`r(V5Ub0QL-ywlkkC)JJX9>1yxmADk{;hD{;MW55v{rdckJH$WU+zD=TQI z@>pe@F%zAHj0=`5(q!)9uSTQ}2xj_~8nc))q8iJ4Twv4o^EQ_w>hd!56oB+W><|>f z(?H5-^8RfN4are9452}3nwK{V_%N_?;0zrJr5o~F&+@^m#60_0sbON5rZSODZ8ZG& z*7Vy3i`=Gz2bt02rQyCo{19n?z&m>BE-Z#xR|old3kGxZP?pJYfq6SNl^WdZx*zRn zRm{kzTG?2Qo;)c6Ft7JH+wX$|5+nJzQ29l&sADSTtj(vsJG7fGGqoj`fBOb>mVQW1g?sPx^b`meITG$X?<0N# zz6Nc9YE<*o_wVl6Q}}@m#Z2uxQM@(b?j5t7)c$XecZAPjB_W3;6gwN67Z z)gbn3%b(iFB+wNO=CME6rhWSqa?E*OR6_WQk|9{oBXziN@)vCQY>aXggZC|S_STxu zFUG~n!*9OhXRD6i%}ACKKF=sgll^|<7x!fq*YO99O#1;p*tD0wB2Pr6K`R47Q7aVN z0Gfl1&dK{J{A1C#Q}}%3+vb9PTUiav+gqyhbSl(f);vBj5pcvrb^d{#NLp8cwbEc4d-EGJ#|c7Lgs zTE<%AlGfV+E&;Mt3kJ-&!c|rYm`OXekayxJuR=iswHXxXmyfqUPfgK$@!YSrBSQg; zGw^ct^gIGjNf`7HTm(TTi-iIl201xDVPpYwzYWx@kh}@po z*z|Q+w|*{3|BAo!E2F7OqKM4h@*Y;Dzn!&%t)m_Hnc?7~X{CT|`Aa0x(7EjBi(o6v zPG^UQhlhp)z~e+vkeH>U)A#uN#1-szSq?+W^=~uz?POuJ;ee$NV2@x&zd+PR3`2(} zt@njY$1si&mapL>0b}4_Oie=&g2i44_=jDJh9ny=$=CZKkf#=PjP+O@ZF?!1$7O08 z+dL<3>!X7iHHm}ivW|8$DZO-4Y!>f&YF5_f!h)HjqXxrPJfq~tv8vg{&#}Pl00bW# zNG;6G5!8!{sKyfrC-qdKPr|@7gW9NriI&wJ?8Jd9w@;X)xL+G%XW?L|7oj@w%EDRn zR;XIB_cj4voK?MIoMCcOQcxIG2-vi51CbntMEwC^Al`xte*=J8i);31!F^+6DprkMzSm$A4*NUZ>XGNcCQFj|-`qlL2W%>^9rpbLnNp@`p;gF5mcKX%2 zRE_uV-@}j&X1h?G0D|2Gpf=3oWFg&tvnmfF>>%v}4n|~~4GS4c{>uGMY|cFE6>=Oy z@*>4Y#yUDW^76rt39#4MflL4DmFq$WrmSo$ICPf_T>Yu9-GB0J%h&t<0JIB)(y~|O zv&5nkB=>#s40tY=KiuAEDKJib;=-}@`O9?|++$enfySbTfB#-=l;ltCrVv!V0P_bG zRaM1Leuvv&t3#`3mIA&%u)*e+U?*R<2gAh)?09raLT0#*jz?@PE_=w7gvZv(2{^R( zU@H}meL^Js1+!-_xIC3z9#d={Y}r7RM(QF{0{Pqn+2`UL%94ba&V?T%k}RYV8i?p3ZVH&zy&|d3Dec7 zf#eNU7QGY8GtByqvKI{4@c8XnAvRaVS5J?&S!B<-cMaYTlp9mDB9aL1%mYtWi#`gx zo!f)I+*m^^B87M7Kxp9O?VT$d$aLTV^L8M*XwPY&#W9GJm0beGGA=$Id?#KN7DB`M zg_v##ZL*7`Egvc`WMQ5T6)N!G*m!w=ym11=1?)1nE<2?6Za>L?XM?|`=JG=WE={;J z{TL>AwNS-a!9Q;)1%0!kpbcIX|D6 z*RDU68w}9?Ov^Q3<`I@B1N|h9|6XF>E{VP!&~JYNIES0t3fN9P0oNNAG` z<)#SbKRSVdmLo4=Un*|pvyd*V*vdX|6%M1p%?{G01E!o8Q*X^iZSZqw10Y-+|2@qe zvVaN;9|K-Rg>QFEq|?i>MviV4w%JMFYZ!o0InT?dEHh zl+s9@E=7DNT2Y5-H?Vhu6F(v{QnBOf0^QL0D%?B}D+6;sL&yjI6z+TTvCX!4g%Jur*X8D=jfn*xTVatU#Kd#QVqvif=RIX*>}IQ^p$< zBHV(4Tw&_QtpV}tcZL|c=c))Z-MMbr>wUvvI2|wU^yLm!s z`51`H@U&o4nE7 z>2u~5oWDDBp2x3mw3h1@`gyUB?@QOG<45Qv(BWqx(XfuTz1(wGG~Jd@tsg zmob<}#>SB6v|~YK(oXuC-IGyrF60{n0vs{eLjkkb==7(TshJ`aqTF$y1D|EVQpR7+zcs{EU))revJQ%l&T92 zrdsaqe}V6XUIkOrD`1W6v?9(ovV1TqM@hrog?RDDgoT;F)uY1cE7Wd2(`Z;w0`oIY zHT!-_OM7Hz$9ulLu_5kzXpi({x+U&&zX8jg z4$k>&IPc>Gd>VycUn{7n-OOqmi$Z2v$+rf(ut}qo28d0ke=lR2EL^=Kv5(SbY08ap z3&(+E9sxGUuky@M5xSb6jS)&7wNvxj%7!cl}VKoIrh*oRT6fB?Uu52uf-2 z3A0E0qU85!`nKqJJ)-RcE*MPgwF6dyIQMpTzQJOTCOd40&J3Yhr6g=-DKL3jyur0y zL+qQ)XZz|csS}?6r%4uR%?@L>Dl~WOVQyg>^PEd^UMh`paKjQPBEOGjtqDnB+uwy1 zw!uou3e3B5yNqD~pw4?35^yh5>iGLD+#oP{GVe>~z?(xamoIEjz)f0ew?by2h3S;w zWw)&+7}Fn%*%K7-g0nyi(Lug^C>FnAU1fQBLik7k^}=WF3;V~V(qgkeyAc%;@>G+L z3V(PlhgDN8T{}j$3>z>y>#CZC@V)Cp+-v9_cL0!iSe2okEu!_=*z}g!37(9nI+`g4 zu^L7;r(vT9p$z1ZI17Q<4_iUqXvDYb6G}y{?})8Lv;x&-a#7M z*(l;3wzfP}Chg6~Q-=APc}9I|(Q6uz6Y-B7VRj71{V}gRp^pA4i=k$sB7q@GPAP{U|rbQaAVuvaHha0 z$2GOJy;o@sAXOzUppnfGV-H4fj*bG6t;8L42&1p10ZT^`s|p`VH0r{ej^y8DZ$)8S zI6evg@RPqBeQ$n#_u45?@mXJEnSN88-#a{v$ai&d0oEW|xhYUgy2_y-T>Uf`0N7D* zYipqB3rO0*To3bNh-E?uN6s)15bqn!H$vM#s5!+hAfTb9MsRMXsEEE5+Ko~1Q#s1{ z2eXNSGPx#)_6NIlDw8&^X9;@9L=nt<%9Uz4mFam}; zVK4MjN(Bn75k&mrTbE>^~SzaEn+)E1Nf!^NUY>`#?ny=!7jh&{SHaXni&;kbe zK-)I|DH>!_Na+sWE{;Gjc~GGXx4OP@6$V8?^ASc50VZLc*sl2o(^hnZpm8o(m~gH5qAS!?^nr-s)o| zdY*1YG%CfW1g?C;(N6$gjnMd`X=0L}m#1MZ2DU>^z=+DdnFFLzvm9tO;qA<8wX6Ov ziKuS7Pjz`w2IKg--?w@y)hnonfdrh-MSh^+%D`lhFOrMwom^e>q~|pqr4z0KjT@$v#E!G*ta<%b#0hu!g>>BV~Ujh&s61YLw zvEV>x6>P_v2&ObYfBu9(Eof1JPhO(EzyxgylnBr=89{iezV6ztlBbdx&fe!SrAW1A2(*NKY zrj|#kc5n#^pS(xnUAeMs6$SWPa(Qv@pfZ>z5rCN5U@_X^u`%C2>!kKI)O@-q8E~_i z0h+w7(VLBu&3Vwl2|H2IEiFC0TXPD-51DX?V1dz;OyN5iwZmW)%2>X{W%)P*YRl;! zeU27}zf;b4&2GmMGB#>OHHATm6T+xs|BqnTQ$clFaF4SV;;{FD8*i9t0PQDQ=@szz9 z(bB~}SZ3mea?Qty6{fOap~u6;Wo2iFZ|4E0UMv-VVLEY8EpGG{Oq>AB8o9Xs1PyvN z5ClOh?1XX}CWSphNejb4H#j{bQ_rAKj4Fqd9F#BGnr&V^VOZ=s^dVp6&V6)`3I4h> zCUd_-k0Y%}nXJ7j$JwnZ6i$V#Mc3*%4^U=1R$0ZxY0rPd{NoZr4U{KYDqekaf*wfB zYXj+dBmgpJ8d6eJZf1pq7VLI(w?#YwsIE}45g~z}obJm;2P<-_5*y#Fs_14fU>pmJ z3!rRivTxdYT+74{PgrNpIt+yyxLGWU+yrnw*>auAD`6dPR0_sjHI*t*MzAZ-+YT=d zY%}XC#Qiw$WjNq%oxBnC6wJdn4d_;yiG@dv;>()#BbO&B=XkRAp>;mBAV z@cH~UJ`P#oXyZi@t)SQ#$QDiHIV7W^nxOn2{OQ>i56nR-E4?Er3hU}<78JxWphSWe zMUG{PIXX60aXY9+6K=zwGRT7I11_*#FZX+xxzw{Bf+(}=VF?{718|UPJo9x+B^~!eg6(>1!$ha&a-bsiBBs)KIi#8Al#Rw3}Uu?c8Uc0!P~+gV{bz5!2z)zWwN^oiC) zes!x~)d2It9Ftf+s#z-F9O9yaNqn6>Ji14P^BXx)uS2y8>YMhb*dDwvlUJf8I$i@D zY>(hM>C{aYF0P)oHk6Ajex-)i%Ts*)!p4|{gcgV`Zcv8{9%vFgytEJT5X8IZB?5Xz z?q3qQkm;nt&jRK}Q zQAmB$yLZf6Z6Fl@tXtuHAmFR@9(@Ly#PYR~VJl|Ny1mKFOE^Jf?^0QC%DW*FV?}hKGP_AJ>~*j^Y7QCYF!lP55}Ys-*J5$10@A^st-!3xR4<=Ed z@#*XDFMjauRhH`QvN<`{UPmd1HZ~6%_@V0;uldK;Vu1 zaoPIRoE&hl^@ENhkE!WuFIh)szu+{jiwP;Tr*(%XHMl88lm8W|05xh=w2g@r_;Ez0jKZ5_ie+0?r`zl0=hYsQq;xn-MmgH>m zF}dDl_RFxMnK^sovC_rp?8h6+yR#GhmzdYMr50_j9-)?y+*g>Wy~+1S=6Y|yBlFuL z%_3^3NIM+V7tb6V)IUf!0DoK_SpRmR?TyGo%P)#p-JLtlTju1xV3F6a_|v=Ju3k7y zqw`7st_NtEZBHW%NSh#p2>=UzH_``9rr*QYA zAn!(LoHP3MY4~X;c@!6m>?aX{195i8lKm#B*zX0bD9^W`FmS?zn2-_vSFc_HTNYeh zNgQB00LReW-9+6t+oAqiuGG0+^y()1AM9qaT2(3oRswH!8w>YhP!X})E2MvkzUtb3 ztZ_7jX~Wcc(8AvS6&7m~9QBo$UZ;P!wZuR;`~#RoGsUPHX5YffvI=2bGq^WpG^49p zLD*IIzF>`|d*+L8$T|;jA}&ALCiR5yzML#Bs8wk{!l?_X`#^vkH;`-q7h-|+CB7)2 zgZGq`L74e%a%`pnZdRDFz}O$WB5z0%fVDOKMwc+kQ0x15UJnjN#Ub-_b|rE6x63T{ z9SMh*5!LW|mW)2}%!rQ1*ZIh7B(-CJ#vBNFW~6<@23519B>jX;HM9Ns zXwG5F{9wsJISu6G|21xLuW^$S#Yw1=l$?sn z!`WH>W^7_2WNw=@dEKbmYmHr9eG24#pdH^uPusGrpE-{qse}S#IA%p~?0{0WOuyC^VzOX7Pr51hY($34A~N<$;<3_- zRe`K!IMLPU(WuZA0>uF4SZ;kj6guhq1-S0aVdUUtjzvH-zqWR?I!M1)1zz716M)te zF^G{Mz1jQ2CTojiB^+|LTV90(#Dyw(0vr(KT&`Cdc_T0FNl1n6qA$%pmb|M$?Axm< zSmMujVNicC6v*T5%GhN>M)eZqxg*T&Ds$c_)Xv>@bGxaQ39*9~Y%|Y0^FcUdN>*^A z?^zENHIP3OF8IqKy!Hv4l%m2R8d9#)=e>THM$NFPAPS>2eiMd;a=p!3SGuo%{_P9Q zdXU*uB@;7esC^%m%>HGtmE&u@knG3n_1?y3Ewe7oA0PG}ixx;%V?E20)OY{*Cnq|& zGZN>58lt66fUO2g(qkczfBqFlh4$UKI@Z@`$6r_=ojMp6sPcA z*nvH+Z~SeU$V~&y8yXi2(E%d7Os(p7+*Xbxia*sJq~EowV!)Pv9g2_toXDS;6-4$T zc7XU-I=`C4`sn*5oj>$FbKehIsABNlIzDs6J$7RZDolDUpS}X5kUTG6tK*fTfV;s{ z&1I=kPFS?l)@$mq{zP_>Mi_G|>5Np`S&tuFwOq^{<}pc<&+|@>OItgfCI2p$h<1M&_vW9K zgJc{(C-0s(TJ+3nm@I?IHb5Rw)=f`~6+NIykH#S&*#7ZJaKA}lSp&G?V3cTBV@=vg z46<;|dJwe9Ur4hX$+6eno3{b*{o)sPc6PSx1vPLo*q))OF)z-DIw7$0^gH7uUt_aM zsJ!hB4znsaLhVwqP&L?k8>H?3{t+`J2Wx|mCJFXGcscI#(tN{Ao%7NcYOFLLsJwwv z^9kzIx)*<@QL4<`V`F1`dU}M&j2C z1HU2Yy~MX$gwDEWv6HhfN4Zl)bhv?9@GRv(re(=;LHl4{bTW*3zJK+WEigtt@)yy7vWkz9+KSSUG54{UzExJA!xsr!>$B6u)^R?l655 znqbz$_OyJVC;i~c4h%F#Mn<@#czJj#W#1}c)fsAox)IvHl$2IS18@?98D(cDt*nmP zN;1@KfaLAn)zP8$Lc7=oNwjq}sH!$Mhq48aUk+b|_><3{qB94f%Mm~}>b&Z#uJlzo zQys#`0b!?4Z#@bP_xhl#l1?xq=7UMh%)^KKb?qSH|DNthY;TVuKA;Ppe^#HKMapGmp3j;0=G1}a_BTO8u z0~Cz%XeorUwBXkehqE_cR=t^4Gb2T-3MA8rHdNOH$Yg6|oofz~YE`vR2htsU832&O z1q}+xn3#TejfgQsIZ{7oYVaN+vEZxN*vL#y4hi^-i|^xMpuR$eT6auxWX6^J4?uwy zK!dp}PpDnziqaQd*X-|sOvB1&%L+IKY@b)8Trt^W?QfYa2rJ35+&1CRp1{c4`HuDm zoCcEIAT6(txn zC_9R40;lBWVfuuSY-sL61`tYom&1B8tRCO=TYt__XVqQ=D@)*(zl}-*t1)NU6)MQE%qT1zQm?g9Z>L@MY zU{J5{T)QU6>U?sr0)rng0IA&bKu2Tf;IMsw!NN3J0#a82-Aa@XA)u{=*;1jT*&N3F z-oDsfM4zmnAL(F_Q5QXH)X@^6+X3&!uETqR6A^%5KJ^ufyrom_OLbJ#)YJq;4}jT` zQEpD&u-C6TiDkh_f_2(SGUM8D*~|V=IBGeTZY`@9B=D=W@ zEk+w~t?PM4u9CoM0}PsW=vAQ-0A6SLx;$s}3V2F`QQ$F;n47?p#y&}gdc^h-)1o-) zP!~K985!BnnVI6nemh;o6%;qOA zb*TQE4OB7%;tJK0Q27rW?@P));)bR@7%uciI4i`)o%!LFqR@(lz&&yH26l&__YI2|t=C(8 zRqsrNp&b7J0i+wfav^}L-P0*;NH8nqvDcccd!Ntv-~l2hC~@%bn@3mT$urdM{+~ma z#@Y0yM$J#~m|l_-zi6YM4~{ndj-gy{_Nc>=XoPcjsZr}ShxX5EX7EdA%j}>7>Kis> zw1dkhFnwU&SnqpO4sxxJA4MK5M@j*=kHWYLjXJBPMlS0vLmyq&R0Y1!FE@7lR8ER) zVwu{Lwea205HUl%NU7_jQl@Cng0MM5ZjmGK!*AO%?1oq?Di_OT#fU`)^N;2OH1`RN z==N7K7d_~#`zSo*9!y!nb|VN^2bXyCf0YZEN&bg^nNn3!@|V90bKthuxZg83JzX#* zwHZj69oX_W622afyN2Q!%L{4rnJ|0#c3icUVCfBE>z^@z_5jj3kQns_gEE|Eh@1Ve zEq!m1LOB4@EHCTWgFBxdEf<7IUf#SHoaPIhU2_H4RCgqwWzBzlPjMnqZ=A~Ew(^2B z?pOk{w`R{wGi2~?<7AhXmclHbf`WnuFT1X>5%@mP;0RG}{PzWgFa=e8V~iODI0^3a zo#>`ww>iyjcY3g5#p(Hr-%M;Bo+nAUyPZnQ@OS6-NJ-32AVw;DQ3h%-7y1LB7E3)x zo(lyvbv~#l;3`sAQv-Q(hsCRo-(-k*?8p<}%I^e;GZzluN{?P~HSPwPPL5GsNBN}I zbyelbWU9rk&Mf-VyYR)0(EV{?L{uqFTh8^DR$VALrfpGqh30KxxYyBmUcSVMc^QHi zVu~nLzK%e|L`B5}v)?0gYL!@P^ghDr>=@b^5c%u7Q*UMHW7Y8S@`urkr1odIC+cFS z$3kMgH^=kL5<+*~vDfNz?|-$@$V7Wikh9q}_KqMU_WZbMWWNtxu*_6B^PstO_}7X0 zKKfEyTN^NhArD2EO9XU2Z?9KHS62%UxPi8}@F)g_A6L0bVXyj7TTOrTo6b?9R7;J- zl*Za770pXJhHRz#RwO=e8u+t(2L06cZX7>2W)J^d)VeBVAxwyxki<_8B@l=_U8YvL z&i{6AN_6V;c%IifH(yDw%vKIPGs^x%TU1u%br6DH+=Djs*-tq&ZOz2;3+a;w5NKZ5 zyns(MXBRO-Ke5D^yVeotq-jj=C7FYv@!62~WTAvwT|@27^(Xx|^BqX2KU+1Ce#UrO z+Qpf|scx=eF9gCDJaS~tNC$Q!)IFqD5@YPoE*dOxj#iH z`HU>Ah1p3ux+rr;Y4RcjCQiC7?X2a^V+^{(X_Kq@sW=6P2&||ST7jeEu`k7k+T&m8 zt%Lx?WKrl(z2^+}FL=~;tehg?R)pdA5xVR`f@lnDl{O3HQypnrm@Pab-La*yzO^_y zop3ccRpw>;vA6^`PGi+mQ>SrH%QIXajb}miCsO} zg~b@7)Hg8j5X=QgL-uI07(p5-vjEM&($d2A1I<6uEWi2VM7RQ;+UIRFzM4BJ!aEEc z9n&-Vu}|V<=ljgi<1Fg0Vxp>`B9s3;)83MMWEj0Fqntqj@J#s6^og768r;Rh-4RIw zM!I)Laj18In*oaqT{GM_UBJi(^lczxiUtZ1zn0%X-@3Dhe0GX3Z<)jC@tErBc*fV! z6axtj9gkb5h5`K)Q7`{I+kOY1m;xrR>tiK}kctDZmo|%0%n3zi8DNAP18%Um5VX9d ze-H(`c@0wdQN+5VB_3yzA$ML}Obi1P zv+7YDb}fcq|KRqD?Nq-r)^Bx0Bb#C7v$q&|RCUuoVlDO0p&u>0l z$b(oGaEMA3_zBjbia=HYE_&Ot?uzQm^ih{;pGJW&%mBkvyNIV}tY39r2ywvsF2inY zJpUVf<4J#EHa3r1CMzopMPNx-Y1p&kk(h%Xik^;6NK_K=&Kkbm3i!q~Rd_?@?u>27 zj}P&4KWD-)FHuq0kS~Nh0vCSsA(Sj?VhSd%>;os;4#W3vJ0EyO03z|W=Kuc?8d znYQIcf0_i}I>u6LU&X1YnpT=3 znwm)|DZfGR`8GEgPJAAl&oqMC@U``-+P(RMSj(HkQ;kot_91CuY2s*H3dVQkHXDJ@ zf$>5P3q!21pVJb|`Y~n)0(io{6AU6KBE=n-jP6DR^KPixN^;HMX&0B(xz==~a;JrB z*t_f+VbYK!f(R2tH+oN?{*!mAIyN&d`71Al!`SsqF;{Qpvhwbr) zj*9ips?vI)Vo(mw^ENbIqmB5rvtw*#4n&)AP~pRbnuAkF{o;v2P&&qIT^7!V z-K$#_%u~00LyzC^f9K>XPMu9QqP1iz;MYTAVBzIlUR%@g{*5}@8#z5Z&>O=>QyPM6 zg4beWs5^TcrZL__O)!{n+k}+k37(o0+*_Ph!qOToBFkbq_gwnsl+^4O=jS!yufuWb z4!Ajpwqfp=wuuhUo@5>#rAeC|IxFLx@LQDljLYITN#Y}i`$L7Q5628X+zEdCC%w$g z)`x5!NAE^^TiZ*O6i#$3f)}!ZmoQ&sVM0=BGQ*oqm*b8}cIlXLY)Ah7z-&)EuZni! zyO7br({si^PrJp^h_FiP>A%y%71~OZCEj)1HrWI(r!{{97D|6aBN1h>8Y#0xl?qG} z6|2F|Rp-SNI}kIGLqSLm)6k%5Mu%l87emSMQb;6pUa+aITJYd}(Ez*0E7L>#)!mW~ z`1iz3mF5Zgm}ylfkNaP}7JS3t*!S;aF91Fa0-A_EM|c@f9{hRb@P4}HG1HWe#dImE zD(BJ7F`K71%DdYaYV>QQbOf7h9B5aDq>>I$|9yI)1v`0xu6Klq_d{N$7JPp*wM8-v zSMm$-C-)|WPw$mH~0Qwo+ezP<^&&X8x=<2b3 zBQV-{-iGQOG#=g|jLwSNixMMXFpU%v)$K}t&uYhP-DD#=^#0udL*bp!XRL3|zI;_e zomLW(w!RG*l-tfjF%DKmzKoQkaC?AKA8tMGZ|y_oGKX#$psjNmIJczZ>YE~Ow{YMM zTOzG+A?WR2FB^v>;a%LL*BKX(09bz}!rLA|T(dL3WX)D4c)Ye$L;r=^G9c`ybZ966 zLg(H+ya%ozb_LX1X#7BV={8FPT5y0*1D{_f!XFT&BU=F(YAtp}5wx=!=|SlQ6Cs6+ zs+~~2FBBL?{NEnHFEdP5aA%BS6TNGUWVWMEIT<1VGb5vdnVAeX_tQFxlu>Jd^6;GD zHv`^Bz4tF1e&dJ{W?5DAb3?RzjK+w9jx7J6ZiY#+WNh3E^`5<;3JL{Qj&Qg8(O)+KiTm9n!$@I1RsH7z5v}Y!g!vDa zg!L|wV$n{)ey1332so|;Sr3?pE$5rUfuru`_<1C3a-}!sXUs#3Q9}xA!PPZeO`|_n z7u}0H4X!9NUAsjS?8lzx?8ql9 zAkYuSq59qhxQCt~)0fl=9wgVJeI043rKW>5h^%f_u^buTMmbbGVb|e$IVooZDUx9I zt~E_(k#=5@a58sBYPj zsV)|y_NU$_-zhi6ztuNYHJ?@zQ)FBmiC9(X4MxObw+ylV{kRF(x)#(1Rf43$FP5s* zKD;CNNd4N$71*qg!on1}CUs_ob;)G8weI;*?8bh7E*3=o%_DQhPTy^b81qU{;hnL7 z3(~fDI@m^U?S4-OmwqbBcglbK6$0Jt$Vec$b?J2#ARP{^k@*!(nTctg03MdIZf5t#EbAc zP|99@^)BvJG2|35tohjVL(z4|;ye9&P47}j^pd0OEXGURSmoN)boGyxrLT10-+El6 zkptKl@a=7^lPiAQhRdqV2n|g*g$9A{zSFBeMJKC{#{!gbsGDqR;YVA5$=+R~+iGgm zCrLo&1QtHn$+cAgG6Yn~Aq_BaG^lm7xA*bYOAreWDOYe%oK{Q`05+GyaM_FcdOz^C zh>>})f|X3-i=-`QDw93c6oF+F+H&CL|mg8p;Bx z24Gjt=0MLZ>Kue8Lm!)~rGt1FJbu4xZ*T9#E-&1;;ZIdfWAf;N^o6yUTR<#6UC5!o zIXKuYX?vY2PL+*yH$h8`-+uL56{WL!Pgyp+D15yS%fM^%oXsZZHu`iDcfz?`Q3}xs zsG8~N*Y`D`kH*Pa3b`yK6x!u5Jpjgr$}lr};~e+ae4{5d$(2CVdqZ!`~&`p5BuU zIQo7uQ9#!R@t0mvO?Q%W#z+-CnZ&RYrQ9eP&dE)7^{VSw_9{oTCrB9zi{CLh`oD8I;uk)FIf&=EhJa8ZRnsn&IQpU@ z4Wbj&CrPl}5NIr&LV)X{Tt9C(4g!D(f7cbmik!hhze5MesayzlB^;@T@MIlv)T$wj zn(C6FmsF7E=oHAAXn@uEpdYx1+e!_lh7UYF5h}?(U9wBEVmpYnbNm;npl8ks_f8ig zoHg1sG9@QB(!cMJFAxPOx%nM4D2>TBj~*^t!# zKS+$v$^dmw2Xtzal5{jRfn};FFCVtD^=8Km8d< zLFbhQC`jr$_#@ScRMY`dh0_Guk{A0UW&q?F%$2hhIbwBT6XgKKzb^#NWK>iXBq+5m zQ_x%EKAC74*-kCJ(AL#uI=hS12L9d5{^+PdXs0{}7gu;K=(gr2SQGFsBEpJIJ{~}8 zhu5r$ij-9M1od+hGaI$mC2ny(}$llpuf z8L5S=PAy#st)j)4t+7Q8PIA`(CKk3l`$dp=Wg-|-0!vs!&NxXZBOKv7oA27;G{F?XOG(j);M#IY7 z9Qws1G&5j;c5S4*ytMT6c+bwwGeg*UMgzv0TL-9=P>Rb*%<86v2?Vs#PA)D4S6~o) zkOD4eFW~ATnWzArPj`GGpQzOSEUCF!hD!Db{}CA#s3pM)Q2du(L2qqu8x7q zfss003UDH^F6NY-Ra;wNqvrvLvcBrTv%oqN2(Lx74G7E;z4rk^fsvU2OrV=~Uy9c)Y#1zql8EeL`*8YvuJ&GomMy|M z8j|Bc`K`TX7ZrW(J_qpR)YKHT7D0t+_hkRR!TI6Jk9ior2L9cva8F3xVdUJ-&Q24C zy`KX8NM}x^ULXR-hHODN>k7O9;xkfh7+j~br39iS)fG%k?WbBdE2Bl-PY!-%goVk2 zOPQ?JHY<#a!+gSQ(#y;eN;cMTGIVtCL!p_39|)HxOxkG@?4N*?wXAvbp99Jj1Ui$? z=kf9D>F{fUK4UB~ak~-par-ctgkahVmOf3?*MUA%6DY{AA!P0kbM@uT6eU0d5O_%l z)@hJgoVpgl6bq8$K07UsuE1;vsv80m+A!OpHMhW4w1(M*J^NTdguDAyJNHK$c~{)N z8SalmIIvI*sORHgW?HkeTtoXNz|PGSaH+~D)Lq^HUfO>X!2Y*Bv2_(}WZ(ssI|R`<$j^ z<~kEMw=+<4;lK>^^#LWH<<{#VHC6Pti;5+wz^M=hXMvX9_^aP$-wVPOm0S;Ee`yFXo z_Vx2KGBD8m0#v<8?@0YL)OGkcHWoKUAWmUpV*^_DzC+AETk|qSuM*aZhY}S7-Uc~+ zJ64VO{ac@kGdl-}eUhRE?!n1=eRpQQpv2rQ0>-|xvq7ViD7e1ePBfXMc5e2Ox@n^y z+#zh1;YQ9iOf#XGSOePk?orhUZo@jkin&ct7@2^W$}942$jkQj_U2y+!W96sQv8B~ zFNF(^`c(#0XuVyp4BwGTm7xH#t#{5IdCc)lVsJ7W}3iv%eE zY(VW=97RRN1g62YLT0oPcLoF1|6P#!rZ1HB&~(>{SKUT5dBwQ?nH8K2(jPir3s|}I z(alBOE0tuKRvL_ocMFuCUf&LWE3qqcG|UW*j$XG@L%rqFy}ZjCt#x8UhU%HYM;omQ z9{>7R-Q_KoW%1dDfBY*0gCzwnO?q1`&(0f}Oga+xlAgQE3Re_WNW~uf>yw(;_|hsm z|4NrY-NU>GX4smxBOz$75TE6&$DPUauWEqN^u4g0TVB>m>XRH%9#c3EWtA7(X}JSu z$MeqW#9eFJ?{&0JPBvh+s>-TsUse5_n`uq@A_?y7+HFnMZ#G=v3M^Quj@u5ay3Tp6 ztXP?^cv2tBiG8IK;9wmjCY*hfs>o}#qeOmSUV1se8_{)ZX{L%%F_|Ps?X~{)7!gBi z#Y`3U5KguP`XZC@_9Rh$XUsB-E-J%7V@W`xh@)%6L8tpf^fyctc8R1j1WzTw0t4-z z$%nOUBxC2huuK2?CbT%ZPY40JHX>^Sxl0JqzrN}3Y$Rb8OQ4MI(q9>iwQL9@3I^(Q zLr~}=Fa7JA{{AFu(J(wttdM`FFn^^v0#tI5WQc!#)BpR&qxWC|GJ*f!vRR&9UX1{H zJ36jGkNLwUDQ;&bV^{cp`KT9y7I1yR1p|pfE~g@J*1WcU2DXM{LoQtV{(Y0@YCoVU zHk}MOFFX`q{FjxW|HuMYG_=J)2NsyuZZH@EeT+4zwPEysWwjh)5)9U4fsRUX@oI(D zm%nujp24`2*OzW(Z)$qH|6?KOzQJR{jMX)Aa)>atpoCy;Ed=CiAY8zi%1A$T94;${ zTole)QBf_FrjVdRKW;BinCHL0IK@2RL$I^2!i5Awhj3ZI%t>5iq%s`y)dDjZ`i0tE z+j;5N9e(Kd^c!%t_jh)Hi{1VGyGCE;=$)?}PsIF7vvNT6!>5Fk ze$&1O`zDUdshzYL2J@XakIILC{Mh?*813Pn-Hno>QwRpBfe*Xm+mqpdSjL%;Ag)E2 z^E{$>IzIeJjKEg>`PND(?vmVdd<*sP7Y|^J7#I{$b;P!J1O#40qaeX#_IEgjEqwloP@@SvGD|q(zK2af$7EU2NS@4V1hl8 zmxLq<9>c88otcGY3Gg=JUu5NU08zylqRr|6@2k1_0(78JqONcNU`l$h%rL2mQs2ZR z5Z(iPz0AYSdoX9^?JfL~8ksdm`~ z+HT2j6ewrrQ*An;XM6Y5k@djOZgt%3cuLt?4j0tO5%1h{J`e{4ZisH%l&gjGvaOf2 z)HI!Wa7ynd5AGQBS{B(47>^Q*ii+T5R8;Ih!VMi><)}?rs3T-w!@Luu$}ltnCY`YK zP*5zqswIv9-2c^mB~w$Hc6sm@8;6n-928NhU4yTOkr)6$G$3yc7BOXIWps{@(Y_z-#{|i!kOm`=VNIdh=8KmfL*=M1g=0qZ+RyH0{4&{|U?;bM=8E1}U~{H2{0BKCpM85Ga)96PF8{?J zUB7v)oxu&FwBhUjMh^|-mnK>*d4iP}CW-Jq-BWfRM0qC{7pIIoAtT{2Yr>oFfcVue z7V`vse0YCCZv+H#1`QtcwApdTMB}}Xg>3>S2&M$-^^OiwO5~|M2EE3|!l=hf~^3Rbd!Ceco zJI`RG6kx#^!>xcM8Y(}}2dlr@Uhi9*ctI zlP6}<^*hdBo)_`}k3|CVeZP}O#Yh%Kjwr!9&-k=jJq%;w22V$&QuM~Z(GYduAKbAR2_*-pScIcsO!*t^KGd^(VJPfSFW zMHnB2PrtK4Kk5dZ7W52|Jk}RyArbv@+(uO^vYUBSG?$VZzV#!*#{twc0YQP=tM}_y z3!kgycg%_UgafcZ@x#N*YpUjcT3+5PfaVfV_-3}De43;g0t5fRg(t@ZbSrcVnY$B5 z0Un4J_s6|HRoolDsbp~X_d1}zkUwmHBmRkFu+x(4{XZ4Pc-2qs;-3qsE2-507p>Yz zRub+R!=`|`#zv_Ta@{9)M8hb^)!4YW5EPF@Ksu#ehVr5=Yj@>tMg;_@vT*B${={vm zdP-rZ51y;URNy<2a;{%_DpBr)W^o2%(_mH&5)6PGK%xP&JAL^|xZ(*HK+1)?^)|DA zp1Y_j>lki%H4~kYy7VoJ@6?6(I|em$eZ;L(&@i^_PwZ-#Psd3Od-Mp1lTb%O9oP@r zW{{ezHut7PxVo31Z=_#Z*80nJTuL-Kg=9C?ZuKkdkSFyGoBl)+wQD=KRa*~Nz&2_J zF4Ap=GEKU3t{;h0AHBcMKp;b|n$)?Yg!+JYATBPxU4T?S=zp!n2t^;qSS)bX;^PTF zel_(eU`G4lPAu5|Sxt5IQ$03FeSym64Mp~E62t=EizdGTj_;RglJMdp<&DR#rs|+n zJ$Pf}ekma1)K9i&^q3X7+xOKJ<^Ky%G{=}K5T%2U7q9x=ysM>Uv1lYYn0RTCscKtC zJw=|F1&Z|H(z~FF>YylX3A_rvPAeOLxo??i8%lBqSL@Evm;m1FnD({5e-|obJlBPI zYym{p#v&ji-)PHMw!4VfSPi2tsod;Dz#tFQonf!Wq_~$!FW9oZ0_~vlXi@xNBm0Gn z5yyjg9>grXa^T3IbT&MAh|ZsDexFSNz^sx0xGtI4z_kw->~4Rfm|wN}^XqjLWxvON zj$|D)RrMh2qh(s0vB5w!oKndI8s`yOwWM$5Lo%7{(4WXa8dVB6bBwN4Wvosn?Y9S~ zFMCwy7G4*W?0_hpc+?(DV8VW0-`u33p~+5VX>__7@CX&JZ9YS#0}qY4t_FJ=UK&_c z>{huEnizaJU#)3Yh|{RAubZHF1g$<*i$X?Ob>bV4$!QuJe}pXzx+E#>#|2-%ix37^ zP*klfp<&}^!)JmPGs;8`tl{ruiRB#6A{QR(v3&x4Mh&wGhoQ26DdsXaN-J^@Rmp z)7fko(|VqsUTN?_<-@x;Eg({m|Ezxh#Qjjcq5HN)Ru{aDSLX823xNC(DrxkZ?rxZ80~kp* z(lFy3;!y}FikTTC>DqW)k!`h^bI1AYZkn4#)}ZTZ z1O98Kg=lN#(Z45zEg+Nb6w^TpwzE zPA9J#$sBYCKnM6}?*ae&A$C%hmYhn}Da{HRl&!nv8BxvI}qJ6$^iM zJMdnY5qeSL=|pzU4msm#^tYREV@PZre6O(lMAO5=Jsyz@r{C(Lu{<7 zz5NO#O0ODXTIPhJ+%1<*)~`SdI*qOD_ z=h>}H!Pu}nPgE0UAcq1B2cGb6MTL)w1j~@*q2^N|*qDJj4D>ajKLDN^&tJS?&pUs& zh?T4p(O%H}(`}@My+`ABt2nft;NCGZHhy&NRo^ZCXOlw9^Yc0;9MO~Yj>{JB@}V>g{bb_NFzL|@Xzl{l50 zPSzn}0T++=`}l7zadh=cuxW-{%|IWf*LOS$Af<$i`F^}*mfq+L-nio5%JN?@kIg&gBa5vryF?aNQBGiyICN4Cw>SI`S^_t zvH<{L?bIE=VAHw_h>P~>dEZFfooe6`w?sw4ivsX$f*EMii_CmYb`sFM82~Xs;t4;% ztRehVRaVyf?94%@#(;3`2oG{NX!!z^Iu5BPe4ViC&wDjG6{iKM*kUP#xw)65zd?zp zmxEOz1VjLh0aK-5b<$-f;w(lw?|c`>{6QNiR40G_-%-c*4V$;}EZ5orEhVI9C?_Uf zB3~kOGaK%DpzQv>dVR5CB^nY?(61JflA>;nfyDaetR3lNj_0z@>%RW}wBnx8MQkv#k+O)B+Elr18y_P&1H< z1&hi2qM|G5SFE7b4DAU<4-K2!Sl-Qp0vtuK$z=Qw@EBf0fL5p`3%ueFyu-iUnz2wc{`53scJ+JyM**|5!Df*WL4%12FW?$ZAdxsef7*ItuEZWcl$|-)r zFAcjY&otDbXMDWgv+DXqMBB74Na|pq@9pm|mRu$U)+t@aWkRVBYnNh;!lGX*7@~gD z591V_-@ixLPk#AQ)IinaR+gU+^FYaf5h-LqwFj;CF8ehxE%Ol~>+aQ@Zx>tB({&zS ze*O%CSOXVZZk)-~0#KtuB?Ai+?=^ruvTFvX6nla+}wuXE%A}SglC|FyVJB`~| z|AhX=dD#*|CfGMe$bq5brlqV zD+9iga~I&QusF7)Hie8zVHI`NP?QouUU0hJ&%e3oO-Qnh>M~ewu zB~vQ`p)z}=x8*%Z%8~-#J@sm*8pQaPYDM835;2E&_09fyWRcnUc#(7*+fDX2^Dokq zD@i<+fSX(i3hls}U73W&m=8GJ!6vhl*({XDg2I6B48gd#yzFr?U>4j(UZJhEE#C5d z@v<|$Cf3R;rSU3bufehv*159iycy*0Pz5$viA^XA?1Q{ zyBQ3T7u&qZ%u4ccr$q}ea-Oc3TV>4FOG9<|_B|AfevX;vku*{(Wu zxqKF{u^lAZ05Wko*cgLOQp*HCz!xjZC7?&Swq|)FGbu4KH>zWu@`9ipr)9KQYSeYI zZCb^ATca&FQ$o%y!k}C&$$zzH&C6mIE5l22+MuX3fddiH{Ykek2AY$ykkf*PlYB>! z<`>mIo$I1*GjNpa-nZu#=DB2N^sr6f_XW=NuvUi3(t}YS2z1+cCH`}kgyWZ261YEkP!ep+H+JGC~y15=)PMf&!q$pB2NGqAoGT;gi?5OzAxG) zWAWMagJIhXHu`H4UeW%x26n>34zBYw*Vy;Amn5)n$qxoA0GDELkhDHx`vk5Jrn*>1 z)36}M$#~G`9HV`8H7K^%`o-bLtJcAqtn`Y}2M6!z)>y_AZty;!J$m1&xd}=UsEn?K z8gB;z-pKn=T>Ul*&I5U6e7pQ`^Nm6Job^({?!EfMsvtlRe^X&KGq=*;ir%V^eTARR zw!-Yh7`a7W&VsXUO}p~QM8l1Bi?JSuUS9T#|LNghpCret6%H8@=%kd5#=d>~77*!& zv5lYl;uGQ-Z)0_+<8k$eC#0m9>`F!dA;rV-gH+W4^6&+2exEbx$Z1_u*1>S?-X1N@ z=(qg^NPu%X;%N!rf?M^nYx5tNnfnd(B;?z-W8^uI4lrtHZOx>=p#;GOM#mC38E2Tu zIkceDBCjbJzP9GLG8so9$imATKg!7(+y(RPTEC^gPM{94Vl)(q04>)9%7FaHc}x=L z!$_V8BninQjdgtRm8BiQyT&IA&2jMh0|G8IZh=;_;cz2UEb2Ayji(zRfYaX(xfe)( z{sj6OmtqUY&H|d$>&cS2BHB6bBpT`fTbJFgq@>bk$&3K4{)Xj#zXj4EtujQXhJ$xJ z%3M|PAUkGdXTLQgIDs)#AT#EiJ)5s*s8NS=P4SQL;gp|%!ExU~kEPk~iOIVclVcKa+(2^3ZQK4 zjEn=7S1_iQfN%-6yQh$`Amu@`*OfM!`Eg}^S7(@Rl!%-i!)Do`Nce$TpFFm4GOPLf zd*9^#wBu5$zt#BZ>M>@6!wxMzJHkK%QpiDa1EWu?YfICZG*}I%A}xqhn#g z3@m{mJb?amg6CS35j6^r>Ui<=&dFqQt_<(9soXL!woi zI(#)m@^5iK(&KR(*DH5C>75)u+1`VYxXpSSZ^CB}F!Xz9-!_CdB{-=tq= zfM#+C9W(`dB$m)aT^F!$e$DH3xH0I=Tk*k$bp<2vXq#SCbQG>h(8C7BHk6)Z)3tC0 z%ENbK23OAUDviW?)$Arf)b=Hn$$-LKtYFF7tzz0%25yyr!&R0<8jpOR^vj)L^4BqqE1R}$s&8-<zXF}NtMe~C3N~So;+yun*XkcJQ$je^NlP4^VvWh z@pPD{39!nV&Tnq6Ai$e6KeCk{?+`bMz>#;u;+7A=yyCY6Dqo|ug{mQXM{0|Q7Y?^O zo)TdZMwpxZrc_pjzKgcDHlS7;k5Tu1l1UiHmQL}b+c9Fjb z>dwu2Upj+j(au7=DnAd+i{3;Tn+JAQS#Ral=<5+C@~_{%&4%QwTE4EXE=z{9cp>Xu z!QLEsw3fUE8Mp+fy`-ZSJcxtB<2J7F!s zuCN7Tv!b~2bD_B4c*(i~lFaBlT=s@f|=!Y%8^#e@xZVUI3a4Yaj) z0U&@4V=03Uj!sY>gLaj^w)UFaR`*)u_}E*2IF#Q?=lTx}sNkl|Fn$N(BygRAbUeVE_XOq-piMl14Cdhp&HzZiXqQ{lL8&!==xg+ zeLqxh$|oZSD+T~103zMMz`&2!#_~3|*QHfRUoi!DwN68XIX*oNKHkMJMa}X_KXJEt|c2mU*Lo}`rcke5Vk;4WcZkPekxG7@xl{?^HL@bMA-X*Ne7v`_4Z zWJ4%z=xS<)$eeS)nJz1XKAHv4MiUZ{k4L!k1Ztj|CEorCd+HC(e8Zowv;y3YB^y4w zSAwqjI;g`%cKdb-B$d3TXEpTcjg5ao&@PYW3{|F6dDerLCEY1LsEG^P{it?uba65L z==~-)*Sxg9@_bB-FHQn!+fz!pE{wnd1pYm!YCtvjtT9K6(|(QlH=RBf$0&Bw7VJ(y zWr2&{zLt6?Q!z#vPkUOACw^s=x3;^mcIEQS*&mq6F7OG(>nmtlAN5JP>yu` z9Aom;$RI~OT5ekFc<3HyRn#pH(oh5X6&qH29HU`w=-+(1Lr}4NL!e+K6!i?mtr!?8?^)EnN+N$%7m&_HYmXah>RA3xa`~<;VbT&CdWC%7 zl=_>sRFImv4P2;lV_-2~Kt&GWBU{q9zo%ytKse|@51v(&=N0oea*eQZ&N-f4ep57( z>O)22{1L6ZSm2woS4;E#lEE9qRd`ST{VWY9wU919*%-|XlN}&MUi&nXQQkYZgRN9; zDBVYn9`Rh#w=yxYKgJdKPGA^kSuWNr;E&uUzK-NXBzimQ)mo2L+5oYk&8{{QkAi`j zSx-kH#f06p`5;jw}6<>u!n zQpSNc=iu=0()hCXO?e2FA^Ag9 zLxZrRIcqHCc{-A<9q?+Pk)DFJx7kN;XFzrUbX`Q1hnuGIn3fTcW4F=kVQqeh9X8$V zj@xkNApAo$T&ofX6({T;xL$n&&8DFE0gpNW<11LbP<|IOsOqP0T$N1@RnPc|zQSpt ze)wZV75npx%iJk<-y29nV?+YlRWJ_P6nv3gdpL|AO*RK96NqqRXJ-S$FbO}fG5lIM zF4SzS3h$Bk+^(uMCg0T)=EZq-VDjMon9l|JG`VaFw;(o;?P5Qgp2Oqps31K)6*oQaeZ)(Dxo$6<&Jm3VA;7wOObQA5JPs@ZPu~!Y(7u?Npp-u=}>r z3N|1E9o;(swjJLFAM+q^=s~G%eVF^37>wO+y}LuD)z_o?TK=D_(W6>#VS84%izcSE zLHIH7RJa}Gwy`-$wv1#8hw2r)=3B&Iei5>)i{sv4(Pmq_+^jUs2m>g#_T6rhg-LAY zr={I7^*s=HR&{fC^IHoT5yGIYKcr}1MU?OmIH25N6L6nz2JXG;Q13+pApk@nFDiRWfwWmaI?%*~65Z3|> z{2(GTFN9@V4RoL=`%<9cs45cv5J1`+6j^O1dW-|wrs5pQLD5qC(C&0SDC>9Kfmv62ieBT z_D)*j^t5WpuhBYB~gQDn6Y2x_fl~$3rtMrhl)7i-_oqRsO+iB^e z{AdGHj6h&&Ec&B&acIIR-Q~nu6H+bMB$npSSP&vpN>_QMd}&WBPIL}?pmS9aRH^`1oL9V-tBLZOC<~;;TK~;@Ev8(>~U4AhF-?t@8;~|>XizWu7>2jx=RIr4O5i zTdqEmXQ_IXz4M3s6pM9c>Ct1)6T6YjiXt0Od}2Kb3styDobUfs@6fzKktmuNVmSbz zn5W#kzy$_vX^@Ge&W>>H#8pdzTIxIq3_hyC`a=yY;Xq}@S>x5X`tN-;{Df?}TXH4+ zxXoU?QsLiQ9Bjx_(os zm0in21z6TOLBq(WJy0G5H+|f%wO8Sp#2d;jPBUnzBDqY7>?Fbp55XjLNaq8DyQHYd z4y;T_NCqk$IDka$#c;KL+Quv8ff5CwK-+n=#}tX5Ld$yi^GAL%9vA^l4?a9CNzUjg z48bij9UpHvz&%GEzGZ06i&2mG7!y%wYRUgn>~@%JoGcweDEH<-_+bTrho;u7S)O`o}A@0k%PVDa$X* z(!SkAG`@&TlJEFxmqp1=NSjt9`DFwv~xQU@6^JxUPdH9%v?)+jBdBUhDc`7MC zPcRa}CFg>80(MmJTG02vphZMT=v|Z4IifI31l$5f&5Dxf%@^eWf!*Gv6Cm_dsbM2z zFn@n`_0+6lmky5ly#WOPFp~%MH3LFryim1Up+i32AW=T_*926wb;9mvr2X_!i8F1jCa`TLaV`6g&xBwq;)xMCQ;u}dd~Ks2<= zGvgp~b3s5=Kv=kABvC?K+!T!Mhxos3HIRmfnXYlXk-y2?{dr)Ft~ABKHvyvaH=nyS z;ZfCPqXEBj9)D+aDQC38rh)P;j~D}8Qwc*!n3$Df&&26}yCIS2p-6E;(~2=}#zD3N z8vP32{6?cvcEcA}>WyhHf>`EyvXVpkCc`s(Bc3j3e9fmvaOeAg`3ZCp0nvg^h1bR? zKP6=byf846eBaC%>`?9#Utyb1rLo93U6!UAw@Y>o#z#l4# zG#3^Zfph{HP=o!Ce4wr=U|M^CW75hj9UTIlJ|GS7o{^l&z)FTY=f$IvQ-(UD+69Zr z8+We0xzQA?@%6+;r7(4A+wC?vKD|AWlDGGJFkpis4Jd~692{L=zC``OW8z}q5;n1n zo3kvYemp=}PGz+%BRya6-1w7NmC2zKL4zC5?&hO!e5NXW{ME=XDbxwW!jd~gnrEiy z?d+^Z9Zi(~(P*rWh`f1n-b7S8GP_z<{vz!8qZ6F+d4GA=Gooc2XI;3rFp8{Kc@UT; z`A53OJG)koO&n*wUah?=BPq37=}4>g)s&<8cOuQ_!*27SXXi(ZNSU~6!ojoXu=^nk zyM|d*5yryzZ?XY#63wI zliYReO}_kIuHH75*C5sgW`SUQY$O+r%eaFrR39EF|Q@uxf zdOH(5DeNJNscC6f!O76Z#tRBh8=FNK;Fs%abI-}moq0kb2pidL=^MqBD^UdA#}g`FX1X-7iHa!SO0Z7 zumA-H6Q}Sz9U!?^Uw%5U!s%A6_WkvcYtk#CAv=2$lu;n<0{{sK-9T8lp0iB_gJZx41&Zqf@X-RkGRzA?iO{+=`;pF^ z6(KKs84*4sj|C7u)NHMLNz~Jl{7>9R&ld;{;}rmv-Tc!VV728+mH}7p7=_cBarH|N zqixWJ?u*uPW{B2BMbm0F^AK9&2Rap6df1@?4nuwje~{7Gm92-opX4DJ z$;ZF8Q4`zlZM||OSHhaqpHTUby+Ewm;4>!mdni1CUl=;^7%l(G1vqp ze3Z%gaG9Y8=rV}XNlG?+`0%_4<;SetW zEjCbT>7gg3Q&DGvT9$dXPfSOGO7f|<6A!CJFwY8UEtZd#Zh6j9t|$`u*EV2aJ^xME~(oqU0`5)2_5|3agOuk!5_m3?cdfm6Do5mR_aC zO&9qfy<5H2n$Qn+cdrD=VPKJn)e-#T+e4I#RrH+R9%-j&X0ARv-ncy(n59Yb*;S{K z?+Nq59F|W^#BEY6*r$KTnowD+@V$a!={Fk+vg*l7i6T!7Mvf`vc_=OaT*ST!=SqRy zXS51^qUUus{fzR`r1Qjnf(>Sjog1V+jRoBEvMrKtP!3fWVU;93vZ6`>B-|9Ja z$SGhMDela)Tn=!UAe1?y_Z1dm;w72sq0_umKM*_nrhWXwf&Yrv z%AUD?=tx6cu9ZDK!v8;?YotA(nWmjSe+rsoP!t17(&P4{b;O{B*5|37Q{+;~w+!c#zt1F)bTX#IkP1)y~> zWWvc=T)Btg6}}?;uPZPMX{7HI>3!?-@;A~V6ZzDOl2OSCuU_%sHmliTgU?I91;%0h z^Wu=1pGw)Jhx8kcj;>D_s0{U*r&lQl?_F5DU$uH~Sveo4Cw*d0nK2S%;TuCW{0t;W zfs^hFBp29t4}pyU4kLh+&68vUH6<}61-u9VKR!4(SOO3mYZUwM>+l1ueD~w;tfr&Y zZ$|O(lRwN8C~YN)=LPFI9R!?80*;6N2Ircy9K@ryia%yM#Jg!|CMf;(l*6Key-`=r zrlbm35ztUmqv}(bdEjuGd=!%P_ihVoK>eoDw2MbmkH63Vde5~Z+A#l>wpmF+yZ}QX z>db1NmE&STjs^~{k|!xQfafHB^B*bKUtck4y7mjtkkFmhT2@{<4SDW&Pf$iiNN8|; z`~=7eFun=BZy5VD*lZKtG;9u%x{%$^uZ;~rLqi(p*Vek06xYK0 z+T>)h&=q`6=+7Z&G*}8s-$w*Tj=k>j7#O>@JpuWIEAy!|l%=V~qi3$DkoSnq$hoYl zfB8=LQ?E~|kb&#|!BcOnZp=72TKtl%QA1`uDKiGiLqB`y80c9|4t)6tZ)2ntVZmO31IE(PQCR)NC0-!rxW9OV@Y#FzAq> zc!3%st{~i*`b96zt11Yd?66Kw70!6ci+uYg`2*Kaaf{Qpp@Lvcmi*?;n{FN+5^(Q; zauM_-MeO@>o704=$3CzEmMjR%(Wg#MPM$FHcg?ian*)YY`-`gU5t7?I%fY~KDKNs4 zA~E$X9+H$bGP>RX#EFW?NKD~*I~aIYPE1Wb*xj`ZW=B_e?R?fRxJ-#18ug2~*xRzX zq9=M{oxF~A60#=uveJ={+k^d7*4gZHh$B}}_$DAEtgWe$0vRCaK>;(I+~ zo~EQg?B<-KqoYq))$G#JH0L-bO}V+b zk00aMCx6oVyNcZ4RjMghh>wVpPp~d(O|OhOa6IY|2i(kVYNC;SrWFu}9)}kG2!~D? zeMK%hIT&}rTQN7d(*Frbi0OB<76~nWl=Y6xbMfhQm`4LN`-2D<(LBa5DE(SkkF1&7 z3aQBXY%)91n8SrRY6h%?@_Ti}l$51VXf{HjIr83(cAgoh)q}Sp`n`^h$H22?{o$|D z!Ap&66&mEvz_4@*E?g+1fc&>z5|JDEcvMw%<;+&o1 znouQ93@lzy2sG$*bdK>FMic!+XcUrjg_Y{9X~%G zkB9&rzZgG{fGC}SARmt~{2!kvkC>PV9k09&eAit%K51!QWjY~VMH>(J7C`~{k=ncl zhDLOPf^-5R{B(Tqr8=JOURIu(7Vb7Ko^-;Z($fEY2(kbDAq4n^c?9|CLd?zW>WZi2av`xB*}CzpaEIzYvd@D4j4rj|hCappYOB ztli&_^glg>5b8(%XC*{#2>)9Qz2G8`^*SzjT<`mFJ!5DtuQ%xYc*%L1 zmu9&8%f|VV$;jc<-n6Uq#VC(!kcs4tU-_=}2M5CbhqHkPn|tebMILflm9g0z`0k&O zb>BXUx5@8%#f6X#1c>Qd_I595XI1(dCYLRZEk~VZrc^cC{9Q; zs=Cg!3THkZoW=6Oe1&!STfrL|A6|8C33tobio1rq5V+|3zVs>Xk4?o`d*@L8Ob4q- zeB2ux=Cg{}q8Hz;R{tsd{C(4oVT%zVniTP4gu8>no;`!>{e#xeN~z6lDv7^toe-*$ zR#ETCYDh?Oh%<^;ezls`HxFisT?9oX;wHN>{)doBIusnih80gqMj#kQmY!uP#&!2LWqa_%Wlf(I*#CAQG zj4S#ME$M+)g3AH&yk4JVCgyYS+ojygdP&=7jALp2&Z^#BF1t^1Z6D#sj~1z8EH1n| zMnrU%u)lpzntwj3*&~mmwpnJ$d4xxSa^V^at5ERcH@u1#ELH=KT1tcHq*rxYi)$I=#>{^#^eQonB)`1NVfYqCj)tRiiZ1%NMoq9w;Wyoe zz9d~=ik)__!J}gSd=jGv-g;DTw!RE{_KXlJIoZ;!u$yV_F{Nl~E-^3AkZ^qeF@E~e zVawu5V4@!zj|X1{`@T?oo=;aqUMHDkU7$GeuBO{!2}7`_x93;QUIp9FoXRft4GQAp zDPGypGCVY{eEq|i0*zOr)e($u(3%Sv@+WL1pOPrpG8yRU(vKCqz%@*mX<$Z76c@vr5mKXyT6n5Jn!@F zZ;ZX){SWLl##&>Jxh8Yo_jO-+oX7b)zbNmzNGg3(NHtbeCCfRcW(T^rOZj-(@LBy{?FI)`J^u04@0I|?>Ot#p z_c@-)Cia(HY}d{7PRi}XOhV@R>reiCF@x>O#84jOn*ZZbj~mmapx=@sa*vzviTBU1 z2W75I*u{P5tviXWTlcmT<^EPdn^12jiZAd!&zy7ZPK}w;JV*&F{9|-6{9vfJZ?21| z=P~A60dB{qgv%)6FRA57-p5!6+?Q9;)cGhGzPl)gnnh>p=_$j_hnQ(`%P~@PSdpb> zsvlukoN)A-u4giJXxUxbw0p94hwk>-ooy`Zr*x{z?$41@ww+Jj5EYvYJ~zr;ND=&7 zkul`m%!@I!N`g8bnXXgwlxfIik^tkP^R4c{)Ifr(2 zE9T)0$|PPG4dwai0lVjsq%+M& zM}#eRpMUfEAnPv1fcP#^JJ*-MS*)paW%)s#4!>kNc?6Lh%gE7Wxb~I|%YdXtqoTOW zOfz8z3rK<}AC7>*>CKGmwS$Hf^&|a6#dXElP{CvtWNEkA(v#L zrf3UCBZ#Q$G~l8y)W1V)`J8z}5-FXmbl3H&ro5UGiEvCDrws$+%b;41V)e|sFB2a{ z*JD1=1i6zvvdsO;@b1nWU6!aK=IK;vt_>=~^ZurHFB?`Pxb{v8w>ECQJ0{AEICHqY zhDSBMX`;f+dQ!1y7qH0m;&Un?i{3Xifih~P%*SdooupRVChOZFIPsj>UbSSQ{+R9q z4JY*zXsu4apZ;zZ*hO#ZiQ}2Nzs>W~OE>Su&&=&j?t6}Hk9~$>H605ZCb|gDTZ;Y3 z%(6mI+;2^};_RtJsp7MNb#Zx8n^ZktGuuOyH@t@!3Nh^MD*dXQi-{Zt0!!@Z8z?~q=X?5)`3hZo; z;J#_qh%55y`1aQ=rUO0sHH#fSfdoa(a(Ar4*dn^5Sjvuci zB!_Mcn^rvbi{G?cO0DYseuGSOV?$p>)b&TeYz&&cu+Q6NwAYco!Khpthkvf-%*#$u z37_iZ@^o+`kAyxGjv>yKOzy!7p!Hd>8Snq*49L~n_~fO6!M;oQ2M1X6os4d{?83(+ z0+h$1CVu6I+#(o+(MWQS-TW)lEo+eH@+=fdcunwRNyYs>E+kS2Gp{v~$7mxpXQRE1 zH0Jza&B*5}UtIhhfnl_3O~tl<*-8|4PbVNdUA9Zy_V<`rOcg3M{xrgWG%K-lm6M>s zRc5$U`gD04+vxdb>;=ki*IQcXQ%gNsc%*&8B{^@1N9xi@|Sbs>ds>^QU zhPZn*lKO=SMBh~+4ZxqvJ}j(qC{U9BiKZNpxM1d=+)-TP=QI0gxdHDEe|k=bQp42( z-_grkf)Z}@lUv8==S-d+sL42&$B!-too}Ru?jI*5%`x6k-X3K+C3@a+<-oVc-6$aN z`Ctk|`P7-oHeokowH{D0Z;0;RzZ$T+dnbZ>Gllxm*zEV6?DL_^l4heHXCa3s?8K4I znIie`?6dY#Utz4O#GD+NJex3OXPR;342pCXpI%DOXfbK^*lk|g*;%g_qJ5HuOM%j; z&nFb|rgmDP3m)cy2R+S{UJX|x>KoO%?u5H5P6 z_muuKMSrYc^JKJkJisVraS2CK`TkuL^{>Hua}NhJ)epZKzZxju1e%21&`Uq8k{Zfh zExFewHRReKC6p+7gyyq$x8XT8_ImnTF^hMZz|V~=HeV&`73oMYQ*H{|6tcV| z*)I(C`FBztA`h4)Y;M1MOGtd!p&j8^NS;UMnYrjrd%L7&Q zGEgYxZ?Dud9E*0^d=>)yhw(?+i}M{p1>iaqwzSnQCAV z7U14tToUoy8nDe#4Uq_TsH3O~i)K?zMd!C)Il^fV`&k%hSiTW{H`=4s(U)s6Hz%t~ zen~@uN(FIm1c$X33*E`X&ZV>- z+wWF6OALL{q-9cTE8|UlBHwfW(cPa#;wxWXta{%+=1O=UZA$kf@0rxo zqWX>dOyAc-%#HXRu^EOW%MCs?jpGvYFuN88XdcVqMkVV~Q=gTma1G#+t+fwjE1Sxg z@Z~AI3|qz1oio0vCXLYf{$OF#IOGE{d0yWBsPGH+8%2VP+d^K@Rjj`jwj0vz zSjKy@u=wl>$Qmte_2b5Ix!-r5e5b>fYW&oK+-7F}IY7Jfl~LPH%4D3W6l13G=lX;< z-+o)4TDH><%_C2r)GU6YC@5UvAs*WhPA_@j^CpY6z2-HQlK!J-I{Wgj_{%0cx|(B% z{e-{9oECeCz_!29S*&=vCI~_d<3|XDL}vL~b8mUw{7B-jBkv~!`lFR@-#*i<`C?ug zk8|YZe(g$c8*kiGzp+Pmg*eXF&>3sJnj>X?g#BpSvU-}y`Po|a_+>CrM}}dw;jDRe zRb{e9jOBKNU#n(KLuXOIOfaPO;gmm-MJnU#KTNATgl6y6m>Cs(|D|SV zaLyL=h^NRSXxpb|fR&fl<*)6KgyPcl@*0xi+dd|0r1DqQQ>@hAj6Sh_37pEkohM-? zoVRjeKdeUjr^IejL5ogf%-Q6q%h_dTxng{-*1!6d5Mr;I%;Wp^j86ZdnqywwmL-GI z`uo7xweDtj)7PvrC(niLR23gf*A%7pM|DbXzKMP>{I+9Nj(-Yqe<~#GM4dkMJyX2a z5mUR+=p@k{A#Ri5(D|3el8z4+_9k@dszhc(KncXKVA(pKtruGo!M z*nOfaK`en=r1A5SMSfF%ihB?9no7q$S7Z<+rjuM@zW%f1@xRy7IIe}S|I*S_u7xxW zc>@cO(f%W>i6QK*L0EG@IN1Yw2eh+)e*j)dTI-t{vvPp01^-^FM*n_;9p3my%evOs z5H^nRQ89S$-*3U2!omnwE%*x-QZ5!YCN2)rXB^iO6=+hj`i}PI*Z+@+m4%I+jGjrw-(Ib=6Eh2HT$Ec0w3Vp~tb<(m-E@l}ci2XMx=-i( ze`xsY7W*F?{-4HVRyQ{S@$6b!WtK2DH#Kwo*Xx%ED}=qWt-hf#D45RXhQ>Tg0^-M{1&Pts1zm^wX(e!IIJ1!CKWK$cwi3frTt( zn(za%cOqtFcZ-9pQkITv1g5_ryfx*zJr_GWmlF5fFK6u7*c=4n-IC&uI^*{Qc!_TO zzkmLx7I?#RjjbyqMy|gyF!uyH{SXS$jTf)j`4!{+AZegULWWXOIV*-uF!lWo0qW4o@k30x?U z;G>qUl&-Q8LY^V|1Ms)t3j`7((D@_6!dQS4JGMEPqgo9W>Aiyt`~7&qqXo6NAm$4y z@elo?i9<(d~ez6*T@&G!-?YR6v%H_tN*Fr+S z)(Yz6JTd_gT#~B;Nk(7$eijynT|$IA^A+X;nE=kCVO<|tEjKU$TcP2~+1S_sj{v~! z86=QN^+y6}KuG`WcrKJS7_!vUjV>>?d@@_Fi@flWxffcGbLO?)8S+2$U}a?mhXxep zdtGF3%R{_xYU;}+5x+Y-(Sstxn>VZ>PXHhxmQaBnEX~^GR!{g0+&w&6TUsD%D$Jq= zl2qV(GDHaWQ$q!yrZ({zD_9Z0K~&lc9Jhq%=!46%Ehq>-)AvP0T{(=&6emuz4WKS|;7Y)%6&B`{qG) z`+d@`e2;Nd*@7X>ef=7`eDKiA(~~9;eDoHf=-=hr*!%LgAd40vrvRj&xfm(4R6j&g zb=G+9d0gn+SjtrCd{ZI9n+u=^Y8GhGo7ecv6J_X#cDbK!{Xt9v_hE&)m}m|Xf3nPg zAhMN4!! z!ej^8pg>vhm<#5zWmg#k*FR7`()UXyYP|@K_})J0ZN1K_gpfVZ6^sQ}k0jvfL9%N4 z#h@1AXET+7sqqlq*sPU11l%CrKbjcEwjD?4nIZ;x$c%JNa_=J-(?$(8k1$dWEF@3x) z4u(&!x_Wv@SPgEU!(VB5vrB;clZF7kuvl1VZ$+`FIrh!L+G;>h!+cAZq?62 zPCKxP=4WOU3v~DorFu<)xH1X}*ihV;GLe$CusM{NKk!KHh3xGy#?>@ilh){LiXG|l==&=C?diyzN@HGe`kIa@j> z+>5bF!xc;^eFQ_oIZg~;$b2Cq9L?2hEe@=>*5YROp`>}o>AdncIYFq~TzkIT!44o| zW58=roxfQHW>$_$HgGJt-GSkT&Z}#tfQ2*E3<<_wmRql!YIvrGqpt^2Rz{ivn@D*+kX(5VE-N1pR zlGTp_xa}d%I*Y-a^$pB;4gSVjMguU2K;m3UNl7lG;RYM9+rePpO}_Qw>2=jQl8Z{G zwMUR)g$;7nFbdshSAWj%?O7X|XMYpGt|imZQxYDXGOijk&BL&?<&0|dC5l4~{a zfKCH|1)5IlXk(mX3Y>KkK8`GS!`7DN^Y7XhiDhf3e-Z_XaC%|1U2QjF0$K*9oAtA% zjg5_!rafq{bW}&1>jDQb^Y`tWbR__&(wxu!uOB1E>;y|};X?d=US z^mic_=&+D#>Hysdm}jHsl0NTY)mRL_5y0d z)e|q*HGP9eG-0jN?)8Y5NN>7X#$q4~u<~sYZ%o)oF~&{`UHswlTZ(yY|JFk=|Fqyf z0Vi+>=6x=ge59yiyjpL@FB-_} zgqf6@+7PU-xvIHh4+%IdfYf`Z+%szP>(j&Z8Jq?f!ADryvOn^i+5K7n?DkOQn< z;5*rYoxmut{4>GXw%wO5jq}RgbNlZvNYZHE26o%Z+abSO5C6L($l!XVf2@6I4u-sSDV`&nhE2cecgOPcN5e^<4pk(Rkk^V#j5QUKPa+KQI zFWeCDrY;QJMqi8Kcbl5gsw`aoM|nPu;=$z_~pyBf$TY;f5PI+ zd0pTK@Dp=!tcD}QXb%p5-Y)$-d-)ypBZ?LwJa=EXmTy`8oFrW*zdoJA8XBbm$=!h| z$;l+Ll5%py{O+ZZk&%FT9OsY)QZW?Afea|vK}23@V*YvbpC3H<5%v26{QTh_8v3nep(E`~Urs$bWwXJIQ0J&L^2)%pecxKTS)g@UN!j zsP}j|bo)OKO*H$ThX$ME8#m_v^%0co*T2)`#y#1WP_JKyy-X@UG3&;>eyvCMuSUCm zeZ%H|dCl{$hWY1xw*TczWrUNeyX@ro{`nvOm;bz>Vte-N;lqb3x_PL`Ll}zJ{qcqZ zn)AK(UqAA|rT3+vpa7`)8_5$B6EJVYeEyt!@P`2{MT1sEkwd~TJxuHd&8_NN3wNq* z!0F!$yH()ojgF7^*AekLLgse=As@3?C@~OChrydmO+k^Cp59vrdx)-TgT*qk!l0KYWw4xFqj$ zXk9*TZf-ul;VeZz;y+O^VD5{j$3m`WSeU|L4&LvlCOA$8nOKS{clv%KncsY)RH&s8 z@@9>bkM9hoN?`izGxHC*b6=t6&EQb#;W(vaz-RIM%$}!V-H`faBgCUo5p9q+15X^b zS);1lcCQ*Wu|tg~Eb8TV433e0kRpmgw zgESzMBF`Jr#z?5NNS`;^4Dz*Dk763i-$&k5jFPHnen+D zJxlNX{W;A0niyg*^|BfMl2YpRYm0R{(JxLZjSSgFkOnH z>DWBMgxYA~Vuqx_gd)tpd4l{Iofv781y1s+4lM=UbrR)jtVGji7$VSgaD+ zBF$rf@3to~+2do02B|1QbCENrg@0BvPxA-nmridM!Et}R(%mynr#&{vi%?R2r(JZq zsO_l27Q|U-Je+r@lt@)y|EV<_0t(;Y z`2Hhqtlau6b~lQD3D>z9ZKNiM)qB!F1-kudg*c?=Y;A6Ka~g}LVm}N1m~snU#^SoC zh#=cI>Z4}p<9B#jypkCGk-)r4=!368r!SbxM8!Lv9y_ymH4R;-y%ybHy9k+c%ixo4 z#pCG6$cNT!7onGzm%yo%la$O}9t*m@Qbs*JrPQpK_>|bTsrUF;{<&1lCu~hEEiDv^ zOaIldti2YrNBcv~&FKR;`s&}`bX7l|8_r8;B7}NNdB7c!lG6l~{bxk2X0O2OPM9y0 z?sBtaUw2RJyOq69kp6YOs{oeiH;~143K6`|KrE1JlLotr=pT?BJx}=*aRs^}6@)eB@-=q*)CUOARRRJrE&?dvDHuKCqk0v?Tw} zYV*HY7ZhGWvQZ+B!!oP|2Dd5l=vmbn#3%#eQ05*-H=jSHd*GnLtB6YFq+)Q13oqU) zN~S==YDZ@_bVU6}oi1-|4Lk4>Ia^|G0GX2~_FPTU|xNwURy zZZTt4pO0uwgwb*XZzYMM<9z(#g}zFJSzL=fo^j5?z3PTvDgr=LfGVeykJkFqJ4(&_ z(a+g-^_~xGpke)U5$}I!D;k7IQLol3RM4ZPM%;Abi_3;-Y2_@Nn9sR$aB#4mnH;m_ zV}jZO>?fBH9&W1HGcqEpd0fVWUe+@HPu>0(x~ z2vOVsd*G91h7#?c3D8oS2^r!19E?%{3$Yi~%a~9MB6VRc^Y5n1UNf3g&eUiq&@3oB zTulJW+4B~8**IOdZ9k#Uom+1ZwwNCTf{rj6d1W%|Porf*&86U|(AdyWN=&2?8`wwl_kgufFR0tf&BnaqC!RMG ztc_R$;@O{VU9%^PD|&Q771)E*4}Z3&Yx}tA3f>wX?@9nLDab4%bC3xPb5c(|vnlR0 z7l+fT%Bc$o4ms$5kd33OEASMj@vS? z+K=Uu4>Z2**KEZV(;)@?y1GXiblz0*seK&<%K`NhvR|H$c687K?T3~&8_s3Q88WQf z9ad}o`A)#Z-q!=oCnvZ+LPdpkvdZCul#7eY5n!9|e?V2c<-})Zxl#6lF4D8dQ=Br= zA{zaLW>kIrw>6sJTrw2L_Tj;-@wYv&)e!&m_}6KL&+*I$$hf%etyyS=;8N3LZsNbdLatTB0cGxzNeqS9i@ zQ`675{M27gV{kWk2R6PYxh-8}n_bZv#CoG$e_49DK=dYTES#O4K|ASp)>};PlcI$L z9Vnd=jL$(gIF4FB51p0Dxhb(!d(?6wV4=tRs*X4fuJmCLlT~Jlm;Ui##6!gjzgslb z2e1f()!PJt@PQiP_fbuBK4nKie-?M*Q{7T&v}6@4d<1{!F52 zR9~~nhHh->^3`dE?!bl~Qv2j7AYF*q&BFZrUP7H^HI!$}Vro*5p9?2AGqAct35iw1 zo0f2oD$b;w1Kz4zGvauU`042mbm0e#y<>gSQZ70i2==)wSH9Vx-c#ruVuk|-?14Z4 zg@q`;t-dp8Ct3-(TA-1FynDcZh$M5*i7$(GTQ06K$L0O)E}vH!X2bBuPI)2dUsJrq zmXC~soQa^-?%Xbik?IXcLRd$0WRv)S8e{oUW0D*pL_AU)T$rG@Eo$9wW;&K_%j@^F5QG()y?Z2-_+c^9kw1Qda7wjK9;+I**OD)(n>-#c96rcK zRRK*3A}=U3dvG#VC4e8vjmm#rbzK{cYufW+fdI=>xcJg%0qU0D-71FP#=FVi|5uN! zNn*{kQa{9ZFwo4_5i>SdswgqRp*j)KlGESR^2p>Z*zlp+#MTj4>-as<$LzTL=NlX) zRV;FzvFtTy(?SWG0nlw=X#%+fEfnkftyO1*fvU7Mx#@$wv8&O@h8iiKCr*4c7SkKL z>uQJiCwsHSKR>nirPHQTGX1?@GAoOM}mx}%8dJYsd;xDshTo2&2!QV+-^ zppHYTVr6powueT0S1vnGbRR$ofYd$;@;z*qH<2_}0~w_k&B|UWT69}R{Fr=wse!dP z_)g7d{bZhiW7yqgl#4NwWjJoSV(iCcPG45^n@U0aFbuvQ^>HcMrXu*p`6q!fJ}~Kk zd|$iMLNL+B%;3&Tx;;IpcElfTTQw2xcMw*3uwqpNY7S`VAWhD@<~KDpfrz-PUE~Aj z0MJBX^5tGsecLIu0-IquIXO5gKvmKiSLd8_KBv`vNW=?q+nT8A2lWz^XWPzPfw62k z!4eba5wY4)bp zw@)S{^A9fG$ax5MF|jzGy)vBy!kL`pVl@YufWVgs2$5LtDr&~PY`!n?;bs?o1(JCc zdSJmuNkQ)M0 zwAQKv6q~C7^_q!^iLf9Do)g;h=1WjCl$KQAo=dHO%mWv{3=55p&H{oz_PR{;)cd*P z3EuIq>}N29)4Frn=*CRn=X=-%*M@bP*!#Sc#DvdDRvs#09Aubw?sF=UkCX*=wq*7U==g7 zJZUSnTmSMZJj$riJPkV?#C7Ns6XGu$JAly4uwmqWw3(Egj5udoF&5ZtvLBEBc{p{M zolFYTV4Yi&4Tqs_n7>(e10DUW@!vKa45JA5~T}(*DOYhEQkDQ3Ng@X~K zQXXxLtM>&Zd^>2H)L?^(=D_TLAWdjcP$X}Gh1vAMLe+hh{PUxB?~qZh*O)18f)~cP z;@O5WCDnnirGL&9Z(bZ$3v~bXVQ`vCOB{&3@WQvjf0P`}U@jJ!W(RRNIDSG&#(51x zKypOQ1E(%4aUNn~rB0aJX?{@mds}l2G~|wDu|f(K}2k2EtHD&nQaPyDFcI$ zyRh(2R(OAh=PGmV3S}M7sVR#5O*MbS8Z{LaFr-VN<|E-~jKkYlxeq|+?Qv<=QOr1{ z_zTg26>#irXe%g4#=b9}XOEGQl5#&;h&4CGB_UyU(sAnK+mb)aU%>LogJV z36(#yi{cgItfp6Y9Mc{?@EhBV3yYz4+e+A__}M%Z`kd>WpJ}glz)dz97Iv<_*RUm- z(Lgx5<^Uj`O44TN`xD)W(a-Kc4#nA%<)2LM+N4q&TF-yY(B_3~Vn1Mhu!YgMa+a_~0Ntnq==i-)5E&U<%vW04c0zzd~DX2b1u9~Cbw)OMq z({mE$0FIb-fLXWh0dx(@5x+ZBr&?#Xq`Tf5c;Q`iux^FKMV8Zh1oIWu^gYry_MfO* zJx)^W+H4P$l()Uo|D8MD%HQ4<#mn5)=YBSV$BF(kI-Jx=CrPsZ7~;D9pCJ52=syei zCJsrq>YV!6c{rKXT^&Hk<#(7&kxn4|rvIHJ1p z@U3ci$wBt2&7ZGAaJ1)at(0z{;U-YTZ{;8ls;d|)=HWhh zLhGcnu+C`XyyRdN0KKE?fINianVL?;+NBIz5~e*xd^i6JdK7b;!I;ZgtpCh&bv+4< zT@uO7%T#9Zh$nOXk0JMUc2>9=SyHR(cmYCt0hibDQ``-8SP1^S-4fLQ9PCC`NXJ%j z3FqdO(o#4%!Z?cJElA{#NTffMVzEvaIoz5?K&)n7%RK$vhYt>1F`q(1O?9z<*)e6j zLojcpdg1A!#G@4Xl-`-Mtp1fS=`_hT`P;;Wve}f9Yvfqy*_C1&PsEm`Y)^+xg@eJ* z1HTldIhc4%ciB%*PI}j2kGmMhMM_$K%;hi5Dj5Ov=QLPezf@2)B~0#6@;n!PHQo|I z({`Whek#RYnXrLE#hMXy6^wM|x^p)12+_uf^Ize&2s4xMlx&nDrpNfA3vjW?X@enz zVDId_3Gf4({GuKj^ON<6-#(Xn$;DS?w5M4!Jzunz*6ut$(Mt9@T`U{ck(Q0FLBuLk z7SkUx*B+j^5Rll8yFiv>u(i{Jw%T}(Po z&_|%P1_J+sbZww5cuEc()eN3{KMyf+ac5}rnS3U?KoOg!Fj)(AV|;wR9Q`dN6KrFA zRm#%lV$;4eIhbrV+|I=;y{nf`q3ixTc|Ia7V`qJ|@UPu z+{924PDF688Dw>%L@wiEaB1ke<6`ea#2r;q3?B}@Qd(jZ{l?Dpx-#rhF+hDn+!okl zN*5nxe;-ZiEDw(F^LSE3p*$%!SUun-t(aw#%%YD>8F(1R7Mn`N!>9xwX2&g;zcl^* zD#T4Q0`=zQXY#i2(Amn!*J$GS*CJB7R5T>U5;&BQwC-NZp>-wh2Ns9qNs}W5tJ*6^ zpT62um}HEdLhWs+^UOKD^xI~thIT2?3l;&6x`LPAqyns<+;~gN;#{cU;tJ{0)FLBC zM|KxJfs;~e=c6A#tO+FL^lf6j`YrMgFyvW(4Qem+T}R1Ybweq2p2g^XjXqwjG<&Y< z{x-AmEP6S6=*#yxz3ap0i*a(sS8o1kZEXd1Mi229V&zxi(`3;FMQNT^$>(h=xX&lk z10&Bj<*#O!Z#;~~?@5|3KJI(`;K2(?0-znG#}aYJ$geqEpF%BRofbG5r{s0*b#qTg zNM7F*)x;^J#5zLnov7NIYmLy=wa*_ZIpaOfANd?;AB{a)sE-1-k%uqc{%ef0-Q2l# zxO_)!jDOR_#eR%afVOk(GPR)J<)R^?Ksu@7b$j|wDATE~w`9;Rl%0*!mNS&Jv<#=q zv`+1$s9=z@|GrNVD_wOqetoLLB(Z^0-QRko6KUo9mf>TNBOOA~|F*BIOZK8JmV-12 zx=_ejpXPXr3$nn{a=WOc^O-)2ZbV0OQVVP>fJG|8=7i?OoONuCt#%Vk?#>IHL zHjl?Rr`jc4^&1sx|GB$91rOplp~j3~aA#zJ8R6L6kk=l8SeGKpETWL!1u7z9=el!M z8%o-gS+6%vpl9!`MiNGL_rEE99q7$;08v3ui|uS}@1Uczw;>Nc%@m~*SH_RmKr9pUyq31Y5QFgU@BKycU0m>Zg&8NN}}W%&4GG9lh`twie>V!my5 z_jE->-j$R%z#)h`e+1&9KOy?&slL9T{KJMCDx=K9J;B9!5ctDHqOv%$E+;qMa-QNV zfK-xGaH3^9i<`ZN|J4!y0I}Zp_LHGAtTAQQ;W^#n<{Bk@Jd!p=2dOwy6xI18I7!ax zt2MW2UR6A#^UA)F!4@aCCQ~MZ``bJ`hpJqj_+-x@L`Mv|K#TEILER2YEd>GTvtR%T zb6i|PaYC8u^EGpqt4cwKalxsBO?6mWp)7>d#mo8}nxcb+iBIkMBQ9%0!E=FL06jm# z^@vx=8gGMh-OG;iqq?gu)-5mxRF3alN5CO04ds3o%nVGP!$X2cb?|wzs64o|W5H}( zLqTO^E0Z}lZ_{J8;tt$fhA@8i{1-rS1_xgY=-eh(hvSGU0_k?i;$udoGK=xJ@T=nk zZ4%{(g7k#SlKeg=K44}*W#|mwCBU^%z5d=eUn-&cq1Kw*1x2a0JebP-#Ve*0bED)P zP2g|cjTM}k=PIxv5vT4}VLRBI9PjAJ?X!flZeF~w))nl_OYt_Kut_VUClsK%9!k;{ zgz$J2GHs~AzA{)yG!5!pmz{v|rx&CD^tj8Iix=MGE%ca%>D^iy`};p%I^=v`b=bG5 z9B`vgm&0HAy&LLt?k9BwCkXq_ONiOW8yZTAYNfTCk4s3}Fp#$)HZR{Xw?!*`v!Pbm z-iOr{RKra9WX_EU+~$b&xjAyZ6($&fYLfX;=j?snTO~aLh9u-1gS#uXVW&$M@uj+K zCeN>Lz+Ov`h9*lYn$GX`{aII%Ft6n=TY=YwXsQZE9F+5HsDYdI*QPa7)ljk#9z%TB zX5~2NE=P4_e|dO_pW}Ps)~1&=Nn!Vu8H1xzSPfegQk*yFmOfX~Q)U*WEtE+__pi(4 zQV_gM`jPZ!D9u_*E2g}l4KJS%X6R`0Ol$HLH@y~uYN{Q|lXOObf7(4VEnLcF=zWx2 zb(BUet3rj=Gtr7mn5bc|EO`7EB_!ys{g`+nzekLA5!a<|<-Mi%B|6eyQKr(@s;xlXSfG4tPVi916g;@Tc2cf_J^FLjIr5$zulkkaB~w1cxnVJsH4Gbr0l-d-<$Xo^S2$V&?T-%EoT*>O8&U{ z(It3m{_I|zO<&n}=BpaE70;Jif0C=tm4xwVO^|JN#8ch`+dZ!epK#uS9Thopq6W~z z#7vjL)2Fl)J*v^i_9|`l^JKczmht{q?lxkA_q&v%mRF`x^A3_3*PWMR%^B*-^nugpIS!Ehd@>5qHZAu|F~7L|;lzWxlbl%c4R?yfZ-$ScUW<+X_} zr3R?9sU=$TgZu4it<#HQ(xi z-m#)NRG{0#TCKKm{Fsj5IVbvHnziBhtceR64Qp4+*se>VE3|hQ{on1g+U<7YB*vKH<3mfsQ=-{>eR1hi#2J#4 zazLclh$zsm&e(T|F7`TeJ_QMh{qU9+_J(2=Om5*vrElb(o{Vu?_(yo|2z^S^L{ICT zo=&b7VKq7VF&jkSSlm-##LLe->VX8KEAq!dV`JmW5gvm^{myI9P#0_C>hnkTylyjs z__?BG42=Qju3T6xZEXt&GBX_F;n3aRK3Z=5dBDwScWlT1-McJXOeM*qVr3YC^ES@Z zFsdn?PvbdMTnnnHPKdFK>Db;ZCO!GleLUYGV+afvD6Iw&Wt)kxb4^j+i}n6YOqU31 znPZ>zB)8XHqgSnpV-%XjTb7SM8C$VF_3(RohJAaljQQCoeTK%zpIFh0(<%7E4a{&v zW5PZasR(1`m6}a=Ui@@wSsEVLa2DL!)3iDIvP44cG`V+@D6r(zv*gsxzjSc&Dn<`H zoAN1sl;*Ax$E&Iq&!?4a&u&wyPf4d672wsx`aGnJ9q(mk4{voaT-e=sAuNncQw56x zh#}aCA%1LRuq*QMm%iz8^A~+aLj8z>h*j$A2bAh#&!W_6-db>Mj+NcxKUZHm2Jb3B zGKKeppWbC=JRFs++rR1$?W|a1`#5yn&%WN7pv1~djei8z+Y}OvpqoDtP#*>k1qgpM zT)n7YN9%f=8FER1A#SGZ|3-Zp*cS6XPWgHC8qq6w01^6Ctjv5u{)rU_l`-@oV%mc4%^Zyg48d zEiBxDzRvxhcPR6V(JJ-w zJ=JPbi%-G9fRq2OoY`GsT0`wb8!hvYK{DzbHUm%F`)*20p=8dFv$|3yyS-Q69jbHX z3aqIm`WO^s385Wehf`kKhbi0lMj@O?v9jwfPjmm`(r^5s(In!?*C;|JI#!t70YM+i zm~?h^4Nkc2@E*vfqC8CaG1i)PLG2WC;;emp_>^Q(r}hjZ&zj8;6Gclzvnu8)TP({9 zf_B;b?)xmyp4~%7pV#^TJsK=8t7~i5B}^Ib-TwjAA=yUsB#XX%vKIL;so27qU*+}S zdv}2l^#fzf_pY&%Q%63vT3EzNG#p~$;!s5SJ4_Bh0P|dXk!sYgCy7tle^WZ}esIL} z)t-0p5%bsiNd20+RCQ04-3-#sSkmG%XVKaSFE7@oa#6n@hv(!e(7}D#zr|z;!<##X zB?D|X0RN=1dus67J!Ror_JMW_?nPky^-WElzP|YmLff-7MlIf;`+Y2Czo#iGOp}@R z8~1`QWJzUmZAKLu2#5KOK|4>6g_!>QIW@W#0}`b-?H=1%L$^_>3d zij1bpGc~%+tSg|3*dYxbgH(Afo`F%2EuuBT1cnjB_RJ_F^(0r|0fy5Nvlj&?MhK~Y zRDMW#<%ybVJG!EoPEV9fZAXghdmEc{40O_AZw3!;_*6E|<9&~G|Q z1)lF5nwruNxQz71mQv`My+D}XI0c8${yGAP6gc|R>`NMDcQj}2NH#SNtMu1EeotyGZ ziRMJ_BiHNzyA;)OB|`lUPB?hoby!!aSijEy`qIHmob10m@mHyH_lzjd$oJHuv!jCm z>V%>tV1C!UmAMu23|^0Z$e!N9WpX75DP9|B)O`o#ejb?JiK?IqlTK2X72c%zrcF8 z*%ImN5h7Wuxh)(he*8PmuzZ^qpJAR&zD{?XrW#>{dwM{K2Fh5sZS|wmL_c@goVf1$ z`fjmFF_9lUO;Bmv%xWWkQmV6|GIKP%x%a9IPNCD&y6{k4caTq2p=J>{3w;YRmyBK| z>ll@rehmD%bUk72=V1c zK+Xr)tUtE;z!R<1ll=C8q8y9J8I|iJOqDx~_txi0#(&!}J^xzM_(lc;-6vq?MDC9LQ^8GK-_m+h24*PBvGDEg)J!Y-WA~MyWU~T)hJ3yICDHE7Gz*R z=M`Hfe)#apYZw2M&22ZPXQRd*{8EDssBws4=8mnbtc>o)q=|=(h|xA~-l-fBLw@|MGW)IMOL0{$HEr>U1B}C|$ zG$OtXtfME~=wsCp{1$zF5$t6~iHH2H1+0@0NP>`#Y7zn+tMxjf98IVC7YE3+w%^vf zukLX9V%NPktksUi$~kBYkw8K5yNBoM6WrW2LcUu}Q+FQ^Ej%$|){syae0#})KHU{o z?(RRz%AA@5aoy6Jv7u|4!PENJ%u;z(gvV5l^+spRcB%g9lA6)d0;!z#ot=TlgoKbW zbRaKy!T$8=w|SQb^)DK+o8v#H{SnZ=TNg$TQ|tdi>6NJSAU3s${uw)h2}e;)%4 zIw3Bu{*DbKUQi0W6saGAqV8{%3GnexkGH4V#Uo>z`C0aVV;RjN@z3&5lG4!yHthgy z+ZQo9FrWhLJvcNff6$il% z|8m)a*LgiIDypf10goX-nN7CPXeCf09lcu14&d>he5145ao1UU^9q*ok@S;z0wO&WlzrN$d_$WLb+Ip zszHw!A=3YqSM#2ir;m@1SL3enD^>n;g*@xy&Wc#2`U zcB-zw^#!b~5d4^l+3q!Z+}AmB&o@g>QV6dV@S;$B3@W}+V#J6jNHuPT@b8GWa_R)R z-{5MhHH|TvN8>c5DaXG$U1JVY29@{$Y8rY>6g`>K&TMmpXqvsgi@31cmi|r7o5Z&$ z3#$Dz`{bj}2h(-^**4I2_Ka$3Y5<@KLfMx{@_HB5z8A3Mu)a?AJmA=pppl|P)Oeuh z&Ch;SB;VKmI}YMrzQM#hK7oCfX`OLplf%r@g@bROeY*pHA5I%RB0nE3Kru-)ef2Gul=gBi#VG2Bc+Cvj-Sl$ zp$1+w;6U1sOWfiSIC-T%?&V_3tTzMfN9pq^*aKl~dqWHmlxtHk{{V%so(crI50`6- zmE|8WNthx-WAUgoPYt!^;S|#-`+9^t2vt@j=cYmnXLsEyX;6gf z%L1`wX9)F;rAyMv;yFVJ)<@0xdhxThQ=Yhe3;je5QBr-@~3Vm`$(# z#spdxHWtOY-04V*AdLZm-)uwY5yqOcy^d7*wvcl~_6}^kU~YrzwL(ohigfW*WVlhE zLL?})%$q)5&kf@T^$**bYRggc=EDb&(xk#$Do`M5ZIoZ!q^+HR08!>r2h3 z*JQp~Sc%c`@lWFNKYeOYW{Sv8BCI0)yLMABXc2{kaChlCIc6eAkErf+m0t9ca2Im6 z5;(0b^z;Cu|3AaW_+uf9BtFXkvPxD>48-6U-Vygkb(4Q>;YpD!gOy#k2#=cnLWtA$dG7}M>mUN;C^j2x01OkK%qi|aZ^`>UQ_m_N@_&~SKLpC z7UR86po9im8`uB;sCvtQsJm``SR|!Gr9nzkL0V9ekQh2dy1To(m6BmZ5s;KnKtNKI zu2DfiKndxTE=l2C<8%J!Jn#L{8<_dU-g~X9R@5X6po^H2eDU%nU_2ZW62}1iZoP#u zQKJf{YokItG2n6mEg<$Jo)j5__|u<$^^A&h<3O~4u3NSEzMY*NNU+ds4b6wM@t75C zd?_Fd(vs6?B>8+++t*(Nk;Um{NZYKLqoOh@Ig@WweKvn1Zvk~;sGck!V6Y2E#mxbS z;s6#(;yw1HIbIxVC$`|9S$mb3UfoeV(aCpjHA;oz#BgPppiW?!DVCJ+N!(njn4T6D ztJY&=18(_8afX0P@Omxm2W#wHNPeB(z#7AaSR2Oz23EWigeJ-<-b*M33JMlWk@}xq zROTUr*}>IiDwW50gAFI$S)g%$UgWY9~qEfaa# z=_k40IHf$3Pf%a$_zkLyfk=hw8Z>}lsJKF*uA%}X5X(Sn{qq+t(N$Evl0w7?=M?tr zh)qiwgUd2pXJm%tRF&|7b(7*I>av%6P)bV5HH_wZt3E6OQdlmOJ_=OhaYT*z zkDZB?10H_y_gZcLJ}ZPgObbp;x%1O=^u1M0UX@fqtUSA1|T64{0uT5(rx2qUiIP>vI{-#2oyEXffUxz5Q6 zr2=u~uyOC4HdAc})e7GPK0^q+bTPLYU%%$KH4fUPs8ASt;CvSU_ARg1;r!hR-$LY( zkMz^Ree_rYG$bt7so;?9=wpO!ApvWuDoCCElk20PggFoyM@SP+Re9hlQ!0iLt2*WH znfqehVz!_U%sRuP7$-<;OxmUSBCBor{TY5-$u*YUUquSdgVK?g+%%B?aHU!yYdb@c2If#@8pc?|WrpvAE`&d~*6TVG$f+2$vQVIGUx z(9>;&9og3X!7a6lgZ}l^m6g`e-gF^_6#fu_CkAWdAZx@hy&bz_IcyGt1(&=KacCTu zB~q8JCg`Uvq_$Hh>!v>X19G1pNB$oEDR=HLCz%xvOgyOV*PxH@rK)p3&~9kkP;{~+ ztHUiB{b)(gHrYAkOZ#IjC)jJuu*o{LN=uR{`(dwpM|<7x4TT&X%;@RqUaMb>rkFu` zQkgIXr~`4m6}cQY1Y6r2)Y^AgP+30qyH)gRagl!anz0Z}tI?VsW=>p*$eFozUL2*H zQ>0G6K3wUmvFIec)c5cy%1MJKF~>lW16)& zBD}h)>Wy|0=_w{PfD20vLfVp}TNtWh*_bM#!zIar7D5Q@_bL2JUiT7dN5Y2?blSd}MTTF(^q z^k{_bpkOj{>!>T;%OzoFb%N%QGWOR%hp2pEhlqqg?soX&cUu0YGxMcGebBwTpYDX5 z3hgToyiHlFfpKs*ntv~P&URd?Mp~1l$vL&>&is|yq(Izbb|*qP`JO36AvZfiH9UT# zdfKB-C7vlP@dI85A}E|9L%2u6MRPfiJU}H&fhd$7ie89l=T85zrCmeUnm6>$bZT&r zGI-%3GqXzJt5=f%v>*LRT-{)}iE3$qWA*DioX;)U8R_XOtE=t!`1ib~3l*CQv6n!5 zS7-md3wm-GpCKoG&wocQMatOH))xO=Ni{UP9%Y#o)X`pYZ%{@-w08BL(-oe)1BkP=LnrQMeD7TGAx6Pg zd=A=M4oNoY@;Dvi>DG*;hm5hl68GZYA(eG3;Xzi4#X0Z%W;pN9m34cbE#~^_*!R|s zRZI2;gO)oh9tYw_og-iU2u9Qi#h4ofxs_lpIE>>iO@do?YhCrFa7xHgwnLP`R0xpq zhZ^5riV&Hr@|!k6Arlop$JUAjVvK)dySGY~*++qP{KX4&V2y){fy{1X>gqZ({dUs9 zqe-Y<@2cMr9v7Nd@<<(0~ln6E$U9Dk7BwM;hb z;bFSCq3rG95fQPw^i6^075K;L=;%mc8+VU|wi`r@M`>5+DaRG5Fqe2J8hIe{SXtf4 z-_F5#X}}?-ZL8 za~-L$VMNHwsPr2HI8&sAsMQh(X1^#DP)tn$OgofW0`6 zE~YwmLw20K^%m%}a@W8=9(`M-F%_yX8pG>$hPXs$hnB%TZQ`~5f`CIA-0D57hQZQ5xGE;*72a=N%wwnY>pNE zL8k{d?J4C{6^`oG`p)da0!KVjB$a+3nbgwry7{~w{aXwS_z}8-loX2pfdNh5+mM|8 z*la#GnaI8K%Jj$hfGc3Y2-hHq!`l{a8nq!Xs+w3C#7T)igWCaS=_|Kau8Q7YZ-FiP zi$=#btng%7Av}t1Y8sljq&X^VU=f})#@dBASxQ&!RN({qG%4~EV1=MB9t3aYHKxJV znsW%{8F?qve(s1gUOg@1IAobRzYN!rzlYARzn1nWr^;^|w?a8Q1*Xc$Sy^k~)9D5| zzq?6Z$3abu|26DjCr(7(_A8;lGwOE$^j%vDOaAYiY-qg|n)1-}wRF`a|0bO{F~h^4 zClM}yX5t)=H2bX61ssrwStc*1^dtmBwIw_wDc2g@I6L>q9=2H-)ZO6YYS_zwGZ=6W zg2|?Fl`kp)0Ml>V;IgEDD?$25sx~FkYK(q^VR?7w=2R6fg;AngeEQu$AD{rp7+Tm9 z0d>wq!bM4=g(*s^-gRSR7@NyQ^VFLb)|wvDay#qb?s|UnSz$Le1qVjd1gpsWHZ{|Q zh>mBU=nrsEP79*UMe}$!=cLh^LC|2gJxT-}39I4SaHh|+_bFNW(R&-|=FfL)Qm4<4 z!?(*@Yi*yu{rR*LSP?eG)sv$mT6+4qKPf3pk8Qe|AQ__S?IU71AOV^t-TAIA#qya+ zDkM`-57Q7)@Nr*e$KdOVtZfC!&Oy(!MgUy=9N2uy`^xvalA6iJmXt2NIPqS9dxq<1 z+c3FwVon~dkWI^t#Vzx%sdw#BDzto$lN5)e_Wg3Ko>Eym<#!-%264FuNuu2vJr)QT z+zaOC(Wz#?04(HlmwTh}^q&8dZ`bAY$tg7j72{p?$pw07isogm4f(;qPC*B$z~`IW z(wD$5bMYZK+@C%&)01mLYJ6f%0vCB>?caAT;r z8&_pk*u6qFfxN~599^w4?N7-yGv(33t;pA6tXCL)ih3S-Xg@lE2Vwegu52El%4%dhE7CPK z{6%6S$%J^hX|tNK!b(oEV&^Ze6_((geotQV5+h+mQl5r~rkexQ=A0j$0mJ|(PfUy& zFBfJHn&mOgPksE5mHBA)5hVt3zF2z($_=nlN&$P(L3!_(_5{dJv^WLJE2uMrT=;J$ zCF3afy}C=!$Y<~9$m@PNa?RWq_>4MBzi!dRPv7VPML#C}`|S=@S9n}e7hqA46$F!& zvhD@b{lCk(#rh0b6o{aizHH>g#&xu-9nZ7Dt^CI*XPhrHDl!F{~rnLs*0ZuB8(11P1YUFAjs`PR-9a<~~fUK)?hfo!P3`I~- z^v!9Oh*2@Sh`bo)vf65!Vq;^Qn(nz=Vn%;~b~@FY5ytC1vu2fHsdA0o_zSA6*nH=y zCPvL5C_6QLS|e})M2AM!D1Z9t-L<6aAYa>KYqI_yHYz~!IE$ESjXqR!m>_=#n6I|( ziSOT2UG<^;EV3G9)8ZIM$hK{aF{CGHd%SHoow1;oMh8z&&!LPM@-{Hj9oGl)V`0#- znOV*?d=wruPxS;JnVQe9+{pySAD5%?)HtlmoAUqrd0wU8b6T^;s{7xCmVg= z(R7m~CJI!Sn)DTaJP_LbKTwSm6b6sI?p^TNlRTamoT}m>H%y$5 zH`c(li*(gTaD2Ivf|xxlc&x}_?mMkFd$?o+P>BZuFT1O*lu&Me{XY2es^c3Pjv&({ zN}>=4Ff;t}c2H+UYE+?~-fJM^2}8-Fr+3Gh(xs{Hzqw2&BBR_3)1&v9m?yULzfXqf zWj6$>oTxFr)hXElfVU}>K(>fHe;-YGZ=%XUU0ppmIM@WO%wW~UbWnN~fk7HCx(6XF z^AS-v$~O&biQO}1mcps$%+DMp*y+=Rf7`;-LHd;<6bi(2?P)KdhJ|U(o~- z%*f>Ne=OKDF+EDg6rNsPy1e9(QrmwX^`no8vQd>6+ zSqF=6@$!Cw69cIA9qUQct35P)z;e7%bPp0X_eCL?UcR$({eS^|rpK`a?m(?pS5e`2 zO0yeh-f#_p85%ipMf2jNYp)+WJNEWU4P!z-=<+qy&o|4>0YL)pw!$K<_vWi*|jZU zUex&+hPo*?`SckB{43>K!QqD1r8BB1aIkCJz|q`^CK$V*wcwrlyFnSEETGbIhTcj> z#&474d5USs5$??%4E$c+6AGa4H6A%9w*J*UvHlyfK)GGZBz3`wz}>!fUdQ!)(aDmotu;^2RKZ|&rzPatlXjAe zJa}S6a-%T!S~@=nLZB|1=mF;7B}W6?;WClq7fE7q`ViPI`gaFclg!2#&;Cps>Z%s# zu&FXIzea*^;q|37KGK&Jjj+q}N^k%>slcVL%I+z6KPU)hK$kfVzkzhE>s{LqEZ_#Z zslw#JYrYT8qT`))IDg71WMv!blL`XtO}N6e)Pbfhh&S#=(*2%p?7{ZfKB(AJd{P+^oIDqV1r52t)=h@wJCf!W=NO z=?7mEag|LUW*zy&;3)PV3p)z+?2_661cw`tSqf5^iK4&Sw(;o>5| z21!GSWPPcTtuk?;qB7CZmHFcLCT@b=3D*4-lSI%j=<4bM!64JIO~J9cx;lr#Xe($_ zp1?H|sQxsTce%>d(B@*8i7YyFy2z1Dusdg%MooF*b4^0x1N7_QJPO^GnKn+G#Uf2f%Ip=f_DYNt_)Z6MnPPvg+=ZrZMv(iVwiB5`iPCwSd{<09k}`dp+D?f z>67MZaeJ^PvR9;Ez4w{9xcMoOIZZEgmt@Eda$4fFQ_awzkk}|E0(X;(mm0B) zqO?6i$*i2%G(ASwgURwyUx`+NCAa=duAtR(Sy~edH-dC#Htf1}$puv>OM7W25y7dx zrx(x$p+H!=6XTqtzVEvkh9wbX%)Y-wJx*{Mv_zoC_`~e$f&|4Peqr$bjf9vZ z*5`Ce45rX>Qd0VHHUor@qYM#o*+E};?#UO-ex(fZ$;OQb|U*awS%9oL3{08E#*UBsJ1!P#QA#&6!tD zk>+Stht_T}-l)kq<=jqlg+zcu4oxW)5p}(`(yKK{QkJMcWrS1c&#bjGdKoou4QJGH zFA;G{zrIM?)eh3e?_5@U>%`vIunOQ6(8AwcC%$pt*C5-?@tkz`KYmRh1;k zQRRmrvwi6Z>*qs&+eINESYr5pK9J{^B^TL0EasJ@K!AptfbNEiZFT zo2P3-`wX=RudlHeW1&0=yO3C5Shv=kaU=y`hjj2eFR}k!AHE?o!y;4?(6z@CrRflQ z0=-Bi3A&8Kw^>|%P>n;Jku9+BC!(({ys~ge47H(K^-^Xs>%NEs&CeX zfZz)R;sENxXfrOd&P*-omJj3O&IuIh5V)v#0gu`g-C$0#C$)@LL1R1Nr7E7%4LR1w#Rm&wVGMlj_@}IxY&$bLv5G zSHAxDp9lvU8{=}Jr9C=kb*=i!&5he7*B(nICLpl2vx98BDS@M%|1r!y+x@7nYW#$W z^b@T#@|M!iIN1{+e&?`Wq;N74@jb2u`2w6A_VW;o?`qgEs z>4vXyHEp5%ihl6wg%^K%rPso7qs{;KRn%MMK}EB)fD;bLlVq0VMvPR>qErj)|T7wrj^=bYRClf zc$BLHLo#bX^9LDB!>;Wmx2`STioe;BlAdwH_lhueX6o&~L+7(!?r%GG_uqiqst9VS zF@Pt>YaF{jAz!@$OONR-=0mKIhkpayD@GovLMkVakbrs~?kp%@akXs9V!;AcP{grq8(1(o`QVkb6_^7RU)kfSsU8#PILKYe<(EdN<2ONESS$~2O}{o zz%S2uANALL%a)oN8$0&CcQ^2lQN3I4?;r7zH6f4XLPMGI(w5>)q#K zN=odZSk)m$AHBK7b@OY`W`zsq(rc^d0$*(f4%>d9Pbru*Ce2%Vh#Wme$&oC=aI*_w z{`l)0SE#0^^C~`l`UI-t^5gwkv$A)jAa8{B+Y!{KKw=Gw!JU0dd>EGT;>8|}KCEAm z|2XvjV{LhK;gX2?Id_-hCGkkHn@=qts+p8Wd0Qi5EHnifAhHc_uK7( z@i>PhHVKA|^cVVVFa7bbV~^DqwXB{F^hpDlW4pAVXs8fVo@3p_cuK#tfSM$t+0p8+ zqmm`PnRR;nXWh`^M||L?{#@O6CdP}+0)Zu@;9>!zAJCOIjDE_TcfvMc@$u6qs1E?* zdYNVHr(N*Ufmq2;zun#nM2yY7ifMUW#94BxoSwid!Xbd~$7h`pL_3>n-Sp`n<4zQb z%-dZDgX8=UncejVru<%9qEktW&|LuGw+gj~mX=oQ4@{l)e>`1E>TdRtlJ^iz+TNSA z-Q|(kqE1_OC0+9v(!8Y9hO7HIZ7$>gC|%f*Qc4F(?oxREGifS z`o@%>)Q11a1KJJ@ki0RWU&C)foyLL%hQAw7cEV=r>3JS{*X^-GP#VL-ZvO-jn^vww zc3nVp2-XCX3z61)q4~FLvX9^_VVj5`FlM{BsJEIAadiQp=Z3c+n6LlJv0Ks zj1{wA@!FP(I)49;@}T6kd`K(tz+G)|Q|#MY@sOj@?NPok6bmhUhw2L>$HGPp9-Z*Q z0wM>wE0}xHIXqkg&dK*Sy$Cfm-8Ws=1G|UEr9@}5c5KN7p!^v^PdxE!xu{4x*#18! z5Ck$MgX$G@0udT|E{EG z$f#AA1eIVBBd1hT9Tr;y>@qMsgi%8 zoKAL0vs$0=6b4IUW9|k#L{8$=$m}9vStoecBGkf>6dA%*lOuGv;$k!Ugb){ z;A^IrO!CeV$@t!c^FA-Jf}W=omBU==WAQ#a30kV4AcRQE#}QG? ziW)4Dt!4r=#CVad{ai4u*45ehuSE8%yMbVQ)!3lB_3!_{4FFJwoTO&|XIr}I z8vOtM7qL7GB_-u&6gL$utx`xd^Yjl8|3z$~KYyOpRf1e=7}B0uCRz|x#${tis*(t2 zY8mEm9;^KrRRLyL%>(gOKEBpSD5pdOh4Irk z+xr=+ud5frqL$lzM(oalS=8rXH10U^*}?Pl+=VESrjWk|Tbj8^8p0~iaOy5X<@3DJ zhxk{XxJXseB>{Fe!FzAo&sLt*jhY0w3~vMU-K-m{+cCO2P+qZD+2=q0KkBOM0?w&d>{MbMHN-mlf9Kx$j;J#2!nyt z>|u3nwFBX7Rmvn=gjuOkD85tPpHt784aVP6`g$b->!e1yXT3*{BhdQN;^;jt9$$x0n? zlrbh+dLY?D=Yu7`HHVG)PJR2P{_x@O&!6>>m4n51${2JU<&#LqM>To>4X@MrvY=+eJGDzVuFn+GUo zp?uWfaw;Y|`S7%p`=PEbOx%t1_Vx61jB}h%q4t~VYrnP7_QbxaQKyXL9p{~r?d*U5 z#*Fho%D>bp!~)?rlJWBV@xl5XQ>pC66EM|7?FPK!yQV;*Z%a=HxLcp!#xyn-&?<;G z%QN|h!;cziX>tp1j~6*PyK$UJub+QD@nO2jeHlgESTFx&l8b%r;lF=-6^f;`(Ug&q z5fRY_&s-|xwC8VhuyM}@DoQ37zIas?IL4nheEOfin#odnxo=;p*y{D>(e>g!vMsoJ zp(kII6-*3%{B)Vk-z%1oYJuAviZ!^|^!~lj=?|zmcSnpR&n(f6kUQw!Il0ykNu{{7o2QSreBrvJeg zb(!cf^yPjk@p@-x2b@VTDEjE+#M3+R&dx=6KH?W%5j6k0U1(O~D`8;3)&-wI%0)%k zWjdPYBDA4~s7ED>#JSoAg>AS8cp3QMol7DOtp?UX7BwRz>9aPy_NGaKT&<+E)Cq*C zzkmNm_B#9f(O~YV6Lw*+_tX7QzV6H*f8%3LcpKXxWB3XBk#F%R%`s=R;<;7iq0jW` z(XL9?>|~=N$@uoFi@|m?Tf+nSUuPhZ*{THYz}@qqDWiCij)B1&h=wgGp=DtBdM2{d zL-f&g$^44II{n6IqAd z`6caJYggf&!4C3m|E0?2Q{Y28sgz9*bm+@%tp*Wh~Os(*vtzKZr16qK0;p z)Cq-~^}KEzSY2IOe0F8lA^wZ9HsO1>nrFJse83iFm32r`!3;sya(@g9-OLqX@Qf^=MUV4vX|eJXc`)@2-JTp`H_y>hM4`; zF^y#&a^gl5%Uyr7y_MYe%4+31k5r19CCg1N{goe*uJ0xGf=}SxY4lo&c)tO4lLGG% z#62x%axDLeK(4Q?k=MXvgDpj4(1<=V|3$BhHXA#*dzUxxanP|31j4*T)YjBkpT0R$ z4MS4nGiX)}c?NnL{^411fcA<*@3I72s#cPzkWN|wUzeu75*|_vc9}D z;?K&|5V5z>?&%DLG3{Nhp7j1YH1rfTPO?+nU`r;U5x8$? zcmiBVfh{!w4v6jg-!(vzy}k4BanZZ*8d{ji~IaGOqHi5)pY7_~qZ zR6CL(s=>K+HOjPG4Val!%t<~T5r4;A;m)}AnT_u#e(0gLb zD*JS@YC^(hOE|VW;wC?Twy-@V*AZyv3u@dlAW{$eT!oX9bI{-d(})Hp)SiFhgEETY z>1jgs)ie7q*vXMdV>>SYB{o5hFGa4;*<%JYG5g18eQhY#))mNYmE+R?K*zzB3fp}> zQEEt#9sLaV6chmvoj97DuAe#H?tSm@&+m6X=jVMpv^nD%$;M2otRBO&Z)}uYS;_Ii zKGpslXP?rc6Jk=cvtCVFFo{k zY5t2BTq$JR_jJTVK16>eXZK4Dpj%DJoMkttbyXT_wTC~PRfmu1+?9=VYAF|Q>LAY2 zmue=i$zT>|oyhq%9?LUgX?wI@c8x*TS}qZ)U2w521F$VGk2f&-z=C7-_wT%CcR&=U z;Q^`a_RbH<97hGKk+4Y+PYpuFM_w~=0r4upnc4t-we=yw$IqR_-2s7ap7Oo+unOL< zfuat9mk1wkZ|j-CCYR|i`t(lm+8&=AP(SH`U+btD4m<^5X7W?u>hki(;1k_6?8UUJ zd!D9)f94bDJjdsk;D*sp{CZbfnhAm-B$85Q{Nde&E4=+|e*goCA*!vcWGS=0QmA{; z#@f0y`2&CzaYuzQGrOn`h4hN*#PGcHZ;N zzQa}7Bz7Nbsj2JW4V8Ay*pTdbk)fGE#a}M7{=rN#%Kkt(=;9xzmzy^$913bwN+{$A zv`(RBgVb^L01}Oh(c&E1x0UTUFsL^wfym?pY5F@HX=`#GS%`FgWm#)qh$6PRlxx+n zznK$*eCi(%0kue2FTzqxf!zZ7W zV*<;EtHc6cO5gG0Exr75^5;7z=V?ZTwF}{l$(h#QdW*w7_ehG3{eMuJd%JMPmWh-@Vk)8Q)QXWq@O+l{FLSl9y^P#`GfB1G zbH!`po?ReB%e;B>M$Bd6TG{Jer@t4cSOk|*7nWl}4}SS(N&2cG5Ugd-3cg20N2iSF z>garZDWd$~L3B!izI<4%WA%@iE*~o9f=}#aWv6<#&qt=@?6HSgdV7oeMjzF+{^q+R zPWa=#L7{&f0#CnDOgmmq9MHG^IexfeE1HlBdc^En7y1JY0b8#fs zS6}5U3ReFXfjlSfYz}*BoCwn}V8TZXDfNJH>fb}aEMULb0MsyRou?)NmkFn}B&8}d zPN6)<{{Z)5D;!tu86ku2kCDD{rad1?ZL%nz0wf1mBbd$#3n@8kgUO4&SKAG+va&{0 zr>U7HxFdLREcwsN?Il^b_HV;3E!-KFUto_-*)soh(MIutC+=0E55#e{R9@ zHCPgSaoXAf|6Gl*&7SXlSI5Q0EvNwln{j8(GvcDce5;NxF67?;ksD~$|0<)6?%&4Y zi1bz%8q}=9NehysG4PA%ze8Z#LWUo5&yIs06g{Y~;?tZ1M0L4tD_NE~^Y4XGr`=Qr z@fC4H1dKQBLIKan&&&HMeg4KSQAaG*WV;jdd?#r&KV7@TW=;T>%nYP~-3K-=E?- zS_MUINBfYD1j$~f%T!l&xe69p+~a%W{SCCdn+7s$n+%C7!3;zPQDd-RC~79%51y5g zEFLo7x7xd_lsr%+Mz_(B))#0+A?ZMT>C(X9;PJw9Q-?biRxEH{L6)Z-2N{h(SRjXX zt3JuA_j20k^C;7ZvGrtua~uS^Q0xyI%7`&7=mZm8qoOj+eScRxlSNbT=B zw+O1y@{j5aaV)PsfA}|N!yuea{egJ^GYVOEw?pp1!&sd}pqQYR#B3`;A65Jb3c5p2 z4`HxMA~?ib6f{F~O_ZRj2<%i6(!ty^nuZgvf42%cKfhYmtG^z>!gz5Dn=$pvH79Y7 z6hE>!5WoSUb)c``(THnpYbzTGFlxr5cfVgSax(H{;T>B@OG_8jBwR!H)~6hzs&$c~ zZnL;HwaFx+V%YV6{-iwF|4IAf;OX_23+CzXGO}Kr?sg7Coj5o~S5eRfhNm_*ypWZx zK+dwn_;?73#p1BFq1?PGx4<``DJWrU4}2{Y9k$;rOd9)(c!GIEeJDQ26X&xh2=J{AqUkUoJtM8UMr2eJMemXL!Q{_B=rTCZMInLh% z-6%RCK|wHgjDRIcf0BkZiL4L*^c#>T5TP?^T}oP|TQMX_rLB&ic(A@htnHiKSM4CN zP2pwXK^w(UgcJ5C+=q!l{LpRC!0luj)pEr4CybYy8$@Ho6DTlt!N=j0w(&K8GNUig z^AHl1a+50}5pUXyHi$Q)@4#-8;>KZe&@Nw>V{f?=$~*-dJ{;=1m4>alv3ErvTvXPz zuchU_pai=lEOaQN*CEeA4GpEcJa0O@1YERtWJ%OA+v1Qva9Ey|+E|Jo z{U(hv!ZGa{yc(r&-HQLNvVOr&NO@R z08bme`|ni#clpnZ=ridcj1F9oKLKP8!0&hKKG@%XsHq96&|eF@JJ~_Ln24c^Bd=UR$<3-x7}Po)VWP+u7m*`{MMZ#O zFpgzLMd1vNL2(3yH(>vFgAsR@6Xo@|PlwOVKDl0!>Mrj-tj2n77mBO=X(7Cuf-Ej< z-uAcX-1S4J2kb3!3+%NX!e`qPgA-b_qjTPEZ|DFA85$nuRC#fYoSZ)+5$q@+W-vfw z>`@%~o@w{daf`2W7w4d=v#&%x$32VG-8tkn?EjMI*z)_s(Dx#BUM?;^0A7ivZoHMC zrKR=qNQ1>k-oEb@ChA{auiCU1!)Fw2aWMBiXPzySw?VDn0o9a%I@^XPaQX6;vt7@V zW@|&Kz7rq+n9)YEWJ^D;_Waut5-+O(9ohnV0TXiR($SQQ$I7MWjkqr-hvOBOV;CiF z=HT!fbh0PCVpHs#oT(#MFJIOWTNo9ZRazwAkU(<^+JJ%MgMt{_pKtd?V02kq~phlr%3kQ}_rw|n&S@~zY>k-aWwN(yM<=MR2_IZu&- zh`;5QnDf})6@6cxTf{|HeZ^x4!-?ySU1o*3Or*OPI5tCq=ronN{maj#@l+e*YU!Bo z1^@@l2CnU@st<;N6Wi(vC)&G0&<)S*g4zB0-`K65D^3%^5+`Nzi;H=_ium+fQ|D*Y zeA0e?;`TRG#oZ9B+he(}4?NAPmMUky!W!n1sWytRfuvXw&Y1^w!W@cEUz!=x`V4p_y--0MXINbRe=vS^vc~#Cjr*lmD{j-{ z;SgZoY7TOk)-`keXaq5MC9sP{kU8>2#j9~1%9Y5vJk;9m9~Q3B4l+@eJslnz zg7ZRm=_&5U;m9|Beo}FnU`>2dhNsp&S(u$f+1GT3iTswBY?TLoCEg7wDM;cAhe4BZ zr8gwp=1^hK@5+XT!kNUu(UJV<@ZOAJ^yby)YOrdv!DI_uEplW;`JvIDyMbO~Va5ey z63#7RGq)tf*zT;VzNbn(U#%oexsYn~bhOi2?a^~nZ6c{gQzp7TX!~b!i;KlW&w@JI zT43Am*Ytp_hXKR}cns9gP;p_x`iF+te?8EY=i+d!w@udA*^lDPv9g@{Wyf(oUx^G! z({!!}jR=Iz;VJ-c%Ri@JlMETGUKTWO!!C-&ja&7z=S`e-J}fp))~Yg{rF!Tnt0BmS z*>>XSVE!R=d#hgsWtf|fsXM==&ZLkA`ep~@CuRV>ks@6tEn8an6^$Mk9`1bIl{@p5 z8w)ni;r@OBVd2w>w-Vd{r(-HjXddgGR|EHdzs}Fc{zf-wz=lvPZQQg0v`~3M>c@6_Y)32IU@q#TL=WAz>Lewv|KRX?ob><-2>@qWw;bFI@!j&scLTAd!TAgm73JK)Y9dV!7MUwPmxp8l1QMy4&j);f|m>J~?yFrx$2g_}< zmavZ>@BFK|!E)gHdkE6~Q>y=XuKCYCQKNTDY<6!dHE{2C$v7;jDkk%U&d$Z;UctuJ2>-UnDz(}y5Vv|KrKy1&3T zgZhf6lM%#4+)RC>aI4@U3Q5B6YQyuTSd{q9HWca@I6^lqiJuX(Lpn|U1$Pw2DndP$ z3Cuq9Zt#S|joHhd-FZ&y^+Idoj^`cBUlM?hR)Z9urR*8w#<1euHY*jG;#OcM z?07I~yy6`B3V7_w_B3eN4ChZaO~R1-AI0*XO`Pz_qpR90UGZ^U#;Px=nOjDVj=azH zJ3GBCaVtSTMl#YvmU&A6BnHd>Xqea>9 zsAlKpyuG}@ei%((xychl5zEcE0nd2h^A&4rYnYiOSEvBqR46oKN$6t~i^9vvhqGnA zhP~-av85ic$9_JEqiv5J7p zKGo!ut=l3pU*5xvxelI4I(|Y7PKz}GJALFOuUdVa*iraocvact*B;xEPu)DO9z6i5 z1VuU%Z0`=R$N-I{7$1=heex13TCpgXtBiclU1;f^^EF|i+LFKQMlSNRu?ZMx&Zw9C zlE9I5H1;?C?>;Y%Su@0~F1@l&Eq8hJ{uZ&?Gc@a<<{W5JuUx*Ymfhu52wEjV+tC_F z>bwyc+*xMDV|%H{3T<@Hs1I3Qesg0=S(@8FZOWv>LJNVG|BDDF7(*(9-m84F1@e%i@VQ5qiFh=S90h+QK6@!b2)(3(a0zh?Aei#*!C6|JX2mvZID zX$OjhS~rNCyqm1>vW>U*QBC|8hC}9p8i0bb@94Ibw7-vIL$(aio^0q=GOcE8>2*;` zIN)3fPrI+VvGD?}3@1Po6(nXdYX*WSN_-OlR&WG*q(SC?KjQ2k9K9kHwPy*d2T~E65lW96a+8!YXs}t>pSB z8KKL*`;AYJ+Uej*x{rpS{mbynB3k1-FeWlVtw`y^7>w?WebwZB^N-s8EoRJyP~dwG z5X&;$FY`*so?r=~_PSo>QSmdj$@Lt{%IRHzUZgX##?}JCoAY8JJvAv4e_I$eBTOEXyjv0 z1%o(IfrIfLTpUYjlulFEjT84>-pcJB4&j-{6@6UFxCZP`P3yUGq;3 zkQIRGvbk_Q?^mLaLn}FWdPqPq{a(BdaCix|_p?1zW*EK-fF&ofZB2=7c`2YFpcX8# zdL6p0O0LNgEJ7G91Dlhz^#UZ^l4lvP>+V1aGVKN9r8&!RGcz-Dd_ax!g8a67-ymRc zZnu@8jsbAI;3zNC*<|5*B=Y3LjV|t^llMg9lD%?V4F!2l)j#l}ko5f+6bSCKs2W=M zw)gMLr^=7Y45D@6+)-lc;X$`^H<*YZrgR^lZNr2E;?Udfvr%i^d&;F_FLS#-YG7WK z`bK_zg_sv@_{~J#-uvK?VKrcPK>PtM$?cF>J^qBRuJ6WvTeRa9SJl-i7)8Po8)~Bv z7$-YAZX*FFjYN6n>;)wkJXYsvpi-V?V3pmqclJ>VVHVr1<+SZsqrv{+pr%LrvCjq6g@EOeGWvr#N)!^#xa4&5<)v8N zoo0Fs!97Fp&pk*Sszm_vEvw@B=HM_V3ba=|TwL`HXw*+C)UnYoo6LdXbrGBI+fBx= zq^JJETLxg%aD%npCq?8G`3)^Mw~E5TMa;m8kXSCp!jady&NpCQ`sLZhMD0{(c#bi} zpMs7Ag>_+fli%z7xu_JNLM53`@qC2ULuhib$h%|FWf~eGy2;wx{vwX9XCCYVm16~WY#Qyi95Xz~o-RQr2eMLkOwKhL5XF#N9V4yYH zL(ndZDGeAXDR(-XhLjJVk|H){B&0sY`3b5+A+d*h=#x@Ug%CO=BzZj#M2p$g>39-$ zm!P#Ngo(cSxo2~Y*NZ=bl31EI^Xa7O$|%{o@xKT|C0n7s_4QdubrJ1DvZh-m^bSQI zci?X1dLEQqNr9S4Ue8zJ24&VK1HA}tz`y8ULF-9Emk8{_weoqeM#k)(ZFe)rjlqw~ zSVqv>nk83!{h_+f`8VNnuOd&+VV5;2teECfxBjDrF;Z=|sqWN~G$P(Yqwx|4=jCj< zk0N9QW~xtnfe01+57^SDc$mK|`ZxU^slDR|`==p!IAQ*#t9>rZU_I z`76cG1M02C*HVn0UP7Gx*_+TQMH-5$_$O)Y3^_KsAqo#Eg96(<=6fg(IX5Ri(Q7Iv zCeL`JHK{;n|Lopnpu;2iOD}LJSh4u(po|lTUR^GTG!tZM{tvGVm@uddk0)}c8@|Ia z0mQ(Y-B!-nW;Uiv!khPMSSjlsvhF)|Hgxc>GqOgiDY0)P^BdKZAInkSH1jEi5J~3D zW0+YWH}s*Qq17K`kayE&AV;y`^QQ``_e6wwW_uG<((}uzPXZ%;IhrZk~X>IL-j@ zn`-0=U%NRr0u4Dp%m@QsO~JiTB}Zwr(7W?+?JAtfy7+tCv}gQzX=)ZfpLa=CB2OCl zrJZi!{kejg7z&}3K~u&JDUvXr0_^F;V0w)_fnJPsEL~!J_=i8YvzpouZ_}NInx&m1 z$=TeT&;w9M^lKj$+pZ!xl*L2n`^3LW0Qh%%7^;(8)47F(Xs#(R`(^%x{Upu+wC>CC zpj7+V`W`pWgR*3TIIrWvlfAr5rt;l^2HN(!KO9Es@Ak&AlwHv!RCQf@R4clb#OBk` zlBCdQHkKUy{YkT#Q6dcBW|L2_8!SN2bywMFBA#*`fzBXK8-|+4+w@VYp}~zTjcD9mPN;1H+?AkF&DoD`k6k zO{O@?aL@ca@KjDrb6@1H{I1CD0tqc7o(1e`*!F3`E)m{fT1 zm%s4R@yb+|=<2MX!f}E)=B!cq75qDq(#1eTB!!rod1i4jCJAj#E}Dr{{Fn(zF#pTK zMDtrgY(zndHf3>vzQ8%jPA9@cND$ravs^%e(k19JIWune3DWrOIcRX0mxjaeuwqD- zz2$=0kJjqV;3w&XCfI311PhQ{Zw~@j0L?JG8Yr^?AnF8bDx?PS*upF~)#c3B0KN+M z1bxgLB>Ow@JPZi}9!1A5Q5W04WJsfZwI91)4boKJWRT@;z~ejm1EcB6Ev7^Jo8*(8 za?w7gv4LEM8W2G^Jr%uu^E@l*d#MTZ5Mx#dG-!ct>v}koJ3u{QRACm5L?R)ChrSy^ z=pevO9q@v=xw)D6-*Yyk-Q`R;!x3m~tRmDyhT4rnN%5|oK0Lr%qqX0HZ(XW5pO=Uf_vUIeO3B}Z-! zM@f~kre8$~0HtJ24-qs6FMm`vl}9Wc0;YOxPAyCF;Qg)0?4WAmT=kEx=Lk$cq9^!T z-Okamomogo2%@nIN=h)vOQ7i=kqnO-ZB$pqi!5T#`vY=Z4M5IJG#NA@bDI zncUPPBYOT!Q9SM@yvlFF7ii!_l0zNS#4U~_5^OZ{6oR)*(L>0E{T@eISwIbi6Oryx z2hl98YFO{?7mdc1CT#8B)(;cG-;3c(z8ChsebkXlj`Vuj==<2#REOjuzcC@=9?YGx zFI(2e>_;%ui{Tr%-N_m%@{TSQAd8N4xkvf8FToaoUX57qN0d|P{&?Nh*}1a3y!U&U zh$$$6;VCvdqHEL@{!98{RB+iwI%8sZ;Sr`W$(e_~Bm&v~e)8!EJgK|kdg$y=Or>-j zUlf_|__cIxn|5}-sd38tS-ZW?l|B--+w_+gH|PXt3dd6ll}2zr05S&X&o8J~0&WPu z_ig4OvHX5L&zoU5H$r3jQhf1ip#phk-7Hv@Vb*}3L60ADxW!^$#(v}K8vg7MgyrBL z7e3u7-}>uY)a%wj8y$hn74{IaIa+^M$rO0y*@gE<;cR-0jye|;=9Eo~YY1x1E54v> z=^z=X>HG{{?D1xQ*;Bqk)C7Q?E6L3gxh8s%(Oul>4km8=I3-7Z^5wjwE*F9{lG1`GAreYTBi$e&-5?;+ASf*$BGM&|AOeyi-Q6Kw zcdmWz_uX@U?&sMoYMvQ59fKl9KSsZKoz};VL~Vm~tHh|VT6|Ze zd?!Lg)f8k4FkAt8vXdjOy?O^A&=M{darbzs*^uBY|8K!!g2M;9P^QAIVPOU1abFsT z`xZh;pP0Ow*e_HSDg0R;sGU?X~YvJg{S`?G*-8x;W_k0tO3+P|yl zI#j9tO2Fos@b?T!ZRb6Tg^jj(8hS!e?rQm&L{b#Y;)(T?m8XcPC-;!Tph}_dA83!} ze}F-yF31Y%rsaX_F$5#RIjxea~+pn>iiW_O~kIdX6QlpSQl^8Zyif>rLW&5To=iLl$>t0 zg|nHiIp!d(R2`Zwk0A0_L6OYVBEPkg)XabIBW@;*{%W;-a4s#2- zNnga+bY{HB*iJ!*H>ewdq&;NP&!lk-x(4T(tI+{_g{BQA{sUJS%O9B&MZ}Rr7}M@H z-oC~b_D;iB9I1GC*K=U2Ez``BNx^0#q>&nuApugFf`=P;txsTYr1V{GKd>SKy~u>TG%#@hflR{XyV4ht<0ah%Ts0tJ;D^^-J-50F-+{KCp6nKo{Z!T4nTvZS z2<%VL_Gl6$h9_=PyO24pw;9u0BL3nDH_~|=ZBqTi$db}lmHs{@D=YJ;>kFD)84zhn zFQd)uJ{e5p!E;8a*afICDk9@8tMB73G!|=RIBWo7R(MrX%DH@UTmSh7)v)tK$3uGw z%r^noFg7+usxGC3hCReW%u>$gOPN!{I5II_k%ro8j)9figA4$28+x zTw*vW_n#BE-vG>o4w^;cgNGl-eZ`wN7-X!<(kq-$`qCzBLduvf9<9XdguVP+xh`Qf zMFTpsXw5PN0iqktLIum?>>s=MK6s`YJbTvOu{Va2AP&0f^f(=6XrMA{p1k#rnH>AY zvlro~j$`%w&#LM>f`Yw8rKtb#@dx$!IXMSyL{h`r&s$0zRyrq|tea_|AWnTh!=381 zK0C{6+0VEZ@}(+!coP4GWx1JPUjCiE>DcF4-W*!l8QnNFuD~G`)dAgB0cfO9lE;#+ zl~o}KY(ITsb=`PuGxsSaEsf*u-H&3OJ?E})?kEx;0JE_=8FhHPIfrT-^Q-q;U^iXEPR30^6pr3GfMFy=>3A?rlzJ8 zYyzaNFXCJB1QOz8DB|3g%Fu62GHUW$k~&%UOX+s;x8^xu!ytTrfm#mC+axC>d~?0g zf!ZkuP7CRARQb}XQTFjxWtRi&syTMI_oOp6c?&vHQc|D{4*U@q8Xmo%6qYe_l~@Kw zejp%tumP`a5m@c0tKIE)5f&8W;s#HVm>m!pH`L!P_c`wF?uObT7|-tJ|0u9DHRWx~ zA0N~z-{^#P?;`^9n6mD|$*Enzi7^IXwyYtm9?&IS7E<%Z2}$1(ZsSKQhsnD2`@>%e z%jC5`q$y-s3xxvK%0o%Iv%unoODEVWb)!^*O}rI~U-V*P<{|V1{ErOzW|o)DSpb!R zIJ6=zRaX&R@uN?UVpsCQ4jh~(c;2fR*5bcw<6efD&Je0!&Qkk69UFSeK>qnZwmlHL0uFOLqE2F?_kFJfIxemTfc5|vNrzpI8f|Q zQ+QEzi{YG>=#+n&Zg?P4a~^l%)NX`!kpSNR11v|%qCKzYpnYsF-vqOWX$TK#H=hX! z34z&Q@b15X%jBEJzV?*KTv`Ft(fwBF|AT@7m6GXvk1))*3`5`4{;u{q2sE*cU!iHV z{;}(bt}ny9+RClu6TR-7PHp~^aOLN%`|OSF%XcsF=Pi=$IQgn_&4|lFHaqO`TeUlH zoezF`e3xA*@qNnH4|0`hV9t&M-z>!-b!$Fi?7c4}ABUHi@ty)F%`&LHXBNAg%S zV!(;*D~XYjFDkghXc^0q2*;G)8f0sMj$C zuRMM54BS!gqGC-KzDc~gbxeSX7?z)ZvK*FFHB=|Y^|+UkgCi({I!h)?2ONf?YBuP> zDbAT%#F25{`oW;pc(p@}#yZ+M+pbyMIwFiP#}Bxe&szwDn(ErRw%!IF|^!9{n;z3T;JC(azEzpudwvt)ZU`*GDAZ61p!e$B7nibwk! z3W@L|OqUtotBX@er(sOefIPY?AhoK;+`()E%y6Iv2(?sQT}6s4K4#!%SD?PI@5suh zY+2v*2-eZspkCv#5b8MT06tL|E7<`X2D6Oh%)xR_m7h$ytN#wr-6Z*DKFy-uw?0f(TJCMAoKYm zPN%ZFHHdGwk)NNR<1R`$HA{e-^>#C8hdedza+m4wZ4zzsWVO`me@I`_UHXIleM>_L zaD)KmL``pFmW(mu2#oQ47K$s7;8mvK#d8YN9rD)*7o-2^!0*R4 zOq5znJZutUMElVx+Ubg($G|x6E+Wzj3o|A<)`aOy0e4g zXBZQDz(^6ETD}@73 z?06NI|5l@eGCp$=%&u4xdN8sVfx2aKPD)|Zp=MP4=o+HiCHOhkwkS6%YwwR;VhRJy zR)Jps3@o!uRAvH8PWLA|U%rjGvK9I;JCR5AV&k69BdnQ7-~{Gh-xlcujs5(QwK0W4 zu~LK2xsZ1u$oYtMzRb-B=#Y3ozXO5s?LDJ=yqkN=dD_Jev-!Amr`37NGDQK1*MK4)=rd4CxA0|0n z_Uz{2#i(<{XBfe|c>|Ql(YOs?RQH2%$sE@NED>#gs-R@fG;{r}zU5uuew4_==2<#p z3|CmaQ{RB1Z{i+(!sL(@$%C$?%C_p{d)h=liL)1>YGHOx3XgL-?p(in<;0V~b@)o0 z7w@9`ho-5rGX60<+9QGo+Un6!JOgl<+buzRnAlz7#^0h zP5ikyes{bm@2F?vU8V*a*cshe*aNzHM?E|%Kn>K!_orKq> z*F2if-;9_(W@`oe(5~J~oFW$~$!9+l{Sqe5E=bqKI&PzWni#`mKTWNuv^v$1NWWPn zB1myTYR4s0W({tcf#YzwvZVI`W6Mvgs|v5^`S z4j}CVq~qxy>v@KmmKM~jA@C&mysp(H-yw!h*Y3%LS{P`&@#x`0q`^Bt`L1%aq;Plm zJ4P&0nQW#0WzxyV0HeL1-Q6%9MOK;)q|xK&&!6k*(Zs3%nFshG&8c^q$5}rlQ{1@Xvnnr@y8(Avt6|-i2F+qclOo4yRt%- z(Xp7L6WGl`Qlm|NeoKaNVK#sIwvZJHOYDf>>3-h(_tf_M))4Jk(~i84=UIM!e@jSr zpV7n_fk&f)>*(wZHWT_FGHBg}IcC|gAfI6$OO=U)^bWil5dw5puGnB98S3QQ*cg6jBqpHWve zkHPm_0a1~LkL@>>tz`#aGQ)yWl^L6k*^!M7|UugWEUq5~T6-iXCZbnyyHR~S9 z;#&#sU7f&hiwXX~1^)c`6U4Bz#)g*{7XX$2J3J)8LvL$sMV>?CsIiR1SdgeitHV!P zhK36vR^gyrf~e!kz{fN=N-@vNQztt;735JXqsUx_1mQIimAJIKdAP7SIe%fCJR(%V zkE5I*{YKKudJ??2X;*gV+QN0S&0gH^g^>miHU2K0K>E<+Q;+OPd=R6kmYpfcrpLwa z^WPWiX=6WC(%#=P%7!)uf-hg_^HFJd%Ov#h@+u8Uq?(MF&fCHZuR6kR z=6?cF*4AcV5*xyg;%K=VnrN6rfr7jbTq92hM1qja29{!|OhHwRFRoKuOQ6 z9T~X{HC`LbeA zg=S`F^CRs$EB*^~wVy;*6_TSC*3%6Jaw-J(mC?<5GC4GZ({M zBM8*B^^<;To2d%Kg3l6?1UQB=~8IwZcp#5+!-dkbY8UAVokWsuROuV#mpbT!8svzm?x~|9m;X$QYzL5Tz<&vP4*$ zCj{Xk1_?b74g)?E>ZbW>6x-X|FnJ6`yF$(^eNI2KB8~Kl`RpK;i6kpjRatAil(A{C zIw?6z-=FoG?{`<+8XwOdG(j&ZR=m03J%c?oqKQ6XjnUlzChx$Qh>3~GiK^{uX^>k7 zFLL&R8FrQ#D-lNOm&|+w+ZyQGOBv!**lJ6<`P1Wy0zj+{BLOhc3cxk!q!iavXr&Xx zO*`;|<7yY3T~YpZ4K-ZL{p-qHl)uA@-%vwcyr@FxLh6jD?Rb+#4RF50$$1bqk>b%w z3IDXPl+|db(}1>w{n!IC;izd=MY-H2wy^v>nk59#3z5zIlFj(WDtb9p`Z=! zzSj4;0#OpPHL&MMCnXb;K?R8BN{ZD=2njEks%`rpbf=9g<11uM4}Ght zFdIzVy0Kh0T^dkPDVR=iZjG_g2*K*#wqfGK`4EV-7?phExN;0dWH&!iW#IiWPcAUd zukIUK)*_K8J8b3Mi+$FSTZq4oW=AfJFzWy6l@!%SFvx$r`^{E10ix4hz!J>qDl@vO zhHAMp-i!~7lYuduYY%gw3nJgl;$r$`YXL#Q5K@>y!ln0X+zI$H z#=j>b>c|dGskm=3LTHQ$OjC$Tv7*7s`lTIT0L=^`B-`-|DJnyQ)ojV7Q-)FQV)?_A zyhenXJ|{e=ul|nmAXc;JvHK~-hqQs;_N>gpEctAHyFZ>st0+s~Zf+iJ|LIlzz4qt-ohz3nM-*$r6xWp&Ef%Y}rX{R9J*K2#|FGiwJEtyX33hddNbs!Z2LrrPW3b@k%nB&-6AW>vZtp?MpaJ!A7JK_>mo*{8Vcm-(@ zS#f@D`e7PnB903a0nn(xHwmNJSXhcDs5gsv74S`C{%^(4bY^ChJ8@>fh6wVxjLC@z zErv3Eo{>bbmC*)Rnf@IZ4l8x?b|QwtqRo0EzLsh}_l^&4{GSA6B=WpAk>xc5`Si4t z5Er0kZ)ZnP36uoQJpbI*R&iz~FM#yz9-r@S|7cPzHAxWaZ?j@zwnI^@6Z84i>k+Te z5FnDiZh1U2SFzI>HaL=WsUb9T?Ehu#R`TC!OPe3TN`kF_UBJzB_jw2pryUoYBTRES zn!ueKp3Ew$1m2x(w=WEv-u~9Pfs1Q3Uaw1AdbwNno0an#n2LZHmb(Zib|8# z)Ohxi8ZA+{y&=2=8(Yb_1!n^CR?C+pI;E?z;{3S@OPe#^#bw}L=MHY+7!FGE{|41u#67W=`GH$mFjS0BCWz+Ouyy&0cjN9NBQ6{{(>9U#divyo1D%42X%)udsP0IU(H+>pd z1ma4hG-dDE%a6C4mJvv7LZCdA{JnfbDxreET19`u={Bw%m%!NP`sEoqhL<#M(N#$| zw*P=aHW#cQK#94s@<V$}1-eU875EAFJny8G~*+>UfUH{;qx|S9Z9-isW^V@{TYd~2Y+{%CccrN;o zQ=a%4=BjA=lOY3cE9B%@Xy{cRi&jfDo+FBIv4fRWL}Tjg>@3P(*P1dG>xj1WdlrNf zMZPueic1vbn?`cz3-3#~-xbCp9VV!dqn6>i9uGZ`c{-U=qk(bE1bQs2wNy0~s2-vo z1FR!t8F{J>28QaI)*i3I0&BM`Z>{52WKJI*tN2@0ziv6B=lfnznwcs6=+Pq`9i4RI z@;+krQ>dq5$Isb%9c{`CLw-2@hjO{3!7p&t?9~O=q(6Jd7k=vF#&5^vg7|xn!Ao$N z#Ey$=66{sExQdb1TDlb;m%95Lc4Pn5Mjh$z5(=P$f$4>-((uEdhK)vg(SZ(>vnaoH zGlGTcYiby+yM!WZ?yR*Nmo*q=Q070o+Cihbl@=UkI+Cz8Sxcs}KQ7p_Wyt?gx6YOQ ztYteWm{)Eb<@;pwK{v>L${jYJOBfR;{JWu6zYyC1rT3DhvQfwohavA)=YyKj)ptCC zz@>txCo)!UZEbjNE^`qS|ABx7JXZKU2&EiJr5QJaebzBdU=TYby?dAT2}+0r+5?h! zT!$X%E@*$HyXXgwNh&LL=O+T-48nXb#6c^X$un3GBUm&ec^6;mM!oMf<66lJ$Gn;$ zhCipjS zVH0Lq*VEuXwAovn2!L@F2acO@C_en1X?zX8JgzSb*(E;Q%p2f|ysCnGRhmv(Q8$Bg zv@@h296d%Q!Vn}kvvwf!`FOna42?NgGkWHR)%CKuzv2A(3+=E+Gu(`Xv8hf_P`{E8 zV}MS*m+8bU;J*LAiblv+`#uD!$!|`MR&I(Q_X={a^E@zbsryTss}o(B5f!*8`CDo9 zcW;3NPGf^myw;2U-}awAwN19aSadCMz=KT>L2jQ9iRi^o=-k{7*JGXYT_KUgqa3P= z#dA3fNv0+1FTG|I%m3U@D+_zu0Rr^Szcrb+9QG$8zDb^>!9WpLyScIzo& zef?`e9F+*~Z@?5U!6#q{j;NPK?qeCg_%nKzJi90iKktsysGoVn_}^baUL`rEPe;k1 zMiHoi2E03#!zpx1-WlN)ECr=E>_#lZ;0HWx7hs((#2Cr%0hWnQ#+QXT*rio*AyI={=v!cyZ`>L-|boj z#;UOU+GwPJJd`736G5PPe?w-*2*cCXdtCdNCy?5P0u%8Nnw*j9;d+|m^c|NuROGVJ zW6W@?V!s<7!49i+T~miD7JN6BACaZU;hY$0f!b+j>i6%rJ1(*6umbdN6eBym?A6L{ zt(;4yz(-*Jua8(B;o;!82lG7f@%0{i%x$u`Duw*JE!@x*cgVqMUOIn~OCl3p89YB2 zr=V5Ek+Si+6Kmgx`#W+Qj0gR<`s&kXFvkTgZ9;N#6KH$m7jRV8?lsr-r3pXP)I0)- zzjsFDFjGOPkSg3niw%0yFT%=woNey>uNAHERZ!?kVAlX_3r5EWuu0MflfygYpwx$- zb!94ubyw_GjCR;eyhpP@z1DYJyBh_++nVUVqlQyVL{3QwjGPR7$lt$|5OmJ&Lvf4j z$%p%u5Q3of^~D_g88|Z07wSh($Gm6I%gjw*7|g&9KY0YtOyz$If*L$>hy3PE;J)v2 zaGal;ZpXk<+%-}b06G~HGX#WdyFT25ftm93rxml8|L4VorKm*v|JIXO*H8`&Qjl&x z0+zdAg+H#_!u}jRDgxSjsL?JiE>2);NUO3K`db;k`g>8okYn5Sbq_uS^M8Lx6Mt=O zE$AbFXL-H9i2FQiHsN#rfA=)@rHHKm4VmcgD4%>T8_0~tiw!)G{|21@{o~j7%F4_A zAcGP{8-s&`!wnH4dDLAH*UV=k2jC>V_`7uv0z%^9pAtsn!cyNbhkQBPqjy{i_5auQ z>_0P%1Luiq2SY&DARCoW2_yeD;i6OM=g-!vs>CxZ>d7SV0dP4sS1{n;RDUY<|69uI zyWqP4c&P0__7AwUew=!>ut(r}rVoHQCu}SDi(lQ8iKZ~)(ezPM^-n##)vWgh`}e#-HaU6co1%Zu^E`-RHg?5gj~#_~N)i{0!mDVCeD z_0%=r6mW5KjqUxE%zr$2tW9-dbnQEz{w^)F0CR9?2q`@QdMG>`XOaZ2ZJ=zTiy$== z+8)}kfbv$u_1E^NwH^j&+;w!QzP|82QD3Ovyv8|FE#mV!fX!?y6<+Wc#dAY^Nanto`%CYs{#qS&>5WP${tRN{SDWy#t%(544LmOSlYBOKB=uL|9CnIFuIjZ zmYQ#q0|5hPeDFFbe_)gf(z^G70tqMU9~#mCNv_|uJGByZ_&Ab6Sns=*EBZHt6b$`I(Yxww(@w96F{BNhwCkoEUaeF%!=`IM-Lq*irnM}7r z{VPoY;v%`>H$=yc5}Xp6m$}9aD~Osq-@k*mkdG-3+nB_S95aR3^z@62>x)OSvMdnf ze6AG}9uBGLMu&*oMe?iLlJz-&#{~Qu9UFUov<25;Hk&O2Lk1`vw2-S-dHB%1+o}E? zZ|&!8BDVnDZ`TCQowfCiXd82Lc|E-J`>$?T z!#3~z_-aKv8rTv2zTXZn#klTre5}&UU4&<~0wkWE!pHZX@w<|TkC&Ht1HEUk>8{Nj zjiU%_&IY1BfHgkynTEm;t4bHR9bp-R1|hvl(_7?d3s4w8KWoMR;6okGhTwLX&7Yj) zu@S;L*48Pz4Fnl>o54u* zfkf2%0%L$^c1g`)pMd|9{PU*kO;r$nFkCOeKn7=}-cNmTXt8Q`=M_v#HEIGbjf~9QA5NX54dlFPo zd_&a=FnQ3IcGifjk4f(C5kmH;kZp!4s5PM%2B#PYhjpuGnOi_{%be@#=?#sbfqX-? z-nb15^m)nF81BQsl!+jAm5_^AN)J&tHSF9*X#DO~zUK3ZvLe2t^Bpj^DL= z7kwM2rKJTVYeiZ+4vN3#e0G_yp8WFJ^ZmC#kKBg8do!)*&EA3 zrLdqvd&Z?a?uSUz@lz|CIdmQ0bCDN}zcZ6;=)V5!9X(RrucZPTG}r|&sIL6@alvWH zb#hOSG8hf~$2=7}jNbeY6Kocq`kQ?A2A5&^4FyML#M6upd?J`Qy-|tO0B2Xpi$DyD z@!O;W_5Y}?;p%1St_6u||2Q8cDCv8)yEB`J{{bA;V#3@&fLU~AeY)=p zQ>CP*D;CESO&6CIVe_-T=Y}#W{gjuN?Nn+?%+x|s-yYs_SiQ`=`T7C(6E9IMl|n!! z=M(J!nG~K@lX@CPWLDgS@D>ru(jw-uyP8C|7RF46m>BRH8SL-(FHvxIhQ9MBeIxY@ za6d&_!y1=!^_fLQdO-2lyBDtLde^B{nMFiJ^^BFLU=zbB#14TtQ9Lz0iOGV!{|kzG ze^hJ`M@HcpU6o4(=vVQ1AT`CGeR%yHB>l;O!x0opDluF zESxsvBs_tZo?bwUFSKJh=2|;`Y{&k zR5<|=1=m4#3R)3_+BixI_PFjqI^x?*crsQHMy1iY&ZSYyt7^-EYZw2z{7|UyE#!!q^3teTfq1K z!m`DFxGJCCqN5>bJa@1VUReqFL60ipo;H5JQE79*MXVUggoQ6)G>acZ*sc_*U~PRD zxR&x~s;bfnQ9ofUr!A2X#8%&fDfeAwCl*}P#m4)rEQAmAg(}+?BcFDgp{Zmg_7E^W zSd9mCm#{3i_+=dQG+lq%RHGl}{dk@HB$4ZIr_=9J4VHfP4%GL^IYFj7OJVoQGi{1X zgGiUF$!29)KIv*`902~wd%@0r>Tr!Og-^wgaRm;FyRwijH*3*<`xL!qGw#9e0hHEg zriNqHTFW3#S>z#Usm(Y&qTtYsy2Zx~ie!1~U0pb{U#^ayGwqGs5jJ-U=m37tf6u^| zhHSu*egA8E?wt>Bh$d7N64C;cXhx5Ahtl)fp7JnTZ?%@>(1avG8Gg&^Xl-bvJ4{Xx zZDADzyKTFtP+e41^4<;OiG>Fi_LTx+z4)m=GJB@qO0 zLg{f(w?}I_KLN{KOSJLCbz4pv#T? zAs}QfSN~SrSl)A&uB19TNvz z=(`p7zFxEMDdd~9e%3Ha5o$II;qeSE{kcPIk;8Or0ywq`@OJ7?92kL-lUJ+#Fr)74 zalCzC6()#gm<|&TZO!5$DX;4Uz4!9LfThGhf`?~)Wh#R{#C8%#oy5h$z{%)&G_>(K zQ+`^dgPn+GVnl|Y3(el37yW|8%m=Tnux*2j1p_v{^$ic(0$SRZyV=Oq1~y-%3vOUY zmBTD*l&CcEG*X(d3Sac2XHuM|2_C!T0(PpCD8cZ%2Wkgc@%uuFQqunIvvd)OBfXQQ) zigYvj@~awhRg#AlxP0)Ivov;tWjH$n8&0^ zkOX*B3I{D3Z{LtxHs9UHLBGH{v2Yw18ynv1o^@%i4)S`GJJkH#s@Wfv*V?}n-OdgS zaX}Obz5Z9jd;`NHtF@N#%FHB?)enyNQXCCyYwHSZms$BJHHbM4M{Yc zcrlWKXkqzmQFynA^K31^Xp0gw`v5&eNlRfiVqzT=u?$ zyWL9xl~k;Sc#Ea{$LGx;)h0h%lnKHDHlmTV?el%c2hsiR#e3Dn7C}OjdoFMGrR=h5 z{A|;Xwq`kae(M^wRFlcsllkHh5mi@KZb7Wo-XiS6-D}_=bFL`8yuAAFoze{arZha< zf-VJ=ZM$9|cp((JPsyNA*@zcZzhppB)DZREoh3@x)7NgcGBIf7@1N#44dz;^)rx?e zz83?YDS$67^}aGUm}%=~iE3_c2Kfnu)qd%~mN(po{t|W}GQgk$?V-2P;#a7b!8>Ji zv%a9KQR$S58zIj_@1Ir z#zBUVQhB2)VVvn>^~Bx@+BhmwGBR|<5fC@AfdFq1?m8z?CBxP@hd$1pwT-ebslB=V z0_$UP;?Cb6bm^%U;w9&xdw72VduCHm_}JOm$z^b=^LIuVg8+af^`iw-hayJ%9-iVG z`MP2U%R?)?hbNgJmYV|U5;Y|`PUDB!xjAD?%ai#Z*A^!6h50CBT7(7PX#_72qvtUD z6Gw`kJhlI(n+%_bzRGWpOi;mmd(RurKSDfytxte_TZq9OT)~} z3}RX{kChY@vWlO;NCY_I*q84F2BLxT5zJ8rrY+{t+fE-w-jv~CdhHw*N+1|WieIwr zU-H?V^ROL++j9V-Q$Nj2usv)gv-WFph}PUEPl(I-^xew#vO5!xqdamSlqV12{?hFN z=R>4oZ4e}JGF{>6c}U?2i16%9B=&S}9Nu?@Bi-?m+%OUs195P!eF~Qf0kuF1*ac*3 z=9;}FYm-FJl8-BATFzx1UkKWw+NsTJ%=>65eA6Qa`ZJiSD)!Zko1MS~w5&b7t=Vz( zn{od_tRFXUq~G14H2Nt8_BTRiKZzm7<0LiLy!WF-+QprBBQa7}k9}{Rx;dB7qx^Z^ zsMWWkSB!Bma>uR|`P+{Fc^ls|8)j6D$N z-@O7g_alc_&4uF6YB{^__GA?&ayrsI;-H$8WTW~}9<1s75>zWp>}oO!%KEg)#E6nQ zo42a_y*Cg~cgKHuWq5*HKan;h-NL*F#Mp|HR4Ke~YT$t!;inVs_6 zZ`=8E3TT6*W*&`+T4C=BCunwKqbN7m%*Y7E)M9)bgl&-i0Z;l%bMwIwR#sME9_EG4 zLmMA}w5~3;Gp4YuOG!xyhOlriW5Tc{=~T+BgCirI9->9r+&+{I-uFxN#>q1a!(ph1 zbI|%flAX2E-lx>S2#VzRzZT-5d6$l;fCon%q>xCA2KJ8#BGTxU{(5yFsP|R(2lXB~ zDt>h+6ei4Dyf)oYYQI{+=3i^9?CW36Z%Fi=`|kWzp7FW-IwP3e?K0ixwSAm^&e?YZR@b^IyMUyG9CuN7|iAO-(ACLFC*faN{W_E9H1k=guvS%0}`I39OaP(T(;iZBpJ? z{n?~#!$~RM(|z#n%b|yE34Z8S+XJJ+hS#L}mzS3y4S`{s+q}0a_Ze}uvtw7?gccOQ zqxF5mTzF*Bjb8=s-G*j&s_q+$&rX)TaLO?BBP2{D^{Ni2ttMh*#c63^X;)Mnu7U7w z@I8*Ay4QF=5ciR`cFOzr;@zYKDRiVEDI~K5fS66qo!hR1Qm4h6} zU%%w{oNf5h0{D2qQp6AYlXy2;SYhQVi zSD2`3va{vE=FadMI4Z-7)6Q30XT0iWR8|v5j-d(F4UbU0-u3fuPWVOUJ@RFDL{G;F z$0wTbCuVNCl6J^X6m+-h1z{2@dw=@$=~c{~+jSrG1hA&)({JJ}bXk@NhVR|}>{ji~ zYh5kQU2V?srTRSkOK1g!X72}e(&Pzg%d>8*S33pVpi+SQf4L`BLq|dCQo?hv!fJ@m zw1b>Nt#1k!7Z(ZV!nY5@=U|WmuY}`|MhM%15l}WZzaHB3Z))FI7XMvfZ4nh*wJ&n9 zvBo>&vkLPmvkI>|dZ9%y42gY6&zlnxu6%cP#U;fksl;15n=+Xti!Bh36uKSgkk;GZ zQQr2mXTSbv+H6+woSKI9y1J!&Bk!tzvgjsu8+y@o^RQ2Apf|c;$PXz z?#5e>5p@lG=mY2N=Efq{;t_>YTwzS)PEG<$Je3WR3T=}&7UbB+9vQw|5b_r)jrr+4 zHEBH9kH_(2HLV3>r7t6l#1^#8;q|dVxv4_f)ZpM<_ACP{sB15G+lW?8K=pDgUq1wc z;$XDuV{iHcl7MmWUtIU2j#^T_3Rxe(2=1~`Q@)vCDxKRSXWod)x?tarpftN$wZ3s3 zbe$Q!Lajg5%=dvHnxoiS2V$Ho@!N$w@oT7hOO z-qDKM!Q6kN*X)HTmXWhkCp)^DvId`23%(;I2n3-%mW#QQT)ja#AnEH@J9Huj@0z^}Sxe-9u~c-7-b) z5`x&S=k?y!8(P{d|0L=$w6s2mj&kIM_xz<%GuQ}VkF~5u)ea$L0#M`&>l;o`C&fbL|k6=kQdkKY79h ze2X!TlF&AxB!PM-|76fnzNX)PulAe$UPk4Sy|G|Ze&rgUQsr&W*S=yaR1W(EJHF+G z{^%t?fsJl$Me8*al2ihJCb^m+lz8Jm%9Ve3h9w>Q*iY%#gb|uGQ=pb@1G8za_&FfWq;+!hr0BXrdyB6mheEmu$fgXrFRcI*x%A& zL4bfdD;nW%Winagi%gM=s5fV~q&UW+xFmqEb z;%xy2_DG*{;{`)wL3f_x;PkA^SQKqKB|WRYCYN{74p`Z==*G?^_wY!_>__gfHvP)l zraV~%N8R@4+Ki0KSX%+Ls?RpcVa4`z&{z+6cOsRl8F0j~m9OqCRunKiqjZq^)S~5Y zw7)T#Q1yGr?|!*|v}=3T@`Hi1!+roytHh<=KlP4`{^qf>ZH{#}WJyqTdX#%$E1F9D zpQPoXeg1MQWo*JzA1s11;$VdGFC3o~E!|8{Us0)$Z$Gjp#%c(@iC!@lWoh1tGA%1~ zkHIWZxT)7bZ4e8_jLhljH1=a|RC1D6}`DW*hx_ucNpueNFx1BXxF zW=3Dq1|#wJj@!E%HB!-T{!O4cf%i6Gkh&}s^wbEeVozCCB5bD$ak7C@x9`~ zI)`$)G%u@|sA$9z_PZIz!K0=h9f4)@V~yv;ckEk-9wO@W(bS}e8#2`D^`_}7^;FFN zjIjBn7-|Y!%zri~lz$4`mMO`sVRiZ5U@+#h{P=RmF0^md)UMOCnzC^7SeZ6SNB3sd z)S!)YDhpvdqgPzY1mk>D(BT)I^s6^&P{|K) ze^Q?rhiN1zuo;LMR>btK7f88;q$Ei{dKc_hVDPjXhtz$B153*(?mm$HP{f@Yfye`G zHp`?H(B^-@?g4;D_>WtG7DCgC__5U^son$&X?sy29JY#f<9P4~zmoX!mxFCKDAfH3g-Q9Ecu31L-8#w`?}Qf)B{ozkxSH@;AC}@{UeZ9LoPYe@ z1trqx8HPM+iHENMO($>Y?7L_!8@ziyYNijeClIr!*&COWgJ}P#TxE3j!pC(4#e-gJ zauu=-HCD)ssq8@EG_Eah@=Vco5^L~Ad4xm9M9lE->X4-$IpTgAmBKg*E6#!#vj5UW zUEs<8-oUqw08-*+sVz%?dk0$(GX`hhrz+H$M{hZ5munbfwsxnS#I9) z*BzRjMm(*Gfj>?A*sa3o@>s2A!5o~%&e)V>wDEp>FM>>+Up`;5o$=u{Ja3oEjiz`D zgPz%`o1pD7hg1Gu#EH4}Ln!Xa@YYwp#)^=&AB0Bx*OSY0q&0FFx8lKm5zn+NKmX2G zOzi+IH$SWxuG z-y_At^BztFid(XZRZE&Wvpu@VOjOVOWX5k!%MEclr^u3;Y}fST`xIiAorS|8TI&yA zzRZh=hydTnM2=zmIo|IeHl{KF5evoq02Dd?qo7D(`GgCo0@QZ^>?bB`D2+hn^Xk3a^X_%Jf_||Ks=*0cwd}2$#K$jetX>lHBu_u#9H+6siHA!gnhCT|lWs7L$24gb9K;|LxE-=XF2D=BL-VcW41TLw|Ah!^+gG?rnXFc8HT|Q ze!q5j$+GexdY)ts-W=k32wJS@8yqgw5Eb5Ae$@3M9547VVQj1E>S4A-{i5kixsrcZ zc9mEgYKoDqE&dl2`~D6z^uNV=*mS*3rYVl!EEdYgTa{TF)lzW(Hda?xH>XW;9SOy^ zQjtzgr?hzPP$niv9U2swA9uk4^RBw=7N$2BNMcRl26{LlI=h z$*Xhyi=lb3ey7#S)CT9K2d!MaO%#tE=T;C%Kr_oE0BTSanQO_h$En%&=la4Z*DUz0pY>lOc8-xJTDlR|TvF7pEoGy>J94vXqj* zo8^OO&>p>%$1dZ)zaW|*DW&9o@h&Opqp$-#^M`fdM<9F+RK>Ch#n(6GdOPw{1qpfE zxV0CP)z06c#Mc}7+`~|Q+mriM97;$*?EA-Cjbc|trvC)TwJ~O$Kx^>uoD(ccvQw2O zk=6+!%J}4!_Dxqu5$T7)g~D!ohK*DNM*GAu6uor3%F9%|tdKg?nnDYffHZi@235AH z+O%zrm_oM7zn+#<^OAk_@lXTWBe;qEUeVvEG(UbFUqxgc71qEr`ul-x51QH&`|a3D z`j}C!lCym4fvG0r#Mf7eQg)nL#D(f68r)TxUpz#F+O#+y-C{JXtf&B|ZJGYTj=)ir zJ$zE?V1fAo(B0f|9Rug?knmS9uDynPlQ4XWNjsN~o&8A)Zve+rqS!LN&m*Yv2N(Me zTOzoqY3hW*UDijTaDHsVP5E6{KRvo5U7;s+kQMz9Jw+VM7mdoous>OE!SkD0Ped!? zAC8bz0_(VC#k$C6)#t1scSl!m=}(33@{>}IPO^kVkdFQ=ZxM@9GE??Fd3T?ZM!UkO z1+BvL)>wFlXs8c`rO+qJCCi#WQTnaIwEbJytdD+O4DN{)?o9sX)m?auTo0`VqI(Mo z8$Y)=iKks07~Yi*9=-O-5V~ppX9=}4_pQwb(!JA4$8Z()7RLZL8l#0UT(aQ8l()P< zV93YN7k~atO-rjx1HSO0pogNON`)_yiM{~_VOA?y9=&BW(bw0PsVf0}Ez=atf`RxO z)Gs940ciNMM-{3W^(1RS!D zTOh>%i;Hw5O5RBz+HdKiz#Ld}yI#b3NHZ0YEX*>P{R30zD_)@KOUMqOLhD$Bj#mKu z_PfZ|AEC$pSh}T2DI(PB__xz%j|iWW&T%X3Q_~mrtku(V+oAVekqX@3ZrkPgy!EZ7{gfzpY-g+~v3GIbj<5M?tH@ zh|vcU8_@B9FGJ7tkgS}ocVhfxhn23w@k0hxLSD<6sJJ)$@Fbj6dsFrvp+Pt(BYSp|od}Up2pL%se)sA5e!supAHV1Iyq?$d>d~v` ze9rrGzwi6HuKT+7O$F~y7vVz#Ueo{}^iaOm*+sb{jcj33tLiHLNIh1)!cDm*xDezJ^1P~u z@Nc-jL^@2WH`DX46q_yPG#_l_+U`n8%RY;q^2kRv?vYxCyAUA_4t8wJ+|AUlIynyP zO+7#qB$f`o!q<;{+`d&KN$+tpg9ipQZemoV^=0^px4qdnhc~Npm82NcE2+r(_0va< zm6`%R>|M5OnV?OJdu4QK83&lx6Hc+JMrZX9RjolxpCaDXhTiXfs^Zd>XIs|+oa=Y>2 zI@sHtbd%J8(b+?Xtk%vFHcl>csn~K3_g+9i6F;L?JiAbiu5GYQu-QSYxg0>*c#(+gM?vx`S!_+*%(VmhduR;X}wHt3ynAMoyNO5_V*?g2wQ9bg9 z@C^&cGAv0evXGq1{Mai%zFd11hlZ)2hvz-&ga2{ds}t`(49LpUA*Wfo95x6;Z2P+t z2bkR0^Enhv>dJmExkwBDun(8zi-S-cB$q;k?h>ccdPubg)jR%A@MoWMEyq5j_PdG~ zD4Y@6rsr)NpI#t@W(tx&L7fbNLMsn(Q&ZzvWKGo6R!;+dHpc+I4UR&9As0wLPKRd% z66}?L5Kev?h+6#C2)Y9mE+Y5--lfowe3i99o;M=^=o#SWVe~WSM2fTjzUkz6OSg1; zFON6NG0CFF_!CXE@!4=uNa|Mt1lA;Hz-pm(=~p7djhLZ-L3d~Z_JP|cdcjD8K|j0& z6Tbv%2{MGJaPq(sEvQw;b4eW3`Bd+uNovEcvb(-W zJ@+A2#l$GVi%E`)k>uCq`TN?%ipt8pnzARKV3regd7&o&MyYT&gSgT6Z>G%e;#VI) z;-jqmW8v1vtUgu8^Yw$DpX_lddNZ50C9YHdfcU7=|eV$2-|zd{DP4b~iON z$W?$jf9SFi`iWS3oku>kX+fNVarMhKgqa|_b=lDvg~UvLu;IGX%f$zP2Qz)zQeZW6 zS0xFEAoD^YWNw3U-Yf2G}>G--={b@OsSmgYSeP%Uv>&YJFWmmoR z%(?;?;U_I4niiM*`dK@>q3x{)Ax%k%f7T#B7Vl`7JrgRikwTOA`H-%lI+8_q|1`bKW`wxZkY&@brN+!{ae=OPU- z>1i%Roo26&E~dDNu0U$ba_88*C~K!L&TYiS0#4=>m>h5})x3FAnn3f;dFcA!+D7Y& zik?os^ekb-Eh_lz1;0XnQP^MemS;K-_xd5Pfcav6ve4F#q@w++Yff!2y2VdyzS<`u zU%Pu1f$&-xE`leRm36i$k$}C6S^dt;dZ-^)uODZd`(A+Y`)Hlz7ibr04e&6Zm zgkg%#TfaT;NsGDr_wQ4Iv9`_i!CG5OID~<3&Lai(Hb}+^&1;GiDrLzKiys7-{rsp0 ztg0pstSjo44}ZB-7w6$*R=j+Pd5UrLPls*I7kEA8(;n(O#|4H{Kf~Qx9_P#`+|?&A zPa_gD>R-C7$vA90Ohg!BN&k9CyT$P;>8hBoj?1xi+V`dmQJycOP6O~}VyzZ{RqP)a zIQblQ)ec0BC*SJy&GtUCqBQFJ<8r9p{mj3q;A0w1mil(o#C76=KzSrwHhUUmk4x*| z3fai%=ym$$vw?i`9W7L>qcCdeTX`ctIe8eILBNKfU)ce^^%fkbl&P7Eh-G3U4(HIgyy^U=qK;@#5@{=&^Zy#eb~y ze8z`~*H(JoE}U+mkpOv|k$QKUzwbZtKo&B~qbNIn-U*_i!V94~p8yk-CYXc42@2c_ zZd$j2Y;RZu8R=;S%!$$n{IpoQ+J+(%crTlk%}z$_1CsnU;Z@sxGh?w&g1$FmF`^g4 z%mr_@wq8Ux1!es{-)b~<4e#(Juyf#sH9^Pq4TXb(qAYqF`=Z%rR4$Bev@e;{@n(zD zOAnNsP8uw%^T?V#qj*(KV!pF^u;D|y(H;vMKBr*Y+0%2z=zZsKp!3e)ngR(Ee`_n3 zQ)xoVHPlp9rF5K9s{GaH=;%ZL2zG3p`MWFQPYuo6?_bPH>*--cIGo6hUcTz6pFw9+TI;c_QFyT{2je(N zQf{gHxa^j)3~|Lv+pkk`*yq)I10N%wf2)ut8$)yrAv@$ZJ?eJOU%oB`j|y?HF-_OE z1T)eC7Gnj}wnrd~)?#D>aIrR@N?b%#i4HS`E8!t6)sNEgH^(j7eQ)j)U7_nY9tQ0R zY>lj}tg!Yj1(CJ(^oBRzOJ+cZqNUsx6E^*XS@!_r9@#fxU$VI}cAB&pt(Hfw=(U-_S~_@hn}U`bv~; zBYBGxBd+;QhwDdk0&_vcQB2C5Uj&L-*qEd zQOd`>2_QbmjNdXS#ye|j`Cn`-V0X4m-?eOC)h}ia{w&w8u>Gy2HPYT*Sby@=}vCG72OfIou-LYvN^RFUFG1QRfGShMp%xe3|ciz*-iQ^rokE zegwQx>(JE-S%TK*DknufwmvGGjaalPvWm}`YG=q-oOWB~0sy(=|4BYS7!w`GSLLBI zC84?b$j02pjnB!^80QH|3iajgNX*uJyW;%F7WYW6An{Ps<=#?K8KyMHz08RQM-o<2 zqXrIpXI#aL{2)y@~H|mBQAX9!=+v$(Bvvm7rz>Rie+6w#~d}za(D4So>)9zpg z*BEnUBy{4uJUo&fKTwf5m;#P}%)%ouXpQ!K1w;|7FL;4QW(0cm!Zwlmv1wx;TJ(ew4`{FXI|E zABjHY4!Gt`a}yB0DNMjaKVqRL|FEQU?-`~yJKi%*tWYR7`OR~E{upqYQppXox4TI>J z5_3>E7?XctN^EF&RT#h6&8exW36c_U>Q>rbgNgw5y}-NB(r8tUfH-pOf2fC_2T()c zHLr*J>fS6PLCK^y$Wig|@$D_Mf<;9DD4eM$6p>IiRSds~Saf(njnHgEGxb^NPb_Bs z<&%9@yS5H&Q7eC;MjY*V5fm79*>=G_6mBMXajVSg0}l_vbehfOwFl3WCcZR!x7X>I z1_Hj34v01CT@v^B!TMGQ2UKvDz409a3>);l1VD@5%jm2VJ5(EaHJQk4)?(Xd|5-s$fyE@aTOJRx7ZMII%0?B97w9G zqSYge-*eoMaLLIeoDFHY{ZkY|Fe#Nz%Z-Rj7juEv=*9XC757=uFZg(om(%4|KlzmR z_tj+rK-9WI7h`HZxFfvpV!3+7OD!zv+v)?eu1h`RKucPTAh()*`A(e0>oYW7wkK&@ zEQ-ke;$1cUgTIj@@NGs=={d6RKUMAfv9Zd9UxZiqew_FbvxxXo)I;VKbh*N2=p8&aN+$a38< z)YpfltLExzs`P>}=k?JRqo5+`7-yG{-Mi@6-WKJP-O`f~4)|IItV~0GzHs;Rr;RUcH*cuHy-Vp6>8=08Ulb1IiEm4Eg@&6$w20&kd z&7Ft!?vj$LQ~C=UV}R3dx$;#l?zAgwpZi;djK?Q8-+^KpeM!pw*g14aS?NJtFvg(g zQ%f%7%ddUx1lHAbxoo(1bOB_Sb_JaKDD2)uC`N(R;p=(c2d)#^Q|i+ke8J@+zBuMP zJ_Qs~lX>V6;5T+Bz#W&`Qqa7nsEEx%G6Kl}qdfud7gb){hEa4d^3`s>dB&DwY7k4Cu_qaSb^N0TWreUnuW zsC0ZD)xX85Lf@d#(E(0Lv(w-)K|R(D?r`ANgmq|FeeMUI$ScdR0s!0{FnAOH6G>fi zH%`LrJp`W^*4b^ZGuZCa727O!Z2d<1Zk+E;#e?_%X?fzi04{nH!Icy>^PZ`L^$Wv) z3;)_%xDR$D3v4( zxNb{HMh1{MPO6?0BAt^^tOtK1Ru2AyuR}MCpScM95%~A%%{SXVs5t{4U!T`wQ!n)f zdArMrFI}ayrhadm;7w+qe@JX<1ubXGeQ$pvS-}=}}j1I$TP`blZ}4ELACZHeFsy zfxZjioF0n(*V{w@t0Mb4toni$Hn2{06K|cr#zu3Ze#{B1m&?^|+gKn|I#Db8^-^A^ z&IYCoz5MF`c!!@ShyRPyv7qzPjFSI6E9d^Z8FzsQm28lE3`b#WMbx+0J<$Shgqu_3 zyG4P`=R+IvwuW<0=5PB8HRWe>q;tfBn^5QUF|1es+rV#<`s7L0|FI2A%@?%RkKtqh zUT}gn3$$K?Pys-Kego0i=C5Y_qK;3g538zTP5CVA*}mR9F6et5hPqti5R7p(>Qw)} zShwXZvOcvZS)XpIeF?*!>0OZhn(cs6k_r|0`v*lf<`EnF*Yo5?hHIKJ;XuMalOi7QV|ifltLK~bh#ySrCU z$)6r?oZ+@xcz|^+^u#oOG0+kKJ!>T+Z)}|R*H>9NvZ$yC7Q(>a4F-3eP1{bjO)iiC zNr=Otadu#P!>#hb+{;^3XvWzWhE=F5f27(@X_ZP+p@7pkCOhM6y=i60Ue4h7i=3M{ zvI%yM$}dg$evWl&o-j06rMv0fyRHee73Knpuf65*)*lq0SM%XGubApYuepXxQusF( zrpob$oIX!D!DF|0Hujqd*;vtP8-|g2VJ%p>C4e;e>L!?t`KmF{BhU!waCeU`Fd6d< zGw&*p1W1M-+W@n15Dc9hAACVJ-pLUWLj>VcukL4`U>4i1oMk^(2_s}mvT&~;|6M_! zlyOK2`~fBjJ>%mv3UbY) zEH-fKgmeuJ!Qwrw*ja?LsG#7h;|%$_oXkuJaA4!(s{{xE{-&7~JD#QUc~<1zKP{oD zvYs9XdlBVS5!ySNiE{5Kn*U$kVJ?2Rn0X=RNwSJ@Z-k-a^jnHq^A_gQ>t?(7tF7+e z)#DE|oX{=-0Rc)9;{X=I!V(0A;=l{}KPZ#u%GUmd;nd_1+c}Z^SC4`PD8*vkj$c~u zDBYW1%zS`2kbFnbw{+$Ca=nT`M? zFIcfBH+>2t@}q>j*RNl%wQye_**H6wE9F~b^7VY)H4rpjySToxBIdJYHa}3a9uU&2 z=<^JX+5f=L6RP>LG0bPMYAw_QE3F8=%7~Rx?GGG=(9OmB;VOOsvHrK4QombDc%-#E z7Q>>C6z3Do5}^%z1xnBlUVfBwaj#;p(RBALfk3S&i#J&HxAxf$oC|m~L0+cOW`f~z ztce&3ig4_uYX(OIg>zk`^E#=?W0r%SkD?SWrnq7%<4(?x2Qb0C7$pY?ZzSc@Li6HE zXX2Ri!p;!I0s-gIYkH7|{I}@a=1g0VsjluDXC3IGSY+A6#TmXHhi(%TZSSa)5#@PMVy7Qfg+V_F$qg zsb&l;iL!QLdVAnzLehv=nWUwp z=11AsF5~ZgwbrC$r3ttt?f6VCzPK81BPx&KcYkc8akbqKvrA9Z0@CP8*2Oj- zu@YT*?rz!IK`M4ny_#ODR#fR71r_M?ibKB5Qd@9xY6ssnX!Qp%5~u>6Q_X|hbz~yW zAP1JM0O$l81HEr+=Vv!Sl|y4~#J_%VcwQhxps zgt+?zAFlUPR5|Nbk2I!SQfA_DfM4`ahdJ*mc*i+7#G1K-ePA|bJi(;C*1X2do9Cxu z^R>X~Ht6!i`l=mz9Y698Mm(ZLmDTwQkWW*-Xpy)kHAjqSh46=tZttUAJ!WsU{sMf_ zJ~dWt|DtKxsJ6Bs7(`w@JWvKElQ4*{>BNg(Q+SA9Wi$P3p;38!mi4N1y2#tQ8dRUH z4t$RX+G0ZQeh?UObLzhr_Rj1cb;8-m8FBq}IUoceY2dy(*mu{z)JAWCf>6$M{p&kA z>b3cK+#%y?GhBE%SjdbQ z$-FRAsJKpg@1Aa_EbS>~i}HEGcjBOvJ-S`2p(quWuE#-u+a${}R-~*ryfIuPcJXR@ za1p&-{OwbVuB9^(Xca)8GfA>j2BhqqUvdYvYYz77CDWJ=r;3F!X|<^qdQ=KrC%44P{%ytE{k*F_c42#6s+- zY0Cun^s2OafCdxULJ=UkZpEqH-$igPOYAH-+gGsH1CuB8`Uug3_YacVn9rg`(l!5`7Vu`nN14FJ>!2-ZpA~w|tJ4aB z2%N#oL{*Fej!m(}XBUPyB~W1^(wW2J=0?MtkzX_@_S1te z*I=`iOsqDH+>UTI1ra!esK&*`!Nd6iB3IlBwXPuD|Sj~mwm@@{!6Vo#;GtMxSL)|5sG#9 z%g>)1^d(Bf0Nwd&a+SDLYp$>ygb=?r@Ud)e)#ga=P7?4E#R2JNS3>l`6R~dS!i^LF zyGc_X`HQe+ek8&}a8!&Epq%dl#?MHSVt=_Syp~#3aMm`gChv-$$feoS^4z7*Z)9BV zF)?m6!kSRWE9~~qoL@2l1qFPoVU32jZbcl{VbA2LD~Mnq_`zFF*Db&_iroU2rrYG= z&Cf5iQ0r*3zDO<0OEfpt$zPzED$w4mtc8Ww^r_@aud&l+;}H=ukz9vyDQN5PZ$V$h z$3Dt=frFWYALf}YH8mvLALBg^X5P7oAA{Kesow}Z92X#%2CBhB*F3wypwFw84f})87qN@$AQDr9+8}SlMx9v=YACx);0@XR z8aB$44a`XJuu|2J1PJ8P~p$n2O%DSWnxFuCr60BR?c~ApSQV-qMFxQ!-{H zDjbWvf8D~d=jAjqG6J=h24l|=uK`#yIy*o}YS$9D5qI}pLn# zG3d3d-)3T)Q9^e?3K<5<0B{!5mFnPI*>56OTH~PQ`xbZD=>O)gpTaeJ-uPE>E?2^} z_B#6|OVCe0CbITdeeBB(2#ga>ClM6S3id$@JL9{)Q`4U#_d)lD^re0517z|JV?TD{ z)yz-U#JQ~wm*;&DB!WWiX>X@xmlf?4HXvJ=*38-YJQ_MKd6Fyq{QLm1!{j@9>G;RW zs(e@PaH>89)5(*J307t^vkQHPHdNwBq}k5QqidNJ&NrzAzNy#b<7;9$N6rL7{gcHQ znLzUTe7KvDU*_gmsQZy$zy{pyAsp*yu)Yxr#h88&&#F$fwL|DEC@bhQ^?eaoLea+D zMm%=HMG!ZCHJ%Xvb zYdR)xdznaI=p=iubqRl7`OIcbCVh|*Qi5K8@smD`LAO3KH*|7QD}x$c0zzh;Bb$$% z&Oz0+Y-@HO*?xoV>%1a}d7;3c>~Ok;3cU}MmTpl_VEo#qDa8QLsbP&(4DkOsIZXDv zX7Gc|)*zwo!dbHm`_)KYOV3`PP;=LZ~0 zrLp8^f*5Q`4?J4s0*Z6^?oOAUwKZf++y%2De}><5Um%fD*f~LMPPY>bS7Bv#a#BCA z(o?Ed04$?(`W1{utQht5v+FW6J%9U^Ni&* zzE4ir+!>Y3JKZzPxJ9$UO!$>AnD~9w9%mZg=E6Z?eMD~Lq@`A7$I9Js$?vB(xv`q& z@7%dAy$WYoC6!OQL%I zlrk*YkHq3}rhLaO)BFV?k{0YWst>`9sVl1cd4N6wJaHC&8FE+TRhe_kZLD&6oT><`XC_&bI5Q1VOqphY^-_#5?d%q%?B8L)XZn(225#L}R|gARP-;QnplTUn{BWW&0`a!CmII0# zM5?VH?X7+9(4L(n`MDxFVU3;SpY-CTfH58cf&=^cGYFY+x7TAoh{|rgrx>uJ(%T}u z(dtW;k4w!*D2%ef@@QfOrGPF5=my++qz)jti8WWpUuv5t_WwS8^WQnV#DxnDOXuJj zKt9XgU=kLHIzozr$C7aJBXY$APbkPrQlgFa<`BYJ;b$GhaH$6>>N#KM;JYsVCVX!58 zt{D^O8>7SyQ+SXIcKdKQADCAHL^ST2E^@hI;(_WIv`KdGqr_f_|&;A^2}Vf7l`5Fhi?T62`)r zvL#U3GB)GPN2arbiB09E+L}6RusrW5MYC4EV*3&gRWJi{u2BJ`70AJD&e(?xAJR@J zw6zS3Dfwk3^lnubF|t@mnZmIjKNDBsZKN(k+SJ0Qc;$85L6OxP|2ajJnqPmA6r0y? zeB$t}NH=C>j}_8CRkj0R+Qn{6W<$Jr{;>w#W{lR00goG7AqA=ds9kK{;f(~bdI2G! zTJUFsIj*~L3^1G`*!B1RWfrdttF)CRC7%1dHrv>wQ@N+$M8#%N8cZw_c^u2g#nkZ;8J|=rbQQ`vzBJN_zYLZtcA|SSR?xbCEv$P;Ei^bfqCQ0J=t zWZyL*vf61y0kT>ots?KPO|;hDOdO6iS&KEUidAdS0O8Jkb?7KkZPPQAanapfjw=8= zy?*fKC#>u!D-+o}f;6tmqy-l8Pkz#%J2cGA=V2{<~Ei%ptC?F z$4AMcOuv<$(mXHo8e<i47HIAh-wl<*Cl)eBl2so8u7wsC9^Cn zw`x`M+c>bg5NY`qphj5!15nw)&MrJ6;z@68&jR?jfRl}nk56*%KU>gy*ljfS zx9acH65{dWBNGDkAxF=qbx!8+p7a9JQlQIfW>wNg=+{B}MG|O*67JP!i}kGWKWQLv zdq{u{r}y5}SjBFAEy6$C(~`@bHE+EC8EE4s`Mgnz&bf@hBXO4`7Sn+dkfQVeb2RYK zU4h+G8^kul)hr~#JpKL4!;azJt?e$R2@zSt6G@K?^xMMH-7oI7zc4!=L@8fCVgJ5* zPgABP(IoTHV>iPu3NEX!yP~gP)*d1K?|1}w^{)ujiPa^%(3_Domgi(Y2W7M-dJZB zDR^f}vG)kr)^!Lps=wE)e@c`U%1r@zLMk@i{Bs@>68W_yQ5U zB9j%=SA9Lik|XExeaisXnLXLq9pWa2hnMp|t>#qC-)adk^vG_*c9mA+M|?wnNBRw} zxEOS4XLn@gFAYenrw`~F7=lC*1)$=kUk9$fwTlO zz$s!LsX^&^EIQ{O=JA{XcAnGIKg*s`<>bO?fp8E-5FSosi1nk~9Ul)cvc`k0ul@RR z@Jn%sN9G zZgK`=U{gq0=nojm;9x-+8wgT`)1k>u`8D5Q7rogy_aIl;CtamZ3eQjS+or@44EI>EcMg$6sz5!R3?5gk6xooy+qMg*A%ny>HTiTRe^xiOw4tXcx)gwE*x*!ErC7%9CL4hXYG2E!Ykp&%Z)N^xprv*ckiOnkpqd-9P z;*^THb;`hHikN^((n;M>`+ns;ZEcBw?9X*7R~*ANQFGJeGWCWoF&VC$y^`L%!3i1y z-GMm;r`9{B9rWcF84yVb=m|K4Ht`bbZxsN-?(77P6$th9GGo-qD3;$g}{s3S#D zQGr)H$vX3hlxgWr%~IvbwUPbw(tg4p#X`fxt?6|MR1aXBgH6kCuypu7Jz>ip!X#fi zh!3va+%{Fw)_?0C$FQ8!f%Mg-io~3{Tq(+8K4CMN4vegv-W2cgEkW<#yl@syk>3!) z@|7JGjWC!a!w?D9Y!l6&_z4pQE+BG<4A?XMQ$isCs(lGEonc^AP*GvLy-4318l3>O zLrrJD!L;Cx=j$5NfWxh#$jDxZb$Df1`zmMd2@a9)>!J8~nCldGxen%J?X83iHqY6- zkz(JlzBv6-kw5!K%%!(M#-ug6>cYC^x!nFjnI0+-Vai9#6G#@;17H!v`+#cv7W)F0 zrL~9sV%Ru6USD?{orDSl*-79q1xGzZSVUGf2u3?d+?=29ru}Y#_5tySa8IlCdu6vQ zANfZ7kKgsWr-_3v7Y)L6fUxcr+Y%c>B@Un*-9`)N8s}3#oaxo_)M-9USQJ{c;!ZyV%7+rwkTvjV}DTt+=8BpX^npbqxg=WdvTkx<8OC8jOM8CH#-aPsBi(?-2N-voXBU z&VI`&4~a2Vu0PL~iK=bdGbK#0oa7pG+ztAzI^f>9&h$=I^US?bCVHRZ`=e{j`LoG^ zH)UmI=?`td<(^hAkQ`f5zdbmU-_cpo>@_h+TG7l5d!~4|{5v`;MuLmtsKC^uJiSu} zqruJn!HM>*ajOv9R=FBznEuQ6)-r3P} zk|cZZSZg6CsnqT;#azMZ)< zq-<7bpUhc3B2X4dQ*_;Y`|#Y9XjmCDe!o?Bw%kJBr^M$!9z7C34RXmS4su}^buuCK z>hV9jjjCtQ6o1y1=wlgb8mk^_C!HTSh-a@y9p(*&9}w1A>L)nHqk>3xY?i*?6kD5M ztQu;tq&Aq!7xz#GjNsF!It%aXSXgiLb5Qn^;I{@o>3@ghq3fpmMTmAK`vZBHff=*Q*HbGMj*>b&!mksYJ zNzGZs1{{EPX?}GZ04i*Rw8QvMXx2;6D71^VH={aM#g!)6J_$&e46=PcMzqwW_v0FU z78BWBxvNy%R27!=TD?)8U_)W=EUPoI;awL8h?G!FWwh2Ts7oqq zK|A`>`D&p?NZp?Kk&AmD9{R!Qb;+0TjhomClz(^e7?S`f-C`$9Nf5FFs&YbEM8joJ z{3Kw#vp}@88^~^0PF4{9oz~eKp2Y2Rd3o2%_^CX)$aJw8M=a)W7Pl{nrK-V?1ABe~ zAF#AW?`wFpJu(Q_zd`(Zw&x3#=W&kFf)>pWk}ZZ#e_R`i60}mXxyg0v%=K?EFV@X$ zj-NpbC>R&ba$$;ENpjbHTfE!WnxP#j;D6|f4KS;C_Kc%>TKzbZ*$wUy=*BejBxukL zyGxjhU~`S#rje3{We4!jodQ`(cem2$B)D|K+OoZagFCOGW>;@p*HG{$@k{d>N67_N z^Em-YSx(qPAvb0nW`~d^;HenMLH9vC^dq-t+4M4Hlm*@<-Z{V?f6&s?U zc+EBUSTpyP0X2CFplxwxHAzX7|1dD5!K`ANg6DCFiZkn!cT_>znz2=!-WN5eYE(Mu zRT(ZO5xF%0oOqUSGi{UC_R1|S<<+QnjY(Nz$}i~onf-%)cANt;Tf80#tot}zv3Qev zS(o}~>)AC^c>SD?8Ae#5H4&`sZO+f#m4>YPB*5S5^z7 zB7FnSu$u+{f%jVV**?d{mtc-D%LFXR2E|yFLHFNBWti9Ky8L%--%=|>uFi!*oD*P7 z=Jj+>b(!sXP6maiz3&`ogp*-O=FD3*@UIxCR3xqXl{0Klby^0lJ6Di28RJkUKU^DI zm$#)`7I@}B{^vzD*L7z1k-C>(>i{h;L5powq}t68nc)$Rz2F;+jJ68NU1$z*?iI|X zQ2R)vS8*4H29HSAw)`F_k@K9JfWT5eULEBG`zu97MI{R}RaI3e1u6-Q5nH>_Nl8Fl zIl-zr6-X?=w8Afg?oQeEKzn1SwjhIa6+f_6ZEx=kxQEW=M3tb&=OL#dZD+6nHVqQ6 zLn(7dt#dsuKdNoJwWV3l*_s3qLe^)MrD&03??@g{Q!CWlokj#G645`7xWg(jnT@HO z_v6(SFI*|Pz}K3!#3x#*vhM>*q5TPZuQDF`w77up?4|iLmh_!?VOp&IH(^>%dp4>{ zb7<55NROUd)zae@p{&UvzK!CO)tX$25JwR~LGAnZy*#>dm9TdD&C7sKuvrY{NfIAG zhBUWA)7(4nDtLzh#epIT4eZ|P7_rvde6mBUXD$|Er7lGNtI<;4W;^h*fo=^(gFdUw zoWK5-Tpx8RTYh9CKEjEfpNpP^@`az0!D|p|=?+(tE9wWH%%gWFDGynvATr=6R7tXH zN#h!#pR}EQg(gI+oo9x|jHtyUu%6b0hvunIAFwQ+V`9m|XVY_g@ zKgvYd$wG2Zi?Pwgz^W~Z;B3;)i$MMr1|;Gd+ut)Too}mq zFm_*7N_W4O4Xxh{3)=AgQc}i8VPL?V*JS|*Tk5s-^$HM!IXcGEuK>LFy0TKne)MYi za}`I&io`^Xv(uB>8Y|O>V-*266%dZ8+L>y@8+bm!QV(SH?rnt~f3WcyV|m{9 zq?b1JQo@e@^rVft>N$f)Nt!h>p6UdFso2x-ppnG}s5G}F+w@6gQVoRjr9?15&l9rM zXz|-?ZjP4jg3vRd-f?!TBHySFY`Uivh!ZUZ+tH}8I^4y%>Ia21iLp)*(lznlq?I+#6V9z^!t#C}$ zG6pBq9B6$g^boyTf*x+EGJZ#E(f~1d3)m0-&rYKo>2`ATDAV~Pq&R{(1t_mz@&-N~ zbg(4=^LCP-|HN-+7LHHeWi>o&zl14ix4L_5hv?!3s}9y)7hDw}Uq8IN0H}BF(9_~f zUP7sr{kW=70FnT}ISf^LY%|i=x*Uj)_-*DLNrdcZZ^t=WViJumAE-$*hrowuL!q=t zSDD+|a+u!9k4j}ookvmk+$Ll_SE&A?LBE2m1d+NYA@&(ef40r2buIs{(|ii>3;mYJ zQ{nqYi2TLh-qQWsvv5E@Q2z(yR)CG!#|!TwLhLRPD6v0*!3k{mUK@7*B$1GS^6s;& zp`j6N2vh^J1*CKTvn_(F02%_t5}k;sDA4#M(hK0tYv^2F8tC2nWQ!DFakHmDjjCBP zAiO;cqHP4@b2No6s9M@L(G`AX@&4E*$l`JRbA~W2ZaOd?oVNrTCEgp>KmK1Jh_bU^ zRqH+9Ak#OScuT>dU>Phq00*(n_cy|xx72bHw&sRme>EgB!RVY&QB~Wcwfl!DA8OwX z9YFN@5jVCQVt5O{i!gK>&TpFQP*T5}tlrFU2|$TdbzW&R?NQj=;R$`PRTE*9cEOO3 z);w4)CG%ETt;x-+8@6Q0gx;*K)Z6_J%&4jZl(eZ6Z^od@5cBX$tl8&B*~Vf|QVZ z4}U!fv#Wo+aECFD2zv}J)cW_yrj&;pJv!bCAI75pUnkXc8q#fXQbv|lLon^i-OGyj zD=A=}6a6d2p1F%2vB=!@iXb~-v;W3r2i}k2t*5U=sjokiQ2(5#k@xCP6&pMO3WGPT zsz$||PG!zFw?7+da9-w7fNy`T)^OZCcVf+KRG(_&w#()FZ-*t%7DVhT$k}8|m2~*h zdnqsWc6<@_;T=bPYDojOY2@j%y7NbNdQs!GhxqAK&m`jKz{WQFj`J;tq-n!)4a06fJDMXbt zCU?SH0Uf78f&ZC|o3aR;2~bzTnJB>T>y7Ws1?5EDVlqk6yFgWSlrL$|UR?au0;vom z=#BnCltr=v4XyYs4_b%Mf@33QOZDVS(8yPnH(zZ zvWs}(6V$M#l$0r*bP~?^>^b zue^8)&70RockgbEY0BCI&kz~;v{zd&t5=}n0+r+6YaduvY%025Z%Hg1X{}ae1EdGV zaTwp^6J+tN0WN9zGy@B6uNbl%rTDf!zIUU~IDyOLaw%p%bW(P6s{Mt-*%G6D#YZ;E zQl&+ayZR5LdMz)b=I#B3W=x5m_I{v(I4J-=Ahpt7iD3J@1}vF^q^{`T_dY^q8mD=x z!HuIXue6lYp0|B=_KL+sg+UmV)c-*=g}b}S#7w4EEf2qMc|e~mMoe$!{;=;$bZ1}t z6720m3wMxe^HLt~hfX>=K z@lvLv=Nc9A%blX>53mCWt8h031r@rXkcuH7?U{6g{yueyBf$;nh0$!pTOE1h{>39C zT`)rjj;N&<@kNOs1cv9D>XdpJ_Q_l`EExBp(<=Xt@aA>>VsatprI||f8z27&EH>Hr zk2JhM@eiHf%@j`0Q_D-Mbs?gIgsBzKs{qFNn-YynvKz=6EKd|hk4T8fsP@})u#=&h zFG1rZt5K0XC1vw5yBk)NP%~No5fVdxx4vG?y<$uI@1WFwd;36_G!f#A-G%KGI(FZ6AK$l`_)B8 zD*O1nAuj$0&W;_ezmGP&Vd_0Zo44_XXPvzJN@9Dic{;IGIkv^NyFdXFqv!M%T*~5X zJUrz=XQvVp5&{BC=Wnr(EMZc{$H(D?P_>-b{R!Vzy?@LpHV2loTrX!;jsiS8+o0mmJ1Q?3I;v zSB_@}(HFb3WqWhIB!S4c0AT^U#diXR>!nG>e_8ndfZELy~vNdvl2d1OVW@@ zkA8U1Ty<82e7l&q>Qd7dS@E~CzWi;+Z9#n7+9m4FCNlVZ{eQW0*zU7K2+7R|}wh(74=s~+n5Q{m}MWU1?Bi+n7n*Zp^--m{LNQ&ca#j^Ve42YXURdyhsx^H{W+quETbXr+A z>l!Cv69uxBLFLj?P6r`)4*~GL2ATi`1w4C42=D`+Fkre$ndvId|A)LoOO>Bb&n1V| z+;)IPA~hP?E-DD$R`}qRRW0zEwY)E@#|{dkVC5+zh$cENnW?-G%EkbpU|ub9Udm*; ziH_EkJ!P%J*_JHsTV}J+N5Yn-y*Z*adM{Wv9o6S^oS}7RY(W?-?)|=fRk`raMwwg1 zi>Log8!zoL%iM*DhM|Fh%ThkBSe}<7 zrr#P2P6o4YmV@^rfU;Z(Y3r!hrj)4aw{M{iKpa@WEtB<}`~;SB9!Cv)J>u<1l(XHP z9;`9$H&BqfsxVj{wCH5;_Y?Se4khO#0$N$!i#* z$@N=!$ZH=sA9phaxPIFEbA$6Y8f1gz5ydAdys4vuNVT9+FVvK;+CsfYjU7ThNP+xkX4$SZ_}8gUDQQv zXuj|Bcn8M}psof?S)lFUR{webqKgiaK`$|c!PO1bx_e!|O+a=Tage`@Y4W7fpvh zIv%{cgh<)*>Q*yTugA4Hw)~ABH6_mU>caN+HfU(qVE@nI`gYO7m3deMd;aVh{#RCe z$N%y-p!so`LztY4oOlQ`G^8J|$7`j5r14K|$)qR+lSk}t&#!?KZwsggoS5p_)w@f_ zn(JQ~Xl(rxMOxQ%>5!)kaA&UXN$8Ke&oCFX-EJd*7aQE5Y-65cN@#@E3Y}#S*l{L( zcDT38h}svnX`;XGep{mZ#Km)t{Yi8kvfz@4LV3r%wIs9qq-6cb)bc3|Q(1J7~3etj7(u_0$Qqndh zUBbI&-_QF!e*3R|Y}hbd*C*CG*BQBMP^5?M&za?9cG{;Cs>BPAcob=`;BxiJtk0T$ zhwaX9)fP2k1J1v((_mM<2R0JG6(gWoDJo)?dOT?VF@w2qQnNV$)TLv;l#5!cTGX`IyWI#nY?KHRS!8Hh39%QLx z-Idn}o}gg~E@z`lUPXRjd~%~Z%Uzh6lz4-tHfaC%$n*=-g-|hE1s)ZU)HWfKu-q3l&%^ib;iW``8kkSlHvWZ)VDpf zcXASUn{r2^5aTcEua(Z>PB5wjEx&U=j+d`N;y`Xkf^uMmo4YXLHeDSfi;T=88Sd=8 z&h2yF}6DQ>>VBDQfuDQL*}IuY_&)2RS#dRtgJk{ zSb@tx?vHv5$O^z(47fCD4ix`kuBf}b819}vj8?>`)8l&sJ=~q8jyc8ayqhaU%G8tZ}7{=1|ZLuSQRJ%3sL%SbOF_)3i~2VeHee&&bC?y$!0 zwg@bo2?91ms)Z#==|_ny$@|QdOP57g;1}M+diY{+i!BU~kB<)?VQhT7?Rlz+k<&ks z8nSY9;;}RBcYf1`ql^y+}f=jMII22iD9u z1WQ<(YTt{wLz7R{Zk_(Ru0BpcArpU*ITtPjNe*pv%;-xFFA0U$m6gQij_tW&(`g?&G4};QCG65_c&&$e) zRDgH&ER}`WNVAcAGBQa=&wf3YwVu%8HE+@}G7^XIgz>Elpsv7R9{dZsF6i%gmw+Gb z-l7qH;eK2-Y%xCz;3vP?3&i|7I=Fn#&yK-_2NJW>&uM7H;s?5coCuuqW-4XJzFRVv zD4V#m0cQo8yYuwVm{LjoZegNVHz^)AUt-!0U4P+gZlL_7uQWdP6+yu#*$e)Y1h?ByDk8?A>oT0aFYNzY`))UM~6QO_KQ z>?oTb4z?~FZww;859fxzPRYzPdL^TvFw@x%UfEC_3Hi?!o(>NWJGNPU{vj&smVedJ z_ajsaH$zl{B^3R@VG8CZU^l$Bw50QQ*L2c&=e>RQE~UTShMVkW#DbvW!K+EMKuo*1 z*q1=+LoKyKm6U6*kYy9t&Ks$i>J+Z+YfR7KS^bp%J;$S02(Dcar!Uy53|0EH!H_i$OKE2GRe3xU%s6WBlQj+ z3FwD+3HLXyy6EcYV2p6VZ35~(!TeZVb`{^qW-shw=Hb>*V|o9pM47Q(LbyHACuGeG z&wt-lK2h6e;;F^j-$)C%R2h7IDHKGx>~P20+3BmnU;G*py8sT+9>_kpMI0<_{kX-}K^zCf(U|;_8to>1Ua|n+*Y?H)!3g$U}I) z1{U|zPc-WPzFdD`qGw=G53VR+g9|@0l`iV!v6>V_ycUm_n_a~jtf|`;_!U{I!uVQA?Q0>5zWgeWUKCQ4=;gud zBuT}`)~@-Tg#UfV1m!Dwo=5m8|9wZyf8P63YlZ&fwbTs)+0O2|QIrsMgkTmG* z3%ITZNpkCd#E$ACAWxMxj-M!WEtz1vQJ$if!lrm+SZwWDHQTS><}Bthf7@{|E6W@7 z0B~4vx-3#g%N6QyUnPo`D^JYGpib?j^Yr$1ovAH-xE1sN|4N|&8z?h@H47rFLFVPq zrDqU7bFDo!Kc7Ef4waY3L{G1_Xj&WfR-<5o@xPD#LF)K{gK{tPODmNS@mE5XatR$#z29UHt!|WV>L3qF4RL}Pk!`3> z{=uL?D8YC8On=N}^N&`TJ2S-`mFH*GpM19>b^b0DU;qB3SMl!_^Ha&%a>FFx?~)P{ zrW{D=*(bOPuRdCz_5wQ@D164-B;wt5M|NZ);M2(cm}_?y5f!ztxCktXPEAdPGB=Aq zv}oqkWLxZxL6`SQ@=dp5lr6)kPbRnCH=C%&+PKZZz(Dtw%;_>k|hs735fX&o3m!PiU6VDO zyUVss!;N|9bb_GrbKQxRX?jMc$-dw?zB(mU=BvybFH`pzAJbf=nJQ62BDVlZKiFEd zs(<=aZaL?9ZemW38BA!vl}X_TAy0%X>|hU6dU}KBvK(jVX^~zE!g`h zX6V!2V3Cq*cA%7a@;Lsg|MlIMu>CDg8IOB#OKm5AZfz~X{{oo!=g0%@PL^IYWI@1< z>Ion&aD?<)|37Dhtg(`La~+pMV?a5Mp@2hMKP0=ytx*vk8tOixP`95^4>T2Akutz=bP zSaE5dM>uNr3S~v=#i>R2M=|~s>Yxm09Ua(1qsLBp$X)@R}3cg`!T={`|H8wDyuBrI| zPLlWUFRrK;dM0paeTShrh(^>icng2L)Ax(UY~HY`wl$9LCBZ&6Ch*4>($%D&$HSBS z*upd7AhP%SL*iM5S}3BBUP@9Dk`dspbk-s#3RU@OkI~ff^S6T}-t{Zb8Rj*?L2D(a z=Pi6P{nkC$QA;P_;beO$d^nEF-`*$UnV<9As(LQ^8};TdoJ{bXV5IlZjwPCl!&eAE z#mv-HGZ@{&zeWFlUWygf_--AZr6s8r;hA28RRR1o+HjXKO#H6Bn#cYfo7f5P?s^45 zpkU3@*(ukynZ?#caVJ~YY53!!$83F^sV@=p|DBIj>Dst6U2orhuFgH@yi?!11^=Bg zDmiibo#AePg?V09s^Mo}l}QLYv;g-VjI=D5>FO}+BO&nI*~Wxg>;KVOzW#sjbp_2g z$mnwDqn}XT78h>YrUdWx5}P#Q;cfBu<{{LPyB9te>xAE*sas$#4kW)Z_*ovZx3eMi zuhTc=;-9aqM>3oIf1ixN@YJ1CyX5v?JstC=iQ`zKps=GV)v?CVLh=j*@0PhKd6K64 zLqokEHwabU;Fk45^$u!#esGH#>ix+7wLQr9D4a2 zr(`n1uo%n+z-%9UMx%tu!2mx`>;e7ebbyY~*`Svi89BM~%{#YlVHPFaZL!AX*lL#R zegu`6{dya5g*7X+9KTvuNRi4_kA0fN86W5yemC+RY8otK+4KqYPr;B7#={T5>)g7a zX_9ji>rl*nS^)G<;>d+1|!O)nIT^&5X0B@J>$B1iZ+hud~oMQgwPGWo|}sW!x(p&+Ma zIzmsCY)H%?ib;8Bdk=xq-Me01UT|T6H;=M?nN9f`v{kEVxi4NwfjK;A=9rn8$Bln= za{D*@Aym+O-`ApaGy5vt-p1&CU|_PPU1w$<;ie;zJZw}fH%a;k7Q*%Q_0mB{AC-^Y zw;s#c6Y7XXOKb#co3`ZmADRlk><2MjT&~FDsE?{0%K1V2!)ll=p(9yCXC7_!~_W zYtZ&mcx~;@V74&jj0-KIi-mEFPBtORQ0UC_2|4ou$do{vp3QUr-<0e-TmM+U75vnM)kkwyRBgqdWdm4eL0^-K0INPMOnF&<8KOK{ob05Q{Jz9~2N`na)Q-nPN=3@67# z+hfys+V(cw2VaJroCF+&2KCYQMIDj<2;Qvb&`wX-{yu!nK$#HEE_~%~p4sS7FGWp- z6#G55?R3#wyiP}DQIxpeMZKVXm+xl|J(GRt@&s)7!%#4>Or(txQiJ1&R|^f`O&8A{ zD8vA$$F!ZtMY%{?#7N+FKASh9U4z87cui}HxLq1aYc}`pr&|9@cwP_%tHvbkNFs*@ z+dF5(09L`8W7*53>bWcvnFDvX`*vXUvG)W>ydW`iS9skJBq1RI$rtb;bayNh{d>$^ zEMBdjJ>|aNF?kN;HM0iSNdT9-rG<_x>XC~~;&#jo45#~Ftb8GwK?r4iqI;>ybTQ<3 z$6?QA5ts=s^G!AH-ifVDpA-}nOjVj;qycEO{fk=b+#TPrcWwzWcO3sJbrU;W`I>!r zKuw*>FRw;4R-G%o{`-SAZugduZ7r(VnE$tXtH9j^zbN8=W4>Re#P5mzyRRqU!fWIT z;noxi09YpP;e|m>9)>ciFlKE*&DFa^>SNqcqU2d&A${cb@3r9Je*fpVr(o^Xpu)Iu z_Wnlcw?AzErKk4~A(*&v@$rk(&D;G`uM?>;1w67)lK$IFqB=pq?h9N)R{19^1~*iX zfmE=)47C`?uK|sPY56`QQNl=#e^0s7Wxkw)pa&{Sd~&AT6xkO0OqUdEm4vLCXYuh+ zM(G1z?9YUR0@-0Hc*4Lwd+g)mHt`BGW1+kwwa^|)o^&7BDqeS_{Jyz=#CC3;YG$wz zQ&(y=5PVUxRoz^W<#&I%In7gZARC*%N{efYWu;EPd5-s&r$@*U*UDd)$2Oa<{^U&D z+6$(V1`db!>t477WS5|bpls~m_6KaJ^X7XH7~Dp1ad2R~GoVg>v|Y+nyvE}ja>Wu2 z1>H)T`Hi_YMo(W~d<29ae6|!?wLCf)?*n{+)YcWtE-`0`%>;3 zd;MN7O3Ckg z`@aK*ZhDB9+3f!GiO3cVH@T}(kd!#4=J;;;A*i&qzM0rot+lqO+fG z#Dn!=7tPW+0i+ zzWi+BWH9%m$jUsYzasA4gSq=ddwJb$h@@*BmGrXVp!<&YoWFA$qK7Gn5d;-$aLW`r zU7L3ATE*ZQ3!f&fp;LT+F-s==E=_ujHYH_Cj9gUG(Di=#U3X9!th{jaOVzi#zLzXm{<4XJcgc68uG8Zgb9G{PA|mzI%vXK6>dO?Q?ko;`zSQ2|c$=v&l`5A43SI<`gene`V&(=j8e13ZdnYE$VPa5m? zt<`YE2}bz~hd~`LU-iE_KHKk+h{)LItxa9Au}6vaftM(lU8DFM;ZPXuF>ms`OgYT$ z4+d+o#!6^RiNrkvj-HkFx+@jk3M)fd=5T%-HISD03#*lut$kAbyE+H!pY+93un!pU zj*3Fl_HvF;@!8QD%@c=`J=hWY0WQ+*9ENI}iTAw`6s}R!P3r0t4x?Uq&B^{bD7p{8&Yj*Y*>1STL+u?}g819fZ5%i)IJ*4EY_ zjyv7^6{$|tN2P?8e_Nr*eMju(@t@r%aE9K1TH^GfMF{nen_pd*T750CaIPt+n#9-BPrnsuTsj=-W+KbkIiW}Uj7ee-B{<0DXI3SMDcvuvSt082-> zM`MhOwttSX11A!;TKD4{Vv_MY`fyqJI?=Ep@lqdHFMXSgb;s6NNQ8aUDHNNjw08A- z()${duX2kL;YSE>FV(+teUb5;cMl5ZZ&RF45LgU?qT|}ts~!t2aRf0>$vsv?zl-%ut}*sLd<_><3;R)u|(9Vm1(rT_XzR8>793N5g9KdUt8?ppPAYFna8v?m$|gyyHD>u4V>cSgq^hH0F{LaEE{xIV)X z^c}}CSVCm0{ru!+8eVaTLKBcyy5WzY8`G2wI)WTdf51dz)jF0bcMT^skVXHhV&$i? zKNp$hXMg^3psesNykvfFfyOAvK~((q3%{GT9=hjIW^?p8f20xumFdj@lP&)blZ|&W zu8T4f?R*skvMT{OUr*0h|K=`;xjg|3_vC`|HdZ}fI@olFW-1+mYH$#%&uKVE#-VM& z8Ti$~O_wPisq4*+f=rp--iLi4Lp+>VWtFDQ0No_zr zP;NvQtl4`b(>l_|Sec&qVhyU&i8RGRLt4b@4sVT%WftQeXv`>B%J7TVkbJ!I z+;>E`^xM_njH=~54CLaTk@_y`QbFstzo8Khh?v_#%kiHt<_ltr0{$65pL(KGo9x$< z*??a4IFFcK8qq^TyXop5KrM&1wJuhARSwn*n2?~hpzOJw)jEBse5c2PHp4n1O@ZS_ zp%aK_{d>-VPTKVXeCB^1hCvGqabU*qHE4Q{&^DAo5~brrNlzLceJlRVu?*%qp7)^e zm+fBo@QC;yy`pyzm2^=ZsX~yNbuDSZuA{T*a8M?d7QLB{`KzsJvz~`2^$B%&D5LwE zO4hSuGl_0;mqa^%8e3U0D}CbQ>t=si8+vrI?|Bc*rR2?@>k8#as^3Nk`)7FGK_Zk( z>=hPBbao1ExI5g*G0oG!BYUE%v&1_<&@5;Ii?x_rU=_c z-YZ`75PRZ`Xspg%&hPv?fScjU%fllmA`&H5^0G0hX@^FF>Ex5E2Xtfrki)?aJInHo zZxQD1DL=^GHS|Q*&*UZ#YmklPh(9xmF}Zd*SI@=2_%GyobauT5HypMzV8}(7%>u=R zEyF*l7UZW2r7BoC?$~1{sz2Sa^NDI>EG)j9P3(P9h4MPOM^SidD&`tO4CVoY-*hY0#S;6xmF^eoZ*a5v98mdcMmHDSile>?ALC z4XJ1mzBQH{)u;ruc&D$Y*lA}hE4VK18{(WQ=Z|~0F=D7Le23{b0x`}8P8{k+)`-l$M9$olc-(m?@GTjXVBFXQz{-WhbI?Bz)}RAGRTR18538P?|Seo75C z{%T_#9_z;2p)~{c475QGq<7~o-9`37ZwIeuUz3C*3HOcNwskuSt$@EnBH%F4lK%ql zv&T+vo`@k8jz0UekwA^LL&qqpS4a(ym{f`0%SB+r88J6ITa-Sy*3!dh#?J&>FIJlt z@vQ!ou54|^8*%gb&w0XKj-+%Pn;?l~V1EFW04O;)_&!^QN0I2dhu}Wrve*0@BLu8ws)CgkS9Ik$DVPubvQgp?$w)=(BnK|knrE(U;}53 zy8rmqWR<6<6R{Z;b)-u>j)|=`rE%}i0U9W&v3^7?!K5cJ)ke1yB|<%YI!~{0>UD3h zU!5w-z)nkR7#axRm$kOWS>1&`m(QF)t?kht>XmW#w4lxTJM}1F;2Rhic=kXMzh$8rIA9i$f1ZNvSXO>4pIqud%PnLIh|8~Sos5qydT9~FkkEwXMw3kJZ zhU(aUldb(XzA89eob2sH){7Vaz~vTBfwQ5Uir%j3vFr?O-82!z^(dU{|T%;dr=nG6V1M=Q*ISot8dlqdFUtphO^x!8Y* zkoj^QY3z=pWLz-lw&CK49?*rJ;AZ$_3s+@2_QE^sS@P4Vrn)#OtT$ju;?o^V34Ar! z9$-j=7y;#HTF@ci-vCyfCFqpK{4=JxvuD(V!5m-f-FUS@?Lp-RZ+Z}2sXZa(cq6u; zpdd7H8_-<`A0kCAv|nm?I}2lG#$}unZ>UiX%XsVF|Hcg+RQ)~AfdYHSW^t( z@FlE?b7=3w^E7ThUZ?GrBaAiXykIpV)0f18<8UXJ33@T$yA}Rt7bBURA9_ybu>}}b ztU6e+$*+SIoQd@GXyWu^C(@`9F_up`%hdnkMZi1JZC>jWoo3VaWuM-YMgFP?my2E8 z@BL%j+JC7+Qj_RGpSHS+Xibg6(|~V zvM)Q{ObQRbJRqsghG^GL0<$87wqvDz4tX~z0VUSy<4e~ijvjk%EiE566E(&PJ&Ds6 z?iaVA^BUzmY^mlAZ_#wrerSkNCVBR<)u4i&oh>#rOjT7C2*0T6AP-C(PFZkJsAx-- zzruNij7f6(Qs5xlssOYJJ&liO78)}%s%mq<2{t~SXr+3q))K58U{DR<4?EwGqhwFA zk-=zB043j?j8e#ywfnp9h?Pz#@&6lwQgH?GF>FbS;jTWo<_QLPM?dy8WB(0(CZY_K z*vm$tyHA&?9x&J!E6K}uftef!2L-=|rn)+VfOQy*p*U0rZ@}EOpsWmE?d=4i8kWH) z$Os)%lLj$Wz%Ay~Ggl~2V~z2|)cAP$o`2t#;GR+S1YEcU$$v}RdjUkQIzqM-p9pJe zy5A@mJCfLG+dInj`4G1ODx}_a>qNbfQ#8W5;2*}mMEhGE@4G1skgBlJMVq>JgZJ~Z zI+Jt!c7?s?6lj~Rxn)XLCU;BAym#*&1OA;a$!h6rp{G|1^9-ml1ZSLkF0u1u)X9rS zNZ|})Rsw6`<+lU|qf=be){l74;zk&-1{W8?rypo*vpz}oY_y(T9$r3297{8Z;^E^r z0#yO9O9IiCB_*%~%HT2&5ASYfmqh~!707i=Ox?tlCO5K82L7`=1_M?p+viU&rwMF+ z=XXYow6&8Pc=6{Ry&A1e_tw@9rw)oJ4)0 zR43J=E4sV!L?3H>-&xY8XLE0tj)kFA7boL&R{>z9w;oAPE(!YnBsDd~u4tk~yXV?$ zD5icGpFP~)k8axsO(EF(X(r5ot*`COb=rXMgLG{<9tUmzw%RW|fDh~J%>Ap>$J-me z@eA1I^YgECb83j0LToOW(xvuhWK=0&y?H{Q&;3rrKHjQSV5~RHTS6EiW2?&d49ePQ?@_{@%Ob)T0eq8D@)SMCeScsS? z3MY0jvc*Nfq3$0*#U*wb!EaKFQ)v%<+3Ad!UV%5Izp#SZJM!T8rk@^Fbp^elI%?7k z+cHBzpZe-&cPHYKGm!lMjL?7?U2Dq5LOf%>$5d%AM%Oj%$+MlZmhksCpoUXU-zg9x z6vay4%?Ycyn?wd0GRSG^=sJw4#>8HxA;V{mjxLONbr$&slBc9|^oJEc+Zw$!XH9Z= z&yj-!fcgK%4C~XBg29>T>7w3iD)TmrkmCd*O&H?AY0jB+jaF<1Pfa?L_Aa9#+#F0A zx$f7Gc6)HE?l$43`VMVMlD?Bl; z6XG=YJ~?z~drLetge|lCI4P*opL6OL_jjNwqf~Q@RKfH@?m|z{1c|6;EgZX4E0g^s z7t83y_mTeB?z0q>~TW4ILVKDWj{DT`QVHJWlY9j<7?eFDV>p6+RMbTs%IeB5A< zq;~nc(gSlwaU|8qL2QP-Ra4l^RNu|q3S`Wpccyo2$OH*oF^j>6jGNWSSEFFKDI2}e zK5}YnB=SGV$7=@f$WXlYM%Kgg>o>m8)Eg!r+FI))P`J{{W+fQFjDoJq{<0GK zv^7rq!JGR@+iOn4xii%^^4*pUs-G}WD3HtG2&TG%FnyM$pCx{B^8F>X{y$Viw>eC& zmRzmspOMtCQ-#pLC+l$tiCo^^`8M$F+qWvqoJh~e;XmIzOfK2+(xPMPt)IE)My5*s`&fCzINSf5#h*AMF= z$!xcL2DNG!I+%!hXA>C7Sv5}o-$VmzLkxp0@BP1FNL_90h-F-2V$>#K*&1A5P%bn& zk6{(sZ#z085-fk4nAjYU?Ix+9r+Cv^`iP6ya#*2)qfvj?Rm7%u2uQP9hFZL03Tm+p zk@vuQhc_HWxofx2poDG@d?DYhR@8fT7R1Q-0?rNgj65~H5Km7}3<$CRCo{uvVCC#= z1q-HsNkjXRt2f$hU&7@j`=!lUdF^vqw;QkGEh@Gc8=!B(3?EP^Pp5`$93w440#Z^a zR5rl1h<(|wg$>e_M(KgnPUST%E@B97%&rr!Qkt73TzIRlQqyR*HlTzmGWa#2T_rTEF&IM3|3H>uF6M zogME28zGTVLX0cssKQ6b8%mRagY~$6Eo`;T#rF1XwYPQr`?qy>o;sBY((!k-$e?$b z&XhYJjHSOKZnKCF6Sqq{JD2)fZd_pR7&17S-iLFFg-z5=7#FU)Q>A^&>&^;?f!*WB z2QZq-5p`2~K5f{!14yo@h)CP%X7k`6JoWn~4Jwmz4U+e+N`_TFh+44HeW$-o^Yf(aVxj3R8R zq1Bd|6OB<^NZioARhS|km|(IU^l9<3ZK%I(_)iulg%%yLv1mi1ouLvoORi{P~(cJISRi4`Espo57E5X^*jomnLSd+ zj=Itdi`cI#Hc#)^+~{OTlU(IfLX!vqPTi2tF#?SB&UG>#14S?P_2IXC27>)MR4UD>bS((4qx?|Aa=w9Z|gIU>|)^R*5GKoD82 z{Vbr?3!uE?H3~JnKf~@_vK+%JfLe1|+UR{VZuiAORQZa_aUbiZF?0KWda+2MM%|H$ zyf7ylki&s?aY%fTAYU-i$5?t-%5sV~y=9oR*q&0sRYOfJ!qjcCt#!?5izrF*l<_4d9n04-bvn`QI_Yd4*O2J<;Co?uVQ6bT_m*dqPUS-TNOv26)!!l0d#d6^E+@ zrE+jHZ7M2*11_nj7> zKY%+$L_~yQ#^v_qSv~=VpDlEwqB70+UvbzDOp_(dX+Cf%-~&;JG4(QMW2~R$-P=;% zZ@zio%Kw%{>dVU;lI4Gac(G)Mv6M4VXtBo%`3lrin)bXp&Z6RCSqxd4(imMNsZP!$ z$pY~l8EQ{#w)E0aQma`1!;o*vq0^VpjSw#+a|dI43y*?}u8V>Z!S&JS9KDZ|^&{bx z*m3`W5Moub-D2BLXNRh+-T3)|UK%}k2EerNb0gA9bs?DjlhacRm@)gSs?M}~v>z&} zTB>478qfQuMLn-NI=Y=M;{JOiK(GQ%WRm*PAgkmZ#K*ZghVw_;$f!^r7kBUo1iC7e z9#3ztOGrrtgTMeRn?Sq3p5&l9b4;_Tgn2qEg>|I!MEPmeGhrlyBe*#qQad3?BcEE^GosAXgfX|K9%4Omr5Ux=YC5X0T+u z+5l7;{{9cbbX}ur;8G?4K)(7CA<|PEPc)D8W1@|A+XvQb2qWOsgoRz&<^q)N&7;}N zM(03pU2=m}XU~+zQOtlTlgZrfv9e;~NUg6HrXrFYgzZL3(CI4*3OxB*gb?Rqps&vW zzGfbLUgGprR6CPKR?B3k;3xiTt`P~8I%%jf#wkf3QshVt7Hrh+acNy4%Bt-b^K6tv z_RgP%vW+vgXpp!*cAX_Kz{~0%jDk^jbDq`p)M!bpW~!FgW5dH*kN^dQ^1*cqN=r*i z_q6$U56pCgpqHd*Hn6Wg>F#?rOG2K|e3cFpI&h zmQ8gj=#0|dAs3~!)UM!V&~^e)-F9s8!gvW`y~Rr7kKzGwVc|%mSk|=58~bdESI7&w85og5>}hBFN5AU&po;A^zm&2_Fn9L~W;bk+j@JECFp;rwnEQ3Tu0p zsv@M<8Rn&LBg{)#!VX;f>OPP5=M?NX=7#u^+lu>VvE3!;Q^|kwvcH86iINxPEhBIJ zz!w=L%;WdSbA4&9qcyd63TM%^zM&yKGt=t5H=^@Hpr2nD3YcpYYGfoN3O`J+5XCt( zx_O9hvViM}sL+r^qn*9xKnk@Gv$tzd)v~3TUuijc3`-kZX9=45rW-n!rnya)GKoFk zL%k42pJcTt7nSetn_r5;+*-p92G_(~?cb&R{_`hcW;K%U!ZkT(zTRW|8;}Imy@e-$ z$rXmo2jhZL@{W22TG}vtG9seiGj%^eK+r?e>Az8T2N|7R)Au5I*ikJ>Do6r>8EykZ z4%oLm+$;;6)D#p2rKQml>k`t^ZNPJB`odkZ9&DNMV5upeku3u)Rz0=B{BVJ!Ric@~ zUl>b|yD}P?KBLrbo0d4O@rm32xtWHQG>mbYikHkNQ@CtN4r z0K3&Tq^qmi33*`uANd_mTVH?0uVqBT6I# zfpr@O>xX$GS%TOppI6}og#gV0{O((V=@|FUZnvvwn}gM*T2gbOAP!_j(ty~~e=`y7 zzK}pmN-uoMEb6Z?SB7c$%QY$|HPug=TaUbsjSPJvKgTti#VgZ_=1B+Bf0_q~#*uSq zj519!_~c55Cs59^gcF# zY8TI1cQ3u!=|AyH=CfA3+JkZyi9=Hr_dcN{j%5=oVmVM+$K`cSq_^85iVeF;c@-Cj zQZe4Pl$$#uB0dbagr*!TbAIci`(|H}`DSk865rv)+KiL$T491h?BXqZ6R zyVy2b30(4HYwPRJ{O08w`CosC7DLR0s+c}G(@Yy+CHs;b9}2P$*|&TY1XNe90J;w~ z5v#6|zW$YevJW4M@RHmk<3L{f6Q6LOA+?pR#2ep8lQ0hPcXoxl>#W3YAT8=*S_z;} z@HwM|M3|^Vu%qyD4^~r)&&V)@AsSe6t~}yBV4Hogq_-ngKld?m(5X_3p7hW%a(hT6 zIDc%&K?<o&Y~9g1qP63$vn4fqhkv8xJZtR#JP}m7mHCgr5DyXPbghKrJl~7*G+Q zvX6K#qX96*MIrX=-@bu{i}tg%edOOHF~gQvWN)IIgcnDn^c_m&oi~TsGCN;9d=}Dm z?%Aa%oULuht;jdn;);;DT>F#c!^O$r!zILtB=hZjP$o_~Pk2$FI`!8yiF-R!!F|jR zXix<6U@Wh^A%0`r+d|f|@8o{TiUCkhbPNrj#>MIC=~4X~;0#*OQdn>%rxzg@I|o!z zRTQz3rI_-C`VXC>{EDzX)(i0BH?)VZC||`}X!g}sRCEBZSpw~6a8`gE4I--9ru0eC ze8c&LNW9?(OS+G#X2n-Eb*D0o@v{!EAtHzQk#6_yh~>QbYA*S?32rlI%uok*91Q$b z@*ofH`5GLct11{@@~4=TPl$%t(UWzjURrf2kwGxMIZ+$y8KKrclQb1*i zvIb5J8E^Sh#UALm%`{Xtr)!Eagf;Xfp3-0ArNlQ&MWWW?G8(J|MT-uqchRQ2dBj<8Rop> z1dAg;Ew|aQY8>zzo+~|_yZBQNyxZ+(**F2Ob6UPOK-g2~#=S z;Z8<$k+*WpM~y@&9rZvr@_B(N_BQKvjMtL-LBd;bG?$eF7jhs|KDFo6##1O2Aqmdt zbPX2n6=c6tl^c*tnUZU+(mIl_Ci!E7C0s@TW)~(VCRSEfn{PWmWZiyJgAn56fA3YI z`n2}-dGqsTt_=m!F@pRrv$M~|hQeM2aH#J-F7I{&q9ascVOA%ZRw3`fEM{f!Qe7M* z{ngeAl{CZn%j4lH_q|qajT{)gC;0L9u4O3-@sraEg#C&5$BCYkg<_fXTEEEaj~`_K ze1+UBHc6>|%yWr<)$)1;R`l(~#O7arZ#-6M45z_GioSSxu~fqyvmlNw*LG-3uslle zfOq~k?c%=YobULmv)%GdQ`&&)=myLFKhCP>d4x$|tdfzL3B-P)4Na_Y5H%-{cCXt} zqHgi>QgA@#*tsgFv^p%PV$!RG)|WUgk%*u#ag1=lt7~BdiGV2Nva;M^x#SbXAfKL* zfi>cc0>h}mZzg{()v@edveh_8>dK3B{$?0YRC;z_u5o0qjiEW&U`VaL)yl#GNI{9O z*xAXics}dNx=yS1{Tr}n$ME#x{1@_4Qz0NHvI$B%QFCV)R6#Y z{#8Cz_D{S+${G|YbcC*tl|D#Q6Eudebw0f1q@RmavOGRz9(nF|n;u*&qzP{HTFjON z*#=F|Dc9GGR;qVB{dApqHG`JdhTsZ4t4)@O3#zEdR!4_o#SkcFq%_{35=_DccwD9; zJ^fk)oGLgR;IpI&WLENSY~>r2me+N+czR@YTe%Z8|tRV>uzguUDbkdwtVZ?4~n$QC59D%Uf&F78cPGbjvDjbfL<)-4dKnW5{8Rve{^}s&( z--+lK-2S7)b*WWRL}m|cB98i5EjwKNjV-o7=*|Mc18fPniu+)qS0 zwcUZrU;U}gYZY(h2^@e#9Qo2oHP0l1js#|D!;g5ERh}j$LT~hWa#Hb#S5Vi(gf(LA zi=rsU;aw@N7j3+z3f|^gLJ!`KW>RQG3xKKt2gJa zxs$wosh3|z*k1L>S4(c(gGRY=IrmNyAiPjsSL?y8CN^0b-N3mFLr-#FX|7UarGtH9RT*B&t1|N_qQqVqeQhod~y6T_QXJf#lJr~-5C|ydQ#Rh zG<~x?#UNZWtC84E6)J(gltG=SH4FjWHqMUr>DqS|Id6tjRY4Q}S6B?Cx@R?NBy%xh zXkP7o4=J`)o*5d`MjR!~5?T((&-H%#CU$RU2Q*eaciJsl1MbVpE`PBK{dUY~TWsqP zDiOpAl&)}o{KFd$p!fGwG#+EO{W=fHa`3`uIlif&eyV!B{?KKRJNK~}HNW8D)BJu= z=EyP4Amf)`rN^W3pR(VDXN_eWK8kpj#bb&Mnbjt`!SWvxBo%jrA3aMprt^56W*VpAnmCe}OqQVm_Vk|pN@ zNuRILeQ{MeP;XObn5*}f05K4W%H~)x{zXnN5VtQgKL+-3tT99_upft-v7xq{ILCX{ zCyWi9O$`A3} zn8N)0eCTz->T_g^q9>v9gTax`%a<>=FH`0*Z98C?S$qLt?mTOp3=`(=x0bCZ!nvGz z-`aoq{Wf}#hEy_iIrrTp{%|{+B}f?VCuPfegN7W66xrZ@t^LJGpNPXxrhojoz7{3n zZp7ZjRy_^R+Kn}S=giB;H#If}iiUlNzjfY%A_sD-l7qk)a@Lq-`*1PTNd;r-Ix4M> zLBCO*GJmO3&QN)4wXeyG`QBoqx)yCz@)%mkRSx7*=GWjEL8T>_a<|V;vD>LUZ=f-X z4Qz-|7ex~7F}I&mBvzL_=BAIo-hf9V)&x+pt~ zFZB=fp-x@kx|Slq#|p+LDr*(g^X2l_<0>^&jZcfgAkGj%>;uwm)rBd8vSI$~`4yn! z@!kj!r?Nc$ce0;9DUxE4JT>DIV09Qh;wwWi8S22|P>P~P&d`MyY=OYf10X_XsQ?8Av-|gPIf}o!gvmEKS);_L zan6eKZv^G>?<;WVZ`|UdN(qu6)voQU)0s6c8ev5u<>FMNy;gA8AckQ22M-I~hAj@q zv;DbOLAGa=^?gm$9A#cWZi>atjM~V|%zQ>Ks-~&A0Y@&WX!?n?{J_lBjNkbpiLM^) z?B&1XZMIVH63^?#*?aUj#O2RPC{PO{Y#!W6hio;tM^sec)(v6>7Jrnw#GN~=e40;@ zy_G#pI%>=*RP6amA z**3EmjLqOukUj&7AABQ2NfBK5bdH=UDJwW&z2ON0}sU!huh|{*HBgM{wfT_ zR$ptY?VM3@&;1wpD&%+m%iCN#hO=Ts6PGzY^ll@LVF3Y`Ad7#*#g>fs&m7u4QWdRY z=Zw}7?Fv^6L=kew3SFXOvy;-4*yfP>8wW3jyP-VTi?v_Jz?bXKUXZCSx-Q1?Z zhNpL@hti@N@|a-!39?9WF1@mpsOPfJZ;lvt0-v7W|7xPD> z9*O70@bTDDgKYY>vm3?U^MKIU{>#083HRk4lZWi?#WsBS(I4eH6_4*Dp2)nB^u((-UHaO1EEEc{=n`+JROjkE)tfg)B(X zV(EhUP)43TTeW<6q0|+8(J%U#L)RzDKPS*!=vw6H8y>wl#ezaZ(1CZnrS(oi`a(=M z1AT{oiG1fXz7JfdcR{K)x>hdnCmU7$F4q+r9=PLtxAeY5$kx^XSDe;!IKR(5d7;6L zX8sH=2FeXWc&_O{Y zkfNgQJ$niQ9FW$+1=Z@8GMkKpW5qLn0sgp@HG}E8s4smFERAZ&uhZ+PI?V7dxOsVC zN4Rzj7JLPJ@=<0=zU1H6{i6hRy;PexeS!ves#gbT3a^i4WoHxWgCyKgji@0`D!Jm# z8)6P+CB3RAVQVvWPQ|JVFm?z!005fRRy$w%7CU>L`%EpTzmSZIVV!XNr;n-Yr&RJg z;QcLv-tX33$V*NxfEvEC@OiTWC5Vp%&lRe_qBo|Z;`i@m1-$ntYQn+f{2|3%47|yKVZYU|*KGI9qA)X2$Y&Q6sKx)$9@%1j}lK_24Hl76D2^1gT{-VJ^ z%;@UifXm#`+xtW;qp)^>=;QuXq)m6c;HfFV6nF2M=`7MHIMkg5h!oa6hZD z_Zen!wJJKW330mJ)l^PP!$R8yeZfZiel91rSKxpgL22yG)Xp(uOH1j1J!epgDZOa$ z+yLnYSjB`~bRB;1xVfAVKd4ghk3~=PX3g}e!0WSvIi!$~5EIjcG_9!eV1*gJuPqY! ztV!fbmy=<)#Y=_fCm__BgFVN6Sj~s(23}}x`8YFFEJsHBtiCu&_Mqsh2c(Y7~N8wWvqpxXbRNGr4Txh&C zRw0qD?QCbDs_o>)t)zhaxZ(n!O$Gk%iV7%9ZX{o)rBz|Lxnt5v$`6*BOaAwmFi(t? zZH7^C=RAuZWkVSs`>*77rF!t*Ps$|`zwMZ= zUQfER`^xAK8w&YS_hQXWD4sz%Y)v?B;+%w(P_y_|;vV{n*B|GHr!`*Bu9-HZ{aU!o zqFY2`|KH;Jx={7@Ee1K+lwuK-3$I_COrKSi-($f#R7rY&8*`ZB^4|NmmkM)+BiYqd za7N1#u!#Vt1844iNiwT-pfVKddsgW>E6(FhFus&VYnCQeQW(%%EJPd(8GG&C1^}m@ z9jWr@Jl0dT`=qVqrc*KU@q^5;)+@d5Ukbv@gi#F$^@jTk7J7ik{4vTNBFaV3-A;tc zLNm`|-V~Ij$J$3B@mT4cABL#CxDxSHdMAAP7Ufku`lR^JEsWp_8i|QF`S~*~jpYyG zIJ4i0aGAEZ&RS!0>wo=>`R}h@QTB>vk;z{ss-|5-s>LhAmCDK{Li%mHqm;6u8=$Qe z6}4(}_q;;<_TTUJH%@pP8DDQVmA?KIOUVw`Soek0#tZfRdz@2&RxA{7fD|rbKhMco z)l!MUgyH5flO=RNddX3-uYL5T{d^K!W}HS|J(J+&=Vz99>^_ij3o%n@q+LxNsnyY{ zR5*SbkCILNiubnFb(E};b+W~6D>m*jYEe+=yBJ+m*z&v3?8vbX znnQ(uzS+D*5giU3HD?sv32zEb#OCH^eNAWRjh|k5jVNN3ya3!^Il99CKc?O~EDAN+ z9u`4D5DBFlhVGE=kP<`$X(Xf@=@MxHg`pJ*K>-C6Y3X(pBt^PQkZvTtJ)Zmh?mho` zo(E;#d1LRj)?N#HstS{8eZJ_h11}?yTg7B~JqO?zxE3J#3rexiBiscf$J~snOLwd_ zT^wbP0c3OLjZzxxXe^H2A2TN zU`1l0PF|C|{nS)uhw^3;sINCM7&S5_eRxk|r2WL=d@XzLN?EHjd`)D~eHsWMhWxp)iaY^%PBv;*N5OKf8QwV8o2NiE!ptM54J$RzL8~x!>vH)zJ=0YYf!8P{~E7 zy))fbdn;uY;!yqH(8s3Y>#$Z1`@j4zrJ)c`hMaMznIsj36&VAkx@~ik^{S>6#TdHm3J9J3lnGj(6>xjWUZi_!Z8OF>jR`L93WA@oQS@ z{%2a-m8RpcoXhSYw2JQp2QhD$e>QIlK0Y2Qu#8ddtQ|yy&Z^)**^?{vQf%!2~d!=o%TI~#C5&uiUcFrs_0HH!&AFiT=D$x~}BV!c4$Rt~(;=Usos(DT;*;;exE-&m_p|N|g#J%C4UEl9A z`gw7Ed}uJC5c`_W9lRn*_xL31{%@%Oj?00!iZqlS(4TRi+FQjwdlpGRIRJsbQP+z^ zLm%krflbdHxZxR|^ow)v-#621X?K1ozYb=X55Y~~Ai<{y&HtdJD11(iUC zfc5YdtDU1U0r5ZU%vpRvN4ew)^*qQn=Thzg&`%bDv>vp+wO7g4W7C9PU4P7dIkXUY z4{JD(Tk#9eWYPfos}P@rVQ!=ef1_3mi5 zjZu#2TzGgQkWu^4?u@xLLbqB^K$xi%KMB~|N0YnBZ~f}-OGQUb+3xUT3_O90c<#C3 zjh^o?7*$bLF((VA2nYm(mCO*eGV_29d)0M(Wk-1l} zP!DwT`dD;h0N{N*+j?c?B^cl^s=9EZ=>TPGO>EAG$&N_s8To7d=^%auOAe()=$VC_ zs`~NOeipZDZze_I)pS{ER$a8z7lTcZVoPw+!g>xww7?)e|GglXn@6i)=2gZMzWS>J zge;Xg#`^?!DcXcszsj;az!jUunPTyi{My!rsTx^%^P)&nQ}5EvUKtGn zL?KysjsE@#(w_aLIYy>ixHnha?uFMmheA)!9 zL=f}>&_gLcwgU%GP`~;5rs-(6qoW;XQz_yOdT;~&ssd6_qw$J}kUz%nBb0$5QjS>< zw4}RjQkOzJZ5mU{?LBhyD+vhfAA5U8%9^3Ghm4=nzXW3l5Xn48Cwv>sFsrg|2m?IO zdoM34y`0PPUJwJer_EVLI}w8)DxNugQO~G&CuZ9YdF`~=G z2Et*OB$4*p31>u!b@8*lzi~eUdt*;Qycxm=tpOYT{+>OHBs!N^Lc`dY`rZxJSk~=| zw#VYYu(yw>{?Pqkj6$G+HoJfe0H{}Vf1ohUczAficVF2tJAyx!Gxv4NtZE!=Y);5R zqNZm4R-_aa#2nnwm)rNMuizRj%eJ9?Z&*2?*u6&6aS=c|Oa6A}^p5mzq`*o4j{*tV zEKA@VXM9S88}4SOw8Oq6JT^YwHVv981`;}j9gwU5fCui0ayy{02A0EziU!4$jX2`y z@7&>2*m>HAiHqEP*M*60Npv4cDv2Rq@w<|rEAGJjf}hr8sCA@_hMZulK=E;Og4D^J zMAeSBh4Zrkc$%S5%ClA6UXKb7Zyq^X``C1^XXnl2W3SlSd^CLa<`7DPIv@w8wieIsz6C{3api2;Pe;v{%nAHz-nnFChH+ z1${;WkuKkj)_Q>IR`p^M%z8gc`TN9xf#x9+f3Pz1PLsHhYQ1^yAfdhHsRt(J_@Prr{&Nx=l(+BeM~J=UE`S{=jZ z-e9J4q=TLzDq|C3Vp0Hg(Srx%v%eEz4E?PJ`H>6phVbcEiDB~fSF_n9ywF)tMQ`^+ z|L~tWS!lN~`|ymb@H>xrLoe_v!)tq)dvi2nBdymvFs0I}e9}qMk~FEmlf_37(mpMO z?Ke2}WQ!gNV!e2SaIk)SUsRN<7P*aaTX5TOps(*p%6t{iQ7yHa(h-1gc5;ApBfuwF5Ycusscfly_-I9{~5|3gXOw%A~#-q!qnr{A-u@T?YG#}#@JHG zNnDhqcD_=xHkux;HCd)Et@jwNOtE=cH{W{0Bq$YWl+iJ^)lFVmiJIfoia2)M2k1YF6viOSlh*Q0#v-#+o zH+*GI2j8m>l5k#|pEthA-)=hBetUB{a8Y7S<5!03v%>80|7A3&%LkFlREK{?F+)M; ztT2Ji=m&$zP(n&xdu|PL-Zbz|{N4>Ly8RCuyX_%~BZZRI?(IzEK9z!9zb;-YWuCYK z+ujbDAr^V|an)T*FyVCfcrjw{%DF;}z0-;TqH2t7yc*{GSQ!k`!$M&#h)dOr1i!jaNUA+q4jqS7twB+PbX0h&! z0Nn6mwDNS>T6DUC$3Ir_elq0G*kx9=yzw|s`47crG- zSjC&pF7*%v1An1Z1N6m9u6YBk4NQ|zm-0!*|A06jdVwhh#mP9^__S=N$YWkr2Oc}=R%7+WiN7rRf zpL{|+GA#!F9h`jZEPz5tpARiH0dvxV5@R+d$1-&I&9LR4)z;WoJjMG9BJ@*cyg~iB!TwG449#p z=sO)3UPO`~VzH)^0f$(sbjrV9$tT}Wz%~?Jm=Z%W-fm4ac|fzco`8o*O+Y=X3q-N9 zWF!D$nN}2aJ6d0^$jwog^DQ!{mxXE9p%@H41Z?OvAms z*ReBU2$6G69n?P z2?wkHzQkNCZ86aoQjT1&S#zZu=o}^`7jC^#4%2UT?I;u;tid^qlMtsy zLBN6qN52Sak~=FvepgG_@5LUDA=Ot&>W2;~O<&)EbEceNvXy&O;Hlxp@{9DgUmf_j z0{h|S#^2_BGz8@9OJHz$juF!-<^AS?XGBZ~!8X@WZ4JM+G(Dsl?e_Nek`ogRtDG`e zUeMYBW7*TwV_ad!DA9OLUvwcQ{D{{5f1psd+laX;z1zylxv?8Cc^y{;xG{VYIgm0S zCn@RuN3Pu;i~_!WgNu?}hk}BRt`y?pU@A+gfdAKGGOpjNvcZoVKvK3_H`rOp;;p~x zZ}Vau!?4L&{_IVdL_a9meE;fuRvah2PXf69J+1w+H2t``-AIJS!>f{>O9V`rcqs7A z1A&MeG*oZVx$0^M^Tfy~69dEh@(+%dNA+_w|Cf#=2=~%VzA#) zr$0WDJ&BJ=PtcsOf9asvh@`@^+Nt;u!qmACH5#XSPNK2X)!MIGRbpgl=-w~GJ=fjc zy#RwkCbgH%@2@41TD+cGA!YlwQ7ku;dJ2J5Q*rvhs(g5Iasq{erjy4giI1P89gtUj zL;t9Rc8{yeTArZUfX-%4A|j@=;meDI9-`W7&_Ve>qoARPzYbqX*b zp58i0ntQI2V+WCf;L8jeXQlAXu}XXwE3hb!9)Yo8!9By)#8ViK(RwYh-9-N)fq+Xs z+!ZqI459`)TOG*n?67rAo_nv{0T+*Sv7C^M>dsdb5|J&-5!08+MOjtkKhm?0QE7fT z#j(_UZYAg8@2>T6(9?}2^VsXw9gh5|8#?z44`L6EL?i(rRYKF$4{iEf#HoX((|qi$s7VThuVOug z5i;0IU^i9))#FYQ2?MAbD*y#!&@^;8$*^!rVsChnTg=A$?B-HB98N|zlFJ1}ly0eI~unHpu z@4+lG@0j*iP5q-%D}2Op&Z}JHL>#(*wrn`GxD_Q%YvbKc)|ivS00JP09cRcI5%SCL zGy#l{zaBtr5eYkC791R$e;B;w%Np->s>fG1M6Q;&6m$S+JrdqXsdi;ZIr@ zfu(IU`!7%|`I~(`2bhK2Y$8AYCUdEMxSn`%k3|GHDr!@IPp}41A1*|w19}I5ukqV5 z^v}Ae8Z5F0zV@4Z;>53@T-MYDfyOm=mRRBfpqOiFd`CW6z%U!23pO9_(6z(S*G?2L z?DbLfySbmxeTLKLYuhXyDeKmOn*mEHaILT9F_CUcV&Z=SV4OGF{wew+ zKU=sOa(Mrz%VkgzKC+NdoF@2sOuB0}*x7p0+T5#t+suABeyo{_9 z%LoK5YO5&o$J2%bL(g+HGQQGX?*a70Cc|t@eEg#_!&0xgc06I!DFBVkd`3)HPcxE= zXYJw`4C^TdaHXNPV^Vbu3=A}I3i0mCD*f^9M4EjWan~!= zqG3{HU3wb{QElMIxG3CqY5*W>>zKUN@y_B_2W&W|Ruo|atUQyYT0wWFcv7Af4&|3? zk3BP5+C>#7Emo5z?xVFmZ~GT{)T`zdrrY~E6&{q<_?^bi=b4g}_IKXm47LmZDNvkf z@s@^4ySHEb|q~?n8&D)086|`u2 z`udH2N&eZ_@4^fzAxm9d9W!qONKh*zdthv&j|KiMZScw6Tj_uFXb2UDR`7Vd-mgkO zxf{UXM}&ziFq21e_cv^NZQT2B_#EvvFRe%?Ji&tYBzbAQvBxfn*SXL_uFueEolyGX z%1i4S_5=daCp9kf>*exR%E}So))*H+Pp-79Quhy4t2jhW5kTsa(yA7xkAAU1Xf(pi zSnNVE6Rk$B#JiqPvxTAj{Nw$nc$ZxWSn-v(^9}kq5?soTEJbyYyc$jWl za<=S{%G_Ytf*k=DBkTxD3JL}(S89@k)5`Ho7hWN=hOPuD0*%i(Mh~fKNN;}*pW{iw z0QZqYXti=0(mQrNEL;AcHNG%=EsWhH&v*!v{JE)S}Y|0&uR92Pss z;`ukgMHL1EIa!xv&2-d2SLsXit3ms~#%a2dmRRFG2fzo!o?PznR-$b;m}P_zv$D@W zLL&_slF?hxIW;%XLe3L9W0Rn)W`C$l{cFJ6(EXl=bGI0FdZK|sjE}f)>)l6Z=qR0? zKMryw=fu*43VQ#Qi0G8NQiBHf?g>IDaeC{UH-+>g&s4!#RN3TZLg2zzVkK_(f|f*0 zb$b|IG~js4$l~KJNZi1cW+L~w^o4R#0(b6F_qaR&Q{Yhg8^8ngtAFdbYe*qGEU~-v zYkG0eu++!Q%}h6o75V>=_u4wFSYfi8H?Pr%-PO?ePLtpyKoj0cchO-)aY{Qd0 zFfafkv!g)r|N3ZH>ad_CH`5tU-ESpmd>I!*`4Wi$tI)4Hq={CNcdh}Id1L;R6=+Z& zxPE|V5B+He|DxZ5%Z_7#D}9W62eL?uQg~R_hF)LPo3cw3%j;|>T@jqSV*YjY=Z9HB zC{>Bm;1jECK4E&AoQD>dj5oh9sK!WiRr#m)6v2s{5@P~60N`6=27REiN~pM?T#Ss~ zKbJlDAyvBc=VCtTo|?iVJb}~}{?rU7Au3kK^v~8Y$w37c1Xk17VV(LqZz^z8ZUrF8 zG}xcrQc4_nr~FsqY|5+Alm0CK%kr6_%lGBuE~$ zWfjUziVt>;x}vrhW$7!Ft2mFkJ~T+8l-pDD3eWnL z^2DAj08@MGV%ruQiTw+ebna53P=$b>d)F+iA#a_|FN8MdLjkVQE z^|YNCg~X-iFxtKfy2pDY`$}uj1c2p4BecK3nTYntp+M4h8S(e%sBgHH!xU8EZJBS; zN=Ju}=1tGNBe%+Kn;7A!2WOo`!pamI=PnqtBQBOgE|*x3Z5As91{g*l!HXwge>Ejt zRl5HrBN$-9^r;$wl|rz)#RKCoI&T%cJ^WLI^09J~w8*pa5|X6(r0a3|R_bOd?r+b{ zx7NKt@<&5~M+ok4dr1=$CJ=W(cC#73UnckGS-pgvF+5SY{w(dh%WuXl!4x33gy*m8 z3B8MYRYOCN5tWw$!_Dp>$vE|~YS)V$#QrzI&!g2(pDM^*+GqI*#{$g3MeGL_Kzv$J z`RJxlqVKpoGC?mrR*U+HmiFctuc7DqT3I?)y6lG0)1PHZh-L$+CW1;gtZ@4%ZTxIX z8NV{$cMWl6RC#%MKZk}+4%S~G6=-o1U%ngzFWU;VJ}OFI5T)1YjG^t33TQ`E;iIRU ze-jEgKH7bVM!tIaf_(Q_tCXQ({{oDoc5u8cx z1~(gU&67vLBU@3)B0vBh95qURyB;BSPv6-^BgeCb^I&`_jH+W)KL)k^o%6C2oY~kF z>XwQ_Z-Y-`y%k?XNeUgCKbfkiU^jkE392hA+lGnvd9EYSV6>3@I^A8(aXth6Jrwg3 zu$a{KgoK3AMuLecsB2~$4>!gk%K%JI$ANNWvKehz(FED3j?Q)q=IUMC8qMY-LaM!i zw;C0dQc1e^vu-u>i?;1v^ZJ8l=lUJf@sq^6+@Y5^#muFOC1p+GtrY=H_^Fb)B>|6& zE$}*69F`p7VBU8P|FKv!1|KP6d2vzT!RJ_fn;t%tp`d_({Ll^_yWc^N2O^-pHi~_k z%ns8*>W3W4@wB@znOpwfB0)~v{^s<>{O#4-0h$crFyfrl&Yy{-##2JwR$SpVuAI8g z))o@pK)iee_VtZArlzkQsPt6i51OBy4RP{?5(fq}%z*P$N>IS7Th2;8Z-H^XLO%L3 z#Edo?piPhG2De$iw5%;GC>e9TL^@kpDH?wlDeiU!#t!<=la4@MVRn#MPr*dL=egIpkmNM;4k!uJ>9sOwAmAP!T^kRj-g~s>Z zS*1>kKjU8y-p=jO0yRo)ZB96EMfgpT$cTud!a_ScJ8Ns}yA1(`2t-$B=fLo@0ed%f zn4-TWVQUY~rCaKAdfDfNi%|n18u2c)mk@P1$@R;LnCznqcwW=^VoH8t$h3Yg!_SSh zxYw@(*LwvaWFoH_tBL|smUktO(kX^#7}aI=!cS^LN*q|mIicxd8Xk-GJcVI>w2k}b zlxeqp*=U5Hti(tAAct?Z<-f6}&G-ucj1LYB%dJz-z8(c|1W#t%z zjYZzr?-Igpbb7)ruo~-%HCg=idl`7&pIzpD3-&{CiEvuE`1qy)rGc~}f%-EK5EX#R zPEn|Z{H~6UdtZ(`l&Xpofgk>i$1svhw?+=Lc{hB?4)JD3Zrr0(;#1dGnyXT`Mp_sc z8GU)!^7P9AK_XAVeSKo3KCm8lZ9K*<(-dK5hSRO6tc>91j;kV*;DV=`BGxa!s`Xw? zUQWK-n`N70Qqm3nuRWh)+b5*NPlvAX5=qiq4oBCj;HbWe7uh>8?(WD>@k^%|ZBU#-pkq>xNP4Kes_I?I zV)9O9bz_%2s)`*!Uvzipxm+A_(rg;MbCfx1i(QVq@f18#5Q$CcpG?MRfCgL^M_)P0EIla*&wH|i$=SB>E z$6;;{?1$`F8>V;eTr&gc>@`YY_U=QMzlWQ$z(P|HCd@PQUe(*Ij=fATQ#Wn4zno3` z?D))E0YVvppmj4wJo9G>*9f0Y2@REh{qk4K8{Ny`Xn={?0GQJ)@eKbIqR~Ip$=K4E z&OEq^z^;js&Eu}rv6_$a`Y4RA(e4-H>9}zResl}a5C_Mz%n^Vd?_^UxW-WQ zoFqh`D0*fgjfcl8s*b{ze5rq!WJJ&E;lmsVhcoGLFkLXa`NrBr;u^ajjI-GxwA1zF1rXUXI)a3Dx7V-X}?<(xS|C6Uk=<3-O1=eaCHc9|Yj^0hJ0!d1B%X z$(J!9rwIuOAnXe5J+*97Pc3VHu-Q4zSHO;YIF9G$#qjRS4zDaMt7(m!g<;1)6U+r4 zuJkKHm>skg5MTdBiasF$D0*m$K{WmD9qUn`*jIRTP)+E;!y(>wmXL^VR3}csl3&$s zoZ*DIuaG*N`+4#IQJ&QfYq}vC@nV+tFQ80{#*m*ER+WlB;-%gUGr2I zyzIvy$$B_+yn55?zOtDa9c3&d9r0&Eg9yrBK0@6+V%r2lz$uU1EIgBLzU&_QBujtT zBw(!ab@aG;F2~1|l6M4NM*BtlQC0d_YYyQqL=x4&7wb<^-qn-~XHX_$=~XA2)H`yr zy5Ua5(yA zq1_s{>osYtz8`_?p=77ThG1)}+7^HXxWaH_XR++2 z(42!o!cW|;CUX{H;pFsHxZ#14#D;i2ah3}#KF}@^USQ2L`}V_FqwlqA>cO+XpX3vr zp1zPh$F=$V*_92_AFzW5z@Y%<{UOVXSw$uL_yB^P9qsKOSz8AlZDBko==ylP{IjKg zzr4IW*2@x?dMq4Skz%U6)17GbAnY8F>Z<}dr z?qQHFgJ@_{Fg7_kwY#?Vg@p^Y^Vp>p);a8<(gPu7w&s<)tj$ENvYqdZ?);r?-HE%r zs>YErs}K)K&;0F?p|{>_J_FmM(igeN>H1tkaU8@202w(8FjzNcv=z=G_8puxt5Qv? zaI%!Sj-3|na108FA*P=PFojsQeoU(dp8(3|fq$WiOXD%_Y%?2iYLOBs=G{YKoBi$g zyzh-Q_7wR-szcMQGJfwe;^Ut5k53);KXGN=gJ2CFGg8mb^y}N;qTWmj0IJ@?Efh1J zn2tzqG-;qDhQunm2Hpu|goxg)kv=!@kbH6xF{qJs(0cg%v=x?ecV}nj*GwghbBgcI z_`N?l0Hq9)`j=;MuRI3wm#-wD&j{Ed8qa2^Q zk|$1xj;NTL? zLpz&vol;N<^v1$___3B^S0t7ZGEtOEHZud*ndZm2o(sa4N`;|fA)OsSBy$7g*)tZn)f{B#M!fC{ z5-1Q?=2%l4{J70=d>-X?cZWS0`Jr|4)EhLbVnZ;tyO!u>ix7yrkfHp+PjIP4N>a(< z9zLOT3)d)Oj6PXvb69@qT?4^~PRQ#3^_PmGV&3p{JM9IC|7Hjvk-~!H56jYu%GNqo zzYo2cl8Q>J8w3DMwI_iSYg>qgrtryym8~rfIrEO>o35gt$KGhpcE%iPiSNDidR*L9 zGI1&}SB4+*@Uif|St?xC;CgmXPfvNGj7^}Hq@Gx(^R4b>2$Y7%R;&=K5^e4GB-GR& zN=v88ZMBsouEli(AI;*}C4;(k%t&}qGU)G1!S0@BX%wQC5>6s zjq`@qyLO^!7((C=IS=grD#6cxS6%(;xfdMM`g7->Z7yqU2n2ZI;~&x_VC7bT=~ZV{ zMJ24Q{LBys2JD;%_wUn)INbP2z|{&Pmlpg#3$*E#Bzhne1uB&njyJI06a{H|2)Wr~ zvWQxB?oUQf#Id118ohpbniT7<&*(L$Zlb^Qs|tH^onXGaD9u?P=a<7{clS^&SEyE^ zBSJxbb_oHuKjQ#6IBBAbt9^v+Z!AoN`JlK{Q*FvaNQR{OUM2Ze?XPD`GMa#!nznRbWN2gL7ahElrF68Vp^*{%!!ldi3%W zh@@J0E^WC;7{dzZs~^Hf{m0$P@a>>qpgmVjJ;U3mJeZ8IMe66u@bCh*uHn6sb}7^L4(@{hu~4f zst$dE9pcwn=G(#9Uvj9XW2@vp`@E_f;aJHgm?nR{A409~)(um^<-VGw7D$Rt>YU~~ zhuUFOcN8ay7HwNU=A{IFunfIe;2O`crqqkbrGlYikj6Qtvqwud{#^?gxVbBY)_ydF zfZ4;CY<@p7A>otXuFcl+%m;Y7n+xNpM80)&M5yvY^d+2;Egr|=;tc3IW1g~PXn|!R z2pqON`A=crvk+z}UJbZkz_5=A;oqvDI57?vSE${u#D;21&EX0pgnFX?>+?pXhyno&v zt_J=E9zsk5Q|M>@y!G>5;Hvm|jJSQnpXCqZD_z$2Lhd+3(u+V2wz`^HiAgPq=Z~O3 zmbVKap%Ume^xs?g9`mhOF1fyyyj*np^&z7YD*91>&cdyNee5jrXLmh%wr0k^pO0&9 zL7)AHE>)L_nOV(}LIHIuH>^shvX45@sqRxhD7m|%)Z2rL_(kA}zE7T{y^;+%CwDcR zdEO-Z)qQDS8u1pfnER1hl-H=Da5#ze$lJTfFF3$7EN_^t7Xu<0Kmw&;OifS!!mL)& zrKrVtS4wQIivt4;ox``!(fYIuIv`$k_4Uz4R!oCoQYy#b(?&c#9E+(&{Sf&oRD&8f zI;7OiV?gXheDnkX^3q_h`*xSq>TU)!-G>uQB5N7_ltcJDjuE0FKrFXdKpPtC*Qt|> zMjKVtQ7f5mq0a8#c*D-RYfW5Y5uOsHN*y+T|C7MRoVK&m9I&t-ky|Zw7WZQ zlG*br%CMCq!|_1cJ6wj68}a6#^}A%wf-*Z|`i}3^gyn#NPaM)Q;wCyYO-o(<+sH_b zLsOJYv1##J!>J-P{FGP7E<4LQeQ>@j3}BDn%4MgyLLi z<+u#LI|sQC?+@ku%D|&7v7JrkCOkbl5ELZ+dpJEi1G5vw^qG?BERpUwzmt5VE80?e z_fN+A)?CApn54|`(45YlNGhP_np3INQu_gL6PE}s!EJnPiZ#v4^q+c7Q4xk)pqnU` zZ<{P*D{?mx>t3B1Ne8XT{cwvWiN})^aNFBR%QRNEfTAtNZ@-D@YAh=(TmdddHSqnu^k{y95iNPZB0@;RI?5UA=Q3;9W9qgC_6 z?7W;a^?krx8y=GpzD0?SI9ga;iXA3U{O{9__82m$4@0b8?m|McUBV#$@=shuQ?C}I zdL(&82VE8m9^S$D8R0r@$L$eTv=vt!DgWgaQxcV?<_o+mPUCpREm3t3nF@IgE} zf+#p==fuCI8h`83^%8m-nz+UcqJJ8bXX7@BCTZyWJvy>KyPL_EYWQ0g@v29wv$M@% zgb}PO;KYJ#1wADon)RL_sRHBQ(e=$bQR#44&57kAs-?smEt38d_a*c7?g8n(FMl1s zYfY@XhQ{QKwac$N8cOkCHO+&Lb}2;cDz~^WCeLhc_9~%f8fL%6surkeX~7#2z4hT+ z!UVx2*ErGlH*GSVLvMldQpS@)uYjNjsC&y6mMm{luc{Tf12_f9PPCf) zllb^+?36x#sf6S71#5-eAm~r2g#GmD>vAl0y7hxDtYT!^r1({Jt?ymJYqHvzl19C7 zNnJe;3NY;0Sb(#IiA^BK{W^gGB3FS2z)`|g6erS?9&3L_R{E3u$Qy%hrer@u^FzY@ z^^PzBm5Bd=p2E_3Z(@6TU)^ycq^KU{-X zs4gk2J&3x~er;R#6Uz#KHfaBQ`fsSo%nushDDK_EzS9RFL7Qb^J0{M7=;eT?$BwVn5%P&5-v+nv3 z1=h)7%GsUuZUuVM6X$b_y*1jlNqEI57m=e9Bj=HdO-N%vU!dn11s`UW@PB?kbISf} z7Gn&LS~z~jnFrdHCz1BoQeM7~>yUf{<{h9lt$Dpfh5j)A?>B3V9(UJX(AMmepM+oS z-n9u0Tt^oH`?$Z@k6j`!-F!-gP=!L-zn~U7=Kuaf+W+(H`M!{K7Y)BsaG&;n>ix&2 zX~ryNJxWy6@S_@c?%+#kZ1(E2Se(p-I&)Z{KIi0gu(w}aT2enLSEYbMFhmQ>(|3u;NLK6^;j3{c zZQA46*3>;Wbv?h-6O~biZ+8pyvad$eJ+EgfEaP>9Tv^cTKvJ@r+xW?sC;$5ygm;Fo z-|_2tR(&ILz&1Q2POtLiL0iNk<&{z7Id?pve7sjUBv?ZDGUSVw<>YLa{y~2ulk!{Z zm#;nPoH!x2cOB0hw(2mxs#PS&H!wf;I%8_u9o1E@sTPvj8J*MlB@FxvKr9WBe6i1{ z>`#mS&%Zr39FlR~c$PtVMnhgc$8)8Cg-L?BF>JqEW|{2W!Xwp_a^ZVeBEuY|rgg*6 z{=uVpgIu(8z4L!}Mvi;c`K#{m=fmaxlH964?$|Cqd8*6Q9;|-9x_{GNmQUdJh`)*@ zu0ydSxQBlI_Y4)}hr3usWB=bZXdMnoOBq`HlZoczMvK=~dBqS6yj;hQ#$g-Yk=uV> z8(?5PrCMX)<>iGp63oN+cz9CwY-ex&?-ya`RC3*nHV!eO!JolRr|+ijW@*8W#YPsX zj9y1tSVvM2F>!DdAv98jIoEjM>;Do4OF?xh{{4QE$6$aY_ucCL7pT+bjYR5NT_(0F zQXDq@OY+PQtV}z>sIK@gG+Q8xG*=5D%OVO4pe@khsrz?0a<(gi9n6+|p2rRq+ctjl z?LBFsF4t8@eP0PEJ#AO9ZPz#{$4wqg|7mbo_WASEDuZelQ|7Xbr>-BrFDxw)Qs3$Z zT5*6robBI+vSTU9&dL_qIvuDg*k>A?@^C*@SxX++k-a9?R56+HTgQHr<;)Q4{C1w6 zRS<>~iEtO>Sd3B>*UgNt3FgRAC@91>yWJbej=28subU~~&oIeP9}4E|rVMm{`}t?` za*=BvPSGL{%W5)HXwHbyo6GF4^wde9sI4~6IAW*Hq$tIHf2VK!QFBd z6QgDN3j&>2rs;>!D}K;T%4TU&NRExcdVNn-eecB&N4HV2&2rsH@sFL?Y#$0^RhDSq z!Mo!Z&wd;8K?Z0VHpZ*3KOhXY2N8NrL@1cM8fSoAF&u4J3Pt2hL(;r}4 zFav)RRWh(tAR#d+m#ko+xOGCuC1k>%Jg)FjE5L|3ItLq*v0SoXgKC6=a_e8Hd7CgP z56RwoL2GF@kkU03GLKxJB6H1yI!Afl@qyJaIM5jQz|N7GP7C>OMGN+2mmVE>noOK` z$EE!q^8DUHPv~y+)=A__f65}+;>Fxa06zZfsF=ix%KNI{I3j}Lbjn^ztjt|)PyI;m zAroxnir(qf*@dJJo73&ToDL7%6NNj(nJ2ffP}SW00(i{mF(Aa-YDLiGqlB1P^P)X8 z^U<%20K5cqLCm(Wkt!KZ&iCMh4iB2D#=O+1**%B7U~+CB1>?hSRjnke4f67+_xgjv zpVT+(Wj{h?hl^Ogk-}w&y2r-;`--!(nte>}uj%J}E`wyxK+k#I2nGF|BG)L$ij#{= zN}xvILGG;IA{K2Fq;2TzEvB3FAm;WyMsBqg60?`^2z zc_d*o`Z{m_Yej7G=}R+I@ui@hOmWW~ZOBwi`wCW=a1#k%s&P4kO$Vi^TRBJi1P>oK z$IHilv8OS3K2uSB!%Q>N91Y)mKiWv|N_Ht1`DIj87TtUy>((#9{XFtnY%KUdy}ae@ z4>8VTMm01Spt5s|MEkC}I@{ahRRT0~+hlll5QfoP*={csM1+Tf&7ME7m6-?CrysV> z6ev_Jklk&hL%hFBygfTzE!nga_Rer72=`@7osR{1 zBGNp+OKrT{K+f6{i?_xP~!eqE*6yV`D4eY z-(mdnp6S#=M{RrPgXE*0O4r69P2<=w+kzWO$N#@drS^A4rRE#2tLj(T{9RnUJ>KwX zK-pRQ5H1Lbgo}t?;R@EGps{f`Q{J?irv_2KhKTk=x0b{>BXusN@$F zWxT#4&8Vd5t3RLD&Q4+S;;|_Xrt_Flr208v1-IA{LX^MKK7QJg=6xj5$vY5RRrLk) z99$#%$+RD;oksKwRI{q!a9`&N;4!@XR*6pIHq8&dne=eZ5UiW zSI!m(Z^;bCbEBr5{JY~;r2iJHz*KRHhy0F&S3DtX>C3KK95RxXuhu6@tFYY-Q5BVj z5w-ux0@PL2g`Ig!a0<+)ul;IwEW2*+=!lS&4FSs?pa+}4%fOjuWC_HYw?K9SD0Xx; zC{pzfY zI@-F0X$?=1w)feOInQe7CiXi6^dF9COYP>Al-#@v<65zO>gQmj->=^2Hha0AuU?Y( z^*bA1PeHeaCm%ZD6mU&n6wgN|ciT@;US3Jd@p&r43Fe6i7>MOyzv{25#G@l9Ay8~% zpkrk{W~}$YUNKW&9>V7X-Jt@pii{nG8)l&P^Bq9KpnSG-)KJ&hbN!PdK<^{IgyCFb zH}E?m&BxA>vh{_oCGu8+Zt~&o+sc)RIC0TK`8?nH+=HK%$XBoae&=hdu_*&=@e*4AkqymGhf^Pkw*75A>g!$b0(#5%gasx+9)M+cn*YbE9;MsIwa(~4^k z;;UcSIkx!r)_D<;_up;w3;$S@{F}l!mG$+#mM~rQA9prAT^RQ$qAM`^GvDIS^Vd@5 z>zJf3+6<{fG6xhJ##K&~G@8=a3qaUDTx91zzFvIQ!1qFdNd&9`)mI!YPzT*jJUSS^~PJv=mY|K2??*3Q!DqvY&p>aCkK7~1CV)E zR4$$zpZVB*1QM=)_%fK_hL1spF%v;A*uFfh5r8K|N#c%|TZzin_{^7^KyZ$Nsq|0iXxZ@azF~EKfntgDLIDvzQi%qwTPGraNJAxq z?d^X*y@H?r_U&SteVqF=!;dXpG=@cHoIUZQbuvAI@3(`jZjB(*MrzvhBmaljDs6H% zDk_~?iqPk(**fB*5}gIeqfWQlu7i4Ec6O*b_bNU<*v^CDq|5ZX1DnOGCJj_CyTOC0 ziVU4jHv}j*tUzQAI_M z`n=g#1R8qGXvXdZ_YDub0BMj-zi$%lagEP7Qma55 zqV6k>KxjZz2_+?r{A7R^5f?sYt*+Jo5=*~HA1YDPGgo;$ME<$j^M!YYx|7S8kGd!{}6e^~QYy<`ygip``kCiZGvlWjv%k*6Pkw``Q zSvO#WGSoFBO>f<5ZuKi~x7C{k5d2(5FeAb69~3&+h*x>ZffA=wL#>zenZ{k7A~Tl3 zp>+qEB%;b}(2|7wKSMnZovMt6Rz%ONOy6K z?9jVb7HWxAUGJ92lG+VvBgM|gwbfuRPfKvCqP{;N1?T6t`T6(PnD|<%f4Re`3!E0Y zY7mCN9r@tw5&t@juWj87<9h(i5QGcp@3v%A2OkHA9BRpjSjK#Lt*jrAA=P4H@;stu z=k)Z8M2#DIyQ!;dEcu_s|L7SZw)<}H5-G;Q%6hQ+Q^V9$nl)0wfv&R_P5Vu4_A{@( zG6H`MKmhs4ygl0pSU|!qN4=e1dqWXbh84FV@wJKI7A!L&Q9TC5Xhjna|D6e z?JsjFPAmJ>6{7~#zbdE0-hcZMwWuz$Gjv7Ak@`0R`hvHc?lI}!3N9`iATEND#ZcXy ziCWKo^(k;mw_Ae$7d`gPZDAa`5IW!B-{eBltE^0jdBJDEA1HEV@BlK^e7_q!<8`?1 z30}llq~z)`xF?dG>xX=ieMT;noXkp=0)&KlQCh-recyMQHC6c3WZ{^aICY3t43F!se!MKV_cI z(n)x&p8bf0LrJ|c`Ww5=8*6VhO-&`&y>2N1g0%Acl$;_+E)Igq_Um%*B+dc$T6qAd z@5vt7 z31I*kQV@_vP`XM7J6L_@y1YH^9kIh0&v5tXC;SAt(n(R>|-W_5IQ{0^f|QR5r; zQv|S@jYJ)_OU${AZj`#;u9QlCF*#X&DROr8Pt2%DgRT1aOQHFHAfK$k0wqmm*0YX> z*jExX?LR&!m%D!OTBLP3f{erSh^41eH_6(+Ru?Ln2a9>{&FI5H zoAe2uN#<`_v3w@NHc%9-D-7hLmgj^Cnn*L<8KVFen(i$ls;WOif}BTdE#%}e#kXDm z8XJN`1*h{urjhBmp+tgL`&{jtPnEmBoIOZ#fq;UiW@GHn3?j+ogYbOsO8Os!A+Gs zE%YWMBc}FLxplEZzV#hqnYk_ELH(9*bU!aA@7eh`qNmjrH?P+RnPSge+n^Kjk>mY< z|HJK3%LiJF%yfs6NU*ww9Dyfe+ck2|6qo*m+cgYCPAn%OBE)De{9ZNZ&iis5OIIzA zt_i>S$cND!Pntc27I{0ytsqR@N3fQWjV%vG*+6IWt*Wu>lYLJ<&6%m%EI+R-h^f*KHm@)_EkkIyhNfT|wif^^$@6?^wHfXF^ZubY?>e z@sH2Wx6W6|@2gajp&_Y#6Mv{f+#L4LZ4j)}=+6kbdcQBYQnqWZguwN@jkH-|K%bZi zVi#&lvA$s+bq?it0(-nmCY7U%XU=J?Euc?Z2zA_xF(P8gBgz z*zt^7j9kt-Qu11YS-c|R2J1;#Ew`=}9OxJeVDJtlrkTCP^8+a~CCcP?j86Xbm*6sJY*u-$!9C6MO!_ld3)i{Wjw z@qaE{P!N+1d^hcDxLjropBa8x-j0sELa)P*bm}m=zr|xc6gbXLu%Pkf6+(V?{_rM^ zuT;;Ti^FD+UW8Y@FMO_8zra>l$yfPV9F7HG8-@-R zBGlSDItZ!w=HZBmV~C{kHLWP!evieR5gVCZJF};ctC_l~L)D-i_kOB1WfI+B%eOc} zlQ_1`)zuYboaq@Enl!24>Ip|7JQHj;Z~o|{prix?-8Gc_oghbqsHC_!WGB14yu3l; zz)N*{vq9@gftRtBnTlu1#mN1)?3Tw?59WAD1*z8rn$~{mxk`jqyzv-NXQS)P_qOwY zGV!wF9yKT?Fq2UQyBV0Mn3|NQrqVplv~!c-^UV5XNsFYGO^&Kim7w$_NC-!rpSR;55z7dve0)oj82jsHHb4<$pojTmjWW=FttWH?;Q&fyyhL zhoH=xQd_9JoXa{OSpfEc$k>DR0W&}~K~uUM##BTDmSjamP$j#A#SOyUIEU_UYmYKK zC`H>0vN(=0cck|_v`Us4b+h^!o8=zqX}}H?%eJ-I^(p=kD_yC9@cg2<(|*2Hu*a8R ztpFR0d3AsKmmUz4l47*e3tAE|8-cqROu3OY^AbKjRhBU^SpfNcRb^#RaN$&+&Z9@b z&%gNizEO9W&-3w7 zA4i2Z>3&}(=V1F{^ug&f&V?-xU8kqKjt)h1cazsa94-=UDjxr#quy|*U=s}D^Ymv- z@1A@4mvh(P2e@pN#af>-s)zoZ|0{8mlraS{wwS?3k)!5!@jf%MltZRx59TkfY8^oY ziO4>)*jsXIC5DhI+{z@y&F_vMbqo^M=x-xWwP(EQ-=ArQe=N01Dbb(miLWLXYjEg? zsnch-3M^e{YPG%3%l5Zr+XJLj{Bu1`JAV_4wtM5s_zBg2@?n5fTF0ThUaJh>&{CG* zh(aLIL5``Srk0Jk4X#dN&NsQ!3lyC)TI4;fdm7N5XkCSc1GBuTF<3t(yEXIm>)DitT&YvM*@8%j4*HD3C=Z2XujM}A@Nd}H!8(=<2H z?&0C5=O5U{wtA^QM>RijCSKN@b`To~Lq4mC*Ljd+(D*gl=DCD^fa18%V~oL|4*P(W zk58)x$HsU$-_wuQ*4FYE)bGP|QLw^k z9Jrl`LTdo?Mhf!sKRO{0%mJoAfE|Z@8|-<(bA5MzIVVhS{1`Eo!RP78c318^j>e}r z0CBiLHAkSm);!Zs-)5eS7ufyB(G1!at`CUbCmD4; z`2-p}`}j!WqZU|DcV%pZ18+%jGHiFW$O$bSCg-T#cK*6oE5jv@mu$#}OrU1~wnsT% zE-znF2(@<}2e~516d%s`n>VttQ%$YZH;Ksw4EV?2m>j%uf*}6&Wzl&z>>yM}>~cGB>B+iy(yVAm`FyZ1jOvxA^^geu&Zdaamb5 zFfaf@_VeSVn7KwzIU0N>;&D(y7Zh+z>K;EPBp{d{3pGj?p^##R>B{J6bZ)y4N5Vez zP@((P*A6lfA1#Yq*s(ob9JlX1q6;Z|wwuLh+x`kXk+Ygl3zQx@S*y+=N$y7M;rf}?@MfZ>>rB-6ZL5^WMr*7!HKNqak67#$+$18W zTARSdtmHu#gNfMhNp}~;XV2dCciFB(KLM>Je8HX9Nxi(mSw}NfgmA&1oP^|kQIP~J z2%r!r$BT=L!5SVgJMcApsI8qu)~A>F=bw$PYkt85-Bel0DIV#zbWz zu+2!6Oz_K#7C98@a++>1daxhVUNh`HvlGlW0fvih%U{1L8}Ff#L)Gc|jM1XIr9|kZgkEGR(Ch_0iToFKN+>qcbRX`zd^L8w9M&M)GP2i&+vqy z#cr%41+AwYPKu`GlOKmk_=hDwHp3qj7fZe-d3oR6M#`T18U`0DMNME>A=Abv0BJsg z%q~b*%^QCjZaBYq5kO=MyEfHQGvs4De%x}p!SyEm)DZ+FUgwxa6q}k7ZYB~SR1Q+RK!P{!SUoTa3lgL@kDy)R5|3{}t2sj&| zBLVS`Fe^tj((0=?Om5yI(vP8n;-nX?rA6=`C6epn)#d~Ogw;lcIvsD%C{$%4P{9do zE>~jg&W+93P~R__vg5v7xtVmi+;*bUO0H{Z#!uY|q&ekHid-tr(#H5V)MCg6C36NvW zt&k$|d3lB73H+B=%Yv6%Jv2&a+=kVh{i{}GS)9|a(c9$Y2{5GvSwi9cqnf z#@qq_gw*lv9Dauy$xnAsVQWi z?Br=x)M#$9I|5|)(AazH1`85GjSxYUBC6IsA>L9xpBII%aZA_b$k#bI_QiItBWL#7 zG`O4^R%SGKH(lIqy!hSp)sw?L8(megOyUO*Uiw!~$vn^z%63%T5mXz>sk;IGaeO|e zun-m64eOj0R8(w0j~4EH%irH0LeU_p2EH3pGc!@Ysj&fX7_NLcYG83qlUD|DmyF07lFElC1zSA{wz77zE*L{L1R7j&ZO=( zqdGVQm+`^nFC=pV`QUCm->Ed(s@PCUBPpdb$*4}zx=nxp`oGl}71~GwGt`fTkb1OV zZU(FoPQWhhYX2N>W6gg3od2+GP@ z{FcB)GWZw5+<^N{6R^Ph{EiJsZ-76byH>0nGPkmVn7IN}MAX1QWf>_W8jUPtpgKlb z2=P`(w~K>u^7-C?JRQ8u@}Ppg_>~vAgs1=QlTRm#Zz={pI|rT4_@|#VNCeC%BG+)@Cl>XRp;61B1Ej zM(>6xYshGy+rB6wbI#}Fkr?iH)#1U5ad4vdaRBM3Lc6%QH~7nlw|yIlYQqIGR|I-LQoDGF+Vb_$JmT`*GNi zPY+D`=0^V*E@3!%Met@e<7q!L6kqt(IVvKEDd;IhZG!{5p9wfR@#}3?v}e<0RJxsM z_$$SVD+L=Idu5_A9w|GiQ_QYfzyubbISj@qwgXzL1J2omfv*rbHVx)~%=iC5EYS6Y z>z7vQUD>i}$=s+?NHW^*#uxX|)WIOxwI{Bwe28vM`}6!uGOV4~iiXb!$;5FUP3kr? zKF(c6o|flUk@SH%lZoGAB_4Qk;R=7htR zq|_Aujzc{W z&5F_$H%l(-R%*AnW8>v`lYgZPMfc>6-hP!u`qj+d-m2wm^n1h#$>2lP90Tq?d$dP* zlSIg4%dg8gnB}7YxXlk$R8=X2fV-3ICZq=t548h<^0d;L-*HwlMB@I77o~9Z&}@Je zCdBK3XmA~7#AY-`t(9XCN!bjy=tTMC#9B**q$ngBm>uTOktQ>E>WVp($=N1N1kVX| zC5Af7zf4ZvP_zf+8vOns-m?-cJ-T<~yz1Y*^xE|GivFt=#FW_PChJoiWh}rF5D3Wz zp@9`;WwId|cbqVKp`;FRA@F9nBLa1l?L^;=o$~hs_g+r_Q4vl2*j&9FytvrUh*}Yi zQ5UcpV&2)hf6x9?_^!Qu#1OERqbu`jH+N6mI-U=iGwd-sCU z(+yz(2@2?z+*FA`Y&$i7OMFN%I}!6DfJyxVfEJuN0%L*63E@kFffs8#FVra>C-y`0 z*K7M0DtLQtBNS6ZFb$pq#AV=*+=aSIGv~X?Dm9mMi<7bWN=Pg?0^QJ)CGX{ReTzkc zQ6G`n+I+xC4@@jL3PIUC2&@vZ`X}rwG}(LFuFrda*FIuqO!w#C2n_029+~a>nzh<5 zeHju9JDPTraQ{UOJejMG=+ z@GI&icsATDaX%9wi*uyPQ|aPvcksMMV8kWY)rlz|{aNFmokwMb`dol*0uA#a7-s>9 z23rQ4DiB~pNQh8DvMo%GdTb2az<6(X^?@4`p~$Q{j9joHufss zRf6LXzW&cfhl;^`bx>J{{KDWn8T$U~h59PVu8sfEj7RKKrv>>pcso=&l$Cbsw|a&p zg#BCVRvc>$Y_ohX;Hb3t&Z#CzG<>%WYG8Jln$vUge?!}g7nMzP5@IbGBKlOtbAs}T zXtg;1ADgQO4krz&1FkV!P(w20sph&6-hTN+^*w9Y5NV--V!t;3jFdj&K!G)m2Nt8B?l-|qhYt@Y$d2E=rH#$7o0H3s8Cm&-^^ zXXgTs<@Wh^h;)_yM;{5S3@*tR&@k0Ut5XH{7Hw&f-Wht6Tl@E}AS2~-Ge!hXJaw+y za~I!Votwq}bv^m9jae@G=Vis3c_|!Y0iWO(>R=Xohb@w+$y?x3A&V|~dTmoXO^Kh+ zv+?^?TBIlTl2tRKzZIN?xJrTHB`epD{_)a+oEfvm{swy#F{=n8z05tdg&kIvqKA^Vj~}aYJ7C7z zQuzuRvy#V>$kK4J0{RLQD#`$m_x2K!G{gB$^$I4b@JYNuXdRBi_f0Xq7DiNnHj|EbK;Uhn7wR zEc1Px)8#e6?{wUq`EqXvZ}qavn-`TUi zImeQ_BqCyV;0G0oIAgcpG zGkpIs;~9wSJ`M|vpaz$D-< zBe}+B%H|?z`U#&ONx~|)NaGqabPoXKi^9{+Uz&h3QD)Qs=u{;a=2?-x2tU*V+(8ai zeJ&(Fo7r@eQ2U+q(gVM3nGN5V@w>3wV#WR!BI)@Y2_+GV*Q%Y*o)+1QhE)p+bgjW* zF;M>Q;3R?J=A%z-`IQhKm3$0r^2~bVvb9btLP|}F6S{O?v;5b&@2v&}C&#~i_|}n* z9e+E&w&N7i@5p^PSr6x*K4e3{D>5*s;WwWwEnN-CuNNJ}Wie zFehyfGXLy~)#OK4{t2{JpK+NuI0}H90hc{Fp|jOU#051N?~@sbvV6;$@D+S<^ zO_k_6u{VbBmQdT6U93!0 zb3yqu)HtyA=ri5F-*?>VFrfW-ns57R%mnUuQdSc@aD;~O+KW@_i>YmJ)uk&;aLF69 z182k$Fs`Z?+qON|qDh1Q(-#DVaJ)bR82$tT<*tXKhCsu0pFDX2yby?jvQvHie|Jz; z_BMN(ELHT3+1oXv4mo9cgG6P({8Ix3lTA$oVwb%czF(Fd4=y}j>cz9b0?=1VjT`lp zs|I(?oEl?`YLtU!0U1V%@jX|mFj+8K zF@wPE()k)6q|qp+VkJgq^O<}+lo~a! z`8=MWt*Rvw>DWnSITtk*p=o1=b=ABS2E8inCibA5t}KHddbxfc&P?JG6viL53TaZr zWiaC#as&b2v`)E0;PN1JmbbTWY_$}*NtHR2pEL$`rUBh|=_r?c1n1q_MKK4W-)DUo zJYID$-urmPmQBuUVy`yuOYQw}w@KYVbnd6HjVgOULX-R*FgW9fl|CS#S6xr1&HMv* z&@9_K^&ARttAwlSB6U^RUCXPOK!*IHVoO0VYC(`7r?OImvWlPI(*YXgsHXlO_SVhOsHz2GMgcud}h*;&7sD}V#`5nanZUL{Wl9&^5d;9{Q4$|yfY+y z{f~^Fs6rw8qnR2^;ullj+lj3_Jtv#grMezT{G|Oh4n=^^yE{%K}|zq;vun^58?D-tZe>!sNdeU4VpLb6d@!etP?%6G&j@LXsn(|BcXh< z2G+5yJ|Ty_96Ti^0y#;5mD-qbU0ReYr7_ss-Iw^vel6B8pue&#s@C%lTXZ*^mzZo| zvyPG_TMBSffa)qMi$fbh0Pwh8mjh39H#e~$^?+`OvJxQW3+wAtWGvqgUsa+F%tE`~ zDEV4U8H0w2sJPz$%FS;H$)`!$FFmDNH*y=G=?I@r&d87k;S1cf{{Xwh*%|tZ_Y>YO z8SJ!>bF*bcv`%YYbno2k`OxY)soQ!X!yaChuqSe(!68hVY@x1An~q!RqBXY=kX0&P z@;ed|>}=XbXOKpDasH9;d`7ovw!1g)I+_l-@=%pKykRhF12dq|92blV9F4tm2oPA#p|lNU06zZR+9$oP=! zL6nu!2#kX9-U4IPRriSBvgc)2T0@|+z(oFf7vO@&^Sy>*F-Bv^@OA`RNKEH`F zlVTzE%dNj|`9A<<2|tUkIpJJ5azT`2L7rjp!)s%nP~GGtlCnqhMWnHK+#_wKa|BlL zj-5NO^T_Qw%5$TulR!lbPbfzk%N9ky=8>oEY$?n~jalIwKbVldKeVtd#Y`_4D}XYk9$a zC0}`ZlQtr6U@S7mz3JXmhAc~H5ulv|TP=${e4K#LaVn$OYl1%kx97Z@@w*JsjH*U$ zVx-iGr~Yo5SXRn!B_j36FA(9cb{N|)&f8@J4rxFCa;K<4FLS)Qc$|GYH$q&TPh95q zCOLT=J^)Bqa`GSQ_flTJ9z`prt*_s(ogf{}sBjAUC{P(dLRg{U`Gq%?p0~I6q;3Eh z6SkuyQEe*8kHs!-;?o2^!6}pjf6jM8c~$p|E|R)y#XX!R@T0qxf(!z>8!-4rz5&|L zNhhki=$RSU9B602C*TRvyjGNFHSoMY)K{+oqH0~rlfMe!58KwSHNXTH}+No!kaAO9IG~n`%Z8_th!PSceO9f5dmVPQxw7CD? zV4N^~W`8||@9x2s(JiQ+U==(o3s4B>iYa>kFHA2jh&=sf1Y!`-!-1m-3vC?02Fmw0jeZSs+cz<%I-uB7TUu+4}->Xtk~92waJ(-lozwSFe{-PCaE?8a=2X%lM#sqpzth zQprfxcn>^h0QHu6yr+VO0&t8`*hLC{$naL7f}__vJ&gLhb$n_!`jf8OdVM*We4oNi z*bcsj+H^wJ58OqnSg)g);OK7aF23mbNJQ+BqS4bLGtrt^-0WvH-Vg3m*ul;%WT$%CA&2}agP0vu+Z~MLOvmR z^*w1U2p$0>1o!ioqb2}yXewJ2$_pb zJR6G)BcKlY+8u!E3X`8beTr8G&^9{I-ydJ*`@4}XH)R%|rjq{$%pY9W5A%}YIUN|g z#xIzTXe=`3_e31ch3o##eaNKXUG+dUV!%FNT2K7CD+!4=&mKL&w;H00xIq#{YwH4_ zw}G$5$1C7s9vT{g;rF4B`l8UTYiSJtL5PAo*7^$_X(0w1m4pfR26PnJuv?ssH`7s> znwZGaOmT#mu&5gXwT3!0lIS&P@YO{cJa9(G0PJ8kY&!ZWCWP#7IlL!$VqzO!f8Z*2 zlrj6*S>W(JmI=y#Z9v^}UvF=>mG+BDD4!}@gOLCHi6c-Eoy#t_F>UaUZ|$?}`74hTjR&UZ-+ z6lxCtskxbZa16PrW#jcN!ip2TVTTiFW$Ag}Y0IMJwnJ(54=OLf@qLFbZo`)2&K+Gf zwO>P;(zm&D4!@|YsQiSJ1Sb9g(BV@Aw%-N-e^iRFou}ss6e4hM9A>h3C;}Y2;C2Pv zgs^1*@|nJWlAsr6AJU2Jl5B0-;2#5XL zUlh8fjE21=GV|$G#wL5o>YV9aJlp^O@4as$2MwpWvIRA zR-2aPbo&}a7oZLjQa(?Z!3UV}4jV+LT|u(SOxEB14_M^7xf;55bN4_U{l#VnAmgvv zVJvZ~(gstkt{{B!W!kH5(|`U(z$-*-!`IQ2kUtL7f57gl+rB?0hIjC;O;3mFlTTBr zg2!~e(9Qu{8@^SXii=(vCEQd+S*E}JZV%(p6>HpC-gnL+j}kz`6JTNgig&w@RP1vr zA>K93bjg9?;j}wWrYb79Ft2212mJ~*0aa{*fhK$Xj`xpN1C`1JU5r;df~){-N0x2{r84{N+V&%GEJ3w zy2o>(pl(^c0T3m1*7+E5WUb!3=H%qWtxhjs9{%YQ6sbdb_rLJhF)2T;K}o%uE2}?l zZN}Pd6b%c6kYgfchjCYHWME94NKDVjh~W(q7x~JA;5m-9$S7@$l=y(P8QcD!Y%5I@;$zj;MjBrUebimrYL&7zoOuy znmMYT98f=0_qs~9Ql~6MBwsuKdkJb);HinlsFiU5VgCnAe)A|ycrqFH=={=N@@Oi@ z9b(tG`g19C|5+?Id;!*(>SG=;9J*W0@}cSLNfk(OSXYKmLX?y>Im>DZiRTE?81mpu zcso)_J?8fYZVOn#Kxql4)bh->s*PR;9{|b$I`#YaYzMt$7@7iM5l9R`{yEeMq9W8x z5eUH0(T6{Uf}kJxd@x3Ec5}-`_%djlvJ`ijJpSZ5-71_oDMnU0G)FRXoP4KY(RO5q z9Xp$7$#8|OTEa%M=VYo;VB;;mJ24v(o%r>(WeA6+_|HQ$7Y4ZC&2!#Q4k617v29b9 z#b|d}SlB9-K*}781bp{mo6gNwNnZYnNqK9u$qr_hqI02*zwuJ^X!shEo`SD@mXmir zcyM5VTG;kcc1zqdNDkT+I6boU_#WsTzQx_L{{mzO0Gu+rfA2myu;Q7cjL7l(`A8rq zCti0N%+5PMoGB4|B*HFPT`BT*<06|Nh7SsF6u1LMPXt33HTd9JVveGUvYc8yUM|c)~uDPYkUAZkSv^G^kX84 zq`HzrBt`_N*EUZ%gs&${cF?&{9f|YSH8ph`=O>8e4s$Q!|AUA?>jz;S9=}Ej+sw;Q zYKsF#))}d_PM+jDQtLkl%$=Avq$abIUv>Mm4Gld6k9ib7sH-OyI)WL5XtxSp%4M5e zBa@h}zsnsI9K7Hy3K)2IO&qGD3HIHWT`JVq%m|&lP1F-*XaDfw)~oT`kUEoZFKPws zn4`k5a8#q*4b=#crCh&}4BUHqPRpPqzgb+ouV5|Ob?$ZD2Njt%HfFMp);2Z>Ithve z4Y{|z!Cu&e0G(6}f}6j|NPtUzpc`JXj&bij8hy&f3KF(rM}~8(MQRV3ORFEO#(T5* zRBR3H#~e_yyB`q+rqJrgj&`@X0@$m^3YvrSAQTn`)10W7PIW*1UmC$?&P{8|ay4hU zVcz+T#y;V(wV!*IBR-{Cq$3@{A2+1EV{efv)Y#eJNBNtcoH?gzM6Q?LeW#(n*}FbT zm)x!gfBp>p_`wH0hA*c7;iICmHTQ7)XtosGqY}4@%aX?62|~#XEIj=FN&!&3Yz)0` zr)B%?>bxIlI={p3VV1|80#ASe;=w>rT>#e(GhMg1e%_Tymx4U*kq}%m;%%Ydzkfq} z30CXhzgkbL*3kb)X_}XvO-8R_Z+9(X^6S^|vI)@^yO3^+zh9PG6~mrTg|)WGJ!3jd zV!sxyt1VD5-zRkinPJhaC@8jIuQ)b@f0}0S)==Gmm5GoTr9B}dB@O5X*}VcEP;?>2 zxTU9H~u(z=~IlGe2 zB2}ukw6c;5%ht|r`bYZXKj-w_vaJHA+$T4=*$t;0`o2aMk&GDfpA^#3sY|;*{jKP} zHg?cjqHX#QPRLepFL-{_dS6x5tJd{r9He`uq);=DAQ6#JGA1NyKa3YTqrdZlF>As@ zL!%E0bWr+Uo*q1()OD(GWMEMD_-Jc5!Qrk|iu+}Hmg(j z9ilD;K$r5bGo?1FVYZ^u#)Oa(ZW~wBf$K&4j|Az8^Gx4o^YVs+pDS+nZ?RX*Lu&!< zh^-yvcS1>CSS@`SCKfKTsjTs@llJ#$rS5+9iJqPwhzT$+{5#)1M$n?<93DvT`;{F? zx1~MYx;x_gL7ooO2i|0}mh11{&RtILsq1AJjbX*0Dp;8gQax+pTP#qFKzwE+c!&bE zvM7jt0RD4QZX1t6$nHTK!9nrkHUwiO>&b8i8e)vd(|jM_Oyw0B6>>YMz!rHLV*J5k z>&x%IV?gr0?Owkd2!&!(Klhfqm&u~O?Uc9;W->vM7%fuP4HG3pc%lBu;jsQMj~ogP z=4$!5Vy+k1!C(AVkOoum`8iLs>&!j<`>nrElx)u)fFSYBy(hZ!t?f0Iy>E{mTDzRl zcWY7B4iNRL{3uJ6z(~;i+A#p&p?T8kP-oo7QbVi6)_qkPuE)E%@#8aBUsOw$AN0-# zp;Rqvbr2pOB^sTS1m59M)Pk_87Zw(PhX$-mN$3o`@YbhlN`Oy8MKTt{j%dtSbzj`b z?xw`Ouwb5{3yc7i^z-i+{cyEeE58`CsMb-=Z=eBi*sKzl5B|#pVA()cb-*#m4uE+N zv2=DB-GZ(BhmMUY8;g6*g=26B$Vq5S>h|BvY@!~{bDmnsjATK_ql&e?R-k`Blc6l2 zx_RTW_0XZjt~n>me;n=;cPvuz>n9d8Hs3k&4ue&B`+kDv!lO$;<<)%$t|-df_HB^> z!LpcdYCnyo9ZQG4<+<#v2bWe=y^3)K92r~$%ilE=0JO2i1JOjTg9MFHZE|UwG3T$6B0C5B8b;QM!71titSWuF} zcHV0adaNPKD-?y!kJ0;<%Wfso$QU}Sb)ErtY{HZ8MS5~y_HS+ zd|(vi}sm1(c|~p&v&z zVgXk5k){aR7Tp63*p4@jl3jBr`~Z1$CIVHb-**rzu_~>FEW3yd&9@+^~{;@hFbA zqC`3*^iPp@i9(te-j_x4y@32ET;$Ty5-2PNOXgsfLb5gMPQzV#_d;e4RsI|5=jx`s z1M+6W;aX8m?@1I&^Q^C^4a-Y=n?{yl3>H&`Gc$+lK9DVYAC#5J$ibmnq8esUtF&s# zn^eBpC@3MgnLdYP?=Rm@IoeFS>^skYSA$#}8ZF9q)coq*`=I+?1LdOtVpjrWiDD_o zgR1445Td4UMi;qBB$FB}-A@%;d3_=Br?dd{Fe4JgSvnksz7rG9vQ>J-IO=JGbvY94 zGgpaoc(|ol5sX!tr`+Jgb-ztYZR6(@i9@D1AY>xy@Y`=XW^&(K-jL4NT_$Ux7lp6Y z`-(<%#z?=GXNKTS3-jpGub?=nci+PM7IGh~!kg?~FMvHXjBf|q&cCv={TMpyh=Wy> zp_BZlaIN?-EFZtLH;9son@c}A4(-1^i;bWB`|Z7V8?5fxSF}o)Yrr(6jK~OgkkFM= zclil0~;{5>;zp3O$vXk9WwC z|J4vh?dOFhTZ4Y(4d$tP2xftl8dTv)C0zSkIM@&9zM#?%FWU$WeIPAEx_rE+0)`NV z3$(DW(YDTMA<@*&fX*!X!0QM0>+a($P!SOk2@1j|sr=wiShqR*d2`e5+b7X|zmEzZjan|6 zE;XC#ef4#OTDzKar$Vq(ZVoLMl2@i_%Xbb@d|JXzcV+zbe?M9g<`@D417E#Tkn7ie z@2GTV30w?K%f?4XtKU-;`rJ+WHoEtm@DbUrTX9SfAC$C1!^8Vf{|{#jd%Eede|ow$ z^{d(Fxy|b|N8Ve!CzBq9?|;=c{47&FH~kQ_sKI&bj~>@i#9z(x^E~x|(Z@LV@Zr8D zJ!|nZFfy7O94ybtNiPb6@0dpnqTrQpz6IC+M^80lT*?1zy=*opg`}XrK(G4jEvxj3 zA`l4BOcso_wY33!zBlI>?&bDaTTA48AI~?5U-Y5b_{p^o(LoxVY!cn$zRnr`e$1(n zgtFJseI-8gg9-^sKj=F@J*WRs!$J#oC57+b{{|>$Mw(K2S~CXmB80; z&5~m6HIP-G-NhdpdO!XG8znZhd2`-@j-zEOvG zsxfz!_0^wuPsrq&AF6sGE&aaMBiA-7KX>MH$`Xgdn;n0in=^X&&;|7B<=B`5H7q`5 z9y^A_Q;^zYF-ixU0>;-5yl+%34X1CpS4#ffygH{Me0TXy?s5$^16|qAm&1o{vJ^4Y z%p0f&N=YMKl9M9Rs`stJU8?YAx`j6&1P_=MahO*A8v&Em=mi^VD{H+C=Sg1&_T+sy z=BR~LZaAx!aYoFKx7D=xr1qJ4g+C31zeowQ`frqlz&p2e8*ZoHM5CvmvI7XA9>ebv z0zO%uue;Q}S1rKZt_}zLXtWF=u%>IWr)oPEJlx z@A7`Ncmni%s9Tiz;d0hJ5|e-JxGxuXDE6W4k`O1;j&Mu_lPwPXISv?O1-KF6;oXDV zs|HdgF!x7|P46zq?QbbR3=^kzUK#i(ii273v&dG%O8w(FO$^Ji2BDV34Q;bt6>;Ja zOdw7S)DRpBA(S#Np=$f4!(wGy((p!U82iI14p-~9cJfG^E00*1Y0YirIhh;5an?ES z@GXsJj>^MFk3dGLRP`c^ayLJXNG(^J<1TGgnV)2VPAgu>h^~vN#j`RTqs$v53lIMJ z2}FdY=Ghed)&SHxA5Td>XnD=L=j(+{pa68L_~xNNOBLrya+!S-FD~zVKgd|$W~=U?>n^?kBg;w&)C!}&Fj30Lp~v*qhoNph`! zxeh*{j39m3!wcvR+&fbREWnlrf3A?pHth95;FU`@Z|wdWQ5#_M{%6NHa$~P9&rg7lM^3EbIp3yx_FrEEP4vuE?Cw(^&g> z)B5Mprq|3!I2Xl2+~>W;wq`W~gRU91nCHp}nA{7x1b+@>mn4Z>wjn!aLp~V9M0A)M z85#Kv$qnP}Z-w)5a&Q3i`2Ms86hk0oe9#wGc51H7mHUV1lGV9V@?w7`mk^e9%=IVn zeoHltb>GLQY+{OT=$Jt=1uA6g6*v?7cQbD&$Z04tzflL}{lkZXjEn(u1SAKrEoNaD zAK(@g7e6DeS2CM>(LZjyK?iSV6#pn5v1>E|rAFyk4Ctr|NOBrrZsO(E#>TgY&EZ~5 zUEN?#qpPQ9V`BsK*zdP2Sv;_DJVGm$GH5;F#&z*?oa2j0zML9E?M?61wWU^jYz@}5 zp2Y2ZpN%h3BrXjO#yt0g$$5GK#bJUIjl9N2ABZ-9HU>8L^Of)de0Vm=(TX(jcO_e9 z_R)s6_kXmkp27dz=1h8|F5X}`WwYFobY$B+S8zkN6Eyb7(9ljuWVq8yr6qmCO#r6Y zL2(MU-1vlqE9?sL_A+zP1!%gyPwjA|@$!7f{vxYpQJb*(t4Dh?b@yRYOuye~@GIlG zR`#P?5M1?1`KGwj;uR%o7TXABjl7(k_Q64I@caUw44>tHKVHcl{Tn>pwHqHSq7 zJ!|Ee6ULuSXZ;jf+^_KRi4L-jSH1}bNN}L`_iO&7X4!Zhq>)!p@b&ZO&#kSDl9FkE zXi41u`$dqO!>2Ny91CM8JTih^H;w6Id|%yu{?~y)_6yr4h6m3OpnU=i7h-hqz#Yi6 zGyFz&EfSUp)twi{fc}ARZEY`!79|{Sx6pKqNYWTigKaJkLcL)&s)+$^zza-{-LBY~ z8)u>39R?2Tf|bL%hK5owqGNBrg-^-D!D=So^nS&B{x7sQFog%?pHuR`->hFYZMYbA zp07%>i(Fn49-OJTnKEZZ(V3lJG?zW?>TS8XxS&b{kFRa;gVXJ!((+yxPibsy1cI50 zhNi2%{YApRZ`SOyr;7iyLGvjryl9&e*im2HGgoZP8^WnsSKPK?dSC`dAHY5V9?tVd zSyzoxHatxoM1N42LJHgB;^Lj|e=oE}i}cUK+yw2D3{)-k<EF(KvOOOoQse0VPe^8*RVHG+2pcieZk z=4ac!aNSmtH~8<`(~-#!`OjvVERg~Oe=aVlP+nN6ob3O9FVb}q03ig(Xtux-#nLj5 z)cJotwyWxYKlW{QG8&ramX@QC^$TTS_5WVmzb{f?2W_4iph*DQHa1F%Jd_{4`hPdz z-xnFMlaZ5yd-!mRzyEc?duXVOH!eB_A?JObM1}aZ_+ihE)g+2|m2cgqYOEHH-|)Ik zVO*=?^`CB2^k!2F4i$uUp%6QJ*quhsThyj2n(;m8yJP_j7<$|>s)=p&$cXT8>w&Be z$fd={!+C-qgJaV0?F!qJIUC=jasmP2TYk?eP!+gtp&x86pz#AY9bD54DPLP_>z5GP z7&N}TYz}h_pi~Odb=Qh}=B% zz3&n0ld%>$b-X4Ai>Gb~7znq>S_X8AUrV4J>PXV|&$6lz0EQ}Kh`&>0E=O365la4) zDPE3&cTJ=uBiP#ik)hiytQCsHB_awVjE4$aON->oer<@hv$ZwHTm(5U-?oM2`gTq{r%+^=7Akd5QO zecb}E@CWf=&-epj@v6o`!RVbU+SCha(Y|&I- zlGw9iIt&C7H*oj7YPB$U>GTA4-{s5$;}M+DPZ4p$ILuc#57!5R=hK+vSoNPi6?n7< zgvrlN3~*x#Jg9d2r4yvP8W7haQG*-rBl25hMmHty9m6TyBzj*8(>8AYmg&r!>@MJ^ z7kkBSQHGv?vV%HyBb zsvltZxxQ{~!Tq1xr9VW2nUXMyvru`w~d##GzqvSctFx!%#=ALGC+hxa|Q<}oj_+X^7MWv4v|;R(k{F*iaTQCC+7h)$IHee>wix!)1hCgVlaO|5}B zr=+%x^A{GBfg^Q?r|)Mk23Y?3b0x%+5ExMAe&81u2ZM}JuuJvyliHfP&&le4ybz`y zPfJ1~E2@)c9@L9}-F4){ zte~nWJu92o{4gJ>bFbV-Hz2ofPYtp z-zleE1N82>Q7c1Oz767E91bAGP$`;r0vg29Qoi~!7*z+D69R5fc5yShrQ1I5X!9O? zc=+K%GqMGM9&@Cx+%>=v9N}X=dIZQKn*vj2PS&RWrBUh@1Ex-*1NS%qAC0kC>q}w+ zLN;u7;?h*ca4Z)r3j8RFz>z=$rcGj#8XO6wp!dHNCTx9n=L3QM12d%AW?wbYMD0EQ zi$4s_@#-&RiHpx*77v5#M{h5}fg7EVmKH52?$R>Tymb3YS*AIEVI0l935`_{BRtT7 z-TIEW+4GcdSN-l7`JJ$yhvUTe@aM*rzqx4nxy9gnAUsyM9WIHkI_nzN+f|%gKvHc0 zYBwlSE7H1KP+fDY*Fj`tWXs8FDqt;h&G*mJdjPV8x(k9kLcz5Q0VDjS1QrskjGUaf zA~#-q)#RJ-Ff=rTT^wq0GgIpfJ`-o^SFTpmNR?Y|AS?R?85pKXur2&KAH5Kx?J=d zQT+LwKK6tGx14ZiR~Hyh`W{YL5%w|$K!b{A@hGF)O=hF}9iaRql64QExUw0%3B?sa zVuJ_^9Bd8Tmxr5rP=z$OZ>hqaad&qI@rX$(86~BXsE#}J3e1eTnvv%`)8_xV>1Ch) zhSHE@f86hWfdgjg8;?jfF>@Ecb)vGC*%$1!9V>Q=_P6sL^V6qvW#=~JE!%5cQ+G#H z!fGtIkJ{%I5da$peQ~*?946;hUzWKHa;3mH-1;FfI@{kEiHwK?7gL=2p#Vw0^WRD^ zWzurVR>zS&PyGG!GpbvjtZ(9i<32|ua5FfhMU|A4=yF$AR3KIL8niR*%vITbRpi(i zr%H;g&xo9yM@rBVZSAcSaCmkQijFpv3Orc?oCR3y=F^QLFHJq@0q)4Cj957!%-pacyG`K;v@lf#E@+w;<%^&`an24PZj% zbEdc=SV9H!ue6?qRH?$|`t<1}q_5jLIHbqN%ZpZ*mhOVZ6S#FgDn)(_YQ1K<+VRm; zNv*}|X4AlE7}A5rcu;f+o*y`1%=(Ji;3nKzdleBjVs&|*tbgz9;)TMzHUgER^~L;- zF564KSnWL!Zh_=PO;qIW-6mj@GR@1kg#g?_XPv8)JCo0nXD%x%BZ=&QGBJ;aD}!)B zNw!-eBHpL+X*a&y5h;h*@(8ACx7}R|1N^=;f){Xx!omS{`ks`2;^G6}#INziH;q&6 z^qcNWCCHqK35UMyyc!%w8M3XVjWwZd2BNa)=srm1vc3Blh3KH=>SWZ5X2X~Q%!IQl zqtw6PrvOuNBs03UEq2DnGNndr!DI!Yw9@i#`K{&bEHE>UvInE$%Y+i> zXlJMytI04YVX!XABb@fOW_uWEh~Q6=`M>*kQEH>2ESm?}mS zt;6(&e<~)DFG>vW=q$nTKvh*0yGEL>p~e|;s5Z)2L3a9GXl?)C6C++r+zhrF~vG%u|Q+BE95cBv-%r7jUe>ZAOHt!Q|HqJ61MJES< zi;`m_Bfm03!0hYqd?vnaNoH0qoJd>nc^w`ehRBJJp1#=7RH1+S7-sjBRd*9!^GxNv%F>^kLbN2b}y`N{T^{h44*eY^U&G^H^ zN@my6#nNZq4Djf1h-XcFvoyH)kfbVeg7Z6>=20F#%8mxbIjh09b3zgY<3xU$_g=tWzVFtvtD|k z!B-P0JLGRVg%67KW2Qwq<;M6^ezN8G51UZs_48fGB2t8{Za$|^WAQ7 zwNDRo-E)ahOF^wJ&d=>;#$8y#92V6ybBF!BD>Yma5e z;KI4fCtM5(jv~vCbWfW5#P^fm=_;JOvD4kkXa&u#wUyQA@G#-tUZuxDVS4(^kfF~b z)Ed~>+2JcPFF~3GbA7z>AJ$H2d6!xT%?!(;wXU-XO&>i{+1b1517}Xqsv94o&);JT ze+{Oy<#cLX3m(fG$Ahxj1o0J})nS|8TxHHL!MXOP#6nvC0=Tm2Ude<5(>H0Q;pP6T zLo@UkOH$*8aqvNbsPCuZU%JB~d19p5gefPj zjCzWP)z?(KZhs=&?{s^c*wbsH8b>YSDuWl z`(Rd>1vIY>q?s24n@z{U_QEaaB$_G@f;yK~zc*c{pbRYvL8eji&)hT$5DayIX>i;V4r@tNbkX+`j@Euj#Di z*2e-R{vIrqh@=4bJvSG{Mi421!W=@r<$L;=goWXessw!`=7eQsYTDcT6@EP{^%R*H znnZh@Zqd6<{mpq52LwWh(yQ;E#Eu;eCllc7h<9gm8t@%z%t0?_{25?JqW;jZXj2mt z2x>d#0JY_Zv~J?-(>?DVB43`!M>0NkvgvwYf{6K+X^#y~YFC@V8z3oZM%xcIAoG{W zYD3?@@VYnobbubfLk^FZf0unf=Lqa4=7fCNk*(EOUl2-OSN=ouKd6A8J!XOnRpo~r zg{h~n7c8UeDYo52T4%hBV*)O2)Hz;w>FMSlHoL)s0F0ML{|ae3wC<|QH`B0`U4;`@eVcD`qu zJPixwWEIg92D{Dg3`n!%qV~al(mi9hSZG;>#z+YfSSG}?f4}RhzqUzWnjHKIAfEl% ztu+R&4N63qDc=i-1{&Hw;C{6+Ty3JFqBtf(AOoCeQ9Nhw4beunY^{#v=LygNujwa= z^<+dB#?*+}VMZT?tTaB@zd~7GOq?$oI31g(?)ZHWvle&zoxa1#!}k-s?w*kW@)uU= zzpd2Q4bdVMnFC%-J<+h80EtWv^rirIaJ4-R-5MC4|8MAsFjP(I?JWS>Zw5|}q2;Oz z0}T?TFu(qYuj)ny<$qzX{~v)u-(J@k81#ZM3zOnQ2oxEQe1n7be-R%^!TP$qYWqb{ zc0-cP+1Z)<{Yx`IUHuJ={WmTose*cyM*%1d$-aU!^NV_&IVi zf;gQO7|;|!QbW(LU*GIFS1$iw?2KMw3Lo^GZ)or7Dl2n?)NAS?Ar~NaTOE*Y$$#fg z%SA7AeR(TFHIQdG1&tP13UV4$tKYIU3#R=1ALv@s-ns;sUjuGdUw;k*zeinIGqC13 z4Zil!T}TxOd@I${(=&hi^gGyOZj8G_g}p=O4^ZF#e4{V*G_1F7l|U!Q!vh)Tiy~x! zSE0K(eLPelP4wNQ*ye-Xix&qlhk-Q2(M-M3!^Y5rHV}$~&2ZwHmX>A&Q`jIT3=IvP z79o>ahP6M$@)W6bfja-L?dZ=G5dj$6=HnxtjPj!A;P~0o^To~x(99$s_x+U{nyErd zv4k_eRx*JBjb0ua&lRCd6yW0nQR^(cTUAbiJ@y6juG93YHXW96%Inv7TPwt2ARHeA z$hf^7_jNU}wQCsgpfg70*wK@pADoN(kG{Gq`#&FY=ERkro4W|k7jWj|<>mEU^k*dk zI+Y*IuO zQ&sfsQoxOVu($8U#KiQ3HoX6A9W>YdNq{#QdTs}uN_?}EO(Om8?gqk*o3f8$uxt!}?PIGrxcT4#H%}8-%l-vsTD;Z5S0;bK}N65N`p- z253xd1L1%EIdflyRFPfvEG4!^`7)*czW79I$%eN zp!$oxzT13!y0vb_GK^9G=gm4QFdz^T=3jW%^bG4gkuSJ#I{vYP9(7e_^=O99FSOB{fr z;V1b5-01(tmU~>hLM_?Xf}k2Q3)vt)bFhxfOrZ9rd3ZHpkZ<8wdlGIfcV?qrX2o{J9_hcq8@Z3Jf4B z0Z=6+B?UeJJ~`n(-|DqJw%vGTdQp)$ghoe46T@36{P)52KbW^VvI673NO-UU;pAL^ zU3d3ymZux{zn`A~cv7YzR|SY5IN)KBb`D-e`;S+xz^ee!0KGq$w*n~wAFJmdOQmvu z3LhR?3o9#KJ-r7vQJ;zg>;JB&ipCWF;qmdS?$yINE!4)d?k7wc5+87!%e}9wD zr5lgX|8Yi^d5Qd2H-vSS6sGfcQm*#m{NrP{pSU)HGiH51`zDoU%x1sSAfuKe2 z!zWNJ%dB#l!J!09&VQUh{M+k|;OH`)UtmxZw<+`X@;`pD{7H|nL7J%H zme^&9F^6-qve$3Ta_QIvsn1@VUe@6t`LQ|j+%hZU8asL3HcTeM!*MfAlYoSp`XOnD z3ewx#yG0Qn503+KSi={>`Nu)nZGglL?no@i^kt&j5ir`Ofi z8l}M@LfGPeFJKr-B7TO|^P7KlzzP>rI3|7JP9k(k7#DuwpQ!ZIpO`pW={%^oQiZTz zZdLq9JQl9^{Y_l45I4ZotId(M752j*#lrRekWOi9YYQX8o0Ev`=K%48V#Me33m~&Q8TKfy??E!SL`fjH19-aABSZahgevJnN-ejoE3ByhB9hO`>z5!ByO6pF}e!3lg+FG@?pqDA2&p8J{K7$$90$?`N2)l$<8pcINM*giOx>Ur# zcd%|m&ipVTEi+TvAqnmCLr{g0ZvcqxV(JG-q2wCKIGDF4TE3pfx?*QP7rV<+#~N>E z19GuRkc++4zK3*|6ew4JeD{{oI{>O(y4Te;;(urVO{2C(kKZd{+QCft16YZrEUy-d zc!wTn5-169GS6xaUsjiBIZu?ogB+FlJc%4Rdvf;M!{yO<)KxFr? z2&_MHN0MtRdf3Z(DL44Rd=O^n$68wEw@pVQF;oyhdpfFOTMp(K%HhLf3p|ApH*+Y)%T2+(1|8d(B=eeiNNYt<}Q2 z`$sek&lw8OfzhCM0R9j&;PF|>k8%Y7m;;Al?h~iI`s=>F6-;rR=Irn9!z_-H05(+O z;O!Kpum`jf?3Z$w7{3B(+KKdjHD}K?~hr@scJBZ)@0Q=Ity(*$b@rh$I zY)LCr$MWv<>|p9TyF0+pUkc9;=LiO`TUYx=XKS(_0ix{hfHhG0tdr7hYG6QvqJx`* zgIdrbEh*~s2wox-2`0B%xVu!ejjebyYDe>Ssx-@bRL zl{AiwK)7nS=lOWuo?vw`ac;};DIJh(ILD2?d|Fe&IGX16v2+vK)6;X#gw{R-Q4eQ0 z@_3n;GNEvUu|pUOqL|QDM51CCp0AQlX!D2y$ljtjS?S2Mt%IektUTN15R1*~{$-O` zWoKef8wHIoli|F>`&;T?&r71C)?mBatnZNZXlC*gWvbzD% z%Hm9Gompv=GZj!25R9R5b5jzKvem<^<0)tz503ol`-<{mX%>N3fljK_Q@B>hNT#%O}6_{tPc*#40HIVqxd<>lE4Q`Sr!7EKoQYs|? zaMgK~7Fje)=9EFwGGcS022R3(2C{unkYSoYB%fl#wjKaq7RI=WA8ynd%v&s9SML2` z(wAb|qWDP#E@zDbRARGlG+aXk-O>uykPk0M^#;lahvN3_964m)6e$M*P^W-Sa36ZeWtR+`OU|lJy1CU+>WD zM3W`^!g#ZGE>Cdu?q{c1MiVrSVuAjg;&8d*o1`k={KJM#@2Hvzgt>DTfB!=%@~GHU z5{pDtMOF9}E{?xHD-I()J?EZu*c4ODfhQ(rx2Rqh(eGm>ai{qa1J{k)9^D&1>qtWl zV$4IK@R#V{!EaDnHn9BE20slYDDsJkrSu>>R}dx_U+%9|RX{~|m_*E^BJpdY&Dd)8G`N9AY` z^R0#wfbR+L=Yn6OnV__c4*^#cDsu=XYlMDt%)~y7o0<0idKgp_ZA>1jwo@n}qOwyS zVop@Xhq<%PbMrLt;R6`Th6)L?u7n=6_poFyn%^K{ntLpXG)?iwKzOvN6}g7QY-OH^ zl}0~R$tBXgc1%|pJz2%Hsgzru`u6RmGb=doKqJ!O{eiQS%F|NkdeVIj4pZa$I_BoO z{VAVx?^7ayAm!i0Fu+P!!@Mi(@M7&K=tVXrq=W3O4k^pavv6>{F4w&Ej*^Nh9|nBu zy^_i`?~;l@4xlY(WFuBa2j<|k#L`Dc}Fsw@=kPPdhU)Y{T7dC-jn42&#A>V*A1 zY?A-(x%XL5Bz#%As)I3*($v%x26g+uLd7Yb+?`$#_|{gok(QP~)4$>;1g%j{bEO+4 z<9Nx@K0BhJnmc0nid^9m^>F{{Y+3LMoePf$B#R=zQE9e|5TVTGLpKTdqmoq<@wF(XiK?JC^(w*Z-Idw4= z%gf5L3b3H0#8|Wi8DpsK#t*04gqwH0Ql*!6pCIoV_e&o8em7+)jqYH0Yu^Cv1|$r~ zy(W)JO6qV_%V6lxnT;k7R^xkd9{n4yw(?_G=f}4sD_LA=^T;8A^mn!GZMgkzH`ky_ zmL;^p^rIjrzhl*}@WFg4bX__g;ts|@K6J=0sMmoCx zhKS+kLV;?g@Pxu`@3FUpR`RhRh?rG|-5vD%76^bR4lED=lh7zlbfqMCyw<;jXr{-H z^^6FJ=2TI&%mlEozETDcGsEYuluCO32bov8R7g$&cT2B~ zAVPOGOmzpF$i)B@C)p6}2t|OTBpNpr30fkY@3~$a`m6%>lgZIiG$9>9<4vbCZ}KU% zsl?p^mSfB(MsrQXj-5#M1+j)*E=Y8dbeIKlC9g@23-jU^Gb?<+YA8&%PS>Dp{lgFb zvUf*+-s3eCaKBSzfkGV&o;Eg=^d4*s4;APgjPkFv8JAEI%}!A>>LsP5oGru~qWEG~*3k0uoSY>fEP~{S7V#rF1m^5!uaD}!vBbD3FxTKmewOUz ziv77Oa^r+B{C?dmsQFtXo*!SU8_j2w%I5{572I!pF3tg>DJ_#h0$%f-%^g&|`c82U zigBH7w*>^U3uGHr!F0m2Xx_jtAzm<m_2(OuPkGuONBTykAfu}R1!K-O^aT-={FLCu0vQIjoT^SC_%wuy!Qzr& z50LUTP?Jwz8|Jtox@xmViIAOw9g;{M9NGTShAqu+x2kXh|E&9hDEsw~hmRgD@QNy; zlOe+8B#Zldmc>^z=W1tj33kpH*&@#@5Em3KM6tCCXx%YKLMZjm@z-v#Rrupim8l%Z zSgO9oMW&#FFQOD0k)C^&WPoK-FzBXsBn5nYybQJzj_fbY1VV}o+c>_@`s2WLy*#=L zDB}wdh`-wQe)=&x`(S-cOJ05vd3{B1q@;~?r_C6K>3l3PUd-mu;YyHIv|LZU`$c>h z&C2IMJS?YomxW3@bep?nXRxNAz>WOJy25g3=$)t zb(-n56uhOQp>ZSKti+#IX$}fUOKUdQZwvwftnL99S7Zv*N>f`*@RX1mOhDSg>~!N6 zh_UM`6Bg&l@VUZ_883O2Y?ocjiwmwWR23(2qf`9-6yHj8o5ZiWfwKFSJ=7^<{>CIZV(R6MG%HKN?a|FzGYSovJ1iKV_DF2Q5{$D&Enqm6Pto}-tLorlO>`dRcD+5?5o zUW?jY;TDtLY|G+m`9~+ioDaLRKTvS7Fy{Z7m@s(s=o*)CXpo-_vgd(>p`;$W8Xsdy zFe}9i^E621{QWbLIBXuK=1R-neM6u&Nbw?CVD!Bw*a6XP1_m8-B}-1B%VQ`D|A9h- z6A;8aJ`A^8U}0m+g%+(+`~jpmJ-wg%Fm8cvXMrbQ&I~jq&@IEpaPTIq35|^0gy$Zv6cCMD zSVQ8_Wdd5fIkpzO9H-b*gPpS!aiLGw*6`B2A8syasLYU{`(v|O!bsA#G0U3M~&UroZKXURO`!l5+D11#J*O@p2;xt zj_)z38m6v&2r0=!pDLPa_Da=W8YaroqFo7){>Fl*ZUY%-YV?D}7o==P}!E zAEzpNsF4W}65dRQ>&u@Nw`C*(2bGnS;)p(#9;flpZF!-1>G0a&>a@K~ z?HW}fzC*;NZj2##;OB`;enbrD4W%>7BR`cO{b+GqYQ>U4 zHSr2(r1GlkvnNhm`*jPYrwtDaZ9}_a^NTBUwt+JQWK)*>1N_@B5=$Wb$KG2uiYO%} zH9ju$D+Oj;Gg?pKD~3EqbUTg(sgzt$)kfzMPE}xl|M!=8e(OO`5Q980A?N;aY&M1? z5Jc^Zqy9;*vA>_B_=#NCy7Ty&{EL zt#0dgJ!6Y6$LYq1GUX)4jvW${ZkyxJaxTL4K0WrWj>&E-dWd88lUXxbz;iO;{*)Sf z0vsIeDkp@A&;U5pAv@}c5@{jKCj?CutOJRb@1*$wn`r?0_;QuY8=46kOB$7xr?Vd9 zDM`jFB@|If+pG&-=s(jQJb(kP=&Raj>idzIevU)G@;}^`Z_d9XA|fB+Dn-2*Z&!_g zUE*(eJ{Ku&L2#qeZQBG$InaWJXzl=t3u*L%h|*}JzL>u7wfM0r6_GYuGI<0OX^j6{ z2_^blZ=s67IjkA0$!#ro1$=)L=wv zC9(>83mapwc!uyzp;u$%nguNhO5YkB92$?ZyO9X+FC{E0AQQiJG#qnaiP2HVNobB- z*Ps^`VA``SE$^H^CnGiQVQIW+*063PlD&?kDE3k95Yu}v(Bkdh+|2BUjM~w1)MC=6 zIXr`(tB=Nx4Q8mZ5Eqb-Gs~X%pbItf-^dMo+pj^V^!ZKV(&$Hpwr@$*ikNX8cwJH4 z2zl&eqI65uNuN~KB^%vKAtjwD z{LFILV{P#!Mn+$p7TWW~i|}MtLxO@Jt$S`TDhigaPGY>6<+=`VBCp-F!h5#(lz+E` zT~{fw8R2WmIt54r{Jvn9!0zcy1Lqa#kA0z6?o(;c7t=X2gqbl92C%O_ z&qI~Gw6rws6qlyuSDyv6S7^vf6Ldw*3QBVrwvZWRS8?KwjgE2dt4WXjtUZN~LLdKB zLgV}Ek3yyM;u|p10DBl2O{Yhf3g#p8ctu62yF8!516F?qlRn=d+w3_1)N5|$i?!KZ zgBv0ytSqRye02=)b}!HKFiV(2v4%%#AXV#f;XYEY%aNUN#sf`0=z%wrT1n0G)nHhc zv@QOIGZ<)6*$0Um6J9qc?*SbUc-l@P^GV*p!2u2%z?9#W(|rlw%P|!@J`wR@jGI1U z4jinI`~B=KW&-WvZzf2U$Hdj6zWC$bYCWne*srSPJ@AE+09#qFBb-vw&%x2}7Uzk_?8QCG)Ihl>a)71Deb*McJFEF??DhF9se@Kjl-iQLWc!_rTXm=67+_t)WS;51d zbzg9HA(`d7mz-vfi^pZ%;ayAJaP=Sf?a2V0q_wV0f4 zXFRxWRQ(|_K2Xtj+3URRll-IRz8o^)8zc`R3JW#9-q~@Zd8l{tMx5UtUF)+pS`aGR zgZlCd1S-5KN(t-*hQAm~Cr1Mwl|I(S*-S*4F%Qiyp`v$U;_ymf;-R8)sEG$PZluhT}Ej=LaTxX zFbr-ftJR_eBfB1 z@=1SpcM%-zptqKLIKzarucQs@&4;3WGNwHJJlWU$`LR5(GU(Yu4KeeibAt4x2X|K}^D_V~+ zm)KGUKr-J(t>y(qaQzp}Ox-69je&x%%>y2W(Ojr;^`b=5?srM;A zUvGc$-M2b2MDycu5-}%R8c15qkY`kGHckr+MJ~`xdWn2--+eaR1T{Hf8O+4?Fi2GR zy6;zoV88wjLKTupf^n|SPM%gc?Ml&H?BtSS`b^I=@G(2seo|RANUeTxlpubdW8`?h zcqGD>IGk7Mmp9}Y&lp4)-F*hp$`C^|J?+c7SZD@6Y)x+sF5K9Hh?u8>P*Hd@C~jC+ z;H1wB2wA=?ch*bgX=WQ~G)Ax<858e|Y_>Ofip)UkxzxhLrC3Popz5S!G2ph+pR0Rk z)g`A~`B)=NW_Ing=&-YKUg2;uqW0AJMu)c5+CdJr13vhos{N@70L<#elvjL0f`z#` z_5~o|igr)sWo4rb!E^2<$aGFAPi-tL6l_EV|CH3z)E8_uz&7wtb>CZuriMny$qYoU z)U<2XK-X*dJfWy96-&nE3nJL2ep@Lsp5eh)gh3Y5!7X*@y2Of-b5i-K-pok;tk+hz zM%8MP<_ggLww4oe>lJ}2+YEUTouxH`px9A;2WG-}qr&X$ED#~V!M8-s!&r~P zoy9-)ax-Hge7yg7sO*H}tsK}psX7TI*vC79N=)zIn|jnljV&w$8BUFE|0@#pdo+@CZslA&q2|=f)1i1D$@ym>WKyD1;Oy+Q@Lo$YU+xcMgwm8($@)Mr){&*WrPreQj(Nfr48D=>P-BHiN~ zTjD&&HOe~*47=N*(cg;G-r;>n4Uevyqw5>g3_SW%;(Kmwe7(7n^{5Xk7H>{7Ihx&H zZ4Rm^Ya|)s)v%a$JPcb678zcE=3(nyNIt=7-SQFLvV+y(fkN<_2c~#nNJw)}C3Iyi zF#()`7m7Edi!M6hXojc+8Sl1VzoKH}_t$S|`L@GJr`|(%%;c&Kj45^cqw z&c8bN&ZZ`G)ebUrAy_RdeUS0KZZmB7q-2DFPb-QOGPAiEnFFG5^v+Zv=gX=cL9@}b zT;)^OoEzt#U6Rupk`m&wHA;-H+EDGw&KZ9#%@9Ap5iveT%V1?-O4~1XXGQ&xQ^r;{ zPz{Fd`S0Gf`;~z;du8Z^ESKA`OBLy0Dcuj)?u}R*YwPeFgwO*RE0^ha!i@cFE^_b) z|5-+tll@hiBsS&8ri5ksvHC>6$2uIrOF$3S@hz zM&R2dNb>S>avH@sbxP(9EKeZxy2JAX)?Y795+17aD?d3;U)NmP#&0ONIOIJ)$Lr59 zq{O%N^LW8yh_eWQ^{ZDMVb`u)ksNZeB;k^I%B+4C`vv#$4j;fw?y^`>%j8-|g?k@_ zgq`OAxs-OX6~fdom6Vi}!E|ccWyza|c9xMA%LzM{x?2*tbP_}TP9OjG7(3J!`1trs z@?)uts5L01Og~_?#&@3*JH^Mt?cKR}nZh}w>-Og!qkffBPyz}N1!ezQICnk$MUBIZ zRId6c5H&ju(0|bJncz%P`BIPHslKa09v;{37?yvT7&k!g~}tOO4u@D}`zT>Yk_$u)tdOxczMn#qqRyG4ce zzdmD=@zbXoSChy!Z~yE6_IH|YL=#Mq<{xVG^>K4W+zXwj!XZ;X~b_=TOZ_OEY4?R|0fLa-Rx zOSL^eR(?Qi^3Xo-yZZ?tf_EDGf50cUC&S)RQWmUu<@LNWy4h1Cld|#ue9>Q*R_m<> zZVA?b<<=`PQbf>^s~o>C{4W3bl}*qGy^zre_!YqQFg7DnmIA)I>NTT+?yEo9cw@0Y z$-y7=toace8+*FdOkgz8og$LGWU4BhU~jIDp}o3`?yLyQnj4ia$}`0!W;6i@az5Bl z+S?bwtGNE{RfU95^_%}pV2DxxRGY+$LlJX#{k7F4Yp*9pN@4Im{Cu(4zsGLC@H>j2 zhNWDcoy~xEBmLKRlYlgB7{JQO%Ugk>0$zsuuP@Vv02pxM$c9^YW+ok8Ci-uSswD`F z2~@^|Jw03d`x<-2tmOap6S8B#iNGZH+M*}Tuc4s;r`3ZA^?kp z{m2h0F-AtIUu}$c|MikaoR8LZx{i=d`lA`Z5+4+cB$*|B-7Jf`K6&fB|Uk zy!Kb#!CC>m3(t;~((0y{nc-^B5O6ht!)}jU6>9>x8pFzP>++T`j8CN>X=d*10{%q> zbg^acJlF%kQ_Kr{EHNxR+#0gqpxg%sKUh_hM%44KBCR_$TYTNEfK3D7!m4}4 zIQXcg^-UsnC89njO3*6_5LEmM`QaxtR@8nby zF01N4emzzONSRJ0}&--tvdI+Ct zu1jaey`&-T*Spou=BLVAQ~bz;msylEuAEfqN3}=WOv_}bm+$a?F-xIJP16{$mk9`F zLF{LdZ}b9E?CeHL5c<}HA}RMg%3&MTd63&zL4+-oRZwoptV7o=Nr2K3XT*`p=JICq z*yo(RYL#csT2R|m_%xrrr+9fV{?-Wp5UXBLPFH9&GYVew2Zg!i?1nO00*TLEOH?y?JPq^D$~_2~^~BJzw0T+D6C%Ywo;07K<*KT{GHD2ePJC&3 zxeRO(nVCWQkr@rACYqwF6R}yHvJT1Y7~oUl;wN4mbA1`NuVYRSH4vhBQB?*HAMlKT z;9S@vL|!}D=zl%SI0eej)c&I7D3B97siFjoR^0MO{?{*w)0bNMWP|?1r~g#M!${qP zY}t=>ipT+<@+ruVWC{$HEiAsJ+@n^uQBY9$8+0NvUk058C}%CE>hGOO=p{ich1(Vk zkW)g9^cSQ40|r#>CaOa%FQ?5I`kdWRhpSaq??KR^)lcr|H;%fQ#{82446)^;>ZNJw z$VH|JqMFOs0-`0mLv{HIFV4GCaS$hnzySycK5nNx93!fUBZ48WVpDk)Cw+ZtTTXd| za*FVcY_nS}2kW`+iTAoWQML9Dmz6F_KSG4PPpB%Np+xF93=RYDd8>sG3@5+@^##59 zR(>morJX~218p4dR2Lyyx?ZYiKx^kGdaZ#YRv)+ZEFXjL;%5To>Djf98A{arW2%OC zFTJ*~VHzh~xT$Ntd)61K8$vcI`X(3xDWMb)gn(`^(HVsSyZv_l5W#R1<0G;3l!M_KI7_^TW266A1tWfr`niyC0%gZ6^>7mKqQv_evg z9NRgqwHf|V@jiC$p1LDZBLBq;c8k>ne0U_e5jR(uSMda^|0_?jrnE zYN;)ANPPsU$qBk->}hI^yzG-{Lka>~QC~6l!>yT6vBaxs^B}~8#AV$0uPyPE=~GG5 zwNECj^s882|3#No4mRwKUp>P+ez`ocV{cHwSeW;rC;(cNR*@oU^C5b|PPV#K>zNmr z!tSdo<}ycs{4QCfP!$sx-7#A!Nj&)VHZ|1{uxx+{Q&N?^4tC(Qgo!Y4S8oF^ZAmTbQLGKJ|NKb%?}+$MO0O@{f#5$h&4kf zk;8H${!a%LP2wSI(DCYLiO^`R6K(b|OP04{D7BNjyU8L)`a2GS+MFnkj1ce5XKT_7 z<=jwX51CKLp!z;Pdf87-8q$@aw1)$Qoi$DA1do8g{=Z0;WmM8F(971~ zgyM2%|5`Qp2!j0Rjz!P(xtG-t?w}X+=@85ed48p3AO+xW(((Z5#TI`Ew@5}Z>yZQx z1g}zfUoV&!agLPyuQ)+OB!pOjlgHFl zYWbhc&^a?F+}&WI&%MGnr#x)_Q=C_ES#<&{Z&L%O)5AhVNy&I@#s!4z1H6374XeO7 z##KkL%Fqu-uE97szm5xe35-~eBUVXUCY*{637_XX&J`ANvVOWdNiZq1CAb`m>zGU& zUy8<6Me$MfwMqn|IQS80YG^=Aory6+Svpj+kb4iog+9-ipR>~4&xU*$PR0~w5fKlN z6@jxa9^S_fh{yW+HqV}cyd^n>H&Q820+T-uNau<5iCe!j*M)92uHiR{+R=aABy|Fb zZl4dp&T+IuYs39d7JXzs>bDc9^2H}3lB1oSoPzOba24e~_NhnUC@~l1Os{q{Sm-KO zCLIR0b>enbJw{wC)hl0lB;vP+1WM*{ZqC6s2_$Uf%9byAGdXe=xdUQ@Jug!7%IuNPYrz8QFY zjhjb`m!CgK{0Qo1&|%O;r&(d%IGe5~Bf)CLP*sYpU7yjB;Y{=r*!*Kx8vSjWA@-D! z>HO>5l&iRiID$K%JdQLmnrI#I7yaytTN8|tR#1SRo`R>((JFlQ2`(s+Cyo2BYcUjq zjW=JgBEqk@JnE5@6bM?1sk&sqE6n>DS1>OskR>TZpUC25r`glvQSj7Y$cO7*rhkkd z2M!v~8RO>@zUYvilCvIrNXy#zQB8I&4HCS(wKr-5ga`=ha6q(2W=4mIm{iBdCx#U$Cp^D*o10A_rJNMI4aJ!P z>mK{iDUMLF5Kl7Yb!nx8|L#lB0m7!KwNas#KrXp|oSj=+!}BTuq%TAHw?79jUes50 zP@gG0kyhI;xDBllW25Qn)C12v%X?@qMA!uvWC3o*Hq?znWIbu5jrxToC`~Es!Du5tKD%c?bgF=;4dXcz9+ZyY&kn^^T&&7e*v`ShP=7x-uHNKI`2>3t zPJstXN}mcIL4*=qc>sViGUj9tk&=**Q&8mQ=XMxjLS&1HFUO#>m zdrGyUfv-Q97Z9E9aYUR2WWxP*rOq{%mr7E1_nXYQ2oagiah0rzAN%NIPbCAL`JjxC zDYDNqH`aD^Pveu0eVrtq$EuJj3JwQR1tRe+UkLm)neLaKFIiha^YQ~GUPp>evB@z; zSl~8;$d&T;i=M$j=2U5UTlitp(||A+U)zMN##IC=C5|VjM7=(6Q=1ZbK+w3#3+B@Hdz)<~Au9`W( z^Ej&;b8{HSbbyq@nZUF9945zvYzP5g)M$aW8 z!HJ{lw>To-jeeA2HXa^X#qK`+9TB>L5IeDL>-##E`ko?Q*4DRbZguslV(S72GM2-s z>#;1R6Q9w`?1@7RysfhM)1s^!Z}f`LdvdS7`rg%re>@LY7!w?Y4)^i$ag-v=GIB-p zn_+bb$0sHb#8*6@wjccoyUwC{`jlrK=6ll|R774s1`<$Zzt!FHEVNV$6athkmTa?j zM;$*~fMD~;)bv37{`l(jw3-j}y{=KTOQmR2fhvD%Q;Di5Kwg5<4vlgWvkFMlDy52D zW9kDJ4MwYbqN30*!NiuYpgoKig7DPqcu#VC@u8xkKequC@`MDgPnYk6y6-FyDnyLY z)%&zr`YDfSF9fP#=iIPr=h)t^^yNPM+BWXZNWU?c1)il zkg>0|w^i}|l7H6rKK&J+nWLtCr(lpNoy-x$5A`g1;$?*GnS#U9PxC=2gGJ@W92@-ELa3_L75?lB*s*EG*cA zUeAK7y2sr=jURlPx?V5G8^YtY15x9cv#*{%r)fBI$S5{sn#Kg`3pikds2|YH<#D|Toza2(N zH>sL6w6!ZiLGh=LWM{rrkAlGADb>{3s@XW9Xq?jJb?g3SL7v7|907%M9L|tNhG}Sc z`$`<0UcM9_MQY5jA(P!HH%>x}sxsXC9ZMSdUsZh{8KHP@57J9?`2#=%!R+~qAzM>~ z&i5+9+a#W1QqCe-xA>h!WL)6*1NY0$#M>ckK4er>SLxd8ualAvg5dsrIB}Sa{rLj; z@s#e5G#^x|7I~`}$w?|2*+@)ksuRKyD8Stx)$`I}u zpFD)>pYTpbP*6~bKwyGg+*m$()tlWi7465I6+p(KOe_`Hn3y2f-=ApMtN`Q6+o>YL!hq&$ zots#4w_mx#rmBv;BKK-V!r8<0DFB2$vEX>B`6rmL=ZBNXy*KK~R#j(3pg#b*0@6{y z#_SNFY`96Ijy$2P10aTzi%UsSab>dZ1V%C!xZ}#jSpL_?N&uo*(i-AkBu1z*SnpJ* z10nSN0@I?N=hz@*xyA(@jA^b0F(utGECZ+ zPU;X_tU9Cr=aPlyCiEG1Qd+2hUH?=2aH*bgB}>en!9~pXv8;I*&zWSOr&ZNCs_}*Z zoz(LQO1$>Txa0JylwO>)g6{wI?K816P5``uDOosh-qmsLOTdFdB%zmj-QsaZ4t{?G z{7%1JpG(|&2V+xY88tBBSyCZO3txy5t#n_=j~M}rIH90QV_~OtoxZvo3?Jc^fkqAf zqVT`{#hPRKD1}c-DR~72>zkWJYqq&Gu>B$~o$EH}-zpAu=w$oOp>wQRy$~n=%#lk9 zew(p%)yeLwC4{ItdLT zSM7w!UcH;1Z+6VytW)}Cxo{n72@V#NS#-ALs-;~#0ToL=J<4u_MG0_qwoxSXW)>_% zSGDXsqYs=e)?@eyDB##~l=Ow?i3@PvINi*mDs4GbNJBYUb3!muj9qM!g_vZAl z0mAMU`p2A9m=6>kX!a(4iHis(eym5{9C&qemI!Ti&n86v+0AGx<2J6!mpGIQ^D(O* zM_++C_9L^kx8el$UrlInKv7Ha1?oM^@ zbct|&x6YsLaTUfi>?CW@b1@J6jw9t!QgV-x%zGzvq}ht`xZo#}h1QU10NtS^c;o4d zpS*$PPsmxI7o7sTjImJIrc3OQ`To<2ulgH? z5mGb8l0Z*RQ#fZnUob zVYTm(I+e3vSP38{W|r!>Qp=nb?ewcC`@+gjeC65ky~hQ61uoCtB9iGHHnZ%*_oM`H z-@sRd$>>_(qrsf><0uRxfV*lgRMWh?Et$8#uZ88&`;P?$G~gebhqZXyo(!yeVyw{M ztn2~t{QqO?tD~xFyKj+_k`57(5{@8(fHablB1lMw0n$f0q!9#>^pJXJlmXaaG5#cREHuzESWT)%gC}HsBb4-XYipwB=GnDwz)fKL5 z%y6qxUtw{6F1m!r+t~9wTIyjFj-#{dtu14R>3MFQ4*@7VTruyV*X~bjsz*0_c*ID% z8)x7IQp6Fn|Jz$RFVkk2Q8q$IgoVEr_tdeWH86X*R`g$`~ny`peQL%}ls zax$_!9!dEVLpd_2@v<8~%z&~SpLCrp0tG{pFI=`k(>S1E2BP4qw&vzQUE}L;6A0Y- zO>lxx^;#OpFj`vL-QQCzXrHA%W^D&Auj*H?h{cXt2zqEv-cC;#@Wn?+R;u>9AAV=> zI5d#P6_$w_x?F7JZ&glF(-ZVyVRHMzSruzJl7cUA_~dN`wZb3CpKsn2j>&fJ7-&1R zO?=ZYn#P|w#SzLqleL_TfYXxbhBwep(m*`|XeoI6c8c!1#KaLOc;IZEs#W&U(dVU) zz9g)suP=Hv_^^;f(r0V7ZD1LRL}nXJLrz_K#mA4b)&_0hf0ev6&cBA`M` z@i^i!Azm=eJVboQJ^%YKBgJRYrYfOgaOke2qouwft_FDJP>6}Mu?6aqYJe5!EirMi zi$Z;!6BC!o=2p6zY1H42Tk5(}jT8<%V7SD`n#~?(X>0T=uLl z*2o+p97Iw7kkYs_snGE|#`Ownx5|m7sil#w3+ulRRn;utY;P@z%;jHbA~(G9G_?5L zBbO1Da=RAh_^KyhfqM)N3DJiV*bj7&^m(QRJ)GVkRr z72bD#x+qQA2 zeop0E6^#4iAw^XrJ7`uH=f~_@jyM|VwS~5QC#K$YGP>ybmbF0*s^%NcZ!E}4Q@ z^`&fNTzur7Z}$C1m;V(E4SUHEl^Y*~W_y-Trv)!lRSlZ?X%^dY&q0&nm>S2*bK>`i zsdx-$tw%T>?}Gh(HzC%Q7w(2hMPEx^vbrfe24~=BW{q5_>`QP20#_h>^$f(aF}8Qr z)WD+=RK$+py{cUV84Le-CoW_ec*2}A*@NHztAVcRiO7)I@NVudO1^UvEPi|uIu zf2$~R(d^Xp+!C+{c51y3eg-t)KU-{ ze`Smj5WXq!w1fU4ScHRPuArm=pdg0KuE)T7!&=8J;N#`J`W>`5Frk8_aydCebV=S9`&v(N0OQFzX_u%SiJn9kGc=j z*475mxDbnUyAJ~fQ0HNz95Iy5GP6dW$9;ngLLFqGCz4_wz_Sg{`xNX_M#e6hs#&c# zb_S(670!NyrHtPR=Ve}N&F-w)!gE#YZp+xa5ST|g`re*v`{B21K9nph zXD}ay7P)cbfIs0Kq<&CQ+@iZ0$N%&5HQ3Mivt9|GAqH>sE!_XBsnLkV(-9~HE*+Ae za0{dUf4_tFXrkV`5;$<+0BD-%{xu=E<BKv#};mo9BJfIpI5=~C4(UjRrTKD-z#vZ#I%HlT)ac%OXz8}C= z7D|z;)as09FqT7h=~ix4)x``DcR^F>4Y0I46D{o!2<^{P+;q9f9kz)qkCOb>Yv)O)Y{$!RKXec=DM{)f?}c6Kq5hk3g)=$uVqEU9N}d!0&BgHUHKK7&k7RC)Bpt8i4q3(R65d|54ftVRs{ z5ymipaz&tEhg7M6uy8p^L%HCd_35SuGs;bo?px=e5JNO!9GGJ@7O_>ebryERtjD+E z%;ph+J48NdP3Lh5{aN@cDrccP6q@9hjl-IKn!jfys^5*Q^~{=R-#|Z**-6@Z^{0G^ zz%YAEpVQ0S`)mu=hURwgyKi!x*|aCTN+ZLbJ%*%rhEzRMCiEa53K_O`db4h$s8_)D|j0)ff{b8~+n-`Hcrbg}$8G(_;l zh57jyTbQN;XTE)iSGdZ_8EZ=?ry`22S`qv^cEe$RS+=F<(Nf2SAqLt?`2>hc4|IUL zpvlM_+C*1RaC-i00(n-J5X&3eg}L?hNcU}!HZ@_X3o%3^oSkBb>p?Nqlk6>Y6Q0X` z+|u-YGzVrm=_x%d;BE8b=@=wdY9OCJ6@dda5Z*SE>cPLwmX2fLH~-lBbNuTG^Y+-D z`L9W@yHI>9Z?V_DF@I7bWi_*ruoTyDw6UD_TU=AU#|w5YK%Rb$b&L7dQhmW2es#5# zU!g_%*RsXe)#bF0i7FM)SWVVH^`92x=9Y=nHRd1KgB-pq?vSuRC%-W}i#rDPL97}y zAnF4&6K1%nUcWl9f&f!!pn)xTksAy;meV!{bF8t0uy8#5hM?-f>jDN}x*W}x<$8!~ zzGB)>?VL1TKDoR;f90o36Gju0Vq&5vak$&HxS!?uJgxcrce?fY>FI}#+dxA)X@D6I zoDh5L25)}Q|02+`)hy&+y1N)1THQaWvB*S9N@|@4F$HYp5vbnYUIKULddFF%M#PA4 zQDl$&{`ZsBf$qc`D-N7{erebR6+Nt3;8S_>WN305fR9vZe^;f+8&+l#4ohOvGj4); z37jog-*1-VLt(CDrN^Okv((G#mz(OTsPt3T(2y8gI{fGPfMSvECqeF$JUP!m3pHiV zE3j;Wxt*k>BvwMgpVclo09Ao;8UM9wtlt|vw`RYo!Mj=H6YMm%)uBH#FD&U9y?bK| zZ|mK!gC@*NE4}zQnyCjbQqL70RvfUDsBNoMM4%+a&-Sk+dZvAm6CLt*6RM4U#6PE6 z5$*zxs^uu~;RBs&deSz&>p9<|})<0Gnu^<)j2lxVSgSYtywJI!Q4 z0)d-1Auj{y6&NnP#>4Ihe~|DVxMCh}ck_SyCXgkF?W$A#o-1^ZSsyQ$h7I~p&%;}F zKGi77Wg9w6Kw?7NozEjsf95sl?{+B>7LR;l_FFh|(7wS9$c;7PJRxBagk7XYI64EE z3c#zQM#2l6>c7*>GDPc491~(DTvOR~7+Fcfn>>U^e5NGp3u|i;F+v(D+$a*dyi>k7xA_lHQ3#^&ylq)tyn%ymacJRV7Q zY;FB$<^L;>6L1lyRD?$;R=1nj2k{;b3@8v!T%A=jl02KD?1TAdQ#eH;gK%mN0BcjA zSX@{LiTMqi)cz5uUa5@0j`hZk8$a$*09DNXR`i)POf%f!31a0br}41fzb+ zB76Wa|4ri6e20DOA|8L)RtkwdXscY=m`3ZAd2W8xXW#`BPb>lh3yM)+U=2hbfYcE% ziONrX|N3Zm`rGvdDb73mQy2_!!;jXFt7DD`MUj`-m+s4OKMh^xGplCRlSV{h6EQRS z1#O4_^r!HiiGKSXYg-*OlHK}Y3`a*4PqZb`E4bNVGU9pQs1z6{f_IR)d5?EKoLRw> z4R#}Z!I03C_0dd7NXW$PEd*4+_%~brH>u~lxp0b2`RrWKNBQoowBOCsih01Mfyb)h*@ZNnpKbZ0Dno>xo$KX9o56yV9fU>Oc}rM zTP2gzy5;4(1ck%9YU&k%KRXsV@-N)nyD8)RF6iQnv98|s>nUgHm8V0m@31EeJb4M2 z8)&2!@MOgxPW2Q{Gz$2EW(+Akuk46n zi+DBH$%6aA`9$GDG6u8T69Mv)@A65@I=aGWIPbEG0Qw@Iwzjks8uKadVtYGAxVc(D zT;{KD;#A|7K!L~MP2|z3>WtB_(x;#U`^g%*1~_9MeU%B$)N}lL45Irq4L+NNn}i;f z6U#leuqbA1h_lB8RgXqLF}XQJI_*d}5oxNv$`jcv>NjznBwKc%bQ4nRQfy2;ecf2Y zxTpOupd=e=E)c}OvCZ%{2ZVLa$Ii~KH9vVN9TI_4i99)dZ| z-uk$xOq%Dkqp+v*++L!`yC0F47aIu$c#Rh4N9Mm|0$?Mcp%n4pJc1(6RdDtZdbfzS zFZ=DVl1Z8X@GC;HXhds9F7YIqUksEtirtWwO_L^!B#2FHrG+3=AQ$ zFj-L1*jSP^ph`r#JN%J7+T=no6Ra(N?IUd+C9S_9+Em@4yO~d(NNu$YJLlceOUw8v zXLhTyKnSdT^_bT62)Afs$_C9c_%~u~Et(;TgU9Mq_iDTuDwPlN!6Pc?; zD%HzAyeQ*W2LTnpJE>L-{3$Xh#oa%Hz-r0@@KhpLhn9{i=M~VmUl!VdSfCQV4Tmgn z+N-eoV=eDdNKX*yJ32;u`FX21EB=wX8e2A{vs?LLo2a2i&eQ!L&pXf8vx=Qdqv z8S2gTf6_V&9Q3bWze0j3tq{w9>G{pb?qO*~gFTq;l&f``Qwko2$XH`zZ;*S0&A_ao z)(zv&Ct%AnI(bhA1?@v|&&a~0I8v*#V$P7W-7p;46UE_8<{ z9tdY<@_Lu0KJF^^aHHlAdp|6smYor%C$)LKA3x1|bZ2y&a#*B&sEGZYbzl32rsKP~ zBwM^RuO3fTGlzg)Sv(PaZ7DYDSKvJVUo*)3kZ-MZUOPZx)zu3;NP+$!>+o8JEn3P; z83@rV5pz82WKPYXN zI^cd0`!O&8&q_q&>HUdFAdc}g_(F=KbHCG?OA8HDm^0&<&Kz6A%odcR85tF!mxZOG zt_cnlne)Y`_eW6H-Fx9~2aDo4wNif3wYzS6F)!mYtR7S5CCG@WtCblRq|#Apw)B1A z)jVp{*UsPksr7)hft*pWwW$dQ_eLIlA2|f141;ejBxpYVPC1rA1?Ml75#&q|8NnS_ zL}yI1zXvfiDCxg^-<=+a-1?#F==^K7_GX&TO>VIxQq4Nlpu*E83=~=&lQaPvmh1Nr zMA+k1wW5x_01ldMfDD^)06J8%ZpM5xL!+khe39o73MWtIEF9it1Y*uMOC8kCnY4b; zrD8%f>WoIRtG0D#67F+w2zDxYa#x+?9NDCp`X$OZ0~QYvuCy9HR8$qaPgdM7Yvl*o z`4YcA#h0%&1eEg$tc97v8r~lIzWlxez8G7tkb>;11tI}S72$dB0t-Z%>O8UwvAe0R z?rt}KBiQ2#3cfk~fPzB@#E~@;y&!4IU9RA93E#e&MGQlAYRYa%A!R1 zx-2Mt!0mxLvF8S--;^|}?ZmFEhB1syXnLNJ_h z!3F(=BTfduDsFx2W!g6O=!mM??^TcHEEFe*67O;?RW6TR;xJR!wm5=j*B9Cx=#KX^ zsvnq`0015UrAtTK1OCJuy$-7Pm%@far*8{&QXkVaB9inO=uIoWeiYpGbY#r1M27sx zA1(Umaha3z!Bta>Vx;dNjCo7H-@OW{!~uC z9XXL7l@OqpNAKwBioXstGgaWy|91CA6#Z&`#l*7EusnY<4xPe}dAf(PU%#FIW%0w6 zEDTp?dS0B(yBX8LV$S&;S2EKl-TMa?SfC?x+y=}Lb^{n1Y9fSrn6L+cD}1g_xtDi~ z&WAzQEmsBCZe$j1Qo@wXqfPk?_e~hfZ=SM~LIAB;s_$uieC`i|aDZbaT$eXDXU=FX z^wQ?Sv}wK$C&n9!9WRsosD~6vpkWz8& zx*0CTZK4Y+MMEI5z`1PzWH0ob-Uob6S_EMYpo4sIk?c{DuJKl!)ek}Idk93x)3O3W zw23TfEHTw2TdJH2_;OKe-J{q?`n*ls5A4p~F#GhW6Y=Og!rx=JKMLaO5ELcos2Luw zz`SsMtLrIi@fX$e3HICddTurdf2GNwqyD!Quz9|qVGZWc>m4#=cPo3o=>DUXy+zWH z8Orenrt3<5W}lwwD5{-qyWAz6=AFVEiDoig`tjoj@NUcSBtTSPdOb6j?oLsE)g{xX zGkzNRQIVx?6O6$k3-<_j19`l!M_6h<)^WfQ+d8~dGbZc0r~vO*{(VseDzH5OB;pVF zba9k}iS>MEEEez06eAS#hj?uO3HrOc6$+2Rf{d~}fAXeQ4B(Xtt`sEf7kitfEBlFm z{0Ygic~158bU0;DdfH+lE((!mGEyN-u5_=1617K!UVENaWqS?B-^S&S7SpMd4W-cG zO}7N#G|H{DpmTQAc)G&tFjlVW^a?I}U&Fn+#OQZW{-p>Mq)C9vlvW?(!GZQ?%lya~ z!)J8_<w=I*C24b^Y}f2AI~?){H9AnG)CpA)~IQrgYduVd%Q- zRSwXKQ0N=$;>t9YY|E68wi!zcH{OD_N59blWDicsgJNIWn$9-nGF4Wd)Stkax` zO!GV)xyr{^3}`5%JL`bp=yud4d?-UNf}U{41%A4v<+M>s7XnF4LjHZ?);dtMMC+83 zh7P`2gt6F5r_|}yz6LySN{c8MUYS@#zu<-~ z{FV1k6Ki}{V;t$_|Kx9ST(E9U&}LP`Qc$H@zd<_K`_9a0 zjd);rEIj-X=yqTrCFJF>;jS5XbqYnL(A{E^d5OLe`A08ua;#va0R$&sPP!)aLrM7- zM{Ezq?flFfY)F1S^YC^gmjCl+RpeIr$m?0vJQJQ|&qsF9I4|iSiEFV-4Y1mWD(|(S z*#s~ZWEtO&3E=93w2X{wAz--BPzdA+->ogGQ+__vH{`u>0}F&rH=mK20<%%SmMM7c zS}wp-u%chr_$=()yz?I*?9v#WTyyr-ShWu(W5o_aLo(bZn-1Bzq_BvB@iS|IVFq_8 zkmmRLzlWQD6}S@Ypc70++_`h7*WUp??uv3g28P8!&13vY_cz2aG@}9L-_!(17D7A{ z&%8{B#*VRsmmyOx$xja7>kFIDY+A3qiF;_(M^+Zh?7fX~v$W?>G=(+AeiOvLkZJ^D zApjD8KYaxk-2^En7a)%g>Uogl{O4;px@1aF4CUhrSt`Ns2pHNXL3}K5H(7#HC969% zxj&AfUa=l0OL4ZTy`v_@m^P;UOgm@*NnfVjbw>mv)-LYyW!^tA@cXnoySrE6Z-eJSO-*gV@)axS(slTIz5m(j4{{dYVR>F| zoqc!VoAXyWiPpPY?B5w&aH`SQv@s+Jp@C1oMhdYFK8KE=8UxB-APrb2EqZu=K%bA@ zMIJw9-y!Z@k5dXL-1}nIF8YZ{yvILMgom9da0 z0>1WmcrP|L!v_!GPQ|D%*X>=|FV zEDh*0NV+VaX(+re&K_!YPPdFGp%#tEe2Kj%36Ke5_a%VCTE2XN?c`eG|B*UCKo0g# zl7Pz@IQ|y(%r7?O^kq;;as!F`(+cL={kq69(a22qp@sH*laGVlln+F; zuWQG(01DdM(*sVho^DSn8WLqHN7m-KSMa_a(`p2ysCw`!D*hw81pj?E`4p12XZ?Ys zSNJh&yjsSunf%C|Rk;dWch)tf(PY;S?KpC*5HEL8$yPC(8d$ZU>iVY$O`8E)1_r<% z1a1e|v9BQre54 zAjlEOoml^$AH09fOHMYQIe#cDDq6YX0TDcFH}XKr45@d=pV{s%GqwGX z?DAloJ73|+^G4=3O!J~Y5HYKVqzSZUkAft!Jr+k`ztfN0A_IBMqFcnLG5PDB_x~%G zT*chi4OP<7{J;~l>e(Gn>-T7*62|le8;89cQ;u+pK6FzX3B1dc_(v=@^BCz|+tV&! zg1ao@I4!GkPwGZmQ(Q9%Q04(PD$LuaSYM z5~;>WP}mAmJqvy-S4K6E{%4+0t_WG@I1|*+&`??`@MrmY()cf@p-zK3&n-|z>;Mt- z?b{5`*NF*}zQhn4SUkT)9ma-QTk)wTb+om2cXy|gv=^3(3n$3+Z+%RFbOiSD*w|P= z2868o>>YDn0fko-9l;D@x{Q0is3V|>fMTHSX?lR6q1%^i$OS#n#8+JHA_Hxa(}}0d zOmnShDZGHPH8ycAuLxxD+qA)Ee+ggAz>sA~W*^4H02s2S;CLg$zE?m%+iZ zJr_Z-QO?ZLZB;wDR}}VjfSZ>W4V{%vBk00^ZTBl@7nwUv^^Y?R762-qp5^W6_%U4B zW54>gsOZSl6iev}h>+8#9A2PE(Oge^k_H1bXta`FQH}vCr`3=fqx)`~YwpQ`4G^UM zdNHc`A;CK@eg}ytx3@jL&GlX0n#J3lBY58hLj~77n_vQIeMoP8E$@#`PgGSgU`2;* z|4pjpYx1=7hI)F`LGL#?nWFn=!yExKP{dm*v#{}5W3^{;t}I}six<8TfnuxVK$B(E zm;9D5N2#VU<~>5ncx_)D!#JKRc>kUNu|YV5xc*vd+e*oK!t>WxPS8pdLkNL6SxlqP zqW*;8m8$KN{nst}Zbf04dNH4+T@-LU{6K3Lyc{LTH|>0}tQA~5b%km^^{>ND9@I3q zxcJgQ(%LYUYg)cU4$Z|bL~Se{akLeD#3bOFfIyA1@?O!#1)p6;^h=qTl*P8`H`yZ9OoV3JRKT!0l zpR@09hL@{p-O0wBH1;0dK9Di`;N4g6@ihMr9JKd@c5WZ^-A?oF`-AqTmpkpezN*XI zTioas#ze|kbGYvpu>VEt;+*Ei8Y-lKHHX4o#-l`f|wgK+a$ATLf^92<-z?oQ`l#qaZE5 zO02SrZQg=8r}T#yv)G$S&xI~JSz5@&p1sa5{uh|)SU0;vL`Z@7TYjQ3x@neG`D0a_F$ngJ&(;|loSUQx{eNZ7S%awqZ2c5Qg}_H`0WJw z7??(#dtTUh%k)%l0e85A?Y`1f=djm(P#wDk8Pr{i6l zV;e$FozpO%q=vEyToD&lUJm#|$_G5$s#0`#{MNdo4wT9)nPAXH-1i8!$@r?bRcTi) z4E!8%`L*P;#vQyRo_8YI13m`LgFFiN96=p8&+9KSg+ z+L9JR8nlmFE-BOalBqkK3C!w|j~-=!ImGJfYj51?c?jFO=~@PCT_B3zq$<6zP<{9b zcy`>D#fJC!p7tE&J`?2Z9KztN-?B0YE9i2Tw1$>z^U2xCj^<+o?73RVZVN+V@#MlI z)sDyjQOMi?`eA(NoMEW8xPm43sZld(R58Tz}=JXsUo@UQxY3P}0 zED7!4xDgE(pBoZjkgJ^<@2D!pSr-ZPrroD-f7KAhblsvqGGGH-EO1;Ko#qHP!~^ia zN(uiwR$q%O+>}9-7=C)$gaOeGK9yOGkMhBb2xq}_<)3CPf);Z= z+PX+u1H{;>vA?~Ii)~#68J$cqcf^y^kGQkf+kr3QzILq#p0eo3ayCWhEl@NyA#zt ziU_UWrAOL7HH(@__NwWdOSPPZ;cfj#1+i}7mzBk`KGyzi4#Z}C&Ow$ez9>Dmg$@HtRpCHXeSQ6_loNoQVXOk~n{n~V9cw>! z1kEiOMu{IiOPT@u#)rL{**tyg>5y4ZE%~FPBL@m%P?RJV2qwR3!m#COK}Z@p;?7hg z@VuLYfXq}UghRP$Qg`hBp(%A3lh{WIu>Aa8DHQ&zcM%9u{!c1u^5wyuU*A*u0o@ejsC%!_s3MsI$GI0fP{~qpQ3@V1nP0hZ@8t2^X^V^y`DQLFr?~kYcWlY}yO!Ov@E- zaK_Ai)NQ8)<<553Jj`Vh&eqN2xAvYWABgWoMMj^-gfacu>!@u0Szvd$jHqR5jCiHY zu<|ord2QR{enL8iRk(*PyUT;qBtZ`G;q+52|vSXWB1YsVc#zPVS0;`klA9XEc(G zk-VHPg0&>7ku65|R7^TC>Jr+WXkI=O(ag7Tr-cHAyBRN;g>7kZECe=R`>>dg*p$b% zRz!Ir`Sn?3{7hyG?sh6EM4~|s1*dQ~VKm4?F?uYV_DLQ?7h!iNP&|?B= z$VrIZ)_lTwFj%NmR{an*hVxTz);(Bdmir{Qi4~jhLZq(ZF3VF!IMi7huQ4b< zg20wO*WAoUy(RMRHpjE{5w+ZrAf43NcHDB!gK@%vjnftu(a+gp&K&8I&p)n$>()a)^zqoA!HwPbSD5%IdEAR>`g zd4imK7mW2G$(MrVamhy>1!ifVq*0763xk{@#w1srYgHu!t&X#OX zCQP!=06u>7RGb^$D0pMXV8MWZ@Y(JR7p-ml*{`7p$R>c-@hJWvHou?B)J{RtrRH&GwTgN|- zcIms)-rzYPn^CgkG0s@d&k2%&4Jw8( zCgc1n7moDW_wP9;7Qum_s&rZw1|GyBL4-nc`BJ2C$ZT8_M*TS<2vu+en1imRt!IDbFS;;k;i^Uy!A!-)>EOi_7SmeMe|n z<AZRxbD5Z$i(Ea zGDQ5>=E)OOTG~{-x5us(?99i|%;jovc>1mkaTB2>+<1;VA<`IvdX|OT);DY59HT^9 zSOgPdU?7o0kowP-ghJ`NKh|>8WptxHzP=fEAZM91*LYB40S@@M?-~mev2X6=g z8{rk*d%Px9F;#C_?KERCGTfKbeUthx;adAA>=})CnIJsuBo@pB-uq%~;hf<}e8vB% zG~>3YO#fkvbHILWUcciLQW64`ujqB=i_Qt0)(p;TN>vpZ4&;?y)vuG{X~i}HzsT14 z8_xrcc9`kMhv%AWy41rNt|e4dQ~x`UPqLeM(Yg1*K=vSlgCyuM}JCagNiHJcTI!akE~UaDHEEYOv) z8)ol|QZwIfSFjqPE_)#p3*iOrJ)N!nT z`#d$HmoCp)eEQkx-QZ!>_kuNAD17!$84_%+Tw#2>e_511-S^FJu7Ld={%3RTYLp{L zeAm!z5|U!*Lgw0oP*AXvB0fFoAcrGI(w(fRfY%Zs<9n>DtegYXgrVW+^_poCVVMj4 zJsj9VNXW+TjksZu*n)co(ImhV6w6mGUFy<%s~rQbo297t+`3%d*&OeS#?=K8cPVK^ zwRc>v@(!veoUWqFxjKrB#P5l$Op=PB`U)?kj=$%?%#B-e@;S5INLBxwz>2C7zIrtq z1Wkf+9>s9#^4wZmUuWxu4TL5*%eJMx{bl;w&7GYW>H?yoOSF^E8ZNvbd3?wz39IaF za{An@&#yx-oGZ_nfBM<|9eLYR9}OPl+R{nMU#YWPH;ehdu4_u7vVU5c&UjcZKc9JU z6Ip1X+6<*OY|@0TG9DY3k-TDFpaOA}1~*3hP$i92(PvizQO_o82sFSqh{JIkIto`e zHyBb75-QcdpHp%z7KM46)`_v`g@qTe?`^~D!Q#}ro(}j87))O~0jj0@ahb^~Ae*fk z2~j6WZrPNQ$*RiX76Psu6}2Su%&&j-?@tYWb-OgtI6ws5hUopr)f64D*rM{uD@l`Y z;6+`FC7aBcc=?ryuPkoRlqCBqE!`z(pF=G`wXTFcPeJ1oWZMFw4O3HJFt@&(QTuF` zu$S7(vR1}IuEv>TdjHC!|%bTsmi(3@N7Y%{?{v2;=_Sc?z6ir{qJ$sq2G0U z;!i}vTXJ?`z;Ek2^Tg=3raBLjmIxnO1OnHC&CRAr0z{Ufyz7VO@k3xA(1d~I!RfvY zs0p#VgZJr4HGo8cfz7ZcoSyt55@&py2Gt+ZZ=Vj^YZw-1H?7MLzIX<66&EfkFxOY>JLJr(zp)rVu{#SOcdxfjB#nKIVON=pMxBq6ES_}3S@5BoaZ7)-`Dp(dAF zSKaqjrk(avG($I(hMTX`xF(Wwvt)r#~yNC(Yg~c#_58W|@lsi7* zB!x-?PlGGKecpnrS5px2P0ifs51(A144{>=2h$G|SROj@h>D6nM!4iKy;8K?4(r~> zw-mI_jV{`i)Lssw6P4G1{9Dql`Zst4?-$)u#|z5DdVR}ZP|&y{yfc|ET_RaVViA+; zHUzr#ocE9nr{|?WLBI~zk54O!{~e!Ue8|juM|L6oNAC629fDCV;(kxVrRC)-Rt#!p zE1z}i|DF`LVe)X+pGm);YhU!~Uw88>9Ujzr^xJG2Du?L$&!xxBkEDrBZhuSsz;tdB zYi{dBee*}C>xCaL3Issk`HwY#?mOfbY(eFO!qE&CE2g@Ijhrzj{;=UuD%e9JA_ zn)97n&ITsGe~)LSA28L?LD_e(_0MXAx9FQN=gytOFSY(_0OwQ?c#n+&!3qZc zZ&^R!Da0i^f@$*)6VZ};;m}aYw{k}QGlp+661Y~gY@K5)DJq^ zHk^J7W^S^!R?MxoOeTQ5fL%NHtk|r+OY!(kn~S&N5NP<*e+CNj2ndvdH#)eU$^OQx zgt@zLKdAJ51wUs$Cf4J>qo$(58r2rRTW#8T*dqAKa;v4`?@3fuP-jnf>_?h%3*YOV znix*>LxaOlF{;s&409rFoW7zEP21r2_pMkRy96VNSx`rqB{5o^a9zG#W6@d`my_G2 z_I~*#cUzuHnQ&48C&Ajs+FU|ZG>C7s&=*oagq{X$H2~%|$>x`ns?>LN_*wS@jEL(K z6p$q(Z-^^*W*_8xif+GQQB527+TyueSSuhS!?JuA5{GVIpQ%n^xEJUELz9Dc)9##{ zoYpvClB zbmZhRAp8hYDBeAz6? zUS*^A0r_7;q`zc}8!L;n&nRg50A3m%kBC>Yxc-|NQjo!tXZEf*{Ou+u5;1kz>8qs~ zH=>Lbx$0w1g#F+Hv<->^F(AeIXYpC`xp4q9A(JiW>^%QV;KWxK6Zh_&B&@M}vEaIKywJ@+Z2lqb4{Z&A zS5#^5WbdD*v?=nAJeF50O!u~||Hw%YvI9VJaxet2=~bEPMdgQ;3dR;rgr)m_Jg}7* z;45sIFnd37?K+!vnj*zm$gPRa&920FKNAxZfDa*r1dPTywNLe8WLn4d<~}N_*y`q4 z^%blTT|bzy16+w+>7cb0OH}FD=|1L$*AY|-u=C)TYEvp`Ki{5Lg_`cw;BDx!`PP7d zBzl}71?FuIie3@8Av>;(4*vL(Z$i%Yn%w5&-7E%X=~t~>k%LPdl9QM+P^xGu?8xP0wG3#t>Y}4}qnb5R0_#9EA z0e(~%?$3XN{hsMD)mkdGD?r49wtZITO^lzbYk6@o67(oUUG2YvPG%CGciXFfua2Up@shP_WswwYH{Ahq`R)1~0Gfg9i&VlgTJ*c|bR7X(onL zRaJr3vmY*GJhyp2T5G=XEsk2UyUTUjp({>-_%lo_=)%wb1B=QV_@%`HqICNdsdJ4e zKLR=Ko8q`#<7_`a?`nlo%hp0yk9SZF(BUs-0gpLoRHx1GToFmDv{Y1}=^ZX&3WNX@ zR5J$D4V*YvxHL|;=0ZWI1Wy)vR%GFB2)gdqpiPhg9E&KcKfhTW0m3FN;Hi9pO-&)t z&`?oniVS%Ta)vZOn=$^$yb4sZwI8KK{f@TVMn^9eOF1bg%ZdH5lH=or4`=fj8kNg}aBd`O z1QCSbcCY&D%ZROwy)#GOciZ4F<5W!e1FsDLAjrarOi5#*bB5=x;JW7I6Cd62z+YE8 z{}OG9vu^yQkFu5Bx{`cUk!<~o&oHHeH^O7Jrw)z1uu&$U0zhb!#o8lIkFNZxel-&LV-Pfo8*OJhOuDnE{m3yugpKT!X5cN3>v zv$~p~tJsDAh2xWRZo^YDa%8|uJd-65;D0E}SPmp5O7736!V#SHmsX3haFdjW2gofe*#)(q11YB3)LrEz)cM5X zzh*Mh|DJS@SC!J1OG-)Kb+pFE(pg~{lfaj74WufPtjm*65G|7*T$*sQSS|*{6gUp&e+Y&I%w%xVX5S zV;dS9W#S4r^*4fy{j>)B4of#tHdn&*1=`x%*=s&mR_6R9BPND~q|f(n8IAY#6$3K{ zrD#keY}i@uZ9%X7AuS-+)pjWN+yS$r^i@GH5ZTSEs1QC;9iHjQOoByR3n#RIYmS#y zeE7z#Cyl;;UJm>PiFH+J;cGW->V$JrxBMUGH7N*v)}4k^{RZ#JSyp<6?{3E59F?I} zw}WWB6?=*t()HO;ET2cPK3iJ(QyO5zMhj&cx7H_yf$nUg$Y4T2d5@~!xuP~^Y2YaJ z7?DUKWuU=FD0S<}xk;>(3J7eXHojJqhKn;+9&vA6n^?^8KTM^=;jT*qZXlTzBe^8!Z6^pHd-p-s zO^z>S#ofJmJx)cUG=LJsuCz6H>$_5w%SxGKPxYMT)LN*UXkvEp`83+~jply7en2zG zLM>Ts+~JolOj&SoGu!`2Rz@d`z@ry{Q5ePGpuLyb5#&Aq_Hr=35|Pw#no1hyE!omP z7;zi?y(<>kg7%NfwU!_U#nB@RL*&P9&7#3H29yTG_Jl1`DO++ai$$*YTpSBQS5s{W zR*g(YDJ*ZmvALSUhhwia(+akHD ztgPIHQBU=wrwL@5s`J7lI?osi3U$ul{hbqzCGTGa&6L>`2c}>{Ra97*-V|u;*K3NX zT3}8~_>_3(K|}O1(IwxZ0Q#!|>eK7x<+ms(C=PWVJlF%sdGj95)!mQ+{Sug%Y#y1e z0B6t^&vc#eao9nOih@B~2(19;f_aDDh7QoKeMO6ioTkRgw5h>}_XB^+&LZ7&o(j7r zZwg8#KjjVCU?@4p*}*xI>Sq735|kmv&Y5ws##@nKR{GP2{8y`wDhI=Zc0<*5zPB#m ztf{lwzk8j? zdL)Z8fKQ3wv0Uwa`@O!^M{*A*`L2lcWSDloXX1ACfZnt=`wM_=lr`SBFSMGX@kz!X zYkX)F;aZr#ey*Vti&or``?Vw(Yn`~ zc#bK&(15-sMNa^Z0E~33_zx%;c)IcjRyBIVWWs-25VN~*lz+M;VxL=5@|$X-z$3qU z`N0=yU{%QKR1~Fx9tQyMwe%zmM9Z?`h(KecV0x^5YQwwe2dc*9eaU->$u@8 z=)R?0E|&eh)i#{O)}>bM+*s^=j&h?$rSUB*E1b=bvle>krkyU{!~(9{Fo5U8AfA_= ziMC(ATXxSNkKO%=y}doNsY8g;+b?~EJq#xgZ~q0fk^35t?&^2PRSqMU{-ZxCWv!u? z+FM(zs;bKEM#!})VacDA6c$P?>sQy*NPBJL`-U26s-wptUw1HPDDxI@^r12jbak^& zRtv(j)2u%gkE&DLZRq^$^dUiS77RmA-g5;Z9 zSI>CJ9HK5Y8k9QzFY?~{t;+7}7FQ7iq?B$&q(Kmr5D@8&bax}&AySG8(z5ABx?4J= z1wmRGq#L9g&fL%EJ5FRWMS@mSpnClqWj-ci~W$ouRjDOS-?}v&6tE zbt`#Q+M(6vE+4-Tr%W2#lGW;qKl8AvjZI7pTD`-*HFjWq``!V^^%j40y34{h+1c;c z=Gg>y0(&mMS`JD;8EsW1PmSZf)9{pokczA$Hb(%6VG5mTh zD>gWSnpUy(-rh~%sJAk)bLR0-T|htp4y*zGCzpwlAdyYIda+j6(2KcUmHt=yyW0rIm7vkbKFV;cfsrH5}R7pos0}9+U{rv@b@GD#_POjbK zUAvZh+S=O8j+u)*0;7_# zfS@jaPzseb9jQ{$sb3~kj##USgibGnFh3l8)xV+k9W5($)%59XiN{D-dnIP(l`;#y zpp{kjOLceV?Is4=)&#QbQT>WDo5s4Qmc%!z6?e&-X=s4Rd1Z2ikA%fkcYc1@?$5jr z_jh5QW3x^vPRfva08DRcY3c0#{ro5G2Y9z4ZcYySo4j6sJL?K9+8;M(@9jOzy)`B( z1A_F*ii*wl9I*-dcCIY%!)O5lvy<0+^4PA#G10SA#nibCcHAEwttKt^0=j|@2U5S{ z5-)$_*qiZyW*HELiTXCqpnl>;!`ft3vw2}h1U8-*z*za2UkjU@QbBE99bUX}KE@Ta zA*_>uwzfxtf&i^-F86@Z;NHeKQSU9CRQ*-CU#TJQzCnrhy^i8q2RAQoii<|MaBWDOSY%W_}0E)B?A$1 zz%Mwig-IXKafQZ^fPHyh0U2x=uP^S!YHm4z(H6Qn2? z;Mp@be8=M#Wv1bO+MPIFQrOWAOx$2c)iPh=mHB26@aR(Gs%dHl6za5w9*Dl(xa+{N z>Ych`l=M+IJ`nwR$W*J$!DL{(mL(un0s=#*ryY5yPEwKnl~zoCg7q;^t&~2 z#7$EG;Lhn=)=$@j=*Ko3fg)=>gnfO5%D?(L5Lbt9pQ~|>Ie-{HvagPDTHG-_Hr5>Ch zEa6&iNptH*-UmnVN)u{?%4N)fb^Kb*K-0zjNb{$0H8<@e=JQ!br*7fcm&-EcKT0br zZE3=2DWaKWnvATv47z2R@0$)ke|}e}<_^k&=hG&i?0zFwWR#yPe;ks^CsR{hgxyUx>k zNJ_j93MhBKCA9rOE5ocBSy{9qW-m)6K<^-=3%zG5Zt!`N0Pjuode2Kgr)RST^z^Jg z-LfwzR-Or79Xr)kyV2{(7WDgrV8N$&OG889KSqzf3$t8pxek{`#PPwovqPD1Ec8nl z5Q?Ic`69_u1s}dF5RyLG9)Hc+?-H>8!QM%~w;rjz2G!h;-@$>$ck%rjyNAwIsQyAw z&m<)!1^Ym?5~JJugC?*~$~7^a(}C@7$I9al51-xwueZ(%@0{#Pzka!{plMTGXA{TQbSj|CT5-t^i{=f z*=ux9Y*1y}sbq@VJg(401C=>6%6Tx!6P|EwLD4XjcT0Etc&Z0n=6AyGssSW=KLU*NejZ%5;1<>j;lvv^Z+e}d0Q2YnC_c1Q$qJpu$*!P| zgbhd0AIUITbX`9<-_K2(k2|`RJUP3g!6590ur{%%Sb4{L+g?Qo--@KllV9r<95uXc z7ngtGzPZ&)!J-<>6{4oaTrI!YEA#tIMCQ?l1J6XSD`nHNQ8cFFywH=(UY$xY1bhm; z+teshUWvJMkXAYG}Y9Shx=!o-yK9248$fx1Cxcs@`c;H8)4+6p1ft~UGD?DsEv+f3KkiK zm}CmS`_)gbQmXGOE1Z_%Ci7`pix{$99kYvifPZn=l?Awj;F1@*X`zz{_U3qMlcH2sS!`>rR$ikqI3^8J3Cs z@0IG*AwD}ISCiJD!LjJ;y{zMlbTJT$VE;ik_4-2jfs9G|J{{Q1MM2&3o$pA8a^yJE z!hkFfo;|7v4803Q*M2(D6!GHn+49+9H~K}yUF<)@q1~#~cqMmMpjCDEF3;ON(+;$& z4f4$GzwaK_rcu9sy#%C_^vp|~uTVXGH-vso$kH+oWXvFTAbgU2*=AF8{U0sWzrMCX zA=8uX)rBM;TjLFi1gM}y2ytsnTt{8}&W5eWvTV}V@CS?;SDbW~dNqPymB0MWV(}x% z2tcEhR3Ph8Y+6i+CVsN4#1lGANj`7sm02|M>6KZZ_rJCw(UtJ(%JZhncuPIKzZd6j zC-TZ+<9KDNv%&Z=m^`jP^Hr0HTo-#0jyfR;Sc__rA!IIpkGIP`z3?=i#JQTk;1HUO zs{^Yz%>h)>3~t^fQVKR&>XPb&u3E5{&!Y$iN210N+G)B?8@Q3=yjt>IDd+3G*eBta z#x2Hf#~+>V8!8xFgZg;M_hzb_3r%)xnnXk(flCJH{%tiS`kY6A z;l22sdG$L8Dae!uU?uaF5fzmy82{R!GqhF+pquLyWN@Di^72ctWW}WOIu3od+Iix; zQhdFb1L||ab$VuIlI!y@?)qQdhUO2Jas2)OX@4WQZyS+LExHEcRYmA2&vU`xN%!gD|km4GcQhsjD4V?z#)v zKiHF!_vzKCYW*1A`{BvE1NTN8)}ByRWjxI}RH=_a%ds$a;+GUJ1_nmcOBblEsGPVc zCZTBAvsO|?P2gPzuyU8Fav*8@r^NX>kdZxJLee`cV#FZggaYDez%Y}9fqzj}2i+3lYSeJ2%KmjfT04EnMT zu#Ou)pf<2?6Kp>}j-%=L0At3?-mZ0+D)4F0-G7J=$|@=#&at?a>QDcJ!_<|*kAa;X zZweHwk8K*xGcpdmHYV%*N#%)e{Na3=Gx1?Kw4amVVZs{%hL>aX^Ed2CgwV_g6~-6t zmfK_IZa7L{s0R2^GnaJows-YZ{MrpCoO27FzO(f%wXkM#)0A_}@>f~ogQN>XGcTd( zjI2>(X*1n1!3lTyF^147nIV5wm%4e+0apXHSa&Xr_1Ivl$FUCZs4soHZkw{>$HU)$f@>N#6UoRYzE9;%z( zYG2>js`u)oyolT#(!E^(r_-1lL z18U=CRJ!wKA;<0){>XJ1S~4lzj8JaM%s%@5%ACyNax=l&9|M#>7wivL*a!pqw{8~p zTwov0*X?eWyBwa(^f0tfjJ%HQO^(hk$!h$S!4Y&xW0J#cUNmSH$m2g6FgYdG@+yLh zH|VrtiXz8J2Gr0tW)uK|KXp zGh0$w%ACA8-$`3ieM!3h$-1|aYUvkJxGZt3$XLcjM)&>VIB7f6k`l zYHQ4aqm zjoCzuoy`o56~te%y)<)lkTbRywz0Ccu{O4Lq~>Omw01PMx3#g({6s;o*4tj6*;G3*&!Z?VI@ZNmng`{R_dr-0#R9vfjF7 z&YB*hGR4W|V&CAW!TO9Vs->$7eZ|~)O(&|*LL*c&BfBcCGL882qNgY=k&>1iz6knN z0wX+JT>b_Z_xc1j!N}&Ap7@yKcn6&xL3eU7lV83u!!fQs@jP3p!)X8GnEt>01~nwO z@S5>oe|Op1e+1s;`8Tg!xgv+5B%RDM2#gP4gcRTWOnc=s1L(d+#7S4Z*)}jWU51kY zIEQ?r#2l#xL_`*__NI@wp#rH9f^InpC^s~8fV?%`0S&_C3T`*BSKJQPT@TjP!#AHk z;O0L1M5L3Ykd8r-2tBR`o0GZwpT9eAOjMPc59RPC1otnmtyx3|PvT{50p{fV35;Z) zT8?~w^uYfLbuhFM7f&hy3mB3JUyAUd@jAC>_tINjuJb`Ai~EtC{?vnozB}hF%xrTsIiC(38 z?z340xRA{)ECB4E5B1OX_I8jqNGNwpM??S-+jL}hWG^}R?_XO$ZdFx>f$o0}kp*As zq*(habVgD8Cf7riae%cL+RnUA$ETOyr>4#U_w>2*`pB0rxZ#^ulB=Lq6qq;tQ`sJ; z2kSYi4W8#&jy*j+nU@~m@Aw<-xWD+kvIrNbqM$aDk9qWB0cDdaeUy-mtI%P)3eoR&SaL zb+olj_7V>w6}>64PJH!+CJ;tZ1yK&;PDsna6{RIO8UPn|c6KQBW|FcoGrxlG;pw|^ z4hF$cv+DMQ7BH)kqWe~Rz|1f)GO7hl0tlx~PBL=sAi&~-LnVXrQdoFYngg_rQ0j>_ z!*D|#8aTL?GBPs3adG3&{)vSaHpF|SGC|;jZPo5# zTWy2n~^ABKEgqlZIEjN>Kg-I`u=5U(Bp&I(seDE{2mt_<;ECeV% z*MTlJHz}hPirX2vT zDunrYEWa`!d#HlsYhAGe1Wy_XZ;F!0(m)zdBH+&L_7jILBsr_3pBqHWofrK551{ZB z3tmztkz(s)5v5S@&{%>tjG&+(9AeBw^D_yMA)X8xA0P5#A z^C5#md|{t3B=YS19NMVlo7C^oXu894e`A9_X!I}$cAG-A;uI)N7g%}o`lt7GcUytE z&`FbxE<#4;cdCji`p|0Gjv3QOAD{jZJ3Bk*jzG;8*SI+7&H4s<+de%EB4T0%(1Qcj z>UH&^uR?0;dFAlTK++OgX5D8#5skRKBF0X*efu_Z8@H*3uYCpgMx|WY2VR786B!bD+ zux()wFoLkM4<(QuBYq8!oH7I7>ifPk?&#|z4>2Ls`Wg*^?Z$vF7uv?iHa7?61}0Qm zeh@;LyMAU^?$LXUR}+_i9%C>*Mz(=qSgf=NK7kWVAb`<|PL4t0Gv#FhB>>q)<;Mg} zDn>S5KU!8v4y^EC;!KvAK~tIsYz{g2DSnC>uR<@GuV9b}dOkm0ZioGCG#otIN>qd5 z;^JmB^sHg}&H!yndoEvOD z9r*j8kd~!o14MAhrJ+;)68I6|fCSFWwpV!J1TO3KAWd4_-^rzU;5`wkfX^=^6c#D>gwpwhvU+pKQHJK3+qo9`I8;S zp(Pfz5;LB`MRRc}kB6dBS=!;=?7ubpg7lNZjug4~hYube9xLm9=MuT+&_$9dU1Yz2 zIPNhx%83Pepzw%INcl1D+<7G_xzd+<^PJmZ@zv(*tB;AAu~vb!3Eg^mLM|{0z^itK zd3j&hXXL!q)~Ck(cxPD0jfIgBQ7ZJ|&d4m>AzsUSPK!Tt1zhovEonsg;U^MDp?*Ok3!AJ?ptV zlkICM*^0jM^Aig;+=~#oMCvVsRh-Qc{$H4v;=$w{o0eP90C*V19e%<*?Rm-~-SOi` zG>0()LYk=oJTzY{vbM;OehSW(dd~QlZ@{?+f;=A&PiqT`5iOQEFqE$uM*#j}6RdP2T-GEJd%*5O^^Qj_N(LD=TJ|EH@9^-j##1ucX^U|8(ecnQ8U~P9caJ zfj>)&vkIu_R)QZIgy&^DU^}U6Z7(1QvHrcf(CmSyRNcoyw$Q>>D4*&BM{KC`jf8`g z!r94MA*3KCx=b%d4oQ2h{ZC6Nsj>fix~0gByK^qx#N1gQeS*jXK7uGaHTAm;P{j@+ z<>~}v8(cazVjs!GtJS@AqM}NI_T!Gryql(&|NeMi);~YqI6J?z1QRZjLvkquHq}jt zY{vcdxcpg4*$S4H>&?D6SPdZ-n3353e$wv0K6w}Tt8%v(BJnk#_UY`+V?Idl<>kej znns1CE1A)*?XGh5-_QK}YoI(jG&&liEoQ@x6N8gw8J)Y)_fkiR2_uEC9(N3w}|`^(?YidtdL(tJiU8;s<& z+U<*{%9BJOIw9PTdU!x>R5*+&*Z1FN*g0qx7qWP!Cv8dvUQ6PUGq6#lc0>&Q_YWKY z*AH`$kdQz+u>wPqV3N`i!Sdf5jP!rK1>v~qdbp{5K9qN@oqq%M{{Qh$tOVqr5)#Q?-zaa5@QR=|lxr{8M_A}@ zu9a&6l6!D?2t60Q=dI9s(9^>PZE$N})d@t3wLYhqe`&M7A9O|Tjx%N_p7SKDdPy1t zXea|Xpz=dthoUUTHe7T9Nx5<$0f3PHIV40@S~}ee@3H%= zr2VG6X?R!s8?A+S3<@-P4ZtPN5jY6?`uccyczatp%DFnO3k0zRGg&~%ynF9nfOHxI zr*^=M8X0?P77<6#=al|0VgvtPlfxwNjUbo9kAprm%~BI->9%I2d<`z(MUS+gGMa*p zH+g~4@`IQ$Vj}Wp5~(X+#IDfas6k)&TxAVy^x%!BW@!K^X9S}%Ckf6IgO*!1Hd`~P=E;ndC>Wv4>+9U5kt67{NLUm_)O1;WyVR-f9p!CUPUrF`>++{9?yP^K#~MD2F^d3_!Gh6S4Ab#C$)ssKun&^h!jsW{eDn} zFzMA~;&sAsn#Jn$)Q1#_+FUV6Oy~p}vMawY8ZusxlwN@OSXu*#F<{>rF-meDi^T$1`w1!b?UD-oa;|u#&nXYDW%QY+WQmi~z9bDu=_XGKt_`gk)=S?f7hXUQf_kn>Yyu7je z_0Ag_5L1~Aj<5Xg#K2a#_H@#?MXSPKljV~Y7TxU>`iOUCWx@2Tp<*jv+EVUVq$;E% zd=5?Fz-C!c5RG7?`mUkgD1`W)Q4<)wrqWfHUX(xcPzXQ$?&F}}c%uCM^c4MS;WC+S z6}b6?%}Wl=&CSr_V2bziW|4}D?U%bnE3EKr2pnt}EG@VtaYeJQze;rykq_ZS_;M0* zm43d7Ls{epD}Za*Lj`_}8BZYB8Y6hp+mxwXZD0~kUIT`}(Cv=f^jE)ftG*(9j8rCz z^MSvDU%Xf_>nDj+hA(2+De3ZASs&63la&8ij-p?Baxe=fCcXbK{qA)*?X2MYtiJ5texFmv#{f%qgP1+=ddgKtoZQB1mHb^*^ zpH#}jT%!`guzJd`?acSz?jDq`Mz$99Ji~Qv#TcpmS&3Xzxjq(b4w*bm8IVcWfy0*h zaDfLfC!bCh+5O+Y^fM&xXP=_Jx!3gxV&z2?B`Fgr!eQV-B^J3<+f<--HO_@Sqfj#t z+`fMPJy-@EWw3Z1$zTTJbq1b%nfH6ipcG28a3Jflu78Ian=LDi$)K*fE1EDTi)KN) z{(7iY)CK4I_TxatZ8c0Dk&`$4GTOKl3~emRIklO1Z`^o=)VXryx$dsQVRO5Jx;jjizXf#c@-C?c*q@w>yEx z&R01=%g@iw0?#;h^mqq1Fz_6d-pb$fsn(V^2u852ewV9Y!bnA4cj8maf+I1>NjAzh z3G63+u+iXT2u~K^{^xhH#tCQ_v-VQ3?>N<@JZ6-XtN1{RfJn6A21aEEH2ld_0bG|o z@VTR(km`VsDX3Sk`&GnkJIqvuXi&fh)MU$I3*Z3(b5NK+sMku zmR!upH|DycwJKU| zLBV?e2v(o<`~Jx#kfKZTlzaCd&87b_*nZ+h8lmHj;2;J(czgk>Q12cDJauPN3-c5U%dut*~iWfwY0I z;lugoYKpuUefgJrD+BC?ZOe|;Fcud4teA$i3W!&)zxP0Yqt2&Wy=x9e!pM61qnihsLp+l~2e`u_4%NI?yn>Zk3^drysB zjc^Gz6hki6O8yziTs@<;rNu=6ero^)f&@a|LyJ&MAswHBVq<2e5BjVi=w@+?6%k~O z3vs#GB;*w34cFX%vV)lzeu9}b9ufKF%b$gXZ=f@ue>0_y3X|;135BMPd0&AzTyEph zZ?Cf>JBSQ`8wXPf7VLay4j4o*Ffw*_cjtpn0my=y2jN+w!v(j$AAilny0vicOj$?B zQ@!CswYC|Yz$y6OmdRv}mly~92Eb>G$t~M7$<|R)U9?6gwR&=$XnA?TXE>CF$z|>J z>(?$T{R_uSNtd8pGe{n7Jd+g;FxmX->KGs*K(FQUx%Cvyi7)Gq#MyBt)5VnAAyE~& ztC%O2Qw>1X7=V{r+t>g*+&RBJB`8Q0XM|@mic@JRxWhn;bAq;=6ZK_SFVU=i0nBqMWHNpjWys5CQiHE%RD{HRyWm zE5}r{E)vf7#gq)-Z1rN|7Sd>%ocxxRl5(31dZwkV4Wz`bbH}#nW(8}V^p5OsU@a;G zUrap3ZzLHY^*#P(tv}AxI ziuC6E64CP&qO&c;PdBPyLEY6Kjm{EdCywPuaMTBQUKdc+IiM)q9V@r`>C%eoFoov? z1nZ?}Rdf!fbQld@K0Y@$H<&gK&-s)u;Ko6-p0d3bFHDaB5B>K4>8F<_HxJ+|x$R|pEL$Ko=7BqabGB8lVXs@k!5;ksmzNgSBJ@7hI z7v(S>NY1mfGGIUD$T|dM;WQZ#+WDZ!{@DQGVJN9 z#zM}=tdhcGUauYum0N?1_ZCA3I{I@aL@-nb6p_c)L7xKxQXhge&^Eu5j?NU{R;^;T zLOPI1c36a@nOHX$W$Y#N(fWnHmLjZPXfV#1rQQ8hJ_ZLx?i-4mJ2Vd-z^M<^L4Oq6 zr0okYJ=$yE^gB%*q`MaPz-dkK9PE0Z$jbHrSS#UC09~z68G+-UeJv}50Q$f9rvRr& zB&eeAFr`CVo-+I}5C}u4qAq_su!8``x)pbDPkt#DqExrRQ+u_RF*U_wY+Jd9Wd$LM z*}k498hu$z#a)n9Wa6^5!B9GyfJe01u5?)}I5b7#iky~}e6yF_cQS+JN$E_d_>zH1 zFbH@`eJhS|!XZz0Q&WqEuCo)#40T9?e?`s5$CLl&4!FVQq_bcAaNstx3o@w8iqjjZ zWvG1l`)*jC)g&dj*GfW{FXyx*{WFaFqM?POY~3%}uEI=s%k!6?uWoO7q6)S??x7Jlhd9R}X%ER{X(H%BkJi@3Wl4aPRQCde5=`kphbRQvH3p(2nO=7_ zw!+%2m_D+BnEdpU;Tjz+Ef+|oK{EWz22pwAM-kHjxTKOtwStx{(8M$$w1HBGRD!CR zN!61XD5{lZqD1M^h`5B;!fJ|*?CrrEszD2;2lO##@8(p?Bwv*yol*L+%xA4zqeo(^wE%HJE&(0%P4(IB@N6J(4?XO#u_U46 z>WH1DYet@Jw^QZ1H{YR+VSK?ml^t&+cBa_pB!9Xd>G))-yqNjsmWYzZR80?H|I+qV zW{~~}P++YM<+_|6SY@Yj7#~9%$pfJO^5T4VSDKV3KfdEqTZZj8Db@4Yj)jcIHvezKkz0(Nz)^oU03y!CTd zfq1I0q}-mTNM<_yjoW~%y_T$CGZU?aPW&1-HtwURHB8D%t1J~mXd0H3aVKg!Q(Qc! zGe5}$_cV4xM)qnHmbg?&0VkGnkdEgLUHbm^uO0xgoRaoH;X6ilO%3LP#9Fk?DriPI zKP#v;aY;9j+$7*k;Oi)KpjF?kPRP@&2u^f_bj&@SFQOhYhZza*Q;1^du&Ld?;ev$P z#?Lz0r+c=9f0YJX%7fB*V{PB_b!4Q4cE;VnBJI7(&LBC4e(zos6*>9(pFfWsm*o_# zr%xE&t+H*{Q>`nECp427opZ*s-)4T1N*K0%AGPK00f)Gwf)S>6t%$p$(8u!$gJ8GMp!;bZwYr04Pi>KeN2Cb6YP=wuNA_U@S`+fs`+12Hoj@2U z=(tXF=?rIxeOLt<(8>yOI5B?BunX1`7(P&sWt%2$pdiIUSW)Jx%{L!Oxq@TbRiE79?RguS(q z@@EnzT929IN~rjP8gJ6`PV%IGJjkwF3Kd~6Jq8q^ zRxcg)aNw9)9Jw+P_Ui>qw%#y>71m#>*$DG-wO4QYlO#8Has|baBm)D)1OyAvj$U(3 zQnPz+q4e-wV7vrtz?~GdX|E=)rwgc|m^gu2O7cKI9FGB0(_O8#hhcz8(pU?V1bS8g z3-Gf^CvX6L(akm_teSPH2i(6}!~aemQQhq(uO?~JxA`nwZaF>Q52*g1h?Ce&ccrFv1-9vA7ypH^VGV4ZO zeq%LH+U`k2t(2V#fz!Qii^94{bmZm8T8FV-0mOiFbcqrjM#MFIGX2- z4D|E+>QD?h^Uk-qCp6BDGrM!S+yx=%MCG^0$a%;22=#A}cW>UYe*I|p(p4>Wzxf^| zG>C>Ko-TKP-{w-;4{@hnLu*2$+4ZG}mVF#8eeKXygUTI|>rf?%G7|35?A~ghGj)Fb z9eMV<+>ep2ivH8;!T0aqvB=&`eK3?v%B^mB}P~Vsb~kk*2gb4I~wH`2-w7 zr`LmPtZ2kb`FskkXoTBocp3YO{mfaicL`JFNMYoqFyX8$&|Yg#Dm7*wp!9oP#$>$B zaiuO$4Vi77xe9TfuBe+tu|kfp9T$7FBLvYpU`ApNz)wb{tj8dyqi7vEt9GHP>mk}u3U!_XnuJMi<~8}v!@y*C zwu@sYM66m_I(59$OFtl$dbH&40l}2Bp-}iM!Y@y^0m9d2n5@9e3ibWeVk0Fbd>1}} z<1#DfVTH-^podI?nRD0;IZPfhba6AQ1@>l8NqfZd8WbbQcRS6P&Ei-Ts^TVBjLWPB zi5^vL`-D0T*Y||ZBk^J51!Dg{XG@@E^Oz*TbHg%(hyKs!7#c(wbq&GRP2hNdCa2u$ zVU*%*Wti4oT~d%>r#FLu#&~EHWO!UA0PxH5uqW7X2)q31YYg}`@Y6%;no|wSleyo5 zaCQK_K}de7_>pHPNQc(M$>Bzq)+w#<-0&Yj<)|FrHXBk?!2Vq>F9WNoJV;g1#^G5_ z*g!cu-=$rY?s7jmH)aQS{ z4>ML+g@?_T{E(!vpgjS*SNh_!Cz7fZ(|aYQ!{?+%{Y024Fb7ShPu2^kWat(~|z3DaZ*W5{9`6`XxSfe!P7T2tO#Q7yGHpv`717u&fNS z)h0AYdAtoWR~9tEuMc$*><+KB)7PAlQBg~CbHeC7us6W1N&>X?0LyGcBhjQZuNbvS zcF0%%Gz-naE|>N6Djc&S>+XM7rnEEgLtw7daYf+?s~7Bpy647s61WvR20=9x%1!oj zAFC%L8OrwIq$%sx2RtDAi6$37CwNrXM`^j>8QNzlR)nF>?jyll8x_-w+({gwMe8Om zFrHgRQ(@EP+fC=h*}IEG&t0DxiG4DzJDB8UahkHv4in=oafjqcsoG|uGSu(@7$IYK z$)p&^9w?(0j6E8Oq;=iCV*SG?DHu|@#sO&Sg~?CFV=uB=8$J(8MRs)RGKP|o^PqEi zORf5+&G?A(`1Ob|E!JbRv@&;aU0*j$ThstoJcWyD!LS3VjKG_)b0}J`fj5FmmYn|g zcUQ@`&`NM7ceRTJomTM=3EZr6hBl0VIXAI!RA$a>Ms6`qUph+RGA>qc;$lDcwBPQ_ zFS+Gt;->N2qufLN5)7npMxq5*)Nq?w)+m^azlY)KOZoDVmKaW}&Pd>`Kye*_PYRty6o2JYkaE)d>LRet>xfRf9@0 zDX1MrpStHTla^aq*6OF7Sdn2S65g47Hmt86k~}g8xqE1=Sz6QqfchI78!qc3jsGkq zdn7Y#dcvhOz9F>JZfu!Wsjg(OBYZ=M>AuPDA0Hnd9X$rwd4;8jQ8gqjYVhj`vk`h$ zWYfnRy5tmkpAUca93kVwBh1?X|KU(2RcE6S7jFj|4QvedUtudi*mxYEJCq5|S4LA` z`t$3?X<;)CZ=LzkOYUE0S4KX1pzHP&1YjwE=|sAK;x5jj(Ni4VhQ^ zFALs?0SVtCV5a>2HWxOk*3YYvMjAVrF0p0oSopXj(c;D~Z481LvC|>4iyF*WC%%r( zPLx{RQR&xWdWmmqZLB1iir=PuxoVH0~w=?XJG*nOk@Cz9?-W#(I z-eMFK6yOMu_AA%bP|`{>to>w!;V`T{_Pdxu5zdy~6nEg(iYU=+!W`|htHWF|hhpqx;J^`Q3 z8$DbfxXi{!Qi?_sI<3eq&+wOKw%|ZJ{zlpUC+3cm$R7u(>ex4eweg(TWKZ)@z2>2z z3gQxM#egOa{6v+bVg5-Oc7S!QMgIdQ=oV3U&G}`tycpdEweGhjb>)yDElF^CQd%9} zcnC^HaOw#+3J#MKHk9P~XV-Rzwb&^n8SI|ie~1?q(FS48ctnRfs>#NaOX ze8b)2yOLF&Mwh}HW*E{UP}K~87BIF_hiRs<$}+-qD*(d105b?a)*M5RIYtRg15rcE zzO4JP63oS9N-biz5hDaO#Vs#W*h%k!(NDoMKR-QTTs$ce(UzAY=%RU_Ny+>%{JlRb zq&?fZ&~YhPYX4+B>beyEDPfDsQ*ZJxzB%S~kE@CEBG2c(``j_TWZnXyZQnK)Ud|rb zHX?y*)tl5LZ9}akS)Z#klF(HgRH$%Iwg7#uZsCzr3un<{r%sl(v&?WjFCkY(MuszZ z5&#H+I;-&i0uGg|z%j7Q*WriA(4YdsjBvKMf5~*b+avh;PGpN{TFDp9XdG<`ElV0q zkq#CQZM7Ol6%mo(SN55#0LEj}#Kalw9c48>^<7ZHn3)G-tb}rvR>F1{ zI9ZV_QWGrci`|v7q~To;rY+wdD%EZp{;+*eD|6n>_h(y{deD<#Cc9>bWatQ*$G}+) zFaQy8#X@C#7)?YXm?1xCM53gr^&|;5(*)jz$Gp-+udtD`-;jQV!1lC#Umi(c66 zdWQWKgSNMz4G4p&HJ8HxvzAn{y+i*ntkbieR#LAs(fvf3W;9<2_ zmTL9YxB{w=8jq!vVqdEo+S}U$`~~%XHG_i;*2itA4>A&jGSZ23=&72DlPb4{yMGbt zpbST-^j4sO>W~}62Rp>C?rtosR%TIo0!0p1HKa(|tT!zaM$9n}KXLf;ogX;2Xrpq) zc0S_A*D<)9Ces6;D<$<40JgRI?vcF~v8mLlY4&eN=DM5XSR`6&AGXEBUwqMLe|-5_ zqn`rI3PbDFUE~)l00c(KXTb{p93vqrDhkxrXB-@36*g2>`Ti_w3Y3hdmUATzpnZR4 zhdw|*^(^GY#uD9de-0=g@|PxstO+;N(X5%AQwU zIWv?Yt>UZnIrDz9!B{+W1ep@V4zlMs+*D&4Vzf9Eyy5a4JK|}G_sR2eH)w4FKt(=Q zwBtJe_6DN~YBHCIqZZvhmfX%CG^wuNSiE?$lTp5k>`d!8Wyssrxy4yos5)!!Oo}T`Xa0~I%Hxjqgv99a8*nar_X|lNG>4lv! z)u5-tZ?c(_gXw_B2g`y7TS@-Qihb8bR8dkRVUMI?^FE)yy+3=c^#iRPUbd~i{;Nn_ z%(juXD!NIbAXvcE&fO_SSN+`GfR_f^-N##}x=?cNMVQ=)G?M3#DoU%Y{n5in|5Bh7 zE&RDi{ygqz1oMk@v1MDK+?cpHUteFT;&Z^X-;r8bT7vDb#@YV5w^ogZwF+56S>DJ3 z>V|DRiZ%hVY00NC^rP|}gDwWuMuB)4EfE(4qZ2t)s5Wfxn^aX*L9=^BQ4zP3|67A{ zn0Vl_hAD4A)`z;^5yx|CE0&^QfpYmjdh1oIlxSk)>hC{uumip)f{fnnCbfX+?$VLp3A?+~mTY8k#=_WViLY{$$*EgvB3EOC3rI-j=(IaWOxlIe!L1MW$oL{s4}7HwkmzY_y#FlfWG2y{ zDTej|0TFYDjEN_<)RmR2KU6{dCv)Lv`!U_7v{6^SwgGz&&?7jBszz<8gl#IqXO?uxDBxvB?F`viOvk&)boZ3hl+ zhg*t*x1o@T(j|l8Q8FvO-o`CJDB@>jbi1ax^{I6gNnn80^*@YO)z--ajmF2K=%2Nt zUhr~FRXgQEqXm`c(fwI{&On3-PNb2aL@T;f$%}By`AuBgkg>U|(}}x{txt1wm|SIu z#X8bMtvyi{u&SzKK$>@Uc1$fsBf9Rwxyq~~#i-@3su=9y?G)!E;k8=@1*Ovdur>Gp z8H@%M2@4BMk2?Fg7k1+zfq@?b$Zi?F>c!zbu&%TzO#Y6AewNc`kt}$NO~B*C5&EM+ zDY*B8gM$OpA<2UW<(@)9SJ%e|2lqzUSJT~@UDcb=gk9EOJfbY3Nl?576;=uIX{bG# zT0kMd4OF6lAF4%8#jAEVh+f~t%_s!AlLUN%t?sxvwz{AHdH-H_iNJ67ue#*WoAVQ= z%0WkvKw)7CDp$D`*}@P*t5#wln#SQXd2ZW?LoqH9{(P=wYuUJu_KTx7=ffk*1&YM;!sMju;#pr#@)B57C@iAniR|%wf%c~(v+KhhOQlK85 zhgU}0s9txvSp)Usfkg3-Q=UNcDK^_@v|6=pC>Y>y-yCP6|1%B@G^yHKzeqOaAan-! z`2pdG;n|7u#BVT6CgU{4wdP=y^11)>a~-|JYPgc6oxANF;H`p^V0l*~@)o1%$YcTE zR|qo$w}=1ujM!YJ#J^l+Z!Y4f2 z(^H<8k=yAEh-Nq!=}-*5)MOqgKnln{;^rP{X)%KCA9VD=X=PO73>4eBX<<$H$(3~7 z8a9d?5sOE9Rh41p&zI=Q(`eOvuMqZQeM8=N%xeLifd>Ep05djP53M9 z*hC#BN93W4aZS~@6oNqs6!h;!hWeLy?0joVQbqe-_wEyrvsez@G&jf5zn=odIv?oo zkO)6}@&qkzu);?7piq2f!WyEGf9xNq-mhKJ^8Q0Gg9TQ- z5HZp*At~0Wy;1!orK0{Z$$k_1YQ&taB`gJ>un;xJo}D~GF(cc5c6ulzPD6E0i+mb3 z)gQ#nez({MLNQ34iQZf)Th2u{s6NR1)tY9S8zfVA!1{yn$JhL!;OFKaWuLjX30@o- zijY&e6{S7E%U=G=H0jcqVpO>$yrD1}kw-z|&R67h>1k*<2gXrN((&bl;dh=P;*p>9 zpgJltrU7XQiC&;pyh|tA;IfmjHLZoOc9898Hjt-5Y?jk`&vsl8-)&Pj>Vl65WxrPK zUMOCE=R*3g{byy$h!66qVt6i~DWPe*)m!YH^z~VHi8ORSFt+0MRREI@zm``k1N0ag z>1rw~-^u&(W&ia7m>1Qk>3Cn=Oqsl|Fr}%?yajFXBc5AJireT7Un%MkMTI(u^#d7L ztif9!aKMu(xe=HdW)@%NQKn*XSCv^)K2CN}jlcPPznp#5>{Y(>ugS9|lX=CPqY(j? z=Fc4#1Hm^5&fa)-?yztANublF(kOH&kTN__AZgzGt;=`i8>Xy{2-C6R;uVFUA9LeZ zp=3!zlM1!_$eNs7P9W5YLX-g9=Yg^UY?->!u@EuC zHK(eY8sSW`O?1GEt|#G;6_=LMTw>nnpPHhq9!fbqn1zlw@62mqmL?{TYO@C4@Lesx z?V&L7&p3(T`ad?nS^8K`7-T7P`^s<-Hv0!YOm5o0QVRtEY*>((5W+R9H*G#IqlEd#3BqIO{cB?L+7 zlGsR!l%#~xAR&ziNJ&U{cMHO%LrPLa1VKPbl-klMDXD03>59EG*$Dm+kwIK|bFKv)?OH zkikS75CBaZine=6^wRboNJX&BO=jxI;CN;|*g#7W!Id~;j)C@cE>Z=G{eb-;g(-YQ zb4;~_sTx&PWDAcy5A-mxx6RraBD0|;{QJ6pYKn%^4EwwfFylHnZ$s4F6mU~z!_sYJ zlPBbjWYY&KTFc*VM8?`nFfdwva2UKPGk3#`)=}a``HE=Mm$w-$a?+*nfdhTf5 zYIyewzEDkp85y8?E4#FeT&@4t+jxpBGQqOr61%E%nQ2d@tOp0R8)L zzs22uh2iq40P%6M8sWBY5x>NRVg8K8cjDz)RooB<5v`ZMZKJEa^4qX)kB-N1>u%Re z5K{ABrtB&)eOD+cTS$t)Co?LW2sjmiHMP7{p0|t<9P5hTfp=pps<(xH`_G?<$GtDb zDfd{fxW8jHL^pu4s3=no`WPABJVCEC}iKj>F_RpFnSBzg1+>ANN-hgVhV49Z`8%3HEj zIbZ>vo}Ny?C`m`sRRY~{HAxw~7=dLe62}v^chUFZtc8A}-kD|nM=Wp4Cy54qSRh)v zfGWzUL9Lr=fGUtSqs^y{inV`3oLq!JjqTt=s(1t>oUpF*-c zla((dUT;@IopF8vogY4>-!8*~xYnd!BYXGAiG_na^fJVuc%P&0n*E4}jR6F5sU8Zu zrygU!MOuK@Bz-gdgKn9r5++{4lW6;7srAj7B)zJ*=0pGBDErGjv)#VQadXRs&-yUT z3>8uSI}82P2f~dr%0o8V8FFzj z;DzvXmMPEF^{ASf=D~z>r~D9JWQE>8(DC4aO)IWECJ(zrE907upMc3H5m0<@0Zl}SqKiUR>`mG@naOSmZ z0#PaeY21*)Z=v_u1pl_M7x}k;V+Vv&rkrWT%1oauH((74$Z=;==G28yQDB)XL4Q-u zK{AP1^X~Ky99-Jvy7nMAiFy?m73BrAPw)|bB$oQx$jr6u8i)``3N27Z0f*1Y_}13w1D&Ri_UxB zLx&n9IZ>)C_m?LNKLU-Jll??CCDjtVjoE8`o&wwzJC|ne1cs&-)7)GCqT6MV9+j)^SWFdm)_ej zBpgwi8({8_{X1TZKYHmtlr-@~^Z=+TFv@4XrCs&gepG{$IS>4|uJA535Y zf2=1KAmyHfS!*@J+#3-7AkWhB>eg(=yAOeRGV2^Bs7$DL855YD*3 z{IDnj@gb4DVK#n^Te&4Gf|_RtXO8eH1j=x%AHIHC`PdPho1teuV5-|$z!9V1%b-xc)Yog}_9$e&*GcyaTZ(4rcrdoSBe58a=@D~Gk)UEOaONXBrl8r3^7 zjlMN$WpHNYR@S|vPfD4{Nl$|JO$PM`13%><{}E!!FhugmBgGH}G{=y~m5=HOHTxO3 zMW0RF&PaKao2}G@p}ZOUkbj?U?lUJC^wWUK!p6!9oBq~o-4Xv&F5NeS-xCR%7@sU} z-UeWr#?cQw+<9IR2vVs1U$jr!8LB+}vyeP*VY`0&_)$1h{J_Kn6{Q&kHFb3SQW9q8 zyG9z??0u;ZRU)uyhZ1u0nLwn&5c!lLhBI7$HgKVm9ltkGqb~|Y+I2JVUEy_N#Po*i zefHx3pLuFpdL_kX{VI0X@ocN^H=JN-rRXzX_R z&Q@P7Wx1}O{J#)m9Asb~S`vPAG+9-&B|u*D_CmFr&qq~jU9X4X%)}$Kc4l=<7|Mdh z7+LvsNW0D>nq!LDoYtvqRR)w}i?|lpMBu?k1I7|!@dIFcJ48uFNjhDsI6-gz%>41d zlaF0@t|qAZj<{pJxZ5QA=n>rz6olzeg+k_#`)^& zZjSNIDQtC?@rva5E)R0to(UCDgk5&Q)C{h%wv+jE!#o?Q5~Q&N<=((k zf^dRCmPenv{%-K>y&1Oo2;9033@q#$G~q9=QhJp5&`;~TE+xQ>b8!6LoJD&gxs$pC zm2doq4}zp+B-(;7{C+n$h>%Tw&yGgf7Uc_J=%a0msM~k9|0(dN4c`{=^yf-miV;n; zpSW8)1T`*nf?8bW5k6-oFpZ$rCn-bwo}L5nLr$L<)2La^qWsL9rWr{_m25uc z9LGmshI4%ytct?aN~5Fk(o5}rW$6iDH2eraw4uMA*>8w%st3Cq@B@-oT`*^?f~Kf0 z1dC+&7%JM8KZ}b;itm=!C*K1s1f;Df`6n_v@5EClI`P#@&1qweDdNsgZ<@aVO@yX6 zahzgFXF)*$xD3-$Q=`=#n*HdV6BR?1_?6THM>)WAniVF%|IcVmmu0xmF%rK8g6WOU z2423}+z3IUL{?xu_5^xz=!YPpkx%=xqvI9B9k|U}x!-l&38;sL>6ZH|GK8=^hXISV z5YmyL$KH7clCjXf6c$zvx~E*?BAOrf0ZtXRXtzIyXevmHoVhNEObe&2y16F?rduW5 z(qYPGbA)M!=yLnR2SA-=kAL=M<$Hh&2h^6Zl^VU8*O2%(GFDx@HZ}D>Q#JWb!oPf@ zb4O|6g|=bF#~_qnFb{yliz!1UebN$(4wi?OkCg9!-jtl+yH>J%XSxL7bQ$!BZ86!1 z0{K=2nku-RB`GO778b1bJTrw+$Z(FUAHPr~{(}5q!cY1y;nU7=7>4h` z>aNUrV7~|htFUX(JYZ--+MG}6W7z9&=~u%Oub0mc51WO8ktO|4Syy1i&&_vm@Jt!< zC|1btcHAn(tdV)tQ)&Uos1jYS1W6g|Iw4(;*2xS30Gu+Y`4MZo+8bq#IQ;zld5@u# zazEa+oHEqlH=2}2!PKyz!t%MIB~0wdTpn0O+qZL2`vo-8j~HMo3Vkk+w=M&78yl0Q z(CGlP@jbmg0paJib7kg$`@=D$b{gqKaFxR_S5>~_LAj*Ud{9*tpUy7w5_MJA-STjf zvX{u$`gFOS6QwRpZAgfjmziNxe1)cHJpA2HI^E6S(M2^)rOY^udyP-P?ZH^>`3Jek z!Esv!Y@v_?cMp$<7t{Y{bt%`-d&}4N$r0?wv-U|j7MfB?(3p}WOB{V7eeWKs)~?lP zvZ1&!9sD1>8oY44!uX2;hus?>gTegM)2&t;yDNwuMs5xw=t-;Aq943dR_p7tZv8FL z2}sxN*NJa-;K@w22DU~`dtN&(s`3f2vbB9(QDG0$?Vgm&hZ_;kWh>N+bp|p7aGd3) z8a;V^hVD{VfqX6TiBr7@zX(F7YJwf$d%V<9OXmk*<)E5=8)GPU_zFCPqR*T9(AsjW zT~@GDI;w!ziRl zY^%dE^@5SQqrJWV9@_y+1iU}dNM|&Hu#2Oje9iZnxr)hizE?d#i^Yi$h=?*O_Z46>m4L*IC%)ZzIMMC}(+kDD>T?5V;|u$8>ikkhyi z5Cyo8frG;hu}mL(5q582nh)=Xd6hxPCqW!u=nOpk>aaHhRK9U3fd``APb z1mBuL)}>PA|9BooPKh-K1}!+Ov6}|+>tJp#=b3KFl%@5s%#qAg^pjW6xpeZEYnX%t z4ir&_Kw-5`28X*SFEeU$+97~os^VB)oN3IOOQXkRhJ$;Hxazkz7%ns_BSuI>$GWMy zx(d**mq7@kCj_y6OF*gwCkqMFF_z>vXh86WsAvP!^k~g5P~$7EAP#?jBT6=!dY*!B zF3SnP~*ju!=`MFAF0|?s>C`u)w!(q6s9sx(=Sy0g`pDiN=6I>sxtMiu8ynSgE!#+!IVzI5LC3*#!)(_iepdD)xYmFO8GU^Am{49G~j6q4bJo@>EKnH6rMn0~bg zbOoZ}-g(QycSIJGCDtZvHFjZ4HD$;N7@{<^*MOfx=Xx@aaqwDR^WRvO)ruIt1;xE* zO9Cf${NjVbDWf&z?YicGc0_Mc|GRjJ3iY2>Z;a!-C3k@^KFIQibvtABNh%s0D^k06 zW57V%CCtZushnfn#1BIO(HyR~e9s&hg<^_{rUvn4_IXG`(Z zqrble=_HD`)l@b`Wn5MXXSNNCgYdChSm;qAlJW>Hs>G{Z>py=+Oz;MW%?4|I=4c4K z!H;fLeoX~TYbqDt3z#F6@Yyc~p)V-BU%V)hta=W_BtX>Kkn6CPbgvicvK(&EQ72R! z0Lu?s{Ih=~ZUm#P^AOoOp%&$JXE;zWGk_flBh2p~e?s3D|3j7M@IU%VjlL8L)$ znAn|Ts8NGjNyG^2T^?(EAo9}b8a;`}%>xp# zd@<3R_5+W9&0oK#_Jm=A)jK4UTpOM>r-MySi~$OSde#Uq5W{*y^Oiz^#&v?8{n*P- z`*>^QaxBtD^DFclFDT?_O*M95&h1p2XQ9Coi?j^5j0YFl@$Vu(NgL)qpHNAwEy~k+ zmu1w3ea!^1b+t;3!8%vBV8n;2&u#wVC8VYz73$R=oQ@-Zn02e!KseND z2R|F2Hh>ovRKO zM^t-Zz+j$)9!u|)WRgyTc^wh;w7$$j%j1Dbzopx^K2$M({`coJDlUE{3dd4@nftx4 zz_giqZH7b1(@YeXz$nf~MeRvWv)Bhd=7o>LUCfL#b*}q3o718xm+ss7JEh}Oa0f|E z!imt{=TgFYS;YaoH`mPh-0Eo#5@tT10hrzQjR;tAy4G;HWU}7!`f`!zr}pX6BSyNP zP$$8w7lWTViVF;l1GK5at+O$>-O7xyGGE3@9hb>}z??v?>%ZT}86F-6I!~ytp@QXq zxApd;3$(DSM7j`FSs|GOB|NB?ydkb4Ly=a&|t&E(7T?NE2?v`dmiQoNNv&)>F8>{-|< z_azXa^E6&F*TmkOKDaDdTCmviQj6pE@;EOiCk;59R8={fn%-C{uGiuu)c4fW(_1Aw zK0W2~=Z?VDdzEvk*xzki8LDZ(GBr>1uZNy`oMXK01&~T$S%&=IUBz-dhv^Nt?bW~* z2mydrlZ2ki2;vi2u)TxyPXfZX^8R@+pFet4{(Of1&({Nshv#9Z9?kcT3MOyk==@s+ zUcbTIRYY7oD>E}9EG$p>^UH@edFp~L%ZjUH^b%eg@PN4MX~UH5J{bH;{kiuiVyfx@Zee~u;~8xT1>LxoB*OD zK*oEZ7|{-&;jmFS5@V5&fD11>gUi57{}Wy!9AaYOV4GZ8ju8njJ4}+=Qdu2uyL}WSaoHe|6e_ty; zA1KFwMI8V=Sq|2)&I$1{JbUyv3$TGj4cc&h*e4+v__zM&m&jWdmX=^9(gV!%&*Av+ zNtO}tNvXMWSBuZTvET0FL`dK(m={+TkAx6|9VZ*Bt03$^`4bc=XO|u{~i;? z|5pys(bATmzntOFOw7tE-)GWRgv~eg*5k~?#Qbx65uqGkmm~$s#crNny%XYn%Iu^` z-^1j@#xKcB7pZ?QiDEw!rRlxKFO!Hn~I8xUcTfjiT!TW?{E3G&(ia6;}Bc7pG8?I;Ms zHAF;2l7(iYl;iZ0w#Ihoro0cc_nTD9;?vDPX^3XDG8zf)Pyqxz9a3x#OyH7cEpkORhbdiR> zK$w?jW@Vv`3MG4rF7!NwzI!p~4Y+zmJ{Hh&?a2@rSePH2MbW)@!T(6Oo6p+Dn8bwl zQ(~C%&*`>SJY%(`=Ac*H9sc5c%k#siN(dpi>Cyb=o60lWY!-4{UG2iYyw z-`>(^Pmo@~&nj4f9vuvpBMGRHxhBKQtEHW*$&uHZBx`3ym%P8+#0@q4-IyKwuVPZLCkN1qv*H8t=5M%?M>njS){@u32t7jsio zQQ`V42J7lr)MYseA~JxU2YIeBt#4y#%!e6-_8BHUPDhus1t$s9&4X4KE!1ur`I?8C zx1PtkTs=-_Qn<&Nfu70xs6`)d%rijr3u*xDeBmKZnegH}@Y*!^9N0pGD+UCgS;`%< zp2Xn1ygV@YdO;^z{cihW1uLz*({~Lou1R6?=z-eTTa#rgPVGfTH(fVTeSm)YY@ISU z26cULDJ()BdpO+Q&n9g-2Y7l5jyFR?Ly-4^s{p8R|63^}vT|~;TFZfwn2~Yu_-hy3 zKn_eyOnC9Zx>Qelf9~wshV66Tox-w#blqtqviHhrhl@_K3XMt8svqrZhssuRj)B$v zm7b+b112*pL?HBg{iyD3Q4wP1^Z;4%dUM7+zQX$L+A+v8o%5TGjZ zWl(_G88BYg>lUzj;5s1rb{K*ibqvR-a(8Nu$DMgT#0MsqJ@FQbWDzsW?)|Z>hf(O zNDhyT2)*8U`M^B$88!Xky*&Y35ju{+Rg3%goG{L zdu-YAA{l4xF3D)h3~0V-#=L%=$Q8YaH+M=XeIgSl8zYl;f9vL5?UO+f>_~Z`7_Y2U zpHs^Xpd%-YuS8*Hs|J{f`e5p@C~bfqR_4mj8{-CvY$? zhzz$RvCw;)tVa_{l=#0I_u`CV=&mFh##NU!@5|+A<)ixhJ>1<9nhD!jc$~{lB6Nh2 zUlyE1d^%u=0^&f1^0l$@Z(zZeEXn-ezrYV@k=oh)26y*e*Z(%Al)DW7|G$MKCnt02 zyl(I6N-|l%IRchN?U66+>|q8EK@(Vc{5ZN|jpw6^QBROD551!jf#%uylAB<&&W z?4~kTWaTu73x$i6cw^5T(r@nVK1{uP*xPUB@m4D51<|Ch)x@#OPqC-{4TMKxJJc%1 zX5^NmZOvL2z%qMZ99LrT>>2mCQ2#?N$+l3v5fXKl%3>Bt^$5L@p+GkE;^I%=qWkoA zNh?ctz$*M=!~FZIm-s<2%zWqg`j857O%>}4*J5Dt;*u!J7(=sAt9J*Z+BXY)!py#z6z*@iS5xGn7$}& zPkAY2(G={EwKg#DswL|g9+m&b7W!S5Pd$`dKP7{ttn^7iUhFjPVb5+HknIk4JYacR zvlpmv!3QUa1L*riMMYun2l{|vYd~`X(TJxMJ3G2fgbxQ_=Ee1g7XqY<$*_6MGeSf# zX_JKUL&W#GJ!Ib1uQBo;?Hz22PFZ=Xk@sO!(0g1&X?5sqH2fpm_?d%p(UHxvzc_C; zcrT*W@jvQ#jp{Xyh<*AR8L6lyz}f~xX@A4<%fdv8;2gLBM~iJopt|B2Gx4sGY)yMB zDa-vpEWLHGa}YXyqAYSihW@k3lGlIix7Ihknw?qAzIY(A;xAr#5o9~-4;Tytvps%1 zJ3RalWjv9FSWv@za|%ST`HQKE3B^qsZ>3nTxqcO9PLkwbUYpug*~__SA%B_Be@ND# zBNtnw@@&duJnPNnlgK8=l>ySpxU@S#QWFG=+eh3v+=ySpc* zr&WHEdMm}jr^foGT>a`+xusb>*pNm1aBl@Zp`+PFc^BugafO2bi^QIPU+!MZ%Wr|w z2j9`Tr*O=Ip?0wJjWYbxYStL7*QKSm1qCnv-dtX0jka4x@GQcei`QXe&-EVBA2M3e zof$E_h(3hGo$-?4kle@PwUPpN*S|DQ{rjISenM`CusJO+FQ;-FJaj#zFutZFTXNat zC(uI{V%?pc7ta3Y`^T!J=;7`+5uvi|Ok(}D4_kLzUQ(ug<>jGS_^3H%C@gYrKYQ`@ z)Wi4!1Fzu}2>Af=LML`|b}Vp6`NvYpt%Q{rcmLo3g1#ZlyP;X$c$gWU%c5$?Rmj(Q zLk*9(gV> zyC}m4LeQ43hDJSXl#$=MRSetVCud3*T{~Rn$f~Uj(+W0)_Nl+u#iGL*zJC1*qx+Oy z!RW=^hdeLn`?zmPDSD3$K81(zTPja}s|8Z0{Y+u-*5ppf{IEn@R~JA7K?DK;wxn<0 zCZ^Ryp>HO!1W7=vr*fy;*|Fl2I4Zbk;#@XOX7-nE8|s#^(TB*-VScL2EiVr+KHjuR zEX!Csn@b#3zU~0H=e1nGcNf=Rw}>!hKU7je6)Ghqr=+a)*?aGp$-(1CI0W0w5u5%^ zS*w3v>4QgSoghSV}-*XCPXGPj!_X-IhBL*^EGHvrK;sO{YNkrMNx zkjJz4sFkHGC2Hm`Pe0Wvxq0j6&8N_F>e!I}APe0>^l zmhhZi$8L6L^U?5o$?ZUehg?Gq=(J})bbKtG(HP~;8ZAz4ZaT<=M9+XjLfZY}nhO7Y zN8OEpXE?>@xwkzvCE<_W0#Y@?z`y`1Adad{SP^H_ItKDNRYY>fO|pKzy1L_F^$!OH zC=Ju;&fKlCKlVL;qBs6Cg<>~QVVJw15fU&Rj^34(ah$jT<4n~yYn{%QO^riJDiE|X zfUvc-#WgLFF-Mqie@ykYyl%CUyu4(K-xHIxD>ZM+C8^iGTs&_pvoAkQHwq{H;7e8I zd&cjiZ$M4gHm0i_zg#@iG^(TNtz>45{gy&8AT8;+@%ud^nTHVxZW|;kOO)?PLf%XPwW@cy^omu|C;Xn9z7Bp=I zsaSk&%G*`YI|H3ljGLRl`461r(3^e#IMyvY;&T8t=x|5v?CfND4o~!x%NHy}I_!iC zKGZj9PrE9#;;URCZ{L$zz<9FiTM*ahW|X#I{qB9paai4CbktOj=HYU04b96s1Fc^F zQzSygXMZ(VKmyPM`6Ry|U&-D2N!@&dEx5S2WbFael6VO2J|}Ye^e7}j@%iBU*n%|0 z@jBkeAFn^)Cu^v$zs)*1JWT5U2VT_AX-&WZW$PDs;368cOdfF4QngFG`GCAxStJfr z&DgP@Ei4{(@8si)!r=vnCeu`B;TaK}LB8U*)OdT}9?CWqDi?Y(P6Y$)`@ZLQaCP4| z(l~{t^79(B;)-@M;D8uB1w#a8+YX+eY8laTdD)>{dv#Sd{U7CT8ZfcEsTVKr&c^}*JZ6ZkMxr^0a zUS*wYaE+{?a3G^1KN8wlUxy3beitzX$^#+i^7~2aX-C;L6alqDhw4};LITC_w6gNm z7XH5X?qOzHuoaMfNlzg7L+g(*7#z-mCL*DBdlQyx4e48n!D9^Z^qx$7ltU{0tYr>g(aa=JdZwnq6YO7 zTn+T98g^!8A*JNy`Wc$pDJepxpMucA7^^QbVV{bI`A|$uOlxbYBY}<1kB0NV;`<%9 zjOwKgdoo;9b|a09UpWXYjdke{Pw~qf@6-B{_tC1;mX^PMY8`ZCf_(a0dcMG3T8>8J zULp@91swdXg0<%T9Y+c0m3}_oojV@@3I>Ya19kPAV!oE(SeBV-jl8Lrjmrx0iMcf& zUPspY2xTsV7VA@84e;e|%GuasKoFP_&>v}E@Mr-~qHixNKw!&tu%hw{f!Mn(n* zxzO4r_^+I(sfY9TODie#{rzP^b#aFq!`w5om8GTnOqnU+^+Uy}_1 zk;t&&2kXnY7#*NKS^%kQ;kQc&jVP9kuLk#$nt;l4gPO8wJlW(I2Flfu-c+%=L#6jl z-*uE}Dlx0dOr0J-3$=xnRcFF7$lnsihgnTNx<%Kdj>8y!VH&2VmrigOi688`Uq=OM zCq0X2Hn>i61+xtXWaQVcH#IkRIG;f-573W301uyoBaS;g1ZVCYm)-P)YT!%in`h`( zbhVM_+3&SAR8Nnuw|CnOX#*Ya_9?9Gk{6WyzRBulTYt@(rh`*2Zg)cH;( z`m7ML)dn7Ku7r&QNqOpg#4W=b+!Hf+sSp+9<^L6qy0zo}scI5lT&2XTbCTSSZ#PKp zuV0dpm-$n$Vl|5NU$!rOyy6)1POK$5UztMeP*TQ3s2A9xL3x%fF(kz$t8(_!uN~6&3dd&Wra(kn94`r`bRQi z`D^jQvx+SOhIk=r5-EOYf%hnGdqkQc=x!c3A^5fZGCL4zBwo6XXd^gDa)k z8RKJAi!B7!jtN)^&tMDj^dPa}=HeO|8`II$WQwz<*f|0$D2PlNQOIJTZOs4k*LbS# z$F?;`+PfsYWu!@PGa-LXOH*?PK#sy%)trhqP4_MIxWy2}ZeLn~ECkTpO;%QTlxuD#KsIwm2;V`);oic9)zXKI;oAz7RZA z{SKcYv;7SJltpj`N! z&8AC!xxpxIs>y37q$X9#=-R;y9?r58hB@T8IddAxU@} zF>;_YDhPc)L4F^Zzl6sc>aH-#&y4G&MB^YOMRJ3!xM?ozJNUnU9Z%S7@aho1GT!L5l`l?_tC7j*nWe z-^919L%#`U6j*mF+PS*8UIt3|)OR9x8$W&-1+$|Vq`I+C;@CP%OCF*%giH)Ong}cH>`&-;x^4XcGBGv?igt|I9 z=K!DD-Zhiega`&G3kF$e8TQ2JE76LO#b<1o2lw?|n)PBVG?jePUvVBKvN-sBRrhJ}`LB;m zl{E=9Ad-_^f<1bH1S@cD`%ul0mW*tq!S(44QI=4z7v!8zqw!Fy*+nqJLv?o>HAQ)E zd^XZqdlalop!B^hXnB$yX{V&xGTYbpKuxXx)2B;SeAXHo#F-Y`j?(BoC_SN<^(meV zKUc^d@1K3I?@WsS<*>0I!+(P_ZjIzIc^o>mK{RlCphk8uJtyZo_!hxfJ8HL+7=(|K z^MTOlf)MWDmihglhUaTX_>MGdc{?qSGdsp-pYm5^D*0VbN;jfFu$5fKti;@q5OLaG zD6F0T)5a0Jr?8Hp2MR;TeyZmH4H0vNCHuRpdrd?IBiJ62)p8}*CR$qXYO^qTh>37> z!%V$OAGB6cCSKaxHGf8%wRZnjC91>W$|J4CR}h^^?Bfb6d4Dxw6^e{sgZZHm5!ms| z;L-9*sn`V$t_1c0c0$4&hhfLnC_LYb2)Bd_VRNihB)`GqZ!6FBf-|7Zg zzmdmxhJ}T}#P#0OiXjZanH|{Y%K&k$th`tWeJ$l}N8%&g?~IF_1A+LFfKj?LpIB&Pm{)ri;505XKAOh>+3~^ zr{HCV$d}izUrYMRORUCGe5t`$x_MNC5vj;XJ+foBnJX`Gyc{qdM$q9C9*{F zxqgpv^)y{eO0t253V?xz1_sm(47_)jQAI`VJyzfe46k=2TLbi%()?mi@f76+d6Ahf zy26(C##k#`E{HOhmA?Iz<&AxdsCuh%H}G%M^XI)V*d=aVgT#?RR?sUHD5LUu-QdGP7{)ja9zRfOx;>906#H7H(C`$rYbJr`wXm@3Nqm*A$!d_6YhgBf?O=+@4)WZiyDchu zJPlO@=)@twM#&Den59{}XQDf-u3?;#dfa<=Zza}XtguUo@ZJ(;xqYl7(1V)F5s&GK zz9y~h@i6ket6(;PKx~x2EgEFK&Hm1grnYuABp4b{JKqdfE}`bPz-1l@{c3=TbY2r& z{aMu1eG-cGbKUNe06pPFNRX!t?I^pq85n1m_yE zV%HK>n~7x;R4emVZlHG_hcQ;#8PU!#i-?{AJEfTxdW$DdFuG;{Hn(C66hf1N7jl+i z%a~XEsjBnHMz0PP?|6GeLp|){CDdt7w!-K`R^DCFuLU-h7OLhN8X8b`)YjIjDF1TY zaPE>~T)W>lASfghKCm@i_hO%N0ii?MHT$%BPT9lbj!&eUwL~DWfUOnbXjQbMj=?I? zHfa9SWZ9ECcvps%2?W35!(e6DZZ)@WcYSE-onpsr`}zIYa_y;0i)|a!u>*a$YJugp z6{lI##Ua(q!&CL@HnZz1haN`RhSCx$y7l$>Lx8s%8wa$7T!~U8S~a>)N&CPBhs^&)8bxQ-}aiDBU*Uu$x@#lqsJx=@(hUx^gr=f5`! zqDSR03r;=)1%8}@aYf8H50X0KAH`#=7sspIAr_T~FM=)ZIuf8hcVx>S?dd&ym?`RN z)l7ePW~a}@=k2z*lD{T&s3ex%FVSZ>->m=1-5X{O*v&3DRoFcLHI2}$M!n%-TkPZr z2JbZ7^KS@?vqlD< z?I_TpIyySY*i_Lf&$U}I%rVT~(^z(wQFV(sn5g#-oA;B>-uH<8G>~`ih;zg_2aL&S z84HK-hHXZI>JRP(#rWuGnVS*OI@0qWe%+x?mrZ^YX%abA!H#TR%H_=beF^o38@3-^ z>;2u`ka*)}1NqgZZ)XKZ-uz8rDt?~1KCd7%F)=4ctwQGho8sQDUx!CW+lxB$J2JE| zf%H&bS_~9hC8-ck-^Q|osbrNA2J+qfB`{F_!k5$ zOt#cWf@aax5vF0A^aO6UA^Y7mN9purHXwAA&+pqA#-H=~j8x`@OFD7V7WnZhC?(Se8n zYe0opSPL*bY~Y*Y6|`pWw<2mT%2>qgKkf9L{IP^uQ6e77c`$;21PZW5Bufn2UhKfPeec-*LniPw z^rM5h_n!TEtfxKoX~^`c?1m+_4HnTuW1+fxKdod}Slw%~PD4j_Uo7L@*1!zcAjE-{ z^LS=N@$P3?)qAByp!^kJ^#<9b2y%!8YyTmQu1jiq!r5&Eog%|l75xL3i&!e89A#Gv zJ?Es_BNN@zreI4Hp43SFMlP zBfov0aKOqhKT0az`&w!LRL+Xz5TSCpXqqvH#F-6?jOP6O8~|MfA8#PNxQcWD--#q* z1*q`&Ep(E=(~u(5o& zCWwoiBpqX;m2yt#o?O7E9NDKbyf%b;Xa_UhC>`utjao#bMgz6#F!nwE zg3&i6i!K6-QbnG1-?n~O2Pp=jcuugQAiPo^G`A?A?%lj?z#7{f)(KRL zp6>2!sTXUiz&HA20Pf@@wSXpGidS|QTvFkza&F~C#4H0gMG~vTfsAZ)kWMvX%l*ltav3p)ol4$^o+A9 zFmnpB&``2OXxtZsp7#SF4ny`SgI!O3X5(I>yV(8B`&+vZArLhttbHcppwG?BjCnUv zh>finHnux=?i7a0nr%*#k&<%r@Q{?WxUC`Iyjcfp`k^d;I~$vYJExlvL+bD>mGtNu zyzjkPolzZyypruTk^=q{DYd!E(f6m3yHfjZ`V(KCV4Q{4+^MYDewya)^y?i_JBiZ# zs@3dFiK*m*)eISlO8Ow~bqp2hJAbN_gZcIhcpCii%#a$hnv??5L>TIJSN#pc!bu4C^J+0+4+{ z6sU^s|AUWzva-MiSz6yzvlU+S2(v=u&W0c=>Md7-YNpkVFnQZYMc|@(drc6p1Wh&( z_AjsYp?sZbE{QI_6ThE>viQ|uL~%q1yl|mMji&sGDWM&b08~ScG*+&Z_b%(=`l!_$ zv^t&K15JS@a&nev6|5@Jrk*CB5^a!tWksK=k%PRgjFn?o2h*BIx7fM7UoD z;Bu1(MvVl{7_d{GUu~Q`1cD?CR-g-vsLwZwQJVUc^(O4k)2B-=0{mCspFY-~nshKG z@WxbqVz2|%<|F+Fc*-o_S4$RM!`oh(!_*F{(x|H$BkVBT1E>EVKPn$L%DesgYKw#e z>spC85cF(#xs0wwU6r#R1N z$5C*3C3tmCH~M%7NM?9aqTn&I+O;2FIhC0|-lXxpPUXFmXRfhAMEd70R^Hf5BrpZ_ z^m8^kvG^=nKG#pSbqeeL{L5>^n$p&<_Y-wh-Q1jbkB@H4@2WE@rpAo;bD6bO`$KxEufKZ5q#JsXH9v|F;uJf+HpF}d8~dTSk<%HW zX8nX=nqh72e#AA6s4QW%2Ol7dQn(-$c4AOCMYW{Y9|&$dP~n=@+{_|rpx$!TYBX~d zsE*wW_*nBA#wNJCm(%~7nhY*~SGmekafJZpCJLQyP4^3lZ0r%fg7*dtfx!x4&v4Qd zev?jm_VlTOAwKXni>ebzA|k&Ud`<*5_KbzeuMU4@Eaa)kEhVlSIlU|#NUp3P$I3UG zVQBX`1^!jQZmf|&BOPSb!X_1#98Z=@oSAm!3t7%ao&~1`INAy9xm(dKjxrBV41Q34 zL&wNH0#mF{l0qkhs|G$}p^h_S_U>B75HQeZ=5km@o(O_dX?;&N9@3;{!*PdtCMH^Yaw+ZN9Me)+*I^nj1)Sj(nukjpN3nUXO&diPC?#=o7;FZnkL#xBg3=*lkMNq?17A(A#R`kkqI zHJP*gbO}KDP|2Ybu5|j}5RiVkMABoOs$d+2!t1xQww{oEBQA2yx+uS*A7K)LGjPRA z42ti(lEGiSH7kq-N_IV5mt7q;^ls-mE%_7M@SM?acv)i=&n_Z%+>8v@hxGwP3OG9g zDV>y`;=4Pkq%YN6VXEC;20S;+G=DH8ldX9FUH}GRJx=XRF&0kZgG7jmBBhAGyf!=u z5{Kg$bf}t4p!v7Wjt&oFA{YK5$N5=d7VsL?jx}O zLhVdDjd#2SkkDAI)_RC}^_x+%54mQG=a#WZBZwsn#@pI3PRxM#u^{p&?=5rnVn*S$ zeP^SP{ywU;4+dj~hXyCK!-g{9(ieZ8021|r$}A0^|7tbPNB}1R+48+ucN%`BP$-wQzPTAMQVW8%FWCq>6jW08ah_IjWiB5!v}b8(u-4TK`)5Wlc?S=W z2%>7*m#0C8Xl@>D!gvVhUvX%)r zZdi}f6BMVUf3!AG_f@b-Df>S#e6-VGhMH})?5i;Nsiyf$2UPL>L;<|=Krrtu|^_Ns5(;%s*}XBUkaR3SbZkCkL>b& zhJ$1*4r5V!tW_we+h;gaues0hu&@hlA}8in`thTWZ2N^-W?t@JC=H>*4jW)uM87~4 zjq1&spC6PLvmqB2S)g8+cLb%Q4w62K7>z9(s|i8Gy_{SJbQ($HpDC-o_pFAyISjRj zxzhuq{i@uiq_K}D2$O1p7k7v8aso^B)vhWAC>i3Lj4Df!0W=tOnTmMA{q_b#!2kimktp2(NTAbf{pF1(Xq?rIw^gJ*n->$($E``x?5sAjxH^2=~72zF@w*>KZzA zSLUeishzT);Lqh{0y>d*K$5F8AtPECW<5P2ZIn6k0sab6D`va}=Tm^(aWz$baIgLT z?JB`Ygl_DGGA(9*`vS*ilZ}{&L(hNgua!0Ox3b^f^wQE2+}Cj^EJw8$y(y*9aX~@L z+fIv2M!JeHt9U8tPh;!cW1hkOSIbBE^thSs+C%&(&-?>7t8Xlethe%66XY9dWkS1&|>^MESj6?3eA$6CIC=-Po2XtF*?RzjjSv4%M50vk9 zicUoavDa;7E{&awza6%~g=>velKXRFHceO~hbZfxXS;FoCnS}2gq)cgM0>u}*0{hWIA5CjQ$P{)77hg$s^XNKm&hL*%h?sHPl;X6!6P^RWr zzrFYa&AmH_#5|>L#!3A9IX+??hv=<0RHy`^dcf!q1*~tCgS1qiHD!%2xqRu>zQsgo)J_ zPnD^*{Hh4HxuK+?Ez7n^TUt&LxgtaeE_BW!9BPiml_-1SaNxl;t(0zEx_Uml_a7X?a$nF`U~-y1K*Gy-56!zuk=rDLMN3LmJ;36v-a{4IT643W*#`^%^-Db|~c*eFncGR>O|*5#UB$-L)w6|+sDfzd1G-LpYK{VB7Z`y4ejgUhVh zuz~gxLf=O?%v_5^BEbPL1$L)585$CM_h3ap&kaG@fMUa7klaDr8nP)ik$Y2hr3Sag z)^2QZTFpIGX7O=>X5^Bw+=h))huNIR!+w(v5q=>FiF)ubfTGpDDt^xQgyzoX+NM4z zKz`-O;V;)iVt-gzShLnL2@W3K{e*{K%JcIV2b)w?RiB=%4{ls0gBr*k?p!AoQ1pX3 zCAbA-@4yL@;cFIFdaoUU7-1ar_R!5Hj0~jt&eX`(r;(h|w+#%Ss`TW6f$bM-{n!=R zsRsW_mBpEObTjGhU>&({Ru#4DVG4yohQ+NU(&`>I~7_5?1I>xb@rn8R*(7%UW6y|M%2&?{~7@+ewF`#N$b8PhlI1t?HS zCz+Q}IiZLW`$t{2pu_q%(3u(;1?)d@y*j;m>@DC3kKT6;z@}9t<$av@7!W)ZT zck`p8iEQ$+C$2qi0=%IY+N5Z2C^i3*01pRVoe}PQ=6JivJ-s)kL2E0j65etfajy-6 z6|L=4Qd6PEBNNdi!11`Asj(Rnv_xsk-7xkAY@4>+XY{|pFqK~&6KOs!Gt8i9RZB-R zEmS%F!Sn|3DSFlyI!f{-t6wjP36`{ZoIjna(sxm2IlS&9K0E!rC1C^${eR?MXHsjw zq|7#m58(=h^Eb$`Wn?6=irUh_;aW8at}UnPu+a%cDveo`iblqFA3fBlTu2i~>SSGU zlTDcXP_wm`4R1Ht|NM87+4eT_%PVDd^?&_&9Z3q62EayIxutRaq(>V)Ro-4XxwwR0 zVu@gUn2V&M$EYm!{}EITJtpm@TN{eEyXtde)q0Lq4~`fu1qIx?Q$T5VNm%brYerAkR_!-j=Q_fJNAR`PI8tGw^k816F>=US`S) zkH`MWorOuFuSnzk>g++@()(-c9qv~y{?MH~m^OKJz9tuRwL<=llga{|K@e94L#48+ zgyzpmlH*K_jP8L8-?e7|2*dzOm_RjP6bJS1s+LOXqBuKB1SdI9MfjOaCHs6AIsp7} zc(L`scT!VQ#*MVMwOvL7VBH>%VPAyMfD(U?=VJSvxJXj!kYMJEm>#<-bTZ;6gl53z z3TXdS#*c_%{%jNCUIyz|`gBvSM3?INiMwC@SroPB0tjDpb|(IMc>`7kV;jj#*+=x= zq^1Q%)fMRL)YX!0+%Tm22~!}BYoke%VBa00uOyrcyjx+kLX) zE6i80m?Lo?6y6y~+oeA*6Zb#ZEc`a@;cl(xp1G5;!88Xho0AFg-i+FV+GelJ^NM<# zq2JZqC3~4xvO7t281_XX3*Uivw{mR~jf>Df=uQ zXy4&;fgntC(5c?P-`p#GF*#fMGuPn^NPUPon9X(gSX2c5Hzp8Z{YroR$1UX%k$3_9 zG#7`B8SQpQ2gQ=z<(oi1$-Ez=rKg{7*D*t?GCZglh*7@XIG{_wBC_y;(n=xOeplne zV@8`W(0DmAawgk#Z;t6|E7Zrv&a%4NgouRxx=G1nK5LLobKlbY5_2tqp!;<)^K{GaZRL*mfQAI&v99Qg%pY4GUsd(undHhV(HsCqr1~ zx7GF7R!f3=?Z>Ja8K+9$Y(s_#JTD&g`;1rFv29MP*jc=scK6`(Ouo}+tq`sV?IHLc zH>{;M;SRr1IiXB1Pv8c;m41IUC=q#!){|NpBT}>LeZSuR7j}jog`B} z7a>nzU8a0RqahHVe6Yp@v}uiiCXD!&-3A5E--Od{g{ZwK&8Nu^9+_yX?xC^v1@FH> zS`qn`)YHZV^46k>pgbUaK(}w5u^>C!}4zi2dO9%9q4+l=kf< z?2Sib53M7YJ`boqW!Yle@D-o%I?IUpq>A(NXSN)R5WCPSCfv?r3b2<;?_swvm$E&& zrvW-8C&UGeofdkT=rCC$YeY!20imxIk3!5ECogdBi7YsU5XSi|lZ7p+Ib!u865Mr) z;ckQ?GjI_iEa#OShKq(lwzM4AMPf;9;;Q_vu(HTSqs$%@DzY4{z5*IIy#5vWcH@XR zJ}?jp>h2pEad0wb-QDWHZK8i`X@b=-zvKd%W!Yf7Tl-;uW~k*RXA1<+NbcD~3X!}r zP5ko7J_uwrAjzd7{SJhDz<;<3W1=+gI(%}3Zark_J&WJ?B_5H5)&$NcZu?>8Bg1ta zxaed}{+gGx&UWlV+;u4h>z)VQZB(r!>lEA4uJwFPu?gDV71?7$LbkgQ3!B43~SjQ6Aw>M>afL zr9p9%NiE*$#@gSQ^5V!n4sK%59qjFqAzE0mm4VO{+QC!}%U2Yg7_hp}X80vdMbRur z8xO?gx#Noc+!i`qf}*S>g%aPqv9Ym{$s42-8rJ#>pl>}+5IgNqS8Z+CzN=`@7qv=d zV`+dx8;-;LgGKH{idK>|>I}X4*Y{xl1~(n4@61hVYG{`PJ!65(&M$+xu6+-(_T~fY z8!rKnNI|zfGBhO5_1=5uj|SZDklh{kR^iDeyQ~U~c!o1b@SWBxY21nILA(!>(cJKl zF?w#>%IK5BKUpren4i&(wk0Cl1N6CR_uo9c`pK{Iq9N@p*8|tGg+Hfnd1d7eH#bPj zO&)7VaUhjIrj*c+z>X6|+5X}BZi|mkQAYCY;fMrE1mBQZxiRDPipKV*;t)MyTGZIy z+TD7k6q#)>)RQ3w$&3;Hyt&HLl$L>PUVq%4q8Rey$Mp)KL_<ZHE@{D`zh zH~!FkRGqRHHNoihtd&GjC#v$57|;5PFHC<~oKvgHZCt?#f^IOf2gH zfJmyPU(dt)SkAky&dwnET}Xpbq|{+56&5n?{BW35AM`RCwQfAl&pi^lj^_Q85U`k2 z`jHgzX?;Q#OjR=5FxJZXQ`82O*Bl#nY8Srj91x6XCnQYH`*w{-No?W%b=rya!=_e7+(Gr;g#1l**8cx0`O2mVmiz z`38^tZF4c!I7qq(3TlQI*8(TtlCk7&IAJmw);HfXO z#x7!Nf&tq;M9VNnNsdvisYo!cJofpGk0{%ejxbQCPu?cd*#0=_A+s&B^!p0RuC?XM zs1KVLFdWI>Po};KsbETL(!WBK=$aFYc`oal=*}}OCgp3YUZs{fd4EF;d%DkdjK-4K3=cYY@VGBUCMjs*$?Kl#w@vy{hh3|r zxjI-dbsf8X0n<6bK7rX_nC|4oKCuNqXp(XV`#T)^JZ|{7l0k23MLQ)KRwIMs@0s(W zP!Mflr^34BD}3kjyBURrE|kQ;_&?O?<(|&*%q&7b5Zj}iY^$oOLdKz1U?%DX&;TUc z@DO6yykHQ|6__Tin|<>?_+K+=W)dHjl&ySC#W#XYjEA?NVNHTo0YGd78>qDapW|(K z)=AxEDQ4O;aG+4>c@@O!8I`{d*n<|s3;Q*=-)qUotE)KC!&L&!e;5g)W^2)Ghli_c z$VC+>i|TChg$1T**4cj$Izw#Si!+FW-Nk#^M;55|Z@NLene|~WX`8iaxKjrCkIqgt zJw5li=1}DCN02Od=gu9_D#1mhD8Wn_swvr^Dk!qx@xnn-Q0+1Cfd)o?bUO5!<`3wx zm_4zG%xVj8!^E#}ACW_TjrQ;})y%&PhLh%ZCOx48ndn7?sp zUZj6&l|X~;(>X!^etB^U+YQuknFCi5lj&?P4Ot8(YIM5(2m+LF%${nw`T*p9ST^)J z#SED`Vc4tKp6-}r+3)E4Jh&EfAHUnw=BWTSvqpR$BP$=T{wNAdqfQ44JRAy&idGOm z3{L}8_siXlM;83@_hOu$^>~h7D)jo5ThF4WgIs>>_=vcMO1KF**xd)(RgfG zvXePeTF*E)dkd7tVCtD^V$=p{y)M&OUmvLkYz52-=jxK^^xd#|*zG3xIM(IjP7xGZ z+h2o-m}%F0L-YM{z?5GS2k!TS`Lf@a3+5I3{WTNrM)*Sod|$AcPs ziIzQIcv>_)4Glg|g;|pyQsH2z{x$SmEKglRIkiPu<6w|gRn;zZAY@4q+1X6XPELP? zh74W+x`*s;3@v!c>&TZS{9s2+#G17n8NrkDwf;feJ3o`Y2<({st5I+L%+sk>%cA*90jw}I|4Xa=2 z*yt29`1Wy7=GRq?PD14uC-jRAF3Ur}_Ru*9^_{M5!kjpeSYN~RndYMW>IU~Nw%OkP z4E=zMqUx&l+c{#8-QFn<84(r?uRRN7IwHJf14YZ*q@*lyk5b6oBJF_eV4BkyNHN&c zqMZU>m#1z=>ud)p3a19S-s)hAo`!Y5q+7>LngmYyh#wq*kh@uspARKSu%QR|us?kG zke3&1ph*Q$D&Wbs{1G>4GLOEw`;75rxYhVK*NqHhRp4^ErEZ!bY>q#P2nM(R_S3zk38tPe5h$oN-@#VsRExeD8tAO>B@A4IQ zbu27dF;ORAp`R~VEhua9_N=Yd;3#d6lE*vZQ#=D(CkxBM`fGG@uCkQ#JWg)nPeY>y za8_BSMwl1xqOa9U9mNI=CVQNnbHo)x!7A%Q6ubDHhm#cH&wV{6Gc!{rl}S4nx!^yZ zWZ8np^b?Kd2sn`aF52*2i5#a^Vc?`mea|9 zw7273tF|3vlMnY=>bU{sJ&ftG8!nd9NbC!%w6fCi@{gT&czX&ZFAKMtt9Gfu@CJ zoMetnPt=JxgLQlY*d}aobhwqH7G9IIT&AV`)+giqn|D)|K%j08v#DxI7|F?Vi;E>} z2N11QEASetZ2F&q)M;6K|HahxQnCt1e2B|!=?AgU>l$^FCchgz_Mob5vEPqzpiD1q z#r^r@rQmWtaqE(QHpn!7&?E$j(=XRwmyl+@j8|vFUDlLpVAFyOuKLUaR34KP{H>D^ z#t15=unjwj6fuZ4%&nujC#F_IwJWNP(AxYJWT1W(;ZZ)^5udJe>>Ss5l}Suk$tIXG zzEll&RUFJJUS3C_xr-mc3WVe{*#vd&JNa_kFn*mBA?QPc>-u)|HpB5v*}uUj#%|q+#@SqgwL=K$~3ycHkn zp8Ky08+hDs{|=tdtL~-hB{1y4W!dOsXk;`F)Kx`#WRC^qcZCAtK6LlD0^)kkAIo9g z-G>IILh}Qudcxk?DXp*Q^&aW=)HfdX^ToWIRl70 zXV7+ek?S%VI$$_)!++FMnP>?v1T1PNjWR3ynI)@)FUBX0R8_UdO@514$ig<%(|cF2 zhu)mT*fXh>O+iI__bctz09;wn1xGuuh05XIJ!v;>efp4z&I#+oC2rVIN4HOA=KMFY zZSBu;wOH;p{A_PmM0S|195_Om_(^p;$1Ii4JO}`~U=rv5&R6Z#L9bkmwYbufk{AGf zr>?`uMv3{?k~jDlCS;ug5AO-9%7}@J=ZT!WyP5Z`jckinf%K@+SN*pArSu)AFT=}| z(LC;iKo*4lX3Z-rc67Ll9e)Yig5t~=-?Ce%fr1e=BFt`OgY)TfA~s$yA42{Oh5*te z2+KtpMOxB|LMcf^^aw=UAX>U-Z3}}wn1332$7d-2wpxr&NH=;j1py%2zc?8J7CY$5 zRO}ciu^VsTgjs{$4BjE15NKdfkO3F4sFuVhF2E?^8OMgMY30*i2rF)r%zm!`tjq(^4BC;eEvKS%*}I{j8`%39c$gl`#)B* znKnA&`7qWNoTko&&NlpiChVj-6fq2>wjD%GVY`2F1at-lp#s>`>RIH6+o#Ncd>1bq zXPQ3If&Q=%dPH#4Dwf+uRY1R(`-roWD7`x$_`|Hjk`vd=odk zOw4DD>^BV85J?|~Fjdm(NB6p6OZ6kbYjQ(Y;H?*gmp^Fzytl{6cDt51b}{|oyj8Bs zHlPinC|okZ#v6}JOp2jG%FQLesd;)SirG+65%0_5OVBb1VLVhp2aN`c>nYMQ3=E8o~EJsEx#79UBtPzcR*51UbN3IZb@ zP9Z2FqEs*R_CFd$z4>Bo0$f~h+k*;eE_#MKG6ylK2ca=sdK(E#5W_He3^;!7FC`kl|4i@;&Kw6}41c41-8>E$9fZ zZ4H3KIE7Dq<@hi_WpH#dBFSkWqcrE?};F$=SGV(sx z!C8c2`GuyEm^x@}wabppr#Xf~iT(PY0abr|0O18eZvPzy{rZord^i~z$UfjCZz$xxi~sMf zT9jyo3V*Gwz4g4%p4z#Wkt3XA1^1*_v>>&Y(DKO(a*AL>9VnQjMA2y?{9pVFw}yaP z?IUq#e5c4VfH(VF(bOndli~2J$DE3D3c0I~9B(kcR%w+wN{+vmgv;;1KvIZNCe3gGU>pyfj@I%3$?Kx_1P;TCu<|-J<{b%|* zDi9jdWOYw$dc+2y2M&f>AcG&;_9YmUm@sx{;-fxqs|0&0g4eJzgbj@xW)|8L5LX$t z8hJ3Pi^M&ji5H#eDI`OpM-KM)6}2M6{D?ASmS<-JUT2a%cp+9mZ%Q5n>0zbRQlwg1 zTD!=w)XUY7%^tR7Db*bfrk8r&E+Va{wqZXm-uvpYIghk;IU;{_I7TYaM@2@Gz6|E* zzf~LdC7w9dlD1AD{d#9_H}Yaynaz(o(MOz}o12)7P$fbEm7`_lDfmvmcEe9X+B$n@ z`^9!!{!G$;-%R2MYXA2^E=Ds@QBPwepbE}TD(MRtQ6M09q{qfXL*F&0y7F|qj`+tQoo&+Yf^A(COgR=G-)zD6ePfU`j;ZtN`(6IE`?jT&jzof_#+y46|sIlW&J`h)5ay$hk`h78`r8n%~ z@xKDI@3{cFgBMv*i;;QS#gB{(rH1WZQ4p?udt(hxgAksE=oRljKw-4pc64y)*{?0< z==t*UIS%UANVwk%(5!(B3}QldW81}88)t)o+2XGE5EmDR7kt01pIz!sSyA$OgHFFs z>daT=&%L2H;8m&PNbZ1f(*N7~lV76$ds3*$V@O0q!CKtY+~#oog)Ozl$+%A^4P ze%M-LQZy=*N_oj2a*JI4>`4N^`~}ql|8oTXev8cD0FG3--WQIJz3>fL{`U6zzYF3ndR%@FICNuTS4|J4A%e=JTGBj3Y4sTY)q zIcNF=VcKQ6FAonvt^N;&egl*Y)+M0LS&=&!rEH&c{Z;Y*qLkXmTZ%)uN(O zftr^F?XHopOh{oZRKkZjV68TSf}f5q1vCxEa^!Ac?;Pi0# z>hQ6p0-lhbJ_)fk@Sz0op3KKvD z&S7X?C@Di~rQ5{{>ToQW;r4}w3~)2}4JhWq<+3tfcnWd#~E{r%DrR!}s&TxUH)!!a1$0wHd_`w@JI|NDj) zgpe|VNo)rfsJ6CtkiN5Kp)3^*jaD*W4(Pl{7*$>C@v!G@EKQ$`dq`B_4*IeE42I){ z7uH`F?sBE?)vo{l^e&75(r!B4jw&j8?&?|2@U4a4Brsq4-Hv(A%!3{Mwa^^;6Kf~kN?S@_$8!~ZSJWZSc%m7=pbO#n`j;~0J@>&jB}yot$L zQ+yIoE~M3W>qF=+U!b_(zcJIh!uaBu8&&RS*hh}Za^|(!8x@~tgC)0r-K}P4&?W?^ zhEBrM31lo!Ei9asHg|W4u3Y&x92Op)cW_l*T^**f5|vU<@4*i)`uNfLcTf~J{S0+} za73F%5-#c>RJ`*3HsIZi<&Q&b-`q}iPl(%DR%wR$f(PrjET#h?t9VhvkLd_rXpIqb z#qS9g!D-P}rcc&0RF>t+E+8w&)nl*x{F&OveffYLp3+m0!75z9xGWG+9yW3C@CXYD zy=bR6y>wB0ao`paII^^H-$jrEVv)Rihy(vPJ*z+cBQ;;(5o#JRPS}4K^8lBUQ2E-^93LUg&dJJ&~kl%2t`JyALh!W*uPt6xD9lp^CIOJUsZ8t(w@&isPWy zJ>T2D$8@AI4L%SiFCJpl7u<nY!M}-qyhd}&LFh6XdQ_zZiH$#gAjQyz?GbPp z8z<|=v1BBi2g&hGO-;qcT)Vy1zT%!F#KbX_nIO#)ahNo?*jPn!s%uQ7?9xWBhs zSxXC(3=1wTu$w8{nahB`$&)rcEHpHJB*YTekxd1Wj^l0gHoLQ)JaN zxsI}H<0sa(!mMvU;UHK($6P`Y6B7qwVt9;y!CaFE&ZmdY@+GrSE-Eb{K4zpz0=eAm z`JQG83!TB!rIaw)^-x5DSZ;g6;~F+PbBh)gS^#;IFj$}#uw=;7>1y(R@uD~>DM{Jv zevJ?o!faA|5w`X?)Se&LCZLWz)P{(PM*H!qLRfJs`6azg#;+$rLwa=g4DuO8(~v;m6hZTq?FU^=@}_PR``%FO0Tp(QdSK&Qzb2Aif<6-SumDlGlmU zI6D=bPmkM<_BZZV`@VvtU7<0MaDMP_CfV$roSdxlyJ*V+_(RHX1e2pRbnK-4tLLk+ z!`AhkyAp@X%tJSQqic&YBeny^ALr1JR*UlgeUqKkw>$pMGKb>LHtcvFQlRkyemnX7 z4)^@a68m^AIP{(D&U*(mR%q3J0)Q{b1#e!DDv=L7wA8G#WkmccY zLL}+F1@r<)PzI&#pBN>5)cx73*>YhHbbc~#v{D2@uc2LBTtplrpMNzs@4s?p>TkVp z?elTOQDLsAUvTbgBI4Eqm)X^j^E)%4AhLq+5~!MBL}wnTO`)xV8#^~CN4pLp{6h#R zVCrFhc6MK*?<=STLOMPinaya@xj6kDk_I}4|^cRrDW?yB=20;9DawWdj3bUV-pRQ#F@`B}`muuFH* zu;5$wfF>-DjJzs@4pL?pqJBd|;$ci&)%vHNs%;CG%x!TT%k1{9{t-`pS(%nE7R0J! zA54R)@~{>h_pYyPU%Vi~|2y$z1EdAdEG=iPX(RpC3K)h)4<T?1L2pFxK zpPw%Tobu+86Diy-D^2%AMK_mb>S}5p@CJIaAGMug{7rt|$sl+D@a3$jOf7d=p-^T4 z-~pu3Ju>2+`x&mP2)uN1Y5PlE*tXpMJ>@FF z)Sx$@L?hE#ndnMgWwr~{?wkJT4RE}8kUZS-y&M3iFSxDOD3ou$d?xvePY4Zw$8-b< zh#t8U2*hI9Qs^#&=L8G}jOB=uf&^N{@Wo>wE8sv_vttA4+IPmrGxp0KKAQ1~!g20XTF39N@}-9qS`*0bLi2fdpuaPiGbf43%-2F)Y6A0Atw zbU-JM?iD{wKnAXb*~FL85y%1k&?Jwu8u9!AABLwEA<6=MmWrMEqibf=Q1*jYC(R08I0Vw(e?JKN1_n_vw-w4< zpvqF)=OZ5ydl=QyZR+kJI}nTyKW#W6bf!920tBz1&SoUFg|gl)G+^^gB9qw2r7^(> zAkktuf;req2_{MMYZHS}FHc12-0JzH1APpI;v}Yh1>>$KQp-W|9mA%wdn_Sy3GN#4 zOUN0?$4p?>f~iD&{^upLO!zcXDTNLzo>SRzPp*#qK}PQj7!Sxo z|2HxC*bkB%fxMSIz0JYFksGcAMs|46VfukLsr>i^1d8SqNCS+sUB+`hL2`HQc}*$2 zfw|98ZsLR<79)FmlD2~!ou{u^qQ7{|%WYN>Ixtcm=j&W%efVBp&_1B&s@V!zyUPVd zbWGTj7ZZTG0fZu_uV|D8`T~%7|4*;{yQ&2b*DM{Dd&@To0hA}|j?Weh#kdW1(SqD* z{mEcn+}gxts>>dKsN|UoDyk}dDwnc^UJCLw5XjW;<|rMGdGM5-^-rli`1z8R@CGO= ztAknO@AzSie|O+jR5mtBy%(36&|O+%4X3ywaDHdH*G_86q{MP0_hF1zge;<)X^lGY}LFT>J#ztmnJ)u%?|h5#O(n!wSmj z)9D{SkbyBhxJl(kGcoN`E{xO9d4aeiZBO&|D_Q*%6}U9Ir!21-@v~dzJ#o6<%Ilt% zUDYdzcLz;8Pe?gq`I=i=Kwc*d2nZe{t#3(dD`o%_gpH&%VJSF$|KSVBAEc^)PNlQ6 z)6LZtT+xt+0*9l*84Q45@TQ$g81dsK>j$onJ=t!mb(@%)n2tT6Apgnz2xi%Neh z?&#HlDx6yP4U=i=J2>qF7GW~TE#-G9pM-pmT_HFJBfEg*ne!y~~yz)cz|50`>N(?S=ww$X1ZH z@zDQkH?WARPKd3XJd zV;Qgv9^Uvd;rzS5?T)O`49lNehTKZKx4)1FQ*w7`HGHOUo{0Kq7P~X~$cQ5c2L7sC zPa)5n2+%#GLQWyKyEk9^-_e^PlJY)1tVm6z?|K~-RjBh0f_17Krs%z!y1LZIe?k5q zJm0xUUpU^PK_IWI`}U)6yBS}G2z2hIK@+udYS)FZ2o$Hk_BGP2GLB!05oSk~v@|fe zx3Cxsp$D}Q(w)%`&+YgY=Zj7&na2%sC@Cm{*?>T1P*yzQ0wU|0vf?3^FB|{wwS@Z5 z!k_LEb>}yR_r&{1nhqd7c~Z7$5h6x6K4ZuusI#8JP(Wk}Z%27=NOt_;i+?2Z!`y>X zj02fyQ~x3Zjm4shZ!;Bu6+xyjjqe?m)Gri|8+}p`OWm&F6H23L6Ya#zQxyNuN1ED33BG(enfav)Np0my>G)@WmN&fdIsM`U$fCw z`ZCe2eF|Z#eb@TtOZc;PoB|zd=j&+wr-j{lML#>GoSz`P_I%YKT{hbGh0 zaI^Ae#*}q5*=q2AmNMk-@XC`^|30&SwFfy+l_GFDkm^0wRLcEbkd^w&WtaYMT^sCA z17@ur?lp$Wx`J>Ch)0kLC5sQXgGBM6vGG^9VF917s(`U2fHSqhlWc?tbF#h|9K45V z3Ho|^(3&$GGiqtoi>H?hU7b&JzM@e1GM^;XvgtYMw&_?9lNhsj&To}Gsinj}Zysd* zp$bR#lBZhEf1+e$)M9}2-0NcGN&>elrqv1t!Y}-imtI~{c7GK-CV$BJjSdcaoP|Sg zwrycT^c_8l+u6zvHbuN0e43Ah>J+C68Y(+3j$~MBmyg^0VC1zKAP|7|*xBg~)ITO;hF=ZF5Fgzwp~^b|0fO^{7{iWt>U{MT~}-m z)^|V%|Cb|eYY~ByA5X+1GvtLgLv$#@oM@6_7|jerJ8Dw*5DlHQs5}>x(OB^G&L%MUU~W&CAT>fo%e$Gtt0K9ghFUy9auh!ty%4M&ga{S z7Pbuno>Gi#Sa$29&i!!oe}%dkOq=PqYLPT5Z66d-UlPvc54Q2-{mD^eCANakeiaPk zJ{$V@yR4GJC|zj96>R#JT z^{SxYV89QW$ZD7O!rvEb?0p!)5wD!e%CuUol^f8xtSdP_c-ocBrWLwNih zz*Q2kG!#Wi3rE-AP&QrnAX07@tB7lUDW3IoJ^GW$`BTHG=Re@Rm&exIBJ5B)Wo!A8 zXPrAo6K6L9**|V_=4OG9?=I~j;Jwe&Qj~Yx&4+2X_XDbwm$m%d&BB+pnm_7y;^*jf z=Dqxw*z8@$g?_HAX3iMe`DUflo{9a;E%=_tBjfe;bpSJ<@F*$HXFSi8($&{zgye2* zvOV0EnwlEG%jY9~mlhY{O}~fpkBn?c_c71`5a<4(xVY=B^dDYXn?=2le`fQ(f}eAh z*?!YTJ3dinyS?mWhefb4C@$JF;+&aer0jdHC%`PF##b||>;PlEQWv4If+w*e>1d6h zdy|}XE#UzUdT~2xQN`g9v)HAC?t2fmdRI>WjzI?F_+)BzrFiK*s&$$R0-^}xily@# zsBhpW>_2X`GG{N6j`(28z46MAZhqFr#Ps=uPCM~S4)r+oh<^J^_D>m8kKUz}%xTf+ zkK{SxF+#el{4xUTIFBF7{yETO+ax&+64pZjf=G6Y zZVAxPgXMngCFm|{{dU}$oEBKbrR>toC_Ce~(+C1(wuKy{b?FJwFa>v=HjJrOlfGr9 zt@$>|Ut$M>k3CBi9$a_SCLlH7yWIKVLTu_(p^&3vSFZipkAL#qUkkAxC$YSs&V8BC z_%b;{Tr@Q~Yr5Z7Gih+=;nfc>Zxhg3S6Hj|#7phX3vV)WzCN^K>bapZ9sjQ@MGzRg zY8u8v7`6x|)Mw2h1fm|PaQ?!(wzjl{oW2`NnmQ2W;=OC*=H}+_?|-iSPpG*ZOX#M> zX19S`FP4Tt>2>V5VtbJi>Bz;Q=Hk+xoeA;t)Xhw9?o@@hZ{H%d{z5WGw3HgO4OX{O zRw~`k{;Vy^H1|L7kpEYxFH)CfnEW{CNPDB6SeAy`D}{;#p?1#2K2OP^)E3m#t(>-Ha)eqnj)iO+H6>jSb7u%Jq}}S45wcO# z*3?R36^$ihZjGRc3l7HiDaWLZ<%fQ86fPcB=mQ@EsvEmy{Y*6iPLqZRN9jXT(&}QJ)d#!xu6d7}6`R9QOU1bIZKn z_Ddmj*~{r%6)SBO6@pzss4w3xs!5MH5y^GY2P^`er4z%#Y!mVi%W6F?*OGc^;2hzT zc~RkQe_3A}7Yxp(pK`M>>jtI1+9HBg>F}e0?Bnmar563dDGrMtr?wV`SIu=)zP&1Z z^tARySLNB|TX9uiDzh$=o6XSX}fK-9;lQ0Q4h=2jQ2b^lHd}Q?W<6y#r zx$^|h8Fx51{+>L)HEBDoLw#bEvz0Yl^eO2TR`0-h98>a*PNQY+dSBADw95#VpZuh# zSLu(VX`Mbv#b19bH^sAAz_3(Hpl=?Cj3cVW|0APa-3#sDJZvAeCzEAm^%)t9xa=s# z_l-Cx|E)u8+3mE+#SsG(ovxdZLkpe_JpeNVO>l>RzMChlV0hFf1pP{X=_sy%?`A>* z?HcB&P{S%Asr81N=WU*b_C8A#fhM6I6A7g+)CGxtg%Hwzm2kpGi8{AnW2TJ>k#DoHjCXhupF#)gAZ3 z)?3bB{1o?|dl?Ts7o*cP&fRV~t<23y(i5q^>zPQW!4MU16zzX$O$aR(wSG&`&%KA6an!WNOEPG2`{&gbJMlM~(M@lE z9BiVlkf9(!G!)5-_;d)r{|_&EX)Uo=@|JDK4RHjbx8|HC)iT`z`&RAF-&*u8f{?vb z3v`l#Ue+*qiE0;Jvy&jf$`!Mex31ZAuHVf`1Vzyu0_Eb8tTw0Ad0)f1Y(mb|2|8xQ z;oNOh*3@XB@hX14!a(~xVosfdOAzm|*VXpWjSTgBIXr~lUfBF-)5YouW3o4eIU&%o zeB}H?v;ybaoZNJizclkc)TW!$tZIp~leS>L1-329>3u|GBy(FM*}9Mft2upyWBZUD z?M(C%QE54Pc+|8Hk+-xL*Dq^|Kb8T(D&xP(Y@z?_li75X|97;r6li&PGsia(;`c}q zJ8wdE+~=zs*8{1TZEqtMe)_*euv}+ANk5*w|3Zb3C;N)G0@Kx+W=V6%rMT<(V4Eh& zf>w(_d0}{^Sv_kwIquKeLlv2MyR*pwtlgWgvW8pbE82Gv!QPL!3)u(1)@yfr59Ga} zsj9G15zu++%%IHK#nwo-wNaSiNZ1*rR8uls-cO=V<06dnco%*r$PXkW_|{ngunp-w z>dTQs{sMv#LO7FCQf`HLKJ0S9G|oQR zvk-Sx<&1cy%H@Ea(pD>-{&;BIoNb=c%vs?tZLD;vftDXi^|5DxrmmhIJp%(LJG-Zs z_SaCkgGMlJ9e&{E;vy}Rg4oK|Qo~Pp{OXf+ZfY zye&A`bw8>sNQmV43VPQjGe7^}3?aM)r71t+Peu1w?f*e%VBmC;ogO+}AaY=9u}aB; z(IeWRe0_WZEU~e%anNq6Pg9~hSH1+?}b zTUMJ4TB$nW`Ie+a3LM`1mNajE)z|HCMecoMV)ONT1t?1aOh-kl^TaZ63wN7!++cv6 zuT(Zuk)cy3j!!*JSOcb7 zZ~=yVKx=UWqTZV`6sr(&EL+{Q!Q0&dDJ*a$h@@KnIC4g`aA7M6%vKB?@L7h%F%GnE z`?mqTpHuMS#S79oxTHwGf*-M}cL1tYT#>CrbvA544zXhFu7n|dzF3B}iknIX+pX`Sqa&`PKwbyQd+$y>p)4ZZT~9(P~o-j8{Jl6gmYAw|3<%!G6d+nBJf zs&M7K!wC{vXd4?}hTr22TK9#)O`Cz@_wF&9ViNI?Kr)CkkHDOC2seiyWaR22fOR1? z2on-qGl-1Cu_00~+wP{F`%JB^ORDU!acBp3+ZEJHHNxYJ3ylef@y=q;aRVl=%F!&D z?TjzCsw#8?ro^wQeAQ{?9yb`Lfl;Sd=~st10Ch}<5HY+8AW`L7$+HUJnG?R^e}Lun z)%qWcS7*g1$8`nKZ{>06|Bt8dj;Hc}|4$^MjO>wh?9H(vdmJlC$;!?uduJE2#~~w? zjEF>5w#w{e?@dNg_RRLXPVdj}`&WPVaG(2ry{_vyH)I=3Q7LI}bY6=*hywH%v}N{I zQfT&d8VEevhiz*2l!imGlrj@7Goel?s z52Y#j+h?>0a%02R9FiMesZ&1x7nl6_uZ- z0e{>H?sjBprhbQz-y6}ck7S{Xu3`jsKC)73K~p&D?|(GMnY%s&3MD9)6|w20Q7&CC zQJ+N0b`-&ba&x^KNLxulm@rAWobff0Aw~VZZ%Wug;pJBshi4ehTGv{|#tlM$c!DZK z>4*;;I&k2uAtA7Udc*Qf^|sgge*UGD`Mewka|6^(o^RGHz8hhbU5|tjng+d`AYQ^b zTma6nuT2EqIY+Z6R@$4EmT78h!7;f#`*gg*{_3%M&Daf_sr`(e9ld|gWAt#+jmpx3 zN?xq7RBhUg&$9BkIY^GLW?QiH_Whi-{Osn!=&M+ki0S2WHpp3+S?_^9E3$PlqwS)Ax=>m8}vG+ZI7g!J`FpOp_6 z)Z*W*Te>6W8JfQ-BD;lGq7?_yOp}$2Om>i$PmcZ6E!@~9g4niWw+&6*8SJO0)7bAU zH-D4tqOW;*A@Z+GCN8`f@FMlLHNC5yTu_46-pZ2a99XKk(7O5}PV$rdt0PZo^&&h~-ykxh^H8@Tbx3r$$U_SV>7) z8P>NB_i`n>EZobgGus`cjkLds-!e#|$-IYX)u#^z3DJAaJpwG2hIojKw6r$?DPAk1 z3SQG>r7gDhtx=Ru7<5U^rUNCo?;Ny*M&A3xT&$vYCu#Dc=hw_c>x$*`RUi1?;Bf0G zWXd=Ua(R00=Ia!gbtPgmAAQ84tqdGz?LyazQ=@--%kVL}^pI!--M_2YxB6F#hc~E_31_Ma;W=&EV_1$6IeD9E(1Z>*ck~iQh8O zE%ft!3i!%P=eDMai&?q?%13=|m$E_B2ap(67KjKv2M|SAT>OIi= zJQ1e?mDj~mko`9MsLj*^U=6^}9WNac;XahVD4+$t&vv`^uc)}GtI)F0&aPYn(T^46 zB0Ik!@({fEBvExZ1{nPAmxKsx#l83^hYa&=8#@n8P0g0~kv;ujDH+rA2JDqYuaLof zM_``OJkY~1hB^IhsDzqGlP3)Y`!!(P)jNYHgSmK2t3Tlo>L zn+0jleM>IWoc=3YMX{KnYh)-s?Oq-=qzn(LtWNEXZg*4{0A6q#;$Nm4gt1|?2*8dDim>4~&AjKwgvtOd?PA)QJB@_);u&$%P-ap7Qup5vk|jMV6j zD~n2<7%QZ;ILS)F3wT?akSW5R)OyphU4Dyp@w+NN2h}<(->a{A*;rhUT9EBTu*pQd z|DJZpsFK8&{ALIr0w01-{gwID$`%-#9imcH(;Q+q_m!rMvPN}U_xee9fFRt8ZG+`PO;hnC+= z8vSeF9!*NR>d3bQ7een2jETxEk9~Xqa0dMQqt9c4n{c1^Vxgck6?#qVNpc8~gm_9q zk%#5+s`4LpS-X~@zx;g&CZg@&QMpX>BB4;q1W4quMq;pmzDfQI-^jk$z>FPXaMSYp z1WOhVSF_|757<)Jq)IBx&*5V9Xi%o^XL!9!8&Tu&x-MY&%aXv8LK`wPyU1$3d}qg3 zQpC*0efCqZaPf0jPjKL_TsM)dF)_3c&_TjpUf!>ATeAipuC59Cas#8xj!g!(cOy^a zg*X(8R7M{tuY~;ACan|vc7crd3SkC!Y4v6KdrAUylc+bc4Be7R!1|n(!Wiw)m6quDa z8R8rU}t$HAt;nu03&d%FPd9B$h=_W+iTXWIkOZnf8Y*7qstSggSu2OUmiWM+*QS+LKe_e0MvXElSf_ZQlT z6F{erToHqpExq@x^b=YTIHGT#l+Dei4#O$kOMSW8U9CDjc^}GCH=9Z1z?ROz5&oD; z&%{K=rZWxllVp4qgSO|IpTWMT9o*LbZ||v$`}H8-|FAPagXim_yWB0v`$a zN~^gYA0fvL6wAUd&oq;ZwI=$rr;IZ%&*u;EGXSm}YF%MPk7||ue!wTBuW)O{N-Jt;3=rgfC&lNp%fU!f(3vLskDfP*5AnC%J&n)cpirM>t0kc zaYeZ6+e@Mi^{eR0Xw*64EF?48WsyzZL4asZl1a<<^a%3Od02#{ObuS|U~c*{0y;Gy zSlKy)0$-V7Czx5Iv-K~9r!6vg^ZfqEK}O4}BWYGk`d_z4)j~wxqN79_I_UcV+;GA@ zdQ#{wx_8l{I~1kb>*~2SN&EO?F2aHS8Au>ll){mY<9AwIzm^naXInyFB=~Wsrw3oF z+&wvipUONs05$~06`1Mm?WsdmRcDBq4TqvI61WyZyPH=r)mJ?>rb7NUP0ZTLhib~l zAg*RYj54eec%iVJ+gUB&aPHyhUjAK{`}`#49fJcp(wsH2I^sJem4eCX;pj&9lS{k` zSXyv1%T?!x;r>E=)-Yn(KM;01;7PiiMj{eQJbs%S(16o@J)IKM36-184w zGK@}A^(=NA);D}0)ufynQTp-QECl(>I?CHBT%cx2?-Pz0wk^0xhY9mAd91g%9rBLw z-lum)ef(XZya_s5t%5R7Ojp;*cyzAA9l)D`R(SI`L+;njzX0AcdjZ*}JQpB*__`=; zI$QczgsvFZiCFfFYtKhT-+n&-a=F#HnPfALkbAI12zG2uHNSB zF;#VAFd@Yy;0rLT`^~2KUv4)TB=)wqb7DVySNVrBy?ph`-bw)^a_19(>S&b~ss=^^JS11$56zKLkl|Qz%rzk`53?DgeU~yDxsh z`%Rmg=pG`T*RIdqoHKl;H&T9XtiJkEX^>2X0`mLr{<#Ysu4+oc>>Cm0JA&LFQ!7)pKXC}hiXzb!hru7!?{D9B^5HwxoS2g}bJuO$BWg)~D+7A%X++ z`=HKYWu|pipv*7nqe)G@WRG7VJs+3RVwd`BHP(>a?u55AK8vI!fbH8m2UyrFxmSQj z6Ze(1Y%79;pNIC3R-gbiNMYZK*UENKZ}Zx;^HZ(sqMgthi2Leb5PM;-5$X0f2YTo7T;WC z5;rX#8vzUkUOBA-s_g@s4C2?FR1=P`FRoVY4ftgmzqsGD!*~%5y>g%@H{0Ie(IG?k z$!Y=(cM(k{t9b|QkNpy^x6P9*9g{uG2(>mOYKZRpOIpGacNs3QPq|_J*q)Q;&v$os zJHEHb+70UiDbnN>tifux6XmCvVe=*PtTW*Le@I{5Zp{t57KekAg~g>~Qa2LMhbM_n z%cNl+$>9IFIf=iAZ#jt%TT_-G5rH&)ZQx05cJ>^AM`Rhf;Bkj6i=*H9nOoc2_Er`i z`;A5s7IOifrOrN{kj3oC*Le%uwZgmCWk#My#wD_Q1CRS6;#!1@qDVJoNoum7&j(lnArY8j{-de)hdGeTNx&_e7Q%9Z(?nS5CzQ zq+EY~c12tbvYLFZn#S?XLY+##S4`pJg(-?!0S8mlYsCRT`hy^LXK&Sb((eEzPlsvL zy4XG@_Ds=!THrYzVtDxTwz%>^3*O|l9l*Y{i#X5bHZNMRbh1F4Pm;?xLb8|h8_8i9zUZWAQ34e+um1kzvQwGvdDhJxlps zJhoshX%ZG;f%~=049z|oTyz+U5-$sm{~l4}a8Lq6v4a?4)kVhSQ=?|ppwNl&CbvJ* zB7$Xe6)SmHT*Nwf->ymYqOJ3kC-KGQWFtp!Hqb4Wm`gbg$9^W8U}B-|dm*kDe&6W+ zEsl*=_Ww8#m}-OeVGSyz?VBKAxvrH8sIv*{))P1Q-Ar+Y*VlA2VQ_w?(gC z@A~cb*i-{&7xpnr7Ag29!+1#E7((7}Rxoh#5HO|`PVErmF!t6Z< zt#^o~zni!BGg)7#&y4t)3^Bl95R0c1yQC&EE*uk&mzBs%WmYGYgmg~TI8Uy(e3XCp zLrVPD;uMK;vE$!!8Z8dK)?St!NvZ*9B(ETtGo2iPva1RKDqHB?*WU1IKGGE0fyr(c zz{}Pb^XsK(F0jrJ zF9BH@d3k$FWWUxj?9{`7ch{?Q4rl)^@ic)LgAP>>JQbiqT?=KEs`+t$all<#sw-sW zm3T@#0S4%LIA|5iLIYC%BwLHnq8{5$?bDP0c+Yh)tt(z9TV3{`pWbpr_RAeygqLmj zh*!i!0ZkTc|4NzI0w!6Xp|jt*L=J9>lkT%{JBrim0f!Y( z(TdI13aSI@nunJcRA<1M{sBuYmM?+1;yH(sKt~u!jHN3K8kTbDaWrd(!rQtAIK}HF5hWeetF9((ph@?jmqc}|p>Uv*e z`DOC@-DVN17h|l{Hjyeyg4BBOwlp2>?8SZd@_bFw%AoS682CQ z)#Qs+e2)!qc=pL_)u{9P&ialacwdlci9Rw4BY<$EG>YqgG_uUrFkQl2Q z5*lCDVpM>A={1E$7)VE2LCpK7C`Tl!T1xR|&k?R2Ks2cK3*-T07;4r3ZQ4nSe_s6W znS9!<`N`MP^hnF}5^*%pNudV(Iy~Q27Lbd6oJrPMB(JuT0SJeSS@cWL%fs7UkIC1< z1;?7ch+Vn~BAehIM~EVhuxOf?Fji2ofR;3PE}%_$dh835R|4-Ba1ML@87u$+4+b-+ z&PXa^*UM~D>P*%{!5e!~305($i$4NHT@Xoif2s$(P5z=4g3>8$iW++O!(K9p1dSWK zB*0+5nXLpCC4|=22x7RN@p0tL+O-h-AL*!0e&_83=tMKaNmFdg<-+)91|hV{7IX#; zjE$9&97jLmOmS*X3$Hx1%u1xNA#d;q4{XfL%zT;MBq8S@-SESc|35xp^iWqy{lkjw zkCbu){SG~%a*vmb3X(FR;`&^XVtSU`)bwfB3M}+~K8utrSN0c0d=7I3xMzO}@Yj$C z=uJ$93^!5RD|6TH!r}z?5O{O|$pv2^=r5N6Yc!(UWr_L^pt67XkS!`8NZ0c1rrkT~ z-0Ks^IxLpZRDb(QsP*K1PiESwhJ_TW9dEILPZ@h6ysyPmR6-Yk2gy;tCTxj^)f_-D zVKlkf^p)QOe~wb*bDG~TN4U8CnC;=bvP3=&Gl&VbdCb$VYc@|p3n^T$1BF%r<ds>F@wuJ31JlH@G3;?sS;*%FOYwaopdF?AVhd)Zt{bK3V)qIjCPE47xRVz8z0v z@J&`2dE{dqZRq=+!}mHaM$+~kbIsM1g})ENo(dM(IoZ+xWs;P9KNR++e)u|r7a+Zg z`Clh&l8G_EZy{fHhhAS3=ekI<>{9ZWCB0ezN`*f?C&Oj0>+I^vn)OR*tpqqf;^2OF0y)Yo#gsO0K$=KpY7rZ`f>daZG#{>K>l|v;`O9o7F1}lEMFbdJN67SD&=K*%|pW zd}t+eDnDAk;oGP(`t!w*0le?bcxZ_}7eIIM1Wzp_WIP0OEiPrc3i@WwQ$2#Squ}<&EJ|hx!T$Ei3<(FRMncZEAzVp z6$Il37Ah#yQp*c%e(fWWNxliW~3&Poz; za;#qYN+YvhzGS$^G1}ej1m(NgOdX?#CUMU;RIO@OA#DWD?5)kG-SVLO#!qbj-RIc~ zAQH2iiCIU?W$5O=;hm^5_spM4pz$4ibTGjIXhu03N(!;n^}g==e^`v< zKo5YthOs#q`-gM#&{?G5G5NuT zd#cA@xR?}OoY(lwA&3q(tY`A>?(VMz(S;U;Uei~&ubWT8)s5A0j+8*93+J8Zkm@gu< zD;YIZ*mrH&wUKry8h=uYN5mM#CP}664F8pkKe~68tqFRwAqQQUQmVxC?yXzk>ZO8j zt1%KXp1{+sLUD8x8Z@BsRI}9C!rtTkUKdsco%jcm>!0g6yOf)xvmmNu>_eGrD#{); zJ?DoV{{*dQZ`H~P`;~2E0%RO$F<@G32^B5a2cKDO5q@EPf|kBi%`_jsW{ zf9CI|so0_dXHAu25d0&y+Zf#D2`N=JMvQPUyu%hf=3dB2u;}d-pe?hY?jc)#s&T&Cb?^_ zFR{}pvb9kvw{P)oTK$-Hz-J}N8RERt7ilGF4= zJM?|x;UCZhfLyMq(3D>H_HE0v`)e$4c=zKn{*xVSmElb4>nz^=-IQ236DTBxNWGIG)b}L_1S|3192D4>6vDl zgpA7voY`n*kJea4Y%!=vw{Q&hY~#2Rn=nn>P@k8xYSPk%a4lv^Onhf4rMEo(317nG zs~~}y0eB+>OU4&V-Y&16QIaQCfsdg9gg$r8{#KQT6+(i0_+@&I!?Fm|ijPIsgOqGy z%Z8&YwE72e9vtS8iuEi4z1s9POSz#nZ3{q+}6 zrVyUk4W>Z1oW)gQ%yllbA&Yn<4#w_1k`4`;X~}Sf^YIZX(&1t#HkpIIdokv$K$*zR#kGHQ6kVNoM_aFQYY*$13=4anoD5Xz@iV4Cb2F(f z2tn=3R)MMkK)t{o4vn`JOC9J>vJcBi(?bmTZN=m8Kl>@t8S8SV`7O&6m2+#mbH~W) zSx(`*X+(aGL^|PWw@c(GD3E*tSp{6iY42E@u-&=*Zl}k-%0&$gH(KauX<5i$fB7O0 z3R=(-hhA0m_m=^s+>O|c%VF!kq4qZBD;HW6+^16XUZuvtY`#xMs3<$qXK7ocmza4) z2_!je3s(wV$voaCghGzyS zvN~Tt#O1A-*jxt(D4Gc=HC9mx+q9QJB140qQv#d!m_@w`1xSEnj9nduZab~3Sobt+ zE0@=B*c?x)bqW%gSRYY+zd%{;|M!P}N7kZ=$I(YPMb-yz^&9NpUMKv_^}A4OCmpSQ zeEFp_e(Z$2y-NC?n1#gdj{pXHzTE3|%_}~8QAO|G^$rYR%iTU0w@{LYghBedu5f#ObN(|9?J6*vg@|+XCc4%WmW_43a z4~^kFN4K{a`#~EcB{fy=n}m$oH7xu1QGRl8eEdbYwYGNG;GHLLzo~Xk{8qpAqm0~Q zBJJe4^|t{OTAzPJEtGdgPU+F4_s1|I=KP`r(H&Mz}wo(u3TKc)C7 z&>G^=S0T-Dh^uSJj>nV+@@))6J~*tI17h1{2z4oZCSN2hO?Mp*x+)0z4QTFO#?|@q z?$0J`8Zw1HbahK60-1t(xuPX6V?bAhQ$6KTiM@DlV(@LKEodSz!Tbh_kFeq z&3z9*hYhwzkOit+%~~~EQ;oNFcgG>AtSv3imqI0kB0@%1Ru%v(~jO;#dE z?1DT+$0c$~%2^U|aoK*o^z*xfe%_hSf^RZgu;>ShRGQoBRtmp#8u}*UXR&T?d^GO) z^Vs7Wf_EZ}GhVzXGe%7G94%+wSY!I_X`!oig}TTX;(+zuZ(ro96_ zQ;$Z4AnK-CvT>0p==9)Ui363eN#D16*CdIIl_MU8O<}tF)3ZSHE#1?zi-8BXw7(qF_Zzc$PrGsy%WmxGKQtV@7nqcm7&Ty}qU0LTk3j_>;PjedjK7^|u4@CJj0 zx!<4{BH7*;lKh*%qXOn9?ZJ8qD9rja<--(ZB{oDUvXK_bAg7azKwU6cu^nxfc<|UVe1oa`d#fcNKQ7d;5)n-V^a)&9_K2!9%NpGE2DKY!uVREYD zhU#rGUiI2t3%WYirQD)|Pe;*yxuq-b98!a6@BR%n3mtWjFJZM*Zw;Eh`8z|H`sncK ztILKh7UJn6g8=y9Ut!qql9;LYQSkpl+aCzU8F2eS;|j(c{)J8o5GhPePNJ+7KtvUi z0#P_zta|`lxtLW;cM~ccpGD@8*^EWpdA8ZLH2ZO1TY!X_P8^F*2H?&G z%Nd{20|;3f!hV21^;qyNcoEK2^IcNtub$D?~QbE~(G~YRqD^-gw;>qdh zVI6UHfylQxF@nVK<3S5-`FI2~ZFY z2NaR+FF)26(O6m)seqR$vr!xa)lK~jeg)YXBcekX3H%JV{s=zI>}-n^eZnJi^WZ&0 ztkfge6^MQ1qO>_FXPs~A&bFe9?Hv!vj`~Ba0l(IOQYI!GG+9Zm6!icn7{l1vFZ?Ei zilvi}ABB|PBqow2`2YM)IT_GO_6*s`-}z_lBTaCD{UQie;BQAi447@KJlgl}^=oEi z-dnNiH1`l`d$n~URrc@P4g-(!};LN+=00XCIhHarQYCy zipd*~Js?diI|oA|O@s$01e_DHvMm#kzXNQG;t|a3=(Z=1935G(2RymP)IqNBdqm0S zj;L<0Z8Ejb_73lGFde1sriG1Ye^z|a5^vvY2&?SMUu%X}GN6f7)Q0$lbsIKkK`*7R zr6m-pueF(8CD#)$(C4*AQvALg6R%PZzK|f%QwTO{nn^=jt4I7!0*B4OfUt^DzE*lR z#M3O1Z8|ZfSVN$D3p|W_$g;3>hlbQd5V~(N6IRL+Uz9Gq*U^<}*%b)4vBBVlA@7??TPso5odadYFKde+?rmHaA(jad+M(j7L(>8q2;Q&?c%RM& z&2(Sgu74ns;H)%!xyGC0z~VL?I^7{y5)E$cz(sg&n8@EremhGzatUqFx_7+QAqc8a z7|=OlA22qP%4*I~fJmuz6YFl-w|TCBncp`St7Kf`TzP8c3KTV%fL1wgL}-=L6$@!%ww-6m;%+ zPznFm&AJBZ`DiUMMN(N!%}BBt0BCK_#4~I!5Ne)^`ybfB}*ipt)>q z$TJB*w}j2#12LNG8WP*-3p25cP@A0xFoxi)6A(yiodDGAJOUf>-V#Mhf(#VkrCV!M zn8Mcs0r%?wE2ad0=^If!&6GM`Z%pH0tP%R z$;jagsET}9xoGI$e@nYvq)A!eG;Eou30aVm*hHLp&XC(x#QkND z{izLOEJVVrPW+u$n?si*BXVHsi$=Ezp0{{K05jz-rA^cQ8JI<(6dXw2lWn!Mz_`YK zmk>G7yu57nT^1!OKVlUkDi@+s;@h!_(A|Zh$xv?v~V_;E?0LoH+OM zHG7^ev*DbDcrxT-!VL;-D-UdKliGmXxlAMU11d~?5749L`MJ5uutBZfw6e3~g62SX zMM;k(QWv+CQDNmr0cd1w*aP+$XH_kZHVLbc896u#IMdn3VxCy1ePKm~w-EP>ra8$M zhF6Ov8erMtWJC$7~driu7xA>W- zIX+MKKMl97>%?KF8`TafBC)UZaixFrjZTM}#ST&_J;$fPR^n8dp^v-~O8S)o% zOWy!Rc%BD@wF)i6+>!m?_94A`FtQUicfL)zOJ;XVC^`)~6JjO;+UsMCgVtr9EJ!3nAJa$;m`!`LrCdG;6dk#F7$L$F1S z<6<bo21%jTMf^7Q_4p#YF{sg>UpCu3;AM()efWoLo`28z_)O zo8&3$2~h3mB#Vxd3{;LwJUzQd1KpA!h=V4&jvh|&FrTy2fD+S&TJQBZuY6rSz351m z=|Ur2U18YJAYpy~ko@y5l>poNC{6{KjuP93xiNk4e7KCezZ}>IA=ose?gV&xwZ6KW z?@Q}_cHe|InpUsgd(gvgUV#{sEDMFp&_W!HeI5cJ6T{H|DBGg1J2;cQHu~m`SY@#z zE*iRboxkL9Yuw%)^2Di%-*MZOk9@zX$(E-B(`sg4fJj1&OMaFs@q0ZNGK3Pa6ztqe;f43O&I)cvuOyvc)`~Xq&IXj z3&4wHluOzN*i`znJ5)|q9afB-{~Sb`->}vgP-HFZ7~Uf=J#99^scwyZWQ*6#!VKnJ zVum8U9o_+2pi)I5F~=M8?j0A@SVR96P@RcwjgNN=0fvSurT|rZ*OO$BColyam%@h^ zVL1cCt*C7`gr3*BKO42zvr8(ocnWO8Q``^s@d)fqHcIp85^|iA^vF>^fZf-drtME? z_<|Z!=6j>xUlNS&+%%C-;RUrCh`Hm~P~9CJR@k%=9E^fO#c3!xpw*khAJZ8#wd>;v zP{X@o`p8VW{TDuj_w4GgT848R_*bfJIwh-Z?hZba-)fqOJuo@|g+MQ-puxv;WF2TN zxW`=b3)t~u)tK%-(U<0*&g1A`Zl-XH`g`R;2koQF(SWxD-DeLC*V*1!RrfEx!(rQd z9b_kwXUl3!Lt7Wu!`4eU{hSvMPCQ@D$zV^Uj}diON+)z1X(d42!6CLjj1zfi6On@@ zee2V$he(XOE~4G0;a2%;v3@JI z>_e$44Rv*Uihr|@6ar5#C-!;h)?Vn#op!?(&fXBbhK!?-1!H>}bd`S6qcGMnk?xC@ zRoOC8vja`G;X2bx0d7EH*T(T^iuA-;h^c7g`0gg&fL6AM;+|_BA1Z0iUv&h_1sR)a zgij@CmNUw^j5FKlSFg1?_$sSfKGb|=R&sjR#8iWhm(VqXTetS|sBB%>9@xt$JlFuB zK&~~i=_iQF|2lVoYfYA0+S{xoN8(G=^a@{Xnwl=bOnt_H8 z7=-Ru;bc`vkw)4ro1_3!va{<=O=UwRY1)}4N|P^GeDw&z7lKFn?9lVFZyiQEul@QY zd#1@^UxdIj#5&scXt7AC5Xg=7!)cNBe7upbHQCWWcrhb-kwJ_W7GpxvZ~7Jpf3+0f zf0AIGLn?}g386qphzTRM*J;|KB2@Ep&-JtU&6#E1STeHxe7Mw?1LkCGx^kl!FZL2w z8940xwm%Wu5OIpw38*joNxb?H*605%pk~56d=D%Bj#Rcd<|)sM|3J-Vd|i=#5g4#Y ztuX(&$sD3RfOgN#$hr{l5Vz5duws>BI4^RgASIaOvQvZ=x@2!oIWjvTs)Bb{RA7#ABO#0 zUq5%M+Fkp@q;D~~!ztJ2OPo}TQrZJF?p7T*Rv(4;JZPDC&CYp6l)cUI++v1ndQ%E4 zyI;C;5g)KhIWDfw*Ej0wU+xce!i)#hueATZi?VS&Fj0?7)NbpOmmBn~dv;1yKaKaCHQ!_DN+2g_=zoqlY9q$$iGF?3Khf&!K#B%Nb&I zA{2xS=Ck}052Ar-2xF@$v|m`2?<;gb3$kBjk|{W>ZN1>5+9LAovp(J{YFihsw1+~`5BA~ip`4+tP?1v5z4=%LPl__V&{O&~f-vcS$ zWeW7cKZzW!bc{F+9h1C%snY0mTGI6Clj+`$hc5;dxSdLKYWc5N`eQD@8)gv_k5Ov&QJDCxPAJ>8&li zA;(Z(PAxk0C+t8>}YH}RRGBD|-!3V=DGLj!sj#evm(02j--Yv?dp&qy#y zVJXl(Zu2U8^9&E&r*Gevj=}!*i7UBH<0u5zC_PEkn-?u5#U3JuN3#SE>|bsSuLW|$zzG zyIX{_hxf){yltK3vy4iNk3fO;lPKA-W4PggM1>+DC@runZ*0` z_IEF%$(p}{91ir(D#&e01*`EzZ08n!ZE;K2AYFan-QD_gY09J*u0vsM*AV@`HBkpL zMY7*Foc_JpflU>8|JMx}Tz}$;v-$3lJKjEBlRu8l<=j%`xeXJZ#I%901Gn;@rjIZ^ z6+C&Zr*X`Z#mHj+%v3-u)2D=wjfSHwj`5+T<&%iY^75y)#>)*^X_uh0>&_j>Mpxkz zXl-wQXlHl4GoS;>690i%^!HZk-o3Mfh!Rj1TBdx+hu9G=ThzUqZPiJEs08=h%x|6B zfcf*~kSdp~VoF&+M+w5{8&#uId2?)HLaFZ2WHp&KuuX*jVu|9cZ;RZ zC>M2XGczCjmA3WaIPhJ|i2LO3iWuksdFc!MEv47%^b`~xAX5rlyUTOBBc) z8#ciYXl19n0TO8%;LEglZvJ%c+FR#v>=}OJEineyMBnE5LSmau>H>+|{R;@rMe-^F z|KA*b&dv{n=fC>R<-8DUO{j5D0csbGsAwRpqz!(_M9nZdpU}q;8*Y=BSr|4 z$UtW^dPE-&Be*^7*@o_n43*`as8U)sFfc%Vzi2M|RJp{ADYo_J+g|Ut0UX5|Cd;^* z5IN$LkZa6jQ_}E?xc0sYh~I40?kWfF*A&^MO+1Eg#x*m-n}fLppz3*Xd3OWm{2kHG z@Nu(V%}y3miDLHISv58`9}O?BH1a(T4i3_~ha&==!qdwm;O+wjIGLJBy5)>jTZ{rj zzJ`tt=#g6}kEqO!2fq0*3MybvuBQG3;_bt%)R7-3fvd?q8)I*+WmmBcP^D*7N?X8O z2)FwL`1c}2{|j1C_20~_$_!W`R6lyX5sm#~T2Iw_!PQJ3Z2jxGhbr0TFZL_!GHrGC zSCI)HK7d`O*I2I1IFEwMjwhks8bgkU zv5ZopTju*81PPe*3-8{{j;QcD=UY_=tK0|gj7&@qmxn$Z8UInXK_s?ix7uPS&U5ok z-d8jqOY#Nk$-hK4`RqiuGYN^e+PF&5KdJBq$2HtBPEKjB-eIYTMSsXjP+kET?nmQ$ z{SS({Yv6;m8!CMM>63W(t2}_=-~^qE(4lkAS5v^3KEj`tz-{~;;l3s>vLI!LR(t%Y zC6SnxaltcTYRZRwP1r)X6>yX#zk{s@%)UT;NTT5mgQyHP*=?_keforoGv678#0pVt z^orbx5oBqIter~@#iLLtAyCG-7vZuLn-vMCP)tOa?l}<0##LvRHa2Nt181%wvW9z( zZtwqxV*>Z7W+n_s6#XmpwTyeZc>#`fFpKv7-OMdT&hhJk1>wgS>m|PfD)5RZ&p`K#b6#>zmF>JJLOYm|pbaD9Qhojq~ za$x&Y5`A^u^Ag!Iln`^IV_%HPH$ENK4CE&dv?_*Yq~oFa86)cN#e%x|<{dr(B51v$ zp7+nWROJiyR>NT-8bCy)wy-~RHI#**)aRO`0ff$90l1tOygyV-fA6W- z{y7dQX6AF+Wp~TDq<*vh#I4AS7ZJiv0kgYd?OJ>Y~XYE%-mQmHK&VOidZ# z7l<(vrMldkh;Bh04M_X06I)TQyl@obQAHFvYAK< zK%)F?(M%i{&@2;bNyO#EAUA7U_wN3dW~W|Dj;9GXWI-v4isjnY9vef1qL<5 zDiV>u@Hw9|{bHSHMKB&T7%U|@Pq7w@cU%vLR#R4|t~+#12LQ0L$3{y1*0RVu6fc-7 z4%%8!-*;}0XR{9SI0@gILBQJP!&MB^)9K}KoXn?D~9 z6r`a&@>=MAuub9{z(?|$eUB19TbC4s^aFi;ApN;?iM2cKO%7IOPn6;sZd1@Id_054bbcg6LS0lt#~uy6*7gTCVoH!i4k&=#S#y1 zf195B0OQiBmB??)Imdp3Py_6;9;@OOfcBVPdqv0NEbiO4+DvefCVT-t*E!U1eKjdc zRWh*r2t|h2=37dF=sZBA{xS00jLqGda`9fo?{$#KD7`eofD;JZ_;Qx~YQ{VAOxHCp z^M5$%87o)DpA!kE%KGfvLW_AWm6L)u&`2s~iGRwEfL(S4ejg|HKCMW8>Sz}%+T3wy zGkG(fHCAQ0BUSMKxQI#V0u}SO>g^8!K9xL z!)nPV54(>-R}T*rN~{(*Jgyc?->)CtD8WKDRn-nBA__HeC8g+tiZ>FL&pnE`|GzTG zI`f`IZfQbA$))S;4+{gN4g~D^p8W?Zr?in=ZTosqbw&I;FEPrDQ~O13QPDBIMS+9a9O_@0{cmL1pG2#v-Y5ijtJ#MAad`%*M~~+*_Ht%H)LCY#?Z&~ z8=a0fnk1gG(BAtV!cr6aTp-SRr$h4F_0vEf9JCiON4cfRmg-*K?SsWPfWf$oaWP}S zoTV&40Ov?w)Gs~TgEa-oFG7g3l}}gHL~6MRW0w_MM?%m&VHB#poDc2o6*3v2j{OPU zbt(`Upm+CfYz%@!gPD}iw4P@5W^;WBwlRuz7?Qhh|AiVGa|ZskN%*Kq#EN5mm*Kqj@_I-J?3N7Ryh!7VSC&jk$#XQ|vod5tH z)G&EFI|+EPm>Os4vF@DBuKhMA+3XGBgd$Hz`tC7Glz1-l!L4LSOli*+rW!UWAXB&T zhnV(}WPmSHl_o4D0inMTW&P*RADv8c`e6C>i-bbCUlWLv+M1O1A%rOFfSHi<|6)>j#19Xf zdQ}ZqGTiC(wr0FbjeC2>ASdN(!lXj|P@z#a!nbRx+zzeNR;~2cjk%wYtm6ZhdUy-H zc3Dl0)C+5UfY3uOe=9)c(4dbj)vw*wc4Ixj3EHn1C|<`yuP6FZ5LHFoOIk^$kf-aY zfuErF{#Ce}gjY&FpDId+m$GJ;YTdf>VfgzK^MmN6!;&qzbFsK44on{W{QR$=2oz)< zIoW@vNdM6ec1D?uw65=e8Z#MW>G(0mc!ZDY38&rYM3Yvf-=0oYObgBl5beEq8Sa}S zyt%-;ivJb6+JT4qD)~3YsPEq)&>{yD(}n)^5ss3E!S@QLrXzNnWc;bfR=*9FF7&L{vDpK)Z6H~I$x53{+%iWT+d zuC_N;3-uG>;U^}ixFh|fgoy*xx-KYgKZF|LW{Z|eE7|Ss9xoO^IVC*r2joP z;XWceFzAXMP_C4EG;}QPX++)YdUXfpRG02v9~Uo-UXyxN3?~P;=70@`Bzu(KO+y0% zA+H4`NH3`p@NO*V>jMJ>G#WiW?E(V(OT<|a#70hpOG&JY5t$WnS&SU!b7gK1k_k4A zaXxXl!<((j7_I7?>M)&{0h2W_mu?vHPA6p=m-}?D~rTf60nd`Zyud)ZT za;L|a5~bjvxo>4Lk;kZ(9i9(*&-*DVFV60KnIHpZw*EJw8F+ zR~WP({1Dm#_QFeVEMN$E%;ve(W)j!qw(ahTJ*RtWzh64Zd8ojr6`48rQX?0fLXusq zr=I)Lo0_@X#rx1pFf0*X-v3z0QMU;w@a6sQk7gA(_m@e$;*P<-3&PM|m6g5krAX@s z`fIw>BM%s-c+9k_)A0rg*Spgv*x1@C01>I7;q0=1H6nlxmMcE)U#TMDN)pRVBW2z;QOA2mmI)Ob4gct^T2*a~QshiirG{0;dqb zeQF&C4UCNb9B@`pF$JwQyHIx8+`q3$MVp6Rr?pOAUb zFE=;g^l18MdceuoDMw+&%&yP@vNS+8P+&I57aaIN$KZo7QDCn1i||3LLJ#qk8*fpQ zSmJ$S2N$>|v5OZA`Lh+!EjhO1AWh>?eTP3x)@^HJ2ipP2#%)tL_Glx)FOvY!vz_>C>J@ra~4dAX=Gbv6lx=gha=lQp+e0@#4tF5E!NqzsNZSCf z0F?@Lh4ytAw!~kTA^XLysiC1(I(bR&M=|nPV9flu(;~=BSJST;X_dn)B0}{<@}VVi zG*7~FP9D5>VJHJ$1|1&XZ#jEs3=t7x;#1Ausn>83}FxDMRlrg`}A+D(Lt@)sV0 z!&fsg`>H)c8nQ>rcr4>`w3Um8t#yvZhQx@?a0UE@QHn@?Zy@1I8Vhkd&*mhg&8$x^ zRFJB8y)8qoDyD&=e)-v$xwjWGg=n)?A#JAhdu0w>@%a0S9oL3m9IIZJZ!8(_}_ z89NlOhR4St36=81-_q0gcsR4LgZzs=O;b;Ru#0A2W4F5kTc3Sc#XFgA=g(G| zXrPXD9emDC40t87yqR_`6Zuhk0Bbz^ftce%IAOjkH4V0VxeoB(7;ATY(x3SbtASBa z=7~bM#*^jZ$Pca&=IYGc?39mvgwRJBfVMxlRZEtJT~fd!C&t1{9az+$O$r9PV`B^= z&Sdxz3L-dTe*7F^ZG(C3*#;57tNOjVDl7-)0=tF=fdK)o&d!(4T5me96LCmMfx6)y zq)CVW``Cs>Py1RurF=!m?P?K`kmnY|+3e=xBJ-V}VE0y-Vi{3nk~;6eMxgDa$^I_6 znFSXG8ylOFwl?UYZHEyW&3yH&9@g5yQpI>S-@r$TP_EPO>Z{m}aQcui>)M){-+x^I}l*OmP&u|g3ymxF<2@Jm*>ECvZh!)I5RluRrSj^4mDH!H*|i> zX)sw;Yvx~iGKO}bS|)C7GT8ky{m86`TZ4;Y1(aGKKTP7#c1=f+&}AU-&wCHNPt?bf zpG0J5XM?3iQg@Qz;z^@E@Z4i9D_3{vNq8FBr6D)GD|-Jpn&g)_;ro;~hGxvg`r>PCzjIXBEE&d$=6 zZ!!#j3<`YM`8E24he`~-6g=Sz`1rydLPKAU?l<0-MnFU`om1h^6gj9jy5eATez$x) z`>!vdlhLxKMVq;b%JRw4x6jptmAwX5lWTWdJZWvXsBFn*&_O)t^W{D|I1MUCk&VhF zdXf2wN4|es+unI^%_)HHlrCcx8n{EVRtWkAw*JLXE9x;T!XZ#{%~`F_v>ort>CfuS zqkz_oskr+3tlk>*`^G4~s(zCte*P)Ie?R6 z(tY~-TUuI7&ReZSQ_l_C$5s>GL~VRIxGZr)Q2Fo6uV{hH4qmfWId$ShJzDNjoW1pA z6|C}L0}Qv#I^^i0$BW`(_Ve5D2`KWxlH@;<85}V`Lb*)inC2LRpZBNhHXlBJ_UAhl zw`x@q_i~C>TXowJ+T}8Zm3l=imCNJ5z8b7yAEiwrD|XyjDi3W zULGDWPR~733ZqkK218R3!SfBbiMCsxRP&J(*g7SVCa!bswHaTiDkqqtQxjQk=*{Z5 zTp0fL0sHI&Sk1ujdSs+J{I73}5@by0Nc($3Rg^CC^|yh&j?Sm$52c-TQ;|(ygM|o{ zlgE}E`uhBRJiwVbD}2T>S&=#Fe;YAiiouj`p(h#ygnxE-Uu(u?O7rH{ z-EH+wu@K}oP%YQt;FY>R|6K;&n1_-~irY4q!NvbQG_ft80E;G1_`)#`=%FD-agQu` z2l4T`<*shtdMgu}P6Vk5EKy^>9j-R5p=yWQt{&;t+$&V0A8RzE!>-#mGcY!80Lzj8 zJ_rS;xcDP*!viKo{VT7uO&5aHIF@Yq<&dVgo8FP}4n!)6itrpoa9}mLt~Dzwbh=3q zi6_mN#`NU77Kt#pzD(3eWKrlTSNSw{=Fh5|cs>1FDe^*7#-CmTx=TiJ$xm>tI`YnJ9tz z<=Z5p2HXa0$fhqJGrq|o7P+lOpxONju{bXkAps5oy++v!^th-9qZIS?xI{q?>oh8X z3oc~9?nK?V{O?aOKP*##WdUc@!ootj^*sKeC@=5pzk*O`^m{v%U=U>8-?+Q-&idKYDW~4)pTJTk{b`{c4FfT?b=Ji;G3UiDGflK|Sp`i={}7 zbi&;1VeatCy0r)tUb3?1Q=?s=6@b{>&Ud-a`yp3b)#DB7vqigpl{VG&a6Fi0k5PI{ zO_^sS`F?A^BKFOI`InbvWs)|XVSoNSAh?QRaSwXs&iMj-!XVWVTtvW)SFPrKq1u)D zSNf6P>zanFzeaIc-vV+}wt%goudlCx0qXhlPheSgfkEp&qqeDIZ1eZ5cWK7=t}KOw z(Eo}n_KbcsO{Dy~3~xgUDxZgCf=o=H^0o)w;EDeOs+gv-e)Qqrzha1)ATSU4Bgq^k zj9Fe<|2u3>*5TkfIy%B3Evb+H5$qcjczkrqe^ZzLO%SU}u#|GRw3grZlf^o2kEs}c z_A5AFXecN~!2<(W7oX$yxu%&q(SH8^qW8z$z_5g!ot@$atFk%V!lRe+>f@PiahCna zh-za9RpUOK?ZU=v#03|Fm#F%)M>p>h_MaV08&uJOj`WU^kxCpL#5M9}P#6{wywh2R zu@7KAF!2E2*(8hq_Y7O`Hf({~QL0gbdGmaQz;IS4@e(%od|xT?g9-#6*Xg-`*WN2Z z0RcUz#qZv|yVWm27$viJD+9bE!IAO-$Udd7>nh$WQX~4`28~wh8~PdYNojTMp!zNM z7r9cn7-jfB&oowNNPhDVwoBvOj690V5<$dTB1d0Xt>6S{uqUWoYb3~Nuo6aY<41>b-9Z`S^hJLiSJ?56+S zlz!)rON_NKt{H_lTj`({t%MvxNIRy+Gw6xj(&>HoP66)NQiy8-D7@+ac`5wFlkfd+ z^H<`PUI-~0$Az`vsiI^o9m{A-m7#AMs6)dd3TD1s|54xS>WnPdOVk9Fbb3F1B0Y0; zbW|xnPSZS+TiVhcEq;0Je_wcSrwsBaQ$$}i)LNCQe(@)U&!}^lkTmfwL}8Yw-+-v* z_pK*JaZn(kz}-bIRCe%}0=Z<7PA@PYNWprQw|>K#Melw|J7=mtqeuD|RVb*)mBnLS#`Tun2X&M))~@={afnod1Z;GWA?S-QHaKj%^bHQu@JXrc1ZRgVb>^r6r3h&dl z7(Ym6MBrcUvIRJsI#W~gl_PyfF%p;SXh_$svo1Ht=l1BmGRl8X3Hl2`LH)aTrJye% z&&KUXv||uW|C7o0aPoX(GmNM) zH1I>uE>{#T;m__pHTz80gjQX zS^nS@-wQENUQ^={sQLTP4R>H1H=^;WukVF~S3zzrBa87u7>YV@O_kR_W=bpz6)i7C zE3od#M&mdA5TZ3kG#qc+*f4QPvf}QFIu$xZ*M2@HPT!||_m$m}Oku28rHXc#v=zei zv{sz_zXk9sCGs?t1v~eE_7Dy}T5@LR2K93JNtNdeFpT^AckjMevNjuhV+dDNucy}gPw8vWKm>m z{k`B>hipvcSHxVU1NZqD-a@C>bGp61Zw&EP0s`_5 zrM6`lfMX#!D>t_iCZ})S5c@t8F|HBNuv+(F&^LtICO<`ske;*FXGKTi5Qu)rRUr*4 zNJalJE(5PES^B|Gy-0B3Nup(scuXCP?m_1c4c*QR`WZ#Zay7a2_nDwaYR!1DiVfKR z0*nr(RtpkxQ{G=6F~=Awm>&;U$#-NWB*+KkLjennxd%Uov9|@V>Exa9=B4)b-DP&~ zuw4wC-B?|10j5PySEos(72FR&;L(b7U;Xjp2gny!;mGN_TvUy+3!P*C$+UmbN~!0T z@*#vW+&qvC|H+!e_Jqr9RoDCU?aSDS->pN>5eT`C^Y=@?9y)}PV@1j{MSgt|R%dMb zYNWRpk94NhU$|>)x-HNICL_bcWQo~N^o58=$+OAp)STJ7sh!4N>brldBOoOOvj?hJ zRGL5IvMD-ol0f)-=xfnqXUi%tp}bBI*mrFMvAIaO{Llmq;=+ZA<_HuRVs{)Z(`TVt z9SWKm$9et39Ge79VaT?f8S3QLu~jlfv~H4Oob*0 z3E~$QUY`QgjG5~;kY8y1mAD55?(b$YR*VsFzykhi$LLlixR~0Vr*=n;}4`^r!P!Ck(SJgLrEp_ zW-L*D>??jwFD8%7&c5dI82g_saDzMjhw)R`!Fm6jn35Uj>8FY{sr9_=3iLL=vh`QL z5~CHrpID4a(WQ-TX%Os4wy8+75mo#=Qcm~)!F71@Xk!}e99949NS&wSTlldAI}j!TD>uD;J7VdXw^uip|GV!UeIHX!SoUP-*&lfc{0{lVR^5JY zMASykCr_w8udEEbDZqjUd2_In%O6}xkfve+8elA`z$JO>7G1f+&FY|aJolZ3QeULl zq|7(QWNBy!CD{LW(R&a~ww0U2G0KHR&Q!q~Y`Q|Ymop1YTF5#E(ool|;PMrpIRE^y zMorK`t>9dVQKMJmdUelnFf0dIi87{O$%>k5%i2tGdkd?`YZwQ$JVNpJS%VBY;) zvtZ$%!>ufa1@0EDXBTdp_fQ@~1SM2J4`6Pm&~}?@zS#(b*nb>np;3aJoGa!|$5N^a zW~?H{aN+hYLG+2!%nYo-bzGkTJ9HCujIklf^rO@*q|=orjzLDXq6`8455@fP6SHzB zKYOV|$silTay@rB?W(J_u%YZdvhW)5iw7eQNq=z+%g@Ng%$ zBs+UC>Y%R_+%3aznC$6M0{u9gIhbDsZmj^zRXifT z6lw58iA24VFG%47lQC(B0nYO6_l;-?_Kv|={l-k;#hrMiXMBg3%6dWL(*UQ_bdy(9 zlmns!a9&~Z>X}FKo=K~8wEFO*^LuT8?tnv9oavQF(k6x3C3lT7(%au`sF7X{i%n<&K}SqD@bBz={bf^o3r#g&$o~h@VU$N=t)&>08avz8g+AFz-0F_Q!md zmU%?RgR2FGYEYrwn&3ZVvcOCK4H$*C=j8NgKtNjy-dNP7FP9EZzFLPynS!^Jk~$cV zGy8&F3Ines8{36)2S?iHsj0>heYHJSXk7jZCEg?6JX+XsKz_x{)!AKW`hw-x@+LNcyv3`Wdz>w&;igvy zx%@ldU!h$jI-!j;`2k<`Zk@BB`!|sSbIP-m0VyWzc%=$u+2s0R%lY6UWAwj=_2v{L zu284Zq_P+*)%?0U*-Ao_EOE_PAYZ$es7keE{7PdUwdn z=@cG=v&7>q;NmcMK|#TM^;2gx3i_E5-76D(-Q@VOs1BdWaAP4qL>=lK_hS%_FR?1l zl7>-`ZKOS}bQor1VfYhoyaQzqWVS$tV7FJVz&7@cE6v(DNCTl*23qz~6#v%B?IIOj zUEP=U;B7n`#=fD1XlmgJ|!Eb?RK_O_vSDNHjY&l0k9PlUK$b3KCQmXZ*`Q0wwFCA}h zkGQ_l4H9s1d#F^QBESQn!BgRhsVUYjhyz0)zgOGicWnu1^e&m%1E?lEn%!{?PHs8u%|yN|1| zV25kKlkz5bkhpa5lN0sc1B>vf#^;yI=%6o$_&PYYI2M2773@aDz<7z1mDOjyE7ZVQ zR?Y8XTPN{BHV((RA#5&W(LP*>$egZ&lOvx$1;-Fu{t^gdS{o{YIZ>l$UlLv2rzb zI^Nc(eDNJ$eXuY&GORSzyw?)GL=|t^K#vuDJ8t+rB%;$brvv|$9@J$p85uC^%Rh_ejI{ z!2>0W130{#*)@6W(_c27ZqwGGHQPv;?W{|x4ZKa>pzeq1vDW!{#%5$>2t&}5-}mb8v7*&vvaV$UK4(z z{z`ivN#f=xdJo1@NUs;JtQKUc2QBa@%#`Bz=X)NbVrFQPnE-Lq49Km8r)dGi@XAtwq3f1_mJjc`#Y6 z0mf+!SeCUlqf9>4htsAsxQ6x+8}d%{Gy(piEK^uX;)S0xqDCZp3|R>^o&iE(1XoXj z;t55D$tU5RiLYY@LpNe39*@Nwj$N`GfF}#f*TTYrY<_rKLGT}e?Ot+hf9>Lr9}&ho zu7n3#(PqYT&;P>DNFNh(M7z0HC0(y<9d+G6&-Zj5dXTLqb-%M*#nYc zS7|UaVekhYwpSM8H+BKJ!Mh?RNB5+ImZH@E{#z^GzBzFeVoa_|wgi8%LVkD@6y6;3 zwQ6Lq5l@#-)2u@5&eo{i z<{b`AKEU8#L;r+gUj$7uiH4nvOLS8mj6t^e-`t;@pU2kd73J5;DF(5qXX@t&eW9BO zyu!tOF81y;((=V-vRni{0AT=KeFlUL+VN4?hQqf1jH0emeT4VUouS=Z_0ze=a$=&p z4&7XFcf?(YOETL6O*VRNSB#o zsloi7|ChVw>aU+}xYyRG=W0l{b{!*~C^lE_4DeGKFCwz8$D)q2tYh@N z9Y;zP(v0&{r1yG#^>!j6fvSW9FVRUy!h2B-W^XmXJ;;Ww*!IIJ zp;s}gBG`tsK!%K3N4|sTZ0tWm09M`>!&OUsth}y}L3B2m%qJnnY;hRq$ zSTyTeYQ1v39YyXhERXKa(t2xBW7D%f^%$lcAm;+UM${9E4i1~-={IJ=7QQcFFqEG% z;6Oa})`ycNl|E0~BiOaqbcF;SNh~T*;zeG{^ti9D+7qPp%_<|-+9T>Z=7(+WJ=vkv zAI>V*R#aEm?m?@=AmIt^_Fe$YD~f3azsT0k5^ltaWnLT2c-jrIM`W+NPZ&tEV6_xh zL(*N7B%o1>kubg|i>Y%Ox~{~xOLq@!vymF)cw&=y3%jGklX$Wzi}9^BD(@Y~U)KR> zP~0liJ=H?RS33QSfGMs#Xzk=hD;Go<6)u8zOo7h~A#UMf<+WHzV{bD_#HaP|d5bNAL+bXc{OO0mQk^u{ z!B_3kCbE&nF&P}>cJVJGS6)3vg#sQ`hnjRCrWAkYlE_qwsH{+KMCUmiJ{Gxp6Q4Rt z^ENi-bF|U*1K(~ahXhxKk&#h9Tdq_hUXGdzz~!42XX_L%5owM+CdUJT^3&7Aj!z#< z)73$-QRw5IKi|uJ$4HZYX>Bd`(o<3#jfX$UI1;HJXz6j=G%B6h9cTv_-JP23&ej^4 z#2fY(?v=ny|5Wr`FSziTW=B0b_;^>xmvacqP;&6N>{_~S|5uNq>aNZVx_9m2(HD7% zo}`MT>I<1V3ydN)BV1H@kKnX+W*3%3yjx~A9wvH{E6%!1se%T;axQt`wSl*xW?SY+ z2Bv=(pV@-aINz(F4A>BA#uACz)#Oci6V%5f5aoBmPRydu8gu}hW~e)lj>4Tkjp<7LYe{G=#lE04eM;77LAT4#X3+<0=gGj#*w{B zFEzp6$?YHBznnV$`7~)YcP{R>k?Vzekb0{?l<3~-Ip2NEP#b%pBA6p^6pDSa>eSR! zvKD|}lxF)DzjZCiDRmYbzIqvOM;wYM5DLJB3hY*rBN&V3yK(B~C|P_1Sj-NNOg0q# zv8+^k+iB27E#~Bv)(N^zclS*9$a|Bu=4!JJd3`Hqd0HDb^}9O8ORs$HQ3gsF+k1Fy zfG&Tk&SiIJNpsXx4--=?k8boJAEL&&2ysYPvYn#iP#MJ{&=#?nX>=Q4E)+AKBOwj6 zVplN&AJR798X5=P(}d~5UTiE1e7jB;E^f1h>g~Xl0Z%3gjFbf2g)UKya0s^!#zuea zatJen2I#$dEGkMh(skg|Ct$~1BBprlR9%R977SyunMREOtwxzj;IgEmL%DdKa??D= zTsj<)8NyCt_s0i5e@+s)>hTde$vi*U-YOQ~y1H=ez;6E2PA?2Fe5Gp>j@@uiT4~$2 zK`paoEw(4aY%lvONMf5uY;Nlrug$mfpy!j`e}0rYFvmL-_5$}~(ZQC#2SA7wumjS+ zbEka!k2;M!8d_8w`n7q5R$1y--4Heak7F zrIME~kyC3^|EAB&&?6!>YiV@_sZvsc)@@?@#S#pGT(^CC@dbq6~Mzvi~8?jgwK z{UCL@ywh*0qwO{w(aCAwMZ3_wTGgHzWB?CB@(8BgGjp~2h0)RKPy-kkAUR7gHc7NX zO36`IaG2f!Tlg>Fd=g2y3dbqL1Fzz7TcAvOUTOAIm-p_fM^RmCNK z8?au&b6tl%!S0q^|L5^Q0Hhzk45qP1Sw-FZ>m~O290)o&lq}L~!BW#KJ(x%e$W*yw zU=VPU?pN>-c1p|@UIz$sb|}1p6j4c_g6T@kj!k!SGA;)8T$AKw%x{PXlW;5Qvyepc`Q9KQ4nUL;5??RL{|Cy0{iN( zUzohf5!Qw1alVv+fu$)72sJ)9NV1*&96MRy-92G*w9n=V_N%7TM8DU=eHq@mZt`(? z>Z21euY$9Y&(n~I%9jQ^Cf334RnSdqB6$!2%UPJx3Rh*-#`GrA+{hT`dE4s`%1G`c(|HkT4#i zr$<(&hHT3}ihs{=?HYNtn;5YSsNM^HafG)aU|!=1`hUo)RpYnR(-Q#D6uc0Rjww0C zY->jPApEe!H%y-zgB!E`H@lR_RnJ3G~{ zf?n4dV%2bFl8+l`r?AoYbV*&6k+bpem}w6VRGLcHufO1U+I^l|Ee{veZS!L{U&$4@ zt3KY|EOGZF+3!wQ9jvRXtAmFIMCB<;xA*t=(@XjCdDJ4V$2WS;cfo#~Y4VB?NwuGF zpyxU-o$lEU%Zl2lS6guS(Fs$Rh%b^mI7ut(K}~6~M{I_(l$SpLHyf%e6K|m2y=0J9 zWn!oAmQ?*6*W4dU8mV~*eDFX}K!bbMd+td%D8)aZiZMmau>lr4 zv?$18)a5p&U5fB8ABoTBKh3sA`(L@Uc?5LuJohzlmJ5=8*Zf_V{fIRy#GqF6P5Xr> ze|=r>^pD+)XXT&O0Z#;sYN@FgN@Y&CCs&5-Q}?2;Bn}{+Zti@&Ll#^{>+R#yak_sO z#AefuvoIciOxWh5x|U=)0_W7vx9;!n(T9=RDc_li)g652czwK}vF%1KBSO{bv7R0>37mS#MTQdI!Xy)>?ix*9>*5FQbs zZV%;{JQ~`hOYFP5d@xf_>h;F6MpMs{BMgZcE;SQt%+QDF@1z{Gdw#z+^He>B&urG} zoenyPOv=65NGQpPAQ3X6v5e|=&h%;UYV>z3*8yZJuW3Ed+XH88K z!0v*|ptyukl(8(Y&Uv~%u3UG}FJs@TGYa@P75hi4A2B%SVt@GjR?WSBIFKqW-bwRQ zFcz4n(NR)<2JHX3OaL1@IVal?T!dn7K(sSgQ@iBCXY>=Q1kcQ1A9;dGqw{4_;7iE9 zY^9YuIwQH(^HyNNmcw(cSoi@MFmjD!EaQXNobp>H?B%-Z zs&O`W&c|X*gDK46fCR<}b4d=N&)LV1AK~OAp%dZaeVv0odf$oydPee_y;GEHyIlz& z7z$ryLzUU=A|yF>=_{1&e47%IlDgMDnK_h}lEZ)~Kzi*K+!(4KxBVUh88BaTczF2I z<;$FLQN6A}5&>%Fdkx_Aj+f77nrUw%*+YIcj-7(ZjK8l#2ypKA# z8rP-$1P(Mjs`&Sm3KrdU*9B<#3r`i$L>p&Orjn*30zrXMi^;?@vHISoG}{%Xc@E1* z>GAP2lxsd(_fRpaoC^q}H_gJ|alAuP{YV`GaHo@r z{eVwQ6J|=0l9K}%&pC*AyVcd~VL%6(!$|sP(F7b*ShoTy+qwA8VntJRWt#w415*xsdedmV%tHAzkci>M7NY-VNNEWfn{DzgF;2 zLmtGV&CP#7C-XZvWnU5&B5-UsAkhI>voz0s{ro8(xU)uEt$S6Hua@LwIrJ*s5#_s{ zxA9@Mwg;H;M_`y)%JQ`XMZkz3AAh2_zU}xTpO*298wnUlXIsYl=MOP?i-H&J8DpT+ z^)&na^K*4&{aCWeINE-iDS^~?rmW^lsScdSIb>GS>IVa|c%rjO-H>r7owNtD=)Mt}Qg z2TRNEfN$Dth{QS!I7Q3c!BQ(3%f8l)9b3D|tg^E&g}n3W41Y#xXMnc_3&sEbI|nN6 z6CbWxs+x}fafk9koE;ljO<8ZV0cy;^LF-FQX^F#||K`#+T{c8ZoavGP%IT1qO|wVY z-rHMf&m80<4wY^FUqa;Nu|}L79E{$q)i|hTB`fy!wYKqT`fX}9Y(+iwVWe>|vx8yd zZAC>tdv-+k>1pEUp9>m)w4#*`k~Vn0-)-)0t>(9CJp{gs4Q(XtL}!G&g$ zlWUMmEO0I0r>6Zfl@5eA@NV0_Xu+rP<6HI)vkgeUYT~n*F#eRY)RBM=OjQfx<1gPx za;dg(i%C=l?k6h>x}nPne8Wxz92rM^Wv^d%f1peU?1lE`S2XvEiOyem=NzZQ`a=0}wGFKIv*osPMXVuAQ zOuH97dW^ae)ahu1rVN}LmYMqccycj@*WW>-2tM%V>|fJwYm+TKtszd1fkeWz8u51m zW_DvEBiPd}O5|~t$!?wPw8@LZO{xT!$pTNduQcD(*6qCDb-EP1jI}G9cI;wh6cFTI zYI}oJd$Iif?%PGBLdZN=s>YaO4ld0IDRgW=gQ_t*bR;ShdX}Yz@ynyqs_Ah8z0xyr zV?F}5jMTOtOekDy73WFXJz1BF2^L!3eN#YAxV>&_X2u*f@F};{qxjPuY4m^~#2hcs z;AtfnAUAfC`=_}5WkFP$>2U=WyL95Af2Oo!nj43cw2y=A{rS@wgavSZqIWjvopVzx zb71R+gv>YEzs{tvuHfF?ui>i>u&$vM<%wz8&rP^}01!W5EclK|yakKN+=+^cia)1D z9u*<_=;1$%%my;KvEH|T{`?vEN+xe68s)EycQl2!kPIX7TS&43ytM~QHq|ym!@7&a zDOh+n1vf(MuFqjn?*6?e^z6gJa!zTPeARXhCZvWgCNBNQ8_?7zwBa%2~ z-5X6l;dW^kS;VvYPW$)mck5lF zAOw|t*uB#@Y^w7u%WSOe%2%v*+D6p%P=wspqmS6jlT$rUj#b5E1#ZdW@;$cH@-7}3 z9MryZ2P3vw`$d)pO0UQSi&C9yEVTP(U20%+B?kXq>ktL2Ke`Yzi#shoB-QRQSe+vUe67> z6fKJ21V++xI*X1F8M45`E!*@(c&lA(L|(*{enM1D)2P+JXqD9}L;1s*8&;E zsn$!Y-5jt&nC2npeU8HJJ(fV;T7``HNz z!$sz>C-nFDfb5jq-&|R29avP=G`M}Us(-5*EAipa5$8RJ)|`mnv`A^ zJ&0G1p_2arHfF$;Ez!*-on8kM2ud^{NdoBfA&Nxe4zlE|JbY<{|9iEZ6cNE)(Et^zd3ANk!01jvY)0F zl6$tBPf(xzM7h|9(IDyO`(ZF6RC5P>liDA(yspg#hB5HM|Cr&ZIc|uguavLZvEBXa z!F>gFx7i$r4Gx)|{Vu02%~ZYG@$qqa30D74S(1btn8#sPce5v@cX3brGH$i%bxRHu zry`qmu)bQ&8F!?^FAAN)mX^S`-b)<1IVZ~IRr@Bho`N!sEaGzUDQ@6>uNBS3v%7!( zk_%DFmyH(({3-2pfyW`oBKgA|YG+JLOnKf*eYyCKPEH+1TpIC9BgcvW-1^%MT_?5I zU5io^%%J!MMRVN@45lSuH~Hy}Z$tc_{k>K0sdv*Z4kOEVKiD*4Kf?CF9S|?1Ud-HH z+84Ps0neQfDBBFI)M}E zgtj{;MmwhU1e2NXbT({Cxw)$&+Zt@R`CuVA(~G)}adi^R_r}pHslkEv_=l08pddh} zKvv|Q(_yug>ec~EgVMsvZVy~>N=D-u`{}{pn{%1u#~k@*?c~}Py*Q)-zb0uru4kBk zx7|85scx;VQA-r@m~GECKU=Nr2%7+W4FHxn21yDgGbC&sIvF)StA=UyD9! zMZ--fXr2ho*L4YGxfjBi13+NwY;U$0tGgG*>=bI89h=4hH#0}9@7%(i!i*mX9KW+o)yYP zNcesB8msw%AZk$B#FmXxC;R!(1+|czL@l=Ss!*y^>KRko;wr6@urI;s(q;hHgIT)# zA)q|%>9J@QX3QNQ>)?-!LlZd&iyc`+d@5)AcX&C_U3W>iP{I35sVGrJI_8FNq~1IK zmL31&P;-E)A*4U-9HJ zHn)T!%SeyX=g-O9h6yinOe%(XhB`a34vT@<283O^4Uyb_Zk$K-TmDxGTS%q@uNV&6 z>Kzw;#+r?#rsam0`;Ngz$b06ISEl8?cy(u#GxzL@fpnZ3Vc~`A7^fL_R7ZogQS-^^ zX;gv*pxuBJ;OgQ{=)VBzD$+aoNury#ucRlRxuW+iIW)hpH94AO-%L7~^#xo%1`abi z1$7xHCyjjWM7*x3Bx#pXHl~Nv3p5-R6Y0{cF`{l3N!e+*30g1cZKg2Ie8ZphH4 z^TzeCwvY#J{-1qSln#)W)yjec^Zu=oKe)=QEnt-hIN_QuQtgSF{F@8D3l)E^s z_&ia5Vu%-*+|Qr-u`Ned0biCWb)6baGdSeKAw2xa6Filg1WsI3p$JZJ>PbwH@LwWU zNzrG4S&0b@r!N9?<^MEgljop5lR%HMW4+*@7nl&`RwWi;}#TLR(ZK3q4aqi}xIfzHA=C)<${sw0S4 z*LbutkYTvsEA{@5tg!nA)G}M9T51`}V*D@%v6{i(*JMXS6>c1$Q5 zHs^(goGxE^!Fx-~C$P?!GriW|w`ut1O?CeGvl5`wZQg8NvUTjJ_mRJXk~@oTKCU?a z+sc^Y-w6}z$lc!28!vvg6lc|+y?4s5IcX!K!#EmMRBZhEg+6AE?!qza6j6+tn5by6NIJ@P#`JGESWVneMhSipU@l0pj|je6*!mUssd^ML zaeDL%g~ViH>fZjLeLGLDfpc>48&twRQH)MK&`#`c97_|OK9I{4Lh$!5te-;4W>cSJ zVCQ{DN4!`yw`0bJ#63tG-U(Ud1U)RsaKLW}s6An?5#O6rD(YRo@GzxbB^t{wRE@_Q zy?k+c!j4co-~u#bd8(9@%bDA^qzX?*T)r^ieGk0SO`n-`@NRK!hp+)pwnpa0xlvR` zRmb|HmH;W`tTY)7rnos9KC$3YTHhW@R@md zOE&3dFsEdUcnYU$hoAi6p@2cfJ@DT7qYqJ__&oq2Cs#<>c3lWt1R36)S4Pq856c*dlW(ec7oM{I znDI8LXGH<+@?w(o;)(0S%#fVwcNUL}(;KO&5;57v=?=^*&uP){yBF;;>}gbXLY2GJ z&t^jA%8Nc&JZ2x|YI}p~+%MA>>-TB9dszc%bF3&QgHt##)n8LJ<5Y^MqD%yGCYao& zTaN=T2FiRnhmT|zJ8tvo7px&l>4h8zXqqJ_Ue|$_*~k?a<;y+IO-*`F!EU(R=s7F8 z;JDK6wG?shG@g}XyX%5b1JpZV2$kih2bSNLjb`kFgtwUyZdM^VZ@-$0X0}6(wA+hH zy?9H1v|Tv%*e#n&r2RjCa|mQtP?)}}dlA(du;-e-2)t_wG*m^(q*BL6F0QVmn6#IN zMw%bB?y-GnpZ9HIN42~iic-Jsm7m}c|MPxpb#Z265}>C;YNW)(k$0QCqY!d8rMQKK zYXLb);WZ&8Ai(azFy#`p?AYw_-=t%fPUD-!gI`MCfKCaZQ~nQ6-yKhN|G!_6y_3Bi zTUKVu%AQHGvSnmbM0U2UWABVmk!&(U>ST{3WOa~P_U89I-JkF8{_lR=j~nNGUhmiQ z`Mj>{d0lHn6>ZI=4~&)R9MFd(Z`4>djz#N$RfX^3=s4}~^Ma^~@Is>|!NaS37;K2O zWk6d|z`T_q@~Q}1B>}bJH&!XNGR@a7f2%m#+TL&VgTp0NCwhIT?*XKWn|g!sRC6T) zZvAM}2Ddp0F)hVcpGWX!<_8t&)_ttaTLOO$f2$gs&W|4^!%c5xis9YBVYJ|nVXz>P zou4+;m>}~FMeaReeJAG>Tq=r@TEkg^BiiD*MB?h?;v$ep)HFgCv6B&{Hf>0;Nh+&< zAa1^*rn)#`(vM)3VqxO?v70aG%0E({@b66mGUpGpoPhf@S1MlGltWlsSCJvZB*Xo) z*pMcT+HV@Y@svGe<7LIXN&m*T#h99U(LpzQ{L}7XO4lP<*f$0!XJTUFE)}xUl>(~I z$fc#2aNU1!s|6e$D#O=`;`LTW^Ywcqx?PyZBAHTI=^jRK6-vt&dJPF1P_I3901?CN zaGBrrI;x}yE3>w;h$Gvghb_*}Y}AQWs__D5USv0uhTEk+8sBTl>fDA&oL1AwoRCRAN@f(o}Ta+}hvdA;5y2s0RFEe*0 zMkul#-evG^HGV2S;>x8dG9rPjPcmx@_zBPR(YH5%5LcKZWHiRp`9p2?{D{~OO_Jm= zxHLP`|Ch^0W;ri94nhGtJ3F76yu?JzP72r-2*$k?j21takP$j^6d?E?++6)|8Jh1P z!&Hu{Xx-+c;=L!4Sv*%MeSB$XXzU+7x^&FD96lHwJ|+fIY;1uRP@ex81kc@-|8OpX z$Mo%f*EI*qOD!B{PhUI>jO@DDy(zPQ+ZNepN*ZnI_fr?|Tfqp>ZxbJ$I0DlCMRkMt zm7)hE-9_2R%F0d~{`a}xyoZ~G1q{O&#rr7?Uf3|=jx=$j7M$gfOE{FsHMc(uu4^So zz3DSBe(6%&_{FQ}0i@)k(bCP?i{9(k7poBoHmsOiWPZg*S>yp-)Fo+#tcyuEe^05i z{aEGMzj%JWP5ac|<~onTy6L+;*n5iXhM63Om_Y6C#m#}IfFeA3<#J|#xlsysWcY+4 zoaYKdnPnai>^wB+CatAvmc3rkW`<9dE18r&Yj zkgs4o-0T*~dvzCnY`7&NhC`h5$~`$J<^E@^2-5wdELAGA+^96=6)_;Ve$=aO*5yH< zFJN2nTVg@ngn`sAizDEJf_ld>nAdl+qa?2XOf{8L> zK|X)pXaK|lh^{P=V0NlD;}7dY5QB$>3%wSgWM;fNI5emUuWktnH>A!jc~vv=8fMPM zbl@&zAq?HXpdK&-nDUz|U!Y+`Q&jPkJ-Dg>&^|t2BXH6Ms#G!Qy&+8pe%RkKxzoF z=wl9-)vipRBgRFt7%nv2P{PXiaahdtE}g43MC!pat08;`!2r-~G(L%Wre_XHGJtAk zt!84V1&b>ztgSog`R<_pjF>QF8csSG-t)cbx^_IZHm%f38#F0*)^_+yz$L{0dOB8b z_Z_t5M1DkTa*a-FjaD+judTvOSSFM`JFtF^LyXXguz`X!cxU5C4_GhNZogD8JW(*8 znV5k3g$59<61ri1x0QbQPuC+0HmN02s>hbfIZ->_c-mV_$eFXfzkmP2RRpVp0=I7E zNbD{2q$Tr^qjd5-Tx(Jf_&*{D9}{&>7Z8xsC+%j3-_l-^nH3~JsW7{L4>~Roq61eP z-~A1(m|BXQ+V^`7r==w&`x-swz74$v^jEX*Y;g+w52_z26#+*;5IPtbxG=xyptyej z{tjjXDOiJ0*pEtSt0&n|4^mzShlcP834g_&!ny`5MrjFAi<*AaB10b9Xs@4g*o)A*6hAQHxx^X!oa204)86jOfVt zB{TgY+~2?^1F|8Egj-M(@yy2uP#?e4_j3;cb0V0I|BQJ^2n2J8>h$Bc2{y)|mnzMZ z9}K*LK`Fuwi1d-kdIqKI8M(57{i?<19rQh_;h$|5X#fVWv%~c>VpZ?+9Xa}IW2#d9 z<=)1(520wj%_)gDLWK>@IjS-=D1WL0#}J2P2KJUf^NAHK7g_!jm&31D_37?OQS*!t ziu`*}E|^g*o?e3FZGFd!JKXCtZF!Swx3^d>Jp&Zlqc52tn&kb-(P{;3u&2q``{{zb zL|IuG1mdl&Q`?e1$7!$I-YK!D(0!U>R#RK?p>r16M;~j?Eg70TQ<7ub-sUR8y}t2) z&pCH<+Cug&R+i*G(5qf{bhk9keR{QfLCf07$qC5KOi>49LVHUL&#hagcqrJ}6+-pe z;j?F~f9}*C+*Vfru;8OACtVJEgQvo9oHQ4=mk(J)uyPdAaa#KW7_9;a0W~r+1Lbdq zRDEaOcBUhHy0gWxKTR41ql?J4(WNN|N`AVeNyeP}eU^hQ22)MHu#p&4Wau~L$MqH- zB?bE;qXu}Lq1xVk^V@2D={!poE|qVx()y!sMuD#eG)Uw8*|g5P(Q`YBmi_ImEq;Ff z+5I5kdb)Xw^wvOOdG*8%1{^!}%SvbX<;JydjH(wqBc>Ke;LyTVXi#aTWaRbx>Xq_O zrF!qbb3gD+O8Qax#rb7dxqqIPUh!jJRnTXG?dY#q4&*AFHPD(CAvU)Ez+=box0We}SJ}lm z4QeG?k{CYG^Ch4!8|;(CK4XC&!4ex46$R!afW6OspB}PUAHnYOsx<`{^I`{U>t7Us z&;NA^faI@~1Cq}J*5zLOCtbO>7;tjO36n30a8N9$CiCdFQeORZD|(os6*{dHUj2f^ zV2S%%faOc`n>|FI{PAJ9+(b<2@~dq_^-3r)I&1FfOp?_S&iJL*ar@O-SDlEVfq02e z(ojRm6R!MKpW8a0DEoeLbLAy9SXz^@ zsCqKaGduhOEp>*68h#bjoI^=QCWjy2+8S{GK9wtf;sb~cfHke+BDdo{3e+{@njp1s z|5QpAcpyKlDKfj9o7of4uY$D|f4vhzGkkcAUm`3Z@D8Mj5bJq7&rS%NglXggiRUJB z^PuvsVlcUnxx_tI{89Al8fguyN(54kx3@9lV_y#>7R<$7W%~q#bZ98H@t^N+4Ftu- zQzh=JR=T1blc5XV>`pEH4<93KB7Q+NEB;=jVNk*v05TS@b1c^sQ~%=Qb?m!28t(OC zPRI1QMwx)~Juv=2scfyNs0h^){IjxByG^fZD2Oa-k3ex{qun;5lkz}ZQxHaGEM0TG zs$_&5fB0laQ0s2X~OmNHMXTh-V2o z{4g3$gGyneo_=m_))0yNEU34x4P+=27t{i6BDW}5R5XA*OBBSlEXI~K|AvagU72Tv zrj=Hof-Y<@SD=aL+%l1Oy%*^se4H)s8Wt-?$H%+EIxDn84s!$aeDBBi0qBLLrGcC) z_aa`US9y3IWg8o!`u|Q4Zg^zFq^0xs=YxLZa}Zn24zDUaa0nSEi{{@Xk^P|fy>{p4 zB?eC7NYaXi$@dkX*^V~aUuh(>Iba+)Kv-{x!k8eP$PZPUrEzflWI%S= z7W)3JZuP`a8|Wz#o6k7+_Y{T`Rnjbx4--2?Xp!~x5`A=7xFkiip%STyCx9Hc1e}Id zZ4H0&OSM2+eio97>gG7ri?M(4;V7i(J%5xTTS1&0;@q=3!RAPZf+gq69PcOFN7?Ex_DWc9ms`Jg4L3<{dsBM`ngF>s zR}MrJdiqP3P?!J6xU~(Y=@JwtZV`XYb08=r!h8(PAJVhhw-ML#IKDQ3ctl3y0xB#_ zajE~+F$i?N&>z?0(4vsu3lAndRwWagf%GF=HB;Q8$s_hqpCF`!QQiKUGW$7vT@F;U zu@4bIL(mF2S;}b8pj>4>j9fs(2+$?rQpTbH3AVTY*Vw3W3%(uUBn4iFCR>ToCD~c zyK7Z)To^&Y;6>Z6u-YK3-oCDglzkd+&%CU|e4EcfR!D!+=W=wxeH1WMgMZ=1bc?i` zP=e|^g9;{$I)w}cxz3*;5;ZlEXl}rk9 zJD0b2?Xll!B+Wvmg-m%#S?^@Z-5|=rV+AJ8J9LB9b~Ikn`hJiEn40x$aaydIdj3s{>wYaNbz?Q-k1^kpt1@iJ1g z7d*M7-n?mXDEW1LX3>qSe&9M1Mj_V1kvKBCdZ!;E;5MUU$3_c0(5`w4?=Fb{sIF97 z0no4^3KR*20;a;M7Mq9y=g@ni&W4ZbLcmp0OmzV{@Oe5&Z4n87-7pw3zjg#J2sdd8 z?l_3asSb@rDKK;@La!G^K)0a2fW<9mUMLJ^F3`-jqXcOusmGn7sPDefR1 zbcX&LM|+)j_7|-`-m%aSf0zy@UoM|Y0DoZ8-_NhNuTSmkSGH;#mu%eUES$%p4lbI_W#e(cs!!o7s~n{OOw-AXx`(SD$f!N z@{^s}aXL2)`LDk=&qcfv@(OSB37;9J$kO6v7XRp+-7c$coWyJ(`?cl11wItu+d zt)9K-?SW9=Pb>>0@lC3d<|WBSBV!+FjHk%sa_A)65%jdQ7ZEeC70ZH3w@xSX|I#D2 zZ@fzN$#nMiOB$5k9Sl9#GO+Meoh3|pr40Bny=XR@Aw-T>ra%NSJr3EYmDm2hNDYkc zYjdx)NLQ*7Ihy^5HEnoV_38A+!h(LQpAUHM&Pt{fjZ6IHD<*k0 zT_kfk+MmyC)s-pyCr%vIp{-`9-}QH`cojBwrGW(QK|a}M4+b2@fsARaV&o*qmm?@q zwuuK;ADHdaQ|JJLQOSunpZ>LIA7SU`f1!%eIAUt$M^zYYhIFDN?r&Nlpkxsx_^p4s z#nw3RI_$v@k}Jv{Jd5j>nbmS7T$z+T;Drjh{^qh0V)gfLJOr&9(Xs0?)HgOC5W@+f zT6Bz~QUKw4#SQ!S&Ol(^q*r&vY+H+plhBHUI;N(r6(8>irD0fwOTKyE!n@yOSK~IP zn^R4Gc~eq?d$TgFUhxMPXuoO2#KZvaoR!qKx3`D>>x0)i!BgLx{zsGod*!Om*Zy4= z+KQJ;jaTFM%t$rKcR5xWzUj)sWi;c2sR_;<;G&cf?pS|DPzXs_STTuP+TFDaD}-Q zBF$ym>~A6(#!f5*p%go6$^lbU-=9Zo%QO-s`LVMiRWZskK@@=DJa4f_2U1@7lai8x zkmOwwwWABeNi#tT=Hqwx%JT9O$oO~Wj>8>JLzDQsL{XM|Evn@K$Jp5(Fa(3~zK{pk zp4$tnyHI!mxxbLh1ST1y3kC{pI(6P7k+{0 zdpu$i5+D}C)rvPSVyJKMbP323+$D%syr30)A{L@hoFJ;RE8n4dVD1((9moeNH$ zaBFyNDRjq1PX1{a>8ckfI^eU#OnsO4Wu#Sr;sWB1};JnK1~T6FGj(*A5W{OS*aUHY0*kvr57$_6mknza@} z0u30M*gNGQgxuZP5kh7R4A&ptjZefNa2Pl*8{LhoY>lAyqxX6CQ3VP3Ea87LHpoA` zpxO$y>zY+$pWnM0QRjxYBWARsI5A~wGxIzsFt}GVpu7xexL&<(0?r7D)0vx>1F85F2bpTfp|fthtD!$Wukq)az0 zKD7qdrk|i0=9H|zm+_6kg(-~Aa{r^R`6S}wIty-kT1G`?!;A7ZSH(5@jTV~>=4tOI zS$aP4n|+FdB-#(?49bm_DnW~P(JCxqZE30G_3IyCqTidNY+yjmZsbA4btCgwE8+xA z@2$~U*EW>#@B(Vz{1XKV6ea`-*$Ej0NxeI<==CD_Qbt|@4^Pwuc-(8fk`71(MuC|? z7ei;OmT7d876STzf&_-dqvXWw)G&q{+_U+cP;-Hef!69l3+J=rHRnaJrP!`5j=TAx z>_3RiT=Ab&7Juy&G_jqR7ssbyWSFcG-6(G>TOn)4*Lob4#gn+V@l#_CE{==E_^z&h zfLVp7(%IQLSDOx_XMP1){YALiiEdJLk_eRprC;RjAj_P2=d;7Xl?+A;gzrA2vOVH++;;+X=+;JalEkcA}W!8juRvud$UV* zV`-BMvbSm)GM`1%c;xIvpBp~jI)2R7ST`aRWE0hz)0~6hPH0X2m-=($B)5$+Z#@&= zx-d7d#EO8~lF^gsc#M*a+04D3HIaznFSSCI;sep6!$Sy}h57kwfOf=N|2@^NiHW1_ z^O*JZpt^6`BG zX3@DOc2Zs5!e)lIJHfC$2E^)=t1L*GI&ty=} z=*5Npr=r&5)O7l*eAU>~(C8zgdYzaC(^3Ip3&YvFslpR{Retfm4?|OP66KId2c!pq zyk>L|cvS-OTi+df+Im&@S{+Kr_su)AfHItS*5o=>4lMDoW5NFcwRd^R$2&e(DXw9} z(?f7B?go;v#>3SFHg}I~VBbKlEFuDzSdv}&#f3P%A>%T*LBJOkE80Ki)m?Xb?t|Z1 zL!%ELC~)q_EZ3CPJI8Cjp0lU=xO_qH&OOVizrA;fC(qL6UADWal5W};@z?@yG8&nb zbOjnnGx$=;dN36O6KQ||I_@flv=?(2E~4~LjYNNa_Ttbn@lyat3v38ES z-Z&_fgWXsWASD2$Vh2Xv0ZvzfDW^|gp!+}Dwe-!Mp+S)qiMc{QW043+WiZC|wX2>f zIBuaP9SZW7!#NpW?qr49P*YRS4m;Ux0Z9^U2vVxNSHpBkx2&H#XfpW*hzW7dXj3VA zuqUGj!iK)ol>@tF$h1Df*5A>g0L%<5|GL}*0b7l>;AT`3rs`5u?*Fj8r>Cc9VIn5| zcm^u{Y}6@qU>2Qrm54StHeTz-j~;cNzyDvL0%=GH-hF<>%6e6&k5n98;UU;-(FHiq zKD&$>T1n6q4l6Ae?64VopZt4 zS8O{Vo0zCje=2%O2?_-qt*Gki>je-C*7;XBj_Fo(Y0J}1w$5x01{6f%!Hi)QD8s}` zs~i$JMZ|;>B6(FFHYAXVh99s!nGY?M*j)F;bq$lA1T+X0UaHxq)8MoTo?K(;?@>I% z&#$sI*7M(IIYKWjyG|HseNN0yPMe+#Adf>=5NNSSQIy-!@+AFw{tzTzZ_2Wrdakv# zhs`f7emeA0OSLcoZzj_4Sh#}aHRsqJsfA9UT=9IzUuh&Uav5E8dPDa*=4_}*0vaL5lo=2Rm5se%%&sa?3bfODo| zqsisd(yC%&k`}6N4LiFqbQZmO1yOzUXUl?J**2U~8j?t#?M*Vi-|m-?lX+QEJHzS> zs8a@_rkAP$+}Do2VX1kTQmlNq_xmp_Y7yqhPF=Eh3`T>*>=KHJi78SqNhyp=$BHvH zA)5mtcFwxTiga@4tFKh@XC~Z4&l6yycrF{AGE)CieW#&mkGSUY=n?OWUn(Wd4&pDZ zuQV2)eUvyn6v}o@v%zoPopT|%#X9waT<-wna#>=I;xK=AU3tbCKbo#F-pcr4AB5GS z;|`-7;G9zW6T<%H#i8?fVoy2cNNKz|jH8&6MV_YBf>-&z>}kl-7!}ox$OYr8d3;1z zaT3BqkNvM|nwZ3gYO%lgYUxD6z5_Cfkued(AuM(ic|tH(QuZsbW~@%Arp(&SA36Mi zm6Q6aQ1yPx`+Kw+TZ$FVlE~G>JO0w#ph18Z6Tn~sLc+rG%Sb3teC+Jc*)!1m{$iv> z^1^CF)_O%cV>=UZszKy@}mnug)$(g3aY;CtzO0asD#_2aLMDL0 z!E(=Hi4A!c+tHR@MBLo3W|f|GF;|xJdT!;~RaLoq8z^&>~Si>ZeGU z`r*4l8W%&}NSmRQhhzTWqYP>nzLgvXPiQbLp0csAVGagNd@t4hohpn`ETWoymUAGd)!gx4_cszEItNJ!zgY(GP-WkJ_@tx~E}VwLO4*6=icMcrZsO zF>k+K8pc9B>VF%I{_^7KgfS;Vf}6@u8jmPGGcyy39FCzL6(kITtuj+1qaQ&*7bc9& z`Y}Q^=WD-V)zTgrRz8@VHpun1$ysg(Wg$rs1lU~@b_T*f@{F?e zMYDZ@CWxYvlI`kz#L^^(QHP`dmhv;`X7N_FbH_p8#bEhUZH#Io>Xz}EGbM~W5+FXDR+q|Lsch};z6oU#e14c1G;a;yavN7#T~6?W^wqA{7&EuW zH)7wu<;z5aZx0t)JqZ4pnZ)$oxwkriBkALtI$xLBg3fk~|4S z%t3Xj6~72Wlxcngh|CtXF@%MpfvK z4;LCFAAQF~bh;6#wtC0=hDjzg!``);P8sCdmts}XX(yIN9; zy79*>$JoVl?k)sIRfvd)OixcoVtkxB?W8f*1*uMZvGh9ruYdP?Grn)6)f~oo+DJ>9 zROsONafN?Jme;vpncO>~j;iKQK4bb`lFm=ETEFOrblcn8frks!4Pm!@6;In!RjSg` zsOLvR(ZF=TH-LF}s*+(WGGNhgz(eJfFKtbyYzwmZlzp~MOz15c-&ubNQcZxe0GaFh ztT#{vX5%2)zH{$hS7Pr?JA9QY3#B9UM`{BbD%MGE@l~u4o5A6%b8f>EDkMXgw4f{n zOKf9fBZwjXz&?E+H&K7RrVxQP)6SH*h-R}ssc@Ki|L+YMZM(hhfA2-1QEk6F}Te@I01U?pGuF=-k(e0U_ zcFaZplZ#OO0_#gWIkqF4#iR5*)_WsnMuM>w=6VCdJ-xkUV*26R>)D26(lD^jwElUY z-)G}_V@mPv+vYGZ*E-2sb$S&Qz{BNRiy%G`Cv4OeU( zQ{rZFSlH!))Grb;p$jz^ky)&c#a9#L7PSa|zSO@b_ zbEBa34TtI7+Y2Y_Lj9r24S`RK$IoMKGuQ}Ooxs~BgosuBBtI;rt?)@(d!)m;tP9;0Rx>v8}N~j#K@cz0G@ABwt zJtzL%uOd}H>cL<&M|w2r9wfj(nE6DJS`u&pLu2>v4=7qWaI)MpEgvPZ(Y4a6%( zP0hQs4QM^wej&G2_VzdX(XraE=yQzld)M^NMe4lg3vbPTlJz!knBq z8-jy#EI7@YssjA{HriEL^tOG1@cbY(x*%2SB5M1cLE_qJ_>Dd68tE<^`({qU!s!44nZF)`wo_`NqLZ~(PL)QCD0Ps&wU5P1Y8P0nHmH?xV(-D6_t zZ_2!2&Ud}1tLw8LXJz&PEM5~$ORz$kOlE5fu!7B%C$aPrvz#X9`>PZM zyFPDS!BHC?47dTp6`)t;F8u&c|5C=x8PHD{*ZlQ~l$H2QNUJqBHEjVn2eU9%D?~N& z{d+`ZJ}k96UvuQ;zDBpycsWhHy{FGDzA096_e_Pv?fJnHKIdE?>tcBFW|w}wHml|{ zH!DTYZJr4k>6o5n1r2NkYsg0fX$JW7gJsO?elw%zXxRT49@`pra6=4aNs#7L?|_oO zL;nV(C>NwM>ifMjeEf?hVJ)2Pu$^6n<*@6@N-H&*=E?;`MG==#O)KNh{3k-#n#~J1 zg7;O&$7eJu@+J~I#x+fPgoHIuE+ftZez}Gtm;)6~r`zP|P<85XIovL*om)D<_~U@* zDrDZ}jaY4~69jsh&MdCg7*{V#&YWqL7P~?n3*~%veikUmH%E9dQ&v{?q1Al#4y5el zosB?}8(H~)i3-kbtO@hbw6rkQfVajp4Uew+erKIiyR&*XU&{ABVe89NA}FRnygq^k z^XXGb=$L;)T!baR*DkN^*SGKW@7*t0t~H&`)^+LrWVo_i{IW2gdJf0^Ksa1tY23tZ znnOaT-R@<1OB&^m4h6e{?{ys_D+zA$LwGC~wJ%(q-;h(?7%IfAke)XWMFZc%t}4LC z$Bwv;GNKqr^OYN_dW6g|yX$&~@CKC^zE)MwTNcSb^FhDxkV+eq^03h@w-rgw?zU80 zRi&^Gjn*>khS$_;Qgn7bkQ>@+Pa;y2IY>V$_vZ){-Kx`9Kpbv+o`A0aY=XXh^G5uD z1kc6q%FTLS!;?Mk#=ZMi=r8SvfgXG}=_pqX3m!%W28$O{@b*GQ0DCrAmCK#`l?L`4 z@wC@*GV#wYT&1Lh!s&IiW3plOZ zqWCeVFg49e@RcVRR^z^dKn_kTOX_Q6U+ZM8BQ-6pnn@2-pZqrs%{I{k+EUloO^7_M zuN4ZD>S)V6c~0Ne8j&t$#pLc?q$Nsy#Ju1s?4Wf^Gwy!PAmeIf3$-%*mzm>n{s+ys>Yn@$)6MajP|RyE)(`zQlTP?$)*YW&Tkb>{@( z+sOUU@%jFKuGlBeZxN-xK6iE1xXv(rXz)EFQ}5|{?C;$fD<~;k@b1btN|%TJ?1M01 zqpk+!lQnpQj`iRJRCdic{D@1Q()NQaU@ijv;AT?i-gEgHzN(sYf@O1DS-swlbW{bw3`Sa>ONP#kYK23GIG&1MNZz zs-EkxgkcZ;mfH8Ino^-EZ{&5_n07~qisjr~TDgdgQnZ?%ln3KXke)&#D`bbSijzmh zsMR7t_3wGosIwqk+w8*nnHTfAr>3UBk`~8-)2?$t0btLG1Y zs>JOUte@V;?|k8gPqke-et{jkf40d*^)8eEh zqR2$A{$avGjhqT0wh@RKk~R|2E6!IRI|g9~0WP8GkvgkGs#4Xizu9%_O>Ko1< z$rn);#~Ewg3QE?tww$8gfZ80+JWE?S04gZRHV5+LuJxGK-mLRgesGJhs^M^55w4o9 zd;9B?-^a%#A?u}=^^ohp3Xq?_Izlobf)h0kSmw8%Bbg6i8(WR_;1DC;>$TCE(XI(N z^*z97k-&`#jyE}bp>eA}pyyDxd;0s&4mQ~tXyeQ=rEPm`pr?nOzL%A=P{hxw0z5{z zxV*PHZR4^nINacgM@NTPeuo1{mca!3b*C_1gSQJ9qOqvx>Tb^6epJsdmtv4S&=!ym z{V7{kspFj{fpMCg3xygP7jEyc{VGR(dyywl{#BqjAjMp-uG(Z`@X#)PvHaC35%H9K0JV zV68irEm-vO{h_^SK#)TR>YHDq&|3Fb-1;`-U!fygBVBQO@ zn3`f2L=RjIzxerw4*dnwr#@HGvUCs4FTF_JCz-&e8SE+BXB-%yL)YA_j&v~KN6>*7 z?xO!Jw8xfTpyG$xAhod&>>jD<3B5f=AN**qJP{yc^n0MRHDgJG%z2|`Mt&e@CA_@e zWhb{~^G7!NYpL2h4%S02xRNCML4qp$d^P9-i;<2_MN#NcJ-d{Krm8KHhI31%5yn-h zor+SP$h4m)xbhy?SnWf*SB78pKq}6G&p?P|Yej|NV-cXc5P#zLZq@u+TA*8+^nu12 z`qYrNubCT+XM^nyf%|Zs`G|;$Cj0eGa?HJLZK*mEfP|J^u#WLFXek$;Y%^h`xVM_b zG-)O90o!8SSm|{V+<1kUP z47&(Vbs`R^S$)7o0}lyyi|wIkaI90<4`G2hGwP8HQ#u0+Elz%>6q(?tBQFIy~bq0smR&@r&86~)D4vpVx}e;^E?-)(p08&}7JS%r_+UhGjP8g8EL z&BcIW4^2O)jPss)z>>y+;EO2ElR(%R?(B;^A^P*=k!fqrZ@4}p0>6!p+AO6#x^v0N zpC%UOnhrr4YL%9>hY)QEs(%1S$aL1!G~eMdE8Zg*;R$Av4Zs*X9x&4TJ==N({)%r4 z3WkA}QM^I?;#0uUGM9ZawV>JHSOG@|W^lEf4a82&+T@l}9#Syq^w$gE*|i$3&qL}v z?Ho(da4r$r8kpw~|6X4wzBLD;;jcZfD=1ivpl+ymV66WTus(DJ2@kdMCi9M+aqZ?!$D{qwII#-U=n5{xk4>U+a2Empan&Z2wBy zSDDfJUAHP@PXE;tu7nL0p`&-SV;Ej!9(4}b)|OIV80+68i$5RT)HO6ZWy?*Q>Z&r` zL^#3^_KJ!SfObxQxq(0^s;d*6LBc>v*_q1|jrZkwfX`N#%T7I$qX-=azXY1e;j!qy z^4N)YawvX-r3xesLep20X{odW0cC)Xu;p;Fvs=TU)1WLLDN!4Lig~WW$14GfF5O9^ z2IT|VjO(_^CJH%vn=%r$O*!&{$6--EqcJ~MknbHHdwM?C{)6!;=O^;}J9{w!W0#;< zjGNF4IHBs$7#KbWp-UX}5$9_knB;+UdE<(kBk6vL1QX(;Sl-kGj~!e)urbi$ zzX?C}GPX;r^+i!*1T3Wim3~M9%pD|vDj--B`dV>G7$V2l=;`NKs4|m-Dz@ ze7LTJw!NtlI5PHV!fi=RY9~hR|J%JHj%ZK+-%(O zQjqd5JRQZuLu!K|#7wE#tn4>$(j@*u!A}X@K|w*#QGL6vOXlrY@qNGO?UEgnq}zpn zGbmLxZINBPJUkB2M9dC+H0b&AB}kW6HlR5xi(XzpRyGi5_rC^X%|4!>a;3PeCXxgB zun@(AKZDze$p@SN8|51DmcaTDu%M2%kY@ncwFC!WWTTOPN};^Ug+M^&5%E5gkdzdy z0QFJStrvA}MkYj`Li8>{4uPJwwzi52Ze@0K(FhT^Hul^+$5EoY{8!n@WS~tkMV##5 z>~(fVUH^h<GxQa2jfhFfR_8s>kB$xSBiYSUJGF{X23`@gnV>eY zc;{pBvQ!o0bI_R$>tU_ArTk>%NCRBUy~UH zL2Uh03Y5Q3DHoPdI&CRnV9M6Rh1MLh!o6&e*bUCG6GLG0*v`*VM-1+++4NAlB$~ znQ9>4jw{SW!{U5Yw9VuGyHLO!ymirf)Ye#8>GoAR9!_Hs+Dmp=sO24Cejq3HThbh( z^z0XK%Zl8B&9B?*FewWCkvusK{*P!4EANcYd8vb=5mnkk`HWC&>u1r@HU7tgrAnWv9n#N%Oq#C6}?F4AlIC2CADm7d9q&6)sye+lSb_A zsk9-hJ|6x1pOwWR$Q1k#A`#es>1S^!$1}yMDzIevKVOo!@g1$WqVs@e(ROwihsp27 z-S-E?X<70V76EP#vW?|V%ZU8WSV|UV53NPQLb$U;Gcz(iCIZ9}XQX#W3uCO^#)_0Z zO{>PX#sxKvlE-zbpOWF6KG{pDA$M2s=_@>uH%taTCHBUTZ?311Ql9w&xPxlDCRbopyFywj$A{_sAk7XA7z_>al@c?849?^2VhOo%xD7|FGV5F=sW{K2a+ zZuNsF7%U@TQ>O|TqArg&Fcj3=4_QGddF10vaLk1f8!qv#rz;wXHNnpN{Z-lG?Fflq z{w|M5rJv_|5t88xK^T$BhcahRFJncz-LH<()$g_08^bm;Lhooma;lt%>9*1;^=%9b z7h>~qk)IrCXLd*^@A1@_VOI3(6UGoUsgFab%|S?AYGCCu;L!Uz4=Sy~AYxf9_T@K# zFJ6=G`-p~TA0?J4s)chMIs&8=n zXRIL=V+Og7YJV};p>=>VP{5|nORTEJ6g|)qPkiVUBIB=#i6JaFfrW{`_?Fz!rB6IO zF8xD|-)|s! z%o!tv-T&2%^;pz?Ok||}WZgB;Mbx5Ooh&VbOJ5;Xfz8~tC58d9;UpUh6@)I$dL>1@ zcoDiedbsVkPPVBp)iotoLM(@RI#3Rj&eh=a0?+H(H{F-53eP#Y@O4)Yk6fT*i$D!U z8uLa)cf(x?stR__Pr2f|r|nwjdYY)FH%m zYB{{yQbFp7;v7c&EaiVhU0JP6lEc-4H&5bPAXCx!ny%EVuddr2B5L$3U$61Nr$|ms z%kSKnjZ3|s9{aYuEG|+0`<9^Ykl_>eR(ZON(XG0-a!dRdQIQXa6d?q=fGs01s;mBa zdu5?HUO*w@x`D4H$p@V?&MtViv2qVqzjMH-d{SZ&HCP|$3CE^AirHPXiU+AZ0w_um zB44)YjEaMIrax*V4VcstWtMllZM+ott!#hIWoBqRw4-!P;{sW%Z>XKqaXJTG$(y&g zF+$kA2A*fFEabcn^K0hW;gdbWo;N<%NMw1~8T9K!mTWPbLiWSnhVFH}@(hDBinXzC z*^iyzg zUi6C>PT$`#x|B3_^Y+)rRBv!7)x975@$|FiD03(WOx$|xWZ#c&{`5On?AJ7*-jz%D zRNsx9qT}>+zP)7YRzRUrhCLOD zbdz82y1vLtd|!Wqg75!+*vkYo_vM(GZXXwU%rhJ|*hTE7U{BL23dAWV@mJl7ztDoed@Jh6NdHU8y>~^0|Et+3tAqwbKx?n`}F8i<oSmIP(+C9mwQDJZ z2YT>*#hX10yi=E3=xgZ!Jxi%`)`_!}l4m--}R6>2dEy?L^2&Dk#2k;*LaCqA!= z_Gp@<-KjE9Pv;ssKK%FO6R_(76kOCLCJ$TIsX6f9;r_~|`0ek)>+MK}3y&=ykWUb$ z0a6D1pyH{KA)Mq-@{kFBw*Q)b>R9nqUXlOb%Y>{M!>u6crdZ=h*SyG->Z$H#X~~UE zo!TT8`G6sIo(I4)Xk}sfTqF6(5-1=x$^X3)YWG%4C~bDVACy@YGUsjUzE??gD^V11 zqGo+ysEwy489NVo)6(xZz{3UBJ*rD(2{kb9l}16TsT=0tB%o^od@cLqwKY=8mzuwx zvHX3mQKbib>HU^FPb_~`T~RtUlGIm2I#32ew=ZwvG*P3bP$mr@!U)bJ&?q8eVpn%@ zE!Cj9jp89#%k%V;rP1@zAcs=6YrWFF$uY+BkjkIV&s_;lij~L}cm|TEI)F0Nv%Oav zWB~u(2!x?G=7kNCqftv@`$+laA?urv9Sh0=Ur)HM|t)6BFPKF|^+)&cd7V9ggGCo&k&SqCXf ztCMX?1%}1s+V!BY0?ZNo7sUtoDE|9hj^%mA31Z!Szunb|3dG-X1C%fVxK!jsGqPjI zMHyybf8-H?Zhwj5?_|*K1bCUo_jDg7q=`e*;3A1t$XfcZ=a_I*lrnZpgT;qF(A&8$ z%%*|iK!^zqI0y^Fe*aY6EuzX!e>}mx@|!Up8xR=)fu1fq$&Cq zRTB89cASD#R@7jH%#{iD;tB$8V{kAv^oG+TKG4$&7w~IxsJ{c;y|lk;%0LEKX*BBD z^N~5u&1iD$5nTZvA25lll}1Af`*8C*UrPZ4eYWfKlSW$1i_LVE3#*?`UdWKdeE88H zxF2=nBF>8pxO48O`U2ZfQ>zGDY@pfS*w~1i064d^_7;X@g*;+%7acx>cZxSh0eFDD zHz^vXuDV??xBxo?D5!3>#e+%fN=nvp3h8ATskwhPkRB^7^1(KsaC>BkJic)<4T`rA z^vN(U(&V2ksHpr}1-JR{z|rLetqFx(ND>R;JcLXgdoaj&9&7_(6gH}cBE_{n>cd{n zW+?Evk*l{=$CMIJ*nVB}M9u`L7neWee!l^mXOL3`T_Jy7rq+L_fUunO=Xag7`|<`< zO0x9~Cv!9s?0+L7OBR$Ka%7rKni&3`((=_d(cnvze3jlC3dfwJv4O2R@`(TSWh2>p zT4oVJOBT~5ymoA8@lM)kQ11`TjjD8dJh_hR{Ql2>Oj{xH=g#qu@VmK2S z3)uIRlz>U7f(g*)Ki-*2i;2~)tvO9V`x4qc+ZDOEs8{3fMU|#!POGqGfrId_%Z=eQ z`Z!+?^Cu*d|JUOCALTr?6e0zGWhJ&Gt-07{C12TrNiw!{(p zhH&)xBbRU@vg!}7B@OZ=r0pU9PK)Yzd7jB#q2Y(uT*NOY zl`L>9yt_Gi6J}IUe@2keYh$md*hmxU~hi8Rz<*RaLgF_tbJ)&nG=M( zg8maD`T+TI9Ef!Y48{SRbbkaxROwIfOq;JG$*Y66{0TaW0O6H z<}^xtRn47=`-aM=C#eZ#|7wG{^R*;uAGs~WpLI|-cvHMq!x?E^Ta?w(6cy~o86F#> zAZvgfBzs0!6;nKfiZFyQZ}K8m%@wv*cHr67dwBM#)z#boDv?&TO z7i@fcQS5MenDp-77iVD!s-ybg4epf8Fwj#N5Zhm3SiA}C2hqF0-EnLz=LzF*#Q)>!Eu*SjyRcDEK@^Z~1c5~<2oeH< zbR#8-lprD9ph!zM!lJuN0hMl$QlwitB}KaH%;kQ+^PO?_pFM_~y<9xc9dlmu3cUKV zlE=YDObaI;Nl3*^vO}52Wljf2M&2~xLiGC86cJAvRciwSZzfPebOJ?-xU*qucmJgs zQ8&hgOy_D|IXB%g>OyQsFzCu*>+~@LRg~n!$kP)sotvA>51rEh&=7r1rZ8j~_=&1W znsEuv{5iw*8vpTHb=S#aNj@A-2D@IzbsCg)S67#t%XB{_*W;@+13&H-6~oIBkGmatzyWt11@^+!ge__|a5c`8Z#+ftM1vT|IwX=)r<6Bhxi$dqevTqR5<>j_uLE6^(xps3Po zdTCfQ({O3HDU_ZmG|hXF=Yyg8Cz$aeN-|;ohlHT^mz0#m5KD#ZA52%7nx`aUDox&j z!C09r1J^KC(;c&ANi>?>Ub{`RD!_-f1%YLp{CelQPqD*yYCEJCuA$tm3;! z!1wU?pbZnr%?|o7K?$rA#+8)x4?DYA9>^DHkbB)xj9MVBCyLd^B(HSUno}KwEVNG!TmiOM^@npxQe;w+S%dudR)!EQ*x$+gU*QCXk%~x2 zBa-c+mGiP$%39a?x_LnoGqj8lbD<1Rr#Bf4eEBTCX)bHQBD}XI@tAna{ z>xqk$Hg>z~eoItUAm-mW0%7T=(rT7|Wh>G02Db7EZPn++MSe6_BOf+tm+sNyMkR?+ zw)wI1Whs%6l7>@o=Ys94nAp&|Etv8{twaw~;^iJ#^>@C5RWT(5ekcGB5oS^n8$hQa z@c#}z?P6kLaDP-$U2PL(=;$V2Nk0wp&~{XpqoEUc$l+H(Dx0dKxU*YU2?W)hM47x- zI-AtH(G{e7%CpZoH}bq6M9q0)V`e}N1x*O*KqCw{vb_aIp)5|r6bEBnP(eG-;ZzRE zk8APhx7O_thm95Cev7%rlvv0%rj;n*&3!5?JUijt59XrjsA~NJW;>Y)b4c6?=l4PVC8h3q;)dflw_N9Xef{i~yql|GtgUt$U&Oe?l7j`Rl*;rEY6wYn1W3pT5W3{fmM&a@Af#7SWJu#VMGM-bYIW8a&q97>! zJ*k;Xrmo`3+7(uzu0dFUdLXaH=CnCJbHb3tABSQ9jL|A>3i2{wp34Ir2r)5@NbZ5L z&J{izf2PukirgX$Fm*&Xhw0>pd>|0U#l*a9=nn(6lHVap7h!JN0E((1FnPyQJ(UO=s=RJ2 z$(m#lURzz1pX|cT8Zp^sY*rr;sw;Cd%$07B)PO)Z1i1Z=93fuaTvGCy6?=q^UXZ>B zabkP#YblA8y;;^S&v^d^4_WEZ$v&m0B?Fa@uetOyZEu8WHk-R21zWk<)9|HWlnbUN zWSMk+sGDzS6eiM1%c~3OVf~toyS2k65Xsm9w$#N<53;-dmAK}T-e|!+ZW__j0 zbeVm|2Con>Gbc=tK}0-xN`}8K>-;%};=_qJRFt5JEr)XKZm!44Nnf;H^Q`R^}NfiK_w}AJ}a^9Je+vK~aJK zD)#s;Id;@#T0+8&mI)ZqmzfXC>*+NrQQe6iSq=|z56ckw>LGJ(Kvq`iQZdQWMWU)O ztw)8aD=~CamEK;S&Fm^kOmf@d2SAN32sN$!@4)(51ClXHs$p1=WdkQNBpplwl(5`e zKxPqn81pIvC=FuT*gz5Rl@@W{L1d16`-a}o57^G1k(ZJ@xunK$FJm-3u1+5rZ7nbO zQt0cq$e4PG))8p*;*N9gZl`F;gW$zZo5$&-m|qMYZb*u9x%LoZcc5}*S9AJyx-Sp2Zl7V$wJ*yDrviY4E6@0lXUVp3A3Ae9ROCt6!oE};gYh%4O- z!{>>$`)N|OX=a{F_v0^T>wc85Nxr7VVg1Lb=g8}{L#;tXkp8|txyZP^fvW>r2srJU zHuxG|Dk!X<(qax47Zu^^Jvg$5=p(%9j_SJ?5qSRko6ebw|#nIeWUj z*!UT}Q6^P>#@I4+B}Nm!Q9DQLb}uyT$R%T}%)Pxm;Qi$n6fh!GWV&1U4=ZOpe5N;t z@2$zP-wG&oJGLjsuB|b^L;{u{@D_j%p-iZ6NefRwFGl_{lf|@)rDwyiF}Jk*t+)pa z!R(w@kF?(*U%1?ktL7QzE&eNzzxig1F>cX;?tY-wge(VZ7OUrxP9fQ#64*-%fzb=x zwF|PkAlgY>1g10^H_R<1hDM?T-A=nEFnvDT9O>s?_eAeld-3M|;=2>vi#mq7Hszg+ zxVNBE$_Wj{;UobWrP$Djp@D{C|A6e3h~Hr*LWF;Y2*=m+NZi--D|eVDNa_|ZA=3sZ z7{FiH(PO>4y>UrW^7U)MV5J1Zl?htmv(1xxP|*R)ZL5*Zbv??mEF^zyNA26un0@J% zpbEBxzl+<)9fV_EEApYru}#h`32RKA>uz6|eI%2T=mMWY_UPV9KgdoO7mdcXR?a;j zu?XyWW?!vIWrHbT2q`-2TU)yAutI)U>j zV#q&L8zeaSFeNaBT5TB_%<3TdB;*?**6V0u;$V0lVyb#o6KQ`t<@bNi8?h3T|fgzvX(8ab^UcOjy zow-H&Qdbv-!v_FLb@whGyxMy6{<7ng0*q2YEVRk759e2}-v0K2Oza3V4PIOrraG

=$adFvi^eoK*)pHF}`!flxNN#}d4Lcz($2eR#SL6{ z9X&G`BN66}UppI-8edNn)Wd!NQdaP+x$W}?Bwi%@{`@Y?gBD%_l(==%bWJ`k9I0OK zJip$3v*mI{3cRFg0%RF09fAs_P`NuNlMa&f?vpoC!*4lDsj(L1=1Ku2(ZE2Mkuju_ zgpx3s*CskLvgQqO%!VhB$QIl%ss=zNk@M-3eXtkYSK4A?3*b^INp;lAinzC7o@ZZ6 z{?sDoZd|$ou!bKs3`^`c9Qha3P_prHAuWgpP$W-!AD?+U( zGwTJxP$yhM4}>Wc1i2T1_JLQ`*VniEdb0Ed%J<#o`#JDK!0Fme->}OVD~MqV>C@L; zhIaR!tYD^@9LAI1*XcN>wN<)Nmv9;J$lro5{0LU!*}s(lfad0nKCo=bM~z-^a-ijN zC7Ywv`xr+5xIPO{hVtb%h*Fpk5ugPw*1oKlxNi4&6npS(9`u|g@lU`0kpH>KkYMKZqF#HquL=m!AO~&nf5f^)cu9spU+s(fvj@}PS{Tj=>Fvk~ z>s^U#rMl@8OP?PR6EVHa&JXP()FZI6mM|78WOOxd^J_1ju(Iwv$j1+V&T51rxs@d) znFQa(O;rgZA~DfVawTcU@u#M9G|QzPf6``9dX900^R^o6xAX;_dQ(z8GM+u(vO%dD zh+5^VX=f(ROd*J|xlv|G+?nj#%`2KF7|m=al?cps;MIX}Mz^Uc1uYfgWx!%&;FrZ% zi6QPXB2-mk3-(EK9XKFR?RA&KDFF1($OljH-g#l=NI}j(-f#ujSq*HKaJX~fshf#* z#<&ZKRqrj3##9`1x=R$6Bheuo6pNO?*6i&q@EZ#aS>c47xD8WjO zS)p2%$(|^x6l!wE{3ngwEw-r}L~Go#2WF)YKpl*TJn=t0cPf@gM6ZAx2t59+#`$pt zINo_pO-$H_qgvb8JaC1%HTnJzbxek z1**+G1} zm*=stuj$cMY-}ue#gUVdL5|KdakGsn7A}4HblD1fff4`BvX5xeHtBH!Q{qTILX}#- zGTE<`LOX%Leez**Fwq(%g!agg68U%<74?nWPy@O3TZ%$J)ez}eXR%IdsHII{rFr8H zw%JFktpr1Nl1D*9l@-QS(a-TS9DKD)70@?thf{FdK#%-8cG&+Lh3xEmDrKb~%e^P- zhpbQQ2l?r6ne{&`@E+e(H>6B`sIlLP$5&j|?0Wr(<^A>g=GzcrLU5`D`@se=45RP( z!SwF&HOxA?bChWWGiwvCQ=(6sSy`!@qA}`G*!h|lp@|K zUa>EE#Luxv0{iuEpq?vtcW%A06?-y;A5zCbPoE1X6O4JoKYU=@6OENaJs-L*r+(Z2 zAdO2O?_Dbf!58Zj{v74U>-K`1T)>!!m#AK?&TE~Ici|*$zyH-+HThBgt6)aRZitk zYWr|1Wh5GG*{HJ}_NaFP{B?74XKn2#=O+c8{epU;Eh^`U)gi|j|C7HR3u!hZ`S^zp zi@$%%3ps^tltKm^Bh$Tmso5{9H&Rd@-J=Z7NPjNvzY=Z$I{#!JViP{A@GK(fD?Q{_xAUZ z&v6=y-~r?iQDM`s8E9=?@_GQQcm0OfWsCjt{-*0O7VA@H46CAK`K6aICJF?I+{ zEwZc;q^K~c5#18?z}$pFDKpLT{H)*p`cBz<>r*-r5g(|ZL6GC*$|m?CkhaNdB(SRH z{jW9*Z0tfXGyozuC<@lRG}tD)-&~=KhGr1JvkUM%GP_97GF$# z|G#Tsgf%b=+&h@MRsh?AhUbRrJ$`%9z4(#pcRvEh4K~itlwGR8UnI5J1iK!lz>5#* z_O>mj6j1diE(=8p~l~wzfOq$7MB=@30x4MKm0J;##e;q+6Y{@aNCv)u5sRf25hmi%^WWS6RkUZwuSpVIqOFQ6;|-Jr=RG{QpP|1 z(aE&DvWoNVYg15oA`A1>jle82SeeCL)4v16CqD+os=VnZ!oVHp4}}{Gob%x&71uu}5K3P*Z>;5jbHg>go`1 z#_WKyF(o-|Q`ZDR>9g zz6yB|%Qp(DhrQJt)uiIPhILvqQ+8ih*(@i2H4iZ@lP)RS|I)CzzOlMnpOsFN^Ur%o zp}9`^pWyovEX&%y<-j|k2Tdx}?(n61g8%TI9m_|%yyGZKPKb^N7BZ2teI3+)c&}h? z=V=PzUZ$owu;CcX4m+_)<*RMt?1Qjw^D1Gmt!QXj)s(IpddNC_i2dg{71ar}4M=~1 zkltd0M%^VASa<3euswk*9h}%eBTRf6cIFy}?wR<$FgKSCO^1`_;ltkV-~E;>(zz`N z(BrHPC*iX-9F{cI8g{;_xz9Mva8*6obhU;W%S059*kb9BVF_gd=m@53_D03i zLG0Yh0M|Vfj&PQnrm@k(Zx!4iV!BMmZ=yI~$0qG#bd>{XRGOQI@@V6wQiBTjXsS5= zO?ZFN(EW1(y@GM{xtH;RSs3O>F^14y7?qF#T%RF zO5VjItIzsW=3|>HA02%yD6cM=Ip9?5R%7%-_&qVhISCOcKv{EH?Yp;+0iyl_83hIF zxu(X<%oh+2b{c?q$+t{ie~!f_(R$DXO1k>`qs~5cQRO_&flL3Hn@vmQl+Z&DCO%9_ zJTOR*kpXTwIHdtJw=;Xmd7cKV7yyR0)o>02C9lMwF`xF4I@It%Ehm( ztwB->m;}S5173#d5A>R%>v}0OWW-blp?-m^hNz?!KLek8d3_tnlPYkI&6PTp6ksGW zMR`+ES&5B_8LdSVM=ePJ@}yTiAMq(Ei#2#i3OwTSjuzvDa`r{#(y7tyIo1Pn^3gTI ziAH@@5x?yiTO>6&!e-d1&WsCBnabgS6=8#jcEMXL^aOl1`o>bC&P>Zs`L`uUWO8Jp z@Jzp`a#xX-EHN!70q_ij@yMKndDMouv-gZdn1SM%DvU&Osw8as<}Y9FH$_4}&Sub{ zy}^|Q|Nh^PB{|sHcaM(fOQ6RCFe&E=)&Kk%W}sRCm~}Bf`z#?P6C7Z1C*3X>AXuAURfWr1XOs`G|z5Wh%tZ@PzwL{`uuT?bAK1+pdh#qvRXar-%C`(c9y2u^G5iN_iPlk(__~q;_&SeJ2C*qgy>q=RLEu9326x4S#^sh^*rOOn==1T!=0O8s8igJ_R_V+=& zWh|WedwZDzV!6PJe1G!4lcuFTT|&oY6D}3N%fRrqW*4q@6H`+R54rDZ7o1^EFBUXi zfEU&#o|z)#&M6{dIH68}c3q<-wzp6-;Z{H;l#Og(#`Mb~;m6M6OQ(YaIJAD4O1FxS z6eR4pqZVSmi=Y63YT_>reOU#C2AJirX^PXO1M545eg>J&LqfQvZC{m>K{&L^&jeo- zkyhFzEa$*%lX|iA`%7K0w6ic59}~;74JW2Jg3;H%+i_s}0YdhxF)i+@cdk^btuKJe z0*75^XD9GR2i^hSSgo{-KL@N>^`RL+pr3{3V1Ev5kA*Zf(@nNGyI@M3yUPGlOSHn7 z3mkOCRDr5#9>R#<;+uCVZN2}xNm#e$;m)O&n!8vE$m6mqJGv(A{W|??Eq$LNH|1hF z>e_l_!j1&Wa4Xizm3NIcpSQL*3HCyIk32`rEi7Or+UtDrxXfahUM1GwAB8%e5s_vf z+A93!5WNU;)y(hRyO+-nv_kygc)YA7D7p!CI`yNvs{Nx|-n+kk&3G;<$qyv8vrJSG z5fPPrK%Basr*2Ulw^h15=ED8d#uMK!im4^1o?TxrIPo4FE?1V87>%zz`|Ar)RJ#O0 zT0%lXLhEx5Mj=Gi()#k(-8FLs>LI$oRAw>X+*w)4T^vjOW~VGC=jiGxbr&HgTQge( zGEFbyBDPskQX#QwBf5HpCvr0>I}mLUZO3t+Dg3aLygY?TI|eUkAm6v`;&tRdle z*ZXogepc%-NJ#qUum5g5E8VF%N8T{V_l3!HeiC9Ku3>oEybG~rcO9HVmVYUTN|)$! z6E>ASsKUkBbnV+vzy0dnUwXRswUbt}qZk_X->?c6f1J&1p*~j@cKj;v>-6!{lAjYh5@cOtTEo!#?{tpdNI^IZk+MQy4gjX>tbNyc>VY9qsKE zKxq%B`_K^o9o)SKazQ%`N}Z{vOyqUxnX-2^wGz6jyHA)B84xbjK2BgXflinH#DgSO z2xxA=U@=WeE{*h!EnX#=R}fU?d~;uN&Xp(+-@DDS!F8KLI~I`+Skka~`DdaGg|Bdd za=pZY%Y0{h=3jRu8Y`Th`S9fo7^Z}l?f^v1Y&COw?#eC%VPSsBX8@SD)ZTD;ra z`eTB$>Nd{h@`ZSS1+H?at)r*7(>c@rQxaC$p0klbiw|krKbs?7=o`0eVAs}>D4h4A zzqd$t^T7Rh^rOGn0duKDs-_JRc5ViBjE=?)XPO4e!LiG$Ob6o(lFTJ=8On_F{4@Im z#5C~lpKDwgU?pC;tCj~eJ*UrGf=?gnBoSmY^|i(4?EKF+npgzLrm`P^L8#H7wDLGo zJof#XHv!$#W3Kt%R!Y96pZB1vvtJ(kCdM)=!c_>q08B^c4flvMT#QN+4=iiYhgOy9M6ZoN6z5%ry6ql$gUTZTH>7F=s%f+EFbsOeZMv$Ht4Gdrg)#bti^ z9D;&V(7j59Q$SXszVeM&A-5fU*g6$vIO3g<8j`PBL}A;y1w1aJ8L(A}O8T5dxcQy# z8RtjWcrMnZsIh-r2Sy{vuD9_|z@GjzS ztOO^(azZnn<#lO4lpQTs3yjvTYlQbcISn+-EfXiY8}=sVG&L&B&pv3XgF0+}7M^4D zl4KsM_E+hx)lVN~l-z|fflW|NWgtiSmW9JNIe(Y&fdMj*{Z7^$|Cuw)DXdpHQ2T~m z)|ZUt3lgc2VQ9(FTA`M#^Wz~rHuiJDy}j-Jf+NfKb?QaaVA&UQG)wb{2Tp13`mnfhbCi&I?2xiEqr^a@Odj9%N$Hgx{IE9wi^ zTChJL)gP@O=8CRFw+b_J3Cz0v{-HT4h-o^UN`HYSns4NfKBJsv5@10J*+V;d3aFm- z@6)4wL7|oz*8o^SWprl64LM3KGwgJR;<%dyF&-&~0e5690=B(5<)oZNObbU}gRwa@ z^L>e2!Bz!jzeKci!I8t?%a2!UZ+4z~T!_E*aH#zKbXGqZcoBY#jKq6635u9*&owXy zpW%27UUNu|>rh+yATszIg=~S~Kw-VNd?=bslftd`@y9?PBWuNz;o(vTIEbIqfsKS$dPOEJOy8M zaNviceP^sFS*|K{;F1=xRPM~bdfa(Fy7GaX*^l|eRVOn#80Suq=05Z<3zgv85yFoU zCk)ELumN_)S;@Dn&uT857c{B;J^wa`61`kI&Zk{fPTxifFmS~*6su)1CgIi2h12fo zy&R*Y&vw=_MNbW-z6?3Z*ZhP=ke;1A^5h64xA3~-{XhY1_ay~gXmAPmg`hmz7%Rdm znX0sS%6lKz5_x_#llUz~c8%Tm$jH`s34&4HeDM@b?Be>vtMvo-45AI?N*Az;&}W2}wo78Hu~0w~>cu&liUXHrd(&=q0_JoVRcp^4fA49K$a^CQgqz z1ZB>wtzy{KwwtY(;@TTvj&0kz<88Ag?t5)+Yv!s`Ygs28= zF8L2Z$%Gd?Tk#6Fl}+y}(b3eUV>2&KnIbd& zf?>Ubx&3W(x)=>>p@S6k+cNlk@W@NyeyVgz303DrVc?+@a1ODO@AAGjO3ZNei+)%} zvrcvVxo=@%rcVc=y1|S&jERvE@=p9O)YqV+1U~Ki&`<|QN55)7>h5pG1O*00;biFk z?CA;Uov%tvB;Tu?pLlof>YgEt@3Vt_J;X3}@7SEXYzORmo@yUMw!>Q&AO@8%YrDPp zZ{@%_c)2cd*ZX>TP#D@8yxdDuHIpCN#(JqqR}W|kwu-*dv(XfU|F-)ZaY2zn*22R} z!}kRaBxtPcocd+3bG?$Lc-gmaKS+7y-$hdcj#PKy#K}i4D;!E#I(SF=V-U`s2>wH$ zsNMeu1=Dd=%;S%~OHVfhR^R*Z@R*pG+ICDV)%}S49R?~oe~Ujpr|&FQ6ZQ0Do1QQ- zqIZQ@5iueizSZPh!&9QvM|4gy$(p>l8q}sDL+}iFM=u(Fi3g|TH}F_$PwjaAjlNO6p`nSc4qXJd=w*x7%C_noLX$BYmn;Nirg{;#-7{f z(Gun--ssj)N)8qLgVIuYTwB?2lI>+m5PeeI62(!Zd>O>GjfcJvf*L)qac-b{JNKSa znw7P;SX0gvZSXVB{mQ}a2rhpsB zxp3-l59PcI7`rF&Gw&Qj?$(zB@#&pc__0eyNUfQ&v0uqC5GajDfUB|AP+G zoYJx~F_y&sm>hAjjipewWc)IrIEA`&bUIR`OexOVkIRce`p3%jiU7@Ue!&)!f1Pb7 z_&Iy{ZrkZY@H54Bde;JHw!z&eZw`a0yzi$?_w-<2X#&pkXMAF6O2mCXz}jhVQIf!* zwVD)3H<&pvVlm`EnFIY9uzdNaLEE71*2???8VjSD^TTzTA{VV7^=OO{1$!^?i2F8N zNVZ5qDEw#v7QuD*EMMoZ{;uJ}8^{&yM&9o3{v=aewqBcsR|( zxOp-)Q*g+s(HOsmY=IucQ|fu@$d5KXat?mXqJ_Xp67Tf%9`UzqFlc;(@a>jcETp@F z?_5TyZSqB=+5j87lXdBFWgUAaXs%H z9#F_#MGBDfMSf=_e=&K=Sj*4P6CGHIaJw|!5@BJnns4^i)zw7|tFcVY`RXp$bwYJa zQwZSMb#QeJ8e@hb<*5lgf2}dsdmU6ufe{h72M?_=j)dvxz$x%vQ<8{JQ{k@JHIRLm zz0RR@0(kQ0=D>hTzR&zr6peJ)aRf27(RvK`?V14E);e5r&YQ1h5SqQJ9XUt2a`ELt z?J~btTxCWrevTswuO);B1=wq@m*XTk4a*+Xl+Idh^8YqA;~bxuIBbP>=X_;NyA`*^ zXwt(_Ul*{9^tFug>MIKN?HJoj)Z7`Skr*rMP)6l&qt`IfL8*RB{gCosQj$FdoE{Q) zlZ31=N10%KrEVc3BRAK%ZB1}+L>)w&d!n;#c|$kZzms?D(I>}-zx-tGw%{o^XJ@Q~ z4(7qQw#q&f9CWJt0ExOYC!XRPatpa*f4X@a)7#5IfdiIf~jObjt#$l8-nfHzTw6oQ#n zS!9yq2yuP|n#@{h>e0l!gs|BceedAZ=HzpYb))1D3KWC zq})Ul+1OfKMC6|q9H_xt&HM#$LZi>MFf1`==X#6uznS&8b~CURi1_cPo_6`UI@@P9nlX3bp;rL)bSx>E_1gH4Cs)0E zb8sxK!f>S()mJs(gEkBSAeTMk`Tc_X;C5mfsN4o+05IKt;LbQL?vl^hT3GyobTmap zMHiR9&~=ES5~VL|q+FGz@LT-RQhDwCMibd>m@<`QZs>(&w()O6hHtABShfo*kY!L#q& z`*@(~xtXA7G}+%+_k%|hsY{cFLrBE|9(_Ow7*C=44vzT+Nra07&~80&kCW8ghkzMS z-}s!Z=LOvCb7!v+uS?%n?iY6!35`B=7IBc%e5>YFUpP>Ay!DH)@Eiy!P?be9D12;I z?}QcjO~JyVz!jod{xMQOC)zjm&plT7lN(e#oCnTCpUm60uNCEb5}xH-Gf=o~VhYFf z7D9?co7mpLwp!-m{w$`ysF?C|HghRQuZ%q%5vf<|Rh+%{Z ztluY%P4CTADsDXgl0vbMM|pb-XJ4}g_!xw`mnqDadJ!ln(H2RQ151bPJ0|3FL?j|w1L*^Yi zU43n>Z**@@51yR&(N?0buNd@HzkfeoO*K;tBF#bp^L;9Zd>H(TezGOj%jPrEtYJAamMzC@MLr`VqugYqa6N1tVE-JHI5 zKHl!#V)${1l{$c6FUYeVbU`Bu8cLcx(K6^|d1B){t3&BvMq#@a@L*y zYE*$H=2c?I{FG^+h$mvVLn!rB@&Qg;p<$MIU z9v&+Da2DXptyIiALd-8HZJ}@{Bqi?BEQ%1 zMM7Qh?{*w{PQ0*AOK^ls$7PlmpcJJ$fGo<%tH&Ze6Ky@*=6sNZk?)Gv8^9S~(0Fx} z+3sDjFg~`re{KJyh0ca?U$!nIbtlb?oo7pFnW%xz}r` zDHYE1H}AkuY3*(?(J&jK7CbDuN~xHN+M(IXru!gm{2mG;(CeqI5QIeV0kZcnR-1X^ zBccr(UL%b)LS=@md(1#(D0W=I_4&M$k$e!^ty{xaYyRb$nKS=C5=}ud`o)td;`rH1wTD8%ZSdr&18AGLHVf? zJL`#l^D7A%@_w5xnkI^tT?n|lbfcvK#~q3y`~PfuuK5v1 zP=Ml8c~4^>u!KxPIf5>Sz=-I;K;_gPG+{ZjQ0Oixj6jm1vIwYB*TgsEe7i?~M4!K~ zwN*0sl3`bZEwebq6ie`Lp0arEYN2}XT#@O+yk$>kC(<#P;L3WlfYbYTg2-M#lRK?; zw-JaelJ!7F3p|+WYH}ZYQ&Uqf*A26>vdq^Hf+5Xl1CeHQk2%>cFQ2nZ1rmtIK-CUL z&5ezXl1JO~b7YSg_RCj@gu$nKw8T(^_vo4u+M zTJm!hggM8Dhn1zJ1A~LA>gq7ZOkKR$_TMOrh3J1rS*pS`0x-)e$oT)wvOeQFS2f3f z>Bh&$M@oOBQ!b&URSoDB)S{phaDW16zGQ?!9x>bcH>(PJiJ%m0D;m^;h>U_l&O`Wr zM_-sZ=zt+T!+gaOS#2_D0f`V7y!!`g&;-MvX1yd{C{vmKf5%YiF`4|-*qEQ~oC0#W`yFM*G_+`HbZ7eek}m(=w6g&-OuN_*)Rr^d?{_~kX}ARI z$|ak`-`m#vqA+doKAv$)OH1=RCTGE}>G6QeeRI0D!jqW+Gf;#ZmsawoZNRkEo7bL3 zOY&{WgF=yucqAl8Apa8-5Rmht9QD)UA~*kv4Y6%G_<&$7W2QSy+E^Q5?n<-fi;wa z=X&wil4}jE%?m%zHa%v&aJrpuFok%i_tk`?W9jJ|TQeB2T2fKCGW4X+N)v|y=lEk% zk{>Z(C@`)nLkz1pTFnsE4yj;w~>&L=Dg2s5Pibp;>p%sTRs3%oN;e^L6HIf z+xL}~+)x(Hh|?=3K>}B?k?~PZzqT7pmkw)}R};KmG0|xL+I5ig(>@M+^Wq&!8~~^3 zrvX|g+G3vd8ez;4WzqNJC_*j%xVfX=8D>*Qnqzy`DVHTxx&yZWi{V&m1pF|)y|1!c zJ3lHg*AV@j92xm`%1P5e>AIW6<#y7!1X|qdmDnznyrJOUI@j&6uRE}z zMXpY0VzQ$N%gV|?xeYRSvDUB~%6W&N7kcw|BeSSJA%RqX0yw+WFEfQtug?E;clR@@ zJXUs?sr%V|i}b4F>3Qb(*g_IUFrF4%AL>p-MK3iesSg>ti`Uuffa9#+esHr}7x0B7lYlF`3}qPxF!vDOLzLL@CkA zK{YOqx&Eks$l6PRBbRVEEwSV1g|3CyIcz|&&P9>4O+azwybo4zsE?pE%s@zzrZdSm zy%)ZtPE)onDI3D!OP|{~&o#9iO!NicG7YG1072q9aGLIj@bgbJH)kMH!Rt5#MPBX3 z@#v!6`hlap=*t*~S)8L>=m9_#-q;fwnja%08q(5eFJ>|q!9cr2H9N>s*9~Za+Q*<} zqOsioLc3XS67WABVB;QeVb&$$;p9&-%$;E|Tjr@KOPsU^Gr+X6{xph)dhDvL=(ET9 z<;oS``7OT|E@t2tT_tFhR|{YpP2y}zRlB6@ad0Ji5&7`UHn|Fji>BOHjNMeV8p~vYL<% zU;_!0&D(N-&Oc^+EF_d-#j2lT=zW?4OW|2~ezNx)n#gCy=7-qluL})r%S~9Ed!@*YDP5|VakL+S`MS>jL{B+s% z>879Yi-pSgR6-vp74}>LICazYc3^URe7jk&JL;*i?S+olb@Qg(H#G=wizC-x$9g&A z<>^_|{<#orL+Bwi0>0WrGjSfo3j(Ptec{FA^}b+u2wkpMZpZCP3#zpSOq6NJ88#Dqb)b zQy@S0Ib6dhvVw6XQU8-zVqQz_%klT1tC|1 z$-JD7TV_i1mf;Woik+bkyNnq5_4F?k&*6mO6FM0Zwf5Qy;6L0WWiub%pnloKsaTyIQbRYs=^dwqs1Hpl@Hmb;CG z^_!xb@e~+d`o1kj$)o1(JiZ5duP@LTC?bm))n&!)!b_HqQ1QB=cFMqkH? z>p~3pP9w06cI#j`?RkWeOUjH?K1Jnofv-!xHVq~-x`?@G{}3rWV~u);d>NdoJezEV z>d~?Sni#z|)A1>hUMQT`YPr?4bLP&R?UA=;!u7CSi)Ay}o1%5Vv5lb(mxv$YS;OdH z9+aaWKGSHeJJ|9j82+&1z0bYP`D}l?{CgmJXmmxnHeFdPv|bS(KG^=3Jxp7+!;IW- zC~|sX*4iu^4tIu%rD76yXh?`k`_;_Mj97RZms?9f+|)1YiumpCFwMQ&8}-h<4V(3v#`z z1^m_TBt#e2*^$!c6Uj&Yf$SyYPxx=)wDS6V&3%3~Ank8w&$+!{X8VWI--6276i5CG z4HZj>MFNv_I2{FL{CJ+kj~|?0;XksSlGwQ269jpGL!C&8R5{0Y@*1*gTt$|R0Vi5k zWtwCAowo#SyKgjrNeri~)6|TQA3h9cJcZG)^;a$YFvi3$771E3={tEs`lDpT#D8bp z=Ag$VO43+)ii#CtKl2UT0ku022D2+GoPvgnUN_icXAU;EuJPsY!9Fq6E;!j1BHJJ_ zP9-O%WL^>=g@rEyE?0&guaJq`;&vQd*`*{LQ0Z-rxtVu9ICyv1KZQ0-p8FPkSe75Xgesnc7@@?0etQOEfTW^|1S%1+iqQ=ZrN)S+GJ`zd+?}G& z#o|qKot^9L#Ty%+eV$#~jy4+pYQeJCdOcI5$G7hb97)axCl}C17;Wy(8fa?X3cmeX z5CBDBj>GnU1v@3oSt-(FbF;uhQ`MmV&&oCA8JXy+xM?#q)U6VUY#%fpp9b>rG1n|G2Rzb*s>9;d>x$ zXJv0cuwaEdU+#Esv_!UpRND7@OAt+hi81FRoDUbDN(;{{*|86TFZ+NOI6O>vo4+(m z(*WC-m^ekhyMlJ4l&XN-{BZA{F8ck%`6gZ*wvPk8W2+mdgHMR15WkVg+^`7%8 zO2s|#Cp$z!c>E7bcBGZ>qO2kvoGvqUXR}FA$1phvY*5BIemF|w@k!#3d!431L^Nxf zB%*jXB5#(_T7$A&vp68`tNUhrfkXNzH)S3@9O9_Me!GQrmBDh8k5iL^1?^g!F}@4F zsNW^LS-B~Tk&(@`n*S+vxM0?unFA?nisid}L#2~rZWL+S4Btx&d4b(==IrSHK@tXE z|009v!14AauxY1~j`lpS9iTnf@V>t|^+Gyf50@o24yOGR+V|nR`ks^eCE8?RsBitS zS1{?WaMN7`a3XtCG;aIFdZEPPuIUtP!hHAUPF<_{}aep z#=UJ_u4iBGRJrx5cY1#^OB324;Ab41|QJ$$H3co|E&IxH3{K2I#H!~jW z@!>C`@~exu`=o1RXxeq%Nb*x^%y2CKwndhMx}bWVln>5v&XX9wy6WwLzCu}lCA}x+ z9gn|TWL-BO_gb?ipGHLLAzsd#<&t@-G-mdRIL&qdVCb4V8c$Pr4YMO%k(S20VfAz3eOnZ#mZ&zDaFR|g3 z$mYncNn%vluN8JLH%zc(a~~=pe;?lI2N;;5p?);=YsM4W(@QawO4(fD1`U$n!<3gV z%`Ca9tMNajic)0K``_amjOe?u@PBxF%c!incVAdKrMtVOySuwXkS^(zmPWd}JEcKN z8cFFcX`~S;sWW~4JKwR-{&L26)eq}lu9$1~oYyZv3rv(1DARU4XzGf}NXpL4tWT4qk1g;=k@m z`Q3)+#^-kZ!e5IZ5!6@ZKFQ`BdQ1VTKkDn(r)zB-^$ylG{V#yxCHk(#AC^udBnSeZFIY zklQ=!Mwo0#Ij)W`)zCnHRkyz&-B!OMem>cM{HsH=cb(Qwt2dZItk$Z-SE!tGtqU%+ zyYDQZpfnZbx<=-CAZHA_*tdHL6j8^{-YLneuzK-J&+xwCG439wOg&I5JoNT-ybe*Vbo3%Xup@Fj2 zw&p#&G`6PT2eOfc_hF)mVEzPlepI;LP%V=ro*2W)eFe(V^G)%)o2V>~JjF^b&Qd?8 z`tx)j2dCH8h`!fo&Ne_y8@lr)Q!THqW^s@BsH=REYnZPT({6d~eRDLz`lmr(^Z3*> zkti=mG7;Ej+wD@_J^_#hixf1YeXEyLmFEVYKH!(+N5Gb+jBeHv-5d0Jg5K{`?|8 z24;g1?tddYyP;=G>UUI!LUww*-;)S$v$=Z!4d9^5uxJ|CAR%v{5b>fJNze#*oLCpW zVKV!t1BA1oT{lZS-#i08C4hw!D&l}#{3LYxbX{l1b$F68;pHbdc&I-q{R{( zl|bO^^71meTMU~0Msdo6)1B{TbJ2k+A*-=(K8N%mJ<|(`APa1y_stV4qviBj@r5b3 z-T&Zj<6~@XfB#%`DfpBjHFZeo$$aI!Enb;pSYG?qJArHv&m*^(x+3L z1(9;`@kkf0!`I2z-E@X)#RZe>^oBgiTZZXwuN8T}k6tT*CFb$4j@17V;-;C+j4mXc z^x(tOehhG3``C`8!cK4fE@QW&m3#z+v#+0)?|zGThE(N4qS(sI@!?Es_x%s?n+dRy zM<}zdVKOEukHClH3Jp=C)qk*TES=g%DG%hnPz zHm0R^1bXnJV`E_VN?`*GBkoiAV)@b+nQj{4JA(JLPBmcrAZgK8C<6?pKv0^PA6Rc@ zSUf?+`R-Gci`80P4o zNY#&m($UsK>vxRZRHnqPM!ZgiQ z%8>sMYw=7zqS<%lYBaRDBTOr|Kq$imi|yTE10(w7c)-z5$fhhv*5`d@ zf~jC<6YYV*Y={U@)-}{JHN>(b$v}8Duvo zr%7=Nop5A?8!9t?rT2kKh0%Bo6Y3EGJfT+t9{S0n+s5a9{tTU@@`ZND+x6?&c#F z0GL&DR|ki&z|L%mu!D0wncV?J%_ zY})kBRT6m9r&% zCo#H*hU1dJy3_8BRt%vZQDe~v89A(Q?vj5$9@l~DkV zCPux%OarF*`1JFSR2NWHT3SJtFV9c$yMQ5EPo3dGRL=}K`=ON;Zbd!^bsXo;j(6pr z^!_^NezC|hcioydCf}9yKJA^q9;i_6u^{y23shBho1Hs}i@PM2qPZWQzmuKDmBim< zsZh~%kdv!tU?kk2#@1RUd8lXaDkXXZ$-3(tA;zV@PXVY0*cH*kuaO#6!zcb-bNv0h9U`o0sZ#Ni{ftH#E ze~x>K-ik?1MDmU2jgnSb6Xp+i>7!Zz$xASCxzza(%_5k8R_sk{pI{|7GrTxt9`9k1 zm1idhYeS0l6KUqTcFk?YCI&(7*IJuv43prvoc7x4Ag9P+{>i$^HFEW5giAS~o9||0 zLq8wVT#EjdIGVVVSTNk9B4qq!uB$36?@J|IhzEe1vL*p|vepyD1<%G_F@tj)ULe>| z{_8EmbL$xjd`{TDv`ZHOCnY>?JK%L{FfTVefom8-Dn;7qOCi0|L6SXd8z<1%#Pd3% zT-@fMn3bC1bjNIwRY9PdbgSR$1BorFMn1nM5=;uE1`fu0X8R+)H2s#Gf`Xa0=Tfar zl%T76ae{1zN*{`)jg>-!tc{hji(!IC5Jg{qmogy8T>+1g5S5bZ)+?A(>;#Vz=Hd%8 zPEE$s5Fpo-AVa-f9r03=K zAZs3gQ2q2vV0LmbH?SNS4hawM4{R!>B?1crlpw?F^YO*iL0N%|rlu*bp**pbz`|eK zpt5O~HMtfl$wfE0t~B(RnLXlzwVXB{p{t!m4p>GIk{ zl%PWX5)@A9x%1DajPkvcD?d1H=J|)Q>l=O< zV`F2WG%_>$zEczn6BCM*RSDWqG02VD z^pB!A;Uh5roR_Jo(XHS9wKo>O1y~sF=No=N=`ieo(&;$3DamJw?sUG-PM-Gs390x= zBH2Tde`*z7PfkSz&i#C$W_9OV^DTt5BM*VOaWj7v`3PM3gMnig7ZB%41eP8m)e)`V zHswNM*iQ}{5)sulV)0hoGILUyJFbhn% zz{v2qun>n3tKr(qrdFbhJ{mcT9^EDntplfoI05aqhonPDm4J7LvuXer)je;(bEl}g z4)AMlZ9|Ft?Wd7A37!P)uTD1_* zQ5dXn?*&Iv|8WEZUiI6fSXdQE6QH4eR2jyGoNre@h#V{_6wg5#s?&5FM`eTj*At~X zG>;y!v-NS2!A&r;x~J8}nccQJyg_8}|G)ej{S`2Y*wgtM(Za#p)y>7i#QvW{M^oF^ zh-_S3WUOTW9104uXtb2A`gV`F9JVJG8c=U`^zB4g)ZW#;B3 z4$vAj;nc3LL*tq{@$p1?sIRAShc>hZx zq;2fnEL>Qm?M&P(BrVJw%|XdnI9Rz^lW}vg{ge2AFU}9y&4%`lgnWt5BxEmQq$M5B zW%o0VQQ3jK%lPA4u)~H8@3-1P^Q5!*|x>JBCbYXdzZj zw%^xhc3jiTxaETv->0KmX_S#)I>#;X{8+}ZGcb)m9+%$*c!^|n-OGLq@Orrl*Sp{E zPVHD-_HFLCTP+)WRgj6m7q)mHKnd(%kX>$_%^BX!vtHC6&~PrMP=2Uhad*Rnr%=y~jGex8={; z;fH%Q%pn9uD!3<}#}lXG5%*U`5XuaPC8wS$99IE)D~;9u){1V5ZdaQ&c4t?!{YT!@ z<2=VPdTegxxtmjqs2-0z=z^6sNLsHi?Z)3AC$274(n(E6YoZZ;W?n6{4L>;ZM7i8@ zun^(8x*jEK4fgkc`-VZJSm>u|;|j6$aDlSF6Rq9Cjl?mQ**+O;h<4^&=t8f2N82)y zY%W{z-U2*^VR-!1mI!xv$5QmsXH|(UGc-qkyKju|G~2CVb-Lb+RnjZ?Sv)se=v6AX z9)!g%?C0?n3jFcZ%aHif#ph!p!BzIMF&^QW#ewq@JNr+w}B-e-*Lf zG=Xi6Ln1}XplRJH*+2ka1&Je}YUx*^Wm~K12gA80^Rg3tWR+(x@))@`y+hpS>CtI( z99(<1oXSz%qr(lW1?-LmQF=muZIy$TP?DUE4LcnX$r$m`LgJcl2W9hSCvI<|N{Ms* z(S=%>34{a}y#u-M-H)Hj=S%0eo`in(wnspD5$KI3l{wS$vtaU6t7E(Sxsfcc9uRK4 ziz?*qWd9O*3cW?%bP3GDk-5&ho3(D>K?(4 zwPsO`iJ>Q9csI8TY8y%6k#B5v7-e(@(*o=l-+yp$H0xk{;nFgAl#^t)Bo`uOKj0iRg|(-JNS!>Z+#{EhS!TJNMRJpn$v zvD*Dx|56+J#G=)tF5wL{QlS|Zca}kaPYE3+Gqjk6b_e*YI72tlQQabfT)$Rlh>wB( zi!8m*SLC*Zl_^%H{yBNyKIM_5c9@mZnHHGhBII-FJ~`W@ZJ#r~T3Ik`V3GiEexyoy$n+V8V4 z?HZ#DcV`!?*&}ZT!(&zW#c+`#STf$3}DjG5U$aU%F#G2&R@JG_=K=L zG1o>?$91!9WkS`s9a>fk1tkf&-J>>a-x*Tn+Km7XAL)roEQH$S8Ha zH@GK%suMKWAUf=NLj1Uw7^;S_8Jon!k4wv?<)51V$V((=SL1khg^jcDi&=^DQx!8J zaY33N^nD-o=sLV>JG83Jo1&G8Yj2(1ILy`dfKWei)JyNBmEV1{>S4|kR!Rr8vE+9C zz1EZc3X0K?OogUBfsfXWjw;cVXjnEpY;OE;6MgC(?(a)Dg+JN?V@0#=Kkbt`^2BdB zY!;T}Zuo!l;GJ39eyAU=Sv4e6bL;C!$x%uRM;ZK3M$Fg-nV8iQ-%KLkZCpYY#fc!M z6wK4sTB&Tg%u^9>qCDeT&v7UU@|itlCN{O=n>yB+t8c+#zM1#<^NW$#QS9 zLfEd?WOmx0uz^luH?YNm!WG(N4IABT?RXR$jZ>Jh5u&BAyFFZ~C09L*+Uz;jDxDgK z`k3vMn6S#R^=|g4;P+n94nqIaqyu`fy#@t~)r{h=O4^?T`^_akTDAwt&uK!GP~oft z0)Fgi2CFs;p=!|+CA+{UqN%SC(zG2E9&2JLLKG2g?M;irl`*XR#FuT`n009o-tAS^ z6tO|j2>M#Vw^+f2s6brxh6VkeVmd==9+}$cI^#xa@y2D{yKr-}@e^8ue?{_Zt$sox zAzu3jw789&>iHFKy|9II`?(zi;+zSwP7~*){EE>^Rl}iVM)OqpmgFVWV%^rZm5sW- zcytDPoG+t+#Twj2m5GQ@1+pt5iD|eVlkLZz8Op0_I*}YyTusviB}uO>AcDz#`7M(a z{y2A0l-&)-BJIQHFt1G8YE>uGR1iztu4j0B=Bx_l-cxj|u$dGzBS>(@aBZPYHiFX1_taEL8l!~EKOXQ;BWf9#hhO=>va zOK{cyv***?!zLVOt0`_m*mAIKlm~p;KuxL}(zyq30jpH`nD_x5#@W@}=-E|>I;{2wM`DQ)U$g3suX`PWk+v8S7a z(WZ*Fm+yrVZ&ownwc^f9D9Iz&>>`F^A=qosxUUhK=dCb3!c-ENT5H3jb?Ho6XT^KF z(hh|}UR!z{sFFA0XxZ%Cs5<};JNnPF?~ zJ9{%kR3_G@kd7sl%1cVDn@eI*8n?v60UTaeC*F<} zVtn22&Kz=eJ0o6&nuN%Q-=<6Xh`{>%(b}2&b1>-8ZBXFng19sW@jUzaD@B%vNDC5! zZYJtyX*oUJbH1AJGEDDxHP)yz4%+A@&0R#3`u)(>Xl(LQU3gGLb};$2NoGkj(pA#z zFBrp398yV(EA9-#&nX#gpO40C{iULJR+MulJZ#ZD5~OsG3)TG57n);h`v~q|1v$7t zE&rB{970b0>ecx=;2|#$cI3a&QHocRnQErAtG(<%{ zo7~8@leK7@(=&8#ns7Ah=kFGAV>Hy7n^#pk@i&Qad8bEUP)?!0d{M62!WhaliekMt zJ00%JHEPWB>wPCg*^X$`^{-EZD?Oa8I19bmCKmHcWXFlgB3j1t-^Y!|cAJ^ z(CE#A2+dVyir;A|I+9v$et7GI^ma}&@4FUZ135LdmnpJ?y|M}Qc4Tsm<6cm(JKegP zY+Uct>=b_-N@`K&zRy%|VKPEs<@C|rj_mg4NiMw(Q-8F1;pqF0sw=wp5%T>i1hEtRKu3pO0oLkAz8gWV)9;03_T(Jb3dm5z}ddPDm?W-2ULQ#$& z9~#M!H|i#er=9LXZ(X$`69R-rqii=ve8d<(URp(@EhNjYKre3U^Z|_&H1| zuRr$Ln8$qXH_=l_lWFSvWVjoojz(u2N3dq2*&vSb`7 z1hd8=j|D}YB07_|(puLE>2QvvhC<|=nAFvzc%@8QID)!-bOMCgp{Byn7l{f}#qzS% zY!F%&1;A*U+2+8XYzWtk_6Q}++TD-)5Skw^Hv?NecIt^^DyJ^NQ$1i<*UiR@9*&$D zrfKLM%HSiqlO+Eo=9l$vPVe5h{^&|BGZs#Fa*%q3!11_uL!o-=g)nub!c zyFDAM)eZ{d;E2g0Wi$-0FB1OB*>)30$M$nfTw! z%=b0w@WExXTfG}dc`Bi%4ZrA@qPDIoke%KNV@KleYuK#!7LS%l;9tbs+_}+#C!zE% zAmyQ0rw(OlM&%Z+#;;D2*UiBiTOtY)cBzdhxK zo;Fb2ivss1Qa-FOXmG)RkOdRppvZnzla4cD3}*EUopEdMv2-7+2=j7O_qk?T>bL~Yw>eG9{B|@=a zj)Y2+aJ#9g9}(ZH;15+$$BySIyDbnO7^u)4lj6`Elr&vJSm|NUWSUMh(g%irkVuvt zQj_Cb^M8S&<0?tiJq?OWHgSZe{vmGup2$PCyqzdep=me@#eplGS3pC{>3yJ)8@qUa z$J?dfqAZDHl28I+c12pTw#MI7H_cO^V9yBBkEP7fpB}eFMjLi@cu(~6c?Rc}H>6V8 zV9S{pm1HfbPE^Rs-p$YTVLAU1WOytLC0SCdBqUQH#b}}B{loi7z7pY$^CS^@AufA( z$P;2N-}kWGSFg>Oz7e7Q-Z?U97JR&s)zxXu5;BNRt@9Dn?sJi4);@eB4mTgibbF4K z_z@5DB22qfMkesE(6h$YX9=T1kM=g{6`W0vnmjY_ILsdW+djw9;SMQp`3&K>I*DEl zdAj{gFSd9jq7w7`UUOr5Y6-4?etZlR7#1q&k($#$WW@Z+P{i(?IDCA8g3E))>yaru zZr0tCp5DZCBv^bAy_}scc~buVY@~8*bs=UXD=X4mZ#a|_VFld{SPysgN~PW zCAWcAEzJl_IDI}Sj5Y<`)?39ywz#|b*Vp(2xpKz$>l|{h1mh5?a_pK=^gV1NZmf52 zkcEZB;JP%Epb<& zqt%mRn|cut58P2gL#(d!5g#F@{QVNEO7;c1peM7PHuIRWm*mBeP&XAi{401L zMWTWb(+}c-p|0S!H7mBRc*bV}U6|Vtc4A~ETkxnTYsi8g(-zv~#?WFfJtvl7bon7x zN|tlh3jD5TKY3fou{lY)84omH+=8COCd%Z0GCQyQHUwkTo4%k&=2WO)Hz+KWO@{{2enCMFp(+uJDCM6~OLC8ej}!DX zq<6+!u;b~a49`Fgiyur&Rn9!!4E*S>6zu){&&Ub_d8LC6$JLA|{0HUl4G+oSc%>1` zK6-Z4iJsuw{8D7Ndu+EVRz=SGNvBJC6P+AL^9-5tvr?^&=mei03fT_-93SCxAtxoh z?w{*ux0d)NGUeWxTC3t{6K*}B!W;YRLzM!EkvIGN({8`Z7eh$Yv`T%35HP)R`RzWL zJ@8@sv##Gnz!t7?z?!Vq1@ug|Jd8+wGOSzi9ytkHMH7!g-uAvpv&T+ z6a&S^;$VwqucYZ?Yg?^w_1>4+xg6{H7|TyI1u0`>mV6iCDH>6`&X6qm6Dm%f zA(0zKN^;-sUP1CeofEr57CuKEs}1iQ@8)Q^@!pF4**-(oNN+#HuWFR48Rj1&s+dWE z;p!h~k&Y*s*D_*ZQu7|Ht=jdCN0wm(@Kv{$dEN?Q>f>Qo^K{%%xx!SWchoY$C=F+7 z70)TEwBCy_WSnlBtS`6SB`w)d?)v)D9hWGRXqR;*O~OogdBO9)=h5B@?|oQbdRu+> z$?4!yr6JLjHw`s>oQIU*j_%89ArjOE>^;U1iaZ_4cIdJdl;LxF$7p%TgDJDr^;Oe3%IQ$wHc~P_}giTzc z+@PV`bfbtB;Z2X;TiHTeC^%f#2%F8?LQ_!`;qD3MSrb~EHeo=WbmMmUu;FR$_ z4kirgh}AB>%VVR=Fd#f!8)Fg0lrZ_N%#`Q`;G$0(IP{HddVY;7 zc+>k8TZR8gtFl=j4PoX}a@`Zoj%PhewUxjE-=`e&{JPRp8`|V+^^HI+$4P;Ve3R1$ zUOxEWAY4PJ+!$($Gfo!6}Ki%2pfU(PLl#{Khj+` zjUhkaB1Ns|dX-L6`fo3*snCTTkm#=7tR)4~7@du&zOJ_Mxfywd$#S~|dfWhGt<|f^ zW%2EoI;v`0AIMQWOH30oy_r1!?1mJa3gcM;{ivgd2xyVBHOvvUBP^fUy8KfwDzs~H zg6X>LQyVHwcBW-M9I^ODk7-V(@CU=DqB}Q*qA(ce#hG*g^TTK2ynOPj*D(YOeL^|r z;=$iH>k)!dt*z!R>a7+96Qf=?it-JlI;9h&b_=CJtAAj+le3bc4#a9nEWCWbM4Y7f zEr`AcW5lgl6w*nMofz@?f!Ch=;z+JBBDOQQ%PQd#(v*@gX0zi=*8C5iSq!m-OgfF` zCKnSLW;oit?>p|oOU&;=pRxy(**QT6!t}Stjp@N?C`))dRoB~h2@kz)MQOKIU}Cat zMcD>lu#?{}4>$00Arxb~0`r4rX>XGEP1aLgFOj=Hmc*EdMVd zB<}wfLgEJTw*QD@Y5oo8>8hC8f?(c%g!iN!UF<=4&(+c0#mvGLMDYIY0&pZ}Z(?P^ z#{D<2#ReXu{_iJ1-0N=$?LSX&fD;`5^#mt4@lW9IZ!FN!!3{i28l3yLQ{beyxZ?){ za0e?HHwfUdbAn(Z4>Ql-D58>yn~Tlg{{s;mjR=!C0r7#V-Muf5@8#F=S(R{4jDC2!L@6 zR3vqB(HCA{5hB~O0^90kqb#c@(@Cetp7qR@Z>7;L-Fv8%9G1?J+WBrFk!Ua95wpqp z;2qzGRUu~=!kI_~qKO5yzsG$)YToT7`bEzbu|J5ELY(d=G((apb>(*2bUzh=q2K{; z<(IpQoXB=XO9FmdUkF-C>EZvm68)3${~m|@AItBbLSoUeF$a-Ac3xH%SqmF0Yd11> zK2{b9M>|Is4JQ*b3lNO;uraexml0jn)Cwhk|Otl-!@IjD>6cI3z)tpE-8yz39>wo3!e!ka*B)7tVY`7A>x{E&+$`CN|AI~1i zxz*NG1rB)s`yccppoEKnfB>{U0T~+@Kvz~)0@xGKg~$M;ko&6xDFdAkAAkp4D?mvC zz%Yvg*8!; z@^(N*$gJI2R8%zOB}i@^2<&I%0X0r;AZi6LSwMoWPM3+1u?oQGz6QK}{dEuEn}80% z<0=kRm;j{v!#4unvR_zd0KEA&_WAK@vfY*nTZ>phLL&I*P-J8z5M~Dkln~~X*-)Zd z0;I<)&Bs9PGu`zgFUXFQ>{GzmU7H0=r{m8Vnwstm0G+dui`qW{bWLt9wj_AB03rG> zr#n_9n-|^E4>*9dwzOEr0$B@fCuzo>c)l5+fDXv2000hz`W8mjBqgC+XM%zt>zgwp z<(t}pG7@eigSwX11aQI`9yT^2z{htk0zj5QA~&GWl?K3Q;|l52eQ5pJjUO&epNZDn zT*CzjZa_TvdzQSERvH5%BOq`e&sU{oXTQZx27DR7UekDEv6g_eVFH9FJArj55NwrM zGx^@r^AZ1{yqq8qK$L+QJdi4}1xiTF^cRV!GJyAqc6fe1+3Du#dGM8xG#1!W{bhFS z2LKgMSeC(MMFxiHBLKz(AoXtN9qE)EkOC|Iw&Y|@Km@&NY-#aOQ=1MAh!Qgf0JL#F zQ8-V&&or?B<2;b_9q1kW1YAQ?8%As`!-}S+a>w6@Z-te72qRjf`?rQ?;89K6rbxJ93#0L@>Mz?uCx2DJ!Go9f@B5 zW>Zx|piU3Au(bT1$%VA96ORcXY-6LNN5{uNY-n&`!1w7-iqAfv)dThxkUql&O3}Qp z>wpPH4!6C&g9Ec`ACTSwY}LuN0xJ`fdZ7ORP?B0(hrn$%nUaAa+1Bv3w6L&J>+g>( zDZau&45Q>C=~AFM`0(csgDbR%W}89=ux&&GDqs^E8_(o2fJ~dsW+v_T4jp?wjut3^ zWoHxZ9{{ztqr=0w^J`VLCnpAG<}ms{pcFFFE}%NizZ&gUc6L5qd#0qO0%;>q4|HlP z%8#U{I)H7Y>iuzB7z->>{wFU`C}X3#Y}=LtnwqAV`I@AnXp#7Fxx#*u;5$_t0iX#~sn>XsCB&LBl8U6k0ml#GPKfaEPHt`?0|%h=Z*FdY!^NH}`>Zsc zH6V!rl|w~E#aIB7s)2=&2YFQOuNu1ngs@Bp;P)SjfVkXOpWE2Uk+-oQ(Jh@P=jQ&B z{=sExKjHS^OGb$x=mD9X71eIJVXs1?j7~4@ftRHBl9Y}q0k(A1Rp^?c4 z5(j|E3A%{2wYQ__De-?MU7+Qal(c|MMoY{0v9Tf|HH4xUcA0$O00N2_;K&{yeMix; z>z{Xs0)cC!mX;P!j6p&~1n)2}BntF6->4}poB=&wU!OR*E|5+p8klAPA79Xi24qr# zQ}QqLvFTf-G=oD!4D|GWg--ufCz789h;n$Hx83YlfWJpjkT}3!*LKl${eDIgYr8vZ zOou8Ee5PAiz5Yb1OaClduk~khs~m95-@fXtE6Fb}Uu)B;(ugz2^9f~0%~-@o$g}O%twL#dZQ4R1lLd=T1}~2wRDV6OHNV+?}o6{~%O~7?5_* zI?js8OHD8d!;J-f-+i}5ab3znE`cCj!vBPf|KvwgAZMHhQ`Tb4*M9MtBVcH{kGT0O9FC1vFr7Zcp%zjD5fM)5rvSZm_!kyDxSm2E1CX7$^ zXYT`Ei-8gS*D^x3FYYH_a4o(FIlM}+IudU)E{pE)Di>A#of7;iAz**=UH4XLho_N< z4GCAZMbkOu?qib60Am;>-B5uKmHA%sX^#G$h|INZRgtGIPEE;Nvpx-g{Bc^+5SH*& z5u(TxNclh%zU+mVlDBLB%D34?%UY_R9FtcI%=_wlBX7V&xSQ-xiwknI%UjpQtlK$! z5nOq@UvasBUb^Z}BfCu})4A<3AWhG!CN8Z#*gHt`0)tX=vdVO&J(J1*=62UaZH9pB zHM4HOMrK%#+FhvMtLfiGz0i7L{<~<$>1>nG-`Vl}p8pKeK6z&3LsYb?6hMkyE;hU> zoDP2;5O;f-Xv8s)$+RPe5HV(~$tTh(8zmBBTlu0Ot}k5@CqPhAo#kS=Ftg&lPYt*11OZAqaSSp^#gmEttl#)?wKIe-->8GZq$|N2f>aqUCUBz(=x7Xtulcs7jcQ%5A zCDeq2lhZeh;jaoWGXhL#WiM#g>1=r)y}nuEPraDXLEvCqyu zK%pF)uIV?v#RaVgTZ|WPE%@Pq2pb+-OdTpkJSZ3uT8sc@1glt6H-gN$apeq7j;PpW zNxtfDCS~%P7X3cZ7L}VgrR9`|V;&=DiMO$}PB03Q_^J4*eUXk^p0RJBA3douj{go0 zhr;-q+v;Sz#hOp>7<+zUefXkyNe>GHTC&GMlU;n0&dRpRJK-Y8ytI^-Nc>e!m*Hj% z^mHEe*bjr@lyqTLlv%R!l|IPfB(ah|6YV(6@S!N9qNA*nVPiw&U+YNCamlcjQ7Ful zcKJ=SucVQUy^46`cnRY(2dxFPnW1&THe=e!xlqz>UvZePbUyR^9@!29KNtsZ>r%8C zN6~BVT%k*rBb~VAgMI7i^iI0TiZL=&HC#j?vsFH%Soj%sT~N-t2CgwV!}CS^DM~=j#70`=xhZ3f94L(WQBW8b1+0rR zcqie~^rxMQvz84LN^ek&dJ8mltM~b#EmPu4=V4^%Ta{l)ik!X+n_$oV%2E%sZPD&$ zl>{XrmIIX_A0&8pEDfFfPh7R1##_8)8MMIv zr-i;!TME_GB%^!S;=4yngBWoY@f{M=)PHUDvh z{g3r@8D+UC3)5*FY^~#+u@V(ON^D=mJ|<58Auimeds1cwuevGFaHkoYin?gicj>-q zWJDm#bhpD4#ZpfWLWJHEWKxHBK5ey9Ubb2@8q?Wx6gbZ=L>Im|xguPWe~%41?jToa zseoR7Zh1Nib&Er2{YfT)}6NfFiP3W?WsmjIVqr zZ3#XsduQv_^pLBMYGD3O95Y>5`^|>X!&4b*c@Gcirsq1B)|>Ihi)DAak|0pI8G-(J z@_udT?c%T3qD9HsEHx!#wl?x?) zi+(&@OiFk){iji`Ty&~}HZ56VojCFN_Dlnx0gdb^msQCIK>ov$r;{?NhxNt4>zSd$jK-Q;4&v&G(`uJd&J$gW+|UJ^PdTHOgfY znHEc*6@HYmJxs(if6~X-<9;qu7D+yn1NgcTKYSyCbU(n^l{n?hh&{pF8;J33&)+ZZ zEoC(=(@hwSNZXt+ams%xE1qix14B*hMsGp#tzz53D}>a}yTAG>lY*FT1;=A0$NSSN4C$ybz%b*cJD>>QqcYal_GB_A91iTuvKfOQt)(02D* z+wR9bZYyU$oO^VhW@HFFO_lP<71@KpuOCb;Gj&K6(EmK&bOAqv{Uw7lZ~XC_oeXz$ zFrt0CZr+C}C*B~+ER zu6v-P=t~)MJdy`Lope+I;;5Q?W#`$12UC3$cH@1V&2Mr!DT_grb^RP~&2A#G)M?L2 zmiS6iAI*>$?C!y-GL}3c4ct-KPxGb`f2UMhUq!ZgQb~|x^HU0clnkz5G(B0N)=%>zf(6-6E)CDa0NqEQl-_FhWn7I^~y9EdlY$c z0zKvD!KsZ^TD3rIab09B9FVB_jmaMF7q(^!&KG<(1dHeXH#F!EODH3TK8v=;4EnLJ zC>FcA6HAVw2d9!L4>fbfhnos>nZhdn8Qjj08pb=B7yE+d9>3+C>Ne1!#mww?*@f?( zJ>9LgN5)gi&I|EY{0a2AU(yB1pxBVjTN=jt^AZ8Y7#Lbi$XDKjNX``YVZMh?AK3YY zgoMm_+SuCm^#YxgzaBeubU>nPJe%+8cu_yh41MF>H;_^X`l9#7_2)!5)G_2cV0gx3 zLJ%(+;HA1H>VczHbB5L%%J(~AGJ7#>N~nm(k${#ioqF*@{S1p^tSbhi?c0h+vDB(t zLm(^zWU91KASD4?+Yb3V2@yq|8OB)D)89?(S|LAi+xWYc`!jA8U4hdT26#W z#;Jmpb-nv#a;W~|;sR)ZT?Jq!Y@ScBy|5)rJ1Uz#a}1}MJ!8g7)6)f>#;>i`=o{#4 z@dc4^QTfsT{+w(;TUReoONtI^6mRUHDssgjTJKjWVcR<*2rj@?1lrF)-O}s&NXx{8 zoM zWMf|yn&6*k=JX76CV(cO-iadtmMpzQE~|S`|0<%psz6uh3beYby|FY#ozJ|yydQjg zpm;||N7pbB5fP~X76}TXhvp@9_?EQS`1ktb0Xs@bpD9;sf!()6k=8;r{qA7<BcGXyOX4VlMi%<|?$z0f0ZNtNecQa{D4 zf0LWFwpHr!etH#c5rY2 z(qsXE&Iw3T)z#I1RpOr=)fXQ|!Q|N_>!qoVjP2zoPdsQ+zTcieLBkHcCL-~?D zV5Zm)ZS#;Kv9$E$8QOq16N9H{%*L5z#)jo9jE}8*xo%p1p8Kv2N1|5btAVd~6`8%= z52A@|zS*TEJQ^1o8XDkc@aOj@p&B56{;8^par|;_ejeL0*qknMYk4qiw;n?Pp`BuEWb5tH(nWY$e3j zB}a@R7mIiNLG(#-iTg~zN6vE;vT33(SI|a@Sp14qMyKUo#fbu^#FDYk{CAcC$yaIa z+J5gaO?WG|IcA3-?Jv0XgH!E|pZ-<=9Elq_j`}x)X9mSYmq)ihSjDgMif4(gNC`2n zNPTF*lMYFkA`A>R0(5qF9< z>$`rhw9T4b=l!G{y}MFs$(xmWs`Ct&xM#5ROsd0TlHcBfS9c#8)_lZ~s6yJV3ydCA zc0k}UzA-LA^Rd=1*}kE14a@$_jAVHM4sjBl__5Sn{v=~3wKyy79V+ZK$4A- z)#2$U;3A>*b}xy7YJN1Rk1Tfl$B8NIU!&&sXO8%+mw~?nQV*5V$al4%k|_%Ink^^j zfQ+qkj>i-l+4)az!i(J1anv3NA!XtzMN41ZLpoO~8=E68qlrRipmWF9;~kM1{o6p+aHeGU zY)0S{GX5AIqyCK~5fKbU#MSR&f8{o4xc3C|YKdG!+nbN-B0qVsCoY+m-$8vAL27hN z_&bz!ZjwHkJzt4{oV-y4gFcFU9M+0wil!#_y8oB2yg?&)pnbFaex)1 znNyE9>J`y~hq$kcJR#g7myIIV+=nL%#8< z9-4v9w2K>@Nrhys5trCpRC<^pkb9?eCV_BSVVDvNhMaJp{!nmF?J?VnFXq z{+`%FrTt%-`5ptLzIo&PP4s=7^C3x#nH-D%zQ{Zd=%%FJ)4SK$+8c%*9NLcfb-|Nn z=_WD-*W<}|2P@B;ef~epy=7F@UHk8ggmkxbw+JfIi|%ejBo^Hvp~RAqkj4cFh)8!g zA`Jr49Rkt~(#<*(pZ|0AezEr$=jFMFW8808znXJi^O{$DzO)a+x|gC<4x0V#nmmG^ z{2Q9fd!JtNOclfW?o+2Hx2sD*?@gW@95@3dg*6_e@mV`LyE^m2R{c*c5+4xYu3JzR zT~HpD^tn{2D3;NGn>{&A}(O^D@dVXRXJDaX-0dq zT!@PcDA9I&c#1eIUN&GvQhEW$i8QoJQB!_#9h~!&1?D_ChBAZ7^1>rVvWI?5BmLO6 zpcVcneVaUkCxl*+FtuwKw0wo}grMFJdH!uV^>KPS*byhYfbaCs+e~wtI6kx3n!PM{ zt4UOgBKqizg(}!PIr!?!d>#2~tU0D7jn&Nw*3G{4sHe#Od-1eXx2EQ-c!!2_)^MA( zAOYqep-L=8M22@$+!k=l7?O|LsFJb3An(}_NK{{%KjI`{0Us0NhO&Mgg6h*@Mvnp? zN573+#+Y_BqAAJFD*INjQpx!7eO4%?A#oi-ublUTkBme#D~%yOLDzDW%F~1NI9#_Q zCF$>3O_vv6D<~TE0;?#H2OxwG2prpua58{BnOKQ25&817qV-$$^LLGFOZL;PlfcYo zor;uGOS&4^A}*Eu?^peVd*AcIGG3?^Cu1*xyjIh}IOC?R@{WZx-(^>YkE&9gXyH4X@OW`>mMlT8*C=hrGGo$_}*2&eyRxkx2rY z928MVT~rYNk)B;MS|-1H>ZkX_#jF9byhh@zNKsG4NrEn-5SOJd4rAitilXe13Due~5h2Y3ocQHd)hwXh4ScKh!Tt>|sFsIr*D!o!+4AU>XW;-(d!@N{_imALPTa7(xb z5=)=ve8J%<-|Oei0+W4&aJ1;hAD?jA+Lci3%L^6S|Mz(%1(98>eDL&QKiQY&{BWzM zTgzAb@14EJhfC*t{Wd-5Nu6#V|M%R zRgt5=_3zEft0~P&X=dF7zBV@vlb)Gi~(QytwWi+BuTB zoc_lHW6tIXEHlA(v2m1XBzTbDv!7^JcVcF7L?)VBBH?hq)M)a$-5(WVK^$J^Uwu6e z66kaOD6|zw=b?%=xV?2fmc2>z4wPV598ruZ6=lTW7S@FS;7u*oVz=z7fk>+c`>9At zNr5z0rT`G0*QL4xS;+)0{aTQ`@>vD>8<5rNqi)0h8;@}1tFB<>8=?-c{~V2L@l|NK z-iM46OAtBiRh>%Q{4@k+!XHF5nQnH>+wx06sZO(IW_-Ua43FxYLk0GXxaoKLF1=rR zvU-*t=M%_Zhj@sM)Pom4l;!XWJQ?THk$l;v)WMnQj4CjxHiT!MyNW;_^_|}fIr~&^I=$VE7G$Lhs^5WH|Qk-Mtd*11w%UV!W5{#vMbCi*W@(3bbp>W z`>+ldIphh8nL*(99FZI0Zm&l%SizOm2U>~Ge(4Pfcq(vnKAB}}%&}5tG0b^BXkVVt zjT?yqH`Q52P!T?r2L##=V|G`I`T6;M)VB8aKvw4o1TbVZ4f0}xC5_In4oJ%#5O00lPuCw|JxcYT zW%ee4!w)%V1!(_LCI5Ek=OpM;s!MPib=ym+QKtSBhV`4zE;P9|x$?`=!voxW2?@o1 z>aL$ZlM*KQZ`!DF)QYxXT<$urUg3~?BJTXwS#FScm6eq2gkP_ffIO=SZ0bIJ7sj-n z%tTx@X~sZwmdq4z%SbH!At3A*A&zv*o6k7x9*(mN3r~qAe%40)w#v84FUVJXA8*9< z;vxt21gkZ(`nO|4G2B7EBU%yqshmC8F1fH=M*weOI#!72b%KyN(gVBCfU(?c@wZ^h zJE0C7Yh6FVlvf>#e_BU7%(SQ~gmJo8kFhxDU59HC#m}eC>`~UCN5lsd1|(+cT@1T$ zD?OfkOr*-3JE#6LvwGZ;NipR6UGk|Bx$!ryE+jH4E)7Daweijv<>FvYRNZEQq95H(E7|iqDH+IzVvgFse5#A7?VxY&39)!w42PHzk-OQ@{8bBzrV z#TaG0`)MEFGiP8jMExMl{I65%bIhxUCj^^pzr~;C4N*%J)V*~^hM(x1yn%Z#hRB*~ zZadG=ai&f9kdftdsYT?le;;PZz)faSxRI8U;-pLFgFYPA zu{R}+29PLf8X9F~Wsu6mk)8t*p_gFAWI95#-mYWt%4>XsNSEzH7tr4c7P`yf#f zvh-L`NdRBR9RlaR&7zp0PeqsD1ry;43UXp@_)Co z8=$Ykrg}8_MCj=75EC1_$d?%cSp>?a!Mfj=3kCRxs43%{b#6dq_(e#a-b5!Md<$>v z0gJ+Gt?s;U!fe?JvFwtK)gH{|m}!!|3;nbjDO1k0WI30&>nKVD(XyD2D<@^+->us8 z!8t$}fr`@>P|^UOW}}(J%s5bi+3~m6HHpM(S}p1|Ff|9tJzs^82?qKxAB44UgGJ;k z7|35UrE@X-s@NEO!t~&;n771tf1sM%wzI@x?-?FOjQ(zR50Io72P2ltsp)+ju zV}dsG^YIbh-{C@Fza^E6YW11RA|tm9Z{O>hXc~~yqqy!Td~X9h3nb0ImlitT{wxm> z!a2n9S~u36R={J&*tN5z9TC|d?7Q_G$&KMGCW~=#jH7klKbe9Jdm^p z@WVlHk}qF8jd;~weF)-@dgWC+3nmrkZsaiU`F}^-sSinYngM@Z!D)(!{-u`yA!%3w zJ+)-=uphGTyC4U2GfgZ?qDFQ<*IG~3!Cg+?_;DtAP?waLm#4XT!wN=TGWtMRMSz8r zD{5C+1f6fB^c;TI*>-DNK%ZNm)vT2PoaiTW?F$Ed_7WGPfD)b0vP3<*I1{*8u;@~g zXdGPJp)W$(mp!1cgOrq1)&NauTHvdU`GT>H>*EqhI` zp{!j`sYwAjIjym&F-};dsz^Rsuk5wh+oRP!c~Ap^*1($|9qb4ljiROqU+^pRd*N#N zk~L7ke2U^hzvi{kS-aKC?Qg6}eFs zZ)(%SdiQ2clWktjK5fceWTPCf^urt3xRcX3kzJE8{Z#@$exjk+h%UBi)IOu1zuyJ9{1KU*t!!8y&3m_8%2 z&T4e!FpuyqOmDwmLPgjtgdKho`5ApK&pR}O77`F&t3%EoB9g*!GeIOSc z6$C=&jt9>7l_6-;)A_$Bi30rC=w|2zKrlr2?GLrTevVD-4RiIMk`cu&MhsjmTx!SZ zKNiUqO;e^!g=RU+gUhg4;@nRTf3d}kqq()IQFF5ZOt)bum<)|alZ%Lgh4JEDd?%tJgTenx+K#^F#5 z0w_GS(IAq&TxQo2YS|Ba1Al|j4|`~UM-Ohi+!Q>0^HZTC%puMqc8`9Ol(@i9Jj9Wrn9rcC6R?)ClF0ybBTXuE{Jj=^_`;{e@OsrIO>X#?;n z*$zH}h*_#a1jBp~Rw1R| zJWL|+0D?tB)VFKT^aN4yw3fK#5+AywRIV^O*W{J(pWk1{AXlQKqMn5}1q<*GG{Hhh zcX#9Y>FoJ*j+Stfy9kmst}p7ZXFKigRwQihI%+&v447S?xFf+$ofL%%86CjuZ^;Ms zMykYMIP$%rb>@>{-Ss!HGNDSoRL@&a1JSBoS=&1%dZiyKK9W{1S{8l#G%d~M>T_M# z{uKz6Y=YEmWEX*F)_KuUU7DAgCjP%1u0u}D;*ZXd8*L7M6Cvmww9rz?^lq%J90gog zq73Qcw4ufgyw!-#IEhd2$kIW1oYsQ6BPyAc<$`U%btV zUn!!W;p|Yp`2N(|lv=r`A2$2k9-Y19jZwI)t=mxVU#gSxLxRy8^bbT;3j9O?1WxXLacp?^5A zF<8*MAgtBL@x}YiDQ-HN=Z>PctCeo~_ep$IGHC)9uJyfiR}J5nc7=kZHRtQ3{=_u9stQqLwq=w;-wdfw!(qRv%w#E~30$HQ} zRP2V+U#L5b+s_k7y69!uyI9<}ykI{nv;TOO+_dR(wK2nGfb19%LG1qTgZ_*SwJN^F z<9?hV-^H>Gd~d5k(GAhB_(SgC|2(hxvXOp`jLgW-Gz#Ed4kiBONfYs)-F@=8!+=ri zt|QN5&uU>$UCfn9q%ZFM*IDm7r-})mrRGqCmM(%oHhby7a@cd$WFhNNx zR|uSNeMe(wmPCLRLAm_|K4;L*fCwnc)7jYukT}2YGs?Xz5K`Y1d* zPc{8)b0y$;Vptra|L|*20;1e;uG(={JzgUk_03$j^0{&{?!_I`Z*+lY&qKbxygn&y z9c%iSq2H`BxdiLWy%8(+zKqGsVvD)SW6Qfims8Z3nyE|mB1)MyCf6lcKwyCIZbj4P0r@zNzq`cAb(ihLeH06o+UMx>OavRJVGQ3&r>A zg^fFgXM{V%%UpV%^!5e%Dc5>U%^dP`UD%+3cmh+5NkZfGIHa zRm@0V-|OVy~q(Oy`y=XUgcSi>YZXTYQa~aT_Q2zAs

jwEbnEd%Uu;0xHBs48Zs*C}8|vH#9T^ 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

rHFR}_Ap-fxB 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