Skip to content

Reporting error for high self-complementary sequences #93

@Trecek

Description

@Trecek

Description

When using PRIMER_TASK=generic with sequences where all primers should be rejected for high self-complementarity, the EXPLAIN output shows an incorrect "ok 1" count instead of "ok 0". This occurs due to a lazy evaluation optimization where one primer's self-complementarity is never calculated if no valid primer pairs can be formed.

This can make reporting stats screwy for repetitive regions and throw off unit testing. Otherwise, I can't imagine a functional impact. There may be a slight performance impact as the error causes all primer pairs to be evaluated, but they fail fast since the pairs don't exist.

Steps to Reproduce

# Minimal test case
echo 'SEQUENCE_ID=gatc_bug
SEQUENCE_TEMPLATE=GATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC
PRIMER_TASK=generic
PRIMER_MIN_SIZE=20
PRIMER_MAX_SIZE=20
PRIMER_EXPLAIN_FLAG=1
PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT=0
PRIMER_PRODUCT_SIZE_RANGE=30-52
=' | primer3_core

Note: Only affects PRIMER_TASK=generic (not check_primers or pick_primer_list)
Note: The "ok 1" primer is never returned (PRIMER_LEFT_NUM_RETURNED=0)

Expected Behavior

PRIMER_LEFT_EXPLAIN=considered 23, low tm 6, high any compl 17, ok 0
PRIMER_RIGHT_EXPLAIN=considered 23, low tm 6, ok 17
PRIMER_PAIR_EXPLAIN=considered 0, ok 0

Actual Behavior

PRIMER_LEFT_EXPLAIN=considered 23, low tm 6, high any compl 16, ok 1
PRIMER_RIGHT_EXPLAIN=considered 23, low tm 6, ok 17
PRIMER_PAIR_EXPLAIN=considered 23, unacceptable product size 23, ok 0

Note:

  • LEFT shows "ok 1" when all 17 non-low-tm primers have self_any scores of 18.0-20.0 (well above the 8.0 threshold)
  • PAIR shows "considered 23" even though no valid left primers exist (the "ok 1" is a ghost primer that was never fully evaluated)

Root Cause

The issue is caused by lazy evaluation in primer3_core when self-complementarity weights are 0:

In libprimer3.cc lines 3550-3587:

if ((must_use 
    || pa->file_flag              // P3_FILE_FLAG check here
    || retval->output_type == primer_list
    || po_args->weights.compl_any // weight = 0 by default
    || po_args->weights.compl_end) // weight = 0 by default
    && pa->thermodynamic_oligo_alignment==0) {
    
    // Calculate self-complementarity
    oligo_compl(h, po_args, stats, dpal_arg_to_use, ...);
    
} else {
    // LAZY EVALUATION BUG: Skip calculation entirely
    h->self_any = h->self_end = ALIGN_SCORE_UNDEF;  // line 3584
}

When P3_FILE_FLAG=0 (default):

  • Condition evaluates to FALSE (all weights are 0, file_flag is 0)
  • Takes else branch: self_any = ALIGN_SCORE_UNDEF (never calculated)
  • Explain counters not updated: ok stays at 1, compl_any not incremented

When P3_FILE_FLAG=1:

  • Condition evaluates to TRUE (pa->file_flag is true)
  • Calls oligo_compl() which calculates self_any
  • In oligo_compl() at line 5318: ostats->ok-- properly decrements ok count
  • Result: Correct "ok 0" in explain output

Demonstration

1. Melting Temperature Analysis for ALL 23 Primers:

