
    3j                    P   d dl mZ d dlmZ d dlZd dlZd dlZd dlmZ d dl	m
Z
 d dl	mZ d dl	mZ d dlmZ  G d	 d
ej                        Z G d de      Z G d dej$                        Z G d d      Z G d dej*                        Zedk(  rd dl	Z	 e	j0                  e       yy)    )annotations)CounterN)cast)base)exceptions21)metadata)hasherc                      e Zd Zy)AlignerExceptionN__name__
__module____qualname__     K/DATA/.local/lib/python3.12/site-packages/music21/alpha/analysis/aligner.pyr   r          r   r   c                      e Zd Zy)AlignmentTracebackExceptionNr   r   r   r   r   r      r   r   r   c                  0    e Zd ZdZdZdZdZdZed        Z	y)	ChangeOpsa  
    >>> ins = alpha.analysis.aligner.ChangeOps.Insertion
    >>> ins.color
    'green'

    >>> deletion = alpha.analysis.aligner.ChangeOps.Deletion
    >>> deletion.color
    'red'

    >>> subs = alpha.analysis.aligner.ChangeOps.Substitution
    >>> subs.color
    'purple'

    >>> noChange = alpha.analysis.aligner.ChangeOps.NoChange
    >>> noChange.color is None
    True
    r            c                .    dddd d}|| j                      S )Ngreenredpurple)r   r   r   r   )value)self	colorDicts     r   colorzChangeOps.color8   s    Eh4@	$$r   N)
r   r   r   __doc__	InsertionDeletionSubstitutionNoChangepropertyr"   r   r   r   r   r   !   s/    " IHLH% %r   r   c                  n    e Zd ZdZddZd Zd Zd Zd Zd Z	d	 Z
d
 Zd Zd Zd Zd Zd Zd ZddZy)StreamAligneraa  
    Stream Aligner is a dumb object that takes in two streams and forces them to align
    without any thought to any external variables

    These terms are associated with the Target stream are:
    - n, the number of rows in the distance matrix, the left-most column of the matrix
    - i, the index into rows in the distance matrix
    - the first element of tuple

    These terms are associated with the Source stream are:
    - m, the number of columns in the distance matrix, the top-most row of the matrix
    - j, the index into columns in the distance matrix
    - the second element of tuple
    Nc                    || _         || _        d | _        || j                         }|| _        || _        g | _        d| _        d| _        d| _	        d | _
        d | _        d | _        y )Nr   )targetStreamsourceStreamdistanceMatrixgetDefaultHasherr	   	preHashedchangessimilarityScorenmhashedTargetStreamhashedSourceStreamchangesCount)r    r,   r-   hasher_funcr0   s        r   __init__zStreamAligner.__init__N   ss    (("//1K!"  "&"& r   c                J    t        j                         }d|_        d|_        |S )a  
        returns a default hasher.Hasher object
        that does not hashOffset or include the reference.

        called by __init__ if no hasher is passed in.

        >>> sa = alpha.analysis.aligner.StreamAligner()
        >>> h = sa.getDefaultHasher()
        >>> h
        <music21.alpha.analysis.hasher.Hasher object at 0x1068cf6a0>
        >>> h.hashOffset
        False
        >>> h.includeReference
        True
        FT)r	   Hasher
hashOffsetincludeReference)r    hs     r   r/   zStreamAligner.getDefaultHashere   s#    " MMO!r   c                    | j                          | j                          | j                          | j                          y )N)makeHashedStreamssetupDistanceMatrixpopulateDistanceMatrixcalculateChangesListr    s    r   alignzStreamAligner.align{   s2       "##%!!#r   c                
   | j                   sU| j                  j                  | j                        | _        | j                  j                  | j
                        | _        y| j                  | _        | j
                  | _        y)a)  
        Hashes streams if not pre-hashed

        >>> tStream = stream.Stream()
        >>> sStream = stream.Stream()

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        >>> tStream.append([note1, note2])
        >>> sStream.append([note3, note4])
        >>> sa1 = alpha.analysis.aligner.StreamAligner(tStream, sStream)

        >>> h = alpha.analysis.hasher.Hasher()
        >>> h.includeReference = True

        >>> toBeHashedTarStream = stream.Stream()
        >>> toBeHashedSouStream = stream.Stream()

        >>> note5 = note.Note('A4')
        >>> note6 = note.Note('B4')
        >>> note7 = note.Note('A4')
        >>> note8 = note.Note('G4')

        >>> toBeHashedTarStream.append([note5, note6])
        >>> toBeHashedSouStream.append([note7, note8])
        >>> hashedTarStr = h.hashStream(toBeHashedTarStream)
        >>> hashedSouStr = h.hashStream(toBeHashedSouStream)
        >>> sa2 = alpha.analysis.aligner.StreamAligner(
        ...             hashedTarStr, hashedSouStr, preHashed=True)

        >>> sa2.makeHashedStreams()
        >>> sa1.makeHashedStreams()

        >>> sa1.hashedTargetStream
        [NoteHashWithReference(Pitch=60, Duration=1.0),
         NoteHashWithReference(Pitch=62, Duration=1.0)]
        >>> sa1.hashedSourceStream
        [NoteHashWithReference(Pitch=60, Duration=1.0),
         NoteHashWithReference(Pitch=64, Duration=1.0)]

        >>> sa2.hashedTargetStream
        [NoteHashWithReference(Pitch=69, Duration=1.0, Offset=0.0),
         NoteHashWithReference(Pitch=71, Duration=1.0, Offset=1.0)]
        >>> sa2.hashedSourceStream
        [NoteHashWithReference(Pitch=69, Duration=1.0, Offset=0.0),
         NoteHashWithReference(Pitch=67, Duration=1.0, Offset=1.0)]

        N)r0   r	   
