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
2c56bcd
docs(design): add annotation-lifting manifesto
mmolari Jun 11, 2026
56ab456
feat(annotate): add internal Feature annotation model
mmolari Jun 11, 2026
2d4a25e
feat(annotate): add GFF reader
mmolari Jun 11, 2026
68aa077
feat(annotate): add GenBank reader
mmolari Jun 11, 2026
aa52c0e
feat(annotate): match annotation seqids to graph paths
mmolari Jun 11, 2026
6664e44
docs(annotate): add implementation notes for the annotation feature
mmolari Jun 11, 2026
2ce4ea8
fix(annotate): tolerate blank lines in GFF input
mmolari Jun 11, 2026
b8b37d7
test(annotate): add klebs graph + real annotation smoke tests
mmolari Jun 11, 2026
0ba31ff
docs(annotate): record real-data smoke-test findings
mmolari Jun 11, 2026
efb5a96
test(annotate): cover GenBank join-collapse and unresolvable-bounds skip
mmolari Jun 11, 2026
2a7462c
refactor(annotate): drop GenBank support, GFF-only for v1
mmolari Jun 11, 2026
7d54687
feat(annotate): add consensus_coords_from_node inverse coordinate helper
mmolari Jun 11, 2026
f1c18e7
test(annotate): cover shared-position del/ins tie-break in consensus_…
mmolari Jun 11, 2026
8eb6bf4
Merge pull request #186 from neherlab/feat/annotate-lift-coords
mmolari Jun 12, 2026
dcd9459
feat(annotate): expose per-endpoint in-insertion flag in consensus_co…
mmolari Jun 12, 2026
6c727b1
feat(annotate): lift features onto graph nodes with pluggable CSV writer
mmolari Jun 12, 2026
a6c5115
test(annotate): round-trip integration test for the node-level lift
mmolari Jun 12, 2026
44f1288
docs(annotate): record P3 node-level lift as-built notes
mmolari Jun 12, 2026
9740c90
feat(annotate): add node-level annotate CLI command
mmolari Jun 12, 2026
8caded4
docs(annotate): node-CLI-first roadmap and P5.1 as-built notes
mmolari Jun 12, 2026
9fb075e
fix(annotate): reword unmatched-seqid error to drop the unexposed map…
mmolari Jun 12, 2026
ffaa6a1
test(annotate): normalize klebs gff seqids and add real-data lift smoke
mmolari Jun 12, 2026
02e3e72
feat(annotate): lift origin-spanning features on circular paths
mmolari Jun 12, 2026
6870247
docs(annotate): regenerate CLI reference for the annotate command
mmolari Jun 12, 2026
78d5d06
ci(docs): fix misleading output path in CLI-reference staleness message
mmolari Jun 12, 2026
3eae878
refactor(annotate): debug-assert arc-contiguity when merging wrapped …
mmolari Jun 14, 2026
709bbac
docs(annotate): refine P4 block-level compaction to coordinate-consen…
mmolari Jun 14, 2026
c972a58
test(annotate): extract two_node_forward_graph fixture helper
mmolari Jun 14, 2026
31272cb
Merge pull request #187 from neherlab/feat/annotate-cli
mmolari Jun 14, 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
2 changes: 1 addition & 1 deletion .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ jobs:

- name: "Check that the git diff is clean"
run: |
git -c color.ui=always diff --exit-code 'docs/docs/reference.md' || (echo "Looks like command-line interface has changed, but the autogenerated CLI reference documentation at 'docs/docs/reference.md' is not up-to-date. Please build the fresh version of pangraph, then run 'cd docs && ./generate-reference-docs <path_to_pangraph> docs/docs/reference.md', then verify and commit changes to the file docs/docs/reference.md." >&2; exit 1)
git -c color.ui=always diff --exit-code 'docs/docs/reference.md' || (echo "Looks like the command-line interface has changed, but the autogenerated CLI reference at 'docs/docs/reference.md' is not up-to-date. Please build pangraph, then from the repository root run 'cd docs && ./generate-reference-docs <path_to_pangraph> docs/reference.md' (the executable path is relative to docs/, e.g. ../target/release/pangraph), then verify and commit changes to the file docs/docs/reference.md." >&2; exit 1)


publish-to-github-releases:
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ log = "=0.4.28"
maplit = "=1.0.2"
ndarray = { version = "=0.16.1", features = ["rayon", "serde", "blas", "approx"] }
ndarray-stats = "=0.6.0"
noodles = { version = "=0.60.0", features = ["core", "sam"] }
noodles = { version = "=0.60.0", features = ["core", "sam", "gff"] }
num = "=0.4.3"
num-traits = "=0.2.19"
num_cpus = "=1.17.0"
Expand Down
6 changes: 6 additions & 0 deletions data/example.gff
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
##gff-version 3
##sequence-region chr1 1 1000
chr1 prokka gene 5 30 . + . ID=gene0001;Name=dnaA
chr1 prokka CDS 5 30 . + 0 ID=cds0001;Parent=gene0001;product=replication initiator
chr1 prokka gene 50 90 . - . ID=gene0002;Name=repA
chr2 prokka misc_feature 10 20 . . . ID=feat0003;note=unstranded
Binary file added data/klebs_annotations/NC_017540.gff.gz
Binary file not shown.
Binary file added data/klebs_annotations/NZ_CP013711.gff.gz
Binary file not shown.
Binary file added data/klebs_graph.json.gz
Binary file not shown.
294 changes: 294 additions & 0 deletions dev/design/annotate-implementation-notes.md

