pub type Position = u32;
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Direction {
Left,
Right,
}
impl Direction {
pub const NAMES: [&str; 2] = ["Left", "Right"];
pub fn code(self) -> u8 {
match self {
Direction::Left => 0,
Direction::Right => 1,
}
}
pub fn opposite(self) -> Self {
match self {
Direction::Left => Direction::Right,
Direction::Right => Direction::Left,
}
}
pub fn shift(self, p: Position) -> Option<Position> {
match self {
Direction::Left => p.checked_sub(1),
Direction::Right => p.checked_add(1),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SiteKind {
Plain,
Ctcf {
captures_from: Direction,
strength: f64,
},
}
impl SiteKind {
pub const NAMES: [&str; 2] = ["Plain", "Ctcf"];
pub fn code(self) -> u8 {
match self {
SiteKind::Plain => 0,
SiteKind::Ctcf { .. } => 1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Site {
pub kind: SiteKind,
}
impl Site {
pub const fn plain() -> Self {
Site {
kind: SiteKind::Plain,
}
}
pub fn ctcf(captures_from: Direction, strength: f64) -> Self {
Site {
kind: SiteKind::Ctcf {
captures_from,
strength,
},
}
}
pub fn is_ctcf(&self) -> bool {
matches!(self.kind, SiteKind::Ctcf { .. })
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct Ctcf {
pub site: Position,
pub captures_from: Direction,
pub strength: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LatticeError {
Empty,
TooLarge {
n_sites: usize,
},
CtcfOutOfRange {
site: Position,
n_sites: usize,
},
DuplicateCtcf {
site: Position,
},
BadStrength {
site: Position,
strength: f64,
},
}
impl std::fmt::Display for LatticeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LatticeError::Empty => write!(f, "a lattice needs at least one site"),
LatticeError::TooLarge { n_sites } => {
write!(
f,
"a lattice has at most {} sites, got {n_sites}",
Position::MAX
)
}
LatticeError::CtcfOutOfRange { site, n_sites } => {
write!(
f,
"CTCF site {site} is outside a lattice of {n_sites} sites"
)
}
LatticeError::DuplicateCtcf { site } => write!(f, "CTCF site {site} is listed twice"),
LatticeError::BadStrength { site, strength } => {
write!(
f,
"CTCF strength must be in [0, 1], got {strength} at site {site}"
)
}
}
}
}
impl std::error::Error for LatticeError {}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(
try_from = "super::serial::LatticeData",
into = "super::serial::LatticeData"
)
)]
pub struct Lattice {
pub(crate) sites: Vec<Site>,
}
impl Lattice {
pub fn new(sites: Vec<Site>) -> Result<Self, LatticeError> {
if sites.is_empty() {
return Err(LatticeError::Empty);
}
if sites.len() > Position::MAX as usize {
return Err(LatticeError::TooLarge {
n_sites: sites.len(),
});
}
for (i, site) in sites.iter().enumerate() {
if let SiteKind::Ctcf { strength, .. } = site.kind
&& !(0.0..=1.0).contains(&strength)
{
return Err(LatticeError::BadStrength {
site: i as Position,
strength,
});
}
}
Ok(Lattice { sites })
}
pub fn plain(n_sites: usize) -> Result<Self, LatticeError> {
Self::with_ctcfs(n_sites, &[])
}
pub fn with_ctcfs(n_sites: usize, ctcfs: &[Ctcf]) -> Result<Self, LatticeError> {
if n_sites == 0 {
return Err(LatticeError::Empty);
}
if n_sites > Position::MAX as usize {
return Err(LatticeError::TooLarge { n_sites });
}
let mut sites = vec![Site::plain(); n_sites];
for c in ctcfs {
let slot = sites
.get_mut(c.site as usize)
.ok_or(LatticeError::CtcfOutOfRange {
site: c.site,
n_sites,
})?;
if slot.is_ctcf() {
return Err(LatticeError::DuplicateCtcf { site: c.site });
}
*slot = Site::ctcf(c.captures_from, c.strength);
}
Self::new(sites)
}
pub fn len(&self) -> usize {
self.sites.len()
}
pub fn is_empty(&self) -> bool {
self.sites.is_empty()
}
pub fn sites(&self) -> &[Site] {
&self.sites
}
pub fn ctcfs(&self) -> Vec<Ctcf> {
self.sites
.iter()
.enumerate()
.filter_map(|(i, s)| match s.kind {
SiteKind::Ctcf {
captures_from,
strength,
} => Some(Ctcf {
site: i as Position,
captures_from,
strength,
}),
SiteKind::Plain => None,
})
.collect()
}
pub fn n_plain(&self) -> usize {
self.sites.iter().filter(|s| !s.is_ctcf()).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctcf(site: Position, captures_from: Direction, strength: f64) -> Ctcf {
Ctcf {
site,
captures_from,
strength,
}
}
#[test]
fn with_ctcfs_places_ctcfs_and_reads_them_back() {
let specs = [
ctcf(3, Direction::Left, 0.5),
ctcf(7, Direction::Right, 1.0),
];
let lattice = Lattice::with_ctcfs(10, &specs).unwrap();
assert_eq!(lattice.len(), 10);
assert_eq!(lattice.ctcfs(), specs);
assert_eq!(lattice.n_plain(), 8);
assert_eq!(lattice.sites()[3], Site::ctcf(Direction::Left, 0.5));
}
#[test]
fn plain_lattice_has_no_ctcfs() {
let lattice = Lattice::plain(5).unwrap();
assert!(lattice.ctcfs().is_empty());
assert_eq!(lattice.n_plain(), 5);
}
#[test]
fn rejects_invalid_lattices() {
let left = Direction::Left;
assert_eq!(Lattice::plain(0), Err(LatticeError::Empty));
assert_eq!(Lattice::new(vec![]), Err(LatticeError::Empty));
assert_eq!(
Lattice::with_ctcfs(10, &[ctcf(10, left, 1.0)]),
Err(LatticeError::CtcfOutOfRange {
site: 10,
n_sites: 10
})
);
assert_eq!(
Lattice::with_ctcfs(10, &[ctcf(3, left, 1.0), ctcf(3, left, 0.5)]),
Err(LatticeError::DuplicateCtcf { site: 3 })
);
assert_eq!(
Lattice::with_ctcfs(10, &[ctcf(3, left, 1.5)]),
Err(LatticeError::BadStrength {
site: 3,
strength: 1.5
})
);
assert!(matches!(
Lattice::new(vec![Site::ctcf(left, f64::NAN)]),
Err(LatticeError::BadStrength { site: 0, .. })
));
}
}