primer3 0.1.0

Safe Rust bindings to the primer3 primer design library
Documentation
//! Melting temperature (Tm) calculation.
//!
//! Calculates the melting temperature of DNA oligonucleotides using
//! nearest-neighbor thermodynamics (for sequences up to 60 bp) or the
//! GC% formula (for longer sequences).

use std::ffi::CString;
use std::os::raw::c_int;

use crate::conditions::SolutionConditions;
use crate::error::Result;
use crate::init::ensure_initialized;

/// Method for calculating melting temperature.
///
/// Maps to the C enum `tm_method_type` in `oligotm.h`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum TmMethod {
    /// Breslauer et al. 1986 thermodynamic parameters (`breslauer_auto` = 0).
    Breslauer,
    /// `SantaLucia` 1998 unified nearest-neighbor parameters (`santalucia_auto` = 1).
    /// **This is the recommended value.**
    #[default]
    SantaLucia,
    /// `SantaLucia` 2004 updated parameters (`santalucia_2004` = 2).
    SantaLucia2004,
}

impl TmMethod {
    /// Converts to the C `tm_method_type` constant.
    pub(crate) fn to_c(self) -> u32 {
        match self {
            Self::Breslauer => primer3_sys::tm_method_type_breslauer_auto,
            Self::SantaLucia => primer3_sys::tm_method_type_santalucia_auto,
            Self::SantaLucia2004 => primer3_sys::tm_method_type_santalucia_2004,
        }
    }
}

/// Method for salt concentration correction.
///
/// Maps to the C enum `salt_correction_type` in `oligotm.h`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum SaltCorrectionMethod {
    /// Schildkraut & Lifson 1965 (`schildkraut` = 0).
    Schildkraut,
    /// `SantaLucia` 1998 (`santalucia` = 1). **Recommended.**
    #[default]
    SantaLucia,
    /// Owczarzy et al. 2008 (`owczarzy` = 2).
    Owczarzy,
}

impl SaltCorrectionMethod {
    /// Converts to the C `salt_correction_type` constant.
    pub(crate) fn to_c(self) -> u32 {
        match self {
            Self::Schildkraut => primer3_sys::salt_correction_type_schildkraut,
            Self::SantaLucia => primer3_sys::salt_correction_type_santalucia,
            Self::Owczarzy => primer3_sys::salt_correction_type_owczarzy,
        }
    }
}

/// Parameters for Tm calculation.
///
/// Use [`TmParams::default()`] for standard PCR conditions, or customize
/// individual fields. Defaults match primer3-py.
///
/// # Example
///
/// ```no_run
/// use primer3::{TmParams, SolutionConditions};
///
/// let params = TmParams {
///     conditions: SolutionConditions {
///         mv_conc: 75.0,
///         ..Default::default()
///     },
///     ..Default::default()
/// };
/// ```
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TmParams {
    /// Salt and buffer concentrations.
    pub conditions: SolutionConditions,
    /// Actual PCR annealing temperature in Celsius (default: -10.0, meaning unset).
    pub annealing_temp_c: f64,
    /// Maximum sequence length for nearest-neighbor model (default: 60).
    /// Sequences longer than this use the GC% formula.
    pub max_nn_length: usize,
    /// Tm calculation method (default: `SantaLucia` 1998).
    pub tm_method: TmMethod,
    /// Salt correction method (default: `SantaLucia` 1998).
    pub salt_correction_method: SaltCorrectionMethod,
}

impl Default for TmParams {
    fn default() -> Self {
        Self {
            conditions: SolutionConditions::default(),
            annealing_temp_c: -10.0,
            max_nn_length: 60,
            tm_method: TmMethod::default(),
            salt_correction_method: SaltCorrectionMethod::default(),
        }
    }
}

