From e293d14e82a903a4cab64dd72dfa3f3798466176 Mon Sep 17 00:00:00 2001 From: Saim Momin <64724322+SaimMomin12@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:14:44 +0200 Subject: [PATCH] Updating of the ERGA EAR tool (#1492) * Updated make_EAR.py script * Added methods section to the tool as per new script update * Updated test data * Bump version * Updated test * Updated tool version --- tools/ear/macros.xml | 26 +++++- tools/ear/make_EAR.py | 165 ++++++++++++++++------------------ tools/ear/make_EAR.xml | 32 +++++-- tools/ear/test-data/EAR.pdf | Bin 336760 -> 336755 bytes tools/ear/test-data/EAR_2.pdf | Bin 379489 -> 379487 bytes 5 files changed, 128 insertions(+), 95 deletions(-) diff --git a/tools/ear/macros.xml b/tools/ear/macros.xml index aa42b95c97..a543fdb25a 100644 --- a/tools/ear/macros.xml +++ b/tools/ear/macros.xml @@ -1,6 +1,6 @@ - 1.0.0 - 1 + 24.08.26 + 0 23.2 @@ -21,4 +21,26 @@ + +
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/tools/ear/make_EAR.py b/tools/ear/make_EAR.py index 2f2fe3fea2..9aaa83ec58 100644 --- a/tools/ear/make_EAR.py +++ b/tools/ear/make_EAR.py @@ -1,6 +1,5 @@ import argparse -import glob import logging import math import os @@ -22,7 +21,7 @@ # CAUTION: This is for the Galaxy version! # by Diego De Panis # ERGA Sequencing and Assembly Committee -EAR_version = "v24.05.20_glxy_beta" +EAR_version = "v24.08.26" def make_report(yaml_file): @@ -120,19 +119,9 @@ def get_completeness_value(file_path, order, tool, haplotype): fifth_column_value = target_line.split('\t')[4].strip() return fifth_column_value except Exception as e: - logging.warning(f"Error reading {file_path}: {str(e)}") + logging.error(f"Error reading {file_path} for tool {tool} and haplotype {haplotype}: {str(e)}") return '' - # Getting kmer plots for curated asm - def get_png_files(dir_path): - png_files = glob.glob(f"{dir_path}/*.ln.png") - if len(png_files) < 4: - logging.warning(f"Warning: Less than 4 png files found in {dir_path}. If this is diploid, some images may be missing.") - # fill missing with None - while len(png_files) < 4: - png_files.append(None) - return png_files[:4] - # get unique part in file names def find_unique_parts(file1, file2): # Split filenames into parts @@ -141,7 +130,6 @@ def find_unique_parts(file1, file2): # Find unique parts unique_parts1 = [part for part in parts1 if part not in parts2] unique_parts2 = [part for part in parts2 if part not in parts1] - return ' '.join(unique_parts1), ' '.join(unique_parts2) # extract BUSCO values @@ -274,33 +262,34 @@ def generate_assembly_warnings(asm_data, gaps_per_gbp_data, obs_haploid_num): # Parse pipeline and generate "tree" def generate_pipeline_tree(pipeline_data): tree_lines = [] - indent = " " * 2 # Adjust indent spacing as needed - - for tool_version_param in pipeline_data: - parts = tool_version_param.split('|') - tool_version = parts[0] - tool, version = tool_version.split('_v') if '_v' in tool_version else (tool_version, "NA") - - # Handle parameters: join all but the first (which is tool_version) with ', ' - param_text = ', '.join(parts[1:]) if len(parts) > 1 else "NA" - - # Tool line - tool_line = f"- {tool}" - tree_lines.append(tool_line) - - # Version line - version_line = f"{indent*2}|_ ver: {version}" - tree_lines.append(version_line) - - # Param line(s) - if param_text != "NA": - for param in param_text.split(','): - param = param.strip() - param_line = f"{indent*2}|_ key param: {param if param else 'NA'}" + indent = " " * 2 # Adjust indent spacing + + if isinstance(pipeline_data, dict): + for tool, version_param in pipeline_data.items(): + # Tool line + tool_line = f"- {tool}" + tree_lines.append(tool_line) + + # Convert version_param to string and split + version_param_str = str(version_param) + parts = version_param_str.split('/') + version = parts[0] + params = [p for p in parts[1:] if p] # This will remove empty strings + + # Version line + version_line = f"{indent * 2}|_ ver: {version}" + tree_lines.append(version_line) + + # Param line(s) + if params: + for param in params: + param_line = f"{indent * 2}|_ key param: {param}" + tree_lines.append(param_line) + else: + param_line = f"{indent * 2}|_ key param: NA" tree_lines.append(param_line) - else: - param_line = f"{indent*2}|_ key param: NA" - tree_lines.append(param_line) + else: + tree_lines.append("Invalid pipeline data format") # Join lines with HTML break for paragraph tree_diagram = "
".join(tree_lines) @@ -330,10 +319,10 @@ def generate_pipeline_tree(pipeline_data): tags = yaml_data["Tags"] # Check if tag is valid - valid_tags = ["ERGA-BGE", "ERGA-Pilot", "ERGA-Satellite"] + valid_tags = ["ERGA-BGE", "ERGA-Pilot", "ERGA-Community", "ERGA-testing"] if tags not in valid_tags: tags += "[INVALID TAG]" - logging.warning("# SAMPLE INFORMATION section in the yaml file contains an invalid tag. Valid tags are ERGA-BGE, ERGA-Pilot and ERGA-Satellite") + logging.warning("# SAMPLE INFORMATION section in the yaml file contains an invalid tag. Valid tags are ERGA-BGE, ERGA-Pilot and ERGA-Community.") # Get data from GoaT based on species name # urllib.parse.quote to handle special characters and spaces in the species name @@ -401,16 +390,15 @@ def generate_pipeline_tree(pipeline_data): # Create a list of lists for the table table_data = [headers, data_values] - # Extract pipeline data from 'Pre-curation' category - asm_pipeline_data = yaml_data.get('ASSEMBLIES', {}).get('Pre-curation', {}).get('pipeline', []) - asm_pipeline_tree = generate_pipeline_tree(asm_pipeline_data) + # Extract pipeline data + asm_pipeline_data = yaml_data.get('PIPELINES', {}).get('Assembly', {}) + curation_pipeline_data = yaml_data.get('PIPELINES', {}).get('Curation', {}) # Extract pipeline data from 'Curated' category - curation_pipeline_data = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}).get('pipeline', []) + asm_pipeline_tree = generate_pipeline_tree(asm_pipeline_data) curation_pipeline_tree = generate_pipeline_tree(curation_pipeline_data) # Reading GENOME PROFILING DATA section from yaml ############################################# - profiling_data = yaml_data.get('PROFILING') # Check if profiling_data is available @@ -418,38 +406,46 @@ def generate_pipeline_tree(pipeline_data): logging.error('Error: No profiling data found in the YAML file.') sys.exit(1) - # Handle GenomeScope specific processing + # Check for GenomeScope data (mandatory) genomescope_data = profiling_data.get('GenomeScope') - if genomescope_data: - summary_file = genomescope_data.get('genomescope_summary_txt') - if summary_file and os.path.exists(summary_file): - with open(summary_file, "r") as f: - summary_txt = f.read() - genome_haploid_length = re.search(r"Genome Haploid Length\s+([\d,]+) bp", summary_txt).group(1) - proposed_ploidy_match = re.search(r"p = (\d+)", summary_txt) - proposed_ploidy = proposed_ploidy_match.group(1) if proposed_ploidy_match else 'NA' - else: - logging.error(f"File {summary_file} not found for GenomeScope.") - sys.exit(1) - else: - logging.error("GenomeScope data is missing in the PROFILING section.") + if not genomescope_data: + logging.error("Error: GenomeScope data is missing in the YAML file. This is mandatory.") sys.exit(1) - # Handle Smudgeplot specific processing + genomescope_summary = genomescope_data.get('genomescope_summary_txt') + if not genomescope_summary: + logging.error("Error: GenomeScope summary file path is missing in the YAML file.") + sys.exit(1) + + # Read the content of the GenomeScope summary file + try: + with open(genomescope_summary, "r") as f: + summary_txt = f.read() + # Extract values from summary.txt + genome_haploid_length = re.search(r"Genome Haploid Length\s+([\d,]+) bp", summary_txt).group(1) + proposed_ploidy = re.search(r"p = (\d+)", summary_txt).group(1) + except Exception as e: + logging.error(f"Error reading GenomeScope summary file: {str(e)}") + sys.exit(1) + + # Check for Smudgeplot data (optional) smudgeplot_data = profiling_data.get('Smudgeplot') if smudgeplot_data: - verbose_summary_file = smudgeplot_data.get('smudgeplot_verbose_summary_txt') - if verbose_summary_file and os.path.exists(verbose_summary_file): - with open(verbose_summary_file, "r") as f: - smud_summary_txt = f.readlines() - for line in smud_summary_txt: - if line.startswith("* Proposed ploidy"): - proposed_ploidy = line.split(":")[1].strip() - break + smudgeplot_summary = smudgeplot_data.get('smudgeplot_verbose_summary_txt') + if smudgeplot_summary: + try: + with open(smudgeplot_summary, "r") as f: + smud_summary_txt = f.readlines() + for line in smud_summary_txt: + if line.startswith("* Proposed ploidy"): + proposed_ploidy = line.split(":")[1].strip() + break + except Exception as e: + logging.warning(f"Error reading Smudgeplot summary file: {str(e)}. Using GenomeScope ploidy.") else: - logging.warning(f"Verbose summary file {verbose_summary_file} not found for Smudgeplot; skipping detailed Smudgeplot analysis.") + logging.warning("Smudgeplot summary file path is missing. Using GenomeScope ploidy.") else: - logging.warning("Smudgeplot data is missing in the PROFILING section; skipping Smudgeplot analysis.") + logging.info("Smudgeplot data not provided. Using GenomeScope ploidy.") # Reading ASSEMBLY DATA section from yaml ##################################################### @@ -459,7 +455,7 @@ def generate_pipeline_tree(pipeline_data): asm_stages = [] for asm_stage, stage_properties in asm_data.items(): for haplotypes in stage_properties.keys(): - if haplotypes != 'pipeline' and haplotypes not in asm_stages: + if haplotypes not in asm_stages: asm_stages.append(haplotypes) # get gfastats-based data @@ -483,7 +479,7 @@ def generate_pipeline_tree(pipeline_data): except (ValueError, ZeroDivisionError): gaps_per_gbp_data[(asm_stage, haplotypes)] = '' - # Define the contigging table (column names) DON'T MOVE THIS AGAIN!!!!!!! + # Define the contigging table (column names) asm_table_data = [["Metrics"] + [f'{asm_stage} \n {haplotypes}' for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]] # Fill the table with the gfastats data @@ -493,8 +489,6 @@ def generate_pipeline_tree(pipeline_data): asm_table_data.append([metric] + [format_number(gfastats_data.get((asm_stage, haplotypes), [''])[i]) if (asm_stage, haplotypes) in gfastats_data else '' for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]) # Add the gaps/gbp in between - gc_index = display_names.index("GC %") - gc_index asm_table_data.insert(gaps_index + 1, ['Gaps/Gbp'] + [format_number(gaps_per_gbp_data.get((asm_stage, haplotypes), '')) for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]) # get QV, Kmer completeness and BUSCO data @@ -502,7 +496,7 @@ def generate_pipeline_tree(pipeline_data): completeness_data = {} busco_data = {metric: {} for metric in ['BUSCO sing.', 'BUSCO dupl.', 'BUSCO frag.', 'BUSCO miss.']} for asm_stage, stage_properties in asm_data.items(): - asm_stage_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + asm_stage_elements = list(stage_properties.keys()) for i, haplotypes in enumerate(asm_stage_elements): haplotype_properties = stage_properties[haplotypes] if isinstance(haplotype_properties, dict): @@ -580,7 +574,7 @@ def generate_pipeline_tree(pipeline_data): styles.add(ParagraphStyle(name='subTitleStyle', fontName='Courier', fontSize=16)) styles.add(ParagraphStyle(name='normalStyle', fontName='Courier', fontSize=12)) styles.add(ParagraphStyle(name='midiStyle', fontName='Courier', fontSize=10)) - styles.add(ParagraphStyle(name='LinkStyle', fontName='Courier', fontSize=10, textColor='blue', underline=True)) + # styles.add(ParagraphStyle(name='LinkStyle', fontName='Courier', fontSize=10, textColor='blue', underline=True)) styles.add(ParagraphStyle(name='treeStyle', fontName='Courier', fontSize=10, leftIndent=12)) styles.add(ParagraphStyle(name='miniStyle', fontName='Courier', fontSize=8)) styles.add(ParagraphStyle(name='FileNameStyle', fontName='Courier', fontSize=6)) @@ -659,7 +653,7 @@ def generate_pipeline_tree(pipeline_data): # Iterate over haplotypes in the Curated category to get data for EBP metrics curated_assemblies = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}) - haplotype_names = [key for key in curated_assemblies.keys() if key != 'pipeline'] + haplotype_names = list(curated_assemblies.keys()) for haplotype in haplotype_names: properties = curated_assemblies[haplotype] @@ -756,7 +750,7 @@ def generate_pipeline_tree(pipeline_data): # Store BUSCO version and lineage information from each file in list busco_info_list = [] for asm_stages, stage_properties in asm_data.items(): - for haplotype_keys, haplotype_properties in stage_properties.items(): + for i, haplotype_properties in stage_properties.items(): if isinstance(haplotype_properties, dict): if 'busco_short_summary_txt' in haplotype_properties: busco_version, lineage_info = extract_busco_info(haplotype_properties['busco_short_summary_txt']) @@ -787,9 +781,9 @@ def generate_pipeline_tree(pipeline_data): tool_count = 0 # Add title and images for each step - for idx, (asm_stages, stage_properties) in enumerate(asm_data.items(), 1): + for asm_stages, stage_properties in asm_data.items(): if asm_stages == 'Curated': - tool_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + tool_elements = list(stage_properties.keys()) images_with_names = [] @@ -825,7 +819,7 @@ def generate_pipeline_tree(pipeline_data): # Add images and names to the elements in pairs for i in range(0, len(images_with_names), 4): # Process two images (and their names) at a time - elements_to_add = images_with_names[i:i + 4] + elements_to_add = images_with_names[i: i + 4] # Create table for the images and names table = Table(elements_to_add) @@ -856,7 +850,6 @@ def generate_pipeline_tree(pipeline_data): # Iterate over haplotypes in the Curated category to get K-mer spectra images curated_assemblies = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}) - haplotype_names = [key for key in curated_assemblies.keys() if key != 'pipeline'] # Get paths for spectra files spectra_files = { @@ -974,9 +967,9 @@ def generate_pipeline_tree(pipeline_data): tool_count = 0 # Add title and images for each step - for idx, (asm_stages, stage_properties) in enumerate(asm_data.items(), 1): + for asm_stages, stage_properties in asm_data.items(): if asm_stages == 'Curated': # Check if the current stage is 'Curated' - tool_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + tool_elements = list(stage_properties.keys()) for haplotype in tool_elements: haplotype_properties = stage_properties[haplotype] diff --git a/tools/ear/make_EAR.xml b/tools/ear/make_EAR.xml index 87c9b9152e..9cabd9a296 100644 --- a/tools/ear/make_EAR.xml +++ b/tools/ear/make_EAR.xml @@ -39,7 +39,6 @@ PROFILING: # ASSEMBLY DATA ASSEMBLIES: Pre-curation: - pipeline: [Hifiasm_v0.19.4|HiC|l0, Purge_Dups_v1.2.6|, Bionano_vGalaxy_3.7.0, YaHS_v1.1] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${pre_curation_assembly_data.gfstats_nstar_report_precuration}' busco_short_summary_txt: '${pre_curation_assembly_data.busco_short_summary_precuration}' @@ -52,7 +51,6 @@ ASSEMBLIES: merqury_completeness_stats: '${pre_curation_assembly_data.hap2_precuration_data.merqury_completeness_stats_hap2_precuration}' Curated: - pipeline: [GRIT_rapid_v2.0, HiGlass_v1.0] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${curated_assembly_data.gfstats_nstar_report_curated}' busco_short_summary_txt: '${curated_assembly_data.busco_short_summary_curated}' @@ -80,7 +78,6 @@ ASSEMBLIES: # ASSEMBLY DATA ASSEMBLIES: Pre-curation: - pipeline: [Hifiasm_v0.19.4|HiC|l0, Purge_Dups_v1.2.6|, Bionano_vGalaxy_3.7.0, YaHS_v1.1] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${pre_curation_assembly_data.gfstats_nstar_report_precuration}' busco_short_summary_txt: '${pre_curation_assembly_data.busco_short_summary_precuration}' @@ -88,7 +85,6 @@ ASSEMBLIES: merqury_completeness_stats: '${pre_curation_assembly_data.merqury_completeness_stats_precuration}' Curated: - pipeline: [GRIT_rapid_v2.0, HiGlass_v1.0] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${curated_assembly_data.gfstats_nstar_report_curated}' busco_short_summary_txt: '${curated_assembly_data.busco_short_summary_curated}' @@ -102,6 +98,18 @@ ASSEMBLIES: blobplot_cont_png: '${curated_assembly_data.blobplot_cont_curated}' #end if +# METHODS DATA +PIPELINES: + Assembly: + #for $repeat in $method_data.assembly_method_info: + ${repeat.assembly_tools_info} + #end for + + Curation: + #for $repeat in $method_data.curation_method_info: + ${repeat.curation_tools_info} + #end for + # CURATION NOTES NOTES: Obs_Haploid_num: '${curation_notes.obs_haploid_num}' @@ -131,7 +139,7 @@ NOTES: - + @@ -150,7 +158,6 @@ NOTES:
- @@ -177,7 +184,6 @@ NOTES:
- @@ -210,6 +216,16 @@ NOTES:
+ +
+ + + + + + +
+
@@ -282,6 +298,7 @@ NOTES:
+
@@ -356,6 +373,7 @@ NOTES:
+
diff --git a/tools/ear/test-data/EAR.pdf b/tools/ear/test-data/EAR.pdf index f440f02ce781283ec884d038163afddf413a3568..9993cbb167401e8a015d05caa24a796ec174140c 100644 GIT binary patch delta 3072 zcmai#%g*wMk%rYtz9{*2QCe9Y;5oWFkrwKb2eb`0c?E4oVOt(^##oC zzl)J(G*U0K%Vo?hjG}C^%->R^NXa6#s8YQusW$aK_18ar`(zNGF*Vo0nHC&*lLAtxty2o@6){vG5jQ3TLML_O4(7@^&^umgGjD2SCRZ8D5Ii#2ZcPsyEUpH)%F?Y? zEhNoirjqFgw{yqimtyzoWbavYW9O+=Y-iL;0ab(w=5~P=ze+olD($QS%aSj4eZtcA z976lBsj7AUpoFkO2Xl9fr^|9d^5G&O-v<}#_}0zdgsOH0|02~l9zpUp)Mo6KW(3U6 z8J>2{T3Z~=`f^)46$?JZwfLf&CtJB`eZ2J&wUBb|c91oUc8sj1l)1Pz*R=+Xc@?D5 z@a}g@!%vZ4T^1dfMq5P}+|=+^urDA}KJcR%{yx#fqF8bErFBfuUCm^u%JPvFY?nNv z-I{Jxc>Ps}O4+ScG}`JpCjBncs}0UzZnSRS>!~$=a}9?W8U?+o}-WAvJ( z&;4$yX0xb*j19ym1pZZ7%k(zMiin1uX0gC+T~}1*ulSN&H+QQbKnYmBuL7!*L;*cy zX7hbBAoY{vm&PvKmmrB$jM>P|LF!Q}#FhdgZ%2$r#at^lfY{xwQyoe?v4UZ_oS8t& z-nGXUa^V=v%_TKrnl}gOyz-Q`(g`HXOnC*C-a0)JBy0UjJgG+qdNRYQ zI4vzP6oSv3J0n>OS7rQ2HNlGg2oU3lEpmE~eD5DoJ!))wIY5Wf8^l)*y0_kJHcBf3 zGu&+4ctHk#XqsyYPnrEHj zB;=<-3*9s2q4NUm+Cjfy=RS*ItK?9+JTB${z}~kVkyE^(y+R8aNxHQ5{c}`d<$$^h zRh$}-_MxH#d26C*BdoYASZBAL6Kw(F+Hp&+lP%3kOe>*dOb;tj;!Nn*3f8RW!p&W3 z!xnTysuDjf2E#P1XH7pYsr{keh;5Q&$IcD6-f50l=n_b)jl2d=M$kcgTJuK%lXwEKo>%!H)jLGT{n_Q3X2s>|1`jC)0Hm8Z( zf{`!r1RO?>WV1}3YQ0}$p_X_R_fVq(usU!&FX6eWJa3h8LEZ8h`ke4<*&S>!Y~4OM z@oFQMC;RFq_2=ZI+%Y6IF`!^ro z{_w|Nef;_7Z#I#3m3+2J53$G1xo@4$V|IeK!Gdk{%#gOI+sdQ%Nti!L#yFRWpQVzf z-GSZWJZ1;YKuyJK>mXm?(;Tqa?kk4}cK$ieh-EQlU-ht1y4ZmCcogO~P_TwilVbX8ITM){L(acupgI!xperw< zo1Z1yQCZKd?c7q8mP^-}jgk8@WQKt;2@kalqbnUwR7r4r1y8xOIq!ta;jTywPHo>! zZc<5F&1y+^_vW|7$NW~UU0Z3eMlXePGIWkf28*H?N<}p6WM%IwYV>+7UzhJ3bepJo z1ly2yI3>%XNaE#qb$S}mXaq{U-}BY>(cj8us|tpIz>y|-0KQra;g!_bZM(v3cxMaA zwEx)HN1d}LU-uvG+KPsgl4qa~*YDo)ZM!8chU$RuXMVzFo2C&#VqspY3-f*kxyV(} zPPkxB)xcuKj7_oQkLFcnhA=(2`1O{hiD4ndE&=EShtoD?h!dA(tW03LH+^mXUhq=) z?+@Kezx?qxA3y)$e}4O6{^T3#%ddX-k$H1?W2fBJNh>^Y(5`{5&nqy!4+fVUwI^uZ zZx~{^I+q}mV8w!QJ+AIF430k^yEb|0|z(p}uz2&%gn!;$yAX`AupIas1sMm+L zQEk2~8y=M9hNn_g=fdYfxSKFwWiltY@D$&by+#f9v3u;9^LUlK_Nm~x)_AcPXEVEb zc6s%8SZ||oqpGPNU4095(mgQ)Z-d=9SY6u|x-T_QyXSB-ur0-pJ-NFLmY(stL3f_j z4B!+Rsp$=?LaD|VEp(3#RYVvgoHcV&2fso$k-FN#KIIP<4g8KQ~K1{GYkrU|M za1uCF1y}MMT3MHii&Ap|zkU%cIksoKGYGYN(->ub!oD`x_82dm>^P-zai@pA5W zw%%DIiNn-u)Sf45m#!J#zQ{qV>_g}iM*O#T9i+jvZ9-o;rf#MMsKw?F{YM7h`lA@% zXE)DNdPpv7X;F7`-JEo6pONYDY%^JTdURm!DwV{2XM7t2-l?z8XOA2pE&9yGKs`Dy z;NGC(p#0h5MhBZWFllFy1(jZOUC;B36 z6t^JfSJ|agfmTvU!F?$)yZ8Ca3b^$*T>AOH8wl&E}%{}|t;-+lgANs_Fx%)e+XLHx5;rEv0}H43kO`A^{E ycWvf~Q?z$-YB)nNjz>91szTwXis#V8kvx_xU#?34s9s delta 3072 zcmai#InVNnvBz~z@??3sD5)YvI>yGvHU_-g7|d#e+05z%FlIAgya9$bB+}{f0p2=Y zs-%f@zCk`lK0w+uDeoKUN>|D)GSX-?e~mQ7U-O%P{_R))`rEI5_x(5DfBXH9zW?!G zKbD_A=SvJpQ8Y<^IedOJ|F8Yw&z~RYH*bObl;zL#`S5npXJ_}U;_bXh>Tp+l+PPaH zvN|>B^>BK#urL-FyRrdC13frWYFs&RDPFZ2Ia$%d*KjkB8mWxQcojKu=a^h`<8van z@9q`9LfHhnj=5e+Oxpbc4R!qODut`4L%*R%6BVk*^~}r>mGYVbG-b7D!xV#nn?$gp zapJ1(VQT2@0#|dJVSMV{VYJRci$rLY%^fusMYl_pV{$(vbYoLQR-|@E2OA` zF&PW&5qE1(f&~{z_LA?~Y!B{1j4~K(!~$K9>M&niUusKtv9y;X^;T%aVI^u2=kl-@ zh>Z?hHDI4t^R-%YQ7Ba4el7RxLznwr&6zT|6hDCmgCuah#(bc3ce=cFXRbbGR;)y6`zhktx{jO?jTfW)mf18JimIa>a;= z+SSgzJzn@!yeDhpz(L|;*6A{Ld$Wr{&U`oHCu!eMV4F8H#bc6yi`L4VpI$C8U(-_F z-OT1IUurrH$Z1u&okIV(2x}bInm2Tza&&=_O;zgp5{o8qkx(|TvfVh1r9Ca1svuq@+^2($)Nb2JVny_HiG zaO$ouvB84PnEPoyRqmlL0CsC>C*Dh<1_el+`%D*$D}zaO7CtzG>`Z1$*g(mmH#KJW zfUJkNjM2N18Y7zGv*!;5qK>Wa_nW8gi*GAQyFQrsGKQ>MZG6wkwmT^|!U{9(OgHdS2i|r|u0@Ebdj}L>~7Sq`o zA`gIw2+Gyr6~&wemdUb`7ENN+Ljda?4q_d|wE7@CO&PT5@ktjw_P6!^VTVrjxC%rP zH21}t-&9mCr66ER9hH^Sd5P$~bY|?>SWOrD?gF#N*%mX2dV_u}=7>J8GsAfHoZFir zWEulLjBlDE>E+CN&42-LF0ORhDeT|yg|pRZPsa15x= z2ZQ7eO(qi(P?_Oj_mt_uwn5SBln`9{QD5$r=3u96rClAC^-QB>PEsX`aROi_&{VJ} zQLDh`!}6;8edbVL{AKGn1oGfjLmXFXX-0fNw5#Zqg|(hp0MZmX5rehekOYbw=ER$f z5dxT(g}yKt6I`h169I#TIl0CuxD;0Sskp83!MHRRvTY7y7-MvHZ?i#?+^zi&fBePA z$4}o*m^rjzNxT!oR+RXR()Bk=JglG4P!&DC6@xpVM@fsBQW#_HBMg;JmwS+cxy^Lk zh-X3et4o&iXqZ`;TF<*vJgvGqKs@j!TeQl|ZGQn?tlF=rc(!~*c%MAwszaFvhspe0 zs%PIs+6H1G?I*pKh#HSzhTq7cwITA8k-90JA~?q`Hk;YTwC2T320kb23s<{{b}K|g z(P-rT#UMi3lT>G}!-8Sg;TqkaTP@VZd1%kwqqw`Pxq(g&is$)c*mFjsnLv!Un}u8H zjd1@xtpSw&G|qOdiGyA~7ln=0{@OG!=g!mqy6-!)vwxk5m$2+L_z1$cw(rvYHv4+< zE5rb7z*~0e^$s1@ADxU+;+k@Xur=+J?GQKR&RMJWikavMHqNh@t`X=PMT>%Rt29N& zyGP|6nNl}c;x<|k=$!1oT25_P4Vb>dN=@|q$g5ANi0(qMhdG6HuenI)qr+)CjFO<+ zbN1etiqOY!eEj5xKmGb+^X)gKFF*aw2k@>tr?qx^Qlg2@jqVwewy4_oj{W>h@ut1kYuD(b z2)Rssu9drKW;ZOYxJ4@uo_l!ilpY7oTr-tdO*+BYjCf!psteVL%(k==zw#V|GSsY4 z&K|ki8t{#1hIx^=Ye79%h@g$Xw6-kZI|crCd$ty<+)!y|ls} zGYuI-$mMfvzZR@jZCl@A>!4QBt6k|$the|4_8~&lh%ZBEuj*t~%+q?e*7ccdS*N%c zREF}`#fRG?-+&PS+F~RF4qNR5!I>+*bYC`>oiB`!yON};r-q}$uY@k_d_E>d+ zheWH4D}#6sbnTe9XdDBjtNh{lboU7V$qUYm%+3gJ)}EzU*z~N zU(xx?AJ*S}{Qa8=MSWNLcYGIr_xbNi(==NW{+}jN~yW;B2Q-B*A6-B-W)<_F*W@S7if^W#5# z{OsGGGDIC?sx(f0{_5M0&cDm=4&Q!YKlo9#PJRCUA3thuogY7LR%Yz-RB!GS#8(yN z#|hGdG7QRr%0B9J;8=`P3~rUe9QMFO@j1M@_YKg%wh)3!-4?cj59a>5@H=eB>uAP= zHtJN|qyY@vA3-#Lj_Xs;tiT%h)Rr0lxzm=`{ecv*(RJ{?ag}?3KfTqksdDyxyTtEz ziZtU_@?_eX7{W-kE>IFW_B-AsHwSEty-Vy;eicf3)sGKq4k8<2OYkP`Vf6t4tHzC1 zr&Gc|+^iu#1%i2bHKzSn)T}z`@sT^@>jc4!R?z>Uvs{3STum#QnCi}wb^qF zI#C|Ov08{P%o>f5<^qC*FnP43D$tPClNht}#r3v$wzq31s?xytkv3COzb+<|-IXZa zC};x?3Qk$RKigorHh27}?JbLstWOPAHe3YMPVE$oaKL!(pLs@Q%*{ExPP7+lv6g27YI7FZ3r$b?gF`2^Ky@@V5PWe0*0~(rX^(wc6 zI)>0Okq-FvyuW2O=4AA0%SH#+Gq&++)A?&lWDxhQeM{cw^cWOs&LP(H*x2Kx_>=*= zLn&-3Tpcim=3roIuz>W>{CT~|yH{~nvQ&7OZbouB=8vQODKUziIk(Xz-UH$41Or2rYahOcp zg0xAVZ_Jsi2rn)xe0EYFvRA*f2A5ZH7avVuw$CP-V%HmLp|@OV4Q&I<)gKG7w;yk| z)f%26{U&~mh|38#DR?5}H`s~HM}*j-PHAU+x~s2-ALidryKc=IsRv!CAeVGzxKgR|jTCIG>nXv$*VFJ)x8zq{}wZy`anQe)Fe~Km6oPe-eaGk7s(?ho1)frx6{q z<>3I#76-W;L&(@U;MEI^R~j^Yx68TQx=dD8PF z$c(ye`^|&~JB=eeU7YX1tRj>LE@R(ooC0yyi@)^9ZO9CL3^8X9hZVxm8D0DPp0qz< zoOC3UOdSDd65K!nFv_`t@QTIp(B4nNKE%xctoS%e;R|!FZ|`uvjnQ28vKk;6W)UGP z%PG?~u#}4W;KYc`=$OR&#kjiH{JDk=L5h%-1>EkTbQB5 zlc;1}PN|zB6R11f>Jj>*pUXFVEq^{9+6bea?C~9k(5X4jW_zlaLYJLV+4UOnsnxF6 z78{F(6^Ky7g0MyiQqPu8(DRbcOF&4^hZC0rR~O|f4|}aO`u_IB^B+NUopIT_s`110gO_~_#&-CuWJa@$oapT7|Fi|-zP{$PImCaC}5=(W?% z3JiexhO!9~D!t#nT#M!+#vZ`5ef8MQ`8J0nCc+Q*4O)x`cx~-^0#<+vi&j&jwr78_ zpue;f&Z93u3Tk_S$Qje;wPY=JB9~wRz`{f*HH4t)*2C?aC(AoOWmlK1tj~eUEa5iB zx$9bIHs66QRGL8&(nlqv%s;7UdVBU4Me<<Ne9a35rL9bP{X z6@ia?`No)BuoQ7AT2M*7ZuZT90Ycz*~OZ3&8clduc%AXr|}2Chk^ zr{qpk;nX*&WqahV0bM5G07=SnD!bvZyck4TV?Eswhr+vkHpUxGqsr%%0B^}@$Q_R* z?p&nG*In|UjBvQ)wCmuaAWd#6)LYbGk{xxSfSP%JaD<*Z8x&rn1B2N@B)hxuvU1I6 z)!VpGzNd%6ZS5&tSL^rJ=B7<++{V$=g#5C7uDou2fqMQHQ2X@=VUsI)EvF=U)oXs3 zqlJ=vq~di{ewG^Q&TZGNq&r)ka2O9$$K@3?oH#Rp%1Q{tZQ|Q)5xf{7zp7CL`bZ)3lQj6=Gx}P(UbF%ji z9M($b($lchRPaS#tkZEIg!XoT-B!|`P5Gv!)@xvK;!7ombq|uR&dE+Zosw;M)Yyb>LQ`^Hdaro82I|dx7PaNbk`;}Q~+-D&c?@{ks{yy<1Zhj9|}yp z_BHme_&WOf)4wuFQxt>!w^qYQ;(xRn^Lh6D$IpLB{*RrevCqH${^M7jJyoq$52w0Y p+tV~%BdZL7)9$|NlC{IW?(PNd|2aPY_4|+C{-pK^1Vy#~>7V;ql(GN- delta 3185 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 LPY^Vkai9JN+gYIj