hashStreamr,   r5   r-   r6   rD   s    r   r@   zStreamAligner.makeHashedStreams   sd    h ~~&*kk&<&<T=N=N&OD#&*kk&<&<T=N=N&OD# '+&7&7D#&*&7&7D#r   c                   | j                   s| j                          t        | j                         | _        t        | j                        | _        | j                  dk(  rt        d      | j
                  dk(  rt        d      dt        j                  v rt        d      ddl	}|j                  | j                  dz   | j
                  dz   ft              | _        y)	a  
        Creates a distance matrix of the right size after hashing

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        Test for streams of length 3 and 4

        >>> target0 = converter.parse('tinyNotation: C4 D C E')
        >>> source0 = converter.parse('tinyNotation: C4 D C')

        >>> sa0 = alpha.analysis.aligner.StreamAligner(target0, source0)
        >>> sa0.setupDistanceMatrix()
        >>> sa0.distanceMatrix.size
        20
        >>> sa0.distanceMatrix.shape
        (5, 4)

        Test for empty target stream

        >>> target1 = stream.Stream()
        >>> source1 = stream.Stream()
        >>> source1.append(note1)
        >>> sa1 = alpha.analysis.aligner.StreamAligner(target1, source1)
        >>> sa1.makeHashedStreams()
        >>> sa1.setupDistanceMatrix()
        Traceback (most recent call last):
        music21.alpha.analysis.aligner.AlignerException:
        Cannot perform alignment with empty target stream.

        Test for empty source stream

        >>> target2 = stream.Stream()
        >>> source2 = stream.Stream()
        >>> target2.append(note3)
        >>> sa2 = alpha.analysis.aligner.StreamAligner(target2, source2)
        >>> sa2.makeHashedStreams()
        >>> sa2.setupDistanceMatrix()
        Traceback (most recent call last):
        music21.alpha.analysis.aligner.AlignerException:
            Cannot perform alignment with empty source stream.

        r   z2Cannot perform alignment with empty target stream.z2Cannot perform alignment with empty source stream.numpyz!Cannot run Aligner without numpy.Nr   )dtype)r5   r@   lenr3   r6   r4   r   r   _missingImportrI   zerosintr.   )r    nps     r   rA   z!StreamAligner.setupDistanceMatrix   s    \ &&""$T,,-T,,-66Q;"#WXX66Q;"#WXX d)))"#FGG hh
