diff --git a/bin/workflow_glue/report.py b/bin/workflow_glue/report.py index 458a58b..fabec8b 100644 --- a/bin/workflow_glue/report.py +++ b/bin/workflow_glue/report.py @@ -650,16 +650,15 @@ def main(args): with open(args.metadata, "r") as handle: metadata = json.load(handle) - with report.add_section("Workflow overview", "Overview"): - p( - "This report summarises joint bambu transcript discovery " - "and quantification, per-sample transcriptomes, optional " - "SQANTI3 structural classification, and optional " - "differential analysis outputs." - ) - if args.stats: with report.add_section("Read summary", "Reads"): + p( + "Read quality and alignment statistics generated by fastcat bamstats " + "for each input sample. Shown are distributions of read " + "length and quality score, and a summary of alignment " + "outcomes (mapped, unmapped, primary, supplementary) " + "against the reference genome." + ) stats = tuple(args.stats) sample_names = tuple( item["alias"] for item in metadata if item.get("has_stats") @@ -690,6 +689,12 @@ def main(args): ) with report.add_section("Sample metadata", "Samples"): + p( + "Metadata for each input sample as parsed from the sample sheet " + "and workflow parameters. Includes the sample alias, barcode, " + "type, and any experimental design columns such as condition or " + "batch that were supplied for differential analysis." + ) tabs = Tabs() for item in sorted(metadata, key=lambda value: value["alias"]): with tabs.add_tab(item["alias"]): @@ -702,6 +707,10 @@ def main(args): mod_summaries = _sample_mod_summaries(args.mod_summary_dir) if mod_summaries is not None and not mod_summaries.empty: with report.add_section("Modified base summaries", "Modifications"): + p( + "Summary of base modification calls from modkit pileup for each " + "combination of sample and modification type." + ) dom_style(raw(_mod_summary_matrix_style())) raw(_render_mod_summary_matrix(mod_summaries)) small(raw( @@ -720,6 +729,14 @@ def main(args): if annotation_reference_summary: with report.add_section("Reference and Annotation Checks", "Reference"): + p( + "Results of compatibility checks between the supplied reference " + "genome and annotation performed before bambu transcript " + "modelling. " + "Build and provider hints are inferred from sequence names and " + "file content to help identify mismatched genome/annotation " + "combinations." + ) for warning in annotation_reference_summary.get("warnings", []): _create_warning_banner(warning, level="warning") @@ -819,6 +836,17 @@ def main(args): # Add Bambu QC section with warnings if bambu_qc: with report.add_section("Bambu Quality Control", "Bambu QC"): + p( + "Quality metrics from the bambu transcript discovery and " + "quantification run. Library size statistics show cohort-level " + "summary values to " + "flag large inter-sample variation that could affect CPM " + "normalisation, with the individual per-sample counts listed " + "below. The transcript discovery table shows the transcriptome " + "mode used, the novel discovery rate (NDR) threshold applied, " + "and how many transcripts were present before and after " + "low-count filtering." + ) # Check for warnings if bambu_qc.get("library_size_warning"): _create_warning_banner( @@ -939,6 +967,13 @@ def main(args): f"{primary_label} transcriptome", f"{primary_label} transcriptome" ): + p( + f"Summary of the " + f"{'per-sample' if is_single_sample else 'joint cohort'} bambu " + "transcriptome model. Shows the total number of transcripts and " + "genes in the final model, and " + f"the top 500 rows of the transcript abundance count table." + ) transcriptome_metrics, transcriptome_classes = _transcriptome_summary(bambu_dir) if transcriptome_metrics is not None: DataTable.from_pandas( @@ -972,6 +1007,13 @@ def main(args): "Per-sample transcriptomes", "Per-sample transcriptomes" ): + p( + "Per-sample transcript and gene count summaries derived from " + "the individual bambu quantification runs. Each tab shows the " + "number of transcripts and genes detected in that sample after " + "filtering. These can be used to spot samples with unusually " + "low transcript detection compared to the rest of the cohort." + ) tabs = Tabs() for sample, summary_df in _sample_summaries(args.samples_dir).items(): with tabs.add_tab(sample): @@ -985,7 +1027,14 @@ def main(args): sqanti_table = _sqanti_table(args.sqanti_dir) if sqanti_table is not None and not sqanti_table.empty: with report.add_section("SQANTI3 classification", "SQANTI3"): - p("Summary of structural classification of isoforms using SQANTI3.") + p( + "Structural classification of transcript isoforms by SQANTI3. " + "Each transcript is assigned a category based on how its " + "splice junctions and exon structure compared to the reference " + "annotation. The table shows the count of " + "transcripts in each category per sample and for " + "the whole cohort." + ) DataTable.from_pandas(sqanti_table, use_index=False) with p(): for category, description in classification_categories.items(): @@ -1002,6 +1051,18 @@ def main(args): "Differential Analysis Quality Control", "DE/DTU QC", ): + p( + "Quality control summary for the differential expression " + "analyses. Shows the experimental design " + "and the number of samples per group. For each contrast, " + "the statistical methods chosen by DESeq2 and DEXSeq are " + "reported , including the dispersion estimation strategy " + "(parametric or gene-wise fallback) and size factor " + "normalisation method, alongside the analysis status and " + "count of significant hits. Any warnings about low sample " + "numbers, dispersion fallbacks, dropped covariates, or " + "analysis failures are highlighted here." + ) # Check for critical warnings has_warnings = False sample_size_warnings = _as_string_list( @@ -1279,6 +1340,16 @@ def main(args): cohort_samples = _load_cohort_samples(args.cohort_dir) with report.add_section("Differential gene expression", "DGE"): + p( + "Differential gene expression results from DESeq2. The " + "heatmap, PCA plot, and sample distance matrix are derived " + "from CPM-normalised counts " + "and give an overview of sample clustering relative to " + "the experimental conditions. Each contrast tab shows a " + "results table of genes ranked by adjusted p-value and a " + "volcano plot highlighting significantly up- and " + "down-regulated genes." + ) dom_style(raw(_heatmap_style() + _volcano_style())) if condition_column: if cohort_cpm['gene'] is None: @@ -1352,6 +1423,16 @@ def main(args): EZChart(gn_selected_table, width="100%", height="auto") with report.add_section("Differential transcript usage", "DTU"): + p( + "Differential transcript usage results from DEXSeq. Unlike " + "DGE, DTU tests whether individual transcripts change their " + "proportional contribution to total gene expression between " + "conditions , i.e. isoform switching , rather than testing " + "for changes in total gene abundance. The heatmap, PCA, and " + "sample distance matrix use CPM-normalised counts. " + "Each contrast tab shows a " + "results table ranked by adjusted p-value and a volcano plot." + ) if condition_column: if cohort_cpm["transcript"] is None: _create_warning_banner(