Get the consensus of an alignment, as a string.

    Emit gap characters for majority-gap columns; apply various strategies to
    choose the consensus amino acid type for the remaining columns.

    Parameters
    ----------

    simple : bool
        If True, use simple plurality to determine the consensus amino acid
        type, without weighting sequences for similarity. Otherwise, weight
        sequences for similarity and use relative entropy to choose the
        consensus amino acid type.
    weights : dict or None
        Sequence weights. If given, used to calculate amino acid frequencies;
        otherwise calculated within this function (i.e. this is a way to speed
        up the function if sequence weights have already been calculated).
        Ignored in 'simple' mode.
    trim_ends : bool
        If False, stretch the consensus sequence to include the N- and C-tails
        of the alignment, even if those flanking columns are mostly gap
        characters. This avoids terminal gaps in the consensus (needed for
        MAPGAPS).
    gap_threshold : float
        If the proportion of gap characters in a column is greater than or equal
        to this value (after sequence weighting, if applicable), then the
        consensus character emitted will be a gap instead of an amino acid type.

def consensus(aln, weights=None, gap_threshold=0.5, simple=False, trim_ends=True):
    """Get the consensus of an alignment, as a string.

    Emit gap characters for majority-gap columns; apply various strategies to
    choose the consensus amino acid type for the remaining columns.

    Parameters
    ----------

    simple : bool
        If True, use simple plurality to determine the consensus amino acid
        type, without weighting sequences for similarity. Otherwise, weight
        sequences for similarity and use relative entropy to choose the
        consensus amino acid type.
    weights : dict or None
        Sequence weights. If given, used to calculate amino acid frequencies;
        otherwise calculated within this function (i.e. this is a way to speed
        up the function if sequence weights have already been calculated).
        Ignored in 'simple' mode.
    trim_ends : bool
        If False, stretch the consensus sequence to include the N- and C-tails
        of the alignment, even if those flanking columns are mostly gap
        characters. This avoids terminal gaps in the consensus (needed for
        MAPGAPS).
    gap_threshold : float
        If the proportion of gap characters in a column is greater than or equal
        to this value (after sequence weighting, if applicable), then the
        consensus character emitted will be a gap instead of an amino acid type.

    """
    # Choose your algorithms!
    if simple:
        # Use the simple, unweighted algorithm
        col_consensus = make_simple_col_consensus(alnutils.aa_frequencies(aln))
        def is_majority_gap(col):
            return (float(col.count('-')) / len(col) >= gap_threshold)
        # ENH (alternatively/additionally): does any aa occur more than once?
        # ENH: choose gap-decisionmaking separately from col_consensus
    else:
        # Use the entropy-based, weighted algorithm
        if weights is None:
            seq_weights = alnutils.sequence_weights(aln, 'avg1')
        else:
            seq_weights = weights
        aa_frequencies = alnutils.aa_frequencies(aln, weights=seq_weights)
        col_consensus = make_entropy_col_consensus(aa_frequencies)
        def is_majority_gap(col):
            gap_count = 0.0
            for wt, char in zip(seq_weights, col):
                if char == '-':
                    gap_count += wt
            return (gap_count / sum(seq_weights) >= gap_threshold)

    # Traverse the alignment, handling gaps etc.
    def col_wise_consensus(columns):
        """Calculate the consensus chars for an iterable of columns."""
        if not trim_ends:
            # Track if we're in the N-term or C-term end of the sequence
            in_left_end = True
            maybe_right_tail = []
        # prev_col = None
        # prev_char = None
        for col in columns:
            # Lowercase cols mean explicitly, "don't include in consensus"
            if all(c.islower() for c in col if c not in '.-'):
                yield '-'
                continue
            if any(c.islower() for c in col):
                logging.warn('Mixed lowercase and uppercase letters in a '
                        'column: ' + ''.join(col))
                col = map(str.upper, col)

            # Gap chars
            is_gap = is_majority_gap(col)
            if not trim_ends:
                # Avoid N-terminal gaps in the consensus sequence
                if in_left_end:
                    if not is_gap:
                        # Match -- we're no longer in the left end
                        in_left_end = False
                    is_gap = False

            # When to yield a gap here:
            #   -----------     ---------   ------  ----------
            #   in_left_end     trim_ends   is_gap  yield gap?
            #   -----------     ---------   ------  ----------
            #   True            True        (True)  yes
            #   True            False       (False) (no -- def. char)
            #   False           True        T/F     yes, if is_gap
            #   False           False       (T/F)   NO! use maybe_right_tail
            #   -----------     ---------   ------  ----------

            if is_gap and trim_ends:
                yield '-'
                continue

            # Get the consensus character, using the chosen algorithm
            cons_char = col_consensus(col)

            if trim_ends:
                yield cons_char
            else:
                # Avoid C-terminal gaps in the consensus sequence
                if is_gap:
                    maybe_right_tail.append(cons_char)
                else:
                    # Match -> gaps weren't the right tail; emit all gaps
                    for char in maybe_right_tail:
                        yield '-'
                    maybe_right_tail = []
                    yield cons_char

            # prev_col = col
            # prev_char = cons_char

        # Finally, if we were keeping a right (C-term) tail, emit it
        if not trim_ends:
            for char in maybe_right_tail:
                yield char

    return ''.join(col_wise_consensus(zip(*aln)))