Nicholas H.Tollervey
ntoll.org
/
ntoll@ntoll.org
/
@ntoll
Two contrasting yet related melodies that fit together.
Pick a part, play the counterpoint and concentrate on its shape while listening to the piece as a whole
.
Listen again in the same way but, this time, follow the other part and concentrate on how often the notes sound.
Contrasting yet related.
As demonstrated by J.S.Bach (contrapuntal genius)
Original | Inversion | Retrograde | Retrograde inversion |
As demonstrated by J.S.Bach (contrapuntal genius)
Voicing | Augmentation | Diminution |
(1660-1741)
Steps to Parnassus (dwelling place of the gods).
(Fixed song
based on medieval plainchant)
Governing valid intervals (distance) between notes.
Similar, Parallel, Contrary & Oblique motion.
Furthermore, any solution should be:
Acceptable(fooling most people most of the time).
(In other words, on a par with a human derived solution.)
Findacceptable solutions relatively quickly.
explorethe universe of all possible solutions.
Hey presto! Foox is born (geddit?)
def genetic_algorithm(population, fitness, generate, halt):
"""
A generator that yields a list of genomes ordered by fitness (descending).
Each yielded list represents a generation in the execution of the genetic
algorithm.
@param population: the starting population of genomes.
@param fitness: a function which given a genome will return a fitness
score.
@param generate: a function that produces offspring genomes for the next
generation.
@param halt: a function to test if the genetic algorithm should stop.
"""
current_population = sorted(population, key=fitness, reverse=True)
generation_count = 1
yield current_population
while not halt(current_population, generation_count):
generation_count += 1
new_generation = generate(current_population)
current_population = sorted(new_generation, key=fitness, reverse=True)
yield current_population
Wordolution evolves a solution from a starting population of words containing random letters:
$ wordolution -w cat
['pot', 'int', 'qag', 'ovw', 'wmb', 'jzs', 'gnp', 'oyw', 'gze', 'mss']
pot 1
['hot', 'qag', 'bnt', 'qot', 'ovt', 'nag', 'owt', 'pdt', 'bnt', 'hot']
hot 1
['qat', 'hot', 'qag', 'bnt', 'qot', 'ovt', 'qrt', 'bot', 'zqg', 'hoy']
qat 2
['qat', 'qat', 'zat', 'nat', 'qat', 'qat', 'jaj', 'vxc', 'vxc', 'vxc']
qat 2
['qat', 'nat', 'zat', 'eat', 'lat', 'lat', 'nat', 'laj', 'iab', 'wut']
qat 2
['lat', 'zat', 'gat', 'zat', 'zat', 'zat', 'zat', 'yat', 'gct', 'zht']
lat 2
['cat', 'lat', 'zat', 'gat', 'zat', 'zat', 'gat', 'maw', 'lft', 'zac']
cat 3
(A truncated evolution of cat
)
The fitness of a candidate solution is based upon its Levenshtein distance (number of different characters) from the target word.
def make_fitness_function(word):
"""
Given the target word, will return a function that takes a single Genome
instance and returns a fitness score (based upon its Levenshtein distance).
"""
word_len = len(word)
def fitness_function(genome):
"""
Returns the fitness score of the genome.
"""
if genome.fitness:
return genome.fitness
genome.fitness = abs(word_len -
levenshtein(word, ''.join(genome.chromosome)))
return genome.fitness
return fitness_function
Candidate solutions are instances of Genome (base class)
class Genome(object):
"""
Genomes represent candidate solutions.
"""
def __init__(self, chromosome):
"""
The chromosome is a list of genes that encode specific characteristics
of the candidate solution (genome). The different settings that a gene
may possess are called alleles, and their location in the chromosome is
called the locus. The state of the alleles in a particular chromosome
is called the genotype. It is the genotype that provides information
about the state of the actual candidate solution. The candidate
solution itself is called a genome (hence the name of this class).
"""
self.chromosome = chromosome # e.g. ['c', 'a', 't']
self.fitness = None # Denotes unknown. Set by the fitness function.
def breed(self, other):
"""
Returns two new offspring bred from this and another instance. Uses the
crossover function.
"""
return crossover(self, other, self.__class__)
def mutate(self, mutation_range, mutation_rate, context):
"""
To be overridden as per requirements in the child classes.
"""
return NotImplemented
def __eq__(self, other):
... etc ...
The crossover function is synonymous with breeding new
candidate solutions. The genetic
information from both parents is mixed together to create
two new children
.
def crossover(mum, dad, klass):
"""
Given two parent genomes and a Genome class, randomly selects a midpoint
and then swaps the ends of each genome's chromosome to create two new
genomes that are returned in a tuple.
"""
# Check if the parents are the same,
if mum == dad:
# and do nothing if they are.
return (mum, dad)
crossover_point = random.randint(0, len(mum))
baby1 = klass(mum[:crossover_point] + dad[crossover_point:])
baby2 = klass(dad[:crossover_point] + mum[crossover_point:])
return (baby1, baby2)
So cat
and dog
could produce cog
and dat
.
Wordolution's Genome sub-class
class Genome(ga.Genome):
"""
A class to represent a candidate solution for a target word.
"""
def mutate(self, mutation_rate, mutation_range, context):
"""
Mutates the genotypes. Only the mutation_rate is used in this simple
example.
"""
for locus in range(len(self.chromosome)):
if mutation_rate >= random.random():
# Verbose, but correctly named for clarity.
new_allele = random.choice(LETTERS)
self.chromosome[locus] = new_allele
self.fitness = None # force recalculation of fitness
The mutate
method has a
mutation_rate
chance of changing a letter at
random.
The generate
function makes new generations.
def make_generate_function(mutation_range, mutation_rate, word):
"""
Given a target word, mutation range and mutation rate will use return a
function that takes a seed generation and returns a new generation.
"""
def generate(seed_generation):
"""
Given a seed generation will return a new generation of candidates.
"""
length = len(seed_generation)
# Keep the fittest 50%
new_generation = seed_generation[:length/2]
# Breed the remaining 50% using roulette wheel selection
offspring = []
while len(offspring) < length/2:
mum = ga.roulette_wheel_selection(seed_generation)
dad = ga.roulette_wheel_selection(seed_generation)
children = mum.breed(dad)
offspring.extend(children)
# Mutate
for genome in offspring:
genome.mutate(mutation_range, mutation_rate, word)
# Ensure the new generation is the right length
new_generation.extend(offspring)
new_generation = new_generation[:length]
return new_generation
return generate
Casino like selection that favours the fittest.
def roulette_wheel_selection(population):
"""
A random number between 0 and the total fitness score of all the genomes in
a population is chosen (a point within a slice of a roulette wheel). The
code iterates through the genomes adding up the fitness scores. When the
subtotal is greater than the randomly chosen point it returns the genome
at that point "on the wheel".
See: https://en.wikipedia.org/wiki/Fitness_proportionate_selection
"""
total_fitness = 0.0
for genome in population:
total_fitness += genome.fitness
# Ensures random selection if no solutions are "fit".
if total_fitness == 0.0:
return random.choice(population)
random_point = random.uniform(0.0, total_fitness)
fitness_tally = 0.0
for genome in population:
fitness_tally += genome.fitness
if fitness_tally > random_point:
return genome
Choose random point between zero and the sum of the fitness scores.
The halt
function checks if an acceptable solution has evolved (or gives up after n generations).
def halt(population, generation_count):
"""
Given a population of candidate solutions and generation count (the number
of epochs the algorithm has run) will return a boolean to indicate if an
acceptable solution has been found within the referenced population.
"""
word_length = len(population[0].chromosome)
return population[0].fitness == word_length or generation_count > 100
[5, 7, 6, 5, 8, 7, 9, 8, 7, 6, 5]
Specific rules of a species are encoded as various tests and punished or rewarded accordingly:
# Make sure the solution starts correctly (at a 5th or octave).
first_interval = contrapunctus[0] - cantus_firmus[0]
if first_interval == 7 or first_interval == 4:
fitness_score += REWARD_FIRST
else:
fitness_score -= PUNISH_FIRST
# Make sure the solution finishes correctly (at an octave).
if contrapunctus[-1] - cantus_firmus[-1] == 7:
fitness_score += REWARD_LAST
else:
fitness_score -= PUNISH_LAST
(An example from first species counterpoint.)
# Reward / punishment to ensure the solution starts correctly (5th or 8ve).
REWARD_FIRST = 1
PUNISH_FIRST = 0.1
# Reward / punishment to ensure the solution finishes correctly (at an 8ve).
REWARD_LAST = 1
PUNISH_LAST = 0.1
# Reward / punishment to ensure the penultimate note is step wise onto the
# final note.
REWARD_LAST_STEP = 1
PUNISH_LAST_STEP = 0.7
# Reward / punish contrary motion onto the final note.
REWARD_LAST_MOTION = 1
PUNISH_LAST_MOTION = 0.1
# Punishment if the penultimate note is a repeated note.
PUNISH_REPEATED_PENULTIMATE = 0.1
# Make sure the movement to the penultimate note isn't from too
# far away (not greater than a third).
REWARD_PENULTIMATE_PREPARATION = 1
PUNISH_PENULTIMATE_PREPARATION = 0.7
# Punish parallel fifths or octaves.
PUNISH_PARALLEL_FIFTHS_OCTAVES = 0.5
# Punishment for too many repeated notes.
PUNISH_REPEATS = 0.1
# Punishment for too many parallel thirds
PUNISH_THIRDS = 0.1
# Punishment for too many parallel sixths.
PUNISH_SIXTHS = 0.1
# Punishment for too many parallel/similar movements.
PUNISH_PARALLEL = 0.1
# Punishment for too many large leaps in the melody.
PUNISH_LEAPS = 0.1
$ foox -s 1 -cf 5 7 6 5 8 7 9 8 7 6 5 -o first
... evolution happens ...
$ lilypond first.ly
... lilypond magic ...
$ evince first.pdf
$ timidity first.midi
$ foox --help
usage: foox [-h] [--version] [-v] -s SPECIES -cf
[CANTUS_FIRMUS [CANTUS_FIRMUS ...]] [-o OUT]
Evolves valid solutions to species counterpoint problems.
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
-v, --verbose increased amount of verbosity
-s SPECIES, --species SPECIES
indicated species to use (1-5)
-cf [CANTUS_FIRMUS [CANTUS_FIRMUS ...]], --cantus-firmus [CANTUS_FIRMUS [CANTUS_FIRMUS ...]]
specify the cantus firmus
-o OUT, --out OUT name the output file
FUX | PYTHON |
MOZART | PYTHON |
PYTHON | FUX |
FUX | PYTHON |
Fifth species combines all of the previous species as well as extra rules concerning ornamentation of the melody.
Foox is a fun experiment with no serious use case.
However, I'd like to:
Licensed under CC BY 2.0
Sourced from Wikipedia for educational (fair) use.
Created using third party services. ;-)