Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ area:packaging:
- "src-tauri/tauri.conf.json"
- "CITATION.cff"

area:galaxy:
- changed-files:
- any-glob-to-any-file:
- "galaxy/**"

# Everything under .github, not a list of the files there. Naming them one by one
# would leave this file itself uncovered, so the pull request adding a rule would
# arrive with no label, which is a small thing that says exactly how such a list
Expand Down
21 changes: 21 additions & 0 deletions galaxy/tools/pathotypr/.shed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: pathotypr
owner: iuc
description: Lineage classification and marker-driven genotyping from assemblies or raw reads
long_description: |
pathotypr classifies microbial genomes into lineages and genotypes them
against user-defined marker panels. It works from assembled genomes (FASTA)
and from raw reads (FASTQ) without aligning them, and it carries no assumption
about the organism: the marker panel supplied defines what is typed.
categories:
- Sequence Analysis
- Variant Analysis
remote_repository_url: https://github.com/galaxyproject/tools-iuc/tree/main/tools/pathotypr
homepage_url: https://pathogenomics-lab.github.io/pathotypr/
type: unrestricted
auto_tool_repositories:
name_template: "{{ tool_id }}"
description_template: "Wrapper for the pathotypr tool {{ tool_name }}."
suite:
name: "suite_pathotypr"
description: A suite of Galaxy tools for the five pathotypr commands.
type: repository_suite_definition
74 changes: 74 additions & 0 deletions galaxy/tools/pathotypr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Galaxy wrappers for pathotypr

Five tools, one per subcommand, sharing `macros.xml`.

| Tool | Wraps | Outputs |
|---|---|---|
| `pathotypr_train` | `train` | model, and optionally the two feature-importance reports |
| `pathotypr_predict` | `predict` | predictions TSV |
| `pathotypr_classify` | `classify` | per-marker hits, summary, optionally masked FASTA |
| `pathotypr_split_fastq` | `split-fastq` | per-marker calls, summary |
| `pathotypr_match` | `match` | best-match report |

## Running the tests

```bash
python3 -m venv .venv && .venv/bin/pip install planemo
.venv/bin/planemo lint tools/pathotypr/*.xml
.venv/bin/planemo test tools/pathotypr/
```

`planemo test` resolves `pathotypr` from Bioconda. On a machine where that
package has no build for the local platform, put a `pathotypr` binary on `PATH`
and add `--no_dependency_resolution`; that still exercises the command lines,
the outputs and the assertions, and leaves only the dependency resolution to CI.

## The test data

Everything under `test-data/` is synthetic and generated deterministically:
a 2 kb reference, eight markers, two samples, paired reads at about 18x, and a
twelve-genome training set in two classes. 64 KB in total.

It is not filler. The generator asserts that **every marker k-mer occurs exactly
once in the reference**, in both orientations, because a marker that matched in
two places would make the tests pass for the wrong reason. The expected calls
follow from how the samples were built:

- `sample1` carries the L2 to L2.1 to L2.1.1 path plus the RIF marker, so
`classify` calls L2 and reports `rpoB` / `S450L`.
- `sample2` carries L3 to L3.2 plus the INH marker.
- `split-fastq` on `sample1`'s reads produces the **same summary** as `classify`
on its assembly, which is the property the marker format promises.
- `predict` assigns the two held-out queries to L2 and L3.
- `match` picks `ref_A`, which is `sample1`'s own genome.

## Three things the wrappers have to work around

1. **Sample names come from filenames.** Galaxy datasets arrive as
`dataset_NNN.dat`, so `classify` and `split-fastq` would name their outputs
after a Galaxy id. Each input is symlinked to a stable name first.
2. **`match` echoes input paths into its report.** Same fix, otherwise the first
column is full of absolute paths that differ between instances.
3. **`--min-alt-percent` is an integer.** A Galaxy `float` parameter renders
`95.0`, which the tool rejects outright. It is declared as an integer, so
fractional thresholds such as 99.5% are not available.

## Submitting to the IUC

The directory layout matches `galaxyproject/tools-iuc`, so submission is a copy:

```bash
cp -r galaxy/tools/pathotypr <tools-iuc>/tools/pathotypr
```

then a pull request against that repository. Their CI runs the same planemo
lint and tests on Linux, where the Bioconda package resolves.

## Not done yet

Marker panels and models come from the history. A **data manager** plus a
`.loc` table would let an administrator install the published MTBC panels once
so users pick them from a dropdown, which is how Galaxy normally handles
reference data. That is a separate piece of work and was left out deliberately
rather than half-built: shipping a data table with nothing to populate it moves
the burden to administrators without helping anyone.
85 changes: 85 additions & 0 deletions galaxy/tools/pathotypr/macros.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<macros>
<token name="@TOOL_VERSION@">1.0.2</token>
<token name="@VERSION_SUFFIX@">0</token>
<token name="@PROFILE@">23.0</token>

