Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Validate PCR primers for specificity, dimers, hairpins, and secondary structures using primer3-py thermodynamic calculations. Check self-complementarity, heterodimer formation, and 3' stability. Use when validating primer specificity and properties.
.claude/skills/bio-primer-design-primer-validation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 28% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 189% | 0% |
| case-04 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 39% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 154% | 0% |
<!--
#
#
-->
Check primers for secondary structures, dimers, and other issues using primer3-py.
pythonimport primer3
pythonprimer = 'ATGCGATCGATCGATCGATC' hairpin = primer3.calc_hairpin(primer) print(f'Hairpin Tm: {hairpin.tm:.1f}C') print(f'Hairpin dG: {hairpin.dg:.1f} cal/mol') print(f'Hairpin dH: {hairpin.dh:.1f} cal/mol') print(f'Hairpin dS: {hairpin.ds:.1f} cal/mol/K') # Hairpin is problematic if Tm > annealing temp - 10 annealing_temp = 60.0 if hairpin.tm > annealing_temp - 10: print(f'WARNING: Hairpin Tm too high for annealing at {annealing_temp}C')
pythonprimer = 'ATGCGATCGATCGATCGATC' homodimer = primer3.calc_homodimer(primer) print(f'Homodimer Tm: {homodimer.tm:.1f}C') print(f'Homodimer dG: {homodimer.dg:.1f} cal/mol') # Self-dimer is problematic if Tm is close to annealing temp if homodimer.tm > 40: print('WARNING: Significant self-dimer potential')
pythonforward = 'ATGCGATCGATCGATCGATC' reverse = 'GCTAGCTAGCTAGCTAGCTA' heterodimer = primer3.calc_heterodimer(forward, reverse) print(f'Heterodimer Tm: {heterodimer.tm:.1f}C') print(f'Heterodimer dG: {heterodimer.dg:.1f} cal/mol') if heterodimer.tm > 40: print('WARNING: Significant primer dimer potential between forward and reverse')
pythondef validate_primer(primer_seq, name='Primer', annealing_temp=60.0): '''Comprehensive primer validation''' print(f'\n=== Validating {name}: {primer_seq} ===') # Basic properties tm = primer3.calc_tm(primer_seq) gc = (primer_seq.count('G') + primer_seq.count('C')) / len(primer_seq) * 100 print(f'Length: {len(primer_seq)}bp') print(f'Tm: {tm:.1f}C') print(f'GC: {gc:.1f}%') # Hairpin hairpin = primer3.calc_hairpin(primer_seq) print(f'Hairpin Tm: {hairpin.tm:.1f}C, dG: {hairpin.dg:.1f}') if hairpin.tm > annealing_temp - 10: print(' WARNING: Hairpin may interfere with annealing') # Homodimer homodimer = primer3.calc_homodimer(primer_seq) print(f'Homodimer Tm: {homodimer.tm:.1f}C, dG: {homodimer.dg:.1f}') if homodimer.tm > 40: print(' WARNING: Self-dimer potential') # 3' end stability (last 5 bases) end_3 = primer_seq[-5:] end_gc = (end_3.count('G') + end_3.count('C')) print(f"3' end (last 5bp): {end_3}, {end_gc} G/C bases") if end_gc > 3: print(" WARNING: 3' end may be too GC-rich") if end_gc == 0: print(" WARNING: 3' end lacks GC clamp") # Poly-X runs for base in 'ATGC': for run_len in range(5, len(primer_seq)): if base * run_len in primer_seq: print(f' WARNING: Contains {base}x{run_len} run') break return {'tm': tm, 'gc': gc, 'hairpin_tm': hairpin.tm, 'homodimer_tm': homodimer.tm} validate_primer('ATGCGATCGATCGATCGATC', 'Forward')
pythondef validate_primer_pair(forward, reverse, annealing_temp=60.0): '''Validate a primer pair''' print(f'\n=== Primer Pair Validation ===') print(f'Forward: {forward}') print(f'Reverse: {reverse}') # Individual primer checks fwd_tm = primer3.calc_tm(forward) rev_tm = primer3.calc_tm(reverse) print(f'\nTm Forward: {fwd_tm:.1f}C') print(f'Tm Reverse: {rev_tm:.1f}C') print(f'Tm Difference: {abs(fwd_tm - rev_tm):.1f}C') if abs(fwd_tm - rev_tm) > 2: print(' WARNING: Tm difference > 2C') # Heterodimer check heterodimer = primer3.calc_heterodimer(forward, reverse) print(f'\nHeterodimer Tm: {heterodimer.tm:.1f}C') print(f'Heterodimer dG: {heterodimer.dg:.1f} cal/mol') if heterodimer.tm > 40: print(' WARNING: Significant primer dimer potential') # Check 3' complementarity specifically end_heterodimer = primer3.calc_heterodimer(forward[-6:], reverse[-6:]) print(f"3' end heterodimer Tm: {end_heterodimer.tm:.1f}C") if end_heterodimer.tm > 20: print(" WARNING: 3' ends may form stable dimer") # Individual hairpins and homodimers fwd_hairpin = primer3.calc_hairpin(forward) rev_hairpin = primer3.calc_hairpin(reverse) fwd_homodimer = primer3.calc_homodimer(forward) rev_homodimer = primer3.calc_homodimer(reverse) print(f'\nForward hairpin Tm: {fwd_hairpin.tm:.1f}C') print(f'Reverse hairpin Tm: {rev_hairpin.tm:.1f}C') print(f'Forward homodimer Tm: {fwd_homodimer.tm:.1f}C') print(f'Reverse homodimer Tm: {rev_homodimer.tm:.1f}C') return { 'fwd_tm': fwd_tm, 'rev_tm': rev_tm, 'heterodimer_tm': heterodimer.tm, 'fwd_hairpin_tm': fwd_hairpin.tm, 'rev_hairpin_tm': rev_hairpin.tm, } validate_primer_pair('ATGCGATCGATCGATCGATC', 'GCTAGCTAGCTAGCTAGCTA')
python# Use native calc_end_stability for 3' end thermodynamics primer = 'ATGCGATCGATCGATCGATC' # Calculate stability of last 5 bases (default) end_stability = primer3.calc_end_stability(primer) print(f"3' end stability: dG = {end_stability.dg:.1f} cal/mol") # More negative dG = more stable 3' end = better extension but higher mispriming risk if end_stability.dg < -9000: print(' Note: Very stable 3\' end - good extension but watch for mispriming')
python# For high-throughput screening, use Tm-only functions (return float, not ThermoResult) primer = 'ATGCGATCGATCGATCGATC' # Quick hairpin Tm check hairpin_tm = primer3.calc_hairpin_tm(primer) print(f'Hairpin Tm: {hairpin_tm:.1f}C') # Quick homodimer Tm check homodimer_tm = primer3.calc_homodimer_tm(primer) print(f'Homodimer Tm: {homodimer_tm:.1f}C') # Quick heterodimer Tm check forward = 'ATGCGATCGATCGATCGATC' reverse = 'GCTAGCTAGCTAGCTAGCTA' heterodimer_tm = primer3.calc_heterodimer_tm(forward, reverse) print(f'Heterodimer Tm: {heterodimer_tm:.1f}C')
pythondef quick_screen_primers(primer_list, max_hairpin_tm=45, max_homodimer_tm=45): '''Fast screening using Tm-only functions''' passed = [] failed = [] for seq in primer_list: hairpin_tm = primer3.calc_hairpin_tm(seq) homodimer_tm = primer3.calc_homodimer_tm(seq) if hairpin_tm < max_hairpin_tm and homodimer_tm < max_homodimer_tm: passed.append(seq) else: failed.append((seq, hairpin_tm, homodimer_tm)) return passed, failed primers = ['ATGCGATCGATCGATCGATC', 'GCGCGCGCGCGCGCGCGCGC', 'ATATATATATATATATATAT'] passed, failed = quick_screen_primers(primers) print(f'Passed: {len(passed)}, Failed: {len(failed)}')
pythondef check_3prime_specificity(primer_seq): '''Check if 3' end is suitable for specific priming''' end_5bp = primer_seq[-5:] end_3bp = primer_seq[-3:] # Count G/C in last 5 bases gc_5 = end_5bp.count('G') + end_5bp.count('C') # Check last base last_base = primer_seq[-1] print(f"3' sequence: ...{end_5bp}") print(f"G/C in last 5bp: {gc_5}") print(f"Last base: {last_base}") # Ideal: 1-2 G/C in last 5, ending in G or C if gc_5 == 0: print(' Consider: No GC clamp at 3\' end') elif gc_5 > 3: print(' Consider: 3\' end may be too stable (mispriming risk)') if last_base in 'AT': print(' Consider: Ending in A/T may reduce specificity') return {'gc_5': gc_5, 'last_base': last_base} check_3prime_specificity('ATGCGATCGATCGATCGATC')
pythonimport pandas as pd def batch_validate_primers(primers): '''Validate multiple primers''' results = [] for name, seq in primers.items(): tm = primer3.calc_tm(seq) gc = (seq.count('G') + seq.count('C')) / len(seq) * 100 hairpin = primer3.calc_hairpin(seq) homodimer = primer3.calc_homodimer(seq) results.append({ 'name': name, 'sequence': seq, 'length': len(seq), 'tm': round(tm, 1), 'gc_pct': round(gc, 1), 'hairpin_tm': round(hairpin.tm, 1), 'homodimer_tm': round(homodimer.tm, 1), }) return pd.DataFrame(results) primers = { 'GAPDH_F': 'GTCTCCTCTGACTTCAACAGCG', 'GAPDH_R': 'ACCACCCTGTTGCTGTAGCCAA', 'ACTB_F': 'CATGTACGTTGCTATCCAGGC', 'ACTB_R': 'CTCCTTAATGTCACGCACGAT', } df = batch_validate_primers(primers) print(df.to_string(index=False))
pythonprimer = 'ATGCGATCGATCGATCGATC' # Standard conditions tm_standard = primer3.calc_tm(primer) hairpin_standard = primer3.calc_hairpin(primer) # Custom salt conditions tm_custom = primer3.calc_tm(primer, mv_conc=100.0, dv_conc=2.0, dntp_conc=0.4, dna_conc=200.0) hairpin_custom = primer3.calc_hairpin(primer, mv_conc=100.0, dv_conc=2.0) print(f'Standard conditions: Tm={tm_standard:.1f}C, Hairpin Tm={hairpin_standard.tm:.1f}C') print(f'Custom conditions: Tm={tm_custom:.1f}C, Hairpin Tm={hairpin_custom.tm:.1f}C')
| Property | Acceptable | Optimal | |----------|------------|---------| | Length | 18-30 bp | 20-25 bp | | Tm | 55-65C | 58-62C | | GC% | 35-65% | 45-55% | | Hairpin Tm | <45C | <35C | | Homodimer Tm | <45C | <35C | | Heterodimer Tm | <45C | <35C | | 3' GC (last 5bp) | 1-3 | 2 |
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 24,646 | 32,378 | +31% | 1 | 1 | 0% | 5,690 | 7,282 | +28% | 0 | 0 | — |
case-02 | pass→pass | 18,218 | 7,953 | -56% | 1 | 1 | 0% | 3,986 | 5,201 | +30% | 0 | 0 | — |
case-03 | fail→pass | 7,692 | 4,682 | -39% | 1 | 1 | 0% | 1,578 | 4,561 | +189% | 0 | 0 | — |
case-04 | fail→pass | 28,186 | 9,338 | -67% | 1 | 1 | 0% | 5,777 | 5,634 | -2% | 0 | 0 | — |
case-09 | pass→pass | 12,562 | 17,339 | +38% | 1 | 1 | 0% | 2,601 | 5,482 | +111% | 0 | 0 | — |
case-10 | pass→pass | 10,179 | 5,661 | -44% | 1 | 1 | 0% | 2,180 | 4,962 | +128% | 0 | 0 | — |
case-11 | fail→fail | 12,387 | 8,015 | -35% | 1 | 1 | 0% | 2,645 | 4,755 | +80% | 0 | 0 | — |
case-12 | pass→pass | 12,366 | 7,452 | -40% | 1 | 1 | 0% | 2,652 | 5,385 | +103% | 0 | 0 | — |
case-13 | fail→pass | 16,667 | 4,835 | -71% | 1 | 1 | 0% | 3,350 | 4,649 | +39% | 0 | 0 | — |
case-14 | fail→pass | 9,572 | 6,517 | -32% | 1 | 1 | 0% | 2,019 | 5,130 | +154% | 0 | 0 | — |
case-15 | fail→pass | 14,156 | 5,858 | -59% | 1 | 1 | 0% | 2,834 | 4,790 | +69% | 0 | 0 | — |
case-20 | pass→fail | 11,949 | 11,935 | -0% | 1 | 1 | 0% | 2,587 | 5,974 | +131% | 0 | 0 | — |
case-21 | pass→pass | 17,903 | 11,130 | -38% | 1 | 1 | 0% | 3,746 | 6,050 | +62% | 0 | 0 | — |
case-22 | pass→pass | 10,405 | 10,212 | -2% | 1 | 1 | 0% | 2,435 | 5,918 | +143% | 0 | 0 | — |
case-05 | pass→pass | 12,809 | 3,339 | -74% | 1 | 1 | 0% | 2,530 | 4,352 | +72% | 0 | 0 | — |
case-06 | fail→pass | 15,812 | 5,617 | -64% | 1 | 1 | 0% | 2,775 | 4,780 | +72% | 0 | 0 | — |
case-07 | pass→pass | 8,742 | 4,747 | -46% | 1 | 1 | 0% | 1,861 | 4,629 | +149% | 0 | 0 | — |
case-08 | fail→fail | 4,702 | 7,825 | +66% | 1 | 1 | 0% | 868 | 5,242 | +504% | 0 | 0 | — |
case-16 | pass→pass | 8,156 | 3,093 | -62% | 1 | 1 | 0% | 1,552 | 4,214 | +172% | 0 | 0 | — |
case-17 | fail→pass | 27,950 | 3,664 | -87% | 1 | 1 | 0% | 5,530 | 4,274 | -23% | 0 | 0 | — |
case-18 | pass→pass | 7,940 | 2,361 | -70% | 1 | 1 | 0% | 1,670 | 4,109 | +146% | 0 | 0 | — |
case-19 | pass→pass | 15,210 | 5,259 | -65% | 1 | 1 | 0% | 3,022 | 4,873 | +61% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | +36% |
Other measured skills in the registry, with their headline benchmark lift.