More Tales of MiP, MaP, AmP and AlP


I realise this thread of posts is probably getting a bit old but I've done a bit more tidying up of my (initially flippant) project around "Hungarian pentatonics" and have some moderately nice initial results.

As I looked over the previous posts I realised pretty much all the "altered note heptatonics" seemed to be covered. That is, the Major, Harmonic Minor and Melodic Minor plus the scales you can get by altering just one note from one of those three -- all these seem to contain at least one of the four pentatonics I call MiP, MaP, AlP and AmP.

A quick reminder:

  • MaP is 1 3 #4 5 7
  • MiP is 1 b3 #4 5 7
  • AlP is 1 3 #4 #5 7
  • AmP is 1 b3 #4 #5 7

I hope the pattern is obvious. The choice of these four pentatonics was mostly just instinctive -- MiP seemed interesting and the others dropped out of experimenting with it on the guitar.

The altered-note heptatonics are the "nearest neighbours" of the common scales. There isn't anything very special about them -- certainly not structurally -- but they seem to be a natural thing to look at when building up 7-note scales. So I was interested to see how many of them contain one or more of these four pentatonics. Pleasingly, it turns out the answer is "all of them".

This stuff is tiresome to do by hand and pretty easy with a computer. I used to do this kind of thing in Java but my environment's no longer working right and rather than fixing it I knocked up a Python script instead -- see the bottom of the post. It's ugly and inefficient and doesn't even quite do what I'd like but it seems to be correct, which is all I care about for now. Here's what I got.

First, the heptatonics that contain only one of the four pentatonics:

  • Harmonic Minor #1 : 1 b2 2 3 b5 5 b7
    • 5 MiP = b2 2 b5 5 b7
  • Ionian : 1 2 3 4 5 6 7
    • 4 MaP = 1 3 4 6 7
  • Harmonic Minor b2 : 1 b2 b3 4 5 b6 7
    • b2 MaP = 1 b2 4 5 b6
  • Ionian #6 : 1 2 3 4 5 b7 7
    • 7 AmP = 2 4 5 b7 7
  • Melodic Minor b5 : 1 2 b3 4 b5 6 7
    • b5 AmP = 1 2 4 b5 6
  • Harmonic Minor b5 : 1 2 b3 4 b5 b6 7
    • 1 AmP = 1 b3 b5 b6 7
  • Melodic Minor b4 : 1 2 b3 3 5 6 7
    • b3 AlP = 2 b3 5 6 7
  • Melodic Minor : 1 2 b3 4 5 6 7
    • b3 AlP = 2 b3 5 6 7
  • Melodic Minor #1 : 1 b2 2 3 b5 b6 b7
    • 2 AlP = b2 2 b5 b6 b7

The following scales contain two different pentatonics:

  • Ionian b5 : 1 2 3 4 b5 6 7
    • b5 AmP = 1 2 4 b5 6
    • 4 MaP = 1 3 4 6 7
  • Harmonic Minor : 1 2 b3 4 5 b6 7
    • b6 MiP = 2 b3 5 b6 7
    • b6 MaP = 1 2 b3 5 b6
  • Melodic Minor #6 : 1 2 b3 4 5 b7 7
    • 7 AmP = 2 4 5 b7 7
    • 7 AlP = b3 4 5 b7 7

The following contain three different pentatonics:

  • Harmonic Minor #3 : 1 2 3 4 5 b6 7
    • 4 MiP = 1 3 4 b6 7
    • b6 AmP = 2 3 5 b6 7
    • b6 AlP = 1 2 3 5 b6
  • Harmonic Minor #4 : 1 2 b3 b5 5 b6 7
    • 1 MiP = 1 b3 b5 5 7
    • 1 AmP = 1 b3 b5 b6 7
    • b6 MaP = 1 2 b3 5 b6
  • Ionian b2 : 1 b2 3 4 5 6 7
    • b2 AmP = 1 b2 3 5 6
    • 4 AlP = b2 3 4 6 7
    • 4 MaP = 1 3 4 6 7

And finally, the Mother Scale, which contains all four:

  • Harmonic Minor b4 : 1 2 b3 3 5 b6 7
    • b6 MiP = 2 b3 5 b6 7
    • b6 AmP = 2 3 5 b6 7
    • b6 AlP = 1 2 3 5 b6
    • b6 MaP = 1 2 b3 5 b6

With all four pentatonics positioned at the root this scale becomes Dhatuvardani.

Note that my code isn't yet sophisticated enough to identify when a heptatonic contains more than one copy of the same pentatonic. This is more of a classificatory effort at the moment.

More to come on all this; sorry if it's not your thing, I'm sure I'll get diverted onto something else in due course. And like I said, I'm not proud of the code below but in case it helps someone, here it is...


##########################################################
# This script finds all heptatonc scales
# that contain at least one of the four chosen
# pentatonics and classifies them.
##########################################################
import string
import copy

#############################
# Some commonly-used constants
#############################
noteNames = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
noteNamesSharp = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
noteNums = ["1", "b2", "2", "b3", "3", "4", "b5", "5", "b6", "6", "b7", "7"]
noteNumsSharp = ["1", "#1", "2", "#2", "3", "4", "#4", "5", "#5", "6", "#6", "7"]

