use std::ffi::{CStr, c_void};
use std::fmt;
use crate::conditions::SolutionConditions;
use crate::error::{Primer3Error, Result};
use crate::init::ensure_initialized;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThermoResult {
structure_found: bool,
tm: f64,
dg: f64,
dh: f64,
ds: f64,
ascii_structure: Option<String>,
}
impl ThermoResult {
pub fn structure_found(&self) -> bool {
self.structure_found
}
pub fn tm(&self) -> f64 {
self.tm
}
pub fn dg(&self) -> f64 {
self.dg
}
pub fn dh(&self) -> f64 {
self.dh
}
pub fn ds(&self) -> f64 {
self.ds
}
pub fn ascii_structure(&self) -> Option<&str> {
self.ascii_structure.as_deref()
}
pub fn approx_eq(&self, other: &Self, epsilon: f64) -> bool {
self.structure_found == other.structure_found
&& (self.tm - other.tm).abs() < epsilon
&& (self.dg - other.dg).abs() < epsilon
&& (self.dh - other.dh).abs() < epsilon
&& (self.ds - other.ds).abs() < epsilon
&& self.ascii_structure == other.ascii_structure
}
}
impl fmt::Display for ThermoResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ThermoResult(structure_found={}, tm={:.2}, dg={:.0}, dh={:.0}, ds={:.1})",
self.structure_found, self.tm, self.dg, self.dh, self.ds,
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThermoArgs {
pub conditions: SolutionConditions,
pub temp_c: f64,
pub max_loop: usize,
pub output_structure: bool,
}
impl Default for ThermoArgs {
fn default() -> Self {
Self {
conditions: SolutionConditions::default(),
temp_c: 37.0,
max_loop: 30,
output_structure: false,
}
}
}
const THAL_MAX_ALIGN: usize = 60;
const THAL_MAX_SEQ: usize = 10_000;
fn call_thal(
seq1: &str,
seq2: &str,
alignment_type: u32,
is_dimer: bool,
args: &ThermoArgs,
) -> Result<ThermoResult> {
let mode = if args.output_structure {
primer3_sys::thal_mode_THL_STRUCT
} else {
primer3_sys::thal_mode_THL_FAST
};
let thal_args = primer3_sys::thal_args {
type_: alignment_type,
maxLoop: args.max_loop as std::os::raw::c_int,
mv: args.conditions.mv_conc,
dv: args.conditions.dv_conc,
dntp: args.conditions.dntp_conc,
dna_conc: args.conditions.dna_conc,
temp: args.temp_c + 273.15, dimer: i32::from(is_dimer),
};
let mut s1_c: Vec<u8> = seq1.bytes().map(|b| b.to_ascii_uppercase()).collect();
s1_c.push(0);
let mut s2_c: Vec<u8> = seq2.bytes().map(|b| b.to_ascii_uppercase()).collect();
s2_c.push(0);
let mut results: primer3_sys::thal_results = unsafe { std::mem::zeroed() };
unsafe {
primer3_sys::thal(s1_c.as_ptr(), s2_c.as_ptr(), &thal_args, mode, &mut results);
}
if results.msg[0] != 0 {
let msg = unsafe { CStr::from_ptr(results.msg.as_ptr()) }.to_string_lossy().into_owned();
if !results.sec_struct.is_null() {
unsafe { libc::free(results.sec_struct.cast::<c_void>()) };
}
return Err(Primer3Error::Library(msg.into_boxed_str()));
}
let ascii_structure = if results.sec_struct.is_null() {
None
} else {
let s = unsafe { CStr::from_ptr(results.sec_struct) }.to_string_lossy().into_owned();
unsafe { libc::free(results.sec_struct.cast::<c_void>()) };
Some(s)
};
let structure_found = results.temp > 0.0;
Ok(ThermoResult {
structure_found,
tm: results.temp,
dg: results.dg,
dh: results.dh,
ds: results.ds,
ascii_structure,
})
}
fn validate_single_seq(seq: &str, operation: &'static str) -> Result<()> {
if seq.is_empty() {
return Err(Primer3Error::InvalidSequence("sequence is empty".into()));
}
if seq.len() > THAL_MAX_ALIGN {
return Err(Primer3Error::SequenceTooLong {
length: seq.len(),
max: THAL_MAX_ALIGN,
operation,
});
}
Ok(())
}
fn validate_pair_seqs(seq1: &str, seq2: &str, operation: &'static str) -> Result<()> {
if seq1.is_empty() || seq2.is_empty() {
return Err(Primer3Error::InvalidSequence("sequence is empty".into()));
}
if seq1.len() > THAL_MAX_ALIGN && seq2.len() > THAL_MAX_ALIGN {
return Err(Primer3Error::InvalidSequence(
format!("at least one sequence must be <= {THAL_MAX_ALIGN} bp for {operation}")
.into_boxed_str(),
));
}
let longer = seq1.len().max(seq2.len());
if longer > THAL_MAX_SEQ {
return Err(Primer3Error::SequenceTooLong { length: longer, max: THAL_MAX_SEQ, operation });
}
Ok(())
}
pub fn calc_hairpin(seq: &str) -> Result<ThermoResult> {
calc_hairpin_with(seq, &ThermoArgs::default())
}
pub fn calc_hairpin_with(seq: &str, args: &ThermoArgs) -> Result<ThermoResult> {
ensure_initialized()?;
validate_single_seq(seq, "hairpin")?;
call_thal(seq, seq, primer3_sys::thal_alignment_type_thal_hairpin, false, args)
}
pub fn calc_homodimer(seq: &str) -> Result<ThermoResult> {
calc_homodimer_with(seq, &ThermoArgs::default())
}
pub fn calc_homodimer_with(seq: &str, args: &ThermoArgs) -> Result<ThermoResult> {
ensure_initialized()?;
validate_single_seq(seq, "homodimer")?;
call_thal(seq, seq, primer3_sys::thal_alignment_type_thal_any, true, args)
}
pub fn calc_heterodimer(seq1: &str, seq2: &str) -> Result<ThermoResult> {
calc_heterodimer_with(seq1, seq2, &ThermoArgs::default())
}
pub fn calc_heterodimer_with(seq1: &str, seq2: &str, args: &ThermoArgs) -> Result<ThermoResult> {
ensure_initialized()?;
validate_pair_seqs(seq1, seq2, "heterodimer")?;
call_thal(seq1, seq2, primer3_sys::thal_alignment_type_thal_any, true, args)
}
pub fn calc_end_stability(seq1: &str, seq2: &str) -> Result<ThermoResult> {
calc_end_stability_with(seq1, seq2, &ThermoArgs::default())
}
pub fn calc_end_stability_with(seq1: &str, seq2: &str, args: &ThermoArgs) -> Result<ThermoResult> {
ensure_initialized()?;
validate_pair_seqs(seq1, seq2, "end_stability")?;
call_thal(seq1, seq2, primer3_sys::thal_alignment_type_thal_end1, true, args)
}