Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a67c666
chore: bump version to 1.0.1
Paururo Jul 19, 2026
79ef44e
fix(classify): support multi-contig assemblies in --input-list
Paururo Jul 19, 2026
92adb6a
fix(paired-end): keep all R1/R2 chunks and avoid sample-name collisions
Paururo Jul 19, 2026
92d757c
fix: validate k-mer size in match and split-fastq subcommands
Paururo Jul 19, 2026
df7cffa
fix(core): assorted robustness fixes in path, parsing and IO handling
Paururo Jul 19, 2026
5e81e39
fix(gui): make per-marker output prefixes unique across marker files
Paururo Jul 19, 2026
b0bba54
fix(gui): block IPv4-mapped IPv6 addresses in the SSRF filter
Paururo Jul 19, 2026
7468c0b
fix(frontend): escape quotes in escapeHtml for attribute-safe output
Paururo Jul 19, 2026
ce35c94
fix(frontend): correctness fixes in demo loader, header detection, pr…
Paururo Jul 19, 2026
7877303
fix(gui): close DNS-rebinding gap in download_file with a validating …
Paururo Jul 19, 2026
dd0d968
docs: add MkDocs Material documentation site
Paururo Jul 19, 2026
e1d6340
docs: use the dark workflow diagram in dark mode on the home page
Paururo Jul 19, 2026
91b711a
docs: expand and sharpen command, reference and how-to pages
Paururo Jul 19, 2026
8a32acb
docs: polish the algorithm pages and fix inherited inaccuracies
Paururo Jul 19, 2026
1d64d4a
docs: match the theme to the logo (white header, brand-red accent)
Paururo Jul 19, 2026
4ac654e
fix(paired-end): keep same-named FASTQs from different folders apart
Paururo Jul 24, 2026
28aa2ed
fix(classify): scan both strands and count each marker once per genome
Paururo Jul 24, 2026
1c51634
fix(classify): keep unclassified genomes in the summary, and guard ou…
Paururo Jul 24, 2026
58bd6a8
fix(core): stop leaving partial outputs behind and deleting the wrong…
Paururo Jul 24, 2026
31cd89b
fix(gui): stop shredding nested lineages, losing the record name and …
Paururo Jul 24, 2026
6fe1889
fix(excel): make the summary workbook agree with its TSV and drop cor…
Paururo Jul 24, 2026
eeebde9
fix(gui): stop shifting classify sample positions by one base
Paururo Jul 24, 2026
13c106d
fix(classify): only mask FASTAs that are in reference coordinates
Paururo Jul 24, 2026
c9fd753
fix(gui): route dropped files to empty inputs and clear stale result …
Paururo Jul 24, 2026
3b5f81e
fix(gui): refresh the stale hardcoded version fallback
Paururo Jul 24, 2026
2cb6056
feat(gui): clinical resistance profile, lineage levels and allele-fra…
Paururo Jul 24, 2026
0cd8383
feat(gui): detect mixed infections from fixed markers on divergent li…
Paururo Jul 24, 2026
3638996
feat(gui): show the resistance and lineage panels for classify too
Paururo Jul 24, 2026
7d0be0a
fix(gui): describe a resistance run by its drugs, not by a raw marker…
Paururo Jul 24, 2026
8836c3a
feat(gui): resistance matrix and lineage composition charts for batch…
Paururo Jul 24, 2026
d5fafd8
feat(gui): depth vs allele fraction quality-control scatter
Paururo Jul 24, 2026
cde2ea1
fix(classify): actually keep unclassified genomes in the summary
Paururo Jul 24, 2026
164e040
fix(classify): make --input-files usable on its own and accept severa…
Paururo Jul 24, 2026
2d1c7ef
docs: fix factual errors found by auditing every page against the code
Paururo Jul 24, 2026
4fbabbd
docs: apply the remaining low-severity audit corrections
Paururo Jul 24, 2026
33ba803
fix(gui): make cargo tauri dev start without a frontend dev server
Paururo Jul 25, 2026
49d8b0d
docs: move the organism warning below the workflow figure and shorten it
Paururo Jul 25, 2026
9c80883
docs: cut the site's image payload and add citation, edit links and d…
Paururo Jul 25, 2026
69f7717
docs: keep the brand-red header rule in dark mode and on narrow screens
Paururo Jul 25, 2026
f521216
fix(gui): stop shipping 8 MB of duplicated artwork inside the app
Paururo Jul 25, 2026
0393746
chore(gui): drop the unused legacy app.js from the bundle
Paururo Jul 25, 2026
4b84ad3
docs: add Bioconda version and download badges to the README
Paururo Jul 25, 2026
a381954
docs: fix broken README links and point readers at the documentation …
Paururo Jul 25, 2026
1a1ba98
Delete logo/pathotypr.svg
Paururo Jul 25, 2026
e2b819b
Add files via upload
Paururo Jul 25, 2026
fdd9959
docs: point the edit links at main ahead of the merge
Paururo Jul 25, 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
53 changes: 53 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Deploy docs