#############################
# NOTE class
#############################
class Note:
    p = 0
    def __init__(self, pitch):
        self.p = pitch
    def update(self, num):
        self.p = num % 12
    def transpose(self, num):
        self.update(self.p + num)
    def getName(self):
        return noteNames[self.p]
    def getNum(self):
        return noteNums[self.p]
    def __str__(self):
        return noteNames[self.p]
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        return False
    def __ne__(self, other):
        return not self.__eq__(other)
    def __cmp__(self, other):
        return cmp(self.p, other.p)
    def __le__(self, other):
        return self.p <= other.p
    def __lt__(self, other):
        return self.p < other.p
    def __ge__(self, other):
        return self.p >= other.p
    def __gt__(self, other):
        return self.p > other.p
    
#############################
# SCALE class
#############################
class Scale:
    notes = []
    idx = 0
    def __init__(self, notes):
        self.notes = notes
        self.trimAndSort()
    def addNote(self, n):
        test= True
        for t in self.notes:
            test = (test and not t == n)
        if test:
            self.notes.insert(len(self.notes), Note(n))
        self.trimAndSort()
    def shift(self, amount):
        for n in self.notes:
            n.transpose(amount)
        self.trimAndSort()
    def tranposeNote(self, index, amount):
        self.notes[index].transpose(amount)
        self.trimAndSort()
    def numNotes(self):
        return len(self.notes)
    def isSubsetOf(self, scale):
        for n in self.notes:
            if n not in scale.notes:
                return False
        return True
    def isSupersetOf(self, scale):
        return scale.isSubsetOf(self)
    def isSubsetModRotationOf(self, scale):
        for i in range(0, 12):
            tmp = copy.deepcopy(scale)
            tmp.shift(i)
            if self.isSubsetOf(tmp):
                return i
        return -1
    def trimAndSort(self):
        self.notes.sort()
        prevN = -1
        for n in self.notes:
            if n == prevN:
                self.notes.remove(n)
            prevN = n
    def normalize(self):
        root = Note(0)
        while root not in self.notes:
            self.shift(-1)
    def binaryRepresentation(self):
        str = ""
        for i in range(0, 12):
            if Note(i) in self.notes:
                str = str + "1"
            else:
                str = str + "0"
        return str
    def isMode(self, other):
        return other.binaryRepresentation() in self.binaryRepresentation() + self.binaryRepresentation()
    def __str__(self):
        self.trimAndSort()
        return " ".join(p.getNum() for p in self.notes)
    def __len__(self):
        return len(self.notes)
    def __iter__(self):
        self.idx = -1
        return self
    def __next__(self):
        self.idx = self.idx + 1
        if self.idx < len(self.notes):
            return self.notes[self.idx]
        else:
            raise StopIteration

#############################
# Convenience functions for wrking with scales
#############################

def combineScales(sc1, sc2):
    scFinal = Scale([])
    for n in sc1:
        scFinal.addNote(n.p)
    for n in sc2:
        scFinal.addNote(n.p)
    return scFinal

def modeExistsInDict(scale, dictOfScales):
    for k in dictOfScales.keys():
        if scale.isMode(dictOfScales[k]):
            return True
    return False

#############################
# Definitions of the scales of interest.
#############################
SpecialScales = {
    "MiP": Scale([Note(0), Note(3), Note(6), Note(7), Note(11)]),
    "MaP": Scale([Note(0), Note(4), Note(6), Note(7), Note(11)]),
    "AlP": Scale([Note(0), Note(4), Note(6), Note(8), Note(11)]),
    "AmP": Scale([Note(0), Note(3), Note(6), Note(8), Note(11)])
    }

BaseScales = {
    "Ionian": Scale([Note(0), Note(2), Note(4), Note(5), Note(7), Note(9), Note(11)]),
    "Harmonic Minor": Scale([Note(0), Note(2), Note(3), Note(5), Note(7), Note(8), Note(11)]),
    "Melodic Minor": Scale([Note(0), Note(2), Note(3), Note(5), Note(7), Note(9), Note(11)])
    }


#############################
# For each base scale, construct the set of all altered-note scales
#############################

AlteredScales = copy.deepcopy(BaseScales)
for key in BaseScales.keys():
    currScale = BaseScales.get(key)
    for i in range(0, len(currScale)):
        # alter this note up
        s = copy.deepcopy(currScale)
        s.tranposeNote(i, 1)
        s.normalize()
        # if result is heptatonic, store it
        if(len(s) == 7 and not modeExistsInDict(s, AlteredScales)):
            newKey = key + " #" + str(i+1)
            AlteredScales[newKey] = s
        # repeat, altering the note down
        s = copy.deepcopy(currScale)
        s.tranposeNote(i, -1)
        s.normalize()
        # if result is heptatonic, store it
        if(len(s) == 7 and not modeExistsInDict(s, AlteredScales)):
            newKey = key + " b" + str(i+1)
            AlteredScales[newKey] = s

AlteredScaleSubsets = {}

for k in AlteredScales.keys():
    AlteredScales[k].numSpecialSubsets = 0
    AlteredScaleSubsets[k] = []
    for ks in SpecialScales.keys():
        pos = SpecialScales[ks].isSubsetModRotationOf(AlteredScales[k])
        if -1 != pos:
            AlteredScales[k].numSpecialSubsets = AlteredScales[k].numSpecialSubsets + 1
            tmp = copy.deepcopy(SpecialScales[ks])
            tmp.shift(12-pos)
            tmp.offset = (12 - pos)%12
            tmp.name = ks
            AlteredScaleSubsets[k].append(tmp)
            

for i in range(0, 5):
    for k in AlteredScales.keys():
        if AlteredScales[k].numSpecialSubsets == i:
            print(k, "  :  ", AlteredScales[k], "       ", AlteredScales[k].binaryRepresentation())
            for s in AlteredScaleSubsets[k]:
                print("         ", noteNums[s.offset], s.name, "=", s)