# Using oligotm to calculate Tm for each primer position:
oligotm GATCGATCGATCGATCGATC  # Position 0:  Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 1:  Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 2:  Output: 59.312195 (OK)
oligotm CGATCGATCGATCGATCGAT  # Position 3:  Output: 57.363116 (OK)
oligotm GATCGATCGATCGATCGATC  # Position 4:  Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 5:  Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 6:  Output: 59.312195 (OK)
oligotm CGATCGATCGATCGATCGAT  # Position 7:  Output: 57.363116 (OK)
oligotm GATCGATCGATCGATCGATC  # Position 8:  Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 9:  Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 10: Output: 59.312195 (OK)
oligotm CGATCGATCGATCGATCGAT  # Position 11: Output: 57.363116 (OK)
oligotm GATCGATCGATCGATCGATC  # Position 12: Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 13: Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 14: Output: 59.312195 (OK)
oligotm CGATCGATCGATCGATCGAT  # Position 15: Output: 57.363116 (OK)
oligotm GATCGATCGATCGATCGATC  # Position 16: Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 17: Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 18: Output: 59.312195 (OK)
oligotm CGATCGATCGATCGATCGAT  # Position 19: Output: 57.363116 (OK)
oligotm GATCGATCGATCGATCGATC  # Position 20: Output: 56.580423 (REJECTED < 57°C)
oligotm ATCGATCGATCGATCGATCG  # Position 21: Output: 57.363116 (OK)
oligotm TCGATCGATCGATCGATCGA  # Position 22: Output: 59.312195 (OK)

# Summary: 6 primers rejected for low Tm, 17 primers have acceptable Tm

2. Remaining 17 all have High Self-Complementarity with primer3_core:

# For loop to test all 17 primers with acceptable Tm
SEQUENCE="GATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC"
OK_TM_POSITIONS=(1 2 3 5 6 7 9 10 11 13 14 15 17 18 19 21 22)

for pos in "${OK_TM_POSITIONS[@]}"; do
    primer=${SEQUENCE:$pos:20}
    echo "Position $pos: $primer"
    echo "SEQUENCE_TEMPLATE=$SEQUENCE
PRIMER_TASK=check_primers
PRIMER_LEFT_INPUT=$primer
PRIMER_THERMODYNAMIC_OLIGO_ALIGNMENT=0
PRIMER_MAX_SELF_ANY=8.0
=" | primer3_core | grep -E "(PRIMER_LEFT_NUM_RETURNED|EXPLAIN)"
    echo
done

# Output for all 17 primers:
# PRIMER_LEFT_NUM_RETURNED=0

Workaround

Force complete evaluation by adding P3_FILE_FLAG=1

# With workaround flags - shows CORRECT output
echo 'SEQUENCE_ID=gatc_bug_workaround
SEQUENCE_TEMPLATE=GATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC
PRIMER_TASK=generic
PRIMER_MIN_SIZE=20
PRIMER_MAX_SIZE=20
PRIMER_EXPLAIN_FLAG=1
PRIMER_PRODUCT_SIZE_RANGE=30-52
P3_FILE_FLAG=1
=' | primer3_core

# Output with workaround:
# PRIMER_LEFT_EXPLAIN=considered 23, low tm 6, high any compl 17, ok 0
# This is the CORRECT output - all 17 primers rejected for high self_any
# PRIMER_PAIR_EXPLAIN=considered 0, ok 0
# No ghost primer pairs considered

P3_FILE_FLAG=1 forces complete evaluation by making the condition at line 3550 in libprimer3.cc evaluate to TRUE:

if ((must_use || pa->file_flag || ...))  // pa->file_flag is now TRUE
    oligo_compl(h, ...);  // Self-complementarity IS calculated

Without P3_FILE_FLAG (when it's 0), the condition is FALSE and the code skips to:

else
    h->self_any = ALIGN_SCORE_UNDEF;  // Never calculated

This bypasses the lazy evaluation optimization that causes incorrect EXPLAIN statistics.
Note: P3_FILE_FLAG=1 requires SEQUENCE_ID to be set.

Possible fixes

Option 1: Force evaluation when EXPLAIN_FLAG is set

if ((must_use 
    || pa->file_flag
    || pa->explain_flag  // Add this condition
    || retval->output_type == primer_list
    || po_args->weights.compl_any
    || po_args->weights.compl_end)
    && pa->thermodynamic_oligo_alignment==0) {

Option 2: Update statistics during lazy evaluation

} else {
    h->self_any = h->self_end = ALIGN_SCORE_UNDEF;
    // Add: Check if primer would fail and update stats
    if (pa->explain_flag) {
        // Calculate self_any just for statistics
        double temp_self_any = align(oligo_seq, revc_oligo_seq, ...);
        if (temp_self_any > po_args->max_self_any) {
            ostats->compl_any++;
            ostats->ok--;
        }
    }
}

System Information

  • primer3 Version: 2.6.1
  • Operating System: Linux
  • Architecture: x86_64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions