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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ members = [
"crates/ogar-ad",
"crates/ogar-az",
"crates/ogar-dir-sim",
"crates/ogar-genom",
]

[workspace.package]
Expand Down
11 changes: 11 additions & 0 deletions crates/ogar-genom/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "ogar-genom"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
rust-version.workspace = true
description = "Packed genomic sequence substrate (first proof): DNA2 resident sequence (2 bits/base) with exact-round-trip ambiguity and soft-mask interval sidecars, the V4 base-set alphabet for IUPAC ambiguity, half-open coordinates with strand, zero-copy reverse-complement views, and the codon translation fold over the standard genetic code. Zero dependencies; mints no classids (the Genetics domain 0x0E is operator-gated)."

[dependencies]
332 changes: 332 additions & 0 deletions crates/ogar-genom/lab/genom_probe.rs

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions crates/ogar-genom/src/alphabet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! The two alphabets: DNA2 [`Base`] (resident) and V4 [`BaseSet`] (ambiguity algebra).

/// One unambiguous nucleotide, in DNA2 order. Complement is `3 - b` (= `b ^ 0b11`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum Base {
A = 0,
C = 1,
G = 2,
T = 3,
}

impl Base {
pub const ALL: [Base; 4] = [Base::A, Base::C, Base::G, Base::T];

/// From the 2-bit code; only the low two bits are read.
#[inline]
pub const fn from_code(code: u8) -> Base {
match code & 3 {
0 => Base::A,
1 => Base::C,
2 => Base::G,
_ => Base::T,
}
}

#[inline]
pub const fn code(self) -> u8 {
self as u8
}

#[inline]
pub const fn complement(self) -> Base {
Base::from_code(3 - self as u8)
}

/// Upper-case ASCII letter.
#[inline]
pub const fn to_ascii(self) -> u8 {
b"ACGT"[self as usize]
}

/// From an unambiguous ASCII base (either case); `None` for anything else.
#[inline]
pub const fn from_ascii(c: u8) -> Option<Base> {
match c {
b'A' | b'a' => Some(Base::A),
b'C' | b'c' => Some(Base::C),
b'G' | b'g' => Some(Base::G),
b'T' | b't' => Some(Base::T),
_ => None,
}
}
}

/// V4: a nibble naming the set of possible bases. Bit `b` set ⇔ `Base` with code `b` is possible.
///
/// `A=0001 C=0010 G=0100 T=1000`; `R=A|G=0101`, `Y=C|T=1010`, `N=1111`.
/// The empty set (`0000`) is representable and means "contradiction"; it is
/// never produced by IUPAC decoding.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BaseSet(u8);

impl BaseSet {
pub const EMPTY: BaseSet = BaseSet(0);
pub const ANY: BaseSet = BaseSet(0b1111);

/// From the raw nibble; bits above 3 are discarded.
#[inline]
pub const fn from_bits(bits: u8) -> BaseSet {
BaseSet(bits & 0b1111)
}

#[inline]
pub const fn bits(self) -> u8 {
self.0
}

#[inline]
pub const fn single(b: Base) -> BaseSet {
BaseSet(1 << b as u8)
}

#[inline]
pub const fn union(self, o: BaseSet) -> BaseSet {
BaseSet(self.0 | o.0)
}

#[inline]
pub const fn intersection(self, o: BaseSet) -> BaseSet {
BaseSet(self.0 & o.0)
}

#[inline]
pub const fn is_subset_of(self, o: BaseSet) -> bool {
self.0 & !o.0 == 0
}

#[inline]
pub const fn contains(self, b: Base) -> bool {
self.0 & (1 << b as u8) != 0
}

#[inline]
pub const fn is_empty(self) -> bool {
self.0 == 0
}

#[inline]
pub const fn len(self) -> u32 {
self.0.count_ones()
}

/// The single base, if the set has exactly one member.
#[inline]
pub const fn as_single(self) -> Option<Base> {
match self.0 {
0b0001 => Some(Base::A),
0b0010 => Some(Base::C),
0b0100 => Some(Base::G),
0b1000 => Some(Base::T),
_ => None,
}
}

/// Complement of every member (A↔T, C↔G) — the set on the other strand.
/// Reverses the nibble's bit order, because complement is `3 - code`.
#[inline]
pub const fn complement(self) -> BaseSet {
let b = self.0;
BaseSet(((b & 1) << 3) | ((b & 2) << 1) | ((b & 4) >> 1) | ((b & 8) >> 3))
}

/// Iterate the members in code order.
pub fn iter(self) -> impl Iterator<Item = Base> {
Base::ALL.into_iter().filter(move |&b| self.contains(b))
}

/// From an IUPAC nucleotide letter (either case; `U` reads as `T`).
pub const fn from_iupac(c: u8) -> Option<BaseSet> {
let bits = match c.to_ascii_uppercase() {
b'A' => 0b0001,
b'C' => 0b0010,
b'G' => 0b0100,
b'T' | b'U' => 0b1000,
b'R' => 0b0101,
b'Y' => 0b1010,
b'S' => 0b0110,
b'W' => 0b1001,
b'K' => 0b1100,
b'M' => 0b0011,
b'B' => 0b1110,
b'D' => 0b1101,
b'H' => 0b1011,
b'V' => 0b0111,
b'N' => 0b1111,
_ => return None,
};
Some(BaseSet(bits))
}

/// Upper-case IUPAC letter. The empty set has no letter and returns `b'-'`
/// (IUPAC's gap symbol); callers that can produce it should check first.
pub const fn to_iupac(self) -> u8 {
b"-ACMGRSVTWYHKDBN"[self.0 as usize]
}
}
108 changes: 108 additions & 0 deletions crates/ogar-genom/src/coord.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Coordinates: half-open intervals, strand, reading frame.
//!
//! These are **reference coordinates** — positions in one packed sequence. A
//! coordinate on a variant-bearing haplotype is a different space (indels
//! shift it), and is deliberately not representable here: the liftover between
//! the two belongs to the variant layer, not to this type.

