use crate::{
alignment::phmm::{
CorePhmm, EmissionParams, GlobalPhmm, LayerParams, PhmmNumber, TransitionParams,
indexing::{GetLayer, PhmmIndexable},
state::PhmmState::{self, *},
},
data::mappings::{AA_UNAMBIG_PROFILE_MAP, ByteIndexMap, DNA_UNAMBIG_PROFILE_MAP},
unwrap_or_return_some_err,
};
use std::{
fs::File,
io::{BufRead, BufReader, BufWriter, Error as IOError, ErrorKind, Lines, Read, Write},
marker::PhantomData,
path::Path,
};
pub struct SamHmmParser;
impl SamHmmParser {
pub fn dna_hmm_from_readable<R: Read>(read: R) -> std::io::Result<GlobalPhmm<f32, 4>> {
SupportedConfig::parse_sam_model(read)
}
pub fn dna_hmm_from_path(path: impl AsRef<Path>) -> std::io::Result<GlobalPhmm<f32, 4>> {
SupportedConfig::parse_sam_model(File::open(path)?)
}
pub fn protein_hmm_from_readable<R: Read>(read: R) -> Result<GlobalPhmm<f32, 20>, std::io::Error> {
SupportedConfig::parse_sam_model(read)
}
pub fn protein_hmm_from_path(path: impl AsRef<Path>) -> Result<GlobalPhmm<f32, 20>, std::io::Error> {
SupportedConfig::parse_sam_model(File::open(path)?)
}
}
#[cfg(feature = "alignment-diagnostics")]
pub struct GenericSamHmmParser<T>(PhantomData<T>);
#[cfg(feature = "alignment-diagnostics")]
impl<T: PhmmNumber> GenericSamHmmParser<T> {
pub fn dna_hmm_from_readable<R: Read>(read: R) -> std::io::Result<GlobalPhmm<T, 4>> {
SupportedConfig::parse_sam_model(read)
}
pub fn dna_hmm_from_path(path: impl AsRef<Path>) -> std::io::Result<GlobalPhmm<T, 4>> {
SupportedConfig::parse_sam_model(File::open(path)?)
}
pub fn protein_hmm_from_readable<R: Read>(read: R) -> std::io::Result<GlobalPhmm<T, 20>> {
SupportedConfig::parse_sam_model(read)
}
pub fn protein_hmm_from_path(path: impl AsRef<Path>) -> std::io::Result<GlobalPhmm<T, 20>> {
SupportedConfig::parse_sam_model(File::open(path)?)
}
}
pub struct SamHmmWriter;
impl SamHmmWriter {
#[inline]
pub fn write_dna_model<T: PhmmNumber>(path: impl AsRef<Path>, model: &GlobalPhmm<T, 4>) -> std::io::Result<()> {
SupportedConfig::write_sam_model_file(path, model)
}
#[inline]
pub fn write_protein_model<T: PhmmNumber>(path: impl AsRef<Path>, model: &GlobalPhmm<T, 20>) -> std::io::Result<()> {
SupportedConfig::write_sam_model_file(path, model)
}
}
const PARAMS_TO_COPY_PARSING: [(PhmmState, PhmmState); 6] = [
(Delete, Delete),
(Delete, Match),
(Match, Delete),
(Match, Match),
(Insert, Delete),
(Insert, Match),
];
struct SupportedConfig;
trait SamHmmConfig<const S: usize, const L: usize> {
fn parse_mapping(mapping: &str) -> std::io::Result<&'static ByteIndexMap<S>>;
fn unparse_mapping(mapping: &'static ByteIndexMap<S>) -> std::io::Result<&'static str>;
fn group_params<T: PhmmNumber>(params: [T; L]) -> LayerParams<T, S>;
fn ungroup_params<T: PhmmNumber>(params: &LayerParams<T, S>) -> [T; L];
fn parse_sam_model<R, T>(read: R) -> Result<GlobalPhmm<T, S>, IOError>
where
R: Read,
T: PhmmNumber,
SupportedConfig: SamHmmConfig<S, L>, {
let mut lines = LineIterator::from_readable(read);
validate_model_line(&mut lines)?;
let mapping = Self::parse_alphabet_line(&mut lines)?;
let mut layers = LayerIter::new(&mut lines)?.collect::<Result<Vec<_>, IOError>>()?;
let [first_layer, .., last_layer] = layers.as_mut_slice() else {
return Err(IOError::new(
ErrorKind::InvalidData,
"At least three layers must be specified in the SAM model file",
));
};
first_layer.transition[(Delete, Delete)] = T::INFINITY;
first_layer.transition[(Delete, Match)] = T::INFINITY;
first_layer.transition[(Delete, Insert)] = T::INFINITY;
last_layer.transition[(Delete, Delete)] = T::INFINITY;
last_layer.transition[(Insert, Delete)] = T::INFINITY;
last_layer.transition[(Match, Delete)] = T::INFINITY;
last_layer.emission_match = EmissionParams::default();
Ok(GlobalPhmm {
mapping,
core: CorePhmm::new_unchecked(layers),
})
}
fn write_sam_model_file<T: PhmmNumber, P>(path: P, model: &GlobalPhmm<T, S>) -> std::io::Result<()>
where
P: AsRef<Path>,
SupportedConfig: SamHmmConfig<S, L>, {
if model.seq_len() < 1 {
return Err(IOError::new(
ErrorKind::InvalidData,
"The pHMM must correspond to a reference of length at least 1!",
));
}
let mut writer = BufWriter::new(File::create(path.as_ref())?);
writeln!(writer, "MODEL")?;
writeln!(writer, "alphabet {}", Self::unparse_mapping(model.mapping())?)?;
let [first_layer, rest @ ..] = model.layers() else {
return Err(IOError::new(
ErrorKind::InvalidData,
"At least two layers must be present in the model!",
));
};
let mut current_layer = LayerParams::<T, S>::default();
write!(writer, "0 ")?;
current_layer.transition[Insert] = first_layer.transition[Insert];
current_layer.emission_insert = first_layer.emission_insert.clone();
print_params(&mut writer, Self::ungroup_params(¤t_layer))?;
current_layer = first_layer.clone();
for (i, layer) in (1..).zip(rest.iter()) {
write!(writer, "{i} ")?;
current_layer.transition[Insert] = layer.transition[Insert];
current_layer.emission_insert = layer.emission_insert.clone();
print_params(&mut writer, Self::ungroup_params(¤t_layer))?;
current_layer = layer.clone();
}
write!(writer, "END ")?;
current_layer.transition[Insert] = [T::INFINITY; 3];
current_layer.emission_insert = EmissionParams::default();
print_params(&mut writer, Self::ungroup_params(¤t_layer))?;
writeln!(writer)?;
writeln!(writer, "ENDMODEL")
}
fn parse_alphabet_line<R: Read>(lines: &mut LineIterator<R>) -> std::io::Result<&'static ByteIndexMap<S>> {
let line = lines
.next()
.ok_or(IOError::new(ErrorKind::InvalidData, "Could not locate alphabet line in file"))??
.to_ascii_uppercase();
let mut tokens = line.split_whitespace();
let field = tokens.next().unwrap_or("");
if !field.eq_ignore_ascii_case("ALPHABET") {
return Err(IOError::new(ErrorKind::InvalidData, "Expected alphabet line"));
}
let Some(mut alphabet) = tokens.next() else {
return Err(IOError::new(ErrorKind::InvalidData, "Alphabet was missing"));
};
alphabet = alphabet.trim();
Self::parse_mapping(alphabet)
}
fn parse_layer_params<'a, R: Read, T: PhmmNumber>(
mut rest_of_line: impl Iterator<Item = &'a str>, lines: &mut LineIterator<R>,
) -> std::io::Result<LayerParams<T, S>> {
let mut params = [T::ZERO; L];
let mut i = Self::fill_params_from_iter::<T>(rest_of_line.by_ref(), &mut params, 0)?;
if i == L {
return Ok(Self::group_params(params));
}
for line in lines {
let line = line?;
let mut split = line.split_whitespace();
i = Self::fill_params_from_iter::<T>(split.by_ref(), &mut params, i)?;
if i == L {
break;
}
}
if i < params.len() {
return Err(IOError::new(
ErrorKind::InvalidData,
"The file ended before a full set of parameters was parsed",
));
}
Ok(Self::group_params(params))
}
fn fill_params_from_iter<'a, T: PhmmNumber>(
iter: &mut impl Iterator<Item = &'a str>, params: &mut [T], mut i: usize,
) -> Result<usize, std::io::Error> {
for param in iter.take(L - i) {
params[i] = parse_param(param)?;
i += 1;
}
if iter.next().is_some() {
return Err(IOError::new(
ErrorKind::InvalidData,
"Layers of the model must be separated by line breaks",
));
}
Ok(i)
}
}
impl SamHmmConfig<4, 17> for SupportedConfig {
#[inline]
fn parse_mapping(mapping: &str) -> Result<&'static ByteIndexMap<4>, IOError> {
match mapping {
"DNA" => Ok(&DNA_UNAMBIG_PROFILE_MAP),
"PROTEIN" => Err(IOError::new(
ErrorKind::InvalidData,
"A protein alphabet was found, but a DNA alphabet was expected",
)),
_ => Err(IOError::new(ErrorKind::InvalidData, "Unsupported alphabet specified")),
}
}
#[inline]
fn unparse_mapping(mapping: &'static ByteIndexMap<4>) -> std::io::Result<&'static str> {
if mapping == &DNA_UNAMBIG_PROFILE_MAP {
return Ok("DNA");
}
Err(IOError::new(ErrorKind::InvalidData, "The mapping is unsupported by SAM!"))
}
#[inline]
fn group_params<T: PhmmNumber>(params: [T; 17]) -> LayerParams<T, 4> {
let transition = TransitionParams([
[params[4], params[3], params[5]],
[params[1], params[0], params[2]],
[params[7], params[6], params[8]],
]);
let emission_match = EmissionParams::from_array([params[9], params[11], params[10], params[12]]);
let emission_insert = EmissionParams::from_array([params[13], params[15], params[14], params[16]]);
LayerParams {
transition,
emission_match,
emission_insert,
}
}
#[inline]
fn ungroup_params<T: PhmmNumber>(params: &LayerParams<T, 4>) -> [T; 17] {
[
params.transition[(Delete, Delete)],
params.transition[(Match, Delete)],
params.transition[(Insert, Delete)],
params.transition[(Delete, Match)],
params.transition[(Match, Match)],
params.transition[(Insert, Match)],
params.transition[(Delete, Insert)],
params.transition[(Match, Insert)],
params.transition[(Insert, Insert)],
params.emission_match[0],
params.emission_match[2],
params.emission_match[1],
params.emission_match[3],
params.emission_insert[0],
params.emission_insert[2],
params.emission_insert[1],
params.emission_insert[3],
]
}
}
impl SamHmmConfig<20, 49> for SupportedConfig {
#[inline]
fn parse_mapping(mapping: &str) -> Result<&'static ByteIndexMap<20>, IOError> {
match mapping {
"DNA" => Err(IOError::new(
ErrorKind::InvalidData,
"A DNA alphabet was found, but a protein alphabet was expected",
)),
"PROTEIN" => Ok(&AA_UNAMBIG_PROFILE_MAP),
_ => Err(IOError::new(ErrorKind::InvalidData, "Unsupported alphabet specified")),
}
}
#[inline]
fn unparse_mapping(mapping: &'static ByteIndexMap<20>) -> std::io::Result<&'static str> {
if mapping == &AA_UNAMBIG_PROFILE_MAP {
return Ok("PROTEIN");
}
Err(IOError::new(ErrorKind::InvalidData, "The mapping is unsupported by SAM!"))
}
#[inline]
fn group_params<T: PhmmNumber>(params: [T; 49]) -> LayerParams<T, 20> {
let (transition, rest) = params.split_at(9);
let transition = TransitionParams([
[transition[4], transition[3], transition[5]],
[transition[1], transition[0], transition[2]],
[transition[7], transition[6], transition[8]],
]);
let (emission_match, emission_insert) = rest.split_at(20);
let emission_match = EmissionParams::from_array(emission_match.try_into().unwrap());
let emission_insert = EmissionParams::from_array(emission_insert.try_into().unwrap());
LayerParams {
transition,
emission_match,
emission_insert,
}
}
#[inline]
fn ungroup_params<T: PhmmNumber>(params: &LayerParams<T, 20>) -> [T; 49] {
let mut out = [T::ZERO; 49];
out[0..9].copy_from_slice(&[
params.transition[(Delete, Delete)],
params.transition[(Match, Delete)],
params.transition[(Insert, Delete)],
params.transition[(Delete, Match)],
params.transition[(Match, Match)],
params.transition[(Insert, Match)],
params.transition[(Delete, Insert)],
params.transition[(Match, Insert)],
params.transition[(Insert, Insert)],
]);
out[9..29].copy_from_slice(params.emission_match.as_slice());
out[29..49].copy_from_slice(params.emission_insert.as_slice());
out
}
}
fn parse_layer_name(token: &str) -> Result<Option<usize>, IOError> {
if token.eq_ignore_ascii_case("BEGIN") {
Ok(Some(0))
} else if token.eq_ignore_ascii_case("END") {
Ok(None)
} else if token.starts_with('-') {
Err(IOError::new(
ErrorKind::InvalidData,
"Negatively-numbered nodes in models are not supported. Consider using a prior version of SAM's hmmconvert to convert the model",
))
} else if let Ok(layer) = token.parse::<usize>() {
Ok(Some(layer))
} else {
Err(IOError::new(ErrorKind::InvalidData, "Could not parse the layer name"))
}
}
#[inline]
fn validate_model_line<R: Read>(lines: &mut LineIterator<R>) -> std::io::Result<()> {
let line = lines
.next()
.ok_or(IOError::new(ErrorKind::InvalidData, "Could not locate initial line in file"))??;
let token = line.split_whitespace().next().unwrap_or("");
match token {
"MODEL" => Ok(()),
"REGULARIZER" => Err(IOError::new(
ErrorKind::InvalidData,
"REGULARIZER is not supported by this parser",
)),
"NULLMODEL" => Err(IOError::new(
ErrorKind::InvalidData,
"NULLMODEL is not supported by this parser",
)),
_ => Err(IOError::new(ErrorKind::InvalidData, "Could not locate initial line in file")),
}
}
#[inline]
fn parse_param<T: PhmmNumber>(prob: &str) -> Result<T, IOError> {
if prob.starts_with('-') {
return Err(IOError::new(
ErrorKind::InvalidData,
format!("A negative parameter was found: {prob}"),
));
}
let prob = prob.parse::<f64>().map_err(|_| {
IOError::new(
ErrorKind::InvalidData,
format!("When parsing the model parameters, the value {prob} could not be parsed as a float"),
)
})?;
Ok(T::from_prob(prob))
}
struct LayerIter<'a, R: Read, T, const S: usize, const L: usize> {
raw_layers: RawLayerIter<'a, R, T, S, L>,
last_layer: LayerParams<T, S>,
}
impl<'a, R: Read, T: PhmmNumber, const S: usize, const L: usize> LayerIter<'a, R, T, S, L>
where
SupportedConfig: SamHmmConfig<S, L>,
{
fn new(lines: &'a mut LineIterator<R>) -> std::io::Result<Self> {
let mut raw_layers = RawLayerIter::new(lines);
let last_layer = raw_layers
.next()
.ok_or(IOError::new(ErrorKind::InvalidData, "No model layers found!"))??;
Ok(Self { raw_layers, last_layer })
}
}
impl<R: Read, T: PhmmNumber, const S: usize, const L: usize> Iterator for LayerIter<'_, R, T, S, L>
where
SupportedConfig: SamHmmConfig<S, L>,
{
type Item = std::io::Result<LayerParams<T, S>>;
fn next(&mut self) -> Option<Self::Item> {
let mut next_layer = unwrap_or_return_some_err!(self.raw_layers.next()?);
for param in PARAMS_TO_COPY_PARSING {
self.last_layer.transition[param] = next_layer.transition[param];
}
self.last_layer.emission_match = std::mem::take(&mut next_layer.emission_match);
Some(Ok(std::mem::replace(&mut self.last_layer, next_layer)))
}
}
struct RawLayerIter<'a, R: Read, T, const S: usize, const L: usize> {
lines: &'a mut LineIterator<R>,
expected_layer: Option<usize>,
phantom: PhantomData<T>,
}
impl<'a, R: Read, T, const S: usize, const L: usize> RawLayerIter<'a, R, T, S, L> {
fn new(lines: &'a mut LineIterator<R>) -> Self {
Self {
lines,
expected_layer: Some(0),
phantom: PhantomData,
}
}
}
impl<R: Read, T: PhmmNumber, const S: usize, const L: usize> Iterator for RawLayerIter<'_, R, T, S, L>
where
SupportedConfig: SamHmmConfig<S, L>,
{
type Item = Result<LayerParams<T, S>, IOError>;
fn next(&mut self) -> Option<Self::Item> {
let expected_layer = self.expected_layer?;
let Some(line) = unwrap_or_return_some_err!(self.lines.next().transpose()) else {
return Some(Err(IOError::new(ErrorKind::InvalidData, "File ended before ENDMODEL")));
};
let mut tokens = line.split_whitespace();
let token = tokens.next().unwrap_or("");
if token.eq_ignore_ascii_case("ENDMODEL") {
self.expected_layer = None;
return None;
}
if token.eq_ignore_ascii_case("FREQAVE") {
unwrap_or_return_some_err!(SupportedConfig::parse_layer_params::<R, T>(tokens, self.lines));
return self.next();
}
if let Some(layer_number) = unwrap_or_return_some_err!(parse_layer_name(token)) {
if layer_number != expected_layer {
return Some(Err(IOError::new(
ErrorKind::InvalidData,
format!("Found model node {layer_number}, expected model node {expected_layer}"),
)));
}
self.expected_layer = Some(expected_layer + 1);
} else {
self.expected_layer = None;
}
let layer = unwrap_or_return_some_err!(SupportedConfig::parse_layer_params(tokens, self.lines));
if self.expected_layer.is_none() {
if let Some(line) = self.lines.next()
&& let Some(token) = unwrap_or_return_some_err!(line).split_whitespace().next()
{
if !token.eq_ignore_ascii_case("ENDMODEL") {
return Some(Err(IOError::new(
ErrorKind::InvalidData,
format!("Unexpected token {token} found between END layer and ENDMODEL"),
)));
}
} else {
return Some(Err(IOError::new(ErrorKind::InvalidData, "Failed to find ENDMODEL")));
}
}
Some(Ok(layer))
}
}
struct LineIterator<R: Read> {
lines: Lines<BufReader<R>>,
}
impl<R: Read> LineIterator<R> {
fn from_readable(read: R) -> Self {
Self {
lines: BufReader::new(read).lines(),
}
}
}
impl<R: Read> Iterator for LineIterator<R> {
type Item = std::io::Result<String>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let line = match self.lines.next()? {
Ok(line) => line,
Err(e) => return Some(Err(e)),
};
let line = line.trim().to_string();
if !line.is_empty() && !line.starts_with('%') {
return Some(Ok(line));
}
}
}
}
#[inline]
fn print_params<T: PhmmNumber, const N: usize>(writer: &mut impl Write, params: [T; N]) -> std::io::Result<()> {
let mut params = params.into_iter().map(super::PhmmNumber::to_prob::<f32>);
let Some(param) = params.next() else { return Ok(()) };
write!(writer, "{param:.6}")?;
for param in params {
write!(writer, " {param:.6}")?;
}
writeln!(writer)
}