on:
push:
branches:
- main
- 1.0.1
paths:
- docs/**
- mkdocs.yml
- .github/workflows/docs.yml
workflow_dispatch:

permissions:
contents: write

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Cache MkDocs build
uses: actions/cache@v4
with:
key: mkdocs-material-${{ github.sha }}
path: .cache
restore-keys: |
mkdocs-material-

# Cairo and pango are what the social-card plugin renders through.
- name: Install image libraries for social cards
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libcairo2-dev libfreetype6-dev libffi-dev \
libjpeg-dev libpng-dev libz-dev

- name: Install dependencies
run: pip install -r docs/requirements.txt

# CI is set by GitHub Actions, which is what switches social cards on.
- name: Build (strict)
run: mkdocs build --strict

- name: Deploy to GitHub Pages
run: mkdocs gh-deploy --force
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/target/
**/target/
*.DS_Store

# MkDocs build output & cache
/site/
.cache/
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors:
given-names: Mireia
orcid: "https://orcid.org/0000-0003-2078-1032"
affiliation: "Institute for Integrative Systems Biology (I2SysBio), CSIC - University of Valencia"
version: 1.0.0
version: 1.0.1
date-released: 2026-03-24
license: AGPL-3.0-only
repository-code: "https://github.com/PathoGenOmics-Lab/pathotypr"
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 0 additions & 35 deletions docs/README.md

This file was deleted.

18 changes: 16 additions & 2 deletions docs/algorithms/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Algorithm Documentation
# Algorithm documentation

This directory describes the algorithms and data structures behind each pathotypr module. For CLI usage and options, see the [command docs](../).
**This section explains the algorithms and data structures behind each pathotypr module — the core ideas, not the command-line switches.**

