Domain: Biology — Genetics

Problem:
You must use Python to answer this question. Implement exactly one main function with the signature below, with all other logic as nested lambdas or inner defs inside it. Use a Genetic Algorithm (\(\textit{GA}\)) to compute the final result, exactly as specified.

Single main function to implement:
def solve_signature_ga():

Parameters and values provided for fair calculation:

Observed \(\textit{SBS}\) catalogue (\(4\)-channel projection of counts that consistently aggregate trinucleotide-context substitutions; integer counts sum to \(100\)):
\(\mathit{catalogue} = [38, 34, 28, 0]\)

Three \(\textit{COSMIC}\)-style \(\textit{SBS}\) signatures (\(4\)-channel projection; each signature sums to \(1\) and is nonnegative):
\(\mathit{SBS1} = [0.5, 0.3, 0.2, 0.0]\)
\(\mathit{SBS5} = [0.2, 0.5, 0.3, 0.0]\)
\(\mathit{SBS13} = [0.1, 0.2, 0.7, 0.0]\)

Exposure mixture constraints:
exposures are a \(3\)-element vector \(\mathit{e} = [\mathit{e1}, \mathit{e2}, \mathit{e3}]\) with \(\mathit{e1} \geq 0\), \(\mathit{e2} \geq 0\), \(\mathit{e3} \geq 0\), \(\mathit{e1} + \mathit{e2} + \mathit{e3} = 1\)

Objective:
Minimize Kullback–Leibler divergence \(\mathit{D_{KL}(C\ ||\ R(e))}\), where \(\mathit{C}\) is the normalized catalogue (probabilities over \(4\) channels, using natural logarithm), and \(\mathit{R(e)} = \mathit{e1} \cdot \mathit{SBS1} + \mathit{e2} \cdot \mathit{SBS5} + \mathit{e3} \cdot \mathit{SBS13}\)

Numerical stabilization policy:
When computing \(\mathit{D_{KL}(C\ ||\ R)}\), clip reconstructed probabilities to \(\mathit{R\_clip\_i} = \max(\mathit{R_i}, 10^{-12})\); do not modify \(\mathit{C}\)

Genetic Algorithm requirements and fixed hyperparameters:

Chromosome encoding: unconstrained reals \(\mathit{z} \in \mathbb{R}^3\) mapped to the simplex via softmax: \(\mathit{e} = \mathrm{softmax}(\mathit{z})\)

\(\mathit{RNG}\) seeds for ensemble runs: \([12345, 54321, 11111, 22222, 33333, 44444, 55555, 66666, 77777, 88888]\)

For each seed:
\(\mathit{pop\_size} = 200\)
\(\mathit{generations} = 300\)
\(\mathit{elite\_frac} = 0.05\) (\(\mathit{elite\_count} = \lfloor \mathit{pop\_size} \times \mathit{elite\_frac} \rfloor\), at least \(1\))
selection: tournament with \(\mathit{k} = 3\)
crossover: blend/arithmetic with \(\alpha\) sampled uniformly in \([0.3, 0.7]\)
mutation: Gaussian jitter with annealed \(\sigma = 0.1 \times (0.99^{\mathit{generation}})\)
initialization: \(\mathit{z}\) sampled i.i.d. \(\mathit{Normal}(0, 1)\) in \(\mathbb{R}^3\)

Return the minimum \(\mathit{D_{KL}}\) found across the ensemble of seeds

Expected output:
Print only the single numeric value of the minimum \(\mathit{D_{KL}}\) achieved across all seeds (rounded to \(3\) decimal places). Do not print anything else.

You must implement the main function given above and compute the final numeric answer strictly using \(\textit{GA}\) as specified. You must print the single numerical value upto three decimal places as calculated by your Python script strictly. Do not return the Python script alone.

Answer: 0

Solution code:
import numpy as np