DFFQJ'?shKr   c                :   | j                  | j                  d         }| j                  | j                  d         }t        d| j                  dz         D ]*  }| j
                  |dz
     d   |z   | j
                  |   d<   , t        d| j                  dz         D ]*  }| j
                  d   |dz
     |z   | j
                  d   |<   , t        d| j                  dz         D ]  }t        d| j                  dz         D ]  }| j                  | j                  |dz
     | j                  |dz
           }| j
                  |dz
     |   |z   | j
                  |   |dz
     |z   | j
                  |dz
     |dz
     |z   g}t        |      | j
                  |   |<     y)a  
        Sets up the distance matrix for back-tracing

        >>> note1 = note.Note('C#4')
        >>> note2 = note.Note('C4')

        Test 1: similar streams

        >>> targetA = stream.Stream()
        >>> sourceA = stream.Stream()
        >>> targetA.append([note1, note2])
        >>> sourceA.append([note1, note2])
        >>> saA = alpha.analysis.aligner.StreamAligner(targetA, sourceA)
        >>> saA.makeHashedStreams()
        >>> saA.setupDistanceMatrix()
        >>> saA.populateDistanceMatrix()
        >>> saA.distanceMatrix
        array([[0, 2, 4],
               [2, 0, 2],
               [4, 2, 0]])

        Second Test

        >>> targetB = stream.Stream()
        >>> sourceB = stream.Stream()
        >>> targetB.append([note1, note2])
        >>> sourceB.append(note1)
        >>> saB = alpha.analysis.aligner.StreamAligner(targetB, sourceB)
        >>> saB.makeHashedStreams()
        >>> saB.setupDistanceMatrix()
        >>> saB.populateDistanceMatrix()
        >>> saB.distanceMatrix
        array([[0, 2],
               [2, 0],
               [4, 2]])

        Third Test

        >>> note3 = note.Note('D5')
        >>> note3.quarterLength = 3
        >>> note4 = note.Note('E3')
        >>> targetC = stream.Stream()
        >>> sourceC = stream.Stream()
        >>> targetC.append([note1, note2, note4])
        >>> sourceC.append([note3, note1, note4])
        >>> saC = alpha.analysis.aligner.StreamAligner(targetC, sourceC)
        >>> saC.makeHashedStreams()
        >>> saC.setupDistanceMatrix()
        >>> saC.populateDistanceMatrix()
        >>> saC.distanceMatrix
        array([[0, 2, 4, 6],
           [2, 2, 2, 4],
           [4, 4, 3, 3],
           [6, 6, 5, 3]])

        r   r   N)

insertCostr6   
deleteCostranger3   r.   r4   substitutionCostr5   min)r    rQ   rR   ij	substCostpreviousValuess          r   rB   z$StreamAligner.populateDistanceMatrix   s   v __T%<%<Q%?@
__T%<%<Q%?@
 q$&&1*%A(,(;(;AE(B1(E
(RD"1% & q$&&1*%A(,(;(;A(>q1u(E
(RD"1% & q$&&1*%A1dffqj) 11$2I2I!a%2P262I2I!a%2PR	 #'"5"5a!e"<Q"?*"L"&"5"5a"8Q"?*"L"&"5"5a!e"<QU"Ci"O"Q -0,?##A&q) * &r   c                    |dk\  rt        | j                  |dz
     |         nd}|dk\  rt        | j                  |   |dz
           nd}|dk\  r&|dk\  r!t        | j                  |dz
     |dz
           nd}|||g}|S )a  
        i and j are current row and column indices in self.distanceMatrix
        returns all possible moves (0 up to 3)
        vertical, horizontal, diagonal costs of adjacent entries in self.distMatrix

        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        >>> target.append([note1, note2, note3, note4])
        >>> source.append([note1, note2, note3])
        >>> sa = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa.makeHashedStreams()
        >>> sa.setupDistanceMatrix()
        >>> for i in range(4+1):
        ...     for j in range(3+1):
        ...         sa.distanceMatrix[i][j] = i * j

        >>> sa.distanceMatrix
        array([[ 0,  0,  0,  0],
               [ 0,  1,  2,  3],
               [ 0,  2,  4,  6],
               [ 0,  3,  6,  9],
               [ 0,  4,  8, 12]])

        >>> sa.getPossibleMovesFromLocation(0, 0)
        [None, None, None]

        >>> sa.getPossibleMovesFromLocation(1, 1)
        [0, 0, 0]

        >>> sa.getPossibleMovesFromLocation(4, 3)
        [9, 8, 6]

        >>> sa.getPossibleMovesFromLocation(2, 2)
        [2, 2, 1]

        >>> sa.getPossibleMovesFromLocation(0, 2)
        [None, 0, None]

        >>> sa.getPossibleMovesFromLocation(3, 0)
        [0, None, None]

        r   N)rN   r.   )r    rV   rW   verticalCosthorizontalCostdiagonalCostpossibleMovess          r   getPossibleMovesFromLocationz*StreamAligner.getPossibleMovesFromLocationP  s    d >?!Vs4..q1u5a89?@AvT003AE:;4BCq&QRSVs4..q1u5a!e<=Z^%~|Dr   c                V   | j                  ||      }|d    |d   t        d      t        j                  S |d   t        j                  S | j
                  |   |   }t        t        |      t        j                  d            \  }}||k(  rt        j                  S t        |      S )a  
        Insert, Delete, Substitution, No Change = range(4)

        return the direction that traceback moves
        0: vertical movement, insertion
        1: horizontal movement, deletion
        2: diagonal movement, substitution
        3: diagonal movement, no change

        raises a ValueError if i == 0 and j == 0.
        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        >>> target.append([note1, note2, note3, note4])
        >>> source.append([note1, note2, note3])

        >>> sa = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa.makeHashedStreams()
        >>> sa.setupDistanceMatrix()
        >>> sa.populateDistanceMatrix()
        >>> sa.distanceMatrix
        array([[0, 2, 4, 6],
               [2, 0, 2, 4],
               [4, 2, 0, 2],
               [6, 4, 2, 0],
               [8, 6, 4, 2]])

        >>> sa.getOpFromLocation(4, 3)
        <ChangeOps.Insertion: 0>

        >>> sa.getOpFromLocation(2, 2)
        <ChangeOps.NoChange: 3>

        >>> sa.getOpFromLocation(0, 2)
        <ChangeOps.Deletion: 1>

        >>> sa.distanceMatrix[0][0] = 1
        >>> sa.distanceMatrix
        array([[1, 2, 4, 6],
               [2, 0, 2, 4],
               [4, 2, 0, 2],
               [6, 4, 2, 0],
               [8, 6, 4, 2]])

        >>> sa.getOpFromLocation(1, 1)
        <ChangeOps.Substitution: 2>

        >>> sa.getOpFromLocation(0, 0)
        Traceback (most recent call last):
        ValueError: No movement possible from the origin
        r   r   z$No movement possible from the origin)key)r_   
