Self-modifying algorithms

I think that you are slightly varying the “rules” of the cellular automata as you are modifying the same generation. Changes should not affect the current generation at hand. I mean when you are ticking and next time looking left it should be the older generation and not the change you made last time … that’s why I was using the new array for the next generation.

Can you explain what you mean? I think the result of the mutation in the loop is exactly the same as if it would be done inside a .each loop, isn’t it? The generation is only modified with the assigned ruleset and from step to step there is no modification of the generation sequence. The state is preserved. Hmm… ?

I mean, its different result when you start messing with the current generation. Like creating a Hulk using radioactive gamma bomb vs. modifying dna and giving birth to cute baby hulk :joy:

Here’s an example that does the both:

rule = {
  [1,1,1] => 0, [1,1,0] => 1,
  [1,0,1] => 1, [1,0,0] => 0,
  [0,1,1] => 1, [0,1,0] => 1,
  [0,0,1] => 1, [0,0,0] => 0
}

define :mutate_same_gen do |gen|
  gen.each_with_index do |s,i|
    left = i > 0 ? i - 1 : gen.length - 1;
    right = i < gen.length - 1 ? i + 1 : 0;
    pattern = [gen[left], s, gen[right]]
    gen[i] = rule[pattern]
  end
  return gen;
end

define :mutate_next_gen do |gen|
  next_gen = []
  gen.each_with_index do |s,i|
    left = i > 0 ? i - 1 : gen.length - 1;
    right = i < gen.length - 1 ? i + 1 : 0;
    pattern = [gen[left], s, gen[right]]
    next_gen[i] = rule[pattern]
  end
  return next_gen;
end

gen = [0, 0, 1, 0, 0, 0]

print mutate_same_gen gen
print mutate_next_gen gen

Thanks, got it. I fixed it in the post above. The gen1 array now contains also the last unmodified left element in order to apply the rule correctly.

On a different tack, if you think of a Markov chain (or a Finite State Machine) as a series of allowed transitions, you could make one with some musical heuristics, such as “the IV chord can resolve to the I chord or go to the vi chord”, “the vi chord can go to the IV chord or to the ii chord, and change the measure length to 2 beats”, etc, so have a random pattern that makes some kind of harmonic and rhythmic sense.

Exactly. See Harmonic random walk in major.

1 Like

I think there was still a bug as the last right is also the first one on the left :slight_smile:

Here’s a new version of the code that does the mutation in more lazy fashion. I removed the extra mutation live_loops as i understand calling the mutate after the each loop does exactly the same thing. Added also some helper functions to make the code more readable.

I felt like all these changes were making the changes too slow for me, so i added some values to the initial state to kickstart it. Also used only the Rule 150 this time as I feel like when combining the different rules the melody seems bit out of sync. Also noticed that if you use Rule 150 with only 7 values it quickly turns to all zeros, so added a few extra notes.

use_debug false

generations = {
  :gen1=>{:cells=>[0, 0, 1, 0, 0, 0, 0, 1, 1]},
  :gen2=>{:cells=>[1, 0, 0, 0, 1, 0, 0, 0, 0]}
}

# Rule 110 https://en.wikipedia.org/wiki/Rule_110
rule_110 = {
  "111" => 0, "110" => 1,
  "101" => 1, "100" => 0,
  "011" => 1, "010" => 1,
  "001" => 1, "000" => 0
}

rule_150 = {
  "111" => 1, "110" => 0,
  "101" => 0, "100" => 1,
  "011" => 0, "010" => 1,
  "001" => 1, "000" => 0
}

define :cells do |gen|
  generations[gen][:cells]
end

define :store_state do |gen|
  generations[gen][:last] = generations[gen][:cells].dup # dup to create new object
end

define :mutate do |rule, gen, i|
  store_state gen if i == 0 # Store state in first cycle
  cells = generations[gen][:cells]
  last_gen = generations[gen][:last]
  left = i > 0 ? i - 1 : cells.length - 1;
  right = i < cells.length - 1 ? i + 1 : 0;
  pattern = "#{last_gen[left]}#{last_gen[i]}#{last_gen[right]}";
  cells[i] = rule[pattern]
end

