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}