ValueErrorr   r%   r$   r.   rU   	enumerateoperator
itemgetterr'   )r    rV   rW   r^   currentCostminIndex
minNewCosts          r   getOpFromLocationzStreamAligner.getOpFromLocation  s    r 99!Q?#Q' !GHH%%%1%&&&))!,Q/"9]#;ATATUVAWX**$%%%X&&r   c                0    t        |j                        }|S )a  
        Cost of inserting an extra hashed item.
        For now, it's just the size of the keys of the NoteHashWithReference

        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        >>> target.append([note1, note2, note3, note4])
        >>> source.append([note1, note2, note3])

        This is a StreamAligner with default hasher settings

        >>> sa0 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa0.align()
        >>> tup0 = sa0.hashedTargetStream[0]
        >>> sa0.insertCost(tup0)
        2

        This is a StreamAligner with a modified hasher that doesn't hash pitch at all

        >>> sa1 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa1.hasher.hashPitch = False
        >>> sa1.align()
        >>> tup1 = sa1.hashedTargetStream[0]
        >>> sa1.insertCost(tup1)
        1

        This is a StreamAligner with a modified hasher that hashes 3 additional properties

        >>> sa2 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa2.hasher.hashOctave = True
        >>> sa2.hasher.hashIntervalFromLastNote = True
        >>> sa2.hasher.hashIsAccidental = True
        >>> sa2.align()
        >>> tup2 = sa2.hashedTargetStream[0]
        >>> sa2.insertCost(tup2)
        5
        rK   hashItemsKeysr    tupkeyDictSizes      r   rQ   zStreamAligner.insertCost      X #++,r   c                0    t        |j                        }|S )a  
        Cost of deleting an extra hashed item.
        For now, it's just the size of the keys of the NoteHashWithReference

        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('C4')
        >>> note2 = note.Note('D4')
        >>> note3 = note.Note('C4')
        >>> note4 = note.Note('E4')

        >>> target.append([note1, note2, note3, note4])
        >>> source.append([note1, note2, note3])

        This is a StreamAligner with default hasher settings

        >>> sa0 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa0.align()
        >>> tup0 = sa0.hashedSourceStream[0]
        >>> sa0.deleteCost(tup0)
        2

        This is a StreamAligner with a modified hasher that doesn't hash pitch at all

        >>> sa1 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa1.hasher.hashPitch = False
        >>> sa1.align()
        >>> tup1 = sa1.hashedSourceStream[0]
        >>> sa1.deleteCost(tup1)
        1

        This is a StreamAligner with a modified hasher that hashes 3 additional properties

        >>> sa2 = alpha.analysis.aligner.StreamAligner(target, source)
        >>> sa2.hasher.hashOctave = True
        >>> sa2.hasher.hashIntervalFromLastNote = True
        >>> sa2.hasher.hashIsAccidental = True
        >>> sa2.align()
        >>> tup2 = sa2.hashedSourceStream[0]
        >>> sa2.deleteCost(tup2)
        5
        rk   rm   s      r   rR   zStreamAligner.deleteCost  rp   r   c                    | j                  ||      ryt        |j                        }| j                  ||      }||z  }|S )a0
  
        Finds the cost of substituting the targetTup with the sourceTup.
        For now, it's just an interpolation of how many things they have in common

        Example: equality testing, both streams made from same note
        targetA will not have the same reference as sourceA
        but their hashes will be equal, which makes for their hashed objects to be
        able to be equal.

        >>> note1 = note.Note('C4')
        >>> targetA = stream.Stream()
        >>> sourceA = stream.Stream()
        >>> targetA.append(note1)
        >>> sourceA.append(note1)
        >>> targetA == sourceA
        False

        >>> saA = alpha.analysis.aligner.StreamAligner(targetA, sourceA)
        >>> saA.align()
        >>> hashedItem1A = saA.hashedTargetStream[0]
        >>> hashedItem2A = saA.hashedSourceStream[0]
        >>> print(hashedItem1A)
        NoteHashWithReference(Pitch=60, Duration=1.0)

        >>> print(hashedItem2A)
        NoteHashWithReference(Pitch=60, Duration=1.0)

        >>> saA.tupleEqualityWithoutReference(hashedItem1A, hashedItem2A)
        True
        >>> saA.substitutionCost(hashedItem1A, hashedItem2A)
        0

        >>> note2 = note.Note('D4')
        >>> targetB = stream.Stream()
        >>> sourceB = stream.Stream()
        >>> targetB.append(note1)
        >>> sourceB.append(note2)
        >>> saB = alpha.analysis.aligner.StreamAligner(targetB, sourceB)
        >>> saB.align()
        >>> hashedItem1B = saB.hashedTargetStream[0]
        >>> hashedItem2B = saB.hashedSourceStream[0]

        hashed items only differ in 1 spot

        >>> print(hashedItem1B)
        NoteHashWithReference(Pitch=60, Duration=1.0)

        >>> print(hashedItem2B)
        NoteHashWithReference(Pitch=62, Duration=1.0)

        >>> saB.substitutionCost(hashedItem1B, hashedItem2B)
        1

        >>> note3 = note.Note('E4')
        >>> note4 = note.Note('E#4')
        >>> note4.duration = duration.Duration('half')
        >>> targetC = stream.Stream()
        >>> sourceC = stream.Stream()
        >>> targetC.append(note3)
        >>> sourceC.append(note4)
        >>> saC = alpha.analysis.aligner.StreamAligner(targetC, sourceC)
        >>> saC.align()
        >>> hashedItem1C = saC.hashedTargetStream[0]
        >>> hashedItem2C = saC.hashedSourceStream[0]

        hashed items should differ in 2 spots

        >>> print(hashedItem1C)
        NoteHashWithReference(Pitch=64, Duration=1.0)

        >>> print(hashedItem2C)
        NoteHashWithReference(Pitch=65, Duration=2.0)

        >>> saC.substitutionCost(hashedItem1C, hashedItem2C)
        2
        r   )tupleEqualityWithoutReferencerK   rl   calculateNumSimilarities)r    	targetTup	sourceTuptotalPossibleDifferencesnumSimilaritiesInTuples        r   rT   zStreamAligner.substitutionCost0  sN    Z --iC#&y'>'>#? !%!>!>y)!T $:: ''r   c                j    d}|j                   D ]!  }t        ||      t        ||      k(  s|dz  }# |S )a  
        Returns the number of attributes that two tuples have that are the same

        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('D1')
        >>> target.append([note1])
        >>> source.append([note1])
        >>> sa = alpha.analysis.aligner.StreamAligner(target, source)

        >>> from collections import namedtuple
        >>> NoteHash = namedtuple('NoteHash', ['Pitch', 'Duration'])
        >>> nh1 = NoteHash(60, 4)
        >>> nhwr1 = alpha.analysis.hasher.NoteHashWithReference(nh1)
        >>> nhwr1.reference = note.Note('C4')
        >>> nhwr1
        NoteHashWithReference(Pitch=60, Duration=4)

        >>> nh2 = NoteHash(60, 4)
        >>> nhwr2 = alpha.analysis.hasher.NoteHashWithReference(nh2)
        >>> nhwr2.reference = note.Note('C4')
        >>> nhwr2
        NoteHashWithReference(Pitch=60, Duration=4)

        >>> sa.calculateNumSimilarities(nhwr1, nhwr2)
        2

        >>> nh3 = NoteHash(61, 4)
        >>> nhwr3 = alpha.analysis.hasher.NoteHashWithReference(nh3)
        >>> nhwr3.reference = note.Note('C#4')
        >>> nhwr3
        NoteHashWithReference(Pitch=61, Duration=4)

        >>> sa.calculateNumSimilarities(nhwr1, nhwr3)
        1

        >>> nh4 = NoteHash(59, 1)
        >>> nhwr4 = alpha.analysis.hasher.NoteHashWithReference(nh4)
        >>> nhwr4.reference = note.Note('B3')
        >>> nhwr4
        NoteHashWithReference(Pitch=59, Duration=1)

        >>> sa.calculateNumSimilarities(nhwr2, nhwr4)
        0
        r   r   rl   getattr)r    ru   rv   countvals        r   rt   z&StreamAligner.calculateNumSimilarities  s@    ` **Cy#&')S*AA
 + r   c                \    |j                   D ]  }t        ||      t        ||      k7  s y y)aA  
        Returns whether two hashed items have the same attributes,
        even though their references are different?

        >>> target = stream.Stream()
        >>> source = stream.Stream()

        >>> note1 = note.Note('D1')
        >>> target.append([note1])
        >>> source.append([note1])
        >>> sa = alpha.analysis.aligner.StreamAligner(target, source)

        >>> from collections import namedtuple
        >>> NoteHash = namedtuple('NoteHash', ['Pitch', 'Duration'])
        >>> nh1 = NoteHash(60, 4)
        >>> nhwr1 = alpha.analysis.hasher.NoteHashWithReference(nh1)
        >>> nhwr1.reference = note.Note('C4')
        >>> nhwr1
        NoteHashWithReference(Pitch=60, Duration=4)

        >>> nh2 = NoteHash(60, 4)
        >>> nhwr2 = alpha.analysis.hasher.NoteHashWithReference(nh2)
        >>> nhwr2.reference = note.Note('B#3')
        >>> nhwr2
        NoteHashWithReference(Pitch=60, Duration=4)

        >>> sa.tupleEqualityWithoutReference(nhwr1, nhwr2)
        True

        This is a very difference has

        >>> nh3 = NoteHash(61, 4)
        >>> nhwr3 = alpha.analysis.hasher.NoteHashWithReference(nh3)
        >>> nhwr3.reference = note.Note('C#4')
        >>> nhwr3
        NoteHashWithReference(Pitch=61, Duration=4)

        >>> sa.tupleEqualityWithoutReference(nhwr1, nhwr3)
        False

        FTrz   )r    tup1tup2r}   s       r   rs   z+StreamAligner.tupleEqualityWithoutReference  s3    T %%CtS!WT3%77 & r   c                   | j                   }| j                  }|dk7  s|dk7  r| j                  ||      }| j                  |dz
     j                  }| j
                  |dz
     j                  }|||f}| j                  j                  d|       |t        j                  k(  r|dz  }nA|t        j                  k(  r|dz  }n(|t        j                  k(  r|dz  }|dz  }n
|dz  }|dz  }|dk7  r|dk7  r|dk7  r|dk7  rt        d      t        d | j                  D              | _        | j                  t        j                     t!        | j                        z  | _        y)a  
        Traverses through self.distanceMatrix from the bottom right corner to top left looking at
        bestOp at every move to determine which change was most likely at any point. Compiles
        the list of changes in self.changes. Also calculates some metrics like self.similarityScore
        and self.changesCount.

        >>> note1 = note.Note('C#4')
        >>> note2 = note.Note('C4')

        test 1: one insertion, one no change. Target stream has one more note than
        source stream, so source stream needs an insertion to match target stream.
        should be 0.5 similarity between the two

        >>> targetA = stream.Stream()
        >>> sourceA = stream.Stream()
        >>> targetA.append([note1, note2])
        >>> sourceA.append(note1)
        >>> saA = alpha.analysis.aligner.StreamAligner(targetA, sourceA)
        >>> saA.makeHashedStreams()
        >>> saA.setupDistanceMatrix()
        >>> saA.populateDistanceMatrix()
        >>> saA.calculateChangesList()
        >>> saA.changesCount[alpha.analysis.aligner.ChangeOps.Insertion]
        1
        >>> saA.changesCount[alpha.analysis.aligner.ChangeOps.NoChange]
        1
        >>> saA.similarityScore
        0.5

        test 2: one deletion, one no change. Target stream has one fewer note than
        source stream, so source stream needs a deletion to match target stream.
        should be 0.5 similarity between the two

        >>> targetB = stream.Stream()
        >>> sourceB = stream.Stream()
        >>> targetB.append(note1)
        >>> sourceB.append([note1, note2])
        >>> saB = alpha.analysis.aligner.StreamAligner(targetB, sourceB)
        >>> saB.makeHashedStreams()
        >>> saB.setupDistanceMatrix()
        >>> saB.populateDistanceMatrix()
        >>> saB.calculateChangesList()
        >>> saB.changesCount[alpha.analysis.aligner.ChangeOps.Deletion]
        1
        >>> saB.changesCount[alpha.analysis.aligner.ChangeOps.NoChange]
        1
        >>> saB.similarityScore
        0.5

        test 3: no changes

        >>> targetC = stream.Stream()
        >>> sourceC = stream.Stream()
        >>> targetC.append([note1, note2])
        >>> sourceC.append([note1, note2])
        >>> saC = alpha.analysis.aligner.StreamAligner(targetC, sourceC)
        >>> saC.makeHashedStreams()
        >>> saC.setupDistanceMatrix()
        >>> saC.populateDistanceMatrix()
        >>> saC.calculateChangesList()
        >>> saC.changesCount[alpha.analysis.aligner.ChangeOps.NoChange]
        2
        >>> saC.similarityScore
        1.0

        test 4: 1 no change, 1 substitution

        >>> targetD = stream.Stream()
        >>> sourceD = stream.Stream()
        >>> note3 = note.Note('C4')
        >>> note3.quarterLength = 2  # same pitch and offset as note2
        >>> targetD.append([note1, note2])
        >>> sourceD.append([note1, note3])
        >>> saD = alpha.analysis.aligner.StreamAligner(targetD, sourceD)
        >>> saD.makeHashedStreams()
        >>> saD.setupDistanceMatrix()
        >>> saD.populateDistanceMatrix()
        >>> saD.calculateChangesList()
        >>> saD.changesCount[alpha.analysis.aligner.ChangeOps.Substitution]
        1
        >>> saD.changesCount[alpha.analysis.aligner.ChangeOps.NoChange]
        1
        >>> saD.similarityScore
        0.5

        r   r   z0Traceback of best alignment did not end properlyc              3  &   K   | ]	  }|d      yw)r   Nr   ).0elems     r   	<genexpr>z5StreamAligner.calculateChangesList.<locals>.<genexpr>_  s     #EDGs   N)r3   r4   ri   r5   	referencer6   r1   insertr   r$   r%   r&   r   r   r7   r'   rK   r2   )r    rV   rW   bestOptargetStreamReferencesourceStreamReferenceopTuples          r   rC   z"StreamAligner.calculateChangesList  s[   n FFFF1fQ ++Aq1F$($;$;AE$B$L$L!$($;$;AE$B$L$L!,.CVLGLL7+
 ,,,Q9---Q9111QQ QQ1 1fQ2 6a1f-.`aa##E#EE#001C1CDs4<<GXXr   c                   t        | j                        D ]u  \  }\  }}}|t        j                  k(  r|j                  |j
                  _        |j                  |       |j                  |j
                  _        |j                  |       w t        j                         | j                  _        t        j                         | j                  _        dt        | j                  j                        z   | j                  j                  _        dt        | j                  j                        z   | j                  j                  _        | j                  j                  j                  | j                  j                  _        | j                  j                  j                  | j                  j                  _        |r5| j                  j                          | j                  j                          yy)z
        Visual and debugging feature to display which notes are changed.
        Will open in MuseScore, unless show is set to False
        zTarget zSource N)rc   r1   r   r'   r"   styleaddLyricr   Metadatar,   r-   stridtitlemovementNameshow)r    r   idxmidiNoteRef
