use crate::conditions::SolutionConditions;
use crate::error::{Primer3Error, Result};
use crate::tm::{SaltCorrectionMethod, TmMethod};
use crate::weights::{OligoWeights, PairWeights};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PrimerTask {
#[default]
PickPcrPrimers,
PickPcrPrimersAndHybProbe,
PickLeftOnly,
PickRightOnly,
PickHybProbeOnly,
Generic,
PickCloningPrimers,
PickDiscriminativePrimers,
PickSequencingPrimers,
PickPrimerList,
CheckPrimers,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SequencingParams {
pub lead: usize,
pub spacing: usize,
pub interval: usize,
pub accuracy: usize,
}
impl Default for SequencingParams {
fn default() -> Self {
Self { lead: 50, spacing: 500, interval: 250, accuracy: 20 }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OligoSettings {
pub opt_size: usize,
pub min_size: usize,
pub max_size: usize,
pub opt_tm: f64,
pub min_tm: f64,
pub max_tm: f64,
pub opt_bound: f64,
pub min_bound: f64,
pub max_bound: f64,
pub opt_gc_content: f64,
pub min_gc: f64,
pub max_gc: f64,
pub conditions: SolutionConditions,
pub max_self_any: f64,
pub max_self_end: f64,
pub max_self_any_th: f64,
pub max_self_end_th: f64,
pub max_hairpin_th: f64,
pub max_repeat_compl: f64,
pub max_template_mispriming: f64,
pub max_template_mispriming_th: f64,
pub must_match_five_prime: Option<String>,
pub must_match_three_prime: Option<String>,
pub num_ns_accepted: usize,
pub max_poly_x: usize,
pub weights: OligoWeights,
}
impl Default for OligoSettings {
fn default() -> Self {
Self {
opt_size: 20,
min_size: 18,
max_size: 27,
opt_tm: 60.0,
min_tm: 57.0,
max_tm: 63.0,
opt_bound: 0.0,
min_bound: 0.0,
max_bound: 0.0,
opt_gc_content: 50.0,
min_gc: 20.0,
max_gc: 80.0,
conditions: SolutionConditions::default(),
max_self_any: 8.0,
max_self_end: 3.0,
max_self_any_th: 47.0,
max_self_end_th: 47.0,
max_hairpin_th: 47.0,
max_repeat_compl: 12.0,
max_template_mispriming: 12.0,
max_template_mispriming_th: 47.0,
must_match_five_prime: None,
must_match_three_prime: None,
num_ns_accepted: 0,
max_poly_x: 5,
weights: OligoWeights::default(),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(clippy::struct_excessive_bools)]
pub struct PrimerSettings {
pub task: PrimerTask,
pub pick_left_primer: bool,
pub pick_right_primer: bool,
pub pick_internal_oligo: bool,
pub num_return: usize,
pub pick_anyway: bool,
pub tm_method: TmMethod,
pub salt_correction_method: SaltCorrectionMethod,
pub annealing_temp: f64,
pub max_end_stability: f64,
pub max_end_gc: usize,
pub gc_clamp: usize,
pub product_size_ranges: Vec<(usize, usize)>,
pub product_opt_size: usize,
pub product_max_tm: f64,
pub product_min_tm: f64,
pub product_opt_tm: f64,
pub pair_max_compl_any: f64,
pub pair_max_compl_end: f64,
pub pair_max_compl_any_th: f64,
pub pair_max_compl_end_th: f64,
pub max_diff_tm: f64,
pub pair_max_template_mispriming: f64,
pub pair_max_template_mispriming_th: f64,
pub pair_repeat_compl: f64,
pub thermodynamic_oligo_alignment: bool,
pub thermodynamic_template_alignment: bool,
pub lowercase_masking: bool,
pub first_base_index: usize,
pub liberal_base: bool,
pub min_left_three_prime_distance: i32,
pub min_right_three_prime_distance: i32,
pub sequencing: SequencingParams,
pub primer: OligoSettings,
pub internal_oligo: OligoSettings,
pub pair_weights: PairWeights,
}
impl Default for PrimerSettings {
fn default() -> Self {
Self {
task: PrimerTask::default(),
pick_left_primer: true,
pick_right_primer: true,
pick_internal_oligo: false,
num_return: 5,
pick_anyway: false,
tm_method: TmMethod::default(),
salt_correction_method: SaltCorrectionMethod::default(),
annealing_temp: -10.0,
max_end_stability: 9.0,
max_end_gc: 5,
gc_clamp: 0,
product_size_ranges: vec![(100, 300)],
product_opt_size: 0,
product_max_tm: 1_000_000.0,
product_min_tm: -1_000_000.0,
product_opt_tm: 0.0,
pair_max_compl_any: 8.0,
pair_max_compl_end: 3.0,
pair_max_compl_any_th: 47.0,
pair_max_compl_end_th: 47.0,
max_diff_tm: 100.0,
pair_max_template_mispriming: 24.0,
pair_max_template_mispriming_th: 47.0,
pair_repeat_compl: 24.0,
thermodynamic_oligo_alignment: true,
thermodynamic_template_alignment: true,
lowercase_masking: false,
first_base_index: 0,
liberal_base: false,
min_left_three_prime_distance: -1,
min_right_three_prime_distance: -1,
sequencing: SequencingParams::default(),
primer: OligoSettings::default(),
internal_oligo: OligoSettings::default(),
pair_weights: PairWeights::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct PrimerSettingsBuilder {
settings: PrimerSettings,
product_size_ranges_set: bool,
}
impl PrimerSettings {
pub fn builder() -> PrimerSettingsBuilder {
PrimerSettingsBuilder {
settings: PrimerSettings::default(),
product_size_ranges_set: false,
}
}
}
impl PrimerSettingsBuilder {
pub fn task(mut self, task: PrimerTask) -> Self {
self.settings.task = task;
self
}
pub fn pick_left_primer(mut self, pick: bool) -> Self {
self.settings.pick_left_primer = pick;
self
}
pub fn pick_right_primer(mut self, pick: bool) -> Self {
self.settings.pick_right_primer = pick;
self
}
pub fn pick_internal_oligo(mut self, pick: bool) -> Self {
self.settings.pick_internal_oligo = pick;
self
}
pub fn num_return(mut self, n: usize) -> Self {
self.settings.num_return = n;
self
}
pub fn primer_opt_tm(mut self, tm: f64) -> Self {
self.settings.primer.opt_tm = tm;
self
}
pub fn primer_min_tm(mut self, tm: f64) -> Self {
self.settings.primer.min_tm = tm;
self
}
pub fn primer_max_tm(mut self, tm: f64) -> Self {
self.settings.primer.max_tm = tm;
self
}
pub fn primer_opt_size(mut self, size: usize) -> Self {
self.settings.primer.opt_size = size;
self
}
pub fn primer_min_size(mut self, size: usize) -> Self {
self.settings.primer.min_size = size;
self
}
pub fn primer_max_size(mut self, size: usize) -> Self {
self.settings.primer.max_size = size;
self
}
pub fn primer_min_gc(mut self, gc: f64) -> Self {
self.settings.primer.min_gc = gc;
self
}
pub fn primer_max_gc(mut self, gc: f64) -> Self {
self.settings.primer.max_gc = gc;
self
}
pub fn primer_mv_conc(mut self, conc: f64) -> Self {
self.settings.primer.conditions.mv_conc = conc;
self
}
pub fn primer_dv_conc(mut self, conc: f64) -> Self {
self.settings.primer.conditions.dv_conc = conc;
self
}
pub fn primer_dntp_conc(mut self, conc: f64) -> Self {
self.settings.primer.conditions.dntp_conc = conc;
self
}
pub fn primer_dna_conc(mut self, conc: f64) -> Self {
self.settings.primer.conditions.dna_conc = conc;
self
}
pub fn product_size_range(mut self, min_bp: usize, max_bp: usize) -> Self {
if !self.product_size_ranges_set {
self.settings.product_size_ranges.clear();
self.product_size_ranges_set = true;
}
self.settings.product_size_ranges.push((min_bp, max_bp));
self
}
pub fn product_size_ranges(mut self, ranges: Vec<(usize, usize)>) -> Self {
self.settings.product_size_ranges = ranges;
self.product_size_ranges_set = true;
self
}
pub fn max_diff_tm(mut self, diff: f64) -> Self {
self.settings.max_diff_tm = diff;
self
}
pub fn tm_method(mut self, method: TmMethod) -> Self {
self.settings.tm_method = method;
self
}
pub fn salt_correction_method(mut self, method: SaltCorrectionMethod) -> Self {
self.settings.salt_correction_method = method;
self
}
pub fn build(self) -> Result<PrimerSettings> {
let p = &self.settings.primer;
if p.min_tm > p.opt_tm || p.opt_tm > p.max_tm {
return Err(Primer3Error::InvalidSetting(format!(
"primer Tm constraints violated: min_tm ({}) <= opt_tm ({}) <= max_tm ({}) required",
p.min_tm, p.opt_tm, p.max_tm,
)));
}
if p.min_size > p.opt_size || p.opt_size > p.max_size {
return Err(Primer3Error::InvalidSetting(format!(
"primer size constraints violated: min_size ({}) <= opt_size ({}) <= max_size ({}) required",
p.min_size, p.opt_size, p.max_size,
)));
}
if self.settings.product_size_ranges.is_empty() {
return Err(Primer3Error::InvalidSetting(
"product_size_ranges must not be empty".into(),
));
}
Ok(self.settings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_settings() {
let s = PrimerSettings::default();
assert_eq!(s.task, PrimerTask::PickPcrPrimers);
assert!(s.pick_left_primer);
assert!(s.pick_right_primer);
assert!(!s.pick_internal_oligo);
assert_eq!(s.num_return, 5);
assert_eq!(s.product_size_ranges, vec![(100, 300)]);
}
#[test]
fn test_default_oligo_settings() {
let o = OligoSettings::default();
assert_eq!(o.opt_size, 20);
assert_eq!(o.min_size, 18);
assert_eq!(o.max_size, 27);
assert!((o.opt_tm - 60.0).abs() < f64::EPSILON);
assert!((o.min_tm - 57.0).abs() < f64::EPSILON);
assert!((o.max_tm - 63.0).abs() < f64::EPSILON);
assert!((o.min_gc - 20.0).abs() < f64::EPSILON);
assert!((o.max_gc - 80.0).abs() < f64::EPSILON);
}
#[test]
fn test_default_sequencing_params() {
let s = SequencingParams::default();
assert_eq!(s.lead, 50);
assert_eq!(s.spacing, 500);
assert_eq!(s.interval, 250);
assert_eq!(s.accuracy, 20);
}
#[test]
fn test_builder_minimal() {
let s = PrimerSettings::builder().build().unwrap();
assert_eq!(s.task, PrimerTask::PickPcrPrimers);
assert_eq!(s.product_size_ranges, vec![(100, 300)]);
}
#[test]
fn test_builder_product_size_range_clears_default() {
let s = PrimerSettings::builder().product_size_range(50, 150).build().unwrap();
assert_eq!(s.product_size_ranges, vec![(50, 150)]);
}
#[test]
fn test_builder_multiple_product_size_ranges() {
let s = PrimerSettings::builder()
.product_size_range(50, 150)
.product_size_range(150, 300)
.build()
.unwrap();
assert_eq!(s.product_size_ranges, vec![(50, 150), (150, 300)]);
}
#[test]
fn test_builder_tm_validation() {
let result = PrimerSettings::builder().primer_min_tm(65.0).primer_opt_tm(60.0).build();
assert!(result.is_err());
}
#[test]
fn test_builder_size_validation() {
let result = PrimerSettings::builder().primer_min_size(25).primer_opt_size(20).build();
assert!(result.is_err());
}
#[test]
fn test_builder_empty_ranges_fails() {
let result = PrimerSettings::builder().product_size_ranges(vec![]).build();
assert!(result.is_err());
}
#[test]
fn test_builder_sets_task() {
let s = PrimerSettings::builder().task(PrimerTask::CheckPrimers).build().unwrap();
assert_eq!(s.task, PrimerTask::CheckPrimers);
}
#[test]
fn test_builder_sets_concentrations() {
let s =
PrimerSettings::builder().primer_mv_conc(100.0).primer_dna_conc(250.0).build().unwrap();
assert!((s.primer.conditions.mv_conc - 100.0).abs() < f64::EPSILON);
assert!((s.primer.conditions.dna_conc - 250.0).abs() < f64::EPSILON);
}
}