Skip to main content

dnagent_domain/
gibson.rs

1//! Explicit PCR-tail Gibson candidates, not thermodynamically optimised primers.
2use crate::digest::{FragmentStrand, reverse_complement};
3use crate::fragment_annotations::{FeatureMapping, map_feature};
4use crate::ligation::Orientation;
5use crate::{DomainError, SequenceRecord, Topology};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9pub const MAX_BASES: usize = 1_000_000;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct CoreSelection {
14    pub input: usize,
15    pub start: usize,
16    pub length: usize,
17    pub orientation: Orientation,
18}
19
20/// How a fragment reaches the reaction. A `Provided` fragment (restriction digest,
21/// synthesis, a stock linear DNA) is used as it is: it gets no primers, and the homology
22/// at its junctions is written into the neighbouring PCR fragments' primers instead.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum Preparation {
26    Pcr,
27    Provided,
28}
29
30#[derive(Debug, Error)]
31pub enum GibsonError {
32    #[error("invalid Gibson design: {0}")]
33    Invalid(&'static str),
34    #[error("component {component}: {reason}")]
35    Component {
36        component: usize,
37        reason: &'static str,
38    },
39    #[error(transparent)]
40    Coordinates(#[from] DomainError),
41}
42
43#[derive(Debug, Clone, Serialize)]
44pub struct PrimerCandidate {
45    pub sequence_5to3: String,
46    pub annealing_sequence_5to3: String,
47    pub tail_sequence_5to3: String,
48    pub annealing_gc_bases: usize,
49}
50
51#[derive(Debug, Clone, Serialize)]
52pub struct GibsonComponent {
53    pub selection: CoreSelection,
54    pub preparation: Preparation,
55    pub product_start: usize,
56    pub core_sequence_5to3: String,
57    /// The fragment as it enters the reaction: the core plus any primer tails for a PCR
58    /// fragment, the core itself for a provided one.
59    pub fragment_sequence_5to3: String,
60    /// Absent for provided fragments, which are not amplified.
61    pub forward_primer: Option<PrimerCandidate>,
62    pub reverse_primer: Option<PrimerCandidate>,
63    /// Core-local, not PCR-tail or inferred fused-product annotations.
64    pub annotations: Vec<FeatureMapping>,
65}
66
67#[derive(Debug, Clone, Serialize)]
68pub struct GibsonJunction {
69    pub after_component: usize,
70    pub before_component: usize,
71    pub closure: bool,
72    pub product_start: usize,
73    pub overlap_sequence_5to3: String,
74    /// Which primer carries this overlap as a 5-prime tail: the upstream fragment's
75    /// reverse primer, or the downstream fragment's forward primer when the upstream
76    /// fragment is provided and so cannot be extended. `None` when the fragments already
77    /// overlap and no primer adds anything.
78    pub added_by: Option<TailCarrier>,
79}
80
81/// The primer that writes a junction's overlap into its fragment (1-based component).
82#[derive(Debug, Clone, Copy, Serialize)]
83#[serde(tag = "primer", content = "component", rename_all = "snake_case")]
84pub enum TailCarrier {
85    ReverseOf(usize),
86    ForwardOf(usize),
87}
88
89#[derive(Debug, Clone, Serialize)]
90pub struct GibsonReport {
91    pub inputs: Vec<SequenceRecord>,
92    pub topology: Topology,
93    pub overlap_length: usize,
94    pub annealing_length: Option<usize>,
95    pub product_sequence_5to3: String,
96    pub components: Vec<GibsonComponent>,
97    pub junctions: Vec<GibsonJunction>,
98    pub assumptions: Vec<&'static str>,
99}
100
101pub fn validate_request(
102    input_count: usize,
103    selected: &[CoreSelection],
104    overlap: usize,
105    annealing: usize,
106) -> Result<(), GibsonError> {
107    if !(1..=16).contains(&input_count) || selected.is_empty() || selected.len() > 128 {
108        return Err(GibsonError::Invalid(
109            "expected 1..=16 templates and 1..=128 cores",
110        ));
111    }
112    if !(20..=60).contains(&overlap) || !(18..=40).contains(&annealing) {
113        return Err(GibsonError::Invalid(
114            "overlap must be 20..=60 and annealing length 18..=40 bases",
115        ));
116    }
117    let mut total = 0usize;
118    for selection in selected {
119        if selection.input == 0
120            || selection.input > input_count
121            || selection.length < overlap.max(2 * annealing)
122        {
123            return Err(GibsonError::Invalid(
124                "invalid template number or core shorter than overlap/two annealing regions",
125            ));
126        }
127        total = total
128            .checked_add(selection.length)
129            .filter(|n| *n <= MAX_BASES)
130            .ok_or(GibsonError::Invalid("product exceeds 1,000,000 bases"))?;
131    }
132    Ok(())
133}
134
135pub fn design(
136    records: &[SequenceRecord],
137    selected: &[CoreSelection],
138    preparation: &[Preparation],
139    topology: Topology,
140    overlap: usize,
141    annealing: usize,
142) -> Result<GibsonReport, GibsonError> {
143    validate_request(records.len(), selected, overlap, annealing)?;
144    let mut report = design_with_lengths(
145        records,
146        selected,
147        preparation,
148        topology,
149        overlap,
150        &vec![(annealing, annealing); selected.len()],
151    )?;
152    report.assumptions.extend([
153        "Primer candidates use explicit fixed annealing lengths; GC counts are descriptive, not optimisation or thermodynamic validation",
154        "Exact primer-site uniqueness is checked on each full template in both orientations; mismatched/off-template binding, dimers, hairpins and melting temperatures are not evaluated",
155    ]);
156    Ok(report)
157}
158
159pub(crate) fn validate_templates(records: &[SequenceRecord]) -> Result<(), GibsonError> {
160    if records.iter().any(|r| {
161        r.sequence().len() > MAX_BASES
162            || !r.sequence().as_str().bytes().all(|b| b"ACGT".contains(&b))
163    }) {
164        return Err(GibsonError::Invalid(
165            "templates must be unambiguous ACGT and at most 1,000,000 bases",
166        ));
167    }
168    Ok(())
169}
170
171/// Plan each junction: which sequence is the shared overlap, and which primer writes it.
172/// A fragment's forward tail can be decided while visiting the fragment before it, so this
173/// runs before any primer is built.
174fn plan_junctions(
175    cores: &[String],
176    preparation: &[Preparation],
177    starts: &[usize],
178    product_sequence_5to3: &str,
179    topology: Topology,
180    overlap: usize,
181) -> Result<Vec<GibsonJunction>, GibsonError> {
182    let mut junctions: Vec<GibsonJunction> = Vec::new();
183    for i in 0..cores.len() {
184        let closure = i + 1 == cores.len();
185        if closure && topology != Topology::Circular {
186            continue;
187        }
188        let next = (i + 1) % cores.len();
189        let (overlap_sequence_5to3, added_by, product_start) = match (
190            preparation[i],
191            preparation[next],
192        ) {
193            // A provided fragment cannot be extended, so its neighbour's forward
194            // primer copies the provided fragment's own 3-prime end.
195            (Preparation::Provided, Preparation::Pcr) => (
196                cores[i][cores[i].len() - overlap..].to_owned(),
197                TailCarrier::ForwardOf(next + 1),
198                starts[i] + cores[i].len() - overlap,
199            ),
200            (Preparation::Provided, Preparation::Provided) => {
201                return Err(GibsonError::Component {
202                    component: i + 1,
203                    reason: "both fragments at this junction are provided, so no primer can add the overlap; give them existing overlaps instead",
204                });
205            }
206            // Otherwise keep the overlap in the downstream fragment's 5-prime end.
207            (Preparation::Pcr, _) => (
208                cores[next][..overlap].to_owned(),
209                TailCarrier::ReverseOf(i + 1),
210                if closure { 0 } else { starts[next] },
211            ),
212        };
213        if !unique_duplex_site(product_sequence_5to3, &overlap_sequence_5to3, topology) {
214            return Err(GibsonError::Component {
215                component: i + 1,
216                reason: "overlap is repeated or reverse-complement ambiguous in the intended product",
217            });
218        }
219        junctions.push(GibsonJunction {
220            after_component: i + 1,
221            before_component: next + 1,
222            closure,
223            product_start,
224            overlap_sequence_5to3,
225            added_by: Some(added_by),
226        });
227    }
228    Ok(junctions)
229}
230
231pub(crate) fn design_with_lengths(
232    records: &[SequenceRecord],
233    selected: &[CoreSelection],
234    preparation: &[Preparation],
235    topology: Topology,
236    overlap: usize,
237    lengths: &[(usize, usize)],
238) -> Result<GibsonReport, GibsonError> {
239    validate_preparation(selected, preparation, overlap)?;
240    validate_lengths(records.len(), selected, overlap, lengths, preparation)?;
241    validate_templates(records)?;
242    let cores = selected
243        .iter()
244        .map(|s| core(&records[s.input - 1], s))
245        .collect::<Result<Vec<_>, _>>()?;
246    let product_sequence_5to3 = cores.concat();
247    let mut starts = Vec::with_capacity(cores.len());
248    let mut at = 0usize;
249    for sequence in &cores {
250        starts.push(at);
251        at += sequence.len();
252    }
253
254    for (i, (selection, sequence)) in selected.iter().zip(&cores).enumerate() {
255        if preparation[i] == Preparation::Provided {
256            continue;
257        }
258        let template = &records[selection.input - 1];
259        let reverse_anneal = reverse_complement(&sequence[sequence.len() - lengths[i].1..]);
260        for annealing in [&sequence[..lengths[i].0], reverse_anneal.as_str()] {
261            if !unique_duplex_site(template.sequence().as_str(), annealing, template.topology()) {
262                return Err(GibsonError::Component {
263                    component: i + 1,
264                    reason: "primer annealing sequence lacks a unique exact site on its full template",
265                });
266            }
267        }
268    }
269
270    let junctions = plan_junctions(
271        &cores,
272        preparation,
273        &starts,
274        &product_sequence_5to3,
275        topology,
276        overlap,
277    )?;
278
279    let mut components = Vec::new();
280    for (i, (selection, sequence)) in selected.iter().zip(&cores).enumerate() {
281        let template = &records[selection.input - 1];
282        let tail_of = |want: TailCarrier| -> &str {
283            junctions
284                .iter()
285                .find(|j| match (j.added_by, Some(want)) {
286                    (Some(TailCarrier::ReverseOf(a)), Some(TailCarrier::ReverseOf(b)))
287                    | (Some(TailCarrier::ForwardOf(a)), Some(TailCarrier::ForwardOf(b))) => a == b,
288                    _ => false,
289                })
290                .map_or("", |j| j.overlap_sequence_5to3.as_str())
291        };
292        let forward_tail = tail_of(TailCarrier::ForwardOf(i + 1));
293        let reverse_tail = tail_of(TailCarrier::ReverseOf(i + 1));
294        let annotations = core_annotations(template, selection, sequence)?;
295        let (fragment_sequence_5to3, forward_primer, reverse_primer) = match preparation[i] {
296            Preparation::Provided => (sequence.clone(), None, None),
297            Preparation::Pcr => {
298                let forward_anneal = &sequence[..lengths[i].0];
299                let reverse_anneal = reverse_complement(&sequence[sequence.len() - lengths[i].1..]);
300                (
301                    format!("{forward_tail}{sequence}{reverse_tail}"),
302                    Some(primer(forward_anneal, forward_tail)),
303                    Some(primer(&reverse_anneal, &reverse_complement(reverse_tail))),
304                )
305            }
306        };
307        components.push(GibsonComponent {
308            selection: selection.clone(),
309            preparation: preparation[i],
310            product_start: starts[i],
311            core_sequence_5to3: sequence.clone(),
312            fragment_sequence_5to3,
313            forward_primer,
314            reverse_primer,
315            annotations,
316        });
317    }
318    verify_assembly(&components, &junctions, &product_sequence_5to3)?;
319    Ok(GibsonReport {
320        inputs: records.to_vec(),
321        topology,
322        overlap_length: overlap,
323        annealing_length: lengths
324            .iter()
325            .all(|&(f, r)| f == lengths[0].0 && r == f)
326            .then_some(lengths[0].0),
327        product_sequence_5to3,
328        components,
329        junctions,
330        assumptions: vec![
331            "PCR-tail design mode: requested cores concatenate without deduplicating endogenous sequence; this is not intake of pre-existing overlapping fragments",
332            "Each junction's overlap is written by one primer: the upstream fragment's reverse primer, or the downstream fragment's forward primer where the upstream fragment is provided rather than amplified",
333            "Provided fragments (restriction digest, synthesis, stock linear DNA) receive no primers; their ends are used exactly as given and are not verified against any digest here",
334            "Overlaps must occur exactly once in the intended product across both orientations, including circular-origin matches",
335            "Assumes clean sequence-faithful PCR and ideal overlap-directed assembly; reaction yield, enzyme conditions and experimental validity are not predicted",
336            "Core-local annotations map through product_start; feature reunion/fusion, translations and biological function are not inferred",
337            "Synthetic overlap copies are removed exactly once at each planned junction; final core bases remain unchanged",
338            "Source primers are provenance only, not reused binding-site predictions; opaque SnapGene metadata is not exported",
339            "Linear single-core designs are PCR-only, without an assembly junction; circular single-core designs model PCR-assisted reclosure",
340        ],
341    })
342}
343
344/// A provided fragment is not amplified, so it must already carry any overlap taken from
345/// its own ends: up to one at each junction.
346fn validate_preparation(
347    selected: &[CoreSelection],
348    preparation: &[Preparation],
349    overlap: usize,
350) -> Result<(), GibsonError> {
351    if preparation.len() != selected.len() {
352        return Err(GibsonError::Invalid(
353            "every core needs an explicit preparation",
354        ));
355    }
356    for (i, selection) in selected.iter().enumerate() {
357        if preparation[i] == Preparation::Provided && selection.length < 2 * overlap {
358            return Err(GibsonError::Component {
359                component: i + 1,
360                reason: "a provided fragment must be at least twice the overlap length",
361            });
362        }
363    }
364    Ok(())
365}
366
367fn validate_lengths(
368    input_count: usize,
369    selected: &[CoreSelection],
370    overlap: usize,
371    lengths: &[(usize, usize)],
372    preparation: &[Preparation],
373) -> Result<(), GibsonError> {
374    validate_request(input_count, selected, overlap, 18)?;
375    if lengths.len() != selected.len()
376        || lengths
377            .iter()
378            .zip(selected)
379            .zip(preparation)
380            .filter(|(_, p)| **p == Preparation::Pcr)
381            .any(|((&(f, r), s), _)| {
382                !(18..=40).contains(&f) || !(18..=40).contains(&r) || f + r > s.length
383            })
384    {
385        return Err(GibsonError::Invalid(
386            "invalid or overlapping primer annealing lengths",
387        ));
388    }
389    Ok(())
390}
391
392pub(crate) fn core_annotations(
393    template: &SequenceRecord,
394    selection: &CoreSelection,
395    sequence: &str,
396) -> Result<Vec<FeatureMapping>, DomainError> {
397    let interval = FragmentStrand {
398        source_start: selection.start,
399        length: selection.length,
400        sequence_5to3: sequence.into(),
401    };
402    template
403        .features()
404        .iter()
405        .map(|f| {
406            map_feature(
407                f,
408                &interval,
409                Topology::Linear,
410                template.sequence().len(),
411                selection.orientation == Orientation::Reverse,
412            )
413        })
414        .collect::<Result<Vec<_>, _>>()
415        .map(|m| m.into_iter().filter(|m| !m.parts.is_empty()).collect())
416}
417
418pub fn core(record: &SequenceRecord, selection: &CoreSelection) -> Result<String, GibsonError> {
419    let n = record.sequence().len();
420    let end = selection
421        .start
422        .checked_add(selection.length)
423        .ok_or(GibsonError::Invalid("source interval overflow"))?;
424    if selection.start >= n
425        || selection.length > n
426        || (record.topology() == Topology::Linear && end > n)
427    {
428        return Err(GibsonError::Invalid(
429            "source interval outside template bounds/topology",
430        ));
431    }
432    let sequence = record.sequence().as_str();
433    let core = if end <= n {
434        sequence[selection.start..end].to_owned()
435    } else {
436        format!("{}{}", &sequence[selection.start..], &sequence[..end - n])
437    };
438    Ok(if selection.orientation == Orientation::Reverse {
439        reverse_complement(&core)
440    } else {
441        core
442    })
443}
444
445pub(crate) fn primer(annealing: &str, tail: &str) -> PrimerCandidate {
446    PrimerCandidate {
447        sequence_5to3: format!("{tail}{annealing}"),
448        annealing_sequence_5to3: annealing.into(),
449        tail_sequence_5to3: tail.into(),
450        annealing_gc_bases: annealing
451            .bytes()
452            .filter(|b| matches!(b, b'G' | b'C'))
453            .count(),
454    }
455}
456
457#[must_use]
458pub fn unique_duplex_site(sequence: &str, motif: &str, topology: Topology) -> bool {
459    let rc = reverse_complement(motif);
460    if rc == motif || motif.len() > sequence.len() {
461        return false;
462    }
463    let extended = if topology == Topology::Circular {
464        format!("{sequence}{}", &sequence[..motif.len() - 1])
465    } else {
466        sequence.into()
467    };
468    extended
469        .as_bytes()
470        .windows(motif.len())
471        .filter(|w| *w == motif.as_bytes() || *w == rc.as_bytes())
472        .take(2)
473        .count()
474        == 1
475}
476
477fn verify_assembly(
478    components: &[GibsonComponent],
479    junctions: &[GibsonJunction],
480    expected: &str,
481) -> Result<(), GibsonError> {
482    let mut assembled = components[0].fragment_sequence_5to3.clone();
483    for junction in junctions {
484        let overlap = &junction.overlap_sequence_5to3;
485        let next = &components[junction.before_component - 1].fragment_sequence_5to3;
486        if !assembled.ends_with(overlap) || !next.starts_with(overlap) {
487            return Err(GibsonError::Invalid("fragment junction mismatch"));
488        }
489        if junction.closure {
490            assembled.truncate(assembled.len() - overlap.len());
491        } else {
492            assembled.push_str(&next[overlap.len()..]);
493        }
494    }
495    if assembled != expected {
496        return Err(GibsonError::Invalid(
497            "fragment assembly does not conserve requested cores",
498        ));
499    }
500    Ok(())
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use crate::DnaSeq;
507    fn record() -> SequenceRecord {
508        // Deterministic synthetic original, not a biological reference sequence.
509        let mut state = 1_234_567_u64;
510        let sequence: String = (0..300)
511            .map(|_| {
512                state ^= state << 13;
513                state ^= state >> 7;
514                state ^= state << 17;
515                char::from(b"ACGT"[(state % 4) as usize])
516            })
517            .collect();
518        SequenceRecord::new(
519            "synthetic",
520            DnaSeq::new(sequence).unwrap(),
521            Topology::Circular,
522            vec![],
523            vec![],
524        )
525        .unwrap()
526    }
527    fn selections() -> Vec<CoreSelection> {
528        vec![
529            CoreSelection {
530                input: 1,
531                start: 270,
532                length: 90,
533                orientation: Orientation::Forward,
534            },
535            CoreSelection {
536                input: 1,
537                start: 90,
538                length: 100,
539                orientation: Orientation::Reverse,
540            },
541        ]
542    }
543    #[test]
544    fn circular_origin_reverse_core_and_pcr_tails_reconstruct_product() {
545        let report = design(
546            &[record()],
547            &selections(),
548            &[Preparation::Pcr; 2],
549            Topology::Circular,
550            25,
551            22,
552        )
553        .unwrap();
554        assert_eq!(report.product_sequence_5to3.len(), 190);
555        assert_eq!(report.junctions.len(), 2);
556        assert!(report.junctions[1].closure);
557        assert_eq!(
558            report.components[0]
559                .forward_primer
560                .as_ref()
561                .unwrap()
562                .tail_sequence_5to3,
563            ""
564        );
565        assert_eq!(
566            report.components[0]
567                .reverse_primer
568                .as_ref()
569                .unwrap()
570                .tail_sequence_5to3,
571            reverse_complement(&report.components[1].core_sequence_5to3[..25])
572        );
573    }
574    #[test]
575    fn linear_last_primer_has_no_tail_and_source_errors_fail() {
576        let r = record();
577        let report = design(
578            std::slice::from_ref(&r),
579            &selections(),
580            &[Preparation::Pcr; 2],
581            Topology::Linear,
582            25,
583            22,
584        )
585        .unwrap();
586        assert_eq!(
587            report.components[1]
588                .reverse_primer
589                .as_ref()
590                .unwrap()
591                .tail_sequence_5to3,
592            ""
593        );
594        assert_eq!(report.junctions.len(), 1);
595        let mut bad = selections();
596        bad[0].start = usize::MAX;
597        assert!(
598            design(
599                std::slice::from_ref(&r),
600                &bad,
601                &[Preparation::Pcr; 2],
602                Topology::Linear,
603                25,
604                22
605            )
606            .is_err()
607        );
608        assert!(
609            design(
610                &[r],
611                &selections(),
612                &[Preparation::Pcr; 2],
613                Topology::Linear,
614                19,
615                22
616            )
617            .is_err()
618        );
619    }
620    #[test]
621    fn resource_and_template_bounds_are_explicit() {
622        let mut selected = selections();
623        assert!(validate_request(17, &selected, 25, 22).is_err());
624        assert!(validate_request(0, &selected, 25, 22).is_err());
625        assert!(validate_request(1, &vec![selected[0].clone(); 129], 25, 22).is_err());
626        selected.truncate(1);
627        selected[0].length = MAX_BASES;
628        assert!(validate_request(1, &selected, 60, 40).is_ok());
629        selected[0].length += 1;
630        assert!(validate_request(1, &selected, 60, 40).is_err());
631        selected[0].length = usize::MAX;
632        assert!(validate_request(1, &selected, 60, 40).is_err());
633        let mut ambiguous = record().sequence().as_str().to_owned();
634        ambiguous.replace_range(0..1, "N");
635        let r = SequenceRecord::new(
636            "synthetic",
637            DnaSeq::new(ambiguous).unwrap(),
638            Topology::Circular,
639            vec![],
640            vec![],
641        )
642        .unwrap();
643        assert!(
644            design(
645                &[r],
646                &selections(),
647                &[Preparation::Pcr; 2],
648                Topology::Circular,
649                25,
650                22
651            )
652            .is_err()
653        );
654    }
655
656    /// The usual bench design: the vector is cut and used as it is, so both overlaps are
657    /// written into the insert's primers and the vector gets none.
658    #[test]
659    fn a_provided_vector_puts_both_tails_on_the_amplified_insert() {
660        let report = design(
661            &[record()],
662            &selections(),
663            &[Preparation::Provided, Preparation::Pcr],
664            Topology::Circular,
665            25,
666            22,
667        )
668        .unwrap();
669        let (vector, insert) = (&report.components[0], &report.components[1]);
670        assert!(vector.forward_primer.is_none() && vector.reverse_primer.is_none());
671        assert_eq!(vector.fragment_sequence_5to3, vector.core_sequence_5to3);
672
673        let forward = insert.forward_primer.as_ref().unwrap();
674        let reverse = insert.reverse_primer.as_ref().unwrap();
675        assert_eq!(forward.tail_sequence_5to3.len(), 25);
676        assert_eq!(reverse.tail_sequence_5to3.len(), 25);
677        // The insert copies the provided vector's own ends: its 3-prime end before the
678        // insert, and its 5-prime start after it.
679        assert_eq!(
680            forward.tail_sequence_5to3,
681            vector.core_sequence_5to3[vector.core_sequence_5to3.len() - 25..]
682        );
683        assert_eq!(
684            reverse.tail_sequence_5to3,
685            reverse_complement(&vector.core_sequence_5to3[..25])
686        );
687        assert!(matches!(
688            report.junctions[0].added_by,
689            Some(TailCarrier::ForwardOf(2))
690        ));
691        assert!(matches!(
692            report.junctions[1].added_by,
693            Some(TailCarrier::ReverseOf(2))
694        ));
695    }
696
697    #[test]
698    fn two_provided_fragments_cannot_form_a_junction() {
699        let error = design(
700            &[record()],
701            &selections(),
702            &[Preparation::Provided; 2],
703            Topology::Circular,
704            25,
705            22,
706        )
707        .unwrap_err();
708        assert!(format!("{error}").contains("both fragments at this junction are provided"));
709    }
710
711    #[test]
712    fn preparation_must_cover_every_core() {
713        assert!(
714            design(
715                &[record()],
716                &selections(),
717                &[Preparation::Pcr],
718                Topology::Circular,
719                25,
720                22
721            )
722            .is_err()
723        );
724    }
725
726    #[test]
727    fn exact_uniqueness_includes_opposite_strand_and_origin_crossings() {
728        assert!(!unique_duplex_site(
729            "AACGTGGACGTT",
730            "AACGT",
731            Topology::Linear
732        ));
733        assert!(unique_duplex_site("ACGTTCGGA", "GGAAC", Topology::Circular));
734        assert!(!unique_duplex_site("ACGTTCGGA", "GGAAC", Topology::Linear));
735    }
736
737    #[test]
738    fn repeated_primers_overlaps_and_palindromes_are_rejected() {
739        assert!(!unique_duplex_site("AAAAAA", "AAA", Topology::Linear));
740        assert!(!unique_duplex_site("AAGCTT", "AAGCTT", Topology::Linear));
741        assert!(unique_duplex_site("GACCTAGT", "TGA", Topology::Circular));
742        let s = selections()[0].clone();
743        assert!(
744            design(
745                &[record()],
746                &[s.clone(), s],
747                &[Preparation::Pcr; 2],
748                Topology::Circular,
749                25,
750                22
751            )
752            .is_err()
753        );
754    }
755}