omrNoteRefchanges         r   showChangeszStreamAligner.showChangesb  sr   
 9B$,,8O4S3;
F+++*0,,!!'$$S))/
  &##C( 9P &.%6%6%8"%-%6%6%8"+4s4;L;L;O;O7P+P""(+4s4;L;L;O;O7P+P""(262C2C2L2L2R2R""/262C2C2L2L2R2R""/""$""$ r   )NNNF)F)r   r   r   r#   r9   r/   rE   r@   rA   rB   r_   ri   rQ   rR   rT   rt   rs   rC   r   r   r   r   r*   r*   >   sd    !.,$:8x?LBP@d7rG'R-^-^S(j4l-^vYp%r   r*   c                  H    e Zd Zd Zd Zd Zd Zd Zd Zd Z	d Z
d	 Zd
 Zy)Testc                T   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j                  |       |j                  |       t        ||      }|j                          | j                  |j                  d       y)zI
        two streams of the same note should have 1.0 similarity
        r   streamnoteC4      ?N
music21r   r   StreamNoteappendr*   rE   assertEqualr2   r    r   r   targetsourcenote1note2sas           r   testSimpleStreamOneNotezTest.testSimpleStreamOneNote  s}     	# 		$		$ee66*

++S1r   c                b   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }d|_        |j                  |       |j                  |       t        ||      }|j                          | j                  |j                  d       y)	zO
        two streams of two different notes should have 0.0 similarity
        r   r   r   r   zC#4   g        N)r   r   r   r   r   quarterLengthr   r*   rE   r   r2   r   s           r    testSimpleStreamOneNoteDifferentz%Test.testSimpleStreamOneNoteDifferent  s     	# 		$		% ee66*