Large diffs are not rendered by default.

367 changes: 367 additions & 0 deletions dev/design/annotate.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions docs/docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ If you have Pangraph CLI installed, you can type `pangraph --help` to read the l
* [`pangraph export core-genome`↴](#pangraph-export-core-genome)
* [`pangraph simplify`↴](#pangraph-simplify)
* [`pangraph reconstruct`↴](#pangraph-reconstruct)
* [`pangraph annotate`↴](#pangraph-annotate)
* [`pangraph schema`↴](#pangraph-schema)
* [`pangraph completions`↴](#pangraph-completions)
* [`pangraph help-markdown`↴](#pangraph-help-markdown)
Expand Down Expand Up @@ -58,6 +59,7 @@ Questions, ideas, bug reports: https://github.com/neherlab/pangraph/issues
* `export` — Export a pangraph to a chosen file format(s)
* `simplify` — Generates a simplified graph that only contains a subset of the input genomes
* `reconstruct` — Reconstruct all input fasta sequences from graph
* `annotate` — Lift genome annotations onto the pangenome graph
* `schema` — Generate JSON schema for Pangraph file format
* `completions` — Generate shell completions
* `help-markdown` — Print command-line reference documentation in Markdown format
Expand Down Expand Up @@ -329,6 +331,33 @@ Reconstruct all input fasta sequences from graph



## `pangraph annotate`

Lift genome annotations onto the pangenome graph

**Usage:** `pangraph annotate [OPTIONS] --gff <GFF> [INPUT]`

###### **Arguments:**

* `<INPUT>` — Path to Pangraph JSON.

Accepts plain or compressed file. If a compressed file is provided, it will be transparently decompressed. Supported compression formats: `gz`, `bz2`, `xz`, `zstd`. Decompressor is chosen based on file extension.

If no input file provided, the uncompressed input is read from standard input (stdin).

###### **Options:**

* `--gff <GFF>` — Path to a GFF3 annotation file. Repeat the flag to provide multiple files.

Accepts plain or compressed files (`gz`, `bz2`, `xz`, `zstd`), chosen by file extension. At least one file is required. Annotation `seqid`s must match the graph path names exactly.
* `-o`, `--output <OUTPUT>` — Path to the output node-level annotation table (CSV).

Will be created if it does not exist. The output is compressed if the path ends in a known compression extension (`gz`, `bz2`, `xz`, `zstd`). Use `-` to write uncompressed CSV to standard output (stdout).

Default value: `-`



## `pangraph schema`

Generate JSON schema for Pangraph file format
Expand Down
69 changes: 69 additions & 0 deletions packages/pangraph/src/annotation/feature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::pangraph::strand::Strand;
use crate::utils::interval::Interval;
use serde::{Deserialize, Serialize};

/// A genome annotation feature, normalized from an annotation file (currently GFF)
/// into a single, format-agnostic representation.
///
/// The model is deliberately format-neutral: additional readers (e.g. GenBank) can be
/// added later without touching anything downstream. Coordinates live in `interval` as
/// 0-based, half-open `[start, end)` over the genome/contig identified by `seqid`.
/// Everything downstream (matching, the annotation lift, writers) consumes `Feature`
/// and never the original file format.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Feature {
/// Identifier of the genome/contig the feature belongs to (GFF `seqid` column).
/// Matched against pangraph path names.
pub seqid: String,

/// Free-text provenance of the annotation (GFF `source` column), if any.
pub source: Option<String>,

/// Feature type, e.g. `"CDS"` or `"gene"` (GFF `type` column).
pub feature_type: String,

/// Location on `seqid`, as a 0-based half-open interval `[start, end)`.
pub interval: Interval,

/// Strand of the feature, or `None` when unstranded (e.g. GFF `.` or `?`).
pub strand: Option<Strand>,

/// Stable feature identifier (GFF `ID` attribute), if present.
pub id: Option<String>,

/// Human-readable name (GFF `Name` attribute), if present.
pub name: Option<String>,

/// All remaining key→value metadata, preserving order and duplicate keys.
pub attributes: Vec<(String, String)>,
}

/// Build a 0-based half-open [`Interval`] from 1-based, fully-closed coordinates,
/// the convention used by GFF.
///
/// For example `(1, 3)` (three bases, 1-based inclusive) becomes `[0, 3)`.
pub fn interval_from_one_based_inclusive(start: usize, end: usize) -> Interval {
debug_assert!(start >= 1, "1-based coordinates must be >= 1, got start={start}");
Interval::new(start - 1, end)
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;

#[test]
fn test_interval_from_one_based_inclusive_multibase() {
assert_eq!(interval_from_one_based_inclusive(1, 3), Interval::new(0, 3));
}

#[test]
fn test_interval_from_one_based_inclusive_single_base() {
assert_eq!(interval_from_one_based_inclusive(5, 5), Interval::new(4, 5));
}

#[test]
fn test_interval_from_one_based_inclusive_offset() {
assert_eq!(interval_from_one_based_inclusive(10, 20), Interval::new(9, 20));
}
}
Loading
Loading