Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9360efb
fix: add extra ntm field to summary
JeremyWesthead Mar 23, 2026
cd6a310
chore: add bumping to PRs
JeremyWesthead Mar 24, 2026
ece53e1
fix: push changes to source branch not develop
JeremyWesthead Mar 24, 2026
ef065e7
Merge ece53e1027dc2a92bd4ad48c76114565d12e2f91 into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
7dd1a21
bump: active version from 2.5.3 to ef065e7
github-actions[bot] Mar 24, 2026
b412793
Merge 7dd1a2145f3f71af4945db644bf04fdb408bbfe2 into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
8c4eb1f
bump: active version from ef065e7 to b412793
github-actions[bot] Mar 24, 2026
512127d
Merge 8c4eb1fd4325aa8122f5eb788d070755f3df9e7c into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
9f4d4d5
bump: active version from b412793 to 512127d
github-actions[bot] Mar 24, 2026
94848e7
Merge 9f4d4d5f6d3d607eeee24a30383abbc305265638 into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
e31d257
bump: active version from 512127d to 94848e7
github-actions[bot] Mar 24, 2026
a6321af
Merge e31d25718cec14b47d3bda7ebcf7eb2a42af1fc8 into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
74e283f
bump: active version from 94848e7 to a6321af
github-actions[bot] Mar 24, 2026
ec9b962
Merge 74e283f4cec1b8335eef288de0148fab0a814c33 into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
adaccd7
bump: active version from a6321af to ec9b962
github-actions[bot] Mar 24, 2026
420af19
Merge adaccd73586c12eab22a2f2ce70a8c54b102545f into b05d516fe7f17de0a…
JeremyWesthead Mar 24, 2026
9e5d966
bump: active version from ec9b962 to 420af19
github-actions[bot] Mar 24, 2026
c7f3e23
fix: revert actions hack
JeremyWesthead Mar 24, 2026
4374fe7
fix: pass through assembled species from nextflow
JeremyWesthead Mar 24, 2026
3ca4aa9
test: add new arg for nftest
JeremyWesthead Mar 24, 2026
202e9aa
chore: container `2.5.3+ntms`
JeremyWesthead Mar 24, 2026
da9787b
chore: container `2.5.3-ntms`
JeremyWesthead Mar 24, 2026
d854bb2
fix: properly parse assembled species from nextflow
JeremyWesthead Mar 24, 2026
6b2824b
fix: correct type wrangling for assembled species
JeremyWesthead Mar 25, 2026
0e49e36
fix: ambivalance for `M.` and `Mycobacterium` from speciation
JeremyWesthead Mar 25, 2026
9fb4aa0
chore: bump container
JeremyWesthead Mar 25, 2026
c355c1b
fix: add assembled species list to ntm block
JeremyWesthead Mar 26, 2026
cea431b
fix: ensure all relevant CM results
JeremyWesthead Mar 30, 2026
0987eab
chore: bump container
JeremyWesthead Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions main.nf
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ workflow {
reports_ch = Channel.of(reports_list_abs).map { ["sample", it] }
}
reports_ch.view()
summary(reports_ch)
summary(reports_ch, params.assembled_species)
}

