Skip to main content

primer3/
tm.rs

1//! Melting temperature (Tm) calculation.
2//!
3//! Calculates the melting temperature of DNA oligonucleotides using
4//! nearest-neighbor thermodynamics (for sequences up to 60 bp) or the
5//! GC% formula (for longer sequences).
6
7use std::ffi::CString;
8use std::os::raw::c_int;
9
10use crate::conditions::SolutionConditions;
11use crate::error::Result;
12use crate::init::ensure_initialized;
13
14/// Method for calculating melting temperature.
15///
16/// Maps to the C enum `tm_method_type` in `oligotm.h`.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[non_exhaustive]
20pub enum TmMethod {
21    /// Breslauer et al. 1986 thermodynamic parameters (`breslauer_auto` = 0).
22    Breslauer,
23    /// `SantaLucia` 1998 unified nearest-neighbor parameters (`santalucia_auto` = 1).
24    /// **This is the recommended value.**
25    #[default]
26    SantaLucia,
27    /// `SantaLucia` 2004 updated parameters (`santalucia_2004` = 2).
28    SantaLucia2004,
29}
30
31impl TmMethod {
32    /// Converts to the C `tm_method_type` constant.
33    pub(crate) fn to_c(self) -> u32 {
34        match self {
35            Self::Breslauer => primer3_sys::tm_method_type_breslauer_auto,
36            Self::SantaLucia => primer3_sys::tm_method_type_santalucia_auto,
37            Self::SantaLucia2004 => primer3_sys::tm_method_type_santalucia_2004,
38        }
39    }
40}
41
42/// Method for salt concentration correction.
43///
44/// Maps to the C enum `salt_correction_type` in `oligotm.h`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[non_exhaustive]
48pub enum SaltCorrectionMethod {
49    /// Schildkraut & Lifson 1965 (`schildkraut` = 0).
50    Schildkraut,
51    /// `SantaLucia` 1998 (`santalucia` = 1). **Recommended.**
52    #[default]
53    SantaLucia,
54    /// Owczarzy et al. 2008 (`owczarzy` = 2).
55    Owczarzy,
56}
57
58impl SaltCorrectionMethod {
59    /// Converts to the C `salt_correction_type` constant.
60    pub(crate) fn to_c(self) -> u32 {
61        match self {
62            Self::Schildkraut => primer3_sys::salt_correction_type_schildkraut,
63            Self::SantaLucia => primer3_sys::salt_correction_type_santalucia,
64            Self::Owczarzy => primer3_sys::salt_correction_type_owczarzy,
65        }
66    }
67}
68
69/// Parameters for Tm calculation.
70///
71/// Use [`TmParams::default()`] for standard PCR conditions, or customize
72/// individual fields. Defaults match primer3-py.
73///
74/// # Example
75///
76/// ```no_run
77/// use primer3::{TmParams, SolutionConditions};
78///
79/// let params = TmParams {
80///     conditions: SolutionConditions {
81///         mv_conc: 75.0,
82///         ..Default::default()
83///     },
84///     ..Default::default()
85/// };
86/// ```
87#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct TmParams {
90    /// Salt and buffer concentrations.
91    pub conditions: SolutionConditions,
92    /// Actual PCR annealing temperature in Celsius (default: -10.0, meaning unset).
93    pub annealing_temp_c: f64,
94    /// Maximum sequence length for nearest-neighbor model (default: 60).
95    /// Sequences longer than this use the GC% formula.
96    pub max_nn_length: usize,
97    /// Tm calculation method (default: `SantaLucia` 1998).
98    pub tm_method: TmMethod,
99    /// Salt correction method (default: `SantaLucia` 1998).
100    pub salt_correction_method: SaltCorrectionMethod,
101}
102
103impl Default for TmParams {
104    fn default() -> Self {
105        Self {
106            conditions: SolutionConditions::default(),
107            annealing_temp_c: -10.0,
108            max_nn_length: 60,
109            tm_method: TmMethod::default(),
110            salt_correction_method: SaltCorrectionMethod::default(),
111        }
112    }
113}
114
115/// Calculates the melting temperature of a DNA sequence using default parameters.
116///
117/// Uses nearest-neighbor thermodynamics for sequences up to 60 bp, and
118/// the GC% formula for longer sequences.
119///
120/// # Errors
121///
122/// Returns an error if the sequence is empty or contains invalid characters.
123///
124/// # Example
125///
126/// ```no_run
127/// let tm = primer3::calc_tm("GTAAAACGACGGCCAGT").unwrap();
128/// assert!((tm - 53.0).abs() < 2.0);
129/// ```
130pub fn calc_tm(seq: &str) -> Result<f64> {
131    calc_tm_with(seq, &TmParams::default())
132}
133
134/// Calculates the delta G (Gibbs free energy) of disruption of a DNA
135/// oligonucleotide using the nearest-neighbor model.
136///
137/// Uses the default Tm method ([`SantaLucia`](TmMethod::SantaLucia) 1998).
138///
139/// # Errors
140///
141/// Returns an error if the sequence is empty or contains invalid characters.
142///
143/// # Example
144///
145/// ```no_run
146/// let dg = primer3::calc_oligodg("GTAAAACGACGGCCAGT").unwrap();
147/// println!("dG = {dg:.0} cal/mol");
148/// ```
149pub fn calc_oligodg(seq: &str) -> Result<f64> {
150    calc_oligodg_with(seq, TmMethod::default())
151}
152
153/// Calculates the delta G of disruption with a specific Tm method.
154///
155/// # Errors
156///
157/// Returns an error if the sequence is empty or contains invalid characters.
158pub fn calc_oligodg_with(seq: &str, tm_method: TmMethod) -> Result<f64> {
159    if seq.is_empty() {
160        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
161    }
162
163    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
164        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
165    })?;
166
167    let result = unsafe { primer3_sys::oligodg(c_seq.as_ptr(), tm_method.to_c() as c_int) };
168
169    Ok(result)
170}
171
172/// Calculates the delta G (Gibbs free energy) of the last `len` bases of a
173/// DNA oligonucleotide using the nearest-neighbor model.
174///
175/// If the sequence is shorter than `len`, returns the delta G of the entire
176/// sequence. Uses the default Tm method ([`SantaLucia`](TmMethod::SantaLucia) 1998).
177///
178/// # Errors
179///
180/// Returns an error if the sequence is empty or contains invalid characters.
181///
182/// # Example
183///
184/// ```no_run
185/// let dg = primer3::calc_end_oligodg("GTAAAACGACGGCCAGT", 5).unwrap();
186/// println!("dG of last 5 bases = {dg:.0} cal/mol");
187/// ```
188pub fn calc_end_oligodg(seq: &str, len: usize) -> Result<f64> {
189    calc_end_oligodg_with(seq, len, TmMethod::default())
190}
191
192/// Calculates the delta G of the last `len` bases with a specific Tm method.
193///
194/// # Errors
195///
196/// Returns an error if the sequence is empty or contains invalid characters.
197pub fn calc_end_oligodg_with(seq: &str, len: usize, tm_method: TmMethod) -> Result<f64> {
198    if seq.is_empty() {
199        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
200    }
201
202    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
203        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
204    })?;
205
206    let result = unsafe {
207        primer3_sys::end_oligodg(c_seq.as_ptr(), len as c_int, tm_method.to_c() as c_int)
208    };
209
210    Ok(result)
211}
212
213/// Calculates the melting temperature of a DNA sequence with custom parameters.
214///
215/// # Errors
216///
217/// Returns an error if the sequence is empty or contains invalid characters.
218pub fn calc_tm_with(seq: &str, params: &TmParams) -> Result<f64> {
219    ensure_initialized()?;
220
221    if seq.is_empty() {
222        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
223    }
224
225    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
226        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
227    })?;
228
229    let result = unsafe {
230        primer3_sys::seqtm(
231            c_seq.as_ptr(),
232            params.conditions.dna_conc,
233            params.conditions.mv_conc,
234            params.conditions.dv_conc,
235            params.conditions.dntp_conc,
236            params.conditions.dmso_conc,
237            params.conditions.dmso_fact,
238            params.conditions.formamide_conc,
239            params.max_nn_length as c_int,
240            params.tm_method.to_c(),
241            params.salt_correction_method.to_c(),
242            params.annealing_temp_c,
243        )
244    };
245
246    // OLIGOTM_ERROR (-999999.9999) signals an error in the C library
247    if result.Tm < -999_999.0 {
248        return Err(crate::error::Primer3Error::InvalidSequence(
249            "Tm calculation failed (invalid sequence or parameters)".into(),
250        ));
251    }
252
253    Ok(result.Tm)
254}