use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct BasePairProb {
pub left: usize,
pub right: usize,
pub prob: f64,
}
#[derive(Debug, Clone)]
pub struct SequenceBpp {
pub seq_index: usize,
pub pairs: Vec<BasePairProb>,
}
pub fn find_tool(name: &str) -> Option<PathBuf> {
if let Ok(bindir) = std::env::var("MAFFT_BINARIES") {
let path = Path::new(&bindir).join(name);
if path.exists() {
return Some(path);
}
}
if let Ok(output) = Command::new("which").arg(name).output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
}
None
}
pub fn run_mccaskill(sequence: &[u8], tool_path: &Path) -> Result<Vec<BasePairProb>, String> {
let tmpdir = std::env::temp_dir();
let infile = tmpdir.join("_mafftrs_mccaskillin");
let outfile = tmpdir.join("_mafftrs_mccaskillout");
let mut f = std::fs::File::create(&infile)
.map_err(|e| format!("Cannot create temp file: {e}"))?;
writeln!(f, ">seq").map_err(|e| format!("Write error: {e}"))?;
f.write_all(sequence).map_err(|e| format!("Write error: {e}"))?;
writeln!(f).map_err(|e| format!("Write error: {e}"))?;
drop(f);
let output = Command::new(tool_path)
.args(["-m", "-writebpp"])
.stdin(std::fs::File::open(&infile).map_err(|e| format!("Cannot open temp: {e}"))?)
.output()
.map_err(|e| format!("Failed to run mxscarnamod: {e}"))?;
if !output.status.success() {
return Err(format!("mxscarnamod failed: {}", String::from_utf8_lossy(&output.stderr)));
}
std::fs::write(&outfile, &output.stdout)
.map_err(|e| format!("Cannot write output: {e}"))?;
let content = String::from_utf8_lossy(&output.stdout);
let pairs = parse_mccaskill_output(&content);
let _ = std::fs::remove_file(&infile);
let _ = std::fs::remove_file(&outfile);
Ok(pairs)
}
fn parse_mccaskill_output(content: &str) -> Vec<BasePairProb> {
let mut pairs = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
if let (Ok(left), Ok(right), Ok(prob)) = (
parts[0].parse::<usize>(),
parts[1].parse::<usize>(),
parts[2].parse::<f64>(),
) {
if prob >= 0.01 {
pairs.push(BasePairProb { left, right, prob });
}
}
}
}
pairs
}
pub fn run_contrafold(sequence: &[u8], tool_path: &Path) -> Result<Vec<BasePairProb>, String> {
let tmpdir = std::env::temp_dir();
let infile = tmpdir.join("_mafftrs_contrafoldin");
let outfile = tmpdir.join("_mafftrs_contrafoldout");
let mut f = std::fs::File::create(&infile)
.map_err(|e| format!("Cannot create temp file: {e}"))?;
writeln!(f, ">seq").map_err(|e| format!("Write error: {e}"))?;
f.write_all(sequence).map_err(|e| format!("Write error: {e}"))?;
writeln!(f).map_err(|e| format!("Write error: {e}"))?;
drop(f);
let status = Command::new(tool_path)
.args(["predict", infile.to_str().unwrap(), "--posteriors", "0.01", outfile.to_str().unwrap()])
.status()
.map_err(|e| format!("Failed to run contrafold: {e}"))?;
if !status.success() {
return Err("contrafold failed".to_string());
}
let content = std::fs::read_to_string(&outfile)
.map_err(|e| format!("Cannot read contrafold output: {e}"))?;
let pairs = parse_contrafold_output(&content);
let _ = std::fs::remove_file(&infile);
let _ = std::fs::remove_file(&outfile);
Ok(pairs)
}
fn parse_contrafold_output(content: &str) -> Vec<BasePairProb> {
let mut pairs = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() { continue; }
if let Ok(left) = parts[0].parse::<usize>() {
let left = left.saturating_sub(1); for &part in &parts[1..] {
if let Some(colon) = part.find(':') {
if let (Ok(right), Ok(prob)) = (
part[..colon].parse::<usize>(),
part[colon + 1..].parse::<f64>(),
) {
let right = right.saturating_sub(1);
if prob >= 0.01 {
pairs.push(BasePairProb { left, right, prob });
}
}
}
}
}
}
pairs
}
pub fn run_dash(
sequences: &[Vec<u8>],
names: &[String],
server_url: &str,
) -> Result<Vec<(usize, usize, f64, usize, usize, usize, usize)>, String> {
let tool_path = find_tool("dash_client")
.ok_or_else(|| "dash_client not found in PATH or MAFFT_BINARIES".to_string())?;
let tmpdir = std::env::temp_dir();
let infile = tmpdir.join("_mafftrs_dashin");
let outfile = tmpdir.join("_mafftrs_hat3seed");
let mut f = std::fs::File::create(&infile)
.map_err(|e| format!("Cannot create temp file: {e}"))?;
for (name, seq) in names.iter().zip(sequences.iter()) {
writeln!(f, ">{name}").map_err(|e| format!("Write error: {e}"))?;
let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
f.write_all(&ungapped).map_err(|e| format!("Write error: {e}"))?;
writeln!(f).map_err(|e| format!("Write error: {e}"))?;
}
drop(f);
let status = Command::new(&tool_path)
.args([
"-url", server_url,
"-i", infile.to_str().unwrap(),
"-hat3", outfile.to_str().unwrap(),
])
.status()
.map_err(|e| format!("Failed to run dash_client: {e}"))?;
if !status.success() {
return Err("dash_client failed".to_string());
}
let content = std::fs::read_to_string(&outfile)
.map_err(|e| format!("Cannot read DASH output: {e}"))?;
let constraints = parse_hat3(&content);
let _ = std::fs::remove_file(&infile);
let _ = std::fs::remove_file(&outfile);
Ok(constraints)
}
fn parse_hat3(content: &str) -> Vec<(usize, usize, f64, usize, usize, usize, usize)> {
let mut constraints = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 8 {
if let (Ok(i), Ok(j), Ok(opt), Ok(s1), Ok(e1), Ok(s2), Ok(e2)) = (
parts[0].parse::<usize>(),
parts[1].parse::<usize>(),
parts[3].parse::<f64>(),
parts[4].parse::<usize>(),
parts[5].parse::<usize>(),
parts[6].parse::<usize>(),
parts[7].parse::<usize>(),
) {
constraints.push((i, j, opt, s1, e1, s2, e2));
}
}
}
constraints
}
pub fn compute_bpp_mccaskill(sequences: &[Vec<u8>]) -> Result<Vec<SequenceBpp>, String> {
let tool = find_tool("mxscarnamod")
.ok_or_else(|| {
"mxscarnamod not found. Q-INS-i requires the McCaskill base-pair probability program.\n\
Install it and ensure it is in your PATH or set MAFFT_BINARIES.\n\
See: https://mafft.cbrc.jp/alignment/software/source.html".to_string()
})?;
let mut results = Vec::with_capacity(sequences.len());
for (idx, seq) in sequences.iter().enumerate() {
let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
let pairs = run_mccaskill(&ungapped, &tool)?;
results.push(SequenceBpp { seq_index: idx, pairs });
}
Ok(results)
}
pub fn compute_bpp_contrafold(sequences: &[Vec<u8>]) -> Result<Vec<SequenceBpp>, String> {
let tool = find_tool("contrafold")
.ok_or_else(|| {
"contrafold not found. X-INS-i requires CONTRAfold.\n\
Install CONTRAfold v2.02+ and ensure it is in your PATH or set MAFFT_BINARIES.\n\
See: https://mafft.cbrc.jp/alignment/software/source.html".to_string()
})?;
let mut results = Vec::with_capacity(sequences.len());
for (idx, seq) in sequences.iter().enumerate() {
let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
let pairs = run_contrafold(&ungapped, &tool)?;
results.push(SequenceBpp { seq_index: idx, pairs });
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mccaskill_format() {
let input = "0 5 0.85\n1 4 0.72\n2 3 0.005\n";
let pairs = parse_mccaskill_output(input);
assert_eq!(pairs.len(), 2); assert_eq!(pairs[0].left, 0);
assert_eq!(pairs[0].right, 5);
assert!((pairs[0].prob - 0.85).abs() < 1e-10);
}
#[test]
fn parse_contrafold_format() {
let input = "1 2:0.95 5:0.42\n3 4:0.31\n";
let pairs = parse_contrafold_output(input);
assert_eq!(pairs.len(), 3);
assert_eq!(pairs[0].left, 0);
assert_eq!(pairs[0].right, 1);
assert!((pairs[0].prob - 0.95).abs() < 1e-10);
}
#[test]
fn parse_hat3_format() {
let input = "0 1 100 5.8 10 20 30 40 info\n2 3 50 2.9 15 25 35 45 info\n";
let constraints = parse_hat3(input);
assert_eq!(constraints.len(), 2);
assert_eq!(constraints[0].0, 0); assert_eq!(constraints[0].1, 1); assert!((constraints[0].2 - 5.8).abs() < 1e-10); }
#[test]
fn find_tool_nonexistent() {
assert!(find_tool("nonexistent_tool_xyz_12345").is_none());
}
}