/// Half-open `[start, end)` interval in base positions.
///
/// The fields are private so `start <= end` holds for every value: the only
/// public constructor is [`Interval::new`], which refuses inverted bounds.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Interval {
start: u64,
end: u64,
}

impl Interval {
/// `None` when `start > end`.
pub const fn new(start: u64, end: u64) -> Option<Interval> {
if start <= end {
Some(Interval { start, end })
} else {
None
}
}

/// Crate-internal constructor for bounds the caller has already ordered.
#[inline]
pub(crate) const fn ordered(start: u64, end: u64) -> Interval {
debug_assert!(start <= end);
Interval { start, end }
}

/// Crate-internal: grow a run by one base at its end.
#[inline]
pub(crate) fn extend_end(&mut self) {
self.end += 1;
}

#[inline]
pub const fn start(self) -> u64 {
self.start
}

#[inline]
pub const fn end(self) -> u64 {
self.end
}

#[inline]
pub const fn len(self) -> u64 {
self.end - self.start
}

#[inline]
pub const fn is_empty(self) -> bool {
self.start == self.end
}

#[inline]
pub const fn contains(self, pos: u64) -> bool {
self.start <= pos && pos < self.end
}

#[inline]
pub const fn contains_interval(self, o: Interval) -> bool {
self.start <= o.start && o.end <= self.end
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Strand {
Forward,
Reverse,
}

impl Strand {
#[inline]
pub const fn flip(self) -> Strand {
match self {
Strand::Forward => Strand::Reverse,
Strand::Reverse => Strand::Forward,
}
}
}

/// Reading frame offset `0..3` within a view (after strand is applied).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Frame(u8);

impl Frame {
pub const ZERO: Frame = Frame(0);

/// `None` for offsets ≥ 3.
pub const fn new(offset: u8) -> Option<Frame> {
if offset < 3 {
Some(Frame(offset))
} else {
None
}
}

#[inline]
pub const fn offset(self) -> u8 {
self.0
}
}
42 changes: 42 additions & 0 deletions crates/ogar-genom/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! # ogar-genom — packed genomic sequence substrate (first proof)
//!
//! The law this crate tests: **the sequence stays resident; another biological
//! interpretation is a new coordinate, a mask, or a fold — not a copy.**
//!
//! | operation | what bytes move |
//! |---|---|
//! | [`encode`](sequence::encode) | ASCII in → 2 bits/base + interval sidecars (a durable write, once) |
//! | [`SeqView::sub`](view::SeqView::sub) / [`SeqView::reverse_complement`](view::SeqView::reverse_complement) | none — a view is a borrow plus an interval and a strand |
//! | [`SeqView::base`](view::SeqView::base), [`SeqView::base_set`](view::SeqView::base_set) | one word read, plus a binary search of the ambiguity sidecar |
//! | [`translate`](translate::translate) | streaming fold: yields amino acids, allocates nothing |
//! | [`decode`](sequence::decode) | full materialization, by name, O(n) |
//!
//! ## Representation (measured, not chosen by elegance)
//!
//! Resident sequence is **DNA2** (`A=00 C=01 G=10 T=11`, 32 bases per `u64`).
//! Ambiguity (IUPAC `N`, `R`, `Y`, …) lives in a **sparse run sidecar**, not in
//! the resident bits. On GRCh38 chr21 (46.7 Mbp) the reference has **52** `N`
//! runs, **zero** non-`N` IUPAC symbols, and **48,677** soft-mask runs: the
//! sidecars cost 0.067 bits/base against V4's 4 bits/base everywhere. **V4**
//! ([`BaseSet`](alphabet::BaseSet)) is kept as the *query/answer* algebra —
//! union, intersection and subset over possible bases — not as storage.
//! See `docs/OGAR-GENOM-CAPSTONE.md` for the measurements and the falsifiers.
//!
//! ## What this crate does NOT do
//!
//! No variants, transcripts, populations, lineage or ontology addresses (those
//! are later layers), no file-format parsers (adapters at the boundary), no
//! classid mints, and no SIMD: kernels here are word-level bit operations. SIMD
//! fast paths, when they land, come from `ndarray::simd` only.

pub mod alphabet;
pub mod coord;
pub mod sequence;
pub mod translate;
pub mod view;

pub use alphabet::{Base, BaseSet};
pub use coord::{Frame, Interval, Strand};
pub use sequence::{AmbiguityRun, EncodeError, PackedSeq, decode, encode};
pub use translate::{AminoAcids, CODON_TABLE, codon_index, translate};
pub use view::SeqView;
Loading
Loading