<xml name="requirements">
<requirements>
<requirement type="package" version="@TOOL_VERSION@">pathotypr</requirement>
</requirements>
</xml>

<xml name="xrefs">
<xrefs>
<xref type="bio.tools">pathotypr</xref>
</xrefs>
</xml>

<xml name="version_command">
<version_command>pathotypr --version</version_command>
</xml>

<xml name="citations">
<citations>
<citation type="doi">10.64898/2026.03.24.714002</citation>
<citation type="doi">10.5281/zenodo.19210044</citation>
</citations>
</xml>

<!-- Threads. Galaxy sets GALAXY_SLOTS; without it a tool helps itself to
every core on a shared node. -->
<token name="@THREADS@">-t \${GALAXY_SLOTS:-1}</token>

<xml name="kmer_size" token_default="31" token_help="">
<param argument="--kmer-size" type="integer" min="1" max="31" value="@DEFAULT@"
label="k-mer size"
help="Must be between 1 and 31. An odd value centres the variant inside the k-mer, so 31 and 21 are the usual choices. @HELP@"/>
</xml>

<!-- Marker panels and the reference they were called against. They travel
together on purpose: positions are 1-based coordinates on that exact
sequence, and a panel read against a different assembly produces
confident, wrong calls with no warning. -->
<xml name="markers_and_reference">
<param argument="--markers" type="data" format="tabular" label="Marker panel"
help="Tab-separated: position, REF, ALT, then one lineage level per column. The published MTBC panels are on Zenodo, doi:10.5281/zenodo.19210044."/>
<param argument="--reference" type="data" format="fasta" label="Reference genome"
help="Single-record FASTA. This must be the assembly the marker positions were called against."/>
</xml>

<xml name="nested_classification">
<param argument="--nested-classification" type="boolean" truevalue="--nested-classification"
falsevalue="" checked="false" label="Nested lineage classification"
help="Requires a panel whose lineage hierarchy is spread across one column per level."/>
</xml>

<token name="@MARKER_HELP@">
**Marker file format**

Tab-separated, no header required. Column order:

::

position REF ALT level1 level2 ... (empty) gene mutation

Positions are 1-based on the reference genome you supply. The lineage hierarchy
runs from column 4 until the first empty cell; whatever follows that empty cell
is read as gene and mutation. SNPs and equal-length MNVs work everywhere;
indels are accepted by *classify* and skipped by *split-fastq*.

The full rules are at
https://pathogenomics-lab.github.io/pathotypr/marker_format/

Ready-made panels for the *M. tuberculosis* complex, and a pre-trained model,
are published on Zenodo: https://doi.org/10.5281/zenodo.19210044
</token>

<token name="@ORGANISM_NOTE@">
**A note on other organisms**