use_synth :pluck
use_synth_defaults release: 1.5, coef: 0.4

with_fx :reverb, mix: 0.4 do
  with_fx :flanger, wave: 3, depth: 7, decay: 1.5 do
    live_loop :organism_1 do
      print cells(:gen1)
      cells(:gen1).each_with_index do |s,i|
        play (degree i+1, :d, :dorian), pan: rrand(-0.5, 0.5) if s==1
        sleep 0.25
      end
      mutate(rule_150, :gen1, tick%cells(:gen1).length)
    end
    
    live_loop :organism_2 do
      print cells(:gen2)
      cells(:gen2).each_with_index do |s,i|
        play (degree i+1, :d3, :dorian), pan: rrand(-0.5, 0.5) if s==1
        sleep 0.25
      end
      mutate(rule_150, :gen2, tick%cells(:gen2).length)
    end
  end
end

Thanks for fixing it! Yes, storing the complete state is a good idea. Then you also do not depend on the direction of parsing (left or right).

Actually, I do not mind if the piece develops slowly. We should take the time to let it slowly evolve :innocent:

EDIT: your gen sequence has got length 9. It works, but note 0 will be overweighted (?) Maybe that’s why the combination of rule 110/150 didn’t sound good to you? I like org1=150 and org2=110 with starting sequence

generations = {
  :gen1=>{:cells=>[0, 0, 1, 0, 0, 0, 0, 0]},
  :gen2=>{:cells=>[1, 0, 0, 0, 0, 0, 0, 0]}
}

With this state and 8 notes gen2 will be all zeros in around 1 minute. Not sure what is actually going on, but it seems that with 9 notes it runs longer, might turn to all zeros later on also.

I would not listen even myself too much about what sounds good and not. After all its just how we individually hear and feel in the moment. Im sure that some combinations of those rules would sound great together … and maybe combine note lengths from different rules as well.

Here is a convenience function to create the ruleset for any number 0 ... 255:

define :create_ruleset do |n|
  rules = Hash.new
  return rules if n > 255
  binary = "%08b" % n
  8.times do |i|
    key = "%03b" % i
    rules[key] = binary[7-i].to_i
  end
  return rules
end

Usage:

rule_110 = create_ruleset(110)

Note: In the above examples there is a bug in the rule 150. It should read "111" => 1

1 Like

Another easy and neverending source for self-modifying or self-similar melodies are different kinds of string rewrite systems, such as lindenmayer system.

The idea is similar to cellular automata in some ways. Define set of rules and iterate them over same string over and over again. For the rules you can invent your own “vocabulary” and the meaning of things. Here is a simple example where the rules and results are interpreted as degrees and applied using gsub. You could also add more meaning and use some characters to represent rests or certain samples or anything you like:

rules = {
  "1"=>"1 3",
  "3"=>"2 4",
  "4"=> "6",
  "6"=>"1"
}

melody = "1 2"

live_loop :lindenmayer do
  melody = melody.gsub(Regexp.union(rules.keys), rules)
  print melody
  melody.split(" ").each do |d|
    play degree d, :e, :mixolydian
    sleep 0.25
  end
end
3 Likes

To make the rewrite system more powerful, you can also add support for regular expressions and use function calls (or lambdas) as values:


rules = {
  "1"=>"1 3",
  "2"=>"2 4",
  /[3-7]/=>->{rrand_i(1, 7).to_s},
}

