Source code for genome_entropy.entropy.shannon

"""Shannon entropy calculation for sequences."""

import math
from collections import Counter
from dataclasses import dataclass
from typing import Dict, Optional, Set

from ..logging_config import get_logger

logger = get_logger(__name__)

DNA_ALPHABET_SIZE = 4
PROTEIN_ALPHABET_SIZE = 20
THREE_DI_ALPHABET_SIZE = 20
TWELVE_STATE_ALPHABET_SIZE = 12


[docs] def normalise_entropy(entropy: float | None, alphabet_size: int) -> float | None: """Normalise a raw Shannon entropy using its theoretical alphabet size. This helper is intended for downstream analysis. Normalised values are derived from raw entropy and are therefore not stored in standard output. Args: entropy: Raw Shannon entropy in bits, or ``None`` for missing data. alphabet_size: The theoretical number of symbols in the representation. Returns: Entropy divided by ``log2(alphabet_size)``, or ``None`` when entropy is ``None``. Raises: ValueError: If ``alphabet_size`` is not greater than one. """ if entropy is None: return None if alphabet_size <= 1: raise ValueError("alphabet_size must be greater than 1") return entropy / math.log2(alphabet_size)
[docs] def normalise_dna_entropy(entropy: float | None) -> float | None: """Normalise raw DNA entropy using the theoretical four-symbol alphabet.""" return normalise_entropy(entropy, DNA_ALPHABET_SIZE)
[docs] def normalise_protein_entropy(entropy: float | None) -> float | None: """Normalise raw protein entropy using the theoretical 20-symbol alphabet.""" return normalise_entropy(entropy, PROTEIN_ALPHABET_SIZE)
[docs] def normalise_three_di_entropy(entropy: float | None) -> float | None: """Normalise raw 3Di entropy using the theoretical 20-symbol alphabet.""" return normalise_entropy(entropy, THREE_DI_ALPHABET_SIZE)
[docs] def normalise_twelve_state_entropy(entropy: float | None) -> float | None: """Normalise raw 12-state entropy using its theoretical alphabet.""" return normalise_entropy(entropy, TWELVE_STATE_ALPHABET_SIZE)
[docs] @dataclass class EntropyReport: """Report containing entropy values at different representation levels. Attributes: dna_entropy_global: Entropy of the entire input DNA sequence orf_nt_entropy: Dictionary mapping ORF IDs to their nucleotide entropy protein_aa_entropy: Dictionary mapping ORF IDs to their amino acid entropy three_di_entropy: Dictionary mapping ORF IDs to their 3Di token entropy alphabet_sizes: Dictionary with alphabet sizes for each representation twelve_state_entropy: Optional mapping of ORF IDs to 12-state entropy """ dna_entropy_global: float orf_nt_entropy: Dict[str, float] protein_aa_entropy: Dict[str, float] three_di_entropy: Dict[str, float] alphabet_sizes: Dict[str, int] twelve_state_entropy: Dict[str, float] | None = None
[docs] def shannon_entropy( sequence: str, alphabet: Optional[Set[str]] = None, normalize: bool = False ) -> float: """Calculate Shannon entropy of a sequence. Shannon entropy: :math:`H = -\\sum_i p_i \\log_2(p_i)`, where :math:`p_i` is the frequency of symbol :math:`i`. Args: sequence: String to calculate entropy for alphabet: Optional set of symbols in the alphabet for normalization normalize: Legacy explicit in-memory normalisation switch. Standard pipeline output never enables it; prefer ``normalise_entropy`` for downstream analysis. Returns: Shannon entropy value (bits) - Returns 0.0 for empty sequences - Returns normalized entropy in [0, 1] if normalize=True Examples: >>> shannon_entropy("AAAA") 0.0 >>> shannon_entropy("ACGT") 2.0 """ if not sequence: return 0.0 # Count symbol frequencies counts = Counter(sequence) total = len(sequence) # Calculate entropy: -sum_i p_i * log2(p_i) entropy = 0.0 for count in counts.values(): if count > 0: p_i = count / total entropy -= p_i * math.log2(p_i) # Normalize if requested if normalize and alphabet: alphabet_size = len(alphabet) if alphabet_size > 1: max_entropy = math.log2(alphabet_size) return entropy / max_entropy if max_entropy > 0 else 0.0 return entropy
[docs] def calculate_sequence_entropy( sequence: str, alphabet: Optional[Set[str]] = None, normalize: bool = False ) -> float: """Calculate entropy for a biological sequence. Convenience wrapper around shannon_entropy that handles common preprocessing (e.g., converting to uppercase). Args: sequence: Biological sequence (DNA, protein, 3Di tokens) alphabet: Optional alphabet for the legacy normalisation switch normalize: Legacy explicit normalisation switch; standard output is raw Returns: Shannon entropy in bits, or a legacy explicitly normalised value """ # Convert to uppercase for consistency sequence = sequence.upper() return shannon_entropy(sequence, alphabet=alphabet, normalize=normalize)
[docs] def calculate_entropies_for_sequences( sequences: Dict[str, str], alphabet: Optional[Set[str]] = None, normalize: bool = False, ) -> Dict[str, float]: """Calculate entropy for multiple sequences. Args: sequences: Dictionary mapping IDs to sequences alphabet: Optional alphabet for normalization normalize: Whether to normalize by alphabet size Returns: Dictionary mapping IDs to entropy values """ logger.debug("Calculating entropy for %d sequence(s)", len(sequences)) entropies = { seq_id: calculate_sequence_entropy(seq, alphabet=alphabet, normalize=normalize) for seq_id, seq in sequences.items() } logger.debug("Calculated entropy for %d sequence(s)", len(entropies)) return entropies