Σάββατο 29 Ιουνίου 2013

Musical CSPs with Mingus and python-constraint

Hello,

I was looking for a nice python library for constraint programming. Unfortunately, the only one I could find was python-constraint which seemed quite nice and straightforward, but after using it for a while, I find it very limited.

So, as a first example, I tried solving the pretty classic musical CSP described at the Strasheela Examples page as the 'All Interval Series' which is taken from music serialism.

The problem states that we want to put a series of all different pitch classes on the chromatic scale, where each interval appears exactly once. We also take account for inversely equivalent intervals.

I used python-constraint in order to construct my CSP by using pure python syntax, and mingus to create a midi track and render it to .pdf, .png and .mid files.

Here's the code:
from constraint import Problem,\
RecursiveBacktrackingSolver,\
AllDifferentConstraint,\
MinConflictsSolver
import mingus.core.notes as notes
import mingus.extra.LilyPond as LilyPond
from mingus.containers import Bar,Track
from mingus.midi import MidiFileOut
from time import time # Time execution.
# Initialize our CSP problem.
AllIntervalCSP = Problem()
# Each variable is named `N#' where # is a number from 0 to 11
# This is not a requirement, but helps the eye.
NoteList = ['N'+ str(n) for n in range(0,12)] # N0..N11
# Same for the eleven intervals.
IntervalList = ['I'+ str(i) for i in range(1,12)] # I1..I11
# Add the variables to the csp. Each variable has a finite domain
# of integral elements that represent a pitch-class (from 0 to 11).
AllIntervalCSP.addVariables(NoteList, range(0,12)) # 0..11
AllIntervalCSP.addVariables(IntervalList, range(0, 12)) # 0..11
# Restrict every variable to take a different value.
AllIntervalCSP.addConstraint(AllDifferentConstraint(), NoteList) # All pitches different
AllIntervalCSP.addConstraint(AllDifferentConstraint(), IntervalList) # All intervals different
# A constraint restricts the intervals to be pair-wise distinct.
# The modulo is there because the intervals are inversional-equivalent
# (i.e. a 5th upward is an inverse fourth downward).
for i in range(1,12):
AllIntervalCSP.addConstraint(lambda I,N1,N2: I == (N2-N1) % 12 , (IntervalList[i-1],NoteList[i-1],NoteList[i]))
# Constraint first note to be of pitch class 0 (C) and
# last note to be of pitch class L/2
AllIntervalCSP.addConstraint(lambda x: x==0, (NoteList[0],)) # First note is a C
AllIntervalCSP.addConstraint(lambda x: x==6, (NoteList[-1],)) # Last note is a F#/Gb
# Time while searching for a solution...
print "Searching for a solution:"
startTime = time()
AllIntervalSolution = AllIntervalCSP.getSolution()
duration = time() - startTime
if AllIntervalSolution is not None:
print "Solution found in %.2fms." % (duration*1000.0)
# Store the solution on a list of integers.
Sol = [AllIntervalSolution[n] for n in NoteList]
# Create a track using mingus.
track = Track()
for n in Sol:
track.add_notes(notes.int_to_note(n))
# Create a lilypond string from our track.
lilytrack = LilyPond.from_Track(track)
# Store it to .pdf and .png files.
LilyPond.to_pdf(lilytrack, "output")
LilyPond.to_png(lilytrack, "output")
# Output it as a midi file.
MidiFileOut.write_Track('output.mid', track, 120, 0)
else:
# Solution not found. Show time wasted.
print "Solution not found. Exhausted in %ss" % duration
view raw gistfile1.py hosted with ❤ by GitHub
https://gist.github.com/mmxgn/5891884

Here's the output score with lilypond:

and the link to the midi file.