workflow summary {
take:
reports_list
assembled_species // String of comma-separated species to include in summary report, e.g. "Mycobacterium tuberculosis, Mycobacterium avium"

main:
summary_json(reports_list)
summary_json(reports_list, assembled_species)

emit:
main_report = summary_json.out.main_report
Expand All @@ -64,7 +65,7 @@ process summary_json {
cpus 1
memory '0.5 GB'
container {
params.test_container_myco_summary == "" ? params.container_prefix + '/gpas/summary_pipeline:2.5.3' : params.test_container_myco_summary
params.test_container_myco_summary == "" ? params.container_prefix + '/gpas/summary_pipeline:2.5.3-ntms5' : params.test_container_myco_summary
}

pod label: "name", value: "summary_pipeline:summary_json"
Expand All @@ -73,12 +74,16 @@ process summary_json {

input:
tuple val(sample_name), path (reports)
val assembled_species

output:
tuple val(sample_name), path ("main_report.json"), emit: main_report

script:
// Annoyingly taking a list means you can't directly do `.length()` because nextflow is amazing :')
// So we have to convert to string before checking length - an empty list give length 2 ("[]")
ASSEMBLED_SPECIES = assembled_species.toString().length() > 2 ? "--assembled_species ${assembled_species.join(' ')}" : ""
"""
summary_json --reports ${reports} --output main_report.json
summary_json --reports ${reports} --output main_report.json ${ASSEMBLED_SPECIES}
"""
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ version_provider = "pep621"
tag_format = "$version"

[tool.version_bumper]
active_version = "2.5.3"
active_version = "420af19"
version_files = [
"main.nf",
]
8 changes: 8 additions & 0 deletions src/summary/cli_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ def __init__(self, argv: list):
dest="reports",
help="A list of report files, the contents of which will be inferred by filename",
)
report_list.add_argument(
"--assembled_species",
nargs="*",
dest="assembled_species",
default=[],
help="A list of species that were assembled in the pipeline.",
)
outputs = parser.add_argument_group(title="Output parameters")
outputs.add_argument(
"--output_path",
Expand Down Expand Up @@ -128,6 +135,7 @@ def __init__(self, argv: list):
self.gnomonicus = Path(args.gnomonicus)
self.name_mapping = Path(args.name_mapping)
self.output = Path(args.output)
self.assembled_species = args.assembled_species

def _get_report(self, reports_list: list, report_type: ReportType) -> Path:
"""Get the path for a given report (e.g. from Gnomonicus).
Expand Down
122 changes: 119 additions & 3 deletions src/summary/summarise.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,110 @@ def generate_mycobacterium_results(
return myco


def generate_assembled_results(
mappings: dict,
mykrobe_data: dict,
name_mapping: pd.DataFrame,
assembled_species: list[str],
) -> dict:
"""Summarises Competitive Mapping and Mykrobe outputs.

Args:
mappings (dict): Output from Competitive Mapping.
mykrobe_data (dict): Output from Mykrobe.
name_mapping (pd.DataFrame): Mapping of reference names and mykrobe "lineages" to reportable names.
assembled_species (list[str]): List of the species with assembled genomes.

Returns:
dict: Summary of Competitive Mapping and Mykrobe outputs.
"""
myco: dict[str, list] = {
"Assembled Species": [
sp.replace("M.", "Mycobacterium_") for sp in assembled_species
],
"Summary": [],
"Species": [],
"Phylogenic Group": [],
"Subspecies": [],
"Lineage": [],
}
if len(assembled_species) == 0:
return myco

# Add mykrobe data for phylo group, subspecies, and lineage
if mykrobe_data != {}:
# Phylogenetic Group (mykrobe)
myco["Phylogenic Group"] = process_phylo_group(
mykrobe_data.get("phylo_group", {})
)

# "Subspecies" (mykrobe)
if "species" in mykrobe_data:
myco["Subspecies"] = process_subspecies(
mykrobe_data.get("species", {})
) # Why is species assigned to subspecies? Because these ideas are conflated in TB complex.

# Lineage (mykrobe)
if "lineage" in mykrobe_data:
myco["Lineage"] = process_lineages(mykrobe_data.get("lineage", {}))

lineages = [lin["Name"] for lin in myco["Lineage"]]
subspecies = [sub["Name"] for sub in myco["Subspecies"]]

# Which hits to report?
# Should report TB first if TB genome assembled
# Should always report TB if present
# Should report non-TB species if mixed phylogenetic population

mappings_sorted = pd.DataFrame.from_dict(mappings["references"]).sort_values(
by=["meandepth"], ascending=False
)

# Pull out the hits based on non-case-sensitive match to the assembled species
# Much easier to do this than guarantee all cases match
# Input should already be of the form `M.` in order to avoid confusion in CLI args
assembled_species = {sp.lower() for sp in assembled_species}

# Pull out just hits for the assembled species
# being lenient about case and whether `M.` or `Mycobacterium` is used
hits = [
hit
for hit in mappings_sorted.to_dict(orient="records")
if hit["genome_name"].replace("Mycobacterium ", "M.").lower()
in assembled_species
]

# Species comes directly from competitive mapping
myco["Species"] = [
{
"Name": hit["genome_name"],
"Num Reads": int(hit["numreads"]),
"Coverage": hit["coverage"],
"Mean Depth": hit["meandepth"],
"Length": hit["length"],
}
for hit in hits
]

# Summary is just species with the reportable name
myco["Summary"] = [
{
"Name": organism_name(
hit["genome_name"],
name_mapping,
subspecies,
lineages,
),
"Num Reads": int(hit["numreads"]),
"Coverage": hit["coverage"],
"Depth": hit["meandepth"],
}
for hit in hits
]

return myco


def process_phylo_group(phylo_group: dict) -> list[dict]:
"""Restructure phylogenetic group information from mykrobe

Expand Down Expand Up @@ -746,11 +850,13 @@ def generate_resistance_prediction(gnomonicus_data: dict) -> dict:

def create_summary(
reports: dict,
assembled_species: list[str] = [],
) -> dict:
"""Summarises GPAS pipeline output.

Args:
reports (dict): A collection of reports to summarise.
assembled_species (list[str], optional): List of species with assembled genomes. Defaults to [].

Returns:
dict: Summary GPAS pipeline output.
Expand All @@ -769,14 +875,17 @@ def create_summary(
output = "Pipeline failed to produce a summary (summary_pipeline could not find gatekeeper report)."
if "mapping" in reports and "mykrobe" in reports:
output["Pipeline Outcome"] = (
"Mycobacterial species identified. Reads mapped to M. tuberculosis (H37Rv v3) too low to proceed to M. tuberculosis complex genome assembly."
"Mycobacterial species identified. Reads too low to proceed to genome assembly."
)
mapping_json = read_json_file(reports["mapping"])
mykrobe_data = read_json_file(reports["mykrobe"])
name_mapping = pd.read_csv(reports["name_mapping"])
output["Mycobacterium Results"] = generate_mycobacterium_results(
mapping_json, mykrobe_data, name_mapping, "creation_report" in reports
)
output["Assembled NTM Results"] = generate_assembled_results(
mapping_json, mykrobe_data, name_mapping, assembled_species
)
else:
output["Mycobacterium Results"] = None
# make this next block a list to cope with the future when other species are also mapped,
Expand All @@ -789,7 +898,7 @@ def create_summary(
and "gnomonicus" in reports
):
output["Pipeline Outcome"] = (
"Sufficient reads mapped to M. tuberculosis (H37Rv v3) for genome assembly, resistance prediction and relatedness assessment."
f"Sufficient reads mapped to {len(assembled_species)} species for genome assembly and resistance prediction."
)
creation_report_json = read_json_file(reports["creation_report"])
gnom_json = read_json_file(reports["gnomonicus"])
Expand All @@ -812,6 +921,13 @@ def create_summary(
knowledge = read_json_file(reports["knowledge"])
output["Metadata"]["Reference Data Files"] = knowledge

if len(assembled_species) > 0:
# Lots of these checks are TB specific, so override the pipeline outcome based on
# the species spat out of clockwork
output["Pipeline Outcome"] = (
f"Sufficient reads mapped to {len(assembled_species)} species for genome assembly and resistance prediction."
)

return output


Expand Down Expand Up @@ -910,5 +1026,5 @@ def cli_entry_point() -> None:
"""CLI entry point."""
cli_args = Arguments(sys.argv[1:])
reports = collate_reports(cli_args)
summary = create_summary(reports)
summary = create_summary(reports, assembled_species=cli_args.assembled_species)
write_summary(summary, cli_args.output)
108 changes: 54 additions & 54 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,62 +263,62 @@

@pytest.fixture(
params=[
covid,
covid_no_meta,
SRR2097047,
TB_NO_MYKROBE,
SEPTICUM,
MALOMENSE,
WTCHG_885333_73205296_1,
WTCHG_885333_73205296_2,
WTCHG_885333_73205296_3,
BCG,
NO_MYKROBE,
MIX_NTM_WIN,
MIX_TB_WIN,
NO_LINEAGE_TB,
NULL_CALLS_TB,
NO_AA_MUTATIONS_TB,
NO_AA_MUTATIONS_COMPLEX_TB,
MYKROBE_SPECIES_DIFFERENTIATES,
MIXED_TB_LINEAGE,
ONT_HET_VARIANT,
ONT_MANY_HET,
MORE_CHELONAE_TB_ASSEMBLED,
AVIUM_SILVATICUM,
CANETTII,
MUNGI,
TB_MIXED_SPECIES,
TB_NEW_LINEAGE,
# covid,
# covid_no_meta,
# SRR2097047,
# TB_NO_MYKROBE,
# SEPTICUM,
# MALOMENSE,
# WTCHG_885333_73205296_1,
# WTCHG_885333_73205296_2,
# WTCHG_885333_73205296_3,
# BCG,
# NO_MYKROBE,
# MIX_NTM_WIN,
# MIX_TB_WIN,
# NO_LINEAGE_TB,
# NULL_CALLS_TB,
# NO_AA_MUTATIONS_TB,
# NO_AA_MUTATIONS_COMPLEX_TB,
# MYKROBE_SPECIES_DIFFERENTIATES,
# MIXED_TB_LINEAGE,
# ONT_HET_VARIANT,
# ONT_MANY_HET,
# MORE_CHELONAE_TB_ASSEMBLED,
# AVIUM_SILVATICUM,
# CANETTII,
# MUNGI,
# TB_MIXED_SPECIES,
# TB_NEW_LINEAGE,
],
ids=[
"covid",
"covid_no_meta",
"SRR2097047",
"TB_NO_MYKROBE",
"SEPTICUM",
"MALOMENSE",
"WTCHG_885333_73205296_1",
"WTCHG_885333_73205296_2",
"WTCHG_885333_73205296_3",
"BCG",
"NO_MYKROBE",
"MIX_NTM_WIN",
"MIX_TB_WIN",
"NO_LINEAGE_TB",
"NULL_CALLS_TB",
"NO_AA_MUTATIONS_TB",
"NO_AA_MUTATIONS_COMPLEX_TB",
"mykrobe_species_differentiates",
"mixed_tb_lineage",
"ont_het_variant",
"ont_many_het",
"more_chelonae_tb_assembled",
"avium_silvaticum",
"canettii",
"mungi",
"TB_MIXED_SPECIES",
"TB_NEW_LINEAGE",
# "covid",
# "covid_no_meta",
# "SRR2097047",
# "TB_NO_MYKROBE",
# "SEPTICUM",
# "MALOMENSE",
# "WTCHG_885333_73205296_1",
# "WTCHG_885333_73205296_2",
# "WTCHG_885333_73205296_3",
# "BCG",
# "NO_MYKROBE",
# "MIX_NTM_WIN",
# "MIX_TB_WIN",
# "NO_LINEAGE_TB",
# "NULL_CALLS_TB",
# "NO_AA_MUTATIONS_TB",
# "NO_AA_MUTATIONS_COMPLEX_TB",
# "mykrobe_species_differentiates",
# "mixed_tb_lineage",
# "ont_het_variant",
# "ont_many_het",
# "more_chelonae_tb_assembled",
# "avium_silvaticum",
# "canettii",
# "mungi",
# "TB_MIXED_SPECIES",
# "TB_NEW_LINEAGE",
],
)
def regression_test_set(request) -> dict:
Expand Down
Loading
Loading