++S1r   c                   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j                  ||||g       |j                  ||||g       t        ||      }	|	j                          | j                  |	j                  d       y	)
zU
        two streams of the same notes should have 1.0 percentage similarity
        r   r   r   r   D4E4F4r   Nr   
r    r   r   r   r   r   r   note3note4r   s
             r   testSameSimpleStreamzTest.testSameSimpleStream  s     	# 		$		$		$		$ueUE23ueUE2366*

++S1r   c                   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j                  |||g       |j                  |||g       t        ||      }	|	j                          | j                  |	j                  d       y)	zN
        two streams of the 2/3 same notes should have 2/3 similarity
        r   r   r   r   zD#4zD-4gUUUUUU?Nr   r   s
             r   testSameSimpleStream2zTest.testSameSimpleStream2  s     	# 		$		% 		% 		$ueU+,ueU+,66*

++U3r   c                   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }	|j                  ||||g       |j                  ||||	g       t        ||      }
|
j                          | j                  |
j                  d	       y
)z_
        two streams with just 1 note different should have 0.75 percentage similarity
        r   r   r   r   r   r   r   G4      ?Nr   )r    r   r   r   r   r   r   r   r   note5r   s              r   testSameOneOffStreamzTest.testSameOneOffStream  s     	# 		$		$		$		$		$ueUE23ueUE2366*