Nothing in pathotypr is hard-coded to one organism: the panel you supply defines
what is typed. In practice it has only been validated on the *M. tuberculosis*
complex, so treat other organisms as exploratory and check the calls against a
truth set you trust.
</token>
</macros>
160 changes: 160 additions & 0 deletions galaxy/tools/pathotypr/pathotypr_classify.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<tool id="pathotypr_classify" name="pathotypr classify" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="@PROFILE@">
<description>genotypes assembled genomes against a marker panel</description>
<macros>
<import>macros.xml</import>
</macros>
<expand macro="xrefs"/>
<expand macro="requirements"/>
<expand macro="version_command"/>
<command detect_errors="exit_code"><![CDATA[
## Galaxy datasets arrive as dataset_NNN.dat, and pathotypr takes the sample
## name from the filename. Link each one to the element identifier so the
## output names the sample rather than a Galaxy id.
## The extension is stripped before it is re-added, or an identifier that
## already ends in .fasta becomes sample.fasta.fasta, and that name is what the
## summary reports back in the genome column.
#import re
#set $safe = lambda s: re.sub('[.](fasta|fa|fna|fas)$', '', re.sub('[^A-Za-z0-9_.-]', '_', str(s)))
#for $i, $g in enumerate($genomes)
ln -s '$g' '${safe($g.element_identifier)}.fasta' &&
#end for
#if $annotation.use_gff == 'yes'
#for $i, $f in enumerate($annotation.gff_files)
ln -s '$f' '${safe($f.element_identifier)}.gff' &&
#end for
#end if

pathotypr classify
--markers '$markers'
--reference '$reference'
--input-files
#for $g in $genomes
'${safe($g.element_identifier)}.fasta'
#end for
#if $annotation.use_gff == 'yes'
--gff-files
#for $f in $annotation.gff_files
'${safe($f.element_identifier)}.gff'
#end for
#end if
--output-prefix 'result'
--kmer-size $kmer_size
--min-flank-bases $min_flank_bases
$nested_classification
$output_masked_fasta
@THREADS@
]]></command>
<inputs>
<param name="genomes" type="data" format="fasta" multiple="true" label="Assembled genomes"
help="One or more FASTA assemblies to genotype."/>
<expand macro="markers_and_reference"/>
<conditional name="annotation">
<param name="use_gff" type="select" label="Annotate hits with a GFF">
<option value="no" selected="true">No</option>
<option value="yes">Yes</option>
</param>
<when value="no"/>
<when value="yes">
<param name="gff_files" type="data" format="gff,gff3" multiple="true" label="GFF annotation"
help="Matched to the genomes by filename, so the names must correspond."/>
</when>
</conditional>
<expand macro="kmer_size" default="31"/>
<param argument="--min-flank-bases" type="integer" min="0" value="10"
label="Minimum flanking bases"
help="Bases required on each side of the allele inside the marker k-mer. At the defaults this caps usable alleles at 11 bp."/>
<expand macro="nested_classification"/>
<param argument="--output-masked-fasta" type="boolean" truevalue="--output-masked-fasta"
falsevalue="" checked="false" label="Also write masked FASTA"
help="Writes each genome with the marker positions replaced by N."/>
</inputs>
<outputs>
<data name="detailed" format="tabular" label="${tool.name} on ${on_string}: per-marker hits"
from_work_dir="result.tsv"/>
<data name="summary" format="tabular" label="${tool.name} on ${on_string}: summary"
from_work_dir="result_summary.tsv"/>
<collection name="masked" type="list" label="${tool.name} on ${on_string}: masked FASTA">
<filter>output_masked_fasta</filter>
<discover_datasets pattern="(?P&lt;designation&gt;.+)_masked\.fasta" format="fasta" directory="."/>
</collection>
</outputs>
<tests>
<!-- One genome carrying the L2 -> L2.1 -> L2.1.1 path and a RIF marker. -->
<test expect_num_outputs="2">
<param name="genomes" value="sample1.fasta" ftype="fasta"/>
<param name="markers" value="markers.tsv" ftype="tabular"/>
<param name="reference" value="reference.fasta" ftype="fasta"/>
<output name="summary">
<assert_contents>
<has_text text="major_lineage"/>
<has_text text="L2;L2.1;L2.1.1"/>
</assert_contents>
</output>
<output name="detailed">
<assert_contents>
<has_text text="rpoB"/>
<has_text text="S450L"/>
<has_n_lines n="5"/>
</assert_contents>
</output>
</test>
<!-- Two genomes at once, and the second one is a different lineage. -->
<test expect_num_outputs="2">
<param name="genomes" value="sample1.fasta,sample2.fasta" ftype="fasta"/>
<param name="markers" value="markers.tsv" ftype="tabular"/>
<param name="reference" value="reference.fasta" ftype="fasta"/>
<param name="kmer_size" value="21"/>
<output name="summary">
<assert_contents>
<has_text text="L2"/>
<has_text text="L3"/>
<has_n_lines n="3"/>
</assert_contents>
</output>
</test>
<!-- The masked FASTA is a discovered collection, so its naming deserves
a test of its own. The input genomes contain no N at all, so an N in
the output can only have come from masking. -->
<test expect_num_outputs="3">
<param name="genomes" value="sample1.fasta" ftype="fasta"/>
<param name="markers" value="markers.tsv" ftype="tabular"/>
<param name="reference" value="reference.fasta" ftype="fasta"/>
<param name="output_masked_fasta" value="true"/>
<output_collection name="masked" type="list" count="1">
<element name="sample1" ftype="fasta">
<assert_contents>
<has_text text="&gt;sample1"/>
<has_text text="N"/>
</assert_contents>
</element>
</output_collection>
</test>
</tests>
<help><![CDATA[
**What it does**

`pathotypr classify` calls a panel of diagnostic variants directly on assembled
genomes. For each marker it builds a k-mer centred on the variant, looks for it
in the assembly, and reports which allele was found. Hits are then collapsed
into a lineage call per genome.

It produces two tables:

- **per-marker hits**: one row per marker found, with the k-mer, its position,
the reference and alternate alleles, the lineage the marker belongs to and,
where the panel carries them, the gene and amino-acid change.
- **summary**: one row per genome, with the count of hits per lineage and the
major lineage call.

@MARKER_HELP@

@ORGANISM_NOTE@

**Choosing between classify and split-fastq**

Use this tool when you have assemblies. Use *pathotypr split-fastq* when you
have raw reads and would rather not assemble them. Both read the same panel and
agree on the call; only the input differs.
]]></help>
<expand macro="citations"/>
</tool>
Loading