Each page below focuses on a single module: the concept it implements, the data structures it relies on, and how the pieces fit together end to end. For command-line usage and options, see the command guides in [See also](#see-also).

!!! tip
The table is ordered roughly along the pathotypr pipeline — from turning k-mers into feature vectors, through training and prediction, to marker- and reference-based calling. Reading it top to bottom is a good first pass.

| Module | Document | Core Idea |
|---|---|---|
Expand All @@ -11,3 +16,12 @@ This directory describes the algorithms and data structures behind each pathotyp
| Marker Genotyping | [marker-genotyping.md](marker-genotyping.md) | Diagnostic k-mers + Bloom filter for FASTQ scanning |
| Reference Matching | [reference-matching.md](reference-matching.md) | K-mer containment scoring with streaming batches |
| Assembly Classification | [assembly-classification.md](assembly-classification.md) | Marker calling on FASTA assemblies with GFF annotation |

## See also

- [train](../train.md)
- [predict](../predict.md)
- [classify](../classify.md)
- [split-fastq](../split-fastq.md)
- [match](../match.md)
- [Home](../index.md)
89 changes: 54 additions & 35 deletions docs/algorithms/assembly-classification.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Assembly-Based Marker Classification
# Assembly-based marker classification

## Overview
**A walkthrough of how `classify` matches known variant markers in assembled genomes: diagnostic k-mer generation, genome scanning, optional GFF annotation, lineage classification, and output.**

The `classify` command calls known variant markers in assembled genomes (FASTA). Unlike the FASTQ `split-fastq` approach (which uses diagnostic k-mers on raw reads), this module works on full-length assemblies and supports SNPs, MNVs, **and indels**.
The `classify` command calls known variant markers directly in assembled genomes (FASTA), supporting SNPs, MNVs, and indels.

!!! info "Assemblies, not reads"
Unlike the FASTQ `split-fastq` path, which screens raw reads with diagnostic k-mers, this module scans full-length assemblies — so it can call indels and, with a GFF, report amino-acid changes.

## Architecture

```
```text
Marker TSV + Reference FASTA ──→ Generate marker k-mers ──→ MarkerIndex
Query FASTA(s) ──→ Scan each genome for markers ──────────────→ MarkerMatch results
Expand All @@ -18,95 +21,105 @@ Query FASTA(s) ──→ Scan each genome for markers ────────
Summary TSV + Excel
```

## Step 1: Marker K-mer Generation
## Step 1: Marker k-mer generation

For each marker in the TSV (position, REF, ALT):

1. Extract flanking sequence from the reference genome (minimum 10 bp each side, configurable via `--min-flank-bases`)
2. Build a diagnostic k-mer centered on the variant:
- For SNPs/MNVs: k-mer contains the variant at the center with equal flanks
- For indels: separate ref and alt k-mers with adjusted flank lengths to maintain k-mer size
- For SNPs/MNVs: the k-mer holds the ALT allele at the center with equal flanks
- For indels: the ALT allele is placed in the k-mer and the flank lengths are adjusted so the k-mer keeps its length of k

3. Index k-mers for fast lookup:
- **Encoded index** (`MarkerIndex::Encoded`): For k ≤ 32, k-mers are 2-bit encoded into `u64` keys using needletail's `BitNuclKmer`. Lookup is a single hash table query.
- **Text index** (`MarkerIndex::Text`): For k > 32, k-mers are stored as strings. Fallback for unusually long k-mers.
- **Encoded index** (`MarkerIndex::Encoded`): For k ≤ 32, marker k-mers are 2-bit encoded into `u64` keys with a hand-rolled `encode_kmer_2bit` (needletail's `bit_kmers` is used to generate the *query* k-mers during genome scanning). Lookup is a single hash table query.
- **Text index** (`MarkerIndex::Text`): Used when a marker k-mer contains a non-ACGT base that cannot be 2-bit encoded; k-mers are stored as strings. (The code also falls back for k > 32, but `validate_kmer_size` caps k at 31, so that branch is unreachable in practice.)

### Index Structure
### Index structure

```rust
enum MarkerIndex {
Encoded(FxHashMap<u64, MarkerKmerEntry>), // k ≤ 32: 2-bit encoded
Text(FxHashMap<String, MarkerKmerEntry>), // k > 32: string keys
Text(FxHashMap<String, MarkerKmerEntry>), // k > 32 or non-ACGT: string keys
}
```

Each entry stores the marker metadata: position, alleles, lineage, gene name, and flanking information needed to distinguish REF from ALT matches.
Each entry stores the marker metadata: position, alleles, lineage, gene name, and the flank lengths needed to locate the variant bases within a matching k-mer.

## Step 2: Genome Scanning
## Step 2: Genome scanning

For each query genome (parallelized across genomes with rayon):

1. Read the genome sequence with needletail
2. Extract all k-mers (canonical, same k as the marker k-mers)
3. Look up each k-mer in the MarkerIndex
4. For matches, determine whether the match supports REF or ALT allele by comparing the variant-spanning region
1. Read every record (contig) of the genome with needletail
2. Scan each contig on **both strands** — the forward sequence and its reverse complement — because contig orientation in a draft assembly is arbitrary
3. Extract every k-mer of the same size k used for the marker k-mers and look it up in the `MarkerIndex`; a marker seen on several contigs, or on both strands, counts once
4. Because only the ALT (variant) k-mer is indexed, any hit means the genome carries the variant allele; the entry's flank lengths then locate the exact variant-spanning bases

### Match Result
### Match result

```rust
struct MarkerMatch {
position: usize, // Genomic position (1-based)
kmer: String, // the diagnostic (ALT) k-mer that matched
genome_position: usize, // 0-based start of the match in the genome
ref_position: usize, // 1-based marker position in the reference
lineage: String,
ref_allele_len: usize,
alt_allele_len: usize,
left_flank_len: usize, // flank bases before the variant in the k-mer
ref_allele: String,
alt_allele: String,
found_allele: String, // What was actually observed
lineage: String,
gene: Option<String>,
mutation: Option<String>,
}
```

## Step 3: Annotation (Optional)
## Step 3: Annotation (optional)

When GFF files are provided, each called variant is annotated:

1. **GFF parsing**: Extracts CDS features with strand, phase, and locus tags using an interval tree (`rust_lapper::Lapper`) for O(log n) position lookups
1. **GFF parsing**: Reads `CDS` features and stores each one's strand and gene name (taken from the `gene`, `locus_tag`, `Name`, or `ID` attribute) in an interval tree (`rust_lapper::Lapper`) for O(log n) position lookups
2. **Amino acid translation**: Determines the codon position, extracts the codon from the reference and query, translates both to amino acids
3. **Output**: `gene_name:REF_AA{position}ALT_AA` notation (e.g., `rpoB:S450L`)
3. **Output**: for each affected codon the change is written as `REF_AA{position}ALT_AA(REF_codon>ALT_codon)` using three-letter amino-acid codes (e.g. `Ser450Leu(TCG>TTG)`); synonymous changes get a trailing ` syn`, an MNV spanning several codons joins its per-codon changes with `;`, and a length-changing (indel) variant is reported as `frameshift_pos{N}`. The gene name and first affected codon are written to the separate `Gene` and `AA_Pos` columns.

Supports:

- Forward and reverse strand genes
- Multi-nucleotide variants spanning codon boundaries
- Synonymous vs. non-synonymous classification

## Step 4: Lineage Classification
## Step 4: Lineage classification

Two modes:

### Simple (default)
- Each marker votes for its deepest lineage level
- The lineage with the most ALT-supporting markers wins
- Resolves nested lineages by walking the lineage hierarchy
- Each matched marker increments the count of its full lineage path **and of every ancestor prefix** (`L4`, `L4;L4.9`, `L4;L4.9;L4.9.1`)
- The path with the highest count wins, which is normally the shallowest level
- Ties are broken deterministically by lineage name

### Nested (`--nested-classification`)
- Hierarchical classification: first resolves major lineage, then sub-lineage
- Only considers markers belonging to the parent lineage when resolving sub-lineages
- More robust for datasets with uneven marker distribution across lineages

!!! note "Nested vs. simple"
Nested classification is more robust for datasets with an uneven marker distribution across lineages.

## Step 5: Output

### Summary TSV (`{prefix}_summary.tsv`)
Per-genome: sample name, predicted lineage, marker counts (REF/ALT/missing), percentage resolved.
Per genome, three columns: `genome`, `lineage:count` (marker tallies at **every** hierarchical level — each ancestor path prefix is counted separately, space-separated), and `major_lineage` (the final call).

### Detailed TSV (`{prefix}_details.tsv`)
Per-marker-per-genome: position, alleles, found allele, lineage, gene, amino acid change.
### Detailed TSV (`{prefix}.tsv`)
One row per matched marker per genome, with columns: `genome`, `k-mer`, `k-merPOS`, `SNPgenome`, `SNPreference`, `REF`, `ALT`, `lineage`, `Gene`, `Gene_Start`, `Gene_End`, `AA_Pos`, `AA_Change`.

### Masked FASTA (optional, `--output-masked-fasta`)
Query sequences with marker positions replaced by `N`. Useful for downstream phylogenetic analysis where marker sites should be excluded (e.g., removing sites under selection before building neutral phylogenies).
Query sequences with marker positions replaced by `N`, written as `<input-stem>_masked.fasta`. Only produced for sequences in reference coordinates (records whose length matches the reference).

!!! tip "Masked output"
Useful for downstream phylogenetic analysis where marker sites should be excluded — for example, removing sites under selection before building neutral phylogenies.

### Excel (optional, `--excel`)
Same data as TSV with conditional formatting for confidence metrics.
Same data as the TSV outputs, with a frozen header row and an autofilter. No conditional formatting is applied — classify has no confidence column.

## Key Differences from FASTQ Mode (`split-fastq`)
## Key differences from FASTQ mode (`split-fastq`)

| Aspect | `classify` (FASTA) | `split-fastq` (FASTQ) |
|---|---|---|
Expand All @@ -126,3 +139,9 @@ Same data as TSV with conditional formatting for confidence metrics.
| Scan one genome | O(L × log M) where L = genome length | O(L + M) |
| GFF annotation | O(V × log G) where V = variants, G = genes | O(G) |
| Total (N genomes) | O(N × L × log M) / num_threads | O(L + M + G) |

## See also

- [classify](../classify.md)
- [Marker format](../marker_format.md)
- [Input formats](../input-formats.md)
34 changes: 24 additions & 10 deletions docs/algorithms/feature-hashing.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Feature Hashing (The Hashing Trick)
# Feature hashing (the hashing trick)

**pathotypr converts each genome into a fixed-length numerical vector by hashing its k-mers into a fixed set of buckets — no explicit vocabulary required.**

This page explains why a vocabulary-based encoding does not scale, how the hashing trick replaces it, and how important buckets can be traced back to the k-mers and genomic coordinates they came from.

## Problem

Representing genomic sequences as numerical vectors for machine learning requires counting k-mers — short subsequences of length *k*. A naive vocabulary-based approach needs to enumerate all possible k-mers (4^k = 4 billion for k=16) or build a dictionary from the data (dataset-dependent, expensive to store).

## Solution: Feature Hashing
## Solution: feature hashing

pathotypr uses the **hashing trick** (Weinberger et al., 2009): each k-mer is hashed into a fixed set of buckets using a bitmask, bypassing the need for an explicit vocabulary.

```
```text
Sequence: ACTGACTGCTA...
↓ extract k-mers (k=21)
ACTGACTGCTA... → hash → bucket_idx = hash & 0xFFFFF
Expand All @@ -20,37 +24,47 @@ Sparse vector: [(bucket_42, 3.0), (bucket_1087, 1.0), ...]

1. **K-mer extraction**: Uses [needletail](https://github.com/onecodex/needletail)'s `bit_kmers()` which encodes each k-mer as a 2-bit packed `u64`. This handles canonical k-mers (choosing the lexicographically smaller of forward and reverse complement) automatically.

2. **Hashing**: The bucket index is simply `bitkmer.0 & mask` where `mask = num_buckets - 1`. Since needletail's 2-bit encoding already acts as a hash function (deterministic, uniform for biological sequences), no additional hash function is needed.
2. **Hashing**: The bucket index is simply `bitkmer.0 & mask` where `mask = num_buckets - 1`.

3. **Bucket count**: 2^20 = 1,048,576 buckets. The power-of-two constraint enables bitmask indexing (`& mask` instead of `% modulus`).
3. **Bucket count**: 2^20 = 1,048,576 buckets.

4. **Sparse representation**: Each sequence produces a `Vec<(usize, f32)>` — only non-zero buckets are stored, sorted by bucket index. This is critical for efficiency: a typical 4.4 Mb genome at k=21 produces ~4.4M k-mers but only ~800K–1M unique bucket entries.

### Why This Works
!!! info "Why no extra hash function is needed"
Since needletail's 2-bit encoding already acts as a hash function (deterministic, uniform for biological sequences), no additional hash function is needed. The power-of-two constraint enables bitmask indexing (`& mask` instead of `% modulus`).

### Why this works

- **Stateless**: The same hash function is applied at train and predict time. No vocabulary to store or update.
- **Collisions are acceptable**: Multiple k-mers can map to the same bucket. With 1M buckets and ~4M k-mers per genome, collisions occur but empirically don't degrade classification accuracy because the Random Forest learns on aggregate count patterns, not individual k-mers.
- **Fixed dimensionality**: All genomes map to the same 1M-dimensional space, regardless of genome size or k-mer diversity.

### Reverse Mapping
!!! note "Collisions are acceptable"
Multiple k-mers can map to the same bucket. With 1M buckets and ~4M k-mers per genome, collisions occur but empirically don't degrade classification accuracy because the Random Forest learns on aggregate count patterns, not individual k-mers.

### Reverse mapping

For feature importance analysis, pathotypr can reverse-map important buckets back to the original k-mers and their genomic coordinates:

1. **`reverse_map_buckets()`**: Re-scans all training sequences, collects k-mers that hash into important buckets. Used for the importance TSV.
2. **`reverse_map_with_coords()`**: Same but also records (sequence_index, 1-based position) for each hit. Used for the genomic coordinates TSV.

### Complexity
## Complexity

| Operation | Time | Space |
|---|---|---|
| Vectorize one sequence | O(L) where L = sequence length | O(min(4^k, B)) where B = buckets |
| Vectorize N sequences | O(N × L), parallelized with rayon | O(N × nnz) sparse |
| Reverse map | O(N × L × \|targets\|) | O(\|targets\| × hits) |

### Parameters
## Parameters

| Parameter | Value | Rationale |
|---|---|---|
| `num_buckets` | 2^20 = 1,048,576 | Balances collision rate vs. model size. Bucket count >> number of classes ensures discriminative power |
| Default k (train) | 21 | Long enough for specificity in bacterial genomes (4^21 ≈ 4.4 × 10^12 >> genome size), short enough that SNPs create distinguishable k-mer patterns |
| Default k (genotyping) | 31 | Longer k for higher specificity in marker-based genotyping where false positives are costly |

## See also

- [train](../train.md)
- [predict](../predict.md)
Loading
Loading