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
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
delta 3072
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
delta 3185
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
LPY^Vkai9JN+gYIj