def rewrite(ax, rules)
  ax.gsub(Regexp.union(rules.keys)) do |m|
    v = rules.to_a.detect{|k,v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
    v.respond_to?(:call) ? v.call : v # Hack for lambda calls
  end
end

melody = "1 2"

live_loop :lindenmayer do
  melody = rewrite melody, rules
  print melody
  melody.split(" ").each do |d|
    play degree d, :e, :mixolydian
    sleep 0.25
  end
end

EDIT: Changed function calls to lambdas as function calls in hashes are evaluated in init.

With regular expressions and functions you could start doing different variations such as stochastic or context sensitive grammars.

2 Likes

Trying to apply the same trick again and ticking along the melody string instead of rewriting the whole string at once. The slice parameter can be set to e.g. 2 or 3 in order to modify by a slicing window instead of just single characters. Not sure whether this is already optimal, but it sounds quite ok.

use_debug false
rules = {
  /(0 )+/ => ->{[0, 0, 1].choose.to_s + " "},
  "1 " => "2 ",
  "2 " => "4 5 6 ",
  /([4-7] )/ => "8 ",
  "8 " => "9 ",
  /(9 )+/ => ->{["0", "x", "x"].choose + " "},
  /(x )+/ => "0 ",
}

melody = "0 "

def rewrite(ax, rules)
  ax.gsub(Regexp.union(rules.keys)) do |m|
    v = rules.to_a.detect{|k, v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
    v.respond_to?(:call) ? v.call : v # Hack for lambda calls
  end
end

define :modify do |mel, slice, i|
  mel = mel.split(" ")
  l = mel.length
  i = i%l
  
  left = i;
  right = [i + slice - 1, l - 1].min;
  ax = ""
  mel[left..right].each do |m|
    ax += (m + " ")
  end
  melc = rewrite(ax, rules)
  melc = melc.split(" ")
  #puts melc
  
  if right+1 < l
    res = left > 0 ? mel[0..left-1] : []
    res += melc
    res += mel[right+1..-1]
  else
    res = left > 0 ? mel[0..left-1] : []
    res += melc
  end
  rres = ""
  res.each do |m|
    rres += (m + " ")
  end
  return rres
end

use_synth :pluck
use_synth_defaults release: 2.5, coef: 0.3
sca = scale :D3, :melodic_minor_asc, num_octaves: 3

with_fx :reverb, mix: 0.4 do
  with_fx :flanger, wave: 3, depth: 7, decay: 2 do
    live_loop :lindenmayer do
      melody.split(" ").each do |s|
        play sca[s.to_i] unless s == "x"
        sleep 0.25
      end
      melody = modify(melody, 2, 2*tick)
      puts melody
    end
  end
end

EDIT: now everything is in one place: regex, slicing, blanks for numbers > 9

1 Like

Thanks! This makes experimenting so much easier

1 Like

I think you could use sub for changing just the first occurence.

Blanks can be pain but those are just characters that you can take into account :slight_smile: … But no need for spaces or other separators until you start to do something more complex, for example rules like “1”=>“135” that could harmonize the melody and make up some chords.

1 Like

I started with sub but then introduced the slice, which requires to replace more than one, if the slice is large. Anyhow, the idea was to have a somewhat smoother development of the music but still rich in its total variation. So, having a larger string but modifying only a part of it seems a good aproach.

Ah yes, if you want to change the slice size. Nice idea.

With lindenmayer system you can of course generate larger melodies by applying the string replacement multiple times and then play only last generation, for example only the 10th generation like this:

rules = {
  "1"=>"1 3",
  "2"=>"2 4",
  /[3-7]/=>->{rrand_i(1, 7).to_s},
}

def rewrite(ax, rules)
  ax.gsub(Regexp.union(rules.keys)) do |m|
    v = rules.to_a.detect{|k,v| Regexp.union(k) =~ m}[1] # Hack for regexp keys
    v.respond_to?(:call) ? v.call : v # Hack for lambda calls
  end
end

melody = "1 2"

10.times do
  melody = rewrite melody, rules
end

with_synth :kalimba do
  play_pattern_timed (melody.split(" ").map {|d| degree(d, :d, :major) }), [0.5,1.0]
end

I tried this running on three different machines, spaced around the room, , with start times synced using OSC signals to be 2 seconds apart. Sounded really ethereal and great.

2 Likes

Yeah, that’s a very sensitive tool. I was just observing the strings in the output and whenever you feel like some note should be played more or less often that’s easy to do with the rules. Like this:

rules = {
  /(0 )+/ => ->{["0 ", "0 ", "0 ", "1 "].choose},
  "1 " => "4 5 6 ",
  /([4-7] )/ => "8 ",
  /(8 )+/  => ->{["4 ", "9 "].choose},
  /(9 )+/ => ->{["0 ", "x ", "x "].choose},
  /(x )+/ => "0 ",
}.freeze

melody = "0 1"

I kicked out the 2 and instead have a drop from 8 to 4 in addition. Amazing.

Not yet tried that one, but sounds great!