Source code for djalgo.genetic

import copy
import random
from .analysis import Index

[docs] class Darwin: def __init__( self, initial_phrases, mutation_rate=0.05, population_size=50, mutation_probabilities=None, scale=None, measure_length = 4, time_resolution=[0.125, 4], weights = None, targets=None, seed=None ): """ Initialize the Darwin class for evolving musical phrases. Args: initial_phrases (list): List of initial musical phrases, each a list of tuples (pitch, duration, offset). index_weights (dict): Weights for each musical aspect ('gini', 'balance', 'motif', 'dissonance'). mutation_rate (float): Probability of mutation per attribute in a phrase. population_size (int): Number of phrases in each generation. scale (list): List of pitches considered harmonious or in-scale; used for dissonance calculation. """ if mutation_probabilities: self.mutation_probabilities = mutation_probabilities else: self.mutation_probabilities = { 'pitch': lambda: random.choice(self.scale), 'duration': lambda: random.choice(self.possible_durations), 'rest': lambda: None if random.random() < 0.02 else 1 # 2% chance of a rest } self.population = [self.mutate(phrase, rate=0) for phrase in initial_phrases for _ in range(population_size // len(initial_phrases))] self.weights = weights self.mutation_rate = mutation_rate self.population_size = population_size self.scale = scale self.measure_length = measure_length self.time_resolution = time_resolution if self.time_resolution[1] > self.measure_length: print("Warning: Max duration exceeds measure length. Decreasing max duration to measure length.") self.time_resolution[1] = self.measure_length all_durations = [0.125, 0.25, 0.5, 1, 2, 3, 4, 8] self.possible_durations = [d for d in all_durations if self.time_resolution[0] <= d <= self.time_resolution[1]] # Set the weights and targets for each metric are set to default values if not provided if weights is None: self.weights = { 'gini': (1.0, 1.0, 0.0), 'balance': (1.0, 1.0, 0.0), 'motif': (10.0, 1.0, 0.0), 'dissonance': (1.0, 0.0, 0.0), 'rhythmic': (0, 10.0, 0), 'rest': (1.0, 0.0, 0.0) } else: self.weights = weights if targets is None: self.targets = { 'gini': (0.05, 0.5, 0.0), # Example targets; adjust as needed 'balance': (0.1, 0.1, 0.0), 'motif': (1.0, 1.0, 0.0), 'dissonance': (0.0, 0.0, 0.0), 'rhythmic': (0.0, 1.0, 0.0), # Assuming rhythmic applies to duration 'rest': (0.0, 0.0, 0.0) # rests are None pitches } else: self.targets = targets if seed is not None: random.seed(seed) self.best_individuals = [] # List to store the best individual of each generation self.best_scores = []
[docs] def calculate_fitness_components(self, phrase): """ Calculate the fitness components based on the Index class for a given phrase. Args: phrase (list): The musical phrase. Returns: dict: A dictionary containing the fitness components ('gini', 'balance', 'motif', 'dissonance', 'rhythmic'). """ fitness_components = {} # Split the phrase into its components pitches, durations, offsets = zip(*phrase) # Gini coefficient for pitch and duration fitness_components['gini_pitch'] = Index(pitches).gini() fitness_components['gini_duration'] = Index(durations).gini() # Balance for pitch and duration fitness_components['balance_pitch'] = Index(pitches).balance() fitness_components['balance_duration'] = Index(durations).balance() # Motif for pitch and duration (and potentially offsets if applicable) fitness_components['motif_pitch'] = Index(pitches).motif() fitness_components['motif_duration'] = Index(durations).motif() # Add this if you're analyzing motifs in durations too # Dissonance, only applicable to pitches if self.scale is not None: fitness_components['dissonance_pitch'] = Index(pitches).dissonance(self.scale) # Rhythmic fit based on durations # Note: Here, I am assuming you have a method in Index class for rhythm analysis. Adjust if necessary. fitness_components['rhythmic'] = Index(durations).rhythmic(self.measure_length) # Adjust method as needed # gini and balance for offsets? #fitness_components['gini_offset'] = Index(offsets).gini() # Add if relevant #fitness_components['balance_offset'] = Index(offsets).balance() # Add if relevant # Calculate the proportion of rest notes (notes where pitch is None) rest_notes = [note for note in phrase if note[0] is None] total_notes = len(phrase) rest_proportion = len(rest_notes) / total_notes if total_notes > 0 else 0 fitness_components['rest'] = rest_proportion return fitness_components
[docs] def fitness(self, phrase): """ Calculate the fitness of a phrase based on how closely it meets the desired musical metrics targets. Args: phrase (list): Musical phrase. Returns: float: Fitness score of the phrase. """ fitness_score = 0.0 components = self.calculate_fitness_components(phrase) # Iterate through each metric and calculate the contribution to fitness for metric, (pitch_target, duration_target, offset_target) in self.targets.items(): for i, component in enumerate(['pitch', 'duration', 'offset']): actual_value = components.get(f"{metric}_{component}", 0) target_value = (pitch_target, duration_target, offset_target)[i] weight = self.weights[metric][i] # Calculate the similarity from the target (only if there is a target) if target_value is not None and weight > 0: # Ensure there's a target and weight for this component similarity = 1 - abs(actual_value - target_value) / (target_value if target_value != 0 else 1) fitness_score += max(0, similarity) * weight # Apply weight and ensure non-negative score return fitness_score
[docs] def mutate(self, phrase, rate=None, rest_rate=0.02): """ Mutate a musical phrase while respecting musical structures and boundaries. """ if rate is None: rate = self.mutation_rate new_phrase = [] total_offset = 0 # Track total offset for alignment for note in phrase: pitch, duration, offset = note # Mutate pitch, duration, and rest based on mutation probabilities new_pitch = self.mutation_probabilities['pitch']() if random.random() < rate else pitch new_duration = self.mutation_probabilities['duration']() if random.random() < rate else duration new_rest = self.mutation_probabilities['rest']() # Align offsets based on sequential order, ensuring notes/rests align with previous ones new_offset = total_offset total_offset += new_duration # Increment total offset by new duration # Append the new note or rest to the phrase new_phrase.append((new_pitch if new_rest else None, new_duration, new_offset)) return new_phrase
[docs] def select(self, k=25): """ Select top-k phrases based on fitness. Args: k (int): Number of top phrases to select. Returns: list: Top k musical phrases. """ # Evaluate the fitness of each phrase in the population fitness_scores = [(phrase, self.fitness(phrase)) for phrase in self.population] # Sort the phrases by their fitness scores in descending order sorted_phrases = sorted(fitness_scores, key=lambda x: x[1], reverse=True) # Select the top-k phrases selected_phrases = [phrase for phrase, score in sorted_phrases[:k]] return selected_phrases
[docs] def crossover(self, parent1, parent2): """ Combine two phrases to create a new phrase. Args: parent1, parent2 (list): Parent phrases. Returns: list: New musical phrase generated from parents. """ # Cut points for the crossover cut1, cut2 = sorted(random.sample(range(len(parent1)), 2)) # Create new phrase by combining parts of the parent phrases new_phrase = parent1[:cut1] + parent2[cut1:cut2] + parent1[cut2:] return new_phrase
[docs] def evolve(self, k=25, rest_rate=0.02): """ Evolve the population of phrases through selection, crossover, and mutation. Args: k (int): Number of phrases to select for the next generation. """ # Selection based on fitness selected_phrases = self.select(k) # Store the best individual and its fitness score self.best_individuals.append(selected_phrases[0]) self.best_scores.append(self.fitness(selected_phrases[0])) # Crossover and mutation to create a new generation new_population = [] while len(new_population) < self.population_size: parent1, parent2 = random.sample(selected_phrases, 2) parent1 = copy.deepcopy(parent1) parent2 = copy.deepcopy(parent2) child = self.crossover(parent1, parent2) mutated_child = self.mutate(child, rest_rate=rest_rate) new_population.append(mutated_child) self.population = new_population