1use std::io::Write;
12use std::path::{Path, PathBuf};
13use std::process::Command;
14
15#[derive(Debug, Clone)]
17pub struct BasePairProb {
18 pub left: usize,
19 pub right: usize,
20 pub prob: f64,
21}
22
23#[derive(Debug, Clone)]
25pub struct SequenceBpp {
26 pub seq_index: usize,
27 pub pairs: Vec<BasePairProb>,
28}
29
30pub fn find_tool(name: &str) -> Option<PathBuf> {
32 if let Ok(bindir) = std::env::var("MAFFT_BINARIES") {
34 let path = Path::new(&bindir).join(name);
35 if path.exists() {
36 return Some(path);
37 }
38 }
39
40 if let Ok(output) = Command::new("which").arg(name).output() {
42 if output.status.success() {
43 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
44 if !path.is_empty() {
45 return Some(PathBuf::from(path));
46 }
47 }
48 }
49
50 None
51}
52
53pub fn run_mccaskill(sequence: &[u8], tool_path: &Path) -> Result<Vec<BasePairProb>, String> {
59 let tmpdir = std::env::temp_dir();
60 let infile = tmpdir.join("_mafftrs_mccaskillin");
61 let outfile = tmpdir.join("_mafftrs_mccaskillout");
62
63 let mut f = std::fs::File::create(&infile)
65 .map_err(|e| format!("Cannot create temp file: {e}"))?;
66 writeln!(f, ">seq").map_err(|e| format!("Write error: {e}"))?;
67 f.write_all(sequence).map_err(|e| format!("Write error: {e}"))?;
68 writeln!(f).map_err(|e| format!("Write error: {e}"))?;
69 drop(f);
70
71 let output = Command::new(tool_path)
73 .args(["-m", "-writebpp"])
74 .stdin(std::fs::File::open(&infile).map_err(|e| format!("Cannot open temp: {e}"))?)
75 .output()
76 .map_err(|e| format!("Failed to run mxscarnamod: {e}"))?;
77
78 if !output.status.success() {
79 return Err(format!("mxscarnamod failed: {}", String::from_utf8_lossy(&output.stderr)));
80 }
81
82 std::fs::write(&outfile, &output.stdout)
84 .map_err(|e| format!("Cannot write output: {e}"))?;
85
86 let content = String::from_utf8_lossy(&output.stdout);
88 let pairs = parse_mccaskill_output(&content);
89
90 let _ = std::fs::remove_file(&infile);
92 let _ = std::fs::remove_file(&outfile);
93
94 Ok(pairs)
95}
96
97fn parse_mccaskill_output(content: &str) -> Vec<BasePairProb> {
99 let mut pairs = Vec::new();
100 for line in content.lines() {
101 let parts: Vec<&str> = line.split_whitespace().collect();
102 if parts.len() >= 3 {
103 if let (Ok(left), Ok(right), Ok(prob)) = (
104 parts[0].parse::<usize>(),
105 parts[1].parse::<usize>(),
106 parts[2].parse::<f64>(),
107 ) {
108 if prob >= 0.01 {
109 pairs.push(BasePairProb { left, right, prob });
110 }
111 }
112 }
113 }
114 pairs
115}
116
117pub fn run_contrafold(sequence: &[u8], tool_path: &Path) -> Result<Vec<BasePairProb>, String> {
123 let tmpdir = std::env::temp_dir();
124 let infile = tmpdir.join("_mafftrs_contrafoldin");
125 let outfile = tmpdir.join("_mafftrs_contrafoldout");
126
127 let mut f = std::fs::File::create(&infile)
129 .map_err(|e| format!("Cannot create temp file: {e}"))?;
130 writeln!(f, ">seq").map_err(|e| format!("Write error: {e}"))?;
131 f.write_all(sequence).map_err(|e| format!("Write error: {e}"))?;
132 writeln!(f).map_err(|e| format!("Write error: {e}"))?;
133 drop(f);
134
135 let status = Command::new(tool_path)
137 .args(["predict", infile.to_str().unwrap(), "--posteriors", "0.01", outfile.to_str().unwrap()])
138 .status()
139 .map_err(|e| format!("Failed to run contrafold: {e}"))?;
140
141 if !status.success() {
142 return Err("contrafold failed".to_string());
143 }
144
145 let content = std::fs::read_to_string(&outfile)
147 .map_err(|e| format!("Cannot read contrafold output: {e}"))?;
148 let pairs = parse_contrafold_output(&content);
149
150 let _ = std::fs::remove_file(&infile);
152 let _ = std::fs::remove_file(&outfile);
153
154 Ok(pairs)
155}
156
157fn parse_contrafold_output(content: &str) -> Vec<BasePairProb> {
159 let mut pairs = Vec::new();
160 for line in content.lines() {
161 let parts: Vec<&str> = line.split_whitespace().collect();
162 if parts.is_empty() { continue; }
163 if let Ok(left) = parts[0].parse::<usize>() {
164 let left = left.saturating_sub(1); for &part in &parts[1..] {
166 if let Some(colon) = part.find(':') {
167 if let (Ok(right), Ok(prob)) = (
168 part[..colon].parse::<usize>(),
169 part[colon + 1..].parse::<f64>(),
170 ) {
171 let right = right.saturating_sub(1);
172 if prob >= 0.01 {
173 pairs.push(BasePairProb { left, right, prob });
174 }
175 }
176 }
177 }
178 }
179 }
180 pairs
181}
182
183pub fn run_dash(
189 sequences: &[Vec<u8>],
190 names: &[String],
191 server_url: &str,
192) -> Result<Vec<(usize, usize, f64, usize, usize, usize, usize)>, String> {
193 let tool_path = find_tool("dash_client")
194 .ok_or_else(|| "dash_client not found in PATH or MAFFT_BINARIES".to_string())?;
195
196 let tmpdir = std::env::temp_dir();
197 let infile = tmpdir.join("_mafftrs_dashin");
198 let outfile = tmpdir.join("_mafftrs_hat3seed");
199
200 let mut f = std::fs::File::create(&infile)
202 .map_err(|e| format!("Cannot create temp file: {e}"))?;
203 for (name, seq) in names.iter().zip(sequences.iter()) {
204 writeln!(f, ">{name}").map_err(|e| format!("Write error: {e}"))?;
205 let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
206 f.write_all(&ungapped).map_err(|e| format!("Write error: {e}"))?;
207 writeln!(f).map_err(|e| format!("Write error: {e}"))?;
208 }
209 drop(f);
210
211 let status = Command::new(&tool_path)
213 .args([
214 "-url", server_url,
215 "-i", infile.to_str().unwrap(),
216 "-hat3", outfile.to_str().unwrap(),
217 ])
218 .status()
219 .map_err(|e| format!("Failed to run dash_client: {e}"))?;
220
221 if !status.success() {
222 return Err("dash_client failed".to_string());
223 }
224
225 let content = std::fs::read_to_string(&outfile)
227 .map_err(|e| format!("Cannot read DASH output: {e}"))?;
228 let constraints = parse_hat3(&content);
229
230 let _ = std::fs::remove_file(&infile);
232 let _ = std::fs::remove_file(&outfile);
233
234 Ok(constraints)
235}
236
237fn parse_hat3(content: &str) -> Vec<(usize, usize, f64, usize, usize, usize, usize)> {
239 let mut constraints = Vec::new();
240 for line in content.lines() {
241 let parts: Vec<&str> = line.split_whitespace().collect();
242 if parts.len() >= 8 {
243 if let (Ok(i), Ok(j), Ok(opt), Ok(s1), Ok(e1), Ok(s2), Ok(e2)) = (
244 parts[0].parse::<usize>(),
245 parts[1].parse::<usize>(),
246 parts[3].parse::<f64>(),
247 parts[4].parse::<usize>(),
248 parts[5].parse::<usize>(),
249 parts[6].parse::<usize>(),
250 parts[7].parse::<usize>(),
251 ) {
252 constraints.push((i, j, opt, s1, e1, s2, e2));
253 }
254 }
255 }
256 constraints
257}
258
259pub fn compute_bpp_mccaskill(sequences: &[Vec<u8>]) -> Result<Vec<SequenceBpp>, String> {
261 let tool = find_tool("mxscarnamod")
262 .ok_or_else(|| {
263 "mxscarnamod not found. Q-INS-i requires the McCaskill base-pair probability program.\n\
264 Install it and ensure it is in your PATH or set MAFFT_BINARIES.\n\
265 See: https://mafft.cbrc.jp/alignment/software/source.html".to_string()
266 })?;
267
268 let mut results = Vec::with_capacity(sequences.len());
269 for (idx, seq) in sequences.iter().enumerate() {
270 let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
272 let pairs = run_mccaskill(&ungapped, &tool)?;
273 results.push(SequenceBpp { seq_index: idx, pairs });
274 }
275 Ok(results)
276}
277
278pub fn compute_bpp_contrafold(sequences: &[Vec<u8>]) -> Result<Vec<SequenceBpp>, String> {
280 let tool = find_tool("contrafold")
281 .ok_or_else(|| {
282 "contrafold not found. X-INS-i requires CONTRAfold.\n\
283 Install CONTRAfold v2.02+ and ensure it is in your PATH or set MAFFT_BINARIES.\n\
284 See: https://mafft.cbrc.jp/alignment/software/source.html".to_string()
285 })?;
286
287 let mut results = Vec::with_capacity(sequences.len());
288 for (idx, seq) in sequences.iter().enumerate() {
289 let ungapped: Vec<u8> = seq.iter().filter(|&&c| c != b'-').copied().collect();
290 let pairs = run_contrafold(&ungapped, &tool)?;
291 results.push(SequenceBpp { seq_index: idx, pairs });
292 }
293 Ok(results)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn parse_mccaskill_format() {
302 let input = "0 5 0.85\n1 4 0.72\n2 3 0.005\n";
303 let pairs = parse_mccaskill_output(input);
304 assert_eq!(pairs.len(), 2); assert_eq!(pairs[0].left, 0);
306 assert_eq!(pairs[0].right, 5);
307 assert!((pairs[0].prob - 0.85).abs() < 1e-10);
308 }
309
310 #[test]
311 fn parse_contrafold_format() {
312 let input = "1 2:0.95 5:0.42\n3 4:0.31\n";
313 let pairs = parse_contrafold_output(input);
314 assert_eq!(pairs.len(), 3);
315 assert_eq!(pairs[0].left, 0);
317 assert_eq!(pairs[0].right, 1);
318 assert!((pairs[0].prob - 0.95).abs() < 1e-10);
319 }
320
321 #[test]
322 fn parse_hat3_format() {
323 let input = "0 1 100 5.8 10 20 30 40 info\n2 3 50 2.9 15 25 35 45 info\n";
324 let constraints = parse_hat3(input);
325 assert_eq!(constraints.len(), 2);
326 assert_eq!(constraints[0].0, 0); assert_eq!(constraints[0].1, 1); assert!((constraints[0].2 - 5.8).abs() < 1e-10); }
330
331 #[test]
332 fn find_tool_nonexistent() {
333 assert!(find_tool("nonexistent_tool_xyz_12345").is_none());
334 }
335}