++T2r   c                   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j                  ||||g       |j                  |||g       t        ||      }	|	j                          |	j                          | j                  |	j                  d       y	)
zv
        two streams, both the same, but one has an extra note should
        have 0.75 percentage similarity
        r   r   r   r   r   r   r   r   N)r   r   r   r   r   r   r*   rE   r   r   r2   r   s
             r   testOneOffDeletionStreamzTest.testOneOffDeletionStream  s    
 	# 		$		$		$		$ueUE23ueU+,66*


++T2r   c                6   ddl m} ddl m} |j                         }|j                         }|j	                  g d      }|j                  |       |j                  |       t        ||      }|j                          | j                  |j                  d       y)z6
        two streams, one with explicit chord
        r   r   )chord)E3r   r   r   N)
r   r   r   r   Chordr   r*   rE   r   r2   )r    r   r   r   r   cMajorr   s          r   testChordSimilarityStreamzTest.testChordSimilarityStream	  sr     	#!/0ff66*

++S1r   c                D   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }	|j	                  d      }
|j	                  d      }|j                  ||||g       |j                  ||	|
g       t        ||      }|j                          |j                          |j                  |j                  d   d   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       |j                  |j                  d   d	   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       y
)z
        Given two streams:

        MIDI is `C C C B`
        OMR is `C C C`

        Therefore, there needs to be an insertion to get from OMR to MIDI
        r   r   r   r   B3r   r   3r   Nr   r   r   r   r   r   r*   rE   r   getElementByIdr1   r   assertIsNotNoner   r   r   r"   lyricr    r   r   r   r   noteC1noteC2noteC3noteC4noteC5noteC6noteBr   n0n1s                  r   testShowInsertionzTest.testShowInsertion  s    	# 444444		$vvvu56vvv./66*