/// Calculates the melting temperature of a DNA sequence using default parameters.
///
/// Uses nearest-neighbor thermodynamics for sequences up to 60 bp, and
/// the GC% formula for longer sequences.
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
///
/// # Example
///
/// ```no_run
/// let tm = primer3::calc_tm("GTAAAACGACGGCCAGT").unwrap();
/// assert!((tm - 53.0).abs() < 2.0);
/// ```
pub fn calc_tm(seq: &str) -> Result<f64> {
    calc_tm_with(seq, &TmParams::default())
}

/// Calculates the delta G (Gibbs free energy) of disruption of a DNA
/// oligonucleotide using the nearest-neighbor model.
///
/// Uses the default Tm method ([`SantaLucia`](TmMethod::SantaLucia) 1998).
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
///
/// # Example
///
/// ```no_run
/// let dg = primer3::calc_oligodg("GTAAAACGACGGCCAGT").unwrap();
/// println!("dG = {dg:.0} cal/mol");
/// ```
pub fn calc_oligodg(seq: &str) -> Result<f64> {
    calc_oligodg_with(seq, TmMethod::default())
}

/// Calculates the delta G of disruption with a specific Tm method.
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
pub fn calc_oligodg_with(seq: &str, tm_method: TmMethod) -> Result<f64> {
    if seq.is_empty() {
        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
    }

    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
    })?;

    let result = unsafe { primer3_sys::oligodg(c_seq.as_ptr(), tm_method.to_c() as c_int) };

    Ok(result)
}

/// Calculates the delta G (Gibbs free energy) of the last `len` bases of a
/// DNA oligonucleotide using the nearest-neighbor model.
///
/// If the sequence is shorter than `len`, returns the delta G of the entire
/// sequence. Uses the default Tm method ([`SantaLucia`](TmMethod::SantaLucia) 1998).
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
///
/// # Example
///
/// ```no_run
/// let dg = primer3::calc_end_oligodg("GTAAAACGACGGCCAGT", 5).unwrap();
/// println!("dG of last 5 bases = {dg:.0} cal/mol");
/// ```
pub fn calc_end_oligodg(seq: &str, len: usize) -> Result<f64> {
    calc_end_oligodg_with(seq, len, TmMethod::default())
}

/// Calculates the delta G of the last `len` bases with a specific Tm method.
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
pub fn calc_end_oligodg_with(seq: &str, len: usize, tm_method: TmMethod) -> Result<f64> {
    if seq.is_empty() {
        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
    }

    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
    })?;

    let result = unsafe {
        primer3_sys::end_oligodg(c_seq.as_ptr(), len as c_int, tm_method.to_c() as c_int)
    };

    Ok(result)
}

/// Calculates the melting temperature of a DNA sequence with custom parameters.
///
/// # Errors
///
/// Returns an error if the sequence is empty or contains invalid characters.
pub fn calc_tm_with(seq: &str, params: &TmParams) -> Result<f64> {
    ensure_initialized()?;

    if seq.is_empty() {
        return Err(crate::error::Primer3Error::InvalidSequence("sequence is empty".into()));
    }

    let c_seq = CString::new(seq.to_ascii_uppercase()).map_err(|_| {
        crate::error::Primer3Error::InvalidSequence("sequence contains null byte".into())
    })?;

    let result = unsafe {
        primer3_sys::seqtm(
            c_seq.as_ptr(),
            params.conditions.dna_conc,
            params.conditions.mv_conc,
            params.conditions.dv_conc,
            params.conditions.dntp_conc,
            params.conditions.dmso_conc,
            params.conditions.dmso_fact,
            params.conditions.formamide_conc,
            params.max_nn_length as c_int,
            params.tm_method.to_c(),
            params.salt_correction_method.to_c(),
            params.annealing_temp_c,
        )
    };

    // OLIGOTM_ERROR (-999999.9999) signals an error in the C library
    if result.Tm < -999_999.0 {
        return Err(crate::error::Primer3Error::InvalidSequence(
            "Tm calculation failed (invalid sequence or parameters)".into(),
        ));
    }

    Ok(result.Tm)
}