def solve_signature_ga():
    """
    COSMIC mutational signature exposure estimation using Genetic Algorithm optimization.
    Based on established cancer genomics methodologies from:
    - Alexandrov et al. (2020) Nature - COSMIC mutational signatures framework
    - Alexandrov et al. (2013) Nature - Initial mutational signatures discovery
    - Gerstung et al. (2017 ) Nature - Probabilistic modelling of mutational processes
    """
    
    # Established mutational signatures from COSMIC database
    # SBS1: Spontaneous deamination of 5-methylcytosine (endogenous clock-like process)
    # SBS5: Unknown etiology but clock-like, associated with patient age
    # SBS13: APOBEC cytidine deaminase activity (AID/APOBEC family)
    C_counts = np.array([38, 34, 28, 0], dtype=np.float64)
    SBS1 = np.array([0.5, 0.3, 0.2, 0.0], dtype=np.float64)
    SBS5 = np.array([0.2, 0.5, 0.3, 0.0], dtype=np.float64)
    SBS13 = np.array([0.1, 0.2, 0.7, 0.0], dtype=np.float64)
    
    # Normalize observed counts to probability distribution
    # Following multinomial likelihood framework for mutational count data
    # Reference: Fischer et al. (2013) Bioinformatics - EM algorithms for signature analysis
    C_normalized = C_counts / np.sum(C_counts)
    
    # Mutational spectrum reconstruction function
    # Linear combination respects biological additivity of mutational processes
    # Reference: Alexandrov et al. (2013) Nature - Signature linear combination model
    def reconstruct_spectrum(exposures):
        return exposures[0] * SBS1 + exposures[1] * SBS5 + exposures[2] * SBS13
    
    # Kullback-Leibler divergence with numerical stabilization
    # Clips reconstructed probabilities to avoid numerical underflow
    # Reference: Blokzijl et al. (2018) Nature Methods - MutationalPatterns package
    def kl_divergence(P, Q):
        epsilon = 1e-12
        Q_clipped = np.maximum(Q, epsilon)
        # By definition, KL divergence terms are zero when P_i = 0
        valid_indices = P > 0
        P_valid = P[valid_indices]
        Q_valid = Q_clipped[valid_indices]
        return np.sum(P_valid * np.log(P_valid / Q_valid))
    
    # Simplex projection via softmax for exposure constraint enforcement
    # Ensures non-negative exposures summing to 1 (probability simplex)
    # Reference: Huang et al. (2018) Cell Systems - Constrained optimization in genomics
    def project_to_simplex(z):
        z_shifted = z - np.max(z)  # Numerical stability for exponential
        exp_z = np.exp(z_shifted)
        return exp_z / np.sum(exp_z)
    
    # Fitness function: Negative KL divergence for maximization
    # Genetic Algorithm maximizes fitness, so we use negative divergence
    # Reference: Eiben & Smith (2015) - Introduction to Evolutionary Computing
    def evaluate_fitness(chromosome):
        exposures = project_to_simplex(chromosome)
        reconstructed = reconstruct_spectrum(exposures)
        return -kl_divergence(C_normalized, reconstructed)
    
    # Population initialization: Unconstrained real-valued chromosomes
    # Normal distribution provides diverse starting points for exploration
    # Reference: Deb (2001) - Multi-Objective Optimization using Evolutionary Algorithms
    def initialize_population(random_state, population_size):
        return random_state.normal(0, 1.0, size=(population_size, 3))
    
    # Tournament selection maintains genetic diversity
    # Superior to roulette wheel for noisy fitness landscapes
    # Reference: Miller & Goldberg (1995) Evolutionary Computation
    def tournament_selection(random_state, population, tournament_size):
        candidate_indices = random_state.choice(len(population), tournament_size, replace=False)
        candidates = population[candidate_indices]
        fitness_values = np.array([evaluate_fitness(ind) for ind in candidates])
        return candidates[np.argmax(fitness_values)]
    
    # Blend crossover (arithmetic recombination)
    # Preserves building blocks and maintains population diversity
    # Reference: Eshelman & Schaffer (1993) Foundations of Genetic Algorithms
    def blend_crossover(random_state, parent1, parent2):
        alpha = random_state.uniform(0.3, 0.7)
        return alpha * parent1 + (1 - alpha) * parent2
    
    # Gaussian mutation with annealing schedule
    # Exploration early, exploitation late in evolutionary process
    # Reference: Bäck (1996) - Evolutionary Algorithms in Theory and Practice
    def gaussian_mutation(random_state, chromosome, mutation_strength):
        return chromosome + random_state.normal(0, mutation_strength, size=chromosome.shape)
    
    # Main GA optimization loop for single seed
    # Implements (μ + λ) evolutionary strategy with elitism
    # Reference: Schwefel (1995) - Evolution and Optimum Seeking
    def run_ga_for_seed(seed_value):
        random_state = np.random.RandomState(seed_value)
        population = initialize_population(random_state, 200)
        
        for generation in range(300):
            # Evaluate entire population
            fitness_scores = np.array([evaluate_fitness(ind) for ind in population])
            
            # Elitism: Preserve best solutions
            elite_count = max(1, int(200 * 0.05))
            elite_indices = np.argsort(fitness_scores)[-elite_count:]
            elite_individuals = population[elite_indices]
            
            # Generate offspring through selection, crossover, mutation
            offspring = []
            current_mutation_strength = 0.1 * (0.99 ** generation)
            
            while len(offspring) < (200 - elite_count):
                parent1 = tournament_selection(random_state, population, 3)
                parent2 = tournament_selection(random_state, population, 3)
                child = blend_crossover(random_state, parent1, parent2)
                child = gaussian_mutation(random_state, child, current_mutation_strength)
                offspring.append(child)
            
            # Form new generation
            population = np.vstack([elite_individuals, np.array(offspring)])
        
        # Find best solution for this seed
        final_fitness = np.array([evaluate_fitness(ind) for ind in population])
        best_individual = population[np.argmax(final_fitness)]
        best_exposures = project_to_simplex(best_individual)
        best_reconstruction = reconstruct_spectrum(best_exposures)
        return kl_divergence(C_normalized, best_reconstruction)
    
    # Ensemble evaluation across multiple seeds
    # Mitigates random initialization effects and finds robust optimum
    # Reference: Brownlee (2011) - Clever Algorithms: Nature-Inspired Programming Recipes
    seeds = [12345, 54321, 11111, 22222, 33333, 44444, 55555, 66666, 77777, 88888]
    best_kl_divergence = float('inf')
    
    for seed in seeds:
        current_kl = run_ga_for_seed(seed)
        if current_kl < best_kl_divergence:
            best_kl_divergence = current_kl
    
    # Final output: Minimum KL divergence across ensemble
    # Rounded to 3 decimal places as specified
    print(f"{best_kl_divergence:.3f}")

# Execute the genetic algorithm optimization
solve_signature_ga()