""2::a=#3#6#67R $))R 13'""2::a=#3#6#67R $))R 13'r   c                D   ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }	|j	                  d      }
|j	                  d      }|j                  |||g       |j                  ||	|
|g       t        ||      }|j                          |j                          |j                  |j                  d   d   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       |j                  |j                  d   d	   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       y
)z
        Given two streams:

        MIDI is `C C C`

        OMR is `C C C B`

        Therefore, there needs to be a deletion to get from OMR to MIDI.
        r   r   r   r   r   r   r   r   r   Nr   r   s                  r   testShowDeletionzTest.testShowDeletionE  s    	# 444444		$vvv./vvvu5666*


""2::a=#3#6#67R $))R /3'""2::a=#3#6#67R $))R /3'r   c                    ddl m} ddl m} |j                         }|j                         }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }|j	                  d      }	|j	                  d      }
|j                  |||g       |j                  ||	|
g       t        ||      }|j                          |j                          |j                  |j                  d   d   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       |j                  |j                  d   d	   j                        }| j                  |       t        |j                  |      }| j                  |j                  j                   d       | j                  |j"                  d       y
)z
        two streams:
        MIDI is `C C C`
        OMR is `C C B`

        Therefore, there needs to be a substitution to get from OMR to MIDI
        r   r   r   r   r   r   r   2r   Nr   )r    r   r   r   r   r   r   r   r   r   r   r   r   r   s                 r   testShowSubstitutionzTest.testShowSubstitutionp  s    	# 44444		$vvv./vvu-.66*


 ""2::a=#3#6#67R $))R 23'""2::a=#3#6#67R $))R 23'r   N)r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   ~  s6    2*2,2.4.30322$((T)(V'(r   r   __main__)
__future__r   collectionsr   enumrd   unittesttypingr   r   r   r   r   music21.alpha.analysisr	   Music21Exceptionr   r   IntEnumr   r*   TestCaser   r   mainTestr   r   r   <module>r      s    #          )	|44 		"2 	% %:}% }%@Y(8 Y(x zGT r   